Gestos, zoom y scroll Web
Funciones experimentales disponibles desde 0.1.0-rc.9, seguidas en #340–#345. Las seis capacidades comparten el runtime del documento y sus ámbitos. La entrada Web no depende de NativeScript ni de frameworks. Los vídeos nativos son referencias conservadas, no grabaciones de esta implementación Web.
Crea el runtime después del montaje. Las coordenadas usan píxeles CSS, la rotación grados y la velocidad unidades por segundo. Mantén la superficie de entrada sin transforms y anima un hijo; se conserva la frontera de coordenadas renderizadas/top-layer. La aplicación controla marcado, semántica, datos y navegación.
import { createWebRuntime } from '@rafaelaugusto/jolt/web'
const motion = createWebRuntime(document)
const scope = motion.scope()
const x = scope.motionValue(0)Arrastre inercial y valores
Activa release: "decay" para reutilizar el solver nativo: fricción, snap proyectado y recuperación en los límites. rubberband añade resistencia fuera de ellos. El comportamiento predeterminado existente sigue regresando con spring a la posición inicial o al snap. value con un eje controla un valor prestado; sin él, mueve el elemento de entrada. dragging/settling/position/animation/paused/disposed y los callbacks de inicio, movimiento, liberación, cancelación y reposo describen su ciclo de vida. Cancelar congela el movimiento propio; dispose conserva el valor prestado. La reproducción finita de elementos usa WAAPI.
const drag = scope.drag(handle, {
axis: 'x',
value: x,
release: 'decay',
bounds: { left: -200, right: 200 },
rubberband: true,
friction: 0.998,
snap: [-200, 0, 200],
onRelease: ({ animation }) => animation.finished.then(onFinished),
})
drag.pause()
drag.play()
drag.cancel()Contactos compuestos
Pan, pinch y rotación se reconocen juntos. Añadir o retirar contactos reajusta las medidas sin saltos del centroide. Los valores se observan sin renders del framework; settleTo/jumpTo/updateBounds y pause/play/cancel/dispose controlan la interacción. Pan acepta eje, threshold, failOffset y límites, incluso una función de la posición. Un tap espera cuando hay doble tap. El foco usa coordenadas CSS de la viewport. Solo se programan frames con entrada o movimiento activo.
const gesture = scope.gestures(inputSurface, {
pan: true,
pinch: { min: 0.5, max: 4 },
rotate: true,
onTap: select,
onDoubleTap: reset,
})
scope.bind(card, {
x: gesture.x,
y: gesture.y,
scale: gesture.scale,
rotate: gesture.rotate,
})
gesture.settleTo({ x: 0, y: 0, scale: 1, rotate: 0 })Panel y scroll anidado
Declara un pan limitado de un solo eje y su descendiente de scroll. Primero se expande el panel y después se desplaza el contenido; al invertir, se vacía el scroll anidado antes de colapsar. Cada delta se consume una sola vez y la liberación usa decay. El eje horizontal contempla offsets RTL. Rueda, teclado y foco conservan el scroll del navegador. El eje táctil coordinado lo controla Jolt: Pointer Events decide touch-action antes del contacto; cambiar CSS durante el gesto no devuelve el pan al navegador. Los campos mantienen sus interacciones.
const gesture = scope.gestures(inputSurface, {
pan: { axis: 'y', bounds: { top: 0, bottom: 240 } },
initial: { y: 240 },
scroll: { element: content },
})
scope.bind(panel, { y: gesture.y })Zoom y cierre de imágenes
Usa una caja de imagen centrada en su padre directo, normalmente width:100%; height:100%; object-fit:contain. Su contribución de transform es independiente. Pinch focal, pan limitado, doble tap, rueda, teclas +/−/0/flechas y Escape comparten controlador; los atajos de zoom de página siguen en el navegador. ready/error/state describen carga, geometría y estado. Backdrop/chrome opcionales usan opacidad independiente. El cierre es una intención protegida: la aplicación navega y los cancel/complete obsoletos devuelven false.
const photo = scope.zoomable(image, {
viewport,
scale: [1, 5],
doubleTap: 2.5,
backdrop,
chrome,
onDismiss(intent) {
closeRoute().then(
(closed) => (closed ? intent.complete() : intent.cancel()),
() => intent.cancel(),
)
},
})
photo.zoomTo(2, { x: 180, y: 120 })
photo.reset()Cabecera, anclas y actualización
Declara portada, imagen/títulos/sección fija opcionales y el contenedor real de scroll. Jolt controla un espaciador desmontable y contribuciones de movimiento. La altura mínima puede medirse dinámicamente. preserveAnchor conserva una fila visible por clave durante cambios síncronos o asíncronos; si desaparece, usa una posición superviviente. scrollToItem/scrollToTop/remeasure y los controles comparten fuente. Los títulos transfieren visibilidad y foco. El refresh opcional tiene una única promise, umbral, cancelación y error/reintento. commit comprueba la solicitud antes del cambio y de restaurar el ancla. Un cambio asíncrono debe comprobar cancelled antes de escribir después de un await. Cancelar resuelve inmediatamente el resultado.
const header = scope.scrollHeader(scroller, {
header: cover,
artwork,
title,
toolbarTitle,
section,
expanded: () => cover.offsetHeight,
minimum: () => toolbar.offsetHeight,
items: () => rows.map((row) => ({ key: row.id, target: row.element })),
async onRefresh(context) {
const next = await fetchRows()
await context.commit(() => replaceRows(next))
},
onRefreshError: showRetry,
})
await header.preserveAnchor(() => insertRows())
header.scrollToItem('album-6')
header.cancelRefresh()Reordenación interactiva por identidad
La aplicación aporta claves string/number estables y elementos DOM realizados, y confirma el movimiento por identidad. Jolt previsualiza vecinos de distinta altura en layers privados y usa FLIP para reconciliar el renderer. Se conservan transforms y press externos. Hay mouse, pulsación larga, scroll de borde y controles de teclado. Prefiere handles dedicados para preservar el scroll normal del contenido. Espacio/Enter inicia o confirma; flechas/Home/End mueven; Escape cancela. move/moveTo/moveUp/moveDown comparten commit. Cambios de datos, identidad, geometría y lifecycle cancelan trabajo obsoleto. El controlador no implementa renderizado de datos ni virtualización.
const reorder = scope.reorder(list, {
items: () => rows.map((row) => ({ key: row.id, target: row.element })),
handle: '[data-reorder-handle]',
onCommit: ({ key, beforeKey }) => moveItemBefore(key, beforeKey),
edge: { size: 72, speed: 640 },
})
reorder.moveDown('album-1')
reorder.move('album-1', null)
reorder.refresh()Ciclo de vida y evidencias
Los controladores admiten dispose y pausa/reanudación por ámbito. En Vue, usa useWebMotion(ref).use((scope, element) => ...) para sustitución de refs, KeepAlive y desmontaje, separando la entrada del elemento animado. Movimiento reducido conserva el destino evitando springs opcionales. El scheduler queda inactivo cuando no hay trabajo; esto no promete FPS ni sustituye la semántica de la aplicación.
Ejecuta npm run web:gestures:typecheck, npm run web:gestures:test y npm run web:gestures. Las pruebas separan reconocimiento sintético y multitouch confiable de Chromium. Consulta el informe. La adopción Mavis sigue en #234, #235, #236 y #237.