Skip to content
jolt.
← Recipes

MOTION / iOS / NATIVESCRIPT VUE

A little opening spell.

A floating potion, a traced seal and a soft reveal into the app.

From Hexdrop ↗
Native recording · iOSHexdrop launch on iOS: the 3.8–7.0 second excerpt of the creator-provided recording. Floating mark, traced seal, particles, light wave and app reveal. English UI, original speed, no audio. Capture device unspecified; not a performance measurement.

The motion behind it.

Jolt 0.1.0-rc.11

The complete source file from the project, pinned to the reviewed revision. Adapt it with the dependencies described below.

NativeScript-Vue 3 and Jolt RC.11. Supply the measured, mounted native GridLayout and child IDs from LaunchScreen.vue, its styles and hexdrop-mark.png. Keep drawing surfaces at fixed bounds and transform their containers. The reveal callback mounts app content and awaits nextTick before the overlay fades. Own the returned handle, await finished and call dispose on permanent unload; preserve the reduced-motion branch and failure fallback. The system launch screen is static; this timeline starts after NativeScript is ready. iOS simulator playback only.

launch-animation.ts
import type { GridLayout, View } from '@nativescript/core'
import { motionScope, particles, reducedMotion, set } from '@rafaelaugusto/jolt'
import { shape, type Shape } from '@rafaelaugusto/jolt/shapes'

const SEAL = 'M128 10 L230 69 L230 187 L128 246 L26 187 L26 69 Z'

/** A single, pausable entrance. Native launch artwork uses the same resting pose. */
export function playLaunch(root: GridLayout, reveal: () => Promise<void>) {
  const motion = motionScope()
  const drawings: Shape[] = []
  const view = (id: string) => root.getViewById<View>(id)
  const mark = view('launch-mark')
  const halo = view('launch-halo')
  const seal = view('launch-seal')
  const flash = view('launch-flash')
  const title = view('launch-title')
  const caption = view('launch-caption')

  const finished = (async () => {
    if (reducedMotion()) {
      set([title, caption], { opacity: 1 })
      if (!await motion.animate(root, { opacity: [1, 1] }, { duration: 100 }).finished) return false
    } else {
      // Transform containers, keeping the native drawing surfaces at fixed bounds.
      motion.gradient(view('launch-halo-paint'), {
        type: 'radial', radius: 0.5,
        colors: ['rgba(131,238,61,0.28)', 'rgba(91,179,32,0.09)', 'rgba(10,9,14,0)'],
        stops: [0, 0.4, 1],
      })
      motion.gradient(view('launch-flash-paint'), {
        type: 'radial', radius: 0.5,
        colors: ['rgba(207,255,154,0)', 'rgba(172,255,92,0.20)', 'rgba(10,9,14,0)'],
        stops: [0, 0.65, 1],
      })
      const outline = shape(view('launch-seal-paint'), {
        viewBox: [0, 0, 256, 256], d: SEAL,
        stroke: '#8ebc63', strokeWidth: 0.8, strokeEnd: 0,
        lineJoin: 'round',
      })
      drawings.push(outline)
      motion.use(() => particles(view('launch-motes'), {
        colors: ['#d9ffb1', '#b5f564', '#628941'], source: 'center',
        rate: 14, duration: 800, lifetime: 1100, size: 2,
        velocity: 58, velocityRange: 18, spread: 360, gravity: -7,
      }))

      const entrance = motion.timeline([
        [mark, { scale: [1, 1.08, 1.03, 1], y: [0, -10, -4, 0], rotate: [0, -5, 2, 0] },
          { at: 0, duration: 1550, times: [0, 0.35, 0.7, 1], ease: 'easeInOut' }],
        [halo, { opacity: [0, 0.9, 0.5], scale: [0.7, 1.15, 1] },
          { at: 0, duration: 1550, ease: 'easeInOut' }],
        [seal, { opacity: [0, 0.6, 0.6, 0], rotate: [-16, 0, 18], scale: [0.92, 1, 1.1] },
          { at: 0, duration: 1500, ease: 'easeInOut' }],
        { at: 80, run: () => outline.animate({ strokeEnd: 1 }, { duration: 900, ease: 'easeInOut' }) },
        [title, { opacity: [0, 1], y: [14, 0] }, { at: 300, duration: 540, ease: 'easeOut' }],
        [caption, { opacity: [0, 1], y: [8, 0] }, { at: 520, duration: 440, ease: 'easeOut' }],
        [flash, { opacity: [0, 1, 0], scale: [0.55, 1.2, 1.8] },
          { at: 820, duration: 650, ease: 'easeOut' }],
      ])
      if (!await entrance.finished) return false
    }

    if (motion.disposed) return false
    await reveal()
    if (motion.disposed) return false
    return motion.animate(root, { opacity: 0 }, {
      duration: reducedMotion() ? 150 : 320, ease: 'easeInOut',
    }).finished
  })()

  return {
    finished,
    dispose() {
      motion.dispose()
      drawings.forEach(drawing => drawing.dispose())
    },
  }
}
View full demo ↗

Read the full guide ↗