Skip to content

Commit

Permalink
feat: support of jumping to labels using ACMP invoke
Browse files Browse the repository at this point in the history
  • Loading branch information
indr committed Jan 23, 2020
1 parent 94e3f9a commit a0c82ae
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 136 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ webcg-adobe-animate-adapter is an adapter to use Adobe Animate HTML5 Canvas temp
## Features

- Support of ACMP commands `play`, `next`, `stop`, `update` and `invoke`.
- Support of "intro" and "outro" labels.
- Support of three data formats: plain JavaScript object, JSON and the templateData XML format.
- Support of "intro" and "outro" labels for playing and stopping.
- Support of jumping to labels using ACMP `invoke`.
- Automatic update of template instances according to the provided data via `update`.
- Three data formats: plain JavaScript object, JSON and the templateData XML format.
- The adapter comes with lazy-loaded [development tools](https://github.com/indr/webcg-devtools) that provide a user interface to edit the template data and invoke the above commands.

## Installation
Expand Down
Binary file modified docs/example-lower-third.fla
Binary file not shown.
2 changes: 1 addition & 1 deletion docs/example-lower-third.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ if (loop == null) { loop = false; } this.initialize(mode,startPosition,loop,{});

// stage content:
(lib.examplelowerthird = function(mode,startPosition,loop) {
if (loop == null) { loop = false; } this.initialize(mode,startPosition,loop,{intro:1,outro:20});
if (loop == null) { loop = false; } this.initialize(mode,startPosition,loop,{intro:1,label1:10,outro:20});

// timeline functions:
this.frame_0 = function() {
Expand Down
17 changes: 17 additions & 0 deletions src/Adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ const Adapter = class {
webcg.addEventListener('stop', this.stop.bind(this))
webcg.addEventListener('next', this.next.bind(this))
webcg.addEventListener('data', this.data.bind(this))

if (this.movieClip) {
const labels = this.movieClip.getLabels().filter(label => ['play', 'stop', 'next', 'update', 'data'].indexOf(label.label) === -1)
for (let i = 0; i < labels.length; i++) {
const labelName = labels[i].label
webcg.addEventListener(labelName, this._gotoAndPlay.bind(this, labelName))
}
}
}

play () {
Expand Down Expand Up @@ -54,6 +62,15 @@ const Adapter = class {
return null
}

_gotoAndPlay (labelName) {
const label = this._findLabel(labelName)
if (label) {
this.movieClip.gotoAndPlay(label.position)
} else {
console.warn('[webcg-adobe-animate-adapter] label "%s" not found', labelName)
}
}

_updateInstances (data) {
const instances = this._getDisplayObjectInstances(this.movieClip)
Object.keys(data).forEach(componentId => {
Expand Down
309 changes: 176 additions & 133 deletions test/Adapter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,138 +16,181 @@ describe('Adapter', () => {
movieClip.visible = null
})

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

it('should throw TypeError when movieClip is null or not an object', () => {
expect(() => new Adapter(webcg, null))
.to.throw(TypeError, 'movieClip must be an object')
expect(() => new Adapter(webcg, ''))
.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, createjs)
expect(adapter).to.be.instanceOf(Adapter)
})

it('should add webcg event listeners', () => {
// eslint-disable-next-line no-new
new Adapter(webcg, movieClip, createjs)
assert(webcg.addEventListener.calledWith('play'))
assert(webcg.addEventListener.calledWith('stop'))
assert(webcg.addEventListener.calledWith('next'))
assert(webcg.addEventListener.calledWith('data'))
})

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

it('play should set visible true', () => {
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, 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, 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, createjs)
adapter.stop()
assert(movieClip.gotoAndPlay.calledWith(2))
})

it('stop should set visibile false given no label outro', () => {
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, createjs)
adapter.next()
expect(movieClip.play.calledOnce).to.equal(true)
})

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

it('data without argument or null must not throw exception', () => {
const adapter = new Adapter(webcg, movieClip, createjs)
adapter.data()
adapter.data(null)
})

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.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 object 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)
describe('constructor', () => {
it('should throw TypeError when webcg is null or not an object', () => {
expect(() => new Adapter(null, movieClip, createjs))
.to.throw(TypeError, 'webcg must be an object')
expect(() => new Adapter('', movieClip, createjs))
.to.throw(TypeError, 'webcg must be an object')
})

it('should throw TypeError when movieClip is null or not an object', () => {
expect(() => new Adapter(webcg, null))
.to.throw(TypeError, 'movieClip must be an object')
expect(() => new Adapter(webcg, ''))
.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, createjs)
expect(adapter).to.be.instanceOf(Adapter)
})

it('should add webcg event listeners', () => {
// eslint-disable-next-line no-new
new Adapter(webcg, movieClip, createjs)
assert(webcg.addEventListener.calledWith('play'))
assert(webcg.addEventListener.calledWith('stop'))
assert(webcg.addEventListener.calledWith('next'))
assert(webcg.addEventListener.calledWith('data'))
})

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

it('should register labels as invokables', () => {
labels = [{ label: 'label1', position: 1 }, { label: 'label2', position: 20 }]
// eslint-disable-next-line no-new
new Adapter(webcg, movieClip, createjs)
assert(webcg.addEventListener.calledWith('label1'))
assert(webcg.addEventListener.calledWith('label2'))
})

it('should not register labels play, stop, next, update, data', () => {
labels = [{ label: 'label1', position: 1 }]
// eslint-disable-next-line no-new
new Adapter(webcg, movieClip, createjs)
const callCount = webcg.addEventListener.callCount

webcg.addEventListener = sinon.spy()
labels = ['play', 'stop', 'next', 'update', 'data', 'label1'].map((label, idx) => ({ label, position: idx + 1 }))
// eslint-disable-next-line no-new
new Adapter(webcg, movieClip, createjs)
expect(webcg.addEventListener.callCount).to.equal(callCount)
})
})

describe('invoke label', () => {
it('should go to and play', () => {
labels = [{ label: 'label1', position: 1 }, { label: 'label2', position: 20 }]
// eslint-disable-next-line no-new
new Adapter(webcg, movieClip, createjs)
assert(webcg.addEventListener.calledWith('label2'))
expect(webcg.addEventListener.lastCall.args[0]).to.equal('label2')
webcg.addEventListener.lastCall.args[1]()
assert(movieClip.gotoAndPlay.calledWith(20))
})
})

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

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

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

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

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

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

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

describe('data', () => {
it('without argument or null must not throw exception', () => {
const adapter = new Adapter(webcg, movieClip, createjs)
adapter.data()
adapter.data(null)
})

it('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.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('with object 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)
})
})
})
4 changes: 4 additions & 0 deletions test/createjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ class DisplayObject {
}

class MovieClip extends DisplayObject {
getLabels () {
return []
}

stop () {}
}

Expand Down

0 comments on commit a0c82ae

Please sign in to comment.