Files
Alfredv2/alfred/app/static/js/dona-waves-v1.js
T
2026-11-01 15:01:22 +01:00

259 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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<steps.length;i++)ctx.lineTo(steps[i].x,steps[i].y);
for(let i=steps.length-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<steps.length;i++)ctx.lineTo(steps[i].x,steps[i].y);
ctx.lineWidth=1.1+(layer%3)*.35+strength;
ctx.strokeStyle=g;ctx.shadowBlur=behind?11:21;
ctx.shadowColor=layer%2?'#f2a1bc':'#5faaff';ctx.stroke();ctx.shadowBlur=0;
// Fines nervures / maillage, effet étoffe lumineuse.
if(layer%2===0){
ctx.globalAlpha=.13+strength*.08;
for(let i=6;i<n;i+=7){
const p=steps[i],x=p.x,y=p.y;
ctx.beginPath();ctx.moveTo(x,y);
ctx.lineTo(x+Math.sin(layer+phi)*5,y+thickness*p.env);
ctx.lineWidth=.7;ctx.strokeStyle=layer%4?'#b9adff':'#ffcaad';ctx.stroke();
}
}
ctx.globalAlpha=1;
};
for(let layer=7;layer>=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<palette.length;layer++){
const [left,center,right]=palette[layer];
const m=layer*1.07;
const points=[];
for(let x=0;x<=w+5;x+=5){
const nx=x/w;
const envelope=Math.pow(Math.max(0,Math.sin(Math.PI*nx)),.75);
const s1=Math.sin(nx*Math.PI*(2.55+m*.24)+phase*(.70+m*.04)+m*.79);
const s2=Math.sin(nx*Math.PI*(5.0-m*.17)-phase*(.42+m*.02)+m*1.18);
const y=mid+(s1*.77+s2*.23)*amplitude*envelope
+Math.cos(nx*Math.PI*2.6+phase*.28+m)*h*.014;
points.push([x,y]);
}
const gradient=ctx.createLinearGradient(0,0,w,0);
gradient.addColorStop(0,left);gradient.addColorStop(.5,center);gradient.addColorStop(1,right);
ctx.beginPath();ctx.moveTo(0,mid);
for(const [x,y] of points)ctx.lineTo(x,y);
const thickness=h*(.11+.018*(layer%3)+.09*smoothed);
for(let i=points.length-1;i>=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();
})();