|
| 1 | +// From: https://github.com/emilwidlund/ASCII |
| 2 | +// https://twitter.com/emilwidlund/status/1652386482420609024 |
| 3 | + |
| 4 | +import { forwardRef, useMemo } from 'react' |
| 5 | +import { CanvasTexture, Color, NearestFilter, RepeatWrapping, Texture, Uniform } from 'three' |
| 6 | +import { Effect } from 'postprocessing' |
| 7 | + |
| 8 | +const fragment = ` |
| 9 | +uniform sampler2D uCharacters; |
| 10 | +uniform float uCharactersCount; |
| 11 | +uniform float uCellSize; |
| 12 | +uniform bool uInvert; |
| 13 | +uniform vec3 uColor; |
| 14 | +
|
| 15 | +const vec2 SIZE = vec2(16.); |
| 16 | +
|
| 17 | +vec3 greyscale(vec3 color, float strength) { |
| 18 | + float g = dot(color, vec3(0.299, 0.587, 0.114)); |
| 19 | + return mix(color, vec3(g), strength); |
| 20 | +} |
| 21 | +
|
| 22 | +vec3 greyscale(vec3 color) { |
| 23 | + return greyscale(color, 1.0); |
| 24 | +} |
| 25 | +
|
| 26 | +void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { |
| 27 | + vec2 cell = resolution / uCellSize; |
| 28 | + vec2 grid = 1.0 / cell; |
| 29 | + vec2 pixelizedUV = grid * (0.5 + floor(uv / grid)); |
| 30 | + vec4 pixelized = texture2D(inputBuffer, pixelizedUV); |
| 31 | + float greyscaled = greyscale(pixelized.rgb).r; |
| 32 | +
|
| 33 | + if (uInvert) { |
| 34 | + greyscaled = 1.0 - greyscaled; |
| 35 | + } |
| 36 | +
|
| 37 | + float characterIndex = floor((uCharactersCount - 1.0) * greyscaled); |
| 38 | + vec2 characterPosition = vec2(mod(characterIndex, SIZE.x), floor(characterIndex / SIZE.y)); |
| 39 | + vec2 offset = vec2(characterPosition.x, -characterPosition.y) / SIZE; |
| 40 | + vec2 charUV = mod(uv * (cell / SIZE), 1.0 / SIZE) - vec2(0., 1.0 / SIZE) + offset; |
| 41 | + vec4 asciiCharacter = texture2D(uCharacters, charUV); |
| 42 | +
|
| 43 | + asciiCharacter.rgb = uColor * asciiCharacter.r; |
| 44 | + asciiCharacter.a = pixelized.a; |
| 45 | + outputColor = asciiCharacter; |
| 46 | +} |
| 47 | +` |
| 48 | + |
| 49 | +interface IASCIIEffectProps { |
| 50 | + characters?: string |
| 51 | + fontSize?: number |
| 52 | + cellSize?: number |
| 53 | + color?: string |
| 54 | + invert?: boolean |
| 55 | +} |
| 56 | + |
| 57 | +class ASCIIEffect extends Effect { |
| 58 | + constructor({ |
| 59 | + characters = ` .:,'-^=*+?!|0#X%WM@`, |
| 60 | + fontSize = 54, |
| 61 | + cellSize = 16, |
| 62 | + color = '#ffffff', |
| 63 | + invert = false, |
| 64 | + }: IASCIIEffectProps = {}) { |
| 65 | + const uniforms = new Map<string, Uniform>([ |
| 66 | + ['uCharacters', new Uniform(new Texture())], |
| 67 | + ['uCellSize', new Uniform(cellSize)], |
| 68 | + ['uCharactersCount', new Uniform(characters.length)], |
| 69 | + ['uColor', new Uniform(new Color(color))], |
| 70 | + ['uInvert', new Uniform(invert)], |
| 71 | + ]) |
| 72 | + |
| 73 | + super('ASCIIEffect', fragment, { uniforms }) |
| 74 | + |
| 75 | + const charactersTextureUniform = this.uniforms.get('uCharacters') |
| 76 | + |
| 77 | + if (charactersTextureUniform) { |
| 78 | + charactersTextureUniform.value = this.createCharactersTexture(characters, fontSize) |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + /** Draws the characters on a Canvas and returns a texture */ |
| 83 | + public createCharactersTexture(characters: string, fontSize: number): THREE.Texture { |
| 84 | + const canvas = document.createElement('canvas') |
| 85 | + const SIZE = 1024 |
| 86 | + const MAX_PER_ROW = 16 |
| 87 | + const CELL = SIZE / MAX_PER_ROW |
| 88 | + |
| 89 | + canvas.width = canvas.height = SIZE |
| 90 | + const texture = new CanvasTexture(canvas, undefined, RepeatWrapping, RepeatWrapping, NearestFilter, NearestFilter) |
| 91 | + const context = canvas.getContext('2d') |
| 92 | + |
| 93 | + if (!context) { |
| 94 | + throw new Error('Context not available') |
| 95 | + } |
| 96 | + |
| 97 | + context.clearRect(0, 0, SIZE, SIZE) |
| 98 | + context.font = `${fontSize}px arial` |
| 99 | + context.textAlign = 'center' |
| 100 | + context.textBaseline = 'middle' |
| 101 | + context.fillStyle = '#fff' |
| 102 | + |
| 103 | + for (let i = 0; i < characters.length; i++) { |
| 104 | + const char = characters[i] |
| 105 | + const x = i % MAX_PER_ROW |
| 106 | + const y = Math.floor(i / MAX_PER_ROW) |
| 107 | + context.fillText(char, x * CELL + CELL / 2, y * CELL + CELL / 2) |
| 108 | + } |
| 109 | + |
| 110 | + texture.needsUpdate = true |
| 111 | + return texture |
| 112 | + } |
| 113 | +} |
| 114 | + |
| 115 | +export const ASCII = forwardRef<ASCIIEffect, IASCIIEffectProps>( |
| 116 | + ({ characters = ` .:,'-^=*+?!|0#X%WM@`, fontSize = 54, cellSize = 16, color = '#ffffff', invert = false }, fref) => { |
| 117 | + const effect = useMemo( |
| 118 | + () => new ASCIIEffect({ characters, fontSize, cellSize, color, invert }), |
| 119 | + [characters, fontSize, cellSize, color, invert] |
| 120 | + ) |
| 121 | + return <primitive ref={fref} object={effect} /> |
| 122 | + } |
| 123 | +) |
0 commit comments