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 } ```