#
tokens: 26170/50000 7/81 files (page 2/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 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

--------------------------------------------------------------------------------
/tests/benchmark.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 | 		body {
  9 | 			padding: 0;
 10 | 			margin: 2em;
 11 | 			font-size: 1em;
 12 | 			line-height: 1.2;
 13 | 			font-family: 'Simple Console', monospace;
 14 | 		}
 15 | 		pre {
 16 | 			font-family: inherit;
 17 | 			margin:0 0 2em 0;
 18 | 		}
 19 | 	</style>
 20 | </head>
 21 | <body>
 22 | 	<pre></pre>
 23 | 	<pre></pre>
 24 | 	<script type="module">
 25 | 
 26 | 		const cols = 100
 27 | 		const rows = 40
 28 | 		const chars = 'ABCDEFG0123456789. '.split('')
 29 | 		const colors = ['red', 'blue']
 30 | 		const NUM_FRAMES = 100
 31 | 
 32 | 		const target = document.querySelectorAll('pre')[0]
 33 | 		const output = document.querySelectorAll('pre')[1]
 34 | 		const functions = [baseline, a, b, c]//, d]
 35 | 
 36 | 
 37 | 		let frame = 0
 38 | 		let step = 0
 39 | 		let t0
 40 | 		let fun
 41 | 
 42 | 		fun = functions[step]
 43 | 
 44 | 		function loop(t) {
 45 | 
 46 | 			const af = requestAnimationFrame(loop)
 47 | 
 48 | 			if (frame == 0) t0 = performance.now();
 49 | 
 50 | 			fun(target, frame)
 51 | 			frame++
 52 | 
 53 | 			if (frame == NUM_FRAMES) {
 54 | 				const elapsed = performance.now() - t0
 55 | 				let out = []
 56 | 				out.push('-----------------------------------')
 57 | 				out.push('step:     ' + (step+1) + '/' + functions.length)
 58 | 				out.push('function: ' + fun.name)
 59 | 				out.push('elapsed:  ' + elapsed)
 60 | 				out.push('avg:      ' + (elapsed / NUM_FRAMES))
 61 | 				out.push('')
 62 | 				output.innerHTML += out.join('<br>')
 63 | 
 64 | 				frame = 0
 65 | 				step++
 66 | 				if (step < functions.length) {
 67 | 					fun = functions[step]
 68 | 				} else {
 69 | 					cancelAnimationFrame(af)
 70 | 				}
 71 | 			}
 72 | 		}
 73 | 
 74 | 		requestAnimationFrame(loop)
 75 | 
 76 | 		// ---------------------------------------------------------------------
 77 | 
 78 | 		// Unstyled; should run at 60fps
 79 | 		// Direct write to innerHTML
 80 | 		function baseline(target, frame) {
 81 | 			let html = ''
 82 | 			for (let j=0; j<rows; j++) {
 83 | 				for (let i=0; i<cols; i++) {
 84 | 					const idx = (i + j * rows + frame) % chars.length
 85 | 					html += chars[idx]
 86 | 				}
 87 | 				html += '<br>'
 88 | 			}
 89 | 			target.innerHTML = html
 90 | 		}
 91 | 
 92 | 		// ---------------------------------------------------------------------
 93 | 
 94 | 		// Every char is wrapped in a span, same style
 95 | 		// Direct write to innerHTML
 96 | 		function a(target, frame) {
 97 | 			let html = ''
 98 | 			for (let j=0; j<rows; j++) {
 99 | 				for (let i=0; i<cols; i++) {
100 | 					const idx = (i + j * rows + frame) % chars.length
101 | 					html += `<span>${chars[idx % chars.length]}</span>`
102 | 				}
103 | 				html += '<br>'
104 | 			}
105 | 			target.innerHTML = html
106 | 		}
107 | 
108 | 		// ---------------------------------------------------------------------
109 | 
110 | 		// Every char is wrapped in a span, foreground and background change
111 | 		// Direct write to innerHTML
112 | 		function b(target, frame) {
113 | 			let html = ''
114 | 			for (let j=0; j<rows; j++) {
115 | 				for (let i=0; i<cols; i++) {
116 | 					const idx = (i + j * rows + frame)
117 | 					const style = `color:${colors[idx % colors.length]};background-color:${colors[(idx+1) % colors.length]};`
118 | 					html += `<span style="${style}">${chars[idx % chars.length]}</span>`
119 | 				}
120 | 				html += '<br>'
121 | 			}
122 | 			target.innerHTML = html
123 | 		}
124 | 
125 | 		// ---------------------------------------------------------------------
126 | 
127 | 		// Direct write to innerHTML of each span
128 | 		// Re-use of <spans>
129 | 		const r = new Array(rows).fill(null).map(function(e) {
130 | 			const span = document.createElement('span')
131 | 			span.style.display = 'block'
132 | 			return span
133 | 		})
134 | 
135 | 		function c(target, frame) {
136 | 			if (frame == 0) {
137 | 				target.innerHTML = ''
138 | 				for (let j=0; j<rows; j++) {
139 | 					target.appendChild(r[j])
140 | 				}
141 | 			}
142 | 			// for (let j=0; j<rows; j++) {
143 | 			// 	r[j].style.display = 'none'
144 | 			// }
145 | 			for (let j=0; j<rows; j++) {
146 | 				let html = ''
147 | 				for (let i=0; i<cols; i++) {
148 | 					const idx = (i + j * rows + frame)
149 | 					const style = `color:${colors[idx % colors.length]};background-color:${colors[(idx+1) % colors.length]};`
150 | 					html += `<span style="${style}">${chars[idx % chars.length]}</span>`
151 | 				}
152 | 				r[j].innerHTML = html
153 | 			}
154 | 			// for (let j=0; j<rows; j++) {
155 | 			// 	r[j].style.display = 'block'
156 | 			// }
157 | 		}
158 | 
159 | 		// ---------------------------------------------------------------------
160 | 
161 | 		// Document fragments
162 | 		/*
163 | 		const fragment = new DocumentFragment()
164 | 		const p = document.createElement("pre")
165 | 		fragment.appendChild(p)
166 | 
167 | 		function d(target, frame) {
168 | 			p.innerHTML = ''
169 | 			for (let j=0; j<rows; j++) {
170 | 			//	let html = ''
171 | 				for (let i=0; i<cols; i++) {
172 | 					const idx = (i + j * rows + frame)
173 | 					const style = `color:${colors[idx % colors.length]};background-color:${colors[(idx+1) % colors.length]};`
174 | 					p.innerHTML += `<span style="${style}">${chars[idx % chars.length]}</span>`
175 | 				}
176 | 				// r[j].innerHTML = html
177 | 				// fragment.appendChild(r[j])
178 | 				p.innerHTML += '<br>'
179 | 			}
180 | 			target.innerHTML = ''
181 | 			target.appendChild(fragment)
182 | 			// for (let j=0; j<rows; j++) {
183 | 			// 	r[j].style.display = 'block'
184 | 			// }
185 | 		}
186 | 		*/
187 | 
188 | 	</script>
189 | </body>
190 | </html>
```

--------------------------------------------------------------------------------
/src/modules/vec3.js:
--------------------------------------------------------------------------------

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

--------------------------------------------------------------------------------
/src/modules/drawbox.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 | @module   drawbox.js
  3 | @desc     Draw text boxes with optional custom styles
  4 | @category public
  5 | 
  6 | A style object can be passed to override the default style:
  7 | 
  8 | const style = {
  9 | 	x               : 3,
 10 | 	y               : 2,
 11 | 	width           : 0,
 12 | 	height          : 0,
 13 | 	backgroundColor : 'white',
 14 | 	color           : 'black',
 15 | 	fontWeight      : 'normal',
 16 | 	shadowStyle     : 'none',
 17 | 	borderStyle     : 'round'
 18 | 	paddingX        : 2,
 19 | 	paddingY        : 1,
 20 | }
 21 | */
 22 | 
 23 | // The drawing styles for the borders.
 24 | const borderStyles = {
 25 | 	double : {
 26 | 		topleft     : '╔',
 27 | 		topright    : '╗',
 28 | 		bottomright : '╝',
 29 | 		bottomleft  : '╚',
 30 | 		top         : '═',
 31 | 		bottom      : '═',
 32 | 		left        : '║',
 33 | 		right       : '║',
 34 | 		bg          : ' ',
 35 | 	},
 36 | 	single : {
 37 | 		topleft     : '┌',
 38 | 		topright    : '┐',
 39 | 		bottomright : '┘',
 40 | 		bottomleft  : '╰',
 41 | 		top         : '─',
 42 | 		bottom      : '─',
 43 | 		left        : '│',
 44 | 		right       : '│',
 45 | 		bg          : ' ',
 46 | 	},
 47 | 	round : {
 48 | 		topleft     : '╭',
 49 | 		topright    : '╮',
 50 | 		bottomright : '╯',
 51 | 		bottomleft  : '╰',
 52 | 		top         : '─',
 53 | 		bottom      : '─',
 54 | 		left        : '│',
 55 | 		right       : '│',
 56 | 		bg          : ' ',
 57 | 	},
 58 | 	singleDouble : {
 59 | 		topleft     : '┌',
 60 | 		topright    : '╖',
 61 | 		bottomright : '╝',
 62 | 		bottomleft  : '╘',
 63 | 		top         : '─',
 64 | 		bottom      : '═',
 65 | 		left        : '│',
 66 | 		right       : '║',
 67 | 		bg          : ' ',
 68 | 	},
 69 | 	fat : {
 70 | 		topleft     : '█' ,
 71 | 		topright    : '█',
 72 | 		bottomright : '█',
 73 | 		bottomleft  : '█',
 74 | 		top         : '▀',
 75 | 		bottom      : '▄',
 76 | 		left        : '█',
 77 | 		right       : '█',
 78 | 		bg          : ' ',
 79 | 	},
 80 | 	none : {
 81 | 		topleft     : ' ',
 82 | 		topright    : ' ',
 83 | 		bottomright : ' ',
 84 | 		bottomleft  : ' ',
 85 | 		top         : ' ',
 86 | 		bottom      : ' ',
 87 | 		left        : ' ',
 88 | 		right       : ' ',
 89 | 		bg          : ' ',
 90 | 	}
 91 | }
 92 | 
 93 | // The glyphs to draw a shadow.
 94 | const shadowStyles = {
 95 | 	light : {
 96 | 		char   : '░',
 97 | 	},
 98 | 	medium : {
 99 | 		char   : '▒',
100 | 	},
101 | 	dark : {
102 | 		char   : '▓',
103 | 	},
104 | 	solid : {
105 | 		char   : '█',
106 | 	},
107 | 	checker : {
108 | 		char   : '▚',
109 | 	},
110 | 	x : {
111 | 		char   : '╳',
112 | 	},
113 | 	gray : {
114 | 		color : 'dimgray',
115 | 		backgroundColor : 'lightgray'
116 | 	},
117 | 	none : {
118 | 	}
119 | }
120 | 
121 | const defaultTextBoxStyle = {
122 | 	x               : 2,
123 | 	y               : 1,
124 | 	width           : 0, // auto width
125 | 	height          : 0, // auto height
126 | 	paddingX        : 2, // text offset from the left border
127 | 	paddingY        : 1, // text offset from the top border
128 | 	backgroundColor : 'white',
129 | 	color           : 'black',
130 | 	fontWeight      : 'normal',
131 | 	shadowStyle     : 'none',
132 | 	borderStyle     : 'round',
133 | 	shadowX         : 2, // horizontal shadow offset
134 | 	shadowY         : 1, // vertical shadow offset
135 | }
136 | 
137 | import { wrap, measure } from './string.js'
138 | import { merge, setRect, mergeRect, mergeText } from './buffer.js'
139 | 
140 | export function drawBox(text, style, target, targetCols, targetRows) {
141 | 
142 | 	const s = {...defaultTextBoxStyle, ...style}
143 | 
144 | 	let boxWidth  = s.width
145 | 	let boxHeight = s.height
146 | 
147 | 	if (!boxWidth || !boxHeight) {
148 | 		const m = measure(text)
149 | 		boxWidth = boxWidth || m.maxWidth + s.paddingX * 2
150 | 		boxHeight = boxHeight || m.numLines + s.paddingY * 2
151 | 	}
152 | 
153 | 	const x1 = s.x
154 | 	const y1 = s.y
155 | 	const x2 = s.x + boxWidth - 1
156 | 	const y2 = s.y + boxHeight - 1
157 | 	const w  = boxWidth
158 | 	const h  = boxHeight
159 | 
160 | 	const border = borderStyles[s.borderStyle] || borderStyles['round']
161 | 
162 | 	// Background, overwrite the buffer
163 | 	setRect({
164 | 		char       : border.bg,
165 | 		color      : s.color,
166 | 		fontWeight     : s.fontWeight,
167 | 		backgroundColor : s.backgroundColor
168 | 	}, x1, y1, w, h, target, targetCols, targetRows)
169 | 
170 | 	// Corners
171 | 	merge({ char : border.topleft     }, x1, y1, target, targetCols, targetRows)
172 | 	merge({ char : border.topright    }, x2, y1, target, targetCols, targetRows)
173 | 	merge({ char : border.bottomright }, x2, y2, target, targetCols, targetRows)
174 | 	merge({ char : border.bottomleft  }, x1, y2, target, targetCols, targetRows)
175 | 
176 | 	// Top & Bottom
177 | 	mergeRect({ char : border.top    }, x1+1, y1, w-2, 1, target, targetCols, targetRows)
178 | 	mergeRect({ char : border.bottom }, x1+1, y2, w-2, 1, target, targetCols, targetRows)
179 | 
180 | 	// Left & Right
181 | 	mergeRect({ char : border.left  }, x1, y1+1, 1, h-2, target, targetCols, targetRows)
182 | 	mergeRect({ char : border.right }, x2, y1+1, 1, h-2, target, targetCols, targetRows)
183 | 
184 | 	// Shadows
185 | 	const ss = shadowStyles[s.shadowStyle] || shadowStyles['none']
186 | 	if (ss !== shadowStyles['none']) {
187 | 		const ox = s.shadowX
188 | 		const oy = s.shadowY
189 | 		// Shadow Bottom
190 | 		mergeRect(ss, x1+ox, y2+1, w, oy, target, targetCols, targetRows)
191 | 		// Shadow Right
192 | 		mergeRect(ss, x2+1, y1+oy, ox, h-oy, target, targetCols, targetRows)
193 | 	}
194 | 
195 | 	// Txt
196 | 	mergeText({
197 | 		text,
198 | 		color : style.color,
199 | 		backgroundColor : style.backgroundColor,
200 | 		fontWeight : style.weght
201 | 	}, x1+s.paddingX, y1+s.paddingY, target, targetCols, targetRows)
202 | }
203 | 
204 | // -- Utility for some info output ---------------------------------------------
205 | 
206 | const defaultInfoStyle = {
207 | 	width           : 24,
208 | 	backgroundColor : 'white',
209 | 	color           : 'black',
210 | 	fontWeight      : 'normal',
211 | 	shadowStyle     : 'none',
212 | 	borderStyle     : 'round',
213 | }
214 | 
215 | export function drawInfo(context, cursor, target, style) {
216 | 
217 | 	let info = ''
218 | 	info += 'FPS         ' + Math.round(context.runtime.fps) + '\n'
219 | 	info += 'frame       ' + context.frame + '\n'
220 | 	info += 'time        ' + Math.floor(context.time) + '\n'
221 | 	info += 'size        ' + context.cols + '×' + context.rows + '\n'
222 | 	// info += 'row repaint ' + context.runtime.updatedRowNum + '\n'
223 | 	info += 'font aspect ' + context.metrics.aspect.toFixed(2) + '\n'
224 | 	info += 'cursor      ' + Math.floor(cursor.x) + ',' + Math.floor(cursor.y) + '\n'
225 | 	// NOTE: width and height can be a float in case of user zoom
226 | 	// info += 'context      ' + Math.floor(context.width) + '×' + Math.floor(context.height) + '\n'
227 | 
228 | 	const textBoxStyle = {...defaultInfoStyle, ...style}
229 | 
230 | 	drawBox(info, textBoxStyle, target, context.cols, context.rows)
231 | }
232 | 
```

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

```javascript
  1 | /**
  2 | [header]
  3 | @author zspotter
  4 |         (IG @zzz_desu, TW @zspotter)
  5 | @title  Slime Dish
  6 | @desc   Low-res physarum slime mold simulation
  7 | 
  8 | 🔍  Tap and hold to magnify.
  9 | 
 10 | With inspiration from:
 11 | - https://sagejenson.com/physarum
 12 | - https://uwe-repository.worktribe.com/output/980579
 13 | - http://www.tech-algorithm.com/articles/nearest-neighbor-image-scaling
 14 | */
 15 | 
 16 | import * as v2 from '/src/modules/vec2.js'
 17 | import { map } from '/src/modules/num.js'
 18 | 
 19 | // Environment
 20 | const WIDTH = 400;
 21 | const HEIGHT = 400;
 22 | const NUM_AGENTS = 1500;
 23 | const DECAY = 0.9;
 24 | const MIN_CHEM = 0.0001;
 25 | 
 26 | // Agents
 27 | const SENS_ANGLE = 45 * Math.PI / 180;
 28 | const SENS_DIST = 9;
 29 | const AGT_SPEED = 1;
 30 | const AGT_ANGLE = 45 * Math.PI / 180;
 31 | const DEPOSIT = 1;
 32 | 
 33 | // Rendering
 34 | const TEXTURE = [
 35 | 	"  ``^@",
 36 | 	" ..„v0",
 37 | ];
 38 | const OOB = ' ';
 39 | 
 40 | // 0@0@0@0@0@0^v^v^v^v^v^„`„`„`„`„`„`.`.`.`.`.`.`. . . . . .
 41 | // @0@0@0@0@0@v^v^v^v^v^v`„`„`„`„`„`„`.`.`.`.`.`.`. . . . . .
 42 | // 0@0@0@0@0@0^v^v^v^v^v^„`„`„`„`„`„`.`.`.`.`.`.`. . . . . .
 43 | // @0@0@0@0@0@v^v^v^v^v^v`„`„`„`„`„`„`.`.`.`.`.`.`. . . . . .
 44 | 
 45 | export const settings = {
 46 | 	backgroundColor : 'black',
 47 | 	color : 'white',
 48 | 	fontSize: '12px',
 49 | }
 50 | 
 51 | export function boot(context, buffer, data) {
 52 | 	document.body.style.cursor = 'crosshair';
 53 | 
 54 | 	data.chem = new Float32Array(HEIGHT*WIDTH);
 55 | 	data.wip = new Float32Array(HEIGHT*WIDTH);
 56 | 
 57 | 	data.agents = [];
 58 | 	for (let agent = 0; agent < NUM_AGENTS; agent++) {
 59 | 		data.agents.push(new Agent(
 60 | 			// Random position
 61 | 			v2.mulN(v2.addN(v2.mulN(randCircle(), 0.5), 1), 0.5 * WIDTH),
 62 | 			// Random direction
 63 | 			v2.rot(v2.vec2(1, 0), Math.random()*2*Math.PI),
 64 | 		));
 65 | 	}
 66 | 
 67 | 	data.viewScale = { y: 100/context.metrics.aspect, x: 100 };
 68 | 	data.viewFocus = { y: 0.5, x: 0.5 };
 69 | }
 70 | 
 71 | export function pre(context, cursor, buffer, data) {
 72 | 	// Diffuse & decay
 73 | 	for (let row = 0; row < HEIGHT; row++) {
 74 | 		for (let col = 0; col < WIDTH; col++) {
 75 | 			let val = DECAY * blur(row, col, data.chem);
 76 | 			if (val < MIN_CHEM)
 77 | 				val = 0;
 78 | 			data.wip[row*HEIGHT+col] = val;
 79 | 		}
 80 | 	}
 81 | 	const swap = data.chem;
 82 | 	data.chem = data.wip;
 83 | 	data.wip = swap;
 84 | 
 85 | 	const { chem, agents, view } = data;
 86 | 
 87 | 	// Sense, rotate, and move
 88 | 	const isScattering = Math.sin(context.frame/150) > 0.8;
 89 | 	for (const agent of agents) {
 90 | 		agent.scatter = isScattering;
 91 | 		agent.react(chem);
 92 | 	}
 93 | 
 94 | 	// Deposit
 95 | 	for (const agent of agents) {
 96 | 		agent.deposit(chem);
 97 | 	}
 98 | 
 99 | 	// Update view params
100 | 	updateView(cursor, context, data);
101 | }
102 | 
103 | export function main(coord, context, cursor, buffer, data) {
104 | 	const { viewFocus, viewScale } = data;
105 | 
106 | 	// A down and upscaling algorithm based on nearest-neighbor image scaling.
107 | 
108 | 	const offset = {
109 | 		y: Math.floor(viewFocus.y * (HEIGHT - viewScale.y * context.rows)),
110 | 		x: Math.floor(viewFocus.x * (WIDTH - viewScale.x * context.cols)),
111 | 	};
112 | 
113 | 	// The "nearest neighbor"
114 | 	const sampleFrom = {
115 | 		y: offset.y + Math.floor(coord.y * viewScale.y),
116 | 		x: offset.x + Math.floor(coord.x * viewScale.x),
117 | 	};
118 | 
119 | 	// The next nearest-neighbor cell, which we look up to the border of
120 | 	const sampleTo = {
121 | 		y: offset.y + Math.floor((coord.y+1) * viewScale.y),
122 | 		x: offset.x + Math.floor((coord.x+1) * viewScale.x),
123 | 	};
124 | 
125 | 	if (!bounded(sampleFrom) || !bounded(sampleTo))
126 | 		return OOB;
127 | 
128 | 	// When upscaling, sample W/H may be 0
129 | 	const sampleH = Math.max(1, sampleTo.y - sampleFrom.y);
130 | 	const sampleW = Math.max(1, sampleTo.x - sampleFrom.x);
131 | 
132 | 	// Combine all cells in [sampleFrom, sampleTo) into a single value.
133 | 	// For this case, the value half way between the average and max works well.
134 | 	let max = 0;
135 | 	let sum = 0;
136 | 	for (let x = sampleFrom.x; x < sampleFrom.x + sampleW; x++) {
137 | 		for (let y = sampleFrom.y; y < sampleFrom.y + sampleH; y++) {
138 | 			const v = data.chem[y*HEIGHT+x];
139 | 			max = Math.max(max, v);
140 | 			sum += v;
141 | 		}
142 | 	}
143 | 	let val = sum / (sampleW * sampleH);
144 | 	val = (val + max) / 2;
145 | 
146 | 	// Weight val so we get better distribution of textures
147 | 	val = Math.pow(val, 1/3);
148 | 
149 | 	// Convert the cell value into a character from the texture map
150 | 	const texRow = (coord.x+coord.y) % TEXTURE.length;
151 | 	const texCol = Math.ceil(val * (TEXTURE[0].length-1));
152 | 	const char = TEXTURE[texRow][texCol];
153 | 	if (!char)
154 | 		throw new Error(`Invalid char for ${val}`);
155 | 
156 | 	return char;
157 | }
158 | 
159 | // import { drawInfo } from '/src/modules/drawbox.js'
160 | // export function post(context, cursor, buffer) {
161 | // 	drawInfo(context, cursor, buffer)
162 | // }
163 | 
164 | function updateView(cursor, context, data) {
165 | 	let targetScale;
166 | 	if (cursor.pressed) {
167 | 		// 1 display char = 1 grid cell
168 | 		targetScale = {
169 | 			y: 1 / context.metrics.aspect,
170 | 			x: 1,
171 | 		};
172 | 	}
173 | 	else if (context.rows / context.metrics.aspect < context.cols) {
174 | 		// Fit whole grid on wide window
175 | 		targetScale = {
176 | 			y: 1.1*WIDTH / context.rows,
177 | 			x: 1.1*WIDTH / context.rows * context.metrics.aspect,
178 | 		};
179 | 	}
180 | 	else {
181 | 		// Fit whole grid on tall window
182 | 		targetScale = {
183 | 			y: 1.1*WIDTH / context.cols / context.metrics.aspect,
184 | 			x: 1.1*WIDTH / context.cols,
185 | 		};
186 | 	}
187 | 
188 | 	if (data.viewScale.y !== targetScale.y || data.viewScale.x !== targetScale.x) {
189 | 		data.viewScale.y += 0.1 * (targetScale.y - data.viewScale.y);
190 | 		data.viewScale.x += 0.1 * (targetScale.x - data.viewScale.x);
191 | 	}
192 | 
193 | 	let targetFocus = !cursor.pressed
194 | 		? { y: 0.5, x: 0.5 }
195 | 		: { y: cursor.y / context.rows, x: cursor.x / context.cols };
196 | 	if (data.viewFocus.y !== targetFocus.y || data.viewFocus.x !== targetFocus.x) {
197 | 		data.viewFocus.y += 0.1 * (targetFocus.y - data.viewFocus.y);
198 | 		data.viewFocus.x += 0.1 * (targetFocus.x - data.viewFocus.x);
199 | 	}
200 | }
201 | 
202 | // 0@0@0@0@0@0^v^v^v^v^v^„`„`„`„`„`„`.`.`.`.`.`.`. . . . . .
203 | // @0@0@0@0@0@v^v^v^v^v^v`„`„`„`„`„`„`.`.`.`.`.`.`. . . . . .
204 | // 0@0@0@0@0@0^v^v^v^v^v^„`„`„`„`„`„`.`.`.`.`.`.`. . . . . .
205 | // @0@0@0@0@0@v^v^v^v^v^v`„`„`„`„`„`„`.`.`.`.`.`.`. . . . . .
206 | 
207 | class Agent {
208 | 	constructor(pos, dir) {
209 | 		this.pos = pos;
210 | 		this.dir = dir;
211 | 		this.scatter = false;
212 | 	}
213 | 
214 | 	sense(m, chem) {
215 | 		const senseVec = v2.mulN(v2.rot(this.dir, m * SENS_ANGLE), SENS_DIST);
216 | 		const pos = v2.floor(v2.add(this.pos, senseVec));
217 | 		if (!bounded(pos))
218 | 			return -1;
219 | 		const sensed = chem[pos.y*HEIGHT+pos.x];
220 | 		if (this.scatter)
221 | 			return 1 - sensed;
222 | 		return sensed;
223 | 	}
224 | 
225 | 	react(chem) {
226 | 		// Sense
227 | 		let forwardChem = this.sense(0, chem);
228 | 		let leftChem = this.sense(-1, chem);
229 | 		let rightChem = this.sense(1, chem);
230 | 
231 | 		// Rotate
232 | 		let rotate = 0;
233 | 		if (forwardChem > leftChem && forwardChem > rightChem) {
234 | 			rotate = 0;
235 | 		}
236 | 		else if (forwardChem < leftChem && forwardChem < rightChem) {
237 | 			if (Math.random() < 0.5) {
238 | 				rotate = -AGT_ANGLE;
239 | 			}
240 | 			else {
241 | 				rotate = AGT_ANGLE;
242 | 			}
243 | 		}
244 | 		else if (leftChem < rightChem) {
245 | 			rotate = AGT_ANGLE;
246 | 		}
247 | 		else if (rightChem < leftChem) {
248 | 			rotate = -AGT_ANGLE;
249 | 		}
250 | 		else if (forwardChem < 0) {
251 | 			// Turn around at edge
252 | 			rotate = Math.PI/2;
253 | 		}
254 | 		this.dir = v2.rot(this.dir, rotate);
255 | 
256 | 		// Move
257 | 		this.pos = v2.add(this.pos, v2.mulN(this.dir, AGT_SPEED));
258 | 	}
259 | 
260 | 	deposit(chem) {
261 | 		const {y, x} = v2.floor(this.pos);
262 | 		const i = y*HEIGHT+x;
263 | 		chem[i] = Math.min(1, chem[i] + DEPOSIT);
264 | 	}
265 | }
266 | 
267 | const R = Math.min(WIDTH, HEIGHT)/2;
268 | 
269 | function bounded(vec) {
270 | 	return ((vec.x-R)**2 + (vec.y-R)**2 <= R**2);
271 | }
272 | 
273 | function blur(row, col, data) {
274 | 	let sum = 0;
275 | 	for (let dy = -1; dy <= 1; dy++) {
276 | 		for (let dx = -1; dx <= 1; dx++) {
277 | 			sum += data[(row+dy)*HEIGHT + col + dx] ?? 0;
278 | 		}
279 | 	}
280 | 	return sum / 9;
281 | }
282 | 
283 | function randCircle() {
284 | 	const r = Math.sqrt(Math.random());
285 | 	const theta = Math.random() * 2 * Math.PI;
286 | 	return {
287 | 		x: r * Math.cos(theta),
288 | 		y: r * Math.sin(theta)
289 | 	};
290 | }
```

--------------------------------------------------------------------------------
/src/modules/canvas.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 | @module   canvas.js
  3 | @desc     A wrapper for a canvas element
  4 | @category public
  5 | 
  6 | A canvas 'wrapper' class.
  7 | The purpose is to offer a ready to use buffer (a "pixel" array
  8 | of {r, g, b, (a, v)} objects) of the same size of the ASCII context (or not)
  9 | which can be read or sampled.
 10 | Some convenience functions are provided.
 11 | 
 12 | Resizes the canvas:
 13 | - resize(w, h)
 14 | 
 15 | Five main functions are implemented to copy a source (canvas, video, image)
 16 | to the internal canvas:
 17 | - image(source)  // resizes the canvas to the source image and copies it
 18 | - copy(source, ...)
 19 | - cover(source, ...)
 20 | - fit(source, ...)
 21 | - center(source, ...)
 22 | 
 23 | A call to these functions will also update the internal 'pixels' array trough:
 24 | - loadPixels()
 25 | 
 26 | A few extra functions are provided to manipulate the array directly:
 27 | - mirrorX()
 28 | - normalize() // only v values
 29 | - quantize()
 30 | 
 31 | Finally the whole buffer can be copied to a destination trough:
 32 | - writeTo()
 33 | 
 34 | Or accessed with:
 35 | - get(x, y)
 36 | - sample(x, y)
 37 | */
 38 | 
 39 | import { map, mix, clamp } from './num.js'
 40 | 
 41 | export const MODE_COVER  = Symbol()
 42 | export const MODE_FIT    = Symbol()
 43 | export const MODE_CENTER = Symbol()
 44 | 
 45 | const BLACK = { r:0, g:0, b:0, a:1, v:0 }
 46 | const WHITE = { r:255, g:255, b:255, a:1, v:1 }
 47 | 
 48 | export default class Canvas {
 49 | 
 50 | 	constructor(sourceCanvas) {
 51 | 		this.canvas = sourceCanvas || document.createElement('canvas')
 52 | 
 53 | 		// Initialize the canvas as a black 1x1 image so it can be used
 54 | 		this.canvas.width = 1
 55 | 		this.canvas.height = 1
 56 | 		this.ctx = this.canvas.getContext('2d')
 57 | 		this.ctx.putImageData(this.ctx.createImageData(1, 1), 0, 0);
 58 | 
 59 | 		// A flat buffer to store image data
 60 | 		// in the form of {r, g, b, [a, v]}
 61 | 		this.pixels = []
 62 | 		this.loadPixels()
 63 | 	}
 64 | 
 65 | 	get width() {
 66 | 		return this.canvas.width
 67 | 	}
 68 | 
 69 | 	get height() {
 70 | 		return this.canvas.height
 71 | 	}
 72 | 
 73 | 	// -- Functions that act on the canvas -------------------------------------
 74 | 
 75 | 	resize(dWidth, dHeight) {
 76 | 		this.canvas.width = dWidth
 77 | 		this.canvas.height = dHeight
 78 | 		this.pixels.length = 0
 79 | 		return this
 80 | 	}
 81 | 
 82 | 	// Copies the source canvas or video element to dest via drawImage
 83 | 	// allows distortion, offsets, etc.
 84 | 	copy(source, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) {
 85 | 
 86 | 		sx = sx || 0
 87 | 		sy = sy || 0
 88 | 		sWidth = sWidth || source.videoWidth || source.width
 89 | 		sHeight = sHeight || source.videoHeight || source.height
 90 | 
 91 | 		dx = dx || 0
 92 | 		dy = dy || 0
 93 | 		dWidth = dWidth || this.canvas.width
 94 | 		dHeight = dHeight || this.canvas.height
 95 | 
 96 | 		this.ctx.drawImage(source, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
 97 | 		this.loadPixels()
 98 | 
 99 | 		return this
100 | 	}
101 | 
102 | 	// Resizes the canvas to the size of the source image
103 | 	// and paints the image on it.
104 | 	image(source) {
105 | 		const w = source.videoWidth || source.width
106 | 		const h = source.videoHeight || source.height
107 | 		this.resize(w, h)
108 | 		this.copy(source, 0, 0, w, h, 0, 0, w, h)
109 | 		return this
110 | 	}
111 | 
112 | 	// Covers the destintation canvas with the source image
113 | 	// without resizing the canvas.
114 | 	// An otional aspect factor can be passed.
115 | 	cover(source, aspect=1) {
116 | 		centerImage(source, this.canvas, 1, aspect, MODE_COVER)
117 | 		this.loadPixels()
118 | 		return this
119 | 	}
120 | 
121 | 	// Fits the source image on the destintation canvas
122 | 	// without resizing the canvas.
123 | 	// An otional aspect factor can be passed.
124 | 	fit(source, aspect=1) {
125 | 		centerImage(source, this.canvas, 1, aspect, MODE_FIT)
126 | 		this.loadPixels()
127 | 		return this
128 | 	}
129 | 
130 | 	// Centers the source image on the destination canvas
131 | 	// without resizing the canvas.
132 | 	// Optional scaling factors can be passed.
133 | 	center(source, scaleX=1, scaleY=1) {
134 | 		centerImage(source, this.canvas, scaleX, scaleY, MODE_CENTER)
135 | 		this.loadPixels()
136 | 		return this
137 | 	}
138 | 
139 | 	// -- Functions that act directly on the pixel array -----------------------
140 | 
141 | 	mirrorX() {
142 | 		const w = this.canvas.width
143 | 		const h = this.canvas.height
144 | 		const buf = this.pixels
145 | 		const half = Math.floor(w / 2)
146 | 		for (let j=0; j<h; j++) {
147 | 			for (let i=0; i<half; i++) {
148 | 				const a = w * j + i
149 | 				const b = w * (j + 1) - i - 1
150 | 				const t = buf[b]
151 | 				buf[b] = buf[a]
152 | 				buf[a] = t
153 | 			}
154 | 		}
155 | 		return this
156 | 	}
157 | 
158 | 	normalize() {
159 | 		normalizeGray(this.pixels, this.pixels, 0.0, 1.0)
160 | 		return this
161 | 	}
162 | 
163 | 	quantize(palette) {
164 | 		paletteQuantize(this.pixels, this.pixels, palette)
165 | 		return this
166 | 	}
167 | 
168 | 	// -- Getters (pixel array) ------------------------------------------------
169 | 
170 | 	// Get color at coord
171 | 	get(x, y) {
172 | 		if (x < 0 || x >= this.canvas.width) return BLACK
173 | 		if (y < 0 || y >= this.canvas.height) return BLACK
174 | 		return this.pixels[x + y * this.canvas.width]
175 | 	}
176 | 
177 | 	// Sample value at coord (0-1)
178 | 	sample(sx, sy, gray=false) {
179 | 		const w = this.canvas.width
180 | 		const h = this.canvas.height
181 | 
182 | 	  	const x  = sx * w - 0.5
183 | 	  	const y  = sy * h - 0.5
184 | 
185 | 		let l = Math.floor(x)
186 |   		let b = Math.floor(y)
187 |   		let r = l + 1
188 |   		let t = b + 1
189 |   		const lr = x - l
190 |   		const bt = y - b
191 | 
192 |   		// Instead of clamping use safe "get()"
193 |   		// l = clamp(l, 0, w - 1) // left
194 |   		// r = clamp(r, 0, w - 1) // right
195 |   		// b = clamp(b, 0, h - 1) // bottom
196 |   		// t = clamp(t, 0, h - 1) // top
197 | 
198 |   		// Avoid 9 extra interpolations if only gray is needed
199 |   		if (gray) {
200 | 	  		const p1 = mix(this.get(l, b).v, this.get(r, b).v, lr)
201 | 	  		const p2 = mix(this.get(l, t).v, this.get(r, t).v, lr)
202 | 	  		return mix(p1, p2, bt)
203 | 	  	} else {
204 | 	  		const p1 = mixColors(this.get(l, b), this.get(r, b), lr)
205 | 	  		const p2 = mixColors(this.get(l, t), this.get(r, t), lr)
206 | 	  		return mixColors(p1, p2, bt)
207 | 	  	}
208 | 	}
209 | 
210 | 	// Read
211 | 	loadPixels() {
212 | 		// New data could be shorter,
213 | 		// empty without loosing the ref.
214 | 		this.pixels.length = 0
215 | 		const w = this.canvas.width
216 | 		const h = this.canvas.height
217 | 		const data = this.ctx.getImageData(0, 0, w, h).data
218 | 		let idx = 0
219 | 		for (let i=0; i<data.length; i+=4) {
220 | 			const r = data[i  ] // / 255.0,
221 | 			const g = data[i+1] // / 255.0,
222 | 			const b = data[i+2] // / 255.0,
223 | 			const a = data[i+3] / 255.0 // CSS style
224 | 			this.pixels[idx++] = {
225 | 				r, g, b, a,
226 | 				v : toGray(r, g, b)
227 | 			}
228 | 		}
229 | 		return this
230 | 	}
231 | 
232 | 	// -- Helpers --------------------------------------------------------------
233 | 
234 | 	writeTo(buf) {
235 | 		if (Array.isArray(buf)) {
236 | 			for (let i=0; i<this.pixels.length; i++) buf[i] = this.pixels[i]
237 | 		}
238 | 		return this
239 | 	}
240 | 
241 | 	// Debug -------------------------------------------------------------------
242 | 
243 | 	// Attaches the canvas to a target element for debug purposes
244 | 	display(target, x=0, y=0) {
245 | 		target = target || document.body
246 | 		this.canvas.style.position = 'absolute'
247 | 		this.canvas.style.left = x + 'px'
248 | 		this.canvas.style.top = y + 'px'
249 | 		this.canvas.style.width = 'auto'
250 | 		this.canvas.style.height = 'auto'
251 | 		this.canvas.style.zIndex = 10
252 | 		document.body.appendChild(this.canvas)
253 | 	}
254 | }
255 | 
256 | 
257 | // Helpers ---------------------------------------------------------------------
258 | 
259 | function mixColors(a, b, amt) {
260 | 	return {
261 | 		r : mix(a.r, b.r, amt),
262 | 		g : mix(a.g, b.g, amt),
263 | 		b : mix(a.b, b.b, amt),
264 | 		v : mix(a.v, b.v, amt)
265 | 	}
266 | }
267 | 
268 | function getElementSize(source) {
269 | 	const type = source.nodeName
270 | 	const width = type == 'VIDEO' ? source.videoWidth : source.width || 0
271 | 	const height = type == 'VIDEO' ? source.videoHeight : source.height || 0
272 | 	return { width, height }
273 | }
274 | 
275 | function centerImage(sourceCanvas, targetCanvas, scaleX=1, scaleY=1, mode=MODE_CENTER) {
276 | 
277 | 	// Source size
278 | 	const s = getElementSize(sourceCanvas)
279 | 
280 | 	// Source aspect (scaled)
281 | 	const sa = (scaleX * s.width) / (s.height * scaleY)
282 | 
283 | 	// Target size and aspect
284 | 	const tw = targetCanvas.width
285 | 	const th = targetCanvas.height
286 | 	const ta = tw / th
287 | 
288 | 	// Destination width and height (adjusted for cover / fit)
289 | 	let dw, dh
290 | 
291 | 	// Cover the entire dest canvas with image content
292 | 	if (mode == MODE_COVER) {
293 | 		if (sa > ta) {
294 | 			dw = th * sa
295 | 			dh = th
296 | 		} else {
297 | 			dw = tw
298 | 			dh = tw / sa
299 | 		}
300 | 	}
301 | 	// Fit the entire source image in dest tanvas (with black bars)
302 | 	else if (mode == MODE_FIT) {
303 | 		if (sa > ta) {
304 | 			dw = tw
305 | 			dh = tw / sa
306 | 		} else {
307 | 			dw = th * sa
308 | 			dh = th
309 | 		}
310 | 	}
311 | 	// Center the image
312 | 	else if (mode == MODE_CENTER) {
313 | 		dw = s.width * scaleX
314 | 		dh = s.height * scaleY
315 | 	}
316 | 
317 | 	// Update the targetCanvas with correct aspect ratios
318 | 	const ctx = targetCanvas.getContext('2d')
319 | 
320 | 	// Fill the canvas in case of 'fit'
321 | 	ctx.fillStyle = 'black'
322 | 	ctx.fillRect(0, 0, tw, th)
323 | 	ctx.save()
324 | 	ctx.translate(tw/2, th/2)
325 | 	ctx.drawImage(sourceCanvas, -dw/2, -dh/2, dw, dh)
326 | 	ctx.restore()
327 | }
328 | 
329 | // Use this or import 'rgb2gray' from color.js
330 | // https://en.wikipedia.org/wiki/Grayscale
331 | function toGray(r, g, b) {
332 | 	return Math.round(r * 0.2126 + g * 0.7152 + b * 0.0722) / 255.0
333 | }
334 | 
335 | function paletteQuantize(arrayIn, arrayOut, palette) {
336 | 	arrayOut = arrayOut || []
337 | 
338 | 	// Euclidean:
339 | 	// const distFn = (a, b) => Math.pow(a.r - b.r, 2) + Math.pow(a.g - b.g, 2) + Math.pow(a.b - b.b, 2)
340 | 
341 | 	// Redmean:
342 | 	// https://en.wikipedia.org/wiki/Color_difference
343 | 	const distFn = (a, b) => {
344 | 		const r = (a.r + b.r) * 0.5
345 | 		let s = 0
346 | 		s += (2 + r / 256) * Math.pow(a.r - b.r, 2)
347 | 		s += 4 * Math.pow(a.g - b.g, 2)
348 | 		s += (2 + (255 - r) / 256) * Math.pow(a.b - b.b, 2)
349 | 		return Math.sqrt(s)
350 | 	}
351 | 
352 | 	for (let i=0; i<arrayIn.length; i++) {
353 | 		const a = arrayIn[i]
354 | 		let dist = Number.MAX_VALUE
355 | 		let nearest
356 | 		for (const b of palette) {
357 | 			const d = distFn(a, b)
358 | 			if (d < dist) {
359 | 				dist = d
360 | 				nearest = b
361 | 			}
362 | 		}
363 | 		arrayOut[i] = {...nearest, v : arrayIn[i].v } // Keep the original gray value intact
364 | 	}
365 | 	return arrayOut
366 | }
367 | 
368 | // Normalizes the gray component (auto levels)
369 | function normalizeGray(arrayIn, arrayOut, lower=0.0, upper=1.0) {
370 | 	arrayOut = arrayOut || []
371 | 
372 | 	let min = Number.MAX_VALUE
373 | 	let max = 0
374 | 	for (let i=0; i<arrayIn.length; i++) {
375 | 		min = Math.min(arrayIn[i].v, min)
376 | 		max = Math.max(arrayIn[i].v, max)
377 | 	}
378 | 	// return target.map( v => {
379 | 	//     return map(v, min, max, 0, 1)
380 | 	// })
381 | 	for (let i=0; i<arrayIn.length; i++) {
382 | 		const v = min == max ? min : map(arrayIn[i].v, min, max, lower, upper)
383 | 		arrayOut[i] = {...arrayOut[i], v}
384 | 	}
385 | 	return arrayOut
386 | }
387 | 
```

--------------------------------------------------------------------------------
/src/modules/color.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 | @module   color.js
  3 | @desc     Some common palettes and simple color helpers
  4 | @category public
  5 | 
  6 | Colors can be defined as:
  7 | 
  8 | rgb : { r:255, g:0, b:0 }
  9 | int : 16711680 (0xff0000)
 10 | hex : '#FF0000'
 11 | css : 'rgb(255,0,0)'
 12 | 
 13 | CSS1 and CSS3 palettes are exported as maps
 14 | C64 and CGA palettes are exported as arrays
 15 | 
 16 | Most of the times colors are ready to use as in CSS:
 17 | this means r,g,b have 0-255 range but alpha 0-1
 18 | 
 19 | Colors in exported palettes are augmented to:
 20 | {
 21 | 	name : 'red',
 22 | 	r    : 255,        // 0-255 (as in CSS)
 23 | 	g    : 0,          // 0-255 (as in CSS)
 24 | 	b    : 0,          // 0-255 (as in CSS)
 25 | 	a    : 1.0,        // 0-1   (as in CSS)
 26 | 	v    : 0.6,        // 0-1   (gray value)
 27 | 	hex  : '#FF0000',
 28 | 	css  : 'rgb(255,0,0)'
 29 | 	int  : 16711680
 30 | }
 31 | 
 32 | */
 33 | 
 34 | // Convert r,g,b,a values to {r,g,b,a}
 35 | export function rgb(r,g,b,a=1.0) {
 36 | 	return {r,g,b,a}
 37 | }
 38 | 
 39 | // Convert r,g,b,a values to {r,g,b,a}
 40 | export function hex(r,g,b,a=1.0) {
 41 | 	return rgb2hex({r,g,b,a})
 42 | }
 43 | 
 44 | // Convert r,g,b,a values to 'rgb(r,g,b,a)'
 45 | export function css(r,g,b,a=1.0) {
 46 | 	if (a === 1.0) return `rgb(${r},${g},${b})`
 47 | 	// CSS3 (in CSS4 we could return rgb(r g b a))
 48 | 	return `rgba(${r},${g},${b},${a})`}
 49 | 
 50 | // Convert {r,g,b,a} values to 'rgb(r,g,b,a)'
 51 | export function rgb2css(rgb) {
 52 | 	if (rgb.a === undefined || rgb.a === 1.0) {
 53 | 		return `rgb(${rgb.r},${rgb.g},${rgb.b})`
 54 | 	}
 55 | 	// CSS3 (in CSS4 we could return rgb(r g b a))
 56 | 	return `rgba(${rgb.r},${rgb.g},${rgb.b},${rgb.a})`
 57 | }
 58 | 
 59 | // Convert {r,g,b} values to '#RRGGBB' or '#RRGGBBAA'
 60 | export function rgb2hex(rgb) {
 61 | 
 62 | 	let r = Math.round(rgb.r).toString(16).padStart(2, '0')
 63 | 	let g = Math.round(rgb.g).toString(16).padStart(2, '0')
 64 | 	let b = Math.round(rgb.b).toString(16).padStart(2, '0')
 65 | 
 66 |   	// Alpha not set
 67 | 	if (rgb.a === undefined) {
 68 | 		return '#' + r + g + b
 69 | 	}
 70 | 
 71 | 	let a = Math.round(rgb.a * 255).toString(16).padStart(2, '0')
 72 | 	return '#' + r + g + b + a
 73 | }
 74 | 
 75 | // Convert {r,g,b} values to gray value [0-1]
 76 | export function rgb2gray(rgb) {
 77 | 	return Math.round(rgb.r * 0.2126 + rgb.g * 0.7152 + rgb.b * 0.0722) / 255.0
 78 | }
 79 | 
 80 | // hex is not a string but an int number
 81 | export function int2rgb(int) {
 82 | 	return {
 83 | 		a : 1.0,
 84 | 		r : int >> 16 & 0xff,
 85 | 		g : int >>  8 & 0xff,
 86 | 		b : int       & 0xff
 87 | 	}
 88 | }
 89 | 
 90 | // export function int2hex(int) {
 91 | // 	return '#' + (int).toString(16)
 92 | // }
 93 | 
 94 | // https://www.c64-wiki.com/wiki/Color
 95 | const _C64 = [
 96 | 	{ int : 0x000000, name : 'black' },       //  0
 97 | 	{ int : 0xffffff, name : 'white' },       //  1
 98 | 	{ int : 0x880000, name : 'red' },         //  2
 99 | 	{ int : 0xaaffee, name : 'cyan' },        //  3
100 | 	{ int : 0xcc44cc, name : 'violet' },      //  4
101 | 	{ int : 0x00cc55, name : 'green' },       //  5
102 | 	{ int : 0x0000aa, name : 'blue' },        //  6
103 | 	{ int : 0xeeee77, name : 'yellow' },      //  7
104 | 	{ int : 0xdd8855, name : 'orange' },      //  8
105 | 	{ int : 0x664400, name : 'brown' },       //  9
106 | 	{ int : 0xff7777, name : 'lightred' },    // 10
107 | 	{ int : 0x333333, name : 'darkgrey' },    // 11
108 | 	{ int : 0x777777, name : 'grey' },        // 12
109 | 	{ int : 0xaaff66, name : 'lightgreen' },  // 13
110 | 	{ int : 0x0088ff, name : 'lightblue' },   // 14
111 | 	{ int : 0xbbbbbb, name : 'lightgrey' }    // 15
112 | ]
113 | 
114 | const _CGA = [
115 | 	{ int : 0x000000, name : 'black' },        //  0
116 | 	{ int : 0x0000aa, name : 'blue' },         //  1
117 | 	{ int : 0x00aa00, name : 'green' },        //  2
118 | 	{ int : 0x00aaaa, name : 'cyan' },         //  3
119 | 	{ int : 0xaa0000, name : 'red' },          //  4
120 | 	{ int : 0xaa00aa, name : 'magenta' },      //  5
121 | 	{ int : 0xaa5500, name : 'brown' },        //  6
122 | 	{ int : 0xaaaaaa, name : 'lightgray' },    //  7
123 | 	{ int : 0x555555, name : 'darkgray' },     //  8
124 | 	{ int : 0x5555ff, name : 'lightblue' },    //  9
125 | 	{ int : 0x55ff55, name : 'lightgreen' },   // 10
126 | 	{ int : 0x55ffff, name : 'lightcyan' },    // 11
127 | 	{ int : 0xff5555, name : 'lightred' },     // 12
128 | 	{ int : 0xff55ff, name : 'lightmagenta' }, // 13
129 | 	{ int : 0xffff55, name : 'yellow' },       // 14
130 | 	{ int : 0xffffff, name : 'white' }         // 15
131 | ]
132 | 
133 | // https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
134 | const _CSS1 = [
135 | 	{ int : 0x000000, name : 'black' },
136 | 	{ int : 0xc0c0c0, name : 'silver' },
137 | 	{ int : 0x808080, name : 'gray' },
138 | 	{ int : 0xffffff, name : 'white' },
139 | 	{ int : 0x800000, name : 'maroon' },
140 | 	{ int : 0xff0000, name : 'red' },
141 | 	{ int : 0x800080, name : 'purple' },
142 | 	{ int : 0xff00ff, name : 'fuchsia' },
143 | 	{ int : 0x008000, name : 'green' },
144 | 	{ int : 0x00ff00, name : 'lime' },
145 | 	{ int : 0x808000, name : 'olive' },
146 | 	{ int : 0xffff00, name : 'yellow' },
147 | 	{ int : 0x000080, name : 'navy' },
148 | 	{ int : 0x0000ff, name : 'blue' },
149 | 	{ int : 0x008080, name : 'teal' },
150 | 	{ int : 0x00ffff, name : 'aqua' }
151 | ]
152 | 
153 | const _CSS2 = [..._CSS1,
154 | 	{int : 0xffa500, name :'orange'}
155 | ]
156 | 
157 | const _CSS3 = [..._CSS2,
158 | 	{ int : 0xf0f8ff, name : 'aliceblue' },
159 | 	{ int : 0xfaebd7, name : 'antiquewhite' },
160 | 	{ int : 0x7fffd4, name : 'aquamarine' },
161 | 	{ int : 0xf0ffff, name : 'azure' },
162 | 	{ int : 0xf5f5dc, name : 'beige' },
163 | 	{ int : 0xffe4c4, name : 'bisque' },
164 | 	{ int : 0xffebcd, name : 'blanchedalmond' },
165 | 	{ int : 0x8a2be2, name : 'blueviolet' },
166 | 	{ int : 0xa52a2a, name : 'brown' },
167 | 	{ int : 0xdeb887, name : 'burlywood' },
168 | 	{ int : 0x5f9ea0, name : 'cadetblue' },
169 | 	{ int : 0x7fff00, name : 'chartreuse' },
170 | 	{ int : 0xd2691e, name : 'chocolate' },
171 | 	{ int : 0xff7f50, name : 'coral' },
172 | 	{ int : 0x6495ed, name : 'cornflowerblue' },
173 | 	{ int : 0xfff8dc, name : 'cornsilk' },
174 | 	{ int : 0xdc143c, name : 'crimson' },
175 | 	{ int : 0x00ffff, name : 'aqua' },
176 | 	{ int : 0x00008b, name : 'darkblue' },
177 | 	{ int : 0x008b8b, name : 'darkcyan' },
178 | 	{ int : 0xb8860b, name : 'darkgoldenrod' },
179 | 	{ int : 0xa9a9a9, name : 'darkgray' },
180 | 	{ int : 0x006400, name : 'darkgreen' },
181 | 	{ int : 0xa9a9a9, name : 'darkgrey' },
182 | 	{ int : 0xbdb76b, name : 'darkkhaki' },
183 | 	{ int : 0x8b008b, name : 'darkmagenta' },
184 | 	{ int : 0x556b2f, name : 'darkolivegreen' },
185 | 	{ int : 0xff8c00, name : 'darkorange' },
186 | 	{ int : 0x9932cc, name : 'darkorchid' },
187 | 	{ int : 0x8b0000, name : 'darkred' },
188 | 	{ int : 0xe9967a, name : 'darksalmon' },
189 | 	{ int : 0x8fbc8f, name : 'darkseagreen' },
190 | 	{ int : 0x483d8b, name : 'darkslateblue' },
191 | 	{ int : 0x2f4f4f, name : 'darkslategray' },
192 | 	{ int : 0x2f4f4f, name : 'darkslategrey' },
193 | 	{ int : 0x00ced1, name : 'darkturquoise' },
194 | 	{ int : 0x9400d3, name : 'darkviolet' },
195 | 	{ int : 0xff1493, name : 'deeppink' },
196 | 	{ int : 0x00bfff, name : 'deepskyblue' },
197 | 	{ int : 0x696969, name : 'dimgray' },
198 | 	{ int : 0x696969, name : 'dimgrey' },
199 | 	{ int : 0x1e90ff, name : 'dodgerblue' },
200 | 	{ int : 0xb22222, name : 'firebrick' },
201 | 	{ int : 0xfffaf0, name : 'floralwhite' },
202 | 	{ int : 0x228b22, name : 'forestgreen' },
203 | 	{ int : 0xdcdcdc, name : 'gainsboro' },
204 | 	{ int : 0xf8f8ff, name : 'ghostwhite' },
205 | 	{ int : 0xffd700, name : 'gold' },
206 | 	{ int : 0xdaa520, name : 'goldenrod' },
207 | 	{ int : 0xadff2f, name : 'greenyellow' },
208 | 	{ int : 0x808080, name : 'grey' },
209 | 	{ int : 0xf0fff0, name : 'honeydew' },
210 | 	{ int : 0xff69b4, name : 'hotpink' },
211 | 	{ int : 0xcd5c5c, name : 'indianred' },
212 | 	{ int : 0x4b0082, name : 'indigo' },
213 | 	{ int : 0xfffff0, name : 'ivory' },
214 | 	{ int : 0xf0e68c, name : 'khaki' },
215 | 	{ int : 0xe6e6fa, name : 'lavender' },
216 | 	{ int : 0xfff0f5, name : 'lavenderblush' },
217 | 	{ int : 0x7cfc00, name : 'lawngreen' },
218 | 	{ int : 0xfffacd, name : 'lemonchiffon' },
219 | 	{ int : 0xadd8e6, name : 'lightblue' },
220 | 	{ int : 0xf08080, name : 'lightcoral' },
221 | 	{ int : 0xe0ffff, name : 'lightcyan' },
222 | 	{ int : 0xfafad2, name : 'lightgoldenrodyellow' },
223 | 	{ int : 0xd3d3d3, name : 'lightgray' },
224 | 	{ int : 0x90ee90, name : 'lightgreen' },
225 | 	{ int : 0xd3d3d3, name : 'lightgrey' },
226 | 	{ int : 0xffb6c1, name : 'lightpink' },
227 | 	{ int : 0xffa07a, name : 'lightsalmon' },
228 | 	{ int : 0x20b2aa, name : 'lightseagreen' },
229 | 	{ int : 0x87cefa, name : 'lightskyblue' },
230 | 	{ int : 0x778899, name : 'lightslategray' },
231 | 	{ int : 0x778899, name : 'lightslategrey' },
232 | 	{ int : 0xb0c4de, name : 'lightsteelblue' },
233 | 	{ int : 0xffffe0, name : 'lightyellow' },
234 | 	{ int : 0x32cd32, name : 'limegreen' },
235 | 	{ int : 0xfaf0e6, name : 'linen' },
236 | 	{ int : 0xff00ff, name : 'fuchsia' },
237 | 	{ int : 0x66cdaa, name : 'mediumaquamarine' },
238 | 	{ int : 0x0000cd, name : 'mediumblue' },
239 | 	{ int : 0xba55d3, name : 'mediumorchid' },
240 | 	{ int : 0x9370db, name : 'mediumpurple' },
241 | 	{ int : 0x3cb371, name : 'mediumseagreen' },
242 | 	{ int : 0x7b68ee, name : 'mediumslateblue' },
243 | 	{ int : 0x00fa9a, name : 'mediumspringgreen' },
244 | 	{ int : 0x48d1cc, name : 'mediumturquoise' },
245 | 	{ int : 0xc71585, name : 'mediumvioletred' },
246 | 	{ int : 0x191970, name : 'midnightblue' },
247 | 	{ int : 0xf5fffa, name : 'mintcream' },
248 | 	{ int : 0xffe4e1, name : 'mistyrose' },
249 | 	{ int : 0xffe4b5, name : 'moccasin' },
250 | 	{ int : 0xffdead, name : 'navajowhite' },
251 | 	{ int : 0xfdf5e6, name : 'oldlace' },
252 | 	{ int : 0x6b8e23, name : 'olivedrab' },
253 | 	{ int : 0xff4500, name : 'orangered' },
254 | 	{ int : 0xda70d6, name : 'orchid' },
255 | 	{ int : 0xeee8aa, name : 'palegoldenrod' },
256 | 	{ int : 0x98fb98, name : 'palegreen' },
257 | 	{ int : 0xafeeee, name : 'paleturquoise' },
258 | 	{ int : 0xdb7093, name : 'palevioletred' },
259 | 	{ int : 0xffefd5, name : 'papayawhip' },
260 | 	{ int : 0xffdab9, name : 'peachpuff' },
261 | 	{ int : 0xcd853f, name : 'peru' },
262 | 	{ int : 0xffc0cb, name : 'pink' },
263 | 	{ int : 0xdda0dd, name : 'plum' },
264 | 	{ int : 0xb0e0e6, name : 'powderblue' },
265 | 	{ int : 0xbc8f8f, name : 'rosybrown' },
266 | 	{ int : 0x4169e1, name : 'royalblue' },
267 | 	{ int : 0x8b4513, name : 'saddlebrown' },
268 | 	{ int : 0xfa8072, name : 'salmon' },
269 | 	{ int : 0xf4a460, name : 'sandybrown' },
270 | 	{ int : 0x2e8b57, name : 'seagreen' },
271 | 	{ int : 0xfff5ee, name : 'seashell' },
272 | 	{ int : 0xa0522d, name : 'sienna' },
273 | 	{ int : 0x87ceeb, name : 'skyblue' },
274 | 	{ int : 0x6a5acd, name : 'slateblue' },
275 | 	{ int : 0x708090, name : 'slategray' },
276 | 	{ int : 0x708090, name : 'slategrey' },
277 | 	{ int : 0xfffafa, name : 'snow' },
278 | 	{ int : 0x00ff7f, name : 'springgreen' },
279 | 	{ int : 0x4682b4, name : 'steelblue' },
280 | 	{ int : 0xd2b48c, name : 'tan' },
281 | 	{ int : 0xd8bfd8, name : 'thistle' },
282 | 	{ int : 0xff6347, name : 'tomato' },
283 | 	{ int : 0x40e0d0, name : 'turquoise' },
284 | 	{ int : 0xee82ee, name : 'violet' },
285 | 	{ int : 0xf5deb3, name : 'wheat' },
286 | 	{ int : 0xf5f5f5, name : 'whitesmoke' },
287 | 	{ int : 0x9acd32, name : 'yellowgreen' }
288 | ]
289 | 
290 | const _CSS4 = [..._CSS3,
291 | 	{ int: 0x663399, name : 'rebeccapurple'}
292 | ]
293 | 
294 | 
295 | // Helper function
296 | function augment(pal) {
297 | 	return pal.map(el => {
298 | 		const rgb = int2rgb(el.int)
299 | 		const hex = rgb2hex(rgb)
300 | 		const css = rgb2css(rgb)
301 | 		const v   = rgb2gray(rgb)
302 | 	 	return {...el, ...rgb, v, hex, css}
303 | 	})
304 | }
305 | 
306 | // Helper function
307 | function toMap(pal) {
308 | 	const out = {}
309 | 	pal.forEach(el => {
310 | 		out[el.name] = el
311 | 	})
312 | 	return out
313 | }
314 | 
315 | export const CSS4 = toMap(augment(_CSS4))
316 | export const CSS3 = toMap(augment(_CSS3))
317 | export const CSS2 = toMap(augment(_CSS2))
318 | export const CSS1 = toMap(augment(_CSS1))
319 | export const C64  = augment(_C64)
320 | export const CGA  = augment(_CGA)
321 | 
322 | 
```

--------------------------------------------------------------------------------
/src/run.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 | Runner
  3 | */
  4 | 
  5 | // Both available renderers are imported
  6 | import textRenderer from './core/textrenderer.js'
  7 | import canvasRenderer from './core/canvasrenderer.js'
  8 | import FPS from './core/fps.js'
  9 | import storage from './core/storage.js'
 10 | import RUNNER_VERSION from './core/version.js'
 11 | 
 12 | export { RUNNER_VERSION }
 13 | 
 14 | const renderers = {
 15 | 	'canvas' : canvasRenderer,
 16 | 	'text'   : textRenderer
 17 | }
 18 | 
 19 | // Default settings for the program runner.
 20 | // They can be overwritten by the parameters of the runner
 21 | // or as a settings object exported by the program (in this order).
 22 | const defaultSettings = {
 23 | 	element         : null,    // target element for output
 24 | 	cols            : 0,       // number of columns, 0 is equivalent to 'auto'
 25 | 	rows            : 0,       // number of columns, 0 is equivalent to 'auto'
 26 | 	once            : false,   // if set to true the renderer will run only once
 27 | 	fps             : 30,      // fps capping
 28 | 	renderer        : 'text',  // can be 'canvas', anything else falls back to 'text'
 29 | 	allowSelect     : false,   // allows selection of the rendered element
 30 | 	restoreState    : false,   // will store the "state" object in local storage
 31 | 	                           // this is handy for live-coding situations
 32 | }
 33 | 
 34 | // CSS styles which can be passed to the container element via settings
 35 | const CSSStyles = [
 36 | 	'backgroundColor',
 37 | 	'color',
 38 | 	'fontFamily',
 39 | 	'fontSize',
 40 | 	'fontWeight',
 41 | 	'letterSpacing',
 42 | 	'lineHeight',
 43 | 	'textAlign',
 44 | ]
 45 | 
 46 | // Program runner.
 47 | // Takes a program object (usually an imported module),
 48 | // and some optional settings (see above) as arguments.
 49 | // Finally, an optional userData object can be passed which will be available
 50 | // as last parameter in all the module functions.
 51 | // The program object should export at least a main(), pre() or post() function.
 52 | export function run(program, runSettings, userData = {}) {
 53 | 
 54 | 	// Everything is wrapped inside a promise;
 55 | 	// in case of errors in ‘program’ it will reject without reaching the bottom.
 56 | 	// If the program reaches the bottom of the first frame the promise is resolved.
 57 | 	return new Promise(function(resolve) {
 58 | 		// Merge of user- and default settings
 59 | 		const settings = {...defaultSettings, ...runSettings, ...program.settings}
 60 | 
 61 | 		// State is stored in local storage and will loaded on program launch
 62 | 		// if settings.restoreState == true.
 63 | 		// The purpose of this is to live edit the code without resetting
 64 | 		// time and the frame counter.
 65 | 		const state = {
 66 | 			time  : 0, // The time in ms
 67 | 			frame : 0, // The frame number (int)
 68 | 			cycle : 0  // An cycle count for debugging purposes
 69 | 		}
 70 | 
 71 | 		// Name of local storage key
 72 | 		const LOCAL_STORAGE_KEY_STATE = 'currentState'
 73 | 
 74 | 		if (settings.restoreState) {
 75 | 			storage.restore(LOCAL_STORAGE_KEY_STATE, state)
 76 | 			state.cycle++ // Keep track of the cycle count for debugging purposes
 77 | 		}
 78 | 
 79 | 		// If element is not provided create a default element based
 80 | 		// on the renderer settings.
 81 | 		// Then choose the renderer:
 82 | 		// If the parent element is a canvas the canvas renderer is selected,
 83 | 		// for any other type a text node (PRE or any othe text node)
 84 | 		// is expected and the text renderer is used.
 85 | 		// TODO: better / more generic renderer init
 86 | 		let renderer
 87 | 		if (!settings.element) {
 88 | 			renderer = renderers[settings.renderer] || renderers['text']
 89 | 			settings.element = document.createElement(renderer.preferredElementNodeName)
 90 | 			document.body.appendChild(settings.element)
 91 | 		} else {
 92 | 			if (settings.renderer == 'canvas') {
 93 | 				if (settings.element.nodeName == 'CANVAS') {
 94 | 					renderer = renderers[settings.renderer]
 95 | 				} else {
 96 | 					console.warn("This renderer expects a canvas target element.")
 97 | 				}
 98 | 			} else {
 99 | 				if (settings.element.nodeName != 'CANVAS') {
100 | 					renderer = renderers[settings.renderer]
101 | 				} else {
102 | 					console.warn("This renderer expects a text target element.")
103 | 				}
104 | 			}
105 | 		}
106 | 
107 | 		// Apply CSS settings to element
108 | 		for (const s of CSSStyles) {
109 | 			if (settings[s]) settings.element.style[s] = settings[s]
110 | 		}
111 | 
112 | 		// Eventqueue
113 | 		// Stores events and pops them at the end of the renderloop
114 | 		// TODO: needed?
115 | 		const eventQueue = []
116 | 
117 | 		// Input pointer updated by DOM events
118 | 		const pointer = {
119 | 			x        : 0,
120 | 			y        : 0,
121 | 			pressed  : false,
122 | 			px       : 0,
123 | 			py       : 0,
124 | 			ppressed : false,
125 | 		}
126 | 
127 | 		settings.element.addEventListener('pointermove', e => {
128 | 			const rect = settings.element.getBoundingClientRect()
129 | 			pointer.x = e.clientX - rect.left
130 | 			pointer.y = e.clientY - rect.top
131 | 			eventQueue.push('pointerMove')
132 | 		})
133 | 
134 | 		settings.element.addEventListener('pointerdown', e => {
135 | 			pointer.pressed = true
136 | 			eventQueue.push('pointerDown')
137 | 		})
138 | 
139 | 		settings.element.addEventListener('pointerup', e => {
140 | 			pointer.pressed = false
141 | 			eventQueue.push('pointerUp')
142 | 		})
143 | 		
144 | 		const touchHandler = e => {
145 | 			const rect = settings.element.getBoundingClientRect()
146 | 			pointer.x = e.touches[0].clientX - rect.left
147 | 			pointer.y = e.touches[0].clientY - rect.top
148 | 			eventQueue.push('pointerMove')
149 | 		}
150 | 
151 | 		settings.element.addEventListener('touchmove', touchHandler)
152 | 		settings.element.addEventListener('touchstart', touchHandler)
153 | 		settings.element.addEventListener('touchend', touchHandler)
154 | 
155 | 
156 | 		// CSS fix
157 | 		settings.element.style.fontStrech = 'normal'
158 | 
159 | 		// Text selection may be annoing in case of interactive programs
160 | 		if (!settings.allowSelect) disableSelect(settings.element)
161 | 
162 | 		// Method to load a font via the FontFace object.
163 | 		// The load promise works 100% of the times.
164 | 		// But a definition of the font via CSS is preferable and more flexible.
165 | 		/*
166 | 		const CSSInfo = getCSSInfo(settings.element)
167 | 		var font = new FontFace('Simple Console', 'url(/css/fonts/simple/SimpleConsole-Light.woff)', { style: 'normal', weight: 400 })
168 | 		font.load().then(function(f) {
169 | 			...
170 | 		})
171 | 		*/
172 | 
173 | 		// Metrics needs to be calculated before boot
174 | 		// Even with the "fonts.ready" the font may STILL not be loaded yet
175 | 		// on Safari 13.x and also 14.0.
176 | 		// A (shitty) workaround is to wait 3! rAF.
177 | 		// Submitted: https://bugs.webkit.org/show_bug.cgi?id=217047
178 | 		document.fonts.ready.then((e) => {
179 | 			// Run this three times...
180 | 			let count = 3
181 | 			;(function __run_thrice__() {
182 | 				if (--count > 0) {
183 | 					requestAnimationFrame(__run_thrice__)
184 | 				} else {
185 | 					// settings.element.style.lineHeight = Math.ceil(metrics.lineHeightf) + 'px'
186 | 					// console.log(`Using font faimily: ${ci.fontFamily} @ ${ci.fontSize}/${ci.lineHeight}`)
187 | 					// console.log(`Metrics: cellWidth: ${metrics.cellWidth}, lineHeightf: ${metrics.lineHeightf}`)
188 | 					// Finally Boot!
189 | 					boot()
190 | 				}
191 | 			})()
192 | 			// Ideal mode:
193 | 			// metrics = calcMetrics(settings.element)
194 | 			// etc.
195 | 			// requestAnimationFrame(loop)
196 | 		})
197 | 
198 | 		// FPS object (keeps some state for precise FPS measure)
199 | 		const fps = new FPS()
200 | 
201 | 		// A cell with no value at all is just a space
202 | 		const EMPTY_CELL = ' '
203 | 
204 | 		// Default cell style inserted in case of undefined / null
205 | 		const DEFAULT_CELL_STYLE = Object.freeze({
206 | 			color           : settings.color,
207 | 			backgroundColor : settings.backgroundColor,
208 | 			fontWeight      : settings.fontWeight
209 | 		})
210 | 
211 | 		// Buffer needed for the final DOM rendering,
212 | 		// each array entry represents a cell.
213 | 		const buffer = []
214 | 
215 | 		// Metrics object, calc once (below)
216 | 		let metrics
217 | 
218 | 		function boot() {
219 | 			metrics = calcMetrics(settings.element)
220 | 			const context = getContext(state, settings, metrics, fps)
221 | 			if (typeof program.boot == 'function') {
222 | 				program.boot(context, buffer, userData)
223 | 			}
224 | 			requestAnimationFrame(loop)
225 | 		}
226 | 
227 | 		// Time sample to calculate precise offset
228 | 		let timeSample = 0
229 | 		// Previous time step to increment state.time (with state.time initial offset)
230 | 		let ptime = 0
231 | 		const interval = 1000 / settings.fps
232 | 		const timeOffset = state.time
233 | 
234 | 		// Used to track window resize
235 | 		let cols, rows
236 | 
237 | 		// Main program loop
238 | 		function loop(t) {
239 | 
240 | 			// Timing
241 | 			const delta = t - timeSample
242 | 			if (delta < interval) {
243 | 				// Skip the frame
244 | 				if (!settings.once) requestAnimationFrame(loop)
245 | 				return
246 | 			}
247 | 
248 | 			// Snapshot of context data
249 | 			const context = getContext(state, settings, metrics, fps)
250 | 
251 | 			// FPS update
252 | 			fps.update(t)
253 | 
254 | 			// Timing update
255 | 			timeSample = t - delta % interval // adjust timeSample
256 | 			state.time = t + timeOffset       // increment time + initial offs
257 | 			state.frame++                     // increment frame counter
258 | 			storage.store(LOCAL_STORAGE_KEY_STATE, state) // store state
259 | 
260 | 			// Cursor update
261 | 			const cursor = {
262 | 				          // The canvas might be slightly larger than the number
263 | 				          // of cols/rows, min is required!
264 | 				x       : Math.min(context.cols-1, pointer.x / metrics.cellWidth),
265 | 				y       : Math.min(context.rows-1, pointer.y / metrics.lineHeight),
266 | 				pressed : pointer.pressed,
267 | 				p : { // state of previous frame
268 | 					x       : pointer.px / metrics.cellWidth,
269 | 					y       : pointer.py / metrics.lineHeight,
270 | 					pressed : pointer.ppressed,
271 | 				}
272 | 			}
273 | 
274 | 			// Pointer: store previous state
275 | 			pointer.px = pointer.x
276 | 			pointer.py = pointer.y
277 | 			pointer.ppressed = pointer.pressed
278 | 
279 | 			// 1. --------------------------------------------------------------
280 | 			// In case of resize / init normalize the buffer
281 | 			if (cols != context.cols || rows != context.rows) {
282 | 				cols = context.cols
283 | 				rows = context.rows
284 | 				buffer.length = context.cols * context.rows
285 | 				for (let i=0; i<buffer.length; i++) {
286 | 					buffer[i] = {...DEFAULT_CELL_STYLE, char : EMPTY_CELL}
287 | 				}
288 | 			}
289 | 
290 | 			// 2. --------------------------------------------------------------
291 | 			// Call pre(), if defined
292 | 			if (typeof program.pre == 'function') {
293 | 				program.pre(context, cursor, buffer, userData)
294 | 			}
295 | 
296 | 			// 3. --------------------------------------------------------------
297 | 			// Call main(), if defined
298 | 			if (typeof program.main == 'function') {
299 | 				for (let j=0; j<context.rows; j++) {
300 | 					const offs = j * context.cols
301 | 					for (let i=0; i<context.cols; i++) {
302 | 						const idx = i + offs
303 | 						// Override content:
304 | 						// buffer[idx] = program.main({x:i, y:j, index:idx}, context, cursor, buffer, userData)
305 | 						const out = program.main({x:i, y:j, index:idx}, context, cursor, buffer, userData)
306 | 						if (typeof out == 'object' && out !== null) {
307 | 							buffer[idx] = {...buffer[idx], ...out}
308 | 						} else {
309 | 							buffer[idx] = {...buffer[idx], char : out}
310 | 						}
311 | 						// Fix undefined / null / etc.
312 | 						if (!Boolean(buffer[idx].char) && buffer[idx].char !== 0) {
313 | 							buffer[idx].char = EMPTY_CELL
314 | 						}
315 | 					}
316 | 				}
317 | 			}
318 | 
319 | 			// 4. --------------------------------------------------------------
320 | 			// Call post(), if defined
321 | 			if (typeof program.post == 'function') {
322 | 				program.post(context, cursor, buffer, userData)
323 | 			}
324 | 
325 | 			// 5. --------------------------------------------------------------
326 | 			renderer.render(context, buffer, settings)
327 | 
328 | 			// 6. --------------------------------------------------------------
329 | 			// Queued events
330 | 			while (eventQueue.length > 0) {
331 | 				const type = eventQueue.shift()
332 | 				if (type && typeof program[type] == 'function') {
333 | 					program[type](context, cursor, buffer)
334 | 				}
335 | 			}
336 | 
337 | 			// 7. --------------------------------------------------------------
338 | 			// Loop (eventually)
339 | 			if (!settings.once) requestAnimationFrame(loop)
340 | 
341 | 			// The end of the first frame is reached without errors
342 | 			// the promise can be resolved.
343 | 			resolve(context)
344 | 		}
345 | 	})
346 | }
347 | 
348 | // -- Helpers ------------------------------------------------------------------
349 | 
350 | // Build / update the 'context' object (immutable)
351 | // A bit of spaghetti... but the context object needs to be ready for
352 | // the boot function and also to be updated at each frame.
353 | function getContext(state, settings, metrics, fps) {
354 | 	const rect = settings.element.getBoundingClientRect()
355 | 	const cols = settings.cols || Math.floor(rect.width / metrics.cellWidth)
356 | 	const rows = settings.rows || Math.floor(rect.height / metrics.lineHeight)
357 | 	return Object.freeze({
358 | 		frame : state.frame,
359 | 		time : state.time,
360 | 		cols,
361 | 		rows,
362 | 		metrics,
363 | 		width : rect.width,
364 | 		height : rect.height,
365 | 		settings,
366 | 		// Runtime & debug data
367 | 		runtime : Object.freeze({
368 | 			cycle : state.cycle,
369 | 			fps : fps.fps
370 | 			// updatedRowNum
371 | 		})
372 | 	})
373 | }
374 | 
375 | // Disables selection for an HTML element
376 | function disableSelect(el) {
377 | 	el.style.userSelect = 'none'
378 | 	el.style.webkitUserSelect = 'none' // for Safari on mac and iOS
379 | 	el.style.mozUserSelect = 'none'    // for mobile FF
380 | 	el.dataset.selectionEnabled = 'false'
381 | }
382 | 
383 | // Enables selection for an HTML element
384 | function enableSelect(el) {
385 | 	el.style.userSelect = 'auto'
386 | 	el.style.webkitUserSelect = 'auto'
387 | 	el.style.mozUserSelect = 'auto'
388 | 	el.dataset.selectionEnabled = 'true'
389 | }
390 | 
391 | // Copies the content of an element to the clipboard
392 | export function copyContent(el) {
393 | 	// Store selection default
394 | 	const selectionEnabled = !el.dataset.selectionEnabled == 'false'
395 | 
396 | 	// Enable selection if necessary
397 | 	if (!selectionEnabled) enableSelect(el)
398 | 
399 | 	// Copy the text block
400 | 	const range = document.createRange()
401 | 	range.selectNode(el)
402 | 	const sel = window.getSelection()
403 | 	sel.removeAllRanges()
404 | 	sel.addRange(range)
405 | 	document.execCommand('copy')
406 | 	sel.removeAllRanges()
407 | 
408 | 	// Restore default, if necessary
409 | 	if (!selectionEnabled) disableSelect(el)
410 | }
411 | 
412 | // Calcs width (fract), height, aspect of a monospaced char
413 | // assuming that the CSS font-family is a monospaced font.
414 | // Returns a mutable object.
415 | export function calcMetrics(el) {
416 | 
417 | 	const style = getComputedStyle(el)
418 | 
419 | 	// Extract info from the style: in case of a canvas element
420 | 	// the style and font family should be set anyways.
421 | 	const fontFamily = style.getPropertyValue('font-family')
422 | 	const fontSize   = parseFloat(style.getPropertyValue('font-size'))
423 | 	// Can’t rely on computed lineHeight since Safari 14.1
424 | 	// See:  https://bugs.webkit.org/show_bug.cgi?id=225695
425 | 	const lineHeight = parseFloat(style.getPropertyValue('line-height'))
426 | 	let cellWidth
427 | 
428 | 	// If the output element is a canvas 'measureText()' is used
429 | 	// else cellWidth is computed 'by hand' (should be the same, in any case)
430 | 	if (el.nodeName == 'CANVAS') {
431 | 		const ctx = el.getContext('2d')
432 | 		ctx.font = fontSize + 'px ' + fontFamily
433 | 		cellWidth = ctx.measureText(''.padEnd(50, 'X')).width / 50
434 | 	} else {
435 | 		const span = document.createElement('span')
436 | 		el.appendChild(span)
437 | 		span.innerHTML = ''.padEnd(50, 'X')
438 | 		cellWidth = span.getBoundingClientRect().width / 50
439 | 		el.removeChild(span)
440 | 	}
441 | 
442 | 	const metrics = {
443 | 		aspect : cellWidth / lineHeight,
444 | 		cellWidth,
445 | 		lineHeight,
446 | 		fontFamily,
447 | 		fontSize,
448 | 		// Semi-hackish way to allow an update of the metrics object.
449 | 		// This may be useful in some situations, for example
450 | 		// responsive layouts with baseline or font change.
451 | 		// NOTE: It’s not an immutable object anymore
452 | 		_update : function() {
453 | 			const tmp = calcMetrics(el)
454 | 			for(var k in tmp) {
455 | 				// NOTE: Object.assign won’t work
456 | 				if (typeof tmp[k] == 'number' || typeof tmp[k] == 'string') {
457 | 					m[k] = tmp[k]
458 | 				}
459 | 			}
460 | 		}
461 | 	}
462 | 
463 | 	return metrics
464 | }
465 | 
466 | 
467 | 
```
Page 2/2FirstPrevNextLast