@aksp/opencrew 1.6.2 → 1.7.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/CHANGELOG.md +102 -0
- package/README.md +70 -13
- package/package.json +2 -2
- package/src/cli.js +15 -38
- package/src/commands/init.js +41 -44
- package/src/commands/update.js +78 -75
- package/src/lib/blocos.js +148 -0
- package/src/lib/deteccao.js +69 -0
- package/src/lib/fsx.js +1 -55
- package/src/lib/ides.js +4 -0
- package/src/lib/legado.js +142 -0
- package/src/lib/manifest.js +67 -26
- package/src/lib/mcp.js +131 -0
- package/src/lib/migrations.js +77 -74
- package/src/lib/node-version.js +43 -0
- package/src/lib/prompts.js +25 -2
- package/src/lib/resumo.js +125 -0
- package/templates/.mcp.json +1 -1
- package/templates/AGENTS.md +20 -6
- package/templates/_opencrew/.opencrew-version +1 -1
- package/templates/_opencrew/core/best-practices/social-networks-publishing.md +14 -14
- package/templates/_opencrew/core/escritorio/animacao.js +64 -0
- package/templates/_opencrew/core/escritorio/app.js +137 -0
- package/templates/_opencrew/core/escritorio/cena.js +132 -0
- package/templates/_opencrew/core/escritorio/demo.js +79 -0
- package/templates/_opencrew/core/escritorio/escala.js +27 -0
- package/templates/_opencrew/core/escritorio/index.html +166 -0
- package/templates/_opencrew/core/escritorio/modelo-agentes.js +93 -0
- package/templates/_opencrew/core/escritorio/modelo-estado.js +71 -0
- package/templates/_opencrew/core/escritorio/modelo-mesas.js +81 -0
- package/templates/_opencrew/core/escritorio/modelo-pagina.js +95 -0
- package/templates/_opencrew/core/escritorio/modelo-textos.js +65 -0
- package/templates/_opencrew/core/escritorio/modelo-visao.js +91 -0
- package/templates/_opencrew/core/escritorio/modelo.js +29 -0
- package/templates/_opencrew/core/escritorio/painel.js +120 -0
- package/templates/_opencrew/core/escritorio/quadro.js +106 -0
- package/templates/_opencrew/core/escritorio/rota.js +62 -0
- package/templates/_opencrew/core/escritorio/rotulos.js +78 -0
- package/templates/_opencrew/core/escritorio/sprites-mesa.js +122 -0
- package/templates/_opencrew/core/escritorio/sprites-sala.js +92 -0
- package/templates/_opencrew/core/escritorio/sprites.js +187 -0
- package/templates/_opencrew/core/prompts/build.prompt.md +3 -3
- package/templates/_opencrew/core/prompts/export.prompt.md +1 -1
- package/templates/_opencrew/core/prompts/repair.prompt.md +7 -12
- package/templates/_opencrew/core/prompts/sherlock-shared.md +5 -5
- package/templates/_opencrew/core/runner.pipeline.md +76 -139
- package/templates/_opencrew/core/scripts/comum.mjs +49 -4
- package/templates/_opencrew/core/scripts/conferir-fontes/busca.mjs +42 -3
- package/templates/_opencrew/core/scripts/conferir-fontes/relatorio.mjs +18 -6
- package/templates/_opencrew/core/scripts/conferir-fontes.mjs +97 -39
- package/templates/_opencrew/core/scripts/escritorio/leitura.mjs +31 -0
- package/templates/_opencrew/core/scripts/escritorio/porta.mjs +98 -0
- package/templates/_opencrew/core/scripts/escritorio/projeto.mjs +29 -0
- package/templates/_opencrew/core/scripts/escritorio/servidor.mjs +78 -0
- package/templates/_opencrew/core/scripts/escritorio.mjs +117 -0
- package/templates/_opencrew/core/scripts/estado/argumentos.mjs +61 -0
- package/templates/_opencrew/core/scripts/estado/arquivo.mjs +53 -0
- package/templates/_opencrew/core/scripts/estado/decisao.mjs +56 -0
- package/templates/_opencrew/core/scripts/estado/elenco.mjs +58 -0
- package/templates/_opencrew/core/scripts/estado/nucleo.mjs +113 -0
- package/templates/_opencrew/core/scripts/estado/preferencia.mjs +24 -0
- package/templates/_opencrew/core/scripts/estado.mjs +96 -0
- package/templates/_opencrew/core/scripts/verificar.mjs +7 -4
- package/templates/_opencrew/core/skills.engine.md +7 -3
- package/templates/gitignore +1 -0
- package/templates/skills/blotato/SKILL.md +39 -10
- package/templates/skills/image-ai-generator/SKILL.md +18 -5
- package/templates/skills/image-ai-generator/scripts/generate.py +52 -10
- package/templates/skills/instagram-publisher/SKILL.md +4 -0
- package/templates/skills/opencrew-skill-creator/references/skill-format.md +1 -0
- package/templates/skills/resend/SKILL.md +52 -13
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// What `update` says (spec R2, rules 13 to 16; texts in §6). Each step of the command returns
|
|
2
|
+
// what it created, rewrote or kept, and the lines here are built ONLY from that: no line states
|
|
3
|
+
// something the step did not do, and a step that changed nothing gets no line. These sentences
|
|
4
|
+
// are in PT-BR; the rest of the CLI is not yet (→ U5).
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { copyLine } from './blocos.js';
|
|
7
|
+
import { legacyLines } from './legado.js';
|
|
8
|
+
import { mcpMessage } from './mcp.js';
|
|
9
|
+
import { UNREADABLE } from './manifest.js';
|
|
10
|
+
import { log, info, ok, warn } from './ui.js';
|
|
11
|
+
|
|
12
|
+
/** Last line of `update`: only what the code guarantees (a deleted crew template comes back). */
|
|
13
|
+
export const UNTOUCHED = 'Não foram alterados: as crews que você criou, `_opencrew/_memory/`, `_opencrew/best-practices.local/` e `.env`.';
|
|
14
|
+
|
|
15
|
+
/** `init` in a workspace that is already installed (rule 15). */
|
|
16
|
+
export const ALREADY_INSTALLED = 'Para atualizar, rode `npx @aksp/opencrew@latest update`. Não apague `_opencrew/` para reinstalar: a pasta guarda a sua memória (`_opencrew/_memory/`) e as suas best-practices (`_opencrew/best-practices.local/`).';
|
|
17
|
+
|
|
18
|
+
const NO_BRIDGES = 'Nenhuma ponte de IDE encontrada: nada a atualizar. Para criar a ponte de uma IDE: `npx @aksp/opencrew@latest init --repair-bridges --ide=<id>`.';
|
|
19
|
+
const LEAK_REMOVED = 'CLAUDE.md: removi a seção de STATUS.md que as versões 1.4.0 e 1.4.1 gravaram por engano.';
|
|
20
|
+
const FIRST_PROTECTED = 'Primeira atualização com proteção: sem registro anterior, guardamos tudo o que diferia. Daqui em diante, só o que você editar.';
|
|
21
|
+
// By action of `deliverBlock`; `kept` has no line.
|
|
22
|
+
const BLOCK = {
|
|
23
|
+
'AGENTS.md': {
|
|
24
|
+
created: 'AGENTS.md criado com o bloco do OpenCrew.',
|
|
25
|
+
added: 'AGENTS.md: bloco do OpenCrew acrescentado no topo; o seu texto foi mantido.',
|
|
26
|
+
updated: 'AGENTS.md: bloco do OpenCrew atualizado.',
|
|
27
|
+
},
|
|
28
|
+
'.gitignore': {
|
|
29
|
+
created: '`.gitignore` criado com o bloco do OpenCrew.',
|
|
30
|
+
added: '`.gitignore`: bloco do OpenCrew acrescentado no fim; as suas linhas foram mantidas.',
|
|
31
|
+
updated: '`.gitignore`: bloco do OpenCrew atualizado.',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const LEFTOVER = {
|
|
35
|
+
header: (n) => `${n} resto(s) de uma instalação antiga do OpenSquad (a pasta \`_opensquad/\` não existe neste projeto). Nada foi apagado:`,
|
|
36
|
+
bridge: (file) => `${file} — ponte do OpenSquad; o OpenCrew não usa este arquivo.`,
|
|
37
|
+
cites: (file) => `${file} — cita \`_opensquad/\`; confira antes de apagar, pode ser um arquivo seu.`,
|
|
38
|
+
end: 'Se você não usa mais o OpenSquad, pode apagar as pontes listadas. Este aviso volta a cada `update` enquanto os arquivos existirem.',
|
|
39
|
+
config: (file) => `O servidor \`playwright\` do \`.mcp.json\` aponta para \`${file}\`, que não existe neste projeto (resto do OpenSquad). Não alterei o arquivo.`,
|
|
40
|
+
};
|
|
41
|
+
const MCP_REWRITTEN = new Set(['added', 'output-dir']); // actions of `updateMcp` that change the file
|
|
42
|
+
const MAX_COPIES = 15;
|
|
43
|
+
|
|
44
|
+
/** Print the lines a function of this module returned: `[printer, text]` each. */
|
|
45
|
+
export function say(lines) {
|
|
46
|
+
for (const [print, text] of lines) print(text);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Rule 14: the files `update` wrote in `crews/` because they were missing (`results` of deliverTree). */
|
|
50
|
+
export function recreatedLines(results) {
|
|
51
|
+
const files = results.filter((r) => r.action === 'created').map((r) => r.file);
|
|
52
|
+
if (!files.length) return [];
|
|
53
|
+
return [[info, `${files.length} arquivo(s) que faltava(m) em \`crews/\` foram entregues de novo: ${files.join(', ')}.`]];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const blockLines = (results) => results
|
|
57
|
+
.map((r) => BLOCK[r?.file]?.[r.action])
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.map((text) => [ok, text]);
|
|
60
|
+
|
|
61
|
+
/** One line per case that happened: IDEs with a bridge rewritten, files created, or neither. */
|
|
62
|
+
function bridgeLines(ides, bridges, legacy = []) {
|
|
63
|
+
if (!ides.length) return [[info, NO_BRIDGES]];
|
|
64
|
+
const cleaned = new Set(legacy.filter((l) => l.action === 'removed').map((l) => l.file));
|
|
65
|
+
const rewritten = bridges.filter((b) => b.action === 'updated' || b.action === 'added' || cleaned.has(b.file));
|
|
66
|
+
const labels = [...new Set(rewritten.map((b) => b.ide.label))];
|
|
67
|
+
const created = bridges.filter((b) => b.action === 'created' && !b.shared).map((b) => b.file);
|
|
68
|
+
const lines = [];
|
|
69
|
+
if (labels.length) lines.push([ok, `Pontes atualizadas: ${labels.join(', ')}.`]);
|
|
70
|
+
if (created.length) lines.push([ok, `Pontes criadas: ${created.join(', ')}.`]);
|
|
71
|
+
return lines.length ? lines : [[ok, 'Pontes das IDEs já estavam em dia.']];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function mcpLines(ctx, mcp) {
|
|
75
|
+
const said = mcpMessage(ctx, mcp.action);
|
|
76
|
+
return said ? [[said.warn ? warn : ok, said.text]] : [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Rule 16. `mcp` = what this run did to .mcp.json: the line only says "não alterei" if so. */
|
|
80
|
+
function leftoverLines({ files, config }, mcp) {
|
|
81
|
+
const lines = [];
|
|
82
|
+
if (files.length) {
|
|
83
|
+
lines.push([warn, LEFTOVER.header(files.length)]);
|
|
84
|
+
for (const { file, bridge } of files) lines.push([log, ` ${(bridge ? LEFTOVER.bridge : LEFTOVER.cites)(file)}`]);
|
|
85
|
+
lines.push([info, LEFTOVER.end]);
|
|
86
|
+
}
|
|
87
|
+
if (config && !MCP_REWRITTEN.has(mcp.action)) lines.push([warn, LEFTOVER.config(config)]);
|
|
88
|
+
return lines;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The copies of this run. `unreadable` = the manifest existed and could not be used. */
|
|
92
|
+
function copyLines(ctx, unreadable) {
|
|
93
|
+
const n = ctx.copied.length;
|
|
94
|
+
if (!n) return [];
|
|
95
|
+
const dir = path.relative(ctx.target, ctx.backupDir).split(path.sep).join('/');
|
|
96
|
+
const header = ctx.manifest
|
|
97
|
+
? `${n} arquivo(s) foram copiados para ${dir}/ antes de serem substituídos (editados por você, ou sem registro de entrega):`
|
|
98
|
+
: `${n} arquivo(s) diferentes do pacote novo foram copiados para ${dir}/ antes de serem substituídos:`;
|
|
99
|
+
const lines = [[warn, header], ...ctx.copied.slice(0, MAX_COPIES).map((f) => [log, ` ${copyLine(ctx, f)}`])];
|
|
100
|
+
if (n > MAX_COPIES) lines.push([log, ` … e mais ${n - MAX_COPIES}`]);
|
|
101
|
+
if (unreadable) lines.push([info, UNREADABLE.summary]);
|
|
102
|
+
else if (!ctx.manifest) lines.push([info, FIRST_PROTECTED]);
|
|
103
|
+
return lines;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The summary of an `update`, in the order the steps ran.
|
|
108
|
+
* @param {object} ctx delivery context (src/lib/manifest.js)
|
|
109
|
+
* @param {object} done what each step returned: `agents` and `gitignore` (deliverBlock; `agents`
|
|
110
|
+
* is null when a pre-1.3 AGENTS.md was migrated), `ides` (detected), `bridges` (deliverBridges),
|
|
111
|
+
* `leak` (the STATUS.md section left CLAUDE.md's block), `mcp` (updateMcp), `leftovers`
|
|
112
|
+
* (findLeftovers) and `unreadable` (manifest)
|
|
113
|
+
* @returns {Array<[Function, string]>} lines for `say`
|
|
114
|
+
*/
|
|
115
|
+
export function updateSummary(ctx, done) {
|
|
116
|
+
return [
|
|
117
|
+
...blockLines([done.agents, done.gitignore]),
|
|
118
|
+
...bridgeLines(done.ides, done.bridges, ctx.legacy),
|
|
119
|
+
...(done.leak ? [[ok, LEAK_REMOVED]] : []),
|
|
120
|
+
...legacyLines(ctx).map((line) => [warn, line]),
|
|
121
|
+
...mcpLines(ctx, done.mcp),
|
|
122
|
+
...leftoverLines(done.leftovers, done.mcp),
|
|
123
|
+
...copyLines(ctx, done.unreadable),
|
|
124
|
+
];
|
|
125
|
+
}
|
package/templates/.mcp.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "
|
|
2
|
+
"_comment": "Versão do Playwright MCP fixada por estabilidade. Para trocar, edite a versão abaixo e reinicie a IDE. O `update` do OpenCrew não altera a versão que está neste arquivo.",
|
|
3
3
|
"mcpServers": {
|
|
4
4
|
"playwright": {
|
|
5
5
|
"command": "npx",
|
package/templates/AGENTS.md
CHANGED
|
@@ -58,6 +58,8 @@ Route input to the matching action:
|
|
|
58
58
|
| `/opencrew edit-company` | Re-run company profile setup |
|
|
59
59
|
| `/opencrew show-company` | Display company.md contents |
|
|
60
60
|
| `/opencrew settings` | Show/edit preferences.md |
|
|
61
|
+
| `/opencrew dashboard` | Turn on and open the Escritório (live view) — see "Dashboard (Optional)" |
|
|
62
|
+
| `/opencrew dashboard off` | Turn the Escritório off — see "Dashboard (Optional)" |
|
|
61
63
|
| `/opencrew reset` | Confirm and reset all configuration |
|
|
62
64
|
| Natural language about crews | Infer intent and route accordingly |
|
|
63
65
|
|
|
@@ -90,12 +92,24 @@ When running a crew:
|
|
|
90
92
|
|
|
91
93
|
## Dashboard (Optional)
|
|
92
94
|
|
|
93
|
-
The dashboard is
|
|
94
|
-
It is **disabled by default
|
|
95
|
-
`
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
The dashboard is the **Escritório**: a local page that shows the crew at work, step by step.
|
|
96
|
+
It is **disabled by default**. The switch is the `Dashboard` line of
|
|
97
|
+
`_opencrew/_memory/preferences.md`; while it says `enabled`, the Pipeline Runner reports each
|
|
98
|
+
step with one short command (see `_opencrew/core/runner.pipeline.md`).
|
|
99
|
+
|
|
100
|
+
**`/opencrew dashboard`** — in this order:
|
|
101
|
+
1. Write `- **Dashboard:** enabled` in `_opencrew/_memory/preferences.md`, changing only that
|
|
102
|
+
line (if the file has no `Dashboard` line, add it at the end). Leave the rest of the file as is.
|
|
103
|
+
2. Start `node _opencrew/core/scripts/escritorio.mjs` in the background, from the project root.
|
|
104
|
+
The line it prints carries the address (`http://127.0.0.1:<port>`; the port may vary).
|
|
105
|
+
3. Show the address and tell the user that the next crew run appears there.
|
|
106
|
+
|
|
107
|
+
If your IDE cannot keep a process running in the background, do step 1 and show the user the
|
|
108
|
+
command of step 2 to run in another terminal. Running `/opencrew dashboard` again is safe: the
|
|
109
|
+
script answers with the same address instead of opening a second page.
|
|
110
|
+
|
|
111
|
+
**`/opencrew dashboard off`** — write `- **Dashboard:** disabled` the same way (only that line)
|
|
112
|
+
and touch nothing else: stop no process, delete no file.
|
|
99
113
|
|
|
100
114
|
## Language Handling
|
|
101
115
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.7.0
|
|
@@ -11,7 +11,7 @@ version: "1.0.0"
|
|
|
11
11
|
## Compact Rules
|
|
12
12
|
|
|
13
13
|
1. Never publish live content without explicit user confirmation.
|
|
14
|
-
2.
|
|
14
|
+
2. With `instagram-publisher`, execute and report a successful dry-run before offering the live publish option; `blotato` has no dry-run: send nothing to it, not even media, before the user answers with the word `publicar`.
|
|
15
15
|
3. Validate all platform-specific constraints (image format, caption length) before API calls.
|
|
16
16
|
4. Adapt and format content natively for each target platform; do not cross-post raw text.
|
|
17
17
|
5. Report publishing results immediately, including success (with URL) or failure details.
|
|
@@ -19,7 +19,7 @@ version: "1.0.0"
|
|
|
19
19
|
7. Track API usage and proactively warn users if approaching rate limits.
|
|
20
20
|
8. Fall back gracefully if a required publishing skill is missing; list available alternatives.
|
|
21
21
|
9. Inform the user and seek permission before converting image formats (e.g., PNG to JPEG).
|
|
22
|
-
10. Display a structured preview (platform, images, caption, hashtags, validations) before dry-run.
|
|
22
|
+
10. Display a structured preview (platform, images, caption, hashtags, validations) before anything is sent (the `instagram-publisher` dry-run included).
|
|
23
23
|
11. Do not silently truncate captions; ask the user to shorten them if limits are exceeded.
|
|
24
24
|
12. Request user direction (continue or abort) if one platform fails in a multi-platform batch.
|
|
25
25
|
|
|
@@ -29,9 +29,9 @@ version: "1.0.0"
|
|
|
29
29
|
|
|
30
30
|
## Core Principles
|
|
31
31
|
|
|
32
|
-
1. **Never publish without explicit user confirmation.** This is the cardinal rule. Before any live post, present the full preview (platform, images, caption, hashtags) and wait for the user to confirm. A dry-run is not confirmation. The user must
|
|
32
|
+
1. **Never publish without explicit user confirmation.** This is the cardinal rule. Before any live post, present the full preview (platform, images, caption, hashtags) and wait for the user to confirm. A dry-run is not confirmation. The user must answer with the confirmation word the publishing skill asks for (e.g. `publicar`) before any live API call is made; any other answer means do not publish.
|
|
33
33
|
|
|
34
|
-
2. **Dry-run first,
|
|
34
|
+
2. **Dry-run first, where the skill has one (`instagram-publisher`).** There, the first execution of the publishing workflow must be a dry-run (test mode). This validates that credentials are configured, images meet requirements, captions are within limits, and the API connection works. Only after a successful dry-run should the user be offered the option to publish for real. `blotato` has no dry-run: send nothing to it, not even media, before the user answers with the word `publicar` — its upload is part of the publication.
|
|
35
35
|
|
|
36
36
|
3. **Validate platform requirements before attempting to publish.** Every platform has specific constraints. Validate all of them before making any API call. If validation fails, report the specific issue and suggest a fix before proceeding.
|
|
37
37
|
|
|
@@ -107,12 +107,12 @@ Every platform has specific constraints that must be validated before making any
|
|
|
107
107
|
Status: All validations passed
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
5. **Execute dry-run.** Run the publishing workflow in test mode:
|
|
110
|
+
5. **Execute dry-run (`instagram-publisher` only).** Run the publishing workflow in test mode:
|
|
111
111
|
- Instagram: `--dry-run` flag on the publish script
|
|
112
|
-
- Blotato:
|
|
113
|
-
- Report dry-run results: credentials OK, media uploaded, container created, ready to publish.
|
|
112
|
+
- Blotato: no dry-run. Skip this step: nothing is uploaded or sent before the confirmation word (step 6)
|
|
113
|
+
- Report the `instagram-publisher` dry-run results: credentials OK, media uploaded, container created, ready to publish.
|
|
114
114
|
|
|
115
|
-
6. **Request final confirmation.** Present the dry-run results
|
|
115
|
+
6. **Request final confirmation.** Present the dry-run results (Blotato: the preview) and ask for the skill's confirmation word. Do not proceed without it.
|
|
116
116
|
|
|
117
117
|
7. **Publish and report.** Execute the live publish. Report the result immediately:
|
|
118
118
|
- Success: post URL, post ID, platform, timestamp
|
|
@@ -130,7 +130,7 @@ Every platform has specific constraints that must be validated before making any
|
|
|
130
130
|
## Quality Criteria
|
|
131
131
|
|
|
132
132
|
- [ ] User confirmation was received before any live publish (not just dry-run)
|
|
133
|
-
- [ ] Dry-run was executed and passed before live publish
|
|
133
|
+
- [ ] Dry-run was executed and passed before live publish (`instagram-publisher`); nothing was sent to Blotato before the confirmation word
|
|
134
134
|
- [ ] All platform-specific validations passed (image format, dimensions, caption length, image count)
|
|
135
135
|
- [ ] Publish preview was presented with complete details (platform, images, caption, validation status)
|
|
136
136
|
- [ ] Successful publishes include post URL/permalink and post ID
|
|
@@ -213,7 +213,7 @@ Skill: blotato (multi-platform)
|
|
|
213
213
|
|
|
214
214
|
PLATFORM 1/3: Instagram
|
|
215
215
|
Validation: All checks passed
|
|
216
|
-
|
|
216
|
+
Preview: Confirmed with the word "publicar"
|
|
217
217
|
Publish: Published successfully
|
|
218
218
|
Post URL: https://www.instagram.com/p/DEF456abc/
|
|
219
219
|
Post ID: ig_17899506834567890
|
|
@@ -221,7 +221,7 @@ PLATFORM 1/3: Instagram
|
|
|
221
221
|
|
|
222
222
|
PLATFORM 2/3: LinkedIn
|
|
223
223
|
Validation: All checks passed
|
|
224
|
-
|
|
224
|
+
Preview: Confirmed with the word "publicar"
|
|
225
225
|
Publish: FAILED
|
|
226
226
|
Error: 403 Forbidden — "Publishing permission not granted"
|
|
227
227
|
HTTP Status: 403
|
|
@@ -245,7 +245,7 @@ PLATFORM 3/3: X/Twitter
|
|
|
245
245
|
[User chooses: b, provides short caption]
|
|
246
246
|
|
|
247
247
|
Validation: All checks passed (short caption: 142 chars)
|
|
248
|
-
|
|
248
|
+
Preview: Confirmed with the word "publicar"
|
|
249
249
|
Publish: Published successfully
|
|
250
250
|
Post URL: https://x.com/brandname/status/1234567890123456789
|
|
251
251
|
Post ID: tw_1234567890123456789
|
|
@@ -276,7 +276,7 @@ SUMMARY
|
|
|
276
276
|
|
|
277
277
|
5. **Never report success without a URL.** "Published successfully" without a post URL is not verifiable. Every successful publish must include the post permalink. If the API does not return a URL, report that as a limitation.
|
|
278
278
|
|
|
279
|
-
6. **Never assume credentials are valid.** Always verify credentials
|
|
279
|
+
6. **Never assume credentials are valid.** Always verify credentials before publishing (the dry-run with `instagram-publisher`; listing the accounts, read-only, with `blotato`). Tokens expire, permissions get revoked, accounts get disconnected. A credential check is part of every publish workflow.
|
|
280
280
|
|
|
281
281
|
7. **Never publish the same raw caption across all platforms without adaptation.** Instagram, LinkedIn, and X/Twitter have different formatting conventions, character limits, and audience expectations. At minimum, verify the caption fits the platform constraints. Ideally, suggest platform-specific adaptations.
|
|
282
282
|
|
|
@@ -284,7 +284,7 @@ SUMMARY
|
|
|
284
284
|
|
|
285
285
|
1. **Present a structured preview before every publish.** Show: platform, account, images (with dimensions and format), caption (with character count), hashtags, and validation status. The user must see exactly what will be published.
|
|
286
286
|
|
|
287
|
-
2. **Run a dry-run before every live publish
|
|
287
|
+
2. **Run a dry-run before every live publish with `instagram-publisher`.** Test the full workflow without posting. Verify credentials, upload media, create containers, validate everything. Report dry-run results before requesting confirmation. With `blotato` there is no dry-run: the preview and the confirmation word come before any upload.
|
|
288
288
|
|
|
289
289
|
3. **Report results immediately after each publish.** Do not batch results. After each platform publish (success or failure), report the outcome with all relevant details before moving to the next platform.
|
|
290
290
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// O que anima (regra 28): a passagem de bastão e a comemoração nascem da diferença entre duas
|
|
2
|
+
// leituras da mesma crew, nunca de um status gravado. `transicao` anota no modelo a hora em que
|
|
3
|
+
// cada uma começou; `quadro` (em quadro.js) desenha pelo tempo decorrido desde então.
|
|
4
|
+
// Puro: sem DOM, sem relógio, sem rede. A hora entra por parâmetro.
|
|
5
|
+
// Spec: fase-e1-escritorio-ao-vivo.md, regra 28 (repositório do OpenCrew).
|
|
6
|
+
|
|
7
|
+
/** A entrega: ida até a frente da mesa de quem recebe, com o papel, e volta até sentar. */
|
|
8
|
+
export const IDA_MS = 2000;
|
|
9
|
+
export const VOLTA_MS = 2000;
|
|
10
|
+
/** Quanto dura a comemoração de uma execução concluída. */
|
|
11
|
+
export const COMEMORACAO_MS = 4000;
|
|
12
|
+
/** Quem digita troca de pose a cada 0,3 s; quem anda, a cada 0,15 s. */
|
|
13
|
+
export const DIGITAR_MS = 300;
|
|
14
|
+
export const ANDAR_MS = 150;
|
|
15
|
+
|
|
16
|
+
/** Nada em curso: é o que vale na primeira leitura de cada crew. */
|
|
17
|
+
export const SEM_ANIMACAO = Object.freeze({ entrega: null, comemoracao: null });
|
|
18
|
+
|
|
19
|
+
/** As duas leituras têm estado e são da mesma crew (ou as duas são da demonstração). */
|
|
20
|
+
function mesmaCrew(anterior, atual) {
|
|
21
|
+
if (!anterior || !atual || anterior.semEstado || atual.semEstado) return false;
|
|
22
|
+
return Boolean(anterior.demo) === Boolean(atual.demo) && anterior.crew === atual.crew;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A passagem de bastão cuja data a leitura anterior não trazia; senão (ou sem data), `null`. */
|
|
26
|
+
function entregaNova(anterior, atual, agoraMs) {
|
|
27
|
+
const { passagem } = atual;
|
|
28
|
+
if (!passagem || passagem.completedAt === (anterior.passagem?.completedAt ?? '')) return null;
|
|
29
|
+
return { deId: passagem.deId, paraId: passagem.paraId, inicioMs: agoraMs };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Começa quando a execução passa a `completed`, segue enquanto ela continuar assim. */
|
|
33
|
+
function comemoracaoDe(anterior, atual, agoraMs) {
|
|
34
|
+
if (atual.execucao !== 'completed') return null;
|
|
35
|
+
return anterior.execucao === 'completed' ? (anterior.animacao?.comemoracao ?? null) : { inicioMs: agoraMs };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Compara duas leituras e devolve a atual com o que está animando. A página chama a cada leitura
|
|
40
|
+
* (cada consulta ao servidor, cada passo da demonstração), e não só quando vai pintar: assim a
|
|
41
|
+
* entrega que acontece com a aba oculta já aparece terminada na volta.
|
|
42
|
+
* @param {object|null} anterior o que esta função devolveu na leitura anterior; `null` na primeira
|
|
43
|
+
* @param {object} atual o modelo da leitura atual (de `montarPagina` ou `montarModelo`)
|
|
44
|
+
* @param {number} agoraMs a hora desta leitura, em milissegundos, no mesmo relógio de `quadro`
|
|
45
|
+
* @returns {object} `atual` mais `animacao`: `{ entrega, comemoracao }` · `entrega`:
|
|
46
|
+
* `{ deId, paraId, inicioMs }` desde que `passagem.completedAt` mudou entre duas leituras da
|
|
47
|
+
* mesma crew, ou `null`; uma passagem nova substitui a que está em curso, e qualquer outra
|
|
48
|
+
* mudança de estado a deixa como está · `comemoracao`: `{ inicioMs }` desde que a execução
|
|
49
|
+
* passou a `completed`, ou `null` · na primeira leitura de uma crew (abrir, trocar de crew,
|
|
50
|
+
* sair da demonstração, voltar de "sem estado"), as duas são `null` · `animacao` só guarda
|
|
51
|
+
* quando cada uma começou, e a entrega fica anotada depois de terminar: quem diz o que ainda
|
|
52
|
+
* se move num instante é `quadro`
|
|
53
|
+
*/
|
|
54
|
+
export function transicao(anterior, atual, agoraMs) {
|
|
55
|
+
if (!mesmaCrew(anterior, atual)) return { ...atual, animacao: SEM_ANIMACAO };
|
|
56
|
+
const emCurso = anterior.animacao?.entrega ?? null;
|
|
57
|
+
return {
|
|
58
|
+
...atual,
|
|
59
|
+
animacao: {
|
|
60
|
+
entrega: entregaNova(anterior, atual, agoraMs) ?? emCurso,
|
|
61
|
+
comemoracao: comemoracaoDe(anterior, atual, agoraMs),
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// A página do escritório: pergunta o estado ao servidor a cada segundo (e na hora, ao voltar para
|
|
2
|
+
// a aba), entrega a resposta ao modelo e aplica o que ele devolve — o desenho no canvas, os
|
|
3
|
+
// rótulos por cima e o painel em volta. Consulta que falha mantém o último estado bom e liga o
|
|
4
|
+
// aviso; a página segue tentando, sem recarregar.
|
|
5
|
+
// É o único módulo que toca na janela: o relógio, a rede, o endereço (`?demo`), o tamanho da tela
|
|
6
|
+
// e a preferência "reduzir movimento" entram no modelo por parâmetro.
|
|
7
|
+
// Spec: fase-e1-escritorio-ao-vivo.md, regras 16, 20 a 23 e 28 (repositório do OpenCrew).
|
|
8
|
+
import { ALTURA, LARGURA, MEMORIA_INICIAL, consultar, emExibicao, escala, escolher, montarPagina, quadro, transicao } from './modelo.js';
|
|
9
|
+
import { animar } from './cena.js';
|
|
10
|
+
import { criarPainel } from './painel.js';
|
|
11
|
+
import { criarRotulos } from './rotulos.js';
|
|
12
|
+
|
|
13
|
+
/** De quanto em quanto tempo a página pergunta o estado. */
|
|
14
|
+
const INTERVALO_MS = 1000;
|
|
15
|
+
/** Consulta sem resposta neste prazo é abandonada e conta como falha: a seguinte sai no ritmo normal. */
|
|
16
|
+
const PRAZO_MS = 4000;
|
|
17
|
+
/** Na demonstração nenhuma consulta traz o passo seguinte: o roteiro é relido neste ritmo. */
|
|
18
|
+
const RITMO_DA_DEMO_MS = 200;
|
|
19
|
+
/** Relativo à página: o servidor só atende o próprio endereço. */
|
|
20
|
+
const ENDERECO = 'estado';
|
|
21
|
+
|
|
22
|
+
/** Regra 16: o canvas tem 320·N × 180·N pixels de verdade e, na página, o tamanho que `escala` devolve. */
|
|
23
|
+
function ajustar(p) {
|
|
24
|
+
const [moldura, sala, tela] = ['moldura', 'sala', 'tela'].map(p.el);
|
|
25
|
+
p.razao = p.janela.devicePixelRatio;
|
|
26
|
+
const { n, largura, altura } = escala(moldura.clientWidth, moldura.clientHeight, p.razao);
|
|
27
|
+
if (tela.width !== LARGURA * n) Object.assign(tela, { width: LARGURA * n, height: ALTURA * n });
|
|
28
|
+
sala.style.width = `${largura}px`;
|
|
29
|
+
sala.style.height = `${altura}px`;
|
|
30
|
+
// Quantos pixels da página vale um pixel lógico: o CSS dos rótulos acompanha a escala por aqui.
|
|
31
|
+
sala.style.setProperty('--u', String(largura / LARGURA));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Uma leitura: monta o modelo deste instante, compara com a leitura anterior (é daí que saem a
|
|
36
|
+
* entrega e a comemoração) e escreve a página. Na primeira, mostra a página e mede o canvas.
|
|
37
|
+
*/
|
|
38
|
+
function ler(p) {
|
|
39
|
+
const agora = Date.now();
|
|
40
|
+
const naDemo = emExibicao(p.memoria, { demo: p.demo }).modo === 'demo';
|
|
41
|
+
p.demoDesde = naDemo ? (p.demoDesde ?? agora) : null;
|
|
42
|
+
const opcoes = { agoraMs: agora, reduzirMovimento: p.menosMovimento.matches, demo: p.demo, decorridoDemoMs: naDemo ? agora - p.demoDesde : 0 };
|
|
43
|
+
const primeira = p.leitura === null;
|
|
44
|
+
p.leitura = transicao(p.leitura, montarPagina(p.memoria, opcoes), agora);
|
|
45
|
+
p.lidaEm = agora;
|
|
46
|
+
p.painel.atualizar(p.leitura);
|
|
47
|
+
p.rotulos.atualizar(p.leitura.agentes);
|
|
48
|
+
if (!primeira) return;
|
|
49
|
+
p.el('pagina').hidden = false;
|
|
50
|
+
ajustar(p);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A resposta de `/estado`, ou `null` se a consulta falhou, demorou demais ou veio inválida. */
|
|
54
|
+
async function perguntar(p) {
|
|
55
|
+
const corte = new AbortController();
|
|
56
|
+
const prazo = p.janela.setTimeout(() => corte.abort(), PRAZO_MS);
|
|
57
|
+
try {
|
|
58
|
+
const resposta = await p.janela.fetch(ENDERECO, { cache: 'no-store', signal: corte.signal });
|
|
59
|
+
return resposta.ok ? await resposta.json() : null;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
} finally {
|
|
63
|
+
p.janela.clearTimeout(prazo);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Regra 20: uma consulta por vez; a que falha entra na memória como falha, e a página não para. */
|
|
68
|
+
async function consultarServidor(p) {
|
|
69
|
+
if (p.consultando) return;
|
|
70
|
+
p.consultando = true;
|
|
71
|
+
const resposta = await perguntar(p);
|
|
72
|
+
p.consultando = false;
|
|
73
|
+
p.memoria = consultar(p.memoria ?? MEMORIA_INICIAL, resposta);
|
|
74
|
+
ler(p);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Regra 22: a escolha do usuário no seletor vale até ele trocar. */
|
|
78
|
+
function escolherCrew(p, crew) {
|
|
79
|
+
if (!p.memoria) return;
|
|
80
|
+
p.memoria = escolher(p.memoria, crew);
|
|
81
|
+
ler(p);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Os bonecos deste instante, para a cena pintar; `null` antes da primeira leitura. */
|
|
85
|
+
function quadroAgora(p) {
|
|
86
|
+
if (!p.leitura) return null;
|
|
87
|
+
const agora = Date.now();
|
|
88
|
+
if (p.leitura.modo === 'demo' && agora - p.lidaEm >= RITMO_DA_DEMO_MS) ler(p);
|
|
89
|
+
if (p.janela.devicePixelRatio !== p.razao) ajustar(p);
|
|
90
|
+
const bonecos = quadro(p.leitura, agora);
|
|
91
|
+
p.rotulos.acompanhar(bonecos);
|
|
92
|
+
return bonecos;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** O que a página guarda entre uma consulta e a seguinte. */
|
|
96
|
+
function criarPagina(janela) {
|
|
97
|
+
const documento = janela.document;
|
|
98
|
+
const el = (id) => documento.getElementById(id);
|
|
99
|
+
const p = {
|
|
100
|
+
janela,
|
|
101
|
+
documento,
|
|
102
|
+
el,
|
|
103
|
+
demo: new URLSearchParams(janela.location.search).has('demo'),
|
|
104
|
+
menosMovimento: janela.matchMedia('(prefers-reduced-motion: reduce)'),
|
|
105
|
+
rotulos: criarRotulos(el('rotulos'), documento),
|
|
106
|
+
memoria: null, // as consultas ao servidor; `null` até a primeira voltar
|
|
107
|
+
leitura: null, // o modelo da última leitura, com o que está animando
|
|
108
|
+
lidaEm: 0,
|
|
109
|
+
demoDesde: null, // quando a demonstração entrou na tela
|
|
110
|
+
consultando: false,
|
|
111
|
+
razao: 0, // a razão de pixels com que o canvas foi medido
|
|
112
|
+
};
|
|
113
|
+
p.painel = criarPainel(documento, (crew) => escolherCrew(p, crew));
|
|
114
|
+
return p;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Liga a página a uma janela. A frase de abertura, que já está no HTML, só some depois do
|
|
119
|
+
* primeiro desenho: se algo aqui não carregar ou não rodar, ela continua na tela.
|
|
120
|
+
* @param {Window} janela a janela do navegador (nos testes, uma de mentira)
|
|
121
|
+
*/
|
|
122
|
+
export function iniciar(janela) {
|
|
123
|
+
const p = criarPagina(janela);
|
|
124
|
+
const reajustar = () => ajustar(p);
|
|
125
|
+
janela.addEventListener('resize', reajustar);
|
|
126
|
+
if (janela.ResizeObserver) new janela.ResizeObserver(reajustar).observe(p.el('moldura'));
|
|
127
|
+
p.documento.addEventListener('visibilitychange', () => {
|
|
128
|
+
if (!p.documento.hidden) consultarServidor(p);
|
|
129
|
+
});
|
|
130
|
+
janela.setInterval(() => consultarServidor(p), INTERVALO_MS);
|
|
131
|
+
animar(janela, p.el('tela'), () => quadroAgora(p), () => {
|
|
132
|
+
p.el('abertura').hidden = true;
|
|
133
|
+
});
|
|
134
|
+
consultarServidor(p);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (typeof window !== 'undefined') iniciar(window);
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// A cena: pinta no canvas, em pixel lógico (320×180), o quadro que o modelo calculou — a sala, a
|
|
2
|
+
// mesa de cada agente do elenco com o monitor, e os bonecos. Nada aqui decide pose, posição ou
|
|
3
|
+
// cor de monitor: isso chega pronto de `quadro`. A cena só preenche retângulos; nenhum texto
|
|
4
|
+
// entra no canvas (nome e balão são elementos da página, numa camada por cima).
|
|
5
|
+
// O laço de animação pede um quadro por vez ao navegador e só pinta de novo quando ele muda.
|
|
6
|
+
// Spec: fase-e1-escritorio-ao-vivo.md, regras 16, 18, 19 e 28 (repositório do OpenCrew).
|
|
7
|
+
import { ALTURA, BONECO, COLUNAS, LARGURA, LARGURA_COLUNA, TOPO } from './modelo.js';
|
|
8
|
+
import { coresDoBoneco, linhasDoBoneco, retangulos } from './sprites.js';
|
|
9
|
+
import { JANELA, LADRILHO, LADRILHO_ESCURO, PAREDE, PLANTA, RELOGIO } from './sprites-sala.js';
|
|
10
|
+
import {
|
|
11
|
+
MESA, MESA_ACIMA, MONITOR, MONITOR_EM, SINAL_DUVIDA, SINAL_FALHA, SINAL_FEITO, SOMBRA,
|
|
12
|
+
TELA_AMBAR, TELA_EM, TELA_VERMELHA, TELAS_ACESAS,
|
|
13
|
+
} from './sprites-mesa.js';
|
|
14
|
+
|
|
15
|
+
/** Quem foi pulado aparece meio transparente. */
|
|
16
|
+
const ALFA_DO_PULADO = 0.45;
|
|
17
|
+
/** O sinal sobre a cabeça, por ação; quem entrega ou comemora não tem sinal. */
|
|
18
|
+
const SINAIS = Object.freeze({ done: SINAL_FEITO, failed: SINAL_FALHA, 'sem-sinal': SINAL_DUVIDA });
|
|
19
|
+
/** A tela do monitor, pela cor que o quadro manda pintar agora; apagado é o próprio monitor. */
|
|
20
|
+
const TELAS = Object.freeze({
|
|
21
|
+
aceso: (fase) => TELAS_ACESAS[fase % TELAS_ACESAS.length],
|
|
22
|
+
ambar: () => TELA_AMBAR,
|
|
23
|
+
vermelho: () => TELA_VERMELHA,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const largura = (desenho) => desenho.linhas[0].length;
|
|
27
|
+
const altura = (desenho) => desenho.linhas.length;
|
|
28
|
+
const meio = (coluna) => coluna * LARGURA_COLUNA + LARGURA_COLUNA / 2;
|
|
29
|
+
const centrado = (desenho, x, y) => ({ desenho, x: x - Math.floor(largura(desenho) / 2), y });
|
|
30
|
+
|
|
31
|
+
// Na parede, uma janela sobre cada coluna de mesas e o relógio no meio; no chão, encostadas na
|
|
32
|
+
// parede e fora do caminho de quem anda, duas plantas.
|
|
33
|
+
const DECORACAO = Object.freeze([
|
|
34
|
+
...Array.from({ length: COLUNAS }, (_, coluna) => centrado(JANELA, meio(coluna), 3)),
|
|
35
|
+
centrado(RELOGIO, LARGURA / 2, 3),
|
|
36
|
+
centrado(PLANTA, LARGURA_COLUNA, 6),
|
|
37
|
+
centrado(PLANTA, LARGURA - LARGURA_COLUNA, 6),
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
/** Preenche uma matriz de pixels com o canto superior esquerdo em (x, y). */
|
|
41
|
+
function pintar(ctx, { paleta, linhas }, x, y) {
|
|
42
|
+
for (const [dx, dy, l, a, letra] of retangulos(linhas)) {
|
|
43
|
+
ctx.fillStyle = paleta[letra];
|
|
44
|
+
ctx.fillRect(x + dx, y + dy, l, a);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** O piso: ladrilhos claros e escuros em xadrez, da parede até o fim da tela. */
|
|
49
|
+
function pintarPiso(ctx) {
|
|
50
|
+
for (let y = TOPO, linha = 0; y < ALTURA; y += altura(LADRILHO), linha++) {
|
|
51
|
+
for (let x = 0, coluna = 0; x < LARGURA; x += largura(LADRILHO), coluna++) {
|
|
52
|
+
pintar(ctx, (linha + coluna) % 2 ? LADRILHO_ESCURO : LADRILHO, x, y);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function pintarSala(ctx) {
|
|
58
|
+
for (let x = 0; x < LARGURA; x += largura(PAREDE)) pintar(ctx, PAREDE, x, 0);
|
|
59
|
+
pintarPiso(ctx);
|
|
60
|
+
for (const { desenho, x, y } of DECORACAO) pintar(ctx, desenho, x, y);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A mesa de um agente, com o monitor na cor e na fase que o quadro traz. */
|
|
64
|
+
function pintarMesa(ctx, { mesa, monitor, monitorFase }) {
|
|
65
|
+
const [x, y] = [mesa.retangulo.x + MONITOR_EM.x, mesa.retangulo.y + MONITOR_EM.y];
|
|
66
|
+
pintar(ctx, MESA, mesa.retangulo.x, mesa.retangulo.y - MESA_ACIMA);
|
|
67
|
+
pintar(ctx, MONITOR, x, y);
|
|
68
|
+
const tela = TELAS[monitor]?.(monitorFase);
|
|
69
|
+
if (tela) pintar(ctx, tela, x + TELA_EM.x, y + TELA_EM.y);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** O boneco na pose do quadro, com a sombra no chão (se está em pé) e o sinal sobre a cabeça. */
|
|
73
|
+
function pintarBoneco(ctx, boneco) {
|
|
74
|
+
const { x, y, acao, pose, papel, espelhado } = boneco;
|
|
75
|
+
const linhas = linhasDoBoneco(pose, { papel, espelhado, festa: acao === 'comemorando' && pose === 'mao' });
|
|
76
|
+
const sinal = SINAIS[acao];
|
|
77
|
+
if (!boneco.sentado) pintar(ctx, SOMBRA, x + Math.floor((BONECO - largura(SOMBRA)) / 2), y + BONECO);
|
|
78
|
+
ctx.globalAlpha = acao === 'skipped' ? ALFA_DO_PULADO : 1;
|
|
79
|
+
pintar(ctx, { linhas, paleta: coresDoBoneco(boneco.aparencia) }, x, y);
|
|
80
|
+
ctx.globalAlpha = 1;
|
|
81
|
+
if (sinal) pintar(ctx, sinal, x + Math.floor((BONECO - largura(sinal)) / 2), y - altura(sinal) - 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Pinta um quadro inteiro, em pixel lógico: a sala e, do fundo para a frente, mesas e bonecos.
|
|
86
|
+
* Quem está mais abaixo na tela fica na frente: o boneco sentado é pintado antes da própria
|
|
87
|
+
* mesa (o tampo cobre as pernas dele) e quem entrega, parado diante de outra mesa, depois dela.
|
|
88
|
+
* @param {object} ctx o contexto 2D (ou qualquer objeto com `fillStyle`, `globalAlpha` e `fillRect`)
|
|
89
|
+
* @param {object[]} bonecos o que `quadro` devolveu; só há mesa para quem está nesta lista
|
|
90
|
+
*/
|
|
91
|
+
export function pintarCena(ctx, bonecos) {
|
|
92
|
+
pintarSala(ctx);
|
|
93
|
+
const camadas = bonecos.flatMap((boneco) => [
|
|
94
|
+
{ base: boneco.mesa.retangulo.y + boneco.mesa.retangulo.altura, pintar: pintarMesa, boneco },
|
|
95
|
+
{ base: boneco.y + BONECO, pintar: pintarBoneco, boneco },
|
|
96
|
+
]);
|
|
97
|
+
camadas.sort((a, b) => a.base - b.base);
|
|
98
|
+
for (const camada of camadas) camada.pintar(ctx, camada.boneco);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** O que muda o desenho de um boneco: se nada disto mudou, o quadro é o mesmo. */
|
|
102
|
+
const resumo = (b) => [b.id, b.indice, b.acao, b.pose, b.papel, b.espelhado, b.x, b.y, b.monitor, b.monitorFase].join(':');
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* O laço de animação. A cada quadro do navegador pede os bonecos e, se algo mudou desde a última
|
|
106
|
+
* pintura (ou o canvas mudou de tamanho), pinta na escala inteira do canvas, sem suavização.
|
|
107
|
+
* Um quadro que falha não para o laço.
|
|
108
|
+
* @param {object} janela de quem vem o `requestAnimationFrame`
|
|
109
|
+
* @param {object} tela o canvas, já com 320·N × 180·N pixels
|
|
110
|
+
* @param {() => object[] | null} obterQuadro os bonecos deste instante; `null`: ainda não há o que pintar
|
|
111
|
+
* @param {() => void} aoPintar chamada depois de cada pintura
|
|
112
|
+
*/
|
|
113
|
+
export function animar(janela, tela, obterQuadro, aoPintar) {
|
|
114
|
+
const ctx = tela.getContext('2d');
|
|
115
|
+
let pintado = '';
|
|
116
|
+
function passo() {
|
|
117
|
+
try {
|
|
118
|
+
const bonecos = obterQuadro();
|
|
119
|
+
const marca = bonecos ? `${tela.width}|${bonecos.map(resumo).join(';')}` : pintado;
|
|
120
|
+
if (marca === pintado) return;
|
|
121
|
+
const n = tela.width / LARGURA;
|
|
122
|
+
ctx.setTransform(n, 0, 0, n, 0, 0);
|
|
123
|
+
ctx.imageSmoothingEnabled = false;
|
|
124
|
+
pintarCena(ctx, bonecos);
|
|
125
|
+
pintado = marca;
|
|
126
|
+
aoPintar();
|
|
127
|
+
} finally {
|
|
128
|
+
janela.requestAnimationFrame(passo);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
janela.requestAnimationFrame(passo);
|
|
132
|
+
}
|