White screen on Preview

Hey guys,

Can anyone help me with some component code that I’m working on to animate some hand drawn clouds. The clouds are meant to slide back and forth with a few sub elements rotating and bobbing while they do.

I keep getting a white screen and the following error when I try to preview:

Unhandled promise rejection: Error: Unknown type: [object Object]
at (anonymous) runtime.js
at Vd runtime.js
at (anonymous) runtime.js
at jd runtime.js
at (anonymous) runtime.js
at (anonymous) runtime.js
at (anonymous) runtime.js
at (anonymous) runtime.js

Here’s the component code:

// cloud-animator.ts
import * as ecs from '@8thwall/ecs'

// Studio injects all of the component APIs onto `ecs.components` at runtime.
// Locally we cast it to any so webpack only has to resolve "@8thwall/ecs".
const {
  Position,
  Hierarchy,
  Name,
  Visible,
  RotateAnimation,
} = (ecs as any).components

/** Per-entity animation state */
type CloudAnimState = {
  baseX: number
  baseY: number
  baseZ: number
  rotating: bigint[]       // IDs of children that rotate
  slides: bigint[]         // IDs of slide-frame children
  topChild: bigint | null  // ID of the child bobbing up/down
  time: number             // accumulated seconds
  frameTime: number        // seconds since last slide swap
  currentSlide: number     // index into `slides`
}

ecs.registerComponent<CloudAnimState>({
  name: 'cloud-animator',

  // β€”β€”β€” THIS SCHEMA MUST BE A LITERAL OBJECT β€”β€”β€”
  schema: {
    rotSpeed: ecs.f32,       // deg/sec
    bobHeight: ecs.f32,      // world units up/down
    bobDuration: ecs.f32,    // secs per bob cycle
    slideDistance: ecs.f32,  // world units back-and-forth
    slideDuration: ecs.f32,  // secs per full slide cycle
    slideInterval: ecs.f32,  // secs between frame swaps
    reverse: ecs.boolean,    // invert slide direction?
  },

  // β€”β€”β€” AND THESE DEFAULTS MUST ALSO BE A LITERAL OBJECT β€”β€”β€”
  schemaDefaults: {
    rotSpeed: 30,
    bobHeight: 0.2,
    bobDuration: 2,
    slideDistance: 1.5,
    slideDuration: 4,
    slideInterval: 0.15,
    reverse: false,
  },

  // one hidden slot for runtime state
  data: {
    _state: {} as CloudAnimState,
  },

  // Called once, when component is added to an entity
  add: (world, component) => {
    const eid = Number(component.eid)  // cast once
    const cfg = component.schema
    // record starting transform
    const {x, y, z} = Position.get(world, eid)
    component.data._state = {
      baseX: x,
      baseY: y,
      baseZ: z,
      rotating: [],
      slides: [],
      topChild: null,
      time: 0,
      frameTime: 0,
      currentSlide: 0,
    }

    // bucket children by name
    const hier = Hierarchy.get(world, eid)
    const kids = Array.isArray(hier.children) ? hier.children : []
    for (const kid of kids) {
      const nm = Name.get(world, kid).value.toLowerCase()
      if (nm.includes('top')) {
        component.data._state.topChild = kid
      } else if (nm.includes('rot')) {
        component.data._state.rotating.push(kid)
      } else if (nm.includes('slide') || nm.includes('frame')) {
        component.data._state.slides.push(kid)
      }
    }

    // hide all but the first slide
    component.data._state.slides.forEach((cid, i) => {
      Visible.getMutable(world, cid).value = i === 0
    })
  },

  // Runs every frame
  tick: (world, component, dt) => {
    const eid = Number(component.eid)
    const cfg = component.schema
    const s = component.data._state

    // dt is a bigint in ms β†’ seconds
    const sec = Number(dt) * 0.001
    s.time += sec
    s.frameTime += sec

    // β€” SLIDE BACK & FORTH β€”
    const alpha = Math.sin((s.time / cfg.slideDuration) * Math.PI * 2) * 0.5 + 0.5
    const dir = cfg.reverse ? -1 : 1
    const nx = s.baseX + (alpha - 0.5) * cfg.slideDistance * dir
    Position.set(world, eid, {x: nx, y: s.baseY, z: s.baseZ})

    // β€” BOB THE TOP CHILD β€”
    if (s.topChild !== null) {
      const bob = Math.sin((s.time / cfg.bobDuration) * Math.PI * 2) * cfg.bobHeight
      Position.set(world, s.topChild, {
        x: s.baseX,
        y: s.baseY + bob,
        z: s.baseZ,
      })
    }

    // β€” ROTATE CHILDREN β€”
    for (const cid of s.rotating) {
      RotateAnimation.set(world, cid, {
        loop: true,
        fromX: 0,
        fromY: 0,
        fromZ: 0,
        toX: 0,
        toY: 360,
        toZ: 0,
        shortestPath: false,
        duration: 1000 / cfg.rotSpeed,  // ms per deg/sec
      })
    }

    // β€” CYCLE SLIDES β€”
    if (s.slides.length > 0 && s.frameTime >= cfg.slideInterval) {
      Visible.getMutable(world, s.slides[s.currentSlide]).value = false
      s.currentSlide = (s.currentSlide + 1) % s.slides.length
      Visible.getMutable(world, s.slides[s.currentSlide]).value = true
      s.frameTime = 0
    }
  },

  // Called when component is removed (no-op here)
  remove: () => {},
})