import { useState, useRef, useCallback, useEffect } from "react"; import { Upload, Download, X, ImageIcon } from "lucide-react"; import { cn } from "../lib/utils"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const CHECKERBOARD: React.CSSProperties = { backgroundImage: ` linear-gradient(45deg, #444 25%, transparent 25%), linear-gradient(-45deg, #444 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #444 75%), linear-gradient(-45deg, transparent 75%, #444 75%) `, backgroundSize: "16px 16px", backgroundPosition: "0 0, 0 8px, 8px -8px, -8px 0px", backgroundColor: "#2a2a2a", }; // --------------------------------------------------------------------------- // WebGL — shaders + init // --------------------------------------------------------------------------- const VERT = ` attribute vec2 aPos; varying vec2 vUV; void main() { vUV = aPos * 0.5 + 0.5; vUV.y = 1.0 - vUV.y; gl_Position = vec4(aPos, 0.0, 1.0); }`; // RGB↔HSL math in GLSL — runs massively parallel on GPU const FRAG = ` precision mediump float; uniform sampler2D uImage; uniform float uHueShift; uniform float uSaturation; uniform float uLightness; varying vec2 vUV; vec3 rgb2hsl(vec3 c) { float maxC = max(c.r, max(c.g, c.b)); float minC = min(c.r, min(c.g, c.b)); float l = (maxC + minC) * 0.5; float d = maxC - minC; if (d < 0.001) return vec3(0.0, 0.0, l); float s = l > 0.5 ? d / (2.0 - maxC - minC) : d / (maxC + minC); float h; if (maxC == c.r) h = (c.g - c.b) / d + (c.g < c.b ? 6.0 : 0.0); else if (maxC == c.g) h = (c.b - c.r) / d + 2.0; else h = (c.r - c.g) / d + 4.0; return vec3(h / 6.0, s, l); } float hue2rgb(float p, float q, float t) { if (t < 0.0) t += 1.0; if (t > 1.0) t -= 1.0; if (t < 1.0 / 6.0) return p + (q - p) * 6.0 * t; if (t < 0.5) return q; if (t < 2.0 / 3.0) return p + (q - p) * (2.0 / 3.0 - t) * 6.0; return p; } vec3 hsl2rgb(vec3 hsl) { float h = hsl.x, s = hsl.y, l = hsl.z; if (s < 0.001) return vec3(l); float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s; float p = 2.0 * l - q; return vec3( hue2rgb(p, q, h + 1.0 / 3.0), hue2rgb(p, q, h), hue2rgb(p, q, h - 1.0 / 3.0) ); } void main() { vec4 c = texture2D(uImage, vUV); if (c.a < 0.001) { gl_FragColor = c; return; } vec3 hsl = rgb2hsl(c.rgb); hsl.x = fract(hsl.x + uHueShift + 1.0); hsl.y = clamp(hsl.y + uSaturation, 0.0, 1.0); hsl.z = clamp(hsl.z + uLightness, 0.0, 1.0); gl_FragColor = vec4(hsl2rgb(hsl), c.a); }`; type GlState = { gl: WebGLRenderingContext; tex: WebGLTexture; uHueShift: WebGLUniformLocation; uSaturation: WebGLUniformLocation; uLightness: WebGLUniformLocation; }; function initGl(canvas: HTMLCanvasElement): GlState | null { const gl = canvas.getContext("webgl", { premultipliedAlpha: false, preserveDrawingBuffer: true, }) as WebGLRenderingContext | null; if (!gl) return null; const vert = gl.createShader(gl.VERTEX_SHADER)!; gl.shaderSource(vert, VERT); gl.compileShader(vert); const frag = gl.createShader(gl.FRAGMENT_SHADER)!; gl.shaderSource(frag, FRAG); gl.compileShader(frag); const prog = gl.createProgram()!; gl.attachShader(prog, vert); gl.attachShader(prog, frag); gl.linkProgram(prog); gl.useProgram(prog); const buf = gl.createBuffer()!; gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData( gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW, ); const aPos = gl.getAttribLocation(prog, "aPos"); gl.enableVertexAttribArray(aPos); gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0); const tex = gl.createTexture()!; gl.bindTexture(gl.TEXTURE_2D, tex); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); return { gl, tex, uHueShift: gl.getUniformLocation(prog, "uHueShift")!, uSaturation: gl.getUniformLocation(prog, "uSaturation")!, uLightness: gl.getUniformLocation(prog, "uLightness")!, }; } // --------------------------------------------------------------------------- // Slider // --------------------------------------------------------------------------- function Slider({ label, value, min, max, unit, onChange, gradient, }: { label: string; value: number; min: number; max: number; unit: string; onChange: (v: number) => void; gradient?: string; }) { return (
{label}
{`${value > 0 ? "+" : ""}${value}${unit}`}Upload an image and shift its hue, saturation, and lightness to create colour variants. Fully transparent pixels are preserved.
{dragOver ? "Drop to upload" : "Drop an image here"}
or click to browse · PNG, JPEG, WebP · max 15 MB
Original
Result