Skip to content

Instantly share code, notes, and snippets.

@AgentO3
Created December 14, 2025 13:56
Show Gist options
  • Select an option

  • Save AgentO3/d219f71d08903464ed1775400dc1460d to your computer and use it in GitHub Desktop.

Select an option

Save AgentO3/d219f71d08903464ed1775400dc1460d to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Clarity from Constraint - Generative Art</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background: #f8f8f6;
}
canvas {
display: block;
}
</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
// Scene setup
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf8f8f6);
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 50;
camera.position.y = 0;
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
// Color palette matching the reference image
const colorStops = [
{ pos: 0.0, color: new THREE.Color(0xd4755a) }, // Warm coral/terracotta
{ pos: 0.15, color: new THREE.Color(0xe8a090) }, // Soft coral
{ pos: 0.3, color: new THREE.Color(0xf0c4b8) }, // Light pink/peach
{ pos: 0.45, color: new THREE.Color(0xd8c0c8) }, // Dusty rose
{ pos: 0.55, color: new THREE.Color(0xb0a8b8) }, // Mauve gray
{ pos: 0.7, color: new THREE.Color(0x8898a8) }, // Cool slate
{ pos: 0.85, color: new THREE.Color(0x6080a0) }, // Steel blue
{ pos: 1.0, color: new THREE.Color(0x506878) } // Deep slate blue
];
function getColorAtPosition(t) {
t = Math.max(0, Math.min(1, t));
for (let i = 0; i < colorStops.length - 1; i++) {
if (t >= colorStops[i].pos && t <= colorStops[i + 1].pos) {
const localT = (t - colorStops[i].pos) / (colorStops[i + 1].pos - colorStops[i].pos);
const color = new THREE.Color();
color.lerpColors(colorStops[i].color, colorStops[i + 1].color, localT);
return color;
}
}
return colorStops[colorStops.length - 1].color.clone();
}
// Noise function for organic movement
function noise(x, y, z) {
const p = [151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180];
const perm = [...p, ...p];
function fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); }
function lerp(a, b, t) { return a + t * (b - a); }
function grad(hash, x, y, z) {
const h = hash & 15;
const u = h < 8 ? x : y;
const v = h < 4 ? y : h === 12 || h === 14 ? x : z;
return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
}
const X = Math.floor(x) & 255, Y = Math.floor(y) & 255, Z = Math.floor(z) & 255;
x -= Math.floor(x); y -= Math.floor(y); z -= Math.floor(z);
const u = fade(x), v = fade(y), w = fade(z);
const A = perm[X] + Y, AA = perm[A] + Z, AB = perm[A + 1] + Z;
const B = perm[X + 1] + Y, BA = perm[B] + Z, BB = perm[B + 1] + Z;
return lerp(
lerp(lerp(grad(perm[AA], x, y, z), grad(perm[BA], x - 1, y, z), u),
lerp(grad(perm[AB], x, y - 1, z), grad(perm[BB], x - 1, y - 1, z), u), v),
lerp(lerp(grad(perm[AA + 1], x, y, z - 1), grad(perm[BA + 1], x - 1, y, z - 1), u),
lerp(grad(perm[AB + 1], x, y - 1, z - 1), grad(perm[BB + 1], x - 1, y - 1, z - 1), u), v), w);
}
// Flow ribbon class - creates delicate mesh-like flowing ribbons
class FlowRibbon {
constructor(yOffset, zOffset, phaseOffset, amplitude, frequency, lineCount) {
this.yOffset = yOffset;
this.zOffset = zOffset;
this.phaseOffset = phaseOffset;
this.amplitude = amplitude;
this.frequency = frequency;
this.lineCount = lineCount;
this.lines = [];
this.group = new THREE.Group();
this.createRibbon();
}
createRibbon() {
const segmentCount = 200;
const xStart = -60;
const xEnd = 60;
for (let l = 0; l < this.lineCount; l++) {
const lineOffset = (l / this.lineCount - 0.5) * 2;
const points = [];
for (let i = 0; i <= segmentCount; i++) {
const t = i / segmentCount;
const x = xStart + t * (xEnd - xStart);
points.push(new THREE.Vector3(x, 0, 0));
}
const geometry = new THREE.BufferGeometry().setFromPoints(points);
// Color gradient along the line
const colors = new Float32Array((segmentCount + 1) * 3);
for (let i = 0; i <= segmentCount; i++) {
const t = i / segmentCount;
const color = getColorAtPosition(t);
colors[i * 3] = color.r;
colors[i * 3 + 1] = color.g;
colors[i * 3 + 2] = color.b;
}
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.LineBasicMaterial({
vertexColors: true,
transparent: true,
opacity: 0.4 + Math.random() * 0.3,
linewidth: 1
});
const line = new THREE.Line(geometry, material);
this.lines.push({
line,
lineOffset,
localPhase: Math.random() * Math.PI * 2
});
this.group.add(line);
}
}
update(time) {
const segmentCount = 200;
const xStart = -60;
const xEnd = 60;
this.lines.forEach((lineData, lineIdx) => {
const positions = lineData.line.geometry.attributes.position.array;
const { lineOffset, localPhase } = lineData;
for (let i = 0; i <= segmentCount; i++) {
const t = i / segmentCount;
const x = xStart + t * (xEnd - xStart);
// Primary wave
const wave1 = Math.sin(t * Math.PI * this.frequency + time * 0.3 + this.phaseOffset) * this.amplitude;
// Secondary wave for complexity
const wave2 = Math.sin(t * Math.PI * this.frequency * 2.3 + time * 0.2 + this.phaseOffset + localPhase) * this.amplitude * 0.4;
// Tertiary wave
const wave3 = Math.sin(t * Math.PI * this.frequency * 0.7 + time * 0.15 + this.phaseOffset) * this.amplitude * 0.3;
// Noise for organic feel
const noiseVal = noise(t * 3, lineOffset * 0.5 + time * 0.1, this.phaseOffset) * this.amplitude * 0.5;
// Ribbon spread - lines separate and converge
const spread = (1 + Math.sin(t * Math.PI * 1.5 + time * 0.2) * 0.5) * 2;
const ribbonY = lineOffset * spread;
const y = this.yOffset + wave1 + wave2 + wave3 + noiseVal + ribbonY;
const z = this.zOffset + Math.cos(t * Math.PI * this.frequency * 0.5 + time * 0.25 + localPhase) * 2;
positions[i * 3] = x;
positions[i * 3 + 1] = y;
positions[i * 3 + 2] = z;
}
lineData.line.geometry.attributes.position.needsUpdate = true;
});
}
}
// Create multiple flowing ribbon groups
const ribbons = [];
// Main central ribbon group
ribbons.push(new FlowRibbon(0, 0, 0, 8, 2, 40));
// Upper ribbon
ribbons.push(new FlowRibbon(6, 2, Math.PI * 0.3, 6, 2.5, 25));
// Lower ribbon
ribbons.push(new FlowRibbon(-5, -2, Math.PI * 0.7, 7, 1.8, 30));
// Additional subtle ribbons
ribbons.push(new FlowRibbon(3, 4, Math.PI * 1.2, 5, 3, 15));
ribbons.push(new FlowRibbon(-2, -4, Math.PI * 0.5, 4, 2.2, 15));
ribbons.forEach(ribbon => scene.add(ribbon.group));
// Subtle floating particles
class FloatingParticles {
constructor() {
this.count = 100;
this.particles = [];
this.group = new THREE.Group();
this.create();
}
create() {
for (let i = 0; i < this.count; i++) {
const geometry = new THREE.CircleGeometry(0.05 + Math.random() * 0.1, 8);
const t = Math.random();
const color = getColorAtPosition(t);
const material = new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.2 + Math.random() * 0.3,
side: THREE.DoubleSide
});
const particle = new THREE.Mesh(geometry, material);
particle.position.x = (Math.random() - 0.5) * 100;
particle.position.y = (Math.random() - 0.5) * 40;
particle.position.z = (Math.random() - 0.5) * 20;
this.particles.push({
mesh: particle,
baseX: particle.position.x,
baseY: particle.position.y,
baseZ: particle.position.z,
speedX: (Math.random() - 0.5) * 0.02,
speedY: (Math.random() - 0.5) * 0.01,
phase: Math.random() * Math.PI * 2
});
this.group.add(particle);
}
}
update(time) {
this.particles.forEach(p => {
p.mesh.position.x = p.baseX + Math.sin(time * 0.5 + p.phase) * 2;
p.mesh.position.y = p.baseY + Math.cos(time * 0.3 + p.phase) * 1;
p.mesh.position.z = p.baseZ + Math.sin(time * 0.4 + p.phase) * 0.5;
// Gentle opacity pulse
p.mesh.material.opacity = 0.15 + Math.sin(time + p.phase) * 0.1;
});
}
}
const particles = new FloatingParticles();
scene.add(particles.group);
// Very subtle mesh overlay
class MeshOverlay {
constructor() {
this.lines = [];
this.group = new THREE.Group();
this.create();
}
create() {
const gridSize = 80;
const divisions = 30;
const step = gridSize / divisions;
const material = new THREE.LineBasicMaterial({
color: 0xd0d0d0,
transparent: true,
opacity: 0.05
});
// Horizontal lines
for (let i = 0; i <= divisions; i++) {
const y = -gridSize / 2 + i * step;
const points = [
new THREE.Vector3(-gridSize / 2, y, -10),
new THREE.Vector3(gridSize / 2, y, -10)
];
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const line = new THREE.Line(geometry, material.clone());
this.lines.push(line);
this.group.add(line);
}
}
update(time) {
this.lines.forEach((line, i) => {
const wave = Math.sin(time * 0.2 + i * 0.1) * 0.02;
line.material.opacity = 0.03 + wave;
});
}
}
const meshOverlay = new MeshOverlay();
scene.add(meshOverlay.group);
// Mouse interaction for subtle depth
let mouseX = 0;
let mouseY = 0;
let targetX = 0;
let targetY = 0;
document.addEventListener('mousemove', (e) => {
mouseX = (e.clientX / window.innerWidth - 0.5) * 2;
mouseY = (e.clientY / window.innerHeight - 0.5) * 2;
});
// Animation
const startTime = Date.now();
function animate() {
requestAnimationFrame(animate);
const time = (Date.now() - startTime) * 0.001;
// Smooth camera movement
targetX += (mouseX * 3 - targetX) * 0.02;
targetY += (-mouseY * 2 - targetY) * 0.02;
camera.position.x = targetX;
camera.position.y = targetY;
camera.lookAt(0, 0, 0);
// Update all elements
ribbons.forEach(ribbon => ribbon.update(time));
particles.update(time);
meshOverlay.update(time);
renderer.render(scene, camera);
}
// Handle resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
animate();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment