8 |
9 | Inkplate display(INKPLATE_1BIT);
10 |
11 | void setup()
12 | {
13 | display.begin();
14 | display.clearDisplay();
15 | display.clean();
16 |
17 | display.setCursor(50, 290);
18 | display.setTextSize(3);
19 | display.print(F("Loading art"));
20 | display.display();
21 |
22 | WiFi.begin("[your ssid here]", "[your password here]");
23 | while (WiFi.status() != WL_CONNECTED)
24 | {
25 | delay(100);
26 | display.print(".");
27 | display.partialUpdate();
28 | }
29 |
30 | display.println("Connected!");
31 | display.partialUpdate();
32 | }
33 |
34 | void loop()
35 | {
36 | // janky reconnect logic
37 | if (WiFi.status() != WL_CONNECTED)
38 | {
39 | WiFi.reconnect();
40 | delay(5000);
41 | int cnt = 0;
42 |
43 | while (WiFi.status() != WL_CONNECTED)
44 | {
45 | delay(1000);
46 | cnt++;
47 |
48 | if (cnt == 10)
49 | {
50 | ESP.restart();
51 | }
52 | }
53 | }
54 |
55 | display.clearDisplay();
56 | // https://github.com/jdan/hashart#a-small-screenshot-service
57 | char *url = "https://FILLMEIN/random/800/600/random.png";
58 | display.drawImage(url, 0, 0, true, 0);
59 | display.display();
60 |
61 | // Sleep for 5 minutes
62 | esp_sleep_enable_timer_wakeup(1000L * 1000L * 60L * 5L);
63 | (void)esp_light_sleep_start();
64 | }
65 |
--------------------------------------------------------------------------------
/art/combinators.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _ } = require("./util.js");
3 |
4 | class Combinators extends Art {
5 | constructor() {
6 | super({
7 | branches: 32,
8 | });
9 | this.created = "06 Jun 2021";
10 | this.filename = "combinators.js";
11 | this.hidden = true;
12 | }
13 |
14 | nthDepthFirst(tree, n) {
15 | function leaves(tree) {
16 | if (tree.value) {
17 | return [tree];
18 | } else {
19 | return leaves(tree.left).concat(leaves(tree.right));
20 | }
21 | }
22 |
23 | const ls = leaves(tree);
24 | return ls[n % ls.length];
25 | }
26 |
27 | getValues(byte) {
28 | const choices = "SKI";
29 | return [
30 | choices[(17 * byte) % choices.length],
31 | choices[(23 * byte) % choices.length],
32 | ];
33 | }
34 |
35 | fork(leaf, byte) {
36 | delete leaf.value;
37 | let [left, right] = this.getValues(byte);
38 | leaf.left = {
39 | value: left,
40 | };
41 | leaf.right = {
42 | value: right,
43 | };
44 | }
45 |
46 | treeToString(tree) {
47 | if (tree.value) {
48 | return tree.value;
49 | } else {
50 | return (
51 | "(" + this.treeToString(tree.left) + this.treeToString(tree.right) + ")"
52 | );
53 | }
54 | }
55 |
56 | draw(ctx, { branchesBuffer }) {
57 | let tree = {
58 | value: "I",
59 | };
60 | branchesBuffer.forEach((byte) => {
61 | let node = this.nthDepthFirst(tree, byte);
62 | this.fork(node, byte);
63 | });
64 | console.log(this.treeToString(tree));
65 | }
66 | }
67 |
68 | exports.Combinators = Combinators;
69 |
--------------------------------------------------------------------------------
/admin.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Admin console
5 |
6 |
7 | Enabled pieces
8 |
9 |
37 |
38 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## hashart
2 |
3 | The code behind [hash.jordanscales.com](https://hash.jordanscales.com), an experiment in turning SHA-256 hashes into pixels.
4 |
5 | 
6 |
7 | ### running
8 |
9 | ```
10 | npm i
11 | npm run dev
12 | ```
13 |
14 | ### a small screenshot service
15 |
16 | This repository contains a small service for rendering art directly to PNGs using [canvas](https://www.npmjs.com/package/canvas). You can run it with:
17 |
18 | ```
19 | node server.js
20 | ```
21 |
22 | This service is also contained in a Docker image that I automatically publish to [GitHub packages](https://github.com/jdan/hashart/packages/728823):
23 |
24 | ```
25 | docker run --rm -p "3000:3000" docker.pkg.github.com/jdan/hashart/hashart-srv:latest
26 | ```
27 |
28 | To be able to use the "mario" piece:
29 |
30 | ```
31 | docker run --rm -p "3000:3000" -v "/path/to/mariobros.nes:/app/vendor/roms/mariobros.nes" docker.pkg.github.com/jdan/hashart/hashart-srv:latest
32 | ```
33 |
34 | ### rendering hashart on screens
35 |
36 | I uploaded [arduino.cc](/arduino.cc) to an [Inkplate 6](https://inkplate.io/) to display random
37 | hashart pieces in my apartment. I framed my inkplate using [Level Frames](https://www.levelframes.com/frames/new?width=5.25&height=4) (5 1/4" x 4" with 1 1/2" matting) and gave it a [2000mAh battery](https://www.adafruit.com/product/2011) which lasts quite a long time.
38 |
39 | 
40 |
--------------------------------------------------------------------------------
/art/voronoi.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _, project } = require("./util.js");
3 | const V = require("voronoi");
4 |
5 | const NUM_POINTS = 16;
6 |
7 | class Voronoi extends Art {
8 | constructor() {
9 | super({
10 | x: 1,
11 | y: 1,
12 | others: (NUM_POINTS - 1) * 2,
13 | });
14 |
15 | this.filename = "voronoi.js";
16 | this.created = "30 Dec 2022";
17 | }
18 |
19 | getDescription() {
20 | return `
21 | Read ${NUM_POINTS} byte pairs from the array, representing
22 | a series of (x, y) points. Do not draw the points on the canvas. Partition
23 | the plane into a Voronoi diagram ,
24 | identifying regions of the graph which are closest to each point.
25 |
26 | Under the hood we use @gorhill 's excellent
27 | Voronoi library ,
28 | which took no time at all to plug in.
29 | `;
30 | }
31 |
32 | draw(ctx, { xBuffer, yBuffer, othersBuffer }) {
33 | const w = ctx.canvas.width;
34 | const h = ctx.canvas.height;
35 | ctx.lineWidth = _(5, w);
36 |
37 | const voronoiGenerator = new V();
38 |
39 | const coords = [...xBuffer, ...yBuffer, ...othersBuffer];
40 | const vertices = [];
41 | for (let i = 0; i < NUM_POINTS; i++) {
42 | vertices.push({
43 | x: project(coords[2 * i], 0, 256, 0, w),
44 | y: project(coords[2 * i + 1], 0, 256, 0, h),
45 | });
46 | }
47 |
48 | const { edges } = voronoiGenerator.compute(vertices, {
49 | xl: 0,
50 | xr: w,
51 | yt: 0,
52 | yb: h,
53 | });
54 |
55 | for (let i = 0; i < edges.length; i++) {
56 | ctx.beginPath();
57 | ctx.moveTo(edges[i].va.x, edges[i].va.y);
58 | ctx.lineTo(edges[i].vb.x, edges[i].vb.y);
59 | ctx.stroke();
60 | }
61 | }
62 | }
63 |
64 | exports.Voronoi = Voronoi;
65 |
--------------------------------------------------------------------------------
/art/pieces.js:
--------------------------------------------------------------------------------
1 | const { Circle } = require("./circle.js");
2 | const { Boxes } = require("./boxes.js");
3 | const { Stocks } = require("./stocks.js");
4 | const { Collatz } = require("./collatz.js");
5 | const { Mario } = require("./mario.js");
6 | const { Chess } = require("./chess.js");
7 | const { Walk } = require("./walk.js");
8 | const { Fraction } = require("./fraction.js");
9 | const { Semicircle } = require("./semicircle.js");
10 | const { Turing } = require("./turing.js");
11 | const { Divisions } = require("./divisions.js");
12 | const { QuasiFlake } = require("./quasiflake.js");
13 | const { Combinators } = require("./combinators.js");
14 | const { Noise } = require("./noise.js");
15 | const { Sandpiles } = require("./sandpiles.js");
16 | const { Element } = require("./element.js");
17 | const { Rings } = require("./rings.js");
18 | const { HoneyComb } = require("./honeycomb.js");
19 | const { Knots } = require("./knots.js");
20 | const { Fifteen } = require("./fifteen.js");
21 | const { Automata } = require("./automata.js");
22 | const { Nes } = require("./nes.js");
23 | const { Epicycles } = require("./epicycles.js");
24 | const { Network } = require("./network.js");
25 | const { Voronoi } = require("./voronoi.js");
26 | const { Julia } = require("./julia.js");
27 | const { ThreeBody } = require("./three-body.js");
28 |
29 | module.exports = {
30 | circles: Circle,
31 | boxes: Boxes,
32 | stocks: Stocks,
33 | collatz: Collatz,
34 | mario: Mario,
35 | chess: Chess,
36 | walk: Walk,
37 | fraction: Fraction,
38 | semicircle: Semicircle,
39 | turing: Turing,
40 | divisions: Divisions,
41 | quasiflake: QuasiFlake,
42 | combinators: Combinators,
43 | noise: Noise,
44 | sandpiles: Sandpiles,
45 | element: Element,
46 | rings: Rings,
47 | honeycomb: HoneyComb,
48 | knots: Knots,
49 | fifteen: Fifteen,
50 | automata: Automata,
51 | nes: Nes,
52 | epicycles: Epicycles,
53 | network: Network,
54 | voronoi: Voronoi,
55 | julia: Julia,
56 | threebody: ThreeBody,
57 | };
58 |
--------------------------------------------------------------------------------
/scripts/generate-state.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Using vendor/rom.nes, generates a snapshot of the game after a series
3 | * of preliminary loading screens and button presses
4 | */
5 |
6 | const fs = require("fs");
7 | const path = require("path");
8 | const jsnes = require("jsnes");
9 |
10 | const VENDOR_PATH = path.join(__dirname, "../vendor");
11 | const ROM_PATH = path.join(VENDOR_PATH, "roms/mariobros.nes");
12 | const STATE_PATH = path.join(VENDOR_PATH, "state.json");
13 |
14 | // Copied from art.js
15 | function buttonPress(nes, button, holdFrames = 1) {
16 | nes.buttonDown(1, button);
17 | for (let i = 0; i < holdFrames; i++) {
18 | nes.frame();
19 | }
20 | nes.buttonUp(1, button);
21 | nes.frame();
22 | }
23 |
24 | module.exports = () => {
25 | if (!fs.existsSync(ROM_PATH)) {
26 | console.log(
27 | "vendor/roms/mariobros.nes not found, skipping state generation..."
28 | );
29 | return;
30 | } else if (fs.existsSync(STATE_PATH)) {
31 | return fs.readFileSync(STATE_PATH, "utf-8");
32 | }
33 |
34 | const nes = new jsnes.NES();
35 | const romData = fs.readFileSync(ROM_PATH, { encoding: "binary" });
36 |
37 | nes.loadROM(romData);
38 |
39 | const BUTTON_START = 3;
40 | const BUTTON_RIGHT = 7;
41 |
42 | // Wait 137 frames for the menu to load
43 | for (let i = 0; i < 137; i++) {
44 | nes.frame();
45 | }
46 | buttonPress(nes, BUTTON_START);
47 |
48 | // Wait 31 games for the first game to load
49 | for (let i = 0; i < 31; i++) {
50 | nes.frame();
51 | }
52 |
53 | buttonPress(nes, BUTTON_START);
54 |
55 | // Wait 162 frames for level 1-1 to start
56 | for (let i = 0; i < 162; i++) {
57 | nes.frame();
58 | }
59 |
60 | // Walk right for 30 frames
61 | buttonPress(nes, BUTTON_RIGHT, 30);
62 |
63 | // Wait 30 frames for the walking animation to stop
64 | for (let i = 0; i < 30; i++) {
65 | nes.frame();
66 | }
67 |
68 | const state = JSON.stringify(nes.toJSON());
69 | fs.writeFileSync(STATE_PATH, state);
70 | return state;
71 | };
72 |
--------------------------------------------------------------------------------
/art/divisions.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _ } = require("./util.js");
3 |
4 | class Divisions extends Art {
5 | constructor() {
6 | super({
7 | divisions: 32,
8 | });
9 | this.created = "05 Jun 2021";
10 | this.filename = "divisions.js";
11 | }
12 |
13 | getDescription({ divisionsBuffer }) {
14 | return `
15 | Begin with a single rectangle covering the entire canvas.
16 |
17 | For each of the ${divisionsBuffer.length} bytes in divisions,
18 | determine which rectangle to split (byte % rectangles.length) and
19 | divide that rectangle in half either horizontally or vertically (byte % 2).
20 | `;
21 | }
22 |
23 | draw(ctx, { divisionsBuffer }) {
24 | const w = ctx.canvas.width;
25 | const h = ctx.canvas.height;
26 |
27 | ctx.lineWidth = _(5, w);
28 |
29 | let regions = [{ x: 0, y: 0, w, h }];
30 |
31 | for (let i = 0; i < divisionsBuffer.length; i += 1) {
32 | const idx = divisionsBuffer[i] % regions.length;
33 | const direction = divisionsBuffer[i] % 2;
34 | const region = regions[idx];
35 |
36 | if (direction === 0) {
37 | regions.splice(
38 | idx,
39 | 1,
40 | { x: region.x, y: region.y, w: region.w, h: region.h / 2 },
41 | {
42 | x: region.x,
43 | y: region.y + region.h / 2,
44 | w: region.w,
45 | h: region.h / 2,
46 | }
47 | );
48 | } else {
49 | regions.splice(
50 | idx,
51 | 1,
52 | { x: region.x, y: region.y, w: region.w / 2, h: region.h },
53 | {
54 | x: region.x + region.w / 2,
55 | y: region.y,
56 | w: region.w / 2,
57 | h: region.h,
58 | }
59 | );
60 | }
61 | }
62 |
63 | regions.forEach(({ x, y, w, h }) => {
64 | ctx.beginPath();
65 | ctx.rect(x, y, w, h);
66 | ctx.stroke();
67 | });
68 | }
69 | }
70 |
71 | exports.Divisions = Divisions;
72 |
--------------------------------------------------------------------------------
/art/epicycles.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _, project } = require("./util.js");
3 |
4 | class Epicycles extends Art {
5 | constructor() {
6 | super({
7 | l1: 4,
8 | v1: 4,
9 | l2: 4,
10 | v2: 4,
11 | });
12 | this.filename = "epicycles.js";
13 | this.created = "12 Mar 2022";
14 | }
15 |
16 | getDescription() {
17 | return `
18 | Generate four numbers: Two representing the lengths of two arms, and two
19 | representing the speed at which they rotate. The two arms can range from
20 | 0 to half the width of the canvas. The two speeds can range from -1/10 to
21 | 1/10 (radians per unit of time).
22 |
23 | The first arm is affixed to the center of the canvas, and the second arm
24 | is affixed to the end of the first arm. Both arms begin pointing east.
25 |
26 | At each unit of time, increase the angle of the first arm according to
27 | v1 without rotating the second arm. Then, increase the angle
28 | of the second arm according to v2. Draw a dot at the end of
29 | the second arm. Repeat this process 2000 times.
30 | `;
31 | }
32 |
33 | draw(ctx, { l1, l2, v1, v2 }) {
34 | const w = ctx.canvas.width;
35 | const h = ctx.canvas.height;
36 | const s = Math.min(w, h);
37 |
38 | ctx.fillStyle = "rgb(0, 0, 0)";
39 |
40 | const FRAMES = 2000;
41 | for (let i = 0; i < FRAMES; i++) {
42 | const radialUnit = 1 / 5;
43 |
44 | const x =
45 | (l1 / 2) * Math.cos((v1 - 0.5) * i * radialUnit) +
46 | (l2 / 2) * Math.cos((v2 - 0.5) * i * radialUnit);
47 | const y =
48 | (l1 / 2) * Math.sin((v1 - 0.5) * i * radialUnit) +
49 | (l2 / 2) * Math.sin((v2 - 0.5) * i * radialUnit);
50 |
51 | ctx.beginPath();
52 | ctx.arc(
53 | project(x, -1, 1, w / 2 - s / 2, w / 2 + s / 2),
54 | project(y, -1, 1, h / 2 - s / 2, h / 2 + s / 2),
55 | _(4, s),
56 | 0,
57 | 2 * Math.PI
58 | );
59 | ctx.fill();
60 | }
61 | }
62 | }
63 |
64 | exports.Epicycles = Epicycles;
65 |
--------------------------------------------------------------------------------
/art/knots.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _, project } = require("./util.js");
3 |
4 | class Knots extends Art {
5 | constructor() {
6 | super({
7 | values: 32,
8 | });
9 |
10 | this.filename = "knots.js";
11 | this.created = "20 Feb 2022";
12 | }
13 |
14 | getDescription() {
15 | return `
16 | For each byte in in the buffer, place a point on a line. Extend an arc
17 | from the previous point to the current point - alternating between above
18 | and below the horizon. The radius of the arc should be halfway the distance
19 | between the two points.
20 |
21 | This technique comes from an excellent Numberphile video on the
22 | Recamán sequence .
23 |
24 | I previously used this technique to draw knots using the values of the
25 | Collatz sequence , instead
26 | of random numbers from a hash as in this piece.
27 | `;
28 | }
29 |
30 | draw(ctx, { valuesBuffer }) {
31 | // Side length of the bounding box
32 | const s = Math.min(ctx.canvas.width, ctx.canvas.height);
33 |
34 | // Width of 3 based on `s`
35 | ctx.lineWidth = _(3, s);
36 |
37 | // Padding on either side
38 | const PADDING = 0.1 * s;
39 |
40 | for (let i = 1; i < valuesBuffer.length; i++) {
41 | const a = project(valuesBuffer[i - 1], 0, 255, PADDING, s - PADDING);
42 | const b = project(valuesBuffer[i], 0, 255, PADDING, s - PADDING);
43 | const midpoint = (a + b) / 2;
44 |
45 | ctx.beginPath();
46 |
47 | ctx.arc(
48 | // Compute the x-coordinate of the center
49 | ctx.canvas.width / 2 - s / 2 + midpoint,
50 |
51 | // y-coordinate is always halfway up
52 | ctx.canvas.height / 2,
53 |
54 | // radius is half the distance between `a` and `b`
55 | Math.abs(a - b) / 2,
56 |
57 | // We want the arc to be above the line, then below the line
58 | i % 2 ? 0 : Math.PI,
59 | i % 2 ? Math.PI : 2 * Math.PI
60 | );
61 |
62 | ctx.stroke();
63 | }
64 | }
65 | }
66 |
67 | exports.Knots = Knots;
68 |
--------------------------------------------------------------------------------
/art/circle.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 |
3 | class Circle extends Art {
4 | constructor() {
5 | super({
6 | x1: 2,
7 | r1: 2,
8 | x2: 2,
9 | r2: 2,
10 | x3: 2,
11 | r3: 2,
12 | x4: 2,
13 | r4: 2,
14 | });
15 | this.filename = "circle.js";
16 | this.created = "28 Mar 2021";
17 | }
18 |
19 | getDescription() {
20 | return `
21 | From the template we gather ${
22 | Object.keys(this.template).length / 2
23 | } circles and their radii and place
24 | them on the horizon. Intersections of these circles (if any) are labeled by drawing a chord between the two
25 | intersection points.
26 | `;
27 | }
28 |
29 | drawCircle(ctx, x, r) {
30 | const h = ctx.canvas.height;
31 | ctx.beginPath();
32 | ctx.arc(x, h / 2, r, 0, 2 * Math.PI);
33 | ctx.stroke();
34 | }
35 |
36 | drawIntersection(ctx, x1, r1, x2, r2) {
37 | const h = ctx.canvas.height;
38 | const d = x2 - x1;
39 |
40 | // https://mathworld.wolfram.com/Circle-CircleIntersection.html
41 | const intersectionOffset = (d * d - r2 * r2 + r1 * r1) / (2 * d);
42 | const y = Math.sqrt(
43 | (4 * d * d * r1 * r1 - Math.pow(d * d - r2 * r2 + r1 * r1, 2)) /
44 | (4 * d * d)
45 | );
46 |
47 | ctx.beginPath();
48 | ctx.moveTo(x1 + intersectionOffset, h / 2 - y);
49 | ctx.lineTo(x1 + intersectionOffset, h / 2 + y);
50 | ctx.stroke();
51 | }
52 |
53 | draw(ctx, { x1, r1, x2, r2, x3, r3, x4, r4 }) {
54 | const w = ctx.canvas.width;
55 | const h = ctx.canvas.height;
56 | ctx.lineWidth = 3;
57 |
58 | const xs = [x1, x2, x3, x4].map((x) => Math.round(x * w));
59 | const rs = [r1, r2, r3, r4].map((r) => r * 0.6 * w);
60 |
61 | xs.forEach((x, i) => {
62 | this.drawCircle(ctx, x, rs[i]);
63 | });
64 |
65 | // Pair up each circle to draw an intersection chord (if any)
66 | [
67 | [0, 1],
68 | [0, 2],
69 | [0, 3],
70 | [1, 2],
71 | [1, 3],
72 | [2, 3],
73 | ].forEach(([a, b]) => {
74 | this.drawIntersection(ctx, xs[a], rs[a], xs[b], rs[b]);
75 | });
76 | }
77 | }
78 |
79 | exports.Circle = Circle;
80 |
--------------------------------------------------------------------------------
/art/_base.js:
--------------------------------------------------------------------------------
1 | class Art {
2 | constructor(template) {
3 | if (!template) {
4 | throw "input template must be used";
5 | }
6 | this.template = template;
7 | }
8 |
9 | description(buffer) {
10 | let obj = {};
11 | let idx = 0;
12 |
13 | for (let [name, bytes] of Object.entries(this.template)) {
14 | const slice = buffer.slice(idx, idx + bytes);
15 | obj[name + "Buffer"] = slice;
16 | obj[name] = this.normalize(slice);
17 |
18 | idx += bytes;
19 | }
20 |
21 | return this.getDescription(obj);
22 | }
23 |
24 | getDescription() {
25 | return null;
26 | }
27 |
28 | templateEntries() {
29 | let usedBytes = Object.values(this.template).reduce((a, b) => a + b, 0);
30 | return Object.entries(this.template).concat(
31 | usedBytes < 32 ? [["unused", 32 - usedBytes]] : []
32 | );
33 | }
34 |
35 | explanation(buff) {
36 | let buffer = new Uint8Array(buff);
37 | let idx = 0;
38 | let segments = [];
39 |
40 | for (let [name, bytes] of this.templateEntries()) {
41 | let slice = buffer.slice(idx, idx + bytes);
42 | segments.push({
43 | name,
44 | bytes: Array.prototype.map
45 | .call(slice, (x) => ("00" + x.toString(16)).slice(-2))
46 | .join(""),
47 | normalized: this.normalize(slice),
48 | });
49 |
50 | idx += bytes;
51 | }
52 |
53 | return segments;
54 | }
55 |
56 | normalize(buffer) {
57 | if (buffer.byteLength === 0) {
58 | return 0;
59 | } else {
60 | return buffer[0] / 0x100 + this.normalize(buffer.slice(1)) / 0x100;
61 | }
62 | }
63 |
64 | render(ctx, buffer, props) {
65 | let idx = 0;
66 | let obj = {};
67 |
68 | for (let [name, bytes] of Object.entries(this.template)) {
69 | const slice = buffer.slice(idx, idx + bytes);
70 | obj[name + "Buffer"] = slice;
71 | obj[name] = this.normalize(slice);
72 |
73 | idx += bytes;
74 | }
75 |
76 | ctx.save();
77 | ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
78 | ctx.fillStyle = "rgba(255, 255, 255, 1)";
79 | ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
80 | this.draw(ctx, obj, props);
81 | ctx.restore();
82 | }
83 |
84 | draw() {
85 | throw "Child class must implement draw()";
86 | }
87 | }
88 |
89 | exports.Art = Art;
90 |
--------------------------------------------------------------------------------
/art/semicircle.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _ } = require("./util.js");
3 |
4 | class Semicircle extends Art {
5 | constructor() {
6 | super({
7 | first: 5,
8 | second: 5,
9 | third: 5,
10 | fourth: 5,
11 | });
12 | this.filename = "semicircle.js";
13 | this.created = "25 May 2021";
14 | }
15 |
16 | getDescription({ firstBuffer }) {
17 | return `
18 | From the template we form ${
19 | Object.keys(this.template).length
20 | } semicircles,
21 | each built by extracting an x, y, count, distance, direction tuple from
22 | the buffer.
23 |
24 | This piece was inspired by the amazing semicircles in Saskia Freeke's "Geometric Shapes"
25 | [link ].
26 | `;
27 | }
28 |
29 | drawSemicircle(ctx, buffer) {
30 | const w = ctx.canvas.width;
31 | const h = ctx.canvas.height;
32 |
33 | // x and y from [0.25*w, 0.75*w]
34 | const x = (buffer[0] / 256) * (w / 2) + w / 4;
35 | const y = (buffer[0] / 256) * (h / 2) + h / 4;
36 |
37 | // fixed radius of 200
38 | const radius = _(200, Math.min(w, h));
39 |
40 | // count from 2 to 21
41 | const count = Math.floor((20 * buffer[2]) / 256) + 2;
42 |
43 | const distance = Math.floor((40 * buffer[3]) / 256) + 4;
44 |
45 | // theta is 0, pi/2, pi, or 3pi/2
46 | const theta = ((buffer[4] % 4) * Math.PI) / 2;
47 |
48 | for (let i = 0; i < count; i++) {
49 | // Compute the center of the flat edge
50 | const x_ = x + i * distance * Math.cos(theta + Math.PI / 2);
51 | const y_ = y + i * distance * Math.sin(theta + Math.PI / 2);
52 |
53 | ctx.lineWidth = 3;
54 |
55 | ctx.beginPath();
56 |
57 | // flat side
58 | ctx.moveTo(x_ - radius * Math.cos(theta), y_ - radius * Math.sin(theta));
59 | ctx.lineTo(x_ + radius * Math.cos(theta), y_ + radius * Math.sin(theta));
60 |
61 | // curve
62 | ctx.arc(x_, y_, radius, theta, theta + Math.PI);
63 |
64 | ctx.stroke();
65 | }
66 | }
67 |
68 | draw(ctx, { firstBuffer, secondBuffer, thirdBuffer, fourthBuffer }) {
69 | [firstBuffer, secondBuffer, thirdBuffer, fourthBuffer].forEach((buff) => {
70 | this.drawSemicircle(ctx, buff);
71 | });
72 | }
73 | }
74 |
75 | exports.Semicircle = Semicircle;
76 |
--------------------------------------------------------------------------------
/art/quasiflake.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _ } = require("./util.js");
3 |
4 | class QuasiFlake extends Art {
5 | constructor() {
6 | super({
7 | fractures: 32,
8 | });
9 | this.hidden = true;
10 | this.filename = "quasiflake.js";
11 | this.created = "05 Jun 2021";
12 | }
13 |
14 | getDescription({ fracturesBuffer }) {
15 | return `
16 | Begin with three line segments forming an equilateral triangle. For each
17 | of the ${fracturesBuffer.length} bytes in fractures, grab
18 | the (byte % segments.length)th segment and split it into three sections
19 | of equal length. Remove the middle segment, replacing it with two segments of
20 | the same length arranged to form a mountain.
21 |
22 | If we repeated this process indefinitely on every segment (and not just those
23 | selected with fractures), we would form a
24 | Koch snowflake .
25 | `;
26 | }
27 |
28 | draw(ctx, { fracturesBuffer }) {
29 | let lines = [
30 | { theta: -Math.PI / 3, r: 1 },
31 | { theta: Math.PI, r: 1 },
32 | { theta: Math.PI / 3, r: 1 },
33 | ];
34 | fracturesBuffer.forEach((byte) => {
35 | const idx = byte % lines.length;
36 | const line = lines[idx];
37 |
38 | // ___ -> _/\_
39 | lines.splice(
40 | idx,
41 | 1,
42 | {
43 | theta: line.theta,
44 | r: line.r / 3,
45 | },
46 | {
47 | theta: line.theta + Math.PI / 3,
48 | r: line.r / 3,
49 | },
50 | {
51 | theta: line.theta - Math.PI / 3,
52 | r: line.r / 3,
53 | },
54 | {
55 | theta: line.theta,
56 | r: line.r / 3,
57 | }
58 | );
59 | });
60 |
61 | let w = ctx.canvas.width;
62 | let h = ctx.canvas.height;
63 |
64 | // Center as large a square as possible on the canvas
65 | ctx.lineWidth = _(5, w);
66 | let scale = 0.75 * Math.min(w, h);
67 |
68 | let x = w / 2;
69 | let y = h / 2 - (scale * Math.sqrt(3)) / 3;
70 |
71 | ctx.beginPath();
72 | ctx.moveTo(x, y);
73 |
74 | lines.forEach(({ theta, r }) => {
75 | x += r * scale * Math.cos(theta);
76 | y -= r * scale * Math.sin(theta);
77 | ctx.lineTo(x, y);
78 | });
79 |
80 | ctx.stroke();
81 | }
82 | }
83 |
84 | exports.QuasiFlake = QuasiFlake;
85 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import Image from "next/image";
3 | import Link from "next/link";
4 | import pieces from "../art/pieces.js";
5 |
6 | export default function Index() {
7 | return (
8 |
9 |
10 | index | hash.jordanscales.com
11 |
12 |
13 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
hash
27 |
28 |
29 |
30 | Art generated from{" "}
31 |
32 | SHA-256
33 | {" "}
34 | hashes. by{" "}
35 |
36 | jdan
37 |
38 | .
39 |
40 |
41 |
42 | Source available{" "}
43 |
44 | on GitHub
45 |
46 | .
47 |
48 | Browse the collection:
49 |
50 | {Object.keys(pieces)
51 | .filter((name) => !new pieces[name]().hidden)
52 | .reverse()
53 | .map((name) => (
54 |
55 | {new pieces[name]().created ? (
56 | <>
57 | {new pieces[name]().created}
58 | {" - "}
59 | >
60 | ) : null}
61 |
62 | {name}
63 |
64 |
65 | ))}
66 |
67 |
68 |
69 | This art was created for{" "}
70 |
71 | small e-ink displays
72 |
73 | , such as my setup below.
74 |
75 |
76 |
82 |
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/art/mario.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _ } = require("./util.js");
3 |
4 | class Mario extends Art {
5 | // requires SRC_ROOT/vendor/roms/mariobros.nes
6 | constructor() {
7 | super({
8 | inputs: 32,
9 | });
10 | this.hidden = true;
11 | this.filename = "mario.js";
12 | this.created = "18 Apr 2021";
13 | }
14 |
15 | drawBuffer(ctx, frameBuffer) {
16 | const WIDTH = 256;
17 | const HEIGHT = 240;
18 | const verticalPadding = 8;
19 | const horizontalPadding = 8;
20 |
21 | // https://github.com/bfirsh/jsnes-web/blob/master/src/Screen.js
22 | const imageData = ctx.getImageData(
23 | 0,
24 | 0,
25 | WIDTH - 2 * horizontalPadding,
26 | HEIGHT - 2 * verticalPadding
27 | );
28 | const buf = new ArrayBuffer(imageData.data.length);
29 | const buf32 = new Uint32Array(buf);
30 |
31 | // TODO: 10px border on either side
32 | for (let y = verticalPadding; y < HEIGHT - verticalPadding; ++y) {
33 | for (let x = horizontalPadding; x < WIDTH - horizontalPadding; ++x) {
34 | const nesPx = y * WIDTH + x;
35 | const bufPx =
36 | (y - verticalPadding) * (WIDTH - 2 * horizontalPadding) +
37 | (x - horizontalPadding);
38 | // Convert pixel from NES BGR to canvas ABGR
39 | buf32[bufPx] = 0xff000000 | frameBuffer[nesPx]; // Full alpha
40 | }
41 | }
42 |
43 | imageData.data.set(new Uint8ClampedArray(buf));
44 | ctx.putImageData(imageData, 0, 0);
45 |
46 | // turn off antialiasing
47 | ctx.imageSmoothingEnabled = false;
48 | const scale = Math.min(
49 | ctx.canvas.width / (WIDTH - 2 * horizontalPadding),
50 | ctx.canvas.height / (HEIGHT - 2 * verticalPadding)
51 | );
52 |
53 | ctx.scale(scale, scale);
54 | // TODO - center and draw some black/white bars
55 | ctx.drawImage(ctx.canvas, 0, 0);
56 | }
57 |
58 | buttonPress(nes, button, holdFrames = 1) {
59 | nes.buttonDown(1, button);
60 | for (let i = 0; i < holdFrames; i++) {
61 | nes.frame();
62 | }
63 | nes.buttonUp(1, button);
64 | nes.frame();
65 | }
66 |
67 | draw(ctx, { inputsBuffer }, { nes, getFrameBuffer }) {
68 | const BUTTON_A = 0;
69 | const BUTTON_B = 1;
70 | const BUTTON_UP = 4;
71 | const BUTTON_DOWN = 5;
72 | const BUTTON_LEFT = 6;
73 | const BUTTON_RIGHT = 7;
74 |
75 | inputsBuffer.forEach((v) => {
76 | const options = [
77 | //BUTTON_LEFT,
78 | BUTTON_UP,
79 | BUTTON_RIGHT,
80 | BUTTON_RIGHT,
81 | BUTTON_RIGHT,
82 | BUTTON_RIGHT,
83 | BUTTON_LEFT,
84 | BUTTON_A,
85 | BUTTON_A,
86 | BUTTON_B,
87 | ];
88 |
89 | const button = options[Math.floor((v / 256) * options.length)];
90 | // hold for 10 frames
91 | this.buttonPress(nes, button, 10);
92 | });
93 |
94 | this.drawBuffer(ctx, getFrameBuffer());
95 | }
96 | }
97 |
98 | exports.Mario = Mario;
99 |
--------------------------------------------------------------------------------
/art/sandpiles.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _, bigIntOfBuffer } = require("./util.js");
3 |
4 | class Sandpiles extends Art {
5 | constructor() {
6 | super({
7 | pile1: 3,
8 | pile2: 3,
9 | pile3: 3,
10 | pile4: 3,
11 | pile5: 3,
12 | pile6: 3,
13 | pile7: 3,
14 | pile8: 3,
15 | });
16 | this.filename = "sandpiles.js";
17 | this.created = "17 Jun 2021";
18 | }
19 |
20 | getDescription() {
21 | return `
22 | For each of the ${Object.keys(this.template).length} piles, read ${
23 | this.template.pile1
24 | } bytes as a triple (x, y, amount). At position (x, y), place
25 | amount grains. Then, for all cells in the grid, if there are
26 | 4 or more grains, spill one grain in each of the four cardinal directions. Repeat
27 | this process until no grains fall.
28 |
29 | This is a cellular automaton known as the Abelian
30 | sanpile model and it produces surprisingly interesting and complex patterns. When two piles interact, the result is
31 | mostly a jumbled mess.
32 | `;
33 | }
34 |
35 | getSize(ctx) {
36 | return _(30, ctx.canvas.width);
37 | }
38 |
39 | draw(ctx, props) {
40 | const s = this.getSize(ctx);
41 | const w = Math.ceil(ctx.canvas.width / s);
42 | const h = Math.ceil(ctx.canvas.height / s);
43 |
44 | let grid = [];
45 |
46 | for (let i = 0; i < Object.keys(this.template).length; i++) {
47 | const buf = props[`pile${i + 1}Buffer`];
48 |
49 | let x = Math.floor((buf[0] / 256) * w);
50 | let y = Math.floor((buf[1] / 256) * h);
51 |
52 | grid[[x, y]] = buf[2];
53 | }
54 |
55 | // avalanche
56 | let shouldPropagate = true;
57 | while (shouldPropagate) {
58 | shouldPropagate = false;
59 |
60 | Object.keys(grid).forEach((pos) => {
61 | const [x, y] = pos.split(",").map((i) => parseInt(i));
62 | if (x < 0 || y < 0 || x > w || y > h) {
63 | return;
64 | }
65 |
66 | if (grid[[x, y]] >= 4) {
67 | let spillover = Math.floor(grid[[x, y]] / 4);
68 | grid[[x + 1, y]] = (grid[[x + 1, y]] || 0) + spillover;
69 | grid[[x, y + 1]] = (grid[[x, y + 1]] || 0) + spillover;
70 | grid[[x - 1, y]] = (grid[[x - 1, y]] || 0) + spillover;
71 | grid[[x, y - 1]] = (grid[[x, y - 1]] || 0) + spillover;
72 | grid[[x, y]] = grid[[x, y]] % 4;
73 | shouldPropagate = true;
74 | }
75 | });
76 | }
77 |
78 | for (let x = 0; x < w; x++) {
79 | for (let y = 0; y < h; y++) {
80 | let value = +grid[[x, y]];
81 | if (!value) continue;
82 |
83 | if (value > 3) {
84 | value = 3;
85 | }
86 | let g = [255, 160, 90, 40][value];
87 | ctx.fillStyle = `rgb(${g}, ${g}, ${g})`;
88 | ctx.fillRect(x * s, y * s, s, s);
89 | }
90 | }
91 | }
92 | }
93 |
94 | exports.Sandpiles = Sandpiles;
95 |
--------------------------------------------------------------------------------
/art/walk.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _ } = require("./util.js");
3 |
4 | class Walk extends Art {
5 | constructor() {
6 | super({
7 | turns: 32,
8 | });
9 | this.filename = "walk.js";
10 | this.created = "26 Apr 2021";
11 | }
12 |
13 | getDescription() {
14 | return `
15 | We illustrate a random walk , starting at
16 | (0, 0) with a direction of "east." For each bit in the moves buffer (256 in total):
17 | we walk forward, then turn "left" if the bit is 1 and "right" if it is 0.
18 |
19 | There is no underlying pattern to the illustration since it is a random walk. This is in contrast
20 | to the elegance of one of my favorite patterns, the dragon curve .
21 | `;
22 | }
23 |
24 | draw(ctx, { turnsBuffer }) {
25 | const w = ctx.canvas.width;
26 | const h = ctx.canvas.height;
27 | const s = Math.min(w, h);
28 | const WALK_LENGTH = _(10, s);
29 | let dir = { dx: 1, dy: 0 };
30 | let pos = { x: 0, y: 0 };
31 | const path = [pos];
32 |
33 | const bits = Array.from(turnsBuffer)
34 | .map((byte) => {
35 | return byte
36 | .toString(2)
37 | .padStart(8, "0")
38 | .split("")
39 | .map((i) => parseInt(i));
40 | })
41 | .reduce((acc, bits) => acc.concat(bits));
42 |
43 | bits.forEach((bit) => {
44 | // TODO: get 8 turns from each one?
45 | pos = {
46 | x: pos.x + WALK_LENGTH * dir.dx,
47 | y: pos.y + WALK_LENGTH * dir.dy,
48 | };
49 | path.push(pos);
50 |
51 | if (dir.dx === 1) {
52 | dir = {
53 | dx: 0,
54 | dy: bit ? -1 : 1,
55 | };
56 | } else if (dir.dy === 1) {
57 | dir = {
58 | dx: bit ? 1 : -1,
59 | dy: 0,
60 | };
61 | } else if (dir.dx === -1) {
62 | dir = {
63 | dx: 0,
64 | dy: bit ? 1 : -1,
65 | };
66 | } else {
67 | dir = {
68 | dx: bit ? -1 : 1,
69 | dy: 0,
70 | };
71 | }
72 | });
73 |
74 | const minX = Math.min(...path.map(({ x }) => x));
75 | const maxX = Math.max(...path.map(({ x }) => x));
76 | const minY = Math.min(...path.map(({ y }) => y));
77 | const maxY = Math.max(...path.map(({ y }) => y));
78 | const maxSideLength = Math.max(maxX - minX, maxY - minY);
79 |
80 | const PADDING = _(120, s);
81 |
82 | ctx.lineWidth = _(6, s);
83 | ctx.beginPath();
84 | path.forEach(({ x, y }, idx) => {
85 | const x_ =
86 | ((x - minX) / maxSideLength) * (s - PADDING - PADDING) + PADDING;
87 | const y_ =
88 | ((y - minY) / maxSideLength) * (s - PADDING - PADDING) + PADDING;
89 |
90 | if (idx === 0) {
91 | ctx.moveTo(x_, y_);
92 | } else {
93 | ctx.lineTo(x_, y_);
94 | }
95 | });
96 | ctx.stroke();
97 | }
98 | }
99 |
100 | exports.Walk = Walk;
101 |
--------------------------------------------------------------------------------
/art/nes.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _ } = require("./util.js");
3 |
4 | class Nes extends Art {
5 | // requires SRC_ROOT/vendor/roms/**/*.nes
6 | constructor() {
7 | super({
8 | rom: 12,
9 | });
10 | this.hidden = true;
11 | this.filename = "nes.js";
12 | this.created = "1 Mar 2022";
13 | }
14 |
15 | drawBuffer(ctx, secondCtx, frameBuffer) {
16 | const WIDTH = 256;
17 | const HEIGHT = 240;
18 | const verticalPadding = 8;
19 | const horizontalPadding = 8;
20 |
21 | const w = WIDTH - 2 * horizontalPadding;
22 | const h = HEIGHT - 2 * verticalPadding;
23 |
24 | // https://github.com/bfirsh/jsnes-web/blob/master/src/Screen.js
25 | const imageData = secondCtx.getImageData(0, 0, w, h);
26 |
27 | const buf = new ArrayBuffer(imageData.data.length);
28 | const buf32 = new Uint32Array(buf);
29 |
30 | // TODO: 10px border on either side
31 | for (let y = verticalPadding; y < HEIGHT - verticalPadding; ++y) {
32 | for (let x = horizontalPadding; x < WIDTH - horizontalPadding; ++x) {
33 | const nesPx = y * WIDTH + x;
34 | const bufPx = (y - verticalPadding) * w + (x - horizontalPadding);
35 | // Convert pixel from NES BGR to canvas ABGR
36 | buf32[bufPx] = 0xff000000 | frameBuffer[nesPx]; // Full alpha
37 | }
38 | }
39 |
40 | imageData.data.set(new Uint8ClampedArray(buf));
41 | secondCtx.putImageData(imageData, 0, 0);
42 |
43 | const scale = Math.min(
44 | Math.floor(ctx.canvas.width / w),
45 | Math.floor(ctx.canvas.height / h)
46 | );
47 |
48 | ctx.scale(scale);
49 |
50 | ctx.drawImage(
51 | secondCtx.canvas,
52 | 0,
53 | 0,
54 | w,
55 | h,
56 | Math.floor(ctx.canvas.width / 2 - (w * scale) / 2),
57 | Math.floor(ctx.canvas.height / 2 - (h * scale) / 2),
58 | w * scale,
59 | h * scale
60 | );
61 | }
62 |
63 | draw(ctx, { rom }, { nes, getFrameBuffer, roms, fs, path, secondCtx }) {
64 | let romPath = roms[Math.floor(rom * roms.length)];
65 | let romData = fs.readFileSync(romPath, {
66 | encoding: "binary",
67 | });
68 |
69 | let loaded = false;
70 |
71 | while (!loaded) {
72 | try {
73 | nes.loadROM(romData);
74 | loaded = true;
75 | } catch (e) {
76 | // random :(
77 | romPath = roms[Math.floor(Math.random() * roms.length)];
78 | romData = fs.readFileSync(romPath, {
79 | encoding: "binary",
80 | });
81 | }
82 | }
83 |
84 | // Wait 200 frames
85 | for (let i = 0; i < 500; i++) {
86 | try {
87 | nes.frame();
88 | } catch (e) {
89 | continue;
90 | }
91 | }
92 |
93 | this.drawBuffer(ctx, secondCtx, getFrameBuffer());
94 | ctx.font = `bold ${_(30, ctx.canvas.width)}px monospace`;
95 | ctx.textAlign = "center";
96 | ctx.fillStyle = "rgb(0, 0, 0)";
97 | ctx.fillText(
98 | path.parse(romPath).name,
99 | ctx.canvas.width / 2,
100 | ctx.canvas.height - 40
101 | );
102 | }
103 | }
104 |
105 | exports.Nes = Nes;
106 |
--------------------------------------------------------------------------------
/art/julia.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _, project } = require("./util.js");
3 |
4 | class Julia extends Art {
5 | constructor() {
6 | super({
7 | real: 16,
8 | imaginary: 16,
9 | });
10 | // not very interesting
11 | this.hidden = true;
12 | this.filename = "julia.js";
13 | this.created = "10 Oct 2023";
14 | }
15 |
16 | getDescription({ real, imaginary }) {
17 | return `
18 | z = z^2 + ${this.getReal(real)} + ${this.getImaginary(imaginary)}i
19 | `;
20 | }
21 |
22 | getReal(real) {
23 | return project(real, 0, 1, -1, 1);
24 | }
25 |
26 | getImaginary(imaginary) {
27 | return project(imaginary, 0, 1, -1, 1);
28 | }
29 |
30 | draw(ctx, { real, imaginary }) {
31 | const cr = this.getReal(real);
32 | const ci = this.getImaginary(imaginary);
33 |
34 | const w = ctx.canvas.width;
35 | const h = ctx.canvas.height;
36 |
37 | const s = _(8, w);
38 | const ITERATIONS = 20;
39 |
40 | const aspectRatio = w / h;
41 |
42 | for (let x = 0; x < w; x += s) {
43 | for (let y = 0; y < h; y += s) {
44 | const im = project(y, 0, h, -1, 1);
45 | const re = project(x, 0, w, -1 * aspectRatio, 1 * aspectRatio);
46 |
47 | // TODO: after this works, a "flow" would be more interesting
48 | let zr = re;
49 | let zi = im;
50 | for (let i = 0; i < ITERATIONS; i++) {
51 | // z = z^2 + c
52 | const zr2 = zr * zr - zi * zi;
53 | const zi2 = 2 * zr * zi;
54 |
55 | zr = zr2 + cr;
56 | zi = zi2 + ci;
57 | }
58 |
59 | let shade = project(zr * zr + zi * zi, 0, 4, 0, 255);
60 | if (Number.isNaN(shade)) {
61 | shade = 255;
62 | }
63 |
64 | if (shade < 0) {
65 | shade = 0;
66 | }
67 | if (shade > 255) {
68 | shade = 255;
69 | }
70 |
71 | ctx.fillStyle = `rgb(${shade}, ${shade}, ${shade})`;
72 | ctx.fillRect(x, y, s, s);
73 | }
74 | }
75 |
76 | const s2 = _(32, w);
77 | for (let x = 0; x < w; x += s2) {
78 | for (let y = 0; y < h; y += s2) {
79 | const re = project(x, 0, w, -1 * aspectRatio, 1 * aspectRatio);
80 | const im = project(y, 0, h, -1, 1);
81 |
82 | // Get the next coordinate
83 | const zr2 = re * re - im * im;
84 | const zi2 = 2 * re * im;
85 | const re2 = zr2 + cr;
86 | const im2 = zi2 + ci;
87 |
88 | const dy = im2 - im;
89 | const dx = re2 - re;
90 | const r = project(Math.sqrt(dx * dx + dy * dy), 0, 2, 0, s2 / 2);
91 | const theta = Math.atan2(dy, dx);
92 |
93 | // draw a line of length s, centered as (x + s/2, y + s/2), with angle theta
94 | ctx.strokeStyle = "rgb(0, 0, 0)";
95 | ctx.beginPath();
96 | ctx.moveTo(x + s2 / 2, y + s2 / 2);
97 | ctx.lineTo(
98 | x + s2 / 2 + r * Math.cos(theta),
99 | y + s2 / 2 + r * Math.sin(theta)
100 | );
101 | ctx.stroke();
102 |
103 | // let shade = project(zr * zr + zi * zi, 0, 4, 0, 255);
104 | // if (Number.isNaN(shade)) {
105 | // shade = 255;
106 | // }
107 |
108 | // if (shade < 0) {
109 | // shade = 0;
110 | // }
111 | // if (shade > 255) {
112 | // shade = 255;
113 | // }
114 |
115 | // ctx.fillStyle = `rgb(${shade}, ${shade}, ${shade})`;
116 | // ctx.fillRect(x, y, s, s);
117 | }
118 | }
119 | }
120 | }
121 |
122 | exports.Julia = Julia;
123 |
--------------------------------------------------------------------------------
/art/collatz.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _ } = require("./util.js");
3 |
4 | class Collatz extends Art {
5 | constructor() {
6 | super({
7 | input: 8,
8 | });
9 | this.filename = "collatz.js";
10 | this.created = "14 Apr 2021";
11 | }
12 |
13 | getDescription() {
14 | return `
15 | We convert input to a number, and use it as the first number in a
16 | Collatz sequence
17 | (i.e. even n => n / 2, odd n => 3n+1) until the number reaches 1.
18 |
19 | Each iteration is drawn as a bit string where 1s are filled and 0s are empty, continuing to the next line and
20 | wrapping back to the top when necessary.
21 |
22 | It is unknown if the Collatz sequence reaches 1 for every input, but we know that
23 | generalized Collatz sequences
24 | can be used to perform arbitrary computations and their behavior is therefore undecidable.
25 | `;
26 | }
27 |
28 | bitSize(ctx) {
29 | return _(8, ctx.canvas.height);
30 | }
31 |
32 | bufferToBitString(buff) {
33 | let arr = [];
34 | for (let i = 0; i < buff.byteLength; i++) {
35 | arr = arr.concat(
36 | buff[i]
37 | .toString(2)
38 | .padStart(8, "0")
39 | .split("")
40 | .map((s) => parseInt(s))
41 | );
42 | }
43 | return arr;
44 | }
45 |
46 | halfBitString(bitString) {
47 | return bitString.slice(1);
48 | }
49 |
50 | addBitString(a, b) {
51 | const result = [];
52 | const maxLength = Math.max(a.length, b.length);
53 | let carry = 0;
54 | for (let i = 0; i < maxLength; i++) {
55 | const a_ = a[i] || 0;
56 | const b_ = b[i] || 0;
57 | const sum = a_ + b_ + carry;
58 | result.push(sum % 2);
59 | carry = sum > 1 ? 1 : 0;
60 | }
61 | if (carry) {
62 | result.push(carry);
63 | }
64 | return result;
65 | }
66 |
67 | triplePlusOneBitString(bitString) {
68 | const doubleBitString = [0].concat(bitString);
69 | return this.addBitString(
70 | // 3n
71 | this.addBitString(bitString, doubleBitString),
72 | // +1
73 | [1]
74 | );
75 | }
76 |
77 | drawBitString(ctx, x, y, bitString) {
78 | const bs = this.bitSize(ctx);
79 | for (let i = 0; i < bitString.length; i++) {
80 | if (bitString[i]) {
81 | ctx.beginPath();
82 | ctx.rect(x + i * bs, y, bs, bs);
83 | ctx.fill();
84 | }
85 | }
86 | }
87 |
88 | draw(ctx, { inputBuffer }) {
89 | const bs = this.bitSize(ctx);
90 | let x = 0;
91 | let y = 0;
92 | let current = this.bufferToBitString(inputBuffer);
93 |
94 | let maxWidthOfColumn = 0;
95 |
96 | ctx.fillStyle = "rgb(0, 0, 0)";
97 |
98 | while (current.length > 0 && x * bs <= ctx.canvas.width) {
99 | maxWidthOfColumn = Math.max(maxWidthOfColumn, current.length);
100 | this.drawBitString(ctx, x * bs, y * bs, current);
101 |
102 | // Make sure we draw the `1` bit :)
103 | if (current.length == 1) {
104 | break;
105 | }
106 |
107 | current = current[0]
108 | ? this.triplePlusOneBitString(current)
109 | : this.halfBitString(current);
110 | y++;
111 |
112 | const GAP = 2;
113 | if (y * bs >= ctx.canvas.height) {
114 | y = 0;
115 | x += maxWidthOfColumn + GAP;
116 | maxWidthOfColumn = 0;
117 | }
118 | }
119 | }
120 | }
121 |
122 | exports.Collatz = Collatz;
123 |
--------------------------------------------------------------------------------
/art/chess.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _ } = require("./util.js");
3 | const { chessPieces } = require("./chess-pieces");
4 |
5 | let ChessGame = require("chess.js");
6 | if (ChessGame.Chess) {
7 | ChessGame = ChessGame.Chess;
8 | }
9 |
10 | function getGame(movesBuffer) {
11 | const game = new ChessGame();
12 |
13 | for (let i = 0; i < movesBuffer.byteLength; i++) {
14 | let moves = game.moves();
15 | game.move(moves[Math.floor((movesBuffer[i] / 256) * moves.length)]);
16 | }
17 |
18 | return game;
19 | }
20 |
21 | function getPgn(movesBuffer) {
22 | return getGame(movesBuffer).pgn({ newline_char: " " });
23 | }
24 |
25 | class Chess extends Art {
26 | constructor() {
27 | super({
28 | moves: 16,
29 | });
30 | this.filename = "chess.js";
31 | this.created = "24 Apr 2021";
32 | }
33 |
34 | getDescription({ movesBuffer }) {
35 | return `
36 | We use the excellent chess.js library
37 | to simulate a chess game where each move is generated from the moves buffer. That is,
38 | for each byte in moves, we cycle through the list of all possible moves
39 | and choose the one at the index specified by the byte (modulo the length of the list of moves).
40 |
41 | Even though each player only plays 8 moves, they're probably not very good moves so
42 | you're likely to generate a never-seen-before game.
43 |
44 | If you'd like to analyze or continue the game above, you can do so by heading to
45 | lichess.org/paste and entering the following PGN:
46 |
47 | ${getPgn(movesBuffer)}
48 | `;
49 | }
50 |
51 | squareToDrawFn(square) {
52 | if (!square) {
53 | return () => {};
54 | }
55 |
56 | const filename = `${square.color}${square.type.toUpperCase()}.svg`;
57 | return chessPieces[filename].draw;
58 | }
59 |
60 | // Unused
61 | squareToAscii(square) {
62 | return square
63 | ? {
64 | k: "♔♚",
65 | q: "♕♛",
66 | r: "♖♜",
67 | b: "♗♝",
68 | n: "♘♞",
69 | p: "♙♟",
70 | }[square.type][square.color === "w" ? 0 : 1]
71 | : "";
72 | }
73 |
74 | draw(ctx, { movesBuffer }) {
75 | const w = ctx.canvas.width;
76 | const h = ctx.canvas.height;
77 |
78 | const game = getGame(movesBuffer);
79 |
80 | const squareSize = _(140, Math.min(w, h));
81 | ctx.font = `${squareSize}px monospace`;
82 | ctx.fillStyle = "rgb(0, 0, 0)";
83 |
84 | const leftPadding = w / 2 - (8 * squareSize) / 2;
85 | const topPadding = h / 2 - (8 * squareSize) / 2;
86 |
87 | const board = game.board();
88 | for (let r = 0; r < 8; r++) {
89 | for (let c = 0; c < 8; c++) {
90 | const x = leftPadding + c * squareSize;
91 | const y = topPadding + r * squareSize;
92 |
93 | if (r % 2 !== c % 2) {
94 | ctx.save();
95 | ctx.fillStyle = "rgb(180, 180, 180)";
96 | ctx.fillRect(x, y, squareSize, squareSize);
97 | ctx.restore();
98 | }
99 |
100 | const SVG_PIECE_SIZE = 45;
101 | const drawPiece = this.squareToDrawFn(board[r][c]);
102 | ctx.save();
103 | ctx.transform(
104 | squareSize / SVG_PIECE_SIZE,
105 | 0,
106 | 0,
107 | squareSize / SVG_PIECE_SIZE,
108 | x,
109 | y
110 | );
111 | drawPiece(ctx);
112 | ctx.restore();
113 | }
114 | }
115 | }
116 | }
117 |
118 | exports.Chess = Chess;
119 | exports.getPgn = getPgn;
120 |
--------------------------------------------------------------------------------
/art/automata.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _, project } = require("./util.js");
3 |
4 | class Automata extends Art {
5 | constructor() {
6 | super({
7 | rule: 1,
8 | seed: 4,
9 | size: 2,
10 | });
11 | this.filename = "automata.js";
12 | this.created = "26 Feb 2022";
13 | }
14 |
15 | getDescription({ rule, seedBuffer }) {
16 | return `
17 | Begin with an empty row of 0's and populate the center
18 | with a bitstring taken from seed.
19 |
20 | Build a table
21 | by converting rule to binary, and pairing each bit with a
22 | number starting from 7 and decreasing to 0. For our above seed, rule ${
23 | rule * 256
24 | } will generate the following table:
25 |
26 |
27 |
28 | seq
29 | ${this.ruleToLookup(rule * 256)
30 | .map(([seq, _]) => `${seq} `)
31 | .join("\n")}
32 |
33 |
34 | val
35 | ${this.ruleToLookup(rule * 256)
36 | .map(([_, val]) => `${val} `)
37 | .join("\n")}
38 |
39 |
40 |
41 | For each bit in the row, generate a three-bit string using the bit to
42 | its left, the bit itself, and the bit to its right. Look up this value
43 | in the table above, and assign val as the new bit.
44 |
45 | Finally draw the resulting row on the next line of the canvas.
46 | `;
47 | }
48 |
49 | ruleToLookup(rule) {
50 | const bits = Math.floor(rule).toString(2).padStart(8, "0");
51 | return ["111", "110", "101", "100", "011", "010", "001", "000"].map(
52 | (seq, idx) => [seq, bits[idx]]
53 | );
54 | }
55 |
56 | nextRow(row, lookup) {
57 | const nextRow = row.slice();
58 | for (let i = 0; i < row.length; i++) {
59 | const seq = [row[i - 1] || 0, row[i] || 0, row[i + 1] || 0].join("");
60 | nextRow[i] = lookup[seq];
61 | }
62 | return nextRow;
63 | }
64 |
65 | drawRow(ctx, row, bitSize, y) {
66 | const widthInBits = Math.floor(ctx.canvas.width / bitSize);
67 | const start = Math.floor(row.length / 2 - widthInBits / 2);
68 | const end = Math.floor(row.length / 2 + widthInBits / 2);
69 |
70 | for (let i = start; i < end; i++) {
71 | const x = (i - start) * bitSize;
72 | if (row[i] === "1") {
73 | ctx.fillStyle = `rgb(0, 0, 0)`;
74 | ctx.beginPath();
75 | ctx.rect(x, y, bitSize, bitSize);
76 | ctx.fill();
77 | }
78 | }
79 | }
80 |
81 | bufferToBinary(buffer) {
82 | let base10 = buffer.reduce((acc, d) => acc * 256 + d, 1);
83 | return base10
84 | .toString(2)
85 | .split("")
86 | .map((d) => parseInt(d));
87 | }
88 |
89 | draw(ctx, { size, rule, seedBuffer }) {
90 | const w = ctx.canvas.width;
91 | const h = ctx.canvas.height;
92 |
93 | // 12px bits at 1200px looks decent
94 | const bitSize = _(project(size, 0, 1, 3, 12), w);
95 |
96 | // Initialize an array large enough to handle all possible growth
97 | let row = Array.from({
98 | length: Math.floor((w + h + h) / bitSize),
99 | }).map((_) => 0);
100 |
101 | // Place `seed` on the tape
102 | const input = this.bufferToBinary(seedBuffer);
103 | for (let i = 0; i < input.length; i++) {
104 | const idx = Math.floor(row.length / 2 - input.length / 2) + i;
105 | row[idx] = input[i];
106 | }
107 |
108 | const lookup = {};
109 | this.ruleToLookup(rule * 256).forEach(([seq, val]) => {
110 | lookup[seq] = val;
111 | });
112 |
113 | for (let i = 0; i < Math.floor(h / bitSize) + 1; i++) {
114 | row = this.nextRow(row, lookup);
115 | this.drawRow(ctx, row, bitSize, i * bitSize);
116 | }
117 | }
118 | }
119 |
120 | exports.Automata = Automata;
121 |
--------------------------------------------------------------------------------
/art/three-body.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _, project } = require("./util.js");
3 |
4 | const G = 0.0000001;
5 |
6 | class ThreeBody extends Art {
7 | constructor() {
8 | super({
9 | m1: 4,
10 | x1: 3,
11 | y1: 3,
12 | m2: 4,
13 | x2: 3,
14 | y2: 3,
15 | m3: 4,
16 | x3: 3,
17 | y3: 3,
18 | });
19 | this.filename = "three-body.js";
20 | this.created = "03 Feb 2024";
21 | }
22 |
23 | getDescription() {
24 | return `
25 | Place three bodies in space. Each body is defined by 10 bytes. The first 4 control
26 | its mass. The next 3 control its starting x position (0, 1). The next 3 control its
27 | starting y position (0, 1). All bodies start at rest.
28 |
29 | Then, simulate the gravitational forces between the bodies. We run the simulating 1000
30 | and use a value of ${G} for the gravitational constant.
31 | `;
32 | }
33 |
34 | arc(ctx, x, y, r) {
35 | const w = ctx.canvas.width;
36 | const h = ctx.canvas.height;
37 | const s = Math.min(w, h);
38 |
39 | const px = project(x, 0, 1, w / 2 - s / 2, w / 2 + s / 2);
40 | const py = project(y, 0, 1, h / 2 - s / 2, h / 2 + s / 2);
41 |
42 | ctx.arc(px, py, r, 0, 2 * Math.PI);
43 | }
44 |
45 | draw(ctx, { m1, x1, y1, m2, x2, y2, m3, x3, y3 }) {
46 | let body1 = { x: x1, y: y1, vx: 0, vy: 0 };
47 | let body2 = { x: x2, y: y2, vx: 0, vy: 0 };
48 | let body3 = { x: x3, y: y3, vx: 0, vy: 0 };
49 |
50 | ctx.fillStyle = "rgb(0, 0, 0)";
51 | ctx.strokeStyle = "rgb(0, 0, 0)";
52 |
53 | const FRAMES = 1000;
54 | for (let i = 0; i < FRAMES; i++) {
55 | // graviational force between m1 and m2
56 | let d12 = Math.sqrt((body1.x - body2.x) ** 2 + (body1.y - body2.y) ** 2);
57 | let f12 = (G * m1 * m2) / d12 ** 2;
58 | let theta12 = Math.atan2(body2.y - body1.y, body2.x - body1.x);
59 |
60 | // graviational force between m1 and m3
61 | let d13 = Math.sqrt((body1.x - body3.x) ** 2 + (body1.y - body3.y) ** 2);
62 | let f13 = (G * m1 * m3) / d13 ** 2;
63 | let theta13 = Math.atan2(body3.y - body1.y, body3.x - body1.x);
64 |
65 | // graviational force between m2 and m3
66 | let d23 = Math.sqrt((body2.x - body3.x) ** 2 + (body2.y - body3.y) ** 2);
67 | let f23 = (G * m2 * m3) / d23 ** 2;
68 | let theta23 = Math.atan2(body3.y - body2.y, body3.x - body2.x);
69 |
70 | // Update position of m1
71 | let ax1 = (f12 * Math.cos(theta12) + f13 * Math.cos(theta13)) / m1;
72 | let ay1 = (f12 * Math.sin(theta12) + f13 * Math.sin(theta13)) / m1;
73 | body1.vx += ax1;
74 | body1.vy += ay1;
75 | body1.x += body1.vx;
76 | body1.y += body1.vy;
77 |
78 | // Update position of m2
79 | let ax2 = (-f12 * Math.cos(theta12) + f23 * Math.cos(theta23)) / m2;
80 | let ay2 = (-f12 * Math.sin(theta12) + f23 * Math.sin(theta23)) / m2;
81 | body2.vx += ax2;
82 | body2.vy += ay2;
83 | body2.x += body2.vx;
84 | body2.y += body2.vy;
85 |
86 | // Update position of m3
87 | let ax3 = (-f13 * Math.cos(theta13) - f23 * Math.cos(theta23)) / m3;
88 | let ay3 = (-f13 * Math.sin(theta13) - f23 * Math.sin(theta23)) / m3;
89 | body3.vx += ax3;
90 | body3.vy += ay3;
91 | body3.x += body3.vx;
92 | body3.y += body3.vy;
93 |
94 | ctx.beginPath();
95 | this.arc(ctx, body1.x, body1.y, 1);
96 | ctx.fill();
97 |
98 | ctx.beginPath();
99 | this.arc(ctx, body2.x, body2.y, 1);
100 | ctx.fill();
101 |
102 | ctx.beginPath();
103 | this.arc(ctx, body3.x, body3.y, 1);
104 | ctx.fill();
105 | }
106 |
107 | ctx.beginPath();
108 | this.arc(ctx, body1.x, body1.y, 10);
109 | ctx.stroke();
110 |
111 | ctx.beginPath();
112 | this.arc(ctx, body2.x, body2.y, 10);
113 | ctx.stroke();
114 |
115 | ctx.beginPath();
116 | this.arc(ctx, body3.x, body3.y, 10);
117 | ctx.stroke();
118 | }
119 | }
120 |
121 | exports.ThreeBody = ThreeBody;
122 |
--------------------------------------------------------------------------------
/art/boxes.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _ } = require("./util.js");
3 |
4 | class Boxes extends Art {
5 | constructor() {
6 | super({
7 | box1: 6,
8 | box2: 6,
9 | box3: 6,
10 | box4: 6,
11 | });
12 |
13 | this.filename = "boxes.js";
14 | this.created = "29 Mar 2021";
15 | }
16 |
17 | getDescription() {
18 | return `
19 | From the template we gather dimensions for ${
20 | Object.keys(this.template).length
21 | } boxes and place them
22 | next to each other on the ground.
23 |
24 | Each box is represented as six bytes, which is divided into three sections
25 | for width, depth, and height respectively.
26 |
27 | The boxes are rendered isometrically with a front-facing light source.
28 | `;
29 | }
30 |
31 | coordToIso(x, y, z) {
32 | return [x + 0.4 * z, y - 0.2 * z];
33 | }
34 |
35 | drawBox(ctx, { x, y, w, d, h }) {
36 | const cols = {
37 | front: "rgb(240, 240, 240)",
38 | top: "rgb(180, 180, 180)",
39 | side: "rgb(120, 120, 120)",
40 | };
41 |
42 | // front
43 | ctx.fillStyle = cols.front;
44 | ctx.beginPath();
45 | ctx.moveTo(...this.coordToIso(x, y, -d));
46 | ctx.lineTo(...this.coordToIso(x, y - h, -d));
47 | ctx.lineTo(...this.coordToIso(x + w, y - h, -d));
48 | ctx.lineTo(...this.coordToIso(x + w, y, -d));
49 | ctx.lineTo(...this.coordToIso(x, y, -d));
50 | ctx.fill();
51 | ctx.stroke();
52 |
53 | // top
54 | ctx.fillStyle = cols.top;
55 | ctx.beginPath();
56 | ctx.moveTo(...this.coordToIso(x, y - h, -d));
57 | ctx.lineTo(...this.coordToIso(x, y - h, 0));
58 | ctx.lineTo(...this.coordToIso(x + w, y - h, 0));
59 | ctx.lineTo(...this.coordToIso(x + w, y - h, -d));
60 | ctx.lineTo(...this.coordToIso(x, y - h, -d));
61 | ctx.fill();
62 | ctx.stroke();
63 |
64 | // side
65 | ctx.fillStyle = cols.side;
66 | ctx.beginPath();
67 | ctx.moveTo(...this.coordToIso(x + w, y, -d));
68 | ctx.lineTo(...this.coordToIso(x + w, y - h, -d));
69 | ctx.lineTo(...this.coordToIso(x + w, y - h, 0));
70 | ctx.lineTo(...this.coordToIso(x + w, y, 0));
71 | ctx.lineTo(...this.coordToIso(x + w, y, -d));
72 | ctx.fill();
73 | ctx.stroke();
74 | }
75 |
76 | draw(ctx, { box1Buffer, box2Buffer, box3Buffer, box4Buffer }) {
77 | const w = ctx.canvas.width;
78 | const h = ctx.canvas.height;
79 | const x = 0.25 * w;
80 | const y = 0.7 * h;
81 |
82 | const wMin = _(20, w);
83 | const dScale = _(400, w);
84 | const wScale = _(300, w);
85 | const hScale = _(600, h);
86 |
87 | const w1 = (256 * box1Buffer[0] + box1Buffer[1]) / 65536;
88 | const d1 = (256 * box1Buffer[2] + box1Buffer[3]) / 65536;
89 | const h1 = (256 * box1Buffer[4] + box1Buffer[5]) / 65536;
90 |
91 | const w2 = (256 * box2Buffer[0] + box2Buffer[1]) / 65536;
92 | const d2 = (256 * box2Buffer[2] + box2Buffer[3]) / 65536;
93 | const h2 = (256 * box2Buffer[4] + box2Buffer[5]) / 65536;
94 |
95 | const w3 = (256 * box3Buffer[0] + box3Buffer[1]) / 65536;
96 | const d3 = (256 * box3Buffer[2] + box3Buffer[3]) / 65536;
97 | const h3 = (256 * box3Buffer[4] + box3Buffer[5]) / 65536;
98 |
99 | const w4 = (256 * box4Buffer[0] + box4Buffer[1]) / 65536;
100 | const d4 = (256 * box4Buffer[2] + box4Buffer[3]) / 65536;
101 | const h4 = (256 * box4Buffer[4] + box4Buffer[5]) / 65536;
102 |
103 | this.drawBox(ctx, {
104 | x,
105 | y,
106 | d: d1 * dScale,
107 | w: w1 * wScale + wMin,
108 | h: h1 * hScale,
109 | });
110 |
111 | this.drawBox(ctx, {
112 | x: x + w1 * wScale + wMin,
113 | y,
114 | d: d2 * dScale,
115 | w: w2 * wScale + wMin,
116 | h: h2 * hScale,
117 | });
118 |
119 | this.drawBox(ctx, {
120 | x: x + w1 * wScale + wMin + w2 * wScale + wMin,
121 | y,
122 | d: d3 * dScale,
123 | w: w3 * wScale + wMin,
124 | h: h3 * hScale,
125 | });
126 |
127 | this.drawBox(ctx, {
128 | x: x + w1 * wScale + wMin + w2 * wScale + wMin + w3 * wScale + wMin,
129 | y,
130 | d: d4 * dScale,
131 | w: w4 * wScale + wMin,
132 | h: h4 * hScale,
133 | });
134 | }
135 | }
136 |
137 | exports.Boxes = Boxes;
138 |
--------------------------------------------------------------------------------
/pages/[piece]/[seed].js:
--------------------------------------------------------------------------------
1 | import crypto from "crypto";
2 | import Head from "next/head";
3 | import Link from "next/link";
4 | import { useRouter } from "next/router";
5 | import { useEffect, useRef } from "react";
6 | import classnames from "classnames";
7 | import debounce from "lodash.debounce";
8 |
9 | import pieces from "../../art/pieces.js";
10 | import styles from "./art.module.css";
11 |
12 | function Hash({ parts }) {
13 | return (
14 |
15 | {parts.map(({ name, bytes, normalized }) => (
16 |
23 |
{name}
24 |
{bytes}
25 |
26 | ))}
27 |
28 | );
29 | }
30 |
31 | function Art({ piece, seed, hashString }) {
32 | const router = useRouter();
33 | const art = new pieces[piece]();
34 | const hash = new Uint8Array(Buffer.from(hashString, "hex"));
35 |
36 | const canvasEl = useRef(null);
37 | useEffect(() => {
38 | if (!canvasEl || !hash) return;
39 | let ctx = canvasEl.current.getContext("2d");
40 | art.render(ctx, hash);
41 | }, [canvasEl, art, hashString]);
42 |
43 | function handleChange(e) {
44 | if (/[^\.\s]/.test(e.target.value)) {
45 | router.replace(`/${piece}/${encodeURIComponent(e.target.value)}`);
46 | }
47 | }
48 |
49 | return (
50 |
51 |
52 | {piece} | hash.jordanscales.com
53 |
54 |
58 |
59 |
65 |
66 |
67 |
68 |
69 |
70 |
{piece}
71 |
72 |
73 |
home
74 |
75 | {art.filename ? (
76 | <>
77 | {" . "}
78 |
81 |
github
82 |
83 | >
84 | ) : null}
85 |
86 |
87 |
88 |
89 |
90 |
91 | seed
92 |
93 |
99 | {art.debounce ? " (debounced for performance)" : null}
100 |
101 |
102 |
103 |
104 | {hash ? : null}
105 |
106 |
107 |
113 |
114 |
138 |
139 | );
140 | }
141 |
142 | export default Art;
143 |
144 | export async function getServerSideProps(context) {
145 | const { piece, seed } = context.params;
146 | const shaSum = crypto.createHash("sha256");
147 | shaSum.update(seed);
148 | const buffer = shaSum.digest();
149 |
150 | return {
151 | props: {
152 | piece,
153 | seed,
154 | hashString: buffer.toString("hex"),
155 | },
156 | };
157 | }
158 |
--------------------------------------------------------------------------------
/art/fraction.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _, bigIntOfBuffer } = require("./util.js");
3 |
4 | class Fraction extends Art {
5 | constructor() {
6 | super({
7 | a: 4,
8 | b: 4,
9 | });
10 | this.filename = "fraction.js";
11 | this.created = "29 Apr 2021";
12 | }
13 |
14 | getDescription() {
15 | return `
16 | Convert a and b into integers and form them into a proper fraction
17 | (a fraction whose numerator is less than the denominator).
18 |
19 | We then show the process of turning this fraction into a sum of unit fractions. This is commonly
20 | referred to as an Egyptian fraction .
21 |
22 | In particular, our algorithm for doing so is a
23 | greedy one .
24 | The numbers grow fast very quickly.
25 | `;
26 | }
27 |
28 | drawFraction(ctx, fontSize, lineHeight, numer, denom, x, y, maxWidth) {
29 | // If we've halted iteration or run into an error
30 | if (denom === "...") {
31 | ctx.fillText("...", x, y + lineHeight);
32 | return ctx.measureText("...");
33 | }
34 |
35 | const oneCharWidth = ctx.measureText("0").width;
36 | const numerWidth = ctx.measureText(numer.toString()).width;
37 | const denomWidth = Math.min(
38 | Math.floor(maxWidth / oneCharWidth) * oneCharWidth,
39 | ctx.measureText(denom.toString()).width
40 | );
41 |
42 | ctx.fillText(
43 | numer.toString(),
44 | x + (denomWidth - numerWidth) / 2,
45 | y + fontSize
46 | );
47 | ctx.beginPath();
48 | ctx.moveTo(x, y + lineHeight);
49 | ctx.lineTo(x + denomWidth, y + lineHeight);
50 | ctx.stroke();
51 |
52 | const charsPerLine = Math.floor(maxWidth / oneCharWidth);
53 | const denomString = denom.toString();
54 | let line = 0;
55 |
56 | for (let idx = 0; idx < denomString.length; idx += charsPerLine, line++) {
57 | ctx.fillText(
58 | denomString.slice(idx, idx + charsPerLine),
59 | x,
60 | y + fontSize * (line + 2)
61 | );
62 | }
63 |
64 | return {
65 | width: denomWidth,
66 | height: lineHeight + line * fontSize,
67 | };
68 | }
69 |
70 | draw(ctx, { aBuffer, bBuffer }) {
71 | const a = bigIntOfBuffer(aBuffer);
72 | const b = bigIntOfBuffer(bBuffer);
73 |
74 | let numer = a < b ? a : b;
75 | let denom = a < b ? b : a;
76 | const fractions = [];
77 |
78 | const w = ctx.canvas.width;
79 | const h = ctx.canvas.height;
80 | const leftPadding = _(30, w);
81 | const topPadding = _(30, h);
82 |
83 | const f = 36;
84 | const fontSize = _(f, w);
85 | const lineHeight = _(f * 1.2, w);
86 |
87 | const equationPadding = _(30, w);
88 |
89 | ctx.font = `${fontSize}px monospace`;
90 | ctx.fillStyle = "rgb(0, 0, 0)";
91 | const equalsSignWidth = ctx.measureText("=").width;
92 |
93 | const fractionWidth = this.drawFraction(
94 | ctx,
95 | fontSize,
96 | lineHeight,
97 | numer,
98 | denom,
99 | leftPadding,
100 | topPadding,
101 | Infinity // lol
102 | ).width;
103 |
104 | const room =
105 | w -
106 | 2 * leftPadding -
107 | fractionWidth -
108 | equalsSignWidth -
109 | 2 * equationPadding;
110 |
111 | let idx = 0;
112 | const MAX_ITERATION = 20;
113 | for (; numer > 0 && idx < MAX_ITERATION; idx++) {
114 | if (denom % numer === 0n) {
115 | fractions.push(denom / numer);
116 | break;
117 | }
118 |
119 | let greedy = denom / numer + 1n;
120 | fractions.push(greedy);
121 |
122 | // numer/denom - 1/greedy
123 | // (numer*greedy)/(denom*greedy) - denom/(denom*greedy)
124 | try {
125 | numer = numer * greedy - denom;
126 | denom = denom * greedy;
127 | } catch (e) {
128 | break;
129 | }
130 | }
131 |
132 | // If we've run into an error or halted iteration for time
133 | if (denom % numer !== 0n) {
134 | fractions.push("...");
135 | }
136 |
137 | let y = topPadding;
138 | fractions.forEach((denom, idx) => {
139 | // draw = or +
140 | ctx.fillText(
141 | idx === 0 ? "=" : "+",
142 | leftPadding + fractionWidth + equationPadding,
143 | y + 1.25 * lineHeight // 1.25 is arbitrary but the equals sign is not very tall
144 | );
145 |
146 | // draw fraction
147 | const { height } = this.drawFraction(
148 | ctx,
149 | fontSize,
150 | lineHeight,
151 | 1,
152 | denom,
153 | leftPadding + fractionWidth + equalsSignWidth + 2 * equationPadding,
154 | y,
155 | room
156 | );
157 |
158 | y += height;
159 | });
160 | }
161 | }
162 |
163 | exports.Fraction = Fraction;
164 |
--------------------------------------------------------------------------------
/art/element.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _ } = require("./util.js");
3 | const { chain, elements } = require("./element-markov.js");
4 |
5 | function getName(buffer) {
6 | let currentNode = "start";
7 | let result = "";
8 |
9 | for (let i = 0; i < buffer.length; i++) {
10 | let options = chain[currentNode];
11 | let nonterminatingOptions = options.filter((o) => o !== "end");
12 |
13 | let next = options[Math.floor((buffer[i] / 256) * options.length)];
14 |
15 | if (next === "end") {
16 | break;
17 | }
18 |
19 | currentNode = next;
20 | if (result === "") {
21 | result = currentNode;
22 | } else {
23 | result += currentNode[currentNode.length - 1];
24 | }
25 | }
26 |
27 | return result[0].toUpperCase() + result.slice(1);
28 | }
29 |
30 | class Element extends Art {
31 | constructor() {
32 | super({
33 | traverse: 16,
34 | protons: 2,
35 | weight: 2,
36 | rotations: 8,
37 | });
38 | this.filename = "element.js";
39 | this.created = "01 Aug 2021";
40 | }
41 |
42 | getDescription() {
43 | return `
44 | Begin with a Markov chain built using the
45 | bigrams of the chemical elements (up to Oganesson (Og, 118) as of 1 Aug 2021). For example,
46 | the element oxygen forms the chain START -> ox -> xy -> yg -> ge -> en -> END.
47 |
48 | Combine these chains
49 | (things get interesting when one bigram can go in many directions such as
50 | "li" )
51 | and traverse the graph by picking the next node in the graph using the traverse buffer.
52 | Continue until END to get the name of your element, which may be the name of an existing element.
53 |
54 | This markov chain was generated with markov.rb
55 | and converted to JSON.
56 |
57 | The atomic number of our new element is computed by multiplying the protons vector by 300.
58 | The atomic weight is computed using atomicNumber * (1.5 + weight).
59 |
60 | From the atomic number, draw electrons according to the
61 | Bohr model , where each ring can hold 2n²
62 | electrons. Rotate each ring according to the nth byte in the rotations buffer.
63 | `;
64 | }
65 |
66 | getAtomicNumber(protons) {
67 | // return Math.floor(119 + protons * 100);
68 | return Math.floor(protons * 300);
69 | }
70 |
71 | draw(ctx, { traverseBuffer, protons, weight, rotationsBuffer }) {
72 | const w = ctx.canvas.width;
73 | const h = ctx.canvas.height;
74 |
75 | const name = getName(traverseBuffer);
76 | const left = 100;
77 |
78 | let atomicNumber = this.getAtomicNumber(protons);
79 | ctx.font = `bold ${_(130, h)}px Arial`;
80 | ctx.fillStyle = "rgb(0, 0, 0)";
81 | ctx.fillText(atomicNumber, _(left, w), _(180, h));
82 |
83 | ctx.font = `bold ${_(240, h)}px Arial`;
84 | ctx.fillStyle = "rgb(0, 0, 0)";
85 | ctx.fillText(name.slice(0, 2), _(left - 8, w), h - _(280, h));
86 |
87 | ctx.font = `bold ${_(80, h)}px Arial`;
88 | ctx.fillStyle = "rgb(0, 0, 0)";
89 | ctx.fillText(name, _(left, w), h - _(190, h));
90 |
91 | let atomicWeight = atomicNumber * (1.5 + weight);
92 | ctx.font = `bold ${_(80, h)}px Arial`;
93 | ctx.fillStyle = "rgb(0, 0, 0)";
94 | ctx.fillText(atomicWeight.toFixed(2), _(left, w), h - _(105, h));
95 |
96 | const bohrCenterX = _(830, w);
97 | const bohrCenterY = h / 2;
98 | // (w * 7) / 24 - nice outer width
99 | const innerRadius = _(45, w);
100 |
101 | ctx.lineWidth = _(2, w);
102 | ctx.beginPath();
103 | ctx.arc(bohrCenterX, bohrCenterY, innerRadius, 0, 2 * Math.PI);
104 | ctx.stroke();
105 |
106 | let shells = [];
107 | for (let i = 1; atomicNumber > 0; i++) {
108 | let electrons = Math.min(2 * i * i, atomicNumber);
109 | atomicNumber -= electrons;
110 | shells.push(electrons);
111 | }
112 |
113 | const electronRadius = 6;
114 | ctx.fillStyle = "rgb(255, 255, 255)";
115 | shells.forEach((electrons, i) => {
116 | const radius = innerRadius * (i + 2);
117 | ctx.beginPath();
118 | ctx.arc(bohrCenterX, bohrCenterY, radius, 0, 2 * Math.PI);
119 | ctx.stroke();
120 |
121 | for (let n = 0; n < electrons; n++) {
122 | const theta =
123 | (2 * Math.PI * n) / electrons +
124 | (rotationsBuffer[i] / 256) * 2 * Math.PI;
125 |
126 | ctx.save();
127 | ctx.beginPath();
128 | ctx.lineWidth = _(5, w);
129 | ctx.arc(
130 | bohrCenterX + radius * Math.cos(theta),
131 | bohrCenterY + radius * Math.sin(theta),
132 | _(electronRadius, w),
133 | 0,
134 | 2 * Math.PI
135 | );
136 | ctx.stroke();
137 | ctx.fill();
138 | ctx.restore();
139 | }
140 | });
141 | }
142 | }
143 |
144 | exports.Element = Element;
145 | exports.getName = getName;
146 |
--------------------------------------------------------------------------------
/art/network.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _, project } = require("./util.js");
3 |
4 | const NUM_POINTS = 12;
5 |
6 | class Network extends Art {
7 | constructor() {
8 | super({
9 | x: 1,
10 | y: 1,
11 | others: (NUM_POINTS - 1) * 2,
12 | });
13 | this.filename = "network.js";
14 | this.created = "22 May 2022";
15 | }
16 |
17 | getDescription() {
18 | return `
19 | Read ${NUM_POINTS} byte pairs from the array, representing
20 | a series of (x, y) points. Draw each point on the canvas,
21 | connecting it to every other point.
22 |
23 | Then, determine the
24 | Minimum spanning tree
25 | of the points: the continguous series of lines which connect to every point
26 | while minimizing the total length of the lines.
27 |
28 | You can use
29 | Kruskal's algorithm
30 | to determine this.
31 |
32 |
33 | algorithm Kruskal(G) is
34 | F:= ∅
35 | for each v ∈ G.V do
36 | MAKE-SET(v)
37 | for each (u, v) in G.E ordered by weight(u, v), increasing do
38 | if FIND-SET(u) ≠ FIND-SET(v) then
39 | F:= F ∪ {(u, v)} ∪ {(v, u)}
40 | UNION(FIND-SET(u), FIND-SET(v))
41 | return F
42 |
43 | Draw the lines in the Minimum spanning tree in bold.
44 | `;
45 | }
46 |
47 | kruskal({ edges, vertices }) {
48 | // https://en.wikipedia.org/wiki/Kruskal%27s_algorithm
49 | //
50 | // F:= ∅
51 | // for each v ∈ G.V do
52 | // MAKE-SET(v)
53 | // for each (u, v) in G.E ordered by weight(u, v), increasing do
54 | // if FIND-SET(u) ≠ FIND-SET(v) then
55 | // F:= F ∪ {(u, v)} ∪ {(v, u)}
56 | // UNION(FIND-SET(u), FIND-SET(v))
57 | // return F
58 |
59 | const F = new Set([]);
60 | const disjointSets = new Set(
61 | vertices.map((v) => new Set([`${v[0]},${v[1]}`]))
62 | );
63 |
64 | const sortedEdges = edges.slice();
65 | sortedEdges.sort((e1, e2) => {
66 | // Oh dear
67 | const d1 =
68 | (e1[0][0] - e1[1][0]) * (e1[0][0] - e1[1][0]) +
69 | (e1[0][1] - e1[1][1]) * (e1[0][1] - e1[1][1]);
70 | const d2 =
71 | (e2[0][0] - e2[1][0]) * (e2[0][0] - e2[1][0]) +
72 | (e2[0][1] - e2[1][1]) * (e2[0][1] - e2[1][1]);
73 | return d1 - d2;
74 | });
75 |
76 | console.log(
77 | sortedEdges.map(
78 | ([u, v]) =>
79 | (u[1] - u[0]) * (u[1] - u[0]) + (v[1] - v[0]) * (v[1] - v[0])
80 | )
81 | );
82 |
83 | sortedEdges.forEach(([u, v]) => {
84 | const uSet = [...disjointSets].find((set) => set.has(`${u[0]},${u[1]}`));
85 | const vSet = [...disjointSets].find((set) => set.has(`${v[0]},${v[1]}`));
86 |
87 | if (uSet !== vSet) {
88 | F.add([u, v]);
89 | disjointSets.delete(uSet);
90 | disjointSets.delete(vSet);
91 | disjointSets.add(new Set([...uSet, ...vSet]));
92 | }
93 | });
94 |
95 | return [...F];
96 | }
97 |
98 | draw(ctx, { xBuffer, yBuffer, othersBuffer }) {
99 | const w = ctx.canvas.width;
100 | const h = ctx.canvas.height;
101 |
102 | const coords = [...xBuffer, ...yBuffer, ...othersBuffer];
103 |
104 | const vertices = [];
105 | for (let i = 0; i < NUM_POINTS; i++) {
106 | vertices.push([coords[2 * i], coords[2 * i + 1]]);
107 | }
108 |
109 | const edges = [];
110 | for (let i = 0; i < NUM_POINTS - 1; i++) {
111 | for (let j = i + 1; j < NUM_POINTS; j++) {
112 | edges.push([
113 | [coords[2 * i], coords[2 * i + 1]],
114 | [coords[2 * j], coords[2 * j + 1]],
115 | ]);
116 | }
117 | }
118 |
119 | // Padding
120 | const p = _(120, w);
121 |
122 | // Draw the network of lines
123 | ctx.lineWidth = _(1, w);
124 | edges.forEach((edge) => {
125 | ctx.beginPath();
126 | ctx.moveTo(
127 | project(edge[0][0], 0, 256, p, w - p),
128 | project(edge[0][1], 0, 256, p, h - p)
129 | );
130 | ctx.lineTo(
131 | project(edge[1][0], 0, 256, p, w - p),
132 | project(edge[1][1], 0, 256, p, h - p)
133 | );
134 | ctx.stroke();
135 | });
136 |
137 | // Draw the minimum spanning tree
138 | const mst = this.kruskal({ edges, vertices });
139 | ctx.lineWidth = _(10, w);
140 | mst.forEach((edge) => {
141 | ctx.beginPath();
142 | ctx.moveTo(
143 | project(edge[0][0], 0, 256, p, w - p),
144 | project(edge[0][1], 0, 256, p, h - p)
145 | );
146 | ctx.lineTo(
147 | project(edge[1][0], 0, 256, p, w - p),
148 | project(edge[1][1], 0, 256, p, h - p)
149 | );
150 | ctx.stroke();
151 | });
152 |
153 | // Draw circles for every node
154 | ctx.fillStyle = `rgb(255, 255, 255)`;
155 | vertices.forEach(([x, y]) => {
156 | ctx.beginPath();
157 | ctx.arc(
158 | project(x, 0, 256, p, w - p),
159 | project(y, 0, 256, p, h - p),
160 | _(15, w),
161 | 0,
162 | 2 * Math.PI
163 | );
164 | ctx.fill();
165 | ctx.stroke();
166 | });
167 | }
168 | }
169 |
170 | exports.Network = Network;
171 |
--------------------------------------------------------------------------------
/art/honeycomb.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _ } = require("./util.js");
3 |
4 | class HoneyComb extends Art {
5 | constructor() {
6 | super({
7 | paths: 19,
8 | });
9 |
10 | this.filename = "honeycomb.js";
11 | this.created = "13 Feb 2022";
12 | }
13 |
14 | getDescription({ pathsBuffer }) {
15 | return `
16 | Arrange ${pathsBuffer.length} hexagons into one larger hexagon. For
17 | each of the ${pathsBuffer.length} bytes in the buffer: If the byte is
18 | greater than 216, do nothing. Otherwise, use the byte value
19 | to generate two numbers a, b between 0 and 5.
20 |
21 | Draw a path connecting side a to side b, or a
22 | dead end if they are equal.
23 | `;
24 | }
25 |
26 | drawHexagon(ctx, pattern, x, y, sideLength) {
27 | // Edge
28 | ctx.beginPath();
29 | for (let i = 0; i <= 6; i++) {
30 | const theta = Math.PI / 6 + (i * 2 * Math.PI) / 6;
31 | const [px, py] = [
32 | x + sideLength * Math.cos(theta),
33 | y + sideLength * Math.sin(theta),
34 | ];
35 |
36 | if (i === 0) {
37 | ctx.moveTo(px, py);
38 | } else {
39 | ctx.lineTo(px, py);
40 | }
41 | }
42 | ctx.stroke();
43 |
44 | const [a, b] = [Math.floor(pattern / 36), pattern % 6];
45 |
46 | const gap = sideLength / 4;
47 | const l = (sideLength * Math.sqrt(3)) / 2;
48 |
49 | const aTheta = (a * 2 * Math.PI) / 6;
50 | const bTheta = (b * 2 * Math.PI) / 6;
51 |
52 | if (a === b) {
53 | ctx.beginPath();
54 | ctx.moveTo(
55 | x + l * Math.cos(aTheta) - (gap / 2) * Math.cos(aTheta - Math.PI / 2),
56 | y + l * Math.sin(aTheta) - (gap / 2) * Math.sin(aTheta - Math.PI / 2)
57 | );
58 | ctx.lineTo(
59 | x + gap * Math.cos(aTheta) - (gap / 2) * Math.cos(aTheta - Math.PI / 2),
60 | y + gap * Math.sin(aTheta) - (gap / 2) * Math.sin(aTheta - Math.PI / 2)
61 | );
62 |
63 | ctx.lineTo(
64 | x + gap * Math.cos(aTheta) + (gap / 2) * Math.cos(aTheta - Math.PI / 2),
65 | y + gap * Math.sin(aTheta) + (gap / 2) * Math.sin(aTheta - Math.PI / 2)
66 | );
67 | ctx.lineTo(
68 | x + l * Math.cos(aTheta) + (gap / 2) * Math.cos(aTheta - Math.PI / 2),
69 | y + l * Math.sin(aTheta) + (gap / 2) * Math.sin(aTheta - Math.PI / 2)
70 | );
71 |
72 | ctx.stroke();
73 | } else {
74 | // A
75 | ctx.beginPath();
76 | ctx.moveTo(
77 | x + l * Math.cos(aTheta) - (gap / 2) * Math.cos(aTheta - Math.PI / 2),
78 | y + l * Math.sin(aTheta) - (gap / 2) * Math.sin(aTheta - Math.PI / 2)
79 | );
80 | ctx.lineTo(
81 | x + gap * Math.cos(aTheta) - (gap / 2) * Math.cos(aTheta - Math.PI / 2),
82 | y + gap * Math.sin(aTheta) - (gap / 2) * Math.sin(aTheta - Math.PI / 2)
83 | );
84 |
85 | // todo - arc?
86 |
87 | ctx.lineTo(
88 | x + gap * Math.cos(bTheta) + (gap / 2) * Math.cos(bTheta - Math.PI / 2),
89 | y + gap * Math.sin(bTheta) + (gap / 2) * Math.sin(bTheta - Math.PI / 2)
90 | );
91 | ctx.lineTo(
92 | x + l * Math.cos(bTheta) + (gap / 2) * Math.cos(bTheta - Math.PI / 2),
93 | y + l * Math.sin(bTheta) + (gap / 2) * Math.sin(bTheta - Math.PI / 2)
94 | );
95 | ctx.stroke();
96 |
97 | // B
98 |
99 | ctx.beginPath();
100 | ctx.moveTo(
101 | x + l * Math.cos(aTheta) + (gap / 2) * Math.cos(aTheta - Math.PI / 2),
102 | y + l * Math.sin(aTheta) + (gap / 2) * Math.sin(aTheta - Math.PI / 2)
103 | );
104 | ctx.lineTo(
105 | x + gap * Math.cos(aTheta) + (gap / 2) * Math.cos(aTheta - Math.PI / 2),
106 | y + gap * Math.sin(aTheta) + (gap / 2) * Math.sin(aTheta - Math.PI / 2)
107 | );
108 |
109 | // todo - arc?
110 |
111 | ctx.lineTo(
112 | x + gap * Math.cos(bTheta) - (gap / 2) * Math.cos(bTheta - Math.PI / 2),
113 | y + gap * Math.sin(bTheta) - (gap / 2) * Math.sin(bTheta - Math.PI / 2)
114 | );
115 | ctx.lineTo(
116 | x + l * Math.cos(bTheta) - (gap / 2) * Math.cos(bTheta - Math.PI / 2),
117 | y + l * Math.sin(bTheta) - (gap / 2) * Math.sin(bTheta - Math.PI / 2)
118 | );
119 | ctx.stroke();
120 | }
121 | }
122 |
123 | draw(ctx, { pathsBuffer }) {
124 | const w = ctx.canvas.width;
125 | const h = ctx.canvas.height;
126 | const s = Math.max(w, h);
127 | ctx.lineWidth = _(3, s);
128 |
129 | const sideLength = _(80, s);
130 |
131 | // https://stackoverflow.com/questions/2459402/hexagonal-grid-coordinates-to-pixel-coordinates
132 | [
133 | [-2, 2],
134 | [-1, 2],
135 | [0, 2],
136 | [-2, 1],
137 | [-1, 1],
138 | [0, 1],
139 | [1, 1],
140 | [-2, 0],
141 | [-1, 0],
142 | [0, 0],
143 | [1, 0],
144 | [2, 0],
145 | [-1, -1],
146 | [0, -1],
147 | [1, -1],
148 | [2, -1],
149 | [0, -2],
150 | [1, -2],
151 | [2, -2],
152 | ].forEach(([a, b], idx) => {
153 | const x = w / 2 - Math.sqrt(3) * sideLength * (b / 2 + a);
154 | const y = h / 2 - (3 / 2) * sideLength * b;
155 | if (pathsBuffer[idx] < 216) {
156 | this.drawHexagon(ctx, pathsBuffer[idx], x, y, sideLength);
157 | }
158 | });
159 | }
160 | }
161 |
162 | exports.HoneyComb = HoneyComb;
163 |
--------------------------------------------------------------------------------
/art/stocks.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _ } = require("./util.js");
3 |
4 | class Stocks extends Art {
5 | constructor() {
6 | super({
7 | name: 4,
8 | date: 2,
9 | open: 1,
10 | moves: 25,
11 | });
12 | this.filename = "stocks.js";
13 | this.created = "18 Apr 2021";
14 | }
15 |
16 | getDescription() {
17 | return `
18 | We generate a ${this.template.moves}-day Candlestick chart
19 | using the name buffer to compute a random 4-digit stock symbol.
20 | The stock opens at a random value specified by open on a random day generated
21 | from date. For each byte in the moves buffer we generate a
22 | close, high, and low using the following equations.
23 |
24 | const close = Math.sin(0.1337 * movesBuffer[i]) * closeVariance + open;
25 |
26 | const low =
27 | Math.min(open, close) -
28 | Math.abs(Math.sin(0.4242 * movesBuffer[i]) * lowHighVariance);
29 |
30 | const high =
31 | Math.max(open, close) +
32 | Math.abs(Math.sin(0.1729 * movesBuffer[i]) * lowHighVariance);
33 |
34 | These four numbers are used to draw each candlestick, and the global high and low are
35 | rendered in the bottom right.
36 | `;
37 | }
38 |
39 | draw(ctx, { nameBuffer, date, open, movesBuffer }) {
40 | const w = ctx.canvas.width;
41 | const h = ctx.canvas.height;
42 | const s = Math.min(w, h);
43 |
44 | // Name
45 | const name = Array.from(nameBuffer)
46 | .map((b) => String.fromCharCode((b % 26) + 65))
47 | .join("");
48 | ctx.font = `bold ${_(80, s)}px monospace`;
49 | ctx.fillStyle = "rgb(0, 0, 0)";
50 | ctx.fillText("$" + name, _(60, w), h - _(80, h));
51 |
52 | // Graph
53 | ctx.lineWidth = _(3, w);
54 | const leftPadding = _(236, w);
55 | const topPadding = _(140, h);
56 | const bottomPadding = _(400, h);
57 | let barWidth = _(20, w);
58 | const halfBarWidth = Math.floor(barWidth / 2) + 1;
59 |
60 | const barDistance = _(40, w);
61 |
62 | const sticks = [];
63 | let lastClose = open * 128 + 50;
64 | for (let i = 0; i < movesBuffer.byteLength; i++) {
65 | const closeVariance = 10;
66 | const lowHighVariance = 5;
67 |
68 | const open = lastClose;
69 | const close = Math.sin(0.1337 * movesBuffer[i]) * closeVariance + open;
70 | const low =
71 | Math.min(open, close) -
72 | Math.abs(Math.sin(0.4242 * movesBuffer[i]) * lowHighVariance);
73 | const high =
74 | Math.max(open, close) +
75 | Math.abs(Math.sin(0.1729 * movesBuffer[i]) * lowHighVariance);
76 |
77 | lastClose = close;
78 |
79 | sticks.push([low, open, close, high]);
80 | }
81 |
82 | const min = Math.min(...sticks.map((v) => v[0]));
83 | const max = Math.max(...sticks.map((v) => v[3]));
84 |
85 | sticks
86 | // map [low, open, close, high] to canvas y-coordinates
87 | .map((stick) =>
88 | stick.map(
89 | (el) =>
90 | ((el - min) / (max - min)) * (h - (topPadding + bottomPadding)) +
91 | topPadding
92 | )
93 | )
94 | .forEach(([low, open, close, high], i) => {
95 | const x = leftPadding + i * barDistance;
96 |
97 | ctx.beginPath();
98 |
99 | ctx.moveTo(x + halfBarWidth, low);
100 | ctx.lineTo(x + halfBarWidth, Math.min(open, close));
101 |
102 | ctx.moveTo(x + halfBarWidth, high);
103 | ctx.lineTo(x + halfBarWidth, Math.max(open, close));
104 |
105 | ctx.rect(x, open, barWidth, close - open);
106 | ctx.stroke();
107 | });
108 |
109 | // Labels
110 | const labelPadding = _(64, w);
111 | ctx.font = `bold ${_(36, s)}px monospace`;
112 | ctx.textAlign = "right";
113 | ctx.fillText(
114 | `${movesBuffer.byteLength}d high: ${max.toFixed(2)}`,
115 | w - _(80, w),
116 | h - _(120, h)
117 | );
118 | ctx.fillText(
119 | `${movesBuffer.byteLength}d low: ${min.toFixed(2)}`,
120 | w - _(80, w),
121 | h - _(80, h)
122 | );
123 |
124 | // Dates
125 | ctx.font = `bold ${_(30, s)}px monospace`;
126 | ctx.textAlign = "left";
127 |
128 | function dateFromDay(year, day) {
129 | var date = new Date(year, 0); // initialize a date in `year-01-01`
130 | return new Date(date.setDate(day)); // add the number of days
131 | }
132 |
133 | const firstDay = Math.floor(date * 365);
134 |
135 | const markers = 4;
136 |
137 | for (let i = 0; i < markers + 1; i++) {
138 | const date = dateFromDay(
139 | 2021,
140 | firstDay + i * ((movesBuffer.byteLength - 1) / markers)
141 | );
142 |
143 | ctx.textAlign = "center";
144 | ctx.fillText(
145 | `${date.getMonth() + 1}/${date.getDate()}`,
146 | leftPadding +
147 | barDistance * i * ((movesBuffer.byteLength - 1) / markers) +
148 | halfBarWidth,
149 | h - bottomPadding + labelPadding
150 | );
151 | }
152 |
153 | // Prices
154 | ctx.textAlign = "right";
155 | ctx.fillText(max.toFixed(2), leftPadding - labelPadding, topPadding);
156 | ctx.fillText(min.toFixed(2), leftPadding - labelPadding, h - bottomPadding);
157 | ctx.fillText(
158 | ((max + min) / 2).toFixed(2),
159 | leftPadding - labelPadding,
160 | (topPadding + h - bottomPadding) / 2
161 | );
162 | }
163 | }
164 |
165 | exports.Stocks = Stocks;
166 |
--------------------------------------------------------------------------------
/art/fifteen.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 |
3 | class Fifteen extends Art {
4 | constructor() {
5 | super({
6 | moves: 32,
7 | });
8 | this.filename = "fifteen.js";
9 | this.created = "21 Feb 2022";
10 | }
11 |
12 | getDescription() {
13 | return `
14 | Begin with the initial 15 puzzle ,
15 | where numbers are arranged in a 4x4 grid starting at 1 and increasing to 15.
16 | The last square is left empty.
17 |
18 | For each of the bytes in moves, split into four 2-bit pairs
19 | and use each of them to encode a single "move" N/E/S/W.
20 |
21 | For each individual "move," go to the empty space and attempt to grab the
22 | tile in the N/E/S/W direction. If there is no tile there (such as, if we are
23 | along an edge), skip the move.
24 |
25 | After each valid move, draw the current grid on the canvas. Use "black" for
26 | the empty square, and increase the lightness for each tile (tile 15 being white).
27 | `;
28 | }
29 |
30 | getEmptyPosition(grid) {
31 | for (let x = 0; x < 4; x++) {
32 | for (let y = 0; y < 4; y++) {
33 | if (grid[y][x] === 0) {
34 | return { x, y };
35 | }
36 | }
37 | }
38 | }
39 |
40 | computeIdealLayout(w, h, grids) {
41 | const idealNegativeSpace = (w * h) / 2;
42 | let bestLayout = { negativeSpace: Infinity };
43 |
44 | for (let columns = 1; columns < grids.length; columns++) {
45 | for (let puzzleSize = 8; puzzleSize < 200; puzzleSize += 4) {
46 | for (let padding = puzzleSize / 4; padding < puzzleSize; padding += 1) {
47 | let rows = Math.ceil(grids.length / columns);
48 | let layoutWidth = columns * puzzleSize + (columns + 1) * padding;
49 | let layoutHeight = rows * puzzleSize + (rows + 1) * padding;
50 |
51 | // Doesn't fit, continue
52 | if (layoutWidth > w || layoutHeight > h) {
53 | continue;
54 | }
55 |
56 | const negativeSpace =
57 | // Size of the canvas
58 | w * h -
59 | // Subtract the area of the puzzles
60 | puzzleSize * puzzleSize * columns * rows;
61 |
62 | if (
63 | Math.abs(idealNegativeSpace - negativeSpace) <
64 | Math.abs(idealNegativeSpace - bestLayout.negativeSpace)
65 | ) {
66 | bestLayout = {
67 | negativeSpace,
68 | columns,
69 | rows,
70 | layoutWidth,
71 | layoutHeight,
72 | padding,
73 | puzzleSize,
74 | };
75 | }
76 | }
77 | }
78 | }
79 |
80 | return bestLayout;
81 | }
82 |
83 | drawPuzzle(ctx, x, y, puzzleSize, grid) {
84 | const pixelSize = puzzleSize / 4;
85 |
86 | grid.forEach((n, idx) => {
87 | // prob scale
88 | const gray = (n / 15) * 255;
89 | ctx.fillStyle = `rgb(${gray}, ${gray}, ${gray})`;
90 |
91 | ctx.beginPath();
92 | ctx.rect(
93 | x + pixelSize * (idx % 4),
94 | y + pixelSize * Math.floor(idx / 4),
95 | pixelSize,
96 | pixelSize
97 | );
98 | ctx.fill();
99 | });
100 | }
101 |
102 | draw(ctx, { movesBuffer }) {
103 | const w = ctx.canvas.width;
104 | const h = ctx.canvas.height;
105 |
106 | const currentGrid = [
107 | [1, 2, 3, 4],
108 | [5, 6, 7, 8],
109 | [9, 10, 11, 12],
110 | [13, 14, 15, 0],
111 | ];
112 |
113 | const grids = [[].concat.apply([], currentGrid)];
114 |
115 | // From each byte we get one of N/E/S/W, representing
116 | // the direction of the block we'll be moving
117 | const moves = [].concat(
118 | ...Array.from(movesBuffer).map((byte) => [
119 | byte >> 6,
120 | (byte >> 4) % 4,
121 | (byte >> 2) % 4,
122 | byte % 4,
123 | ])
124 | );
125 |
126 | moves.forEach((move) => {
127 | const { x, y } = this.getEmptyPosition(currentGrid);
128 | const tmp = currentGrid[y][x];
129 | let changed = false;
130 | if (move === 0 && y > 0) {
131 | currentGrid[y][x] = currentGrid[y - 1][x];
132 | currentGrid[y - 1][x] = tmp;
133 | changed = true;
134 | } else if (move === 1 && x < 3) {
135 | currentGrid[y][x] = currentGrid[y][x + 1];
136 | currentGrid[y][x + 1] = tmp;
137 | changed = true;
138 | } else if (move === 2 && y < 3) {
139 | currentGrid[y][x] = currentGrid[y + 1][x];
140 | currentGrid[y + 1][x] = tmp;
141 | changed = true;
142 | } else if (move === 3 && x > 0) {
143 | currentGrid[y][x] = currentGrid[y][x - 1];
144 | currentGrid[y][x - 1] = tmp;
145 | changed = true;
146 | }
147 |
148 | if (changed) {
149 | grids.push([].concat.apply([], currentGrid));
150 | }
151 | });
152 |
153 | const { rows, columns, layoutWidth, layoutHeight, padding, puzzleSize } =
154 | this.computeIdealLayout(w * 0.9, h * 0.9, grids);
155 |
156 | grids.forEach((grid, idx) => {
157 | const row = Math.floor(idx / columns);
158 | const column = idx % columns;
159 |
160 | const x =
161 | Math.floor(w / 2 - layoutWidth / 2) +
162 | column * puzzleSize +
163 | (column + 1) * padding;
164 | const y =
165 | Math.floor(h / 2 - layoutHeight / 2) +
166 | row * puzzleSize +
167 | (row + 1) * padding;
168 |
169 | this.drawPuzzle(ctx, x, y, puzzleSize, grid);
170 | });
171 | }
172 | }
173 |
174 | exports.Fifteen = Fifteen;
175 |
--------------------------------------------------------------------------------
/art/turing.js:
--------------------------------------------------------------------------------
1 | const { Art } = require("./_base.js");
2 | const { _ } = require("./util.js");
3 |
4 | class Turing extends Art {
5 | constructor() {
6 | super({
7 | α0: 3,
8 | β0: 3,
9 | γ0: 3,
10 | α1: 3,
11 | β1: 3,
12 | γ1: 3,
13 | α2: 3,
14 | β2: 3,
15 | γ2: 3,
16 | input: 5,
17 | });
18 | this.filename = "turing.js";
19 | this.created = "28 May 2021";
20 | }
21 |
22 | transitionTable(params) {
23 | function triplet(key) {
24 | return {
25 | write: Math.floor((params[key + "Buffer"][0] / 256) * 3),
26 | move: Math.floor((params[key + "Buffer"][1] / 256) * 2),
27 | nextState: "αβγ"[Math.floor((params[key + "Buffer"][0] / 256) * 3)],
28 | };
29 | }
30 |
31 | return {
32 | 0: {
33 | α: triplet("α0"),
34 | β: triplet("β0"),
35 | γ: triplet("γ0"),
36 | },
37 | 1: {
38 | α: triplet("α1"),
39 | β: triplet("β1"),
40 | γ: triplet("γ1"),
41 | },
42 | 2: {
43 | α: triplet("α2"),
44 | β: triplet("β2"),
45 | γ: triplet("γ2"),
46 | },
47 | };
48 | }
49 |
50 | transitionTableHtml(params) {
51 | const table = this.transitionTable(params);
52 | const body = [0, 1, 2]
53 | .map((i) => {
54 | const row = "αβγ"
55 | .split("")
56 | .map((state) => {
57 | const entry = table[i][state];
58 | return `
59 | ${entry.write}
60 | ${entry.move ? "R" : "L"}
61 | ${entry.nextState}
62 | `;
63 | })
64 | .join("");
65 |
66 | return `
67 |
68 | ${i}
69 | ${row}
70 |
71 | `;
72 | })
73 | .join("");
74 |
75 | return `
76 |
77 |
78 |
79 | Symbol
80 | State α
81 | State β
82 | State γ
83 |
84 |
85 | Write
86 | Move
87 | State
88 | Write
89 | Move
90 | State
91 | Write
92 | Move
93 | State
94 |
95 |
96 | ${body}
97 |
98 | `;
99 | }
100 |
101 | getDescription(params) {
102 | return `
103 | From the hash we construct a 3-symbol, 3-state
104 | Turing machine
105 | with the following transition table:
106 |
107 |
108 | ${this.transitionTableHtml(params)}
109 |
110 |
111 | We begin at state α on a tape seeded with input,
112 | and use the lookup table to determine which symbol to write (a white square,
113 | gray square, or black square),
114 | which direction to move the cursor (left or right), and which state to become.
115 | (For artistic purposes, the "halt" state H never appears).
116 |
117 | With each transition, we draw the one-dimensional tape on the canvas, and move down
118 | a line. The result is a two-dimensional drawing that grows off the sides of the screen, empties
119 | out completely, or draws something interesting.
120 |
121 | Turing machines are named after Alan Turing ,
122 | who developed them while researching the
123 | Entscheidungsproblem . These machines
124 | form the basis of "computation" - following a series of steps on some input to produce some output.
125 | In order to construct a universal Turing Machine (one which can compute anything), we
126 | need more states and symbols*.
127 |
128 | [* ]
129 | Universal machines with the following amounts of (state, symbol) have been found - (15, 2),
130 | (9, 3), (6, 4), (5, 5), (4, 6), (3, 9), and (2, 18)
131 | `;
132 | }
133 |
134 | transition(table, tape, cursorPosition, state) {
135 | let value = tape[cursorPosition];
136 | let { write, move, nextState } = table[value][state];
137 |
138 | tape[cursorPosition] = write;
139 | return [move ? cursorPosition + 1 : cursorPosition - 1, nextState];
140 | }
141 |
142 | drawTape(ctx, tape, cursorPosition, bitSize, y) {
143 | const widthInBits = Math.floor(ctx.canvas.width / bitSize);
144 | const start = Math.floor(tape.length / 2 - widthInBits / 2);
145 | const end = Math.floor(tape.length / 2 + widthInBits / 2);
146 |
147 | for (let i = start; i < end; i++) {
148 | const x = (i - start) * bitSize;
149 | if (i === cursorPosition) {
150 | ctx.lineWidth = _(3, ctx.canvas.width);
151 | ctx.strokeStyle = "rgb(255, 255, 255)";
152 | ctx.fillStyle = "rgb(0, 0, 0)";
153 | ctx.beginPath();
154 | ctx.moveTo(x + bitSize / 2, y);
155 | ctx.lineTo(x, y - bitSize / 2);
156 | ctx.lineTo(x + bitSize, y - bitSize / 2);
157 | ctx.lineTo(x + bitSize / 2, y);
158 | ctx.stroke();
159 | ctx.fill();
160 | }
161 |
162 | if (tape[i]) {
163 | const shade = (tape[i] - 1) * 128;
164 | ctx.fillStyle = `rgb(${shade}, ${shade}, ${shade})`;
165 |
166 | ctx.beginPath();
167 | ctx.rect(x, y, bitSize, bitSize);
168 | ctx.fill();
169 | }
170 | }
171 | }
172 |
173 | bufferToTernary(buffer) {
174 | let base10 = buffer.reduce((acc, d) => acc * 256 + d, 1);
175 | return base10
176 | .toString(3)
177 | .split("")
178 | .map((d) => parseInt(d));
179 | }
180 |
181 | draw(ctx, params) {
182 | const w = ctx.canvas.width;
183 | const h = ctx.canvas.height;
184 |
185 | // 12px bits at 1200px looks decent
186 | const bitSize = _(12, w);
187 |
188 | // Initialize a tape large enough to handle all possible movements
189 | let tape = Array.from({
190 | length: 2 * Math.floor(Math.max(w, h) / bitSize) + 2,
191 | }).map((_) => 0);
192 |
193 | let cursorPosition = Math.floor(tape.length / 2);
194 | let state = "α";
195 |
196 | // Place `input` on the tape
197 | const input = this.bufferToTernary(params.inputBuffer);
198 | for (let i = 0; i < input.length; i++) {
199 | tape[cursorPosition - Math.floor(input.length / 2) + i] = input[i];
200 | }
201 |
202 | // Generate the transition table
203 | const table = this.transitionTable(params);
204 |
205 | for (let i = 0; i < Math.floor(h / bitSize) + 1; i++) {
206 | [cursorPosition, state] = this.transition(
207 | table,
208 | tape,
209 | cursorPosition,
210 | state
211 | );
212 |
213 | this.drawTape(ctx, tape, cursorPosition, bitSize, i * bitSize);
214 | }
215 | }
216 | }
217 |
218 | exports.Turing = Turing;
219 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const crypto = require("crypto");
3 | const { createCanvas } = require("canvas");
4 | const ejs = require("ejs");
5 | const fs = require("fs");
6 | const glob = require("glob");
7 | const jsnes = require("jsnes");
8 | const path = require("path");
9 | const pieces = require("./art/pieces.js");
10 | const generateState = require("./scripts/generate-state.js");
11 |
12 | const state = generateState();
13 | const app = express();
14 |
15 | app.use(express.json());
16 |
17 | app.use((req, res, next) => {
18 | console.log(`${new Date()} ${req.ip} ${req.path}`);
19 | next();
20 | });
21 |
22 | app.get("/", (req, res) => {
23 | res.send(`
24 | Provide a piece, width, height, and seed
25 |
36 |
37 | Provide a piece and have the server pick a random seed
38 |
41 |
42 | Have the server pick both the piece and seed
43 |
46 |
47 | Provide a piece and seed with a default resolution of 1320x1320
48 |
51 |
52 | Display a 5x5 grid of randomly generated pieces
53 |
56 |
57 | Adjust the pieces that appear in the random rotation
58 |
59 | /admin (requires ADMIN_PASSWORD environment variable)
60 |
61 | `);
62 | });
63 |
64 | function sendArt(res, { piece, width, height, seed }) {
65 | const art = new pieces[piece]();
66 | const canvas = createCanvas(parseInt(width), parseInt(height));
67 | const ctx = canvas.getContext("2d");
68 |
69 | let props = {};
70 | if (piece === "mario") {
71 | if (!state) {
72 | res.send(
73 | "State snapshot not found (does $ROOT/vendor/roms/mariobros.nes exist?)"
74 | );
75 | return;
76 | }
77 |
78 | let latestFrameBuffer = null;
79 | const nes = new jsnes.NES({
80 | onFrame: function (frameBuffer) {
81 | latestFrameBuffer = frameBuffer;
82 | },
83 | });
84 |
85 | nes.fromJSON(JSON.parse(state));
86 | nes.frame();
87 |
88 | props = {
89 | nes,
90 | getFrameBuffer() {
91 | return latestFrameBuffer;
92 | },
93 | };
94 | } else if (piece === "nes") {
95 | let latestFrameBuffer = null;
96 | const nes = new jsnes.NES({
97 | onFrame: function (frameBuffer) {
98 | latestFrameBuffer = frameBuffer;
99 | },
100 | });
101 |
102 | const romsGlob = path.join(__dirname, "vendor/roms/**/*.nes");
103 | const roms = glob.sync(romsGlob);
104 | if (roms.length === 0) {
105 | res.send("No roms found (place them in $ROOT/vendor/roms)");
106 | return;
107 | }
108 |
109 | const secondCanvas = createCanvas(parseInt(width), parseInt(height));
110 |
111 | props = {
112 | nes,
113 | roms,
114 | fs,
115 | path,
116 | getFrameBuffer() {
117 | return latestFrameBuffer;
118 | },
119 | secondCtx: secondCanvas.getContext("2d"),
120 | };
121 | }
122 |
123 | const shaSum = crypto.createHash("sha256");
124 | shaSum.update(seed);
125 | const buffer = shaSum.digest();
126 | const hash = new Uint8Array(buffer);
127 |
128 | art.render(ctx, hash, props);
129 | res.set("Content-Type", "image/png");
130 |
131 | /**
132 | * `createPNGStream` is handy but unfortunately for my e-ink display
133 | * I need a proper Content-Length set. So we'll dump the stream into
134 | * a buffer and send it as a whole.
135 | */
136 | var buffs = [];
137 | const pngStream = canvas.createPNGStream();
138 | pngStream.on("data", function (d) {
139 | buffs.push(d);
140 | });
141 | pngStream.on("end", function () {
142 | const buff = Buffer.concat(buffs);
143 | res.set("Content-Length", buff.byteLength);
144 | res.send(buff);
145 | });
146 | }
147 |
148 | function defaultPieces() {
149 | return Object.keys(pieces).reduce(
150 | (acc, key) => ({ ...acc, [key]: true }),
151 | {}
152 | );
153 | }
154 |
155 | function getEnabledPieces() {
156 | if (!fs.existsSync("db.json")) {
157 | return defaultPieces();
158 | } else {
159 | return JSON.parse(fs.readFileSync("db.json"));
160 | }
161 | }
162 |
163 | function setEnabledPieces(pieces) {
164 | fs.writeFileSync("db.json", JSON.stringify(pieces, null, 2));
165 | }
166 |
167 | app.get("/admin", (req, res) => {
168 | if (!process.env.ADMIN_PASSWORD) {
169 | res.statusCode = 404;
170 | res.send("Not found");
171 | } else {
172 | ejs.renderFile(
173 | "admin.ejs",
174 | {
175 | pieces: Object.keys(pieces),
176 | enabledPieces: getEnabledPieces(),
177 | },
178 | (err, str) => {
179 | if (err) {
180 | res.statusCode = 500;
181 | res.send("Error");
182 | } else {
183 | res.send(str);
184 | }
185 | }
186 | );
187 | }
188 | });
189 |
190 | app.post("/admin", (req, res) => {
191 | if (
192 | !process.env.ADMIN_PASSWORD ||
193 | req.header("Authorization") !== `Bearer ${process.env.ADMIN_PASSWORD}`
194 | ) {
195 | res.statusCode = 404;
196 | res.send("Not found");
197 | } else {
198 | let enabledPieces = getEnabledPieces();
199 |
200 | Object.keys(req.body).forEach((key) => {
201 | if (req.body[key]) {
202 | enabledPieces[key] = true;
203 | } else {
204 | enabledPieces[key] = false;
205 | }
206 | });
207 |
208 | const allFalse = !Object.keys(enabledPieces).some(
209 | (key) => enabledPieces[key]
210 | );
211 |
212 | if (allFalse) {
213 | res.statusCode = 404;
214 | res.send("Not found");
215 | } else {
216 | setEnabledPieces(enabledPieces);
217 | res.send("Ok");
218 | }
219 | }
220 | });
221 |
222 | app.get("/random/:width/:height/random.png", (req, res) => {
223 | const { width, height } = req.params;
224 |
225 | const enabledPieces = getEnabledPieces();
226 | const pieces = Object.keys(enabledPieces).filter((key) => enabledPieces[key]);
227 |
228 | const pieceKeys =
229 | state == null ? pieces.filter((name) => name !== "mario") : pieces;
230 | const piece = pieceKeys[Math.floor(Math.random() * pieceKeys.length)];
231 | const seed = Math.random() + "";
232 |
233 | sendArt(res, { piece, seed, width, height });
234 | });
235 |
236 | app.get("/:piece/grid", (req, res) => {
237 | const { piece } = req.params;
238 |
239 | let images = "";
240 | for (let i = 0; i < 25; i++) {
241 | const seed = Math.random() + "";
242 | images += ` `;
243 | }
244 |
245 | res.send(`
246 |
247 |
248 |
249 | ${piece} grid
250 |
258 |
259 |
260 |
261 | ${images}
262 |
263 |
264 |
265 | `);
266 | });
267 |
268 | app.get("/:piece/:width/:height/random.png", (req, res) => {
269 | const { piece, width, height } = req.params;
270 | const seed = Math.random() + "";
271 |
272 | sendArt(res, { piece, seed, width, height });
273 | });
274 |
275 | app.get("/:piece/:width/:height/:seed.png", (req, res) => {
276 | const { piece, seed, width, height } = req.params;
277 | sendArt(res, { piece, seed, width, height });
278 | });
279 |
280 | app.get("/:piece/:seed.png", (req, res) => {
281 | const { piece, seed } = req.params;
282 | sendArt(res, { piece, seed, width: 1320, height: 1320 });
283 | });
284 |
285 | const port = process.env.VIRTUAL_PORT || 3000;
286 | app.listen(port, "0.0.0.0", () => {
287 | console.log(`Listening on http://localhost:${port}`);
288 | });
289 |
--------------------------------------------------------------------------------
/art/element-markov.js:
--------------------------------------------------------------------------------
1 | // Generated with the help of https://github.com/jdan/markov.rb
2 | //
3 | // m = Markov.new(options[:n])
4 | // names = STDIN.read.split.map &:chomp
5 | // names.each do |name|
6 | // m.digest(name)
7 | // end
8 | //
9 | // + require 'json'
10 | // + puts m.chain.to_json
11 |
12 | exports.chain = {
13 | start: [
14 | "hy",
15 | "he",
16 | "li",
17 | "be",
18 | "bo",
19 | "ca",
20 | "ni",
21 | "ox",
22 | "fl",
23 | "ne",
24 | "so",
25 | "ma",
26 | "al",
27 | "si",
28 | "ph",
29 | "su",
30 | "ch",
31 | "ar",
32 | "po",
33 | "ca",
34 | "sc",
35 | "ti",
36 | "va",
37 | "ch",
38 | "ma",
39 | "ir",
40 | "co",
41 | "ni",
42 | "co",
43 | "zi",
44 | "ga",
45 | "ge",
46 | "ar",
47 | "se",
48 | "br",
49 | "kr",
50 | "ru",
51 | "st",
52 | "yt",
53 | "zi",
54 | "ni",
55 | "mo",
56 | "te",
57 | "ru",
58 | "rh",
59 | "pa",
60 | "si",
61 | "ca",
62 | "in",
63 | "ti",
64 | "an",
65 | "te",
66 | "io",
67 | "xe",
68 | "ca",
69 | "ba",
70 | "la",
71 | "ce",
72 | "pr",
73 | "ne",
74 | "pr",
75 | "sa",
76 | "eu",
77 | "ga",
78 | "te",
79 | "dy",
80 | "ho",
81 | "er",
82 | "th",
83 | "yt",
84 | "lu",
85 | "ha",
86 | "ta",
87 | "tu",
88 | "rh",
89 | "os",
90 | "ir",
91 | "pl",
92 | "go",
93 | "me",
94 | "th",
95 | "le",
96 | "bi",
97 | "po",
98 | "as",
99 | "ra",
100 | "fr",
101 | "ra",
102 | "ac",
103 | "th",
104 | "pr",
105 | "ur",
106 | "ne",
107 | "pl",
108 | "am",
109 | "cu",
110 | "be",
111 | "ca",
112 | "ei",
113 | "fe",
114 | "me",
115 | "no",
116 | "la",
117 | "ru",
118 | "du",
119 | "se",
120 | "bo",
121 | "ha",
122 | "me",
123 | "da",
124 | "ro",
125 | "co",
126 | "ni",
127 | "fl",
128 | "mo",
129 | "li",
130 | "te",
131 | "og",
132 | ],
133 | hy: ["yd"],
134 | yd: ["dr"],
135 | dr: ["ro"],
136 | ro: [
137 | "og",
138 | "on",
139 | "og",
140 | "om",
141 | "on",
142 | "om",
143 | "on",
144 | "om",
145 | "op",
146 | "os",
147 | "ot",
148 | "oe",
149 | "ov",
150 | ],
151 | og: ["ge", "ge", "ga"],
152 | ge: ["en", "en", "en", "er", "en"],
153 | en: [
154 | "end",
155 | "end",
156 | "end",
157 | "ni",
158 | "ni",
159 | "nu",
160 | "ni",
161 | "no",
162 | "end",
163 | "ni",
164 | "nd",
165 | "nc",
166 | "nt",
167 | "ni",
168 | "nn",
169 | ],
170 | he: ["el", "en", "en", "er"],
171 | el: ["li", "end", "le", "ll", "li", "le", "li"],
172 | li: ["iu", "it", "iu", "ic", "iu", "in", "iu", "iu", "iu", "if", "iu", "iv"],
173 | iu: [
174 | "um",
175 | "um",
176 | "um",
177 | "um",
178 | "um",
179 | "um",
180 | "um",
181 | "um",
182 | "um",
183 | "um",
184 | "um",
185 | "um",
186 | "um",
187 | "um",
188 | "um",
189 | "um",
190 | "um",
191 | "um",
192 | "um",
193 | "um",
194 | "um",
195 | "um",
196 | "um",
197 | "um",
198 | "um",
199 | "um",
200 | "um",
201 | "um",
202 | "um",
203 | "um",
204 | "um",
205 | "um",
206 | "um",
207 | "um",
208 | "um",
209 | "um",
210 | "um",
211 | "um",
212 | "um",
213 | "um",
214 | "um",
215 | "um",
216 | "um",
217 | "um",
218 | "um",
219 | "um",
220 | "um",
221 | "um",
222 | "um",
223 | "um",
224 | "um",
225 | "um",
226 | "um",
227 | "um",
228 | "um",
229 | "um",
230 | "um",
231 | "um",
232 | "um",
233 | "um",
234 | "um",
235 | "um",
236 | "um",
237 | "um",
238 | "um",
239 | "um",
240 | "um",
241 | "um",
242 | "um",
243 | "um",
244 | "um",
245 | "um",
246 | "um",
247 | "um",
248 | "um",
249 | "um",
250 | "um",
251 | "um",
252 | "um",
253 | ],
254 | um: [
255 | "end",
256 | "end",
257 | "end",
258 | "end",
259 | "end",
260 | "mi",
261 | "end",
262 | "end",
263 | "end",
264 | "end",
265 | "end",
266 | "end",
267 | "end",
268 | "end",
269 | "end",
270 | "end",
271 | "end",
272 | "end",
273 | "end",
274 | "end",
275 | "end",
276 | "end",
277 | "end",
278 | "end",
279 | "end",
280 | "end",
281 | "end",
282 | "end",
283 | "end",
284 | "end",
285 | "end",
286 | "end",
287 | "end",
288 | "end",
289 | "end",
290 | "end",
291 | "end",
292 | "end",
293 | "end",
294 | "end",
295 | "end",
296 | "end",
297 | "end",
298 | "end",
299 | "end",
300 | "end",
301 | "end",
302 | "end",
303 | "end",
304 | "end",
305 | "end",
306 | "end",
307 | "end",
308 | "end",
309 | "end",
310 | "end",
311 | "end",
312 | "end",
313 | "end",
314 | "end",
315 | "end",
316 | "end",
317 | "end",
318 | "end",
319 | "end",
320 | "end",
321 | "end",
322 | "end",
323 | "end",
324 | "end",
325 | "end",
326 | "end",
327 | "end",
328 | "end",
329 | "end",
330 | "end",
331 | "end",
332 | "end",
333 | "end",
334 | "end",
335 | "end",
336 | "end",
337 | "end",
338 | "end",
339 | ],
340 | it: ["th", "tr", "ta", "tn"],
341 | th: ["hi", "he", "ha", "hi", "hu", "ha", "end", "ho", "he"],
342 | hi: ["iu", "iu"],
343 | be: ["er", "er", "el"],
344 | er: [
345 | "ry",
346 | "end",
347 | "rm",
348 | "end",
349 | "ri",
350 | "rb",
351 | "rb",
352 | "rb",
353 | "rc",
354 | "ri",
355 | "rk",
356 | "rm",
357 | "rf",
358 | "ri",
359 | "rn",
360 | "ro",
361 | "rm",
362 | ],
363 | ry: ["yl", "yp", "end"],
364 | yl: ["ll"],
365 | ll: ["li", "li", "la", "lu", "li"],
366 | bo: ["or", "on", "or", "oh"],
367 | or: ["ro", "ri", "ru", "ri", "ri", "rn", "rd", "rg", "ri"],
368 | on: [
369 | "end",
370 | "end",
371 | "end",
372 | "end",
373 | "end",
374 | "end",
375 | "end",
376 | "nt",
377 | "ni",
378 | "ny",
379 | "end",
380 | "ni",
381 | "end",
382 | "ni",
383 | "ni",
384 | "end",
385 | ],
386 | ca: ["ar", "al", "an", "ad", "ae", "al"],
387 | ar: ["rb", "rg", "rs", "ri", "ri", "rm"],
388 | rb: ["bo", "bi", "bi", "bi"],
389 | ni: [
390 | "it",
391 | "iu",
392 | "iu",
393 | "ic",
394 | "iu",
395 | "ic",
396 | "iu",
397 | "iu",
398 | "io",
399 | "iu",
400 | "iu",
401 | "iu",
402 | "iu",
403 | "iu",
404 | "iu",
405 | "iu",
406 | "iu",
407 | "iu",
408 | "iu",
409 | "iu",
410 | "iu",
411 | "iu",
412 | "iu",
413 | "ic",
414 | "ih",
415 | "iu",
416 | ],
417 | tr: ["ro", "ro", "ri"],
418 | ox: ["xy"],
419 | xy: ["yg"],
420 | yg: ["ge"],
421 | fl: ["lu", "le"],
422 | lu: ["uo", "um", "ur", "ut", "um", "ut"],
423 | uo: ["or"],
424 | ri: [
425 | "in",
426 | "in",
427 | "iu",
428 | "iu",
429 | "iu",
430 | "iu",
431 | "iu",
432 | "id",
433 | "iu",
434 | "ic",
435 | "iu",
436 | "iu",
437 | "iu",
438 | "iu",
439 | ],
440 | in: [
441 | "ne",
442 | "ni",
443 | "ne",
444 | "nc",
445 | "ne",
446 | "nd",
447 | "end",
448 | "ne",
449 | "ni",
450 | "nu",
451 | "ne",
452 | "ni",
453 | "ni",
454 | "ns",
455 | "ni",
456 | "ne",
457 | ],
458 | ne: [
459 | "end",
460 | "eo",
461 | "es",
462 | "end",
463 | "es",
464 | "end",
465 | "et",
466 | "end",
467 | "eo",
468 | "end",
469 | "ep",
470 | "er",
471 | "es",
472 | "end",
473 | "es",
474 | ],
475 | eo: ["on", "od", "od"],
476 | so: ["od", "on"],
477 | od: ["di", "di", "di", "dy", "dy"],
478 | di: ["iu", "iu", "iu", "iu", "iu", "iu", "iu", "in", "iu", "iu", "iu"],
479 | ma: ["ag", "an", "an", "ar"],
480 | ag: ["gn"],
481 | gn: ["ne"],
482 | es: ["si", "se", "si", "ss", "ss"],
483 | si: ["iu", "il", "iu", "il", "iu", "iu", "iu", "in"],
484 | al: ["lu", "lc", "lt", "ll", "ll", "lu", "ll", "li"],
485 | mi: ["in", "iu", "in", "iu", "iu", "iu", "iu", "iu", "iu"],
486 | il: ["li", "lv"],
487 | ic: ["co", "ck", "end", "ci", "ci"],
488 | co: ["on", "ob", "op", "on", "op", "ov"],
489 | ph: ["ho", "ho"],
490 | ho: ["os", "or", "od", "ol", "or", "on"],
491 | os: ["sp", "si", "sm", "sc"],
492 | sp: ["ph", "pr"],
493 | ru: ["us", "ub", "ut", "ut"],
494 | us: ["end"],
495 | su: ["ul"],
496 | ul: ["lf", "li"],
497 | lf: ["fu"],
498 | fu: ["ur"],
499 | ur: ["end", "ri", "ro", "ry", "ra", "ri"],
500 | ch: ["hl", "hr", "hn"],
501 | hl: ["lo"],
502 | lo: ["or", "on"],
503 | rg: ["go", "gi"],
504 | go: ["on", "ol"],
505 | po: ["ot", "ol"],
506 | ot: ["ta", "ta"],
507 | ta: ["as", "an", "an", "al", "at", "ac", "ad"],
508 | as: ["ss", "se", "st", "ss"],
509 | ss: ["si", "si", "si", "so"],
510 | lc: ["ci"],
511 | ci: ["iu", "iu", "iu", "iu", "iu"],
512 | sc: ["ca", "co"],
513 | an: [
514 | "nd",
515 | "ni",
516 | "na",
517 | "ng",
518 | "ne",
519 | "ni",
520 | "nt",
521 | "nt",
522 | "nu",
523 | "nt",
524 | "nc",
525 | "ni",
526 | "ne",
527 | ],
528 | nd: ["di", "di", "de"],
529 | ti: ["it", "iu", "iu", "in", "im", "iu", "in", "in", "in", "in", "iu"],
530 | va: ["an"],
531 | na: ["ad"],
532 | ad: ["di", "di", "dm", "do", "end", "do", "di", "dt"],
533 | hr: ["ro", "ri"],
534 | om: ["mi", "mi", "me"],
535 | ng: ["ga", "gs"],
536 | ga: ["an", "al", "ad", "an"],
537 | se: ["end", "en", "el", "eo", "ea"],
538 | ir: ["ro", "rc", "ri"],
539 | ob: ["ba", "bi", "be"],
540 | ba: ["al", "ar"],
541 | lt: ["end"],
542 | ck: ["ke"],
543 | ke: ["el", "el"],
544 | op: ["pp", "pi", "pe"],
545 | pp: ["pe"],
546 | pe: ["er", "er"],
547 | zi: ["in", "ir"],
548 | nc: ["end", "ci", "ci"],
549 | rm: ["ma", "mi", "ms", "mo"],
550 | rs: ["se"],
551 | le: ["en", "ea", "ev", "er"],
552 | br: ["ro"],
553 | kr: ["ry"],
554 | yp: ["pt"],
555 | pt: ["to", "tu"],
556 | to: ["on", "on"],
557 | ub: ["bi", "bn"],
558 | bi: ["id", "iu", "iu", "iu", "iu", "is"],
559 | id: ["di", "di"],
560 | st: ["tr", "te", "ta", "te", "ta"],
561 | nt: ["ti", "ti", "th", "ta", "tg"],
562 | yt: ["tt", "tt"],
563 | tt: ["tr", "te"],
564 | rc: ["co", "cu"],
565 | io: ["ob", "od"],
566 | mo: ["ol", "on", "os", "or"],
567 | ol: ["ly", "li", "lm", "ld", "lo"],
568 | ly: ["yb"],
569 | yb: ["bd"],
570 | bd: ["de"],
571 | de: ["en", "el"],
572 | nu: ["um", "um", "um"],
573 | te: ["ec", "el", "er", "er", "et", "en", "ei", "en"],
574 | ec: ["ch"],
575 | hn: ["ne"],
576 | et: ["ti", "th", "ti"],
577 | ut: ["th", "te", "th", "to", "th"],
578 | rh: ["ho", "he"],
579 | pa: ["al"],
580 | la: ["ad", "an", "at", "aw"],
581 | lv: ["ve"],
582 | ve: ["er", "er"],
583 | dm: ["mi"],
584 | im: ["mo"],
585 | ny: ["end"],
586 | xe: ["en"],
587 | no: ["on", "ob"],
588 | ae: ["es"],
589 | ha: ["an", "af", "al", "as"],
590 | ce: ["er"],
591 | pr: ["ra", "ro", "ro", "ro"],
592 | ra: ["as", "ad", "an", "ad", "an"],
593 | dy: ["ym", "ym", "ys"],
594 | ym: ["mi", "mi"],
595 | me: ["et", "er", "er", "en", "ei"],
596 | sa: ["am"],
597 | am: ["ma", "me"],
598 | eu: ["ur"],
599 | pi: ["iu"],
600 | do: ["ol", "on"],
601 | ys: ["sp"],
602 | lm: ["mi"],
603 | hu: ["ul"],
604 | af: ["fn"],
605 | fn: ["ni"],
606 | tu: ["un", "un"],
607 | un: ["ng", "ni"],
608 | gs: ["st"],
609 | sm: ["mi", "mu"],
610 | pl: ["la", "lu"],
611 | at: ["ti", "ti"],
612 | ld: ["end"],
613 | cu: ["ur", "ur"],
614 | ea: ["ad", "ab"],
615 | is: ["sm"],
616 | mu: ["ut"],
617 | fr: ["ra"],
618 | ac: ["ct", "ct"],
619 | ct: ["ti", "ti"],
620 | ep: ["pt"],
621 | rk: ["ke"],
622 | if: ["fo"],
623 | fo: ["or", "or"],
624 | rn: ["ni", "ni"],
625 | ei: ["in", "in", "it"],
626 | ns: ["st"],
627 | fe: ["er"],
628 | ev: ["vi"],
629 | vi: ["iu", "iu", "iu"],
630 | aw: ["wr"],
631 | wr: ["re"],
632 | re: ["en"],
633 | rf: ["fo"],
634 | rd: ["di"],
635 | du: ["ub"],
636 | bn: ["ni"],
637 | ab: ["bo"],
638 | gi: ["iu"],
639 | oh: ["hr"],
640 | tn: ["ne"],
641 | da: ["ar"],
642 | ms: ["st"],
643 | dt: ["ti"],
644 | oe: ["en"],
645 | tg: ["ge"],
646 | ih: ["ho"],
647 | ov: ["vi", "vi"],
648 | iv: ["ve"],
649 | nn: ["ne"],
650 | };
651 |
652 | exports.elements = new Set([
653 | "hydrogen",
654 | "helium",
655 | "lithium",
656 | "beryllium",
657 | "boron",
658 | "carbon",
659 | "nitrogen",
660 | "oxygen",
661 | "fluorine",
662 | "neon",
663 | "sodium",
664 | "magnesium",
665 | "aluminium",
666 | "silicon",
667 | "phosphorus",
668 | "sulfur",
669 | "chlorine",
670 | "argon",
671 | "potassium",
672 | "calcium",
673 | "scandium",
674 | "titanium",
675 | "vanadium",
676 | "chromium",
677 | "manganese",
678 | "iron",
679 | "cobalt",
680 | "nickel",
681 | "copper",
682 | "zinc",
683 | "gallium",
684 | "germanium",
685 | "arsenic",
686 | "selenium",
687 | "bromine",
688 | "krypton",
689 | "rubidium",
690 | "strontium",
691 | "yttrium",
692 | "zirconium",
693 | "niobium",
694 | "molybdenum",
695 | "technetium",
696 | "ruthenium",
697 | "rhodium",
698 | "palladium",
699 | "silver",
700 | "cadmium",
701 | "indium",
702 | "tin",
703 | "antimony",
704 | "tellurium",
705 | "iodine",
706 | "xenon",
707 | "caesium",
708 | "barium",
709 | "lanthanum",
710 | "cerium",
711 | "praseodymium",
712 | "neodymium",
713 | "promethium",
714 | "samarium",
715 | "europium",
716 | "gadolinium",
717 | "terbium",
718 | "dysprosium",
719 | "holmium",
720 | "erbium",
721 | "thulium",
722 | "ytterbium",
723 | "lutetium",
724 | "hafnium",
725 | "tantalum",
726 | "tungsten",
727 | "rhenium",
728 | "osmium",
729 | "iridium",
730 | "platinum",
731 | "gold",
732 | "mercury",
733 | "thallium",
734 | "lead",
735 | "bismuth",
736 | "polonium",
737 | "astatine",
738 | "radon",
739 | "francium",
740 | "radium",
741 | "actinium",
742 | "thorium",
743 | "protactinium",
744 | "uranium",
745 | "neptunium",
746 | "plutonium",
747 | "americium",
748 | "curium",
749 | "berkelium",
750 | "californium",
751 | "einsteinium",
752 | "fermium",
753 | "mendelevium",
754 | "nobelium",
755 | "lawrencium",
756 | "rutherfordium",
757 | "dubnium",
758 | "seaborgium",
759 | "bohrium",
760 | "hassium",
761 | "meitnerium",
762 | "darmstadtium",
763 | "roentgenium",
764 | "copernicium",
765 | "nihonium",
766 | "flerovium",
767 | "moscovium",
768 | "livermorium",
769 | "tennessine",
770 | "oganesson",
771 | ]);
772 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 2.1, February 1999
3 |
4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc.
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | [This is the first released version of the Lesser GPL. It also counts
10 | as the successor of the GNU Library Public License, version 2, hence
11 | the version number 2.1.]
12 |
13 | Preamble
14 |
15 | The licenses for most software are designed to take away your
16 | freedom to share and change it. By contrast, the GNU General Public
17 | Licenses are intended to guarantee your freedom to share and change
18 | free software--to make sure the software is free for all its users.
19 |
20 | This license, the Lesser General Public License, applies to some
21 | specially designated software packages--typically libraries--of the
22 | Free Software Foundation and other authors who decide to use it. You
23 | can use it too, but we suggest you first think carefully about whether
24 | this license or the ordinary General Public License is the better
25 | strategy to use in any particular case, based on the explanations below.
26 |
27 | When we speak of free software, we are referring to freedom of use,
28 | not price. Our General Public Licenses are designed to make sure that
29 | you have the freedom to distribute copies of free software (and charge
30 | for this service if you wish); that you receive source code or can get
31 | it if you want it; that you can change the software and use pieces of
32 | it in new free programs; and that you are informed that you can do
33 | these things.
34 |
35 | To protect your rights, we need to make restrictions that forbid
36 | distributors to deny you these rights or to ask you to surrender these
37 | rights. These restrictions translate to certain responsibilities for
38 | you if you distribute copies of the library or if you modify it.
39 |
40 | For example, if you distribute copies of the library, whether gratis
41 | or for a fee, you must give the recipients all the rights that we gave
42 | you. You must make sure that they, too, receive or can get the source
43 | code. If you link other code with the library, you must provide
44 | complete object files to the recipients, so that they can relink them
45 | with the library after making changes to the library and recompiling
46 | it. And you must show them these terms so they know their rights.
47 |
48 | We protect your rights with a two-step method: (1) we copyright the
49 | library, and (2) we offer you this license, which gives you legal
50 | permission to copy, distribute and/or modify the library.
51 |
52 | To protect each distributor, we want to make it very clear that
53 | there is no warranty for the free library. Also, if the library is
54 | modified by someone else and passed on, the recipients should know
55 | that what they have is not the original version, so that the original
56 | author's reputation will not be affected by problems that might be
57 | introduced by others.
58 |
59 | Finally, software patents pose a constant threat to the existence of
60 | any free program. We wish to make sure that a company cannot
61 | effectively restrict the users of a free program by obtaining a
62 | restrictive license from a patent holder. Therefore, we insist that
63 | any patent license obtained for a version of the library must be
64 | consistent with the full freedom of use specified in this license.
65 |
66 | Most GNU software, including some libraries, is covered by the
67 | ordinary GNU General Public License. This license, the GNU Lesser
68 | General Public License, applies to certain designated libraries, and
69 | is quite different from the ordinary General Public License. We use
70 | this license for certain libraries in order to permit linking those
71 | libraries into non-free programs.
72 |
73 | When a program is linked with a library, whether statically or using
74 | a shared library, the combination of the two is legally speaking a
75 | combined work, a derivative of the original library. The ordinary
76 | General Public License therefore permits such linking only if the
77 | entire combination fits its criteria of freedom. The Lesser General
78 | Public License permits more lax criteria for linking other code with
79 | the library.
80 |
81 | We call this license the "Lesser" General Public License because it
82 | does Less to protect the user's freedom than the ordinary General
83 | Public License. It also provides other free software developers Less
84 | of an advantage over competing non-free programs. These disadvantages
85 | are the reason we use the ordinary General Public License for many
86 | libraries. However, the Lesser license provides advantages in certain
87 | special circumstances.
88 |
89 | For example, on rare occasions, there may be a special need to
90 | encourage the widest possible use of a certain library, so that it becomes
91 | a de-facto standard. To achieve this, non-free programs must be
92 | allowed to use the library. A more frequent case is that a free
93 | library does the same job as widely used non-free libraries. In this
94 | case, there is little to gain by limiting the free library to free
95 | software only, so we use the Lesser General Public License.
96 |
97 | In other cases, permission to use a particular library in non-free
98 | programs enables a greater number of people to use a large body of
99 | free software. For example, permission to use the GNU C Library in
100 | non-free programs enables many more people to use the whole GNU
101 | operating system, as well as its variant, the GNU/Linux operating
102 | system.
103 |
104 | Although the Lesser General Public License is Less protective of the
105 | users' freedom, it does ensure that the user of a program that is
106 | linked with the Library has the freedom and the wherewithal to run
107 | that program using a modified version of the Library.
108 |
109 | The precise terms and conditions for copying, distribution and
110 | modification follow. Pay close attention to the difference between a
111 | "work based on the library" and a "work that uses the library". The
112 | former contains code derived from the library, whereas the latter must
113 | be combined with the library in order to run.
114 |
115 | GNU LESSER GENERAL PUBLIC LICENSE
116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
117 |
118 | 0. This License Agreement applies to any software library or other
119 | program which contains a notice placed by the copyright holder or
120 | other authorized party saying it may be distributed under the terms of
121 | this Lesser General Public License (also called "this License").
122 | Each licensee is addressed as "you".
123 |
124 | A "library" means a collection of software functions and/or data
125 | prepared so as to be conveniently linked with application programs
126 | (which use some of those functions and data) to form executables.
127 |
128 | The "Library", below, refers to any such software library or work
129 | which has been distributed under these terms. A "work based on the
130 | Library" means either the Library or any derivative work under
131 | copyright law: that is to say, a work containing the Library or a
132 | portion of it, either verbatim or with modifications and/or translated
133 | straightforwardly into another language. (Hereinafter, translation is
134 | included without limitation in the term "modification".)
135 |
136 | "Source code" for a work means the preferred form of the work for
137 | making modifications to it. For a library, complete source code means
138 | all the source code for all modules it contains, plus any associated
139 | interface definition files, plus the scripts used to control compilation
140 | and installation of the library.
141 |
142 | Activities other than copying, distribution and modification are not
143 | covered by this License; they are outside its scope. The act of
144 | running a program using the Library is not restricted, and output from
145 | such a program is covered only if its contents constitute a work based
146 | on the Library (independent of the use of the Library in a tool for
147 | writing it). Whether that is true depends on what the Library does
148 | and what the program that uses the Library does.
149 |
150 | 1. You may copy and distribute verbatim copies of the Library's
151 | complete source code as you receive it, in any medium, provided that
152 | you conspicuously and appropriately publish on each copy an
153 | appropriate copyright notice and disclaimer of warranty; keep intact
154 | all the notices that refer to this License and to the absence of any
155 | warranty; and distribute a copy of this License along with the
156 | Library.
157 |
158 | You may charge a fee for the physical act of transferring a copy,
159 | and you may at your option offer warranty protection in exchange for a
160 | fee.
161 |
162 | 2. You may modify your copy or copies of the Library or any portion
163 | of it, thus forming a work based on the Library, and copy and
164 | distribute such modifications or work under the terms of Section 1
165 | above, provided that you also meet all of these conditions:
166 |
167 | a) The modified work must itself be a software library.
168 |
169 | b) You must cause the files modified to carry prominent notices
170 | stating that you changed the files and the date of any change.
171 |
172 | c) You must cause the whole of the work to be licensed at no
173 | charge to all third parties under the terms of this License.
174 |
175 | d) If a facility in the modified Library refers to a function or a
176 | table of data to be supplied by an application program that uses
177 | the facility, other than as an argument passed when the facility
178 | is invoked, then you must make a good faith effort to ensure that,
179 | in the event an application does not supply such function or
180 | table, the facility still operates, and performs whatever part of
181 | its purpose remains meaningful.
182 |
183 | (For example, a function in a library to compute square roots has
184 | a purpose that is entirely well-defined independent of the
185 | application. Therefore, Subsection 2d requires that any
186 | application-supplied function or table used by this function must
187 | be optional: if the application does not supply it, the square
188 | root function must still compute square roots.)
189 |
190 | These requirements apply to the modified work as a whole. If
191 | identifiable sections of that work are not derived from the Library,
192 | and can be reasonably considered independent and separate works in
193 | themselves, then this License, and its terms, do not apply to those
194 | sections when you distribute them as separate works. But when you
195 | distribute the same sections as part of a whole which is a work based
196 | on the Library, the distribution of the whole must be on the terms of
197 | this License, whose permissions for other licensees extend to the
198 | entire whole, and thus to each and every part regardless of who wrote
199 | it.
200 |
201 | Thus, it is not the intent of this section to claim rights or contest
202 | your rights to work written entirely by you; rather, the intent is to
203 | exercise the right to control the distribution of derivative or
204 | collective works based on the Library.
205 |
206 | In addition, mere aggregation of another work not based on the Library
207 | with the Library (or with a work based on the Library) on a volume of
208 | a storage or distribution medium does not bring the other work under
209 | the scope of this License.
210 |
211 | 3. You may opt to apply the terms of the ordinary GNU General Public
212 | License instead of this License to a given copy of the Library. To do
213 | this, you must alter all the notices that refer to this License, so
214 | that they refer to the ordinary GNU General Public License, version 2,
215 | instead of to this License. (If a newer version than version 2 of the
216 | ordinary GNU General Public License has appeared, then you can specify
217 | that version instead if you wish.) Do not make any other change in
218 | these notices.
219 |
220 | Once this change is made in a given copy, it is irreversible for
221 | that copy, so the ordinary GNU General Public License applies to all
222 | subsequent copies and derivative works made from that copy.
223 |
224 | This option is useful when you wish to copy part of the code of
225 | the Library into a program that is not a library.
226 |
227 | 4. You may copy and distribute the Library (or a portion or
228 | derivative of it, under Section 2) in object code or executable form
229 | under the terms of Sections 1 and 2 above provided that you accompany
230 | it with the complete corresponding machine-readable source code, which
231 | must be distributed under the terms of Sections 1 and 2 above on a
232 | medium customarily used for software interchange.
233 |
234 | If distribution of object code is made by offering access to copy
235 | from a designated place, then offering equivalent access to copy the
236 | source code from the same place satisfies the requirement to
237 | distribute the source code, even though third parties are not
238 | compelled to copy the source along with the object code.
239 |
240 | 5. A program that contains no derivative of any portion of the
241 | Library, but is designed to work with the Library by being compiled or
242 | linked with it, is called a "work that uses the Library". Such a
243 | work, in isolation, is not a derivative work of the Library, and
244 | therefore falls outside the scope of this License.
245 |
246 | However, linking a "work that uses the Library" with the Library
247 | creates an executable that is a derivative of the Library (because it
248 | contains portions of the Library), rather than a "work that uses the
249 | library". The executable is therefore covered by this License.
250 | Section 6 states terms for distribution of such executables.
251 |
252 | When a "work that uses the Library" uses material from a header file
253 | that is part of the Library, the object code for the work may be a
254 | derivative work of the Library even though the source code is not.
255 | Whether this is true is especially significant if the work can be
256 | linked without the Library, or if the work is itself a library. The
257 | threshold for this to be true is not precisely defined by law.
258 |
259 | If such an object file uses only numerical parameters, data
260 | structure layouts and accessors, and small macros and small inline
261 | functions (ten lines or less in length), then the use of the object
262 | file is unrestricted, regardless of whether it is legally a derivative
263 | work. (Executables containing this object code plus portions of the
264 | Library will still fall under Section 6.)
265 |
266 | Otherwise, if the work is a derivative of the Library, you may
267 | distribute the object code for the work under the terms of Section 6.
268 | Any executables containing that work also fall under Section 6,
269 | whether or not they are linked directly with the Library itself.
270 |
271 | 6. As an exception to the Sections above, you may also combine or
272 | link a "work that uses the Library" with the Library to produce a
273 | work containing portions of the Library, and distribute that work
274 | under terms of your choice, provided that the terms permit
275 | modification of the work for the customer's own use and reverse
276 | engineering for debugging such modifications.
277 |
278 | You must give prominent notice with each copy of the work that the
279 | Library is used in it and that the Library and its use are covered by
280 | this License. You must supply a copy of this License. If the work
281 | during execution displays copyright notices, you must include the
282 | copyright notice for the Library among them, as well as a reference
283 | directing the user to the copy of this License. Also, you must do one
284 | of these things:
285 |
286 | a) Accompany the work with the complete corresponding
287 | machine-readable source code for the Library including whatever
288 | changes were used in the work (which must be distributed under
289 | Sections 1 and 2 above); and, if the work is an executable linked
290 | with the Library, with the complete machine-readable "work that
291 | uses the Library", as object code and/or source code, so that the
292 | user can modify the Library and then relink to produce a modified
293 | executable containing the modified Library. (It is understood
294 | that the user who changes the contents of definitions files in the
295 | Library will not necessarily be able to recompile the application
296 | to use the modified definitions.)
297 |
298 | b) Use a suitable shared library mechanism for linking with the
299 | Library. A suitable mechanism is one that (1) uses at run time a
300 | copy of the library already present on the user's computer system,
301 | rather than copying library functions into the executable, and (2)
302 | will operate properly with a modified version of the library, if
303 | the user installs one, as long as the modified version is
304 | interface-compatible with the version that the work was made with.
305 |
306 | c) Accompany the work with a written offer, valid for at
307 | least three years, to give the same user the materials
308 | specified in Subsection 6a, above, for a charge no more
309 | than the cost of performing this distribution.
310 |
311 | d) If distribution of the work is made by offering access to copy
312 | from a designated place, offer equivalent access to copy the above
313 | specified materials from the same place.
314 |
315 | e) Verify that the user has already received a copy of these
316 | materials or that you have already sent this user a copy.
317 |
318 | For an executable, the required form of the "work that uses the
319 | Library" must include any data and utility programs needed for
320 | reproducing the executable from it. However, as a special exception,
321 | the materials to be distributed need not include anything that is
322 | normally distributed (in either source or binary form) with the major
323 | components (compiler, kernel, and so on) of the operating system on
324 | which the executable runs, unless that component itself accompanies
325 | the executable.
326 |
327 | It may happen that this requirement contradicts the license
328 | restrictions of other proprietary libraries that do not normally
329 | accompany the operating system. Such a contradiction means you cannot
330 | use both them and the Library together in an executable that you
331 | distribute.
332 |
333 | 7. You may place library facilities that are a work based on the
334 | Library side-by-side in a single library together with other library
335 | facilities not covered by this License, and distribute such a combined
336 | library, provided that the separate distribution of the work based on
337 | the Library and of the other library facilities is otherwise
338 | permitted, and provided that you do these two things:
339 |
340 | a) Accompany the combined library with a copy of the same work
341 | based on the Library, uncombined with any other library
342 | facilities. This must be distributed under the terms of the
343 | Sections above.
344 |
345 | b) Give prominent notice with the combined library of the fact
346 | that part of it is a work based on the Library, and explaining
347 | where to find the accompanying uncombined form of the same work.
348 |
349 | 8. You may not copy, modify, sublicense, link with, or distribute
350 | the Library except as expressly provided under this License. Any
351 | attempt otherwise to copy, modify, sublicense, link with, or
352 | distribute the Library is void, and will automatically terminate your
353 | rights under this License. However, parties who have received copies,
354 | or rights, from you under this License will not have their licenses
355 | terminated so long as such parties remain in full compliance.
356 |
357 | 9. You are not required to accept this License, since you have not
358 | signed it. However, nothing else grants you permission to modify or
359 | distribute the Library or its derivative works. These actions are
360 | prohibited by law if you do not accept this License. Therefore, by
361 | modifying or distributing the Library (or any work based on the
362 | Library), you indicate your acceptance of this License to do so, and
363 | all its terms and conditions for copying, distributing or modifying
364 | the Library or works based on it.
365 |
366 | 10. Each time you redistribute the Library (or any work based on the
367 | Library), the recipient automatically receives a license from the
368 | original licensor to copy, distribute, link with or modify the Library
369 | subject to these terms and conditions. You may not impose any further
370 | restrictions on the recipients' exercise of the rights granted herein.
371 | You are not responsible for enforcing compliance by third parties with
372 | this License.
373 |
374 | 11. If, as a consequence of a court judgment or allegation of patent
375 | infringement or for any other reason (not limited to patent issues),
376 | conditions are imposed on you (whether by court order, agreement or
377 | otherwise) that contradict the conditions of this License, they do not
378 | excuse you from the conditions of this License. If you cannot
379 | distribute so as to satisfy simultaneously your obligations under this
380 | License and any other pertinent obligations, then as a consequence you
381 | may not distribute the Library at all. For example, if a patent
382 | license would not permit royalty-free redistribution of the Library by
383 | all those who receive copies directly or indirectly through you, then
384 | the only way you could satisfy both it and this License would be to
385 | refrain entirely from distribution of the Library.
386 |
387 | If any portion of this section is held invalid or unenforceable under any
388 | particular circumstance, the balance of the section is intended to apply,
389 | and the section as a whole is intended to apply in other circumstances.
390 |
391 | It is not the purpose of this section to induce you to infringe any
392 | patents or other property right claims or to contest validity of any
393 | such claims; this section has the sole purpose of protecting the
394 | integrity of the free software distribution system which is
395 | implemented by public license practices. Many people have made
396 | generous contributions to the wide range of software distributed
397 | through that system in reliance on consistent application of that
398 | system; it is up to the author/donor to decide if he or she is willing
399 | to distribute software through any other system and a licensee cannot
400 | impose that choice.
401 |
402 | This section is intended to make thoroughly clear what is believed to
403 | be a consequence of the rest of this License.
404 |
405 | 12. If the distribution and/or use of the Library is restricted in
406 | certain countries either by patents or by copyrighted interfaces, the
407 | original copyright holder who places the Library under this License may add
408 | an explicit geographical distribution limitation excluding those countries,
409 | so that distribution is permitted only in or among countries not thus
410 | excluded. In such case, this License incorporates the limitation as if
411 | written in the body of this License.
412 |
413 | 13. The Free Software Foundation may publish revised and/or new
414 | versions of the Lesser General Public License from time to time.
415 | Such new versions will be similar in spirit to the present version,
416 | but may differ in detail to address new problems or concerns.
417 |
418 | Each version is given a distinguishing version number. If the Library
419 | specifies a version number of this License which applies to it and
420 | "any later version", you have the option of following the terms and
421 | conditions either of that version or of any later version published by
422 | the Free Software Foundation. If the Library does not specify a
423 | license version number, you may choose any version ever published by
424 | the Free Software Foundation.
425 |
426 | 14. If you wish to incorporate parts of the Library into other free
427 | programs whose distribution conditions are incompatible with these,
428 | write to the author to ask for permission. For software which is
429 | copyrighted by the Free Software Foundation, write to the Free
430 | Software Foundation; we sometimes make exceptions for this. Our
431 | decision will be guided by the two goals of preserving the free status
432 | of all derivatives of our free software and of promoting the sharing
433 | and reuse of software generally.
434 |
435 | NO WARRANTY
436 |
437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
446 |
447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
456 | DAMAGES.
457 |
458 | END OF TERMS AND CONDITIONS
459 |
460 | How to Apply These Terms to Your New Libraries
461 |
462 | If you develop a new library, and you want it to be of the greatest
463 | possible use to the public, we recommend making it free software that
464 | everyone can redistribute and change. You can do so by permitting
465 | redistribution under these terms (or, alternatively, under the terms of the
466 | ordinary General Public License).
467 |
468 | To apply these terms, attach the following notices to the library. It is
469 | safest to attach them to the start of each source file to most effectively
470 | convey the exclusion of warranty; and each file should have at least the
471 | "copyright" line and a pointer to where the full notice is found.
472 |
473 |
474 | Copyright (C)
475 |
476 | This library is free software; you can redistribute it and/or
477 | modify it under the terms of the GNU Lesser General Public
478 | License as published by the Free Software Foundation; either
479 | version 2.1 of the License, or (at your option) any later version.
480 |
481 | This library is distributed in the hope that it will be useful,
482 | but WITHOUT ANY WARRANTY; without even the implied warranty of
483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
484 | Lesser General Public License for more details.
485 |
486 | You should have received a copy of the GNU Lesser General Public
487 | License along with this library; if not, write to the Free Software
488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
489 | USA
490 |
491 | Also add information on how to contact you by electronic and paper mail.
492 |
493 | You should also get your employer (if you work as a programmer) or your
494 | school, if any, to sign a "copyright disclaimer" for the library, if
495 | necessary. Here is a sample; alter the names:
496 |
497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the
498 | library `Frob' (a library for tweaking knobs) written by James Random
499 | Hacker.
500 |
501 | , 1 April 1990
502 | Ty Coon, President of Vice
503 |
504 | That's all there is to it!
505 |
--------------------------------------------------------------------------------
/art/chess-pieces.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file was generated using
3 | * http://demo.qunee.com/svg2canvas/
4 | * Using the SVG files found in
5 | * https://github.com/ornicar/lila/tree/master/public/piece/cburnett
6 | */
7 |
8 | exports.chessPieces = {
9 | "bB.svg": {
10 | draw: function (ctx) {
11 | ctx.save();
12 | ctx.strokeStyle = "rgba(0,0,0,0)";
13 | ctx.miterLimit = 4;
14 | ctx.font = "";
15 | ctx.font = " 10px sans-serif";
16 | ctx.save();
17 | ctx.fillStyle = "rgba(0,0,0,0)";
18 | ctx.strokeStyle = "#000";
19 | ctx.lineWidth = 1.5;
20 | ctx.lineCap = "round";
21 | ctx.lineJoin = "round";
22 | ctx.font = " 10px sans-serif";
23 | ctx.save();
24 | ctx.fillStyle = "#000";
25 | ctx.strokeStyle = "#000";
26 | ctx.lineCap = "butt";
27 | ctx.font = " 10px sans-serif";
28 | ctx.save();
29 | ctx.fillStyle = "#000";
30 | ctx.strokeStyle = "#000";
31 | ctx.font = " 10px sans-serif";
32 | ctx.beginPath();
33 | ctx.moveTo(9, 36);
34 | ctx.bezierCurveTo(12.39, 35.03, 19.11, 36.43, 22.5, 34);
35 | ctx.bezierCurveTo(25.89, 36.43, 32.61, 35.03, 36, 36);
36 | ctx.bezierCurveTo(36, 36, 37.65, 36.54, 39, 38);
37 | ctx.bezierCurveTo(38.32, 38.97, 37.35, 38.99, 36, 38.5);
38 | ctx.bezierCurveTo(32.61, 37.53, 25.89, 38.96, 22.5, 37.5);
39 | ctx.bezierCurveTo(19.11, 38.96, 12.39, 37.53, 9, 38.5);
40 | ctx.bezierCurveTo(7.646, 38.99, 6.677, 38.97, 6, 38);
41 | ctx.bezierCurveTo(7.354, 36.06, 9, 36, 9, 36);
42 | ctx.closePath();
43 | ctx.fill("evenodd");
44 | ctx.stroke();
45 | ctx.restore();
46 | ctx.save();
47 | ctx.fillStyle = "#000";
48 | ctx.strokeStyle = "#000";
49 | ctx.font = " 10px sans-serif";
50 | ctx.beginPath();
51 | ctx.moveTo(15, 32);
52 | ctx.bezierCurveTo(17.5, 34.5, 27.5, 34.5, 30, 32);
53 | ctx.bezierCurveTo(30.5, 30.5, 30, 30, 30, 30);
54 | ctx.bezierCurveTo(30, 27.5, 27.5, 26, 27.5, 26);
55 | ctx.bezierCurveTo(33, 24.5, 33.5, 14.5, 22.5, 10.5);
56 | ctx.bezierCurveTo(11.5, 14.5, 12, 24.5, 17.5, 26);
57 | ctx.bezierCurveTo(17.5, 26, 15, 27.5, 15, 30);
58 | ctx.bezierCurveTo(15, 30, 14.5, 30.5, 15, 32);
59 | ctx.closePath();
60 | ctx.fill("evenodd");
61 | ctx.stroke();
62 | ctx.restore();
63 | ctx.save();
64 | ctx.fillStyle = "#000";
65 | ctx.strokeStyle = "#000";
66 | ctx.font = " 10px sans-serif";
67 | ctx.beginPath();
68 | ctx.moveTo(25, 8);
69 | ctx.translate(22.5, 8);
70 | ctx.rotate(0);
71 | ctx.arc(0, 0, 2.5, 0, 3.141592653589793, 0);
72 | ctx.rotate(0);
73 | ctx.translate(-22.5, -8);
74 | ctx.translate(22.5, 8);
75 | ctx.rotate(0);
76 | ctx.arc(0, 0, 2.5, 3.141592653589793, 6.283185307179586, 0);
77 | ctx.rotate(0);
78 | ctx.translate(-22.5, -8);
79 | ctx.fill("evenodd");
80 | ctx.stroke();
81 | ctx.restore();
82 | ctx.restore();
83 | ctx.save();
84 | ctx.fillStyle = "rgba(0,0,0,0)";
85 | ctx.strokeStyle = "#ececec";
86 | ctx.lineJoin = "miter";
87 | ctx.font = " 10px sans-serif";
88 | ctx.beginPath();
89 | ctx.moveTo(17.5, 26);
90 | ctx.lineTo(27.5, 26);
91 | ctx.moveTo(15, 30);
92 | ctx.lineTo(30, 30);
93 | ctx.moveTo(22.5, 15.5);
94 | ctx.lineTo(22.5, 20.5);
95 | ctx.moveTo(20, 18);
96 | ctx.lineTo(25, 18);
97 | ctx.fill("evenodd");
98 | ctx.stroke();
99 | ctx.restore();
100 | ctx.restore();
101 | ctx.restore();
102 | },
103 | },
104 | "bK.svg": {
105 | draw: function (ctx) {
106 | ctx.save();
107 | ctx.strokeStyle = "rgba(0,0,0,0)";
108 | ctx.miterLimit = 4;
109 | ctx.font = "";
110 | ctx.font = " 10px sans-serif";
111 | ctx.save();
112 | ctx.fillStyle = "rgba(0,0,0,0)";
113 | ctx.strokeStyle = "#000";
114 | ctx.lineWidth = 1.5;
115 | ctx.lineCap = "round";
116 | ctx.lineJoin = "round";
117 | ctx.font = " 10px sans-serif";
118 | ctx.save();
119 | ctx.fillStyle = "rgba(0,0,0,0)";
120 | ctx.strokeStyle = "#000";
121 | ctx.lineJoin = "miter";
122 | ctx.font = " 10px sans-serif";
123 | ctx.beginPath();
124 | ctx.moveTo(22.5, 11.63);
125 | ctx.lineTo(22.5, 6);
126 | ctx.fill("evenodd");
127 | ctx.stroke();
128 | ctx.restore();
129 | ctx.save();
130 | ctx.fillStyle = "#000";
131 | ctx.strokeStyle = "#000";
132 | ctx.lineCap = "butt";
133 | ctx.lineJoin = "miter";
134 | ctx.font = " 10px sans-serif";
135 | ctx.beginPath();
136 | ctx.moveTo(22.5, 25);
137 | ctx.bezierCurveTo(22.5, 25, 27, 17.5, 25.5, 14.5);
138 | ctx.bezierCurveTo(25.5, 14.5, 24.5, 12, 22.5, 12);
139 | ctx.bezierCurveTo(20.5, 12, 19.5, 14.5, 19.5, 14.5);
140 | ctx.bezierCurveTo(18, 17.5, 22.5, 25, 22.5, 25);
141 | ctx.fill("evenodd");
142 | ctx.stroke();
143 | ctx.restore();
144 | ctx.save();
145 | ctx.fillStyle = "#000";
146 | ctx.strokeStyle = "#000";
147 | ctx.font = " 10px sans-serif";
148 | ctx.beginPath();
149 | ctx.moveTo(11.5, 37);
150 | ctx.bezierCurveTo(17, 40.5, 27, 40.5, 32.5, 37);
151 | ctx.lineTo(32.5, 30);
152 | ctx.bezierCurveTo(32.5, 30, 41.5, 25.5, 38.5, 19.5);
153 | ctx.bezierCurveTo(34.5, 13, 25, 16, 22.5, 23.5);
154 | ctx.lineTo(22.5, 27);
155 | ctx.lineTo(22.5, 23.5);
156 | ctx.bezierCurveTo(19, 16, 9.5, 13, 6.5, 19.5);
157 | ctx.bezierCurveTo(3.5, 25.5, 11.5, 29.5, 11.5, 29.5);
158 | ctx.lineTo(11.5, 37);
159 | ctx.closePath();
160 | ctx.fill("evenodd");
161 | ctx.stroke();
162 | ctx.restore();
163 | ctx.save();
164 | ctx.fillStyle = "rgba(0,0,0,0)";
165 | ctx.strokeStyle = "#000";
166 | ctx.lineJoin = "miter";
167 | ctx.font = " 10px sans-serif";
168 | ctx.beginPath();
169 | ctx.moveTo(20, 8);
170 | ctx.lineTo(25, 8);
171 | ctx.fill("evenodd");
172 | ctx.stroke();
173 | ctx.restore();
174 | ctx.save();
175 | ctx.fillStyle = "rgba(0,0,0,0)";
176 | ctx.strokeStyle = "#ececec";
177 | ctx.font = " 10px sans-serif";
178 | ctx.beginPath();
179 | ctx.moveTo(32, 29.5);
180 | ctx.bezierCurveTo(32, 29.5, 40.5, 25.5, 38.03, 19.85);
181 | ctx.bezierCurveTo(34.15, 14, 25, 18, 22.5, 24.5);
182 | ctx.lineTo(22.51, 26.6);
183 | ctx.lineTo(22.5, 24.5);
184 | ctx.bezierCurveTo(20, 18, 9.906, 14, 6.997, 19.85);
185 | ctx.bezierCurveTo(4.5, 25.5, 11.85, 28.85, 11.85, 28.85);
186 | ctx.fill("evenodd");
187 | ctx.stroke();
188 | ctx.restore();
189 | ctx.save();
190 | ctx.fillStyle = "rgba(0,0,0,0)";
191 | ctx.strokeStyle = "#ececec";
192 | ctx.font = " 10px sans-serif";
193 | ctx.beginPath();
194 | ctx.moveTo(11.5, 30);
195 | ctx.bezierCurveTo(17, 27, 27, 27, 32.5, 30);
196 | ctx.moveTo(11.5, 33.5);
197 | ctx.bezierCurveTo(17, 30.5, 27, 30.5, 32.5, 33.5);
198 | ctx.moveTo(11.5, 37);
199 | ctx.bezierCurveTo(17, 34, 27, 34, 32.5, 37);
200 | ctx.fill("evenodd");
201 | ctx.stroke();
202 | ctx.restore();
203 | ctx.restore();
204 | ctx.restore();
205 | },
206 | },
207 | "bN.svg": {
208 | draw: function (ctx) {
209 | ctx.save();
210 | ctx.strokeStyle = "rgba(0,0,0,0)";
211 | ctx.miterLimit = 4;
212 | ctx.font = "";
213 | ctx.font = " 10px sans-serif";
214 | ctx.save();
215 | ctx.fillStyle = "rgba(0,0,0,0)";
216 | ctx.strokeStyle = "#000";
217 | ctx.lineWidth = 1.5;
218 | ctx.lineCap = "round";
219 | ctx.lineJoin = "round";
220 | ctx.font = " 10px sans-serif";
221 | ctx.save();
222 | ctx.fillStyle = "#000";
223 | ctx.strokeStyle = "#000";
224 | ctx.font = " 10px sans-serif";
225 | ctx.beginPath();
226 | ctx.moveTo(22, 10);
227 | ctx.bezierCurveTo(32.5, 11, 38.5, 18, 38, 39);
228 | ctx.lineTo(15, 39);
229 | ctx.bezierCurveTo(15, 30, 25, 32.5, 23, 18);
230 | ctx.fill("evenodd");
231 | ctx.stroke();
232 | ctx.restore();
233 | ctx.save();
234 | ctx.fillStyle = "#000";
235 | ctx.strokeStyle = "#000";
236 | ctx.font = " 10px sans-serif";
237 | ctx.beginPath();
238 | ctx.moveTo(24, 18);
239 | ctx.bezierCurveTo(24.38, 20.91, 18.45, 25.37, 16, 27);
240 | ctx.bezierCurveTo(13, 29, 13.18, 31.34, 11, 31);
241 | ctx.bezierCurveTo(9.958, 30.06, 12.41, 27.96, 11, 28);
242 | ctx.bezierCurveTo(10, 28, 11.19, 29.23, 10, 30);
243 | ctx.bezierCurveTo(9, 30, 5.997, 31, 6, 26);
244 | ctx.bezierCurveTo(6, 24, 12, 14, 12, 14);
245 | ctx.bezierCurveTo(12, 14, 13.89, 12.1, 14, 10.5);
246 | ctx.bezierCurveTo(13.27, 9.506, 13.5, 8.5, 13.5, 7.5);
247 | ctx.bezierCurveTo(14.5, 6.5, 16.5, 10, 16.5, 10);
248 | ctx.lineTo(18.5, 10);
249 | ctx.bezierCurveTo(18.5, 10, 19.28, 8.008, 21, 7);
250 | ctx.bezierCurveTo(22, 7, 22, 10, 22, 10);
251 | ctx.fill("evenodd");
252 | ctx.stroke();
253 | ctx.restore();
254 | ctx.save();
255 | ctx.fillStyle = "#ececec";
256 | ctx.strokeStyle = "#ececec";
257 | ctx.font = " 10px sans-serif";
258 | ctx.beginPath();
259 | ctx.moveTo(9.5, 25.5);
260 | ctx.translate(9, 25.5);
261 | ctx.rotate(0);
262 | ctx.arc(0, 0, 0.5, 0, 3.141592653589793, 0);
263 | ctx.rotate(0);
264 | ctx.translate(-9, -25.5);
265 | ctx.translate(9, 25.5);
266 | ctx.rotate(0);
267 | ctx.arc(0, 0, 0.5, 3.141592653589793, 6.283185307179586, 0);
268 | ctx.rotate(0);
269 | ctx.translate(-9, -25.5);
270 | ctx.moveTo(14.933, 15.75);
271 | ctx.translate(14.495025042545517, 15.508616850991054);
272 | ctx.rotate(0.5235987755982988);
273 | ctx.scale(0.3333333333333333, 1);
274 | ctx.arc(0, 0, 1.5, -0.006629074978609724, -3.1349551104967315, 0);
275 | ctx.scale(3, 1);
276 | ctx.rotate(-0.5235987755982988);
277 | ctx.translate(-14.495025042545517, -15.508616850991054);
278 | ctx.translate(14.504974957454483, 15.491383149008946);
279 | ctx.rotate(0.5235987755982988);
280 | ctx.scale(0.3333333333333333, 1);
281 | ctx.arc(0, 0, 1.5, 3.1349635786111834, 0.0066375430930616375, 0);
282 | ctx.scale(3, 1);
283 | ctx.rotate(-0.5235987755982988);
284 | ctx.translate(-14.504974957454483, -15.491383149008946);
285 | ctx.closePath();
286 | ctx.fill("evenodd");
287 | ctx.stroke();
288 | ctx.restore();
289 | ctx.save();
290 | ctx.fillStyle = "#ececec";
291 | ctx.strokeStyle = "rgba(0,0,0,0)";
292 | ctx.font = " 10px sans-serif";
293 | ctx.beginPath();
294 | ctx.moveTo(24.55, 10.4);
295 | ctx.lineTo(24.1, 11.85);
296 | ctx.lineTo(24.6, 12);
297 | ctx.bezierCurveTo(27.75, 13, 30.25, 14.49, 32.5, 18.75);
298 | ctx.bezierCurveTo(34.75, 23.009999999999998, 35.75, 29.06, 35.25, 39);
299 | ctx.lineTo(35.2, 39.5);
300 | ctx.lineTo(37.45, 39.5);
301 | ctx.lineTo(37.5, 39);
302 | ctx.bezierCurveTo(38, 28.939999999999998, 36.62, 22.15, 34.25, 17.66);
303 | ctx.bezierCurveTo(31.88, 13.17, 28.46, 11.02, 25.060000000000002, 10.5);
304 | ctx.lineTo(24.55, 10.4);
305 | ctx.closePath();
306 | ctx.fill("evenodd");
307 | ctx.stroke();
308 | ctx.restore();
309 | ctx.restore();
310 | ctx.restore();
311 | },
312 | },
313 | "bP.svg": {
314 | draw: function (ctx) {
315 | ctx.save();
316 | ctx.strokeStyle = "rgba(0,0,0,0)";
317 | ctx.miterLimit = 4;
318 | ctx.font = "";
319 | ctx.font = " 10px sans-serif";
320 | ctx.save();
321 | ctx.strokeStyle = "#000";
322 | ctx.lineWidth = 1.5;
323 | ctx.lineCap = "round";
324 | ctx.font = " 10px sans-serif";
325 | ctx.beginPath();
326 | ctx.moveTo(22.5, 9);
327 | ctx.bezierCurveTo(20.29, 9, 18.5, 10.79, 18.5, 13);
328 | ctx.bezierCurveTo(18.5, 13.89, 18.79, 14.71, 19.28, 15.379999999999999);
329 | ctx.bezierCurveTo(17.33, 16.5, 16, 18.59, 16, 21);
330 | ctx.bezierCurveTo(16, 23.03, 16.94, 24.84, 18.41, 26.03);
331 | ctx.bezierCurveTo(15.41, 27.09, 11, 31.580000000000002, 11, 39.5);
332 | ctx.lineTo(34, 39.5);
333 | ctx.bezierCurveTo(34, 31.58, 29.59, 27.09, 26.59, 26.03);
334 | ctx.bezierCurveTo(28.06, 24.84, 29, 23.03, 29, 21);
335 | ctx.bezierCurveTo(29, 18.59, 27.67, 16.5, 25.72, 15.379999999999999);
336 | ctx.bezierCurveTo(
337 | 26.209999999999997,
338 | 14.709999999999999,
339 | 26.5,
340 | 13.889999999999999,
341 | 26.5,
342 | 13
343 | );
344 | ctx.bezierCurveTo(26.5, 10.79, 24.71, 9, 22.5, 9);
345 | ctx.closePath();
346 | ctx.fill();
347 | ctx.stroke();
348 | ctx.restore();
349 | ctx.restore();
350 | },
351 | },
352 | "bQ.svg": {
353 | draw: function (ctx) {
354 | ctx.save();
355 | ctx.strokeStyle = "rgba(0,0,0,0)";
356 | ctx.miterLimit = 4;
357 | ctx.font = "";
358 | ctx.font = " 10px sans-serif";
359 | ctx.save();
360 | ctx.strokeStyle = "#000";
361 | ctx.lineWidth = 1.5;
362 | ctx.lineCap = "round";
363 | ctx.lineJoin = "round";
364 | ctx.font = " 10px sans-serif";
365 | ctx.save();
366 | ctx.strokeStyle = "rgba(0,0,0,0)";
367 | ctx.font = " 10px sans-serif";
368 | ctx.save();
369 | ctx.strokeStyle = "rgba(0,0,0,0)";
370 | ctx.font = " 10px sans-serif";
371 | ctx.beginPath();
372 | ctx.arc(6, 12, 2.75, 0, 6.283185307179586, false);
373 | ctx.closePath();
374 | ctx.fill("evenodd");
375 | ctx.stroke();
376 | ctx.restore();
377 | ctx.save();
378 | ctx.strokeStyle = "rgba(0,0,0,0)";
379 | ctx.font = " 10px sans-serif";
380 | ctx.beginPath();
381 | ctx.arc(14, 9, 2.75, 0, 6.283185307179586, false);
382 | ctx.closePath();
383 | ctx.fill("evenodd");
384 | ctx.stroke();
385 | ctx.restore();
386 | ctx.save();
387 | ctx.strokeStyle = "rgba(0,0,0,0)";
388 | ctx.font = " 10px sans-serif";
389 | ctx.beginPath();
390 | ctx.arc(22.5, 8, 2.75, 0, 6.283185307179586, false);
391 | ctx.closePath();
392 | ctx.fill("evenodd");
393 | ctx.stroke();
394 | ctx.restore();
395 | ctx.save();
396 | ctx.strokeStyle = "rgba(0,0,0,0)";
397 | ctx.font = " 10px sans-serif";
398 | ctx.beginPath();
399 | ctx.arc(31, 9, 2.75, 0, 6.283185307179586, false);
400 | ctx.closePath();
401 | ctx.fill("evenodd");
402 | ctx.stroke();
403 | ctx.restore();
404 | ctx.save();
405 | ctx.strokeStyle = "rgba(0,0,0,0)";
406 | ctx.font = " 10px sans-serif";
407 | ctx.beginPath();
408 | ctx.arc(39, 12, 2.75, 0, 6.283185307179586, false);
409 | ctx.closePath();
410 | ctx.fill("evenodd");
411 | ctx.stroke();
412 | ctx.restore();
413 | ctx.restore();
414 | ctx.save();
415 | ctx.strokeStyle = "#000";
416 | ctx.lineCap = "butt";
417 | ctx.font = " 10px sans-serif";
418 | ctx.beginPath();
419 | ctx.moveTo(9, 26);
420 | ctx.bezierCurveTo(17.5, 24.5, 30, 24.5, 36, 26);
421 | ctx.lineTo(38.5, 13.5);
422 | ctx.lineTo(31, 25);
423 | ctx.lineTo(30.7, 10.9);
424 | ctx.lineTo(25.5, 24.5);
425 | ctx.lineTo(22.5, 10);
426 | ctx.lineTo(19.5, 24.5);
427 | ctx.lineTo(14.3, 10.9);
428 | ctx.lineTo(14, 25);
429 | ctx.lineTo(6.5, 13.5);
430 | ctx.lineTo(9, 26);
431 | ctx.closePath();
432 | ctx.fill("evenodd");
433 | ctx.stroke();
434 | ctx.restore();
435 | ctx.save();
436 | ctx.strokeStyle = "#000";
437 | ctx.lineCap = "butt";
438 | ctx.font = " 10px sans-serif";
439 | ctx.beginPath();
440 | ctx.moveTo(9, 26);
441 | ctx.bezierCurveTo(9, 28, 10.5, 28, 11.5, 30);
442 | ctx.bezierCurveTo(12.5, 31.5, 12.5, 31, 12, 33.5);
443 | ctx.bezierCurveTo(10.5, 34.5, 10.5, 36, 10.5, 36);
444 | ctx.bezierCurveTo(9, 37.5, 11, 38.5, 11, 38.5);
445 | ctx.bezierCurveTo(17.5, 39.5, 27.5, 39.5, 34, 38.5);
446 | ctx.bezierCurveTo(34, 38.5, 35.5, 37.5, 34, 36);
447 | ctx.bezierCurveTo(34, 36, 34.5, 34.5, 33, 33.5);
448 | ctx.bezierCurveTo(32.5, 31, 32.5, 31.5, 33.5, 30);
449 | ctx.bezierCurveTo(34.5, 28, 36, 28, 36, 26);
450 | ctx.bezierCurveTo(27.5, 24.5, 17.5, 24.5, 9, 26);
451 | ctx.closePath();
452 | ctx.fill("evenodd");
453 | ctx.stroke();
454 | ctx.restore();
455 | ctx.save();
456 | ctx.fillStyle = "rgba(0,0,0,0)";
457 | ctx.strokeStyle = "#000";
458 | ctx.lineCap = "butt";
459 | ctx.font = " 10px sans-serif";
460 | ctx.beginPath();
461 | ctx.moveTo(11, 38.5);
462 | ctx.translate(22.5, 5.443230647868809);
463 | ctx.rotate(0.017453292519943295);
464 | ctx.arc(0, 0, 35, 1.888133661192342, 1.2185524073575649, 1);
465 | ctx.rotate(-0.017453292519943295);
466 | ctx.translate(-22.5, -5.443230647868809);
467 | ctx.fill("evenodd");
468 | ctx.stroke();
469 | ctx.restore();
470 | ctx.save();
471 | ctx.fillStyle = "rgba(0,0,0,0)";
472 | ctx.strokeStyle = "#ececec";
473 | ctx.font = " 10px sans-serif";
474 | ctx.beginPath();
475 | ctx.moveTo(11, 29);
476 | ctx.translate(22.5, 62.05676935213119);
477 | ctx.rotate(0.017453292519943295);
478 | ctx.arc(0, 0, 35, -1.9230402462322285, -1.2534589923974515, 0);
479 | ctx.rotate(-0.017453292519943295);
480 | ctx.translate(-22.5, -62.05676935213119);
481 | ctx.moveTo(12.5, 31.5);
482 | ctx.lineTo(32.5, 31.5);
483 | ctx.moveTo(11.5, 34.5);
484 | ctx.translate(22.5, 1.2735045483277005);
485 | ctx.rotate(0.017453292519943295);
486 | ctx.arc(0, 0, 35, 1.8730471829377007, 1.233638885612206, 1);
487 | ctx.rotate(-0.017453292519943295);
488 | ctx.translate(-22.5, -1.2735045483277005);
489 | ctx.moveTo(10.5, 37.5);
490 | ctx.translate(22.5, 4.621435554452809);
491 | ctx.rotate(0.017453292519943295);
492 | ctx.arc(0, 0, 35, 1.9032997455180158, 1.203386323031891, 1);
493 | ctx.rotate(-0.017453292519943295);
494 | ctx.translate(-22.5, -4.621435554452809);
495 | ctx.fill("evenodd");
496 | ctx.stroke();
497 | ctx.restore();
498 | ctx.restore();
499 | ctx.restore();
500 | },
501 | },
502 | "bR.svg": {
503 | draw: function (ctx) {
504 | ctx.save();
505 | ctx.strokeStyle = "rgba(0,0,0,0)";
506 | ctx.miterLimit = 4;
507 | ctx.font = "";
508 | ctx.font = " 10px sans-serif";
509 | ctx.save();
510 | ctx.strokeStyle = "#000";
511 | ctx.lineWidth = 1.5;
512 | ctx.lineCap = "round";
513 | ctx.lineJoin = "round";
514 | ctx.font = " 10px sans-serif";
515 | ctx.save();
516 | ctx.strokeStyle = "#000";
517 | ctx.lineCap = "butt";
518 | ctx.font = " 10px sans-serif";
519 | ctx.beginPath();
520 | ctx.moveTo(9, 39);
521 | ctx.lineTo(36, 39);
522 | ctx.lineTo(36, 36);
523 | ctx.lineTo(9, 36);
524 | ctx.lineTo(9, 39);
525 | ctx.closePath();
526 | ctx.moveTo(12.5, 32);
527 | ctx.lineTo(14, 29.5);
528 | ctx.lineTo(31, 29.5);
529 | ctx.lineTo(32.5, 32);
530 | ctx.lineTo(12.5, 32);
531 | ctx.closePath();
532 | ctx.moveTo(12, 36);
533 | ctx.lineTo(12, 32);
534 | ctx.lineTo(33, 32);
535 | ctx.lineTo(33, 36);
536 | ctx.lineTo(12, 36);
537 | ctx.closePath();
538 | ctx.fill("evenodd");
539 | ctx.stroke();
540 | ctx.restore();
541 | ctx.save();
542 | ctx.strokeStyle = "#000";
543 | ctx.lineCap = "butt";
544 | ctx.lineJoin = "miter";
545 | ctx.font = " 10px sans-serif";
546 | ctx.beginPath();
547 | ctx.moveTo(14, 29.5);
548 | ctx.lineTo(14, 16.5);
549 | ctx.lineTo(31, 16.5);
550 | ctx.lineTo(31, 29.5);
551 | ctx.lineTo(14, 29.5);
552 | ctx.closePath();
553 | ctx.fill("evenodd");
554 | ctx.stroke();
555 | ctx.restore();
556 | ctx.save();
557 | ctx.strokeStyle = "#000";
558 | ctx.lineCap = "butt";
559 | ctx.font = " 10px sans-serif";
560 | ctx.beginPath();
561 | ctx.moveTo(14, 16.5);
562 | ctx.lineTo(11, 14);
563 | ctx.lineTo(34, 14);
564 | ctx.lineTo(31, 16.5);
565 | ctx.lineTo(14, 16.5);
566 | ctx.closePath();
567 | ctx.moveTo(11, 14);
568 | ctx.lineTo(11, 9);
569 | ctx.lineTo(15, 9);
570 | ctx.lineTo(15, 11);
571 | ctx.lineTo(20, 11);
572 | ctx.lineTo(20, 9);
573 | ctx.lineTo(25, 9);
574 | ctx.lineTo(25, 11);
575 | ctx.lineTo(30, 11);
576 | ctx.lineTo(30, 9);
577 | ctx.lineTo(34, 9);
578 | ctx.lineTo(34, 14);
579 | ctx.lineTo(11, 14);
580 | ctx.closePath();
581 | ctx.fill("evenodd");
582 | ctx.stroke();
583 | ctx.restore();
584 | ctx.save();
585 | ctx.fillStyle = "rgba(0,0,0,0)";
586 | ctx.strokeStyle = "#ececec";
587 | ctx.lineWidth = 1;
588 | ctx.lineJoin = "miter";
589 | ctx.font = " 10px sans-serif";
590 | ctx.beginPath();
591 | ctx.moveTo(12, 35.5);
592 | ctx.lineTo(33, 35.5);
593 | ctx.moveTo(13, 31.5);
594 | ctx.lineTo(32, 31.5);
595 | ctx.moveTo(14, 29.5);
596 | ctx.lineTo(31, 29.5);
597 | ctx.moveTo(14, 16.5);
598 | ctx.lineTo(31, 16.5);
599 | ctx.moveTo(11, 14);
600 | ctx.lineTo(34, 14);
601 | ctx.fill("evenodd");
602 | ctx.stroke();
603 | ctx.restore();
604 | ctx.restore();
605 | ctx.restore();
606 | },
607 | },
608 | "wB.svg": {
609 | draw: function (ctx) {
610 | ctx.save();
611 | ctx.strokeStyle = "rgba(0,0,0,0)";
612 | ctx.miterLimit = 4;
613 | ctx.font = "";
614 | ctx.font = " 10px sans-serif";
615 | ctx.save();
616 | ctx.fillStyle = "rgba(0,0,0,0)";
617 | ctx.strokeStyle = "#000";
618 | ctx.lineWidth = 1.5;
619 | ctx.lineCap = "round";
620 | ctx.lineJoin = "round";
621 | ctx.font = " 10px sans-serif";
622 | ctx.save();
623 | ctx.fillStyle = "#fff";
624 | ctx.strokeStyle = "#000";
625 | ctx.lineCap = "butt";
626 | ctx.font = " 10px sans-serif";
627 | ctx.save();
628 | ctx.fillStyle = "#fff";
629 | ctx.strokeStyle = "#000";
630 | ctx.font = " 10px sans-serif";
631 | ctx.beginPath();
632 | ctx.moveTo(9, 36);
633 | ctx.bezierCurveTo(12.39, 35.03, 19.11, 36.43, 22.5, 34);
634 | ctx.bezierCurveTo(25.89, 36.43, 32.61, 35.03, 36, 36);
635 | ctx.bezierCurveTo(36, 36, 37.65, 36.54, 39, 38);
636 | ctx.bezierCurveTo(38.32, 38.97, 37.35, 38.99, 36, 38.5);
637 | ctx.bezierCurveTo(32.61, 37.53, 25.89, 38.96, 22.5, 37.5);
638 | ctx.bezierCurveTo(19.11, 38.96, 12.39, 37.53, 9, 38.5);
639 | ctx.bezierCurveTo(7.646, 38.99, 6.677, 38.97, 6, 38);
640 | ctx.bezierCurveTo(7.354, 36.06, 9, 36, 9, 36);
641 | ctx.closePath();
642 | ctx.fill("evenodd");
643 | ctx.stroke();
644 | ctx.restore();
645 | ctx.save();
646 | ctx.fillStyle = "#fff";
647 | ctx.strokeStyle = "#000";
648 | ctx.font = " 10px sans-serif";
649 | ctx.beginPath();
650 | ctx.moveTo(15, 32);
651 | ctx.bezierCurveTo(17.5, 34.5, 27.5, 34.5, 30, 32);
652 | ctx.bezierCurveTo(30.5, 30.5, 30, 30, 30, 30);
653 | ctx.bezierCurveTo(30, 27.5, 27.5, 26, 27.5, 26);
654 | ctx.bezierCurveTo(33, 24.5, 33.5, 14.5, 22.5, 10.5);
655 | ctx.bezierCurveTo(11.5, 14.5, 12, 24.5, 17.5, 26);
656 | ctx.bezierCurveTo(17.5, 26, 15, 27.5, 15, 30);
657 | ctx.bezierCurveTo(15, 30, 14.5, 30.5, 15, 32);
658 | ctx.closePath();
659 | ctx.fill("evenodd");
660 | ctx.stroke();
661 | ctx.restore();
662 | ctx.save();
663 | ctx.fillStyle = "#fff";
664 | ctx.strokeStyle = "#000";
665 | ctx.font = " 10px sans-serif";
666 | ctx.beginPath();
667 | ctx.moveTo(25, 8);
668 | ctx.translate(22.5, 8);
669 | ctx.rotate(0);
670 | ctx.arc(0, 0, 2.5, 0, 3.141592653589793, 0);
671 | ctx.rotate(0);
672 | ctx.translate(-22.5, -8);
673 | ctx.translate(22.5, 8);
674 | ctx.rotate(0);
675 | ctx.arc(0, 0, 2.5, 3.141592653589793, 6.283185307179586, 0);
676 | ctx.rotate(0);
677 | ctx.translate(-22.5, -8);
678 | ctx.fill("evenodd");
679 | ctx.stroke();
680 | ctx.restore();
681 | ctx.restore();
682 | ctx.save();
683 | ctx.fillStyle = "rgba(0,0,0,0)";
684 | ctx.strokeStyle = "#000";
685 | ctx.lineJoin = "miter";
686 | ctx.font = " 10px sans-serif";
687 | ctx.beginPath();
688 | ctx.moveTo(17.5, 26);
689 | ctx.lineTo(27.5, 26);
690 | ctx.moveTo(15, 30);
691 | ctx.lineTo(30, 30);
692 | ctx.moveTo(22.5, 15.5);
693 | ctx.lineTo(22.5, 20.5);
694 | ctx.moveTo(20, 18);
695 | ctx.lineTo(25, 18);
696 | ctx.fill("evenodd");
697 | ctx.stroke();
698 | ctx.restore();
699 | ctx.restore();
700 | ctx.restore();
701 | },
702 | },
703 | "wK.svg": {
704 | draw: function (ctx) {
705 | ctx.save();
706 | ctx.strokeStyle = "rgba(0,0,0,0)";
707 | ctx.miterLimit = 4;
708 | ctx.font = "";
709 | ctx.font = " 10px sans-serif";
710 | ctx.save();
711 | ctx.fillStyle = "rgba(0,0,0,0)";
712 | ctx.strokeStyle = "#000";
713 | ctx.lineWidth = 1.5;
714 | ctx.lineCap = "round";
715 | ctx.lineJoin = "round";
716 | ctx.font = " 10px sans-serif";
717 | ctx.save();
718 | ctx.fillStyle = "rgba(0,0,0,0)";
719 | ctx.strokeStyle = "#000";
720 | ctx.lineJoin = "miter";
721 | ctx.font = " 10px sans-serif";
722 | ctx.beginPath();
723 | ctx.moveTo(22.5, 11.63);
724 | ctx.lineTo(22.5, 6);
725 | ctx.moveTo(20, 8);
726 | ctx.lineTo(25, 8);
727 | ctx.fill("evenodd");
728 | ctx.stroke();
729 | ctx.restore();
730 | ctx.save();
731 | ctx.fillStyle = "#fff";
732 | ctx.strokeStyle = "#000";
733 | ctx.lineCap = "butt";
734 | ctx.lineJoin = "miter";
735 | ctx.font = " 10px sans-serif";
736 | ctx.beginPath();
737 | ctx.moveTo(22.5, 25);
738 | ctx.bezierCurveTo(22.5, 25, 27, 17.5, 25.5, 14.5);
739 | ctx.bezierCurveTo(25.5, 14.5, 24.5, 12, 22.5, 12);
740 | ctx.bezierCurveTo(20.5, 12, 19.5, 14.5, 19.5, 14.5);
741 | ctx.bezierCurveTo(18, 17.5, 22.5, 25, 22.5, 25);
742 | ctx.fill("evenodd");
743 | ctx.stroke();
744 | ctx.restore();
745 | ctx.save();
746 | ctx.fillStyle = "#fff";
747 | ctx.strokeStyle = "#000";
748 | ctx.font = " 10px sans-serif";
749 | ctx.beginPath();
750 | ctx.moveTo(11.5, 37);
751 | ctx.bezierCurveTo(17, 40.5, 27, 40.5, 32.5, 37);
752 | ctx.lineTo(32.5, 30);
753 | ctx.bezierCurveTo(32.5, 30, 41.5, 25.5, 38.5, 19.5);
754 | ctx.bezierCurveTo(34.5, 13, 25, 16, 22.5, 23.5);
755 | ctx.lineTo(22.5, 27);
756 | ctx.lineTo(22.5, 23.5);
757 | ctx.bezierCurveTo(19, 16, 9.5, 13, 6.5, 19.5);
758 | ctx.bezierCurveTo(3.5, 25.5, 11.5, 29.5, 11.5, 29.5);
759 | ctx.lineTo(11.5, 37);
760 | ctx.closePath();
761 | ctx.fill("evenodd");
762 | ctx.stroke();
763 | ctx.restore();
764 | ctx.save();
765 | ctx.fillStyle = "rgba(0,0,0,0)";
766 | ctx.strokeStyle = "#000";
767 | ctx.font = " 10px sans-serif";
768 | ctx.beginPath();
769 | ctx.moveTo(11.5, 30);
770 | ctx.bezierCurveTo(17, 27, 27, 27, 32.5, 30);
771 | ctx.moveTo(11.5, 33.5);
772 | ctx.bezierCurveTo(17, 30.5, 27, 30.5, 32.5, 33.5);
773 | ctx.moveTo(11.5, 37);
774 | ctx.bezierCurveTo(17, 34, 27, 34, 32.5, 37);
775 | ctx.fill("evenodd");
776 | ctx.stroke();
777 | ctx.restore();
778 | ctx.restore();
779 | ctx.restore();
780 | },
781 | },
782 | "wN.svg": {
783 | draw: function (ctx) {
784 | ctx.save();
785 | ctx.strokeStyle = "rgba(0,0,0,0)";
786 | ctx.miterLimit = 4;
787 | ctx.font = "";
788 | ctx.font = " 10px sans-serif";
789 | ctx.save();
790 | ctx.fillStyle = "rgba(0,0,0,0)";
791 | ctx.strokeStyle = "#000";
792 | ctx.lineWidth = 1.5;
793 | ctx.lineCap = "round";
794 | ctx.lineJoin = "round";
795 | ctx.font = " 10px sans-serif";
796 | ctx.save();
797 | ctx.fillStyle = "#fff";
798 | ctx.strokeStyle = "#000";
799 | ctx.font = " 10px sans-serif";
800 | ctx.beginPath();
801 | ctx.moveTo(22, 10);
802 | ctx.bezierCurveTo(32.5, 11, 38.5, 18, 38, 39);
803 | ctx.lineTo(15, 39);
804 | ctx.bezierCurveTo(15, 30, 25, 32.5, 23, 18);
805 | ctx.fill("evenodd");
806 | ctx.stroke();
807 | ctx.restore();
808 | ctx.save();
809 | ctx.fillStyle = "#fff";
810 | ctx.strokeStyle = "#000";
811 | ctx.font = " 10px sans-serif";
812 | ctx.beginPath();
813 | ctx.moveTo(24, 18);
814 | ctx.bezierCurveTo(24.38, 20.91, 18.45, 25.37, 16, 27);
815 | ctx.bezierCurveTo(13, 29, 13.18, 31.34, 11, 31);
816 | ctx.bezierCurveTo(9.958, 30.06, 12.41, 27.96, 11, 28);
817 | ctx.bezierCurveTo(10, 28, 11.19, 29.23, 10, 30);
818 | ctx.bezierCurveTo(9, 30, 5.997, 31, 6, 26);
819 | ctx.bezierCurveTo(6, 24, 12, 14, 12, 14);
820 | ctx.bezierCurveTo(12, 14, 13.89, 12.1, 14, 10.5);
821 | ctx.bezierCurveTo(13.27, 9.506, 13.5, 8.5, 13.5, 7.5);
822 | ctx.bezierCurveTo(14.5, 6.5, 16.5, 10, 16.5, 10);
823 | ctx.lineTo(18.5, 10);
824 | ctx.bezierCurveTo(18.5, 10, 19.28, 8.008, 21, 7);
825 | ctx.bezierCurveTo(22, 7, 22, 10, 22, 10);
826 | ctx.fill("evenodd");
827 | ctx.stroke();
828 | ctx.restore();
829 | ctx.save();
830 | ctx.fillStyle = "#000";
831 | ctx.strokeStyle = "#000";
832 | ctx.font = " 10px sans-serif";
833 | ctx.beginPath();
834 | ctx.moveTo(9.5, 25.5);
835 | ctx.translate(9, 25.5);
836 | ctx.rotate(0);
837 | ctx.arc(0, 0, 0.5, 0, 3.141592653589793, 0);
838 | ctx.rotate(0);
839 | ctx.translate(-9, -25.5);
840 | ctx.translate(9, 25.5);
841 | ctx.rotate(0);
842 | ctx.arc(0, 0, 0.5, 3.141592653589793, 6.283185307179586, 0);
843 | ctx.rotate(0);
844 | ctx.translate(-9, -25.5);
845 | ctx.moveTo(14.933, 15.75);
846 | ctx.translate(14.495025042545517, 15.508616850991054);
847 | ctx.rotate(0.5235987755982988);
848 | ctx.scale(0.3333333333333333, 1);
849 | ctx.arc(0, 0, 1.5, -0.006629074978609724, -3.1349551104967315, 0);
850 | ctx.scale(3, 1);
851 | ctx.rotate(-0.5235987755982988);
852 | ctx.translate(-14.495025042545517, -15.508616850991054);
853 | ctx.translate(14.504974957454483, 15.491383149008946);
854 | ctx.rotate(0.5235987755982988);
855 | ctx.scale(0.3333333333333333, 1);
856 | ctx.arc(0, 0, 1.5, 3.1349635786111834, 0.0066375430930616375, 0);
857 | ctx.scale(3, 1);
858 | ctx.rotate(-0.5235987755982988);
859 | ctx.translate(-14.504974957454483, -15.491383149008946);
860 | ctx.closePath();
861 | ctx.fill("evenodd");
862 | ctx.stroke();
863 | ctx.restore();
864 | ctx.restore();
865 | ctx.restore();
866 | },
867 | },
868 | "wP.svg": {
869 | draw: function (ctx) {
870 | ctx.save();
871 | ctx.strokeStyle = "rgba(0,0,0,0)";
872 | ctx.miterLimit = 4;
873 | ctx.font = "";
874 | ctx.font = " 10px sans-serif";
875 | ctx.save();
876 | ctx.fillStyle = "#fff";
877 | ctx.strokeStyle = "#000";
878 | ctx.lineWidth = 1.5;
879 | ctx.lineCap = "round";
880 | ctx.font = " 10px sans-serif";
881 | ctx.beginPath();
882 | ctx.moveTo(22.5, 9);
883 | ctx.bezierCurveTo(20.29, 9, 18.5, 10.79, 18.5, 13);
884 | ctx.bezierCurveTo(18.5, 13.89, 18.79, 14.71, 19.28, 15.379999999999999);
885 | ctx.bezierCurveTo(17.33, 16.5, 16, 18.59, 16, 21);
886 | ctx.bezierCurveTo(16, 23.03, 16.94, 24.84, 18.41, 26.03);
887 | ctx.bezierCurveTo(15.41, 27.09, 11, 31.580000000000002, 11, 39.5);
888 | ctx.lineTo(34, 39.5);
889 | ctx.bezierCurveTo(34, 31.58, 29.59, 27.09, 26.59, 26.03);
890 | ctx.bezierCurveTo(28.06, 24.84, 29, 23.03, 29, 21);
891 | ctx.bezierCurveTo(29, 18.59, 27.67, 16.5, 25.72, 15.379999999999999);
892 | ctx.bezierCurveTo(
893 | 26.209999999999997,
894 | 14.709999999999999,
895 | 26.5,
896 | 13.889999999999999,
897 | 26.5,
898 | 13
899 | );
900 | ctx.bezierCurveTo(26.5, 10.79, 24.71, 9, 22.5, 9);
901 | ctx.closePath();
902 | ctx.fill();
903 | ctx.stroke();
904 | ctx.restore();
905 | ctx.restore();
906 | },
907 | },
908 | "wQ.svg": {
909 | draw: function (ctx) {
910 | ctx.save();
911 | ctx.strokeStyle = "rgba(0,0,0,0)";
912 | ctx.miterLimit = 4;
913 | ctx.font = "";
914 | ctx.font = " 10px sans-serif";
915 | ctx.save();
916 | ctx.fillStyle = "#fff";
917 | ctx.strokeStyle = "#000";
918 | ctx.lineWidth = 1.5;
919 | ctx.lineCap = "round";
920 | ctx.lineJoin = "round";
921 | ctx.font = " 10px sans-serif";
922 | ctx.save();
923 | ctx.fillStyle = "#fff";
924 | ctx.strokeStyle = "#000";
925 | ctx.font = " 10px sans-serif";
926 | ctx.beginPath();
927 | ctx.moveTo(8, 12);
928 | ctx.translate(6, 12);
929 | ctx.rotate(0);
930 | ctx.arc(0, 0, 2, 0, 3.141592653589793, 0);
931 | ctx.rotate(0);
932 | ctx.translate(-6, -12);
933 | ctx.translate(6, 12);
934 | ctx.rotate(0);
935 | ctx.arc(0, 0, 2, 3.141592653589793, 6.283185307179586, 0);
936 | ctx.rotate(0);
937 | ctx.translate(-6, -12);
938 | ctx.moveTo(24.5, 7.5);
939 | ctx.translate(22.5, 7.5);
940 | ctx.rotate(0);
941 | ctx.arc(0, 0, 2, 0, 3.141592653589793, 0);
942 | ctx.rotate(0);
943 | ctx.translate(-22.5, -7.5);
944 | ctx.translate(22.5, 7.5);
945 | ctx.rotate(0);
946 | ctx.arc(0, 0, 2, 3.141592653589793, 6.283185307179586, 0);
947 | ctx.rotate(0);
948 | ctx.translate(-22.5, -7.5);
949 | ctx.closePath();
950 | ctx.moveTo(41, 12);
951 | ctx.translate(39, 12);
952 | ctx.rotate(0);
953 | ctx.arc(0, 0, 2, 0, 3.141592653589793, 0);
954 | ctx.rotate(0);
955 | ctx.translate(-39, -12);
956 | ctx.translate(39, 12);
957 | ctx.rotate(0);
958 | ctx.arc(0, 0, 2, 3.141592653589793, 6.283185307179586, 0);
959 | ctx.rotate(0);
960 | ctx.translate(-39, -12);
961 | ctx.closePath();
962 | ctx.moveTo(16, 8.5);
963 | ctx.translate(14, 8.5);
964 | ctx.rotate(0);
965 | ctx.arc(0, 0, 2, 0, 3.141592653589793, 0);
966 | ctx.rotate(0);
967 | ctx.translate(-14, -8.5);
968 | ctx.translate(14, 8.5);
969 | ctx.rotate(0);
970 | ctx.arc(0, 0, 2, 3.141592653589793, 6.283185307179586, 0);
971 | ctx.rotate(0);
972 | ctx.translate(-14, -8.5);
973 | ctx.closePath();
974 | ctx.moveTo(33, 9);
975 | ctx.translate(31, 9);
976 | ctx.rotate(0);
977 | ctx.arc(0, 0, 2, 0, 3.141592653589793, 0);
978 | ctx.rotate(0);
979 | ctx.translate(-31, -9);
980 | ctx.translate(31, 9);
981 | ctx.rotate(0);
982 | ctx.arc(0, 0, 2, 3.141592653589793, 6.283185307179586, 0);
983 | ctx.rotate(0);
984 | ctx.translate(-31, -9);
985 | ctx.closePath();
986 | ctx.fill("evenodd");
987 | ctx.stroke();
988 | ctx.restore();
989 | ctx.save();
990 | ctx.fillStyle = "#fff";
991 | ctx.strokeStyle = "#000";
992 | ctx.lineCap = "butt";
993 | ctx.font = " 10px sans-serif";
994 | ctx.beginPath();
995 | ctx.moveTo(9, 26);
996 | ctx.bezierCurveTo(17.5, 24.5, 30, 24.5, 36, 26);
997 | ctx.lineTo(38, 14);
998 | ctx.lineTo(31, 25);
999 | ctx.lineTo(31, 11);
1000 | ctx.lineTo(25.5, 24.5);
1001 | ctx.lineTo(22.5, 9.5);
1002 | ctx.lineTo(19.5, 24.5);
1003 | ctx.lineTo(14, 10.5);
1004 | ctx.lineTo(14, 25);
1005 | ctx.lineTo(7, 14);
1006 | ctx.lineTo(9, 26);
1007 | ctx.closePath();
1008 | ctx.fill("evenodd");
1009 | ctx.stroke();
1010 | ctx.restore();
1011 | ctx.save();
1012 | ctx.fillStyle = "#fff";
1013 | ctx.strokeStyle = "#000";
1014 | ctx.lineCap = "butt";
1015 | ctx.font = " 10px sans-serif";
1016 | ctx.beginPath();
1017 | ctx.moveTo(9, 26);
1018 | ctx.bezierCurveTo(9, 28, 10.5, 28, 11.5, 30);
1019 | ctx.bezierCurveTo(12.5, 31.5, 12.5, 31, 12, 33.5);
1020 | ctx.bezierCurveTo(10.5, 34.5, 10.5, 36, 10.5, 36);
1021 | ctx.bezierCurveTo(9, 37.5, 11, 38.5, 11, 38.5);
1022 | ctx.bezierCurveTo(17.5, 39.5, 27.5, 39.5, 34, 38.5);
1023 | ctx.bezierCurveTo(34, 38.5, 35.5, 37.5, 34, 36);
1024 | ctx.bezierCurveTo(34, 36, 34.5, 34.5, 33, 33.5);
1025 | ctx.bezierCurveTo(32.5, 31, 32.5, 31.5, 33.5, 30);
1026 | ctx.bezierCurveTo(34.5, 28, 36, 28, 36, 26);
1027 | ctx.bezierCurveTo(27.5, 24.5, 17.5, 24.5, 9, 26);
1028 | ctx.closePath();
1029 | ctx.fill("evenodd");
1030 | ctx.stroke();
1031 | ctx.restore();
1032 | ctx.save();
1033 | ctx.fillStyle = "rgba(0,0,0,0)";
1034 | ctx.strokeStyle = "#000";
1035 | ctx.font = " 10px sans-serif";
1036 | ctx.beginPath();
1037 | ctx.moveTo(11.5, 30);
1038 | ctx.bezierCurveTo(15, 29, 30, 29, 33.5, 30);
1039 | ctx.moveTo(12, 33.5);
1040 | ctx.bezierCurveTo(18, 32.5, 27, 32.5, 33, 33.5);
1041 | ctx.fill("evenodd");
1042 | ctx.stroke();
1043 | ctx.restore();
1044 | ctx.restore();
1045 | ctx.restore();
1046 | },
1047 | },
1048 | "wR.svg": {
1049 | draw: function (ctx) {
1050 | ctx.save();
1051 | ctx.strokeStyle = "rgba(0,0,0,0)";
1052 | ctx.miterLimit = 4;
1053 | ctx.font = "";
1054 | ctx.font = " 10px sans-serif";
1055 | ctx.save();
1056 | ctx.fillStyle = "#fff";
1057 | ctx.strokeStyle = "#000";
1058 | ctx.lineWidth = 1.5;
1059 | ctx.lineCap = "round";
1060 | ctx.lineJoin = "round";
1061 | ctx.font = " 10px sans-serif";
1062 | ctx.save();
1063 | ctx.fillStyle = "#fff";
1064 | ctx.strokeStyle = "#000";
1065 | ctx.lineCap = "butt";
1066 | ctx.font = " 10px sans-serif";
1067 | ctx.beginPath();
1068 | ctx.moveTo(9, 39);
1069 | ctx.lineTo(36, 39);
1070 | ctx.lineTo(36, 36);
1071 | ctx.lineTo(9, 36);
1072 | ctx.lineTo(9, 39);
1073 | ctx.closePath();
1074 | ctx.moveTo(12, 36);
1075 | ctx.lineTo(12, 32);
1076 | ctx.lineTo(33, 32);
1077 | ctx.lineTo(33, 36);
1078 | ctx.lineTo(12, 36);
1079 | ctx.closePath();
1080 | ctx.moveTo(11, 14);
1081 | ctx.lineTo(11, 9);
1082 | ctx.lineTo(15, 9);
1083 | ctx.lineTo(15, 11);
1084 | ctx.lineTo(20, 11);
1085 | ctx.lineTo(20, 9);
1086 | ctx.lineTo(25, 9);
1087 | ctx.lineTo(25, 11);
1088 | ctx.lineTo(30, 11);
1089 | ctx.lineTo(30, 9);
1090 | ctx.lineTo(34, 9);
1091 | ctx.lineTo(34, 14);
1092 | ctx.fill("evenodd");
1093 | ctx.stroke();
1094 | ctx.restore();
1095 | ctx.save();
1096 | ctx.fillStyle = "#fff";
1097 | ctx.strokeStyle = "#000";
1098 | ctx.font = " 10px sans-serif";
1099 | ctx.beginPath();
1100 | ctx.moveTo(34, 14);
1101 | ctx.lineTo(31, 17);
1102 | ctx.lineTo(14, 17);
1103 | ctx.lineTo(11, 14);
1104 | ctx.fill("evenodd");
1105 | ctx.stroke();
1106 | ctx.restore();
1107 | ctx.save();
1108 | ctx.fillStyle = "#fff";
1109 | ctx.strokeStyle = "#000";
1110 | ctx.lineCap = "butt";
1111 | ctx.lineJoin = "miter";
1112 | ctx.font = " 10px sans-serif";
1113 | ctx.beginPath();
1114 | ctx.moveTo(31, 17);
1115 | ctx.lineTo(31, 29.5);
1116 | ctx.lineTo(14, 29.5);
1117 | ctx.lineTo(14, 17);
1118 | ctx.fill("evenodd");
1119 | ctx.stroke();
1120 | ctx.restore();
1121 | ctx.save();
1122 | ctx.fillStyle = "#fff";
1123 | ctx.strokeStyle = "#000";
1124 | ctx.font = " 10px sans-serif";
1125 | ctx.beginPath();
1126 | ctx.moveTo(31, 29.5);
1127 | ctx.lineTo(32.5, 32);
1128 | ctx.lineTo(12.5, 32);
1129 | ctx.lineTo(14, 29.5);
1130 | ctx.fill("evenodd");
1131 | ctx.stroke();
1132 | ctx.restore();
1133 | ctx.save();
1134 | ctx.fillStyle = "rgba(0,0,0,0)";
1135 | ctx.strokeStyle = "#000";
1136 | ctx.lineJoin = "miter";
1137 | ctx.font = " 10px sans-serif";
1138 | ctx.beginPath();
1139 | ctx.moveTo(11, 14);
1140 | ctx.lineTo(34, 14);
1141 | ctx.fill("evenodd");
1142 | ctx.stroke();
1143 | ctx.restore();
1144 | ctx.restore();
1145 | ctx.restore();
1146 | },
1147 | },
1148 | };
1149 |
--------------------------------------------------------------------------------