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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more