#
tokens: 46639/50000 79/81 files (page 1/2)
lines: off (toggle) GitHub
raw markdown copy
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
}

```
Page 1/2FirstPrevNextLast