<div class="canvas-container"></div>
<div class="page">
	<section class="section">
		<p>Scroll Down</p>
	</section>
	<section class="section">
		<h1>Bear vs. Witch</h1>
	</section>
	<section class="section bear-stats">
		Bear Stats
	</section>
	<section class="section witch-stats">
		Witch Stats
	</section>
	<section class="section winner">
		Winner
	</section>
</div>

html, body {
	width: 100%;
	height: 100%;
	margin: 0;
	padding: 0;
}

.canvas-container {
	z-index: 1;
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
}

.page {
	position: relative;
	z-index: 2;
}

.section {
	width: 100vw;
	height: 100vh;
	// outline: 1px solid red;
	display: flex;
}
View Compiled
console.clear();

import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.121.1/build/three.module.js";
import { gsap } from "https://cdn.skypack.dev/gsap";
import { ScrollTrigger } from "https://cdn.skypack.dev/gsap/ScrollTrigger";
import { GLTFLoader } from "https://cdn.jsdelivr.net/npm/three@0.121.1/examples/jsm/loaders/GLTFLoader.js";
// import threeGLTFLoader from "https://cdn.skypack.dev/three-gltf-loader"

gsap.registerPlugin(ScrollTrigger);

// --- CONSTS

const COLORS = {
	background: "white",
	light: "#ffffff",
	sky: "#aaaaff",
	ground: "#88ff88",
	blue: "steelblue"
};

const PI = Math.PI;

const wireframeMaterial = new THREE.MeshBasicMaterial({
	color: "white",
	wireframe: true
});

// --- SCENE
const scenes = {
	real: new THREE.Scene(),
	wire: new THREE.Scene()
};

scenes.wire.overrideMaterial = wireframeMaterial;

let size = { width: 0, height: 0 };

// const scene = new THREE.Scene();
scenes.real.background = new THREE.Color(COLORS.background);
scenes.real.fog = new THREE.Fog(COLORS.background, 15, 20);
scenes.wire.background = new THREE.Color(COLORS.blue);

const views = [
	{ height: 1, bottom: 0, scene: scenes.real, camera: null },
	{ height: 0, bottom: 0, scene: scenes.wire, camera: null }
];

// --- RENDERER

const renderer = new THREE.WebGLRenderer({
	antialias: true
});

renderer.physicallyCorrectLights = true;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ReinhardToneMapping;
renderer.toneMappingExposure = 5;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

const container = document.querySelector(".canvas-container");
container.appendChild(renderer.domElement);

// --- CAMERA
let cameraTarget = new THREE.Vector3(0, 3, 0);
views.forEach((view) => {
	view.camera = new THREE.PerspectiveCamera(
		40,
		size.width / size.height,
		0.1,
		100
	);
	view.camera.position.set(0, 1, 0);
	view.scene.add(view.camera);
});

// --- LIGHTS

const directionalLight = new THREE.DirectionalLight(COLORS.light, 2);
directionalLight.castShadow = true;
directionalLight.shadow.camera.far = 10;
directionalLight.shadow.mapSize.set(1024, 1024);
directionalLight.shadow.normalBias = 0.05;
directionalLight.position.set(2, 5, 3);

scenes.real.add(directionalLight);

const hemisphereLight = new THREE.HemisphereLight(
	COLORS.sky,
	COLORS.ground,
	0.5
);
scenes.real.add(hemisphereLight);

// --- FLOOR

const plane = new THREE.PlaneGeometry(100, 100);
const floorMaterial = new THREE.MeshStandardMaterial({ color: COLORS.ground });
const floor = new THREE.Mesh(plane, floorMaterial);
floor.receiveShadow = true;
floor.rotateX(-Math.PI * 0.5);

scenes.real.add(floor);

// --- ON RESIZE

const onResize = () => {
	size.width = container.clientWidth;
	size.height = container.clientHeight;

	views.forEach((view) => {
		view.camera.aspect = size.width / size.height;
		view.camera.updateProjectionMatrix();
	});

	renderer.setSize(size.width, size.height);
	renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
};

window.addEventListener("resize", onResize);
onResize();

// --- TICK

const tick = () => {
	views.forEach((view) => {
		view.camera.lookAt(cameraTarget);
		let bottom = size.height * view.bottom;
		let height = size.height * view.height;

		renderer.setViewport(0, 0, size.width, size.height);
		renderer.setScissor(0, bottom, size.width, height);
		renderer.setScissorTest(true);
		renderer.render(view.scene, view.camera);
	});
	window.requestAnimationFrame(() => tick());
};

tick();

const toLoad = [
	{
		name: "witch",
		file:
			"https://vazxmixjsiawhamofees.supabase.co/storage/v1/object/public/models/witch/model.gltf",
		group: new THREE.Group()
	},
	{
		name: "bear",
		file:
			"https://vazxmixjsiawhamofees.supabase.co/storage/v1/object/public/models/bear/model.gltf",
		group: new THREE.Group()
	}
];

const models = {};
const clones = {};
let cameras = null;
let witches = null;
let bears = null;

const setupAnimation = () => {
	cameras = { positions: [views[0].camera.position, views[1].camera.position] };

	witches = {
		position: [models.witch.position, clones.witch.position],
		rotation: [models.witch.rotation, clones.witch.rotation]
	};
	bears = {
		position: [models.bear.position, clones.bear.position],
		rotation: [models.bear.rotation, clones.bear.rotation]
	};

	gsap.set(witches.position, {x: 5});
	gsap.set(bears.position, {x: -5});
	
	ScrollTrigger.matchMedia({
		"(prefers-reduced-motion: no-preference)": desktopAnimation
	});
};

const desktopAnimation = () => {
	let section = 0;

	const tl = gsap.timeline({
		default: {
			duration: 1,
			ease: "power2.inOut"
		},
		scrollTrigger: {
			trigger: ".page",
			start: "top top",
			end: "bottom bottom",
			scrub: 0.1
		}
	});
	
	tl.to(cameraTarget, {y: 1}, section)
	tl.to(witches.position, { x: 1 }, section);
	tl.to(bears.position, { x: -1 }, section);
	tl.to(cameras.positions, {z: 5, ease: "power2.out"}, section)

	section += 1;
	tl.to(witches.position, { x: 5, ease: "power4.in" }, section);
	tl.to(bears.position, { z: 2 }, section);
	tl.to(views[1], {height: 1, ease: "none"}, section)

	section += 1;
	tl.to(witches.position, { x: 1, z: 2, ease: "power4.out" }, section);
	tl.to(bears.position, { z: 0, x: -5, ease: "power4.in" }, section);

	section += 1;
	tl.to(witches.position, { x: 1, z: 0 }, section);
	tl.to(bears.position, { z: 0, x: -1 }, section);
	tl.to(views[1], {height: 0, bottom: 1, ease: "none"}, section)
	
};

const LoadingManager = new THREE.LoadingManager(() => {
	setupAnimation();
});
const gltfLoader = new GLTFLoader(LoadingManager);

toLoad.forEach((item) => {
	gltfLoader.load(item.file, (model) => {
		model.scene.traverse((child) => {
			if (child instanceof THREE.Mesh) {
				child.receiveShadow = true;
				child.castShadow = true;
			}
		});
		item.group.add(model.scene);
		scenes.real.add(item.group);
		models[item.name] = item.group;
		const clone = item.group.clone();
		clones[item.name] = clone;
		scenes.wire.add(clone);
	});
});
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.