diff --git a/.gitignore b/.gitignore index 9832614..5cbddbe 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .eslintrc.js docs/.$raf-test.drawio.bkp docs/.$raf-onArrival.drawio.bkp +desktop.ini diff --git a/acues.js b/acues.js index 301562d..537dab9 100644 --- a/acues.js +++ b/acues.js @@ -36,7 +36,7 @@ export class ACues { } // this.cues is an array of cue objects - get cues() { return this.#cues; } + get cues() { return this.#cues; } // returns the source, not a copy set cues(val) { val = Ez.toArray(val, "cues", this.#validate, ...Ez.okEmptyUndef); if (Ez._mustAscendErr(val, "cues", false)) { @@ -167,11 +167,10 @@ export class ACues { _resume(now) { // #now stays the same, that's the whole idea of pausing this.#zero = now - this.#now; } -// _reset(): helps AFrame.prototype.#cancel() reset this to the requested state - _reset(sts) { +// _runPost() helps AFrame.prototype.#stop() run .post() for unfinished ACues + _runPost() { if (!sts && this.#i < this.#last) // E.arrived only this._next(Infinity); //!!include more statuses?? E.empty always excluded + this.#post?.(this); } -// _runPost() helps AFrame.prototype.#cancel() run .post() for unfinished ACues - _runPost() { this.#post?.(this); } } \ No newline at end of file diff --git a/aframe.js b/aframe.js index d6d4801..fc2624a 100644 --- a/aframe.js +++ b/aframe.js @@ -1,39 +1,35 @@ //!!rename #targets to #active and #backup to #targets to mimic class Easies!! -import {E, Ez, Is, Easy, Easies, ACues} from "./raf.js"; +import {E, Ez, Is, Easies, ACues} from "./raf.js"; // AFrame: the animation frame manager export class AFrame { - #backup; #callback; #fps; #frame; #gpu; #keepPost; #now; #onArrival; #peri; - #post; #preInit; #promise; #syncZero; #targets; #useNow; #zero; - #status = E.empty; - #frameZero = true; // Unfortunately, this is the best default (for now...) + #backup; #callback; #fps; #frame; #frameZero; #gpu; #keepPost; #initZero; + #now; #oneShot; #peri; #post; #promise; #status; #syncZero; #targets; + #useNow; #zero; //============================================================================== - constructor(arg) { // arg is int, bool, arrayish, object, or undefined - this.#callback = this.#animate; // overwritten by this.useNow setter + constructor(arg) { // arg is int, bool, arrayish, object, or undefined + this.#status = E.empty; + this.#callback = this.#animate; // overwritten by this.useNow below + this.#frameZero = true; // ditto this.frameZero and else if + if (arg?.isEasies || arg?.isACues) this.targets = arg; else if (Is.Arrayish(arg)) this.targets = Ez.toArray(arg, "new AFrame(targets)", AFrame.#validTarget, false); - else if (Ez._validObj(arg)) { - this.peri = arg.peri; this.onArrival = arg.onArrival; - this.post = arg.post; this.keepPost = arg.keepPost; - this.useNow = arg.useNow; this.frameZero = arg.frameZero; - this.targets = arg.targets; this.syncZero = arg.syncZero; - this.preInit = arg.preInit ?? arg.jumpStart; - } + else if (Ez._validObj(arg)) + for (const p of ["peri","post","keepPost","oneShot","useNow", + "targets","frameZero","initZero","syncZero"]) + this[p] = arg[p]; else { this.#targets = new Set; this.#backup = new Set; - if (Easy._validArrival(arg, undefined, true) !== false) - this.#onArrival = arg; // legit int status or undefined - else if (arg === true || arg === false) - this.#frameZero = arg; - else + if (arg === true || arg === false) + this.#frameZero = arg; //!!iffy choice of property, oneShot, initZero instead?? + else if (Is.def(arg)) Ez._mustBeErr("new AFrame(arg): arg", - "a valid onArrival status, a boolean, array-ish, " - + "a valid object with properties, or undefined."); + "a boolean, array-ish, a valid object with " + + "properties, or undefined."); } - this.jumpStart = this.preInit; // steps-like alias Object.seal(this); } //============================================================================== @@ -99,33 +95,26 @@ export class AFrame { return t; } //============================================================================== +// this.oneShot causes #stop() to clear targets on arrival + get oneShot() { return this.#oneShot; } + set oneShot(b) { this.#oneShot = Boolean(b); } + get useNow() { return this.#useNow; } set useNow(b) { this.#useNow = Boolean(b); this.#callback = b ? this.#animateNow : this.#animate; } - get frameZero() { return this.#frameZero} + get frameZero() { return this.#frameZero } set frameZero(b) { this.#frameZero = Boolean(b); } - get preInit() { return this.#preInit; } - set preInit(b) { this.#preInit = Boolean(b); } + get initZero() { return this.#initZero; } + set initZero(b) { this.#initZero = Boolean(b); } -// this.onArrival - get onArrival() { return this.#onArrival; } - set onArrival(sts) { this.#onArrival = Easy._validArrival(sts, "AFrame"); } - -// this.oneShot is an alias/shortcut for onArrival(E.empty) - get oneShot() { return this.#onArrival == E.empty; } - set oneShot(b) { // if (!b && this.#onArrival != E.empty) - if (b) // then it's already "off", leave #onArrival alone - this.#onArrival = E.empty; - else if (this.#onArrival == E.empty) - this.#onArrival = undefined; - } // this.syncZero is optional, run by #animateZero(), only if #frameZero == true get syncZero() { return this.#syncZero; } set syncZero(val) { this.#syncZero = Ez._validFunc(val, ".syncZero"); } + // this.peri is optional, runs once per frame, helps throttle events get peri() { return this.#peri; } set peri(val) { this.#peri = Ez._validFunc(val, ".peri"); } @@ -135,30 +124,11 @@ export class AFrame { // this.keepPost indicates whether to keep or delete this.#post after playing it get keepPost() { return this.#keepPost; } set keepPost(val) { this.#keepPost = Boolean(val); } + // this.fps get fps() { return this.#fps; } // this.gpu get gpu() { return this.#gpu; } - -// status-related properties and methods: - get status() { return this.#status; } - get atOrigin() { return this.#status == E.original; } - get atStart() { return this.#status == E.initial; } - get atInitial() { return this.#status == E.initial; } - get atEnd() { return this.#status == E.arrived; } - get hasArrived() { return this.#status == E.arrived; } - get isPausing() { return this.#status == E.pausing; } - get isPlaying() { return this.#status == E.playing; } - get isEmpty() { return this.#status == E.empty; } - -// public status setters: #stop() with different statuses - arrive () { return this.#stop(E.arrived); } - init () { return this.#stop(E.initial); } - stop () { return this.#stop(E.initial); } - restore() { return this.#stop(E.original); } - pause () { return this.#stop(E.pausing); } - clear () { return this.#stop(E.empty); } - cancel () { return this.#stop(); } //============================================================================== // play() initiates the #animate() callback loop play() { @@ -168,34 +138,27 @@ export class AFrame { this.#promise.resolve(E.empty); else { let t; - const now = this.#useNow ? performance.now() - : document.timeline.currentTime; - const isResuming = this.isPausing; - if (isResuming) + const now = this.#useNow + ? performance.now() + : document.timeline.currentTime, + isZero = !this.isPausing; + if (isZero) { // starting from zero + for (t of this.#targets) + t.pre?.(t); + if (!this.#frameZero) + this.#setZero(now); + } + else { // resuming from pause + this.#zero = this.#zero + now - this.#now; + this.#now = now; for (t of this.#targets) t._resume(now); - else { - if (this.#frameZero) - for (t of this.#targets) - t.pre?.(t); - else { - for (t of this.#targets) { - t._zero(now); - t.pre?.(t); - } - } - if (this.#preInit) - for (t of this.#targets) - t.init(t); } this.#status = E.playing; - if (this.#frameZero && !isResuming) + if (isZero && this.#frameZero) this.#frame = requestAnimationFrame(t => this.#animateZero(t)); - else { - this.#zero = isResuming ? this.#zero + now - this.#now : now; - this.#now = now; - this.#frame = requestAnimationFrame(t => this.#callback(t)); - } + else { console.log("animate()") + this.#frame = requestAnimationFrame(t => this.#callback(t));} } } return this.#promise; @@ -205,64 +168,95 @@ export class AFrame { this.#now = timeStamp; // set it first so callbacks can use it for (const t of this.#targets) { // execute this frame if (t._next(timeStamp)) { // true = arrived, finished - t.post?.(t); + t.post?.(t); // ACues or Easies.proto.post() this.#targets.delete(t); } } if (this.#peri?.(this, timeStamp) || this.#targets.size) this.#frame = requestAnimationFrame(t => this.#callback(t)); else - this.#stop(this.#onArrival, true); + this.#stop(E.arrived, true); } // #animateNow() is the animation callback that uses performance.now() #animateNow() { this.#animate(performance.now()); } -// #animateZero() is for one frame only +// #animateZero() is the callback for one frame only #animateZero(timeStamp) { - this.#zero = timeStamp; - this.#now = timeStamp; if (this.#useNow) timeStamp = performance.now(); - for (const t of this.#targets) - t._zero(timeStamp); - this.#syncZero?.(timeStamp); // not an exact sync unless #useNow + this.#setZero (timeStamp); + console.log("animateZero()") this.#callback(timeStamp); // timeStamp is always less than now } +// #setZero() helps play() and #animateZero() + #setZero(now) { + this.#zero = now; + this.#now = now; + for (const t of this.#targets) { + t._zero(now); + if (this.#initZero) + t.init(t); + } + this.#syncZero?.(now); + } //============================================================================== +// status-related properties and methods: + get status() { return this.#status; } + get atOrigin() { return this.#status == E.original; } + get atInitial() { return this.#status == E.initial; } + get hasArrived() { return this.#status == E.arrived; } + get isPausing() { return this.#status == E.pausing; } + get isPlaying() { return this.#status == E.playing; } + get isEmpty() { return this.#status == E.empty; } + +// reset methods call #stop() with different statuses + arrive(isArriving) { return this.#stop(E.arrived, isArriving, true); } + init (isArriving) { return this.#stop(E.initial, isArriving, true); } + stop (isArriving) { return this.#stop(E.original, isArriving, true); } + pause () { return this.#stop(E.pausing); } + cancel() { return this.#stop(); } + // #stop() stops the animation and leaves it in the requested state - #stop(sts, hasArrived) { - const wasPlaying = this.isPlaying; - if (wasPlaying && !hasArrived) +// isArriving does #post() and #oneShot +// runReset runs #reset(sts, runReset) + #stop(sts, isArriving, runReset) { + let ez, t; + const // #targets is (partially) spent + easieses = Array.from(this.#backup).filter(v => v.isEasies), + wasPlaying = this.isPlaying, + notCancel = Is.def(sts); + if (wasPlaying && !isArriving) cancelAnimationFrame(this.#frame); - if (sts == E.pausing) - this.#status = sts; - else { - let t; - const isArriving = hasArrived ?? !sts; // E.arrived = 0 = !sts - if (isArriving && (wasPlaying || this.isPausing)) { - if (!hasArrived) - for (t of this.#targets) // remaining targets, not #backup - t._runPost(); - if (this.#post) { - const post = this.#post; // this.#post can be set in post() - if (!this.#keepPost) // and it defaults to single-use. - this.#post = undefined; - post(this); - } + if (isArriving && !sts && (wasPlaying || this.isPausing)) { + const post = this.#post; // this.#post can be set in post() + if (post) { + if (!this.#keepPost) // and it defaults to single-use. + this.#post = undefined; + post(this); // run it } - const forceIt = !hasArrived; // an alternate interpretation - for (t of this.#backup) // #targets is (partially) spent - t._reset(sts, forceIt); // see ../docs/onArrival.svg - if (sts == E.empty) - this.clearTargets(); // sets #status = E.empty - else { - this.#targets = new Set(this.#backup); - this.#status = sts ?? E.arrived; + } + if (notCancel) { // requires targets intact + this.#status = sts; + if (runReset) // _reset() cascades down to [M]Easer + for (t of easieses) + t._reset(sts); + } + if (isArriving) { // evaluate #oneShot + for (t of easieses) { // cascade bottom-up, clearing targets + for (ez of t.easies) + if (ez.oneShot) + ez.clearTargets(); + if (t.oneShot) + t.clearTargets(); } } + if (isArriving && this.#oneShot) + this.clearTargets(); // sets #status = E.empty + else if (notCancel) + this.#targets = new Set(this.#backup); - if (wasPlaying) // .then() won't execute otherwise + if (wasPlaying) this.#promise.resolve(this.#status); return this.#status; } diff --git a/docs/diagram.css b/docs/diagram.css index c016dc2..6724fb5 100644 --- a/docs/diagram.css +++ b/docs/diagram.css @@ -6,7 +6,7 @@ path:not(.check), rect:not(#bg, .box), use[data-name], ellipse, polyline { stroke:#000; } -path:not(.check), rect:not(#bg, .box, .legend, .title) { +path:not(.check), rect:not(#bg, .box, .legend, .title), ellipse { filter:var(--shadow); } path { @@ -14,7 +14,7 @@ path { translate:0.5px 0.5px; } path:not(.check) { - marker-end:url(#arrowhead); + marker-end:url(#arrow-end); } rect:not(#bg, .box, .legend, .title) { height:40px; @@ -87,6 +87,7 @@ circle.on, text.on.active { fill: #000 } use.on.active { stroke:#000; fill:#FFF } +.pale-green { fill:#EFFFF7 } .palest-blue { fill:#F7FFFF } .pale-blue { fill:#EFF7FF } .pale-purple { fill:#F7EFFF } @@ -103,6 +104,7 @@ circle.on, .bg-end2a { fill:#005577 } .bg-end2b { fill:#428 } .bg-end2c { fill:#009 } -.bg-end-dead { fill:#666 } +.bg-end-dead { fill:#666 } +.border-gray { stroke:#555 } .no-shadow { filter:none } .shadow { filter:var(--shadow) } diff --git a/docs/onArrival.svg b/docs/onArrival.svg index 3077831..a3c1022 100644 --- a/docs/onArrival.svg +++ b/docs/onArrival.svg @@ -1,113 +1,167 @@ - + - - - - AFrame - Easies - Easy - EBase - - - - - - - + + + + + + + + _reset(sts, b) + + + + - - E.empty + + #oneShot clearTargets() - - - - + + + AFrame + Easies + Easy + [M]Easer - - + + #stop(sts, b) + - - clearTargets() + - - - - - - - + + - - E.empty - - - - undefined - - - else - - - - clearTargets() - - - _apply() + + + + + sts: E.status + ------or------ + #onArrival +        ² + + + + + + + + + undefined + + + E.noop + + + E.original + + + + else + + + + restore() + + + _apply() + + + + ¹ + Each function has its own constant + sts + : + + + arrive() = E.arrived + init() = E.initial + stop() = E.original + pause() = E.pausing + escape() = E.noop + cancel() = undefined + + + + + ² + if (b && #onArrival != undefined) + sts = #onArrival + - - #animate() - - - #stop(_, true) - _reset(_, false) - _reset(_, false) + + + + + AFrame.prototype.#animate() + calls: +  #stop(this.#onArrival, true) + where + true + signifies + isArriving + . + - #onArrival - #oneShot - #onArrival + + + AFrame.prototype.arrive().init().stop().pause().cancel() + call: +  #stop(sts)¹ + - ¹ - ¹ .oneShot = true; is an alias for .onArrival =  E.empty; + + Execution order is left-to-right then top-to-bottom. It starts with + AFrame.prototype.#stop() + , cascades down through + Easies + and + Easy.prototype._reset() + to + [M]Easer.prototype.restore() + or + apply() + [or noop] + , then cascades + back up through + Easy + and + Easies + to + AFrame + , executing each local + clearTargets() + if the local + #oneShot + is truthy. - - #oneShot - - - ² - ² if (!E.empty) - - - - - arrive() stop() return() pause() cancel() - - - #stop(sts, _) - _reset(sts, true) - _reset(sts, true) - - forceIt - forceIt - forceIt + \ No newline at end of file diff --git a/docs/raf.xlsx b/docs/raf.xlsx index ce9ce7c..4ed3a02 100644 Binary files a/docs/raf.xlsx and b/docs/raf.xlsx differ diff --git a/easy/easer.js b/easy/easer.js index 168a32b..45bebe2 100644 --- a/easy/easer.js +++ b/easy/easer.js @@ -7,9 +7,9 @@ import {E, Ez, Is, Easy} from "../raf.js"; import {CFunc} from "../prop/func.js" class EBase { - #assign; #autoTrip; #cElms; #cjs; #eKey; #elms; #evaluate; #iElm; - #loopByElm; #mask; #oneD; #peri; #plays; #prop; #restore; #setOne; - #space; #twoD; #value; + #assign; #autoTrip; #cElms; #cjs; #eKey; #elms; #evaluate; #hasF; + #iElm; #isSDE; #loopByElm; #mask; #oneD; #onLoop; #onLoopByElm; #peri; + #plays; #prop; #restore; #setOne; #space; #twoD; #value; _autoTripping; // the active autoTrip value during an animation @@ -19,6 +19,8 @@ class EBase { this.#oneD = o.oneD; this.#iElm = 0; // EaserByElm.proto.apply() calls _setElm() this.#loopByElm = o.loopByElm; + if (o.loopByElm) + this.onLoopByElm = o.onLoopByElm; } else { // Easer.proto.apply() calls _set() this._set = this.#setElms; @@ -47,12 +49,12 @@ class EBase { Ez.is(this, "MEaser"); // do it here so setters can use it below } // use setters for values not yet validated - this.#mask = o.mask; this.#cElms = o.l; - this.#twoD = o.twoD; this.#value = o.value; - this.#peri = o.peri; this.#restore = o.restore; - this.plays = o.plays; this.evaluate = o.evaluate; - this.eKey = o.eKey; this.autoTrip = o.autoTrip; - + this.#mask = o.mask; this.#cElms = o.l; + this.#twoD = o.twoD; this.#value = o.value; + this.#peri = o.peri; this.#restore = o.restore; + this.#isSDE = o.isSDE; this.#hasF = Boolean(o.f); + this.plays = o.plays; this.evaluate = o.evaluate; + this.eKey = o.eKey; this.autoTrip = o.autoTrip; Ez.is(this, "Easer"); } // static _validate() validates that obj is an instance an Easer class @@ -72,7 +74,6 @@ class EBase { this.#assign(this.#twoD, this.#mask, this.#value); this.#elms.forEach((elm, i) => this.#setOne(prop, elm, val[i])); this.#peri?.(this.#twoD, e); - //console.log(val.map(v => v.toFixed(2))); } // #runPeri() does not apply values to an element property, no elms, no prop #runPeri(e) { @@ -86,8 +87,6 @@ class EBase { this.#mask.forEach((m, i) => val[m] = oneD[i]); this.#setOne(this.#prop, elm, val); this.#peri?.(oneD, e, elm); - //console.log(val.join("")); - //console.log(oneD.map(v => v.toFixed(2))); } //============================================================================== // The two #setOne functions: @@ -113,10 +112,16 @@ class EBase { ); } //============================================================================== -// Getters, setters, and some related methods: +// Callbacks, getters, setters, and some related methods: get peri() { return this.#peri; } set peri(val) { this.#peri = Ez._validFunc(val, "peri"); } + get onLoop() { return this.#onLoop; } + set onLoop(val) { this.#onLoop = Ez._validFunc(val, "onLoop"); } + + get onLoopByElm() { return this.#onLoopByElm; } + set onLoopByElm(val) { this.#onLoopByElm = Ez._validFunc(val, "onLoopByElm"); } + get loopByElm() { return this.#loopByElm; } set loopByElm(val) { this.#loopByElm = Boolean(val); } @@ -139,20 +144,19 @@ class EBase { ?? (this.isMEaser ? this.#MEval : this.#Eval); } -// this.eKeys, this.autoTrip, this.plays are byEasy arrays for MEBase +// this.eKey, this.autoTrip, this.plays are byEasy arrays for MEBase +// this.eKey get eKey() { - return this.isMEaser - ? this.#eKey.slice() - : this.#eKey; + return this.isMEaser ? this.#eKey.slice() : this.#eKey; } set eKey(val) { const name = "eKey"; if (!this.isMEaser) - this.#eKey = EBase.#validEKey(val, name); + this.#eKey = this.#validEKey(val, name); else if (!Is.Arrayish(val)) - this.#eKey.fill(EBase.#validEKey(val, name)); + this.#eKey.fill(this.#validEKey(val, name)); else { - const eKey = Ez.toArray(val, name, EBase.#validEKey); + const eKey = Ez.toArray(val, name, this.#validEKey); const lNew = eKey.length; const lOld = this.#eKey.length; if (lNew != lOld) @@ -162,18 +166,30 @@ class EBase { this.#eKey = eKey; } } - static #validEKey(val, name) { - if (!Is.def(val)) - return E.value; // hard default - else if (Easy.eKey.includes(val)) + #validEKey(val, name) { + const isDef = Is.def(val); + if (this.#isSDE) { + if (isDef) { + const eKey = "The eKey property"; + const SDE = "because you created it using start-distance-end style" + if (val != E.unit) + Ez._mustBeErr(`${eKey} for this ${this.constructor.name}`, + `E.unit ${SDE}`); + else if (Is.def(this.#eKey)) + console.info(`${eKey} was already set to E.unit by default ${SDE}.`); + } + return E.unit; + } + if (!isDef) // hasFactor defaults to E.unit + return this.#hasF ? E.unit : E.value; + if (Easy.eKey.includes(val)) return val; else Ez._invalidErr(name, val, Easy._listE(name)); } +// this.autoTrip get autoTrip() { - return this.isMEaser - ? this.#autoTrip.slice() - : this.#autoTrip; + return this.isMEaser ? this.#autoTrip.slice() : this.#autoTrip; } set autoTrip(val) { // 3 states: true, false, undefined const validate = EBase.#validTrip; @@ -184,10 +200,9 @@ class EBase { static #validTrip(val) { return Is.def(val) ? Boolean(val) : val; } +// this.plays get plays() { - return this.isMEaser - ? this.#plays.slice() - : this.#plays; + return this.isMEaser ? this.#plays.slice() : this.#plays; } set plays(val) { // a positive integer or undefined const validate = EBase.#validPlays; @@ -195,26 +210,27 @@ class EBase { ? this.#tripPlays(val, "plays", this.#plays, validate) : validate(val); } - static #validPlays(val) { - return Ez.toNumber(val, "plays", undefined, ...Ez.intGrThan0); + static #validPlays(val) { // default, !neg,!zero,!float,!undef,!null + return Ez.toNumber(val, "plays", undefined, true, true, true, false, false); } - #tripPlays(val, name, cv, validate) { +// MEaser only: + #tripPlays(val, name, currentVal, validate) { const arr = Ez.toArray(val, name, validate, ...Ez.okEmptyUndef); if (!arr.length || (arr.length == 1 && arr[0] == val)) { - arr.length = cv.length; - arr.fill(arr[0]); + arr.length = currentVal.length; // should always be > 1 + arr.fill(arr[0]); // undefined || val } return arr; } -// Some MEaser-specific methods are here because of private members +// Some MEaser methods are here to accommodate shared private members: + meEKey(i, val) { + this.#eKey[i] = this.#meOne(val, "meEKey", this.#validEKey); + } meTrip(i, val) { this.#autoTrip[i] = this.#meOne(val, "meTrip", EBase.#validTrip); } mePlays(i, val) { - this.#plays [i] = this.#meOne(val, "mePlays", EBase.#validPlays); - } - meEKey(i, val) { - this.#eKey [i] = this.#meOne(val, "meEKey", EBase.#validEKey); + this.#plays[i] = this.#meOne(val, "mePlays", EBase.#validPlays); } #meOne(val, name, validate) { return this.isMEaser ? validate(val) // console.log returns undefined @@ -222,7 +238,7 @@ class EBase { } //============================================================================== // "Protected" methods: -// _autoTrippy() returns run-time autoTrip, falling back to ez.autoTrip or false +// _autoTrippy() returns run-time autoTrip, falls back to ez.autoTrip or false _autoTrippy(ez, autoTrip) { return autoTrip ?? ez.autoTrip ?? false; } // _zero() resets stuff before playback @@ -232,12 +248,18 @@ class EBase { if (this.#loopByElm) this.#iElm = 0; } -// _restore() reverts to the values from when this instance was created - _restore() { +// restore() reverts to the elms' values from when this instance was created, +// or the initial values if {enableRestore:false}. If called directly +// and !this.#restore, each e/ez.e must be initialized beforehand. + restore(e, ezs) { if (this.#restore) this.#prop.setEm(this.#elms, this.#restore); - else - throw new Error("These settings require explicitly enabling {restore:true}."); + else if (e) // Easer + this.apply(e); + else if (ezs) // MEaser + ezs.forEach(ez => this.apply(ez.e)); + //!!else + //!! Ez._cantErr("You", "restore values with {enableRestore:false}"); } // _nextElm() moves to the next element for loopByElm, cycles back to zero _nextElm(plugCV = this.isMEaser) { // plugCV for MEaser w/loopByElm diff --git a/easy/easies.js b/easy/easies.js index 1ef330d..09884fa 100644 --- a/easy/easies.js +++ b/easy/easies.js @@ -1,7 +1,7 @@ import {create} from "./efactory.js"; import {MEBase} from "./measer.js"; -import {E, Ez, Easy} from "../raf.js"; +import {E, Ez, Is, Easy} from "../raf.js"; export class Easies { #active; #byEasy; #byTarget; #easy2ME; #oneShot; #peri; #post; @@ -11,15 +11,20 @@ export class Easies { // Set.prototype properties and methods, so an Easies instance acts like a Set // (except add() doesn't return a value because that value would be a reference // to #easies, and I don't want users modifying it directly). - constructor(easies, onArrival, post) { - this.#easies = new Set(Ez.toArray(easies, "new Easies(arg1, ...): arg1", - Easy._validate, ...Ez.okEmptyUndef)); + constructor(easies, post) { + const arr = Ez.toArray( + easies, + "new Easies(arg1, ...): arg1", + Easy._validate, + ...Ez.okEmptyUndef); + + this.#easies = new Set(arr); + this.post = post; + this.#targets = new Set; // Set(MEaser) this.#byEasy = new Map; // Map(Easy, Map(Easer, plays)) this.#byTarget = new Map; // Map(MEaser, Map(Easy, plays)) - this.#easy2ME = new Map; // Map(Easy, MEaser) - this.post = post; - this.onArrival = onArrival; + this.#easy2ME = new Map; // Map(Easy, Set(MEaser)) Ez.is(this); Object.seal(this); } @@ -68,7 +73,7 @@ export class Easies { // newTarget() creates a MEaser or MEaserByElm instance and adds it to #targets newTarget(o) { - return create(o, this.#targets, Boolean(o.easies), "Single", Easy.name); + return create(o, this.#targets, Boolean(o.easies), "single", [Easy, Easies]); } // addTarget() validates a MEaser instance, adds it to #targets. returns it addTarget(t) { @@ -81,7 +86,7 @@ export class Easies { clearTargets() { this.#targets.clear(); } // static createFromTargets() uses targets.easies to create a new Easies - static createFromTargets(targets, onArrival, post) { + static createFromTargets(targets, post) { targets = Ez.toArray(targets, "createFromTargets(arg): arg", MEBase._validate); let easies, ez, t; @@ -90,7 +95,7 @@ export class Easies { for (ez of t.easies) // ez is Easy easies.add(ez); - easies = new Easies(easies, onArrival, post); + easies = new Easies(easies, post); easies.#targets = targets; return easies; } @@ -100,11 +105,11 @@ export class Easies { get peri() { return this.#peri; } set peri(val) { this.#peri = Ez._validFunc(val, "peri"); } -// this.post is an optional callback run after the last Easy has played +// this.post is a callback that runs on arrival, after the last Easy has played get post() { return this.#post; } set post(val) { this.#post = Ez._validFunc(val, "easies.post"); } -// this.oneShot causes _reset() to call clearTargets() +// this.oneShot runs clearTargets() on AFrame arrival get oneShot() { return this.#oneShot; } set oneShot(val) { this.#oneShot = Boolean(val); } @@ -113,7 +118,7 @@ export class Easies { for (const ez of this.#easies) ez.restore(); for (const mezr of this.#targets) - mezr._restore(); + mezr.restore(); } init(applyIt) { for (const ez of this.#easies) @@ -133,34 +138,31 @@ export class Easies { // "Protected" methods, called by AFrame instances: // _zero() helps AFrame.prototype.play() zero out before first call to _next() _zero(now = 0) { - let e2M, easies, easy, map, plays, set, t, tplays; - this.#active = new Set(this.#easies); // init the "live" set + let e2M, easies, easy, map, plays, t, tplays; + this.#active = new Set(this.#easies); // the "live" set - this.#byEasy.clear(); + this.#byEasy.clear(); // Easies for (easy of this.#easies) { - easy._zero(now); // cascade the zeroing-out process down - map = new Map; // map target to plays - for (t of easy.targets) - map.set(t, t.plays || easy.plays); + easy._zero(now); // cascade the zeroing-out down + map = new Map; // map target to plays + for (t of easy.targets) // fall back to easy.plays + map.set(t, t.plays ?? easy.plays); this.#byEasy.set(easy, map); } - this.#byTarget.clear(); - e2M = this.#easy2ME; + + this.#byTarget.clear(); // MEasers + e2M = this.#easy2ME; // Map(Easy, Set(MEaser)) e2M.clear(); for (t of this.#targets) { - easies = t.easies; // getter returns a shallow copy - tplays = t.plays; // ditto + easies = t.easies; // getter returns a shallow copy + tplays = t.plays; // ditto plays = new Array(easies.length); - easies.forEach((ez, i) => { - plays[i] = tplays[i] || ez.plays; - set = e2M.get(ez); - if (set) - set.add(t); - else { - set = new Set; - set.add(t); - e2M.set(ez, set); - } + easies.forEach((ez, i) => { // fall back to ez.plays + plays[i] = tplays[i] ?? ez.plays; + if (e2M.has(ez)) + e2M.get(ez).add(t); + else + e2M.set(ez, new Set([t])); }); this.#byTarget.set(t, plays); } @@ -170,11 +172,14 @@ export class Easies { for (const ez of this.#active) ez._resume(now); } -// _reset() helps AFrame.prototype.#cancel() reset this to the requested state. -// see ../../docs/onArrival.svg for flow. - _reset(sts, forceIt) { +// _runPost() helps AFrame.prototype.#stop() by conforming to ACues + _runPost() { + this.#post?.(this); + } +// _reset() helps AFrame.prototype.#stop() reset this to the requested state. + _reset(sts) { for (const ez of this.#easies) // easys must go first because measers - ez._reset(sts, forceIt); // use their values. + ez._reset(sts); // use their values. let t; // #targets is Set(MEaser) if (sts == E.original) @@ -190,26 +195,17 @@ export class Easies { t._apply(vals); } } - // Easies.prototype has no on onArrival property, only oneShot - if ((forceIt && sts == E.empty) || this.#oneShot) - this.clearTargets(); - } -// _runPost() helps AFrame.prototype.#cancel() run .post() for unfinished ACues - _runPost() { - for (const ez of this.#active) - ez.post?.(ez); - this.#post?.(this); } //============================================================================== // _next() is the animation run-time. The name matches ACues.protytope._next(). -// AFrame.prototype.#animate() runs it once per frame. It runs ez._easeMe() to -// get eased values, and t._apply() to apply those values to #targets. +// AFrame.prototype.#animate() runs it once per frame. It runs easy._easeMe() +// to get eased values, and t._apply() to apply those values to #targets. // Returns true upon arrival, else false. No easies means no targets. _next(timeStamp) { - let byElm, e, e2, easers, easy, map, noWait, plays, set, sts, t, - val, val2; + let byElm, e, e2, easers, easy, map, nextElm, noWait, plays, set, sts, + t, val, val2; - // Execute each easy + // Execute every active easy for (easy of this.#active) easy._easeMe(timeStamp); @@ -223,39 +219,53 @@ export class Easies { if (sts > E.waiting) // apply it for (t of easers) t._apply(e); - else { // arrive, trip, or loop + else { // arrive, trip, loop e2 = easy.e2; - noWait = !e.waitNow; + noWait = !e.waitNow; // !tripWait, !loopWait if (sts == E.tripped) { for (t of easers) { // delete non-autoTrippers t._apply(t._autoTripping && noWait ? e2 : e); if (!t._autoTripping) - map.delete(t); - } + map.delete(t);// !autoTrip means plays = 1, + } // and loopByElm = false. + if (map.size) + easy.onAutoTrip?.(easy, map); } else { // sts == E.arrived for (t of easers) { - byElm = t.loopByElm; plays = map.get(t); - t._apply(!byElm && noWait && plays > 1 - ? e2 // loop w/o wait - : e); // arrive, loop w/wait, loopByElm - if (!byElm || !t._nextElm()) { - plays--; - plays ? map.set(t, plays) - : map.delete(t); + byElm = t.loopByElm; + if (!byElm && noWait && plays > 1) { + t._apply(e2); + console.log("t._apply(e2):", e2, e); } + else + t._apply(e); + + //t._apply(!byElm && noWait && plays > 1 + // ? e2 // loop w/o wait + // : e); // arrive, loop w/wait, loopByElm + // // _nextElm() increments/returns #iElm + nextElm = byElm && t._nextElm(); + if (!nextElm) + --plays ? map.set(t, plays) : map.delete(t); else if (noWait) - t._apply(e2); // loopByElm w/o wait + t._apply(e2); // loopByElm w/o wait, next elm + if (plays) { + t.onLoop?.(easy, t); + if (nextElm) + t.onLoopByElm?.(easy, t); + } } } } if (!map.size) this.#byEasy.delete(easy); } + // Process #targets, the MEasers for ([t, plays] of this.#byTarget) { - val = []; // val is sparse like t.#calcs + val = []; // val is sparse like t.#calcs if (!t.loopByElm) { plays.forEach((p, i) => { easy = t.easies[i]; @@ -263,9 +273,9 @@ export class Easies { sts = e.status; if (sts != E.waiting) { if (--p && !sts && !e.waitNow) - e = easy.e2 //!!e2 for loopNoWait, not tripNoWait?? + e = easy.e2 //!!e2 for loopNoWait, not tripNoWait?? val[i] = t.eVal(e, i); - + // arrived || tripped and done if (!sts || (sts == E.tripped && !t._autoTripping[i])) { if (!p) { // no more plays, arriving delete plays[i]; // delete preserves order/indexes @@ -307,8 +317,8 @@ export class Easies { else if (sts > E.waiting) val2[i] = t.eVal(e, i); }); - t._apply(val); // apply to current elm - if (!t._nextElm(true)) { // go to next elm or arrive + t._apply(val); // apply to current elm + if (!t._nextElm(true)) { // go to next elm or arrive plays.forEach((p, i) => p > 1 ? --plays[i] : delete plays[i]); if (!plays.some(v => v)) @@ -318,6 +328,7 @@ export class Easies { t._apply(val2); } } + // Clean up and return for (easy of this.#active) { if (!this.#byEasy.has(easy) && !this.#easy2ME.has(easy)) { @@ -326,21 +337,18 @@ export class Easies { } else { e = easy.e; - if (!e.status) { - if (e.waitNow) { - e.status = E.waiting; - e.waitNow = false; - } - else - e.status = E.outbound; - - easy.loop?.(easy); - } + if (e.waitNow) // similar to Easy.proto._zero(): + e.status = E.waiting; //$$ else if (e.status == E.tripped) - e.status = E.inbound; + e.status = E.inbound; //$$ + else if (!e.status) { // E.arrived + e.status = E.outbound; //$$ + easy.onLoop?.(easy); // plays > 1 loop, not loopByElm + } + e.waitNow = false; } } - this.#peri?.(this); // wait until everything is updated - return !this.#active.size; + this.#peri?.(this); // wait until everything is updated + return !this.#active.size; // no active Easys returns true } } \ No newline at end of file diff --git a/easy/easy-construct.js b/easy/easy-construct.js index d83cede..ebbe177 100644 --- a/easy/easy-construct.js +++ b/easy/easy-construct.js @@ -1,28 +1,84 @@ // Not exported by raf.js // export everything -export {override, spreadToEmpties, legText, legNumber, getType, legType, getIO, - splitIO, toBezier, toNumberArray}; +export {prepLegs, override, spreadToEmpties, legText, legNumber, legUnit, + getType, getIO, splitIO, toBezier, toNumberArray}; -import {E, Ez, Is, Easy} from "../raf.js"; +import {E, Is, Ez, Easy} from "../raf.js"; +import {steps} from "./easy-steps.js" import {EBezier} from "./ebezier.js"; //============================================================================== +// prepLegs() collects legsTotal and o.emptyLegs for spreading #time or #count +// across legs, and sets #wait, #legsWait, and #time or #count. +function prepLegs(o, type, s, e, w, tc, isInc) { // tc is "time" or "count" + let + legsTotal = 0, + legsWait = 0; + o.emptyLegs = []; + o.legs.forEach((leg, i) => { + leg.prev = o.legs[i - 1]; // overwritten by stepsToLegs() + leg.next = o.legs[i + 1]; // ditto + legNumber(leg, s, i); // all non-incremental legs define + legNumber(leg, e, i); // start and end. + legNumber(leg, w, i, ...Ez.defZero); + legsWait += leg[w]; + // time and count require extra effort + legNumber(leg, tc, i, ...Ez.undefGrThan0); + legType(o, leg, i, type, isInc); + if (leg.type == E.steps) + steps(o, leg); // E.steps: leg.timing can set leg.time + if (leg[tc]) + legsTotal += leg[tc]; // accumulate leg.time|count + else + o.emptyLegs.push(leg); // will receive o.spread + }); + + o.cEmpties = o.emptyLegs.length; // process o[tc] and legsTotal: + if (!isInc) + legsTotal += legsWait; + if (o[tc]) { + o.leftover = o[tc] - legsTotal; + if (o.cEmpties) { // calculate o.spread + if (o.leftover <= 0) + throw new Error(`${o.cEmpties} legs with ${tc} undefined ` + + "and nothing left over to assign to them."); + //--------------------------------- + o.spread = o.leftover / o.cEmpties; + } + else // legsTotal overrides o[tc] + override(tc, undefined, o, "every", Ez.defGrThan0, legsTotal); + } + else if (!o.cEmpties) + o[tc] = legsTotal; // o[tc] is previously undefined + else if (!isInc) + throw new Error("You must define a non-zero value for " + + `obj.${tc} or for every leg.${tc}.`); + //-------------- + return legsWait; +} +//============================================================================== // override() overrides o vs leg for start, end, wait, time|count // prop is a string property name, not a Prop instance -function override(prop, leg, o, name, legVal = leg[prop], oVal = o[prop]) { - if (Is.def(legVal)) { - if (Is.def(oVal)) { - name += " leg"; - if (legVal != oVal) - console.log( - `You defined obj.${prop} and ${name}.${prop}, and they ` - + `don't match.${leg ? "" : "The sum of"} ` - + `${name}.${prop} overrides obj.${prop}`); +// called by constructor(), #prepLegs() +function override(prop, leg, o, txt, args, legVal = leg[prop]) { + let obj, val; + const oVal = o[prop]; + + if (Is.def(legVal)) { // leg over o + if (Is.def(oVal) && legVal != oVal) { + txt += ` leg.${prop}`; + console.info( + `You defined obj.${prop} and ${txt}, and they don't match. ` + + `${leg ? "" : "The sum of "}${txt} overrides obj.${prop}`); } - o[prop] = legVal; + [obj, val] = [o, legVal]; } - else if (Is.def(oVal)) - leg[prop] = oVal; + else if (Is.def(oVal)) // oVal over default value + [obj, val] = [leg, oVal]; + else + obj = o; // default: o[prop] = args[0], leg[prop] = undefined + + obj[prop] = Ez.toNumber(val, prop, ...args); } function spreadToEmpties(leg, tc, v) { do { leg[tc] = v; } while((leg = leg.next)); @@ -35,6 +91,11 @@ function legNumber(leg, name, i, defVal, notNeg, notZero) { defVal, notNeg, notZero); return leg[name]; } +// legUnit() leg.unit = this._legUnit() = the e.unit end value for a leg, +// called by _finishLegs(), stepsToLegs(). +function legUnit(leg, start, dist) { // start is o.start, not leg.start + return (leg.end - start) / Math.abs(dist); +} //============================================================================== // E.pow, E.bezier, E.steps, and E.increment require a value in a // property of the same name. If a leg inherits the value, we might @@ -76,7 +137,7 @@ function legType(o, leg, i, defaultType, isInc) { // For E.steps and E.increment additional validation occurs later const name = Easy.type[type]; if (!Is.def(leg[name])) { - if (!Is.def(o[name]) && type < E.steps) + if (!Is.def(o[name]) && type < E.steps) Ez._mustBeErr(`${legText(i, name)}`, "defined"); //------------------ leg[name] = o[name]; @@ -136,9 +197,10 @@ function toNumberArray(val, name, notNeg) { return Ez.toArray(val, name, notNeg ? validNotNeg : validNumber); } -function validNumber(val, name) { // it can be any number, but not undefined - return Ez.toNumber(val, name, undefined, false, false, true); -} -function validNotNeg(val, name) { // it must be a positive number +function validNumber(val, name) { // val can be any number, but not undefined + return Ez.toNumber(val, name, null, false, false, false, true); +} // !neg, !zero,!float,!undef + +function validNotNeg(val, name) { // val must be a positive number return Ez.toNumber(val, name, ...Ez.defNotNeg); } \ No newline at end of file diff --git a/easy/easy-steps.js b/easy/easy-steps.js index 4a7341f..82b29b5 100644 --- a/easy/easy-steps.js +++ b/easy/easy-steps.js @@ -1,7 +1,7 @@ // Not exported by raf.js export {steps, stepsToLegs}; -import {toNumberArray} from "./easy-construct.js" +import {legUnit, toNumberArray} from "./easy-construct.js" import {E, Ez, Is, Easy} from "../raf.js"; const s = "steps"; // inside this module they aren't "time" & "start" @@ -14,18 +14,19 @@ const t = "timing"; // If leg.timing is Array-ish, leg.steps can be undefined, otherwise // the legs.steps or leg.steps.length must match leg.timing.length. function steps(o, leg) { - let stepsIsN = Is.Number(leg[s]); - let stepsIsA = Is.def(leg[s]) && !stepsIsN; - let c, l; - if (stepsIsA) { // validate/convert leg[s] + let c, l, stepsIsA, + stepsIsN = Is.Number(leg[s]); + + if (!stepsIsN && Is.def(leg[s])) { // numeric string, array, or invalid const n = Is.A(leg[s]) ? NaN : parseFloat(leg[s]); - try { - if (Number.isNaN(n)) { // slice() preserves user array + try { // validate/convert leg[s] + if (Number.isNaN(n)) { // ...slice() preserves user array leg[s] = toNumberArray(leg[s].slice(), s); - leg.stepsReady = true; // it's an array of numbers + leg.stepsReady = true; + stepsIsA = true; // it's an array of numbers } else { - leg[s] = n; // it was converted to a number + leg[s] = n; // parseFloat() converted it to a number stepsIsA = false; stepsIsN = true; } @@ -61,14 +62,16 @@ function steps(o, leg) { console.warn("Your timing array extends past the total" + "leg.time, and has overriden leg.time: " + `${last} > ${leg.time}`); - leg.time = last; // avoids spreadToEmpties() and errors + leg.time = last; // avoids spreadToEmpties(), errors } leg.timingReady = true; } else { // auto-generate linear waits based const // on steps/ends and jump. - j = "jump", // E.end is the CSS steps() default - jump = Number(leg[j] ?? o[j] ?? E.end); + j = "jump", + jump = leg.easy // eased values means start = 0, end = 1 + ? E.end // E.end is the CSS steps() default + : Number(leg[j] ?? o[j] ?? E.end); if (!Easy.jump[jump]) Ez._invalidErr(j, jump, Easy._listE(j)); @@ -91,13 +94,13 @@ function steps(o, leg) { leg.waits = Array.from({length:l}, (_, i) => (i + offset) / c); } } -// stepsToLegs() helps _finishlegs() turn 1 leg into >1 legs for _calc() -function stepsToLegs(o, leg, ez, idx, last, lastLeg) { +// stepsToLegs() helps _finishLegs() turn 1 leg into >1 legs for _calc() +function stepsToLegs(o, leg, dist, idx, last) { let ends, retval, waits; if (leg.timingReady) // leg.timing is an array of wait times waits = leg[t]; else if (leg[t]) // leg.timing is an Easy instance - waits = easeSteps(leg[t], leg.waits, 1, 0, leg.time, false, t); + waits = easeSteps(leg[t], leg.waits, 1, 0, leg.time, t); else // leg.timing is undefined waits = leg.waits.map(v => v * leg.time); // leg.waits is 0-1, portion of time @@ -106,12 +109,10 @@ function stepsToLegs(o, leg, ez, idx, last, lastLeg) { if (leg.stepsReady) // leg.steps is already an array of step values ends = leg[s]; else if (leg.easy) // generate eased steps - ends = easeSteps(leg.easy, waits, leg.time, leg.start, leg.dist, - leg.down, "easy"); + ends = easeSteps(leg.easy, waits, leg.time, leg.start, leg.dist, "easy"); else { const j = leg.dist / l; // generate linear steps - ends = leg.down ? Array.from(LENGTH, (_, i) => leg.start - ((i + 1) * j)) - : Array.from(LENGTH, (_, i) => (i + 1) * j + leg.start); + ends = Array.from(LENGTH, (_, i) => leg.start + (j * (i + 1))); } // transform the steps into legs: const legs = Array.from(LENGTH, () => new Object); @@ -119,7 +120,7 @@ function stepsToLegs(o, leg, ez, idx, last, lastLeg) { obj.type = E[s]; obj.io = E.in; // must be defined obj.end = ends[i]; // the step value - obj.unit = ez._legUnit(obj, o.start, leg.down); + obj.unit = legUnit(obj, o.start, dist); obj.prev = legs[i - 1]; obj.next = legs[i + 1]; obj.time = 0; // steps don't have a duration @@ -140,8 +141,7 @@ function stepsToLegs(o, leg, ez, idx, last, lastLeg) { if (idx == last) { // #lastLeg o.end = legs[l].end; o.time -= leftover; - if (lastLeg) - retval = {leg:legs[l]}; + retval = {leg:legs[l]}; } else { legs[l].next = leg.next; @@ -157,35 +157,33 @@ function stepsToLegs(o, leg, ez, idx, last, lastLeg) { // fixed values, which is pointless. // For eased values, jump:E.start has no effect because time=0 produces // value=0. So E.start is the same as E.none, E.end same as E.both. -function easeSteps(ez, nows, time, start, dist, isDown, name) { - Easy._validate(ez, name); // phase one validation - if (ez.isIncremental) // phase two +function easeSteps(ez, nows, time, start, dist, name) { + Easy._validate(ez, name); // phase 1 validation + if (ez.isIncremental) // phase 2: can't be E.increment Ez._cantBeErr(name, "class Incremental"); - //-------------------------------------- + //------------------------------------------ const ezDown = ez.end < ez.start, isTiming = (name[0] == "t"); let leg = ez._firstLeg; - do { - if (leg.type == E[s]) // phase three + do { // for each leg: + if (leg.type == E[s]) // phase 3: can't be E.steps Ez._cantBeErr(name, `type:E.${s}`); - if (isTiming // phase four - && (leg.down != ezDown - || (leg.type >= E.back && leg.type <= E.bounce) - || (leg.type == E.bezier && Ez.unitOutOfBounds(leg.bezier.array)))) { - const - msg = Ez._cantBe(name, "an Easy that changes direction. " - + "Time only moves in one direction"); - throw new Error(msg, {cause:"reverse time"}); //!!better *not* as string... - } + if (isTiming && (ezDown != leg.dist < 0 // phase 4: can't mix directions + || (leg.type >= E.back && leg.type <= E.bounce) + || (leg.type == E.bezier && Ez.unitOutOfBounds(leg.bezier.array)))) + Ez._cantBeErr( + name, + "an Easy that changes direction. Time only moves in one direction", + {cause:"reverse time"} // better *not* as string... + ); } while ((leg = leg.next)); - //---------------------------------------- validation complete - ez._zero(0); // prep for pseudo-animation - const d = time / ez.time; // d for divisor - const prop = ezDown ? E.comp : E.unit; // nows always ascends - const sign = isDown ? -1 : 1; + //------------------------------------------// validation complete + ez._zero(0); // prep for pseudo-animation + const d = time / ez.time; // d for divisor + const prop = ezDown ? E.comp : E.unit; // nows always ascends return nows.map(v => { ez._easeMe(v / d); - return start + (ez.e[prop] * dist * sign); + return start + (ez.e[prop] * dist); }); } \ No newline at end of file diff --git a/easy/easy.js b/easy/easy.js index a347940..e97142d 100644 --- a/easy/easy.js +++ b/easy/easy.js @@ -1,21 +1,13 @@ import {easings} from "./easings.js"; import {create} from "./efactory.js"; import {EBase} from "./easer.js"; -import {steps, stepsToLegs} from "./easy-steps.js" -import {override, spreadToEmpties, legNumber, getType, legType, getIO, splitIO, +import {stepsToLegs} from "./easy-steps.js" +import {prepLegs, override, legUnit, spreadToEmpties, getType, getIO, splitIO, toBezier} from "./easy-construct.js" import {E, Ez, Is, Easies} from "../raf.js"; export class Easy { - #autoTrip; #base; #dist; #e; #end; #flipTrip; #lastLeg; #legsWait; - #loopWait; #onArrival; #onLoop; #oneShot; #peri; #plays; #post; #pre; - #reversed; #roundTrip; #start; #targets; #time; #tripWait; #wait; #zero; - - _leg; _now; _inbound; // shared with Incremental.prototype._calc() - _value; // for Incremental only, but used in shared code here - _firstLeg; // shared with easeSteps(), may be useful elsewhere... - // Public string arrays for enums and Copied! + + +
+
+ +
+

+

+
+
+ +
+
\ No newline at end of file diff --git a/test/common.css b/test/common.css index 253449e..1f5c6de 100644 --- a/test/common.css +++ b/test/common.css @@ -16,16 +16,18 @@ --border-hidden:1px solid transparent; --btn-size: 1.75rem; - --8th: calc(1rem / 8); - --3-8ths: calc(3 * var(--8th)); - --5-8ths: calc(5 * var(--8th)); + --8th: calc(1rem / 8); + --3-8ths: calc(3 * var(--8th)); + --5-8ths: calc(5 * var(--8th)); + --7-8ths: calc(1rem - var(--8th)); + --16th: calc(1rem / 16); --3-16ths: calc(3 * var(--16th)); --5-16ths: calc(5 * var(--16th)); --7-16ths: calc(7 * var(--16th)); --9-16ths: calc(9 * var(--16th)); - --11-16ths: calc(11 * var(--16th)); - --15-16ths: calc(1rem - var(--16th)); + --11-16ths:calc(11 * var(--16th)); + --15-16ths:calc(1rem - var(--16th)); } html * { font-family:"Roboto Mono", monospace; @@ -183,7 +185,8 @@ check-box[checked] { color: var(--dark-blue); } check-box[disabled] { - pointer-events:none; + stroke:var(--medium-gray); + color: var(--medium-gray); } check-box:hover { fill-opacity:0.05; @@ -296,6 +299,9 @@ input.mt1px { margin-top: 1px; } font-family:sans-serif; margin:0 0.25rem; } +.notMono { + font-family:"Roboto", sans-serif; +} /* Specialty */ #copied { @@ -312,7 +318,29 @@ input.mt1px { margin-top: 1px; } border-radius:10%; padding:var(--3-16ths); } -::backdrop { +#msgBox { + margin:0 auto; /* javascript sets top */ + padding:var(--7-8ths); +} +#title, #msg { + white-space:pre-wrap; + line-height:1.25rem; + margin-left:0.5rem; + color:black; +} +#title{ + font-weight:500; + padding-top:0.125rem; +} +#msg { + font-weight:400; +} +#close { + width:fit-content; + align-self:center; + margin-top:0.5rem; +} +#dialog::backdrop { backdrop-filter:blur(var(--16th)); background-color:#AAA7; } \ No newline at end of file diff --git a/test/common.js b/test/common.js index 32a61bc..3224b47 100644 --- a/test/common.js +++ b/test/common.js @@ -1,26 +1,25 @@ import {Ez} from "../raf.js"; -// export everything except errorMessage -export const ZERO = "0", ONE = "1", TWO = "2"; - -export const MILLI = 1000; // for milliseconds, and #chart is 1000 x 1000 -export const COUNT = 3; // multi.js: easys.length, loopByElm: elms.length - -export const PLAYS = "plays"; -export const CHANGE = "change"; // event names -export const CLICK = "click"; -export const INPUT = "input"; -export const SELECT = "select"; -export const EASY_ = "Easy-"; // localStorage -export const MEASER_ = "MEaser-"; - -export const LITE = ["lo","hi"]; - -export const elms = {}; // the HTML elements of interest -export const dlg = {}; // sub-elements -export const g = { // g for global, these properties are read-write: - notLoopWait:null, // notX = !isX, bools to help choose easy.e vs easy.e2 - notTripWait:null // in multi.js they are arrays of bools +export const // export everything: +MILLI = 1000, // for milliseconds, and #chart is 1000 x 1000 +COUNT = 3, // multi.js: easys.length, loopByElm: elms.length +ZERO = "0", ONE = "1", TWO = "2", +PLAYS = "plays", +CHANGE = "change", // event names +CLICK = "click", +INPUT = "input", // event & tag name +BUTTON = "button", // tag names +SELECT = "select", +LABEL = "label", +DIV = "div", +EASY_ = "Easy-", // localStorage +MEASER_ = "MEaser-", +LITE = ["lo","hi"], // playback formatting +elms = {}, // the HTML elements of interest +dlg = {}, // sub-elements +g = { // g for global, these properties are read-write: + notLoopWait:null, // notX = !isX, bools to help choose easy.e vs easy.e2 + notTripWait:null // in multi.js they are arrays of bools }; //====== wrappers for addEventListener() ======================================= export function addEventByClass(type, name, obj, func) { @@ -50,22 +49,25 @@ export function boolToString(b) { // for localStorage and -

End:  1000

- -

unit:/1

-

comp:/1

- - +

End:   1000

-
-
- - +
+
+
+ +
-
+
+
+
Copy:
@@ -175,21 +177,21 @@
- + - - - - - - - - + + + + + + + +
-
+
@@ -202,6 +204,19 @@
+ + +
+
+ +
+

+

+
+
+ +
+
diff --git a/test/easings/index.js b/test/easings/index.js index 4c9fcb3..e92d00d 100644 --- a/test/easings/index.js +++ b/test/easings/index.js @@ -1,6 +1,5 @@ // export everything but update -export {initEzXY, newEzY, pointToString, updateTrip, twoLegs, isBezier, - bezierArray}; +export {initEzXY, newEzY, updateTrip, twoLegs, isBezier, bezierArray}; export let ezY; export const @@ -37,22 +36,20 @@ function newEzY(obj) { try { ezY = new Easy(obj); } catch (err) { - if (err.cause == "reverse time") // maybe eventually a switch?? + switch (err.cause) { + case "multiPlayTripNoAuto": + case "reverse time": alert(err.message); - else + break; + default: errorAlert(err); + } return; } g.easies.add(ezY); return obj; } //============================================================================== -// pointToString() converts x and y to a comma-separated pair of coordinates, -// called by drawEasing(), drawSteps(). -function pointToString(x, y) { - return `${x.toFixed(2)},${y.toFixed(2)}`; -} -//============================================================================== // updateTrip() updates roundTrip display, called by openNamed(), initEasies(), // change.roundTrip() function updateTrip(isTrip = elms.roundTrip.checked) { diff --git a/test/easings/msg.js b/test/easings/msg.js index 0fbe59b..65cbad3 100644 --- a/test/easings/msg.js +++ b/test/easings/msg.js @@ -8,8 +8,8 @@ import {Ez, P, U} from "../../raf.js"; import {msecs, secs} from "../update.js"; import {listenInputNumber, formatInputNumber, isInvalid, invalidInput, maxMin} from "../input-number.js"; -import {MILLI, CLICK, INPUT, CHANGE, elms, addEventToElms, addEventsByElm, - addEventByClass, toggleClass, isTag, boolToString} +import {MILLI, BUTTON, DIV, LABEL, INPUT, CLICK, CHANGE, elms, addEventToElms, + addEventsByElm, addEventByClass, toggleClass, boolToString} from "../common.js"; import {chart, refresh} from "./_update.js"; @@ -17,57 +17,58 @@ import {isSteps} from "./steps.js"; import {OTHER, isBezier} from "./index.js"; let sgInputs; -const LOCK = "lock"; -const LOCKED = "locked" -const locks = ["lock_open", LOCK]; // boolean acts as index +const +LOCK = "lock", +LOCKED = "locked", +locks = ["lock_open", LOCK]; // boolean acts as index //============================================================================== // loadMSG() is called by easings.loadIt() once per session function loadMSG() { - let elm, id, key, obj, val; - const CLEAR = "clear"; + let div, elm, id, key, obj, val; + const + CLEAR = "clear", + divSplit = elms.divSplit; elm = elms.clearMid; Ez.readOnly(elm, OTHER, elms.mid); Ez.readOnly(elms.mid, CLEAR, elm); - const div = elms.divSplit; - const divGap = Ez.toCamel("div", MSG[2]); - div.id = ""; - elms[divGap] = div.cloneNode(true); + divSplit.id = ""; + elms.divGap = divSplit.cloneNode(true); elms.divGap.style.marginTop = "1" + U.px; for (id of MSG.slice(1)) { // split, gap obj = {}; - for (elm of elms[Ez.toCamel("div", id)].children) { - if (isTag(elm, INPUT)) { - elm.id = id; - elms[id] = elm; - Ez.readOnly(obj[CLEAR], OTHER, elm); - for ([key, val] of Object.entries(obj)) - Ez.readOnly(elm, key, val); - } - else if (isTag(elm, "label")) { - elm.htmlFor = id; - if (!elm.textContent) // 5 = "split".length - elm.innerHTML = Ez.initialCap(id).padStart(5) + ":"; - } - else //
+ + +
+
+ +
+

+

+
+
+ +
+
+ diff --git a/test/named.js b/test/named.js index 12567f1..34d3e8e 100644 --- a/test/named.js +++ b/test/named.js @@ -27,8 +27,10 @@ async function loadNamed(isMulti, dir, _load) { elms.multis.addEventListener(CHANGE, openNamed, false); else if (elms.save) { elms.revert.addEventListener(CLICK, openNamed, false); - const btns = [elms.save, elms.preset, elms.delete, dlg.ok, dlg.cancel]; - addEventsByElm(CLICK, btns, click); + addEventsByElm( + CLICK, + [elms.save, elms.preset, elms.delete, dlg.ok, dlg.cancel, dlg.close], + click); } return import(`${dir}_named.js`).then(namespace => { ns = namespace; @@ -89,6 +91,9 @@ const click = { }, cancel() { elms.dialog.close(); + }, + close() { + elms.msgBox.close(); } } // reply all, u turn left, turn left, subdirectory arrow left //============================================================================== diff --git a/test/play.js b/test/play.js index 9df335d..ca7d0f3 100644 --- a/test/play.js +++ b/test/play.js @@ -3,12 +3,12 @@ export {loadPlay, changeStop}; import {E, P} from "../raf.js"; import {ezX, raf} from "./load.js"; -import {updateCounters, updateTime, updateDuration, setFrames} - from "./update.js"; +import {frameIndex, updateCounters, updateTime, updateDuration, setFrames, + prePlay} from "./update.js"; import {MILLI, ZERO, ONE, TWO, LITE, CHANGE, elms, g, toggleClass, errorAlert} from "./common.js"; -let ns; // _update.js namespace: refresh, formatPlay, flipZero (easings) +let ns; // _update.js namespace: refresh, formatPlay, postPlay (easings) //============================================================================== // -- Button States -- // initial: PLAY STOP = stop disabled @@ -34,9 +34,9 @@ function changePlay() { raf.pause(); else { // PLAY or RESUME if (elms.play.value == PLAY) { - g.frameIndex = 0; + prePlay(); ns.newTargets(); - ns.formatPlayback?.(true); + ns.formatPlayback?.(true); // multi only elms.stop.disabled = false; } formatPlay(true); @@ -44,19 +44,17 @@ function changePlay() { elms.play.value = PAUSE; elms.stop.value = STOP; raf.play() - .then(sts => { // ...some time later: - if (!resetPlay(sts == E.pausing)) { - if (ezX.e.status > E.tripped) - return; // user clicked #stop - //---------------------- // else animation ends: - setFrames(g.frameIndex); // don't call timeFrames(), updateTime() - updateDuration(raf.elapsed / MILLI); - ns.flipZero?.(); // color doesn't need it //!! multi does!! - elms.x .value = g.frameIndex; - elms.stop.value = RESET; - if (!ezX.e.status) // only enabled for status == E.tripped + .then(sts => { // if (!pausing and !#stop.onclick) + if (!resetPlay(sts == E.pausing) || ezX.e.status <= E.tripped) { + if (!ezX.e.status) // E.arrived or E.tripped elms.play.disabled = true; - } // else sts == E.pausing + elms.stop.value = RESET; + // don't call updateTime(), timeFrames() + updateDuration(raf.elapsed / MILLI); + setFrames(frameIndex); + elms.x.value = frameIndex; + ns.postPlay?.(); // color doesn't need it //!!multi does + } }).catch(errorAlert); } } @@ -66,23 +64,24 @@ function changePlay() { // and pseudoAnimate() is not called. function changeStop(evt) { if (elms.stop.disabled) return; // nothing to do that hasn't been done - //----------------------------- // only occurs if (!evt) + //----------------------------- elms.x.value = 0; elms.stop.value = STOP; elms.stop.disabled = true; // must precede ns.refresh() below!! elms.play.disabled = false; switch (elms.play.value) { - case RESUME: // stop = STOP, promise resolved - resetPlay(); - case PAUSE: // stop = STOP, promise unresolved - raf.stop(); // -RESUME: reset raf - break; // -PAUSE: cancel animation, resolve promise - case PLAY: // stop = RESET, play.disabled = true except - setFrames(); // when E.tripped and no autoTrip. - updateTime(); + case RESUME: // stop = STOP, e.status == E.pausing + resetPlay(); // was pausing, promise resolved previously + case PAUSE: // stop = STOP, e.status > E.tripped + raf.stop(true); // was playing, resolves promise, cancelAF() + break; // sets e.everything to E.original + case PLAY: // stop = RESET, e.status <= E.tripped + setFrames(); // play.disabled = true, except + updateTime(); // when E.tripped && !autoTrip. updateDuration(); } // only easings refresh() uses target arg: - ns.refresh(evt?.target); // calls pseudoAnimate()=>changeStop()!! + if (evt !== null) // null = called by pseudoAnimate() + ns.refresh(evt?.target); // calls pseudoAnimate()=>changeStop(null)!! updateCounters(); ns.formatPlayback?.(false); } diff --git a/test/update.js b/test/update.js index 48823e6..23105a4 100644 --- a/test/update.js +++ b/test/update.js @@ -1,11 +1,11 @@ -export {loadUpdate, inputX, timeFrames, updateTime, updateFrame, pseudoFrame, - updateCounters, formatNumber, updateDuration, setFrames, getFrames, - eGet, newEasies, pseudoAnimate}; - +export {loadUpdate, inputX, timeFrames, prePlay, updateTime, updateCounters, + formatNumber, updateDuration, setFrames, eGet, callbacks, updateFrame, + pseudoFrame, pseudoAnimate, newEasies}; export let msecs, secs, // alternate versions of time, both integers targetInputX, // only imported by easings/_update.js - frameCount // ditto by easings/non-steps.js + frameIndex, // the current frame + playZero // for measuring time between changePlay() and first frame ; export const D = 3, // D for decimals: .toFixed(D) = milliseconds, etc. @@ -27,11 +27,14 @@ import {loadPlay, changeStop} from "./play.js"; import {MILLI, COUNT, INPUT, elms, g, errorAlert} from "./common.js"; /* -import(_update.js): getFrame, initPseudo, updateX; - refresh, flipZero, formatPlayback pass through to play.js. +import(_update.js): formatDuration, formatFrames, getFrame, getMsecs, + initPseudo, isInitZero, setCounters, updateX; + postPlay and loopFrames for easings only. */ -let ns, // _update.js namespace - prevCount; // previous frameCount for scaling #x.value +let ns, // _update.js namespace + lastFrame, // frames.length - 1 + prevLast, // previous lastFrame for scaling #x.value + isContinuing; // isLooping || isAutoTripping during animation //============================================================================== // loadUpdate() is called by loadCommon() async function loadUpdate(isMulti, dir) { @@ -41,6 +44,7 @@ async function loadUpdate(isMulti, dir) { } Object.freeze(pad); + isContinuing = false; // not strictly necessary elms.x.addEventListener(INPUT, inputX, false); return import(`${dir}_update.js`).then(namespace => { ns = namespace; @@ -66,39 +70,30 @@ function timeFrames(evt) { else updateDuration(); } +// prePlay() helps changePlay(), exports can't be set outside the module +function prePlay() { + playZero = performance.now(); + frameIndex = 0; +} //============================================================================== // updateTime() is called by change.time(), changeStop(), and updateAll(). // !addIt is easings page, doesn't add targetInputX to ezX.targets // because next run is pseudoAnimate() and a different target. function updateTime() { - const f = prevCount ? elms.x.valueAsNumber / prevCount : 0; - prevCount = frameCount; // for next time - elms.x.value = Math.round(f * frameCount); + const f = prevLast ? elms.x.valueAsNumber / prevLast : 0; + prevLast = lastFrame; // for next time - ezX.time = msecs; - const addIt = !ns.flipZero; - if (addIt) // changing factor requires new target - ezX.cutTarget(targetInputX); // see newTargets() + elms.x.value = Math.round(f * lastFrame); + ezX.time = msecs; + const addIt = !ns.drawLine; + if (addIt) // changing factor requires new target + ezX.cutTarget(targetInputX); // see newTargets() - targetInputX = ezX.newTarget( - {elm:elms.x, prop:P.value, factor:frameCount / MILLI}, + targetInputX = ezX.newTarget( // for factor: eKey defaults to E.unit + {elm:elms.x, prop:P.value, factor:lastFrame}, addIt ); } -// updateFrame() consolidates easy.peri() code, records frames, sets textContent -// NOTE: Chrome pre-v120 currentTime can be > first frame's timeStamp. -function updateFrame(...args) { - const t = raf.elapsed; // frames[0] isn't modified by animation - if (t > 0) { // ignore frameZero & Chrome pre-v120 1st frame - const frm = ns.getFrame(t, ...args) - frames[++g.frameIndex] = frm; - updateCounters(g.frameIndex, frm); - } -} -// pseudoFrame() is the pseudoAnimation version of updateFrame() -function pseudoFrame(...args) { - frames[++g.frameIndex] = ns.getFrame(0, ...args); // 0 is dummy time -} // updateCounters() is called by inputX(), updateFrame(), and changeStop() function updateCounters(i = 0, frm = frames[i]) { formatNumber(i, pad.frame, 0, elms.frame); @@ -123,43 +118,63 @@ function formatNumber(n, digits, decimals, elm) { function updateDuration(val = secs) { return (elms.duration.textContent = ns.formatDuration(val, D)); } -//============================================================================== // setFrames() is called by timeFrames(), changePlay() function setFrames(val = Math.ceil(secs * FPS)) { elms.x.max = val; - frameCount = val; - frames.length = val + 1; //!!no more grow but don't shrink policy?? - let txt = frameCount.toString(); - if (ns.formatFrames) // multi and color - txt += "f"; - elms.frames.textContent = txt; -} -//------------------------------------------------------------------------------ -// getFrames() gets the full set of current frames. The frames array grows but -// doesn't shrink, frameCount makes it work, not to be confused with -// _update.js/getFrame(). -function getFrames() { - return frames.slice(0, frameCount + 1); + lastFrame = val; + frames.length = val + 1; + elms.frames.textContent = val.toString() + (ns.formatFrames ? "f" : ""); } //============================================================================== -// eGet() chooses ez.e or ez.e2, called by multi.getFrame() and easings.update() +// eGet() chooses ez.e or ez.e2, called by multi.getFrame(), easings.update(); +// multi defines notLW, notTW args; returns e2 when: +// unit is a whole number && (looping w/o wait || tripping w/o wait) function eGet(ez) { const e = ez.e; - return !(e.unit % 1) && ((g.notLoopWait && e.status == E.outbound) - || (g.notTripWait && e.status == E.inbound)) - ? ez.e2 - : e; -} -// newEasies() helps all the initEasies(), encapsulates try/catch into boolean -function newEasies(...args) { - try { - g.easies = new Easies(...args); - raf.targets = g.easies; - } catch (err) { - errorAlert(err, "new Easies() failed"); - return false; + if (isContinuing) { // isLooping || isTripping + isContinuing = false; + if (e.status == E.waiting) + return e; + else { + console.log("eGet:", ez.e2, ez.e) + return ez.e2 + } } - return true; + else + return e; +} +// centralized callbacks that affect eGet() and easings/drawLine(). +const callbacks = { + onAutoTrip () { isContinuing = true; }, + onLoop (ez) { loopIt(ez, "onLoop"); }, + onLoopByElm(ez) { loopIt(ez, "onLoopByElm"); } +} +// loopIt does the work for the loop callbacks +function loopIt(ez, txt) { // loopFrames is easings only, for drawLine() + isContinuing = true; // + 1 because it's prior to ++frameIndex + ns.loopFrames?.push(frameIndex + 1 + Number(ez.e.status == E.waiting)); + console.log(txt, ns.loopFrames?.at(-1), ns.loopFrames?.length); +} +//============================================================================== +// updateFrame() consolidates .peri() code: get t, increment frameIndex, set the +// current frame's value, and call updateCounters(). +// NOTE: Chrome pre-v120 currentTime can be > first frame's timeStamp. +function updateFrame(...args) { + const t = raf.elapsed; + if (t <= 0) //!! + console.log(`updateFrame(): ${t} <= 0`); //!! +//!!if (t > 0 || ns.isInitZero?.()) { + const frm = ns.getFrame(t, ...args); + frames[++frameIndex] = frm; // frames[0] isn't modified by animation + updateCounters(frameIndex, frm); +//!!} +//##updateCounters( +//## ++frameIndex, +//## frames[frameIndex] = ns.getFrame(raf.elapsed, ...args)); +} +// pseudoFrame() is the pseudoAnimation version of updateFrame() +function pseudoFrame(...args) { // 0 is dummy time + frames[++frameIndex] = ns.getFrame(0, ...args); } //============================================================================== // pseudoAnimate() populates the frames array via the .peri() callbacks, does @@ -168,15 +183,28 @@ function pseudoAnimate() { let i, l, t; const ezs = g.easies; - ns.initPseudo(); // page-specific initialization, calls newTargets() - ezs._zero(); // zeros-out everything under ezs - changeStop(); // resets stuff if pausing or arrived + changeStop(null); // resets stuff if pausing or arrived + ns.initPseudo(); // page-specific init, calls newTargets() + ezs._zero(); // zeros-out everything under ezs + frameIndex = 0; // frames[frameIndex] in updateFrame() + i = ns.isInitZero?.() ? 0 : MILLI; - g.frameIndex = 0; // incremented in .peri() - for (i = MILLI, l = frameCount * MILLI; i <= l; i += MILLI) { - t = i / FPS; // more efficient to increment by MILLI/FPS, but I - ezs._next(t); // prefer not to += floating point if I can avoid it. - frames[g.frameIndex].t = t; // EBase.proto.peri() doesn't have time + for (l = lastFrame * MILLI; i <= l; i += MILLI) { + t = i / FPS; // derive t and execute the next frame + ezs._next(t); + frames[frameIndex].t = t; // EBase.proto.peri() doesn't have time + } + raf.init(); // reset properties set by final frame +} //!!must it be E.original for jump:start?? +//============================================================================== +// newEasies() helps all the initEasies(), encapsulates try/catch into boolean +function newEasies(...args) { + try { + g.easies = new Easies(...args); + raf.targets = g.easies; + } catch (err) { + errorAlert(err, "new Easies() failed"); + return false; } - raf.init(); // init() is an alias for stop() //!!necessary here?? + return true; } \ No newline at end of file