UNPKG

scrawl-canvas

Version:

Responsive, interactive and more accessible HTML5 canvas elements. Scrawl-canvas is a JavaScript library designed to make using the HTML5 canvas element easier, and more fun

881 lines (604 loc) 24.4 kB
// # Ticker factory // Ticker objects represent a timeline against which [Tween](./tween.html) and [Action](./action.html) objects will run. // #### Imports import { animation, animationtickers, constructors, tween } from '../core/library.js'; import { convertTime, doCreate, isa_obj, mergeOver, pushUnique, removeItem, xt, Ωempty } from '../helper/utilities.js'; import { makeAnimation } from './animation.js'; import { releaseArray, requestArray } from '../helper/array-pool.js'; import baseMix from '../mixin/base.js'; // Shared constants import { _floor, _now, FUNCTION, PC, T_ACTION, T_RENDER_ANIMATION, T_TICKER, T_TWEEN, ZERO_STR } from '../helper/shared-vars.js'; // Local constants const ANIMATIONTICKERS = 'animationtickers' // #### Ticker constructor const Ticker = function (items = Ωempty) { this.makeName(items.name); this.register(); this.subscribers = []; this.subscriberObjects = []; this.set(this.defs); this.set(items); this.cycleCount = 0; this.active = false; this.effectiveDuration = 0; this.startTime = 0; this.currentTime = 0; this.tick = 0; if (items.subscribers) this.subscribe(items.subscribers); this.setEffectiveDuration(); return this; }; // #### Ticker prototype const P = Ticker.prototype = doCreate(); P.type = T_TICKER; P.lib = ANIMATIONTICKERS; P.isArtefact = false; P.isAsset = false; // #### Mixins baseMix(P); // #### Ticker attributes const defaultAttributes = { // __order__ - positive integer Number - determines the order in which each Ticker animation object will be actioned before the Display cycle starts. // + Higher order Tickers will be processed after lower order Tickers. // + Tickers with the same `order` value will be processed in the order in which they were defined in code. order: 1, // __duration__ - can accept a variety of values: // + Number, representing milliseconds. // + String time value, for example `'500ms', '0.5s'`. // + (% String values cannot be used with Ticker objects - they have nothing to measure such a relative value against). duration: 0, // __subscribers__ - Array of Tween and Action name-Strings. Use `subscribe` and `unsubscribe` functions rather than the `set` function to add/remove Tweens and/or Actions to/from the Ticker subscribers: null, // __killOnComplete__ - Boolean flag. When set, the Ticker will kill both itself and all Tweens and Actions associated with it at the end of its run killOnComplete: false, // __cycles__ - positive integer Number representing the number of cycles the Ticker will run before it completes. // + A value of `0` indicates that the Ticker should repeat itself forever, until its `halt`, `seekTo`, `seekFor`, `complete` or `reset` functions are triggered. // + Note that Tween and Action animation direction is determined by those objects (via their `reverseOnCycleEnd` and `reversed` flags). Tickers always repeat in a forwards direction - they loop back to their start; they never reverse time. cycles: 1, // __observer__ - String name of a RenderAnimation object, or the object itself - halt/resume the ticker based on the running state of the animation object observer: null, // The Ticker object supports some __hook functions__: // + __onRun__ - triggers each time the Ticker's `run` function is invoked // + __onHalt__ - triggers each time the Ticker's `halt` function is invoked // + __onReverse__ - triggers each time the Ticker's `reverse` function is invoked // + __onResume__ - triggers each time the Ticker's `resume` function is invoked // + __onSeekTo__ - triggers each time the Ticker's `seekTo` function is invoked // + __onSeekFor__ - triggers each time the Ticker's `seekFor` function is invoked // + __onComplete__ - triggers each time the Ticker's `complete` function is invoked // + __onReset__ - triggers each time the Ticker's `reset` function is invoked onRun: null, onHalt: null, onReverse: null, onResume: null, onSeekTo: null, onSeekFor: null, onComplete: null, onReset: null, }; P.defs = mergeOver(P.defs, defaultAttributes); // #### Packet management P.packetExclusions = pushUnique(P.packetExclusions, ['subscribers']); P.packetFunctions = pushUnique(P.packetFunctions, ['onRun', 'onHalt', 'onReverse', 'onResume', 'onSeekTo', 'onSeekFor', 'onComplete', 'onReset']); // #### Clone management // No additional clone functionality required. // #### Kill management // `kill` - remove Ticker from Scrawl-canvas system. P.kill = function (killTweens = true, autokill = true) { if (killTweens) { const subs = [...this.subscribers]; for (let i = 0, iz = subs.length; i < iz; i++) { const sub = tween[subs[i]]; if (sub) { sub.completeAction(); sub.kill(); } } } if (autokill) { if (this.active) this.halt(); removeItem(tickerAnimations, this.name); tickerAnimationsFlag = true; this.deregister(); return true; } return this; }; // `killTweens` - remove a Ticker's subscribed Tweens from Scrawl-canvas system. // + If the function is invoked with a truthy argument, the Ticker will also be removed from the system. P.killTweens = function(autokill = false) { return this.kill(true, autokill); }; // #### Get, Set, deltaSet const G = P.getters, S = P.setters; // __subscribers__ - see also the `subscribe` and `unsubscribe` functions below // + getter returns a copy of the `subscribers` Array, containing Tween and Action object name-Strings. // + setter accepts a Tween or Action name-String, or an Array of such Strings. Will replace the existing `subscribers` Array with this new data. G.subscribers = function () { return [...this.subscribers]; }; S.subscribers = function (item) { this.subscribe(item); }; // __order__ S.order = function (item) { this.order = item; if (this.active) tickerAnimationsFlag = true; }; // __cycles__ S.cycles = function (item) { this.cycles = item; if (!this.cycles) this.cycleCount = 0; }; // __duration__ - changes to the `duration` (and as a consequence `effectiveDuration`) attributes will be cascaded down to subscribed Tweens and Actions immediately. S.duration = function (item) { let i, iz, target; const subscribers = this.subscribers; this.duration = item; this.setEffectiveDuration(); if(xt(subscribers)){ for (i = 0, iz = subscribers.length; i < iz; i++) { target = tween[subscribers[i]]; if (target) { target.calculateEffectiveTime(); if (target.type === T_TWEEN) target.calculateEffectiveDuration(); } } } }; // #### Subscription management // `subscribe` - can accept one or more arguments, each of which can be: // + a Tween or Action name-String, or the Tween or Action objects themselves // + an Array of such name-Strings or objects P.subscribe = function (...args) { const items = args.flat(Infinity); if (items.length) { items.forEach(item => { let obj; if (item.substring) obj = tween[item]; else if (isa_obj(item) && (item.type === T_ACTION || item.type === T_TWEEN)) obj = item; if (obj) { pushUnique(this.subscribers, obj.name); obj.ticker = this.name; obj.calculateEffectiveTime(); } }); this.sortSubscribers(); this.recalculateEffectiveDuration(); } return this; }; // `unsubscribe` - can accept one or more arguments, each of which can be: // + a Tween or Action name-String, or the Tween or Action objects themselves // + an Array of such name-Strings or objects P.unsubscribe = function (...args) { const items = args.flat(Infinity); if (items.length) { items.forEach(item => { let obj; if (item.substring) obj = tween[item]; else if (isa_obj(item) && (item.type === T_ACTION || item.type === T_TWEEN)) obj = item; if (obj) { removeItem(this.subscribers, obj.name); obj.ticker = ZERO_STR; } }); this.sortSubscribers(); this.recalculateEffectiveDuration(); } return this; }; // `repopulateSubscriberObjects` P.repopulateSubscriberObjects = function () { const arr = this.subscriberObjects, subs = this.subscribers; let t; arr.length = 0; subs.forEach(sub => { t = tween[sub]; if (t) arr.push(t); }); }; // `getSubscriberObjects` P.getSubscriberObjects = function () { if (this.subscribers.length && !this.subscriberObjects.length) this.repopulateSubscriberObjects(); return this.subscriberObjects; }; // `sortSubscribers` - internal Helper function called by `subscribe` and `unsubscribe` P.sortSubscribers = function () { const subs = this.subscribers, len = subs.length; if(len > 1) { const buckets = requestArray(); let i, iz, obj, eTime; for (i = 0; i < len; i++) { obj = subs[i]; eTime = _floor(obj.effectiveTime) || 0; if (!buckets[eTime]) buckets[eTime] = requestArray(); buckets[eTime].push(obj); } subs.length = 0; for (i = 0, iz = buckets.length; i < iz; i++) { obj = buckets[i]; if (obj) { subs.push(...obj); releaseArray(obj); } } releaseArray(buckets); } this.repopulateSubscriberObjects(); }; // `updateSubscribers` - internal function called by the `run`, `reset` and `complete` functions below. // + First argument is an object that gets applied as the argument to each Tween/Action object's `set` function. // + Second argument is a Boolean; when set, subscribed Tween/Actions will be told to reverse their current direction. P.updateSubscribers = function(items, reversed) { reversed = (xt(reversed)) ? reversed : false; const subs = this.getSubscriberObjects(); let i, iz; if (reversed) { for (i = subs.length - 1; i >= 0; i--) { subs[i].set(items); } } else{ for (i = 0, iz = subs.length; i < iz; i++) { subs[i].set(items); } } return this; }; // `changeSubscriberDirection` - internal function - when invoked, Tween/Actions will be told to reverse their current direction. P.changeSubscriberDirection = function () { const subs = this.getSubscriberObjects(); subs.forEach(sub => sub.reversed = !sub.reversed); return this; }; // #### Animation // `recalculateEffectiveDuration` - where a Ticker has not been given a `duration` value, it needs to consult its subscribed Tween/Action objects to calculate an `effectiveDuration` value with sufficient time allocated for each Tween to run to completion, and each Action to trigger. // + Tween/Actions with a relative `time` attribute - eg: `30%` - will not be included in the calculation. // + Tweens can overlap - they do not all have to start and end at the same time, nor do they need to run sequentially. P.recalculateEffectiveDuration = function() { const subs = this.getSubscriberObjects(); let durationValue, duration = 0; if (!this.duration) { subs.forEach(sub => { durationValue = sub.getEndTime(); if (durationValue > duration) duration = durationValue; }); this.effectiveDuration = duration; } // Shouldn't cause an infinite loop ... else this.setEffectiveDuration(); return this; }; // `setEffectiveDuration` - internal helper function - convert `duration` value into `effectiveDuration` value. P.setEffectiveDuration = function() { let temp; if (this.duration) { temp = convertTime(this.duration); // Cannot use %-String values for Ticker `duration` attribute if (temp[0] === PC) { this.duration = 0 this.recalculateEffectiveDuration(); } else this.effectiveDuration = temp[1]; } return this; }; // `checkObserverRunningState` - internal helper function P.checkObserverRunningState = function () { let observer = this.observer; if (observer) { if (observer.substring) { const anim = animation[observer]; if (anim && anim.type === T_RENDER_ANIMATION) { observer = this.observer = anim; } else return true; } if (observer.type === T_RENDER_ANIMATION) { return observer.isRunning(); } } return true; }; // `fn` - internal - the __animation function__ will trigger once per RequestAnimationFrame (RAF) tick - approximately 60 times a second, depending on other calculation work. // + Only triggers when the Ticker is running in a qualifying state. // + __reverseOrder__ argument is a Boolean value; when set, subscribed Tween/Action objects will be processed in reverse order. P.fn = function (reverseOrder) { // Determine the order in which subscribed objects will be processed reverseOrder = xt(reverseOrder) ? reverseOrder : false; // Request a `result` object from the pool. const result = requestResultObject(); const startTime = this.startTime, cycles = this.cycles, effectiveDuration = this.effectiveDuration; let i, iz, subs, currentTime, tick, active = this.active, cycleCount = this.cycleCount; // Process only if the Ticker is currently ___active___ and has a ___startTime___ value assigned to it. if (active && startTime) { // Process only if the Ticker's `cycles` attribute has been set to `0`, or if the Ticker has not yet completed all its cycles. if (!cycles || cycleCount < cycles) { currentTime = this.currentTime = _now(); tick = this.tick = currentTime - startTime; // Update the results object // + Functionality performed if the ___Tween is not on its final cycle___. if (!cycles || cycleCount + 1 < cycles) { if (tick >= effectiveDuration) { tick = this.tick = 0; this.startTime = this.currentTime; result.tick = effectiveDuration; result.reverseTick = 0; result.willLoop = true; if (cycles) { cycleCount++; this.cycleCount = cycleCount; } } else { result.tick = tick; result.reverseTick = effectiveDuration - tick; } result.next = true; } // + Functionality performed only when the ___Tween is on its final cycle___. else { if (tick >= effectiveDuration) { result.tick = effectiveDuration; result.reverseTick = 0; active = this.active = false; if (cycles) { cycleCount++ this.cycleCount = cycleCount; } } else { result.tick = tick; result.reverseTick = effectiveDuration - tick; result.next = true; } } // Invoke the `update` function on each subscribed Tween/Action subs = this.getSubscriberObjects(); if (reverseOrder) { for (i = subs.length - 1; i >= 0; i--) { subs[i].update(result); } } else{ for (i = 0, iz = subs.length; i < iz; i++) { subs[i].update(result); } } // If this invocation of the function has completed the Ticker's run, switch it off. if (!active) this.halt(); // If the Ticker's run is completed and the `killOnComplete` flag is set, kill everything. if (this.killOnComplete && cycleCount >= cycles) this.killTweens(true); } } // Release the `result` object back to the pool. releaseResultObject(result); }; // #### Animation control // `run` // + Start the Ticker from time 0. // + Trigger the object's `onRun` function. P.run = function () { if (!this.active) { this.startTime = this.currentTime = _now(); this.cycleCount = 0; this.updateSubscribers({ reversed: false }); this.active = true; pushUnique(tickerAnimations, this.name); tickerAnimationsFlag = true; if (typeof this.onRun === FUNCTION) this.onRun(); } return this; }; // `isRunning` - check to see if Ticker is in a running state. P.isRunning = function () { return this.active; }; // `reset` // + Halt the Ticker, if it is running. // + Set all attributes to their initial values. // + Update subscribed Tween/Actions. // + Trigger the object's `onReset` function. P.reset = function () { if (this.active) this.halt(); this.startTime = this.currentTime = _now(); this.cycleCount = 0; this.updateSubscribers({ reversed: false }); this.active = true; this.fn(true); this.active = false; if (typeof this.onReset === FUNCTION) this.onReset(); return this; }; // `complete` // + Halt the Ticker, if it is running. // + Set all attributes to their initial values. // + Update subscribed Tween/Actions. // + Trigger the object's `onComplete` function. P.complete = function () { if (this.active) this.halt(); this.startTime = this.currentTime = _now(); this.cycleCount = 0; this.updateSubscribers({ reversed: true }); this.active = true; this.fn(); this.active = false; if (typeof this.onComplete === FUNCTION) this.onComplete(); return this; }; // `reverse` - simulates a reversal in the Ticker's animation. // + Halt the Ticker, if it is running. // + Trigger the object's `onReverse` function. // + after recalculation, resume the Ticker - if required. // + Function accepts a Boolean argument - if true, Ticker will start playing "in reverse". // + Directionality is determined by Tween/Action object attribute settings, not the Ticker. P.reverse = function (resume = false) { if (this.active) this.halt(); const timePlayed = this.currentTime - this.startTime; this.startTime = this.currentTime - (this.effectiveDuration - timePlayed); this.changeSubscriberDirection(); this.active = true; this.fn(); this.active = false; if (typeof this.onReverse === FUNCTION) this.onReverse(); if (resume) this.resume(); return this; }; // `halt` // + Stop the Ticker at its current point in time // + Trigger the object's `onHalt` function. P.halt = function () { this.active = false; removeItem(tickerAnimations, this.name); tickerAnimationsFlag = true; if (typeof this.onHalt === FUNCTION) this.onHalt(); return this; }; // `resume` - this function can also be triggered by the `reverse`, `seekTo` and `seekFor` functions // + Start the Ticker from its current point in time // + Trigger the object's `onResume` function. P.resume = function () { let now, current, start; if (!this.active) { now = _now(); current = this.currentTime; start = this.startTime; this.startTime = now - (current - start); this.currentTime = now; this.active = true; pushUnique(tickerAnimations, this.name); tickerAnimationsFlag = true; if (typeof this.onResume === FUNCTION) this.onResume(); } return this; }; // `seekTo` // + First argument - Number representing the millisecond time to move to on the Ticker's timeline // + Second argument - Boolean - if true, Ticker will resume playing from new point // + Halt the Ticker, if it is running. // + Update the Ticker's `currentTime`, `startTime` attributes // + Trigger the object's `onSeekTo` function. // + Resume the Ticker - if required. P.seekTo = function (milliseconds = 0, resume = false) { let backwards = false; if (this.active) this.halt(); if (this.cycles && this.cycleCount >= this.cycles) this.cycleCount = this.cycles - 1; if (milliseconds < this.tick) backwards = true; this.currentTime = _now(); this.startTime = this.currentTime - milliseconds; this.active = true; this.fn(backwards); this.active = false; if (typeof this.onSeekTo === FUNCTION) this.onSeekTo(); if (resume) this.resume(); return this; }; // `seekFor` // + First argument - Number representing the number of milliseconds to move along the Ticker's timeline (forwards or backwards) // + Second argument - Boolean - if true, Ticker will resume playing from new point // + Halt the Ticker, if it is running. // + Update the Ticker's `currentTime`, `startTime` attributes // + Trigger the object's `onSeekFor` function. // + Resume the Ticker - if required. P.seekFor = function (milliseconds = 0, resume = false) { let backwards = false; if (this.active) this.halt(); if (this.cycles && this.cycleCount >= this.cycles) this.cycleCount = this.cycles - 1; this.startTime -= milliseconds; if (milliseconds < 0) backwards = true; this.active = true; this.fn(backwards); this.active = false; if (typeof this.onSeekFor === FUNCTION) this.onSeekFor(); if (resume) this.resume(); return this; }; // #### Ticker animation controller const tickerAnimations = []; let tickerAnimationsFlag = true; // `coreTickersAnimation` makeAnimation({ name: 'SC-core-tickers-animation', order: 0, fn: function () { // We only sort active Ticker objects when absolutely necessary. // + Sorted using a bucket sort algorithm. let arr, obj, order, i, iz, name; if (tickerAnimationsFlag) { tickerAnimationsFlag = false; const buckets = requestArray(); for (i = 0, iz = tickerAnimations.length; i < iz; i++) { obj = tickerAnimations[i]; if (obj) { order = _floor(obj.order) || 0; if (!buckets[order]) buckets[order] = requestArray(); buckets[order].push(obj); } } tickerAnimations.length = 0; for (i = 0, iz = buckets.length; i < iz; i++) { arr = buckets[i]; if (arr) { tickerAnimations.push(...arr); releaseArray(arr); } } releaseArray(buckets); } // Invoke each Ticker's `fn` function. // + It's up to the Ticker object to decide whether it's active for (i = 0, iz = tickerAnimations.length; i < iz; i++) { name = tickerAnimations[i]; obj= animationtickers[name]; if (obj && obj.fn && obj.checkObserverRunningState()) obj.fn(); } } }); // #### ResultObject pool // TODO: do we need a pool for this? // + To use a pool result object, request it using `requestResultObject` function. // + It is imperative that requested result objects are released - `releaseResultObject` - once work with them completes. const resultObjectPool = []; // `requestResultObject` const requestResultObject = function () { if (!resultObjectPool.length) { resultObjectPool.push({ tick: 0, reverseTick: 0, willLoop: false, next: false }); } return resultObjectPool.shift(); }; // `releaseResultObject` const releaseResultObject = function (r) { if (r) { r.tick = 0; r.reverseTick = 0; r.willLoop = false; r.next = false; resultObjectPool.push(r); } }; // #### Factory export const makeTicker = function (items) { if (!items) return false; return new Ticker(items); }; constructors.Ticker = Ticker;