@brunosouzas/design-system 0.4.0 → 0.5.1
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/README.md +43 -4
- package/dist/src/pieces/cast.d.ts +32 -0
- package/dist/src/pieces/cast.js +280 -0
- package/dist/src/pieces/diagram.js +1 -9
- package/dist/src/pieces/index.d.ts +1 -0
- package/dist/src/pieces/index.js +1 -0
- package/dist/src/pieces/ink.d.ts +5 -0
- package/dist/src/pieces/ink.js +10 -0
- package/dist/src/pieces/library.d.ts +8 -2
- package/dist/src/pieces/library.js +17 -5
- package/dist/src/pieces/metro.d.ts +4 -0
- package/dist/src/pieces/metro.js +46 -19
- package/dist/tokens/generated/tokens.d.ts +37 -0
- package/dist/tokens/generated/tokens.js +37 -0
- package/package.json +1 -1
- package/tokens/generated/tokens.css +21 -0
- package/tokens/generated/tokens.json +37 -0
- package/tokens/generated/tokens.ts +37 -0
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ The visual identity of [brunosouzas.com](https://brunosouzas.com): design tokens
|
|
|
25
25
|
|
|
26
26
|
## Status
|
|
27
27
|
|
|
28
|
-
Early. The first change of the [roadmap](docs/roadmap.md) is implemented: the token source and its CSS, TypeScript and JSON exports, the motion components, and the circuit-breaker state machine as the first catalogue scene (16:9 and 4:5, Portuguese and English), with commands to render its stills and videos. The first variety scene, `publish-subscribe`, is implemented with independent message copies, Publish acceptance, two ACKs and a local invoice NACK. Its owner stills/video approval is pending; implementation is not visual approval. The illustration pieces (roadmap item 3), the metro and diagram pieces (item 6) stackledger's map markers, zones and icons (AB#167, 0.3.0) and the body diagram pieces (AB#141, 0.3.2) are implemented: the static drawing library that covers, banners, diagrams and scenes share, with a command that builds a blog cover from a JSON spec. The published covers are not ported to it yet (item 5).
|
|
28
|
+
Early. The first change of the [roadmap](docs/roadmap.md) is implemented: the token source and its CSS, TypeScript and JSON exports, the motion components, and the circuit-breaker state machine as the first catalogue scene (16:9 and 4:5, Portuguese and English), with commands to render its stills and videos. The first variety scene, `publish-subscribe`, is implemented with independent message copies, Publish acceptance, two ACKs and a local invoice NACK. Its owner stills/video approval is pending; implementation is not visual approval. The illustration pieces (roadmap item 3), the metro and diagram pieces (item 6) stackledger's map markers, zones and icons (AB#167, 0.3.0) and the body diagram pieces (AB#141, 0.3.2) are implemented: the static drawing library that covers, banners, diagrams and scenes share, with a command that builds a blog cover from a JSON spec. The published covers are not ported to it yet (item 5). The cast of mascots and the emotes (AB#152, 0.5.0) are pieces too, with the approved cast motion test in the Studio; new videos are language-neutral by default ([ADR 0005](docs/adr/0005-neutral-videos-and-the-cast.md)).
|
|
29
29
|
|
|
30
30
|
## What it is
|
|
31
31
|
|
|
@@ -34,7 +34,8 @@ The blog's articles explain how systems behave: a circuit breaker opening, a pip
|
|
|
34
34
|
- **Design tokens, one source.** Colours with fixed meanings (green healthy, orange blocked, yellow transitional, red the failure path), the Lato type scale, the thick ink outline, the hard offset shadow, radii, and motion timings. Exported as CSS variables for the site, TypeScript for the animations and JSON for anything else (the SVG generators of the static diagrams).
|
|
35
35
|
- **Motion components.** React components for [Remotion](https://www.remotion.dev): an ink block with a hard shadow, a pill label, an arrow that draws itself, a token travelling along a transition, call-result squares, a countdown ring. Flow primitives in `src/brand/flow-components.tsx` add a white hexagonal `Exchange`, rail-ended `Queue`, yellow-tab `TaggedContainer` and persistent `PositionedToken` (stationary or path-positioned). Copy identities, splits and ACK/NACK rules belong to scenes, not to a generic message engine.
|
|
36
36
|
- **Illustration pieces.** The objects the blog's covers and diagrams draw again and again (pill label, sheet, code panel, robot, server stack, gear, road, the 26 EIP icons and more), each a pure TypeScript function that returns an SVG node tree from the tokens. The same tree becomes an SVG file for a cover and React elements for a scene, and a scene animates a piece by recomputing its parameters every frame. See [Illustration pieces and covers](#illustration-pieces-and-covers).
|
|
37
|
-
- **A
|
|
37
|
+
- **A cast and emotes.** Seven mascots that are the technical objects themselves (Dev, AI agent, Architect, DevOps, Data, Requester, Message) and eight reaction symbols with a fixed meaning, so a video can tell its story without words. See [The cast and the emotes](#the-cast-and-the-emotes).
|
|
38
|
+
- **A catalogue of scenes.** Each scene animates something a published article already says. The first two are in 16:9 (article) and 4:5 (LinkedIn), in Portuguese and English from one text table; new scenes are one language-neutral 16:9 video ([ADR 0005](docs/adr/0005-neutral-videos-and-the-cast.md)). The first is the [circuit-breaker state machine](docs/spikes/2026-10-22-motion-pilot/README.md).
|
|
38
39
|
|
|
39
40
|
The style follows the illustration covers of the [Spotify Engineering blog](https://engineering.atspotify.com/): flat shapes, a dark ink outline, a hard shadow with no blur, saturated colours on a calm page.
|
|
40
41
|
|
|
@@ -135,13 +136,14 @@ The components take every colour and size from the tokens, and what is visible a
|
|
|
135
136
|
|
|
136
137
|
### Maps and diagrams
|
|
137
138
|
|
|
138
|
-
Thirteen pieces draw a metro map and its page diagrams, for [stackledger](https://github.com/brunosouzas/stackledger) and any other map in this style: `lineBadge`, `stationMarker` (stop, interchange as a capsule, deprecated), `pipe`, `countBadge`, `nodeBlock`, `pillNode`, `hexNode` (normal, focus, placeholder), `messageArrow` (curved, straight or through two `controls` points, with a head and an optional label; `kind` `reply`, `failure` or `failFast` dashes it and paints it as ADR 0003 says), `ledgerMark` (stackledger's mark; `surface: "dark"` lines it with a light rim), `renewalRing` (orange bands round a station), `suggestedHalo` (a dashed muted circle), `licenceMark` (a 10-unit disc, green when held, white to buy) and `zone` (the outline the caller gives, widened by a round 32-unit stroke at the surface's zone opacity, with an optional title). They paint what the caller lays out: positions, hulls, text measurement and truncation stay with the map engine.
|
|
139
|
+
Thirteen pieces draw a metro map and its page diagrams, for [stackledger](https://github.com/brunosouzas/stackledger) and any other map in this style: `lineBadge`, `stationMarker` (stop, interchange as a capsule, deprecated), `pipe`, `countBadge`, `nodeBlock`, `pillNode`, `hexNode` (normal, focus, placeholder), `messageArrow` (curved, straight or through two `controls` points, with a head and an optional label; `kind` `reply`, `failure` or `failFast` dashes it and paints it as ADR 0003 says; `heads: "start"` or `"both"` puts heads at `from`, `dashed` overrides the kind's dash), `ledgerMark` (stackledger's mark; `surface: "dark"` lines it with a light rim), `renewalRing` (orange bands round a station), `suggestedHalo` (a dashed muted circle), `licenceMark` (a 10-unit disc, green when held, white to buy) and `zone` (the outline the caller gives, widened by a round 32-unit stroke at the surface's zone opacity, with an optional title). They paint what the caller lays out: positions, hulls, text measurement and truncation stay with the map engine.
|
|
139
140
|
|
|
140
141
|
- A `placeholder` node may have a `subtitle` (drawn like a normal node's, all muted), and `lineBadge` may have no `text` (the pill alone, 30 wide by default).
|
|
141
142
|
- The licence's disc is small, so it has its own room rule: `size` at least three outlines (9 at ink scale 1).
|
|
142
143
|
|
|
143
144
|
- `surface: "light" | "dark"` picks the background they sit on (`surface.*` tokens); every map and diagram piece takes it except `pipe`; `ledgerMark` takes only `surface`. On dark the hard shadow takes `surface.dark.shadow` and the outline gets a light rim ([ADR 0003](docs/adr/0003-visual-language-and-colour-meaning.md), amendment of AB#157).
|
|
144
145
|
- `inkScale` (0.25 to 8) thickens the outline, rim and shadow without changing the geometry, on `lineBadge`, `stationMarker`, `countBadge` and the three nodes; the map uses 1, the page diagrams 1.6. A shape's height (a marker's diameter) must be at least four outlines; a placeholder node, which draws no ink, is exempt.
|
|
146
|
+
- `countBadge` takes `textSize` (6 to 48, default 11): without `height`, the height, width and baseline grow with it.
|
|
145
147
|
- A line's colour comes from the map's data, so `pipe.fill`, `lineBadge.fill` and `zone.fill` also accept `#RRGGBB`. Every other colour is a token path, and a cover spec rejects any hex.
|
|
146
148
|
- `map.bendRadius` (32) is the radius of a bundle's centre line at a 45° or 90° bend; no piece reads it, the map engine draws the arcs (`pipe.d` takes `A`).
|
|
147
149
|
- The `map.*` tokens hold the measures (outline 3, shadow 3.5, line 12, casing 6, badge 20, node 46, ring 3, halo 1.5, licence 10, zone padding 32, bend radius 32…), `surface.*.zoneOpacity` the zone's opacity (0.12, dark 0.22), and `site.*` the page palette, fonts and type scale of brunosouzas.com in light and dark.
|
|
@@ -158,11 +160,47 @@ const badge = lineBadge({ x: 250, y: 40, text: "SH", fill: "#7B3FE4", surface: "
|
|
|
158
160
|
Ten pieces draw what the blog's body diagrams need beyond the map pieces: `diagramBox` (a box or pill with a fill and centred lines), `decisionNode` (diamond), `storeNode` (cylinder), `queueNode` (box with two rails), `exchangeNode` (hexagon), `taggedContainer` (a frame with its name on a tab), `lifeline` (dashed, or sparse dots once nothing calls it), `phaseBand` (a pill across a sequence in its state's colour), `altFrame` (alt/else frame with a tag and condition per part) and `edgeLabel` (words next to an arrow, optionally on a backdrop). They take `surface` and, the stickers, `inkScale`, like the map pieces, and paint what the caller lays out.
|
|
159
161
|
|
|
160
162
|
- The shapes take one to three centred lines (`text`), an optional `subtitle` and `textSize`. Without `fill` the face is the surface's panel; with a fill, every word is ink.
|
|
161
|
-
- Backticks mark code: `` "adopts a `correlationId`" `` draws `correlationId` in the monospace stack, as a `tspan`. An odd number of backticks is drawn as is.
|
|
163
|
+
- Backticks mark code: `` "adopts a `correlationId`" `` draws `correlationId` in the monospace stack, as a `tspan`. An odd number of backticks is drawn as is. The cover `pill` follows the same rule (`` "`main`" ``).
|
|
164
|
+
- `milestonePin` takes `size`, the head's radius (6 to 48, default 18); the head, its outline and the label scale with it.
|
|
162
165
|
- Widths the piece estimates (`tagWidth`, `conditionWidth`, a label's `width`) can be passed by a caller that measures its text.
|
|
163
166
|
- A body diagram is 800 wide. Lay it out in 560 units and show it 800 wide: the pieces then come out at the sizes of the published diagrams (text about 18, outline about 4); only the hard shadow is shallower (5 instead of 8). The cover command does this for you with `"format": "diagram"` (below).
|
|
164
167
|
- `stateStart({ cx, cy, r, surface })` draws the filled start dot of a state machine (7.7 units, 11 at 800), in the surface's text colour, with no outline and no shadow.
|
|
165
168
|
|
|
169
|
+
### The cast and the emotes
|
|
170
|
+
|
|
171
|
+
`mascot({ who, x, y, face, action, scale, look, headTilt, prop })` draws one character standing with the centre of its ground line at `(x, y)`. Each one carries its identity in three layers, so no video has to introduce it: a body shape, a mark always visible, and a prop when its hands are free.
|
|
172
|
+
|
|
173
|
+
| `who` | Body | Mark | Prop | Colour |
|
|
174
|
+
|---|---|---|---|---|
|
|
175
|
+
| `dev` | modern all-in-one computer, face and code on the screen | `</>` on the chin | | `cast.dev.*` |
|
|
176
|
+
| `agent` | retro lamp all-in-one: dome, chrome neck, the screen is the head | AI spark | | white, glowing face (`cast.glow`) |
|
|
177
|
+
| `architect` | drafting set square with its hole and a pencil | ruler ticks | diagram board | `cast.architect.*` |
|
|
178
|
+
| `devops` | gear | ∞ | wrench | `cast.devops.*` (magenta, never the blocked orange) |
|
|
179
|
+
| `data` | cylinder with a lid | two bands | magnifier | `cast.data.*` |
|
|
180
|
+
| `request` | speech bubble | the request line | | `color.caller` |
|
|
181
|
+
| `message` | envelope (0.7 of the others) | flap and seal | | white |
|
|
182
|
+
|
|
183
|
+
- `face`: `normal`, `blink`, `happy`, `wink`, `surprised`, `worried`, `thinking`, `focused`. One drawing for everyone; the agent's face glows on its dark screen.
|
|
184
|
+
- `action`: `none` (the default: an expression has no hands or feet), `rest`, `wave`, `hold`, `reach`, `up`, `shrug`, `type`. Arms and legs start at the body's sides; hands are mittens.
|
|
185
|
+
- `look` (-1 to 1) moves the gaze, `headTilt` (-30 to 30) turns the agent's head on its neck, `prop: false` hides the prop. Every part is a group named by `data-part` (`body`, `face`, `eyes`, `brows`, `mouth`, `arm-left`, `arm-right`, `legs`, `prop`, `mark`, …), so a scene can animate it.
|
|
186
|
+
|
|
187
|
+
`emote({ kind, x, y, scale, burst })` draws a reaction beside a head, never over the face. Each kind keeps one meaning in every video, like the colours:
|
|
188
|
+
|
|
189
|
+
| `kind` | Shape | Meaning |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| `idea` | bulb | an idea |
|
|
192
|
+
| `doubt` | ? | doubt, not understood |
|
|
193
|
+
| `problem` | ! in a red triangle | a problem appears |
|
|
194
|
+
| `worked` | green check | it worked |
|
|
195
|
+
| `refused` | red cross | refused |
|
|
196
|
+
| `pressure` | sweat drops | under pressure |
|
|
197
|
+
| `liked` | heart | liked |
|
|
198
|
+
| `waiting` | Z Z Z | waiting |
|
|
199
|
+
|
|
200
|
+
In a scene, an emote pops in on the frame the face changes (`emotePop` in `src/brand/cast-motion.ts`, with `leave`, `blinking`, `breathe`, `squashAfter` and `hop`), one at a time, and the body acts through `pose`. A mascot's colour is its identity and never changes with state. The approved motion test is the Studio composition `cast-motion-test` (folder `cast`): `npx remotion render src/index.ts cast-motion-test`. It is a proof of the cast, not an article scene, so `npm run stills` and `npm run render` do not list it.
|
|
201
|
+
|
|
202
|
+
The cast draws the same on any background; a dark-surface variant is planned (AB#187).
|
|
203
|
+
|
|
166
204
|
### A cover
|
|
167
205
|
|
|
168
206
|
A cover is a JSON spec kept with its article, outside this repository: drafts are private, and only the pieces are public. `npm run cover -- <spec.json> --out <file.svg>` validates it, reports every problem with its JSON path, and writes an 800x420 SVG; the same spec always gives the same bytes. With `"format": "banner"` the same command writes a site banner instead (see below).
|
|
@@ -236,6 +274,7 @@ Project-wide decisions are recorded as ADRs in [`docs/adr/`](docs/adr/README.md)
|
|
|
236
274
|
- **One token source.** Colours, type, outline, shadow and motion timings live in one file; the site, the animations and the SVG scripts read generated exports instead of keeping their own copies. [ADR 0002](docs/adr/0002-one-token-source.md)
|
|
237
275
|
- **One visual language, fixed colour meanings.** Green is always healthy, orange always blocked, red always the failure path, so a reader learns the code once. Rotated text is allowed since the 2026-10-23 amendment. [ADR 0003](docs/adr/0003-visual-language-and-colour-meaning.md)
|
|
238
276
|
- **Scenes illustrate published articles.** Every string on screen comes from the article, so an animation never says more than the reviewed text. [ADR 0004](docs/adr/0004-scenes-illustrate-published-articles.md)
|
|
277
|
+
- **Neutral videos, told by a fixed cast.** One video for both languages, no words on screen, a caption only when the motion cannot explain itself, and every gesture tied to an article line. [ADR 0005](docs/adr/0005-neutral-videos-and-the-cast.md)
|
|
239
278
|
|
|
240
279
|
## How this project is built
|
|
241
280
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type SvgNode } from "./node.ts";
|
|
2
|
+
export declare const MASCOTS: readonly ["dev", "agent", "architect", "devops", "data", "request", "message"];
|
|
3
|
+
export declare const MASCOT_FACES: readonly ["normal", "blink", "happy", "wink", "surprised", "worried", "thinking", "focused"];
|
|
4
|
+
export declare const MASCOT_ACTIONS: readonly ["none", "rest", "wave", "hold", "reach", "up", "shrug", "type"];
|
|
5
|
+
export declare const EMOTE_KINDS: readonly ["idea", "doubt", "problem", "worked", "refused", "pressure", "liked", "waiting"];
|
|
6
|
+
export type MascotName = (typeof MASCOTS)[number];
|
|
7
|
+
export type MascotFace = (typeof MASCOT_FACES)[number];
|
|
8
|
+
export type MascotAction = (typeof MASCOT_ACTIONS)[number];
|
|
9
|
+
export type EmoteKind = (typeof EMOTE_KINDS)[number];
|
|
10
|
+
export declare const face: (cx: number, cy: number, f: MascotFace, col: string, mouthFill: string) => SvgNode;
|
|
11
|
+
export type MascotParams = {
|
|
12
|
+
who: MascotName;
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
face?: MascotFace;
|
|
16
|
+
action?: MascotAction;
|
|
17
|
+
scale?: number;
|
|
18
|
+
look?: number;
|
|
19
|
+
headTilt?: number;
|
|
20
|
+
prop?: boolean;
|
|
21
|
+
};
|
|
22
|
+
export type EmoteParams = {
|
|
23
|
+
kind: EmoteKind;
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
scale?: number;
|
|
27
|
+
burst?: boolean;
|
|
28
|
+
};
|
|
29
|
+
/** One character standing with the centre of its ground line at (x, y). */
|
|
30
|
+
export declare const mascot: import("./piece.ts").Piece<MascotParams>;
|
|
31
|
+
/** One reaction symbol with a fixed meaning, centred on (x, y), beside a head. */
|
|
32
|
+
export declare const emote: import("./piece.ts").Piece<EmoteParams>;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// The cast (spec illustration-pieces, change cast-and-emotes, card 152):
|
|
2
|
+
// `mascot`, seven characters with a shared face, actions and named parts, and
|
|
3
|
+
// `emote`, eight reaction symbols with a fixed meaning (ADR 0005).
|
|
4
|
+
//
|
|
5
|
+
// Why here: covers, diagrams and scenes draw the same characters from one
|
|
6
|
+
// source; a scene animates them by recomputing parameters per frame and
|
|
7
|
+
// wrapping the piece in `pose`.
|
|
8
|
+
//
|
|
9
|
+
// The geometry is the approved spike's, ported unchanged
|
|
10
|
+
// (~/.hermes/spikes/design-system/cast-and-emotes/cast.mts). The frozen fixture
|
|
11
|
+
// test/fixtures/cast-0.5.0.json pins every call's output, so a refactor here
|
|
12
|
+
// must keep every hash.
|
|
13
|
+
import { drawn, el, g } from "./node.js";
|
|
14
|
+
import { colour, pos } from "./params.js";
|
|
15
|
+
import { definePiece } from "./piece.js";
|
|
16
|
+
const c = (path) => colour(path);
|
|
17
|
+
export const MASCOTS = ["dev", "agent", "architect", "devops", "data", "request", "message"];
|
|
18
|
+
export const MASCOT_FACES = ["normal", "blink", "happy", "wink", "surprised", "worried", "thinking", "focused"];
|
|
19
|
+
export const MASCOT_ACTIONS = ["none", "rest", "wave", "hold", "reach", "up", "shrug", "type"];
|
|
20
|
+
export const EMOTE_KINDS = ["idea", "doubt", "problem", "worked", "refused", "pressure", "liked", "waiting"];
|
|
21
|
+
const ink = () => c("color.ink");
|
|
22
|
+
const O = () => ({ stroke: ink(), "stroke-width": 4, "stroke-linejoin": "round" });
|
|
23
|
+
const shade = (d, dx = 8) => el("path", { d, fill: ink(), transform: `translate(${dx} ${dx})` });
|
|
24
|
+
const solid = (d, fill, dx = 8) => [shade(d, dx), el("path", { d, fill, ...O() })];
|
|
25
|
+
const shine = (d, col) => el("path", { d, fill: "none", stroke: col, "stroke-width": 9, "stroke-linecap": "round" });
|
|
26
|
+
const rr = (x, y, w, h, r) => `M${x + r},${y} H${x + w - r} Q${x + w},${y} ${x + w},${y + r} V${y + h - r} Q${x + w},${y + h} ${x + w - r},${y + h} H${x + r} Q${x},${y + h} ${x},${y + h - r} V${y + r} Q${x},${y} ${x + r},${y} Z`;
|
|
27
|
+
// ---------------------------------------------------------------- face (macs.ts)
|
|
28
|
+
export const face = (cx, cy, f, col, mouthFill) => {
|
|
29
|
+
const k = 1, dx = 24 * k;
|
|
30
|
+
const eye = (x, right) => {
|
|
31
|
+
if (f === "blink")
|
|
32
|
+
return el("path", { d: `M${x - 9 * k},${cy} H${x + 9 * k}`, stroke: col, "stroke-width": 5 * k, "stroke-linecap": "round" });
|
|
33
|
+
if (f === "happy" || (f === "wink" && right))
|
|
34
|
+
return el("path", { d: `M${x - 10 * k},${cy + 3 * k} Q${x},${cy - 10 * k} ${x + 10 * k},${cy + 3 * k}`, fill: "none", stroke: col, "stroke-width": 5 * k, "stroke-linecap": "round" });
|
|
35
|
+
if (f === "focused")
|
|
36
|
+
return el("path", { d: right ? `M${x + 10 * k},${cy - 6 * k} L${x - 8 * k},${cy} L${x + 10 * k},${cy + 6 * k}` : `M${x - 10 * k},${cy - 6 * k} L${x + 8 * k},${cy} L${x - 10 * k},${cy + 6 * k}`, fill: "none", stroke: col, "stroke-width": 5 * k, "stroke-linecap": "round", "stroke-linejoin": "round" });
|
|
37
|
+
const s = f === "surprised" ? 1.35 : 1, o = f === "thinking" ? 5 * k : 0;
|
|
38
|
+
return g({}, el("ellipse", { cx: x + o, cy: cy - o, rx: 7 * k * s, ry: 10 * k * s, fill: col }), el("circle", { cx: x + o + 2.5 * k, cy: cy - o - 4 * k, r: 2.6 * k, fill: c("color.white") }));
|
|
39
|
+
};
|
|
40
|
+
const by = cy - 22 * k, brow = (x, a, b) => el("path", { d: `M${x - 9 * k},${by + a * k} L${x + 9 * k},${by + b * k}`, stroke: col, "stroke-width": 4.5 * k, "stroke-linecap": "round" });
|
|
41
|
+
const brows = f === "worried" ? [brow(cx - dx, 4, -3), brow(cx + dx, -3, 4)]
|
|
42
|
+
: f === "surprised" ? [brow(cx - dx, -6, -8), brow(cx + dx, -8, -6)]
|
|
43
|
+
: f === "thinking" ? [brow(cx + dx, 0, -7)] : [];
|
|
44
|
+
const my = cy + 26 * k, ln = (d, fill = "none") => el("path", { d, fill, stroke: col, "stroke-width": 5 * k, "stroke-linecap": "round", "stroke-linejoin": "round" });
|
|
45
|
+
const smile = ln(`M${cx - 12 * k},${my - 2 * k} Q${cx},${my + 11 * k} ${cx + 12 * k},${my - 2 * k}`);
|
|
46
|
+
const mouth = {
|
|
47
|
+
blink: smile,
|
|
48
|
+
normal: smile,
|
|
49
|
+
happy: ln(`M${cx - 15 * k},${my - 4 * k} Q${cx},${my + 16 * k} ${cx + 15 * k},${my - 4 * k} Z`, mouthFill),
|
|
50
|
+
wink: ln(`M${cx - 12 * k},${my - 2 * k} Q${cx + 2 * k},${my + 12 * k} ${cx + 14 * k},${my - 6 * k}`),
|
|
51
|
+
surprised: el("ellipse", { cx, cy: my + 4 * k, rx: 7 * k, ry: 9 * k, fill: mouthFill, stroke: col, "stroke-width": 4 * k }),
|
|
52
|
+
worried: ln(`M${cx - 12 * k},${my + 5 * k} Q${cx},${my - 6 * k} ${cx + 12 * k},${my + 5 * k}`),
|
|
53
|
+
thinking: ln(`M${cx - 7 * k},${my + 2 * k} L${cx + 10 * k},${my - 2 * k}`),
|
|
54
|
+
focused: ln(`M${cx - 8 * k},${my} L${cx + 8 * k},${my}`),
|
|
55
|
+
}[f];
|
|
56
|
+
return g({ "data-part": "face" }, g({ "data-part": "eyes" }, eye(cx - dx, false), eye(cx + dx, true)), brows.length ? g({ "data-part": "brows" }, ...brows) : null, g({ "data-part": "mouth" }, mouth));
|
|
57
|
+
};
|
|
58
|
+
// Arm targets as offsets from the shoulders (left, right). "computer" = macs.ts, "object" = mascots4.ts.
|
|
59
|
+
const COMPUTER = {
|
|
60
|
+
armWidth: 15, legWidth: 15, bend: 8,
|
|
61
|
+
offsets: { rest: [[-20, 40], [20, 40]], wave: [[-20, 40], [30, -86]], hold: [[30, 50], [-30, 50]], reach: [[-20, 40], [70, -4]], up: [[-30, -90], [30, -90]], shrug: [[-44, -26], [44, -26]], type: [[26, 58], [-26, 58]] },
|
|
62
|
+
};
|
|
63
|
+
const OBJECT = {
|
|
64
|
+
armWidth: 16, legWidth: 16, bend: 8,
|
|
65
|
+
offsets: { rest: [[-22, 34], [22, 34]], wave: [[-22, 34], [34, -92]], hold: [[0, 0], [0, 0]], reach: [[-22, 34], [70, -4]], up: [[-34, -96], [34, -96]], shrug: [[-46, -30], [46, -30]], type: [[26, 58], [-26, 58]] },
|
|
66
|
+
};
|
|
67
|
+
const arm = (from, to, side, w) => [
|
|
68
|
+
el("path", { d: `M${from[0]},${from[1]} Q${(from[0] + to[0]) / 2 + side * 14},${Math.max(from[1], to[1]) + 8} ${to[0]},${to[1]}`, fill: "none", stroke: ink(), "stroke-width": w, "stroke-linecap": "round" }),
|
|
69
|
+
el("ellipse", { cx: to[0], cy: to[1], rx: 15, ry: 12, fill: ink(), transform: `rotate(${side * 20} ${to[0]} ${to[1]})` }),
|
|
70
|
+
];
|
|
71
|
+
// ---------------------------------------------------------------- props (mascots4.ts)
|
|
72
|
+
const boardProp = (x, y) => [
|
|
73
|
+
shade(rr(x, y, 104, 78, 10), 6), el("path", { d: rr(x, y, 104, 78, 10), fill: c("color.white"), ...O() }),
|
|
74
|
+
el("path", { d: `M${x + 30},${y + 28} H${x + 52} V${y + 52} H${x + 74}`, fill: "none", stroke: ink(), "stroke-width": 3 }),
|
|
75
|
+
el("rect", { x: x + 12, y: y + 18, width: 22, height: 20, rx: 4, fill: c("color.caller"), stroke: ink(), "stroke-width": 3 }),
|
|
76
|
+
el("rect", { x: x + 70, y: y + 42, width: 22, height: 20, rx: 4, fill: c("color.healthy"), stroke: ink(), "stroke-width": 3 }),
|
|
77
|
+
el("rect", { x: x + 42, y: y + 8, width: 20, height: 18, rx: 4, fill: c("color.transitional"), stroke: ink(), "stroke-width": 3 }),
|
|
78
|
+
];
|
|
79
|
+
const wrenchProp = (x, y) => {
|
|
80
|
+
const d = `M${x},${y} L${x + 50},${y - 50} M${x + 50},${y - 50}`;
|
|
81
|
+
return [
|
|
82
|
+
el("path", { d, stroke: ink(), "stroke-width": 18, "stroke-linecap": "round" }),
|
|
83
|
+
el("path", { d, stroke: c("cast.metal"), "stroke-width": 10, "stroke-linecap": "round" }),
|
|
84
|
+
el("path", { d: `M${x + 40},${y - 74} A24,24 0 1 0 ${x + 76},${y - 40} L${x + 62},${y - 46} L${x + 56},${y - 60} Z`, fill: c("cast.metal"), ...O() }),
|
|
85
|
+
];
|
|
86
|
+
};
|
|
87
|
+
const magnifierProp = (x, y) => [
|
|
88
|
+
el("path", { d: `M${x},${y} L${x + 24},${y - 24}`, stroke: ink(), "stroke-width": 16, "stroke-linecap": "round" }),
|
|
89
|
+
el("circle", { cx: x + 44, cy: y - 44, r: 30, fill: c("cast.data.lens"), stroke: ink(), "stroke-width": 9 }),
|
|
90
|
+
el("circle", { cx: x + 44, cy: y - 44, r: 30, fill: "none", stroke: c("color.caller"), "stroke-width": 4 }),
|
|
91
|
+
el("path", { d: `M${x + 30},${y - 56} Q${x + 36},${y - 66} ${x + 48},${y - 66}`, fill: "none", stroke: c("color.white"), "stroke-width": 5, "stroke-linecap": "round" }),
|
|
92
|
+
];
|
|
93
|
+
const spark = (x, y) => el("path", { d: `M${x},${y - 14} L${x + 4},${y - 4} L${x + 14},${y} L${x + 4},${y + 4} L${x},${y + 14} L${x - 4},${y + 4} L${x - 14},${y} L${x - 4},${y - 4} Z`, fill: c("color.transitional"), ...O(), "stroke-width": 3 });
|
|
94
|
+
const CAST = () => ({
|
|
95
|
+
// DEV: modern all-in-one. Screen frame = body, code on the screen, face on the screen, </> on the chin, stand.
|
|
96
|
+
dev: {
|
|
97
|
+
family: COMPUTER, anchor: [110, 230], ground: 80, shoulders: [[22, 118], [198, 118]], hips: [[86, 200], [134, 200]], foot: 226,
|
|
98
|
+
faceAt: [110, 88], faceColour: ink(), mouthFill: c("cast.mouth"),
|
|
99
|
+
parts: () => [
|
|
100
|
+
g({ "data-part": "stand" }, ...solid("M88,184 H132 L140,212 H80 Z", c("cast.metal"), 6), ...solid(rr(62, 208, 96, 12, 6), c("cast.metal"), 5)),
|
|
101
|
+
g({ "data-part": "body" }, ...solid(rr(4, 24, 212, 168, 18), c("cast.dev.fill")), el("path", { d: "M8,152 H212 V174 Q212,188 198,188 H22 Q8,188 8,174 Z", fill: c("cast.dev.chin"), stroke: "none" }), el("path", { d: "M4,152 H216", stroke: ink(), "stroke-width": 4 }), el("path", { d: rr(18, 36, 184, 108, 10), fill: c("cast.dev.screen"), stroke: ink(), "stroke-width": 3.5 }), g({ "data-part": "screen-code", stroke: "none" }, el("rect", { x: 30, y: 48, width: 34, height: 7, rx: 3.5, fill: c("color.healthy") }), el("rect", { x: 36, y: 61, width: 22, height: 7, rx: 3.5, fill: c("cast.dev.fill") }), el("rect", { x: 156, y: 48, width: 34, height: 7, rx: 3.5, fill: c("color.healthy") }), el("rect", { x: 162, y: 61, width: 22, height: 7, rx: 3.5, fill: c("cast.dev.fill") }))),
|
|
102
|
+
g({ "data-part": "mark" }, el("path", { d: "M98,163 L90,170 L98,177 M122,163 L130,170 L122,177 M114,160 L106,180", fill: "none", stroke: c("color.white"), "stroke-width": 4.5, "stroke-linecap": "round", "stroke-linejoin": "round" })),
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
// AI AGENT: retro lamp all-in-one. Dome = body, chrome neck, the screen is the head (tilts on the neck).
|
|
106
|
+
agent: {
|
|
107
|
+
family: COMPUTER, anchor: [110, 250], ground: 82, shoulders: [[44, 196], [176, 196]], hips: [[84, 236], [136, 236]], foot: 248,
|
|
108
|
+
faceAt: [110, 58], faceColour: c("cast.glow"), mouthFill: c("cast.glow"), faceInHead: true,
|
|
109
|
+
parts: (tilt) => {
|
|
110
|
+
const top = [110 + tilt * 1.4, 110];
|
|
111
|
+
const neck = `M110,176 Q${108 + tilt},150 ${top[0]},${top[1]}`;
|
|
112
|
+
return [
|
|
113
|
+
g({ "data-part": "neck" }, el("path", { d: neck, fill: "none", stroke: ink(), "stroke-width": 16, "stroke-linecap": "round" }), el("path", { d: neck, fill: "none", stroke: c("cast.agent.neck"), "stroke-width": 8, "stroke-linecap": "round" })),
|
|
114
|
+
g({ "data-part": "body" }, ...solid("M30,240 Q30,166 110,164 Q190,166 190,240 Z", c("color.white")), el("path", { d: "M44,226 H176", stroke: c("cast.metal"), "stroke-width": 4, "stroke-dasharray": "3 6", "stroke-linecap": "round" }), el("path", { d: "M58,196 Q80,180 104,178", fill: "none", stroke: c("cast.agent.shine"), "stroke-width": 8, "stroke-linecap": "round" })),
|
|
115
|
+
g({ "data-part": "mark" }, spark(110, 206)),
|
|
116
|
+
];
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
// ARCHITECT: drafting set square with its hole, ruler ticks (mark), pencil; board as prop.
|
|
120
|
+
architect: {
|
|
121
|
+
family: OBJECT, anchor: [105, 216], ground: 76, shoulders: [[46, 152], [174, 152]], hips: [[78, 190], [146, 190]], foot: 212, hold: [64, 168],
|
|
122
|
+
faceAt: [110, 132], faceColour: ink(), mouthFill: c("cast.mouth"),
|
|
123
|
+
parts: () => [
|
|
124
|
+
g({ "data-part": "pencil", transform: "rotate(28 128 40)" }, el("path", { d: "M120,6 H136 V60 L128,74 L120,60 Z", fill: c("color.transitional"), ...O(), "stroke-width": 3.5 }), el("path", { d: "M120,6 H136 V16 H120 Z", fill: c("color.backend"), ...O(), "stroke-width": 3.5 })),
|
|
125
|
+
g({ "data-part": "body" }, ...solid("M99,30 Q110,13 121,30 L198,158 Q210,184 177,193 H35 Q6,187 21,160 Z", c("cast.architect.fill")), el("path", { d: "M110,58 L130,94 H90 Z", fill: c("color.panel"), stroke: ink(), "stroke-width": 3.5, "stroke-linejoin": "round" }), shine("M70,110 Q78,92 88,80", c("cast.architect.shine"))),
|
|
126
|
+
g({ "data-part": "mark", stroke: ink(), "stroke-width": 3, "stroke-linecap": "round" }, ...Array.from({ length: 9 }, (_, i) => { const x = 42 + i * 16; const h = i % 2 ? 9 : 15; return el("path", { d: `M${x},193 V${193 - h}` }); })),
|
|
127
|
+
],
|
|
128
|
+
prop: () => boardProp(118, 152),
|
|
129
|
+
},
|
|
130
|
+
// DEVOPS: gear with the infinity loop (mark); wrench as prop.
|
|
131
|
+
devops: {
|
|
132
|
+
family: OBJECT, anchor: [105, 216], ground: 76, shoulders: [[34, 138], [184, 138]], hips: [[68, 176], [146, 176]], foot: 212, hold: [72, 160],
|
|
133
|
+
faceAt: [110, 100], faceColour: ink(), mouthFill: c("cast.mouth"),
|
|
134
|
+
parts: () => {
|
|
135
|
+
const loop = "M110,160 C98,146 76,146 76,160 C76,174 98,174 110,160 C122,146 144,146 144,160 C144,174 122,174 110,160 Z";
|
|
136
|
+
return [
|
|
137
|
+
g({ "data-part": "body" }, ...solid("M86,22H128L137,42L161,36L183,58L175,82L196,94V128L174,141L182,163L158,187L136,178L124,197H87L77,180L54,186L28,163L37,138L16,125V93L39,81L31,57L53,35L76,42Z", c("cast.devops.fill")), shine("M60,60 Q80,46 104,46", c("cast.devops.shine"))),
|
|
138
|
+
g({ "data-part": "mark" }, el("path", { d: loop, fill: "none", stroke: c("color.white"), "stroke-width": 11, "stroke-linejoin": "round" }), el("path", { d: loop, fill: "none", stroke: ink(), "stroke-width": 4, "stroke-linejoin": "round" })),
|
|
139
|
+
];
|
|
140
|
+
},
|
|
141
|
+
prop: () => wrenchProp(168, 178),
|
|
142
|
+
},
|
|
143
|
+
// DATA: cylinder with lid and two bands (mark); magnifier as prop.
|
|
144
|
+
data: {
|
|
145
|
+
family: OBJECT, anchor: [105, 216], ground: 76, shoulders: [[38, 140], [172, 140]], hips: [[68, 196], [142, 196]], foot: 212, hold: [70, 170],
|
|
146
|
+
faceAt: [105, 104], faceColour: ink(), mouthFill: c("cast.mouth"),
|
|
147
|
+
parts: () => [
|
|
148
|
+
g({ "data-part": "body" }, ...solid("M35,60 V174 Q35,205 105,205 Q175,205 175,174 V60 Z", c("cast.data.fill"))),
|
|
149
|
+
g({ "data-part": "mark", fill: "none", stroke: ink(), "stroke-width": 4 }, el("path", { d: "M35,128 Q105,158 175,128" }), el("path", { d: "M35,166 Q105,196 175,166" })),
|
|
150
|
+
g({ "data-part": "lid" }, el("ellipse", { cx: 105, cy: 61, rx: 70, ry: 25, fill: c("cast.data.lid"), ...O() }), shine("M50,84 V112", c("cast.data.shine"))),
|
|
151
|
+
],
|
|
152
|
+
prop: () => magnifierProp(164, 186),
|
|
153
|
+
},
|
|
154
|
+
// REQUESTER: speech bubble in caller blue, the request line (mark).
|
|
155
|
+
request: {
|
|
156
|
+
family: OBJECT, anchor: [105, 216], ground: 76, shoulders: [[30, 120], [180, 120]], hips: [[78, 176], [134, 176]], foot: 212, hold: [70, 150],
|
|
157
|
+
faceAt: [105, 82], faceColour: ink(), mouthFill: c("cast.mouth"),
|
|
158
|
+
parts: () => [
|
|
159
|
+
g({ "data-part": "body" }, ...solid("M54,30 H156 Q186,30 186,60 V130 Q186,160 156,160 H96 L66,190 L72,160 H54 Q24,160 24,130 V60 Q24,30 54,30 Z", c("color.caller")), shine("M44,62 Q56,46 78,46", c("cast.request.shine"))),
|
|
160
|
+
g({ "data-part": "mark" }, el("rect", { x: 62, y: 134, width: 86, height: 12, rx: 6, fill: c("color.white"), stroke: ink(), "stroke-width": 3 })),
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
// MESSAGE: envelope (0.7 of the others), seal (mark).
|
|
164
|
+
message: {
|
|
165
|
+
family: OBJECT, anchor: [105, 216], scale: 0.7, ground: 76, shoulders: [[40, 140], [170, 140]], hips: [[78, 190], [132, 190]], foot: 212, hold: [70, 170],
|
|
166
|
+
faceAt: [105, 136], faceColour: ink(), mouthFill: c("cast.mouth"),
|
|
167
|
+
parts: () => [
|
|
168
|
+
g({ "data-part": "body" }, ...solid(rr(32, 88, 146, 106, 16), c("color.white"))),
|
|
169
|
+
g({ "data-part": "mark" }, el("path", { d: "M36,94 L105,140 L174,94", fill: "none", stroke: ink(), "stroke-width": 4, "stroke-linejoin": "round" }), el("circle", { cx: 105, cy: 140, r: 9, fill: c("color.transitional"), stroke: ink(), "stroke-width": 3 })),
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
const drawMascot = (p) => {
|
|
174
|
+
const ch = CAST()[p.who];
|
|
175
|
+
const f = p.face ?? "normal", act = p.action ?? "none", look = p.look ?? 0, tilt = p.headTilt ?? 0;
|
|
176
|
+
const k = (p.scale ?? 1) * (ch.scale ?? 1);
|
|
177
|
+
const fam = ch.family;
|
|
178
|
+
const [L, R] = ch.shoulders;
|
|
179
|
+
let arms = [];
|
|
180
|
+
let legs = null;
|
|
181
|
+
const front = act === "hold" || act === "type";
|
|
182
|
+
if (act !== "none") {
|
|
183
|
+
const [ol, or] = fam.offsets[act];
|
|
184
|
+
let targetL = [L[0] + ol[0], L[1] + ol[1]], targetR = [R[0] + or[0], R[1] + or[1]];
|
|
185
|
+
// mascots4: "hold" puts both mittens at the hold point, mirrored round x 105.
|
|
186
|
+
if (act === "hold" && ch.hold) {
|
|
187
|
+
targetL = ch.hold;
|
|
188
|
+
targetR = [210 - ch.hold[0], ch.hold[1]];
|
|
189
|
+
}
|
|
190
|
+
arms = [g({ "data-part": "arm-left" }, ...arm(L, targetL, -1, fam.armWidth)), g({ "data-part": "arm-right" }, ...arm(R, targetR, 1, fam.armWidth))];
|
|
191
|
+
legs = g({ "data-part": "legs" }, ...ch.hips.flatMap(([hx, hy], i) => {
|
|
192
|
+
const s = i ? 1 : -1;
|
|
193
|
+
return fam === COMPUTER
|
|
194
|
+
? [el("path", { d: `M${hx},${hy} L${hx + s * 4},${ch.foot - 6}`, stroke: ink(), "stroke-width": fam.legWidth, "stroke-linecap": "round" }), el("ellipse", { cx: hx + s * 11, cy: ch.foot, rx: 19, ry: 11, fill: ink() })]
|
|
195
|
+
: [el("path", { d: `M${hx},${hy} L${hx + s * 6},${ch.foot - 6}`, stroke: ink(), "stroke-width": fam.legWidth, "stroke-linecap": "round" }), el("ellipse", { cx: hx + s * 13, cy: ch.foot, rx: 19, ry: 11, fill: ink() })];
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
let faceWrapped = face(ch.faceAt[0] + (ch.faceInHead ? tilt * 1.4 : 0), ch.faceAt[1], f, ch.faceColour, ch.mouthFill);
|
|
199
|
+
if (look !== 0)
|
|
200
|
+
faceWrapped = { ...faceWrapped, attrs: { ...faceWrapped.attrs, transform: `translate(${look * 6} 0)` } };
|
|
201
|
+
const showProp = (p.prop ?? true) && ch.prop && (act === "rest" || act === "hold");
|
|
202
|
+
const [ax, ay] = ch.anchor;
|
|
203
|
+
let parts = ch.parts(tilt);
|
|
204
|
+
if (ch.faceInHead) {
|
|
205
|
+
const top = [110 + tilt * 1.4, 110];
|
|
206
|
+
parts = [...parts, g({ "data-part": "head", transform: `rotate(${tilt} ${top[0]} ${top[1]})` }, ...solid(rr(top[0] - 96, 0, 192, 132, 22), c("color.white")), el("path", { d: rr(top[0] - 80, 14, 160, 104, 12), fill: c("illustration.codePanel"), stroke: ink(), "stroke-width": 4 }), faceWrapped)];
|
|
207
|
+
}
|
|
208
|
+
const root = g({ "data-mascot": p.who, transform: `translate(${p.x} ${p.y}) scale(${k}) translate(${-ax} ${-ay})`, stroke: "none" }, g({ "data-part": "ground" }, el("ellipse", { cx: ax, cy: ay, rx: ch.ground, ry: 9, fill: c("cast.ground") })), ...(front ? [] : arms), legs, ...parts, ch.faceInHead ? null : faceWrapped, showProp ? g({ "data-part": "prop" }, ...ch.prop()) : null, ...(front ? arms : []));
|
|
209
|
+
return drawn([root]);
|
|
210
|
+
};
|
|
211
|
+
// ---------------------------------------------------------------- emotes (emotes.ts)
|
|
212
|
+
const rays = (cx, cy, r1, r2, n, from = -150, to = -30) => Array.from({ length: n }, (_, i) => {
|
|
213
|
+
const a = ((from + (i * (to - from)) / (n - 1)) * Math.PI) / 180;
|
|
214
|
+
const r = (v) => Math.round(v * 100) / 100;
|
|
215
|
+
return el("path", { d: `M${r(cx + r1 * Math.cos(a))},${r(cy + r1 * Math.sin(a))} L${r(cx + r2 * Math.cos(a))},${r(cy + r2 * Math.sin(a))}`, stroke: ink(), "stroke-width": 6, "stroke-linecap": "round" });
|
|
216
|
+
});
|
|
217
|
+
const EO = () => ({ stroke: ink(), "stroke-width": 5, "stroke-linejoin": "round", "stroke-linecap": "round" });
|
|
218
|
+
const disc = (cx, cy, r, fill) => [el("circle", { cx: cx + 6, cy: cy + 6, r, fill: ink() }), el("circle", { cx, cy, r, fill, ...EO() })];
|
|
219
|
+
const zed = (x, y, s) => {
|
|
220
|
+
// A Z of height s with its top-left at (x, y - s): ink casing, caller fill.
|
|
221
|
+
const w = s * 0.72, d = `M${x},${y - s} H${x + w} L${x},${y} H${x + w}`;
|
|
222
|
+
return [el("path", { d, fill: "none", stroke: ink(), "stroke-width": s * 0.34, "stroke-linecap": "round", "stroke-linejoin": "round" }),
|
|
223
|
+
el("path", { d, fill: "none", stroke: c("color.caller"), "stroke-width": s * 0.16, "stroke-linecap": "round", "stroke-linejoin": "round" })];
|
|
224
|
+
};
|
|
225
|
+
const drawEmote = (p) => {
|
|
226
|
+
const { kind, x: cx, y: cy } = p, burst = p.burst ?? true, s = p.scale ?? 1;
|
|
227
|
+
const parts = [];
|
|
228
|
+
switch (kind) {
|
|
229
|
+
case "idea": {
|
|
230
|
+
if (burst)
|
|
231
|
+
parts.push(el("circle", { cx, cy: cy - 6, r: 62, fill: c("color.transitional"), opacity: 0.35 }), ...rays(cx, cy - 6, 50, 72, 7, -180, 0));
|
|
232
|
+
const bulb = `M${cx - 30},${cy - 10} A34,34 0 1 1 ${cx + 30},${cy - 10} Q${cx + 22},${cy + 6} ${cx + 18},${cy + 22} H${cx - 18} Q${cx - 22},${cy + 6} ${cx - 30},${cy - 10} Z`;
|
|
233
|
+
parts.push(el("path", { d: bulb, fill: ink(), transform: "translate(6 6)" }), el("path", { d: bulb, fill: c("color.transitional"), ...EO() }), el("path", { d: `M${cx - 16},${cy - 22} Q${cx - 12},${cy - 32} ${cx - 2},${cy - 34}`, fill: "none", stroke: c("color.white"), "stroke-width": 6, "stroke-linecap": "round" }), el("path", { d: `M${cx - 8},${cy + 22} V${cy} L${cx},${cy + 8} L${cx + 8},${cy} V${cy + 22}`, fill: "none", stroke: ink(), "stroke-width": 4, "stroke-linejoin": "round" }), el("rect", { x: cx - 18, y: cy + 22, width: 36, height: 12, rx: 3, fill: c("cast.metal"), ...EO(), "stroke-width": 4 }), el("rect", { x: cx - 14, y: cy + 34, width: 28, height: 10, rx: 5, fill: c("cast.metal"), ...EO(), "stroke-width": 4 }));
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
case "doubt":
|
|
237
|
+
parts.push(...disc(cx, cy, 40, c("color.white")), el("path", { d: `M${cx - 14},${cy - 12} Q${cx - 14},${cy - 28} ${cx},${cy - 28} Q${cx + 16},${cy - 28} ${cx + 16},${cy - 13} Q${cx + 16},${cy - 2} ${cx + 2},${cy + 4} V${cy + 10}`, fill: "none", stroke: ink(), "stroke-width": 9, "stroke-linecap": "round", "stroke-linejoin": "round" }), el("circle", { cx: cx + 2, cy: cy + 24, r: 6, fill: ink() }));
|
|
238
|
+
break;
|
|
239
|
+
case "problem":
|
|
240
|
+
if (burst)
|
|
241
|
+
parts.push(...rays(cx, cy, 48, 70, 5));
|
|
242
|
+
parts.push(el("path", { d: `M${cx},${cy - 44} L${cx + 46},${cy + 34} H${cx - 46} Z`, fill: ink(), transform: "translate(6 6)" }), el("path", { d: `M${cx},${cy - 44} L${cx + 46},${cy + 34} H${cx - 46} Z`, fill: c("color.failure"), ...EO() }), el("path", { d: `M${cx},${cy - 18} V${cy + 8}`, stroke: ink(), "stroke-width": 9, "stroke-linecap": "round" }), el("circle", { cx, cy: cy + 22, r: 5.5, fill: ink() }));
|
|
243
|
+
break;
|
|
244
|
+
case "pressure":
|
|
245
|
+
for (const [dx, dy, k] of [[0, 0, 1], [30, 22, 0.6]])
|
|
246
|
+
parts.push(el("path", { d: `M${cx + dx},${cy + dy - 34 * k} Q${cx + dx + 24 * k},${cy + dy} ${cx + dx},${cy + dy + 12 * k} Q${cx + dx - 24 * k},${cy + dy} ${cx + dx},${cy + dy - 34 * k} Z`, fill: c("cast.glow"), ...EO(), "stroke-width": 4 }), el("path", { d: `M${cx + dx - 6 * k},${cy + dy - 2 * k} Q${cx + dx - 8 * k},${cy + dy + 6 * k} ${cx + dx - 2 * k},${cy + dy + 8 * k}`, fill: "none", stroke: c("color.white"), "stroke-width": 3, "stroke-linecap": "round" }));
|
|
247
|
+
break;
|
|
248
|
+
case "liked":
|
|
249
|
+
parts.push(el("path", { d: `M${cx},${cy + 30} C${cx - 50},${cy} ${cx - 34},${cy - 40} ${cx},${cy - 16} C${cx + 34},${cy - 40} ${cx + 50},${cy} ${cx},${cy + 30} Z`, fill: c("color.backend"), ...EO() }));
|
|
250
|
+
break;
|
|
251
|
+
case "worked":
|
|
252
|
+
if (burst)
|
|
253
|
+
parts.push(...rays(cx, cy, 48, 68, 5));
|
|
254
|
+
parts.push(...disc(cx, cy, 38, c("color.healthy")), el("path", { d: `M${cx - 16},${cy} L${cx - 4},${cy + 12} L${cx + 18},${cy - 12}`, fill: "none", stroke: ink(), "stroke-width": 8, "stroke-linecap": "round", "stroke-linejoin": "round" }));
|
|
255
|
+
break;
|
|
256
|
+
case "refused":
|
|
257
|
+
parts.push(...disc(cx, cy, 38, c("color.failure")), el("path", { d: `M${cx - 13},${cy - 13} L${cx + 13},${cy + 13} M${cx + 13},${cy - 13} L${cx - 13},${cy + 13}`, stroke: ink(), "stroke-width": 8, "stroke-linecap": "round" }));
|
|
258
|
+
break;
|
|
259
|
+
case "waiting":
|
|
260
|
+
parts.push(...zed(cx - 12, cy + 12, 26), ...zed(cx + 20, cy - 20, 20), ...zed(cx + 44, cy - 46, 14));
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
const attrs = s === 1 ? { "data-emote": kind, stroke: "none" } : { "data-emote": kind, stroke: "none", transform: `translate(${cx} ${cy}) scale(${s}) translate(${-cx} ${-cy})` };
|
|
264
|
+
return drawn([g(attrs, ...parts)]);
|
|
265
|
+
};
|
|
266
|
+
const unit = { kind: "number", min: 0, exclusiveMin: true, max: 10 };
|
|
267
|
+
/** One character standing with the centre of its ground line at (x, y). */
|
|
268
|
+
export const mascot = definePiece("mascot", {
|
|
269
|
+
who: { kind: "enum", required: true, values: MASCOTS },
|
|
270
|
+
x: pos(),
|
|
271
|
+
y: pos(),
|
|
272
|
+
face: { kind: "enum", values: MASCOT_FACES },
|
|
273
|
+
action: { kind: "enum", values: MASCOT_ACTIONS },
|
|
274
|
+
scale: unit,
|
|
275
|
+
look: { kind: "number", min: -1, max: 1 },
|
|
276
|
+
headTilt: { kind: "number", min: -30, max: 30 },
|
|
277
|
+
prop: { kind: "boolean" },
|
|
278
|
+
}, drawMascot, ({ who, headTilt }) => (headTilt !== undefined && headTilt !== 0 && who !== "agent" ? [{ path: "headTilt", message: "applies only to the agent" }] : []));
|
|
279
|
+
/** One reaction symbol with a fixed meaning, centred on (x, y), beside a head. */
|
|
280
|
+
export const emote = definePiece("emote", { kind: { kind: "enum", required: true, values: EMOTE_KINDS }, x: pos(), y: pos(), scale: unit, burst: { kind: "boolean" } }, drawEmote);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// between parameters are checked in the order of design §3 and the first one is
|
|
8
8
|
// thrown as a RangeError naming the parameter.
|
|
9
9
|
import { tokens } from "../../tokens/generated/tokens.js";
|
|
10
|
-
import { inkOf, inkSchema, limits, roomFor, sticker, surfaceParam } from "./ink.js";
|
|
10
|
+
import { inkOf, inkSchema, limits, roomFor, spans, sticker, surfaceParam } from "./ink.js";
|
|
11
11
|
import { drawn, el, r2 } from "./node.js";
|
|
12
12
|
import { col, colour, label, MAX_SIZE, pos, size } from "./params.js";
|
|
13
13
|
import { definePiece } from "./piece.js";
|
|
@@ -20,14 +20,6 @@ const textSize = { kind: "number", min: 6, max: 48 };
|
|
|
20
20
|
// ---------- words ----------
|
|
21
21
|
/** Characters of a word for a width estimate: backticks do not count. */
|
|
22
22
|
const characters = (value) => Array.from(value.replace(/`/g, "")).length;
|
|
23
|
-
/** Backticks mark code (design §4): with an even number of them, each part between backticks becomes a tspan in
|
|
24
|
-
* the monospace stack; empty parts are dropped. With an odd number, the string is drawn as is. */
|
|
25
|
-
const spans = (value) => {
|
|
26
|
-
const parts = value.split("`");
|
|
27
|
-
if (parts.length % 2 === 0)
|
|
28
|
-
return [value];
|
|
29
|
-
return parts.flatMap((part, i) => (part === "" ? [] : i % 2 === 1 ? [el("tspan", { "font-family": tokens.font.mono }, part)] : [part]));
|
|
30
|
-
};
|
|
31
23
|
// Every text sets its own anchor, size and weight, so a surrounding group (a cover's label style) does not leak in.
|
|
32
24
|
const words = (x, y, value, attrs) => el("text", { x: r2(x), y: r2(y), "font-family": font, ...attrs }, ...spans(value));
|
|
33
25
|
/** No fill: the surface's panel with its text; a fill: that token with ink words (ADR 0003). */
|
|
@@ -7,3 +7,4 @@ export * from "./library.ts";
|
|
|
7
7
|
export { EIP_ICONS, EIP_ICON_NAMES, EIP_ICON_HEIGHT, EIP_ICON_WIDTH, type EipIcon } from "./eip-icons.ts";
|
|
8
8
|
export { FORMATS, LANGUAGE, SAFE_ID, backgroundNames, bannerRingCount, comparePaths, coverTree, diagramRings, validateCoverSpec, type CoverFormat, type CoverOptions, type CoverPiece, type CoverRings, type CoverSpec, type CustomNode } from "./cover.ts";
|
|
9
9
|
export { tokens, type Tokens } from "../../tokens/generated/tokens.ts";
|
|
10
|
+
export { EMOTE_KINDS, MASCOTS, MASCOT_ACTIONS, MASCOT_FACES, type EmoteKind, type EmoteParams, type MascotAction, type MascotFace, type MascotName, type MascotParams } from "./cast.ts";
|
package/dist/src/pieces/index.js
CHANGED
|
@@ -12,3 +12,4 @@ export * from "./library.js";
|
|
|
12
12
|
export { EIP_ICONS, EIP_ICON_NAMES, EIP_ICON_HEIGHT, EIP_ICON_WIDTH } from "./eip-icons.js";
|
|
13
13
|
export { FORMATS, LANGUAGE, SAFE_ID, backgroundNames, bannerRingCount, comparePaths, coverTree, diagramRings, validateCoverSpec } from "./cover.js";
|
|
14
14
|
export { tokens } from "../../tokens/generated/tokens.js";
|
|
15
|
+
export { EMOTE_KINDS, MASCOTS, MASCOT_ACTIONS, MASCOT_FACES } from "./cast.js";
|
package/dist/src/pieces/ink.d.ts
CHANGED
|
@@ -45,4 +45,9 @@ export declare const atLeast: (name: string, value: number, least: number, what:
|
|
|
45
45
|
export type Shape = (dx: number, dy: number, attrs: Attrs) => SvgNode;
|
|
46
46
|
/** The ink sticker (metro-and-diagram-pieces design §3): shadow copy, rim on dark, then the face. */
|
|
47
47
|
export declare const sticker: (shape: Shape, face: string, inkParams: Ink) => SvgNode[];
|
|
48
|
+
/** Backticks mark code (body-diagram-pieces design §4; the pill since card 190): with an even number of them, each part between backticks becomes a tspan in
|
|
49
|
+
* the monospace stack; empty parts are dropped. With an odd number, the string is drawn as is. */
|
|
50
|
+
export declare const spans: (value: string) => (SvgNode | string)[];
|
|
51
|
+
/** A pill's text length for its default width: backticks of code runs do not count; otherwise `length` as in 0.5.0. */
|
|
52
|
+
export declare const codeLength: (value: string) => number;
|
|
48
53
|
export declare const words: (x: number, y: number, value: string, attrs: Attrs) => SvgNode;
|
package/dist/src/pieces/ink.js
CHANGED
|
@@ -34,5 +34,15 @@ export const sticker = (shape, face, inkParams) => {
|
|
|
34
34
|
shape(0, 0, { fill: face, stroke: ink, "stroke-width": r2(outline), "stroke-linejoin": "round" }),
|
|
35
35
|
];
|
|
36
36
|
};
|
|
37
|
+
/** Backticks mark code (body-diagram-pieces design §4; the pill since card 190): with an even number of them, each part between backticks becomes a tspan in
|
|
38
|
+
* the monospace stack; empty parts are dropped. With an odd number, the string is drawn as is. */
|
|
39
|
+
export const spans = (value) => {
|
|
40
|
+
const parts = value.split("`");
|
|
41
|
+
if (parts.length % 2 === 0)
|
|
42
|
+
return [value];
|
|
43
|
+
return parts.flatMap((part, i) => (part === "" ? [] : i % 2 === 1 ? [el("tspan", { "font-family": tokens.font.mono }, part)] : [part]));
|
|
44
|
+
};
|
|
45
|
+
/** A pill's text length for its default width: backticks of code runs do not count; otherwise `length` as in 0.5.0. */
|
|
46
|
+
export const codeLength = (value) => (value.split("`").length % 2 === 1 && value.includes("`") ? value.replace(/`/g, "") : value).length;
|
|
37
47
|
// Every text sets its own anchor, size and weight, so a surrounding group (a cover's label style) does not leak in.
|
|
38
48
|
export const words = (x, y, value, attrs) => el("text", { x: r2(x), y: r2(y), "font-family": font, ...attrs }, value);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { emote, mascot } from "./cast.ts";
|
|
2
|
+
export { emote, mascot };
|
|
1
3
|
export * from "./metro.ts";
|
|
2
4
|
export * from "./diagram.ts";
|
|
3
5
|
export * from "./ui-icons.ts";
|
|
@@ -10,7 +12,7 @@ export type PillParams = {
|
|
|
10
12
|
fill?: string;
|
|
11
13
|
width?: number;
|
|
12
14
|
};
|
|
13
|
-
/** Pill label centred on (x, y), usually overlapping the top edge of what it names. */
|
|
15
|
+
/** Pill label centred on (x, y), usually overlapping the top edge of what it names. Backticks mark code (card 190). */
|
|
14
16
|
export declare const pill: import("./piece.ts").Piece<PillParams>;
|
|
15
17
|
export declare const RING_COUNT = 14;
|
|
16
18
|
/** Most rings one call draws: enough for a banner's farthest corner from any centre a cover spec accepts (548). */
|
|
@@ -138,8 +140,10 @@ export type MilestonePinParams = {
|
|
|
138
140
|
text: string;
|
|
139
141
|
fill?: string;
|
|
140
142
|
stem?: number;
|
|
143
|
+
size?: number;
|
|
141
144
|
};
|
|
142
|
-
/** A pin standing on (x, y): a stem and a round head with a short label.
|
|
145
|
+
/** A pin standing on (x, y): a stem and a round head with a short label. `size` is the head's radius (18) and
|
|
146
|
+
* scales the head, its outline and the label together (card 190). */
|
|
143
147
|
export declare const milestonePin: import("./piece.ts").Piece<MilestonePinParams>;
|
|
144
148
|
export type PriceTagParams = {
|
|
145
149
|
x: number;
|
|
@@ -326,6 +330,8 @@ export declare const PIECES: {
|
|
|
326
330
|
readonly uiIcon: import("./piece.ts").Piece<import("./ui-icons.ts").UiIconParams>;
|
|
327
331
|
readonly linkIcon: import("./piece.ts").Piece<import("./link-icons.ts").LinkIconParams>;
|
|
328
332
|
readonly stateStart: import("./piece.ts").Piece<import("./diagram.ts").StateStartParams>;
|
|
333
|
+
readonly mascot: import("./piece.ts").Piece<import("./cast.ts").MascotParams>;
|
|
334
|
+
readonly emote: import("./piece.ts").Piece<import("./cast.ts").EmoteParams>;
|
|
329
335
|
};
|
|
330
336
|
export type PieceName = keyof typeof PIECES;
|
|
331
337
|
export declare const PIECE_NAMES: PieceName[];
|
|
@@ -15,17 +15,20 @@ import { METRO_PIECES } from "./metro.js";
|
|
|
15
15
|
import { DIAGRAM_PIECES, stateStart } from "./diagram.js";
|
|
16
16
|
import { uiIcon } from "./ui-icons.js";
|
|
17
17
|
import { linkIcon } from "./link-icons.js";
|
|
18
|
+
import { emote, mascot } from "./cast.js";
|
|
19
|
+
import { codeLength, spans } from "./ink.js";
|
|
20
|
+
export { emote, mascot };
|
|
18
21
|
export * from "./metro.js";
|
|
19
22
|
export * from "./diagram.js";
|
|
20
23
|
export * from "./ui-icons.js";
|
|
21
24
|
export * from "./link-icons.js";
|
|
22
25
|
const ink = tokens.color.ink;
|
|
23
26
|
const mono = tokens.font.mono;
|
|
24
|
-
/** Pill label centred on (x, y), usually overlapping the top edge of what it names. */
|
|
27
|
+
/** Pill label centred on (x, y), usually overlapping the top edge of what it names. Backticks mark code (card 190). */
|
|
25
28
|
export const pill = definePiece("pill", { x: pos(), y: pos(), text: label(), fill: col(), width: size() }, ({ x, y, text, fill = "color.white", width }) => {
|
|
26
29
|
const h = tokens.cover.pillHeight;
|
|
27
|
-
const w = width ?? Math.max(68, Math.round(text
|
|
28
|
-
return drawn([el("rect", { x: x - w / 2, y: y - h / 2, width: w, height: h, rx: h / 2, fill: colour(fill, "fill") })], [el("text", { x, y: y + 7 }, text)]);
|
|
30
|
+
const w = width ?? Math.max(68, Math.round(codeLength(text) * 10.5 + 34));
|
|
31
|
+
return drawn([el("rect", { x: x - w / 2, y: y - h / 2, width: w, height: h, rx: h / 2, fill: colour(fill, "fill") })], [el("text", { x, y: y + 7 }, ...spans(text))]);
|
|
29
32
|
});
|
|
30
33
|
// ---------- background ----------
|
|
31
34
|
export const RING_COUNT = 14;
|
|
@@ -192,8 +195,15 @@ export const road = definePiece("road", { d: { kind: "string", required: true, n
|
|
|
192
195
|
el("path", { d, fill: "none", stroke: tokens.illustration.road, "stroke-width": 40, "stroke-linecap": "round" }),
|
|
193
196
|
el("path", { d, fill: "none", stroke: tokens.color.white, "stroke-width": tokens.outline.block, "stroke-dasharray": "14 12" }),
|
|
194
197
|
]));
|
|
195
|
-
|
|
196
|
-
|
|
198
|
+
const PIN_SIZE = 18;
|
|
199
|
+
/** A pin standing on (x, y): a stem and a round head with a short label. `size` is the head's radius (18) and
|
|
200
|
+
* scales the head, its outline and the label together (card 190). */
|
|
201
|
+
export const milestonePin = definePiece("milestonePin", { x: pos(), y: pos(), text: label(), fill: col(), stem: size(), size: { kind: "number", min: 6, max: 48 } }, ({ x, y, text, fill = "color.healthy", stem = 44, size: r }) => {
|
|
202
|
+
// Without `size`, the 0.5.0 expressions exactly (no rounding), so every 0.5.0 call keeps its bytes.
|
|
203
|
+
const k = (r ?? PIN_SIZE) / PIN_SIZE;
|
|
204
|
+
const at = r === undefined ? (v) => v : r2;
|
|
205
|
+
return drawn([el("line", { x1: x, y1: y, x2: x, y2: y - stem }), el("circle", { cx: x, cy: at(y - stem - 14 * k), r: r ?? PIN_SIZE, fill: colour(fill, "fill"), "stroke-width": at(tokens.outline.small * k) })], [el("text", { x, y: at(y - stem - 7 * k), "font-size": r ?? PIN_SIZE, "font-weight": tokens.font.weightHeavy }, text)]);
|
|
206
|
+
});
|
|
197
207
|
/** Price or version tag pointing left from its tip (x, y), with a hole and an optional string. */
|
|
198
208
|
export const priceTag = definePiece("priceTag", { x: pos(), y: pos(), text: label(), width: size(), fill: col(), stringFrom: { kind: "point" } }, ({ x, y, text, width: w = 128, fill = "color.white", stringFrom }) => {
|
|
199
209
|
const h = 48;
|
|
@@ -385,5 +395,7 @@ export const PIECES = {
|
|
|
385
395
|
linkIcon,
|
|
386
396
|
...DIAGRAM_PIECES,
|
|
387
397
|
stateStart,
|
|
398
|
+
mascot,
|
|
399
|
+
emote,
|
|
388
400
|
};
|
|
389
401
|
export const PIECE_NAMES = Object.keys(PIECES);
|
|
@@ -34,6 +34,7 @@ export type CountBadgeParams = Ink & {
|
|
|
34
34
|
text: string;
|
|
35
35
|
width?: number;
|
|
36
36
|
height?: number;
|
|
37
|
+
textSize?: number;
|
|
37
38
|
};
|
|
38
39
|
/** A count (rules of a skill): a circle when as wide as high, a pill otherwise, in the surface's panel colour. */
|
|
39
40
|
export declare const countBadge: import("./piece.ts").Piece<CountBadgeParams>;
|
|
@@ -56,12 +57,15 @@ export declare const pillNode: import("./piece.ts").Piece<NodeParams>;
|
|
|
56
57
|
/** A diagram hexagon (an agent). */
|
|
57
58
|
export declare const hexNode: import("./piece.ts").Piece<NodeParams>;
|
|
58
59
|
export type ArrowKind = "message" | "reply" | "failure" | "failFast";
|
|
60
|
+
export type ArrowHeads = "end" | "start" | "both";
|
|
59
61
|
export type MessageArrowParams = {
|
|
60
62
|
from: Point;
|
|
61
63
|
to: Point;
|
|
62
64
|
curve?: "bezier" | "straight";
|
|
63
65
|
controls?: [Point, Point];
|
|
64
66
|
head?: boolean;
|
|
67
|
+
heads?: ArrowHeads;
|
|
68
|
+
dashed?: boolean;
|
|
65
69
|
label?: string;
|
|
66
70
|
width?: number;
|
|
67
71
|
kind?: ArrowKind;
|
package/dist/src/pieces/metro.js
CHANGED
|
@@ -70,20 +70,30 @@ export const pipe = definePiece("pipe", { d: PIPE_PATH, fill: dataColour(), widt
|
|
|
70
70
|
el("path", { ...line, stroke: paint(fill), "stroke-width": width }),
|
|
71
71
|
]);
|
|
72
72
|
});
|
|
73
|
-
|
|
73
|
+
// `textSize` scales the default height, the per-character width and the baseline with it (card 190);
|
|
74
|
+
// without it, k is 1 and every expression is the 0.5.0 one.
|
|
75
|
+
const countScale = ({ textSize }) => (textSize ?? M.countTextSize) / M.countTextSize;
|
|
76
|
+
const countHeight = (params) => params.height ?? (params.textSize === undefined ? M.countHeight : r2(M.countHeight * countScale(params)));
|
|
77
|
+
const countWidth = (params) => {
|
|
78
|
+
const { width, text } = params;
|
|
79
|
+
const height = countHeight(params);
|
|
80
|
+
const k = countScale(params);
|
|
74
81
|
const characters = Array.from(text).length;
|
|
75
|
-
return width ?? (characters === 1 ? height : Math.max(height, 6.4 * characters + 8));
|
|
82
|
+
return width ?? (characters === 1 ? height : Math.max(height, k === 1 ? 6.4 * characters + 8 : r2(k * (6.4 * characters + 8))));
|
|
76
83
|
};
|
|
77
84
|
/** A count (rules of a skill): a circle when as wide as high, a pill otherwise, in the surface's panel colour. */
|
|
78
|
-
export const countBadge = definePiece("countBadge", { cx: pos(), cy: pos(), text: label(), width: size(), height: size(), ...inkSchema }, (params) => {
|
|
79
|
-
const { cx, cy, text,
|
|
85
|
+
export const countBadge = definePiece("countBadge", { cx: pos(), cy: pos(), text: label(), width: size(), height: size(), textSize: { kind: "number", min: 6, max: 48 }, ...inkSchema }, (params) => {
|
|
86
|
+
const { cx, cy, text, textSize = M.countTextSize } = params;
|
|
87
|
+
const height = countHeight(params);
|
|
80
88
|
const width = countWidth(params);
|
|
81
89
|
const { s } = inkOf(params);
|
|
82
90
|
const shape = width === height ? (dx, dy, attrs) => el("circle", { cx: r2(cx + dx), cy: r2(cy + dy), r: r2(height / 2), ...attrs }) : pill(cx - width / 2, cy - height / 2, width, height);
|
|
83
|
-
return drawn(sticker(shape, s.panel, params), [words(cx, cy + 4, text, { fill: s.text, "font-size":
|
|
91
|
+
return drawn(sticker(shape, s.panel, params), [words(cx, cy + 4 * countScale(params), text, { fill: s.text, "font-size": textSize, "font-weight": bold, "text-anchor": "middle" })]);
|
|
84
92
|
}, (params) => {
|
|
85
|
-
const height = params
|
|
86
|
-
|
|
93
|
+
const height = countHeight(params);
|
|
94
|
+
// A height derived from textSize is reported at textSize, the parameter the caller passed (card 190).
|
|
95
|
+
const name = params.height === undefined && params.textSize !== undefined ? "textSize" : "height";
|
|
96
|
+
return limits(atLeast("width", countWidth(params), height, "the height"), roomFor(name, height, params));
|
|
87
97
|
});
|
|
88
98
|
const nodeSchema = {
|
|
89
99
|
x: pos(),
|
|
@@ -147,11 +157,13 @@ export const messageArrow = definePiece("messageArrow", {
|
|
|
147
157
|
curve: { kind: "enum", values: ["bezier", "straight"] },
|
|
148
158
|
controls: { kind: "array", minItems: 2, maxItems: 2, of: point() },
|
|
149
159
|
head: { kind: "boolean" },
|
|
160
|
+
heads: { kind: "enum", values: ["end", "start", "both"] },
|
|
161
|
+
dashed: { kind: "boolean" },
|
|
150
162
|
label: label(false),
|
|
151
163
|
width: { kind: "number", min: 1, max: 12 },
|
|
152
164
|
kind: { kind: "enum", values: ["message", "reply", "failure", "failFast"] },
|
|
153
165
|
surface: surfaceParam,
|
|
154
|
-
}, ({ from, to, curve = "bezier", controls, head = true, label: text, width = M.edgeWidth, kind = "message", surface = "light" }) => {
|
|
166
|
+
}, ({ from, to, curve = "bezier", controls, head = true, heads = "end", dashed, label: text, width = M.edgeWidth, kind = "message", surface = "light" }) => {
|
|
155
167
|
const s = tokens.surface[surface];
|
|
156
168
|
const paint_ = kind === "failure" ? tokens.color.failure : kind === "failFast" ? tokens.color.failFast : s.text;
|
|
157
169
|
const length = 5 * width;
|
|
@@ -166,21 +178,35 @@ export const messageArrow = definePiece("messageArrow", {
|
|
|
166
178
|
};
|
|
167
179
|
const tail = controls && [controls[1], controls[0], from].find((q) => q[0] !== to[0] || q[1] !== to[1]);
|
|
168
180
|
const direction = tail !== undefined ? towards(tail) : bent ? [Math.sign(dx), 0] : [dx / distance, (to[1] - from[1]) / distance];
|
|
169
|
-
const
|
|
181
|
+
const atEnd = head && heads !== "start";
|
|
182
|
+
const atStart = head && heads !== "end";
|
|
183
|
+
const end = atEnd ? [to[0] - direction[0] * length, to[1] - direction[1] * length] : to;
|
|
184
|
+
// A head at `from` points back at it, from the first of c1, c2, to that differs from `from` (card 190).
|
|
185
|
+
const away = (q) => {
|
|
186
|
+
const l = Math.hypot(from[0] - q[0], from[1] - q[1]);
|
|
187
|
+
return [(from[0] - q[0]) / l, (from[1] - q[1]) / l];
|
|
188
|
+
};
|
|
189
|
+
const lead = controls && [controls[0], controls[1], to].find((q) => q[0] !== from[0] || q[1] !== from[1]);
|
|
190
|
+
const back = lead !== undefined ? away(lead) : bent ? [-Math.sign(dx), 0] : [-direction[0], -direction[1]];
|
|
191
|
+
const start = atStart ? [from[0] - back[0] * length, from[1] - back[1] * length] : from;
|
|
170
192
|
const mx = (from[0] + to[0]) / 2;
|
|
171
193
|
const at = (p) => `${r2(p[0])},${r2(p[1])}`;
|
|
172
194
|
const d = controls
|
|
173
|
-
? `M${at(
|
|
195
|
+
? `M${at(start)} C${at(controls[0])} ${at(controls[1])} ${at(end)}`
|
|
174
196
|
: bent
|
|
175
|
-
? `M${at(
|
|
176
|
-
: `M${at(
|
|
177
|
-
const dash = kind
|
|
197
|
+
? `M${at(start)} C${r2(mx)},${r2(from[1])} ${r2(mx)},${r2(to[1])} ${at(end)}`
|
|
198
|
+
: `M${at(start)} L${at(end)}`;
|
|
199
|
+
const dash = !(dashed ?? kind !== "message") ? {} : { "stroke-dasharray": `${r2(2.4 * width)} ${r2(3.2 * width)}` };
|
|
178
200
|
const body = [el("path", { d, fill: "none", stroke: paint_, "stroke-width": r2(width), "stroke-linecap": "round", ...dash })];
|
|
179
|
-
|
|
180
|
-
const normal = [-
|
|
181
|
-
const points = [
|
|
201
|
+
const arrowhead = (tip, base, along) => {
|
|
202
|
+
const normal = [-along[1] * 2 * width, along[0] * 2 * width];
|
|
203
|
+
const points = [tip, [base[0] + normal[0], base[1] + normal[1]], [base[0] - normal[0], base[1] - normal[1]]];
|
|
182
204
|
body.push(el("polygon", { points: points.map(at).join(" "), fill: paint_, stroke: paint_, "stroke-width": 1, "stroke-linejoin": "round" }));
|
|
183
|
-
}
|
|
205
|
+
};
|
|
206
|
+
if (atEnd)
|
|
207
|
+
arrowhead(to, end, direction);
|
|
208
|
+
if (atStart)
|
|
209
|
+
arrowhead(from, start, back);
|
|
184
210
|
const words_ = [];
|
|
185
211
|
if (text !== undefined) {
|
|
186
212
|
const middle = [mx, (from[1] + to[1]) / 2];
|
|
@@ -189,9 +215,10 @@ export const messageArrow = definePiece("messageArrow", {
|
|
|
189
215
|
words_.push(words(middle[0], middle[1] + 4, text, { fill: s.text, "font-size": 11, "font-weight": 400, "text-anchor": "middle" }));
|
|
190
216
|
}
|
|
191
217
|
return drawn(body, words_);
|
|
192
|
-
}, ({ from, to, head = true, width = M.edgeWidth, controls, curve }) => {
|
|
218
|
+
}, ({ from, to, head = true, heads, width = M.edgeWidth, controls, curve }) => {
|
|
193
219
|
const distance = Math.hypot(to[0] - from[0], to[1] - from[1]);
|
|
194
|
-
|
|
220
|
+
const need = (heads === "both" ? 20 : 10) * width;
|
|
221
|
+
return limits(head === false && heads !== undefined && { path: "heads", message: "cannot be combined with head: false" }, distance === 0 && { path: "to", message: "must differ from from" }, distance > 0 && head && distance < need && { path: "to", message: `is too close to from for the ${heads === "both" ? "heads" : "head"}: needs at least ${r2(need)}, found ${r2(distance)}` }, controls !== undefined && curve !== undefined && { path: "controls", message: "cannot be combined with curve" });
|
|
195
222
|
});
|
|
196
223
|
/** stackledger's mark (favicon, header): an ink square with a bent line and two stations, 32 units scaled to `size`.
|
|
197
224
|
* On a surface with a rim (dark), a light rim of 2 × rimWidth lines the square inside its 32 box (card 167). */
|
|
@@ -11,6 +11,43 @@ export declare const tokens: {
|
|
|
11
11
|
readonly startRadius: 11;
|
|
12
12
|
readonly width: 800;
|
|
13
13
|
};
|
|
14
|
+
readonly cast: {
|
|
15
|
+
readonly agent: {
|
|
16
|
+
readonly neck: "#E3E8F5";
|
|
17
|
+
readonly shine: "#E6ECFF";
|
|
18
|
+
};
|
|
19
|
+
readonly architect: {
|
|
20
|
+
readonly fill: "#22B5A4";
|
|
21
|
+
readonly shine: "#9CF0E4";
|
|
22
|
+
};
|
|
23
|
+
readonly data: {
|
|
24
|
+
readonly fill: "#8C6CF0";
|
|
25
|
+
readonly lens: "#EAF1FF";
|
|
26
|
+
readonly lid: "#C2B1FF";
|
|
27
|
+
readonly shine: "#B39CFF";
|
|
28
|
+
};
|
|
29
|
+
readonly dev: {
|
|
30
|
+
readonly chin: "#8FA6FF";
|
|
31
|
+
readonly fill: "#5B7CFA";
|
|
32
|
+
readonly screen: "#DCE6FF";
|
|
33
|
+
};
|
|
34
|
+
readonly devops: {
|
|
35
|
+
readonly fill: "#D76AA5";
|
|
36
|
+
readonly shine: "#F3B8D6";
|
|
37
|
+
};
|
|
38
|
+
readonly emotePop: {
|
|
39
|
+
readonly damping: 9;
|
|
40
|
+
readonly mass: 0.7;
|
|
41
|
+
readonly stiffness: 170;
|
|
42
|
+
};
|
|
43
|
+
readonly glow: "#7FD1FF";
|
|
44
|
+
readonly ground: "#D3DBF5";
|
|
45
|
+
readonly metal: "#C9D2E8";
|
|
46
|
+
readonly mouth: "#B94A3E";
|
|
47
|
+
readonly request: {
|
|
48
|
+
readonly shine: "#D6E2FF";
|
|
49
|
+
};
|
|
50
|
+
};
|
|
14
51
|
readonly color: {
|
|
15
52
|
readonly backend: "#FF6B6B";
|
|
16
53
|
readonly blocked: "#FF8A3D";
|
|
@@ -12,6 +12,43 @@ export const tokens = {
|
|
|
12
12
|
"startRadius": 11,
|
|
13
13
|
"width": 800
|
|
14
14
|
},
|
|
15
|
+
"cast": {
|
|
16
|
+
"agent": {
|
|
17
|
+
"neck": "#E3E8F5",
|
|
18
|
+
"shine": "#E6ECFF"
|
|
19
|
+
},
|
|
20
|
+
"architect": {
|
|
21
|
+
"fill": "#22B5A4",
|
|
22
|
+
"shine": "#9CF0E4"
|
|
23
|
+
},
|
|
24
|
+
"data": {
|
|
25
|
+
"fill": "#8C6CF0",
|
|
26
|
+
"lens": "#EAF1FF",
|
|
27
|
+
"lid": "#C2B1FF",
|
|
28
|
+
"shine": "#B39CFF"
|
|
29
|
+
},
|
|
30
|
+
"dev": {
|
|
31
|
+
"chin": "#8FA6FF",
|
|
32
|
+
"fill": "#5B7CFA",
|
|
33
|
+
"screen": "#DCE6FF"
|
|
34
|
+
},
|
|
35
|
+
"devops": {
|
|
36
|
+
"fill": "#D76AA5",
|
|
37
|
+
"shine": "#F3B8D6"
|
|
38
|
+
},
|
|
39
|
+
"emotePop": {
|
|
40
|
+
"damping": 9,
|
|
41
|
+
"mass": 0.7,
|
|
42
|
+
"stiffness": 170
|
|
43
|
+
},
|
|
44
|
+
"glow": "#7FD1FF",
|
|
45
|
+
"ground": "#D3DBF5",
|
|
46
|
+
"metal": "#C9D2E8",
|
|
47
|
+
"mouth": "#B94A3E",
|
|
48
|
+
"request": {
|
|
49
|
+
"shine": "#D6E2FF"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
15
52
|
"color": {
|
|
16
53
|
"backend": "#FF6B6B",
|
|
17
54
|
"blocked": "#FF8A3D",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brunosouzas/design-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Design tokens and illustration pieces of brunosouzas.com: tokens as CSS, JSON and TypeScript, pieces that draw SVG (and React), and a cover command.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -8,6 +8,27 @@
|
|
|
8
8
|
--ds-body-diagram-ring-radius: 70px;
|
|
9
9
|
--ds-body-diagram-start-radius: 11px;
|
|
10
10
|
--ds-body-diagram-width: 800px;
|
|
11
|
+
--ds-cast-agent-neck: #E3E8F5;
|
|
12
|
+
--ds-cast-agent-shine: #E6ECFF;
|
|
13
|
+
--ds-cast-architect-fill: #22B5A4;
|
|
14
|
+
--ds-cast-architect-shine: #9CF0E4;
|
|
15
|
+
--ds-cast-data-fill: #8C6CF0;
|
|
16
|
+
--ds-cast-data-lens: #EAF1FF;
|
|
17
|
+
--ds-cast-data-lid: #C2B1FF;
|
|
18
|
+
--ds-cast-data-shine: #B39CFF;
|
|
19
|
+
--ds-cast-dev-chin: #8FA6FF;
|
|
20
|
+
--ds-cast-dev-fill: #5B7CFA;
|
|
21
|
+
--ds-cast-dev-screen: #DCE6FF;
|
|
22
|
+
--ds-cast-devops-fill: #D76AA5;
|
|
23
|
+
--ds-cast-devops-shine: #F3B8D6;
|
|
24
|
+
--ds-cast-emote-pop-damping: 9;
|
|
25
|
+
--ds-cast-emote-pop-mass: 0.7;
|
|
26
|
+
--ds-cast-emote-pop-stiffness: 170;
|
|
27
|
+
--ds-cast-glow: #7FD1FF;
|
|
28
|
+
--ds-cast-ground: #D3DBF5;
|
|
29
|
+
--ds-cast-metal: #C9D2E8;
|
|
30
|
+
--ds-cast-mouth: #B94A3E;
|
|
31
|
+
--ds-cast-request-shine: #D6E2FF;
|
|
11
32
|
--ds-color-backend: #FF6B6B;
|
|
12
33
|
--ds-color-blocked: #FF8A3D;
|
|
13
34
|
--ds-color-caller: #8FB2FF;
|
|
@@ -11,6 +11,43 @@
|
|
|
11
11
|
"startRadius": 11,
|
|
12
12
|
"width": 800
|
|
13
13
|
},
|
|
14
|
+
"cast": {
|
|
15
|
+
"agent": {
|
|
16
|
+
"neck": "#E3E8F5",
|
|
17
|
+
"shine": "#E6ECFF"
|
|
18
|
+
},
|
|
19
|
+
"architect": {
|
|
20
|
+
"fill": "#22B5A4",
|
|
21
|
+
"shine": "#9CF0E4"
|
|
22
|
+
},
|
|
23
|
+
"data": {
|
|
24
|
+
"fill": "#8C6CF0",
|
|
25
|
+
"lens": "#EAF1FF",
|
|
26
|
+
"lid": "#C2B1FF",
|
|
27
|
+
"shine": "#B39CFF"
|
|
28
|
+
},
|
|
29
|
+
"dev": {
|
|
30
|
+
"chin": "#8FA6FF",
|
|
31
|
+
"fill": "#5B7CFA",
|
|
32
|
+
"screen": "#DCE6FF"
|
|
33
|
+
},
|
|
34
|
+
"devops": {
|
|
35
|
+
"fill": "#D76AA5",
|
|
36
|
+
"shine": "#F3B8D6"
|
|
37
|
+
},
|
|
38
|
+
"emotePop": {
|
|
39
|
+
"damping": 9,
|
|
40
|
+
"mass": 0.7,
|
|
41
|
+
"stiffness": 170
|
|
42
|
+
},
|
|
43
|
+
"glow": "#7FD1FF",
|
|
44
|
+
"ground": "#D3DBF5",
|
|
45
|
+
"metal": "#C9D2E8",
|
|
46
|
+
"mouth": "#B94A3E",
|
|
47
|
+
"request": {
|
|
48
|
+
"shine": "#D6E2FF"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
14
51
|
"color": {
|
|
15
52
|
"backend": "#FF6B6B",
|
|
16
53
|
"blocked": "#FF8A3D",
|
|
@@ -13,6 +13,43 @@ export const tokens = {
|
|
|
13
13
|
"startRadius": 11,
|
|
14
14
|
"width": 800
|
|
15
15
|
},
|
|
16
|
+
"cast": {
|
|
17
|
+
"agent": {
|
|
18
|
+
"neck": "#E3E8F5",
|
|
19
|
+
"shine": "#E6ECFF"
|
|
20
|
+
},
|
|
21
|
+
"architect": {
|
|
22
|
+
"fill": "#22B5A4",
|
|
23
|
+
"shine": "#9CF0E4"
|
|
24
|
+
},
|
|
25
|
+
"data": {
|
|
26
|
+
"fill": "#8C6CF0",
|
|
27
|
+
"lens": "#EAF1FF",
|
|
28
|
+
"lid": "#C2B1FF",
|
|
29
|
+
"shine": "#B39CFF"
|
|
30
|
+
},
|
|
31
|
+
"dev": {
|
|
32
|
+
"chin": "#8FA6FF",
|
|
33
|
+
"fill": "#5B7CFA",
|
|
34
|
+
"screen": "#DCE6FF"
|
|
35
|
+
},
|
|
36
|
+
"devops": {
|
|
37
|
+
"fill": "#D76AA5",
|
|
38
|
+
"shine": "#F3B8D6"
|
|
39
|
+
},
|
|
40
|
+
"emotePop": {
|
|
41
|
+
"damping": 9,
|
|
42
|
+
"mass": 0.7,
|
|
43
|
+
"stiffness": 170
|
|
44
|
+
},
|
|
45
|
+
"glow": "#7FD1FF",
|
|
46
|
+
"ground": "#D3DBF5",
|
|
47
|
+
"metal": "#C9D2E8",
|
|
48
|
+
"mouth": "#B94A3E",
|
|
49
|
+
"request": {
|
|
50
|
+
"shine": "#D6E2FF"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
16
53
|
"color": {
|
|
17
54
|
"backend": "#FF6B6B",
|
|
18
55
|
"blocked": "#FF8A3D",
|