- 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>
413 lines
12 KiB
Svelte
413 lines
12 KiB
Svelte
<script lang="ts">
|
|
import { ArrowLeft, Check } from '@lucide/svelte';
|
|
import {
|
|
cor, CORES, FORMATOS_INFO, formato, gesto, GESTOS_INFO, hex, nomeRosto, rgb, rosto, ROSTOS_INFO,
|
|
} from '../lib/comandos.svelte';
|
|
import RostoPrevia from '../lib/componentes/RostoPrevia.svelte';
|
|
import Toggle from '../lib/componentes/Toggle.svelte';
|
|
import { estado } from '../lib/estado.svelte';
|
|
|
|
let previa = $state<{ ordem: (t: string) => void }>();
|
|
// Por padrão as emoções só aparecem aqui na prévia; ligado, vão também para o celular.
|
|
let noCelular = $state(false);
|
|
|
|
const atual = $derived(estado.aparencia.rosto);
|
|
const formatoAtual = $derived(estado.aparencia.formato ?? 'padrao');
|
|
const aceitaFormato = $derived(!!ROSTOS_INFO[atual]?.formato);
|
|
const novos = $derived(estado.rostos.filter((r) => ROSTOS_INFO[r]?.novo));
|
|
const classicos = $derived(estado.rostos.filter((r) => !ROSTOS_INFO[r]?.novo));
|
|
const formatos = $derived(estado.formatos.length ? estado.formatos : Object.keys(FORMATOS_INFO));
|
|
|
|
const EMOCOES = ['feliz', 'coracao', 'surpreso', 'triste', 'bravo', 'piscadinha', 'olhar_esq', 'olhar_dir', 'dormir', 'acordar'];
|
|
function emocao(g: string) {
|
|
if (noCelular) gesto(g);
|
|
else previa?.ordem(g);
|
|
}
|
|
|
|
const IDEIAS: { titulo: string; texto: string; feito: boolean; onde: string }[] = [
|
|
{ titulo: 'Pálpebras no lugar de formas prontas', texto: 'Cada emoção é posição de pálpebra; as trocas ficam suaves em vez de saltar.', feito: true, onde: 'rosto' },
|
|
{ titulo: 'Nunca parado', texto: 'Respiração, sacadas e piscada que achata o olho. Também protege a tela AMOLED.', feito: true, onde: 'rosto' },
|
|
{ titulo: 'Toque na tela', texto: 'Cutucar dá susto e risada; deslizar o dedo é carinho; tocar acorda.', feito: true, onde: 'rosto' },
|
|
{ titulo: 'Fala pelo próprio celular', texto: 'A resposta da conversa sai no alto-falante do S10, com os olhos pulsando na voz.', feito: false, onde: 'app' },
|
|
{ titulo: 'Ritmo do dia', texto: 'Boceja à noite, fica sonolento com bateria baixa, mostra um raio quando carrega.', feito: false, onde: 'rosto e servidor' },
|
|
{ titulo: 'Contato visual', texto: 'Sustenta o olhar de quem a câmera vê e desvia quando fica tímido.', feito: false, onde: 'rosto' },
|
|
{ titulo: 'Humor que dura', texto: 'Energia e alegria sobem com conversa e carinho e caem devagar.', feito: false, onde: 'servidor e IA' },
|
|
{ titulo: 'Hora nos olhos', texto: 'Ao perguntar a hora, ela aparece dentro dos olhos por alguns segundos.', feito: false, onde: 'rosto' },
|
|
];
|
|
</script>
|
|
|
|
<div class="rosto-pagina">
|
|
<a class="voltar" href="#/ao-vivo"><ArrowLeft size={16} /> Ao vivo</a>
|
|
|
|
<header>
|
|
<span class="rotulo">Galaxy S10 · 19:9 · AMOLED</span>
|
|
<h1>Rosto do Carinha</h1>
|
|
<p>Escolha o rosto, o formato dos olhos e a cor. A prévia abaixo é o mesmo rosto que roda no celular; clique nela para cutucar.</p>
|
|
</header>
|
|
|
|
<section class="bancada" aria-label="Prévia">
|
|
<div class="celular">
|
|
<div class="tela"><RostoPrevia bind:this={previa} titulo="Prévia do rosto" /></div>
|
|
<span class="camera-frontal" aria-hidden="true"></span>
|
|
<span class="etiqueta">{nomeRosto(atual).toUpperCase()}{aceitaFormato ? ' · ' + (FORMATOS_INFO[formatoAtual] ?? formatoAtual).toUpperCase() : ''}</span>
|
|
</div>
|
|
|
|
<div class="emocoes" role="group" aria-label="Testar emoção">
|
|
{#each EMOCOES.filter((g) => GESTOS_INFO[g]) as g (g)}
|
|
<button class="tecla" onclick={() => emocao(g)}>{GESTOS_INFO[g].emoji} {GESTOS_INFO[g].nome}</button>
|
|
{/each}
|
|
</div>
|
|
<Toggle
|
|
rotulo="Mandar as emoções também para o celular"
|
|
ajuda="Desligado, elas aparecem só na prévia"
|
|
checked={noCelular}
|
|
onchange={(v) => (noCelular = v)}
|
|
/>
|
|
</section>
|
|
|
|
<section class="bloco-escolha" aria-labelledby="t-rostos">
|
|
<h2 id="t-rostos">Rosto</h2>
|
|
<div class="cartoes" role="radiogroup" aria-label="Rosto">
|
|
{#each novos as r (r)}
|
|
<button class="cartao" role="radio" aria-checked={atual === r} onclick={() => rosto(r)}>
|
|
<span class="nome">{ROSTOS_INFO[r].nome} {#if atual === r}<Check size={16} />{/if}</span>
|
|
<span class="desc">{ROSTOS_INFO[r].descricao}</span>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
{#if classicos.length}
|
|
<div class="classicos" role="radiogroup" aria-label="Rostos clássicos">
|
|
<span class="rotulo">Clássicos</span>
|
|
{#each classicos as r (r)}
|
|
<button class="tecla" role="radio" aria-checked={atual === r} onclick={() => rosto(r)}>{nomeRosto(r)}</button>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</section>
|
|
|
|
<section class="bloco-escolha" aria-labelledby="t-formato">
|
|
<h2 id="t-formato">Formato dos olhos</h2>
|
|
<div class="formatos" role="radiogroup" aria-label="Formato dos olhos">
|
|
{#each formatos as f (f)}
|
|
<button class="formato" role="radio" aria-checked={formatoAtual === f} disabled={!aceitaFormato} onclick={() => formato(f)}>
|
|
<span class="amostra {f}" aria-hidden="true"></span>
|
|
{FORMATOS_INFO[f] ?? f}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
{#if !aceitaFormato}
|
|
<p class="fraco pequeno-texto">O formato vale para Carinha Viva, Bloco e Matriz.</p>
|
|
{/if}
|
|
</section>
|
|
|
|
<section class="bloco-escolha" aria-labelledby="t-cor">
|
|
<h2 id="t-cor">Cor dos olhos</h2>
|
|
<div class="cores">
|
|
{#each CORES as c (c.nome)}
|
|
<button class="cor" style="--c: {hex(c.rgb)}" aria-label={c.nome} aria-pressed={hex(estado.aparencia.cor) === hex(c.rgb)} onclick={() => cor(c.rgb)}></button>
|
|
{/each}
|
|
<label class="cor livre" title="Outra cor">
|
|
<input type="color" value={hex(estado.aparencia.cor)} onchange={(e) => cor(rgb(e.currentTarget.value))} />
|
|
<span class="sr-only">Escolher outra cor</span>
|
|
</label>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="bloco-escolha" aria-labelledby="t-vivo">
|
|
<h2 id="t-vivo">O que deixa o Carinha vivo</h2>
|
|
<ul class="ideias">
|
|
{#each IDEIAS as i (i.titulo)}
|
|
<li>
|
|
<div class="linha entre">
|
|
<h3>{i.titulo}</h3>
|
|
<span class="status" class:feito={i.feito}>{i.feito ? 'Pronto' : 'Próximo'}</span>
|
|
</div>
|
|
<p>{i.texto}</p>
|
|
<span class="onde">{i.onde}</span>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</section>
|
|
</div>
|
|
|
|
<style>
|
|
.rosto-pagina {
|
|
display: grid;
|
|
gap: var(--e-6);
|
|
max-width: 1120px;
|
|
}
|
|
.voltar {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--e-1);
|
|
width: fit-content;
|
|
color: var(--texto-2);
|
|
text-decoration: none;
|
|
font-size: var(--t-sm);
|
|
}
|
|
header {
|
|
display: grid;
|
|
gap: var(--e-2);
|
|
}
|
|
header h1 {
|
|
font-size: clamp(1.6rem, 3.5vw, 2.3rem);
|
|
letter-spacing: -0.01em;
|
|
}
|
|
header p {
|
|
color: var(--texto-2);
|
|
max-width: 65ch;
|
|
}
|
|
.rotulo {
|
|
font: 500 11px var(--fonte-mono);
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
color: var(--texto-3);
|
|
}
|
|
.bancada {
|
|
display: grid;
|
|
gap: var(--e-4);
|
|
}
|
|
.celular {
|
|
position: relative;
|
|
aspect-ratio: 19 / 9;
|
|
max-width: 100%;
|
|
border-radius: clamp(18px, 3.4vw, 40px);
|
|
padding: clamp(6px, 0.9vw, 12px);
|
|
background: linear-gradient(145deg, #2a313b, #11161d 45%, #222932);
|
|
box-shadow: 0 30px 80px rgb(0 0 0 / 0.45), inset 0 1px 0 rgb(255 255 255 / 0.08);
|
|
}
|
|
.tela {
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: clamp(13px, 2.6vw, 30px);
|
|
overflow: hidden;
|
|
background: #000;
|
|
}
|
|
.camera-frontal {
|
|
position: absolute;
|
|
left: clamp(14px, 2.2vw, 26px);
|
|
top: 50%;
|
|
translate: 0 -50%;
|
|
width: clamp(6px, 0.8vw, 10px);
|
|
aspect-ratio: 1;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle at 35% 35%, #2b3a55, #05070a 70%);
|
|
}
|
|
.etiqueta {
|
|
position: absolute;
|
|
right: clamp(20px, 3vw, 36px);
|
|
top: clamp(14px, 2vw, 24px);
|
|
font: 500 11px var(--fonte-mono);
|
|
color: rgb(255 255 255 / 0.4);
|
|
letter-spacing: 0.08em;
|
|
pointer-events: none;
|
|
}
|
|
.emocoes,
|
|
.classicos {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--e-2);
|
|
align-items: center;
|
|
}
|
|
.tecla {
|
|
min-height: 40px;
|
|
padding: 0 var(--e-4);
|
|
border: 1px solid var(--borda);
|
|
border-radius: var(--raio-pill);
|
|
background: var(--superficie);
|
|
color: var(--texto);
|
|
font: 500 var(--t-sm) var(--fonte);
|
|
cursor: pointer;
|
|
}
|
|
.tecla:hover {
|
|
border-color: var(--destaque);
|
|
}
|
|
.tecla[aria-checked='true'] {
|
|
background: var(--destaque);
|
|
border-color: var(--destaque);
|
|
color: var(--destaque-texto);
|
|
}
|
|
.bloco-escolha {
|
|
display: grid;
|
|
gap: var(--e-3);
|
|
}
|
|
.bloco-escolha h2 {
|
|
font-size: var(--t-lg);
|
|
}
|
|
.cartoes {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
gap: var(--e-3);
|
|
}
|
|
.cartao {
|
|
display: grid;
|
|
gap: var(--e-1);
|
|
align-content: start;
|
|
padding: var(--e-4);
|
|
text-align: left;
|
|
border: 1px solid var(--borda);
|
|
border-radius: var(--raio);
|
|
background: var(--superficie);
|
|
color: var(--texto);
|
|
font: inherit;
|
|
cursor: pointer;
|
|
}
|
|
.cartao:hover {
|
|
border-color: var(--borda-forte);
|
|
}
|
|
.cartao[aria-checked='true'] {
|
|
border-color: var(--destaque);
|
|
background: var(--destaque-suave);
|
|
}
|
|
.nome {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
font-weight: 600;
|
|
}
|
|
.nome :global(svg) {
|
|
color: var(--destaque);
|
|
}
|
|
.desc {
|
|
font-size: var(--t-sm);
|
|
color: var(--texto-2);
|
|
line-height: 1.4;
|
|
}
|
|
.formatos {
|
|
display: grid;
|
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
gap: var(--e-2);
|
|
}
|
|
.formato {
|
|
display: grid;
|
|
justify-items: center;
|
|
gap: var(--e-2);
|
|
padding: var(--e-3) var(--e-2);
|
|
border: 1px solid var(--borda);
|
|
border-radius: var(--raio);
|
|
background: var(--superficie);
|
|
color: var(--texto-2);
|
|
font: 500 var(--t-sm) var(--fonte);
|
|
cursor: pointer;
|
|
}
|
|
.formato[aria-checked='true'] {
|
|
border-color: var(--destaque);
|
|
color: var(--texto);
|
|
}
|
|
.formato:disabled {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
}
|
|
/* amostras com as mesmas proporções do rosto-motor.js */
|
|
.amostra {
|
|
display: block;
|
|
width: 20px;
|
|
height: 29px;
|
|
border-radius: 10px;
|
|
background: var(--destaque);
|
|
}
|
|
.amostra.redondo {
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 50%;
|
|
}
|
|
.amostra.pilula {
|
|
width: 16px;
|
|
height: 31px;
|
|
border-radius: 8px;
|
|
}
|
|
.amostra.quadrado {
|
|
width: 26px;
|
|
height: 26px;
|
|
border-radius: 4px;
|
|
}
|
|
.amostra.largo {
|
|
width: 33px;
|
|
height: 18px;
|
|
border-radius: 6px;
|
|
}
|
|
.cores {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--e-2);
|
|
}
|
|
.cor {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 50%;
|
|
border: 2px solid var(--borda-forte);
|
|
background: var(--c);
|
|
cursor: pointer;
|
|
position: relative;
|
|
}
|
|
.cor[aria-pressed='true'] {
|
|
outline: 2px solid var(--texto);
|
|
outline-offset: 2px;
|
|
}
|
|
.livre {
|
|
background: conic-gradient(red, yellow, lime, cyan, blue, magenta, red);
|
|
}
|
|
.livre input {
|
|
position: absolute;
|
|
inset: 0;
|
|
opacity: 0;
|
|
cursor: pointer;
|
|
}
|
|
.ideias {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 1px;
|
|
background: var(--borda);
|
|
border: 1px solid var(--borda);
|
|
border-radius: var(--raio-lg);
|
|
overflow: hidden;
|
|
}
|
|
.ideias li {
|
|
display: grid;
|
|
gap: var(--e-2);
|
|
align-content: start;
|
|
padding: var(--e-4) var(--e-5);
|
|
background: var(--superficie);
|
|
}
|
|
.ideias h3 {
|
|
font-size: var(--t-md);
|
|
}
|
|
.ideias p {
|
|
font-size: var(--t-sm);
|
|
color: var(--texto-2);
|
|
}
|
|
.status {
|
|
flex: none;
|
|
font: 500 11px var(--fonte-mono);
|
|
padding: 2px 8px;
|
|
border-radius: var(--raio-pill);
|
|
border: 1px solid var(--borda-forte);
|
|
color: var(--texto-3);
|
|
}
|
|
.status.feito {
|
|
color: var(--ok);
|
|
border-color: transparent;
|
|
background: var(--ok-suave);
|
|
}
|
|
.onde {
|
|
font: 500 11px var(--fonte-mono);
|
|
color: var(--texto-3);
|
|
}
|
|
@media (max-width: 900px) {
|
|
.cartoes {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
.ideias {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
@media (max-width: 520px) {
|
|
.formatos {
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
}
|
|
}
|
|
</style>
|