primocode 10.14.0 → 10.15.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/LICENSES/robo.txt +19 -0
- package/README.md +6 -2
- package/bin/primocode.js +46 -3
- package/lib/act.js +9 -2
- package/lib/estudio/cota.js +10 -3
- package/lib/estudio/editor.js +5 -0
- package/lib/estudio/index.js +9 -2
- package/lib/estudio/projetos.js +13 -1
- package/lib/limites.js +100 -0
- package/lib/marca-kit.js +3 -0
- package/lib/planos.js +10 -8
- package/lib/preparar.js +2 -0
- package/lib/voz-ponte.js +15 -0
- package/lib/voz.js +75 -5
- package/package.json +3 -2
- package/voz/primo_voz/__main__.py +307 -54
- package/voz/primo_voz/conversa.py +236 -0
- package/voz/primo_voz/escuta.py +32 -2
- package/voz/primo_voz/hud.html +550 -0
- package/voz/primo_voz/hud.py +224 -0
- package/voz/primo_voz/identidade.py +88 -0
- package/voz/primo_voz/luz.py +137 -0
- package/voz/primo_voz/painel.py +459 -0
- package/voz/primo_voz/painel_tui.py +26 -0
- package/voz/primo_voz/sons.py +96 -0
- package/voz/primo_voz/tela.py +337 -0
- package/voz/primo_voz/vendor/RobotExpressive.glb +0 -0
- package/voz/primo_voz/vendor/spline-runtime.min.js +8219 -0
- package/voz/primo_voz/vendor/three-robo.min.js +3824 -0
- package/voz/primo_voz/vendor/three.LICENSE.txt +21 -0
- package/voz/primo_voz/vivo.py +34 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="pt-BR">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Primo</title>
|
|
6
|
+
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--fundo:#131312; --cartao:#1c1c1a; --cartao2:#232321; --borda:#2e2e2b;
|
|
10
|
+
--texto:#ecebe6; --apagado:#a3a29b; --fraco:#74736d;
|
|
11
|
+
--marca:#22d3ee; --marca-clara:#67e8f9; --ok:#7ad7a3; --erro:#f08a7e; --raio:28px;
|
|
12
|
+
--serifa:"Instrument Serif", Georgia, serif; --sans:"Inter", -apple-system, system-ui, sans-serif; --mono:"JetBrains Mono", Menlo, monospace;
|
|
13
|
+
}
|
|
14
|
+
* { box-sizing:border-box; margin:0; }
|
|
15
|
+
html,body { height:100%; background:var(--fundo); color:var(--texto); font-family:var(--sans); -webkit-font-smoothing:antialiased; user-select:none; -webkit-user-select:none; }
|
|
16
|
+
body { background:radial-gradient(ellipse 60% 55% at 50% 42%, #1a2a2c 0%, var(--fundo) 62%); overflow:hidden; }
|
|
17
|
+
.tela { height:100vh; padding:38px 28px 24px; display:grid; gap:18px; grid-template-columns:minmax(250px,22%) 1fr minmax(280px,25%); grid-template-rows:minmax(0,1fr) auto auto; }
|
|
18
|
+
.cartao { background:linear-gradient(180deg,var(--cartao2),var(--cartao)); border:1px solid var(--borda); border-radius:var(--raio); padding:18px 22px; box-shadow:0 1px 0 rgba(255,255,255,.04) inset, 0 20px 40px -24px rgba(0,0,0,.8); }
|
|
19
|
+
.rotulo { font-size:11px; font-weight:600; letter-spacing:.12em; text-transform:uppercase; color:var(--fraco); }
|
|
20
|
+
|
|
21
|
+
/* esquerda */
|
|
22
|
+
.esq { display:flex; flex-direction:column; gap:18px; min-height:0; }
|
|
23
|
+
.cal-topo { display:flex; align-items:baseline; justify-content:space-between; margin-bottom:12px; }
|
|
24
|
+
.cal-mes { font-family:var(--serifa); font-size:30px; line-height:1; text-transform:capitalize; } .cal-mes i { color:var(--marca); }
|
|
25
|
+
.cal { display:grid; grid-template-columns:repeat(7,1fr); gap:5px 0; text-align:center; font-size:13px; font-variant-numeric:tabular-nums; }
|
|
26
|
+
.cal .sem { color:var(--fraco); font-size:11px; font-weight:600; padding-bottom:3px; height:auto; }
|
|
27
|
+
.cal span { height:30px; display:grid; place-items:center; color:var(--apagado); border-radius:50%; width:30px; margin:auto; position:relative; }
|
|
28
|
+
.cal .fds { color:var(--fraco); }
|
|
29
|
+
.cal .hoje { background:var(--marca); color:#06191c; font-weight:700; }
|
|
30
|
+
.cal .tem::after { content:""; width:4px; height:4px; border-radius:50%; background:var(--marca); position:absolute; bottom:1px; }
|
|
31
|
+
.cal .hoje.tem::after { background:#06191c; }
|
|
32
|
+
.hoje-card { flex:1; min-height:0; overflow:hidden; }
|
|
33
|
+
.hoje-lista { display:flex; flex-direction:column; gap:12px; margin-top:12px; }
|
|
34
|
+
.ev { display:flex; gap:12px; align-items:center; }
|
|
35
|
+
.ev .barra { width:3px; align-self:stretch; border-radius:2px; background:#3d3d39; }
|
|
36
|
+
.ev.prox .barra { background:var(--marca); }
|
|
37
|
+
.ev b { display:block; font-size:14px; font-weight:600; } .ev small { color:var(--apagado); font-size:12px; } .ev.prox small { color:var(--marca-clara); }
|
|
38
|
+
.vazio { color:var(--apagado); font-size:13px; line-height:1.5; }
|
|
39
|
+
.vazio button { margin-top:10px; display:block; background:var(--marca); color:#06191c; border:0; border-radius:14px; padding:8px 14px; font:600 12px var(--sans); cursor:pointer; }
|
|
40
|
+
|
|
41
|
+
/* centro */
|
|
42
|
+
.centro { display:grid; place-items:center; position:relative; min-height:0; cursor:pointer; }
|
|
43
|
+
.palco { position:absolute; inset:0 0 86px 0; display:grid; place-items:center; }
|
|
44
|
+
.palco > * { grid-area:1/1; }
|
|
45
|
+
.centro svg { height:min(62vh,600px); max-width:100%; width:auto; filter:drop-shadow(0 30px 40px rgba(0,0,0,.6)); }
|
|
46
|
+
.nome { position:absolute; bottom:4px; text-align:center; }
|
|
47
|
+
.nome h1 { font-family:var(--serifa); font-weight:400; font-size:44px; letter-spacing:.02em; }
|
|
48
|
+
.nome p { color:var(--apagado); font-size:13px; }
|
|
49
|
+
.nome p::before { content:""; display:inline-block; width:7px; height:7px; border-radius:50%; background:var(--ok); margin-right:8px; box-shadow:0 0 10px var(--ok); }
|
|
50
|
+
.nome[data-e="pensando"] p::before, .nome[data-e="executando"] p::before { background:#f5b94a; box-shadow:0 0 10px #f5b94a; }
|
|
51
|
+
.nome[data-e="dormindo"] p::before { background:var(--fraco); box-shadow:none; }
|
|
52
|
+
.fala { position:absolute; top:0; left:50%; transform:translateX(-50%); max-width:70%; text-align:center; color:var(--apagado); font-size:13px; line-height:1.4; max-height:3em; overflow:hidden; }
|
|
53
|
+
/* robô 3D: canvas transparente sobre o palco; o SVG só aparece se o WebGL falhar */
|
|
54
|
+
#robo3d { width:100%; height:100%; display:none; -webkit-mask-image:linear-gradient(#000 82%,transparent); mask-image:linear-gradient(#000 82%,transparent); }
|
|
55
|
+
.tres-d #robo3d { display:block; } .tres-d #svg-robo { display:none; }
|
|
56
|
+
/* robô do Spline (cena da comunidade): canvas transparente, brilho ciano por CSS, busto pelo zoom da câmera */
|
|
57
|
+
#robo-spline { width:100% !important; height:100% !important; visibility:hidden; background:transparent; --nv:0; -webkit-mask-image:linear-gradient(#000 44%,rgba(0,0,0,.55) 58%,transparent 76%); mask-image:linear-gradient(#000 44%,rgba(0,0,0,.55) 58%,transparent 76%); filter:drop-shadow(0 0 18px rgba(34,211,238,.22)); transition:filter .4s; }
|
|
58
|
+
.spline #robo-spline { visibility:visible; } .spline #robo3d, .spline #svg-robo { display:none; }
|
|
59
|
+
.palco > #robo-spline { grid-area:1/1; }
|
|
60
|
+
.spline.e-ouvindo #robo-spline { filter:drop-shadow(0 0 26px rgba(34,211,238,.55)); }
|
|
61
|
+
.spline.e-pensando #robo-spline, .spline.e-executando #robo-spline { filter:drop-shadow(0 0 22px rgba(34,211,238,.38)); animation:sp-pensa 1.8s ease-in-out infinite; }
|
|
62
|
+
.spline.e-falando #robo-spline { filter:drop-shadow(0 0 calc(18px + var(--nv)*26px) rgba(34,211,238,calc(.4 + var(--nv)*.5))); transition:none; }
|
|
63
|
+
@keyframes sp-pensa { 50% { opacity:.82; } }
|
|
64
|
+
.chao { position:absolute; left:50%; bottom:2%; width:min(46vh,420px); height:70px; transform:translateX(-50%); background:radial-gradient(closest-side,rgba(34,211,238,.20),rgba(34,211,238,.05) 60%,transparent); pointer-events:none; }
|
|
65
|
+
/* robô simples (fallback) : camadas, respiração, boot */
|
|
66
|
+
#r-tudo { animation:respira 4.2s ease-in-out infinite; }
|
|
67
|
+
@keyframes respira { 0%,100% { transform:translateY(0) } 50% { transform:translateY(-5px) } }
|
|
68
|
+
.peca { transition:opacity .35s ease var(--d,0s), transform .6s cubic-bezier(.2,.9,.3,1.15) var(--d,0s); }
|
|
69
|
+
.desmontado .peca { opacity:0; transform:translateY(var(--dy,0)); transition:none; }
|
|
70
|
+
#cabeca { transform-origin:200px 300px; transition:transform .6s ease; }
|
|
71
|
+
.e-ouvindo #cabeca { transform:rotate(-3.5deg) translateX(-3px); }
|
|
72
|
+
#visor { transition:opacity .4s ease 1.15s; } .desmontado #visor { opacity:0; transition:none; }
|
|
73
|
+
#luz { transform-origin:200px 196px; animation:pulsa 3s ease-in-out infinite; transition:transform .12s ease-in; }
|
|
74
|
+
@keyframes pulsa { 0%,100% { opacity:.72 } 50% { opacity:1 } }
|
|
75
|
+
#luz.pisca { transform:scaleY(.05); }
|
|
76
|
+
#foco { transition:transform .7s cubic-bezier(.4,0,.2,1), opacity .4s; }
|
|
77
|
+
.e-pensando #luz, .e-executando #luz { animation-duration:1.2s; }
|
|
78
|
+
@keyframes gira { to { transform:rotate(360deg) } }
|
|
79
|
+
|
|
80
|
+
/* direita */
|
|
81
|
+
.dir { display:flex; flex-direction:column; gap:18px; min-height:0; }
|
|
82
|
+
.clima { background:linear-gradient(160deg,#1d5560 0%,#163c44 45%,#12282d 100%); border-color:#2b5a63; }
|
|
83
|
+
.clima .cidade { font-size:15px; font-weight:600; }
|
|
84
|
+
.clima .temp { font-size:60px; font-weight:300; line-height:1; margin:4px 0 6px; letter-spacing:-.03em; }
|
|
85
|
+
.clima .cond { font-size:14px; } .clima .mm { color:#b9d9de; font-size:13px; margin-top:2px; }
|
|
86
|
+
.clima .dias { display:flex; justify-content:space-between; margin-top:12px; padding-top:10px; border-top:1px solid rgba(255,255,255,.12); font-size:12px; text-align:center; }
|
|
87
|
+
.clima .dias div b { display:block; font-size:14px; font-weight:600; margin-top:2px; } .clima .dias div span { display:block; font-size:15px; margin-top:2px; }
|
|
88
|
+
.data { display:flex; align-items:baseline; gap:12px; padding:12px 22px; }
|
|
89
|
+
.data .dia { font-family:var(--serifa); font-size:46px; line-height:1; }
|
|
90
|
+
.data .resto b { display:block; font-size:15px; text-transform:capitalize; } .data .resto small { color:var(--apagado); font-size:13px; }
|
|
91
|
+
.musica { flex:1; min-height:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px; padding:14px 22px; }
|
|
92
|
+
.disco { height:min(100%,230px); max-height:min(22vw,230px); min-height:90px; aspect-ratio:1; flex:0 1 auto; border-radius:50%; position:relative; animation:gira 9s linear infinite; animation-play-state:paused;
|
|
93
|
+
background:repeating-radial-gradient(circle at 50% 50%, #161615 0 2px, #1e1e1c 2px 4px); box-shadow:0 0 0 1px var(--borda), 0 24px 40px -18px #000, inset 0 0 40px rgba(0,0,0,.6); }
|
|
94
|
+
.disco.toca { animation-play-state:running; }
|
|
95
|
+
.disco::before { content:""; position:absolute; inset:0; border-radius:50%; background:conic-gradient(from 30deg, transparent 0 20%, rgba(255,255,255,.08) 25%, transparent 32% 70%, rgba(255,255,255,.06) 75%, transparent 82%); }
|
|
96
|
+
.capa { position:absolute; inset:30%; border-radius:50%; background:#2a2a28 center/cover; box-shadow:0 0 0 3px #121211; }
|
|
97
|
+
.capa::after { content:""; position:absolute; inset:44%; border-radius:50%; background:var(--fundo); box-shadow:0 0 0 2px #0b0b0a; }
|
|
98
|
+
.faixa { text-align:center; max-width:100%; } .faixa b { display:block; font-size:16px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } .faixa small { color:var(--apagado); font-size:13px; }
|
|
99
|
+
.prog { width:100%; display:flex; align-items:center; gap:10px; font:12px var(--mono); color:var(--fraco); }
|
|
100
|
+
.prog .t { flex:1; height:4px; border-radius:2px; background:var(--borda); overflow:hidden; } .prog .t i { display:block; height:100%; width:0; background:var(--texto); border-radius:2px; }
|
|
101
|
+
.ctrl { display:flex; gap:30px; align-items:center; }
|
|
102
|
+
.ctrl button { background:none; border:0; color:var(--texto); cursor:pointer; display:grid; place-items:center; }
|
|
103
|
+
.ctrl button:active { transform:scale(.9); }
|
|
104
|
+
.ctrl .play { width:50px; height:50px; border-radius:50%; background:var(--texto); color:var(--fundo); }
|
|
105
|
+
|
|
106
|
+
/* medidores */
|
|
107
|
+
.medidores { grid-column:1 / -1; display:grid; grid-template-columns:repeat(4,1fr) 1.6fr; gap:18px; }
|
|
108
|
+
.med { display:flex; align-items:center; gap:16px; padding:14px 20px; }
|
|
109
|
+
.anel { --v:0; width:62px; height:62px; border-radius:50%; flex:none; display:grid; place-items:center; background:conic-gradient(var(--c,var(--marca)) calc(var(--v)*1%), var(--borda) 0); position:relative; }
|
|
110
|
+
.anel::before { content:""; position:absolute; inset:6px; border-radius:50%; background:var(--cartao); }
|
|
111
|
+
.anel span { position:relative; font:600 15px var(--sans); font-variant-numeric:tabular-nums; }
|
|
112
|
+
.med b { display:block; font-size:14px; } .med small { color:var(--apagado); font-size:12px; }
|
|
113
|
+
.rede { padding:12px 20px; display:flex; flex-direction:column; justify-content:space-between; }
|
|
114
|
+
.rede .lin { display:flex; justify-content:space-between; font:12px var(--mono); color:var(--apagado); }
|
|
115
|
+
.rede svg { width:100%; height:44px; }
|
|
116
|
+
|
|
117
|
+
/* voz */
|
|
118
|
+
.voz { grid-column:1 / -1; display:flex; align-items:center; gap:18px; padding:12px 22px; }
|
|
119
|
+
#barras { flex:1; height:46px; display:flex; align-items:center; gap:3px; }
|
|
120
|
+
#barras i { flex:1; border-radius:3px; height:8%; background:var(--marca); transition:height .08s linear; }
|
|
121
|
+
#barras i.p { background:var(--ok); } #barras i.z { background:var(--borda); }
|
|
122
|
+
.leg { display:flex; gap:16px; font-size:12px; color:var(--apagado); white-space:nowrap; }
|
|
123
|
+
.leg span::before { content:""; display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:6px; background:var(--marca); }
|
|
124
|
+
.leg span.p::before { background:var(--ok); }
|
|
125
|
+
.tecla { font-size:12px; color:var(--fraco); white-space:nowrap; }
|
|
126
|
+
</style>
|
|
127
|
+
</head>
|
|
128
|
+
<body>
|
|
129
|
+
<main class="tela" id="tela">
|
|
130
|
+
|
|
131
|
+
<section class="esq">
|
|
132
|
+
<div class="cartao">
|
|
133
|
+
<div class="cal-topo"><div class="cal-mes" id="cal-mes"></div><span class="rotulo">Calendário</span></div>
|
|
134
|
+
<div class="cal" id="cal"></div>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="cartao hoje-card">
|
|
137
|
+
<span class="rotulo">Hoje</span>
|
|
138
|
+
<div class="hoje-lista" id="hoje"><div class="vazio">carregando…</div></div>
|
|
139
|
+
</div>
|
|
140
|
+
</section>
|
|
141
|
+
|
|
142
|
+
<section class="centro" id="centro">
|
|
143
|
+
<div class="fala" id="fala"></div>
|
|
144
|
+
<div class="palco">
|
|
145
|
+
<div class="chao"></div>
|
|
146
|
+
<canvas id="robo3d" aria-label="Primo"></canvas>
|
|
147
|
+
<canvas id="robo-spline" aria-label="Primo"></canvas>
|
|
148
|
+
<svg id="svg-robo" viewBox="0 0 400 520" aria-label="Primo" class="desmontado">
|
|
149
|
+
<defs>
|
|
150
|
+
<linearGradient id="metal" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fbfbf9"/><stop offset=".4" stop-color="#d4d4cf"/><stop offset=".8" stop-color="#9b9b96"/><stop offset="1" stop-color="#76766f"/></linearGradient>
|
|
151
|
+
<linearGradient id="metalEsc" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#63635e"/><stop offset="1" stop-color="#242422"/></linearGradient>
|
|
152
|
+
<linearGradient id="placa" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ecece8"/><stop offset="1" stop-color="#a9a9a3"/></linearGradient>
|
|
153
|
+
<linearGradient id="visorG" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#0b2a30"/><stop offset=".5" stop-color="#22d3ee"/><stop offset="1" stop-color="#0b2a30"/></linearGradient>
|
|
154
|
+
<radialGradient id="brilho" cx=".3" cy=".22" r=".7"><stop offset="0" stop-color="#fff" stop-opacity=".95"/><stop offset=".55" stop-color="#fff" stop-opacity="0"/></radialGradient>
|
|
155
|
+
<radialGradient id="foco-g"><stop offset="0" stop-color="#cffafe"/><stop offset=".4" stop-color="#22d3ee"/><stop offset="1" stop-color="#22d3ee" stop-opacity="0"/></radialGradient>
|
|
156
|
+
<pattern id="escovado" width="4" height="3" patternUnits="userSpaceOnUse"><path d="M0 1.5H4" stroke="#fff" stroke-opacity=".10" stroke-width=".6"/><path d="M0 .3H4" stroke="#000" stroke-opacity=".05" stroke-width=".4"/></pattern>
|
|
157
|
+
<clipPath id="visor-clip"><path d="M126 176 Q200 150 274 176 L270 218 Q200 240 130 218 Z"/></clipPath>
|
|
158
|
+
<clipPath id="cabeca-clip"><path d="M200 70 C268 70 300 118 300 190 C300 262 262 312 200 312 C138 312 100 262 100 190 C100 118 132 70 200 70 Z"/></clipPath>
|
|
159
|
+
<filter id="luz-f" x="-30%" y="-60%" width="160%" height="220%"><feGaussianBlur stdDeviation="5"/></filter>
|
|
160
|
+
<filter id="sombra-f"><feGaussianBlur stdDeviation="9"/></filter>
|
|
161
|
+
</defs>
|
|
162
|
+
<ellipse cx="200" cy="514" rx="160" ry="10" fill="#000" opacity=".55" filter="url(#sombra-f)"/>
|
|
163
|
+
<g id="r-tudo">
|
|
164
|
+
<!-- ombros e peito -->
|
|
165
|
+
<g class="peca" style="--dy:90px;--d:0s">
|
|
166
|
+
<path d="M34 520 C34 424 66 372 128 354 L272 354 C334 372 366 424 366 520 Z" fill="url(#metal)"/>
|
|
167
|
+
<path d="M34 520 C34 424 66 372 128 354 L272 354 C334 372 366 424 366 520 Z" fill="url(#escovado)"/>
|
|
168
|
+
<path d="M34 520 C34 424 66 372 128 354 L272 354 C334 372 366 424 366 520 Z" fill="url(#brilho)" opacity=".55"/>
|
|
169
|
+
<path d="M104 366 Q70 398 62 470" stroke="#7d7d78" stroke-width="2" fill="none"/><path d="M296 366 Q330 398 338 470" stroke="#7d7d78" stroke-width="2" fill="none"/>
|
|
170
|
+
<path d="M92 380 Q64 410 58 460 L84 468 Q90 430 112 396 Z" fill="url(#placa)" opacity=".8"/><path d="M308 380 Q336 410 342 460 L316 468 Q310 430 288 396 Z" fill="url(#placa)" opacity=".6"/>
|
|
171
|
+
<circle cx="96" cy="380" r="13" fill="url(#metalEsc)"/><circle cx="96" cy="380" r="5" fill="#8e8e89"/>
|
|
172
|
+
<circle cx="304" cy="380" r="13" fill="url(#metalEsc)"/><circle cx="304" cy="380" r="5" fill="#8e8e89"/>
|
|
173
|
+
</g>
|
|
174
|
+
<g class="peca" style="--dy:70px;--d:.22s">
|
|
175
|
+
<path d="M148 354 L252 354 L240 478 Q200 496 160 478 Z" fill="url(#metalEsc)"/>
|
|
176
|
+
<path d="M160 372 H240 M164 392 H236 M170 450 H230" stroke="#111" stroke-width="2" opacity=".6"/>
|
|
177
|
+
<path d="M148 354 L160 478" stroke="#8e8e89" stroke-width="1.5" opacity=".6"/><path d="M252 354 L240 478" stroke="#8e8e89" stroke-width="1.5" opacity=".6"/>
|
|
178
|
+
<circle cx="200" cy="418" r="22" fill="#0c0d0d" stroke="#3a3a37" stroke-width="2"/>
|
|
179
|
+
<circle cx="200" cy="418" r="9" fill="#22d3ee"><animate attributeName="opacity" values=".6;1;.6" dur="2.4s" repeatCount="indefinite"/></circle>
|
|
180
|
+
<circle cx="200" cy="418" r="20" fill="#22d3ee" opacity=".28" filter="url(#luz-f)"/>
|
|
181
|
+
</g>
|
|
182
|
+
<!-- pescoço segmentado -->
|
|
183
|
+
<g class="peca" style="--dy:40px;--d:.5s">
|
|
184
|
+
<rect x="172" y="296" width="56" height="64" rx="12" fill="url(#metalEsc)"/>
|
|
185
|
+
<rect x="176" y="312" width="48" height="9" rx="4" fill="#3b3b38"/><rect x="176" y="326" width="48" height="9" rx="4" fill="#33332f"/><rect x="176" y="340" width="48" height="9" rx="4" fill="#2c2c29"/>
|
|
186
|
+
</g>
|
|
187
|
+
<!-- cabeça -->
|
|
188
|
+
<g class="peca" style="--dy:-90px;--d:.72s">
|
|
189
|
+
<g id="cabeca">
|
|
190
|
+
<path d="M200 70 C268 70 300 118 300 190 C300 262 262 312 200 312 C138 312 100 262 100 190 C100 118 132 70 200 70 Z" fill="url(#metal)"/>
|
|
191
|
+
<g clip-path="url(#cabeca-clip)">
|
|
192
|
+
<rect x="90" y="60" width="220" height="260" fill="url(#escovado)"/>
|
|
193
|
+
<path d="M98 190 C98 110 140 66 200 66 C170 90 150 130 146 200 C146 250 160 290 190 316 C130 306 98 260 98 190 Z" fill="url(#brilho)" opacity=".55"/>
|
|
194
|
+
<path d="M300 200 C300 270 262 312 200 312 C250 296 278 260 284 200 Z" fill="#000" opacity=".16"/>
|
|
195
|
+
<path d="M130 108 Q200 84 270 108" stroke="#fff" stroke-width="2" fill="none" opacity=".5"/>
|
|
196
|
+
</g>
|
|
197
|
+
<path d="M104 214 C112 270 150 306 200 308 C250 306 288 270 296 214" fill="none" stroke="#6c6c67" stroke-width="1.4" opacity=".6"/>
|
|
198
|
+
<path d="M168 270 H232" stroke="#8a8a85" stroke-width="3" stroke-linecap="round"/><path d="M180 282 H220" stroke="#a7a7a2" stroke-width="2" stroke-linecap="round" opacity=".7"/>
|
|
199
|
+
<circle cx="104" cy="196" r="13" fill="url(#metalEsc)"/><circle cx="104" cy="196" r="5" fill="#8e8e89"/>
|
|
200
|
+
<circle cx="296" cy="196" r="13" fill="url(#metalEsc)"/><circle cx="296" cy="196" r="5" fill="#8e8e89"/>
|
|
201
|
+
<!-- visor -->
|
|
202
|
+
<g id="visor">
|
|
203
|
+
<path d="M112 166 Q200 136 288 166 L282 226 Q200 252 118 226 Z" fill="#0b0c0c" stroke="#303030" stroke-width="2"/>
|
|
204
|
+
<g clip-path="url(#visor-clip)">
|
|
205
|
+
<g id="luz">
|
|
206
|
+
<rect x="126" y="150" width="148" height="90" fill="url(#visorG)" opacity=".8"/>
|
|
207
|
+
<rect x="126" y="176" width="148" height="40" fill="#22d3ee" opacity=".3" filter="url(#luz-f)"/>
|
|
208
|
+
</g>
|
|
209
|
+
<ellipse id="foco" cx="200" cy="196" rx="34" ry="22" fill="url(#foco-g)" opacity=".9"/>
|
|
210
|
+
<g id="eq" opacity="0"></g>
|
|
211
|
+
</g>
|
|
212
|
+
<path d="M122 172 Q200 144 278 172" stroke="#fff" stroke-width="1.5" fill="none" opacity=".25"/>
|
|
213
|
+
</g>
|
|
214
|
+
</g>
|
|
215
|
+
</g>
|
|
216
|
+
</g>
|
|
217
|
+
</svg>
|
|
218
|
+
</div>
|
|
219
|
+
<div class="nome" id="nome" data-e="dormindo"><h1 id="nome-h">Primo</h1><p id="estado-t">em espera</p></div>
|
|
220
|
+
</section>
|
|
221
|
+
|
|
222
|
+
<section class="dir">
|
|
223
|
+
<div class="cartao clima" id="clima">
|
|
224
|
+
<div class="cidade" id="c-cidade">—</div><div class="temp" id="c-temp">–°</div><div class="cond" id="c-cond">buscando o clima…</div>
|
|
225
|
+
<div class="mm" id="c-mm"></div><div class="dias" id="c-dias"></div>
|
|
226
|
+
</div>
|
|
227
|
+
<div class="cartao data"><div class="dia" id="d-dia"></div><div class="resto"><b id="d-sem"></b><small id="d-mes"></small></div></div>
|
|
228
|
+
<div class="cartao musica">
|
|
229
|
+
<div class="disco" id="disco"><div class="capa" id="capa"></div></div>
|
|
230
|
+
<div class="faixa"><b id="m-faixa">Nada tocando</b><small id="m-artista">abra o Spotify ou a Música</small></div>
|
|
231
|
+
<div class="prog"><span id="m-pos">0:00</span><div class="t"><i id="m-barra"></i></div><span id="m-dur">0:00</span></div>
|
|
232
|
+
<div class="ctrl">
|
|
233
|
+
<button data-m="anterior" aria-label="Anterior"><svg width="26" height="26" viewBox="0 0 24 24" fill="currentColor"><path d="M6 5h2v14H6zM20 5v14L9 12z"/></svg></button>
|
|
234
|
+
<button class="play" data-m="tocar" aria-label="Tocar ou pausar" id="m-play"></button>
|
|
235
|
+
<button data-m="proxima" aria-label="Próxima"><svg width="26" height="26" viewBox="0 0 24 24" fill="currentColor"><path d="M16 5h2v14h-2zM4 5l11 7-11 7z"/></svg></button>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</section>
|
|
239
|
+
|
|
240
|
+
<section class="medidores">
|
|
241
|
+
<div class="cartao med"><div class="anel" id="a-cpu"><span>–</span></div><div><b>CPU</b><small id="s-cpu"> </small></div></div>
|
|
242
|
+
<div class="cartao med"><div class="anel" id="a-mem" style="--c:var(--marca-clara)"><span>–</span></div><div><b>Memória</b><small id="s-mem"> </small></div></div>
|
|
243
|
+
<div class="cartao med"><div class="anel" id="a-disco" style="--c:var(--apagado)"><span>–</span></div><div><b>Disco</b><small id="s-disco"> </small></div></div>
|
|
244
|
+
<div class="cartao med"><div class="anel" id="a-bat" style="--c:var(--ok)"><span>–</span></div><div><b>Bateria</b><small id="s-bat"> </small></div></div>
|
|
245
|
+
<div class="cartao rede">
|
|
246
|
+
<div class="lin"><span>REDE</span><span id="r-txt">↓ 0 KB/s · ↑ 0 KB/s</span></div>
|
|
247
|
+
<svg viewBox="0 0 300 44" preserveAspectRatio="none"><path id="r-down" d="" fill="none" stroke="#22d3ee" stroke-width="2"/><path id="r-up" d="" fill="none" stroke="#7ad7a3" stroke-width="1.5" opacity=".85"/></svg>
|
|
248
|
+
</div>
|
|
249
|
+
</section>
|
|
250
|
+
|
|
251
|
+
<section class="cartao voz">
|
|
252
|
+
<div id="barras"></div>
|
|
253
|
+
<div class="leg"><span>você</span><span class="p" id="leg-nome">Primo</span></div>
|
|
254
|
+
<div class="tecla">⌨ luz do teclado no ritmo da voz</div>
|
|
255
|
+
</section>
|
|
256
|
+
</main>
|
|
257
|
+
|
|
258
|
+
<script src="vendor/three-robo.min.js"></script>
|
|
259
|
+
<script src="vendor/spline-runtime.min.js"></script>
|
|
260
|
+
<script>
|
|
261
|
+
const $ = (id) => document.getElementById(id);
|
|
262
|
+
const post = (o) => { try { window.webkit.messageHandlers.primo.postMessage(JSON.stringify(o)); } catch (e) {} };
|
|
263
|
+
window.addEventListener('error', (e) => post({ acao: 'erro', arg: e.message + ' @' + e.lineno }));
|
|
264
|
+
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
265
|
+
|
|
266
|
+
/* ── robô 3D (three.js local + RobotExpressive CC0). Sem WebGL: fica o SVG ── */
|
|
267
|
+
const R3 = (function () {
|
|
268
|
+
const R = { ok: false, estado: '', estadoFn() {}, boot() {}, evento() {}, nivel: 0 };
|
|
269
|
+
R.estado = (e) => { R.est = e; R.mudou && R.mudou(); }; R.nivelFala = (n) => { R.nivel = n; };
|
|
270
|
+
const T = window.THREE; if (!T) return R;
|
|
271
|
+
let rend; try {
|
|
272
|
+
const cv = document.getElementById('robo3d');
|
|
273
|
+
rend = new T.WebGLRenderer({ canvas: cv, alpha: true, antialias: true, powerPreference: 'low-power' });
|
|
274
|
+
rend.setClearColor(0, 0); rend.shadowMap.enabled = false; rend.outputColorSpace = T.SRGBColorSpace;
|
|
275
|
+
rend.toneMapping = T.ACESFilmicToneMapping; rend.toneMappingExposure = 1.05;
|
|
276
|
+
} catch (e) { return R; }
|
|
277
|
+
const cv = rend.domElement, cena = new T.Scene(), cam = new T.PerspectiveCamera(26, 1, 0.1, 100);
|
|
278
|
+
cena.add(new T.HemisphereLight(0xe6f7fb, 0x20262a, 1.1));
|
|
279
|
+
const rim = new T.DirectionalLight(0x22d3ee, 3.2); rim.position.set(3, 2.5, -3); cena.add(rim); // contorno ciano da marca
|
|
280
|
+
const rim2 = new T.DirectionalLight(0x22d3ee, 1.6); rim2.position.set(-4, 1, -2); cena.add(rim2);
|
|
281
|
+
const quente = new T.DirectionalLight(0xffd7a8, 0.9); quente.position.set(-2, 2, 4); cena.add(quente); // luz quente fraca
|
|
282
|
+
const chave = new T.DirectionalLight(0xffffff, 1.0); chave.position.set(2, 3, 4); cena.add(chave);
|
|
283
|
+
let mixer, acoes = {}, atual = null, cabeca = null, morfos = null, base = 'Idle';
|
|
284
|
+
const mouse = { x: 0, y: 0, alvoX: 0, alvoY: 0 }, olho = { x: 0, y: 0 }, morfo = { Angry: 0, Surprised: 0, Sad: 0 };
|
|
285
|
+
let tocandoUma = false, ultimo = 0, visivel = true;
|
|
286
|
+
const tam = () => {
|
|
287
|
+
const p = cv.parentElement, w = p.clientWidth, h = p.clientHeight; if (!w || !h) return;
|
|
288
|
+
rend.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.5)); rend.setSize(w, h, false);
|
|
289
|
+
cam.aspect = w / h; cam.updateProjectionMatrix();
|
|
290
|
+
};
|
|
291
|
+
new ResizeObserver(tam).observe(cv.parentElement);
|
|
292
|
+
const baseDe = () => (R.est === 'falando' ? 'Yes' : 'Idle');
|
|
293
|
+
function tocar(nome, uma, vel) {
|
|
294
|
+
const a = acoes[nome]; if (!a) return;
|
|
295
|
+
if (a === atual && !uma) return;
|
|
296
|
+
a.reset(); a.enabled = true; a.setLoop(uma ? T.LoopOnce : T.LoopRepeat, Infinity); a.clampWhenFinished = !!uma; a.timeScale = vel || 1;
|
|
297
|
+
if (atual && atual !== a) a.crossFadeFrom(atual, 0.3, false);
|
|
298
|
+
a.play(); atual = a; tocandoUma = !!uma;
|
|
299
|
+
}
|
|
300
|
+
R.mudou = () => { if (!mixer || tocandoUma) return; tocar(baseDe(), false, R.est === 'falando' ? 0.7 : 1); };
|
|
301
|
+
R.boot = () => { if (mixer) tocar('Wave', true); };
|
|
302
|
+
R.evento = (x) => { if (!mixer) return; if (x === 'sucesso') tocar('ThumbsUp', true); else if (x === 'erro') tocar('No', true); };
|
|
303
|
+
addEventListener('mousemove', (e) => { const r = cv.getBoundingClientRect(); mouse.alvoX = Math.max(-1, Math.min(1, (e.clientX - r.left - r.width / 2) / (innerWidth / 2))); mouse.alvoY = Math.max(-1, Math.min(1, (e.clientY - r.top - r.height / 3) / (innerHeight / 2))); });
|
|
304
|
+
document.addEventListener('mouseleave', () => { mouse.alvoX = mouse.alvoY = 0; });
|
|
305
|
+
document.addEventListener('visibilitychange', () => { visivel = !document.hidden; });
|
|
306
|
+
const relogio = new T.Clock();
|
|
307
|
+
function quadro(agora) {
|
|
308
|
+
requestAnimationFrame(quadro);
|
|
309
|
+
if (!visivel || document.hidden || !mixer || (typeof SP !== "undefined" && SP.ok)) return;
|
|
310
|
+
const falando = R.est === 'falando', passo = falando ? 1000 / 60 : 1000 / 30;
|
|
311
|
+
if (agora - ultimo < passo - 2) return; ultimo = agora;
|
|
312
|
+
const dt = Math.min(relogio.getDelta(), 0.1); mixer.update(dt);
|
|
313
|
+
mouse.x += (mouse.alvoX - mouse.x) * 0.08; mouse.y += (mouse.alvoY - mouse.y) * 0.08;
|
|
314
|
+
// olhar: mouse + virada para a pessoa (ouvindo) + de lado (pensando)
|
|
315
|
+
const pensa = R.est === 'pensando' || R.est === 'executando';
|
|
316
|
+
const ay = mouse.x * 0.45 + (R.est === 'ouvindo' ? -0.28 : 0) + (pensa ? 0.5 * Math.sign(Math.sin(agora / 2500) || 1) : 0);
|
|
317
|
+
const ax = mouse.y * 0.25 + (pensa ? -0.1 : 0);
|
|
318
|
+
olho.x += (ax - olho.x) * 0.06; olho.y += (ay - olho.y) * 0.06;
|
|
319
|
+
const alvo = { Angry: pensa ? 0.3 : 0, Surprised: falando ? Math.min(1, R.nivel * (0.5 + 0.5 * Math.abs(Math.sin(agora / 90)))) : 0, Sad: 0 };
|
|
320
|
+
if (morfos) for (const k in morfo) { morfo[k] += (alvo[k] - morfo[k]) * (falando ? 0.5 : 0.1); const i = morfos.dict[k]; if (i !== undefined) morfos.infl[i] = morfo[k]; }
|
|
321
|
+
let q0 = null;
|
|
322
|
+
if (cabeca) { q0 = cabeca.quaternion.clone(); cabeca.rotation.y += olho.y; cabeca.rotation.x += olho.x; }
|
|
323
|
+
rend.render(cena, cam);
|
|
324
|
+
if (cabeca) cabeca.quaternion.copy(q0);
|
|
325
|
+
}
|
|
326
|
+
const GLTF = T.GLTFLoader;
|
|
327
|
+
fetch('vendor/RobotExpressive.glb').then((r) => r.arrayBuffer()).then((buf) => new Promise((ok, no) => new GLTF().parse(buf, '', ok, no))).then((g) => {
|
|
328
|
+
const m = g.scene;
|
|
329
|
+
m.traverse((o) => {
|
|
330
|
+
if (o.isMesh) {
|
|
331
|
+
o.castShadow = o.receiveShadow = false;
|
|
332
|
+
const n = o.material && o.material.name;
|
|
333
|
+
const cor = n === 'Main' ? [0xe9eaec, 0.35, 0.38, 0] : n === 'Grey' ? [0x2b2f33, 0.55, 0.45, 0] : [0x061a1e, 0.2, 0.5, 0x22d3ee];
|
|
334
|
+
o.material = new T.MeshStandardMaterial({ color: cor[0], metalness: cor[1], roughness: cor[2], emissive: cor[3] || 0x000000, emissiveIntensity: cor[3] ? 0.55 : 0 });
|
|
335
|
+
if (o.morphTargetInfluences && o.morphTargetDictionary) morfos = { infl: o.morphTargetInfluences, dict: o.morphTargetDictionary };
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
cabeca = m.getObjectByName('Head') && (m.getObjectByName('Head').parent || m.getObjectByName('Head'));
|
|
339
|
+
cena.add(m);
|
|
340
|
+
// enquadra em busto: topo da cabeça até meio do tronco
|
|
341
|
+
m.updateMatrixWorld(true);
|
|
342
|
+
mixer = new T.AnimationMixer(m);
|
|
343
|
+
g.animations.forEach((a) => { acoes[a.name] = mixer.clipAction(a); });
|
|
344
|
+
mixer.addEventListener('finished', () => { tocandoUma = false; tocar(baseDe(), false, R.est === 'falando' ? 0.7 : 1); });
|
|
345
|
+
R.modelo = m; R.cam = cam; tam();
|
|
346
|
+
cam.position.set(0, 3.7, 10.2); cam.lookAt(0, 3.0, 0);
|
|
347
|
+
tocar('Idle'); R.ok = true; classeTela(); R.boot();
|
|
348
|
+
}).catch((e) => { post({ acao: 'erro', arg: '3d: ' + e }); });
|
|
349
|
+
requestAnimationFrame(quadro);
|
|
350
|
+
return R;
|
|
351
|
+
})();
|
|
352
|
+
|
|
353
|
+
/* ── robô do Spline (runtime local, cena na rede). Falhou ou passou de 6 s: fica o RobotExpressive ── */
|
|
354
|
+
const SP = (function () {
|
|
355
|
+
const CENA = 'https://prod.spline.design/kZDDjO5HuC9GJUM2/scene.splinecode';
|
|
356
|
+
const X = { ok: false, tentou: false, app: null, bot: null };
|
|
357
|
+
X.iniciar = () => {
|
|
358
|
+
if (X.tentou || !window.SplineApp) return; X.tentou = true;
|
|
359
|
+
let acabou = false;
|
|
360
|
+
const cai = (why) => { if (acabou) return; acabou = true; try { X.app && X.app.stop(); } catch (e) {} X.ok = false; post({ acao: 'erro', arg: 'spline: ' + why + ' (fallback RobotExpressive)' }); classeTela(); };
|
|
361
|
+
const tempo = setTimeout(() => cai('mais de 6 s'), 6000);
|
|
362
|
+
try {
|
|
363
|
+
const app = new window.SplineApp($('robo-spline')); X.app = app;
|
|
364
|
+
app.load(CENA).then(() => {
|
|
365
|
+
if (acabou) return; acabou = true; clearTimeout(tempo);
|
|
366
|
+
try { app.setBackgroundColor('transparent'); } catch (e) {}
|
|
367
|
+
X.bot = app.findObjectByName('Bot');
|
|
368
|
+
X.t0 = performance.now();
|
|
369
|
+
X.ok = true; classeTela();
|
|
370
|
+
if (document.hidden) app.stop();
|
|
371
|
+
}).catch((e) => { clearTimeout(tempo); cai(String(e)); });
|
|
372
|
+
} catch (e) { clearTimeout(tempo); cai(String(e)); }
|
|
373
|
+
};
|
|
374
|
+
// leve: janela oculta/minimizada = para o render
|
|
375
|
+
document.addEventListener('visibilitychange', () => { if (!X.app || !X.ok) return; if (document.hidden) X.app.stop(); else X.app.play(); });
|
|
376
|
+
// falando: o robô respira no ritmo da voz (escala do objeto "Bot" da cena) + brilho ciano pelo CSS (--nv)
|
|
377
|
+
X.quadro = (agora) => {
|
|
378
|
+
if (!X.ok) return;
|
|
379
|
+
// enquadra do tronco pra cima: câmera mais longe e mais alta (só pega depois dos primeiros quadros)
|
|
380
|
+
if (agora - X.t0 > 1500) { if (!X.cam) X.cam = X.app.findObjectByName('Camera 2'); if (X.cam) { X.cam.position.z = 1130; X.cam.position.y = 188; } }
|
|
381
|
+
const nv = S.estado === 'falando' ? Math.min(1, S.nivel * (0.5 + 0.5 * Math.abs(Math.sin(agora / 90)))) : 0;
|
|
382
|
+
$('robo-spline').style.setProperty('--nv', nv.toFixed(2));
|
|
383
|
+
if (X.bot) { const k = 1 + 0.018 * nv; X.bot.scale.x = X.bot.scale.y = X.bot.scale.z = k; }
|
|
384
|
+
};
|
|
385
|
+
return X;
|
|
386
|
+
})();
|
|
387
|
+
|
|
388
|
+
const S = { estado: '', nivel: 0, quem: null, nome: 'Primo', perm: 'pendente', hoje: [], musica: null, musicaT: 0, olhar: 0 };
|
|
389
|
+
const ROTULO = { ouvindo: 'ouvindo', pensando: 'pensando…', falando: 'falando', executando: 'trabalhando…', esperando: 'pronto', interrompido: 'interrompido' };
|
|
390
|
+
const pt = (o) => new Date().toLocaleDateString('pt-BR', o);
|
|
391
|
+
const mmss = (s) => { s = Math.max(0, Math.floor(s || 0)); return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); };
|
|
392
|
+
|
|
393
|
+
/* ── calendário e data (do relógio do Mac) ── */
|
|
394
|
+
let diasComEvento = [], diaDesenhado = -1;
|
|
395
|
+
function desenharCalendario() {
|
|
396
|
+
const hj = new Date(), a = hj.getFullYear(), m = hj.getMonth();
|
|
397
|
+
$('cal-mes').innerHTML = pt({ month: 'long' }) + ' <i>' + a + '</i>';
|
|
398
|
+
const el = $('cal'); el.innerHTML = '';
|
|
399
|
+
'DSTQQSS'.split('').forEach((d) => el.insertAdjacentHTML('beforeend', '<span class="sem">' + d + '</span>'));
|
|
400
|
+
const primeiro = new Date(a, m, 1).getDay(), n = new Date(a, m + 1, 0).getDate();
|
|
401
|
+
for (let i = 0; i < primeiro; i++) el.insertAdjacentHTML('beforeend', '<span></span>');
|
|
402
|
+
for (let d = 1; d <= n; d++) {
|
|
403
|
+
const col = (primeiro + d - 1) % 7;
|
|
404
|
+
const c = [d === hj.getDate() ? 'hoje' : '', col === 0 || col === 6 ? 'fds' : '', diasComEvento.includes(d) ? 'tem' : ''].join(' ');
|
|
405
|
+
el.insertAdjacentHTML('beforeend', '<span class="' + c + '">' + d + '</span>');
|
|
406
|
+
}
|
|
407
|
+
$('d-dia').textContent = hj.getDate();
|
|
408
|
+
$('d-sem').textContent = pt({ weekday: 'long' });
|
|
409
|
+
$('d-mes').textContent = pt({ month: 'long', year: 'numeric' });
|
|
410
|
+
}
|
|
411
|
+
function viraDia() { const d = new Date().getDate(); if (d !== diaDesenhado) { diaDesenhado = d; desenharCalendario(); } }
|
|
412
|
+
|
|
413
|
+
function desenharHoje() {
|
|
414
|
+
const el = $('hoje');
|
|
415
|
+
if (S.perm !== 'ok') {
|
|
416
|
+
const txt = S.perm === 'pendente' ? 'Libere o Calendário para ver os seus compromissos aqui.'
|
|
417
|
+
: S.perm === 'negada' ? 'Libere o Calendário: Ajustes › Privacidade e Segurança › Calendários.' : 'Sem acesso ao app Calendário neste computador.';
|
|
418
|
+
el.innerHTML = '<div class="vazio">' + txt + (S.perm === 'sem' ? '' : '<button id="b-cal">' + (S.perm === 'pendente' ? 'Liberar o Calendário' : 'Abrir os Ajustes') + '</button>') + '</div>';
|
|
419
|
+
const b = $('b-cal'); if (b) b.onclick = (e) => { e.stopPropagation(); post({ acao: 'calendario' }); };
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (!S.hoje.length) { el.innerHTML = '<div class="vazio">Nada marcado para hoje.</div>'; return; }
|
|
423
|
+
el.innerHTML = S.hoje.slice(0, 4).map((e) => '<div class="ev' + (e.prox ? ' prox' : '') + '"><i class="barra"></i><div><b>' + esc(e.titulo) + '</b><small>' + (e.prox && e.em ? esc(e.em) + ' · ' : '') + esc(e.hora) + '</small></div></div>').join('');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/* ── clima ── */
|
|
427
|
+
function desenharClima(c) {
|
|
428
|
+
if (!c || c.temp === undefined) return;
|
|
429
|
+
$('c-cidade').textContent = c.cidade || '';
|
|
430
|
+
$('c-temp').textContent = c.temp + '°';
|
|
431
|
+
$('c-cond').textContent = c.icone + ' ' + c.cond;
|
|
432
|
+
$('c-mm').textContent = 'Máx. ' + c.max + '° · Mín. ' + c.min + '°';
|
|
433
|
+
$('c-dias').innerHTML = (c.dias || []).slice(0, 4).map((d) => {
|
|
434
|
+
const [y, m, dd] = d.data.split('-').map(Number);
|
|
435
|
+
const s = new Date(y, m - 1, dd).toLocaleDateString('pt-BR', { weekday: 'short' }).replace('.', '');
|
|
436
|
+
return '<div>' + s.charAt(0).toUpperCase() + s.slice(1) + '<span>' + d.icone + '</span><b>' + d.max + '°</b></div>';
|
|
437
|
+
}).join('');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/* ── música ── */
|
|
441
|
+
const ICO_PLAY = '<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>';
|
|
442
|
+
const ICO_PAUSA = '<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M7 5h4v14H7zM13 5h4v14h-4z"/></svg>';
|
|
443
|
+
$('m-play').innerHTML = ICO_PLAY;
|
|
444
|
+
function desenharMusica(m) {
|
|
445
|
+
S.musica = m && m.faixa ? m : null; S.musicaT = Date.now();
|
|
446
|
+
if (!S.musica) { $('m-faixa').textContent = 'Nada tocando'; $('m-artista').textContent = 'abra o Spotify ou a Música'; $('disco').classList.remove('toca'); $('capa').style.backgroundImage = ''; $('m-dur').textContent = '0:00'; $('m-play').innerHTML = ICO_PLAY; return; }
|
|
447
|
+
$('m-faixa').textContent = m.faixa; $('m-artista').textContent = m.artista; $('m-dur').textContent = mmss(m.dur);
|
|
448
|
+
const tocando = m.estado === 'playing';
|
|
449
|
+
$('disco').classList.toggle('toca', tocando);
|
|
450
|
+
$('m-play').innerHTML = tocando ? ICO_PAUSA : ICO_PLAY;
|
|
451
|
+
$('capa').style.backgroundImage = m.capa ? 'url("' + m.capa + '")' : 'linear-gradient(135deg,#8a3b2b,#d07a3c 45%,#3a2622)';
|
|
452
|
+
}
|
|
453
|
+
function andarMusica() {
|
|
454
|
+
const m = S.musica; if (!m) { $('m-barra').style.width = '0'; $('m-pos').textContent = '0:00'; return; }
|
|
455
|
+
const pos = m.pos + (m.estado === 'playing' ? (Date.now() - S.musicaT) / 1000 : 0);
|
|
456
|
+
$('m-pos').textContent = mmss(pos);
|
|
457
|
+
$('m-barra').style.width = (m.dur ? Math.min(100, 100 * pos / m.dur) : 0) + '%';
|
|
458
|
+
}
|
|
459
|
+
document.querySelectorAll('.ctrl button').forEach((b) => b.onclick = (e) => {
|
|
460
|
+
e.stopPropagation(); post({ acao: 'midia', arg: b.dataset.m });
|
|
461
|
+
const m = S.musica;
|
|
462
|
+
if (b.dataset.m === 'tocar' && m) { // responde na hora; o próximo lido do Spotify confirma
|
|
463
|
+
m.pos += m.estado === 'playing' ? (Date.now() - S.musicaT) / 1000 : 0;
|
|
464
|
+
m.estado = m.estado === 'playing' ? 'paused' : 'playing'; desenharMusica(m);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
/* ── a máquina ── */
|
|
469
|
+
const fmtBytes = (b) => b >= 1e6 ? (b / 1e6).toFixed(1) + ' MB/s' : Math.round(b / 1e3) + ' KB/s';
|
|
470
|
+
const hist = { down: Array(60).fill(0), up: Array(60).fill(0) };
|
|
471
|
+
function caminho(a, max) { return a.map((v, i) => (i ? 'L' : 'M') + (i * 300 / (a.length - 1)).toFixed(1) + ' ' + (40 - 36 * Math.min(1, v / max)).toFixed(1)).join(' '); }
|
|
472
|
+
function anel(id, pct, texto, cor) { const e = $(id); e.style.setProperty('--v', Math.max(0, Math.min(100, pct))); e.firstChild.textContent = texto; if (cor) e.style.setProperty('--c', cor); }
|
|
473
|
+
function desenharMaquina(m) {
|
|
474
|
+
anel('a-cpu', m.cpu, m.cpu + '%'); $('s-cpu').textContent = m.nucleos + ' núcleos';
|
|
475
|
+
if (m.mem && m.mem.total_gb) { anel('a-mem', m.mem.pct, m.mem.pct + '%'); $('s-mem').textContent = String(m.mem.usada_gb).replace('.', ',') + ' de ' + m.mem.total_gb + ' GB'; }
|
|
476
|
+
if (m.disco && m.disco.livre_gb !== undefined) { anel('a-disco', m.disco.pct, m.disco.pct + '%'); $('s-disco').textContent = m.disco.livre_gb + ' GB livres'; }
|
|
477
|
+
if (m.bat && m.bat.pct !== undefined) { anel('a-bat', m.bat.pct, String(m.bat.pct), m.bat.pct <= 20 && !m.bat.tomada ? 'var(--erro)' : 'var(--ok)'); $('s-bat').textContent = m.bat.tomada ? 'na tomada' : 'na bateria'; }
|
|
478
|
+
else { $('s-bat').textContent = 'sem bateria'; }
|
|
479
|
+
hist.down.push(m.down || 0); hist.down.shift(); hist.up.push(m.up || 0); hist.up.shift();
|
|
480
|
+
const mx = Math.max(2e4, ...hist.down, ...hist.up);
|
|
481
|
+
$('r-down').setAttribute('d', caminho(hist.down, mx)); $('r-up').setAttribute('d', caminho(hist.up, mx));
|
|
482
|
+
$('r-txt').textContent = '↓ ' + fmtBytes(m.down || 0) + ' · ↑ ' + fmtBytes(m.up || 0);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/* ── barras da voz: o histórico rola; ciano = a pessoa falou, verde = o Primo falou ── */
|
|
486
|
+
const NB = 72, bars = [];
|
|
487
|
+
for (let i = 0; i < NB; i++) { const e = document.createElement('i'); e.className = 'z'; $('barras').appendChild(e); bars.push(e); }
|
|
488
|
+
const fila = Array(NB).fill(null).map(() => ({ h: 0.06, q: null }));
|
|
489
|
+
let ruido = 0;
|
|
490
|
+
function empurraBarra() {
|
|
491
|
+
ruido = (ruido + 1) % 1000;
|
|
492
|
+
const jit = 0.45 + 0.55 * Math.abs(Math.sin(ruido * 1.7) * Math.cos(ruido * 0.43));
|
|
493
|
+
fila.push({ h: S.quem ? Math.max(0.06, Math.min(1, S.nivel * jit * 1.15 + 0.04)) : 0.06, q: S.quem }); fila.shift();
|
|
494
|
+
fila.forEach((b, i) => { const x = bars[i]; x.style.height = (8 + 92 * b.h) + '%'; x.className = b.q === 'primo' ? 'p' : b.q === 'pessoa' ? '' : 'z'; });
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/* ── o robô ── */
|
|
498
|
+
function classeTela() { $('tela').className = 'tela e-' + S.estado + (SP.ok ? ' spline' : R3.ok ? ' tres-d' : ''); }
|
|
499
|
+
function setEstado(e) {
|
|
500
|
+
if (!e || e === S.estado) return;
|
|
501
|
+
S.estado = e; classeTela(); $('nome').dataset.e = e;
|
|
502
|
+
$('estado-t').textContent = e === 'dormindo' ? 'diga "' + S.nome + '"' : (ROTULO[e] || e);
|
|
503
|
+
R3.estado(e);
|
|
504
|
+
S.olhar = (e === 'pensando' || e === 'executando') ? 1 : 0;
|
|
505
|
+
$('eq').setAttribute('opacity', e === 'falando' ? 1 : 0);
|
|
506
|
+
$('foco').style.opacity = e === 'falando' ? 0 : .9;
|
|
507
|
+
}
|
|
508
|
+
$('centro').onclick = () => post({ acao: 'ptt' });
|
|
509
|
+
|
|
510
|
+
/* simples: o visor vira equalizador de 13 barras quando fala; olha de lado quando pensa */
|
|
511
|
+
const EQ = []; (function () { const g = $('eq'); for (let i = 0; i < 13; i++) { const r = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); r.setAttribute('x', 136 + i * 9.8); r.setAttribute('width', 6); r.setAttribute('rx', 3); r.setAttribute('fill', '#a5f3fc'); g.appendChild(r); EQ.push(r); } })();
|
|
512
|
+
function animaEq(t) {
|
|
513
|
+
EQ.forEach((r, i) => { const v = S.estado === 'falando' ? Math.max(.08, S.nivel * (.45 + .55 * Math.abs(Math.sin(t / 130 + i * 1.3)))) : .06; const h = 8 + 44 * v; r.setAttribute('height', h.toFixed(1)); r.setAttribute('y', (196 - h / 2).toFixed(1)); });
|
|
514
|
+
if (S.estado !== 'falando') $('foco').style.transform = 'translateX(' + (S.olhar ? (Math.sin(t / 900) > 0 ? 42 : -42) : 0) + 'px)';
|
|
515
|
+
}
|
|
516
|
+
function pisca() { const l = $('luz'); l.classList.add('pisca'); setTimeout(() => l.classList.remove('pisca'), 140); setTimeout(pisca, 2800 + Math.random() * 3200); }
|
|
517
|
+
|
|
518
|
+
function montar() {
|
|
519
|
+
const svg = $('svg-robo');
|
|
520
|
+
svg.classList.add('desmontado'); void svg.getBoundingClientRect();
|
|
521
|
+
setTimeout(() => svg.classList.remove('desmontado'), 40);
|
|
522
|
+
R3.boot();
|
|
523
|
+
}
|
|
524
|
+
function laco(t) { SP.quadro(t); if (!R3.ok && !SP.ok) animaEq(t); requestAnimationFrame(laco); }
|
|
525
|
+
|
|
526
|
+
/* ── a ponte com o Python ── */
|
|
527
|
+
window.primo = (m) => {
|
|
528
|
+
if (m.nome) { S.nome = m.nome; $('nome-h').textContent = m.nome; $('leg-nome').textContent = m.nome; if (S.estado === 'dormindo') $('estado-t').textContent = 'diga "' + m.nome + '"'; }
|
|
529
|
+
if (m.robo === 'spline') SP.iniciar();
|
|
530
|
+
if (m.evento) R3.evento(m.evento);
|
|
531
|
+
if (m.boot) montar();
|
|
532
|
+
if (m.estado) setEstado(m.estado);
|
|
533
|
+
if (m.barras) { S.quem = m.barras.quem; S.nivel = m.barras.nivel; R3.nivelFala(m.barras.quem === 'primo' ? m.barras.nivel : 0); }
|
|
534
|
+
if (m.transcricao !== undefined || m.resposta !== undefined) { const t = m.resposta || m.transcricao || ''; $('fala').textContent = t.length > 160 ? t.slice(0, 157) + '…' : t; }
|
|
535
|
+
if (m.calendario) { S.perm = m.calendario.permissao; diasComEvento = m.calendario.dias || []; desenharCalendario(); desenharHoje(); }
|
|
536
|
+
if (m.hoje) { S.hoje = m.hoje; desenharHoje(); }
|
|
537
|
+
if (m.clima) desenharClima(m.clima);
|
|
538
|
+
if (m.musica !== undefined) desenharMusica(m.musica);
|
|
539
|
+
if (m.maquina) desenharMaquina(m.maquina);
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
viraDia(); desenharHoje(); setEstado('dormindo');
|
|
543
|
+
setInterval(() => { viraDia(); andarMusica(); }, 250);
|
|
544
|
+
setInterval(empurraBarra, 50);
|
|
545
|
+
setTimeout(pisca, 3000);
|
|
546
|
+
requestAnimationFrame(laco);
|
|
547
|
+
setTimeout(montar, 50);
|
|
548
|
+
</script>
|
|
549
|
+
</body>
|
|
550
|
+
</html>
|