@aksp/opencrew 1.6.3 → 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 +51 -0
- package/README.md +50 -5
- package/package.json +1 -1
- package/templates/AGENTS.md +20 -6
- package/templates/_opencrew/.opencrew-version +1 -1
- 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/repair.prompt.md +7 -12
- package/templates/_opencrew/core/runner.pipeline.md +45 -128
- 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/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,57 @@
|
|
|
3
3
|
All notable changes to opencrew are documented here.
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
5
5
|
|
|
6
|
+
## [1.7.0] — 2026-10-06
|
|
7
|
+
|
|
8
|
+
Fase E1 "Escritório ao vivo — a equipe trabalhando, em 8 bits"
|
|
9
|
+
(`specs/fase-e1-escritorio-ao-vivo.md`). Chega a quem já usa com um
|
|
10
|
+
`npx @aksp/opencrew@latest update`.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Escritório ao vivo.** Uma página em pixel-art, aberta no navegador, mostra a crew
|
|
14
|
+
trabalhando: cada agente na sua mesa, digitando na vez dele, levando o papel ao colega na
|
|
15
|
+
passagem de bastão, de mão levantada quando espera a sua resposta, com ✓ quando termina e "!"
|
|
16
|
+
quando falha. Ao lado, o passo atual, a lista dos agentes com o status por extenso e o que cada
|
|
17
|
+
um fez, e a última passagem de bastão. O título da aba acompanha a execução.
|
|
18
|
+
- **`/opencrew dashboard`** liga o Escritório, sobe a página e mostra o endereço
|
|
19
|
+
(`http://127.0.0.1:4747`, ou a porta livre seguinte); repetir o comando devolve o mesmo
|
|
20
|
+
endereço. **`/opencrew dashboard off`** desliga. Continua desligado por padrão.
|
|
21
|
+
- Roda só no seu computador, sem internet e sem medição: o servidor
|
|
22
|
+
(`_opencrew/core/scripts/escritorio.mjs`) escuta só em `127.0.0.1`, só lê e não escreve em disco.
|
|
23
|
+
- Sem execução nenhuma, a página roda uma demonstração e troca sozinha para a execução real
|
|
24
|
+
quando ela aparece (`?demo` no endereço força a demonstração). Com mais de uma crew, mostra a
|
|
25
|
+
de atualização mais recente e um seletor com as outras.
|
|
26
|
+
- A página não mente sobre o que não sabe: execução há mais de 2 minutos sem novidade mostra há
|
|
27
|
+
quanto tempo foi a última atualização; há mais de 20, o agente sai da pose de digitar e aparece
|
|
28
|
+
"sem sinal". Servidor fora do ar: a página avisa e tenta de novo sozinha.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- **Quem avisa o Escritório é um script, não a IA escrevendo JSON.** Com o Escritório ligado, o
|
|
32
|
+
runner roda um comando curto por passo (`_opencrew/core/scripts/estado.mjs`). Checkpoint, agente
|
|
33
|
+
pulado e execução que falha passam a aparecer; antes nunca eram gravados. Falha desse comando
|
|
34
|
+
não para a execução: o runner avisa uma vez e segue.
|
|
35
|
+
- **Com o Escritório ligado, o estado final deixa de ser copiado para
|
|
36
|
+
`crews/<crew>/output/<run>/state.json`.** O estado da execução mora só em
|
|
37
|
+
`crews/<crew>/state.json`.
|
|
38
|
+
- `state.json`: os agentes ganham os status `checkpoint` e `failed` e o campo `label`; a
|
|
39
|
+
execução ganha `checkpoint` e `failed`; `desk` e `delivering` deixam de ser gravados. Arquivo
|
|
40
|
+
gravado por versão anterior continua sendo lido pela página.
|
|
41
|
+
- Os prompts de criar e de consertar crew não escrevem mais `state.json`.
|
|
42
|
+
- README: a nota "o dashboard não é instalado" deu lugar à seção "Escritório ao vivo".
|
|
43
|
+
|
|
44
|
+
### Removed
|
|
45
|
+
- A pasta `dashboard/` do repositório (o desenho antigo, que nunca foi instalado pelo `init`). O
|
|
46
|
+
que servia migrou para `_opencrew/core/escritorio/`, sem os defeitos apontados na auditoria:
|
|
47
|
+
nome de agente entrava na página como HTML, o modo ao vivo lia o arquivo no lugar errado e a
|
|
48
|
+
página congelava se a primeira leitura falhasse.
|
|
49
|
+
|
|
50
|
+
### Internal
|
|
51
|
+
- `npm run verify`: o lint passa a cobrir `templates/_opencrew/core/escritorio/` (com os nomes
|
|
52
|
+
globais de navegador) e o alerta de tamanho mede os `.js` dessa pasta. Travas novas:
|
|
53
|
+
`tests/estado*.test.js`, `tests/escritorio*.test.js`, `tests/runtime-contracts-e1.test.js`,
|
|
54
|
+
`tests/docs-e1.test.js` e os cenários E1-07a, E1-07c e E1-upg nos testes de pacote, de
|
|
55
|
+
referências e de upgrade. A trava de conteúdo do mantenedor passa a ler também `.mjs` e `.css`.
|
|
56
|
+
|
|
6
57
|
## [1.6.3] — 2026-10-06
|
|
7
58
|
|
|
8
59
|
**O Node mínimo subiu para o 20.17, numa versão de correção.** O pacote dizia 20.0, mas a lista
|
package/README.md
CHANGED
|
@@ -40,6 +40,9 @@ dentro da sua IDE.**
|
|
|
40
40
|
termos que você proibiu e `[PREENCHER]` pendentes, e aponta afirmações a confirmar.
|
|
41
41
|
Bloqueio não passa, seja qual for a nota do revisor. A crew não inventa casos nem números:
|
|
42
42
|
quando falta um dado real, ela pergunta na aprovação final.
|
|
43
|
+
- 🖥️ **Escritório ao vivo** — veja a equipe trabalhando numa sala em pixel-art, no navegador:
|
|
44
|
+
quem está digitando, quem passou o bastão, quem espera a sua resposta. Opcional, desligado por
|
|
45
|
+
padrão, só neste computador. Liga com `/opencrew dashboard`.
|
|
43
46
|
- 📂 **Crew que conhece o projeto** — liste em `fontes:` os arquivos e pastas do seu projeto
|
|
44
47
|
(decisões, calendário, manual de marca) e a crew os lê em todo run, tratando-os como verdade.
|
|
45
48
|
Reorganizou as pastas? No início do run ela confere os caminhos, acha para onde o arquivo foi
|
|
@@ -175,6 +178,8 @@ meu-projeto/
|
|
|
175
178
|
│ │ ├── skills.engine.md ← gerenciador de skills
|
|
176
179
|
│ │ ├── architect.agent.yaml ← definição do Arquiteto
|
|
177
180
|
│ │ ├── best-practices/ ← 22 guias de melhores práticas + _catalog.yaml
|
|
181
|
+
│ │ ├── scripts/ ← verificador, conferência de fontes e os scripts do Escritório
|
|
182
|
+
│ │ ├── escritorio/ ← página do Escritório ao vivo (abre com /opencrew dashboard)
|
|
178
183
|
│ │ └── prompts/ ← 13 prompts de fase (discovery, design, build, etc.)
|
|
179
184
|
│ ├── agents/ ← 5 agentes base compartilhados
|
|
180
185
|
│ │ ├── researcher.agent.md
|
|
@@ -184,7 +189,7 @@ meu-projeto/
|
|
|
184
189
|
│ │ └── strategist.agent.md
|
|
185
190
|
│ ├── _memory/
|
|
186
191
|
│ │ ├── company.md ← perfil da sua empresa (onboarding)
|
|
187
|
-
│ │ └── preferences.md ← idioma, tier padrão,
|
|
192
|
+
│ │ └── preferences.md ← idioma, tier padrão, Escritório ligado ou desligado
|
|
188
193
|
│ └── .opencrew-version
|
|
189
194
|
│
|
|
190
195
|
├── crews/ ← suas crews vivem aqui
|
|
@@ -202,9 +207,47 @@ meu-projeto/
|
|
|
202
207
|
│ └── ...
|
|
203
208
|
```
|
|
204
209
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Escritório ao vivo
|
|
213
|
+
|
|
214
|
+
Quer ver a equipe trabalhando? O **Escritório** é uma página em pixel-art, aberta no navegador,
|
|
215
|
+
em que cada agente tem a sua mesa: digita quando é a vez dele, leva o papel ao colega na passagem
|
|
216
|
+
de bastão e levanta a mão quando espera uma resposta sua. Ao lado do desenho ficam o passo atual,
|
|
217
|
+
a lista dos agentes (com o status por extenso e o que cada um fez) e a última passagem de bastão.
|
|
218
|
+
|
|
219
|
+
**Como abrir:** no chat da sua IDE, digite `/opencrew dashboard`. O comando liga o Escritório,
|
|
220
|
+
sobe a página e mostra o endereço — `http://127.0.0.1:4747`, ou a porta livre seguinte. A próxima
|
|
221
|
+
execução de crew aparece ali; enquanto não há nenhuma, a página roda uma demonstração. Repetir o
|
|
222
|
+
comando é seguro: ele devolve o mesmo endereço.
|
|
223
|
+
|
|
224
|
+
Se a sua IDE não roda comando em segundo plano, ela mostra o comando para você rodar em outro
|
|
225
|
+
terminal, na pasta do projeto:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
node _opencrew/core/scripts/escritorio.mjs # Ctrl+C para fechar
|
|
229
|
+
node _opencrew/core/scripts/escritorio.mjs --porta 5000
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Para desligar: `/opencrew dashboard off`.
|
|
233
|
+
|
|
234
|
+
O que vale saber antes de ligar:
|
|
235
|
+
|
|
236
|
+
- **Vem desligado.** Sem o `/opencrew dashboard`, nada muda nas suas execuções.
|
|
237
|
+
- **Roda só neste computador, sem internet e sem medição.** A página é servida em `127.0.0.1`,
|
|
238
|
+
só lê o estado das suas crews (`crews/<crew>/state.json`), não carrega nada de fora e não envia
|
|
239
|
+
dado nenhum para lugar nenhum.
|
|
240
|
+
- **Com ele ligado, cada passo custa um comando curto a mais.** É assim que a IA avisa o que está
|
|
241
|
+
fazendo: um comando de terminal por passo. Em IDE que pede aprovação a cada comando, libere o
|
|
242
|
+
`estado.mjs` uma vez.
|
|
243
|
+
- **A tela mostra o que a IA avisa, e pode atrasar.** Se a IA pular um aviso, o desenho só se
|
|
244
|
+
acerta no passo seguinte. Depois de 2 minutos sem novidade a página diz há quanto tempo foi a
|
|
245
|
+
última atualização; depois de 20, o agente aparece "sem sinal" — o que não quer dizer que
|
|
246
|
+
travou: um passo longo é normal.
|
|
247
|
+
- **O Escritório nunca para a execução.** Se o aviso falhar, a crew segue trabalhando.
|
|
248
|
+
- **Até 12 mesas.** Numa crew maior, os agentes a mais aparecem só na lista ao lado.
|
|
249
|
+
|
|
250
|
+
Quem já usa o OpenCrew recebe o Escritório com um `npx @aksp/opencrew@latest update`.
|
|
208
251
|
|
|
209
252
|
---
|
|
210
253
|
|
|
@@ -284,7 +327,9 @@ npx @aksp/opencrew update --check
|
|
|
284
327
|
| `/opencrew delete <nome>` | Remove uma crew |
|
|
285
328
|
| `/opencrew skills` | Navega, instala ou remove skills |
|
|
286
329
|
| `/opencrew install <skill>` | Instala uma skill do catálogo |
|
|
287
|
-
| `/opencrew settings` | Altera preferências (idioma, tier,
|
|
330
|
+
| `/opencrew settings` | Altera preferências (idioma, tier, Escritório) |
|
|
331
|
+
| `/opencrew dashboard` | Liga e abre o Escritório ao vivo (a equipe trabalhando, no navegador) |
|
|
332
|
+
| `/opencrew dashboard off` | Desliga o Escritório |
|
|
288
333
|
| `/opencrew show-company` | Mostra o perfil da empresa |
|
|
289
334
|
| `/opencrew edit-company` | Reconfigura o perfil da empresa |
|
|
290
335
|
| `/opencrew help` | Mostra a lista de comandos |
|
package/package.json
CHANGED
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
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Demonstração do escritório (regra 23): um roteiro fixo e uma função pura que devolve o estado
|
|
2
|
+
// de cada instante, no formato do `state.json`. O roteiro passa por todos os status de agente,
|
|
3
|
+
// por um checkpoint, por três passagens de bastão, por uma falha e pela execução concluída, e
|
|
4
|
+
// recomeça. A página o desenha pelo mesmo caminho de uma execução de verdade.
|
|
5
|
+
// Puro: sem DOM, sem relógio, sem rede.
|
|
6
|
+
// Spec: fase-e1-escritorio-ao-vivo.md, regras 18 e 23 (repositório do OpenCrew).
|
|
7
|
+
|
|
8
|
+
const ELENCO = [
|
|
9
|
+
{ id: 'pesquisa', name: 'Pedro Pesquisa', icon: '🔎' },
|
|
10
|
+
{ id: 'redacao', name: 'Rita Redação', icon: '✍️' },
|
|
11
|
+
{ id: 'design', name: 'Davi Design', icon: '🎨' },
|
|
12
|
+
{ id: 'revisao', name: 'Renata Revisão', icon: '🧐' },
|
|
13
|
+
{ id: 'social', name: 'Sara Social', icon: '📣' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const [I, W, C, D, S, F] = ['idle', 'working', 'checkpoint', 'done', 'skipped', 'failed'];
|
|
17
|
+
|
|
18
|
+
// Cada cena: duração, status da execução, passo (`n` e `rotulo`) e o status de cada agente, na
|
|
19
|
+
// ordem do elenco. `passagem` ([de, para, mensagem]) marca a cena em que o bastão troca de mão.
|
|
20
|
+
const CENAS = [
|
|
21
|
+
{ ms: 3000, status: 'running', n: 0, rotulo: '', agentes: [I, I, I, I, S] },
|
|
22
|
+
{ ms: 6000, status: 'running', n: 1, rotulo: 'Pesquisar o tema', agentes: [W, I, I, I, S] },
|
|
23
|
+
{ ms: 5000, status: 'checkpoint', n: 1, rotulo: 'Aprovar a pauta', agentes: [C, I, I, I, S] },
|
|
24
|
+
{ ms: 7000, status: 'running', n: 2, rotulo: 'Escrever o texto', agentes: [D, W, I, I, S], passagem: ['pesquisa', 'redacao', 'Pauta aprovada, com três fontes.'] },
|
|
25
|
+
{ ms: 7000, status: 'running', n: 3, rotulo: 'Criar a imagem', agentes: [D, D, W, I, S], passagem: ['redacao', 'design', 'Texto pronto, com título e chamada.'] },
|
|
26
|
+
{ ms: 5000, status: 'failed', n: 3, rotulo: 'Criar a imagem', agentes: [D, D, F, I, S], motivo: 'Sem acesso ao banco de imagens.' },
|
|
27
|
+
{ ms: 6000, status: 'running', n: 3, rotulo: 'Criar a imagem', agentes: [D, D, W, I, S] },
|
|
28
|
+
{ ms: 7000, status: 'running', n: 4, rotulo: 'Revisar tudo', agentes: [D, D, D, W, S], passagem: ['design', 'revisao', 'Imagem pronta, no formato do post.'] },
|
|
29
|
+
{ ms: 8000, status: 'completed', n: 4, rotulo: 'Revisar tudo', agentes: [D, D, D, D, S] },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/** O roteiro, como dado: o elenco e as cenas, na ordem. */
|
|
33
|
+
export const ROTEIRO = Object.freeze({ crew: 'crew-de-exemplo', total: 4, agentes: ELENCO, cenas: CENAS });
|
|
34
|
+
|
|
35
|
+
const INICIOS = CENAS.map((_, i) => CENAS.slice(0, i).reduce((soma, cena) => soma + cena.ms, 0));
|
|
36
|
+
/** Duração de uma volta do roteiro. */
|
|
37
|
+
export const DURACAO_DEMO_MS = INICIOS.at(-1) + CENAS.at(-1).ms;
|
|
38
|
+
// Data fixa das passagens: só precisa mudar de uma passagem para a outra.
|
|
39
|
+
const BASE = Date.UTC(2026, 0, 5, 9);
|
|
40
|
+
|
|
41
|
+
/** Índice da cena em que cai o instante; o roteiro recomeça ao chegar ao fim. */
|
|
42
|
+
function cenaEm(decorridoMs) {
|
|
43
|
+
const t = Number.isFinite(decorridoMs) && decorridoMs > 0 ? decorridoMs % DURACAO_DEMO_MS : 0;
|
|
44
|
+
return INICIOS.findLastIndex((inicio) => inicio <= t);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** O `label` do agente: o rótulo do último passo em que ele trabalhou, até a cena `ate`. */
|
|
48
|
+
function rotuloDe(agente, ate) {
|
|
49
|
+
const feitas = CENAS.slice(0, ate + 1).filter((cena) => cena.agentes[agente] === W);
|
|
50
|
+
return feitas.length ? feitas.at(-1).rotulo : '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A última passagem de bastão até a cena `ate`, no formato do `handoff`. */
|
|
54
|
+
function passagemAte(ate) {
|
|
55
|
+
const cena = CENAS.slice(0, ate + 1).findLastIndex((c) => c.passagem);
|
|
56
|
+
if (cena < 0) return null;
|
|
57
|
+
const [from, to, message] = CENAS[cena].passagem;
|
|
58
|
+
return { from, to, message, completedAt: new Date(BASE + INICIOS[cena]).toISOString() };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* O estado da demonstração num instante. Mesmo instante, mesmo estado; depois de
|
|
63
|
+
* `DURACAO_DEMO_MS` o roteiro recomeça. Vem sem `updatedAt`: a demonstração nunca fica "sem sinal".
|
|
64
|
+
* @param {number} decorridoMs tempo desde que a demonstração começou (inválido vale como 0)
|
|
65
|
+
* @returns {object} um estado no formato do `state.json`
|
|
66
|
+
*/
|
|
67
|
+
export function estadoDaDemo(decorridoMs) {
|
|
68
|
+
const indice = cenaEm(decorridoMs);
|
|
69
|
+
const cena = CENAS[indice];
|
|
70
|
+
return {
|
|
71
|
+
crew: ROTEIRO.crew,
|
|
72
|
+
status: cena.status,
|
|
73
|
+
step: { current: cena.n, total: ROTEIRO.total, label: cena.rotulo },
|
|
74
|
+
agents: ELENCO.map((agente, i) => ({ ...agente, status: cena.agentes[i], label: rotuloDe(i, indice) })),
|
|
75
|
+
handoff: passagemAte(indice),
|
|
76
|
+
...(cena.motivo ? { motivo: cena.motivo } : {}),
|
|
77
|
+
updatedAt: null,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Escala do canvas (regra 16): a tela interna de 320×180 só é ampliada em escala inteira, e a
|
|
2
|
+
// conta é feita em pixels do dispositivo, para o pixel continuar quadrado com a tela do sistema
|
|
3
|
+
// em 125% ou 150%.
|
|
4
|
+
// Puro: sem DOM, sem relógio, sem rede. O tamanho disponível e a razão de pixels entram por
|
|
5
|
+
// parâmetro.
|
|
6
|
+
// Spec: fase-e1-escritorio-ao-vivo.md, regra 16 (repositório do OpenCrew).
|
|
7
|
+
import { ALTURA, LARGURA } from './modelo-mesas.js';
|
|
8
|
+
|
|
9
|
+
// Folga da conta com decimais: um canvas do tamanho exato que esta função devolveu tem de caber
|
|
10
|
+
// de novo (com razão 1,7 e escala 5, a divisão dá 4,999999999999999).
|
|
11
|
+
const FOLGA = 1e-9;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A maior escala inteira em que a tela cabe no espaço disponível.
|
|
15
|
+
* @param {number} largura largura disponível, em pixels de CSS
|
|
16
|
+
* @param {number} altura altura disponível, em pixels de CSS
|
|
17
|
+
* @param {number} dpr pixels do dispositivo por pixel de CSS; inválido vale como 1
|
|
18
|
+
* @returns {{ n: number, largura: number, altura: number }} `n`: o maior inteiro N ≥ 1 em que
|
|
19
|
+
* 320·N × 180·N cabe em largura·dpr × altura·dpr (se nem N = 1 cabe, 1: a página rola) ·
|
|
20
|
+
* `largura` e `altura`: o tamanho do canvas em pixels de CSS (320·N/dpr × 180·N/dpr)
|
|
21
|
+
*/
|
|
22
|
+
export function escala(largura, altura, dpr) {
|
|
23
|
+
const razao = Number.isFinite(dpr) && dpr > 0 ? dpr : 1;
|
|
24
|
+
const cabe = Math.min((largura * razao) / LARGURA, (altura * razao) / ALTURA);
|
|
25
|
+
const n = Number.isFinite(cabe) ? Math.max(1, Math.floor(cabe + FOLGA)) : 1;
|
|
26
|
+
return { n, largura: (LARGURA * n) / razao, altura: (ALTURA * n) / razao };
|
|
27
|
+
}
|