Gravidade, haptics, sensores, glass e galeria na Web
Auditoria da RC.10
A RC.10 publicada inclui os PRs #358, #365, #372 e #379. A matriz completa de exports e a auditoria de contratos distinguem disponibilidade de paridade funcional. npm run parity:check verifica todos os exports de execução e as propriedades de keyframes. Ainda há diferenças em movimento por caminho, rotação 3D, sombras/profundidade, replay/inspector e validação de hardware em navegadores móveis reais. As gravações desktop abaixo não comprovam esse aceite físico.
Disponível desde a 0.1.0-rc.10. Este lote cobre #373–#378 dentro da #236 e avança a parte Web da #117. /web pode ser importado durante SSR; crie o runtime e seus alvos montados no cliente. Os recursos criados pelo escopo acompanham sua suspensão, retomada e descarte.
import { createWebRuntime } from '@rafaelaugusto/jolt/web'
const motion = createWebRuntime(document)
const scope = motion.scope()
// scope.suspend(); scope.resume(); scope.dispose(); motion.dispose()Gravidade finita
animate() aceita type: 'gravity' em x, y e valores numéricos de movimento. Usa o mesmo compilador balístico finito do nativo: aceleração positiva em direção ao destino, restituição em [0, 1), velocidade explícita ou herdada com sinal, um destino, atrasos/repetições e camadas independentes. Pausa, retomada, busca, interrupção, movimento reduzido e validação transacional seguem as regras existentes. Uma animação pausada transmite velocidade zero. Este recurso é separado de physicsWorld().
const fall = scope.animate(
card,
{ y: 280 },
{
type: 'gravity',
acceleration: 1800,
restitution: 0.6,
},
)
fall.pause()
fall.progress = 0.3
fall.play()
const height = scope.motionValue(0)
scope.animate(height, 280, { type: 'gravity', velocity: -600 })Haptics opcionais
haptic() retorna backend: 'vibration' | 'none' e status: 'requested' | 'unavailable' | 'blocked' | 'disabled'. requested confirma a solicitação ao navegador, não a vibração física. A Vibration API oferece durações curtas de 10/20/30 ms, sem equivalência com as intensidades nativas light/medium/heavy. É necessário um documento visível e ativação prévia do usuário; movimento reduzido silencia as solicitações. Não há vibração forçada como alternativa. O feedback de press dispara uma vez por contato ou tecla aceitos; controles desabilitados/em carregamento não disparam. Na animação, o padrão é a chegada; times crescentes em (0, 1] excluem o atraso inicial. Pausar reagenda os pontos; buscar consome os pontos ultrapassados, interromper os cancela e atrasos maiores que 100 ms são descartados.
const result = motion.haptic('light')
scope.press(button, { scale: 0.96 }, { haptic: 'light' })
scope.animate(
card,
{ x: 240 },
{
duration: 800,
haptic: { style: 'medium', times: [0.5, 1] },
},
)Sensores de rotação e gravidade
sensor('rotation') fornece XYZ relativos em radianos, quaternion Hamilton normalizado (xyzw) e recenter(). sensor('gravity') fornece aceleração gravitacional para baixo em m/s²; um aparelho deitado com a tela para cima tem Z negativo. A gravidade é a aceleração linear menos a aceleração com gravidade; se uma leitura faltar, o Jolt não usa a aceleração total como substituta. As coordenadas seguem a interface por padrão; device conserva os eixos do aparelho. A Device Orientation and Motion API exige contexto seguro e pode exigir permissão. Chame requestPermission() diretamente na interação do usuário; criação e retomada nunca abrem uma solicitação. A presença da API não comprova hardware: available fica verdadeiro após uma amostra válida; dois segundos sem dados úteis produzem unavailable / no-data. Também existem permission-required, denied, idle, starting, active, paused e disposed. Os timestamps são dos eventos monotônicos do navegador, sem promessa de timestamp do hardware. interval: 'auto' | 5–1000 limita a entrega, não configura a frequência física. Bindings e assinaturas iniciam a aquisição, compartilhada por evento/documento. Remover o último consumidor, suspender o escopo, ir ao fundo, reduzir movimento ou descartar interrompe a aquisição. A rotação preserva a pose relativa na retomada. Use reducedMotion: 'continue' apenas para entrada intencional não decorativa.
const rotation = scope.sensor('rotation')
const gravity = scope.sensor('gravity', { coordinates: 'interface' })
scope.bind(card, {
rotate: scope.map(rotation.z, [-Math.PI, Math.PI], [-180, 180]),
x: scope.map(gravity.x, [-10, 10], [-80, 80]),
})
enableButton.onclick = () => {
void rotation.requestPermission()
void gravity.requestPermission()
}
rotation.recenter()
console.log(gravity.snapshot())Glass controlado
glass() fornece set, animate, stop, suspend, resume, dispose e snapshot. O backend real é css-backdrop-filter ou solid. Falta de suporte, transparência reduzida e cores forçadas usam fundo sólido; não há promessa de refração ou fusão do UIKit Liquid Glass. O filtro atua no fundo e mantém os controles filhos interativos. Intensidade em [0, 1], raio de 1–64 pixels CSS. O descarte restaura apenas estilos ainda pertencentes ao efeito e preserva alterações da aplicação. Evite dois donos de blur/glass no mesmo canal de fundo.
const surface = scope.glass(panel, {
amount: 0.7,
radius: 24,
tint: 'rgba(30, 38, 58, 0.35)',
fallbackColor: '#202638',
})
scope.add(surface.animate(1, { duration: 400 }))
console.log(surface.snapshot().backend)Retorno de rota por gesto
interactiveNavigation() usa o motor de gestos existente e uma camada própria de translação. Por padrão, o gesto horizontal começa nos primeiros 32 pixels CSS da borda; o vertical usa toda a superfície. Posição e velocidade ao soltar escolhem cancelar ou sair. Uma mola parte da pose e velocidade atuais; após a saída, commit chama o roteador uma vez. Retornar false, rejeitar ou atingir o timeout traz a superfície de volta. cancel() reverte a saída em andamento; back() atende botão e teclado. O roteador precisa respeitar o sinal de cancelamento e resolver após confirmar a rota. O Jolt não desfaz uma rota já alterada, não intercepta o histórico, não implementa o retorno preditivo do sistema e não administra o foco da rota automaticamente. O descarte remove a contribuição visual e os listeners próprios.
const backGesture = scope.interactiveNavigation(page, {
axis: 'x',
edgeWidth: 32,
canBegin: () => !editorIsBusy(),
commit: ({ animated, signal }) => routerBack({ animated, signal }),
})
backButton.onclick = () => backGesture.back()
// backGesture.cancel()Galeria e retorno à miniatura por chave
sharedNavigationEntry() coordena uma entrada e o retorno, com as mesmas restrições de snapshots do shared-layout. returnFrom(intent) captura a pose de zoom/arrasto antes da troca de rota e transmite a velocidade vertical ao soltar. Um retorno rejeitado chama intent.cancel() e mantém o visualizador utilizável. Pedidos repetidos compartilham a operação. resolveListTarget() relê o índice atual, pede ao virtualizador que materialize o item, confere a chave e exige um retângulo estável em dois frames de layout. Não guarda células ou índices antigos. O resolver recebe sinal de cancelamento e tem prazo padrão de 1500 ms, máximo de 5000 ms. Item excluído, ausente ou expirado conclui a rota sem voo compartilhado e usa fallbackFocus; torne esse destino focável. A miniatura atual direciona o foco ao botão/link que a contém. Os callbacks de rota pertencem à aplicação e devem respeitar cancelamento; cancelar a animação não desfaz navegação confirmada. Materialização e decodificação da imagem ficam no callback da aplicação. Os vídeos mostram botão de retorno, zoom focal, arrasto cancelado/concluído, reciclagem e exclusão. Isso avança a #117 sem declarar concluída sua validação nativa restante de acessibilidade/aparelhos.
import { resolveListTarget } from '@rafaelaugusto/jolt/web'
const entry = scope.sharedNavigationEntry(region, {
tag: photoKey,
source: () => thumbnailForKey(photoKey),
detail: () => detailImage,
back: ({ animated, signal }) => showCollection({ animated, signal }),
returnTarget: {
key: photoKey,
resolve: (context) =>
resolveListTarget(context, {
container: list,
indexOf: (key) => photos.findIndex((photo) => photo.id === key),
scrollToIndex: (index, { signal }) => realizeRow(index, signal),
viewForKey: thumbnailForKey,
keyOf: (element) => element.dataset.key,
}),
fallbackFocus: () => collectionHeading,
},
})
await entry.open(({ animated, signal }) => showPhoto({ animated, signal }))
scope.zoomable(detailImage, {
viewport,
onDismiss: (intent) => {
void entry.returnFrom(intent)
},
})
backButton.onclick = () => {
void entry.back()
}Validação e reprodução
Execute npm run web:parity:typecheck, npm run web:parity:test, npm run build, npm run core:check, npm run package:check, python3 benchmarks/verify-sensor-math.py --web, npm run docs:check-media e npm run docs:build. A suíte cobre Chromium, Firefox e WebKit. Entradas injetadas de permissão/sensor/vibração são fixtures de teste, sem comprovação de hardware físico. Reproduza os vídeos reais do navegador com node benchmarks/web-parity/record.mjs; a captura nativa segue CONTRIBUTING.md. A adoção no Mavis permanece em uma frente separada.