feat(reacoes): emojis e figurinhas no mesmo padrao do rosto novo
- rosto-motor.js desenha emoji e figurinha dentro dos olhos no estilo e formato escolhidos (brilho, piscada; na Lente no lugar da iris; na Matriz os LEDs usam as cores do desenho) - MotorRosto.retrato desenha um retrato parado em qualquer canvas - miniaturas da biblioteca no painel usam o mesmo motor, no rosto, formato e cor atuais do robo (3:2, com o simbolo "ao redor" no canto) - emoji e figurinha "ao redor" ganham o brilho na cor da reacao - cartoes da biblioteca com as acoes numa linha so Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -388,11 +388,13 @@ function desenhar() {
|
|||||||
const mover = seguindo ? .55 : (r.kf !== null || r.dormindo) ? 0.25 : (mirando ? 0.25 : 0.0);
|
const mover = seguindo ? .55 : (r.kf !== null || r.dormindo) ? 0.25 : (mirando ? 0.25 : 0.0);
|
||||||
const dx = seguindo ? MOV.olharX : r.dx, dy = seguindo ? MOV.olharY : r.dy;
|
const dx = seguindo ? MOV.olharX : r.dx, dy = seguindo ? MOV.olharY : r.dy;
|
||||||
const olhos = [[E1, ab, r.forma, +1], [E2, ab2, f2, -1]];
|
const olhos = [[E1, ab, r.forma, +1], [E2, ab2, f2, -1]];
|
||||||
const comMotor = ROSTO.motor && window.MotorRosto && !window.carinhaOlhosIcone && !window.carinhaOlhosDesenho;
|
const comMotor = ROSTO.motor && window.MotorRosto;
|
||||||
OLHOS.forEach((g) => { g.style.display = comMotor ? "none" : ""; });
|
OLHOS.forEach((g) => { g.style.display = comMotor ? "none" : ""; });
|
||||||
if (comMotor) {
|
if (comMotor) {
|
||||||
window.MotorRosto.desenhar({
|
window.MotorRosto.desenhar({
|
||||||
estilo: ROSTO.motor, formato: FORMATO, t: tAgora, cor: r.cor, dormindo: r.dormindo,
|
estilo: ROSTO.motor, formato: FORMATO, t: tAgora, cor: r.cor, dormindo: r.dormindo,
|
||||||
|
icone: !r.dormindo && !window.carinhaOlhosDesenho ? window.carinhaOlhosIcone || null : null,
|
||||||
|
desenho: !r.dormindo ? window.carinhaOlhosDesenho || null : null,
|
||||||
parado: !r.kf && !seguindo && !r.dormindo,
|
parado: !r.kf && !seguindo && !r.dormindo,
|
||||||
olhos: olhos.map(([c, abx, fx, lado]) => ({
|
olhos: olhos.map(([c, abx, fx, lado]) => ({
|
||||||
cx: c[0] + dx * mover * ESC, cy: c[1] + dy * mover * ESC, ry: RY * esc,
|
cx: c[0] + dx * mover * ESC, cy: c[1] + dy * mover * ESC, ry: RY * esc,
|
||||||
|
|||||||
@@ -82,9 +82,35 @@
|
|||||||
c.fill();
|
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).
|
// Olho plano (vivo, bloco, matriz): corpo + pálpebras recortadas (transparentes).
|
||||||
function olhoPlano(c, o, p, est, e) {
|
function olhoPlano(c, o, p, est, e) {
|
||||||
const respira = 1 + 0.012 * Math.sin(o.t * 1.4);
|
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 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 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 amassa = fecha > 0.5 && e.top < 0.5 ? (fecha - 0.5) * 0.2 : 0; // piscada achata e alarga
|
||||||
@@ -105,7 +131,8 @@
|
|||||||
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.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.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';
|
c.globalCompositeOperation = 'source-over';
|
||||||
if (est.brilho && fecha < 0.55 && e.bot < 0.3) { // reflexo que acompanha o olhar
|
if (temConteudo && fecha < 0.6) conteudo(c, o, p.cx, p.cy, Math.min(w, h) * 0.74);
|
||||||
|
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 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));
|
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.fillStyle = 'rgba(245,250,255,0.9)';
|
||||||
@@ -117,11 +144,15 @@
|
|||||||
// Olho lente: esclera, íris, pupila que dilata e pálpebras de metal.
|
// Olho lente: esclera, íris, pupila que dilata e pálpebras de metal.
|
||||||
function olhoLente(c, o, p, est, e) {
|
function olhoLente(c, o, p, est, e) {
|
||||||
const R = est.w * p.ry * 0.5 * Math.max(1, p.ab) * (1 + 0.01 * Math.sin(o.t * 1.4));
|
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(e.top, 1 - Math.min(1, p.ab)));
|
const fecha = Math.min(1, Math.max(o.icone || o.desenho ? 0 : e.top, 1 - Math.min(1, p.ab)));
|
||||||
c.save();
|
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 * 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.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);
|
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 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 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);
|
const g = c.createRadialGradient(ix, iy, R * 0.05, ix, iy, R * 0.55);
|
||||||
@@ -130,6 +161,7 @@
|
|||||||
const pupila = p.ab > 1.05 ? 0.12 : e.bot > 0.3 ? 0.3 : 0.2; // susto contrai, alegria dilata
|
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, 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();
|
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);
|
const metal = c.createLinearGradient(0, p.cy - R, 0, p.cy + R);
|
||||||
metal.addColorStop(0, '#5b6675'); metal.addColorStop(1, '#2b323d');
|
metal.addColorStop(0, '#5b6675'); metal.addColorStop(1, '#2b323d');
|
||||||
c.fillStyle = metal;
|
c.fillStyle = metal;
|
||||||
@@ -180,7 +212,7 @@
|
|||||||
for (let j = 0; j < linhas; j++) for (let i = 0; i < colunas; i++) {
|
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;
|
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.beginPath(); ctx.arc((i + 0.5) * passo, (j + 0.5) * passo, raio, 0, Math.PI * 2);
|
||||||
ctx.fillStyle = a > 0.25 ? css(o.cor, 0.35 + a * 0.65) : 'rgba(255,255,255,0.045)';
|
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();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,7 +249,41 @@
|
|||||||
ctx.restore();
|
ctx.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();
|
||||||
|
}
|
||||||
|
|
||||||
function esconder() { if (tela) tela.style.display = 'none'; antes = 0; }
|
function esconder() { if (tela) tela.style.display = 'none'; antes = 0; }
|
||||||
|
|
||||||
window.MotorRosto = { desenhar, esconder, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS) };
|
window.MotorRosto = { desenhar, retrato, esconder, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS) };
|
||||||
})();
|
})();
|
||||||
|
|||||||
+3
-1
@@ -388,11 +388,13 @@ function desenhar() {
|
|||||||
const mover = seguindo ? .55 : (r.kf !== null || r.dormindo) ? 0.25 : (mirando ? 0.25 : 0.0);
|
const mover = seguindo ? .55 : (r.kf !== null || r.dormindo) ? 0.25 : (mirando ? 0.25 : 0.0);
|
||||||
const dx = seguindo ? MOV.olharX : r.dx, dy = seguindo ? MOV.olharY : r.dy;
|
const dx = seguindo ? MOV.olharX : r.dx, dy = seguindo ? MOV.olharY : r.dy;
|
||||||
const olhos = [[E1, ab, r.forma, +1], [E2, ab2, f2, -1]];
|
const olhos = [[E1, ab, r.forma, +1], [E2, ab2, f2, -1]];
|
||||||
const comMotor = ROSTO.motor && window.MotorRosto && !window.carinhaOlhosIcone && !window.carinhaOlhosDesenho;
|
const comMotor = ROSTO.motor && window.MotorRosto;
|
||||||
OLHOS.forEach((g) => { g.style.display = comMotor ? "none" : ""; });
|
OLHOS.forEach((g) => { g.style.display = comMotor ? "none" : ""; });
|
||||||
if (comMotor) {
|
if (comMotor) {
|
||||||
window.MotorRosto.desenhar({
|
window.MotorRosto.desenhar({
|
||||||
estilo: ROSTO.motor, formato: FORMATO, t: tAgora, cor: r.cor, dormindo: r.dormindo,
|
estilo: ROSTO.motor, formato: FORMATO, t: tAgora, cor: r.cor, dormindo: r.dormindo,
|
||||||
|
icone: !r.dormindo && !window.carinhaOlhosDesenho ? window.carinhaOlhosIcone || null : null,
|
||||||
|
desenho: !r.dormindo ? window.carinhaOlhosDesenho || null : null,
|
||||||
parado: !r.kf && !seguindo && !r.dormindo,
|
parado: !r.kf && !seguindo && !r.dormindo,
|
||||||
olhos: olhos.map(([c, abx, fx, lado]) => ({
|
olhos: olhos.map(([c, abx, fx, lado]) => ({
|
||||||
cx: c[0] + dx * mover * ESC, cy: c[1] + dy * mover * ESC, ry: RY * esc,
|
cx: c[0] + dx * mover * ESC, cy: c[1] + dy * mover * ESC, ry: RY * esc,
|
||||||
|
|||||||
+2
-2
@@ -15,13 +15,13 @@
|
|||||||
function visual(recipe){
|
function visual(recipe){
|
||||||
if(recipe.emoji){
|
if(recipe.emoji){
|
||||||
const e=recipe.emoji;if(e.local==='olhos'){window.carinhaOlhosIcone=e.caractere;return;}
|
const e=recipe.emoji;if(e.local==='olhos'){window.carinhaOlhosIcone=e.caractere;return;}
|
||||||
badge.style.cssText='position:fixed;right:5%;top:8%;z-index:15;font:clamp(42px,10vw,90px) sans-serif;pointer-events:none';badge.textContent=e.caractere;
|
badge.style.cssText='position:fixed;right:5%;top:8%;z-index:15;font:clamp(42px,10vw,90px) sans-serif;pointer-events:none;filter:drop-shadow(0 0 14px '+(recipe.passos[0].cor||'#5cd6ff')+')';badge.textContent=e.caractere;
|
||||||
animation=badge.animate([{transform:'scale(.92)'},{transform:'scale(1.06)'},{transform:'scale(.92)'}],{duration:1400,iterations:Infinity});return;
|
animation=badge.animate([{transform:'scale(.92)'},{transform:'scale(1.06)'},{transform:'scale(.92)'}],{duration:1400,iterations:Infinity});return;
|
||||||
}
|
}
|
||||||
if(recipe.figurinha){
|
if(recipe.figurinha){
|
||||||
const svg=window.carinhaConstruirSVG(recipe.figurinha);if(!svg)return;
|
const svg=window.carinhaConstruirSVG(recipe.figurinha);if(!svg)return;
|
||||||
if(recipe.figurinha.local==='olhos'){window.carinhaOlhosDesenho=recipe.figurinha;return;}
|
if(recipe.figurinha.local==='olhos'){window.carinhaOlhosDesenho=recipe.figurinha;return;}
|
||||||
badge.style.cssText='position:fixed;right:5%;top:8%;width:24%;height:30%;z-index:15;pointer-events:none';svg.style.cssText='width:100%;height:100%';badge.append(svg);
|
badge.style.cssText='position:fixed;right:5%;top:8%;width:24%;height:30%;z-index:15;pointer-events:none;filter:drop-shadow(0 0 14px '+(recipe.passos[0].cor||'#5cd6ff')+')';svg.style.cssText='width:100%;height:100%';badge.append(svg);
|
||||||
animation=svg.animate([{transform:'scale(.92) rotate(-3deg)'},{transform:'scale(1.05) rotate(3deg)'},{transform:'scale(.92) rotate(-3deg)'}],{duration:1500,iterations:Infinity});return;
|
animation=svg.animate([{transform:'scale(.92) rotate(-3deg)'},{transform:'scale(1.05) rotate(3deg)'},{transform:'scale(.92) rotate(-3deg)'}],{duration:1500,iterations:Infinity});return;
|
||||||
}
|
}
|
||||||
if(['ideia','cafe'].includes(recipe.simbolo)){window.carinhaOlhosIcone=icons[recipe.simbolo];return;}
|
if(['ideia','cafe'].includes(recipe.simbolo)){window.carinhaOlhosIcone=icons[recipe.simbolo];return;}
|
||||||
|
|||||||
+70
-4
@@ -82,9 +82,35 @@
|
|||||||
c.fill();
|
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).
|
// Olho plano (vivo, bloco, matriz): corpo + pálpebras recortadas (transparentes).
|
||||||
function olhoPlano(c, o, p, est, e) {
|
function olhoPlano(c, o, p, est, e) {
|
||||||
const respira = 1 + 0.012 * Math.sin(o.t * 1.4);
|
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 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 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 amassa = fecha > 0.5 && e.top < 0.5 ? (fecha - 0.5) * 0.2 : 0; // piscada achata e alarga
|
||||||
@@ -105,7 +131,8 @@
|
|||||||
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.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.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';
|
c.globalCompositeOperation = 'source-over';
|
||||||
if (est.brilho && fecha < 0.55 && e.bot < 0.3) { // reflexo que acompanha o olhar
|
if (temConteudo && fecha < 0.6) conteudo(c, o, p.cx, p.cy, Math.min(w, h) * 0.74);
|
||||||
|
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 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));
|
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.fillStyle = 'rgba(245,250,255,0.9)';
|
||||||
@@ -117,11 +144,15 @@
|
|||||||
// Olho lente: esclera, íris, pupila que dilata e pálpebras de metal.
|
// Olho lente: esclera, íris, pupila que dilata e pálpebras de metal.
|
||||||
function olhoLente(c, o, p, est, e) {
|
function olhoLente(c, o, p, est, e) {
|
||||||
const R = est.w * p.ry * 0.5 * Math.max(1, p.ab) * (1 + 0.01 * Math.sin(o.t * 1.4));
|
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(e.top, 1 - Math.min(1, p.ab)));
|
const fecha = Math.min(1, Math.max(o.icone || o.desenho ? 0 : e.top, 1 - Math.min(1, p.ab)));
|
||||||
c.save();
|
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 * 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.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);
|
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 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 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);
|
const g = c.createRadialGradient(ix, iy, R * 0.05, ix, iy, R * 0.55);
|
||||||
@@ -130,6 +161,7 @@
|
|||||||
const pupila = p.ab > 1.05 ? 0.12 : e.bot > 0.3 ? 0.3 : 0.2; // susto contrai, alegria dilata
|
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, 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();
|
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);
|
const metal = c.createLinearGradient(0, p.cy - R, 0, p.cy + R);
|
||||||
metal.addColorStop(0, '#5b6675'); metal.addColorStop(1, '#2b323d');
|
metal.addColorStop(0, '#5b6675'); metal.addColorStop(1, '#2b323d');
|
||||||
c.fillStyle = metal;
|
c.fillStyle = metal;
|
||||||
@@ -180,7 +212,7 @@
|
|||||||
for (let j = 0; j < linhas; j++) for (let i = 0; i < colunas; i++) {
|
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;
|
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.beginPath(); ctx.arc((i + 0.5) * passo, (j + 0.5) * passo, raio, 0, Math.PI * 2);
|
||||||
ctx.fillStyle = a > 0.25 ? css(o.cor, 0.35 + a * 0.65) : 'rgba(255,255,255,0.045)';
|
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();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,7 +249,41 @@
|
|||||||
ctx.restore();
|
ctx.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();
|
||||||
|
}
|
||||||
|
|
||||||
function esconder() { if (tela) tela.style.display = 'none'; antes = 0; }
|
function esconder() { if (tela) tela.style.display = 'none'; antes = 0; }
|
||||||
|
|
||||||
window.MotorRosto = { desenhar, esconder, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS) };
|
window.MotorRosto = { desenhar, retrato, esconder, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS) };
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
<!-- mesmo motor de rosto do celular, usado nas miniaturas das reações -->
|
||||||
|
<script src="/rosto-motor.js"></script>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,56 +1,84 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { rgb, ROSTOS_INFO } from '../comandos.svelte';
|
||||||
|
import { estado } from '../estado.svelte';
|
||||||
import { SIMBOLO_EMOJI } from '../treinamento.svelte';
|
import { SIMBOLO_EMOJI } from '../treinamento.svelte';
|
||||||
|
|
||||||
// Prévia estática de uma reação (sem iframe): figurinha SVG, emoji ou olhos do 1º passo.
|
// Retrato parado da reação, desenhado pelo mesmo motor do celular (rosto-motor.js),
|
||||||
|
// no rosto, formato e cor que o robô está usando agora.
|
||||||
let { receita, tamanho = 64 }: { receita: any; tamanho?: number } = $props();
|
let { receita, tamanho = 64 }: { receita: any; tamanho?: number } = $props();
|
||||||
|
let tela: HTMLCanvasElement;
|
||||||
|
|
||||||
const passo = $derived(receita?.passos?.[0]);
|
const passo = $derived(receita?.passos?.[0]);
|
||||||
const simbolo = $derived(receita?.emoji?.caractere || SIMBOLO_EMOJI[receita?.simbolo] || '');
|
const nosOlhos = $derived({
|
||||||
|
icone:
|
||||||
|
receita?.emoji?.local === 'olhos'
|
||||||
|
? receita.emoji.caractere
|
||||||
|
: ['ideia', 'cafe'].includes(receita?.simbolo) && !receita?.emoji && !receita?.figurinha
|
||||||
|
? SIMBOLO_EMOJI[receita.simbolo]
|
||||||
|
: null,
|
||||||
|
desenho: receita?.figurinha?.local === 'olhos' ? receita.figurinha : null,
|
||||||
|
});
|
||||||
|
// O que aparece fora dos olhos (emoji/figurinha "ao redor", notas, estrelas, corações).
|
||||||
|
const aoRedor = $derived.by(() => {
|
||||||
|
if (receita?.emoji && receita.emoji.local !== 'olhos') return { emoji: receita.emoji.caractere };
|
||||||
|
if (receita?.figurinha && receita.figurinha.local !== 'olhos') return { figurinha: receita.figurinha };
|
||||||
|
if (['musica', 'estrela', 'coracao'].includes(receita?.simbolo)) return { emoji: SIMBOLO_EMOJI[receita.simbolo] };
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
const estilo = $derived(ROSTOS_INFO[estado.aparencia.rosto]?.novo ? estado.aparencia.rosto : 'vivo');
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const motor = (window as any).MotorRosto;
|
||||||
|
if (!motor || !tela) return;
|
||||||
|
const d = Math.min(2, devicePixelRatio || 1);
|
||||||
|
tela.width = Math.round(tamanho * 1.5 * d); // 3:2, cabe os dois olhos com folga
|
||||||
|
tela.height = Math.round(tamanho * d);
|
||||||
|
motor.retrato(tela, {
|
||||||
|
estilo,
|
||||||
|
formato: estado.aparencia.formato ?? 'padrao',
|
||||||
|
cor: /^#[0-9a-f]{6}$/i.test(passo?.cor ?? '') ? rgb(passo.cor) : estado.aparencia.cor,
|
||||||
|
forma: passo?.olhos ?? 'aberto',
|
||||||
|
icone: nosOlhos.icone,
|
||||||
|
desenho: nosOlhos.desenho ? $state.snapshot(nosOlhos.desenho) : null,
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="miniatura" style="--t: {tamanho}px" aria-hidden="true">
|
<div class="miniatura" style="--t: {tamanho}px" aria-hidden="true">
|
||||||
{#if receita?.figurinha?.formas}
|
<canvas bind:this={tela}></canvas>
|
||||||
<svg viewBox="0 0 100 100" width={tamanho} height={tamanho}>
|
{#if aoRedor?.emoji}
|
||||||
{#each receita.figurinha.formas as forma, i (i)}
|
<span class="extra">{aoRedor.emoji}</span>
|
||||||
<path d={forma.caminho} fill={forma.cor} />
|
{:else if aoRedor?.figurinha}
|
||||||
{/each}
|
<svg class="extra" viewBox="0 0 100 100">
|
||||||
|
{#each aoRedor.figurinha.formas as forma, i (i)}<path d={forma.caminho} fill={forma.cor} />{/each}
|
||||||
</svg>
|
</svg>
|
||||||
{:else if receita?.emoji}
|
|
||||||
<span class="emoji">{receita.emoji.caractere}</span>
|
|
||||||
{:else}
|
|
||||||
<svg viewBox="0 0 100 60" width={tamanho} height={tamanho * 0.6}>
|
|
||||||
{#if passo?.olhos === 'feliz'}
|
|
||||||
<path d="M18 38 Q30 18 42 38 M58 38 Q70 18 82 38" stroke={passo.cor} stroke-width="8" fill="none" stroke-linecap="round" />
|
|
||||||
{:else if passo?.olhos === 'triste'}
|
|
||||||
<path d="M18 26 Q30 44 42 26 M58 26 Q70 44 82 26" stroke={passo.cor} stroke-width="8" fill="none" stroke-linecap="round" />
|
|
||||||
{:else}
|
|
||||||
{@const ry = passo?.olhos === 'surpreso' ? 20 : 16}
|
|
||||||
<ellipse cx="30" cy="30" rx="12" {ry} fill={passo?.cor ?? '#5cd6ff'} />
|
|
||||||
<ellipse cx="70" cy="30" rx="12" {ry} fill={passo?.cor ?? '#5cd6ff'} />
|
|
||||||
{/if}
|
|
||||||
</svg>
|
|
||||||
{#if simbolo}<span class="simbolo">{simbolo}</span>{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.miniatura {
|
.miniatura {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: var(--t);
|
width: calc(var(--t) * 1.5);
|
||||||
height: var(--t);
|
height: var(--t);
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
border-radius: var(--raio);
|
border-radius: var(--raio);
|
||||||
background: #05080c;
|
background: #000;
|
||||||
flex: none;
|
flex: none;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.emoji {
|
canvas {
|
||||||
font-size: calc(var(--t) * 0.55);
|
width: 100%;
|
||||||
line-height: 1;
|
height: 100%;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
.simbolo {
|
.extra {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 2px;
|
right: 4px;
|
||||||
bottom: 0;
|
top: 3px;
|
||||||
font-size: calc(var(--t) * 0.3);
|
width: calc(var(--t) * 0.34);
|
||||||
|
height: calc(var(--t) * 0.34);
|
||||||
|
font-size: calc(var(--t) * 0.26);
|
||||||
|
line-height: calc(var(--t) * 0.34);
|
||||||
|
text-align: center;
|
||||||
|
filter: drop-shadow(0 0 4px rgb(92 214 255 / 0.6));
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -364,6 +364,14 @@
|
|||||||
.espaco {
|
.espaco {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
/* seis ações pequenas + Mostrar cabem numa linha num cartão de 300 px */
|
||||||
|
.acoes :global(.btn.pequeno.icone) {
|
||||||
|
width: 28px;
|
||||||
|
}
|
||||||
|
.acoes {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
.marcado {
|
.marcado {
|
||||||
color: var(--ok);
|
color: var(--ok);
|
||||||
border-color: var(--ok);
|
border-color: var(--ok);
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|||||||
const alvo = process.env.CARINHA_SERVIDOR || 'http://127.0.0.1:8100';
|
const alvo = process.env.CARINHA_SERVIDOR || 'http://127.0.0.1:8100';
|
||||||
const api = ['/eventos', '/estado', '/comando', '/parar-tudo', '/rearmar', '/fila', '/config', '/faces', '/gestos',
|
const api = ['/eventos', '/estado', '/comando', '/parar-tudo', '/rearmar', '/fila', '/config', '/faces', '/gestos',
|
||||||
'/app', '/rtc', '/camera', '/conversa', '/treinamento', '/braco', '/servos', '/ia', '/api', '/gesto', '/face',
|
'/app', '/rtc', '/camera', '/conversa', '/treinamento', '/braco', '/servos', '/ia', '/api', '/gesto', '/face',
|
||||||
'/carinha.html', '/reacoes-render.js', '/rastreador-worker.js', '/vendor', '/qualidade-auto.js', '/app.apk'];
|
'/carinha.html', '/reacoes-render.js', '/rosto-motor.js', '/rastreador-worker.js', '/vendor', '/qualidade-auto.js', '/app.apk'];
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [svelte()],
|
plugins: [svelte()],
|
||||||
|
|||||||
Reference in New Issue
Block a user