@bongos/core 1.24.94 → 1.24.95
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/.bongos-core.json +107 -27
- package/docs/adr/0405-each-character-has-one-colour-and-the-visor-wears-it.md +31 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +7 -7
- package/docs/copy-registry.json +7 -7
- package/docs/design/characters/v2/artisan-visor.png +0 -0
- package/docs/design/characters/v2/artisan.png +0 -0
- package/docs/design/characters/v2/artist-visor.png +0 -0
- package/docs/design/characters/v2/artist.png +0 -0
- package/docs/design/characters/v2/draw-characters.js +465 -0
- package/docs/design/characters/v2/patron-visor.png +0 -0
- package/docs/design/characters/v2/patron.png +0 -0
- package/docs/design/characters/v2/sheet.html +114 -0
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +35 -35
- package/modules/hall-ui/public/characters/artisan-face.png +0 -0
- package/modules/hall-ui/public/characters/artisan-visor.png +0 -0
- package/modules/hall-ui/public/characters/artisan.png +0 -0
- package/modules/hall-ui/public/characters/artist-face.png +0 -0
- package/modules/hall-ui/public/characters/artist-visor.png +0 -0
- package/modules/hall-ui/public/characters/artist.png +0 -0
- package/modules/hall-ui/public/characters/patron-face.png +0 -0
- package/modules/hall-ui/public/characters/patron-visor.png +0 -0
- package/modules/hall-ui/public/characters/patron.png +0 -0
- package/modules/hall-ui/public/craft-mode.js +15 -4
- package/modules/hall-ui/public/style.css +18 -5
- package/modules/hall-ui/public/visor.css +20 -5
- package/modules/hall-ui/public/visor.js +6 -3
- package/modules/hall-ui/records/characters.md +19 -0
- package/modules/hall-ui/records/visor.md +3 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/character_creation.mjs +44 -7
- package/tests/hall_visor.mjs +28 -2
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// draw-characters.js — the second look for the three characters (exploration, 2026-11-11).
|
|
3
|
+
//
|
|
4
|
+
// The owner's brief: keep "two animals combined", leave ancient Egypt for an alien,
|
|
5
|
+
// space-opera feel that sits with the Helm UI, draw at 64x64, give each character ONE
|
|
6
|
+
// colour of its own, and draw a second version of each wearing the VR visor with the rim
|
|
7
|
+
// in that colour.
|
|
8
|
+
//
|
|
9
|
+
// The sprites are DRAWN BY THIS FILE, pixel by pixel, so a change to a character is an edit
|
|
10
|
+
// here and a re-run, never a hand-edited PNG:
|
|
11
|
+
//
|
|
12
|
+
// node docs/design/characters/v2/draw-characters.js
|
|
13
|
+
//
|
|
14
|
+
// writes, beside this file: <id>.png and <id>-visor.png (64x64, transparent), and
|
|
15
|
+
// contact.png (all six, enlarged, on the hall's night ground) for looking at.
|
|
16
|
+
//
|
|
17
|
+
// node docs/design/characters/v2/draw-characters.js --hall
|
|
18
|
+
//
|
|
19
|
+
// also writes the three files per character the hall serves (task 1005218), into
|
|
20
|
+
// modules/hall-ui/public/characters/: <id>.png (the whole figure, 64x64), <id>-face.png
|
|
21
|
+
// (the head, 32x32, for the top-bar chip) and <id>-visor.png (the head wearing the visor,
|
|
22
|
+
// 32x32, for the visor's corner). The hall's pictures are never edited by hand.
|
|
23
|
+
'use strict';
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
const zlib = require('node:zlib');
|
|
27
|
+
|
|
28
|
+
const N = 64;
|
|
29
|
+
const OUT = '#04080f';
|
|
30
|
+
|
|
31
|
+
// ---------- a 64x64 sheet of colours ----------
|
|
32
|
+
function sheet() {
|
|
33
|
+
const px = new Array(N * N).fill(null);
|
|
34
|
+
const S = {
|
|
35
|
+
px,
|
|
36
|
+
set(x, y, c) { x = Math.round(x); y = Math.round(y); if (x >= 0 && y >= 0 && x < N && y < N) px[y * N + x] = c; },
|
|
37
|
+
get(x, y) { return x >= 0 && y >= 0 && x < N && y < N ? px[y * N + x] : null; },
|
|
38
|
+
rect(x0, y0, x1, y1, c) { for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) S.set(x, y, c); },
|
|
39
|
+
ell(cx, cy, rx, ry, c) {
|
|
40
|
+
for (let y = Math.floor(cy - ry); y <= Math.ceil(cy + ry); y++) for (let x = Math.floor(cx - rx); x <= Math.ceil(cx + rx); x++) {
|
|
41
|
+
const dx = (x - cx) / rx, dy = (y - cy) / ry;
|
|
42
|
+
if (dx * dx + dy * dy <= 1) S.set(x, y, c);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
poly(pts, c) {
|
|
46
|
+
const ys = pts.map((p) => p[1]);
|
|
47
|
+
for (let y = Math.min(...ys); y <= Math.max(...ys); y++) {
|
|
48
|
+
const xs = [];
|
|
49
|
+
for (let i = 0; i < pts.length; i++) {
|
|
50
|
+
const [x0, y0] = pts[i], [x1, y1] = pts[(i + 1) % pts.length];
|
|
51
|
+
if ((y0 <= y && y1 > y) || (y1 <= y && y0 > y)) xs.push(x0 + ((y - y0) / (y1 - y0)) * (x1 - x0));
|
|
52
|
+
}
|
|
53
|
+
xs.sort((a, b) => a - b);
|
|
54
|
+
for (let i = 0; i + 1 < xs.length; i += 2) for (let x = Math.round(xs[i]); x <= Math.round(xs[i + 1]); x++) S.set(x, y, c);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
// a line with a round brush; `w` may taper from w to w1
|
|
58
|
+
line(x0, y0, x1, y1, c, w = 1, w1 = w) {
|
|
59
|
+
const n = Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0)) * 2 || 1;
|
|
60
|
+
for (let i = 0; i <= n; i++) {
|
|
61
|
+
const t = i / n, x = x0 + (x1 - x0) * t, y = y0 + (y1 - y0) * t, r = (w + (w1 - w) * t) / 2;
|
|
62
|
+
if (r <= 0.6) S.set(x, y, c); else S.ell(x, y, r, r, c);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
path(pts, c, w = 1, w1 = w) {
|
|
66
|
+
for (let i = 0; i + 1 < pts.length; i++) {
|
|
67
|
+
const a = w + (w1 - w) * (i / (pts.length - 1)), b = w + (w1 - w) * ((i + 1) / (pts.length - 1));
|
|
68
|
+
S.line(pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1], c, a, b);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
dots(list, c) { for (const [x, y] of list) S.set(x, y, c); },
|
|
72
|
+
// one light from the upper right: the silhouette's lit edge up a tone, its far edge down
|
|
73
|
+
shade(skip) {
|
|
74
|
+
const src = px.slice(), at = (x, y) => (x >= 0 && y >= 0 && x < N && y < N ? src[y * N + x] : null);
|
|
75
|
+
const mix = (c, to, k) => '#' + [1, 3, 5].map((i) => Math.round(parseInt(c.slice(i, i + 2), 16) * (1 - k) + parseInt(to.slice(i, i + 2), 16) * k).toString(16).padStart(2, '0')).join('');
|
|
76
|
+
for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
|
|
77
|
+
const c = at(x, y);
|
|
78
|
+
if (!c || skip.has(c)) continue;
|
|
79
|
+
const lit = !at(x, y - 1) || !at(x + 1, y), far = !at(x - 1, y) || !at(x, y + 1);
|
|
80
|
+
if (lit && !far) px[y * N + x] = mix(c, '#ffffff', 0.2);
|
|
81
|
+
else if (far && !lit) px[y * N + x] = mix(c, '#04080f', 0.3);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
// one dark pixel round the whole figure
|
|
85
|
+
outline(c = OUT) {
|
|
86
|
+
const add = [];
|
|
87
|
+
for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
|
|
88
|
+
if (S.get(x, y)) continue;
|
|
89
|
+
if (S.get(x - 1, y) || S.get(x + 1, y) || S.get(x, y - 1) || S.get(x, y + 1)) add.push([x, y]);
|
|
90
|
+
}
|
|
91
|
+
S.dots(add, c);
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
return S;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------- shared cloth and metal ----------
|
|
98
|
+
const SUIT = { d: '#111e2d', s: '#182a3c', m: '#1f3549', l: '#34566f', hi: '#5d8aa6' };
|
|
99
|
+
const SHADOW = '#081320';
|
|
100
|
+
const ICE = '#c8f1f7';
|
|
101
|
+
const GLASS = '#0a1624';
|
|
102
|
+
|
|
103
|
+
// The three colours. One per character; the visor rim, the lit parts of the suit and the
|
|
104
|
+
// glass tint all take it.
|
|
105
|
+
const COLOUR = {
|
|
106
|
+
artisan: { c: '#fb9569', l: '#ffc7ab', d: '#b3562f', name: 'Ember' },
|
|
107
|
+
artist: { c: '#c59bff', l: '#e8d8ff', d: '#7a52c4', name: 'Violet' },
|
|
108
|
+
patron: { c: '#ffd166', l: '#fff0bd', d: '#b3851c', name: 'Gold' },
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// THE RULES OF THIS LOOK (owner, 2026-11-11, four rounds):
|
|
112
|
+
// 1. "the animals weren't very recognizable": one animal gives the HEAD, whole, the other
|
|
113
|
+
// gives the BODY, whole (limbs, feet, tail). Never a blend of the two in one part.
|
|
114
|
+
// 2. "way too kiddish": no big heads, no round eyes, no fluffy or candy shapes. They stand
|
|
115
|
+
// tall and lean, in armour and long coats, the eye a narrow slit of light in the
|
|
116
|
+
// character's colour. Natural colours, held back.
|
|
117
|
+
// 3. each holds the one tool its role is known by, carried like a staff.
|
|
118
|
+
// 4. "the originals were more 2.5D, which felt more alive than our side view": the body is
|
|
119
|
+
// turned three-quarters to the viewer (both shoulders, a hand on the hip, a stride, the
|
|
120
|
+
// far foot higher than the near one), the head stays in profile, and they stand on a
|
|
121
|
+
// lit disc as the originals stood on a plinth.
|
|
122
|
+
// 5. "simplify": at 64 pixels a detail smaller than two pixels is noise. Big flat shapes,
|
|
123
|
+
// three tones each, the character's colour in a few strong places, no speckle.
|
|
124
|
+
// 6. one light, from the upper right, where they are looking: every figure's lit edge is
|
|
125
|
+
// raised and its far edge dropped by shade() below, so the three share one light without
|
|
126
|
+
// a hand-placed highlight. And each has its own build: the Artisan broad in heavy plate,
|
|
127
|
+
// the Artist narrow in cloth, the Patron upright under a cape.
|
|
128
|
+
|
|
129
|
+
// what the light leaves alone: anything that glows, the glass, the disc
|
|
130
|
+
function lit(A) { return new Set([A.c, A.l, A.d, ICE, GLASS, SHADOW, '#0d1a29', '#12233a']); }
|
|
131
|
+
|
|
132
|
+
// a spiral with a round brush
|
|
133
|
+
function spiral(S, cx, cy, r0, r1, a0, a1, c, w0, w1) {
|
|
134
|
+
const n = 220;
|
|
135
|
+
for (let i = 0; i <= n; i++) {
|
|
136
|
+
const t = i / n, a = (a0 + (a1 - a0) * t) * Math.PI / 180, r = r0 + (r1 - r0) * t, w = (w0 + (w1 - w0) * t) / 2;
|
|
137
|
+
S.ell(cx + Math.cos(a) * r, cy + Math.sin(a) * r, w, w, c);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// a ram's horn: a thick curl that thins to a tip, ridged across, lit along its outer edge
|
|
141
|
+
function horn(S, cx, cy, r0, r1, a0, a1, w0, w1, H, ridges) {
|
|
142
|
+
spiral(S, cx, cy, r0, r1, a0, a1, H.d, w0 + 1.4, w1 + 1);
|
|
143
|
+
spiral(S, cx, cy, r0, r1, a0, a1, H.m, w0, w1);
|
|
144
|
+
for (let k = 1; k < ridges; k++) {
|
|
145
|
+
const t = k / ridges, a = (a0 + (a1 - a0) * t) * Math.PI / 180, r = r0 + (r1 - r0) * t, w = (w0 + (w1 - w0) * t) / 2;
|
|
146
|
+
S.line(cx + Math.cos(a) * (r - w), cy + Math.sin(a) * (r - w), cx + Math.cos(a) * (r + w), cy + Math.sin(a) * (r + w), H.d, 1);
|
|
147
|
+
const b = (a0 + (a1 - a0) * (t - 0.5 / ridges)) * Math.PI / 180;
|
|
148
|
+
S.set(cx + Math.cos(b) * (r + w - 0.6), cy + Math.sin(b) * (r + w - 0.6), H.l);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// the lit disc they stand on
|
|
153
|
+
function ground(S, A) {
|
|
154
|
+
S.ell(31, 59.6, 20, 3.6, A.d);
|
|
155
|
+
S.ell(31, 59, 20, 3.2, '#0d1a29');
|
|
156
|
+
S.ell(31, 58.6, 15, 2, '#12233a');
|
|
157
|
+
S.ell(30, 58.8, 12, 1.8, SHADOW); // their shadow, falling away from the light
|
|
158
|
+
}
|
|
159
|
+
// plate armour, turned three-quarters: both shoulders, the breastplate off-centre
|
|
160
|
+
function torso(S, A) {
|
|
161
|
+
S.poly([[20, 24], [40, 23], [41, 28], [38, 35], [36, 42], [25, 42], [23, 35], [19, 29]], SUIT.m);
|
|
162
|
+
S.poly([[27, 25], [39, 24], [40, 28], [37, 35], [35, 41], [28, 41], [26, 34]], SUIT.l);
|
|
163
|
+
S.poly([[20, 24], [24, 24], [26, 34], [27, 41], [25, 41], [23, 35], [19, 29]], SUIT.s); // the near flank, out of the light
|
|
164
|
+
S.path([[35, 25], [39, 25], [40, 28]], SUIT.hi, 1); // the plate's lit rim
|
|
165
|
+
S.rect(25, 41, 36, 42, SUIT.d); S.rect(31, 41, 33, 42, A.c);
|
|
166
|
+
S.rect(28, 21, 36, 23, SUIT.d); S.rect(28, 21, 36, 21, SUIT.l); // high collar
|
|
167
|
+
}
|
|
168
|
+
// a stride: the far leg behind and higher, the near leg forward and lower
|
|
169
|
+
function legs(S, A, foot, claw) {
|
|
170
|
+
S.poly([[31, 42], [37, 42], [38, 49], [39, 55], [35, 55], [33, 49]], SUIT.d);
|
|
171
|
+
S.ell(39, 56.4, 4.4, 1.9, foot.d); S.dots([[43, 57], [41, 58]], claw);
|
|
172
|
+
S.poly([[24, 42], [31, 42], [31, 50], [30, 58], [25, 58], [24, 50]], SUIT.m);
|
|
173
|
+
S.poly([[28, 43], [31, 43], [31, 50], [30, 58], [28, 58], [29, 50]], SUIT.l);
|
|
174
|
+
S.ell(28.5, 59.4, 5.2, 2.1, foot.m); S.rect(25, 58, 31, 58, foot.l);
|
|
175
|
+
S.dots([[33, 60], [32, 61], [29, 61]], claw);
|
|
176
|
+
}
|
|
177
|
+
// the near arm, the body-animal's own, fist on the hip, elbow out
|
|
178
|
+
function nearArm(S, A, skin, claw) {
|
|
179
|
+
S.path([[20, 29], [15, 35], [21, 40]], skin.d, 4.6, 3.8);
|
|
180
|
+
S.path([[20, 28], [15, 34], [21, 39]], skin.m, 3, 2.6);
|
|
181
|
+
S.ell(22.5, 40, 2.2, 2, skin.l); S.dots([[24, 41], [22, 42]], claw);
|
|
182
|
+
S.ell(21, 26, 4.8, 3.4, SUIT.l); S.rect(18, 24, 24, 24, SUIT.hi); S.rect(19, 27, 23, 27, A.c);
|
|
183
|
+
}
|
|
184
|
+
// the far arm, reaching to the tool it carries
|
|
185
|
+
function farArm(S, A, skin, claw, hx) {
|
|
186
|
+
S.path([[40, 28], [44, 34], [hx - 2, 33]], skin.d, 4.2, 3.6);
|
|
187
|
+
S.path([[40, 27], [44, 33], [hx - 2, 32]], skin.m, 2.6, 2.4);
|
|
188
|
+
S.ell(hx, 32.5, 2.6, 2.4, skin.l); S.dots([[hx - 1, 35], [hx + 1, 35], [hx + 3, 34]], claw);
|
|
189
|
+
S.ell(39, 25, 3.6, 2.8, SUIT.l); S.rect(37, 23, 41, 23, A.c);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// the visor: a dark glass band over the eyes, rim in the character's colour, a strap
|
|
193
|
+
// running to the back of the head
|
|
194
|
+
function visor(S, A, x0, y0, x1, y1, strapTo) {
|
|
195
|
+
if (strapTo) S.line(x0, y0 + 2, strapTo[0], strapTo[1], A.d, 1.6);
|
|
196
|
+
S.rect(x0, y0, x1, y1, A.c);
|
|
197
|
+
S.rect(x0 + 1, y0 + 1, x1 - 1, y1 - 1, GLASS);
|
|
198
|
+
S.set(x0, y0, null); S.set(x0, y1, null);
|
|
199
|
+
S.rect(x0 + 2, y1 - 1, x1 - 2, y1 - 1, A.d); // the colour, caught along the glass
|
|
200
|
+
S.line(x1 - 3, y1 - 1, x1 - 2, y0 + 1, ICE, 1);
|
|
201
|
+
S.rect(x1 + 1, y0 + 1, x1 + 1, y1 - 1, A.d); // the front of the housing
|
|
202
|
+
}
|
|
203
|
+
// the bare eye: a dark socket, a slit of the character's colour
|
|
204
|
+
function slit(S, A, x, y, dark) { S.rect(x - 2, y - 2, x + 3, y, dark); S.rect(x, y, x + 2, y, A.c); S.set(x + 2, y, A.l); }
|
|
205
|
+
|
|
206
|
+
// ---------- ARTISAN: a ram's head on a lizard's body, the long spanner ----------
|
|
207
|
+
function artisan(withVisor) {
|
|
208
|
+
const S = sheet(), A = COLOUR.artisan;
|
|
209
|
+
const LZ = { m: '#4f7d5f', d: '#33574a', l: '#7fae7c' };
|
|
210
|
+
const WOOL = { m: '#b3a892', d: '#857b68', l: '#d6ccb4' };
|
|
211
|
+
const FACE = { m: '#8b767a', d: '#4a3b42', l: '#b39ea4' };
|
|
212
|
+
const HORN = { m: '#d8c491', d: '#8a7446', l: '#f6ebc4' };
|
|
213
|
+
const FAR = { m: '#a08c5c', d: '#6b5932', l: '#a08c5c' };
|
|
214
|
+
const STEEL = { m: '#93adbd', d: '#5b7687', l: '#d9eaf2' };
|
|
215
|
+
|
|
216
|
+
ground(S, A);
|
|
217
|
+
// the lizard's tail: heavy at the hip, swung out toward the viewer, plated along the top
|
|
218
|
+
S.path([[25, 41], [17, 48], [10, 54], [5, 56], [2, 53]], LZ.d, 6.6, 1.4);
|
|
219
|
+
S.path([[25, 40], [17, 47], [10, 53], [6, 55]], LZ.m, 3.6, 1);
|
|
220
|
+
S.path([[24, 39], [17, 45], [11, 50]], LZ.l, 1.4, 1); // light along the top of the tail
|
|
221
|
+
S.rect(20, 42, 21, 43, A.c); S.rect(13, 48, 14, 49, A.c);
|
|
222
|
+
|
|
223
|
+
legs(S, A, LZ, WOOL.l);
|
|
224
|
+
S.ell(21, 28, 5.6, 5, SUIT.s); S.ell(40, 27, 4.6, 4.4, SUIT.m); // broad round shoulders under the plate
|
|
225
|
+
torso(S, A);
|
|
226
|
+
S.poly([[26, 25], [40, 24], [41, 29], [38, 35], [27, 35]], SUIT.l); // a deeper breastplate
|
|
227
|
+
S.path([[36, 25], [40, 25], [41, 28]], SUIT.hi, 1);
|
|
228
|
+
S.rect(30, 28, 36, 30, A.c); S.rect(32, 29, 34, 29, A.l); // the lit core in the chest
|
|
229
|
+
S.rect(24, 43, 27, 46, SUIT.l); S.rect(34, 43, 36, 45, SUIT.l); // tool pouches on the belt
|
|
230
|
+
|
|
231
|
+
// neck: short dense wool
|
|
232
|
+
S.rect(28, 17, 35, 21, WOOL.d);
|
|
233
|
+
// the far horn first: its curl rising behind the skull
|
|
234
|
+
horn(S, 31, 10, 7.5, 6, -160, -20, 3.4, 2, FAR, 5);
|
|
235
|
+
// skull, close wool
|
|
236
|
+
S.ell(28, 11, 7, 6.5, WOOL.m); S.ell(26, 13, 5, 4.4, WOOL.d);
|
|
237
|
+
// the long bony ram's face, nose line bowed out
|
|
238
|
+
S.poly([[31, 5], [37, 5], [42, 8], [46, 13], [48, 18], [46, 21], [38, 21], [32, 19], [30, 11]], FACE.m);
|
|
239
|
+
S.path([[33, 6], [37, 6], [42, 9], [46, 14]], FACE.l, 1.4);
|
|
240
|
+
S.poly([[32, 15], [39, 17], [38, 20], [33, 19]], '#73616a'); // the cheek, in shade
|
|
241
|
+
S.path([[37, 20], [46, 20]], FACE.d, 1.4);
|
|
242
|
+
S.set(47, 16, FACE.d); S.path([[42, 19], [47, 18]], FACE.d, 1); // nostril, mouth
|
|
243
|
+
S.poly([[38, 21], [41, 21], [40, 25]], WOOL.d); // beard
|
|
244
|
+
// ear, inside the curl
|
|
245
|
+
S.poly([[26, 12], [20, 15], [22, 17], [27, 15]], FACE.m); S.path([[25, 14], [22, 15]], FACE.d, 1);
|
|
246
|
+
// the near horn: up off the brow, back, down, and forward to a point at the cheek
|
|
247
|
+
horn(S, 24, 12, 8.6, 5.4, -62, -352, 5.2, 1.6, HORN, 10);
|
|
248
|
+
|
|
249
|
+
if (withVisor) visor(S, A, 33, 7, 44, 13, null);
|
|
250
|
+
else slit(S, A, 37, 11, FACE.d);
|
|
251
|
+
|
|
252
|
+
// the spanner, carried like a staff: a long shaft, an open jaw at the head
|
|
253
|
+
S.rect(51, 18, 52, 58, STEEL.d); S.rect(52, 19, 52, 57, STEEL.m);
|
|
254
|
+
S.ell(51.5, 14, 4.6, 4.6, STEEL.d); S.ell(51.5, 14, 3.2, 3.2, STEEL.m);
|
|
255
|
+
S.rect(50, 8, 53, 14, null); // the jaw
|
|
256
|
+
S.rect(51, 24, 52, 25, A.c); S.rect(50, 57, 53, 58, STEEL.d);
|
|
257
|
+
farArm(S, A, LZ, WOOL.l, 51);
|
|
258
|
+
nearArm(S, A, LZ, WOOL.l);
|
|
259
|
+
S.ell(20, 25.5, 6, 4.2, SUIT.l); S.ell(21.5, 23.6, 4, 1.4, SUIT.hi); S.rect(17, 28, 23, 28, A.c); // heavy round pauldrons
|
|
260
|
+
S.ell(40, 24.5, 4.6, 3.2, SUIT.l); S.ell(41, 23.2, 3, 1.1, SUIT.hi); S.rect(38, 26, 42, 26, A.c);
|
|
261
|
+
|
|
262
|
+
S.shade(lit(A));
|
|
263
|
+
S.outline();
|
|
264
|
+
return S;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------- ARTIST: an ibis's head on an octopus's body, the light-brush ----------
|
|
268
|
+
function artist(withVisor) {
|
|
269
|
+
const S = sheet(), A = COLOUR.artist;
|
|
270
|
+
const FE = { m: '#e6e3ee', d: '#a59fbb', l: '#ffffff' };
|
|
271
|
+
const BEAK = { m: '#b3a6dd', d: '#6f609f', l: '#e3dbf7', face: '#3a3058' };
|
|
272
|
+
const OC = { m: '#6657aa', d: '#453a80', l: '#8f80d0', suck: '#cdbcf5' };
|
|
273
|
+
|
|
274
|
+
ground(S, A);
|
|
275
|
+
// the far arms under the hem, higher and darker
|
|
276
|
+
S.path([[24, 52], [16, 53], [10, 50], [8, 46]], OC.d, 4.2, 1.2);
|
|
277
|
+
S.path([[39, 52], [47, 53], [53, 50], [55, 46]], OC.d, 4.2, 1.2);
|
|
278
|
+
// the near ones, lower and lit
|
|
279
|
+
S.path([[25, 54], [19, 59], [12, 60], [8, 57]], OC.m, 4.6, 1.2);
|
|
280
|
+
S.path([[37, 54], [43, 59], [50, 60], [54, 57]], OC.m, 4.6, 1.2);
|
|
281
|
+
S.rect(13, 59, 14, 59, OC.suck); S.rect(48, 59, 49, 59, OC.suck);
|
|
282
|
+
|
|
283
|
+
// a long coat to the ground, turned three-quarters, open down the front, a lit hem
|
|
284
|
+
S.poly([[22, 24], [38, 23], [39, 28], [38, 40], [42, 54], [19, 54], [23, 40], [21, 29]], SUIT.m);
|
|
285
|
+
S.poly([[28, 25], [37, 24], [38, 28], [37, 40], [41, 53], [29, 53], [28, 40]], SUIT.l);
|
|
286
|
+
S.poly([[22, 24], [25, 24], [26, 38], [23, 40], [21, 29]], SUIT.s); // the near flank, out of the light
|
|
287
|
+
S.path([[25, 41], [22, 53]], SUIT.d, 1.6); S.path([[31, 42], [31, 53]], SUIT.m, 1.6); // folds
|
|
288
|
+
S.path([[38, 41], [40, 52]], SUIT.hi, 1); // the lit fall of the cloth
|
|
289
|
+
S.path([[33, 25], [35, 53]], A.d, 1);
|
|
290
|
+
S.rect(19, 54, 42, 55, A.c); S.rect(19, 53, 42, 53, A.d);
|
|
291
|
+
S.rect(23, 38, 39, 39, SUIT.d); S.rect(35, 38, 37, 42, A.c); // sash, its tail
|
|
292
|
+
S.rect(28, 21, 36, 23, SUIT.d); S.rect(28, 21, 36, 21, A.d); // high collar
|
|
293
|
+
|
|
294
|
+
// the ibis: a long white neck
|
|
295
|
+
S.path([[31, 22], [29, 19], [29, 16]], FE.d, 4.4);
|
|
296
|
+
S.path([[32, 22], [30, 19], [30, 16]], FE.m, 2.2);
|
|
297
|
+
// head, two plumes off the back of it
|
|
298
|
+
S.ell(29, 11, 7, 7, FE.m); S.ell(26, 13, 4.4, 4.4, FE.d); S.ell(28, 11, 5.4, 5.4, FE.m);
|
|
299
|
+
S.path([[23, 9], [18, 11], [16, 15]], FE.m, 2, 1); S.path([[23, 12], [19, 15], [18, 19]], FE.d, 1.6, 1);
|
|
300
|
+
// the bare dark face and the long down-curved beak
|
|
301
|
+
S.ell(34, 11, 3.4, 3.8, BEAK.face);
|
|
302
|
+
S.path([[36, 11], [41, 12], [45, 15], [47, 19], [48, 24]], BEAK.d, 4.8, 1.2);
|
|
303
|
+
S.path([[36, 10], [41, 11], [45, 14], [47, 18], [48, 23]], BEAK.m, 3, 1);
|
|
304
|
+
S.path([[37, 9], [41, 10], [44, 12]], BEAK.l, 1);
|
|
305
|
+
|
|
306
|
+
if (withVisor) visor(S, A, 28, 7, 37, 12, [22, 9]);
|
|
307
|
+
else slit(S, A, 32, 10, BEAK.face);
|
|
308
|
+
|
|
309
|
+
// the light-brush: a long thin rod, the head burning in her colour
|
|
310
|
+
S.path([[44, 44], [55, 22]], '#5b7687', 1.8); S.path([[45, 42], [55, 22]], '#93adbd', 1);
|
|
311
|
+
S.poly([[54, 22], [57, 22], [59, 17], [58, 12], [55, 17]], A.c);
|
|
312
|
+
S.path([[56, 21], [57, 15]], A.l, 1); S.set(58, 11, ICE);
|
|
313
|
+
// the far arm, wound once round the rod
|
|
314
|
+
S.path([[38, 27], [43, 33], [48, 35], [51, 32], [49, 30]], OC.d, 4, 1.6);
|
|
315
|
+
S.path([[38, 27], [43, 32], [48, 34], [50, 32]], OC.m, 2.4, 1.2);
|
|
316
|
+
// the near arm, curled at the hip
|
|
317
|
+
S.path([[22, 29], [18, 37], [19, 44], [16, 48]], OC.d, 4.4, 1.6);
|
|
318
|
+
S.path([[22, 28], [18, 36], [19, 43]], OC.m, 2.6, 1.4);
|
|
319
|
+
// a shawl over both shoulders, a lit edge: cloth, where the others wear plate
|
|
320
|
+
S.poly([[22, 23], [38, 22], [41, 27], [37, 30], [30, 32], [24, 30], [19, 28]], '#2a2350');
|
|
321
|
+
S.poly([[27, 23], [37, 22], [39, 26], [33, 27], [28, 26]], '#3d3378'); // light across the shawl
|
|
322
|
+
S.path([[19, 28], [24, 30], [30, 32], [37, 30], [41, 27]], A.c, 1.4);
|
|
323
|
+
S.rect(28, 21, 36, 22, '#2a2350'); S.rect(28, 21, 36, 21, A.d);
|
|
324
|
+
|
|
325
|
+
S.shade(lit(A));
|
|
326
|
+
S.outline();
|
|
327
|
+
return S;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---------- PATRON: a falcon's head on a lion's body, the staff of office ----------
|
|
331
|
+
function patron(withVisor) {
|
|
332
|
+
const S = sheet(), A = COLOUR.patron;
|
|
333
|
+
const FE = { m: '#e9e5db', d: '#aaa392', l: '#ffffff' };
|
|
334
|
+
const CAP = { m: '#44516c', d: '#28324a', l: '#62749a' };
|
|
335
|
+
const LI = { m: '#b98d55', d: '#85602f', l: '#dcb682', tuft: '#573820' };
|
|
336
|
+
const BEAK = { m: '#8090aa', d: '#262e40', cere: '#e0b040' };
|
|
337
|
+
|
|
338
|
+
ground(S, A);
|
|
339
|
+
// the lion's tail: thin, swung out, a dark tuft, a ring of office on it
|
|
340
|
+
S.path([[18, 50], [11, 57], [6, 55], [4, 49], [6, 44]], LI.d, 3, 2.2);
|
|
341
|
+
S.path([[18, 49], [11, 56], [6, 54]], LI.m, 1.4, 1.2);
|
|
342
|
+
S.ell(7, 40, 2.6, 3.8, LI.tuft); S.set(7, 36, LI.tuft); S.rect(5, 44, 8, 44, A.c);
|
|
343
|
+
|
|
344
|
+
legs(S, A, LI, LI.tuft);
|
|
345
|
+
torso(S, A);
|
|
346
|
+
S.path([[24, 25], [36, 40]], A.c, 2); S.path([[25, 25], [37, 39]], A.d, 1); // the sash of office
|
|
347
|
+
S.ell(32, 41.5, 1.8, 1.8, A.c); S.set(32, 41, A.l);
|
|
348
|
+
S.rect(28, 23, 36, 23, A.c); // gold at the collar
|
|
349
|
+
// the cape of office: over the near shoulder, to the ground, a gold edge
|
|
350
|
+
S.poly([[17, 25], [27, 24], [27, 34], [25, 45], [25, 56], [12, 54], [14, 40]], '#27386a');
|
|
351
|
+
S.poly([[17, 25], [21, 25], [18, 40], [16, 54], [12, 54], [14, 40]], '#1a2748');
|
|
352
|
+
S.path([[23, 28], [21, 42], [20, 55]], '#3d5290', 1.6); // a lit fold
|
|
353
|
+
S.path([[25, 34], [23, 46], [23, 55]], '#1a2748', 1); // a deep one
|
|
354
|
+
S.ell(21, 26, 4, 1.6, '#3d5290'); // light on the shoulder
|
|
355
|
+
S.path([[27, 24], [27, 34], [25, 45], [25, 56]], A.c, 1.6);
|
|
356
|
+
S.path([[12, 54], [25, 56]], A.c, 1.6);
|
|
357
|
+
S.ell(26, 25, 2, 2, A.c); S.set(26, 24, A.l);
|
|
358
|
+
|
|
359
|
+
// neck: white feathers
|
|
360
|
+
S.rect(28, 18, 35, 21, FE.d); S.rect(31, 18, 35, 21, FE.m);
|
|
361
|
+
// head: the falcon. Slate cap and nape, white cheek and throat
|
|
362
|
+
S.poly([[23, 8], [17, 10], [22, 12]], CAP.d); S.poly([[22, 12], [17, 15], [23, 16]], CAP.d); // nape feathers
|
|
363
|
+
S.ell(29, 11, 7.6, 7.2, CAP.m); S.ell(28, 6, 5, 2, CAP.l); S.ell(23, 13, 2.6, 4.4, CAP.d);
|
|
364
|
+
S.ell(33, 15, 5, 4.4, FE.m); S.ell(31, 17, 4.4, 2.4, FE.m);
|
|
365
|
+
S.path([[34, 13], [33, 18]], CAP.d, 1.6); // the falcon's tear
|
|
366
|
+
// the hooked beak: yellow cere, blue-grey bill, dark tip turned down
|
|
367
|
+
S.poly([[37, 8], [43, 9], [47, 12], [48, 16], [47, 22], [45, 19], [43, 16], [38, 15]], BEAK.m);
|
|
368
|
+
S.poly([[46, 15], [48, 16], [47, 22], [45, 19]], BEAK.d);
|
|
369
|
+
S.ell(38, 12, 1.8, 2.2, BEAK.cere); S.path([[38, 16], [43, 16]], BEAK.d, 1);
|
|
370
|
+
S.path([[29, 8], [37, 9]], CAP.d, 2); // the heavy brow
|
|
371
|
+
|
|
372
|
+
if (withVisor) visor(S, A, 28, 7, 37, 12, [21, 9]);
|
|
373
|
+
else slit(S, A, 33, 11, CAP.d);
|
|
374
|
+
|
|
375
|
+
// the staff: a tall rod, a ring of light at its head
|
|
376
|
+
S.rect(52, 11, 53, 58, '#5b7687'); S.rect(53, 12, 53, 57, '#93adbd');
|
|
377
|
+
S.ell(52.5, 6, 4.6, 4.6, A.c); S.ell(52.5, 6, 2.6, 2.6, null); S.ell(52.5, 6, 1, 1, ICE);
|
|
378
|
+
S.rect(51, 11, 54, 12, A.d); S.rect(51, 57, 54, 58, A.d);
|
|
379
|
+
farArm(S, A, LI, LI.tuft, 52);
|
|
380
|
+
S.ell(39, 24.5, 4.2, 2.8, A.c); S.rect(36, 26, 42, 26, A.d); // the epaulette of office
|
|
381
|
+
|
|
382
|
+
S.shade(lit(A));
|
|
383
|
+
S.outline();
|
|
384
|
+
return S;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ---------- PNG ----------
|
|
388
|
+
function hex(c) { return [parseInt(c.slice(1, 3), 16), parseInt(c.slice(3, 5), 16), parseInt(c.slice(5, 7), 16)]; }
|
|
389
|
+
function png(w, h, rgba) {
|
|
390
|
+
const raw = Buffer.alloc((w * 4 + 1) * h);
|
|
391
|
+
for (let y = 0; y < h; y++) { raw[y * (w * 4 + 1)] = 0; rgba.copy(raw, y * (w * 4 + 1) + 1, y * w * 4, (y + 1) * w * 4); }
|
|
392
|
+
const chunk = (type, data) => {
|
|
393
|
+
const len = Buffer.alloc(4); len.writeUInt32BE(data.length);
|
|
394
|
+
const body = Buffer.concat([Buffer.from(type), data]);
|
|
395
|
+
const crc = Buffer.alloc(4); crc.writeUInt32BE(zlib.crc32(body) >>> 0);
|
|
396
|
+
return Buffer.concat([len, body, crc]);
|
|
397
|
+
};
|
|
398
|
+
const head = Buffer.alloc(13); head.writeUInt32BE(w, 0); head.writeUInt32BE(h, 4); head[8] = 8; head[9] = 6;
|
|
399
|
+
return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk('IHDR', head), chunk('IDAT', zlib.deflateSync(raw, { level: 9 })), chunk('IEND', Buffer.alloc(0))]);
|
|
400
|
+
}
|
|
401
|
+
function spritePng(S) {
|
|
402
|
+
const b = Buffer.alloc(N * N * 4);
|
|
403
|
+
S.px.forEach((c, i) => { if (!c) return; const [r, g, bl] = hex(c); b[i * 4] = r; b[i * 4 + 1] = g; b[i * 4 + 2] = bl; b[i * 4 + 3] = 255; });
|
|
404
|
+
return png(N, N, b);
|
|
405
|
+
}
|
|
406
|
+
// the head: a 32x32 window on the top of the figure, where every head sits
|
|
407
|
+
const FACE = { x: 16, y: 0, size: 32 };
|
|
408
|
+
function facePng(S) {
|
|
409
|
+
const n = FACE.size, b = Buffer.alloc(n * n * 4);
|
|
410
|
+
for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) {
|
|
411
|
+
const c = S.get(FACE.x + x, FACE.y + y); if (!c) continue;
|
|
412
|
+
const [r, g, bl] = hex(c), i = (y * n + x) * 4;
|
|
413
|
+
b[i] = r; b[i + 1] = g; b[i + 2] = bl; b[i + 3] = 255;
|
|
414
|
+
}
|
|
415
|
+
return png(n, n, b);
|
|
416
|
+
}
|
|
417
|
+
function contact(list, k) {
|
|
418
|
+
const cols = 3, rows = Math.ceil(list.length / cols), pad = 2, cell = N + pad * 2;
|
|
419
|
+
const w = cols * cell * k, h = rows * cell * k, b = Buffer.alloc(w * h * 4);
|
|
420
|
+
const [br, bg, bb] = hex('#050d16');
|
|
421
|
+
for (let i = 0; i < w * h; i++) { b[i * 4] = br; b[i * 4 + 1] = bg; b[i * 4 + 2] = bb; b[i * 4 + 3] = 255; }
|
|
422
|
+
list.forEach((S, n) => {
|
|
423
|
+
const ox = (n % cols) * cell + pad, oy = Math.floor(n / cols) * cell + pad;
|
|
424
|
+
for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
|
|
425
|
+
const c = S.get(x, y); if (!c) continue;
|
|
426
|
+
const [r, g, bl] = hex(c);
|
|
427
|
+
for (let dy = 0; dy < k; dy++) for (let dx = 0; dx < k; dx++) {
|
|
428
|
+
const i = (((oy + y) * k + dy) * w + (ox + x) * k + dx) * 4;
|
|
429
|
+
b[i] = r; b[i + 1] = g; b[i + 2] = bl;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
return png(w, h, b);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const DRAW = { artisan, artist, patron };
|
|
437
|
+
// the nine files the hall serves, by name: what --hall writes, and what the test holds the
|
|
438
|
+
// checked-in pictures against
|
|
439
|
+
function hallFiles() {
|
|
440
|
+
const out = {};
|
|
441
|
+
for (const id of Object.keys(DRAW)) {
|
|
442
|
+
out[id + '.png'] = spritePng(DRAW[id](false));
|
|
443
|
+
out[id + '-face.png'] = facePng(DRAW[id](false));
|
|
444
|
+
out[id + '-visor.png'] = facePng(DRAW[id](true));
|
|
445
|
+
}
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
if (require.main === module) {
|
|
449
|
+
const all = [];
|
|
450
|
+
for (const id of Object.keys(DRAW)) {
|
|
451
|
+
const bare = DRAW[id](false), worn = DRAW[id](true);
|
|
452
|
+
fs.writeFileSync(path.join(__dirname, id + '.png'), spritePng(bare));
|
|
453
|
+
fs.writeFileSync(path.join(__dirname, id + '-visor.png'), spritePng(worn));
|
|
454
|
+
all.push(bare);
|
|
455
|
+
}
|
|
456
|
+
for (const id of Object.keys(DRAW)) all.push(DRAW[id](true));
|
|
457
|
+
fs.writeFileSync(path.join(__dirname, 'contact.png'), contact(all, 6));
|
|
458
|
+
if (process.argv.includes('--hall')) {
|
|
459
|
+
const hall = path.join(__dirname, '..', '..', '..', '..', 'modules', 'hall-ui', 'public', 'characters');
|
|
460
|
+
for (const [name, buf] of Object.entries(hallFiles())) fs.writeFileSync(path.join(hall, name), buf);
|
|
461
|
+
console.log('wrote the hall pictures to ' + path.relative(process.cwd(), hall));
|
|
462
|
+
}
|
|
463
|
+
console.log('drew ' + Object.keys(DRAW).join(', ') + ' (plain and visor) and contact.png');
|
|
464
|
+
}
|
|
465
|
+
module.exports = { DRAW, COLOUR, N, FACE, hallFiles };
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Characters, second look</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@400;500;600&family=Barlow:wght@400;500&display=swap">
|
|
10
|
+
<style>
|
|
11
|
+
/* A mock-up, not a hall page: the hall's night, ice and salmon are typed in here so the
|
|
12
|
+
sheet opens from a file. The pictures are drawn by draw-characters.js. */
|
|
13
|
+
:root { --bg: #050d16; --card: #0a1622; --ink: #fff; --ice: #c8f1f7; --rule: rgba(200, 241, 247, .2); --night: rgba(5, 13, 22, .72); }
|
|
14
|
+
* { box-sizing: border-box; }
|
|
15
|
+
body { margin: 0; padding: 40px; background: var(--bg); color: var(--ink); font: 400 17px/1.45 Barlow, system-ui, sans-serif; width: 1280px; }
|
|
16
|
+
h1, h2, .name, .cap, .place, .btn, .hint, .unit { font-family: "Barlow Condensed", sans-serif; font-weight: 500; text-transform: uppercase; letter-spacing: .08em; }
|
|
17
|
+
h1 { font-size: 34px; margin: 0 0 4px; }
|
|
18
|
+
.lede { color: var(--ice); margin: 0 0 32px; max-width: 60em; }
|
|
19
|
+
h2 { font-size: 20px; color: var(--ice); margin: 40px 0 14px; padding-top: 14px; border-top: 1px solid var(--rule); }
|
|
20
|
+
img { image-rendering: pixelated; display: block; }
|
|
21
|
+
|
|
22
|
+
.trio { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
|
|
23
|
+
.door { background: var(--card); padding: 20px; border-top: 2px solid var(--c); clip-path: polygon(0 0, 100% 0, 100% calc(100% - 18px), calc(100% - 18px) 100%, 0 100%); }
|
|
24
|
+
.door .pair { display: flex; align-items: flex-end; justify-content: center; gap: 8px; }
|
|
25
|
+
.door .pair img { width: 192px; height: 192px; }
|
|
26
|
+
.door .pair img.was { width: 120px; height: auto; opacity: .85; image-rendering: pixelated; }
|
|
27
|
+
.door .arrow { color: var(--ice); font-size: 22px; padding-bottom: 80px; }
|
|
28
|
+
.name { font-size: 30px; margin-top: 12px; color: var(--c); }
|
|
29
|
+
.mix { color: var(--ice); margin: 2px 0 8px; }
|
|
30
|
+
.swatch { display: inline-flex; align-items: center; gap: 8px; color: var(--ice); font-size: 15px; }
|
|
31
|
+
.swatch i { width: 16px; height: 16px; background: var(--c); display: inline-block; }
|
|
32
|
+
.sizes { display: flex; align-items: flex-end; gap: 18px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--rule); }
|
|
33
|
+
.sizes span { color: var(--ice); font-size: 13px; display: block; margin-top: 4px; text-align: center; }
|
|
34
|
+
|
|
35
|
+
/* the visor, tinted */
|
|
36
|
+
.glass { position: relative; height: 340px; margin-bottom: 20px; overflow: hidden;
|
|
37
|
+
background: linear-gradient(to bottom, var(--night), transparent 110px, transparent 55%, var(--night)),
|
|
38
|
+
radial-gradient(120% 90% at 50% 120%, #f0b08a 0, #7f6a8f 30%, #25405f 60%, #0b1c30 100%); }
|
|
39
|
+
.glass svg { position: absolute; left: 0; width: 100%; overflow: visible; }
|
|
40
|
+
.glass svg path { fill: none; stroke: var(--c); stroke-width: 1.5; vector-effect: non-scaling-stroke; opacity: .9; }
|
|
41
|
+
.glass svg.top { top: 14px; height: 42px; } .glass svg.foot { bottom: 22px; height: 18px; }
|
|
42
|
+
.place { position: absolute; left: 50%; top: 38px; transform: translateX(-50%); font-size: 22px; letter-spacing: .24em; color: var(--ice); text-shadow: 0 0 6px var(--bg); }
|
|
43
|
+
.sight { position: absolute; left: 50%; top: 50%; width: 24px; height: 24px; margin: -12px; border: 1.5px solid var(--c); border-radius: 50%; }
|
|
44
|
+
.sight::after { content: ""; position: absolute; left: 8px; top: 8px; width: 5px; height: 5px; border-radius: 50%; background: var(--c); }
|
|
45
|
+
.foot { position: absolute; left: 48px; right: 48px; bottom: 64px; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; }
|
|
46
|
+
.me { display: flex; align-items: center; gap: 14px; transform: perspective(900px) rotateY(16deg); transform-origin: left center; }
|
|
47
|
+
.face { width: 76px; height: 76px; flex: none; background-color: var(--card); background-repeat: no-repeat; background-size: 192px 192px; image-rendering: pixelated;
|
|
48
|
+
border: 2px solid var(--c); clip-path: polygon(0 0, 100% 0, 100% calc(100% - 12px), calc(100% - 12px) 100%, 0 100%); }
|
|
49
|
+
.who .cap { font-size: 14px; color: var(--c); line-height: 1; }
|
|
50
|
+
.who .num { font: 400 34px/1 "Barlow Condensed", sans-serif; }
|
|
51
|
+
.unit { font-size: 16px; color: var(--ice); }
|
|
52
|
+
.btn { justify-self: center; display: inline-flex; align-items: center; gap: 14px; min-height: 56px; padding: 6px 14px 6px 24px; background: var(--c); color: #1c1206; font-size: 26px; font-weight: 600; letter-spacing: .04em;
|
|
53
|
+
clip-path: polygon(0 0, 100% 0, 100% calc(100% - 14px), calc(100% - 14px) 100%, 0 100%); }
|
|
54
|
+
kbd { display: inline-flex; align-items: center; justify-content: center; min-width: 24px; height: 24px; padding: 0 5px; border: 1.5px solid currentColor; font: 500 14px/1 "Barlow Condensed", sans-serif; }
|
|
55
|
+
.hints { justify-self: end; display: flex; gap: 20px; transform: perspective(900px) rotateY(-16deg); transform-origin: right center; }
|
|
56
|
+
.hint { display: inline-flex; align-items: center; gap: 6px; font-size: 18px; letter-spacing: .04em; color: var(--ice); text-shadow: 0 0 6px var(--bg); }
|
|
57
|
+
.hint kbd { background: var(--bg); }
|
|
58
|
+
.note { color: var(--ice); font-size: 15px; margin: 6px 0 0; }
|
|
59
|
+
</style>
|
|
60
|
+
</head>
|
|
61
|
+
<body>
|
|
62
|
+
<h1>Characters, second look</h1>
|
|
63
|
+
<p class="lede">Still two animals in one body: one gives the head, the other the body. They stand three-quarters to you on a lit disc, in armour and long coats, each drawn at 64 by 64 with one colour of its own, carrying the tool of its role, and with a second picture wearing the visor.</p>
|
|
64
|
+
|
|
65
|
+
<h2>Who they are</h2>
|
|
66
|
+
<div class="trio">
|
|
67
|
+
<div class="door" style="--c:#fb9569">
|
|
68
|
+
<div class="pair"><img src="artisan.png" alt=""></div>
|
|
69
|
+
<div class="name">Artisan</div><div class="mix">A ram's head on a lizard's body. Carries the long spanner: the one who builds.</div>
|
|
70
|
+
<span class="swatch"><i></i>Ember</span>
|
|
71
|
+
<div class="sizes"><div><img src="artisan.png" width="64" height="64" alt=""><span>64</span></div><div><img src="artisan.png" width="128" height="128" alt=""><span>128</span></div><div><img src="artisan-visor.png" width="128" height="128" alt=""><span>visor on</span></div></div>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="door" style="--c:#c59bff">
|
|
74
|
+
<div class="pair"><img src="artist.png" alt=""></div>
|
|
75
|
+
<div class="name">Artist</div><div class="mix">An ibis's head on an octopus's body. Carries the light-brush: the one who shapes the look.</div>
|
|
76
|
+
<span class="swatch"><i></i>Violet</span>
|
|
77
|
+
<div class="sizes"><div><img src="artist.png" width="64" height="64" alt=""><span>64</span></div><div><img src="artist.png" width="128" height="128" alt=""><span>128</span></div><div><img src="artist-visor.png" width="128" height="128" alt=""><span>visor on</span></div></div>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="door" style="--c:#ffd166">
|
|
80
|
+
<div class="pair"><img src="patron.png" alt=""></div>
|
|
81
|
+
<div class="name">Patron</div><div class="mix">A falcon's head on a lion's body. Carries the staff of office: the one who decides.</div>
|
|
82
|
+
<span class="swatch"><i></i>Gold</span>
|
|
83
|
+
<div class="sizes"><div><img src="patron.png" width="64" height="64" alt=""><span>64</span></div><div><img src="patron.png" width="128" height="128" alt=""><span>128</span></div><div><img src="patron-visor.png" width="128" height="128" alt=""><span>visor on</span></div></div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<h2>The visor takes your colour, and you wear it</h2>
|
|
88
|
+
<div class="glass" style="--c:#fb9569">
|
|
89
|
+
<svg class="top" viewBox="0 0 1000 100" preserveAspectRatio="none"><path d="M76 100Q500 -100 924 100"/></svg>
|
|
90
|
+
<svg class="foot" viewBox="0 0 1000 100" preserveAspectRatio="none"><path d="M76 0Q500 200 924 0"/></svg>
|
|
91
|
+
<p class="place" style="margin:0">Cloud city</p><span class="sight"></span>
|
|
92
|
+
<div class="foot"><div class="me"><span class="face" style="background-image:url(artisan-visor.png);background-position:-58px 2px"></span><span class="who"><span class="cap">Artisan</span><br><span class="num">1,240</span> <span class="unit">credits</span></span></div>
|
|
93
|
+
<span class="btn">Claim a task <kbd>C</kbd></span>
|
|
94
|
+
<div class="hints"><span class="hint"><kbd>A</kbd>Actions</span><span class="hint"><kbd>Esc</kbd>Options</span></div></div>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="glass" style="--c:#c59bff">
|
|
97
|
+
<svg class="top" viewBox="0 0 1000 100" preserveAspectRatio="none"><path d="M76 100Q500 -100 924 100"/></svg>
|
|
98
|
+
<svg class="foot" viewBox="0 0 1000 100" preserveAspectRatio="none"><path d="M76 0Q500 200 924 0"/></svg>
|
|
99
|
+
<p class="place" style="margin:0">Cloud city</p><span class="sight"></span>
|
|
100
|
+
<div class="foot"><div class="me"><span class="face" style="background-image:url(artist-visor.png);background-position:-58px 2px"></span><span class="who"><span class="cap">Artist</span><br><span class="num">860</span> <span class="unit">credits</span></span></div>
|
|
101
|
+
<span class="btn">Review this page <kbd>R</kbd></span>
|
|
102
|
+
<div class="hints"><span class="hint"><kbd>A</kbd>Actions</span><span class="hint"><kbd>Esc</kbd>Options</span></div></div>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="glass" style="--c:#ffd166">
|
|
105
|
+
<svg class="top" viewBox="0 0 1000 100" preserveAspectRatio="none"><path d="M76 100Q500 -100 924 100"/></svg>
|
|
106
|
+
<svg class="foot" viewBox="0 0 1000 100" preserveAspectRatio="none"><path d="M76 0Q500 200 924 0"/></svg>
|
|
107
|
+
<p class="place" style="margin:0">Cloud city</p><span class="sight"></span>
|
|
108
|
+
<div class="foot"><div class="me"><span class="face" style="background-image:url(patron-visor.png);background-position:-62px 2px"></span><span class="who"><span class="cap">Patron</span><br><span class="num">2,015</span> <span class="unit">credits</span></span></div>
|
|
109
|
+
<span class="btn">Governor city <kbd>V</kbd></span>
|
|
110
|
+
<div class="hints"><span class="hint"><kbd>A</kbd>Actions</span><span class="hint"><kbd>Esc</kbd>Options</span></div></div>
|
|
111
|
+
</div>
|
|
112
|
+
<p class="note">A mock-up: nothing in the hall has changed yet. The numbers are made up.</p>
|
|
113
|
+
</body>
|
|
114
|
+
</html>
|
|
@@ -3436,5 +3436,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3436
3436
|
landed since 1.24.92 with no explicit bump. run 43299203682. (task 1002620)
|
|
3437
3437
|
1.24.94 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3438
3438
|
landed since 1.24.93 with no explicit bump. run 43302410261. (task 1002620)
|
|
3439
|
+
1.24.95 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3440
|
+
landed since 1.24.94 with no explicit bump. run 43304959846. (task 1002620)
|
|
3439
3441
|
---------------------------------------------------------------------------
|
|
3440
3442
|
```
|