// 3D Flow Field
//
// A smooth 3D vector field built from sin/cos of offset coordinates.
// Thousands of particles advect through the field inside a [-1,1]^3 bounding
// box, wrapping at the boundaries. Their collective drift reveals the invisible
// field structure — orbit the camera to explore from any angle.
import * as THREE from 'three';
export const WARMUP = { framesBeforeReady: 480 };
export const PARAMS = {
particles: { value: 3000, min: 500, max: 12000, step: 100, label: "Particles", folder: "Structure", rebuildOnChange: true },
noiseScale: { value: 0.4, min: 0.05, max: 2, step: 0.01, label: "Field Frequency", folder: "Behavior" },
speed: { value: 0.6, min: 0.05, max: 3, step: 0.05, label: "Speed", folder: "Behavior" },
pointSize: { value: 0.04, min: 0.005, max: 0.2, step: 0.005, label: "Point Size", folder: "Appearance" },
hue: { value: 220, min: 0, max: 360, step: 1, label: "Base Hue", folder: "Appearance" },
};
export const SHARE = {
bookmarked: ["particles", "noiseScale", "speed", "pointSize", "hue"],
};
// Smooth deterministic 3D vector field via sin/cos combinations with offsets.
// Produces curl-like flow without needing a noise library.
function vectorField(x, y, z, scale) {
const s = scale;
const fx = Math.sin(y * s * 1.7) * Math.cos(z * s * 1.3) - Math.sin(x * s);
const fy = Math.sin(z * s * 1.9) * Math.cos(x * s * 1.5) - Math.sin(y * s);
const fz = Math.sin(x * s * 2.1) * Math.cos(y * s * 1.1) - Math.sin(z * s);
return [fx, fy, fz];
}
// Wrap a value into [-bound, bound]
function wrap(v, bound) {
const range = bound * 2;
return ((v + bound) % range + range) % range - bound;
}
export function sceneSetup(THREE, scene, camera, renderer, params, seed) {
scene.background = new THREE.Color(0x07080a);
camera.position.set(0, 0, 3.5);
camera.lookAt(0, 0, 0);
const n = params.particles | 0;
// Positions: current particle locations
const positions = new Float32Array(n * 3);
// Colors: per-particle HSL-derived color
const colors = new Float32Array(n * 3);
const hueBase = params.hue / 360;
for (let i = 0; i < n; i++) {
// Random start in [-1, 1]^3
positions[i * 3] = Math.random() * 2 - 1;
positions[i * 3 + 1] = Math.random() * 2 - 1;
positions[i * 3 + 2] = Math.random() * 2 - 1;
// Vary hue slightly per particle for depth
const hueOffset = (Math.random() - 0.5) * 0.15;
const col = new THREE.Color().setHSL(
((hueBase + hueOffset) % 1 + 1) % 1,
0.8,
0.55
);
colors[i * 3] = col.r;
colors[i * 3 + 1] = col.g;
colors[i * 3 + 2] = col.b;
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const mat = new THREE.PointsMaterial({
size: params.pointSize,
sizeAttenuation: true,
vertexColors: true,
transparent: true,
opacity: 0.85,
});
const points = new THREE.Points(geo, mat);
scene.add(points);
return { positions, colors, geo, mat, points, n };
}
export function sceneAnimate(THREE, scene, camera, state, params, time, delta) {
const { positions, colors, geo, mat, n } = state;
// Update material properties live
mat.size = params.pointSize;
const speed = params.speed * 0.01; // scale down to world units
const scale = params.noiseScale;
const hueBase = params.hue / 360;
const BOUND = 1.0;
for (let i = 0; i < n; i++) {
const base = i * 3;
let x = positions[base];
let y = positions[base + 1];
let z = positions[base + 2];
const [fx, fy, fz] = vectorField(x, y, z, scale);
// Normalize to get consistent speed regardless of field magnitude
const mag = Math.sqrt(fx * fx + fy * fy + fz * fz) || 1;
x += (fx / mag) * speed;
y += (fy / mag) * speed;
z += (fz / mag) * speed;
// Wrap into bounding box
positions[base] = wrap(x, BOUND);
positions[base + 1] = wrap(y, BOUND);
positions[base + 2] = wrap(z, BOUND);
// Recolor periodically based on current hue param + position for variety
const hueOffset = (positions[base] + positions[base + 1]) * 0.05;
const col = new THREE.Color().setHSL(
((hueBase + hueOffset) % 1 + 1) % 1,
0.8,
0.5 + positions[base + 2] * 0.15
);
colors[base] = col.r;
colors[base + 1] = col.g;
colors[base + 2] = col.b;
}
geo.attributes.position.needsUpdate = true;
geo.attributes.color.needsUpdate = true;
}