Bounce animation on a scaled object - javascript

What is the best way to have an object scale and then perform a bounce animation at that scale factor before going back to the original scale factor. I realize I could do something like scaling it to 2.2, then 1.8, then 2.0, but I'm looking for a way where you just have to perform the bounce animation on the scale factor because my scale factor will change.
Here is my example. Basically I want to combine the two to work like I said but as you can see the bounce animation performs based off the image size prior to scaling.
I need all sides of the image to bounce equally, like the example.
function myFunction() {
var image = document.getElementById('test');
image.style.WebkitTransform = ('scale(2,2)');
image.classList.remove('bounce');
image.offsetWidth = image.offsetWidth;
image.classList.add('bounce') ;
}
div#test {
position:relative;
display: block;
width: 50px;
height: 50px;
background-color: blue;
margin: 50px auto;
transition-duration: 1s;
}
.bounce {
animation: bounce 450ms;
animation-timing-function: linear;
}
#keyframes bounce{
25%{transform: scale(1.15);}
50%{transform: scale(0.9);}
75%{transform: scale(1.1);}
100%{transform: scale(1.0);}
}
<div id='test'> </div>
<button class = 'butt' onclick = 'myFunction()'>FIRST</button>

You can try the use of CSS variable in order to make the scale factor dynamic within the keyframe:
function myFunction(id,s) {
var image = document.querySelectorAll('.test')[id];
image.style.setProperty("--s", s);
image.classList.remove('bounce');
image.offsetWidth = image.offsetWidth;
image.classList.add('bounce');
}
div.test {
display: inline-block;
width: 50px;
height: 50px;
background-color: blue;
margin: 50px;
transform:scale(var(--s,1));
}
.bounce {
animation: bounce 450ms;
animation-timing-function: linear;
}
#keyframes bounce {
25% {
transform: scale(calc(var(--s,1) + 0.2));
}
50% {
transform: scale(calc(var(--s,1) - 0.1));
}
75% {
transform: scale(calc(var(--s,1) + 0.1));
}
100% {
transform: scale(var(--s,1));
}
}
<div class='test'> </div>
<div class='test'> </div>
<div class='test'> </div>
<div>
<button class='butt' onclick='myFunction(0,"2")'>first</button>
<button class='butt' onclick='myFunction(1,"3")'>Second</button>
<button class='butt' onclick='myFunction(2,"0.5")'>third</button>
<button class='butt' onclick='myFunction(1,"1")'>second again</button>
</div>

Dispatch each transform on a different wrapping element.
This way you can achieve several level of transforms, all relative to their parent wrapper.
Then you just need to set your animation to fire after a delay equal to the transition's duration:
btn.onclick = e => {
// in order to have two directions, we need to be a bit verbose in here...
const test = document.querySelector('.test');
const classList = test.classList;
const state = classList.contains('zoomed-in');
// first remove previous
classList.remove('zoomed-' + (state ? 'in' : 'out'));
// force reflow
test.offsetWidth;
// set new one
classList.add('zoomed-' + (state ? 'out' : 'in'));
};
div.test {
position: relative;
display: inline-block;
margin: 50px 50px;
}
div.test div{
width: 100%;
height: 100%;
transition: transform 1s;
}
div.test.zoomed-in .scaler {
transform: scale(2);
}
div.test.zoomed-in .bouncer,
div.test.zoomed-out .bouncer {
animation: bounce .25s 1s;/* transition's duration delay */
}
div.test .inner {
background-color: blue;
width: 50px;
height: 50px;
}
#keyframes bounce {
0% {
transform: scale(1.15);
}
33% {
transform: scale(0.9);
}
66% {
transform: scale(1.1);
}
100% {
transform: scale(1.0);
}
}
<div class="test">
<div class="scaler">
<div class="bouncer">
<div class="inner"></div>
</div>
</div>
</div>
<button id="btn">toggle zoom</button>

Related

Toggling between classes keeps animation from going through, just skips to end

I'm trying to animate a hamburger menu by having the bottom and top line translate to the middle and then rotate into an X and want to reverse the animation when the X is clicked. Using jquery I'm toggling the class menu-open and menu-closed. When I remove the CSS for the menu-closed animation, it fires just fine but when I add the CSS back the animations just skip to the last frame. It forms what I want but just refuses to use the animation fully.
CSS
.navbar .mobile-menu.menu-open .line::before {
animation: menu-open-top 250ms linear forwards;
}
.navbar .mobile-menu.menu-open .line {
animation: menu-middle 250ms linear forwards;
}
.navbar .mobile-menu.menu-open .line::after {
animation: menu-open-bottom 250ms linear forwards;
}
.navbar .mobile-menu.menu-closed .line::before {
animation: menu-open-top 250ms linear reverse;
}
.navbar .mobile-menu.menu-closed .line {
animation: menu-middle 250ms linear reverse;
}
.navbar .mobile-menu.menu-closed .line::after {
animation: menu-open-bottom 250ms linear reverse;
}
Animation
#keyframes menu-open-top {
30% {
bottom: 0;
}
60% {
bottom: 0;
transform: rotate(0) translate(0);
}
100% {
transform: rotate(45deg) translate(5px, 5px);
visibility: visible;
}
}
#keyframes menu-middle {
40% {
visibility: hidden;
}
to {
visibility: hidden;
}
}
#keyframes menu-open-bottom {
30% {
top: 0;
}
60% {
top: 0;
transform: rotate(0) translate(0);
}
100% {
transform: rotate(-45deg) translate(6px, -6px);
visibility: visible;
}
}
JS
$(".mobile-menu").click(expandMenu);
function expandMenu() {
$(".primary-nav").toggleClass("menu-expand");
$(this).toggleClass("menu-open menu-closed");
}
I must be missing something or maybe I need to add new keyframes for the reverse animation but that feels like it would be unnecessary.
edit: here is the html as well
HTML
<div class="mobile-menu menu-closed">
<div class="line"></div>
</div>
Here's how to do it using simple prop value changes with careful timing. I guess it can be done using #keyframe animations as well, but I find them more difficult to follow/control/sync, at least in this case, considering it's (basically) a two-step animation.
document.querySelector('.mobile-menu').addEventListener('click', ({
target
}) => {
target.closest('.mobile-menu').classList.toggle('menu-open');
})
.mobile-menu {
--duration: 0.42s;
--size: 3rem;
--padding: 0.5rem;
--color: red;
--distance-timing: cubic-bezier(0.5, 0, 0.3, 1);
--rotation-timing: cubic-bezier(0.4, 0, 0.2, 1);
width: var(--size);
height: var(--size);
padding: var(--padding);
display: flex;
flex-direction: column;
justify-content: space-between;
cursor: pointer;
}
.mobile-menu * {
box-sizing: border-box;
}
.mobile-menu>div {
border: 1px solid var(--color);
height: 0;
width: 100%;
position: relative;
transform-origin: 50% 50%;
transition:
top calc(0.6 * var(--duration)) var(--distance-timing) calc(0.4 * var(--duration)),
bottom calc(0.6 * var(--duration)) var(--distance-timing) calc(0.4 * var(--duration)),
transform calc(0.8 * var(--duration)) var(--rotation-timing) 0s;
}
.mobile-menu> :nth-child(1) {
top: calc(var(--padding)/2);
}
.mobile-menu> :nth-child(3) {
bottom: calc(var(--padding)/2);
}
.mobile-menu.menu-open>div {
transition:
top calc(0.4 * var(--duration)) var(--distance-timing) 0s,
bottom calc(0.4 * var(--duration)) var(--distance-timing) 0s,
transform calc(0.8 * var(--duration)) var(--rotation-timing) calc(0.2 * var(--duration));
}
.mobile-menu.menu-open> :nth-child(1) {
top: calc(50% - 1px);
transform: rotate(0.125turn);
}
.mobile-menu.menu-open> :nth-child(2) {
transform: rotate(0.125turn);
}
.mobile-menu.menu-open> :nth-child(3) {
bottom: calc(50% - 1px);
transform: rotate(-0.125turn);
}
<div class="mobile-menu">
<div></div>
<div></div>
<div></div>
</div>
Same thing, in SCSS: https://jsfiddle.net/websiter/dybre2f9/
I've extracted the values into CSS vars, so it can be reused and modified with ease. Feel free to tweak it to your liking.
Note: the reason why I'm using bottom and top to animate the movement (and not translateY - which is slightly more performant) is because I wanted the two animations to be completely independent of each other, to allow me to play with various overlapping values and timing functions. What I've come up with doesn't respect the requirement 100% (as in, it slightly overlaps the rotation with the movement - but I'm doing it on purpose, as not overlapping them looks too mechanical). When overlapped, the entire animation seems more organic. It's like the button is alive and happy to have been asked to do the animation. Or maybe I'm just a bit crazy, I don't know...

Triggering two different animations onClick?

I am trying to trigger two different animations by adding and removing two different css classes with two different css animations by using javascript to do so. However, the div does not preserve the previous animated state, and I need it to do so because of how I designed the UI. I am using forwards in the animations property, but the div performs the animation when it is clicked and then goes back to previous state and then performs the other animation.
const projects = document.getElementById('projects')
projects.addEventListener('click', () => {
if(projects.classList.contains('projects-animation') == true){
projects.classList.remove('projects-animation')
projects.classList.add('projects-animation2')
}
else{
console.log('animation2')
console.log(projects.classList)
projects.classList.remove('projects-animation2')
projects.classList.add('projects-animation')
}
})
.projects-container{
position: relative;
z-index: 1;
display: flex;
align-items: center;
grid-area: projects;
padding: 20px;
font-size: 100px;
background-color: hsl(4, 7%, 45%);
cursor: pointer;
}
.projects{
position: relative;
height: fit-content;
}
.projects-animation{
animation: projects 1s ease-in-out backwards;
}
.projects-animation2{
animation: projects2 1s ease-in-out backwards;
}
#keyframes projects {
100%{
transform: translate(50%,-250%);
}
}
#keyframes projects2 {
100%{
transform: translate(-50%, 250%);
}
}
<div class="projects-container">
<div id = "projects" class="projects">Projects</div>
</div>
Please check this one,
1: You need to add 0% as starting point for the second animation,
2: I have used smaller value as larger value moving the child div out of view port, if you want to keep the values then make the parent div large or position it accordingly
const projects = document.getElementById('projects')
projects.addEventListener('click', () => {
if(projects.classList.contains('projects-animation') == true){
projects.classList.remove('projects-animation')
projects.classList.add('projects-animation2')
}
else{
console.log('animation2')
console.log(projects.classList)
projects.classList.remove('projects-animation2')
projects.classList.add('projects-animation')
}
})
.projects-container{
position: relative;
z-index: 1;
display: flex;
align-items: center;
grid-area: projects;
padding: 20px;
font-size: 100px;
background-color: hsl(4, 7%, 45%);
cursor: pointer;
}
.projects{
position: relative;
height: fit-content;
}
.projects-animation{
animation: projects 1s ease-in-out forwards;
}
.projects-animation2{
animation: projects2 1s ease-in-out forwards;
}
#keyframes projects {
100%{
transform: translate(10%,-10%);
}
}
#keyframes projects2 {
0%{
transform: translate(10%,-10%);
}
100%{
transform: translate(0%, 0%);
}
}
<div class="projects-container">
<div id = "projects" class="projects">Projects</div>
</div>

How to resolve slideout animation not being applied

so I have a simple side bar that I want to slide in and out using css animation, now the slide animation in is working the problem that I am facing is making the animation for the slideout to work.
Can I please get help on that..
Html(Sidebar)
<div class="SideBarMenu" id="SideBarMenu">
<div class="sidebar-menu-header">
<h2 class="nav-bleft-companyname">
Sofast<span class="nav-bleft-periodmark">Cart.</span>
</h2>
</div>
<hr />
<h4 class="sidebar-menuLink">MenuItem1</h4>
<h4 class="sidebar-menuLink">MenuItem2</h4>
<h4 class="sidebar-menuLink">MenuItem3</h4>
<h4 class="sidebar-menuLink">MenuItem4</h4>
<h4 class="sidebar-menuLink">MenuItem5</h4>
</div>
JS Function triggered to toggle menu
const OpenMenu = () => {
const menu = document.getElementById("SideBarMenu");
if (menu.style.display === "block") {
menu.classList.add("sidebar-closed");
menu.style.animation = "slideOut 0.4s backwards";
menu.style.display = "none";
} else {
menu.style.display = "block";
menu.style.animation = "slideIn 0.4s forwards";
menu.classList.remove("sidebar-closed");
}
};
Side Bar css
.SideBarMenu {
top: 0% !important;
z-index: 999;
position: fixed;
background-color: #333333;
height: 100vh;
color: white;
width: 20%;
transform: translateX(-350px);
padding: 2rem;
}
.sidebar-menuLink {
margin-top: 1rem;
}
#keyframes slideIn {
100% {
transform: translateX(0px);
}
}
#keyframes slideOut {
100% {
transform: translateX(-350px);
}
}
You can use css transition to achieve that. I also advise you to put all your styles in css classes, you don't need to apply the styles in javascript.
const animate = () => {
const elem = document.getElementById("my-elem");
if (!elem.classList.contains('elem-out')) {
elem.classList.add("elem-out");
} else {
elem.classList.remove("elem-out");
}
};
const btn = document.getElementById("my-btn");
btn.addEventListener("click", animate)
.elem {
width: 300px;
height: 50px;
background-color: red;
margin-bottom: 10px;
}
.elem:not(.elem-out) {
transition: transform 0.4s ease-in;
transform: translateX(0px);
}
.elem-out {
transition: transform 0.4s ease-out;
transform: translateX(-350px);
}
<div id="my-elem" class="elem"></div>
<button id="my-btn">toggle animate</button>
Do not change the display property, the transition will brake. There's no transition for propperty like display, if you want a fadeIn / fadeOut effect too, you can put a transition on opacity property

Play new css animation seamlessly

I created a simple HTML game, which disappears under the screen when I click on a moving box.
However, the animation that disappears starts at the original location, not where it was clicked.
I think 0% of the remove #keyframes should have the location of the click, but I couldn't find a way
How shall I do it?
(function () {
const charactersGroup = document.querySelectorAll('.character');
const stage = document.querySelector('.stage')
const clickHandler = (e) => {
const target = e.target;
if (target.classList.contains('character')) {
target.classList.remove(`f${target.dataset.id}`);
target.classList.add('f0');
target.classList.add('remove');
setTimeout(() => { stage.removeChild(target) }, 2000);
}
}
stage.addEventListener('click', clickHandler);
}());
.stage {
overflow: hidden;
position: relative;
background: #eeeeaa;
width: 40vw;
height: 20vw;
}
#keyframes moving {
0% {
transform: translateX(0);
}
100% {
transform: translateX(30vw);
}
}
#keyframes remove {
0% {
transform: translate(0);
}
100% {
transform: translateY(60vw);
}
}
.character {
position: absolute;
width: 50px;
height: 50px;
background-repeat: no-repeat;
background-position: 50% 50%;
background-size: contain;
animation: moving infinite alternate;
}
.remove {
animation: remove 0.2s cubic-bezier(.68,-0.55,.27,1.55) forwards;
}
.f0 {
background-color: black;
animation-duration: 2s;
}
.f1 {
left: 5%;
bottom: 5%;
animation-duration: 2s;
background-color: red;
}
<div class="stage">
<div class="character f1" data-id="1"></div>
</div>
Change the first animation to consider left instead of translate then append both of them to the element initially and you simply toggle the animation-play-state when adding the remove class
(function() {
const charactersGroup = document.querySelectorAll('.character');
const stage = document.querySelector('.stage')
const clickHandler = (e) => {
const target = e.target;
if (target.classList.contains('character')) {
target.classList.add('remove');
setTimeout(() => {
stage.removeChild(target)
}, 2000);
}
}
stage.addEventListener('click', clickHandler);
}());
.stage {
overflow: hidden;
position: relative;
background: #eeeeaa;
width: 40vw;
height: 20vw;
}
#keyframes moving {
100% {
left:calc(95% - 50px);
}
}
#keyframes remove {
50% {
transform: translateY(-30vh);
}
100% {
transform: translateY(60vw);
}
}
.character {
position: absolute;
width: 50px;
height: 50px;
background:red;
left: 5%;
bottom: 5%;
animation:
moving 2s infinite alternate,
remove 1s cubic-bezier(.68, -0.55, .27, 1.55) forwards paused;
}
.remove {
animation-play-state:paused,running;
background: black;
}
<div class="stage">
<div class="character f1" data-id="1"></div>
</div>
If your use case is to deal with a lot of such boxes and complexity, it's better to go with handling everything with pure JS but I tried to make this work with minimal changes in JS and CSS.
I have added comments to the new JS lines.
Also taken the liberty to have a separate class with name moving for animation moving so that we can remove it on click.
(function () {
const charactersGroup = document.querySelectorAll('.character');
const stage = document.querySelector('.stage')
const clickHandler = (e) => {
const target = e.target;
if (target.classList.contains('character')) {
target.classList.remove(`f${target.dataset.id}`);
target.classList.add('f0');
// remove the moving animation
target.classList.remove('moving');
// Get offsetWidth which is the half of width to substract later while calculating left for the target i.e our box.
const offsetWidth = parseInt(getComputedStyle(target).width)/2;
// e.clientX gives us the x coordinate of the mouse pointer
// target.getBoundingClientRect().left gives us left position of the bounding rectangle and acts as a good offset to get the accurate left for our box.
target.style.left = `${e.clientX -target.getBoundingClientRect().left - offsetWidth}px`;
target.classList.add('remove');
setTimeout(() => { stage.removeChild(target) }, 2000);
}
}
stage.addEventListener('click', clickHandler);
}());
.stage {
overflow: hidden;
position: relative;
background: #eeeeaa;
width: 40vw;
height: 20vw;
}
#keyframes moving {
0% {
transform: translateX(0);
}
100% {
transform: translateX(30vw);
}
}
#keyframes remove {
0% {
transform: translate(0vh);
}
100% {
transform: translateY(60vw);
}
}
.character {
position: absolute;
width: 50px;
height: 50px;
background-repeat: no-repeat;
background-position: 50% 50%;
background-size: contain;
}
.moving{
animation: moving infinite alternate;
}
.remove {
animation: remove 0.2s cubic-bezier(.68,-0.55,.27,1.55) forwards;
}
.f0 {
background-color: black;
animation-duration: 2s;
}
.f1 {
left: 5%;
bottom: 5%;
animation-duration: 2s;
background-color: red;
}
<div class="stage">
<div class="character moving f1" data-id="1"></div>
</div>

vue.js v class binding is overriding other bindings and not removing classes properly

I have a punching bag "game" where I want to add an animation class one every click event. And when the health meter is down to zero, I am replacing the bag picture with an image of a burst bag.
This works ok as long I don't try to add the animation classes. When I try to add the animations, neither functionality is working.
The link is here: https://codepen.io/damianocel/pen/mqXady
This is the HTML:
<div id="vue-app-one">
<!-- the bag images -->
<div id="bag" v-bind:class="[animationToggle ? activeClass : 'swingLeft',
'noSwing']" v-bind:class="{ burst: ended }"></div>
<!-- health meter -->
<div id="bag-health">
<div v-bind:class="[danger ? activeClass : 'dangerous', 'safe']" v-
bind:style="{ width: health + '%'}"></div>
</div>
<!-- the game buttons -->
<div id="controls">
<button v-on:click="punch" v-show="!ended">Punch it!</button>
<button v-on:click="restart">Restart game</button>
</div>
</div>
The problematic part is this:
<div id="bag" v-bind:class="[animationToggle ? activeClass : 'swingLeft',
'noSwing']" v-bind:class="{ burst: ended }"></div>
// Above I try to bind the noSwing CSS class as default and change it to swingLeft if the animationToggle property changes. However, this adds both classes when I check dev tools, and no animation is happening.Can I have 2 class bindings on 1 element like that?
// Further, I bind the ended property to the burst CSS class, this only works if I remove the animationToggle binding and all the relevant CSS.
The instance looks like this:
var one = new Vue({
el: '#vue-app-one',
data: {
health: 100, //init health bar, this works
ended: false, // init ended state, works partially
punched: false, //init punched, don't need for now
danger: false, // this works
animationToggle: false, // there is a problem with this
activeClass: "" // have to init or I get the errors in the console
},
methods: {
punch: function(){
this.health -=10; //works
this.animationToggle= true; // is set on click
if(this.health <= 0){
this.ended = true; // works partially, the background img change is not working ,though
}
if(this.health <= 20){
this.danger = true; // works
}
else{
this.danger = false;
}
setTimeout(function () {
this.animationToggle = false // I am not sure this ever works, give no error, but I am still not sure
}.bind(this),500);
},
restart: function(){
this.health =100;
this.ended = false; // works partially, no img change when health is 0, though
}
}
});
The relevant CSS:
#bag.noSwing {
width: 300px;
height: 500px;
margin: -80px auto;
background: url("https://3.imimg.com/data3/VM/TI/MY-18093/classical-heavy-
bag-250x250.png") center no-repeat;
background-size: 70%;
-webkit-animation-name: swingRight;
-webkit-animation-duration:1s;
-webkit-animation-iteration-count: 1;
-webkit-transition-timing-function: cubic-bezier(.11,.91,.91,.39);
-webkit-animation-fill-mode: forwards;
-webkit-transform-origin: center;
transform-origin: center;
}
#bag.swingLeft {
width: 300px;
height: 500px;
margin: -80px auto;
background: url("https://3.imimg.com/data3/VM/TI/MY-18093/classical-heavy-
bag-250x250.png") center no-repeat;
background-size: 70%;
-webkit-animation-name: swingLeft;
-webkit-animation-duration:1s;
-webkit-animation-iteration-count: 1;
-webkit-transform-origin: right;
transform-origin: right;
-webkit-transition-timing-function: cubic-bezier(.91,.11,.31,.69);
}
#keyframes swingLeft {
0% { -webkit-transform: rotate (0deg); transform: rotate (0deg); }
20% { -webkit-transform: rotate (-20deg); transform: rotate (-20deg); }
50% { -webkit-transform: rotate (20deg); transform: rotate (20deg); }
70% { -webkit-transform: rotate (-10deg); transform: rotate (-10deg); }
100% { -webkit-transform: rotate (0deg); transform: rotate (0deg); }
}
#keyframes swingRight {
0% { -webkit-transform: rotate (0deg); transform: rotate (0deg); }
20% { -webkit-transform: rotate (20deg); transform: rotate (20deg); }
50% { -webkit-transform: rotate (-20deg); transform: rotate (-20deg); }
70% { -webkit-transform: rotate (10deg); transform: rotate (10deg); }
100% { -webkit-transform: rotate (0deg); transform: rotate (0deg); }
}
#bag.burst {
background: url("http://i.imgur.com/oRUzTNx.jpg") center no-repeat;
background-size: 70%;
}
#bag-health {
width: 200px;
border: 2px solid #004;
margin: -80px auto 20px auto;
}
#bag-health div.safe {
height: 20px;
background: #44c466;
}
#bag-health div.dangerous {
background: #00ffff;
}
So why are the animations not applied when the "punch it" button is clicked, why does it add both the noSwing and swingLeft class? And it overrides the functionality which changes the background image to a burst bad when the health meter reaches a value of zero.
First of all you can't have 2 class bindings on the same element, nor should you need them.
You have quite a lot of logic regarding adding/removing classes, so i would suggest extracting it to a computed:
computed: {
className () {
let classes= [];
if (this.animationToggle){
classes.push(this.activeClass)
}
else{
classes.push('swingLeft')
}
return classes;
}
}
}
<div id="bag" :class="className"></div>

Categories