├── .gitattributes ├── public └── favicon.ico ├── src ├── shaders │ ├── vertex.glsl │ ├── ascii-vertex.glsl │ ├── fragment.glsl │ └── ascii-fragment.glsl ├── js │ └── main.js └── styles │ └── styles.css ├── vite.config.js ├── package.json ├── .gitignore ├── README.md ├── LICENSE └── index.html /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrico1234/codrops-ascii-ogl/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/shaders/vertex.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | in vec2 uv; 3 | in vec2 position; 4 | out vec2 vUv; 5 | void main() { 6 | vUv = uv; 7 | gl_Position = vec4(position, 0., 1.); 8 | } -------------------------------------------------------------------------------- /src/shaders/ascii-vertex.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | in vec2 uv; 3 | in vec2 position; 4 | out vec2 vUv; 5 | void main() { 6 | vUv = uv; 7 | gl_Position = vec4(position, 0., 1.); 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig({ 4 | root: '.', 5 | base: './', 6 | build: { 7 | outDir: 'dist', 8 | sourcemap: true 9 | }, 10 | server: { 11 | open: true, 12 | port: 3000 13 | }, 14 | resolve: { 15 | alias: { 16 | '@': '/src' 17 | } 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codrops-ascii-ogl", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.html", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "serve": "vite preview" 10 | }, 11 | "dependencies": { 12 | "ogl": "^0.0.29", 13 | "resolve-lygia": "^1.0.2", 14 | "tweakpane": "^2.1.0", 15 | "vite": "^4.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Node.js dependencies 2 | node_modules/ 3 | 4 | # Ignore build output 5 | dist/ 6 | 7 | # Ignore local environment settings 8 | .env 9 | .env.*.local 10 | 11 | # Ignore Vite cache 12 | .vite/ 13 | 14 | # Ignore log files 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Ignore MacOS system files 20 | .DS_Store 21 | 22 | # Ignore IDE/editor settings 23 | .idea/ 24 | .vscode/ 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | 30 | # Ignore any temporary files or scratch files 31 | *.tmp 32 | *.temp 33 | -------------------------------------------------------------------------------- /src/shaders/fragment.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision mediump float; 3 | uniform float uFrequency; 4 | uniform float uTime; 5 | uniform float uSpeed; 6 | uniform float uValue; 7 | in vec2 vUv; 8 | out vec4 fragColor; 9 | 10 | #include "lygia/generative/cnoise.glsl" 11 | 12 | vec3 hsv2rgb(vec3 c) { 13 | vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); 14 | vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); 15 | return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); 16 | } 17 | 18 | void main() { 19 | float hue = abs(cnoise(vec3(vUv * uFrequency, uTime * uSpeed))); 20 | vec3 rainbowColor = hsv2rgb(vec3(hue, 1.0, uValue)); 21 | fragColor = vec4(rainbowColor, 1.0); 22 | } 23 | -------------------------------------------------------------------------------- /src/shaders/ascii-fragment.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision highp float; 3 | uniform vec2 uResolution; 4 | uniform sampler2D uTexture; 5 | out vec4 fragColor; 6 | 7 | float character(int n, vec2 p) { 8 | p = floor(p * vec2(-4.0, 4.0) + 2.5); 9 | if(clamp(p.x, 0.0, 4.0) == p.x && clamp(p.y, 0.0, 4.0) == p.y) { 10 | int a = int(round(p.x) + 5.0 * round(p.y)); 11 | if(((n >> a) & 1) == 1) return 1.0; 12 | } 13 | return 0.0; 14 | } 15 | 16 | void main() { 17 | vec2 pix = gl_FragCoord.xy; 18 | vec3 col = texture(uTexture, floor(pix / 16.0) * 16.0 / uResolution.xy).rgb; 19 | float gray = 0.3 * col.r + 0.59 * col.g + 0.11 * col.b; 20 | int n = 4096; 21 | if(gray > 0.2) n = 65600; 22 | if(gray > 0.3) n = 163153; 23 | if(gray > 0.4) n = 15255086; 24 | if(gray > 0.5) n = 13121101; 25 | if(gray > 0.6) n = 15252014; 26 | if(gray > 0.7) n = 13195790; 27 | if(gray > 0.8) n = 11512810; 28 | vec2 p = mod(pix / 8.0, 2.0) - vec2(1.0); 29 | col = col * character(n, p); 30 | fragColor = vec4(col, 1.0); 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASCII Shader with OGL 2 | 3 | Demo for the tutorial on how to create an ASCII art animation using OGL. 4 | 5 | ![ASCII Shader](https://tympanus.net/codrops/wp-content/uploads/2024/11/asciishader-1.jpg) 6 | 7 | [Article on Codrops](https://tympanus.net/codrops/?p=82209) 8 | 9 | [Demo](https://tympanus.net/Tutorials/ascii-shader-ogl/) 10 | 11 | ## Installation 12 | 13 | ```sh 14 | npm install 15 | ``` 16 | 17 | ## Build and Run 18 | ```sh 19 | npm run dev 20 | ``` 21 | or 22 | ```sh 23 | npm run build 24 | ``` 25 | 26 | ## Misc 27 | 28 | Follow Andrico: [Twitter](https://x.com/AndricoKaroulla), [LinkedIn](https://www.linkedin.com/in/andrico-karoulla/), [GitHub](https://github.com/andrico1234) 29 | 30 | Follow Codrops: [Bluesky](https://bsky.app/profile/codrops.bsky.social), [Facebook](http://www.facebook.com/codrops), [GitHub](https://github.com/codrops), [Instagram](https://www.instagram.com/codropsss/), [X](http://www.x.com/codrops) 31 | 32 | ## License 33 | [MIT](LICENSE) 34 | 35 | Made with :blue_heart: by [Andrico](https://x.com/AndricoKaroulla) and [Codrops](http://www.codrops.com) 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2009 - 2024 [Codrops](https://tympanus.net/codrops) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ASCII Shader with OGL | Codrops 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

ASCII Shader with OGL

18 | Tutorial 19 | All demos 20 | GitHub 21 | 28 |
29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | import { Camera, Mesh, Plane, Program, Renderer, RenderTarget } from 'ogl'; 2 | import { resolveLygia } from 'resolve-lygia'; 3 | import { Pane } from 'tweakpane'; 4 | 5 | import vertex from '../shaders/vertex.glsl?raw'; 6 | import fragment from '../shaders/fragment.glsl?raw'; 7 | import asciiVertex from '../shaders/ascii-vertex.glsl?raw'; 8 | import asciiFragment from '../shaders/ascii-fragment.glsl?raw'; 9 | 10 | const renderer = new Renderer(); 11 | const gl = renderer.gl; 12 | document.body.appendChild(gl.canvas); 13 | 14 | const camera = new Camera(gl, { near: 0.1, far: 100 }); 15 | camera.position.set(0, 0, 3); 16 | 17 | const resize = () => { 18 | renderer.setSize(window.innerWidth, window.innerHeight); 19 | camera.perspective({ aspect: gl.canvas.width / gl.canvas.height }); 20 | }; 21 | window.addEventListener('resize', resize); 22 | resize(); 23 | 24 | // Setup Perlin noise shader 25 | const perlinProgram = new Program(gl, { 26 | vertex, 27 | fragment: resolveLygia(fragment), 28 | uniforms: { 29 | uTime: { value: 0 }, 30 | uFrequency: { value: 5.0 }, 31 | uBrightness: { value: 0.5 }, 32 | uSpeed: { value: 0.75 }, 33 | uValue: { value: 0.4 } // Start with a lower lightness value 34 | } 35 | }); 36 | 37 | const perlinMesh = new Mesh(gl, { 38 | geometry: new Plane(gl, { width: 2, height: 2 }), 39 | program: perlinProgram, 40 | }); 41 | 42 | const renderTarget = new RenderTarget(gl); 43 | 44 | // Setup ASCII shader 45 | const asciiProgram = new Program(gl, { 46 | vertex: asciiVertex, 47 | fragment: asciiFragment, 48 | uniforms: { 49 | uResolution: { value: [gl.canvas.width, gl.canvas.height] }, 50 | uTexture: { value: renderTarget.texture }, 51 | } 52 | }); 53 | 54 | const asciiMesh = new Mesh(gl, { 55 | geometry: new Plane(gl, { width: 2, height: 2 }), 56 | program: asciiProgram, 57 | }); 58 | 59 | // Setup tweakpane controls 60 | const pane = new Pane(); 61 | pane.addBinding(perlinProgram.uniforms.uFrequency, 'value', { min: 0, max: 10, label: 'Frequency' }); 62 | pane.addBinding(perlinProgram.uniforms.uSpeed, 'value', { min: 0, max: 2, label: 'Speed' }); 63 | pane.addBinding(perlinProgram.uniforms.uValue, 'value', { min: 0, max: 1, label: 'Lightness' }); 64 | 65 | // Set up frame rate limiting 66 | let lastTime = 0; 67 | const frameRate = 30; // Target frame rate in frames per second 68 | const frameInterval = 1000 / frameRate; 69 | 70 | function update(time) { 71 | requestAnimationFrame(update); 72 | 73 | // Only update if enough time has passed since the last frame 74 | if (time - lastTime < frameInterval) return; 75 | lastTime = time; 76 | 77 | // Rendering code 78 | const elapsedTime = time * 0.001; 79 | perlinProgram.uniforms.uTime.value = elapsedTime; 80 | 81 | // Render Perlin noise to render target 82 | renderer.render({ scene: perlinMesh, camera, target: renderTarget }); 83 | 84 | // Render ASCII shader to screen 85 | asciiProgram.uniforms.uResolution.value = [gl.canvas.width, gl.canvas.height]; 86 | renderer.render({ scene: asciiMesh, camera }); 87 | } 88 | 89 | // Start the render loop 90 | requestAnimationFrame(update); 91 | -------------------------------------------------------------------------------- /src/styles/styles.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::after, 3 | *::before { 4 | box-sizing: border-box; 5 | } 6 | 7 | :root { 8 | font-size: 12px; 9 | --color-text: #fff; 10 | --color-bg: #000; 11 | --color-link: #fff; 12 | --color-link-hover: #fff; 13 | --page-padding: 1.5rem; 14 | } 15 | 16 | body { 17 | margin: 0; 18 | color: var(--color-text); 19 | background-color: var(--color-bg); 20 | font-family: ui-monospace, monospace; 21 | -webkit-font-smoothing: antialiased; 22 | -moz-osx-font-smoothing: grayscale; 23 | } 24 | 25 | /* Page Loader */ 26 | .js .loading::before, 27 | .js .loading::after { 28 | content: ''; 29 | position: fixed; 30 | z-index: 10000; 31 | } 32 | 33 | .js .loading::before { 34 | top: 0; 35 | left: 0; 36 | width: 100%; 37 | height: 100%; 38 | background: var(--color-bg); 39 | } 40 | 41 | .js .loading::after { 42 | top: 50%; 43 | left: 50%; 44 | width: 100px; 45 | height: 1px; 46 | margin: 0 0 0 -50px; 47 | background: var(--color-link); 48 | animation: loaderAnim 1.5s ease-in-out infinite alternate forwards; 49 | 50 | } 51 | 52 | @keyframes loaderAnim { 53 | 0% { 54 | transform: scaleX(0); 55 | transform-origin: 0% 50%; 56 | } 57 | 50% { 58 | transform: scaleX(1); 59 | transform-origin: 0% 50%; 60 | } 61 | 50.1% { 62 | transform: scaleX(1); 63 | transform-origin: 100% 50%; 64 | } 65 | 100% { 66 | transform: scaleX(0); 67 | transform-origin: 100% 50%; 68 | } 69 | } 70 | 71 | .js canvas { 72 | position: fixed; 73 | top: 0; 74 | left: 0; 75 | } 76 | 77 | a { 78 | text-decoration: none; 79 | color: var(--color-link); 80 | outline: none; 81 | cursor: pointer; 82 | } 83 | 84 | a:hover { 85 | text-decoration: underline; 86 | color: var(--color-link-hover); 87 | outline: none; 88 | } 89 | 90 | /* Better focus styles from https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible */ 91 | a:focus { 92 | /* Provide a fallback style for browsers 93 | that don't support :focus-visible */ 94 | outline: none; 95 | background: lightgrey; 96 | } 97 | 98 | a:focus:not(:focus-visible) { 99 | /* Remove the focus indicator on mouse-focus for browsers 100 | that do support :focus-visible */ 101 | background: transparent; 102 | } 103 | 104 | a:focus-visible { 105 | /* Draw a very noticeable focus style for 106 | keyboard-focus on browsers that do support 107 | :focus-visible */ 108 | outline: 2px solid red; 109 | background: transparent; 110 | } 111 | 112 | .frame { 113 | padding: 3rem var(--page-padding) 0; 114 | position: fixed; 115 | top: 0; 116 | left: 0; 117 | width: 100%; 118 | height: auto; 119 | display: grid; 120 | z-index: 1000; 121 | grid-row-gap: 1rem; 122 | grid-column-gap: 2rem; 123 | pointer-events: none; 124 | justify-items: start; 125 | grid-template-columns: auto auto; 126 | grid-template-areas: 'title' 'back' 'archive' 'github' 'sponsor' 'tags'; 127 | } 128 | 129 | .frame #cdawrap { 130 | justify-self: start; 131 | } 132 | 133 | .frame a { 134 | pointer-events: auto; 135 | } 136 | 137 | .frame__title { 138 | grid-area: title; 139 | font-size: inherit; 140 | margin: 0; 141 | } 142 | 143 | .frame__back { 144 | grid-area: back; 145 | justify-self: start; 146 | } 147 | 148 | .frame__archive { 149 | grid-area: archive; 150 | justify-self: start; 151 | } 152 | 153 | .frame__github { 154 | grid-area: github; 155 | } 156 | 157 | .frame__tags { 158 | grid-area: tags; 159 | display: flex; 160 | flex-wrap: wrap; 161 | gap: 1rem; 162 | } 163 | 164 | canvas { 165 | display: block; 166 | width: 100vw; 167 | height: 100vh; 168 | /* Optional border or shadow for aesthetic */ 169 | border: 1px solid #444; 170 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); 171 | } 172 | 173 | @media screen and (min-width: 53em) { 174 | body { 175 | --page-padding: 2rem 3rem; 176 | } 177 | .frame { 178 | padding: var(--page-padding); 179 | height: 100%; 180 | grid-template-columns: auto auto auto auto 1fr; 181 | grid-template-rows: auto auto; 182 | align-content: space-between; 183 | grid-template-areas: 'title back github archive ...' 'tags tags tags sponsor sponsor'; 184 | } 185 | .frame__tags { 186 | align-self: end; 187 | } 188 | .frame #cdawrap { 189 | justify-self: end; 190 | text-align: right; 191 | max-width: 300px; 192 | } 193 | } 194 | --------------------------------------------------------------------------------