返回 CodeWhale
habitat.js
根目录 / web / vendor / whale-character-v2 / habitat.js
1 // The companion's little cove. It uses the existing animation clock and stops
2 // with reduced motion / page visibility; it never changes Engine presence.
3 (function(root){
4 'use strict';
5 const R=root.WhaleRig,TAU=Math.PI*2;
6 let target={x:0,y:0},gaze={x:0,y:0},lastFrame=0;
7 const ripples=[];
8 function observe(x,y){target={x:R.clamp(x/62,-1,1),y:R.clamp(y/62,-1,1)};}
9 function leave(){target={x:0,y:0};}
10 function tap(x,y,d){
11 const point={x:R.clamp(x,-42,42),y:R.clamp(y,25,48),at:d.f};
12 ripples.push(point);if(ripples.length>3)ripples.shift();observe(point.x,point.y);
13 }
14 function pose(d){
15 const dt=Math.max(0,Math.min(.25,(d.f-lastFrame)/30));lastFrame=d.f;
16 if(d.reduced){gaze={x:0,y:0};return {};}
17 const active=d.acting==='rest'&&!d.reduced,w=1-Math.exp(-dt*4);
18 gaze.x=R.lerp(gaze.x,active?target.x:0,w);gaze.y=R.lerp(gaze.y,active?target.y:0,w);
19 const p=d.pose();return {lookX:p.lookX+gaze.x*.85,lookY:p.lookY+gaze.y*.65,head:p.head-gaze.y*5,rot:p.rot+gaze.x*2};
20 }
21 function palette(theme){return theme==='charcoal'
22 ? {air:'#243c4b',shore:'#426878',water:'#254e64',deep:'#1d4055',reed:'#4c8490',glint:'#699cab'}
23 : {air:'#edf5f5',shore:'#c4d8d7',water:'#d6eaed',deep:'#b9dce3',reed:'#86b5b6',glint:'#f7fcfb'};}
24 function fill(ctx,d,color,alpha=1){ctx.save();ctx.globalAlpha=alpha;ctx.fillStyle=color;ctx.beginPath();R.trace(ctx,R.parse(d));ctx.fill();ctx.restore();}
25 function behind(ctx,d,theme){
26 const p=palette(theme),t=d.reduced?0:d.f/30,w=Math.sin(t*.55)*1.4;
27 fill(ctx,'M-59 7 C-63 -29 -37 -58 0 -59 C35 -62 60 -41 61 -6 C66 31 48 57 13 60 C-22 67 -54 48 -59 7 Z',p.air);
28 fill(ctx,'M-61 29 C-61 13 -56 5 -51 11 C-46 16 -48 22 -43 25 C-39 27 -37 30 -35 34 C-45 37 -55 36 -61 29 Z',p.shore);
29 fill(ctx,`M-60 ${21+w} C-37 ${15+w} -14 ${23-w} 6 21 C26 ${18+w} 43 ${15-w} 61 18 C61 42 42 59 12 61 C-19 65 -51 47 -60 ${21+w} Z`,p.water);
30 fill(ctx,'M-55 40 C-29 49 -9 42 12 45 C32 49 44 39 56 36 C44 56 21 63 -3 61 C-25 59 -44 49 -55 40 Z',p.deep,.7);
31 for(const [x,h,phase] of [[-54,29,0],[-49,22,1],[53,19,2],[57,26,3]]){
32 const sway=Math.sin(t*.65+phase)*2;
33 fill(ctx,`M${x} 52 C${x-2} 42 ${x+sway-5} ${52-h+5} ${x+sway} ${52-h} C${x+sway-1} ${52-h+9} ${x+3} 40 ${x+1} 52 Z`,p.reed,.8);
34 }
35 fill(ctx,`M-53 33 C-45 ${30+w} -40 ${30+w} -34 33 C-42 32 -47 34 -53 33 Z`,p.glint,.7);
36 fill(ctx,`M35 30 C43 ${28-w} 48 ${28-w} 54 30 C47 30 42 32 35 30 Z`,p.glint,.65);
37 // Water bubbles live low in the cove, away from the thinking-cloud region.
38 for(let i=0;i<3;i++){
39 const phase=d.reduced?.35:(t/(7+i*2)+i*.31)%1;
40 const x=[-47,47,39][i]+Math.sin(t*.6+i)*1.2,y=55-phase*24,r=.55+phase*.5;
41 ctx.save();ctx.globalAlpha=Math.sin(phase*Math.PI)*.65;ctx.fillStyle=p.glint;ctx.beginPath();R.trace(ctx,R.ellipse(x,y,r,r));ctx.fill();ctx.restore();
42 }
43 }
44 function front(ctx,d,theme){
45 const p=palette(theme),t=d.reduced?0:d.f/30,w=Math.sin(t*.45)*2;
46 // The near current passes below the face, preserving the white crescent.
47 fill(ctx,`M-40 51 C-23 ${47+w} -9 ${54-w} 8 51 C21 ${48+w} 30 49 40 47 C26 54 16 54 5 54 C-11 57 -26 50 -40 51 Z`,p.glint,.6);
48 fill(ctx,`M-23 58 C-8 ${56-w*.3} 8 60 21 57 C9 62 -8 60 -23 58 Z`,p.glint,.55);
49 for(const ripple of ripples){
50 const age=d.reduced?12:d.f-ripple.at;if(age<0||age>78)continue;
51 for(let i=0;i<2;i++){
52 const a=age-i*9;if(a<0)continue;const radius=2+a*.19,alpha=(1-a/78)*.72;
53 ctx.save();ctx.globalAlpha=alpha;ctx.fillStyle=p.glint;ctx.beginPath();
54 R.trace(ctx,R.ellipse(ripple.x,ripple.y,radius,radius*.25));
55 R.trace(ctx,R.ellipse(ripple.x,ripple.y,Math.max(0,radius-.7),Math.max(0,radius*.25-.55)));
56 ctx.fill('evenodd');ctx.restore();
57 }
58 }
59 }
60 root.WhaleHabitat={behind,front,pose,observe,leave,tap,inspect:()=>({target:{...target},gaze:{...gaze},ripples:ripples.length})};
61 })(window);
62
62 lines JAVASCRIPT