Applying Animation to React Component while using Styled Components - animation

I am using a new library to create React Components, Styled-Components.
I want to apply an animation Tremble on my component via an onClick.
export class FormLoginTest extends React.Component { // eslint-disable-line react/prefer-stateless-function
static propTypes = {
isTrembling: React.PropTypes.bool
};
static defaultProps = {
isTrembling: true
};
onMakeTremble() {
alert("hello")
}
render() {
return (
<Form >
<ContainerFluid>
<H2>Login</H2>
</ContainerFluid>
<ContainerFluid>
<Label htmlFor="username">Username</Label>
<Input type="text" id="username" placeholder="bob" autoCorrect="off" autoCapitalize="off" spellCheck="false" />
</ContainerFluid>
<ContainerFluid>
<Label htmlFor="password">Password</Label>
<Input id="password" type="password" placeholder="••••••••••" />
</ContainerFluid>
<ContainerFluid>
<Button nature="success" onClick={this.onMakeTremble}>Hello</Button>
</ContainerFluid>
</Form>
);
}
}
So there is no Style.css sheet with Styled Components, all css is applied via javascript. Form has already been applied a css:
export class Form extends React.Component { // eslint-disable-line react/prefer-stateless-function
static propTypes = {
children: React.PropTypes.node.isRequired,
className: React.PropTypes.string
};
//
static defaultProps = {
isTrembling: true
};
render() {
return (
<form className={this.props.className}>
{React.Children.toArray(this.props.children)}
</form>
);
}
}
// eslint-disable-next-line no-class-assign
Form = styled(Form)`
max-width: 800px;
margin:0 auto;
display: block;
height: 100%;
border: 1px solid grey;
& h2{
text-align:center;
};
`;
export default Form;
And I have a component Tremble as well:
const shake = keyframes`
10%, 90% {
transform: translate3d(-1px, 0, 0);
}
20%, 80% {
transform: translate3d(2px, 0, 0);
}
30%, 50%, 70% {
transform: translate3d(-4px, 0, 0);
}
40%, 60% {
transform: translate3d(4px, 0, 0);
}
`;
const Tremble = styled.div`
display: inline-block;
&:hover {
animation: ${shake} 0.82s cubic-bezier(.36,.07,.19,.97) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
}
`;
export default Tremble;
Any clue as to how this may work?

Check out the docs under the keyframes section.
https://www.npmjs.com/package/styled-components
You might try importing keyframes from 'styled-components' and use it like this:
Example
import styled, {keyframes} from 'styled-components';
const moveGradient = keyframes`
0%{background-position:38% 0%}
50%{background-position:63% 100%}
100%{background-position:38% 0%}
`;
const Wrapper = styled.div`
background-size: 800% 800%;
width: 100vw;
height: 100vh;
background: linear-gradient(${props => props.gradientRotation}, #cc6bbb, #ffa86d);
animation: ${moveGradient} 10s ease-out infinite;
`;
I am encountering a problem with keyframes myself, as this code doesnt work with my gradient animation, but it will with others.
I will link to my question/issue in a comment here later :)!

Related

How to stop Flickering a moving image in Nextjs

I'm new to next and I have been trying to make the game flappy bird. I used useEffect and styled components to animate the player(or bird) and pipes too. Basically when I run my app on chrome, it works fine but it flickers in safari. And after I play it a few times in safari it starts to work almost fine. For state management, I'm using redux.
Can someone help me to solve the problem and explain what is actually going on?
From what I think it is because of the re-rendering of the images but why is it working properly in chrome? And is there a better way to animate this?
This is my main code and I used the useEffect inside Bird and Pipes file to move them across the GameBox
import styled from 'styled-components'
import Pipes from './Pipes'
import { startGame, setBirdPosition, resetGame } from './features/app-slice'
import { store, constants } from './store'
import { useSelector } from 'react-redux'
import Bird from './Bird'
import { useEffect, useState } from 'react'
export default function GameBox() {
const [jumpAudio, setAudio] = useState(null)
useEffect(() => {
setAudio(new Audio('/sound-effects/jump.wav'))
// only run once on the first render on the client
}, [])
const birdPosition = useSelector((state) => state.birdPosition)
const score = useSelector((state) => state.score)
const gameStarted = useSelector(state => state.gameStarted)
const isGameOver = useSelector(state => state.isGameOver)
function jump() {
const JUMP = constants.JUMP
if (isGameOver) {
store.dispatch(resetGame())
return
}
else if (!gameStarted) {
// store.dispatch(resetGame())
store.dispatch(startGame())
}
else if (birdPosition - JUMP >= 0)
store.dispatch(setBirdPosition(-JUMP))
else store.dispatch(setBirdPosition(0))
jumpAudio.pause();
jumpAudio.currentTime = 0;
jumpAudio.play()
}
return (
<Box onClick={jump}>
{isGameOver ? <GameOver /> : null}
{gameStarted || isGameOver ? <Score>{score}</Score> : null}
{true ? <Bird /> : null}
<Pipes height={200} />
{!gameStarted && !isGameOver ? <GameStart /> : null}
{/* <Pipes height={200} position={props.width + 300} wh={props.height} /> */}
</Box>
)
}
const Box = styled.div`
user-select: none; /* supported by Chrome and Opera */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none;
background: no-repeat center/100% url('/img/background-day.png');
overflow: hidden;
position: relative;
width: ${constants.WINDOW_WIDTH}px;
height: ${constants.WINDOW_HEIGHT}px
`
const GameStart = styled.div`
background: no-repeat center/70% url('/img/gamestart.png');
text-align: center;
width: 100%;
height: 100%;
`
const GameOver = styled.div`
position: relative;
z-index: 10;
background: no-repeat center/70% url('/img/gameover.png');
text-align: center;
width: 100%;
height: 100%;
`
const Score = styled.div`
font-family: 'Gamja Flower', cursive;
color: white;
text-shadow: black 2px 2px;
position: absolute;
font-size: 3rem;
z-index:1;
right: 10%;
top: 0;
`

Microsoft Botframework webchat. How to fix issue with activityMiddleware and showAvatarInGroup after update to 4.14

Working from the minimizable webchat sample (react) a custom typing indicator is added that renders as soon as the bot sends an typingIndicator event:
await context.sendActivity({ name: 'typingIndicator', type: 'event' });
In WebChat.js
import TypingIndicator from './TypingIndicator';
const activityMiddleware = () => next => ({ activity, nextVisibleActivity, ...otherArgs }) => {
const { name } = activity;
// first remove all existing typing indicators from the DOM
let elements = document.getElementsByClassName('typing-indicator');
for (var i = 0; i < elements.length; i++) {
elements[i].style.display = 'none'
}
// if typing indicator event received from the bot, return one to be rendered
if (name === 'typingIndicator') {
return () => <TypingIndicator activity={activity} nextVisibleActivity={nextVisibleActivity} />;
}
else {
return next({ activity, nextVisibleActivity, ...otherArgs });
}
};
TypingIndicator.js
import React from 'react';
import './TypingIndicator.css';
const TypingIndicator = () => {
return (
<div className="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
);
};
export default TypingIndicator
TypingIndicator.css
.typing-indicator {
background-color: transparent;
height: 35px;
width: 60px!important;
border-radius: 20px;
padding:10px;
margin-left: 70px;
}
.typing-indicator span {
line-height: 35px;
display:inline-block;
vertical-align: middle;
height: 10px;
width: 10px;
margin: 0 1px;
background-color: #9E9EA1;
border-radius: 50%;
opacity: 0.4;
animation: bounce 0.7s linear infinite;
}
.typing-indicator span:nth-child(1)
{
animation-delay: 0.1s;
}
.typing-indicator span:nth-child(2)
{
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3)
{
animation-delay: 0.1s;
}
#keyframes bounce {
30% { transform: translateY(-4px); }
60% { transform: translateY(0px); }
80% { transform: translateY(4px); }
100% { transform: translateY(0px); opacity: 0.5; }
}
This worked fine in 4.9. Afer updating to 4.14 it still works until I decided to only show one avatar for each group of activities:
const styleOptions = {
showAvatarInGroup: 'sender',
...
}
As soon as I add this option to styleOptions, the Bot avatar is not shown anymore. As soon as I change the setting to something else, the botAvatar is shown again.
As soon as I remove the activityMiddleware, I am able to show one avatar per group of activities.
[Update]
I did some additional digging and narrowed it down to my botcode.
.1 Installed webchat sample d.reaction-buttons
.2 Added Styleoptions to show bot avatar
.3 Added ShowAvatarInGroup: 'sender'
This works fine. The BotAvator is shown
But as soon as I replace the webchat-mockbot with my own bot, the problem returns:
ShowAvatarInGroup: 'sender' -> Not bot avatar
ShowAvatarInGroup: '' -> bot avatar
In both cases the console is showing these error messages:
Not sure whats causing this. Was never a problem before. I will try and figure out what I am sending to webchat that's breaking the rendering of botAvatar
[Update2]
According to webchats changelog, I need to rewrite the activity midleware to something like this:
() => next => (...setupArgs) => {
const render = next(...setupArgs);
return render && (...renderArgs) => {
const element = render(...renderArgs);
return element && <div>{element}</div>;
};
}
I can't get this to work for my purpose however. My typingIndicator reactelement is not being rendered if I use it like this.
Any guidance on how to fix this, is much appreciated.

How to fade one react component out, then fade another in?

I've got 2 components that are conditionally shown or hidden based on a string value stored in useState as showModal
{showModal === 'SIGNIN' && <SignIn />}
{showModal === 'JOIN' && <Join />}
I want to fade in one component, then when the state changes, fade it out and fade in the other component.
Can this be done with react transition group?
I tried
<TransitionGroup>
<CSSTransition in={showModal === 'SIGNIN' ? true : false} classNames='fade' timeout={220} key={showModal}>
<div>
<SignIn />
</div>
</CSSTransition>
<CSSTransition in={showModal === 'JOIN' ? true : false} classNames='fade' timeout={220} key={showModal}>
<div>
<Join />
</div>
</CSSTransition>
</TransitionGroup>
I don't get any error, one component is shown, changing showModal from 'SIGNIN' to 'JOIN' does nothing. Inspecting the divs with the timeout set to 22000 shows that no new classes have been added.
SwitchTransition from react transition group might help.
Example
const { useState, useEffect } = React;
const { SwitchTransition, CSSTransition } = ReactTransitionGroup;
const SignIn = () => <div className="block sign-in">Sign In</div>;
const Join = () => <div className="block join">Join</div>;
const App = () => {
const [showModal, setModal] = useState("SIGNIN");
useEffect(() => {
let handle;
const loop = () => {
setModal(state => state === "JOIN" ? "SIGNIN" : "JOIN");
handle = setTimeout(loop, 2500);
};
handle = setTimeout(loop, 1000);
return () => {
clearTimeout(handle);
}
}, []);
const addEndListener = (node, done) => {
node.addEventListener("transitionend", done, false);
}
return <div>
<SwitchTransition mode="out-in">
<CSSTransition
key={showModal === "SIGNIN"}
addEndListener={addEndListener}
classNames="fade">
{showModal === "SIGNIN" ? <SignIn/> : <Join/>}
</CSSTransition>
</SwitchTransition>
</div>;
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
body {
margin: 0;
overflow: hidden;
font-family: Georgia, serif;
}
.block {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 10px;
color: white;
width: 200px;
height: 100px;
display: flex;
justify-content: center;
align-items: center;
font-size: 30px;
}
.sign-in {
background: #0984e3;
}
.join {
background: #6c5ce7;
}
.fade-enter {
opacity: 0;
transform: translate(-100%, -50%);
}
.fade-exit {
opacity: 1;
transform: translate(-50%, -50%);
}
.fade-enter-active {
opacity: 1;
transform: translate(-50%, -50%);
}
.fade-exit-active {
opacity: 0;
transform: translate(100%, -50%);
}
.fade-enter-active,
.fade-exit-active {
transition: opacity 500ms, transform 500ms;
}
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react-transition-group#4.4.2
/dist/react-transition-group.min.js"></script>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<div id="root"></div>

Change border color of TextField while focused

I'm restyling my cross-platform app made with NativeScript + Angular and I want to change the border color of a textfield when I'm putting data on it.
I've tried this but it doesn't work
TextField {
margin-bottom: 10;
border-radius: 4;
background: #FFFFFF;
border-width: 0.5;
border-color: #C4C4C4;
box-sizing: border-box;
}
TextField:focus{
border-color: red;
}
This is a part of login.component.html:
<label text="Email"></label>
<TextField
hint="mario.rossi#gmail.com"
keyboardType="email"
autocorrect="false"
autocapitalizationType="none"
[(ngModel)]="user.email"
class="input"></TextField>
<label text="Password"></label>
<TextField
hint="Password"
secure="true"
[(ngModel)]="user.password"
class="input"></TextField>
How can I do?
Here is the Playground
NativeScript doesn't support any pseudo-selector while text field is focused. But you could simply listen to focus and blur events, add / remove a class for changing border color.
Since you are using Angular, a simple directive could solve this problem once in for all TextFields in your app.
HTML
<ScrollView class="page">
<StackLayout class="form">
<TextField appHighlightBorder class="m-10 input input-border"
hint="First Name"></TextField>
<TextField appHighlightBorder class="m-b-10 m-x-10 input input-border"
hint="Last Name"></TextField>
<TextField appHighlightBorder class="m-b-10 m-x-10 input input-border"
hint="Email"></TextField>
</StackLayout>
</ScrollView>
CSS
.form .input-border.focus {
border-color: red;
}
Directive
import { Directive, ElementRef, OnDestroy, Renderer2 } from '#angular/core';
import { TextField } from 'tns-core-modules/ui/text-field';
#Directive({
selector: '[appHighlightBorder]'
})
export class HighlightDirective implements OnDestroy {
private removeFocusEvent: () => void;
private removeBlurEvent: () => void;
constructor(private elementRef: ElementRef, private renderer: Renderer2) {
this.removeFocusEvent = this.renderer.listen(elementRef.nativeElement, TextField.focusEvent, () => {
renderer.addClass(elementRef.nativeElement, 'focus');
});
this.removeBlurEvent = this.renderer.listen(elementRef.nativeElement, TextField.blurEvent, () => {
renderer.removeClass(elementRef.nativeElement, 'focus');
});
}
ngOnDestroy() {
this.removeFocusEvent();
this.removeBlurEvent();
}
}
Playground Sample
Now you can use the focus event from css and change the border color.
if this is your TextField:
<TextField class="input"></TextField>
the css rule will be:
.input:focus{
border-color: blue;
}
Unfortunately, it is not there by default. However, you could implement it by yourself with focus and blur events.
For example:
<some-page>.xml
...
<TextField class="input-field"
text="{{ email }}"
keyboardType="email"
autocapitalizationType="none"
autocorrect="false"
focus="onTextFieldFocus"
blur="onTextFieldBlur" />
...
<some-page>.ts
...
const focusedPseudoClass = "focused";
export function onTextFieldFocus(args: EventData) {
const textField = <TextField>args.object;
textField.addPseudoClass(focusedPseudoClass);
}
export function onTextFieldBlur(args: EventData) {
const textField = <TextField>args.object;
textField.deletePseudoClass(focusedPseudoClass);
}
...
<some-page>.css
...
.input-field {
border-bottom-color: gray;
border-bottom-width: 1;
}
.input-field:focused {
border-bottom-color: red;
}
...
textarea {
margin-bottom: 10;
border-radius: 4;
background: #FFFFFF;
outline:0;
border-width: 0.5;
border-color: #C4C4C4;
box-sizing: border-box;
}
textarea:focus{
border-color: red;
}
<textarea></textarea>
just removed outline from textarea!
Add this to your css file. Hope it works.
TextField:focus {
outline: none !important;
border-color: red;
}

How To Sync CSS Animations Across Multiple Elements?

I have two elements on a page that I'd like to animate via CSS (specifically, -webkit-animation). The animation itself simply bounces an element up and down. One element is always shown and bouncing, the other is not animated until mouse-over (hover).
My question is: Is it possible to sync (have both elements reach their apex at the same time, etc) the animation across both elements regardless of when the 2nd element's animation is started?
Here's my HTML:
<div id="bouncy01">Drip</div>
<div id="bouncy02">droP</div>
and my CSS:
#-webkit-keyframes bounce {
0% {-webkit-transform: translateY(0px);}
25% {-webkit-transform: translateY(-2px);}
50% {-webkit-transform: translateY(-4px);}
75% {-webkit-transform: translateY(-2px);}
100% {-webkit-transform: translateY(0px);}
}
#bouncy01,
#bouncy02 {
margin:10px;
float: left;
background: #ff0000;
padding: 10px;
border: 1px solid #777;
}
#bouncy01 {
-webkit-animation:bounce 0.25s ease-in-out infinite alternate;
}
#bouncy02 {
background: #ffff00;
}
#bouncy02:hover {
-webkit-animation:bounce 0.25s ease-in-out infinite alternate;
}
and finally, a working demo of the issue: http://jsfiddle.net/7ZLmq/2/
(to see the problem, mouse-over the yellow block)
I don't think its possible natively, but you can actually hack similar functionality by using a bouncing wrapper and some position altering
html:
<div id="bouncywrap">
<div id="bouncy01">Drip</div>
<div id="bouncy02">droP</div>
<div>
CSS:
#-webkit-keyframes bounce {
0% { padding-top:1px;}
/* using padding as it does not affect position:relative of sublinks
* using 0 instead of 0 b/c of a box-model issue,
* on kids wiht margin, but parents without margin, just try out
*/
50% { padding-top:5px;} /*desired value +1*/
100% { padding-top:1px;}
}
#bouncy01,
#bouncy02 {
margin:10px;
background: #ff0000;
padding: 10px;
border: 1px solid #777;
width:30px;
position:absolute;
}
#bouncywrap {
-webkit-animation:bounce 0.125s ease-in-out infinite;
position:relative;
width:140px;
height:50px;
/* background:grey; /*debug*/
}
#bouncy02 {
background: #ffff00;
left:60px;
top:2px; /*half of desired value, just a fix for the optic*/
}
#bouncy02:hover {
position:relative; /*here happens the magic*/
top:0px;
}
demo http://jsfiddle.net/A92pU/1/
The Web Animations API now allows to control animations very precisely and quite easily.
There are various ways to declare a Web Animation, but since here we started with CSS, here is how to hook to it:
// when the animation starts
document.querySelector("#bouncy02")
.addEventListener("animationstart", (evt) => {
// double check it the animation we wanted
if (evt.animationName === "bounce") {
// retrieve both Animation objects
const myAnim = findAnimByName(evt.target, "bounce");
const otherAnim = findAnimByName(document.querySelector("#bouncy01"), "bounce");
// update mine to act as if it started
// at the same time as the first one
myAnim.startTime = otherAnim.startTime;
}
});
// simple helper to find an Animation by animationName
function findAnimByName(elem, name) {
// get all the active animations on this element
const anims = elem.getAnimations();
// return the first one with the expected animationName
return anims.find((anim) => anim.animationName === name);
}
#keyframes bounce {
0% {transform: translateY(0px);}
25% {transform: translateY(-2px);}
50% {transform: translateY(-4px);}
75% {transform: translateY(-2px);}
100% {transform: translateY(0px);}
}
#bouncy01,
#bouncy02 {
margin:10px;
float: left;
background: #ff0000;
padding: 10px;
border: 1px solid #777;
}
#bouncy01 {
animation:bounce 0.25s ease-in-out infinite alternate;
}
#bouncy02 {
background: #ffff00;
}
#bouncy02:hover {
animation:bounce 0.25s ease-in-out infinite alternate;
}
<div id="bouncy01">Drip</div>
<div id="bouncy02">droP</div>
Note that while it's surprisingly not yet that propular, this API actually exists for some times now, and its browser support (all except IE) is quite good.
I was looking for an alternative solution to those proposed here because:
I am animating a background color - which can't use the positioning magic in the accepted answer.
I wanted to avoid calculations for a very simple animation in my app.
After further research I came across this module by bealearts.
It exposes a very neat API that lets you keep an animation in sync across the app by referring to it's name:
import sync from 'css-animation-sync';
sync('spinner');
Since this seemed a little too good to be true, I tested the library (which is a single short file) in this fiddle and am happy to report it works (hover on the third image and see that I quickly syncs to the second image's animation) :).
Credit: I used the animation from this fiddle by Simurai as a basis for my fiddle.
If anyone wants to replicate the mechanism behind this synchronisation, the code is open, but in it's essence, it uses events listeners for the animation itself as sync points:
window.addEventListener('animationstart', animationStart, true);
window.addEventListener('animationiteration', animationIteration, true);
Hope this helps the next person looking for a solution to this problem.
Calculate and add a delay before adding the class:
function getTime (seconds) {
const msDuration = (seconds * 1000).toFixed(0);
const currentTime = (new Date()).getTime();
const msDelay = msDuration - (currentTime % msDuration);
return (msDelay / 1000).toFixed(4);
}
$('div').css({animationDelay: getTime(0.6) + "s"}).addClass('animating');
https://codepen.io/s-flhti/pen/GRoVXZw
Looks like you can just stack two of the yellow ones and switch visibility on :hover through a parent element.
You need the animation to always be running otherwise you'll run into the sync issue you've got.
I modified your code a bit to get this.
You could use a setInterval to maintain the animation state of the first animation and give the other animation a negative delay to seek to its matching keyframe on mouse-over.
Read about the state-maintaining-interval-thing here, at the "Manipulating CSS Animations" section; read about the negative delay to seek here.
This was my little quest in synchronizing animations for different elements and pseudo-elements, thanks to the ideas above, the solution turned out to be very simple. I hope this small code helps someone.
window.addEventListener('animationstart', e =>
e.animationName == 'rgb' && e.target.getAnimations({subtree: true}).forEach(e => e.startTime = 0), true)
on mouse hover:
remove animation classes from both elements
use requestAnimationFrame(() => { ... add here "bounce" class to both elements })
Should sync nicely.
You could set a class on the root element which set the altertating state, and then alternate the class using a timer
CSS
.alt .bouncy {
padding-top:5px !important;
}
.bouncy {
padding-top: 1px;
transition: padding-top ease 500ms;
}
HTML
<div class="container">
<div class="bouncy">Drip</div>
<div class="bouncy">droP</div>
<div>
Javascript
$(function () {
setInterval(() => $(".container").toggleClass("alt"), 1000)
})
In this way transition and timer do the work of css animation, but controlled by a single master switch (the container).
With css animation sync lib by bealearts, you can easly synchonize animations. But in version 0.4.1 (lastest today), it had the bugs:
Sync gets lost, when all the sync-animation elements are stopped they animation (for example display:none )
First animation starts from non-zero frame after restarting, that may be critical.
Some time after the start of the first animation, it flashes.
Not working with pseudo-elements :before, :after
To fix all theese bugs (but 4) , you can fix library code:
-Add animation-cancel callback
function animationCancel(event) {
if (shouldSync(event)) {
elements.delete(event.target);
}
}
window.addEventListener('animationcancel', animationCancel, true);
-Modify animation-start callback to process first animation
function animationStart(event) {
if (shouldSync(event)) {
const { target: element, timeStamp } = event;
elements.add(element);
let diff;
if (elements.size == 1){
diff = 0;
lastIterationTimestamp = timeStamp;
}else diff = timeStamp - lastIterationTimestamp;
element.style.setProperty('animation-delay', `-${diff}ms`);
}
}
-And empty the body of init() method.
Here is the fixed using sample:
//Sample demo code
jQuery(function($){
window.cssAnimationSync('pulse-visible');
let animateGroup = function(selector){
let hideNext = function(){
let next = $(selector + ':visible:first');
if (next.length){
next.fadeOut();
setTimeout(hideNext, 200 + Math.random()*200);
}else setTimeout(showNext, 200 + Math.random()*200);
}
let showNext = function(){
let next = $(selector + ':hidden:first');
if (next.length){
next.fadeIn();
setTimeout(showNext, 200 + Math.random()*200);
}else setTimeout(hideNext, 200 + Math.random()*200);
};
showNext();
};
animateGroup('.pulsar_sync');
animateGroup('.pulsar');
});
//Fixed library code
/** #see https://github.com/bealearts/css-animation-sync */
window.cssAnimationSync = function(animationNameOrNames) {
const animationNames = new Set(
Array.isArray(animationNameOrNames) ? animationNameOrNames : [animationNameOrNames]
);
const elements = new Set();
let animationDuration;
let isPaused = false;
let lastIterationTimestamp = 0;
const api = {
getElements() {
return elements;
},
free() {
window.removeEventListener('animationiteration', animationIteration, true);
window.removeEventListener('animationstart', animationStart, true);
this.start();
elements.clear();
},
start() {
elements.forEach((el) => {
if (validate(el)) {
if (isPaused) {
el.style.removeProperty('animation-play-state');
} else {
el.style.removeProperty('animation');
}
}
});
isPaused = false;
},
stop() {
isPaused = false;
elements.forEach((el) => {
if (validate(el)) {
el.style.setProperty('animation', 'none');
}
});
},
pause() {
isPaused = true;
elements.forEach((el) => {
if (validate(el)) {
el.style.setProperty('animation-play-state', 'paused');
}
});
}
};
function shouldSync(event) {
return animationNames.has(event.animationName);
}
function validate(el) {
const isValid = document.body.contains(el);
if (!isValid) {
elements.delete(el);
}
return isValid;
}
function init() {
//setTimeout(restart, animationDuration);
}
function restart() {
api.stop();
setTimeout(api.start, 50);
}
function animationStart(event) {
if (shouldSync(event)) {
const { target: element, timeStamp } = event;
elements.add(element);
let diff;
if (elements.size == 1){
diff = 0;
lastIterationTimestamp = timeStamp;
}else diff = timeStamp - lastIterationTimestamp;
element.style.setProperty('animation-delay', `-${diff}ms`);
}
}
function cssToMs(time) {
const num = parseFloat(time);
let unit = time.match(/m?s/);
if (!unit) return 0;
[unit] = unit;
switch (unit) {
case 's':
return num * 1000;
case 'ms':
return num;
default:
return 0;
}
}
function animationIteration(event) {
if (shouldSync(event)) {
const { target: element, timeStamp } = event;
elements.add(element);
lastIterationTimestamp = timeStamp;
if (!animationDuration) {
animationDuration = cssToMs(window.getComputedStyle(element).animationDuration);
init();
}
}
}
function animationCancel(event) {
if (shouldSync(event)) {
elements.delete(event.target);
}
}
window.addEventListener('animationiteration', animationIteration, true);
window.addEventListener('animationstart', animationStart, true);
window.addEventListener('animationcancel', animationCancel, true);
return api;
};
#keyframes pulse-visible {
0% { opacity: 0.85;}
30% { opacity: 0.85;}
40% { opacity: 0.55;}
45% { opacity: 0;}
85% { opacity: 0;}
90% { opacity: 0.55;}
100% { opacity: 0.85;}
}
#keyframes pulse-visible-copy {
0% { opacity: 0.85;}
30% { opacity: 0.85;}
40% { opacity: 0.55;}
45% { opacity: 0;}
85% { opacity: 0;}
90% { opacity: 0.55;}
100% { opacity: 0.85;}
}
.pulsar
{
animation-name: pulse-visible-copy;
}
.pulsar_sync
{
animation-name: pulse-visible;
}
.pulsar, .pulsar_sync
{
animation-duration: 0.7s;
animation-iteration-count: infinite;
animation-timing-function: linear;
/*styles not depending on animation*/
display: inline-block;
width: 30px;
height: 30px;
margin: 5px;
border: 3px solid red;
border-radius: 25%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
With cssAnimationSync
<div style='min-height:50px;'>
<div class="pulsar_sync" style="display: none;"></div>
<div class="pulsar_sync" style="display: none;"></div>
<div class="pulsar_sync" style="display: none;"></div>
<div class="pulsar_sync" style="display: none;"></div>
<div class="pulsar_sync" style="display: none;"></div>
</div>
Without
<div>
<div class="pulsar" style="display: none;"></div>
<div class="pulsar" style="display: none;"></div>
<div class="pulsar" style="display: none;"></div>
<div class="pulsar" style="display: none;"></div>
<div class="pulsar" style="display: none;"></div>
</div>

Resources