-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex_4.txt
More file actions
162 lines (156 loc) · 10.7 KB
/
Copy pathindex_4.txt
File metadata and controls
162 lines (156 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
<!DOCTYPE html><html><head><title>Silhouette Spin</title><style>body{margin:0;background:#111;overflow:hidden;font-family:monospace;}canvas{display:block;width:100vw;height:100vh;background:#222;image-rendering:pixelated;touch-action:none;}</style></head><body><canvas id="c"></canvas><script>
const canvas=document.getElementById('c'),ctx=canvas.getContext('2d');
let w=0,h=0,cx=0,cy=0,scale=0;
function resize(){
w=canvas.width=window.innerWidth|0;
h=canvas.height=window.innerHeight|0;
cx=w/2; cy=h/2;
scale=Math.floor(Math.min((w-80)/4.2,(h-180)/4.2));
}
resize();window.addEventListener('resize',resize);
let state='init',level=1,mode='2d',startTime=0,totalLevels=50,celebrateFrames=0,showUnlock=0,p2=Math.PI/2,p=Math.PI,t2p=Math.PI*2,dt=0,lastTime=0,gp=null,connected=false,audioCtx,osc,gain;
const DEADZONE=0.15,ROT_SPEED=1.2,ROT_FINE=0.6,ROT_SPEED_2D=1.8,ROT_FINE_2D=0.6,VIBE_INTERVAL=40; // Reduced 2D sensitivity ~35%
const verts3d=[[-1,-1,-1],[1,-1,-1],[1,1,-1],[-1,1,-1],[-1,-1,1],[1,-1,1],[1,1,1],[-1,1,1]];
const faces=[[0,1,2,3],[4,5,6,7],[0,1,5,4],[1,2,6,5],[2,3,7,6],[3,0,4,7]];
const edges=[[0,1],[1,2],[2,3],[3,0],[4,5],[5,6],[6,7],[7,4],[0,4],[1,5],[2,6],[3,7]];
const verts2d=[[0,1],[0.7,0.7],[1,0],[0.7,-0.7],[0,-1],[-0.7,-0.7],[-1,0],[-0.7,0.7]];
const edges2d=[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,0]];
let angle=0,targetAngle=0,euler={x:0,y:0,z:0},target={x:0,y:0,z:0},proj=[],targetProj=[],signals={scalar:{pitch:0,yaw:0,roll:0},discrete:{pitch:0,yaw:0,roll:0}};
let lastVibe=0,matchScore=0,displayScore=0;
// Signal norm
function getSignal(raw){
let scalar=0;if(Math.abs(raw)>DEADZONE)scalar=raw>0?(raw-DEADZONE)/(1-DEADZONE):(raw+DEADZONE)/(1-DEADZONE)*-1;
return{scalar:scalar,discrete:Math.round(scalar*10)};
}
// Mechanics
const rotation2d={update(s){angle-=(s.yaw*ROT_SPEED_2D+s.roll*ROT_FINE_2D)*dt;angle=(angle+t2p)%t2p;}}; // Inverted sign: left(-yaw)=+angle=CCW, right(+yaw)=-angle=CW; +right stick fine
const rotation3d={update(s){euler.x+=s.pitch*ROT_SPEED*dt;euler.x=Math.max(-p2,Math.min(p2,euler.x));euler.y+=s.yaw*ROT_SPEED*dt;euler.y=(euler.y+t2p)%t2p;euler.z+=s.roll*ROT_FINE*dt;euler.z=Math.max(-p2,Math.min(p2,euler.z));}};
const scoring2d={get(){let diff=Math.abs(angle-targetAngle)%t2p;diff=Math.min(diff,t2p-diff);return 1-diff/p;}};
const scoring3d={get(e,t){const ad=(a,b,per=t2p)=>(d=>Math.min(d,per-d))(((a-b)%per+per)%per);const dx=ad(e.x,t.x,p)/p2,dy=ad(e.y,t.y,t2p)/p,dz=ad(e.z,t.z,p)/p2;return 1-(dx+dy+dz)/3;}};
const feedback={update(score){const now=performance.now();if(gp?.vibrationActuator?.type==='dual-rumble'&&now-lastVibe>VIBE_INTERVAL){gp.vibrationActuator.playEffect('dual-rumble',{duration:VIBE_INTERVAL,weakMagnitude:score*0.6,strongMagnitude:score});lastVibe=now;}osc.frequency.value=150+score*400;gain.gain.value=Math.pow(score,2)*0.15;}};
const levelMgr={
genTarget(){
if(mode==='2d'){targetAngle=(Math.random()*t2p);}else{target.x=(Math.random()-0.5)*p;target.y=Math.random()*t2p;target.z=(Math.random()-0.5)*p;projectCube(target,targetProj);}
},
advance(){
level++;celebrateFrames=30;
if(level===16){mode='3d';euler={x:0,y:0,z:0};showUnlock=90;celebrateFrames=60;}
if(level>totalLevels){state='win';return;}
this.genTarget();
}
};
// 3D project
function projectCube(eu,pout){
const ex=eu.x,ey=eu.y,ez=eu.z,cx=Math.cos(ex),sx=Math.sin(ex),cy=Math.cos(ey),sy=Math.sin(ey),cz=Math.cos(ez),sz=Math.sin(ez);
const m00=cy*cz,m01=-cy*sz,m02=sy,m10=cx*sz+sx*sy*cz,m11=cx*cz-sx*sy*sz,m12=-sx*cy,m20=sx*sz-cx*sy*cz,m21=sx*cz+cx*sy*sz,m22=cx*cy;
for(let i=0;i<8;i++){const v=verts3d[i],vx=v[0],vy=v[1],vz=v[2];pout[i]={x:(m00*vx+m01*vy+m02*vz)*scale+cx,y:(m10*vx+m11*vy+m12*vz)*scale+cy,z:m20*vx+m21*vy+m22*vz};}
}
// Render
function render(score){
ctx.fillStyle='#111';ctx.fillRect(0,0,w,h);
if(state==='init'||state==='start'){
ctx.fillStyle='#0ff';ctx.textAlign='center';ctx.font='bold 48px monospace';ctx.fillText(state==='init'?'Connect Gamepad!':'Press A to Start!',cx,cy-60);
ctx.font='28px monospace';ctx.fillText('Left Stick X: Rotate (Left=CCW, Right=CW)',cx,cy+20);
ctx.fillText('Right Stick X: Fine Rotate',cx,cy+50);
if(connected){ctx.fillStyle='#0f0';ctx.fillText('Gamepad Ready!',cx,cy+80);}
return;
}
displayScore=score;
if(mode==='2d'){
const ca=Math.cos(angle),sa=Math.sin(angle);
const proj2d=verts2d.map(v=>({x:(v[0]*ca-v[1]*sa)*scale+cx,y:(v[0]*sa+v[1]*ca)*scale+cy}));
ctx.beginPath();ctx.moveTo(proj2d[0].x,proj2d[0].y);for(let i=1;i<verts2d.length;i++)ctx.lineTo(proj2d[i].x,proj2d[i].y);ctx.closePath();
ctx.fillStyle=`hsl(${180+score*80},70%,${45+score*25}%)`;ctx.fill();
ctx.strokeStyle='rgba(0,0,0,0.4)';ctx.lineWidth=3;ctx.lineCap='round';ctx.stroke();
const tca=Math.cos(targetAngle),tsa=Math.sin(targetAngle);
const tproj2d=verts2d.map(v=>({x:(v[0]*tca-v[1]*tsa)*scale+cx,y:(v[0]*tsa+v[1]*tca)*scale+cy}));
ctx.strokeStyle=`rgba(0,255,0,${score*0.3})`;ctx.lineWidth=18;ctx.lineCap='round';ctx.lineJoin='round';
ctx.beginPath();ctx.moveTo(tproj2d[0].x,tproj2d[0].y);for(let i=1;i<verts2d.length;i++)ctx.lineTo(tproj2d[i].x,tproj2d[i].y);ctx.closePath();ctx.stroke();
ctx.strokeStyle='#000';ctx.lineWidth=4;ctx.beginPath();ctx.moveTo(tproj2d[0].x,tproj2d[0].y);for(let i=1;i<verts2d.length;i++)ctx.lineTo(tproj2d[i].x,tproj2d[i].y);ctx.closePath();ctx.stroke();
}else{
projectCube(euler,proj);
const faceZs=faces.map((f,i)=>({i,z:f.reduce((s,j)=>s+proj[j].z,0)/4}));faceZs.sort((a,b)=>b.z-a.z);
for(let fi of faceZs){
const f=faces[fi.i];ctx.beginPath();ctx.moveTo(proj[f[0]].x,proj[f[0]].y);for(let k=1;k<4;k++)ctx.lineTo(proj[f[k]].x,proj[f[k]].y);ctx.closePath();
ctx.fillStyle=`hsl(${180+score*80},70%,${45+score*25}%)`;ctx.fill();
}
ctx.strokeStyle='rgba(0,0,0,0.6)';ctx.lineWidth=2;ctx.lineCap='round';
for(let e of edges){const p1=proj[e[0]],p2=proj[e[1]];ctx.beginPath();ctx.moveTo(p1.x,p1.y);ctx.lineTo(p2.x,p2.y);ctx.stroke();}
ctx.strokeStyle=`rgba(0,255,0,${score*0.3})`;ctx.lineWidth=18;ctx.lineCap='round';ctx.lineJoin='round';
for(let e of edges){const p1=targetProj[e[0]],p2=targetProj[e[1]];ctx.beginPath();ctx.moveTo(p1.x,p1.y);ctx.lineTo(p2.x,p2.y);ctx.stroke();}
ctx.strokeStyle='#000';ctx.lineWidth=4;
for(let e of edges){const p1=targetProj[e[0]],p2=targetProj[e[1]];ctx.beginPath();ctx.moveTo(p1.x,p1.y);ctx.lineTo(p2.x,p2.y);ctx.stroke();}
}
ctx.textAlign='left';ctx.fillStyle='rgba(0,255,0,0.95)';ctx.font='bold 28px monospace';
ctx.fillText(`Level ${level}/${totalLevels} | ${mode.toUpperCase()}`,20,35);
ctx.fillRect(20,45,displayScore*(w-60),16);ctx.strokeStyle='#000';ctx.lineWidth=2;ctx.strokeRect(20,45,w-60,16);
if(state==='playing'){
ctx.fillStyle='rgba(0,0,0,0.8)';ctx.fillRect(w-290,h-220,270,200);
ctx.fillStyle='#0ff';ctx.font='20px monospace';ctx.textAlign='left';
ctx.fillText(`Yaw: ${signals.scalar.yaw.toFixed(2)} / ${signals.discrete.yaw}`,w-280,h-190);
ctx.fillText(`Pitch: ${signals.scalar.pitch.toFixed(2)} / ${signals.discrete.pitch}`,w-280,h-165);
ctx.fillText(`Roll: ${signals.scalar.roll.toFixed(2)} / ${signals.discrete.roll}`,w-280,h-140);
ctx.fillStyle='#0f0';ctx.fillText(`Score: ${displayScore.toFixed(3)}`,w-280,h-110);
if(mode==='2d'){
let diff=Math.abs(angle-targetAngle)%t2p;diff=Math.min(diff,t2p-diff);
ctx.fillText(`Angle Diff: ${(diff/p*180).toFixed(0)}°`,w-280,h-85);
}else{
const ad=(a,b,per=t2p)=>(d=>Math.min(d,per-d))(((a-b)%per+per)%per);
ctx.fillText(`DX:${ad(euler.x,target.x,p).toFixed(2)} DY:${ad(euler.y,target.y,t2p).toFixed(2)} DZ:${ad(euler.z,target.z,p).toFixed(2)}`,w-280,h-85);
}
}
if(celebrateFrames>0&&showUnlock>0){
ctx.fillStyle='rgba(0,0,0,0.5)';ctx.fillRect(0,0,w,h);
ctx.fillStyle='#0f0';ctx.textAlign='center';ctx.font='bold 52px monospace';ctx.fillText('3D MODE UNLOCKED!',cx,cy);
ctx.font='bold 32px monospace';ctx.fillText('Match the 3D silhouette!',cx,cy+60);
}
if(state==='paused'){
ctx.fillStyle='rgba(0,0,0,0.8)';ctx.fillRect(0,0,w,h);
ctx.fillStyle='#fff';ctx.textAlign='center';ctx.font='bold 56px monospace';ctx.fillText('PAUSED',cx,cy);
ctx.font='32px monospace';ctx.fillText('A: Resume B: Quit',cx,cy+70);
}
if(state==='win'){
ctx.fillStyle='rgba(0,0,0,0.8)';ctx.fillRect(0,0,w,h);
ctx.fillStyle='#0f0';ctx.textAlign='center';ctx.font='bold 56px monospace';ctx.fillText('Master Spinner!',cx,cy-40);
ctx.font='32px monospace';const t=(performance.now()-startTime)/1000|0;ctx.fillText(`${t}s - ${level-1}/${totalLevels}`,cx,cy+30);
ctx.fillText('A: Restart',cx,cy+90);
}
}
// Audio
function initAudio(){audioCtx=new AudioContext();osc=audioCtx.createOscillator();osc.type='sine';gain=audioCtx.createGain();osc.connect(gain);gain.connect(audioCtx.destination);osc.start();gain.gain.value=0;}
function playPop(){const o=audioCtx.createOscillator();o.type='sine';const g=audioCtx.createGain();g.gain.setValueAtTime(0,audioCtx.currentTime);g.gain.linearRampToValueAtTime(0.4,audioCtx.currentTime+0.01);g.gain.exponentialRampToValueAtTime(0.01,audioCtx.currentTime+0.15);const startF=700;o.frequency.setValueAtTime(startF,audioCtx.currentTime);o.frequency.linearRampToValueAtTime(startF*1.6,audioCtx.currentTime+0.15);o.connect(g);g.connect(audioCtx.destination);o.start();o.stop(audioCtx.currentTime+0.15);}
// Loop
function loop(time){
dt=Math.min(1,(time-lastTime)/16.66);lastTime=time;
const gps=navigator.getGamepads();gp=gps[0];connected=!!gp;
if(state==='init'&&connected)state='start';
if(gp){
const s=getSignal(gp.axes[0]),p=getSignal(gp.axes[1]),r=getSignal(gp.axes[2]);
signals.scalar={yaw:s.scalar,pitch:-p.scalar,roll:r.scalar};
signals.discrete={yaw:s.discrete,pitch:-p.discrete,roll:r.discrete};
if(gp.buttons[0].pressed){
if(state==='start'){state='playing';startTime=performance.now();levelMgr.genTarget();canvas.requestFullscreen?.();initAudio();}
else if(state==='paused')state='playing';
else if(state==='win'){state='start';level=1;mode='2d';angle=0;euler={x:0,y:0,z:0};}
}
if(gp.buttons[1].pressed&&state==='playing')state='paused';
}
if(state==='playing'){
if(mode==='2d')rotation2d.update(signals.scalar);else rotation3d.update(signals.scalar);
matchScore=mode==='2d'?scoring2d.get():scoring3d.get(euler,target);
feedback.update(matchScore);
if(matchScore>=0.99&&celebrateFrames===0){playPop();levelMgr.advance();}
if(celebrateFrames>0){
celebrateFrames--;showUnlock=Math.max(0,showUnlock-1);
const spin=0.5*dt*60;
if(mode==='2d')angle+=spin;else euler.y+=spin;
}
render(matchScore);
}else{render(0);}
requestAnimationFrame(loop);
}
loop();
document.addEventListener('pointerdown',e=>e.preventDefault(),{capture:true});
document.addEventListener('keydown',e=>e.preventDefault(),{capture:true});
document.body.style.touchAction='none';
</script></body></html>