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” (since v1.1 of the playground a more friendly and less brutalist navigation has been added).
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 (Local Storage Directory) or uploaded to this playground 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:
-
boot() is called once before the first pre call;
it can be used to access context info (for example font metrics) before the firs render pass. -
pre() is called once per loop, before main;
it can be used to prepare data for rendering (for example bitmaps, webcam data or normalisation of the mouse pointer). -
main() is called once per each cell;
this is the main loop to prepare the data for the renderer: it can return a single character (or an object).
-
post() is called once per loop after pre and main, just before the rendering;
it can be used to overwrite data in the buffer (for example for a window overlay).
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 frameThe 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:
let extraCellData let cols, rows export function pre(context, cursor, buffer) { if (cols != context.cols || rows != context.rows) { // The window has been resized or first run 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+I : immediate mode Cmd/Ctrl+Period : toggle editor views Cmd/Ctrl+Shift+F : enter fullscreen (output only) Cmd/Ctrl+Shift+C : copy a frame from the output as text Cmd/Ctrl+S : save to local storage, with permalink Cmd/Ctrl+Shift+U : share to playground (needs author and title tags)
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:
- @author and @title tags need to be set somewhere (see below)
- the program must generate an output
- there must be at least one LOC (line of code)
/** @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
Webcam init and helper
A wrapper for a canvas element
Some common palettes and simple color helpers
Draw text boxes with optional custom styles
Exports a single frame (or a range) to an image
Image loader and helper
Some GLSL functions ported to JS
Some signed distance functions
Sorts a set of characters by brightness
2D vector helper functions
3D vector helper functions
internal
Safe buffer helpers, mostly for internal use
Exports a file via Blob
Various file type loader, returns a Promise
String helpers
Source: Examples
basics
10 PRINT CHR$(205.5+RND(1)); : GOTO 10
Use of coord.index
Use of coord.x and coord.y
Crosshair example with mouse cursor
Use of context.metrics.aspect
Draw a square using a distance function
Console output inside the main() loop
What’s your name?
Vertical vs horizontal changes impact FPS
Rendering to a canvas element
Export 10 frames as images
The smallest program possible?
Use of context.frame (ASCII horizon)
Use of context.time
sdf
Smooth SDF balls
Draw a smooth circle with exp()
The cursor controls box thickness and exp
Smooth SDF Rectangles
Smooth union of two circles
demos
Think inside of the box
Shadertoy port
Ported from a1k0n’s donut demo.
Oldschool flame effect
Oldschool flame effect
A remix of Paul Haeberli’s Dynadraw
Double resolution version of GOL
Function hotlink example (GitHub)
Patterns obtained trough modulo and xor
Click or tap to toggle mode
Fun with integers
Oldschool plasma demo
Checker variation
Wave variation
Shadertoy port
Draw donuts with SDF
camera
Doubled vertical resolution input from camera
Grayscale input from camera
Color input from camera (quantised)
contributed
¯\_(ツ)_/¯
From wingdings icons to unicode emojis
Inspired by Frederick Hammersley, 1969
Inspired by Ernst Jandl, 1964
Conway's Game of Life
Click to spawn new path segments
Click to drop sand
Low-res physarum slime mold simulation
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
A single fullscreen app
Multiple instances on the same page
tests
A benchmark for JS Proxy()
A naive rendering benchmark
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 four 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.
-
Bug 217047:
document.fonts.ready is resolved too quickly in first run of module.
A workaround has been found, involving an artifical delay of 3 rAFs which solves the issue. -
Bug 218275:
Web Inspector: error line number missing when code is a module, making debugging difficult.
This one makes debugging code inside a module on Safari extremely painful and almost impossoible. -
Bug 218284:
Error event not captured when error originates in module.
This bug won’t allow proper error highlighting in the live-edior on Safari.
-
Bug 225695:
Fractional CSS line-height value translates to rounded (floored) element height.
This will result in erroneus line height calculations.
-
Bug 240213:
.measureText() returns TextMetrics object with rounded values for bounding box.
The values of the bounding box returned by CanvasRenderingContext2D.measureText() are rounded resulting in imprecise calculations in typographic centered applications (except for .actualBoundingBoxRight).
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
- Eric Fischer: The Evolution of Character Codes, 1874-1968
- Ernst Jandl: oeö (1964)
- Stan VanderBeek: Poemfield No 3 (1967)
- Richard Williams: Art1 [GitHub] (1968)
- Frederick Hammersley: Computer-generated drawings on paper (1969)
- Peter Finch: Typewriter Poems (1972)
- John Socha: Norton Commander (1986)
- AA-lib, a portable ascii art GFX library (1997)
- libcaca, the industry-standard colour ASCII-art library (2003)
- Tarn Adams, Zach Adams: Dwarf Fortress (2006)
- Barrie Tullett: Typewriter Art – A modern Anthology, Laurence King Publishing (2014)
- Marvin and Ruth Sackners: The art of typewriting, Thames and Hudson (2015)
- Ruth Wolf Rehfeldt: Signs Fiction, Chert Galerie, Motto Books (2015)
- Michael Straßburger: MapSCII (2016)
- O. Akiyama: ASCII Art Synthesis with Convolutional Networks (2017)
- Domenico Barra: Ascii Pron Remix Art (2017)
- shiru8bit: AONDEMO, A demo for an old telephone (2018)
- Paco García Barcos: Sobre como se instalo… (2019)
- Doeke Wartena: P5 Terminal Graphics (2020)
- Teletext Art
- Typography in 8 bits: System fonts
- 16colors, retro ANSI/ASCII art gallery
- VTM: A Text-based Desktop Environment, aka Monotty Desktop (~2020)
- Wikipedia: Box-drawing character
- Wikipedia: Semigraphics
- Wikipedia: PETSCII
- Twitter: datad00r
- tmdc.scene.org, Text Mode Demo Contest
- int10h.org, Oldschool PC Fonts
- text-mode.org, A collection of standard text graphics
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