返回 CodeWhale
props.js
1 // Flat physical props; shared paths for the canvas and standalone SVG exports.
2 (function(root){
3 'use strict';
4 const R=root.WhaleRig,{clamp,D2R}=R;
5 const poly=pts=>{const a=[['M',...pts[0]]];for(let i=1;i<=pts.length;i++){const p=pts[(i-1)%pts.length],q=pts[i%pts.length];a.push(['C',p[0],p[1],q[0],q[1],q[0],q[1]]);}a.push(['Z']);return a;};
6 function transform(path,x,y,a=0,s=1){const c=Math.cos(a),sn=Math.sin(a);return path.map(cmd=>{const o=[cmd[0]];for(let i=1;i<cmd.length;i+=2)o.push(x+(cmd[i]*c-cmd[i+1]*sn)*s,y+(cmd[i]*sn+cmd[i+1]*c)*s);return o;});}
7 function shapes(p,parts,view){const small=(view.lod||0)>=2,out=[],boost=(small?1.15:1)*parts.unitScale;
8 const add=(id,path,role,x,y,a,s,visible)=>out.push({id,path:transform(path,x,y,a,s),role,opacity:clamp(visible||0,0,1)});
9 const hand=parts.anchors.finTip,angle=parts.anchors.finAng;
10 const offset=(anchor,x,y,a=angle)=>({x:anchor.x+(x*Math.cos(a)-y*Math.sin(a))*boost,y:anchor.y+(x*Math.sin(a)+y*Math.cos(a))*boost});
11 const page=R.parse('M-9 -13 C-5 -14 0 -14 4 -13 C6 -11 7 -9 9 -8 C9 -1 9 6 8 13 C3 12 -3 13 -9 14 C-8 5 -8 -4 -9 -13 Z'),fold=poly([[4,-13],[4,-8],[9,-8]]);
12 // Optical separation: a literal air gap survives at 24 px and in one ink.
13 const pc=offset(hand,22+(p.pageDX||0),-31+(p.pageDY||0)),px=pc.x,py=pc.y,pa=angle-.08+(p.pageRot||0)*D2R;
14 add('prop-page',page,'tool',px,py,pa,boost,p.page);
15 add('prop-page-fold',fold,'cutout',px,py,pa,boost,p.page);
16 for(let i=0;i<2;i++)add(`prop-page-rule-${i}`,poly([[-5,-4+i*6],[5,-4+i*6],[5,-2+i*6],[-5,-2+i*6]]),'cutout',px,py,pa,boost,p.page);
17 // The page turns out and returns, rather than resetting a 1 to 0 in one frame.
18 add('prop-page-turn',page,'tool',px+2*boost,py,pa,boost*Math.sin((p.pageFlip||0)*Math.PI/2)*.75,p.page*(p.pageFlip||0));
19 const pad=offset(hand,21,-16);
20 add('prop-pad',poly([[-11,-5],[11,-5],[11,5],[-11,5]]),'tool',pad.x,pad.y,angle-.05,boost,p.pad);
21 add('prop-pad-binding',poly([[-8.5,-3],[8.5,-3],[8.5,-1.5],[-8.5,-1.5]]),'cutout',pad.x,pad.y,angle-.05,boost,p.pad);
22 for(let i=0;i<4;i++)add(`prop-pad-line-${i}`,poly([[-8+i*4,0],[-6+i*4,0],[-6+i*4,1.2],[-8+i*4,1.2]]),'cutout',pad.x,pad.y,angle-.05,boost,p.pad*clamp((p.padLines||0)-i,0,1));
23 const pencil=poly([[-2,-5],[2,-5],[2,6],[0,10],[-2,6]]);
24 const pen=offset(hand,16.4+(p.scribX||0)*.35,-23.3+(p.scribY||0)*.35);
25 add('prop-pencil',pencil,'accent',pen.x,pen.y,angle-.48,boost,p.pencil);
26 add('prop-pencil-tip',poly([[-1.3,6.5],[1.3,6.5],[0,10]]),'tool',pen.x,pen.y,angle-.48,boost,p.pencil);
27 const lens=[...R.ellipse(0,0,9,9),...R.ellipse(0,0,5.8,5.8)];
28 const la=angle+.55,lc=offset(hand,3,-42,la);
29 add('prop-lens',lens,'tool',lc.x,lc.y,la,boost,p.lens);
30 add('prop-lens-handle',poly([[-1.7,7],[1.7,7],[1.7,17],[-1.7,17]]),'tool',lc.x,lc.y,la,boost,p.lens);
31 const wrench=poly([[-2,9],[-2,-8],[-6,-11],[-6,-18],[-3,-21],[-3,-14],[3,-14],[3,-21],[6,-18],[6,-11],[2,-8],[2,9],[5,12],[5,17],[2,20],[2,14],[-2,14],[-2,20],[-5,17],[-5,12]]);
32 const wc=offset(parts.anchors.tail,40,-5+(p.wrenchY||0));
33 add('prop-wrench',wrench,'tool',wc.x,wc.y,parts.anchors.tailAng+1.1+(p.wrenchSpin||0)*D2R,(small?.78:.7)*parts.unitScale,p.wrench);
34 const ga=angle-.22,gs=boost*(.95+.3*clamp(p.glassExt||0,0,1)),gc=offset(hand,12,-33,ga);
35 add('prop-spyglass',poly([[0,-2.5],[6,-2.5],[6,-3.5],[13,-3.5],[13,-4.5],[19,-4.5],[19,4.5],[13,4.5],[13,3.5],[6,3.5],[6,2.5],[0,2.5]]),'accent',gc.x,gc.y,ga,gs,p.glass);
36 add('prop-spyglass-end',poly([[17,-4.5],[20,-4.5],[20,4.5],[17,4.5]]),'tool',gc.x,gc.y,ga,gs,p.glass);
37 // Pointer proportions follow the existing Codewhale Computer Use mark.
38 const cursor=offset(parts.anchors.spout,11+(p.cursorX||0),-31+(p.cursorY||0));
39 add('prop-cursor',poly([[-8,-12],[10,1.6],[1.6,2.8],[-2.2,10.4]]),'pointer',cursor.x,cursor.y,angle,boost,p.cursor);
40 const link=offset(parts.anchors.spout,16,-34),linkAngle=angle+(.55+(p.linkTilt||0)*D2R);
41 for(const side of [-1,1])add(`prop-link-${side}`,[...R.ellipse(side*2.3,side*5.5,4.5,8),...R.ellipse(side*2.3,side*5.5,2.1,5.3)],'pointer',link.x,link.y,linkAngle,boost,p.link);
42 // A recognisable fan-shaped jet, with a separate water surface below it.
43 // These are filled silhouettes, so the fountain survives one-ink rendering.
44 const jet=R.parse('M-2 0 C-3 -12 -9 -28 -17 -29 C-22 -29 -23 -24 -19 -21 C-25 -21 -27 -28 -23 -32 C-16 -39 -5 -32 0 -16 C5 -33 16 -38 23 -31 C27 -26 23 -20 19 -21 C23 -25 20 -29 16 -28 C8 -26 4 -12 2 0 C1 1 -1 1 -2 0 Z');
45 const spout=clamp(p.spout||0,0,1),surface=clamp(p.splash||0,0,1),origin=parts.anchors.spout;
46 add('prop-spout',jet,'water',origin.x,origin.y,0,(.35+.65*spout)*parts.unitScale,spout);
47 for(const side of [-1,1])add(`prop-spout-drop-${side}`,R.parse('M0 -3 C3 0 3 3 0 3 C-3 3 -3 0 0 -3 Z'),'water',origin.x+side*25*parts.unitScale,origin.y-35*parts.unitScale,-side*.35,parts.unitScale*spout,spout);
48 add('prop-water-back',R.parse('M-43 0 C-28 -7 -12 -4 0 -1 C14 2 28 -7 43 -2 C29 -1 15 7 0 3 C-15 -1 -28 -3 -43 0 Z'),'water',0,43,0,.8+.2*surface,surface);
49 add('prop-water-front',R.parse('M-29 0 C-11 -2 8 4 28 0 C13 8 -11 4 -29 0 Z'),'water',0,50,0,.8+.2*surface,surface);
50 const splash=R.parse('M0 0 C-2 -8 -8 -12 -11 -15 C-5 -15 2 -8 3 0 C2 2 1 2 0 0 Z');
51 for(const side of [-1,1])add(`prop-water-splash-${side}`,splash.map(c=>[c[0],...c.slice(1).map((v,i)=>i%2?v:-side*v)]),'water',side*35,39,0,.5+.5*surface,surface);
52 return out;
53 }
54 function particles(list,view){const out=[],small=(view.lod||0)>=2;
55 for(const [i,p] of list.entries()){
56 if(p.age<0||p.kind==='mist'||(small&&p.kind==='bubble'))continue;
57 const fade=clamp((p.life-p.age)/(p.life*.2),0,1),r=p.r*(small?1.65:1);
58 let x=p.x,y=p.y;
59 if(small&&p.kind==='thought'){x+=4;y-=6;}
60 if(p.kind==='drop'){
61 const shape=R.parse('M0 -2 C1 -1 2 1 0 2 C-2 1 -1 -1 0 -2 Z');
62 out.push({id:`spout-${i}`,path:transform(shape,x,y,0,r),role:'water',opacity:fade});
63 }else{
64 const path=p.cloud?R.parse('M-5 1 C-8 0 -7 -4 -4 -4 C-3 -8 2 -8 4 -5 C8 -5 9 0 5 2 C4 6 -3 6 -5 1 Z'):R.ellipse(0,0,1,1);
65 out.push({id:`bubble-${i}`,path:transform(path,x,y,0,p.cloud?r/6:r),role:'water',opacity:fade});
66 }
67 }
68 return out;
69 }
70 root.WhaleProps={shapes,particles};
71 })(typeof window!=='undefined'?window:globalThis);
72
72 lines JAVASCRIPT