/* * MotionGuidePlugin * Visit http://createjs.com/ for documentation, updates and examples. * * Copyright (c) 2010 gskinner.com, inc. * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ /** * @module TweenJS */ // namespace: this.createjs = this.createjs||{}; (function() { "use strict"; /** * A TweenJS plugin for working with motion guides. Defined paths which objects can follow or orient along. * * To use the plugin, install the plugin after TweenJS has loaded. To define a path, add * * createjs.MotionGuidePlugin.install(); * * <h4>Example</h4> * * // Using a Motion Guide * createjs.Tween.get(target).to({guide:{ path:[0,0, 0,200,200,200, 200,0,0,0] }},7000); * // Visualizing the line * graphics.moveTo(0,0).curveTo(0,200,200,200).curveTo(200,0,0,0); * * Each path needs pre-computation to ensure there's fast performance. Because of the pre-computation there's no * built in support for path changes mid tween. These are the Guide Object's properties:<UL> * <LI> path: Required, Array : The x/y points used to draw the path with a moveTo and 1 to n curveTo calls.</LI> * <LI> start: Optional, 0-1 : Initial position, default 0 except for when continuing along the same path.</LI> * <LI> end: Optional, 0-1 : Final position, default 1 if not specified.</LI> * <LI> orient: Optional, string : "fixed"/"auto"/"cw"/"ccw"<UL> * <LI>"fixed" forces the object to face down the path all movement (relative to start rotation),</LI> * <LI>"auto" rotates the object along the path relative to the line.</LI> * <LI>"cw"/"ccw" force clockwise or counter clockwise rotations including Adobe Flash/Animate-like * behaviour. This may override your end rotation value.</LI> * </UL></LI> * </UL> * Guide objects should not be shared between tweens even if all properties are identical, the library stores * information on these objects in the background and sharing them can cause unexpected behaviour. Values * outside 0-1 range of tweens will be a "best guess" from the appropriate part of the defined curve. * * @class MotionGuidePlugin * @constructor */ function MotionGuidePlugin() { throw("MotionGuidePlugin cannot be instantiated.") } var s = MotionGuidePlugin; // static properties: /** * @property priority * @protected * @static */ s.priority = 0; // high priority, should run sooner /** * READ-ONLY. A unique identifying string for this plugin. Used by TweenJS to ensure duplicate plugins are not installed on a tween. * @property ID * @type {String} * @static * @readonly */ s.ID = "MotionGuide"; // static methods /** * Installs this plugin for use with TweenJS. Call this once after TweenJS is loaded to enable this plugin. * @method install * @static */ s.install = function() { createjs.Tween._installPlugin(MotionGuidePlugin); return createjs.Tween.IGNORE; }; /** * Called by TweenJS when a new property initializes on a tween. * See {{#crossLink "SamplePlugin/init"}}{{/crossLink}} for more info. * @method init * @param {Tween} tween * @param {String} prop * @param {any} value * @return {any} * @static */ s.init = function(tween, prop, value) { if(prop == "guide") { tween._addPlugin(s); } }; /** * Called when a new step is added to a tween (ie. a new "to" action is added to a tween). * See {{#crossLink "SamplePlugin/step"}}{{/crossLink}} for more info. * @method step * @param {Tween} tween * @param {TweenStep} step * @param {Object} props * @static */ s.step = function(tween, step, props) { for (var n in props) { if(n !== "guide") { continue; } var guideData = step.props.guide; var error = s._solveGuideData(props.guide, guideData); guideData.valid = !error; var end = guideData.endData; tween._injectProp("x", end.x); tween._injectProp("y", end.y); if(error || !guideData.orient) { break; } var initRot = step.prev.props.rotation === undefined ? (tween.target.rotation || 0) : step.prev.props.rotation; guideData.startOffsetRot = initRot - guideData.startData.rotation; if(guideData.orient == "fixed") { // controlled rotation guideData.endAbsRot = end.rotation + guideData.startOffsetRot; guideData.deltaRotation = 0; } else { // interpreted rotation var finalRot = props.rotation === undefined ? (tween.target.rotation || 0) : props.rotation; var deltaRot = (finalRot - guideData.endData.rotation) - guideData.startOffsetRot; var modRot = deltaRot % 360; guideData.endAbsRot = finalRot; switch(guideData.orient) { case "auto": guideData.deltaRotation = deltaRot; break; case "cw": guideData.deltaRotation = ((modRot + 360) % 360) + (360 * Math.abs((deltaRot/360) |0)); break; case "ccw": guideData.deltaRotation = ((modRot - 360) % 360) + (-360 * Math.abs((deltaRot/360) |0)); break; } } tween._injectProp("rotation", guideData.endAbsRot); } }; /** * Called before a property is updated by the tween. * See {{#crossLink "SamplePlugin/change"}}{{/crossLink}} for more info. * @method change * @param {Tween} tween * @param {TweenStep} step * @param {String} prop * @param {any} value * @param {Number} ratio * @param {Boolean} end * @return {any} * @static */ s.change = function(tween, step, prop, value, ratio, end) { var guideData = step.props.guide; if( !guideData || // Missing data (step.props === step.prev.props) || // In a wait() (guideData === step.prev.props.guide) // Guide hasn't changed ) { return; // have no business making decisions } if( (prop === "guide" && !guideData.valid) || // this data is broken (prop == "x" || prop == "y") || // these always get over-written (prop === "rotation" && guideData.orient) // currently over-written ){ return createjs.Tween.IGNORE; } s._ratioToPositionData(ratio, guideData, tween.target); }; // public methods /** * Provide potentially useful debugging information, like running the error detection system, and rendering the path * defined in the guide data. * * NOTE: you will need to transform your context 2D to the local space of the guide if you wish to line it up. * @param {Object} guideData All the information describing the guide to be followed. * @param {DrawingContext2D} [ctx=undefined] The context to draw the object into. * @param {Array} [higlight=undefined] Array of ratio positions to highlight * @returns {undefined|String} */ s.debug = function(guideData, ctx, higlight) { guideData = guideData.guide || guideData; // errors var err = s._findPathProblems(guideData); if(err) { console.error("MotionGuidePlugin Error found: \n" + err); } // drawing if(!ctx){ return err; } var i; var path = guideData.path; var pathLength = path.length; var width = 3; var length = 9; ctx.save(); //ctx.resetTransform(); ctx.lineCap = "round"; ctx.lineJoin = "miter"; ctx.beginPath(); // curve ctx.moveTo(path[0], path[1]); for(i=2; i < pathLength; i+=4) { ctx.quadraticCurveTo( path[i], path[i+1], path[i+2], path[i+3] ); } ctx.strokeStyle = "black"; ctx.lineWidth = width*1.5; ctx.stroke(); ctx.strokeStyle = "white"; ctx.lineWidth = width; ctx.stroke(); ctx.closePath(); // highlights var hiCount = higlight.length; if(higlight && hiCount) { var tempStore = {}; var tempLook = {}; s._solveGuideData(guideData, tempStore); for(var i=0; i<hiCount; i++){ tempStore.orient = "fixed"; s._ratioToPositionData(higlight[i], tempStore, tempLook); ctx.beginPath(); ctx.moveTo(tempLook.x, tempLook.y); ctx.lineTo( tempLook.x + Math.cos(tempLook.rotation * 0.0174533) * length, tempLook.y + Math.sin(tempLook.rotation * 0.0174533) * length ); ctx.strokeStyle = "black"; ctx.lineWidth = width*1.5; ctx.stroke(); ctx.strokeStyle = "red"; ctx.lineWidth = width; ctx.stroke(); ctx.closePath(); } } // end draw ctx.restore(); return err; }; // private methods /** * Calculate and store optimization data about the desired path to improve performance and accuracy of positions. * @param {Object} source The guide data provided to the tween call * @param {Object} storage the guide data used by the step calls and plugin to do the job, will be overwritten * @returns {undefined|String} Can return an error if unable to generate the data. * @private */ s._solveGuideData = function(source, storage) { var err = undefined; if(err = s.debug(source)) { return err; } var path = storage.path = source.path; var orient = storage.orient = source.orient; storage.subLines = []; storage.totalLength = 0; storage.startOffsetRot = 0; storage.deltaRotation = 0; storage.startData = {ratio: 0}; storage.endData = {ratio: 1}; storage.animSpan = 1; var pathLength = path.length; var precision = 10; var sx,sy, cx,cy, ex,ey, i,j, len, temp = {}; sx = path[0]; sy = path[1]; // get the data for each curve for(i=2; i < pathLength; i+=4) { cx = path[i]; cy = path[i+1]; ex = path[i+2]; ey = path[i+3]; var subLine = { weightings: [], estLength: 0, portion: 0 }; var subX = sx, subY = sy; // get the distance data for each point for(j=1; j <= precision;j++) { // we need to evaluate t = 1 not t = 0 s._getParamsForCurve(sx,sy, cx,cy, ex,ey, j/precision, false, temp); var dx = temp.x - subX, dy = temp.y - subY; len = Math.sqrt(dx*dx + dy*dy); subLine.weightings.push(len); subLine.estLength += len; subX = temp.x; subY = temp.y; } // figure out full lengths storage.totalLength += subLine.estLength; // use length to figure out proportional weightings for(j=0; j < precision; j++) { len = subLine.estLength; subLine.weightings[j] = subLine.weightings[j] / len; } storage.subLines.push(subLine); sx = ex; sy = ey; } // use length to figure out proportional weightings len = storage.totalLength; var l = storage.subLines.length; for(i=0; i<l; i++) { storage.subLines[i].portion = storage.subLines[i].estLength / len; } // determine start and end data var startRatio = isNaN(source.start) ? 0 : source.start; var endRatio = isNaN(source.end) ? 1 : source.end; s._ratioToPositionData(startRatio, storage, storage.startData); s._ratioToPositionData(endRatio, storage, storage.endData); // this has to be done last else the prev ratios will be out of place storage.startData.ratio = startRatio; storage.endData.ratio = endRatio; storage.animSpan = storage.endData.ratio - storage.startData.ratio; }; /** * Convert a percentage along the line into, a local line (start, control, end) t-value for calculation. * @param {Number} ratio The (euclidean distance) percentage into the whole curve. * @param {Object} guideData All the information describing the guide to be followed. * @param {Object} output Object to save output properties of x,y, and rotation onto. * @returns {Object} The output object, useful for isolated calls. * @private */ s._ratioToPositionData = function(ratio, guideData, output) { var lineSegments = guideData.subLines; var i,l, t, test, target; var look = 0; var precision = 10; var effRatio = (ratio * guideData.animSpan) + guideData.startData.ratio; // find subline l = lineSegments.length; for(i=0; i<l; i++) { test = lineSegments[i].portion; if(look + test >= effRatio){ target = i; break; } look += test; } if(target === undefined) { target = l-1; look -= test; } // find midline weighting var subLines = lineSegments[target].weightings; var portion = test; l = subLines.length; for(i=0; i<l; i++) { test = subLines[i] * portion; if(look + test >= effRatio){ break; } look += test; } // translate the subline index into a position in the path data target = (target*4) + 2; // take the distance we've covered in our ratio, and scale it to distance into the weightings t = (i/precision) + (((effRatio-look) / test) * (1/precision)); // position var pathData = guideData.path; s._getParamsForCurve( pathData[target-2], pathData[target-1], pathData[target], pathData[target+1], pathData[target+2], pathData[target+3], t, guideData.orient, output ); if(guideData.orient) { if(ratio >= 0.99999 && ratio <= 1.00001 && guideData.endAbsRot !== undefined) { output.rotation = guideData.endAbsRot; } else { output.rotation += guideData.startOffsetRot + (ratio * guideData.deltaRotation); } } return output; }; /** * For a given quadratic bezier t-value, what is the position and rotation. Save it onto the output object. * @param {Number} sx Start x. * @param {Number} sy Start y. * @param {Number} cx Control x. * @param {Number} cy Control y. * @param {Number} ex End x. * @param {Number} ey End y. * @param {Number} t T value (parametric distance into curve). * @param {Boolean} orient Save rotation data. * @param {Object} output Object to save output properties of x,y, and rotation onto. * @private */ s._getParamsForCurve = function(sx,sy, cx,cy, ex,ey, t, orient, output) { var inv = 1 - t; // finding a point on a bezier curve output.x = inv*inv * sx + 2 * inv * t * cx + t*t * ex; output.y = inv*inv * sy + 2 * inv * t * cy + t*t * ey; // finding an angle on a bezier curve if(orient) { // convert from radians back to degrees output.rotation = 57.2957795 * Math.atan2( (cy - sy)*inv + (ey - cy)*t, (cx - sx)*inv + (ex - cx)*t ); } }; /** * Perform a check to validate path information so plugin can avoid later error checking. * @param {Object} guideData All the information describing the guide to be followed. * @returns {undefined|String} The problem found, or undefined if no problems. * @private */ s._findPathProblems = function(guideData) { var path = guideData.path; var valueCount = (path && path.length) || 0; // ensure this is a number to simplify later logic if(valueCount < 6 || (valueCount-2) % 4) { var message = "\tCannot parse 'path' array due to invalid number of entries in path. "; message += "There should be an odd number of points, at least 3 points, and 2 entries per point (x & y). "; message += "See 'CanvasRenderingContext2D.quadraticCurveTo' for details as 'path' models a quadratic bezier.\n\n"; message += "Only [ "+ valueCount +" ] values found. Expected: "+ Math.max(Math.ceil((valueCount-2)/4)*4+2, 6); //6, 10, 14,... return message; } for(var i=0; i<valueCount; i++) { if(isNaN(path[i])){ return "All data in path array must be numeric"; } } var start = guideData.start; if(isNaN(start) && !(start === undefined)/* || start < 0 || start > 1*/) { // outside 0-1 is unpredictable, but not breaking return "'start' out of bounds. Expected 0 to 1, got: "+ start; } var end = guideData.end; if(isNaN(end) && (end !== undefined)/* || end < 0 || end > 1*/) { // outside 0-1 is unpredictable, but not breaking return "'end' out of bounds. Expected 0 to 1, got: "+ end; } var orient = guideData.orient; if(orient) { // mirror the check used elsewhere if(orient != "fixed" && orient != "auto" && orient != "cw" && orient != "ccw") { return 'Invalid orientation value. Expected ["fixed", "auto", "cw", "ccw", undefined], got: '+ orient; } } return undefined; }; createjs.MotionGuidePlugin = MotionGuidePlugin; }());