From fa4f92372bb08c203ffd6df3edb92fe1d52d7235 Mon Sep 17 00:00:00 2001 From: Thai Pangsakulyanont Date: Tue, 3 Feb 2015 21:16:19 +0700 Subject: [PATCH 1/6] feat(scintillator/animation): generate keyframes from xml I found a library called "keytime" that does exactly what I need: given a set of keyframes and the time, return the state of the animation. An animation can work like this: ```jade sprite(...) animation keyframe(t='0' x='-100') keyframe(t='1' x='0') animation(on='exitEvent') keyframe(t='0' x='0') keyframe(t='1' x='1000') ``` Here, there are two animations. The first one is the default animation, and the second one is the "on exit" animation. Which animation to use is selected based on latest event. An event is the timestamp in the data passed to `render()` of the specified key. { t: 1.5, exitEvent: 1.2 } This means the exitEvent has happened 0.3 seconds ago, so the exit animation should be performed. --- .../nodes/concerns/animation_spec.js | 39 +++++++++++++++++++ src/scintillator/nodes/concerns/animation.js | 32 +++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 spec/scintillator/nodes/concerns/animation_spec.js create mode 100644 src/scintillator/nodes/concerns/animation.js diff --git a/spec/scintillator/nodes/concerns/animation_spec.js b/spec/scintillator/nodes/concerns/animation_spec.js new file mode 100644 index 000000000..e50e193b0 --- /dev/null +++ b/spec/scintillator/nodes/concerns/animation_spec.js @@ -0,0 +1,39 @@ + +import { _parse, _attrs } + from '../../../../src/scintillator/nodes/concerns/animation' +import $ from 'jquery' + +let $xml = xml => $($.parseXML(xml).documentElement) + +describe('Scintillator::Animation', function() { + + describe('_attrs', function() { + it('lists all attributes of an element', function() { + let xml = $xml(``)[0] + expect(_attrs(xml)).to.deep.equal({ t: '0', x: '10', y: '30' }) + }) + }) + + describe('_parse', function() { + it('compiles animation into keyframes', function() { + let xml = $xml(` + + + + `) + expect(_parse(xml)).to.deep.equal([ + { name: 'x', keyframes: [ + { time: 0, value: 10, ease: 'linear' }, + { time: 2, value: 20, ease: 'linear' }, + { time: 5, value: 15, ease: 'linear' }, + ], }, + { name: 'y', keyframes: [ + { time: 0, value: 30, ease: 'linear' }, + { time: 5, value: 20, ease: 'linear' }, + ], }, + ]) + }) + }) + +}) + diff --git a/src/scintillator/nodes/concerns/animation.js b/src/scintillator/nodes/concerns/animation.js new file mode 100644 index 000000000..919f539a4 --- /dev/null +++ b/src/scintillator/nodes/concerns/animation.js @@ -0,0 +1,32 @@ + +import R from 'ramda' + +export class Animation { +} + +export function _parse($el) { + let keyframes = R.map(_attrs, Array.from($el.children('keyframe'))) + let attrs = { } + for (let keyframe of keyframes) { + let time = +keyframe.t + let ease = keyframe.ease || 'linear' + if (isNaN(time)) throw new Error('Expected keyframe to have "t" attribute') + for (let key in keyframe) { + if (key === 't' || key === 'ease') continue + let value = +keyframe[key] + let attr = attrs[key] || (attrs[key] = _createKeyframes(key)) + attr.keyframes.push({ time, value, ease }) + } + } + return R.values(attrs) +} + +function _createKeyframes(name) { + return { name, keyframes: [] } +} + +export function _attrs(el) { + return R.fromPairs(R.map(n => [n.name.toLowerCase(), n.value], el.attributes)) +} + +export default Animation From 8bea6848e265e8dc6155806f359ba0e6b6b8f1a1 Mon Sep 17 00:00:00 2001 From: Thai Pangsakulyanont Date: Tue, 3 Feb 2015 21:46:59 +0700 Subject: [PATCH 2/6] feat(scintillator): only call the side-effect function on value change The last item in the pipeline is the "side-effect" function. To optimize performance, we want to call this side-effect function only when the value has actually changed, not every frame. --- src/scintillator/nodes/lib/instance.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/scintillator/nodes/lib/instance.js b/src/scintillator/nodes/lib/instance.js index 197beadbd..1a9c15a44 100644 --- a/src/scintillator/nodes/lib/instance.js +++ b/src/scintillator/nodes/lib/instance.js @@ -16,8 +16,10 @@ export function Instance(context, callback) { } }, bind(...pipeline) { + let sideEffect = onChange(pipeline.pop()) helper.onData(function(value) { for (let f of pipeline) value = f(value) + sideEffect(value) }) }, onData(f) { @@ -45,4 +47,14 @@ export function Instance(context, callback) { } +function onChange(f) { + let value + return function receiveNewValue(v) { + if (value !== v) { + value = v + f(v) + } + } +} + export default Instance From ac6a59c15cdfd56a9cb3f71760bbebfd65a36aa8 Mon Sep 17 00:00:00 2001 From: Thai Pangsakulyanont Date: Tue, 3 Feb 2015 22:12:11 +0700 Subject: [PATCH 3/6] feat(scintillator/animation): allow simple, single animation --- package.json | 3 +- spec/scintillator/fixtures/animation.xml | 12 ++++++ .../nodes/concerns/animation_spec.js | 41 +++++++++++++------ spec/scintillator/scintillator_spec.js | 11 +++++ src/scintillator/nodes/concerns/animation.js | 36 ++++++++++++++-- .../nodes/concerns/display-object.js | 16 ++++---- 6 files changed, 95 insertions(+), 24 deletions(-) create mode 100644 spec/scintillator/fixtures/animation.xml diff --git a/package.json b/package.json index 8dbb3c045..e27287525 100644 --- a/package.json +++ b/package.json @@ -75,8 +75,9 @@ "co": "~4.1.0", "debug": "^2.1.1", "jquery": "^2.1.3", + "keytime": "^0.1.0", "pixi.js": "^2.2.3", "prfun": "^1.0.2", - "ramda": "^0.8.0" + "ramda": "^0.9.1" } } diff --git a/spec/scintillator/fixtures/animation.xml b/spec/scintillator/fixtures/animation.xml new file mode 100644 index 000000000..8df09d320 --- /dev/null +++ b/spec/scintillator/fixtures/animation.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/spec/scintillator/nodes/concerns/animation_spec.js b/spec/scintillator/nodes/concerns/animation_spec.js index e50e193b0..b4ff52124 100644 --- a/spec/scintillator/nodes/concerns/animation_spec.js +++ b/spec/scintillator/nodes/concerns/animation_spec.js @@ -1,5 +1,5 @@ -import { _parse, _attrs } +import { _compile, _attrs, Animation } from '../../../../src/scintillator/nodes/concerns/animation' import $ from 'jquery' @@ -14,24 +14,39 @@ describe('Scintillator::Animation', function() { }) }) - describe('_parse', function() { + describe('_compile', function() { it('compiles animation into keyframes', function() { let xml = $xml(` `) - expect(_parse(xml)).to.deep.equal([ - { name: 'x', keyframes: [ - { time: 0, value: 10, ease: 'linear' }, - { time: 2, value: 20, ease: 'linear' }, - { time: 5, value: 15, ease: 'linear' }, - ], }, - { name: 'y', keyframes: [ - { time: 0, value: 30, ease: 'linear' }, - { time: 5, value: 20, ease: 'linear' }, - ], }, - ]) + expect(_compile(xml)).to.deep.equal({ + on: '', + data: [ + { name: 'x', keyframes: [ + { time: 0, value: 10, ease: 'linear' }, + { time: 2, value: 20, ease: 'linear' }, + { time: 5, value: 15, ease: 'linear' }, + ], }, + { name: 'y', keyframes: [ + { time: 0, value: 30, ease: 'linear' }, + { time: 5, value: 20, ease: 'linear' }, + ], }, + ], + }) + }) + }) + + describe('#_events', function() { + it('list distinct events', function() { + let anim = Animation.compile(null, $xml(` + + + + + `)) + expect(anim._events).to.deep.equal(['', 'exit']) }) }) diff --git a/spec/scintillator/scintillator_spec.js b/spec/scintillator/scintillator_spec.js index 7ac8670bf..5ff2100a4 100644 --- a/spec/scintillator/scintillator_spec.js +++ b/spec/scintillator/scintillator_spec.js @@ -127,5 +127,16 @@ describe('Scintillator', function() { })) }) + describe('AnimationNode', function() { + it('should allow animations', co.wrap(function*() { + let skin = yield Scintillator.load(fixture('animation.xml')) + let context = new Scintillator.Context(skin) + let group = context.stage.children[0] + context.render({ t: 0 }) + expect(group.x).to.equal(10) + context.destroy() + })) + }) + }) diff --git a/src/scintillator/nodes/concerns/animation.js b/src/scintillator/nodes/concerns/animation.js index 919f539a4..d956853d9 100644 --- a/src/scintillator/nodes/concerns/animation.js +++ b/src/scintillator/nodes/concerns/animation.js @@ -1,10 +1,37 @@ -import R from 'ramda' +import R from 'ramda' +import $ from 'jquery' +import keytime from 'keytime' + +let createKeytime = R.evolve({ data: keytime }) export class Animation { + constructor(animations) { + this._animations = R.map(createKeytime, animations) + this._events = R.uniq(R.map(R.prop('on'), animations)) + } + prop(name, fallback) { + return data => { + let values = this._getAnimation(data) + if (values.hasOwnProperty(name)) { + return values[name] + } else { + return fallback(data) + } + } + } + _getAnimation(data) { + let t = data.t + return this._animations[0].data.values(t) + } + static compile(compiler, $el) { + let animationElements = Array.from($el.children('animation')) + let animations = R.map(el => _compile($(el)), animationElements) + return new Animation(animations) + } } -export function _parse($el) { +export function _compile($el) { let keyframes = R.map(_attrs, Array.from($el.children('keyframe'))) let attrs = { } for (let keyframe of keyframes) { @@ -18,7 +45,10 @@ export function _parse($el) { attr.keyframes.push({ time, value, ease }) } } - return R.values(attrs) + return { + on: $el.attr('on') || '', + data: R.values(attrs), + } } function _createKeyframes(name) { diff --git a/src/scintillator/nodes/concerns/display-object.js b/src/scintillator/nodes/concerns/display-object.js index 6d3a5690b..8c7cade9a 100644 --- a/src/scintillator/nodes/concerns/display-object.js +++ b/src/scintillator/nodes/concerns/display-object.js @@ -5,22 +5,24 @@ import SkinNode from '../lib/base' import Instance from '../lib/instance' import Expression from '../../expression' +import Animation from './animation' export class DisplayObject extends SkinNode { compile(compiler, $el) { - this.x = new Expression($el.attr('x') || '0') - this.y = new Expression($el.attr('y') || '0') - this.alpha = new Expression($el.attr('alpha') || '1') + this.x = new Expression($el.attr('x') || '0') + this.y = new Expression($el.attr('y') || '0') + this.alpha = new Expression($el.attr('alpha') || '1') + this.animation = Animation.compile(compiler, $el) + this.blendMode = parseBlendMode($el.attr('blend') || 'normal') if ($el.attr('width')) this.width = new Expression($el.attr('width')) if ($el.attr('height')) this.height = new Expression($el.attr('height')) if ($el.attr('visible')) this.visible = new Expression($el.attr('visible')) - this.blendMode = parseBlendMode($el.attr('blend') || 'normal') } instantiate(context, object) { return new Instance(context, self => { - self.bind(this.x, x => object.x = x) - self.bind(this.y, y => object.y = y) - self.bind(this.alpha, a => object.alpha = a) + self.bind(this.animation.prop('x', this.x), x => object.x = x) + self.bind(this.animation.prop('y', this.y), y => object.y = y) + self.bind(this.animation.prop('alpha', this.alpha), a => object.alpha = a) if (this.width) self.bind(this.width, w => object.width = w) if (this.height) self.bind(this.height, h => object.height = h) if (this.visible) self.bind(this.visible, v => object.visible = v) From 8de921cec8f38f4c98a38e8cb3974047b42dbac9 Mon Sep 17 00:00:00 2001 From: Thai Pangsakulyanont Date: Tue, 3 Feb 2015 22:33:21 +0700 Subject: [PATCH 4/6] feat(scintillator/animation): allow animation on multiple values --- spec/scintillator/fixtures/animation.xml | 4 ++-- .../nodes/concerns/animation_spec.js | 16 ++++++++++++++ spec/scintillator/scintillator_spec.js | 22 +++++++++++++++++++ src/scintillator/nodes/concerns/animation.js | 13 +++++++++-- 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/spec/scintillator/fixtures/animation.xml b/spec/scintillator/fixtures/animation.xml index 8df09d320..4ccde774c 100644 --- a/spec/scintillator/fixtures/animation.xml +++ b/spec/scintillator/fixtures/animation.xml @@ -1,10 +1,10 @@ - + - + diff --git a/spec/scintillator/nodes/concerns/animation_spec.js b/spec/scintillator/nodes/concerns/animation_spec.js index b4ff52124..1a09606c2 100644 --- a/spec/scintillator/nodes/concerns/animation_spec.js +++ b/spec/scintillator/nodes/concerns/animation_spec.js @@ -38,6 +38,22 @@ describe('Scintillator::Animation', function() { }) }) + describe('#_properties', function() { + it('should return a set of properties', function() { + let anim = Animation.compile(null, $xml(` + + + + + + + + + `)) + expect(Array.from(anim._properties)).to.deep.equal(['x', 'y']) + }) + }) + describe('#_events', function() { it('list distinct events', function() { let anim = Animation.compile(null, $xml(` diff --git a/spec/scintillator/scintillator_spec.js b/spec/scintillator/scintillator_spec.js index 5ff2100a4..a5d2eee70 100644 --- a/spec/scintillator/scintillator_spec.js +++ b/spec/scintillator/scintillator_spec.js @@ -134,6 +134,28 @@ describe('Scintillator', function() { let group = context.stage.children[0] context.render({ t: 0 }) expect(group.x).to.equal(10) + expect(group.y).to.equal(0) + context.render({ t: 0.5 }) + expect(group.x).to.equal(15) + expect(group.y).to.equal(1) + context.render({ t: 1 }) + expect(group.x).to.equal(20) + expect(group.y).to.equal(2) + context.destroy() + })) + it('should allow animations on different events', co.wrap(function*() { + let skin = yield Scintillator.load(fixture('animation.xml')) + let context = new Scintillator.Context(skin) + let group = context.stage.children[0] + context.render({ t: 0.5, exitEvent: 0.5 }) + expect(group.x).to.equal(50) + expect(group.y).to.equal(1) + context.render({ t: 1, exitEvent: 0.5 }) + expect(group.x).to.equal(60) + expect(group.y).to.equal(2) + context.render({ t: 1.5, exitEvent: 0.5 }) + expect(group.x).to.equal(70) + expect(group.y).to.equal(3) context.destroy() })) }) diff --git a/src/scintillator/nodes/concerns/animation.js b/src/scintillator/nodes/concerns/animation.js index d956853d9..c77fc31df 100644 --- a/src/scintillator/nodes/concerns/animation.js +++ b/src/scintillator/nodes/concerns/animation.js @@ -4,13 +4,18 @@ import $ from 'jquery' import keytime from 'keytime' let createKeytime = R.evolve({ data: keytime }) +let getEvents = R.pipe(R.prop('data'), R.map(R.prop('name'))) export class Animation { constructor(animations) { + this._properties = new Set(R.chain(getEvents, animations)) this._animations = R.map(createKeytime, animations) this._events = R.uniq(R.map(R.prop('on'), animations)) } prop(name, fallback) { + if (!this._properties.has(name)) { + return fallback + } return data => { let values = this._getAnimation(data) if (values.hasOwnProperty(name)) { @@ -21,8 +26,12 @@ export class Animation { } } _getAnimation(data) { - let t = data.t - return this._animations[0].data.values(t) + let event = R.maxBy(e => data[e] || 0, + this._events.filter(e => e === '' || e in data)) + let t = data.t - (data[event] || 0) + let animations = this._animations.filter(a => a.on === event) + let values = animations.map(a => a.data.values(t)) + return Object.assign({}, ...values) } static compile(compiler, $el) { let animationElements = Array.from($el.children('animation')) From a73663ce83db46f95329c2d9286fca01c117f124 Mon Sep 17 00:00:00 2001 From: Thai Pangsakulyanont Date: Tue, 3 Feb 2015 22:42:04 +0700 Subject: [PATCH 5/6] feat(skins/default): make use of animations --- public/skins/default/skin.xml | 15 ++++++++++++--- public/skins/default/skin_template.jade | 10 ++++++++-- src/app/index.js | 4 ++-- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/public/skins/default/skin.xml b/public/skins/default/skin.xml index b9399b1c8..2b6cb5f08 100644 --- a/public/skins/default/skin.xml +++ b/public/skins/default/skin.xml @@ -8,12 +8,16 @@ + + + + - + - + @@ -107,5 +111,10 @@ - + + + + + + \ No newline at end of file diff --git a/public/skins/default/skin_template.jade b/public/skins/default/skin_template.jade index 98949d953..4dab0ea0f 100644 --- a/public/skins/default/skin_template.jade +++ b/public/skins/default/skin_template.jade @@ -35,14 +35,20 @@ skin(width='1280' height='720') // note panel group + animation + keyframe(t='0.25' x='-361') + keyframe(t='0.6' x='0' ease='quadOut') sprite(image='NotePanel/Left.png' x='0' y='0') group(x='34' y='0') sprite(image='NoteArea/Background/DXL.png' x='0' y='0') sprite(image='NoteArea/Flash.png' x='1' y='476' blend='screen' - alpha='1 - (time % 400) / 400') + alpha='1 - (t % 0.4) / 0.4') group(x='1' mask='283x550+1+0') - group(y='(time / 2 % 1500) - 500') + group(y='(t * 500 % 1500) - 500') +notes(mode.iidx_l) // info panel sprite(image='InfoPanel/Background.png' y='616') + animation + keyframe(t='0' y='720') + keyframe(t='0.3' y='616' ease='quadOut') diff --git a/src/app/index.js b/src/app/index.js index 68dba1215..fb131ba16 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -37,9 +37,9 @@ export function main() { data[i].push({ key: 2, y, height, active: true }) } - + let started = new Date().getTime() let draw = () => { - data.time = new Date().getTime() + data.t = (new Date().getTime() - started) / 1000 context.render(data) } draw() From 8e92429cb6ff3e06a9833051259199a2e3690955 Mon Sep 17 00:00:00 2001 From: Thai Pangsakulyanont Date: Tue, 3 Feb 2015 22:48:18 +0700 Subject: [PATCH 6/6] test(scintillator): test more edge cases --- spec/scintillator/fixtures/animation.xml | 4 ++-- spec/scintillator/nodes/concerns/animation_spec.js | 6 ++++++ spec/scintillator/scintillator_spec.js | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/spec/scintillator/fixtures/animation.xml b/spec/scintillator/fixtures/animation.xml index 4ccde774c..76eb6fa76 100644 --- a/spec/scintillator/fixtures/animation.xml +++ b/spec/scintillator/fixtures/animation.xml @@ -5,8 +5,8 @@ - - + + diff --git a/spec/scintillator/nodes/concerns/animation_spec.js b/spec/scintillator/nodes/concerns/animation_spec.js index 1a09606c2..12de120a6 100644 --- a/spec/scintillator/nodes/concerns/animation_spec.js +++ b/spec/scintillator/nodes/concerns/animation_spec.js @@ -36,6 +36,12 @@ describe('Scintillator::Animation', function() { ], }) }) + it('throws when there is no time', function() { + let xml = $xml(` + + `) + expect(() => _compile(xml)).to.throw(Error) + }) }) describe('#_properties', function() { diff --git a/spec/scintillator/scintillator_spec.js b/spec/scintillator/scintillator_spec.js index a5d2eee70..129ac8841 100644 --- a/spec/scintillator/scintillator_spec.js +++ b/spec/scintillator/scintillator_spec.js @@ -149,13 +149,13 @@ describe('Scintillator', function() { let group = context.stage.children[0] context.render({ t: 0.5, exitEvent: 0.5 }) expect(group.x).to.equal(50) - expect(group.y).to.equal(1) + expect(group.y).to.equal(0) context.render({ t: 1, exitEvent: 0.5 }) expect(group.x).to.equal(60) - expect(group.y).to.equal(2) + expect(group.y).to.equal(50) context.render({ t: 1.5, exitEvent: 0.5 }) expect(group.x).to.equal(70) - expect(group.y).to.equal(3) + expect(group.y).to.equal(100) context.destroy() })) })