kite
[github]a diamond kite riding the wind, its string sagging in lulls
objects 64×24 15 fps 4.1 kB
.^. ,':| `. ,':::| `. ,':::::| `. .<-------+-------> _.' \ |::::::/ _- \ |:::::/ .' \ |::::/ _-' \ |:::/ .' \ |::/ _-' \ |:/ _.' \|/ .- ) _-' >)< _.' \ .- \ .-' >/< _-' \ -' ) .^. ,':| `. ,':::| `. ,':::::| `. .<-------+-------> _.' \ |::::::/ _- \ |:::::/ .' \ |::::/ _-' \ |:::/ .' \ |::/ _-' \ |:/ _.' \|/ .- ) _-' >)< _.' \ .- \ .-' >/< _-' \ -' )
use it
html
<script type="module" src="https://ascii.rest/ascii.js"></script> <ascii-art piece="kite"></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 { kite } from "ascii.rest/pieces";
<Ascii piece={kite} /> A client component, so it works in the Next.js app router as it is. Pass the name instead, piece="kite", to fetch the piece only when it mounts.
astro
--- // npm install github:bas3line/ascii import Ascii from "ascii.rest/astro"; --- <Ascii piece="kite" />
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 { kite } from "ascii.rest/pieces";
const el = document.querySelector<HTMLPreElement>("pre")!;
const stop = mount(el, kite); 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.
/*
* kite: a diamond kite on a long string from off the bottom left. Gusts lift
* it and pull the string straight, lulls let it sag, and the tail flutters.
*/
import type { Frame, Meta } from "../types.ts";
export const meta = {
name: "kite",
category: "objects",
note: "a diamond kite riding the wind, its string sagging in lulls",
cols: 64,
rows: 24,
fps: 15,
} satisfies Meta;
const P = 40; // seconds before the wind repeats
const TAU = Math.PI * 2;
// Half the spar, in columns. Above the spar the edges climb two columns a
// row, so the top is B / 2 rows up; below they drop one a row, so the bottom
// is B rows down. Width to height is about 0.7, square for square.
const B = 8;
const TAIL = 7; // rows of tail
const FILL = ":";
// Where a shallow line sits inside its cell, as the glyph that sits there.
const level = (f: number) => (f < 0.3 ? "'" : f < 0.55 ? "-" : f < 0.78 ? "." : "_");
export default function kite(): Frame {
const { cols, rows } = meta;
let g: string[][];
const put = (r: number, c: number, ch: string) => {
if (r >= 0 && r < rows && c >= 0 && c < cols) g[r][c] = ch;
};
return (t) => {
g = Array.from({ length: rows }, () => new Array<string>(cols).fill(" "));
const ph = (TAU * t) / P;
const wind = 0.5 + 0.3 * Math.sin(2 * ph) + 0.2 * Math.sin(5 * ph + 1.3);
// The kite rides up in a gust and drifts a little sideways, a whole cell
// at a time and seldom, so its outline holds.
const mid = Math.floor(45 + 0.7 * Math.sin(ph + 0.4));
const rs = Math.floor(6.5 + 1.4 * (0.5 - wind)); // the spar's row
// It rolls into the wind. A roll is drawn as the bottom half of the sail
// standing a column off the top half, so every edge keeps an even step
// and only the corners give.
const roll = Math.sin(5 * ph) + 0.35 * Math.sin(13 * ph + 1);
const up = mid; // the top half's middle
const lo = mid + (roll > 0.55 ? -1 : roll < -0.55 ? 1 : 0); // the bottom half's
// The sail, a row at a time out from the spar. Two opposite panels are
// shaded, like a harlequin kite.
for (let k = 1; k < B / 2; k++) {
const r = rs - k, e = B - 2 * k;
put(r, up - e - 1, ","), put(r, up - e, "'");
put(r, up + e, "`"), put(r, up + e + 1, ".");
for (let c = up - e + 1; c < up; c++) put(r, c, FILL);
put(r, up, "|");
}
put(rs - B / 2, up - 1, "."), put(rs - B / 2, up, "^"), put(rs - B / 2, up + 1, ".");
const a0 = Math.min(up, lo) - B, b0 = Math.max(up, lo) + B;
for (let c = a0 + 1; c < b0; c++) put(rs, c, "-");
put(rs, a0, "<"), put(rs, b0, ">"), put(rs, up, "+");
for (let j = 1; j < B; j++) {
const r = rs + j, e = B - j;
put(r, lo - e, "\\"), put(r, lo + e, "/");
for (let c = lo + 1; c < lo + e; c++) put(r, c, FILL);
put(r, lo, "|");
}
// The tail streams downwind from the bottom corner, one glyph a row, its
// flutter growing toward the free end, with a bow tied every few rows.
const vr = rs + B - 1;
const lean = 0.15 + 0.35 * wind;
const amp = (0.35 + 0.3 * wind) / 3.5;
const tx = (s: number) => lo + 0.5 + lean * s + amp * s * Math.sin(2.2 * s - 15 * ph);
for (let r = vr + 1; r < Math.min(rows, vr + 1 + TAIL); r++) {
const s = r - vr - 0.5;
const slope = tx(s + 0.5) - tx(s - 0.5);
const bend = tx(s + 0.5) - 2 * tx(s) + tx(s - 0.5);
const c = Math.floor(tx(s));
put(r, c, slope > 0.6 ? "\\" : slope < -0.6 ? "/" : bend > 0 ? "(" : ")");
if ((r - vr) % 3 === 2) put(r, c - 1, ">"), put(r, c + 1, "<");
}
// The string comes in from a hand far off the bottom left, a glyph a
// column at its height in the cell. It sags as the wind drops and pulls
// straighter in a gust.
const hand = [-20, rows + 3];
const knot = [a0, rs + 0.5];
const sag = 1.7 - 1.1 * wind;
for (let c = 0; c < a0; c++) {
const u = (c + 0.5 - hand[0]) / (knot[0] - hand[0]);
const y = hand[1] + (knot[1] - hand[1]) * u + 4 * sag * u * (1 - u);
put(Math.floor(y), c, level(y % 1));
}
return g.map((row) => row.join("")).join("\n");
};
}