A Pen by Cameron Knight on CodePen.
Created
April 17, 2026 01:34
-
-
Save hahagu/553ced76d8cbc8efa52457d32fb15373 to your computer and use it in GitHub Desktop.
Interactive Liquid Gradient using Three.js
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
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Interactive Liquid Gradient</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Syne:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600;700;800&family=Bebas+Neue&family=Montserrat:wght@400;500;600;700;800&display=swap" rel="stylesheet"> | |
| </head> | |
| <body> | |
| <!-- Heading: Syne (Modern, Geometric) --> | |
| <h1 class="heading option1">Liquid Gradient</h1> | |
| <!-- Color Scheme Controls --> | |
| <div class="color-controls"> | |
| <button class="color-btn active" data-scheme="1">Scheme 1</button> | |
| <button class="color-btn" data-scheme="2">Scheme 2</button> | |
| <button class="color-btn" data-scheme="3">Scheme 3</button> | |
| <button class="color-btn" data-scheme="4">Scheme 4</button> | |
| <button class="color-btn" data-scheme="5">Scheme 5</button> | |
| </div> | |
| <!-- Color Adjuster Toggle Button --> | |
| <button class="toggle-adjuster-btn" id="toggleAdjusterBtn">Adjust Colors</button> | |
| <!-- Color Adjuster Panel --> | |
| <div class="color-adjuster-panel" id="colorAdjusterPanel"> | |
| <div class="color-adjuster-header"> | |
| <h3 class="color-adjuster-title">Color Adjuster</h3> | |
| <button class="color-adjuster-close" id="closeAdjusterBtn">×</button> | |
| </div> | |
| <div class="color-picker-group"> | |
| <div class="color-picker-label"> | |
| <span>Color 1</span> | |
| </div> | |
| <div class="color-picker-wrapper"> | |
| <input type="color" class="color-picker-input" id="colorPicker1" data-color="1"> | |
| <input type="text" class="color-value-display" id="colorValue1" readonly> | |
| <button class="copy-btn" data-copy="1">Copy</button> | |
| </div> | |
| </div> | |
| <div class="color-picker-group"> | |
| <div class="color-picker-label"> | |
| <span>Color 2</span> | |
| </div> | |
| <div class="color-picker-wrapper"> | |
| <input type="color" class="color-picker-input" id="colorPicker2" data-color="2"> | |
| <input type="text" class="color-value-display" id="colorValue2" readonly> | |
| <button class="copy-btn" data-copy="2">Copy</button> | |
| </div> | |
| </div> | |
| <div class="color-picker-group"> | |
| <div class="color-picker-label"> | |
| <span>Color 3</span> | |
| </div> | |
| <div class="color-picker-wrapper"> | |
| <input type="color" class="color-picker-input" id="colorPicker3" data-color="3"> | |
| <input type="text" class="color-value-display" id="colorValue3" readonly> | |
| <button class="copy-btn" data-copy="3">Copy</button> | |
| </div> | |
| </div> | |
| <div class="color-picker-group"> | |
| <div class="color-picker-label"> | |
| <span>Color 4</span> | |
| </div> | |
| <div class="color-picker-wrapper"> | |
| <input type="color" class="color-picker-input" id="colorPicker4" data-color="4"> | |
| <input type="text" class="color-value-display" id="colorValue4" readonly> | |
| <button class="copy-btn" data-copy="4">Copy</button> | |
| </div> | |
| </div> | |
| <div class="color-picker-group"> | |
| <div class="color-picker-label"> | |
| <span>Color 5</span> | |
| </div> | |
| <div class="color-picker-wrapper"> | |
| <input type="color" class="color-picker-input" id="colorPicker5" data-color="5"> | |
| <input type="text" class="color-value-display" id="colorValue5" readonly> | |
| <button class="copy-btn" data-copy="5">Copy</button> | |
| </div> | |
| </div> | |
| <div class="color-picker-group"> | |
| <div class="color-picker-label"> | |
| <span>Color 6</span> | |
| </div> | |
| <div class="color-picker-wrapper"> | |
| <input type="color" class="color-picker-input" id="colorPicker6" data-color="6"> | |
| <input type="text" class="color-value-display" id="colorValue6" readonly> | |
| <button class="copy-btn" data-copy="6">Copy</button> | |
| </div> | |
| </div> | |
| <div class="color-adjuster-actions"> | |
| <button class="export-btn" id="exportAllBtn">Export All Colors</button> | |
| </div> | |
| </div> | |
| <footer class="footer"> | |
| <a href="https://madebybeings.com" target="_blank" rel="noopener noreferrer">Made By Beings</a> | |
| </footer> | |
| <div class="custom-cursor" id="customCursor"></div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| </body> |
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
| // TouchTexture class | |
| class TouchTexture { | |
| constructor() { | |
| this.size = 64; | |
| this.width = this.height = this.size; | |
| this.maxAge = 64; | |
| this.radius = 0.25 * this.size; // Much larger touch radius for more obvious effect | |
| this.speed = 1 / this.maxAge; | |
| this.trail = []; | |
| this.last = null; | |
| this.initTexture(); | |
| } | |
| initTexture() { | |
| this.canvas = document.createElement("canvas"); | |
| this.canvas.width = this.width; | |
| this.canvas.height = this.height; | |
| this.ctx = this.canvas.getContext("2d"); | |
| this.ctx.fillStyle = "black"; | |
| this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); | |
| this.texture = new THREE.Texture(this.canvas); | |
| } | |
| update() { | |
| this.clear(); | |
| let speed = this.speed; | |
| // Use reverse iteration to safely remove items | |
| for (let i = this.trail.length - 1; i >= 0; i--) { | |
| const point = this.trail[i]; | |
| let f = point.force * speed * (1 - point.age / this.maxAge); | |
| point.x += point.vx * f; | |
| point.y += point.vy * f; | |
| point.age++; | |
| if (point.age > this.maxAge) { | |
| this.trail.splice(i, 1); | |
| } else { | |
| this.drawPoint(point); | |
| } | |
| } | |
| this.texture.needsUpdate = true; | |
| } | |
| clear() { | |
| this.ctx.fillStyle = "black"; | |
| this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); | |
| } | |
| addTouch(point) { | |
| let force = 0; | |
| let vx = 0; | |
| let vy = 0; | |
| const last = this.last; | |
| if (last) { | |
| const dx = point.x - last.x; | |
| const dy = point.y - last.y; | |
| if (dx === 0 && dy === 0) return; | |
| const dd = dx * dx + dy * dy; | |
| let d = Math.sqrt(dd); | |
| vx = dx / d; | |
| vy = dy / d; | |
| force = Math.min(dd * 20000, 2.0); // Much stronger force for very noticeable effect | |
| } | |
| this.last = { x: point.x, y: point.y }; | |
| this.trail.push({ x: point.x, y: point.y, age: 0, force, vx, vy }); | |
| } | |
| drawPoint(point) { | |
| const pos = { | |
| x: point.x * this.width, | |
| y: (1 - point.y) * this.height | |
| }; | |
| let intensity = 1; | |
| if (point.age < this.maxAge * 0.3) { | |
| intensity = Math.sin((point.age / (this.maxAge * 0.3)) * (Math.PI / 2)); | |
| } else { | |
| const t = 1 - (point.age - this.maxAge * 0.3) / (this.maxAge * 0.7); | |
| intensity = -t * (t - 2); | |
| } | |
| intensity *= point.force; | |
| const radius = this.radius; | |
| let color = `${((point.vx + 1) / 2) * 255}, ${ | |
| ((point.vy + 1) / 2) * 255 | |
| }, ${intensity * 255}`; | |
| let offset = this.size * 5; | |
| this.ctx.shadowOffsetX = offset; | |
| this.ctx.shadowOffsetY = offset; | |
| this.ctx.shadowBlur = radius * 1; | |
| this.ctx.shadowColor = `rgba(${color},${0.2 * intensity})`; | |
| this.ctx.beginPath(); | |
| this.ctx.fillStyle = "rgba(255,0,0,1)"; | |
| this.ctx.arc(pos.x - offset, pos.y - offset, radius, 0, Math.PI * 2); | |
| this.ctx.fill(); | |
| } | |
| } | |
| // GradientBackground class | |
| class GradientBackground { | |
| constructor(sceneManager) { | |
| this.sceneManager = sceneManager; | |
| this.mesh = null; | |
| this.uniforms = { | |
| uTime: { value: 0 }, | |
| uResolution: { | |
| value: new THREE.Vector2(window.innerWidth, window.innerHeight) | |
| }, | |
| uColor1: { value: new THREE.Vector3(0.945, 0.353, 0.133) }, // F15A22 - Orange | |
| uColor2: { value: new THREE.Vector3(0.039, 0.055, 0.153) }, // 0a0e27 - Navy Blue | |
| uColor3: { value: new THREE.Vector3(0.945, 0.353, 0.133) }, // F15A22 - Orange | |
| uColor4: { value: new THREE.Vector3(0.039, 0.055, 0.153) }, // 0a0e27 - Navy Blue | |
| uColor5: { value: new THREE.Vector3(0.945, 0.353, 0.133) }, // F15A22 - Orange | |
| uColor6: { value: new THREE.Vector3(0.039, 0.055, 0.153) }, // 0a0e27 - Navy Blue | |
| uSpeed: { value: 1.2 }, // Faster animation | |
| uIntensity: { value: 1.8 }, | |
| uTouchTexture: { value: null }, | |
| uGrainIntensity: { value: 0.08 }, | |
| uZoom: { value: 1.0 }, // Zoom/scale control - lower = less zoomed (more visible) | |
| uDarkNavy: { value: new THREE.Vector3(0.039, 0.055, 0.153) }, // #0a0e27 - Dark navy base color | |
| uGradientSize: { value: 1.0 }, // Control gradient size (smaller = more gradients) | |
| uGradientCount: { value: 6.0 }, // Number of gradient centers | |
| uColor1Weight: { value: 1.0 }, // Weight for color1 (orange) - reduce for more navy | |
| uColor2Weight: { value: 1.0 } // Weight for color2 (navy) - increase for more navy | |
| }; | |
| } | |
| init() { | |
| const viewSize = this.sceneManager.getViewSize(); | |
| const geometry = new THREE.PlaneGeometry( | |
| viewSize.width, | |
| viewSize.height, | |
| 1, | |
| 1 | |
| ); | |
| const material = new THREE.ShaderMaterial({ | |
| uniforms: this.uniforms, | |
| vertexShader: ` | |
| varying vec2 vUv; | |
| void main() { | |
| vec3 pos = position.xyz; | |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.); | |
| vUv = uv; | |
| } | |
| `, | |
| fragmentShader: ` | |
| uniform float uTime; | |
| uniform vec2 uResolution; | |
| uniform vec3 uColor1; | |
| uniform vec3 uColor2; | |
| uniform vec3 uColor3; | |
| uniform vec3 uColor4; | |
| uniform vec3 uColor5; | |
| uniform vec3 uColor6; | |
| uniform float uSpeed; | |
| uniform float uIntensity; | |
| uniform sampler2D uTouchTexture; | |
| uniform float uGrainIntensity; | |
| uniform float uZoom; | |
| uniform vec3 uDarkNavy; | |
| uniform float uGradientSize; | |
| uniform float uGradientCount; | |
| uniform float uColor1Weight; | |
| uniform float uColor2Weight; | |
| varying vec2 vUv; | |
| #define PI 3.14159265359 | |
| // Grain function for film grain effect | |
| float grain(vec2 uv, float time) { | |
| vec2 grainUv = uv * uResolution * 0.5; | |
| float grainValue = fract(sin(dot(grainUv + time, vec2(12.9898, 78.233))) * 43758.5453); | |
| return grainValue * 2.0 - 1.0; | |
| } | |
| vec3 getGradientColor(vec2 uv, float time) { | |
| // Dynamic gradient size based on uniform | |
| float gradientRadius = uGradientSize; | |
| // Multiple animated centers with different speeds and patterns | |
| // Support up to 12 centers for more gradient action | |
| vec2 center1 = vec2( | |
| 0.5 + sin(time * uSpeed * 0.4) * 0.4, | |
| 0.5 + cos(time * uSpeed * 0.5) * 0.4 | |
| ); | |
| vec2 center2 = vec2( | |
| 0.5 + cos(time * uSpeed * 0.6) * 0.5, | |
| 0.5 + sin(time * uSpeed * 0.45) * 0.5 | |
| ); | |
| vec2 center3 = vec2( | |
| 0.5 + sin(time * uSpeed * 0.35) * 0.45, | |
| 0.5 + cos(time * uSpeed * 0.55) * 0.45 | |
| ); | |
| vec2 center4 = vec2( | |
| 0.5 + cos(time * uSpeed * 0.5) * 0.4, | |
| 0.5 + sin(time * uSpeed * 0.4) * 0.4 | |
| ); | |
| vec2 center5 = vec2( | |
| 0.5 + sin(time * uSpeed * 0.7) * 0.35, | |
| 0.5 + cos(time * uSpeed * 0.6) * 0.35 | |
| ); | |
| vec2 center6 = vec2( | |
| 0.5 + cos(time * uSpeed * 0.45) * 0.5, | |
| 0.5 + sin(time * uSpeed * 0.65) * 0.5 | |
| ); | |
| // Additional centers for more gradient action (7-12) | |
| vec2 center7 = vec2( | |
| 0.5 + sin(time * uSpeed * 0.55) * 0.38, | |
| 0.5 + cos(time * uSpeed * 0.48) * 0.42 | |
| ); | |
| vec2 center8 = vec2( | |
| 0.5 + cos(time * uSpeed * 0.65) * 0.36, | |
| 0.5 + sin(time * uSpeed * 0.52) * 0.44 | |
| ); | |
| vec2 center9 = vec2( | |
| 0.5 + sin(time * uSpeed * 0.42) * 0.41, | |
| 0.5 + cos(time * uSpeed * 0.58) * 0.39 | |
| ); | |
| vec2 center10 = vec2( | |
| 0.5 + cos(time * uSpeed * 0.48) * 0.37, | |
| 0.5 + sin(time * uSpeed * 0.62) * 0.43 | |
| ); | |
| vec2 center11 = vec2( | |
| 0.5 + sin(time * uSpeed * 0.68) * 0.33, | |
| 0.5 + cos(time * uSpeed * 0.44) * 0.46 | |
| ); | |
| vec2 center12 = vec2( | |
| 0.5 + cos(time * uSpeed * 0.38) * 0.39, | |
| 0.5 + sin(time * uSpeed * 0.56) * 0.41 | |
| ); | |
| float dist1 = length(uv - center1); | |
| float dist2 = length(uv - center2); | |
| float dist3 = length(uv - center3); | |
| float dist4 = length(uv - center4); | |
| float dist5 = length(uv - center5); | |
| float dist6 = length(uv - center6); | |
| float dist7 = length(uv - center7); | |
| float dist8 = length(uv - center8); | |
| float dist9 = length(uv - center9); | |
| float dist10 = length(uv - center10); | |
| float dist11 = length(uv - center11); | |
| float dist12 = length(uv - center12); | |
| // Smaller, tighter influence areas based on uGradientSize | |
| float influence1 = 1.0 - smoothstep(0.0, gradientRadius, dist1); | |
| float influence2 = 1.0 - smoothstep(0.0, gradientRadius, dist2); | |
| float influence3 = 1.0 - smoothstep(0.0, gradientRadius, dist3); | |
| float influence4 = 1.0 - smoothstep(0.0, gradientRadius, dist4); | |
| float influence5 = 1.0 - smoothstep(0.0, gradientRadius, dist5); | |
| float influence6 = 1.0 - smoothstep(0.0, gradientRadius, dist6); | |
| float influence7 = 1.0 - smoothstep(0.0, gradientRadius, dist7); | |
| float influence8 = 1.0 - smoothstep(0.0, gradientRadius, dist8); | |
| float influence9 = 1.0 - smoothstep(0.0, gradientRadius, dist9); | |
| float influence10 = 1.0 - smoothstep(0.0, gradientRadius, dist10); | |
| float influence11 = 1.0 - smoothstep(0.0, gradientRadius, dist11); | |
| float influence12 = 1.0 - smoothstep(0.0, gradientRadius, dist12); | |
| // Multiple rotation layers for depth | |
| vec2 rotatedUv1 = uv - 0.5; | |
| float angle1 = time * uSpeed * 0.15; | |
| rotatedUv1 = vec2( | |
| rotatedUv1.x * cos(angle1) - rotatedUv1.y * sin(angle1), | |
| rotatedUv1.x * sin(angle1) + rotatedUv1.y * cos(angle1) | |
| ); | |
| rotatedUv1 += 0.5; | |
| vec2 rotatedUv2 = uv - 0.5; | |
| float angle2 = -time * uSpeed * 0.12; | |
| rotatedUv2 = vec2( | |
| rotatedUv2.x * cos(angle2) - rotatedUv2.y * sin(angle2), | |
| rotatedUv2.x * sin(angle2) + rotatedUv2.y * cos(angle2) | |
| ); | |
| rotatedUv2 += 0.5; | |
| float radialGradient1 = length(rotatedUv1 - 0.5); | |
| float radialGradient2 = length(rotatedUv2 - 0.5); | |
| float radialInfluence1 = 1.0 - smoothstep(0.0, 0.8, radialGradient1); | |
| float radialInfluence2 = 1.0 - smoothstep(0.0, 0.8, radialGradient2); | |
| // Blend all colors with dynamic intensities - increased for more contrast | |
| vec3 color = vec3(0.0); | |
| color += uColor1 * influence1 * (0.55 + 0.45 * sin(time * uSpeed)) * uColor1Weight; | |
| color += uColor2 * influence2 * (0.55 + 0.45 * cos(time * uSpeed * 1.2)) * uColor2Weight; | |
| color += uColor3 * influence3 * (0.55 + 0.45 * sin(time * uSpeed * 0.8)) * uColor1Weight; | |
| color += uColor4 * influence4 * (0.55 + 0.45 * cos(time * uSpeed * 1.3)) * uColor2Weight; | |
| color += uColor5 * influence5 * (0.55 + 0.45 * sin(time * uSpeed * 1.1)) * uColor1Weight; | |
| color += uColor6 * influence6 * (0.55 + 0.45 * cos(time * uSpeed * 0.9)) * uColor2Weight; | |
| // Add extra centers if uGradientCount > 6 | |
| if (uGradientCount > 6.0) { | |
| color += uColor1 * influence7 * (0.55 + 0.45 * sin(time * uSpeed * 1.4)) * uColor1Weight; | |
| color += uColor2 * influence8 * (0.55 + 0.45 * cos(time * uSpeed * 1.5)) * uColor2Weight; | |
| color += uColor3 * influence9 * (0.55 + 0.45 * sin(time * uSpeed * 1.6)) * uColor1Weight; | |
| color += uColor4 * influence10 * (0.55 + 0.45 * cos(time * uSpeed * 1.7)) * uColor2Weight; | |
| } | |
| if (uGradientCount > 10.0) { | |
| color += uColor5 * influence11 * (0.55 + 0.45 * sin(time * uSpeed * 1.8)) * uColor1Weight; | |
| color += uColor6 * influence12 * (0.55 + 0.45 * cos(time * uSpeed * 1.9)) * uColor2Weight; | |
| } | |
| // Add radial overlays - increased for more contrast, with color weighting | |
| color += mix(uColor1, uColor3, radialInfluence1) * 0.45 * uColor1Weight; | |
| color += mix(uColor2, uColor4, radialInfluence2) * 0.4 * uColor2Weight; | |
| // Clamp and apply intensity | |
| color = clamp(color, vec3(0.0), vec3(1.0)) * uIntensity; | |
| // Enhanced color saturation for more vibrant look | |
| float luminance = dot(color, vec3(0.299, 0.587, 0.114)); | |
| color = mix(vec3(luminance), color, 1.35); | |
| color = pow(color, vec3(0.92)); // Slight gamma adjustment for better contrast | |
| // Ensure minimum brightness (navy blue base instead of grey/black) | |
| // Use higher threshold to ensure navy blue shows through in low-intensity areas | |
| float brightness1 = length(color); | |
| float mixFactor1 = max(brightness1 * 1.2, 0.15); // Higher threshold for navy blue base | |
| color = mix(uDarkNavy, color, mixFactor1); | |
| // Cap maximum brightness - increased for more contrast | |
| float maxBrightness = 1.0; | |
| float brightness = length(color); | |
| if (brightness > maxBrightness) { | |
| color = color * (maxBrightness / brightness); | |
| } | |
| return color; | |
| } | |
| void main() { | |
| vec2 uv = vUv; | |
| // Apply water distortion from touch texture - very strong | |
| vec4 touchTex = texture2D(uTouchTexture, uv); | |
| float vx = -(touchTex.r * 2.0 - 1.0); | |
| float vy = -(touchTex.g * 2.0 - 1.0); | |
| float intensity = touchTex.b; | |
| // Much increased distortion strength for very obvious effect | |
| uv.x += vx * 0.8 * intensity; | |
| uv.y += vy * 0.8 * intensity; | |
| // Combined ripple and wave effect for better performance | |
| vec2 center = vec2(0.5); | |
| float dist = length(uv - center); | |
| float ripple = sin(dist * 20.0 - uTime * 3.0) * 0.04 * intensity; | |
| float wave = sin(dist * 15.0 - uTime * 2.0) * 0.03 * intensity; | |
| uv += vec2(ripple + wave); | |
| vec3 color = getGradientColor(uv, uTime); | |
| // Apply grain effect | |
| float grainValue = grain(uv, uTime); | |
| color += grainValue * uGrainIntensity; | |
| // Subtle color shifting - optimized with single calculation | |
| float timeShift = uTime * 0.5; | |
| color.r += sin(timeShift) * 0.02; | |
| color.g += cos(timeShift * 1.4) * 0.02; | |
| color.b += sin(timeShift * 1.2) * 0.02; | |
| // Ensure minimum brightness (navy blue base instead of grey/black) | |
| // Use higher threshold to ensure navy blue shows through in low-intensity areas | |
| float brightness2 = length(color); | |
| float mixFactor2 = max(brightness2 * 1.2, 0.15); // Higher threshold for navy blue base | |
| color = mix(uDarkNavy, color, mixFactor2); | |
| // Clamp to valid color range | |
| color = clamp(color, vec3(0.0), vec3(1.0)); | |
| // Cap maximum brightness - increased for more contrast | |
| float maxBrightness = 1.0; | |
| float brightness = length(color); | |
| if (brightness > maxBrightness) { | |
| color = color * (maxBrightness / brightness); | |
| } | |
| gl_FragColor = vec4(color, 1.0); | |
| } | |
| ` | |
| }); | |
| this.mesh = new THREE.Mesh(geometry, material); | |
| this.mesh.position.z = 0; | |
| this.sceneManager.scene.add(this.mesh); | |
| } | |
| update(delta) { | |
| if (this.uniforms.uTime) { | |
| this.uniforms.uTime.value += delta; | |
| } | |
| } | |
| onResize(width, height) { | |
| const viewSize = this.sceneManager.getViewSize(); | |
| if (this.mesh) { | |
| this.mesh.geometry.dispose(); | |
| this.mesh.geometry = new THREE.PlaneGeometry( | |
| viewSize.width, | |
| viewSize.height, | |
| 1, | |
| 1 | |
| ); | |
| } | |
| if (this.uniforms.uResolution) { | |
| this.uniforms.uResolution.value.set(width, height); | |
| } | |
| } | |
| } | |
| // App class | |
| class App { | |
| constructor() { | |
| this.renderer = new THREE.WebGLRenderer({ | |
| antialias: true, | |
| powerPreference: "high-performance", | |
| alpha: false, | |
| stencil: false, | |
| depth: false | |
| }); | |
| this.renderer.setSize(window.innerWidth, window.innerHeight); | |
| this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // Cap pixel ratio for performance | |
| this.renderer.setAnimationLoop(null); // We'll use our own tick loop | |
| document.body.appendChild(this.renderer.domElement); | |
| this.renderer.domElement.id = "webGLApp"; | |
| this.camera = new THREE.PerspectiveCamera( | |
| 45, | |
| window.innerWidth / window.innerHeight, | |
| 0.1, | |
| 10000 | |
| ); | |
| this.camera.position.z = 50; | |
| this.scene = new THREE.Scene(); | |
| this.scene.background = new THREE.Color(0x0a0e27); // Dark navy | |
| this.clock = new THREE.Clock(); | |
| this.touchTexture = new TouchTexture(); | |
| this.gradientBackground = new GradientBackground(this); | |
| this.gradientBackground.uniforms.uTouchTexture.value = this.touchTexture.texture; | |
| // Color schemes | |
| this.colorSchemes = { | |
| 1: { | |
| // Orange + Navy Blue | |
| color1: new THREE.Vector3(0.945, 0.353, 0.133), // F15A22 - Orange | |
| color2: new THREE.Vector3(0.039, 0.055, 0.153) // 0a0e27 - Navy Blue | |
| }, | |
| 2: { | |
| // Turquoise + Coral Red-Orange | |
| color1: new THREE.Vector3(1.0, 0.424, 0.314), // FF6C50 - Coral Red-Orange | |
| color2: new THREE.Vector3(0.251, 0.878, 0.816) // 40E0D0 - Turquoise | |
| }, | |
| 3: { | |
| // Orange + Navy + Turquoise (identical to scheme 1 but with turquoise added) | |
| color1: new THREE.Vector3(0.945, 0.353, 0.133), // F15A22 - Orange | |
| color2: new THREE.Vector3(0.039, 0.055, 0.153), // 0a0e27 - Navy Blue | |
| color3: new THREE.Vector3(0.251, 0.878, 0.816) // 40E0D0 - Turquoise | |
| }, | |
| 4: { | |
| // Based on Scheme 3: F26633 + 2D6B6D + D1AF9C | |
| color1: new THREE.Vector3(0.949, 0.4, 0.2), // F26633 - Orange/Coral | |
| color2: new THREE.Vector3(0.176, 0.42, 0.427), // 2D6B6D - Teal/Blue-Green | |
| color3: new THREE.Vector3(0.82, 0.686, 0.612) // D1AF9C - Beige/Peach | |
| }, | |
| 5: { | |
| // F15A22 + 004238 + F15A22 + 000000 + F15A22 + 000000 | |
| color1: new THREE.Vector3(0.945, 0.353, 0.133), // F15A22 - Orange | |
| color2: new THREE.Vector3(0.0, 0.259, 0.22), // 004238 - Dark Teal (0, 66, 56) | |
| color3: new THREE.Vector3(0.945, 0.353, 0.133), // F15A22 - Orange | |
| color4: new THREE.Vector3(0.0, 0.0, 0.0), // 000000 - Black | |
| color5: new THREE.Vector3(0.945, 0.353, 0.133), // F15A22 - Orange | |
| color6: new THREE.Vector3(0.0, 0.0, 0.0) // 000000 - Black | |
| } | |
| }; | |
| this.currentScheme = 1; | |
| this.init(); | |
| } | |
| setColorScheme(scheme) { | |
| if (!this.colorSchemes[scheme]) return; | |
| this.currentScheme = scheme; | |
| const colors = this.colorSchemes[scheme]; | |
| const uniforms = this.gradientBackground.uniforms; | |
| // Update all color uniforms | |
| if (scheme === 3) { | |
| // Scheme 3: Orange + Navy + Turquoise (identical to scheme 1 but with turquoise) | |
| uniforms.uColor1.value.copy(colors.color1); // Orange | |
| uniforms.uColor2.value.copy(colors.color2); // Navy | |
| uniforms.uColor3.value.copy(colors.color3); // Turquoise | |
| uniforms.uColor4.value.copy(colors.color1); // Orange | |
| uniforms.uColor5.value.copy(colors.color2); // Navy | |
| uniforms.uColor6.value.copy(colors.color3); // Turquoise | |
| } else if (scheme === 4) { | |
| // Scheme 4: Based on Scheme 3 with F26633, 2D6B6D, D1AF9C | |
| uniforms.uColor1.value.copy(colors.color1); // F26633 - Orange/Coral | |
| uniforms.uColor2.value.copy(colors.color2); // 2D6B6D - Teal/Blue-Green | |
| uniforms.uColor3.value.copy(colors.color3); // D1AF9C - Beige/Peach | |
| uniforms.uColor4.value.copy(colors.color1); // F26633 - Orange/Coral | |
| uniforms.uColor5.value.copy(colors.color2); // 2D6B6D - Teal/Blue-Green | |
| uniforms.uColor6.value.copy(colors.color3); // D1AF9C - Beige/Peach | |
| } else if (scheme === 5) { | |
| // Scheme 5: F15A22 + 004238 + F15A22 + 000000 + F15A22 + 000000 | |
| uniforms.uColor1.value.copy(colors.color1); // F15A22 - Orange | |
| uniforms.uColor2.value.copy(colors.color2); // 004238 - Dark Teal | |
| uniforms.uColor3.value.copy(colors.color3); // F15A22 - Orange | |
| uniforms.uColor4.value.copy(colors.color4); // 000000 - Black | |
| uniforms.uColor5.value.copy(colors.color5); // F15A22 - Orange | |
| uniforms.uColor6.value.copy(colors.color6); // 000000 - Black | |
| } else { | |
| uniforms.uColor1.value.copy(colors.color1); | |
| uniforms.uColor2.value.copy(colors.color2); | |
| uniforms.uColor3.value.copy(colors.color1); | |
| uniforms.uColor4.value.copy(colors.color2); | |
| uniforms.uColor5.value.copy(colors.color1); | |
| uniforms.uColor6.value.copy(colors.color2); | |
| } | |
| // Update background color and base color | |
| if (scheme === 1) { | |
| this.scene.background = new THREE.Color(0x0a0e27); // Navy blue for scheme 1 | |
| uniforms.uDarkNavy.value.set(0.039, 0.055, 0.153); // #0a0e27 - Navy blue base color | |
| // More gradient action: smaller gradients, more of them | |
| uniforms.uGradientSize.value = 0.45; // Smaller gradient radius for more defined gradients | |
| uniforms.uGradientCount.value = 12.0; // More gradient centers (12 instead of 6) | |
| uniforms.uSpeed.value = 1.5; // Slightly faster for more movement | |
| // Balance colors: reduce orange, increase navy | |
| uniforms.uColor1Weight.value = 0.5; // Reduce orange intensity | |
| uniforms.uColor2Weight.value = 1.8; // Increase navy intensity | |
| } else if (scheme === 6) { | |
| // Scheme 6: Identical to scheme 1 but with Orange, Navy, and Turquoise | |
| this.scene.background = new THREE.Color(0x0a0e27); // Navy blue (same as scheme 1) | |
| uniforms.uDarkNavy.value.set(0.039, 0.055, 0.153); // #0a0e27 - Navy blue base color (same as scheme 1) | |
| // More gradient action: smaller gradients, more of them (same as scheme 1) | |
| uniforms.uGradientSize.value = 0.45; // Smaller gradient radius for more defined gradients | |
| uniforms.uGradientCount.value = 12.0; // More gradient centers (12 instead of 6) | |
| uniforms.uSpeed.value = 1.5; // Slightly faster for more movement | |
| // Balance colors: reduce orange, increase navy (same as scheme 1) | |
| uniforms.uColor1Weight.value = 0.5; // Reduce orange intensity | |
| uniforms.uColor2Weight.value = 1.8; // Increase navy intensity | |
| } else if (scheme === 7) { | |
| // Scheme 7: Based on Scheme 6 with F26633, 2D6B6D, D1AF9C (same settings as Scheme 6) | |
| this.scene.background = new THREE.Color(0x0a0e27); // Navy blue (same as scheme 6) | |
| uniforms.uDarkNavy.value.set(0.039, 0.055, 0.153); // #0a0e27 - Navy blue base color (same as scheme 6) | |
| // More gradient action: smaller gradients, more of them (same as scheme 6) | |
| uniforms.uGradientSize.value = 0.45; // Smaller gradient radius for more defined gradients | |
| uniforms.uGradientCount.value = 12.0; // More gradient centers (12 instead of 6) | |
| uniforms.uSpeed.value = 1.5; // Slightly faster for more movement | |
| // Balance colors: same as scheme 6 | |
| uniforms.uColor1Weight.value = 0.5; // Reduce orange/coral intensity | |
| uniforms.uColor2Weight.value = 1.8; // Increase teal intensity | |
| } else if (scheme === 8) { | |
| // Scheme 8: Identical to Scheme 1 | |
| this.scene.background = new THREE.Color(0x0a0e27); // Navy blue for scheme 8 | |
| uniforms.uDarkNavy.value.set(0.039, 0.055, 0.153); // #0a0e27 - Navy blue base color | |
| // More gradient action: smaller gradients, more of them | |
| uniforms.uGradientSize.value = 0.45; // Smaller gradient radius for more defined gradients | |
| uniforms.uGradientCount.value = 12.0; // More gradient centers (12 instead of 6) | |
| uniforms.uSpeed.value = 1.5; // Slightly faster for more movement | |
| // Balance colors: reduce orange, increase navy | |
| uniforms.uColor1Weight.value = 0.5; // Reduce orange intensity | |
| uniforms.uColor2Weight.value = 1.8; // Increase navy intensity | |
| } else if (scheme === 5) { | |
| // Scheme 5: Same settings as Scheme 1 but with F15A22 + 004238 + F15A22 + 000000 + F15A22 + 000000 | |
| this.scene.background = new THREE.Color(0x0a0e27); // Navy blue for scheme 5 (same as scheme 1) | |
| uniforms.uDarkNavy.value.set(0.039, 0.055, 0.153); // #0a0e27 - Navy blue base color (same as scheme 1) | |
| // More gradient action: smaller gradients, more of them (same as scheme 1) | |
| uniforms.uGradientSize.value = 0.45; // Smaller gradient radius for more defined gradients | |
| uniforms.uGradientCount.value = 12.0; // More gradient centers (12 instead of 6) | |
| uniforms.uSpeed.value = 1.5; // Slightly faster for more movement | |
| // Balance colors: reduce orange, increase navy (same as scheme 1) | |
| uniforms.uColor1Weight.value = 0.5; // Reduce orange intensity | |
| uniforms.uColor2Weight.value = 1.8; // Increase navy intensity | |
| } else if (scheme === 4) { | |
| this.scene.background = new THREE.Color(0xffffff); // Off-white for scheme 4 | |
| uniforms.uDarkNavy.value.set(0, 0, 0); // #FAFAFA - Off-white base | |
| } else if (scheme === 2) { | |
| this.scene.background = new THREE.Color(0x0a0e27); // Default dark navy for scheme 2 | |
| uniforms.uDarkNavy.value.set(0.039, 0.055, 0.153); // #0a0e27 - Default dark navy | |
| uniforms.uGradientSize.value = 1.0; // Default size | |
| uniforms.uGradientCount.value = 6.0; // Default count | |
| uniforms.uSpeed.value = 1.2; // Default speed | |
| uniforms.uColor1Weight.value = 1.0; // Default weight | |
| uniforms.uColor2Weight.value = 1.0; // Default weight | |
| } else { | |
| this.scene.background = new THREE.Color(0x0a0e27); // Default dark navy | |
| uniforms.uDarkNavy.value.set(0.039, 0.055, 0.153); // #0a0e27 - Default dark navy | |
| uniforms.uGradientSize.value = 1.0; // Default size | |
| uniforms.uGradientCount.value = 6.0; // Default count | |
| uniforms.uSpeed.value = 1.2; // Default speed | |
| uniforms.uColor1Weight.value = 1.0; // Default weight | |
| uniforms.uColor2Weight.value = 1.0; // Default weight | |
| } | |
| } | |
| init() { | |
| this.gradientBackground.init(); | |
| // Apply Scheme 1 settings on startup | |
| this.setColorScheme(1); | |
| // Force initial render to wake up the browser | |
| this.render(); | |
| // Start animation loop | |
| this.tick(); | |
| window.addEventListener("resize", () => this.onResize()); | |
| window.addEventListener("mousemove", (ev) => this.onMouseMove(ev)); | |
| window.addEventListener("touchmove", (ev) => this.onTouchMove(ev)); | |
| // Handle visibility changes to prevent throttling | |
| document.addEventListener("visibilitychange", () => { | |
| if (!document.hidden) { | |
| // Force render when page becomes visible | |
| this.render(); | |
| } | |
| }); | |
| // Wake up animation on any user interaction | |
| const wakeUpAnimation = () => { | |
| this.render(); | |
| window.removeEventListener("click", wakeUpAnimation); | |
| window.removeEventListener("touchstart", wakeUpAnimation); | |
| window.removeEventListener("mousemove", wakeUpAnimation); | |
| }; | |
| window.addEventListener("click", wakeUpAnimation, { once: true }); | |
| window.addEventListener("touchstart", wakeUpAnimation, { once: true }); | |
| window.addEventListener("mousemove", wakeUpAnimation, { once: true }); | |
| } | |
| onTouchMove(ev) { | |
| const touch = ev.touches[0]; | |
| this.onMouseMove({ clientX: touch.clientX, clientY: touch.clientY }); | |
| } | |
| onMouseMove(ev) { | |
| this.mouse = { | |
| x: ev.clientX / window.innerWidth, | |
| y: 1 - ev.clientY / window.innerHeight | |
| }; | |
| this.touchTexture.addTouch(this.mouse); | |
| } | |
| getViewSize() { | |
| const fovInRadians = (this.camera.fov * Math.PI) / 180; | |
| const height = Math.abs( | |
| this.camera.position.z * Math.tan(fovInRadians / 2) * 2 | |
| ); | |
| return { width: height * this.camera.aspect, height }; | |
| } | |
| update(delta) { | |
| this.touchTexture.update(); | |
| this.gradientBackground.update(delta); | |
| } | |
| render() { | |
| const delta = this.clock.getDelta(); | |
| // Only update if delta is reasonable (prevents large jumps) | |
| const clampedDelta = Math.min(delta, 0.1); | |
| this.renderer.render(this.scene, this.camera); | |
| this.update(clampedDelta); | |
| } | |
| tick() { | |
| this.render(); | |
| // Use arrow function to maintain context and ensure continuous rendering | |
| requestAnimationFrame(() => this.tick()); | |
| } | |
| onResize() { | |
| this.camera.aspect = window.innerWidth / window.innerHeight; | |
| this.camera.updateProjectionMatrix(); | |
| this.renderer.setSize(window.innerWidth, window.innerHeight); | |
| this.gradientBackground.onResize(window.innerWidth, window.innerHeight); | |
| } | |
| } | |
| // Start the app | |
| const app = new App(); | |
| // Force animation to start immediately by triggering a render | |
| // This helps prevent browser throttling of requestAnimationFrame | |
| if (document.readyState === "loading") { | |
| document.addEventListener("DOMContentLoaded", () => { | |
| app.render(); | |
| }); | |
| } else { | |
| // DOM already loaded, force immediate render | |
| setTimeout(() => app.render(), 0); | |
| } | |
| // Color scheme buttons | |
| const colorButtons = document.querySelectorAll(".color-btn"); | |
| colorButtons.forEach((btn) => { | |
| btn.addEventListener("click", () => { | |
| const scheme = parseInt(btn.dataset.scheme); | |
| app.setColorScheme(scheme); | |
| // Update active state | |
| colorButtons.forEach((b) => b.classList.remove("active")); | |
| btn.classList.add("active"); | |
| // Update color pickers when scheme changes | |
| updateColorPickersFromScheme(); | |
| }); | |
| }); | |
| // Color Adjuster Panel Functions | |
| const colorAdjusterPanel = document.getElementById("colorAdjusterPanel"); | |
| const toggleAdjusterBtn = document.getElementById("toggleAdjusterBtn"); | |
| const closeAdjusterBtn = document.getElementById("closeAdjusterBtn"); | |
| // Toggle panel | |
| toggleAdjusterBtn.addEventListener("click", () => { | |
| colorAdjusterPanel.classList.toggle("open"); | |
| if (colorAdjusterPanel.classList.contains("open")) { | |
| updateColorPickersFromScheme(); | |
| toggleAdjusterBtn.style.display = "none"; | |
| } else { | |
| toggleAdjusterBtn.style.display = "block"; | |
| } | |
| }); | |
| closeAdjusterBtn.addEventListener("click", () => { | |
| colorAdjusterPanel.classList.remove("open"); | |
| toggleAdjusterBtn.style.display = "block"; | |
| }); | |
| // Convert RGB (0-1) to Hex | |
| function rgbToHex(r, g, b) { | |
| const toHex = (n) => { | |
| const hex = Math.round(n * 255).toString(16); | |
| return hex.length === 1 ? "0" + hex : hex; | |
| }; | |
| return "#" + toHex(r) + toHex(g) + toHex(b); | |
| } | |
| // Convert Hex to RGB (0-1) | |
| function hexToRgb(hex) { | |
| const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); | |
| return result | |
| ? { | |
| r: parseInt(result[1], 16) / 255, | |
| g: parseInt(result[2], 16) / 255, | |
| b: parseInt(result[3], 16) / 255 | |
| } | |
| : null; | |
| } | |
| // Update color pickers from current scheme | |
| function updateColorPickersFromScheme() { | |
| const uniforms = app.gradientBackground.uniforms; | |
| const colors = [ | |
| uniforms.uColor1.value, | |
| uniforms.uColor2.value, | |
| uniforms.uColor3.value, | |
| uniforms.uColor4.value, | |
| uniforms.uColor5.value, | |
| uniforms.uColor6.value | |
| ]; | |
| colors.forEach((color, index) => { | |
| const picker = document.getElementById(`colorPicker${index + 1}`); | |
| const display = document.getElementById(`colorValue${index + 1}`); | |
| const hex = rgbToHex(color.x, color.y, color.z); | |
| picker.value = hex; | |
| display.value = hex.toUpperCase(); | |
| }); | |
| } | |
| // Update gradient when color picker changes | |
| for (let i = 1; i <= 6; i++) { | |
| const picker = document.getElementById(`colorPicker${i}`); | |
| const display = document.getElementById(`colorValue${i}`); | |
| picker.addEventListener("input", (e) => { | |
| const hex = e.target.value; | |
| const rgb = hexToRgb(hex); | |
| if (rgb) { | |
| const uniforms = app.gradientBackground.uniforms; | |
| const colorUniform = uniforms[`uColor${i}`]; | |
| if (colorUniform) { | |
| colorUniform.value.set(rgb.r, rgb.g, rgb.b); | |
| display.value = hex.toUpperCase(); | |
| } | |
| } | |
| }); | |
| } | |
| // Copy color value | |
| document.querySelectorAll(".copy-btn").forEach((btn) => { | |
| btn.addEventListener("click", (e) => { | |
| const colorIndex = e.target.dataset.copy; | |
| const display = document.getElementById(`colorValue${colorIndex}`); | |
| const text = display.value; | |
| navigator.clipboard.writeText(text).then(() => { | |
| e.target.textContent = "Copied!"; | |
| e.target.classList.add("copied"); | |
| setTimeout(() => { | |
| e.target.textContent = "Copy"; | |
| e.target.classList.remove("copied"); | |
| }, 2000); | |
| }); | |
| }); | |
| }); | |
| // Export all colors | |
| const exportAllBtn = document.getElementById("exportAllBtn"); | |
| exportAllBtn.addEventListener("click", () => { | |
| const colors = []; | |
| for (let i = 1; i <= 6; i++) { | |
| const display = document.getElementById(`colorValue${i}`); | |
| colors.push(display.value); | |
| } | |
| const exportText = `Color Scheme:\n${colors | |
| .map((c, i) => `Color ${i + 1}: ${c}`) | |
| .join("\n")}\n\nHex Array: [${colors.map((c) => `"${c}"`).join(", ")}]`; | |
| navigator.clipboard.writeText(exportText).then(() => { | |
| exportAllBtn.textContent = "Copied!"; | |
| exportAllBtn.style.background = "rgba(76, 175, 80, 0.3)"; | |
| exportAllBtn.style.borderColor = "rgba(76, 175, 80, 0.5)"; | |
| setTimeout(() => { | |
| exportAllBtn.textContent = "Export All Colors"; | |
| exportAllBtn.style.background = ""; | |
| exportAllBtn.style.borderColor = ""; | |
| }, 2000); | |
| }); | |
| }); | |
| // Custom cursor | |
| const cursor = document.getElementById("customCursor"); | |
| let mouseX = 0; | |
| let mouseY = 0; | |
| let cursorX = 0; | |
| let cursorY = 0; | |
| document.addEventListener("mousemove", (e) => { | |
| mouseX = e.clientX; | |
| mouseY = e.clientY; | |
| }); | |
| let isCursorAnimating = false; | |
| function animateCursor() { | |
| if (!isCursorAnimating) return; | |
| // Instant following - no delay | |
| cursorX = mouseX; | |
| cursorY = mouseY; | |
| cursor.style.left = cursorX + "px"; | |
| cursor.style.top = cursorY + "px"; | |
| requestAnimationFrame(animateCursor); | |
| } | |
| // Only start animation when mouse moves | |
| document.addEventListener( | |
| "mousemove", | |
| () => { | |
| if (!isCursorAnimating) { | |
| isCursorAnimating = true; | |
| animateCursor(); | |
| } | |
| }, | |
| { once: false } | |
| ); | |
| // Cursor animation starts on first mouse move | |
| // Make cursor larger on hover over interactive elements | |
| const footerLink = document.querySelector(".footer a"); | |
| footerLink.addEventListener("mouseenter", () => { | |
| cursor.style.width = "50px"; | |
| cursor.style.height = "50px"; | |
| cursor.style.borderWidth = "3px"; | |
| }); | |
| footerLink.addEventListener("mouseleave", () => { | |
| cursor.style.width = "40px"; | |
| cursor.style.height = "40px"; | |
| cursor.style.borderWidth = "2px"; | |
| }); | |
| // Make cursor larger on hover over color buttons | |
| colorButtons.forEach((btn) => { | |
| btn.addEventListener("mouseenter", () => { | |
| cursor.style.width = "50px"; | |
| cursor.style.height = "50px"; | |
| cursor.style.borderWidth = "3px"; | |
| }); | |
| btn.addEventListener("mouseleave", () => { | |
| cursor.style.width = "40px"; | |
| cursor.style.height = "40px"; | |
| cursor.style.borderWidth = "2px"; | |
| }); | |
| }); | |
| // Make cursor larger on hover over toggle adjuster button | |
| toggleAdjusterBtn.addEventListener("mouseenter", () => { | |
| cursor.style.width = "50px"; | |
| cursor.style.height = "50px"; | |
| cursor.style.borderWidth = "3px"; | |
| }); | |
| toggleAdjusterBtn.addEventListener("mouseleave", () => { | |
| cursor.style.width = "40px"; | |
| cursor.style.height = "40px"; | |
| cursor.style.borderWidth = "2px"; | |
| }); | |
| // Optimized pulse effect - use requestAnimationFrame instead of setTimeout | |
| let lastMouseMoveTime = 0; | |
| let pulseFrame = null; | |
| function checkPulse() { | |
| if (Date.now() - lastMouseMoveTime > 100) { | |
| cursor.style.borderWidth = "2px"; | |
| pulseFrame = null; | |
| } else { | |
| pulseFrame = requestAnimationFrame(checkPulse); | |
| } | |
| } | |
| document.addEventListener("mousemove", () => { | |
| lastMouseMoveTime = Date.now(); | |
| cursor.style.borderWidth = "2.5px"; | |
| if (!pulseFrame) { | |
| pulseFrame = requestAnimationFrame(checkPulse); | |
| } | |
| }); |
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
| <script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script> |
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
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| overflow: hidden; | |
| font-family: sans-serif; | |
| } | |
| #webGLApp { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 1; | |
| } | |
| .heading { | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| z-index: 10; | |
| color: white; | |
| text-align: center; | |
| white-space: nowrap; | |
| pointer-events: none; | |
| text-wrap: auto; | |
| font-family: "Syne", sans-serif; | |
| font-size: clamp(3.5rem, 9vw, 9rem); | |
| font-weight: 700; | |
| letter-spacing: -0.02em; | |
| text-transform: none; | |
| line-height: 1; | |
| } | |
| /* Show all options in a grid for comparison */ | |
| .headings-container { | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| z-index: 10; | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| grid-template-rows: 1fr 1fr; | |
| gap: 4rem; | |
| width: 80vw; | |
| max-width: 1200px; | |
| pointer-events: none; | |
| } | |
| .color-controls { | |
| position: fixed; | |
| top: 2rem; | |
| right: 2rem; | |
| z-index: 10; | |
| display: flex; | |
| gap: 1rem; | |
| pointer-events: auto; | |
| } | |
| .color-btn { | |
| min-width: 48px; | |
| height: 48px; | |
| padding: 0.75rem 1.25rem; | |
| background: rgba(255, 255, 255, 0.1); | |
| border: 1px solid rgba(255, 255, 255, 0.3); | |
| border-radius: 24px; | |
| color: white; | |
| font-family: "Syne", sans-serif; | |
| font-size: 0.75rem; | |
| font-weight: 500; | |
| letter-spacing: 0.05em; | |
| text-transform: uppercase; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| backdrop-filter: blur(10px); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| white-space: nowrap; | |
| } | |
| .color-btn:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| border-color: rgba(255, 255, 255, 0.5); | |
| transform: translateY(-2px); | |
| } | |
| .color-btn.active { | |
| background: rgba(255, 255, 255, 0.25); | |
| border-color: rgba(255, 255, 255, 0.6); | |
| } | |
| /* Color Adjuster Panel */ | |
| .color-adjuster-panel { | |
| position: fixed; | |
| top: 2rem; | |
| left: 2rem; | |
| z-index: 100; | |
| background: rgba(255, 255, 255, 0.1); | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| min-width: 320px; | |
| max-width: 400px; | |
| pointer-events: auto; | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); | |
| transition: all 0.3s ease; | |
| display: none; | |
| } | |
| .color-adjuster-panel.open { | |
| display: block; | |
| } | |
| .color-adjuster-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1.5rem; | |
| padding-bottom: 1rem; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .color-adjuster-title { | |
| color: white; | |
| font-family: "Syne", sans-serif; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| letter-spacing: 0.05em; | |
| text-transform: uppercase; | |
| } | |
| .color-adjuster-close { | |
| background: none; | |
| border: none; | |
| color: white; | |
| font-size: 1.5rem; | |
| cursor: pointer; | |
| padding: 0; | |
| width: 24px; | |
| height: 24px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: opacity 0.3s ease; | |
| } | |
| .color-adjuster-close:hover { | |
| opacity: 0.7; | |
| } | |
| .color-picker-group { | |
| margin-bottom: 1.25rem; | |
| } | |
| .color-picker-label { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 0.5rem; | |
| color: white; | |
| font-family: "Inter", sans-serif; | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| } | |
| .color-picker-wrapper { | |
| display: flex; | |
| gap: 0.75rem; | |
| align-items: center; | |
| } | |
| .color-picker-input { | |
| width: 60px; | |
| height: 40px; | |
| border: 2px solid rgba(255, 255, 255, 0.3); | |
| border-radius: 8px; | |
| cursor: pointer; | |
| background: none; | |
| padding: 0; | |
| -webkit-appearance: none; | |
| appearance: none; | |
| } | |
| .color-picker-input::-webkit-color-swatch-wrapper { | |
| padding: 0; | |
| } | |
| .color-picker-input::-webkit-color-swatch { | |
| border: none; | |
| border-radius: 6px; | |
| } | |
| .color-value-display { | |
| flex: 1; | |
| background: rgba(0, 0, 0, 0.2); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| border-radius: 6px; | |
| padding: 0.5rem 0.75rem; | |
| color: white; | |
| font-family: "Inter", monospace; | |
| font-size: 0.75rem; | |
| font-weight: 500; | |
| letter-spacing: 0.05em; | |
| cursor: text; | |
| user-select: all; | |
| transition: all 0.3s ease; | |
| } | |
| .color-value-display:hover { | |
| background: rgba(0, 0, 0, 0.3); | |
| border-color: rgba(255, 255, 255, 0.4); | |
| } | |
| .color-value-display:focus { | |
| outline: none; | |
| background: rgba(0, 0, 0, 0.4); | |
| border-color: rgba(255, 255, 255, 0.5); | |
| } | |
| .copy-btn { | |
| background: rgba(255, 255, 255, 0.1); | |
| border: 1px solid rgba(255, 255, 255, 0.3); | |
| border-radius: 6px; | |
| padding: 0.5rem 0.75rem; | |
| color: white; | |
| font-family: "Inter", sans-serif; | |
| font-size: 0.75rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| backdrop-filter: blur(10px); | |
| } | |
| .copy-btn:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| border-color: rgba(255, 255, 255, 0.5); | |
| } | |
| .copy-btn.copied { | |
| background: rgba(76, 175, 80, 0.3); | |
| border-color: rgba(76, 175, 80, 0.5); | |
| } | |
| .color-adjuster-actions { | |
| margin-top: 1.5rem; | |
| padding-top: 1rem; | |
| border-top: 1px solid rgba(255, 255, 255, 0.1); | |
| display: flex; | |
| gap: 0.75rem; | |
| } | |
| .export-btn { | |
| flex: 1; | |
| background: rgba(255, 255, 255, 0.15); | |
| border: 1px solid rgba(255, 255, 255, 0.3); | |
| border-radius: 8px; | |
| padding: 0.75rem; | |
| color: white; | |
| font-family: "Syne", sans-serif; | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| letter-spacing: 0.05em; | |
| text-transform: uppercase; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| backdrop-filter: blur(10px); | |
| } | |
| .export-btn:hover { | |
| background: rgba(255, 255, 255, 0.25); | |
| border-color: rgba(255, 255, 255, 0.5); | |
| transform: translateY(-2px); | |
| } | |
| .toggle-adjuster-btn { | |
| position: fixed; | |
| top: 2rem; | |
| left: 2rem; | |
| z-index: 100; | |
| padding: 0.75rem 1.5rem; | |
| background: rgba(255, 255, 255, 0.1); | |
| border: 1px solid rgba(255, 255, 255, 0.3); | |
| color: white; | |
| font-family: "Syne", sans-serif; | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| letter-spacing: 0.05em; | |
| text-transform: uppercase; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| backdrop-filter: blur(10px); | |
| border-radius: 8px; | |
| pointer-events: auto; | |
| } | |
| .toggle-adjuster-btn:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| border-color: rgba(255, 255, 255, 0.5); | |
| transform: translateY(-2px); | |
| } | |
| .footer { | |
| position: fixed; | |
| bottom: 2rem; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| z-index: 10; | |
| color: white; | |
| font-family: "Syne", sans-serif; | |
| font-size: clamp(0.875rem, 1.5vw, 1.125rem); | |
| font-weight: 500; | |
| letter-spacing: 0.05em; | |
| text-transform: uppercase; | |
| text-align: center; | |
| pointer-events: auto; | |
| } | |
| .footer a { | |
| color: white; | |
| text-decoration: none; | |
| transition: opacity 0.3s ease; | |
| } | |
| .footer a:hover { | |
| opacity: 0.7; | |
| } | |
| .custom-cursor { | |
| position: fixed; | |
| width: 40px; | |
| height: 40px; | |
| border: 2px solid white; | |
| border-radius: 50%; | |
| pointer-events: none; | |
| z-index: 1000; | |
| transform: translate(-50%, -50%); | |
| transition: width 0.2s ease, height 0.2s ease, border-width 0.2s ease; | |
| background: transparent; | |
| will-change: transform; | |
| } | |
| .custom-cursor::before { | |
| content: ""; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| width: 8px; | |
| height: 8px; | |
| background: white; | |
| border-radius: 50%; | |
| } | |
| body { | |
| cursor: none; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment