carousel.ts
examples/spark/hello-world/carousel.ts · raw · open the example · all examples · site home
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
// Spark's hello-world/carousel.html: three scans fading into one another
// every 5 seconds, orbited by the camera controls. The fades set each
// model's opacity (js/objects.js); only the shown scan is visible.
import * as THREE from "three";
import { CameraControls } from "@sparkjsdev/radix";
import { asset } from "../js/assets.js";
import { SplatSet } from "../js/objects.js";
import { examplePage, windowRenderer } from "../js/page.js";
const page = examplePage("hello-world");
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 1000);
// createRenderer, sized to the window and added to the page; it follows
// window resizes, the camera's aspect too (js/page.ts)
const renderer = await windowRenderer(camera);
const controls = new CameraControls({
canvas: renderer.domElement, object: camera, storageKey: "spark-hello-world", settings: { mode: "orbit", minDistance: 2, maxDistance: 3 },
});
controls.lookAt(new THREE.Vector3(0, 0, -3));
// One renderer drawing the visible models; their SH seen from the camera
const splats = new SplatSet({ renderer, scene });
const SCANS = [
{ file: "butterfly.spz", scale: 1.2 },
{ file: "furry-logo-pedestal.spz", scale: 0.45 },
{ file: "forge.spz", scale: 1 },
];
const models = await Promise.all(SCANS.map(async ({ file, scale }) => {
const model = await splats.load(asset(file), { onProgress: page.progress(file) });
model.rotation.x = Math.PI;
model.position.set(0, 0, -3);
model.scale.setScalar(scale);
return model;
}));
let current = 0;
models.forEach((model, i) => (model.visible = i === current));
page.ready(renderer);
// Every 5 s the shown scan fades out over FADE ms, then the next fades in
const FADE = 1000;
const easeInOutSine = (x: number) => -(Math.cos(Math.PI * x) - 1) / 2;
let fade: number | undefined;
setInterval(() => (fade = 0), 5000);
function step(delta: number) {
if (fade === undefined) {
return;
}
const before = fade;
fade += delta * 1000;
if (fade < FADE) {
models[current].opacity = 1 - easeInOutSine(fade / FADE);
return;
}
if (before < FADE) {
models[current].visible = false;
current = (current + 1) % models.length;
models[current].visible = true;
}
models[current].opacity = easeInOutSine(Math.min((fade - FADE) / FADE, 1));
if (fade >= 2 * FADE) {
fade = undefined;
}
}
let last = 0;
renderer.setAnimationLoop((time) => {
const delta = Math.min((time - last) / 1000, 0.1);
last = time;
controls.update();
models[current].rotation.y = time / 1500;
step(delta);
renderer.render(scene, camera);
});