@dixy-ui/themes 0.0.0-stage → 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +104 -2
- package/dist/ansi/ornament.d.ts +3 -0
- package/dist/ansi/ornament.js +8 -0
- package/dist/ansi-panes.css +929 -0
- package/dist/art-deco.css +850 -0
- package/dist/atelier/ornament.d.ts +4 -0
- package/dist/atelier/ornament.js +9 -0
- package/dist/atomic/ornament.d.ts +3 -0
- package/dist/atomic/ornament.js +8 -0
- package/dist/atomic-lounge.css +946 -0
- package/dist/authoring.d.ts +142 -0
- package/dist/authoring.js +83 -0
- package/dist/azulejo/ornament.d.ts +3 -0
- package/dist/azulejo/ornament.js +8 -0
- package/dist/azulejo-tile.css +962 -0
- package/dist/blueprint/ornament.d.ts +3 -0
- package/dist/blueprint/ornament.js +8 -0
- package/dist/blueprint.css +897 -0
- package/dist/brutalist/ornament.d.ts +3 -0
- package/dist/brutalist/ornament.js +8 -0
- package/dist/brutalist-concrete.css +936 -0
- package/dist/bytes.json +162 -0
- package/dist/candy-clay.css +778 -0
- package/dist/catalog.d.ts +1346 -0
- package/dist/catalog.js +1379 -0
- package/dist/character.d.ts +103 -0
- package/dist/character.js +477 -0
- package/dist/chrome.d.ts +114 -0
- package/dist/chrome.js +247 -0
- package/dist/clay/ornament.d.ts +3 -0
- package/dist/clay/ornament.js +8 -0
- package/dist/compile-css.d.ts +3 -0
- package/dist/compile-css.js +133 -0
- package/dist/deco/ornament.d.ts +3 -0
- package/dist/deco/ornament.js +8 -0
- package/dist/diwali/ornament.d.ts +3 -0
- package/dist/diwali/ornament.js +8 -0
- package/dist/diwali-lights.css +894 -0
- package/dist/documents.d.ts +104 -0
- package/dist/documents.js +1411 -0
- package/dist/forest/ornament.d.ts +3 -0
- package/dist/forest/ornament.js +12 -0
- package/dist/foundations.d.ts +105 -0
- package/dist/foundations.js +140 -0
- package/dist/frame-corners.d.ts +5 -0
- package/dist/frame-corners.js +5 -0
- package/dist/frame-styles.d.ts +36 -0
- package/dist/frame-styles.js +146 -0
- package/dist/freeze.d.ts +2 -0
- package/dist/freeze.js +9 -0
- package/dist/freighter/ornament.d.ts +3 -0
- package/dist/freighter/ornament.js +8 -0
- package/dist/freighter.css +942 -0
- package/dist/glass/ornament.d.ts +3 -0
- package/dist/glass/ornament.js +8 -0
- package/dist/glass-ui.css +923 -0
- package/dist/hardware/ornament.d.ts +4 -0
- package/dist/hardware/ornament.js +10 -0
- package/dist/holi/ornament.d.ts +3 -0
- package/dist/holi/ornament.js +8 -0
- package/dist/holi-colours.css +887 -0
- package/dist/hud/ornament.d.ts +3 -0
- package/dist/hud/ornament.js +8 -0
- package/dist/light/ornament.d.ts +3 -0
- package/dist/light/ornament.js +10 -0
- package/dist/living-forest.css +917 -0
- package/dist/lunar/ornament.d.ts +3 -0
- package/dist/lunar/ornament.js +8 -0
- package/dist/lunar-new-year.css +811 -0
- package/dist/manifests.d.ts +2485 -0
- package/dist/manifests.js +264 -0
- package/dist/material-light.css +880 -0
- package/dist/neon/ornament.d.ts +3 -0
- package/dist/neon/ornament.js +8 -0
- package/dist/neon-night-market.css +944 -0
- package/dist/ocean/ornament.d.ts +3 -0
- package/dist/ocean/ornament.js +8 -0
- package/dist/ocean-depths.css +825 -0
- package/dist/orbital-hud.css +973 -0
- package/dist/organic-css.d.ts +3 -0
- package/dist/organic-css.js +23 -0
- package/dist/organic-geometry.d.ts +20 -0
- package/dist/organic-geometry.js +74 -0
- package/dist/organic-ornament.d.ts +9 -0
- package/dist/organic-ornament.js +28 -0
- package/dist/organic-paint-launch.d.ts +43 -0
- package/dist/organic-paint-launch.js +1156 -0
- package/dist/organic-paint-world.d.ts +24 -0
- package/dist/organic-paint-world.js +676 -0
- package/dist/organic-paint.d.ts +50 -0
- package/dist/organic-paint.js +695 -0
- package/dist/organic-runtime.d.ts +4 -0
- package/dist/organic-runtime.js +231 -0
- package/dist/organic-settings.d.ts +680 -0
- package/dist/organic-settings.js +433 -0
- package/dist/ornament-parts.d.ts +20 -0
- package/dist/ornament-parts.js +15 -0
- package/dist/papel-picado.css +904 -0
- package/dist/phosphor/ornament.d.ts +3 -0
- package/dist/phosphor/ornament.js +8 -0
- package/dist/phosphor-terminal.css +1011 -0
- package/dist/picado/ornament.d.ts +3 -0
- package/dist/picado/ornament.js +8 -0
- package/dist/prism/ornament.d.ts +4 -0
- package/dist/prism/ornament.js +9 -0
- package/dist/prism-glass.css +970 -0
- package/dist/recipes.d.ts +938 -0
- package/dist/recipes.js +939 -0
- package/dist/resolve.d.ts +40 -0
- package/dist/resolve.js +24 -0
- package/dist/retro/ornament.d.ts +5 -0
- package/dist/retro/ornament.js +10 -0
- package/dist/retro-os.css +867 -0
- package/dist/riso/ornament.d.ts +3 -0
- package/dist/riso/ornament.js +8 -0
- package/dist/riso-zine.css +786 -0
- package/dist/sail/ornament.d.ts +3 -0
- package/dist/sail/ornament.js +8 -0
- package/dist/scrapbook.css +685 -0
- package/dist/signal/ornament.d.ts +4 -0
- package/dist/signal/ornament.js +11 -0
- package/dist/skins/ansi-panes.d.ts +68 -0
- package/dist/skins/ansi-panes.js +68 -0
- package/dist/skins/art-deco.d.ts +68 -0
- package/dist/skins/art-deco.js +68 -0
- package/dist/skins/atomic-lounge.d.ts +68 -0
- package/dist/skins/atomic-lounge.js +68 -0
- package/dist/skins/azulejo-tile.d.ts +68 -0
- package/dist/skins/azulejo-tile.js +68 -0
- package/dist/skins/blueprint.d.ts +68 -0
- package/dist/skins/blueprint.js +68 -0
- package/dist/skins/brutalist-concrete.d.ts +68 -0
- package/dist/skins/brutalist-concrete.js +68 -0
- package/dist/skins/candy-clay.d.ts +57 -0
- package/dist/skins/candy-clay.js +57 -0
- package/dist/skins/diwali-lights.d.ts +68 -0
- package/dist/skins/diwali-lights.js +68 -0
- package/dist/skins/freighter.d.ts +68 -0
- package/dist/skins/freighter.js +68 -0
- package/dist/skins/glass-ui.d.ts +68 -0
- package/dist/skins/glass-ui.js +68 -0
- package/dist/skins/holi-colours.d.ts +68 -0
- package/dist/skins/holi-colours.js +68 -0
- package/dist/skins/living-forest.d.ts +68 -0
- package/dist/skins/living-forest.js +68 -0
- package/dist/skins/lunar-new-year.d.ts +68 -0
- package/dist/skins/lunar-new-year.js +68 -0
- package/dist/skins/material-light.d.ts +57 -0
- package/dist/skins/material-light.js +57 -0
- package/dist/skins/neon-night-market.d.ts +68 -0
- package/dist/skins/neon-night-market.js +68 -0
- package/dist/skins/ocean-depths.d.ts +57 -0
- package/dist/skins/ocean-depths.js +57 -0
- package/dist/skins/orbital-hud.d.ts +68 -0
- package/dist/skins/orbital-hud.js +68 -0
- package/dist/skins/papel-picado.d.ts +68 -0
- package/dist/skins/papel-picado.js +68 -0
- package/dist/skins/phosphor-terminal.d.ts +68 -0
- package/dist/skins/phosphor-terminal.js +68 -0
- package/dist/skins/prism-glass.d.ts +57 -0
- package/dist/skins/prism-glass.js +57 -0
- package/dist/skins/retro-os.d.ts +57 -0
- package/dist/skins/retro-os.js +57 -0
- package/dist/skins/riso-zine.d.ts +68 -0
- package/dist/skins/riso-zine.js +68 -0
- package/dist/skins/scrapbook.d.ts +57 -0
- package/dist/skins/scrapbook.js +57 -0
- package/dist/skins/solar-sail.d.ts +68 -0
- package/dist/skins/solar-sail.js +68 -0
- package/dist/skins/studio-hardware.d.ts +57 -0
- package/dist/skins/studio-hardware.js +57 -0
- package/dist/skins/sumi-ink.d.ts +68 -0
- package/dist/skins/sumi-ink.js +68 -0
- package/dist/skins/swiss-poster.d.ts +68 -0
- package/dist/skins/swiss-poster.js +68 -0
- package/dist/skins/tactile-atelier.d.ts +57 -0
- package/dist/skins/tactile-atelier.js +57 -0
- package/dist/skins/tokyo-transit.d.ts +68 -0
- package/dist/skins/tokyo-transit.js +68 -0
- package/dist/skins/winamp-signal.d.ts +57 -0
- package/dist/skins/winamp-signal.js +57 -0
- package/dist/skins/y2k-titanium.d.ts +57 -0
- package/dist/skins/y2k-titanium.js +57 -0
- package/dist/skins/yule-knit.d.ts +68 -0
- package/dist/skins/yule-knit.js +68 -0
- package/dist/solar-sail.css +901 -0
- package/dist/starters-launch.d.ts +361 -0
- package/dist/starters-launch.js +808 -0
- package/dist/studio-hardware.css +867 -0
- package/dist/sumi/ornament.d.ts +3 -0
- package/dist/sumi/ornament.js +8 -0
- package/dist/sumi-ink.css +733 -0
- package/dist/swiss/ornament.d.ts +3 -0
- package/dist/swiss/ornament.js +8 -0
- package/dist/swiss-poster.css +727 -0
- package/dist/tactile-atelier.css +922 -0
- package/dist/theme-document.schema.json +790 -0
- package/dist/theme-schema.d.ts +453 -0
- package/dist/theme-schema.js +416 -0
- package/dist/titanium/ornament.d.ts +3 -0
- package/dist/titanium/ornament.js +11 -0
- package/dist/tokyo-transit.css +756 -0
- package/dist/transit/ornament.d.ts +3 -0
- package/dist/transit/ornament.js +8 -0
- package/dist/winamp-signal.css +1048 -0
- package/dist/y2k-titanium.css +979 -0
- package/dist/yule/ornament.d.ts +3 -0
- package/dist/yule/ornament.js +8 -0
- package/dist/yule-knit.css +850 -0
- package/package.json +351 -3
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Painters for the 2026-11 frames: drafting (Blueprint), riso (Riso Zine),
|
|
3
|
+
* neon (Neon Night Market), sumi (Sumi Ink) and transit (Tokyo Transit).
|
|
4
|
+
*
|
|
5
|
+
* Every painter follows the shared contract: it draws only around the card
|
|
6
|
+
* edge (the canvas is clipped away from content by the frame's safe insets),
|
|
7
|
+
* reads its four inks from the frame palette, and animates only while
|
|
8
|
+
* `playing` is true, so reduced motion gets a still frame.
|
|
9
|
+
* Card space: origin at the card's top-left, the card spans 0..w × 0..h.
|
|
10
|
+
* `p.at(d).t` is the clockwise tangent, so `t - π/2` points outward.
|
|
11
|
+
*/
|
|
12
|
+
import { type OrganicPainter } from "./organic-paint.ts";
|
|
13
|
+
export declare const paintDraft: OrganicPainter;
|
|
14
|
+
export declare const paintRiso: OrganicPainter;
|
|
15
|
+
export declare const paintNeon: OrganicPainter;
|
|
16
|
+
export declare const paintSumi: OrganicPainter;
|
|
17
|
+
export declare const paintTransit: OrganicPainter;
|
|
18
|
+
export declare const worldPainters: {
|
|
19
|
+
readonly drafting: OrganicPainter;
|
|
20
|
+
readonly riso: OrganicPainter;
|
|
21
|
+
readonly neon: OrganicPainter;
|
|
22
|
+
readonly sumi: OrganicPainter;
|
|
23
|
+
readonly transit: OrganicPainter;
|
|
24
|
+
};
|
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Painters for the 2026-11 frames: drafting (Blueprint), riso (Riso Zine),
|
|
3
|
+
* neon (Neon Night Market), sumi (Sumi Ink) and transit (Tokyo Transit).
|
|
4
|
+
*
|
|
5
|
+
* Every painter follows the shared contract: it draws only around the card
|
|
6
|
+
* edge (the canvas is clipped away from content by the frame's safe insets),
|
|
7
|
+
* reads its four inks from the frame palette, and animates only while
|
|
8
|
+
* `playing` is true, so reduced motion gets a still frame.
|
|
9
|
+
* Card space: origin at the card's top-left, the card spans 0..w × 0..h.
|
|
10
|
+
* `p.at(d).t` is the clockwise tangent, so `t - π/2` points outward.
|
|
11
|
+
*/
|
|
12
|
+
import { anchor, paintState, rand, tau, trace, } from "./organic-paint.js";
|
|
13
|
+
const out = (t) => t - Math.PI / 2;
|
|
14
|
+
function glow(ctx, color, blur) {
|
|
15
|
+
ctx.shadowColor = color;
|
|
16
|
+
ctx.shadowBlur = blur;
|
|
17
|
+
}
|
|
18
|
+
function noGlow(ctx) {
|
|
19
|
+
ctx.shadowBlur = 0;
|
|
20
|
+
ctx.shadowColor = "transparent";
|
|
21
|
+
}
|
|
22
|
+
/* ───────────────────────── Blueprint ───────────────────────── */
|
|
23
|
+
function arrow(ctx, x, y, angle, size) {
|
|
24
|
+
ctx.save();
|
|
25
|
+
ctx.translate(x, y);
|
|
26
|
+
ctx.rotate(angle);
|
|
27
|
+
ctx.beginPath();
|
|
28
|
+
ctx.moveTo(0, 0);
|
|
29
|
+
ctx.lineTo(-size, -size * 0.38);
|
|
30
|
+
ctx.lineTo(-size, size * 0.38);
|
|
31
|
+
ctx.closePath();
|
|
32
|
+
ctx.fill();
|
|
33
|
+
ctx.restore();
|
|
34
|
+
}
|
|
35
|
+
function dimension(ctx, colors, x1, y1, x2, y2, label, font) {
|
|
36
|
+
const angle = Math.atan2(y2 - y1, x2 - x1);
|
|
37
|
+
ctx.strokeStyle = colors.b;
|
|
38
|
+
ctx.fillStyle = colors.b;
|
|
39
|
+
ctx.lineWidth = 0.6;
|
|
40
|
+
ctx.beginPath();
|
|
41
|
+
ctx.moveTo(x1, y1);
|
|
42
|
+
ctx.lineTo(x2, y2);
|
|
43
|
+
ctx.stroke();
|
|
44
|
+
arrow(ctx, x1, y1, angle + Math.PI, 5);
|
|
45
|
+
arrow(ctx, x2, y2, angle, 5);
|
|
46
|
+
ctx.save();
|
|
47
|
+
ctx.translate((x1 + x2) / 2, (y1 + y2) / 2);
|
|
48
|
+
ctx.rotate(Math.abs(angle) > 1 ? -Math.PI / 2 : 0);
|
|
49
|
+
ctx.font = font;
|
|
50
|
+
const width = ctx.measureText(label).width + 8;
|
|
51
|
+
ctx.fillStyle = colors.paper;
|
|
52
|
+
ctx.fillRect(-width / 2, -6, width, 12);
|
|
53
|
+
ctx.fillStyle = colors.c;
|
|
54
|
+
ctx.textAlign = "center";
|
|
55
|
+
ctx.textBaseline = "middle";
|
|
56
|
+
ctx.fillText(label, 0, 0.5);
|
|
57
|
+
ctx.restore();
|
|
58
|
+
}
|
|
59
|
+
export const paintDraft = (ctx, p, w, h, settings, colors, clock, playing, index, _limits, shown = settings.density) => {
|
|
60
|
+
const state = paintState(settings), seed = state.seed + index * 719, quiet = state.style === "Quiet draft", font = `500 ${Math.round(7 + state.scale * 0.12)}px "IBM Plex Mono", ui-monospace, monospace`;
|
|
61
|
+
// Inked outline plus a dashed construction line just outside it.
|
|
62
|
+
trace(ctx, p);
|
|
63
|
+
ctx.strokeStyle = colors.a;
|
|
64
|
+
ctx.lineWidth = 0.6 + state.weight * 0.012;
|
|
65
|
+
ctx.stroke();
|
|
66
|
+
if (!quiet) {
|
|
67
|
+
trace(ctx, p, 4);
|
|
68
|
+
ctx.setLineDash([5, 3, 1, 3]);
|
|
69
|
+
ctx.strokeStyle = colors.b;
|
|
70
|
+
ctx.globalAlpha = 0.55;
|
|
71
|
+
ctx.lineWidth = 0.5;
|
|
72
|
+
ctx.stroke();
|
|
73
|
+
ctx.setLineDash([]);
|
|
74
|
+
ctx.globalAlpha = 1;
|
|
75
|
+
}
|
|
76
|
+
// Ruler ticks: every fifth tick is long, like a scale rule.
|
|
77
|
+
if (state.style === "Ruler ticks" || state.style === "Dimensioned") {
|
|
78
|
+
const step = 4 + (100 - shown) * 0.08, span = state.style === "Ruler ticks" ? p.total : p.total * 0.25;
|
|
79
|
+
ctx.strokeStyle = colors.a;
|
|
80
|
+
ctx.lineWidth = 0.5;
|
|
81
|
+
ctx.beginPath();
|
|
82
|
+
for (let d = 0, k = 0; d < span; d += step, k++) {
|
|
83
|
+
const q = p.at(d), len = k % 5 ? 2.5 : 6, n = out(q.t);
|
|
84
|
+
ctx.moveTo(q.x, q.y);
|
|
85
|
+
ctx.lineTo(q.x + Math.cos(n) * len, q.y + Math.sin(n) * len);
|
|
86
|
+
}
|
|
87
|
+
ctx.stroke();
|
|
88
|
+
}
|
|
89
|
+
// Dimension lines with extension lines and a measured label.
|
|
90
|
+
if (state.style === "Dimensioned" && w > 120) {
|
|
91
|
+
const gap = 10 + state.drape * 0.1;
|
|
92
|
+
ctx.strokeStyle = colors.b;
|
|
93
|
+
ctx.lineWidth = 0.5;
|
|
94
|
+
ctx.beginPath();
|
|
95
|
+
ctx.moveTo(0, -3);
|
|
96
|
+
ctx.lineTo(0, -gap - 4);
|
|
97
|
+
ctx.moveTo(w, -3);
|
|
98
|
+
ctx.lineTo(w, -gap - 4);
|
|
99
|
+
ctx.moveTo(-3, 0);
|
|
100
|
+
ctx.lineTo(-gap - 4, 0);
|
|
101
|
+
ctx.moveTo(-3, h);
|
|
102
|
+
ctx.lineTo(-gap - 4, h);
|
|
103
|
+
ctx.stroke();
|
|
104
|
+
dimension(ctx, colors, 0, -gap, w, -gap, `${Math.round(w)}`, font);
|
|
105
|
+
if (h > 90)
|
|
106
|
+
dimension(ctx, colors, -gap, h, -gap, 0, `${Math.round(h)}`, font);
|
|
107
|
+
}
|
|
108
|
+
// Construction lines run past the corners; registration crosshairs mark them.
|
|
109
|
+
if (!quiet) {
|
|
110
|
+
const reach = 8 + state.drape * 0.22;
|
|
111
|
+
ctx.strokeStyle = colors.b;
|
|
112
|
+
ctx.globalAlpha = state.style === "Construction lines" ? 0.75 : 0.4;
|
|
113
|
+
ctx.lineWidth = 0.45;
|
|
114
|
+
ctx.beginPath();
|
|
115
|
+
for (const [x, y, dx, dy] of [
|
|
116
|
+
[0, 0, -1, 0],
|
|
117
|
+
[0, 0, 0, -1],
|
|
118
|
+
[w, 0, 1, 0],
|
|
119
|
+
[w, 0, 0, -1],
|
|
120
|
+
[0, h, -1, 0],
|
|
121
|
+
[0, h, 0, 1],
|
|
122
|
+
[w, h, 1, 0],
|
|
123
|
+
[w, h, 0, 1],
|
|
124
|
+
]) {
|
|
125
|
+
ctx.moveTo(x, y);
|
|
126
|
+
ctx.lineTo(x + dx * reach, y + dy * reach);
|
|
127
|
+
}
|
|
128
|
+
ctx.stroke();
|
|
129
|
+
ctx.globalAlpha = 1;
|
|
130
|
+
const marks = Math.round((state.corners / 100) * 4);
|
|
131
|
+
for (let k = 0; k < marks; k++) {
|
|
132
|
+
const [x, y] = [
|
|
133
|
+
[-6, -6],
|
|
134
|
+
[w + 6, h + 6],
|
|
135
|
+
[w + 6, -6],
|
|
136
|
+
[-6, h + 6],
|
|
137
|
+
][k];
|
|
138
|
+
const r = 2.5 + state.scale * 0.08;
|
|
139
|
+
ctx.strokeStyle = colors.a;
|
|
140
|
+
ctx.lineWidth = 0.6;
|
|
141
|
+
ctx.beginPath();
|
|
142
|
+
ctx.arc(x, y, r, 0, tau);
|
|
143
|
+
ctx.moveTo(x - r * 1.8, y);
|
|
144
|
+
ctx.lineTo(x + r * 1.8, y);
|
|
145
|
+
ctx.moveTo(x, y - r * 1.8);
|
|
146
|
+
ctx.lineTo(x, y + r * 1.8);
|
|
147
|
+
ctx.stroke();
|
|
148
|
+
}
|
|
149
|
+
if (state.style === "Construction lines") {
|
|
150
|
+
// Compass arcs swung from two corners.
|
|
151
|
+
ctx.globalAlpha = 0.45;
|
|
152
|
+
ctx.beginPath();
|
|
153
|
+
const r = 14 + state.scale * 0.9;
|
|
154
|
+
ctx.arc(0, 0, r, Math.PI, Math.PI * 1.5);
|
|
155
|
+
ctx.moveTo(w + r, h);
|
|
156
|
+
ctx.arc(w, h, r, 0, Math.PI / 2);
|
|
157
|
+
ctx.stroke();
|
|
158
|
+
ctx.globalAlpha = 1;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Plotter head: a small dot that travels the outline while animated.
|
|
162
|
+
if (playing && state.motion > 0) {
|
|
163
|
+
const q = p.at((((clock * state.motion) / 900 + rand(seed)) % 1) * p.total);
|
|
164
|
+
ctx.fillStyle = colors.c;
|
|
165
|
+
ctx.beginPath();
|
|
166
|
+
ctx.arc(q.x, q.y, 1.8, 0, tau);
|
|
167
|
+
ctx.fill();
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
/* ───────────────────────── Riso Zine ───────────────────────── */
|
|
171
|
+
function tape(ctx, x, y, angle, length, color, seed) {
|
|
172
|
+
ctx.save();
|
|
173
|
+
ctx.translate(x, y);
|
|
174
|
+
ctx.rotate(angle);
|
|
175
|
+
const hh = 6.5;
|
|
176
|
+
ctx.beginPath();
|
|
177
|
+
ctx.moveTo(-length / 2, -hh);
|
|
178
|
+
ctx.lineTo(length / 2, -hh);
|
|
179
|
+
// Torn ends: a short zigzag rather than a clean cut.
|
|
180
|
+
for (let k = 0; k <= 4; k++)
|
|
181
|
+
ctx.lineTo(length / 2 + (k % 2 ? 2.2 : 0) + rand(seed + k) * 1.2, -hh + (hh * 2 * k) / 4);
|
|
182
|
+
ctx.lineTo(-length / 2, hh);
|
|
183
|
+
for (let k = 4; k >= 0; k--)
|
|
184
|
+
ctx.lineTo(-length / 2 - (k % 2 ? 2.2 : 0) - rand(seed + k + 9) * 1.2, -hh + (hh * 2 * k) / 4);
|
|
185
|
+
ctx.closePath();
|
|
186
|
+
ctx.fillStyle = color;
|
|
187
|
+
ctx.globalAlpha = 0.62;
|
|
188
|
+
ctx.fill();
|
|
189
|
+
ctx.globalAlpha = 1;
|
|
190
|
+
ctx.restore();
|
|
191
|
+
}
|
|
192
|
+
function burst(ctx, x, y, r, spin) {
|
|
193
|
+
ctx.beginPath();
|
|
194
|
+
for (let k = 0; k < 16; k++) {
|
|
195
|
+
const a = spin + (k / 16) * tau, rr = k % 2 ? r * 0.45 : r;
|
|
196
|
+
ctx.lineTo(x + Math.cos(a) * rr, y + Math.sin(a) * rr);
|
|
197
|
+
}
|
|
198
|
+
ctx.closePath();
|
|
199
|
+
ctx.fill();
|
|
200
|
+
}
|
|
201
|
+
export const paintRiso = (ctx, p, w, h, settings, colors, clock, playing, index, _limits, shown = settings.density) => {
|
|
202
|
+
const state = paintState(settings), seed = state.seed + index * 719, clean = state.style === "Clean stamp", sway = playing ? Math.sin(clock * 0.9) * (state.motion / 100) : 0, shift = clean ? 0 : 2 + state.wild * 0.04 + sway;
|
|
203
|
+
const weight = 1 + state.weight * 0.04;
|
|
204
|
+
// Two inks printed on separate passes, slightly out of register.
|
|
205
|
+
ctx.globalAlpha = 0.9;
|
|
206
|
+
trace(ctx, p, 0, 0.25, seed);
|
|
207
|
+
ctx.strokeStyle = colors.b;
|
|
208
|
+
ctx.lineWidth = weight;
|
|
209
|
+
ctx.stroke();
|
|
210
|
+
ctx.save();
|
|
211
|
+
ctx.translate(shift, shift * 0.6);
|
|
212
|
+
trace(ctx, p, 0, 0.35, seed + 3);
|
|
213
|
+
ctx.strokeStyle = colors.a;
|
|
214
|
+
ctx.lineWidth = weight * 0.8;
|
|
215
|
+
ctx.stroke();
|
|
216
|
+
ctx.restore();
|
|
217
|
+
ctx.globalAlpha = 1;
|
|
218
|
+
// Halftone dots fading out of the corners.
|
|
219
|
+
if (state.style === "Halftone" || state.style === "Misprint") {
|
|
220
|
+
const reach = 14 + state.corners * (state.style === "Halftone" ? 0.42 : 0.22), step = 4.2;
|
|
221
|
+
for (let k = 0; k < 4; k++) {
|
|
222
|
+
const cx = k % 2 ? w : 0, cy = k > 1 ? h : 0, sx = k % 2 ? 1 : -1, sy = k > 1 ? 1 : -1;
|
|
223
|
+
ctx.fillStyle = k % 2 ? colors.a : colors.b;
|
|
224
|
+
for (let i = 0; i * step < reach; i++)
|
|
225
|
+
for (let j = 0; j * step < reach; j++) {
|
|
226
|
+
const x = cx + sx * (i * step + (j % 2) * step * 0.5 - reach * 0.3), y = cy + sy * (j * step - reach * 0.3), dist = Math.hypot(i * step, j * step) / reach;
|
|
227
|
+
// Keep the dots off the card face: only the outside quarter.
|
|
228
|
+
if ((x > 2 && x < w - 2 && y > 2 && y < h - 2) || dist > 1)
|
|
229
|
+
continue;
|
|
230
|
+
ctx.beginPath();
|
|
231
|
+
ctx.arc(x, y, (1 - dist) * 1.7 + 0.2, 0, tau);
|
|
232
|
+
ctx.fill();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Masking tape on the corners.
|
|
237
|
+
if (state.style === "Taped corners" || state.style === "Misprint") {
|
|
238
|
+
const len = 26 + state.scale * 1.1, corners = state.style === "Taped corners" ? [0, 1, 2, 3] : [0, 3];
|
|
239
|
+
for (const k of corners)
|
|
240
|
+
tape(ctx, k % 2 ? w - 4 : 4, k > 1 ? h - 4 : 4, (k === 0 || k === 3 ? -0.75 : 0.75) + (rand(seed + k) - 0.5) * 0.3, len, colors.c, seed + k * 7);
|
|
241
|
+
}
|
|
242
|
+
// Little print bursts along the edge, the zine's punctuation.
|
|
243
|
+
if (!clean && state.style !== "Taped corners") {
|
|
244
|
+
const count = Math.round(shown * 0.06);
|
|
245
|
+
ctx.fillStyle = colors.d;
|
|
246
|
+
for (let i = 0; i < count; i++) {
|
|
247
|
+
const s = seed + i * 13, q = anchor(p, i, s, state), n = out(q.t), r = 2.5 + rand(s + 2) * state.scale * 0.18;
|
|
248
|
+
ctx.globalAlpha = 0.85;
|
|
249
|
+
burst(ctx, q.x + Math.cos(n) * (r + 3), q.y + Math.sin(n) * (r + 3), r, rand(s) * tau);
|
|
250
|
+
}
|
|
251
|
+
ctx.globalAlpha = 1;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
/* ───────────────────────── Neon Night Market ───────────────────────── */
|
|
255
|
+
function signShape(ctx, kind, x, y, r) {
|
|
256
|
+
ctx.beginPath();
|
|
257
|
+
if (kind === 0) {
|
|
258
|
+
// Star
|
|
259
|
+
for (let k = 0; k < 10; k++) {
|
|
260
|
+
const a = -Math.PI / 2 + (k / 10) * tau, rr = k % 2 ? r * 0.45 : r;
|
|
261
|
+
ctx.lineTo(x + Math.cos(a) * rr, y + Math.sin(a) * rr);
|
|
262
|
+
}
|
|
263
|
+
ctx.closePath();
|
|
264
|
+
}
|
|
265
|
+
else if (kind === 1) {
|
|
266
|
+
// Arrow
|
|
267
|
+
ctx.moveTo(x - r, y);
|
|
268
|
+
ctx.lineTo(x + r * 0.8, y);
|
|
269
|
+
ctx.moveTo(x + r * 0.2, y - r * 0.6);
|
|
270
|
+
ctx.lineTo(x + r * 0.8, y);
|
|
271
|
+
ctx.lineTo(x + r * 0.2, y + r * 0.6);
|
|
272
|
+
}
|
|
273
|
+
else if (kind === 2) {
|
|
274
|
+
// Ring
|
|
275
|
+
ctx.arc(x, y, r * 0.8, 0, tau);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
// Lightning
|
|
279
|
+
ctx.moveTo(x + r * 0.2, y - r);
|
|
280
|
+
ctx.lineTo(x - r * 0.4, y + r * 0.1);
|
|
281
|
+
ctx.lineTo(x + r * 0.15, y + r * 0.1);
|
|
282
|
+
ctx.lineTo(x - r * 0.25, y + r);
|
|
283
|
+
}
|
|
284
|
+
ctx.stroke();
|
|
285
|
+
}
|
|
286
|
+
export const paintNeon = (ctx, p, w, h, settings, colors, clock, playing, index, _limits, shown = settings.density) => {
|
|
287
|
+
const state = paintState(settings), seed = state.seed + index * 719, hum = state.style === "Low hum", broken = state.style === "Broken tubes", tube = 1.6 + state.weight * 0.03, bloom = hum ? 4 : 6 + state.drape * 0.14;
|
|
288
|
+
const flicker = (k) => playing &&
|
|
289
|
+
state.motion > 0 &&
|
|
290
|
+
!hum &&
|
|
291
|
+
rand(Math.floor(clock * 9) * 3.1 + k + seed) < state.motion / 600
|
|
292
|
+
? 0.35
|
|
293
|
+
: 1;
|
|
294
|
+
// Tubes are drawn as runs between electrodes, so broken tubes leave real gaps.
|
|
295
|
+
const runs = [];
|
|
296
|
+
if (broken) {
|
|
297
|
+
let d = rand(seed) * 30;
|
|
298
|
+
while (d < p.total) {
|
|
299
|
+
const len = 40 + rand(seed + d) * (60 + state.wild), gap = 7 + rand(seed + d + 1) * 10;
|
|
300
|
+
runs.push([d, Math.min(p.total, d + len)]);
|
|
301
|
+
d += len + gap;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else
|
|
305
|
+
runs.push([0, p.total]);
|
|
306
|
+
ctx.lineCap = "round";
|
|
307
|
+
ctx.lineJoin = "round";
|
|
308
|
+
const offsetPath = (a, b, offset) => {
|
|
309
|
+
ctx.beginPath();
|
|
310
|
+
for (let d = a; d <= b; d += 3) {
|
|
311
|
+
const q = p.at(Math.min(d, p.total - 0.01)), n = out(q.t);
|
|
312
|
+
const x = q.x + Math.cos(n) * offset, y = q.y + Math.sin(n) * offset;
|
|
313
|
+
if (d === a)
|
|
314
|
+
ctx.moveTo(x, y);
|
|
315
|
+
else
|
|
316
|
+
ctx.lineTo(x, y);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
const tubes = state.wild > 50 && !hum
|
|
320
|
+
? [
|
|
321
|
+
[3, colors.a],
|
|
322
|
+
[8, colors.b],
|
|
323
|
+
]
|
|
324
|
+
: [[3, colors.a]];
|
|
325
|
+
tubes.forEach(([offset, color], t) => runs.forEach(([a, b], k) => {
|
|
326
|
+
ctx.globalAlpha = flicker(k + t * 31);
|
|
327
|
+
offsetPath(a, b, offset);
|
|
328
|
+
glow(ctx, color, bloom);
|
|
329
|
+
ctx.strokeStyle = color;
|
|
330
|
+
ctx.lineWidth = tube;
|
|
331
|
+
ctx.stroke();
|
|
332
|
+
noGlow(ctx);
|
|
333
|
+
// The hot white core of the gas tube.
|
|
334
|
+
ctx.strokeStyle = "rgba(255,255,255,.75)";
|
|
335
|
+
ctx.lineWidth = tube * 0.35;
|
|
336
|
+
ctx.stroke();
|
|
337
|
+
if (broken) {
|
|
338
|
+
ctx.fillStyle = colors.d;
|
|
339
|
+
for (const d of [a, b]) {
|
|
340
|
+
const q = p.at(Math.min(d, p.total - 0.01)), n = out(q.t);
|
|
341
|
+
ctx.beginPath();
|
|
342
|
+
ctx.arc(q.x + Math.cos(n) * offset, q.y + Math.sin(n) * offset, tube * 0.9, 0, tau);
|
|
343
|
+
ctx.fill();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}));
|
|
347
|
+
ctx.globalAlpha = 1;
|
|
348
|
+
// Mounting clips holding the tube to the wall.
|
|
349
|
+
if (!hum) {
|
|
350
|
+
ctx.fillStyle = colors.d;
|
|
351
|
+
const every = 70 + (100 - shown) * 1.2;
|
|
352
|
+
for (let d = every / 2; d < p.total; d += every) {
|
|
353
|
+
const q = p.at(d);
|
|
354
|
+
ctx.save();
|
|
355
|
+
ctx.translate(q.x, q.y);
|
|
356
|
+
ctx.rotate(q.t);
|
|
357
|
+
ctx.fillRect(-1.5, -10, 3, 9);
|
|
358
|
+
ctx.restore();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Sign shapes hung at the corners.
|
|
362
|
+
if (!hum) {
|
|
363
|
+
const count = state.style === "Sign shapes" ? 4 : Math.round((state.corners / 100) * 2);
|
|
364
|
+
const spots = [
|
|
365
|
+
[w - 6, -12],
|
|
366
|
+
[-10, h * 0.32],
|
|
367
|
+
[w + 10, h - 18],
|
|
368
|
+
[18, h + 12],
|
|
369
|
+
];
|
|
370
|
+
for (let k = 0; k < count; k++) {
|
|
371
|
+
const [x, y] = spots[k], color = [colors.b, colors.c, colors.a, colors.c][k], r = 5 + state.scale * 0.32;
|
|
372
|
+
ctx.globalAlpha = flicker(100 + k);
|
|
373
|
+
glow(ctx, color, bloom);
|
|
374
|
+
ctx.strokeStyle = color;
|
|
375
|
+
ctx.lineWidth = tube;
|
|
376
|
+
signShape(ctx, Math.floor(rand(seed + k * 5) * 4), x, y, r);
|
|
377
|
+
noGlow(ctx);
|
|
378
|
+
ctx.strokeStyle = "rgba(255,255,255,.7)";
|
|
379
|
+
ctx.lineWidth = tube * 0.3;
|
|
380
|
+
signShape(ctx, Math.floor(rand(seed + k * 5) * 4), x, y, r);
|
|
381
|
+
}
|
|
382
|
+
ctx.globalAlpha = 1;
|
|
383
|
+
}
|
|
384
|
+
// Light pooling on the wall under the bottom edge.
|
|
385
|
+
const pool = ctx.createLinearGradient(0, h, 0, h + 26);
|
|
386
|
+
pool.addColorStop(0, colors.a);
|
|
387
|
+
pool.addColorStop(1, "rgba(0,0,0,0)");
|
|
388
|
+
ctx.globalAlpha = hum ? 0.06 : 0.12;
|
|
389
|
+
ctx.fillStyle = pool;
|
|
390
|
+
ctx.fillRect(8, h + 2, w - 16, 24);
|
|
391
|
+
ctx.globalAlpha = 1;
|
|
392
|
+
};
|
|
393
|
+
/* ───────────────────────── Sumi Ink ───────────────────────── */
|
|
394
|
+
function brushLine(ctx, points, base, color, seed, dry) {
|
|
395
|
+
// A brush stroke: width swells and tapers, and a dry brush skips and splits.
|
|
396
|
+
ctx.strokeStyle = color;
|
|
397
|
+
ctx.lineCap = "round";
|
|
398
|
+
for (let i = 1; i < points.length; i++) {
|
|
399
|
+
const u = i / points.length, taper = Math.sin(u * Math.PI) ** 0.6, ink = 0.55 + 0.45 * Math.sin(i * 0.21 + seed);
|
|
400
|
+
if (dry > 0 && rand(seed + i * 0.7) < dry * (0.2 + u * 0.6))
|
|
401
|
+
continue;
|
|
402
|
+
ctx.lineWidth = Math.max(0.35, base * taper * ink);
|
|
403
|
+
ctx.beginPath();
|
|
404
|
+
ctx.moveTo(points[i - 1][0], points[i - 1][1]);
|
|
405
|
+
ctx.lineTo(points[i][0], points[i][1]);
|
|
406
|
+
ctx.stroke();
|
|
407
|
+
if (dry > 0 && i % 2) {
|
|
408
|
+
// Bristle streaks beside the main stroke.
|
|
409
|
+
ctx.globalAlpha = 0.35;
|
|
410
|
+
ctx.lineWidth = 0.35;
|
|
411
|
+
for (const side of [-1, 1]) {
|
|
412
|
+
ctx.beginPath();
|
|
413
|
+
ctx.moveTo(points[i - 1][0], points[i - 1][1] + side * base * 0.45);
|
|
414
|
+
ctx.lineTo(points[i][0], points[i][1] + side * base * 0.45);
|
|
415
|
+
ctx.stroke();
|
|
416
|
+
}
|
|
417
|
+
ctx.globalAlpha = 1;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function bambooLeaf(ctx, x, y, angle, length, color) {
|
|
422
|
+
ctx.save();
|
|
423
|
+
ctx.translate(x, y);
|
|
424
|
+
ctx.rotate(angle);
|
|
425
|
+
ctx.beginPath();
|
|
426
|
+
ctx.moveTo(0, 0);
|
|
427
|
+
ctx.quadraticCurveTo(length * 0.35, -length * 0.14, length, 0);
|
|
428
|
+
ctx.quadraticCurveTo(length * 0.35, length * 0.1, 0, 0);
|
|
429
|
+
ctx.fillStyle = color;
|
|
430
|
+
ctx.fill();
|
|
431
|
+
ctx.restore();
|
|
432
|
+
}
|
|
433
|
+
function seal(ctx, x, y, size, color, paper, seed) {
|
|
434
|
+
// A hanko: a carved red square with abstract strokes (not real characters).
|
|
435
|
+
ctx.save();
|
|
436
|
+
ctx.translate(x, y);
|
|
437
|
+
ctx.rotate((rand(seed) - 0.5) * 0.08);
|
|
438
|
+
ctx.fillStyle = color;
|
|
439
|
+
ctx.globalAlpha = 0.9;
|
|
440
|
+
ctx.beginPath();
|
|
441
|
+
ctx.roundRect(-size / 2, -size / 2, size, size, size * 0.12);
|
|
442
|
+
ctx.fill();
|
|
443
|
+
ctx.globalAlpha = 1;
|
|
444
|
+
ctx.strokeStyle = paper;
|
|
445
|
+
ctx.lineWidth = Math.max(0.8, size * 0.08);
|
|
446
|
+
ctx.lineCap = "square";
|
|
447
|
+
const q = size * 0.26;
|
|
448
|
+
ctx.beginPath();
|
|
449
|
+
ctx.moveTo(-q, -q);
|
|
450
|
+
ctx.lineTo(q, -q);
|
|
451
|
+
ctx.moveTo(0, -q);
|
|
452
|
+
ctx.lineTo(0, q);
|
|
453
|
+
ctx.moveTo(-q, q * 0.2);
|
|
454
|
+
ctx.lineTo(-q * 0.2, q);
|
|
455
|
+
ctx.moveTo(q, q * 0.2);
|
|
456
|
+
ctx.lineTo(q * 0.3, q);
|
|
457
|
+
ctx.stroke();
|
|
458
|
+
ctx.restore();
|
|
459
|
+
}
|
|
460
|
+
export const paintSumi = (ctx, p, w, h, settings, colors, clock, playing, index, _limits, shown = settings.density) => {
|
|
461
|
+
const state = paintState(settings), seed = state.seed + index * 719, dry = state.style === "Dry brush" ? 0.55 : state.wild / 400, base = 1 + state.weight * 0.05;
|
|
462
|
+
// The border itself, painted in four strokes like a calligrapher would.
|
|
463
|
+
for (let side = 0; side < 4; side++) {
|
|
464
|
+
const a = p.corners[side], b = p.corners[(side + 1) % 4] || p.total, span = (b - a + p.total) % p.total;
|
|
465
|
+
const pts = [];
|
|
466
|
+
for (let d = 0; d <= span; d += 3) {
|
|
467
|
+
const q = p.at((a + d) % p.total), n = out(q.t), wob = Math.sin(d * 0.05 + seed + side) * 0.6;
|
|
468
|
+
pts.push([q.x + Math.cos(n) * wob, q.y + Math.sin(n) * wob]);
|
|
469
|
+
}
|
|
470
|
+
brushLine(ctx, pts, base, colors.a, seed + side * 11, dry);
|
|
471
|
+
}
|
|
472
|
+
const sway = playing ? Math.sin(clock * 0.8) * 0.05 * (state.motion / 50) : 0;
|
|
473
|
+
// Bamboo grows from the frame: a ground stroke continues the bottom border past the
|
|
474
|
+
// corner, and one tapered stalk rises from it beside the left edge.
|
|
475
|
+
if (state.style === "Bamboo") {
|
|
476
|
+
const x0 = -9, top = h * (0.6 - state.drape / 250), lean = -2.5, at = (y) => x0 + lean * ((h - y) / (h - top)) ** 2;
|
|
477
|
+
brushLine(ctx, [
|
|
478
|
+
[-22, h + 0.6],
|
|
479
|
+
[-14, h + 0.9],
|
|
480
|
+
[-6, h + 0.5],
|
|
481
|
+
[Math.min(p.r, 10), h],
|
|
482
|
+
], base * 1.4, colors.a, seed + 21, dry * 0.5);
|
|
483
|
+
ctx.strokeStyle = colors.b;
|
|
484
|
+
ctx.lineCap = "round";
|
|
485
|
+
for (let y = h; y > top; y -= 2) {
|
|
486
|
+
ctx.lineWidth = 3.4 - ((h - y) / (h - top)) * 1.6;
|
|
487
|
+
ctx.beginPath();
|
|
488
|
+
ctx.moveTo(at(y), y);
|
|
489
|
+
ctx.lineTo(at(Math.max(top, y - 2)), Math.max(top, y - 2));
|
|
490
|
+
ctx.stroke();
|
|
491
|
+
}
|
|
492
|
+
const nodes = [];
|
|
493
|
+
for (let y = h, k = 0;; k++) {
|
|
494
|
+
y -= 20 + rand(seed + k) * 12;
|
|
495
|
+
if (y < top + 10)
|
|
496
|
+
break;
|
|
497
|
+
nodes.push(y);
|
|
498
|
+
}
|
|
499
|
+
// Node rings in the darker ink, slightly bowed, instead of gaps in the stalk.
|
|
500
|
+
ctx.strokeStyle = colors.a;
|
|
501
|
+
ctx.lineWidth = 1;
|
|
502
|
+
for (const y of nodes) {
|
|
503
|
+
const x = at(y), r = 2.2 - ((h - y) / (h - top)) * 0.7;
|
|
504
|
+
ctx.beginPath();
|
|
505
|
+
ctx.moveTo(x - r, y + 0.5);
|
|
506
|
+
ctx.quadraticCurveTo(x, y - 0.9, x + r, y + 0.5);
|
|
507
|
+
ctx.stroke();
|
|
508
|
+
}
|
|
509
|
+
// Leaves droop in fans from the upper nodes; most point away from the card, and the
|
|
510
|
+
// few that cross the edge are short enough to stay in its padding.
|
|
511
|
+
const fans = Math.min(nodes.length, Math.round(1 + shown * 0.035)), len = 11 + state.scale * 0.7;
|
|
512
|
+
for (let i = 0; i < fans; i++) {
|
|
513
|
+
const y = nodes[nodes.length - 1 - i], x = at(y), inward = i % 2 === 1, count = 2 + (rand(seed + i * 5) < 0.5 ? 1 : 0);
|
|
514
|
+
for (let k = 0; k < count; k++) {
|
|
515
|
+
const droop = 0.25 + k * 0.38 + (rand(seed + i * 7 + k) - 0.5) * 0.2;
|
|
516
|
+
bambooLeaf(ctx, x + (inward ? 1 : -1), y - 1, (inward ? droop : Math.PI - droop) + sway, len * (inward ? 0.6 : 1) * (0.8 + rand(seed + i + k * 3) * 0.35), colors.b);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// The growing tip ends in a pair of young leaves.
|
|
520
|
+
bambooLeaf(ctx, at(top), top, -Math.PI / 2 - 0.5 + sway, len * 0.55, colors.b);
|
|
521
|
+
bambooLeaf(ctx, at(top), top, -Math.PI / 2 + 0.35 + sway, len * 0.45, colors.b);
|
|
522
|
+
}
|
|
523
|
+
// A plum branch along the top edge, blossoms in the accent ink.
|
|
524
|
+
if (state.style === "Plum blossom") {
|
|
525
|
+
// The branch leaves the frame at the top-left corner and arcs along the top edge.
|
|
526
|
+
const root = p.r * 0.3, pts = [[root, root * 0.4]];
|
|
527
|
+
const end = w * (0.3 + state.drape / 220);
|
|
528
|
+
for (let x = root + 6; x < end; x += 8 + rand(seed + x) * 8) {
|
|
529
|
+
const u = (x - root) / (end - root);
|
|
530
|
+
pts.push([x, -Math.sin(u * Math.PI) * 7 - rand(seed + x * 0.3) * 2]);
|
|
531
|
+
}
|
|
532
|
+
brushLine(ctx, pts, base * 1.6, colors.a, seed + 4, 0.15);
|
|
533
|
+
const blooms = Math.round(2 + shown * 0.07);
|
|
534
|
+
for (let i = 0; i < blooms; i++) {
|
|
535
|
+
const [bx, by] = pts[Math.floor(rand(seed + i * 3) * pts.length)], r = 2 + state.scale * 0.09, dropY = playing && i === 0 ? (clock * 6) % 40 : 0;
|
|
536
|
+
ctx.fillStyle = colors.c;
|
|
537
|
+
for (let k = 0; k < 5; k++) {
|
|
538
|
+
const a = (k / 5) * tau + rand(seed + i);
|
|
539
|
+
ctx.beginPath();
|
|
540
|
+
ctx.arc(bx + Math.cos(a) * r, by - 3 + Math.sin(a) * r + dropY, r * 0.75, 0, tau);
|
|
541
|
+
ctx.fill();
|
|
542
|
+
}
|
|
543
|
+
ctx.fillStyle = colors.d;
|
|
544
|
+
ctx.beginPath();
|
|
545
|
+
ctx.arc(bx, by - 3 + dropY, r * 0.35, 0, tau);
|
|
546
|
+
ctx.fill();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// Ensō: one open circle, never closed.
|
|
550
|
+
if (state.style === "Ensō") {
|
|
551
|
+
const r = 10 + state.scale * 0.7, cx = w + 2, cy = h + 2, pts = [];
|
|
552
|
+
for (let a = 0.35; a < tau - 0.25; a += 0.12)
|
|
553
|
+
pts.push([cx + Math.cos(a + 1.2) * r, cy + Math.sin(a + 1.2) * r]);
|
|
554
|
+
brushLine(ctx, pts, base * 2.4, colors.a, seed + 9, 0.25);
|
|
555
|
+
}
|
|
556
|
+
// The artist's seal, stamped on one corner.
|
|
557
|
+
if (state.corners > 25)
|
|
558
|
+
seal(ctx, w - 10, h + 10, 9 + state.scale * 0.35, colors.d, colors.paper, seed);
|
|
559
|
+
};
|
|
560
|
+
/* ───────────────────────── Tokyo Transit ───────────────────────── */
|
|
561
|
+
const lineLetters = ["G", "M", "H", "T", "C", "Y", "Z", "N", "F"];
|
|
562
|
+
function roundel(ctx, x, y, r, color, ink, code) {
|
|
563
|
+
ctx.beginPath();
|
|
564
|
+
ctx.arc(x, y, r, 0, tau);
|
|
565
|
+
ctx.fillStyle = "#ffffff";
|
|
566
|
+
ctx.fill();
|
|
567
|
+
ctx.lineWidth = Math.max(1.6, r * 0.24);
|
|
568
|
+
ctx.strokeStyle = color;
|
|
569
|
+
ctx.stroke();
|
|
570
|
+
ctx.fillStyle = ink;
|
|
571
|
+
ctx.textAlign = "center";
|
|
572
|
+
ctx.textBaseline = "middle";
|
|
573
|
+
ctx.font = `700 ${Math.round(r * 0.62)}px "Zen Kaku Gothic New", "Hiragino Sans", ui-sans-serif, sans-serif`;
|
|
574
|
+
ctx.fillText(code[0], x, y - r * 0.24);
|
|
575
|
+
ctx.font = `700 ${Math.round(r * 0.5)}px "Zen Kaku Gothic New", "Hiragino Sans", ui-sans-serif, sans-serif`;
|
|
576
|
+
ctx.fillText(code.slice(1), x, y + r * 0.34);
|
|
577
|
+
}
|
|
578
|
+
export const paintTransit = (ctx, p, w, h, settings, colors, clock, playing, index, _limits, shown = settings.density) => {
|
|
579
|
+
const state = paintState(settings), seed = state.seed + index * 719, quiet = state.style === "Quiet line", band = 2 + state.weight * 0.04, lineColors = [colors.a, colors.b, colors.c];
|
|
580
|
+
trace(ctx, p);
|
|
581
|
+
ctx.strokeStyle = colors.d;
|
|
582
|
+
ctx.globalAlpha = 0.5;
|
|
583
|
+
ctx.lineWidth = 0.75;
|
|
584
|
+
ctx.stroke();
|
|
585
|
+
ctx.globalAlpha = 1;
|
|
586
|
+
// Route lines: parallel bands along the top that bend 45° down a side,
|
|
587
|
+
// the way metro maps draw them.
|
|
588
|
+
const lines = quiet ? 1 : state.style === "Line map" ? 3 : 2, drop = Math.min(h - 20, 30 + state.drape * 1.4);
|
|
589
|
+
ctx.lineCap = "round";
|
|
590
|
+
ctx.lineJoin = "round";
|
|
591
|
+
for (let k = 0; k < lines; k++) {
|
|
592
|
+
const o = 5 + k * (band + 1.6), start = p.r + 6 + k * 3;
|
|
593
|
+
ctx.beginPath();
|
|
594
|
+
ctx.moveTo(start, -o);
|
|
595
|
+
ctx.lineTo(w - o * 0.4 - 8, -o);
|
|
596
|
+
ctx.lineTo(w + o, -o + o * 1.4 + 8);
|
|
597
|
+
ctx.lineTo(w + o, drop + k * 6);
|
|
598
|
+
ctx.strokeStyle = lineColors[k];
|
|
599
|
+
ctx.lineWidth = band;
|
|
600
|
+
ctx.stroke();
|
|
601
|
+
}
|
|
602
|
+
if (!quiet) {
|
|
603
|
+
// Stations: white ticks with a dark ring; interchanges span every line.
|
|
604
|
+
const step = 60 + (100 - shown) * 1.1;
|
|
605
|
+
for (let x = p.r + 26, i = 0; x < w - 30; x += step, i++) {
|
|
606
|
+
const interchange = lines > 1 && i % 3 === 1;
|
|
607
|
+
ctx.fillStyle = "#ffffff";
|
|
608
|
+
ctx.strokeStyle = colors.d;
|
|
609
|
+
ctx.lineWidth = 1.1;
|
|
610
|
+
ctx.beginPath();
|
|
611
|
+
if (interchange) {
|
|
612
|
+
const top = -5 - (lines - 1) * (band + 1.6) - band;
|
|
613
|
+
ctx.roundRect(x - 3.2, top - 1.5, 6.4, -top - 5 + band + 3, 3.2);
|
|
614
|
+
}
|
|
615
|
+
else
|
|
616
|
+
ctx.arc(x, -5, band * 0.9 + 0.8, 0, tau);
|
|
617
|
+
ctx.fill();
|
|
618
|
+
ctx.stroke();
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
// Numbered station roundels on the corners.
|
|
622
|
+
const roundels = state.style === "Station roundels"
|
|
623
|
+
? 4
|
|
624
|
+
: quiet
|
|
625
|
+
? 0
|
|
626
|
+
: Math.round((state.corners / 100) * 2);
|
|
627
|
+
const spots = [
|
|
628
|
+
[-4, -4],
|
|
629
|
+
[w + 4, h + 4],
|
|
630
|
+
[-4, h + 4],
|
|
631
|
+
[w + 4, -4],
|
|
632
|
+
];
|
|
633
|
+
for (let k = 0; k < roundels; k++) {
|
|
634
|
+
const [x, y] = spots[k], code = lineLetters[Math.floor(rand(seed + k) * lineLetters.length)] +
|
|
635
|
+
String(1 + Math.floor(rand(seed + k + 4) * 28)).padStart(2, "0");
|
|
636
|
+
roundel(ctx, x, y, 7 + state.scale * 0.28, lineColors[k % 3], colors.d, code);
|
|
637
|
+
}
|
|
638
|
+
// Platform edge: the tactile yellow strip with raised dots, below the card.
|
|
639
|
+
if (state.style === "Platform edge") {
|
|
640
|
+
const y = h + 5, depth = 6 + state.scale * 0.2;
|
|
641
|
+
ctx.fillStyle = "#e9b616";
|
|
642
|
+
ctx.fillRect(p.r, y, w - p.r * 2, depth);
|
|
643
|
+
ctx.fillStyle = "rgba(0,0,0,.22)";
|
|
644
|
+
for (let x = p.r + 3; x < w - p.r - 2; x += 4)
|
|
645
|
+
for (let yy = y + 2; yy < y + depth - 1; yy += 4) {
|
|
646
|
+
ctx.beginPath();
|
|
647
|
+
ctx.arc(x, yy, 0.8, 0, tau);
|
|
648
|
+
ctx.fill();
|
|
649
|
+
}
|
|
650
|
+
ctx.strokeStyle = "#ffffff";
|
|
651
|
+
ctx.setLineDash([6, 4]);
|
|
652
|
+
ctx.lineWidth = 1;
|
|
653
|
+
ctx.beginPath();
|
|
654
|
+
ctx.moveTo(p.r, y + depth + 3);
|
|
655
|
+
ctx.lineTo(w - p.r, y + depth + 3);
|
|
656
|
+
ctx.stroke();
|
|
657
|
+
ctx.setLineDash([]);
|
|
658
|
+
}
|
|
659
|
+
// A train glides along the top line while animated.
|
|
660
|
+
if (playing && state.motion > 0) {
|
|
661
|
+
const span = w - p.r - 14, x = p.r + 6 + ((clock * state.motion * 0.5 + rand(seed) * span) % span);
|
|
662
|
+
ctx.fillStyle = colors.a;
|
|
663
|
+
ctx.beginPath();
|
|
664
|
+
ctx.roundRect(x - 7, -5 - band / 2 - 1.5, 14, band + 3, 2.5);
|
|
665
|
+
ctx.fill();
|
|
666
|
+
ctx.fillStyle = "#ffffff";
|
|
667
|
+
ctx.fillRect(x + 2.5, -5 - band / 2, 2.5, band);
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
export const worldPainters = {
|
|
671
|
+
drafting: paintDraft,
|
|
672
|
+
riso: paintRiso,
|
|
673
|
+
neon: paintNeon,
|
|
674
|
+
sumi: paintSumi,
|
|
675
|
+
transit: paintTransit,
|
|
676
|
+
};
|