Skip to content

Commit b949ef9

Browse files
Merge pull request #22 from mkeblx/sides
Add sides page
2 parents 918dfa1 + 21cc016 commit b949ef9

5 files changed

Lines changed: 164 additions & 0 deletions

File tree

assets/cube-sides.glb

742 KB
Binary file not shown.

assets/cube-sides.jpg

256 KB
Loading

assets/cube-sides.usdz

602 KB
Binary file not shown.

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ <h1>List of model samples:</h1>
3434
<a href="page/chrome.html">chrome ball</a>
3535
<a href="page/usdz.html">usdz</a>
3636
<a href="page/glb.html">glb</a>
37+
<a href="page/sides.html">sides</a>
3738
<a href="page/filter.html">filter</a>
3839
</ul>
3940

page/sides.html

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Model: cube sides</title>
8+
<link rel="stylesheet" href="../css/style.css" />
9+
<style>
10+
.cube-view-controls {
11+
display: flex;
12+
flex-wrap: wrap;
13+
gap: 8px;
14+
margin: 12px 0;
15+
}
16+
17+
.cube-view-controls button {
18+
display: inline-block;
19+
margin: 0;
20+
cursor: pointer;
21+
border-radius: 8px;
22+
border: 1px solid #bbb;
23+
padding: 8px 14px;
24+
background: #e8e8e8;
25+
}
26+
27+
.cube-view-controls button.active {
28+
background: #1a5fb4;
29+
color: #fff;
30+
border-color: #0d3d82;
31+
font-weight: 600;
32+
}
33+
</style>
34+
</head>
35+
<body>
36+
<div class="container">
37+
<h1>Model: cube sides</h1>
38+
<hr />
39+
<p>
40+
Use the buttons to set <code>entityTransform</code> so each labeled face points toward the camera.
41+
</p>
42+
43+
<div class="cube-view-controls" id="cube-view-controls">
44+
<button type="button" data-face="front">Front</button>
45+
<button type="button" data-face="back">Back</button>
46+
<button type="button" data-face="left">Left</button>
47+
<button type="button" data-face="right">Right</button>
48+
<button type="button" data-face="top">Top</button>
49+
<button type="button" data-face="bottom">Bottom</button>
50+
</div>
51+
52+
<model id="cube-sides-model" alt="A model of a cube with labeled sides">
53+
<source src="../assets/cube-sides.usdz" type="model/vnd.usdz+zip" />
54+
<source src="../assets/cube-sides.glb" type="model/gltf-binary" />
55+
<img
56+
src="../assets/cube-sides.jpg"
57+
alt="An image of a cube with labeled sides" />
58+
</model>
59+
</div>
60+
61+
<script type="module">
62+
const model = document.querySelector('#cube-sides-model');
63+
const controls = document.querySelector('#cube-view-controls');
64+
65+
const faceRotations = {
66+
front: [0, 0, 0],
67+
back: [0, 180, 0],
68+
left: [0, 90, 0],
69+
right: [0, -90, 0],
70+
top: [90, 0, 0],
71+
bottom: [-90, 0, 0],
72+
};
73+
74+
const DURATION_MS = 500;
75+
76+
let currentAngles = [...faceRotations.front];
77+
78+
let rafId = null;
79+
let animStartTime = 0;
80+
let animFrom = null;
81+
let animDeltas = null;
82+
let animTargetFace = null;
83+
84+
function easeInOutCubic(t) {
85+
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
86+
}
87+
88+
function shortestAngleDelta(fromDeg, toDeg) {
89+
return ((((toDeg - fromDeg + 360+180) % 360) + 360) % 360) - 180;
90+
}
91+
92+
function applyAngles(rx, ry, rz) {
93+
model.entityTransform = base.rotate(rx, ry, rz);
94+
}
95+
96+
function setActiveFace(face) {
97+
for (const btn of controls.querySelectorAll('button[data-face]')) {
98+
btn.classList.toggle('active', btn.dataset.face === face);
99+
}
100+
}
101+
102+
function stopAndSampleCurrent() {
103+
if (rafId === null) return;
104+
const now = performance.now();
105+
const u = Math.min(1, (now - animStartTime) / DURATION_MS);
106+
const e = easeInOutCubic(u);
107+
currentAngles = animFrom.map((a, i) => a + animDeltas[i] * e);
108+
cancelAnimationFrame(rafId);
109+
rafId = null;
110+
}
111+
112+
function goToFace(face) {
113+
stopAndSampleCurrent();
114+
setActiveFace(face);
115+
116+
const target = faceRotations[face];
117+
animFrom = [...currentAngles];
118+
animDeltas = [
119+
shortestAngleDelta(animFrom[0], target[0]),
120+
shortestAngleDelta(animFrom[1], target[1]),
121+
shortestAngleDelta(animFrom[2], target[2]),
122+
];
123+
if (animDeltas.every((d) => Math.abs(d) < 1e-6)) {
124+
currentAngles = [...target];
125+
applyAngles(...currentAngles);
126+
return;
127+
}
128+
animTargetFace = face;
129+
animStartTime = performance.now();
130+
131+
function frame(now) {
132+
const u = Math.min(1, (now - animStartTime) / DURATION_MS);
133+
const e = easeInOutCubic(u);
134+
const rx = animFrom[0] + animDeltas[0] * e;
135+
const ry = animFrom[1] + animDeltas[1] * e;
136+
const rz = animFrom[2] + animDeltas[2] * e;
137+
applyAngles(rx, ry, rz);
138+
139+
if (u < 1) {
140+
rafId = requestAnimationFrame(frame);
141+
} else {
142+
rafId = null;
143+
currentAngles = [...faceRotations[animTargetFace]];
144+
}
145+
}
146+
147+
rafId = requestAnimationFrame(frame);
148+
}
149+
150+
await model.ready;
151+
const base = model.entityTransform.translate(0, 0, -0.1);
152+
153+
applyAngles(...currentAngles);
154+
setActiveFace('front');
155+
156+
controls.addEventListener('click', (e) => {
157+
const btn = e.target.closest('button[data-face]');
158+
if (!btn) return;
159+
goToFace(btn.dataset.face);
160+
});
161+
</script>
162+
</body>
163+
</html>

0 commit comments

Comments
 (0)