Skip to content

Instantly share code, notes, and snippets.

@lumpenspace
Last active January 15, 2025 03:15
Show Gist options
  • Save lumpenspace/c57cd11648c0dd4e8dab1635ceea37d7 to your computer and use it in GitHub Desktop.
Save lumpenspace/c57cd11648c0dd4e8dab1635ceea37d7 to your computer and use it in GitHub Desktop.
TTYGL - WEBGL shader to render anything as ascii art, and react component

ASCIIGL

usage

<AsciiEffect characters=' .,⦁↬∞∂λ⍼☿⁜ℵ'cellSize={20}/>

example

My.Movie.15.-.SD.480p.mov
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} />);
}
// 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