move to vite, landing page, add more shapes
This commit is contained in:
@@ -1 +1,44 @@
|
|||||||
# Thrive
|
# Thrive
|
||||||
|
|
||||||
|
Sacred geometry, lifted into three dimensions with [three.js](https://threejs.org/).
|
||||||
|
|
||||||
|
Most sacred-geometry figures are drawn flat. This project asks what they look like
|
||||||
|
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
|
||||||
|
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
|
||||||
|
parameters.
|
||||||
|
|
||||||
|
## Pages
|
||||||
|
|
||||||
|
| Page | File | What it shows |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Landing | `src/index.js` | Overview of all explorations, with an animated seed-of-life backdrop |
|
||||||
|
| 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 |
|
||||||
|
| 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 |
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
npm start # dev server with hot reload
|
||||||
|
npm run build # production build into build/
|
||||||
|
```
|
||||||
|
|
||||||
|
Each page is its own webpack entry; the page list lives at the top of
|
||||||
|
`webpack.config.js`. To add a new exploration, add `src/<name>.js` (the shared
|
||||||
|
scene setup is in `src/lib/stage.js`) and one line to the `pages` array.
|
||||||
|
|
||||||
|
Every page shares the chrome in `src/lib/chrome.js`: fade transitions between
|
||||||
|
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)
|
||||||
|
|
||||||
|
* `?noaa` - disable antialiasing
|
||||||
|
* `?noshadow` - disable shadow maps
|
||||||
|
* `?only=<fire|fire2|air|earth|water|aether>` - show a single solid
|
||||||
|
|||||||
Generated
+942
-4133
File diff suppressed because it is too large
Load Diff
+7
-10
@@ -1,17 +1,14 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"css-loader": "^7.1.4",
|
"three": "^0.185.1"
|
||||||
"html-webpack-plugin": "^5.6.7",
|
},
|
||||||
|
"devDependencies": {
|
||||||
"sass": "^1.101.0",
|
"sass": "^1.101.0",
|
||||||
"sass-loader": "^17.0.0",
|
"vite": "^7.3.0"
|
||||||
"style-loader": "^4.0.0",
|
|
||||||
"three": "^0.185.1",
|
|
||||||
"webpack": "^5.108.4",
|
|
||||||
"webpack-cli": "^7.2.1",
|
|
||||||
"webpack-dev-server": "^6.0.0"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "webpack serve --env mode=development",
|
"start": "vite",
|
||||||
"build": "webpack --config webpack.config.js --env mode=production"
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,254 @@
|
|||||||
|
// theme variables; dark is the default (also covers the instant before JS sets
|
||||||
|
// the data-theme attribute), light is the same landing palette inverted.
|
||||||
|
// Every page shares --page-bg: the scene canvases are transparent, so the
|
||||||
|
// landing gradient is the scenery background, 1:1.
|
||||||
|
:root {
|
||||||
|
--page-bg: radial-gradient(ellipse at 50% 30%, #2a1204 0%, #120701 60%, #000 100%);
|
||||||
|
--fade-bg: #120701;
|
||||||
|
--ink: #f5e9de;
|
||||||
|
--ink-soft: rgba(245, 233, 222, 0.7);
|
||||||
|
--ink-faint: rgba(245, 233, 222, 0.55);
|
||||||
|
--panel-bg: rgba(18, 7, 1, 0.75);
|
||||||
|
--panel-border: rgba(244, 79, 4, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='light'] {
|
||||||
|
--page-bg: radial-gradient(ellipse at 50% 30%, #fff8f0 0%, #ffe7d3 60%, #ffd4b3 100%);
|
||||||
|
--fade-bg: #ffe7d3;
|
||||||
|
--ink: #1b0a02;
|
||||||
|
--ink-soft: rgba(27, 10, 2, 0.7);
|
||||||
|
--ink-faint: rgba(27, 10, 2, 0.5);
|
||||||
|
--panel-bg: rgba(255, 248, 240, 0.85);
|
||||||
|
--panel-border: rgba(244, 79, 4, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
background: var(--page-bg);
|
||||||
|
font-family: Georgia, 'Times New Roman', serif;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
// page transition: opaque at first paint, faded out by chrome.js once the page
|
||||||
|
// is ready, faded back in before navigating away. High z-index so it also
|
||||||
|
// covers stats (z-index 10000) and lil-gui.
|
||||||
|
.fade {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 10001;
|
||||||
|
background: var(--fade-bg);
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.35s ease;
|
||||||
|
|
||||||
|
&.done {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 16px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 20;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--panel-bg);
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #f44f04;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// back-link on the exploration pages; bottom-left because stats sits top-left
|
||||||
|
// and lil-gui top-right
|
||||||
|
.page-nav {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 16px;
|
||||||
|
left: 16px;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: baseline;
|
||||||
|
font-family: Georgia, 'Times New Roman', serif;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--panel-bg);
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #f44f04;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--ink-soft);
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- control panel (lil-gui reskin) ---------- //
|
||||||
|
|
||||||
|
.lil-gui {
|
||||||
|
--background-color: var(--panel-bg);
|
||||||
|
--text-color: var(--ink);
|
||||||
|
--title-background-color: transparent;
|
||||||
|
--title-text-color: #f44f04;
|
||||||
|
--widget-color: rgba(244, 79, 4, 0.18);
|
||||||
|
--hover-color: rgba(244, 79, 4, 0.3);
|
||||||
|
--focus-color: rgba(244, 79, 4, 0.45);
|
||||||
|
--number-color: #ff9a5c;
|
||||||
|
--string-color: #ff9a5c;
|
||||||
|
--font-family: Georgia, 'Times New Roman', serif;
|
||||||
|
--font-size: 13px;
|
||||||
|
--input-font-size: 13px;
|
||||||
|
--widget-border-radius: 6px;
|
||||||
|
|
||||||
|
&.root {
|
||||||
|
margin: 12px;
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.root > .title {
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='light'] .lil-gui {
|
||||||
|
--number-color: #c2410c;
|
||||||
|
--string-color: #c2410c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- landing ---------- //
|
||||||
|
|
||||||
|
body.landing {
|
||||||
|
--card-bg: rgba(18, 7, 1, 0.65);
|
||||||
|
--card-bg-hover: rgba(42, 18, 4, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='light'] body.landing {
|
||||||
|
--card-bg: rgba(255, 255, 255, 0.6);
|
||||||
|
--card-bg-hover: rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-bg {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 10vh 24px 12vh;
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 64px;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 64px;
|
||||||
|
font-weight: normal;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
margin: 0;
|
||||||
|
color: #f44f04;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 12px 0 0;
|
||||||
|
font-size: 18px;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
display: block;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
border-color: #f44f04;
|
||||||
|
background: var(--card-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #f44f04;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.soon {
|
||||||
|
margin-top: 72px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
margin: 0 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html oncontextmenu="return false;">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Flower of Life</title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="./css/styles.scss">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="fade"></div>
|
||||||
|
<nav class="page-nav">
|
||||||
|
<a href="index.html">← Thrive</a>
|
||||||
|
<span>Flower of Life</span>
|
||||||
|
</nav>
|
||||||
|
<script type="module" src="./flower.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+45
-37
@@ -4,12 +4,19 @@ import * as THREE from 'three';
|
|||||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||||
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
|
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
|
||||||
|
|
||||||
import Stats from 'three/addons/libs/stats.module.js';
|
|
||||||
|
|
||||||
import './css/styles.scss';
|
import './lib/chrome.js';
|
||||||
|
import { sceneColors } from './lib/theme.js';
|
||||||
|
import { bindWorld } from './lib/transit.js';
|
||||||
|
|
||||||
|
const colors = sceneColors();
|
||||||
|
|
||||||
|
// the landing gradient shows through a transparent canvas; all shapes live in
|
||||||
|
// `world`, which page transitions fly in and out
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
scene.background = new THREE.Color(0xf44f04);
|
const world = new THREE.Group();
|
||||||
|
scene.add( world );
|
||||||
|
const updateWorld = bindWorld( world );
|
||||||
|
|
||||||
|
|
||||||
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 );
|
||||||
@@ -18,7 +25,7 @@ camera.position.x = 10;
|
|||||||
camera.position.y = 4;
|
camera.position.y = 4;
|
||||||
|
|
||||||
|
|
||||||
const renderer = new THREE.WebGLRenderer( { antialias: true } );
|
const renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } );
|
||||||
renderer.setPixelRatio( Math.min( window.devicePixelRatio, 2 ) );
|
renderer.setPixelRatio( Math.min( window.devicePixelRatio, 2 ) );
|
||||||
renderer.setSize( window.innerWidth, window.innerHeight );
|
renderer.setSize( window.innerWidth, window.innerHeight );
|
||||||
renderer.shadowMap.enabled = true;
|
renderer.shadowMap.enabled = true;
|
||||||
@@ -42,13 +49,13 @@ controls.target.set( -4, 0, 0 );
|
|||||||
// const torus = new THREE.Mesh( geometry_torus, material_torus );
|
// const torus = new THREE.Mesh( geometry_torus, material_torus );
|
||||||
// torus.rotation.x = Math.PI / 2;
|
// torus.rotation.x = Math.PI / 2;
|
||||||
// torus.castShadow = true;
|
// torus.castShadow = true;
|
||||||
// scene.add( torus );
|
// world.add( torus );
|
||||||
|
|
||||||
// const icoGeometry = new THREE.IcosahedronGeometry( 5 );
|
// const icoGeometry = new THREE.IcosahedronGeometry( 5 );
|
||||||
// const icoMaterial= new THREE.MeshBasicMaterial( {color: 0x049ef4, wireframe: true, wireframeLinewidth: 8 } );
|
// const icoMaterial= new THREE.MeshBasicMaterial( {color: 0x049ef4, wireframe: true, wireframeLinewidth: 8 } );
|
||||||
// const iso = new THREE.Mesh( icoGeometry, icoMaterial );
|
// const iso = new THREE.Mesh( icoGeometry, icoMaterial );
|
||||||
// iso.castShadow = true;
|
// iso.castShadow = true;
|
||||||
// scene.add( iso );
|
// world.add( iso );
|
||||||
|
|
||||||
// --------- Mekaraba ---------- //
|
// --------- Mekaraba ---------- //
|
||||||
// const tetraGeometry = new THREE.TetrahedronGeometry( 2 );
|
// const tetraGeometry = new THREE.TetrahedronGeometry( 2 );
|
||||||
@@ -58,7 +65,7 @@ controls.target.set( -4, 0, 0 );
|
|||||||
|
|
||||||
// // tetra.rotation.x = Math.PI / 4 ;
|
// // tetra.rotation.x = Math.PI / 4 ;
|
||||||
// tetra.rotation.z = Math.PI / 6 ;
|
// tetra.rotation.z = Math.PI / 6 ;
|
||||||
// // scene.add( tetra );
|
// // world.add( tetra );
|
||||||
|
|
||||||
// const tetraEdges = new THREE.EdgesGeometry( tetraGeometry );
|
// const tetraEdges = new THREE.EdgesGeometry( tetraGeometry );
|
||||||
// const tetraEdgesLine = new THREE.LineSegments( tetraEdges, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 3 } ) );
|
// const tetraEdgesLine = new THREE.LineSegments( tetraEdges, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 3 } ) );
|
||||||
@@ -69,7 +76,7 @@ controls.target.set( -4, 0, 0 );
|
|||||||
// // tetraEdgesLine.rotation.y = Math.PI / 6;
|
// // tetraEdgesLine.rotation.y = Math.PI / 6;
|
||||||
|
|
||||||
// tetraEdgesLine.castShadow = true;
|
// tetraEdgesLine.castShadow = true;
|
||||||
// scene.add( tetraEdgesLine );
|
// world.add( tetraEdgesLine );
|
||||||
|
|
||||||
// const tetraGeometry2 = new THREE.TetrahedronGeometry( 2 );
|
// const tetraGeometry2 = new THREE.TetrahedronGeometry( 2 );
|
||||||
// const tetraMaterial2 = new THREE.MeshBasicMaterial( {color: 0xffff00 } );
|
// const tetraMaterial2 = new THREE.MeshBasicMaterial( {color: 0xffff00 } );
|
||||||
@@ -78,7 +85,7 @@ controls.target.set( -4, 0, 0 );
|
|||||||
// tetra2.rotation.x = Math.PI / 4 ;
|
// tetra2.rotation.x = Math.PI / 4 ;
|
||||||
// // tetra2.rotation.y = Math.PI / 4 ;
|
// // tetra2.rotation.y = Math.PI / 4 ;
|
||||||
// tetra2.rotation.z = Math.PI / 4 ;
|
// tetra2.rotation.z = Math.PI / 4 ;
|
||||||
// // scene.add( tetra2 );
|
// // world.add( tetra2 );
|
||||||
|
|
||||||
// const tetraEdges2 = new THREE.EdgesGeometry( tetraGeometry2 );
|
// const tetraEdges2 = new THREE.EdgesGeometry( tetraGeometry2 );
|
||||||
// const tetraEdgesLine2 = new THREE.LineSegments( tetraEdges2, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 3 } ) );
|
// const tetraEdgesLine2 = new THREE.LineSegments( tetraEdges2, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 3 } ) );
|
||||||
@@ -87,7 +94,7 @@ controls.target.set( -4, 0, 0 );
|
|||||||
// tetraEdgesLine2.rotation.x = Math.PI / 4 ;
|
// tetraEdgesLine2.rotation.x = Math.PI / 4 ;
|
||||||
// // tetraEdgesLine2.rotation.y = Math.PI / 4 ;
|
// // tetraEdgesLine2.rotation.y = Math.PI / 4 ;
|
||||||
// tetraEdgesLine2.rotation.z = Math.PI / 4 ;
|
// tetraEdgesLine2.rotation.z = Math.PI / 4 ;
|
||||||
// scene.add( tetraEdgesLine2 );
|
// world.add( tetraEdgesLine2 );
|
||||||
|
|
||||||
|
|
||||||
// ---------- David ------------ //
|
// ---------- David ------------ //
|
||||||
@@ -96,14 +103,14 @@ controls.target.set( -4, 0, 0 );
|
|||||||
// const tetra = new THREE.Mesh( tetraGeometry, tetraMaterial );
|
// const tetra = new THREE.Mesh( tetraGeometry, tetraMaterial );
|
||||||
// tetra.rotation.y = Math.PI / 6 ;
|
// tetra.rotation.y = Math.PI / 6 ;
|
||||||
// tetra.position.y = 1 / 2
|
// tetra.position.y = 1 / 2
|
||||||
// scene.add( tetra );
|
// world.add( tetra );
|
||||||
|
|
||||||
// const tetraEdges = new THREE.EdgesGeometry( tetraGeometry );
|
// const tetraEdges = new THREE.EdgesGeometry( tetraGeometry );
|
||||||
// const tetraEdgesLine = new THREE.LineSegments( tetraEdges, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 15 } ) );
|
// const tetraEdgesLine = new THREE.LineSegments( tetraEdges, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 15 } ) );
|
||||||
// tetraEdgesLine.rotation.y = Math.PI / 6 ;
|
// tetraEdgesLine.rotation.y = Math.PI / 6 ;
|
||||||
// tetraEdgesLine.position.y = 1 / 2
|
// tetraEdgesLine.position.y = 1 / 2
|
||||||
// tetraEdgesLine.castShadow = true;
|
// tetraEdgesLine.castShadow = true;
|
||||||
// scene.add( tetraEdgesLine );
|
// world.add( tetraEdgesLine );
|
||||||
|
|
||||||
// const tetraGeometry2 = new THREE.ConeGeometry( 2, 3, 3 );
|
// const tetraGeometry2 = new THREE.ConeGeometry( 2, 3, 3 );
|
||||||
// const tetraMaterial2 = new THREE.MeshBasicMaterial( {color: 0xffff00 } );
|
// const tetraMaterial2 = new THREE.MeshBasicMaterial( {color: 0xffff00 } );
|
||||||
@@ -111,7 +118,7 @@ controls.target.set( -4, 0, 0 );
|
|||||||
// tetra2.rotation.y = Math.PI + Math.PI / 6 ;
|
// tetra2.rotation.y = Math.PI + Math.PI / 6 ;
|
||||||
// tetra2.rotation.z = Math.PI;
|
// tetra2.rotation.z = Math.PI;
|
||||||
// tetra2.position.y = - 1 / 2
|
// tetra2.position.y = - 1 / 2
|
||||||
// scene.add( tetra2 );
|
// world.add( tetra2 );
|
||||||
|
|
||||||
// const tetraEdges2 = new THREE.EdgesGeometry( tetraGeometry2 );
|
// const tetraEdges2 = new THREE.EdgesGeometry( tetraGeometry2 );
|
||||||
// const tetraEdgesLine2 = new THREE.LineSegments( tetraEdges2, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 15 } ) );
|
// const tetraEdgesLine2 = new THREE.LineSegments( tetraEdges2, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 15 } ) );
|
||||||
@@ -119,7 +126,7 @@ controls.target.set( -4, 0, 0 );
|
|||||||
// tetraEdgesLine2.rotation.y = Math.PI + Math.PI / 6 ;
|
// tetraEdgesLine2.rotation.y = Math.PI + Math.PI / 6 ;
|
||||||
// tetraEdgesLine2.rotation.z = Math.PI;
|
// tetraEdgesLine2.rotation.z = Math.PI;
|
||||||
// tetraEdgesLine2.position.y = - 1 / 2
|
// tetraEdgesLine2.position.y = - 1 / 2
|
||||||
// scene.add( tetraEdgesLine2 );
|
// world.add( tetraEdgesLine2 );
|
||||||
|
|
||||||
// ---------- Flower of life ------------ //
|
// ---------- Flower of life ------------ //
|
||||||
|
|
||||||
@@ -127,11 +134,11 @@ controls.target.set( -4, 0, 0 );
|
|||||||
// const circleMaterial = new THREE.MeshBasicMaterial( { color: 0x049ef4, side: THREE.DoubleSide } );
|
// const circleMaterial = new THREE.MeshBasicMaterial( { color: 0x049ef4, side: THREE.DoubleSide } );
|
||||||
// const circle = new THREE.Mesh( circleGeometry, circleMaterial );
|
// const circle = new THREE.Mesh( circleGeometry, circleMaterial );
|
||||||
// circle.castShadow = true;
|
// circle.castShadow = true;
|
||||||
// scene.add( circle );
|
// world.add( circle );
|
||||||
|
|
||||||
// const circleLine = new THREE.LineSegments( circleGeometry, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 3 } ) );
|
// const circleLine = new THREE.LineSegments( circleGeometry, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 3 } ) );
|
||||||
// circleLine.castShadow = true;
|
// circleLine.castShadow = true;
|
||||||
// scene.add( circleLine );
|
// world.add( circleLine );
|
||||||
|
|
||||||
let rotatePoint2D = (cx, cy, angle, px, py) => {
|
let rotatePoint2D = (cx, cy, angle, px, py) => {
|
||||||
angle = - angle * (Math.PI / 180)
|
angle = - angle * (Math.PI / 180)
|
||||||
@@ -218,7 +225,7 @@ let renderFlower = (size = 3, radius = 1) => {
|
|||||||
|
|
||||||
let sphereGeometry = new THREE.SphereGeometry( radius, 64, 64 );
|
let sphereGeometry = new THREE.SphereGeometry( radius, 64, 64 );
|
||||||
let sphereMaterial = new THREE.MeshStandardMaterial( {
|
let sphereMaterial = new THREE.MeshStandardMaterial( {
|
||||||
color: 0x0063e4,
|
color: colors.fills.sphere,
|
||||||
metalness: 0.7,
|
metalness: 0.7,
|
||||||
roughness: 0.3,
|
roughness: 0.3,
|
||||||
opacity: 0.8,
|
opacity: 0.8,
|
||||||
@@ -228,11 +235,11 @@ let renderFlower = (size = 3, radius = 1) => {
|
|||||||
sphere.position.x = xtemp2;
|
sphere.position.x = xtemp2;
|
||||||
sphere.position.y = ytemp2;
|
sphere.position.y = ytemp2;
|
||||||
sphere.position.z = ztemp2;
|
sphere.position.z = ztemp2;
|
||||||
scene.add( sphere );
|
world.add( sphere );
|
||||||
|
|
||||||
let circleGeometry = new THREE.TorusGeometry( radius, 0.02, 64, 64 );
|
let circleGeometry = new THREE.TorusGeometry( radius, 0.02, 64, 64 );
|
||||||
let circleMaterial = new THREE.MeshBasicMaterial( {
|
let circleMaterial = new THREE.MeshBasicMaterial( {
|
||||||
color: 0x000000,
|
color: colors.line,
|
||||||
opacity: 0.1,
|
opacity: 0.1,
|
||||||
transparent: true
|
transparent: true
|
||||||
} );
|
} );
|
||||||
@@ -242,7 +249,7 @@ let renderFlower = (size = 3, radius = 1) => {
|
|||||||
circleCircum.position.x = xtemp2;
|
circleCircum.position.x = xtemp2;
|
||||||
circleCircum.position.y = ytemp2;
|
circleCircum.position.y = ytemp2;
|
||||||
circleCircum.position.z = ztemp2;
|
circleCircum.position.z = ztemp2;
|
||||||
scene.add( circleCircum );
|
world.add( circleCircum );
|
||||||
|
|
||||||
sphere.start_x = x;
|
sphere.start_x = x;
|
||||||
sphere.start_y = y;
|
sphere.start_y = y;
|
||||||
@@ -267,7 +274,7 @@ let renderFlower = (size = 3, radius = 1) => {
|
|||||||
circleCircum = new THREE.Mesh( circleGeometry, circleMaterial );
|
circleCircum = new THREE.Mesh( circleGeometry, circleMaterial );
|
||||||
circleCircum.rotation.y = Math.PI / 2;
|
circleCircum.rotation.y = Math.PI / 2;
|
||||||
circleCircum.castShadow = true;
|
circleCircum.castShadow = true;
|
||||||
scene.add( circleCircum );
|
world.add( circleCircum );
|
||||||
|
|
||||||
|
|
||||||
const gui = new GUI();
|
const gui = new GUI();
|
||||||
@@ -300,22 +307,22 @@ let renderFlower = (size = 3, radius = 1) => {
|
|||||||
});
|
});
|
||||||
gui.add( params, 'size', 2, 5).step(1).onChange( (value) => {
|
gui.add( params, 'size', 2, 5).step(1).onChange( (value) => {
|
||||||
for (let sphere of spheres) {
|
for (let sphere of spheres) {
|
||||||
scene.remove(sphere)
|
world.remove(sphere)
|
||||||
}
|
}
|
||||||
for (let sphereLine of spheresLines) {
|
for (let sphereLine of spheresLines) {
|
||||||
scene.remove(sphereLine)
|
world.remove(sphereLine)
|
||||||
}
|
}
|
||||||
scene.remove(circleCircum)
|
world.remove(circleCircum)
|
||||||
renderFlower (value, params['radius'])
|
renderFlower (value, params['radius'])
|
||||||
});
|
});
|
||||||
gui.add( params, 'radius', 0.5, 3).onChange( (value) => {
|
gui.add( params, 'radius', 0.5, 3).onChange( (value) => {
|
||||||
for (let sphere of spheres) {
|
for (let sphere of spheres) {
|
||||||
scene.remove(sphere)
|
world.remove(sphere)
|
||||||
}
|
}
|
||||||
for (let sphereLine of spheresLines) {
|
for (let sphereLine of spheresLines) {
|
||||||
scene.remove(sphereLine)
|
world.remove(sphereLine)
|
||||||
}
|
}
|
||||||
scene.remove(circleCircum)
|
world.remove(circleCircum)
|
||||||
renderFlower (params['size'], value)
|
renderFlower (params['size'], value)
|
||||||
});
|
});
|
||||||
gui.close();
|
gui.close();
|
||||||
@@ -323,8 +330,11 @@ let renderFlower = (size = 3, radius = 1) => {
|
|||||||
|
|
||||||
renderFlower()
|
renderFlower()
|
||||||
|
|
||||||
|
// 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 ) );
|
||||||
|
|
||||||
// legacy lighting was removed in three r155: intensities need the old value times pi
|
// legacy lighting was removed in three r155: intensities need the old value times pi
|
||||||
const spotLight = new THREE.DirectionalLight( 0xeaa7a7, 1.5 * Math.PI );
|
const spotLight = new THREE.DirectionalLight( colors.light, 1.5 * Math.PI );
|
||||||
spotLight.position.set( 10, 0, 0 );
|
spotLight.position.set( 10, 0, 0 );
|
||||||
spotLight.castShadow = true;
|
spotLight.castShadow = true;
|
||||||
spotLight.shadow.mapSize.width = 2048;
|
spotLight.shadow.mapSize.width = 2048;
|
||||||
@@ -338,11 +348,11 @@ spotLight.shadow.camera.bottom = -10;
|
|||||||
// spotLight.shadow.camera.fov = 35;
|
// spotLight.shadow.camera.fov = 35;
|
||||||
scene.add( spotLight );
|
scene.add( spotLight );
|
||||||
|
|
||||||
//Create a plane that receives shadows (but does not cast them)
|
// the wall itself is invisible: only the shadow "projection" shows, in light
|
||||||
|
// orange, floating on the gradient
|
||||||
const planeGeometry = new THREE.PlaneGeometry( 20, 20, 10, 10 );
|
const planeGeometry = new THREE.PlaneGeometry( 20, 20, 10, 10 );
|
||||||
const planeMaterial = new THREE.MeshStandardMaterial( { color: 0xffffff } )
|
const planeMaterial = new THREE.ShadowMaterial( { color: colors.shadow, opacity: 0.9 } )
|
||||||
const plane = new THREE.Mesh( planeGeometry, planeMaterial );
|
const plane = new THREE.Mesh( planeGeometry, planeMaterial );
|
||||||
planeMaterial.side = THREE.DoubleSide;
|
|
||||||
plane.rotation.y = Math.PI / 2;
|
plane.rotation.y = Math.PI / 2;
|
||||||
plane.position.x = -10
|
plane.position.x = -10
|
||||||
plane.receiveShadow = true;
|
plane.receiveShadow = true;
|
||||||
@@ -350,16 +360,14 @@ scene.add( plane );
|
|||||||
|
|
||||||
//Create a helper for the shadow camera (optional)
|
//Create a helper for the shadow camera (optional)
|
||||||
const helper = new THREE.CameraHelper( spotLight.shadow.camera );
|
const helper = new THREE.CameraHelper( spotLight.shadow.camera );
|
||||||
// scene.add( helper );
|
// world.add( helper );
|
||||||
|
|
||||||
const axesHelper = new THREE.AxesHelper( 40 );
|
const axesHelper = new THREE.AxesHelper( 40 );
|
||||||
scene.add( axesHelper );
|
// scene.add( axesHelper );
|
||||||
|
|
||||||
// const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 0.2 );
|
// const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 0.2 );
|
||||||
// scene.add( light );
|
// world.add( light );
|
||||||
|
|
||||||
let stats = new Stats();
|
|
||||||
document.body.appendChild( stats.dom );
|
|
||||||
|
|
||||||
// var axis = new THREE.Vector3(0, 1, 0);
|
// var axis = new THREE.Vector3(0, 1, 0);
|
||||||
|
|
||||||
@@ -378,8 +386,8 @@ function animate () {
|
|||||||
// sphereLine.position.z = - Math.sin( time ) * sphereLine.start_z;
|
// sphereLine.position.z = - Math.sin( time ) * sphereLine.start_z;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
updateWorld();
|
||||||
controls.update();
|
controls.update();
|
||||||
stats.update();
|
|
||||||
|
|
||||||
renderer.render( scene, camera );
|
renderer.render( scene, camera );
|
||||||
};
|
};
|
||||||
|
|||||||
+46
-1
@@ -4,7 +4,52 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Thrive</title>
|
<title>Thrive</title>
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="./css/styles.scss">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="landing">
|
||||||
|
<div class="fade"></div>
|
||||||
|
<main class="landing-content">
|
||||||
|
<header>
|
||||||
|
<h1>Thrive</h1>
|
||||||
|
<p>Sacred geometry, lifted into three dimensions.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="cards">
|
||||||
|
<a class="card" href="metatron.html">
|
||||||
|
<h2>Metatron’s Cube</h2>
|
||||||
|
<p>The five Platonic solids — fire, earth, air, water and aether — nested in the 3D Fruit of Life, every vertex on a circle centre.</p>
|
||||||
|
</a>
|
||||||
|
<a class="card" href="torus.html">
|
||||||
|
<h2>Toroidal Field</h2>
|
||||||
|
<p>The self-sustaining field: strands winding around the torus, through the centre and back around the outside.</p>
|
||||||
|
</a>
|
||||||
|
<a class="card" href="matrix.html">
|
||||||
|
<h2>Vector Equilibrium</h2>
|
||||||
|
<p>The cuboctahedron, where centre and neighbour are the same distance apart — 12 spheres kissing a 13th.</p>
|
||||||
|
</a>
|
||||||
|
<a class="card" href="phyllotaxis.html">
|
||||||
|
<h2>Golden Spiral</h2>
|
||||||
|
<p>Phyllotaxis: seeds placed one golden angle apart become a sunflower, a pinecone, a perfectly even sphere.</p>
|
||||||
|
</a>
|
||||||
|
<a class="card" href="flower.html">
|
||||||
|
<h2>Flower of Life</h2>
|
||||||
|
<p>The flower unfolded into a cubic grid of interlocking spheres.</p>
|
||||||
|
</a>
|
||||||
|
<a class="card" href="seed.html">
|
||||||
|
<h2>Seed of Life</h2>
|
||||||
|
<p>The seven circles of the seed, rotated through space into a sphere of spheres.</p>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="soon">
|
||||||
|
<h3>Still unfolding</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Sri Yantra — the nine interlocking triangles raised into Mount Meru</li>
|
||||||
|
<li>Tesseract — the cube’s shadow from the fourth dimension</li>
|
||||||
|
<li>Tree of Life — ten sephirot and twenty-two paths in space</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<script type="module" src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+29
-730
@@ -1,31 +1,19 @@
|
|||||||
|
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
|
|
||||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
import './lib/chrome.js';
|
||||||
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
|
import { bindWorld } from './lib/transit.js';
|
||||||
|
|
||||||
import Stats from 'three/addons/libs/stats.module.js';
|
|
||||||
|
|
||||||
import './css/styles.scss';
|
|
||||||
|
|
||||||
|
// Landing page backdrop: a slowly turning seed of life and icosahedron drawn in
|
||||||
|
// faint orange lines behind the cards. No controls, no stats - just ambience.
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
scene.background = new THREE.Color(0xf44f04);
|
|
||||||
|
|
||||||
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, 100 );
|
||||||
camera.position.z = 12;
|
camera.position.z = 14;
|
||||||
camera.position.x = 10;
|
|
||||||
camera.position.y = 4;
|
|
||||||
|
|
||||||
// debug switches to bisect driver issues: ?noaa disables MSAA, ?noshadow disables shadow maps
|
const renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } );
|
||||||
const debugFlags = new URLSearchParams( window.location.search );
|
|
||||||
|
|
||||||
const renderer = new THREE.WebGLRenderer( { antialias: !debugFlags.has( 'noaa' ) } );
|
|
||||||
renderer.setPixelRatio( Math.min( window.devicePixelRatio, 2 ) );
|
renderer.setPixelRatio( Math.min( window.devicePixelRatio, 2 ) );
|
||||||
renderer.setSize( window.innerWidth, window.innerHeight );
|
renderer.setSize( window.innerWidth, window.innerHeight );
|
||||||
renderer.shadowMap.enabled = !debugFlags.has( 'noshadow' );
|
renderer.domElement.classList.add( 'landing-bg' );
|
||||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
||||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
||||||
renderer.toneMappingExposure = 1;
|
|
||||||
document.body.appendChild( renderer.domElement );
|
document.body.appendChild( renderer.domElement );
|
||||||
|
|
||||||
window.addEventListener( 'resize', () => {
|
window.addEventListener( 'resize', () => {
|
||||||
@@ -34,729 +22,40 @@ window.addEventListener( 'resize', () => {
|
|||||||
renderer.setSize( window.innerWidth, window.innerHeight );
|
renderer.setSize( window.innerWidth, window.innerHeight );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const controls = new OrbitControls( camera, renderer.domElement );
|
|
||||||
controls.target.set( -4, 0, 0 );
|
|
||||||
|
|
||||||
|
|
||||||
// const geometry_torus = new THREE.TorusGeometry( 2.5, 2.5, 32, 128 );
|
|
||||||
// const material_torus = new THREE.MeshBasicMaterial( { color: 0x049ef4, wireframe: true, wireframeLinewidth: 1} );
|
|
||||||
// const torus = new THREE.Mesh( geometry_torus, material_torus );
|
|
||||||
// torus.rotation.x = Math.PI / 2;
|
|
||||||
// torus.castShadow = true;
|
|
||||||
// scene.add( torus );
|
|
||||||
|
|
||||||
// const icoGeometry = new THREE.IcosahedronGeometry( 5 );
|
|
||||||
// const icoMaterial= new THREE.MeshBasicMaterial( {color: 0x049ef4, wireframe: true, wireframeLinewidth: 8 } );
|
|
||||||
// const iso = new THREE.Mesh( icoGeometry, icoMaterial );
|
|
||||||
// iso.castShadow = true;
|
|
||||||
// scene.add( iso );
|
|
||||||
|
|
||||||
// --------- Mekaraba ---------- //
|
|
||||||
// const tetraGeometry = new THREE.TetrahedronGeometry( 2 );
|
|
||||||
// const tetraMaterial = new THREE.MeshBasicMaterial( {color: 0xffff00 } );
|
|
||||||
// const tetra = new THREE.Mesh( tetraGeometry, tetraMaterial );
|
|
||||||
// // tetra.position.y = - 0.81
|
|
||||||
|
|
||||||
// // tetra.rotation.x = Math.PI / 4 ;
|
|
||||||
// tetra.rotation.z = Math.PI / 6 ;
|
|
||||||
// // scene.add( tetra );
|
|
||||||
|
|
||||||
// const tetraEdges = new THREE.EdgesGeometry( tetraGeometry );
|
|
||||||
// const tetraEdgesLine = new THREE.LineSegments( tetraEdges, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 3 } ) );
|
|
||||||
// // tetraEdgesLine.position.y = -0.81
|
|
||||||
|
|
||||||
// tetraEdgesLine.rotation.x = Math.PI / 4;
|
|
||||||
// // tetraEdgesLine.rotation.y = Math.PI ;
|
|
||||||
// // tetraEdgesLine.rotation.y = Math.PI / 6;
|
|
||||||
|
|
||||||
// tetraEdgesLine.castShadow = true;
|
|
||||||
// scene.add( tetraEdgesLine );
|
|
||||||
|
|
||||||
// const tetraGeometry2 = new THREE.TetrahedronGeometry( 2 );
|
|
||||||
// const tetraMaterial2 = new THREE.MeshBasicMaterial( {color: 0xffff00 } );
|
|
||||||
// const tetra2 = new THREE.Mesh( tetraGeometry2, tetraMaterial2 );
|
|
||||||
// tetra2.position.y = 0.81
|
|
||||||
// tetra2.rotation.x = Math.PI / 4 ;
|
|
||||||
// // tetra2.rotation.y = Math.PI / 4 ;
|
|
||||||
// tetra2.rotation.z = Math.PI / 4 ;
|
|
||||||
// // scene.add( tetra2 );
|
|
||||||
|
|
||||||
// const tetraEdges2 = new THREE.EdgesGeometry( tetraGeometry2 );
|
|
||||||
// const tetraEdgesLine2 = new THREE.LineSegments( tetraEdges2, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 3 } ) );
|
|
||||||
// tetraEdgesLine2.castShadow = true;
|
|
||||||
// tetraEdgesLine2.position.y = 0.81
|
|
||||||
// tetraEdgesLine2.rotation.x = Math.PI / 4 ;
|
|
||||||
// // tetraEdgesLine2.rotation.y = Math.PI / 4 ;
|
|
||||||
// tetraEdgesLine2.rotation.z = Math.PI / 4 ;
|
|
||||||
// scene.add( tetraEdgesLine2 );
|
|
||||||
|
|
||||||
|
|
||||||
// ---------- David ------------ //
|
|
||||||
// const tetraGeometry = new THREE.ConeGeometry( 2, 3, 3 );
|
|
||||||
// const tetraMaterial = new THREE.MeshBasicMaterial( {color: 0xffff00 } );
|
|
||||||
// const tetra = new THREE.Mesh( tetraGeometry, tetraMaterial );
|
|
||||||
// tetra.rotation.y = Math.PI / 6 ;
|
|
||||||
// tetra.position.y = 1 / 2
|
|
||||||
// scene.add( tetra );
|
|
||||||
|
|
||||||
// const tetraEdges = new THREE.EdgesGeometry( tetraGeometry );
|
|
||||||
// const tetraEdgesLine = new THREE.LineSegments( tetraEdges, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 15 } ) );
|
|
||||||
// tetraEdgesLine.rotation.y = Math.PI / 6 ;
|
|
||||||
// tetraEdgesLine.position.y = 1 / 2
|
|
||||||
// tetraEdgesLine.castShadow = true;
|
|
||||||
// scene.add( tetraEdgesLine );
|
|
||||||
|
|
||||||
// const tetraGeometry2 = new THREE.ConeGeometry( 2, 3, 3 );
|
|
||||||
// const tetraMaterial2 = new THREE.MeshBasicMaterial( {color: 0xffff00 } );
|
|
||||||
// const tetra2 = new THREE.Mesh( tetraGeometry2, tetraMaterial2 );
|
|
||||||
// tetra2.rotation.y = Math.PI + Math.PI / 6 ;
|
|
||||||
// tetra2.rotation.z = Math.PI;
|
|
||||||
// tetra2.position.y = - 1 / 2
|
|
||||||
// scene.add( tetra2 );
|
|
||||||
|
|
||||||
// const tetraEdges2 = new THREE.EdgesGeometry( tetraGeometry2 );
|
|
||||||
// const tetraEdgesLine2 = new THREE.LineSegments( tetraEdges2, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 15 } ) );
|
|
||||||
// tetraEdgesLine2.castShadow = true;
|
|
||||||
// tetraEdgesLine2.rotation.y = Math.PI + Math.PI / 6 ;
|
|
||||||
// tetraEdgesLine2.rotation.z = Math.PI;
|
|
||||||
// tetraEdgesLine2.position.y = - 1 / 2
|
|
||||||
// scene.add( tetraEdgesLine2 );
|
|
||||||
|
|
||||||
// ---------- Flower of life ------------ //
|
|
||||||
|
|
||||||
|
|
||||||
let rotatePoint2D = (cx, cy, angle, px, py) => {
|
|
||||||
angle = - angle * (Math.PI / 180)
|
|
||||||
let s = Math.sin(angle);
|
|
||||||
let c = Math.cos(angle);
|
|
||||||
|
|
||||||
// translate point back to origin:
|
|
||||||
px -= cx;
|
|
||||||
py -= cy;
|
|
||||||
|
|
||||||
// rotatePoint3D point
|
|
||||||
let xnew = px * c + py * s;
|
|
||||||
let ynew = px * s - py * c;
|
|
||||||
|
|
||||||
// translate point back:
|
|
||||||
px = xnew + cx;
|
|
||||||
py = ynew + cy;
|
|
||||||
return [px, py];
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotatePoint3D(px, py, pz, pitch, roll, yaw) {
|
|
||||||
pitch = - pitch * (Math.PI / 180)
|
|
||||||
roll = - roll * (Math.PI / 180)
|
|
||||||
yaw = - yaw * (Math.PI / 180)
|
|
||||||
|
|
||||||
var cosa = Math.cos(yaw);
|
|
||||||
var sina = Math.sin(yaw);
|
|
||||||
|
|
||||||
var cosb = Math.cos(pitch);
|
|
||||||
var sinb = Math.sin(pitch);
|
|
||||||
|
|
||||||
var cosc = Math.cos(roll);
|
|
||||||
var sinc = Math.sin(roll);
|
|
||||||
|
|
||||||
var Axx = cosa*cosb;
|
|
||||||
var Axy = cosa*sinb*sinc - sina*cosc;
|
|
||||||
var Axz = cosa*sinb*cosc + sina*sinc;
|
|
||||||
|
|
||||||
var Ayx = sina*cosb;
|
|
||||||
var Ayy = sina*sinb*sinc + cosa*cosc;
|
|
||||||
var Ayz = sina*sinb*cosc - cosa*sinc;
|
|
||||||
|
|
||||||
var Azx = -sinb;
|
|
||||||
var Azy = cosb*sinc;
|
|
||||||
var Azz = cosb*cosc;
|
|
||||||
|
|
||||||
let x = Axx*px + Axy*py + Axz*pz;
|
|
||||||
let y = Ayx*px + Ayy*py + Ayz*pz;
|
|
||||||
let z = Azx*px + Azy*py + Azz*pz;
|
|
||||||
|
|
||||||
return [x, y, z]
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebGL draws lines at 1px regardless of linewidth, so the shape edges are built
|
|
||||||
// from real cylinder tubes instead - same 0.03 thickness as the circle tori
|
|
||||||
function makeEdgeTubes( geometry, color, tubeRadius = 0.03 ) {
|
|
||||||
const edges = new THREE.EdgesGeometry( geometry );
|
|
||||||
const positions = edges.attributes.position;
|
|
||||||
const group = new THREE.Group();
|
const group = new THREE.Group();
|
||||||
const material = new THREE.MeshBasicMaterial( { color } );
|
scene.add( group );
|
||||||
const up = new THREE.Vector3( 0, 1, 0 );
|
const updateWorld = bindWorld( group );
|
||||||
const start = new THREE.Vector3();
|
|
||||||
const end = new THREE.Vector3();
|
|
||||||
|
|
||||||
for ( let i = 0; i < positions.count; i += 2 ) {
|
const ringMaterial = new THREE.MeshBasicMaterial( { color: 0xf44f04, opacity: 0.35, transparent: true } );
|
||||||
start.fromBufferAttribute( positions, i );
|
const ringGeometry = new THREE.TorusGeometry( 2, 0.015, 16, 128 );
|
||||||
end.fromBufferAttribute( positions, i + 1 );
|
|
||||||
const direction = new THREE.Vector3().subVectors( end, start );
|
|
||||||
const length = direction.length();
|
|
||||||
|
|
||||||
const cylinder = new THREE.CylinderGeometry( tubeRadius, tubeRadius, length, 12 );
|
// seed of life: one central ring plus six on its circumference
|
||||||
cylinder.translate( 0, length / 2, 0 );
|
const centreRing = new THREE.Mesh( ringGeometry, ringMaterial );
|
||||||
|
group.add( centreRing );
|
||||||
|
|
||||||
const tube = new THREE.Mesh( cylinder, material );
|
for ( let i = 0; i < 6; i++ ) {
|
||||||
tube.position.copy( start );
|
const angle = ( i / 6 ) * Math.PI * 2;
|
||||||
tube.quaternion.setFromUnitVectors( up, direction.normalize() );
|
const ring = new THREE.Mesh( ringGeometry, ringMaterial );
|
||||||
tube.castShadow = true;
|
ring.position.set( 2 * Math.cos( angle ), 2 * Math.sin( angle ), 0 );
|
||||||
group.add( tube );
|
group.add( ring );
|
||||||
}
|
}
|
||||||
|
|
||||||
return group;
|
const icosa = new THREE.LineSegments(
|
||||||
}
|
new THREE.EdgesGeometry( new THREE.IcosahedronGeometry( 5.2 ) ),
|
||||||
|
new THREE.LineBasicMaterial( { color: 0xf44f04, opacity: 0.25, transparent: true } )
|
||||||
const params = {
|
|
||||||
xangle: 45,
|
|
||||||
yangle: 0,
|
|
||||||
zangle: 54.72972973,
|
|
||||||
radius: 1,
|
|
||||||
tube: 0.03,
|
|
||||||
};
|
|
||||||
|
|
||||||
// const params = {
|
|
||||||
// xangle: 0,
|
|
||||||
// yangle: 0,
|
|
||||||
// zangle: 0,
|
|
||||||
// size: 5,
|
|
||||||
// radius: 1
|
|
||||||
// };
|
|
||||||
|
|
||||||
let sphere, circleCircum, cube, cubeLines;
|
|
||||||
let spheres = [];
|
|
||||||
let spheresLines = [];
|
|
||||||
let gui;
|
|
||||||
let solids = [];
|
|
||||||
|
|
||||||
let renderFlower = (radius = 1) => {
|
|
||||||
spheres = [];
|
|
||||||
spheresLines = [];
|
|
||||||
|
|
||||||
// tear down the previous render's shapes and GUI so re-rendering never stacks duplicates
|
|
||||||
if ( gui ) gui.destroy();
|
|
||||||
for ( let obj of solids ) scene.remove( obj );
|
|
||||||
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 points = [ [ 0, 0, 0 ] ];
|
|
||||||
|
|
||||||
for ( let sx of [ -1, 1 ] ) {
|
|
||||||
for ( let sy of [ -1, 1 ] ) {
|
|
||||||
for ( let sz of [ -1, 1 ] ) {
|
|
||||||
points.push( [ sx * d, sy * d, sz * d ] );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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: 0x0063e4,
|
|
||||||
metalness: 0.7,
|
|
||||||
roughness: 0.3,
|
|
||||||
opacity: 0.8,
|
|
||||||
transparent: true
|
|
||||||
} );
|
|
||||||
sphere = new THREE.Mesh( sphereGeometry, sphereMaterial );
|
|
||||||
sphere.position.x = xtemp;
|
|
||||||
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: 0x000000,
|
|
||||||
opacity: 0.1,
|
|
||||||
transparent: true
|
|
||||||
} );
|
|
||||||
let ring = new THREE.Mesh( circleGeometry, circleMaterial );
|
|
||||||
ring.castShadow = true;
|
|
||||||
ring.rotation.y = Math.PI / 2;
|
|
||||||
ring.position.x = xtemp;
|
|
||||||
ring.position.y = ytemp;
|
|
||||||
ring.position.z = ztemp;
|
|
||||||
parent.add( ring );
|
|
||||||
|
|
||||||
circleGeometry = new THREE.TorusGeometry( 0.01, 0.01, 64, 64 );
|
|
||||||
circleMaterial = new THREE.MeshBasicMaterial( {
|
|
||||||
color: 0x000000,
|
|
||||||
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 ) {
|
|
||||||
addCircleAt( ...point, scene );
|
|
||||||
}
|
|
||||||
|
|
||||||
// the remaining cube circles: the six equatorial edge midpoints and the six
|
|
||||||
// face centres, hidden behind the "allCubeCircles" toggle
|
|
||||||
const extraCircles = new THREE.Group();
|
|
||||||
extraCircles.visible = false;
|
|
||||||
|
|
||||||
for ( let s of [ -1, 1 ] ) {
|
|
||||||
for ( let extraPoint of [
|
|
||||||
[ s * d, -s * d, 0 ], [ s * d, 0, -s * d ], [ 0, s * d, -s * d ],
|
|
||||||
[ s * d, 0, 0 ], [ 0, s * d, 0 ], [ 0, 0, s * d ],
|
|
||||||
] ) {
|
|
||||||
addCircleAt( ...extraPoint, extraCircles );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scene.add( extraCircles );
|
|
||||||
|
|
||||||
// Metatron's web: every circle centre connected to every other one
|
|
||||||
let web = new THREE.Group();
|
|
||||||
const webMaterial = new THREE.MeshBasicMaterial( { color: 0x000000, 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;
|
|
||||||
scene.add( web );
|
|
||||||
|
|
||||||
// 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
|
|
||||||
const gridRotation = new THREE.Matrix4().makeBasis(
|
|
||||||
new THREE.Vector3( ...rotatePoint3D( 1, 0, 0, params['yangle'], params['xangle'], params['zangle'] ) ),
|
|
||||||
new THREE.Vector3( ...rotatePoint3D( 0, 1, 0, params['yangle'], params['xangle'], params['zangle'] ) ),
|
|
||||||
new THREE.Vector3( ...rotatePoint3D( 0, 0, 1, params['yangle'], params['xangle'], params['zangle'] ) )
|
|
||||||
);
|
);
|
||||||
|
group.add( icosa );
|
||||||
|
|
||||||
// earth - width 2d puts every corner exactly on a corner dot
|
|
||||||
const boxGeometry = new THREE.BoxGeometry( 2 * d, 2 * d, 2 * d );
|
|
||||||
const boxMaterial = new THREE.MeshStandardMaterial( {
|
|
||||||
color: 0x994f28,
|
|
||||||
metalness: 0.7,
|
|
||||||
roughness: 0.3,
|
|
||||||
opacity: 0.8,
|
|
||||||
transparent: true,
|
|
||||||
// wireframe: true,
|
|
||||||
} );
|
|
||||||
cube = new THREE.Mesh( boxGeometry, boxMaterial );
|
|
||||||
cube.setRotationFromMatrix( gridRotation );
|
|
||||||
|
|
||||||
cubeLines = makeEdgeTubes( boxGeometry, 0x1b0a02, params['tube'] );
|
|
||||||
cubeLines.setRotationFromMatrix( gridRotation );
|
|
||||||
cubeLines.castShadow = true;
|
|
||||||
|
|
||||||
scene.add( cubeLines );
|
|
||||||
scene.add( cube );
|
|
||||||
|
|
||||||
// fire - radius sqrt(3)d puts every vertex exactly on a corner dot
|
|
||||||
const tetraGeometry = new THREE.TetrahedronGeometry( Math.sqrt( 3 ) * d )
|
|
||||||
const tetraMaterial = new THREE.MeshStandardMaterial( {
|
|
||||||
color: 0xdb1212,
|
|
||||||
metalness: 0.7,
|
|
||||||
roughness: 0.3,
|
|
||||||
opacity: 0.8,
|
|
||||||
transparent: true
|
|
||||||
} );
|
|
||||||
let tetra = new THREE.Mesh( tetraGeometry, tetraMaterial );
|
|
||||||
|
|
||||||
let tetraLines = makeEdgeTubes( tetraGeometry, 0x140101, params['tube'] );
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
tetraLines.setRotationFromMatrix( gridRotation );
|
|
||||||
tetra.setRotationFromMatrix( gridRotation );
|
|
||||||
tetraLines.castShadow = true;
|
|
||||||
|
|
||||||
scene.add( tetraLines );
|
|
||||||
scene.add( tetra );
|
|
||||||
|
|
||||||
// second tetrahedron of the star tetrahedron (Merkaba): the same shape
|
|
||||||
// rotated a quarter turn so it interlocks with the first
|
|
||||||
const tetraGeometry2 = new THREE.TetrahedronGeometry( Math.sqrt( 3 ) * d );
|
|
||||||
tetraGeometry2.rotateZ( Math.PI / 2 );
|
|
||||||
let tetra2 = new THREE.Mesh( tetraGeometry2, tetraMaterial );
|
|
||||||
let tetraLines2 = makeEdgeTubes( tetraGeometry2, 0x140101, params['tube'] );
|
|
||||||
|
|
||||||
tetra2.rotation.copy( tetra.rotation );
|
|
||||||
tetraLines2.rotation.copy( tetraLines.rotation );
|
|
||||||
|
|
||||||
scene.add( tetraLines2 );
|
|
||||||
scene.add( tetra2 );
|
|
||||||
|
|
||||||
// radius 2d puts the projected corners exactly on the outer circle centres
|
|
||||||
const octaGeometry = new THREE.OctahedronGeometry( 2 * d )
|
|
||||||
const octaMaterial = new THREE.MeshStandardMaterial( {
|
|
||||||
color: 0x9dedf6,
|
|
||||||
metalness: 0.7,
|
|
||||||
roughness: 0.3,
|
|
||||||
opacity: 0.8,
|
|
||||||
transparent: true
|
|
||||||
} );
|
|
||||||
let octa = new THREE.Mesh( octaGeometry, octaMaterial );
|
|
||||||
|
|
||||||
let octaLines = makeEdgeTubes( octaGeometry, 0x0a2528, params['tube'] );
|
|
||||||
|
|
||||||
octaLines.setRotationFromMatrix( gridRotation );
|
|
||||||
octa.setRotationFromMatrix( gridRotation );
|
|
||||||
octaLines.castShadow = true;
|
|
||||||
|
|
||||||
scene.add( octaLines );
|
|
||||||
scene.add( octa );
|
|
||||||
|
|
||||||
// radius 1.663d puts the projected corners on the outer circle centres
|
|
||||||
const icosaGeometry = new THREE.IcosahedronGeometry( 1.663 * d )
|
|
||||||
const icosaMaterial = new THREE.MeshStandardMaterial( {
|
|
||||||
color: 0x0063e4,
|
|
||||||
metalness: 0.7,
|
|
||||||
roughness: 0.3,
|
|
||||||
opacity: 0.8,
|
|
||||||
transparent: true
|
|
||||||
} );
|
|
||||||
let icosa = new THREE.Mesh( icosaGeometry, icosaMaterial );
|
|
||||||
|
|
||||||
let icosaLines = makeEdgeTubes( icosaGeometry, 0x000000, params['tube'] );
|
|
||||||
|
|
||||||
// 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
|
|
||||||
const PHI = ( 1 + Math.sqrt( 5 ) ) / 2;
|
|
||||||
const icosaTwist = Math.PI / 6 - Math.atan( ( PHI + 2 ) / ( Math.sqrt( 3 ) * PHI ) );
|
|
||||||
const icosaRotation = new THREE.Matrix4().multiplyMatrices(
|
|
||||||
gridRotation,
|
|
||||||
new THREE.Matrix4().makeRotationAxis( new THREE.Vector3( 1, 1, 1 ).normalize(), icosaTwist )
|
|
||||||
);
|
|
||||||
|
|
||||||
icosaLines.setRotationFromMatrix( icosaRotation );
|
|
||||||
icosa.setRotationFromMatrix( icosaRotation );
|
|
||||||
icosaLines.castShadow = true;
|
|
||||||
|
|
||||||
scene.add( icosaLines );
|
|
||||||
scene.add( icosa );
|
|
||||||
|
|
||||||
// 2.29 * 1.003: tuned against the circle pattern
|
|
||||||
const dodecaGeometry = new THREE.DodecahedronGeometry( 2.297 * radius )
|
|
||||||
const dodecaMaterial = new THREE.MeshStandardMaterial( {
|
|
||||||
color: 0x0063e4,
|
|
||||||
metalness: 0.7,
|
|
||||||
roughness: 0.3,
|
|
||||||
opacity: 0.8,
|
|
||||||
transparent: true
|
|
||||||
} );
|
|
||||||
let dodeca = new THREE.Mesh( dodecaGeometry, dodecaMaterial );
|
|
||||||
|
|
||||||
let dodecaLines = makeEdgeTubes( dodecaGeometry, 0x000000, params['tube'] );
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
// with a tuned x-angle on top
|
|
||||||
dodecaLines.setRotationFromMatrix( gridRotation );
|
|
||||||
dodeca.setRotationFromMatrix( gridRotation );
|
|
||||||
dodecaLines.rotation.x = 52.25 * (Math.PI / 180);
|
|
||||||
dodeca.rotation.x = 52.25 * (Math.PI / 180);
|
|
||||||
dodecaLines.castShadow = true;
|
|
||||||
|
|
||||||
scene.add( dodecaLines );
|
|
||||||
scene.add( dodeca );
|
|
||||||
|
|
||||||
|
|
||||||
// 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 circleMaterial = new THREE.MeshBasicMaterial( { color: 0x000000 } );
|
|
||||||
circleCircum = new THREE.Mesh( circleGeometry, circleMaterial );
|
|
||||||
circleCircum.rotation.y = Math.PI / 2;
|
|
||||||
circleCircum.castShadow = true;
|
|
||||||
scene.add( circleCircum );
|
|
||||||
|
|
||||||
solids.push( cube, cubeLines, tetra, tetraLines, tetra2, tetraLines2, octa, octaLines, icosa, icosaLines, dodeca, dodecaLines, circleCircum, web, extraCircles );
|
|
||||||
|
|
||||||
|
|
||||||
gui = new GUI();
|
|
||||||
|
|
||||||
let guiDict = {};
|
|
||||||
|
|
||||||
let fire = gui.addFolder('Fire');
|
|
||||||
guiDict['fire'] = {'folder': fire, 'object': tetra, 'objectLines': tetraLines};
|
|
||||||
|
|
||||||
let fire2 = gui.addFolder('Fire 2');
|
|
||||||
guiDict['fire2'] = {'folder': fire2, 'object': tetra2, 'objectLines': tetraLines2};
|
|
||||||
|
|
||||||
let air = gui.addFolder('Air');
|
|
||||||
guiDict['air'] = {'folder': air, 'object': octa, 'objectLines': octaLines};
|
|
||||||
|
|
||||||
let earth = gui.addFolder('Earth');
|
|
||||||
guiDict['earth'] = {'folder': earth, 'object': cube, 'objectLines': cubeLines};
|
|
||||||
|
|
||||||
let water = gui.addFolder('Water');
|
|
||||||
guiDict['water'] = {'folder': water, 'object': icosa, 'objectLines': icosaLines};
|
|
||||||
|
|
||||||
let aether = gui.addFolder('Aether')
|
|
||||||
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 = {
|
|
||||||
show: true,
|
|
||||||
fill: true,
|
|
||||||
frame: true,
|
|
||||||
xAngle: 45,
|
|
||||||
yAngle: 0,
|
|
||||||
zAngle: 54.72972973,
|
|
||||||
size: 5,
|
|
||||||
radius: 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)) {
|
|
||||||
// guiDict[fld]['folder'].close()
|
|
||||||
if (fld === "circles") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
folderStandard.show = true;
|
|
||||||
|
|
||||||
folderStandard.xAngle = guiDict[fld]['object'].rotation.x / (Math.PI / 180);
|
|
||||||
folderStandard.yAngle = guiDict[fld]['object'].rotation.y / (Math.PI / 180);
|
|
||||||
folderStandard.zAngle = guiDict[fld]['object'].rotation.z / (Math.PI / 180);
|
|
||||||
|
|
||||||
guiDict[fld]['folder'].add( folderStandard, 'scale', 0, 2 ).step(0.001).onChange( (value) => {
|
|
||||||
guiDict[fld]['object'].scale.x = value
|
|
||||||
guiDict[fld]['object'].scale.y = value
|
|
||||||
guiDict[fld]['object'].scale.z = value
|
|
||||||
guiDict[fld]['objectLines'].scale.x = value
|
|
||||||
guiDict[fld]['objectLines'].scale.y = value
|
|
||||||
guiDict[fld]['objectLines'].scale.z = value
|
|
||||||
});
|
|
||||||
|
|
||||||
guiDict[fld]['folder'].add( folderStandard, 'show', true).onChange((value) => {
|
|
||||||
if (value) {
|
|
||||||
guiDict[fld]['object'].visible = true;
|
|
||||||
guiDict[fld]['objectLines'].visible = true;
|
|
||||||
} else {
|
|
||||||
guiDict[fld]['object'].visible = false;
|
|
||||||
guiDict[fld]['objectLines'].visible = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
guiDict[fld]['folder'].add( folderStandard, 'fill', true).onChange((value) => {
|
|
||||||
if (value) {
|
|
||||||
guiDict[fld]['object'].visible = true;
|
|
||||||
} else {
|
|
||||||
guiDict[fld]['object'].visible = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
guiDict[fld]['folder'].add( folderStandard, 'frame', true).onChange((value) => {
|
|
||||||
if (value) {
|
|
||||||
guiDict[fld]['objectLines'].visible = true;
|
|
||||||
} else {
|
|
||||||
guiDict[fld]['objectLines'].visible = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
guiDict[fld]['folder'].add( folderStandard, 'xAngle', 0, 360 ).step(0.1).onChange( (value) => {
|
|
||||||
guiDict[fld]['object'].rotation.x = value * (Math.PI / 180)
|
|
||||||
guiDict[fld]['objectLines'].rotation.x = value * (Math.PI / 180)
|
|
||||||
});
|
|
||||||
|
|
||||||
guiDict[fld]['folder'].add( folderStandard, 'yAngle', 0, 360 ).step(0.1).onChange( (value) => {
|
|
||||||
guiDict[fld]['object'].rotation.y = value * (Math.PI / 180)
|
|
||||||
guiDict[fld]['objectLines'].rotation.y = value * (Math.PI / 180)
|
|
||||||
});
|
|
||||||
guiDict[fld]['folder'].add( folderStandard, 'zAngle', 0, 360 ).step(0.1).onChange( (value) => {
|
|
||||||
guiDict[fld]['object'].rotation.z = value * (Math.PI / 180)
|
|
||||||
guiDict[fld]['objectLines'].rotation.z = value * (Math.PI / 180)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
circles.add( params, 'xangle', 0, 360 ).step(0.1).onChange( (value) => {
|
|
||||||
for (let sphere of spheres) {
|
|
||||||
[sphere.position.x, sphere.position.y, sphere.position.z] = rotatePoint3D(sphere.start_x, sphere.start_y, sphere.start_z, params['yangle'], value, params['zangle'])
|
|
||||||
}
|
|
||||||
for (let sphereLine of spheresLines) {
|
|
||||||
[sphereLine.position.x, sphereLine.position.y, sphereLine.position.z] = rotatePoint3D(sphereLine.start_x, sphereLine.start_y, sphereLine.start_z, params['yangle'], value, params['zangle'])
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
circles.add( params, 'yangle', 0, 360 ).step(0.1).onChange( (value) => {
|
|
||||||
for (let sphere of spheres) {
|
|
||||||
[sphere.position.x, sphere.position.y, sphere.position.z] = rotatePoint3D(sphere.start_x, sphere.start_y, sphere.start_z, value, params['xangle'], params['zangle'])
|
|
||||||
}
|
|
||||||
for (let sphereLine of spheresLines) {
|
|
||||||
[sphereLine.position.x, sphereLine.position.y, sphereLine.position.z] = rotatePoint3D(sphereLine.start_x, sphereLine.start_y, sphereLine.start_z, value, params['xangle'], params['zangle'])
|
|
||||||
}
|
|
||||||
});
|
|
||||||
circles.add( params, 'zangle', 0, 360 ).step(0.1).onChange( (value) => {
|
|
||||||
for (let sphere of spheres) {
|
|
||||||
[sphere.position.x, sphere.position.y, sphere.position.z] = rotatePoint3D(sphere.start_x, sphere.start_y, sphere.start_z, params['yangle'], params['xangle'], value)
|
|
||||||
}
|
|
||||||
for (let sphereLine of spheresLines) {
|
|
||||||
[sphereLine.position.x, sphereLine.position.y, sphereLine.position.z] = rotatePoint3D(sphereLine.start_x, sphereLine.start_y, sphereLine.start_z, params['yangle'], params['xangle'], value)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
gui.add( params, 'radius', 0.5, 1.5).onChange( (value) => {
|
|
||||||
for (let sphere of spheres) {
|
|
||||||
scene.remove(sphere)
|
|
||||||
}
|
|
||||||
for (let sphereLine of spheresLines) {
|
|
||||||
scene.remove(sphereLine)
|
|
||||||
}
|
|
||||||
renderFlower (value)
|
|
||||||
});
|
|
||||||
gui.add( params, 'tube', 0.01, 0.1 ).step( 0.005 ).onChange( () => {
|
|
||||||
for (let sphere of spheres) {
|
|
||||||
scene.remove(sphere)
|
|
||||||
}
|
|
||||||
for (let sphereLine of spheresLines) {
|
|
||||||
scene.remove(sphereLine)
|
|
||||||
}
|
|
||||||
renderFlower (params['radius'])
|
|
||||||
});
|
|
||||||
|
|
||||||
// ?only=<fire|fire2|air|earth|water|aether> hides the other solids, for
|
|
||||||
// checking a single shape against the reference
|
|
||||||
const only = debugFlags.get( 'only' );
|
|
||||||
if ( only && guiDict[ only ] ) {
|
|
||||||
for ( let fld of Object.keys( guiDict ) ) {
|
|
||||||
if ( fld === only || ! guiDict[ fld ][ 'object' ] ) continue;
|
|
||||||
guiDict[ fld ][ 'object' ].visible = false;
|
|
||||||
guiDict[ fld ][ 'objectLines' ].visible = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
renderFlower()
|
|
||||||
|
|
||||||
// legacy lighting was removed in three r155: intensities need the old value times pi
|
|
||||||
const spotLight = new THREE.DirectionalLight( 0xeaa7a7, 1.5 * Math.PI );
|
|
||||||
spotLight.position.set( 10, 0, 0 );
|
|
||||||
spotLight.castShadow = true;
|
|
||||||
spotLight.shadow.mapSize.width = 2048;
|
|
||||||
spotLight.shadow.mapSize.height = 2048;
|
|
||||||
spotLight.shadow.camera.far = 21;
|
|
||||||
spotLight.shadow.camera.left = -10;
|
|
||||||
spotLight.shadow.camera.right = 10;
|
|
||||||
spotLight.shadow.camera.top = 10;
|
|
||||||
spotLight.shadow.camera.bottom = -10;
|
|
||||||
|
|
||||||
// spotLight.shadow.camera.fov = 35;
|
|
||||||
scene.add( spotLight );
|
|
||||||
|
|
||||||
//Create a plane that receives shadows (but does not cast them)
|
|
||||||
const planeGeometry = new THREE.PlaneGeometry( 20, 20, 10, 10 );
|
|
||||||
const planeMaterial = new THREE.MeshStandardMaterial( { color: 0xffffff } )
|
|
||||||
const plane = new THREE.Mesh( planeGeometry, planeMaterial );
|
|
||||||
planeMaterial.side = THREE.DoubleSide;
|
|
||||||
plane.rotation.y = Math.PI / 2;
|
|
||||||
plane.position.x = -10
|
|
||||||
plane.receiveShadow = true;
|
|
||||||
scene.add( plane );
|
|
||||||
|
|
||||||
//Create a helper for the shadow camera (optional)
|
|
||||||
// const helper = new THREE.CameraHelper( spotLight.shadow.camera );
|
|
||||||
// scene.add( helper );
|
|
||||||
|
|
||||||
// const axesHelper = new THREE.AxesHelper( 40 );
|
|
||||||
// scene.add( axesHelper );
|
|
||||||
|
|
||||||
let stats = new Stats();
|
|
||||||
document.body.appendChild( stats.dom );
|
|
||||||
|
|
||||||
// var axis = new THREE.Vector3(0, 1, 0);
|
|
||||||
|
|
||||||
function animate() {
|
function animate() {
|
||||||
requestAnimationFrame( animate );
|
requestAnimationFrame( animate );
|
||||||
|
|
||||||
|
// in-plane rotation only: the flower turns with the site, never edge-on
|
||||||
const time = performance.now() / 1000;
|
const time = performance.now() / 1000;
|
||||||
|
group.rotation.z = time * 0.05;
|
||||||
|
icosa.rotation.z = -time * 0.08;
|
||||||
|
|
||||||
// for (let sphere of spheres) {
|
updateWorld();
|
||||||
// sphere.position.x = Math.cos( time ) * sphere.start_x;
|
|
||||||
// sphere.position.z = - Math.sin( time ) * sphere.start_z;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// for (let sphereLine of spheresLines) {
|
|
||||||
// sphereLine.position.x = Math.cos( time ) * sphereLine.start_x;
|
|
||||||
// sphereLine.position.z = - Math.sin( time ) * sphereLine.start_z;
|
|
||||||
// }
|
|
||||||
|
|
||||||
controls.update();
|
|
||||||
stats.update();
|
|
||||||
|
|
||||||
renderer.render( scene, camera );
|
renderer.render( scene, camera );
|
||||||
};
|
}
|
||||||
|
|
||||||
animate();
|
animate();
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { currentTheme, toggleTheme } from './theme.js';
|
||||||
|
import { EXIT_MS } from './transit.js';
|
||||||
|
|
||||||
|
// Shared page chrome: theme attribute for the CSS, fade transitions between
|
||||||
|
// pages, and the dark/light toggle. Imported for its side effects by every page.
|
||||||
|
document.documentElement.dataset.theme = currentTheme();
|
||||||
|
|
||||||
|
const FADE_MS = 350;
|
||||||
|
|
||||||
|
window.addEventListener( 'DOMContentLoaded', () => {
|
||||||
|
// the overlay lives in the HTML templates so it covers the very first paint;
|
||||||
|
// pages without one (shouldn't happen) still get a working fallback
|
||||||
|
let fade = document.querySelector( '.fade' );
|
||||||
|
if ( ! fade ) {
|
||||||
|
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' ) );
|
||||||
|
setTimeout( () => fade.classList.remove( 'done' ), EXIT_MS - FADE_MS + 100 );
|
||||||
|
setTimeout( action, EXIT_MS + 100 );
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener( 'click', ( event ) => {
|
||||||
|
const link = event.target.closest( 'a[href]' );
|
||||||
|
if ( ! link || link.origin !== location.origin ) return;
|
||||||
|
event.preventDefault();
|
||||||
|
fadeTo( () => { location.href = link.href; } );
|
||||||
|
} );
|
||||||
|
|
||||||
|
const button = document.createElement( 'button' );
|
||||||
|
button.className = 'theme-toggle';
|
||||||
|
button.textContent = currentTheme() === 'dark' ? '☀' : '☾';
|
||||||
|
button.title = 'toggle dark / light';
|
||||||
|
button.addEventListener( 'click', () => {
|
||||||
|
// the scenes build their materials once at startup, so switching theme
|
||||||
|
// reloads the page - the fade makes that read as a transition
|
||||||
|
fadeTo( () => {
|
||||||
|
toggleTheme();
|
||||||
|
location.reload();
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
document.body.appendChild( button );
|
||||||
|
} );
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
// WebGL draws lines at 1px regardless of linewidth, so shape edges are built
|
||||||
|
// from real cylinder tubes instead - same trick as the Metatron page
|
||||||
|
export function makeEdgeTubes( geometry, color, tubeRadius = 0.03 ) {
|
||||||
|
const edges = new THREE.EdgesGeometry( geometry );
|
||||||
|
const positions = edges.attributes.position;
|
||||||
|
const group = new THREE.Group();
|
||||||
|
const material = new THREE.MeshBasicMaterial( { color } );
|
||||||
|
const up = new THREE.Vector3( 0, 1, 0 );
|
||||||
|
const start = new THREE.Vector3();
|
||||||
|
const end = new THREE.Vector3();
|
||||||
|
|
||||||
|
for ( let i = 0; i < positions.count; i += 2 ) {
|
||||||
|
start.fromBufferAttribute( positions, i );
|
||||||
|
end.fromBufferAttribute( positions, i + 1 );
|
||||||
|
const direction = new THREE.Vector3().subVectors( end, start );
|
||||||
|
const length = direction.length();
|
||||||
|
|
||||||
|
const cylinder = new THREE.CylinderGeometry( tubeRadius, tubeRadius, length, 12 );
|
||||||
|
cylinder.translate( 0, length / 2, 0 );
|
||||||
|
|
||||||
|
const tube = new THREE.Mesh( cylinder, material );
|
||||||
|
tube.position.copy( start );
|
||||||
|
tube.quaternion.setFromUnitVectors( up, direction.normalize() );
|
||||||
|
tube.castShadow = true;
|
||||||
|
group.add( tube );
|
||||||
|
}
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A straight tube between two points, for edges that don't come from a geometry
|
||||||
|
export function makeTube( a, b, material, tubeRadius = 0.03 ) {
|
||||||
|
const direction = new THREE.Vector3().subVectors( b, a );
|
||||||
|
const length = direction.length();
|
||||||
|
|
||||||
|
const cylinder = new THREE.CylinderGeometry( tubeRadius, tubeRadius, length, 12 );
|
||||||
|
cylinder.translate( 0, length / 2, 0 );
|
||||||
|
|
||||||
|
const tube = new THREE.Mesh( cylinder, material );
|
||||||
|
tube.position.copy( a );
|
||||||
|
tube.quaternion.setFromUnitVectors( new THREE.Vector3( 0, 1, 0 ), direction.normalize() );
|
||||||
|
tube.castShadow = true;
|
||||||
|
return tube;
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||||
|
|
||||||
|
import './chrome.js';
|
||||||
|
import { sceneColors } from './theme.js';
|
||||||
|
import { bindWorld } from './transit.js';
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// an invisible wall, and every shape lives in a `world` group that flies in and
|
||||||
|
// out on page transitions. Returns hooks instead of globals so pages stay small.
|
||||||
|
export function createStage( { target = [ 0, 0, 0 ] } = {} ) {
|
||||||
|
const colors = sceneColors();
|
||||||
|
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
|
||||||
|
const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 500 );
|
||||||
|
camera.position.set( 10, 4, 12 );
|
||||||
|
|
||||||
|
const renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } );
|
||||||
|
renderer.setPixelRatio( Math.min( window.devicePixelRatio, 2 ) );
|
||||||
|
renderer.setSize( window.innerWidth, window.innerHeight );
|
||||||
|
renderer.shadowMap.enabled = true;
|
||||||
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||||
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||||
|
renderer.toneMappingExposure = 1;
|
||||||
|
document.body.appendChild( renderer.domElement );
|
||||||
|
|
||||||
|
window.addEventListener( 'resize', () => {
|
||||||
|
camera.aspect = window.innerWidth / window.innerHeight;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
renderer.setSize( window.innerWidth, window.innerHeight );
|
||||||
|
} );
|
||||||
|
|
||||||
|
const controls = new OrbitControls( camera, renderer.domElement );
|
||||||
|
controls.target.set( ...target );
|
||||||
|
|
||||||
|
// 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 ) );
|
||||||
|
|
||||||
|
// legacy lighting was removed in three r155: intensities need the old value times pi
|
||||||
|
const spotLight = new THREE.DirectionalLight( colors.light, 1.5 * Math.PI );
|
||||||
|
spotLight.position.set( 10, 0, 0 );
|
||||||
|
spotLight.castShadow = true;
|
||||||
|
spotLight.shadow.mapSize.width = 2048;
|
||||||
|
spotLight.shadow.mapSize.height = 2048;
|
||||||
|
spotLight.shadow.camera.far = 21;
|
||||||
|
spotLight.shadow.camera.left = -10;
|
||||||
|
spotLight.shadow.camera.right = 10;
|
||||||
|
spotLight.shadow.camera.top = 10;
|
||||||
|
spotLight.shadow.camera.bottom = -10;
|
||||||
|
scene.add( spotLight );
|
||||||
|
|
||||||
|
// the wall itself is invisible: only the shadow "projection" shows, in
|
||||||
|
// light orange, floating on the gradient; front side only so the shapes
|
||||||
|
// stay fully visible when orbiting behind it
|
||||||
|
const planeGeometry = new THREE.PlaneGeometry( 20, 20, 10, 10 );
|
||||||
|
const planeMaterial = new THREE.ShadowMaterial( { color: colors.shadow, opacity: 0.9 } );
|
||||||
|
const plane = new THREE.Mesh( planeGeometry, planeMaterial );
|
||||||
|
plane.rotation.y = Math.PI / 2;
|
||||||
|
plane.position.x = -10;
|
||||||
|
plane.receiveShadow = true;
|
||||||
|
scene.add( plane );
|
||||||
|
|
||||||
|
|
||||||
|
// pages add their shapes to this group; transitions scale it in and out
|
||||||
|
const world = new THREE.Group();
|
||||||
|
scene.add( world );
|
||||||
|
const updateWorld = bindWorld( world );
|
||||||
|
|
||||||
|
const updates = [];
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
requestAnimationFrame( animate );
|
||||||
|
|
||||||
|
const time = performance.now() / 1000;
|
||||||
|
for ( const update of updates ) update( time );
|
||||||
|
|
||||||
|
updateWorld();
|
||||||
|
controls.update();
|
||||||
|
renderer.render( scene, camera );
|
||||||
|
}
|
||||||
|
|
||||||
|
animate();
|
||||||
|
|
||||||
|
return { scene: world, camera, renderer, controls, colors, onUpdate: ( fn ) => updates.push( fn ) };
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
const KEY = 'thrive-theme';
|
||||||
|
|
||||||
|
export function currentTheme() {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem( KEY ) === 'light' ? 'light' : 'dark';
|
||||||
|
} catch {
|
||||||
|
return 'dark';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleTheme() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem( KEY, currentTheme() === 'dark' ? 'light' : 'dark' );
|
||||||
|
} catch {
|
||||||
|
// private mode: theme just won't persist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the landing page palette, used 1:1 in the scenes; the background itself is the
|
||||||
|
// landing CSS gradient showing through a transparent canvas (see styles.scss)
|
||||||
|
const fills = {
|
||||||
|
sphere: 0xf44f04, // orange
|
||||||
|
fire: 0xf44f04, // orange
|
||||||
|
earth: 0x2a1204, // deep brown
|
||||||
|
air: 0xffd4b3, // pale tint
|
||||||
|
water: 0xff9a5c, // light orange
|
||||||
|
aether: 0xf5e9de, // cream
|
||||||
|
};
|
||||||
|
|
||||||
|
export function sceneColors() {
|
||||||
|
return currentTheme() === 'dark'
|
||||||
|
? { dark: true, light: 0xffb37a, line: 0xf5e9de, shadow: 0xff9a5c, fills }
|
||||||
|
: { dark: false, light: 0xffb37a, line: 0x1b0a02, shadow: 0xf44f04, fills };
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Shape transitions between pages, styled as one continuous forward flight:
|
||||||
|
// entering shapes grow inward from the distance into place, leaving shapes keep
|
||||||
|
// growing outward past the camera. The world keeps casting shadows while it
|
||||||
|
// scales, so the projection on the wall travels with it.
|
||||||
|
export const EXIT_MS = 600;
|
||||||
|
const ENTER_MS = 900;
|
||||||
|
|
||||||
|
const easeOutCubic = ( t ) => 1 - Math.pow( 1 - t, 3 );
|
||||||
|
const easeInCubic = ( t ) => t * t * t;
|
||||||
|
|
||||||
|
// wraps a THREE.Group holding all of a page's shapes; call the returned
|
||||||
|
// function every frame
|
||||||
|
export function bindWorld( world ) {
|
||||||
|
let mode = 'enter';
|
||||||
|
let start = performance.now();
|
||||||
|
|
||||||
|
window.addEventListener( 'thrive:exit', () => {
|
||||||
|
mode = 'exit';
|
||||||
|
start = performance.now();
|
||||||
|
} );
|
||||||
|
|
||||||
|
return function update() {
|
||||||
|
if ( mode === 'enter' ) {
|
||||||
|
const k = Math.min( 1, ( performance.now() - start ) / ENTER_MS );
|
||||||
|
world.scale.setScalar( Math.max( 0.001, easeOutCubic( k ) ) );
|
||||||
|
} else {
|
||||||
|
const k = Math.min( 1, ( performance.now() - start ) / EXIT_MS );
|
||||||
|
world.scale.setScalar( 1 + easeInCubic( k ) * 7 );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html oncontextmenu="return false;">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Vector Equilibrium</title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="./css/styles.scss">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="fade"></div>
|
||||||
|
<nav class="page-nav">
|
||||||
|
<a href="index.html">← Thrive</a>
|
||||||
|
<span>Vector Equilibrium</span>
|
||||||
|
</nav>
|
||||||
|
<script type="module" src="./matrix.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
|
||||||
|
import { ConvexGeometry } from 'three/addons/geometries/ConvexGeometry.js';
|
||||||
|
|
||||||
|
import { createStage } from './lib/stage.js';
|
||||||
|
import { makeTube } from './lib/edgeTubes.js';
|
||||||
|
|
||||||
|
// The vector equilibrium (cuboctahedron): the only form where every vertex is
|
||||||
|
// exactly as far from the centre as from its neighbours - Fuller's "zero point"
|
||||||
|
// of the isotropic vector matrix. Its 12 vertices plus centre are the 13 circles
|
||||||
|
// of Metatron's Cube, and 12 spheres kiss one central sphere on those points.
|
||||||
|
const { scene, colors, onUpdate } = createStage();
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
scale: 2,
|
||||||
|
edges: true,
|
||||||
|
spokes: true,
|
||||||
|
fill: true,
|
||||||
|
spheres: false,
|
||||||
|
spin: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const group = new THREE.Group();
|
||||||
|
scene.add( group );
|
||||||
|
|
||||||
|
let parts = [];
|
||||||
|
|
||||||
|
function renderMatrix() {
|
||||||
|
for ( const part of parts ) group.remove( part );
|
||||||
|
parts = [];
|
||||||
|
|
||||||
|
const a = params.scale;
|
||||||
|
|
||||||
|
// all permutations of (+-a, +-a, 0): the 12 cuboctahedron vertices
|
||||||
|
const vertices = [];
|
||||||
|
for ( const s1 of [ -1, 1 ] ) {
|
||||||
|
for ( const s2 of [ -1, 1 ] ) {
|
||||||
|
vertices.push( new THREE.Vector3( s1 * a, s2 * a, 0 ) );
|
||||||
|
vertices.push( new THREE.Vector3( s1 * a, 0, s2 * a ) );
|
||||||
|
vertices.push( new THREE.Vector3( 0, s1 * a, s2 * a ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const edgeLength = a * Math.SQRT2; // equals the distance from centre to vertex
|
||||||
|
const edgeMaterial = new THREE.MeshBasicMaterial( { color: colors.line } );
|
||||||
|
const spokeMaterial = new THREE.MeshBasicMaterial( { color: colors.line, opacity: 0.35, transparent: true } );
|
||||||
|
|
||||||
|
const edges = new THREE.Group();
|
||||||
|
for ( let i = 0; i < vertices.length; i++ ) {
|
||||||
|
for ( let j = i + 1; j < vertices.length; j++ ) {
|
||||||
|
if ( Math.abs( vertices[ i ].distanceTo( vertices[ j ] ) - edgeLength ) < 1e-6 ) {
|
||||||
|
edges.add( makeTube( vertices[ i ], vertices[ j ], edgeMaterial ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
edges.visible = params.edges;
|
||||||
|
group.add( edges );
|
||||||
|
parts.push( edges );
|
||||||
|
|
||||||
|
// the 12 radial spokes are as long as the edges - that's the equilibrium
|
||||||
|
const spokes = new THREE.Group();
|
||||||
|
const centre = new THREE.Vector3( 0, 0, 0 );
|
||||||
|
for ( const vertex of vertices ) {
|
||||||
|
spokes.add( makeTube( centre, vertex, spokeMaterial, 0.02 ) );
|
||||||
|
}
|
||||||
|
spokes.visible = params.spokes;
|
||||||
|
group.add( spokes );
|
||||||
|
parts.push( spokes );
|
||||||
|
|
||||||
|
const fillMaterial = new THREE.MeshStandardMaterial( {
|
||||||
|
color: colors.fills.fire,
|
||||||
|
metalness: 0.7,
|
||||||
|
roughness: 0.3,
|
||||||
|
opacity: 0.8,
|
||||||
|
transparent: true,
|
||||||
|
} );
|
||||||
|
// the fill casts no shadow: only the edges and spokes project, as outlines
|
||||||
|
const fill = new THREE.Mesh( new ConvexGeometry( vertices ), fillMaterial );
|
||||||
|
fill.visible = params.fill;
|
||||||
|
group.add( fill );
|
||||||
|
parts.push( fill );
|
||||||
|
|
||||||
|
// 12 spheres of radius edge/2 kiss each other on the edges and all kiss the
|
||||||
|
// 13th sphere in the centre: closest packing of equal spheres
|
||||||
|
const sphereRadius = edgeLength / 2;
|
||||||
|
const sphereMaterial = new THREE.MeshStandardMaterial( {
|
||||||
|
color: colors.fills.water,
|
||||||
|
metalness: 0.7,
|
||||||
|
roughness: 0.3,
|
||||||
|
opacity: 0.8,
|
||||||
|
transparent: true,
|
||||||
|
} );
|
||||||
|
const spheres = new THREE.Group();
|
||||||
|
const sphereGeometry = new THREE.SphereGeometry( sphereRadius, 48, 48 );
|
||||||
|
for ( const point of [ centre, ...vertices ] ) {
|
||||||
|
const sphere = new THREE.Mesh( sphereGeometry, sphereMaterial );
|
||||||
|
sphere.position.copy( point );
|
||||||
|
sphere.castShadow = true;
|
||||||
|
spheres.add( sphere );
|
||||||
|
}
|
||||||
|
spheres.visible = params.spheres;
|
||||||
|
group.add( spheres );
|
||||||
|
parts.push( spheres );
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMatrix();
|
||||||
|
|
||||||
|
onUpdate( ( time ) => {
|
||||||
|
if ( params.spin ) group.rotation.y = time * 0.15;
|
||||||
|
} );
|
||||||
|
|
||||||
|
const gui = new GUI();
|
||||||
|
gui.add( params, 'scale', 1, 4 ).step( 0.1 ).onChange( renderMatrix );
|
||||||
|
gui.add( params, 'edges' ).onChange( ( value ) => { parts[ 0 ].visible = value; } );
|
||||||
|
gui.add( params, 'spokes' ).onChange( ( value ) => { parts[ 1 ].visible = value; } );
|
||||||
|
gui.add( params, 'fill' ).onChange( ( value ) => { parts[ 2 ].visible = value; } );
|
||||||
|
gui.add( params, 'spheres' ).onChange( ( value ) => { parts[ 3 ].visible = value; } );
|
||||||
|
gui.add( params, 'spin' );
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html oncontextmenu="return false;">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Metatron's Cube</title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="./css/styles.scss">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="fade"></div>
|
||||||
|
<nav class="page-nav">
|
||||||
|
<a href="index.html">← Thrive</a>
|
||||||
|
<span>Metatron's Cube</span>
|
||||||
|
</nav>
|
||||||
|
<script type="module" src="./metatron.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+725
@@ -0,0 +1,725 @@
|
|||||||
|
|
||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||||
|
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
|
||||||
|
|
||||||
|
|
||||||
|
import './lib/chrome.js';
|
||||||
|
import { sceneColors } from './lib/theme.js';
|
||||||
|
import { bindWorld } from './lib/transit.js';
|
||||||
|
|
||||||
|
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
|
||||||
|
// `world`, which page transitions fly in and out
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
const world = new THREE.Group();
|
||||||
|
scene.add( world );
|
||||||
|
const updateWorld = bindWorld( world );
|
||||||
|
|
||||||
|
const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 500 );
|
||||||
|
camera.position.z = 12;
|
||||||
|
camera.position.x = 10;
|
||||||
|
camera.position.y = 4;
|
||||||
|
|
||||||
|
// debug switches to bisect driver issues: ?noaa disables MSAA, ?noshadow disables shadow maps
|
||||||
|
const debugFlags = new URLSearchParams( window.location.search );
|
||||||
|
|
||||||
|
const renderer = new THREE.WebGLRenderer( { antialias: !debugFlags.has( 'noaa' ), alpha: true } );
|
||||||
|
renderer.setPixelRatio( Math.min( window.devicePixelRatio, 2 ) );
|
||||||
|
renderer.setSize( window.innerWidth, window.innerHeight );
|
||||||
|
renderer.shadowMap.enabled = !debugFlags.has( 'noshadow' );
|
||||||
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||||
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||||
|
renderer.toneMappingExposure = 1;
|
||||||
|
document.body.appendChild( renderer.domElement );
|
||||||
|
|
||||||
|
window.addEventListener( 'resize', () => {
|
||||||
|
camera.aspect = window.innerWidth / window.innerHeight;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
renderer.setSize( window.innerWidth, window.innerHeight );
|
||||||
|
} );
|
||||||
|
|
||||||
|
const controls = new OrbitControls( camera, renderer.domElement );
|
||||||
|
controls.target.set( -4, 0, 0 );
|
||||||
|
|
||||||
|
|
||||||
|
// const geometry_torus = new THREE.TorusGeometry( 2.5, 2.5, 32, 128 );
|
||||||
|
// const material_torus = new THREE.MeshBasicMaterial( { color: 0x049ef4, wireframe: true, wireframeLinewidth: 1} );
|
||||||
|
// const torus = new THREE.Mesh( geometry_torus, material_torus );
|
||||||
|
// torus.rotation.x = Math.PI / 2;
|
||||||
|
// torus.castShadow = true;
|
||||||
|
// world.add( torus );
|
||||||
|
|
||||||
|
// const icoGeometry = new THREE.IcosahedronGeometry( 5 );
|
||||||
|
// const icoMaterial= new THREE.MeshBasicMaterial( {color: 0x049ef4, wireframe: true, wireframeLinewidth: 8 } );
|
||||||
|
// const iso = new THREE.Mesh( icoGeometry, icoMaterial );
|
||||||
|
// iso.castShadow = true;
|
||||||
|
// world.add( iso );
|
||||||
|
|
||||||
|
// --------- Mekaraba ---------- //
|
||||||
|
// const tetraGeometry = new THREE.TetrahedronGeometry( 2 );
|
||||||
|
// const tetraMaterial = new THREE.MeshBasicMaterial( {color: 0xffff00 } );
|
||||||
|
// const tetra = new THREE.Mesh( tetraGeometry, tetraMaterial );
|
||||||
|
// // tetra.position.y = - 0.81
|
||||||
|
|
||||||
|
// // tetra.rotation.x = Math.PI / 4 ;
|
||||||
|
// tetra.rotation.z = Math.PI / 6 ;
|
||||||
|
// // world.add( tetra );
|
||||||
|
|
||||||
|
// const tetraEdges = new THREE.EdgesGeometry( tetraGeometry );
|
||||||
|
// const tetraEdgesLine = new THREE.LineSegments( tetraEdges, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 3 } ) );
|
||||||
|
// // tetraEdgesLine.position.y = -0.81
|
||||||
|
|
||||||
|
// tetraEdgesLine.rotation.x = Math.PI / 4;
|
||||||
|
// // tetraEdgesLine.rotation.y = Math.PI ;
|
||||||
|
// // tetraEdgesLine.rotation.y = Math.PI / 6;
|
||||||
|
|
||||||
|
// tetraEdgesLine.castShadow = true;
|
||||||
|
// world.add( tetraEdgesLine );
|
||||||
|
|
||||||
|
// const tetraGeometry2 = new THREE.TetrahedronGeometry( 2 );
|
||||||
|
// const tetraMaterial2 = new THREE.MeshBasicMaterial( {color: 0xffff00 } );
|
||||||
|
// const tetra2 = new THREE.Mesh( tetraGeometry2, tetraMaterial2 );
|
||||||
|
// tetra2.position.y = 0.81
|
||||||
|
// tetra2.rotation.x = Math.PI / 4 ;
|
||||||
|
// // tetra2.rotation.y = Math.PI / 4 ;
|
||||||
|
// tetra2.rotation.z = Math.PI / 4 ;
|
||||||
|
// // world.add( tetra2 );
|
||||||
|
|
||||||
|
// const tetraEdges2 = new THREE.EdgesGeometry( tetraGeometry2 );
|
||||||
|
// const tetraEdgesLine2 = new THREE.LineSegments( tetraEdges2, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 3 } ) );
|
||||||
|
// tetraEdgesLine2.castShadow = true;
|
||||||
|
// tetraEdgesLine2.position.y = 0.81
|
||||||
|
// tetraEdgesLine2.rotation.x = Math.PI / 4 ;
|
||||||
|
// // tetraEdgesLine2.rotation.y = Math.PI / 4 ;
|
||||||
|
// tetraEdgesLine2.rotation.z = Math.PI / 4 ;
|
||||||
|
// world.add( tetraEdgesLine2 );
|
||||||
|
|
||||||
|
|
||||||
|
// ---------- David ------------ //
|
||||||
|
// const tetraGeometry = new THREE.ConeGeometry( 2, 3, 3 );
|
||||||
|
// const tetraMaterial = new THREE.MeshBasicMaterial( {color: 0xffff00 } );
|
||||||
|
// const tetra = new THREE.Mesh( tetraGeometry, tetraMaterial );
|
||||||
|
// tetra.rotation.y = Math.PI / 6 ;
|
||||||
|
// tetra.position.y = 1 / 2
|
||||||
|
// world.add( tetra );
|
||||||
|
|
||||||
|
// const tetraEdges = new THREE.EdgesGeometry( tetraGeometry );
|
||||||
|
// const tetraEdgesLine = new THREE.LineSegments( tetraEdges, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 15 } ) );
|
||||||
|
// tetraEdgesLine.rotation.y = Math.PI / 6 ;
|
||||||
|
// tetraEdgesLine.position.y = 1 / 2
|
||||||
|
// tetraEdgesLine.castShadow = true;
|
||||||
|
// world.add( tetraEdgesLine );
|
||||||
|
|
||||||
|
// const tetraGeometry2 = new THREE.ConeGeometry( 2, 3, 3 );
|
||||||
|
// const tetraMaterial2 = new THREE.MeshBasicMaterial( {color: 0xffff00 } );
|
||||||
|
// const tetra2 = new THREE.Mesh( tetraGeometry2, tetraMaterial2 );
|
||||||
|
// tetra2.rotation.y = Math.PI + Math.PI / 6 ;
|
||||||
|
// tetra2.rotation.z = Math.PI;
|
||||||
|
// tetra2.position.y = - 1 / 2
|
||||||
|
// world.add( tetra2 );
|
||||||
|
|
||||||
|
// const tetraEdges2 = new THREE.EdgesGeometry( tetraGeometry2 );
|
||||||
|
// const tetraEdgesLine2 = new THREE.LineSegments( tetraEdges2, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 15 } ) );
|
||||||
|
// tetraEdgesLine2.castShadow = true;
|
||||||
|
// tetraEdgesLine2.rotation.y = Math.PI + Math.PI / 6 ;
|
||||||
|
// tetraEdgesLine2.rotation.z = Math.PI;
|
||||||
|
// tetraEdgesLine2.position.y = - 1 / 2
|
||||||
|
// world.add( tetraEdgesLine2 );
|
||||||
|
|
||||||
|
// ---------- Flower of life ------------ //
|
||||||
|
|
||||||
|
|
||||||
|
let rotatePoint2D = (cx, cy, angle, px, py) => {
|
||||||
|
angle = - angle * (Math.PI / 180)
|
||||||
|
let s = Math.sin(angle);
|
||||||
|
let c = Math.cos(angle);
|
||||||
|
|
||||||
|
// translate point back to origin:
|
||||||
|
px -= cx;
|
||||||
|
py -= cy;
|
||||||
|
|
||||||
|
// rotatePoint3D point
|
||||||
|
let xnew = px * c + py * s;
|
||||||
|
let ynew = px * s - py * c;
|
||||||
|
|
||||||
|
// translate point back:
|
||||||
|
px = xnew + cx;
|
||||||
|
py = ynew + cy;
|
||||||
|
return [px, py];
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotatePoint3D(px, py, pz, pitch, roll, yaw) {
|
||||||
|
pitch = - pitch * (Math.PI / 180)
|
||||||
|
roll = - roll * (Math.PI / 180)
|
||||||
|
yaw = - yaw * (Math.PI / 180)
|
||||||
|
|
||||||
|
var cosa = Math.cos(yaw);
|
||||||
|
var sina = Math.sin(yaw);
|
||||||
|
|
||||||
|
var cosb = Math.cos(pitch);
|
||||||
|
var sinb = Math.sin(pitch);
|
||||||
|
|
||||||
|
var cosc = Math.cos(roll);
|
||||||
|
var sinc = Math.sin(roll);
|
||||||
|
|
||||||
|
var Axx = cosa*cosb;
|
||||||
|
var Axy = cosa*sinb*sinc - sina*cosc;
|
||||||
|
var Axz = cosa*sinb*cosc + sina*sinc;
|
||||||
|
|
||||||
|
var Ayx = sina*cosb;
|
||||||
|
var Ayy = sina*sinb*sinc + cosa*cosc;
|
||||||
|
var Ayz = sina*sinb*cosc - cosa*sinc;
|
||||||
|
|
||||||
|
var Azx = -sinb;
|
||||||
|
var Azy = cosb*sinc;
|
||||||
|
var Azz = cosb*cosc;
|
||||||
|
|
||||||
|
let x = Axx*px + Axy*py + Axz*pz;
|
||||||
|
let y = Ayx*px + Ayy*py + Ayz*pz;
|
||||||
|
let z = Azx*px + Azy*py + Azz*pz;
|
||||||
|
|
||||||
|
return [x, y, z]
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebGL draws lines at 1px regardless of linewidth, so the shape edges are built
|
||||||
|
// from real cylinder tubes instead - same 0.03 thickness as the circle tori
|
||||||
|
function makeEdgeTubes( geometry, color, tubeRadius = 0.03 ) {
|
||||||
|
const edges = new THREE.EdgesGeometry( geometry );
|
||||||
|
const positions = edges.attributes.position;
|
||||||
|
const group = new THREE.Group();
|
||||||
|
const material = new THREE.MeshBasicMaterial( { color } );
|
||||||
|
const up = new THREE.Vector3( 0, 1, 0 );
|
||||||
|
const start = new THREE.Vector3();
|
||||||
|
const end = new THREE.Vector3();
|
||||||
|
|
||||||
|
for ( let i = 0; i < positions.count; i += 2 ) {
|
||||||
|
start.fromBufferAttribute( positions, i );
|
||||||
|
end.fromBufferAttribute( positions, i + 1 );
|
||||||
|
const direction = new THREE.Vector3().subVectors( end, start );
|
||||||
|
const length = direction.length();
|
||||||
|
|
||||||
|
const cylinder = new THREE.CylinderGeometry( tubeRadius, tubeRadius, length, 12 );
|
||||||
|
cylinder.translate( 0, length / 2, 0 );
|
||||||
|
|
||||||
|
const tube = new THREE.Mesh( cylinder, material );
|
||||||
|
tube.position.copy( start );
|
||||||
|
tube.quaternion.setFromUnitVectors( up, direction.normalize() );
|
||||||
|
tube.castShadow = true;
|
||||||
|
group.add( tube );
|
||||||
|
}
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
xangle: 45,
|
||||||
|
yangle: 0,
|
||||||
|
zangle: 54.72972973,
|
||||||
|
radius: 1,
|
||||||
|
tube: 0.015,
|
||||||
|
};
|
||||||
|
|
||||||
|
// const params = {
|
||||||
|
// xangle: 0,
|
||||||
|
// yangle: 0,
|
||||||
|
// zangle: 0,
|
||||||
|
// size: 5,
|
||||||
|
// radius: 1
|
||||||
|
// };
|
||||||
|
|
||||||
|
let sphere, circleCircum, cube, cubeLines;
|
||||||
|
let spheres = [];
|
||||||
|
let spheresLines = [];
|
||||||
|
let gui;
|
||||||
|
let solids = [];
|
||||||
|
|
||||||
|
let renderFlower = (radius = 1) => {
|
||||||
|
spheres = [];
|
||||||
|
spheresLines = [];
|
||||||
|
|
||||||
|
// tear down the previous render's shapes and GUI so re-rendering never stacks duplicates
|
||||||
|
if ( gui ) gui.destroy();
|
||||||
|
for ( let obj of solids ) world.remove( obj );
|
||||||
|
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 points = [ [ 0, 0, 0 ] ];
|
||||||
|
|
||||||
|
for ( let sx of [ -1, 1 ] ) {
|
||||||
|
for ( let sy of [ -1, 1 ] ) {
|
||||||
|
for ( let sz of [ -1, 1 ] ) {
|
||||||
|
points.push( [ sx * d, sy * d, sz * d ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
metalness: 0.7,
|
||||||
|
roughness: 0.3,
|
||||||
|
opacity: 0.8,
|
||||||
|
transparent: true
|
||||||
|
} );
|
||||||
|
sphere = new THREE.Mesh( sphereGeometry, sphereMaterial );
|
||||||
|
sphere.position.x = xtemp;
|
||||||
|
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,
|
||||||
|
opacity: 0.1,
|
||||||
|
transparent: true
|
||||||
|
} );
|
||||||
|
let ring = new THREE.Mesh( circleGeometry, circleMaterial );
|
||||||
|
ring.castShadow = true;
|
||||||
|
ring.rotation.y = Math.PI / 2;
|
||||||
|
ring.position.x = xtemp;
|
||||||
|
ring.position.y = ytemp;
|
||||||
|
ring.position.z = ztemp;
|
||||||
|
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 ) {
|
||||||
|
addCircleAt( ...point, world );
|
||||||
|
}
|
||||||
|
|
||||||
|
// the remaining cube circles: the six equatorial edge midpoints and the six
|
||||||
|
// face centres, hidden behind the "allCubeCircles" toggle
|
||||||
|
const extraCircles = new THREE.Group();
|
||||||
|
extraCircles.visible = false;
|
||||||
|
|
||||||
|
for ( let s of [ -1, 1 ] ) {
|
||||||
|
for ( let extraPoint of [
|
||||||
|
[ s * d, -s * d, 0 ], [ s * d, 0, -s * d ], [ 0, s * d, -s * d ],
|
||||||
|
[ s * d, 0, 0 ], [ 0, s * d, 0 ], [ 0, 0, s * d ],
|
||||||
|
] ) {
|
||||||
|
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
|
||||||
|
// this coincide with the dots instead of approximating them with tuned angles
|
||||||
|
const gridRotation = new THREE.Matrix4().makeBasis(
|
||||||
|
new THREE.Vector3( ...rotatePoint3D( 1, 0, 0, params['yangle'], params['xangle'], params['zangle'] ) ),
|
||||||
|
new THREE.Vector3( ...rotatePoint3D( 0, 1, 0, params['yangle'], params['xangle'], params['zangle'] ) ),
|
||||||
|
new THREE.Vector3( ...rotatePoint3D( 0, 0, 1, params['yangle'], params['xangle'], params['zangle'] ) )
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
// earth - width 2d puts every corner exactly on a corner dot
|
||||||
|
const boxGeometry = new THREE.BoxGeometry( 2 * d, 2 * d, 2 * d );
|
||||||
|
const boxMaterial = new THREE.MeshStandardMaterial( {
|
||||||
|
color: colors.fills.earth,
|
||||||
|
metalness: 0.7,
|
||||||
|
roughness: 0.3,
|
||||||
|
opacity: 0.8,
|
||||||
|
transparent: true,
|
||||||
|
// wireframe: true,
|
||||||
|
} );
|
||||||
|
cube = new THREE.Mesh( boxGeometry, boxMaterial );
|
||||||
|
cube.setRotationFromMatrix( gridRotation );
|
||||||
|
|
||||||
|
cubeLines = makeEdgeTubes( boxGeometry, lineColor( 0x1b0a02 ), params['tube'] );
|
||||||
|
cubeLines.setRotationFromMatrix( gridRotation );
|
||||||
|
cubeLines.castShadow = true;
|
||||||
|
|
||||||
|
world.add( cubeLines );
|
||||||
|
world.add( cube );
|
||||||
|
|
||||||
|
// fire - radius sqrt(3)d puts every vertex exactly on a corner dot
|
||||||
|
const tetraGeometry = new THREE.TetrahedronGeometry( Math.sqrt( 3 ) * d )
|
||||||
|
const tetraMaterial = new THREE.MeshStandardMaterial( {
|
||||||
|
color: colors.fills.fire,
|
||||||
|
metalness: 0.7,
|
||||||
|
roughness: 0.3,
|
||||||
|
opacity: 0.8,
|
||||||
|
transparent: true
|
||||||
|
} );
|
||||||
|
let tetra = new THREE.Mesh( tetraGeometry, tetraMaterial );
|
||||||
|
|
||||||
|
let tetraLines = makeEdgeTubes( tetraGeometry, lineColor( 0x140101 ), params['tube'] );
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
tetraLines.setRotationFromMatrix( gridRotation );
|
||||||
|
tetra.setRotationFromMatrix( gridRotation );
|
||||||
|
tetraLines.castShadow = true;
|
||||||
|
|
||||||
|
world.add( tetraLines );
|
||||||
|
world.add( tetra );
|
||||||
|
|
||||||
|
// second tetrahedron of the star tetrahedron (Merkaba): the same shape
|
||||||
|
// rotated a quarter turn so it interlocks with the first
|
||||||
|
const tetraGeometry2 = new THREE.TetrahedronGeometry( Math.sqrt( 3 ) * d );
|
||||||
|
tetraGeometry2.rotateZ( Math.PI / 2 );
|
||||||
|
let tetra2 = new THREE.Mesh( tetraGeometry2, tetraMaterial );
|
||||||
|
let tetraLines2 = makeEdgeTubes( tetraGeometry2, lineColor( 0x140101 ), params['tube'] );
|
||||||
|
|
||||||
|
tetra2.rotation.copy( tetra.rotation );
|
||||||
|
tetraLines2.rotation.copy( tetraLines.rotation );
|
||||||
|
|
||||||
|
world.add( tetraLines2 );
|
||||||
|
world.add( tetra2 );
|
||||||
|
|
||||||
|
// radius 2d puts the projected corners exactly on the outer circle centres
|
||||||
|
const octaGeometry = new THREE.OctahedronGeometry( 2 * d )
|
||||||
|
const octaMaterial = new THREE.MeshStandardMaterial( {
|
||||||
|
color: colors.fills.air,
|
||||||
|
metalness: 0.7,
|
||||||
|
roughness: 0.3,
|
||||||
|
opacity: 0.8,
|
||||||
|
transparent: true
|
||||||
|
} );
|
||||||
|
let octa = new THREE.Mesh( octaGeometry, octaMaterial );
|
||||||
|
|
||||||
|
let octaLines = makeEdgeTubes( octaGeometry, lineColor( 0x0a2528 ), params['tube'] );
|
||||||
|
|
||||||
|
octaLines.setRotationFromMatrix( gridRotation );
|
||||||
|
octa.setRotationFromMatrix( gridRotation );
|
||||||
|
octaLines.castShadow = true;
|
||||||
|
|
||||||
|
world.add( octaLines );
|
||||||
|
world.add( octa );
|
||||||
|
|
||||||
|
// radius 1.663d puts the projected corners on the outer circle centres
|
||||||
|
const icosaGeometry = new THREE.IcosahedronGeometry( 1.663 * d )
|
||||||
|
const icosaMaterial = new THREE.MeshStandardMaterial( {
|
||||||
|
color: colors.fills.water,
|
||||||
|
metalness: 0.7,
|
||||||
|
roughness: 0.3,
|
||||||
|
opacity: 0.8,
|
||||||
|
transparent: true
|
||||||
|
} );
|
||||||
|
let icosa = new THREE.Mesh( icosaGeometry, icosaMaterial );
|
||||||
|
|
||||||
|
let icosaLines = makeEdgeTubes( icosaGeometry, lineColor( 0x000000 ), params['tube'] );
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const PHI = ( 1 + Math.sqrt( 5 ) ) / 2;
|
||||||
|
const icosaTwist = Math.PI / 6 - Math.atan( ( PHI + 2 ) / ( Math.sqrt( 3 ) * PHI ) );
|
||||||
|
const icosaRotation = new THREE.Matrix4().multiplyMatrices(
|
||||||
|
gridRotation,
|
||||||
|
new THREE.Matrix4().makeRotationAxis( new THREE.Vector3( 1, 1, 1 ).normalize(), icosaTwist )
|
||||||
|
);
|
||||||
|
|
||||||
|
icosaLines.setRotationFromMatrix( icosaRotation );
|
||||||
|
icosa.setRotationFromMatrix( icosaRotation );
|
||||||
|
icosaLines.castShadow = true;
|
||||||
|
|
||||||
|
world.add( icosaLines );
|
||||||
|
world.add( icosa );
|
||||||
|
|
||||||
|
// 2.29 * 1.003: tuned against the circle pattern
|
||||||
|
const dodecaGeometry = new THREE.DodecahedronGeometry( 2.297 * radius )
|
||||||
|
const dodecaMaterial = new THREE.MeshStandardMaterial( {
|
||||||
|
color: colors.fills.aether,
|
||||||
|
metalness: 0.7,
|
||||||
|
roughness: 0.3,
|
||||||
|
opacity: 0.8,
|
||||||
|
transparent: true
|
||||||
|
} );
|
||||||
|
let dodeca = new THREE.Mesh( dodecaGeometry, dodecaMaterial );
|
||||||
|
|
||||||
|
let dodecaLines = makeEdgeTubes( dodecaGeometry, lineColor( 0x000000 ), params['tube'] );
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
// with a tuned x-angle on top
|
||||||
|
dodecaLines.setRotationFromMatrix( gridRotation );
|
||||||
|
dodeca.setRotationFromMatrix( gridRotation );
|
||||||
|
dodecaLines.rotation.x = 52.25 * (Math.PI / 180);
|
||||||
|
dodeca.rotation.x = 52.25 * (Math.PI / 180);
|
||||||
|
dodecaLines.castShadow = true;
|
||||||
|
|
||||||
|
world.add( dodecaLines );
|
||||||
|
world.add( dodeca );
|
||||||
|
|
||||||
|
|
||||||
|
// 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 circleMaterial = new THREE.MeshBasicMaterial( { color: colors.line } );
|
||||||
|
circleCircum = new THREE.Mesh( circleGeometry, circleMaterial );
|
||||||
|
circleCircum.rotation.y = Math.PI / 2;
|
||||||
|
circleCircum.castShadow = true;
|
||||||
|
world.add( circleCircum );
|
||||||
|
|
||||||
|
solids.push( cube, cubeLines, tetra, tetraLines, tetra2, tetraLines2, octa, octaLines, icosa, icosaLines, dodeca, dodecaLines, circleCircum, web, extraCircles );
|
||||||
|
|
||||||
|
|
||||||
|
gui = new GUI();
|
||||||
|
|
||||||
|
let guiDict = {};
|
||||||
|
|
||||||
|
let fire = gui.addFolder('Fire');
|
||||||
|
guiDict['fire'] = {'folder': fire, 'object': tetra, 'objectLines': tetraLines};
|
||||||
|
|
||||||
|
let fire2 = gui.addFolder('Fire 2');
|
||||||
|
guiDict['fire2'] = {'folder': fire2, 'object': tetra2, 'objectLines': tetraLines2};
|
||||||
|
|
||||||
|
let air = gui.addFolder('Air');
|
||||||
|
guiDict['air'] = {'folder': air, 'object': octa, 'objectLines': octaLines};
|
||||||
|
|
||||||
|
let earth = gui.addFolder('Earth');
|
||||||
|
guiDict['earth'] = {'folder': earth, 'object': cube, 'objectLines': cubeLines};
|
||||||
|
|
||||||
|
let water = gui.addFolder('Water');
|
||||||
|
guiDict['water'] = {'folder': water, 'object': icosa, 'objectLines': icosaLines};
|
||||||
|
|
||||||
|
let aether = gui.addFolder('Aether')
|
||||||
|
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 = {
|
||||||
|
show: true,
|
||||||
|
fill: true,
|
||||||
|
frame: true,
|
||||||
|
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)) {
|
||||||
|
// guiDict[fld]['folder'].close()
|
||||||
|
if (fld === "circles") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
folderStandard.show = true;
|
||||||
|
|
||||||
|
guiDict[fld]['folder'].add( folderStandard, 'scale', 0, 2 ).step(0.001).onChange( (value) => {
|
||||||
|
guiDict[fld]['object'].scale.x = value
|
||||||
|
guiDict[fld]['object'].scale.y = value
|
||||||
|
guiDict[fld]['object'].scale.z = value
|
||||||
|
guiDict[fld]['objectLines'].scale.x = value
|
||||||
|
guiDict[fld]['objectLines'].scale.y = value
|
||||||
|
guiDict[fld]['objectLines'].scale.z = value
|
||||||
|
});
|
||||||
|
|
||||||
|
guiDict[fld]['folder'].add( folderStandard, 'show', true).onChange((value) => {
|
||||||
|
if (value) {
|
||||||
|
guiDict[fld]['object'].visible = true;
|
||||||
|
guiDict[fld]['objectLines'].visible = true;
|
||||||
|
} else {
|
||||||
|
guiDict[fld]['object'].visible = false;
|
||||||
|
guiDict[fld]['objectLines'].visible = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
guiDict[fld]['folder'].add( folderStandard, 'fill', true).onChange((value) => {
|
||||||
|
if (value) {
|
||||||
|
guiDict[fld]['object'].visible = true;
|
||||||
|
} else {
|
||||||
|
guiDict[fld]['object'].visible = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
guiDict[fld]['folder'].add( folderStandard, 'frame', true).onChange((value) => {
|
||||||
|
if (value) {
|
||||||
|
guiDict[fld]['objectLines'].visible = true;
|
||||||
|
} else {
|
||||||
|
guiDict[fld]['objectLines'].visible = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
gui.add( params, 'radius', 0.5, 1.5).onChange( (value) => {
|
||||||
|
for (let sphere of spheres) {
|
||||||
|
world.remove(sphere)
|
||||||
|
}
|
||||||
|
for (let sphereLine of spheresLines) {
|
||||||
|
world.remove(sphereLine)
|
||||||
|
}
|
||||||
|
renderFlower (value)
|
||||||
|
});
|
||||||
|
gui.add( params, 'tube', 0.01, 0.1 ).step( 0.005 ).onChange( () => {
|
||||||
|
for (let sphere of spheres) {
|
||||||
|
world.remove(sphere)
|
||||||
|
}
|
||||||
|
for (let sphereLine of spheresLines) {
|
||||||
|
world.remove(sphereLine)
|
||||||
|
}
|
||||||
|
renderFlower (params['radius'])
|
||||||
|
});
|
||||||
|
|
||||||
|
// ?only=<fire|fire2|air|earth|water|aether> hides the other solids, for
|
||||||
|
// checking a single shape against the reference
|
||||||
|
const only = debugFlags.get( 'only' );
|
||||||
|
if ( only && guiDict[ only ] ) {
|
||||||
|
for ( let fld of Object.keys( guiDict ) ) {
|
||||||
|
if ( fld === only || ! guiDict[ fld ][ 'object' ] ) continue;
|
||||||
|
guiDict[ fld ][ 'object' ].visible = false;
|
||||||
|
guiDict[ fld ][ 'objectLines' ].visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gui.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFlower()
|
||||||
|
|
||||||
|
// 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 ) );
|
||||||
|
|
||||||
|
// legacy lighting was removed in three r155: intensities need the old value times pi
|
||||||
|
const spotLight = new THREE.DirectionalLight( colors.light, 1.5 * Math.PI );
|
||||||
|
spotLight.position.set( 10, 0, 0 );
|
||||||
|
spotLight.castShadow = true;
|
||||||
|
spotLight.shadow.mapSize.width = 2048;
|
||||||
|
spotLight.shadow.mapSize.height = 2048;
|
||||||
|
spotLight.shadow.camera.far = 21;
|
||||||
|
spotLight.shadow.camera.left = -10;
|
||||||
|
spotLight.shadow.camera.right = 10;
|
||||||
|
spotLight.shadow.camera.top = 10;
|
||||||
|
spotLight.shadow.camera.bottom = -10;
|
||||||
|
|
||||||
|
// spotLight.shadow.camera.fov = 35;
|
||||||
|
scene.add( spotLight );
|
||||||
|
|
||||||
|
// the wall itself is invisible: only the shadow "projection" shows, in light
|
||||||
|
// orange, floating on the gradient
|
||||||
|
const planeGeometry = new THREE.PlaneGeometry( 20, 20, 10, 10 );
|
||||||
|
const planeMaterial = new THREE.ShadowMaterial( { color: colors.shadow, opacity: 0.9 } )
|
||||||
|
const plane = new THREE.Mesh( planeGeometry, planeMaterial );
|
||||||
|
plane.rotation.y = Math.PI / 2;
|
||||||
|
plane.position.x = -10
|
||||||
|
plane.receiveShadow = true;
|
||||||
|
scene.add( plane );
|
||||||
|
|
||||||
|
//Create a helper for the shadow camera (optional)
|
||||||
|
// const helper = new THREE.CameraHelper( spotLight.shadow.camera );
|
||||||
|
// world.add( helper );
|
||||||
|
|
||||||
|
// const axesHelper = new THREE.AxesHelper( 40 );
|
||||||
|
// world.add( axesHelper );
|
||||||
|
|
||||||
|
|
||||||
|
// var axis = new THREE.Vector3(0, 1, 0);
|
||||||
|
|
||||||
|
function animate () {
|
||||||
|
requestAnimationFrame( animate );
|
||||||
|
|
||||||
|
const time = performance.now() / 1000;
|
||||||
|
|
||||||
|
// for (let sphere of spheres) {
|
||||||
|
// sphere.position.x = Math.cos( time ) * sphere.start_x;
|
||||||
|
// sphere.position.z = - Math.sin( time ) * sphere.start_z;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for (let sphereLine of spheresLines) {
|
||||||
|
// sphereLine.position.x = Math.cos( time ) * sphereLine.start_x;
|
||||||
|
// sphereLine.position.z = - Math.sin( time ) * sphereLine.start_z;
|
||||||
|
// }
|
||||||
|
|
||||||
|
updateWorld();
|
||||||
|
controls.update();
|
||||||
|
|
||||||
|
renderer.render( scene, camera );
|
||||||
|
};
|
||||||
|
|
||||||
|
animate();
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html oncontextmenu="return false;">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Golden Spiral</title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="./css/styles.scss">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="fade"></div>
|
||||||
|
<nav class="page-nav">
|
||||||
|
<a href="index.html">← Thrive</a>
|
||||||
|
<span>Golden Spiral</span>
|
||||||
|
</nav>
|
||||||
|
<script type="module" src="./phyllotaxis.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
|
||||||
|
|
||||||
|
import { createStage } from './lib/stage.js';
|
||||||
|
|
||||||
|
// Phyllotaxis: nature places seeds, leaves and florets one golden angle
|
||||||
|
// (137.5077...) apart. Nudge the angle a tenth of a degree and the pattern
|
||||||
|
// collapses into spokes - the irrationality of phi is what packs it perfectly.
|
||||||
|
const { scene, colors, onUpdate } = createStage();
|
||||||
|
|
||||||
|
const GOLDEN_ANGLE = 180 * ( 3 - Math.sqrt( 5 ) ); // 137.5077...
|
||||||
|
const MAX_COUNT = 3000;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
count: 800,
|
||||||
|
angle: GOLDEN_ANGLE,
|
||||||
|
form: 'sunflower',
|
||||||
|
grow: false,
|
||||||
|
spin: true,
|
||||||
|
resetAngle: () => {
|
||||||
|
params.angle = GOLDEN_ANGLE;
|
||||||
|
gui.controllersRecursive().forEach( ( c ) => c.updateDisplay() );
|
||||||
|
renderPattern();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const group = new THREE.Group();
|
||||||
|
// stand the disc up facing the light and camera, like the circle pages
|
||||||
|
group.rotation.y = Math.PI / 2;
|
||||||
|
scene.add( group );
|
||||||
|
|
||||||
|
const material = new THREE.MeshStandardMaterial( { metalness: 0.7, roughness: 0.3 } );
|
||||||
|
const mesh = new THREE.InstancedMesh( new THREE.SphereGeometry( 1, 16, 16 ), material, MAX_COUNT );
|
||||||
|
mesh.castShadow = true;
|
||||||
|
group.add( mesh );
|
||||||
|
|
||||||
|
const dummy = new THREE.Object3D();
|
||||||
|
const colour = new THREE.Color();
|
||||||
|
const inner = new THREE.Color( colors.fills.aether );
|
||||||
|
const outer = new THREE.Color( colors.fills.fire );
|
||||||
|
|
||||||
|
function renderPattern() {
|
||||||
|
const n = params.count;
|
||||||
|
const angle = params.angle * ( Math.PI / 180 );
|
||||||
|
|
||||||
|
for ( let i = 0; i < n; i++ ) {
|
||||||
|
const theta = i * angle;
|
||||||
|
const t = i / ( n - 1 );
|
||||||
|
|
||||||
|
if ( params.form === 'sunflower' ) {
|
||||||
|
// r ~ sqrt(i) keeps the seed density constant across the disc
|
||||||
|
const r = 5 * Math.sqrt( t );
|
||||||
|
dummy.position.set( r * Math.cos( theta ), r * Math.sin( theta ), 0 );
|
||||||
|
dummy.scale.setScalar( 0.11 );
|
||||||
|
} else if ( params.form === 'pinecone' ) {
|
||||||
|
const r = 3.5 * Math.sqrt( t );
|
||||||
|
dummy.position.set( r * Math.cos( theta ), r * Math.sin( theta ), 4 * ( 1 - t ) - 2 );
|
||||||
|
dummy.scale.setScalar( 0.13 );
|
||||||
|
} else {
|
||||||
|
// fibonacci sphere: even latitude steps + golden angle in longitude
|
||||||
|
// spreads points almost perfectly uniformly over a sphere
|
||||||
|
const y = 1 - 2 * t;
|
||||||
|
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.scale.setScalar( 0.12 );
|
||||||
|
}
|
||||||
|
|
||||||
|
dummy.updateMatrix();
|
||||||
|
mesh.setMatrixAt( i, dummy.matrix );
|
||||||
|
mesh.setColorAt( i, colour.lerpColors( inner, outer, t ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
mesh.count = n;
|
||||||
|
mesh.instanceMatrix.needsUpdate = true;
|
||||||
|
mesh.instanceColor.needsUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPattern();
|
||||||
|
|
||||||
|
onUpdate( ( time ) => {
|
||||||
|
if ( params.spin ) group.rotation.x = time * 0.1;
|
||||||
|
|
||||||
|
if ( params.grow ) {
|
||||||
|
// seeds appear one by one from the centre, the way the plant grows them
|
||||||
|
mesh.count = Math.min( params.count, Math.floor( ( time % 30 ) * 100 ) + 1 );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
const gui = new GUI();
|
||||||
|
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, 'resetAngle' ).name( 'reset to golden angle' );
|
||||||
|
gui.add( params, 'form', [ 'sunflower', 'pinecone', 'sphere' ] ).onChange( renderPattern );
|
||||||
|
gui.add( params, 'grow' ).onChange( () => { mesh.count = params.count; } );
|
||||||
|
gui.add( params, 'spin' );
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html oncontextmenu="return false;">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Seed of Life</title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="./css/styles.scss">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="fade"></div>
|
||||||
|
<nav class="page-nav">
|
||||||
|
<a href="index.html">← Thrive</a>
|
||||||
|
<span>Seed of Life</span>
|
||||||
|
</nav>
|
||||||
|
<script type="module" src="./seed.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+48
-39
@@ -4,12 +4,19 @@ import * as THREE from 'three';
|
|||||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||||
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
|
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
|
||||||
|
|
||||||
import Stats from 'three/addons/libs/stats.module.js';
|
|
||||||
|
|
||||||
import './css/styles.scss';
|
import './lib/chrome.js';
|
||||||
|
import { sceneColors } from './lib/theme.js';
|
||||||
|
import { bindWorld } from './lib/transit.js';
|
||||||
|
|
||||||
|
const colors = sceneColors();
|
||||||
|
|
||||||
|
// the landing gradient shows through a transparent canvas; all shapes live in
|
||||||
|
// `world`, which page transitions fly in and out
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
scene.background = new THREE.Color(0xf44f04);
|
const world = new THREE.Group();
|
||||||
|
scene.add( world );
|
||||||
|
const updateWorld = bindWorld( world );
|
||||||
|
|
||||||
|
|
||||||
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 );
|
||||||
@@ -18,7 +25,7 @@ camera.position.x = 10;
|
|||||||
camera.position.y = 4;
|
camera.position.y = 4;
|
||||||
|
|
||||||
|
|
||||||
const renderer = new THREE.WebGLRenderer( { antialias: true } );
|
const renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } );
|
||||||
renderer.setPixelRatio( Math.min( window.devicePixelRatio, 2 ) );
|
renderer.setPixelRatio( Math.min( window.devicePixelRatio, 2 ) );
|
||||||
renderer.setSize( window.innerWidth, window.innerHeight );
|
renderer.setSize( window.innerWidth, window.innerHeight );
|
||||||
renderer.shadowMap.enabled = true;
|
renderer.shadowMap.enabled = true;
|
||||||
@@ -42,13 +49,13 @@ controls.target.set( -4, 0, 0 );
|
|||||||
// const torus = new THREE.Mesh( geometry_torus, material_torus );
|
// const torus = new THREE.Mesh( geometry_torus, material_torus );
|
||||||
// torus.rotation.x = Math.PI / 2;
|
// torus.rotation.x = Math.PI / 2;
|
||||||
// torus.castShadow = true;
|
// torus.castShadow = true;
|
||||||
// scene.add( torus );
|
// world.add( torus );
|
||||||
|
|
||||||
// const icoGeometry = new THREE.IcosahedronGeometry( 5 );
|
// const icoGeometry = new THREE.IcosahedronGeometry( 5 );
|
||||||
// const icoMaterial= new THREE.MeshBasicMaterial( {color: 0x049ef4, wireframe: true, wireframeLinewidth: 8 } );
|
// const icoMaterial= new THREE.MeshBasicMaterial( {color: 0x049ef4, wireframe: true, wireframeLinewidth: 8 } );
|
||||||
// const iso = new THREE.Mesh( icoGeometry, icoMaterial );
|
// const iso = new THREE.Mesh( icoGeometry, icoMaterial );
|
||||||
// iso.castShadow = true;
|
// iso.castShadow = true;
|
||||||
// scene.add( iso );
|
// world.add( iso );
|
||||||
|
|
||||||
// --------- Mekaraba ---------- //
|
// --------- Mekaraba ---------- //
|
||||||
// const tetraGeometry = new THREE.TetrahedronGeometry( 2 );
|
// const tetraGeometry = new THREE.TetrahedronGeometry( 2 );
|
||||||
@@ -58,7 +65,7 @@ controls.target.set( -4, 0, 0 );
|
|||||||
|
|
||||||
// // tetra.rotation.x = Math.PI / 4 ;
|
// // tetra.rotation.x = Math.PI / 4 ;
|
||||||
// tetra.rotation.z = Math.PI / 6 ;
|
// tetra.rotation.z = Math.PI / 6 ;
|
||||||
// // scene.add( tetra );
|
// // world.add( tetra );
|
||||||
|
|
||||||
// const tetraEdges = new THREE.EdgesGeometry( tetraGeometry );
|
// const tetraEdges = new THREE.EdgesGeometry( tetraGeometry );
|
||||||
// const tetraEdgesLine = new THREE.LineSegments( tetraEdges, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 3 } ) );
|
// const tetraEdgesLine = new THREE.LineSegments( tetraEdges, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 3 } ) );
|
||||||
@@ -69,7 +76,7 @@ controls.target.set( -4, 0, 0 );
|
|||||||
// // tetraEdgesLine.rotation.y = Math.PI / 6;
|
// // tetraEdgesLine.rotation.y = Math.PI / 6;
|
||||||
|
|
||||||
// tetraEdgesLine.castShadow = true;
|
// tetraEdgesLine.castShadow = true;
|
||||||
// scene.add( tetraEdgesLine );
|
// world.add( tetraEdgesLine );
|
||||||
|
|
||||||
// const tetraGeometry2 = new THREE.TetrahedronGeometry( 2 );
|
// const tetraGeometry2 = new THREE.TetrahedronGeometry( 2 );
|
||||||
// const tetraMaterial2 = new THREE.MeshBasicMaterial( {color: 0xffff00 } );
|
// const tetraMaterial2 = new THREE.MeshBasicMaterial( {color: 0xffff00 } );
|
||||||
@@ -78,7 +85,7 @@ controls.target.set( -4, 0, 0 );
|
|||||||
// tetra2.rotation.x = Math.PI / 4 ;
|
// tetra2.rotation.x = Math.PI / 4 ;
|
||||||
// // tetra2.rotation.y = Math.PI / 4 ;
|
// // tetra2.rotation.y = Math.PI / 4 ;
|
||||||
// tetra2.rotation.z = Math.PI / 4 ;
|
// tetra2.rotation.z = Math.PI / 4 ;
|
||||||
// // scene.add( tetra2 );
|
// // world.add( tetra2 );
|
||||||
|
|
||||||
// const tetraEdges2 = new THREE.EdgesGeometry( tetraGeometry2 );
|
// const tetraEdges2 = new THREE.EdgesGeometry( tetraGeometry2 );
|
||||||
// const tetraEdgesLine2 = new THREE.LineSegments( tetraEdges2, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 3 } ) );
|
// const tetraEdgesLine2 = new THREE.LineSegments( tetraEdges2, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 3 } ) );
|
||||||
@@ -87,7 +94,7 @@ controls.target.set( -4, 0, 0 );
|
|||||||
// tetraEdgesLine2.rotation.x = Math.PI / 4 ;
|
// tetraEdgesLine2.rotation.x = Math.PI / 4 ;
|
||||||
// // tetraEdgesLine2.rotation.y = Math.PI / 4 ;
|
// // tetraEdgesLine2.rotation.y = Math.PI / 4 ;
|
||||||
// tetraEdgesLine2.rotation.z = Math.PI / 4 ;
|
// tetraEdgesLine2.rotation.z = Math.PI / 4 ;
|
||||||
// scene.add( tetraEdgesLine2 );
|
// world.add( tetraEdgesLine2 );
|
||||||
|
|
||||||
|
|
||||||
// ---------- David ------------ //
|
// ---------- David ------------ //
|
||||||
@@ -96,14 +103,14 @@ controls.target.set( -4, 0, 0 );
|
|||||||
// const tetra = new THREE.Mesh( tetraGeometry, tetraMaterial );
|
// const tetra = new THREE.Mesh( tetraGeometry, tetraMaterial );
|
||||||
// tetra.rotation.y = Math.PI / 6 ;
|
// tetra.rotation.y = Math.PI / 6 ;
|
||||||
// tetra.position.y = 1 / 2
|
// tetra.position.y = 1 / 2
|
||||||
// scene.add( tetra );
|
// world.add( tetra );
|
||||||
|
|
||||||
// const tetraEdges = new THREE.EdgesGeometry( tetraGeometry );
|
// const tetraEdges = new THREE.EdgesGeometry( tetraGeometry );
|
||||||
// const tetraEdgesLine = new THREE.LineSegments( tetraEdges, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 15 } ) );
|
// const tetraEdgesLine = new THREE.LineSegments( tetraEdges, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 15 } ) );
|
||||||
// tetraEdgesLine.rotation.y = Math.PI / 6 ;
|
// tetraEdgesLine.rotation.y = Math.PI / 6 ;
|
||||||
// tetraEdgesLine.position.y = 1 / 2
|
// tetraEdgesLine.position.y = 1 / 2
|
||||||
// tetraEdgesLine.castShadow = true;
|
// tetraEdgesLine.castShadow = true;
|
||||||
// scene.add( tetraEdgesLine );
|
// world.add( tetraEdgesLine );
|
||||||
|
|
||||||
// const tetraGeometry2 = new THREE.ConeGeometry( 2, 3, 3 );
|
// const tetraGeometry2 = new THREE.ConeGeometry( 2, 3, 3 );
|
||||||
// const tetraMaterial2 = new THREE.MeshBasicMaterial( {color: 0xffff00 } );
|
// const tetraMaterial2 = new THREE.MeshBasicMaterial( {color: 0xffff00 } );
|
||||||
@@ -111,7 +118,7 @@ controls.target.set( -4, 0, 0 );
|
|||||||
// tetra2.rotation.y = Math.PI + Math.PI / 6 ;
|
// tetra2.rotation.y = Math.PI + Math.PI / 6 ;
|
||||||
// tetra2.rotation.z = Math.PI;
|
// tetra2.rotation.z = Math.PI;
|
||||||
// tetra2.position.y = - 1 / 2
|
// tetra2.position.y = - 1 / 2
|
||||||
// scene.add( tetra2 );
|
// world.add( tetra2 );
|
||||||
|
|
||||||
// const tetraEdges2 = new THREE.EdgesGeometry( tetraGeometry2 );
|
// const tetraEdges2 = new THREE.EdgesGeometry( tetraGeometry2 );
|
||||||
// const tetraEdgesLine2 = new THREE.LineSegments( tetraEdges2, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 15 } ) );
|
// const tetraEdgesLine2 = new THREE.LineSegments( tetraEdges2, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 15 } ) );
|
||||||
@@ -119,7 +126,7 @@ controls.target.set( -4, 0, 0 );
|
|||||||
// tetraEdgesLine2.rotation.y = Math.PI + Math.PI / 6 ;
|
// tetraEdgesLine2.rotation.y = Math.PI + Math.PI / 6 ;
|
||||||
// tetraEdgesLine2.rotation.z = Math.PI;
|
// tetraEdgesLine2.rotation.z = Math.PI;
|
||||||
// tetraEdgesLine2.position.y = - 1 / 2
|
// tetraEdgesLine2.position.y = - 1 / 2
|
||||||
// scene.add( tetraEdgesLine2 );
|
// world.add( tetraEdgesLine2 );
|
||||||
|
|
||||||
// ---------- Flower of life ------------ //
|
// ---------- Flower of life ------------ //
|
||||||
|
|
||||||
@@ -127,11 +134,11 @@ controls.target.set( -4, 0, 0 );
|
|||||||
// const circleMaterial = new THREE.MeshBasicMaterial( { color: 0x049ef4, side: THREE.DoubleSide } );
|
// const circleMaterial = new THREE.MeshBasicMaterial( { color: 0x049ef4, side: THREE.DoubleSide } );
|
||||||
// const circle = new THREE.Mesh( circleGeometry, circleMaterial );
|
// const circle = new THREE.Mesh( circleGeometry, circleMaterial );
|
||||||
// circle.castShadow = true;
|
// circle.castShadow = true;
|
||||||
// scene.add( circle );
|
// world.add( circle );
|
||||||
|
|
||||||
// const circleLine = new THREE.LineSegments( circleGeometry, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 3 } ) );
|
// const circleLine = new THREE.LineSegments( circleGeometry, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 3 } ) );
|
||||||
// circleLine.castShadow = true;
|
// circleLine.castShadow = true;
|
||||||
// scene.add( circleLine );
|
// world.add( circleLine );
|
||||||
|
|
||||||
let rotatePoint = (cx, cy, angle, px, py) => {
|
let rotatePoint = (cx, cy, angle, px, py) => {
|
||||||
angle = - angle * (Math.PI / 180)
|
angle = - angle * (Math.PI / 180)
|
||||||
@@ -229,7 +236,7 @@ let renderFlower = (size = 3, radius = 1) => {
|
|||||||
sphere.position.x = xtemp2;
|
sphere.position.x = xtemp2;
|
||||||
sphere.position.y = ytemp2;
|
sphere.position.y = ytemp2;
|
||||||
sphere.position.z = ztemp2;
|
sphere.position.z = ztemp2;
|
||||||
scene.add( sphere );
|
world.add( sphere );
|
||||||
|
|
||||||
let circleGeometry = new THREE.TorusGeometry( radius, 0.02, 64, 64 );
|
let circleGeometry = new THREE.TorusGeometry( radius, 0.02, 64, 64 );
|
||||||
let circleMaterial = new THREE.MeshBasicMaterial( {
|
let circleMaterial = new THREE.MeshBasicMaterial( {
|
||||||
@@ -243,7 +250,7 @@ let renderFlower = (size = 3, radius = 1) => {
|
|||||||
circleCircum.position.x = xtemp2;
|
circleCircum.position.x = xtemp2;
|
||||||
circleCircum.position.y = ytemp2;
|
circleCircum.position.y = ytemp2;
|
||||||
circleCircum.position.z = ztemp2;
|
circleCircum.position.z = ztemp2;
|
||||||
scene.add( circleCircum );
|
world.add( circleCircum );
|
||||||
|
|
||||||
sphere.start_x = x;
|
sphere.start_x = x;
|
||||||
sphere.start_y = y;
|
sphere.start_y = y;
|
||||||
@@ -269,7 +276,7 @@ let renderFlower = (size = 3, radius = 1) => {
|
|||||||
circleCircum = new THREE.Mesh( circleGeometry, circleMaterial );
|
circleCircum = new THREE.Mesh( circleGeometry, circleMaterial );
|
||||||
circleCircum.rotation.y = Math.PI / 2;
|
circleCircum.rotation.y = Math.PI / 2;
|
||||||
circleCircum.castShadow = true;
|
circleCircum.castShadow = true;
|
||||||
scene.add( circleCircum );
|
world.add( circleCircum );
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderFlower()
|
// renderFlower()
|
||||||
@@ -294,7 +301,7 @@ for (let x_angle of Array.from(Array(4), (_, i) => 360 / 4 * i)) {
|
|||||||
|
|
||||||
let sphereGeometry = new THREE.SphereGeometry( 0.5, 64, 64 );
|
let sphereGeometry = new THREE.SphereGeometry( 0.5, 64, 64 );
|
||||||
let sphereMaterial = new THREE.MeshBasicMaterial( {
|
let sphereMaterial = new THREE.MeshBasicMaterial( {
|
||||||
color: 0x0063e4,
|
color: colors.fills.sphere,
|
||||||
metalness: 1,
|
metalness: 1,
|
||||||
roughness: 1,
|
roughness: 1,
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
@@ -304,11 +311,11 @@ for (let x_angle of Array.from(Array(4), (_, i) => 360 / 4 * i)) {
|
|||||||
sphere.position.x = x;
|
sphere.position.x = x;
|
||||||
sphere.position.y = y;
|
sphere.position.y = y;
|
||||||
sphere.position.z = z;
|
sphere.position.z = z;
|
||||||
scene.add( sphere );
|
world.add( sphere );
|
||||||
|
|
||||||
let circleGeometry = new THREE.TorusGeometry( 0.5, 0.01, 64, 64);
|
let circleGeometry = new THREE.TorusGeometry( 0.5, 0.01, 64, 64);
|
||||||
let circleMaterial = new THREE.MeshBasicMaterial( {
|
let circleMaterial = new THREE.MeshBasicMaterial( {
|
||||||
color: 0x000000,
|
color: colors.line,
|
||||||
opacity: 0.1,
|
opacity: 0.1,
|
||||||
transparent: true
|
transparent: true
|
||||||
} );
|
} );
|
||||||
@@ -318,14 +325,17 @@ for (let x_angle of Array.from(Array(4), (_, i) => 360 / 4 * i)) {
|
|||||||
circleCircum.position.x = x;
|
circleCircum.position.x = x;
|
||||||
circleCircum.position.y = y;
|
circleCircum.position.y = y;
|
||||||
circleCircum.position.z = z;
|
circleCircum.position.z = z;
|
||||||
scene.add( circleCircum );
|
world.add( circleCircum );
|
||||||
}
|
}
|
||||||
z_count += 1;
|
z_count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 ) );
|
||||||
|
|
||||||
// legacy lighting was removed in three r155: intensities need the old value times pi
|
// legacy lighting was removed in three r155: intensities need the old value times pi
|
||||||
const spotLight = new THREE.DirectionalLight( 0xeaa7a7, 1.5 * Math.PI );
|
const spotLight = new THREE.DirectionalLight( colors.light, 1.5 * Math.PI );
|
||||||
spotLight.position.set( 10, 0, 0 );
|
spotLight.position.set( 10, 0, 0 );
|
||||||
spotLight.castShadow = true;
|
spotLight.castShadow = true;
|
||||||
spotLight.shadow.mapSize.width = 2048;
|
spotLight.shadow.mapSize.width = 2048;
|
||||||
@@ -339,11 +349,12 @@ spotLight.shadow.camera.bottom = -10;
|
|||||||
// spotLight.shadow.camera.fov = 35;
|
// spotLight.shadow.camera.fov = 35;
|
||||||
scene.add( spotLight );
|
scene.add( spotLight );
|
||||||
|
|
||||||
//Create a plane that receives shadows (but does not cast them)
|
// the wall itself is invisible: only the shadow "projection" shows, in light
|
||||||
|
// orange, floating on the gradient; front side only so the shapes stay fully
|
||||||
|
// visible when orbiting behind it
|
||||||
const planeGeometry = new THREE.PlaneGeometry( 20, 20, 10, 10 );
|
const planeGeometry = new THREE.PlaneGeometry( 20, 20, 10, 10 );
|
||||||
const planeMaterial = new THREE.MeshStandardMaterial( { color: 0xffffff } )
|
const planeMaterial = new THREE.ShadowMaterial( { color: colors.shadow, opacity: 0.9 } )
|
||||||
const plane = new THREE.Mesh( planeGeometry, planeMaterial );
|
const plane = new THREE.Mesh( planeGeometry, planeMaterial );
|
||||||
planeMaterial.side = THREE.DoubleSide;
|
|
||||||
plane.rotation.y = Math.PI / 2;
|
plane.rotation.y = Math.PI / 2;
|
||||||
plane.position.x = -10
|
plane.position.x = -10
|
||||||
plane.receiveShadow = true;
|
plane.receiveShadow = true;
|
||||||
@@ -351,13 +362,13 @@ scene.add( plane );
|
|||||||
|
|
||||||
//Create a helper for the shadow camera (optional)
|
//Create a helper for the shadow camera (optional)
|
||||||
const helper = new THREE.CameraHelper( spotLight.shadow.camera );
|
const helper = new THREE.CameraHelper( spotLight.shadow.camera );
|
||||||
// scene.add( helper );
|
// world.add( helper );
|
||||||
|
|
||||||
const axesHelper = new THREE.AxesHelper( 40 );
|
const axesHelper = new THREE.AxesHelper( 40 );
|
||||||
scene.add( axesHelper );
|
// scene.add( axesHelper );
|
||||||
|
|
||||||
// const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 0.2 );
|
// const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 0.2 );
|
||||||
// scene.add( light );
|
// world.add( light );
|
||||||
|
|
||||||
// const gui = new GUI();
|
// const gui = new GUI();
|
||||||
|
|
||||||
@@ -390,28 +401,26 @@ scene.add( axesHelper );
|
|||||||
// });
|
// });
|
||||||
// gui.add( params, 'size', 2, 5).step(1).onChange( (value) => {
|
// gui.add( params, 'size', 2, 5).step(1).onChange( (value) => {
|
||||||
// for (let sphere of spheres) {
|
// for (let sphere of spheres) {
|
||||||
// scene.remove(sphere)
|
// world.remove(sphere)
|
||||||
// }
|
// }
|
||||||
// for (let sphereLine of spheresLines) {
|
// for (let sphereLine of spheresLines) {
|
||||||
// scene.remove(sphereLine)
|
// world.remove(sphereLine)
|
||||||
// }
|
// }
|
||||||
// scene.remove(circleCircum)
|
// world.remove(circleCircum)
|
||||||
// renderFlower (value, params['radius'])
|
// renderFlower (value, params['radius'])
|
||||||
// });
|
// });
|
||||||
// gui.add( params, 'radius', 0.5, 3).onChange( (value) => {
|
// gui.add( params, 'radius', 0.5, 3).onChange( (value) => {
|
||||||
// for (let sphere of spheres) {
|
// for (let sphere of spheres) {
|
||||||
// scene.remove(sphere)
|
// world.remove(sphere)
|
||||||
// }
|
// }
|
||||||
// for (let sphereLine of spheresLines) {
|
// for (let sphereLine of spheresLines) {
|
||||||
// scene.remove(sphereLine)
|
// world.remove(sphereLine)
|
||||||
// }
|
// }
|
||||||
// scene.remove(circleCircum)
|
// world.remove(circleCircum)
|
||||||
// renderFlower (params['size'], value)
|
// renderFlower (params['size'], value)
|
||||||
// });
|
// });
|
||||||
// gui.close();
|
// gui.close();
|
||||||
|
|
||||||
let stats = new Stats();
|
|
||||||
document.body.appendChild( stats.dom );
|
|
||||||
|
|
||||||
// var axis = new THREE.Vector3(0, 1, 0);
|
// var axis = new THREE.Vector3(0, 1, 0);
|
||||||
|
|
||||||
@@ -430,8 +439,8 @@ function animate () {
|
|||||||
// sphereLine.position.z = - Math.sin( time ) * sphereLine.start_z;
|
// sphereLine.position.z = - Math.sin( time ) * sphereLine.start_z;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
updateWorld();
|
||||||
controls.update();
|
controls.update();
|
||||||
stats.update();
|
|
||||||
|
|
||||||
renderer.render( scene, camera );
|
renderer.render( scene, camera );
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html oncontextmenu="return false;">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Toroidal Field</title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="./css/styles.scss">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="fade"></div>
|
||||||
|
<nav class="page-nav">
|
||||||
|
<a href="index.html">← Thrive</a>
|
||||||
|
<span>Toroidal Field</span>
|
||||||
|
</nav>
|
||||||
|
<script type="module" src="./torus.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
|
||||||
|
|
||||||
|
import { createStage } from './lib/stage.js';
|
||||||
|
|
||||||
|
// The torus: the self-sustaining "energy field" form - field lines spiral around
|
||||||
|
// the surface, through the centre and back around the outside.
|
||||||
|
const { scene, colors, onUpdate } = createStage();
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
radius: 3,
|
||||||
|
tube: 1.4,
|
||||||
|
strands: 12,
|
||||||
|
windings: 4,
|
||||||
|
surface: true,
|
||||||
|
spin: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const group = new THREE.Group();
|
||||||
|
// face the camera the same way the circle pages do
|
||||||
|
group.rotation.y = Math.PI / 2;
|
||||||
|
scene.add( group );
|
||||||
|
|
||||||
|
let parts = [];
|
||||||
|
let particles = [];
|
||||||
|
let curves = [];
|
||||||
|
|
||||||
|
// point on the torus surface: phi around the main axis, theta around the tube
|
||||||
|
const torusPoint = ( phi, theta, R, r ) =>
|
||||||
|
new THREE.Vector3(
|
||||||
|
( R + r * Math.cos( theta ) ) * Math.cos( phi ),
|
||||||
|
( R + r * Math.cos( theta ) ) * Math.sin( phi ),
|
||||||
|
r * Math.sin( theta )
|
||||||
|
);
|
||||||
|
|
||||||
|
function renderTorus() {
|
||||||
|
for ( const part of parts ) group.remove( part );
|
||||||
|
parts = [];
|
||||||
|
particles = [];
|
||||||
|
curves = [];
|
||||||
|
|
||||||
|
const R = params.radius;
|
||||||
|
const r = params.tube;
|
||||||
|
|
||||||
|
const surfaceGeometry = new THREE.TorusGeometry( R, r, 48, 128 );
|
||||||
|
const surfaceMaterial = new THREE.MeshStandardMaterial( {
|
||||||
|
color: colors.fills.fire,
|
||||||
|
metalness: 0.7,
|
||||||
|
roughness: 0.3,
|
||||||
|
opacity: 0.35,
|
||||||
|
transparent: true,
|
||||||
|
} );
|
||||||
|
// the surface casts no shadow: only the strands project, as outlines
|
||||||
|
const surface = new THREE.Mesh( surfaceGeometry, surfaceMaterial );
|
||||||
|
surface.visible = params.surface;
|
||||||
|
group.add( surface );
|
||||||
|
parts.push( surface );
|
||||||
|
|
||||||
|
const lineMaterial = new THREE.MeshBasicMaterial( { color: colors.line, opacity: 0.5, transparent: true } );
|
||||||
|
const particleMaterial = new THREE.MeshBasicMaterial( { color: colors.fills.aether } );
|
||||||
|
const particleGeometry = new THREE.SphereGeometry( 0.08, 16, 16 );
|
||||||
|
|
||||||
|
// each field line goes once around the main axis while winding `windings`
|
||||||
|
// times around the tube, each strand offset around the ring
|
||||||
|
for ( let s = 0; s < params.strands; s++ ) {
|
||||||
|
const offset = ( s / params.strands ) * Math.PI * 2;
|
||||||
|
const samples = [];
|
||||||
|
|
||||||
|
for ( let i = 0; i <= 256; i++ ) {
|
||||||
|
const t = i / 256;
|
||||||
|
samples.push( torusPoint( t * Math.PI * 2 + offset, t * Math.PI * 2 * params.windings, R, r ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
const curve = new THREE.CatmullRomCurve3( samples, true );
|
||||||
|
const strand = new THREE.Mesh( new THREE.TubeGeometry( curve, 384, 0.025, 8, true ), lineMaterial );
|
||||||
|
strand.castShadow = true;
|
||||||
|
group.add( strand );
|
||||||
|
parts.push( strand );
|
||||||
|
curves.push( curve );
|
||||||
|
|
||||||
|
const particle = new THREE.Mesh( particleGeometry, particleMaterial );
|
||||||
|
particle.userData.phase = s / params.strands;
|
||||||
|
group.add( particle );
|
||||||
|
parts.push( particle );
|
||||||
|
particles.push( particle );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTorus();
|
||||||
|
|
||||||
|
onUpdate( ( time ) => {
|
||||||
|
if ( params.spin ) group.rotation.z = time * 0.1;
|
||||||
|
|
||||||
|
for ( let i = 0; i < particles.length; i++ ) {
|
||||||
|
const t = ( time * 0.05 + particles[ i ].userData.phase ) % 1;
|
||||||
|
particles[ i ].position.copy( curves[ i ].getPointAt( t ) );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
const gui = new GUI();
|
||||||
|
gui.add( params, 'radius', 1, 5 ).step( 0.1 ).onChange( renderTorus );
|
||||||
|
gui.add( params, 'tube', 0.2, 3 ).step( 0.1 ).onChange( renderTorus );
|
||||||
|
gui.add( params, 'strands', 1, 32 ).step( 1 ).onChange( renderTorus );
|
||||||
|
gui.add( params, 'windings', 1, 12 ).step( 1 ).onChange( renderTorus );
|
||||||
|
gui.add( params, 'surface' ).onChange( ( value ) => {
|
||||||
|
parts[ 0 ].visible = value;
|
||||||
|
} );
|
||||||
|
gui.add( params, 'spin' );
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { resolve } from 'path';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
// one html entry per exploration; add new pages here and in src/
|
||||||
|
const pages = ['index', 'metatron', 'torus', 'matrix', 'phyllotaxis', 'flower', 'seed'];
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
root: 'src',
|
||||||
|
base: './',
|
||||||
|
build: {
|
||||||
|
outDir: '../build',
|
||||||
|
emptyOutDir: true,
|
||||||
|
rollupOptions: {
|
||||||
|
input: Object.fromEntries(pages.map((page) => [page, resolve(__dirname, `src/${page}.html`)])),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
|
|
||||||
module.exports = (env) => {
|
|
||||||
return {
|
|
||||||
mode: env.mode,
|
|
||||||
devtool: 'inline-source-map',
|
|
||||||
devServer: {
|
|
||||||
static: './dist',
|
|
||||||
hot: true,
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.css$/i,
|
|
||||||
use: [
|
|
||||||
// Creates `style` nodes from JS strings
|
|
||||||
"style-loader",
|
|
||||||
// Translates CSS into CommonJS
|
|
||||||
"css-loader",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.s[ac]ss$/i,
|
|
||||||
use: [
|
|
||||||
// Creates `style` nodes from JS strings
|
|
||||||
"style-loader",
|
|
||||||
// Translates CSS into CommonJS
|
|
||||||
"css-loader",
|
|
||||||
// Compiles Sass to CSS
|
|
||||||
"sass-loader",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(jpe?g|png|gif|svg)$/i,
|
|
||||||
type: 'asset/resource',
|
|
||||||
generator: {
|
|
||||||
filename: 'public/icons/[name][ext]'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
title: 'Thrive',
|
|
||||||
template: 'src/index.html'
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
output: {
|
|
||||||
filename: '[name].bundle.js',
|
|
||||||
path: path.resolve(__dirname, 'build'),
|
|
||||||
clean: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user