Skip to content

Instantly share code, notes, and snippets.

@TheAnonymous
Created May 1, 2026 08:59
Show Gist options
  • Select an option

  • Save TheAnonymous/92862e8b86a53ac42ce6990ccfb295a1 to your computer and use it in GitHub Desktop.

Select an option

Save TheAnonymous/92862e8b86a53ac42ce6990ccfb295a1 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wireframe Sculpture</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #08080f; font-family: 'Segoe UI', system-ui, sans-serif; }
canvas { display: block; }
#vignette {
position: fixed; inset: 0;
z-index: 2;
pointer-events: none;
background: radial-gradient(ellipse at center, transparent 45%, rgba(0,0,0,0.5) 100%);
}
#info {
position: fixed;
bottom: 20px; left: 50%;
transform: translateX(-50%);
color: rgba(180,160,130,0.15);
font-size: 0.65rem;
letter-spacing: 0.1em;
z-index: 5;
pointer-events: none;
text-align: center;
}
#controls {
position: fixed;
left: 20px;
bottom: 56px;
z-index: 5;
background: rgba(8,8,15,0.7);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(180,160,130,0.12);
border-radius: 12px;
padding: 14px 18px 8px;
min-width: 170px;
user-select: none;
}
#controls .ctrl-group { margin-bottom: 6px; }
#controls .ctrl-group:last-child { margin-bottom: 2px; }
#controls .ctrl-label {
display: flex;
justify-content: space-between;
align-items: center;
color: rgba(180,160,130,0.45);
font-size: 0.6rem;
letter-spacing: 0.06em;
margin-bottom: 1px;
}
#controls .ctrl-label .val {
color: rgba(160,180,200,0.3);
font-variant-numeric: tabular-nums;
}
#controls input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 2px;
background: rgba(180,160,130,0.12);
border-radius: 1px;
outline: none;
cursor: pointer;
}
#controls input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 10px; height: 10px;
border-radius: 50%;
background: rgba(180,160,130,0.5);
border: 1px solid rgba(180,160,130,0.15);
cursor: pointer;
transition: background 0.2s;
}
#controls input[type="range"]::-webkit-slider-thumb:hover {
background: rgba(200,180,150,0.7);
}
#controls input[type="range"]::-moz-range-thumb {
width: 10px; height: 10px;
border-radius: 50%;
background: rgba(180,160,130,0.5);
border: 1px solid rgba(180,160,130,0.15);
cursor: pointer;
}
#fullscreen-btn {
position: fixed;
top: 16px; right: 16px;
z-index: 5;
background: rgba(8,8,15,0.6);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(180,160,130,0.12);
border-radius: 8px;
color: rgba(180,160,130,0.45);
width: 32px; height: 32px;
display: flex; align-items: center; justify-content: center;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
}
#fullscreen-btn:hover {
background: rgba(8,8,15,0.8);
border-color: rgba(180,160,130,0.25);
}
#fullscreen-btn svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.5; }
#export-btn {
position: fixed;
top: 16px; right: 56px;
z-index: 5;
background: rgba(8,8,15,0.6);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(180,160,130,0.12);
border-radius: 8px;
color: rgba(180,160,130,0.45);
width: 32px; height: 32px;
display: flex; align-items: center; justify-content: center;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
}
#export-btn:hover {
background: rgba(8,8,15,0.8);
border-color: rgba(180,160,130,0.25);
}
#export-btn svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.5; }
</style>
</head>
<body>
<div id="vignette"></div>
<div id="info">drag to orbit &bull; scroll to zoom &bull; auto-rotate &bull; <b>F</b> flythrough</div>
<button id="export-btn" title="Export snapshot">
<svg viewBox="0 0 24 24">
<path d="M12 3v12m0 0l-4-4m4 4l4-4M4 17v2a2 2 0 002 2h12a2 2 0 002-2v-2"/>
</svg>
</button>
<button id="fullscreen-btn" title="Toggle fullscreen">
<svg viewBox="0 0 24 24">
<path d="M3 3h5v2H5v3H3V3zm13 0h5v5h-2V5h-3V3zM3 16h2v3h3v2H3v-5zm13 5v-2h3v-3h2v5h-5z"/>
</svg>
</button>
<div id="controls">
<div class="ctrl-group">
<div class="ctrl-label"><span>Amplitude</span><span class="val" id="val-waveHeight">1.0</span></div>
<input type="range" id="waveHeight" min="0" max="2.0" step="0.05" value="1.0">
</div>
<div class="ctrl-group">
<div class="ctrl-label"><span>Speed</span><span class="val" id="val-waveSpeed">1.0</span></div>
<input type="range" id="waveSpeed" min="0" max="2.0" step="0.05" value="1.0">
</div>
<div class="ctrl-group">
<div class="ctrl-label"><span>Ico Particles</span><span class="val" id="val-icoCount">500</span></div>
<input type="range" id="icoCount" min="0" max="3000" step="50" value="500">
</div>
<div class="ctrl-group">
<div class="ctrl-label"><span>Knot Particles</span><span class="val" id="val-knotCount">250</span></div>
<input type="range" id="knotCount" min="0" max="2000" step="50" value="250">
</div>
<div class="ctrl-group">
<div class="ctrl-label"><span>Particle Size</span><span class="val" id="val-particleSize">0.13</span></div>
<input type="range" id="particleSize" min="0.02" max="0.5" step="0.01" value="0.13">
</div>
<div class="ctrl-group">
<div class="ctrl-label"><span>Morph</span><span class="val" id="val-morphTarget">0.00</span></div>
<input type="range" id="morphTarget" min="0" max="2" step="0.01" value="0">
</div>
<div class="ctrl-group">
<div class="ctrl-label"><span>Flow Speed</span><span class="val" id="val-flowSpeed">1.0</span></div>
<input type="range" id="flowSpeed" min="0" max="3.0" step="0.05" value="1.0">
</div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.162.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.162.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// ─── Scene ────────────────────────────────────────────────────────────
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x08080f);
scene.fog = new THREE.Fog(0x08080f, 30, 60);
const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 120);
camera.position.set(14, 8, 18);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
document.body.prepend(renderer.domElement);
// ─── Controls ─────────────────────────────────────────────────────────
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.target.set(0, 0, 0);
controls.minDistance = 6;
controls.maxDistance = 45;
controls.maxPolarAngle = Math.PI * 0.85;
// ─── Lighting ─────────────────────────────────────────────────────────
const ambient = new THREE.AmbientLight(0x1a1a2e, 0.6);
scene.add(ambient);
const hemi = new THREE.HemisphereLight(0xc8b890, 0x08080f, 0.4);
scene.add(hemi);
const key = new THREE.DirectionalLight(0xe0d0b8, 1.8);
key.position.set(10, 18, 8);
scene.add(key);
const fill = new THREE.DirectionalLight(0x8898a8, 0.6);
fill.position.set(-12, 4, -10);
scene.add(fill);
// ─── Wireframe Structure 1: Icosahedron ─────────────────────────────
const ICOSA_RADIUS = 6.5;
const ICOSA_DETAIL = 3;
const icoGeo = new THREE.IcosahedronGeometry(ICOSA_RADIUS, ICOSA_DETAIL);
const icoPosOrig = new Float32Array(icoGeo.attributes.position.array);
const icoMat1 = new THREE.MeshBasicMaterial({
color: 0xc8b890,
wireframe: true,
transparent: true,
opacity: 0.3,
});
const icoMesh1 = new THREE.Mesh(icoGeo, icoMat1);
scene.add(icoMesh1);
const icoMat2 = new THREE.MeshBasicMaterial({
color: 0xe8e0d0,
wireframe: true,
transparent: true,
opacity: 0.06,
});
const icoMesh2 = new THREE.Mesh(icoGeo, icoMat2);
scene.add(icoMesh2);
// ─── Wireframe Structure 2: Torus Knot ──────────────────────────────
const KNOT_RADIUS = 3.2;
const KNOT_TUBE = 0.9;
const KNOT_P = 2;
const KNOT_Q = 3;
const knotGeo = new THREE.TorusKnotGeometry(KNOT_RADIUS, KNOT_TUBE, 80, 12, KNOT_P, KNOT_Q);
const knotPosOrig = new Float32Array(knotGeo.attributes.position.array);
const knotMat1 = new THREE.MeshBasicMaterial({
color: 0x7a9aaa,
wireframe: true,
transparent: true,
opacity: 0.25,
});
const knotMesh1 = new THREE.Mesh(knotGeo, knotMat1);
scene.add(knotMesh1);
const knotMat2 = new THREE.MeshBasicMaterial({
color: 0xaabbc8,
wireframe: true,
transparent: true,
opacity: 0.06,
});
const knotMesh2 = new THREE.Mesh(knotGeo, knotMat2);
scene.add(knotMesh2);
// ─── Geometry Morphing ──────────────────────────────────────────────
const icoVertCount = icoGeo.attributes.position.count;
const icoPosMorphed = new Float32Array(icoPosOrig);
const icoPosSphere = new Float32Array(icoPosOrig.length);
const icoPosOcta = new Float32Array(icoPosOrig.length);
function computeSpherePositions(orig, out, count, radius) {
for (let i = 0; i < count; i++) {
const i3 = i * 3;
const x = orig[i3], y = orig[i3 + 1], z = orig[i3 + 2];
const len = Math.sqrt(x * x + y * y + z * z) || 1;
out[i3] = (x / len) * radius;
out[i3 + 1] = (y / len) * radius;
out[i3 + 2] = (z / len) * radius;
}
}
function computeOctaPositions(orig, out, count, radius) {
for (let i = 0; i < count; i++) {
const i3 = i * 3;
const x = orig[i3], y = orig[i3 + 1], z = orig[i3 + 2];
const ax = Math.abs(x), ay = Math.abs(y), az = Math.abs(z);
const max = Math.max(ax, ay, az) || 1;
const sx = x >= 0 ? 1 : -1, sy = y >= 0 ? 1 : -1, sz = z >= 0 ? 1 : -1;
const blend = 0.65;
out[i3] = x * (1 - blend) + (sx * radius * (ax / max)) * blend;
out[i3 + 1] = y * (1 - blend) + (sy * radius * (ay / max)) * blend;
out[i3 + 2] = z * (1 - blend) + (sz * radius * (az / max)) * blend;
}
}
computeSpherePositions(icoPosOrig, icoPosSphere, icoVertCount, ICOSA_RADIUS);
computeOctaPositions(icoPosOrig, icoPosOcta, icoVertCount, ICOSA_RADIUS);
// ─── Flythrough ─────────────────────────────────────────────────────
const flythrough = { active: false, t: 0 };
// ─── Edge-Flow Particles ─────────────────────────────────────────────
function getEdgeList(geo) {
const idx = geo.index;
const pos = geo.attributes.position;
const set = new Set();
const k = (x, y) => `${Math.min(x,y)},${Math.max(x,y)}`;
if (idx) {
for (let i = 0; i < idx.count; i += 3) {
const a = idx.getX(i), b = idx.getX(i + 1), c = idx.getX(i + 2);
set.add(k(a, b)); set.add(k(b, c)); set.add(k(c, a));
}
} else {
for (let i = 0; i < pos.count; i += 3) {
set.add(k(i, i + 1)); set.add(k(i + 1, i + 2)); set.add(k(i + 2, i));
}
}
return Array.from(set).map(s => s.split(',').map(Number));
}
const icoEdges = getEdgeList(icoGeo);
const knotEdges = getEdgeList(knotGeo);
const ICO_MAX = 3000;
const KNOT_MAX = 2000;
const icoPPos = new Float32Array(ICO_MAX * 3);
const icoPCol = new Float32Array(ICO_MAX * 3);
const icoPartState = [];
for (let i = 0; i < ICO_MAX; i++) {
icoPartState.push({ edge: Math.floor(Math.random() * icoEdges.length), t: Math.random(), speed: 0.15 + Math.random() * 0.4, dir: Math.random() < 0.5 ? 1 : -1 });
}
const knotPPos = new Float32Array(KNOT_MAX * 3);
const knotPCol = new Float32Array(KNOT_MAX * 3);
const knotPartState = [];
for (let i = 0; i < KNOT_MAX; i++) {
knotPartState.push({ edge: Math.floor(Math.random() * knotEdges.length), t: Math.random(), speed: 0.15 + Math.random() * 0.4, dir: Math.random() < 0.5 ? 1 : -1 });
}
const icoPGeo = new THREE.BufferGeometry();
icoPGeo.setAttribute('position', new THREE.BufferAttribute(icoPPos, 3));
icoPGeo.setAttribute('color', new THREE.BufferAttribute(icoPCol, 3));
icoPGeo.setDrawRange(0, 0);
const knotPGeo = new THREE.BufferGeometry();
knotPGeo.setAttribute('position', new THREE.BufferAttribute(knotPPos, 3));
knotPGeo.setAttribute('color', new THREE.BufferAttribute(knotPCol, 3));
knotPGeo.setDrawRange(0, 0);
function makeGlowTex() {
const c = document.createElement('canvas');
c.width = 32; c.height = 32;
const ctx = c.getContext('2d');
const g = ctx.createRadialGradient(16, 16, 0, 16, 16, 16);
g.addColorStop(0, 'rgba(255,255,240,1)');
g.addColorStop(0.3, 'rgba(220,210,200,0.5)');
g.addColorStop(1, 'rgba(255,255,255,0)');
ctx.fillStyle = g; ctx.fillRect(0, 0, 32, 32);
return new THREE.CanvasTexture(c);
}
const glowTex = makeGlowTex();
const ptMat = new THREE.PointsMaterial({
size: 0.13, map: glowTex, blending: THREE.AdditiveBlending,
depthWrite: false, transparent: true, opacity: 0.85,
sizeAttenuation: true, vertexColors: true,
});
icoMesh1.add(new THREE.Points(icoPGeo, ptMat.clone()));
knotMesh1.add(new THREE.Points(knotPGeo, ptMat.clone()));
const icoCol = [0.78, 0.72, 0.56];
const knotCol = [0.48, 0.60, 0.67];
// ─── Starfield ────────────────────────────────────────────────────────
const starCount = 2500;
const starGeo = new THREE.BufferGeometry();
const starPos = new Float32Array(starCount * 3);
for (let i = 0; i < starCount * 3; i++) {
starPos[i] = (Math.random() - 0.5) * 280;
}
starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
const starMat = new THREE.PointsMaterial({
color: 0x8888aa,
size: 0.08,
transparent: true,
opacity: 0.35,
sizeAttenuation: true,
});
scene.add(new THREE.Points(starGeo, starMat));
// ─── Icosahedron Pulse ────────────────────────────────────────────────
function icoPulse(pos, orig, count, time, amp, speed) {
const a = 0.4 * amp;
const f = 1.2;
for (let i = 0; i < count; i++) {
const i3 = i * 3;
const ox = orig[i3], oy = orig[i3 + 1], oz = orig[i3 + 2];
const wave = Math.sin(ox * 0.35 + oy * 0.25 + oz * 0.30 + time * 0.6 * speed);
const scale = 1 + a * wave;
pos[i3] = ox * scale;
pos[i3 + 1] = oy * scale;
pos[i3 + 2] = oz * scale;
}
}
// ─── Torus Knot Morph ────────────────────────────────────────────────
function knotMorph(pos, orig, count, time, amp, speed) {
const a = 0.15 * amp;
const f = 0.8;
for (let i = 0; i < count; i++) {
const i3 = i * 3;
const ox = orig[i3], oy = orig[i3 + 1], oz = orig[i3 + 2];
const wave = Math.sin(ox * 0.5 + oy * 0.4 + oz * 0.3 + time * 0.5 * speed);
const scale = 1 + a * wave;
pos[i3] = ox * scale;
pos[i3 + 1] = oy * scale;
pos[i3 + 2] = oz * scale;
}
}
// ─── Update Edge Particles ──────────────────────────────────────────
function updateEdgeParticles(srcPos, edges, state, posArr, colArr, colBase, dt, mul, outGeo, count) {
const edgeCount = edges.length;
if (edgeCount === 0 || count === 0) return;
for (let i = 0; i < count; i++) {
const s = state[i];
s.t += s.speed * dt * mul * s.dir;
if (s.dir === 1 && s.t >= 1) {
s.t = 0; s.edge = Math.floor(Math.random() * edgeCount);
if (Math.random() < 0.5) s.dir = -1;
} else if (s.dir === -1 && s.t <= 0) {
s.t = 1; s.edge = Math.floor(Math.random() * edgeCount);
if (Math.random() < 0.5) s.dir = 1;
}
const edge = edges[s.edge];
if (!edge) continue;
const [a, b] = edge;
const i3 = i * 3, ai = a * 3, bi = b * 3;
const t = s.t;
posArr[i3] = srcPos[ai] + (srcPos[bi] - srcPos[ai]) * t;
posArr[i3 + 1] = srcPos[ai + 1] + (srcPos[bi + 1] - srcPos[ai + 1]) * t;
posArr[i3 + 2] = srcPos[ai + 2] + (srcPos[bi + 2] - srcPos[ai + 2]) * t;
const bright = s.dir === 1 ? (0.25 + t * 0.75) : (0.25 + (1 - t) * 0.75);
colArr[i3] = colBase[0] * bright;
colArr[i3 + 1] = colBase[1] * bright;
colArr[i3 + 2] = colBase[2] * bright;
}
outGeo.attributes.position.needsUpdate = true;
outGeo.attributes.color.needsUpdate = true;
}
// ─── Resize ───────────────────────────────────────────────────────────
window.addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
// ─── Keydown ──────────────────────────────────────────────────────────
window.addEventListener('keydown', (e) => {
if (e.key === 'f' || e.key === 'F') {
flythrough.active = !flythrough.active;
controls.enabled = !flythrough.active;
if (!flythrough.active) {
controls.enabled = true;
}
}
});
function exportState() {
const p = camera.position;
const vals = params;
let html = document.documentElement.outerHTML;
const ctrlStart = html.indexOf('<div id="controls">');
let depth = 0, end = ctrlStart;
for (let i = ctrlStart; i < html.length; i++) {
if (html.slice(i, i+4) === '<div') depth++;
if (html.slice(i, i+5) === '</div') { depth--; if (depth === 0) { end = i + 6; break; } }
}
html = html.slice(0, ctrlStart) + html.slice(end);
html = html.replace(
'camera.position.set(14, 8, 18)',
`camera.position.set(${p.x.toFixed(2)}, ${p.y.toFixed(2)}, ${p.z.toFixed(2)})`
);
for (const [key, val] of Object.entries(vals)) {
const re = new RegExp(`(${key}:\\s*)[\\d.]+`);
html = html.replace(re, `$1${val}`);
}
html = html.replace(/^\s*setupSlider\(.*\n/gm, '');
const blob = new Blob([html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'wireframe-scene.html';
a.click();
URL.revokeObjectURL(url);
}
document.getElementById('export-btn').addEventListener('click', exportState);
const fsBtn = document.getElementById('fullscreen-btn');
fsBtn.addEventListener('click', () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => {});
} else {
document.exitFullscreen().catch(() => {});
}
});
// ─── Params & Sliders ─────────────────────────────────────────────────
const params = {
waveHeight: 1.0,
waveSpeed: 1.0,
icoCount: 500,
knotCount: 250,
particleSize: 0.13,
flowSpeed: 1.0,
morphTarget: 0.0,
};
function setupSlider(id, key, fmt) {
const input = document.getElementById(id);
const display = document.getElementById('val-' + id);
if (!input || !display) return;
const update = () => {
params[key] = parseFloat(input.value);
display.textContent = fmt ? fmt(params[key]) : params[key].toFixed(2);
};
input.addEventListener('input', update);
update();
}
setupSlider('waveHeight', 'waveHeight');
setupSlider('waveSpeed', 'waveSpeed');
setupSlider('icoCount', 'icoCount', v => Math.round(v));
setupSlider('knotCount', 'knotCount', v => Math.round(v));
setupSlider('particleSize', 'particleSize', v => v.toFixed(2));
setupSlider('flowSpeed', 'flowSpeed');
setupSlider('morphTarget', 'morphTarget', v => v.toFixed(2));
// ─── Animation Loop ───────────────────────────────────────────────────
let lastTime = performance.now();
let time = 0;
function animate() {
requestAnimationFrame(animate);
const now = performance.now();
const dt = Math.min((now - lastTime) / 1000, 0.05);
lastTime = now;
time += dt;
// Geometry morphing (Icosahedron: Icosa <-> Sphere <-> Octa)
const morphT = params.morphTarget;
for (let i = 0; i < icoPosMorphed.length; i++) {
if (morphT <= 1) {
icoPosMorphed[i] = icoPosOrig[i] + (icoPosSphere[i] - icoPosOrig[i]) * morphT;
} else {
icoPosMorphed[i] = icoPosSphere[i] + (icoPosOcta[i] - icoPosSphere[i]) * (morphT - 1);
}
}
// Pulse wireframe vertices
const icoPos = icoGeo.attributes.position.array;
const knotPos = knotGeo.attributes.position.array;
icoPulse(icoPos, icoPosMorphed, icoGeo.attributes.position.count, time, params.waveHeight, params.waveSpeed);
knotMorph(knotPos, knotPosOrig, knotGeo.attributes.position.count, time, params.waveHeight, params.waveSpeed);
icoGeo.attributes.position.needsUpdate = true;
knotGeo.attributes.position.needsUpdate = true;
// Update edge-flow particles
const icoN = Math.round(params.icoCount);
const knotN = Math.round(params.knotCount);
icoPGeo.setDrawRange(0, icoN);
knotPGeo.setDrawRange(0, knotN);
updateEdgeParticles(icoGeo.attributes.position.array, icoEdges, icoPartState, icoPPos, icoPCol, icoCol, dt, 0.5 * params.flowSpeed, icoPGeo, icoN);
updateEdgeParticles(knotGeo.attributes.position.array, knotEdges, knotPartState, knotPPos, knotPCol, knotCol, dt, 0.4 * params.flowSpeed, knotPGeo, knotN);
// Update particle size
icoMesh1.children[0].material.size = params.particleSize;
knotMesh1.children[0].material.size = params.particleSize;
// Rotate structures
icoMesh1.rotation.y += dt * 0.06;
icoMesh1.rotation.x += dt * 0.02;
icoMesh2.rotation.copy(icoMesh1.rotation);
knotMesh1.rotation.y -= dt * 0.10;
knotMesh1.rotation.x += dt * 0.03;
knotMesh2.rotation.copy(knotMesh1.rotation);
// Flythrough
if (flythrough.active) {
flythrough.t += dt * 0.04;
const ft = flythrough.t;
const sinT = Math.sin(ft);
const cosT = Math.cos(ft);
const denom = 1 + sinT * sinT;
const fx = 16 * cosT / denom;
const fy = 4 * Math.sin(2 * ft) / denom;
const fz = 12 * sinT / denom;
camera.position.set(fx, fy, fz);
controls.target.set(0, 0, 0);
controls.update();
} else {
controls.target.set(0, 0, 0);
controls.update();
}
// Light orbit
const angle = time * 0.03;
key.position.x = 16 * Math.cos(angle);
key.position.z = 16 * Math.sin(angle);
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment