//
// Triple Play - utilities for use in PlayN-based games
// Copyright (c) 2011-2014, Three Rings Design, Inc. - All rights reserved.
// http://github.com/threerings/tripleplay/blob/master/LICENSE
package tripleplay.flump;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import react.Signal;
import pythagoras.f.FloatMath;
import playn.core.GroupLayer;
import playn.core.Layer;
import playn.core.util.Clock;
import static playn.core.PlayN.*;
public class Movie
implements Instance
{
public static class Symbol
implements tripleplay.flump.Symbol
{
/** The number of frames in this movie. */
public final int frames;
/** The layers in this movie. */
public final List<LayerData> layers;
/** The duration of this movie, in milliseconds. */
public final float duration;
protected Symbol (float frameRate, String name, List<LayerData> layers) {
_name = name;
this.layers = Collections.unmodifiableList(layers);
int frames = 0;
for (LayerData layer : layers) {
frames = Math.max(layer.frames(), frames);
}
this.frames = frames;
_framesPerMs = frameRate/1000;
this.duration = frames/_framesPerMs;
}
@Override public String name () {
return _name;
}
@Override public Movie createInstance () {
return new Movie(this);
}
protected String _name;
protected float _framesPerMs;
}
public final Signal<String> labelPassed = Signal.create();
protected Movie (Symbol symbol) {
_symbol = symbol;
_animators = new LayerAnimator[symbol.layers.size()];
for (int ii = 0, ll = _animators.length; ii < ll; ++ii) {
LayerAnimator animator = new LayerAnimator(symbol.layers.get(ii));
_animators[ii] = animator;
_root.add(animator.content);
}
setFrame(1, 0);
}
@Override public GroupLayer layer () {
return _root;
}
@Override public void paint (Clock clock) {
paint(clock.dt());
}
@Override public void paint (float dt) {
dt *= _speed;
_position += dt;
if (_position > _symbol.duration) {
_position = _position % _symbol.duration;
} else if (_position < 0) {
// Normally we shouldn't be negative, but if we're setPositioning, submovies may
// have completely different durations, so stepping backwards wraps around the
// other way
_position = _symbol.duration + (_position % _symbol.duration);
}
float nextFrame = _position*_symbol._framesPerMs;
setFrame(nextFrame, dt);
}
@Override public void destroy () {
_root.destroy();
}
/** The playback position, in milliseconds. */
public float position () {
return _position;
}
/** Changes the playback position. */
public void setPosition (float position) {
if (position < 0) position = 0;
paint(position - _position);
}
public Symbol symbol () {
return _symbol;
}
/** The playback speed multiplier, defaults to 1. Larger values will play faster. */
public float speed () {
return _speed;
}
/** Changes the playback speed multiplier. */
public void setSpeed (float speed) {
_speed = speed;
}
/** Retrieves a named layer. It should generally not be modified. */
public Layer getNamedLayer (String name) {
LayerAnimator animator = getNamedAnimator(name);
return animator != null ? animator.content : null;
}
/** Returns all of the {@code Instance}s on a named layer. */
public List<Instance> getInstances (String name) {
LayerAnimator animator = getNamedAnimator(name);
if (animator == null) return Collections.<Instance>emptyList();
if (animator._instances == null) return Collections.singletonList(animator._current);
return Collections.unmodifiableList(Arrays.asList(animator._instances));
}
/**
* Replaces the instance on a named layer. The layer should already be empty or contain a single
* Movie.
*/
public void setNamedLayer (String name, Instance instance) {
LayerAnimator animator = getNamedAnimator(name);
if (animator != null) {
assert animator.content instanceof GroupLayer :
"Layer not a container[name=" + name + "]";
animator.setCurrent(instance);
}
}
/** Retrieves all named layers. They generally should not be modified. */
public Map<String,Layer> namedLayers () {
Map<String,Layer> namedLayers = new HashMap<String,Layer>();
for (LayerAnimator animator : _animators) {
namedLayers.put(animator.data.name, animator.content);
}
return Collections.unmodifiableMap(namedLayers);
}
protected LayerAnimator getNamedAnimator (String name) {
for (LayerAnimator animator : _animators) {
if (animator.data.name.equals(name)) {
return animator;
}
}
return null; // Not found
}
protected void setFrame (float frame, float dt) {
if (frame == _frame) {
return;
}
if (frame < _frame) {
// Wrap back to the beginning
for (int ii = 0, ll = _animators.length; ii < ll; ++ii) {
LayerAnimator animator = _animators[ii];
animator.changedKeyframe = true;
animator.keyframeIdx = 0;
}
}
for (int ii = 0, ll = _animators.length; ii < ll; ++ii) {
LayerAnimator animator = _animators[ii];
animator.setFrame(frame, dt);
}
_frame = frame;
}
// Controls a single Flash layer
protected class LayerAnimator {
public final LayerData data;
public final Layer content;
public int keyframeIdx = 0;
public boolean changedKeyframe = false;
public LayerAnimator (LayerData data) {
this.data = data;
if (data._multipleSymbols) {
_instances = new Instance[data.keyframes.size()];
for (int ii = 0, ll = _instances.length; ii < ll; ++ii) {
tripleplay.flump.Symbol sym = data.keyframes.get(ii).symbol();
if (sym == null) {
throw new IllegalArgumentException("Keyframe missing symbol layer=" +
data.name + " frame=" + ii);
}
_instances[ii] = sym.createInstance();
}
content = graphics().createGroupLayer();
setCurrent(_instances[0]);
} else if (data._lastSymbol != null) {
_current = data._lastSymbol.createInstance();
content = _current.layer();
} else {
content = graphics().createGroupLayer();
}
}
public void setFrame (float frame, float dt) {
List<KeyframeData> keyframes = data.keyframes;
int finalFrame = keyframes.size()-1;
int startFrame = keyframeIdx + 1;
while (keyframeIdx < finalFrame && keyframes.get(keyframeIdx+1).index <= frame) {
++keyframeIdx;
changedKeyframe = true;
}
if (changedKeyframe && _instances != null) {
// Switch to the next instance if this is a multi-symbol layer
setCurrent(_instances[keyframeIdx]);
changedKeyframe = false;
}
KeyframeData kf = keyframes.get(keyframeIdx);
tripleplay.flump.Symbol currSymbol = kf.symbol();
boolean visible = currSymbol != null && kf.visible;
content.setVisible(visible);
// NOTE: This has some exciting limitations regarding setPosition. If you jump to a
// position, note that like flash itself, we're not smart enough to start anywhere but
// at the very beginning
if (currSymbol != _prevFrameSymbol && _current instanceof Movie) {
((Movie)_current).setPosition(0);
}
_prevFrameSymbol = currSymbol;
if (!visible) {
emitLabelSignals(startFrame, keyframeIdx);
return; // Don't bother animating invisible layers
}
float locX = kf.loc.x();
float locY = kf.loc.y();
float scaleX = kf.scale.x();
float scaleY = kf.scale.y();
float skewX = kf.skew.x();
float skewY = kf.skew.y();
float alpha = kf.alpha;
if (kf.tweened && keyframeIdx < finalFrame) {
// Interpolate with the next keyframe, if there's something on the next keyframe
KeyframeData nextKf = keyframes.get(keyframeIdx+1);
if (nextKf.symbol() != null) {
float interp = (frame-kf.index) / kf.duration;
float ease = kf.ease;
if (ease != 0) {
float t;
if (ease < 0) {
// Ease in
float inv = 1 - interp;
t = 1 - inv*inv;
ease = -ease;
} else {
// Ease out
t = interp*interp;
}
interp = ease*t + (1-ease)*interp;
}
locX += (nextKf.loc.x()-locX) * interp;
locY += (nextKf.loc.y()-locY) * interp;
scaleX += (nextKf.scale.x()-scaleX) * interp;
scaleY += (nextKf.scale.y()-scaleY) * interp;
skewX += (nextKf.skew.x()-skewX) * interp;
skewY += (nextKf.skew.y()-skewY) * interp;
alpha += (nextKf.alpha-alpha) * interp;
}
}
float sinX = FloatMath.sin(skewX), cosX = FloatMath.cos(skewX);
float sinY = FloatMath.sin(skewY), cosY = FloatMath.cos(skewY);
// Create a transformation matrix that translates to locX/Y, skews, then scales
float m00 = cosY * scaleX;
float m01 = sinY * scaleX;
float m10 = -sinX * scaleY;
float m11 = cosX * scaleY;
content.transform().setTransform(m00, m01, m10, m11, locX, locY);
content.setOrigin(kf.pivot.x(), kf.pivot.y());
content.setAlpha(alpha);
if (_current != null) {
_current.paint(dt);
}
emitLabelSignals(startFrame, keyframeIdx);
}
protected void emitLabelSignals (int startIdx, int endIdx) {
for (int ii = startIdx; ii <= endIdx; ii++) {
String label = data.keyframes.get(ii).label;
if (label != null) {
labelPassed.emit(label);
}
}
}
protected void setCurrent (Instance current) {
if (_current != current) {
_current = current;
GroupLayer group = (GroupLayer)content;
group.removeAll();
group.add(current.layer());
}
}
protected Instance _current; // The instance currently visible
protected Instance[] _instances; // Null if only 0-1 instance on this layer
/** We track this to know where we've added a new symbol and thus should reset position. */
protected tripleplay.flump.Symbol _prevFrameSymbol = null;
}
protected Symbol _symbol;
protected GroupLayer _root = graphics().createGroupLayer();
protected LayerAnimator[] _animators;
protected float _frame = 0;
protected float _position = 0;
protected float _speed = 1;
}