Spaces:
Running on Zero
Running on Zero
Download cat-sun-scene.html from KwabsHug/VisualLearningIdea: direct link, hf CLI and curl.
- Browser
- Download file 27 kB
-
https://huggingface.co/spaces/KwabsHug/VisualLearningIdea/resolve/main/cat-sun-scene.html
- Command line
-
hf download hf://spaces/KwabsHug/VisualLearningIdea/cat-sun-scene.html
-
curl -L -o cat-sun-scene.html https://huggingface.co/spaces/KwabsHug/VisualLearningIdea/resolve/main/cat-sun-scene.html
27 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>The cat sat and the sun shone — serial reading + scene</title> | |
| <style> | |
| :root{--bg:#0e1116;--panel:#151a22e6;--border:#2a3242;--text:#e8ecf4;--muted:#93a0b4;--accent:#ffc857;--accent2:#7ee0a3;--warn:#f0b429} | |
| *{box-sizing:border-box;margin:0} | |
| body{background:var(--bg);color:var(--text);font:14px/1.55 -apple-system,"Segoe UI",Roboto,sans-serif;overflow:hidden} | |
| #scene{position:fixed;inset:0} | |
| #panel{position:fixed;top:16px;left:16px;width:352px;max-height:calc(100vh - 32px);overflow-y:auto;background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:14px;backdrop-filter:blur(8px);z-index:10} | |
| h1{font-size:15px;margin-bottom:6px} | |
| .sub{color:var(--muted);font-size:12px;margin-bottom:10px} | |
| .tabs{display:flex;gap:6px;margin-bottom:10px} | |
| .tab{flex:1;text-align:center;padding:6px 0;border:1px solid var(--border);border-radius:8px;font-size:12.5px;cursor:pointer;color:var(--muted)} | |
| .tab.active{color:var(--accent);border-color:var(--accent);background:#ffc85714} | |
| .view{display:none} | |
| .view.active{display:block} | |
| .cap{min-height:48px;font-size:12.5px;background:#ffc85710;border:1px solid var(--border);border-radius:8px;padding:8px 10px;margin-bottom:10px} | |
| .cap b{color:var(--accent)} | |
| .btnrow{display:flex;gap:8px;margin-bottom:10px} | |
| button{flex:1;background:var(--panel);border:1px solid var(--border);color:var(--text);border-radius:8px;padding:7px 0;font-size:12.5px;cursor:pointer} | |
| button:hover{border-color:var(--accent)} | |
| button.primary{background:#ffc85722;border-color:var(--accent)} | |
| button:disabled{opacity:.4;cursor:default} | |
| .section{margin-bottom:10px} | |
| .label{font-size:11px;letter-spacing:1px;text-transform:uppercase;color:var(--accent2);margin-bottom:6px} | |
| .ledger{display:flex;flex-direction:column;gap:4px} | |
| .item{font-size:12px;padding:4px 8px;border-radius:6px;border:1px solid transparent;opacity:.38;transition:all .25s} | |
| .item b{color:var(--accent)} | |
| .item .alt{color:var(--muted);font-size:11px} | |
| .item.done{opacity:1;border-color:var(--border)} | |
| .item.active{opacity:1;border-color:var(--accent);background:#ffc85714} | |
| .unres{border-left:3px solid var(--warn);padding-left:10px} | |
| .unres div{color:var(--muted);font-size:12px;padding:2px 0} | |
| .ctrl{margin:8px 0} | |
| .ctrl label{display:flex;justify-content:space-between;font-size:12px;color:var(--muted);margin-bottom:3px} | |
| input[type=range]{width:100%;accent-color:var(--accent)} | |
| select{background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:12px;width:100%} | |
| .note{font-size:11px;color:var(--muted);border-top:1px solid var(--border);padding-top:8px;margin-top:4px} | |
| #hud{position:fixed;bottom:12px;right:16px;color:var(--muted);font-size:11px;background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:6px 10px} | |
| /* ---- serial reading: full-bleed linear timeline, starts right of sidebar ---- */ | |
| #reading{position:fixed;inset:0;display:none;overflow-x:auto;overflow-y:auto;padding:24px 40px 40px 396px} | |
| #reading.active{display:block} | |
| .railhead{margin-bottom:16px;color:var(--muted);font-size:12px} | |
| .railhead b{color:var(--accent2)} | |
| .rail{display:flex;align-items:flex-start;min-width:max-content} | |
| .tokwrap{display:flex;flex-direction:column;align-items:center;position:relative;padding:0 16px} | |
| .tokwrap:not(:last-child)::after{content:'';position:absolute;top:26px;left:calc(50% + 30px);width:calc(100% - 60px);height:2px;background:var(--border)} | |
| .tokwrap.committed:not(:last-child)::after{background:var(--accent2)} | |
| .tok{width:52px;height:52px;border-radius:50%;border:2px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:12.5px;color:var(--muted);background:var(--panel);position:relative;z-index:1;transition:all .3s} | |
| .tok.committed{border-color:var(--accent2);color:var(--text)} | |
| .tok.active{border-color:var(--accent);color:var(--accent);background:#ffc85722;box-shadow:0 0 18px #ffc85744} | |
| .tok.q::before{content:'?';position:absolute;top:-8px;right:-8px;background:var(--warn);color:#000;font-size:10px;font-weight:700;width:16px;height:16px;border-radius:50%;display:flex;align-items:center;justify-content:center} | |
| .model{width:150px;height:112px;margin-top:10px;border:1px solid var(--border);border-radius:10px;background:#10131a;object-fit:cover;display:block} | |
| .model.qbox{display:flex;align-items:center;justify-content:center;color:var(--warn);font-size:40px;font-weight:700} | |
| .word{margin-top:8px;font-size:16px;color:var(--muted)} | |
| .tokwrap.committed .word{color:var(--text)} | |
| .tokwrap.active .word{color:var(--accent)} | |
| .map{margin-top:4px;font-size:11.5px;color:var(--accent2);text-align:center;max-width:150px} | |
| .commits{margin-top:8px;display:flex;flex-direction:column;gap:5px;max-width:150px} | |
| .chip{font-size:10.5px;border:1px solid var(--border);border-radius:6px;padding:3px 7px;color:var(--muted);text-align:center;opacity:.35;transition:all .3s} | |
| .chip.on{opacity:1;border-color:var(--accent2);color:var(--text)} | |
| .chip.rel{border-color:#7eb8e0} | |
| .chip.q{border-color:var(--warn);color:var(--warn)} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="scene"></div> | |
| <div id="reading"> | |
| <div class="railhead"><b>SERIAL READING</b> — linear, no camera. Each word shows the model it commits (snapshotted from the same geometry the Scene tab uses). The sidebar no longer overlaps: the timeline starts to its right.</div> | |
| <div class="rail" id="rail"></div> | |
| </div> | |
| <div id="panel"> | |
| <h1>“The cat sat and the sun shone”</h1> | |
| <div class="sub">two independent steppers — serial reading (default) and scene</div> | |
| <div class="tabs"> | |
| <div class="tab" id="tabScene">🌐 Scene</div> | |
| <div class="tab active" id="tabSerial">📏 Serial reading</div> | |
| </div> | |
| <!-- SERIAL VIEW --> | |
| <div class="view active" id="viewSerial"> | |
| <div class="cap" id="capSerial">Press <b>▶ step</b> — each word commits its models and items in reading order.</div> | |
| <div class="btnrow"><button id="rPrev">◀</button><button id="rNext" class="primary">▶ step</button><button id="rAuto">⏩ auto</button></div> | |
| <div class="note">Each word card in the timeline shows the <b>model snapshot</b> its commitment produces. Determiners (“the”) commit nothing — they stay <b style="color:var(--warn)">?</b>. “sat” is the compression star: one word, three commits.</div> | |
| </div> | |
| <!-- SCENE VIEW --> | |
| <div class="view" id="viewScene"> | |
| <div class="cap" id="capScene">Scene stepper is independent — press <b>▶ step</b> to build the configuration item by item.</div> | |
| <div class="btnrow"><button id="sPrev">◀</button><button id="sNext" class="primary">▶ step</button><button id="sAuto">⏩ auto</button></div> | |
| <div class="section"> | |
| <div class="label">Ledger</div> | |
| <div class="ledger" id="ledger"> | |
| <div class="item" data-i="1"><b>1 · ground_plane</b> — grass (implied by “sat”)</div> | |
| <div class="item" data-i="2"><b>2 · form: cat</b> — orange tabby, stylized</div> | |
| <div class="item" data-i="3"><b>3 · pose: sat</b> — haunches down, tail curled</div> | |
| <div class="item" data-i="4"><b>4 · position</b> — left of center (frame decision)</div> | |
| <div class="item" data-i="5"><b>5 · orient</b> — facing the sun (derived from 7)</div> | |
| <div class="item" data-i="6"><b>6 · light_source: sun</b> — disc + warm light</div> | |
| <div class="item" data-i="7"><b>7 · under-light-of</b> — coupling: shadow computed</div> | |
| <div class="item" data-i="8"><b>8 · supported-by</b> — contact: haunches + paws</div> | |
| </div> | |
| </div> | |
| <div class="section"> | |
| <div class="label">Unresolved</div> | |
| <div class="unres"> | |
| <div>· “the (cat)” — <b style="color:var(--warn)">?</b> defaulted: tabby</div> | |
| <div>· “the (sun)” — <b style="color:var(--warn)">?</b> defaulted: late afternoon</div> | |
| <div>· ground material? — defaulted: grass</div> | |
| <div>· aware of the sun? — defaulted: yes</div> | |
| </div> | |
| </div> | |
| <div class="section"> | |
| <div class="label">Refinement (post-serial)</div> | |
| <div class="ctrl"><label>Sun elevation <span id="elevV">35°</span></label><input type="range" id="elev" min="5" max="85" value="35"></div> | |
| <div class="ctrl"><label>Sun azimuth <span id="azV">-40°</span></label><input type="range" id="az" min="-180" max="180" value="-40"></div> | |
| <div class="ctrl"><label>Cat orientation</label> | |
| <select id="facing"> | |
| <option value="toward">facing the sun</option> | |
| <option value="away">back to the sun</option> | |
| <option value="profile">profile to the sun</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="hud">drag = orbit · scroll = zoom · ◀ ▶ keys step the active tab</div> | |
| <script type="importmap"> | |
| {"imports":{ | |
| "three":"https://unpkg.com/three@0.160.0/build/three.module.js", | |
| "three/addons/":"https://unpkg.com/three@0.160.0/examples/jsm/" | |
| }} | |
| </script> | |
| <script type="module"> | |
| import * as THREE from 'three'; | |
| import {OrbitControls} from 'three/addons/controls/OrbitControls.js'; | |
| /* ================= reusable model builders (shared by scene + snapshots) ================= */ | |
| function matFur(){ return new THREE.MeshStandardMaterial({color:0xd9822b, roughness:0.9}); } | |
| function matDark(){ return new THREE.MeshStandardMaterial({color:0xb5641c, roughness:0.9}); } | |
| function shadowify(g){ g.traverse(o=>{ if(o.isMesh) o.castShadow = true; }); return g; } | |
| function buildGround(){ | |
| const g = new THREE.Mesh(new THREE.CircleGeometry(40, 64), | |
| new THREE.MeshStandardMaterial({color:0x4d7c3a, roughness:1})); | |
| g.rotation.x = -Math.PI/2; g.receiveShadow = true; return g; | |
| } | |
| function buildGroundSmall(){ | |
| const g = new THREE.Mesh(new THREE.CircleGeometry(3.2, 48), | |
| new THREE.MeshStandardMaterial({color:0x4d7c3a, roughness:1})); | |
| g.rotation.x = -Math.PI/2; g.receiveShadow = true; return g; | |
| } | |
| function buildForm(){ | |
| const fur = matFur(), furDark = matDark(); | |
| const g = new THREE.Group(); | |
| const body = new THREE.Mesh(new THREE.SphereGeometry(0.75, 20, 16), fur); | |
| body.scale.set(1, 1.25, 0.9); body.position.y = 0.85; g.add(body); | |
| const chest = new THREE.Mesh(new THREE.SphereGeometry(0.5, 18, 14), fur); | |
| chest.scale.set(0.9, 1.05, 0.7); chest.position.set(0, 0.95, 0.25); g.add(chest); | |
| const head = new THREE.Mesh(new THREE.SphereGeometry(0.42, 20, 16), fur); | |
| head.scale.set(1, 0.92, 0.95); head.position.set(0, 1.95, 0.18); g.add(head); | |
| [-0.28, 0.28].forEach(x=>{ | |
| const ear = new THREE.Mesh(new THREE.ConeGeometry(0.13, 0.3, 4), furDark); | |
| ear.position.set(x, 2.32, 0.15); g.add(ear); | |
| }); | |
| [-0.16, 0.16].forEach(x=>{ | |
| const eye = new THREE.Mesh(new THREE.SphereGeometry(0.055, 10, 10), | |
| new THREE.MeshStandardMaterial({color:0x222a18, emissive:0x0a1400})); | |
| eye.position.set(x, 2.0, 0.52); g.add(eye); | |
| }); | |
| for(let i=0;i<3;i++){ | |
| const s = new THREE.Mesh(new THREE.TorusGeometry(0.72, 0.035, 6, 24, Math.PI*0.7), furDark); | |
| s.position.set(0, 0.75+i*0.32, 0); s.rotation.x = Math.PI/2; s.rotation.z = Math.PI*0.15; g.add(s); | |
| } | |
| return shadowify(g); | |
| } | |
| function buildPose(){ | |
| const fur = matFur(), furDark = matDark(); | |
| const g = new THREE.Group(); | |
| [-0.62, 0.62].forEach(x=>{ | |
| const h = new THREE.Mesh(new THREE.SphereGeometry(0.34, 16, 12), fur); | |
| h.scale.set(0.8, 1.1, 1.1); h.position.set(x, 0.5, -0.15); g.add(h); | |
| }); | |
| [-0.28, 0.28].forEach(x=>{ | |
| const paw = new THREE.Mesh(new THREE.CapsuleGeometry(0.13, 0.3, 6, 10), fur); | |
| paw.position.set(x, 0.16, 0.45); g.add(paw); | |
| }); | |
| const curve = new THREE.CatmullRomCurve3([ | |
| new THREE.Vector3(0.15,0.55,-0.55), new THREE.Vector3(0.75,0.18,-0.25), | |
| new THREE.Vector3(0.85,0.1,0.35), new THREE.Vector3(0.35,0.08,0.6)]); | |
| g.add(new THREE.Mesh(new THREE.TubeGeometry(curve, 32, 0.09, 8, false), furDark)); | |
| return shadowify(g); | |
| } | |
| function buildSupport(){ | |
| const m = new THREE.MeshBasicMaterial({color:0x7ee0a3, transparent:true, opacity:0.75}); | |
| const g = new THREE.Group(); | |
| [[-0.28,0.45,0.14,0.2],[0.28,0.45,0.14,0.2],[-0.62,-0.15,0.2,0.27],[0.62,-0.15,0.2,0.27]].forEach(([x,z,r1,r2])=>{ | |
| const ring = new THREE.Mesh(new THREE.RingGeometry(r1, r2, 24), m); | |
| ring.rotation.x = -Math.PI/2; ring.position.set(x, 0.02, z); g.add(ring); | |
| }); | |
| return g; | |
| } | |
| function buildSunDisc(){ | |
| return new THREE.Mesh(new THREE.SphereGeometry(1.1, 24, 24), new THREE.MeshBasicMaterial({color:0xffd66b})); | |
| } | |
| /* ================= token spec (output format of parse_sentence_to_items) ================= */ | |
| const TOKENS = [ | |
| {word:'the', map:'determiner → ?', unresolved:true, model:'q', commits:[], | |
| cap:'<b>“the”</b> — commits nothing; opens a <b style="color:var(--warn)">?</b>: WHICH cat? Reading skipped it; the ledger must not.'}, | |
| {word:'cat', map:'cat = model (form)', unresolved:false, model:'cat', | |
| commits:['form: tabby model','+position default (frame)','unresolved: pose pending'], | |
| cap:'<b>“cat”</b> — a model enters. Form defaulted: tabby; placed left of center (frame decision riding along). No pose yet — floating form.'}, | |
| {word:'sat', map:'sat → pose + implied surface + support', unresolved:false, model:'sat', | |
| commits:['ground_plane (implied)','pose: sitting','supported-by: contact'], | |
| cap:'<b>“sat”</b> — one word, THREE commits: surface exists (implied), pose becomes geometry, contact established. Serial reading exposes this compression.'}, | |
| {word:'and', map:'connector — serial join', unresolved:false, model:'and', | |
| commits:[], cap:'<b>“and”</b> — no geometry. Its job is serial: binding two clauses into one conversion, promising a second subject.'}, | |
| {word:'the', map:'determiner → ?', unresolved:true, model:'q', commits:[], | |
| cap:'<b>“the”</b> again — another <b style="color:var(--warn)">?</b>. WHICH sun? A second unresolved decision, symmetric with the first.'}, | |
| {word:'sun', map:'sun = model (light source)', unresolved:false, model:'sun', | |
| commits:['light_source: disc + warm light'], | |
| cap:'<b>“sun”</b> — the second model enters: a light source. Light exists — but is not yet <i>related to</i> the cat.'}, | |
| {word:'shone', map:'shone → relation: acting-on-scene', unresolved:false, model:'shone', | |
| commits:['under-light-of: coupling ON','orient default: facing sun'], | |
| cap:'<b>“shone”</b> — a verb of relation: the sun ACTS on the scene. Shadow now computed from sun↔cat geometry; cat faces the light. <b>Configuration complete.</b>'} | |
| ]; | |
| /* ================= snapshot renderer: static model images per word ================= */ | |
| const snapR = new THREE.WebGLRenderer({antialias:true, preserveDrawingBuffer:true}); | |
| snapR.setSize(300, 224); | |
| snapR.shadowMap.enabled = true; | |
| snapR.shadowMap.type = THREE.PCFSoftShadowMap; | |
| function snapshot(build, camPos, lookAt){ | |
| const s = new THREE.Scene(); | |
| s.background = new THREE.Color(0x10131a); | |
| const cam = new THREE.PerspectiveCamera(40, 300/224, 0.1, 200); | |
| cam.position.set(...camPos); cam.lookAt(...lookAt); | |
| s.add(new THREE.AmbientLight(0xffffff, 0.6)); | |
| const dl = new THREE.DirectionalLight(0xffffff, 2.2); | |
| dl.position.set(4, 6, 5); dl.castShadow = true; | |
| dl.shadow.mapSize.set(512, 512); | |
| Object.assign(dl.shadow.camera, {left:-6, right:6, top:6, bottom:-6, far:40}); | |
| s.add(dl); | |
| build(s); | |
| snapR.render(s, cam); | |
| return snapR.domElement.toDataURL(); | |
| } | |
| function addShadowedLight(s, pos, color, intensity, target){ | |
| const dl = new THREE.DirectionalLight(color, intensity); | |
| dl.position.set(...pos); dl.castShadow = true; | |
| dl.shadow.mapSize.set(512, 512); | |
| Object.assign(dl.shadow.camera, {left:-6, right:6, top:6, bottom:-6, far:40}); | |
| if(target) dl.target.position.set(...target); | |
| s.add(dl); s.add(dl.target); | |
| } | |
| const SNAPSHOTS = { | |
| ground: snapshot(s=>s.add(buildGroundSmall()), [3,4,5], [0,0,0]), | |
| cat: snapshot(s=>s.add(buildForm()), [2.2,2.2,3.4], [0,1.1,0]), | |
| sat: snapshot(s=>{ | |
| const d = buildGroundSmall(); s.add(d); | |
| const c = new THREE.Group(); c.add(buildForm()); c.add(buildPose()); c.add(buildSupport()); s.add(c); | |
| }, [2.6,2.4,3.8], [0,0.8,0]), | |
| and: snapshot(s=>{ | |
| const a = new THREE.Mesh(new THREE.SphereGeometry(0.4,16,12), matFur()); a.position.x = -1.1; | |
| const b = new THREE.Mesh(new THREE.SphereGeometry(0.4,16,12), new THREE.MeshBasicMaterial({color:0xffd66b})); b.position.x = 1.1; | |
| const link = new THREE.Mesh(new THREE.CylinderGeometry(0.06,0.06,2.2,8), new THREE.MeshStandardMaterial({color:0x93a0b4})); | |
| link.rotation.z = Math.PI/2; | |
| s.add(a, b, link); | |
| }, [0,0.6,5], [0,0,0]), | |
| sun: snapshot(s=>s.add(buildSunDisc()), [0,0,4], [0,0,0]), | |
| shone: snapshot(s=>{ | |
| s.add(buildGroundSmall()); | |
| const c = new THREE.Group(); c.add(buildForm()); c.add(buildPose()); c.add(buildSupport()); s.add(c); | |
| const sun = buildSunDisc(); sun.position.set(4, 2.2, -3); sun.scale.setScalar(0.8); s.add(sun); | |
| addShadowedLight(s, [4,2.2,-3], 0xffe3a3, 2.5, [0,0.6,0]); | |
| }, [3.2,2.4,4.6], [0,0.7,0]) | |
| }; | |
| /* ================= tabs ================= */ | |
| const tabScene = document.getElementById('tabScene'), tabSerial = document.getElementById('tabSerial'); | |
| const viewScene = document.getElementById('viewScene'), viewSerial = document.getElementById('viewSerial'); | |
| function showTab(which){ | |
| tabScene.classList.toggle('active', which==='scene'); | |
| tabSerial.classList.toggle('active', which==='serial'); | |
| viewScene.classList.toggle('active', which==='scene'); | |
| viewSerial.classList.toggle('active', which==='serial'); | |
| document.getElementById('scene').style.visibility = which==='scene' ? 'visible' : 'hidden'; | |
| document.getElementById('reading').classList.toggle('active', which==='serial'); | |
| document.getElementById('hud').style.visibility = which==='scene' ? 'visible' : 'hidden'; | |
| } | |
| tabScene.onclick = ()=>showTab('scene'); | |
| tabSerial.onclick = ()=>showTab('serial'); | |
| /* ================= build serial rail with models ================= */ | |
| const rail = document.getElementById('rail'); | |
| const tokEls = []; | |
| TOKENS.forEach((t,i)=>{ | |
| const wrap = document.createElement('div'); wrap.className='tokwrap'; | |
| const tok = document.createElement('div'); tok.className='tok'+(t.unresolved?' q':''); tok.textContent=i+1; | |
| const model = document.createElement('div'); | |
| if(t.model === 'q'){ model.className='model qbox'; model.textContent='?'; } | |
| else { model.className='model'; const img = document.createElement('img'); img.src = SNAPSHOTS[t.model]; img.style.width='100%'; img.style.height='100%'; img.style.objectFit='cover'; img.style.borderRadius='9px'; model.appendChild(img); } | |
| const word = document.createElement('div'); word.className='word'; word.textContent=t.word; | |
| const map = document.createElement('div'); map.className='map'; map.textContent=t.map; | |
| const commits = document.createElement('div'); commits.className='commits'; | |
| t.commits.forEach(c=>{ | |
| const chip = document.createElement('div'); | |
| chip.className='chip'+(c.startsWith('under')||c.startsWith('supported')?' rel':''); | |
| chip.textContent=c; commits.appendChild(chip); | |
| }); | |
| wrap.append(tok, model, word, map, commits); | |
| rail.appendChild(wrap); | |
| tokEls.push({wrap, tok, chips:[...commits.children]}); | |
| }); | |
| /* ================= main scene ================= */ | |
| const scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x2a3346); | |
| scene.fog = new THREE.Fog(0x2a3346, 40, 120); | |
| const camera = new THREE.PerspectiveCamera(50, innerWidth/innerHeight, 0.1, 300); | |
| camera.position.set(6, 3.4, 8); | |
| const renderer = new THREE.WebGLRenderer({antialias:true}); | |
| renderer.setSize(innerWidth, innerHeight); | |
| renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| document.getElementById('scene').appendChild(renderer.domElement); | |
| const controls = new OrbitControls(camera, renderer.domElement); | |
| controls.target.set(0, 0.8, 0); | |
| controls.enableDamping = true; | |
| const ground = buildGround(); ground.visible = false; scene.add(ground); | |
| const cat = new THREE.Group(); | |
| const formG = buildForm(), poseG = buildPose(), supG = buildSupport(); | |
| formG.visible = false; poseG.visible = false; supG.visible = false; | |
| cat.add(formG, poseG, supG); | |
| cat.visible = false; | |
| scene.add(cat); | |
| const sunDisc = buildSunDisc(); sunDisc.scale.setScalar(2); sunDisc.visible = false; scene.add(sunDisc); | |
| const sunLight = new THREE.DirectionalLight(0xffe3a3, 0); | |
| sunLight.castShadow = false; | |
| sunLight.shadow.mapSize.set(2048, 2048); | |
| Object.assign(sunLight.shadow.camera, {left:-12, right:12, top:12, bottom:-12, far:80}); | |
| scene.add(sunLight); | |
| const ambient = new THREE.AmbientLight(0x8098c0, 0.85); scene.add(ambient); | |
| /* ================= SERIAL stepper (independent) ================= */ | |
| let serialStep = 0; // 0..7 tokens committed | |
| function applySerial(){ | |
| tokEls.forEach((el,i)=>{ | |
| el.tok.classList.toggle('committed', i < serialStep); | |
| el.tok.classList.toggle('active', i === serialStep-1); | |
| el.wrap.classList.toggle('committed', i < serialStep); | |
| el.wrap.classList.toggle('active', i === serialStep-1); | |
| el.chips.forEach(c=>c.classList.toggle('on', i < serialStep)); | |
| }); | |
| document.getElementById('capSerial').innerHTML = serialStep === 0 | |
| ? 'Press <b>▶ step</b> — each word commits its models and items in reading order.' | |
| : TOKENS[serialStep-1].cap; | |
| document.getElementById('rPrev').disabled = serialStep === 0; | |
| document.getElementById('rNext').disabled = serialStep === 7; | |
| } | |
| function gotoSerial(n){ serialStep = Math.max(0, Math.min(7, n)); applySerial(); } | |
| /* ================= SCENE stepper (independent) ================= */ | |
| let sceneStep = 0; // 0..8 items | |
| const catHome = new THREE.Vector3(0,0,0), catPlaced = new THREE.Vector3(-0.6,0,0); | |
| const targetPos = catHome.clone(); | |
| let targetRotY = 0; | |
| const SCENE_CAPS = { | |
| 0:'Scene stepper is independent — press <b>▶ step</b> to build the configuration item by item.', | |
| 1:'<b>1 · ground_plane</b> — “sat” forces a surface. Defaulted: grass.', | |
| 2:'<b>2 · form: cat</b> — a body exists. No pose yet: floating form.', | |
| 3:'<b>3 · pose: sat</b> — haunches drop, tail curls. “Sat” became geometry.', | |
| 4:'<b>4 · position</b> — frame decision: left of center.', | |
| 5:'<b>5 · orient</b> — facing the sun (default derived from item 7).', | |
| 6:'<b>6 · light_source: sun</b> — light enters, sky turns. Not yet coupled.', | |
| 7:'<b>7 · under-light-of</b> — coupling ON: shadow computed from relations.', | |
| 8:'<b>8 · supported-by</b> — contact marked. Configuration complete.' | |
| }; | |
| function applyScene(){ | |
| ground.visible = sceneStep >= 1; | |
| cat.visible = sceneStep >= 2; | |
| formG.visible = sceneStep >= 2; | |
| poseG.visible = sceneStep >= 3; | |
| targetPos.copy(sceneStep >= 4 ? catPlaced : catHome); | |
| const sunOn = sceneStep >= 6; | |
| sunDisc.visible = sunOn; sunLight.intensity = sunOn ? 3.0 : 0; | |
| sunLight.castShadow = sceneStep >= 7; | |
| scene.background.set(sunOn ? 0x87b5e0 : 0x2a3346); | |
| scene.fog.color.set(sunOn ? 0x87b5e0 : 0x2a3346); | |
| ambient.intensity = sunOn ? 0.55 : 0.85; | |
| supG.visible = sceneStep >= 8; | |
| document.querySelectorAll('#ledger .item[data-i]').forEach(el=>{ | |
| const i = +el.dataset.i; | |
| el.classList.toggle('done', i <= sceneStep); | |
| el.classList.toggle('active', i === sceneStep); | |
| }); | |
| document.getElementById('capScene').innerHTML = SCENE_CAPS[sceneStep]; | |
| document.getElementById('sPrev').disabled = sceneStep === 0; | |
| document.getElementById('sNext').disabled = sceneStep === 8; | |
| } | |
| function gotoScene(n){ sceneStep = Math.max(0, Math.min(8, n)); applyScene(); applyConfiguration(); } | |
| /* ================= wiring ================= */ | |
| document.getElementById('rNext').onclick = ()=>gotoSerial(serialStep+1); | |
| document.getElementById('rPrev').onclick = ()=>gotoSerial(serialStep-1); | |
| document.getElementById('sNext').onclick = ()=>gotoScene(sceneStep+1); | |
| document.getElementById('sPrev').onclick = ()=>gotoScene(sceneStep-1); | |
| let autoS = null, autoR = null; | |
| document.getElementById('rAuto').onclick = ()=>{ | |
| if(autoR){ clearInterval(autoR); autoR=null; document.getElementById('rAuto').textContent='⏩ auto'; return; } | |
| if(serialStep >= 7) gotoSerial(0); | |
| document.getElementById('rAuto').textContent='⏸ stop'; | |
| autoR = setInterval(()=>{ if(serialStep >= 7){ clearInterval(autoR); autoR=null; document.getElementById('rAuto').textContent='⏩ auto'; return; } gotoSerial(serialStep+1); }, 1700); | |
| }; | |
| document.getElementById('sAuto').onclick = ()=>{ | |
| if(autoS){ clearInterval(autoS); autoS=null; document.getElementById('sAuto').textContent='⏩ auto'; return; } | |
| if(sceneStep >= 8) gotoScene(0); | |
| document.getElementById('sAuto').textContent='⏸ stop'; | |
| autoS = setInterval(()=>{ if(sceneStep >= 8){ clearInterval(autoS); autoS=null; document.getElementById('sAuto').textContent='⏩ auto'; return; } gotoScene(sceneStep+1); }, 1400); | |
| }; | |
| addEventListener('keydown', e=>{ | |
| const active = tabSerial.classList.contains('active') ? 'serial' : 'scene'; | |
| if(e.key==='ArrowRight') active==='serial' ? gotoSerial(serialStep+1) : gotoScene(sceneStep+1); | |
| if(e.key==='ArrowLeft') active==='serial' ? gotoSerial(serialStep-1) : gotoScene(sceneStep-1); | |
| }); | |
| /* refinement controls */ | |
| const DIST = 45; | |
| function applyConfiguration(){ | |
| const elev = THREE.MathUtils.degToRad(+document.getElementById('elev').value); | |
| const az = THREE.MathUtils.degToRad(+document.getElementById('az').value); | |
| const dir = new THREE.Vector3(Math.cos(elev)*Math.sin(az), Math.sin(elev), Math.cos(elev)*Math.cos(az)); | |
| sunDisc.position.copy(dir).multiplyScalar(DIST); | |
| sunLight.position.copy(dir).multiplyScalar(20); | |
| sunLight.target.position.set(targetPos.x, 0.5, targetPos.z); | |
| scene.add(sunLight.target); | |
| document.getElementById('elevV').textContent = document.getElementById('elev').value + '°'; | |
| document.getElementById('azV').textContent = document.getElementById('az').value + '°'; | |
| } | |
| const FACINGS = {toward:180, away:0, profile:90}; | |
| function applyFacing(){ | |
| const v = document.getElementById('facing').value; | |
| targetRotY = THREE.MathUtils.degToRad(FACINGS[v] - document.getElementById('az').value); | |
| } | |
| ['elev','az'].forEach(id=>document.getElementById(id).addEventListener('input', applyConfiguration)); | |
| document.getElementById('facing').addEventListener('change', applyFacing); | |
| applyConfiguration(); applyFacing(); applySerial(); applyScene(); | |
| addEventListener('resize', ()=>{ | |
| camera.aspect = innerWidth/innerHeight; camera.updateProjectionMatrix(); | |
| renderer.setSize(innerWidth, innerHeight); | |
| }); | |
| (function animate(){ | |
| requestAnimationFrame(animate); | |
| cat.position.lerp(targetPos, 0.08); | |
| cat.rotation.y += (targetRotY - cat.rotation.y) * 0.08; | |
| controls.update(); | |
| renderer.render(scene, camera); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |