├── .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 |
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 |
--------------------------------------------------------------------------------