forked from sparkjsdev/spark
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
245 lines (206 loc) · 7.81 KB
/
index.html
File metadata and controls
245 lines (206 loc) · 7.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Spark • Multiple Splats</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<a href="#" id="mobile_button">Menu</a>
<div id="menu">
<div class="border">
<div class="border">
<h3>Tipatat's Splat Restaurant</h3>
<h1> Menu </h1>
<h2>CHEF SPECIALS</h2>
<div id="menu_list"></div>
<h2>Food scans by <a href="https://x.com/tipatat" target="_blank">Tipatat</a></h2>
</div>
</div>
</div>
<script type="importmap">
{
"imports": {
"three": "/examples/js/vendor/three/build/three.module.js",
"three/addons/": "/examples/js/vendor/three/examples/jsm/",
"@sparkjsdev/spark": "/dist/spark.module.js"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { SplatMesh } from "@sparkjsdev/spark";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { EXRLoader } from "three/addons/loaders/EXRLoader.js";
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { FOOD_ASSETS, FOOD_URL } from './food.js';
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
import { preloadSplats } from "/examples/js/preloader.js";
// Add food items to the menu
FOOD_ASSETS.forEach((food, i) => {
const el = document.createElement("a");
el.textContent = food.name;
el.href = 'javascript:;';
el.addEventListener('click', async function () {
switchToFood(i);
});
document.getElementById('menu_list').appendChild(el);
});
// Setup mobile menu button
let IS_MOBILE = 'ontouchstart' in window || navigator.msMaxTouchPoints;
document.getElementById('mobile_button').addEventListener('click', () => {
document.getElementById('menu').classList.toggle('visible');
});
const scene = new THREE.Scene();
const renderer = new THREE.WebGLRenderer();
renderer.shadowMap.enabled = true;
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Setup camera
const camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0.9, -1.2);
// handle windows resize
window.addEventListener('resize', onWindowResize, false);
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// Setup mouse controls to orbit the camera around
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0.2, 0, 0);
controls.minDistance = 0.8;
controls.maxDistance = 2.3;
controls.enablePan = false;
controls.update();
// Current and next food splats
let food, nextFood;
// Other items
let table, shadow;
// Add table
const gltfLoader = new GLTFLoader();
const modelURL = await getAssetFileURL("table.glb");
const gltfTable = await gltfLoader.loadAsync(modelURL);
table = gltfTable.scene;
// Transition length in frames.
// Two transitions: one for fading out the old food, another for fading in the next one
const TRANSITION_LENGTH = IS_MOBILE ? 30 : 60;
// Transition timers. `null` if transition is not active
let fadeOutTime = null;
let fadeInTime = null;
// Preload all splat files
let splats;
preloadSplats(FOOD_ASSETS.map(t => t.file)).then(loaded_splats => {
splats = loaded_splats;
init();
});
async function init() {
// Setup lighting
const spotLight = new THREE.SpotLight(0xffcc88);
spotLight.position.set(0, 1, 0);
spotLight.castShadow = true;
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.camera.near = 0.1;
spotLight.shadow.camera.far = 5;
spotLight.angle = 0.9;
spotLight.penumbra = 1;
spotLight.intensity = 3;
scene.add(spotLight);
const fillLight = new THREE.PointLight(0xffcc88, 0.2);
fillLight.position.set(0, 0, -3);
scene.add(fillLight);
// Splats don't project shadows, so we add a cylinder below the spotlight to fake one ;)
const geometry = new THREE.CylinderGeometry(0.45, 0.45, 0.04, 40, 1);
const material = new THREE.MeshPhongMaterial({ colorWrite: false, depthWrite: false });
shadow = new THREE.Mesh(geometry, material);
shadow.visible = false;
shadow.castShadow = true;
shadow.position.set(0, 0.1, 0);
scene.add(shadow);
// Set the table cloth to receive shadows
const tableCloth = table.children.find(item => item.name == 'cover');
tableCloth.receiveShadow = true;
scene.add(table);
// Add floor
const plane = new THREE.PlaneGeometry(10, 10);
const floormat = new THREE.MeshPhongMaterial({ color: 0x777777 });
const floor = new THREE.Mesh(plane, floormat);
floor.rotation.x = -Math.PI / 2;
floor.position.set(0, -1.397, 0);
scene.add(floor);
// Show menu
document.getElementById('menu').classList.add('visible');
if (IS_MOBILE) document.getElementById('mobile_button').classList.add('visible');
// Load first food by default
switchToFood(0);
// Start render loop
renderer.setAnimationLoop(animate);
}
function animate(time) {
controls.update();
renderer.render(scene, camera);
const rotation = time / 10000;
table.rotation.y = rotation;
if (food) food.rotation.y = -rotation;
if (nextFood) nextFood.rotation.y = -rotation;
// fade out
if (fadeOutTime !== null) {
fadeOutTime++;
if (fadeOutTime < TRANSITION_LENGTH) {
if (food) food.opacity = 1 - easeInOutSine(fadeOutTime / TRANSITION_LENGTH);
} else {
// Fade out finished
fadeOutTime = null;
// Fade in next food
fadeInTime = 0;
shadow.visible = true;
shadow.scale.setScalar(shadow.nextScale);
}
}
// fade in
if (fadeInTime != null && nextFood.isInitialized) {
fadeInTime++;
if (fadeInTime < TRANSITION_LENGTH) {
nextFood.opacity = easeInOutSine(fadeInTime / TRANSITION_LENGTH);
} else {
// Fade in finished
food = nextFood;
fadeInTime = null;
}
}
};
// Change food from menu link
function switchToFood(foodIndex) {
// already transitioning
if (fadeOutTime !== null || fadeInTime !== null) return;
const foodItem = FOOD_ASSETS[foodIndex];
nextFood = splats[foodItem.file];
nextFood.quaternion.set(1, 0, 0, 0);
// Customize splat depending on the settings set for this food
if (foodItem['offsetY']) nextFood.position.set(0, foodItem.offsetY, 0);
nextFood.scale.setScalar(foodItem.scale);
shadow.nextScale = foodItem.shadowSize;
// Set opacity to 0 to prepare the fade in transition
nextFood.opacity = 0;
// Setup shadow to the required size, and make it visible only when the splat is initialized
nextFood.initialized.then(() => {
shadow.visible = false;
fadeOutTime = 0; // Fade in next food
});
scene.add(nextFood);
// Hide menu if on mobile
if (IS_MOBILE) document.getElementById('menu').classList.remove('visible');
// toggle menu item
const menu_items = document.getElementById('menu_list').children;
for (let i = 0; i < menu_items.length; i++) {
menu_items[i].classList.toggle('active', foodIndex == i);
}
}
function easeInOutSine(x) {
return -(Math.cos(Math.PI * x) - 1) / 2;
}
</script>
</body>
</html>