Play: About

This playground is an attempt of a browser-based live-code environment with text-only output.
It is born from the joy and pleasure ASCII, ANSI and in general text-based art can give and it is an homage to all the artists, poets and designers which used and use text as their medium.
From the design perspective it is also an exercise in reduction: there is almost no interface, just a preview window and a code editor; margins and line numbers are removed as well. All the possible interactions are handled by a few hotkeys and some special “bang” commands; feedback is given trough the default browser modal dialogs “alert” and “confirm”.
The API follows a similar philosophy: the problem is not complex (the whole program is basically a loop) and the exposed features are just the essential ones. Eventually some extra functionality can be imported trough ES6 modules (details about modules and the hack to make them work can be found in the technical notes).
Finally, programs can be saved in the browser’s local storage (lsd.html) or uploaded to the server and shared trough a permalink. In favour of a frictionless experience no credentials are asked and no account is needed to store a program on the server.
The project will stay up until it breaks.

Play: Background

The API is inspired by GLSL programming/rasterisation: instead of thinking in geometric primitives painted on a canvas this approach enforces to write a single function which will be invoked against each cell, an approach not so different from fragment shaders.
If the number of “fragment” operations is low common shader techniques can be used even in (comparably slow) JavaScript: a maximised browser window on a typical laptop screen holds about 5-8000 monospaced characters. Even a low performing computer can do enough computation on this amount of characters, at 30 frames per second.
For conceptual reasons the output is rendered as “text” inside a DOM element and not drawn as pixels on a graphics canvas (see the alternate “Canvas” renderer): this has some performance drawbacks but if managed with some care a smooth frame-rate can still be obtained, even in a fullscreen window.
See also this note about performance.

API: Modules

Programs are ES6 modules. All code is sealed inside the module and there are no globals.
External modules can be imported (see examples below).
A program can export four optional functions:

These functions have slightly different signatures with main taking one more argument; it is also the only function to return a value. All parameters are optional; see the next chapter for a description.

Example of the smallest program possible (with an output):
export function main(){
	return '?'
}
		
Example of use of cell coordinates:
export function main(coord){
	return String.fromCharCode((coord.y + coord.x) % 32 + 65)
}
		
Example of timing:
export function main(coord, context){
	const f = context.frame
	return String.fromCharCode((coord.y + coord.x + f) % 32 + 65)
}
		
Example of use of a density map:
const density = 'ABCxyz '
export function main(coord, context){
	const t = context.time * 0.0001
	const x = coord.x
	const y = coord.y
	const o = Math.sin(y * Math.sin(t) * 0.2 + x * 0.04 + t) * 20
	const i = Math.round(Math.abs(x + y + o)) % density.length
	return density[i]
}
		

API: Function signatures and parameters

function boot(       context,         buffer, data) // Called only once
function pre (       context, cursor, buffer, data) // Called every frame
function main(coord, context, cursor, buffer, data) // Called for every cell
function post(       context, cursor, buffer, data) // Called every frame
		
The coord object contains the coordinates of each cell as x and y or as index.
const coord = {
	x,      // column of the cell (starting top left)
	y,      // row of the cell
	index   // index of the cell
}
		
The context object contains timing and size informations.
NOTE: The font is defined via CSS; the runner has no knowledge about the font.
A monospaced font is assumed but not mandatory.
An important value (in case of fixed with fonts) is the aspect ratio of a single character: it allows to correct the geometry of the rendered images and is stored in the aspect field of the metrics object.
The function to calculate the metrics is exposed in the run.js module and is used internally to determine the correct aspect ratio of a single character.
const context = {
	frame,          // current frame
	time,           // current time in ms
	cols,           // number of columns of the rendering context
	rows,           // number of rows of the rendering context
	width,          // width of the container element (pixels)
	height,         // height of the container element (pixels)
	metrics : {
		aspect      // aspect ratio of a single char (monospaced font assumed)
		cellWidth,  // width of a char
		lineHeight, // computed, but should be the same as CSS
		fontFamily, // CSS string
		fontSize,   // Derived from CSS (pixels)
		_update()   // Function to recalc the metrics (slow!)
	},
	settings,       // The settings object
	runtime : {
		cycle,      // local cycle count of the script (for debugging purposes)
		fps         // average frame rate (measured)
	},
}
		
The cursor object contains basic information about an eventual input (pointer).
The x and y fields are fractional, for greater precision.
const cursor = {
	x,          // column of the cell hovered
	y,          // row of the cell hovered
	pressed,    // mouse or pointer is pressed (boolean)
	p : {       // cursor state of the previous frame
		x,
		y,
		pressed
	}
		
The buffer parameter is an array and represents the display state.
The buffer can be manipulated by hand in pre() and (especially) in post().
NOTE: The buffer is not cleared automatically.

The data parameter is a reference to an optional (user-)data object passed as argument in the runner and shared with all the functions. It can also be used to pass data around the functions.

API: The return object in main()

The main() function can return a single char. In this case the char will be rendered black on a white background, with ‘regular’ weight (400).
Optionally the function can also return an object with the following fields:
const out = {
	char,            // the char to be rendered
	color,           // the foreground color (CSS string)
	backgroundColor, // the background color (CSS string)
	fontWeight       // the font weight (CSS value or string)
				     // the online version supports these three weights:
				     // 300, 400, 700 (other values will 'snap' to those)
}
		

API: Boot

Ideally a program can be considered as a loop without any beginning; there is no start and no end; just eternity. The optional boot() function will be executed once, before the first pre() call and can be useful when metrics info is needed before the program start and/or to access user data passed as argument in the runner.
NOTE: As the ouput element size may change (for example when the browser window gets resized) the boot function is less suited for data initalization.
An example of data initalization and reset without the use of the boot() function:
const extraCellData
let cols, rows
export function pre(context, cursor, buffer) {
	if (cols != context.cols || rows != context.rows) {
		// The window has been resized!
		rows = context.rows
		cols = context.cols
		// ...do something with the buffer (resize, reset, etc.):
		extraCellData = new Array(rows * cols).fill(0)
	}
}
		

API: Settings

An optional settings object can be exported and allows to overwrite some of the default run settings (the default values are listed below).
export const settings = {
	// Base settings and the default values
	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 settings for the output element (will override document settings)
	backgroundColor : '',     // document CSS: 'white'
	color           : '',     // document CSS: 'black'
	fontFamily      : '',     // document CSS: 'Simple Console'
	fontSize        : '',     // document CSS: '1em'
	fontWeight      : '',     // document CSS: 400
	letterSpacing   : '',     // document CSS: 'initial'
	lineHeight      : '',     // document CSS: '19px'

	// Settings specific to 'canvas' renderer (may change in future):
	canvasOffset : {          // can also be set to 'auto'
		x : 0,
		y : 0
	},
	canvasSize : {            // canvas size in pixels
		width  : 0,
		height : 0
	}
}
		

Live coding: Hotkeys

The editor (the excellent and lightweight CodeMirror) is configured with a Sublime Text-like keymap.
The most useful key combination while coding is probably Cmd/Ctrl-D.

Playground specific hotkeys:
Cmd/Ctrl-Enter   : run
Cmd/Ctrl-Period  : show/hide editor
Cmd/Ctrl-S       : save to local storage, with permalink
Cmd/Ctrl-Shift-U : share to playground (needs author and title tags)
		
NOTE: It’s possible to copy the text content of the output by clicking on the canvas and pressing Cmd-C (or Ctrl-C).

Live coding: Bangs

Any comment area can accept special “bang” commands preceded with a ? which are executed immediately after typed (almost like a console but without the “enter”):
/**
Type ?help
	 anywhere to open this page for an overview about the playground,
	 more commands like this and some examples.
	 ?abc is an alias for ?help.

Type ?lsd
	 anywhere to open the local storage directory.

Type ?immediate on
	 to enable immediate mode; change back with off.

Type ?video night
	 to switch to dark mode for the editor; change back with day.

Type ?new
	 to open a new document (unsaved changes will be lost).

Type ?save
	 to save a local copy of the program (useful on mobile).

Type ?fullscreen
	 to request a fullscreen window (press ESC to switch back to windowed).
*/
		
The bangs !video and !immediate can alternatively be preceded by a !. These bangs will be evaluated at load and can be used as a sort of per-program preference.
NOTE: These bangs are evaluated only on locally saved programs.
/**
Type !video night
	 anywhere in a comment to start the editor in night mode
	 the next time it is reloaded.

Type !immediate on
	 anywhere in a comment to start the editor in immediate mode
	 the next time it is reloaded.
*/
		

Live coding: Save and upload

Saving a program locally:
When a program is saved locally (Cmd/Ctrl-S) a timestamp which serves as permalink is created. Subsequent saves will overwrite the local version.
Local versions can be accessed even after browser restart.
A directory of all the saved programs (local and remote) can be accessed trough lsd.html.

Uploading and sharing a program:
A program can be uploaded to the server (Cmd/Ctrl-Shift-U). A new timestamp which serves as permalink will be created. Subsequent uploads will generate a new permalink.

A program needs a few requirements to be stored permanently on the server:

Title and author can both be defined in a comment, not necessarily at the top, and an optional @desc can also be set:
/**
@author ertdfgcvb
@title  Dynamic resize
@desc   Resize the window to alter the pattern.
*/

export function main(coord){
	return '--|-------'[coord.index % 10]
}
		

Source: Built-in modules

A few modules with specific functions are provided.
Please see the comments in the source code for details.

public
camera.js
Webcam init and helper
canvas.js
A wrapper for a canvas element
color.js
Some common palettes and simple color helpers
drawbox.js
Draw text boxes with optional custom styles
exportframe.js
Exports a single frame (or a range) to an image
image.js
Image loader and helper
num.js
Some GLSL functions ported to JS
sdf.js
Some signed distance functions
sort.js
Sorts a set of characters by brightness
vec2.js
2D vector helper functions
vec3.js
3D vector helper functions
internal
buffer.js
Safe buffer helpers, mostly for internal use
loader.js
Various file type loader, returns a Promise
string.js
String helpers

Source: Examples

basics
Coordinates: index
Use of coord.index
Coordinates: x, y
Use of coord.x and coord.y
Cursor
Crosshair example with mouse cursor
How to draw a circle
Use of context.metrics.aspect
How to draw a square
Draw a square using a distance function
How to log
Console output inside the main() loop
Name game
What’s your name?
Perfomance test
Vertical vs horizontal changes impact FPS
Canvas renderer
Rendering to a canvas element
Sequence export
Export 10 frames as images
Simple output
The smallest program possible?
Time: frames
Use of context.frame (ASCII horizon)
Time: milliseconds
Use of context.time
sdf
Balls
Smooth SDF balls
Circle
Draw a smooth circle with exp()
Wireframe cube
The cursor controls box thickness and exp
Rectangles
Smooth SDF Rectangles
Two circles
Smooth union of two circles
demos
Chroma Spiral
Shadertoy port
Donut
Ported from a1k0n’s donut demo.
Doom Flame
Oldschool flame effect
Doom Flame (full color)
Oldschool flame effect
Golgol
Double resolution version of GOL
Mod Xor
Patterns obtained trough modulo and xor
Numbers
Fun with integers
Plasma
Oldschool plasma demo
Sin Sin
Checker variation
Sin Sin
Wave variation
Spiral
Shadertoy port
Wobbly
Draw donuts with SDF
camera
Camera double resolution
Doubled vertical resolution input from camera
Camera grayscale
Grayscale input from camera
Camera RGB
Color input from camera (quantised)
contributed
Color Waves
¯\_(ツ)_/¯
EQUAL TEA TALK, #65
Inspired by Frederick Hammersley, 1969
oeö
Inspired by Ernst Jandl, 1964
GOL
Conway's Game of Life
Pathfinder
Click to spawn new path segments
Sand game
Click to drop sand
Stacked sin waves
noob at frag shaders

Source: Standalone version

The modules were initially designed for standalone use and can be easily emedded in any project.
Multiple instances can run on the same page.
The main module consists in an app runner.

How to embed:

<!-- existing element, styled via CSS -->
<pre></pre>

<script type="module">

	// Import the program runner
	import {run} from './run.js'

	// Import a custom program; with at least a main() function exported
	import * as program from './program.js'

	// Run settings can override the default- and program seetings.
	// See the API for details.
	const settings = {
		fps : 60,
		element : document.querySelector('pre')
	}

	// Boot (returns a promise)
	run(program, settings).catch(function(e){
		console.warn(e.message)
		console.log(e.error)
	})

</script>
		
A few examples:

examples
single.html
A single fullscreen app
multi.html
Multiple instances on the same page
tests
proxy_test.html
A benchmark for JS Proxy()
benchmark.html
A naive rendering benchmark
font.html
Visualize the font table and display metrics

Notes: On performance

The buffer is rendered by line, starting from top. Frequent horizontal changes in weight, color or background will slow down the rendering considerably! The reason is that each change in style needs to wrap the single character in an inline <span> element augmenting the complexity of the DOM tree.
See this example for some details and possible optimisations.
NOTE: The online playground has the frame rate capped at 30fps; the fps can be altered in the standalone version or via the settings export.

Notes: Technical

The main program runner is just a few lines of JavaScript and it offers mostly marginal features: handling window resize, font metrics, smaller output optimizations, the update loop, etc.
Modules offer a proper way to extend these functionalies and all code in the playground runs directly as ES6 modules. To gain a little in download time and faster reload the “runner” code has been combined and minimized (still as a module), but all the addon modules are served unaltered.

To allow the dynamic execution from the editor the code has to be encoded via TextEncoder and then loaded as a Blob from an ObjectURL.
The only preprocessing of the code is a fix of import statement paths.

During development three bugs have been submitted to WebKit: they are mostly reconducible to issues with code running inside a module: the same code running in a script tag or .js file (not module) behaves differetly/correctly.

Font: Simple Console

The monospaced font used for the online live code is ‘Simple Console’ by Norm, a custom version of ‘Simple’ which features the complete set of box-drawing characters.
Originally it was chosen for ertdfgcvb because of the special design of the lower case ‘r’ (a letter which is present in the name of the site).

Font: Character set

Some of the glyphs of Simple Console.
Displayed here in medium (400 in CSS). click to change
0 1 2 3 4 5 6 7 8 9
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
a b c d e f g h i j k l m n o p q r s t u v w x y z
. , ; : ¿ ? ¡ ! @ # % ‰ & * ' "
€ £ $ ¥ ¢
¤ ¦ ¶ § © ­® ™ ° º ª ¹ ² ³ ¼ ½ ¾
· + - × ÷ ± = ≠ < > √ ∞ ∫ ≈ ~ ¬ ≤ ≥ [ ] ( ) { } | \ / ⁄
fi fl Æ Œ æ œ ß Ø ø Ə ə ^ ¨ `
Ω ∂ ∆ π ∏ ◊ ∑ μ
‘ ’ ‚ “ ” „ « » ‹ ›
† ‡ • − – _ ¯ …

┐ ┌ ┘ └ ┼ ├ ┤ ┴ ┬ │ ─ ╎ ╌ ╵╷╴╶

┓ ┏ ┛ ┗ ╋ ┣ ┫ ┻ ┳ ┃ ━ ╏ ╍ ╹╻╸╺

╗ ╔ ╝ ╚ ╬ ╠ ╣ ╩ ╦ ║ ═

╕ ╒ ╛ ╘ ╪ ╞ ╡ ╧ ╤

╖ ╓ ╜ ╙ ╫ ╟ ╢ ╨ ╥

┑ ┍ ┙ ┕ ┿ ┝ ┥ ┷ ┯

┒ ┎ ┚ ┖ ╂ ┠ ┨ ┸ ┰

╮ ╭ ╯ ╰

╇ ╈ ╉ ╊ ╃ ╄ ╅ ╆ ┽ ┾ ╀ ╁

┡ ┢ ┩ ┪ ┞ ┟ ┦ ┧

┲ ┱ ┹ ┺ ┮ ┭ ┵ ┶

╼ ╾ ╽ ╿

█ ▛ ▜ ▟ ▙ ▄ ▀ ▐ ▌ ▞ ▚ ▖▗ ▘▝

█ ▇ ▆ ▅ ▄ ▃ ▂ ▁ ▔

█ ▉ ▊ ▋ ▌ ▍ ▎ ▏ ▕

█ ▓ ▒ ░

╱ ╲ ╳

■ □ ▢ ▣ ▪ ▫ ▬ ▭ ▮ ▯ ◆ ◇
○ ◎ ◉ ● ◐ ◑ ◒ ◓ ◕ ◖ ◗ ◙ ◚ ◛ ◜ ◝ ◞ ◟ ◠ ◡
◧ ◨ ◩ ◪ ◫ ◰ ◱ ◲ ◳ ◴ ◵ ◶ ◷ ◢ ◣ ◤ ◥
		

Resources: Artists, books and references

Resources: Contributors

An incomplete list of enthusiasts and testers which contributed with invaluable help, feedback and ideas!
<3

Resources: Changelog

Resources: Repository

The core files with all the demos, tests and examples:
github.com/ertdfgcvb/play.core

This project is licensed under the Apache License 2.0:
www.apache.org/licenses

Winter, 2020