React Native custom loader Animation from CSS code - react-native

I'm having a problem recreating a
CSS loader animation in React Native with Animated API.
My React Native attempt:
screen record.
CSS code is below:
.whitePillar {
width: 8px;
height: 50px;
margin-right:5px;
background-color: #FFF;
animation: animation 1s infinite;
float: left;
}
.whitePillar :nth-child(10) {
animation-delay: 0.9s;
}
.whitePillar :nth-child(9){
-webkit-animation-delay: 0.8s;
animation-delay: 0.8s;
}
.whitePillar :nth-child(8) {
animation-delay: 0.7s;
}
.whitePillar :nth-child(7){
-webkit-animation-delay: 0.6s;
animation-delay: 0.6s;
}
.whitePillar :nth-child(6) {
animation-delay: 0.5s;
}
.whitePillar :nth-child(5) {
animation-delay: 0.4s;
}
.whitePillar :nth-child(4){
-webkit-animation-delay: 0.3s;
animation-delay: 0.3s;
}
.whitePillar :nth-child(3) {
animation-delay: 0.2s;
}
.whitePillar :nth-child(2) {
animation-delay: 0.1s;
}
#keyframes animation {
50% {
transform: translateX(-25px) scaleY(0.5);
}
}
React Native code is below:
<View style={styles.loaderContainer}>
<View style={styles.pillarsWrapper}>
{[...Array(numberOfPillars)].map((value, index) => (
<LoaderPillar
delay={index*100}
key={index}
/>
))}
</View>
</View>
LoaderPillar represents every individual white pillar:
<Animated.View
style={[styles.pillar, pillarAnimation]}
/>
and I've done the animation on LoaderPillar using this:
const [ pillarAnimationTranslate ] = useState(new Animated.Value(0));
const [ pillarAnimationScale ] = useState(new Animated.Value(1));
const pillarAnimation = {
transform: [{translateX: pillarAnimationTranslate}, {scaleY: pillarAnimationScale}]
};
useEffect(() => {
Animated.loop(Animated.timing(pillarAnimationScale, {
toValue: 0.5,
duration: 500,
delay: delay,
useNativeDriver: true,
easing: Easing.ease
})).start();
}, [pillarAnimationScale]);
useEffect(() => {
Animated.loop(Animated.timing(pillarAnimationTranslate, {
toValue: -25,
duration: 500,
delay: delay,
useNativeDriver: true,
easing: Easing.ease
})).start()
}, [pillarAnimationTranslate]);
I am new to React Native animations, so I don't even know if I'm on the right track. The problem, I think, is the keyframes part in CSS - that percentage (50%) - and I don't know how would I recreate that.
Any advices on upgrading this or even completely changing would be greatly appreciated. Thank You in advance.

EDIT: still NOT working, but very close to the end result.
I broke down this problem into two parts: first making scale animation and then just adding translate animation - it is easier that way.
Code:
const [ pillarAnimationTranslate ] = useState(new Animated.Value(0));
Animated.loop(
Animated.sequence([
Animated.timing(
pillarAnimationScale,
{
toValue: 1,
duration: 500,
delay: delay,
useNativeDriver: true,
easing: Easing.ease
}
),
Animated.timing(
pillarAnimationScale,
{
toValue: 0,
duration: 500,
useNativeDriver: true
})
])
).start();
And then using interpolate function:
const scale = pillarAnimationScale.interpolate({
inputRange: [0, 1],
outputRange: [1, 0.5]
});
const pillarAnimation = {
transform: [{scaleY: scale}]
};
And in the ned returning Animated View (as in the original post);
return (
< Animated.View
style={[styles.pillar, pillarAnimation]}
/>
);
The problem I'm now having is inconsistency of the iterations, as you can see in the GIF below (first iteration is perfect and then it just gets chaotic for some reason, it should be a perfect wave at all times):
Does anybody see what is causing inconsistent iterations?

Related

How to animate shrinking and growing of two elements at the same time?

I want to create a weekly plan and the user should be able to "select" a day which should then grow in the ui (example below).
I am using the Animated class from react-native and noticed I am not able to animate the flex-property, so I believe I have to do this using width and height.
I tried the following after reading #M.N.s awnser:
selectDay = (i: number) => {
const {selected, grow, shrink} = this.state;
this.setState({selected: i, latestSelected: selected});
grow.setValue(0.5);
shrink.setValue(1);
Animated.parallel(
[
Animated.timing(grow, {
toValue: 1,
duration: 200,
useNativeDriver: false,
easing: Easing.cubic,
}),
Animated.timing(shrink, {
toValue: 0.5,
duration: 200,
useNativeDriver: false,
easing: Easing.cubic,
}),
],
{stopTogether: false},
).start();
};
// In my render:
const growInterpolate = grow.interpolate({
inputRange: [0, 1],
outputRange: ['0%', '25%'],
});
const shrinkInterpolate = shrink.interpolate({
inputRange: [0, 1],
outputRange: ['0%', '25%'],
});
But it still behaves very laggy:
Video of the animation
In such case you need to use parallel animation:
Animated.parallel([
Animated.timing(animatedHeight, {
toValue: newHeight,
duration: 200,
}),
Animated.timing(animatedWidth, {
toValue: newWidth,
duration: 200,
})
], { stopTogether: false }).start()
More information:
https://reactnative.dev/docs/animated#parallel

how to create a barcode scanner with quagga with fixed scanning canvas

I'm on Nuxt 2.15.4 and using Quagga2. I want to create a scanner like mobile scanners where the camera is full screen and there is a blue rectangle that the barcode must be placed inside of, and after scanning there will be a red line through the barcode canvas. With Quagga.js I can manipulate the CSS and area property in canvas but it cause 2 problems:
The actual scanning zone will be messed up and I don't know where the scanning exactly happens.
The successful red line-through style will be messed up.
Also, after a successful scan I need the red line-through to remain and not disappear and no scanning happen for a period of time (so I can decide to continue scanning or do something with result).
Here is the code I implemented on in app (also need help on activating torch and zoom):
<template>
<section id="container" class="container">
<div id="interactive" class="viewport"></div>
{{code}}
</section>
</template>
<script>
import Quagga from '#ericblade/quagga2';
export default {
data() {
return {
code:[]
};
},
methods:{
async checkDevice() {
let md = navigator.mediaDevices;
if (!md || !md.enumerateDevices) return false;
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.some((device) => "videoinput" === device.kind);
},
initQuagga(){
Quagga.init({
inputStream : {
name : "Live",
type : "LiveStream",
area: {
top: "0%",
right: "0%",
left: "0%",
bottom: "0%",
},
},
locate: false,
decoder : {
readers : [
"ean_reader",
],
}
},(err)=>{
if(err){
return
}
// this.checkCapabilities();
this.startQuagga()
});
},
// checkCapabilities(){
// var track = Quagga.CameraAccess.getActiveTrack();
// var capabilities = {};
// if (typeof track.getCapabilities === 'function') {
// capabilities = track.getCapabilities();
// }
// this.applySettingsVisibility('zoom', capabilities.zoom);
// this.applySettingsVisibility('torch', capabilities.torch);
// },
// applySetting: function(setting, value) {
// var track = Quagga.CameraAccess.getActiveTrack();
// if (track && typeof track.getCapabilities === 'function') {
// switch (setting) {
// case 'zoom':
// return track.applyConstraints({advanced: [{zoom: parseFloat(value)}]});
// case 'torch':
// return track.applyConstraints({advanced: [{torch: !!value}]});
// }
// }
// },
startQuagga(){
Quagga.start()
Quagga.onProcessed(function (result) {
let drawingCtx = Quagga.canvas.ctx.overlay,
drawingCanvas = Quagga.canvas.dom.overlay;
if (result) {
if (result.boxes) {
drawingCtx.clearRect(0, 0, parseInt(drawingCanvas.getAttribute("width")), parseInt(drawingCanvas.getAttribute("height")));
result.boxes.filter(function (box) {
return box !== result.box;
}).forEach(function (box) {
Quagga.ImageDebug.drawPath(box, {x: 0, y: 1}, drawingCtx, {color: "#00F", lineWidth: 2});
});
}
if (result.box) {
Quagga.ImageDebug.drawPath(result.box, {x: 0, y: 1}, drawingCtx, {color: "#008", lineWidth: 2});
}
if (result.codeResult && result.codeResult.code) {
Quagga.ImageDebug.drawPath(result.line, {x: 'x', y: 'y'}, drawingCtx, {color: 'red', lineWidth: 3});
}
}
})
Quagga.onDetected(this.onDetected);
},
onDetected(data) {
let barCodeData = data.codeResult.code;
console.log(barCodeData);
},
},
async mounted(){
let data = await this.checkDevice();
if (data) {
this.initQuagga();
}
},
beforeDestroy(){
Quagga.stop()
}
}
</script>
<style lang="scss">
#container {
width: 640px;
margin: 20px auto;
padding: 10px;
}
#interactive.viewport {
width: 640px;
height: 480px;
}
#interactive.viewport canvas, video {
float: left;
width: 640px;
height: 480px;
}
#interactive.viewport canvas.drawingBuffer, video.drawingBuffer {
margin-left: -640px;
}
</style>

React Native Animated Rotating Circles with scale dependency

I have an animated component where you can select one of seventeen circles. It looks like this so far:
I would like to add an animation that scales the circle as it gets closer to the center. How do I do that?
Until now I tried to calculate the x value of the circle as Math.sin(index*deltaTheta*Math.PI/180 + Math.PI)*Radius and use this value in a functions which maps to a scaling factor (e.g. a gaussian). This fails because the x value does not change, because I am using CSS transform rotate.
Then I tried to use a different interpolating range for every single circle, but did not achieved a satisfying result.
My code:
import React, { Component } from 'react'
import { Text, View, PanResponder, Animated, Dimensions } from 'react-native'
import styled from 'styled-components'
import Circle from './Circle'
const SCREEN_WIDTH = Dimensions.get('window').width
const Container = styled(Animated.View)`
margin: auto;
width: 200px;
height: 200px;
position: relative;
top: 100px;
`
const gaussFunc = (x, sigma, mu) => {
return 1/sigma/Math.sqrt(2.0*Math.PI)*Math.exp(-1.0/2.0*Math.pow((x-mu)/sigma,2))
}
const myGaussFunc = (x) => gaussFunc(x, 1/2/Math.sqrt(2*Math.PI), 0)
const circles = [{
color: 'red'
}, {
color: 'blue'
}, {
color: 'green'
}, {
color: 'yellow'
}, {
color: 'purple'
}, {
color: 'black'
}, {
color: 'gray'
}, {
color: 'pink'
}, {
color: 'lime'
}, {
color: 'darkgreen'
}, {
color: 'crimson'
}, {
color: 'orange'
}, {
color: 'cyan'
}, {
color: 'navy'
}, {
color: 'indigo'
}, {
color: 'brown'
}, {
color: 'peru'
}
]
function withFunction(callback) {
let inputRange = [], outputRange = [], steps = 50;
/// input range 0-1
for (let i=0; i<=steps; ++i) {
let key = i/steps;
inputRange.push(key);
outputRange.push(callback(key));
}
return { inputRange, outputRange };
}
export default class SDGCircle extends Component {
state = {
deltaTheta: 360/circles.length,
Radius: 0, // radius of center circle (contaienr)
radius: 25, // radius of orbiting circles
container: { height: 0, width: 0 },
deltaAnim: new Animated.Value(0),
}
offset = () => parseInt(this.state.container.width/2)-this.state.radius
_panResponder = PanResponder.create({
nMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (event, gestureState) => true,
onPanResponderGrant: () => {
const { deltaAnim } = this.state
deltaAnim.setOffset(deltaAnim._value)
deltaAnim.setValue(0)
},
onPanResponderMove: (event, gestureState) => {
const { deltaAnim, scaleAnim, deltaTheta, Radius } = this.state
deltaAnim.setValue(gestureState.dx)
console.log(deltaAnim)
},
onPanResponderRelease: (event, gestureState) => {
const {dx, vx} = gestureState
const {deltaAnim} = this.state
deltaAnim.flattenOffset()
Animated.spring(deltaAnim, {
toValue: this.getIthCircleValue(dx, deltaAnim),
friction: 5,
tension: 10,
}).start(() => this.simplifyOffset(deltaAnim._value));
}
})
getIthCircleValue = (dx, deltaAnim) => {
const selectedCircle = Math.round(deltaAnim._value/(600/circles.length))
return (selectedCircle)*600/circles.length
}
getAmountForNextSlice = (dx, offset) => {
// This just rounds to the nearest 200 to snap the circle to the correct thirds
const snappedOffset = this.snapOffset(offset);
// Depending on the direction, we either add 200 or subtract 200 to calculate new offset position. (200 are equal to 120deg!)
// const newOffset = dx > 0 ? snappedOffset + 200 : snappedOffset - 200; // fixed for 3 circles
const newOffset = dx > 0 ? snappedOffset + 600/circles.length : snappedOffset - 600/circles.length;
return newOffset;
}
snapOffset = (offset) => { return Math.round(offset / (600/circles.length)) * 600/circles.length; }
simplifyOffset = (val) => {
const { deltaAnim } = this.state
if(deltaAnim._offset > 600) deltaAnim.setOffset(deltaAnim._offset - 600)
if(deltaAnim._offset < -600) deltaAnim.setOffset(deltaAnim._offset + 600)
}
handleLayout = ({ nativeEvent }) => {
this.setState({
Radius: nativeEvent.layout.width,
container: {
height: nativeEvent.layout.height,
width: nativeEvent.layout.width
}
})
}
render() {
const {deltaAnim, radius} = this.state
return (
<Container
onLayout={this.handleLayout}
{...this._panResponder.panHandlers}
style={{
transform: [{
rotate: deltaAnim.interpolate({
inputRange: [-200, 0, 200],
outputRange: ['-120deg', '0deg', '120deg']
})
}]
}}
>
{circles.map((circle, index) => {
const {deltaTheta, Radius} = this.state
return (
<Circle
key={index}
color={circle.color}
radius={radius}
style={{
left: Math.sin(index*deltaTheta*Math.PI/180 + Math.PI)*Radius+this.offset(),
top: Math.cos(index*deltaTheta*Math.PI/180 + Math.PI)*Radius+this.offset(),
}}
>
<Text style={{color: 'white'}}>{index}</Text>
</Circle>
)
})}
</Container>
)
}
}
FYI: I got a solution. The result looks like this:
and the source code is given by:
import React, { Component } from 'react'
import { Text, View, PanResponder, Animated, Dimensions } from 'react-native'
import styled from 'styled-components'
import Circle from './Circle'
const SCREEN_WIDTH = Dimensions.get('window').width
const Container = styled(Animated.View)`
margin: auto;
width: 200px;
height: 200px;
position: relative;
top: 100px;
`
const gaussFunc = (x, sigma, mu) => {
return 1/sigma/Math.sqrt(2.0*Math.PI)*Math.exp(-1.0/2.0*Math.pow((x-mu)/sigma,2))
}
const myGaussFunc = (x) => gaussFunc(x, 1/2/Math.sqrt(2*Math.PI), 0)
const circles = [{
color: 'red'
}, {
color: 'blue'
}, {
color: 'green'
}, {
color: 'yellow'
}, {
color: 'purple'
}, {
color: 'black'
}, {
color: 'gray'
}, {
color: 'pink'
}, {
color: 'lime'
}, {
color: 'darkgreen'
}, {
color: 'crimson'
}, {
color: 'orange'
}, {
color: 'cyan'
}, {
color: 'navy'
}, {
color: 'indigo'
}, {
color: 'brown'
}, {
color: 'peru'
}]
function withFunction(callback) {
let inputRange = [], outputRange = [], steps = 50;
/// input range 0-1
for (let i=0; i<=steps; ++i) {
let key = i/steps;
inputRange.push(key);
outputRange.push(callback(key));
}
return { inputRange, outputRange };
}
export default class SDGCircle extends Component {
constructor(props) {
super(props)
const deltaTheta = 360/circles.length
const pxPerDeg = 200/120
const thetas = []
for (const i in circles) {
let val = i*deltaTheta*pxPerDeg
if(i >= 9)
val = -(circles.length-i)*deltaTheta*pxPerDeg
thetas.push(val)
}
this.state = {
deltaTheta,
Radius: 0, // radius of center circle (contaienr)
radius: 25, // radius of orbiting circles
container: { height: 0, width: 0 },
deltaAnim: new Animated.Value(0),
thetas,
thetasAnim: thetas.map(theta => new Animated.Value(theta)),
}
}
offset = () => parseInt(this.state.container.width/2)-this.state.radius
_panResponder = PanResponder.create({
nMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (event, gestureState) => true,
onPanResponderGrant: () => {
const { deltaAnim, thetasAnim, thetas } = this.state
deltaAnim.setOffset(deltaAnim._value)
deltaAnim.setValue(0)
const iSel = Math.round((deltaAnim._value+deltaAnim._offset)/(600/circles.length))
for(let i=0; i<circles.length; i++) {
let xi = i+iSel
if(xi > 16)
xi -= circles.length
if(xi < 0)
xi += circles.length
try {
thetasAnim[xi].setOffset(thetas[i])
} catch(err) {console.log(xi)}
}
},
onPanResponderMove: (event, gestureState) => {
const { deltaAnim, scaleAnim, deltaTheta, Radius, thetasAnim } = this.state
deltaAnim.setValue(gestureState.dx)
for (theta of thetasAnim) {
theta.setValue(-gestureState.dx)
}
},
onPanResponderRelease: (event, gestureState) => {
const {dx, vx} = gestureState
const {deltaAnim, thetasAnim, deltaTheta, thetas} = this.state
deltaAnim.flattenOffset()
const ithCircleValue = this.getIthCircleValue(dx, deltaAnim)
Animated.spring(deltaAnim, {
toValue: ithCircleValue,
friction: 5,
tension: 10,
}).start(() => {
this.simplifyOffset(deltaAnim)
});
}
})
getIthCircleValue = (dx, deltaAnim) => {
const selectedCircle = Math.round((deltaAnim._value+deltaAnim._offset)/(600/circles.length))
return (selectedCircle)*600/circles.length
}
snapOffset = (offset) => { return Math.round(offset / (600/circles.length)) * 600/circles.length; }
simplifyOffset = (anim) => {
if(anim._value + anim._offset >= 600) anim.setOffset(anim._offset - 600)
if(anim._value + anim._offset <= -600) anim.setOffset(anim._offset + 600)
}
handleLayout = ({ nativeEvent }) => {
this.setState({
Radius: nativeEvent.layout.width,
container: {
height: nativeEvent.layout.height,
width: nativeEvent.layout.width
}
})
}
render() {
const {deltaAnim, radius} = this.state
return (
<Container
onLayout={this.handleLayout}
{...this._panResponder.panHandlers}
style={{
transform: [{
rotate: deltaAnim.interpolate({
inputRange: [-200, 0, 200],
outputRange: ['-120deg', '0deg', '120deg']
})
}]
}}
>
{circles.map((circle, index) => {
const {deltaTheta, thetasAnim, Radius} = this.state
/* const difInPx = index*deltaTheta*200/120 */
let i = index
/* if(index >= Math.round(circles.length/2)) */
/* i = circles.length - index */
scale = thetasAnim[i].interpolate({
inputRange: [-300, 0, 300],
outputRange: [0, 2, 0],
})
return (
<Circle
key={index}
color={circle.color}
radius={radius}
style={{
left: Math.sin(index*deltaTheta*Math.PI/180 + Math.PI)*Radius+this.offset(),
top: Math.cos(index*deltaTheta*Math.PI/180 + Math.PI)*Radius+this.offset(),
transform: [{ scale }],
}}
>
<Text style={{color: 'white'}}>{index}</Text>
</Circle>
)
})}
</Container>
)
}
}

How to trigger TimelineMax animation when ScrollToPlugin is scrolling (ScrollMagic)?

I have a section with button which on click triggers scroll to the next section.
What I want to do is when this scroll event is happening I want to trigger my tl.from animations.
Right now animations tl.from are triggered only on user scroll but not on button pressed.
const button = document.getElementById('cta');
let tl = new TimelineMax({ onUpdate: updatePercentage })
function scrollToNextSection() {
TweenLite.to(window, 2, { scrollTo: '#section-1'});
}
tl.from('.section__left', .5, { x: 200, opacity: 0, ease: Power4.easeOut })
tl.from('.section__right', .5, { x: -200, opacity: 0, ease: Power4.easeOut })
tl.from('.section__img', 1, { x: 400, opacity: 0 })
// init controller
var controller = new ScrollMagic.Controller();
// create a scene
new ScrollMagic.Scene({
triggerElement: '#section-1',
triggerHook: 'onLeave',
duration: '100%',
})
.setPin("#section-1")
.setTween(tl)
.addTo(controller);
function updatePercentage() {
tl.progress();
}
button.addEventListener('click', scrollToNextSection);
Try to change your code like that :)
const button = document.getElementById('cta');
let tl = new TimelineMax({ onUpdate: updatePercentage }).from('.section__left', .5, { x: 200, opacity: 0, ease: Power4.easeOut })
.from('.section__right', .5, { x: -200, opacity: 0, ease: Power4.easeOut })
.from('.section__img', 1, { x: 400, opacity: 0 });
function scrollToNextSection() {
TweenLite.to(window, 2, { scrollTo: '#section-1'});
}
// init controller
var controller = new ScrollMagic.Controller();
// create a scene
new ScrollMagic.Scene({
triggerElement: '#section-1',
triggerHook: 'onLeave',
duration: '100%',
})
.setPin("#section-1")
.setTween(tl)
.addTo(controller);
function updatePercentage() {
tl.progress();
}

scrollmagic anchoring or pausing animation until user scrolls again

with the following code,
var firstScene = new TimelineLite()
.to(".some-background", 1000, { transform: 'translate(350px, 0)' })
.to(".old-text", 1000, { opacity: 0 })
.to(".new-text", 1000, { opacity: 1 })
.to(".new-text", 1000, { opacity: 0 })
.to(".new-text-2", 1000, { opacity: 1 })
new ScrollMagic.Scene({triggerHook: 'onLeave', triggerElement: "#container", duration: 5000})
.setPin("#container")
.setTween(firstScene)
.addTo(controller);
is it possible to 'anchor' or stop/pause the animation between fading in .new-text and fading it out until user scrolls again rather than creating another trigger somewhere to just delay the animation?