Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(scintillator/animation): Keyframe-based animation #84

Merged
merged 6 commits into from
Feb 3, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
15 changes: 12 additions & 3 deletions public/skins/default/skin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
<!-- mixins-->
<!-- note panel-->
<group>
<animation>
<keyframe t="0.25" x="-361"></keyframe>
<keyframe t="0.6" x="0" ease="quadOut"></keyframe>
</animation>
<sprite image="NotePanel/Left.png" x="0" y="0"></sprite>
<group x="34" y="0">
<sprite image="NoteArea/Background/DXL.png" x="0" y="0"></sprite>
<sprite image="NoteArea/Flash.png" x="1" y="476" blend="screen" alpha="1 - (time % 400) / 400"></sprite>
<sprite image="NoteArea/Flash.png" x="1" y="476" blend="screen" alpha="1 - (t % 0.4) / 0.4"></sprite>
<group x="1" mask="283x550+1+0">
<group y="(time / 2 % 1500) - 500">
<group y="(t * 500 % 1500) - 500">
<object key="note_sc">
<sprite image="Note/DX.png" frame="61x12+0+0" x="0" y="y"></sprite>
</object>
Expand Down Expand Up @@ -107,5 +111,10 @@
</group>
</group>
<!-- info panel-->
<sprite image="InfoPanel/Background.png" y="616"></sprite>
<sprite image="InfoPanel/Background.png" y="616">
<animation>
<keyframe t="0" y="720"></keyframe>
<keyframe t="0.3" y="616" ease="quadOut"></keyframe>
</animation>
</sprite>
</skin>
10 changes: 8 additions & 2 deletions public/skins/default/skin_template.jade
Original file line number Diff line number Diff line change
Expand Up @@ -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')
12 changes: 12 additions & 0 deletions spec/scintillator/fixtures/animation.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<skin width="123" height="456">
<group y="t*2">
<animation>
<keyframe t="0" x="10" />
<keyframe t="1" x="20" />
</animation>
<animation on="exitEvent">
<keyframe t="0" x="50" y="0" />
<keyframe t="1" x="70" y="100" />
</animation>
</group>
</skin>
76 changes: 76 additions & 0 deletions spec/scintillator/nodes/concerns/animation_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@

import { _compile, _attrs, Animation }
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(`<keyframe t="0" x="10" y="30" />`)[0]
expect(_attrs(xml)).to.deep.equal({ t: '0', x: '10', y: '30' })
})
})

describe('_compile', function() {
it('compiles animation into keyframes', function() {
let xml = $xml(`<animation>
<keyframe t="0" x="10" y="30" />
<keyframe t="2" x="20" />
<keyframe t="5" x="15" y="20" />
</animation>`)
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' },
], },
],
})
})
it('throws when there is no time', function() {
let xml = $xml(`<animation>
<keyframe />
</animation>`)
expect(() => _compile(xml)).to.throw(Error)
})
})

describe('#_properties', function() {
it('should return a set of properties', function() {
let anim = Animation.compile(null, $xml(`<group>
<animation>
<keyframe t="0" x="10" />
<keyframe t="1" x="20" />
</animation>
<animation>
<keyframe t="0" y="10" />
<keyframe t="1" y="20" />
</animation>
</group>`))
expect(Array.from(anim._properties)).to.deep.equal(['x', 'y'])
})
})

describe('#_events', function() {
it('list distinct events', function() {
let anim = Animation.compile(null, $xml(`<group>
<animation />
<animation />
<animation on="exit" />
<animation on="exit" />
</group>`))
expect(anim._events).to.deep.equal(['', 'exit'])
})
})

})

33 changes: 33 additions & 0 deletions spec/scintillator/scintillator_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,5 +127,38 @@ 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)
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(0)
context.render({ t: 1, exitEvent: 0.5 })
expect(group.x).to.equal(60)
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(100)
context.destroy()
}))
})

})

4 changes: 2 additions & 2 deletions src/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
71 changes: 71 additions & 0 deletions src/scintillator/nodes/concerns/animation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@

import R from 'ramda'
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)) {
return values[name]
} else {
return fallback(data)
}
}
}
_getAnimation(data) {
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'))
let animations = R.map(el => _compile($(el)), animationElements)
return new Animation(animations)
}
}

export function _compile($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 {
on: $el.attr('on') || '',
data: 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
16 changes: 9 additions & 7 deletions src/scintillator/nodes/concerns/display-object.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions src/scintillator/nodes/lib/instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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