Skip to content

Commit

Permalink
fix: update DisplayObject instances (MovieClip, Text, etc) recursively
Browse files Browse the repository at this point in the history
  • Loading branch information
indr committed Jan 24, 2019
1 parent c16292d commit d16782e
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 41 deletions.
53 changes: 41 additions & 12 deletions src/Adapter.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const Adapter = class {
constructor (webcg, movieClip) {
constructor (webcg, movieClip, createjs) {
if (!webcg || typeof webcg !== 'object') throw new TypeError('webcg must be an object')
if (!movieClip || typeof movieClip !== 'object') throw new TypeError('movieClip must be an object')
if (!createjs || typeof createjs !== 'object') throw new TypeError('createjs must be an object')

this.movieClip = movieClip
this.createjs = createjs

// Immediately call stop since CasparCG will invoke play
// to start the template
Expand Down Expand Up @@ -40,7 +42,7 @@ const Adapter = class {
}

data (data) {
this._updateMovieClipInstances(data)
this._updateInstances(data)
}

_findLabel (label) {
Expand All @@ -51,20 +53,47 @@ const Adapter = class {
return null
}

_updateMovieClipInstances (data) {
const instance = this.movieClip.instance
_updateInstances (data) {
const instances = this._getDisplayObjectInstances(this.movieClip)
Object.keys(data).forEach(componentId => {
if (!instance.hasOwnProperty(componentId)) return
// Skip if there are not instanes with current id
if ((instances[componentId] || []).length <= 0) return

if (typeof data[componentId] === 'object') {
Object.keys(data[componentId]).forEach(dataKey => {
if (!instance[componentId].hasOwnProperty(dataKey)) return
instance[componentId][dataKey] = data[componentId][dataKey]
// If the current value is a string, update given text property
if (typeof data[componentId] === 'string' || typeof data[componentId] === 'number') {
instances[componentId].forEach(instance => {
this._updateInstanceProps(instance, { text: data[componentId] })
})
} else if (typeof data[componentId] === 'string') {
if (!instance[componentId].hasOwnProperty('text')) return
instance[componentId]['text'] = data[componentId]
} else if (typeof data[componentId] === 'object') {
instances[componentId].forEach(instance => {
this._updateInstanceProps(instance, data[componentId])
})
}
})
}

_getDisplayObjectInstances (instance, result) {
return Object.keys(instance).reduce((map, curr) => {
// Ignore parent property to prevent infinite recursion
if (curr === 'parent') return map
// Ignore inherited properties
if (!instance.hasOwnProperty(curr)) return map

if (instance[curr] instanceof this.createjs.DisplayObject) {
// Add instance to the result map
map[curr] = map[curr] || []
map[curr].push(instance[curr])
// Recurse over the properties
return this._getDisplayObjectInstances(instance[curr], map)
}
return map
}, result || {})
}

_updateInstanceProps (instance, props) {
Object.keys(props).forEach(key => {
if (!instance.hasOwnProperty(key)) return
instance[key] = props[key]
})
}
}
Expand Down
6 changes: 5 additions & 1 deletion src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ const init = function (window) {
console.warn('[webcg-adobe-animate-adapter] expected window.AdobeAn to be an object')
return
}
if (typeof window.createjs !== 'object') {
console.warn('[webcg-adobe-animate-adapter] expected window.createjs to be an object')
return
}
window.AdobeAn.bootstrapCallback(() => {
/* eslint-disable no-new */
new Adapter(window.webcg, window.exportRoot)
new Adapter(window.webcg, window.exportRoot, window.createjs)
})
}

Expand Down
87 changes: 59 additions & 28 deletions test/Adapter.spec.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import Adapter from '../src/Adapter'
import './createjs'

describe('Adapter', () => {
let webcg, movieClip, labels
let createjs, webcg, movieClip, labels

beforeEach(() => {
createjs = window.createjs
webcg = { addEventListener: sinon.spy() }
movieClip = {}
movieClip = new createjs.MovieClip()
labels = []
movieClip.getLabels = () => labels
movieClip.gotoAndPlay = sinon.spy()
movieClip.instance = {}
movieClip.stop = sinon.spy()
movieClip.play = sinon.spy()
movieClip.visible = null
})

it('should throw TypeError when webcg is null or not an object', () => {
expect(() => new Adapter(null, movieClip))
expect(() => new Adapter(null, movieClip, createjs))
.to.throw(TypeError, 'webcg must be an object')
expect(() => new Adapter('', movieClip))
expect(() => new Adapter('', movieClip, createjs))
.to.throw(TypeError, 'webcg must be an object')
})

Expand All @@ -29,14 +30,21 @@ describe('Adapter', () => {
.to.throw(TypeError, 'movieClip must be an object')
})

it('should throw TypeError when createjs is null or not an object', () => {
expect(() => new Adapter(webcg, movieClip, null))
.to.throw(TypeError, 'createjs must be an object')
expect(() => new Adapter(webcg, movieClip, ''))
.to.throw(TypeError, 'createjs must be an object')
})

it('should be able to instantiate an adapter instance', () => {
const adapter = new Adapter(webcg, movieClip)
const adapter = new Adapter(webcg, movieClip, createjs)
expect(adapter).to.be.instanceOf(Adapter)
})

it('should add webcg event listeners', () => {
// eslint-disable-next-line no-new
new Adapter(webcg, movieClip)
new Adapter(webcg, movieClip, createjs)
assert(webcg.addEventListener.calledWith('play'))
assert(webcg.addEventListener.calledWith('stop'))
assert(webcg.addEventListener.calledWith('next'))
Expand All @@ -45,72 +53,95 @@ describe('Adapter', () => {

it('should stop immediately', () => {
// eslint-disable-next-line no-new
new Adapter(webcg, movieClip)
new Adapter(webcg, movieClip, createjs)
expect(movieClip.stop.calledOnce).to.equal(true)
})

it('play should set visible true', () => {
const adapter = new Adapter(webcg, movieClip)
const adapter = new Adapter(webcg, movieClip, createjs)
expect(movieClip.visible).to.not.equal(true)
adapter.play()
expect(movieClip.visible).to.equal(true)
})

it('play should goto and play intro', () => {
labels.push({ label: 'intro', position: 1 })
const adapter = new Adapter(webcg, movieClip)
const adapter = new Adapter(webcg, movieClip, createjs)
adapter.play()
assert(movieClip.gotoAndPlay.calledWith(1))
})

it('play should goto and play frame 0 given no intro label', () => {
const adapter = new Adapter(webcg, movieClip)
const adapter = new Adapter(webcg, movieClip, createjs)
adapter.play()
assert(movieClip.gotoAndPlay.calledWith(0))
})

it('stop should goto and play outro', () => {
labels.push({ label: 'outro', position: 2 })
const adapter = new Adapter(webcg, movieClip)
const adapter = new Adapter(webcg, movieClip, createjs)
adapter.stop()
assert(movieClip.gotoAndPlay.calledWith(2))
})

it('stop should set visibile false given no label outro', () => {
const adapter = new Adapter(webcg, movieClip)
const adapter = new Adapter(webcg, movieClip, createjs)
adapter.stop()
expect(movieClip.gotoAndPlay.calledOnce).to.equal(false)
expect(movieClip.visible).to.equal(false)
})

it('next should play movie clip', () => {
const adapter = new Adapter(webcg, movieClip)
const adapter = new Adapter(webcg, movieClip, createjs)
adapter.next()
expect(movieClip.play.calledOnce).to.equal(true)
})

it('next should set visible true', () => {
const adapter = new Adapter(webcg, movieClip)
const adapter = new Adapter(webcg, movieClip, createjs)
expect(movieClip.visible).to.not.equal(true)
adapter.next()
expect(movieClip.visible).to.equal(true)
})

it('data should update movie clip instances', () => {
movieClip.instance.f0 = { text: 'title' }
movieClip.instance.f1 = { text: 'subtitle' }
const adapter = new Adapter(webcg, movieClip)
const data = JSON.parse('{"f0":"updated title","f1":"updated subtitle"}')
it('data with string data should update createjs Text instances', () => {
movieClip.f0 = new createjs.Text('f0')
movieClip.f0.parent = movieClip
movieClip.instance = new createjs.MovieClip()
movieClip.instance.parent = movieClip
movieClip.instance.f0 = new createjs.Text('instance f0')
movieClip.instance.f0.parent = movieClip.instance
movieClip.instance.f1 = new createjs.Text('instance f1')
movieClip.instance.f1.parent = movieClip.instance

const adapter = new Adapter(webcg, movieClip, createjs)
const data = JSON.parse('{"f0":"updated f0","f1":1,"instance":"not a Text instance"}')
adapter.data(data)
expect(movieClip.instance.f0.text).to.equal('updated title')
expect(movieClip.instance.f1.text).to.equal('updated subtitle')
expect(movieClip.f0.text).to.equal('updated f0')
expect(movieClip.instance.text).to.equal(undefined)
expect(movieClip.instance.f0.text).to.equal('updated f0')
expect(movieClip.instance.f1.text).to.equal(1)
})

it('data with invalid data should not change movie clip instances', () => {
movieClip.instance.f0 = { text: 'title' }
const adapter = new Adapter(webcg, movieClip)
const event = new window.CustomEvent('data', { detail: '' })
adapter.data(event)
expect(movieClip.instance.f0.text).to.equal('title')
it('data should update createjs MovieClip instances', () => {
movieClip.f0 = new createjs.Text('f0')
movieClip.f0.parent = movieClip
movieClip.instance = new createjs.MovieClip()
movieClip.instance.parent = movieClip
movieClip.instance.f0 = new createjs.Text('instance f0')
movieClip.instance.f0.parent = movieClip.instance
movieClip.instance.f1 = new createjs.Text('instance f1')
movieClip.instance.f1.parent = movieClip.instance

const adapter = new Adapter(webcg, movieClip, createjs)
const data = JSON.parse('{"f0":"updated f0","f1":{"color":"red","text":1},"instance":{"visible":false,"unknownProp": true}}')
adapter.data(data)
expect(movieClip.f0.text).to.equal('updated f0')
expect(movieClip.instance.text).to.equal(undefined)
expect(movieClip.instance.visible).to.equal(false)
expect(movieClip.instance.unknownProp).to.equal(undefined)
expect(movieClip.instance.f0.text).to.equal('updated f0')
expect(movieClip.instance.f1.color).to.equal('red')
expect(movieClip.instance.f1.text).to.equal(1)
})
})
23 changes: 23 additions & 0 deletions test/createjs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class DisplayObject {
constructor () {
this.visible = true
}
}

class MovieClip extends DisplayObject {
}

class Text extends DisplayObject {
constructor (text, font, color) {
super()
this.text = text || ''
this.font = font || '10px sans-serif'
this.color = color || '#000'
}
}

window.createjs = {
DisplayObject,
MovieClip,
Text
}
19 changes: 19 additions & 0 deletions test/main.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe('init', () => {
beforeEach(() => {
window = { location: { search: '' } }
window.AdobeAn = AdobeAn = {}
window.createjs = {}
window.webcg = {}
AdobeAn.bootstrapCallback = sinon.spy()
})
Expand All @@ -14,4 +15,22 @@ describe('init', () => {
init(window)
expect(AdobeAn.bootstrapCallback.calledOnce).to.equal(true)
})

it('should not register bootstrapCallback when window.webcg is not defined', () => {
delete window['AdobeAn']
init(window)
expect(AdobeAn.bootstrapCallback.called).to.equal(false)
})

it('should not register bootstrapCallback when window.AdobeAn is not defined', () => {
delete window['AdobeAn']
init(window)
expect(AdobeAn.bootstrapCallback.called).to.equal(false)
})

it('should not register bootstrapCallback when window.createjs is not defined', () => {
delete window['createjs']
init(window)
expect(AdobeAn.bootstrapCallback.called).to.equal(false)
})
})

0 comments on commit d16782e

Please sign in to comment.