UNPKG

keyshapejs

Version:

KeyshapeJS animation library

1,482 lines (1,336 loc) 53.9 kB
/** @license KeyshapeJS v1.3.0 (c) 2018-2023 Pixofield Ltd | pixofield.com/keyshapejs/mit-license */ window['KeyshapeJS'] = (function () { /* module-start */ function ERR(msg) { return Error(msg); } var NOT_IN_LIST_EXCEPTION = ERR("Not in timeline list"); // Properties with predefined names and types var P_MOTION_DISTANCE = 0; var P_POSITIONX = 1; var P_POSITIONY = 2; var P_ROTATE = 6; var P_SKEWX = 7; var P_SKEWY = 8; var P_SCALEX = 10; var P_SCALEY = 11; var P_ANCHORX = 13; var P_ANCHORY = 14; // names for animate() var transformProps = [ "mpath", "posX", "posY", "", "", "", "rotate", "skewX", "skewY", "", "scaleX", "scaleY", "", "anchorX", "anchorY" ]; // CSS transform names var transformNames = [ "", "translate", "translate", "", "", "", "rotate", "skewX", "skewY", "", "scale", "scale", "", "translate", "translate" ]; // Filter function types var FILTER_NONE = 0; var FILTER_URL = 1; var FILTER_BLUR = 2; //UNUSED var FILTER_BRIGHTNESS = 3; //UNUSED var FILTER_CONTRAST = 4; var FILTER_DROPSHADOW = 5; //UNUSED var FILTER_GRAYSCALE = 6; var FILTER_HUEROTATE = 7; //UNUSED var FILTER_INVERT = 8; //UNUSED var FILTER_OPACITY = 9; //UNUSED var FILTER_SATURATE = 10; //UNUSED var FILTER_SEPIA = 11; // Filter function names, the order matching the types var filterNames = [ "none", "url", "blur", "brightness", "contrast", "drop-shadow", "grayscale", "hue-rotate", "invert", "opacity", "saturate", "sepia" ]; // Indexes to pdata internal array var INX_PROP = 0; var INX_FLAGS = 1; var INX_STARTTIME = 2; var INX_ITERATIONDUR = 3; // begin+iteration dur = iteration end var INX_TIMES = 4; var INX_VALUES = 5; var INX_EASING = 6; //UNUSED var INX_ITERATIONS = 7; var INX_MPATH = 8; var INX_FILL = 9; // INX_MPATH and elements with motion path have this data var MPATH_AUTOROTATE = 0; var MPATH_ELEMENT = 1; var MPATH_PATHLENGTH = 2; // values for INX_FLAGS // bits 0-1: target types var FLAG_TARGET_CSS_PROPERTY = 0x00; var FLAG_TARGET_ATTRIBUTE = 0x01; // bits 4-7: property types var FLAG_TYPE_STRING = 0x00; var FLAG_TYPE_NUMBER = 0x10; var FLAG_TYPE_LENGTH = 0x20; var FLAG_TYPE_COLOR = 0x30; var FLAG_TYPE_LENGTH_LIST = 0x40; var FLAG_TYPE_FILTER = 0x50; var FLAG_TYPE_PATH = 0x60; var FLAG_TYPE_VISIBILITY = 0x70; // Timing function values var TIMING_FN_LINEAR = 0; var TIMING_FN_CUBIC = 1; var TIMING_FN_STEP_START = 2; var TIMING_FN_STEP_END = 3; // fill values var FILL_BACKWARDS = 1; var FILL_FORWARDS = 2; // play states var STATE_IDLE = "idle"; var STATE_PAUSED = "paused"; var STATE_RUNNING = "running"; var STATE_FINISHED = "finished"; // pending callback constants var PENDING_NONE = 0; var PENDING_ONFINISH = 1; var PENDING_ONLOOP = 2; var reqAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || null; if (!reqAnimationFrame) { // IE9 needs setTimeout() reqAnimationFrame = function(cb) { window.setTimeout(cb, 16); }; } var isWebkit = /apple/i.test(navigator.vendor); function isSet(value) { return typeof value !== "undefined"; } function startsWith(str, s) { return str && str.indexOf(s) === 0; } function copyArray(arr) { return arr.slice(); } function copyMap(map) { var res = {}; for (var key in map) { res[key] = map[key]; } return res; } function removeItemFromArray(arr, item) { var i = arr.indexOf(item); if (i > -1) { arr.splice(i, 1); } } function checkIsFinite(value) { if (!isFinite(value)) { throw ERR("Non-finite value"); } } function setAttr(elem, attrName, val) { elem.setAttribute(attrName, val); } // properties with special flags var propertyData = { "d": FLAG_TARGET_ATTRIBUTE | FLAG_TYPE_PATH, "fill": FLAG_TARGET_CSS_PROPERTY | FLAG_TYPE_COLOR, "fillOpacity": FLAG_TARGET_CSS_PROPERTY | FLAG_TYPE_NUMBER, "filter": FLAG_TARGET_CSS_PROPERTY | FLAG_TYPE_FILTER, "height": FLAG_TARGET_ATTRIBUTE | FLAG_TYPE_LENGTH, "opacity": FLAG_TARGET_CSS_PROPERTY | FLAG_TYPE_NUMBER, "offsetDistance": FLAG_TARGET_ATTRIBUTE | FLAG_TYPE_LENGTH, "rx": FLAG_TARGET_ATTRIBUTE | FLAG_TYPE_LENGTH, "ry": FLAG_TARGET_ATTRIBUTE | FLAG_TYPE_LENGTH, "stroke": FLAG_TARGET_CSS_PROPERTY | FLAG_TYPE_COLOR, "strokeDasharray": FLAG_TARGET_CSS_PROPERTY | FLAG_TYPE_LENGTH_LIST, "strokeDashoffset": FLAG_TARGET_CSS_PROPERTY | FLAG_TYPE_LENGTH, "strokeOpacity": FLAG_TARGET_CSS_PROPERTY | FLAG_TYPE_NUMBER, "strokeWidth": FLAG_TARGET_CSS_PROPERTY | FLAG_TYPE_LENGTH, "transform": FLAG_TARGET_ATTRIBUTE | FLAG_TYPE_STRING, "visibility": FLAG_TARGET_ATTRIBUTE | FLAG_TYPE_VISIBILITY, "width": FLAG_TARGET_ATTRIBUTE | FLAG_TYPE_LENGTH }; function isValidProperty(prop) { // is it a string without a dash return typeof prop === "string" && prop.indexOf("-") === -1 && prop !== "" && (propertyData[prop] > 0 || transformProps.indexOf(prop) >= 0); } // returns property type for the given property and its value function propertyType(flags) { return flags & 0xf0; } var allCssProps = window.getComputedStyle(document.documentElement); function propertyFlags(prop) { if (prop <= P_ANCHORY) { return FLAG_TARGET_CSS_PROPERTY | FLAG_TYPE_NUMBER; } var flags = propertyData[prop]; if (!flags) { // check if it is a css property or an attribute if (isSet(allCssProps[prop])) { flags = FLAG_TARGET_CSS_PROPERTY; flags |= (prop.toLowerCase().indexOf("color") === prop.length-5 ? FLAG_TYPE_COLOR : FLAG_TYPE_STRING); } else { flags = FLAG_TARGET_ATTRIBUTE | FLAG_TYPE_STRING; } } return flags; } function cubeRoot(a) { return a >= 0 ? Math.pow(a, 1/3) : -Math.pow(-a, 1/3); } // Solves quadratic equation ax^2 + bx + c = 0 and returns result between [0, 1] function solveRestrictedQuadraticEquation(a, b, c) { if (a === 0) return -c/b; var sqrtDisc = Math.sqrt(b*b - 4.0*a*c); var x = (-b + sqrtDisc) / (2.0*a); if (x >= 0 && x <= 1) return x; x = (-b - sqrtDisc) / (2.0*a); if (x >= 0 && x <= 1) return x; // NOTREACHED return 0; } // tested manually (values from http://www.1728.org/cubic2.htm): // solveRestrictedCubicEquation(2, -4, -22, 24); x1=4, x2=-3, x3=1 // solveRestrictedCubicEquation(3, -10, 14, 27); x1=-1, x2=x3=2.16 // solveRestrictedCubicEquation(1, 6, 12, 8); x1=x2=x3=-2 // Solves cubic equation ax^3 + bx^2 + cx + d = 0 and returns the real part of the result // between [0, 1] function solveRestrictedCubicEquation(a, b, c, d) { // Based on cubic equation solver // theory: http://www.1728.org/cubic2.htm if (a === 0) { return solveRestrictedQuadraticEquation(b, c, d); } var f = c/a - ((b*b)/(a*a))/3.0; var g = ((b*b*b)/(a*a*a))/13.5 - b*c/(a*a)/3.0 + d/a; var h = g*g/4.0 + f*f*f/27.0; var p = -b/(3.0*a); if (h <= 0) { // 3 real roots if (f === 0 && g === 0) { // all roots equal // this code avoids possible divide by zero error when evaluating k below return -cubeRoot(d/a); // x1, x2, x3, i = 0 } else { // 3 real unequal roots var j = Math.sqrt(g*g/4.0 - h); var k = Math.acos(-g/2.0/j); var m = Math.cos(k/3.0); var n = Math.sqrt(3.0)*Math.sin(k/3.0); j = cubeRoot(j); var x = 2.0*j*m + p; // x1, i=0 if (x >= 0 && x <= 1) return x; x = -j*(m+n) + p; // x2, i=0 if (x >= 0 && x <= 1) return x; x = j*(n-m) + p; // x3, i=0 if (x >= 0 && x <= 1) return x; } } else { // 2 complex roots var r = -g/2.0 + Math.sqrt(h); var s = cubeRoot(r); var t = -g/2.0 - Math.sqrt(h); var u = cubeRoot(t); var xx = s + u + p; // x1, i = 0 if (xx >= 0 && xx <= 1) return xx; xx = -(s + u)/2.0 + p; // x2, x3, i = +-(s - u)*sqrt(3.0)/2 if (xx >= 0 && xx <= 1) return xx; } // NOTREACHED return 0; } // calculate y for x on a cubic bezier defined by cbdata [CB, p1x, p1y, p2x, p2y] function cubicBezierY(cbdata, x) { if (x <= 0) return 0; if (x >= 1) return 1; // calculate t for x on a cubic bezier 0.0, x1, x2, 1.0 var x1 = cbdata[1]; var x2 = cbdata[3]; var a = 3*x1 - 3*x2 + 1; var b = -6*x1 + 3*x2; var c = 3*x1; var d = - x; var t = solveRestrictedCubicEquation(a, b, c, d); // get cubic curve y coordinate for t, p1y and p2y (p0y = 0 and p3y = 1) return /*(1-t)*(1-t)*(1-t)*0 +*/ 3*t*(1-t)*(1-t)*cbdata[2] + 3*t*t*(1-t)*cbdata[4] + t*t*t; } function getCurrentTimeIndex(simpleTime, pdata) { var starttime = pdata[INX_STARTTIME]; simpleTime += starttime; var tlen = pdata[INX_TIMES].length; for (var i = 0; i < tlen; ++i) { if (simpleTime < pdata[INX_TIMES][i]) { return i; } } return tlen-1; } // converts the given raw value to an svg value (possibly a string) function convertToSvgValue(type, val) { // non-numeric colors are returned as such if (type === FLAG_TYPE_COLOR && typeof val === 'number') { return "rgba("+(val>>>24)+","+((val>>>16)&255)+","+((val>>>8)&255)+","+((val&255)/255)+")"; } else if (type === FLAG_TYPE_LENGTH_LIST) { val = val.map(function(v) { return v+"px"; }); return val.join(","); } else if (type === FLAG_TYPE_PATH) { var path = ""; var len = val.length; for (var ip = 0; ip < len; ip += 2) { path += val[ip]; // command path += val[ip+1].join(','); // values } return path; } else if (type === FLAG_TYPE_FILTER) { if (val[0] === FILTER_NONE) { return "none"; } var filters = ""; var flen = val.length; var i = 0; while (i < flen) { filters += filterNames[val[i]]; if (val[i] === FILTER_URL) { filters += "("+val[i+1]+") "; } else if (val[i] === FILTER_DROPSHADOW) { var r = val[i+4] >>> 24; // triple >>> to get unsigned shift var g = (val[i+4] >> 16) & 255; var b = (val[i+4] >> 8) & 255; var a = val[i+4] & 255; filters += "("+val[i+1]+"px "+val[i+2]+"px "+val[i+3]+"px rgba("+ r+","+g+","+b+","+(a/255)+")) "; i += 3; } else if (val[i] === FILTER_BLUR) { filters += "("+val[i+1]+"px) "; } else if (val[i] === FILTER_HUEROTATE) { filters += "("+val[i+1]+"deg) "; } else { // other filters // clamp negative values, like all browsers seems to be doing for these values filters += "("+(val[i+1]< 0 ? 0 : val[i+1])+") "; } i += 2; } return filters; } else if (type === FLAG_TYPE_LENGTH) { return val+"px"; } return val; } function clampColor(n) { return n <= 0 ? 0 : n >= 255 ? 255 : n; } // interpolates between the given values v1 and v2 using t [0, 1] function interpolate(type, v1, v2, t) { if (type === FLAG_TYPE_NUMBER || type === FLAG_TYPE_LENGTH) { // number or length return (v2-v1) * t + v1; } if (type === FLAG_TYPE_STRING) { return t < 0.5 ? v1 : v2; } if (type === FLAG_TYPE_VISIBILITY) { if (t <= 0) { return v1; } else if (t >= 1) { return v2; } else { return "visible"; } } if (type === FLAG_TYPE_COLOR) { if (typeof v1 === 'number' && typeof v2 === 'number') { // interpolate rgba colors var nt = 1-t; var r = nt*(v1>>>24) + t*(v2>>>24); var g = nt*((v1>>>16)&255) + t*((v2>>>16)&255); var b = nt*((v1>>>8)&255) + t*((v2>>>8)&255); var a = nt*(v1&255) + t*(v2&255); return ((clampColor(r)<<24) | (clampColor(g)<<16) | (clampColor(b)<<8) | clampColor(a)); } // gradients are like strings return t < 0.5 ? v1 : v2; } if (type === FLAG_TYPE_LENGTH_LIST) { // if either is empty ("none"), then use value zero if (v1.length === 0) { v1 = [ 0 ]; } if (v2.length === 0) { v2 = [ 0 ]; } var lreslen = v1.length; // different sizes will create from*to items to match browser behaviour if (v1.length !== v2.length) { lreslen = v1.length * v2.length; } var lres = []; for (var li = 0; li < lreslen; ++li) { var fromVal = v1[li % v1.length]; var toVal = v2[li % v2.length]; var rv = (toVal - fromVal) * t + fromVal; if (rv < 0) { rv = 0; } lres.push(rv); } return lres; } if (type === FLAG_TYPE_PATH) { if (v1.length !== v2.length) { return t < 0.5 ? v1 : v2; } var preslen = v1.length; var pres = []; for (var pi = 0; pi < preslen; pi += 2) { if (v1[pi] !== v2[pi]) { // commands must match return t < 0.5 ? v1 : v2; } pres[pi] = v1[pi]; // coordinate data pres[pi+1] = []; for (var j = 0; j < v1[pi+1].length; ++j) { var newval = (v2[pi+1][j]-v1[pi+1][j]) * t + v1[pi+1][j]; pres[pi+1].push(newval); } } return pres; } if (type === FLAG_TYPE_FILTER) { var freslen = v1.length; if (freslen !== v2.length) { // TODO: extra filters should be appended to the shorter list return t < 0.5 ? v1 : v2; } var fres = []; var fi = 0; while (fi < freslen) { if (v1[fi] !== v2[fi] || v1[fi] === FILTER_URL) { return t < 0.5 ? v1 : v2; } fres[fi] = v1[fi]; fres[fi+1] = (v2[fi+1]-v1[fi+1]) * t + v1[fi+1]; if (v1[fi] === FILTER_DROPSHADOW) { // multiple values and color for drop-shadow fres[fi+2] = (v2[fi+2]-v1[fi+2]) * t + v1[fi+2]; fres[fi+3] = (v2[fi+3]-v1[fi+3]) * t + v1[fi+3]; // interpolate rgba colors var fnt = 1-t; var c1 = v1[fi+4]; var c2 = v2[fi+4]; var fr = fnt*(c1>>>24) + t*(c2>>>24); // triple >>> to get unsigned shift var fg = fnt*((c1>>16)&255) + t*((c2>>16)&255); var fb = fnt*((c1>>8)&255) + t*((c2>>8)&255); var fa = fnt*(c1&255) + t*(c2&255); fres[fi+4] = ((clampColor(fg)<<16) | (clampColor(fb)<<8) | clampColor(fa)) + (clampColor(fr)|0)*16777216; fi += 3; } fi += 2; } return fres; } return 0; } // returns an interpolated raw value for the given pdata function animateProperty(simpleTime, pdata) { var ti = getCurrentTimeIndex(simpleTime, pdata); var starttime = pdata[INX_STARTTIME]; var t1 = pdata[INX_TIMES][ti-1] - starttime; var t2 = pdata[INX_TIMES][ti] - starttime; var t = (simpleTime-t1) / (t2-t1); // apply easing (timing function) if needed if (pdata[INX_EASING] && pdata[INX_EASING].length > ti-1) { var v = pdata[INX_EASING][ti-1]; // TIMING_FN_LINEAR doesn't need processing, t = t if (v[0] === TIMING_FN_CUBIC) { t = cubicBezierY(v, t); } else if (v[0] === TIMING_FN_STEP_START) { var stepsStart = v[1]; t = Math.ceil(t*stepsStart)/stepsStart; } else if (v[0] === TIMING_FN_STEP_END) { var stepsEnd = v[1]; t = Math.floor(t*stepsEnd)/stepsEnd; } } var v1 = pdata[INX_VALUES][ti-1]; var v2 = pdata[INX_VALUES][ti]; var rawval = interpolate(propertyType(pdata[INX_FLAGS]), v1, v2, t); return rawval; } // true if the requestAnimationFrame is running var isTicking = false; // timeline time var sysTimelineTime = new Date().getTime(); // timeline hold time after global pause, undefined if not paused var holdTimelineTime; // timeline adjust time, which is affected by global pause var sysTimelineTimeDrift = 0; // all timelines var globalTimelineList = []; // list of pending callbacks // values: [ timeline, PENDING_ONFINISH ] or [ timeline, PENDING_ONLOOP ] var pendingCallbacks = []; function updateSysTimelineTime() { if (holdTimelineTime) { return; } sysTimelineTime = new Date().getTime() + sysTimelineTimeDrift; } function updateAllAnimations(mainUpdate) { if (!mainUpdate && isTicking) { // no update if mainloop will do it return; } var hasActiveTimelines = false; for (var myi = 0; myi < globalTimelineList.length; ++myi) { var curTl = globalTimelineList[myi]; if (curTl._updateValues(mainUpdate)) { hasActiveTimelines = true; } } if (mainUpdate) { // call pending callbacks (onloop and onfinish) and do autoremove for finished ones while (pendingCallbacks.length > 0) { var item = pendingCallbacks.shift(); var tl = item[0]; if (item[1] === PENDING_ONFINISH) { if (tl['onfinish']) { // calling as a method sets the timeline object as 'this' tl['onfinish'](); // keep hasActiveTimelines for one more requestAnimationFrame so that // play() etc. in onfinish callback keeps the timeline running hasActiveTimelines = true; } tl._performAutoRemove(); } else if (item[1] === PENDING_ONLOOP) { if (tl['onloop']) { // calling as a method sets the timeline object as 'this' tl['onloop'](); } } } } return hasActiveTimelines; } // mainloop animates elements in DOM and calls again reqAnimationFrame if needed function mainloop() { updateSysTimelineTime(); // isTicking can be set to false or true below // note: onfinish callback may add active animations, in that case isTicking should stay true if (updateAllAnimations(true) && !holdTimelineTime) { isTicking = true; reqAnimationFrame(mainloop); } else { isTicking = false; } }; function startTicking() { if (!isTicking) { isTicking = true; reqAnimationFrame(mainloop); } }; function parseFloatList(str, separator) { var parts = str.split(separator); var res = []; parts.forEach(function(v) { res.push(parseFloat(v)); }); return res; } function parseCommaSeparated(str) { // IE uses space as separator in transform attribute, so convert them to comma before parsing if (str.indexOf(',') === -1) { str = str.replace(' ', ','); } return parseFloatList(str, ','); } function parseTransform(elem) { if (!elem._ks) { elem._ks = { }; } if (elem._ks.transform) { return; } var trans = elem._ks.transform = []; for (var i = 0 ; i <= P_ANCHORY ; ++i) { trans[i] = 0; } trans[P_SCALEX] = 1; trans[P_SCALEY] = 1; // split transforms with ') ' because IE returns 'translate(x y)' while // other browsers return 'translate(x,y)' var transAttr = elem.getAttribute("transform"); if (!transAttr) { return; } var parts = transAttr.trim().split(') '); // skips anchor translate and seeks to position translate to remove extra tranforms before it for (var j = parts.length-2; j >= 0; --j) { if (startsWith(parts[j], "translate(")) { for (var k = 0; k < j; k++) { parts.shift(); } break; } } var first = parts.shift(); var vals; if (startsWith(first, "translate(")) { vals = parseCommaSeparated(first.substring(10)); trans[P_POSITIONX] = vals[0]; trans[P_POSITIONY] = isSet(vals[1]) ? vals[1] : 0; // IE may have only one translate value first = parts.shift(); } if (startsWith(first, "rotate(")) { vals = parseCommaSeparated(first.substring(7)); trans[P_ROTATE] = vals[0]; first = parts.shift(); } if (startsWith(first, "skewX(")) { vals = parseCommaSeparated(first.substring(6)); trans[P_SKEWX] = vals[0]; first = parts.shift(); } if (startsWith(first, "skewY(")) { vals = parseCommaSeparated(first.substring(6)); trans[P_SKEWY] = vals[0]; first = parts.shift(); } if (startsWith(first, "scale(")) { vals = parseCommaSeparated(first.substring(6)); trans[P_SCALEX] = vals[0]; trans[P_SCALEY] = isSet(vals[1]) ? vals[1] : vals[0]; first = parts.shift(); } if (startsWith(first, "translate(")) { vals = parseCommaSeparated(first.substring(10)); trans[P_ANCHORX] = vals[0]; trans[P_ANCHORY] = isSet(vals[1]) ? vals[1] : 0; } } function composeAndSetTransformAttr(elem) { parseTransform(elem); var trans = elem._ks.transform; var transform = ""; var mpath = elem._ks['mpath']; if (mpath) { // use the interpolated distance to get point on motion path var dist = trans[P_MOTION_DISTANCE]; if (dist < 0) { dist = 0; } // old SVG1.1 browsers don't clamp getPointAtLength(dist) if (dist > 100) { dist = 100; } dist = dist * mpath[MPATH_PATHLENGTH] / 100; var pt = mpath[MPATH_ELEMENT].getPointAtLength(dist); transform = "translate("+pt.x+","+pt.y+") "; // auto motion-rotate if (mpath[MPATH_AUTOROTATE]) { // get tangent of path by sampling another point just before or after var prevpt; if (dist < 0.5) { // tangent of start point 0 and point at 0.5 prevpt = pt; pt = mpath[MPATH_ELEMENT].getPointAtLength(0.5); } else { // get point before dist prevpt = mpath[MPATH_ELEMENT].getPointAtLength(dist-0.5); } var deg = Math.atan2(pt.y - prevpt.y, pt.x - prevpt.x) * 180 / Math.PI; transform += "rotate("+deg+") "; } } for (var prop = P_POSITIONX; prop < trans.length; ++prop) { var val = trans[prop]; var defaultVal = (prop === P_SCALEX || prop === P_SCALEY) ? 1 : 0; if (val !== defaultVal) { transform += " "+transformNames[prop]+"("; if (prop <= P_POSITIONY) { // position x and y transform += (prop === P_POSITIONX) ? val+",0" : "0,"+val; } else if (prop >= P_ANCHORX) { // anchor x and y transform += (prop === P_ANCHORX) ? val+",0" : "0,"+val; } else if (prop >= P_SCALEX) { // scale x and y transform += (prop === P_SCALEX) ? val+",1" : "1,"+val; } else { transform += val; // skew x/y or rotate } transform += ")"; } } setAttr(elem, "transform", transform); } /** * @constructor */ function KsAnimation(options) { // optional options this._options = options; // animation data this._targets = []; this._data = []; // duration and end time of the timeline this._endTime = 0; // states are mapped to hold and start times: // idle: holdTime and startTime null // running: holdTime null, startTime set // paused: holdTime set, startTime null // finished: holdTime and startTime set, current time <= in or >= out time // start time this._startTimeVal = null; // hold time - time used when timeline isn't progressing (rate is zero, paused or finished) this._holdTime = null; this._previousCurrentTime = null; // range playback values this._rangeIn = 0; this._rangeOut = 0; // play loop count this._loopCount = 0; // playback rate, default value is 1, which is normal playback rate. // zero and negative values are allowed this._playRate = 1; // set to true when Timeline is scheduled to play - used to set startTime during first update this._hasPendingPlay = false; // set to true if base transform is already parsed and stored this._baseTransformStored = false; // set to true if this has been added to the global timeline list this._addedToList = false; } KsAnimation.prototype = { _updateFinishedState: function(didSeek) { var pendingCallback = PENDING_NONE; // check if finished if (this._startTimeVal !== null) { var curTime = this._getCurrentTime(); // check if end is reached if (this._playRate > 0 && curTime !== null && curTime >= this._rangeOut) { if (this._loopCount) { // loop jumping to range in this._startTimeVal = sysTimelineTime - (this._rangeIn / this._playRate); this._loopCount--; // this doesn't decrement Infinity pendingCallback = PENDING_ONLOOP; } else { // no looping pendingCallback = PENDING_ONFINISH; if (didSeek) { this._holdTime = curTime; } else { if (this._previousCurrentTime) { // _previousCurrentTime ensures that jumping to a time after _rangeOut // during "running" will be possible without clamping the time // to _rangeOut - the time is preserved and state becomes "finished" this._holdTime = Math.max(this._previousCurrentTime, this._rangeOut); } else { this._holdTime = this._rangeOut; } } } // check if zero time is reached } else if (this._playRate < 0 && curTime !== null && curTime <= this._rangeIn) { if (this._loopCount && this._rangeOut !== Infinity) { // loop jumping to range out this._startTimeVal = sysTimelineTime - (this._rangeOut / this._playRate); this._loopCount--; // this doesn't decrement Infinity pendingCallback = PENDING_ONLOOP; } else { // no looping this._loopCount = 0; // clear _loopCount for the rangeOut Infinity case pendingCallback = PENDING_ONFINISH; if (didSeek) { this._holdTime = curTime; } else { if (this._previousCurrentTime) { // _previousCurrentTime ensures that jumping to a time before _rangeIn // during "running" will be possible without clamping the time // to _rangeIn - the time is preserved and state becomes "finished" this._holdTime = Math.min(this._previousCurrentTime, this._rangeIn); } else { this._holdTime = this._rangeIn; } } } // from "finished" state back to "running" } else if (curTime !== null && this._playRate !== 0) { if (didSeek && this._holdTime !== null) { // start time and hold time have values, which means we are in finished state, // this happens if animation is finished and playback rate is reversed or // time is set this._startTimeVal = sysTimelineTime - (this._holdTime / this._playRate); } this._holdTime = null; } } this._previousCurrentTime = this._getCurrentTime(); return pendingCallback; }, _updateValues: function(mainUpdate) { if (mainUpdate) { if (this._hasPendingPlay) { this._hasPendingPlay = false; // set start time during the first animation tick so that no frames are skipped // in the beginning if (this._startTimeVal === null) { if (this._playRate !== 0 && this._holdTime !== null) { this._startTimeVal = sysTimelineTime - this._holdTime / this._playRate; this._holdTime = null; } else { this._startTimeVal = sysTimelineTime; } } } // running animation, so check finished state (can't check getState() because // it may return finished for animation which doesn't have onfinish called) if (this._holdTime === null && this._startTimeVal !== null) { // the callbacks will be called later so they can call remove() successfully var pendingcb = this._updateFinishedState(false); if (pendingcb !== PENDING_NONE) { pendingCallbacks.push([ this, pendingcb ]); } } } var curTime = this._getCurrentTime(); // don't update values if animation is idle if (curTime === null) { return false; } var targets = this._targets; var adata = this._data; for (var e = 0; e < targets.length; ++e) { var target = targets[e]; var hasTransform = false; for (var p = 0; p < adata[e].length; ++p) { var pdata = adata[e][p]; var prop = pdata[INX_PROP]; if (prop === null) { continue; } var val; // interpolate value var starttime = pdata[INX_STARTTIME]; var tlen = pdata[INX_TIMES].length; var dur = pdata[INX_TIMES][tlen-1] - starttime; if (dur === 0) { // zero duration val = pdata[INX_VALUES][tlen-1]; } else if (curTime < starttime) { // before begin time if (!pdata[INX_FILL] || pdata[INX_FILL][0] & FILL_BACKWARDS) { val = pdata[INX_VALUES][0]; } else { // no fill backwards, so use fallback value val = pdata[INX_FILL][1]; } // after iteration has ended } else if (curTime >= starttime + pdata[INX_ITERATIONDUR]) { if (!pdata[INX_FILL] || pdata[INX_FILL][0] & FILL_FORWARDS) { if ((pdata[INX_ITERATIONDUR] % dur) === 0) { // use optimized path, if the end time is a multiple of last keyframe time, // this means that iteration stopped exactly at last keyframe val = pdata[INX_VALUES][tlen-1]; } else { // calculate iteration end value, needs to interpolate because it is not at // a keyframe val = animateProperty(pdata[INX_ITERATIONDUR] % dur, pdata); } } else { // no fill forwards, so use fallback value val = pdata[INX_FILL][1]; } } else { // during active time // calculate simple time var simpleTime = ((curTime-starttime) % dur); val = animateProperty(simpleTime, pdata); } if (prop === P_MOTION_DISTANCE) { // store motion path values target._ks['mpath'] = pdata[INX_MPATH]; target._ks.transform[prop] = val; hasTransform = true; } else if (prop <= P_ANCHORY) { // store transform value target._ks.transform[prop] = val; hasTransform = true; } else { // set other properties var svgval = convertToSvgValue(propertyType(pdata[INX_FLAGS]), val); if (pdata[INX_FLAGS] & FLAG_TARGET_ATTRIBUTE) { setAttr(target, prop, svgval); } else { target.style[prop] = svgval; } } } // if transform was animated, then set it if (hasTransform) { composeAndSetTransformAttr(target); } // Fix for Webkit to get elements under symbols animated: // set attribute value to get symbols repainted if (isWebkit) { target.setAttribute("opacity", target.getAttribute("opacity")); } } // return flag indicating if this animation wants to keep ticking return this._getState() === STATE_RUNNING; }, _performAutoRemove: function() { if (this._options['autoremove'] !== false && this._getState() === STATE_FINISHED) { remove(this); } }, _saveBaseTransform: function() { if (this._baseTransformStored) { return; } this._baseTransformStored = true; var atargets = this._targets; var adata = this._data; for (var e = 0; e < atargets.length; ++e) { var target = atargets[e]; for (var p = 0; p < adata[e].length; ++p) { var pdata = adata[e][p]; var prop = pdata[INX_PROP]; // including motion path (distance) if (prop <= P_ANCHORY) { parseTransform(target); } } } }, _parseTime(value) { if (typeof value === 'number') { return [ value, 0 ]; } if (!isSet(this._options['markers']) || !isSet(this._options['markers'][value])) { throw ERR("Invalid marker: "+value); } var marker = this._options['markers'][value]; // marker contains "time" and "dur" (Keyshape 1.12+) if (isSet(marker["time"])) { return [ +(marker["time"]), +(marker["dur"] || 0) ]; } // marker is assumed to contain a number (Keyshape 1.11) return [ +marker, 0 ]; }, // starts playing the timeline 'play': function(millisecs) { if (isSet(millisecs) && millisecs !== null) { millisecs = this._parseTime(millisecs)[0]; checkIsFinite(millisecs); // don't allow before or after range, because that would go directly to finished state if (this._playRate < 0 && millisecs < this._rangeIn) { millisecs = this._rangeIn; } if (this._playRate > 0 && millisecs > this._rangeOut) { millisecs = this._rangeOut; } this._setCurrentTime(millisecs, true); } if (!this._addedToList) { throw NOT_IN_LIST_EXCEPTION; } var t = this._getCurrentTime(); if (this._playRate > 0 && (t === null || t >= this._rangeOut || t < this._rangeIn)) { this._holdTime = this._rangeIn; } else if (this._playRate < 0 && (t === null || t <= this._rangeIn || t > this._rangeOut)) { if (this._rangeOut === Infinity) { throw ERR("Cannot seek to Infinity"); } this._holdTime = this._rangeOut; } else if (this._playRate === 0 && t === null) { this._holdTime = this._rangeIn; } // break out if the timeline is already playing if (this._holdTime === null) { return this; } // schedule start, so that start time is accurately calculated in animation mainloop this._startTimeVal = null; this._hasPendingPlay = true; this._saveBaseTransform(); startTicking(); return this; }, // pauses the timeline 'pause': function(millisecs) { if (!this._addedToList) { throw NOT_IN_LIST_EXCEPTION; } if (isSet(millisecs)) { millisecs = this._parseTime(millisecs)[0]; checkIsFinite(millisecs); } if (this._getState() !== STATE_PAUSED) { updateSysTimelineTime(); var curTime = this._getCurrentTime(); if (curTime === null) { if (this._playRate >= 0) { this._holdTime = this._rangeIn; } else { if (this._rangeOut === Infinity) { throw ERR("Cannot seek to Infinity"); } this._holdTime = this._rangeOut; } } if (this._startTimeVal !== null && this._holdTime === null) { this._holdTime = curTime; } this._startTimeVal = null; this._hasPendingPlay = false; this._updateFinishedState(false); this._saveBaseTransform(); startTicking(); } if (isSet(millisecs)) { this._setCurrentTime(millisecs, true); } return this; }, 'range': function(inTime, outTime) { if (arguments.length === 0) { return { "in": this._rangeIn, "out": this._rangeOut }; } var pintd = this._parseTime(inTime); var pin = pintd[0]; var pout = this._endTime; if (isSet(outTime)) { pout = this._parseTime(outTime)[0]; } checkIsFinite(pin); if (pin < 0 || pout < 0 || pin >= pout || isNaN(pout)) { throw ERR("Invalid range"); } var oldState = this._getState(); this._rangeIn = pin; this._rangeOut = pout; // update state to play if it was finished and range changed it to running if (oldState === STATE_FINISHED && this._getState() === STATE_RUNNING) { this['play'](); } return this; }, 'loop': function(loopCount) { if (!isSet(loopCount)) { return { "count": this._loopCount }; } this._loopCount = loopCount === true ? Infinity : Math.floor(loopCount); if (this._loopCount < 0 || isNaN(this._loopCount)) { this._loopCount = 0; } return this; }, _getCurrentTime: function() { if (this._holdTime !== null) { return this._holdTime; } if (this._startTimeVal === null) { return null; } return (sysTimelineTime - this._startTimeVal) * this._playRate; }, // sets the time to the given millisecs _setCurrentTime: function(seekTime, updateTime) { // update timeline time before checking seekTime is null if (updateTime) { updateSysTimelineTime(); } if (seekTime === null) { return; } this._saveBaseTransform(); if (this._holdTime !== null || this._startTimeVal === null || this._playRate === 0) { this._holdTime = seekTime; updateAllAnimations(false); } else { this._startTimeVal = sysTimelineTime - (seekTime / this._playRate); } if (!this._addedToList) { this._startTimeVal = null; } this._previousCurrentTime = null; this._updateFinishedState(true); startTicking(); }, // gets the time _getTime: function() { return this._getCurrentTime(); }, 'time': function(millisecs) { if (isSet(millisecs)) { if (!this._addedToList) { throw NOT_IN_LIST_EXCEPTION; } millisecs = this._parseTime(millisecs)[0]; checkIsFinite(millisecs); this._setCurrentTime(millisecs, true); return this; } return this._getTime(); }, _getState: function() { var curTime = this._getCurrentTime(); if (this._hasPendingPlay) { // additional condition, there is no "pending" state return STATE_RUNNING; } if (curTime === null) { return STATE_IDLE; } if (this._startTimeVal === null) { return STATE_PAUSED; } if ((this._playRate > 0 && curTime >= this._rangeOut) || (this._playRate < 0 && curTime <= this._rangeIn)) { return STATE_FINISHED; } return STATE_RUNNING; }, // returns play state 'state': function() { return this._getState(); }, // returns the duration of the animation or Infinity if it runs forever 'duration': function() { return this._endTime; }, // gets the playback rate, 1=normal, zero stops the progress, negative values reverse // the playback _getRate: function() { return this._playRate; }, // sets the playback rate, 1=normal, zero stops the progress, negative values reverse // the playback _setRate: function(value) { checkIsFinite(value); updateSysTimelineTime(); var oldTime = this._getCurrentTime(); this._playRate = value; if (oldTime !== null) { this._setCurrentTime(oldTime, false); } }, 'rate': function(r) { if (isSet(r)) { this._setRate(r); return this; } return this._getRate(); }, 'markers': function(markermap) { if (isSet(markermap)) { this._options['markers'] = copyMap(markermap); return this; } return isSet(this._options['markers']) ? this._options['markers'] : {}; }, 'marker': function(name) { return isSet(this._options['markers']) ? this._options['markers'][name] : undefined; }, '_cancel': function() { if (!this._addedToList || this._getState() === STATE_IDLE) { return this; } this._holdTime = null; this._startTimeVal = null; this._hasPendingPlay = false; return this; } } function getShortOrLong(kf, name1, name2) { var value = kf[name1]; if (value === undefined) { value = kf[name2]; } return value; } function fillEasing(easing, len) { if (!easing) { easing = []; } while (easing.length < len) { easing.push([ TIMING_FN_CUBIC, 0, 0, 0.58, 1 ]); // default: ease-out } return easing; } function parseInt10(value) { return parseInt(value, 10); } function parseTimingFn(val) { if (Array.isArray(val)) { // array means a shorthand timing function return val; } if (startsWith(val, "cubic-bezier(")) { var vals = val.substring(13, val.length-1).split(','); return [ TIMING_FN_CUBIC, parseFloat(vals[0]), parseFloat(vals[1]), parseFloat(vals[2]), parseFloat(vals[3]) ]; } else if (startsWith(val, "steps(")) { var svals = val.substring(6, val.length-1).split(','); return [ svals[1] && svals[1].trim() === "start" ? TIMING_FN_STEP_START : TIMING_FN_STEP_END, parseFloat(svals[0]) ]; } // other are linear return [ TIMING_FN_LINEAR ]; } function parseColor(cstr) { cstr = cstr.trim(); if (startsWith(cstr, "#")) { return (parseInt(cstr.substring(1), 16) << 8) + 255; } if (startsWith(cstr, "rgba(")) { cstr = cstr.substring(5, cstr.length-1); var p = cstr.split(','); return (parseInt10(p[0]) << 24) + (parseInt10(p[1]) << 16) + (parseInt10(p[2]) << 8) + (parseFloat(p[3])*255) << 0; // shift alpha by 0 to convert it to int } return cstr; } function parseFilter(filter) { if (filter === "none") { return [ FILTER_NONE ]; } var res = []; var fninx = filter.indexOf('('); while (fninx > 0) { var inx = filterNames.indexOf(filter.substring(0, fninx)); if (inx >= 0) { res.push(inx); var endinx = filter.indexOf(') '); if (endinx < 0) endinx = filter.length-1; var params = filter.substring(fninx+1, endinx).split(' '); if (inx === FILTER_DROPSHADOW) { // 3 numbers and a color res.push(parseFloat(params[0])); res.push(parseFloat(params[1])); res.push(parseFloat(params[2])); res.push(parseColor(params[3])); } else if (inx === FILTER_URL) { // string parameter res.push(params[0]); } else { // numeric parameter res.push(parseFloat(params[0])); } filter = filter.substring(endinx+1).trim(); // get next filter fninx = filter.indexOf('('); } else { break; } } return res; } /** * @return {Element} DOM element */ function solveTarget(t) { var e = (t instanceof Element) ? t : document.getElementById(t.substring(1)); if (!e) { throw ERR("Invalid target: "+t); } return e; } function parseValues(prop, values) { var type = propertyType(propertyFlags(prop)); for (var i = 0; i < values.length; ++i) { if (type === FLAG_TYPE_PATH) { // split values for fast interpolation var val = values[i].substring(6, values[i].length-2); // remove "path('')" var commands = val.match(/[A-DF-Za-df-z][-+0-9eE., ]*/ig); var s = []; for (var k = 0; k < commands.length; ++k) { s.push(commands[k][0]); var coords = commands[k].trim().length > 1 ? commands[k].substring(1).split(',') : []; for (var ci = 0; ci < coords.length; ++ci) { coords[ci] = parseFloat(coords[ci]); } s.push(coords); } values[i] = s; } else if (type === FLAG_TYPE_COLOR) { if (startsWith(values[i], "#")) { var hasAlpha = (values[i].length === 9); values[i] = parseInt(values[i].substring(1), 16); if (!hasAlpha) { // add alpha to colors without it values[i] = (values[i]*256) | 255; } } else if (!startsWith(values[i], "url(") && values[i] !== "none") { console.warn("unsupported color: "+values[i]); values[i] = 0; } } else if (type === FLAG_TYPE_FILTER) { values[i] = parseFilter(values[i]); } else if (type === FLAG_TYPE_LENGTH_LIST) { if (values[i] !== "none") { if (!/^[0-9 .]*$/.test(values[i])) { // units are not allowed console.warn("unsupported value: "+values[i]); values[i] = [ 0 ]; } else { values[i] = parseFloatList(values[i], ' '); } } else { values[i] = [ 0 ]; } } else if (type === FLAG_TYPE_LENGTH) { checkIsFinite(values[i]); values[i] = parseFloat(values[i]); } else if (prop === P_MOTION_DISTANCE) { // ignores possible trailing percentage values[i] = parseFloat(values[i]); } } } function validateTimes(times) { if (!isFinite(times[0]) || times[0] < 0) { throw ERR("Invalid time: "+times[0]); } for (var i = 1; i < times.length; ++i) { if (!isFinite(times[i]) || times[i] < 0 || times[i] < times[i-1]) { throw ERR("Invalid time: "+times[i]); } } } function initTimeline(tl, tlData) { var totalEnd = 0; for (var ai = 0; ai < tlData.length-1; ai += 2) { var te = solveTarget(tlData[ai]); var anim = tlData[ai+1]; if (!te._ks) { te._ks = { }; } var newadata = []; for (var i = 0; i < anim.length; ++i) { var kf = anim[i]; var prop = getShortOrLong(kf, "p", "property"); if (!isValidProperty(prop)) { throw ERR("Invalid property: "+prop); } var propIndex = transformProps.indexOf(prop); // convert transform strings to numbers if (prop !== "" && propIndex >= 0) { prop = propIndex; } var flags = propertyFlags(prop); var times = getShortOrLong(kf, "t", "times"); if (!times || times.length < 2) { throw ERR("Not enough times"); } times = copyArray(times); validateTimes(times); var starttime = times[0]; var endtime = times[times.length-1]; var rdur = endtime - starttime; var iters = kf["iterations"] || 0; if (iters < 1) { iters =