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

# Directory Structure

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

# Files

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

```javascript
/**
@module   color.js
@desc     Some common palettes and simple color helpers
@category public

Colors can be defined as:

rgb : { r:255, g:0, b:0 }
int : 16711680 (0xff0000)
hex : '#FF0000'
css : 'rgb(255,0,0)'

CSS1 and CSS3 palettes are exported as maps
C64 and CGA palettes are exported as arrays

Most of the times colors are ready to use as in CSS:
this means r,g,b have 0-255 range but alpha 0-1

Colors in exported palettes are augmented to:
{
	name : 'red',
	r    : 255,        // 0-255 (as in CSS)
	g    : 0,          // 0-255 (as in CSS)
	b    : 0,          // 0-255 (as in CSS)
	a    : 1.0,        // 0-1   (as in CSS)
	v    : 0.6,        // 0-1   (gray value)
	hex  : '#FF0000',
	css  : 'rgb(255,0,0)'
	int  : 16711680
}

*/

// Convert r,g,b,a values to {r,g,b,a}
export function rgb(r,g,b,a=1.0) {
	return {r,g,b,a}
}

// Convert r,g,b,a values to {r,g,b,a}
export function hex(r,g,b,a=1.0) {
	return rgb2hex({r,g,b,a})
}

// Convert r,g,b,a values to 'rgb(r,g,b,a)'
export function css(r,g,b,a=1.0) {
	if (a === 1.0) return `rgb(${r},${g},${b})`
	// CSS3 (in CSS4 we could return rgb(r g b a))
	return `rgba(${r},${g},${b},${a})`}

// Convert {r,g,b,a} values to 'rgb(r,g,b,a)'
export function rgb2css(rgb) {
	if (rgb.a === undefined || rgb.a === 1.0) {
		return `rgb(${rgb.r},${rgb.g},${rgb.b})`
	}
	// CSS3 (in CSS4 we could return rgb(r g b a))
	return `rgba(${rgb.r},${rgb.g},${rgb.b},${rgb.a})`
}

// Convert {r,g,b} values to '#RRGGBB' or '#RRGGBBAA'
export function rgb2hex(rgb) {

	let r = Math.round(rgb.r).toString(16).padStart(2, '0')
	let g = Math.round(rgb.g).toString(16).padStart(2, '0')
	let b = Math.round(rgb.b).toString(16).padStart(2, '0')

  	// Alpha not set
	if (rgb.a === undefined) {
		return '#' + r + g + b
	}

	let a = Math.round(rgb.a * 255).toString(16).padStart(2, '0')
	return '#' + r + g + b + a
}

// Convert {r,g,b} values to gray value [0-1]
export function rgb2gray(rgb) {
	return Math.round(rgb.r * 0.2126 + rgb.g * 0.7152 + rgb.b * 0.0722) / 255.0
}

// hex is not a string but an int number
export function int2rgb(int) {
	return {
		a : 1.0,
		r : int >> 16 & 0xff,
		g : int >>  8 & 0xff,
		b : int       & 0xff
	}
}

// export function int2hex(int) {
// 	return '#' + (int).toString(16)
// }

// https://www.c64-wiki.com/wiki/Color
const _C64 = [
	{ int : 0x000000, name : 'black' },       //  0
	{ int : 0xffffff, name : 'white' },       //  1
	{ int : 0x880000, name : 'red' },         //  2
	{ int : 0xaaffee, name : 'cyan' },        //  3
	{ int : 0xcc44cc, name : 'violet' },      //  4
	{ int : 0x00cc55, name : 'green' },       //  5
	{ int : 0x0000aa, name : 'blue' },        //  6
	{ int : 0xeeee77, name : 'yellow' },      //  7
	{ int : 0xdd8855, name : 'orange' },      //  8
	{ int : 0x664400, name : 'brown' },       //  9
	{ int : 0xff7777, name : 'lightred' },    // 10
	{ int : 0x333333, name : 'darkgrey' },    // 11
	{ int : 0x777777, name : 'grey' },        // 12
	{ int : 0xaaff66, name : 'lightgreen' },  // 13
	{ int : 0x0088ff, name : 'lightblue' },   // 14
	{ int : 0xbbbbbb, name : 'lightgrey' }    // 15
]

const _CGA = [
	{ int : 0x000000, name : 'black' },        //  0
	{ int : 0x0000aa, name : 'blue' },         //  1
	{ int : 0x00aa00, name : 'green' },        //  2
	{ int : 0x00aaaa, name : 'cyan' },         //  3
	{ int : 0xaa0000, name : 'red' },          //  4
	{ int : 0xaa00aa, name : 'magenta' },      //  5
	{ int : 0xaa5500, name : 'brown' },        //  6
	{ int : 0xaaaaaa, name : 'lightgray' },    //  7
	{ int : 0x555555, name : 'darkgray' },     //  8
	{ int : 0x5555ff, name : 'lightblue' },    //  9
	{ int : 0x55ff55, name : 'lightgreen' },   // 10
	{ int : 0x55ffff, name : 'lightcyan' },    // 11
	{ int : 0xff5555, name : 'lightred' },     // 12
	{ int : 0xff55ff, name : 'lightmagenta' }, // 13
	{ int : 0xffff55, name : 'yellow' },       // 14
	{ int : 0xffffff, name : 'white' }         // 15
]

// https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
const _CSS1 = [
	{ int : 0x000000, name : 'black' },
	{ int : 0xc0c0c0, name : 'silver' },
	{ int : 0x808080, name : 'gray' },
	{ int : 0xffffff, name : 'white' },
	{ int : 0x800000, name : 'maroon' },
	{ int : 0xff0000, name : 'red' },
	{ int : 0x800080, name : 'purple' },
	{ int : 0xff00ff, name : 'fuchsia' },
	{ int : 0x008000, name : 'green' },
	{ int : 0x00ff00, name : 'lime' },
	{ int : 0x808000, name : 'olive' },
	{ int : 0xffff00, name : 'yellow' },
	{ int : 0x000080, name : 'navy' },
	{ int : 0x0000ff, name : 'blue' },
	{ int : 0x008080, name : 'teal' },
	{ int : 0x00ffff, name : 'aqua' }
]

const _CSS2 = [..._CSS1,
	{int : 0xffa500, name :'orange'}
]

const _CSS3 = [..._CSS2,
	{ int : 0xf0f8ff, name : 'aliceblue' },
	{ int : 0xfaebd7, name : 'antiquewhite' },
	{ int : 0x7fffd4, name : 'aquamarine' },
	{ int : 0xf0ffff, name : 'azure' },
	{ int : 0xf5f5dc, name : 'beige' },
	{ int : 0xffe4c4, name : 'bisque' },
	{ int : 0xffebcd, name : 'blanchedalmond' },
	{ int : 0x8a2be2, name : 'blueviolet' },
	{ int : 0xa52a2a, name : 'brown' },
	{ int : 0xdeb887, name : 'burlywood' },
	{ int : 0x5f9ea0, name : 'cadetblue' },
	{ int : 0x7fff00, name : 'chartreuse' },
	{ int : 0xd2691e, name : 'chocolate' },
	{ int : 0xff7f50, name : 'coral' },
	{ int : 0x6495ed, name : 'cornflowerblue' },
	{ int : 0xfff8dc, name : 'cornsilk' },
	{ int : 0xdc143c, name : 'crimson' },
	{ int : 0x00ffff, name : 'aqua' },
	{ int : 0x00008b, name : 'darkblue' },
	{ int : 0x008b8b, name : 'darkcyan' },
	{ int : 0xb8860b, name : 'darkgoldenrod' },
	{ int : 0xa9a9a9, name : 'darkgray' },
	{ int : 0x006400, name : 'darkgreen' },
	{ int : 0xa9a9a9, name : 'darkgrey' },
	{ int : 0xbdb76b, name : 'darkkhaki' },
	{ int : 0x8b008b, name : 'darkmagenta' },
	{ int : 0x556b2f, name : 'darkolivegreen' },
	{ int : 0xff8c00, name : 'darkorange' },
	{ int : 0x9932cc, name : 'darkorchid' },
	{ int : 0x8b0000, name : 'darkred' },
	{ int : 0xe9967a, name : 'darksalmon' },
	{ int : 0x8fbc8f, name : 'darkseagreen' },
	{ int : 0x483d8b, name : 'darkslateblue' },
	{ int : 0x2f4f4f, name : 'darkslategray' },
	{ int : 0x2f4f4f, name : 'darkslategrey' },
	{ int : 0x00ced1, name : 'darkturquoise' },
	{ int : 0x9400d3, name : 'darkviolet' },
	{ int : 0xff1493, name : 'deeppink' },
	{ int : 0x00bfff, name : 'deepskyblue' },
	{ int : 0x696969, name : 'dimgray' },
	{ int : 0x696969, name : 'dimgrey' },
	{ int : 0x1e90ff, name : 'dodgerblue' },
	{ int : 0xb22222, name : 'firebrick' },
	{ int : 0xfffaf0, name : 'floralwhite' },
	{ int : 0x228b22, name : 'forestgreen' },
	{ int : 0xdcdcdc, name : 'gainsboro' },
	{ int : 0xf8f8ff, name : 'ghostwhite' },
	{ int : 0xffd700, name : 'gold' },
	{ int : 0xdaa520, name : 'goldenrod' },
	{ int : 0xadff2f, name : 'greenyellow' },
	{ int : 0x808080, name : 'grey' },
	{ int : 0xf0fff0, name : 'honeydew' },
	{ int : 0xff69b4, name : 'hotpink' },
	{ int : 0xcd5c5c, name : 'indianred' },
	{ int : 0x4b0082, name : 'indigo' },
	{ int : 0xfffff0, name : 'ivory' },
	{ int : 0xf0e68c, name : 'khaki' },
	{ int : 0xe6e6fa, name : 'lavender' },
	{ int : 0xfff0f5, name : 'lavenderblush' },
	{ int : 0x7cfc00, name : 'lawngreen' },
	{ int : 0xfffacd, name : 'lemonchiffon' },
	{ int : 0xadd8e6, name : 'lightblue' },
	{ int : 0xf08080, name : 'lightcoral' },
	{ int : 0xe0ffff, name : 'lightcyan' },
	{ int : 0xfafad2, name : 'lightgoldenrodyellow' },
	{ int : 0xd3d3d3, name : 'lightgray' },
	{ int : 0x90ee90, name : 'lightgreen' },
	{ int : 0xd3d3d3, name : 'lightgrey' },
	{ int : 0xffb6c1, name : 'lightpink' },
	{ int : 0xffa07a, name : 'lightsalmon' },
	{ int : 0x20b2aa, name : 'lightseagreen' },
	{ int : 0x87cefa, name : 'lightskyblue' },
	{ int : 0x778899, name : 'lightslategray' },
	{ int : 0x778899, name : 'lightslategrey' },
	{ int : 0xb0c4de, name : 'lightsteelblue' },
	{ int : 0xffffe0, name : 'lightyellow' },
	{ int : 0x32cd32, name : 'limegreen' },
	{ int : 0xfaf0e6, name : 'linen' },
	{ int : 0xff00ff, name : 'fuchsia' },
	{ int : 0x66cdaa, name : 'mediumaquamarine' },
	{ int : 0x0000cd, name : 'mediumblue' },
	{ int : 0xba55d3, name : 'mediumorchid' },
	{ int : 0x9370db, name : 'mediumpurple' },
	{ int : 0x3cb371, name : 'mediumseagreen' },
	{ int : 0x7b68ee, name : 'mediumslateblue' },
	{ int : 0x00fa9a, name : 'mediumspringgreen' },
	{ int : 0x48d1cc, name : 'mediumturquoise' },
	{ int : 0xc71585, name : 'mediumvioletred' },
	{ int : 0x191970, name : 'midnightblue' },
	{ int : 0xf5fffa, name : 'mintcream' },
	{ int : 0xffe4e1, name : 'mistyrose' },
	{ int : 0xffe4b5, name : 'moccasin' },
	{ int : 0xffdead, name : 'navajowhite' },
	{ int : 0xfdf5e6, name : 'oldlace' },
	{ int : 0x6b8e23, name : 'olivedrab' },
	{ int : 0xff4500, name : 'orangered' },
	{ int : 0xda70d6, name : 'orchid' },
	{ int : 0xeee8aa, name : 'palegoldenrod' },
	{ int : 0x98fb98, name : 'palegreen' },
	{ int : 0xafeeee, name : 'paleturquoise' },
	{ int : 0xdb7093, name : 'palevioletred' },
	{ int : 0xffefd5, name : 'papayawhip' },
	{ int : 0xffdab9, name : 'peachpuff' },
	{ int : 0xcd853f, name : 'peru' },
	{ int : 0xffc0cb, name : 'pink' },
	{ int : 0xdda0dd, name : 'plum' },
	{ int : 0xb0e0e6, name : 'powderblue' },
	{ int : 0xbc8f8f, name : 'rosybrown' },
	{ int : 0x4169e1, name : 'royalblue' },
	{ int : 0x8b4513, name : 'saddlebrown' },
	{ int : 0xfa8072, name : 'salmon' },
	{ int : 0xf4a460, name : 'sandybrown' },
	{ int : 0x2e8b57, name : 'seagreen' },
	{ int : 0xfff5ee, name : 'seashell' },
	{ int : 0xa0522d, name : 'sienna' },
	{ int : 0x87ceeb, name : 'skyblue' },
	{ int : 0x6a5acd, name : 'slateblue' },
	{ int : 0x708090, name : 'slategray' },
	{ int : 0x708090, name : 'slategrey' },
	{ int : 0xfffafa, name : 'snow' },
	{ int : 0x00ff7f, name : 'springgreen' },
	{ int : 0x4682b4, name : 'steelblue' },
	{ int : 0xd2b48c, name : 'tan' },
	{ int : 0xd8bfd8, name : 'thistle' },
	{ int : 0xff6347, name : 'tomato' },
	{ int : 0x40e0d0, name : 'turquoise' },
	{ int : 0xee82ee, name : 'violet' },
	{ int : 0xf5deb3, name : 'wheat' },
	{ int : 0xf5f5f5, name : 'whitesmoke' },
	{ int : 0x9acd32, name : 'yellowgreen' }
]

const _CSS4 = [..._CSS3,
	{ int: 0x663399, name : 'rebeccapurple'}
]


// Helper function
function augment(pal) {
	return pal.map(el => {
		const rgb = int2rgb(el.int)
		const hex = rgb2hex(rgb)
		const css = rgb2css(rgb)
		const v   = rgb2gray(rgb)
	 	return {...el, ...rgb, v, hex, css}
	})
}

// Helper function
function toMap(pal) {
	const out = {}
	pal.forEach(el => {
		out[el.name] = el
	})
	return out
}

export const CSS4 = toMap(augment(_CSS4))
export const CSS3 = toMap(augment(_CSS3))
export const CSS2 = toMap(augment(_CSS2))
export const CSS1 = toMap(augment(_CSS1))
export const C64  = augment(_C64)
export const CGA  = augment(_CGA)


```

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

```javascript
/**
Runner
*/

// Both available renderers are imported
import textRenderer from './core/textrenderer.js'
import canvasRenderer from './core/canvasrenderer.js'
import FPS from './core/fps.js'
import storage from './core/storage.js'
import RUNNER_VERSION from './core/version.js'

export { RUNNER_VERSION }

const renderers = {
	'canvas' : canvasRenderer,
	'text'   : textRenderer
}

// Default settings for the program runner.
// They can be overwritten by the parameters of the runner
// or as a settings object exported by the program (in this order).
const defaultSettings = {
	element         : null,    // target element for output
	cols            : 0,       // number of columns, 0 is equivalent to 'auto'
	rows            : 0,       // number of columns, 0 is equivalent to 'auto'
	once            : false,   // if set to true the renderer will run only once
	fps             : 30,      // fps capping
	renderer        : 'text',  // can be 'canvas', anything else falls back to 'text'
	allowSelect     : false,   // allows selection of the rendered element
	restoreState    : false,   // will store the "state" object in local storage
	                           // this is handy for live-coding situations
}

// CSS styles which can be passed to the container element via settings
const CSSStyles = [
	'backgroundColor',
	'color',
	'fontFamily',
	'fontSize',
	'fontWeight',
	'letterSpacing',
	'lineHeight',
	'textAlign',
]

// Program runner.
// Takes a program object (usually an imported module),
// and some optional settings (see above) as arguments.
// Finally, an optional userData object can be passed which will be available
// as last parameter in all the module functions.
// The program object should export at least a main(), pre() or post() function.
export function run(program, runSettings, userData = {}) {

	// Everything is wrapped inside a promise;
	// in case of errors in ‘program’ it will reject without reaching the bottom.
	// If the program reaches the bottom of the first frame the promise is resolved.
	return new Promise(function(resolve) {
		// Merge of user- and default settings
		const settings = {...defaultSettings, ...runSettings, ...program.settings}

		// State is stored in local storage and will loaded on program launch
		// if settings.restoreState == true.
		// The purpose of this is to live edit the code without resetting
		// time and the frame counter.
		const state = {
			time  : 0, // The time in ms
			frame : 0, // The frame number (int)
			cycle : 0  // An cycle count for debugging purposes
		}

		// Name of local storage key
		const LOCAL_STORAGE_KEY_STATE = 'currentState'

		if (settings.restoreState) {
			storage.restore(LOCAL_STORAGE_KEY_STATE, state)
			state.cycle++ // Keep track of the cycle count for debugging purposes
		}

		// If element is not provided create a default element based
		// on the renderer settings.
		// Then choose the renderer:
		// If the parent element is a canvas the canvas renderer is selected,
		// for any other type a text node (PRE or any othe text node)
		// is expected and the text renderer is used.
		// TODO: better / more generic renderer init
		let renderer
		if (!settings.element) {
			renderer = renderers[settings.renderer] || renderers['text']
			settings.element = document.createElement(renderer.preferredElementNodeName)
			document.body.appendChild(settings.element)
		} else {
			if (settings.renderer == 'canvas') {
				if (settings.element.nodeName == 'CANVAS') {
					renderer = renderers[settings.renderer]
				} else {
					console.warn("This renderer expects a canvas target element.")
				}
			} else {
				if (settings.element.nodeName != 'CANVAS') {
					renderer = renderers[settings.renderer]
				} else {
					console.warn("This renderer expects a text target element.")
				}
			}
		}

		// Apply CSS settings to element
		for (const s of CSSStyles) {
			if (settings[s]) settings.element.style[s] = settings[s]
		}

		// Eventqueue
		// Stores events and pops them at the end of the renderloop
		// TODO: needed?
		const eventQueue = []

		// Input pointer updated by DOM events
		const pointer = {
			x        : 0,
			y        : 0,
			pressed  : false,
			px       : 0,
			py       : 0,
			ppressed : false,
		}

		settings.element.addEventListener('pointermove', e => {
			const rect = settings.element.getBoundingClientRect()
			pointer.x = e.clientX - rect.left
			pointer.y = e.clientY - rect.top
			eventQueue.push('pointerMove')
		})

		settings.element.addEventListener('pointerdown', e => {
			pointer.pressed = true
			eventQueue.push('pointerDown')
		})

		settings.element.addEventListener('pointerup', e => {
			pointer.pressed = false
			eventQueue.push('pointerUp')
		})
		
		const touchHandler = e => {
			const rect = settings.element.getBoundingClientRect()
			pointer.x = e.touches[0].clientX - rect.left
			pointer.y = e.touches[0].clientY - rect.top
			eventQueue.push('pointerMove')
		}

		settings.element.addEventListener('touchmove', touchHandler)
		settings.element.addEventListener('touchstart', touchHandler)
		settings.element.addEventListener('touchend', touchHandler)


		// CSS fix
		settings.element.style.fontStrech = 'normal'

		// Text selection may be annoing in case of interactive programs
		if (!settings.allowSelect) disableSelect(settings.element)

		// Method to load a font via the FontFace object.
		// The load promise works 100% of the times.
		// But a definition of the font via CSS is preferable and more flexible.
		/*
		const CSSInfo = getCSSInfo(settings.element)
		var font = new FontFace('Simple Console', 'url(/css/fonts/simple/SimpleConsole-Light.woff)', { style: 'normal', weight: 400 })
		font.load().then(function(f) {
			...
		})
		*/

		// Metrics needs to be calculated before boot
		// Even with the "fonts.ready" the font may STILL not be loaded yet
		// on Safari 13.x and also 14.0.
		// A (shitty) workaround is to wait 3! rAF.
		// Submitted: https://bugs.webkit.org/show_bug.cgi?id=217047
		document.fonts.ready.then((e) => {
			// Run this three times...
			let count = 3
			;(function __run_thrice__() {
				if (--count > 0) {
					requestAnimationFrame(__run_thrice__)
				} else {
					// settings.element.style.lineHeight = Math.ceil(metrics.lineHeightf) + 'px'
					// console.log(`Using font faimily: ${ci.fontFamily} @ ${ci.fontSize}/${ci.lineHeight}`)
					// console.log(`Metrics: cellWidth: ${metrics.cellWidth}, lineHeightf: ${metrics.lineHeightf}`)
					// Finally Boot!
					boot()
				}
			})()
			// Ideal mode:
			// metrics = calcMetrics(settings.element)
			// etc.
			// requestAnimationFrame(loop)
		})

		// FPS object (keeps some state for precise FPS measure)
		const fps = new FPS()

		// A cell with no value at all is just a space
		const EMPTY_CELL = ' '

		// Default cell style inserted in case of undefined / null
		const DEFAULT_CELL_STYLE = Object.freeze({
			color           : settings.color,
			backgroundColor : settings.backgroundColor,
			fontWeight      : settings.fontWeight
		})

		// Buffer needed for the final DOM rendering,
		// each array entry represents a cell.
		const buffer = []

		// Metrics object, calc once (below)
		let metrics

		function boot() {
			metrics = calcMetrics(settings.element)
			const context = getContext(state, settings, metrics, fps)
			if (typeof program.boot == 'function') {
				program.boot(context, buffer, userData)
			}
			requestAnimationFrame(loop)
		}

		// Time sample to calculate precise offset
		let timeSample = 0
		// Previous time step to increment state.time (with state.time initial offset)
		let ptime = 0
		const interval = 1000 / settings.fps
		const timeOffset = state.time

		// Used to track window resize
		let cols, rows

		// Main program loop
		function loop(t) {

			// Timing
			const delta = t - timeSample
			if (delta < interval) {
				// Skip the frame
				if (!settings.once) requestAnimationFrame(loop)
				return
			}

			// Snapshot of context data
			const context = getContext(state, settings, metrics, fps)

			// FPS update
			fps.update(t)

			// Timing update
			timeSample = t - delta % interval // adjust timeSample
			state.time = t + timeOffset       // increment time + initial offs
			state.frame++                     // increment frame counter
			storage.store(LOCAL_STORAGE_KEY_STATE, state) // store state

			// Cursor update
			const cursor = {
				          // The canvas might be slightly larger than the number
				          // of cols/rows, min is required!
				x       : Math.min(context.cols-1, pointer.x / metrics.cellWidth),
				y       : Math.min(context.rows-1, pointer.y / metrics.lineHeight),
				pressed : pointer.pressed,
				p : { // state of previous frame
					x       : pointer.px / metrics.cellWidth,
					y       : pointer.py / metrics.lineHeight,
					pressed : pointer.ppressed,
				}
			}

			// Pointer: store previous state
			pointer.px = pointer.x
			pointer.py = pointer.y
			pointer.ppressed = pointer.pressed

			// 1. --------------------------------------------------------------
			// In case of resize / init normalize the buffer
			if (cols != context.cols || rows != context.rows) {
				cols = context.cols
				rows = context.rows
				buffer.length = context.cols * context.rows
				for (let i=0; i<buffer.length; i++) {
					buffer[i] = {...DEFAULT_CELL_STYLE, char : EMPTY_CELL}
				}
			}

			// 2. --------------------------------------------------------------
			// Call pre(), if defined
			if (typeof program.pre == 'function') {
				program.pre(context, cursor, buffer, userData)
			}

			// 3. --------------------------------------------------------------
			// Call main(), if defined
			if (typeof program.main == 'function') {
				for (let j=0; j<context.rows; j++) {
					const offs = j * context.cols
					for (let i=0; i<context.cols; i++) {
						const idx = i + offs
						// Override content:
						// buffer[idx] = program.main({x:i, y:j, index:idx}, context, cursor, buffer, userData)
						const out = program.main({x:i, y:j, index:idx}, context, cursor, buffer, userData)
						if (typeof out == 'object' && out !== null) {
							buffer[idx] = {...buffer[idx], ...out}
						} else {
							buffer[idx] = {...buffer[idx], char : out}
						}
						// Fix undefined / null / etc.
						if (!Boolean(buffer[idx].char) && buffer[idx].char !== 0) {
							buffer[idx].char = EMPTY_CELL
						}
					}
				}
			}

			// 4. --------------------------------------------------------------
			// Call post(), if defined
			if (typeof program.post == 'function') {
				program.post(context, cursor, buffer, userData)
			}

			// 5. --------------------------------------------------------------
			renderer.render(context, buffer, settings)

			// 6. --------------------------------------------------------------
			// Queued events
			while (eventQueue.length > 0) {
				const type = eventQueue.shift()
				if (type && typeof program[type] == 'function') {
					program[type](context, cursor, buffer)
				}
			}

			// 7. --------------------------------------------------------------
			// Loop (eventually)
			if (!settings.once) requestAnimationFrame(loop)

			// The end of the first frame is reached without errors
			// the promise can be resolved.
			resolve(context)
		}
	})
}

// -- Helpers ------------------------------------------------------------------

// Build / update the 'context' object (immutable)
// A bit of spaghetti... but the context object needs to be ready for
// the boot function and also to be updated at each frame.
function getContext(state, settings, metrics, fps) {
	const rect = settings.element.getBoundingClientRect()
	const cols = settings.cols || Math.floor(rect.width / metrics.cellWidth)
	const rows = settings.rows || Math.floor(rect.height / metrics.lineHeight)
	return Object.freeze({
		frame : state.frame,
		time : state.time,
		cols,
		rows,
		metrics,
		width : rect.width,
		height : rect.height,
		settings,
		// Runtime & debug data
		runtime : Object.freeze({
			cycle : state.cycle,
			fps : fps.fps
			// updatedRowNum
		})
	})
}

// Disables selection for an HTML element
function disableSelect(el) {
	el.style.userSelect = 'none'
	el.style.webkitUserSelect = 'none' // for Safari on mac and iOS
	el.style.mozUserSelect = 'none'    // for mobile FF
	el.dataset.selectionEnabled = 'false'
}

// Enables selection for an HTML element
function enableSelect(el) {
	el.style.userSelect = 'auto'
	el.style.webkitUserSelect = 'auto'
	el.style.mozUserSelect = 'auto'
	el.dataset.selectionEnabled = 'true'
}

// Copies the content of an element to the clipboard
export function copyContent(el) {
	// Store selection default
	const selectionEnabled = !el.dataset.selectionEnabled == 'false'

	// Enable selection if necessary
	if (!selectionEnabled) enableSelect(el)

	// Copy the text block
	const range = document.createRange()
	range.selectNode(el)
	const sel = window.getSelection()
	sel.removeAllRanges()
	sel.addRange(range)
	document.execCommand('copy')
	sel.removeAllRanges()

	// Restore default, if necessary
	if (!selectionEnabled) disableSelect(el)
}

// Calcs width (fract), height, aspect of a monospaced char
// assuming that the CSS font-family is a monospaced font.
// Returns a mutable object.
export function calcMetrics(el) {

	const style = getComputedStyle(el)

	// Extract info from the style: in case of a canvas element
	// the style and font family should be set anyways.
	const fontFamily = style.getPropertyValue('font-family')
	const fontSize   = parseFloat(style.getPropertyValue('font-size'))
	// Can’t rely on computed lineHeight since Safari 14.1
	// See:  https://bugs.webkit.org/show_bug.cgi?id=225695
	const lineHeight = parseFloat(style.getPropertyValue('line-height'))
	let cellWidth

	// If the output element is a canvas 'measureText()' is used
	// else cellWidth is computed 'by hand' (should be the same, in any case)
	if (el.nodeName == 'CANVAS') {
		const ctx = el.getContext('2d')
		ctx.font = fontSize + 'px ' + fontFamily
		cellWidth = ctx.measureText(''.padEnd(50, 'X')).width / 50
	} else {
		const span = document.createElement('span')
		el.appendChild(span)
		span.innerHTML = ''.padEnd(50, 'X')
		cellWidth = span.getBoundingClientRect().width / 50
		el.removeChild(span)
	}

	const metrics = {
		aspect : cellWidth / lineHeight,
		cellWidth,
		lineHeight,
		fontFamily,
		fontSize,
		// Semi-hackish way to allow an update of the metrics object.
		// This may be useful in some situations, for example
		// responsive layouts with baseline or font change.
		// NOTE: It’s not an immutable object anymore
		_update : function() {
			const tmp = calcMetrics(el)
			for(var k in tmp) {
				// NOTE: Object.assign won’t work
				if (typeof tmp[k] == 'number' || typeof tmp[k] == 'string') {
					m[k] = tmp[k]
				}
			}
		}
	}

	return metrics
}



```
Page 2/2FirstPrevNextLast