page transition, optimize lighting
This commit is contained in:
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">← Thrive</a>
|
<a href="index.html">← Thrive</a>
|
||||||
<span>Flower of Life</span>
|
<span>Flower of Life</span>
|
||||||
|
|||||||
+6
-1
@@ -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 );
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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 );
|
||||||
} );
|
} );
|
||||||
|
|||||||
@@ -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,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
@@ -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
@@ -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 ) );
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">← Thrive</a>
|
<a href="index.html">← Thrive</a>
|
||||||
<span>Vector Equilibrium</span>
|
<span>Vector Equilibrium</span>
|
||||||
|
|||||||
+2
-2
@@ -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;
|
||||||
|
|||||||
@@ -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">← Thrive</a>
|
<a href="index.html">← Thrive</a>
|
||||||
<span>Metatron's Cube</span>
|
<span>Metatron's Cube</span>
|
||||||
|
|||||||
+113
-190
@@ -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 );
|
||||||
|
|||||||
@@ -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">← Thrive</a>
|
<a href="index.html">← Thrive</a>
|
||||||
<span>Golden Spiral</span>
|
<span>Golden Spiral</span>
|
||||||
|
|||||||
+22
-11
@@ -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' );
|
||||||
|
|||||||
@@ -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">← Thrive</a>
|
<a href="index.html">← Thrive</a>
|
||||||
<span>Seed of Life</span>
|
<span>Seed of Life</span>
|
||||||
|
|||||||
+6
-1
@@ -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 );
|
||||||
|
|||||||
@@ -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">← Thrive</a>
|
<a href="index.html">← Thrive</a>
|
||||||
<span>Toroidal Field</span>
|
<span>Toroidal Field</span>
|
||||||
|
|||||||
+3
-2
@@ -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 );
|
||||||
|
|||||||
Reference in New Issue
Block a user