|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="en"> |
| 3 | +<head> |
| 4 | + <meta charset="UTF-8"> |
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 6 | + <title>termbit</title> |
| 7 | + <style> |
| 8 | + * { margin: 0; padding: 0; } |
| 9 | + body { background: #111; overflow: hidden; } |
| 10 | + </style> |
| 11 | + <script src="../libs/twgl.min.js"></script> |
| 12 | +</head> |
| 13 | +<body> |
| 14 | +<script> |
| 15 | +// https://x.com/corvus_ikshana/status/1827636589817692391 |
| 16 | +// https://codesandbox.io/p/sandbox/gallant-flower-yrv666 |
| 17 | + |
| 18 | +const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$&()*+-:;<=>?@[]^{}~'; |
| 19 | +const fontSize = 11; |
| 20 | +const fontPadding = 2; |
| 21 | +// window.devicePixelRatio = 1.0; |
| 22 | + |
| 23 | +const canvas = document.createElement('canvas'); |
| 24 | +document.body.appendChild(canvas); |
| 25 | +const gl = canvas.getContext('webgl2'); |
| 26 | + |
| 27 | +// Create char atlas |
| 28 | +const textCanvas = document.createElement('canvas'); |
| 29 | +textCanvas.width = chars.length * fontSize * window.devicePixelRatio; |
| 30 | +textCanvas.height = fontSize * window.devicePixelRatio; |
| 31 | +const textCtx = textCanvas.getContext('2d'); |
| 32 | +textCanvas.style = 'image-rendering: crisp-edges; background: #000;'; |
| 33 | +// document.body.appendChild(textCanvas) |
| 34 | +textCtx.setTransform( |
| 35 | + window.devicePixelRatio, 0, |
| 36 | + 0, window.devicePixelRatio, |
| 37 | + 0, 0 |
| 38 | +); |
| 39 | +textCtx.font = `${fontSize}px monospace`; |
| 40 | +textCtx.fillStyle = '#fff'; |
| 41 | +textCtx.textBaseline = 'top'; |
| 42 | +chars.split('').forEach((char, i) => { |
| 43 | + textCtx.fillText(char, fontPadding + i * fontSize, 1); |
| 44 | +}); |
| 45 | +const texture = twgl.createTexture(gl, { |
| 46 | + src: textCanvas, |
| 47 | + min: gl.LINEAR, |
| 48 | + mag: gl.LINEAR, |
| 49 | + wrap: gl.CLAMP_TO_EDGE, |
| 50 | +}); |
| 51 | + |
| 52 | +const vs = `#version 300 es |
| 53 | +in vec4 position; |
| 54 | +in vec2 texcoord; |
| 55 | +
|
| 56 | +uniform float time; |
| 57 | +uniform vec2 resolution; |
| 58 | +uniform int cols; |
| 59 | +
|
| 60 | +out vec2 v_texcoord; |
| 61 | +
|
| 62 | +void main() { |
| 63 | + vec2 center = resolution * 0.5; |
| 64 | +
|
| 65 | + int yi = gl_InstanceID / cols; |
| 66 | + float y = float(yi); |
| 67 | + float x = float(gl_InstanceID - yi*cols); |
| 68 | + float charIndex = mod( |
| 69 | + float(gl_InstanceID) + floor(42.0 * floor(time/5000.0 + sin(float(gl_InstanceID)))), |
| 70 | + ${chars.length}. |
| 71 | + ); |
| 72 | +
|
| 73 | + vec2 instancePos = vec2(x * ${fontSize}., y * ${fontSize}.); |
| 74 | + vec2 relPos = instancePos - center; |
| 75 | +
|
| 76 | + float offset = sin(length(relPos) * 0.03 + time/100000.0) * 3.14159; |
| 77 | + mat2 rotation = mat2( |
| 78 | + cos(offset), -sin(offset), |
| 79 | + sin(offset), cos(offset) |
| 80 | + ); |
| 81 | +
|
| 82 | + vec2 rotatedInstancePos = rotation * relPos + center; |
| 83 | +
|
| 84 | + vec2 vertPos = (position.xy + vec2(1.0)) * vec2(${fontSize}. * 0.5); |
| 85 | + vec2 rotatedVertPos = rotation * vertPos; |
| 86 | +
|
| 87 | + vec2 finalPos = rotatedInstancePos + rotatedVertPos; |
| 88 | +
|
| 89 | + vec2 clipSpace = (finalPos / resolution) * 2.0 - 1.0; |
| 90 | + gl_Position = vec4(clipSpace, 0, 1); |
| 91 | + v_texcoord = vec2((charIndex + texcoord.x) / ${chars.length}., texcoord.y); |
| 92 | +} |
| 93 | +`; |
| 94 | + |
| 95 | +const fs = `#version 300 es |
| 96 | +precision highp float; |
| 97 | +
|
| 98 | +in vec2 v_texcoord; |
| 99 | +out vec4 fragColor; |
| 100 | +
|
| 101 | +uniform sampler2D u_texture; |
| 102 | +
|
| 103 | +void main() { |
| 104 | + vec4 color = texture(u_texture, v_texcoord); |
| 105 | + fragColor = color; |
| 106 | +} |
| 107 | +`; |
| 108 | + |
| 109 | +const programInfo = twgl.createProgramInfo(gl, [vs, fs]); |
| 110 | + |
| 111 | +const arrays = { |
| 112 | + position: { |
| 113 | + numComponents: 2, |
| 114 | + data: new Float32Array([ |
| 115 | + -1, -1, |
| 116 | + 1, -1, |
| 117 | + 1, 1, |
| 118 | + -1, -1, |
| 119 | + 1, 1, |
| 120 | + -1, 1, |
| 121 | + ]) |
| 122 | + }, |
| 123 | + texcoord: { |
| 124 | + numComponents: 2, |
| 125 | + data: new Float32Array([ |
| 126 | + 0, 1, |
| 127 | + 1, 1, |
| 128 | + 1, 0, |
| 129 | + 0, 1, |
| 130 | + 1, 0, |
| 131 | + 0, 0, |
| 132 | + ]) |
| 133 | + } |
| 134 | +}; |
| 135 | + |
| 136 | +const bufferInfo = twgl.createBufferInfoFromArrays(gl, arrays); |
| 137 | +const vertexArrayInfo = twgl.createVertexArrayInfo(gl, programInfo, bufferInfo); |
| 138 | + |
| 139 | +function render(time) { |
| 140 | + canvas.style.width = `${document.documentElement.clientWidth}px`; |
| 141 | + canvas.style.height = `${document.documentElement.clientHeight}px`; |
| 142 | + twgl.resizeCanvasToDisplaySize(gl.canvas, window.devicePixelRatio); |
| 143 | + |
| 144 | + const [w, h] = [ |
| 145 | + gl.canvas.width / window.devicePixelRatio, |
| 146 | + gl.canvas.height / window.devicePixelRatio |
| 147 | + ]; |
| 148 | + const [cols, rows] = [Math.floor(w / fontSize), Math.floor(h / fontSize)]; |
| 149 | + |
| 150 | + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); |
| 151 | + gl.enable(gl.BLEND); |
| 152 | + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); |
| 153 | + gl.clearColor(0, 0.094, 0.42, 1.0); |
| 154 | + gl.clear(gl.COLOR_BUFFER_BIT); |
| 155 | + |
| 156 | + gl.useProgram(programInfo.program); |
| 157 | + twgl.setBuffersAndAttributes(gl, programInfo, vertexArrayInfo); |
| 158 | + twgl.setUniforms(programInfo, { |
| 159 | + time, |
| 160 | + u_texture: texture, |
| 161 | + resolution: [w, h], |
| 162 | + cols, |
| 163 | + }); |
| 164 | + const instanceCount = cols * rows; |
| 165 | + twgl.drawBufferInfo(gl, vertexArrayInfo, gl.TRIANGLES, 6, 0, instanceCount); |
| 166 | + requestAnimationFrame(render); |
| 167 | +} |
| 168 | + |
| 169 | +requestAnimationFrame(render); |
| 170 | +</script> |
| 171 | +</body> |
| 172 | +</html> |
0 commit comments