PageSourceSearch

https://synvox.net/projection-post.ts

js synvox.net collected 2026-10-01 14:03:30 UTC 23,942 bytes, 43 lines download raw bytes

1import p5 from"p5";const ROWS=8;const COLS=10;const PATH_COLOR="#3b82f6";const MESH_URL="/images/projection/views-mesh.json";const turtleProceduralPath=[{r:1,c:1,dir:"right"},{r:1,c:2,dir:"right"},{r:1,c:3,dir:"right"},{r:1,c:4,dir:"down"},{r:2,c:4,dir:"down"},{r:3,c:4,dir:"left"},{r:3,c:3,dir:"left"},{r:3,c:2,dir:"down"},{r:4,c:2,dir:"down"},{r:5,c:2,dir:"right"},{r:5,c:3,dir:"right"},{r:5,c:4,dir:"right"},{r:5,c:5,dir:"right"},{r:5,c:6,dir:"up"},{r:4,c:6,dir:"up"},{r:3,c:6,dir:"up"},{r:2,c:6,dir:"right"},{r:2,c:7,dir:"right"},{r:2,c:8,dir:"down"},{r:3,c:8,dir:"down"},{r:4,c:8,dir:"down"},{r:5,c:8,dir:"down"},{r:6,c:8,dir:"left"},{r:6,c:7,dir:"left"},{r:6,c:6,dir:"left"},{r:6,c:5,dir:"left"},{r:6,c:4,dir:"left"},{r:6,c:3,dir:"left"}];const fogRoute=[{r:1,c:1,dir:"right"},{r:1,c:2,dir:"right"},{r:1,c:3,dir:"right"},{r:1,c:4,dir:"right"},{r:2,c:4,dir:"down"},{r:3,c:4,dir:"down"},{r:4,c:4,dir:"down"},{r:5,c:4,dir:"down"},{r:5,c:5,dir:"right"},{r:5,c:6,dir:"right"},{r:4,c:6,dir:"up"},{r:3,c:6,dir:"up"},{r:2,c:6,dir:"up"},{r:2,c:7,dir:"right"},{r:2,c:8,dir:"right"},{r:3,c:8,dir:"down"},{r:4,c:8,dir:"down"},{r:5,c:8,dir:"down"},{r:6,c:8,dir:"down"}];const fogBlocks=[{afterIndex:3,r:1,c:5},{afterIndex:7,r:6,c:4},{afterIndex:9,r:5,c:7},{afterIndex:12,r:1,c:6},{afterIndex:14,r:2,c:9}];const FOG_START={r:1,c:1};const FOG_FINISH={r:6,c:8};function ensureStylesheet(){const href="/projection-post.css?v=no-shapes-flow-1";const existing=document.querySelector("link[data-projection-css]");if(existing){if(existing.href.indexOf("no-shapes-flow-1")===-1)existing.href=href;return;}const link=document.createElement("link");link.rel="stylesheet";link.href=href;link.dataset.projectionCss="1";document.head.appendChild(link);}function arrowFor(dir){if(dir==="up")return"↑";if(dir==="down")return"↓";if(dir==="left")return"←";return"→";}function cellKey(r,c){return`${r}-${c}`;}function prefersReducedMotion(){try{return window.matchMedia("(prefers-reduced-motion: reduce)").matches;}catch{return false;}}function isDark(){return document.documentElement.classList.contains("dark");}function currentShade(){return isDark()?"white":"metal";}function startFlagSvg(){return`<svg class="turtle-start-flag" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 3v18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M7 4.5h9.5L14 8.5l2.5 4H7z" fill="currentColor"/></svg>`;}function ensureTurtleCells(grid){if(grid.childElementCount===ROWS*COLS){return Array.from(grid.children);}grid.replaceChildren();const cells=[];var _LP1=Date.now();for(let i=0;i<ROWS*COLS;i++){if(Date.now()-_LP1>100)break;const r=Math.floor(i/COLS);const c=i%COLS;const el=document.createElement("div");el.className="turtle-cell";el.dataset.r=String(r);el.dataset.c=String(c);grid.appendChild(el);cells.push(el);}return cells;}function paintTurtleCell(el,paint){const sig=[paint.kind,paint.dir??"",paint.clickable?"1":"0",paint.settled?"1":"0",paint.title??"",String(paint.delayMs??"")].join("|");if(el.dataset.sig===sig)return;const prevKind=(el.dataset.sig??"empty").split("|")[0];el.dataset.sig=sig;el.className="turtle-cell";if(paint.kind==="cleared")el.classList.add("is-cleared");if(paint.kind!=="empty"&&paint.kind!=="cleared")el.classList.add("is-path-slot");if(paint.clickable)el.classList.add("is-clickable");if(paint.title)el.title=paint.title;else el.removeAttribute("title");if(paint.kind==="empty"||paint.kind==="cleared"){el.replaceChildren();return;}const poppable=paint.kind==="path"||paint.kind==="start"||paint.kind==="finish"||paint.kind==="constraint";const shouldPop=poppable&&prevKind!==paint.kind&&!paint.settled;const fill=document.createElement("div");fill.className="turtle-fill is-"+paint.kind;if(paint.kind==="path")fill.style.backgroundColor=PATH_COLOR;if(paint.settled||!shouldPop)fill.classList.add("is-settled");if(typeof paint.delayMs==="number"&&shouldPop){fill.style.animationDelay=paint.delayMs+"ms";}if(paint.kind==="start"){fill.innerHTML=startFlagSvg();}else if(paint.kind==="path"&&paint.dir){const arrow=document.createElement("span");arrow.className="turtle-arrow";arrow.setAttribute("aria-hidden","true");arrow.textContent=arrowFor(paint.dir);fill.appendChild(arrow);}else if(paint.kind==="fog"){const mark=document.createElement("span");mark.className="turtle-fog-mark";mark.setAttribute("aria-hidden","true");mark.textContent="?";fill.appendChild(mark);}else if(paint.kind==="constraint"){const mark=document.createElement("span");mark.className="turtle-constraint-mark";mark.setAttribute("aria-hidden","true");mark.textContent="×";fill.appendChild(mark);}el.replaceChildren(fill);}function wait(ms,signal){return new Promise((resolve,reject)=>{if(signal.aborted){reject(new DOMException("Aborted","AbortError"));return;}const id=window.setTimeout(()=>resolve(),ms);const onAbort=()=>{window.clearTimeout(id);reject(new DOMException("Aborted","AbortError"));};signal.addEventListener("abort",onAbort,{once:true});});}const HOLD_MS=1400;const TWEEN_MS=750;const SPIN_MS=14e3;const FACE_POSES=[{pitch:0,yaw:0},{pitch:0,yaw:-Math.PI/2},{pitch:0,yaw:Math.PI},{pitch:0,yaw:Math.PI/2},{pitch:-Math.PI/2,yaw:0},{pitch:Math.PI/2,yaw:0}];const KEY=normalize([.28,-.42,1]);const FILL_L=normalize([-.65,.05,.55]);const RIM=normalize([.15,.55,-.75]);let meshCache=null;let meshPromise=null;function loadMesh(){if(meshCache)return Promise.resolve(meshCache);
1if(meshPromise)return meshPromise;meshPromise=fetch(MESH_URL).then(r=>{if(!r.ok)throw new Error(`views-mesh.json ${r.status}`);return r.json();}).then(data=>{if(!data?.flat?.length||!data.n){console.error("[projection] mesh payload invalid",data);return null;}meshCache=data;console.info(`[projection] mesh loaded: ${data.n} tris`);return data;}).catch(err=>{console.error("[projection] failed to load mesh",err);return null;});return meshPromise;}function normalize(v){const len=Math.hypot(v[0],v[1],v[2])||1;return[v[0]/len,v[1]/len,v[2]/len];}function lerp(a,b,t){return a+(b-a)*t;}function lerpAngle(a,b,t){let d=b-a;var _LP2=Date.now();while(d>Math.PI){if(Date.now()-_LP2>100)break;d-=Math.PI*2;}var _LP3=Date.now();while(d<-Math.PI){if(Date.now()-_LP3>100)break;d+=Math.PI*2;}return a+d*t;}function easeInOut(t){return t<.5?2*t*t:1-Math.pow(-2*t+2,2)/2;}function toWorldNormal(n,pitch,yaw){const cp=Math.cos(pitch);const sp=Math.sin(pitch);const cy=Math.cos(yaw);const sy=Math.sin(yaw);const x1=n[0];const y1=n[1]*cp-n[2]*sp;const z1=n[1]*sp+n[2]*cp;return[x1*cy+z1*sy,y1,-x1*sy+z1*cy];}function shadeLit(nView){const key=Math.max(0,nView[0]*KEY[0]+nView[1]*KEY[1]+nView[2]*KEY[2]);const fill=Math.max(0,nView[0]*FILL_L[0]+nView[1]*FILL_L[1]+nView[2]*FILL_L[2]);const rim=Math.max(0,nView[0]*RIM[0]+nView[1]*RIM[1]+nView[2]*RIM[2]);const head=Math.max(0,nView[2]);return Math.min(1,.16+.55*key+.28*fill+.16*rim+.22*head);}function shadeWhite(nView){const v=Math.round(255*shadeLit(nView));return[v,v,v];}function shadeGray(nView){const lit=shadeLit(nView);const v=Math.round(42+(168-42)*lit);return[v,v,v];}function shadeMetal(nView){const key=Math.max(0,nView[0]*KEY[0]+nView[1]*KEY[1]+nView[2]*KEY[2]);const fill=Math.max(0,nView[0]*FILL_L[0]+nView[1]*FILL_L[1]+nView[2]*FILL_L[2]);const rim=Math.max(0,nView[0]*RIM[0]+nView[1]*RIM[1]+nView[2]*RIM[2]);const facing=Math.max(0,nView[2]);const body=.1+.16*key+.08*fill+.06*rim+.48*Math.pow(facing,1.05);const lo=16;const hi=112;const specKey=Math.pow(key,40)*1.45;const specCam=Math.pow(facing,12)*1.15;const rimCatch=Math.pow(1-facing,2.1)*rim*.45;const v=Math.min(255,Math.round(lo+(hi-lo)*body+230*specKey+185*specCam+55*rimCatch));const hiMix=Math.min(1,v/200);return[Math.min(255,Math.round(v*(.82+.14*hiMix))),Math.min(255,Math.round(v*(.86+.12*hiMix))),Math.min(255,Math.round(v*(.94+.06*hiMix)))];}function drawStlMesh(sketch,mesh,pitch,yaw,shade){sketch.push();sketch.noStroke();const flat=mesh.flat;const shadeFn=shade==="metal"?shadeMetal:shade==="gray"?shadeGray:shadeWhite;var _LP4=Date.now();for(let i=0;i<mesh.n;i++){if(Date.now()-_LP4>100)break;const o=i*12;const nLocal=[flat[o],flat[o+1],flat[o+2]];const n=toWorldNormal(nLocal,pitch,yaw);const[r,g,b]=shadeFn(n);sketch.fill(r,g,b);sketch.beginShape(sketch.TRIANGLES);sketch.vertex(flat[o+3],flat[o+4],flat[o+5]);sketch.vertex(flat[o+6],flat[o+7],flat[o+8]);sketch.vertex(flat[o+9],flat[o+10],flat[o+11]);sketch.endShape();}sketch.pop();}function poseAt(elapsedMs){const cycle=HOLD_MS+TWEEN_MS;const total=cycle*FACE_POSES.length;const t=(elapsedMs%total+total)%total;const i=Math.floor(t/cycle);const local=t-i*cycle;const from=FACE_POSES[i];const to=FACE_POSES[(i+1)%FACE_POSES.length];if(local<=HOLD_MS)return from;const u=easeInOut((local-HOLD_MS)/TWEEN_MS);return{pitch:lerp(from.pitch,to.pitch,u),yaw:lerpAngle(from.yaw,to.yaw,u)};}function spinAt(elapsedMs){return{pitch:-.28,yaw:elapsedMs%SPIN_MS/SPIN_MS*Math.PI*2};}function mountViewsTumble(host,signal,options={}){const motion=options.motion??"face-tour";let shade=options.shade??currentShade();const revealAt=options.revealAtBuild??0;const cameraDist=options.cameraDist??640;let build=options.initialBuild??0;let sketchRef=null;let opacity=build>=revealAt?1:0;let mesh=null;let animStartedAt=null;const reduced=prefersReducedMotion();void loadMesh().then(m=>{if(signal.aborted)return;mesh=m;if(!m)options.onMeshError?.("Could not load the 3D mesh.");});const instance=new p5(sketch=>{sketchRef=sketch;sketch.setup=()=>{const w=Math.max(1,host.clientWidth);const h=Math.max(1,host.clientHeight);const canvas=sketch.createCanvas(w,h,sketch.WEBGL);canvas.parent(host);sketch.pixelDensity(Math.min(window.devicePixelRatio||1,2));const el=canvas.elt;el.style.display="block";el.style.width="100%";el.style.height="100%";};sketch.draw=()=>{const target=build>=revealAt?1:0;if(reduced)opacity=target;else{opacity+=(target-opacity)*.08;if(Math.abs(target-opacity)<.004)opacity=target;}sketch.clear();if(opacity<.01||!mesh)return;if(build>=revealAt&&animStartedAt===null){animStartedAt=sketch.millis();}if(build<revealAt)animStartedAt=null;let pitch;let yaw;if(reduced||build<revealAt||animStartedAt===null){pitch=FACE_POSES[0].pitch;yaw=FACE_POSES[0].yaw;}else if(motion==="spin"){const pose=spinAt(sketch.millis()-animStartedAt);pitch=pose.pitch;yaw=pose.yaw;}else{const pose=poseAt(sketch.millis()-animStartedAt);pitch=pose.pitch;yaw=pose.yaw;}sketch.camera(0,0,cameraDist,0,0,0,0,1,0);sketch.push();sketch.rotateX(pitch);sketch.rotateY(yaw);drawStlMesh(sketch,mesh,pitch,yaw,shade);sketch.pop();};});const ro=new ResizeObserver(()=>{if(!sketchRef)return;sketchRef.resizeCanvas(Math.max(1,host.clientWidth),Math.max(1,host.clientHeight));});ro.observe(host);const onAbort=()=>{ro.disconnect();instance.remove();};signal.addEventListener("abort",onAbort);return{setBuild(next){build=next;if(next<revealAt)animStartedAt=null;},setShade(next){shade=next;},dispose:onAbort};}function mountViewsWidget(root,signal){root.classList.add("projection-widget");root.innerHTML=`
2    <p class="projection-widget__hint"><strong>Play with it.</strong> Three orthographic views. Combine them into the solid they imply.</p>
3    <div class="views-stage">
4      <div class="views-marks" data-views-marks>
5        <div class="views-mark">
6          <div class="views-mark__shape" aria-hidden="true">
7            <svg viewBox="0 0 120 120"><rect fill="currentColor" height="84" width="84" x="18" y="18" /></svg>
8          </div>
9          <span class="views-mark__label">Front □</span>
10        </div>
11        <div class="views-mark">
12          <div class="views-mark__shape" aria-hidden="true">
13            <svg viewBox="0 0 120 120"><polygon fill="currentColor" points="60,18 102,102 18,102" /></svg>
14          </div>
15          <span class="views-mark__label">Side △</span>
16        </div>
17        <div class="views-mark">
18          <div class="views-mark__shape" aria-hidden="true">
19            <svg viewBox="0 0 120 120"><circle cx="60" cy="60" fill="currentColor" r="42" /></svg>
20          </div>
21          <span class="views-mark__label">Top ○</span>
22        </div>
23      </div>
24      <div class="views-host is-hidden" data-views-host></div>
25      <p class="views-error" data-views-error hidden></p>
26      <div class="projection-widget__actions">
27        <button type="button" class="projection-btn" data-views-reveal>Combine into a solid</button>
28        <button type="button" class="projection-btn projection-btn--ghost" data-views-spin hidden>Spin / face tour</button>
29      </div>
30    </div>
31  `;const host=root.querySelector("[data-views-host]");const errEl=root.querySelector("[data-views-error]");const revealBtn=root.querySelector("[data-views-reveal]");const spinBtn=root.querySelector("[data-views-spin]");const marks=root.querySelector("[data-views-marks]");let tumble=null;let motion="face-tour";let revealed=false;const showError=msg=>{errEl.hidden=false;errEl.textContent=msg;};const syncShade=()=>{tumble?.setShade(currentShade());};const themeObserver=new MutationObserver(syncShade);themeObserver.observe(document.documentElement,{attributes:true,attributeFilter:["class"]});signal.addEventListener("abort",()=>themeObserver.disconnect());const reveal=()=>{if(revealed)return;revealed=true;host.classList.remove("is-hidden");marks.classList.add("is-revealed");revealBtn.disabled=true;revealBtn.textContent="Solid revealed";spinBtn.hidden=false;requestAnimationFrame(()=>{if(signal.aborted)return;void host.offsetWidth;tumble=mountViewsTumble(host,signal,{motion,shade:currentShade(),revealAtBuild:0,initialBuild:1,cameraDist:640,onMeshError:showError});});};revealBtn.addEventListener("click",reveal);spinBtn.addEventListener("click",()=>{motion=motion==="spin"?"face-tour":"spin";spinBtn.textContent=motion==="spin"?"Switch to face tour":"Switch to spin";tumble?.dispose();host.innerHTML="";tumble=mountViewsTumble(host,signal,{motion,shade:currentShade(),revealAtBuild:0,initialBuild:1,cameraDist:640,onMeshError:showError});});}function mountTurtleProcedural(root,signal){root.classList.add("projection-widget");root.innerHTML=`
32    <p class="projection-widget__hint"><strong>Watch the path.</strong> A clean route with no constraints. Pretty. Wrong analogy for product work.</p>
33    <div class="turtle-grid-wrap"><div class="turtle-grid" data-turtle-grid role="img" aria-label="A development path with no constraints"></div></div>
34  `;const grid=root.querySelector("[data-turtle-grid]");const pathMap=new Map(turtleProceduralPath.map((step,index)=>[cellKey(step.r,step.c),{step,index}]));let visible=0;let runId=0;const cells=ensureTurtleCells(grid);const render=()=>{var _LP5=Date.now();for(let i=0;i<ROWS*COLS;i++){if(Date.now()-_LP5>100)break;const r=Math.floor(i/COLS);const c=i%COLS;const hit=pathMap.get(cellKey(r,c));if(!hit||hit.index>=visible){paintTurtleCell(cells[i],{kind:"empty"});continue;}if(hit.index===0){paintTurtleCell(cells[i],{kind:"start"});}else if(hit.index===turtleProceduralPath.length-1){paintTurtleCell(cells[i],{kind:"finish"});}else{paintTurtleCell(cells[i],{kind:"path",dir:hit.step.dir});}}};const play=async thisRun=>{visible=0;render();try{await wait(320,signal);for(let i=0;i<turtleProceduralPath.length;i++){if(signal.aborted||thisRun!==runId)return;visible=i+1;render();await wait(prefersReducedMotion()?0:90,signal);}await wait(1800,signal);if(signal.aborted||thisRun!==runId)return;visible=0;render();await wait(600,signal);if(signal.aborted||thisRun!==runId)return;void play(thisRun);}catch{}};const start=()=>{runId+=1;void play(runId);};const io=new IntersectionObserver(entries=>{if(entries.some(e=>e.isIntersecting)){start();
34io.disconnect();}},{threshold:.3});io.observe(root);signal.addEventListener("abort",()=>{runId+=1;io.disconnect();});}function neighbors(r,c){return[[r-1,c],[r+1,c],[r,c-1],[r,c+1]].filter(([nr,nc])=>nr>=0&&nc>=0&&nr<ROWS&&nc<COLS);}function mountTurtleFog(root,signal){root.classList.add("projection-widget");root.innerHTML=`
35    <p class="projection-widget__hint"><strong>Walk the fog.</strong> Constraints appear as \xd7 marks. Each one forces a turn. That is closer to how a product path actually unfolds.</p>
36    <div class="turtle-grid-wrap"><div class="turtle-grid" data-turtle-grid role="img" aria-label="A product path discovered through fog and constraints"></div></div>
37  `;const grid=root.querySelector("[data-turtle-grid]");const blockKeys=new Set(fogBlocks.map(b=>cellKey(b.r,b.c)));let showStart=false;let pathLen=0;let revealedBlocks=new Set;let cleared=new Set;let runId=0;const clearAround=(r,c)=>{cleared.add(cellKey(r,c));var _LP6=Date.now();for(const[nr,nc]of neighbors(r,c)){if(Date.now()-_LP6>100)break;const k=cellKey(nr,nc);if(!blockKeys.has(k)||revealedBlocks.has(k))cleared.add(k);}};const reset=()=>{showStart=false;pathLen=0;revealedBlocks=new Set;cleared=new Set;};const cells=ensureTurtleCells(grid);const render=()=>{const visiblePath=fogRoute.slice(0,pathLen);const visiblePathMap=new Map(visiblePath.map((step,index)=>[cellKey(step.r,step.c),{step,index}]));var _LP7=Date.now();for(let i=0;i<ROWS*COLS;i++){if(Date.now()-_LP7>100)break;const r=Math.floor(i/COLS);const c=i%COLS;const k=cellKey(r,c);const hit=visiblePathMap.get(k);if(revealedBlocks.has(k)){paintTurtleCell(cells[i],{kind:"constraint",title:"constraint"});continue;}if(hit){const showFinish=r===FOG_FINISH.r&&c===FOG_FINISH.c&&pathLen>=fogRoute.length;if(hit.index===0){paintTurtleCell(cells[i],{kind:"start"});}else if(showFinish){paintTurtleCell(cells[i],{kind:"finish"});}else{paintTurtleCell(cells[i],{kind:"path",dir:hit.step.dir});}continue;}if(cleared.has(k)){paintTurtleCell(cells[i],{kind:"cleared"});continue;}paintTurtleCell(cells[i],{kind:"fog"});}};const play=async thisRun=>{reset();render();try{await wait(280,signal);if(thisRun!==runId)return;showStart=true;clearAround(FOG_START.r,FOG_START.c);pathLen=1;render();await wait(480,signal);for(let i=1;i<fogRoute.length;i++){if(signal.aborted||thisRun!==runId)return;const block=fogBlocks.find(b=>b.afterIndex===i-1);if(block){const bk=cellKey(block.r,block.c);revealedBlocks.add(bk);cleared.add(bk);render();await wait(520,signal);if(signal.aborted||thisRun!==runId)return;}const step=fogRoute[i];pathLen=i+1;clearAround(step.r,step.c);render();await wait(prefersReducedMotion()?0:140,signal);}await wait(2200,signal);if(signal.aborted||thisRun!==runId)return;reset();render();await wait(700,signal);if(signal.aborted||thisRun!==runId)return;void play(thisRun);}catch{}};const io=new IntersectionObserver(entries=>{if(!entries.some(e=>e.isIntersecting))return;runId+=1;void play(runId);io.disconnect();},{threshold:.3});io.observe(root);signal.addEventListener("abort",()=>{runId+=1;io.disconnect();});}const REROUTE_START={r:1,c:1};const REROUTE_FINISH={r:6,c:8};const REROUTE_BASE_WALLS=new Set(["0-8","1-5","1-6","1-8","2-2","2-6","3-2","3-7","5-2","5-3","5-4","5-7","6-3","7-2","7-5"]);function bfsPath(walls,start,finish){const startK=cellKey(start.r,start.c);const finishK=cellKey(finish.r,finish.c);if(walls.has(startK)||walls.has(finishK))return null;const dirs=[{dr:-1,dc:0,dir:"up"},{dr:1,dc:0,dir:"down"},{dr:0,dc:-1,dir:"left"},{dr:0,dc:1,dir:"right"}];const prev=new Map;const queue=[start];const seen=new Set([startK]);var _LP9=Date.now();while(queue.length){if(Date.now()-_LP9>100)break;const cur=queue.shift();const ck=cellKey(cur.r,cur.c);if(ck===finishK)break;var _LP8=Date.now();for(const d of dirs){if(Date.now()-_LP8>100)break;const nr=cur.r+d.dr;const nc=cur.c+d.dc;if(nr<0||nc<0||nr>=ROWS||nc>=COLS)continue;const nk=cellKey(nr,nc);if(seen.has(nk)||walls.has(nk))continue;seen.add(nk);prev.set(nk,{k:ck,dir:d.dir});queue.push({r:nr,c:nc});}}if(!seen.has(finishK)&&startK!==finishK)return null;const steps=[];let cursor=finishK;const chain=[];var _LP10=Date.now();while(cursor!==startK){if(Date.now()-_LP10>100)break;const p=prev.get(cursor);if(!p)return null;const[r,c]=cursor.split("-").map(Number);chain.push({r,c,dir:p.dir});
37cursor=p.k;}chain.reverse();steps.push({r:start.r,c:start.c,dir:chain[0]?.dir??"right"});var _LP11=Date.now();for(const step of chain){if(Date.now()-_LP11>100)break;steps.push(step);}var _LP12=Date.now();for(let i=0;i<steps.length-1;i++){if(Date.now()-_LP12>100)break;const a=steps[i];const b=steps[i+1];if(b.r<a.r)a.dir="up";else if(b.r>a.r)a.dir="down";else if(b.c<a.c)a.dir="left";else a.dir="right";}if(steps.length>1)steps[steps.length-1].dir=steps[steps.length-2].dir;return steps;}function mountTurtleReroute(root,signal){root.classList.add("projection-widget");root.innerHTML=`
38    <p class="projection-widget__hint"><strong>Try it.</strong> Click an empty cell to drop a constraint. Watch the product path reroute. Click again to clear extra walls.</p>
39    <div class="turtle-grid-wrap"><div class="turtle-grid" data-turtle-grid role="img" aria-label="Click to place a constraint and reroute the path"></div></div>
40    <div class="projection-widget__actions">
41      <button type="button" class="projection-btn projection-btn--ghost" data-reroute-reset>Reset walls</button>
42    </div>
43  `;const grid=root.querySelector("[data-turtle-grid]");const resetBtn=root.querySelector("[data-reroute-reset]");const extraWalls=new Set;let path=[];let animToken=0;const allWalls=()=>new Set([...REROUTE_BASE_WALLS,...extraWalls]);const recompute=()=>{const next=bfsPath(allWalls(),REROUTE_START,REROUTE_FINISH);path=next??[];};const cells=ensureTurtleCells(grid);const render=(settle=false)=>{const pathMap=new Map(path.map((step,index)=>[cellKey(step.r,step.c),{step,index}]));const walls=allWalls();var _LP13=Date.now();for(let i=0;i<ROWS*COLS;i++){if(Date.now()-_LP13>100)break;const r=Math.floor(i/COLS);const c=i%COLS;const k=cellKey(r,c);const isWall=walls.has(k);const isExtra=extraWalls.has(k);const hit=pathMap.get(k);const isStart=r===REROUTE_START.r&&c===REROUTE_START.c;const isFinish=r===REROUTE_FINISH.r&&c===REROUTE_FINISH.c;if(isWall){paintTurtleCell(cells[i],{kind:"constraint",clickable:isExtra,settled:true,title:"constraint"});continue;}if(hit){if(isStart){paintTurtleCell(cells[i],{kind:"start",clickable:true,settled:settle});}else if(isFinish){paintTurtleCell(cells[i],{kind:"finish",clickable:true,settled:settle});}else{paintTurtleCell(cells[i],{kind:"path",dir:hit.step.dir,clickable:true,settled:settle,delayMs:settle?0:hit.index*70});}continue;}paintTurtleCell(cells[i],{kind:"empty",clickable:true});}};const animatePath=()=>{animToken+=1;const token=animToken;recompute();const full=path.slice();path=[];render(true);let i=0;const step=()=>{if(signal.aborted||token!==animToken)return;if(i>=full.length){path=full;render(true);return;}path=full.slice(0,i+1);render(false);i+=1;window.setTimeout(step,prefersReducedMotion()?0:55);};step();};grid.addEventListener("click",ev=>{const target=ev.target.closest("[data-r]");if(!target)return;const r=Number(target.dataset.r);const c=Number(target.dataset.c);const k=cellKey(r,c);if(REROUTE_BASE_WALLS.has(k))return;if(r===REROUTE_START.r&&c===REROUTE_START.c||r===REROUTE_FINISH.r&&c===REROUTE_FINISH.c){return;}if(extraWalls.has(k))extraWalls.delete(k);else extraWalls.add(k);animatePath();});resetBtn.addEventListener("click",()=>{extraWalls.clear();animatePath();});const io=new IntersectionObserver(entries=>{if(!entries.some(e=>e.isIntersecting))return;animatePath();io.disconnect();},{threshold:.25});io.observe(root);signal.addEventListener("abort",()=>{animToken+=1;io.disconnect();});}function mountAll(){ensureStylesheet();const ac=new AbortController;document.querySelectorAll("[data-projection-views]").forEach(el=>{mountViewsWidget(el,ac.signal);});document.querySelectorAll("[data-projection-turtle]").forEach(el=>{mountTurtleProcedural(el,ac.signal);});document.querySelectorAll("[data-projection-fog]").forEach(el=>{mountTurtleFog(el,ac.signal);});document.querySelectorAll("[data-projection-reroute]").forEach(el=>{mountTurtleReroute(el,ac.signal);});}if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",mountAll,{once:true});}else{mountAll();}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.