Ir al contenido
jolt.
← Recetas

TOQUE Y GESTOS / iOS / NATIVESCRIPT VUE

Suelta un poco de magia.

Una tarjeta que se inclina, vuelve con un resorte o desaparece en una poción.

Del proyecto Hexdrop ↗
Grabación nativa · iOSHexdrop · simulador iPhone 17 Pro, iOS 26.2. Arrastres reales mediante idb: retorno con resorte, drop aceptado, ignición, llenado y balanceo del tapón. Interfaz en inglés, velocidad original, captura sin audio. Demo local; no se sube ningún archivo. No mide rendimiento en dispositivo físico.

El movimiento detrás.

Jolt 0.1.0-rc.11

El archivo completo del proyecto, fijado a la revisión verificada. Adáptalo con las dependencias descritas abajo.

NativeScript-Vue 3 y Jolt RC.11. Llama a useIngredientDrag durante setup; conecta loaded y tap a la tarjeta nativa. Proporciona disabled, límites del destino en coordenadas de ventana, onStart, onHover y onDrop. Copia transformedRect, intersects y Rect del potion/geometry.ts fijado. Separa el gesto del scope de feedback y conserva limpieza y protecciones de segundo plano. El hook no dibuja la poción ni sube archivos; la escena está en potion-view.ts. Solo se verificó la ejecución en simulador iOS.

useIngredientDrag.ts
import { onBeforeUnmount, ref, watch, type Ref } from 'nativescript-vue'
import { Application, type EventData, type View } from '@nativescript/core'
import { drag, motionScope, set, type Drag, type DragPoint, type DragRelease } from '@rafaelaugusto/jolt'
import { intersects, transformedRect, type Rect } from '../potion/geometry'

interface IngredientDragOptions {
  disabled: Ref<boolean>
  target: () => Rect | undefined
  onStart: () => void
  onHover: (active: boolean) => void
  onDrop: () => void
}

const REST = { x: 0, y: 0, rotate: 0, scale: 1, opacity: 1 }
const DRAG_SCALE = 0.86
const tilt = (x: number) => Math.max(-8, Math.min(8, x / 16))

/** Jolt owns tracking and snap physics; this adapter decides whether a drop lands. */
export function useIngredientDrag(options: IngredientDragOptions) {
  const dragging = ref(false)
  const motion = motionScope()
  let card: View | undefined
  let controller: Drag | undefined
  let origin: Rect | undefined
  let generation = 0
  let ignoreTapUntil = 0
  let hovering = false

  function setHover(active: boolean) {
    if (hovering === active) return
    hovering = active
    options.onHover(active)
  }

  function hitsTarget(point: DragPoint) {
    const target = options.target()
    return !!origin && !!target && intersects(
      transformedRect(origin, point, DRAG_SCALE, tilt(point.x)), target,
    )
  }

  function restore() {
    generation++
    motion.reset()
    origin = undefined
    dragging.value = false
    ignoreTapUntil = Date.now() + 500
    setHover(false)
    if (card) set(card, REST)
  }

  function start(point: DragPoint) {
    if (!card || options.disabled.value || Application.suspended) {
      controller?.cancel()
      return
    }
    generation++
    motion.reset()
    options.onStart()
    // Measure the layout origin, even when catching a card during its return.
    set(card, { scale: 1, rotate: 0 })
    const location = card.getLocationInWindow()
    origin = { x: location.x - point.x, y: location.y - point.y, ...card.getActualSize() }
    dragging.value = true
    ignoreTapUntil = Date.now() + 500
    set(card, { scale: DRAG_SCALE })
  }

  async function release(event: DragRelease) {
    if (!card || !origin || options.disabled.value || Application.suspended) {
      controller?.cancel()
      restore()
      return
    }
    const id = generation
    // Check the final release position: it need not have produced an onMove.
    const accepted = hitsTarget(event)
    setHover(false)
    ignoreTapUntil = Date.now() + 500
    if (accepted) {
      controller?.pause()
      const finished = await motion.animate(card, { scale: 0.08, opacity: 0 }, { duration: 200, ease: 'easeIn' }).finished
      if (id !== generation) return
      restore()
      if (finished && !Application.suspended && !options.disabled.value) options.onDrop()
      if (!options.disabled.value) controller?.play()
    } else {
      // snap: [0] returns both axes using Jolt's release velocity and native spring.
      const appearance = motion.animate(card, { rotate: 0, scale: 1 }, 'bouncy')
      await Promise.all([event.animation.finished, appearance.finished])
      if (id === generation) restore()
    }
  }

  function loaded(event: EventData) {
    const view = event.object as View
    if (view === card && controller) return
    controller?.dispose()
    restore()
    card = view
    // Keep the gesture separate from the visual scope: resetting feedback must
    // not detach the recognizer. The controller handles unload/background itself.
    controller = drag(view, {
      snap: [0],
      onStart: start,
      onMove: point => {
        if (!origin) return
        set(view, { rotate: tilt(point.x) })
        setHover(hitsTarget(point))
      },
      onRelease: event => { void release(event) },
      onCancel: restore,
    })
    if (options.disabled.value) controller.pause()
  }

  function tap() {
    if (Application.suspended || options.disabled.value || dragging.value || Date.now() < ignoreTapUntil) return
    options.onDrop()
  }

  watch(options.disabled, disabled => {
    if (disabled) { controller?.pause(); restore() }
    else controller?.play()
  }, { flush: 'sync' })

  // A release has no active contact for Jolt to cancel. Invalidate its pending
  // absorption callback too, so backgrounding cannot start an invisible brew.
  Application.on(Application.suspendEvent, restore)
  const resume = () => { if (!options.disabled.value) controller?.play() }
  Application.on(Application.resumeEvent, resume)
  onBeforeUnmount(() => {
    controller?.dispose()
    restore()
    motion.dispose()
    Application.off(Application.suspendEvent, restore)
    Application.off(Application.resumeEvent, resume)
  })

  return { dragging, loaded, tap }
}
Ver demo completa ↗

Lee la guía completa ↗