<AsciiEffect characters=' .,⦁↬∞∂λ⍼☿⁜ℵ'cellSize={20}/>
Last active
January 15, 2025 03:15
-
-
Save lumpenspace/c57cd11648c0dd4e8dab1635ceea37d7 to your computer and use it in GitHub Desktop.
TTYGL - WEBGL shader to render anything as ascii art, and react component
This file contains 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 { CanvasTexture, Color, NearestFilter, RepeatWrapping, Texture, Uniform } from 'three'; | |
import { useMemo } from 'react'; | |
import { Effect } from 'postprocessing'; | |
const fragment = ` | |
uniform sampler2D uCharacters; | |
uniform float uCharactersCount; | |
uniform float uCellSize; | |
uniform bool uInvert; | |
uniform vec3 uColor; | |
uniform bool uUseColor; // New uniform to check if uColor is set | |
const vec2 SIZE = vec2(16.); | |
vec3 greyscale(vec3 color, float strength) { | |
float g = dot(color, vec3(0.299, 0.587, 0.114)); | |
return mix(color, vec3(g), strength); | |
} | |
vec3 greyscale(vec3 color) { | |
return greyscale(color, 1.0); | |
} | |
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { | |
vec2 cell = resolution / uCellSize; | |
vec2 grid = 1.0 / cell; | |
vec2 pixelizedUV = grid * (0.5 + floor(uv / grid)); | |
vec4 pixelized = texture2D(inputBuffer, pixelizedUV); | |
float greyscaled = greyscale(pixelized.rgb).r; | |
if (uInvert) { | |
greyscaled = 1.0 - greyscaled; | |
} | |
float characterIndex = floor((uCharactersCount - 1.0) * greyscaled); | |
vec2 characterPosition = vec2(mod(characterIndex, SIZE.x), floor(characterIndex / SIZE.y)); | |
vec2 offset = vec2(characterPosition.x, -characterPosition.y) / SIZE; | |
vec2 charUV = mod(uv * (cell / SIZE), 1.0 / SIZE) - vec2(0., 1.0 / SIZE) + offset; | |
vec4 asciiCharacter = texture2D(uCharacters, charUV); | |
if (uUseColor) { | |
asciiCharacter.rgb = uColor * asciiCharacter.r; | |
} else { | |
asciiCharacter.rgb = pixelized.rgb * asciiCharacter.r; | |
} | |
asciiCharacter.a = pixelized.a; | |
outputColor = asciiCharacter; | |
} | |
`; | |
export interface IAsciiGlProps { | |
characters?: string; | |
fontSize?: number; | |
cellSize?: number; | |
color?: string; | |
invert?: boolean; | |
} | |
export class AsciiGl extends Effect { | |
constructor({ | |
characters = ` .:,'-^=*+?!|0#X%WM@`, | |
fontSize = 54, | |
cellSize = 16, | |
color = '#ffffff', | |
invert = false | |
}: IAsciiGlProps = {}) { | |
const useColor = color !== '#ffffff'; // Determine if color is set | |
const uniforms = new Map<string, Uniform>([ | |
['uCharacters', new Uniform(new Texture())], | |
['uCellSize', new Uniform(cellSize)], | |
['uCharactersCount', new Uniform(characters.length)], | |
['uColor', new Uniform(new Color(color))], | |
['uInvert', new Uniform(invert)], | |
['uUseColor', new Uniform(useColor)] // Add new uniform | |
]); | |
super('AsciiGl', fragment, { uniforms }); | |
const charactersTextureUniform = this.uniforms.get('uCharacters'); | |
if (charactersTextureUniform) { | |
charactersTextureUniform.value = this.createCharactersTexture(characters, fontSize); | |
} | |
} | |
public createCharactersTexture(characters: string, fontSize: number): Texture { | |
const canvas = document.createElement('canvas'); | |
const SIZE = 1024; | |
const MAX_PER_ROW = 16; | |
const CELL = SIZE / MAX_PER_ROW; | |
canvas.width = canvas.height = SIZE; | |
const texture = new CanvasTexture( | |
canvas, | |
undefined, | |
RepeatWrapping, | |
RepeatWrapping, | |
NearestFilter, | |
NearestFilter | |
); | |
const context = canvas.getContext('2d'); | |
if (!context) { | |
throw new Error('Context not available'); | |
} | |
context.clearRect(0, 0, SIZE, SIZE); | |
context.font = `${fontSize}px arial`; | |
context.textAlign = 'center'; | |
context.textBaseline = 'middle'; | |
context.fillStyle = '#fff'; | |
for (let i = 0; i < characters.length; i++) { | |
const char = characters[i]; | |
const x = i % MAX_PER_ROW; | |
const y = Math.floor(i / MAX_PER_ROW); | |
context.fillText(char, x * CELL + CELL / 2, y * CELL + CELL / 2); | |
} | |
texture.needsUpdate = true; | |
return texture; | |
} | |
} | |
export const AsciiEffect = (props: IAsciiGlProps) => { | |
const asciiEffect = useMemo(() => new AsciiGl(props), [props]); | |
return (<primitive object={asciiEffect} />); | |
} |
This file contains 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
// full example: apply the effect on a whole canvas + a video. | |
import { useEffect, useState, useRef, useMemo } from "react"; | |
import { SpotLight,useVideoTexture, PerspectiveCamera, useTexture, shaderMaterial } from '@react-three/drei'; | |
import { Canvas, extend } from '@react-three/fiber'; | |
import { EffectComposer } from '@react-three/postprocessing'; | |
import { Vector2, VideoTexture, Mesh, BackSide, TorusGeometry } from 'three'; | |
import { AsciiEffect } from './AsciiGl'; | |
const VideoBackground = () => { | |
const { dynamicStyle } = useDynamicStyle(); | |
const texture = useVideoTexture(dynamicStyle.bgvid); | |
const meshRef = useRef<Mesh>(null); | |
useEffect(() => { | |
if (meshRef.current && meshRef.current.material instanceof MeshBasicMaterial) { | |
meshRef.current.material.color = new Color(dynamicStyle.color); | |
meshRef.current.material.needsUpdate = true; | |
} | |
}, [dynamicStyle.color]); | |
return ( | |
<mesh ref={meshRef} name="backgroundMesh"> | |
<torusGeometry args={[100, 300, 40]} /> | |
<meshBasicMaterial map={texture} side={BackSide} /> | |
</mesh> | |
); | |
}; | |
const Container = () => | |
<> | |
<PerspectiveCamera makeDefault ref={cameraRef} position={[0, 0, 10]} /> | |
<EffectComposer> | |
<AsciiEffect characters=' .,⦁↬∞λ⍼☿⁜ℵ' cellSize={20}/> | |
</EffectComposer> | |
</> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment