feat(criacao): estúdio para criar expressões, atividades e figurinhas com IA

Menu "Criar com IA" (Ao vivo › Gestos e Reações) com grade igual à dos
favoritos. Para cada pedido escolhe-se o tipo (expressão, atividade ou
figurinha) e a IA: Gemini, IA local (qualquer servidor compatível com
OpenAI na rede, como o ROG Ally com LM Studio/llama-server) ou o Qwen do S10.

- A IA não escreve código: responde uma receita de formato fixo (schema
  JSON) montada só com peças que o rosto já anima. O servidor valida,
  limita números e descarta peças desconhecidas antes de chegar ao celular.
- Reaproveita o que existe: o prompt leva exemplos reais dos gestos e
  atividades atuais, e "Basear em" manda a receita de um gesto, atividade
  ou criação para a IA fazer uma variação.
- Expressões viram sequências como as de sequencias(), com mãos e espiral.
- Atividades usam uma cena genérica com o bonequinho (poses, pernas, pulo,
  balanço) e os objetos das cenas feitas à mão (bola, livro, xícara,
  notas, estrelas, coração, Zzz, chama, bolha, halter, microfone,
  controle, pincel, flor), com movimentos pular/girar/subir/orbitar.
- Figurinhas reaproveitam o desenho e a biblioteca de reações.
- A estrela põe expressões e atividades criadas nos favoritos (e nas
  teclas 1 a 9); a IA local é configurada na própria tela, com teste.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-06 22:22:52 -03:00
co-authored by Claude Opus 5.5
parent ff06f02705
commit 05e82dfd75
19 changed files with 1522 additions and 32 deletions
+124 -5
View File
@@ -568,15 +568,16 @@
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 };
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) {
if (!ATIVIDADES[nome]) { pararAtividade(); return false; }
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.nome = nome; ativ.t0 = agoraS(); ativ.fim = 0; ativ.receita = receita || null;
ativ.dur = segundos || a + Math.random() * (b - a);
return true;
}
@@ -620,6 +621,19 @@
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') {
@@ -765,7 +779,112 @@
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,
@@ -1268,7 +1387,7 @@
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) => (nome ? iniciarAtividade(nome, segundos) : (pararAtividade(), false)),
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 }])) };