This is page 1 of 2. Use http://codebase.md/ertdfgcvb/play.core?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitignore ├── LICENSE ├── README.md ├── src │ ├── core │ │ ├── canvasrenderer.js │ │ ├── fps.js │ │ ├── storage.js │ │ ├── textrenderer.js │ │ └── version.js │ ├── makefile │ ├── modules │ │ ├── buffer.js │ │ ├── camera.js │ │ ├── canvas.js │ │ ├── color.js │ │ ├── drawbox.js │ │ ├── exportframe.js │ │ ├── filedownload.js │ │ ├── image.js │ │ ├── list.sh │ │ ├── load.js │ │ ├── num.js │ │ ├── sdf.js │ │ ├── sort.js │ │ ├── string.js │ │ ├── vec2.js │ │ └── vec3.js │ ├── programs │ │ ├── addheader.sh │ │ ├── basics │ │ │ ├── 10print.js │ │ │ ├── coordinates_index.js │ │ │ ├── coordinates_xy.js │ │ │ ├── cursor.js │ │ │ ├── how_to_draw_a_circle.js │ │ │ ├── how_to_draw_a_square.js │ │ │ ├── how_to_log.js │ │ │ ├── name_game.js │ │ │ ├── performance_test.js │ │ │ ├── rendering_to_canvas.js │ │ │ ├── sequence_export.js │ │ │ ├── simple_output.js │ │ │ ├── time_frames.js │ │ │ └── time_milliseconds.js │ │ ├── camera │ │ │ ├── camera_double_res.js │ │ │ ├── camera_gray.js │ │ │ └── camera_rgb.js │ │ ├── contributed │ │ │ ├── color_waves.js │ │ │ ├── emoji_wave.js │ │ │ ├── equal_tea_talk.js │ │ │ ├── ernst.js │ │ │ ├── game_of_life.js │ │ │ ├── pathfinder.js │ │ │ ├── sand_game.js │ │ │ ├── slime_dish.js │ │ │ └── stacked_sin_waves.js │ │ ├── demos │ │ │ ├── box_fun.js │ │ │ ├── chromaspiral.js │ │ │ ├── donut.js │ │ │ ├── doom_flame_full_color.js │ │ │ ├── doom_flame.js │ │ │ ├── dyna.js │ │ │ ├── gol_double_res.js │ │ │ ├── hotlink.js │ │ │ ├── mod_xor.js │ │ │ ├── moire_explorer.js │ │ │ ├── numbers.js │ │ │ ├── plasma.js │ │ │ ├── sinsin_checker.js │ │ │ ├── sinsin_wave.js │ │ │ ├── spiral.js │ │ │ └── wobbly.js │ │ ├── header.old.txt │ │ ├── list.sh │ │ └── sdf │ │ ├── balls.js │ │ ├── circle.js │ │ ├── cube.js │ │ ├── rectangles.js │ │ └── two_circles.js │ └── run.js └── tests ├── benchmark.html ├── browser_bugs │ ├── console_error_bug.html │ ├── error_listener_bug.html │ └── font_ready_bug.html ├── font.html ├── multi.html ├── promise_chain.html ├── proxy_test.html └── single.html ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # macOS 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # play.core 2 | 3 | Core files, example and demos of the live-code ASCII playground: 4 | [play.ertdfgcvb.xyz](https://play.ertdfgcvb.xyz) 5 | 6 | Examples and demos: 7 | [play.ertdfgcvb.xyz/abc.html#source:examples](https://play.ertdfgcvb.xyz/abc.html#source:examples) 8 | 9 | Embedding examples: 10 | [single](https://play.ertdfgcvb.xyz/tests/single.html) 11 | [multi](https://play.ertdfgcvb.xyz/tests/multi.html) 12 | 13 | Playground manual, API and resources: 14 | [play.ertdfgcvb.xyz/abc.html](https://play.ertdfgcvb.xyz/abc.html) 15 | ``` -------------------------------------------------------------------------------- /src/core/version.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | @module version.js 3 | @desc Runner version string 4 | @category core 5 | */ 6 | 7 | export default '1.1' 8 | ``` -------------------------------------------------------------------------------- /src/programs/basics/simple_output.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Simple output 5 | @desc The smallest program possible? 6 | */ 7 | 8 | export function main() { 9 | return '?' 10 | } 11 | 12 | // Shorter: 13 | // export const main = () => '?' 14 | 15 | // Even shorter: 16 | // export let main=o=>'?' 17 | 18 | // Shrtst: 19 | // export let main=o=>0 20 | ``` -------------------------------------------------------------------------------- /src/core/fps.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | @module fps 3 | @desc Frames-per-second-counter class. 4 | @category core 5 | */ 6 | 7 | export default class FPS { 8 | constructor() { 9 | this.frames = 0 10 | this.ptime = 0 11 | this.fps = 0 12 | } 13 | 14 | update(time) { 15 | this.frames++ 16 | if (time >= this.ptime + 1000) { 17 | this.fps = this.frames * 1000 / (time - this.ptime) 18 | this.ptime = time 19 | this.frames = 0 20 | } 21 | return this.fps 22 | } 23 | } 24 | ``` -------------------------------------------------------------------------------- /src/programs/basics/10print.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title 10 PRINT 5 | @desc 10 PRINT CHR$(205.5+RND(1)); : GOTO 10 6 | See also: 7 | https://10print.org 8 | */ 9 | 10 | // Run the program only once 11 | export const settings = { 12 | once : true 13 | } 14 | 15 | export function main() { 16 | // Also try: ╩ ╦ or ▄ ░ 17 | // or any combination from 18 | // https://play.ertdfgcvb.xyz/abc.html#font:characterset 19 | return Math.random() < 0.5 ? '╱' : '╲' 20 | } 21 | ``` -------------------------------------------------------------------------------- /src/programs/basics/coordinates_index.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Coordinates: index 5 | @desc Use of coord.index 6 | */ 7 | 8 | // Global variables have scope in the whole module. 9 | const pattern = '| |.|,|:|;|x|K|Ñ|R|a|+|=|-|_' 10 | // const pattern = '| |▁|▂|▃|▄|▅|▆|▇|▆|▅|▄|▃|▂|▁' 11 | 12 | // Resize the browser window to modify the pattern. 13 | export function main(coord, context, cursor, buffer) { 14 | const i = coord.index % pattern.length 15 | return pattern[i] 16 | } 17 | ``` -------------------------------------------------------------------------------- /src/core/storage.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | Save and restore a JSON object to and from local storage. 3 | */ 4 | 5 | export default { 6 | store : function(key, obj) { 7 | try { 8 | localStorage.setItem(key, JSON.stringify(obj)) 9 | return true 10 | } catch (e) { 11 | return false 12 | } 13 | }, 14 | restore : function(key, target = {}) { 15 | const obj = JSON.parse(localStorage.getItem(key)) 16 | Object.assign(target, obj) 17 | return target 18 | }, 19 | clear : function(key) { 20 | localStorage.removeItem(key) 21 | } 22 | } 23 | 24 | ``` -------------------------------------------------------------------------------- /src/programs/demos/sinsin_wave.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Sin Sin 5 | @desc Wave variation 6 | */ 7 | 8 | const pattern = '┌┘└┐╰╮╭╯' 9 | 10 | const { sin, round, abs } = Math 11 | 12 | export function main(coord, context, cursor, buffer) { 13 | const t = context.time * 0.0005 14 | const x = coord.x 15 | const y = coord.y 16 | const o = sin(y * x * sin(t) * 0.003 + y * 0.01 + t) * 20 17 | const i = round(abs(x + y + o)) % pattern.length 18 | return pattern[i] 19 | } 20 | 21 | import { drawInfo } from '/src/modules/drawbox.js' 22 | export function post(context, cursor, buffer) { 23 | drawInfo(context, cursor, buffer, { shadowStyle : 'gray' }) 24 | } 25 | ``` -------------------------------------------------------------------------------- /src/programs/basics/coordinates_xy.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Coordinates: x, y 5 | @desc Use of coord.x and coord.y 6 | */ 7 | 8 | const density = 'Ñ@#W$9876543210?!abc;:+=-,._ ' 9 | 10 | export function main(coord, context, cursor, buffer) { 11 | // To generate an output return a single character 12 | // or an object with a “char” field, for example {char: 'x'} 13 | 14 | // Shortcuts for frame, cols and coord (x, y) 15 | const {cols, frame } = context 16 | const {x, y} = coord 17 | 18 | // -1 for even lines, 1 for odd lines 19 | const sign = y % 2 * 2 - 1 20 | const index = (cols + y + x * sign + frame) % density.length 21 | 22 | return density[index] 23 | } ``` -------------------------------------------------------------------------------- /src/programs/contributed/emoji_wave.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ilithya 4 | @title Emoji Wave 5 | @desc From wingdings icons to unicode emojis 6 | Inspired by emojis evolution 7 | */ 8 | 9 | export const settings = { 10 | color : 'white', 11 | backgroundColor : 'rgb(100, 0, 300)' 12 | } 13 | 14 | const {sin, cos, floor} = Math 15 | const density = '☆ ☺︎ 👀 🌈 🌮🌮 🌈 👀 ☺︎ ☆' 16 | 17 | export function main(coord, context) { 18 | const t = context.time * 0.0008 19 | 20 | const x = coord.x 21 | const y = coord.y 22 | 23 | const c = context.cols 24 | const posCenter = floor((c - density.length) * 0.5) 25 | 26 | const wave = sin(y * cos(t)) * 5 27 | 28 | const i = floor(x + wave) - posCenter 29 | 30 | // Note: “undefined” is rendered as a space… 31 | return density[i] 32 | } ``` -------------------------------------------------------------------------------- /tests/browser_bugs/console_error_bug.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html lang="en" > 3 | <head> 4 | <meta charset="utf-8"> 5 | <title>Console error bug</title> 6 | </head> 7 | <body> 8 | 9 | Console output only.<br> 10 | <a href="https://bugs.webkit.org/show_bug.cgi?id=218275">bugs.webkit.org/show_bug.cgi?id=218275</a> 11 | 12 | <!-- 13 | *************************************** 14 | Safari / WebKit 15 | *************************************** 16 | 17 | When the script is of type="module" the line number 18 | of the error won’t be printed to the console. 19 | Check the console for the two errors. 20 | --> 21 | 22 | <script> 23 | function a( // <-- some syntax error here 24 | </script> 25 | 26 | <script type="module"> 27 | function b( // <-- some syntax error here 28 | </script> 29 | </body> 30 | </html> ``` -------------------------------------------------------------------------------- /src/programs/demos/mod_xor.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Mod Xor 5 | @desc Patterns obtained trough modulo and xor 6 | Inspired by this tweet by @ntsutae 7 | https://twitter.com/ntsutae/status/1292115106763960327 8 | */ 9 | 10 | const pattern = '└┧─┨┕┪┖┫┘┩┙┪━' 11 | 12 | export function main(coord, context, cursor, buffer) { 13 | const t1 = Math.floor(context.frame / 2) 14 | const t2 = Math.floor(context.frame / 128) 15 | const x = coord.x 16 | const y = coord.y + t1 17 | const m = t2 * 2 % 30 + 31 18 | const i = (x + y^x - y) % m & 1 19 | const c = (t2 + i) % pattern.length 20 | return pattern[c] 21 | } 22 | 23 | import { drawInfo } from '/src/modules/drawbox.js' 24 | export function post(context, cursor, buffer) { 25 | drawInfo(context, cursor, buffer, { shadowStyle : 'gray' }) 26 | } 27 | 28 | ``` -------------------------------------------------------------------------------- /src/programs/basics/cursor.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Cursor 5 | @desc Crosshair example with mouse cursor 6 | */ 7 | 8 | export function main(coord, context, cursor, buffer) { 9 | // The cursor coordinates are mapped to the cell 10 | // (fractional, needs rounding). 11 | const x = Math.floor(cursor.x) // column of the cell hovered 12 | const y = Math.floor(cursor.y) // row of the cell hovered 13 | 14 | if (coord.x == x && coord.y == y) return '┼' 15 | if (coord.x == x) return '│' 16 | if (coord.y == y) return '─' 17 | return (coord.x + coord.y) % 2 ? '·' : ' ' 18 | } 19 | 20 | import { drawInfo } from '/src/modules/drawbox.js' 21 | export function post(context, cursor, buffer) { 22 | drawInfo(context, cursor, buffer, { 23 | color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray' 24 | }) 25 | } 26 | ``` -------------------------------------------------------------------------------- /src/programs/basics/how_to_log.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title How to log 5 | @desc Console output inside the main() loop 6 | */ 7 | 8 | const { abs, floor, max } = Math 9 | 10 | export function main(coord, context, cursor, buffer) { 11 | 12 | const x = abs(coord.x - cursor.x) 13 | const y = abs(coord.y - cursor.y) / context.metrics.aspect 14 | const dist = floor(max(x, y) + context.frame) 15 | 16 | // Sometimes it’s useful to inspect values from inside the main loop. 17 | // The main() function is called every frame for every cell: 18 | // the console will be flooded with data very quickly! 19 | // Output can be limited to one cell and every 10 frames, for example: 20 | if (coord.index == 100 && context.frame % 10 == 0) { 21 | // console.clear() 22 | console.log("dist = " + dist) 23 | } 24 | 25 | return '.-=:abc123?xyz*;%+,'[dist % 30] 26 | } 27 | ``` -------------------------------------------------------------------------------- /src/programs/header.old.txt: -------------------------------------------------------------------------------- ``` 1 | --------------------------------------------------------------------- 2 | Type ?help 3 | anywhere (or edit the previous line) 4 | to open the manual for an overview about the playground, 5 | more commands like this and links to many examples. 6 | Type ?immediate on 7 | to enable immediate mode (off to disable). 8 | Type ?video night 9 | to switch to dark mode for the editor (day for light). 10 | --------------------------------------------------------------------- 11 | Cmd/Ctrl-Enter : run 12 | Cmd/Ctrl-Period : show/hide editor 13 | Cmd/Ctrl-S : save locally (with permalink) 14 | Cmd/Ctrl-Shift-U : share to playground (needs author and title tags) 15 | Program directory : https://play.ertdfgcvb.xyz/lsd 16 | --------------------------------------------------------------------- ``` -------------------------------------------------------------------------------- /src/programs/basics/rendering_to_canvas.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Canvas renderer 5 | @desc Rendering to a canvas element 6 | */ 7 | 8 | // A few extra fields are available when choosing the canvas renderer: 9 | // The offset (from top, left) and the size of the canvas element. 10 | export const settings = { 11 | renderer : 'canvas', 12 | // Settings available only 13 | // for the 'canvas' renderer 14 | canvasOffset : { 15 | x : 'auto', 16 | y : 20 17 | }, 18 | canvasSize : { 19 | width : 400, 20 | height : 500 21 | }, 22 | // Universal settings 23 | cols : 42, 24 | rows : 22, 25 | backgroundColor : 'pink', 26 | color : 'black' 27 | } 28 | 29 | const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ.:!?' 30 | 31 | export function main(coord, context, cursor, buffer) { 32 | const {x, y} = coord 33 | const f = context.frame 34 | const l = chars.length 35 | const c = context.cols 36 | return y % 2 ? chars[(y + x + f) % l] : chars[(y + c - x + f) % l] 37 | } 38 | ``` -------------------------------------------------------------------------------- /src/programs/basics/name_game.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Name game 5 | @desc What’s your name? 6 | */ 7 | 8 | // The default backround color and font attributes can be altered 9 | // by exporting a ‘settings’ object (see the manual for details). 10 | export const settings = { 11 | backgroundColor : 'black', 12 | color : 'white', 13 | fontSize : '3em', 14 | fontWeight : 'lighter' // or 100 15 | } 16 | 17 | const TAU = Math.PI * 2 18 | 19 | export function main(coord, context, cursor, buffer) { 20 | const a = context.frame * 0.05 21 | const f = Math.floor((1 - Math.cos(a)) * 10) + 1 22 | const g = Math.floor(a / TAU) % 10 + 1 23 | const i = coord.index % (coord.y * g + 1) % (f % context.cols) 24 | // NOTE: If the function returns ‘undefined’ or ‘null’ 25 | // a space character will be inserted. 26 | // In some cases ‘i’ may be greater than 2: 27 | // JavaScript array out of bounds results in ‘undefined’. 28 | return 'Ada'[i] 29 | } 30 | ``` -------------------------------------------------------------------------------- /src/programs/demos/hotlink.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Hotlink 5 | @desc Function hotlink example (GitHub) 6 | The code for the Open Simplex Noise function is downloaded from GitHub 7 | and evaluated through “new Function()”. 8 | */ 9 | 10 | // Don’t do this :) 11 | fetch("https://raw.githubusercontent.com/blindman67/SimplexNoiseJS/master/simplexNoise.js") 12 | .then(e => e.text()) 13 | .then(e => { 14 | const openSimplexNoise = new Function("return " + e)() 15 | noise3D = openSimplexNoise(Date.now()).noise3D 16 | }) 17 | 18 | // Stub function 19 | function noise3D() { return 0 } 20 | 21 | const density = 'Ñ@#W$9876543210?!abcxyz;:+=-,._ ' 22 | 23 | export function main(coord, context, cursor, buffer) { 24 | const t = context.time * 0.0007 25 | const s = 0.03 26 | const x = coord.x * s 27 | const y = coord.y * s / context.metrics.aspect + t 28 | const i = Math.floor((noise3D(x, y, t) * 0.5 + 0.5) * density.length) 29 | return density[i] 30 | } 31 | ``` -------------------------------------------------------------------------------- /src/programs/sdf/circle.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Circle 5 | @desc Draw a smooth circle with exp() 6 | */ 7 | 8 | import { sdCircle } from '/src/modules/sdf.js' 9 | import { sort } from '/src/modules/sort.js' 10 | 11 | const density = sort('/\\MXYZabc!?=-. ', 'Simple Console', false) 12 | 13 | export const settings = { fps : 60 } 14 | 15 | export function main(coord, context, cursor, buffer) { 16 | const t = context.time * 0.002 17 | const m = Math.min(context.cols, context.rows) 18 | const a = context.metrics.aspect 19 | 20 | const st = { 21 | x : 2.0 * (coord.x - context.cols / 2) / m * a, 22 | y : 2.0 * (coord.y - context.rows / 2) / m 23 | } 24 | 25 | const radius = (Math.cos(t)) * 0.4 + 0.5 26 | const d = sdCircle(st, radius) 27 | const c = 1.0 - Math.exp(-5 * Math.abs(d)) 28 | const index = Math.floor(c * density.length) 29 | 30 | return { 31 | char : coord.x % 2 ? '│' : density[index], 32 | backgroundColor : 'black', 33 | color : 'white' 34 | } 35 | } 36 | ``` -------------------------------------------------------------------------------- /tests/single.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html lang="en" > 3 | <head> 4 | <meta charset="utf-8"> 5 | <title>Test single</title> 6 | <link rel="stylesheet" type="text/css" href="/css/simple_console.css"> 7 | <style type="text/css" media="screen"> 8 | html, body { 9 | padding: 0; 10 | margin: 0; 11 | font-size: 1em; 12 | line-height: 1.2; 13 | font-family: 'Simple Console', monospace; 14 | } 15 | pre { 16 | position: absolute; 17 | margin:0; 18 | padding:0; 19 | left:0; 20 | top:0; 21 | width:100vw; 22 | height:100vh; 23 | font-family: inherit; 24 | } 25 | </style> 26 | </head> 27 | <body> 28 | <pre></pre> 29 | <script type="module"> 30 | import { run } from '/src/run.js' 31 | import * as program from '/src/programs/basics/time_milliseconds.js' 32 | run(program, { element : document.querySelector('pre') }).then(function(e){ 33 | console.log(e) 34 | }).catch(function(e) { 35 | console.warn(e.message) 36 | console.log(e.error) 37 | }) 38 | </script> 39 | </body> 40 | </html> ``` -------------------------------------------------------------------------------- /src/programs/contributed/color_waves.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author Eliza 4 | @title Color Waves 5 | @desc ¯\_(ツ)_/¯ 6 | */ 7 | 8 | const chars = '¯\_(ツ)_/¯.::.ᕦ(ò_óˇ)ᕤ '.split('') 9 | 10 | export const settings = { 11 | fontWeight : 700 12 | } 13 | 14 | export function main(coord, context, cursor){ 15 | const t = context.time * 0.0001 16 | 17 | const x = coord.x 18 | const y = coord.y 19 | 20 | const a = Math.cos(y * Math.cos(t) * 0.2 + x * 0.04 + t); 21 | const b = Math.sin(x * Math.sin(t) * 0.2 * y * 0.04 + t); 22 | const c = Math.cos(y * Math.cos(t) * 0.2 + x * 0.04 + t); 23 | 24 | const o = a + b + c * 20; 25 | 26 | const colors = ['mediumvioletred', 'gold', 'orange', 'chartreuse', 'blueviolet', 'deeppink']; 27 | 28 | const i = Math.round(Math.abs(x + y + o)) % chars.length 29 | return { 30 | char : chars[i], 31 | color : colors[i % colors.length] 32 | } 33 | } 34 | 35 | import { drawInfo } from '/src/modules/drawbox.js' 36 | export function post(context, cursor, buffer){ 37 | drawInfo(context, cursor, buffer) 38 | } 39 | ``` -------------------------------------------------------------------------------- /src/programs/demos/sinsin_checker.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Sin Sin 5 | @desc Checker variation 6 | */ 7 | 8 | const pattern = [ 9 | ' _000111_ ', 10 | '.+abc+. ' 11 | ] 12 | const col = ['black', 'blue'] 13 | const weights = [100, 700] 14 | 15 | const { floor, sin } = Math 16 | 17 | export function main(coord, context, cursor, buffer) { 18 | const t = context.time * 0.001 19 | const x = coord.x - context.cols / 2 20 | const y = coord.y - context.rows / 2 21 | const o = sin(x * y * 0.0017 + y * 0.0033 + t ) * 40 22 | const i = floor(Math.abs(x + y + o)) 23 | const c = (floor(coord.x * 0.09) + floor(coord.y * 0.09)) % 2 24 | return { 25 | char : pattern[c][i % pattern[c].length], 26 | color : 'black', //col[c], 27 | // backgroundColor : col[(c+1)%2], 28 | fontWeight : weights[c], 29 | } 30 | } 31 | 32 | import { drawInfo } from '/src/modules/drawbox.js' 33 | export function post(context, cursor, buffer) { 34 | drawInfo(context, cursor, buffer, { shadowStyle : 'gray' }) 35 | } 36 | ``` -------------------------------------------------------------------------------- /src/modules/image.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | @module image.js 3 | @desc Image loader and helper 4 | @category public 5 | 6 | Loads an image and draws it on a canvas. 7 | The returned object is a canvas wrapper and its methods (get, sample, etc.) 8 | can be used before the image has completely loaded. 9 | 10 | Usage: 11 | // Starts async loading: 12 | const img = Image.load('res/pattern.png') 13 | // Returns a black color until the image has been loaded: 14 | const color = img.get(10, 10) 15 | 16 | */ 17 | 18 | import Canvas from './canvas.js' 19 | import Load from './load.js' 20 | 21 | export default { 22 | load 23 | } 24 | 25 | function load(path) { 26 | 27 | const source = document.createElement('canvas') 28 | source.width = 1 29 | source.height = 1 30 | 31 | const can = new Canvas(source) 32 | 33 | Load.image(path).then( img => { 34 | console.log('Image ' + path + ' loaded. Size: ' + img.width + '×' + img.height) 35 | can.resize(img.width, img.height) 36 | can.copy(img) 37 | }).catch(err => { 38 | console.warn('There was an error loading image ' + path + '.') 39 | }) 40 | 41 | return can 42 | } 43 | ``` -------------------------------------------------------------------------------- /tests/promise_chain.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html lang="en" > 3 | <head> 4 | <meta charset="utf-8"> 5 | <title>Promise chain</title> 6 | </head> 7 | <body> 8 | <pre>Console output only</pre> 9 | <script> 10 | addEventListener('error', function(error) { 11 | console.log("------------") 12 | console.log(error) 13 | }, false) 14 | 15 | 16 | addEventListener('unhandledrejection', function(event) { 17 | console.log(event.promise) // [object Promise] - the promise that generated the error 18 | console.log(event.reason) // Error: Whoops! - the unhandled error object 19 | }) 20 | 21 | </script> 22 | <script type="module"> 23 | 24 | // Test to check how errors are 'catched' in async / Promises. 25 | // https://javascript.info/async-await 26 | // http://thecodebarbarian.com/async-await-error-handling-in-javascript.html 27 | 28 | 29 | function b() { 30 | 31 | c( // <--- 32 | function c() { 33 | setTimeout(() => resolve("b"), 1000) 34 | } 35 | 36 | } 37 | 38 | console.log("a") 39 | //b().then(res => console.log(res)) 40 | console.log("c") 41 | 42 | </script> 43 | </body> 44 | </html> ``` -------------------------------------------------------------------------------- /src/programs/basics/time_frames.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Time: frames 5 | @desc Use of context.frame (ASCII horizon) 6 | */ 7 | 8 | // The default framerate can be altered 9 | // by exporting a 'settings' object (see the manual for details). 10 | export const settings = { fps : 30 } 11 | 12 | export function main(coord, context, cursor, buffer) { 13 | const z = Math.floor((coord.y - context.rows / 2)) 14 | 15 | // Avoid division by zero 16 | if (z == 0) return ' ' 17 | 18 | // Calculate a fake perspective 19 | const val = (coord.x - context.cols/2) / z 20 | 21 | // Add time (context.frame) for animation 22 | // and make sure to get adisplayable charCode (int, positive, valid range) 23 | const code = Math.floor(val + context.cols/2 + context.frame * 0.3) % 94 + 32 24 | return String.fromCharCode(code) 25 | } 26 | 27 | // Display some info 28 | import { drawInfo } from '/src/modules/drawbox.js' 29 | export function post(context, cursor, buffer) { 30 | drawInfo(context, cursor, buffer, { 31 | color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray' 32 | }) 33 | } 34 | ``` -------------------------------------------------------------------------------- /src/programs/contributed/stacked_sin_waves.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author Raurir 4 | @title Stacked sin waves 5 | @desc noob at frag shaders 6 | */ 7 | 8 | const chars = "█▓▒░ ".split('') 9 | 10 | import { fract } from "/src/modules/num.js" 11 | 12 | export function main(coord, context, cursor, buffer){ 13 | const t = context.time * 0.002 14 | const x = coord.x 15 | const y = coord.y 16 | //const index = coord.index 17 | //const o = Math.sin(y * Math.sin(t) * 0.2 + x * 0.04 + t) * 20 18 | //const i = Math.round(Math.abs(x + y + o)) % chars.length 19 | const v0 = context.cols / 4 + wave(t, y, [0.15, 0.13, 0.37], [10,8,5]) * 0.9; 20 | const v1 = v0 + wave(t, y, [0.12, 0.14, 0.27], [3,6,5]) * 0.8; 21 | const v2 = v1 + wave(t, y, [0.089, 0.023, 0.217], [2,4,2]) * 0.3; 22 | const v3 = v2 + wave(t, y, [0.167, 0.054, 0.147], [4,6,7]) * 0.4; 23 | const i = x > v3 ? 4 24 | : x > v2 ? 3 25 | : x > v1 ? 2 26 | : x > v0 ? 1 27 | : 0; 28 | 29 | return chars[i]; 30 | } 31 | 32 | function wave(t, y, seeds, amps) { 33 | return ( 34 | (Math.sin(t + y * seeds[0]) + 1) * amps[0] 35 | + (Math.sin(t + y * seeds[1]) + 1) * amps[1] 36 | + (Math.sin(t + y * seeds[2])) * amps[2] 37 | ) 38 | } ``` -------------------------------------------------------------------------------- /src/programs/basics/time_milliseconds.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Time: milliseconds 5 | @desc Use of context.time 6 | */ 7 | 8 | // Globals have module scope 9 | const pattern = 'ABCxyz01═|+:. ' 10 | 11 | // This is the main loop. 12 | // Character coordinates are passed in coord {x, y, index}. 13 | // The function must return a single character or, alternatively, an object: 14 | // {char, color, background, weight}. 15 | export function main(coord, context, cursor, buffer) { 16 | const t = context.time * 0.0001 17 | const x = coord.x 18 | const y = coord.y 19 | const o = Math.sin(y * Math.sin(t) * 0.2 + x * 0.04 + t) * 20 20 | const i = Math.round(Math.abs(x + y + o)) % pattern.length 21 | return { 22 | char : pattern[i], 23 | fontWeight : '100', // or 'light', 'bold', '400' 24 | } 25 | } 26 | 27 | import { drawInfo } from '/src/modules/drawbox.js' 28 | 29 | // This function is called after the main loop and is useful 30 | // to manipulate the buffer; in this case with a window overlay. 31 | export function post(context, cursor, buffer) { 32 | // An extra object can be passed to drawInfo to alter the default style 33 | drawInfo(context, cursor, buffer, { 34 | color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray' 35 | }) 36 | } 37 | ``` -------------------------------------------------------------------------------- /src/modules/filedownload.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | @module filedownload.js 3 | @desc Exports a file via Blob 4 | @category internal 5 | 6 | Downloads a Blob as file and this “hack”: 7 | creates an anchor with a “download” attribute 8 | and then emits a click event. 9 | See: https://github.com/eligrey/FileSaver.js 10 | */ 11 | 12 | const mimeTypes = { 13 | 'js' : 'text/javascript', 14 | 'txt' : 'text/plain', 15 | 'png' : 'image/png', 16 | 'jpg' : 'text/jpeg', 17 | } 18 | 19 | // For text elements 20 | export function saveSourceAsFile(src, filename) { 21 | const ext = getFileExt(filename) 22 | const type = mimeTypes[ext] 23 | const blob = type ? new Blob([src], {type}) : new Blob([src]) 24 | saveBlobAsFile(blob, filename) 25 | } 26 | 27 | // Gets extension of a filename 28 | function getFileExt(filename) { 29 | return filename.split('.').pop() 30 | } 31 | 32 | // For canvas elements 33 | export function saveBlobAsFile(blob, filename) { 34 | 35 | const a = document.createElement('a') 36 | a.download = filename 37 | a.rel = 'noopener' 38 | a.href = URL.createObjectURL(blob) 39 | 40 | setTimeout(() => { URL.revokeObjectURL(a.href) }, 10000) 41 | setTimeout(() => { click(a) }, 0) 42 | } 43 | 44 | function click(node) { 45 | try { 46 | node.dispatchEvent(new MouseEvent('click')) 47 | } catch (err) { 48 | var e = document.createEvent('MouseEvents') 49 | e.initMouseEvent('click') 50 | node.dispatchEvent(e) 51 | } 52 | } 53 | ``` -------------------------------------------------------------------------------- /src/programs/camera/camera_gray.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Camera grayscale 5 | @desc Grayscale input from camera 6 | */ 7 | 8 | import { sort } from '/src/modules/sort.js' 9 | import Camera from '/src/modules/camera.js' 10 | import Canvas from '/src/modules/canvas.js' 11 | 12 | const cam = Camera.init() 13 | const can = new Canvas() 14 | // For a debug view uncomment the following line: 15 | // can.display(document.body, 10, 10) 16 | 17 | const density = sort(' .x?▂▄▆█', 'Simple Console', false) 18 | 19 | const data = [] 20 | 21 | export function pre(context, cursor, buffer) { 22 | const a = context.metrics.aspect 23 | 24 | // The canvas is resized so that 1 cell -> 1 pixel 25 | can.resize(context.cols, context.rows) 26 | // The cover() function draws an image (cam) to the canvas covering 27 | // the whole frame. The aspect ratio can be adjusted with the second 28 | // parameter. 29 | can.cover(cam, a).mirrorX().normalize().writeTo(data) 30 | } 31 | 32 | export function main(coord, context, cursor, buffer) { 33 | // Coord also contains the index of each cell: 34 | const color = data[coord.index] 35 | const index = Math.floor(color.v * (density.length-1)) 36 | return density[index] 37 | } 38 | 39 | import { drawInfo } from '/src/modules/drawbox.js' 40 | export function post(context, cursor, buffer) { 41 | drawInfo(context, cursor, buffer) 42 | } 43 | 44 | ``` -------------------------------------------------------------------------------- /tests/browser_bugs/error_listener_bug.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html lang="en" > 3 | <head> 4 | <meta charset="utf-8"> 5 | <title>Error listener bug</title> 6 | </head> 7 | <body> 8 | 9 | Console output only.<br> 10 | <a href="https://bugs.webkit.org/show_bug.cgi?id=218284">bugs.webkit.org/show_bug.cgi?id=218284</a> 11 | 12 | <!-- 13 | *************************************** 14 | Safari / WebKit 15 | *************************************** 16 | 17 | Some syntax errors are not captured by the event listener when originated in a module. 18 | 19 | Syntax errors inside a module in situations like: 20 | function a() { // missing closing bracket 21 | 22 | won’t get captured by the listener. 23 | While other errors like: 24 | const a = 1 25 | a = 2 26 | 27 | will get captured, even when generated inside the module. 28 | This works as expected in FF and Chrome. 29 | --> 30 | 31 | <script> 32 | addEventListener('error', function(e) { 33 | console.log('Captured: ' + e.message) 34 | }, false) 35 | </script> 36 | 37 | <!-- script --> 38 | 39 | <script> 40 | const a = 1 // CAPTURED 41 | a = 2 42 | </script> 43 | 44 | <script> 45 | for(let b=0; b<2 b++) {} // CAPTURED 46 | </script> 47 | 48 | <!-- module --> 49 | 50 | <script type="module"> 51 | const a = 1 // CAPTURED 52 | a = 2 53 | </script> 54 | 55 | <script type="module"> 56 | for(let a=0; a<2 a++) {} // NOT CAPTURED (but still displayed in the console) 57 | </script> 58 | 59 | </body> 60 | </html> ``` -------------------------------------------------------------------------------- /tests/multi.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html lang="en" > 3 | <head> 4 | <meta charset="utf-8"> 5 | <title>Test multi</title> 6 | <link rel="stylesheet" type="text/css" href="/css/simple_console.css"> 7 | <style type="text/css" media="screen"> 8 | body { 9 | background-color: rgb(250, 250, 250); 10 | margin:2em; 11 | } 12 | pre { 13 | margin: 1em; 14 | width: 40em; 15 | height: 22em; 16 | display: inline-block; 17 | font-size: 1em; 18 | line-height: 1.2; 19 | font-family: 'Simple Console', monospace; 20 | background-color: white; 21 | } 22 | 23 | </style> 24 | </head> 25 | <body> 26 | <pre></pre> 27 | <pre></pre> 28 | <pre></pre> 29 | <pre></pre> 30 | <script type="module"> 31 | import { run } from '/src/run.js' 32 | import * as prog0 from '/src/programs/basics/time_milliseconds.js' 33 | import * as prog1 from '/src/programs/basics/cursor.js' 34 | import * as prog2 from '/src/programs/sdf/balls.js' 35 | import * as prog3 from '/src/programs/basics/time_frames.js' 36 | 37 | const pre = document.querySelectorAll('pre') 38 | run(prog0, { element : pre[0] } ).catch(errorHandler) 39 | run(prog1, { element : pre[1] } ).catch(errorHandler) 40 | run(prog2, { element : pre[2] } ).catch(errorHandler) 41 | run(prog3, { element : pre[3] } ).catch(errorHandler) 42 | 43 | function errorHandler(e) { 44 | console.warn(e.message) 45 | console.log(e.error) 46 | } 47 | </script> 48 | </body> 49 | </html> ``` -------------------------------------------------------------------------------- /src/modules/load.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | @module loader.js 3 | @desc Various file type loader, returns a Promise 4 | @category internal 5 | 6 | Example: 7 | 8 | import Load from './load.js' 9 | 10 | // Usage: load different file types with one callback 11 | Promise.all([ 12 | Load.text('assets/1/text.txt'), 13 | Load.image('assets/1/blocks.png'), 14 | Load.image('assets/1/colors.png'), 15 | Load.json('data.json'), 16 | ]).then(function(res) { 17 | console.log('Everything has loaded!') 18 | console.log(res) 19 | }).catch(function() { 20 | console.log('Error') 21 | }) 22 | 23 | // Usage: load a single resource 24 | Load.image('assets/1/colors.png').then( img => { 25 | console.log(`Image has loaded, size is: ${img.width}x${img.height}`) 26 | }) 27 | 28 | */ 29 | 30 | export default { json, image, text } 31 | 32 | function image (url) { 33 | return new Promise((resolve, reject) => { 34 | const img = new Image() 35 | img.onload = () => resolve(img) 36 | img.onerror = () => { 37 | console.log('Loader: error loading image ' + url) 38 | resolve(null) 39 | } 40 | img.src = url 41 | }) 42 | } 43 | 44 | function text (url) { 45 | return fetch(url).then( response => { 46 | return response.text() 47 | }).catch( err => { 48 | console.log('Loader: error loading text ' + url) 49 | return '' 50 | }) 51 | } 52 | 53 | function json (url) { 54 | return fetch(url).then( response => { 55 | return response.json() 56 | }).catch( err => { 57 | console.log('Loader: error loading json ' + url) 58 | return {} 59 | }) 60 | } 61 | 62 | ``` -------------------------------------------------------------------------------- /src/modules/sdf.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | @module sdf.js 3 | @desc Some signed distance functions 4 | @category public 5 | 6 | SDF functions ported from the almighty Inigo Quilezles: 7 | https://www.iquilezles.org/www/articles/distfunctions/distfunctions.htm 8 | */ 9 | 10 | import { clamp, mix } from "./num.js" 11 | import { length, sub, dot, mulN } from "./vec2.js" 12 | 13 | export function sdCircle(p, radius) { // vec2, float 14 | return length(p) - radius 15 | } 16 | 17 | export function sdBox(p, size) { // vec2, vec2 18 | const d = { 19 | x : Math.abs(p.x) - size.x, 20 | y : Math.abs(p.y) - size.y, 21 | } 22 | d.x = Math.max(d.x, 0) 23 | d.y = Math.max(d.y, 0) 24 | return length(d) + Math.min(Math.max(d.x, d.y), 0.0) 25 | } 26 | 27 | export function sdSegment(p, a, b, thickness) { 28 | const pa = sub(p, a) 29 | const ba = sub(b, a) 30 | const h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0 ) 31 | return length(sub(pa, mulN(ba, h))) - thickness 32 | } 33 | 34 | export function opSmoothUnion( d1, d2, k ) { 35 | const h = clamp(0.5 + 0.5 * (d2 - d1) / k, 0.0, 1.0 ) 36 | return mix( d2, d1, h ) - k * h * (1.0 - h) 37 | } 38 | 39 | export function opSmoothSubtraction( d1, d2, k ) { 40 | const h = clamp( 0.5 - 0.5 * (d2 + d1) / k, 0.0, 1.0 ) 41 | return mix( d2, -d1, h ) + k * h * (1.0 - h) 42 | } 43 | 44 | export function opSmoothIntersection( d1, d2, k ) { 45 | const h = clamp( 0.5 - 0.5 * (d2 - d1) / k, 0.0, 1.0 ) 46 | return mix( d2, d1, h ) + k * h * (1.0 - h) 47 | } 48 | 49 | ``` -------------------------------------------------------------------------------- /src/programs/addheader.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # Script to replace the [header] string of each .js script 4 | # with the contents of the file 'header.txt'. 5 | # Expects a valid path for the output of the files. 6 | # 7 | # > sh ./addheader.sh output_folder 8 | 9 | if [ -z $1 ]; then 10 | echo "Please specify an output folder." 11 | exit 1 12 | fi 13 | 14 | # A bit of a cumbersome workaround 15 | # as the script won't be called from the current folder: 16 | CURRENT_PATH=$(pwd) # path from where this script has been called 17 | TARGET_PATH=$(pwd)/$1 # path to the copied .js files 18 | SCRIPT_PATH=${0%/*} # path to this script 19 | 20 | if [[ $SCRIPT_PATH -ef $TARGET_PATH ]]; then 21 | echo "Can’t overwrite the current files." 22 | exit 1 23 | fi 24 | 25 | RED='\033[0;31m' 26 | BLUE='\033[0;34m' 27 | PURPLE='\033[0;35m' 28 | NC='\033[0m' 29 | echo "${PURPLE}Writing headers...${NC}" 30 | 31 | cd $SCRIPT_PATH 32 | 33 | EXPR_1="/\[header\]/r header.txt" # Insert the contents of _header.txt after [header] 34 | EXPR_2="/\[header\]/d" # Delete the line with [header] 35 | 36 | for folder in `find ./ -mindepth 1 -type d`; do 37 | # echo "creating $TARGET_PATH/$folder..." 38 | 39 | mkdir -p $TARGET_PATH/$folder 40 | 41 | for file in ./$folder/*.js; do 42 | P=$(echo $file | sed 's/\.\///g') # prettier output 43 | echo "Writing $P..." 44 | sed -e "$EXPR_1" -e "$EXPR_2" "$file" > "$TARGET_PATH/$file" 45 | done 46 | done 47 | echo "${PURPLE}...done!${NC}" 48 | 49 | # Restore folder 50 | cd $CURRENT_PATH 51 | ``` -------------------------------------------------------------------------------- /src/programs/sdf/rectangles.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Rectangles 5 | @desc Smooth SDF Rectangles 6 | */ 7 | 8 | import { map } from '/src/modules/num.js' 9 | import { sdBox, opSmoothUnion } from '/src/modules/sdf.js' 10 | 11 | let density = '▚▀abc|/:÷×+-=?*· ' 12 | 13 | export function main(coord, context, cursor, buffer) { 14 | 15 | const t = context.time 16 | const m = Math.max(context.cols, context.rows) 17 | const a = context.metrics.aspect 18 | 19 | const st = { 20 | x : 2.0 * (coord.x - context.cols / 2) / m, 21 | y : 2.0 * (coord.y - context.rows / 2) / m / a 22 | } 23 | 24 | let d = 1e100 25 | 26 | const s = map(Math.sin(t * 0.0005), -1, 1, 0.0, 0.4) 27 | const g = 1.2 28 | for (let by=-g; by<=g; by+=g*0.33) { 29 | for (let bx=-g; bx<=g; bx+=g*0.33) { 30 | const r = t * 0.0004 * (bx + g*2) + (by + g*2) 31 | const f = transform(st, {x: bx, y: by}, r) 32 | const d1 = sdBox(f, {x:g*0.33, y:0.01}) 33 | d = opSmoothUnion(d, d1, s) 34 | } 35 | } 36 | 37 | let c = 1.0 - Math.exp(-5 * Math.abs(d)) 38 | const index = Math.floor(c * density.length) 39 | 40 | return density[index] 41 | } 42 | 43 | function transform(p, trans, rot) { 44 | const s = Math.sin(-rot) 45 | const c = Math.cos(-rot) 46 | const dx = p.x - trans.x 47 | const dy = p.y - trans.y 48 | return { 49 | x : dx * c - dy * s, 50 | y : dx * s + dy * c, 51 | } 52 | } 53 | 54 | import { drawInfo } from '/src/modules/drawbox.js' 55 | export function post(context, cursor, buffer) { 56 | drawInfo(context, cursor, buffer) 57 | } 58 | ``` -------------------------------------------------------------------------------- /src/modules/num.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | @module num.js 3 | @desc Some GLSL functions ported to JS 4 | @category public 5 | */ 6 | 7 | export default { 8 | map, 9 | fract, 10 | clamp, 11 | sign, 12 | mix, 13 | smoothstep, 14 | smootherstep 15 | } 16 | 17 | // Maps a value v from range 'in' to range 'out' 18 | export function map(v, inA, inB, outA, outB) { 19 | return outA + (outB - outA) * ((v - inA) / (inB - inA)) 20 | } 21 | 22 | // Returns the fractional part of a float 23 | export function fract(v) { 24 | return v - Math.floor(v) 25 | } 26 | 27 | // Clamps a value between min and max 28 | export function clamp(v, min, max) { 29 | if (v < min) return min 30 | if (v > max) return max 31 | return v 32 | } 33 | 34 | // Returns -1 for negative numbers, +1 for positive numbers, 0 for zero 35 | export function sign(n) { 36 | if (n > 0) return 1 37 | if (n < 0) return -1 38 | return 0 39 | } 40 | 41 | // GLSL mix 42 | export function mix(v1, v2, a) { 43 | return v1 * (1 - a) + v2 * a 44 | } 45 | 46 | // GLSL step 47 | export function step(edge, x) { 48 | return (x < edge ? 0 : 1) 49 | } 50 | 51 | // GLSL smoothstep 52 | // https://en.wikipedia.org/wiki/Smoothstep 53 | export function smoothstep(edge0, edge1, t) { 54 | const x = clamp((t - edge0) / (edge1 - edge0), 0, 1) 55 | return x * x * (3 - 2 * x) 56 | } 57 | 58 | // GLSL smootherstep 59 | export function smootherstep(edge0, edge1, t) { 60 | const x = clamp((t - edge0) / (edge1 - edge0), 0, 1) 61 | return x * x * x * (x * (x * 6 - 15) + 10) 62 | } 63 | 64 | // GLSL modulo 65 | export function mod(a, b) { 66 | return a % b 67 | } 68 | 69 | 70 | ``` -------------------------------------------------------------------------------- /src/programs/basics/performance_test.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Perfomance test 5 | @desc Vertical vs horizontal changes impact FPS 6 | */ 7 | 8 | import { map } from '/src/modules/num.js' 9 | 10 | export const settings = { fps : 60 } 11 | 12 | const { cos } = Math 13 | 14 | export function main(coord, context, cursor, buffer) { 15 | 16 | // Hold the mouse button to switch the direction 17 | // of the gradient and observe the drop in FPS. 18 | // Frequent *horizontal* changes in style will slow down 19 | // the DOM rendering as each character needs to be 20 | // wrapped in an individual, inline-styled <span>. 21 | // Frequent verical changes won’t affect the speed. 22 | 23 | const direction = cursor.pressed ? coord.x : coord.y 24 | 25 | const f = context.frame * 0.05 26 | 27 | const r1 = map(cos(direction * 0.06 + 1 -f), -1, 1, 0, 255) 28 | const g1 = map(cos(direction * 0.07 + 2 -f), -1, 1, 0, 255) 29 | const b1 = map(cos(direction * 0.08 + 3 -f), -1, 1, 0, 255) 30 | const r2 = map(cos(direction * 0.03 + 1 -f), -1, 1, 0, 255) 31 | const g2 = map(cos(direction * 0.04 + 2 -f), -1, 1, 0, 255) 32 | const b2 = map(cos(direction * 0.05 + 3 -f), -1, 1, 0, 255) 33 | 34 | return { 35 | char : context.frame % 10, 36 | color : `rgb(${r2},${g2},${b2})`, 37 | backgroundColor : `rgb(${r1},${g1},${b1})`, 38 | } 39 | } 40 | 41 | import { drawInfo } from '/src/modules/drawbox.js' 42 | export function post(context, cursor, buffer) { 43 | drawInfo(context, cursor, buffer) 44 | } 45 | ``` -------------------------------------------------------------------------------- /src/programs/camera/camera_double_res.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Camera double resolution 5 | @desc Doubled vertical resolution input from camera 6 | */ 7 | 8 | import { CSS3 } from '/src/modules/color.js' 9 | import Camera from '/src/modules/camera.js' 10 | import Canvas from '/src/modules/canvas.js' 11 | 12 | const cam = Camera.init() 13 | const can = new Canvas() 14 | // For a debug view uncomment the following line: 15 | // can.display(document.body, 10, 10) 16 | 17 | // Palette for quantization 18 | const pal = [] 19 | pal.push(CSS3.red) 20 | pal.push(CSS3.blue) 21 | pal.push(CSS3.white) 22 | pal.push(CSS3.black) 23 | pal.push(CSS3.lightblue) 24 | 25 | // Camera data 26 | const data = [] 27 | 28 | export function pre(context, cursor, buffer) { 29 | const a = context.metrics.aspect 30 | 31 | // The canvas is resized to the double of the height of the context 32 | can.resize(context.cols, context.rows * 2) 33 | 34 | // Also the aspect ratio needs to be doubled 35 | can.cover(cam, a * 2).quantize(pal).mirrorX().writeTo(data) 36 | } 37 | 38 | export function main(coord, context, cursor, buffer) { 39 | // Coord also contains the index of each cell: 40 | const idx = coord.y * context.cols * 2 + coord.x 41 | const upper = data[idx] 42 | const lower = data[idx + context.cols] 43 | 44 | return { 45 | char :'▄', 46 | color : lower.hex, 47 | backgroundColor : upper.hex 48 | } 49 | } 50 | 51 | import { drawInfo } from '/src/modules/drawbox.js' 52 | export function post(context, cursor, buffer) { 53 | drawInfo(context, cursor, buffer) 54 | } 55 | 56 | ``` -------------------------------------------------------------------------------- /src/modules/list.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # Script to generate a list of all the projects 4 | 5 | # Trims a string 6 | function trim { 7 | local var="$*" 8 | # remove leading whitespace characters 9 | var="${var#"${var%%[![:space:]]*}"}" 10 | # remove trailing whitespace characters 11 | var="${var%"${var##*[![:space:]]}"}" 12 | printf '%s' "$var" 13 | } 14 | 15 | 16 | SCRIPT_PATH=${0%/*} # path to this script 17 | 18 | 19 | 20 | function write { 21 | 22 | local var="$*" 23 | 24 | URL_PREFIX='/src/modules' 25 | 26 | FIRST=1 27 | 28 | for file in $SCRIPT_PATH/*.js; do 29 | 30 | CATEGORY=$(sed -En 's/^@category[ \t](.*)$/\1/p' $file) 31 | 32 | if [ $var == "$CATEGORY" ]; 33 | then 34 | # The path in $file is the full path: 35 | # ./play.core/src/modules/[folder]/[file].js 36 | URL=$URL_PREFIX$(echo $file | sed -e 's/\.\///' -e 's/\.play\.core\/src\/modules//') 37 | MODULE=$(sed -En 's/^@module[ \t](.*)$/\1/p' $file) 38 | DESC=$(sed -En 's/^@desc[ \t](.*)$/\1/p' $file) 39 | 40 | if [[ $FIRST == 1 ]]; then 41 | FIRST=0 42 | printf "\t" 43 | printf "<div>" 44 | printf "$(trim $CATEGORY)" 45 | printf "</div>" 46 | printf "\n" 47 | else 48 | printf "\t" 49 | printf "<div>" 50 | printf "</div>" 51 | printf "\n" 52 | fi 53 | 54 | printf "\t" 55 | printf "<div>" 56 | printf "<a target='_blank' href='$URL'>$(trim $MODULE)</a>" 57 | printf "</div>" 58 | printf "\n" 59 | 60 | printf "\t" 61 | printf "<div>" 62 | printf "$(trim $DESC)" 63 | printf "</div>" 64 | printf "\n" 65 | fi 66 | 67 | done 68 | } 69 | 70 | echo $(write "public") 71 | echo $(write "internal") 72 | echo $(write "renderer") 73 | 74 | ``` -------------------------------------------------------------------------------- /src/programs/sdf/balls.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Balls 5 | @desc Smooth SDF balls 6 | */ 7 | 8 | import { map } from '/src/modules/num.js' 9 | import { sdCircle, opSmoothUnion } from '/src/modules/sdf.js' 10 | 11 | const density = '#ABC|/:÷×+-=?*· ' 12 | 13 | const { PI, sin, cos, exp, abs } = Math 14 | 15 | export function main(coord, context, cursor, buffer) { 16 | const t = context.time * 0.001 + 10 17 | const m = Math.min(context.cols, context.rows) 18 | const a = context.metrics.aspect 19 | 20 | const st = { 21 | x : 2.0 * (coord.x - context.cols / 2) / m * a, 22 | y : 2.0 * (coord.y - context.rows / 2) / m 23 | } 24 | 25 | // const z = map(Math.sin(t * 0.00032), -1, 1, 0.5, 1) 26 | // st.x *= z 27 | // st.y *= z 28 | 29 | const s = map(sin(t * 0.5), -1, 1, 0.0, 0.9) 30 | 31 | let d = Number.MAX_VALUE 32 | 33 | const num = 12 34 | for (let i=0; i<num; i++) { 35 | const r = map(cos(t * 0.95 * (i + 1) / (num + 1)), -1, 1, 0.1, 0.3) 36 | const x = map(cos(t * 0.23 * (i / num * PI + PI)), -1, 1, -1.2, 1.2) 37 | const y = map(sin(t * 0.37 * (i / num * PI + PI)), -1, 1, -1.2, 1.2) 38 | const f = transform(st, {x, y}, t) 39 | d = opSmoothUnion(d, sdCircle(f, r), s) 40 | } 41 | 42 | let c = 1.0 - exp(-3 * abs(d)); 43 | //if (d < 0) c = 0 44 | 45 | const index = Math.floor(c * density.length) 46 | 47 | return density[index] 48 | } 49 | 50 | function transform(p, trans, rot) { 51 | const s = sin(-rot) 52 | const c = cos(-rot) 53 | const dx = p.x - trans.x 54 | const dy = p.y - trans.y 55 | return { 56 | x : dx * c - dy * s, 57 | y : dx * s + dy * c, 58 | } 59 | } 60 | ``` -------------------------------------------------------------------------------- /src/modules/camera.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | @module camera.js 3 | @desc Webcam init and helper 4 | @category public 5 | 6 | Initializes a user-facing camera, 7 | returns a video element (initialised asynchronously). 8 | */ 9 | 10 | export default { init } 11 | 12 | let video 13 | function init(callback) { 14 | // Avoid double init of video object 15 | video = video || getUserMedia(callback) 16 | return video 17 | } 18 | 19 | // https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia 20 | function getUserMedia(callback) { 21 | 22 | // getUserMedia is not supported by browser 23 | if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) { 24 | throw new DOMException('getUserMedia not supported in this browser') 25 | return 26 | } 27 | 28 | // Create a video element 29 | const video = document.createElement('video') 30 | video.setAttribute('playsinline', '') // Required to work in iOS 11 & up 31 | 32 | const constraints = { 33 | audio: false, 34 | video: { facingMode: "user" } 35 | } 36 | 37 | navigator.mediaDevices.getUserMedia(constraints).then(function(stream) { 38 | if ('srcObject' in video) { 39 | video.srcObject = stream 40 | } else { 41 | video.src = window.URL.createObjectURL(stream) 42 | } 43 | }).catch(function(err) { 44 | let msg = 'No camera available.' 45 | if (err.code == 1) msg = 'User denied access to use camera.' 46 | console.log(msg); 47 | console.error(err) 48 | }) 49 | 50 | video.addEventListener('loadedmetadata', function() { 51 | video.play() 52 | if (typeof callback === 'function') callback(video.srcObject) 53 | }) 54 | return video 55 | } 56 | ``` -------------------------------------------------------------------------------- /src/programs/demos/chromaspiral.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Chroma Spiral 5 | @desc Shadertoy port 6 | Inspired by this shader by scry 7 | https://www.shadertoy.com/view/tdsyRf 8 | */ 9 | 10 | import { map } from '/src/modules/num.js' 11 | import { sort } from '/src/modules/sort.js' 12 | import { vec2, rot, add, mulN, addN, subN, length } from '/src/modules/vec2.js' 13 | 14 | const { min, sin, cos, floor } = Math 15 | 16 | const density = '#Wabc:+-. ' 17 | const colors = ['deeppink', 'black', 'red', 'blue', 'orange', 'yellow'] 18 | 19 | export function main(coord, context, cursor, buffer) { 20 | const t = context.time * 0.0002 21 | const m = min(context.cols, context.rows) 22 | const a = context.metrics.aspect 23 | 24 | const st = { 25 | x : 2.0 * (coord.x - context.cols / 2) / m * a, 26 | y : 2.0 * (coord.y - context.rows / 2) / m 27 | } 28 | 29 | for (let i=0;i<3;i++) { 30 | const o = i * 3 31 | const v = vec2(sin(t * 3 + o), cos(t * 2 + o)) 32 | add(st, v, st) 33 | 34 | const ang = -t + length(subN(st, 0.5)) 35 | rot(st, ang, st) 36 | } 37 | 38 | mulN(st, 0.6, st) 39 | 40 | const s = cos(t) * 2.0 41 | let c = sin(st.x * 3.0 + s) + sin(st.y * 21) 42 | c = map(sin(c * 0.5), -1, 1, 0, 1) 43 | 44 | const index = floor(c * (density.length - 1)) 45 | const color = floor(c * (colors.length - 1)) 46 | 47 | return { 48 | // char : (coord.x + coord.y) % 2 ? density[index] : '╲', 49 | char : density[index], 50 | color : colors[color] 51 | } 52 | } 53 | 54 | import { drawInfo } from '/src/modules/drawbox.js' 55 | export function post(context, cursor, buffer) { 56 | drawInfo(context, cursor, buffer) 57 | } ``` -------------------------------------------------------------------------------- /src/programs/basics/how_to_draw_a_circle.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title How to draw a circle 5 | @desc Use of context.metrics.aspect 6 | */ 7 | 8 | import { length } from '/src/modules/vec2.js' 9 | 10 | export function main(coord, context, cursor, buffer) { 11 | 12 | // contex.metrics.aspect holds the font (or cell) aspect ratio 13 | const aspectRatio = cursor.pressed ? 1 : context.metrics.aspect 14 | 15 | // Transform coordinate space to (-1, 1) 16 | // width corrected screen aspect (m) and cell aspect (aspectRatio) 17 | const m = Math.min(context.cols * aspectRatio, context.rows) 18 | const st = { 19 | x : 2.0 * (coord.x - context.cols / 2) / m * aspectRatio, // apply aspect 20 | y : 2.0 * (coord.y - context.rows / 2) / m 21 | } 22 | 23 | // Distance of each cell from the center (0, 0) 24 | const l = length(st) 25 | 26 | // 0.7 is the radius of the circle 27 | return l < 0.7 ? 'X' : '.' 28 | } 29 | 30 | // Draw some info 31 | import { drawBox } from '/src/modules/drawbox.js' 32 | export function post(context, cursor, buffer) { 33 | // Apply some rounding to the aspect for better output 34 | const ar = cursor.pressed ? 1 : (''+context.metrics.aspect).substr(0, 8) 35 | 36 | // Output string 37 | let text = '' 38 | text += 'Hold the cursor button\n' 39 | text += 'to change the aspect ratio:\n' 40 | text += 'aspectRatio = ' + ar + '\n' 41 | 42 | // Custom box style 43 | const style = { 44 | backgroundColor : 'tomato', 45 | borderStyle : 'double', 46 | shadowStyle : 'gray' 47 | } 48 | 49 | // Finally draw the box 50 | drawBox(text, style, buffer, context.cols, context.rows) 51 | } 52 | ``` -------------------------------------------------------------------------------- /src/modules/exportframe.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | @module exportframe.js 3 | @desc Exports a single frame (or a range) to an image 4 | @category public 5 | 6 | Exports a frame as image. 7 | Expects the canvas renderer as the active renderer. 8 | Tested on Safari, FF, Chrome 9 | */ 10 | 11 | import {saveBlobAsFile} from '/src/modules/filedownload.js' 12 | 13 | export function exportFrame(context, filename, from=1, to=from) { 14 | 15 | // Error: renderer is not canvas. 16 | // A renderer instance could be imported here and the content of the buffer 17 | // rendere to a tmp canvas… maybe overkill: let’s keep things simple for now. 18 | const canvas = context.settings.element 19 | if (canvas.nodeName != 'CANVAS') { 20 | console.warn('exportframe.js: Can’t export, a canvas renderer is required.') 21 | return 22 | } 23 | 24 | // Error: filename not provided. 25 | // The function doesn’t provide a default name: this operation will probably 26 | // flood the “Downloads” folder with images… 27 | // It’s probably better to require a user-provided filename at least. 28 | if (!filename) { 29 | console.warn('exportframe.js: Filename not provided.') 30 | return 31 | } 32 | 33 | // Filename chunks 34 | const m = filename.match(/(.+)\.([0-9a-z]+$)/i) 35 | const base = m[1] 36 | const ext = m[2] 37 | 38 | // Finally export the frame 39 | const f = context.frame 40 | if (f >= from && f <= to) { 41 | const out = base + '_' + (f.toString().padStart(5, '0')) + '.' + ext 42 | console.info('exportframe.js: Exporting frame ' + out + '. Will stop at ' + to + '.') 43 | canvas.toBlob( blob => saveBlobAsFile(blob, out)) 44 | } 45 | } 46 | 47 | ``` -------------------------------------------------------------------------------- /src/programs/demos/wobbly.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Wobbly 5 | @desc Draw donuts with SDF 6 | */ 7 | 8 | import { sdCircle } from '/src/modules/sdf.js' 9 | import { sort } from '/src/modules/sort.js' 10 | import { length, rot } from '/src/modules/vec2.js' 11 | import { map, fract, smoothstep } from '/src/modules/num.js' 12 | 13 | const density = '▀▄▚▐─═0123.+?' 14 | 15 | export function main(coord, context, cursor, buffer) { 16 | const t = context.time * 0.001 17 | const m = Math.min(context.cols, context.rows) 18 | const a = context.metrics.aspect 19 | 20 | let st = { 21 | x : 2.0 * (coord.x - context.cols / 2) / m * a, 22 | y : 2.0 * (coord.y - context.rows / 2) / m 23 | } 24 | 25 | st = rot(st, 0.6 * Math.sin(0.62 * t) * length(st) * 2.5) 26 | st = rot(st, t * 0.2) 27 | 28 | const s = map(Math.sin(t), -1, 1, 0.5, 1.8) 29 | const pt = { 30 | x : fract(st.x * s) - 0.5, 31 | y : fract(st.y * s) - 0.5 32 | } 33 | 34 | const r = 0.5 * Math.sin(0.5 * t + st.x * 0.2) + 0.5 35 | 36 | const d = sdCircle(pt, r) 37 | 38 | const width = 0.05 + 0.3 * Math.sin(t); 39 | 40 | const k = smoothstep(width, width + 0.2, Math.sin(10 * d + t)); 41 | const c = (1.0 - Math.exp(-3 * Math.abs(d))) * k 42 | 43 | const index = Math.floor(c * (density.length-1)) 44 | 45 | return { 46 | char : density[index], 47 | color : k == 0 ? 'orangered' : 'royalblue', 48 | // backgroundColor : coord.y % 2 ? 'white' : 'cornsilk' 49 | } 50 | } 51 | 52 | import { drawInfo } from '/src/modules/drawbox.js' 53 | export function post(context, cursor, buffer) { 54 | drawInfo(context, cursor, buffer, { 55 | color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray' 56 | }) 57 | } 58 | ``` -------------------------------------------------------------------------------- /src/programs/demos/spiral.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Spiral 5 | @desc Shadertoy port 6 | Inspired by this shader by ahihi 7 | https://www.shadertoy.com/view/XdSGzR 8 | */ 9 | 10 | import { vec2, dot, add, sub, length } from '/src/modules/vec2.js' 11 | import { map } from '/src/modules/num.js' 12 | import { sort } from '/src/modules/sort.js' 13 | 14 | export const settings = { fps : 60 } 15 | 16 | const { sin, cos, floor, PI, atan, sqrt, pow } = Math 17 | const TAU = PI * 2 18 | 19 | const density = sort('▅▃▁?ab012:. ', 'Simple Console', false) 20 | 21 | export function main(coord, context, cursor, buffer) { 22 | const t = context.time * 0.0006 23 | const m = Math.min(context.cols, context.rows) 24 | const a = context.metrics.aspect 25 | 26 | let st = { 27 | x : 2.0 * (coord.x - context.cols / 2) / m * a, 28 | y : 2.0 * (coord.y - context.rows / 2) / m 29 | } 30 | 31 | const radius = length(st) 32 | const rot = 0.03 * TAU * t 33 | const turn = atan(st.y, st.x) / TAU + rot 34 | 35 | const n_sub = 1.5 36 | 37 | const turn_sub = n_sub * turn % n_sub 38 | 39 | const k = 0.1 * sin(3.0 * t) 40 | const s = k * sin(50.0 * (pow(radius, 0.1) - 0.4 * t)) 41 | const turn_sine = turn_sub + s 42 | 43 | const i_turn = floor(density.length * turn_sine % density.length) 44 | const i_radius = floor(1.5 / pow(radius * 0.5, 0.6) + 5.0 * t) 45 | const idx = (i_turn + i_radius) % density.length 46 | 47 | return density[idx] 48 | } 49 | 50 | import { drawInfo } from '/src/modules/drawbox.js' 51 | export function post(context, cursor, buffer) { 52 | drawInfo(context, cursor, buffer, { 53 | color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray' 54 | }) 55 | } 56 | ``` -------------------------------------------------------------------------------- /src/programs/demos/numbers.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Numbers 5 | @desc Fun with integers 6 | */ 7 | 8 | import { map } from '/src/modules/num.js' 9 | import { CGA } from '/src/modules/color.js' 10 | 11 | export const settings = { 12 | backgroundColor : 'black' 13 | } 14 | 15 | // Remove some colors (backwards, in place) from the CGA palette 16 | CGA.splice(10, 1) 17 | //CGA.splice(6, 1) 18 | CGA.splice(4, 1) 19 | CGA.splice(2, 1) 20 | CGA.splice(0, 1) 21 | 22 | const ints = [ 23 | 488162862, 24 | 147460255, 25 | 487657759, 26 | 1042482734, 27 | 71662658, 28 | 1057949230, 29 | 487540270, 30 | 1041305872, 31 | 488064558, 32 | 488080430 33 | ] 34 | 35 | const numX = 5 // number width 36 | const numY = 6 // number height 37 | const spacingX = 2 // spacing, after scale 38 | const spacingY = 1 39 | 40 | const bit = (n, k) => n >> k & 1 41 | 42 | export function main(coord, context, cursor, buffer) { 43 | 44 | const f = context.frame 45 | 46 | const scale = (map(Math.sin(f * 0.01), -1, 1, 0.99, context.rows / numY)) 47 | 48 | const x = coord.x / scale 49 | const y = coord.y / scale 50 | 51 | const sx = numX + spacingX / scale 52 | const sy = numY + spacingY / scale 53 | const cx = Math.floor(x / sx) // cell X 54 | const cy = Math.floor(y / sy) // cell Y 55 | 56 | const offs = Math.round(map(Math.sin(f * 0.012 + (cy * 0.5)), -1, 1, 0, 100)) 57 | const num = (cx + cy + offs) % 10 58 | 59 | const nx = Math.floor(x % sx) 60 | const ny = Math.floor(y % sy) 61 | 62 | let char 63 | 64 | if (nx < numX && ny < numY) { 65 | char = bit(ints[num], (numX - nx - 1) + (numY - ny - 1) * numX) 66 | } else { 67 | char = 0 68 | } 69 | 70 | let color = num % CGA.length 71 | return { 72 | char : '.▇'[char], 73 | color : char ? CGA[color].hex : CGA[5].hex 74 | } 75 | } 76 | 77 | ``` -------------------------------------------------------------------------------- /tests/proxy_test.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html lang="en" > 3 | <head> 4 | <meta charset="utf-8"> 5 | <title>Proxy test</title> 6 | </head> 7 | <body> 8 | <pre>Console output only</pre> 9 | <script type="module"> 10 | 11 | const ARR_LEN = 5000 12 | const array = [] 13 | for (let i=0; i<ARR_LEN; i++) { 14 | array[i] = i 15 | } 16 | 17 | const arrayP = [] 18 | for (let i=0; i<ARR_LEN; i++) { 19 | arrayP[i] = i 20 | } 21 | 22 | const proxy = new Proxy(arrayP, { 23 | apply: function(target, thisArg, argumentsList) { 24 | return thisArg[target].apply(this, argumentList) 25 | }, 26 | // deleteProperty: function(target, property) { 27 | // // console.log('Deleted ' + property) 28 | // return true 29 | // }, 30 | set: function(target, property, value, receiver) { 31 | if (value == target[property]) { 32 | // console.log('Value is identical, not set!') 33 | return true 34 | } 35 | target[property] = value 36 | // console.log('Set ' + property + ' to ' + value) 37 | return true 38 | } 39 | }) 40 | 41 | // -------------------------------------- 42 | 43 | const num = 10000 44 | 45 | const a0 = performance.now() 46 | 47 | for (let i=0; i<num; i++) { 48 | const idx = i % ARR_LEN 49 | if (array[idx] === i) continue 50 | array[idx] = i 51 | } 52 | 53 | const a1 = performance.now() 54 | 55 | console.log('Delta a: ' + (a1-a0)) 56 | 57 | // -------------------------------------- 58 | 59 | const b0 = performance.now() 60 | 61 | for (let i=0; i<num; i++) { 62 | // const idx = i % proxy.length 63 | const idx = i % ARR_LEN // Much faster than proxy.length! 64 | proxy[idx] = i 65 | } 66 | 67 | const b1 = performance.now() 68 | 69 | console.log('Delta b: ' + (b1-b0)) 70 | 71 | 72 | 73 | </script> 74 | </body> 75 | </html> ``` -------------------------------------------------------------------------------- /src/programs/contributed/game_of_life.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author Alex Miller 4 | @title GOL 5 | @desc Conway's Game of Life 6 | See https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life 7 | Each frame of the animation depends on the previous frame. Code in the `pre()` 8 | function saves the previous frame so it can be used in `main()`. 9 | */ 10 | 11 | import { dist, sub } from '/src/modules/vec2.js' 12 | 13 | let prevFrame; 14 | let width, height; 15 | 16 | export function pre(context, cursor, buffer) { 17 | if (width != context.cols || height != context.rows) { 18 | const length = context.cols * context.rows 19 | for (let i = 0; i < length; i++) { 20 | buffer[i] = {char : Math.random() > 0.5 ? '▒' : ' '}; 21 | } 22 | width = context.cols; 23 | height = context.rows; 24 | } 25 | 26 | // Use the spread operator to copy the previous frame 27 | // You must make a copy, otherwise `prevFrame` will be updated prematurely 28 | prevFrame = [...buffer]; 29 | } 30 | 31 | export function main(coord, context, cursor, buffer) { 32 | if (cursor.pressed) { 33 | if (dist(coord, cursor) < 3) { 34 | return Math.random() > 0.5 ? '▒' : ' '; 35 | } 36 | } 37 | 38 | const { x, y } = coord; 39 | const neighbors = 40 | get(x - 1, y - 1) + 41 | get(x, y - 1) + 42 | get(x + 1, y - 1) + 43 | get(x - 1, y) + 44 | get(x + 1, y) + 45 | get(x - 1, y + 1) + 46 | get(x, y + 1) + 47 | get(x + 1, y + 1); 48 | 49 | const current = get(x, y); 50 | if (current == 1) { 51 | return neighbors == 2 || neighbors == 3 ? '▒' : ' '; 52 | } else if (current == 0) { 53 | return neighbors == 3 ? 'x' : ' '; 54 | } 55 | } 56 | 57 | function get(x, y) { 58 | if (x < 0 || x >= width) return 0; 59 | if (y < 0 || y >= height) return 0; 60 | const index = y * width + x; 61 | return prevFrame[index].char === ' ' ? 0 : 1 62 | } 63 | ``` -------------------------------------------------------------------------------- /src/programs/contributed/ernst.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author nkint 4 | @title oeö 5 | @desc Inspired by Ernst Jandl, 1964 6 | */ 7 | 8 | import { dist, vec2, length, add, mulN } from '/src/modules/vec2.js' 9 | import { map, smoothstep } from '/src/modules/num.js'; 10 | 11 | const { PI, atan2, floor, cos, max } = Math; 12 | 13 | function polygon(center, edges, time) { 14 | time = time || 0 15 | // from https://observablehq.com/@riccardoscalco/draw-regular-polygons-by-means-of-functions 16 | const p = center; 17 | const N = edges; 18 | const a = (atan2(p.x, p.y) + 2 + time * PI) / (2. * PI); 19 | const b = (floor(a * N) + 0.5) / N; 20 | const c = length(p) * cos((a - b) * 2. * PI); 21 | return smoothstep(0.3, 0.31, c); 22 | } 23 | 24 | export function main(coord, context, cursor, buffer){ 25 | const m = max(context.cols, context.rows) 26 | const a = context.metrics.aspect 27 | 28 | const st = { 29 | x : 2.0 * (coord.x - context.cols / 2) / m, 30 | y : 2.0 * (coord.y - context.rows / 2) / m / a, 31 | } 32 | 33 | const centerT = add( 34 | st, 35 | vec2(0, cos(context.time * 0.0021) * 0.5) 36 | ); 37 | const colorT = polygon(centerT, 3, context.time * 0.0002) 38 | 39 | const triangle = colorT <= 0.1 ? 1 : 0 40 | 41 | const centerQ = add( 42 | st, 43 | vec2(cos(context.time * 0.0023) * 0.5, 0) 44 | ); 45 | const colorQ = polygon(centerQ, 4, -context.time * 0.0004) 46 | const quadrato = colorQ <= 0.1 ? 2 : 0 47 | const i = triangle + quadrato; 48 | const chars = [' ', 'e', 'o', 'ö'] 49 | 50 | const colors = ['white', '#527EA8', '#BB2A1C', '#DFA636'] 51 | return { 52 | char : chars[i], 53 | color : colors[i], 54 | fontWeight : '100' 55 | } 56 | } 57 | 58 | import { drawInfo } from '/src/modules/drawbox.js' 59 | export function post(context, cursor, buffer){ 60 | drawInfo(context, cursor, buffer) 61 | } 62 | 63 | ``` -------------------------------------------------------------------------------- /src/programs/basics/how_to_draw_a_square.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title How to draw a square 5 | @desc Draw a square using a distance function 6 | */ 7 | 8 | import { map } from '/src/modules/num.js' 9 | 10 | // Set framerate to 60 11 | export const settings = { fps : 60 } 12 | 13 | // Function to measure a distance to a square 14 | export function box(p, size) { 15 | const dx = Math.max(Math.abs(p.x) - size.x, 0) 16 | const dy = Math.max(Math.abs(p.y) - size.y, 0) 17 | // return the distance from the point 18 | return Math.sqrt(dx * dx + dy * dy) 19 | } 20 | 21 | export function main(coord, context, cursor, buffer) { 22 | const t = context.time 23 | const m = Math.min(context.cols, context.rows) 24 | const a = context.metrics.aspect 25 | 26 | // Normalize space and adjust aspect ratio (screen and char) 27 | const st = { 28 | x : 2.0 * (coord.x - context.cols / 2) / m, 29 | y : 2.0 * (coord.y - context.rows / 2) / m / a, 30 | } 31 | 32 | // Transform the coordinate by rotating it 33 | const ang = t * 0.0015 34 | const s = Math.sin(-ang) 35 | const c = Math.cos(-ang) 36 | const p = { 37 | x : st.x * c - st.y * s, 38 | y : st.x * s + st.y * c 39 | } 40 | 41 | // Size of the square 42 | const size = map(Math.sin(t * 0.0023), -1, 1, 0.1, 2) 43 | 44 | // Calculate the distance 45 | const d = box(p, {x:size, y:size}) 46 | 47 | // Visualize the distance field 48 | return d == 0 ? ' ' : (''+d).charAt(2) 49 | // Visualize the distance field and some background 50 | // return d == 0 ? (coord.x % 2 == 0 ? '─' : '┼') : (''+d).charAt(2) 51 | } 52 | 53 | import { drawInfo } from '/src/modules/drawbox.js' 54 | export function post(context, cursor, buffer) { 55 | drawInfo(context, cursor, buffer, { 56 | color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray' 57 | }) 58 | } 59 | ``` -------------------------------------------------------------------------------- /src/programs/demos/plasma.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Plasma 5 | @desc Oldschool plasma demo 6 | Plasma primer: https://www.bidouille.org/prog/plasma 7 | */ 8 | 9 | import { vec2, dot, add, sub, length } from '/src/modules/vec2.js' 10 | import { map } from '/src/modules/num.js' 11 | import { css } from '/src/modules/color.js' 12 | 13 | export const settings = { fps : 60 } 14 | 15 | const {sin, cos, floor, PI} = Math 16 | const density = '$?01▄abc+-><:. ' 17 | 18 | const PI23 = PI * 2 / 3 19 | const PI43 = PI * 4 / 3 20 | 21 | export function main(coord, context, cursor, buffer) { 22 | const t1 = context.time * 0.0009 23 | const t2 = context.time * 0.0003 24 | const m = Math.min(context.cols, context.rows) 25 | const a = context.metrics.aspect 26 | 27 | let st = { 28 | x : 2.0 * (coord.x - context.cols / 2) / m * a, 29 | y : 2.0 * (coord.y - context.rows / 2) / m 30 | } 31 | const center = vec2(sin(-t1), cos(-t1)) 32 | const v1 = sin(dot(coord, vec2(sin(t1), cos(t1))) * 0.08) 33 | const v2 = cos(length(sub(st, center)) * 4.0) 34 | const v3 = v1 + v2 35 | const idx = floor(map(v3, -2, 2, 0, 1) * density.length) 36 | 37 | // Colors are quantized for performance: 38 | // lower value = harder gradient, better performance 39 | const quant = 2 40 | const mult = 255 / (quant - 1) 41 | const r = floor(map(sin(v3 * PI + t1), -1, 1, 0, quant)) * mult 42 | const g = floor(map(sin(v3 * PI23 + t2), -1, 1, 0, quant)) * mult 43 | const b = floor(map(sin(v3 * PI43 - t1), -1, 1, 0, quant)) * mult 44 | 45 | return { 46 | char : density[idx], 47 | color : 'white', 48 | backgroundColor : css(r, g, b) // r, g, b are floats 49 | } 50 | } 51 | 52 | import { drawInfo } from '/src/modules/drawbox.js' 53 | export function post(context, cursor, buffer) { 54 | drawInfo(context, cursor, buffer) 55 | } 56 | ``` -------------------------------------------------------------------------------- /src/programs/basics/sequence_export.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Sequence export 5 | @desc Export 10 frames as images 6 | */ 7 | 8 | import { exportFrame } from '/src/modules/exportframe.js' 9 | 10 | // Important: the frame exporter works only with the canvas renderer. 11 | // Optional: reset the frame count and time at each new run! 12 | export const settings = { 13 | renderer : 'canvas', 14 | restoreState : false, // Reset time 15 | fps : 2 // Slow down: some browsers can’t keep up with high framerates 16 | } 17 | 18 | // The frame is exported at the beginning of each new pass, 19 | // the canvas still contains the previously rendered image. 20 | // Exported frame “10” will in fact be frame “9” of the loop! 21 | 22 | export function pre(context, cursor, buffer) { 23 | // The filename will be postfixed with the frame number: 24 | // export_10.png, export_11.png, etc. 25 | // The last two parameters are the start and the end frame 26 | // of the sequence to be exported. 27 | 28 | exportFrame(context, 'export.png', 10, 20) 29 | 30 | // The image will (probably) be saved in the “Downloads” folder 31 | // and can be assembled into a movie file; for example with FFmpeg: 32 | // 33 | // > ffmpeg -framerate 30 -pattern_type glob -i "export_*.png" \ 34 | // -vcodec h264 -pix_fmt yuv420p \ 35 | // -preset:v slow -profile:v baseline -crf 23 export.m4v 36 | } 37 | 38 | export function main(coord, context, cursor, buffer) { 39 | if ((coord.x + coord.y) % 2 != 0) return ' ' 40 | return (context.frame - 9) % 10 41 | } 42 | 43 | // Display some info (will also be exported!) 44 | import { drawInfo } from '/src/modules/drawbox.js' 45 | export function post(context, cursor, buffer) { 46 | drawInfo(context, cursor, buffer, { 47 | color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray' 48 | }) 49 | } 50 | ``` -------------------------------------------------------------------------------- /src/programs/sdf/two_circles.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Two circles 5 | @desc Smooth union of two circles 6 | */ 7 | 8 | import { sdCircle, opSmoothUnion } from '/src/modules/sdf.js' 9 | import { sub, vec2 } from '/src/modules/vec2.js' 10 | 11 | const density = '#WX?*:÷×+=-· ' 12 | 13 | export function main(coord, context, cursor, buffer) { 14 | const t = context.time 15 | const m = Math.min(context.cols, context.rows) 16 | const a = context.metrics.aspect 17 | 18 | const st = vec2( 19 | 2.0 * (coord.x - context.cols / 2) / m * a, 20 | 2.0 * (coord.y - context.rows / 2) / m 21 | ) 22 | 23 | // A bit of a waste as cursor is not coord dependent; 24 | // it could be calculated in pre(), and stored in a global 25 | // (see commented code below). 26 | const pointer = vec2( 27 | 2.0 * (cursor.x - context.cols / 2) / m * a, 28 | 2.0 * (cursor.y - context.rows / 2) / m 29 | ) 30 | 31 | // Circles 32 | const d1 = sdCircle(st, 0.2) // origin, 0.2 is the radius 33 | const d2 = sdCircle(sub(st, pointer), 0.2) // cursor 34 | 35 | // Smooth operation 36 | const d = opSmoothUnion(d1, d2, 0.7) 37 | 38 | // Calc index of the char map 39 | const c = 1.0 - Math.exp(-5 * Math.abs(d)) 40 | const index = Math.floor(c * density.length) 41 | 42 | return density[index] 43 | 44 | } 45 | 46 | import { drawInfo } from '/src/modules/drawbox.js' 47 | export function post(context, cursor, buffer) { 48 | drawInfo(context, cursor, buffer) 49 | } 50 | 51 | // Uncomment this to calculate the cursor position only once 52 | // and pass it to the main function as a global 53 | /* 54 | const p = vec2(0, 0) 55 | export function pre(context, cursor, buffer) { 56 | const m = Math.min(context.cols, context.rows) 57 | const a = context.metrics.aspect 58 | p.x = 2.0 * (cursor.x - context.cols / 2) / m * a, 59 | p.y = 2.0 * (cursor.y - context.rows / 2) / m 60 | } 61 | */ 62 | ``` -------------------------------------------------------------------------------- /src/programs/camera/camera_rgb.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Camera RGB 5 | @desc Color input from camera (quantised) 6 | */ 7 | 8 | import { map } from '/src/modules/num.js' 9 | import { rgb2hex, rgb} from '/src/modules/color.js' 10 | import Camera from '/src/modules/camera.js' 11 | import Canvas from '/src/modules/canvas.js' 12 | 13 | const cam = Camera.init() 14 | const can = new Canvas() 15 | // For a debug view uncomment the following line: 16 | // can.display(document.body, 10, 10) 17 | 18 | const density = ' .+=?X#ABC' 19 | 20 | // A custom palette used for color quantisation: 21 | const pal = [] 22 | pal.push(rgb( 0, 0, 0)) 23 | pal.push(rgb(255, 0, 0)) 24 | pal.push(rgb(255, 255, 0)) 25 | pal.push(rgb( 0, 100, 250)) 26 | pal.push(rgb(100, 255, 255)) 27 | //pal.push(rgb(255, 182, 193)) 28 | //pal.push(rgb(255, 255, 255)) 29 | 30 | const data = [] 31 | 32 | export function pre(context, cursor, buffer) { 33 | const a = context.metrics.aspect 34 | 35 | // The canvas is resized so that 1 cell -> 1 pixel 36 | can.resize(context.cols, context.rows) 37 | // The cover() function draws an image (cam) to the canvas covering 38 | // the whole frame. The aspect ratio can be adjusted with the second 39 | // parameter. 40 | can.cover(cam, a).mirrorX().quantize(pal).writeTo(data) 41 | } 42 | 43 | export function main(coord, context, cursor, buffer) { 44 | // Coord also contains the index of each cell 45 | const color = data[coord.index] 46 | // Add some chars to the output 47 | const index = Math.floor(color.v * (density.length-1)) 48 | return { 49 | char : density[index], 50 | color : 'white', 51 | // convert {r,g,b} obj to a valid CSS hex string 52 | backgroundColor : rgb2hex(color) 53 | } 54 | } 55 | 56 | import { drawInfo } from '/src/modules/drawbox.js' 57 | export function post(context, cursor, buffer) { 58 | drawInfo(context, cursor, buffer) 59 | } 60 | ``` -------------------------------------------------------------------------------- /src/programs/demos/box_fun.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Box fun 5 | @desc Think inside of the box 6 | */ 7 | 8 | import { clamp, map } from '/src/modules/num.js' 9 | 10 | const {sin, cos, floor} = Math 11 | 12 | export function main(coord, context, cursor, buffer) { 13 | // Basic background pattern 14 | return (coord.x + coord.y) % 2 ? '·' : ' ' 15 | } 16 | 17 | import { drawBox } from '/src/modules/drawbox.js' 18 | export function post(context, cursor, buffer) { 19 | const { rows, cols } = context 20 | const t = context.time * 0.002 21 | const baseW = 15 22 | const baseH = 5 23 | const spacingX = 4 24 | const spacingY = 2 25 | 26 | let marginX = 3 27 | let marginY = 2 28 | 29 | const numX = floor((cols - marginX*2) / (baseW+spacingX)) 30 | const numY = floor((rows - marginY*2) / (baseH+spacingY)) 31 | 32 | marginX = floor((cols - numX * baseW - (numX-1) * spacingX)/2) 33 | marginY = floor((rows - numY * baseH - (numY-1) * spacingY)/2) 34 | 35 | const q = 'THINK INSIDE OF THE BOX' 36 | 37 | const baseStyle = { 38 | paddingX : 2, 39 | paddingY : 1, 40 | color : 'black', 41 | backgroundColor : 'white', 42 | borderStyle : 'double', 43 | shadowStyle : 'light', 44 | } 45 | 46 | for (let j=0; j<numY; j++) { 47 | for (let i=0; i<numX; i++) { 48 | const ox = floor(sin((i + j) * 0.6 + t*3) * spacingX) 49 | const oy = floor(cos((i + j) * 0.6 + t*3) * spacingY) 50 | const ow = 0//floor(sin((i + j) * 0.4 + t*2) * 5) + 5 51 | const oh = 0//floor(cos((i + j) * 0.4 + t*2) * 2) + 2 52 | const style = { 53 | x : marginX + i * (baseW + spacingX) + ox, 54 | y : marginY + j * (baseH + spacingY) + oy, 55 | width : baseW + ow, 56 | height : baseH + oh, 57 | ...baseStyle 58 | } 59 | let txt = '' 60 | txt += `*${q[(i + j * numX) % q.length]}*\n` 61 | txt += `pos: ${style.x}×${style.y}\n` 62 | 63 | drawBox(txt, style, buffer, cols, rows) 64 | } 65 | } 66 | } 67 | ``` -------------------------------------------------------------------------------- /src/programs/demos/moire_explorer.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Moiré explorer 5 | @desc Click or tap to toggle mode 6 | */ 7 | 8 | import { vec2, dist, mulN } from '/src/modules/vec2.js' 9 | import { map } from '/src/modules/num.js' 10 | 11 | export const settings = { fps : 60 } 12 | 13 | // Shorthands 14 | const { sin, cos, atan2, floor, min } = Math 15 | 16 | // Change the mouse pointer to 'pointer' 17 | export const boot = (context) => context.settings.element.style.cursor = 'pointer' 18 | 19 | // Cycle modes with click or tap 20 | export const pointerDown = () => mode = ++mode % 3 21 | let mode = 0 22 | 23 | const density = ' ..._-:=+abcXW@#ÑÑÑ' 24 | 25 | export function main(coord, context, cursor) { 26 | const t = context.time * 0.0001 27 | const m = min(context.cols, context.rows) 28 | const st = { 29 | x : 2.0 * (coord.x - context.cols / 2) / m, 30 | y : 2.0 * (coord.y - context.rows / 2) / m, 31 | } 32 | st.x *= context.metrics.aspect 33 | 34 | const centerA = mulN(vec2(cos(t*3), sin(t*7)), 0.5) 35 | const centerB = mulN(vec2(cos(t*5), sin(t*4)), 0.5) 36 | 37 | // Set A or B to zero to see only one of the two frequencies 38 | const A = mode % 2 == 0 ? atan2(centerA.y-st.y, centerA.x-st.x) : dist(st, centerA) 39 | const B = mode == 0 ? atan2(centerB.y-st.y, centerB.x-st.x) : dist(st, centerB) 40 | 41 | const aMod = map(cos(t*2.12), -1, 1, 6, 60) 42 | const bMod = map(cos(t*3.33), -1, 1, 6, 60) 43 | //const aMod = 27 44 | //const bMod = 29 45 | 46 | const a = cos(A * aMod) 47 | const b = cos(B * bMod) 48 | 49 | const i = ((a * b) + 1) / 2 // mult 50 | //const i = ((a + b) + 2) / 4 // sum 51 | const idx = floor(i * density.length) 52 | return density[idx] 53 | } 54 | 55 | import { drawInfo } from '/src/modules/drawbox.js' 56 | export function post(context, cursor, buffer) { 57 | drawInfo(context, cursor, buffer, { 58 | color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray' 59 | }) 60 | } 61 | ``` -------------------------------------------------------------------------------- /src/programs/list.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # Script to generate a list of all the projects, 4 | # will be inserted in the manual page 5 | 6 | # Trims a string 7 | function trim { 8 | local var="$*" 9 | # remove leading whitespace characters 10 | var="${var#"${var%%[![:space:]]*}"}" 11 | # remove trailing whitespace characters 12 | var="${var%"${var##*[![:space:]]}"}" 13 | printf '%s' "$var" 14 | } 15 | 16 | SCRIPT_PATH=${0%/*} # path to this script 17 | 18 | URL_PREFIX='/#/src' 19 | 20 | NUM_COLS=${1:-3} 21 | 22 | 23 | # All folders 24 | # FOLDERS=`find ./ -mindepth 1 -type d` 25 | 26 | # Odered list of folders 27 | FOLDERS=(basics sdf demos camera contributed) 28 | 29 | for folder in ${FOLDERS[@]}; do 30 | 31 | FIRST=1 32 | 33 | for file in $SCRIPT_PATH/$folder/*.js; do 34 | # The path in $file is the full path: 35 | # ./play.core/src/programs/[folder]/[file].js 36 | URL=$URL_PREFIX$(echo $file | sed -e 's/\.\///' -e 's/\.js//' -e 's/\.play\.core\/src\/programs//') 37 | AUTHOR=$(sed -En 's/^@author[ \t](.*)$/\1/p' $file) 38 | TITLE=$(sed -En 's/^@title[ \t](.*)$/\1/p' $file) 39 | DESC=$(sed -En 's/^@desc[ \t](.*)$/\1/p' $file) 40 | 41 | if [[ $FIRST == 1 ]]; then 42 | FOLDER=$(echo $folder | sed -e 's/\.\///' -e 's/\///' -e 's/\.js//' ) 43 | FIRST=0 44 | printf "\t" 45 | printf "<div class='program-cathegory'>" 46 | printf "$(trim $FOLDER)" 47 | printf "</div>" 48 | printf "\n" 49 | else 50 | if [[ $NUM_COLS == 3 ]]; then 51 | printf "\t" 52 | printf "<div>" 53 | printf "</div>" 54 | printf "\n" 55 | fi 56 | fi 57 | 58 | printf "\t" 59 | printf "<div>" 60 | if [[ $NUM_COLS == 3 ]]; then 61 | printf "<a target='_blank' href='$URL'>$(trim $TITLE)</a>" 62 | else 63 | printf "<a href='$URL'>$(trim $TITLE)</a>" 64 | fi 65 | printf "</div>" 66 | printf "\n" 67 | 68 | if [[ $NUM_COLS == 3 ]]; then 69 | printf "\t" 70 | printf "<div>" 71 | printf "$(trim $DESC)" 72 | printf "</div>" 73 | printf "\n" 74 | fi 75 | done 76 | done 77 | ``` -------------------------------------------------------------------------------- /src/modules/string.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | @module string.js 3 | @desc String helpers 4 | @category internal 5 | 6 | Wraps a string to a specific width. 7 | Doesn’t break words and keeps trailing line breaks. 8 | Counts lines and maxWidth (can be greater than width). 9 | If no width is passed the function just measures the 'box' of the text. 10 | */ 11 | 12 | export function wrap(string, width=0) { 13 | 14 | if (width==0) return measure(string) 15 | 16 | const paragraphs = string.split('\n') 17 | let out = '' 18 | 19 | let maxWidth = 0 20 | let numLines = 0 21 | 22 | for (const p of paragraphs) { 23 | const chunks = p.split(' ') 24 | let len = 0 25 | for(const word of chunks) { 26 | // First word 27 | if (len == 0) { 28 | out += word 29 | len = word.length 30 | maxWidth = Math.max(maxWidth, len) 31 | } 32 | // Subsequent words 33 | else { 34 | if (len + 1 + word.length <= width) { 35 | out += ' ' + word 36 | len += word.length + 1 37 | maxWidth = Math.max(maxWidth, len) 38 | } else { 39 | // Remove last space 40 | out += '\n' + word 41 | len = word.length + 1 42 | numLines++ 43 | } 44 | } 45 | } 46 | out += '\n' 47 | numLines++ 48 | } 49 | 50 | // Remove last \n 51 | out = out.slice(0, -1) 52 | 53 | // Adjust line count in case of last trailing \n 54 | if (out.charAt(out.length-1) == '\n') numLines-- 55 | 56 | return { 57 | text : out, 58 | numLines, 59 | maxWidth 60 | } 61 | } 62 | 63 | // TODO: fix bug for 1 line str, will return numLines = 0) 64 | /* 65 | export function measure(string) { 66 | const chunks = string.split('\n') 67 | return { 68 | text : string, 69 | numLines : chunks.length, 70 | maxWidth : Math.max( chunks.map( e => e.length) ) 71 | } 72 | } 73 | */ 74 | 75 | 76 | export function measure(string) { 77 | let numLines = 0 78 | let maxWidth = 0 79 | let len = 0 80 | 81 | for (let i=0; i<string.length; i++) { 82 | const char = string[i] 83 | if (char == '\n') { 84 | len = 0 85 | numLines++ 86 | } else { 87 | len++ 88 | maxWidth = Math.max(maxWidth, len) 89 | } 90 | } 91 | return { 92 | text : string, 93 | numLines, 94 | maxWidth 95 | } 96 | } 97 | ``` -------------------------------------------------------------------------------- /src/modules/sort.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | @module sort.js 3 | @desc Sorts a set of characters by brightness 4 | @category public 5 | 6 | Paints chars on a temporary canvas and counts the pixels. 7 | This could be done once and then stored / hardcoded. 8 | The fontFamily paramter needs to be set because it's used by the canvas element 9 | to draw the correct font. 10 | */ 11 | 12 | export function sort(charSet, fontFamily, ascending = false) { 13 | 14 | const size = 30 15 | const ctx = document.createElement('canvas').getContext('2d') 16 | ctx.canvas.width = size * 2 17 | ctx.canvas.height = size * 2 18 | 19 | ctx.canvas.style.right = '0' 20 | ctx.canvas.style.top = '0' 21 | ctx.canvas.style.position = 'absolute' 22 | document.body.appendChild(ctx.canvas) // NOTE: needs to be attached to the DOM 23 | 24 | if (ctx.getImageData(0, 0, 1, 1).data.length == 0) { 25 | console.warn("getImageData() is not supported on this browser.\nCan’t sort chars for brightness.") 26 | return charSet 27 | } 28 | 29 | let out = [] 30 | for (let i=0; i<charSet.length; i++) { 31 | ctx.fillStyle = 'black' 32 | ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height) 33 | ctx.fillStyle = 'rgb(255,255,255)' 34 | ctx.font = size + 'px ' + fontFamily // NOTE: font family inherit doesn't work 35 | ctx.textAlign = 'center' 36 | ctx.textBaseline = 'middle' 37 | ctx.fillText(charSet[i], ctx.canvas.width / 2, ctx.canvas.height / 2) 38 | 39 | out[i] = { 40 | count : 0, 41 | char : charSet[i], 42 | index : i, 43 | } 44 | 45 | const data = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data 46 | 47 | for (let y=0; y<ctx.canvas.height; y++) { 48 | const oy = y * ctx.canvas.width 49 | for (let x=0; x<ctx.canvas.width; x++) { 50 | let r = data[4 * (x + oy)] 51 | out[i].count += r 52 | } 53 | } 54 | //console.log(out[i].char, out[i].count) 55 | } 56 | 57 | // cleanup 58 | document.body.removeChild(ctx.canvas) 59 | if (ascending) { 60 | return out.sort((a, b) => a.count - b.count).map( x => x.char).join('') 61 | } else { 62 | return out.sort((a, b) => b.count - a.count).map( x => x.char).join('') 63 | } 64 | } 65 | ``` -------------------------------------------------------------------------------- /src/programs/contributed/equal_tea_talk.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author nkint 4 | @title EQUAL TEA TALK, #65 5 | @desc Inspired by Frederick Hammersley, 1969 6 | See: http://www.hammersleyfoundation.org/index.php/artwork/computer-drawings/184-computer-drawings/331-equal-tea-talk 7 | */ 8 | 9 | import { map, step, mod } from '/src/modules/num.js' 10 | import { vec2, mul, fract as fract2 } from '/src/modules/vec2.js' 11 | 12 | const chars = '#BEFTI_'.split('') 13 | 14 | const wx = new Array(50).fill(0).map((_, i) => i); 15 | const sumW = (n) => (n * 2 + 3) * 5 16 | const getW = (cols) => { 17 | let w = 0 18 | for(let _wx of wx) { 19 | const s = sumW(_wx) 20 | if (cols <= s) { 21 | break; 22 | } 23 | w = s; 24 | } 25 | return w 26 | } 27 | 28 | const blank = ' '; 29 | 30 | export const settings = { 31 | fontWeight: '100' 32 | } 33 | 34 | export function main(coord, context, cursor, buffer){ 35 | const t = context.time * 0.0001 36 | 37 | // compute resolution to fit geometry, not the screen 38 | // 🙏thanks andreas 39 | let w = getW(context.cols); 40 | 41 | // show blank if the resolution is too small 42 | if(context.cols <= sumW(1)) { 43 | return blank 44 | } 45 | 46 | // clamp the space to the resolution 47 | if(coord.x >= w) { 48 | return blank 49 | } 50 | 51 | // ----------------------------- st space 52 | // ----------------------------- 53 | // ----------------------------- 54 | const st = { 55 | x : (coord.x / w), 56 | y : (coord.y / context.rows), 57 | } 58 | const _st = mul(st, vec2(5.0, 1.0)); 59 | const tileIndex = step(1, mod(_st.x, 2.0)); 60 | // make each cell between 0.0 - 1.0 61 | // see Truchet Tiles from https://thebookofshaders.com/09/ 62 | // for reference/inspiration 63 | const __st = fract2(_st); 64 | 65 | const color = tileIndex === 0 ? (_st.y) : (1 - _st.y); 66 | const i = Math.floor( 67 | map( 68 | color, 69 | 0, 1, 70 | 0, chars.length - 1 71 | ) 72 | ); 73 | 74 | let char = chars[i]; 75 | 76 | // ----------------------------- pixel space 77 | // ----------------------------- 78 | // ----------------------------- 79 | const unit = w / 5 80 | const middle = Math.floor(unit/2) - 1; 81 | const xPx = coord.x % unit 82 | 83 | const isFirstOrLast = xPx !==0 && xPx !== unit 84 | 85 | if( 86 | isFirstOrLast && 87 | (((xPx) === middle) || ((xPx) === middle + 2)) 88 | ) { 89 | char = ' ' 90 | } 91 | 92 | if( 93 | isFirstOrLast && 94 | (xPx) === middle + 1 95 | ) { 96 | char = '-' 97 | } 98 | 99 | // some debug utils 100 | // throw new Error('dudee') 101 | // if(coord.x === 0) { console.log() } 102 | 103 | return char 104 | } 105 | ``` -------------------------------------------------------------------------------- /src/programs/demos/donut.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Donut 5 | @desc Ported from a1k0n’s donut demo. 6 | https://www.a1k0n.net/2011/07/20/donut-math.html 7 | 8 | This program writes directly into the frame buffer 9 | in a sort of 'brute force' way: 10 | theta and phi (below) must be small enough to fill 11 | all the gaps. 12 | */ 13 | 14 | export const settings = { backgroundColor : 'whitesmoke' } 15 | 16 | export function pre(context, cursor, buffer) { 17 | 18 | const TAU = Math.PI * 2 19 | 20 | const z = [] 21 | const A = context.time * 0.0015 22 | const B = context.time * 0.0017 23 | 24 | const width = context.cols 25 | const height = context.rows 26 | 27 | const centerX = width / 2 28 | const centerY = height / 2 29 | const scaleX = 50 30 | const scaleY = scaleX * context.metrics.aspect 31 | 32 | // Precompute sines and cosines of A and B 33 | const cA = Math.cos(A) 34 | const sA = Math.sin(A) 35 | const cB = Math.cos(B) 36 | const sB = Math.sin(B) 37 | 38 | // Clear the buffers 39 | const num = width * height 40 | for(let k=0; k<num; k++) { 41 | buffer[k].char = ' ' // char buffer 42 | z[k] = 0 // z buffer 43 | } 44 | 45 | // Theta (j) goes around the cross-sectional circle of a torus 46 | for(let j=0; j<TAU; j+=0.05) { 47 | 48 | // Precompute sines and cosines of theta 49 | const ct = Math.cos(j) 50 | const st = Math.sin(j) 51 | 52 | // Phi (i) goes around the center of revolution of a torus 53 | for(let i=0; i<TAU; i+=0.01) { 54 | 55 | // Precompute sines and cosines of phi 56 | const sp = Math.sin(i) 57 | const cp = Math.cos(i) 58 | 59 | // The x,y coordinate of the circle, before revolving 60 | const h = ct+2 // R1 + R2*cos(theta) 61 | const D = 1/(sp*h*sA+st*cA+5) // this is 1/z 62 | const t = sp*h*cA-st*sA 63 | 64 | // Final 3D (x,y,z) coordinate after rotations 65 | const x = 0 | (centerX + scaleX*D*(cp*h*cB-t*sB)) 66 | const y = 0 | (centerY + scaleY*D*(cp*h*sB+t*cB)) 67 | const o = x + width * y 68 | 69 | // Range 0..11 (8 * sqrt(2) = 11.3) 70 | const N = 0 | (8*((st*sA-sp*ct*cA)*cB-sp*ct*sA-st*cA-cp*ct*sB)) 71 | if(y<height && y>=0 && x>=0 && x<width && D>z[o]) { 72 | z[o] = D 73 | buffer[o].char = '.,-~:;=!*#$@'[N > 0 ? N : 0] 74 | } 75 | } 76 | } 77 | } 78 | 79 | import { drawInfo } from '/src/modules/drawbox.js' 80 | export function post(context, cursor, buffer) { 81 | drawInfo(context, cursor, buffer, { 82 | color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray' 83 | }) 84 | } 85 | ``` -------------------------------------------------------------------------------- /tests/browser_bugs/font_ready_bug.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html lang="en" > 3 | <head> 4 | <meta charset="utf-8"> 5 | <title>fonts.ready bug</title> 6 | <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@100&display=swap" rel="stylesheet"> 7 | <!-- alternative load: 8 | <style>@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@100&display=swap');</style> 9 | --> 10 | <style type="text/css" media="screen"> 11 | span { 12 | /*font-family: monospace;*/ 13 | /*font-family: monospace, monospace;*/ 14 | font-family: 'Roboto Mono', monospace; 15 | } 16 | </style> 17 | </head> 18 | <body> 19 | 20 | Console output only.<br> 21 | <a href="https://bugs.webkit.org/show_bug.cgi?id=217047">bugs.webkit.org/show_bug.cgi?id=217047</a> 22 | 23 | <!-- 24 | *************************************** 25 | Safari / WebKit 26 | *************************************** 27 | 28 | This test measures the width of a span element filled with 100 'X' 29 | It needs to be run twice: 30 | 31 | RUN 1) 32 | Empty the caches (CMD-OPT-E) and reload the page 33 | The output in the console is something similar: 34 | a 1155.47 <-- what happens here? 35 | maybe the monospace, monospace issue? 36 | https://stackoverflow.com/questions/38781089/font-family-monospace-monospace 37 | b 963.28 <-- incorect size! (this is the size of 'monospace' and not 'Roboto Mono') 38 | 39 | RUN 2) 40 | Reload the page, the output is now: 41 | a 960.16 <-- correct size (on second reload the fonts are probably cached) 42 | b 960.16 <-- correct size (finally the expected value) 43 | 44 | 45 | There are two VERY strange fixes for this issue: 46 | 47 | FIX A) 48 | remove type="module" from the <script> tag 49 | 50 | FIX B) 51 | comment line 67 (leave the test element displayed) 52 | --> 53 | 54 | <script type="module"> 55 | 56 | console.log('a ' + calcWidth().toFixed(2)) 57 | 58 | document.fonts.ready.then(()=>{ 59 | console.log('b ' + calcWidth().toFixed(2)) 60 | }) 61 | 62 | function calcWidth(el) { 63 | 64 | const test = document.createElement('span') 65 | document.body.appendChild(test) // Must be visible! 66 | 67 | test.innerHTML = ''.padStart(100, 'X') 68 | const w = test.getBoundingClientRect().width 69 | test.innerHTML += '<br>Measured width: ' + w.toFixed(2) + '<br>' 70 | 71 | document.body.removeChild(test) // If this line is commented the measure works on the first run! 72 | 73 | return w 74 | } 75 | 76 | </script> 77 | </body> 78 | </html ``` -------------------------------------------------------------------------------- /src/programs/sdf/cube.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Wireframe cube 5 | @desc The cursor controls box thickness and exp 6 | */ 7 | 8 | import { sdSegment } from '/src/modules/sdf.js' 9 | import * as v2 from '/src/modules/vec2.js' 10 | import * as v3 from '/src/modules/vec3.js' 11 | import { map } from '/src/modules/num.js' 12 | 13 | export const settings = { fps : 60 } 14 | 15 | const density = ' -=+abcdX' 16 | 17 | // Shorthands 18 | const { vec3 } = v3 19 | const { vec2 } = v2 20 | const { sin, cos, floor, abs, exp, min } = Math 21 | 22 | // Lookup table for the background 23 | const bgMatrix = [ 24 | '┼──────', 25 | '│ ', 26 | '│ ', 27 | '│ ', 28 | '│ ', 29 | '│ ', 30 | ] 31 | 32 | // Box primitive 33 | const l = 0.6 34 | const box = { 35 | vertices : [ 36 | vec3( l, l, l), 37 | vec3(-l, l, l), 38 | vec3(-l,-l, l), 39 | vec3( l,-l, l), 40 | vec3( l, l,-l), 41 | vec3(-l, l,-l), 42 | vec3(-l,-l,-l), 43 | vec3( l,-l,-l) 44 | ], 45 | edges : [ 46 | [0, 1], 47 | [1, 2], 48 | [2, 3], 49 | [3, 0], 50 | [4, 5], 51 | [5, 6], 52 | [6, 7], 53 | [7, 4], 54 | [0, 4], 55 | [1, 5], 56 | [2, 6], 57 | [3, 7] 58 | ] 59 | } 60 | 61 | const boxProj = [] 62 | 63 | const bgMatrixDim = vec2(bgMatrix[0].length, bgMatrix.length) 64 | 65 | export function pre(context, cursor) { 66 | const t = context.time * 0.01 67 | const rot = vec3(t * 0.11, t * 0.13, -t * 0.15) 68 | const d = 2 69 | const zOffs = map(sin(t*0.12), -1, 1, -2.5, -6) 70 | for (let i=0; i<box.vertices.length; i++) { 71 | const v = v3.copy(box.vertices[i]) 72 | let vt = v3.rotX(v, rot.x) 73 | vt = v3.rotY(vt, rot.y) 74 | vt = v3.rotZ(vt, rot.z) 75 | boxProj[i] = v2.mulN(vec2(vt.x, vt.y), d / (vt.z - zOffs)) 76 | } 77 | } 78 | 79 | export function main(coord, context, cursor) { 80 | const t = context.time * 0.01 81 | const m = min(context.cols, context.rows) 82 | const a = context.metrics.aspect 83 | 84 | const st = { 85 | x : 2.0 * (coord.x - context.cols / 2 + 0.5) / m * a, 86 | y : 2.0 * (coord.y - context.rows / 2 + 0.5) / m, 87 | } 88 | 89 | let d = 1e10 90 | const n = box.edges.length 91 | const thickness = map(cursor.x, 0, context.cols, 0.001, 0.1) 92 | const expMul = map(cursor.y, 0, context.rows, -100, -5) 93 | for (let i=0; i<n; i++) { 94 | const a = boxProj[box.edges[i][0]] 95 | const b = boxProj[box.edges[i][1]] 96 | d = min(d, sdSegment(st, a, b, thickness)) 97 | } 98 | 99 | const idx = floor(exp(expMul * abs(d)) * density.length) 100 | 101 | if (idx == 0) { 102 | const x = coord.x % bgMatrixDim.x 103 | const y = coord.y % bgMatrixDim.y 104 | return { 105 | char : d < 0 ? ' ' : bgMatrix[y][x], 106 | color : 'black' 107 | } 108 | } else { 109 | return { 110 | char : density[idx], 111 | color : 'royalblue' 112 | } 113 | } 114 | } 115 | 116 | import { drawInfo } from '/src/modules/drawbox.js' 117 | export function post(context, cursor, buffer) { 118 | drawInfo(context, cursor, buffer) 119 | } 120 | 121 | ``` -------------------------------------------------------------------------------- /src/programs/contributed/sand_game.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author Alex Miller 4 | @title Sand game 5 | @desc Click to drop sand 6 | */ 7 | 8 | import { dist, sub } from '/src/modules/vec2.js' 9 | 10 | let prevFrame; 11 | let width, height; 12 | 13 | function newParticle() { 14 | return 'sand'.charAt(Math.random() * 4) 15 | } 16 | 17 | export function pre(context, cursor, buffer) { 18 | if (width != context.cols || height != context.rows) { 19 | const length = context.cols * context.rows 20 | for (let i = 0; i < length; i++) { 21 | buffer[i] = {char : Math.random() > 0.5 ? newParticle() : ' '}; 22 | } 23 | width = context.cols; 24 | height = context.rows; 25 | } 26 | 27 | // Use the spread operator to copy the previous frame 28 | // You must make a copy, otherwise `prevFrame` will be updated prematurely 29 | prevFrame = [...buffer]; 30 | } 31 | 32 | export function main(coord, context, cursor, buffer) { 33 | if (cursor.pressed) { 34 | if (dist(coord, cursor) < 3) { 35 | return { 36 | char: Math.random() > 0.5 ? newParticle() : ' ', 37 | 38 | backgroundColor: 'white', 39 | color: 'rgb(179, 158, 124)', 40 | fontWeight: 500 41 | } 42 | } 43 | } 44 | 45 | const { x, y } = coord; 46 | 47 | const me = get(x, y); 48 | const below = get(x, y + 1); 49 | const above = get(x, y - 1); 50 | 51 | const left = get(x - 1, y); 52 | const right = get(x + 1, y); 53 | 54 | const topleft = get(x - 1, y - 1); 55 | const topright = get(x + 1, y - 1); 56 | const bottomleft = get(x - 1, y + 1); 57 | const bottomright = get(x + 1, y + 1); 58 | const frame = context.frame; 59 | 60 | 61 | if (y >= context.rows - 1) { 62 | return { 63 | char: 'GROUND_'.charAt(x % 7), 64 | backgroundColor: 'rgb(138, 162, 70)', 65 | color: 'rgb(211, 231, 151)', 66 | fontWeight: 700 67 | } 68 | } 69 | 70 | if (x >= context.cols - 1 || x <= 0) { 71 | return { 72 | char: 'WALL'.charAt(y % 4), 73 | backgroundColor: 'rgb(247, 187, 39)', 74 | color: 'white', 75 | fontWeight: 700 76 | } 77 | } 78 | 79 | let char = ' ' 80 | if (alive(me)) { 81 | if (alive(below) && 82 | ((frame % 2 == 0 && alive(bottomright)) || 83 | (frame % 2 == 1 && alive(bottomleft)))) { 84 | char = me; 85 | } else { 86 | char = ' '; 87 | } 88 | } else { 89 | if (alive(above)) { 90 | char = above; 91 | } else if (alive(left) && frame % 2 == 0 && alive(topleft)) { 92 | char = topleft; 93 | } else if (alive(right) && frame % 2 == 1 && alive(topright)) { 94 | char = topright; 95 | } else { 96 | char = ' '; 97 | } 98 | 99 | 100 | if ('WALL'.indexOf(char) > -1) { 101 | char = ' '; 102 | } 103 | } 104 | 105 | return { 106 | char, 107 | backgroundColor: 'white', 108 | color: 'rgb(179, 158, 124)', 109 | fontWeight: 500 110 | } 111 | } 112 | 113 | function alive(char) { 114 | return char != ' '; 115 | } 116 | 117 | function get(x, y) { 118 | if (x < 0 || x >= width) return 0; 119 | if (y < 0 || y >= height) return 0; 120 | const index = y * width + x; 121 | return prevFrame[index].char; 122 | } 123 | 124 | ``` -------------------------------------------------------------------------------- /src/programs/demos/dyna.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Dyna 5 | @desc A remix of Paul Haeberli’s Dynadraw 6 | 7 | The original from 1989: 8 | http://www.graficaobscura.com/dyna/ 9 | */ 10 | 11 | import { copy, length, vec2, add, sub, divN, mulN } from '/src/modules/vec2.js' 12 | import { smoothstep } from '/src/modules/num.js' 13 | 14 | export const settings = { fps : 60 } 15 | 16 | const MASS = 20 // Pencil mass 17 | const DAMP = 0.95 // Pencil damping 18 | const RADIUS = 15 // Pencil radius 19 | 20 | let cols, rows 21 | 22 | export function pre(context, cursor, buffer) { 23 | 24 | // Detect window resize 25 | if (cols != context.cols || rows != context.rows) { 26 | cols = context.cols 27 | rows = context.rows 28 | for (let i=0; i<cols*rows; i++) { 29 | buffer[i].value = 0 30 | } 31 | } 32 | 33 | const a = context.metrics.aspect // Shortcut 34 | 35 | dyna.update(cursor) 36 | 37 | // Calc line between pos and pre 38 | const points = line(dyna.pos, dyna.pre) 39 | 40 | for (const p of points) { 41 | const sx = Math.max(0, p.x - RADIUS) 42 | const ex = Math.min(cols, p.x + RADIUS) 43 | const sy = Math.floor(Math.max(0, p.y - RADIUS * a)) 44 | const ey = Math.floor(Math.min(rows, p.y + RADIUS * a)) 45 | 46 | for (let j=sy; j<ey; j++) { 47 | for (let i=sx; i<ex; i++) { 48 | const x = (p.x - i) 49 | const y = (p.y - j) / a 50 | const l = 1 - length({x, y}) / RADIUS 51 | const idx = i + cols * j 52 | buffer[idx].value = Math.max(buffer[idx].value, l) 53 | } 54 | } 55 | } 56 | } 57 | 58 | const density = ' .:░▒▓█Ñ#+-'.split('') 59 | 60 | // Just a renderer 61 | export function main(coord, context, cursor, buffer) { 62 | const i = coord.index 63 | const v = smoothstep(0, 0.9, buffer[i].value) 64 | buffer[i].value *= 0.99 65 | const idx = Math.floor(v * (density.length - 1)) 66 | return density[idx] 67 | } 68 | 69 | import { drawInfo } from '/src/modules/drawbox.js' 70 | export function post(context, cursor, buffer) { 71 | drawInfo(context, cursor, buffer, { 72 | color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray' 73 | }) 74 | } 75 | 76 | // ----------------------------------------------------------------------------- 77 | 78 | class Dyna { 79 | constructor(mass, damp) { 80 | this.pos = vec2(0,0) 81 | this.vel = vec2(0,0) 82 | this.pre = vec2(0,0) 83 | this.mass = mass 84 | this.damp = damp 85 | } 86 | update(cursor) { 87 | const force = sub(cursor, this.pos) 88 | const acc = divN(force, this.mass) 89 | this.vel = mulN(add(this.vel, acc), this.damp) 90 | this.pre = copy(this.pos) 91 | this.pos = add(this.pos, this.vel) 92 | } 93 | } 94 | 95 | const dyna = new Dyna(MASS, DAMP) 96 | 97 | // ----------------------------------------------------------------------------- 98 | // Bresenham’s line algorithm 99 | // https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm 100 | // NOTE: vectors a and b will be floored 101 | 102 | function line(a, b) { 103 | let x0 = Math.floor(a.x) 104 | let y0 = Math.floor(a.y) 105 | const x1 = Math.floor(b.x) 106 | const y1 = Math.floor(b.y) 107 | const dx = Math.abs(x1 - x0) 108 | const dy = -Math.abs(y1 - y0) 109 | const sx = x0 < x1 ? 1 : -1 110 | const sy = y0 < y1 ? 1 : -1 111 | let err = dx + dy 112 | 113 | const points = [] 114 | 115 | while (true) { 116 | points.push({x:x0, y:y0}) 117 | if (x0 == x1 && y0 == y1) break 118 | let e2 = 2 * err 119 | if (e2 >= dy) { 120 | err += dy 121 | x0 += sx 122 | } 123 | if (e2 <= dx) { 124 | err += dx 125 | y0 += sy 126 | } 127 | } 128 | return points 129 | } 130 | ``` -------------------------------------------------------------------------------- /src/modules/buffer.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | @module buffer.js 3 | @desc Safe buffer helpers, mostly for internal use 4 | @category internal 5 | 6 | Safe set() and get() functions, rect() and text() ‘drawing’ helpers. 7 | 8 | Buffers are 1D arrays for 2D data, a ‘width’ and a 'height' parameter 9 | have to be known (and passed to the functions) to correctly / safely access 10 | the array. 11 | 12 | const v = get(10, 10, buffer, cols, rows) 13 | 14 | */ 15 | 16 | // Safe get function to read from a buffer 17 | export function get(x, y, target, targetCols, targetRows) { 18 | if (x < 0 || x >= targetCols) return {} 19 | if (y < 0 || y >= targetRows) return {} 20 | const i = x + y * targetCols 21 | return target[i] 22 | } 23 | 24 | // Safe set and merge functions for a generic buffer object. 25 | // A buffer object contains at least a 'state' array 26 | // and a 'width' and a 'height' field to allow easy setting. 27 | // The value to be set is a single character or a 'cell' object like: 28 | // { char, color, backgroundColor, fontWeight } 29 | // which can overwrite the buffer (set) or partially merged (merge) 30 | export function set(val, x, y, target, targetCols, targetRows) { 31 | if (x < 0 || x >= targetCols) return 32 | if (y < 0 || y >= targetRows) return 33 | const i = x + y * targetCols 34 | target[i] = val 35 | } 36 | 37 | export function merge(val, x, y, target, targetCols, targetRows) { 38 | if (x < 0 || x >= targetCols) return 39 | if (y < 0 || y >= targetRows) return 40 | const i = x + y * targetCols 41 | 42 | // Flatten: 43 | const cell = typeof target[i] == 'object' ? target[i] : { char : target[i] } 44 | 45 | target[i] = { ...cell, ...val } 46 | } 47 | 48 | export function setRect(val, x, y, w, h, target, targetCols, targetRows) { 49 | for (let j=y; j<y+h; j++ ) { 50 | for (let i=x; i<x+w; i++ ) { 51 | set(val, i, j, target, targetCols, targetRows) 52 | } 53 | } 54 | } 55 | 56 | export function mergeRect(val, x, y, w, h, target, targetCols, targetRows) { 57 | for (let j=y; j<y+h; j++ ) { 58 | for (let i=x; i<x+w; i++ ) { 59 | merge(val, i, j, target, targetCols, targetRows) 60 | } 61 | } 62 | } 63 | 64 | // Merges a textObj in the form of: 65 | // { 66 | // text : 'abc\ndef', 67 | // color : 'red', 68 | // fontWeight : '400', 69 | // backgroundColor : 'black', 70 | // etc... 71 | // } 72 | // or just as a string into the target buffer. 73 | export function mergeText(textObj, x, y, target, targetCols, targetRows) { 74 | let text, mergeObj 75 | // An object has been passed as argument, expect a 'text' field 76 | if (typeof textObj == "object") { 77 | text = textObj.text 78 | // Extract all the fields to be merged... 79 | mergeObj = {...textObj} 80 | // ...but emove text field 81 | delete mergeObj.text 82 | } 83 | // A string has been passed as argument 84 | else { 85 | text = textObj 86 | } 87 | 88 | let col = x 89 | let row = y 90 | // Hackish and inefficient way to retain info of the first and last 91 | // character of each line merged into the matrix. 92 | // Can be useful to wrap with markup. 93 | const wrapInfo = [] 94 | 95 | text.split('\n').forEach((line, lineNum) => { 96 | line.split('').forEach((char, charNum) => { 97 | col = x + charNum 98 | merge({char, ...mergeObj}, col, row, target, targetCols, targetRows) 99 | }) 100 | const first = get(x, row, target, targetCols, targetRows) 101 | const last = get(x+line.length-1, row, target, targetCols, targetRows) 102 | wrapInfo.push({first, last}) 103 | row++ 104 | }) 105 | 106 | // Adjust for last ++ 107 | row = Math.max(y, row-1) 108 | 109 | // Returns some info about the inserted text: 110 | // - the coordinates (offset) of the last inserted character 111 | // - the first an last chars of each line (wrapInfo) 112 | return { 113 | offset : {col, row}, 114 | // first : wrapInfo[0].first, 115 | // last : wrapInfo[wrapInfo.length-1].last, 116 | wrapInfo 117 | } 118 | } 119 | ``` -------------------------------------------------------------------------------- /src/core/canvasrenderer.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | @module canvasrenderer.js 3 | @desc renders to canvas 4 | @category renderer 5 | */ 6 | 7 | export default { 8 | preferredElementNodeName : 'CANVAS', 9 | render 10 | } 11 | 12 | function render(context, buffer) { 13 | 14 | const canvas = context.settings.element 15 | 16 | const scale = devicePixelRatio 17 | 18 | const c = context.cols 19 | const r = context.rows 20 | const m = context.metrics 21 | 22 | const cw = m.cellWidth 23 | const ch = Math.round(m.lineHeight) 24 | 25 | // Shortcut 26 | const settings = context.settings 27 | 28 | // Fixed size, to allow precise export 29 | if (settings.canvasSize) { 30 | canvas.width = settings.canvasSize.width * scale 31 | canvas.height = settings.canvasSize.height * scale 32 | canvas.style.width = settings.canvasSize.width + 'px' 33 | canvas.style.height = settings.canvasSize.height + 'px' 34 | } 35 | // Stretch the canvas to the container width 36 | else { 37 | canvas.width = context.width * scale 38 | canvas.height = context.height * scale 39 | } 40 | 41 | const ff = ' ' + m.fontSize + 'px ' + m.fontFamily 42 | const bg = settings && settings.backgroundColor ? settings.backgroundColor : 'white' 43 | const fg = settings && settings.color ? settings.color : 'black' 44 | const fontWeight = settings && settings.fontWeight ? settings.color : '400' 45 | 46 | // Set the backgroundColor of the box-element 47 | // canvas.style.backgroundColor = settings.backgroundColor || 'white' 48 | 49 | // Transparent canvas backround for the remaining size 50 | // (fractions of cellWidth and lineHeight). 51 | // Only the 'rendered' area has a solid color. 52 | const ctx = canvas.getContext('2d') 53 | //ctx.clearRect(0, 0, canvas.width, canvas.height) 54 | ctx.fillStyle = bg 55 | ctx.fillRect(0, 0, canvas.width, canvas.height) 56 | 57 | ctx.save() 58 | ctx.scale(scale, scale) 59 | ctx.fillStyle = fg 60 | ctx.textBaseline = 'top' 61 | 62 | // Custom settings: it’s possible to center the canvas 63 | if (settings.canvasOffset) { 64 | const offs = settings.canvasOffset 65 | const ox = Math.round(offs.x == 'auto' ? (canvas.width / scale - c * cw) / 2 : offs.x) 66 | const oy = Math.round(offs.y == 'auto' ? (canvas.height / scale - r * ch) / 2 : offs.y) 67 | ctx.translate(ox, oy) 68 | } 69 | 70 | // Center patch with cell bg color... 71 | // a bit painful and needs some opt. 72 | if( settings.textAlign == 'center' ) { 73 | 74 | for (let j=0; j<r; j++) { 75 | const offs = j * c 76 | const widths = [] 77 | const offsets = [] 78 | const colors = [] 79 | let totalWidth = 0 80 | 81 | // Find width 82 | for (let i=0; i<c; i++) { 83 | const cell = buffer[offs + i] 84 | ctx.font = (cell.fontWeight || fontWeight) + ff 85 | const w = ctx.measureText(cell.char).width 86 | totalWidth += w 87 | widths[i] = w 88 | } 89 | // Draw 90 | let ox = (canvas.width / scale - totalWidth) * 0.5 91 | const y = j * ch 92 | for (let i=0; i<c; i++) { 93 | const cell = buffer[offs + i] 94 | const x = ox 95 | if (cell.backgroundColor && cell.backgroundColor != bg) { 96 | ctx.fillStyle = cell.backgroundColor || bg 97 | ctx.fillRect(Math.round(x), y, Math.ceil(widths[i]), ch) 98 | } 99 | ctx.font = (cell.fontWeight || fontWeight) + ff 100 | ctx.fillStyle = cell.color || fg 101 | ctx.fillText(cell.char, ox, y) 102 | 103 | ox += widths[i] 104 | } 105 | } 106 | 107 | // (Default) block mode 108 | } else { 109 | for (let j=0; j<r; j++) { 110 | for (let i=0; i<c; i++) { 111 | const cell = buffer[j * c + i] 112 | const x = i * cw 113 | const y = j * ch 114 | if (cell.backgroundColor && cell.backgroundColor != bg) { 115 | ctx.fillStyle = cell.backgroundColor || bg 116 | ctx.fillRect(Math.round(x), y, Math.ceil(cw), ch) 117 | } 118 | ctx.font = (cell.fontWeight || fontWeight) + ff 119 | ctx.fillStyle = cell.color || fg 120 | ctx.fillText(cell.char, x, y) 121 | } 122 | } 123 | } 124 | ctx.restore() 125 | } 126 | ``` -------------------------------------------------------------------------------- /src/programs/demos/doom_flame.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Doom Flame 5 | @desc Oldschool flame effect 6 | */ 7 | 8 | import { clamp, map } from '/src/modules/num.js' 9 | import { CSS4 } from '/src/modules/color.js' 10 | import { mix, smoothstep } from '/src/modules/num.js' 11 | 12 | export const settings = { fps : 30, backgroundColor : 'black', color : 'white' } 13 | 14 | const { min, max, sin, floor } = Math 15 | 16 | const flame = '...::/\\/\\/\\+=*abcdef01XYZ#' 17 | let cols, rows 18 | 19 | const noise = valueNoise() 20 | 21 | const data = [] 22 | 23 | export function pre(context, cursor, buffer) { 24 | 25 | 26 | // Detect resize (and reset buffer, in case) 27 | if (cols != context.cols || rows != context.rows) { 28 | cols = context.cols 29 | rows = context.rows 30 | data.length = cols * rows // Don't loose reference 31 | data.fill(0) 32 | } 33 | 34 | // Fill the floor with some noise 35 | if (!cursor.pressed) { 36 | const t = context.time * 0.0015 37 | const last = cols * (rows - 1) 38 | for (let i=0; i<cols; i++) { 39 | const val = floor(map(noise(i * 0.05, t), 0, 1, 5, 40)) 40 | data[last + i] = min(val, data[last + i] + 2) 41 | } 42 | } else { 43 | const cx = floor(cursor.x) 44 | const cy = floor(cursor.y) 45 | data[cx + cy * cols] = rndi(5, 50) 46 | } 47 | 48 | // Propagate towards the ceiling with some randomness 49 | for (let i=0; i<data.length; i++) { 50 | const row = floor(i / cols) 51 | const col = i % cols 52 | const dest = row * cols + clamp(col + rndi(-1, 1), 0, cols-1) 53 | const src = min(rows-1, row + 1) * cols + col 54 | data[dest] = max(0, data[src]-rndi(0, 2)) 55 | } 56 | } 57 | 58 | export function main(coord, context, cursor, buffer) { 59 | const u = data[coord.index] 60 | if (u === 0) return // Inserts a space 61 | 62 | return { 63 | char : flame[clamp(u, 0, flame.length-1)], 64 | fontWeight : u > 20 ? 700 : 100 65 | } 66 | } 67 | 68 | // Random int betweem a and b, inclusive! 69 | function rndi(a, b=0) { 70 | if (a > b) [a, b] = [b, a] 71 | return Math.floor(a + Math.random() * (b - a + 1)) 72 | } 73 | 74 | // Value noise: 75 | // https://www.scratchapixel.com/lessons/procedural-generation-virtual-worlds/procedural-patterns-noise-part-1 76 | function valueNoise() { 77 | 78 | const tableSize = 256; 79 | const r = new Array(tableSize) 80 | const permutationTable = new Array(tableSize * 2) 81 | 82 | // Create an array of random values and initialize permutation table 83 | for (let k=0; k<tableSize; k++) { 84 | r[k] = Math.random() 85 | permutationTable[k] = k 86 | } 87 | 88 | // Shuffle values of the permutation table 89 | for (let k=0; k<tableSize; k++) { 90 | const i = Math.floor(Math.random() * tableSize) 91 | // swap 92 | ;[permutationTable[k], permutationTable[i]] = [permutationTable[i], permutationTable[k]] 93 | permutationTable[k + tableSize] = permutationTable[k] 94 | } 95 | 96 | return function(px, py) { 97 | const xi = Math.floor(px) 98 | const yi = Math.floor(py) 99 | 100 | const tx = px - xi 101 | const ty = py - yi 102 | 103 | const rx0 = xi % tableSize 104 | const rx1 = (rx0 + 1) % tableSize 105 | const ry0 = yi % tableSize 106 | const ry1 = (ry0 + 1) % tableSize 107 | 108 | // Random values at the corners of the cell using permutation table 109 | const c00 = r[permutationTable[permutationTable[rx0] + ry0]] 110 | const c10 = r[permutationTable[permutationTable[rx1] + ry0]] 111 | const c01 = r[permutationTable[permutationTable[rx0] + ry1]] 112 | const c11 = r[permutationTable[permutationTable[rx1] + ry1]] 113 | 114 | // Remapping of tx and ty using the Smoothstep function 115 | const sx = smoothstep(0, 1, tx); 116 | const sy = smoothstep(0, 1, ty); 117 | 118 | // Linearly interpolate values along the x axis 119 | const nx0 = mix(c00, c10, sx) 120 | const nx1 = mix(c01, c11, sx) 121 | 122 | // Linearly interpolate the nx0/nx1 along they y axis 123 | return mix(nx0, nx1, sy) 124 | } 125 | } 126 | 127 | import { drawInfo } from '/src/modules/drawbox.js' 128 | export function post(context, cursor, buffer) { 129 | drawInfo(context, cursor, buffer) 130 | } 131 | ``` -------------------------------------------------------------------------------- /src/programs/demos/gol_double_res.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Golgol 5 | @desc Double resolution version of GOL 6 | 7 | Based on Alex Miller’s version of Game of Life: 8 | https://play.ertdfgcvb.xyz/#/src/contributed/game_of_life 9 | 10 | By using three box chars (plus space) each char can host 11 | two vertical cells of the automata allowing a double resolution. 12 | '█' both cells are occupied 13 | ' ' both cells are empty 14 | '▀' upper cell is occupied 15 | '▄' lower cell is occupied 16 | 17 | All the automata state is stored in the custom 'data' buffer. 18 | Each frame of the animation depends on the previous frame, 19 | so in this case the 'data' buffer is two arrays (see initialization in pre). 20 | */ 21 | 22 | // Safe set function 23 | function set(val, x, y, w, h, buf) { 24 | if (x < 0 || x >= w) return 25 | if (y < 0 || y >= h) return 26 | buf[y * w + x] = val 27 | } 28 | 29 | // Safe get function 30 | function get(x, y, w, h, buf) { 31 | if (x < 0 || x >= w) return 0 32 | if (y < 0 || y >= h) return 0 33 | return buf[y * w + x] 34 | } 35 | 36 | // Some state to detect window resize / init 37 | let cols, rows 38 | 39 | const data = [] 40 | 41 | // The automata is computed in a single step and stored in the 'data' buffer 42 | export function pre(context, cursor, buffer) { 43 | 44 | // The window has been resized (or “init”), reset the buffer: 45 | if (cols != context.cols || rows != context.rows) { 46 | cols = context.cols 47 | rows = context.rows 48 | const len = context.cols * context.rows * 2 // double height 49 | 50 | // We need two buffer (store them in the user 'data' array) 51 | data[0] = [] 52 | data[1] = [] 53 | // Initialize with some random state 54 | for (let i=0; i<len; i++) { 55 | const v = Math.random() > 0.5 ? 1 : 0 56 | data[0][i] = v 57 | data[1][i] = v 58 | } 59 | } 60 | 61 | // Update the buffer 62 | const prev = data[ context.frame % 2] 63 | const curr = data[(context.frame + 1) % 2] 64 | const w = cols 65 | const h = rows * 2 66 | 67 | // Fill a random rect of 10x10 68 | if (cursor.pressed) { 69 | const cx = Math.floor(cursor.x) // cursor has float values 70 | const cy = Math.floor(cursor.y * 2) 71 | const s = 5 72 | for (let y=cy-s; y<=cy+s; y++) { 73 | for (let x=cx-s; x<=cx+s; x++) { 74 | const val = Math.random() < 0.5 ? 1 : 0 75 | set(val, x, y, w, h, prev) 76 | } 77 | } 78 | } 79 | 80 | // Update the automata 81 | for (let y=0; y<h; y++) { 82 | for (let x=0; x<w; x++) { 83 | const current = get(x, y, w, h, prev) 84 | const neighbors = 85 | get(x - 1, y - 1, w, h, prev) + 86 | get(x, y - 1, w, h, prev) + 87 | get(x + 1, y - 1, w, h, prev) + 88 | get(x - 1, y, w, h, prev) + 89 | get(x + 1, y, w, h, prev) + 90 | get(x - 1, y + 1, w, h, prev) + 91 | get(x, y + 1, w, h, prev) + 92 | get(x + 1, y + 1, w, h, prev) 93 | // Update 94 | const i = x + y * w 95 | if (current == 1) { 96 | curr[i] = neighbors == 2 || neighbors == 3 ? 1 : 0 97 | } else { 98 | curr[i] = neighbors == 3 ? 1 : 0 99 | } 100 | } 101 | } 102 | } 103 | 104 | // Just a renderer 105 | export function main(coord, context, cursor, buffer) { 106 | // Current buffer 107 | const curr = data[(context.frame + 1) % 2] 108 | 109 | // Upper and lower half 110 | const idx = coord.x + coord.y * 2 * context.cols 111 | const upper = curr[idx] 112 | const lower = curr[idx + context.cols] 113 | 114 | if (upper && lower) return '█' // both cells are occupied 115 | if (upper) return '▀' // upper cell 116 | if (lower) return '▄' // lower cell 117 | return ' ' // both cells are empty 118 | } 119 | 120 | // Draw some info 121 | import { drawBox } from '/src/modules/drawbox.js' 122 | export function post(context, cursor, buffer) { 123 | 124 | const buff = data[(context.frame + 1) % 2] 125 | const numCells = buff.reduce((a, b) => a + b, 0) 126 | 127 | let text = '' 128 | text += 'frame ' + context.frame + '\n' 129 | text += 'size ' + context.cols + '×' + context.rows + '\n' 130 | text += 'cells ' + numCells + '/' + buff.length + '\n' 131 | 132 | drawBox(text, { 133 | backgroundColor : 'tomato', 134 | color : 'black', 135 | borderStyle : 'double', 136 | shadowStyle : 'gray' 137 | }, buffer) 138 | } 139 | 140 | ``` -------------------------------------------------------------------------------- /src/programs/demos/doom_flame_full_color.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | [header] 3 | @author ertdfgcvb 4 | @title Doom Flame (full color) 5 | @desc Oldschool flame effect 6 | */ 7 | 8 | import { clamp, map } from '/src/modules/num.js' 9 | import { mix, smoothstep } from '/src/modules/num.js' 10 | 11 | export const settings = { backgroundColor : 'black' } 12 | 13 | const { min, max, sin, floor } = Math 14 | 15 | const palette = [ 16 | 'black', // 0 < top 17 | 'purple', // 1 18 | 'darkred', // 2 19 | 'red', // 3 20 | 'orangered', // 4 21 | 'gold', // 5 22 | 'lemonchiffon', // 6 23 | 'white' // 7 < bottom 24 | ] 25 | 26 | // top bottom 27 | // v v 28 | const flame = '011222233334444444455566667'.split('').map(Number) 29 | 30 | const noise = valueNoise() 31 | 32 | let cols, rows 33 | 34 | const data = [] 35 | 36 | export function pre(context, cursor, buffer) { 37 | 38 | // Detect resize (and reset buffer, in case) 39 | if (cols != context.cols || rows != context.rows) { 40 | cols = context.cols 41 | rows = context.rows 42 | data.length = cols * rows // Don't loose reference 43 | data.fill(0) 44 | } 45 | 46 | // Fill the floor with some noise 47 | if (!cursor.pressed) { 48 | const t = context.time * 0.0015 49 | const last = cols * (rows - 1) 50 | for (let i=0; i<cols; i++) { 51 | const val = floor(map(noise(i * 0.05, t), 0, 1, 5, 50)) 52 | data[last + i] = min(val, data[last + i] + 2) 53 | } 54 | } else { 55 | const cx = floor(cursor.x) 56 | const cy = floor(cursor.y) 57 | data[cx + cy * cols] = rndi(5, 50) 58 | } 59 | 60 | // Propagate towards the ceiling with some randomness 61 | for (let i=0; i<data.length; i++) { 62 | const row = floor(i / cols) 63 | const col = i % cols 64 | const dest = row * cols + clamp(col + rndi(-1, 1), 0, cols-1) 65 | const src = min(rows-1, row + 1) * cols + col 66 | data[dest] = max(0, data[src]-rndi(0, 2)) 67 | } 68 | } 69 | 70 | export function main(coord, context, cursor, buffer) { 71 | 72 | const u = data[coord.index] 73 | const v = flame[clamp(u, 0, flame.length-1)] 74 | 75 | if (v === 0) return { 76 | char : ' ', 77 | backgroundColor : 'black' 78 | } 79 | 80 | return { 81 | char : u % 10, 82 | color : palette[min(palette.length-1,v+1)], 83 | backgroundColor : palette[v] 84 | } 85 | } 86 | 87 | // Random int betweem a and b, inclusive! 88 | function rndi(a, b=0) { 89 | if (a > b) [a, b] = [b, a] 90 | return Math.floor(a + Math.random() * (b - a + 1)) 91 | } 92 | 93 | // Value noise: 94 | // https://www.scratchapixel.com/lessons/procedural-generation-virtual-worlds/procedural-patterns-noise-part-1 95 | function valueNoise() { 96 | 97 | const tableSize = 256; 98 | const r = new Array(tableSize) 99 | const permutationTable = new Array(tableSize * 2) 100 | 101 | // Create an array of random values and initialize permutation table 102 | for (let k=0; k<tableSize; k++) { 103 | r[k] = Math.random() 104 | permutationTable[k] = k 105 | } 106 | 107 | // Shuffle values of the permutation table 108 | for (let k=0; k<tableSize; k++) { 109 | const i = Math.floor(Math.random() * tableSize) 110 | // swap 111 | ;[permutationTable[k], permutationTable[i]] = [permutationTable[i], permutationTable[k]] 112 | permutationTable[k + tableSize] = permutationTable[k] 113 | } 114 | 115 | return function(px, py) { 116 | const xi = Math.floor(px) 117 | const yi = Math.floor(py) 118 | 119 | const tx = px - xi 120 | const ty = py - yi 121 | 122 | const rx0 = xi % tableSize 123 | const rx1 = (rx0 + 1) % tableSize 124 | const ry0 = yi % tableSize 125 | const ry1 = (ry0 + 1) % tableSize 126 | 127 | // Random values at the corners of the cell using permutation table 128 | const c00 = r[permutationTable[permutationTable[rx0] + ry0]] 129 | const c10 = r[permutationTable[permutationTable[rx1] + ry0]] 130 | const c01 = r[permutationTable[permutationTable[rx0] + ry1]] 131 | const c11 = r[permutationTable[permutationTable[rx1] + ry1]] 132 | 133 | // Remapping of tx and ty using the Smoothstep function 134 | const sx = smoothstep(0, 1, tx); 135 | const sy = smoothstep(0, 1, ty); 136 | 137 | // Linearly interpolate values along the x axis 138 | const nx0 = mix(c00, c10, sx) 139 | const nx1 = mix(c01, c11, sx) 140 | 141 | // Linearly interpolate the nx0/nx1 along they y axis 142 | return mix(nx0, nx1, sy) 143 | } 144 | } 145 | 146 | import { drawInfo } from '/src/modules/drawbox.js' 147 | export function post(context, cursor, buffer) { 148 | drawInfo(context, cursor, buffer) 149 | } 150 | ``` -------------------------------------------------------------------------------- /src/modules/vec2.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | @module vec2.js 3 | @desc 2D vector helper functions 4 | @category public 5 | 6 | - No vector class (a 'vector' is just any object with {x, y}) 7 | - The functions never modify the original object. 8 | - An optional destination object can be passed as last paremeter to all 9 | the functions (except vec2()). 10 | - All function can be exported individually or grouped via default export. 11 | - For the default export use: 12 | import * as Vec2 from '/src/modules/vec2.js' 13 | */ 14 | 15 | // Creates a vector 16 | export function vec2(x, y) { 17 | return {x, y} 18 | } 19 | 20 | // Copies a vector 21 | export function copy(a, out) { 22 | out = out || vec2(0, 0) 23 | 24 | out.x = a.x 25 | out.y = a.y 26 | 27 | return out 28 | } 29 | 30 | // Adds two vectors 31 | export function add(a, b, out) { 32 | out = out || vec2(0, 0) 33 | 34 | out.x = a.x + b.x 35 | out.y = a.y + b.y 36 | 37 | return out 38 | } 39 | 40 | // Subtracts two vectors 41 | export function sub(a, b, out) { 42 | out = out || vec2(0, 0) 43 | 44 | out.x = a.x - b.x 45 | out.y = a.y - b.y 46 | 47 | return out 48 | } 49 | 50 | // Multiplies a vector by another vector (component-wise) 51 | export function mul(a, b, out) { 52 | out = out || vec2(0, 0) 53 | 54 | out.x = a.x * b.x 55 | out.y = a.y * b.y 56 | 57 | return out 58 | } 59 | 60 | // Divides a vector by another vector (component-wise) 61 | export function div(a, b, out) { 62 | out = out || vec2(0, 0) 63 | 64 | out.x = a.x / b.x 65 | out.y = a.y / b.y 66 | 67 | return out 68 | } 69 | 70 | // Adds a scalar to a vector 71 | export function addN(a, k, out) { 72 | out = out || vec2(0, 0) 73 | 74 | out.x = a.x + k 75 | out.y = a.y + k 76 | 77 | return out 78 | } 79 | 80 | // Subtracts a scalar from a vector 81 | export function subN(a, k, out) { 82 | out = out || vec2(0, 0) 83 | 84 | out.x = a.x - k 85 | out.y = a.y - k 86 | 87 | return out 88 | } 89 | 90 | // Mutiplies a vector by a scalar 91 | export function mulN(a, k, out) { 92 | out = out || vec2(0, 0) 93 | 94 | out.x = a.x * k 95 | out.y = a.y * k 96 | 97 | return out 98 | } 99 | 100 | // Divides a vector by a scalar 101 | export function divN(a, k, out) { 102 | out = out || vec2(0, 0) 103 | 104 | out.x = a.x / k 105 | out.y = a.y / k 106 | 107 | return out 108 | } 109 | 110 | // Computes the dot product of two vectors 111 | export function dot(a, b) { 112 | return a.x * b.x + a.y * b.y 113 | } 114 | 115 | // Computes the length of vector 116 | export function length(a) { 117 | return Math.sqrt(a.x * a.x + a.y * a.y) 118 | } 119 | 120 | // Computes the square of the length of vector 121 | export function lengthSq(a) { 122 | return a.x * a.x + a.y * a.y 123 | } 124 | 125 | // Computes the distance between 2 points 126 | export function dist(a, b) { 127 | const dx = a.x - b.x 128 | const dy = a.y - b.y 129 | 130 | return Math.sqrt(dx * dx + dy * dy) 131 | } 132 | 133 | // Computes the square of the distance between 2 points 134 | export function distSq(a, b) { 135 | const dx = a.x - b.x 136 | const dy = a.y - b.y 137 | 138 | return dx * dx + dy * dy 139 | } 140 | 141 | // Divides a vector by its Euclidean length and returns the quotient 142 | export function norm(a, out) { 143 | out = out || vec2(0, 0) 144 | 145 | const l = length(a) 146 | if (l > 0.00001) { 147 | out.x = a.x / l 148 | out.y = a.y / l 149 | } else { 150 | out.x = 0 151 | out.y = 0 152 | } 153 | 154 | return out 155 | } 156 | 157 | // Negates a vector 158 | export function neg(v, out) { 159 | out = out || vec2(0, 0) 160 | 161 | out.x = -a.x 162 | out.y = -a.y 163 | 164 | return out 165 | } 166 | 167 | // Rotates a vector 168 | export function rot(a, ang, out) { 169 | out = out || vec2(0, 0) 170 | 171 | const s = Math.sin(ang) 172 | const c = Math.cos(ang) 173 | 174 | out.x = a.x * c - a.y * s, 175 | out.y = a.x * s + a.y * c 176 | 177 | return out 178 | } 179 | 180 | // Performs linear interpolation on two vectors 181 | export function mix(a, b, t, out) { 182 | out = out || vec2(0, 0) 183 | 184 | out.x = (1 - t) * a.x + t * b.x 185 | out.y = (1 - t) * a.y + t * b.y 186 | 187 | return out 188 | } 189 | 190 | // Computes the abs of a vector (component-wise) 191 | export function abs(a, out) { 192 | out = out || vec2(0, 0) 193 | 194 | out.x = Math.abs(a.x) 195 | out.y = Math.abs(a.y) 196 | 197 | return out 198 | } 199 | 200 | // Computes the max of two vectors (component-wise) 201 | export function max(a, b, out) { 202 | out = out || vec2(0, 0) 203 | 204 | out.x = Math.max(a.x, b.x) 205 | out.y = Math.max(a.y, b.y) 206 | 207 | return out 208 | } 209 | 210 | // Computes the min of two vectors (component-wise) 211 | export function min(a, b, out) { 212 | out = out || vec2(0, 0) 213 | 214 | out.x = Math.min(a.x, b.x) 215 | out.y = Math.min(a.y, b.y) 216 | 217 | return out 218 | } 219 | 220 | // Returns the fractional part of the vector (component-wise) 221 | export function fract(a, out) { 222 | out = out || vec2(0, 0) 223 | out.x = a.x - Math.floor(a.x) 224 | out.y = a.y - Math.floor(a.y) 225 | return out 226 | } 227 | 228 | // Returns the floored vector (component-wise) 229 | export function floor(a, out) { 230 | out = out || vec2(0, 0) 231 | out.x = Math.floor(a.x) 232 | out.y = Math.floor(a.y) 233 | return out 234 | } 235 | 236 | // Returns the ceiled vector (component-wise) 237 | export function ceil(a, out) { 238 | out = out || vec2(0, 0) 239 | out.x = Math.ceil(a.x) 240 | out.y = Math.ceil(a.y) 241 | return out 242 | } 243 | 244 | // Returns the rounded vector (component-wise) 245 | export function round(a, out) { 246 | out = out || vec2(0, 0) 247 | out.x = Math.round(a.x) 248 | out.y = Math.round(a.y) 249 | return out 250 | } 251 | 252 | 253 | ``` -------------------------------------------------------------------------------- /src/core/textrenderer.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | @module textrenderer.js 3 | @desc renders to a text element 4 | @category renderer 5 | */ 6 | 7 | export default { 8 | preferredElementNodeName : 'PRE', 9 | render 10 | } 11 | 12 | const backBuffer = [] 13 | 14 | let cols, rows 15 | 16 | function render(context, buffer) { 17 | 18 | const element = context.settings.element 19 | 20 | // Set the most used styles to the container 21 | // element.style.backgroundColor = context.settings.background 22 | // element.style.color = context.settings.color 23 | // element.style.fontWeight = context.settings.weight 24 | 25 | // Detect resize 26 | if (context.rows != rows || context.cols != cols) { 27 | cols = context.cols 28 | rows = context.rows 29 | backBuffer.length = 0 30 | } 31 | 32 | // DOM rows update: expand lines if necessary 33 | // TODO: also benchmark a complete 'innerHTML' rewrite, could be faster? 34 | while(element.childElementCount < rows) { 35 | const span = document.createElement('span') 36 | span.style.display = 'block' 37 | element.appendChild(span) 38 | } 39 | 40 | // DOM rows update: shorten lines if necessary 41 | // https://jsperf.com/innerhtml-vs-removechild/15 42 | while(element.childElementCount > rows) { 43 | element.removeChild(element.lastChild) 44 | } 45 | 46 | // Counts the number of updated rows, seful for debug 47 | let updatedRowNum = 0 48 | 49 | // A bit of a cumbersome render-loop… 50 | // A few notes: the fastest way I found to render the image 51 | // is by manually write the markup into the parent node via .innerHTML; 52 | // creating a node via .createElement and then popluate it resulted 53 | // remarkably slower (even if more elegant for the CSS handling below). 54 | for (let j=0; j<rows; j++) { 55 | 56 | const offs = j * cols 57 | 58 | // This check is faster than to force update the DOM. 59 | // Buffer can be manually modified in pre, main and after 60 | // with semi-arbitrary values… 61 | // It is necessary to keep track of the previous state 62 | // and specifically check if a change in style 63 | // or char happened on the whole row. 64 | let rowNeedsUpdate = false 65 | for (let i=0; i<cols; i++) { 66 | const idx = i + offs 67 | const newCell = buffer[idx] 68 | const oldCell = backBuffer[idx] 69 | if (!isSameCell(newCell, oldCell)) { 70 | if (rowNeedsUpdate == false) updatedRowNum++ 71 | rowNeedsUpdate = true 72 | backBuffer[idx] = {...newCell} 73 | } 74 | } 75 | 76 | // Skip row if update is not necessary 77 | if (rowNeedsUpdate == false) continue 78 | 79 | let html = '' // Accumulates the markup 80 | let prevCell = {} //defaultCell 81 | let tagIsOpen = false 82 | for (let i=0; i<cols; i++) { 83 | const currCell = buffer[i + offs] //|| {...defaultCell, char : EMPTY_CELL} 84 | // Undocumented feature: 85 | // possible to inject some custom HTML (for example <a>) into the renderer. 86 | // It can be inserted before the char or after the char (beginHTML, endHTML) 87 | // and this is a very hack… 88 | if (currCell.beginHTML) { 89 | if (tagIsOpen) { 90 | html += '</span>' 91 | prevCell = {} //defaultCell 92 | tagIsOpen = false 93 | } 94 | html += currCell.beginHTML 95 | } 96 | 97 | // If there is a change in style a new span has to be inserted 98 | if (!isSameCellStyle(currCell, prevCell)) { 99 | // Close the previous tag 100 | if (tagIsOpen) html += '</span>' 101 | 102 | const c = currCell.color === context.settings.color ? null : currCell.color 103 | const b = currCell.backgroundColor === context.settings.backgroundColor ? null : currCell.backgroundColor 104 | const w = currCell.fontWeight === context.settings.fontWeight ? null : currCell.fontWeight 105 | 106 | // Accumulate the CSS inline attribute. 107 | let css = '' 108 | if (c) css += 'color:' + c + ';' 109 | if (b) css += 'background:' + b + ';' 110 | if (w) css += 'font-weight:' + w + ';' 111 | if (css) css = ' style="' + css + '"' 112 | html += '<span' + css + '>' 113 | tagIsOpen = true 114 | } 115 | html += currCell.char 116 | prevCell = currCell 117 | 118 | // Add closing tag, in case 119 | if (currCell.endHTML) { 120 | if (tagIsOpen) { 121 | html += '</span>' 122 | prevCell = {} //defaultCell 123 | tagIsOpen = false 124 | } 125 | html += currCell.endHTML 126 | } 127 | 128 | } 129 | if (tagIsOpen) html += '</span>' 130 | 131 | // Write the row 132 | element.childNodes[j].innerHTML = html 133 | } 134 | } 135 | 136 | // Compares two cells 137 | function isSameCell(cellA, cellB) { 138 | if (typeof cellA != 'object') return false 139 | if (typeof cellB != 'object') return false 140 | if (cellA.char !== cellB.char) return false 141 | if (cellA.fontWeight !== cellB.fontWeight) return false 142 | if (cellA.color !== cellB.color) return false 143 | if (cellA.backgroundColor !== cellB.backgroundColor) return false 144 | return true 145 | } 146 | 147 | // Compares two cells for style only 148 | function isSameCellStyle(cellA, cellB) { 149 | if (cellA.fontWeight !== cellB.fontWeight) return false 150 | if (cellA.color !== cellB.color) return false 151 | if (cellA.backgroundColor !== cellB.backgroundColor) return false 152 | return true 153 | } 154 | ```