Skip to content

Instantly share code, notes, and snippets.

@jdrew1303
Created May 10, 2026 13:03
Show Gist options
  • Select an option

  • Save jdrew1303/24a9b2c2c12c23f78766b7f7c73af8fc to your computer and use it in GitHub Desktop.

Select an option

Save jdrew1303/24a9b2c2c12c23f78766b7f7c73af8fc to your computer and use it in GitHub Desktop.
three_site_demo
<!DOCTYPE html>
<html lang="en" data-color-scheme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<title>Three.js High Fidelity Scaffold</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
/* ==========================================================================
1. DESIGN TOKENS & BASE (Kept from original)
========================================================================== */
:root {
--page-border: #dcdcdc;
--page-background: #ffffff;
--page-text: #121212;
--page-max-width: 100%;
}
@media (min-width: 46.25em) { :root { --page-max-width: 740px; } }
@media (min-width: 61.25em) { :root { --page-max-width: 980px; } }
@media (min-width: 71.25em) { :root { --page-max-width: 1140px; } }
@media (min-width: 81.25em) { :root { --page-max-width: 1300px; } }
body {
margin: 0; padding: 0;
background-color: #f6f6f6;
color: var(--page-text);
font-family: 'JetBrains Mono', 'Menlo', 'Cascadia Code', monospace;
font-variant-ligatures: contextual;
letter-spacing: -0.02em;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
h1, h2 { font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em; }
/* ==========================================================================
2. PAGE CONTAINER & HERO
========================================================================== */
.page-container {
position: relative;
margin: 0 auto;
max-width: var(--page-max-width);
background-color: var(--page-background);
border-left: 1px solid var(--page-border);
border-right: 1px solid var(--page-border);
box-sizing: border-box;
}
@media (min-width: 61.25em) {
.page-container::after {
content: ""; position: absolute; top: 0; bottom: 0; left: 50%;
width: 1px; background-color: var(--page-border); z-index: 5; pointer-events: none;
}
}
.article-hero {
position: relative;
background-color: #000;
border-bottom: 1px solid var(--page-border);
isolation: isolate; z-index: 10;
}
.article-hero::before {
content: ""; position: absolute; inset: 0;
background: transparent; z-index: 6; pointer-events: none;
}
.hero-media {
position: sticky; top: 0; height: 100vh; width: 100%;
will-change: transform; overflow: hidden;
background: #050810; /* Dark backdrop for the 3D scene */
}
/* Three.js Canvas adjustments */
.hero-media canvas, .three-mount canvas {
display: block; width: 100% !important; height: 100% !important;
opacity: 0.8;
}
.hero-furniture {
position: relative; z-index: 10; margin-top: -100vh;
pointer-events: none; padding-bottom: 10vh;
}
.hero-furniture > * { pointer-events: auto; }
.hero-headline { padding-top: 45vh; padding-left: 20px; }
.hero-headline h1 { color: #fff; margin: 0 0 24px 0; padding-top: 12px; max-width: 500px; border-top: 6px solid #fff; }
.hero-headline h1 span { font-weight: 700; line-height: 1; display: block; }
.hero-headline h1 span:first-of-type { font-size: clamp(2.5rem, 5vw, 4rem); padding-bottom: 10px; }
.hero-headline h1 span:last-of-type { font-size: clamp(1.2rem, 2vw, 1.7rem); font-weight: 500; }
.hero-standfirst { padding: 0 20px 20vh 20px; max-width: 600px; }
.hero-standfirst p { font-size: 1.4rem; line-height: 1.3; color: #fff; }
@media (min-width: 71.25em) {
.hero-headline, .hero-standfirst { padding-left: 60px; }
}
/* ==========================================================================
3. SCROLLYTELLING SECTIONS
========================================================================== */
.scroll-section { position: relative; border-bottom: 1px solid var(--page-border); }
@media (min-width: 61.25em) {
.scroll-section {
display: grid; grid-template-columns: 1fr 1fr; grid-template-areas: "text media";
}
}
.scroll-text { padding: 10vh 20px; z-index: 10; position: relative; }
@media (min-width: 61.25em) {
.scroll-text { grid-area: text; padding: 15vh 60px 25vh 60px; }
}
.scroll-media {
position: sticky; top: 0; height: 100vh; z-index: 1;
overflow: hidden; background-color: #000;
opacity: 0; transition: opacity 1s ease-in-out;
will-change: opacity;
display: flex; flex-direction: column;
}
.scroll-media.is-visible { opacity: 1; }
@media (min-width: 61.25em) { .scroll-media { grid-area: media; } }
/* The container for the Three.js canvases in the panels */
.three-mount {
flex: 1; width: 100%; height: 100%;
overflow: hidden; box-sizing: border-box;
background: #0a0e1a;
}
/* Typography */
.section-heading h2 { font-size: clamp(2rem, 4vw, 3.2rem); margin: 0 0 30px 0; line-height: 1; }
.body-text { font-size: 1.25rem; line-height: 1.6; margin-bottom: 1.5rem; max-width: 500px; }
.media-caption {
font-size: 0.8rem; background: rgba(18, 18, 18, 0.8); color: #fff;
padding: 8px 12px; position: absolute; bottom: 0; right: 0; z-index: 10;
}
</style>
</head>
<body>
<div class="page-container">
<header class="article-hero">
<div class="hero-media" id="hero-mount"></div>
<div class="hero-furniture">
<div class="hero-headline">
<h1>
<span>A war foretold:</span>
<span>how the CIA and MI6 got hold of Putin's Ukraine plans</span>
</h1>
</div>
<div class="hero-standfirst">
<p>Drawing on more than 100 interviews with senior intelligence officials, this account details how the West uncovered Vladimir Putin's plans to invade.</p>
</div>
</div>
</header>
<main>
<div class="scroll-section">
<div class="scroll-text">
<div class="section-heading">
<h2>Putin starts planning</h2>
</div>
<p class="body-text">The CIA discovered an awful lot about Putin's plans to invade Ukraine, but one thing they never worked out for sure is when he first made up his mind to go all-in.</p>
<p class="body-text">During those months, Putin passed constitutional amendments to ensure he could stay in power beyond 2024. Then, locked away in isolation for months during Covid, he devoured books on Russian history and pondered his own place in it.</p>
</div>
<div class="scroll-media" id="panel-1">
<div class="three-mount" id="mount-1"></div>
<figcaption class="media-caption">Fig 1. Component assembly structure.</figcaption>
</div>
</div>
<div class="scroll-section">
<div class="scroll-text">
<div class="section-heading">
<h2>The final warnings</h2>
</div>
<p class="body-text">On 22 February, the day after Putin's theatrical performance, Ukraine's own security council met in Kyiv. Zaluzhnyi tried to canvass support for martial law.</p>
<p class="body-text">A few hours later, Zelenskyy was handed a red folder containing a top-secret intelligence report about a "direct physical threat" to the president.</p>
</div>
<div class="scroll-media" id="panel-2">
<div class="three-mount" id="mount-2"></div>
<figcaption class="media-caption">Fig 2. Spatial analysis view.</figcaption>
</div>
</div>
<div class="scroll-section">
<div class="scroll-text">
<div class="section-heading">
<h2>See it in practice</h2>
</div>
<p class="body-text">The system draws on the local development plan, national planning policy, and case history. The officer stays in control; the model accelerates the legwork.</p>
<p class="body-text">This is not a chatbot bolted on to a website. It is a deliberate, audited workflow — with clear escalation paths and a paper trail regulators can follow.</p>
</div>
<div class="scroll-media" id="panel-3">
<div class="three-mount" id="mount-3"></div>
<figcaption class="media-caption">Fig 3. Topographic extrapolation.</figcaption>
</div>
</div>
</main>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// =========================================================================
// THREE.JS BOILERPLATE & MANAGER
// =========================================================================
// Store all active Three.js instances to manage resizing and animation
const threeInstances = [];
/**
* Helper function to build a standard Three.js scene.
* You can eventually replace the geometry with GLTFLoader for CAD models.
*/
function createThreeScene(mountElement, customGeometry, wireframeColor) {
// 1. Setup Scene
const scene = new THREE.Scene();
// 2. Setup Camera
const camera = new THREE.PerspectiveCamera(75, mountElement.clientWidth / mountElement.clientHeight, 0.1, 1000);
camera.position.z = 5;
// 3. Setup Renderer
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
renderer.setSize(mountElement.clientWidth, mountElement.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
mountElement.appendChild(renderer.domElement);
// 4. Add Lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);
// 5. Add Object (Placeholder CAD look: Solid core with colored wireframe)
const material = new THREE.MeshStandardMaterial({
color: 0x222222,
roughness: 0.5,
metalness: 0.8
});
const wireMaterial = new THREE.MeshBasicMaterial({
color: wireframeColor || 0x00e5a0,
wireframe: true
});
const mesh = new THREE.Mesh(customGeometry, material);
const wireMesh = new THREE.Mesh(customGeometry, wireMaterial);
// Slightly scale up wireframe to prevent z-fighting
wireMesh.scale.set(1.01, 1.01, 1.01);
mesh.add(wireMesh);
scene.add(mesh);
// Return the instance data
return {
mount: mountElement,
scene,
camera,
renderer,
mesh,
isRunning: false,
animationFrameId: null
};
}
/**
* The render loop for a specific scene
*/
function animateScene(instance) {
if (!instance.isRunning) return;
instance.animationFrameId = requestAnimationFrame(() => animateScene(instance));
// Slow rotation to simulate a CAD turntable
instance.mesh.rotation.x += 0.005;
instance.mesh.rotation.y += 0.005;
instance.renderer.render(instance.scene, instance.camera);
}
/**
* Start/Stop functions to be called by Intersection Observer
*/
function playScene(instance) {
if (!instance.isRunning) {
instance.isRunning = true;
animateScene(instance);
}
}
function pauseScene(instance) {
if (instance.isRunning) {
instance.isRunning = false;
cancelAnimationFrame(instance.animationFrameId);
}
}
// =========================================================================
// INITIALIZE SCENES
// =========================================================================
// HERO: Big Torus Knot
const heroInstance = createThreeScene(
document.getElementById('hero-mount'),
new THREE.TorusKnotGeometry(1.5, 0.4, 128, 16),
0x4fc3f7 // Blue wireframe
);
threeInstances.push(heroInstance);
// PANEL 1: Box
const p1Instance = createThreeScene(
document.getElementById('mount-1'),
new THREE.BoxGeometry(2, 2, 2),
0x00e5a0 // Green wireframe
);
threeInstances.push({ ...p1Instance, panelId: 'panel-1' });
// PANEL 2: Sphere
const p2Instance = createThreeScene(
document.getElementById('mount-2'),
new THREE.SphereGeometry(1.5, 16, 16),
0xffb300 // Amber wireframe
);
threeInstances.push({ ...p2Instance, panelId: 'panel-2' });
// PANEL 3: Cone
const p3Instance = createThreeScene(
document.getElementById('mount-3'),
new THREE.ConeGeometry(1.5, 3, 16),
0xff5252 // Red wireframe
);
threeInstances.push({ ...p3Instance, panelId: 'panel-3' });
// =========================================================================
// OBSERVERS (Visibility & Animation Control)
// =========================================================================
// Observer for CSS fade-in
const fadeObserver = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) e.target.classList.add('is-visible');
});
}, { threshold: 0.1 });
document.querySelectorAll('.scroll-media').forEach(f => fadeObserver.observe(f));
// Observer for Three.js rendering (Pause when off-screen to save battery/GPU)
const renderObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
// Find the specific instance associated with this DOM element
const instance = threeInstances.find(inst =>
inst.mount === entry.target || inst.panelId === entry.target.id
);
if (instance) {
if (entry.isIntersecting) {
playScene(instance);
} else {
pauseScene(instance);
}
}
});
}, { threshold: 0.05 }); // Start rendering just before it fully comes into view
// Observe the Hero
renderObserver.observe(document.getElementById('hero-mount'));
// Observe the Panels
renderObserver.observe(document.getElementById('panel-1'));
renderObserver.observe(document.getElementById('panel-2'));
renderObserver.observe(document.getElementById('panel-3'));
// =========================================================================
// RESIZE HANDLER
// =========================================================================
window.addEventListener('resize', () => {
threeInstances.forEach(instance => {
const width = instance.mount.clientWidth;
const height = instance.mount.clientHeight;
instance.renderer.setSize(width, height);
instance.camera.aspect = width / height;
instance.camera.updateProjectionMatrix();
// Force a render frame immediately on resize so it doesn't look blank
// if it's currently paused but being resized.
if (!instance.isRunning) {
instance.renderer.render(instance.scene, instance.camera);
}
});
});
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment