Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Animation additions part 1 #11

Closed
wants to merge 1 commit into from

2 participants

@khrome

So currently there are a few additions, which allows for animation of paths and transformations for SVGs... it's not yet complete and there are a number of cases not supported:

  • SVG segment type transforms (no tweening from an arc to a line on a path, for example)
  • No segment additions, currently it doesn't support transforming to path with a different number of segments
  • And worst of all, it currently directly tweens the SVG d attribute, when it should reach through the shape interface to alter the line.

In addition, I changed some of the places where it's setting attributes to setting styles, in the interest of external css being able to apply to these elements, I'm not sure if this will affect compatability... but if not, it's a worthwhile change.

regardless, it's pretty cool... I'm currently working inside some visualization code, but I'll loop back around and fill in some of these gaps in a couple of weeks... cheers!

@khrome khrome changes some setting from attributes to style in the interest of styl…
…ing from external css and addition of Fx.Step for tweening function calls and attributes with Object and Path primitives
99fd12c
@sebmarkbage

Glad to see some interest in this. Some points though:

  • Everything in the core library should work with VML, Canvas and other output formats as well.

  • Therefore, the core shouldn't support external styles. It's an abstraction API. Even if you limit it to the SVG mode, you may want to use the SVG engine to generate SVG code on the server. In that case you may not have a setStyle interpreter.

  • For Path morphs you should look at https://github.com/calyptus/art-morph which does handle the cases you mentioned above. Path morphs is a special case that cannot be optimized well for fluent animations, which is why it shouldn't be in the core library.

  • Animation is a much wanted feature ofc. The plan is to make it more specialized to take advantage of hardware acceleration. You may want to take a look at some more recent animation work here: https://github.com/kamicane/moofx

@khrome
@sebmarkbage

Yea, the ART API is write-only on purpose. In fact, in my local code I've started making Path and other parts write-only as well. This has performance implications and simplifies the implementation.

It is not actually necessary to read back the current value to animate color, opacity and transform. This can be optimized at a lower level. Even if it's not hardware accelerated, the software rendering can be optimized using browser features.

To animate stroke width/style or the path you need to first store the previous value, tween it and then render using CPU rendering. To avoid this special case slowing down everything else, it should be an opt-in part and may require a different API.

API suggestions are welcome. However, they shouldn't store the previous value for Shapes that aren't animated and it should never read anything from the DOM.

@khrome

OK, after thinking about this I think the write-only focus art is headed for is not going to allow the tools I need in my own projects, so what I'm going to do is extend the core object and then do my own base instantiation to create a set of mutable ART objects I can animate with out the object life cycle happening within 1 frame of animation or resorting to an external lookup (actually a write-only path implies the create-destroy cycle).

I'd be curious to know what is being simplified by being write-only, if only so I might know what will blow up beneath me when the new world order arrives?

@sebmarkbage

You'll still have the option to have readable properties. All you need is a wrapper layer that stores whatever values you set. The key is that you're not forced to use them and they're not read from the DOM.

Something like:

fill: function(color){
  this.currentFill = color;
  return this.parent(color);
}

I'm not sure how your projects are structured but I would suggest that you consider storing state in a layer above ART. Since ART is a low-level API.

For example, if you have an Button object that wraps a Shape object, you could save the state of the button background (the Shape's fill) in the Button class rather than the Shape.

setButtonBackgroundColor: function(color){
  this.buttonBackgroundColor = color;
  this.shape.fill(color);
}
getButtonBackgroundColor: function(){
  return this.buttonBackgroundColor;
}
@khrome

All excellent advice, but none of it solves the animated path issue, which is the only real reason I need to reach inside the shapes interface (viz for realtime data), although in the long term I'd like to support filter generation symmetrically with a canvas based layered imaging lib, so I can put one convolution matrix against either a raster or vector layer. So shape animation and filter access are important milestones along the road for me.

So buffering simple style values isn't much of an issue other than the fact they're attribute locked... which, due to VML, there seems to be little to be done about that. It's the path updating that's the sticking point... if it's not done through the shape layer it can't support multiple engines (VML/SVG/etc.) otherwise yeah, I wouldn't have to reach into ART at all. I guess I'm just thrown off by all the onetime objects, it doesn't seem intuitive... seems like the same purpose could be achieved via a static function or a member of the shape class....

@khrome

Sorted, Pulled everything out and did a runtime refactor on the SVG and VML before bringing up base, and then extending Path to be mutable. So everything I'm doing can stay external and I can still flush path modifications to animate. Thanks for the help!

@khrome khrome closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 14, 2012
  1. @khrome

    changes some setting from attributes to style in the interest of styl…

    khrome authored
    …ing from external css and addition of Fx.Step for tweening function calls and attributes with Object and Path primitives
This page is out of date. Refresh to see the latest.
View
5 Source/ART.Base.js
@@ -23,7 +23,10 @@ var VML = function(){
};
var MODE = SVG() ? 'SVG' : VML() ? 'VML' : null;
-if (!MODE) return;
+if (!MODE){
+ throw('no supported output mode!');
+ return;
+}
ART.Shape = new Class({Extends: ART[MODE].Shape});
ART.Group = new Class({Extends: ART[MODE].Group});
View
26 Source/ART.SVG.js
@@ -36,8 +36,8 @@ ART.SVG = new Class({
resize: function(width, height){
var element = this.element;
- element.setAttribute('width', width);
- element.setAttribute('height', height);
+ if(!element.getStyle('width')) element.setStyle('width', width);
+ if(!element.getStyle('height')) element.setStyle('height', height);
this.width = width;
this.height = height;
return this;
@@ -217,12 +217,14 @@ ART.SVG.Base = new Class({
this[type + 'Brush'] = null;
var element = this.element;
if (color == null){
- element.setAttribute(type, 'none');
- element.removeAttribute(type + '-opacity');
+ if(type != 'fill'){
+ //element.setStyle(type, 'none');
+ element.setStyle(type + '-opacity');
+ }
} else {
color = Color.detach(color);
- element.setAttribute(type, color[0]);
- element.setAttribute(type + '-opacity', color[1]);
+ element.setStyle(type, color[0]);
+ element.setStyle(type + '-opacity', color[1]);
}
},
@@ -291,8 +293,8 @@ ART.SVG.Base = new Class({
var image = createElement('image');
image.setAttributeNS(XLINK, 'href', url);
- image.setAttribute('width', width);
- image.setAttribute('height', height);
+ if(!image.getStyle('width')) image.setStyle('width', width);
+ if(!image.getStyle('height')) image.setStyle('height', height);
image.setAttribute('preserveAspectRatio', 'none'); // none, xMidYMid slice, xMidYMid meet
if (color1 != null){
@@ -338,8 +340,8 @@ ART.SVG.Base = new Class({
pattern.setAttribute('x', left || 0);
pattern.setAttribute('y', top || 0);
- pattern.setAttribute('width', width);
- pattern.setAttribute('height', height);
+ if(!pattern.getStyle('width')) pattern.setStyle('width', width);
+ if(!pattern.getStyle('height')) pattern.setStyle('height', height);
//pattern.setAttribute('viewBox', '0 0 75 50');
//pattern.setAttribute('preserveAspectRatio', 'xMidYMid slice');
@@ -395,8 +397,8 @@ ART.SVG.Image = new Class({
draw: function(src, width, height){
var element = this.element;
element.setAttributeNS(XLINK, 'href', src);
- element.setAttribute('width', width);
- element.setAttribute('height', height);
+ if(!element.getStyle('width')) element.setStyle('width', width);
+ if(!element.getStyle('height')) element.setStyle('height', height);
this.width = width;
this.height = height;
return this;
View
10 Source/ART.js
@@ -18,7 +18,7 @@ ART.Element = new Class({
/* dom */
- inject: function(element){
+ inject: function(element, position){
if (element.element) element = element.element;
element.appendChild(this.element);
return this;
@@ -163,6 +163,14 @@ ART.Transform = new Class({
if (m.xx < 0 ? m.xy >= 0 : m.xy < 0) flip = -flip;
return this.rotate(deg - Math.atan2(flip * m.yx, flip * m.xx) * 180 / Math.PI, x, y);
},
+
+ centroidScaleTo: function(x, y){
+ if(!y) y = x;
+ var xx = this.xx;
+ var yy = this.yy;
+ this.scaleTo(x, y);
+ this.move( (xx - this.xx) * this.width / 2 , (yy - this.yy) * this.height / 2 );
+ },
scaleTo: function(x, y){
// Normalize
View
199 Source/Fx.Step.js
@@ -0,0 +1,199 @@
+/*
+---
+name: Fx.Step
+description: "It ♥s ART, over time"
+requires: [Fx, Fx/CSS, Color/Color, ART]
+provides: [Fx/Step]
+...
+*/
+
+(function(){
+
+this.Fx.Step = new Class({ // tweening for attrs & functions with support for macros and SVG paths
+ Extends: Fx,
+ initialize: function(element, options){
+ this.element = element;
+ this.parent(options);
+ if( (!this.options.attribute) && (!this.options.setter)) this.options.setter = 'centroidScaleTo';
+ },
+ set: function(now){
+ if(this.options.setter){
+ if(typeOf(this.options.setter) == 'function'){
+ if(this.options.args){
+ var argsv = [];
+ now = this.serve(now);
+ if(typeOf(now) == 'array' && now.length == 1) now = now[0];
+ this.options.args.each(function(arg){
+ argsv.push(now[arg]);
+ }.bind(this));
+ var context = this.options.bind?this.options.bind:this;
+ this.options.setter.apply(context, argsv)
+ }else{
+ now = Math.floor(this.serve(now)*1000)/1000;
+ this.options.setter(now);
+ }
+ }else{
+ if(this.options.args){
+ var argsv = [];
+ now = this.serve(now);
+ this.options.args.each(function(arg){
+ argsv[arg] = now[arg];
+ }.bind(this));
+ var context = this.options.bind?this.options.bind:this;
+ if(this.element[this.options.setter]) this.element[this.options.setter].apply(context, argsv);
+ else if(window[this.options.setter]) window[this.options.setter].apply(context, argsv);
+ }else{
+ now = Math.floor(this.serve(now)*1000)/1000;
+ if(this.element[this.options.setter]) this.element[this.options.setter](now);
+ else if(window[this.options.setter]) window[this.options.setter](now);
+ }
+ }
+ }else{
+ if(this.options.attribute){
+ this.element.setAttribute(this.options.attribute, this.serve(now));
+ }
+ }
+ return now;
+ },
+ serve: function(value, unit){
+ if (typeOf(value) != 'fx:step:value'){
+ if(typeOf(value) == 'array'){
+ return value[0];
+ }else return value;
+ }
+ var returned = [];
+ value.each(function(bit){
+ returned = returned.concat(bit.parser.serve(bit.value, unit));
+ });
+ return returned;
+ },
+ parse: function(value){
+ value = Function.from(value)();
+ //this is a composite path do not rando tokenize
+ //todo: functions ex: transform
+ if(typeOf(value) == 'object' || value.test && value.test(/^([A-Za-z]( [0-9]+ [0-9]+){1,} ?){1,}$/g) ) value = [value];
+ else value = (typeof value == 'string') ? value.split(' ') : Array.from(value);
+ return value.map(function(val){
+ if(typeOf(val) != 'object') val = String(val);
+ else val = JSON.encode(val);
+ var found = false;
+ Object.each(Fx.Step.Parsers, function(parser, key){
+ if (found) return;
+ var parsed = parser.parse(val);
+ if (parsed || parsed === 0) found = {value: parsed, parser: parser};
+ });
+ found = found || {value: val, parser: Fx.Step.Parsers.String};
+ return found;
+ });
+ },
+ render: function(){
+
+ },
+ compute: function(from, to, delta){
+ from = this.parse(from);
+ to = this.parse(to);
+ if(typeOf(from) == 'array'){
+ var computed = [];
+ (Math.min(from.length, to.length)).times(function(i){
+ computed.push({value: from[i].parser.compute(from[i].value, to[i].value, delta), parser: from[i].parser});
+ });
+ computed.$family = Function.from('fx:step:value');
+ return computed;
+ }else return this.parent(from, to, delta);
+ },
+ });
+ Fx.Step.Parsers = {
+ Color : Fx.CSS.Parsers.Color,
+ 'Number' : Fx.CSS.Parsers.Number,
+ 'Transform' : {
+ parse: function(value){
+ return false;
+ },
+ compute: function(zero, one){
+ return one;
+ },
+ serve: function(zero){
+ return zero;
+ }
+ },
+ 'Object' : {
+ parse: function(value){
+ try{
+ var data = JSON.decode(value);
+ return data;
+ }catch(ex){
+ return false;
+ }
+ //return typeOf(value) == 'object'?value:false;
+ },
+ compute: function(old_object, new_object, delta){
+ var results = {};
+ Object.each(old_object, function(value, key){
+ if(new_object[key]) results[key] = Fx.compute(value, new_object[key], delta);
+ });
+ return results;
+ },
+ serve: function(value){
+ if(typeOf(value) == 'array' && value.length == 1) value = value[0];
+ return value;
+ }
+ },
+ 'Path' : {
+ parse: function(value){
+ var result = [];
+ if(!value.test(/^([A-Za-z]( [0-9]+ [0-9]+){1,} ?){1,}$/g)) return false;
+ var parts = value.split(/(?=[A-Za-z])/);
+ parts.each(function(part){
+ result.push(part.split(' '));
+ });
+ return (result.length == 0)?false:result;
+ },
+ compute: function(old_path, new_path, delta){
+ //console.log('compute', old_path, new_path, delta);
+ var new_path_item;
+ var path = [];
+ old_path.each(function(old_path_item, index){
+ new_path_item = new_path[index];
+ if(old_path_item[0] == new_path[index][0]){
+ var path_node = [];
+ path_node.push(old_path_item[0]);
+ if(old_path_item.length >= 3){ //two args
+ path_node.push(Math.floor(Fx.compute(Number.from(old_path_item[1]), Number.from(new_path_item[1]), delta)));
+ path_node.push(Math.floor(Fx.compute(Number.from(old_path_item[2]), Number.from(new_path_item[2]), delta)));
+ }
+ if(old_path_item.length >= 5){ //four args
+ path_node.push(Math.floor(Fx.compute(Number.from(old_path_item[3]), Number.from(new_path_item[3]), delta)));
+ path_node.push(Math.floor(Fx.compute(Number.from(old_path_item[4]), Number.from(new_path_item[4]), delta)));
+ }
+ if(old_path_item.length >= 7){ //six args
+ path_node.push(Math.floor(Fx.compute(Number.from(old_path_item[5]), Number.from(new_path_item[5]), delta)));
+ path_node.push(Math.floor(Fx.compute(Number.from(old_path_item[6]), Number.from(new_path_item[6]), delta)));
+ }
+ path.push(path_node);
+ }else{
+ //transition from one line type to another
+ }
+ });
+ return path;
+ },
+ serve: function(path){
+ var value = '';
+ path.map(function(node){
+ value += node.join(' ');
+ });
+ return value;
+ },
+ diff : function(old_path, new_path){
+
+ }
+ },
+ 'String' : Fx.CSS.Parsers.String
+ }
+ Fx.Step.Parsers.String.serve = function(value){
+ if(typeOf(value) == 'array') return value[0];
+ else return value;
+ }
+ Fx.Step.Parsers.Color.serve = Fx.Step.Parsers.String.serve;
+ Fx.Step.Parsers.Number.serve = Fx.Step.Parsers.String.serve;
+})();
+
Something went wrong with that request. Please try again.