Created
December 14, 2025 13:56
-
-
Save AgentO3/d219f71d08903464ed1775400dc1460d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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