/* rosto-motor.js — motor de pálpebras dos rostos novos (Carinha Viva, Bloco, Matriz, Lente). * * O carinha.html continua decidindo O QUE o rosto faz (gestos, reações, sensores, rastreamento); * aqui só se desenha. Cada "forma" vira posição de pálpebra (cima, inclinação, baixo) e o motor * suaviza a passagem entre elas, então feliz → triste não salta de um desenho para outro. * Também acrescenta vida: respiração, sacadas e achatamento na piscada. */ (function () { 'use strict'; // forma → pálpebras. top: pálpebra de cima (0 aberto, 1 fechado); slant: + = canto interno sobe; // bot: pálpebra de baixo sobe ("olho sorrindo"). const PALPEBRAS = { aberto: { top: 0, slant: 0, bot: 0 }, surpreso: { top: 0, slant: 0, bot: 0 }, feliz: { top: 0, slant: 0, bot: 0.46 }, coracao: { top: 0, slant: 0, bot: 0.42 }, triste: { top: 0.3, slant: 0.32, bot: 0 }, bravo: { top: 0.34, slant: -0.42, bot: 0.06 }, dormir: { top: 0.9, slant: 0.04, bot: 0.08 }, }; // Medidas em frações do raio vertical do olho clássico (RY), para casar com o layout atual. const ESTILOS = { vivo: { w: 1.0, h: 1.45, r: 0.5, brilho: true }, bloco: { w: 1.35, h: 1.25, r: 0.22, brilho: true }, matriz: { w: 1.0, h: 1.45, r: 0.5, brilho: false, pontos: true }, lente: { w: 1.15, h: 1.15, r: 0.58, lente: true }, }; // Formato do olho, escolhido no painel e combinável com Viva, Bloco e Matriz. const FORMATOS = { padrao: null, redondo: { w: 1.15, h: 1.15, r: 0.58 }, pilula: { w: 0.8, h: 1.55, r: 0.4 }, quadrado: { w: 1.3, h: 1.3, r: 0.12 }, largo: { w: 1.65, h: 0.9, r: 0.3 }, }; let tela = null, ctx = null, camada = null, cctx = null, baixa = null, bctx = null; const estado = [novo(), novo()]; const sacada = { x: 0, y: 0, alvoX: 0, alvoY: 0, proxima: 0 }; let antes = 0; function novo() { return { top: 0, slant: 0, bot: 0, coracao: 0 }; } const css = (c, a) => 'rgba(' + c.map((v) => Math.round(v)).join(',') + ',' + (a === undefined ? 1 : a) + ')'; function preparar() { if (tela) return; tela = document.createElement('canvas'); tela.id = 'rostoMotor'; tela.style.cssText = 'position:fixed;inset:0;width:100%;height:100%;pointer-events:none;z-index:2;display:none'; document.body.append(tela); ctx = tela.getContext('2d'); camada = document.createElement('canvas'); cctx = camada.getContext('2d'); baixa = document.createElement('canvas'); bctx = baixa.getContext('2d', { willReadFrequently: true }); } function medir() { // Até 1,5x a densidade: a tela do S10 é enorme e o brilho custa caro. const d = Math.min(1.5, window.devicePixelRatio || 1); const w = Math.round(innerWidth * d), h = Math.round(innerHeight * d); if (tela.width !== w || tela.height !== h) { tela.width = camada.width = w; tela.height = camada.height = h; } return d; } function caixa(c, x, y, w, h, r) { r = Math.max(0, Math.min(r, w / 2, h / 2)); c.beginPath(); if (c.roundRect) c.roundRect(x, y, w, h, r); else { // WebViews antigos c.moveTo(x + r, y); c.arcTo(x + w, y, x + w, y + h, r); c.arcTo(x + w, y + h, x, y + h, r); c.arcTo(x, y + h, x, y, r); c.arcTo(x, y, x + w, y, r); c.closePath(); } } function coracao(c, cx, cy, s) { c.beginPath(); c.moveTo(cx, cy + s * 0.72); c.bezierCurveTo(cx - s * 1.3, cy - s * 0.3, cx - s * 0.8, cy - s * 1.18, cx, cy - s * 0.3); c.bezierCurveTo(cx + s * 0.8, cy - s * 1.18, cx + s * 1.3, cy - s * 0.3, cx, cy + s * 0.72); c.fill(); } const FONTE_EMOJI = '"Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",sans-serif'; /** Figurinha (viewBox 100x100, só M L C Q Z) ou emoji, centrado no olho. */ function conteudo(c, o, cx, cy, tam) { if (o.desenho && Array.isArray(o.desenho.formas)) { c.save(); c.translate(cx - tam / 2, cy - tam / 2); c.scale(tam / 100, tam / 100); for (const f of o.desenho.formas) { if (typeof f.caminho !== 'string' || !/^[MLCQZmlcqz0-9.,\s+-]+$/.test(f.caminho) || !/^#[0-9a-f]{6}$/i.test(f.cor)) continue; c.fillStyle = f.cor; c.fill(new Path2D(f.caminho)); } c.restore(); } else if (o.icone) { c.save(); c.font = Math.round(tam * 0.8) + 'px ' + FONTE_EMOJI; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(o.icone, cx, cy + tam * 0.04); c.restore(); } } // Olho plano (vivo, bloco, matriz): corpo + pálpebras recortadas (transparentes). // Tonto: uma espiral girando no lugar de cada olho (cada um gira para um lado, como no emoji 😵‍💫). function espiral(c, o, cx, cy, R, lado) { const voltas = 2.75, giro = (o.t || 0) * 7 * lado; c.save(); c.strokeStyle = css(o.cor); c.lineWidth = Math.max(2, R * 0.17); c.lineCap = 'round'; c.beginPath(); for (let th = 0; th <= voltas * Math.PI * 2; th += 0.22) { const rr = R * th / (voltas * Math.PI * 2), a = th * lado + giro; if (th) c.lineTo(cx + Math.cos(a) * rr, cy + Math.sin(a) * rr); else c.moveTo(cx, cy); } c.stroke(); c.restore(); } function olhoPlano(c, o, p, est, e) { if (o.espiral) { espiral(c, o, p.cx, p.cy, Math.min(est.w, est.h) * p.ry * 0.5, p.lado || 1); return; } const respira = 1 + 0.012 * Math.sin(o.t * 1.4); const temConteudo = !!(o.icone || o.desenho); if (temConteudo) e = { top: 0, slant: 0, bot: 0, coracao: 0 }; const fecha = Math.min(1, Math.max(e.top, 1 - Math.min(1, p.ab))); const abre = Math.max(1, p.ab); // surpreso abre além de 1 const amassa = fecha > 0.5 && e.top < 0.5 ? (fecha - 0.5) * 0.2 : 0; // piscada achata e alarga const w = est.w * p.ry * respira * (1 + amassa); const h = est.h * p.ry * respira * abre * (1 - amassa * 0.6); const x = p.cx - w / 2, y = p.cy - h / 2; c.fillStyle = css(o.cor); if (o.dormindo && !temConteudo) { // dormindo: "–" (olho fechado) const esp = Math.max(2, p.ry * 0.2); caixa(c, p.cx - w * 0.5, p.cy + h * 0.08 - esp / 2, w, esp, esp / 2); c.fill(); return; } if (e.coracao > 0.5) { coracao(c, p.cx, p.cy, Math.min(w, h) * 0.55 * respira); return; } caixa(c, x, y, w, h, est.r * p.ry * respira); c.fill(); c.save(); caixa(c, x - 2, y - 2, w + 4, h + 4, est.r * p.ry); c.clip(); c.globalCompositeOperation = 'destination-out'; c.fillStyle = '#000'; const sl = p.lado > 0 ? e.slant : -e.slant; // lado +1 = olho esquerdo; canto interno = lado do nariz const yEsq = y + h * (fecha + sl * 0.5), yDir = y + h * (fecha - sl * 0.5); c.beginPath(); c.moveTo(x - 4, y - 4); c.lineTo(x + w + 4, y - 4); c.lineTo(x + w + 4, yDir); c.lineTo(x - 4, yEsq); c.closePath(); c.fill(); c.beginPath(); c.ellipse(p.cx, y + h * (1.62 - e.bot), w * 0.75, h * 0.62, 0, 0, Math.PI * 2); c.fill(); c.globalCompositeOperation = 'source-over'; if (temConteudo && fecha < 0.6) conteudo(c, o, p.cx, p.cy, Math.min(w, h) * 0.74); else if (o.iris && fecha < 0.6 && e.bot < 0.35) { const R = Math.min(w, h) * 0.3; const ix = Math.max(x + R * 1.1, Math.min(x + w - R * 1.1, p.cx + (p.lx + sacada.x) * w * 0.016)); const iy = Math.max(y + R * 1.1, Math.min(y + h - R * 1.1, p.cy + h * 0.04 + (p.ly + sacada.y) * h * 0.016)); c.fillStyle = css(o.cor.map((v) => v * 0.42)); c.beginPath(); c.arc(ix, iy, R, 0, Math.PI * 2); c.fill(); c.globalCompositeOperation = 'destination-out'; const pupila = p.ab > 1.05 ? 0.38 : e.bot > 0.2 ? 0.62 : 0.5; // susto contrai, alegria dilata c.beginPath(); c.arc(ix, iy, R * pupila, 0, Math.PI * 2); c.fill(); c.globalCompositeOperation = 'source-over'; if (est.brilho) { c.fillStyle = 'rgba(245,250,255,0.9)'; c.beginPath(); c.arc(ix - R * 0.42, iy - R * 0.42, R * 0.2, 0, Math.PI * 2); c.fill(); } } else if (est.brilho && fecha < 0.55 && e.bot < 0.3) { // reflexo que acompanha o olhar const hx = Math.max(x + w * 0.18, Math.min(x + w * 0.62, x + w * 0.3 + (p.lx + sacada.x) * w * 0.012)); const hy = Math.max(y + h * 0.18, Math.min(y + h * 0.5, y + h * 0.26 + (p.ly + sacada.y) * h * 0.012)); c.fillStyle = 'rgba(245,250,255,0.9)'; c.beginPath(); c.ellipse(hx, hy, w * 0.1, h * 0.08, 0, 0, Math.PI * 2); c.fill(); } c.restore(); } // Olho lente: esclera, íris, pupila que dilata e pálpebras de metal. function olhoLente(c, o, p, est, e) { if (o.espiral) { espiral(c, o, p.cx, p.cy, est.w * p.ry * 0.5, p.lado || 1); return; } const R = est.w * p.ry * 0.5 * Math.max(1, p.ab) * (1 + 0.01 * Math.sin(o.t * 1.4)); const fecha = Math.min(1, Math.max(o.icone || o.desenho ? 0 : e.top, 1 - Math.min(1, p.ab))); c.save(); c.beginPath(); c.arc(p.cx, p.cy, R * 1.12, 0, Math.PI * 2); c.fillStyle = '#1b2029'; c.fill(); c.beginPath(); c.arc(p.cx, p.cy, R, 0, Math.PI * 2); c.clip(); c.fillStyle = '#e9eef3'; c.fillRect(p.cx - R, p.cy - R, 2 * R, 2 * R); if (o.icone || o.desenho) { conteudo(c, o, p.cx, p.cy, R * 1.3); e = { top: 0, slant: 0, bot: 0 }; } else { const ix = p.cx + Math.max(-R * 0.4, Math.min(R * 0.4, (p.lx + sacada.x) * R * 0.02)); const iy = p.cy + Math.max(-R * 0.35, Math.min(R * 0.35, (p.ly + sacada.y) * R * 0.02)); const g = c.createRadialGradient(ix, iy, R * 0.05, ix, iy, R * 0.55); g.addColorStop(0, css(o.cor.map((v) => v * 0.5))); g.addColorStop(1, css(o.cor)); c.beginPath(); c.arc(ix, iy, R * 0.55, 0, Math.PI * 2); c.fillStyle = g; c.fill(); const pupila = p.ab > 1.05 ? 0.12 : e.bot > 0.3 ? 0.3 : 0.2; // susto contrai, alegria dilata c.beginPath(); c.arc(ix, iy, R * pupila, 0, Math.PI * 2); c.fillStyle = '#05070a'; c.fill(); c.beginPath(); c.arc(ix - R * 0.2, iy - R * 0.22, R * 0.09, 0, Math.PI * 2); c.fillStyle = 'rgba(255,255,255,0.9)'; c.fill(); } const metal = c.createLinearGradient(0, p.cy - R, 0, p.cy + R); metal.addColorStop(0, '#5b6675'); metal.addColorStop(1, '#2b323d'); c.fillStyle = metal; const sl = p.lado > 0 ? e.slant : -e.slant; const yEsq = p.cy - R + 2 * R * (fecha + sl * 0.5), yDir = p.cy - R + 2 * R * (fecha - sl * 0.5); c.beginPath(); c.moveTo(p.cx - R, p.cy - R); c.lineTo(p.cx + R, p.cy - R); c.lineTo(p.cx + R, yDir); c.lineTo(p.cx - R, yEsq); c.closePath(); c.fill(); c.strokeStyle = '#8994a3'; c.lineWidth = Math.max(1, R * 0.03); c.beginPath(); c.moveTo(p.cx - R, yEsq); c.lineTo(p.cx + R, yDir); c.stroke(); c.beginPath(); c.ellipse(p.cx, p.cy + R * (2.3 - 2 * e.bot), R * 1.3, R * 1.3, 0, 0, Math.PI * 2); c.fill(); c.restore(); } function atualizarPalpebras(dt, olhos, lento) { const k = 1 - Math.exp(-dt * (lento ? 4 : 11)); olhos.forEach((p, i) => { const alvo = PALPEBRAS[p.forma] || PALPEBRAS.aberto, e = estado[i]; e.top += (alvo.top - e.top) * k; e.slant += (alvo.slant - e.slant) * k; e.bot += (alvo.bot - e.bot) * k; e.coracao += ((p.forma === 'coracao' ? 1 : 0) - e.coracao) * Math.min(1, k * 2); }); } function atualizarSacada(t, dt, parado) { // Pequenos saltos do olhar quando nada está acontecendo; olhos de verdade nunca ficam parados. if (parado && t > sacada.proxima) { sacada.alvoX = (Math.random() - 0.5) * 14; sacada.alvoY = (Math.random() - 0.5) * 6; sacada.proxima = t + 0.8 + Math.random() * 2.4; } else if (!parado) { sacada.alvoX = sacada.alvoY = 0; } const k = 1 - Math.exp(-dt * 22); sacada.x += (sacada.alvoX - sacada.x) * k; sacada.y += (sacada.alvoY - sacada.y) * k; } function desenharPontos(estilo, o, olhos, d) { // Matriz: desenha os olhos numa tela pequena e acende um LED por pixel. const linhas = 38, colunas = Math.round(linhas * tela.width / tela.height); if (baixa.width !== colunas || baixa.height !== linhas) { baixa.width = colunas; baixa.height = linhas; } const f = linhas / tela.height * d; bctx.clearRect(0, 0, colunas, linhas); bctx.save(); bctx.scale(f, f); olhos.forEach((p, i) => olhoPlano(bctx, o, p, estilo, estado[i])); for (const traco of o.tracos || []) { if (typeof traco.d !== 'string') continue; const caminho = new Path2D(traco.d); bctx.strokeStyle = bctx.fillStyle = css(o.cor); bctx.lineWidth = traco.largura; bctx.lineCap = 'round'; bctx.lineJoin = 'round'; if (traco.cheio) bctx.fill(caminho); else bctx.stroke(caminho); } desenharMaos(bctx, o, olhos, tela.height / d); desenharZzz(bctx, o, olhos, 1.6); // em LEDs o Z precisa ser maior para ler bctx.restore(); const px = bctx.getImageData(0, 0, colunas, linhas).data; ctx.fillStyle = '#000'; ctx.fillRect(0, 0, tela.width, tela.height); const passo = tela.width / colunas, raio = passo * 0.36; for (let j = 0; j < linhas; j++) for (let i = 0; i < colunas; i++) { const k = (j * colunas + i) * 4, a = px[k + 3] / 255; ctx.beginPath(); ctx.arc((i + 0.5) * passo, (j + 0.5) * passo, raio, 0, Math.PI * 2); ctx.fillStyle = a > 0.25 ? css([px[k], px[k + 1], px[k + 2]], 0.35 + a * 0.65) : 'rgba(255,255,255,0.045)'; ctx.fill(); } } /** * Desenha um quadro. o = { estilo, formato, t, cor:[r,g,b], dormindo, parado, * olhos: [{ cx, cy, ry, ab, forma, lado, lx, ly }, ...] } em pixels CSS. */ function desenhar(o) { preparar(); const base = ESTILOS[o.estilo]; if (!base) return esconder(); const formato = !base.lente && FORMATOS[o.formato]; const estilo = formato ? Object.assign({}, base, formato) : base; tela.style.display = ''; const d = medir(); const dt = antes ? Math.min(0.1, o.t - antes) : 0.033; antes = o.t; atualizarPalpebras(dt, o.olhos, o.dormindo); atualizarSacada(o.t, dt, o.parado); const olhos = o.olhos.map((p) => Object.assign({}, p, { cx: (p.cx + (o.parado ? sacada.x * 0.35 : 0)) * d, cy: (p.cy + (o.parado ? sacada.y * 0.35 : 0)) * d, ry: p.ry * d, })); ctx.clearRect(0, 0, tela.width, tela.height); if (estilo.pontos) { desenharPontos(estilo, o, olhos.map((p) => Object.assign({}, p, { cx: p.cx / d, cy: p.cy / d, ry: p.ry / d })), d); } else if (estilo.lente) { olhos.forEach((p, i) => olhoLente(ctx, o, p, estilo, estado[i])); desenharMaos(ctx, o, olhos, tela.height); desenharZzz(ctx, o, olhos); } else { cctx.clearRect(0, 0, camada.width, camada.height); olhos.forEach((p, i) => olhoPlano(cctx, o, p, estilo, estado[i])); desenharMaos(cctx, o, olhos, tela.height); desenharZzz(cctx, o, olhos); ctx.save(); if (estilo.brilho) { ctx.shadowColor = css(o.cor, 0.7); ctx.shadowBlur = Math.min(60, tela.height * 0.05); } ctx.drawImage(camada, 0, 0); ctx.restore(); } desenharFaixa(ctx, o, tela.width, tela.height); } // ------------------------------------------------------------------ mãos // Mãozinhas na cor dos olhos que sobem da borda da tela. Duração casada com as sequências // de rosto de mesmo nome no carinha.html. const DUR_MAOS = { esconder: 2.6, espiar: 3.2, acenar: 2.4, pensar: 2.8, bocejar: 2.1 }; const maos = { modo: null, t0: null }; const suave = (x) => { x = Math.max(0, Math.min(1, x)); return x * x * (3 - 2 * x); }; const faixaDe = (k, a, b) => suave((k - a) / (b - a)); // 0→1 entre a e b // Mão de desenho animado: palma, quatro dedos (o do meio maior), polegar para o lado do rosto // e um punho de robô. dobra (0–1) fecha os dedos; mexe (0–1) agita os dedos ao acenar. // O contorno é traçado antes do preenchimento: assim só a borda externa aparece. function capsula(c, x0, y0, x1, y1, r) { const a = Math.atan2(y1 - y0, x1 - x0) + Math.PI / 2; c.moveTo(x0 + Math.cos(a) * r, y0 + Math.sin(a) * r); c.arc(x0, y0, r, a, a + Math.PI); c.arc(x1, y1, r, a + Math.PI, a + 2 * Math.PI); c.closePath(); } function formaMao(c, s, dobra, mexe, t, indicador) { c.beginPath(); // punho (manga do robô) caixa(c, -0.42 * s, 0.5 * s, 0.84 * s, 0.38 * s, 0.12 * s); // palma caixa(c, -0.5 * s, -0.36 * s, 1.0 * s, 0.95 * s, 0.34 * s); // dedos: x da base, comprimento e abertura // mindinho, anelar, médio e indicador (o indicador fica do lado do polegar) const dedos = [[-0.355, 0.5, -0.16], [-0.12, 0.68, -0.05], [0.12, 0.74, 0.05], [0.355, 0.66, 0.14]]; dedos.forEach(([x, comp, abre], i) => { const onda = mexe ? Math.sin(t * 14 + i * 0.9) * 0.06 * mexe : 0; const fecha = indicador && i === 3 ? 0 : dobra; // "pensando": só o indicador esticado const ang = abre + onda, l = comp * (1 - 0.7 * fecha) * s; const bx = x * s, by = -0.26 * s; capsula(c, bx, by, bx + Math.sin(ang) * l, by - Math.cos(ang) * l, 0.115 * s); }); // polegar, saindo para o lado capsula(c, 0.38 * s, 0.2 * s, (0.38 + 0.42 * (1 - 0.4 * dobra)) * s, (0.2 - 0.34) * s, 0.13 * s); } function mao(c, o, x, y, s, ang, espelho, dobra, mexe, indicador, pulso, costas) { dobra = dobra || 0; mexe = mexe || 0; s *= 0.82; const escura = o.cor.map((v) => v * 0.34); c.save(); c.translate(x, y); c.rotate(ang); c.scale(espelho, 1); if (pulso) c.translate(0, -0.7 * s); // (x, y) é o punho: gira como uma mão de verdade c.lineJoin = 'round'; c.lineCap = 'round'; formaMao(c, s, dobra, mexe, o.t || 0, indicador); c.strokeStyle = css(o.cor); c.lineWidth = s * 0.17; c.stroke(); // contorno (metade some sob o preenchimento) c.fillStyle = css(escura); c.fill(); // detalhes por dentro, finos: linha da palma, vincos dos dedos e a costura do punho c.strokeStyle = css(o.cor, 0.55); c.lineWidth = s * 0.045; if (costas) { // dorso da mão: nós dos dedos em vez da linha da palma for (const nx of [-0.355, -0.12, 0.12, 0.355]) { c.beginPath(); c.arc(nx * s, -0.2 * s, 0.075 * s, Math.PI * 1.15, Math.PI * 1.85); c.stroke(); } } else { c.beginPath(); c.moveTo(-0.3 * s, 0.12 * s); c.quadraticCurveTo(0.02 * s, 0.26 * s, 0.3 * s, 0.06 * s); c.stroke(); } c.beginPath(); c.moveTo(-0.36 * s, 0.6 * s); c.lineTo(0.36 * s, 0.6 * s); c.stroke(); c.restore(); } // Sobe com um leve "passa e volta" no fim (como um braço que freia). const voltinha = (x) => { x = Math.max(0, Math.min(1, x)); const k = 1.6; return 1 + (k + 1) * Math.pow(x - 1, 3) + k * Math.pow(x - 1, 2); }; function desenharMaos(c, o, olhos, H) { if (!maos.modo) return; if (maos.t0 === null) maos.t0 = o.t; const k = (o.t - maos.t0) / DUR_MAOS[maos.modo]; if (k >= 1) { maos.modo = null; return; } const [e, d] = olhos.slice().sort((a, b) => a.cx - b.cx); // olho da esquerda e da direita na tela const s = e.ry * 0.95, fora = H + s * 1.8; if (maos.modo === 'esconder' || maos.modo === 'espiar') { // Cada mão vem do canto de baixo, por fora, num arco, girando até ficar inclinada para dentro. // A da direita chega um pouco depois: duas mãos nunca se mexem igualzinho. const cobrir = (olho, lado, atraso) => { const ida = voltinha((k - atraso) / 0.24), volta = faixaDe(k, 0.8 + atraso * 0.5, 1); const v = Math.max(0, Math.min(1.08, ida)) * (1 - volta); // 0 = fora da tela, 1 = cobrindo const alvoX = olho.cx - lado * olho.ry * 0.05, alvoY = olho.cy + olho.ry * 0.38; const x = alvoX - lado * olho.ry * 1.6 * (1 - Math.min(1, v)); const y = fora + (alvoY - fora) * v; const respira = Math.sin(o.t * 3 + lado) * s * 0.03 * Math.min(1, v); const ang = lado * (0.42 + 0.55 * (1 - Math.min(1, v))); // gira enquanto sobe, termina a ~24° return { x, y: y + respira, ang }; }; const pe = cobrir(e, 1, 0), pd = cobrir(d, -1, 0.05); if (maos.modo === 'espiar') { // a mão da esquerda gira pelo punho para fora, como uma porta, e o olho aparece const abre = faixaDe(k, 0.3, 0.44) - faixaDe(k, 0.64, 0.76); pe.ang -= abre * 1.05; pe.x -= abre * e.ry * 0.35; pe.y += abre * e.ry * 0.2; } const treme = Math.sin(o.t * 22) * s * 0.012; // tímido: tremidinha // dorso virado para quem olha: polegar para fora, perto da bochecha mao(c, o, pe.x + treme, pe.y, s * 1.1, pe.ang, -1, 0, 0, false, true, true); mao(c, o, pd.x - treme, pd.y, s * 1.1, pd.ang, 1, 0, 0, false, true, true); } else if (maos.modo === 'acenar') { const ida = voltinha(k / 0.22), volta = faixaDe(k, 0.82, 1); const v = Math.max(0, Math.min(1.08, ida)) * (1 - volta); const x = d.cx + d.ry * 2.2 + d.ry * 0.6 * (1 - Math.min(1, v)), y0 = d.cy + d.ry * 1.55; const y = fora + (y0 - fora) * v; const balanco = k > 0.22 && k < 0.82 ? Math.sin((k - 0.22) * Math.PI * 7) * 0.32 : 0; mao(c, o, x, y, s * 0.95, -0.18 + balanco, -1, 0, 1, false, true); } else if (maos.modo === 'bocejar') { // mão na frente da boca, com tapinhas, e os Zzz subindo do olho const ida = voltinha(k / 0.25), volta = faixaDe(k, 0.78, 1); const v = Math.max(0, Math.min(1.08, ida)) * (1 - volta); const bx = (e.cx + d.cx) / 2 + e.ry * 0.12, by = e.cy + e.ry * 1.2; const tapa = k > 0.3 && k < 0.75 ? Math.abs(Math.sin((k - 0.3) * Math.PI * 6)) * s * 0.12 : 0; mao(c, o, bx + e.ry * 0.5 * (1 - Math.min(1, v)), fora + (by + s * 0.55 - fora) * v + tapa, s * 0.9, -0.22, -1, 0.15, 0, false, true, true); c.save(); c.globalAlpha *= Math.min(1, v); desenharZzz(c, Object.assign({}, o, { dormindo: true }), olhos, 0.8); c.restore(); } else if (maos.modo === 'pensar') { const ida = voltinha(k / 0.22), volta = faixaDe(k, 0.82, 1); const v = Math.max(0, Math.min(1.08, ida)) * (1 - volta); const x = (e.cx + d.cx) / 2 + e.ry * 1.05, y0 = e.cy + e.ry * 1.4; const y = fora + (y0 - fora) * v + Math.sin(o.t * 9) * s * 0.03; mao(c, o, x, y, s * 0.85, -0.5, -1, 0.95, 0, true); } } // ------------------------------------------------------------------ Zzz function desenharZzz(c, o, olhos, fator) { fator = fator || 1; if (!o.dormindo) return; const d = olhos.slice().sort((a, b) => a.cx - b.cx)[1]; // olho da direita na tela const x0 = d.cx + d.ry * 0.7, y0 = d.cy - d.ry * 0.55; c.save(); c.strokeStyle = css(o.cor); c.lineCap = 'round'; c.lineJoin = 'round'; for (let i = 0; i < 3; i++) { const k = ((o.t || 0) * 0.32 + i / 3) % 1; // cada Z num ponto do ciclo const tam = d.ry * (0.28 + 0.3 * k) * fator; const x = x0 + d.ry * 0.7 * k + Math.sin(k * Math.PI * 2) * d.ry * 0.08, y = y0 - d.ry * 1.7 * k; c.globalAlpha = Math.min(1, k / 0.15) * (1 - k); c.lineWidth = Math.max(1.5, tam * 0.2); c.beginPath(); c.moveTo(x - tam / 2, y - tam / 2); c.lineTo(x + tam / 2, y - tam / 2); c.lineTo(x - tam / 2, y + tam / 2); c.lineTo(x + tam / 2, y + tam / 2); c.stroke(); } c.restore(); } // --------------------------------------------------------- faixa de baixo // A parte de baixo da tela vira palco: legenda da conversa, pensando, equalizador e carga. const faixa = { modo: null, texto: '', t0: null, dur: 0 }; function definirFaixa(modo, opcoes) { opcoes = opcoes || {}; if (!modo) { if (!opcoes.so || faixa.modo === opcoes.so) faixa.modo = null; // so: só apaga esse modo return; } faixa.modo = modo; faixa.t0 = performance.now() / 1000; faixa.texto = String(opcoes.texto || '').replace(/\s+/g, ' ').trim().slice(0, 400); faixa.dur = opcoes.segundos || (modo === 'legenda' ? Math.max(3, Math.min(14, faixa.texto.length / 14 + 2.5)) : modo === 'digitando' ? 20 : 2.6); } function desenharFaixa(c, o, W, H) { const y = H * 0.955, fonte = Math.round(H * 0.042); c.save(); c.shadowColor = css(o.cor, 0.6); c.shadowBlur = H * 0.02; if (o.carga && o.carga.carregando && typeof o.carga.nivel === 'number') { // carregando: canto direito const n = Math.max(0, Math.min(100, o.carga.nivel)), bw = H * 0.075, bh = H * 0.036, bx = W - H * 0.13, by = y - bh / 2; c.globalAlpha = 0.55 + 0.25 * Math.sin(o.t * 2.5); c.strokeStyle = css(o.cor); c.lineWidth = Math.max(1, H * 0.005); caixa(c, bx, by, bw, bh, bh * 0.25); c.stroke(); c.fillStyle = css(o.cor); c.fillRect(bx + bw, y - bh * 0.2, bh * 0.15, bh * 0.4); c.fillRect(bx + bh * 0.12, by + bh * 0.15, (bw - bh * 0.24) * n / 100, bh * 0.7); c.font = '600 ' + Math.round(fonte * 0.8) + 'px system-ui,sans-serif'; c.textAlign = 'right'; c.textBaseline = 'middle'; const tx = bx - H * 0.012; c.fillText(Math.round(n) + '%', tx, y); const rx = tx - c.measureText(Math.round(n) + '%').width - H * 0.022, r = H * 0.016; // raio desenhado na cor dos olhos c.beginPath(); c.moveTo(rx + r * 0.3, y - r); c.lineTo(rx - r * 0.5, y + r * 0.15); c.lineTo(rx, y + r * 0.15); c.lineTo(rx - r * 0.3, y + r); c.lineTo(rx + r * 0.5, y - r * 0.15); c.lineTo(rx, y - r * 0.15); c.closePath(); c.fill(); c.globalAlpha = 1; } if (faixa.modo) { if (faixa.t0 === null) faixa.t0 = o.t; const k = (o.t - faixa.t0) / faixa.dur; if (k >= 1) faixa.modo = null; else { const some = Math.min(1, (faixa.dur - (o.t - faixa.t0)) / 0.4); c.globalAlpha = Math.max(0, some); c.fillStyle = css(o.cor); if (faixa.modo === 'legenda') { // efeito de digitação; se não couber, mostra o final do que já foi "dito" let txt = faixa.texto.slice(0, Math.floor((o.t - faixa.t0) * 30)); c.font = '500 ' + fonte + 'px system-ui,sans-serif'; c.textAlign = 'center'; c.textBaseline = 'middle'; const max = W * 0.8; while (txt.length > 1 && c.measureText(txt).width > max) txt = txt.slice(1); c.fillText(txt, W / 2, y); } else if (faixa.modo === 'digitando') { for (let i = 0; i < 3; i++) { const pulo = Math.max(0, Math.sin(o.t * 7 - i * 0.7)) * H * 0.012; c.beginPath(); c.arc(W / 2 + (i - 1) * H * 0.035, y - pulo, H * 0.011, 0, Math.PI * 2); c.fill(); } } else if (faixa.modo === 'equalizador') { const n = 15, larg = H * 0.016, passo = H * 0.026; for (let i = 0; i < n; i++) { const a = 0.25 + 0.75 * Math.abs(Math.sin(o.t * (5 + i % 4) + i * 1.3)) * Math.sin(Math.PI * (i + 0.5) / n); const h = H * 0.04 * a; caixa(c, W / 2 + (i - (n - 1) / 2) * passo - larg / 2, H * 0.988 - h, larg, h, larg / 2); c.fill(); } } } } c.restore(); } /** * Retrato parado num canvas qualquer (miniaturas da biblioteca no painel). * o = { estilo, formato, cor, forma, icone, desenho }. */ function retrato(alvo, o) { const base = ESTILOS[o.estilo] || ESTILOS.vivo; const formato = !base.lente && FORMATOS[o.formato]; const est = formato ? Object.assign({}, base, formato) : base; const W = alvo.width, H = alvo.height, c = alvo.getContext('2d'); const pal = Object.assign({ coracao: o.forma === 'coracao' ? 1 : 0 }, PALPEBRAS[o.forma] || PALPEBRAS.aberto); const op = { t: 0, cor: o.cor || [92, 214, 255], icone: o.icone || null, desenho: o.desenho || null }; const ry = H * 0.36, sep = Math.min(W * 0.23, H * 0.62); const olhos = [-1, 1].map((s) => ({ cx: W / 2 + s * sep, cy: H / 2, ry, ab: o.forma === 'surpreso' ? 1.12 : 1, forma: o.forma, lado: -s, lx: 0, ly: 0 })); c.clearRect(0, 0, W, H); if (est.lente) { olhos.forEach((p) => olhoLente(c, op, p, est, pal)); return; } const tmp = document.createElement('canvas'); tmp.width = W; tmp.height = H; const t = tmp.getContext('2d'); olhos.forEach((p) => olhoPlano(t, op, p, est, Object.assign({}, pal))); if (est.pontos) { const passo = Math.max(3, Math.round(H / 16)), d = t.getImageData(0, 0, W, H).data; for (let y = passo / 2; y < H; y += passo) for (let x = passo / 2; x < W; x += passo) { const k = (Math.floor(y) * W + Math.floor(x)) * 4, a = d[k + 3] / 255; c.beginPath(); c.arc(x, y, passo * 0.38, 0, Math.PI * 2); c.fillStyle = a > 0.25 ? css([d[k], d[k + 1], d[k + 2]], 0.4 + a * 0.6) : 'rgba(255,255,255,0.05)'; c.fill(); } return; } c.save(); if (est.brilho) { c.shadowColor = css(op.cor, 0.7); c.shadowBlur = H * 0.08; } c.drawImage(tmp, 0, 0); c.restore(); } // ------------------------------------------------------------ atividades // "Momentos dele": um bonequinho do Carinha (silhueta na cor dos olhos) faz algo na parte de // baixo da tela enquanto os olhos sobem, encolhem e acompanham. Camada própria, atrás dos olhos, // 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] }, 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] }, receita: { nome: 'Criada por IA', emoji: '✨', dur: [20, 40] }, // receita do estúdio de criação }; const ativ = { nome: null, t0: 0, dur: 0, fim: 0, receita: null }; let palcoTela = null, pctx = null; const agoraS = () => performance.now() / 1000; function iniciarAtividade(nome, segundos, receita) { if (!ATIVIDADES[nome] || (nome === 'receita' && !receita)) { pararAtividade(); return false; } const [a, b] = ATIVIDADES[nome].dur; ativ.nome = nome; ativ.t0 = agoraS(); ativ.fim = 0; ativ.receita = receita || null; ativ.dur = segundos || a + Math.random() * (b - a); return true; } function pararAtividade() { if (ativ.nome && !ativ.fim) ativ.fim = agoraS(); } // 0..1: entra e sai em 0,6 s; termina sozinha depois da duração function visibilidade(t) { if (!ativ.nome) return 0; if (!ativ.fim && t - ativ.t0 >= ativ.dur) ativ.fim = t; const entra = Math.min(1, Math.max(0, (t - ativ.t0) / 0.6)); if (!ativ.fim) return entra; const sai = 1 - (t - ativ.fim) / 0.6; if (sai <= 0) { ativ.nome = null; return 0; } return Math.min(entra, sai); } // movimentos de cada cena, em função do tempo k (s) desde o início const bola = (k) => { const p = (k % 1.1) / 1.1; return { p, h: 4 * p * (1 - p), x: 0.35 * Math.sin(k * 0.7) }; }; const pincel = (k) => ({ x: Math.sin(k * 3.1) * 0.6 + Math.sin(k * 1.3) * 0.4, y: Math.cos(k * 2.3) * 0.7 }); const forca = (k) => (1 - Math.cos(k * 2.2)) / 2; // 0 = halteres no ombro, 1 = braços em cima /** Para onde os olhos grandes olham (unidades do rosto: dx ±24, dy ±12) e que cara fazem. */ function olharAtividade(t) { const v = visibilidade(t); if (!v) return null; const k = t - ativ.t0, info = { nome: ativ.nome, v, x: 0, y: 0, saindo: !!ativ.fim }; const lado = innerWidth >= innerHeight * 1.2 ? 1 : 0; // deitado: a cena fica à direita if (ativ.nome === 'ler') { // lê linha a linha: varre da esquerda para a direita const linha = (k % 2.4) / 2.4; info.x = (linha < 0.9 ? -8 + 16 * (linha / 0.9) : 8 - 16 * ((linha - 0.9) / 0.1)) + lado * 13; info.y = 7 + (Math.floor(k / 2.4) % 3) * 1.5; } else if (ativ.nome === 'cantar') { info.x = 6 * Math.sin(k * 1.3) + lado * 12; info.y = -4 + 4 * Math.sin(k * 4); info.forma = 'feliz'; info.boca = Math.sin(k * 7) > -0.2 ? 'aberta' : 'sorriso'; } else if (ativ.nome === 'bola') { // segue a bola const b = bola(k); info.x = b.x * 16 + lado * 14; info.y = 9 - b.h * 18; } else if (ativ.nome === 'videogame') { // vidrado na TV; de vez em quando comemora info.x = 18 + lado * 5 + Math.sin(k * 9) * 1.5; info.y = 1; if (k % 9 > 7.6) { info.forma = 'feliz'; info.boca = 'sorriso'; } } else if (ativ.nome === 'pintar') { 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 === 'receita') { const R = ativ.receita || {}, w = Math.PI * 2 * (R.ritmo || 0.8); const o = (R.objetos || [])[0]; if (R.olhar === 'cima') { info.x = lado * 12; info.y = -10; } else if (R.olhar === 'baixo') { info.x = lado * 10; info.y = 8; } else if (R.olhar === 'lado') { info.x = lado * 20; info.y = 2; } else if (R.olhar === 'frente') { info.x = 4 * Math.sin(k * 0.7); info.y = 0; } else { // segue o primeiro objeto info.x = lado * 14 + Math.sin(k * w) * 5; info.y = o && o.movimento === 'pular' ? 8 - Math.abs(Math.sin(k * w * 0.5)) * 16 : o && o.movimento === 'subir' ? -6 : 3; } if (R.forma && R.forma !== 'aberto') info.forma = R.forma; if (R.boca && R.boca !== 'neutra') info.boca = R.boca; } 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; } // --- o bonequinho: cabeça-tela com olhinhos, antena, tronco, braços e pernas articulados. // Ângulos em radianos: 0 = apontando para baixo; positivo gira para a direita da tela. function membro(c, x0, y0, a1, a2, l1, l2, w) { const x1 = x0 + Math.sin(a1) * l1, y1 = y0 + Math.cos(a1) * l1; const x2 = x1 + Math.sin(a1 + a2) * l2, y2 = y1 + Math.cos(a1 + a2) * l2; c.beginPath(); capsula(c, x0, y0, x1, y1, w / 2); c.fill(); c.stroke(); c.beginPath(); capsula(c, x1, y1, x2, y2, w / 2); c.fill(); c.stroke(); return { x: x2, y: y2 }; } function boneco(c, x, y, s, p, cor, k) { c.fillStyle = css(cor.map((v) => v * 0.22), 0.96); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.07); c.lineJoin = 'round'; c.lineCap = 'round'; const w = s * 0.27, braco = [s * 0.62, s * 0.58]; const perna = p.sentado ? [s * 0.38, s * 0.62] : [s * 0.72, s * 0.7]; const hx = x + (p.dx || 0) * s, hy = p.sentado ? y - perna[1] - s * 0.12 : y - (perna[0] + perna[1]) * 0.97; const pE = p.pE || [-0.08, 0], pD = p.pD || [0.08, 0]; membro(c, hx - s * 0.25, hy, pE[0], pE[1], perna[0], perna[1], w * 1.1); membro(c, hx + s * 0.25, hy, pD[0], pD[1], perna[0], perna[1], w * 1.1); c.save(); c.translate(hx, hy); c.rotate(-(p.inclina || 0)); // tronco caixa(c, -s * 0.5, -s * 1.12, s, s * 1.12, s * 0.25); c.fill(); c.stroke(); c.save(); c.fillStyle = css(cor, 0.85); // luzinha no peito c.beginPath(); c.arc(0, -s * 0.62, s * 0.09 * (0.8 + 0.2 * Math.sin(k * 3)), 0, Math.PI * 2); c.fill(); c.restore(); // cabeça (uma telinha, como o celular) com olhinhos e antena c.save(); c.translate(0, -s * 1.25); c.rotate(-(p.cabeca || 0)); c.beginPath(); c.moveTo(0, -s * 0.95); c.lineTo(0, -s * 1.25); c.stroke(); c.save(); c.fillStyle = css(cor); c.beginPath(); c.arc(0, -s * 1.3, s * 0.1, 0, Math.PI * 2); c.fill(); c.restore(); 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.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(); // braços (no mesmo quadro do tronco inclinado); as mãos voltam para segurar os objetos const bE = p.bE || [-0.25, 0.15], bD = p.bD || [0.25, -0.15]; const mE = membro(c, -s * 0.58, -s * 0.98, bE[0], bE[1], braco[0], braco[1], w); const mD = membro(c, s * 0.58, -s * 0.98, bD[0], bD[1], braco[0], braco[1], w); c.restore(); const rot = (pt) => { // do quadro do tronco para a tela const a = -(p.inclina || 0), cs = Math.cos(a), sn = Math.sin(a); return { x: hx + pt.x * cs - pt.y * sn, y: hy + pt.x * sn + pt.y * cs }; }; return { mE: rot(mE), mD: rot(mD), quadril: { x: hx, y: hy }, cabeca: rot({ x: 0, y: -s * 1.75 }) }; } const claro = (cor, a) => css(cor.map((v) => v + (255 - v) * 0.55), a); const escuro = (cor) => css(cor.map((v) => v * 0.25)); function nota(c, x, y, s) { // ♪ c.beginPath(); c.ellipse(x, y, s * 0.2, s * 0.15, -0.4, 0, Math.PI * 2); c.fill(); c.fillRect(x + s * 0.14, y - s * 0.7, s * 0.06, s * 0.7); 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; } // ---- objetos que as receitas podem usar (mesmos desenhos das cenas feitas à mão) function desenharObjeto(c, tipo, ox, oy, s, k, cor, giro, j) { c.save(); c.translate(ox, oy); if (giro) c.rotate(giro); c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05); if (tipo === 'bola') { const r = s * 0.28; c.beginPath(); c.arc(0, 0, r, 0, Math.PI * 2); c.fill(); c.stroke(); c.fillStyle = css(cor.map((v) => v * 0.3)); c.beginPath(); for (let i = 0; i < 5; i++) { const a = i * 1.2566; c.lineTo(Math.cos(a) * r * 0.38, Math.sin(a) * r * 0.38); } c.closePath(); c.fill(); } else if (tipo === 'livro') { const lw = s * 0.62, lh = s * 0.5; for (const lado of [-1, 1]) { c.beginPath(); c.moveTo(0, lh * 0.55); c.lineTo(lado * lw, lh * 0.45); c.lineTo(lado * lw, -lh * 0.45); c.lineTo(0, -lh * 0.35); c.closePath(); c.fill(); c.stroke(); } } else if (tipo === 'xicara') { xicara(c, 0, 0, s, cor); vapor(c, 0, -s * 0.3, s, k, cor); } else if (tipo === 'nota') { c.fillStyle = css(cor); nota(c, 0, 0, s * 0.7); } else if (tipo === 'estrela') { c.fillStyle = css(j % 2 ? AMARELO_ : cor); estrela(c, 0, 0, s * 0.16); } else if (tipo === 'coracao') { c.fillStyle = css(ROSA_); coracao(c, 0, 0, s * 0.22); } else if (tipo === 'zzz') { zzz(c, 0, 0, s, k + j, cor); } else if (tipo === 'chama') { chama(c, 0, s * 0.15, s * (0.35 + 0.08 * Math.sin(k * 17 + j)), s); } else if (tipo === 'bolha') { const r = s * (0.13 + 0.1 * rnd(j, 4)); c.fillStyle = css(cor, 0.1); c.beginPath(); c.arc(0, 0, r, 0, Math.PI * 2); c.fill(); c.stroke(); c.strokeStyle = claro(cor, 1); c.beginPath(); c.arc(0, 0, r * 0.65, -2.4, -1.6); c.stroke(); } else if (tipo === 'halter') { c.fillRect(-s * 0.25, -s * 0.035, s * 0.5, s * 0.07); for (const lado of [-1, 1]) { caixa(c, lado * s * 0.28 - s * 0.07, -s * 0.17, s * 0.14, s * 0.34, s * 0.04); c.fill(); c.stroke(); } } else if (tipo === 'microfone') { c.fillStyle = escuro(cor); c.beginPath(); capsula(c, 0, 0, -s * 0.1, -s * 0.4, s * 0.07); c.fill(); c.stroke(); c.fillStyle = claro(cor, 1); c.beginPath(); c.arc(-s * 0.12, -s * 0.5, s * 0.14, 0, Math.PI * 2); c.fill(); c.stroke(); } else if (tipo === 'controle') { caixa(c, -s * 0.42, -s * 0.16, s * 0.84, s * 0.34, s * 0.16); c.fill(); c.stroke(); c.fillStyle = css(cor.map((v) => v * 0.3)); c.fillRect(-s * 0.3, -s * 0.02, s * 0.16, s * 0.05); c.fillRect(-s * 0.245, -s * 0.08, s * 0.05, s * 0.17); } else if (tipo === 'pincel') { c.beginPath(); c.moveTo(-s * 0.1, s * 0.08); c.lineTo(s * 0.4, -s * 0.12); c.stroke(); c.fillStyle = css([ROSA_, AMARELO_, VERDE_][Math.floor(k) % 3]); c.beginPath(); c.arc(s * 0.43, -s * 0.13, s * 0.07, 0, Math.PI * 2); c.fill(); } else if (tipo === 'flor') { c.strokeStyle = css(VERDE_); c.beginPath(); c.moveTo(0, s * 0.5); c.lineTo(0, 0); c.stroke(); c.fillStyle = css(ROSA_); for (let i = 0; i < 5; i++) { const a = i * 1.2566 + k * 0.3; c.beginPath(); c.arc(Math.cos(a) * s * 0.14, Math.sin(a) * s * 0.14, s * 0.1, 0, Math.PI * 2); c.fill(); } c.fillStyle = css(AMARELO_); c.beginPath(); c.arc(0, 0, s * 0.08, 0, Math.PI * 2); c.fill(); } c.restore(); } /** Onde fica cada objeto da receita e como ele se mexe (pular, girar, subir, orbitar, piscar, balançar). */ function objetoReceita(c, o, i, b, x, y, s, k, cor, w) { const q = Math.max(1, Math.min(5, o.quantidade || 1)), alfa = c.globalAlpha; for (let j = 0; j < q; j++) { let ox, oy; if (o.onde === 'mao_direita') { ox = b.mD.x; oy = b.mD.y; } else if (o.onde === 'mao_esquerda') { ox = b.mE.x; oy = b.mE.y; } else if (o.onde === 'maos') { ox = (b.mE.x + b.mD.x) / 2; oy = (b.mE.y + b.mD.y) / 2; } else if (o.onde === 'chao') { ox = x + s * (0.9 + i * 0.5); oy = y - s * 0.3; } else if (o.onde === 'acima') { ox = b.cabeca.x + s * (0.8 + j * 0.5); oy = b.cabeca.y - s * 0.6; } else { const a = (j / q) * Math.PI * 2 + i; ox = b.quadril.x + Math.cos(a) * s * 1.5; oy = b.quadril.y - s * 1.2 + Math.sin(a) * s * 1.1; } if (q > 1 && (o.onde === 'mao_direita' || o.onde === 'mao_esquerda' || o.onde === 'maos' || o.onde === 'chao')) ox += (j - (q - 1) / 2) * s * 0.4; let giro = 0; c.globalAlpha = alfa; if (o.movimento === 'pular') oy -= Math.abs(Math.sin(k * w * 0.5 + j)) * s * 1.6; else if (o.movimento === 'girar') giro = k * 4 + j; else if (o.movimento === 'balancar') ox += Math.sin(k * w + j) * s * 0.3; else if (o.movimento === 'piscar') c.globalAlpha = alfa * (0.25 + 0.75 * Math.abs(Math.sin(k * 3 + j * 1.7))); else if (o.movimento === 'subir') { const f = (k * 0.35 + j / q) % 1; ox += f * s * 0.8 + Math.sin(k * 2 + j) * s * 0.12; oy -= f * s * 1.8; c.globalAlpha = alfa * Math.min(1, f / 0.12) * (1 - f); } else if (o.movimento === 'orbitar') { const a = k * 1.6 + (j / q) * Math.PI * 2; ox = b.quadril.x + Math.cos(a) * s * 1.6; oy = b.quadril.y - s * 1.3 + Math.sin(a) * s * 0.9; } desenharObjeto(c, o.tipo, ox, oy, s, k, cor, giro, j); } c.globalAlpha = alfa; } const CENAS = { // Atividade criada por IA: uma receita (criacao.py) montada com as peças que já existem aqui: // o bonequinho, poses de braços/pernas e os objetos das outras cenas. receita(c, x, y, s, k, cor) { const R = ativ.receita || {}; const w = Math.PI * 2 * (R.ritmo || 0.8), onda = Math.sin(k * w); const f1 = (onda + 1) / 2, f2 = R.alternados ? 1 - f1 : f1; const rad = (g) => (g * Math.PI) / 180; const ang = (b, f) => rad((b ? b.de : 20) + ((b ? b.ate : 40) - (b ? b.de : 20)) * f); const pulo = (R.pulo || 0) * Math.max(0, onda), sobe = pulo * s * 0.5; const balanco = Math.sin(k * w * 0.5) * (R.balanco || 0); const cot = (R.cotovelos || 0) * 1.4; let pE = [-0.08, 0], pD = [0.08, 0]; if (R.pernas === 'marchando') { pE = [-0.08 - 0.25 * f1, 0.45 * f1]; pD = [0.08 + 0.25 * (1 - f1), -0.45 * (1 - f1)]; } else if (R.pernas === 'chutando') { const chute = Math.pow(Math.max(0, onda), 2); pD = [0.08 + chute * 0.9, -chute * 0.6]; } else if (R.pernas === 'dancando') { pE = [-0.1 - f1 * 0.25, f1 * 0.4]; pD = [0.1 + (1 - f1) * 0.25, -(1 - f1) * 0.4]; } else if (R.pernas === 'pulando') { pE = [-0.06, pulo * 0.3]; pD = [0.06, -pulo * 0.3]; } const sentado = R.postura === 'sentado'; const b = boneco(c, x + balanco * s * 0.3, y - sobe, s, Object.assign(sentado ? Object.assign({}, SENTADO) : { pE, pD }, { inclina: balanco * 0.25, cabeca: -balanco * 0.3, fechados: R.forma === 'dormir', olhoY: R.olhar === 'cima' ? -1 : R.olhar === 'baixo' ? 0.8 : 0, olhoX: R.olhar === 'lado' ? 1 : 0, bE: [-ang(R.braco_esquerdo, f1), cot], bD: [ang(R.braco_direito, f2), -cot], }), cor, k); (R.objetos || []).forEach((o, i) => objetoReceita(c, o, i, b, x, y, s, k, cor, w)); }, ler(c, x, y, s, k, cor) { const b = boneco(c, x, y, s, { sentado: true, pE: [-1.35, 1.35], pD: [1.35, -1.35], cabeca: -0.08, olhoY: 0.8, olhoX: -0.8 + 1.6 * ((k % 2.4) / 2.4), bE: [0.2, 2.4], bD: [-0.2, -2.4], }, cor, k); // livro aberto entre as mãos, com uma página virando de vez em quando const cx = (b.mE.x + b.mD.x) / 2, cy = (b.mE.y + b.mD.y) / 2 + s * 0.14, lw = s * 0.72, lh = s * 0.55; c.fillStyle = claro(cor, 0.95); c.strokeStyle = css(cor); for (const lado of [-1, 1]) { c.beginPath(); c.moveTo(cx, cy + lh * 0.1); c.lineTo(cx + lado * lw, cy); c.lineTo(cx + lado * lw, cy - lh); c.lineTo(cx, cy - lh * 0.9); c.closePath(); c.fill(); c.stroke(); } c.strokeStyle = css(cor, 0.5); c.lineWidth = Math.max(1, s * 0.03); for (let i = 1; i <= 3; i++) for (const lado of [-1, 1]) { const yy = cy - lh * (0.25 * i); c.beginPath(); c.moveTo(cx + lado * lw * 0.15, yy); c.lineTo(cx + lado * lw * 0.85, yy - lh * 0.03); c.stroke(); } const virar = (k % 6) / 0.7; // vira em 0,7 s a cada 6 s if (virar < 1) { const px = cx + lw * Math.cos(virar * Math.PI), sobe = Math.sin(virar * Math.PI) * lh * 0.25; c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05); c.beginPath(); c.moveTo(cx, cy + lh * 0.1); c.lineTo(px, cy - sobe); c.lineTo(px, cy - lh - sobe); c.lineTo(cx, cy - lh * 0.9); c.closePath(); c.fill(); c.stroke(); } }, cantar(c, x, y, s, k, cor) { const balanco = Math.sin(k * 2.2); const b = boneco(c, x, y, s, { dx: balanco * 0.12, inclina: balanco * 0.08, cabeca: balanco * 0.1, olhoY: -0.3, pE: [-0.1 - Math.max(0, balanco) * 0.15, 0], pD: [0.1 - Math.min(0, balanco) * 0.15, 0], bD: [2.45, 0.9], bE: [-0.9 - 0.4 * Math.sin(k * 4.4), -0.4], }, cor, k); // microfone na mão direita, perto da boca c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06); c.beginPath(); capsula(c, b.mD.x, b.mD.y, b.mD.x - s * 0.1, b.mD.y - s * 0.4, s * 0.07); c.fill(); c.stroke(); c.fillStyle = claro(cor, 1); c.beginPath(); c.arc(b.mD.x - s * 0.12, b.mD.y - s * 0.5, s * 0.14, 0, Math.PI * 2); c.fill(); c.stroke(); // notas subindo 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, b.cabeca.x + s * (0.9 + 0.5 * i) + Math.sin(f * 6 + i) * s * 0.2, b.cabeca.y + s * 0.4 - f * s * 1.6, s * 0.75); } c.globalAlpha = alfa; }, bola(c, x, y, s, k, cor) { const bl = bola(k), chute = Math.max(0, 1 - bl.p / 0.18) + Math.max(0, (bl.p - 0.9) / 0.1); const b = boneco(c, x, y, s, { dx: bl.x * 0.5, olhoX: bl.x * 2, olhoY: 0.6 - bl.h * 1.4, pD: [0.08 + chute * 0.9, -chute * 0.6], pE: [-0.08, 0], bE: [-0.6, 0.3], bD: [0.6, -0.3], }, cor, k); const bx = b.quadril.x + s * 0.55 + bl.x * s * 0.3, by = y - s * 0.55 - bl.h * s * 2.6, r = s * 0.3; const alfa = c.globalAlpha; c.save(); c.globalAlpha = alfa * 0.35 * (1 - bl.h); c.fillStyle = css(cor); // sombra no chão c.beginPath(); c.ellipse(bx, y, r * (1.1 - bl.h * 0.5), r * 0.18, 0, 0, Math.PI * 2); c.fill(); c.restore(); c.save(); c.translate(bx, by); c.rotate(k * 5); c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05); c.beginPath(); c.arc(0, 0, r, 0, Math.PI * 2); c.fill(); c.stroke(); c.fillStyle = css(cor.map((v) => v * 0.3)); // gomo da bola c.beginPath(); for (let i = 0; i < 5; i++) { const a = i * 1.2566; c.lineTo(Math.cos(a) * r * 0.38, Math.sin(a) * r * 0.38); } c.closePath(); c.fill(); c.restore(); }, videogame(c, x, y, s, k, cor) { const aperta = Math.sin(k * 23) * 0.05; const b = boneco(c, x - s * 1.3, y, s, { sentado: true, pE: [-1.35, 1.35], pD: [1.35, -1.35], olhoX: 1, olhoY: 0.1, cabeca: -0.05, bE: [0.15 + aperta, 0.95], bD: [-0.15 - aperta, -0.95], }, cor, k); // controle const cx = (b.mE.x + b.mD.x) / 2, cy = (b.mE.y + b.mD.y) / 2; c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05); caixa(c, cx - s * 0.42, cy - s * 0.16, s * 0.84, s * 0.34, s * 0.16); c.fill(); c.stroke(); c.fillStyle = css(cor.map((v) => v * 0.3)); c.fillRect(cx - s * 0.3, cy - s * 0.02, s * 0.16, s * 0.05); c.fillRect(cx - s * 0.245, cy - s * 0.08, s * 0.05, s * 0.17); for (const [dx, dy] of [[0.22, -0.05], [0.3, 0.04]]) { c.beginPath(); c.arc(cx + dx * s, cy + dy * s, s * 0.04, 0, Math.PI * 2); c.fill(); } // TV com um joguinho: bonequinho pulando obstáculos const tx = x + s * 1.15, tw = s * 2.1, th = s * 1.45, ty = y - s * 0.55 - th; c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.07); c.beginPath(); c.moveTo(tx + tw / 2, ty + th); c.lineTo(tx + tw / 2, y); c.moveTo(tx + tw * 0.3, y); c.lineTo(tx + tw * 0.7, y); c.stroke(); caixa(c, tx, ty, tw, th, s * 0.12); c.fill(); c.stroke(); c.save(); caixa(c, tx + s * 0.1, ty + s * 0.1, tw - s * 0.2, th - s * 0.2, s * 0.06); c.clip(); const chao = ty + th - s * 0.32, px = s * 0.11; c.fillStyle = css(cor, 0.6); c.fillRect(tx, chao, tw, s * 0.05); const obst = tx + tw - ((k * 1.4) % 1) * (tw + s * 0.4); c.fillStyle = css(cor); c.fillRect(obst, chao - px * 2, px * 1.4, px * 2); const ox = tx + tw * 0.3, perto = (ox + px * 2 - obst) / (s * 0.9) + 0.5; const pulo = perto > 0 && perto < 1 ? Math.sin(perto * Math.PI) : 0; c.fillStyle = claro(cor, 1); c.fillRect(ox, chao - px * 2.4 - pulo * s * 0.45, px * 1.6, px * 2.4); if (k % 9 > 7.6) { // vitória: estrelinhas na tela c.fillStyle = claro(cor, 1); c.font = Math.round(s * 0.4) + 'px sans-serif'; c.textAlign = 'center'; c.fillText('★ ★ ★', tx + tw / 2, ty + th * 0.4); } c.restore(); }, pintar(c, x, y, s, k, cor) { const pc = pincel(k); const ex = x + s * 0.9, ew = s * 1.6, eh = s * 1.35, ey = y - s * 1.0 - eh; // cavalete c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.07); c.beginPath(); c.moveTo(ex + ew * 0.2, y); c.lineTo(ex + ew * 0.5, ey - s * 0.15); c.lineTo(ex + ew * 0.8, y); c.stroke(); c.fillStyle = claro(cor, 0.95); caixa(c, ex, ey, ew, eh, s * 0.05); c.fill(); c.stroke(); // pinceladas que vão aparecendo (sempre as mesmas para a mesma pintura) const tintas = [cor, [255, 112, 176], [255, 218, 96], [120, 255, 160]]; const n = Math.min(40, Math.floor(k / 0.7)); c.save(); caixa(c, ex, ey, ew, eh, s * 0.05); c.clip(); for (let i = 0; i < n; i++) { const a = Math.sin(i * 12.9898 + ativ.t0) * 43758.5453, rnd = a - Math.floor(a); const b2 = Math.sin(i * 78.233 + ativ.t0) * 12345.678, rnd2 = b2 - Math.floor(b2); c.fillStyle = css(tintas[i % 4], 0.9); c.beginPath(); c.ellipse(ex + ew * (0.12 + rnd * 0.76), ey + eh * (0.12 + rnd2 * 0.76), s * 0.16, s * 0.07, rnd * 3, 0, Math.PI * 2); c.fill(); } c.restore(); const b = boneco(c, x - s * 0.5, y, s, { inclina: -0.06, olhoX: 1, olhoY: pc.y * 0.5, cabeca: -0.08, bD: [1.75 + pc.y * 0.25, -0.35 + pc.x * 0.3], bE: [-0.7, 0.9], pE: [-0.12, 0], pD: [0.08, 0], }, cor, k); // pincel numa mão, paleta na outra c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06); c.beginPath(); c.moveTo(b.mD.x - s * 0.1, b.mD.y + s * 0.08); c.lineTo(b.mD.x + s * 0.4, b.mD.y - s * 0.12); c.stroke(); c.fillStyle = css(tintas[Math.floor(k / 0.7) % 4]); c.beginPath(); c.arc(b.mD.x + s * 0.43, b.mD.y - s * 0.13, s * 0.07, 0, Math.PI * 2); c.fill(); c.fillStyle = claro(cor, 0.95); c.beginPath(); c.ellipse(b.mE.x, b.mE.y, s * 0.32, s * 0.2, -0.3, 0, Math.PI * 2); c.fill(); c.stroke(); for (let i = 0; i < 3; i++) { c.fillStyle = css(tintas[i + 1]); c.beginPath(); c.arc(b.mE.x - s * 0.15 + i * s * 0.13, b.mE.y - s * 0.03, s * 0.05, 0, Math.PI * 2); c.fill(); } }, malhar(c, x, y, s, k, cor) { const f = suave(forca(k)); const b = boneco(c, x, y, s, { olhoY: -0.4 + (1 - f) * 0.5, pE: [-0.22, 0.08], pD: [0.22, -0.08], bE: [-1.25 - f * 1.75, -1.9 + f * 1.75], bD: [1.25 + f * 1.75, 1.9 - f * 1.75], }, cor, k); // halteres c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05); for (const m of [b.mE, b.mD]) { c.fillRect(m.x - s * 0.25, m.y - s * 0.035, s * 0.5, s * 0.07); for (const lado of [-1, 1]) { caixa(c, m.x + lado * s * 0.28 - s * 0.07, m.y - s * 0.17, s * 0.14, s * 0.34, s * 0.04); c.fill(); c.stroke(); } } if (f > 0.85) { // gotinha de suor const g = (k * 2) % 1; c.fillStyle = claro(cor, 1 - g); 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 }. */ function desenharPalco(o) { const v = visibilidade(o.t); if (!v) { if (palcoTela && palcoTela.style.display !== 'none') palcoTela.style.display = 'none'; return false; } if (!palcoTela) { palcoTela = document.createElement('canvas'); palcoTela.id = 'palcoAtividade'; palcoTela.style.cssText = 'position:fixed;inset:0;width:100%;height:100%;pointer-events:none;z-index:1'; document.body.append(palcoTela); pctx = palcoTela.getContext('2d'); } palcoTela.style.display = ''; const d = Math.min(1.5, window.devicePixelRatio || 1), W = Math.round(innerWidth * d), H = Math.round(innerHeight * d); if (palcoTela.width !== W || palcoTela.height !== H) { palcoTela.width = W; palcoTela.height = H; } const deitado = W >= H * 1.2; const s = deitado ? H * 0.145 : Math.min(W * 0.075, H * 0.075); const x = deitado ? W * 0.7 : W / 2, y = deitado ? H * 0.9 : H * 0.95; const c = pctx, k = o.t - ativ.t0; c.clearRect(0, 0, W, H); c.save(); c.globalAlpha = v; c.translate((1 - v) * (deitado ? W * 0.06 : 0), (1 - v) * (deitado ? 0 : H * 0.08)); // desliza ao entrar c.strokeStyle = css(o.cor, 0.35); c.lineWidth = Math.max(1, s * 0.04); // chão c.beginPath(); c.moveTo(x - s * 2.6, y); c.lineTo(Math.min(W * 0.98, x + s * 3.6), y); c.stroke(); c.shadowColor = css(o.cor, 0.55); c.shadowBlur = s * 0.35; CENAS[ativ.nome](c, x, y, s, k, o.cor); c.restore(); return true; } function esconder() { if (tela) tela.style.display = 'none'; antes = 0; } window.MotorRosto = { desenhar, retrato, esconder, /** Metade da altura do olho (em frações de RY) para o estilo/formato: posiciona as sobrancelhas. */ metadeAltura: (estilo, formato) => { const base = ESTILOS[estilo]; if (!base) return 1; if (base.lente) return base.w * 0.5 * 1.12; return ((!base.lente && FORMATOS[formato]) || base).h / 2; }, maos: (modo) => { if (DUR_MAOS[modo]) { maos.modo = modo; maos.t0 = performance.now() / 1000; } }, faixa: definirFaixa, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS), /** Atividades (momentos dele): iniciar/parar, para onde olhar e a camada da cena. */ atividade: (nome, segundos, receita) => (nome ? iniciarAtividade(nome, segundos, receita) : (pararAtividade(), false)), atividadeAtual: () => (ativ.nome && !ativ.fim ? ativ.nome : null), olharAtividade, palco: desenharPalco, atividades: Object.fromEntries(Object.entries(ATIVIDADES).map(([k, v]) => [k, { nome: v.nome, emoji: v.emoji }])) }; })();