Public
Edited
May 6, 2024
5 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// The number of tiles along each edge of the artwork
TILES = 40
Insert cell
// Each tile will be this px wide
TILE_HEIGHT = HEIGHT / TILES
Insert cell
CHAR_SET = "⋰/:_◜◠+*`=?!¬░█▄▀▀▁▂▃▄▅▆▇█▰▱▖▗▘▙▚▛▜▝▞▟═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟╠╡╢╣╤╥╦╧╨╩╪╫╬■□▢▣▤▥▦▧▨▩▪▫▬▭▮▯▰▱○◌◍◎●◐◑◒◓◔◕◖◗◘◙◚◛◜◝◞◟◠◡◢◣◤◥◦◧◨◩◪◫◬◭◮◯◰◱◲◳◴◵◶◷◸◹◺◻◼◿░▒▓█▄▀!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxy░▒▓▔▕▖▗▘▙▚▛▜▝▞▟×✕✖⨉⨯."
Insert cell
Insert cell
FONT_SIZE = 40
Insert cell
/**
* Given a canvas context and a character, draws the character on the canvas,
* and then measures how many pixels were painted black.
*/
function measureCharacterWeight(char) {
const context = DOM.context2d(TILE_HEIGHT, TILE_HEIGHT);
// Set the font size
context.font = `${FONT_SIZE}px monospace`;

// Draw the character
context.fillText(char, 0, 12);

// Find the width of each monospace character in pixels
const charWidth = (FONT_SIZE, context.measureText("A").width);

// Get the raw pixel data
// This is a Uint8ClampedArray representing a one-dimensional array
// containing the data in the RGBA order, with integer values between 0
// and 255 (inclusive).
const pixels = context.getImageData(0, 0, TILE_HEIGHT, TILE_HEIGHT).data;

// For each chunk of 4 values, count the pixels that are not transarent
let weight = 0;
for (let i = 0; i < pixels.length; i += 4) {
if (pixels[i + 3] !== 0) {
weight++;
}
}

// Return the number of black pixels
return weight;
}
Insert cell
measureCharacterWeight("A")
Insert cell
CHAR_TO_WEIGHT = Object.fromEntries(
[...CHAR_SET]
.map((char) => [char, measureCharacterWeight(char)])
.sort((a, b) => a[1] - b[1])
)
Insert cell
first = DOM.context2d(HEIGHT, HEIGHT)
Insert cell
first.canvas
Insert cell
(first.font = `40px monospace`)
Insert cell
{
// Clear the canvas
first.clearRect(0, 0, HEIGHT, HEIGHT);
// Subdivide the canvas into tiles. For each tile, draw a character from
// CHAR_TO_WEIGHT, in ascending order of weight.
for (let x = 0; x < TILES; x++) {
for (let y = 0; y < TILES; y++) {
const chars = Object.keys(CHAR_TO_WEIGHT);
const char = chars[(y * TILES + x) % chars.length];
first.fillText(char, x * TILE_HEIGHT, y * TILE_HEIGHT);
}
}
}
Insert cell
import { rule } from "@mjbo/orcinus"
Insert cell
(second.font = `30px monospace`)
Insert cell
second = DOM.context2d(HEIGHT, HEIGHT)
Insert cell
offset = {
let i = 0;
while (true) {
await Promises.delay(100);
yield i++ % 255;
}
}
Insert cell
offset2 = {
let i = 0;
while (true) {
await Promises.delay(1000);
yield i++ % 255;
}
}
Insert cell
{
// Clear the canvas
second.clearRect(0, 0, HEIGHT, HEIGHT);
// Subdivide the canvas into tiles. For each tile, draw a character from
// CHAR_TO_WEIGHT, in ascending order of weight.
for (let x = 0; x < TILES; x++) {
for (let y = 0; y < TILES; y++) {
if (!rules[y][x]) {
continue;
}

const chars = CHAR_SET;
const char = chars[(y * TILES + x + offset) % chars.length];
second.fillText(char, x * TILE_HEIGHT, y * TILE_HEIGHT);
}
}
}
Insert cell
rules = [...rule(offset2, TILES, TILES)]
Insert cell
Insert cell
CHARACTER_SETS = {
const RAMP = "▁▂▃▄▅▆▇█";
const SQUARES = "■□▢▣▤▥▦▧▨▩▪▫◻◼◨";
const CAPS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const LOWERS = "abcdefghijklmnopqrstuvwxy";
const EDGES = "═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟╠╡╢╣╤╥╦╧╨╩╪╫╬";
const NUMS = "0123456789";
const SYMBOLS = "⋰/:_◜◠+*`=?!¬!#$%&'()*+,-./:;<=>?@[\\]^_`";
const RECTANGLES = "░█▄▀▀▰▱▖▗▘▙▚▛▜▝▞▟░▒▓█▄▀░▒▓▔▕▖▗▘▙▚▛▜▝▞▟";
const CIRCLES = "○◌◍◎●◐◑◒◓◔◕◖◗◘◙◚◛◜◝◞◟◠◡◴◵◶◷◯◦";
const CROSSES = "×✕✖⨉⨯";
const BARS = "▬▭▮▯▰▱";
const TRIANGLES = "◢◣◤◥◧◩◪◫◬◭◮◰◱◲◳◸◹◺◿";

return [
RAMP,
SQUARES,
//CAPS,
//LOWERS,
EDGES,
// NUMS,
// SYMBOLS,
RECTANGLES,
CIRCLES,
CROSSES,
BARS,
TRIANGLES
];
}
Insert cell
/**
* Given a 2D array, and a pair of coordinates, get all the neighbours of the
* coordinates.
*/
function neighbours(grid, x, y) {
return [
[x - 1, y - 1],
[x, y - 1],
[x + 1, y - 1],
[x - 1, y],
[x, y],
[x + 1, y],
[x - 1, y + 1],
[x, y + 1],
[x + 1, y + 1]
].filter(
([x, y]) => x >= 0 && y >= 0 && x < grid.length && y < grid[0].length
);
}
Insert cell
grid = Array(TILES)
.fill(null)
.map(() => Array(TILES).fill(" "))
Insert cell
function seed(grid) {
for (let x = 0; x < grid.length; x++) {
for (let y = 0; y < grid[0].length; y++) {
if (Math.random() > 0.99) {
grid[x][y] = _.sample(_.sample(CHARACTER_SETS));
}
}
}

return grid;
}
Insert cell
seed(grid)
Insert cell
function transition(characters) {
const sets = CHARACTER_SETS.filter((set) => {
const count = characters.reduce((acc, char) => {
if (set.includes(char)) acc++;
return acc;
}, 0);
return count === 1;
});

if (sets.length === 0) {
return " ";
}

return _.sample(_.sample(sets));
}
Insert cell
function step(grid) {
const newGrid = Array(TILES)
.fill(null)
.map(() => Array(TILES).fill(" "));

for (let x = 0; x < grid.length; x++) {
for (let y = 0; y < grid[0].length; y++) {
const ns = neighbours(grid, x, y);
const characters = ns.map((n) => grid[n[0]][n[1]]);
newGrid[x][y] = transition(characters);
}
}

return newGrid;
}
Insert cell
frame = {
let g = grid;
while (true) {
await Promises.delay(50);
g = seed(step(grid));
yield g;
}
}
Insert cell
(third.font = `20px monospace`)
Insert cell
third = DOM.context2d(HEIGHT, HEIGHT)
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more