Last active
May 12, 2025 01:58
-
-
Save bigmistqke/f1a9b6c62a9e0a6e41eab5ade4e6e6f5 to your computer and use it in GitHub Desktop.
pixelate svg component
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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