rafx
v0.0.20
Published
RequestAnimationFrame (rAF) Based Promise-Like Implementation
Downloads
8
Maintainers
Readme
RafX
Rafx is a promise-like implementation solely based on window.requestAnimationFrame
(rAF)
Supported Browsers
Anything equal to or above Internet Explorer 9.
Installation
Server Side
npm install rafx
Client Side
jsDelivr
latest minified version
<script src="https://cdn.jsdelivr.net/npm/rafx"></script>
specific version
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/rafx.v0.0.20.dev.js"></script>
Distreau
<script src="https://distreau.com/rafx/js/rafx.v0.0.20.min.js"></script>
Usage
You can mix and match below patterns, in any way you want.
Thenable Pattern:
rafx.async("mixedVariable")
.then(function(mixed,args){
//do something..
//return a 'thenable' if you like
return rafx
.async()
.then(..);
},"passAdditionalArgs")
.skipFrames(30) //wait half a second if you like
.then(..) //continue
Observer Pattern:
rafx.async("mixedVariable")
.then(function(mixed,args){
//do something..
//when done: true is observed, execution continues
return {
value:"whatever",
done: false
};
},"passAdditionalArgs")
.skipFrames(30) //wait half a second if you like
.then(..) //continue
Advantages
- Unlike Promises, provides
break
which terminates chains. - Unites all
requestAnimationFrame
calls, under a single engine object. - Ability to catch errors without disturbing other items/animations.
- Aggressive break function allowing any nested thenables to terminate.
Why don't you just..
Most animations consist of:
- layout calculation
- remapping a value "t" between 0 - 1
- moving the pieces according to the layout based the value of t
- Rinse & repeat
Many other tasks that have nothing to do with animations also require steps similar to 3 and 4. Tasks that need to follow each other usually don't require micro second level execution.
All above can be combined under a single requestAnimationFrame
(rAF
)call.
RafX is an attempt to unify animations and general purpose scheduled tasks.
Testing
After installing rafx, install dev dependencies:
npm install rafx --dev
To run the tests sequentially:
npm run testDevBand && npm run testMinBand
To run them parallel:
npm run testDev && npm run testMin
Covarage
Unfortunately it was not possible to get coverage reports when using puppeteer with jest. Almost all exposed methods are tested
Examples
Follow below examples in order
- Move an HTML element to left: <fiddle>
- Move an HTML element to left, wait 250ms, then move it down: <fiddle>
- Move an HTML element around a rectangle, and do it infinitely: <fiddle>
- Move an HTML element infinitely, with an ability to pause: <fiddle>
- Move an HTML element infinitely, with an ability to break: <fiddle>
Methods
rafx.skipFrames(frameCount[, args]) <prt.skipFrames>
An additional argument can be given to be passed to the next thenable:
rafx
.skipFrames(20,{value:"whatever", done: false})
.then(function(v){ //v === "whatever"
..
})
If the passed argument does not have value
or done
properties, it is automatically wrapped with an object {value: "your argument", done: true}
and passed on to the next thenable.
If no arguments are given, undefined
is passed.
Returns
Thenable
rafx.async(mixed[, args[, thisArg]]) <prt.async>
Passes a mixed variable to be used within a thenable
immediately. If the mixed variable is a function, then that function is executed with args
and with this pointing to thisArg
:
mixed.call(thisArg || null, rest)
If the mixed is not a function, it is passed directly:
rafx.async("whatever")
.then(function(v){
//v is "whatever"
});
Returns
Thenable
thenable.then(function[, args]) <prt.Thenable.prototype.then>
Executes the passed function with the arguments given, if any and this
pointing out to the current thenable. You can return another thenable
instance, use the observer pattern, or use any other mixed variable (refer to Usage section):
rafx.async() //returns a thenable, let's call it thenable 1
.then(function(){
//here 'this' refers to thenable 1
return "mixed";
}) // returns a new thenable, let's call it thenable 2
.then(function(v){
//here 'this' refers to thenable 2
//v === "mixed"
})
You can inspect thenable.status
to receive information about its state which fluctuates between "completed", "pending" and "idle".
Returns
Thenable
thenable.forceThen(function[, args[, options]])
thenable.force(function[, args[, options]])
thenable.fThen(function[, args[, options]])
<prt.Thenable.prototype.fThen>
Executes the passed function with the arguments given, if any and this
pointing out to the current thenable (the thenable that force
is called from). You can return another thenable
instance, use the observer pattern, or use any other mixed variable (refer to Usage section).
The difference between force
and then
is that Error
objects thrown or returned from upstream or inside the force
does not cause catch
to trigger (if any catch is registered). The following then
receives what is returned from force
or the thrown Error
encapsulated in an object of form {value: ..}
rafx.async(1)
.then(function(v){
return ++v;
})
.force(function(v){
//v === 2
})
rafx.async(1)
.then(function(v){
throw new Error("this will not trigger catch, if any");
})
.force(function(v){
//bypassed and upstream Error is encapsulated {value: Error}
})
.then(function(v){
v.value.message; //"this will not trigger catch, if any"
})
The options objects accepts throw
property. This property, referring to another Error
object, can override what is thrown by default:
rafx
.async(5)
.then(function(v){
throw new Error("wrong error");
})
.filter(function(v){
return v > 10;
}, null, {throw: new Error("right error")})
.then(function(v){
//not executed
}).catch(function(e){
e.message; //"right error"
});
Returns
Thenable
thenable.filter(function[, args[, options]]) <prt.Thenable.prototype.filter>
Executes the passed function with the arguments given, if any and this
pointing out to the current thenable (the thenable that filter
is called from). You can return another thenable
instance, use the observer pattern, or use any other mixed variable (refer to Usage section). If the function returns truthy, then rest of the thenable
chain is executed, being passed the last return value immediately upstream of the filter clause, otherwise silently fails.
rafx.async()
.then(function(){
return 0;
})
.filter(function(v){
return v; //v === 0, not truthy, so next thenable is NOT executed
})
.then(function(v){
..
})
The options objects accepts 2 properties, throw
and done
. Normally the filter executes when the upstream thenable is complete (including all the nested thenables within), but the done
property can point to another thenable
's status property, allowing the filter clause to execute when that thenable is complete:
const = anotherThenable = rafx
.async()
.skipFrames(180);
rafx
.async()
.then(function(){
return "payload";
})
.filter(function(v){ //this will execute after 180 frames (3 secs)
return v === "payload"; //truthy, so remaining chain will execute
},null,{done:anotherThenable.status})
.then(function(v){
//v is "payload"
});
You can also override what to be thrown:
rafx
.async(5)
.then(function(v){return v;})
.filter(function(v){
return v > 10;
},null,{throw: new Error("right error")})
.then(function(v){
//not executed
}).catch(function(e){
e.message; //"right error" instead of default "Value Is Not Validated By Specified Function"
});
Filter does not throw by default, but if you attach a catch clause as shown above, if any options
argument is present, it will be populated with an error
property. <>
Returns
Thenable
thenable.toBool(function[, args[, options]]) <prt.Thenable.prototype.toBool>
Executes the passed function with the arguments given, if any and this
pointing out to the current thenable (the thenable that toBool
is called from). You can return another thenable
instance, use the observer pattern, or use any other mixed variable (refer to Usage section):
rafx.async(5)
.then(function(v){
return v + 1; //v === 6
})
.toBool(function(v){ //v === true
return v && "whatever";
})
..
If toBool
is not passed a function, then identity function is assumed.
Similar to filter
, force
or other clauses, it can receive an options object with a throw
property pointing to a custom Error
to be thrown
Returns
Thenable
thenable.ifTruthy(function[, args[, options]]) <prt.Thenable.prototype.ifTruthy>
thenable.ifFalsey(function[, args[, options]]) <prt.Thenable.prototype.ifFalsey>
Similar to filter
. Recevies the same arguments.
ifFalsey
:
//falsey
rafx
.async(0)
.then(function(v){ return v;})
.ifFalsey(function(v, rest){
//v === 0
return "whatever";
}, "rest")
.then(function(v){
//v === "whatever"
});
ifFalsey
:
rafx
.async("payload")
.then(function(v){ return v;})
.ifTruthy(function(v, rest){
//v === "payload"
return "whatever";
}, "rest")
.then(function(v){
//v === "whatever"
});
You can control what to throw and the timing of the clause as in filter
. These clauses do not throw by default, if you want to catch it, attach a catch
handler:
rafx
.async(0) //not truthy
.ifTruthy(function(v, rest){
return "whatever";
}, "rest")
.then(function(v){
//not executed
})
.catch(function(e){
console.log(e.message); //Value Cannot Be Converted To Truthy
});
Returns
Thenable
thenable.recurse(function[, args])
thenable.loop(function[, args])
thenable.do(function[, args])
<prt.Thenable.prototype.recurse>
Executues the following function and the arguments if any, with this
pointing out to the current thenable (the thenable that recurse
is called from)
The function is executed until the until
block returns true, or the while
block return false. The thenable following the until clause will receive the latest return value from the recurse loop:
rafx
.async(2)
.recurse((v,o) => {o.i += v; return o.i;}, {i:0}) //v === 2
.until((r,o) => r > 10)
.then(function(r){
//r === 12
});
You can also use while
instead of until
rafx
.async(2)
.recurse((v,o) => {o.i += v; return o.i;}, {i:0}) //v === 2
.while((r,o) => r < 10)
.then(function(r){
//r === 10
});
Do not use recurse
with animations, because recurse
is aware of what is returned inside the function, meaning if it is a thenable
or series of nested thenables, recurse
will wait for them to complete. Use animate
instead
Returns
Untillable
thenable.animate(function[, args])
thenable.recurseShallow(function[, args])
<prt.Thenable.prototype.animate>
Executues the following function and the arguments if any, with this
pointing out to the current thenable (the thenable that animate
is called from)
The function is executed until the until
block returns true, or the while
block return false. The thenable following the until clause will receive the latest return value from the recurse loop:
const o = {i:0};
let j = 0;
rafx
.async(2)
.animate(function(v, rest){
++rest.i;
j++;
return v;
}, o)
.until(function(v, rest){
return rest.i + v === 12;
})
.then(function(v){
//v === 2
//o.i === 10
//j === 10
});
Do not return a thenable
inside an animate
clause, only use ordinary objects or variables because animate
will not wait for the thenable's completion. Use recurse
instead.
Returns
Untillable
rafx.repeat(function[, args[, options]]) <prt.repeat>
Creates a repeating function that can be controlled via a controller
. The options argument can contain 3 fields, throw
, handler
and thisArg
. All are optional.
rafx
.repeat(
function(arg, ctrl){
// 'this' refers to "whatever"
arg.c++;
if(arg.c > 20){
// your options below
throw new Error("wrong error message"); // option 1
ctrl.kill //option 2
ctrl.resume //option 3
ctrl.pause // option 4
}
},
{
c:0
},
{
/*if you throw, e will be the error, if handler is specified and ctrl.kill is invoked, e will be null*/
handler:function(e,arg){
/*
e is either null or error object
arg is '{c: 0}'
*/
},
//you can specify what to throw
throw: new Error("right error message"),
//optional this argument
thisArg: "whatever"
}
);
rafx.repeat
returns an id string. This id internally stored inside rafx._processes:
delete rafx._processes[id];
Above has the same effect as calling ctrl.kill
, if no handler is specified, the repeat will cease, otherwise the handler will be called.
Returns
An id string
rafx.throttle(function[, args[, numberFrames]]) <prt.throttle>
Creates a throttled function that fires maximum once every numberFrames
:
const f = rafx.throttle(function(a, b, c){
console.log(a, b, c);
}, 2, 5);
f(1, 10); //1, 2, Arguments[1, 10, callee: f, ..]
Above function can fire at most once every 5 frames, which is about 83ms.
Returns
function
rafx.isInView(node[, numberFrames]) <prt.isInView>
Returns whether a node with nodeType
=== 1 (HTMLElement) is visible once every numberFrames
:
rafx.isInView(node, 5)
Above will return boolean
at most once every 5 frames, which is about 83ms.
Returns
boolean
rafx.ifInView(node[, numberFrames]) <prt.ifInView>
Built on top of isInView
, returns a thenable
that will execute based on whether a node with nodeType
=== 1 (HTMLElement) is visible. The result will be refreshed after numberFrames
:
rafx.ifInView(div,20)
.then(function(){
/*
do something if the element is in view,
otherwise silently fail,
if you attach a catch clause, you will receive 'Value Cannot Be Converted To Truthy' Error
*/
}
Above thenable
will execute if the element is in view. Calling rafx.ifInView(div,20)
again will return a new thenable
, but whether the element is out of view or not will be refreshed after 20 frames (~0.33 secs)
Returns
Thenable
rafx.ifNotInView(node[, numberFrames]) <prt.ifNotInView>
Built on top of isInView
, returns a thenable
that will execute based on whether a node with nodeType
=== 1 (HTMLElement) is NOT visible. The result will be refreshed after numberFrames
:
rafx.ifNotInView(div,20)
.then(function(){
/*
do something if the element is NOT in view,
otherwise silently fail,
if you attach a catch clause, you will receive 'Value Cannot Be Converted To Falsey' Error
*/
}
Above thenable
will execute if the element is NOT in view. Calling rafx.ifNotInView(div,20)
again will return a new thenable
, but whether the element is out of view or not will be refreshed after 20 frames (~0.33 secs)
Returns
Thenable
thenable.break() <prt.Thenable.prototype.break>
Breaks the current thenable and all upstream/parent thenables. Use this if you want to cancel execution.
Breaking behavior in RafX is aggressive, nested thenables will inherit the same breaker
from parent thenables, if you do not want the parent thenable
to be affected, use setTimeout
:
const outer = rafx
.skipFrames(600, "payload") //wait 10 seconds and pass "payload"
.then(function(payload){
setTimeut(function(){
rafx.async()
//breaking below won't terminate outer
.then(function(){this.break();})
.then(..); //not executed due to break
}, 0);
})
.then(function(){
//do something
});
Returns
Thenable
thenable.catch(function) <prt.Thenable.prototype.catch>
Many clauses in RafX do not throw by default and silently fail when their conditions are not met.
Similarly, errors might be thrown within ordinary thenables. For all these cases you can attach a catch
to the thenable. It does not matter where you attach the catch
, the last one overwrites the previous:
rafx.async()
.then(function(){
this._identifier = "thenable 1";
var err = new Error("Custom Error");
err._this = this;
throw err;
})
.catch(function(e){
console.log("catch-1", e._this._identifier);
})
.then(function(){
this._identifier = "thenable 2";
var err = new Error("Custom Error");
err._this = this;
throw err;
})
.catch(function(e){
console.log("catch-2", e._this._identifier);
})
.then(function(){
this._identifier = "thenable 3";
var err = new Error("Custom Error");
err._this = this;
throw err;
});
/*
OUTPUT
catch-2 thenable 1
*/
rafx.duration(string) <Duration>
RafX by default operates on frames, if you want to work with seconds or milliseconds, you can use this constructor:
let dur = rafx.duration();
let dur = rafx.duration("4 mins");
let dur = rafx.duration("5 seconds");
The input string, if any is matched against <Duration.prototype.rgx>
This returns a duration object where you can set the value and the unit. Values are integers, units can be "minutes", "seconds" etc. To set unit, you can use u
, unit
or setUnit
methods, whereas for value you can choose from v
, value
, setVal
or val
:
dur
.set
.unit("f")
.u("frames")
.setUnit("frame")
.unit("ms")
.u("milliseconds")
.setUnit("millisecond")
.u("s")
.setUnit("second")
.unit("seconds")
.u("min")
.setUnit("mins")
.unit("minutes")
.u("minute")
.v(2)
.value(2)
.setVal(2)
.val(2);
Setting the unit will automatically convert the value:
dur.set.unit("min").val(1);
dur.set.u("f");
+dur; //3600
Once you set value and unit, you can provide it to skipFrames
:
rafx.skipFrames(dur).then(..)
rafx.skipFrames(rafx.duration("4 mins")).then(..)
Returns
Duration
Performance
RafX has comparable performance to D3 v5, for more details you can inspect the screen shots and the html files in the perf
folder under src
.
Publishing
- leave a single file inside the untracked
./dev
folder, matching the pattern below:
/^rafx\.v(?:[0-9]+\.){3}dev\.js/i`
- run:
npm run publishPatch
- After running the command:
- The older
dev/min
builds will be copied to./olderVersions
(not tracked) - The file inside
./dev
will be copied to./dist
, renamed and updated within the file to match the npm version - The above file will be minified and placed similarly inside
./dist
with.min.js
extension - Readme source code line pointers will be updated
- For the remaining tasks performed check
package.json
'spublishPatch
script.
- The older