page transition, optimize lighting

This commit is contained in:
Vincent van der Wal
2026-07-19 10:55:51 +02:00
parent f724cbff6d
commit 99c2851285
21 changed files with 388 additions and 302 deletions
+23 -10
View File
@@ -6,7 +6,7 @@ Most sacred-geometry figures are drawn flat. This project asks what they look li
when they are built in real 3D space: the Fruit of Life as a rotated cubic sphere when they are built in real 3D space: the Fruit of Life as a rotated cubic sphere
packing, the Platonic solids nested inside it with their vertices on the circle packing, the Platonic solids nested inside it with their vertices on the circle
centres, the torus as a field of winding strands, and so on. Every page is an centres, the torus as a field of winding strands, and so on. Every page is an
interactive scene with orbit controls and a lil-gui panel for playing with the interactive scene with orbit controls and a control panel for playing with the
parameters. parameters.
## Pages ## Pages
@@ -17,25 +17,38 @@ parameters.
| Metatron's Cube | `src/metatron.js` | The 3D Fruit of Life circle grid with the five Platonic solids (fire, earth, air, water, aether) aligned to it, plus the Merkaba and the full Metatron web | | Metatron's Cube | `src/metatron.js` | The 3D Fruit of Life circle grid with the five Platonic solids (fire, earth, air, water, aether) aligned to it, plus the Merkaba and the full Metatron web |
| Toroidal Field | `src/torus.js` | A torus with field-line strands winding around the surface and particles flowing along them | | Toroidal Field | `src/torus.js` | A torus with field-line strands winding around the surface and particles flowing along them |
| Vector Equilibrium | `src/matrix.js` | The cuboctahedron: radial spokes equal to the edges, and the 12-around-1 kissing-sphere packing | | Vector Equilibrium | `src/matrix.js` | The cuboctahedron: radial spokes equal to the edges, and the 12-around-1 kissing-sphere packing |
| Golden Spiral | `src/phyllotaxis.js` | Phyllotaxis at the golden angle: sunflower disc, pinecone and Fibonacci sphere; nudge the angle to watch the pattern collapse | | Golden Spiral | `src/phyllotaxis.js` | Phyllotaxis at the golden angle: sunflower disc, pinecone and Fibonacci sphere, with a looping growth animation; nudge the angle to watch the pattern collapse |
| Flower of Life | `src/flower.js` | A cubic grid of interlocking spheres | | Flower of Life | `src/flower.js` | A cubic grid of interlocking spheres |
| Seed of Life | `src/seed.js` | The seven seed circles rotated through space | | Seed of Life | `src/seed.js` | The seven seed circles rotated through space |
## Design
The landing page palette (deep warm gradient, orange `#f44f04`, cream `#f5e9de`)
is used 1:1 in the scenes: the canvases are transparent, so the CSS gradient is
the scenery background. The shadow wall is invisible; each shape projects a
light orange outline onto it. Dark/light theme toggle in the nav, persisted in
localStorage.
Page navigations fly the shapes out past the camera, crossfade documents via
the View Transitions API (Chromium; graceful cut elsewhere), and fly the next
page's shapes in from the distance.
## Development ## Development
Built with [Vite](https://vite.dev/) as a multi-page app.
```sh ```sh
npm install npm install
npm start # dev server with hot reload npm start # dev server
npm run build # production build into build/ npm run build # production build into build/
npm run preview # serve the production build
``` ```
Each page is its own webpack entry; the page list lives at the top of Each exploration is a `src/<name>.html` + `src/<name>.js` pair; register new
`webpack.config.js`. To add a new exploration, add `src/<name>.js` (the shared pages in the `pages` list in `vite.config.js`. Shared code lives in `src/lib/`:
scene setup is in `src/lib/stage.js`) and one line to the `pages` array. `stage.js` (scene/camera/lights/wall/controls), `theme.js` (palette),
`transit.js` (fly in/out), `chrome.js` (nav, theme toggle, transitions),
Every page shares the chrome in `src/lib/chrome.js`: fade transitions between `edgeTubes.js` (line-thickness helpers).
pages and a dark/light toggle (bottom right, persisted in localStorage). Dark
mode matches the landing page; light mode is the original orange room.
## Debug flags (Metatron page) ## Debug flags (Metatron page)
+111 -44
View File
@@ -3,22 +3,20 @@
// Every page shares --page-bg: the scene canvases are transparent, so the // Every page shares --page-bg: the scene canvases are transparent, so the
// landing gradient is the scenery background, 1:1. // landing gradient is the scenery background, 1:1.
:root { :root {
--page-bg: radial-gradient(ellipse at 50% 30%, #2a1204 0%, #120701 60%, #000 100%); --page-bg: radial-gradient(ellipse at 50% 30%, #52220a 0%, #140801 60%, #000 100%);
--fade-bg: #120701;
--ink: #f5e9de; --ink: #f5e9de;
--ink-soft: rgba(245, 233, 222, 0.7); --ink-soft: rgba(245, 233, 222, 0.7);
--ink-faint: rgba(245, 233, 222, 0.55); --ink-faint: rgba(245, 233, 222, 0.55);
--panel-bg: rgba(18, 7, 1, 0.75); --panel-bg: rgba(18, 7, 1, 0.6);
--panel-border: rgba(244, 79, 4, 0.35); --panel-border: rgba(244, 79, 4, 0.35);
} }
html[data-theme='light'] { html[data-theme='light'] {
--page-bg: radial-gradient(ellipse at 50% 30%, #fff8f0 0%, #ffe7d3 60%, #ffd4b3 100%); --page-bg: radial-gradient(ellipse at 50% 30%, #fff8f0 0%, #fdca9e 55%, #ef9d55 100%);
--fade-bg: #ffe7d3;
--ink: #1b0a02; --ink: #1b0a02;
--ink-soft: rgba(27, 10, 2, 0.7); --ink-soft: rgba(27, 10, 2, 0.7);
--ink-faint: rgba(27, 10, 2, 0.5); --ink-faint: rgba(27, 10, 2, 0.5);
--panel-bg: rgba(255, 248, 240, 0.85); --panel-bg: rgba(255, 248, 240, 0.6);
--panel-border: rgba(244, 79, 4, 0.4); --panel-border: rgba(244, 79, 4, 0.4);
} }
@@ -34,39 +32,66 @@ body {
color: var(--ink); color: var(--ink);
} }
// the scenes cast their projection onto the wall, so the glow of the gradient
// follows it: lib/glow.js projects the wall centre into screen space every
// frame and drives --glow-x/--glow-y; the landing keeps it centred
html:not([data-theme='light']) body:not(.landing) {
background: radial-gradient(ellipse 75% 90% at var(--glow-x, 12%) var(--glow-y, 50%), #5c2609 0%, #140801 65%, #000 100%);
}
html[data-theme='light'] body:not(.landing) {
background: radial-gradient(ellipse 75% 90% at var(--glow-x, 12%) var(--glow-y, 50%), #fff8f0 0%, #fdca9e 60%, #ef9d55 100%);
}
canvas { canvas {
display: block; display: block;
} }
// page transition: opaque at first paint, faded out by chrome.js once the page // page transitions: while the shapes fly out, the page content fades
// is ready, faded back in before navigating away. High z-index so it also // (html.leaving, set by chrome.js), the browser crossfades the documents
// covers stats (z-index 10000) and lil-gui. // (Chromium), and the next page's content fades back in. The gradient
.fade { // background never fades, so pages blend through it instead of through black.
position: fixed; @view-transition {
inset: 0; navigation: auto;
z-index: 10001; }
background: var(--fade-bg);
opacity: 1;
pointer-events: none;
transition: opacity 0.35s ease;
&.done { ::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.45s;
}
canvas,
.page-nav,
.lil-gui,
.theme-toggle,
.landing-content {
animation: page-enter 0.6s ease;
transition: opacity 0.45s ease;
}
html.leaving :is(canvas, .page-nav, .lil-gui, .theme-toggle, .landing-content) {
opacity: 0; opacity: 0;
} }
@keyframes page-enter {
from {
opacity: 0;
}
to {
opacity: 1;
}
} }
.theme-toggle { .theme-toggle {
position: fixed; width: 48px;
bottom: 16px; height: 48px;
right: 16px;
z-index: 20;
width: 40px;
height: 40px;
border: 1px solid var(--panel-border); border: 1px solid var(--panel-border);
border-radius: 50%; border-radius: 50%;
background: var(--panel-bg); background: var(--panel-bg);
backdrop-filter: blur(10px);
color: var(--ink); color: var(--ink);
font-size: 18px; font-size: 22px;
line-height: 1; line-height: 1;
cursor: pointer; cursor: pointer;
@@ -75,26 +100,34 @@ canvas {
} }
} }
// back-link on the exploration pages; bottom-left because stats sits top-left // floating variant (landing page): top right
// and lil-gui top-right body > .theme-toggle {
position: fixed;
top: 16px;
right: 16px;
z-index: 20;
}
// top bar on the exploration pages: back link, page title, theme toggle
.page-nav { .page-nav {
position: fixed; position: fixed;
bottom: 16px; top: 16px;
left: 16px; left: 16px;
z-index: 10; z-index: 10;
display: flex; display: flex;
gap: 12px; gap: 16px;
align-items: baseline; align-items: center;
font-family: Georgia, 'Times New Roman', serif; font-family: Georgia, 'Times New Roman', serif;
a { a {
color: var(--ink); color: var(--ink);
background: var(--panel-bg); background: var(--panel-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--panel-border); border: 1px solid var(--panel-border);
text-decoration: none; text-decoration: none;
padding: 6px 12px; padding: 14px 26px;
border-radius: 999px; border-radius: 999px;
font-size: 14px; font-size: 18px;
&:hover { &:hover {
border-color: #f44f04; border-color: #f44f04;
@@ -103,7 +136,7 @@ canvas {
span { span {
color: var(--ink-soft); color: var(--ink-soft);
font-size: 14px; font-size: 17px;
letter-spacing: 0.04em; letter-spacing: 0.04em;
} }
} }
@@ -121,16 +154,22 @@ canvas {
--number-color: #ff9a5c; --number-color: #ff9a5c;
--string-color: #ff9a5c; --string-color: #ff9a5c;
--font-family: Georgia, 'Times New Roman', serif; --font-family: Georgia, 'Times New Roman', serif;
--font-size: 13px; --font-size: 17px;
--input-font-size: 13px; --input-font-size: 17px;
--widget-border-radius: 6px; --title-height: 44px;
--widget-height: 34px;
--spacing: 10px;
--padding: 14px;
--folder-indent: 14px;
--widget-border-radius: 8px;
&.root { &.root {
margin: 12px; margin: 16px;
width: 370px;
border: 1px solid var(--panel-border); border: 1px solid var(--panel-border);
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
backdrop-filter: blur(6px); backdrop-filter: blur(10px);
} }
&.root > .title { &.root > .title {
@@ -143,16 +182,37 @@ html[data-theme='light'] .lil-gui {
--string-color: #c2410c; --string-color: #c2410c;
} }
@media (max-width: 600px) {
.lil-gui.root {
width: calc(100vw - 24px);
max-height: 45vh;
overflow-y: auto;
}
.page-nav {
gap: 8px;
a,
span {
font-size: 15px;
}
a {
padding: 10px 16px;
}
}
}
// ---------- landing ---------- // // ---------- landing ---------- //
body.landing { body.landing {
--card-bg: rgba(18, 7, 1, 0.65); --card-bg: rgba(18, 7, 1, 0.5);
--card-bg-hover: rgba(42, 18, 4, 0.85); --card-bg-hover: rgba(42, 18, 4, 0.75);
} }
html[data-theme='light'] body.landing { html[data-theme='light'] body.landing {
--card-bg: rgba(255, 255, 255, 0.6); --card-bg: rgba(255, 255, 255, 0.45);
--card-bg-hover: rgba(255, 255, 255, 0.95); --card-bg-hover: rgba(255, 255, 255, 0.8);
} }
.landing-bg { .landing-bg {
@@ -202,6 +262,7 @@ html[data-theme='light'] body.landing {
border: 1px solid var(--panel-border); border: 1px solid var(--panel-border);
border-radius: 12px; border-radius: 12px;
background: var(--card-bg); background: var(--card-bg);
backdrop-filter: blur(10px);
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease; transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease;
@@ -228,7 +289,13 @@ html[data-theme='light'] body.landing {
} }
.soon { .soon {
margin-top: 72px; width: fit-content;
margin: 72px auto 0;
padding: 28px 40px;
border: 1px solid var(--panel-border);
border-radius: 12px;
background: var(--card-bg);
backdrop-filter: blur(10px);
text-align: center; text-align: center;
h3 { h3 {
@@ -247,7 +314,7 @@ html[data-theme='light'] body.landing {
li { li {
font-size: 14px; font-size: 14px;
color: var(--ink-faint); color: var(--ink-soft);
margin: 6px 0; margin: 6px 0;
} }
} }
-1
View File
@@ -7,7 +7,6 @@
<link rel="stylesheet" href="./css/styles.scss"> <link rel="stylesheet" href="./css/styles.scss">
</head> </head>
<body> <body>
<div class="fade"></div>
<nav class="page-nav"> <nav class="page-nav">
<a href="index.html">&larr; Thrive</a> <a href="index.html">&larr; Thrive</a>
<span>Flower of Life</span> <span>Flower of Life</span>
+6 -1
View File
@@ -8,6 +8,8 @@ import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
import './lib/chrome.js'; import './lib/chrome.js';
import { sceneColors } from './lib/theme.js'; import { sceneColors } from './lib/theme.js';
import { bindWorld } from './lib/transit.js'; import { bindWorld } from './lib/transit.js';
import { bindGlow } from './lib/glow.js';
import { bindCameraMemory } from './lib/cameraMemory.js';
const colors = sceneColors(); const colors = sceneColors();
@@ -16,7 +18,7 @@ const colors = sceneColors();
const scene = new THREE.Scene(); const scene = new THREE.Scene();
const world = new THREE.Group(); const world = new THREE.Group();
scene.add( world ); scene.add( world );
const updateWorld = bindWorld( world ); const updateWorld = bindWorld( world, 1.5 );
const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 500 ); const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 500 );
@@ -42,6 +44,8 @@ window.addEventListener( 'resize', () => {
const controls = new OrbitControls( camera, renderer.domElement ); const controls = new OrbitControls( camera, renderer.domElement );
controls.target.set( -4, 0, 0 ); controls.target.set( -4, 0, 0 );
bindCameraMemory( camera, controls );
const updateGlow = bindGlow( camera );
// const geometry_torus = new THREE.TorusGeometry( 2.5, 2.5, 32, 128 ); // const geometry_torus = new THREE.TorusGeometry( 2.5, 2.5, 32, 128 );
@@ -387,6 +391,7 @@ function animate () {
// } // }
updateWorld(); updateWorld();
updateGlow();
controls.update(); controls.update();
renderer.render( scene, camera ); renderer.render( scene, camera );
-1
View File
@@ -7,7 +7,6 @@
<link rel="stylesheet" href="./css/styles.scss"> <link rel="stylesheet" href="./css/styles.scss">
</head> </head>
<body class="landing"> <body class="landing">
<div class="fade"></div>
<main class="landing-content"> <main class="landing-content">
<header> <header>
<h1>Thrive</h1> <h1>Thrive</h1>
+32
View File
@@ -0,0 +1,32 @@
// Preserves the orbit camera across the reload that a theme switch triggers.
// chrome.js fires thrive:store-camera right before reloading; the saved pose is
// restored (and cleared) on the next load of the same page, so regular
// navigation still gets the default framing.
const KEY = `thrive-camera:${location.pathname}`;
export function bindCameraMemory( camera, controls ) {
window.addEventListener( 'thrive:store-camera', () => {
try {
sessionStorage.setItem( KEY, JSON.stringify( {
position: camera.position.toArray(),
target: controls.target.toArray(),
} ) );
} catch {
// storage unavailable: the camera just resets
}
} );
let saved = null;
try {
saved = JSON.parse( sessionStorage.getItem( KEY ) );
sessionStorage.removeItem( KEY );
} catch {
saved = null;
}
if ( saved ) {
camera.position.fromArray( saved.position );
controls.target.fromArray( saved.target );
controls.update();
}
}
+20 -25
View File
@@ -1,38 +1,30 @@
import { currentTheme, toggleTheme } from './theme.js'; import { currentTheme, toggleTheme } from './theme.js';
import { EXIT_MS } from './transit.js'; import { EXIT_MS } from './transit.js';
// Shared page chrome: theme attribute for the CSS, fade transitions between // Shared page chrome: theme attribute for the CSS, page transitions, and the
// pages, and the dark/light toggle. Imported for its side effects by every page. // dark/light toggle. Imported for its side effects by every page.
//
// Navigation choreography: the shapes fly out first (thrive:exit), then the
// browser's cross-document view transition (see @view-transition in the CSS)
// crossfades the last frame into the next page, whose shapes fly in. No
// blocking overlay - pages blend into each other.
document.documentElement.dataset.theme = currentTheme(); document.documentElement.dataset.theme = currentTheme();
const FADE_MS = 350;
window.addEventListener( 'DOMContentLoaded', () => { window.addEventListener( 'DOMContentLoaded', () => {
// the overlay lives in the HTML templates so it covers the very first paint; // shapes fly out and the page content fades (html.leaving) at the same
// pages without one (shouldn't happen) still get a working fallback // time; navigation happens mid-flight so the document crossfade blends
let fade = document.querySelector( '.fade' ); // moving frames instead of an empty pause
if ( ! fade ) { const leaveTo = ( action ) => {
fade = document.createElement( 'div' );
fade.className = 'fade';
document.body.appendChild( fade );
}
// double rAF so the browser commits the opaque state before transitioning
requestAnimationFrame( () => requestAnimationFrame( () => fade.classList.add( 'done' ) ) );
// fly the shapes out first, fade through the background for the tail of the
// flight, then navigate
const fadeTo = ( action ) => {
window.dispatchEvent( new Event( 'thrive:exit' ) ); window.dispatchEvent( new Event( 'thrive:exit' ) );
setTimeout( () => fade.classList.remove( 'done' ), EXIT_MS - FADE_MS + 100 ); document.documentElement.classList.add( 'leaving' );
setTimeout( action, EXIT_MS + 100 ); setTimeout( action, EXIT_MS * 0.75 );
}; };
document.addEventListener( 'click', ( event ) => { document.addEventListener( 'click', ( event ) => {
const link = event.target.closest( 'a[href]' ); const link = event.target.closest( 'a[href]' );
if ( ! link || link.origin !== location.origin ) return; if ( ! link || link.origin !== location.origin ) return;
event.preventDefault(); event.preventDefault();
fadeTo( () => { location.href = link.href; } ); leaveTo( () => { location.href = link.href; } );
} ); } );
const button = document.createElement( 'button' ); const button = document.createElement( 'button' );
@@ -41,11 +33,14 @@ window.addEventListener( 'DOMContentLoaded', () => {
button.title = 'toggle dark / light'; button.title = 'toggle dark / light';
button.addEventListener( 'click', () => { button.addEventListener( 'click', () => {
// the scenes build their materials once at startup, so switching theme // the scenes build their materials once at startup, so switching theme
// reloads the page - the fade makes that read as a transition // reloads the page behind the same fly-out + crossfade; the camera pose
fadeTo( () => { // is stashed so the reload comes back to the same view
window.dispatchEvent( new Event( 'thrive:store-camera' ) );
leaveTo( () => {
toggleTheme(); toggleTheme();
location.reload(); location.reload();
} ); } );
} ); } );
document.body.appendChild( button ); // scene pages carry the toggle in the top nav; the landing gets it floating
( document.querySelector( '.page-nav' ) || document.body ).appendChild( button );
} ); } );
+35
View File
@@ -0,0 +1,35 @@
import * as THREE from 'three';
// Keeps the gradient's bright spot gravitating toward the projection wall as
// the camera orbits: the wall centre (x = -10 in every scene) is projected into
// screen space each frame and eased into the CSS gradient position.
const WALL_CENTRE = new THREE.Vector3( -10, 0, 0 );
export function bindGlow( camera ) {
const projected = new THREE.Vector3();
let x = 12;
let y = 50;
let writtenX = null;
let writtenY = null;
return function update() {
projected.copy( WALL_CENTRE ).project( camera );
// when the wall goes behind the camera the projection flips sides;
// leave the glow where it last was until the wall swings back
if ( projected.z < 1 ) {
const targetX = THREE.MathUtils.clamp( ( projected.x + 1 ) * 50, -40, 140 );
const targetY = THREE.MathUtils.clamp( ( 1 - projected.y ) * 50, -40, 140 );
x += ( targetX - x ) * 0.08;
y += ( targetY - y ) * 0.08;
}
// writing the property repaints the whole gradient, so only touch the
// DOM while the glow is actually moving
if ( writtenX !== null && Math.abs( x - writtenX ) < 0.05 && Math.abs( y - writtenY ) < 0.05 ) return;
writtenX = x;
writtenY = y;
document.body.style.setProperty( '--glow-x', `${x.toFixed( 2 )}%` );
document.body.style.setProperty( '--glow-y', `${y.toFixed( 2 )}%` );
};
}
+5
View File
@@ -5,6 +5,8 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import './chrome.js'; import './chrome.js';
import { sceneColors } from './theme.js'; import { sceneColors } from './theme.js';
import { bindWorld } from './transit.js'; import { bindWorld } from './transit.js';
import { bindGlow } from './glow.js';
import { bindCameraMemory } from './cameraMemory.js';
// Shared scene setup for the exploration pages: the landing page gradient shows // Shared scene setup for the exploration pages: the landing page gradient shows
// through a transparent canvas, a side light throws a light orange shadow onto // through a transparent canvas, a side light throws a light orange shadow onto
@@ -35,6 +37,8 @@ export function createStage( { target = [ 0, 0, 0 ] } = {} ) {
const controls = new OrbitControls( camera, renderer.domElement ); const controls = new OrbitControls( camera, renderer.domElement );
controls.target.set( ...target ); controls.target.set( ...target );
bindCameraMemory( camera, controls );
const updateGlow = bindGlow( camera );
// dark mode gets a soft warm ambient so unlit faces keep their colour // dark mode gets a soft warm ambient so unlit faces keep their colour
if ( colors.dark ) scene.add( new THREE.AmbientLight( colors.light, 0.9 ) ); if ( colors.dark ) scene.add( new THREE.AmbientLight( colors.light, 0.9 ) );
@@ -78,6 +82,7 @@ export function createStage( { target = [ 0, 0, 0 ] } = {} ) {
for ( const update of updates ) update( time ); for ( const update of updates ) update( time );
updateWorld(); updateWorld();
updateGlow();
controls.update(); controls.update();
renderer.render( scene, camera ); renderer.render( scene, camera );
} }
+2 -2
View File
@@ -29,6 +29,6 @@ const fills = {
export function sceneColors() { export function sceneColors() {
return currentTheme() === 'dark' return currentTheme() === 'dark'
? { dark: true, light: 0xffb37a, line: 0xf5e9de, shadow: 0xff9a5c, fills } ? { dark: true, light: 0xffb37a, line: 0x1b0a02, shadow: 0xffb385, fills }
: { dark: false, light: 0xffb37a, line: 0x1b0a02, shadow: 0xf44f04, fills }; : { dark: false, light: 0xffb37a, line: 0xffffff, shadow: 0xd94403, fills };
} }
+5 -4
View File
@@ -9,8 +9,9 @@ const easeOutCubic = ( t ) => 1 - Math.pow( 1 - t, 3 );
const easeInCubic = ( t ) => t * t * t; const easeInCubic = ( t ) => t * t * t;
// wraps a THREE.Group holding all of a page's shapes; call the returned // wraps a THREE.Group holding all of a page's shapes; call the returned
// function every frame // function every frame. baseScale is the group's resting scale, for pages whose
export function bindWorld( world ) { // raw geometry is smaller than the others.
export function bindWorld( world, baseScale = 1 ) {
let mode = 'enter'; let mode = 'enter';
let start = performance.now(); let start = performance.now();
@@ -22,10 +23,10 @@ export function bindWorld( world ) {
return function update() { return function update() {
if ( mode === 'enter' ) { if ( mode === 'enter' ) {
const k = Math.min( 1, ( performance.now() - start ) / ENTER_MS ); const k = Math.min( 1, ( performance.now() - start ) / ENTER_MS );
world.scale.setScalar( Math.max( 0.001, easeOutCubic( k ) ) ); world.scale.setScalar( baseScale * Math.max( 0.001, easeOutCubic( k ) ) );
} else { } else {
const k = Math.min( 1, ( performance.now() - start ) / EXIT_MS ); const k = Math.min( 1, ( performance.now() - start ) / EXIT_MS );
world.scale.setScalar( 1 + easeInCubic( k ) * 7 ); world.scale.setScalar( baseScale * ( 1 + easeInCubic( k ) * 7 ) );
} }
}; };
} }
-1
View File
@@ -7,7 +7,6 @@
<link rel="stylesheet" href="./css/styles.scss"> <link rel="stylesheet" href="./css/styles.scss">
</head> </head>
<body> <body>
<div class="fade"></div>
<nav class="page-nav"> <nav class="page-nav">
<a href="index.html">&larr; Thrive</a> <a href="index.html">&larr; Thrive</a>
<span>Vector Equilibrium</span> <span>Vector Equilibrium</span>
+2 -2
View File
@@ -13,7 +13,7 @@ import { makeTube } from './lib/edgeTubes.js';
const { scene, colors, onUpdate } = createStage(); const { scene, colors, onUpdate } = createStage();
const params = { const params = {
scale: 2, scale: 3,
edges: true, edges: true,
spokes: true, spokes: true,
fill: true, fill: true,
@@ -93,10 +93,10 @@ function renderMatrix() {
} ); } );
const spheres = new THREE.Group(); const spheres = new THREE.Group();
const sphereGeometry = new THREE.SphereGeometry( sphereRadius, 48, 48 ); const sphereGeometry = new THREE.SphereGeometry( sphereRadius, 48, 48 );
// no castShadow: the projection stays an outline of edges and spokes
for ( const point of [ centre, ...vertices ] ) { for ( const point of [ centre, ...vertices ] ) {
const sphere = new THREE.Mesh( sphereGeometry, sphereMaterial ); const sphere = new THREE.Mesh( sphereGeometry, sphereMaterial );
sphere.position.copy( point ); sphere.position.copy( point );
sphere.castShadow = true;
spheres.add( sphere ); spheres.add( sphere );
} }
spheres.visible = params.spheres; spheres.visible = params.spheres;
-1
View File
@@ -7,7 +7,6 @@
<link rel="stylesheet" href="./css/styles.scss"> <link rel="stylesheet" href="./css/styles.scss">
</head> </head>
<body> <body>
<div class="fade"></div>
<nav class="page-nav"> <nav class="page-nav">
<a href="index.html">&larr; Thrive</a> <a href="index.html">&larr; Thrive</a>
<span>Metatron's Cube</span> <span>Metatron's Cube</span>
+113 -190
View File
@@ -8,11 +8,10 @@ import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
import './lib/chrome.js'; import './lib/chrome.js';
import { sceneColors } from './lib/theme.js'; import { sceneColors } from './lib/theme.js';
import { bindWorld } from './lib/transit.js'; import { bindWorld } from './lib/transit.js';
import { bindGlow } from './lib/glow.js';
import { bindCameraMemory } from './lib/cameraMemory.js';
const colors = sceneColors(); const colors = sceneColors();
// the per-solid near-black line colours only read on the light background;
// dark mode swaps them all for the shared light line colour
const lineColor = ( light ) => colors.dark ? colors.line : light;
// the landing gradient shows through a transparent canvas; all shapes live in // the landing gradient shows through a transparent canvas; all shapes live in
// `world`, which page transitions fly in and out // `world`, which page transitions fly in and out
@@ -46,6 +45,8 @@ window.addEventListener( 'resize', () => {
const controls = new OrbitControls( camera, renderer.domElement ); const controls = new OrbitControls( camera, renderer.domElement );
controls.target.set( -4, 0, 0 ); controls.target.set( -4, 0, 0 );
bindCameraMemory( camera, controls );
const updateGlow = bindGlow( camera );
// const geometry_torus = new THREE.TorusGeometry( 2.5, 2.5, 32, 128 ); // const geometry_torus = new THREE.TorusGeometry( 2.5, 2.5, 32, 128 );
@@ -233,151 +234,81 @@ const params = {
// radius: 1 // radius: 1
// }; // };
let sphere, circleCircum, cube, cubeLines; let circleCircum, cube, cubeLines;
let spheres = [];
let spheresLines = [];
let gui; let gui;
let solids = []; let solids = [];
let renderFlower = (radius = 1) => { let renderFlower = (radius = 1) => {
spheres = [];
spheresLines = [];
// tear down the previous render's shapes and GUI so re-rendering never stacks duplicates // tear down the previous render's shapes and GUI so re-rendering never stacks duplicates
if ( gui ) gui.destroy(); if ( gui ) gui.destroy();
for ( let obj of solids ) world.remove( obj ); for ( let obj of solids ) world.remove( obj );
solids = []; solids = [];
// 3D fruit of life: a circle on the centre, on every cube corner and on
// every cube edge midpoint (the inner ring of the 2D pattern)
const d = 4 * 0.612 * radius; const d = 4 * 0.612 * radius;
const points = [ [ 0, 0, 0 ] ];
for ( let sx of [ -1, 1 ] ) { // circles on demand: a translucent sphere and ring on every vertex of a
for ( let sy of [ -1, 1 ] ) { // solid, toggled per element from its folder
for ( let sz of [ -1, 1 ] ) { const circleSphereGeometry = new THREE.SphereGeometry( radius, 48, 48 );
points.push( [ sx * d, sy * d, sz * d ] ); const circleSphereMaterial = new THREE.MeshStandardMaterial( {
}
}
}
// only the edge midpoints whose coordinates share a sign: the other six sit on
// the projection equator and would add circles between the two rings
for ( let s of [ -1, 1 ] ) {
points.push( [ s * d, s * d, 0 ] );
points.push( [ s * d, 0, s * d ] );
points.push( [ 0, s * d, s * d ] );
}
const addCircleAt = ( x, y, z, parent ) => {
let xtemp, ytemp, ztemp;
[xtemp, ytemp, ztemp] = rotatePoint3D(x, y, z, params['yangle'], params['xangle'], params['zangle'])
let sphereGeometry = new THREE.SphereGeometry( radius, 64, 64 );
let sphereMaterial = new THREE.MeshStandardMaterial( {
color: colors.fills.sphere, color: colors.fills.sphere,
metalness: 0.7, metalness: 0.7,
roughness: 0.3, roughness: 0.3,
opacity: 0.8, opacity: 0.8,
transparent: true transparent: true
} ); } );
sphere = new THREE.Mesh( sphereGeometry, sphereMaterial ); const ringGeometry = new THREE.TorusGeometry( radius, params['tube'], 48, 64 );
sphere.position.x = xtemp; const ringMaterial = new THREE.MeshBasicMaterial( {
sphere.position.y = ytemp;
sphere.position.z = ztemp;
parent.add( sphere );
let circleGeometry = new THREE.TorusGeometry( radius, params['tube'], 64, 64 );
let circleMaterial = new THREE.MeshBasicMaterial( {
color: colors.line, color: colors.line,
opacity: 0.1, opacity: 0.35,
transparent: true transparent: true
} ); } );
let ring = new THREE.Mesh( circleGeometry, circleMaterial );
ring.castShadow = true; const occupiedSpots = new Set();
// one circle per spot: if any group already placed a circle here (fire and
// earth share all eight cube corners), later ones are skipped
const addCircleMeshes = ( point, group ) => {
const spot = point.toArray().map( ( n ) => n.toFixed( 2 ) ).join( ',' );
if ( occupiedSpots.has( spot ) ) return;
occupiedSpots.add( spot );
const sphere = new THREE.Mesh( circleSphereGeometry, circleSphereMaterial );
sphere.position.copy( point );
group.add( sphere );
const ring = new THREE.Mesh( ringGeometry, ringMaterial );
ring.rotation.y = Math.PI / 2; ring.rotation.y = Math.PI / 2;
ring.position.x = xtemp; ring.position.copy( point );
ring.position.y = ytemp; ring.castShadow = true;
ring.position.z = ztemp; group.add( ring );
parent.add( ring );
circleGeometry = new THREE.TorusGeometry( 0.01, 0.01, 64, 64 );
circleMaterial = new THREE.MeshBasicMaterial( {
color: colors.line,
opacity: 0.1,
transparent: true
} );
circleCircum = new THREE.Mesh( circleGeometry, circleMaterial );
circleCircum.castShadow = true;
circleCircum.rotation.y = Math.PI / 2;
circleCircum.position.x = xtemp;
circleCircum.position.y = ytemp;
circleCircum.position.z = ztemp;
parent.add( circleCircum );
sphere.start_x = x;
sphere.start_y = y;
sphere.start_z = z;
ring.start_x = x;
ring.start_y = y;
ring.start_z = z;
circleCircum.start_x = x;
circleCircum.start_y = y;
circleCircum.start_z = z;
spheres.push(sphere)
spheresLines.push(ring)
spheresLines.push(circleCircum)
}; };
for ( let point of points ) { // circles on a solid's vertices
addCircleAt( ...point, world ); const makeVertexCircles = ( geometry, rotation ) => {
const group = new THREE.Group();
const seen = new Set();
const positions = geometry.attributes.position;
const vertex = new THREE.Vector3();
for ( let i = 0; i < positions.count; i++ ) {
vertex.fromBufferAttribute( positions, i );
const key = vertex.toArray().map( ( n ) => n.toFixed( 3 ) ).join( ',' );
if ( seen.has( key ) ) continue;
seen.add( key );
const point = vertex.clone().applyMatrix4( rotation );
addCircleMeshes( point, group );
} }
// the remaining cube circles: the six equatorial edge midpoints and the six return group;
// face centres, hidden behind the "allCubeCircles" toggle };
const extraCircles = new THREE.Group();
extraCircles.visible = false;
for ( let s of [ -1, 1 ] ) { const addCircles = ( group, visible ) => {
for ( let extraPoint of [ group.visible = visible;
[ s * d, -s * d, 0 ], [ s * d, 0, -s * d ], [ 0, s * d, -s * d ], world.add( group );
[ s * d, 0, 0 ], [ 0, s * d, 0 ], [ 0, 0, s * d ], solids.push( group );
] ) { return group;
addCircleAt( ...extraPoint, extraCircles ); };
}
}
world.add( extraCircles );
// Metatron's web: every circle centre connected to every other one
let web = new THREE.Group();
const webMaterial = new THREE.MeshBasicMaterial( { color: colors.line, opacity: 0.5, transparent: true } );
const webUp = new THREE.Vector3( 0, 1, 0 );
for ( let i = 0; i < points.length; i++ ) {
for ( let j = i + 1; j < points.length; j++ ) {
const [ ax, ay, az ] = rotatePoint3D( ...points[ i ], params[ 'yangle' ], params[ 'xangle' ], params[ 'zangle' ] );
const [ bx, by, bz ] = rotatePoint3D( ...points[ j ], params[ 'yangle' ], params[ 'xangle' ], params[ 'zangle' ] );
const a = new THREE.Vector3( ax, ay, az );
const b = new THREE.Vector3( bx, by, bz );
const direction = new THREE.Vector3().subVectors( b, a );
const length = direction.length();
const webTube = params['tube'] * 0.4;
const cylinder = new THREE.CylinderGeometry( webTube, webTube, length, 6 );
cylinder.translate( 0, length / 2, 0 );
const strand = new THREE.Mesh( cylinder, webMaterial );
strand.position.copy( a );
strand.quaternion.setFromUnitVectors( webUp, direction.clone().normalize() );
web.add( strand );
}
}
web.visible = false;
world.add( web );
// the exact rotation the circle grid uses, as a matrix - solids aligned with // the exact rotation the circle grid uses, as a matrix - solids aligned with
// this coincide with the dots instead of approximating them with tuned angles // this coincide with the dots instead of approximating them with tuned angles
@@ -401,13 +332,15 @@ let renderFlower = (radius = 1) => {
cube = new THREE.Mesh( boxGeometry, boxMaterial ); cube = new THREE.Mesh( boxGeometry, boxMaterial );
cube.setRotationFromMatrix( gridRotation ); cube.setRotationFromMatrix( gridRotation );
cubeLines = makeEdgeTubes( boxGeometry, lineColor( 0x1b0a02 ), params['tube'] ); cubeLines = makeEdgeTubes( boxGeometry, colors.line, params['tube'] );
cubeLines.setRotationFromMatrix( gridRotation ); cubeLines.setRotationFromMatrix( gridRotation );
cubeLines.castShadow = true; cubeLines.castShadow = true;
world.add( cubeLines ); world.add( cubeLines );
world.add( cube ); world.add( cube );
const cubeCircles = addCircles( makeVertexCircles( boxGeometry, gridRotation ), true );
// fire - radius sqrt(3)d puts every vertex exactly on a corner dot // fire - radius sqrt(3)d puts every vertex exactly on a corner dot
const tetraGeometry = new THREE.TetrahedronGeometry( Math.sqrt( 3 ) * d ) const tetraGeometry = new THREE.TetrahedronGeometry( Math.sqrt( 3 ) * d )
const tetraMaterial = new THREE.MeshStandardMaterial( { const tetraMaterial = new THREE.MeshStandardMaterial( {
@@ -419,7 +352,7 @@ let renderFlower = (radius = 1) => {
} ); } );
let tetra = new THREE.Mesh( tetraGeometry, tetraMaterial ); let tetra = new THREE.Mesh( tetraGeometry, tetraMaterial );
let tetraLines = makeEdgeTubes( tetraGeometry, lineColor( 0x140101 ), params['tube'] ); let tetraLines = makeEdgeTubes( tetraGeometry, colors.line, params['tube'] );
@@ -427,21 +360,29 @@ let renderFlower = (radius = 1) => {
tetra.setRotationFromMatrix( gridRotation ); tetra.setRotationFromMatrix( gridRotation );
tetraLines.castShadow = true; tetraLines.castShadow = true;
world.add( tetraLines );
world.add( tetra );
// second tetrahedron of the star tetrahedron (Merkaba): the same shape // second tetrahedron of the star tetrahedron (Merkaba): the same shape
// rotated a quarter turn so it interlocks with the first // rotated a quarter turn so it interlocks with the first
const tetraGeometry2 = new THREE.TetrahedronGeometry( Math.sqrt( 3 ) * d ); const tetraGeometry2 = new THREE.TetrahedronGeometry( Math.sqrt( 3 ) * d );
tetraGeometry2.rotateZ( Math.PI / 2 ); tetraGeometry2.rotateZ( Math.PI / 2 );
let tetra2 = new THREE.Mesh( tetraGeometry2, tetraMaterial ); let tetra2 = new THREE.Mesh( tetraGeometry2, tetraMaterial );
let tetraLines2 = makeEdgeTubes( tetraGeometry2, lineColor( 0x140101 ), params['tube'] ); let tetraLines2 = makeEdgeTubes( tetraGeometry2, colors.line, params['tube'] );
tetra2.rotation.copy( tetra.rotation ); tetra2.rotation.copy( tetra.rotation );
tetraLines2.rotation.copy( tetraLines.rotation ); tetraLines2.rotation.copy( tetraLines.rotation );
world.add( tetraLines2 ); // both tetrahedra form one element, controlled together
world.add( tetra2 ); const fireFill = new THREE.Group();
fireFill.add( tetra, tetra2 );
const fireFrame = new THREE.Group();
fireFrame.add( tetraLines, tetraLines2 );
world.add( fireFrame );
world.add( fireFill );
const fireCircles = new THREE.Group();
fireCircles.add( makeVertexCircles( tetraGeometry, gridRotation ) );
fireCircles.add( makeVertexCircles( tetraGeometry2, gridRotation ) );
addCircles( fireCircles, true );
// radius 2d puts the projected corners exactly on the outer circle centres // radius 2d puts the projected corners exactly on the outer circle centres
const octaGeometry = new THREE.OctahedronGeometry( 2 * d ) const octaGeometry = new THREE.OctahedronGeometry( 2 * d )
@@ -454,7 +395,7 @@ let renderFlower = (radius = 1) => {
} ); } );
let octa = new THREE.Mesh( octaGeometry, octaMaterial ); let octa = new THREE.Mesh( octaGeometry, octaMaterial );
let octaLines = makeEdgeTubes( octaGeometry, lineColor( 0x0a2528 ), params['tube'] ); let octaLines = makeEdgeTubes( octaGeometry, colors.line, params['tube'] );
octaLines.setRotationFromMatrix( gridRotation ); octaLines.setRotationFromMatrix( gridRotation );
octa.setRotationFromMatrix( gridRotation ); octa.setRotationFromMatrix( gridRotation );
@@ -463,6 +404,8 @@ let renderFlower = (radius = 1) => {
world.add( octaLines ); world.add( octaLines );
world.add( octa ); world.add( octa );
const octaCircles = addCircles( makeVertexCircles( octaGeometry, gridRotation ), true );
// radius 1.663d puts the projected corners on the outer circle centres // radius 1.663d puts the projected corners on the outer circle centres
const icosaGeometry = new THREE.IcosahedronGeometry( 1.663 * d ) const icosaGeometry = new THREE.IcosahedronGeometry( 1.663 * d )
const icosaMaterial = new THREE.MeshStandardMaterial( { const icosaMaterial = new THREE.MeshStandardMaterial( {
@@ -474,7 +417,7 @@ let renderFlower = (radius = 1) => {
} ); } );
let icosa = new THREE.Mesh( icosaGeometry, icosaMaterial ); let icosa = new THREE.Mesh( icosaGeometry, icosaMaterial );
let icosaLines = makeEdgeTubes( icosaGeometry, lineColor( 0x000000 ), params['tube'] ); let icosaLines = makeEdgeTubes( icosaGeometry, colors.line, params['tube'] );
// a grid-aligned icosahedron projects its corners 22.2 degrees off the dot // a grid-aligned icosahedron projects its corners 22.2 degrees off the dot
// hexagon, so it gets an extra twist about the view diagonal to meet the dots // hexagon, so it gets an extra twist about the view diagonal to meet the dots
@@ -492,6 +435,8 @@ let renderFlower = (radius = 1) => {
world.add( icosaLines ); world.add( icosaLines );
world.add( icosa ); world.add( icosa );
const icosaCircles = addCircles( makeVertexCircles( icosaGeometry, icosaRotation ), false );
// 2.29 * 1.003: tuned against the circle pattern // 2.29 * 1.003: tuned against the circle pattern
const dodecaGeometry = new THREE.DodecahedronGeometry( 2.297 * radius ) const dodecaGeometry = new THREE.DodecahedronGeometry( 2.297 * radius )
const dodecaMaterial = new THREE.MeshStandardMaterial( { const dodecaMaterial = new THREE.MeshStandardMaterial( {
@@ -503,7 +448,7 @@ let renderFlower = (radius = 1) => {
} ); } );
let dodeca = new THREE.Mesh( dodecaGeometry, dodecaMaterial ); let dodeca = new THREE.Mesh( dodecaGeometry, dodecaMaterial );
let dodecaLines = makeEdgeTubes( dodecaGeometry, lineColor( 0x000000 ), params['tube'] ); let dodecaLines = makeEdgeTubes( dodecaGeometry, colors.line, params['tube'] );
// aether: a dodecahedron vertex sits on the cube diagonal, so the plain grid // aether: a dodecahedron vertex sits on the cube diagonal, so the plain grid
// rotation points a vertex straight at the light and shares the cube's frame, // rotation points a vertex straight at the light and shares the cube's frame,
@@ -518,6 +463,18 @@ let renderFlower = (radius = 1) => {
world.add( dodeca ); world.add( dodeca );
// the in-between circles of the 2D pattern: the six edge midpoints that
// form the inner ring
const innerCircles = new THREE.Group();
const innerPoints = [];
for ( let s of [ -1, 1 ] ) {
innerPoints.push( [ s * d, s * d, 0 ], [ s * d, 0, s * d ], [ 0, s * d, s * d ] );
}
for ( const p of innerPoints ) {
addCircleMeshes( new THREE.Vector3( ...p ).applyMatrix4( gridRotation ), innerCircles );
}
addCircles( innerCircles, true );
// Big circle: visible as a solid tube, same thickness as the shape edges // Big circle: visible as a solid tube, same thickness as the shape edges
let circleGeometry = new THREE.TorusGeometry( 5 * radius, params['tube'], 64, 64); let circleGeometry = new THREE.TorusGeometry( 5 * radius, params['tube'], 64, 64);
let circleMaterial = new THREE.MeshBasicMaterial( { color: colors.line } ); let circleMaterial = new THREE.MeshBasicMaterial( { color: colors.line } );
@@ -526,7 +483,7 @@ let renderFlower = (radius = 1) => {
circleCircum.castShadow = true; circleCircum.castShadow = true;
world.add( circleCircum ); world.add( circleCircum );
solids.push( cube, cubeLines, tetra, tetraLines, tetra2, tetraLines2, octa, octaLines, icosa, icosaLines, dodeca, dodecaLines, circleCircum, web, extraCircles ); solids.push( cube, cubeLines, fireFill, fireFrame, octa, octaLines, icosa, icosaLines, dodeca, dodecaLines, circleCircum );
gui = new GUI(); gui = new GUI();
@@ -534,34 +491,20 @@ let renderFlower = (radius = 1) => {
let guiDict = {}; let guiDict = {};
let fire = gui.addFolder('Fire'); let fire = gui.addFolder('Fire');
guiDict['fire'] = {'folder': fire, 'object': tetra, 'objectLines': tetraLines}; guiDict['fire'] = {'folder': fire, 'object': fireFill, 'objectLines': fireFrame, 'circles': fireCircles};
let fire2 = gui.addFolder('Fire 2');
guiDict['fire2'] = {'folder': fire2, 'object': tetra2, 'objectLines': tetraLines2};
let air = gui.addFolder('Air'); let air = gui.addFolder('Air');
guiDict['air'] = {'folder': air, 'object': octa, 'objectLines': octaLines}; guiDict['air'] = {'folder': air, 'object': octa, 'objectLines': octaLines, 'circles': octaCircles};
let earth = gui.addFolder('Earth'); let earth = gui.addFolder('Earth');
guiDict['earth'] = {'folder': earth, 'object': cube, 'objectLines': cubeLines}; guiDict['earth'] = {'folder': earth, 'object': cube, 'objectLines': cubeLines, 'circles': cubeCircles};
let water = gui.addFolder('Water'); let water = gui.addFolder('Water');
guiDict['water'] = {'folder': water, 'object': icosa, 'objectLines': icosaLines}; guiDict['water'] = {'folder': water, 'object': icosa, 'objectLines': icosaLines, 'circles': icosaCircles};
let aether = gui.addFolder('Aether') let aether = gui.addFolder('Aether')
guiDict['aether'] = {'folder': aether, 'object': dodeca, 'objectLines': dodecaLines}; guiDict['aether'] = {'folder': aether, 'object': dodeca, 'objectLines': dodecaLines};
let circles = gui.addFolder('Circles')
guiDict['circles'] = {'folder': circles};
circles.add( { web: false }, 'web' ).onChange( ( value ) => {
web.visible = value;
} );
circles.add( { allCubeCircles: false }, 'allCubeCircles' ).onChange( ( value ) => {
extraCircles.visible = value;
} );
let folderStandard = { let folderStandard = {
show: true, show: true,
fill: true, fill: true,
@@ -569,29 +512,8 @@ let renderFlower = (radius = 1) => {
scale: 1, scale: 1,
} }
circles.add( folderStandard, 'show', true).onChange((value) => {
if (value) {
for (let sphere of spheres) {
sphere.visible = true;
}
for (let sphereLine of spheresLines) {
sphereLine.visible = true;
}
} else {
for (let sphere of spheres) {
sphere.visible = false;
}
for (let sphereLine of spheresLines) {
sphereLine.visible = false;
}
}
});
for (let fld of Object.keys(guiDict)) { for (let fld of Object.keys(guiDict)) {
// guiDict[fld]['folder'].close() guiDict[fld]['folder'].close()
if (fld === "circles") {
continue
}
folderStandard.show = true; folderStandard.show = true;
guiDict[fld]['folder'].add( folderStandard, 'scale', 0, 2 ).step(0.001).onChange( (value) => { guiDict[fld]['folder'].add( folderStandard, 'scale', 0, 2 ).step(0.001).onChange( (value) => {
@@ -601,6 +523,7 @@ let renderFlower = (radius = 1) => {
guiDict[fld]['objectLines'].scale.x = value guiDict[fld]['objectLines'].scale.x = value
guiDict[fld]['objectLines'].scale.y = value guiDict[fld]['objectLines'].scale.y = value
guiDict[fld]['objectLines'].scale.z = value guiDict[fld]['objectLines'].scale.z = value
if (guiDict[fld]['circles']) guiDict[fld]['circles'].scale.setScalar(value);
}); });
guiDict[fld]['folder'].add( folderStandard, 'show', true).onChange((value) => { guiDict[fld]['folder'].add( folderStandard, 'show', true).onChange((value) => {
@@ -626,28 +549,27 @@ let renderFlower = (radius = 1) => {
guiDict[fld]['objectLines'].visible = false; guiDict[fld]['objectLines'].visible = false;
} }
}); });
if (guiDict[fld]['circles']) {
guiDict[fld]['folder'].add( { circles: guiDict[fld]['circles'].visible }, 'circles' ).onChange( (value) => {
guiDict[fld]['circles'].visible = value;
});
}
} }
gui.add( params, 'radius', 0.5, 1.5).onChange( (value) => { gui.add( { innerCircles: true }, 'innerCircles' ).onChange( (value) => {
for (let sphere of spheres) { innerCircles.visible = value;
world.remove(sphere) });
} // rebuilding destroys and recreates the GUI, which would kill the very
for (let sphereLine of spheresLines) { // slider being dragged - so the scene only rebuilds when the drag ends
world.remove(sphereLine) gui.add( params, 'radius', 0.5, 1.5).onFinishChange( (value) => {
}
renderFlower (value) renderFlower (value)
}); });
gui.add( params, 'tube', 0.01, 0.1 ).step( 0.005 ).onChange( () => { gui.add( params, 'tube', 0.01, 0.1 ).step( 0.005 ).onFinishChange( () => {
for (let sphere of spheres) {
world.remove(sphere)
}
for (let sphereLine of spheresLines) {
world.remove(sphereLine)
}
renderFlower (params['radius']) renderFlower (params['radius'])
}); });
// ?only=<fire|fire2|air|earth|water|aether> hides the other solids, for // ?only=<fire|air|earth|water|aether> hides the other solids, for
// checking a single shape against the reference // checking a single shape against the reference
const only = debugFlags.get( 'only' ); const only = debugFlags.get( 'only' );
if ( only && guiDict[ only ] ) { if ( only && guiDict[ only ] ) {
@@ -658,7 +580,7 @@ let renderFlower = (radius = 1) => {
} }
} }
gui.close();
} }
renderFlower() renderFlower()
@@ -717,6 +639,7 @@ function animate () {
// } // }
updateWorld(); updateWorld();
updateGlow();
controls.update(); controls.update();
renderer.render( scene, camera ); renderer.render( scene, camera );
-1
View File
@@ -7,7 +7,6 @@
<link rel="stylesheet" href="./css/styles.scss"> <link rel="stylesheet" href="./css/styles.scss">
</head> </head>
<body> <body>
<div class="fade"></div>
<nav class="page-nav"> <nav class="page-nav">
<a href="index.html">&larr; Thrive</a> <a href="index.html">&larr; Thrive</a>
<span>Golden Spiral</span> <span>Golden Spiral</span>
+22 -11
View File
@@ -17,6 +17,7 @@ const params = {
angle: GOLDEN_ANGLE, angle: GOLDEN_ANGLE,
form: 'sunflower', form: 'sunflower',
grow: false, grow: false,
speed: 1,
spin: true, spin: true,
resetAngle: () => { resetAngle: () => {
params.angle = GOLDEN_ANGLE; params.angle = GOLDEN_ANGLE;
@@ -50,20 +51,20 @@ function renderPattern() {
if ( params.form === 'sunflower' ) { if ( params.form === 'sunflower' ) {
// r ~ sqrt(i) keeps the seed density constant across the disc // r ~ sqrt(i) keeps the seed density constant across the disc
const r = 5 * Math.sqrt( t ); const r = 6.5 * Math.sqrt( t );
dummy.position.set( r * Math.cos( theta ), r * Math.sin( theta ), 0 ); dummy.position.set( r * Math.cos( theta ), r * Math.sin( theta ), 0 );
dummy.scale.setScalar( 0.11 ); dummy.scale.setScalar( 0.15 );
} else if ( params.form === 'pinecone' ) { } else if ( params.form === 'pinecone' ) {
const r = 3.5 * Math.sqrt( t ); const r = 4.5 * Math.sqrt( t );
dummy.position.set( r * Math.cos( theta ), r * Math.sin( theta ), 4 * ( 1 - t ) - 2 ); dummy.position.set( r * Math.cos( theta ), r * Math.sin( theta ), 6 * ( 1 - t ) - 3 );
dummy.scale.setScalar( 0.13 ); dummy.scale.setScalar( 0.17 );
} else { } else {
// fibonacci sphere: even latitude steps + golden angle in longitude // fibonacci sphere: even latitude steps + golden angle in longitude
// spreads points almost perfectly uniformly over a sphere // spreads points almost perfectly uniformly over a sphere
const y = 1 - 2 * t; const y = 1 - 2 * t;
const r = Math.sqrt( Math.max( 0, 1 - y * y ) ); const r = Math.sqrt( Math.max( 0, 1 - y * y ) );
dummy.position.set( 4 * r * Math.cos( theta ), 4 * r * Math.sin( theta ), 4 * y ); dummy.position.set( 5.5 * r * Math.cos( theta ), 5.5 * r * Math.sin( theta ), 5.5 * y );
dummy.scale.setScalar( 0.12 ); dummy.scale.setScalar( 0.16 );
} }
dummy.updateMatrix(); dummy.updateMatrix();
@@ -78,12 +79,19 @@ function renderPattern() {
renderPattern(); renderPattern();
// growth loop: seeds appear one by one from the centre the way the plant grows
// them, rest at the full shape, then start over; speed scrubs the growth rate
const GROW_S = 6;
const PAUSE_S = 4;
onUpdate( ( time ) => { onUpdate( ( time ) => {
if ( params.spin ) group.rotation.x = time * 0.1; if ( params.spin ) group.rotation.x = time * 0.1;
if ( params.grow ) { if ( params.grow ) {
// seeds appear one by one from the centre, the way the plant grows them const growDuration = GROW_S / params.speed;
mesh.count = Math.min( params.count, Math.floor( ( time % 30 ) * 100 ) + 1 ); const position = time % ( growDuration + PAUSE_S );
const progress = Math.min( 1, position / growDuration );
mesh.count = Math.max( 1, Math.floor( progress * params.count ) );
} }
} ); } );
@@ -92,5 +100,8 @@ gui.add( params, 'count', 10, MAX_COUNT ).step( 10 ).onChange( renderPattern );
gui.add( params, 'angle', 130, 145 ).step( 0.001 ).onChange( renderPattern ); gui.add( params, 'angle', 130, 145 ).step( 0.001 ).onChange( renderPattern );
gui.add( params, 'resetAngle' ).name( 'reset to golden angle' ); gui.add( params, 'resetAngle' ).name( 'reset to golden angle' );
gui.add( params, 'form', [ 'sunflower', 'pinecone', 'sphere' ] ).onChange( renderPattern ); gui.add( params, 'form', [ 'sunflower', 'pinecone', 'sphere' ] ).onChange( renderPattern );
gui.add( params, 'grow' ).onChange( () => { mesh.count = params.count; } );
gui.add( params, 'spin' ); const animation = gui.addFolder( 'Animation' );
animation.add( params, 'grow' ).onChange( () => { mesh.count = params.count; } );
animation.add( params, 'speed', 0.25, 4 ).step( 0.05 );
animation.add( params, 'spin' );
-1
View File
@@ -7,7 +7,6 @@
<link rel="stylesheet" href="./css/styles.scss"> <link rel="stylesheet" href="./css/styles.scss">
</head> </head>
<body> <body>
<div class="fade"></div>
<nav class="page-nav"> <nav class="page-nav">
<a href="index.html">&larr; Thrive</a> <a href="index.html">&larr; Thrive</a>
<span>Seed of Life</span> <span>Seed of Life</span>
+6 -1
View File
@@ -8,6 +8,8 @@ import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
import './lib/chrome.js'; import './lib/chrome.js';
import { sceneColors } from './lib/theme.js'; import { sceneColors } from './lib/theme.js';
import { bindWorld } from './lib/transit.js'; import { bindWorld } from './lib/transit.js';
import { bindGlow } from './lib/glow.js';
import { bindCameraMemory } from './lib/cameraMemory.js';
const colors = sceneColors(); const colors = sceneColors();
@@ -16,7 +18,7 @@ const colors = sceneColors();
const scene = new THREE.Scene(); const scene = new THREE.Scene();
const world = new THREE.Group(); const world = new THREE.Group();
scene.add( world ); scene.add( world );
const updateWorld = bindWorld( world ); const updateWorld = bindWorld( world, 3 );
const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 500 ); const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 500 );
@@ -42,6 +44,8 @@ window.addEventListener( 'resize', () => {
const controls = new OrbitControls( camera, renderer.domElement ); const controls = new OrbitControls( camera, renderer.domElement );
controls.target.set( -4, 0, 0 ); controls.target.set( -4, 0, 0 );
bindCameraMemory( camera, controls );
const updateGlow = bindGlow( camera );
// const geometry_torus = new THREE.TorusGeometry( 2.5, 2.5, 32, 128 ); // const geometry_torus = new THREE.TorusGeometry( 2.5, 2.5, 32, 128 );
@@ -440,6 +444,7 @@ function animate () {
// } // }
updateWorld(); updateWorld();
updateGlow();
controls.update(); controls.update();
renderer.render( scene, camera ); renderer.render( scene, camera );
-1
View File
@@ -7,7 +7,6 @@
<link rel="stylesheet" href="./css/styles.scss"> <link rel="stylesheet" href="./css/styles.scss">
</head> </head>
<body> <body>
<div class="fade"></div>
<nav class="page-nav"> <nav class="page-nav">
<a href="index.html">&larr; Thrive</a> <a href="index.html">&larr; Thrive</a>
<span>Toroidal Field</span> <span>Toroidal Field</span>
+3 -2
View File
@@ -9,8 +9,8 @@ import { createStage } from './lib/stage.js';
const { scene, colors, onUpdate } = createStage(); const { scene, colors, onUpdate } = createStage();
const params = { const params = {
radius: 3, radius: 4,
tube: 1.4, tube: 1.7,
strands: 12, strands: 12,
windings: 4, windings: 4,
surface: true, surface: true,
@@ -80,6 +80,7 @@ function renderTorus() {
curves.push( curve ); curves.push( curve );
const particle = new THREE.Mesh( particleGeometry, particleMaterial ); const particle = new THREE.Mesh( particleGeometry, particleMaterial );
particle.castShadow = true;
particle.userData.phase = s / params.strands; particle.userData.phase = s / params.strands;
group.add( particle ); group.add( particle );
parts.push( particle ); parts.push( particle );