Files
spark/examples/interactivity/index.html
T
Diego Marcos 3332f609a9 Initial commit
2025-05-24 00:14:53 -07:00

216 lines
6.9 KiB
HTML

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>Forge • Multiple Splats</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<img src="loading.gif" alt="loading" id="loading">
<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/",
"@forge-gfx/forge": "/dist/forge.module.js"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { SplatMesh } from "@forge-gfx/forge";
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";
// 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 () {
await switchToFood(i);
});
document.getElementById('menu_list').appendChild(el);
});
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);
// 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
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;
const loadingIcon = document.getElementById('loading');
// Transition length in frames.
// Two transitions: one for fading out the old food, another for fading in the next one
const TRANSITION_LENGTH = 60;
// Transition timers. `null` if transition is not active
let fadeOutTime = null;
let fadeInTime = null;
// Load first food by default
await switchToFood(0);
renderer.setAnimationLoop(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
if (food) food.dispose();
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
async function switchToFood(foodIndex) {
// already transitioning
if (fadeOutTime !== null || fadeInTime !== null) return;
const foodItem = FOOD_ASSETS[foodIndex];
const splatURL = await getAssetFileURL(foodItem.file);
nextFood = new SplatMesh({ url: splatURL });
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;
loadingIcon.style.display = 'none';
fadeOutTime = 0; // Fade in next food
});
scene.add(nextFood);
// toggle menu item
const menu_items = document.getElementById('menu_list').children;
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);
}
}
function easeInOutSine(x) {
return -(Math.cos(Math.PI * x) - 1) / 2;
}
</script>
</body>
</html>