Skip to content

Commit 1270386

Browse files
committed
Pill choice: chosen dissolves toward camera, unchosen dims but stays
- Chosen: glows bright → floats toward camera → fades to invisible - Unchosen: dims to 30% opacity, shrinks to 70% — still clickable - No explosion, no split — elegant dissolve - Screen flash: white burst then colored fade - Lure restores both pills when returning to hero
1 parent 566a1c8 commit 1270386

2 files changed

Lines changed: 80 additions & 170 deletions

File tree

docs/index.html

Lines changed: 40 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -115,95 +115,50 @@
115115
const hits=getIntersects(e.clientX,e.clientY);
116116
if(!hits.length)return;
117117
const type=hits[0].object.parent.userData.type;
118-
const clickedPill=type==='blue'?bPill:rPill;
119-
120-
// Split-open animation: top half up, bottom half down, inner glow
121-
const topHalf=clickedPill.children[1]; // sphere top
122-
const botHalf=clickedPill.children[2]; // sphere bottom
123-
const body=clickedPill.children[0]; // cylinder
124-
const origTopY=topHalf.position.y, origBotY=botHalf.position.y;
125-
126-
// Add inner glow light
127-
// Inner glow — warm core light
128-
const glowColor=type==='blue'?0x0044cc:0xcc0000;
129-
const innerGlow=new THREE.PointLight(glowColor,0,8);
130-
innerGlow.position.set(0,0,0);
131-
clickedPill.add(innerGlow);
132-
133-
// Scatter micro-particles from the seam
134-
const sparkGeo=new THREE.BufferGeometry();
135-
const sparkCount=60;
136-
const sparkPos=new Float32Array(sparkCount*3);
137-
const sparkVel=[];
138-
for(let i=0;i<sparkCount;i++){
139-
sparkPos[i*3]=0;sparkPos[i*3+1]=0;sparkPos[i*3+2]=0;
140-
const a=Math.random()*Math.PI*2,sp=.02+Math.random()*.08;
141-
sparkVel.push({vx:Math.cos(a)*sp,vy:(Math.random()-.3)*sp*1.5,vz:Math.sin(a)*sp});
142-
}
143-
sparkGeo.setAttribute('position',new THREE.BufferAttribute(sparkPos,3));
144-
const sparkMat=new THREE.PointsMaterial({color:glowColor,size:.08,transparent:true,opacity:.9,blending:THREE.AdditiveBlending});
145-
const sparks=new THREE.Points(sparkGeo,sparkMat);
146-
clickedPill.add(sparks);
147-
148-
let splitT=0;
149-
const flashEl=document.getElementById('pill-flash');
150-
function splitAnim(){
151-
splitT+=0.025;
152-
// Spring-like overshoot: ease with damped oscillation
153-
const t=Math.min(splitT,1.2);
154-
const spring=1-Math.exp(-6*t)*Math.cos(t*8);
155-
const ease=Math.min(spring,1.3);
156-
157-
// Split with slight outward rotation
158-
topHalf.position.y=origTopY+ease*2.2;
159-
topHalf.rotation.x=ease*.15;
160-
topHalf.rotation.z=ease*.08;
161-
botHalf.position.y=origBotY-ease*2.2;
162-
botHalf.rotation.x=-ease*.12;
163-
botHalf.rotation.z=-ease*.06;
164-
165-
// Body dissolves
166-
body.scale.y=Math.max(0,1-ease*.9);
167-
body.material.opacity=Math.max(0,.94-ease*.94);
168-
169-
// Inner glow ramps up with warmth
170-
innerGlow.intensity=ease*40;
171-
innerGlow.distance=3+ease*12;
172-
173-
// Sparks fly outward
174-
const sp=sparks.geometry.attributes.position.array;
175-
for(let i=0;i<sparkCount;i++){
176-
const v=sparkVel[i];
177-
sp[i*3]+=v.vx;sp[i*3+1]+=v.vy;sp[i*3+2]+=v.vz;
178-
}
179-
sparks.geometry.attributes.position.needsUpdate=true;
180-
sparkMat.opacity=Math.max(0,1-splitT*.6);
181-
182-
// Screen flash: brief white burst then colored glow
183-
if(splitT<.1){
184-
const f=1-splitT/.1;
185-
flashEl.style.background='radial-gradient(circle,rgba(255,255,255,'+f*.7+'),transparent 50%)';
118+
const chosenPill=type==='blue'?bPill:rPill;
119+
const otherPill=type==='blue'?rPill:bPill;
120+
const origZ=chosenPill.position.z;
121+
122+
// Chosen pill: glow → float toward camera → dissolve
123+
let dt=0;
124+
function dissolveAnim(){
125+
dt+=0.02;
126+
const t=Math.min(dt,1);
127+
const ease=1-Math.pow(1-t,3);
128+
129+
// Float toward camera + glow intensely
130+
chosenPill.position.z=origZ+ease*4;
131+
chosenPill.scale.setScalar(1+ease*.3);
132+
chosenPill.children.forEach(m=>{
133+
if(!m.material)return;
134+
m.material.emissiveIntensity=.15+ease*.8;
135+
if(dt>.5) m.material.opacity=Math.max(0,.94-(dt-.5)*1.88);
136+
});
137+
138+
// Unchosen pill: dim down
139+
otherPill.children.forEach(m=>{
140+
if(!m.material)return;
141+
m.material.emissiveIntensity=Math.max(.03,.15-ease*.12);
142+
m.material.opacity=Math.max(.3,.94-ease*.5);
143+
});
144+
otherPill.scale.setScalar(Math.max(.7,1-ease*.3));
145+
146+
// Screen color flash
147+
const flashEl=document.getElementById('pill-flash');
148+
if(dt<.15){
149+
flashEl.style.background='radial-gradient(circle,rgba(255,255,255,'+(1-dt/.15)*.5+'),transparent 50%)';
186150
flashEl.style.opacity='1';
187-
}else if(splitT<.4){
188-
const f=1-(splitT-.1)/.3;
151+
}else if(dt<.5){
189152
const rgb=type==='blue'?'0,68,204':'204,0,0';
190-
flashEl.style.background='radial-gradient(circle,rgba('+rgb+','+f*.3+'),transparent 60%)';
191-
flashEl.style.opacity='1';
153+
flashEl.style.background='radial-gradient(circle,rgba('+rgb+','+(1-(dt-.15)/.35)*.25+'),transparent 60%)';
192154
}else{
193155
flashEl.style.opacity='0';
194156
}
195157

196-
// Fade out everything after split
197-
if(splitT>1){
198-
const fade=Math.max(0,1-(splitT-1)*1.5);
199-
clickedPill.children.forEach(m=>{if(m.material)m.material.opacity=fade*.5});
200-
sparkMat.opacity=fade*.3;
201-
innerGlow.intensity=Math.max(0,innerGlow.intensity*fade);
202-
if(fade<=0){clickedPill.visible=false;return}
203-
}
204-
requestAnimationFrame(splitAnim);
158+
if(dt<1.2) requestAnimationFrame(dissolveAnim);
159+
else chosenPill.visible=false;
205160
}
206-
splitAnim();
161+
dissolveAnim();
207162

208163
// Red pill: shatter video as FIXED OVERLAY during scroll (no waiting)
209164
if(type==='red'){
@@ -239,6 +194,7 @@
239194
const vc=document.getElementById('vid-hero-calm');
240195
const vs=document.getElementById('vid-hero-shatter');
241196
if(vc&&vs){vc.style.opacity='1';vs.style.opacity='0'}
197+
document.getElementById('pill-canvas').style.opacity='0';
242198
resetObs.disconnect();
243199
}
244200
},{threshold:0});
@@ -489,10 +445,9 @@
489445
const vCalm=document.getElementById('vid-hero-calm');
490446
const vShatter=document.getElementById('vid-hero-shatter');
491447
if(vCalm&&vShatter){vCalm.style.opacity='1';vShatter.style.opacity='0'}
492-
// Make pills visible again for re-choosing
448+
// Restore both pills to full state
493449
const bp=window._bPill,rp=window._rPill;
494-
if(bp){bp.visible=true;bp.children.forEach(m=>{if(m.material)m.material.opacity=.94})}
495-
if(rp){rp.visible=true;rp.children.forEach(m=>{if(m.material)m.material.opacity=.94})}
450+
[bp,rp].forEach(p=>{if(!p)return;p.visible=true;p.scale.setScalar(1);p.position.z=0;p.children.forEach(m=>{if(m.material){m.material.opacity=.94;m.material.emissiveIntensity=.15}})})
496451
// Scroll back
497452
document.getElementById('act-choice').scrollIntoView({behavior:'smooth'});
498453
// Pulse the red pill after arrival

mofa-website/src/pages/index.astro

Lines changed: 40 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -542,95 +542,50 @@ let cx=0,cy=0;
542542
const hits=getIntersects(e.clientX,e.clientY);
543543
if(!hits.length)return;
544544
const type=hits[0].object.parent.userData.type;
545-
const clickedPill=type==='blue'?bPill:rPill;
546-
547-
// Split-open animation: top half up, bottom half down, inner glow
548-
const topHalf=clickedPill.children[1]; // sphere top
549-
const botHalf=clickedPill.children[2]; // sphere bottom
550-
const body=clickedPill.children[0]; // cylinder
551-
const origTopY=topHalf.position.y, origBotY=botHalf.position.y;
552-
553-
// Add inner glow light
554-
// Inner glow — warm core light
555-
const glowColor=type==='blue'?0x0044cc:0xcc0000;
556-
const innerGlow=new THREE.PointLight(glowColor,0,8);
557-
innerGlow.position.set(0,0,0);
558-
clickedPill.add(innerGlow);
559-
560-
// Scatter micro-particles from the seam
561-
const sparkGeo=new THREE.BufferGeometry();
562-
const sparkCount=60;
563-
const sparkPos=new Float32Array(sparkCount*3);
564-
const sparkVel=[];
565-
for(let i=0;i<sparkCount;i++){
566-
sparkPos[i*3]=0;sparkPos[i*3+1]=0;sparkPos[i*3+2]=0;
567-
const a=Math.random()*Math.PI*2,sp=.02+Math.random()*.08;
568-
sparkVel.push({vx:Math.cos(a)*sp,vy:(Math.random()-.3)*sp*1.5,vz:Math.sin(a)*sp});
569-
}
570-
sparkGeo.setAttribute('position',new THREE.BufferAttribute(sparkPos,3));
571-
const sparkMat=new THREE.PointsMaterial({color:glowColor,size:.08,transparent:true,opacity:.9,blending:THREE.AdditiveBlending});
572-
const sparks=new THREE.Points(sparkGeo,sparkMat);
573-
clickedPill.add(sparks);
574-
575-
let splitT=0;
576-
const flashEl=document.getElementById('pill-flash');
577-
function splitAnim(){
578-
splitT+=0.025;
579-
// Spring-like overshoot: ease with damped oscillation
580-
const t=Math.min(splitT,1.2);
581-
const spring=1-Math.exp(-6*t)*Math.cos(t*8);
582-
const ease=Math.min(spring,1.3);
583-
584-
// Split with slight outward rotation
585-
topHalf.position.y=origTopY+ease*2.2;
586-
topHalf.rotation.x=ease*.15;
587-
topHalf.rotation.z=ease*.08;
588-
botHalf.position.y=origBotY-ease*2.2;
589-
botHalf.rotation.x=-ease*.12;
590-
botHalf.rotation.z=-ease*.06;
591-
592-
// Body dissolves
593-
body.scale.y=Math.max(0,1-ease*.9);
594-
body.material.opacity=Math.max(0,.94-ease*.94);
595-
596-
// Inner glow ramps up with warmth
597-
innerGlow.intensity=ease*40;
598-
innerGlow.distance=3+ease*12;
599-
600-
// Sparks fly outward
601-
const sp=sparks.geometry.attributes.position.array;
602-
for(let i=0;i<sparkCount;i++){
603-
const v=sparkVel[i];
604-
sp[i*3]+=v.vx;sp[i*3+1]+=v.vy;sp[i*3+2]+=v.vz;
605-
}
606-
sparks.geometry.attributes.position.needsUpdate=true;
607-
sparkMat.opacity=Math.max(0,1-splitT*.6);
608-
609-
// Screen flash: brief white burst then colored glow
610-
if(splitT<.1){
611-
const f=1-splitT/.1;
612-
flashEl.style.background='radial-gradient(circle,rgba(255,255,255,'+f*.7+'),transparent 50%)';
545+
const chosenPill=type==='blue'?bPill:rPill;
546+
const otherPill=type==='blue'?rPill:bPill;
547+
const origZ=chosenPill.position.z;
548+
549+
// Chosen pill: glow → float toward camera → dissolve
550+
let dt=0;
551+
function dissolveAnim(){
552+
dt+=0.02;
553+
const t=Math.min(dt,1);
554+
const ease=1-Math.pow(1-t,3);
555+
556+
// Float toward camera + glow intensely
557+
chosenPill.position.z=origZ+ease*4;
558+
chosenPill.scale.setScalar(1+ease*.3);
559+
chosenPill.children.forEach(m=>{
560+
if(!m.material)return;
561+
m.material.emissiveIntensity=.15+ease*.8;
562+
if(dt>.5) m.material.opacity=Math.max(0,.94-(dt-.5)*1.88);
563+
});
564+
565+
// Unchosen pill: dim down
566+
otherPill.children.forEach(m=>{
567+
if(!m.material)return;
568+
m.material.emissiveIntensity=Math.max(.03,.15-ease*.12);
569+
m.material.opacity=Math.max(.3,.94-ease*.5);
570+
});
571+
otherPill.scale.setScalar(Math.max(.7,1-ease*.3));
572+
573+
// Screen color flash
574+
const flashEl=document.getElementById('pill-flash');
575+
if(dt<.15){
576+
flashEl.style.background='radial-gradient(circle,rgba(255,255,255,'+(1-dt/.15)*.5+'),transparent 50%)';
613577
flashEl.style.opacity='1';
614-
}else if(splitT<.4){
615-
const f=1-(splitT-.1)/.3;
578+
}else if(dt<.5){
616579
const rgb=type==='blue'?'0,68,204':'204,0,0';
617-
flashEl.style.background='radial-gradient(circle,rgba('+rgb+','+f*.3+'),transparent 60%)';
618-
flashEl.style.opacity='1';
580+
flashEl.style.background='radial-gradient(circle,rgba('+rgb+','+(1-(dt-.15)/.35)*.25+'),transparent 60%)';
619581
}else{
620582
flashEl.style.opacity='0';
621583
}
622584

623-
// Fade out everything after split
624-
if(splitT>1){
625-
const fade=Math.max(0,1-(splitT-1)*1.5);
626-
clickedPill.children.forEach(m=>{if(m.material)m.material.opacity=fade*.5});
627-
sparkMat.opacity=fade*.3;
628-
innerGlow.intensity=Math.max(0,innerGlow.intensity*fade);
629-
if(fade<=0){clickedPill.visible=false;return}
630-
}
631-
requestAnimationFrame(splitAnim);
585+
if(dt<1.2) requestAnimationFrame(dissolveAnim);
586+
else chosenPill.visible=false;
632587
}
633-
splitAnim();
588+
dissolveAnim();
634589

635590
// Red pill: shatter video as FIXED OVERLAY during scroll (no waiting)
636591
if(type==='red'){
@@ -666,6 +621,7 @@ let cx=0,cy=0;
666621
const vc=document.getElementById('vid-hero-calm');
667622
const vs=document.getElementById('vid-hero-shatter');
668623
if(vc&&vs){vc.style.opacity='1';vs.style.opacity='0'}
624+
document.getElementById('pill-canvas').style.opacity='0';
669625
resetObs.disconnect();
670626
}
671627
},{threshold:0});
@@ -916,10 +872,9 @@ document.querySelectorAll('.vid-bg').forEach(v=>{
916872
const vCalm=document.getElementById('vid-hero-calm');
917873
const vShatter=document.getElementById('vid-hero-shatter');
918874
if(vCalm&&vShatter){vCalm.style.opacity='1';vShatter.style.opacity='0'}
919-
// Make pills visible again for re-choosing
875+
// Restore both pills to full state
920876
const bp=window._bPill,rp=window._rPill;
921-
if(bp){bp.visible=true;bp.children.forEach(m=>{if(m.material)m.material.opacity=.94})}
922-
if(rp){rp.visible=true;rp.children.forEach(m=>{if(m.material)m.material.opacity=.94})}
877+
[bp,rp].forEach(p=>{if(!p)return;p.visible=true;p.scale.setScalar(1);p.position.z=0;p.children.forEach(m=>{if(m.material){m.material.opacity=.94;m.material.emissiveIntensity=.15}})})
923878
// Scroll back
924879
document.getElementById('act-choice').scrollIntoView({behavior:'smooth'});
925880
// Pulse the red pill after arrival

0 commit comments

Comments
 (0)