You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, intensity = 15.0, speed = 2.0, spiralArms = 6.0) {
// Parse parameters to ensure they are valid numbers
const twistIntensity = Number(intensity) || 15.0;
const twistSpeed = Number(speed) || 2.0;
const armsCount = Number(spiralArms) || 6.0;
const canvas = document.createElement('canvas');
canvas.width = originalImg.width;
canvas.height = originalImg.height;
// Attempt to get WebGL context
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
// Fallback if WebGL isn't supported
if (!gl) {
console.warn("WebGL not supported, returning unmodified image.");
const ctx = canvas.getContext('2d');
ctx.drawImage(originalImg, 0, 0);
return canvas;
}
// Vertex Shader: passes positions and creates UV coordinates
const vsSource = `
attribute vec2 a_position;
varying vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_position * 0.5 + 0.5; // Map from [-1, 1] to [0, 1]
}
`;
// Fragment Shader: core logic for the hypnosis effect
const fsSource = `
precision mediump float;
varying vec2 v_texCoord;
uniform sampler2D u_image;
uniform vec2 u_resolution;
uniform float u_time;
uniform float u_intensity;
uniform float u_speed;
uniform float u_arms;
void main() {
vec2 center = vec2(0.5, 0.5);
vec2 uv = v_texCoord;
// Adjust to maintain a perfect circular effect regardless of aspect ratio
float maxSide = max(u_resolution.x, u_resolution.y);
vec2 aspect = u_resolution / maxSide;
vec2 rel = (uv - center) * aspect;
float dist = length(rel);
float angle = atan(rel.y, rel.x);
// Create a twist based on distance (stronger in the center)
float twistAmount = u_intensity * smoothstep(1.2, 0.0, dist);
// Add animation time for continuous swirling
float currentAngle = angle + twistAmount - (u_time * u_speed);
// Calculate new UVs applied with the distortion
vec2 newRel = vec2(cos(currentAngle), sin(currentAngle)) * dist;
vec2 newUv = center + (newRel / aspect);
// Mirrored wrap (ping-pong repetition) for out-of-bounds UVs
vec2 mirroredUv = 1.0 - abs(mod(newUv, 2.0) - 1.0);
vec4 color = texture2D(u_image, mirroredUv);
// Create a counter-rotating spiral overlay for an extra hypnotic visual
float spiral = sin(dist * 35.0 + angle * u_arms + u_time * u_speed * 4.0);
float spiralOverlay = 0.85 + 0.15 * spiral;
// Pulsing vignette (darkening) at edges
float pulse = 0.5 + 0.5 * sin(u_time * 2.0);
float vignette = smoothstep(1.1, 0.3, dist);
// Composite the final color
vec3 finalColor = color.rgb * spiralOverlay * (0.6 + 0.4 * vignette + 0.1 * pulse);
gl_FragColor = vec4(finalColor, color.a);
}
`;
// Helper to compile shaders
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource);
// Link shader program
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error("Program link failed", gl.getProgramInfoLog(program));
return canvas;
}
gl.useProgram(program);
// Setup viewport quad positions
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const positions = [
-1.0, -1.0,
1.0, -1.0,
-1.0, 1.0,
1.0, 1.0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
const positionLocation = gl.getAttribLocation(program, "a_position");
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
// Upload Image Texture
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); // Ensure Image is right side up in WebGL coordinates
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);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, originalImg);
// Map Uniforms
const resolutionLocation = gl.getUniformLocation(program, "u_resolution");
const timeLocation = gl.getUniformLocation(program, "u_time");
const intensityLocation = gl.getUniformLocation(program, "u_intensity");
const speedLocation = gl.getUniformLocation(program, "u_speed");
const armsLocation = gl.getUniformLocation(program, "u_arms");
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);
gl.uniform1f(intensityLocation, twistIntensity);
gl.uniform1f(speedLocation, twistSpeed);
gl.uniform1f(armsLocation, armsCount);
let startTime = performance.now();
let animationFrameId;
function render(now) {
// Calculate elapsed time in seconds
const time = (now - startTime) / 1000.0;
gl.uniform1f(timeLocation, time);
// Draw Frame
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
// Continuous Hypnosis loop
animationFrameId = requestAnimationFrame(render);
}
// Attach a stop function in case the caller wants to clear resources cleanly
canvas.stopHypnosis = () => {
cancelAnimationFrame(animationFrameId);
};
// Start the hypnotic loop
requestAnimationFrame(render);
return canvas;
}
Apply Changes