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