feat(motion): raise the cruising speed, and make the show gates follow it
Pieces still read as slow. Cruising speed goes from 120ms per unit to 85: the median departure across the shipped fifty drops 970ms → 763ms and a level's whole animation budget 15.6s → 12.7s, about 18% off. The longest departures do not move — the cap holds them at 3.46s — so if those still drag, the dial is the cap, which was settled separately and is left alone. The gates that guarantee a level opens with something worth watching are stated in milliseconds, and they were picked while a piece crossed a unit in 120ms. Held fixed while the speed changes, they stop meaning what they meant: this change alone made 20 of the 50 shipped levels "fail" an opening gate every one of them had passed, purely from the unscaled number. Scaled to the dial, none fail — the set is unchanged, and the alarm was an artifact of the constant. So the cruise threshold and the opening floor now derive from the speed, naming the speed they were calibrated at. exitTiming takes msPerUnit the way it already took knee and maxMs, and the comparison lab gets a speed slider, reads the levels actually shipping rather than the prototype sweep they were selected from, and totals every route in the campaign instead of the four on screen — those four skew long, where the cap flattens any speed change to 6%. The test that pins constant cruising speed now reads the dial from the token instead of hard-coding 120, so tuning motion no longer looks like a broken guarantee. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+10
-9
@@ -9,7 +9,7 @@ import { center, hexPoints, trackPoints, routePoints, pointAlong } from './geome
|
||||
import { TILE_FRAMES, TILE_RADIUS, TILE_ASPECT } from './sprites.js';
|
||||
export const COLORS=PIGMENTS.map(p=>p.color);
|
||||
// Macaron's exit ends in a bubble dissolve that needs more time than the shared 180 ms effect window.
|
||||
const MACARON_DISSOLVE_MS=560;
|
||||
const MACARON_DISSOLVE_MS=640;
|
||||
export function exitDuration(route,artStyle){const timing=exitTiming(route);return artStyle==='macaron'?timing.travelMs+MACARON_DISSOLVE_MS:timing.duration;}
|
||||
// Four-point sparkle star used by the macaron win celebration.
|
||||
const sparkle=(x,y,r)=>Array.from({length:8},(_,j)=>{const b=j*Math.PI/4-Math.PI/2,rr=j%2?r*.38:r;return {x:x+Math.cos(b)*rr,y:y+Math.sin(b)*rr};});
|
||||
@@ -247,13 +247,14 @@ export function drawExit(p,route,view,progress,tile,reduced=false){
|
||||
}
|
||||
}
|
||||
|
||||
// Macaron exit: the token stops at its port and dissolves like a sugar cube. The solid face, rim and arrow fade in the
|
||||
// first fifth while a lattice of dots in the token colour takes over; the dots then erode from the rim inward, each
|
||||
// shrinking, thinning towards white and drifting a little as it goes. Circles, polygons and alpha only; the noise is a
|
||||
// fixed hash of the lattice position, so every frame agrees.
|
||||
// Macaron exit: the token glides out through its port, slowing down as it crosses the tray edge (0.55s beyond the port
|
||||
// over the first 45%), and dissolves mostly outside the board like a sugar cube: the solid face, rim and arrow fade
|
||||
// while a lattice of dots in the token colour takes over; the dots erode from the rim inward, each shrinking, thinning
|
||||
// towards white and drifting a little. Circles, polygons and alpha only; the noise is a fixed hash of the lattice position.
|
||||
function bubbleDissolve(p,edge,angle,size,tile,color,t){
|
||||
const dx=Math.cos(angle),dy=Math.sin(angle),alphaHex=v=>Math.round(Math.max(0,Math.min(1,v))*255).toString(16).padStart(2,'0'),base=color.slice(0,7);
|
||||
const x=edge.x+dx*size*.06,y=edge.y+dy*size*.06,r=size*.90,solid=1-smoothMotion(t/.18);
|
||||
const out=1-Math.pow(1-Math.min(1,t/.45),3),x=edge.x+dx*size*.55*out,y=edge.y+dy*size*.55*out,r=size*.90;
|
||||
const solid=1-smoothMotion((t-.15)/.2);
|
||||
if(solid>0){const face=roundedHex(x,y,r,.16);p.polygon(face,base+alphaHex(solid));p.line([...face,face[0]],'#FFFFFF'+alphaHex(solid),Math.max(1,r*.10));}
|
||||
const step=size*.17,inset=r*.82,rows=Math.ceil(inset/(step*.866))+1,cols=Math.ceil(inset/step)+1;
|
||||
for(let j=-rows;j<=rows;j++)for(let i=-cols;i<=cols;i++){
|
||||
@@ -262,12 +263,12 @@ function bubbleDissolve(p,edge,angle,size,tile,color,t){
|
||||
let h=(i*374761393+j*668265263)|0;h=Math.imul(h^(h>>>13),1274126177);h^=h>>>16;
|
||||
const n1=((h>>>0)%1000)/1000,n2=(((h>>>10)>>>0)%1000)/1000;
|
||||
const rim=Math.max(ay,ax*.866+ay*.5)/(inset*.866);
|
||||
const u=(t-(.05+.40*(1-rim)+.20*n1))/.33;
|
||||
const u=(t-(.16+.38*(1-rim)+.16*n1))/.28;
|
||||
if(u>=1)continue;
|
||||
if(u<=0){p.circle(x+ox,y+oy,size*.115,base);continue;}
|
||||
if(u<=0){if(solid<1)p.circle(x+ox,y+oy,size*.115,base);continue;}
|
||||
const k=1-smoothMotion(u),drift=size*.14*u,a=n2*Math.PI*2;
|
||||
p.circle(x+ox+Math.cos(a)*drift,y+oy+Math.sin(a)*drift-size*.10*u,size*.115*k,tint(base,.45*u)+alphaHex(.95*(1-u*.6)));
|
||||
}
|
||||
const arrow=1-smoothMotion(t/.3);
|
||||
const arrow=1-smoothMotion((t-.15)/.25);
|
||||
if(arrow>0){const rot=pts=>pts.map(([u,v])=>({x:x+r*(u*Math.cos(angle)-v*Math.sin(angle)),y:y+r*(u*Math.sin(angle)+v*Math.cos(angle))})),w=Math.max(1.5,r*.20),white='#FFFFFF'+alphaHex(arrow);p.line(rot([[-.34,0],[.30,0]]),white,w);p.line(rot([[.02,-.30],[.30,0],[.02,.30]]),white,w);}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user