You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, style = "gameboy", pixelSize = 4, crtDistortion = 0.05) {
const canvas = document.createElement('canvas');
const w = originalImg.width;
const h = originalImg.height;
canvas.width = w;
canvas.height = h;
// Use WebGL for a high-performance, single-pass CRT & Pixelation effect
const gl = canvas.getContext('webgl', { preserveDrawingBuffer: true }) ||
canvas.getContext('experimental-webgl', { preserveDrawingBuffer: true });
if (!gl) {
// Graceful fallback if WebGL is unavailable
const ctx = canvas.getContext('2d');
ctx.drawImage(originalImg, 0, 0);
return canvas;
}
// Vertex Shader: Standard Fullscreen Quad
const vsSource = `
attribute vec2 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
}
`;
// Fragment Shader: Applies Vita-Boy / Retro CRT / Pixel Effects in a single pass
const fsSource = `
precision mediump float;
varying vec2 v_texCoord;
uniform sampler2D u_image;
uniform float u_pixelSize;
uniform float u_distortion;
uniform float u_style; // 0=Color, 1=PipBoy, 2=VirtualBoy, 3=GameBoy
uniform vec2 u_resolution;
// Barrel Distortion to simulate curved CRT screens
vec2 crtDistortion(vec2 coord, float bend) {
vec2 cc = coord * 2.0 - 1.0;
float r2 = cc.x*cc.x + cc.y*cc.y;
cc *= 1.0 + bend * r2;
return cc * 0.5 + 0.5;
}
void main() {
vec2 coord = crtDistortion(v_texCoord, u_distortion);
// Black out pixels outside the distorted curvature edges
if (coord.x < 0.0 || coord.x > 1.0 || coord.y < 0.0 || coord.y > 1.0) {
gl_FragColor = vec4(0.04, 0.05, 0.04, 1.0);
return;
}
// Downsample simulation (Pixelation)
vec2 pxSize = max(vec2(1.0), vec2(u_pixelSize));
vec2 screenPx = coord * u_resolution;
// Snap to block centers for perfectly sharp retro pixels
vec2 snappedPx = floor(screenPx / pxSize) * pxSize + (pxSize * 0.5);
vec2 pixelatedCoord = snappedPx / u_resolution;
vec4 texColor = texture2D(u_image, pixelatedCoord);
float lum = dot(texColor.rgb, vec3(0.299, 0.587, 0.114));
vec3 finalColor;
// Apply selected retro console style aesthetics
if (u_style < 0.5) {
// 0: Color (Retro 8-bit System Vibrant)
float levels = 6.0;
finalColor = floor(texColor.rgb * levels + 0.5) / levels;
// Vibrance boost
finalColor = mix(vec3(lum), finalColor, 1.4);
} else if (u_style < 1.5) {
// 1: Pip-Boy (Amber/Green Glowing Terminals)
lum = clamp((lum - 0.05) * 1.3, 0.0, 1.0);
vec3 tint = vec3(0.2, 1.0, 0.2); // Green Phosphor
finalColor = tint * lum + tint * (lum * lum * 0.5); // Add CRT glow
} else if (u_style < 2.5) {
// 2: Virtual-Boy (Red Glowing Terminals)
lum = clamp((lum - 0.05) * 1.3, 0.0, 1.0);
vec3 tint = vec3(1.0, 0.1, 0.15); // Red Phosphor
finalColor = tint * lum + tint * (lum * lum * 0.5);
} else {
// 3: Vita-/Game-Boy (Classic 4-Shade Green LCD)
if (lum < 0.25) finalColor = vec3(15.0, 56.0, 15.0) / 255.0; // Darkest
else if (lum < 0.5) finalColor = vec3(48.0, 98.0, 48.0) / 255.0; // Dark
else if (lum < 0.75) finalColor = vec3(139.0, 172.0, 15.0) / 255.0; // Light
else finalColor = vec3(155.0, 188.0, 15.0) / 255.0; // Lightest
}
// High-resolution scanline simulation mapped precisely over retro pixels
float py = coord.y * u_resolution.y;
float sSize = max(2.0, pxSize.y * 2.0); // Scanline width matches retro block height
float scanline = sin(py * 3.14159265 * 2.0 / sSize);
float darken = 1.0;
if (u_style < 2.5) {
// Heavy scanlines for CRT-based devices/systems
darken = scanline > 0.0 ? 1.0 : 0.6;
} else {
// Very subtle scanlines mimicking LCD pixel grid gaps
darken = scanline > 0.0 ? 1.0 : 0.85;
}
// Screen edge vignette
vec2 cc = v_texCoord * 2.0 - 1.0;
float dist = length(cc);
float vignette = 1.0 - smoothstep(0.6, 1.4, dist);
finalColor *= darken * vignette;
gl_FragColor = vec4(clamp(finalColor, 0.0, 1.0), 1.0);
}
`;
function compileShader(glContext, type, source) {
const shader = glContext.createShader(type);
glContext.shaderSource(shader, source);
glContext.compileShader(shader);
if (!glContext.getShaderParameter(shader, glContext.COMPILE_STATUS)) {
console.error('Shader compilation failed:', glContext.getShaderInfoLog(shader));
glContext.deleteShader(shader);
return null;
}
return shader;
}
const vs = compileShader(gl, gl.VERTEX_SHADER, vsSource);
const fs = compileShader(gl, gl.FRAGMENT_SHADER, fsSource);
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
// Buffers layout: Triangles spanning -1 to 1 perfectly covering the viewport
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1.0, -1.0, 1.0, -1.0, -1.0, 1.0,
-1.0, 1.0, 1.0, -1.0, 1.0, 1.0,
]), gl.STATIC_DRAW);
const texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0.0, 0.0, 1.0, 0.0, 0.0, 1.0,
0.0, 1.0, 1.0, 0.0, 1.0, 1.0,
]), gl.STATIC_DRAW);
// Original Image ingestion as Texture
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
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.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
// Load texture data, wrapped in try-catch in case of CORS security errors
try {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, originalImg);
} catch (err) {
console.error("Could not load image onto webgl texture", err);
return canvas;
}
gl.viewport(0, 0, w, h);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
// Attribute Pointers
const posLoc = gl.getAttribLocation(program, "a_position");
gl.enableVertexAttribArray(posLoc);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
const texLoc = gl.getAttribLocation(program, "a_texCoord");
gl.enableVertexAttribArray(texLoc);
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.vertexAttribPointer(texLoc, 2, gl.FLOAT, false, 0, 0);
// Resolve Style Setting
let styleNum = 3;
const sStr = String(style).toLowerCase();
if (sStr === 'color') styleNum = 0;
else if (sStr === 'pipboy' || sStr === 'green') styleNum = 1;
else if (sStr === 'virtualboy' || sStr === 'red') styleNum = 2;
else if (sStr === 'gameboy') styleNum = 3;
// Send Uniforms
gl.uniform2f(gl.getUniformLocation(program, "u_resolution"), w, h);
gl.uniform1f(gl.getUniformLocation(program, "u_style"), styleNum);
let pSize = parseFloat(pixelSize);
if (isNaN(pSize) || pSize < 1) pSize = 4.0;
gl.uniform1f(gl.getUniformLocation(program, "u_pixelSize"), pSize);
let distort = parseFloat(crtDistortion);
if (isNaN(distort)) distort = 0.05;
gl.uniform1f(gl.getUniformLocation(program, "u_distortion"), distort);
// Execute Pass
gl.drawArrays(gl.TRIANGLES, 0, 6);
return canvas;
}
Apply Changes