mirror of
https://github.com/storytold/spark.git
synced 2026-10-17 00:12:59 +00:00
Add preloader to interactivity example
This commit is contained in:
@@ -7,7 +7,6 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<img src="loading.gif" alt="loading" id="loading">
|
|
||||||
<div id="menu">
|
<div id="menu">
|
||||||
<div class="border">
|
<div class="border">
|
||||||
<div class="border">
|
<div class="border">
|
||||||
@@ -36,6 +35,7 @@
|
|||||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||||
import { FOOD_ASSETS, FOOD_URL } from './food.js';
|
import { FOOD_ASSETS, FOOD_URL } from './food.js';
|
||||||
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
|
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
|
||||||
|
import { preloadSplats } from "/examples/js/preloader.js";
|
||||||
|
|
||||||
// Add food items to the menu
|
// Add food items to the menu
|
||||||
FOOD_ASSETS.forEach((food, i) => {
|
FOOD_ASSETS.forEach((food, i) => {
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
el.textContent = food.name;
|
el.textContent = food.name;
|
||||||
el.href = 'javascript:;';
|
el.href = 'javascript:;';
|
||||||
el.addEventListener('click', async function () {
|
el.addEventListener('click', async function () {
|
||||||
await switchToFood(i);
|
switchToFood(i);
|
||||||
});
|
});
|
||||||
document.getElementById('menu_list').appendChild(el);
|
document.getElementById('menu_list').appendChild(el);
|
||||||
});
|
});
|
||||||
@@ -58,50 +58,6 @@
|
|||||||
const camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
|
const camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||||
camera.position.set(0, 0.9, -1.2);
|
camera.position.set(0, 0.9, -1.2);
|
||||||
|
|
||||||
// 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 });
|
|
||||||
const shadow = new THREE.Mesh(geometry, material);
|
|
||||||
shadow.visible = false;
|
|
||||||
shadow.castShadow = true;
|
|
||||||
shadow.position.set(0, 0.1, 0);
|
|
||||||
scene.add(shadow);
|
|
||||||
|
|
||||||
// Add table
|
|
||||||
const gltfLoader = new GLTFLoader();
|
|
||||||
const modelURL = await getAssetFileURL("table.glb");
|
|
||||||
const gltfTable = await gltfLoader.loadAsync(modelURL);
|
|
||||||
const table = gltfTable.scene;
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Setup mouse controls to orbit the camera around
|
// Setup mouse controls to orbit the camera around
|
||||||
const controls = new OrbitControls(camera, renderer.domElement);
|
const controls = new OrbitControls(camera, renderer.domElement);
|
||||||
controls.target.set(0.2, 0, 0);
|
controls.target.set(0.2, 0, 0);
|
||||||
@@ -110,10 +66,18 @@
|
|||||||
controls.enablePan = false;
|
controls.enablePan = false;
|
||||||
controls.update();
|
controls.update();
|
||||||
|
|
||||||
|
|
||||||
// Current and next food splats
|
// Current and next food splats
|
||||||
let food, nextFood;
|
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;
|
||||||
|
|
||||||
const loadingIcon = document.getElementById('loading');
|
|
||||||
// Transition length in frames.
|
// Transition length in frames.
|
||||||
// Two transitions: one for fading out the old food, another for fading in the next one
|
// Two transitions: one for fading out the old food, another for fading in the next one
|
||||||
const TRANSITION_LENGTH = 60;
|
const TRANSITION_LENGTH = 60;
|
||||||
@@ -121,10 +85,64 @@
|
|||||||
let fadeOutTime = null;
|
let fadeOutTime = null;
|
||||||
let fadeInTime = null;
|
let fadeInTime = null;
|
||||||
|
|
||||||
// Load first food by default
|
// Preload all splat files
|
||||||
await switchToFood(0);
|
let splats;
|
||||||
|
preloadSplats(FOOD_ASSETS.map(t => t.file)).then(loaded_splats => {
|
||||||
|
splats = loaded_splats;
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
renderer.setAnimationLoop(function animate(time) {
|
async function init() {
|
||||||
|
// show menu
|
||||||
|
document.getElementById('menu').style.display = 'block';
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Load first food by default
|
||||||
|
switchToFood(0);
|
||||||
|
|
||||||
|
// Start render loop
|
||||||
|
renderer.setAnimationLoop(animate);
|
||||||
|
}
|
||||||
|
|
||||||
|
function animate(time) {
|
||||||
controls.update();
|
controls.update();
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
const rotation = time / 10000;
|
const rotation = time / 10000;
|
||||||
@@ -161,20 +179,17 @@
|
|||||||
fadeInTime = null;
|
fadeInTime = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// Change food from menu link
|
// Change food from menu link
|
||||||
async function switchToFood(foodIndex) {
|
function switchToFood(foodIndex) {
|
||||||
|
|
||||||
// already transitioning
|
// already transitioning
|
||||||
if (fadeOutTime !== null || fadeInTime !== null) return;
|
if (fadeOutTime !== null || fadeInTime !== null) return;
|
||||||
|
|
||||||
const foodItem = FOOD_ASSETS[foodIndex];
|
const foodItem = FOOD_ASSETS[foodIndex];
|
||||||
|
|
||||||
const splatURL = await getAssetFileURL(foodItem.file);
|
nextFood = splats[foodItem.file];
|
||||||
nextFood = new SplatMesh({ url: splatURL });
|
|
||||||
nextFood.quaternion.set(1, 0, 0, 0);
|
nextFood.quaternion.set(1, 0, 0, 0);
|
||||||
|
|
||||||
// Customize splat depending on the settings set for this food
|
// Customize splat depending on the settings set for this food
|
||||||
@@ -188,7 +203,6 @@
|
|||||||
// Setup shadow to the required size, and make it visible only when the splat is initialized
|
// Setup shadow to the required size, and make it visible only when the splat is initialized
|
||||||
nextFood.initialized.then(() => {
|
nextFood.initialized.then(() => {
|
||||||
shadow.visible = false;
|
shadow.visible = false;
|
||||||
loadingIcon.style.display = 'none';
|
|
||||||
fadeOutTime = 0; // Fade in next food
|
fadeOutTime = 0; // Fade in next food
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -197,10 +211,6 @@
|
|||||||
// toggle menu item
|
// toggle menu item
|
||||||
const menu_items = document.getElementById('menu_list').children;
|
const menu_items = document.getElementById('menu_list').children;
|
||||||
for (let i = 0; i < menu_items.length; i++) {
|
for (let i = 0; i < menu_items.length; i++) {
|
||||||
if (foodIndex == i) {
|
|
||||||
loadingIcon.style.display = 'inline';
|
|
||||||
menu_items[i].appendChild(loadingIcon);
|
|
||||||
}
|
|
||||||
menu_items[i].classList.toggle('active', foodIndex == i);
|
menu_items[i].classList.toggle('active', foodIndex == i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
background-color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
@@ -24,7 +25,7 @@ h3 {
|
|||||||
#menu {
|
#menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -12rem;
|
left: -12rem;
|
||||||
display: inline-block;
|
display: none;
|
||||||
background-color: rgba(223, 203, 171, 0.8);
|
background-color: rgba(223, 203, 171, 0.8);
|
||||||
padding: 2rem 2rem;
|
padding: 2rem 2rem;
|
||||||
margin-top: 4rem;
|
margin-top: 4rem;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
@@ -0,0 +1,41 @@
|
|||||||
|
import { SplatMesh } from "@forge-gfx/forge";
|
||||||
|
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
|
||||||
|
|
||||||
|
// Add loading icon in the center of the screen
|
||||||
|
function addLoader(loading_icon_color) {
|
||||||
|
const loaderIcon = new Image();
|
||||||
|
loaderIcon.src = `/examples/js/forge_loading_${loading_icon_color}.gif`;
|
||||||
|
loaderIcon.id = "_forge_loader";
|
||||||
|
loaderIcon.style.position = "absolute";
|
||||||
|
loaderIcon.style.zIndex = 99999;
|
||||||
|
loaderIcon.style.left = "50%";
|
||||||
|
loaderIcon.style.top = "50%";
|
||||||
|
loaderIcon.style.transform = "translate(-50%, -50%)";
|
||||||
|
loaderIcon.style.pointerEvents = "none";
|
||||||
|
document.body.appendChild(loaderIcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLoader() {
|
||||||
|
const el = document.getElementById("_forge_loader");
|
||||||
|
el.parentNode.removeChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
// preload splats, returning a map [filename -> SplatMesh]
|
||||||
|
export async function preloadSplats(assets, loading_icon_color = "black") {
|
||||||
|
addLoader(loading_icon_color);
|
||||||
|
const map = {};
|
||||||
|
await Promise.all(
|
||||||
|
assets.map(async (asset) => {
|
||||||
|
const splatURL = await getAssetFileURL(asset);
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
map[asset] = new SplatMesh({
|
||||||
|
url: splatURL,
|
||||||
|
onLoad: () => resolve(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
removeLoader();
|
||||||
|
return map;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user