259 lines
12 KiB
JavaScript
259 lines
12 KiB
JavaScript
/* 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();
|
||
})();
|