chalk-talk 0.16.0 → 0.17.2
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 +1 -1
- package/assets/lottie/NOTICE.md +4 -3
- package/assets/player.html +3 -3
- package/embed-mermaid.js +130 -130
- package/embed.js +5 -5
- package/markdown.js +1 -1
- package/mount.global.js +400 -400
- package/mount.js +1 -1
- package/package.json +2 -1
- package/player.js +4 -4
- package/references/examples/caverna.json +128 -0
- package/references/examples/estoicismo.json +78 -0
- package/references/examples/fila.json +102 -0
- package/references/examples/juros.json +51 -0
- package/references/examples/pitagoras.json +64 -0
- package/references/examples/requisicao.json +86 -0
- package/references/hooks.md +16 -0
- package/references/molds.md +17 -0
- package/references/recipe.md +81 -0
- package/references/text.md +115 -0
- package/scripts/build.mjs +15 -3
- package/scripts/contract.mjs +48 -2
- package/scripts/mermaid.mjs +1 -1
- package/scripts/syntax.mjs +2 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "arquitetura-requisicao",
|
|
3
|
+
"title": "Arquitetura web ↔ da mesa à cozinha",
|
|
4
|
+
"category": "Sistemas",
|
|
5
|
+
"width": 900,
|
|
6
|
+
"height": 660,
|
|
7
|
+
"background": "plain",
|
|
8
|
+
"learning": {
|
|
9
|
+
"goal": "Acompanhar uma requisição e distinguir interface, regras do serviço e persistência.",
|
|
10
|
+
"question": "Se alguém esconder o botão de editar no navegador, os dados já estão protegidos? Onde a permissão deve ser conferida?",
|
|
11
|
+
"answer": "Não. O cliente pode ser modificado ou ignorado. O serviço precisa autenticar, autorizar e validar cada operação antes de acessar os dados. Esconder o botão só melhora a interface.",
|
|
12
|
+
"caveat": "O restaurante ajuda a separar responsabilidades, mas pessoas não são servidores. Este é um exemplo cliente–API–banco: sistemas reais podem ter caches, múltiplos serviços e outros fluxos. As setas indicam chamadas lógicas, não uma topologia de rede."
|
|
13
|
+
},
|
|
14
|
+
"steps": [
|
|
15
|
+
{ "type": "text", "chapter": "Um pedido na mesa", "at": [40, 59], "text": "clicou em salvar. por onde passa o pedido?", "size": 41, "duration": 850, "say": "Num restaurante, escolher no cardápio não faz a comida aparecer. Vamos acompanhar o pedido da mesa até a cozinha e o registro." },
|
|
16
|
+
{ "type": "line", "from": [45, 444], "to": [852, 444], "style": { "color": "#C6BAA5", "width": 2 }, "duration": 110 },
|
|
17
|
+
{ "type": "ellipse", "center": [171, 437], "rx": 117, "ry": 8, "style": { "color": "#E1D9CA", "fill": "#E1D9CA", "fillStyle": "solid", "width": 1 }, "duration": 100 },
|
|
18
|
+
{ "type": "polygon", "points": [[72, 348], [101, 348], [101, 405], [72, 405]], "style": { "color": "#9A8261", "fill": "#C5AD86", "fillStyle": "solid", "width": 1.4 }, "duration": 100 },
|
|
19
|
+
{ "type": "line", "from": [77, 403], "to": [77, 436], "style": { "color": "#9A8261", "width": 5 }, "duration": 80 },
|
|
20
|
+
{ "type": "line", "from": [97, 404], "to": [97, 436], "style": { "color": "#9A8261", "width": 5 }, "duration": 80 },
|
|
21
|
+
{ "type": "polygon", "points": [[100, 324], [120, 326], [132, 369], [101, 378], [92, 350]], "style": { "color": "#4D5663", "fill": "#8FA6AD", "fillStyle": "solid", "width": 1.5 }, "duration": 120 },
|
|
22
|
+
{ "type": "circle", "center": [107, 306], "r": 14, "style": { "color": "#4D5663", "fill": "#D8B99B", "fillStyle": "solid", "roughness": 0.7, "width": 1.5 }, "duration": 100 },
|
|
23
|
+
{ "type": "curve", "points": [[95, 303], [98, 292], [113, 293], [120, 300]], "style": { "color": "#4D5663", "width": 4 }, "duration": 80 },
|
|
24
|
+
{ "type": "polyline", "points": [[119, 373], [142, 393], [139, 429]], "style": { "color": "#4D5663", "width": 7 }, "duration": 100 },
|
|
25
|
+
{ "type": "polyline", "points": [[105, 376], [117, 402], [113, 431]], "style": { "color": "#4D5663", "width": 7 }, "duration": 100 },
|
|
26
|
+
{ "type": "polyline", "points": [[120, 335], [140, 347], [157, 326]], "style": { "color": "#4D5663", "width": 5 }, "duration": 100 },
|
|
27
|
+
{ "type": "line", "from": [168, 357], "to": [165, 436], "style": { "color": "#9A8261", "width": 5 }, "duration": 90 },
|
|
28
|
+
{ "type": "line", "from": [259, 357], "to": [267, 436], "style": { "color": "#9A8261", "width": 5 }, "duration": 90 },
|
|
29
|
+
{ "type": "ellipse", "center": [214, 349], "rx": 79, "ry": 20, "style": { "color": "#A28B68", "fill": "#D8C29C", "fillStyle": "solid", "width": 1.6 }, "duration": 140 },
|
|
30
|
+
{ "type": "ellipse", "center": [242, 346], "rx": 24, "ry": 8, "style": { "color": "#A8AAA0", "fill": "#F8F5E9", "fillStyle": "solid", "width": 1.2 }, "duration": 100 },
|
|
31
|
+
{ "type": "ellipse", "center": [242, 346], "rx": 17, "ry": 5, "style": { "color": "#C4C5B7", "width": 1 }, "duration": 70 },
|
|
32
|
+
{ "type": "polygon", "points": [[160, 287], [188, 298], [209, 287], [210, 334], [188, 344], [160, 331]], "style": { "color": "#667B7C", "fill": "#DCE6DC", "fillStyle": "solid", "width": 1.6 }, "duration": 140, "say": "O cardápio é a interface: mostra opções e recebe a intenção. A tela coleta o pedido, mas não decide sozinha se ele é permitido." },
|
|
33
|
+
{ "type": "line", "from": [188, 301], "to": [188, 339], "style": { "color": "#667B7C", "width": 1.3 }, "duration": 80 },
|
|
34
|
+
{ "type": "line", "from": [167, 307], "to": [181, 312], "style": { "color": "#8DA098", "width": 1.3 }, "duration": 70 },
|
|
35
|
+
{ "type": "line", "from": [167, 317], "to": [181, 322], "style": { "color": "#8DA098", "width": 1.3 }, "duration": 70 },
|
|
36
|
+
{ "type": "text", "at": [183, 487], "text": "interface: recebe a intenção", "anchor": "middle", "size": 24, "duration": 400, "style": { "color": "#46638B" } },
|
|
37
|
+
|
|
38
|
+
{ "type": "arrow", "chapter": "Na cozinha: conferir e agir", "from": [259, 245], "to": [359, 245], "style": { "color": "#46638B", "width": 2.4 }, "duration": 350, "say": "A requisição leva o pedido ao serviço. Na nossa cozinha, antes de preparar, é preciso conferir se o pedido é válido e se pode ser atendido." },
|
|
39
|
+
{ "type": "text", "at": [308, 221], "text": "pedido", "anchor": "middle", "size": 25, "duration": 300, "style": { "color": "#46638B" } },
|
|
40
|
+
{ "type": "polygon", "points": [[281, 256], [316, 256], [316, 281], [281, 281]], "style": { "color": "#46638B", "fill": "#EDF0E6", "fillStyle": "solid", "width": 1.3 }, "duration": 100 },
|
|
41
|
+
{ "type": "polyline", "points": [[282, 257], [299, 269], [315, 257]], "style": { "color": "#46638B", "width": 1.2 }, "duration": 100 },
|
|
42
|
+
{ "type": "polygon", "points": [[355, 354], [593, 354], [593, 433], [355, 433]], "style": { "color": "#8F9184", "fill": "#D1D1BF", "fillStyle": "solid", "width": 1.5 }, "duration": 130 },
|
|
43
|
+
{ "type": "polygon", "points": [[347, 346], [585, 346], [603, 358], [355, 365]], "style": { "color": "#8F9184", "fill": "#E8E6D7", "fillStyle": "solid", "width": 1.3 }, "duration": 100 },
|
|
44
|
+
{ "type": "line", "from": [479, 368], "to": [479, 431], "style": { "color": "#A3A494", "width": 1.2 }, "duration": 80 },
|
|
45
|
+
{ "type": "circle", "center": [465, 381], "r": 3, "style": { "color": "#8F9184", "fill": "#8F9184", "fillStyle": "solid" }, "duration": 70 },
|
|
46
|
+
{ "type": "circle", "center": [493, 381], "r": 3, "style": { "color": "#8F9184", "fill": "#8F9184", "fillStyle": "solid" }, "duration": 70 },
|
|
47
|
+
{ "type": "polygon", "points": [[409, 280], [439, 280], [454, 340], [398, 340]], "style": { "color": "#59616D", "fill": "#EEEBDD", "fillStyle": "solid", "width": 1.5 }, "duration": 130 },
|
|
48
|
+
{ "type": "circle", "center": [425, 262], "r": 15, "style": { "color": "#59616D", "fill": "#D8B99B", "fillStyle": "solid", "roughness": 0.7, "width": 1.5 }, "duration": 110 },
|
|
49
|
+
{ "type": "ellipse", "center": [411, 240], "rx": 11, "ry": 10, "style": { "color": "#A2A393", "fill": "#FAF7EC", "fillStyle": "solid", "width": 1.2 }, "duration": 100 },
|
|
50
|
+
{ "type": "ellipse", "center": [425, 234], "rx": 13, "ry": 13, "style": { "color": "#A2A393", "fill": "#FAF7EC", "fillStyle": "solid", "width": 1.2 }, "duration": 100 },
|
|
51
|
+
{ "type": "ellipse", "center": [439, 240], "rx": 11, "ry": 10, "style": { "color": "#A2A393", "fill": "#FAF7EC", "fillStyle": "solid", "width": 1.2 }, "duration": 100 },
|
|
52
|
+
{ "type": "polygon", "points": [[411, 243], [439, 243], [438, 252], [411, 252]], "style": { "color": "#A2A393", "fill": "#FAF7EC", "fillStyle": "solid", "width": 1.1 }, "duration": 90 },
|
|
53
|
+
{ "type": "polygon", "points": [[414, 297], [436, 297], [441, 336], [408, 336]], "style": { "color": "#A2A393", "fill": "#DCE1CF", "fillStyle": "solid", "width": 1.1 }, "duration": 100 },
|
|
54
|
+
{ "type": "polyline", "points": [[440, 290], [467, 318], [495, 315]], "style": { "color": "#59616D", "width": 5 }, "duration": 120 },
|
|
55
|
+
{ "type": "ellipse", "center": [541, 342], "rx": 32, "ry": 8, "style": { "color": "#677571", "fill": "#B9C2B2", "fillStyle": "solid", "width": 1.4 }, "duration": 110 },
|
|
56
|
+
{ "type": "polygon", "points": [[510, 318], [572, 318], [565, 342], [517, 342]], "style": { "color": "#677571", "fill": "#BAC3B5", "fillStyle": "solid", "width": 1.4 }, "duration": 110 },
|
|
57
|
+
{ "type": "ellipse", "center": [541, 316], "rx": 32, "ry": 7, "style": { "color": "#677571", "fill": "#E7E7D7", "fillStyle": "solid", "width": 1.2 }, "duration": 100 },
|
|
58
|
+
{ "type": "line", "from": [571, 324], "to": [588, 318], "style": { "color": "#677571", "width": 4 }, "duration": 90 },
|
|
59
|
+
{ "type": "curve", "points": [[529, 300], [523, 288], [529, 277]], "style": { "color": "#A1A89A", "width": 1.6 }, "duration": 110 },
|
|
60
|
+
{ "type": "curve", "points": [[547, 297], [553, 285], [548, 272]], "style": { "color": "#A1A89A", "width": 1.6 }, "duration": 110 },
|
|
61
|
+
{ "type": "polyline", "points": [[399, 411], [411, 422], [435, 396]], "style": { "color": "#617C61", "width": 4 }, "duration": 180, "say": "No sistema real, o serviço autentica, autoriza, valida os dados e aplica as regras. Só esconder uma opção na tela não protege a operação." },
|
|
62
|
+
{ "type": "text", "at": [477, 487], "text": "serviço: confere e executa", "anchor": "middle", "size": 24, "duration": 400 },
|
|
63
|
+
|
|
64
|
+
{ "type": "arrow", "chapter": "O registro que permanece", "from": [610, 245], "to": [688, 245], "style": { "color": "#46638B", "width": 2.4 }, "duration": 300, "say": "O serviço consulta ou grava o estado. O livro representa o banco de dados: o registro precisa continuar existindo depois que a conversa acaba." },
|
|
65
|
+
{ "type": "text", "at": [649, 221], "text": "gravar", "anchor": "middle", "size": 25, "duration": 300, "style": { "color": "#46638B" } },
|
|
66
|
+
{ "type": "ellipse", "center": [762, 429], "rx": 83, "ry": 9, "style": { "color": "#E1D9CA", "fill": "#E1D9CA", "fillStyle": "solid", "width": 1 }, "duration": 80 },
|
|
67
|
+
{ "type": "line", "from": [704, 372], "to": [699, 430], "style": { "color": "#9A8261", "width": 5 }, "duration": 90 },
|
|
68
|
+
{ "type": "line", "from": [824, 372], "to": [830, 430], "style": { "color": "#9A8261", "width": 5 }, "duration": 90 },
|
|
69
|
+
{ "type": "polygon", "points": [[686, 361], [826, 361], [841, 375], [693, 380]], "style": { "color": "#A28B68", "fill": "#D8C29C", "fillStyle": "solid", "width": 1.3 }, "duration": 110 },
|
|
70
|
+
{ "type": "polygon", "points": [[698, 307], [761, 320], [824, 306], [824, 355], [761, 368], [698, 354]], "style": { "color": "#7C8C72", "fill": "#AAB795", "fillStyle": "solid", "width": 1.5 }, "duration": 130 },
|
|
71
|
+
{ "type": "polygon", "points": [[701, 298], [761, 310], [821, 297], [821, 346], [761, 359], [701, 345]], "style": { "color": "#B7B6A2", "fill": "#F6F0DB", "fillStyle": "solid", "width": 1.1 }, "duration": 120 },
|
|
72
|
+
{ "type": "line", "from": [761, 312], "to": [761, 354], "style": { "color": "#B7B6A2", "width": 1.2 }, "duration": 100 },
|
|
73
|
+
{ "type": "line", "from": [710, 314], "to": [749, 322], "style": { "color": "#A2A38D", "width": 1.3 }, "duration": 90 },
|
|
74
|
+
{ "type": "line", "from": [710, 325], "to": [749, 333], "style": { "color": "#A2A38D", "width": 1.3 }, "duration": 90 },
|
|
75
|
+
{ "type": "line", "from": [774, 322], "to": [811, 314], "style": { "color": "#A2A38D", "width": 1.3 }, "duration": 90 },
|
|
76
|
+
{ "type": "line", "from": [774, 334], "to": [811, 326], "style": { "color": "#A2A38D", "width": 1.3 }, "duration": 90 },
|
|
77
|
+
{ "type": "polygon", "points": [[783, 302], [791, 300], [791, 350], [787, 346], [783, 352]], "style": { "color": "#B78E65", "fill": "#CDA77A", "fillStyle": "solid", "width": 1 }, "duration": 100 },
|
|
78
|
+
{ "type": "text", "at": [759, 487], "text": "banco: guarda o estado", "anchor": "middle", "size": 24, "duration": 400, "style": { "color": "#617C61" } },
|
|
79
|
+
|
|
80
|
+
{ "type": "arrow", "chapter": "A resposta volta pelo serviço", "from": [744, 549], "to": [502, 549], "style": { "color": "#617C61", "width": 2.5 }, "duration": 350 },
|
|
81
|
+
{ "type": "arrow", "from": [451, 549], "to": [192, 549], "style": { "color": "#617C61", "width": 2.5 }, "duration": 350, "say": "O resultado volta pelo serviço até a interface. Pode ser uma confirmação ou uma falha. A tela não deve prometer sucesso antes da resposta." },
|
|
82
|
+
{ "type": "text", "at": [450, 528], "text": "resposta", "anchor": "middle", "size": 25, "duration": 300, "style": { "color": "#617C61" } },
|
|
83
|
+
{ "type": "highlight", "from": [90, 611], "to": [808, 611], "thickness": 24, "duration": 250 },
|
|
84
|
+
{ "type": "text", "at": [450, 619], "text": "a tela pede. o serviço decide. o banco guarda.", "anchor": "middle", "size": 35, "duration": 700 }
|
|
85
|
+
]
|
|
86
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!-- GENERATED by tools/assets.mjs. Do not edit. -->
|
|
2
|
+
# Choosing the hook
|
|
3
|
+
|
|
4
|
+
The hook is the thing of everyday life the idea is tied to. It is the first thing drawn and the question the drawing
|
|
5
|
+
opens with.
|
|
6
|
+
|
|
7
|
+
A good hook:
|
|
8
|
+
- is something the audience has seen or done (a card bill, a bus stop, a café queue), not something they have to imagine;
|
|
9
|
+
- already holds the idea: taking it apart reveals the idea without forcing it;
|
|
10
|
+
- can be drawn with few shapes (a person, a house, a cart, a table);
|
|
11
|
+
- has a number in it only when the idea is about a number.
|
|
12
|
+
|
|
13
|
+
Offer up to three, best first, each with one line saying why it works for this audience. Avoid:
|
|
14
|
+
- hooks that need a fact the person may not know to make sense;
|
|
15
|
+
- hooks tied to one country, brand or person (unless the person asked);
|
|
16
|
+
- sad or delicate situations (illness, debt shame, accidents) unless the person brought them.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!-- GENERATED by tools/assets.mjs. Do not edit. -->
|
|
2
|
+
# The six molds
|
|
3
|
+
|
|
4
|
+
Choose one before the outline and say which. The mold decides what the middle of the drawing is.
|
|
5
|
+
|
|
6
|
+
| Mold | When it fits | The middle of the drawing | A value to move? |
|
|
7
|
+
|---|---|---|---|
|
|
8
|
+
| Measure and formula | A quantity that follows a rule (interest, Pythagoras, speed) | The real thing measured, then the rule drawn over it | Yes: the quantity |
|
|
9
|
+
| Thought experiment | "What if…" ideas (Plato's cave, the trolley) | The situation, then the twist that shows the idea | Rarely |
|
|
10
|
+
| Argument | A claim with reasons and an objection (stoicism, a policy) | Premises → conclusion, the objection beside it | No |
|
|
11
|
+
| System | Parts that pass something along (a web request, a queue) | One request followed end to end, failures included | Sometimes: load, capacity |
|
|
12
|
+
| Timeline | Things that happen in order (a process, a history) | The line with the moments that matter | Rarely |
|
|
13
|
+
| Cycle | Something that comes back (water, a feedback loop) | The loop, and what speeds it up or slows it down | Sometimes |
|
|
14
|
+
|
|
15
|
+
Examples of each, already drawn, in references/examples/: the recipes (juros-fatura and pitagoras-telhado are measure and formula;
|
|
16
|
+
platao-caverna is a thought experiment; estoicismo-onibus is an argument; arquitetura-requisicao and arquitetura-fila
|
|
17
|
+
are systems).
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<!-- GENERATED by tools/assets.mjs. Do not edit. -->
|
|
2
|
+
# Writing the recipe
|
|
3
|
+
|
|
4
|
+
You write hand-drawn visual explanations. The player draws them, stroke by stroke, with a pencil following the line, from a **recipe** in JSON. Your job is to write that recipe for the topic the person asked. Every text the person reads (title, texts on the drawing, narration, learning, labels) is in the person's language, conversational, short and without jargon. Facts and numbers must be correct.
|
|
5
|
+
|
|
6
|
+
## The shape of every scene
|
|
7
|
+
|
|
8
|
+
1. **A real-world hook**: a question about something concrete ("why is the roof tile this long?").
|
|
9
|
+
2. **Take it apart**: draw the real thing simply and reveal, step by step, the shape or pattern hidden in it.
|
|
10
|
+
3. **The idea**: a relation, a distinction, an argument, a flow or a formula. Do not force numbers where they do not help.
|
|
11
|
+
4. **The close**: a highlight (a circle, an arrow, the yellow marker) on the result, tying it back to the real thing.
|
|
12
|
+
|
|
13
|
+
Put `chapter` (a short title) on the first step of each of these four parts. `say` is one short sentence of narration when an idea changes; do not narrate every detail.
|
|
14
|
+
|
|
15
|
+
## Beyond maths
|
|
16
|
+
|
|
17
|
+
- **Philosophy**: start from a lived situation, make the question explicit, compare ideas and give a situated reading (author and work, when it applies). Say in the narration what an arrow means ("produces", "contrasts", "leads to ask"). Never give percentages to beliefs nor treat an analogy as proof. Include an objection or a limit.
|
|
18
|
+
- **Systems architecture**: give a concrete scenario, separate responsibilities and follow a request or message. Name the parts and say what each arrow means, including answers and failures. Say what was simplified. Never promise exactly-once delivery, global order or endless queue capacity.
|
|
19
|
+
- Controls (`params`) only when there is a real quantity and a checkable relation; a scene with no `params` is fine. Prefer drawing the idea over boxes of text: use `card` only for a concept that really is a labelled box.
|
|
20
|
+
- `theme`: "chalkboard" draws in chalk on a green board (good for a class); "paper" by default.
|
|
21
|
+
- `defs` (optional): figures the recipe makes itself, when no prop is the thing — {"name": {"shapes": [...]}}, up to 8, each up to 24 shapes. A shape is {"kind": "polygon" | "polyline" | "curve", "points": [[x, y], ...], "fill": "#hex"} or {"kind": "circle", "c": [x, y], "r": n, "fill": ...}, on a grid of **whole numbers from 0 to 100**, y down like the paper; the figure stands at 50,100 (its bottom centre). The name (lowercase, not a prop's or a step's) is then used as a prop. E.g. {"xicara": {"shapes": [{"kind": "polygon", "points": [[24,48],[76,48],[70,90],[30,90]], "fill": "#FFFFFF"}, {"kind": "curve", "points": [[74,56],[90,62],[72,80]]}]}}.
|
|
22
|
+
- `kind` (optional): what the drawing is for — `lesson` (the default; what you write here), or `pitch`, `arch`, `decision`, `data`, `timeline`, `story` (back-of-napkin's drawings). `source` (optional): where the numbers come from, expected in a `data` drawing.
|
|
23
|
+
- `quiz` (optional, for a class; only a `lesson` has one): {"question", "options": [{"text", "right": true or false, "why"}]} — 2 to 4 options, exactly one right, each `why` one short sentence. It appears when the last chapter is drawn.
|
|
24
|
+
- Always include `learning`: `goal` (what the person will understand), `question` (a new situation to think through with the idea), `answer` (one grounded possibility and its reasoning, not a correction of opinion) and `caveat` (where the analogy stops working, what was simplified, the reference when there is one).
|
|
25
|
+
|
|
26
|
+
## Format
|
|
27
|
+
|
|
28
|
+
{"id": "slug", "title": "Idea ↔ thing of the world", "width": 900, "height": 560, "background": "dots" | "grid" | "plain", "category": "the area, in the person's language", "theme": "paper" | "chalkboard", "locale": "the tag of the texts' language, e.g. pt-BR or en-US", "learning": {"goal": "...", "question": "...", "answer": "...", "caveat": "..."}, "params": {"a": {"label": "...", "min": 1, "max": 6, "step": 1, "value": 3}}, "switches": {"atraso": {"label": "...", "on": false}}, "vars": {"k": 45, "c": "sqrt(a^2 + b^2)"}, "defs": {...}, "steps": [...]}
|
|
29
|
+
|
|
30
|
+
`locale` is the language tag of the texts ("pt-BR", "en-US", "es-ES"…): numbers shown in texts and counters are written its way (3,5 or 3.5). Coordinates have their origin at the top left. Coordinates, radii, card sizes and plot fields may be **expressions as text** over params and vars: `+ - * / ^ ( )`, the functions sqrt abs sin cos tan min max round floor ceil pow log exp and the constants pi and e. Use them so the drawing redraws when a control moves. Texts take `{expr}` and `{expr:digits}` to show a value. `vars` are computed in order. Names are letters, digits and `_`. Conditions: `> < >= <= == !=`, `and`, `or`, `not` (true is 1, false is 0). `switches` are yes/no the person toggles beside the values; in expressions they are 1 or 0.
|
|
31
|
+
|
|
32
|
+
Step types (every step also takes `style`, `say`, `chapter`, `duration`, `id`; a step the person moved on the paper carries `nudge`: [dx, dy], added to its place after the expressions — keep it as it is when you fix a recipe):
|
|
33
|
+
- `card`: `at` (top-left corner), `w`, `h`, `title`, `lines` (up to 6 short texts). The title is size 30 and lines 22; keep `h` at least 70 + 28 per line and 24 px of side room; there is no line wrapping.
|
|
34
|
+
- `line` and `arrow`: `from`, `to`.
|
|
35
|
+
- `polyline` (open), `polygon` (closed, 3 points or more), `curve` (smooth through the points): `points`.
|
|
36
|
+
- `circle`: `center`, `r`. `ellipse`: `center`, `rx`, `ry` (good to circle a result).
|
|
37
|
+
- `plot`: `fn` (an expression of `var`, x by default), `domain` [from, to], `range` [low, high], `box` {x, y, w, h}, `samples`.
|
|
38
|
+
- `text`: `at`, `text`, `size`, `font` ("hand" or "body"), `anchor` ("start", "middle", "end"), `rotate`.
|
|
39
|
+
- `highlight`: `from`, `to`, `thickness` — a marker stroke behind the ink.
|
|
40
|
+
- `pause`: `ms` — a breath between ideas. With `ask` (a short question), the lesson stops on it until the person asks to see the rest: put it, with the `chapter`, as the first step of the part that answers it.
|
|
41
|
+
- `prop`: `name`, `at` (its bottom centre, where it stands), `size` (its box, 80 by default), `label` (text under it), `flip` (mirror). Names: person, person-sitting, person-pointing, house, car, bus, coin, phone, document, server, bank, cloud, queue, desk, computer, clock, lock, money-arrow, tree, school, hospital, factory, truck, cart, safe, bulb, gear, database, robot; logistics: ship, container, warehouse, box, pallet, shop, shelf, field, cup; cloud: function, api-gateway, cache, cluster; school: board, book, pencil. A person can show a `mood`: happy, sad, surprised, worried or idea (a lit bulb above the head). E.g. {"type":"prop","name":"person","at":[150,300],"label":"Ana","id":"ana"}.
|
|
42
|
+
- `repeat`: `times` (number or expression, up to 50) and one `step`; `i` (0 to n-1) is a value in that step. E.g. {"type":"repeat","times":"n","step":{"type":"prop","name":"coin","at":["100 + i*50",400],"size":40}}. Inside, an `id` x becomes x-0, x-1…
|
|
43
|
+
- `when`: `if` (a condition) and `steps`, drawn only when it is true. E.g. {"type":"when","if":"atraso","steps":[...]}. Steps inside repeat and when take no `say`, `chapter`, `duration` nor `pause`.
|
|
44
|
+
- `move`: `target` (an earlier `id`), `to`, `along` ("line" or "curve"), `ms`: the element walks until its `at` (or `center`, or `from`) is at `to`. E.g. {"type":"move","target":"pedido","to":[600,200],"along":"curve"}.
|
|
45
|
+
- `animate`: `target` (an earlier `id`) and `effect` — spin, drive, steam (wisps over it), talk, pulse, float, or stop — and `speed` ("slow" or "fast"): the element keeps moving in place from this step on (only a prop's drawing, not its label). Use it sparingly, where motion says something (the truck leaves, the gear turns, steam over the cup); one or two per drawing. E.g. {"type":"animate","target":"caminhao","effect":"drive"}.
|
|
46
|
+
- `link`: `from` and `to` (earlier `id`s), `label`: an arrow from the edge of one to the edge of the other, no coordinates. E.g. {"type":"link","from":"ana","to":"banco","label":"pede"}.
|
|
47
|
+
- `counter`: `at`, `value` (expression), `format` with `{v}` (e.g. "R$ {v:0}"), `size`, `anchor`: the number counts from 0 while its beat is drawn.
|
|
48
|
+
|
|
49
|
+
Placing without coordinates (prefer these to working out positions by hand):
|
|
50
|
+
- `place` on a `text`, `prop`, `card`, `counter`, `circle` or `ellipse`: {"to": an earlier `id`, "side": "below" | "above" | "left" | "right", "gap": 24, "align": "center" | "start" | "end"}. The step's box sits on that side of the other element; its `at`/`center` can then be left out. E.g. {"type":"text","text":"asks","place":{"to":"ana","side":"right","gap":16}}.
|
|
51
|
+
- `chain`: `at` (the top-left corner) and `steps` — 2 to 12 props, texts, cards, counters, circles or ellipses (give them an `id` and a `label`) — in a line spread over the paper, each joined to the next by an arrow, no coordinates. `forward` and `back` add a long arrow each way under the line, with that label (a flow down, and what comes back up); `gap` ("auto" or a number), `direction` ("right" or "down"), `wrap` (more lines when they do not fit), `size` (of the props). Use it whenever things pass something along in order (a supply chain, a request through systems). E.g. {"type":"chain","at":[40,190],"forward":"o café desce","back":"pedidos sobem","steps":[{"type":"prop","name":"field","id":"fazenda","label":"fazenda"},{"type":"prop","name":"cup","id":"cafe","label":"cafeteria"}]}.
|
|
52
|
+
- `row` and `column`: `at` (the top-left corner), `gap`, `align` and `steps` — texts, props, cards, counters, circles, ellipses or a `repeat` of one — set side by side or one under the other, measured; the steps inside need no coordinates. `grid`: the same with `columns`. E.g. {"type":"row","at":[480,120],"gap":14,"steps":[{"type":"repeat","times":"n","step":{"type":"prop","name":"coin","size":44}}]}.
|
|
53
|
+
- `axis`: `from`, `to`, `domain` [low, high], `ticks` (how many parts, or a list of values), `label`, `format` with `{v}`: an arrow with marks and their numbers, written the recipe's `locale` way.
|
|
54
|
+
- `timeline`: `from`, `to`, `domain` [first, last], `marks` [{"at", "label" above, "text" under}]: a line of time with each mark in its place.
|
|
55
|
+
Steps inside `row`, `column` and `grid` take no `say`, `chapter`, `duration` nor `place`.
|
|
56
|
+
|
|
57
|
+
More pieces:
|
|
58
|
+
- `balloon`: `to` (the `id` of who speaks), `text` (under 300 characters, wrapped), `side` (above by default; another side is used when it does not fit), `kind` ("speech" or "thought"), `width` (240 by default). Prefer it to a caption when a person says or thinks something.
|
|
59
|
+
- `brace`: `from`, `to`, `label`: a curly brace grouping what lies along it, bulging under a line that runs across (left of one that runs up), the other way with `flip`.
|
|
60
|
+
- `note`: `at`, `text`, and `to` (an `id`) or `point`: a handwritten blue note with a curved arrow — «look here».
|
|
61
|
+
- `table`: `at`, `rows` (up to 12 rows of up to 6 short cells; the first is the header unless `header` is false; cells take `{expr}`), `size`.
|
|
62
|
+
- `bars`: `box` {x, y, w, h}, `values` (numbers or expressions, up to 12), `labels`, `max`, `format` with `{v}`: bars with their value on top and label under.
|
|
63
|
+
|
|
64
|
+
`style`: `color`, `width`, `roughness` (0 ruler, 1.6 default, 3 scribble), `bowing`, `fill`, `fillStyle` ("hachure", "solid", "zigzag", "cross-hatch", "dots"), `dashed` (true or false), `opacity` (0 to 1).
|
|
65
|
+
|
|
66
|
+
The player groups steps into beats: each `say`, `chapter` or `pause` starts one, and the steps after it appear together. `duration` on a beat's first step suggests its pace, 1000 to 1600 ms. Keep the whole scene under 15 seconds.
|
|
67
|
+
|
|
68
|
+
## Rules of good drawing
|
|
69
|
+
|
|
70
|
+
- Palette: graphite #2B2B2B (default), pen blue #2F5BEA, correction red #C8442C, green #2E8F62, pencil grey #8C8578, marker #FFE45C. At most 3 accent colours.
|
|
71
|
+
- Title text at the top (size 40 to 44). Labels 26 to 34. Nothing under 22.
|
|
72
|
+
- Leave room: a text, prop, card or counter that would leave the paper is brought back in, and a text that would cover another moves a little, but plan the space anyway: nothing closer than 30 px to the edge; texts never touch lines; nothing leaves the drawing at the controls' extreme values.
|
|
73
|
+
- 8 to 40 steps, read from top to bottom, from the real thing to the idea. Draw the real thing recognisably: use a `prop` for people and everyday things instead of drawing them by hand, and give it an `id` when an arrow or a move needs it.
|
|
74
|
+
- Numbers are literals in `size`, `duration`, `ms`, `rotate`, `samples`, `params` and `style`. Colours are hex or plain names; never URLs, HTML, scripts or links. Text is text, not markup.
|
|
75
|
+
|
|
76
|
+
An example (compound interest and a credit card bill):
|
|
77
|
+
{"id":"juros-fatura","title":"Juros compostos ↔ a fatura","category":"Matemática","width":900,"height":560,"background":"grid","params":{"taxa":{"label":"juros do rotativo (% ao mês)","min":1,"max":15,"step":1,"value":12}},"vars":{"r":"taxa / 100","bx":90,"by":130,"bw":720,"bh":340,"ymax":6000,"final":"1000 * (1 + r)^12","simples":"1000 * (1 + r*12)","ex":"bx + bw","ey":"by + bh - min(final, ymax) / ymax * bh"},"steps":[{"type":"text","at":[40,72],"text":"por que a dívida do rotativo dispara?","size":42,"say":"Você deixou R$ 1.000 no rotativo do cartão. O que acontece em um ano?"},{"type":"polyline","points":[["bx","by - 10"],["bx","by + bh"],["bx + bw + 10","by + bh"]],"style":{"width":2.4}},{"type":"text","at":["bx + bw","by + bh + 38"],"text":"meses →","anchor":"end","size":26,"style":{"color":"#5E5A52"}},{"type":"text","at":["bx - 10","by - 18"],"text":"R$","anchor":"middle","size":26,"style":{"color":"#5E5A52"}},{"type":"plot","fn":"1000 * (1 + r*x)","domain":[0,12],"range":[0,"ymax"],"box":{"x":"bx","y":"by","w":"bw","h":"bh"},"style":{"color":"#8C8578","width":2.4,"dashed":true},"say":"Se os juros fossem simples, a dívida subiria em linha reta."},{"type":"text","at":["ex - 8","by + bh - min(simples, ymax)/ymax*bh + 34"],"text":"simples: R$ {simples:0}","anchor":"end","size":26,"style":{"color":"#5E5A52"}},{"type":"pause","ms":400},{"type":"plot","fn":"1000 * (1 + r)^x","domain":[0,12],"range":[0,"ymax"],"box":{"x":"bx","y":"by","w":"bw","h":"bh"},"style":{"color":"#C8442C","width":3.4},"say":"Mas no rotativo o juro cobra juro do juro. A curva entorta para cima."},{"type":"arrow","from":["ex - 230","ey - 48"],"to":["ex - 10","ey - 6"],"style":{"width":2.4}},{"type":"text","at":["ex - 240","ey - 56"],"text":"12 meses: R$ {final:0}","anchor":"end","size":36,"style":{"color":"#C8442C"},"say":"Em doze meses, os mil reais viram isso. Mexa na taxa e veja a curva mudar."}]}
|
|
78
|
+
|
|
79
|
+
When a previous recipe and a problem are given, fix that recipe: keep what is right and change only what the problem asks.
|
|
80
|
+
|
|
81
|
+
Save the recipe as a .json file and run scripts/build.mjs on it.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Writing the recipe as text (.chalk)
|
|
2
|
+
|
|
3
|
+
The same recipe as `references/recipe.md`, one step per line. `scripts/build.mjs` reads it (and says the line of any
|
|
4
|
+
mistake). Save it as `<id>.chalk`. What each step draws, the palette and the rules of good drawing are in
|
|
5
|
+
`references/recipe.md`; this page is only how to write them.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
title Juntar moedas ↔ o cofrinho
|
|
9
|
+
id coin-jar
|
|
10
|
+
theme chalkboard
|
|
11
|
+
locale pt-BR
|
|
12
|
+
param n "moedas por mês" 1..8 = 3
|
|
13
|
+
|
|
14
|
+
# O gancho
|
|
15
|
+
> A Ana guarda moedas todo mês.
|
|
16
|
+
person ana at 150,330 "Ana" mood=idea
|
|
17
|
+
text "quer poupar" right-of ana gap 16 align=start
|
|
18
|
+
|
|
19
|
+
# Desmontar
|
|
20
|
+
> Cada mês, n moedas vão para o banco.
|
|
21
|
+
row at 470,150 gap 14
|
|
22
|
+
repeat n: coin size 44
|
|
23
|
+
bank banco at 740,450 "banco"
|
|
24
|
+
ana -> banco "deposita"
|
|
25
|
+
|
|
26
|
+
# A ideia
|
|
27
|
+
pause ask "E em um ano, quanto junta?"
|
|
28
|
+
counter at 470,270 "12*n" "{v:0} moedas num ano"
|
|
29
|
+
|
|
30
|
+
learning goal "…"
|
|
31
|
+
learning question "…"
|
|
32
|
+
learning answer "…"
|
|
33
|
+
learning caveat "…"
|
|
34
|
+
|
|
35
|
+
quiz "Se n dobra, o total do ano…"
|
|
36
|
+
* dobra -- 12 × n cresce em linha reta
|
|
37
|
+
- quadruplica -- não há n² aqui
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## The rules
|
|
41
|
+
|
|
42
|
+
- **Header lines** (anywhere, usually on top): `title …`, `id …`, `category …`, `theme paper|chalkboard`,
|
|
43
|
+
`locale en-US`, `background dots|grid|plain`, `size 900x560`, `param name "label" min..max [step=1] = value`,
|
|
44
|
+
`switch name "label" on|off`, `var name = value`, `learning goal|question|answer|caveat "…"`.
|
|
45
|
+
- `# Title` opens a chapter and `> sentence` is the narration: both go to the **next step**.
|
|
46
|
+
- **Numbers or expressions:** a number as it is; an expression in quotes (`"12*n"`, `"x0 + b*k",300`); a bare name
|
|
47
|
+
(`n`) is a value too. A point is `x,y` (no space).
|
|
48
|
+
- **Attributes** after a step: `key=value` (`size=44`, `color=blue`, `id=box`, `stroke=3`, `opacity=0.5`,
|
|
49
|
+
`nudge=4,-2`) or the flags `dashed`, `flip`. `gap`, `size`, `r`, `rx`, `ry`, `w`, `h`, `columns`, `ms` and `thickness`
|
|
50
|
+
can also go with a space (`size 44`).
|
|
51
|
+
- **Placing** instead of `at x,y`: `right-of <id>`, `left-of`, `above`, `below`, then `gap 16` and `align=start|end`.
|
|
52
|
+
- **Indented lines** are the steps inside `row`, `column`, `grid` and `when`, the marks of a `timeline`, the rows of a
|
|
53
|
+
`table` and the options of a `quiz`.
|
|
54
|
+
|
|
55
|
+
## Every step
|
|
56
|
+
|
|
57
|
+
| Text | Step |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `person ana at 150,330 "Ana" mood=idea` | a prop (any prop name), with an optional id and label |
|
|
60
|
+
| `text "…" at 40,60 size=40 anchor=middle` | text |
|
|
61
|
+
| `card at 10,10 w=220 h=130 "Title" "line 1" "line 2"` | card |
|
|
62
|
+
| `arrow 100,200 -> 300,200` · `line …` · `highlight … thickness=18` | from → to |
|
|
63
|
+
| `polyline 0,0 10,0 10,10` · `polygon …` · `curve …` | points |
|
|
64
|
+
| `circle at 100,100 r=40` · `ellipse at 100,100 rx=60 ry=30` | circle, ellipse |
|
|
65
|
+
| `plot "x^2" domain 0..10 range 0..100 box 50,50,300,200` | a curve of a formula |
|
|
66
|
+
| `pause` · `pause 400` · `pause ask "E agora?"` | a breath; with `ask`, the lesson waits for «Show me» |
|
|
67
|
+
| `row at 470,150 gap 14` · `column …` · `grid at 300,300 columns=3` | + indented steps |
|
|
68
|
+
| `repeat n: coin size 44` | one step repeated (`i` counts from 0) |
|
|
69
|
+
| `when atraso:` | + indented steps drawn when the condition holds |
|
|
70
|
+
| `ana -> banco "deposita"` | an arrow between two ids |
|
|
71
|
+
| `ana says "posso?"` · `ana thinks "hmm"` | a balloon (`side=right`, `width=200`) |
|
|
72
|
+
| `move ana to 300,200 along=curve` | the element walks there |
|
|
73
|
+
| `counter at 470,270 "12*n" "{v:0} moedas"` | a number that counts up |
|
|
74
|
+
| `axis 80,470 -> 420,470 domain 0..10 ticks=5 label="anos"` | axis |
|
|
75
|
+
| `timeline 100,400 -> 800,400 domain 1440..1520` + `mark 1450 "1450" "the press"` | timeline |
|
|
76
|
+
| `brace 60,560 -> 290,560 "two years"` | curly brace |
|
|
77
|
+
| `note at 500,100 "look here" -> ana` | a note with a curved arrow (or `-> x,y`) |
|
|
78
|
+
| `table at 60,380` + `\| a \| b \|` lines (`noheader`) | table |
|
|
79
|
+
| `bars box 400,400,300,200 values 1,2,"3*n" labels a,b,c format="{v}%"` | bars |
|
|
80
|
+
| `json {"type": …}` | any step as JSON, for anything the text has no short form for |
|
|
81
|
+
|
|
82
|
+
## A Mermaid diagram
|
|
83
|
+
|
|
84
|
+
**Any Mermaid diagram** (flowchart, sequence, class, state, ER, pie, timeline, journey, gantt, xychart, quadrant,
|
|
85
|
+
mindmap, kanban, gitGraph, sankey, block, packet, treemap, radar, requirement, C4, architecture, ishikawa, venn,
|
|
86
|
+
treeView, cynefin, eventmodeling, railroad, swimlanes, wardley — every kind of Mermaid 11.17 but zenuml) is also a recipe: save it as `<id>.mmd` (or put it in a
|
|
87
|
+
```mermaid block of a `.md`) and build it. Add `%% title …`, `%% # Chapter` and `%% > narration` lines — comments for
|
|
88
|
+
Mermaid, so the diagram still shows as usual on GitHub. A chapter starts at the arrow (message, relation) after it. The
|
|
89
|
+
drawing follows the order of the arrows, each box before the first arrow that reaches it; a sequence goes top to bottom.
|
|
90
|
+
Node shapes are kept (`([…])`, `[(…)]`, `{{…}}`, `[/…/]`, `>…]`, `[[…]]`, `(((…)))`, `@{ shape: … }`). In the other
|
|
91
|
+
kinds a `%% #` goes before a section, period, task, slice, series, point, branch, column or git command; with none,
|
|
92
|
+
each section, column, branch or git branch is a chapter of its own. A kind it does not draw yet makes the build say
|
|
93
|
+
which ones it draws.
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
flowchart LR
|
|
97
|
+
%% title Um café, do pedido à xícara
|
|
98
|
+
%% # O pedido
|
|
99
|
+
%% > Você pede um café no balcão.
|
|
100
|
+
A([Você]) -->|pede| B[Caixa]
|
|
101
|
+
%% # A cozinha
|
|
102
|
+
%% > O caixa passa a comanda para o barista.
|
|
103
|
+
B -->|comanda| C[Barista]
|
|
104
|
+
C -->|café pronto| A
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
sequenceDiagram
|
|
109
|
+
%% title Um café no balcão
|
|
110
|
+
%% # O pedido
|
|
111
|
+
%% > Você pede um café.
|
|
112
|
+
Você->>Caixa: um café, por favor
|
|
113
|
+
%% # A conta
|
|
114
|
+
Caixa-->>Você: são cinco reais
|
|
115
|
+
```
|
package/scripts/build.mjs
CHANGED
|
@@ -22,7 +22,7 @@ import { basename, dirname, join, resolve } from 'node:path'
|
|
|
22
22
|
import { fileURLToPath } from 'node:url'
|
|
23
23
|
import { runPages } from './browser.mjs'
|
|
24
24
|
import { fill, inline } from './page.mjs'
|
|
25
|
-
import { ChalkTextError, chaptersFor, parseChalk, PROP_ALIASES, PROP_NAMES, validateScene } from './contract.mjs'
|
|
25
|
+
import { ChalkTextError, chaptersFor, parseChalk, PROP_ALIASES, PROP_NAMES, purposeWarnings, validateScene } from './contract.mjs'
|
|
26
26
|
import { syntaxForLine, syntaxForStep } from './syntax.mjs'
|
|
27
27
|
|
|
28
28
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
@@ -59,10 +59,12 @@ let checking
|
|
|
59
59
|
const chalk = (text) => parseChalk((reading = text), PROPS)
|
|
60
60
|
|
|
61
61
|
/** The recipes a file holds: one for .json, .chalk and .mmd; one per ```chalk-talk / ```mermaid block in a .md. */
|
|
62
|
+
const idOf = (file) => basename(file).replace(/\.[^.]+$/, '').replace(/[^a-z0-9-]+/gi, '-').slice(0, 60) || 'drawing'
|
|
62
63
|
async function recipesOf(file) {
|
|
63
64
|
const text = readFileSync(resolve(file), 'utf8')
|
|
64
65
|
if (file.endsWith('.json')) return [JSON.parse(text)]
|
|
65
|
-
|
|
66
|
+
// A .chalk file with no `id` line is named after the file (drawing.chalk → drawing.html), not the reader's default.
|
|
67
|
+
if (file.endsWith('.chalk')) return [/^id\s/m.test(text) ? chalk(text) : { ...chalk(text), id: idOf(file) }]
|
|
66
68
|
if (file.endsWith('.mmd')) return [await mermaid(text)]
|
|
67
69
|
const blocks = [...text.matchAll(/^```(chalk-talk|mermaid)[^\n]*\n([\s\S]*?)^```/gm)]
|
|
68
70
|
if (!blocks.length) throw new Error('no ```chalk-talk or ```mermaid block in this file.')
|
|
@@ -149,10 +151,20 @@ if (scenes.some((s) => Object.keys(s.lotties ?? {}).length)) {
|
|
|
149
151
|
}
|
|
150
152
|
writeFileSync(out, page)
|
|
151
153
|
|
|
154
|
+
// back-of-napkin: what a drawing of its purpose usually misses (a pitch with no ask…) — drawn anyway, to fix like a
|
|
155
|
+
// review finding (exit 3).
|
|
156
|
+
let warned = false
|
|
152
157
|
scenes.forEach((scene, k) => {
|
|
153
158
|
const values = Object.keys(scene.params ?? {}).length + Object.keys(scene.switches ?? {}).length
|
|
154
159
|
const who = course ? ` (${k + 1}) ${scene.title}: ` : ' '
|
|
155
160
|
console.log(`✓ contract${who}${scene.steps.length} steps, ${chaptersFor(scene).length} chapters, ${values} values`)
|
|
161
|
+
// one ready-made animation per drawing reads better (it is polished, not chalk): said, not an error
|
|
162
|
+
const lotties = JSON.stringify(scene.steps).match(/"type":"lottie"/g)?.length ?? 0
|
|
163
|
+
if (lotties > 1) console.log(`• lottie ${course ? `(${k + 1}) ` : ''}${lotties} animations in one drawing: one reads better (keep the others only if the person asked for them)`)
|
|
164
|
+
for (const w of purposeWarnings(scene)) {
|
|
165
|
+
console.log(`! ${scene.kind.padEnd(9)}${course ? ` (${k + 1})` : ''} ${w.text}`)
|
|
166
|
+
warned = true
|
|
167
|
+
}
|
|
156
168
|
})
|
|
157
169
|
|
|
158
170
|
// Onda C: a figure made with `define` is seen by nobody when the build passes at once (the review measures, it does
|
|
@@ -172,7 +184,7 @@ const ran = jobs.length ? await runPages(jobs, { timeout }) : { results: [] }
|
|
|
172
184
|
const name = (exe) => basename(exe).replace(/\.exe$/i, '')
|
|
173
185
|
const result = (what, k) => ran.results?.[jobs.findIndex((j) => j.what === what && j.k === k)]
|
|
174
186
|
|
|
175
|
-
let code = 0
|
|
187
|
+
let code = warned ? 3 : 0
|
|
176
188
|
if (noReview) console.log('• review skipped (--no-review); the page checks itself when opened')
|
|
177
189
|
else if (!ran.results) console.log('• review no Chrome, Chromium or Edge here: the page checks itself when opened')
|
|
178
190
|
else {
|
package/scripts/contract.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// GENERATED by tools/assets.mjs from the drawing engine v0.
|
|
1
|
+
// GENERATED by tools/assets.mjs from the drawing engine v0.257.0. Do not edit.
|
|
2
2
|
var WORDS = ["and", "or", "not"];
|
|
3
3
|
|
|
4
4
|
var PROP_NAMES = [
|
|
@@ -417,6 +417,7 @@ var hasFace = (name) => {
|
|
|
417
417
|
};
|
|
418
418
|
|
|
419
419
|
var MOTIONS = ["spin", "drive", "steam", "talk", "pulse", "float"];
|
|
420
|
+
var KINDS = ["lesson", "pitch", "arch", "decision", "data", "timeline", "story"];
|
|
420
421
|
var MAX_DEFS = 8;
|
|
421
422
|
var MAX_LOTTIES = 6;
|
|
422
423
|
var MAX_LOTTIE_CHARS = 2e5;
|
|
@@ -878,6 +879,9 @@ function validateScene(data) {
|
|
|
878
879
|
if (!ok) throw new Error('locale: expected a language tag such as "en-US" or "pt-BR".');
|
|
879
880
|
}
|
|
880
881
|
choice(s.background, ["dots", "grid", "plain"], "background");
|
|
882
|
+
choice(s.kind, KINDS, "kind");
|
|
883
|
+
optionalText(s.source, "source");
|
|
884
|
+
if (s.quiz !== void 0 && s.kind !== void 0 && s.kind !== "lesson") throw new Error(`quiz: only a lesson has a quiz (this drawing is a ${s.kind}).`);
|
|
881
885
|
choice(s.theme, ["paper", "chalkboard"], "theme");
|
|
882
886
|
if (s.quiz !== void 0) {
|
|
883
887
|
object(s.quiz, "quiz");
|
|
@@ -1424,6 +1428,12 @@ function parseChalk(text2, props) {
|
|
|
1424
1428
|
case "locale":
|
|
1425
1429
|
scene.locale = one("a language tag");
|
|
1426
1430
|
continue;
|
|
1431
|
+
case "kind":
|
|
1432
|
+
scene.kind = one("what the drawing is for");
|
|
1433
|
+
continue;
|
|
1434
|
+
case "source":
|
|
1435
|
+
scene.source = one("where the numbers come from");
|
|
1436
|
+
continue;
|
|
1427
1437
|
case "background":
|
|
1428
1438
|
scene.background = one("dots, grid or plain");
|
|
1429
1439
|
continue;
|
|
@@ -1589,7 +1599,8 @@ function printChalk(scene, props) {
|
|
|
1589
1599
|
const out = [];
|
|
1590
1600
|
if (scene.id !== void 0) out.push(`id ${scene.id}`);
|
|
1591
1601
|
if (scene.title !== void 0) out.push(`title ${scene.title}`);
|
|
1592
|
-
for (const k of ["category", "theme", "locale", "background"]) if (scene[k] !== void 0) out.push(`${k} ${scene[k]}`);
|
|
1602
|
+
for (const k of ["kind", "category", "theme", "locale", "background"]) if (scene[k] !== void 0) out.push(`${k} ${scene[k]}`);
|
|
1603
|
+
if (scene.source !== void 0) out.push(`source ${q(scene.source)}`);
|
|
1593
1604
|
if (scene.width !== 900 || scene.height !== 560) out.push(`size ${scene.width}x${scene.height}`);
|
|
1594
1605
|
for (const [k, p] of Object.entries(scene.params ?? {})) {
|
|
1595
1606
|
out.push(`param ${k} ${q(p.label)} ${p.min}..${p.max}${p.step !== 1 ? ` step=${p.step}` : ""} = ${p.value}`);
|
|
@@ -1617,13 +1628,48 @@ function printChalk(scene, props) {
|
|
|
1617
1628
|
}
|
|
1618
1629
|
return out.join("\n").replace(/\n{3,}/g, "\n\n") + "\n";
|
|
1619
1630
|
}
|
|
1631
|
+
|
|
1632
|
+
function all(steps) {
|
|
1633
|
+
return steps.flatMap((s) => {
|
|
1634
|
+
const inner = "steps" in s && Array.isArray(s.steps) ? s.steps : "step" in s && s.step ? [s.step] : [];
|
|
1635
|
+
return [s, ...all(inner)];
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
function purposeWarnings(scene) {
|
|
1639
|
+
const steps = all(scene.steps);
|
|
1640
|
+
const last = chaptersFor(scene).at(-1);
|
|
1641
|
+
const endsLit = last ? all(scene.steps.slice(last.start, last.end + 1)).some((s) => s.type === "highlight") : false;
|
|
1642
|
+
const switches = Object.keys(scene.switches ?? {}).length > 0;
|
|
1643
|
+
const params = Object.keys(scene.params ?? {}).length > 0;
|
|
1644
|
+
const out = [];
|
|
1645
|
+
switch (scene.kind) {
|
|
1646
|
+
case "pitch":
|
|
1647
|
+
if (!endsLit) out.push({ key: "pitch-ask", text: "the pitch does not end with an ask (no highlight in its last chapter)" });
|
|
1648
|
+
break;
|
|
1649
|
+
case "arch":
|
|
1650
|
+
if (!switches && !steps.some((s) => s.type === "when")) out.push({ key: "arch-failure", text: "no failure drawn: what happens when something goes down? (no switch, no when)" });
|
|
1651
|
+
break;
|
|
1652
|
+
case "decision":
|
|
1653
|
+
if (!endsLit && !params && !switches) out.push({ key: "decision-winner", text: "the decision does not say which option wins, nor why (no highlight in its last chapter, no value)" });
|
|
1654
|
+
break;
|
|
1655
|
+
case "data":
|
|
1656
|
+
if (!scene.source?.trim()) out.push({ key: "data-source", text: "where do these numbers come from? (no source)" });
|
|
1657
|
+
break;
|
|
1658
|
+
case "timeline":
|
|
1659
|
+
if (!steps.some((s) => s.type === "timeline")) out.push({ key: "timeline-line", text: "the timeline has no line (no timeline step)" });
|
|
1660
|
+
break;
|
|
1661
|
+
}
|
|
1662
|
+
return out;
|
|
1663
|
+
}
|
|
1620
1664
|
export {
|
|
1621
1665
|
ChalkTextError,
|
|
1666
|
+
KINDS,
|
|
1622
1667
|
PROP_ALIASES,
|
|
1623
1668
|
PROP_NAMES,
|
|
1624
1669
|
chaptersFor,
|
|
1625
1670
|
defaultValues,
|
|
1626
1671
|
parseChalk,
|
|
1627
1672
|
printChalk,
|
|
1673
|
+
purposeWarnings,
|
|
1628
1674
|
validateScene
|
|
1629
1675
|
};
|