How to use element ref inside useMemo? - javascript

I have an HTML element, nav which has a ref set in it, sideNavRef.
I also want to get a variable to control the animations of that element and for that, I'm doing this:
const sideNavAnimation = useMemo(() => {
if (sideNavRef.current) {
const animations = createSideNavKeyframes({
startingWidth: (7 * document.body.offsetWidth) / 100,
endingWidth: 55,
});
const animation = sideNavRef.current.animate(animations.animation);
animation.pause();
return animation;
}
}, []);
The createSideNavKeyframes returns a keyframes array with calculated easing values for each step (0.1 to 1).
The easing function is this:
function easeInOutQuart(x: number): number {
return x < 0.5 ? 8 * x * x * x * x : 1 - Math.pow(-2 * x + 2, 4) / 2;
}
Since it would need to calculate this on every render I chose to use a memoized value since it wouldn't change.
But since the dependecy array is empty, it means that in the first render the value for the ref is still undefined and so the animations will be undefined as well.
Is there a better way to make this work?

Your function is registered with sideNavRef.current still not defined because useMemo runs before the component renders. You could add sideNavRef.current in the dependency array, to ask useMemo to create a new version of your function, but for that, your component should re-render somehow, cause sideNavRef.current changing won't trigger a re-render and useMemo check its dependency only on renders.
You could use a state called componentRendred for example, add it in the dependency array, and change its value in a useEffect, like so:
const [componentRendred, setComponentRendred] = useState(false);
const sideNavAnimation = useMemo(() => {
if (sideNavRef.current) {
const animations = createSideNavKeyframes({
startingWidth: (7 * document.body.offsetWidth) / 100,
endingWidth: 55,
});
const animation = sideNavRef.current.animate(animations.animation);
animation.pause();
return animation;
}
}, [componentRendred]);
useEffect(()=>{
setComponentRendred(true);
},[])

Related

How awful is rendering a component in 3 different phases?

How awful is that :
useLayoutEffect(() => {
setWidth(ganttContainerRef.current.offsetWidth);
setHeight(ganttContainerRef.current.offsetHeight);
}, [])
useLayoutEffect(() => {
if (width > 0) {
setGanttReady(true);
}
}, [width])
useLayoutEffect(() => {
if (ganttReady) {
ganttRef.current.scrollTo({ left: 80 * 80 / 7 + 40 });
}
}, [ganttReady]);
i.e. rendering a component in 3 separate steps...
One to render the container (this container is built with %age, and
I need the real width for step 2 and step 3)
One to render the text
displayed in this container (need the text height for step 3)
One to
render my gantt (all with a lot of absolute positions)
If I render 1 and 2 at the same time, the container width will be 0 (first render) because 2 is not displayed yet.
No matter what I do, if I do const top = refs.current[initiative.id].offsetTop;, offsetTop will have a value as if width was still 0. offsetTop is not updated with a rerender.
With the posted code above it works fine, but how awful is it?
Is it doable to render a component in multiple phases or am I really hacking normal React behaviour?
With the posted code above it works fine, but how awful is it? Is it
doable to render a component in multiple phases or am I really hacking
normal React behaviour?
I wouldn't say it is awful, you could say it is not very efficient, but I would not worry about it if it isn't noticeable.
You can refactor it to something like this:
useLayoutEffect(() => {
let offsetWidth = ganttContainerRef.current.offsetWidth;
setWidth(offsetWidth);
setHeight(ganttContainerRef.current.offsetHeight);
if (offsetWidth > 0) {
setGanttReady(true);
ganttRef.current.scrollTo({ left: (80 * 80) / 7 + 40 });
}
}, []);
But the problem with this approach is as you can see it runs only on mount; which is Ok if that's what you want. So with your previous approach here:
useLayoutEffect(() => {
if (width > 0) {
setGanttReady(true);
}
}, [width])
you had the benefit that if width changed, then you could act on it, you don't get it with my version.
So it depends.

RxJS wait for 2 separate clicks events

I have a button and an image.
When I click the button, I want it to go to a "wait mode" of sorts.
Waiting for two separate clicks, that both return the x, y value of the mouse click events.
I got the mouse xy part no problem but at loss for what RxJS operator to use next
const elem = document.getElementById("MyImage");
const root = fromEvent(elem, "click");
const xy = root.pipe(map(evt => xyCartoPos(elem, evt)));
xy.subscribe(coords => console.log("xy:", coords));
function xyCartoPos(elem, e) {
const elemRect = elem.getBoundingClientRect();
return {
x: e.clientX - elemRect.x - elemRect.width / 2,
y: flipNum(e.clientY - elemRect.y - elemRect.height / 2)
};
}
You can use bufferCount to emit a fixed number of clicks at once (in one array).
const xy = root.pipe(
map(evt => xyCartoPos(elem, evt)),
bufferCount(2),
//take(1) // use take(1) if you only want to emit one pair of clicks and then complete
);
You could use scan to collect the events as an array, then use filter to verify the length of the array is 2:
const xy = root.pipe(
map(evt => xyCartoPos(elem, evt)),
scan((acc, evt) => {
acc.push(evt);
return acc;
}, []),
filter(events => events.length == 2),
);
This will cause only an array with the two mouse events, after two clicks, to be published to the subscriber.

Three.js animate from one position to another

Let's say I have a shape at position.x = 0 and I want to smoothly animate it in the render loop to position.x = 2.435. How would I go about that?
You can use the THREE AnimationMixer. The function below sets up the animation. Example jsFiddle.
const createMoveAnimation = ({ mesh, startPosition, endPosition }) => {
mesh.userData.mixer = new AnimationMixer(mesh);
let track = new VectorKeyframeTrack(
'.position',
[0, 1],
[
startPosition.x,
startPosition.y,
startPosition.z,
endPosition.x,
endPosition.y,
endPosition.z,
]
);
const animationClip = new AnimationClip(null, 5, [track]);
const animationAction = mesh.userData.mixer.clipAction(animationClip);
animationAction.setLoop(LoopOnce);
animationAction.play();
mesh.userData.clock = new Clock();
this.animationsObjects.push(mesh);
};
Set your target position as a variable (outside the render loop):
var targetPositionX = 2.435;
Then in your render loop, create an if statement that checks if the object's X position is less than the targetPositionX. If it is , it will add an increment (which you can change based on how fast you want it to move) to the object's X position. When the object's X position becomes greater or equal to the targetPositionX, it will stop moving.
Something like this:
if (object.position.x <= targetPositionX) {
object.position.x += 0.001; // You decide on the increment, higher value will mean the objects moves faster
}
Here is the full code for the render loop:
function loop(){
// render the scene
renderer.render(scene, camera);
// Check the object's X position
if (object.position.x <= targetPositionX) {
object.position.x += 0.001; // You decide on the increment, higher value will mean the objects moves faster
}
// call the loop function again
requestAnimationFrame(loop);
}
Side note
For more detailed/complex animations you may want to look into Tween.js for Three.js which makes animation easier and also allows you to add easing functions to your animation.
You can find it here:
https://github.com/sole/tween.js
I would recommend reading into it if you are getting into Three.js.

famo.us trigger event at key point of animation

I'm trying to trigger an event half-way through the progress (not time) of a transition. It sounds simple, but since the transition can have any curve it's quite tricky. In my particular case it's not going to be paused or anything so that consideration is out of the way.
(Simplified) essentially I could trigger an animation on a modifier like this:
function scaleModifierTo(stateModifier, scale, animationDuration) {
stateModifier.setTransform(
Transform.scale(scale, scale, scale),
{
duration: animationDuration,
curve: this.options.curve
}
);
}
When the interpolated state of the Transitionable hits 0.5 (half-way through) I want to trigger a function.
I haven't dug that deep behind in the source of famo.us yet, but maybe need to do something like
subclass something and add the possibility to listen when the state passes through a certain point?
reverse the curve defined and use a setTimeout (or try to find a proximity using a few iterations of the chosen curve algorithm (ew))
Is it possible to do this easily? What route should I go down?
I can think of a couple of ways to achieve such, and both lend to the use of Modifier over StateModifier. If you are new, and haven't really had the chance to explore the differences, Modifier consumes state from the transformFrom method which takes a function that returns a transform. This is where we can use our own Transitionable to supply state over the lifetime of our modifier.
To achieve what you wish, I used a Modifier with a basic transformFrom that will alter the X position of the surface based on the value of the Transitionable. I can then monitor the transitionable to determine when it is closest, or in my case greater than or equal to half of the final value. The prerender function will be called and checked on every tick of the engine, and is unbinded when we hit the target.
Here is that example..
var Engine = require('famous/core/Engine');
var Surface = require('famous/core/Surface');
var Modifier = require('famous/core/Modifier');
var Transform = require('famous/core/Transform');
var Transitionable = require('famous/transitions/Transitionable');
var SnapTransition = require('famous/transitions/SnapTransition');
Transitionable.registerMethod('snap',SnapTransition);
var snap = { method:'snap', period:1000, damping:0.6};
var context = Engine.createContext();
var surface = new Surface({
size:[200,200],
properties:{
backgroundColor:'green'
}
});
surface.trans = new Transitionable(0);
surface.mod = new Modifier();
surface.mod.transformFrom(function(){
return Transform.translate(surface.trans.get(),0,0);
});
context.add(surface.mod).add(surface);
function triggerTransform(newValue, transition) {
var prerender = function(){
var pos = surface.trans.get();
if (pos >= (newValue / 2.0)) {
// Do Something.. Emit event etc..
console.log("Hello at position: "+pos);
Engine.removeListener('prerender',prerender);
}
}
Engine.on('prerender',prerender);
surface.trans.halt();
surface.trans.set(newValue,transition);
}
surface.on('click',function(){ triggerTransform(400, snap); });
The downside of this example is the fact that you are querying the transitionable twice. An alternative is to add your transitionable check right in the transformFrom method. This could get a bit strange, but essentially we are modifying our transformFrom method until we hit our target value, then we revert back to the original transformFrom method.. triggerTransform would be defined as follows..
Hope this helps!
function triggerTransform(newValue, transition) {
surface.mod.transformFrom(function(){
pos = surface.trans.get()
if (pos >= newValue/2.0) {
// Do something
console.log("Hello from position: " + pos)
surface.mod.transformFrom(function(){
return Transform.translate(surface.trans.get(),0,0);
});
}
return Transform.translate(pos,0,0)
})
surface.trans.set(newValue,transition);
}
Thank you for your responses, especially #johntraver for the prerender event, I wasn't aware of the existence of that event.
I realised it made more sense that I should handle this logic together with my move animation, not the scale one. Then, I ended up using a (very hacky) way of accessing the current state of the transition and by defining a threshold in px I can trigger my function when needed.
/**
* Move view at index to a specified offset
* #param {Number} index
* #param {Number} xOffset xOffset to move to
* #param {Object} animation Animation properties
* #return void
*/
function moveView(index, xOffset, animation) {
var rectModifier = this._views[index].modifiers.rect;
var baseXOffset = rectModifier._transformState.translate.state[0];
// After how long movement is reflow needed?
// for the sake of this example I use half the distance of the animation
var moveThreshold = Math.abs(baseXOffset - xOffset)/2;
/**
* Callback function triggered on each animation frame to see if the view is now covering
* the opposite so we can trigger a reflow of the z index
* #return void
*/
var prerender = function() {
var numPixelsMoved = Math.abs(baseXOffset - rectModifier._transformState.translate.state[0]);
if (numPixelsMoved > moveThreshold) {
Engine.removeListener('prerender', prerender);
// trigger a method when this is reached
_reflowZIndex.call(this);
}
}.bind(this);
rectModifier.setTransform(
Transform.translate(xOffset, 0, 0),
animation,
function() {
Engine.removeListener('prerender', prerender);
}
);
Engine.on('prerender', prerender);
}
Obviously the ._transformState.translate.state[0] is a complete hack, but I couldn't figure out of getting this value in a clean way without adding my own Transitionable, which I don't want. If there is a cleaner way of finding the current state as a Number between 0.0-1.0 that would be ace; anyone knows of one?

convert css units

i'm trying to get back a style property in all valid 'length' and 'percent' units, converted from the original value set for that property.
e.g., if i have a div with style.width set to 20%, i'd want back an object with that value in percent (of course, 20%), pixels (whatever the actual pixel width is), em, pt, ex, etc.
i realize that 'percentage' is not a 'length' value, and that not all properties that accept length values accept percentage, but want to include that as well.
of course, some values will be dependent on the element specifically, and possibly it's position in the DOM (e.g., getting the em value will require that element's parent computed font size as well).
i can assume that the style is set explicitly for the element - i'm aware of how to retrieve the current computed style of an element - i'm just hoping to not repeat work someone else has probably already done. i'm also aware of http://www.galasoft.ch/myjavascript/WebControls/css-length.html, but it relies on style.pixelWidth or node.clientWidth, and fails in Chrome (I'd assume it fails in Safari as well... and probably others).
i've already got color values worked out (rgb, rgba, hex, name) - this is of course a lot more straightforward. i'm working with values that are mathematically mutable, so really only need 'length' and 'percent' values (if called on a property set with a non-length, non-percent value - like 'font-size: larger' - the function could fail, or throw an error).
if written procedurally, something like this would be ideal:
function getUnits(target, prop){
var value = // get target's computed style property value
// figure out what unit is being used natively, and it's values - for this e.g., 100px
var units = {};
units.pixel = 100;
units.percent = 50; // e.g., if the prop was height and the parent was 200px tall
units.inch = 1.39; // presumably units.pixel / 72 would work, but i'm not positive
units.point = units.inch / 72;
units.pica = units.point * 12;
// etc...
return units;
}
I'm not asking for someone to write code for me, but my hope is that someone has already done this before and it's available in some open-source library, framework, blog post, tut, whatever. failing that, if someone has a clever idea how to streamline the process, that'd be great as well (the author of the link above created a temporary div and computed a single value to determine the ratios for other units - a handy idea but not one i'm entirely sold on, and definitely one that'd need supplemental logic to handle everything i'm hoping accept).
thanks in advance for any insight or suggestions.
EDIT: updated to allow user to pick a single unit to be returned (e.g., exists as %, get back in px) - big improvement in performance for when that's enough - might end up changing it to just accept a single unit to convert, and get rid the loops. Thanks to eyelidlessness for his help. /EDIT
this is what i've come up with - after preliminary testing it appears to work. i borrowed the temporary div idea from the link mentioned in the original question, but that's about all that was taken from that other class.
if anyone has any input or improvements, i'd be happy to hear it.
(function(){
// pass to string.replace for camel to hyphen
var hyphenate = function(a, b, c){
return b + "-" + c.toLowerCase();
}
// get computed style property
var getStyle = function(target, prop){
if(prop in target.style){ // if it's explicitly assigned, just grab that
if(!!(target.style[prop]) || target.style[prop] === 0){
return target.style[prop];
}
}
if(window.getComputedStyle){ // gecko and webkit
prop = prop.replace(/([a-z])([A-Z])/, hyphenate); // requires hyphenated, not camel
return window.getComputedStyle(target, null).getPropertyValue(prop);
}
if(target.currentStyle){ // ie
return target.currentStyle[prop];
}
return null;
}
// get object with units
var getUnits = function(target, prop, returnUnit){
var baseline = 100; // any number serves
var item; // generic iterator
var map = { // list of all units and their identifying string
pixel : "px",
percent : "%",
inch : "in",
cm : "cm",
mm : "mm",
point : "pt",
pica : "pc",
em : "em",
ex : "ex"
};
var factors = {}; // holds ratios
var units = {}; // holds calculated values
var value = getStyle(target, prop); // get the computed style value
var numeric = value.match(/\d+/); // get the numeric component
if(numeric === null) { // if match returns null, throw error... use === so 0 values are accepted
throw "Invalid property value returned";
}
numeric = numeric[0]; // get the string
var unit = value.match(/\D+$/); // get the existing unit
unit = (unit == null) ? "px" : unit[0]; // if its not set, assume px - otherwise grab string
var activeMap; // a reference to the map key for the existing unit
for(item in map){
if(map[item] == unit){
activeMap = item;
break;
}
}
if(!activeMap) { // if existing unit isn't in the map, throw an error
throw "Unit not found in map";
}
var singleUnit = false; // return object (all units) or string (one unit)?
if(returnUnit && (typeof returnUnit == "string")) { // if user wants only one unit returned, delete other maps
for(item in map){
if(map[item] == returnUnit){
singleUnit = item;
continue;
}
delete map[item];
}
}
var temp = document.createElement("div"); // create temporary element
temp.style.overflow = "hidden"; // in case baseline is set too low
temp.style.visibility = "hidden"; // no need to show it
target.parentNode.appendChild(temp); // insert it into the parent for em and ex
for(item in map){ // set the style for each unit, then calculate it's relative value against the baseline
temp.style.width = baseline + map[item];
factors[item] = baseline / temp.offsetWidth;
}
for(item in map){ // use the ratios figured in the above loop to determine converted values
units[item] = (numeric * (factors[item] * factors[activeMap])) + map[item];
}
target.parentNode.removeChild(temp); // clean up
if(singleUnit !== false){ // if they just want one unit back
return units[singleUnit];
}
return units; // returns the object with converted unit values...
}
// expose
window.getUnits = this.getUnits = getUnits;
})();
tyia
Check out Units, a JavaScript library that does these conversions.
Here's a blog post by the author describing the code.
Late to the party and I don't think this necessarily answers the question fully because I haven't included conversion of percentages. However, I do think it's a good start that can be easily modified for your specific usage.
Javascript function
/**
* Convert absolute CSS numerical values to pixels.
*
* #link https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Values_and_units#numbers_lengths_and_percentages
*
* #param {string} cssValue
* #param {null|HTMLElement} target Used for relative units.
* #return {*}
*/
window.convertCssUnit = function( cssValue, target ) {
target = target || document.body;
const supportedUnits = {
// Absolute sizes
'px': value => value,
'cm': value => value * 38,
'mm': value => value * 3.8,
'q': value => value * 0.95,
'in': value => value * 96,
'pc': value => value * 16,
'pt': value => value * 1.333333,
// Relative sizes
'rem': value => value * parseFloat( getComputedStyle( document.documentElement ).fontSize ),
'em': value => value * parseFloat( getComputedStyle( target ).fontSize ),
'vw': value => value / 100 * window.innerWidth,
'vh': value => value / 100 * window.innerHeight,
// Times
'ms': value => value,
's': value => value * 1000,
// Angles
'deg': value => value,
'rad': value => value * ( 180 / Math.PI ),
'grad': value => value * ( 180 / 200 ),
'turn': value => value * 360
};
// Match positive and negative numbers including decimals with following unit
const pattern = new RegExp( `^([\-\+]?(?:\\d+(?:\\.\\d+)?))(${ Object.keys( supportedUnits ).join( '|' ) })$`, 'i' );
// If is a match, return example: [ "-2.75rem", "-2.75", "rem" ]
const matches = String.prototype.toString.apply( cssValue ).trim().match( pattern );
if ( matches ) {
const value = Number( matches[ 1 ] );
const unit = matches[ 2 ].toLocaleLowerCase();
// Sanity check, make sure unit conversion function exists
if ( unit in supportedUnits ) {
return supportedUnits[ unit ]( value );
}
}
return cssValue;
};
Example usage
// Convert rem value to pixels
const remExample = convertCssUnit( '2.5rem' );
// Convert time unit (seconds) to milliseconds
const speedExample = convertCssUnit( '2s' );
// Convert angle unit (grad) to degrees
const emExample = convertCssUnit( '200grad' );
// Convert vw value to pixels
const vwExample = convertCssUnit( '80vw' );
// Convert the css variable to pixels
const varExample = convertCssUnit( getComputedStyle( document.body ).getPropertyValue( '--container-width' ) );
// Convert `em` value relative to page element
const emExample = convertCssUnit( '2em', document.getElementById( '#my-element' ) );
Current supported formats
Any format with preceding plus (+) or minus (-) symbol is valid, along with any of the following units: px, cm, mm, q, in, pc, pt, rem, em, vw, vh, s, ms, deg, rad, grad, turn
For example:
10rem
10.2em
-0.34cm
+10.567s
You can see a full combination of formats here: https://jsfiddle.net/thelevicole/k7yt4naw/1/
Émile kind of does this, specifically in its parse function:
function parse(prop){
var p = parseFloat(prop), q = prop.replace(/^[\-\d\.]+/,'');
return isNaN(p) ? { v: q, f: color, u: ''} : { v: p, f: interpolate, u: q };
}
The prop argument is the computedStyle for some element. The object that's returned has a v property (the value), an f method that is only used later on for animation, and a u property (the unit of the value, if necessary).
This doesn't entirely answer the question, but it could be a start.
While digging through the SVG spec, I found that SVGLength provides an interesting DOM API for builtin unit conversion. Here's a function making use of it:
/** Convert a value to a different unit
* #param {number} val - value to convert
* #param {string} from - unit `val`; can be one of: %, em, ex, px, cm, mm, in, pt, pc
* #param {string} to - unit to convert to, same as `from`
* #returns {object} - {number, string} with the number/string forms for the converted value
*/
const convert_units = (() => {
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
const len = rect.width.baseVal;
const modes = {
"%": len.SVG_LENGTHTYPE_PERCENTAGE,
"em": len.SVG_LENGTHTYPE_EMS,
"ex": len.SVG_LENGTHTYPE_EXS,
"px": len.SVG_LENGTHTYPE_PX,
"cm": len.SVG_LENGTHTYPE_CM,
"mm": len.SVG_LENGTHTYPE_MM,
"in": len.SVG_LENGTHTYPE_IN,
"pt": len.SVG_LENGTHTYPE_PT,
"pc": len.SVG_LENGTHTYPE_PC,
};
return (val, from, to, context) => {
if (context)
context.appendChild(rect);
len.newValueSpecifiedUnits(modes[from], val);
len.convertToSpecifiedUnits(modes[to]);
const out = {
number: len.valueInSpecifiedUnits,
string: len.valueAsString
};
if (context)
context.removeChild(rect);
return out;
};
})();
Usage example:
convert_units(1, "in", "mm");
// output: {"number": 25.399999618530273, "string": "25.4mm"}
Some units are relative, so need to be placed in a parent DOM element temporarily to be able to resolve the unit's absolute value. In those cases provide a fourth argument with the parent element:
convert_units(1, "em", "px", document.body);
// output: {"number": 16, "string": "16px"}

Categories

Resources