Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSS transform optimisation breaks animation #288

Closed
shish opened this issue Sep 14, 2022 · 13 comments · Fixed by #694
Closed

CSS transform optimisation breaks animation #288

shish opened this issue Sep 14, 2022 · 13 comments · Fixed by #694

Comments

@shish
Copy link

shish commented Sep 14, 2022

🐛 bug report

I'm animating some things falling down the screen and rotating, but my code:

        @keyframes rain {
            0% {transform: translateY(0vh) rotate(0deg);}
            100% {transform: translateY(120vh) rotate(360deg);}
        }

gets optimised into:

        @keyframes rain {
            0% {transform: matrix(1, 0, 0, 1, 0, 0);}
            100% {transform: translateY(120vh) rotate(360deg);}
        }

matrix(1, 0, 0, 1, 0, 0) is mathematically equivalent to translate(0)+rotate(0)... but when we're using keyframe animation, each of the keyframes needs to manipulate the same properties - we can animate from rotate to rotate, but can't animate from matrix to rotate...

Notably this only happens with parcel build, not parcel run

🤔 Expected Behavior

Leave transforms alone if they're being animated

😯 Current Behavior

one transform is optimised into a matrix, the other is left alone

🌍 Your Environment

Software Version(s)
Parcel 2.7.0
@mischnic mischnic transferred this issue from parcel-bundler/parcel Sep 15, 2022
@josip
Copy link

josip commented Nov 9, 2022

I'm having the same issue, my source SCSS:

@keyframes saveArticle {
  from {
    transform-origin: 50% 1400px;
    transform: translateY(0) translateX (0) rotateX(0) scale(1);
  }

  to {
    transform-origin: 50% 100%;
    transform: translateY(-600px) translateX(50%) rotateX(-30deg) scale(0);
  }
}

gets converted to this (only in production builds):

@keyframes saveArticle {
    0% {
        transform-origin: 50% 1400px;
        transform: matrix(1, 0, 0, 1, 0, 0)
    }

    to {
        transform-origin: 50% 100%;
        transform: translateY(-600px)translate(50%)rotateX(-30deg)scale(0)
    }
}

which breaks the animation. Still looking for a solution/workaround.

@shish
Copy link
Author

shish commented Nov 10, 2022

FWIW my workaround was to avoid starting the animation with values at 0 -- since my animation loops I went from eg -180deg to +180deg instead of 0deg to 360deg. It's still spinning in a circle so it looks the same, but the minimiser doesn't break it :)

(Aside - was reading more about lightningcss and lol'ed at this from the README - "Note that some [other tools] perform unsafe optimizations that may change the behavior of the original CSS in favor of smaller file size. Lightning CSS does not do this – the output CSS should always behave identically to the input. Keep this in mind when comparing file sizes between tools.")

@aminomancer
Copy link

aminomancer commented Apr 5, 2023

Did either of you find a generic workaround for this? Here's the nightmare I'm dealing with...

@keyframes miniLoader-1 {
  0% {
    transform: translate3d(0, 0, 0) scale(0);
    opacity: 1;
  }
  to {
    transform: translate3d(0, 0, 0) scale(1.5);
    opacity: 0;
  }
}

into

@keyframes g5nwzq_miniLoader-1 {
  0% {
    opacity: 1;
    transform: matrix(0, 0, 0, 0, 0, 0);
  }
  to {
    opacity: 0;
    transform: scale(1.5);
  }
}

As far as I can tell, I don't really have the option to use degrees for this.

Edit: I can actually just use scale instead of transform for this, since I no longer need the translate3d. Still would be nice to be able to ignore a block or something, since we didn't have this problem with parcel v1.

@jalada
Copy link

jalada commented Aug 18, 2023

We lost a good chunk of time to this on a project this week as it was a nasty cause to pin down. Is there a suggested approach that someone could work on a PR for? Would it be OK to stop minifying all transforms down to matrix() or does it just need to get smarter and detect when it's being used in a keyframe animation?

(we did a scale(0.01) trick to fix this).

@oscarotero
Copy link

I have the same problem.
Is it possible to disable this behavior with a configuration option?
Or ideally prevent the optimization if the property is inside a keyframes at-rule.

@DanielJoyce
Copy link

In keyframes.rs:

impl<'i> KeyframesRule<'i> {
  pub(crate) fn minify(&mut self, context: &mut MinifyContext<'_, 'i>) {
    context.handler_context.context = DeclarationContext::Keyframes;

    for keyframe in &mut self.keyframes {
      keyframe
        .declarations
        .minify(context.handler, context.important_handler, &mut context.handler_context)
    }

    context.handler_context.context = DeclarationContext::None;
  }

I think we can just cut out the call to minify here? I know this won't minimize 'from' to '0%' anymore, but should keep it from breaking. I don't think there is any safe way to determine which properties that are being animated can be collapsed / simplified.

@DanielJoyce
Copy link

Hmm, I think on the other end, we can look at the context in Transform and not minify if the context is DeclarationContext::KeyFrames?

@DanielJoyce
Copy link

In general, I also think some kind of ignore facility, whether global, or comment based would be good too.

@DanielJoyce
Copy link

DanielJoyce commented Jan 19, 2024

The actual problem begins at line 60 of transform.rs, which will ignore context and always collapse to a transform if minify is true.

@DanielJoyce
Copy link

DanielJoyce commented Jan 19, 2024

Minification should be moved to 1688 of transform.rs instead of being done in TransformList toCss. I think.

In there we can look at the context and check if it is a KeyFrame context and not minimize, or otherwise minimize the transformlist.

@DanielJoyce
Copy link

I have a draft PR #665 which is mostly to get the discussion going.

This disables minification when emitting declarations of keyframes. It's a hack, but it's to see if this fixes the issue, and discuss what the real solution should be.

@DanielJoyce
Copy link

Anoither option is to just not minify TransformList for now. Or only minify white space, and do no other conversion.

@devongovett
Copy link
Member

I guess we should just disable the part of the code that converts transforms to matrices and back again:

if dest.minify {
// Combine transforms into a single matrix.
if let Some(matrix) = self.to_matrix() {
// Generate based on the original transforms.
let mut base = String::new();
self.to_css_base(&mut Printer::new(
&mut base,
PrinterOptions {
minify: true,
..PrinterOptions::default()
},
))?;
// Decompose the matrix into transform functions if possible.
// If the resulting length is shorter than the original, use it.
if let Some(d) = matrix.decompose() {
let mut decomposed = String::new();
d.to_css_base(&mut Printer::new(
&mut decomposed,
PrinterOptions {
minify: true,
..PrinterOptions::default()
},
))?;
if decomposed.len() < base.len() {
base = decomposed;
}
}
// Also generate a matrix() or matrix3d() representation and compare that.
let mut mat = String::new();
if let Some(matrix) = matrix.to_matrix2d() {
Transform::Matrix(matrix).to_css(&mut Printer::new(
&mut mat,
PrinterOptions {
minify: true,
..PrinterOptions::default()
},
))?
} else {
Transform::Matrix3d(matrix).to_css(&mut Printer::new(
&mut mat,
PrinterOptions {
minify: true,
..PrinterOptions::default()
},
))?
}
if mat.len() < base.len() {
dest.write_str(&mat)?;
} else {
dest.write_str(&base)?;
}
return Ok(());
}
}
Maybe we can just comment that out for now?

Unfortunate that this optimization breaks animations. I don't think disabling it inside keyframes would be enough because this would also affect CSS transitions. Perhaps there is a way to to this more safely somehow, or make it opt in, but for now we can just disable it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants