diff --git a/android/app/assets/carinha.html b/android/app/assets/carinha.html index e2438e7..a9d9c6d 100644 --- a/android/app/assets/carinha.html +++ b/android/app/assets/carinha.html @@ -212,7 +212,7 @@ function caminhoBoca(tipo) { // repouso: leve, com as microexpressões (canto que sobe, boca de lado, entreaberta) const f = FEICAO.atual, w = l * 0.30, mx = cx + f.dx * l * 0.10; const yE = cy - Math.max(0, f.lado) * l * 0.12, yD = cy - Math.max(0, -f.lado) * l * 0.12; - const curva = l * (0.18 + f.curva * 0.35); + const curva = l * (0.18 + f.curva * 0.35 - ZANGA * 0.3); let d = "M " + (mx - w) + "," + yE + " Q " + mx + "," + (cy + curva - f.abre * l * 0.12) + " " + (mx + w) + "," + yD; if (f.abre > 0.04) d += " Q " + mx + "," + (cy + curva + f.abre * l * 0.30) + " " + (mx - w) + "," + yE; return d; @@ -224,7 +224,8 @@ function caminhoSobrancelha(i, esc) { const ab = i === 0 ? r.ab : (r.ab2 === null ? r.ab : r.ab2); const piscando = 1 - Math.min(1, Math.max(0, ab)); const y = c[1] - metadeOlho(esc) - (EXTRAS.distSobrancelha + 4) * ESC - (f.sob[i] - piscando * 3) * ESC; - const inc = ((r.forma === 'bravo' ? 18 : r.forma === 'triste' ? -14 : 0) + f.inc[i]) * lado * ESC; + const base = r.forma === 'bravo' ? 18 : r.forma === 'triste' ? -14 : ZANGA * 14; // zanga franze a testa + const inc = (base + f.inc[i]) * lado * ESC; const ele = r.esc > 1.1 ? -15 * ESC : 0; return 'M ' + (c[0] - largura) + ',' + (y - inc + ele) + ' Q ' + c[0] + ',' + (y - 10 * ESC + ele) + ' ' + (c[0] + largura) + ',' + (y + inc + ele); } @@ -401,12 +402,17 @@ const LIVRES = ["feliz", "olhar_esq", "olhar_dir", "olhar_baixo", const LIVRES_HUMOR = { feliz: ["feliz", "alegre", "piscadinha", "coracao", "olhar_esq", "olhar_dir"], tranquilo: ["olhar_esq", "olhar_dir", "feliz", "piscadinha", "olhar_baixo", "timido"], + bravo: ["bravo", "olhar_baixo", "olhar_esq", "olhar_dir", "confuso"], entediado: ["olhar_esq", "olhar_dir", "olhar_baixo", "pensar", "bocejar", "confuso"], carente: ["olhar_baixo", "triste", "olhar_esq", "olhar_dir", "timido"], triste: ["triste", "olhar_baixo", "olhar_baixo"], cansado: ["olhar_baixo", "piscar2", "bocejar"], }; let HUMOR = "normal"; +/* Modo vida: zanga (0 a 1) deixa o rosto emburrado aos poucos quando a alegria cai; sono profundo = + dormiu de cansaço, e aí som, movimento e câmera não acordam (como na madrugada). */ +let ZANGA = 0; +let SONO_PROFUNDO = false; /* Microexpressões acordado: de vez em quando a boca e as sobrancelhas mexem sozinhas (um canto da boca sobe, uma sobrancelha arqueia, um suspiro) e voltam devagar ao neutro. O humor do modo @@ -431,6 +437,7 @@ const MICRO_HUMOR = { carente: ["preocupado", "preocupado", "suspiro", "meioSorriso"], triste: ["preocupado", "suspiro", "pensativo"], cansado: ["suspiro", "suspiro", "ladinho"], + bravo: ["pensativo", "pensativo", "ladinho", "suspiro"], normal: ["meioSorriso", "arquear", "sorrisinho", "ladinho", "interesse", "pensativo", "suspiro"], }; function atualizarFeicao(s) { @@ -495,11 +502,11 @@ function passo(dt) { // avanca a fila; true se ha gesto em /* Atividade em andamento (rosto-motor.js): para onde olhar e que cara fazer. Atualizado por quadro. */ let ATIV = null; -function atividade(nome) { +function atividade(nome, segundos) { if (!window.MotorRosto || !window.MotorRosto.atividade) return; if (nome === "parar" || !nome) { window.MotorRosto.atividade(null); return; } if (r.dormindo) { r.dormindo = false; animar("surpreso"); } - if (window.MotorRosto.atividade(nome) && nome === "cantar") tocarSom("musica"); + if (window.MotorRosto.atividade(nome, segundos) && nome === "cantar") tocarSom("musica"); } function repouso(t) { // pose calma: olha devagar, "respira" @@ -526,6 +533,8 @@ function repouso(t) { // pose calma: olha devagar, "respira if (ATIV.forma) r.forma = ATIV.forma; if (ATIV.boca) r.boca = ATIV.boca; if (ATIV.ab) r.ab = ATIV.ab; + } else if (ZANGA >= 0.7) { // alegria caindo: emburrado (a cor escolhida fica) + r.forma = "bravo"; } } @@ -634,7 +643,13 @@ function trata(ordem) { } else if (ordem.indexOf("ritmo ") === 0) { PERFIL = PERFIS[ordem.slice(6).trim()] || PERFIS.normal; } else if (ordem.indexOf("atividade ") === 0) { - atividade(ordem.slice(10).trim()); + const [nome, seg] = ordem.slice(10).trim().split(/\s+/); // "atividade bola 180" + atividade(nome, parseFloat(seg) || 0); + } else if (ordem.indexOf("zanga ") === 0) { + const z = parseFloat(ordem.slice(6)); + if (!isNaN(z)) ZANGA = Math.max(0, Math.min(1, z)); + } else if (ordem === "sono profundo" || ordem === "sono leve") { + SONO_PROFUNDO = ordem === "sono profundo"; } else if (ordem === "dormir" || ordem === "sono") { r.dormindo = true; atividade(null); @@ -790,6 +805,7 @@ const MIC = { /* Acordar com som: precisa de N barulhos em 1 minuto (ajuste "Sons para acordar"). Na madrugada silenciosa só barulho ALTO conta, e movimento/câmera não acordam (alguém se mexendo na cama). */ const DESPERTAR = { sons: 3, noite: true, ini: 23, fim: 7 }; +function sonoRespeitado() { return madrugada() || SONO_PROFUNDO; } function madrugada() { if (!DESPERTAR.noite || DESPERTAR.ini === DESPERTAR.fim) return false; const h = new Date().getHours(); @@ -855,7 +871,7 @@ function som(nivel) { if (r.dormindo) { const precisa = Math.max(1, Math.round(DESPERTAR.sons * PERFIL.acordarSom)); - if (contar(CONTA.somAcordar, madrugada() ? alto : temSom, agora, precisa, "ouvido: barulho")) { // acordou + if (contar(CONTA.somAcordar, sonoRespeitado() ? alto : temSom, agora, precisa, "ouvido: barulho")) { // acordou r.dormindo = false; animar("surpreso"); MIC.proxSurpresa = agora + MIC.cooldown; @@ -878,6 +894,7 @@ function checarSilencio(agora) { if (!MIC.ativo) return; // telinha do Pi: sem ouvido if (agora - MIC.visto > 2) return; // o ouvido parou de mandar dado if (r.dormindo) return; + if (HUMOR !== "normal") return; // modo vida ligado: quem manda dormir é a energia // movimento tambem conta como "tem alguem aqui": nao adormece com gente passando const atividade = Math.max(MIC.ultimoSom, MOV.ativo ? MOV.ultimoMov : 0); if (agora - atividade >= MIC.segSilencio) { @@ -949,7 +966,7 @@ function rastrear(t) { MOV.olharY+=((t.y-.5)*2*MOV.olharMax*.6-MOV.olharY)*.65; if(t.atividade===true) { MOV.ultimoMov=agora; - if(r.dormindo && !madrugada() && PERFIL !== PERFIS.calmo) { r.dormindo=false;animar('surpreso'); } + if(r.dormindo && !sonoRespeitado() && PERFIL !== PERFIS.calmo) { r.dormindo=false;animar('surpreso'); } } } function movimento(vel, cx, cy) { @@ -981,7 +998,7 @@ function movimento(vel, cx, cy) { const forte = v >= limiar(MOV.limiarForte); if (r.dormindo) { // acordou: algo se mexeu na frente (de dia; calmo precisa de vários movimentos) - if (!madrugada() && contar(CONTA.movAcordar, mexeu, agora, PERFIL.acordarMov, "presenca: movimento")) { + if (!sonoRespeitado() && contar(CONTA.movAcordar, mexeu, agora, PERFIL.acordarMov, "presenca: movimento")) { r.dormindo = false; animar("surpreso"); MOV.proxSurpresa = agora + MOV.cooldown; diff --git a/android/app/assets/rosto-motor.js b/android/app/assets/rosto-motor.js index 201b874..d17a361 100644 --- a/android/app/assets/rosto-motor.js +++ b/android/app/assets/rosto-motor.js @@ -524,12 +524,26 @@ // vale para qualquer rosto (SVG ou motor). O carinha.html pergunta para onde olhar // (olharAtividade) e chama palco() a cada quadro. const ATIVIDADES = { - ler: { nome: 'Lendo', emoji: '📖', dur: [25, 45] }, - cantar: { nome: 'Cantando', emoji: '🎤', dur: [18, 30] }, - bola: { nome: 'Jogando bola', emoji: '⚽', dur: [20, 35] }, - videogame: { nome: 'Videogame', emoji: '🎮', dur: [25, 45] }, - pintar: { nome: 'Pintando', emoji: '🎨', dur: [25, 40] }, - malhar: { nome: 'Malhando', emoji: '🏋️', dur: [15, 25] }, + ler: { nome: 'Lendo', emoji: '📖', dur: [25, 45] }, + cantar: { nome: 'Cantando', emoji: '🎤', dur: [18, 30] }, + bola: { nome: 'Jogando bola', emoji: '⚽', dur: [20, 35] }, + videogame: { nome: 'Videogame', emoji: '🎮', dur: [25, 45] }, + pintar: { nome: 'Pintando', emoji: '🎨', dur: [25, 40] }, + malhar: { nome: 'Malhando', emoji: '🏋️', dur: [15, 25] }, + dormir: { nome: 'Cochilando', emoji: '🛏️', dur: [40, 80] }, + cafe: { nome: 'Tomando café', emoji: '☕', dur: [20, 35] }, + cozinhar: { nome: 'Cozinhando', emoji: '🍳', dur: [20, 35] }, + regar: { nome: 'Regando planta', emoji: '🪴', dur: [20, 30] }, + violao: { nome: 'Tocando violão', emoji: '🎸', dur: [20, 35] }, + dancar: { nome: 'Dançando', emoji: '💃', dur: [15, 30] }, + corda: { nome: 'Pulando corda', emoji: '🪢', dur: [15, 25] }, + meditar: { nome: 'Meditando', emoji: '🧘', dur: [25, 45] }, + telescopio: { nome: 'Olhando estrelas', emoji: '🔭', dur: [25, 40] }, + pipa: { nome: 'Soltando pipa', emoji: '🪁', dur: [20, 35] }, + bolhas: { nome: 'Bolhas de sabão', emoji: '🫧', dur: [18, 30] }, + skate: { nome: 'Andando de skate', emoji: '🛹', dur: [18, 30] }, + computador: { nome: 'No computador', emoji: '💻', dur: [25, 45] }, + pescar: { nome: 'Pescando', emoji: '🎣', dur: [30, 50] }, }; const ativ = { nome: null, t0: 0, dur: 0, fim: 0 }; let palcoTela = null, pctx = null; @@ -582,6 +596,38 @@ const p = pincel(k); info.x = 16 + lado * 5 + p.x * 4; info.y = -1 + p.y * 5; } else if (ativ.nome === 'malhar') { const f = forca(k); info.x = lado * 14; info.y = 6 - f * 12; info.ab = f > 0.85 ? 0.55 : 1; + } else if (ativ.nome === 'dormir') { + info.forma = 'dormir'; info.boca = 'sono'; info.x = lado * 6; info.y = 4; + } else if (ativ.nome === 'cafe') { + const f = (k % 5) / 5, gole = f > 0.55 && f < 0.92; + info.x = lado * 12; info.y = gole ? -3 : 3; if (gole) info.forma = 'feliz'; + } else if (ativ.nome === 'cozinhar') { + const cc = (k % 2.6) / 2.6, h = cc < 0.55 ? 4 * (cc / 0.55) * (1 - cc / 0.55) : 0; + info.x = lado * 16; info.y = 4 - h * 14; + } else if (ativ.nome === 'regar') { + info.x = lado * 18; info.y = 8; + } else if (ativ.nome === 'violao') { + info.x = lado * 8 + 3 * Math.sin(k * 1.1); info.y = 6 + 1.5 * Math.sin(k * 2.2); info.boca = 'sorriso'; + if (Math.sin(k * 0.7) > 0.3) info.forma = 'feliz'; + } else if (ativ.nome === 'dancar') { + info.x = lado * 10 + 6 * Math.sin(k * 5); info.y = -2 + 4 * Math.abs(Math.sin(k * 5)); info.forma = 'feliz'; info.boca = 'sorriso'; + } else if (ativ.nome === 'corda') { + const th = k * Math.PI * 2 * 0.85; info.x = lado * 12; info.y = 4 - Math.pow(Math.max(0, Math.cos(th)), 0.6) * 10; + } else if (ativ.nome === 'meditar') { + info.forma = 'dormir'; info.boca = 'sorriso'; info.x = lado * 6; info.y = -2 + 2 * Math.sin(k * 1.2); + } else if (ativ.nome === 'telescopio') { + info.x = lado * 16; info.y = -11; + } else if (ativ.nome === 'pipa') { + info.x = lado * 14 + 4 * Math.sin(k * 0.8); info.y = -11 + 2 * Math.cos(k * 1.1); + } else if (ativ.nome === 'bolhas') { + info.x = lado * 14 + 4 * Math.sin(k * 0.9); info.y = -8 + 3 * Math.sin(k * 0.6); + } else if (ativ.nome === 'skate') { + info.x = lado * 12 + 7 * Math.sin(k * 0.9); info.y = 6; + } else if (ativ.nome === 'computador') { + info.x = lado * 18 + Math.sin(k * 20) * 0.6; info.y = 2 + ((k * 2.5) % 1) * 1.5; + } else if (ativ.nome === 'pescar') { + const cc = k % 8; info.x = lado * 18; info.y = 7; + if (cc > 6.2 && cc < 7.6) { info.forma = 'feliz'; info.boca = 'sorriso'; info.y = 0; } } return info; } @@ -618,8 +664,9 @@ caixa(c, -s * 0.66, -s * 0.98, s * 1.32, s * 0.98, s * 0.28); c.fill(); c.stroke(); const ox = (p.olhoX || 0) * s * 0.12, oy = (p.olhoY || 0) * s * 0.1; const pisca = (k % 3.7) < 0.12 ? 0.15 : 1; - c.save(); c.fillStyle = css(cor); + c.save(); c.fillStyle = css(cor); c.strokeStyle = css(cor); for (const lado of [-1, 1]) { + if (p.fechados) { c.lineWidth = Math.max(1.5, s * 0.07); c.beginPath(); c.arc(lado * s * 0.27, -s * 0.52, s * 0.12, 0.2, Math.PI - 0.2); c.stroke(); continue; } c.beginPath(); c.ellipse(lado * s * 0.27 + ox, -s * 0.5 + oy, s * 0.11, s * 0.16 * pisca, 0, 0, Math.PI * 2); c.fill(); } c.restore(); c.restore(); @@ -644,6 +691,56 @@ c.beginPath(); c.moveTo(x + s * 0.2, y - s * 0.7); c.quadraticCurveTo(x + s * 0.5, y - s * 0.55, x + s * 0.42, y - s * 0.3); c.stroke(); } + const ROSA_ = [255, 112, 176], AMARELO_ = [255, 218, 96], VERDE_ = [120, 255, 160], LARANJA_ = [255, 150, 70]; + const SENTADO = { sentado: true, pE: [-1.35, 1.35], pD: [1.35, -1.35] }; + const rnd = (i, sem) => { const a = Math.sin(i * 12.9898 + sem * 78.233) * 43758.5453; return a - Math.floor(a); }; + function estrela(c, x, y, r) { + c.beginPath(); + for (let i = 0; i < 10; i++) { const a = -Math.PI / 2 + i * Math.PI / 5, rr = i % 2 ? r * 0.45 : r; c.lineTo(x + Math.cos(a) * rr, y + Math.sin(a) * rr); } + c.closePath(); c.fill(); + } + function vapor(c, x, y, s, k, cor) { // três fios subindo e ondulando + c.save(); c.strokeStyle = css(cor, 0.7); c.lineWidth = Math.max(1, s * 0.04); + const alfa = c.globalAlpha; + for (let i = 0; i < 3; i++) { + const f = (k * 0.5 + i / 3) % 1, bx = x + (i - 1) * s * 0.1, by = y - f * s * 0.7; + c.globalAlpha = alfa * (1 - f) * Math.min(1, f / 0.2); + c.beginPath(); c.moveTo(bx, by); c.bezierCurveTo(bx + s * 0.1, by - s * 0.12, bx - s * 0.1, by - s * 0.24, bx, by - s * 0.36); c.stroke(); + } + c.restore(); + } + function xicara(c, x, y, s, cor) { + c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05); + c.beginPath(); c.moveTo(x - s * 0.18, y - s * 0.2); c.lineTo(x + s * 0.18, y - s * 0.2); c.lineTo(x + s * 0.14, y + s * 0.08); c.lineTo(x - s * 0.14, y + s * 0.08); c.closePath(); c.fill(); c.stroke(); + c.beginPath(); c.arc(x + s * 0.2, y - s * 0.07, s * 0.08, -1.4, 1.4); c.stroke(); + } + function chama(c, x, y, alt, s) { + c.fillStyle = css(LARANJA_); + c.beginPath(); c.moveTo(x, y - alt); c.quadraticCurveTo(x + s * 0.13, y - alt * 0.3, x, y); c.quadraticCurveTo(x - s * 0.13, y - alt * 0.3, x, y - alt); c.fill(); + c.fillStyle = css(AMARELO_); + c.beginPath(); c.moveTo(x, y - alt * 0.6); c.quadraticCurveTo(x + s * 0.06, y - alt * 0.2, x, y); c.quadraticCurveTo(x - s * 0.06, y - alt * 0.2, x, y - alt * 0.6); c.fill(); + } + function zzz(c, x, y, s, k, cor) { // Zs subindo + c.save(); c.strokeStyle = css(cor); c.lineCap = 'round'; c.lineJoin = 'round'; + const alfa = c.globalAlpha; + for (let i = 0; i < 3; i++) { + const f = (k * 0.3 + i / 3) % 1, tam = s * (0.18 + 0.2 * f), zx = x - s * 0.5 * f, zy = y - s * 1.3 * f; + c.globalAlpha = alfa * Math.min(1, f / 0.15) * (1 - f); c.lineWidth = Math.max(1.5, tam * 0.2); + c.beginPath(); c.moveTo(zx - tam / 2, zy - tam / 2); c.lineTo(zx + tam / 2, zy - tam / 2); c.lineTo(zx - tam / 2, zy + tam / 2); c.lineTo(zx + tam / 2, zy + tam / 2); c.stroke(); + } + c.restore(); + } + function notas(c, x, y, s, k, cor, dir) { + c.fillStyle = css(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05); + const alfa = c.globalAlpha; + for (let i = 0; i < 3; i++) { + const f = (k * 0.45 + i / 3) % 1; + c.globalAlpha = alfa * Math.min(1, f / 0.15) * (1 - f); + nota(c, x + dir * s * (0.3 + 0.5 * i) + Math.sin(f * 6 + i) * s * 0.2, y - f * s * 1.6, s * 0.7); + } + c.globalAlpha = alfa; + } + const CENAS = { ler(c, x, y, s, k, cor) { const b = boneco(c, x, y, s, { @@ -786,6 +883,321 @@ c.beginPath(); c.arc(b.cabeca.x + s * 0.72, b.cabeca.y + s * 0.3 + g * s * 0.5, s * 0.07, 0, Math.PI * 2); c.fill(); } }, + dormir(c, x, y, s, k, cor) { + // cama com cabeceira, travesseiro e cobertor que sobe e desce com a respiração + const bx0 = x - s * 1.7, bx1 = x + s * 1.9, topo = y - s * 0.75; + c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.07); + c.beginPath(); c.moveTo(bx0 + s * 0.12, y); c.lineTo(bx0 + s * 0.12, topo); c.moveTo(bx1 - s * 0.12, y); c.lineTo(bx1 - s * 0.12, topo); c.stroke(); + caixa(c, bx0 - s * 0.08, topo - s * 1.25, s * 0.24, s * 1.3, s * 0.1); c.fill(); c.stroke(); + caixa(c, bx0, topo - s * 0.3, bx1 - bx0, s * 0.32, s * 0.12); c.fill(); c.stroke(); + c.fillStyle = claro(cor, 0.9); + c.beginPath(); c.ellipse(bx0 + s * 0.85, topo - s * 0.5, s * 0.6, s * 0.24, 0, 0, Math.PI * 2); c.fill(); c.stroke(); + // bonequinho deitado: desenhado em pé e girado 90°, pés à direita, cabeça no travesseiro + const e = 0.72, linha = topo - s * 0.3 - s * e * 0.5; + c.save(); c.translate(x + s * 1.5, linha); c.rotate(-Math.PI / 2); + boneco(c, 0, 0, s * e, { fechados: true, bE: [-0.1, 0], bD: [0.1, 0] }, cor, k); + c.restore(); + const resp = Math.sin(k * 1.4) * s * 0.04; + c.fillStyle = css(cor.map((v) => v * 0.45)); c.strokeStyle = css(cor); + caixa(c, x - s * 0.3, linha - s * 0.42 - resp, s * 2.15, topo - s * 0.3 - (linha - s * 0.42 - resp) + s * 0.06, s * 0.18); c.fill(); c.stroke(); + c.strokeStyle = css(cor, 0.5); c.beginPath(); c.moveTo(x - s * 0.1, linha - s * 0.2 - resp); c.lineTo(x + s * 1.7, linha - s * 0.2 - resp); c.stroke(); + zzz(c, x - s * 0.9, linha - s * 0.9, s, k, cor); + }, + cafe(c, x, y, s, k, cor) { + const f = (k % 5) / 5, gole = suave(faixaDe(f, 0.5, 0.64)) * (1 - suave(faixaDe(f, 0.84, 0.97))); + const b = boneco(c, x, y, s, Object.assign({}, SENTADO, { + olhoX: 0.2, olhoY: 0.3 - gole * 0.8, fechados: gole > 0.85, cabeca: -0.04, + bE: [0.15, 0.95], bD: [-0.15 + gole * 1.75, -0.95 - gole * 3.05], + }), cor, k); + xicara(c, b.mD.x, b.mD.y, s, cor); + if (gole < 0.3) vapor(c, b.mD.x, b.mD.y - s * 0.3, s, k, cor); + }, + cozinhar(c, x, y, s, k, cor) { + const ciclo = (k % 2.6) / 2.6, voo = ciclo < 0.55 ? ciclo / 0.55 : -1, h = voo >= 0 ? 4 * voo * (1 - voo) : 0; + const fx = x + s * 1.0, fw = s * 1.7, fh = s * 1.2; + c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.07); + caixa(c, fx, y - fh, fw, fh, s * 0.08); c.fill(); c.stroke(); + caixa(c, fx + fw * 0.15, y - fh * 0.72, fw * 0.7, fh * 0.5, s * 0.06); c.stroke(); + for (let i = 0; i < 3; i++) chama(c, fx + fw * (0.3 + 0.2 * i), y - fh, s * (0.22 + 0.07 * Math.sin(k * 17 + i * 2)), s); + const b = boneco(c, x - s * 0.45, y, s, { + olhoX: 1, olhoY: 0.3 - h, cabeca: -0.05, bE: [-0.4, 0.3], + bD: [1.2 - (voo >= 0 && voo < 0.15 ? 0.25 : 0), 0.35], + }, cor, k); + const px = b.mD.x + s * 0.62, py = b.mD.y + s * 0.02; + c.strokeStyle = css(cor); c.lineWidth = Math.max(2, s * 0.09); + c.beginPath(); c.moveTo(b.mD.x, b.mD.y); c.lineTo(px - s * 0.42, py); c.stroke(); + c.fillStyle = escuro(cor); c.lineWidth = Math.max(1.5, s * 0.06); + c.beginPath(); c.ellipse(px, py, s * 0.46, s * 0.13, 0, 0, Math.PI * 2); c.fill(); c.stroke(); + c.save(); c.translate(px, py - s * 0.1 - h * s * 1.6); c.rotate(voo >= 0 ? voo * Math.PI * 2 : 0); + c.fillStyle = css(AMARELO_); c.beginPath(); c.ellipse(0, 0, s * 0.32, s * 0.08, 0, 0, Math.PI * 2); c.fill(); c.stroke(); + c.restore(); + }, + regar(c, x, y, s, k, cor) { + const cresce = Math.min(1, k / 18), px = x + s * 2.0; + // vaso + c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.07); + c.beginPath(); c.moveTo(px - s * 0.42, y - s * 0.6); c.lineTo(px + s * 0.42, y - s * 0.6); c.lineTo(px + s * 0.3, y); c.lineTo(px - s * 0.3, y); c.closePath(); c.fill(); c.stroke(); + // planta crescendo: caule, folhas e, no fim, a flor + const alt = s * (0.3 + 1.25 * cresce), base = y - s * 0.6, balanca = Math.sin(k * 1.5) * s * 0.06; + c.strokeStyle = css(VERDE_); c.lineWidth = Math.max(1.5, s * 0.07); + c.beginPath(); c.moveTo(px, base); c.quadraticCurveTo(px + balanca, base - alt / 2, px + balanca * 2, base - alt); c.stroke(); + c.fillStyle = css(VERDE_); + for (let i = 1; i <= 3; i++) { + if (cresce < i * 0.22) continue; + const yy = base - alt * i * 0.25, lado = i % 2 ? 1 : -1; + c.beginPath(); c.ellipse(px + balanca * i * 0.5 + lado * s * 0.18, yy, s * 0.2, s * 0.08, lado * -0.5, 0, Math.PI * 2); c.fill(); + } + if (cresce > 0.75) { + const f = (cresce - 0.75) / 0.25, fx = px + balanca * 2, fy = base - alt; + c.fillStyle = css(ROSA_); + for (let i = 0; i < 5; i++) { const a = i * 1.2566 + k * 0.3; c.beginPath(); c.arc(fx + Math.cos(a) * s * 0.16 * f, fy + Math.sin(a) * s * 0.16 * f, s * 0.11 * f, 0, Math.PI * 2); c.fill(); } + c.fillStyle = css(AMARELO_); c.beginPath(); c.arc(fx, fy, s * 0.09 * f, 0, Math.PI * 2); c.fill(); + } + const b = boneco(c, x - s * 0.7, y, s, { olhoX: 1, olhoY: 0.8, cabeca: -0.1, inclina: -0.06, bD: [1.35, 0.35], bE: [-0.3, 0.2] }, cor, k); + // regador inclinado e as gotas caindo na planta + const ang = 0.38, rx = b.mD.x + s * 0.05, ry = b.mD.y; + c.save(); c.translate(rx, ry); c.rotate(ang); + c.fillStyle = claro(cor, 0.95); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06); + caixa(c, 0, -s * 0.22, s * 0.6, s * 0.42, s * 0.08); c.fill(); c.stroke(); + c.beginPath(); c.moveTo(s * 0.55, -s * 0.05); c.lineTo(s * 1.0, -s * 0.32); c.stroke(); + c.restore(); + const ex = rx + Math.cos(ang) * s * 1.0 + Math.sin(ang) * s * 0.32, ey = ry + Math.sin(ang) * s * 1.0 - Math.cos(ang) * s * 0.32; + c.fillStyle = claro(cor, 1); + for (let i = 0; i < 6; i++) { + const f = (k * 1.6 + i / 6) % 1; + c.beginPath(); c.ellipse(ex + (px - ex) * f * 0.8, ey + (base - alt - ey + s * 0.2) * f * f, s * 0.035, s * 0.07, 0, 0, Math.PI * 2); c.fill(); + } + }, + violao(c, x, y, s, k, cor) { + const toque = Math.sin(k * 11); + const b = boneco(c, x, y, s, Object.assign({}, SENTADO, { + olhoX: -0.4, olhoY: 0.7, cabeca: 0.08 * Math.sin(k * 1.1), + bE: [-1.1, -0.9], bD: [-0.2 + toque * 0.12, -1.0], + }), cor, k); + // violão atravessado no colo: corpo perto da mão que toca, braço até a outra mão + const gx = b.quadril.x + s * 0.3, gy = b.quadril.y - s * 0.35, nx = b.mE.x, ny = b.mE.y; + const a = Math.atan2(ny - gy, nx - gx); + c.save(); c.translate(gx, gy); c.rotate(a); + const L = Math.hypot(nx - gx, ny - gy); + c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06); + caixa(c, s * 0.2, -s * 0.08, L - s * 0.1, s * 0.16, s * 0.04); c.fill(); c.stroke(); + caixa(c, L - s * 0.05, -s * 0.12, s * 0.3, s * 0.24, s * 0.06); c.fill(); c.stroke(); + c.fillStyle = claro(cor, 0.95); + c.beginPath(); c.arc(-s * 0.28, 0, s * 0.42, 0, Math.PI * 2); c.fill(); c.stroke(); + c.beginPath(); c.arc(s * 0.22, 0, s * 0.32, 0, Math.PI * 2); c.fill(); c.stroke(); + c.fillStyle = escuro(cor); c.beginPath(); c.arc(s * 0.05, 0, s * 0.11, 0, Math.PI * 2); c.fill(); + c.strokeStyle = css(cor, 0.7); c.lineWidth = Math.max(1, s * 0.015); + for (const d of [-0.04, 0, 0.04]) { c.beginPath(); c.moveTo(-s * 0.5, d * s); c.lineTo(L, d * s); c.stroke(); } + c.restore(); + c.fillStyle = css(cor); c.beginPath(); c.arc(b.mD.x, b.mD.y, s * 0.13, 0, Math.PI * 2); c.fill(); + notas(c, b.cabeca.x - s * 1.6, b.cabeca.y + s * 0.2, s, k, cor, -1); + }, + dancar(c, x, y, s, k, cor) { + const u = (Math.sin(k * 5) + 1) / 2, mexe = Math.sin(k * 5) * 0.18; + // globo espelhado e luzinhas no chão + const gx = x, gy = y - s * 4.5; + c.strokeStyle = css(cor, 0.6); c.lineWidth = Math.max(1, s * 0.03); + c.beginPath(); c.moveTo(gx, gy - s * 0.9); c.lineTo(gx, gy - s * 0.35); c.stroke(); + c.fillStyle = claro(cor, 0.9); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05); + c.beginPath(); c.arc(gx, gy, s * 0.35, 0, Math.PI * 2); c.fill(); c.stroke(); + c.strokeStyle = css(cor, 0.5); + for (let i = -1; i <= 1; i++) { c.beginPath(); c.ellipse(gx, gy, s * 0.35 * Math.abs(Math.cos(k + i)), s * 0.35, 0, 0, Math.PI * 2); c.stroke(); } + const cores = [ROSA_, AMARELO_, VERDE_, cor]; + for (let i = 0; i < 6; i++) { + const lx = x + Math.sin(k * 1.3 + i * 1.7) * s * 2.4, alfa = c.globalAlpha; + c.globalAlpha = alfa * 0.35; c.fillStyle = css(cores[i % 4]); + c.beginPath(); c.ellipse(lx, y, s * 0.35, s * 0.07, 0, 0, Math.PI * 2); c.fill(); + c.globalAlpha = alfa; + } + boneco(c, x, y, s, { + dx: mexe, inclina: mexe * 0.5, cabeca: -mexe * 0.6, olhoY: -0.2, olhoX: mexe * 3, + bE: [-0.7 - 2.1 * u, 0.5], bD: [0.7 + 2.1 * (1 - u), -0.5], + pE: [-0.1 - u * 0.25, u * 0.4], pD: [0.1 + (1 - u) * 0.25, -(1 - u) * 0.4], + }, cor, k); + for (let i = 0; i < 4; i++) { + const f = (k * 0.7 + i / 4) % 1, alfa = c.globalAlpha; + c.globalAlpha = alfa * Math.sin(f * Math.PI); c.fillStyle = css(cores[i]); + estrela(c, x + Math.cos(i * 1.9) * s * 1.6, y - s * (2 + i * 0.5) - f * s * 0.4, s * 0.12); c.globalAlpha = alfa; + } + }, + corda(c, x, y, s, k, cor) { + const th = k * Math.PI * 2 * 0.85, pulo = Math.pow(Math.max(0, Math.cos(th)), 0.6), sobe = pulo * s * 0.45; + c.save(); c.globalAlpha *= 0.3 * (1 - pulo * 0.5); c.fillStyle = css(cor); + c.beginPath(); c.ellipse(x, y, s * 0.6 * (1 - pulo * 0.3), s * 0.08, 0, 0, Math.PI * 2); c.fill(); c.restore(); + const b = boneco(c, x, y - sobe, s, { + olhoY: -0.2, bE: [-0.75 + 0.15 * Math.sin(th), 0.6], bD: [0.75 - 0.15 * Math.sin(th), -0.6], + pE: [-0.06, pulo * 0.3], pD: [0.06, -pulo * 0.3], + }, cor, k); + // a corda passa por baixo dos pés quando ele está no alto do pulo + const my = (b.mE.y + b.mD.y) / 2, embaixo = 2 * (y + s * 0.05) - my, emcima = 2 * (b.cabeca.y - s * 1.0) - my; + const cy = (embaixo + emcima) / 2 + Math.cos(th) * (embaixo - emcima) / 2; + c.strokeStyle = claro(cor, 1); c.lineWidth = Math.max(1.5, s * 0.05); + c.beginPath(); c.moveTo(b.mE.x, b.mE.y); c.quadraticCurveTo(x, cy, b.mD.x, b.mD.y); c.stroke(); + }, + meditar(c, x, y, s, k, cor) { + const flutua = (Math.sin(k * 1.2) + 1) / 2 * s * 0.35, cy = y - s * 1.7 - flutua; + const alfa = c.globalAlpha; + for (let i = 0; i < 3; i++) { // aura + const f = (k * 0.35 + i / 3) % 1; + c.globalAlpha = alfa * 0.35 * (1 - f); c.strokeStyle = css(cor); c.lineWidth = Math.max(1, s * 0.04); + c.beginPath(); c.ellipse(x, cy, s * (1.2 + f * 1.3), s * (1.4 + f * 1.3), 0, 0, Math.PI * 2); c.stroke(); + } + c.globalAlpha = alfa * 0.3; c.fillStyle = css(cor); + c.beginPath(); c.ellipse(x, y, s * 0.8 * (1 - flutua / s), s * 0.09, 0, 0, Math.PI * 2); c.fill(); + c.globalAlpha = alfa; + boneco(c, x, y - s * 0.3 - flutua, s, { + sentado: true, fechados: true, pE: [-1.45, 2.75], pD: [1.45, -2.75], bE: [-0.75, 0.25], bD: [0.75, -0.25], + }, cor, k); + }, + telescopio(c, x, y, s, k, cor) { + // céu estrelado com uma estrela cadente de vez em quando + const alfa = c.globalAlpha; + for (let i = 0; i < 16; i++) { + c.globalAlpha = alfa * (0.35 + 0.65 * Math.abs(Math.sin(k * (1 + rnd(i, 3)) + i))); + c.fillStyle = i % 5 ? claro(cor, 1) : css(AMARELO_); + estrela(c, x + s * (0.2 + rnd(i, 1) * 3.4), y - s * (3.4 + rnd(i, 2) * 1.5), s * (0.05 + 0.05 * rnd(i, 4))); + } + const f = (k % 6) / 1.1; + if (f < 1) { + const sx = x + s * (3.6 - f * 2.2), sy = y - s * (4.9 - f * 0.8); + const g = c.createLinearGradient(sx, sy, sx + s * 0.9, sy - s * 0.33); + g.addColorStop(0, claro(cor, 1)); g.addColorStop(1, css(cor, 0)); + c.globalAlpha = alfa * (1 - f); c.strokeStyle = g; c.lineWidth = Math.max(1.5, s * 0.05); + c.beginPath(); c.moveTo(sx, sy); c.lineTo(sx + s * 0.9, sy - s * 0.33); c.stroke(); + } + c.globalAlpha = alfa; + const b = boneco(c, x - s * 0.7, y, s, { olhoX: 1, olhoY: -1, cabeca: 0.15, bD: [2.1, 0.5], bE: [-0.3, 0.2] }, cor, k); + const ox = b.cabeca.x + s * 0.78, oy = b.cabeca.y + s * 0.3, tx = ox + s * 1.6, ty = oy - s * 0.95; + const mx = (ox + tx) / 2, my = (oy + ty) / 2; + c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06); + c.beginPath(); c.moveTo(mx, my); c.lineTo(mx - s * 0.5, y); c.moveTo(mx, my); c.lineTo(mx + s * 0.5, y); c.moveTo(mx, my); c.lineTo(mx, y); c.stroke(); + c.fillStyle = escuro(cor); + c.beginPath(); capsula(c, ox, oy, tx, ty, s * 0.15); c.fill(); c.stroke(); + c.fillStyle = claro(cor, 1); c.beginPath(); c.arc(tx, ty, s * 0.17, 0, Math.PI * 2); c.fill(); c.stroke(); + }, + pipa(c, x, y, s, k, cor) { + const kx = x + s * (2.3 + 0.45 * Math.sin(k * 0.8)), ky = y - s * (4.2 + 0.35 * Math.cos(k * 1.1)), ang = 0.25 * Math.sin(k * 1.6); + const b = boneco(c, x - s * 0.9, y, s, { olhoX: 1, olhoY: -1, cabeca: 0.12, bD: [2.55, 0.35], bE: [-0.4, 0.3] }, cor, k); + c.strokeStyle = css(cor, 0.8); c.lineWidth = Math.max(1, s * 0.025); + c.beginPath(); c.moveTo(b.mD.x, b.mD.y); + c.quadraticCurveTo((b.mD.x + kx) / 2 + s * 0.4, (b.mD.y + ky) / 2 + s * 0.7, kx, ky + s * 0.45); c.stroke(); + // rabiola com lacinhos + c.save(); c.translate(kx, ky); c.rotate(ang); + c.strokeStyle = css(cor, 0.8); c.beginPath(); c.moveTo(0, s * 0.5); + for (let i = 1; i <= 6; i++) c.lineTo(Math.sin(k * 3 + i) * s * 0.18, s * (0.5 + i * 0.22)); + c.stroke(); + for (let i = 2; i <= 6; i += 2) { + const lx = Math.sin(k * 3 + i) * s * 0.18, ly = s * (0.5 + i * 0.22); + c.fillStyle = css(i % 4 ? ROSA_ : AMARELO_); + c.beginPath(); c.moveTo(lx, ly); c.lineTo(lx - s * 0.1, ly - s * 0.06); c.lineTo(lx - s * 0.1, ly + s * 0.06); c.closePath(); c.fill(); + c.beginPath(); c.moveTo(lx, ly); c.lineTo(lx + s * 0.1, ly - s * 0.06); c.lineTo(lx + s * 0.1, ly + s * 0.06); c.closePath(); c.fill(); + } + const pts = [[0, -0.5], [0.38, 0], [0, 0.5], [-0.38, 0]]; + const cores = [ROSA_, AMARELO_, cor, VERDE_]; + for (let i = 0; i < 4; i++) { + const [ax, ay] = pts[i], [bx, by] = pts[(i + 1) % 4]; + c.fillStyle = css(cores[i]); c.beginPath(); c.moveTo(0, 0); c.lineTo(ax * s, ay * s); c.lineTo(bx * s, by * s); c.closePath(); c.fill(); + } + c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.04); + c.beginPath(); pts.forEach(([px, py], i) => (i ? c.lineTo(px * s, py * s) : c.moveTo(px * s, py * s))); c.closePath(); c.stroke(); + c.restore(); + }, + bolhas(c, x, y, s, k, cor) { + const b = boneco(c, x - s * 0.6, y, s, { olhoX: 0.8, olhoY: -0.6, bD: [1.6, -4.0], bE: [-0.3, 0.2] }, cor, k); + const wx = b.mD.x + s * 0.35, wy = b.mD.y - s * 0.25; + c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05); + c.beginPath(); c.moveTo(b.mD.x, b.mD.y); c.lineTo(wx - s * 0.1, wy + s * 0.08); c.stroke(); + c.beginPath(); c.arc(wx, wy, s * 0.12, 0, Math.PI * 2); c.stroke(); + const alfa = c.globalAlpha; + for (let i = 0; i < 9; i++) { + const f = (k * 0.22 + i / 9) % 1, r = s * (0.12 + 0.18 * rnd(i, 4)); + const bx = wx + f * s * (1.6 + 1.4 * rnd(i, 5)) + Math.sin(k * 2 + i) * s * 0.15, by = wy - f * s * (1.6 + rnd(i, 6)); + c.globalAlpha = alfa * Math.min(1, f / 0.08) * (f > 0.9 ? (1 - f) / 0.1 : 1); + c.fillStyle = css(cor, 0.1); c.strokeStyle = css(cor, 0.9); c.lineWidth = Math.max(1, s * 0.03); + c.beginPath(); c.arc(bx, by, r, 0, Math.PI * 2); c.fill(); c.stroke(); + c.strokeStyle = claro(cor, 1); c.beginPath(); c.arc(bx, by, r * 0.65, -2.4, -1.6); c.stroke(); + } + c.globalAlpha = alfa; + }, + skate(c, x, y, s, k, cor) { + const pos = Math.sin(k * 0.9) * 1.3, vel = Math.cos(k * 0.9), ciclo = k % 4; + const ollie = ciclo < 0.7 ? Math.sin(ciclo / 0.7 * Math.PI) : 0, sobe = ollie * s * 0.6; + const bx = x + pos * s, pe = y - s * 0.3 - sobe; + c.save(); c.globalAlpha *= 0.3; c.fillStyle = css(cor); + c.beginPath(); c.ellipse(bx, y, s * 0.9, s * 0.08, 0, 0, Math.PI * 2); c.fill(); c.restore(); + c.strokeStyle = css(cor, 0.4); c.lineWidth = Math.max(1, s * 0.03); // risquinhos de velocidade + for (let i = 0; i < 3; i++) { const lx = bx - Math.sign(vel) * s * (1.1 + i * 0.25); c.beginPath(); c.moveTo(lx, pe - s * (0.6 + i * 0.4)); c.lineTo(lx - Math.sign(vel) * s * 0.5 * Math.abs(vel), pe - s * (0.6 + i * 0.4)); c.stroke(); } + boneco(c, bx, pe, s, { + olhoX: vel * 0.8, inclina: -vel * 0.1, bE: [-1.35, 0.25 + 0.2 * Math.sin(k * 3)], bD: [1.35, -0.25 - 0.2 * Math.sin(k * 3)], + pE: [-0.25, 0.35], pD: [0.25, -0.35], + }, cor, k); + c.save(); c.translate(bx, pe); c.rotate(-ollie * 0.25); + c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05); + caixa(c, -s * 0.85, 0, s * 1.7, s * 0.12, s * 0.06); c.fill(); c.stroke(); + c.fillStyle = escuro(cor); + for (const lado of [-1, 1]) { c.beginPath(); c.arc(lado * s * 0.55, s * 0.2, s * 0.1, 0, Math.PI * 2); c.fill(); c.stroke(); } + c.restore(); + }, + computador(c, x, y, s, k, cor) { + const mx0 = x + s * 0.3, mx1 = x + s * 2.7, mt = y - s * 1.45; + c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06); + c.beginPath(); c.moveTo(mx0 + s * 0.15, mt); c.lineTo(mx0 + s * 0.15, y); c.moveTo(mx1 - s * 0.15, mt); c.lineTo(mx1 - s * 0.15, y); c.stroke(); + caixa(c, mx0, mt, mx1 - mx0, s * 0.12, s * 0.04); c.fill(); c.stroke(); + const nx = mx0 + s * 0.6, nw = s * 1.4, nh = s * 0.95, ny = mt - s * 0.08 - nh; + caixa(c, nx - s * 0.12, mt - s * 0.08, nw + s * 0.24, s * 0.08, s * 0.03); c.fill(); c.stroke(); + caixa(c, nx, ny, nw, nh, s * 0.06); c.fill(); c.stroke(); + c.save(); caixa(c, nx + s * 0.06, ny + s * 0.06, nw - s * 0.12, nh - s * 0.12, s * 0.03); c.clip(); + const linhas = Math.floor(k * 2.5), visiveis = 6, ini = Math.max(0, linhas - visiveis); + const tintas = [cor, VERDE_, AMARELO_, ROSA_]; + for (let i = ini; i < linhas; i++) { + const yy = ny + s * 0.14 + (i - ini) * s * 0.13, recuo = (rnd(i, 7) < 0.4 ? 1 : 0) * s * 0.15; + c.fillStyle = css(tintas[i % 4], 0.85); c.fillRect(nx + s * 0.12 + recuo, yy, (nw - s * 0.4) * (0.3 + 0.6 * rnd(i, 8)), s * 0.05); + } + if (Math.floor(k * 2) % 2) { c.fillStyle = claro(cor, 1); c.fillRect(nx + s * 0.12, ny + s * 0.14 + Math.min(linhas, visiveis) * s * 0.13, s * 0.04, s * 0.08); } + c.restore(); + const tecla = Math.sin(k * 20) * 0.05; + boneco(c, x - s * 0.55, y, s, Object.assign({}, SENTADO, { + olhoX: 1, olhoY: 0.2, cabeca: -0.05, bE: [0.9 + tecla, 0.9], bD: [1.35 - tecla, 0.45], + }), cor, k); + }, + pescar(c, x, y, s, k, cor) { + const ciclo = k % 8, fisgou = ciclo > 6.2 && ciclo < 7.6, puxa = fisgou ? suave((ciclo - 6.2) / 0.5) : 0; + // água: ondinhas à direita do píer + c.strokeStyle = css(cor, 0.55); c.lineWidth = Math.max(1, s * 0.035); + for (let fila = 0; fila < 3; fila++) { + c.beginPath(); + for (let i = 0; i <= 24; i++) { + const wx = x + s * (0.5 + i * 0.14), wy = y - s * 0.05 + fila * s * 0.16 + Math.sin(k * 2 + i * 0.8 + fila) * s * 0.04; + if (i) c.lineTo(wx, wy); else c.moveTo(wx, wy); + } + c.stroke(); + } + c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06); + caixa(c, x - s * 2.0, y - s * 0.22, s * 2.5, s * 0.22, s * 0.05); c.fill(); c.stroke(); + const b = boneco(c, x - s * 0.8, y - s * 0.22, s, Object.assign({}, SENTADO, { + olhoX: 1, olhoY: 0.6 - puxa * 1.4, bD: [2.0 - puxa * 0.4, -0.9], bE: [0.5, 1.2], + }), cor, k); + const vx = b.mD.x + s * (1.4 - puxa * 0.3), vy = b.mD.y - s * (1.6 + puxa * 0.3); + c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06); + c.beginPath(); c.moveTo(b.mE.x, b.mE.y); c.lineTo(b.mD.x, b.mD.y); c.lineTo(vx, vy); c.stroke(); + const belisca = ciclo > 5.5 && ciclo < 6.2 ? Math.sin(k * 30) * s * 0.06 : 0; + let lx = x + s * 2.6, ly = y - s * 0.05 + Math.sin(k * 2) * s * 0.04 + belisca; + if (fisgou) { // o peixe pula preso na linha + lx = x + s * (2.6 - puxa * 0.6); ly = y - s * (0.2 + Math.sin(puxa * Math.PI) * 1.3 + puxa * 0.3); + c.save(); c.translate(lx, ly + s * 0.25); c.rotate(Math.sin(k * 14) * 0.4); + c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.04); + c.beginPath(); c.ellipse(0, 0, s * 0.12, s * 0.28, 0, 0, Math.PI * 2); c.fill(); c.stroke(); + c.beginPath(); c.moveTo(0, s * 0.25); c.lineTo(-s * 0.14, s * 0.45); c.lineTo(s * 0.14, s * 0.45); c.closePath(); c.fill(); c.stroke(); + c.fillStyle = escuro(cor); c.beginPath(); c.arc(s * 0.04, -s * 0.12, s * 0.03, 0, Math.PI * 2); c.fill(); + c.restore(); + } + c.strokeStyle = css(cor, 0.85); c.lineWidth = Math.max(1, s * 0.02); + c.beginPath(); c.moveTo(vx, vy); c.quadraticCurveTo((vx + lx) / 2, Math.max(vy, ly) - s * (fisgou ? 0.1 : 0.4), lx, ly); c.stroke(); + if (!fisgou) { + c.fillStyle = css(ROSA_); c.beginPath(); c.arc(lx, ly, s * 0.09, Math.PI, 0); c.fill(); + c.fillStyle = claro(cor, 1); c.beginPath(); c.arc(lx, ly, s * 0.09, 0, Math.PI); c.fill(); + } + }, }; /** Desenha a cena da atividade (camada atrás dos olhos). o = { t, cor }. */ diff --git a/carinha.html b/carinha.html index e2438e7..a9d9c6d 100644 --- a/carinha.html +++ b/carinha.html @@ -212,7 +212,7 @@ function caminhoBoca(tipo) { // repouso: leve, com as microexpressões (canto que sobe, boca de lado, entreaberta) const f = FEICAO.atual, w = l * 0.30, mx = cx + f.dx * l * 0.10; const yE = cy - Math.max(0, f.lado) * l * 0.12, yD = cy - Math.max(0, -f.lado) * l * 0.12; - const curva = l * (0.18 + f.curva * 0.35); + const curva = l * (0.18 + f.curva * 0.35 - ZANGA * 0.3); let d = "M " + (mx - w) + "," + yE + " Q " + mx + "," + (cy + curva - f.abre * l * 0.12) + " " + (mx + w) + "," + yD; if (f.abre > 0.04) d += " Q " + mx + "," + (cy + curva + f.abre * l * 0.30) + " " + (mx - w) + "," + yE; return d; @@ -224,7 +224,8 @@ function caminhoSobrancelha(i, esc) { const ab = i === 0 ? r.ab : (r.ab2 === null ? r.ab : r.ab2); const piscando = 1 - Math.min(1, Math.max(0, ab)); const y = c[1] - metadeOlho(esc) - (EXTRAS.distSobrancelha + 4) * ESC - (f.sob[i] - piscando * 3) * ESC; - const inc = ((r.forma === 'bravo' ? 18 : r.forma === 'triste' ? -14 : 0) + f.inc[i]) * lado * ESC; + const base = r.forma === 'bravo' ? 18 : r.forma === 'triste' ? -14 : ZANGA * 14; // zanga franze a testa + const inc = (base + f.inc[i]) * lado * ESC; const ele = r.esc > 1.1 ? -15 * ESC : 0; return 'M ' + (c[0] - largura) + ',' + (y - inc + ele) + ' Q ' + c[0] + ',' + (y - 10 * ESC + ele) + ' ' + (c[0] + largura) + ',' + (y + inc + ele); } @@ -401,12 +402,17 @@ const LIVRES = ["feliz", "olhar_esq", "olhar_dir", "olhar_baixo", const LIVRES_HUMOR = { feliz: ["feliz", "alegre", "piscadinha", "coracao", "olhar_esq", "olhar_dir"], tranquilo: ["olhar_esq", "olhar_dir", "feliz", "piscadinha", "olhar_baixo", "timido"], + bravo: ["bravo", "olhar_baixo", "olhar_esq", "olhar_dir", "confuso"], entediado: ["olhar_esq", "olhar_dir", "olhar_baixo", "pensar", "bocejar", "confuso"], carente: ["olhar_baixo", "triste", "olhar_esq", "olhar_dir", "timido"], triste: ["triste", "olhar_baixo", "olhar_baixo"], cansado: ["olhar_baixo", "piscar2", "bocejar"], }; let HUMOR = "normal"; +/* Modo vida: zanga (0 a 1) deixa o rosto emburrado aos poucos quando a alegria cai; sono profundo = + dormiu de cansaço, e aí som, movimento e câmera não acordam (como na madrugada). */ +let ZANGA = 0; +let SONO_PROFUNDO = false; /* Microexpressões acordado: de vez em quando a boca e as sobrancelhas mexem sozinhas (um canto da boca sobe, uma sobrancelha arqueia, um suspiro) e voltam devagar ao neutro. O humor do modo @@ -431,6 +437,7 @@ const MICRO_HUMOR = { carente: ["preocupado", "preocupado", "suspiro", "meioSorriso"], triste: ["preocupado", "suspiro", "pensativo"], cansado: ["suspiro", "suspiro", "ladinho"], + bravo: ["pensativo", "pensativo", "ladinho", "suspiro"], normal: ["meioSorriso", "arquear", "sorrisinho", "ladinho", "interesse", "pensativo", "suspiro"], }; function atualizarFeicao(s) { @@ -495,11 +502,11 @@ function passo(dt) { // avanca a fila; true se ha gesto em /* Atividade em andamento (rosto-motor.js): para onde olhar e que cara fazer. Atualizado por quadro. */ let ATIV = null; -function atividade(nome) { +function atividade(nome, segundos) { if (!window.MotorRosto || !window.MotorRosto.atividade) return; if (nome === "parar" || !nome) { window.MotorRosto.atividade(null); return; } if (r.dormindo) { r.dormindo = false; animar("surpreso"); } - if (window.MotorRosto.atividade(nome) && nome === "cantar") tocarSom("musica"); + if (window.MotorRosto.atividade(nome, segundos) && nome === "cantar") tocarSom("musica"); } function repouso(t) { // pose calma: olha devagar, "respira" @@ -526,6 +533,8 @@ function repouso(t) { // pose calma: olha devagar, "respira if (ATIV.forma) r.forma = ATIV.forma; if (ATIV.boca) r.boca = ATIV.boca; if (ATIV.ab) r.ab = ATIV.ab; + } else if (ZANGA >= 0.7) { // alegria caindo: emburrado (a cor escolhida fica) + r.forma = "bravo"; } } @@ -634,7 +643,13 @@ function trata(ordem) { } else if (ordem.indexOf("ritmo ") === 0) { PERFIL = PERFIS[ordem.slice(6).trim()] || PERFIS.normal; } else if (ordem.indexOf("atividade ") === 0) { - atividade(ordem.slice(10).trim()); + const [nome, seg] = ordem.slice(10).trim().split(/\s+/); // "atividade bola 180" + atividade(nome, parseFloat(seg) || 0); + } else if (ordem.indexOf("zanga ") === 0) { + const z = parseFloat(ordem.slice(6)); + if (!isNaN(z)) ZANGA = Math.max(0, Math.min(1, z)); + } else if (ordem === "sono profundo" || ordem === "sono leve") { + SONO_PROFUNDO = ordem === "sono profundo"; } else if (ordem === "dormir" || ordem === "sono") { r.dormindo = true; atividade(null); @@ -790,6 +805,7 @@ const MIC = { /* Acordar com som: precisa de N barulhos em 1 minuto (ajuste "Sons para acordar"). Na madrugada silenciosa só barulho ALTO conta, e movimento/câmera não acordam (alguém se mexendo na cama). */ const DESPERTAR = { sons: 3, noite: true, ini: 23, fim: 7 }; +function sonoRespeitado() { return madrugada() || SONO_PROFUNDO; } function madrugada() { if (!DESPERTAR.noite || DESPERTAR.ini === DESPERTAR.fim) return false; const h = new Date().getHours(); @@ -855,7 +871,7 @@ function som(nivel) { if (r.dormindo) { const precisa = Math.max(1, Math.round(DESPERTAR.sons * PERFIL.acordarSom)); - if (contar(CONTA.somAcordar, madrugada() ? alto : temSom, agora, precisa, "ouvido: barulho")) { // acordou + if (contar(CONTA.somAcordar, sonoRespeitado() ? alto : temSom, agora, precisa, "ouvido: barulho")) { // acordou r.dormindo = false; animar("surpreso"); MIC.proxSurpresa = agora + MIC.cooldown; @@ -878,6 +894,7 @@ function checarSilencio(agora) { if (!MIC.ativo) return; // telinha do Pi: sem ouvido if (agora - MIC.visto > 2) return; // o ouvido parou de mandar dado if (r.dormindo) return; + if (HUMOR !== "normal") return; // modo vida ligado: quem manda dormir é a energia // movimento tambem conta como "tem alguem aqui": nao adormece com gente passando const atividade = Math.max(MIC.ultimoSom, MOV.ativo ? MOV.ultimoMov : 0); if (agora - atividade >= MIC.segSilencio) { @@ -949,7 +966,7 @@ function rastrear(t) { MOV.olharY+=((t.y-.5)*2*MOV.olharMax*.6-MOV.olharY)*.65; if(t.atividade===true) { MOV.ultimoMov=agora; - if(r.dormindo && !madrugada() && PERFIL !== PERFIS.calmo) { r.dormindo=false;animar('surpreso'); } + if(r.dormindo && !sonoRespeitado() && PERFIL !== PERFIS.calmo) { r.dormindo=false;animar('surpreso'); } } } function movimento(vel, cx, cy) { @@ -981,7 +998,7 @@ function movimento(vel, cx, cy) { const forte = v >= limiar(MOV.limiarForte); if (r.dormindo) { // acordou: algo se mexeu na frente (de dia; calmo precisa de vários movimentos) - if (!madrugada() && contar(CONTA.movAcordar, mexeu, agora, PERFIL.acordarMov, "presenca: movimento")) { + if (!sonoRespeitado() && contar(CONTA.movAcordar, mexeu, agora, PERFIL.acordarMov, "presenca: movimento")) { r.dormindo = false; animar("surpreso"); MOV.proxSurpresa = agora + MOV.cooldown; diff --git a/kiosk-server.py b/kiosk-server.py index 50f1cbc..3bfa3f6 100644 --- a/kiosk-server.py +++ b/kiosk-server.py @@ -57,7 +57,8 @@ GESTOS = ["piscar", "piscar2", "olhar_esq", "olhar_dir", "olhar_baixo", "feliz", "esconder", "espiar", "acenar", "pensar", "musica", "rir", "bocejar", "confuso", "tonto", "timido", "comemorar"] # Atividades (momentos dele): o bonequinho faz algo embaixo dos olhos (rosto-motor.js). -ATIVIDADES = ["ler", "cantar", "bola", "videogame", "pintar", "malhar"] +ATIVIDADES = ["ler", "cantar", "bola", "videogame", "pintar", "malhar", "dormir", "cafe", "cozinhar", "regar", + "violao", "dancar", "corda", "meditar", "telescopio", "pipa", "bolhas", "skate", "computador", "pescar"] # Ordens internas que o rosto entende mas não viram botão. ORDENS_INTERNAS = {"calma"} @@ -530,7 +531,7 @@ def main(): if tipo == "pir" and not madrugada(servidor.controle.config): with servidor.controle.lock: dormindo = bool(servidor.controle.estado.get("dormindo")) - if dormindo and vida.pir_acorda(): + if dormindo and not vida.sono_profundo and vida.pir_acorda(): enfileirar("acordar") def camera(ligada): @@ -539,7 +540,7 @@ def main(): def alguem_chegou(): camera(True) # modo presença: o PIR liga a câmera do celular - if not madrugada(servidor.controle.config): + if not madrugada(servidor.controle.config) and not vida.sono_profundo: enfileirar("acordar") def ninguem_por_perto(): diff --git a/rosto-motor.js b/rosto-motor.js index 201b874..d17a361 100644 --- a/rosto-motor.js +++ b/rosto-motor.js @@ -524,12 +524,26 @@ // vale para qualquer rosto (SVG ou motor). O carinha.html pergunta para onde olhar // (olharAtividade) e chama palco() a cada quadro. const ATIVIDADES = { - ler: { nome: 'Lendo', emoji: '📖', dur: [25, 45] }, - cantar: { nome: 'Cantando', emoji: '🎤', dur: [18, 30] }, - bola: { nome: 'Jogando bola', emoji: '⚽', dur: [20, 35] }, - videogame: { nome: 'Videogame', emoji: '🎮', dur: [25, 45] }, - pintar: { nome: 'Pintando', emoji: '🎨', dur: [25, 40] }, - malhar: { nome: 'Malhando', emoji: '🏋️', dur: [15, 25] }, + ler: { nome: 'Lendo', emoji: '📖', dur: [25, 45] }, + cantar: { nome: 'Cantando', emoji: '🎤', dur: [18, 30] }, + bola: { nome: 'Jogando bola', emoji: '⚽', dur: [20, 35] }, + videogame: { nome: 'Videogame', emoji: '🎮', dur: [25, 45] }, + pintar: { nome: 'Pintando', emoji: '🎨', dur: [25, 40] }, + malhar: { nome: 'Malhando', emoji: '🏋️', dur: [15, 25] }, + dormir: { nome: 'Cochilando', emoji: '🛏️', dur: [40, 80] }, + cafe: { nome: 'Tomando café', emoji: '☕', dur: [20, 35] }, + cozinhar: { nome: 'Cozinhando', emoji: '🍳', dur: [20, 35] }, + regar: { nome: 'Regando planta', emoji: '🪴', dur: [20, 30] }, + violao: { nome: 'Tocando violão', emoji: '🎸', dur: [20, 35] }, + dancar: { nome: 'Dançando', emoji: '💃', dur: [15, 30] }, + corda: { nome: 'Pulando corda', emoji: '🪢', dur: [15, 25] }, + meditar: { nome: 'Meditando', emoji: '🧘', dur: [25, 45] }, + telescopio: { nome: 'Olhando estrelas', emoji: '🔭', dur: [25, 40] }, + pipa: { nome: 'Soltando pipa', emoji: '🪁', dur: [20, 35] }, + bolhas: { nome: 'Bolhas de sabão', emoji: '🫧', dur: [18, 30] }, + skate: { nome: 'Andando de skate', emoji: '🛹', dur: [18, 30] }, + computador: { nome: 'No computador', emoji: '💻', dur: [25, 45] }, + pescar: { nome: 'Pescando', emoji: '🎣', dur: [30, 50] }, }; const ativ = { nome: null, t0: 0, dur: 0, fim: 0 }; let palcoTela = null, pctx = null; @@ -582,6 +596,38 @@ const p = pincel(k); info.x = 16 + lado * 5 + p.x * 4; info.y = -1 + p.y * 5; } else if (ativ.nome === 'malhar') { const f = forca(k); info.x = lado * 14; info.y = 6 - f * 12; info.ab = f > 0.85 ? 0.55 : 1; + } else if (ativ.nome === 'dormir') { + info.forma = 'dormir'; info.boca = 'sono'; info.x = lado * 6; info.y = 4; + } else if (ativ.nome === 'cafe') { + const f = (k % 5) / 5, gole = f > 0.55 && f < 0.92; + info.x = lado * 12; info.y = gole ? -3 : 3; if (gole) info.forma = 'feliz'; + } else if (ativ.nome === 'cozinhar') { + const cc = (k % 2.6) / 2.6, h = cc < 0.55 ? 4 * (cc / 0.55) * (1 - cc / 0.55) : 0; + info.x = lado * 16; info.y = 4 - h * 14; + } else if (ativ.nome === 'regar') { + info.x = lado * 18; info.y = 8; + } else if (ativ.nome === 'violao') { + info.x = lado * 8 + 3 * Math.sin(k * 1.1); info.y = 6 + 1.5 * Math.sin(k * 2.2); info.boca = 'sorriso'; + if (Math.sin(k * 0.7) > 0.3) info.forma = 'feliz'; + } else if (ativ.nome === 'dancar') { + info.x = lado * 10 + 6 * Math.sin(k * 5); info.y = -2 + 4 * Math.abs(Math.sin(k * 5)); info.forma = 'feliz'; info.boca = 'sorriso'; + } else if (ativ.nome === 'corda') { + const th = k * Math.PI * 2 * 0.85; info.x = lado * 12; info.y = 4 - Math.pow(Math.max(0, Math.cos(th)), 0.6) * 10; + } else if (ativ.nome === 'meditar') { + info.forma = 'dormir'; info.boca = 'sorriso'; info.x = lado * 6; info.y = -2 + 2 * Math.sin(k * 1.2); + } else if (ativ.nome === 'telescopio') { + info.x = lado * 16; info.y = -11; + } else if (ativ.nome === 'pipa') { + info.x = lado * 14 + 4 * Math.sin(k * 0.8); info.y = -11 + 2 * Math.cos(k * 1.1); + } else if (ativ.nome === 'bolhas') { + info.x = lado * 14 + 4 * Math.sin(k * 0.9); info.y = -8 + 3 * Math.sin(k * 0.6); + } else if (ativ.nome === 'skate') { + info.x = lado * 12 + 7 * Math.sin(k * 0.9); info.y = 6; + } else if (ativ.nome === 'computador') { + info.x = lado * 18 + Math.sin(k * 20) * 0.6; info.y = 2 + ((k * 2.5) % 1) * 1.5; + } else if (ativ.nome === 'pescar') { + const cc = k % 8; info.x = lado * 18; info.y = 7; + if (cc > 6.2 && cc < 7.6) { info.forma = 'feliz'; info.boca = 'sorriso'; info.y = 0; } } return info; } @@ -618,8 +664,9 @@ caixa(c, -s * 0.66, -s * 0.98, s * 1.32, s * 0.98, s * 0.28); c.fill(); c.stroke(); const ox = (p.olhoX || 0) * s * 0.12, oy = (p.olhoY || 0) * s * 0.1; const pisca = (k % 3.7) < 0.12 ? 0.15 : 1; - c.save(); c.fillStyle = css(cor); + c.save(); c.fillStyle = css(cor); c.strokeStyle = css(cor); for (const lado of [-1, 1]) { + if (p.fechados) { c.lineWidth = Math.max(1.5, s * 0.07); c.beginPath(); c.arc(lado * s * 0.27, -s * 0.52, s * 0.12, 0.2, Math.PI - 0.2); c.stroke(); continue; } c.beginPath(); c.ellipse(lado * s * 0.27 + ox, -s * 0.5 + oy, s * 0.11, s * 0.16 * pisca, 0, 0, Math.PI * 2); c.fill(); } c.restore(); c.restore(); @@ -644,6 +691,56 @@ c.beginPath(); c.moveTo(x + s * 0.2, y - s * 0.7); c.quadraticCurveTo(x + s * 0.5, y - s * 0.55, x + s * 0.42, y - s * 0.3); c.stroke(); } + const ROSA_ = [255, 112, 176], AMARELO_ = [255, 218, 96], VERDE_ = [120, 255, 160], LARANJA_ = [255, 150, 70]; + const SENTADO = { sentado: true, pE: [-1.35, 1.35], pD: [1.35, -1.35] }; + const rnd = (i, sem) => { const a = Math.sin(i * 12.9898 + sem * 78.233) * 43758.5453; return a - Math.floor(a); }; + function estrela(c, x, y, r) { + c.beginPath(); + for (let i = 0; i < 10; i++) { const a = -Math.PI / 2 + i * Math.PI / 5, rr = i % 2 ? r * 0.45 : r; c.lineTo(x + Math.cos(a) * rr, y + Math.sin(a) * rr); } + c.closePath(); c.fill(); + } + function vapor(c, x, y, s, k, cor) { // três fios subindo e ondulando + c.save(); c.strokeStyle = css(cor, 0.7); c.lineWidth = Math.max(1, s * 0.04); + const alfa = c.globalAlpha; + for (let i = 0; i < 3; i++) { + const f = (k * 0.5 + i / 3) % 1, bx = x + (i - 1) * s * 0.1, by = y - f * s * 0.7; + c.globalAlpha = alfa * (1 - f) * Math.min(1, f / 0.2); + c.beginPath(); c.moveTo(bx, by); c.bezierCurveTo(bx + s * 0.1, by - s * 0.12, bx - s * 0.1, by - s * 0.24, bx, by - s * 0.36); c.stroke(); + } + c.restore(); + } + function xicara(c, x, y, s, cor) { + c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05); + c.beginPath(); c.moveTo(x - s * 0.18, y - s * 0.2); c.lineTo(x + s * 0.18, y - s * 0.2); c.lineTo(x + s * 0.14, y + s * 0.08); c.lineTo(x - s * 0.14, y + s * 0.08); c.closePath(); c.fill(); c.stroke(); + c.beginPath(); c.arc(x + s * 0.2, y - s * 0.07, s * 0.08, -1.4, 1.4); c.stroke(); + } + function chama(c, x, y, alt, s) { + c.fillStyle = css(LARANJA_); + c.beginPath(); c.moveTo(x, y - alt); c.quadraticCurveTo(x + s * 0.13, y - alt * 0.3, x, y); c.quadraticCurveTo(x - s * 0.13, y - alt * 0.3, x, y - alt); c.fill(); + c.fillStyle = css(AMARELO_); + c.beginPath(); c.moveTo(x, y - alt * 0.6); c.quadraticCurveTo(x + s * 0.06, y - alt * 0.2, x, y); c.quadraticCurveTo(x - s * 0.06, y - alt * 0.2, x, y - alt * 0.6); c.fill(); + } + function zzz(c, x, y, s, k, cor) { // Zs subindo + c.save(); c.strokeStyle = css(cor); c.lineCap = 'round'; c.lineJoin = 'round'; + const alfa = c.globalAlpha; + for (let i = 0; i < 3; i++) { + const f = (k * 0.3 + i / 3) % 1, tam = s * (0.18 + 0.2 * f), zx = x - s * 0.5 * f, zy = y - s * 1.3 * f; + c.globalAlpha = alfa * Math.min(1, f / 0.15) * (1 - f); c.lineWidth = Math.max(1.5, tam * 0.2); + c.beginPath(); c.moveTo(zx - tam / 2, zy - tam / 2); c.lineTo(zx + tam / 2, zy - tam / 2); c.lineTo(zx - tam / 2, zy + tam / 2); c.lineTo(zx + tam / 2, zy + tam / 2); c.stroke(); + } + c.restore(); + } + function notas(c, x, y, s, k, cor, dir) { + c.fillStyle = css(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05); + const alfa = c.globalAlpha; + for (let i = 0; i < 3; i++) { + const f = (k * 0.45 + i / 3) % 1; + c.globalAlpha = alfa * Math.min(1, f / 0.15) * (1 - f); + nota(c, x + dir * s * (0.3 + 0.5 * i) + Math.sin(f * 6 + i) * s * 0.2, y - f * s * 1.6, s * 0.7); + } + c.globalAlpha = alfa; + } + const CENAS = { ler(c, x, y, s, k, cor) { const b = boneco(c, x, y, s, { @@ -786,6 +883,321 @@ c.beginPath(); c.arc(b.cabeca.x + s * 0.72, b.cabeca.y + s * 0.3 + g * s * 0.5, s * 0.07, 0, Math.PI * 2); c.fill(); } }, + dormir(c, x, y, s, k, cor) { + // cama com cabeceira, travesseiro e cobertor que sobe e desce com a respiração + const bx0 = x - s * 1.7, bx1 = x + s * 1.9, topo = y - s * 0.75; + c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.07); + c.beginPath(); c.moveTo(bx0 + s * 0.12, y); c.lineTo(bx0 + s * 0.12, topo); c.moveTo(bx1 - s * 0.12, y); c.lineTo(bx1 - s * 0.12, topo); c.stroke(); + caixa(c, bx0 - s * 0.08, topo - s * 1.25, s * 0.24, s * 1.3, s * 0.1); c.fill(); c.stroke(); + caixa(c, bx0, topo - s * 0.3, bx1 - bx0, s * 0.32, s * 0.12); c.fill(); c.stroke(); + c.fillStyle = claro(cor, 0.9); + c.beginPath(); c.ellipse(bx0 + s * 0.85, topo - s * 0.5, s * 0.6, s * 0.24, 0, 0, Math.PI * 2); c.fill(); c.stroke(); + // bonequinho deitado: desenhado em pé e girado 90°, pés à direita, cabeça no travesseiro + const e = 0.72, linha = topo - s * 0.3 - s * e * 0.5; + c.save(); c.translate(x + s * 1.5, linha); c.rotate(-Math.PI / 2); + boneco(c, 0, 0, s * e, { fechados: true, bE: [-0.1, 0], bD: [0.1, 0] }, cor, k); + c.restore(); + const resp = Math.sin(k * 1.4) * s * 0.04; + c.fillStyle = css(cor.map((v) => v * 0.45)); c.strokeStyle = css(cor); + caixa(c, x - s * 0.3, linha - s * 0.42 - resp, s * 2.15, topo - s * 0.3 - (linha - s * 0.42 - resp) + s * 0.06, s * 0.18); c.fill(); c.stroke(); + c.strokeStyle = css(cor, 0.5); c.beginPath(); c.moveTo(x - s * 0.1, linha - s * 0.2 - resp); c.lineTo(x + s * 1.7, linha - s * 0.2 - resp); c.stroke(); + zzz(c, x - s * 0.9, linha - s * 0.9, s, k, cor); + }, + cafe(c, x, y, s, k, cor) { + const f = (k % 5) / 5, gole = suave(faixaDe(f, 0.5, 0.64)) * (1 - suave(faixaDe(f, 0.84, 0.97))); + const b = boneco(c, x, y, s, Object.assign({}, SENTADO, { + olhoX: 0.2, olhoY: 0.3 - gole * 0.8, fechados: gole > 0.85, cabeca: -0.04, + bE: [0.15, 0.95], bD: [-0.15 + gole * 1.75, -0.95 - gole * 3.05], + }), cor, k); + xicara(c, b.mD.x, b.mD.y, s, cor); + if (gole < 0.3) vapor(c, b.mD.x, b.mD.y - s * 0.3, s, k, cor); + }, + cozinhar(c, x, y, s, k, cor) { + const ciclo = (k % 2.6) / 2.6, voo = ciclo < 0.55 ? ciclo / 0.55 : -1, h = voo >= 0 ? 4 * voo * (1 - voo) : 0; + const fx = x + s * 1.0, fw = s * 1.7, fh = s * 1.2; + c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.07); + caixa(c, fx, y - fh, fw, fh, s * 0.08); c.fill(); c.stroke(); + caixa(c, fx + fw * 0.15, y - fh * 0.72, fw * 0.7, fh * 0.5, s * 0.06); c.stroke(); + for (let i = 0; i < 3; i++) chama(c, fx + fw * (0.3 + 0.2 * i), y - fh, s * (0.22 + 0.07 * Math.sin(k * 17 + i * 2)), s); + const b = boneco(c, x - s * 0.45, y, s, { + olhoX: 1, olhoY: 0.3 - h, cabeca: -0.05, bE: [-0.4, 0.3], + bD: [1.2 - (voo >= 0 && voo < 0.15 ? 0.25 : 0), 0.35], + }, cor, k); + const px = b.mD.x + s * 0.62, py = b.mD.y + s * 0.02; + c.strokeStyle = css(cor); c.lineWidth = Math.max(2, s * 0.09); + c.beginPath(); c.moveTo(b.mD.x, b.mD.y); c.lineTo(px - s * 0.42, py); c.stroke(); + c.fillStyle = escuro(cor); c.lineWidth = Math.max(1.5, s * 0.06); + c.beginPath(); c.ellipse(px, py, s * 0.46, s * 0.13, 0, 0, Math.PI * 2); c.fill(); c.stroke(); + c.save(); c.translate(px, py - s * 0.1 - h * s * 1.6); c.rotate(voo >= 0 ? voo * Math.PI * 2 : 0); + c.fillStyle = css(AMARELO_); c.beginPath(); c.ellipse(0, 0, s * 0.32, s * 0.08, 0, 0, Math.PI * 2); c.fill(); c.stroke(); + c.restore(); + }, + regar(c, x, y, s, k, cor) { + const cresce = Math.min(1, k / 18), px = x + s * 2.0; + // vaso + c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.07); + c.beginPath(); c.moveTo(px - s * 0.42, y - s * 0.6); c.lineTo(px + s * 0.42, y - s * 0.6); c.lineTo(px + s * 0.3, y); c.lineTo(px - s * 0.3, y); c.closePath(); c.fill(); c.stroke(); + // planta crescendo: caule, folhas e, no fim, a flor + const alt = s * (0.3 + 1.25 * cresce), base = y - s * 0.6, balanca = Math.sin(k * 1.5) * s * 0.06; + c.strokeStyle = css(VERDE_); c.lineWidth = Math.max(1.5, s * 0.07); + c.beginPath(); c.moveTo(px, base); c.quadraticCurveTo(px + balanca, base - alt / 2, px + balanca * 2, base - alt); c.stroke(); + c.fillStyle = css(VERDE_); + for (let i = 1; i <= 3; i++) { + if (cresce < i * 0.22) continue; + const yy = base - alt * i * 0.25, lado = i % 2 ? 1 : -1; + c.beginPath(); c.ellipse(px + balanca * i * 0.5 + lado * s * 0.18, yy, s * 0.2, s * 0.08, lado * -0.5, 0, Math.PI * 2); c.fill(); + } + if (cresce > 0.75) { + const f = (cresce - 0.75) / 0.25, fx = px + balanca * 2, fy = base - alt; + c.fillStyle = css(ROSA_); + for (let i = 0; i < 5; i++) { const a = i * 1.2566 + k * 0.3; c.beginPath(); c.arc(fx + Math.cos(a) * s * 0.16 * f, fy + Math.sin(a) * s * 0.16 * f, s * 0.11 * f, 0, Math.PI * 2); c.fill(); } + c.fillStyle = css(AMARELO_); c.beginPath(); c.arc(fx, fy, s * 0.09 * f, 0, Math.PI * 2); c.fill(); + } + const b = boneco(c, x - s * 0.7, y, s, { olhoX: 1, olhoY: 0.8, cabeca: -0.1, inclina: -0.06, bD: [1.35, 0.35], bE: [-0.3, 0.2] }, cor, k); + // regador inclinado e as gotas caindo na planta + const ang = 0.38, rx = b.mD.x + s * 0.05, ry = b.mD.y; + c.save(); c.translate(rx, ry); c.rotate(ang); + c.fillStyle = claro(cor, 0.95); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06); + caixa(c, 0, -s * 0.22, s * 0.6, s * 0.42, s * 0.08); c.fill(); c.stroke(); + c.beginPath(); c.moveTo(s * 0.55, -s * 0.05); c.lineTo(s * 1.0, -s * 0.32); c.stroke(); + c.restore(); + const ex = rx + Math.cos(ang) * s * 1.0 + Math.sin(ang) * s * 0.32, ey = ry + Math.sin(ang) * s * 1.0 - Math.cos(ang) * s * 0.32; + c.fillStyle = claro(cor, 1); + for (let i = 0; i < 6; i++) { + const f = (k * 1.6 + i / 6) % 1; + c.beginPath(); c.ellipse(ex + (px - ex) * f * 0.8, ey + (base - alt - ey + s * 0.2) * f * f, s * 0.035, s * 0.07, 0, 0, Math.PI * 2); c.fill(); + } + }, + violao(c, x, y, s, k, cor) { + const toque = Math.sin(k * 11); + const b = boneco(c, x, y, s, Object.assign({}, SENTADO, { + olhoX: -0.4, olhoY: 0.7, cabeca: 0.08 * Math.sin(k * 1.1), + bE: [-1.1, -0.9], bD: [-0.2 + toque * 0.12, -1.0], + }), cor, k); + // violão atravessado no colo: corpo perto da mão que toca, braço até a outra mão + const gx = b.quadril.x + s * 0.3, gy = b.quadril.y - s * 0.35, nx = b.mE.x, ny = b.mE.y; + const a = Math.atan2(ny - gy, nx - gx); + c.save(); c.translate(gx, gy); c.rotate(a); + const L = Math.hypot(nx - gx, ny - gy); + c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06); + caixa(c, s * 0.2, -s * 0.08, L - s * 0.1, s * 0.16, s * 0.04); c.fill(); c.stroke(); + caixa(c, L - s * 0.05, -s * 0.12, s * 0.3, s * 0.24, s * 0.06); c.fill(); c.stroke(); + c.fillStyle = claro(cor, 0.95); + c.beginPath(); c.arc(-s * 0.28, 0, s * 0.42, 0, Math.PI * 2); c.fill(); c.stroke(); + c.beginPath(); c.arc(s * 0.22, 0, s * 0.32, 0, Math.PI * 2); c.fill(); c.stroke(); + c.fillStyle = escuro(cor); c.beginPath(); c.arc(s * 0.05, 0, s * 0.11, 0, Math.PI * 2); c.fill(); + c.strokeStyle = css(cor, 0.7); c.lineWidth = Math.max(1, s * 0.015); + for (const d of [-0.04, 0, 0.04]) { c.beginPath(); c.moveTo(-s * 0.5, d * s); c.lineTo(L, d * s); c.stroke(); } + c.restore(); + c.fillStyle = css(cor); c.beginPath(); c.arc(b.mD.x, b.mD.y, s * 0.13, 0, Math.PI * 2); c.fill(); + notas(c, b.cabeca.x - s * 1.6, b.cabeca.y + s * 0.2, s, k, cor, -1); + }, + dancar(c, x, y, s, k, cor) { + const u = (Math.sin(k * 5) + 1) / 2, mexe = Math.sin(k * 5) * 0.18; + // globo espelhado e luzinhas no chão + const gx = x, gy = y - s * 4.5; + c.strokeStyle = css(cor, 0.6); c.lineWidth = Math.max(1, s * 0.03); + c.beginPath(); c.moveTo(gx, gy - s * 0.9); c.lineTo(gx, gy - s * 0.35); c.stroke(); + c.fillStyle = claro(cor, 0.9); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05); + c.beginPath(); c.arc(gx, gy, s * 0.35, 0, Math.PI * 2); c.fill(); c.stroke(); + c.strokeStyle = css(cor, 0.5); + for (let i = -1; i <= 1; i++) { c.beginPath(); c.ellipse(gx, gy, s * 0.35 * Math.abs(Math.cos(k + i)), s * 0.35, 0, 0, Math.PI * 2); c.stroke(); } + const cores = [ROSA_, AMARELO_, VERDE_, cor]; + for (let i = 0; i < 6; i++) { + const lx = x + Math.sin(k * 1.3 + i * 1.7) * s * 2.4, alfa = c.globalAlpha; + c.globalAlpha = alfa * 0.35; c.fillStyle = css(cores[i % 4]); + c.beginPath(); c.ellipse(lx, y, s * 0.35, s * 0.07, 0, 0, Math.PI * 2); c.fill(); + c.globalAlpha = alfa; + } + boneco(c, x, y, s, { + dx: mexe, inclina: mexe * 0.5, cabeca: -mexe * 0.6, olhoY: -0.2, olhoX: mexe * 3, + bE: [-0.7 - 2.1 * u, 0.5], bD: [0.7 + 2.1 * (1 - u), -0.5], + pE: [-0.1 - u * 0.25, u * 0.4], pD: [0.1 + (1 - u) * 0.25, -(1 - u) * 0.4], + }, cor, k); + for (let i = 0; i < 4; i++) { + const f = (k * 0.7 + i / 4) % 1, alfa = c.globalAlpha; + c.globalAlpha = alfa * Math.sin(f * Math.PI); c.fillStyle = css(cores[i]); + estrela(c, x + Math.cos(i * 1.9) * s * 1.6, y - s * (2 + i * 0.5) - f * s * 0.4, s * 0.12); c.globalAlpha = alfa; + } + }, + corda(c, x, y, s, k, cor) { + const th = k * Math.PI * 2 * 0.85, pulo = Math.pow(Math.max(0, Math.cos(th)), 0.6), sobe = pulo * s * 0.45; + c.save(); c.globalAlpha *= 0.3 * (1 - pulo * 0.5); c.fillStyle = css(cor); + c.beginPath(); c.ellipse(x, y, s * 0.6 * (1 - pulo * 0.3), s * 0.08, 0, 0, Math.PI * 2); c.fill(); c.restore(); + const b = boneco(c, x, y - sobe, s, { + olhoY: -0.2, bE: [-0.75 + 0.15 * Math.sin(th), 0.6], bD: [0.75 - 0.15 * Math.sin(th), -0.6], + pE: [-0.06, pulo * 0.3], pD: [0.06, -pulo * 0.3], + }, cor, k); + // a corda passa por baixo dos pés quando ele está no alto do pulo + const my = (b.mE.y + b.mD.y) / 2, embaixo = 2 * (y + s * 0.05) - my, emcima = 2 * (b.cabeca.y - s * 1.0) - my; + const cy = (embaixo + emcima) / 2 + Math.cos(th) * (embaixo - emcima) / 2; + c.strokeStyle = claro(cor, 1); c.lineWidth = Math.max(1.5, s * 0.05); + c.beginPath(); c.moveTo(b.mE.x, b.mE.y); c.quadraticCurveTo(x, cy, b.mD.x, b.mD.y); c.stroke(); + }, + meditar(c, x, y, s, k, cor) { + const flutua = (Math.sin(k * 1.2) + 1) / 2 * s * 0.35, cy = y - s * 1.7 - flutua; + const alfa = c.globalAlpha; + for (let i = 0; i < 3; i++) { // aura + const f = (k * 0.35 + i / 3) % 1; + c.globalAlpha = alfa * 0.35 * (1 - f); c.strokeStyle = css(cor); c.lineWidth = Math.max(1, s * 0.04); + c.beginPath(); c.ellipse(x, cy, s * (1.2 + f * 1.3), s * (1.4 + f * 1.3), 0, 0, Math.PI * 2); c.stroke(); + } + c.globalAlpha = alfa * 0.3; c.fillStyle = css(cor); + c.beginPath(); c.ellipse(x, y, s * 0.8 * (1 - flutua / s), s * 0.09, 0, 0, Math.PI * 2); c.fill(); + c.globalAlpha = alfa; + boneco(c, x, y - s * 0.3 - flutua, s, { + sentado: true, fechados: true, pE: [-1.45, 2.75], pD: [1.45, -2.75], bE: [-0.75, 0.25], bD: [0.75, -0.25], + }, cor, k); + }, + telescopio(c, x, y, s, k, cor) { + // céu estrelado com uma estrela cadente de vez em quando + const alfa = c.globalAlpha; + for (let i = 0; i < 16; i++) { + c.globalAlpha = alfa * (0.35 + 0.65 * Math.abs(Math.sin(k * (1 + rnd(i, 3)) + i))); + c.fillStyle = i % 5 ? claro(cor, 1) : css(AMARELO_); + estrela(c, x + s * (0.2 + rnd(i, 1) * 3.4), y - s * (3.4 + rnd(i, 2) * 1.5), s * (0.05 + 0.05 * rnd(i, 4))); + } + const f = (k % 6) / 1.1; + if (f < 1) { + const sx = x + s * (3.6 - f * 2.2), sy = y - s * (4.9 - f * 0.8); + const g = c.createLinearGradient(sx, sy, sx + s * 0.9, sy - s * 0.33); + g.addColorStop(0, claro(cor, 1)); g.addColorStop(1, css(cor, 0)); + c.globalAlpha = alfa * (1 - f); c.strokeStyle = g; c.lineWidth = Math.max(1.5, s * 0.05); + c.beginPath(); c.moveTo(sx, sy); c.lineTo(sx + s * 0.9, sy - s * 0.33); c.stroke(); + } + c.globalAlpha = alfa; + const b = boneco(c, x - s * 0.7, y, s, { olhoX: 1, olhoY: -1, cabeca: 0.15, bD: [2.1, 0.5], bE: [-0.3, 0.2] }, cor, k); + const ox = b.cabeca.x + s * 0.78, oy = b.cabeca.y + s * 0.3, tx = ox + s * 1.6, ty = oy - s * 0.95; + const mx = (ox + tx) / 2, my = (oy + ty) / 2; + c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06); + c.beginPath(); c.moveTo(mx, my); c.lineTo(mx - s * 0.5, y); c.moveTo(mx, my); c.lineTo(mx + s * 0.5, y); c.moveTo(mx, my); c.lineTo(mx, y); c.stroke(); + c.fillStyle = escuro(cor); + c.beginPath(); capsula(c, ox, oy, tx, ty, s * 0.15); c.fill(); c.stroke(); + c.fillStyle = claro(cor, 1); c.beginPath(); c.arc(tx, ty, s * 0.17, 0, Math.PI * 2); c.fill(); c.stroke(); + }, + pipa(c, x, y, s, k, cor) { + const kx = x + s * (2.3 + 0.45 * Math.sin(k * 0.8)), ky = y - s * (4.2 + 0.35 * Math.cos(k * 1.1)), ang = 0.25 * Math.sin(k * 1.6); + const b = boneco(c, x - s * 0.9, y, s, { olhoX: 1, olhoY: -1, cabeca: 0.12, bD: [2.55, 0.35], bE: [-0.4, 0.3] }, cor, k); + c.strokeStyle = css(cor, 0.8); c.lineWidth = Math.max(1, s * 0.025); + c.beginPath(); c.moveTo(b.mD.x, b.mD.y); + c.quadraticCurveTo((b.mD.x + kx) / 2 + s * 0.4, (b.mD.y + ky) / 2 + s * 0.7, kx, ky + s * 0.45); c.stroke(); + // rabiola com lacinhos + c.save(); c.translate(kx, ky); c.rotate(ang); + c.strokeStyle = css(cor, 0.8); c.beginPath(); c.moveTo(0, s * 0.5); + for (let i = 1; i <= 6; i++) c.lineTo(Math.sin(k * 3 + i) * s * 0.18, s * (0.5 + i * 0.22)); + c.stroke(); + for (let i = 2; i <= 6; i += 2) { + const lx = Math.sin(k * 3 + i) * s * 0.18, ly = s * (0.5 + i * 0.22); + c.fillStyle = css(i % 4 ? ROSA_ : AMARELO_); + c.beginPath(); c.moveTo(lx, ly); c.lineTo(lx - s * 0.1, ly - s * 0.06); c.lineTo(lx - s * 0.1, ly + s * 0.06); c.closePath(); c.fill(); + c.beginPath(); c.moveTo(lx, ly); c.lineTo(lx + s * 0.1, ly - s * 0.06); c.lineTo(lx + s * 0.1, ly + s * 0.06); c.closePath(); c.fill(); + } + const pts = [[0, -0.5], [0.38, 0], [0, 0.5], [-0.38, 0]]; + const cores = [ROSA_, AMARELO_, cor, VERDE_]; + for (let i = 0; i < 4; i++) { + const [ax, ay] = pts[i], [bx, by] = pts[(i + 1) % 4]; + c.fillStyle = css(cores[i]); c.beginPath(); c.moveTo(0, 0); c.lineTo(ax * s, ay * s); c.lineTo(bx * s, by * s); c.closePath(); c.fill(); + } + c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.04); + c.beginPath(); pts.forEach(([px, py], i) => (i ? c.lineTo(px * s, py * s) : c.moveTo(px * s, py * s))); c.closePath(); c.stroke(); + c.restore(); + }, + bolhas(c, x, y, s, k, cor) { + const b = boneco(c, x - s * 0.6, y, s, { olhoX: 0.8, olhoY: -0.6, bD: [1.6, -4.0], bE: [-0.3, 0.2] }, cor, k); + const wx = b.mD.x + s * 0.35, wy = b.mD.y - s * 0.25; + c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05); + c.beginPath(); c.moveTo(b.mD.x, b.mD.y); c.lineTo(wx - s * 0.1, wy + s * 0.08); c.stroke(); + c.beginPath(); c.arc(wx, wy, s * 0.12, 0, Math.PI * 2); c.stroke(); + const alfa = c.globalAlpha; + for (let i = 0; i < 9; i++) { + const f = (k * 0.22 + i / 9) % 1, r = s * (0.12 + 0.18 * rnd(i, 4)); + const bx = wx + f * s * (1.6 + 1.4 * rnd(i, 5)) + Math.sin(k * 2 + i) * s * 0.15, by = wy - f * s * (1.6 + rnd(i, 6)); + c.globalAlpha = alfa * Math.min(1, f / 0.08) * (f > 0.9 ? (1 - f) / 0.1 : 1); + c.fillStyle = css(cor, 0.1); c.strokeStyle = css(cor, 0.9); c.lineWidth = Math.max(1, s * 0.03); + c.beginPath(); c.arc(bx, by, r, 0, Math.PI * 2); c.fill(); c.stroke(); + c.strokeStyle = claro(cor, 1); c.beginPath(); c.arc(bx, by, r * 0.65, -2.4, -1.6); c.stroke(); + } + c.globalAlpha = alfa; + }, + skate(c, x, y, s, k, cor) { + const pos = Math.sin(k * 0.9) * 1.3, vel = Math.cos(k * 0.9), ciclo = k % 4; + const ollie = ciclo < 0.7 ? Math.sin(ciclo / 0.7 * Math.PI) : 0, sobe = ollie * s * 0.6; + const bx = x + pos * s, pe = y - s * 0.3 - sobe; + c.save(); c.globalAlpha *= 0.3; c.fillStyle = css(cor); + c.beginPath(); c.ellipse(bx, y, s * 0.9, s * 0.08, 0, 0, Math.PI * 2); c.fill(); c.restore(); + c.strokeStyle = css(cor, 0.4); c.lineWidth = Math.max(1, s * 0.03); // risquinhos de velocidade + for (let i = 0; i < 3; i++) { const lx = bx - Math.sign(vel) * s * (1.1 + i * 0.25); c.beginPath(); c.moveTo(lx, pe - s * (0.6 + i * 0.4)); c.lineTo(lx - Math.sign(vel) * s * 0.5 * Math.abs(vel), pe - s * (0.6 + i * 0.4)); c.stroke(); } + boneco(c, bx, pe, s, { + olhoX: vel * 0.8, inclina: -vel * 0.1, bE: [-1.35, 0.25 + 0.2 * Math.sin(k * 3)], bD: [1.35, -0.25 - 0.2 * Math.sin(k * 3)], + pE: [-0.25, 0.35], pD: [0.25, -0.35], + }, cor, k); + c.save(); c.translate(bx, pe); c.rotate(-ollie * 0.25); + c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05); + caixa(c, -s * 0.85, 0, s * 1.7, s * 0.12, s * 0.06); c.fill(); c.stroke(); + c.fillStyle = escuro(cor); + for (const lado of [-1, 1]) { c.beginPath(); c.arc(lado * s * 0.55, s * 0.2, s * 0.1, 0, Math.PI * 2); c.fill(); c.stroke(); } + c.restore(); + }, + computador(c, x, y, s, k, cor) { + const mx0 = x + s * 0.3, mx1 = x + s * 2.7, mt = y - s * 1.45; + c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06); + c.beginPath(); c.moveTo(mx0 + s * 0.15, mt); c.lineTo(mx0 + s * 0.15, y); c.moveTo(mx1 - s * 0.15, mt); c.lineTo(mx1 - s * 0.15, y); c.stroke(); + caixa(c, mx0, mt, mx1 - mx0, s * 0.12, s * 0.04); c.fill(); c.stroke(); + const nx = mx0 + s * 0.6, nw = s * 1.4, nh = s * 0.95, ny = mt - s * 0.08 - nh; + caixa(c, nx - s * 0.12, mt - s * 0.08, nw + s * 0.24, s * 0.08, s * 0.03); c.fill(); c.stroke(); + caixa(c, nx, ny, nw, nh, s * 0.06); c.fill(); c.stroke(); + c.save(); caixa(c, nx + s * 0.06, ny + s * 0.06, nw - s * 0.12, nh - s * 0.12, s * 0.03); c.clip(); + const linhas = Math.floor(k * 2.5), visiveis = 6, ini = Math.max(0, linhas - visiveis); + const tintas = [cor, VERDE_, AMARELO_, ROSA_]; + for (let i = ini; i < linhas; i++) { + const yy = ny + s * 0.14 + (i - ini) * s * 0.13, recuo = (rnd(i, 7) < 0.4 ? 1 : 0) * s * 0.15; + c.fillStyle = css(tintas[i % 4], 0.85); c.fillRect(nx + s * 0.12 + recuo, yy, (nw - s * 0.4) * (0.3 + 0.6 * rnd(i, 8)), s * 0.05); + } + if (Math.floor(k * 2) % 2) { c.fillStyle = claro(cor, 1); c.fillRect(nx + s * 0.12, ny + s * 0.14 + Math.min(linhas, visiveis) * s * 0.13, s * 0.04, s * 0.08); } + c.restore(); + const tecla = Math.sin(k * 20) * 0.05; + boneco(c, x - s * 0.55, y, s, Object.assign({}, SENTADO, { + olhoX: 1, olhoY: 0.2, cabeca: -0.05, bE: [0.9 + tecla, 0.9], bD: [1.35 - tecla, 0.45], + }), cor, k); + }, + pescar(c, x, y, s, k, cor) { + const ciclo = k % 8, fisgou = ciclo > 6.2 && ciclo < 7.6, puxa = fisgou ? suave((ciclo - 6.2) / 0.5) : 0; + // água: ondinhas à direita do píer + c.strokeStyle = css(cor, 0.55); c.lineWidth = Math.max(1, s * 0.035); + for (let fila = 0; fila < 3; fila++) { + c.beginPath(); + for (let i = 0; i <= 24; i++) { + const wx = x + s * (0.5 + i * 0.14), wy = y - s * 0.05 + fila * s * 0.16 + Math.sin(k * 2 + i * 0.8 + fila) * s * 0.04; + if (i) c.lineTo(wx, wy); else c.moveTo(wx, wy); + } + c.stroke(); + } + c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06); + caixa(c, x - s * 2.0, y - s * 0.22, s * 2.5, s * 0.22, s * 0.05); c.fill(); c.stroke(); + const b = boneco(c, x - s * 0.8, y - s * 0.22, s, Object.assign({}, SENTADO, { + olhoX: 1, olhoY: 0.6 - puxa * 1.4, bD: [2.0 - puxa * 0.4, -0.9], bE: [0.5, 1.2], + }), cor, k); + const vx = b.mD.x + s * (1.4 - puxa * 0.3), vy = b.mD.y - s * (1.6 + puxa * 0.3); + c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06); + c.beginPath(); c.moveTo(b.mE.x, b.mE.y); c.lineTo(b.mD.x, b.mD.y); c.lineTo(vx, vy); c.stroke(); + const belisca = ciclo > 5.5 && ciclo < 6.2 ? Math.sin(k * 30) * s * 0.06 : 0; + let lx = x + s * 2.6, ly = y - s * 0.05 + Math.sin(k * 2) * s * 0.04 + belisca; + if (fisgou) { // o peixe pula preso na linha + lx = x + s * (2.6 - puxa * 0.6); ly = y - s * (0.2 + Math.sin(puxa * Math.PI) * 1.3 + puxa * 0.3); + c.save(); c.translate(lx, ly + s * 0.25); c.rotate(Math.sin(k * 14) * 0.4); + c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.04); + c.beginPath(); c.ellipse(0, 0, s * 0.12, s * 0.28, 0, 0, Math.PI * 2); c.fill(); c.stroke(); + c.beginPath(); c.moveTo(0, s * 0.25); c.lineTo(-s * 0.14, s * 0.45); c.lineTo(s * 0.14, s * 0.45); c.closePath(); c.fill(); c.stroke(); + c.fillStyle = escuro(cor); c.beginPath(); c.arc(s * 0.04, -s * 0.12, s * 0.03, 0, Math.PI * 2); c.fill(); + c.restore(); + } + c.strokeStyle = css(cor, 0.85); c.lineWidth = Math.max(1, s * 0.02); + c.beginPath(); c.moveTo(vx, vy); c.quadraticCurveTo((vx + lx) / 2, Math.max(vy, ly) - s * (fisgou ? 0.1 : 0.4), lx, ly); c.stroke(); + if (!fisgou) { + c.fillStyle = css(ROSA_); c.beginPath(); c.arc(lx, ly, s * 0.09, Math.PI, 0); c.fill(); + c.fillStyle = claro(cor, 1); c.beginPath(); c.arc(lx, ly, s * 0.09, 0, Math.PI); c.fill(); + } + }, }; /** Desenha a cena da atividade (camada atrás dos olhos). o = { t, cor }. */ diff --git a/tests/test_vida.py b/tests/test_vida.py index f951d3f..b86ed1f 100644 --- a/tests/test_vida.py +++ b/tests/test_vida.py @@ -100,11 +100,14 @@ class VidaTest(unittest.TestCase): self.v.celular_online = lambda: True self.v.necessidades.update(energia=80, alegria=70, social=70, diversao=10) # entediado self.v.rng.random = lambda: 0.0 # sorteio sempre "sim" - ato = self.v.agir(DIA) + ato = self.v.agir(DIA) # entediado: vai fazer algo self.assertTrue(ato.startswith('atividade ')) - self.assertIn(ato.split()[1], ('videogame', 'bola', 'pintar', 'ler')) - self.assertEqual(self.ordens[-1], ato) + nome, segundos = self.ordens[-1].split()[1:] + self.assertEqual(ato, 'atividade ' + nome) + self.assertTrue(120 <= float(segundos) <= 300) # por alguns minutos self.assertEqual(self.v.diario[0]['tipo'], 'atividade') + self.v.celular_atividade = lambda: nome # fazendo: diversão sobe + self.v.passar_tempo(DIA + 180) self.assertGreater(self.v.necessidades['diversao'], 10) self.v.celular_atividade = lambda: 'videogame' # ocupado: não interrompe self.assertIsNone(self.v.agir(DIA + 60)) @@ -113,6 +116,49 @@ class VidaTest(unittest.TestCase): self.v.config['noite'] = False self.assertFalse(str(self.v.agir(noite)).startswith('atividade')) # de madrugada, nunca + def test_exausto_dorme_e_o_sono_e_respeitado(self): + self.v.necessidades['energia'] = 15 + self.assertEqual(self.v.agir(DIA), 'dormir') + self.assertTrue(self.v.sono_profundo) + self.assertIn('sono profundo', self.ordens) + self.v.necessidades['energia'] = 60 + self.assertIsNone(self.v.agir(DIA + 60)) # ainda não descansou + self.v.necessidades['energia'] = 96 + self.assertEqual(self.v.agir(DIA + 120), 'acordar') + self.assertFalse(self.v.sono_profundo) + self.assertIn('sono leve', self.ordens) + + def test_acordado_de_proposito_encerra_o_sono_pesado(self): + self.v.necessidades['energia'] = 15 + self.v.agir(DIA) + self.v.celular_dormindo = lambda: False # tocaram na tela + self.v.tick(DIA + 30) + self.assertFalse(self.v.sono_profundo) + self.assertEqual(self.v.diario[0]['texto'], 'Acordou antes de descansar') + + def test_cochilo_quando_cansado(self): + self.v.celular_online = lambda: True + self.v.rng.random = lambda: 0.0 + self.v.necessidades.update(energia=35, diversao=80) + self.assertEqual(self.v.agir(DIA), 'atividade dormir') + self.v.celular_atividade = lambda: 'dormir' # na caminha: recupera + antes = self.v.necessidades['energia'] + self.v.passar_tempo(DIA + 300) + self.assertGreater(self.v.necessidades['energia'], antes) + + def test_alegria_caindo_deixa_bravo(self): + self.v.necessidades.update(energia=80, social=80, diversao=80, alegria=100) + self.assertEqual(self.v.zanga(), 0.0) + self.v.necessidades['alegria'] = 42 + self.assertEqual(self.v.zanga(), 0.4) + self.v.tick(DIA + 5) + self.assertIn('zanga 0.4', self.ordens) + self.v.necessidades['alegria'] = 20 + self.assertEqual(self.v.humor(), 'bravo') + self.assertEqual(self.v.zanga(), 0.9) + self.v.dormindo_proprio = True # dormindo, sem cara feia + self.assertEqual(self.v.zanga(), 0.0) + def test_horario_da_madrugada(self): from controle_pc import madrugada h = lambda hora: time.mktime((2026, 10, 6, hora, 10, 0, 0, 0, -1)) diff --git a/vida.py b/vida.py index aa0c337..463822f 100644 --- a/vida.py +++ b/vida.py @@ -48,7 +48,7 @@ CUIDADOS = {'carinho': 'Recebeu carinho', 'cutucar': 'Levou uma cutucada', 'brin HUMORES = { 'dormindo': ('😴', 'Dormindo'), 'cansado': ('🥱', 'Cansado'), 'carente': ('🥺', 'Carente'), - 'entediado': ('😐', 'Entediado'), 'triste': ('😢', 'Triste'), 'feliz': ('😄', 'Feliz'), + 'entediado': ('😐', 'Entediado'), 'triste': ('😢', 'Triste'), 'bravo': ('😠', 'Mal-humorado'), 'feliz': ('😄', 'Feliz'), 'tranquilo': ('🙂', 'Tranquilo'), } @@ -68,18 +68,30 @@ ATOS = { 'carente_perto': [('acenar', 3, 'Acenou chamando atenção'), ('espiar', 2, 'Espiou quem está aí'), ('coracao', 2, 'Pediu carinho'), ('surpreso', 1, 'Fez cara de "oi!"'), ('timido', 1, 'Ficou tímido')], 'triste': [('triste', 3, 'Ficou tristinho'), ('olhar_baixo', 2, 'Olhou para baixo'), ('pensar', 1, 'Ficou pensativo')], + 'bravo': [('bravo', 3, 'Ficou emburrado'), ('olhar_baixo', 2, 'Bufou de mau humor'), ('confuso', 1, 'Resmungou sozinho')], 'cansado': [('bocejar', 3, 'Bocejou'), ('piscar2', 1, 'Piscou devagar'), ('olhar_baixo', 2, 'Cochilou de olho aberto'), ('tonto', 1, 'Ficou meio tonto de sono')], } -# Atividades (momentos dele): o bonequinho faz algo na tela. Por humor: (atividade, peso, diário, efeito) -ATIVIDADES = { - 'feliz': [('cantar', 3, 'Foi cantar'), ('bola', 2, 'Foi jogar bola'), ('malhar', 1, 'Foi malhar')], - 'tranquilo': [('ler', 3, 'Foi ler um livro'), ('pintar', 2, 'Foi pintar um quadro'), ('cantar', 1, 'Foi cantar')], - 'entediado': [('videogame', 3, 'Foi jogar videogame'), ('bola', 2, 'Foi jogar bola'), - ('pintar', 1, 'Foi pintar um quadro'), ('ler', 1, 'Foi ler um livro')], +# Como no The Sims, cada necessidade baixa puxa um comportamento: +# energia < COCHILO: às vezes tira um cochilo (cena da cama); < SONO: dorme de verdade e o sono é +# respeitado (som, movimento e PIR não acordam) até DESCANSADO; alegria caindo: o rosto vai ficando +# bravo (zanga); diversão < TEDIO: vai fazer uma atividade por alguns minutos. +COCHILO, SONO, DESCANSADO, TEDIO = 40, 20, 95, 45 +ATIVAS = ['bola', 'malhar', 'corda', 'skate', 'dancar', 'pipa'] # gastam energia +CALMAS = ['ler', 'cafe', 'meditar', 'telescopio', 'pescar', 'computador', 'regar'] # quase não gastam +CRIATIVAS = ['pintar', 'violao', 'cantar', 'cozinhar', 'bolhas', 'videogame'] +NOME_ATIVIDADE = { + 'ler': 'Foi ler um livro', 'cantar': 'Foi cantar', 'bola': 'Foi jogar bola', 'videogame': 'Foi jogar videogame', + 'pintar': 'Foi pintar um quadro', 'malhar': 'Foi malhar', 'dormir': 'Tirou um cochilo', 'cafe': 'Foi tomar um café', + 'cozinhar': 'Foi cozinhar', 'regar': 'Foi regar a plantinha', 'violao': 'Foi tocar violão', 'dancar': 'Foi dançar', + 'corda': 'Foi pular corda', 'meditar': 'Foi meditar', 'telescopio': 'Foi olhar as estrelas', 'pipa': 'Foi soltar pipa', + 'bolhas': 'Foi fazer bolhas de sabão', 'skate': 'Foi andar de skate', 'computador': 'Foi mexer no computador', + 'pescar': 'Foi pescar', } -GASTO_ATIVIDADE = {'malhar': 8, 'bola': 5, 'cantar': 3} # energia gasta +# Efeito por minuto enquanto faz a atividade +POR_MINUTO_DIVERSAO, POR_MINUTO_ALEGRIA = 6.0, 1.5 +ENERGIA_POR_MINUTO = {**{a: -2.0 for a in ATIVAS}, **{a: -0.2 for a in CALMAS}, 'cafe': 3.0} # café dá pique # Assuntos das figurinhas que ele inventa sozinho, por humor e por hora do dia. TEMAS = { @@ -88,6 +100,7 @@ TEMAS = { 'entediado': ['um foguete', 'um planeta com anel', 'uma pipa', 'um dado', 'um controle de videogame', 'um pião'], 'carente': ['um coração com curativo', 'um ursinho de pelúcia', 'uma carta de amor', 'dois corações juntos'], 'triste': ['uma nuvem de chuva', 'um guarda-chuva', 'uma gota d\'água'], + 'bravo': ['uma nuvem de tempestade', 'um vulcão', 'um raio', 'uma pimenta'], 'cansado': ['uma lua com touca de dormir', 'uma xícara de café', 'um travesseiro', 'uma bateria fraca'], } PERIODOS = [(5, 12, 'de manhã', ['um sol nascendo', 'um pãozinho', 'uma xícara de café']), @@ -115,6 +128,11 @@ class Vida: self.celular_online = lambda: False self.alguem_perto = lambda: False # PIR/movimento recente self.celular_atividade = lambda: None # atividade que o celular está fazendo agora + self.sono_profundo = False # dormindo de cansaço: sensores não acordam + self.dormiu_em = 0.0 + self.atividade_em = 0.0 # quando começou a última atividade + self.ultima_atividade = None + self.zanga_enviada = None self.bloqueado = lambda: False # E-Stop ou modo presença com ninguém em casa self.pode_gemini = lambda: bool(os.environ.get('GEMINI_API_KEY')) self.madrugada = self.noite_padrao # o main troca pelo horário dos ajustes @@ -178,6 +196,10 @@ class Vida: if not limpo['ligado']: self.enfileirar('humor normal') # o celular volta aos gestos soltos de sempre self.enfileirar('ritmo normal') # e à sensibilidade dos Ajustes + self.enfileirar('zanga 0.0') + if self.sono_profundo: + self.sono_profundo = False + self.enfileirar('sono leve') self.proximo_ato = time.time() + 5 if self.config['ligado'] and 'ritmo' in limpo: self.enfileirar('ritmo ' + self.config['ritmo']) @@ -212,11 +234,17 @@ class Vida: if n['diversao'] < 25: return 'entediado' if n['alegria'] < 30: - return 'triste' + return 'bravo' if n['alegria'] >= 65 and n['social'] >= 40 and n['diversao'] >= 40: return 'feliz' return 'tranquilo' + def zanga(self): + """0 a 1: o quanto o rosto está emburrado. Começa abaixo de 60 de alegria e piora devagar.""" + if self.dormindo(): + return 0.0 + return round(max(0.0, min(1.0, (60 - self.necessidades['alegria']) / 45)), 1) + def anotar(self, tipo, texto, agora=None): with self.lock: self.diario.insert(0, dict(horario=agora or time.time(), tipo=tipo, texto=texto)) @@ -234,6 +262,7 @@ class Vida: humor=h, emoji=HUMORES[h][0], nome_humor=HUMORES[h][1], dormindo=self.dormindo(), nascimento=self.nascimento, idade_dias=int((agora - self.nascimento) // 86400), emojis_hoje=self.emojis_hoje(agora), ultimo_emoji=self.emojis.get('ultimo') or None, + zanga=self.zanga(), sono_profundo=self.sono_profundo, pode_criar=self.motivo_sem_emoji(agora) or '', diario=self.diario[:30]) def avisar(self): @@ -244,13 +273,18 @@ class Vida: def passar_tempo(self, agora): minutos = max(0.0, min(10.0, (agora - self.ultimo_tick) / 60)) # pausa longa (placa desligada) não zera tudo self.ultimo_tick = agora - dormindo = self.dormindo() + atividade = self.celular_atividade() + dormindo = self.dormindo() or atividade == 'dormir' with self.lock: n = self.necessidades for k, q in QUEDA.items(): n[k] = _limitar(n[k] - q * minutos * (SONO_LENTO if dormindo else 1)) if dormindo: n['energia'] = _limitar(n['energia'] + RECUPERA_SONO * minutos) + elif atividade: + n['diversao'] = _limitar(n['diversao'] + (QUEDA['diversao'] + POR_MINUTO_DIVERSAO) * minutos) + n['alegria'] = _limitar(n['alegria'] + POR_MINUTO_ALEGRIA * minutos) + n['energia'] = _limitar(n['energia'] + ENERGIA_POR_MINUTO.get(atividade, -0.5) * minutos) # alegria acompanha devagar a média de companhia e diversão alvo = (n['social'] + n['diversao']) / 2 + 10 n['alegria'] = _limitar(n['alegria'] + (alvo - n['alegria']) * min(1.0, 0.05 * minutos)) @@ -284,18 +318,46 @@ class Vida: self.avisar() return self.status(agora) - def dormir(self, agora, motivo): + def dormir(self, agora, motivo, profundo=False): self.dormindo_proprio = True + self.dormiu_em = agora self.enfileirar('dormir') + if profundo: # cansado de verdade: sono respeitado até descansar + self.sono_profundo = True + self.enfileirar('sono profundo') if motivo: self.anotar('sono', motivo, agora) def acordar(self, agora, motivo): self.dormindo_proprio = False + if self.sono_profundo: + self.sono_profundo = False + self.enfileirar('sono leve') self.enfileirar('acordar') if motivo: self.anotar('sono', motivo, agora) + def escolher_atividade(self): + """Pelo pique: cansado prefere coisas calmas (e às vezes um café); cheio de energia, coisas agitadas.""" + e = self.necessidades['energia'] + if 35 <= e < 55 and self.rng.random() < 0.35: + return 'cafe' + if e < 50: + lista = CALMAS * 2 + CRIATIVAS + elif e > 70: + lista = ATIVAS * 2 + CRIATIVAS + CALMAS + else: + lista = ATIVAS + CRIATIVAS * 2 + CALMAS + opcoes = [a for a in lista if a != self.ultima_atividade] or lista + return self.rng.choice(opcoes) + + def comecar_atividade(self, agora, nome, segundos): + self.enfileirar('atividade %s %d' % (nome, segundos)) + self.atividade_em = agora + self.ultima_atividade = nome + self.anotar('atividade', NOME_ATIVIDADE.get(nome, 'Foi fazer algo'), agora) + return 'atividade ' + nome + def pir_acorda(self, agora=None): """Disparo do PIR com o rosto dormindo: acorda? No ritmo calmo precisa de vários em 2 min.""" agora = agora or time.time() @@ -376,30 +438,31 @@ class Vida: n = self.necessidades perto = self.alguem_perto() if self.dormindo(): - # de madrugada nunca acorda sozinho (nem com gente se mexendo na cama); de dia, quando descansou - if n['energia'] >= 95 and not self.madrugada(agora): + # de madrugada nunca acorda sozinho (nem com gente se mexendo na cama); de dia, só descansado + if n['energia'] >= DESCANSADO and not self.madrugada(agora): self.acordar(agora, 'Acordou descansado') return 'acordar' return None if self.config['noite'] and self.madrugada(agora) and agora - self.interagiu_em > PEDIU_ATENCAO: self.dormir(agora, 'Ficou tarde, foi dormir') return 'dormir' - if n['energia'] < 20: - self.dormir(agora, 'Ficou cansado e foi tirar um cochilo') + if n['energia'] < SONO: + self.dormir(agora, 'Ficou exausto e foi dormir', profundo=True) return 'dormir' humor = self.humor() if self.celular_atividade(): return None # ocupado com uma atividade: deixa terminar - # momentos dele: ler, cantar, jogar bola, videogame... (de dia, com o celular online) - if humor in ATIVIDADES and self.celular_online() and not self.madrugada(agora) and self.rng.random() < 0.18: - nome, texto = self.escolher(ATIVIDADES[humor]) - self.enfileirar('atividade ' + nome) - with self.lock: - n['diversao'] = _limitar(n['diversao'] + 18) - n['alegria'] = _limitar(n['alegria'] + 5) - n['energia'] = _limitar(n['energia'] - GASTO_ATIVIDADE.get(nome, 2)) - self.anotar('atividade', texto, agora) - return 'atividade ' + nome + dia = self.celular_online() and not self.madrugada(agora) + folga = agora - self.atividade_em > 90 # um respiro entre uma atividade e outra + # cansado: um cochilo na caminha de vez em quando (recupera energia sem o sono pesado) + if dia and folga and n['energia'] < COCHILO and self.rng.random() < 0.35: + return self.comecar_atividade(agora, 'dormir', self.rng.uniform(120, 240)) + # entediado: vai fazer alguma coisa por alguns minutos (pode ser interrompido) + if dia and folga and n['diversao'] < TEDIO and self.rng.random() < 0.75: + return self.comecar_atividade(agora, self.escolher_atividade(), self.rng.uniform(120, 300)) + # bem: de vez em quando, uma atividade curtinha por vontade própria + if dia and folga and humor in ('feliz', 'tranquilo') and self.rng.random() < 0.1: + return self.comecar_atividade(agora, self.escolher_atividade(), self.rng.uniform(40, 90)) # habilidade: inventar uma figurinha nova (raro, com limite por dia e intervalo) if not self.motivo_sem_emoji(agora) and self.celular_online() and self.rng.random() < 0.08: if self.criar_figurinha(agora, humor): @@ -426,12 +489,22 @@ class Vida: return None ato = None humor = self.humor() + # acordaram ele de propósito (toque, painel) antes de descansar: o sono pesado acaba aqui + if self.sono_profundo and not self.dormindo() and agora - self.dormiu_em > 15: + self.sono_profundo = False + self.enfileirar('sono leve') + self.anotar('sono', 'Acordou antes de descansar', agora) if not self.bloqueado(): # o celular escolhe os gestos soltos pelo humor; reenvia de tempos em tempos (pode ter reiniciado) - if humor != self.humor_enviado[0] or agora - self.humor_enviado[1] > 60: + zanga = self.zanga() + if humor != self.humor_enviado[0] or agora - self.humor_enviado[1] > 60 or zanga != self.zanga_enviada: self.enfileirar('humor ' + humor) self.enfileirar('ritmo ' + self.config['ritmo']) + self.enfileirar('zanga %.1f' % zanga) + if self.sono_profundo: + self.enfileirar('sono profundo') self.humor_enviado = (humor, agora) + self.zanga_enviada = zanga if agora >= self.proximo_ato: ato = self.agir(agora) lo, hi = RITMOS[self.config['ritmo']] diff --git a/web/src/lib/comandos.svelte.ts b/web/src/lib/comandos.svelte.ts index 883f74c..969a4bc 100644 --- a/web/src/lib/comandos.svelte.ts +++ b/web/src/lib/comandos.svelte.ts @@ -41,6 +41,20 @@ export const ATIVIDADES_INFO: Record = videogame: { nome: 'Videogame', emoji: '🎮' }, pintar: { nome: 'Pintar', emoji: '🎨' }, malhar: { nome: 'Malhar', emoji: '🏋️' }, + dormir: { nome: 'Cochilar', emoji: '🛏️' }, + cafe: { nome: 'Café', emoji: '☕' }, + cozinhar: { nome: 'Cozinhar', emoji: '🍳' }, + regar: { nome: 'Regar planta', emoji: '🪴' }, + violao: { nome: 'Violão', emoji: '🎸' }, + dancar: { nome: 'Dançar', emoji: '💃' }, + corda: { nome: 'Pular corda', emoji: '🪢' }, + meditar: { nome: 'Meditar', emoji: '🧘' }, + telescopio: { nome: 'Telescópio', emoji: '🔭' }, + pipa: { nome: 'Pipa', emoji: '🪁' }, + bolhas: { nome: 'Bolhas', emoji: '🫧' }, + skate: { nome: 'Skate', emoji: '🛹' }, + computador: { nome: 'Computador', emoji: '💻' }, + pescar: { nome: 'Pescar', emoji: '🎣' }, }; export const atividade = (nome: string) => tentar(() => postar('/comando', { tipo: 'atividade', valor: nome })); diff --git a/web/src/lib/componentes/VidaCartao.svelte b/web/src/lib/componentes/VidaCartao.svelte index 01b2976..2f2d7fb 100644 --- a/web/src/lib/componentes/VidaCartao.svelte +++ b/web/src/lib/componentes/VidaCartao.svelte @@ -57,7 +57,9 @@
{v.config.ligado ? v.nome_humor : 'Modo autônomo desligado'}{#if v.config.ligado}{/if} - {idade(v.idade_dias)}{v.config.ligado && v.diario[0] ? ' · ' + v.diario[0].texto : ''} + + {idade(v.idade_dias)}{v.config.ligado && v.sono_profundo ? ' · sono pesado: só acorda descansado' : v.config.ligado && v.diario[0] ? ' · ' + v.diario[0].texto : ''} +
configurarVida({ ligado: l })} /> diff --git a/web/src/lib/guia.ts b/web/src/lib/guia.ts index d6675a0..974759f 100644 --- a/web/src/lib/guia.ts +++ b/web/src/lib/guia.ts @@ -42,12 +42,13 @@ export const TOPICOS: Record = { emoji: '🐣', onde: 'Ao vivo › Vida', resumo: - 'Com o modo vida ligado, o Carinha funciona como um bichinho virtual: tem necessidades que mudam com o tempo e com o que acontece em volta, e decide sozinho o que fazer pelo humor.', + 'Com o modo vida ligado, o Carinha funciona como um Sim: tem necessidades que caem com o tempo, e cada necessidade baixa puxa um comportamento. Ele decide sozinho, mas você sempre pode interromper.', itens: [ - { emoji: '⚡', nome: 'Energia', texto: 'Cai enquanto ele está acordado e enche dormindo. Muito baixa, ele vai tirar um cochilo.' }, - { emoji: '😊', nome: 'Alegria', texto: 'Acompanha devagar a companhia e a diversão.' }, + { emoji: '⚡', nome: 'Energia', texto: 'Cai acordado e mais rápido nas atividades agitadas; o café dá um pique. Abaixo de 40%, às vezes tira um cochilo na caminha. Abaixo de 20%, dorme de verdade e o sono é respeitado: som, movimento e PIR não acordam até ele chegar a 95%.' }, + { emoji: '😊', nome: 'Alegria', texto: 'Acompanha a companhia e a diversão. Abaixo de 60%, o rosto vai ficando emburrado aos poucos (testa franzida, boca para baixo e, no fim, olhos bravos). Abaixo de 30%, fica mal-humorado.' }, { emoji: '💬', nome: 'Companhia', texto: 'Sobe quando alguém aparece (PIR), conversa com ele ou faz carinho. Baixa, ele fica carente.' }, - { emoji: '🎈', nome: 'Diversão', texto: 'Sobe com movimento em volta, brincadeiras e quando ele inventa algo. Baixa, ele fica entediado.' }, + { emoji: '🎈', nome: 'Diversão', texto: 'Abaixo de 45%, ele vai fazer uma atividade por 2 a 5 minutos (cansado escolhe coisas calmas; cheio de pique, coisas agitadas). Enquanto faz, a diversão sobe.' }, + { emoji: '🛌', nome: 'Sono do celular', texto: 'Com o modo vida ligado, quem decide dormir é a energia; o celular não adormece mais só porque ficou 20 s em silêncio.' }, { emoji: '🎨', nome: 'Inventar figurinhas sozinho', texto: 'De vez em quando ele tem uma ideia e desenha uma figurinha nova com o Gemini, que vai para a biblioteca de Reações. No máximo 3 por dia, com 90 min entre elas.' }, { emoji: '🌙', nome: 'Dormir à noite', texto: 'Na madrugada ele vai dormir mesmo com gente se mexendo por perto. Carinho ou conversa seguram ele acordado por 10 minutos.' }, { emoji: '📖', nome: 'Diário', texto: 'Lista do que ele fez sozinho e dos cuidados que recebeu.' }, @@ -77,7 +78,7 @@ export const TOPICOS: Record = { { emoji: '🙂', nome: 'Tranquilo', texto: 'Normal. Olha em volta, pisca devagar, meio sorriso.' }, { emoji: '😐', nome: 'Entediado', texto: 'Diversão baixa. Fica pensativo, boca de lado, procura o que fazer e às vezes toca uma reação da biblioteca.' }, { emoji: '🥺', nome: 'Carente', texto: 'Companhia baixa. Com alguém perto, acena e pede carinho; sozinho, fica cabisbaixo.' }, - { emoji: '😢', nome: 'Triste', texto: 'Alegria baixa. Fica tristinho, olha para baixo e mexe menos.' }, + { emoji: '😠', nome: 'Mal-humorado', texto: 'Alegria abaixo de 30%. Testa franzida, olhos bravos, resmunga. Carinho, conversa e brincadeira resolvem.' }, { emoji: '🥱', nome: 'Cansado', texto: 'Energia baixa. Boceja, suspira e logo vai dormir.' }, { emoji: '😴', nome: 'Dormindo', texto: 'Olhos fechados, respira devagar e recupera energia.' }, ], @@ -168,15 +169,13 @@ export const TOPICOS: Record = { emoji: '🎮', onde: 'Ao vivo › Gestos › Atividades', resumo: - 'De vez em quando ele faz alguma coisa: os olhos sobem e encolhem, e embaixo aparece um bonequinho dele, na cor dos olhos, fazendo a atividade. Os olhos grandes acompanham (seguem a bola, leem as linhas, olham a TV).', + 'São 20 atividades. A tela se divide: o rosto vai para a esquerda e à direita aparece um bonequinho dele, na cor dos olhos, fazendo a atividade. Os olhos grandes acompanham (seguem a bola, leem as linhas, olham a TV, a pipa, o peixe).', itens: [ - { emoji: '📖', nome: 'Ler', texto: 'Sentado com um livro; vira a página de vez em quando e os olhos leem linha a linha.' }, - { emoji: '🎤', nome: 'Cantar', texto: 'Com microfone, balançando, notas subindo e boca cantando.' }, - { emoji: '⚽', nome: 'Jogar bola', texto: 'Faz embaixadinha; os olhos seguem a bola.' }, - { emoji: '🎮', nome: 'Videogame', texto: 'Sentado com o controle na frente de uma TV com um joguinho; comemora quando ganha.' }, - { emoji: '🎨', nome: 'Pintar', texto: 'No cavalete; o quadro vai ganhando pinceladas coloridas.' }, - { emoji: '🏋️', nome: 'Malhar', texto: 'Levanta halteres e sua.' }, - { emoji: '🐣', nome: 'No modo vida', texto: 'Ele escolhe sozinho pelo humor: feliz canta e joga bola; tranquilo lê e pinta; entediado joga videogame. Nunca de madrugada.' }, + { emoji: '⚽', nome: 'Agitadas (gastam energia)', texto: 'Jogar bola, malhar, pular corda, skate, dançar (com globo espelhado) e soltar pipa.' }, + { emoji: '📖', nome: 'Calmas', texto: 'Ler, tomar café (dá energia), meditar flutuando, olhar estrelas no telescópio, pescar, computador e regar a plantinha (que cresce e dá flor).' }, + { emoji: '🎨', nome: 'Criativas', texto: 'Pintar, tocar violão, cantar, cozinhar (vira a panqueca), bolhas de sabão e videogame.' }, + { emoji: '🛏️', nome: 'Cochilar', texto: 'Deita na caminha com cobertor e Zzz; recupera energia como dormir.' }, + { emoji: '🐣', nome: 'No modo vida', texto: 'Com a diversão baixa, ele escolhe uma pelo pique e fica nela de 2 a 5 minutos; cansado, cochila. Nunca de madrugada.' }, { emoji: '👆', nome: 'Parar', texto: 'Botão Parar no painel ou um toque na tela do celular. Dormir também encerra a atividade.' }, ], }, diff --git a/web/src/lib/tipos.ts b/web/src/lib/tipos.ts index 8bb872e..7be1c45 100644 --- a/web/src/lib/tipos.ts +++ b/web/src/lib/tipos.ts @@ -97,6 +97,10 @@ export interface Vida { idade_dias: number; emojis_hoje: number; ultimo_emoji: number | null; + /** 0 a 1: o quanto está emburrado (alegria baixa). */ + zanga?: number; + /** Dormindo de cansaço: som, movimento e PIR não acordam. */ + sono_profundo?: boolean; /** '' quando pode inventar figurinha agora; senão, o motivo. */ pode_criar: string; diario: { horario: number; tipo: 'acao' | 'sono' | 'emoji' | 'cuidado' | 'vida' | 'atividade'; texto: string }[];