/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable react-refresh/only-export-components */
'use client';
import { useEffect, useRef, useState } from 'react';
const defaultParams = {
patternScale: 2,
refraction: 0.015,
edge: 1,
patternBlur: 0.005,
liquid: 0.07,
speed: 0.3
};
export function parseLogoImage(file) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
return new Promise((resolve, reject) => {
if (!file || !ctx) {
reject(new Error('Invalid file or context'));
return;
}
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function () {
if (file.type === 'image/svg+xml') {
img.width = 1000;
img.height = 1000;
}
const MAX_SIZE = 1000;
const MIN_SIZE = 500;
let width = img.naturalWidth;
let height = img.naturalHeight;
if (width > MAX_SIZE || height > MAX_SIZE || width < MIN_SIZE || height < MIN_SIZE) {
if (width > height) {
if (width > MAX_SIZE) {
height = Math.round((height * MAX_SIZE) / width);
width = MAX_SIZE;
} else if (width < MIN_SIZE) {
height = Math.round((height * MIN_SIZE) / width);
width = MIN_SIZE;
}
} else {
if (height > MAX_SIZE) {
width = Math.round((width * MAX_SIZE) / height);
height = MAX_SIZE;
} else if (height < MIN_SIZE) {
width = Math.round((width * MIN_SIZE) / height);
height = MIN_SIZE;
}
}
}
canvas.width = width;
canvas.height = height;
const shapeCanvas = document.createElement('canvas');
shapeCanvas.width = width;
shapeCanvas.height = height;
const shapeCtx = shapeCanvas.getContext('2d');
shapeCtx.drawImage(img, 0, 0, width, height);
const shapeImageData = shapeCtx.getImageData(0, 0, width, height);
const data = shapeImageData.data;
const shapeMask = new Array(width * height).fill(false);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx4 = (y * width + x) * 4;
const r = data[idx4];
const g = data[idx4 + 1];
const b = data[idx4 + 2];
const a = data[idx4 + 3];
shapeMask[y * width + x] = !((r === 255 && g === 255 && b === 255 && a === 255) || a === 0);
}
}
function inside(x, y) {
if (x < 0 || x >= width || y < 0 || y >= height) return false;
return shapeMask[y * width + x];
}
const boundaryMask = new Array(width * height).fill(false);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = y * width + x;
if (!shapeMask[idx]) continue;
let isBoundary = false;
for (let ny = y - 1; ny <= y + 1 && !isBoundary; ny++) {
for (let nx = x - 1; nx <= x + 1 && !isBoundary; nx++) {
if (!inside(nx, ny)) {
isBoundary = true;
}
}
}
if (isBoundary) {
boundaryMask[idx] = true;
}
}
}
const interiorMask = new Array(width * height).fill(false);
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
const idx = y * width + x;
if (
shapeMask[idx] &&
shapeMask[idx - 1] &&
shapeMask[idx + 1] &&
shapeMask[idx - width] &&
shapeMask[idx + width]
) {
interiorMask[idx] = true;
}
}
}
const u = new Float32Array(width * height).fill(0);
const newU = new Float32Array(width * height).fill(0);
const C = 0.01;
const ITERATIONS = 300;
function getU(x, y, arr) {
if (x < 0 || x >= width || y < 0 || y >= height) return 0;
if (!shapeMask[y * width + x]) return 0;
return arr[y * width + x];
}
for (let iter = 0; iter < ITERATIONS; iter++) {
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = y * width + x;
if (!shapeMask[idx] || boundaryMask[idx]) {
newU[idx] = 0;
continue;
}
const sumN = getU(x + 1, y, u) + getU(x - 1, y, u) + getU(x, y + 1, u) + getU(x, y - 1, u);
newU[idx] = (C + sumN) / 4;
}
}
u.set(newU);
}
let maxVal = 0;
for (let i = 0; i < width * height; i++) {
if (u[i] > maxVal) maxVal = u[i];
}
const alpha = 2.0;
const outImg = ctx.createImageData(width, height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = y * width + x;
const px = idx * 4;
if (!shapeMask[idx]) {
outImg.data[px] = 255;
outImg.data[px + 1] = 255;
outImg.data[px + 2] = 255;
outImg.data[px + 3] = 255;
} else {
const raw = u[idx] / maxVal;
const remapped = Math.pow(raw, alpha);
const gray = 255 * (1 - remapped);
outImg.data[px] = gray;
outImg.data[px + 1] = gray;
outImg.data[px + 2] = gray;
outImg.data[px + 3] = 255;
}
}
}
ctx.putImageData(outImg, 0, 0);
canvas.toBlob(blob => {
if (!blob) {
reject(new Error('Failed to create PNG blob'));
return;
}
resolve({
imageData: outImg,
pngBlob: blob
});
}, 'image/png');
};
img.onerror = () => reject(new Error('Failed to load image'));
img.src = URL.createObjectURL(file);
});
}
const vertexShaderSource = `#version 300 es
precision mediump float;
in vec2 a_position;
out vec2 vUv;
void main() {
vUv = .5 * (a_position + 1.);
gl_Position = vec4(a_position, 0.0, 1.0);
}`;
const liquidFragSource = `#version 300 es
precision mediump float;
in vec2 vUv;
out vec4 fragColor;
uniform sampler2D u_image_texture;
uniform float u_time;
uniform float u_ratio;
uniform float u_img_ratio;
uniform float u_patternScale;
uniform float u_refraction;
uniform float u_edge;
uniform float u_patternBlur;
uniform float u_liquid;
#define TWO_PI 6.28318530718
#define PI 3.14159265358979323846
vec3 mod289(vec3 x) { return x - floor(x * (1. / 289.)) * 289.; }
vec2 mod289(vec2 x) { return x - floor(x * (1. / 289.)) * 289.; }
vec3 permute(vec3 x) { return mod289(((x*34.)+1.)*x); }
float snoise(vec2 v) {
const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);
vec2 i = floor(v + dot(v, C.yy));
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1;
i1 = (x0.x > x0.y) ? vec2(1., 0.) : vec2(0., 1.);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod289(i);
vec3 p = permute(permute(i.y + vec3(0., i1.y, 1.)) + i.x + vec3(0., i1.x, 1.));
vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.);
m = m*m;
m = m*m;
vec3 x = 2. * fract(p * C.www) - 1.;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130. * dot(m, g);
}
vec2 get_img_uv() {
vec2 img_uv = vUv;
img_uv -= .5;
if (u_ratio > u_img_ratio) {
img_uv.x = img_uv.x * u_ratio / u_img_ratio;
} else {
img_uv.y = img_uv.y * u_img_ratio / u_ratio;
}
float scale_factor = 1.;
img_uv *= scale_factor;
img_uv += .5;
img_uv.y = 1. - img_uv.y;
return img_uv;
}
vec2 rotate(vec2 uv, float th) {
return mat2(cos(th), sin(th), -sin(th), cos(th)) * uv;
}
float get_color_channel(float c1, float c2, float stripe_p, vec3 w, float extra_blur, float b) {
float ch = c2;
float border = 0.;
float blur = u_patternBlur + extra_blur;
ch = mix(ch, c1, smoothstep(.0, blur, stripe_p));
border = w[0];
ch = mix(ch, c2, smoothstep(border - blur, border + blur, stripe_p));
b = smoothstep(.2, .8, b);
border = w[0] + .4 * (1. - b) * w[1];
ch = mix(ch, c1, smoothstep(border - blur, border + blur, stripe_p));
border = w[0] + .5 * (1. - b) * w[1];
ch = mix(ch, c2, smoothstep(border - blur, border + blur, stripe_p));
border = w[0] + w[1];
ch = mix(ch, c1, smoothstep(border - blur, border + blur, stripe_p));
float gradient_t = (stripe_p - w[0] - w[1]) / w[2];
float gradient = mix(c1, c2, smoothstep(0., 1., gradient_t));
ch = mix(ch, gradient, smoothstep(border - blur, border + blur, stripe_p));
return ch;
}
float get_img_frame_alpha(vec2 uv, float img_frame_width) {
float img_frame_alpha = smoothstep(0., img_frame_width, uv.x) * smoothstep(1., 1. - img_frame_width, uv.x);
img_frame_alpha *= smoothstep(0., img_frame_width, uv.y) * smoothstep(1., 1. - img_frame_width, uv.y);
return img_frame_alpha;
}
void main() {
vec2 uv = vUv;
uv.y = 1. - uv.y;
uv.x *= u_ratio;
float diagonal = uv.x - uv.y;
float t = .001 * u_time;
vec2 img_uv = get_img_uv();
vec4 img = texture(u_image_texture, img_uv);
vec3 color = vec3(0.);
float opacity = 1.;
vec3 color1 = vec3(.98, 0.98, 1.);
vec3 color2 = vec3(.1, .1, .1 + .1 * smoothstep(.7, 1.3, uv.x + uv.y));
float edge = img.r;
vec2 grad_uv = uv;
grad_uv -= .5;
float dist = length(grad_uv + vec2(0., .2 * diagonal));
grad_uv = rotate(grad_uv, (.25 - .2 * diagonal) * PI);
float bulge = pow(1.8 * dist, 1.2);
bulge = 1. - bulge;
bulge *= pow(uv.y, .3);
float cycle_width = u_patternScale;
float thin_strip_1_ratio = .12 / cycle_width * (1. - .4 * bulge);
float thin_strip_2_ratio = .07 / cycle_width * (1. + .4 * bulge);
float wide_strip_ratio = (1. - thin_strip_1_ratio - thin_strip_2_ratio);
float thin_strip_1_width = cycle_width * thin_strip_1_ratio;
float thin_strip_2_width = cycle_width * thin_strip_2_ratio;
opacity = 1. - smoothstep(.9 - .5 * u_edge, 1. - .5 * u_edge, edge);
opacity *= get_img_frame_alpha(img_uv, 0.01);
float noise = snoise(uv - t);
edge += (1. - edge) * u_liquid * noise;
float refr = 0.;
refr += (1. - bulge);
refr = clamp(refr, 0., 1.);
float dir = grad_uv.x;
dir += diagonal;
dir -= 2. * noise * diagonal * (smoothstep(0., 1., edge) * smoothstep(1., 0., edge));
bulge *= clamp(pow(uv.y, .1), .3, 1.);
dir *= (.1 + (1.1 - edge) * bulge);
dir *= smoothstep(1., .7, edge);
dir += .18 * (smoothstep(.1, .2, uv.y) * smoothstep(.4, .2, uv.y));
dir += .03 * (smoothstep(.1, .2, 1. - uv.y) * smoothstep(.4, .2, 1. - uv.y));
dir *= (.5 + .5 * pow(uv.y, 2.));
dir *= cycle_width;
dir -= t;
float refr_r = refr;
refr_r += .03 * bulge * noise;
float refr_b = 1.3 * refr;
refr_r += 5. * (smoothstep(-.1, .2, uv.y) * smoothstep(.5, .1, uv.y)) * (smoothstep(.4, .6, bulge) * smoothstep(1., .4, bulge));
refr_r -= diagonal;
refr_b += (smoothstep(0., .4, uv.y) * smoothstep(.8, .1, uv.y)) * (smoothstep(.4, .6, bulge) * smoothstep(.8, .4, bulge));
refr_b -= .2 * edge;
refr_r *= u_refraction;
refr_b *= u_refraction;
vec3 w = vec3(thin_strip_1_width, thin_strip_2_width, wide_strip_ratio);
w[1] -= .02 * smoothstep(.0, 1., edge + bulge);
float stripe_r = mod(dir + refr_r, 1.);
float r = get_color_channel(color1.r, color2.r, stripe_r, w, 0.02 + .03 * u_refraction * bulge, bulge);
float stripe_g = mod(dir, 1.);
float g = get_color_channel(color1.g, color2.g, stripe_g, w, 0.01 / (1. - diagonal), bulge);
float stripe_b = mod(dir - refr_b, 1.);
float b = get_color_channel(color1.b, color2.b, stripe_b, w, .01, bulge);
color = vec3(r, g, b);
color *= opacity;
fragColor = vec4(color, opacity);
}
`;
export default function MetallicPaint({ imageData, params = defaultParams }) {
const canvasRef = useRef(null);
const [gl, setGl] = useState(null);
const [uniforms, setUniforms] = useState({});
const totalAnimationTime = useRef(0);
const lastRenderTime = useRef(0);
function updateUniforms() {
if (!gl || !uniforms) return;
gl.uniform1f(uniforms.u_edge, params.edge);
gl.uniform1f(uniforms.u_patternBlur, params.patternBlur);
gl.uniform1f(uniforms.u_time, 0);
gl.uniform1f(uniforms.u_patternScale, params.patternScale);
gl.uniform1f(uniforms.u_refraction, params.refraction);
gl.uniform1f(uniforms.u_liquid, params.liquid);
}
useEffect(() => {
function initShader() {
const canvas = canvasRef.current;
const gl = canvas?.getContext('webgl2', {
antialias: true,
alpha: true
});
if (!canvas || !gl) {
return;
}
function createShader(gl, sourceCode, type) {
const shader = gl.createShader(type);
if (!shader) {
return null;
}
gl.shaderSource(shader, sourceCode);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
const vertexShader = createShader(gl, vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = createShader(gl, liquidFragSource, gl.FRAGMENT_SHADER);
const program = gl.createProgram();
if (!program || !vertexShader || !fragmentShader) {
return;
}
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Unable to initialize the shader program: ' + gl.getProgramInfoLog(program));
return null;
}
function getUniforms(program, gl) {
let uniforms = {};
let uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
for (let i = 0; i < uniformCount; i++) {
let uniformName = gl.getActiveUniform(program, i)?.name;
if (!uniformName) continue;
uniforms[uniformName] = gl.getUniformLocation(program, uniformName);
}
return uniforms;
}
const uniforms = getUniforms(program, gl);
setUniforms(uniforms);
const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.useProgram(program);
const positionLocation = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(positionLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
setGl(gl);
}
initShader();
updateUniforms();
}, []);
useEffect(() => {
if (!gl || !uniforms) return;
updateUniforms();
}, [gl, params, uniforms]);
useEffect(() => {
if (!gl || !uniforms) return;
let renderId;
function render(currentTime) {
const deltaTime = currentTime - lastRenderTime.current;
lastRenderTime.current = currentTime;
totalAnimationTime.current += deltaTime * params.speed;
gl.uniform1f(uniforms.u_time, totalAnimationTime.current);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
renderId = requestAnimationFrame(render);
}
lastRenderTime.current = performance.now();
renderId = requestAnimationFrame(render);
return () => {
cancelAnimationFrame(renderId);
};
}, [gl, params.speed]);
useEffect(() => {
const canvasEl = canvasRef.current;
if (!canvasEl || !gl || !uniforms) return;
function resizeCanvas() {
if (!canvasEl || !gl || !uniforms || !imageData) return;
const imgRatio = imageData.width / imageData.height;
gl.uniform1f(uniforms.u_img_ratio, imgRatio);
const side = 1000;
canvasEl.width = side * devicePixelRatio;
canvasEl.height = side * devicePixelRatio;
gl.viewport(0, 0, canvasEl.height, canvasEl.height);
gl.uniform1f(uniforms.u_ratio, 1);
gl.uniform1f(uniforms.u_img_ratio, imgRatio);
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
return () => {
window.removeEventListener('resize', resizeCanvas);
};
}, [gl, uniforms, imageData]);
useEffect(() => {
if (!gl || !uniforms) return;
const existingTexture = gl.getParameter(gl.TEXTURE_BINDING_2D);
if (existingTexture) {
gl.deleteTexture(existingTexture);
}
const imageTexture = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, imageTexture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
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.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
try {
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
imageData?.width,
imageData?.height,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
imageData?.data
);
gl.uniform1i(uniforms.u_image_texture, 0);
} catch (e) {
console.error('Error uploading texture:', e);
}
return () => {
if (imageTexture) {
gl.deleteTexture(imageTexture);
}
};
}, [gl, uniforms, imageData]);
return <canvas ref={canvasRef} className="block w-full h-full object-contain" />;
}