How to set on a new random to x, y when gsap is repeated - gsap

Hello i use vue3 and gsap
I want to give new x and y whenever gsap repeats.
Repeatdelay works well randomly, but without refresh, x and y are always given the same value.
This is my code.
gsap.set(array.value[i], {
xPercent: gsap.utils.random(0, 900),
yPercent: gsap.utils.random(0, 300)
})
gsap.to(array.value[i], {
opacity: 0,
scale: 2,
duration: 0.3,
yoyo: true,
onComplete () {
gsap.delayedCall(gsap.utils.random(2, 5, 0.1), () => this.restart())
}
})
I want to set new random values of x and y whenever gsap is repeated.
Is there a good way for me?
Please let me know if you don't have enough explanation.
Thank for your help.

You can set a new random value for x and y before the animation gets restarted.
const setRandomPosition = item => {
gsap.set(item, {
xPercent: gsap.utils.random(0, 900),
yPercent: gsap.utils.random(0, 300)
})
}
setRandomPosition(array.value[i]);
gsap.to(array.value[i], {
opacity: 0,
scale: 2,
duration: 0.3,
yoyo: true,
onComplete(){
gsap.delayedCall(gsap.utils.random(2, 5, 0.1), () => {
setRandomPosition(array.value[i]);
this.restart()
})
}
})

Related

Repeating a repeating animation with delay with reanimated

Using react-native-reanimated, I'm trying to repeat infinitely an animation which is also a repeated animation, with a delay.
With my code, the delay and the nested repeat animation are triggered but not the inifinite one.
Any ideas ?
useEffect(() => {
progress.value = withRepeat(
withDelay(2000, withRepeat(withTiming(0, { duration: 500 }), 6, true)),
-1,
true
);
}, []);
Like #Abe pointed out in his answer, reverse property of withRepeat is not supported when wrapped with other animation modifiers.
But, you can do this without setInterval - making use of withSequence to simulate the `reverse animation
// starting delay of 2000ms
// play animation 6 times
// repeat
progress.value =
withRepeat(
withDelay(2000, withRepeat(
withSequence(
// split duration of 500ms to 250ms
withTiming(goToValue, {duration: 250, easing: Easing.inOut(Easing.ease)}),
withTiming(initialValue, {duration: 250, easing: Easing.inOut(Easing.ease)}),
)
, 6))
, -1)
You've set the outer withRepeat to 1, so it should only repeat once, is that intended? Use a negative number to repeat indefinitely.
The docs for withRepeat say that withRepeat with the third argument (reverse) set to true doesn't work properly for the withDelay and withSequence animations, that could also be causing an issue. You might want to try reversing the animations manually in a withSequence call and repeating that.
No solution found with Reanimated, but as suggested by #Abe,a simple setInterval does the trick
useEffect(() => {
progress.value = withRepeat(withTiming(0, { duration: 400 }), 6, true);
const interval = setInterval(() => {
progress.value = withRepeat(withTiming(0, { duration: 400 }), 6, true);
}, 6000);
return () => clearInterval(interval);
}, []);
You can achieve that without setInterval, put withDelay on each animation.
withRepeat(
withSequence(
withDelay(
2000,
withTiming(0, {
duration: 300,
easing: Easing.inOut(Easing.ease),
}),
),
withDelay(
2000,
withTiming(1, {
duration: 300,
easing: Easing.inOut(Easing.ease),
}),
),
),
-1,
);
You can achieve this by using Animated.sequence
This code basically works by re-run the function when the animation done
function fadeAnimation() {
Animated.sequence([
Animated.timing(progress.value, {
toValue: 0,
duration: 500,
delay: 1000,
useNativeDriver: true,
}),
Animated.timing(progress.value, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}),
]).start(() => {
fadeAnimation();
});
}
useEffect(() => {
fadeAnimation()
}, []);

How to zoom to bounds of arcs with deck.gl?

I've created some arcs using deck.gl. When you click on different points/polygons, different arcs appear between countries. When doing this, I want the map to zoom to the bounds of those arcs.
For clarity, here is an example: When clicking on Glasgow, I'd want to zoom to the arc shown (as tightly as possible):
It appears that with WebMercatorViewport, you can call fitBounds
(see: https://deck.gl/docs/api-reference/core/web-mercator-viewport#webmercatorviewport)
It's not clear to me how this gets used, though. I've tried to find examples, but have come up short. How can I add this to what I have?
Here is the code for the arcs:
fetch('countries.json')
.then(res => res.json())
.then(data => {
console.log('data',data)
const inFlowColors = [
[0, 55, 255]
];
const outFlowColors = [
[255, 200, 0]
];
const countyLayer = new deck.GeoJsonLayer({
id: 'geojson',
data: data,
stroked: true,
filled: true,
autoHighlight: true,
lineWidthScale: 20,
lineWidthMinPixels: 1,
pointRadiusMinPixels: 10,
opacity:.5,
getFillColor: () => [0, 0, 0],
getLineColor: () => [0,0,0],
getLineWidth: 1,
onClick: info => updateLayers(info.object),
pickable: true
});
const deckgl = new deck.DeckGL({
mapboxApiAccessToken: 'pk.eyJ1IjoidWJlcmRhdGEiLCJhIjoiY2pudzRtaWloMDAzcTN2bzN1aXdxZHB5bSJ9.2bkj3IiRC8wj3jLThvDGdA',
mapStyle: 'mapbox://styles/mapbox/light-v9',
initialViewState: {
longitude: -19.903283,
latitude: 36.371449,
zoom: 1.5,
maxZoom: 15,
pitch: 0,
bearing: 0
},
controller: true,
layers: []
});
updateLayers(
data.features.find(f => f.properties.name == 'United States' )
);
function updateLayers(selectedFeature) {
const {exports, centroid, top_exports, export_value} = selectedFeature.properties;
const arcs = Object.keys(exports).map(toId => {
const f = data.features[toId];
return {
source: centroid,
target: f.properties.centroid,
value: exports[toId],
top_exports: top_exports[toId],
export_value: export_value[toId]
};
});
arcs.forEach(a => {
a.vol = a.value;
});
const arcLayer = new deck.ArcLayer({
id: 'arc',
data: arcs,
getSourcePosition: d => d.source,
getTargetPosition: d => d.target,
getSourceColor: d => [0, 55, 255],
getTargetColor: d => [255, 200, 0],
getHeight: 0,
getWidth: d => d.vol
});
deckgl.setProps({
layers: [countyLayer, arcLayer]
});
}
});
Here it is as a Plunker:
https://plnkr.co/edit/4L7HUYuQFM19m9rI
I try to make it simple, starting from a raw implementation with ReactJs then try to translate into vanilla.
In ReactJS I will do something like that.
Import LinearInterpolator and WebMercatorViewport from react-map-gl:
import {LinearInterpolator, WebMercatorViewport} from 'react-map-gl';
Then I define an useEffect for viewport:
const [viewport, setViewport] = useState({
latitude: 37.7577,
longitude: -122.4376,
zoom: 11,
bearing: 0,
pitch: 0
});
Then I will define a layer to show:
const layerGeoJson = new GeoJsonLayer({
id: 'geojson',
data: someData,
...
pickable: true,
onClick: onClickGeoJson,
});
Now we need to define onClickGeoJson:
const onClickGeoJson = useCallback((event) => {
const feature = event.features[0];
const [minLng, minLat, maxLng, maxLat] = bbox(feature); // Turf.js
const viewportWebMercator = new WebMercatorViewport(viewport);
const {longitude, latitude, zoom} = viewport.fitBounds([[minLng, minLat], [maxLng, maxLat]], {
padding: 20
});
viewportWebMercator = {
...viewport,
longitude,
latitude,
zoom,
transitionInterpolator: new LinearInterpolator({
around: [event.offsetCenter.x, event.offsetCenter.y]
}),
transitionDuration: 1500,
};
setViewport(viewportWebMercator);
}, []);
First issue: in this way we are fitting on point or polygon clicked, but what you want is fitting arcs. I think the only way to overcome this kind of issue is to add a reference inside your polygon about bounds of arcs. You can precompute bounds for each feature and storage them inside your geojson (the elements clicked), or you can storage just a reference in feature.properties to point another object where you have your bounds (you can also compute them on the fly).
const dataWithComputeBounds = {
'firstPoint': bounds_arc_computed,
'secondPoint': bounds_arc_computed,
....
}
bounds_arc_computed need to be an object
bounds_arc_computed = {
minLng, minLat, maxLng, maxLat,
}
then on onClick function just take the reference
const { minLng, minLat, maxLng, maxLat} = dataWithComputedBounds[event.features[0].properties.reference];
const viewportWebMercator = new WebMercatorViewport(viewport);
...
Now just define our main element:
return (
<DeckGL
layers={[layerGeoJson]}
initialViewState={INITIAL_VIEW_STATE}
controller
>
<StaticMap
reuseMaps
mapStyle={mapStyle}
preventStyleDiffing
mapboxApiAccessToken={YOUR_TOKEN}
/>
</DeckGL>
);
At this point we are pretty close to what you already linked (https://codepen.io/vis-gl/pen/pKvrGP), but you need to use deckgl.setProps() onClick function instead of setViewport to change your viewport.
Does it make sense to you?

Animate rotation to the nearest value in React Native

Given, interpolation, that has to rotate a car marker:
this.rotation.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg'],
extrapolate: 'clamp'
})
GIF below represents the following bearings:
1. 337.38055647641903
2. 335.202973299875
3. 13.03761189748721
4. 13.042704554861551
5. 358.77805501498045
Green line is how I want it to work (rotate to the nearest value). Red line is how it works currently (on GIF).
Is there a way to animate rotation to the nearest value? (in our case from 335 to 13 clockwise). Maybe there's a better way instead of interpolation?
let heading = this.state.heading;
let oldHeading = this.state.oldHeading;
const duration = 200;
if (oldHeading - heading > 180) {
Animated.timing(
this.state.spinAnim,
{
toValue: 360,
duration: duration,
easing: Easing.linear,
useNativeDriver: true
}).start(() => this.state.spinAnim.setValue(0));
} else if (oldHeading - heading < -180) {
Animated.timing(
this.state.spinAnim,
{
toValue: 0,
duration: duration,
easing: Easing.linear,
useNativeDriver: true
}).start(() => this.state.spinAnim.setValue(360));
} else {
Animated.timing(
this.state.spinAnim,
{
toValue: heading,
duration: duration,
easing: Easing.linear,
useNativeDriver: true
}).start();
}
I had the same problem so my solution was to keep every time the exactly previous heading and if the distance is bigger than 180 i use two separate animations. One that animates from x to 0 and then sets value to 360 (or x to 360 and then sets value to 0) and the one that is the "default" and runs every time.
This is equivalent to avoiding rollbacks in CSS rotate transitions.
A way to do this is to recalculate the next angle based on the amplitude of the previous one. You still have to use interpolation, what changes is that the value fed to the animation will be the result of this function (credits), which adds the difference to the previous value and performs some modulo checks to guess the closest value:
function closestEquivalentAngle(from, to) {
var delta = ((((to - from) % 360) + 540) % 360) - 180;
return from + delta;
}
So for example, next time you move forward from 335 deg to 13 deg, the result will be 373, that is the original value plus a difference of 38 degrees. That works backwards as well. Just remember to store the value of the resulting angle somewhere, so you can use it on the next iteration as prevAngle.
Pulling it all together:
const RotateComp = (props) => {
const rotationValue = React.useRef(new Animated.Value(0)).current;
// Animation interpolation
const rotation = rotationValue.interpolate({
inputRange: [-360, 360],
outputRange: ['-360deg', '360deg'],
});
React.useEffect(() => {
// Obtain prevAngle and nextAngle, then...
Animated.timing(
rotationValue,
{
toValue: closestEquivalentAngle(prevAngle, nextAngle),
duration: 200,
useNativeDriver: true,
},
).start();
}, [rotationValue])
return (
// ...
<Animated.View
style={{
...props.style,
transform: [{rotate: rotation}],
}}
>
{props.children}
</Animated.View>
);
}

ScrollMagic - set a variable scene

I have a page with multiple similar sections with the class ".imgPanel" that I would like to apply the same animation to each section every time the page scrolls into view.
I don't want to create a new scene for each animation as they are all the same. I know there is a way to create a variable scene (i hope that is the correct terminology), and I am hoping someone can help.
Does anyone know how I would adjust the code below to make that happen. I am sure if I get one example of it with my code below I will be able to understand and use this technique again.
Thanks in advance. Adam.
function scrollMagic() {
if (!Modernizr.touch) {
var controller = new ScrollMagic.Controller({
duration: "200%",
});
var panelAnim = new TimelineMax();
panelAnim
.from($('.imgPanel'), 1.4, {
y: '-50px',
autoAlpha: 0,
ease: Power1.easeOut
}, '-=0.2')
var introScene = new ScrollMagic.Scene({
duration: "100%",
})
.setTween(panelAnim)
.addTo(controller);
}
}
I figured it out after watching Ihatetomatoes video
function scrollMagic() {
if (!Modernizr.touch) {
var controller = new ScrollMagic.Controller({
duration: "100%",
});
$('.imgPanel').each(function(){
var tween = TweenMax.from($(this), 1.4, { y: '-50px', autoAlpha: 0,
ease: Power1.easeOut }, '-=0.2');
var scene = new ScrollMagic.Scene ({
duration: "100%",
offset: -300, // offset trigger position by 100px
triggerElement: this
})
.setTween(tween)
.addTo(controller);
});
}
}

GSAP pause animation in a function

I have a set of buttons that when clicked show a sort of pop up and some simple animations. Each pop up contain the same animations for most of the content. Each pop up also has its own sets of animations so I have done the following to get this to work correctly.
$gridTitles.click(function() {
const tl = new TimelineMax();
const $pop = $(this).next('.grid__pop');
const $chars = $pop.find(".grid__pop-title span");
const $items = $pop.find(".grid__pop-list li");
const func = $(this).data("graphic-function");
tl.set($pop, {
autoAlpha: 0,
display: 'block',
scale: .5
})
.to($pop, 1, {
autoAlpha: 1,
scale: 1,
ease: Power2.easeInOut
})
.staggerFrom($chars, 0.01, {
autoAlpha: 0,
ease: Power2.easeIn
}, 0.1)
.add(graphicAnimation[func])
.staggerFrom($items, 0.8, {
autoAlpha: 0,
rotationX: 90,
ease: Power2.easeOut
}, .8);
return tl;
});
This runs my pop up code and also using the .add function I call another function that runs the specific animation for the pop up based on a data attribute matching the name of a function in an object.
const graphicAnimation = {
graphicServer: function() {
const tl = new TimelineMax();
const $server = $(".graphic-server__server");
const $one = $(".graphic-server__one");
const $two = $(".graphic-server__two");
const $three = $(".graphic-server__three");
return tl.to($server, 1, {
autoAlpha: 1,
xPercent: "0",
ease: Power2.easeInOut
})
.to($one, 1, {
autoAlpha: 1,
xPercent: "0",
ease: Power2.easeInOut
})
.to($two, 1, {
autoAlpha: 1,
xPercent: "0",
ease: Power2.easeInOut
})
.to($three, 1, {
autoAlpha: 1,
xPercent: "0",
ease: Power2.easeInOut
})
},
// more functions
}
This works great depending on which button is clicked the correct function is called in the object and my animations run. The problem now is that some of these animations are looping and when I close the popup I can't pause them.
Using something like the following I tried to accomplish this
$gridCloses.click(function() {
const tl = new TimelineMax();
const $pop = $(this).parents(".grid__pop");
const func = $(this).parents('.grid__pop').siblings('.grid__title').data("graphic-function");
graphicAnimation[func].pause();
return tl.to($pop, 1, {
autoAlpha: 0,
scale: .5,
display: 'none'
});
});
But calling graphicAnimation[func].pause(); isn't going to work as pause() is a function on the returned timeline from that function. How can I access the current running function and pause / kill it.
So I thought this out and just need to store the timelines in their own object as well
const graphicTimeLines = {
graphicServer : new TimelineMax(),
graphicType: new TimelineMax()
}
so with that in my closing action I can do something like the following to completely reset my loops and have the timeline be available to be called again.
graphicTimeLines[func].pause().progress(0).kill();
graphicTimeLines[func] = new TimelineMax();