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:
@@ -540,11 +540,31 @@ 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;
|
||||
const OLHAR = { x: 0, y: 0, ax: 0, ay: 0, prox: 0 }; // ponto que os olhos estão fitando no repouso
|
||||
function atividade(nome, segundos) {
|
||||
function atividade(nome, segundos, receita) {
|
||||
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, segundos) && nome === "cantar") tocarSom("musica");
|
||||
if (window.MotorRosto.atividade(nome, segundos, receita) && nome === "cantar") tocarSom("musica");
|
||||
}
|
||||
|
||||
/* Expressão criada no estúdio (criacao.py): receita já validada no servidor, só números e nomes de
|
||||
peças. Vira uma sequência igual às de sequencias(), com mãos e espiral se a receita pedir. */
|
||||
function corDaReceita(nome) {
|
||||
return { claro: "claro", escuro: "escuro", rosa: ROSA, amarelo: AMARELO, verde: VERDE, vermelho: VERM }[nome] || CIANO;
|
||||
}
|
||||
function tocarExpressaoCriada(texto) {
|
||||
let rc;
|
||||
try { rc = JSON.parse(texto); } catch (e) { return; }
|
||||
if (!rc || !Array.isArray(rc.passos) || !rc.passos.length) return;
|
||||
if (r.dormindo) r.dormindo = false;
|
||||
r.fila = rc.passos.slice(0, 12).map((p) => [{
|
||||
forma: p.forma || "aberto", forma2: null, boca: p.boca || "neutra", cor: corDaReceita(p.cor),
|
||||
dx: +p.dx || 0, dy: +p.dy || 0, esc: +p.esc || 1, ab: typeof p.ab === "number" ? p.ab : 1,
|
||||
}, Math.max(0.05, Math.min(2, +p.duracao || 0.3))]);
|
||||
proximo();
|
||||
const total = r.fila.reduce((a, passo) => a + passo[1], 0);
|
||||
if (rc.espiral) ESPIRAL.ate = performance.now() / 1000 + total;
|
||||
if (rc.maos && rc.maos !== "nenhuma" && window.MotorRosto && window.MotorRosto.maos) window.MotorRosto.maos(rc.maos);
|
||||
}
|
||||
|
||||
function repouso(t) { // pose calma: olha devagar, "respira"
|
||||
@@ -689,6 +709,10 @@ function trata(ordem) {
|
||||
HUMOR = ordem.slice(6).trim();
|
||||
} else if (ordem.indexOf("ritmo ") === 0) {
|
||||
PERFIL = PERFIS[ordem.slice(6).trim()] || PERFIS.normal;
|
||||
} else if (ordem.indexOf("expressao_criada ") === 0) {
|
||||
tocarExpressaoCriada(ordem.slice(17));
|
||||
} else if (ordem.indexOf("atividade_criada ") === 0) {
|
||||
try { const rc = JSON.parse(ordem.slice(17)); atividade("receita", +rc.duracao || 25, rc); } catch (e) {}
|
||||
} else if (ordem.indexOf("atividade ") === 0) {
|
||||
const [nome, seg] = ordem.slice(10).trim().split(/\s+/); // "atividade bola 180"
|
||||
atividade(nome, parseFloat(seg) || 0);
|
||||
|
||||
@@ -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 }])) };
|
||||
|
||||
Reference in New Issue
Block a user