/* DONA_SCULPTURE_V11 */ /* Dona Vagues Vocales V1 — Canvas 2D, amplitude PCM Piper envoyée par Socket.IO. Le mode orb conserve l'ancien canvas et son animation. Aucune capture micro et aucun audio client ne sont nécessaires. */ (() => { 'use strict'; if (window.DonaWaves) return; const KEY='dona_visual_voice_v1'; let choice='waves'; try {if (['waves','orb','sculpture'].includes(localStorage.getItem(KEY)))choice=localStorage.getItem(KEY);}catch (_) {} const API={isActive:()=>choice!=='orb',select:selectMode,getChoice:()=>choice}; window.DonaWaves=API; document.body.classList.toggle('dona-waves-selected', choice!=='orb'); document.body.classList.toggle('dona-sculpture-selected',choice==='sculpture'); function selectMode(next){ if(!['waves','orb','sculpture'].includes(next))return; choice=next; document.body.classList.toggle('dona-waves-selected',choice!=='orb'); document.body.classList.toggle('dona-sculpture-selected',choice==='sculpture'); try{localStorage.setItem(KEY,choice);}catch(_){} syncMenu(); } function syncMenu(){ const menu=document.getElementById('dv92ToolsMenu');if(!menu)return; for(const el of menu.querySelectorAll('[data-dona-visual]')){ const active=el.dataset.donaVisual===choice; el.setAttribute('aria-checked',String(active)); el.querySelector('.dona-waves-choice-icon').textContent=active?'◉':'○'; } } function addMenuControls(){ const menu=document.getElementById('dv92ToolsMenu'); if(!menu||menu.querySelector('#donaWavesMenuTitle'))return; const title=document.createElement('div');title.id='donaWavesMenuTitle'; title.className='dona-waves-menu-title';title.textContent='Animation centrale'; menu.append(title); for(const [mode,label] of [['waves','Vagues horizontales (Piper)'],['sculpture','Sculpture de vagues (Piper)'],['orb','Orbe d’origine']]){ const b=document.createElement('button');b.type='button'; b.className='dv92-menu-entry dona-waves-option';b.dataset.donaVisual=mode; b.setAttribute('role','menuitemradio'); const mark=document.createElement('span');mark.className='dona-waves-choice-icon'; b.append(mark,document.createTextNode(label)); b.addEventListener('click',()=>{ selectMode(mode); menu.hidden=true; document.getElementById('dv92Hamburger')?.setAttribute('aria-expanded','false'); }); menu.append(b); } syncMenu(); } let started=false; function init(){ if(started)return true; const root=document.getElementById('alfredMainV1'); const wrap=root?.querySelector('.dona-organic-wrap'); if(!wrap)return false; started=true; const canvas=document.createElement('canvas');canvas.id='donaWavesCanvas'; canvas.setAttribute('role','img');canvas.setAttribute('aria-label','Vagues animées de Dona réagissant à la voix Piper'); wrap.append(canvas); const ctx=canvas.getContext('2d',{alpha:true});if(!ctx)return true; const reduce=window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches||false; let w=0,h=0,dpr=1,mode=String(root.dataset.familyMode||'idle'); let phase=0,smoothed=.02,pcm=0,lastPCM=0,voiceFinished=false,then=0,raf=0; const socket=window.alfredSocket; socket?.on('alfred_state',data=>{ const next=String(data?.mode||'idle').toLowerCase(); if(next==='speaking'&&mode!=='speaking')voiceFinished=false; mode=next; }); socket?.on('dona_piper_level',data=>{ const value=Number(data?.level); if(!Number.isFinite(value))return; pcm=Math.max(0,Math.min(1,value)); lastPCM=performance.now(); }); socket?.on('alfred_tts_finished',()=>{pcm=0;voiceFinished=true;}); const ro=typeof ResizeObserver!=='undefined'?new ResizeObserver(resize):null; ro?.observe(wrap);window.addEventListener('resize',resize,{passive:true}); function resize(){ const box=wrap.getBoundingClientRect(); if(!box.width||!box.height)return; const ratio=Math.min(window.devicePixelRatio||1,1.7); const nw=Math.round(box.width*ratio),nh=Math.round(box.height*ratio); if(nw===canvas.width&&nh===canvas.height)return; w=box.width;h=box.height;dpr=ratio; canvas.width=nw;canvas.height=nh;ctx.setTransform(ratio,0,0,ratio,0,0); } // DONA_SCULPTURE_V11 : membranes de lumière, 2D pur, animées par PCM Piper. function drawSculpture(now){ const cx=w*.5, cy=h*.51; const R=Math.min(w*.23,h*.30); const strength=Math.min(1,smoothed); const phi=phase*.74; ctx.save(); // Lueur ambiante derrière les rubans, sans rectangle de fond. let halo=ctx.createRadialGradient(cx,cy,R*.12,cx,cy,R*2.25); halo.addColorStop(0,'rgba(255,188,186,.13)'); halo.addColorStop(.39,'rgba(120,82,249,.12)'); halo.addColorStop(1,'rgba(12,43,138,0)'); ctx.fillStyle=halo;ctx.fillRect(0,0,w,h); const shell=(layer,behind)=>{ const n=104, z=layer/8; const width=Math.min(w*.40,h*1.16)*(1-z*.34); const rise=h*(.225+.07*Math.sin(layer*.87+phi*.25))*(1-z*.24); const yShift=h*(.015*Math.sin(layer*1.8+phi*.18)); const steps=[]; for(let i=0;i<=n;i++){ const t=i/n, x=cx+(t-.5)*width*2; const env=Math.pow(Math.max(0,Math.sin(Math.PI*t)),.65); const y=cy+yShift+Math.sin(t*Math.PI*(2.7+layer*.16)+phi*.5+layer*.63)*rise*env +Math.cos(t*Math.PI*4.8-phi*.38+layer)*rise*.29*env; steps.push({x,y,t,env}); } const thickness=h*(.21+.045*Math.sin(layer*1.24+phi*.1)); const g=ctx.createLinearGradient(cx-width,cy,cx+width,cy); g.addColorStop(0,'rgba(26,119,250,.60)'); g.addColorStop(.33,'rgba(104,98,253,.58)'); g.addColorStop(.59,'rgba(249,167,209,.70)'); g.addColorStop(1,'rgba(252,192,123,.62)'); ctx.beginPath();ctx.moveTo(steps[0].x,steps[0].y); for(let i=1;i=0;i--){ const p=steps[i]; ctx.lineTo(p.x,p.y+thickness*p.env*(.8+strength*.44)); } ctx.closePath();ctx.globalAlpha=(behind?.19:.30)+strength*.09; ctx.fillStyle=g;ctx.fill();ctx.globalAlpha=.55+strength*.25; ctx.beginPath();ctx.moveTo(steps[0].x,steps[0].y); for(let i=1;i=4;layer--)shell(layer,true); // Noyau irisé : diaphragme vivant, se dilate sur les syllabes. const pulse=1+strength*.20+.035*Math.sin(phi*1.05); const r=R*pulse; const core=ctx.createRadialGradient(cx-r*.19,cy-r*.26,r*.1,cx,cy,r*1.15); core.addColorStop(0,'rgba(255,214,214,.90)'); core.addColorStop(.26,'rgba(244,147,194,.78)'); core.addColorStop(.53,'rgba(117,113,244,.82)'); core.addColorStop(.82,'rgba(41,125,253,.72)'); core.addColorStop(1,'rgba(37,135,245,.025)'); ctx.fillStyle=core;ctx.beginPath(); for(let i=0;i<=180;i++){ const a=i/180*Math.PI*2; const rad=r*(1+.12*Math.sin(4*a+phi*.5)+.085*Math.cos(3*a-phi*.3)); const x=cx+Math.cos(a)*rad,y=cy+Math.sin(a)*rad*.82; if(!i)ctx.moveTo(x,y);else ctx.lineTo(x,y); } ctx.closePath();ctx.shadowColor='#b696ff';ctx.shadowBlur=32+strength*25;ctx.fill();ctx.shadowBlur=0; for(let layer=3;layer>=0;layer--)shell(layer,false); // Poussière irisée légère dans le volume. for(let i=0;i<70;i++){ const t=(i*.61803398875)%1; const x=cx+(t-.5)*w*.70; const env=Math.sin(Math.PI*t); const y=cy+Math.cos(i*1.71+phi*.15)*h*.27*env; ctx.globalAlpha=(.08+strength*.21)*env; ctx.fillStyle=i%3?'#accaff':'#ffccbd'; ctx.beginPath();ctx.arc(x,y,.65+(i%4)*.22,0,Math.PI*2);ctx.fill(); } ctx.restore(); } function draw(now){ raf=requestAnimationFrame(draw); if(choice==='orb'||document.hidden||now-then<30)return; then=now; resize();if(!w||!h)return; const speaking=mode==='speaking'&&!voiceFinished; const current=(now-lastPCM<240&&speaking)?pcm:0; const base=mode==='listening'?.16:mode==='thinking'?.115:speaking?.07:.025; const target=Math.max(base,current*.88); smoothed+=(target-smoothed)*(target>smoothed?.35:.19); if(!reduce)phase+=mode==='listening'?.054:mode==='thinking'?.034:speaking?.047:.018; ctx.clearRect(0,0,w,h); if(choice==='sculpture'){drawSculpture(now);return;} const mid=h*.51, amplitude=h*(.245+smoothed*.35); const palette=[ ['#1362ff','#11c8fc','#6b56e9'], ['#1e92ef','#9274ff','#e47dbd'], ['#6c4bd9','#ef86b5','#ffd29b'], ['#4a5ad8','#ffb3c2','#ffc282'], ['#16b8ed','#8c66f5','#f596b5'], ['#7854e5','#d891ed','#fdad8b'] ]; // Contours en membranes translucides, superposés pour former une onde lumineuse. for(let layer=0;layer=0;i--){const [x,y]=points[i];ctx.lineTo(x,y+thickness);} ctx.closePath();ctx.fillStyle=gradient;ctx.globalAlpha=.095+(layer%3)*.058+smoothed*.1;ctx.fill(); ctx.beginPath();points.forEach(([x,y],i)=>i?ctx.lineTo(x,y):ctx.moveTo(x,y)); ctx.lineWidth=(layer%2?1.5:1.9)+smoothed*1.35; ctx.strokeStyle=gradient;ctx.globalAlpha=.60+smoothed*.30; ctx.shadowBlur=15+smoothed*25;ctx.shadowColor=center;ctx.stroke(); ctx.shadowBlur=0;ctx.globalAlpha=1; } // Lueur centrale très diffuse, avec énergie liée à l'amplitude Piper. const glow=ctx.createRadialGradient(w*.5,mid,0,w*.5,mid,w*.34); glow.addColorStop(0,`rgba(255,194,205,${.06+smoothed*.18})`); glow.addColorStop(.45,`rgba(107,99,255,${.025+smoothed*.045})`); glow.addColorStop(1,'rgba(56,138,244,0)'); ctx.fillStyle=glow;ctx.fillRect(0,0,w,h); // Petits points lumineux déterministes, très discrets au repos. for(let i=0;i<22;i++){ const nx=(i*0.61803398875)%1,ny=.5+.33*Math.sin(i*2.18+phase*.14); const x=nx*w,y=ny*h; ctx.globalAlpha=(.11+smoothed*.37)*Math.pow(Math.sin(nx*Math.PI),2); ctx.fillStyle=i%3===0?'#ffb5cc':'#83ceff';ctx.beginPath();ctx.arc(x,y,.8+(i%3)*.35,0,Math.PI*2);ctx.fill(); } ctx.globalAlpha=1; } resize();raf=requestAnimationFrame(draw); window.addEventListener('pagehide',()=>{cancelAnimationFrame(raf);ro?.disconnect();},{once:true}); return true; } let attempts=0; function ready(){ if(!init()&&attempts++<50)setTimeout(ready,120); addMenuControls(); const menu=document.getElementById('dv92ToolsMenu'); if(menu&&!menu.dataset.donaWavesObserved){ menu.dataset.donaWavesObserved='1'; new MutationObserver(()=>addMenuControls()).observe(menu,{childList:true}); } } if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',ready,{once:true});else ready(); })();