Regenerate STLs, update viewer closed pose, and document assembly. Co-authored-by: Cursor <cursoragent@cursor.com>
491 lines
20 KiB
HTML
491 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>CYD Case — 合盖 / DHT后盖卡位</title>
|
||
<style>
|
||
:root {
|
||
--bg: #0b1220; --panel: #121c2aee; --text: #e8eef7; --muted: #8a9bb5;
|
||
--accent: #3db8a8; --line: #243247; --warm: #f0a04b;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--text);
|
||
font-family: "Segoe UI", "PingFang SC", system-ui, sans-serif; overflow: hidden; }
|
||
#c { display: block; width: 100%; height: 100%; touch-action: none; }
|
||
.hud {
|
||
position: fixed; left: 14px; top: 14px; z-index: 2; width: min(370px, calc(100vw - 28px));
|
||
max-height: calc(100vh - 28px); overflow: auto; background: var(--panel);
|
||
border: 1px solid var(--line); border-radius: 14px; padding: 14px;
|
||
backdrop-filter: blur(10px); box-shadow: 0 12px 40px #0006;
|
||
}
|
||
h1 { margin: 0 0 4px; font-size: 15px; font-weight: 650; }
|
||
.hint { margin: 0 0 10px; font-size: 11px; color: var(--muted); line-height: 1.4; }
|
||
.sec { margin-top: 11px; padding-top: 10px; border-top: 1px solid var(--line); }
|
||
.sec-title { font-size: 11px; color: var(--accent); letter-spacing: .06em; margin-bottom: 8px; text-transform: uppercase; }
|
||
.row { display: flex; flex-wrap: wrap; gap: 6px; }
|
||
button, label.tog {
|
||
appearance: none; border: 1px solid #2c3d56; background: #1a2740; color: var(--text);
|
||
border-radius: 8px; padding: 6px 9px; font-size: 12px; cursor: pointer;
|
||
}
|
||
button:hover, label.tog:hover { border-color: var(--accent); }
|
||
button.active { background: #1e3d3a; border-color: var(--accent); color: #b6f0e6; }
|
||
label.tog { display: inline-flex; align-items: center; gap: 5px; user-select: none; }
|
||
.slider {
|
||
display: grid; grid-template-columns: 72px 1fr 42px; gap: 8px; align-items: center;
|
||
margin: 6px 0; font-size: 12px; color: var(--muted);
|
||
}
|
||
.slider input { width: 100%; accent-color: var(--accent); }
|
||
.slider .val { text-align: right; color: var(--text); font-variant-numeric: tabular-nums; }
|
||
#status { margin-top: 10px; font-size: 11px; color: var(--muted); }
|
||
#status.err { color: #e05a5a; }
|
||
.note { font-size: 11px; color: var(--warm); line-height: 1.45; margin-top: 6px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="hud">
|
||
<h1>CYD Case Preview</h1>
|
||
<p class="hint">DHT11 在后盖外侧卡位,微微突出 · 前后盖止口合盖</p>
|
||
|
||
<div class="sec-title">合盖</div>
|
||
<div class="row">
|
||
<button type="button" id="btn-close" class="active">合盖</button>
|
||
<button type="button" id="btn-open">开盖</button>
|
||
<button type="button" id="btn-play">播放装配</button>
|
||
<button type="button" id="btn-stop">停止</button>
|
||
</div>
|
||
<div class="slider">
|
||
<span>开合度</span>
|
||
<input type="range" id="lid" min="0" max="100" value="0" />
|
||
<span class="val" id="lid-val">0%</span>
|
||
</div>
|
||
<div class="slider">
|
||
<span>分解</span>
|
||
<input type="range" id="explode" min="0" max="100" value="0" />
|
||
<span class="val" id="explode-val">0</span>
|
||
</div>
|
||
|
||
<div class="sec">
|
||
<div class="sec-title">部件</div>
|
||
<div class="row">
|
||
<label class="tog"><input type="checkbox" id="chk-front" checked /> 前框</label>
|
||
<label class="tog"><input type="checkbox" id="chk-back" checked /> 后盖</label>
|
||
<label class="tog"><input type="checkbox" id="chk-board" checked /> 模拟屏板</label>
|
||
<label class="tog"><input type="checkbox" id="chk-dht" checked /> DHT11模块</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sec">
|
||
<div class="sec-title">显示</div>
|
||
<div class="row">
|
||
<label class="tog"><input type="checkbox" id="chk-grid" checked /> 网格</label>
|
||
<label class="tog"><input type="checkbox" id="chk-auto" /> 自转</label>
|
||
<label class="tog"><input type="checkbox" id="chk-wire" /> 线框</label>
|
||
<label class="tog"><input type="checkbox" id="chk-xray" /> 透视</label>
|
||
<label class="tog"><input type="checkbox" id="chk-backview" /> 看后盖</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sec">
|
||
<div class="sec-title">视角</div>
|
||
<div class="row">
|
||
<button type="button" data-view="iso">等轴</button>
|
||
<button type="button" data-view="front">正面</button>
|
||
<button type="button" data-view="back">背面</button>
|
||
<button type="button" data-view="side">侧面</button>
|
||
<button type="button" id="btn-reset">复位</button>
|
||
<button type="button" id="btn-shot">截图</button>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="note">合盖=0%:前框止口落入后盖槽。DHT 从后盖背面卡入并突出约 1.8mm。</p>
|
||
<div id="status">加载中…</div>
|
||
</div>
|
||
<canvas id="c"></canvas>
|
||
|
||
<script type="importmap">
|
||
{
|
||
"imports": {
|
||
"three": "https://unpkg.com/three@0.170.0/build/three.module.js",
|
||
"three/addons/": "https://unpkg.com/three@0.170.0/examples/jsm/"
|
||
}
|
||
}
|
||
</script>
|
||
<script type="module">
|
||
import * as THREE from "three";
|
||
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
||
import { STLLoader } from "three/addons/loaders/STLLoader.js";
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
const statusEl = $("status");
|
||
const canvas = $("c");
|
||
|
||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, preserveDrawingBuffer: true });
|
||
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
||
renderer.setSize(innerWidth, innerHeight, false);
|
||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||
|
||
const scene = new THREE.Scene();
|
||
scene.background = new THREE.Color(0x0b1220);
|
||
|
||
const camera = new THREE.PerspectiveCamera(42, innerWidth / innerHeight, 0.1, 4000);
|
||
camera.position.set(120, 80, 140);
|
||
|
||
const controls = new OrbitControls(camera, canvas);
|
||
controls.enableDamping = true;
|
||
|
||
scene.add(new THREE.AmbientLight(0xffffff, 0.55));
|
||
const key = new THREE.DirectionalLight(0xffffff, 1.1);
|
||
key.position.set(80, 120, 60);
|
||
scene.add(key);
|
||
const fill = new THREE.DirectionalLight(0x88aacc, 0.4);
|
||
fill.position.set(-60, 40, -80);
|
||
scene.add(fill);
|
||
|
||
const grid = new THREE.GridHelper(240, 24, 0x3db8a8, 0x1e2c40);
|
||
grid.position.y = -40;
|
||
scene.add(grid);
|
||
|
||
const root = new THREE.Group();
|
||
scene.add(root);
|
||
|
||
const matFront = new THREE.MeshStandardMaterial({
|
||
color: 0x6a7b92, metalness: 0.12, roughness: 0.5, flatShading: true, side: THREE.DoubleSide,
|
||
});
|
||
const matBack = new THREE.MeshStandardMaterial({
|
||
color: 0x2f9f92, metalness: 0.1, roughness: 0.48, flatShading: true, side: THREE.DoubleSide,
|
||
});
|
||
const matBoard = new THREE.MeshStandardMaterial({ color: 0xc9a227, metalness: 0.3, roughness: 0.4, flatShading: true });
|
||
const matScreen = new THREE.MeshStandardMaterial({
|
||
color: 0x101820, emissive: 0x1a3048, emissiveIntensity: 0.4, roughness: 0.35,
|
||
});
|
||
const matDht = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.65, flatShading: true });
|
||
const matBlue = new THREE.MeshStandardMaterial({ color: 0x2a5cff, roughness: 0.4 });
|
||
|
||
let meshFront, meshBack, boardGroup, dhtGroup;
|
||
let frontPivot, backPivot;
|
||
let closedGap = 0; // measured stack height between parts when closed
|
||
let lidT = 0, explodeT = 0, autoRotate = false, anim = null;
|
||
let backSize = new THREE.Vector3();
|
||
|
||
function setStatus(msg, err = false) {
|
||
statusEl.textContent = msg;
|
||
statusEl.classList.toggle("err", err);
|
||
}
|
||
|
||
function loadSTL(url) {
|
||
const loader = new STLLoader();
|
||
return new Promise((resolve, reject) => {
|
||
loader.load(url, (geo) => {
|
||
geo.computeVertexNormals();
|
||
resolve(geo);
|
||
}, undefined, reject);
|
||
});
|
||
}
|
||
|
||
/** Put mesh so its bounding box min corner is at origin, Z up. */
|
||
function normalizeMesh(mesh) {
|
||
mesh.geometry.computeBoundingBox();
|
||
const bb = mesh.geometry.boundingBox.clone();
|
||
const size = new THREE.Vector3();
|
||
bb.getSize(size);
|
||
// STL from generator: XY = board plane, Z = thickness. Rotate to stand: screen faces +Z
|
||
mesh.rotation.set(0, 0, 0);
|
||
mesh.geometry.translate(-bb.min.x, -bb.min.y, -bb.min.z);
|
||
mesh.geometry.computeBoundingBox();
|
||
return size;
|
||
}
|
||
|
||
function makeBoard(w, h) {
|
||
const g = new THREE.Group();
|
||
const pcb = new THREE.Mesh(new THREE.BoxGeometry(86, 50, 1.6), matBoard);
|
||
const bezel = new THREE.Mesh(new THREE.BoxGeometry(69, 50, 3.8), new THREE.MeshStandardMaterial({
|
||
color: 0x222830, roughness: 0.55, flatShading: true,
|
||
}));
|
||
bezel.position.set(4, 0, 2.7);
|
||
const screen = new THREE.Mesh(new THREE.BoxGeometry(59.5, 45, 0.5), matScreen);
|
||
screen.position.set(4, 0, 4.7);
|
||
g.add(pcb);
|
||
g.add(bezel);
|
||
g.add(screen);
|
||
g.position.set(w / 2, h / 2, 0);
|
||
return g;
|
||
}
|
||
|
||
function makeDht() {
|
||
const g = new THREE.Group();
|
||
const body = new THREE.Mesh(new THREE.BoxGeometry(16, 12.5, 8), matDht);
|
||
const sensor = new THREE.Mesh(new THREE.BoxGeometry(12, 9, 2.2), matBlue);
|
||
sensor.position.z = -5.1; // toward outside (negative Z of back exterior)
|
||
g.add(body);
|
||
g.add(sensor);
|
||
return g;
|
||
}
|
||
|
||
function fit(obj) {
|
||
const box = new THREE.Box3().setFromObject(obj);
|
||
const size = box.getSize(new THREE.Vector3());
|
||
const center = box.getCenter(new THREE.Vector3());
|
||
controls.target.copy(center);
|
||
const maxDim = Math.max(size.x, size.y, size.z, 1);
|
||
const dist = maxDim * 1.7;
|
||
camera.position.set(center.x + dist * 0.65, center.y - dist * 0.55, center.z + dist * 0.55);
|
||
camera.near = maxDim / 200;
|
||
camera.far = maxDim * 60;
|
||
camera.updateProjectionMatrix();
|
||
controls.update();
|
||
}
|
||
|
||
/**
|
||
* Assembly frame (mm):
|
||
* - Back: exterior face at z=0 (DHT protrudes to z<0), rim toward +Z
|
||
* - Board: inside back cavity
|
||
* - Front: closes from +Z onto back rim when lidT=0
|
||
*/
|
||
function applyPose() {
|
||
if (!backPivot || !frontPivot) return;
|
||
|
||
const ex = explodeT * 35;
|
||
// open lid: lift + hinge rotate around Y at top edge
|
||
const open = lidT;
|
||
const lift = open * 28 + ex;
|
||
const hinge = open * THREE.MathUtils.degToRad(105);
|
||
|
||
backPivot.position.set(0, 0, -ex * 0.25);
|
||
backPivot.rotation.set(0, 0, 0);
|
||
|
||
// closed: front sits on top of back rim
|
||
frontPivot.position.set(0, 0, closedGap + lift);
|
||
frontPivot.rotation.set(hinge, 0, 0);
|
||
// hinge pivot offset: rotate around top (+Y) edge
|
||
if (meshFront) {
|
||
const hy = backSize.y;
|
||
frontPivot.position.y = Math.sin(hinge) * 0; // keep
|
||
// rotate around top edge: move pivot
|
||
frontPivot.position.set(0, hy * 0.5 * (1 - Math.cos(hinge)), closedGap + lift + hy * 0.15 * Math.sin(hinge));
|
||
frontPivot.rotation.set(hinge, 0, 0);
|
||
// compensate so mesh origin stays consistent — use sub-offset
|
||
meshFront.position.set(0, -hy * 0.0, 0);
|
||
}
|
||
|
||
if (boardGroup) {
|
||
// sit inside back: above floor wall (~2.4)
|
||
boardGroup.position.set(backSize.x / 2, backSize.y / 2, 4.5 + ex * 0.1);
|
||
boardGroup.rotation.set(0, 0, 0);
|
||
}
|
||
|
||
if (dhtGroup) {
|
||
// DHT bay is on back exterior around x~62+wall, y center — match generator
|
||
const cx = 2.4 + 0.35 + 62;
|
||
const cy = 2.4 + 0.35 + 25;
|
||
dhtGroup.position.set(cx, cy, -2.0 - 0.2 * ex); // slightly outside back face
|
||
dhtGroup.rotation.set(0, 0, 0);
|
||
}
|
||
}
|
||
|
||
function setLid(v, ui = true) {
|
||
lidT = THREE.MathUtils.clamp(v, 0, 1);
|
||
if (ui) {
|
||
$("lid").value = String(Math.round(lidT * 100));
|
||
$("lid-val").textContent = `${Math.round(lidT * 100)}%`;
|
||
}
|
||
applyPose();
|
||
$("btn-close").classList.toggle("active", lidT < 0.05);
|
||
$("btn-open").classList.toggle("active", lidT > 0.95);
|
||
}
|
||
|
||
function setExplode(v, ui = true) {
|
||
explodeT = THREE.MathUtils.clamp(v, 0, 1);
|
||
if (ui) {
|
||
$("explode").value = String(Math.round(explodeT * 100));
|
||
$("explode-val").textContent = String(Math.round(explodeT * 100));
|
||
}
|
||
applyPose();
|
||
}
|
||
|
||
function ease(t) {
|
||
return t < 0.5 ? 2 * t * t : 1 - (Math.pow(-2 * t + 2, 2) / 2);
|
||
}
|
||
|
||
function startAnim(kind) {
|
||
const now = performance.now();
|
||
if (kind === "close") anim = { kind, t0: now, dur: 850, fromL: lidT, toL: 0, fromE: explodeT, toE: 0 };
|
||
if (kind === "open") anim = { kind, t0: now, dur: 850, fromL: lidT, toL: 1, fromE: explodeT, toE: 0 };
|
||
if (kind === "play") {
|
||
anim = { kind: "play", phase: 0, t0: now, dur: 700, fromL: lidT, toL: 1, fromE: explodeT, toE: 1 };
|
||
}
|
||
setStatus(kind === "play" ? "装配播放中…" : kind === "open" ? "开盖…" : "合盖…");
|
||
}
|
||
|
||
function tickAnim(now) {
|
||
if (!anim) return;
|
||
const u = ease(Math.min(1, (now - anim.t0) / anim.dur));
|
||
if (anim.kind === "play") {
|
||
if (anim.phase === 0) {
|
||
setLid(THREE.MathUtils.lerp(anim.fromL, 1, u));
|
||
setExplode(THREE.MathUtils.lerp(anim.fromE, 1, u));
|
||
if (u >= 1) anim = { kind: "play", phase: 1, t0: now, dur: 750, fromL: 1, toL: 1, fromE: 1, toE: 0 };
|
||
} else if (anim.phase === 1) {
|
||
setExplode(THREE.MathUtils.lerp(1, 0, u));
|
||
if (u >= 1) anim = { kind: "play", phase: 2, t0: now, dur: 900, fromL: 1, toL: 0, fromE: 0, toE: 0 };
|
||
} else {
|
||
setLid(THREE.MathUtils.lerp(1, 0, u));
|
||
if (u >= 1) { anim = null; setStatus("已合盖:前框止口落入后盖"); }
|
||
}
|
||
return;
|
||
}
|
||
setLid(THREE.MathUtils.lerp(anim.fromL, anim.toL, u));
|
||
setExplode(THREE.MathUtils.lerp(anim.fromE, anim.toE, u));
|
||
if (u >= 1) {
|
||
anim = null;
|
||
setStatus(lidT < 0.05 ? "已合盖" : "已开盖");
|
||
}
|
||
}
|
||
|
||
async function boot() {
|
||
try {
|
||
const [geoF, geoB] = await Promise.all([
|
||
loadSTL("./export/front.stl"),
|
||
loadSTL("./export/back.stl"),
|
||
]);
|
||
|
||
meshBack = new THREE.Mesh(geoB, matBack);
|
||
meshFront = new THREE.Mesh(geoF, matFront);
|
||
backSize = normalizeMesh(meshBack);
|
||
const frontSize = normalizeMesh(meshFront);
|
||
|
||
// Align both on XY; back Z=0 exterior, grows +Z; front will stack on rim
|
||
backPivot = new THREE.Group();
|
||
backPivot.add(meshBack);
|
||
|
||
frontPivot = new THREE.Group();
|
||
frontPivot.add(meshFront);
|
||
|
||
// closedGap ≈ back shell height to rim (geometry max.z)
|
||
meshBack.geometry.computeBoundingBox();
|
||
meshFront.geometry.computeBoundingBox();
|
||
const backH = meshBack.geometry.boundingBox.max.z;
|
||
const frontH = meshFront.geometry.boundingBox.max.z;
|
||
// Front STL: bezel at z=0 (screen face), lip at +Z. For closing onto back,
|
||
// flip front so lip faces -Z toward back rim, screen faces +Z toward user.
|
||
meshFront.rotation.x = Math.PI;
|
||
meshFront.position.z = frontH; // after 180° about X, compensate
|
||
meshFront.position.y = frontSize.y; // fix Y flip from rot X
|
||
|
||
closedGap = backH - 2.2; // seat lip into groove (~LIP_H)
|
||
|
||
boardGroup = makeBoard(backSize.x, backSize.y);
|
||
// board local was centered; remake simpler placement
|
||
boardGroup.position.set(0, 0, 0);
|
||
boardGroup.children.forEach(() => {});
|
||
// rebuild board at origin extents
|
||
root.remove(boardGroup);
|
||
boardGroup = new THREE.Group();
|
||
{
|
||
const pcb = new THREE.Mesh(new THREE.BoxGeometry(86, 50, 1.6), matBoard);
|
||
pcb.position.set(2.4 + 0.35 + 43, 2.4 + 0.35 + 25, 5.5);
|
||
const bez = new THREE.Mesh(new THREE.BoxGeometry(69, 50, 3.8), new THREE.MeshStandardMaterial({
|
||
color: 0x222830, roughness: 0.55, flatShading: true,
|
||
}));
|
||
bez.position.set(2.4 + 0.35 + 8 + 34.5, 2.4 + 0.35 + 25, 7.5);
|
||
const scr = new THREE.Mesh(new THREE.BoxGeometry(59.5, 45, 0.5), matScreen);
|
||
scr.position.set(2.4 + 0.35 + 8 + 7 + 29.75, 2.4 + 0.35 + 2.5 + 22.5, 9.6);
|
||
boardGroup.add(pcb, bez, scr);
|
||
}
|
||
|
||
dhtGroup = makeDht();
|
||
|
||
root.add(backPivot);
|
||
root.add(frontPivot);
|
||
root.add(boardGroup);
|
||
root.add(dhtGroup);
|
||
|
||
setLid(0);
|
||
setExplode(0);
|
||
fit(root);
|
||
setStatus(`就绪 · 后盖 ${backSize.x.toFixed(1)}×${backSize.y.toFixed(1)} mm · 点「合盖」或拖开合度`);
|
||
} catch (e) {
|
||
console.error(e);
|
||
setStatus("加载失败:请运行 python -m http.server 8765 --directory cad", true);
|
||
}
|
||
}
|
||
|
||
$("lid").oninput = (e) => { anim = null; setLid(Number(e.target.value) / 100, false); $("lid-val").textContent = `${e.target.value}%`; };
|
||
$("explode").oninput = (e) => { anim = null; setExplode(Number(e.target.value) / 100, false); $("explode-val").textContent = e.target.value; };
|
||
$("btn-close").onclick = () => startAnim("close");
|
||
$("btn-open").onclick = () => startAnim("open");
|
||
$("btn-play").onclick = () => startAnim("play");
|
||
$("btn-stop").onclick = () => { anim = null; setStatus("已停止"); };
|
||
|
||
$("chk-front").onchange = (e) => { if (frontPivot) frontPivot.visible = e.target.checked; };
|
||
$("chk-back").onchange = (e) => { if (backPivot) backPivot.visible = e.target.checked; };
|
||
$("chk-board").onchange = (e) => { if (boardGroup) boardGroup.visible = e.target.checked; };
|
||
$("chk-dht").onchange = (e) => { if (dhtGroup) dhtGroup.visible = e.target.checked; };
|
||
$("chk-grid").onchange = (e) => { grid.visible = e.target.checked; };
|
||
$("chk-auto").onchange = (e) => { autoRotate = e.target.checked; };
|
||
$("chk-wire").onchange = (e) => {
|
||
for (const m of [matFront, matBack, matBoard, matScreen, matDht, matBlue]) {
|
||
m.wireframe = e.target.checked; m.needsUpdate = true;
|
||
}
|
||
};
|
||
$("chk-xray").onchange = (e) => {
|
||
for (const m of [matFront, matBack]) {
|
||
m.transparent = e.target.checked;
|
||
m.opacity = e.target.checked ? 0.35 : 1;
|
||
m.depthWrite = !e.target.checked;
|
||
m.needsUpdate = true;
|
||
}
|
||
};
|
||
$("chk-backview").onchange = (e) => {
|
||
if (e.target.checked) setView("back");
|
||
else setView("iso");
|
||
};
|
||
|
||
function setView(name) {
|
||
const t = controls.target.clone();
|
||
const d = camera.position.distanceTo(t) || 160;
|
||
const map = {
|
||
iso: new THREE.Vector3(t.x + d * 0.65, t.y - d * 0.5, t.z + d * 0.55),
|
||
front: new THREE.Vector3(t.x, t.y, t.z + d),
|
||
back: new THREE.Vector3(t.x, t.y, t.z - d),
|
||
side: new THREE.Vector3(t.x + d, t.y, t.z),
|
||
};
|
||
camera.position.copy(map[name] || map.iso);
|
||
controls.update();
|
||
}
|
||
document.querySelectorAll("[data-view]").forEach((b) => b.onclick = () => setView(b.dataset.view));
|
||
$("btn-reset").onclick = () => { if (root.children.length) fit(root); };
|
||
$("btn-shot").onclick = () => {
|
||
renderer.render(scene, camera);
|
||
const a = document.createElement("a");
|
||
a.download = "cyd-case.png";
|
||
a.href = canvas.toDataURL("image/png");
|
||
a.click();
|
||
};
|
||
|
||
addEventListener("resize", () => {
|
||
camera.aspect = innerWidth / innerHeight;
|
||
camera.updateProjectionMatrix();
|
||
renderer.setSize(innerWidth, innerHeight, false);
|
||
});
|
||
addEventListener("keydown", (e) => {
|
||
if (e.target.matches("input,textarea")) return;
|
||
if (e.key === "c") startAnim("close");
|
||
if (e.key === "o") startAnim("open");
|
||
if (e.key === " ") { e.preventDefault(); startAnim("play"); }
|
||
});
|
||
|
||
function tick(now) {
|
||
requestAnimationFrame(tick);
|
||
tickAnim(now);
|
||
if (autoRotate) root.rotation.z += 0.003;
|
||
controls.update();
|
||
renderer.render(scene, camera);
|
||
}
|
||
|
||
boot();
|
||
requestAnimationFrame(tick);
|
||
</script>
|
||
</body>
|
||
</html>
|