Why is this animation incorrect? - css

I'm trying to implement a table (based on CSS-grid at the moment) with a row that is hidden. Upon clicking somewhere, that row should smoothly expand. I want contents of this row to be visible and preserve their correct size during the animation.
This is very similar to this demo, but I'm not implementing a menu. But the fact that in that demo, contents ("Menu item 1", "Menu item 2", ...) have constant size during animation is something I want.
I want to implement this using a FLIP technique as described here to achieve high framerate with ease.
I've added scaleY(0.01) on the row, and scaleY(100) on the inner wrapper, in a hope they would cancel out and thus maintain scale. I also added transform-origin: 0 0 hoping the top edge of the animated row will stay in the same place during the animation.
However, what happens is that the contents are initially way too tall. They also appear to be moving along the Y axis, even when I set transform-origin: 0 0 (but that might be just the effect of the incorrect height).
I tried using Element.animate() as well as manual element.style.transform = ... approaches, to no avail.
My question: why the hidden row isn't animated properly (the height of its contents isn't constant and they appear to move along Y axis)? How to fix this?
let collapsed = Array.from(document.querySelectorAll(".collapsed"));
collapsed.forEach((row, idx) => {
row.dataset["collapsedIdx"] = idx;
document.querySelector("label").addEventListener("click", () => {
let collapsedSelf = row.getBoundingClientRect();
let rowsBelow = Array.from(
row.parentElement.querySelectorAll(`[data-collapsed-idx='${idx}'] ~ *`)
);
row.classList.remove("collapsed");
let expandedSelf = row.getBoundingClientRect();
let diffY = expandedSelf.height - collapsedSelf.height;
let animationTiming = {
duration: 2000,
easing: "linear"
};
let wrapper = row.querySelector(":scope > *");
row.animate(
[{ transform: `scaleY(0.01)` }, { transform: `scaleY(1)` }],
animationTiming
);
wrapper.animate(
[{ transform: `scaleY(100) ` },
{ transform: `scaleY(1) ` }],
animationTiming
);
rowsBelow.forEach((rowBelow) => {
rowBelow.animate([
{ transform: `translateY(-${diffY}px)` },
{ transform: `translateY(0)` }
], animationTiming);
});
});
});
* {
box-sizing: border-box;
}
section {
display: grid;
grid-template-columns: max-content 1fr;
}
.subsection {
grid-column: span 2;
}
label, p {
display: block;
margin: 0;
}
.collapsible,
.collapsible > * {
transform-origin: 0 0;
will-change: transform;
contain: content;
}
.collapsed {
height: 0;
overflow: hidden;
}
/* .collapsed {
transform: scaleY(.2);
}
.collapsed > * {
transform: scaleY(5)
}
*:nth-child(8),
*:nth-child(9),
*:nth-child(10),
*:nth-child(11),
*:nth-child(12) { transform: translateY(-35px); }
*/
/* .animate-on-transforms {
transition: transform 2000ms linear;
} */
<section>
<label><strong>CLICK ME!!!</strong></label>
<p>Value 1</p>
<label>Row 2</label>
<p>Value 2</p>
<label>Row 3</label>
<p>Value 3</p>
<div class="subsection collapsible collapsed">
<section>
<label>Subrow 1</label>
<p>Subvalue 1</p>
<label>Subrow 2</label>
<p>Subvalue 2</p>
<label>Subrow 3</label>
<p>Subvalue 3</p>
<label>Subrow 4</label>
<p>Subvalue 4</p>
</section>
</div>
<label>Row 4</label>
<p>Value 4</p>
<label>Row 5</label>
<p>Value 5</p>
</section>

I think the issue here is the interpolation. You are assuming that at each step of the animation we will always have scale(x) and scale(1/x) where x within [0,1] but due to rounding I don't think you will have this.
Here is a basic example:
.box,
p{
transform-origin:0 0;
transition:1s linear;
}
.box:hover {
transform:scale(0.1);
}
.box:hover p{
transform:scale(10);
}
<div class="box">
<p>
Lorem Ipsume<br>
Lorem Ipsume<br>
Lorem Ipsume<br>
Lorem Ipsume<br>
Lorem Ipsume<br>
</p>
</div>
Logically we may think that the text will stay the same but no. It will grow then shrink again. So yes the scales will get cancelled but not along the animation.

Related

How to make this "scroll tape bar" with CSS

I would like to ask if you can show me how to make the "scroll tape bar" (I don't know the correct term for this object) that is in this store: https://thefutureofficial.eu/
"scroll tape bar here"
Thanks a lot guys.
Simone.
Use following Snippet to achieve this effect. You can style the text as per your choice.
.main {
background:#000;
padding:0.5rem;
overflow: hidden;
}
.text {
color:white;
animation : slide 10s linear infinite;
}
.gap{ margin-right:100px;}
#keyframes slide {
0% {
transform: translateX(100%)
}
100% {
transform: translateX(-100%)
}
}
<div class="main">
<div class="text">
<span class="gap">Text</span>
<span class="gap">Text</span>
<span class="gap">Text</span>
<span class="gap">Text</span>
</div>
</div>

Animating div (enter and exit) in React

I want to build a simple "carousel" in React. I have a list of questions that I want the user to answer. When you click on next, it shows the next question. I want to also add a previous button in the future. Currently the animation for the item being revealed works.
However on mobile the screen jumps up when its animating from one div to another (with a slight delay)
The height of the parent div is always the same, so why would it jump?
JSX
{ this.state.activeIndex === 0 &&
<div className="surveyContainer--surveyList__animate">
<div>
<SelectField labels={data.meat.labels} value={this.props.survey.meat}/>
</div>
<div>
<Button handleClick={() => this.handleActiveIndex(2)} label="Next"/>
</div>
</div>
}
{this.state.activeIndex === 1 &&
<div className="surveyContainer--surveyList__animate">
<div>
<SelectField labels={data.energy.labels} value={this.props.survey.energy}/>
</div>
<div>
<Button handleClick={() => this.handleActiveIndex(2)} label="Next"/>
</div>
</div>
}
<SelectField/> and Button are both custom components.
CSS
.surveyContainer--surveyList__animate {
animation: slide-in 0.4s ease;
}
#keyframes slide-in {
0% {
opacity: 0;
transform: translateX(200px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
How do I fix the jumping? Also what might be a better approach to do this entire thing? If I want to add a previous button, then switching the animation will be a painstaking feature.
Edit:
For the jumping issue, try setting the parent's position to relative, then, for the child container:
.child {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
I recommend ReactTransitionGroup:
https://reactcommunity.org/react-transition-group/css-transition
Your code would look something like this:
import { CSSTranstion } from 'react-transition-group'
<CSSTranstion in={this.state.activeIndex === 0} timeout={200} classNames="survey-list" unmountOnExit>
<div className="survey-list">
<div>
<SelectField labels={data.meat.labels} value={this.props.survey.meat}/>
</div>
<div>
<Button handleClick={() => this.handleActiveIndex(2)} label="Next"/>
</div>
</div>
</CSSTransition>
<CSSTranstion in={this.state.activeIndex === 1} timeout={200} classNames="survey-list" unmountOnExit>
<div className="survey-list">
<div>
<SelectField labels={data.energy.labels} value={this.props.survey.energy}/>
</div>
<div>
<Button handleClick={() => this.handleActiveIndex(2)} label="Next"/>
</div>
</div>
</CSSTransition>
Then your CSS:
.survey-list {
opacity: 1;
transform: translateX(0);
}
.survey-list.survey-list-enter {
opacity: 0;
transform: translateX(200px);
}
.survey-list.survey-list-enter-active {
opacity: 1;
transform: translateX(0);
transition: opacity 200ms;
}
.survey-list.survey-list-exit {
opacity: 1;
}
.survey-list.survey-list-exit-active {
opacity: 0;
transform: translateX(-200px);
transition: opacity 200ms;
}
And I'm not sure entirely the breadth of your intention, but you may also want to look into TransitionGroup, since it will automate adding classes to an array of children as they're toggled in and out:
https://reactcommunity.org/react-transition-group/transition-group
As a sidenote, if you're using BEM for your CSS, you shouldn't (i.e. surveyContainer--survey-list) be using a modifier as a block. Just allow survey-list to be its own block if it has its own elements. It prevents long confusing classes.

CSS animate remove from DOM

I have the following HTML structure:
<div class="products-container">
{foreach from=$cart.products item=product}
<div class="product" data-id-product="{$product.id_product}" data-id-product-attribute="{$product.id_product_attribute}">
...
</div>
</div>
Now I have a javascript that can remove any div .product.
Is there a way to fade the deleted div out of the DOM to the right and animate the other divs 'moving up to the free space'?
A simple example
let btt = document.querySelector('button');
let products_cnt = document.querySelector('.products');
let products = document.querySelectorAll('.product');
products[0].addEventListener('transitionend', function() {
[...products].forEach((p) => p.parentNode.removeChild(p))
});
btt.addEventListener('click', function() {
this.setAttribute('disabled', 'disabled');
products_cnt.classList.add('products--delete');
})
div {
border: 1px #9bc solid;
height: 60px;
width: 200px;
margin: 10px;
color: #69A;
font: 1em system-ui;
}
button {
margin-bottom: 3em;
cursor: pointer; }
.products {
overflow-x: hidden; }
.product {
font-weight: bold;
transition: transform 1.5s 0s, opacity 1.25s 0s;
transform: translateX(0);
opacity: 1;
}
.products--delete .product {
transform: translateX(100vw);
opacity: 0;
}
<button type="button">Remove product/s</button>
<section class="products">
<div>Not a product</div>
<div class="product">Product</div>
<div class="product">Product</div>
<div>Not a product</div>
<div class="product">Product</div>
<div>Not a product</div>
</section>
Explanation: when you click the button the class .products--delete is added to the .products_container element: this starts a CSS transition over the .product elements.
Finally, when the transitionend event occurs on a single product element just remove from the DOM all products.
You can use css transitions in your CSS. The part of removing may be different for you. Please click the product to remove it.
let productsRy = Array.from(document.querySelectorAll(".product"));
productsRy.forEach((p,i)=>{
p.style.top = i * (3 + 1) * 16 +"px";
p.addEventListener("click",()=>{
container.removeChild(p);
productsRy = Array.from(document.querySelectorAll(".product"));
productsRy.forEach((p1,i1)=>{p1.style.top = i1 * (3 + 1) * 16 +"px";})
})
})
.products-container{position:relative;}
.product{
position:absolute;
padding:1em;
margin:.5em;
height:3em;
outline:1px solid; width:200px;
height:auto;
transition: all 1s;
}
}
<div class="products-container" id="container">
<div class="product" data-id-product="a" data-id-product-attribute="a">
product a
</div>
<div class="product" data-id-product="b" data-id-product-attribute="b">
product b
</div>
<div class="product" data-id-product="c" data-id-product-attribute="c">
product c
</div>
</div>
CSS really doesn't have the ability to modify an object in the same manner as JavaScript. you can do this easily.
$(".product").fadeTo("slow", 0.00, function(){
$(this).slideUp("slow", function() {
$(this).remove();
});
});

div only renders after css transform is complete, on iphone (safari)

On iphone safari browser a div element containing text isn't rendered while it is sliding onto screen.
However, remove the text inside the div and it works!
Bug occurs on iphone 5, so IOS Safari 10. (I tested safari 11 and it works correctly)
Here is a jsFiddle which might explain better. https://jsfiddle.net/pip36/wtkodwr6/
let button = document.getElementById('move-button');
let block = document.getElementById('red-block');
let hasMoved = false;
button.addEventListener('click', () => {
if (hasMoved) {
setTransform(0);
hasMoved = false;
} else {
setTransform(-300);
hasMoved = true;
}
})
function setTransform(pixels) {
block.style.transform = 'translateX(' + pixels + 'px)';
}
.container {
perspective: 300px;
}
.parent {
transform-style: preserve-3d;
}
.block {
width: 200px;
height: 200px;
background: red;
transition: transform 1s;
}
button {
width: 200px;
height: 50px;
}
<div class="container">
<div class="parent">
<div id="red-block" class="block">
<!-- remove text and it works -->
HELLO
</div>
</div>
</div>
<button id="move-button"> MOVE </button>
I would appreciate any help and possible workarounds.
Many Thanks!
I'm not sure if anyone will ever replicate the problem, but I'll post my solution in case it helps anyone with a similar issue.
//Forces redraw of element
image.style.opacity = 0.99
setTimeout(() => {
image.style.opacity = 1
}, 250)
Toggling the opacity just after the element slides into the screen forces a redraw, and the opacity switch itself is not noticeable. Ugly, but works.

CSS3 animate or transition elements into position after removal of an element above

What is the best way of achieving this without any visible flickering and any wierdness?
The fiddle to start out: http://jsfiddle.net/35qec14b/2/
$('.element').on('click', function(e){
this.remove();
});
.element {
position:relative;
width: 200px;
margin:5px;
padding:20px;
cursor:pointer;
background: rgb(150,200,250);
transition:1s linear;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
(click to remove)
<div class="element">Element 1</div>
<div class="element">Element 2<br>Second line</div>
<div class="element">Element 3</div>
<div class="element">Element 4<br>Second line</div>
<div class="element">Element 5</div>
Note: the removed element must disappear instantly in this case, as it would appear in another location and we don't want it to be visible in two places simultaneously.
Ideas so far:
transform:translateY for ALL elements below the removed one (probably performance intensive for large lists)
Animate/transform margin of the first element below, from removed element's height to 0 (leveraging chained animations? step-start?)
Replace the removed element with a transparent placeholder and animate it's own height to 0
The best that comes to mind is to hide it, clone it for its new location (not showed here), and then animate its height
When one animate both margins, paddings and height, it becomes not so smooth, so I added an extra inner wrapper for the content so the animation only animates the height
$('.element').on('click', function(e) {
this.style.height = $(this).height()+ 'px';
this.classList.add('hide-me');
(function(el) {
setTimeout(function() {
el.remove();
}, 500);
})(this);
});
.element {
position: relative;
width: 200px;
overflow: hidden;
}
.element > div {
margin: 5px;
padding: 20px;
background: rgb(150, 200, 250);
}
.element.hide-me {
animation: hideme .5s forwards;
opacity: 0;
}
#keyframes hideme {
100% {
height: 0;
}
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
(click to remove)
<div class="element">
<div>
Element 1
</div>
</div>
<div class="element">
<div>
Element 2
<br>Second line
</div>
</div>
<div class="element">
<div>
Element 3
</div>
</div>
<div class="element">
<div>
Element 4
<br>Second line
</div>
</div>
<div class="element">
<div>
Element 5
</div>
</div>
Here's the jQuery approach using .animate()
$('.element').on('click', function(e){
var $this = $(this), $next = $this.next();
$next.css({
marginTop: $this.outerHeight(true)
}).animate({
marginTop: 5
}, 200);
$this.remove();
});
.element {
position:relative;
width: 200px;
margin:5px;
padding:20px;
cursor:pointer;
background: rgb(150,200,250);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
(click to remove)
<div class="element">Element 1</div>
<div class="element">Element 2<br>Second line</div>
<div class="element">Element 3</div>
<div class="element">Element 4<br>Second line</div>
<div class="element">Element 5</div>
And here is a CSS transition approach
$('.element').on('click', function(e){
var $this = $(this), $next = $this.next();
$next.css({
marginTop: $this.outerHeight(true)
});
setTimeout(()=>{
$next.addClass('hide');
setTimeout(()=>{
$next.css({marginTop: ''}).removeClass('hide');
}, 250)
}, 20);
$this.remove();
});
.element {
position:relative;
width: 200px;
margin:5px;
padding:20px;
cursor:pointer;
background: rgb(150,200,250);
}
.element.hide {
transition: margin-top 0.25s linear;
margin-top: 5px !important;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
(click to remove)
<div class="element">Element 1</div>
<div class="element">Element 2<br>Second line</div>
<div class="element">Element 3</div>
<div class="element">Element 4<br>Second line</div>
<div class="element">Element 5</div>
Even though you are concerned that using transforms could have a negative performance, I think that the opposite is true.
Bear in mind that other solutions involve massive reflows, that probably are more performance intensive on the CPU (transforms most probably are handled by the GPU).
However, solving this with this with transforms is a little bit hard to code. Specially changing the amount of pixels that need to be moved, and injecting them into the style.
See a posible solution. I have used just JS to make it more portable.
If you are concern about performance, the result of findKeyframesRule could be assigned to a variable and reused.
document.addEventListener('DOMContentLoaded', setEvent, false);
function setEvent() {
var elements = document.getElementsByClassName('element');
for (var n = 0; n < elements.length; n++) {
elements[n].addEventListener('click', remove, false);
}
}
function remove(event) {
var current = event.currentTarget;
var elements = document.getElementsByClassName('move');
for (var n = 0; n < elements.length; n++) {
elements[n].classList.remove('move');
}
window.setTimeout(function() {
remove2(current);
}, 0);
}
function remove2(current) {
var next = current.nextElementSibling;
if (!next) {
return;
}
var top1 = current.offsetTop;
var top2 = next.offsetTop;
var diff = top2 - top1;
var newTransform = 'translateY(' + diff + 'px)';
var rule = findKeyframesRule('move');
var style = rule.cssRules[0].style;
style.transform = newTransform;
next.classList.add('move');
current.style.height = '0px';
}
function findKeyframesRule(rule) {
// gather all stylesheets into an array
var ss = document.styleSheets;
// loop through the stylesheets
for (var i = 0; i < ss.length; i++) {
var ss1 = ss[i];
// loop through all the rules
if (!ss1.cssRules) {
alert('you are using Chrome in local files');
return null;
}
for (var j = 0; j < ss1.cssRules.length; j++) {
// find the keyframe rule whose name matches our passed parameter
if (ss1.cssRules[j].type == window.CSSRule.KEYFRAMES_RULE && ss1.cssRules[j].name == rule)
return ss1.cssRules[j];
}
}
return null;
}
.element {
position: relative;
width: 200px;
overflow: hidden;
}
.element>div {
margin: 5px;
padding: 20px;
background: rgb(150, 200, 250);
}
.move,
.move~.element {
animation: move 2s;
}
#keyframes move {
from {
transform: translateY(60px);
}
to {
transform: translateY( 0px);
}
}
(click to remove)
<div class="element">
<div>
Element 1
</div>
</div>
<div class="element">
<div>
Element 2
<br>Second line
</div>
</div>
<div class="element">
<div>
Element 3
</div>
</div>
<div class="element">
<div>
Element 4
<br>Second line
</div>
</div>
<div class="element">
<div>
Element 5
</div>
</div>

Resources