mirror of
https://github.com/storytold/spark.git
synced 2026-10-17 00:12:59 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
// Definition of all the food assets and some attributes for each
|
||||
export const FOOD_ASSETS = [
|
||||
{
|
||||
name: "Branzino Amarin",
|
||||
file: "branzino-amarin.spz",
|
||||
scale: 0.3,
|
||||
shadowSize: 1.0,
|
||||
},
|
||||
{
|
||||
name: "Iberico Sandwich",
|
||||
file: "iberico-sandwich-by-reserve.spz",
|
||||
scale: 0.6,
|
||||
shadowSize: 0,
|
||||
offsetY: -0.05,
|
||||
},
|
||||
{
|
||||
name: "Coral Caviar",
|
||||
file: "coral-caviar.spz",
|
||||
scale: 0.4,
|
||||
shadowSize: 0.7,
|
||||
},
|
||||
{
|
||||
name: "Pad Thai",
|
||||
file: "pad-thai.spz",
|
||||
scale: 0.2,
|
||||
shadowSize: 1.0,
|
||||
},
|
||||
{
|
||||
name: "Prime Rib",
|
||||
file: "primerib-tamos.spz",
|
||||
scale: 0.3,
|
||||
shadowSize: 1.1,
|
||||
},
|
||||
{
|
||||
name: "Double-Double",
|
||||
file: "double-double-from-InNOut.spz",
|
||||
scale: 0.5,
|
||||
shadowSize: 0.6,
|
||||
offsetY: 0.03,
|
||||
},
|
||||
{
|
||||
name: "Gyro Kebab",
|
||||
file: "gyro.spz",
|
||||
scale: 0.13,
|
||||
shadowSize: 0,
|
||||
offsetY: -0.13,
|
||||
},
|
||||
{
|
||||
name: "Mels Steak Sandwich",
|
||||
file: "steaksandwich-mels.spz",
|
||||
scale: 0.3,
|
||||
shadowSize: 0,
|
||||
offsetY: 0.08,
|
||||
},
|
||||
{
|
||||
name: "Clams and Caviar",
|
||||
file: "clams-and-caviar-by-ikoyi.spz",
|
||||
scale: 0.4,
|
||||
shadowSize: 0.72,
|
||||
},
|
||||
{
|
||||
name: "Tomahawk Niku",
|
||||
file: "tomahawk-niku.spz",
|
||||
scale: 0.3,
|
||||
shadowSize: 1.0,
|
||||
},
|
||||
{
|
||||
name: "Dh Burger",
|
||||
file: "burger-from-amboy.spz",
|
||||
scale: 0.4,
|
||||
shadowSize: 0.6,
|
||||
offsetY: 0.04,
|
||||
},
|
||||
];
|
||||
export const FOOD_URL = "/examples/assets/splats/food/";
|
||||
@@ -0,0 +1,215 @@
|
||||
<!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>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,91 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Mea+Culpa&display=swap");
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: "Mea Culpa", cursive;
|
||||
font-weight: 500;
|
||||
font-size: 5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: sans-serif;
|
||||
font-weight: normal;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-family: "Mea Culpa", cursive;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#menu {
|
||||
position: absolute;
|
||||
left: -12rem;
|
||||
display: inline-block;
|
||||
background-color: rgba(223, 203, 171, 0.8);
|
||||
padding: 2rem 2rem;
|
||||
margin-top: 4rem;
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.7rem;
|
||||
left: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
#menu .border {
|
||||
border: 1.0px solid rgb(92, 92, 92);
|
||||
padding: 1rem 3rem;
|
||||
}
|
||||
|
||||
#menu > .border {
|
||||
border: 1.5px solid rgb(49, 49, 49);
|
||||
padding: 5px 6px;
|
||||
}
|
||||
|
||||
#menu a {
|
||||
font-style: italic;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: rgb(30, 13, 140);
|
||||
}
|
||||
|
||||
#menu a:hover {
|
||||
/* border-bottom: 1px solid black; */
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#menu a.active {
|
||||
text-decoration: underline;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#menu a.active::before {
|
||||
content: "🙜 ";
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
#menu a.active::after {
|
||||
content: " 🙟";
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
#menu_list {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
#menu h2 a {
|
||||
font-weight: normal;
|
||||
color: black;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#loading {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 40vh;
|
||||
left: 45vw;
|
||||
}
|
||||
Reference in New Issue
Block a user