@cardenelabs/dragon 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -720
- package/dist/cli.cjs +4529 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +4523 -0
- package/dist/cli.js.map +1 -0
- package/dist/export.cjs +3912 -0
- package/dist/export.cjs.map +1 -0
- package/dist/export.d.cts +36 -0
- package/dist/export.d.ts +36 -0
- package/dist/export.js +3907 -0
- package/dist/export.js.map +1 -0
- package/dist/fonts.cjs +7 -0
- package/dist/fonts.cjs.map +1 -0
- package/dist/fonts.js +4 -0
- package/dist/fonts.js.map +1 -0
- package/dist/index.cjs +4105 -20356
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +227 -5522
- package/dist/index.d.ts +227 -5522
- package/dist/index.js +4091 -20283
- package/dist/index.js.map +1 -1
- package/dist/player.global.js +2 -0
- package/dist/schema-BBWfniwH.d.cts +112 -0
- package/dist/schema-BBWfniwH.d.ts +112 -0
- package/package.json +39 -24
- package/src/cli.ts +237 -0
- package/src/export/capture.ts +277 -0
- package/src/export/fonts.ts +68 -0
- package/src/export/html.ts +32 -0
- package/src/export/index.ts +146 -0
- package/src/field-messages.ts +104 -0
- package/src/figure-text.ts +42 -0
- package/src/index.ts +30 -283
- package/src/layout/architecture.ts +381 -0
- package/src/layout/measure.ts +21 -0
- package/src/layout/scene.ts +353 -0
- package/src/layout/sequence.ts +189 -0
- package/src/load-fonts.ts +8 -0
- package/src/look/fonts.ts +33 -0
- package/src/look/palettes.json +209 -0
- package/src/look/values.ts +155 -0
- package/src/markdown.ts +35 -0
- package/src/messages.ts +122 -0
- package/src/parser.ts +342 -310
- package/src/player-global.ts +4 -0
- package/src/player.ts +271 -0
- package/src/printer.ts +76 -0
- package/src/render/icons.ts +41 -0
- package/src/render/motion.ts +106 -0
- package/src/render/parts.ts +279 -0
- package/src/render/svg.ts +341 -0
- package/src/schema.ts +161 -16
- package/src/schemas/diagram.schema.json +150 -0
- package/src/schemas/validate-diagram.d.ts +12 -0
- package/src/schemas/validate-diagram.js +8 -0
- package/src/timeline.ts +166 -0
- package/src/tokenize.ts +20 -281
- package/src/version.ts +55 -0
- package/src/view.ts +16 -0
- package/src/words.ts +79 -0
- package/examples/quick-start.md +0 -142
- package/src/canvas-bounds.ts +0 -52
- package/src/color.ts +0 -197
- package/src/compile/actor-ids.ts +0 -297
- package/src/compile/actor-option-notice.ts +0 -379
- package/src/compile/actors.ts +0 -103
- package/src/compile/c4.ts +0 -74
- package/src/compile/chain.ts +0 -93
- package/src/compile/chart-fields.ts +0 -193
- package/src/compile/deep-rewrite.ts +0 -30
- package/src/compile/derived-source.ts +0 -18
- package/src/compile/direction.ts +0 -102
- package/src/compile/edge-option-notice.ts +0 -112
- package/src/compile/er-relation.ts +0 -200
- package/src/compile/flow.ts +0 -54
- package/src/compile/flowchart.ts +0 -344
- package/src/compile/funnel.ts +0 -87
- package/src/compile/gantt.ts +0 -337
- package/src/compile/generic.ts +0 -696
- package/src/compile/hierarchy.ts +0 -92
- package/src/compile/journey.ts +0 -93
- package/src/compile/kinds.ts +0 -139
- package/src/compile/lanes.ts +0 -138
- package/src/compile/metro.ts +0 -271
- package/src/compile/mind.ts +0 -393
- package/src/compile/node-title.ts +0 -24
- package/src/compile/notice.ts +0 -141
- package/src/compile/order.ts +0 -57
- package/src/compile/parts-lookup.ts +0 -17
- package/src/compile/parts.ts +0 -2709
- package/src/compile/placement-apply.ts +0 -689
- package/src/compile/placement.ts +0 -15
- package/src/compile/quadrant.ts +0 -265
- package/src/compile/record.ts +0 -75
- package/src/compile/row-marks.ts +0 -128
- package/src/compile/rows.ts +0 -40
- package/src/compile/sequence.ts +0 -305
- package/src/compile/slug.ts +0 -37
- package/src/compile/stages.ts +0 -247
- package/src/compile/subtitle.ts +0 -35
- package/src/compile/swimlane-marks.ts +0 -87
- package/src/compile/swimlane.ts +0 -82
- package/src/compile/timeline.ts +0 -213
- package/src/compile/topology.ts +0 -44
- package/src/compile/tree.ts +0 -69
- package/src/compile/value-chart.ts +0 -337
- package/src/compile/values.ts +0 -432
- package/src/compile/word-state.ts +0 -82
- package/src/compile.ts +0 -3168
- package/src/focus.ts +0 -46
- package/src/input-size.ts +0 -181
- package/src/json-line-map.ts +0 -30
- package/src/json-parser.ts +0 -4306
- package/src/keywords.ts +0 -417
- package/src/near-name.ts +0 -42
- package/src/notation-lint.ts +0 -342
- package/src/relative-pos.ts +0 -251
- package/src/schemas/diagram.json +0 -3088
- package/src/types.ts +0 -1009
- package/src/v05/index.ts +0 -9
- package/src/v05/input-table.generated.ts +0 -138
- package/src/v05/parser-types.ts +0 -32
- package/src/v05/parser.ts +0 -6668
- package/src/v05/readout-table.generated.ts +0 -1088
- package/src/v05/removed-syntax.ts +0 -115
- package/src/value-syntax.ts +0 -313
- package/src/write-position.ts +0 -270
package/src/player.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 頁に図を置いて動かす。
|
|
3
|
+
*
|
|
4
|
+
* 動きを嫌う設定 (`prefers-reduced-motion: reduce`) は描く前に読み、その人には一瞬も自動再生せず、
|
|
5
|
+
* 段ごとの止め絵を釦で送る。各段の題と本文は、読み上げの道具が段の順に読める番号付きの一覧として置き、
|
|
6
|
+
* 図そのものは読み上げから隠して同じ中身を 2 度読ませない。
|
|
7
|
+
* 見せ方は枠の幅で選び (`view.ts`)、寄って追う時は窓と全体図で描き直す。
|
|
8
|
+
* 釦と段の一覧の読み上げの名前と全体図の札は、渡した言葉 (`options.lang`、書かなければ日本語) で出す (#3001)。
|
|
9
|
+
*/
|
|
10
|
+
import { FIGURE_TEXT } from "./figure-text";
|
|
11
|
+
import { FONT_VARS, LOOK, paletteVars, resolvePalette, type TextStyle, TYPE } from "./look/values";
|
|
12
|
+
import { drawSvg, type RenderOptions, renderDiagram } from "./render/svg";
|
|
13
|
+
import type { Span } from "./timeline";
|
|
14
|
+
import { chooseView } from "./view";
|
|
15
|
+
|
|
16
|
+
export interface Player {
|
|
17
|
+
/** 今の段 (0 から) */
|
|
18
|
+
readonly step: number;
|
|
19
|
+
/** 動いているか */
|
|
20
|
+
readonly playing: boolean;
|
|
21
|
+
next(): void;
|
|
22
|
+
prev(): void;
|
|
23
|
+
play(): void;
|
|
24
|
+
pause(): void;
|
|
25
|
+
destroy(): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const font = (t: TextStyle): string => `font:${t.weight} ${t.size}px var(--${t.family})`;
|
|
29
|
+
const px = (v: number): string => `${v}px`;
|
|
30
|
+
|
|
31
|
+
function playerCss(): string {
|
|
32
|
+
const P = LOOK.player;
|
|
33
|
+
const hidden = px(P.hidden);
|
|
34
|
+
return [
|
|
35
|
+
`.dg-player{display:flex;flex-direction:column;gap:${px(P.gap)};color:var(--ink)}`,
|
|
36
|
+
`.dg-player .dg-frame{border-radius:${px(P.radius)};overflow:hidden}`,
|
|
37
|
+
`.dg-bar{display:flex;align-items:flex-start;gap:${px(P.gap)}}`,
|
|
38
|
+
`.dg-btns{display:flex;gap:${px(P.gap / 2)};flex:none}`,
|
|
39
|
+
`.dg-btn{width:${px(P.button)};height:${px(P.button)};border-radius:${px(P.radius)};border:${px(LOOK.stroke.hair)} solid var(--hair);background:var(--surf);color:var(--ink);${font(TYPE.button)};cursor:pointer}`,
|
|
40
|
+
`.dg-btn:focus-visible{outline:${px(P.focus)} solid var(--c-blue);outline-offset:${px(P.focus)}}`,
|
|
41
|
+
`.dg-say{display:flex;gap:${px(P.gap)};min-width:0}`,
|
|
42
|
+
`.dg-no{${font(TYPE.stepNo)};color:var(--mute);padding-top:${px(P.focus)}}`,
|
|
43
|
+
`.dg-tt{${font(TYPE.stepTitle)};margin:0}`,
|
|
44
|
+
`.dg-tb{${font(TYPE.stepBody)};color:var(--ink2);margin:0;white-space:pre-line}`,
|
|
45
|
+
`.dg-sr{position:absolute;width:${hidden};height:${hidden};margin:-${hidden};padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}`,
|
|
46
|
+
].join("");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ensureCss(doc: Document): void {
|
|
50
|
+
if (doc.getElementById("dragon-player-css")) return;
|
|
51
|
+
const style = doc.createElement("style");
|
|
52
|
+
style.id = "dragon-player-css";
|
|
53
|
+
style.textContent = playerCss();
|
|
54
|
+
doc.head.appendChild(style);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 釦に出す字。HTML に書き出す時は、この字も字の file に残す。 */
|
|
58
|
+
export const BUTTON_TEXT = { prev: "‹", pause: "❚❚", play: "▶", next: "›" } as const;
|
|
59
|
+
|
|
60
|
+
const pad = (n: number): string => String(n).padStart(2, "0");
|
|
61
|
+
/** 段の番号の札 (`01/12`)。`step` は 0 から数える。 */
|
|
62
|
+
export const stepNo = (step: number, count: number): string => `${pad(step + 1)}/${pad(count)}`;
|
|
63
|
+
|
|
64
|
+
export function mountDiagram(host: HTMLElement, diagram: unknown, options: RenderOptions = {}): Player {
|
|
65
|
+
const doc = host.ownerDocument;
|
|
66
|
+
const win = doc.defaultView;
|
|
67
|
+
// 描く前に読む。描いた後に読むと、動きを嫌う人にも一瞬だけ流れてしまう
|
|
68
|
+
const reduce = Boolean(win?.matchMedia?.("(prefers-reduced-motion: reduce)").matches);
|
|
69
|
+
const rendered = renderDiagram(diagram, options);
|
|
70
|
+
const lang = options.lang ?? "ja";
|
|
71
|
+
const words = FIGURE_TEXT[lang];
|
|
72
|
+
const palette = resolvePalette(rendered.palette);
|
|
73
|
+
const { timeline } = rendered;
|
|
74
|
+
const steps = rendered.diagram.steps;
|
|
75
|
+
const count = steps.length;
|
|
76
|
+
|
|
77
|
+
ensureCss(doc);
|
|
78
|
+
const root = doc.createElement("div");
|
|
79
|
+
root.className = "dg-player";
|
|
80
|
+
root.dataset.dragon = "player";
|
|
81
|
+
root.setAttribute("style", `${FONT_VARS};${paletteVars(palette, rendered.theme)}`);
|
|
82
|
+
|
|
83
|
+
const frame = doc.createElement("div");
|
|
84
|
+
frame.className = "dg-frame";
|
|
85
|
+
const button = (name: string, label: string, text: string): HTMLButtonElement => {
|
|
86
|
+
const b = doc.createElement("button");
|
|
87
|
+
b.type = "button";
|
|
88
|
+
b.className = "dg-btn";
|
|
89
|
+
b.dataset.dragon = name;
|
|
90
|
+
b.setAttribute("aria-label", label);
|
|
91
|
+
b.textContent = text;
|
|
92
|
+
return b;
|
|
93
|
+
};
|
|
94
|
+
const prevButton = button("prev", words.prev, BUTTON_TEXT.prev);
|
|
95
|
+
const toggleButton = button("toggle", words.pause, BUTTON_TEXT.pause);
|
|
96
|
+
const nextButton = button("next", words.next, BUTTON_TEXT.next);
|
|
97
|
+
const buttons = doc.createElement("div");
|
|
98
|
+
buttons.className = "dg-btns";
|
|
99
|
+
// 置いた頁の言葉と違っても、釦の名前を名前の言葉の声で読ませる
|
|
100
|
+
buttons.setAttribute("lang", lang);
|
|
101
|
+
buttons.append(prevButton, toggleButton, nextButton);
|
|
102
|
+
|
|
103
|
+
// 見える語り。読み上げには下の一覧を読ませるので、こちらは読み上げから隠す
|
|
104
|
+
const say = doc.createElement("div");
|
|
105
|
+
say.className = "dg-say";
|
|
106
|
+
say.setAttribute("aria-hidden", "true");
|
|
107
|
+
const no = doc.createElement("div");
|
|
108
|
+
no.className = "dg-no";
|
|
109
|
+
no.dataset.dragon = "step-no";
|
|
110
|
+
const text = doc.createElement("div");
|
|
111
|
+
const title = doc.createElement("p");
|
|
112
|
+
title.className = "dg-tt";
|
|
113
|
+
title.dataset.dragon = "step-title";
|
|
114
|
+
const body = doc.createElement("p");
|
|
115
|
+
body.className = "dg-tb";
|
|
116
|
+
body.dataset.dragon = "step-body";
|
|
117
|
+
text.append(title, body);
|
|
118
|
+
say.append(no, text);
|
|
119
|
+
const bar = doc.createElement("div");
|
|
120
|
+
bar.className = "dg-bar";
|
|
121
|
+
bar.append(buttons, say);
|
|
122
|
+
|
|
123
|
+
// 読み上げの道具が段の順に読む一覧。見た目からは外すが、読み上げからは隠さない
|
|
124
|
+
const list = doc.createElement("ol");
|
|
125
|
+
list.className = "dg-sr";
|
|
126
|
+
list.dataset.dragon = "steps";
|
|
127
|
+
list.setAttribute("aria-label", words.steps(rendered.diagram.title));
|
|
128
|
+
for (const step of steps) {
|
|
129
|
+
const item = doc.createElement("li");
|
|
130
|
+
const head = doc.createElement("strong");
|
|
131
|
+
head.textContent = step.title;
|
|
132
|
+
item.append(head);
|
|
133
|
+
for (const line of step.body.split("\n")) {
|
|
134
|
+
const p = doc.createElement("p");
|
|
135
|
+
p.textContent = line;
|
|
136
|
+
item.append(p);
|
|
137
|
+
}
|
|
138
|
+
list.append(item);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
root.append(frame, bar, list);
|
|
142
|
+
host.append(root);
|
|
143
|
+
|
|
144
|
+
const frameWidth = (): number => frame.clientWidth || host.clientWidth || rendered.layout.width;
|
|
145
|
+
let mode = chooseView(rendered, frameWidth()).mode;
|
|
146
|
+
let width = frameWidth();
|
|
147
|
+
let svg: SVGSVGElement | null = null;
|
|
148
|
+
let step = 0;
|
|
149
|
+
let playing = !reduce;
|
|
150
|
+
let at = reduce ? (timeline.stills[0] ?? 0) : 0;
|
|
151
|
+
let born = Date.now();
|
|
152
|
+
|
|
153
|
+
/** 動きの変数を付けた図を置き直す。置き直すと動きの時計が最初から数え直される */
|
|
154
|
+
const place = (markup?: string): void => {
|
|
155
|
+
let next: SVGSVGElement | null;
|
|
156
|
+
if (markup !== undefined) {
|
|
157
|
+
const holder = doc.createElement("div");
|
|
158
|
+
holder.innerHTML = markup;
|
|
159
|
+
next = holder.querySelector("svg");
|
|
160
|
+
} else {
|
|
161
|
+
next = svg ? (svg.cloneNode(true) as SVGSVGElement) : null;
|
|
162
|
+
}
|
|
163
|
+
if (!next) return;
|
|
164
|
+
next.setAttribute("aria-hidden", "true");
|
|
165
|
+
next.removeAttribute("role");
|
|
166
|
+
next.removeAttribute("aria-label");
|
|
167
|
+
next.style.setProperty("--at", String(at));
|
|
168
|
+
next.style.setProperty("--play", playing ? "running" : "paused");
|
|
169
|
+
next.dataset.motion = playing ? "play" : "still";
|
|
170
|
+
if (svg) svg.replaceWith(next);
|
|
171
|
+
else frame.append(next);
|
|
172
|
+
svg = next;
|
|
173
|
+
born = Date.now();
|
|
174
|
+
};
|
|
175
|
+
const markup = (): string => (mode === "follow" ? drawSvg(rendered.diagram, rendered.layout, timeline, palette, rendered.theme, { width, lang }) : rendered.svg);
|
|
176
|
+
|
|
177
|
+
const now = (): number => (playing ? (at + (Date.now() - born) / 1000) % timeline.total : at);
|
|
178
|
+
const stepAt = (t: number): number => {
|
|
179
|
+
let found = 0;
|
|
180
|
+
timeline.steps.forEach((span: Span, i) => {
|
|
181
|
+
if (span.start <= t) found = i;
|
|
182
|
+
});
|
|
183
|
+
return found;
|
|
184
|
+
};
|
|
185
|
+
const show = (): void => {
|
|
186
|
+
const current = steps[step];
|
|
187
|
+
no.textContent = stepNo(step, count);
|
|
188
|
+
title.textContent = current?.title ?? "";
|
|
189
|
+
body.textContent = current?.body ?? "";
|
|
190
|
+
toggleButton.setAttribute("aria-label", playing ? words.pause : words.play);
|
|
191
|
+
toggleButton.textContent = playing ? BUTTON_TEXT.pause : BUTTON_TEXT.play;
|
|
192
|
+
};
|
|
193
|
+
const restart = (t: number, play: boolean): void => {
|
|
194
|
+
at = t;
|
|
195
|
+
playing = play;
|
|
196
|
+
place();
|
|
197
|
+
show();
|
|
198
|
+
};
|
|
199
|
+
const go = (target: number): void => {
|
|
200
|
+
step = Math.min(count - 1, Math.max(0, target));
|
|
201
|
+
const span = timeline.steps[step] as Span;
|
|
202
|
+
restart(playing ? span.start : (timeline.stills[step] ?? span.start), playing);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
place(markup());
|
|
206
|
+
show();
|
|
207
|
+
|
|
208
|
+
const timer = win?.setInterval(() => {
|
|
209
|
+
if (!playing) return;
|
|
210
|
+
const s = stepAt(now());
|
|
211
|
+
if (s !== step) {
|
|
212
|
+
step = s;
|
|
213
|
+
show();
|
|
214
|
+
}
|
|
215
|
+
}, 120);
|
|
216
|
+
const onPrev = (): void => go(step - 1);
|
|
217
|
+
const onNext = (): void => go(step + 1);
|
|
218
|
+
const onToggle = (): void => (playing ? restart(now(), false) : restart(at, true));
|
|
219
|
+
// 段の番号を押すとその段へ移る
|
|
220
|
+
const onFigure = (event: Event): void => {
|
|
221
|
+
const target = (event.target as Element | null)?.closest?.("[data-step]");
|
|
222
|
+
const s = target ? Number(target.getAttribute("data-step")) : Number.NaN;
|
|
223
|
+
if (Number.isInteger(s)) go(s);
|
|
224
|
+
};
|
|
225
|
+
prevButton.addEventListener("click", onPrev);
|
|
226
|
+
nextButton.addEventListener("click", onNext);
|
|
227
|
+
toggleButton.addEventListener("click", onToggle);
|
|
228
|
+
frame.addEventListener("click", onFigure);
|
|
229
|
+
|
|
230
|
+
// 枠の幅が変わったら見せ方を選び直す。寄って追う窓は幅で決まるので、幅が変われば描き直す
|
|
231
|
+
const Observer = (win as (Window & { ResizeObserver?: typeof ResizeObserver }) | null)?.ResizeObserver;
|
|
232
|
+
const observer = Observer
|
|
233
|
+
? new Observer(() => {
|
|
234
|
+
const w = frameWidth();
|
|
235
|
+
const chosen = chooseView(rendered, w).mode;
|
|
236
|
+
if (chosen === mode && (mode === "fixed" || w === width)) return;
|
|
237
|
+
const t = now();
|
|
238
|
+
mode = chosen;
|
|
239
|
+
width = w;
|
|
240
|
+
at = t;
|
|
241
|
+
place(markup());
|
|
242
|
+
})
|
|
243
|
+
: undefined;
|
|
244
|
+
observer?.observe(frame);
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
get step() {
|
|
248
|
+
return step;
|
|
249
|
+
},
|
|
250
|
+
get playing() {
|
|
251
|
+
return playing;
|
|
252
|
+
},
|
|
253
|
+
next: onNext,
|
|
254
|
+
prev: onPrev,
|
|
255
|
+
play: () => {
|
|
256
|
+
if (!playing) restart(at, true);
|
|
257
|
+
},
|
|
258
|
+
pause: () => {
|
|
259
|
+
if (playing) restart(now(), false);
|
|
260
|
+
},
|
|
261
|
+
destroy: () => {
|
|
262
|
+
if (timer !== undefined) win?.clearInterval(timer);
|
|
263
|
+
observer?.disconnect();
|
|
264
|
+
prevButton.removeEventListener("click", onPrev);
|
|
265
|
+
nextButton.removeEventListener("click", onNext);
|
|
266
|
+
toggleButton.removeEventListener("click", onToggle);
|
|
267
|
+
frame.removeEventListener("click", onFigure);
|
|
268
|
+
root.remove();
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
package/src/printer.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 図の原本の JSON を、人が書く記法へ書き戻す。
|
|
3
|
+
*
|
|
4
|
+
* 並べ方は決まった形に整える。JSON の欄の並び順は見ず、欄の名前で読むので、
|
|
5
|
+
* 同じ内容の JSON からは毎回同じ文字列になる。書き手の行の並べ方とコメントは保たない。
|
|
6
|
+
* 段の中で起きることは JSON の並び順 (起きる順) のまま書く。
|
|
7
|
+
*
|
|
8
|
+
* 語は渡した言葉で書く (#2997)。渡さなければ日本語で書く。
|
|
9
|
+
* 英語では添え書きと下の字と書き換えの字を `"…"` で囲み、名前か字に `"` を含む物だけ「」で囲む。
|
|
10
|
+
* 英語の語の file でも「」は読めるので、読み直すと同じ JSON に戻る。
|
|
11
|
+
*/
|
|
12
|
+
import type { Box, Carry, Diagram, DiagramEvent, Region } from "./schema";
|
|
13
|
+
import { type NotationLang, type WORDS, wordOf } from "./words";
|
|
14
|
+
|
|
15
|
+
type AttrKey = keyof typeof WORDS.attr;
|
|
16
|
+
|
|
17
|
+
export interface PrintOptions {
|
|
18
|
+
/** 記法の語の言葉。既定は日本語 */
|
|
19
|
+
lang?: NotationLang;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 名前の後ろに `text` を囲んで付ける。`text` が無ければ何も付けない */
|
|
23
|
+
function quoted(name: string, text: string | undefined, lang: NotationLang): string {
|
|
24
|
+
if (text === undefined) return "";
|
|
25
|
+
if (lang === "en" && !name.includes('"') && !text.includes('"')) return ` "${text}"`;
|
|
26
|
+
return `「${text}」`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function printNotation(diagram: Diagram, options: PrintOptions = {}): string {
|
|
30
|
+
const lang = options.lang ?? "ja";
|
|
31
|
+
const attrs = (pairs: [AttrKey, string | undefined][]): string =>
|
|
32
|
+
pairs
|
|
33
|
+
.filter((pair): pair is [AttrKey, string] => pair[1] !== undefined)
|
|
34
|
+
.map(([key, value]) => ` [${wordOf("attr", key, lang)} ${value}]`)
|
|
35
|
+
.join("");
|
|
36
|
+
|
|
37
|
+
const region = (r: Region): string =>
|
|
38
|
+
`${wordOf("head", "region", lang)} ${r.id} = ${r.name}${quoted(r.name, r.note, lang)}${attrs([["color", r.color], ["icon", r.icon]])}`;
|
|
39
|
+
const box = (b: Box): string =>
|
|
40
|
+
`${b.id} = ${b.name}${quoted(b.name, b.sub, lang)}${attrs([["color", b.color], ["icon", b.icon], ["region", b.region]])}`;
|
|
41
|
+
|
|
42
|
+
const carry = (c: Carry): string => {
|
|
43
|
+
if (c.kind === "envelope") return "";
|
|
44
|
+
if (c.kind === "tag") return ` [${wordOf("carry", "tag", lang)} ${c.text}]`;
|
|
45
|
+
return ` [${wordOf("carry", c.kind, lang)}]`;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const event = (e: DiagramEvent): string => {
|
|
49
|
+
switch (e.type) {
|
|
50
|
+
case "edge":
|
|
51
|
+
return `${e.from} -> ${e.to}${e.label === undefined ? "" : ` : ${e.label}`}${carry(e.carry)}${e.labelSide ? ` [${wordOf("attr", "label", lang)} ${wordOf("side", e.labelSide, lang)}]` : ""}`;
|
|
52
|
+
case "action":
|
|
53
|
+
return `${e.box} : ${e.label}`;
|
|
54
|
+
case "swap":
|
|
55
|
+
return `${e.box}${quoted("", e.text, lang)}`;
|
|
56
|
+
case "strip":
|
|
57
|
+
return `${wordOf("head", "strip", lang)} ${e.field}${e.value === undefined ? "" : ` = ${e.value}`}${e.tag === undefined ? "" : ` [${e.tag}]`}`;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const head = [
|
|
62
|
+
...(diagram.view ? [`${wordOf("head", "view", lang)} ${wordOf("view", diagram.view, lang)}`] : []),
|
|
63
|
+
...(diagram.regions ?? []).map(region),
|
|
64
|
+
...diagram.boxes.map(box),
|
|
65
|
+
...(diagram.strip ? [`${wordOf("head", "strip", lang)} ${diagram.strip.title} = ${diagram.strip.fields.join(", ")}`] : []),
|
|
66
|
+
];
|
|
67
|
+
const steps = diagram.steps.map((step) =>
|
|
68
|
+
[
|
|
69
|
+
`# ${step.title}`,
|
|
70
|
+
...step.body.split("\n").map((line) => `> ${line}`),
|
|
71
|
+
...(step.code === undefined ? [] : [`\`${step.code}\``]),
|
|
72
|
+
...step.events.map(event),
|
|
73
|
+
].join("\n"),
|
|
74
|
+
);
|
|
75
|
+
return [`${wordOf("kind", diagram.kind, lang)} ${diagram.title}`, head.join("\n"), ...steps].join("\n\n") + "\n";
|
|
76
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 箱と区画の印の形。どれも (0, 0) を中心に 20 四方へ収める。
|
|
3
|
+
*
|
|
4
|
+
* 見本帳 (`docs/design/base/engine.js` の `G`) と同じ形を持つ。`block` だけは見本帳が絵を手で描いていたので、ここで足した。
|
|
5
|
+
*/
|
|
6
|
+
export const ICONS: Readonly<Record<string, string>> = {
|
|
7
|
+
app: "M -9 -7 h18 a2 2 0 0 1 2 2 v10 a2 2 0 0 1 -2 2 h-18 a2 2 0 0 1 -2 -2 v-10 a2 2 0 0 1 2 -2 z M -11 -2 h22",
|
|
8
|
+
router: "M -9 -4 h16 M 3 -8 l4 4 l-4 4 M 9 5 h-16 M -3 1 l-4 4 l4 4",
|
|
9
|
+
on: "M -7 7 L 7 -7 M -1 -7 h8 v8",
|
|
10
|
+
off: "M -7 -7 L 7 7 M 7 -1 v8 h-8",
|
|
11
|
+
pool: "M -9 -5 a9 3.5 0 1 0 18 0 a9 3.5 0 1 0 -18 0 M -9 -5 v10 a9 3.5 0 0 0 18 0 v-10 M -9 0 a9 3.5 0 0 0 18 0",
|
|
12
|
+
db: "M -9 -5 a9 3.5 0 1 0 18 0 a9 3.5 0 1 0 -18 0 M -9 -5 v10 a9 3.5 0 0 0 18 0 v-10 M -9 0 a9 3.5 0 0 0 18 0",
|
|
13
|
+
exe: "M -5 -8 L 7 0 L -5 8 Z",
|
|
14
|
+
idx: "M -9 -7 h18 M -9 -2 h18 M -9 3 h18 M -9 8 h11",
|
|
15
|
+
sign: "M -9 4 c 3 -9 5 -9 6 0 s 3 7 6 -2 s 3 -6 6 0",
|
|
16
|
+
env: "M -8 -5.5 h16 v11 h-16 z M -8 -5.5 l8 6 l8 -6",
|
|
17
|
+
lock: "M -5.5 -1 h11 v8 h-11 z M -3.5 -1 v-3 a3.5 3.5 0 0 1 7 0 v3",
|
|
18
|
+
check: "M -5 0 l3.5 3.5 l6.5 -7",
|
|
19
|
+
x: "M -5 -5 l10 10 M 5 -5 l-10 10",
|
|
20
|
+
chain: "M -1.5 4.5 l-2 2 a3.5 3.5 0 0 1 -5 -5 l3 -3 a3.5 3.5 0 0 1 5 0 M 1.5 -4.5 l2 -2 a3.5 3.5 0 0 1 5 5 l-3 3 a3.5 3.5 0 0 1 -5 0 M -2.5 2.5 l5 -5",
|
|
21
|
+
net: "M -1.3 -4.6 L -5.8 3 M 1.3 -4.6 L 5.8 3 M -4.3 5.5 H 4.3 M -2.2 -7 a 2.2 2.2 0 1 0 4.4 0 a 2.2 2.2 0 1 0 -4.4 0 M -9.2 5.5 a 2.2 2.2 0 1 0 4.4 0 a 2.2 2.2 0 1 0 -4.4 0 M 4.8 5.5 a 2.2 2.2 0 1 0 4.4 0 a 2.2 2.2 0 1 0 -4.4 0",
|
|
22
|
+
user: "M -4 -4.5 a4 4 0 1 0 8 0 a4 4 0 1 0 -8 0 M -8 9 c0 -5 3.6 -7.5 8 -7.5 s8 2.5 8 7.5",
|
|
23
|
+
server: "M -9 -8 h18 v7 h-18 z M -9 1 h18 v7 h-18 z M -5 -4.5 h0.01 M -5 4.5 h0.01",
|
|
24
|
+
key: "M -9 0 a3.5 3.5 0 1 0 7 0 a3.5 3.5 0 1 0 -7 0 M -2 0 h10 M 5 0 v3.5 M 8 0 v2.5",
|
|
25
|
+
shield: "M 0 -9 l8 3 v5 c0 5 -3.5 8 -8 10 c-4.5 -2 -8 -5 -8 -10 v-5 z",
|
|
26
|
+
globe: "M -9 0 a9 9 0 1 0 18 0 a9 9 0 1 0 -18 0 M -9 0 h18 M 0 -9 c-4.5 4 -4.5 14 0 18 M 0 -9 c4.5 4 4.5 14 0 18",
|
|
27
|
+
bolt: "M 2 -9 L -5 1 h5 l-2 8 l7 -10 h-5 z",
|
|
28
|
+
doc: "M -6 -9 h8 l5 5 v13 h-13 z M 2 -9 v5 h5 M -3 1 h7 M -3 5 h5",
|
|
29
|
+
box: "M -9 -4.5 l9 -4.5 l9 4.5 v9 l-9 4.5 l-9 -4.5 z M -9 -4.5 l9 4.5 l9 -4.5 M 0 0 v9",
|
|
30
|
+
truck: "M -10 -6 h11 v10 h-11 z M 1 -2 h5 l3.5 3.5 v2.5 h-8.5 M -7.5 6.5 a1.8 1.8 0 1 0 3.6 0 a1.8 1.8 0 1 0 -3.6 0 M 3 6.5 a1.8 1.8 0 1 0 3.6 0 a1.8 1.8 0 1 0 -3.6 0",
|
|
31
|
+
card: "M -9 -6.5 h18 a2 2 0 0 1 2 2 v9 a2 2 0 0 1 -2 2 h-18 a2 2 0 0 1 -2 -2 v-9 a2 2 0 0 1 2 -2 z M -11 -2 h22 M -7 3 h5",
|
|
32
|
+
cart: "M -10 -8 h3 l3 11 h11 l2.5 -8 h-15.5 M -4.5 7.5 a1.5 1.5 0 1 0 3 0 a1.5 1.5 0 1 0 -3 0 M 5 7.5 a1.5 1.5 0 1 0 3 0 a1.5 1.5 0 1 0 -3 0",
|
|
33
|
+
clock: "M -9 0 a9 9 0 1 0 18 0 a9 9 0 1 0 -18 0 M 0 -5 v5 l3.5 2.5",
|
|
34
|
+
branch: "M -4 -4.5 v9 M 5 0.5 c0 4 -9 3 -9 6.5 M -6.2 -7 a2.2 2.2 0 1 0 4.4 0 a2.2 2.2 0 1 0 -4.4 0 M -6.2 7 a2.2 2.2 0 1 0 4.4 0 a2.2 2.2 0 1 0 -4.4 0 M 2.8 -2 a2.2 2.2 0 1 0 4.4 0 a2.2 2.2 0 1 0 -4.4 0",
|
|
35
|
+
cloud: "M -7 6 h13 a4.5 4.5 0 0 0 0.5 -9 a6 6 0 0 0 -11.5 -1 a4.5 4.5 0 0 0 -2 10 z",
|
|
36
|
+
refresh: "M 7 -3 a7.5 7.5 0 1 0 0.5 5 M 7.5 -8 v5 h-5",
|
|
37
|
+
layers: "M 0 -8 l9 4.5 l-9 4.5 l-9 -4.5 z M -9 2.5 l9 4.5 l9 -4.5",
|
|
38
|
+
code: "M -4 -6 l-5 6 l5 6 M 4 -6 l5 6 l-5 6",
|
|
39
|
+
paint: "M -8 8 c 0 -3 2 -5 4.5 -5 l 9 -9 l 3.5 3.5 l -9 9 c 0 2.5 -2 4.5 -5 4.5 z",
|
|
40
|
+
block: "M -9 -7 h18 a2 2 0 0 1 2 2 v10 a2 2 0 0 1 -2 2 h-18 a2 2 0 0 1 -2 -2 v-10 a2 2 0 0 1 2 -2 z M -11 -2.5 h22 M -6 2.5 h8",
|
|
41
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 時刻の点の並びから CSS の keyframes を組む道具。見本帳の `engine.js` の `kf` / `vis` / `rise` / `pop` / `move` を写す。
|
|
3
|
+
*
|
|
4
|
+
* 動きは全て 1 周 `total` 秒の keyframes で、点の時刻は 1 周の中の割合 (%) に直す。
|
|
5
|
+
* 性質ごとに独立して補間されるので、点ごとに変える性質だけを書く。
|
|
6
|
+
*/
|
|
7
|
+
import { EASE, LOOK, MOTION } from "../look/values";
|
|
8
|
+
|
|
9
|
+
export type Props = Record<string, string | number>;
|
|
10
|
+
export type Frame = [time: number, props: Props, ease?: string];
|
|
11
|
+
|
|
12
|
+
const r2 = (v: number): number => Math.round(v * 100) / 100;
|
|
13
|
+
|
|
14
|
+
export class Motion {
|
|
15
|
+
readonly frames: string[] = [];
|
|
16
|
+
private count = 0;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly prefix: string,
|
|
20
|
+
readonly total: number,
|
|
21
|
+
readonly last: number,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
private pct(t: number): number {
|
|
25
|
+
return +((Math.min(Math.max(t, 0), this.total) / this.total) * 100).toFixed(3);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** keyframes を 1 つ足し、その名前を付ける style の字を返す。 */
|
|
29
|
+
kf(points: Frame[]): string {
|
|
30
|
+
const name = `${this.prefix}-${this.count++}`;
|
|
31
|
+
const at = new Map<number, { props: Props; ease?: string }>();
|
|
32
|
+
for (const [t, props, ease] of [...points].sort((a, b) => a[0] - b[0])) {
|
|
33
|
+
const p = this.pct(t);
|
|
34
|
+
const cur = at.get(p) ?? { props: {} };
|
|
35
|
+
Object.assign(cur.props, props);
|
|
36
|
+
if (ease) cur.ease = ease;
|
|
37
|
+
at.set(p, cur);
|
|
38
|
+
}
|
|
39
|
+
const body = [...at]
|
|
40
|
+
.map(([p, { props, ease }]) => {
|
|
41
|
+
const d = Object.entries(props).map(([k, v]) => `${k}:${v}`);
|
|
42
|
+
if (ease) d.push(`animation-timing-function:${ease}`);
|
|
43
|
+
return `${p}%{${d.join(";")}}`;
|
|
44
|
+
})
|
|
45
|
+
.join("");
|
|
46
|
+
this.frames.push(`@keyframes ${name}{${body}}`);
|
|
47
|
+
return `animation-name:${name}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** `a` で出て `b` から消える。`within` を立てると `b` までに消え終わる。 */
|
|
51
|
+
vis(a: number, b = this.last, o: { f?: number; on?: Props; off?: Props; ein?: string; within?: boolean } = {}): string {
|
|
52
|
+
const f = o.f ?? MOTION.fade;
|
|
53
|
+
const on = o.on ?? {};
|
|
54
|
+
const off = o.off ?? {};
|
|
55
|
+
const out = o.within ? Math.max(a + f, b - f) : b;
|
|
56
|
+
return this.kf([
|
|
57
|
+
[0, { opacity: 0, ...off }],
|
|
58
|
+
[a, { opacity: 0, ...off }, o.ein ?? EASE.out],
|
|
59
|
+
[a + f, { opacity: 1, ...on }],
|
|
60
|
+
[out, { opacity: 1, ...on }, EASE.inOut],
|
|
61
|
+
[Math.min(out + f, this.total), { opacity: 0, ...off }],
|
|
62
|
+
[this.total, { opacity: 0, ...off }],
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 少し下から浮いて出る。 */
|
|
67
|
+
rise(a: number, b = this.last, within = false): string {
|
|
68
|
+
return this.vis(a, b, { on: { transform: "translateY(0)" }, off: { transform: `translateY(${LOOK.rise}px)` }, within });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 小さな所から弾んで出る。 */
|
|
72
|
+
pop(a: number, b = this.last): string {
|
|
73
|
+
return this.vis(a, b, { f: MOTION.popFade, ein: EASE.pop, on: { transform: "scale(1)" }, off: { transform: `scale(${LOOK.popFrom})` } });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 初めから出ていて `b` で消える。 */
|
|
77
|
+
until(b: number): string {
|
|
78
|
+
const f = MOTION.untilFade;
|
|
79
|
+
return this.kf([
|
|
80
|
+
[0, { opacity: 1 }],
|
|
81
|
+
[b, { opacity: 1 }, EASE.inOut],
|
|
82
|
+
[b + f, { opacity: 0 }],
|
|
83
|
+
[this.last, { opacity: 0 }],
|
|
84
|
+
[this.total, { opacity: 1 }],
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 点を辿って動く。`[時刻, x, y, 大きさ, 次の点までの緩急]` の並び。 */
|
|
89
|
+
move(points: [number, number, number, number?, string?][], extra: Frame[] = []): string {
|
|
90
|
+
return this.kf(
|
|
91
|
+
points.map(([t, x, y, s = 1, ease]): Frame => [t, { transform: `translate(${r2(x)}px,${r2(y)}px) scale(${s})` }, ease]).concat(extra),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** `a` から `b` まで見え、`fade` 秒で消える。 */
|
|
96
|
+
blink(a: number, b: number, fade: number): Frame[] {
|
|
97
|
+
return [
|
|
98
|
+
[0, { opacity: 0 }],
|
|
99
|
+
[a - 0.01, { opacity: 0 }],
|
|
100
|
+
[a, { opacity: 1 }],
|
|
101
|
+
[b, { opacity: 1 }],
|
|
102
|
+
[b + fade, { opacity: 0 }],
|
|
103
|
+
[this.total, { opacity: 0 }],
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
}
|