Module

Hylograph.Scene.Engine

Package
purescript-hylograph-simulation
Repository
afcondon/purescript-hylograph-simulation

Scene Engine

Tick-based scene orchestration with adapter pattern.

The engine manages transitions between scenes using a three-phase lifecycle:

  1. Initialize: Apply init rules to prepare starting state
  2. Transition: Interpolate positions from start to target
  3. Finalize: Apply final rules and enter stable mode

The engine is generic over the node type. Apps provide an adapter record with functions that interact with their specific simulation.

Example usage:

-- Create adapter for your simulation
adapter = mkAdapter mySimulation

-- Create engine
engineRef <- createEngine adapter

-- Transition to a scene
transitionTo treeFormScene engineRef

-- Call tick from your simulation's tick handler
Sim.onTick (\_ -> tick engineRef) mySimulation

#SceneEngine Source

type SceneEngine node = Ref (EngineState node)

The scene engine, stored in a Ref for mutation across tick callbacks

#EngineState Source

type EngineState node = { adapter :: EngineAdapter node, currentScene :: Maybe (SceneConfig node), transition :: Maybe (TransitionState node), transitionDelta :: TickDelta }

Internal engine state

#EngineAdapter Source

type EngineAdapter node = { applyRulesInPlace :: Array (NodeRule node) -> Effect Unit, capturePositions :: Array node -> PositionMap, getNodes :: Effect (Array node), interpolatePositions :: PositionMap -> PositionMap -> Number -> Effect Unit, reheat :: Effect Unit, reinitializeForces :: Effect Unit, updatePositions :: PositionMap -> Effect Unit }

Adapter functions that connect the engine to your simulation.

The engine is generic - it doesn't know about Sim.Simulation directly. Instead, you provide these adapter functions that do the actual work.

Example adapter for ce-website:

mkAdapter :: CESimulation -> EngineAdapter SimNode
mkAdapter sim =
  { getNodes: Sim.getNodes sim
  , capturePositions: \nodes -> Object.fromFoldable $
      map (\n -> Tuple (show n.id) { x: n.x, y: n.y }) nodes
  , interpolatePositions: \start target progress ->
      Sim.interpolatePositionsInPlace start target progress sim
  , updatePositions: \positions ->
      Sim.updatePositionsInPlace positions sim
  , applyRulesInPlace: \rules ->
      applyRulesInPlace_ rules sim.nodes
  , reinitializeForces: reinitForcesFor sim
  , reheat: Sim.reheat sim
  }

#createEngine Source

createEngine :: forall node. EngineAdapter node -> Effect (SceneEngine node)

Create a new scene engine.

The engine starts with no active scene. Call transitionTo to begin your first scene transition.

Uses default transition duration of ~2 seconds.

#transitionTo Source

transitionTo :: forall node. SceneConfig node -> SceneEngine node -> Effect Unit

Start a transition to a new scene.

If a transition is already in progress, this is ignored.

The transition lifecycle:

  1. Apply scene's initRules to prepare starting state
  2. Capture current positions as start positions
  3. Calculate target positions via scene's layout function
  4. Begin interpolation (handled by tick)

#tick Source

tick :: forall node. SceneEngine node -> Effect Boolean

Advance the engine by one tick.

Call this from your simulation's tick handler.

Returns true if a transition is in progress, false if stable.

#getCurrentScene Source

getCurrentScene :: forall node. SceneEngine node -> Effect (Maybe (SceneConfig node))

Get the current scene configuration (if any).

#isTransitioning Source

isTransitioning :: forall node. SceneEngine node -> Effect Boolean

Check if a transition is in progress.

#getTransitionProgress Source

getTransitionProgress :: forall node. SceneEngine node -> Effect (Maybe Number)

Get the current transition progress (0.0 to 1.0).

Returns Nothing if no transition is in progress.

#setCurrentScene Source

setCurrentScene :: forall node. SceneConfig node -> SceneEngine node -> Effect Unit

Set the current scene directly (without transition).

Use this when the visualization is already in the target state (e.g., after initial rendering with pre-computed positions).

#defaultTransitionDelta Source

defaultTransitionDelta :: TickDelta

Default transition delta: ~2 seconds at 60fps

Re-exports from Hylograph.Scene.Types

#TransitionState Source

type TransitionState node = { progress :: Progress, startPositions :: PositionMap, targetPositions :: PositionMap, targetScene :: SceneConfig node }

Runtime state during an active transition.

Tracks the target scene, start/end positions, and current progress. The interpolation engine updates progress each tick until complete. Uses tick-based progress (0.0 to 1.0) rather than time-based elapsed.

#SceneConfig Source

type SceneConfig node = { finalRules :: Array node -> Array (NodeRule node), initRules :: Array (NodeRule node), layout :: Array node -> PositionMap, name :: String, stableMode :: EngineMode }

Scene configuration with three-phase lifecycle.

Phase 1: Initialize (initRules) Applied before transition starts. Use this to set up starting positions, e.g., moving tree nodes to the root for a "grow from root" animation.

Phase 2: Transition (layout) The interpolation engine smoothly moves nodes from their current positions to the target positions computed by the layout function.

Phase 3: Finalize (finalRules) Applied after transition completes. Use this to set up the stable state, e.g., unpinning nodes so forces can take over, or setting gridX/gridY.

Example:

treeFormScene :: SceneConfig MyNode
treeFormScene =
  { name: "TreeForm"
  , initRules: [ moveToRootRule ]
  , layout: \nodes -> computeTreePositions nodes
  , finalRules: \_ -> [ pinAtTreePositionsRule ]
  , stableMode: Static
  }

#PositionMap Source

type PositionMap = Object Position

Position map: node ID (as string) -> position Used for capturing current positions and specifying targets

#NodeRule Source

type NodeRule node = { apply :: node -> node, name :: String, select :: node -> Boolean }

A rule that selects nodes and applies a transform.

Rules are applied with first-match-wins semantics (like CSS cascade). If multiple rules match a node, only the first one applies.

Example:

pinPackages :: NodeRule MyNode
pinPackages =
  { name: "pinPackages"
  , select: \n -> n.nodeType == Package
  , apply: \n -> n { fx = notNull n.x, fy = notNull n.y }
  }

#EngineMode Source

data EngineMode

Engine mode determines what happens after a transition completes.

  • Physics: D3 force simulation runs, nodes settle via forces
  • Static: Nodes stay pinned at their final positions

Constructors

Instances