tunnel
[github]flying down a tiled tube that bends and twists
effects 64×24 30 fps 4.5 kB
;;;;;;;;;;;::-*################;:;;;;;;;;;;;;;,%%%%%%%%%%%%%%%@@ ;;;;;;;;;;;;;;::-###************.;;;;;;;;;;;;.#%%%%%%%%%%%%%%%@@ ;;;;;;;;;;;;;;;;;::=************+,;;;;;;;;;;,+##%%%%%%%%%%%%%%@@ ;;;;;;;;;;;;;;;;;;;:::=**********::;;;;;;;;::###%%%%%%%%%%%%%%@@ ;;;;;;;;;;;;;;;;;;:::::::=*+++++**.:::;;;;;.#####%%%%%%%%%%%%%=: ;;;;;;;;;;;;;;;;;:::::::::::=++=++-,::::;;,*#####%%%%%%%%%%=:;-- ;;;;;;;;;;;;;;;;::::::::::,,,,,,,,,.,,:::,-#######%%%%%%+::;---- ;;;;;;;;;;;;;;;:::::::::,,;----;:,,,--,,:;**######%%%+::;;;----- ---------==============.,,,,,,,,,,,,:==-.+***#####+::;;;;;;----- ###########*******++++.,,,:,,. .,:,--,:.***##=::;;;;;;;;------ ###########*******+++;.:--::.. .,,:,::.;+;::;;;;;;;;;;;------ ############******+-,,,==:,,.,,..,,:,,:::.,:;;;;;;;;;;;;;------- ############****=,,:::.=:,,,,,;,,,,,;-=++.:;;;;;;;;;;;;;;------- #############*;:::::::,.,::,--,,,,:;-+++.,;;;;;;;;;;;;;;-------- ###########=,:;;;;::::::.,:,===-,:::::,.;:::::::::;:;;;;;;;;;;;- %%######*::;;;;;;;;;;:.**=,.,-===:,,.:=*######%%%%%%%%%@%%%%%%%% %%%%##-:;;;;;;;;;;;;;,+*****:::,:::,=*######%%%%%%%%%@@@@@@@@@@@ %%%=,:;;;;;;;;;;;;;;:-###****,;;;;;;;:;###%%%%%%%%%%@@@@@@@@@@@@ *::;;;;;;;;;;;;;;;;;.########:;;;;;;;;;::*%%%%%%%%@@@@@@@@@@@@@@ ;;;;;;;;;;;;;;;;;;;,*#########,;;;;;;;;;;;:=%%%%@@@@@@@@@@@@@@@@ ;;;;;;;;;;;;;;;;;;:;%%%%%%%%%%-:;;;;;;;;;;;;::*@@@@@@@@@@@@@@@@@ ;;;;;;;;;;;;;;;;;;,%%%%%%%%%%%%,;;;;;;;;;;;---;:-%@@@@@@@@@@@@@@ ;;;;;;;;;;;;;;;;;,*%%%%%%%%%%%%*:;;;;;;----------;,*@@@@@@@@@@@@ ;;;;;;;;;;;;;;;;;:%%%%%%%%%%%%%%,;;-----------------;;%@@@@@@@@@+++++++++++**=:,,,,,,,,,,,,,,,,+*+++++++++++++#............... ++++++++++++++**=,,,::::::::::::%++++++++++++%,............... +++++++++++++++++**-::::::::::::;#++++++++++#;,,.............. +++++++++++++++++++***-::::::::::**++++++++**,,,.............. ++++++++++++++++++*******-:;;;;;::%***+++++%,,,,,.............-* +++++++++++++++++***********-;;-;;=#****++#:,,,,,..........-*+== ++++++++++++++++**********#########%##***#=,,,,,,,......;**+==== +++++++++++++++*********##+====+*###==##*+::,,,,,,...;**+++===== =========--------------%############*--=%;:::,,,,,;**++++++===== ,,,,,,,,,,,:::::::;;;;%###*##%@@@%#*#==#*%:::,,-**++++++++====== ,,,,,,,,,,,:::::::;;;+%*==**%%@@@@%##*#**%+;+**+++++++++++====== ,,,,,,,,,,,,::::::;=###--*##%##%%##*##***%#*+++++++++++++======= ,,,,,,,,,,,,::::-##***%-*#####+#####+=-;;%*++++++++++++++======= ,,,,,,,,,,,,,:+*******#%#**#==####*+=;;;%#++++++++++++++======== ,,,,,,,,,,,-#*++++******%#*#---=#*****#%+*********+*+++++++++++= ..,,,,,,:**++++++++++*%::-#%#=---*##%*-:,,,,,,......... ........ ....,,=*+++++++++++++#;:::::***#***#-:,,,,,,......... ...-#*++++++++++++++*=,,,::::#+++++++*+,,,.......... :**+++++++++++++++++%,,,,,,,,*+++++++++**:........ +++++++++++++++++++#:,,,,,,,,,#+++++++++++*-.... ++++++++++++++++++*+..........=*++++++++++++**: ++++++++++++++++++#............#+++++++++++===+*=. +++++++++++++++++#:............:*++++++==========+#: +++++++++++++++++*..............#++=================++.
use it
html
<script type="module" src="https://ascii.rest/ascii.js"></script> <ascii-art piece="tunnel"></ascii-art>
No install and no build step. ascii.js defines the <ascii-art> tag, loads the piece from ascii.rest, plays it while it is on screen, and holds the first frame for anyone who prefers reduced motion.
react
// npm install github:bas3line/ascii
import { Ascii } from "ascii.rest/react";
import { tunnel } from "ascii.rest/pieces";
<Ascii piece={tunnel} /> A client component, so it works in the Next.js app router as it is. Pass the name instead, piece="tunnel", to fetch the piece only when it mounts.
astro
--- // npm install github:bas3line/ascii import Ascii from "ascii.rest/astro"; --- <Ascii piece="tunnel" />
Renders the first frame on the server, so the page is whole before any script runs, then plays the piece once it loads.
typescript
// npm install github:bas3line/ascii
import { mount } from "ascii.rest";
import { tunnel } from "ascii.rest/pieces";
const el = document.querySelector<HTMLPreElement>("pre")!;
const stop = mount(el, tunnel); mount plays a piece in a <pre>, or on a <canvas> for the coloured ones, and returns a function that stops it. Every piece, option and frame is typed.
/*
* tunnel: the demo-scene tunnel. Each cell casts a ray down a tiled tube that
* bends away out of sight, so the far end swings about, while the tiles wind
* round it in a slow spiral and fade into the dark.
*/
import type { Frame, Meta } from "../types.ts";
export const meta = {
name: "tunnel",
category: "effects",
note: "flying down a tiled tube that bends and twists",
cols: 64,
rows: 24,
fps: 30,
} satisfies Meta;
const RAMP = " .,:;-=+*#%@";
const F = 18; // focal length, in columns
const AROUND = 10; // tiles round the tube
const ALONG = 0.55; // tiles per unit of depth
const DIM = 0.35; // the dark tiles' share of the light ones
const MORTAR = 0.1; // the mortar's share of a light tile
const GAP = 0.07; // the most of a tile the mortar takes on each side
const SPEED = 2.4; // units of depth a second
const TWIST = 0.06; // turns of the tile pattern per unit of depth
const FOG = 2.8; // depth over which light falls by e
const BEND = 3; // depth over which the tube turns toward its far heading
const KEY = 0.12; // how much a lamp to the upper left favours the far wall
const SHARP = 0.7; // share of a cell's footprint the pattern is averaged over
// A square wave, and pulses of half-width g at the whole numbers, each
// averaged over a span w, so tiles smaller than a cell fade to an even grey.
const wrap = (x: number) => x - Math.floor(x);
const wave = (x: number, w: number) => (2 * (Math.abs(wrap((x - w / 2) / 2) - 0.5) - Math.abs(wrap((x + w / 2) / 2) - 0.5))) / w;
const ridge = (x: number, g: number) => Math.floor(x + g) * 2 * g + Math.min(wrap(x + g), 2 * g);
const pulse = (x: number, w: number, g: number) => (ridge(x + w / 2, g) - ridge(x - w / 2, g)) / w;
export default function tunnel(): Frame {
const { cols, rows } = meta;
const out: string[] = new Array(cols * rows);
const W = cols + 1, H = rows + 1; // one sample more right and below, for each cell's footprint
const U = new Float64Array(W * H), V = new Float64Array(W * H), Z = new Float64Array(W * H), L = new Float64Array(W * H);
const TAU = 2 * Math.PI;
const turn = (d: number) => d - AROUND * Math.round(d / AROUND); // going round, the seam is no jump
return (t, { paper = false } = {}) => {
// Where the far end points, as a slope off the view axis, on a slow loop.
const ax = 0.42 * Math.sin(t * 0.55), ay = 0.26 * Math.sin(t * 0.9 + 1);
const spin = t * 0.06;
for (let r = 0; r < H; r++) {
for (let c = 0; c < W; c++) {
const sx = (c + 0.5 - cols / 2) / F;
const sy = -((r + 0.5 - rows / 2) * 2) / F;
// A ray hits the wall (radius 1) at the depth z where its offset from
// the bent axis is 1. Solve by iteration: the axis at depth z leans
// toward (ax, ay) by a share that grows with z.
let z = 1 / Math.max(1e-3, Math.hypot(sx, sy)), dx = sx, dy = sy;
for (let k = 0; k < 4; k++) {
const lean = 1 - Math.exp(-z / BEND);
dx = sx - ax * lean;
dy = sy - ay * lean;
z = Math.min(60, 1 / Math.max(1e-3, Math.hypot(dx, dy)));
}
const k = c + r * W;
U[k] = (Math.atan2(dy, dx) / TAU + spin + z * TWIST) * AROUND;
V[k] = z * ALONG + t * SPEED * ALONG;
Z[k] = z;
// Light falls off down the tube, and the wall facing the lamp is brighter.
L[k] = Math.exp(-z / FOG) * (1 - KEY + KEY * (dx - dy) * z * Math.SQRT1_2);
}
}
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const k = c + r * W, z = Z[k];
// How far the pattern moves across this cell, in tiles.
const wu = SHARP * Math.max(Math.abs(turn(U[k + 1] - U[k])), Math.abs(turn(U[k + W] - U[k])), 0.01);
const wv = SHARP * Math.max(Math.abs(V[k + 1] - V[k]), Math.abs(V[k + W] - V[k]), 0.01);
// Light tiles and dark ones, set in mortar lines kept about a cell
// wide on screen however near or far they are.
const light = 0.5 - 0.5 * wave(U[k], wu) * wave(V[k], wv);
const gu = Math.min(GAP, (z * AROUND) / (4 * Math.PI * F)), gv = Math.min(GAP, (z * z * ALONG) / F);
const mortar = 1 - (1 - pulse(U[k], wu, gu)) * (1 - pulse(V[k], wv, gv));
const b = ((DIM + (1 - DIM) * light) * (1 - mortar) + MORTAR * mortar) * L[k] * 1.25;
const i = Math.min(RAMP.length - 1, Math.round(Math.max(0, b) ** 0.8 * (RAMP.length - 1)));
out[c + r * cols] = RAMP[paper ? RAMP.length - 1 - i : i];
}
}
const lines: string[] = [];
for (let r = 0; r < rows; r++) lines.push(out.slice(r * cols, (r + 1) * cols).join(""));
return lines.join("\n");
};
}