#
tokens: 48751/50000 74/81 files (page 1/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 2. Use http://codebase.md/ertdfgcvb/play.core?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitignore
├── LICENSE
├── README.md
├── src
│   ├── core
│   │   ├── canvasrenderer.js
│   │   ├── fps.js
│   │   ├── storage.js
│   │   ├── textrenderer.js
│   │   └── version.js
│   ├── makefile
│   ├── modules
│   │   ├── buffer.js
│   │   ├── camera.js
│   │   ├── canvas.js
│   │   ├── color.js
│   │   ├── drawbox.js
│   │   ├── exportframe.js
│   │   ├── filedownload.js
│   │   ├── image.js
│   │   ├── list.sh
│   │   ├── load.js
│   │   ├── num.js
│   │   ├── sdf.js
│   │   ├── sort.js
│   │   ├── string.js
│   │   ├── vec2.js
│   │   └── vec3.js
│   ├── programs
│   │   ├── addheader.sh
│   │   ├── basics
│   │   │   ├── 10print.js
│   │   │   ├── coordinates_index.js
│   │   │   ├── coordinates_xy.js
│   │   │   ├── cursor.js
│   │   │   ├── how_to_draw_a_circle.js
│   │   │   ├── how_to_draw_a_square.js
│   │   │   ├── how_to_log.js
│   │   │   ├── name_game.js
│   │   │   ├── performance_test.js
│   │   │   ├── rendering_to_canvas.js
│   │   │   ├── sequence_export.js
│   │   │   ├── simple_output.js
│   │   │   ├── time_frames.js
│   │   │   └── time_milliseconds.js
│   │   ├── camera
│   │   │   ├── camera_double_res.js
│   │   │   ├── camera_gray.js
│   │   │   └── camera_rgb.js
│   │   ├── contributed
│   │   │   ├── color_waves.js
│   │   │   ├── emoji_wave.js
│   │   │   ├── equal_tea_talk.js
│   │   │   ├── ernst.js
│   │   │   ├── game_of_life.js
│   │   │   ├── pathfinder.js
│   │   │   ├── sand_game.js
│   │   │   ├── slime_dish.js
│   │   │   └── stacked_sin_waves.js
│   │   ├── demos
│   │   │   ├── box_fun.js
│   │   │   ├── chromaspiral.js
│   │   │   ├── donut.js
│   │   │   ├── doom_flame_full_color.js
│   │   │   ├── doom_flame.js
│   │   │   ├── dyna.js
│   │   │   ├── gol_double_res.js
│   │   │   ├── hotlink.js
│   │   │   ├── mod_xor.js
│   │   │   ├── moire_explorer.js
│   │   │   ├── numbers.js
│   │   │   ├── plasma.js
│   │   │   ├── sinsin_checker.js
│   │   │   ├── sinsin_wave.js
│   │   │   ├── spiral.js
│   │   │   └── wobbly.js
│   │   ├── header.old.txt
│   │   ├── list.sh
│   │   └── sdf
│   │       ├── balls.js
│   │       ├── circle.js
│   │       ├── cube.js
│   │       ├── rectangles.js
│   │       └── two_circles.js
│   └── run.js
└── tests
    ├── benchmark.html
    ├── browser_bugs
    │   ├── console_error_bug.html
    │   ├── error_listener_bug.html
    │   └── font_ready_bug.html
    ├── font.html
    ├── multi.html
    ├── promise_chain.html
    ├── proxy_test.html
    └── single.html
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | # macOS
2 | .DS_Store
3 | .AppleDouble
4 | .LSOverride
5 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # play.core
 2 | 
 3 | Core files, example and demos of the live-code ASCII playground:  
 4 | [play.ertdfgcvb.xyz](https://play.ertdfgcvb.xyz)
 5 | 
 6 | Examples and demos:  
 7 | [play.ertdfgcvb.xyz/abc.html#source:examples](https://play.ertdfgcvb.xyz/abc.html#source:examples)
 8 | 
 9 | Embedding examples:  
10 | [single](https://play.ertdfgcvb.xyz/tests/single.html)  
11 | [multi](https://play.ertdfgcvb.xyz/tests/multi.html)  
12 | 
13 | Playground manual, API and resources:  
14 | [play.ertdfgcvb.xyz/abc.html](https://play.ertdfgcvb.xyz/abc.html)
15 | 
```

--------------------------------------------------------------------------------
/src/core/version.js:
--------------------------------------------------------------------------------

```javascript
1 | /**
2 | @module   version.js
3 | @desc     Runner version string
4 | @category core
5 | */
6 | 
7 | export default '1.1'
8 | 
```

--------------------------------------------------------------------------------
/src/programs/basics/simple_output.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Simple output
 5 | @desc   The smallest program possible?
 6 | */
 7 | 
 8 | export function main() {
 9 | 	return '?'
10 | }
11 | 
12 | // Shorter:
13 | // export const main = () => '?'
14 | 
15 | // Even shorter:
16 | // export let main=o=>'?'
17 | 
18 | // Shrtst:
19 | // export let main=o=>0
20 | 
```

--------------------------------------------------------------------------------
/src/core/fps.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | @module   fps
 3 | @desc     Frames-per-second-counter class.
 4 | @category core
 5 | */
 6 | 
 7 | export default class FPS {
 8 | 	constructor() {
 9 | 		this.frames = 0
10 | 		this.ptime = 0
11 | 		this.fps = 0
12 | 	}
13 | 
14 | 	update(time) {
15 | 		this.frames++
16 | 		if (time >= this.ptime + 1000) {
17 | 			this.fps = this.frames * 1000 / (time - this.ptime)
18 | 			this.ptime = time
19 | 			this.frames = 0
20 | 		}
21 | 		return this.fps
22 | 	}
23 | }
24 | 
```

--------------------------------------------------------------------------------
/src/programs/basics/10print.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  10 PRINT
 5 | @desc   10 PRINT CHR$(205.5+RND(1)); : GOTO 10
 6 | See also:
 7 | https://10print.org
 8 | */
 9 | 
10 | // Run the program only once
11 | export const settings = {
12 | 	once : true
13 | }
14 | 
15 | export function main() {
16 | 	// Also try: ╩ ╦ or ▄ ░
17 | 	// or any combination from
18 | 	// https://play.ertdfgcvb.xyz/abc.html#font:characterset
19 | 	return Math.random() < 0.5 ? '╱' : '╲'
20 | }
21 | 
```

--------------------------------------------------------------------------------
/src/programs/basics/coordinates_index.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Coordinates: index
 5 | @desc   Use of coord.index
 6 | */
 7 | 
 8 | // Global variables have scope in the whole module.
 9 | const pattern = '| |.|,|:|;|x|K|Ñ|R|a|+|=|-|_'
10 | // const pattern = '| |▁|▂|▃|▄|▅|▆|▇|▆|▅|▄|▃|▂|▁'
11 | 
12 | // Resize the browser window to modify the pattern.
13 | export function main(coord, context, cursor, buffer) {
14 | 	const i = coord.index % pattern.length
15 | 	return pattern[i]
16 | }
17 | 
```

--------------------------------------------------------------------------------
/src/core/storage.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | Save and restore a JSON object to and from local storage.
 3 | */
 4 | 
 5 | export default {
 6 | 	store : function(key, obj) {
 7 | 		try {
 8 | 			localStorage.setItem(key, JSON.stringify(obj))
 9 | 			return true
10 | 		} catch (e) {
11 | 			return false
12 | 		}
13 | 	},
14 | 	restore : function(key, target = {}) {
15 | 		const obj = JSON.parse(localStorage.getItem(key))
16 | 		Object.assign(target, obj)
17 | 		return target
18 | 	},
19 | 	clear : function(key) {
20 | 		localStorage.removeItem(key)
21 | 	}
22 | }
23 | 
24 | 
```

--------------------------------------------------------------------------------
/src/programs/demos/sinsin_wave.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Sin Sin
 5 | @desc   Wave variation
 6 | */
 7 | 
 8 | const pattern = '┌┘└┐╰╮╭╯'
 9 | 
10 | const { sin, round, abs } = Math
11 | 
12 | export function main(coord, context, cursor, buffer) {
13 | 	const t = context.time * 0.0005
14 | 	const x = coord.x
15 | 	const y = coord.y
16 | 	const o = sin(y * x * sin(t) * 0.003 + y * 0.01 + t) * 20
17 | 	const i = round(abs(x + y + o)) % pattern.length
18 | 	return pattern[i]
19 | }
20 | 
21 | import { drawInfo } from '/src/modules/drawbox.js'
22 | export function post(context, cursor, buffer) {
23 | 	drawInfo(context, cursor, buffer, { shadowStyle : 'gray' })
24 | }
25 | 
```

--------------------------------------------------------------------------------
/src/programs/basics/coordinates_xy.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Coordinates: x, y
 5 | @desc   Use of coord.x and coord.y
 6 | */
 7 | 
 8 | const density = 'Ñ@#W$9876543210?!abc;:+=-,._ '
 9 | 
10 | export function main(coord, context, cursor, buffer) {
11 | 	// To generate an output return a single character
12 | 	// or an object with a “char” field, for example {char: 'x'}
13 | 
14 | 	// Shortcuts for frame, cols and coord (x, y)
15 | 	const {cols, frame } = context
16 | 	const {x, y} = coord
17 | 
18 | 	// -1 for even lines, 1 for odd lines
19 | 	const sign = y % 2 * 2 - 1
20 | 	const index = (cols + y + x * sign + frame) % density.length
21 | 
22 | 	return density[index]
23 | }
```

--------------------------------------------------------------------------------
/src/programs/contributed/emoji_wave.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ilithya
 4 | @title  Emoji Wave
 5 | @desc   From wingdings icons to unicode emojis
 6 | 		Inspired by emojis evolution
 7 | */
 8 | 
 9 | export const settings = {
10 | 	color : 'white',
11 | 	backgroundColor : 'rgb(100, 0, 300)'
12 | }
13 | 
14 | const {sin, cos, floor} = Math
15 | const density = '☆ ☺︎ 👀 🌈 🌮🌮 🌈 👀 ☺︎ ☆'
16 | 
17 | export function main(coord, context) {
18 | 	const t = context.time * 0.0008
19 | 
20 | 	const x = coord.x
21 | 	const y = coord.y
22 | 
23 | 	const c = context.cols
24 | 	const posCenter = floor((c - density.length) * 0.5)
25 | 
26 | 	const wave = sin(y * cos(t)) * 5
27 | 
28 | 	const i = floor(x + wave) - posCenter
29 | 
30 | 	// Note: “undefined” is rendered as a space…
31 | 	return density[i]
32 | }
```

--------------------------------------------------------------------------------
/tests/browser_bugs/console_error_bug.html:
--------------------------------------------------------------------------------

```html
 1 | <!DOCTYPE html>
 2 | <html lang="en" >
 3 | <head>
 4 | 	<meta charset="utf-8">
 5 | 	<title>Console error bug</title>
 6 | </head>
 7 | <body>
 8 | 
 9 | 	Console output only.<br>
10 | 	<a href="https://bugs.webkit.org/show_bug.cgi?id=218275">bugs.webkit.org/show_bug.cgi?id=218275</a>
11 | 
12 | 	<!--
13 | 	***************************************
14 | 	Safari / WebKit
15 | 	***************************************
16 | 
17 | 	When the script is of type="module" the line number
18 | 	of the error won’t be printed to the console.
19 | 	Check the console for the two errors.
20 | 	-->
21 | 
22 | 	<script>
23 | 		function a( // <-- some syntax error here
24 | 	</script>
25 | 
26 | 	<script type="module">
27 | 		function b( // <-- some syntax error here
28 | 	</script>
29 | </body>
30 | </html>
```

--------------------------------------------------------------------------------
/src/programs/demos/mod_xor.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Mod Xor
 5 | @desc   Patterns obtained trough modulo and xor
 6 | Inspired by this tweet by @ntsutae
 7 | https://twitter.com/ntsutae/status/1292115106763960327
 8 | */
 9 | 
10 | const pattern = '└┧─┨┕┪┖┫┘┩┙┪━'
11 | 
12 | export function main(coord, context, cursor, buffer) {
13 | 	const t1 = Math.floor(context.frame / 2)
14 | 	const t2 = Math.floor(context.frame / 128)
15 | 	const x = coord.x
16 | 	const y = coord.y + t1
17 | 	const m = t2 * 2 % 30 + 31
18 | 	const i = (x + y^x - y) % m & 1
19 | 	const c = (t2 + i) % pattern.length
20 | 	return pattern[c]
21 | }
22 | 
23 | import { drawInfo } from '/src/modules/drawbox.js'
24 | export function post(context, cursor, buffer) {
25 |     drawInfo(context, cursor, buffer, { shadowStyle : 'gray' })
26 | }
27 | 
28 | 
```

--------------------------------------------------------------------------------
/src/programs/basics/cursor.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Cursor
 5 | @desc   Crosshair example with mouse cursor
 6 | */
 7 | 
 8 | export function main(coord, context, cursor, buffer) {
 9 | 	// The cursor coordinates are mapped to the cell
10 | 	// (fractional, needs rounding).
11 | 	const x = Math.floor(cursor.x) // column of the cell hovered
12 | 	const y = Math.floor(cursor.y) // row of the cell hovered
13 | 
14 | 	if (coord.x == x && coord.y == y) return '┼'
15 | 	if (coord.x == x) return '│'
16 | 	if (coord.y == y) return '─'
17 | 	return (coord.x + coord.y) % 2 ? '·' : ' '
18 | }
19 | 
20 | import { drawInfo } from '/src/modules/drawbox.js'
21 | export function post(context, cursor, buffer) {
22 | 	drawInfo(context, cursor, buffer, {
23 | 		color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray'
24 | 	})
25 | }
26 | 
```

--------------------------------------------------------------------------------
/src/programs/basics/how_to_log.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  How to log
 5 | @desc   Console output inside the main() loop
 6 | */
 7 | 
 8 | const { abs, floor, max } = Math
 9 | 
10 | export function main(coord, context, cursor, buffer) {
11 | 
12 | 	const x = abs(coord.x - cursor.x)
13 | 	const y = abs(coord.y - cursor.y) / context.metrics.aspect
14 | 	const dist = floor(max(x, y) + context.frame)
15 | 
16 | 	// Sometimes it’s useful to inspect values from inside the main loop.
17 | 	// The main() function is called every frame for every cell:
18 | 	// the console will be flooded with data very quickly!
19 | 	// Output can be limited to one cell and every 10 frames, for example:
20 | 	if (coord.index == 100 && context.frame % 10 == 0) {
21 | 		// console.clear()
22 | 		console.log("dist = " + dist)
23 | 	}
24 | 
25 | 	return '.-=:abc123?xyz*;%+,'[dist % 30]
26 | }
27 | 
```

--------------------------------------------------------------------------------
/src/programs/header.old.txt:
--------------------------------------------------------------------------------

```
 1 | ---------------------------------------------------------------------
 2 | Type ?help
 3 |      anywhere (or edit the previous line)
 4 |      to open the manual for an overview about the playground,
 5 |      more commands like this and links to many examples.
 6 | Type ?immediate on
 7 |      to enable immediate mode (off to disable).
 8 | Type ?video night
 9 |      to switch to dark mode for the editor (day for light).
10 | ---------------------------------------------------------------------
11 | Cmd/Ctrl-Enter    : run
12 | Cmd/Ctrl-Period   : show/hide editor
13 | Cmd/Ctrl-S        : save locally (with permalink)
14 | Cmd/Ctrl-Shift-U  : share to playground (needs author and title tags)
15 | Program directory : https://play.ertdfgcvb.xyz/lsd
16 | ---------------------------------------------------------------------
```

--------------------------------------------------------------------------------
/src/programs/basics/rendering_to_canvas.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Canvas renderer
 5 | @desc   Rendering to a canvas element
 6 | */
 7 | 
 8 | // A few extra fields are available when choosing the canvas renderer:
 9 | // The offset (from top, left) and the size of the canvas element.
10 | export const settings = {
11 | 	renderer : 'canvas',
12 | 	// Settings available only
13 | 	// for the 'canvas' renderer
14 | 	canvasOffset : {
15 | 		x : 'auto',
16 | 		y : 20
17 | 	},
18 | 	canvasSize : {
19 | 		width : 400,
20 | 		height : 500
21 | 	},
22 | 	// Universal settings
23 | 	cols : 42,
24 | 	rows : 22,
25 | 	backgroundColor : 'pink',
26 | 	color : 'black'
27 | }
28 | 
29 | const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ.:!?'
30 | 
31 | export function main(coord, context, cursor, buffer) {
32 | 	const {x, y} = coord
33 | 	const f = context.frame
34 | 	const l = chars.length
35 | 	const c = context.cols
36 | 	return y % 2 ? chars[(y + x + f) % l] : chars[(y + c - x + f) % l]
37 | }
38 | 
```

--------------------------------------------------------------------------------
/src/programs/basics/name_game.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Name game
 5 | @desc   What’s your name?
 6 | */
 7 | 
 8 | // The default backround color and font attributes can be altered
 9 | // by exporting a ‘settings’ object (see the manual for details).
10 | export const settings = {
11 | 	backgroundColor : 'black',
12 | 	color           : 'white',
13 | 	fontSize        : '3em',
14 | 	fontWeight      : 'lighter' // or 100
15 | }
16 | 
17 | const TAU = Math.PI * 2
18 | 
19 | export function main(coord, context, cursor, buffer) {
20 | 	const a = context.frame * 0.05
21 | 	const f = Math.floor((1 - Math.cos(a)) * 10) + 1
22 | 	const g = Math.floor(a / TAU) % 10 + 1
23 | 	const i = coord.index % (coord.y * g + 1) % (f % context.cols)
24 | 	// NOTE: If the function returns ‘undefined’ or ‘null’
25 | 	// a space character will be inserted.
26 | 	// In some cases ‘i’ may be greater than 2:
27 | 	// JavaScript array out of bounds results in ‘undefined’.
28 | 	return 'Ada'[i]
29 | }
30 | 
```

--------------------------------------------------------------------------------
/src/programs/demos/hotlink.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Hotlink
 5 | @desc   Function hotlink example (GitHub)
 6 |         The code for the Open Simplex Noise function is downloaded from GitHub
 7 |         and evaluated through “new Function()”.
 8 | */
 9 | 
10 | // Don’t do this :)
11 | fetch("https://raw.githubusercontent.com/blindman67/SimplexNoiseJS/master/simplexNoise.js")
12 | .then(e => e.text())
13 | .then(e => {
14 | 	const openSimplexNoise = new Function("return " + e)()
15 | 	noise3D = openSimplexNoise(Date.now()).noise3D
16 | })
17 | 
18 | // Stub function
19 | function noise3D() { return 0 }
20 | 
21 | const density = 'Ñ@#W$9876543210?!abcxyz;:+=-,._ '
22 | 
23 | export function main(coord, context, cursor, buffer) {
24 | 	const t = context.time * 0.0007
25 | 	const s = 0.03
26 | 	const x = coord.x * s
27 | 	const y = coord.y * s / context.metrics.aspect + t
28 | 	const i = Math.floor((noise3D(x, y, t) * 0.5 + 0.5) * density.length)
29 | 	return density[i]
30 | }
31 | 
```

--------------------------------------------------------------------------------
/src/programs/sdf/circle.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Circle
 5 | @desc   Draw a smooth circle with exp()
 6 | */
 7 | 
 8 | import { sdCircle } from '/src/modules/sdf.js'
 9 | import { sort } from '/src/modules/sort.js'
10 | 
11 | const density = sort('/\\MXYZabc!?=-. ', 'Simple Console', false)
12 | 
13 | export const settings = { fps : 60 }
14 | 
15 | export function main(coord, context, cursor, buffer) {
16 | 	const t  = context.time * 0.002
17 |     const m = Math.min(context.cols, context.rows)
18 |     const a = context.metrics.aspect
19 | 
20 | 	const st = {
21 | 		x : 2.0 * (coord.x - context.cols / 2) / m * a,
22 | 		y : 2.0 * (coord.y - context.rows / 2) / m
23 | 	}
24 | 
25 | 	const radius = (Math.cos(t)) * 0.4 + 0.5
26 | 	const d = sdCircle(st, radius)
27 | 	const c = 1.0 - Math.exp(-5 * Math.abs(d))
28 | 	const index = Math.floor(c * density.length)
29 | 
30 | 	return {
31 | 		char : coord.x % 2 ? '│' : density[index],
32 | 		backgroundColor : 'black',
33 | 		color : 'white'
34 | 	}
35 | }
36 | 
```

--------------------------------------------------------------------------------
/tests/single.html:
--------------------------------------------------------------------------------

```html
 1 | <!DOCTYPE html>
 2 | <html lang="en" >
 3 | <head>
 4 | 	<meta charset="utf-8">
 5 | 	<title>Test single</title>
 6 | 	<link rel="stylesheet" type="text/css" href="/css/simple_console.css">
 7 | 	<style type="text/css" media="screen">
 8 | 		html, body {
 9 | 			padding: 0;
10 | 			margin: 0;
11 | 			font-size: 1em;
12 | 			line-height: 1.2;
13 | 			font-family: 'Simple Console', monospace;
14 | 		}
15 | 		pre {
16 | 			position: absolute;
17 | 			margin:0;
18 | 			padding:0;
19 | 			left:0;
20 | 			top:0;
21 | 			width:100vw;
22 | 			height:100vh;
23 | 			font-family: inherit;
24 | 		}
25 | 	</style>
26 | </head>
27 | <body>
28 | 	<pre></pre>
29 | 	<script type="module">
30 | 		import { run } from '/src/run.js'
31 | 		import * as program from '/src/programs/basics/time_milliseconds.js'
32 | 		run(program, { element : document.querySelector('pre') }).then(function(e){
33 | 			console.log(e)
34 | 		}).catch(function(e) {
35 | 			console.warn(e.message)
36 | 			console.log(e.error)
37 | 		})
38 | 	</script>
39 | </body>
40 | </html>
```

--------------------------------------------------------------------------------
/src/programs/contributed/color_waves.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author Eliza
 4 | @title  Color Waves
 5 | @desc   ¯\_(ツ)_/¯
 6 | */
 7 | 
 8 | const chars = '¯\_(ツ)_/¯.::.ᕦ(ò_óˇ)ᕤ '.split('')
 9 | 
10 | export const settings = {
11 | 	fontWeight : 700
12 | }
13 | 
14 | export function main(coord, context, cursor){
15 | 	const t = context.time * 0.0001
16 | 
17 | 	const x = coord.x
18 | 	const y = coord.y
19 | 
20 | 	const a = Math.cos(y * Math.cos(t) * 0.2 + x * 0.04 + t);
21 | 	const b = Math.sin(x * Math.sin(t) * 0.2 * y * 0.04 + t);
22 | 	const c = Math.cos(y * Math.cos(t) * 0.2 + x * 0.04 + t);
23 | 
24 | 	const o =  a + b + c * 20;
25 | 
26 | 	const colors = ['mediumvioletred', 'gold', 'orange', 'chartreuse', 'blueviolet', 'deeppink'];
27 | 
28 | 	const i = Math.round(Math.abs(x + y + o)) % chars.length
29 | 	return {
30 | 		char : chars[i],
31 | 		color : colors[i % colors.length]
32 | 	}
33 | }
34 | 
35 | import { drawInfo } from '/src/modules/drawbox.js'
36 | export function post(context, cursor, buffer){
37 | 	drawInfo(context, cursor, buffer)
38 | }
39 | 
```

--------------------------------------------------------------------------------
/src/programs/demos/sinsin_checker.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Sin Sin
 5 | @desc   Checker variation
 6 | */
 7 | 
 8 | const pattern = [
 9 | 	' _000111_ ',
10 | 	'.+abc+.      '
11 | ]
12 | const col = ['black', 'blue']
13 | const weights = [100, 700]
14 | 
15 | const { floor, sin } = Math
16 | 
17 | export function main(coord, context, cursor, buffer) {
18 | 	const t = context.time * 0.001
19 | 	const x = coord.x - context.cols / 2
20 | 	const y = coord.y - context.rows / 2
21 | 	const o = sin(x * y * 0.0017 + y * 0.0033 + t ) * 40
22 | 	const i = floor(Math.abs(x + y + o))
23 | 	const c = (floor(coord.x * 0.09) + floor(coord.y * 0.09)) % 2
24 | 	return {
25 | 		char : pattern[c][i % pattern[c].length],
26 | 		color : 'black', //col[c],
27 | 		// backgroundColor : col[(c+1)%2],
28 | 		fontWeight : weights[c],
29 | 	}
30 | }
31 | 
32 | import { drawInfo } from '/src/modules/drawbox.js'
33 | export function post(context, cursor, buffer) {
34 |     drawInfo(context, cursor, buffer, { shadowStyle : 'gray' })
35 | }
36 | 
```

--------------------------------------------------------------------------------
/src/modules/image.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | @module   image.js
 3 | @desc     Image loader and helper
 4 | @category public
 5 | 
 6 | Loads an image and draws it on a canvas.
 7 | The returned object is a canvas wrapper and its methods (get, sample, etc.)
 8 | can be used before the image has completely loaded.
 9 | 
10 | Usage:
11 | // Starts async loading:
12 | const img = Image.load('res/pattern.png')
13 | // Returns a black color until the image has been loaded:
14 | const color = img.get(10, 10)
15 | 
16 | */
17 | 
18 | import Canvas from './canvas.js'
19 | import Load from './load.js'
20 | 
21 | export default {
22 | 	load
23 | }
24 | 
25 | function load(path) {
26 | 
27 | 	const source = document.createElement('canvas')
28 | 	source.width = 1
29 | 	source.height = 1
30 | 
31 | 	const can = new Canvas(source)
32 | 
33 | 	Load.image(path).then( img => {
34 | 		console.log('Image ' + path + ' loaded. Size: ' + img.width + '×' + img.height)
35 | 		can.resize(img.width, img.height)
36 | 		can.copy(img)
37 | 	}).catch(err => {
38 | 		console.warn('There was an error loading image ' + path + '.')
39 | 	})
40 | 
41 | 	return can
42 | }
43 | 
```

--------------------------------------------------------------------------------
/tests/promise_chain.html:
--------------------------------------------------------------------------------

```html
 1 | <!DOCTYPE html>
 2 | <html lang="en" >
 3 | <head>
 4 | 	<meta charset="utf-8">
 5 | 	<title>Promise chain</title>
 6 | </head>
 7 | <body>
 8 | 	<pre>Console output only</pre>
 9 | 	<script>
10 | 		addEventListener('error', function(error) {
11 | 			console.log("------------")
12 | 		  console.log(error)
13 | 		}, false)
14 | 
15 | 
16 | 		addEventListener('unhandledrejection', function(event) {
17 | 		  console.log(event.promise) // [object Promise] - the promise that generated the error
18 | 		  console.log(event.reason) // Error: Whoops! - the unhandled error object
19 | 		})
20 | 
21 | 	</script>
22 | 	<script type="module">
23 | 
24 | 		// Test to check how errors are 'catched' in async / Promises.
25 | 		// https://javascript.info/async-await
26 | 		// http://thecodebarbarian.com/async-await-error-handling-in-javascript.html
27 | 
28 | 
29 | 		function b() {
30 | 
31 | 	    		c( // <---
32 | 				function c() {
33 | 	    			setTimeout(() => resolve("b"), 1000)
34 | 	    		}
35 | 
36 | 		}
37 | 
38 | 		console.log("a")
39 | 		//b().then(res => console.log(res))
40 | 		console.log("c")
41 | 
42 | 	</script>
43 | </body>
44 | </html>
```

--------------------------------------------------------------------------------
/src/programs/basics/time_frames.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Time: frames
 5 | @desc   Use of context.frame (ASCII horizon)
 6 | */
 7 | 
 8 | // The default framerate can be altered
 9 | // by exporting a 'settings' object (see the manual for details).
10 | export const settings = { fps : 30 }
11 | 
12 | export function main(coord, context, cursor, buffer) {
13 | 	const z = Math.floor((coord.y - context.rows / 2))
14 | 
15 | 	// Avoid division by zero
16 | 	if (z == 0) return ' '
17 | 
18 | 	// Calculate a fake perspective
19 | 	const val = (coord.x - context.cols/2) / z
20 | 
21 | 	// Add time (context.frame) for animation
22 | 	// and make sure to get adisplayable charCode (int, positive, valid range)
23 | 	const code = Math.floor(val + context.cols/2 + context.frame * 0.3) % 94 + 32
24 | 	return String.fromCharCode(code)
25 | }
26 | 
27 | // Display some info
28 | import { drawInfo } from '/src/modules/drawbox.js'
29 | export function post(context, cursor, buffer) {
30 | 	drawInfo(context, cursor, buffer, {
31 | 		color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray'
32 | 	})
33 | }
34 | 
```

--------------------------------------------------------------------------------
/src/programs/contributed/stacked_sin_waves.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author Raurir
 4 | @title  Stacked sin waves
 5 | @desc   noob at frag shaders
 6 | */
 7 | 
 8 | const chars = "█▓▒░ ".split('')
 9 | 
10 | import { fract } from "/src/modules/num.js"
11 | 
12 | export function main(coord, context, cursor, buffer){
13 | 	const t = context.time * 0.002
14 | 	const x = coord.x
15 | 	const y = coord.y
16 | 	//const index = coord.index
17 | 	//const o = Math.sin(y * Math.sin(t) * 0.2 + x * 0.04 + t) * 20
18 | 	//const i = Math.round(Math.abs(x + y + o)) % chars.length
19 | 	const v0 = context.cols / 4 + wave(t, y, [0.15, 0.13, 0.37], [10,8,5]) * 0.9;
20 | 	const v1 = v0 + wave(t, y, [0.12, 0.14, 0.27], [3,6,5]) * 0.8;
21 | 	const v2 = v1 + wave(t, y, [0.089, 0.023, 0.217], [2,4,2]) * 0.3;
22 | 	const v3 = v2 + wave(t, y, [0.167, 0.054, 0.147], [4,6,7]) * 0.4;
23 | 	const i = x > v3 ? 4
24 | 		: x > v2 ? 3
25 | 		: x > v1 ? 2
26 | 		: x > v0 ? 1
27 | 		: 0;
28 | 
29 | 	return chars[i];
30 | }
31 | 
32 | function wave(t, y, seeds, amps) {
33 | 	return (
34 | 		(Math.sin(t + y * seeds[0]) + 1) * amps[0]
35 | 		+ (Math.sin(t + y * seeds[1]) + 1) * amps[1]
36 | 		+ (Math.sin(t + y * seeds[2])) * amps[2]
37 | 	)
38 | }
```

--------------------------------------------------------------------------------
/src/programs/basics/time_milliseconds.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Time: milliseconds
 5 | @desc   Use of context.time
 6 | */
 7 | 
 8 | // Globals have module scope
 9 | const pattern = 'ABCxyz01═|+:. '
10 | 
11 | // This is the main loop.
12 | // Character coordinates are passed in coord {x, y, index}.
13 | // The function must return a single character or, alternatively, an object:
14 | // {char, color, background, weight}.
15 | export function main(coord, context, cursor, buffer) {
16 | 	const t = context.time * 0.0001
17 | 	const x = coord.x
18 | 	const y = coord.y
19 | 	const o = Math.sin(y * Math.sin(t) * 0.2 + x * 0.04 + t) * 20
20 | 	const i = Math.round(Math.abs(x + y + o)) % pattern.length
21 | 	return {
22 | 		char   : pattern[i],
23 | 		fontWeight : '100', // or 'light', 'bold', '400'
24 | 	}
25 | }
26 | 
27 | import { drawInfo } from '/src/modules/drawbox.js'
28 | 
29 | // This function is called after the main loop and is useful
30 | // to manipulate the buffer; in this case with a window overlay.
31 | export function post(context, cursor, buffer) {
32 | 	// An extra object can be passed to drawInfo to alter the default style
33 | 	drawInfo(context, cursor, buffer, {
34 | 		color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray'
35 | 	})
36 | }
37 | 
```

--------------------------------------------------------------------------------
/src/modules/filedownload.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | @module   filedownload.js
 3 | @desc     Exports a file via Blob
 4 | @category internal
 5 | 
 6 | Downloads a Blob as file and this “hack”:
 7 | creates an anchor with a “download” attribute
 8 | and then emits a click event.
 9 | See: https://github.com/eligrey/FileSaver.js
10 | */
11 | 
12 | const mimeTypes = {
13 | 	'js'  : 'text/javascript',
14 | 	'txt' : 'text/plain',
15 | 	'png' : 'image/png',
16 | 	'jpg' : 'text/jpeg',
17 | }
18 | 
19 | // For text elements
20 | export function saveSourceAsFile(src, filename) {
21 | 	const ext = getFileExt(filename)
22 | 	const type = mimeTypes[ext]
23 | 	const blob = type ? new Blob([src], {type}) : new Blob([src])
24 | 	saveBlobAsFile(blob, filename)
25 | }
26 | 
27 | // Gets extension of a filename
28 | function getFileExt(filename) {
29 | 	return filename.split('.').pop()
30 | }
31 | 
32 | // For canvas elements
33 | export function saveBlobAsFile(blob, filename) {
34 | 
35 | 	const a = document.createElement('a')
36 | 	a.download = filename
37 | 	a.rel = 'noopener'
38 | 	a.href = URL.createObjectURL(blob)
39 | 
40 | 	setTimeout(() => { URL.revokeObjectURL(a.href) }, 10000)
41 | 	setTimeout(() => { click(a) }, 0)
42 | }
43 | 
44 | function click(node) {
45 | 	try {
46 | 		node.dispatchEvent(new MouseEvent('click'))
47 | 	} catch (err) {
48 | 		var e = document.createEvent('MouseEvents')
49 | 		e.initMouseEvent('click')
50 | 		node.dispatchEvent(e)
51 | 	}
52 | }
53 | 
```

--------------------------------------------------------------------------------
/src/programs/camera/camera_gray.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Camera grayscale
 5 | @desc   Grayscale input from camera
 6 | */
 7 | 
 8 | import { sort } from '/src/modules/sort.js'
 9 | import Camera from '/src/modules/camera.js'
10 | import Canvas from '/src/modules/canvas.js'
11 | 
12 | const cam = Camera.init()
13 | const can = new Canvas()
14 | // For a debug view uncomment the following line:
15 | // can.display(document.body, 10, 10)
16 | 
17 | const density = sort(' .x?▂▄▆█', 'Simple Console', false)
18 | 
19 | const data = []
20 | 
21 | export function pre(context, cursor, buffer) {
22 | 	const a = context.metrics.aspect
23 | 
24 | 	// The canvas is resized so that 1 cell -> 1 pixel
25 | 	can.resize(context.cols, context.rows)
26 | 	// The cover() function draws an image (cam) to the canvas covering
27 | 	// the whole frame. The aspect ratio can be adjusted with the second
28 | 	// parameter.
29 | 	can.cover(cam, a).mirrorX().normalize().writeTo(data)
30 | }
31 | 
32 | export function main(coord, context, cursor, buffer) {
33 | 	// Coord also contains the index of each cell:
34 | 	const color = data[coord.index]
35 | 	const index = Math.floor(color.v * (density.length-1))
36 | 	return density[index]
37 | }
38 | 
39 | import { drawInfo } from '/src/modules/drawbox.js'
40 | export function post(context, cursor, buffer) {
41 | 	drawInfo(context, cursor, buffer)
42 | }
43 | 
44 | 
```

--------------------------------------------------------------------------------
/tests/browser_bugs/error_listener_bug.html:
--------------------------------------------------------------------------------

```html
 1 | <!DOCTYPE html>
 2 | <html lang="en" >
 3 | <head>
 4 | 	<meta charset="utf-8">
 5 | 	<title>Error listener bug</title>
 6 | </head>
 7 | <body>
 8 | 
 9 | 	Console output only.<br>
10 | 	<a href="https://bugs.webkit.org/show_bug.cgi?id=218284">bugs.webkit.org/show_bug.cgi?id=218284</a>
11 | 
12 | 	<!--
13 | 	***************************************
14 | 	Safari / WebKit
15 | 	***************************************
16 | 
17 | 	Some syntax errors are not captured by the event listener when originated in a module.
18 | 
19 | 	Syntax errors inside a module in situations like:
20 | 		function a() {  // missing closing bracket
21 | 
22 | 	won’t get captured by the listener.
23 | 	While other errors like:
24 | 		const a = 1
25 | 		a = 2
26 | 
27 | 	will get captured, even when generated inside the module.
28 | 	This works as expected in FF and Chrome.
29 | 	-->
30 | 
31 | 	<script>
32 | 		addEventListener('error', function(e) {
33 | 			console.log('Captured: ' + e.message)
34 | 		}, false)
35 | 	</script>
36 | 
37 | 	<!-- script -->
38 | 
39 | 	<script>
40 | 		const a = 1             // CAPTURED
41 | 		a = 2
42 | 	</script>
43 | 
44 | 	<script>
45 | 		for(let b=0; b<2 b++) {} // CAPTURED
46 | 	</script>
47 | 
48 | 	<!-- module -->
49 | 
50 | 	<script type="module">
51 | 		const a = 1             // CAPTURED
52 | 		a = 2
53 | 	</script>
54 | 
55 | 	<script type="module">
56 | 		for(let a=0; a<2 a++) {} // NOT CAPTURED (but still displayed in the console)
57 | 	</script>
58 | 
59 | </body>
60 | </html>
```

--------------------------------------------------------------------------------
/tests/multi.html:
--------------------------------------------------------------------------------

```html
 1 | <!DOCTYPE html>
 2 | <html lang="en" >
 3 | <head>
 4 | 	<meta charset="utf-8">
 5 | 	<title>Test multi</title>
 6 | 	<link rel="stylesheet" type="text/css" href="/css/simple_console.css">
 7 | 	<style type="text/css" media="screen">
 8 | 		body {
 9 | 			background-color: rgb(250, 250, 250);
10 | 			margin:2em;
11 | 		}
12 | 		pre {
13 | 			margin: 1em;
14 | 			width: 40em;
15 | 			height: 22em;
16 | 			display: inline-block;
17 | 			font-size: 1em;
18 | 			line-height: 1.2;
19 | 			font-family: 'Simple Console', monospace;
20 | 			background-color: white;
21 | 		}
22 | 
23 | 	</style>
24 | </head>
25 | <body>
26 | 	<pre></pre>
27 | 	<pre></pre>
28 | 	<pre></pre>
29 | 	<pre></pre>
30 | 	<script type="module">
31 | 		import    { run } from '/src/run.js'
32 | 		import * as prog0 from '/src/programs/basics/time_milliseconds.js'
33 | 		import * as prog1 from '/src/programs/basics/cursor.js'
34 | 		import * as prog2 from '/src/programs/sdf/balls.js'
35 | 		import * as prog3 from '/src/programs/basics/time_frames.js'
36 | 
37 | 		const pre = document.querySelectorAll('pre')
38 | 		run(prog0, { element : pre[0] } ).catch(errorHandler)
39 | 		run(prog1, { element : pre[1] } ).catch(errorHandler)
40 | 		run(prog2, { element : pre[2] } ).catch(errorHandler)
41 | 		run(prog3, { element : pre[3] } ).catch(errorHandler)
42 | 
43 | 		function errorHandler(e) {
44 | 			console.warn(e.message)
45 | 			console.log(e.error)
46 | 		}
47 | 	</script>
48 | </body>
49 | </html>
```

--------------------------------------------------------------------------------
/src/modules/load.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | @module   loader.js
 3 | @desc     Various file type loader, returns a Promise
 4 | @category internal
 5 | 
 6 | Example:
 7 | 
 8 | import Load from './load.js'
 9 | 
10 | // Usage: load different file types with one callback
11 | Promise.all([
12 | 	Load.text('assets/1/text.txt'),
13 | 	Load.image('assets/1/blocks.png'),
14 | 	Load.image('assets/1/colors.png'),
15 | 	Load.json('data.json'),
16 | ]).then(function(res) {
17 | 	console.log('Everything has loaded!')
18 | 	console.log(res)
19 | }).catch(function() {
20 | 	console.log('Error')
21 | })
22 | 
23 | // Usage: load a single resource
24 | Load.image('assets/1/colors.png').then( img => {
25 | 	console.log(`Image has loaded, size is: ${img.width}x${img.height}`)
26 | })
27 | 
28 | */
29 | 
30 | export default { json, image, text }
31 | 
32 | function image (url) {
33 | 	return new Promise((resolve, reject) => {
34 | 		const img = new Image()
35 | 		img.onload = () => resolve(img)
36 | 		img.onerror = () => {
37 | 			console.log('Loader: error loading image ' + url)
38 | 			resolve(null)
39 | 		}
40 | 		img.src = url
41 | 	})
42 | }
43 | 
44 | function text (url) {
45 | 	return fetch(url).then( response => {
46 | 		return response.text()
47 | 	}).catch( err => {
48 | 		console.log('Loader: error loading text ' + url)
49 | 		return ''
50 | 	})
51 | }
52 | 
53 | function json (url) {
54 | 	return fetch(url).then( response => {
55 | 		return response.json()
56 | 	}).catch( err => {
57 | 		console.log('Loader: error loading json ' + url)
58 | 		return {}
59 | 	})
60 | }
61 | 
62 | 
```

--------------------------------------------------------------------------------
/src/modules/sdf.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | @module   sdf.js
 3 | @desc     Some signed distance functions
 4 | @category public
 5 | 
 6 | SDF functions ported from the almighty Inigo Quilezles:
 7 | https://www.iquilezles.org/www/articles/distfunctions/distfunctions.htm
 8 | */
 9 | 
10 | import { clamp, mix } from "./num.js"
11 | import { length, sub, dot, mulN } from "./vec2.js"
12 | 
13 | export function sdCircle(p, radius) { // vec2, float
14 | 	return length(p) - radius
15 | }
16 | 
17 | export function sdBox(p, size) {     // vec2, vec2
18 | 	const d = {
19 | 		x : Math.abs(p.x) - size.x,
20 | 		y : Math.abs(p.y) - size.y,
21 | 	}
22 | 	d.x = Math.max(d.x, 0)
23 | 	d.y = Math.max(d.y, 0)
24 | 	return length(d) + Math.min(Math.max(d.x, d.y), 0.0)
25 | }
26 | 
27 | export function sdSegment(p, a, b, thickness) {
28 |     const pa = sub(p, a)
29 |     const ba = sub(b, a)
30 |     const h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0 )
31 |     return length(sub(pa, mulN(ba, h))) - thickness
32 | }
33 | 
34 | export function opSmoothUnion( d1, d2, k ) {
35 | 	const h = clamp(0.5 + 0.5 * (d2 - d1) / k, 0.0, 1.0 )
36 | 	return mix( d2, d1, h ) - k * h * (1.0 - h)
37 | }
38 | 
39 | export function opSmoothSubtraction( d1, d2, k ) {
40 | 	const h = clamp( 0.5 - 0.5 * (d2 + d1) / k, 0.0, 1.0 )
41 | 	return mix( d2, -d1, h ) + k * h * (1.0 - h)
42 | }
43 | 
44 | export function opSmoothIntersection( d1, d2, k ) {
45 | 	const h = clamp( 0.5 - 0.5 * (d2 - d1) / k, 0.0, 1.0 )
46 | 	return mix( d2, d1, h ) + k * h * (1.0 - h)
47 | }
48 | 
49 | 
```

--------------------------------------------------------------------------------
/src/programs/addheader.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | 
 3 | # Script to replace the [header] string of each .js script
 4 | # with the contents of the file 'header.txt'.
 5 | # Expects a valid path for the output of the files.
 6 | #
 7 | # > sh ./addheader.sh output_folder
 8 | 
 9 | if [ -z $1 ]; then
10 | 	echo "Please specify an output folder."
11 | 	exit 1
12 | fi
13 | 
14 | # A bit of a cumbersome workaround
15 | # as the script won't be called from the current folder:
16 | CURRENT_PATH=$(pwd)    # path from where this script has been called
17 | TARGET_PATH=$(pwd)/$1  # path to the copied .js files
18 | SCRIPT_PATH=${0%/*}    # path to this script
19 | 
20 | if [[ $SCRIPT_PATH -ef $TARGET_PATH ]];  then
21 | 	echo "Can’t overwrite the current files."
22 | 	exit 1
23 | fi
24 | 
25 | RED='\033[0;31m'
26 | BLUE='\033[0;34m'
27 | PURPLE='\033[0;35m'
28 | NC='\033[0m'
29 | echo "${PURPLE}Writing headers...${NC}"
30 | 
31 | cd $SCRIPT_PATH
32 | 
33 | EXPR_1="/\[header\]/r header.txt" # Insert the contents of _header.txt after [header]
34 | EXPR_2="/\[header\]/d"            # Delete the line with [header]
35 | 
36 | for folder in `find ./ -mindepth 1 -type d`; do
37 | 	# echo "creating $TARGET_PATH/$folder..."
38 | 
39 | 	mkdir -p $TARGET_PATH/$folder
40 | 
41 | 	for file in ./$folder/*.js; do
42 | 		P=$(echo $file | sed 's/\.\///g') # prettier output
43 | 		echo "Writing $P..."
44 | 		sed -e "$EXPR_1"  -e "$EXPR_2" "$file" > "$TARGET_PATH/$file"
45 | 	done
46 | done
47 | echo "${PURPLE}...done!${NC}"
48 | 
49 | # Restore folder
50 | cd $CURRENT_PATH
51 | 
```

--------------------------------------------------------------------------------
/src/programs/sdf/rectangles.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Rectangles
 5 | @desc   Smooth SDF Rectangles
 6 | */
 7 | 
 8 | import { map } from '/src/modules/num.js'
 9 | import { sdBox, opSmoothUnion } from '/src/modules/sdf.js'
10 | 
11 | let density = '▚▀abc|/:÷×+-=?*· '
12 | 
13 | export function main(coord, context, cursor, buffer) {
14 | 
15 | 	const t = context.time
16 |     const m = Math.max(context.cols, context.rows)
17 |     const a = context.metrics.aspect
18 | 
19 | 	const st = {
20 | 		x : 2.0 * (coord.x - context.cols / 2) / m,
21 | 		y : 2.0 * (coord.y - context.rows / 2) / m / a
22 | 	}
23 | 
24 | 	let d = 1e100
25 | 
26 | 	const s = map(Math.sin(t * 0.0005), -1, 1, 0.0, 0.4)
27 | 	const g = 1.2
28 | 	for (let by=-g; by<=g; by+=g*0.33) {
29 | 		for (let bx=-g; bx<=g; bx+=g*0.33) {
30 | 			const r = t * 0.0004 * (bx + g*2) + (by + g*2)
31 | 			const f = transform(st, {x: bx, y: by},  r)
32 | 			const d1 = sdBox(f, {x:g*0.33, y:0.01})
33 | 			d = opSmoothUnion(d, d1, s)
34 | 		}
35 | 	}
36 | 
37 | 	let c = 1.0 - Math.exp(-5 * Math.abs(d))
38 | 	const index = Math.floor(c * density.length)
39 | 
40 | 	return density[index]
41 | }
42 | 
43 | function transform(p, trans, rot) {
44 | 	const s = Math.sin(-rot)
45 | 	const c = Math.cos(-rot)
46 | 	const dx = p.x - trans.x
47 | 	const dy = p.y - trans.y
48 | 	return {
49 | 		x : dx * c - dy * s,
50 | 		y : dx * s + dy * c,
51 | 	}
52 | }
53 | 
54 | import { drawInfo } from '/src/modules/drawbox.js'
55 | export function post(context, cursor, buffer) {
56 | 	drawInfo(context, cursor, buffer)
57 | }
58 | 
```

--------------------------------------------------------------------------------
/src/modules/num.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | @module   num.js
 3 | @desc     Some GLSL functions ported to JS
 4 | @category public
 5 | */
 6 | 
 7 | export default {
 8 | 	map,
 9 | 	fract,
10 | 	clamp,
11 | 	sign,
12 | 	mix,
13 | 	smoothstep,
14 | 	smootherstep
15 | }
16 | 
17 | // Maps a value v from range 'in' to range 'out'
18 | export function map(v, inA, inB, outA, outB) {
19 | 	return outA + (outB - outA) * ((v - inA) / (inB - inA))
20 | }
21 | 
22 | // Returns the fractional part of a float
23 | export function fract(v) {
24 | 	return v - Math.floor(v)
25 | }
26 | 
27 | // Clamps a value between min and max
28 | export function clamp(v, min, max) {
29 | 	if (v < min) return min
30 | 	if (v > max) return max
31 | 	return v
32 | }
33 | 
34 | // Returns -1 for negative numbers, +1 for positive numbers, 0 for zero
35 | export function sign(n) {
36 | 	if (n > 0) return  1
37 | 	if (n < 0) return -1
38 | 	return 0
39 | }
40 | 
41 | // GLSL mix
42 | export function mix(v1, v2, a) {
43 | 	return v1 * (1 - a) + v2 * a
44 | }
45 | 
46 | // GLSL step
47 | export function step(edge, x) {
48 | 	return (x < edge ? 0 : 1)
49 | }
50 | 
51 | // GLSL smoothstep
52 | // https://en.wikipedia.org/wiki/Smoothstep
53 | export function smoothstep(edge0, edge1, t) {
54 |   	const x = clamp((t - edge0) / (edge1 - edge0), 0, 1)
55 |   	return x * x * (3 - 2 * x)
56 | }
57 | 
58 | // GLSL smootherstep
59 | export function smootherstep(edge0, edge1, t) {
60 |   const x = clamp((t - edge0) / (edge1 - edge0), 0, 1)
61 |   return x * x * x * (x * (x * 6 - 15) + 10)
62 | }
63 | 
64 | // GLSL modulo
65 | export function mod(a, b) {
66 | 	return a % b
67 | }
68 | 
69 | 
70 | 
```

--------------------------------------------------------------------------------
/src/programs/basics/performance_test.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Perfomance test
 5 | @desc   Vertical vs horizontal changes impact FPS
 6 | */
 7 | 
 8 | import { map } from '/src/modules/num.js'
 9 | 
10 | export const settings = { fps : 60 }
11 | 
12 | const { cos } = Math
13 | 
14 | export function main(coord, context, cursor, buffer) {
15 | 
16 | 	// Hold the mouse button to switch the direction
17 | 	// of the gradient and observe the drop in FPS.
18 | 	// Frequent *horizontal* changes in style will slow down
19 | 	// the DOM rendering as each character needs to be
20 | 	// wrapped in an individual, inline-styled <span>.
21 | 	// Frequent verical changes won’t affect the speed.
22 | 
23 | 	const direction = cursor.pressed ? coord.x : coord.y
24 | 
25 | 	const f = context.frame * 0.05
26 | 
27 |     const r1 = map(cos(direction * 0.06 + 1 -f), -1, 1, 0, 255)
28 |     const g1 = map(cos(direction * 0.07 + 2 -f), -1, 1, 0, 255)
29 |     const b1 = map(cos(direction * 0.08 + 3 -f), -1, 1, 0, 255)
30 |     const r2 = map(cos(direction * 0.03 + 1 -f), -1, 1, 0, 255)
31 |     const g2 = map(cos(direction * 0.04 + 2 -f), -1, 1, 0, 255)
32 |     const b2 = map(cos(direction * 0.05 + 3 -f), -1, 1, 0, 255)
33 | 
34 | 	return {
35 | 		char : context.frame % 10,
36 | 		color : `rgb(${r2},${g2},${b2})`,
37 | 		backgroundColor : `rgb(${r1},${g1},${b1})`,
38 | 	}
39 | }
40 | 
41 | import { drawInfo } from '/src/modules/drawbox.js'
42 | export function post(context, cursor, buffer) {
43 | 	drawInfo(context, cursor, buffer)
44 | }
45 | 
```

--------------------------------------------------------------------------------
/src/programs/camera/camera_double_res.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Camera double resolution
 5 | @desc   Doubled vertical resolution input from camera
 6 | */
 7 | 
 8 | import { CSS3 } from '/src/modules/color.js'
 9 | import Camera from '/src/modules/camera.js'
10 | import Canvas from '/src/modules/canvas.js'
11 | 
12 | const cam = Camera.init()
13 | const can = new Canvas()
14 | // For a debug view uncomment the following line:
15 | // can.display(document.body, 10, 10)
16 | 
17 | // Palette for quantization
18 | const pal = []
19 | pal.push(CSS3.red)
20 | pal.push(CSS3.blue)
21 | pal.push(CSS3.white)
22 | pal.push(CSS3.black)
23 | pal.push(CSS3.lightblue)
24 | 
25 | // Camera data
26 | const data = []
27 | 
28 | export function pre(context, cursor, buffer) {
29 | 	const a = context.metrics.aspect
30 | 
31 | 	// The canvas is resized to the double of the height of the context
32 | 	can.resize(context.cols, context.rows * 2)
33 | 
34 | 	// Also the aspect ratio needs to be doubled
35 | 	can.cover(cam, a * 2).quantize(pal).mirrorX().writeTo(data)
36 | }
37 | 
38 | export function main(coord, context, cursor, buffer) {
39 | 	// Coord also contains the index of each cell:
40 | 	const idx   = coord.y * context.cols * 2 + coord.x
41 | 	const upper = data[idx]
42 | 	const lower = data[idx + context.cols]
43 | 
44 | 	return {
45 | 		char :'▄',
46 | 		color : lower.hex,
47 | 		backgroundColor : upper.hex
48 | 	}
49 | }
50 | 
51 | import { drawInfo } from '/src/modules/drawbox.js'
52 | export function post(context, cursor, buffer) {
53 | 	drawInfo(context, cursor, buffer)
54 | }
55 | 
56 | 
```

--------------------------------------------------------------------------------
/src/modules/list.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | 
 3 | # Script to generate a list of all the projects
 4 | 
 5 | # Trims a string
 6 | function trim {
 7 | 	local var="$*"
 8 | 	# remove leading whitespace characters
 9 | 	var="${var#"${var%%[![:space:]]*}"}"
10 | 	# remove trailing whitespace characters
11 | 	var="${var%"${var##*[![:space:]]}"}"
12 | 	printf '%s' "$var"
13 | }
14 | 
15 | 
16 | SCRIPT_PATH=${0%/*}    # path to this script
17 | 
18 | 
19 | 
20 | function write {
21 | 
22 | 	local var="$*"
23 | 
24 | 	URL_PREFIX='/src/modules'
25 | 
26 | 	FIRST=1
27 | 
28 | 	for file in $SCRIPT_PATH/*.js; do
29 | 
30 | 		CATEGORY=$(sed -En 's/^@category[ \t](.*)$/\1/p' $file)
31 | 
32 | 		if [ $var == "$CATEGORY" ];
33 | 		then
34 | 			# The path in $file is the full path:
35 | 			# ./play.core/src/modules/[folder]/[file].js
36 | 			URL=$URL_PREFIX$(echo $file | sed -e 's/\.\///' -e 's/\.play\.core\/src\/modules//')
37 | 			MODULE=$(sed -En 's/^@module[ \t](.*)$/\1/p' $file)
38 | 			DESC=$(sed -En 's/^@desc[ \t](.*)$/\1/p' $file)
39 | 
40 | 		if [[ $FIRST == 1 ]]; then
41 | 			FIRST=0
42 | 			printf "\t"
43 | 			printf "<div>"
44 | 			printf "$(trim $CATEGORY)"
45 | 			printf "</div>"
46 | 			printf "\n"
47 | 		else
48 | 			printf "\t"
49 | 			printf "<div>"
50 | 			printf "</div>"
51 | 			printf "\n"
52 | 		fi
53 | 
54 | 			printf "\t"
55 | 			printf "<div>"
56 | 			printf "<a target='_blank' href='$URL'>$(trim $MODULE)</a>"
57 | 			printf "</div>"
58 | 			printf "\n"
59 | 
60 | 			printf "\t"
61 | 			printf "<div>"
62 | 			printf "$(trim $DESC)"
63 | 			printf "</div>"
64 | 			printf "\n"
65 | 		fi
66 | 
67 | 	done
68 | }
69 | 
70 | echo $(write "public")
71 | echo $(write "internal")
72 | echo $(write "renderer")
73 | 
74 | 
```

--------------------------------------------------------------------------------
/src/programs/sdf/balls.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Balls
 5 | @desc   Smooth SDF balls
 6 | */
 7 | 
 8 | import { map } from '/src/modules/num.js'
 9 | import { sdCircle, opSmoothUnion } from '/src/modules/sdf.js'
10 | 
11 | const density = '#ABC|/:÷×+-=?*· '
12 | 
13 | const { PI, sin, cos, exp, abs } = Math
14 | 
15 | export function main(coord, context, cursor, buffer) {
16 | 	const t = context.time * 0.001 + 10
17 |     const m = Math.min(context.cols, context.rows)
18 |     const a = context.metrics.aspect
19 | 
20 | 	const st = {
21 | 		x : 2.0 * (coord.x - context.cols / 2) / m * a,
22 | 		y : 2.0 * (coord.y - context.rows / 2) / m
23 | 	}
24 | 
25 | 	// const z = map(Math.sin(t * 0.00032), -1, 1, 0.5, 1)
26 | 	// st.x *= z
27 | 	// st.y *= z
28 | 
29 | 	const s = map(sin(t * 0.5), -1, 1, 0.0, 0.9)
30 | 
31 | 	let d = Number.MAX_VALUE
32 | 
33 | 	const num = 12
34 | 	for (let i=0; i<num; i++) {
35 | 		const r = map(cos(t * 0.95 * (i + 1) / (num + 1)), -1, 1, 0.1, 0.3)
36 | 		const x = map(cos(t * 0.23 * (i / num * PI + PI)), -1, 1, -1.2, 1.2)
37 | 		const y = map(sin(t * 0.37 * (i / num * PI + PI)), -1, 1, -1.2, 1.2)
38 | 		const f = transform(st, {x, y},  t)
39 | 		d = opSmoothUnion(d, sdCircle(f, r), s)
40 | 	}
41 | 
42 | 	let c = 1.0 - exp(-3 * abs(d));
43 | 	//if (d < 0) c = 0
44 | 
45 | 	const index = Math.floor(c * density.length)
46 | 
47 | 	return density[index]
48 | }
49 | 
50 | function transform(p, trans, rot) {
51 | 	const s = sin(-rot)
52 | 	const c = cos(-rot)
53 | 	const dx = p.x - trans.x
54 | 	const dy = p.y - trans.y
55 | 	return {
56 | 		x : dx * c - dy * s,
57 | 		y : dx * s + dy * c,
58 | 	}
59 | }
60 | 
```

--------------------------------------------------------------------------------
/src/modules/camera.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | @module   camera.js
 3 | @desc     Webcam init and helper
 4 | @category public
 5 | 
 6 | Initializes a user-facing camera,
 7 | returns a video element (initialised asynchronously).
 8 | */
 9 | 
10 | export default { init }
11 | 
12 | let video
13 | function init(callback) {
14 | 	// Avoid double init of video object
15 | 	video = video || getUserMedia(callback)
16 | 	return video
17 | }
18 | 
19 | // https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
20 | function getUserMedia(callback) {
21 | 
22 | 	// getUserMedia is not supported by browser
23 | 	if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) {
24 | 		throw new DOMException('getUserMedia not supported in this browser')
25 | 		return
26 | 	}
27 | 
28 | 	// Create a video element
29 | 	const video = document.createElement('video')
30 | 	video.setAttribute('playsinline', '') // Required to work in iOS 11 & up
31 | 
32 | 	const constraints = {
33 | 		audio: false,
34 | 		video: { facingMode: "user" }
35 | 	}
36 | 
37 | 	navigator.mediaDevices.getUserMedia(constraints).then(function(stream) {
38 | 		if ('srcObject' in video) {
39 | 			video.srcObject = stream
40 | 		} else {
41 | 			video.src = window.URL.createObjectURL(stream)
42 | 		}
43 | 	}).catch(function(err) {
44 | 		let msg = 'No camera available.'
45 | 		if (err.code == 1) msg = 'User denied access to use camera.'
46 | 		console.log(msg);
47 | 		console.error(err)
48 | 	})
49 | 
50 | 	video.addEventListener('loadedmetadata', function() {
51 | 		video.play()
52 | 		if (typeof callback === 'function') callback(video.srcObject)
53 | 	})
54 | 	return video
55 | }
56 | 
```

--------------------------------------------------------------------------------
/src/programs/demos/chromaspiral.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Chroma Spiral
 5 | @desc   Shadertoy port
 6 | Inspired by this shader by scry
 7 | https://www.shadertoy.com/view/tdsyRf
 8 | */
 9 | 
10 | import { map } from '/src/modules/num.js'
11 | import { sort } from '/src/modules/sort.js'
12 | import { vec2, rot, add, mulN, addN, subN, length } from '/src/modules/vec2.js'
13 | 
14 | const { min, sin, cos, floor } = Math
15 | 
16 | const density  = '#Wabc:+-. '
17 | const colors = ['deeppink', 'black', 'red', 'blue', 'orange', 'yellow']
18 | 
19 | export function main(coord, context, cursor, buffer) {
20 | 	const t  = context.time * 0.0002
21 |     const m = min(context.cols, context.rows)
22 |     const a = context.metrics.aspect
23 | 
24 | 	const st = {
25 | 		x : 2.0 * (coord.x - context.cols / 2) / m * a,
26 | 		y : 2.0 * (coord.y - context.rows / 2) / m
27 | 	}
28 | 
29 | 	for (let i=0;i<3;i++) {
30 | 		const o = i * 3
31 | 		const v = vec2(sin(t * 3 + o), cos(t * 2 + o))
32 | 		add(st, v, st)
33 | 
34 | 		const ang = -t + length(subN(st, 0.5))
35 | 		rot(st, ang, st)
36 | 	}
37 | 
38 | 	mulN(st, 0.6, st)
39 | 
40 | 	const s = cos(t) * 2.0
41 | 	let c = sin(st.x * 3.0 + s) + sin(st.y * 21)
42 | 	c = map(sin(c * 0.5), -1, 1, 0, 1)
43 | 
44 | 	const index = floor(c * (density.length - 1))
45 | 	const color = floor(c * (colors.length - 1))
46 | 
47 | 	return {
48 | 		// char : (coord.x + coord.y) % 2 ? density[index] : '╲',
49 | 		char : density[index],
50 | 		color : colors[color]
51 | 	}
52 | }
53 | 
54 | import { drawInfo } from '/src/modules/drawbox.js'
55 | export function post(context, cursor, buffer) {
56 | 	drawInfo(context, cursor, buffer)
57 | }
```

--------------------------------------------------------------------------------
/src/programs/basics/how_to_draw_a_circle.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  How to draw a circle
 5 | @desc   Use of context.metrics.aspect
 6 | */
 7 | 
 8 | import { length } from '/src/modules/vec2.js'
 9 | 
10 | export function main(coord, context, cursor, buffer) {
11 | 
12 |     // contex.metrics.aspect holds the font (or cell) aspect ratio
13 | 	const aspectRatio = cursor.pressed ? 1 : context.metrics.aspect
14 | 
15 | 	// Transform coordinate space to (-1, 1)
16 | 	// width corrected screen aspect (m) and cell aspect (aspectRatio)
17 |     const m = Math.min(context.cols * aspectRatio, context.rows)
18 |     const st = {
19 |         x : 2.0 * (coord.x - context.cols / 2) / m * aspectRatio, // apply aspect
20 |         y : 2.0 * (coord.y - context.rows / 2) / m
21 |     }
22 | 
23 | 	// Distance of each cell from the center (0, 0)
24 | 	const l = length(st)
25 | 
26 | 	// 0.7 is the radius of the circle
27 |     return l < 0.7  ? 'X' : '.'
28 | }
29 | 
30 | // Draw some info
31 | import { drawBox } from '/src/modules/drawbox.js'
32 | export function post(context, cursor, buffer) {
33 | 	// Apply some rounding to the aspect for better output
34 | 	const ar = cursor.pressed ? 1 : (''+context.metrics.aspect).substr(0, 8)
35 | 
36 | 	// Output string
37 | 	let text = ''
38 | 	text += 'Hold the cursor button\n'
39 | 	text += 'to change the aspect ratio:\n'
40 | 	text += 'aspectRatio = ' + ar + '\n'
41 | 
42 | 	// Custom box style
43 | 	const style = {
44 | 		backgroundColor : 'tomato',
45 | 		borderStyle : 'double',
46 | 		shadowStyle : 'gray'
47 | 	}
48 | 
49 | 	// Finally draw the box
50 |     drawBox(text, style, buffer, context.cols, context.rows)
51 | }
52 | 
```

--------------------------------------------------------------------------------
/src/modules/exportframe.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | @module   exportframe.js
 3 | @desc     Exports a single frame (or a range) to an image
 4 | @category public
 5 | 
 6 | Exports a frame as image.
 7 | Expects the canvas renderer as the active renderer.
 8 | Tested on Safari, FF, Chrome
 9 | */
10 | 
11 | import {saveBlobAsFile} from '/src/modules/filedownload.js'
12 | 
13 | export function exportFrame(context, filename, from=1, to=from) {
14 | 
15 | 	// Error: renderer is not canvas.
16 | 	// A renderer instance could be imported here and the content of the buffer
17 | 	// rendere to a tmp canvas… maybe overkill: let’s keep things simple for now.
18 | 	const canvas = context.settings.element
19 | 	if (canvas.nodeName != 'CANVAS') {
20 | 		console.warn('exportframe.js: Can’t export, a canvas renderer is required.')
21 | 		return
22 | 	}
23 | 
24 | 	// Error: filename not provided.
25 | 	// The function doesn’t provide a default name: this operation will probably
26 | 	// flood the “Downloads” folder with images…
27 | 	// It’s probably better to require a user-provided filename at least.
28 | 	if (!filename) {
29 | 		console.warn('exportframe.js: Filename not provided.')
30 | 		return
31 | 	}
32 | 
33 | 	// Filename chunks
34 | 	const m = filename.match(/(.+)\.([0-9a-z]+$)/i)
35 | 	const base = m[1]
36 | 	const ext = m[2]
37 | 
38 | 	// Finally export the frame
39 | 	const f = context.frame
40 | 	if (f >= from && f <= to) {
41 | 		const out = base + '_' + (f.toString().padStart(5, '0')) + '.' + ext
42 | 		console.info('exportframe.js: Exporting frame ' + out + '. Will stop at ' + to + '.')
43 | 		canvas.toBlob( blob => saveBlobAsFile(blob, out))
44 | 	}
45 | }
46 | 
47 | 
```

--------------------------------------------------------------------------------
/src/programs/demos/wobbly.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Wobbly
 5 | @desc   Draw donuts with SDF
 6 | */
 7 | 
 8 | import { sdCircle } from '/src/modules/sdf.js'
 9 | import { sort } from '/src/modules/sort.js'
10 | import { length, rot } from '/src/modules/vec2.js'
11 | import { map, fract, smoothstep } from '/src/modules/num.js'
12 | 
13 | const density = '▀▄▚▐─═0123.+?'
14 | 
15 | export function main(coord, context, cursor, buffer) {
16 | 	const t = context.time * 0.001
17 |     const m = Math.min(context.cols, context.rows)
18 |     const a = context.metrics.aspect
19 | 
20 | 	let st = {
21 | 		x : 2.0 * (coord.x - context.cols / 2) / m * a,
22 | 		y : 2.0 * (coord.y - context.rows / 2) / m
23 | 	}
24 | 
25 | 	st = rot(st, 0.6 * Math.sin(0.62 * t) * length(st) * 2.5)
26 | 	st = rot(st, t * 0.2)
27 | 
28 | 	const s = map(Math.sin(t), -1, 1, 0.5, 1.8)
29 | 	const pt = {
30 | 		x : fract(st.x * s) - 0.5,
31 | 		y : fract(st.y * s) - 0.5
32 | 	}
33 | 
34 | 	const r = 0.5 * Math.sin(0.5 * t + st.x * 0.2) + 0.5
35 | 
36 | 	const d = sdCircle(pt, r)
37 | 
38 | 	const width = 0.05 + 0.3 * Math.sin(t);
39 | 
40 | 	const k = smoothstep(width, width + 0.2, Math.sin(10 * d + t));
41 | 	const c = (1.0 - Math.exp(-3 * Math.abs(d))) * k
42 | 
43 | 	const index = Math.floor(c * (density.length-1))
44 | 
45 | 	return {
46 | 		char  : density[index],
47 | 		color : k == 0 ? 'orangered' : 'royalblue',
48 | 		// backgroundColor : coord.y % 2 ? 'white' : 'cornsilk'
49 | 	}
50 | }
51 | 
52 | import { drawInfo } from '/src/modules/drawbox.js'
53 | export function post(context, cursor, buffer) {
54 | 	drawInfo(context, cursor, buffer, {
55 | 		color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray'
56 | 	})
57 | }
58 | 
```

--------------------------------------------------------------------------------
/src/programs/demos/spiral.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Spiral
 5 | @desc   Shadertoy port
 6 | Inspired by this shader by ahihi
 7 | https://www.shadertoy.com/view/XdSGzR
 8 | */
 9 | 
10 | import { vec2, dot, add, sub, length } from '/src/modules/vec2.js'
11 | import { map } from '/src/modules/num.js'
12 | import { sort } from '/src/modules/sort.js'
13 | 
14 | export const settings = { fps : 60 }
15 | 
16 | const { sin, cos, floor, PI, atan, sqrt, pow } = Math
17 | const TAU = PI * 2
18 | 
19 | const density = sort('▅▃▁?ab012:. ', 'Simple Console', false)
20 | 
21 | export function main(coord, context, cursor, buffer) {
22 | 	const t = context.time * 0.0006
23 |     const m = Math.min(context.cols, context.rows)
24 |     const a = context.metrics.aspect
25 | 
26 | 	let st = {
27 | 		x : 2.0 * (coord.x - context.cols / 2) / m * a,
28 | 		y : 2.0 * (coord.y - context.rows / 2) / m
29 | 	}
30 | 
31 | 	const radius = length(st)
32 | 	const rot = 0.03 * TAU * t
33 | 	const turn = atan(st.y, st.x) / TAU + rot
34 | 
35 | 	const n_sub = 1.5
36 | 
37 | 	const turn_sub = n_sub * turn % n_sub
38 | 
39 | 	const k = 0.1 * sin(3.0 * t)
40 | 	const s = k * sin(50.0 * (pow(radius, 0.1) - 0.4 * t))
41 | 	const turn_sine = turn_sub + s
42 | 
43 | 	const i_turn = floor(density.length * turn_sine % density.length)
44 | 	const i_radius = floor(1.5 / pow(radius * 0.5, 0.6) + 5.0 * t)
45 | 	const idx = (i_turn + i_radius) % density.length
46 | 
47 | 	return density[idx]
48 | }
49 | 
50 | import { drawInfo } from '/src/modules/drawbox.js'
51 | export function post(context, cursor, buffer) {
52 | 	drawInfo(context, cursor, buffer, {
53 | 		color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray'
54 | 	})
55 | }
56 | 
```

--------------------------------------------------------------------------------
/src/programs/demos/numbers.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Numbers
 5 | @desc   Fun with integers
 6 | */
 7 | 
 8 | import { map } from '/src/modules/num.js'
 9 | import { CGA } from '/src/modules/color.js'
10 | 
11 | export const settings = {
12 | 	backgroundColor : 'black'
13 | }
14 | 
15 | // Remove some colors (backwards, in place) from the CGA palette
16 | CGA.splice(10, 1)
17 | //CGA.splice(6, 1)
18 | CGA.splice(4, 1)
19 | CGA.splice(2, 1)
20 | CGA.splice(0, 1)
21 | 
22 | const ints = [
23 | 	488162862,
24 | 	147460255,
25 | 	487657759,
26 | 	1042482734,
27 | 	71662658,
28 | 	1057949230,
29 | 	487540270,
30 | 	1041305872,
31 | 	488064558,
32 | 	488080430
33 | ]
34 | 
35 | const numX = 5     // number width
36 | const numY = 6     // number height
37 | const spacingX = 2 // spacing, after scale
38 | const spacingY = 1
39 | 
40 | const bit = (n, k) => n >> k & 1
41 | 
42 | export function main(coord, context, cursor, buffer) {
43 | 
44 | 	const f = context.frame
45 | 
46 | 	const scale = (map(Math.sin(f * 0.01), -1, 1, 0.99, context.rows / numY))
47 | 
48 | 	const x = coord.x / scale
49 | 	const y = coord.y / scale
50 | 
51 | 	const sx = numX + spacingX / scale
52 | 	const sy = numY + spacingY / scale
53 | 	const cx = Math.floor(x / sx) // cell X
54 | 	const cy = Math.floor(y / sy) // cell Y
55 | 
56 | 	const offs = Math.round(map(Math.sin(f * 0.012 + (cy * 0.5)), -1, 1, 0, 100))
57 | 	const num = (cx + cy + offs) % 10
58 | 
59 | 	const nx = Math.floor(x % sx)
60 | 	const ny = Math.floor(y % sy)
61 | 
62 | 	let char
63 | 
64 | 	if (nx < numX && ny < numY) {
65 | 		char = bit(ints[num], (numX - nx - 1) + (numY - ny - 1) * numX)
66 | 	} else {
67 | 		char = 0
68 | 	}
69 | 
70 | 	let color = num % CGA.length
71 | 	return {
72 | 		char : '.▇'[char],
73 | 		color : char ? CGA[color].hex : CGA[5].hex
74 | 	}
75 | }
76 | 
77 | 
```

--------------------------------------------------------------------------------
/tests/proxy_test.html:
--------------------------------------------------------------------------------

```html
 1 | <!DOCTYPE html>
 2 | <html lang="en" >
 3 | <head>
 4 | 	<meta charset="utf-8">
 5 | 	<title>Proxy test</title>
 6 | </head>
 7 | <body>
 8 | 	<pre>Console output only</pre>
 9 | 	<script type="module">
10 | 
11 | 		const ARR_LEN = 5000
12 | 		const array = []
13 | 		for (let i=0; i<ARR_LEN; i++) {
14 | 			array[i] = i
15 | 		}
16 | 
17 | 		const arrayP = []
18 | 		for (let i=0; i<ARR_LEN; i++) {
19 | 			arrayP[i] = i
20 | 		}
21 | 
22 | 		const proxy = new Proxy(arrayP, {
23 | 			apply: function(target, thisArg, argumentsList) {
24 | 				return thisArg[target].apply(this, argumentList)
25 | 			},
26 | 			// deleteProperty: function(target, property) {
27 | 			// 	// console.log('Deleted ' + property)
28 | 			// 	return true
29 | 			// },
30 | 			set: function(target, property, value, receiver) {
31 | 				if (value == target[property]) {
32 | 					// console.log('Value is identical, not set!')
33 | 					return true
34 | 				}
35 | 				target[property] = value
36 | 				// console.log('Set ' + property + ' to ' + value)
37 | 				return true
38 | 			}
39 | 		})
40 | 
41 | 		// --------------------------------------
42 | 
43 | 		const num = 10000
44 | 
45 | 		const a0 = performance.now()
46 | 
47 | 		for (let i=0; i<num; i++) {
48 | 			const idx = i % ARR_LEN
49 | 			if (array[idx] === i) continue
50 | 			array[idx] = i
51 | 		}
52 | 
53 | 		const a1 = performance.now()
54 | 
55 | 		console.log('Delta a: ' + (a1-a0))
56 | 
57 | 		// --------------------------------------
58 | 
59 | 		const b0 = performance.now()
60 | 
61 | 		for (let i=0; i<num; i++) {
62 | 			// const idx = i % proxy.length
63 | 			const idx = i % ARR_LEN // Much faster than proxy.length!
64 | 			proxy[idx] = i
65 | 		}
66 | 
67 | 		const b1 = performance.now()
68 | 
69 | 		console.log('Delta b: ' + (b1-b0))
70 | 
71 | 
72 | 
73 | 	</script>
74 | </body>
75 | </html>
```

--------------------------------------------------------------------------------
/src/programs/contributed/game_of_life.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author Alex Miller
 4 | @title  GOL
 5 | @desc   Conway's Game of Life
 6 | See https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
 7 | Each frame of the animation depends on the previous frame. Code in the `pre()`
 8 | function saves the previous frame so it can be used in `main()`.
 9 | */
10 | 
11 | import { dist, sub } from '/src/modules/vec2.js'
12 | 
13 | let prevFrame;
14 | let width, height;
15 | 
16 | export function pre(context, cursor, buffer) {
17 | 	if (width != context.cols || height != context.rows) {
18 | 		const length = context.cols * context.rows
19 | 		for (let i = 0; i < length; i++) {
20 | 			buffer[i] = {char : Math.random() > 0.5 ? '▒' : ' '};
21 | 		}
22 | 		width = context.cols;
23 | 		height = context.rows;
24 | 	}
25 | 
26 | 	// Use the spread operator to copy the previous frame
27 | 	// You must make a copy, otherwise `prevFrame` will be updated prematurely
28 | 	prevFrame = [...buffer];
29 | }
30 | 
31 | export function main(coord, context, cursor, buffer) {
32 | 	if (cursor.pressed) {
33 | 		if (dist(coord, cursor) < 3) {
34 | 			return Math.random() > 0.5 ? '▒' : ' ';
35 | 		}
36 | 	}
37 | 
38 | 	const { x, y } = coord;
39 | 	const neighbors =
40 | 		  get(x - 1, y - 1) +
41 | 		  get(x, y - 1) +
42 | 		  get(x + 1, y - 1) +
43 | 		  get(x - 1, y) +
44 | 		  get(x + 1, y) +
45 | 		  get(x - 1, y + 1) +
46 | 		  get(x, y + 1) +
47 | 		  get(x + 1, y + 1);
48 | 
49 | 	const current = get(x, y);
50 | 	if (current == 1) {
51 | 		return neighbors == 2 || neighbors == 3 ? '▒' : ' ';
52 | 	} else if (current == 0) {
53 | 		return neighbors == 3 ? 'x' : ' ';
54 | 	}
55 | }
56 | 
57 | function get(x, y) {
58 | 	if (x < 0 || x >= width) return 0;
59 | 	if (y < 0 || y >= height) return 0;
60 | 	const index = y * width + x;
61 | 	return prevFrame[index].char === ' ' ? 0 : 1
62 | }
63 | 
```

--------------------------------------------------------------------------------
/src/programs/contributed/ernst.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author nkint
 4 | @title  oeö
 5 | @desc   Inspired by Ernst Jandl, 1964
 6 | */
 7 | 
 8 | import { dist, vec2, length, add, mulN } from '/src/modules/vec2.js'
 9 | import { map, smoothstep } from '/src/modules/num.js';
10 | 
11 | const { PI, atan2, floor, cos, max } = Math;
12 | 
13 | function polygon(center, edges, time) {
14 | 	time = time || 0
15 | 	// from https://observablehq.com/@riccardoscalco/draw-regular-polygons-by-means-of-functions
16 | 	const p = center;
17 | 	const N = edges;
18 | 	const a = (atan2(p.x, p.y) + 2 + time * PI) / (2. * PI);
19 | 	const b = (floor(a * N) + 0.5) / N;
20 | 	const c = length(p) * cos((a - b) * 2. * PI);
21 | 	return smoothstep(0.3, 0.31, c);
22 | }
23 | 
24 | export function main(coord, context, cursor, buffer){
25 | 	const m = max(context.cols, context.rows)
26 |     const a = context.metrics.aspect
27 | 
28 | 	const st = {
29 | 		x : 2.0 * (coord.x - context.cols / 2) / m,
30 | 		y : 2.0 * (coord.y - context.rows / 2) / m / a,
31 | 	}
32 | 
33 | 	const centerT = add(
34 | 		st,
35 | 		vec2(0, cos(context.time * 0.0021) * 0.5)
36 | 	);
37 | 	const colorT = polygon(centerT, 3, context.time * 0.0002)
38 | 
39 | 	const triangle = colorT <= 0.1 ? 1 : 0
40 | 
41 | 	const centerQ = add(
42 | 		st,
43 | 		vec2(cos(context.time * 0.0023) * 0.5, 0)
44 | 	);
45 | 	const colorQ = polygon(centerQ, 4, -context.time * 0.0004)
46 | 	const quadrato = colorQ <= 0.1 ? 2 : 0
47 | 	const i = triangle + quadrato;
48 | 	const chars = [' ', 'e', 'o', 'ö']
49 | 
50 | 	const colors = ['white', '#527EA8', '#BB2A1C', '#DFA636']
51 | 	return {
52 | 		char       : chars[i],
53 | 		color      : colors[i],
54 | 		fontWeight : '100'
55 | 	}
56 | }
57 | 
58 | import { drawInfo } from '/src/modules/drawbox.js'
59 | export function post(context, cursor, buffer){
60 | 	drawInfo(context, cursor, buffer)
61 | }
62 | 
63 | 
```

--------------------------------------------------------------------------------
/src/programs/basics/how_to_draw_a_square.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  How to draw a square
 5 | @desc   Draw a square using a distance function
 6 | */
 7 | 
 8 | import { map } from '/src/modules/num.js'
 9 | 
10 | // Set framerate to 60
11 | export const settings = { fps : 60 }
12 | 
13 | // Function to measure a distance to a square
14 | export function box(p, size) {
15 | 	const dx = Math.max(Math.abs(p.x) - size.x, 0)
16 | 	const dy = Math.max(Math.abs(p.y) - size.y, 0)
17 | 	// return the distance from the point
18 | 	return Math.sqrt(dx * dx + dy * dy)
19 | }
20 | 
21 | export function main(coord, context, cursor, buffer) {
22 |     const t = context.time
23 |     const m = Math.min(context.cols, context.rows)
24 |     const a = context.metrics.aspect
25 | 
26 | 	// Normalize space and adjust aspect ratio (screen and char)
27 | 	const st = {
28 |         x : 2.0 * (coord.x - context.cols / 2) / m,
29 |         y : 2.0 * (coord.y - context.rows / 2) / m / a,
30 |     }
31 | 
32 | 	// Transform the coordinate by rotating it
33 | 	const ang = t * 0.0015
34 | 	const s = Math.sin(-ang)
35 |     const c = Math.cos(-ang)
36 | 	const p = {
37 | 		x : st.x * c - st.y * s,
38 |         y : st.x * s + st.y * c
39 | 	}
40 | 
41 | 	// Size of the square
42 | 	const size = map(Math.sin(t * 0.0023), -1, 1, 0.1, 2)
43 | 
44 | 	// Calculate the distance
45 |     const d = box(p, {x:size, y:size})
46 | 
47 | 	// Visualize the distance field
48 | 	return d == 0 ? ' ' : (''+d).charAt(2)
49 | 	// Visualize the distance field and some background
50 | 	// return d == 0 ? (coord.x % 2 == 0 ? '─' : '┼') : (''+d).charAt(2)
51 | }
52 | 
53 | import { drawInfo } from '/src/modules/drawbox.js'
54 | export function post(context, cursor, buffer) {
55 | 	drawInfo(context, cursor, buffer, {
56 | 		color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray'
57 | 	})
58 | }
59 | 
```

--------------------------------------------------------------------------------
/src/programs/demos/plasma.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Plasma
 5 | @desc   Oldschool plasma demo
 6 | Plasma primer: https://www.bidouille.org/prog/plasma
 7 | */
 8 | 
 9 | import { vec2, dot, add, sub, length } from '/src/modules/vec2.js'
10 | import { map } from '/src/modules/num.js'
11 | import { css } from '/src/modules/color.js'
12 | 
13 | export const settings = { fps : 60 }
14 | 
15 | const {sin, cos, floor, PI} = Math
16 | const density = '$?01▄abc+-><:. '
17 | 
18 | const PI23 = PI * 2 / 3
19 | const PI43 = PI * 4 / 3
20 | 
21 | export function main(coord, context, cursor, buffer) {
22 | 	const t1 = context.time * 0.0009
23 | 	const t2 = context.time * 0.0003
24 |     const m = Math.min(context.cols, context.rows)
25 |     const a = context.metrics.aspect
26 | 
27 | 	let st = {
28 | 		x : 2.0 * (coord.x - context.cols / 2) / m * a,
29 | 		y : 2.0 * (coord.y - context.rows / 2) / m
30 | 	}
31 | 	const center = vec2(sin(-t1), cos(-t1))
32 | 	const v1 = sin(dot(coord, vec2(sin(t1), cos(t1))) * 0.08)
33 | 	const v2 = cos(length(sub(st, center)) * 4.0)
34 | 	const v3 = v1 + v2
35 | 	const idx = floor(map(v3, -2, 2, 0, 1) * density.length)
36 | 
37 | 	// Colors are quantized for performance:
38 | 	// lower value = harder gradient, better performance
39 | 	const quant = 2
40 | 	const mult  = 255 / (quant - 1)
41 | 	const r = floor(map(sin(v3 * PI   + t1), -1, 1, 0, quant)) * mult
42 | 	const g = floor(map(sin(v3 * PI23 + t2), -1, 1, 0, quant)) * mult
43 | 	const b = floor(map(sin(v3 * PI43 - t1), -1, 1, 0, quant)) * mult
44 | 
45 | 	return {
46 | 		char : density[idx],
47 | 		color : 'white',
48 | 		backgroundColor : css(r, g, b) // r, g, b are floats
49 | 	}
50 | }
51 | 
52 | import { drawInfo } from '/src/modules/drawbox.js'
53 | export function post(context, cursor, buffer) {
54 | 	drawInfo(context, cursor, buffer)
55 | }
56 | 
```

--------------------------------------------------------------------------------
/src/programs/basics/sequence_export.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Sequence export
 5 | @desc   Export 10 frames as images
 6 | */
 7 | 
 8 | import { exportFrame } from '/src/modules/exportframe.js'
 9 | 
10 | // Important: the frame exporter works only with the canvas renderer.
11 | // Optional: reset the frame count and time at each new run!
12 | export const settings = {
13 | 	renderer : 'canvas',
14 | 	restoreState : false, // Reset time
15 | 	fps : 2 // Slow down: some browsers can’t keep up with high framerates
16 | }
17 | 
18 | // The frame is exported at the beginning of each new pass,
19 | // the canvas still contains the previously rendered image.
20 | // Exported frame “10” will in fact be frame “9” of the loop!
21 | 
22 | export function pre(context, cursor, buffer) {
23 | 	// The filename will be postfixed with the frame number:
24 | 	// export_10.png, export_11.png, etc.
25 | 	// The last two parameters are the start and the end frame
26 | 	// of the sequence to be exported.
27 | 
28 | 	exportFrame(context, 'export.png', 10, 20)
29 | 
30 | 	// The image will (probably) be saved in the “Downloads” folder
31 | 	// and can be assembled into a movie file; for example with FFmpeg:
32 | 	//
33 | 	// > ffmpeg -framerate 30 -pattern_type glob -i "export_*.png" \
34 | 	//        -vcodec h264 -pix_fmt yuv420p \
35 | 	//        -preset:v slow -profile:v baseline -crf 23 export.m4v
36 | }
37 | 
38 | export function main(coord, context, cursor, buffer) {
39 | 	if ((coord.x + coord.y) % 2 != 0) return ' '
40 | 	return (context.frame - 9) % 10
41 | }
42 | 
43 | // Display some info (will also be exported!)
44 | import { drawInfo } from '/src/modules/drawbox.js'
45 | export function post(context, cursor, buffer) {
46 | 	drawInfo(context, cursor, buffer, {
47 | 		color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray'
48 | 	})
49 | }
50 | 
```

--------------------------------------------------------------------------------
/src/programs/sdf/two_circles.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Two circles
 5 | @desc   Smooth union of two circles
 6 | */
 7 | 
 8 | import { sdCircle, opSmoothUnion } from '/src/modules/sdf.js'
 9 | import { sub, vec2 } from '/src/modules/vec2.js'
10 | 
11 | const density = '#WX?*:÷×+=-· '
12 | 
13 | export function main(coord, context, cursor, buffer) {
14 | 	const t = context.time
15 |     const m = Math.min(context.cols, context.rows)
16 |     const a = context.metrics.aspect
17 | 
18 | 	const st = vec2(
19 | 		2.0 * (coord.x - context.cols / 2) / m * a,
20 | 		2.0 * (coord.y - context.rows / 2) / m
21 | 	)
22 | 
23 | 	// A bit of a waste as cursor is not coord dependent;
24 | 	// it could be calculated in pre(), and stored in a global
25 | 	// (see commented code below).
26 | 	const pointer = vec2(
27 | 		2.0 * (cursor.x - context.cols / 2) / m * a,
28 | 		2.0 * (cursor.y - context.rows / 2) / m
29 | 	)
30 | 
31 | 	// Circles
32 | 	const d1 = sdCircle(st, 0.2)	           // origin, 0.2 is the radius
33 | 	const d2 = sdCircle(sub(st, pointer), 0.2) // cursor
34 | 
35 | 	// Smooth operation
36 | 	const d = opSmoothUnion(d1, d2, 0.7)
37 | 
38 | 	// Calc index of the char map
39 | 	const c = 1.0 - Math.exp(-5 * Math.abs(d))
40 | 	const index = Math.floor(c * density.length)
41 | 
42 | 	return density[index]
43 | 
44 | }
45 | 
46 | import { drawInfo } from '/src/modules/drawbox.js'
47 | export function post(context, cursor, buffer) {
48 | 	drawInfo(context, cursor, buffer)
49 | }
50 | 
51 | // Uncomment this to calculate the cursor position only once
52 | // and pass it to the main function as a global
53 | /*
54 | const p = vec2(0, 0)
55 | export function pre(context, cursor, buffer) {
56 |    	const m = Math.min(context.cols, context.rows)
57 |     const a = context.metrics.aspect
58 | 	p.x = 2.0 * (cursor.x - context.cols / 2) / m * a,
59 | 	p.y = 2.0 * (cursor.y - context.rows / 2) / m
60 | }
61 | */
62 | 
```

--------------------------------------------------------------------------------
/src/programs/camera/camera_rgb.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Camera RGB
 5 | @desc   Color input from camera (quantised)
 6 | */
 7 | 
 8 | import { map } from '/src/modules/num.js'
 9 | import { rgb2hex, rgb}  from '/src/modules/color.js'
10 | import Camera from '/src/modules/camera.js'
11 | import Canvas from '/src/modules/canvas.js'
12 | 
13 | const cam = Camera.init()
14 | const can = new Canvas()
15 | // For a debug view uncomment the following line:
16 | // can.display(document.body, 10, 10)
17 | 
18 | const density = ' .+=?X#ABC'
19 | 
20 | // A custom palette used for color quantisation:
21 | const pal = []
22 | pal.push(rgb(  0,   0,   0))
23 | pal.push(rgb(255,   0,   0))
24 | pal.push(rgb(255, 255,   0))
25 | pal.push(rgb(  0, 100, 250))
26 | pal.push(rgb(100, 255, 255))
27 | //pal.push(rgb(255, 182, 193))
28 | //pal.push(rgb(255, 255, 255))
29 | 
30 | const data = []
31 | 
32 | export function pre(context, cursor, buffer) {
33 | 	const a = context.metrics.aspect
34 | 
35 | 	// The canvas is resized so that 1 cell -> 1 pixel
36 | 	can.resize(context.cols, context.rows)
37 | 	// The cover() function draws an image (cam) to the canvas covering
38 | 	// the whole frame. The aspect ratio can be adjusted with the second
39 | 	// parameter.
40 | 	can.cover(cam, a).mirrorX().quantize(pal).writeTo(data)
41 | }
42 | 
43 | export function main(coord, context, cursor, buffer) {
44 | 	// Coord also contains the index of each cell
45 | 	const color = data[coord.index]
46 | 	// Add some chars to the output
47 | 	const index = Math.floor(color.v * (density.length-1))
48 | 	return {
49 | 		char       : density[index],
50 | 		color      : 'white',
51 | 		// convert {r,g,b} obj to a valid CSS hex string
52 | 		backgroundColor : rgb2hex(color)
53 | 	}
54 | }
55 | 
56 | import { drawInfo } from '/src/modules/drawbox.js'
57 | export function post(context, cursor, buffer) {
58 | 	drawInfo(context, cursor, buffer)
59 | }
60 | 
```

--------------------------------------------------------------------------------
/src/programs/demos/box_fun.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Box fun
 5 | @desc   Think inside of the box
 6 | */
 7 | 
 8 | import { clamp, map } from '/src/modules/num.js'
 9 | 
10 | const {sin, cos, floor} = Math
11 | 
12 | export function main(coord, context, cursor, buffer) {
13 | 	// Basic background pattern
14 | 	return (coord.x + coord.y) % 2 ? '·' : ' '
15 | }
16 | 
17 | import { drawBox } from '/src/modules/drawbox.js'
18 | export function post(context, cursor, buffer) {
19 | 	const { rows, cols } =  context
20 | 	const t = context.time * 0.002
21 | 	const baseW = 15
22 | 	const baseH = 5
23 | 	const spacingX = 4
24 | 	const spacingY = 2
25 | 
26 | 	let marginX = 3
27 | 	let marginY = 2
28 | 
29 | 	const numX = floor((cols - marginX*2) / (baseW+spacingX))
30 | 	const numY = floor((rows - marginY*2) / (baseH+spacingY))
31 | 
32 | 	marginX = floor((cols - numX * baseW - (numX-1) * spacingX)/2)
33 | 	marginY = floor((rows - numY * baseH - (numY-1) * spacingY)/2)
34 | 
35 | 	const q = 'THINK INSIDE OF THE BOX'
36 | 
37 | 	const baseStyle = {
38 | 		paddingX        : 2,
39 | 		paddingY        : 1,
40 | 		color           : 'black',
41 | 		backgroundColor : 'white',
42 | 		borderStyle     : 'double',
43 | 		shadowStyle     : 'light',
44 | 	}
45 | 
46 | 	for (let j=0; j<numY; j++) {
47 | 		for (let i=0; i<numX; i++) {
48 | 			const ox = floor(sin((i + j) * 0.6 + t*3) * spacingX)
49 | 			const oy = floor(cos((i + j) * 0.6 + t*3) * spacingY)
50 | 			const ow = 0//floor(sin((i + j) * 0.4 + t*2) * 5) + 5
51 | 			const oh = 0//floor(cos((i + j) * 0.4 + t*2) * 2) + 2
52 | 			const style = {
53 | 				x      : marginX + i * (baseW + spacingX) + ox,
54 | 				y      : marginY + j * (baseH + spacingY) + oy,
55 | 				width  : baseW + ow,
56 | 				height : baseH + oh,
57 | 				...baseStyle
58 | 			}
59 | 			let txt = ''
60 | 			txt += `*${q[(i + j * numX) % q.length]}*\n`
61 | 			txt += `pos: ${style.x}×${style.y}\n`
62 | 
63 | 			drawBox(txt, style, buffer, cols, rows)
64 | 		}
65 | 	}
66 | }
67 | 
```

--------------------------------------------------------------------------------
/src/programs/demos/moire_explorer.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Moiré explorer
 5 | @desc   Click or tap to toggle mode
 6 | */
 7 | 
 8 | import { vec2, dist, mulN } from '/src/modules/vec2.js'
 9 | import { map } from '/src/modules/num.js'
10 | 
11 | export const settings = { fps : 60 }
12 | 
13 | // Shorthands
14 | const { sin, cos, atan2, floor, min } = Math
15 | 
16 | // Change the mouse pointer to 'pointer'
17 | export const boot = (context) => context.settings.element.style.cursor = 'pointer'
18 | 
19 | // Cycle modes with click or tap
20 | export const pointerDown = () => mode = ++mode % 3
21 | let mode = 0
22 | 
23 | const density = ' ..._-:=+abcXW@#ÑÑÑ'
24 | 
25 | export function main(coord, context, cursor) {
26 | 	const t = context.time * 0.0001
27 | 	const m = min(context.cols, context.rows)
28 | 	const st = {
29 | 		x : 2.0 * (coord.x - context.cols / 2) / m,
30 | 		y : 2.0 * (coord.y - context.rows / 2) / m,
31 | 	}
32 | 	st.x *= context.metrics.aspect
33 | 
34 | 	const centerA = mulN(vec2(cos(t*3), sin(t*7)), 0.5)
35 | 	const centerB = mulN(vec2(cos(t*5), sin(t*4)), 0.5)
36 | 
37 | 	// Set A or B to zero to see only one of the two frequencies
38 | 	const A = mode % 2 == 0 ? atan2(centerA.y-st.y, centerA.x-st.x) : dist(st, centerA)
39 | 	const B = mode     == 0 ? atan2(centerB.y-st.y, centerB.x-st.x) : dist(st, centerB)
40 | 
41 | 	const aMod = map(cos(t*2.12), -1, 1, 6, 60)
42 | 	const bMod = map(cos(t*3.33), -1, 1, 6, 60)
43 | 	//const aMod = 27
44 | 	//const bMod = 29
45 | 
46 | 	const a = cos(A * aMod)
47 | 	const b = cos(B * bMod)
48 | 
49 | 	const i = ((a * b) + 1) / 2 // mult
50 | 	//const i = ((a + b) + 2) / 4 // sum
51 | 	const idx = floor(i * density.length)
52 | 	return density[idx]
53 | }
54 | 
55 | import { drawInfo } from '/src/modules/drawbox.js'
56 | export function post(context, cursor, buffer) {
57 | 	drawInfo(context, cursor, buffer, {
58 | 		color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray'
59 | 	})
60 | }
61 | 
```

--------------------------------------------------------------------------------
/src/programs/list.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | 
 3 | # Script to generate a list of all the projects,
 4 | # will be inserted in the manual page
 5 | 
 6 | # Trims a string
 7 | function trim {
 8 | 	local var="$*"
 9 | 	# remove leading whitespace characters
10 | 	var="${var#"${var%%[![:space:]]*}"}"
11 | 	# remove trailing whitespace characters
12 | 	var="${var%"${var##*[![:space:]]}"}"
13 | 	printf '%s' "$var"
14 | }
15 | 
16 | SCRIPT_PATH=${0%/*}    # path to this script
17 | 
18 | URL_PREFIX='/#/src'
19 | 
20 | NUM_COLS=${1:-3}
21 | 
22 | 
23 | # All folders
24 | # FOLDERS=`find ./ -mindepth 1 -type d`
25 | 
26 | # Odered list of folders
27 | FOLDERS=(basics sdf demos camera contributed)
28 | 
29 | for folder in ${FOLDERS[@]}; do
30 | 
31 | 	FIRST=1
32 | 
33 | 	for file in $SCRIPT_PATH/$folder/*.js; do
34 | 		# The path in $file is the full path:
35 | 		# ./play.core/src/programs/[folder]/[file].js
36 | 		URL=$URL_PREFIX$(echo $file | sed -e 's/\.\///' -e 's/\.js//' -e 's/\.play\.core\/src\/programs//')
37 | 		AUTHOR=$(sed -En 's/^@author[ \t](.*)$/\1/p' $file)
38 | 		TITLE=$(sed -En 's/^@title[ \t](.*)$/\1/p' $file)
39 | 		DESC=$(sed -En 's/^@desc[ \t](.*)$/\1/p' $file)
40 | 
41 | 		if [[ $FIRST == 1 ]]; then
42 | 			FOLDER=$(echo $folder | sed -e 's/\.\///' -e 's/\///' -e 's/\.js//' )
43 | 			FIRST=0
44 | 			printf "\t"
45 | 			printf "<div class='program-cathegory'>"
46 | 			printf "$(trim $FOLDER)"
47 | 			printf "</div>"
48 | 			printf "\n"
49 | 		else
50 | 			if [[ $NUM_COLS == 3 ]]; then
51 | 				printf "\t"
52 | 				printf "<div>"
53 | 				printf "</div>"
54 | 				printf "\n"
55 | 			fi
56 | 		fi
57 | 
58 | 		printf "\t"
59 | 		printf "<div>"
60 | 		if [[ $NUM_COLS == 3 ]]; then
61 | 			printf "<a target='_blank' href='$URL'>$(trim $TITLE)</a>"
62 | 		else
63 | 			printf "<a href='$URL'>$(trim $TITLE)</a>"
64 | 		fi
65 | 		printf "</div>"
66 | 		printf "\n"
67 | 
68 | 		if [[ $NUM_COLS == 3 ]]; then
69 | 			printf "\t"
70 | 			printf "<div>"
71 | 			printf "$(trim $DESC)"
72 | 			printf "</div>"
73 | 			printf "\n"
74 | 		fi
75 | 	done
76 | done
77 | 
```

--------------------------------------------------------------------------------
/src/modules/string.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | @module   string.js
 3 | @desc     String helpers
 4 | @category internal
 5 | 
 6 | Wraps a string to a specific width.
 7 | Doesn’t break words and keeps trailing line breaks.
 8 | Counts lines and maxWidth (can be greater than width).
 9 | If no width is passed the function just measures the 'box' of the text.
10 | */
11 | 
12 | export function wrap(string, width=0) {
13 | 
14 | 	if (width==0) return measure(string)
15 | 
16 | 	const paragraphs = string.split('\n')
17 | 	let out = ''
18 | 
19 | 	let maxWidth = 0
20 | 	let numLines = 0
21 | 
22 | 	for (const p of paragraphs) {
23 | 		const chunks = p.split(' ')
24 | 		let len = 0
25 | 		for(const word of chunks) {
26 | 			// First word
27 | 			if (len == 0) {
28 | 				out += word
29 | 				len = word.length
30 | 				maxWidth = Math.max(maxWidth, len)
31 | 			}
32 | 			// Subsequent words
33 | 			else {
34 | 				if (len + 1 + word.length <= width) {
35 | 					out += ' ' + word
36 | 					len += word.length + 1
37 | 					maxWidth = Math.max(maxWidth, len)
38 | 				} else {
39 | 					// Remove last space
40 | 					out += '\n' + word
41 | 					len = word.length + 1
42 | 					numLines++
43 | 				}
44 | 			}
45 | 		}
46 | 		out += '\n'
47 | 		numLines++
48 | 	}
49 | 
50 | 	// Remove last \n
51 | 	out = out.slice(0, -1)
52 | 
53 | 	// Adjust line count in case of last trailing \n
54 | 	if (out.charAt(out.length-1) == '\n') numLines--
55 | 
56 | 	return {
57 | 		text : out,
58 | 		numLines,
59 | 		maxWidth
60 | 	}
61 | }
62 | 
63 | // TODO: fix bug for 1 line str, will return numLines = 0)
64 | /*
65 | export function measure(string) {
66 | 	const chunks = string.split('\n')
67 | 	return {
68 | 		text     : string,
69 | 		numLines : chunks.length,
70 | 		maxWidth : Math.max( chunks.map( e => e.length) )
71 | 	}
72 | } 
73 | */
74 | 
75 | 
76 | export function measure(string) {
77 | 	let numLines = 0
78 | 	let maxWidth = 0
79 | 	let len = 0
80 | 
81 | 	for (let i=0; i<string.length; i++) {
82 | 		const char = string[i]
83 | 		if (char == '\n') {
84 | 			len = 0
85 | 			numLines++
86 | 		} else {
87 | 			len++
88 | 			maxWidth = Math.max(maxWidth, len)
89 | 		}
90 | 	}
91 | 	return {
92 | 		text : string,
93 | 		numLines,
94 | 		maxWidth
95 | 	}
96 | }
97 | 
```

--------------------------------------------------------------------------------
/src/modules/sort.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | @module   sort.js
 3 | @desc     Sorts a set of characters by brightness
 4 | @category public
 5 | 
 6 | Paints chars on a temporary canvas and counts the pixels.
 7 | This could be done once and then stored / hardcoded.
 8 | The fontFamily paramter needs to be set because it's used by the canvas element
 9 | to draw the correct font.
10 | */
11 | 
12 | export function sort(charSet, fontFamily, ascending = false) {
13 | 
14 | 	const size = 30
15 | 	const ctx = document.createElement('canvas').getContext('2d')
16 | 	ctx.canvas.width = size * 2
17 | 	ctx.canvas.height = size * 2
18 | 
19 | 	ctx.canvas.style.right = '0'
20 | 	ctx.canvas.style.top = '0'
21 | 	ctx.canvas.style.position = 'absolute'
22 | 	document.body.appendChild(ctx.canvas)  // NOTE: needs to be attached to the DOM
23 | 
24 | 	if (ctx.getImageData(0, 0, 1, 1).data.length == 0) {
25 | 		console.warn("getImageData() is not supported on this browser.\nCan’t sort chars for brightness.")
26 | 		return charSet
27 | 	}
28 | 
29 | 	let out = []
30 | 	for (let i=0; i<charSet.length; i++) {
31 | 		ctx.fillStyle = 'black'
32 | 		ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height)
33 | 		ctx.fillStyle = 'rgb(255,255,255)'
34 | 		ctx.font = size + 'px ' + fontFamily  // NOTE: font family inherit doesn't work
35 | 		ctx.textAlign = 'center'
36 | 		ctx.textBaseline = 'middle'
37 | 		ctx.fillText(charSet[i], ctx.canvas.width / 2, ctx.canvas.height / 2)
38 | 
39 | 		out[i] = {
40 | 			count : 0,
41 | 			char  : charSet[i],
42 | 			index : i,
43 | 		}
44 | 
45 | 		const data = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data
46 | 
47 | 		for (let y=0; y<ctx.canvas.height; y++) {
48 | 			const oy = y * ctx.canvas.width
49 | 			for (let x=0; x<ctx.canvas.width; x++) {
50 | 				let r = data[4 * (x + oy)]
51 | 				out[i].count += r
52 | 			}
53 | 		}
54 | 		//console.log(out[i].char, out[i].count)
55 | 	}
56 | 
57 | 	// cleanup
58 | 	document.body.removeChild(ctx.canvas)
59 | 	if (ascending) {
60 | 		return out.sort((a, b) => a.count - b.count).map( x => x.char).join('')
61 | 	} else {
62 | 		return out.sort((a, b) => b.count - a.count).map( x => x.char).join('')
63 | 	}
64 | }
65 | 
```

--------------------------------------------------------------------------------
/src/programs/contributed/equal_tea_talk.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 | [header]
  3 | @author nkint
  4 | @title  EQUAL TEA TALK, #65
  5 | @desc   Inspired by Frederick Hammersley, 1969
  6 | See: http://www.hammersleyfoundation.org/index.php/artwork/computer-drawings/184-computer-drawings/331-equal-tea-talk
  7 | */
  8 | 
  9 | import { map, step, mod } from '/src/modules/num.js'
 10 | import { vec2, mul, fract as fract2 } from '/src/modules/vec2.js'
 11 | 
 12 | const chars = '#BEFTI_'.split('')
 13 | 
 14 | const wx = new Array(50).fill(0).map((_, i) => i);
 15 | const sumW = (n) => (n * 2 + 3) * 5
 16 | const getW = (cols) => {
 17 | 	let w = 0
 18 | 	for(let _wx of wx) {
 19 | 		const s = sumW(_wx)
 20 | 		if (cols <= s) {
 21 | 			break;
 22 | 		}
 23 | 		w = s;
 24 | 	}
 25 | 	return w
 26 | }
 27 | 
 28 | const blank =  ' ';
 29 | 
 30 | export const settings = {
 31 | 	fontWeight: '100'
 32 | }
 33 | 
 34 | export function main(coord, context, cursor, buffer){
 35 | 	const t = context.time * 0.0001
 36 | 
 37 | 	// compute resolution to fit geometry, not the screen
 38 | 	// 🙏thanks andreas
 39 | 	let w = getW(context.cols);
 40 | 
 41 | 	// show blank if the resolution is too small
 42 | 	if(context.cols <= sumW(1)) {
 43 | 		return blank
 44 | 	}
 45 | 
 46 | 	// clamp the space to the resolution
 47 | 	if(coord.x >= w) {
 48 | 		return blank
 49 | 	}
 50 | 
 51 | 	// ----------------------------- st space
 52 | 	// -----------------------------
 53 | 	// -----------------------------
 54 | 	const st = {
 55 | 		x : (coord.x / w),
 56 | 		y : (coord.y / context.rows),
 57 | 	}
 58 | 	const _st =  mul(st, vec2(5.0, 1.0));
 59 | 	const tileIndex = step(1, mod(_st.x, 2.0));
 60 | 	// make each cell between 0.0 - 1.0
 61 | 	// see Truchet Tiles from https://thebookofshaders.com/09/
 62 | 	// for reference/inspiration
 63 | 	const __st = fract2(_st);
 64 | 
 65 | 	const color = tileIndex === 0 ? (_st.y) : (1 - _st.y);
 66 | 	const i = Math.floor(
 67 | 		map(
 68 | 			color,
 69 | 			0, 1,
 70 | 			0, chars.length - 1
 71 | 		)
 72 | 	);
 73 | 
 74 | 	let char = chars[i];
 75 | 
 76 | 	// ----------------------------- pixel space
 77 | 	// -----------------------------
 78 | 	// -----------------------------
 79 | 	const unit = w / 5
 80 | 	const middle = Math.floor(unit/2) - 1;
 81 | 	const xPx = coord.x % unit
 82 | 
 83 | 	const isFirstOrLast = xPx !==0 && xPx !== unit
 84 | 
 85 | 	if(
 86 | 		isFirstOrLast &&
 87 | 		(((xPx) === middle) || ((xPx) === middle + 2))
 88 | 	) {
 89 | 		char = ' '
 90 | 	}
 91 | 
 92 | 	if(
 93 | 		isFirstOrLast &&
 94 | 		(xPx) === middle + 1
 95 | 	) {
 96 | 		char = '-'
 97 | 	}
 98 | 
 99 | 	// some debug utils
100 | 	// throw new Error('dudee')
101 | 	// if(coord.x === 0) { console.log() }
102 | 
103 | 	return char
104 | }
105 | 
```

--------------------------------------------------------------------------------
/src/programs/demos/donut.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 | [header]
 3 | @author ertdfgcvb
 4 | @title  Donut
 5 | @desc   Ported from a1k0n’s donut demo.
 6 | https://www.a1k0n.net/2011/07/20/donut-math.html
 7 | 
 8 | This program writes directly into the frame buffer
 9 | in a sort of 'brute force' way:
10 | theta and phi (below) must be small enough to fill
11 | all the gaps.
12 | */
13 | 
14 | export const settings = { backgroundColor : 'whitesmoke' }
15 | 
16 | export function pre(context, cursor, buffer) {
17 | 
18 | 	const TAU = Math.PI * 2
19 | 
20 | 	const z = []
21 | 	const A = context.time * 0.0015
22 | 	const B = context.time * 0.0017
23 | 
24 | 	const width  = context.cols
25 | 	const height = context.rows
26 | 
27 | 	const centerX = width / 2
28 | 	const centerY = height / 2
29 | 	const scaleX  = 50
30 | 	const scaleY  = scaleX * context.metrics.aspect
31 | 
32 | 	// Precompute sines and cosines of A and B
33 | 	const cA = Math.cos(A)
34 | 	const sA = Math.sin(A)
35 | 	const cB = Math.cos(B)
36 | 	const sB = Math.sin(B)
37 | 
38 | 	// Clear the buffers
39 | 	const num = width * height
40 | 	for(let k=0; k<num; k++) {
41 | 		buffer[k].char = ' ' // char buffer
42 | 		z[k] = 0             // z buffer
43 | 	}
44 | 
45 | 	// Theta (j) goes around the cross-sectional circle of a torus
46 | 	for(let j=0; j<TAU; j+=0.05) {
47 | 
48 | 		// Precompute sines and cosines of theta
49 | 		const ct = Math.cos(j)
50 | 		const st = Math.sin(j)
51 | 
52 | 		// Phi (i) goes around the center of revolution of a torus
53 | 		for(let i=0; i<TAU; i+=0.01) {
54 | 
55 | 			// Precompute sines and cosines of phi
56 | 			const sp = Math.sin(i)
57 | 			const cp = Math.cos(i)
58 | 
59 | 			// The x,y coordinate of the circle, before revolving
60 | 			const h = ct+2                // R1 + R2*cos(theta)
61 | 			const D = 1/(sp*h*sA+st*cA+5) // this is 1/z
62 | 			const t = sp*h*cA-st*sA
63 | 
64 | 			// Final 3D (x,y,z) coordinate after rotations
65 | 			const x = 0 | (centerX + scaleX*D*(cp*h*cB-t*sB))
66 | 			const y = 0 | (centerY + scaleY*D*(cp*h*sB+t*cB))
67 | 			const o = x + width * y
68 | 
69 | 			// Range 0..11 (8 * sqrt(2) = 11.3)
70 | 			const N = 0 | (8*((st*sA-sp*ct*cA)*cB-sp*ct*sA-st*cA-cp*ct*sB))
71 | 			if(y<height && y>=0 && x>=0 && x<width && D>z[o]) {
72 | 				z[o] = D
73 | 				buffer[o].char = '.,-~:;=!*#$@'[N > 0 ? N : 0]
74 | 			}
75 | 		}
76 | 	}
77 | }
78 | 
79 | import { drawInfo } from '/src/modules/drawbox.js'
80 | export function post(context, cursor, buffer) {
81 | 	drawInfo(context, cursor, buffer, {
82 | 		color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray'
83 | 	})
84 | }
85 | 
```

--------------------------------------------------------------------------------
/tests/browser_bugs/font_ready_bug.html:
--------------------------------------------------------------------------------

```html
 1 | <!DOCTYPE html>
 2 | <html lang="en" >
 3 | <head>
 4 | 	<meta charset="utf-8">
 5 | 	<title>fonts.ready bug</title>
 6 | 	<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@100&display=swap" rel="stylesheet">
 7 | 	<!-- alternative load:
 8 | 	<style>@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@100&display=swap');</style>
 9 | 	-->
10 | 	<style type="text/css" media="screen">
11 | 		span {
12 | 			/*font-family: monospace;*/
13 | 			/*font-family: monospace, monospace;*/
14 | 			font-family: 'Roboto Mono', monospace;
15 | 		}
16 | 	</style>
17 | </head>
18 | <body>
19 | 
20 | 	Console output only.<br>
21 | 	<a href="https://bugs.webkit.org/show_bug.cgi?id=217047">bugs.webkit.org/show_bug.cgi?id=217047</a>
22 | 
23 | 	<!--
24 | 	***************************************
25 | 	Safari / WebKit
26 | 	***************************************
27 | 
28 | 	This test measures the width of a span element filled with 100 'X'
29 | 	It needs to be run twice:
30 | 
31 | 	RUN 1)
32 | 	Empty the caches (CMD-OPT-E) and reload the page
33 | 	The output in the console is something similar:
34 | 	a 1155.47   <--  what happens here?
35 | 	                 maybe the monospace, monospace issue?
36 | 	                 https://stackoverflow.com/questions/38781089/font-family-monospace-monospace
37 | 	b 963.28    <--  incorect size! (this is the size of 'monospace' and not 'Roboto Mono')
38 | 
39 | 	RUN 2)
40 | 	Reload the page, the output is now:
41 | 	a 960.16    <--  correct size (on second reload the fonts are probably cached)
42 | 	b 960.16    <--  correct size (finally the expected value)
43 | 
44 | 
45 | 	There are two VERY strange fixes for this issue:
46 | 
47 | 	FIX A)
48 | 	remove type="module" from the <script> tag
49 | 
50 | 	FIX B)
51 | 	comment line 67 (leave the test element displayed)
52 | 	-->
53 | 
54 | 	<script type="module">
55 | 
56 | 		console.log('a ' + calcWidth().toFixed(2))
57 | 
58 | 		document.fonts.ready.then(()=>{
59 | 			console.log('b ' +  calcWidth().toFixed(2))
60 | 		})
61 | 
62 | 		function calcWidth(el) {
63 | 
64 | 			const test = document.createElement('span')
65 | 			document.body.appendChild(test) // Must be visible!
66 | 
67 | 			test.innerHTML = ''.padStart(100, 'X')
68 | 			const w = test.getBoundingClientRect().width
69 | 			test.innerHTML += '<br>Measured width: ' + w.toFixed(2) + '<br>'
70 | 
71 | 			document.body.removeChild(test) // If this line is commented the measure works on the first run!
72 | 
73 | 			return w
74 | 		}
75 | 
76 | 	</script>
77 | </body>
78 | </html
```

--------------------------------------------------------------------------------
/src/programs/sdf/cube.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 | [header]
  3 | @author ertdfgcvb
  4 | @title  Wireframe cube
  5 | @desc   The cursor controls box thickness and exp
  6 | */
  7 | 
  8 | import { sdSegment } from '/src/modules/sdf.js'
  9 | import * as v2 from '/src/modules/vec2.js'
 10 | import * as v3 from '/src/modules/vec3.js'
 11 | import { map } from '/src/modules/num.js'
 12 | 
 13 | export const settings = { fps : 60 }
 14 | 
 15 | const density = ' -=+abcdX'
 16 | 
 17 | // Shorthands
 18 | const { vec3 } = v3
 19 | const { vec2 } = v2
 20 | const { sin, cos, floor, abs, exp, min } = Math
 21 | 
 22 | // Lookup table for the background
 23 | const bgMatrix = [
 24 | '┼──────',
 25 | '│      ',
 26 | '│      ',
 27 | '│      ',
 28 | '│      ',
 29 | '│      ',
 30 | ]
 31 | 
 32 | // Box primitive
 33 | const l = 0.6
 34 | const box = {
 35 | 	vertices : [
 36 | 		vec3( l, l, l),
 37 | 		vec3(-l, l, l),
 38 | 		vec3(-l,-l, l),
 39 | 		vec3( l,-l, l),
 40 | 		vec3( l, l,-l),
 41 | 		vec3(-l, l,-l),
 42 | 		vec3(-l,-l,-l),
 43 | 		vec3( l,-l,-l)
 44 | 	],
 45 | 	edges : [
 46 | 		[0, 1],
 47 | 		[1, 2],
 48 | 		[2, 3],
 49 | 		[3, 0],
 50 | 		[4, 5],
 51 | 		[5, 6],
 52 | 		[6, 7],
 53 | 		[7, 4],
 54 | 		[0, 4],
 55 | 		[1, 5],
 56 | 		[2, 6],
 57 | 		[3, 7]
 58 | 	]
 59 | }
 60 | 
 61 | const boxProj = []
 62 | 
 63 | const bgMatrixDim = vec2(bgMatrix[0].length, bgMatrix.length)
 64 | 
 65 | export function pre(context, cursor) {
 66 | 	const t = context.time * 0.01
 67 | 	const rot = vec3(t * 0.11, t * 0.13, -t * 0.15)
 68 | 	const d = 2
 69 | 	const zOffs = map(sin(t*0.12), -1, 1, -2.5, -6)
 70 | 	for (let i=0; i<box.vertices.length; i++) {
 71 | 		const v = v3.copy(box.vertices[i])
 72 | 		let vt = v3.rotX(v, rot.x)
 73 | 		vt = v3.rotY(vt, rot.y)
 74 | 		vt = v3.rotZ(vt, rot.z)
 75 | 		boxProj[i] = v2.mulN(vec2(vt.x, vt.y), d / (vt.z - zOffs))
 76 | 	}
 77 | }
 78 | 
 79 | export function main(coord, context, cursor) {
 80 | 	const t = context.time * 0.01
 81 | 	const m = min(context.cols, context.rows)
 82 |     const a = context.metrics.aspect
 83 | 
 84 | 	const st = {
 85 | 		x : 2.0 * (coord.x - context.cols / 2 + 0.5) / m * a,
 86 | 		y : 2.0 * (coord.y - context.rows / 2 + 0.5) / m,
 87 | 	}
 88 | 
 89 | 	let d = 1e10
 90 | 	const n = box.edges.length
 91 | 	const thickness = map(cursor.x, 0, context.cols, 0.001, 0.1)
 92 | 	const expMul = map(cursor.y, 0, context.rows, -100, -5)
 93 | 	for (let i=0; i<n; i++) {
 94 | 		const a = boxProj[box.edges[i][0]]
 95 | 		const b = boxProj[box.edges[i][1]]
 96 | 		d = min(d, sdSegment(st, a, b, thickness))
 97 | 	}
 98 | 
 99 | 	const idx = floor(exp(expMul * abs(d)) * density.length)
100 | 
101 | 	if (idx == 0) {
102 | 		const x = coord.x % bgMatrixDim.x
103 | 		const y = coord.y % bgMatrixDim.y
104 | 		return {
105 | 			char  : d < 0 ? ' ' : bgMatrix[y][x],
106 | 			color : 'black'
107 | 		}
108 | 	} else {
109 | 		return {
110 | 			char  : density[idx],
111 | 			color : 'royalblue'
112 | 		}
113 | 	}
114 | }
115 | 
116 | import { drawInfo } from '/src/modules/drawbox.js'
117 | export function post(context, cursor, buffer) {
118 | 	drawInfo(context, cursor, buffer)
119 | }
120 | 
121 | 
```

--------------------------------------------------------------------------------
/src/programs/contributed/sand_game.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 | [header]
  3 | @author Alex Miller
  4 | @title  Sand game
  5 | @desc   Click to drop sand
  6 | */
  7 | 
  8 | import { dist, sub } from '/src/modules/vec2.js'
  9 | 
 10 | let prevFrame;
 11 | let width, height;
 12 | 
 13 | function newParticle() {
 14 | 	return 'sand'.charAt(Math.random() * 4)
 15 | }
 16 | 
 17 | export function pre(context, cursor, buffer) {
 18 | 	if (width != context.cols || height != context.rows) {
 19 | 		const length = context.cols * context.rows
 20 | 		for (let i = 0; i < length; i++) {
 21 | 			buffer[i] = {char : Math.random() > 0.5 ? newParticle() : ' '};
 22 | 		}
 23 | 		width = context.cols;
 24 | 		height = context.rows;
 25 | 	}
 26 | 
 27 | 	// Use the spread operator to copy the previous frame
 28 | 	// You must make a copy, otherwise `prevFrame` will be updated prematurely
 29 | 	prevFrame = [...buffer];
 30 | }
 31 | 
 32 | export function main(coord, context, cursor, buffer) {
 33 | 	if (cursor.pressed) {
 34 | 		if (dist(coord, cursor) < 3) {
 35 | 			return {
 36 | 				char: Math.random() > 0.5 ? newParticle() : ' ',
 37 | 
 38 | 				backgroundColor: 'white',
 39 | 				color: 'rgb(179, 158, 124)',
 40 | 				fontWeight: 500
 41 | 			}
 42 | 		}
 43 | 	}
 44 | 
 45 | 	const { x, y } = coord;
 46 | 
 47 | 	const me = get(x, y);
 48 | 	const below = get(x, y + 1);
 49 | 	const above = get(x, y - 1);
 50 | 
 51 | 	const left = get(x - 1, y);
 52 | 	const right = get(x + 1, y);
 53 | 
 54 | 	const topleft = get(x - 1, y - 1);
 55 | 	const topright = get(x + 1, y - 1);
 56 | 	const bottomleft = get(x - 1, y + 1);
 57 | 	const bottomright = get(x + 1, y + 1);
 58 | 	const frame = context.frame;
 59 | 
 60 | 
 61 | 	if (y >= context.rows - 1) {
 62 | 		return {
 63 | 			char: 'GROUND_'.charAt(x % 7),
 64 | 			backgroundColor: 'rgb(138, 162, 70)',
 65 | 			color: 'rgb(211, 231, 151)',
 66 | 			fontWeight: 700
 67 | 		}
 68 | 	}
 69 | 
 70 | 	if (x >= context.cols - 1 || x <= 0) {
 71 | 		return {
 72 | 			char: 'WALL'.charAt(y % 4),
 73 | 			backgroundColor: 'rgb(247, 187, 39)',
 74 | 			color: 'white',
 75 | 			fontWeight: 700
 76 | 		}
 77 | 	}
 78 | 
 79 | 	let char = ' '
 80 | 	if (alive(me)) {
 81 | 		if (alive(below) &&
 82 | 			((frame % 2 == 0 && alive(bottomright)) ||
 83 | 			(frame % 2 == 1 && alive(bottomleft)))) {
 84 | 			char = me;
 85 | 		} else {
 86 | 			char = ' ';
 87 | 		}
 88 | 	} else {
 89 | 		if (alive(above)) {
 90 | 			char = above;
 91 | 		} else if (alive(left) && frame % 2 == 0 && alive(topleft)) {
 92 | 			char = topleft;
 93 | 		} else if (alive(right) && frame % 2 == 1 && alive(topright)) {
 94 | 			char = topright;
 95 | 		} else {
 96 | 			char = ' ';
 97 | 		}
 98 | 
 99 | 
100 | 		if ('WALL'.indexOf(char) > -1) {
101 | 			char = ' ';
102 | 		}
103 | 	}
104 | 
105 | 	return {
106 | 		char,
107 | 		backgroundColor: 'white',
108 | 		color: 'rgb(179, 158, 124)',
109 | 		fontWeight: 500
110 | 	}
111 | }
112 | 
113 | function alive(char) {
114 | 	return char != ' ';
115 | }
116 | 
117 | function get(x, y) {
118 | 	if (x < 0 || x >= width) return 0;
119 | 	if (y < 0 || y >= height) return 0;
120 | 	const index = y * width + x;
121 | 	return prevFrame[index].char;
122 | }
123 | 
124 | 
```

--------------------------------------------------------------------------------
/src/programs/demos/dyna.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 | [header]
  3 | @author ertdfgcvb
  4 | @title  Dyna
  5 | @desc   A remix of Paul Haeberli’s Dynadraw
  6 | 
  7 | The original from 1989:
  8 | http://www.graficaobscura.com/dyna/
  9 | */
 10 | 
 11 | import { copy, length, vec2, add, sub, divN, mulN } from '/src/modules/vec2.js'
 12 | import { smoothstep } from '/src/modules/num.js'
 13 | 
 14 | export const settings = { fps : 60 }
 15 | 
 16 | const MASS = 20   // Pencil mass
 17 | const DAMP = 0.95 // Pencil damping
 18 | const RADIUS = 15 // Pencil radius
 19 | 
 20 | let cols, rows
 21 | 
 22 | export function pre(context, cursor, buffer) {
 23 | 
 24 | 	// Detect window resize
 25 | 	if (cols != context.cols || rows != context.rows) {
 26 | 		cols = context.cols
 27 | 		rows = context.rows
 28 | 		for (let i=0; i<cols*rows; i++) {
 29 | 			buffer[i].value = 0
 30 | 		}
 31 | 	}
 32 | 
 33 | 	const a = context.metrics.aspect // Shortcut
 34 | 
 35 | 	dyna.update(cursor)
 36 | 
 37 | 	// Calc line between pos and pre
 38 | 	const points = line(dyna.pos, dyna.pre)
 39 | 
 40 | 	for (const p of points) {
 41 | 		const sx = Math.max(0, p.x - RADIUS)
 42 | 		const ex = Math.min(cols, p.x + RADIUS)
 43 | 		const sy = Math.floor(Math.max(0, p.y - RADIUS * a))
 44 | 		const ey = Math.floor(Math.min(rows, p.y + RADIUS * a))
 45 | 
 46 | 		for (let j=sy; j<ey; j++) {
 47 | 			for (let i=sx; i<ex; i++) {
 48 | 				const x = (p.x - i)
 49 | 				const y = (p.y - j) / a
 50 | 				const l = 1 - length({x, y}) / RADIUS
 51 | 				const idx = i + cols * j
 52 | 				buffer[idx].value = Math.max(buffer[idx].value, l)
 53 | 			}
 54 | 		}
 55 | 	}
 56 | }
 57 | 
 58 | const density = ' .:░▒▓█Ñ#+-'.split('')
 59 | 
 60 | // Just a renderer
 61 | export function main(coord, context, cursor, buffer) {
 62 | 	const i = coord.index
 63 | 	const v = smoothstep(0, 0.9, buffer[i].value)
 64 | 	buffer[i].value *= 0.99
 65 | 	const idx = Math.floor(v * (density.length - 1))
 66 | 	return density[idx]
 67 | }
 68 | 
 69 | import { drawInfo } from '/src/modules/drawbox.js'
 70 | export function post(context, cursor, buffer) {
 71 | 	drawInfo(context, cursor, buffer, {
 72 | 		color : 'white', backgroundColor : 'royalblue', shadowStyle : 'gray'
 73 | 	})
 74 | }
 75 | 
 76 | // -----------------------------------------------------------------------------
 77 | 
 78 | class Dyna {
 79 | 	constructor(mass, damp) {
 80 | 		this.pos = vec2(0,0)
 81 | 		this.vel = vec2(0,0)
 82 | 		this.pre = vec2(0,0)
 83 | 		this.mass = mass
 84 | 		this.damp = damp
 85 | 	}
 86 | 	update(cursor) {
 87 | 		const force = sub(cursor, this.pos)
 88 | 		const acc = divN(force, this.mass)
 89 | 		this.vel = mulN(add(this.vel, acc), this.damp)
 90 | 		this.pre = copy(this.pos)
 91 | 		this.pos = add(this.pos, this.vel)
 92 | 	}
 93 | }
 94 | 
 95 | const dyna = new Dyna(MASS, DAMP)
 96 | 
 97 | // -----------------------------------------------------------------------------
 98 | // Bresenham’s line algorithm
 99 | // https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
100 | // NOTE: vectors a and b will be floored
101 | 
102 | function line(a, b) {
103 | 	let   x0 = Math.floor(a.x)
104 | 	let   y0 = Math.floor(a.y)
105 | 	const x1 = Math.floor(b.x)
106 | 	const y1 = Math.floor(b.y)
107 |     const dx = Math.abs(x1 - x0)
108 |     const dy = -Math.abs(y1 - y0)
109 |     const sx = x0 < x1 ? 1 : -1
110 |     const sy = y0 < y1 ? 1 : -1
111 |     let  err = dx + dy
112 | 
113 | 	const points = []
114 | 
115 |     while (true) {
116 |         points.push({x:x0, y:y0})
117 |         if (x0 == x1 && y0 == y1) break
118 |         let e2 = 2 * err
119 |         if (e2 >= dy) {
120 |             err += dy
121 |             x0 += sx
122 | 		}
123 |         if (e2 <= dx) {
124 |             err += dx
125 |             y0 += sy
126 | 		}
127 | 	}
128 | 	return points
129 | }
130 | 
```

--------------------------------------------------------------------------------
/src/modules/buffer.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 | @module   buffer.js
  3 | @desc     Safe buffer helpers, mostly for internal use
  4 | @category internal
  5 | 
  6 | Safe set() and get() functions, rect() and text() ‘drawing’ helpers.
  7 | 
  8 | Buffers are 1D arrays for 2D data, a ‘width’ and a 'height' parameter
  9 | have to be known (and passed to the functions) to correctly / safely access
 10 | the array.
 11 | 
 12 | const v = get(10, 10, buffer, cols, rows)
 13 | 
 14 | */
 15 | 
 16 | // Safe get function to read from a buffer
 17 | export function get(x, y, target, targetCols, targetRows) {
 18 | 	if (x < 0 || x >= targetCols) return {}
 19 | 	if (y < 0 || y >= targetRows) return {}
 20 | 	const i = x + y * targetCols
 21 | 	return target[i]
 22 | }
 23 | 
 24 | // Safe set and merge functions for a generic buffer object.
 25 | // A buffer object contains at least a 'state' array
 26 | // and a 'width' and a 'height' field to allow easy setting.
 27 | // The value to be set is a single character or a 'cell' object like:
 28 | // { char, color, backgroundColor, fontWeight }
 29 | // which can overwrite the buffer (set) or partially merged (merge)
 30 | export function set(val, x, y, target, targetCols, targetRows) {
 31 | 	if (x < 0 || x >= targetCols) return
 32 | 	if (y < 0 || y >= targetRows) return
 33 | 	const i = x + y * targetCols
 34 | 	target[i] = val
 35 | }
 36 | 
 37 | export function merge(val, x, y, target, targetCols, targetRows) {
 38 | 	if (x < 0 || x >= targetCols) return
 39 | 	if (y < 0 || y >= targetRows) return
 40 | 	const i = x + y * targetCols
 41 | 
 42 | 	// Flatten:
 43 | 	const cell = typeof target[i] == 'object' ? target[i] : { char : target[i] }
 44 | 
 45 | 	target[i] = { ...cell, ...val }
 46 | }
 47 | 
 48 | export function setRect(val, x, y, w, h, target, targetCols, targetRows) {
 49 | 	for (let j=y; j<y+h; j++ ) {
 50 | 		for (let i=x; i<x+w; i++ ) {
 51 | 			set(val, i, j, target, targetCols, targetRows)
 52 | 		}
 53 | 	}
 54 | }
 55 | 
 56 | export function mergeRect(val, x, y, w, h, target, targetCols, targetRows) {
 57 | 	for (let j=y; j<y+h; j++ ) {
 58 | 		for (let i=x; i<x+w; i++ ) {
 59 | 			merge(val, i, j, target, targetCols, targetRows)
 60 | 		}
 61 | 	}
 62 | }
 63 | 
 64 | // Merges a textObj in the form of:
 65 | //	{
 66 | // 		text : 'abc\ndef',
 67 | // 		color : 'red',
 68 | // 		fontWeight : '400',
 69 | // 		backgroundColor : 'black',
 70 | //      etc...
 71 | //	}
 72 | // or just as a string into the target buffer.
 73 | export function mergeText(textObj, x, y, target, targetCols, targetRows) {
 74 | 	let text, mergeObj
 75 | 	// An object has been passed as argument, expect a 'text' field
 76 | 	if (typeof textObj == "object") {
 77 | 		text = textObj.text
 78 | 		// Extract all the fields to be merged...
 79 | 		mergeObj = {...textObj}
 80 | 		// ...but emove text field
 81 | 		delete mergeObj.text
 82 | 	}
 83 | 	// A string has been passed as argument
 84 | 	else {
 85 | 		text = textObj
 86 | 	}
 87 | 
 88 | 	let col = x
 89 | 	let row = y
 90 | 	// Hackish and inefficient way to retain info of the first and last
 91 | 	// character of each line merged into the matrix.
 92 | 	// Can be useful to wrap with markup.
 93 | 	const wrapInfo = []
 94 | 
 95 | 	text.split('\n').forEach((line, lineNum) => {
 96 | 		line.split('').forEach((char, charNum) => {
 97 | 			col = x + charNum
 98 | 			merge({char, ...mergeObj}, col, row, target, targetCols, targetRows)
 99 | 		})
100 | 		const first = get(x, row, target, targetCols, targetRows)
101 | 		const last = get(x+line.length-1, row, target, targetCols, targetRows)
102 | 		wrapInfo.push({first, last})
103 | 		row++
104 | 	})
105 | 
106 | 	// Adjust for last ++
107 | 	row = Math.max(y, row-1)
108 | 
109 | 	// Returns some info about the inserted text:
110 | 	// - the coordinates (offset) of the last inserted character
111 | 	// - the first an last chars of each line (wrapInfo)
112 | 	return {
113 | 		offset : {col, row},
114 | 		// first  : wrapInfo[0].first,
115 | 		// last   : wrapInfo[wrapInfo.length-1].last,
116 | 		wrapInfo
117 | 	}
118 | }
119 | 
```

--------------------------------------------------------------------------------
/src/core/canvasrenderer.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 | @module   canvasrenderer.js
  3 | @desc     renders to canvas
  4 | @category renderer
  5 | */
  6 | 
  7 | export default {
  8 | 	preferredElementNodeName : 'CANVAS',
  9 | 	render
 10 | }
 11 | 
 12 | function render(context, buffer) {
 13 | 
 14 | 	const canvas = context.settings.element
 15 | 
 16 | 	const scale = devicePixelRatio
 17 | 
 18 | 	const c = context.cols
 19 | 	const r = context.rows
 20 | 	const m = context.metrics
 21 | 
 22 | 	const cw = m.cellWidth
 23 | 	const ch = Math.round(m.lineHeight)
 24 | 
 25 | 	// Shortcut
 26 | 	const settings = context.settings
 27 | 
 28 | 	// Fixed size, to allow precise export
 29 | 	if (settings.canvasSize) {
 30 | 		canvas.width  = settings.canvasSize.width * scale
 31 | 		canvas.height = settings.canvasSize.height * scale
 32 | 		canvas.style.width  = settings.canvasSize.width + 'px'
 33 | 		canvas.style.height = settings.canvasSize.height + 'px'
 34 | 	}
 35 | 	// Stretch the canvas to the container width
 36 | 	else {
 37 | 		canvas.width  = context.width * scale
 38 | 		canvas.height = context.height * scale
 39 | 	}
 40 | 
 41 | 	const ff = ' ' + m.fontSize + 'px ' + m.fontFamily
 42 | 	const bg = settings && settings.backgroundColor ? settings.backgroundColor : 'white'
 43 | 	const fg = settings && settings.color ? settings.color : 'black'
 44 | 	const fontWeight = settings && settings.fontWeight ? settings.color : '400'
 45 | 
 46 | 	// Set the backgroundColor of the box-element
 47 | 	// canvas.style.backgroundColor = settings.backgroundColor || 'white'
 48 | 
 49 | 	// Transparent canvas backround for the remaining size
 50 | 	// (fractions of cellWidth and lineHeight).
 51 | 	// Only the 'rendered' area has a solid color.
 52 | 	const ctx = canvas.getContext('2d')
 53 | 	//ctx.clearRect(0, 0, canvas.width, canvas.height)
 54 | 	ctx.fillStyle = bg
 55 | 	ctx.fillRect(0, 0, canvas.width, canvas.height)
 56 | 
 57 | 	ctx.save()
 58 | 	ctx.scale(scale, scale)
 59 | 	ctx.fillStyle = fg
 60 | 	ctx.textBaseline = 'top'
 61 | 
 62 | 	// Custom settings: it’s possible to center the canvas
 63 | 	if (settings.canvasOffset) {
 64 | 		const offs = settings.canvasOffset
 65 | 		const ox = Math.round(offs.x == 'auto' ? (canvas.width / scale - c * cw) / 2 : offs.x)
 66 | 		const oy = Math.round(offs.y == 'auto' ? (canvas.height / scale - r * ch) / 2 : offs.y)
 67 | 		ctx.translate(ox, oy)
 68 | 	}
 69 | 
 70 | 	// Center patch with cell bg color...
 71 | 	// a bit painful and needs some opt.
 72 | 	if( settings.textAlign == 'center' ) {
 73 | 
 74 | 		for (let j=0; j<r; j++) {
 75 | 			const offs = j * c
 76 | 			const widths  = []
 77 | 			const offsets = []
 78 | 			const colors  = []
 79 | 			let totalWidth = 0
 80 | 
 81 | 			// Find width
 82 | 			for (let i=0; i<c; i++) {
 83 | 				const cell = buffer[offs + i]
 84 | 				ctx.font = (cell.fontWeight || fontWeight) + ff
 85 | 				const w = ctx.measureText(cell.char).width
 86 | 				totalWidth += w
 87 | 				widths[i] = w
 88 | 			}
 89 | 			// Draw
 90 | 			let ox = (canvas.width / scale - totalWidth) * 0.5
 91 | 			const y = j * ch
 92 | 			for (let i=0; i<c; i++) {
 93 | 				const cell = buffer[offs + i]
 94 | 				const x = ox
 95 | 				if (cell.backgroundColor && cell.backgroundColor != bg) {
 96 | 					ctx.fillStyle = cell.backgroundColor || bg
 97 | 					ctx.fillRect(Math.round(x), y, Math.ceil(widths[i]), ch)
 98 | 				}
 99 | 				ctx.font = (cell.fontWeight || fontWeight) + ff
100 | 				ctx.fillStyle = cell.color || fg
101 | 				ctx.fillText(cell.char, ox, y)
102 | 
103 | 				ox += widths[i]
104 | 			}
105 | 		}
106 | 
107 | 	// (Default) block mode
108 | 	} else {
109 | 		for (let j=0; j<r; j++) {
110 | 			for (let i=0; i<c; i++) {
111 | 				const cell = buffer[j * c + i]
112 | 				const x = i * cw
113 | 				const y = j * ch
114 | 				if (cell.backgroundColor && cell.backgroundColor != bg) {
115 | 					ctx.fillStyle = cell.backgroundColor || bg
116 | 					ctx.fillRect(Math.round(x), y, Math.ceil(cw), ch)
117 | 				}
118 | 				ctx.font = (cell.fontWeight || fontWeight) + ff
119 | 				ctx.fillStyle = cell.color || fg
120 | 				ctx.fillText(cell.char, x, y)
121 | 			}
122 | 		}
123 | 	}
124 | 	ctx.restore()
125 | }
126 | 
```

--------------------------------------------------------------------------------
/src/programs/demos/doom_flame.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 | [header]
  3 | @author ertdfgcvb
  4 | @title  Doom Flame
  5 | @desc   Oldschool flame effect
  6 | */
  7 | 
  8 | import { clamp, map } from '/src/modules/num.js'
  9 | import { CSS4 } from '/src/modules/color.js'
 10 | import { mix, smoothstep } from '/src/modules/num.js'
 11 | 
 12 | export const settings = { fps : 30, backgroundColor : 'black', color : 'white' }
 13 | 
 14 | const { min, max, sin, floor } = Math
 15 | 
 16 | const flame = '...::/\\/\\/\\+=*abcdef01XYZ#'
 17 | let cols, rows
 18 | 
 19 | const noise = valueNoise()
 20 | 
 21 | const data = []
 22 | 
 23 | export function pre(context, cursor, buffer) {
 24 | 
 25 | 
 26 | 	// Detect resize (and reset buffer, in case)
 27 | 	if (cols != context.cols || rows != context.rows) {
 28 | 		cols = context.cols
 29 | 		rows = context.rows
 30 | 		data.length = cols * rows // Don't loose reference
 31 | 		data.fill(0)
 32 | 	}
 33 | 
 34 | 	// Fill the floor with some noise
 35 | 	if (!cursor.pressed) {
 36 | 		const t = context.time * 0.0015
 37 | 		const last = cols * (rows - 1)
 38 | 		for (let i=0; i<cols; i++) {
 39 | 			const val = floor(map(noise(i * 0.05, t), 0, 1, 5, 40))
 40 | 			data[last + i] = min(val, data[last + i] + 2)
 41 | 		}
 42 | 	} else {
 43 | 		const cx = floor(cursor.x)
 44 | 		const cy = floor(cursor.y)
 45 | 		data[cx + cy * cols] = rndi(5, 50)
 46 | 	}
 47 | 
 48 | 	// Propagate towards the ceiling with some randomness
 49 | 	for (let i=0; i<data.length; i++) {
 50 | 		const row = floor(i / cols)
 51 | 		const col = i % cols
 52 |   		const dest = row * cols + clamp(col + rndi(-1, 1), 0, cols-1)
 53 |   		const src = min(rows-1, row + 1) * cols + col
 54 |   		data[dest] = max(0, data[src]-rndi(0, 2))
 55 | 	}
 56 | }
 57 | 
 58 | export function main(coord, context, cursor, buffer) {
 59 | 	const u = data[coord.index]
 60 | 	if (u === 0) return // Inserts a space
 61 | 
 62 | 	return {
 63 | 		char : flame[clamp(u, 0, flame.length-1)],
 64 | 		fontWeight : u > 20 ? 700 : 100
 65 | 	}
 66 | }
 67 | 
 68 | // Random int betweem a and b, inclusive!
 69 | function rndi(a, b=0) {
 70 | 	if (a > b) [a, b] = [b, a]
 71 | 	return Math.floor(a + Math.random() * (b - a + 1))
 72 | }
 73 | 
 74 | // Value noise:
 75 | // https://www.scratchapixel.com/lessons/procedural-generation-virtual-worlds/procedural-patterns-noise-part-1
 76 | function valueNoise() {
 77 | 
 78 | 	const tableSize = 256;
 79 | 	const r = new Array(tableSize)
 80 | 	const permutationTable = new Array(tableSize * 2)
 81 | 
 82 |     // Create an array of random values and initialize permutation table
 83 |     for (let k=0; k<tableSize; k++) {
 84 |         r[k] = Math.random()
 85 |         permutationTable[k] = k
 86 |     }
 87 | 
 88 |     // Shuffle values of the permutation table
 89 |     for (let k=0; k<tableSize; k++) {
 90 |         const i = Math.floor(Math.random() * tableSize)
 91 |         // swap
 92 |         ;[permutationTable[k], permutationTable[i]] = [permutationTable[i], permutationTable[k]]
 93 |         permutationTable[k + tableSize] = permutationTable[k]
 94 |     }
 95 | 
 96 |     return function(px, py) {
 97 | 	    const xi = Math.floor(px)
 98 | 	    const yi = Math.floor(py)
 99 | 
100 | 	    const tx = px - xi
101 | 	    const ty = py - yi
102 | 
103 | 	    const rx0 = xi % tableSize
104 | 	    const rx1 = (rx0 + 1) % tableSize
105 | 	    const ry0 = yi % tableSize
106 | 	    const ry1 = (ry0 + 1) % tableSize
107 | 
108 | 	    // Random values at the corners of the cell using permutation table
109 | 	    const c00 = r[permutationTable[permutationTable[rx0] + ry0]]
110 | 	    const c10 = r[permutationTable[permutationTable[rx1] + ry0]]
111 | 	    const c01 = r[permutationTable[permutationTable[rx0] + ry1]]
112 | 	    const c11 = r[permutationTable[permutationTable[rx1] + ry1]]
113 | 
114 | 	    // Remapping of tx and ty using the Smoothstep function
115 | 	    const sx = smoothstep(0, 1, tx);
116 | 	    const sy = smoothstep(0, 1, ty);
117 | 
118 | 	    // Linearly interpolate values along the x axis
119 | 	    const nx0 = mix(c00, c10, sx)
120 | 	    const nx1 = mix(c01, c11, sx)
121 | 
122 | 	    // Linearly interpolate the nx0/nx1 along they y axis
123 | 	    return mix(nx0, nx1, sy)
124 | 	}
125 | }
126 | 
127 | import { drawInfo } from '/src/modules/drawbox.js'
128 | export function post(context, cursor, buffer) {
129 | 	drawInfo(context, cursor, buffer)
130 | }
131 | 
```

--------------------------------------------------------------------------------
/src/programs/demos/gol_double_res.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 | [header]
  3 | @author ertdfgcvb
  4 | @title  Golgol
  5 | @desc   Double resolution version of GOL
  6 | 
  7 | Based on Alex Miller’s version of Game of Life:
  8 | https://play.ertdfgcvb.xyz/#/src/contributed/game_of_life
  9 | 
 10 | By using three box chars (plus space) each char can host
 11 | two vertical cells of the automata allowing a double resolution.
 12 | '█' both cells are occupied
 13 | ' ' both cells are empty
 14 | '▀' upper cell is occupied
 15 | '▄' lower cell is occupied
 16 | 
 17 | All the automata state is stored in the custom 'data' buffer.
 18 | Each frame of the animation depends on the previous frame,
 19 | so in this case the 'data' buffer is two arrays (see initialization in pre).
 20 | */
 21 | 
 22 | // Safe set function
 23 | function set(val, x, y, w, h, buf) {
 24 | 	if (x < 0 || x >= w) return
 25 | 	if (y < 0 || y >= h) return
 26 | 	buf[y * w + x] = val
 27 | }
 28 | 
 29 | // Safe get function
 30 | function get(x, y, w, h, buf) {
 31 | 	if (x < 0 || x >= w) return 0
 32 | 	if (y < 0 || y >= h) return 0
 33 | 	return buf[y * w + x]
 34 | }
 35 | 
 36 | // Some state to detect window resize / init
 37 | let cols, rows
 38 | 
 39 | const data = []
 40 | 
 41 | // The automata is computed in a single step and stored in the 'data' buffer
 42 | export function pre(context, cursor, buffer) {
 43 | 
 44 | 	// The window has been resized (or “init”), reset the buffer:
 45 | 	if (cols != context.cols || rows != context.rows) {
 46 | 		cols = context.cols
 47 | 		rows = context.rows
 48 | 		const len = context.cols * context.rows	* 2 // double height
 49 | 
 50 | 		// We need two buffer (store them in the user 'data' array)
 51 | 		data[0] = []
 52 | 		data[1] = []
 53 | 		// Initialize with some random state
 54 | 		for (let i=0; i<len; i++) {
 55 | 			const v = Math.random() > 0.5 ? 1 : 0
 56 | 			data[0][i] = v
 57 | 			data[1][i] = v
 58 | 		}
 59 | 	}
 60 | 
 61 | 	// Update the buffer
 62 | 	const prev = data[ context.frame % 2]
 63 | 	const curr = data[(context.frame + 1) % 2]
 64 | 	const w = cols
 65 | 	const h = rows * 2
 66 | 
 67 | 	// Fill a random rect of 10x10
 68 | 	if (cursor.pressed) {
 69 | 		const cx = Math.floor(cursor.x) // cursor has float values
 70 | 		const cy = Math.floor(cursor.y * 2)
 71 | 		const s = 5
 72 | 		for (let y=cy-s; y<=cy+s; y++) {
 73 | 			for (let x=cx-s; x<=cx+s; x++) {
 74 | 				const val = Math.random() < 0.5 ? 1 : 0
 75 | 				set(val, x, y, w, h, prev)
 76 | 			}
 77 | 		}
 78 | 	}
 79 | 
 80 | 	// Update the automata
 81 | 	for (let y=0; y<h; y++) {
 82 | 		for (let x=0; x<w; x++) {
 83 | 			const current = get(x, y, w, h, prev)
 84 | 			const neighbors =
 85 | 				get(x - 1, y - 1, w, h, prev) +
 86 | 				get(x,     y - 1, w, h, prev) +
 87 | 				get(x + 1, y - 1, w, h, prev) +
 88 | 				get(x - 1, y,     w, h, prev) +
 89 | 				get(x + 1, y,     w, h, prev) +
 90 | 				get(x - 1, y + 1, w, h, prev) +
 91 | 				get(x,     y + 1, w, h, prev) +
 92 | 				get(x + 1, y + 1, w, h, prev)
 93 | 			// Update
 94 | 			const i = x + y * w
 95 | 			if (current == 1) {
 96 | 				curr[i] = neighbors == 2 || neighbors == 3 ? 1 : 0
 97 | 			} else {
 98 | 				curr[i] = neighbors == 3 ? 1 : 0
 99 | 			}
100 | 		}
101 | 	}
102 | }
103 | 
104 | // Just a renderer
105 | export function main(coord, context, cursor, buffer) {
106 | 	// Current buffer
107 | 	const curr = data[(context.frame + 1) % 2]
108 | 
109 | 	// Upper and lower half
110 | 	const idx = coord.x + coord.y * 2 * context.cols
111 | 	const upper = curr[idx]
112 | 	const lower = curr[idx + context.cols]
113 | 
114 | 	if (upper && lower) return '█' // both cells are occupied
115 | 	if (upper)          return '▀' // upper cell
116 | 	if (lower)          return '▄' // lower cell
117 | 	                    return ' ' // both cells are empty
118 | }
119 | 
120 | // Draw some info
121 | import { drawBox } from '/src/modules/drawbox.js'
122 | export function post(context, cursor, buffer) {
123 | 
124 | 	const buff = data[(context.frame + 1) % 2]
125 | 	const numCells = buff.reduce((a, b) => a + b, 0)
126 | 
127 | 	let text = ''
128 | 	text += 'frame ' + context.frame + '\n'
129 | 	text += 'size  ' + context.cols + '×' + context.rows + '\n'
130 | 	text += 'cells ' + numCells + '/' + buff.length + '\n'
131 | 
132 |     drawBox(text, {
133 | 		backgroundColor : 'tomato',
134 | 		color           : 'black',
135 | 		borderStyle     : 'double',
136 | 		shadowStyle     : 'gray'
137 | 	}, buffer)
138 | }
139 | 
140 | 
```

--------------------------------------------------------------------------------
/src/programs/demos/doom_flame_full_color.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 | [header]
  3 | @author ertdfgcvb
  4 | @title  Doom Flame (full color)
  5 | @desc   Oldschool flame effect
  6 | */
  7 | 
  8 | import { clamp, map } from '/src/modules/num.js'
  9 | import { mix, smoothstep } from '/src/modules/num.js'
 10 | 
 11 | export const settings = { backgroundColor : 'black' }
 12 | 
 13 | const { min, max, sin, floor } = Math
 14 | 
 15 | const palette = [
 16 | 	'black',        // 0 < top
 17 | 	'purple',       // 1
 18 | 	'darkred',      // 2
 19 | 	'red',          // 3
 20 | 	'orangered',    // 4
 21 | 	'gold',         // 5
 22 | 	'lemonchiffon', // 6
 23 | 	'white'         // 7 < bottom
 24 | ]
 25 | 
 26 | //             top                       bottom
 27 | //             v                         v
 28 | const flame = '011222233334444444455566667'.split('').map(Number)
 29 | 
 30 | const noise = valueNoise()
 31 | 
 32 | let cols, rows
 33 | 
 34 | const data = []
 35 | 
 36 | export function pre(context, cursor, buffer) {
 37 | 
 38 | 	// Detect resize (and reset buffer, in case)
 39 | 	if (cols != context.cols || rows != context.rows) {
 40 | 		cols = context.cols
 41 | 		rows = context.rows
 42 | 		data.length = cols * rows // Don't loose reference
 43 | 		data.fill(0)
 44 | 	}
 45 | 
 46 | 	// Fill the floor with some noise
 47 | 	if (!cursor.pressed) {
 48 | 		const t = context.time * 0.0015
 49 | 		const last = cols * (rows - 1)
 50 | 		for (let i=0; i<cols; i++) {
 51 | 			const val = floor(map(noise(i * 0.05, t), 0, 1, 5, 50))
 52 | 			data[last + i] = min(val, data[last + i] + 2)
 53 | 		}
 54 | 	} else {
 55 | 		const cx = floor(cursor.x)
 56 | 		const cy = floor(cursor.y)
 57 | 		data[cx + cy * cols] = rndi(5, 50)
 58 | 	}
 59 | 
 60 | 	// Propagate towards the ceiling with some randomness
 61 | 	for (let i=0; i<data.length; i++) {
 62 | 		const row = floor(i / cols)
 63 | 		const col = i % cols
 64 |   		const dest = row * cols + clamp(col + rndi(-1, 1), 0, cols-1)
 65 |   		const src = min(rows-1, row + 1) * cols + col
 66 |   		data[dest] = max(0, data[src]-rndi(0, 2))
 67 | 	}
 68 | }
 69 | 
 70 | export function main(coord, context, cursor, buffer) {
 71 | 
 72 | 	const u = data[coord.index]
 73 | 	const v = flame[clamp(u, 0, flame.length-1)]
 74 | 
 75 | 	if (v === 0) return {
 76 | 		char : ' ',
 77 | 		backgroundColor : 'black'
 78 | 	}
 79 | 
 80 | 	return {
 81 | 		char : u % 10,
 82 | 		color : palette[min(palette.length-1,v+1)],
 83 | 		backgroundColor : palette[v]
 84 | 	}
 85 | }
 86 | 
 87 | // Random int betweem a and b, inclusive!
 88 | function rndi(a, b=0) {
 89 | 	if (a > b) [a, b] = [b, a]
 90 | 	return Math.floor(a + Math.random() * (b - a + 1))
 91 | }
 92 | 
 93 | // Value noise:
 94 | // https://www.scratchapixel.com/lessons/procedural-generation-virtual-worlds/procedural-patterns-noise-part-1
 95 | function valueNoise() {
 96 | 
 97 | 	const tableSize = 256;
 98 | 	const r = new Array(tableSize)
 99 | 	const permutationTable = new Array(tableSize * 2)
100 | 
101 |     // Create an array of random values and initialize permutation table
102 |     for (let k=0; k<tableSize; k++) {
103 |         r[k] = Math.random()
104 |         permutationTable[k] = k
105 |     }
106 | 
107 |     // Shuffle values of the permutation table
108 |     for (let k=0; k<tableSize; k++) {
109 |         const i = Math.floor(Math.random() * tableSize)
110 |         // swap
111 |         ;[permutationTable[k], permutationTable[i]] = [permutationTable[i], permutationTable[k]]
112 |         permutationTable[k + tableSize] = permutationTable[k]
113 |     }
114 | 
115 |     return function(px, py) {
116 | 	    const xi = Math.floor(px)
117 | 	    const yi = Math.floor(py)
118 | 
119 | 	    const tx = px - xi
120 | 	    const ty = py - yi
121 | 
122 | 	    const rx0 = xi % tableSize
123 | 	    const rx1 = (rx0 + 1) % tableSize
124 | 	    const ry0 = yi % tableSize
125 | 	    const ry1 = (ry0 + 1) % tableSize
126 | 
127 | 	    // Random values at the corners of the cell using permutation table
128 | 	    const c00 = r[permutationTable[permutationTable[rx0] + ry0]]
129 | 	    const c10 = r[permutationTable[permutationTable[rx1] + ry0]]
130 | 	    const c01 = r[permutationTable[permutationTable[rx0] + ry1]]
131 | 	    const c11 = r[permutationTable[permutationTable[rx1] + ry1]]
132 | 
133 | 	    // Remapping of tx and ty using the Smoothstep function
134 | 	    const sx = smoothstep(0, 1, tx);
135 | 	    const sy = smoothstep(0, 1, ty);
136 | 
137 | 	    // Linearly interpolate values along the x axis
138 | 	    const nx0 = mix(c00, c10, sx)
139 | 	    const nx1 = mix(c01, c11, sx)
140 | 
141 | 	    // Linearly interpolate the nx0/nx1 along they y axis
142 | 	    return mix(nx0, nx1, sy)
143 | 	}
144 | }
145 | 
146 | import { drawInfo } from '/src/modules/drawbox.js'
147 | export function post(context, cursor, buffer) {
148 | 	drawInfo(context, cursor, buffer)
149 | }
150 | 
```

--------------------------------------------------------------------------------
/src/modules/vec2.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 | @module   vec2.js
  3 | @desc     2D vector helper functions
  4 | @category public
  5 | 
  6 | - No vector class (a 'vector' is just any object with {x, y})
  7 | - The functions never modify the original object.
  8 | - An optional destination object can be passed as last paremeter to all
  9 |   the functions (except vec2()).
 10 | - All function can be exported individually or grouped via default export.
 11 | - For the default export use:
 12 | 	import * as Vec2 from '/src/modules/vec2.js'
 13 | */
 14 | 
 15 | // Creates a vector
 16 | export function vec2(x, y) {
 17 | 	return {x, y}
 18 | }
 19 | 
 20 | // Copies a vector
 21 | export function copy(a, out) {
 22 | 	out = out || vec2(0, 0)
 23 | 
 24 | 	out.x = a.x
 25 | 	out.y = a.y
 26 | 
 27 | 	return out
 28 | }
 29 | 
 30 | // Adds two vectors
 31 | export function add(a, b, out) {
 32 | 	out = out || vec2(0, 0)
 33 | 
 34 | 	out.x = a.x + b.x
 35 | 	out.y = a.y + b.y
 36 | 
 37 | 	return out
 38 | }
 39 | 
 40 | // Subtracts two vectors
 41 | export function sub(a, b, out) {
 42 | 	out = out || vec2(0, 0)
 43 | 
 44 | 	out.x = a.x - b.x
 45 | 	out.y = a.y - b.y
 46 | 
 47 | 	return out
 48 | }
 49 | 
 50 | // Multiplies a vector by another vector (component-wise)
 51 | export function mul(a, b, out) {
 52 | 	out = out || vec2(0, 0)
 53 | 
 54 | 	out.x = a.x * b.x
 55 | 	out.y = a.y * b.y
 56 | 
 57 | 	return out
 58 | }
 59 | 
 60 | // Divides a vector by another vector (component-wise)
 61 | export function div(a, b, out) {
 62 | 	out = out || vec2(0, 0)
 63 | 
 64 | 	out.x = a.x / b.x
 65 | 	out.y = a.y / b.y
 66 | 
 67 | 	return out
 68 | }
 69 | 
 70 | // Adds a scalar to a vector
 71 | export function addN(a, k, out) {
 72 | 	out = out || vec2(0, 0)
 73 | 
 74 | 	out.x = a.x + k
 75 | 	out.y = a.y + k
 76 | 
 77 | 	return out
 78 | }
 79 | 
 80 | // Subtracts a scalar from a vector
 81 | export function subN(a, k, out) {
 82 | 	out = out || vec2(0, 0)
 83 | 
 84 | 	out.x = a.x - k
 85 | 	out.y = a.y - k
 86 | 
 87 | 	return out
 88 | }
 89 | 
 90 | // Mutiplies a vector by a scalar
 91 | export function mulN(a, k, out) {
 92 | 	out = out || vec2(0, 0)
 93 | 
 94 | 	out.x = a.x * k
 95 | 	out.y = a.y * k
 96 | 
 97 | 	return out
 98 | }
 99 | 
100 | // Divides a vector by a scalar
101 | export function divN(a, k, out) {
102 | 	out = out || vec2(0, 0)
103 | 
104 | 	out.x = a.x / k
105 | 	out.y = a.y / k
106 | 
107 | 	return out
108 | }
109 | 
110 | // Computes the dot product of two vectors
111 | export function dot(a, b) {
112 | 	return a.x * b.x + a.y * b.y
113 | }
114 | 
115 | // Computes the length of vector
116 | export function length(a) {
117 | 	return Math.sqrt(a.x * a.x + a.y * a.y)
118 | }
119 | 
120 | // Computes the square of the length of vector
121 | export function lengthSq(a) {
122 | 	return a.x * a.x + a.y * a.y
123 | }
124 | 
125 | // Computes the distance between 2 points
126 | export function dist(a, b) {
127 | 	const dx = a.x - b.x
128 | 	const dy = a.y - b.y
129 | 
130 | 	return Math.sqrt(dx * dx + dy * dy)
131 | }
132 | 
133 | // Computes the square of the distance between 2 points
134 | export function distSq(a, b) {
135 | 	const dx = a.x - b.x
136 | 	const dy = a.y - b.y
137 | 
138 | 	return dx * dx + dy * dy
139 | }
140 | 
141 | // Divides a vector by its Euclidean length and returns the quotient
142 | export function norm(a, out) {
143 | 	out = out || vec2(0, 0)
144 | 
145 | 	const l = length(a)
146 | 	if (l > 0.00001) {
147 | 		out.x = a.x / l
148 | 		out.y = a.y / l
149 | 	} else {
150 | 		out.x = 0
151 | 		out.y = 0
152 | 	}
153 | 
154 | 	return out
155 | }
156 | 
157 | // Negates a vector
158 | export function neg(v, out) {
159 | 	out = out || vec2(0, 0)
160 | 
161 | 	out.x = -a.x
162 | 	out.y = -a.y
163 | 
164 | 	return out
165 | }
166 | 
167 | // Rotates a vector
168 | export function rot(a, ang, out) {
169 | 	out = out || vec2(0, 0)
170 | 
171 | 	const s = Math.sin(ang)
172 | 	const c = Math.cos(ang)
173 | 
174 | 	out.x = a.x * c - a.y * s,
175 | 	out.y = a.x * s + a.y * c
176 | 
177 | 	return out
178 | }
179 | 
180 | // Performs linear interpolation on two vectors
181 | export function mix(a, b, t, out) {
182 | 	out = out || vec2(0, 0)
183 | 
184 | 	out.x = (1 - t) * a.x + t * b.x
185 | 	out.y = (1 - t) * a.y + t * b.y
186 | 
187 | 	return out
188 | }
189 | 
190 | // Computes the abs of a vector (component-wise)
191 |  export function abs(a, out) {
192 | 	out = out || vec2(0, 0)
193 | 
194 | 	out.x = Math.abs(a.x)
195 | 	out.y = Math.abs(a.y)
196 | 
197 | 	return out
198 | }
199 | 
200 | // Computes the max of two vectors (component-wise)
201 | export function max(a, b, out) {
202 | 	out = out || vec2(0, 0)
203 | 
204 | 	out.x = Math.max(a.x, b.x)
205 | 	out.y = Math.max(a.y, b.y)
206 | 
207 | 	return out
208 | }
209 | 
210 | // Computes the min of two vectors (component-wise)
211 | export function min(a, b, out) {
212 | 	out = out || vec2(0, 0)
213 | 
214 | 	out.x = Math.min(a.x, b.x)
215 | 	out.y = Math.min(a.y, b.y)
216 | 
217 | 	return out
218 | }
219 | 
220 | // Returns the fractional part of the vector (component-wise)
221 | export function fract(a, out) {
222 | 	out = out || vec2(0, 0)
223 | 	out.x = a.x - Math.floor(a.x)
224 | 	out.y = a.y - Math.floor(a.y)
225 | 	return out
226 | }
227 | 
228 | // Returns the floored vector (component-wise)
229 | export function floor(a, out) {
230 | 	out = out || vec2(0, 0)
231 | 	out.x = Math.floor(a.x)
232 | 	out.y = Math.floor(a.y)
233 | 	return out
234 | }
235 | 
236 | // Returns the ceiled vector (component-wise)
237 | export function ceil(a, out) {
238 | 	out = out || vec2(0, 0)
239 | 	out.x = Math.ceil(a.x)
240 | 	out.y = Math.ceil(a.y)
241 | 	return out
242 | }
243 | 
244 | // Returns the rounded vector (component-wise)
245 | export function round(a, out) {
246 | 	out = out || vec2(0, 0)
247 | 	out.x = Math.round(a.x)
248 | 	out.y = Math.round(a.y)
249 | 	return out
250 | }
251 | 
252 | 
253 | 
```

--------------------------------------------------------------------------------
/src/core/textrenderer.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 | @module   textrenderer.js
  3 | @desc     renders to a text element
  4 | @category renderer
  5 | */
  6 | 
  7 | export default {
  8 | 	preferredElementNodeName : 'PRE',
  9 | 	render
 10 | }
 11 | 
 12 | const backBuffer = []
 13 | 
 14 | let cols, rows
 15 | 
 16 | function render(context, buffer) {
 17 | 
 18 | 	const element = context.settings.element
 19 | 
 20 | 	// Set the most used styles to the container
 21 | 	// element.style.backgroundColor = context.settings.background
 22 | 	// element.style.color = context.settings.color
 23 | 	// element.style.fontWeight = context.settings.weight
 24 | 
 25 | 	// Detect resize
 26 | 	if (context.rows != rows || context.cols != cols) {
 27 | 		cols = context.cols
 28 | 		rows = context.rows
 29 | 		backBuffer.length = 0
 30 | 	}
 31 | 
 32 | 	// DOM rows update: expand lines if necessary
 33 | 	// TODO: also benchmark a complete 'innerHTML' rewrite, could be faster?
 34 | 	while(element.childElementCount < rows) {
 35 | 		const span = document.createElement('span')
 36 | 		span.style.display = 'block'
 37 | 		element.appendChild(span)
 38 | 	}
 39 | 
 40 | 	// DOM rows update: shorten lines if necessary
 41 | 	// https://jsperf.com/innerhtml-vs-removechild/15
 42 | 	while(element.childElementCount > rows) {
 43 | 		element.removeChild(element.lastChild)
 44 | 	}
 45 | 
 46 | 	// Counts the number of updated rows, seful for debug
 47 | 	let updatedRowNum = 0
 48 | 
 49 | 	// A bit of a cumbersome render-loop…
 50 | 	// A few notes: the fastest way I found to render the image
 51 | 	// is by manually write the markup into the parent node via .innerHTML;
 52 | 	// creating a node via .createElement and then popluate it resulted
 53 | 	// remarkably slower (even if more elegant for the CSS handling below).
 54 | 	for (let j=0; j<rows; j++) {
 55 | 
 56 | 		const offs = j * cols
 57 | 
 58 | 		// This check is faster than to force update the DOM.
 59 | 		// Buffer can be manually modified in pre, main and after
 60 | 		// with semi-arbitrary values…
 61 | 		// It is necessary to keep track of the previous state
 62 | 		// and specifically check if a change in style
 63 | 		// or char happened on the whole row.
 64 | 		let rowNeedsUpdate = false
 65 | 		for (let i=0; i<cols; i++) {
 66 | 			const idx = i + offs
 67 | 			const newCell = buffer[idx]
 68 | 			const oldCell = backBuffer[idx]
 69 | 			if (!isSameCell(newCell, oldCell)) {
 70 | 				if (rowNeedsUpdate == false) updatedRowNum++
 71 | 				rowNeedsUpdate = true
 72 | 				backBuffer[idx] = {...newCell}
 73 | 			}
 74 | 		}
 75 | 
 76 | 		// Skip row if update is not necessary
 77 | 		if (rowNeedsUpdate == false) continue
 78 | 
 79 | 		let html = '' // Accumulates the markup
 80 | 		let prevCell = {} //defaultCell
 81 | 		let tagIsOpen = false
 82 | 		for (let i=0; i<cols; i++) {
 83 | 			const currCell = buffer[i + offs] //|| {...defaultCell, char : EMPTY_CELL}
 84 | 			// Undocumented feature:
 85 | 			// possible to inject some custom HTML (for example <a>) into the renderer.
 86 | 			// It can be inserted before the char or after the char (beginHTML, endHTML)
 87 | 			// and this is a very hack…
 88 | 			if (currCell.beginHTML) {
 89 | 				if (tagIsOpen) {
 90 | 					html += '</span>'
 91 | 					prevCell = {} //defaultCell
 92 | 					tagIsOpen = false
 93 | 				}
 94 | 				html += currCell.beginHTML
 95 | 			}
 96 | 
 97 | 			// If there is a change in style a new span has to be inserted
 98 | 			if (!isSameCellStyle(currCell, prevCell)) {
 99 | 				// Close the previous tag
100 | 				if (tagIsOpen) html += '</span>'
101 | 
102 | 				const c = currCell.color === context.settings.color ? null : currCell.color
103 | 				const b = currCell.backgroundColor === context.settings.backgroundColor ? null : currCell.backgroundColor
104 | 				const w = currCell.fontWeight === context.settings.fontWeight ? null : currCell.fontWeight
105 | 
106 | 				// Accumulate the CSS inline attribute.
107 | 				let css = ''
108 | 				if (c) css += 'color:' + c + ';'
109 | 				if (b) css += 'background:' + b + ';'
110 | 				if (w) css += 'font-weight:' + w + ';'
111 | 				if (css) css = ' style="' + css + '"'
112 | 				html += '<span' + css + '>'
113 | 				tagIsOpen = true
114 | 			}
115 | 			html += currCell.char
116 | 			prevCell = currCell
117 | 
118 | 			// Add closing tag, in case
119 | 			if (currCell.endHTML) {
120 | 				if (tagIsOpen) {
121 | 					html += '</span>'
122 | 					prevCell = {} //defaultCell
123 | 					tagIsOpen = false
124 | 				}
125 | 				html += currCell.endHTML
126 | 			}
127 | 
128 | 		}
129 | 		if (tagIsOpen) html += '</span>'
130 | 
131 | 		// Write the row
132 | 		element.childNodes[j].innerHTML = html
133 | 	}
134 | }
135 | 
136 | // Compares two cells
137 | function isSameCell(cellA, cellB) {
138 | 	if (typeof cellA != 'object')                        return false
139 | 	if (typeof cellB != 'object')                        return false
140 | 	if (cellA.char !== cellB.char)                       return false
141 | 	if (cellA.fontWeight !== cellB.fontWeight)           return false
142 | 	if (cellA.color !== cellB.color)                     return false
143 | 	if (cellA.backgroundColor !== cellB.backgroundColor) return false
144 | 	return true
145 | }
146 | 
147 | // Compares two cells for style only
148 | function isSameCellStyle(cellA, cellB) {
149 | 	if (cellA.fontWeight !== cellB.fontWeight)           return false
150 | 	if (cellA.color !== cellB.color)                     return false
151 | 	if (cellA.backgroundColor !== cellB.backgroundColor) return false
152 | 	return true
153 | }
154 | 
```
Page 1/2FirstPrevNextLast