├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── package.json ├── src └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | 4 | .DS_STORE 5 | *-lock.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "singleQuote": true, 4 | "printWidth": 120, 5 | "tabWidth": 4, 6 | "trailingComma": "none" 7 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true 4 | }, 5 | "editor.formatOnSave": true, 6 | "eslint.format.enable": true, 7 | "editor.defaultFormatter": "esbenp.prettier-vscode", 8 | "[javascript]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASCII 2 | 3 | An ASCII effect for THREE.js - which runs as a fragment shader on the GPU. Inspired by https://github.com/darkroomengineering/aniso. 4 | 5 | ### Supported Props 6 | 7 | ```typescript 8 | interface IASCIIEffectProps { 9 | characters?: string; // The ASCII characters to use in brightness order dark -> light 10 | fontSize?: number; // Font Size of the characters drawn to the texture 11 | cellSize?: number; // Size of each cell in the grid 12 | color?: string; // Color of the characters 13 | invert?: boolean; // Flag which inverts the effect 14 | } 15 | ``` 16 | 17 | ### Example with @react-three/fiber 18 | 19 | ```jsx 20 | import React from 'react'; 21 | import { Canvas } from '@react-three/fiber'; 22 | import { EffectComposer } from '@react-three/postprocessing'; 23 | 24 | const Scene = () => { 25 | const asciiEffect = React.useMemo(() => new ASCIIEffect(), []); 26 | 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | }; 40 | ``` 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ascii", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "build/index.js", 6 | "scripts": { 7 | "build": "tsc" 8 | }, 9 | "keywords": [], 10 | "author": "Emil Widlund", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@types/three": "^0.151.0", 14 | "postprocessing": "6.30.2", 15 | "three": "^0.151.3", 16 | "typescript": "5.0.4" 17 | }, 18 | "type": "module" 19 | } 20 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { CanvasTexture, Color, NearestFilter, RepeatWrapping, Texture, Uniform } from 'three'; 2 | import { Effect } from 'postprocessing'; 3 | 4 | const fragment = ` 5 | uniform sampler2D uCharacters; 6 | uniform float uCharactersCount; 7 | uniform float uCellSize; 8 | uniform bool uInvert; 9 | uniform vec3 uColor; 10 | 11 | const vec2 SIZE = vec2(16.); 12 | 13 | vec3 greyscale(vec3 color, float strength) { 14 | float g = dot(color, vec3(0.299, 0.587, 0.114)); 15 | return mix(color, vec3(g), strength); 16 | } 17 | 18 | vec3 greyscale(vec3 color) { 19 | return greyscale(color, 1.0); 20 | } 21 | 22 | void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { 23 | vec2 cell = resolution / uCellSize; 24 | vec2 grid = 1.0 / cell; 25 | vec2 pixelizedUV = grid * (0.5 + floor(uv / grid)); 26 | vec4 pixelized = texture2D(inputBuffer, pixelizedUV); 27 | float greyscaled = greyscale(pixelized.rgb).r; 28 | 29 | if (uInvert) { 30 | greyscaled = 1.0 - greyscaled; 31 | } 32 | 33 | float characterIndex = floor((uCharactersCount - 1.0) * greyscaled); 34 | vec2 characterPosition = vec2(mod(characterIndex, SIZE.x), floor(characterIndex / SIZE.y)); 35 | vec2 offset = vec2(characterPosition.x, -characterPosition.y) / SIZE; 36 | vec2 charUV = mod(uv * (cell / SIZE), 1.0 / SIZE) - vec2(0., 1.0 / SIZE) + offset; 37 | vec4 asciiCharacter = texture2D(uCharacters, charUV); 38 | 39 | asciiCharacter.rgb = uColor * asciiCharacter.r; 40 | asciiCharacter.a = pixelized.a; 41 | outputColor = asciiCharacter; 42 | } 43 | `; 44 | 45 | export interface IASCIIEffectProps { 46 | characters?: string; 47 | fontSize?: number; 48 | cellSize?: number; 49 | color?: string; 50 | invert?: boolean; 51 | } 52 | 53 | export class ASCIIEffect extends Effect { 54 | constructor({ 55 | characters = ` .:,'-^=*+?!|0#X%WM@`, 56 | fontSize = 54, 57 | cellSize = 16, 58 | color = '#ffffff', 59 | invert = false 60 | }: IASCIIEffectProps = {}) { 61 | const uniforms = new Map([ 62 | ['uCharacters', new Uniform(new Texture())], 63 | ['uCellSize', new Uniform(cellSize)], 64 | ['uCharactersCount', new Uniform(characters.length)], 65 | ['uColor', new Uniform(new Color(color))], 66 | ['uInvert', new Uniform(invert)] 67 | ]); 68 | 69 | super('ASCIIEffect', fragment, { uniforms }); 70 | 71 | const charactersTextureUniform = this.uniforms.get('uCharacters'); 72 | 73 | if (charactersTextureUniform) { 74 | charactersTextureUniform.value = this.createCharactersTexture(characters, fontSize); 75 | } 76 | } 77 | 78 | /** Draws the characters on a Canvas and returns a texture */ 79 | public createCharactersTexture(characters: string, fontSize: number): THREE.Texture { 80 | const canvas = document.createElement('canvas'); 81 | 82 | const SIZE = 1024; 83 | const MAX_PER_ROW = 16; 84 | const CELL = SIZE / MAX_PER_ROW; 85 | 86 | canvas.width = canvas.height = SIZE; 87 | 88 | const texture = new CanvasTexture( 89 | canvas, 90 | undefined, 91 | RepeatWrapping, 92 | RepeatWrapping, 93 | NearestFilter, 94 | NearestFilter 95 | ); 96 | 97 | const context = canvas.getContext('2d'); 98 | 99 | if (!context) { 100 | throw new Error('Context not available'); 101 | } 102 | 103 | context.clearRect(0, 0, SIZE, SIZE); 104 | context.font = `${fontSize}px arial`; 105 | context.textAlign = 'center'; 106 | context.textBaseline = 'middle'; 107 | context.fillStyle = '#fff'; 108 | 109 | for (let i = 0; i < characters.length; i++) { 110 | const char = characters[i]; 111 | const x = i % MAX_PER_ROW; 112 | const y = Math.floor(i / MAX_PER_ROW); 113 | 114 | context.fillText(char, x * CELL + CELL / 2, y * CELL + CELL / 2); 115 | } 116 | 117 | texture.needsUpdate = true; 118 | 119 | return texture; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext", "DOM"], 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "strict": true, 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "downlevelIteration": true, 11 | "outDir": "build", 12 | "declaration": true, 13 | "jsx": "react" 14 | }, 15 | "include": ["./src"] 16 | } 17 | --------------------------------------------------------------------------------