Skip to content

Instantly share code, notes, and snippets.

@bigmistqke
Last active May 12, 2025 01:58
Show Gist options
  • Save bigmistqke/f1a9b6c62a9e0a6e41eab5ade4e6e6f5 to your computer and use it in GitHub Desktop.
Save bigmistqke/f1a9b6c62a9e0a6e41eab5ade4e6e6f5 to your computer and use it in GitHub Desktop.
pixelate svg component
import { render } from "solid-js/web";
import {
type JSX,
type ComponentProps,
type Component,
splitProps,
createSignal,
Show,
createResource,
onCleanup,
onMount,
} from "solid-js";
export function defer<T = void>() {
let resolve: (value: T) => void = null!;
let reject: (value: unknown) => void = null!;
return {
promise: new Promise<T>(
(_resolve, _reject) => ((resolve = _resolve), (reject = _reject)),
),
resolve,
reject,
};
}
interface WebGLImageProps
extends Omit<ComponentProps<"canvas">, "width" | "height"> {
data: ImageData;
}
const WebGLImage: Component<WebGLImageProps> = (props) => {
const [, rest] = splitProps(props, ["style"]);
let canvas!: HTMLCanvasElement;
onMount(() => {
const gl = canvas.getContext("webgl2")!;
if (!gl) throw new Error("WebGL2 not supported");
const vertexShaderSource = `#version 300 es
in vec2 a_position;
in vec2 a_texcoord;
out vec2 v_texcoord;
void main() {
gl_Position = vec4(a_position, 0, 1);
v_texcoord = a_texcoord;
}`;
const fragmentShaderSource = `#version 300 es
precision mediump float;
in vec2 v_texcoord;
out vec4 outColor;
uniform sampler2D u_texture;
void main() {
outColor = texture(u_texture, v_texcoord);
}`;
function compileShader(type: number, source: string): WebGLShader {
const shader = gl.createShader(type)!;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(shader) || "Shader compile error");
}
return shader;
}
const vs = compileShader(gl.VERTEX_SHADER, vertexShaderSource);
const fs = compileShader(gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = gl.createProgram()!;
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(gl.getProgramInfoLog(program) || "Program link error");
}
gl.useProgram(program);
const quadVerts = new Float32Array([
// x, y, u, v
-1,
-1,
0,
1, // bottom-left
1,
-1,
1,
1, // bottom-right
-1,
1,
0,
0, // top-left
-1,
1,
0,
0, // top-left
1,
-1,
1,
1, // bottom-right
1,
1,
1,
0, // top-right
]);
const vao = gl.createVertexArray()!;
gl.bindVertexArray(vao);
const buffer = gl.createBuffer()!;
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, quadVerts, gl.STATIC_DRAW);
const a_position = gl.getAttribLocation(program, "a_position");
gl.enableVertexAttribArray(a_position);
gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 16, 0);
const a_texcoord = gl.getAttribLocation(program, "a_texcoord");
gl.enableVertexAttribArray(a_texcoord);
gl.vertexAttribPointer(a_texcoord, 2, gl.FLOAT, false, 16, 8);
const texture = gl.createTexture()!;
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
props.data.width,
props.data.height,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
props.data.data,
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 6);
onCleanup(() => {
gl.deleteProgram(program);
gl.deleteShader(vs);
gl.deleteShader(fs);
gl.deleteBuffer(buffer);
gl.deleteVertexArray(vao);
gl.deleteTexture(texture);
});
});
return (
<canvas
ref={canvas}
width={props.data.width}
height={props.data.height}
style={{ "image-rendering": "pixelated", ...props.style }}
{...rest}
/>
);
};
function getPixelatedImageData(pixels: number, svg: JSX.Element) {
const { promise, resolve } = defer<ImageData>();
const [ratio, setRatio] = createSignal<number>(1);
<canvas
ref={(element) => {
const ctx = element.getContext("2d")!;
const img = new Image();
img.onload = () => {
setRatio(img.width / img.height);
ctx.drawImage(img, 0, 0, pixels * ratio(), pixels);
resolve(ctx.getImageData(0, 0, element.width, element.height));
};
img.src =
"data:image/svg+xml;charset=utf-8," +
encodeURIComponent((svg as Element).outerHTML);
}}
height={pixels}
width={pixels * ratio()}
/>;
return promise;
}
interface PixelateProps extends ComponentProps<"canvas"> {
pixels: number;
}
function Pixelate(props: PixelateProps) {
const [resource] = createResource(() =>
getPixelatedImageData(props.pixels, props.children),
);
return (
<Show when={resource()}>
{(data) => <WebGLImage data={data()} {...props} />}
</Show>
);
}
render(
() => (
<div style={{ display: "flex", "flex-direction": "column" }}>
<Pixelate pixels={30} style={{ height: "40px", width: "40px" }}>
<svg
fill="white"
stroke-width="0"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 1024"
height="1em"
width="1em"
>
<path d="M928 444H820V330.4c0-17.7-14.3-32-32-32H473L355.7 186.2a8.15 8.15 0 0 0-5.5-2.2H96c-17.7 0-32 14.3-32 32v592c0 17.7 14.3 32 32 32h698c13 0 24.8-7.9 29.7-20l134-332c1.5-3.8 2.3-7.9 2.3-12 0-17.7-14.3-32-32-32zm-180 0H238c-13 0-24.8 7.9-29.7 20L136 643.2V256h188.5l119.6 114.4H748V444z" />
</svg>
</Pixelate>
<Pixelate pixels={30} style={{ height: "40px", width: "40px" }}>
<svg
fill="white"
stroke-width="0"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
height="1em"
width="1em"
style={{ overflow: "visible", color: "white" }}
>
<path d="M20.385 2.879a3 3 0 0 0-4.243 0L14.02 5l-.707-.708a1 1 0 1 0-1.414 1.415l5.657 5.656A1 1 0 0 0 18.97 9.95l-.707-.707 2.122-2.122a3 3 0 0 0 0-4.242Z" />
<path
fill-rule="evenodd"
d="M11.93 7.091 4.152 14.87a3.001 3.001 0 0 0-.587 3.415L2 19.85l1.414 1.415 1.565-1.566a3.001 3.001 0 0 0 3.415-.586l7.778-7.778L11.93 7.09Zm1.414 4.243L11.93 9.92l-6.364 6.364a1 1 0 0 0 1.414 1.414l6.364-6.364Z"
clip-rule="evenodd"
/>
</svg>
</Pixelate>
<Pixelate pixels={30} style={{ height: "40px", width: "40px" }}>
<svg
fill="white"
stroke-width="0"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
height="1em"
width="1em"
style={{ overflow: "visible", color: "white" }}
>
<path d="m2.586 15.408 4.299 4.299a.996.996 0 0 0 .707.293h12.001v-2h-6.958l7.222-7.222c.78-.779.78-2.049 0-2.828L14.906 3a2.003 2.003 0 0 0-2.828 0l-4.75 4.749-4.754 4.843a2.007 2.007 0 0 0 .012 2.816zM13.492 4.414l4.95 4.95-2.586 2.586L10.906 7l2.586-2.586zM8.749 9.156l.743-.742 4.95 4.95-4.557 4.557a1.026 1.026 0 0 0-.069.079h-1.81l-4.005-4.007 4.748-4.837z" />
</svg>
</Pixelate>
</div>
),
document.getElementById("app")!,
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment