Files
carinha-control-panel/web/src/lib/comandos.svelte.ts
T
julioandClaude Opus 5.5 972dd8dd66 feat(rosto): motor de palpebras, 4 rostos novos, formato do olho e tela Rosto
- rosto-motor.js: emocoes como posicao de palpebra (cima, inclinacao, baixo)
  com transicao suave; respiracao, sacadas e piscada que achata o olho
- rostos novos Carinha Viva, Bloco, Matriz (LEDs) e Lente (iris/pupila);
  os classicos continuam; emoji/figurinha nos olhos seguem no SVG
- formato do olho (padrao, redondo, pilula, quadrado, largo) para Viva,
  Bloco e Matriz; salvo na aparencia e enviado como ordem "formato X"
- toque na tela do S10: cutucar = susto e risada, deslizar = carinho,
  tocar dormindo acorda
- painel: tela Rosto (celular em destaque, cartoes de rosto, formatos,
  cores, testar emocoes so na previa ou no celular, lista do que ja esta
  pronto); Ao vivo leva a ela; paleta ganha rostos e formatos
- corrige /comando rosto que nao salvava a aparencia

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 01:33:56 -03:00

106 lines
4.8 KiB
TypeScript

/** Ações que qualquer tela pode disparar: gestos, rosto, cor, E-Stop. */
import { postar } from './api';
import { avisos, tentar } from './avisos.svelte';
import { estado } from './estado.svelte';
/** Nome amigável e emoji de cada gesto do rosto ('parar' fecha o kiosk: não vira botão). */
export const GESTOS_INFO: Record<string, { nome: string; emoji: string }> = {
feliz: { nome: 'Feliz', emoji: '😊' },
alegre: { nome: 'Alegre', emoji: '😄' },
coracao: { nome: 'Coração', emoji: '❤️' },
piscar: { nome: 'Piscar', emoji: '😌' },
piscar2: { nome: 'Piscar duplo', emoji: '😑' },
piscadinha: { nome: 'Piscadinha', emoji: '😉' },
surpreso: { nome: 'Surpreso', emoji: '😮' },
triste: { nome: 'Triste', emoji: '😢' },
bravo: { nome: 'Bravo', emoji: '😠' },
olhar_esq: { nome: 'Olhar à esquerda', emoji: '👈' },
olhar_dir: { nome: 'Olhar à direita', emoji: '👉' },
olhar_baixo: { nome: 'Olhar para baixo', emoji: '👇' },
dormir: { nome: 'Dormir', emoji: '😴' },
acordar: { nome: 'Acordar', emoji: '🌅' },
aleatorio: { nome: 'Aleatório', emoji: '🎲' },
};
export const gestosDisponiveis = () => estado.gestos.filter((g) => g in GESTOS_INFO);
const CHAVE_FAVORITOS = 'carinha_favoritos';
const PADRAO_FAVORITOS = ['feliz', 'coracao', 'piscadinha', 'surpreso', 'triste', 'olhar_esq', 'olhar_dir', 'dormir', 'acordar'];
function lerFavoritos(): string[] {
try {
const v = JSON.parse(localStorage.getItem(CHAVE_FAVORITOS) || 'null');
if (Array.isArray(v) && v.every((x) => typeof x === 'string')) return v.slice(0, 9);
} catch {
/* sem armazenamento */
}
return PADRAO_FAVORITOS;
}
class Favoritos {
lista = $state<string[]>(lerFavoritos());
alternar(g: string) {
this.lista = this.lista.includes(g) ? this.lista.filter((x) => x !== g) : [...this.lista, g].slice(0, 9);
try {
localStorage.setItem(CHAVE_FAVORITOS, JSON.stringify(this.lista));
} catch {
/* ok */
}
}
}
export const favoritos = new Favoritos();
export const gesto = (nome: string) => tentar(() => postar('/comando', { tipo: 'gesto', valor: nome }));
export const rosto = (nome: string) => tentar(() => postar('/comando', { tipo: 'rosto', valor: nome }));
export const cor = (rgb: [number, number, number]) => tentar(() => postar('/comando', { tipo: 'cor', valor: rgb }));
export const formato = (nome: string) => tentar(() => postar('/comando', { tipo: 'formato', valor: nome }));
/** Rostos: os novos usam o motor de pálpebras (rosto-motor.js) e aceitam formato de olho. */
export const ROSTOS_INFO: Record<string, { nome: string; descricao: string; novo: boolean; formato: boolean }> = {
vivo: { nome: 'Carinha Viva', descricao: 'O rosto de sempre com pálpebras, brilho e micro-movimentos.', novo: true, formato: true },
bloco: { nome: 'Bloco', descricao: 'Olhos quadrados que se recortam, estilo robô de mesa.', novo: true, formato: true },
matriz: { nome: 'Matriz', descricao: 'Painel de LEDs retrô; cada olho é feito de pontos.', novo: true, formato: true },
lente: { nome: 'Lente', descricao: 'Íris e pupila que dilata, pálpebras de metal.', novo: true, formato: false },
carinha: { nome: 'Clássico', descricao: 'O desenho original, com boca.', novo: false, formato: false },
miudo: { nome: 'Miúdo', descricao: 'Clássico com olhos menores.', novo: false, formato: false },
eilik: { nome: 'Eilik', descricao: 'Clássico sem boca.', novo: false, formato: false },
};
export const FORMATOS_INFO: Record<string, string> = {
padrao: 'Padrão', redondo: 'Redondo', pilula: 'Pílula', quadrado: 'Quadrado', largo: 'Largo',
};
export const nomeRosto = (id: string) => ROSTOS_INFO[id]?.nome ?? id;
export const recarregarCelular = () =>
tentar(async () => {
const r = await postar<{ mensagem: string }>('/comando', { tipo: 'recarregar' });
avisos.ok(r.mensagem);
});
export async function pararTudo() {
try {
await postar('/parar-tudo');
avisos.mostrar('E-Stop acionado: motores desligados e travados.', 'erro');
} catch (e) {
avisos.erro(e);
}
}
export const rearmar = () => tentar(() => postar('/rearmar'), 'Motores rearmados.');
export function hex(rgb: number[]) {
return '#' + rgb.map((v) => v.toString(16).padStart(2, '0')).join('');
}
export function rgb(hexCor: string): [number, number, number] {
const n = parseInt(hexCor.slice(1), 16);
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
export const CORES: { nome: string; rgb: [number, number, number] }[] = [
{ nome: 'Ciano', rgb: [92, 214, 255] },
{ nome: 'Verde', rgb: [74, 222, 128] },
{ nome: 'Amarelo', rgb: [251, 191, 36] },
{ nome: 'Laranja', rgb: [255, 140, 50] },
{ nome: 'Rosa', rgb: [255, 110, 190] },
{ nome: 'Roxo', rgb: [167, 139, 250] },
{ nome: 'Vermelho', rgb: [248, 90, 90] },
{ nome: 'Branco', rgb: [235, 240, 245] },
];