Skip to content

About

Compile procedural state transitions (do, wait, set, wait, ...) into declarative time-to-state functions (t -> do, set).

Topics

Resources

Code of conduct

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

procedural-to-declarative

npm package Build Status Downloads Issues Code Coverage Commitizen Friendly Semantic Release


📘Documentation: https://34j.github.io/procedural-to-declarative/

📦️NPM Package: https://www.npmjs.com/package/procedural-to-declarative


Compile procedural state transitions (do, wait, set, wait, ...) into declarative time-to-state functions (t -> do, set).

Installation

npm install procedural-to-declarative

Motivation

Video generation using TypeScript is a hot topic. Typically such package requires a function that maps time to state of HTML / React elements, etc.

type DeclarativeFunction<T> = (time: number) => T

However, it's often more intuitive to write state transitions in a procedural way:

const x = useRef(0)
function proc() {
  sleep(1)
  x.current += 1
  sleep(1)
  x.current += 1
}

Unfortunately, once trying to parallelize procedural functions, it turns out to be impossible, since the passed function cannot be "blocked" to sort the procedure (inner lines).

function proc() {
  const x = useRef(0)
  all([
    (() => {
      sleep(1)
      x.current += 1 // 00:01 (Unable to "block" here!)
      sleep(2)
      x.current += 2 // 00:03
    })(),
    (() => {
      sleep(2)
      x.current *= 2 // 00:02
    })(),
  ])
}

By using async/await or yield (like motion-canvas did), the function can be "blocked" and the procedure can be sorted.

async function proc() {
  const x = useRef(0)
  await all([
    (() => {
      await sleep(1) // (1)
      x.current += 1 // 00:01
      await sleep(2) // Blocked until (2) is executed
      x.current += 2 // 00:03
    })(),
    (() => {
      await sleep(2) // Blocked until (1) is executed (2)
      x.current *= 2 // 00:02
    })(),
  ])
}
function* proc() {
  const x = useRef(0)
  yield* all([
    (() => {
      yield sleep(1) // (1)
      x.current += 1 // 00:01
      yield sleep(2) // Blocked until (2) is executed
      x.current += 2 // 00:03
    })(),
    (() => {
      yield sleep(2) // Blocked until (1) is executed (2)
      x.current *= 2 // 00:02
    })(),
  ])
}

Our package uses the second approach.

Usage

Simple Usage

import { all, any, compile, createTrack, runDeclarative, runProcedural, sleep, useCompiled, useRef } from 'procedural-to-declarative'
const track = createTrack<number>()
const x = useRef(track, 0)

function* proc() {
  yield sleep(1)
  x.current = 1
  yield runDeclarative(track, (time) => {
    x.current = 1 + time
  }, 1)
  yield sleep(1)
  x.current += 1
  yield sleep(2)
}

runProcedural(track, proc())
const compiled = compile(track)

x history

Usage x history

Description

  • Track is the main data structure and tracks everything.
  • Task is the main concept of this package.
  • Ref (useRef) registers a mutable reference to the track.
  • 2 type of functions exist:
    • Procedural function (IterableIterator<Task>): Ref is read-write.
    • Declarative function ((time: number) => void): Ref is write-only.
  • compile compiles the top-level procedural function into array of TrackMaterialized, which is a fixed Track at each time point.
  • useCompiled converts TrackMaterialized into a declarative function as a final output.
  • Task has 4 types:
    • TaskConstant: returned by sleep. if yielded, it just blocks for the specified time.
    • TaskProcedural: returned by runProcedural. if yielded, it blocks until the provided procedural function is completed.
    • TaskDeclarative: returned by runDeclarative. if yielded, it blocks until the provided declarative function is completed.
    • TaskAny: returned by any. if yielded, it blocks until any of the provided tasks is completed.
  • Tasks can be suspended and resumed by setting isSuspended property to true and false.
    • If TaskProcedural is suspended, all successor Tasks invoked by the procedural function will also be suspended until the TaskProcedural is resumed.

Advanced Usage

const track = createTrack<number>()
const x = useRef(track, 0)
const y = useRef(track, 0)

function* proc() {
  const task1 = runDeclarative(track, (progress) => {
    x.current = progress
  }, 5)

  function* task2Func() {
    while (true) {
      // Unfortunately this will not work as expected because declarative function is called later (x.current is always 0 here)
      y.current += x.current
      // This will work
      y.current += 1
      yield sleep(1)
    }
  }
  const task2 = runProcedural(track, task2Func())

  yield sleep(1)
  task1.isSuspended = true
  yield sleep(1)
  task1.isSuspended = false
  yield task1
  yield sleep(1)
  task2.isSuspended = true
  yield sleep(2.5)
}

runProcedural(track, proc())
const compiled = compile(track)

x history

Advanced x history

y history

Advanced y history

Comparison

  • From our observation, none of the existing libraries support "waiting" while video / audio is playing.
  • The comparison on the way of writing "animation" using static images is as follows:
import { Circle, makeScene2D, } from '@revideo/2d'
import { all, createRef, makeProject, } from '@revideo/core'

/**
 * The Revideo scene
 */
const scene = makeScene2D('scene', function* (view) {
  const circle = createRef<Circle>()
  view.add(
    <Circle
      ref={circle}
      fill="lightseagreen"
    />
  )
  yield* all(
    circle().width(0).width(100, 1),
    circle().height(0).height(100, 2),
  )
})

/**
 * The final revideo project
 */
export default makeProject({
  scenes: [scene],
  settings: {
    // Example settings:
    shared: {
      size: { x: 100, y: 100 },
    },
  },
})
project.mp4
import { useAnimation, useVariable } from '../src/lib/animation'
import { BEZIER_SMOOTH } from '../src/lib/animation/functions'
import { seconds } from '../src/lib/frame'
import { FillFrame } from '../src/lib/layout/fill-frame'

const x = useVariable(0)
const y = useVariable(0)

function scene() {
  useAnimation(async (ctx) => {
    await ctx.parallel([
      ctx.move(x).to(100, seconds(1), BEZIER_SMOOTH),
      ctx.move(y).to(100, seconds(2), BEZIER_SMOOTH)
    ])
  })

  return (
    <FillFrame style={{ alignItems: 'center', justifyContent: 'center' }}>
      <div
        style={{
          width: x.use(),
          height: y.use(),
        }}
      />
    </FillFrame>
  )
}

About

Compile procedural state transitions (do, wait, set, wait, ...) into declarative time-to-state functions (t -> do, set).

Topics

Resources

Code of conduct

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages