Tag: motion

Pure CSS Bezier Curve Motion Paths

Are you a Bezier curve lover like I am?

Besides being elegant, Bezier curves have nice mathematical properties due to their definition and construction. No wonder they are widely used in so many areas:

  • As a drawing/design tool: They are often refered to as “paths” in vector drawing software.
  • As a format of representing curves: They are used in SVG, fonts and many other vector graphic formats.
  • As a mathematical function: Often used to control animation timing.

Now, how about using Bezier curves as motion paths with CSS?

Quick Recap

Depending on the context, when referring to a “Bezier curve”, we often assume a 2D cubic Bezier curve.

Such a curve is defined by four points:

Bezier curve
MarianSigler, Public domain, via Wikimedia Commons

Note: In this article, we generally refer to P0 and P3 as endpoints, P1 and P2 as control points.

The word “cubic” means the underlying function of the curve is a cubic polynomial. There are also “quadratic” Bezier curves, which are similar, but with one fewer control point.

The Problem

Say you are given an arbitrary 2D cubic Beizer curve, how would you animate an element with pure CSS animation, such that it moves precisely along the curve?

As an example, how would you recreate this animation?

In this article we will explore three methods with different flavors. For each solution we will present an interactive demo, then explain how it works. There are lots of mathematical calculations and proofs behind the scene, but don’t worry, we will not go very deep.

Let’s start!

Method 1: Time Warp

Here’s the basic idea:

  • Set up @keyframes to move the element from one endpoint of the curve to the other.
  • Distort the time for each coordinate individually, using animation-timing-function.

Note: There are lots of examples and explanations in Temani Afif’s article (2021).

Using the cubic-bezier() function with correct parameters, we can create a motion path of any cubic Bezier curve:

This demo shows a pure CSS animation. Yet canvas and JavaScript are used, which serve two purposes:

  • Visualize the underlying Bezier curve (red curve).
  • Allow adjusting the curve with the typical “path” UI.

You can drag the two endpoints (black dots) and the two control points (black squares). The JavaScript code will update the animation accordingly, by updating a few CSS variables.

Note: Here’s a pure CSS version for reference.

How it works

Suppose the desired cubic Bezier curve is defined by four points: p0, p1, p2, and p3. We set up CSS rules as following:

/* pseudo CSS code */ div {   animation-name: move-x, move-y;   /*     Define:     f(x, a, b) = (x - a) / (b - a)     qx1 = f(p1.x, p0.x, p3.x)     qx2 = f(p2.x, p0.x, p3.x)     qy1 = f(p1.y, p0.y, p3.y)     qy2 = f(p2.y, p0.y, p3.y)   */   animation-timing-function:      cubic-bezier(1/3, qx1, 2/3, qx1),     cubic-bezier(1/3, qy1, 2/3, qy2); }  @keyframes move-x {   from {     left: p0.x;   }   to {     left: p3.x;   } }  @keyframes move-y {   from {     top: p0.y;   }   to {     top: p3.y;   } }

The @keyframes rules move-x and move-y determine the starting and finishing locations of the element. In animation-timing-function we have two magical cubic-bezier() functions, the parameters are calculated such that both top and left always have the correct values at any time.

I’ll skip the math, but I drafted a brief proof here, for your curious math minds.

Discussions

This method should work well for most cases. You can even make a 3D cubic Bezier curve, by introducing another animation for the z value.

However there are a few minor caveats:

  • It does not work when both endpoints lie on a horizontal or vertical line, because of the division-by-zero error.

Note: In practice, you can just add a tiny offset as a workaround.

  • It does not support Bezier curves with an order higher than 3.
  • Options for animation timing are limited.
    • We use 1/3 and 2/3 above to achieve a linear timing.
    • You can tweak both values to adjust the timing, but it is limited compared with other methods. More on this later.

Method 2: Competing Animations

As a warm-up, imagine an element with two animations:

div {   animation-name: move1, move2; }

What is the motion path of the element, if the animations are defined as following:

@keyframes move1 {   to {     left: 256px;   } }  @keyframes move2 {   to {     top: 256px;   } }

As you may have guessed, it moves diagonally:

Now, what if the animations are defined like this instead:

@keyframes move1 {   to {     transform: translateX(256px);   } }  @keyframes move2 {   to {     transform: translateY(256px);   } }

“Aha, you cannot trick me!” you might say, as you noticed that both animations are changing the same property, “move2 must override move1 like this:”

Well, earlier I had thought so, too. But actually we get this:

The trick is that move2 does not have a from frame, which means the starting position is animated by move1.

In the following demo, the starting position of move2 is visualized as the moving blue dot:

Quadratic Bezier Curves

The demo right above resembles the construction of a quadratic Bezier curve:

Bézier 2 big
Phil Tregoning, Public domain, via Wikimedia Commons

But they look different. The construction has three linearly moving dots (two green, one black), but our demo has only two (the blue dot and the target element).

Actually the motion path in the demo is a quadratic Bezier curve, we just need to tune the keyframes carefully. I will skip the math and just reveal the magic:

Suppose a quadratic Bezier curve is defined by points p0, p1, and p2. In order to move an element along the curve, we do the following:

/* pseudo-CSS code */ div {   animation-name: move1, move2; }  @keyframes move1 {   from {     transform: translate3d(p0.x, p0.y, p0.z);   }   /* define q1 = (2 * p1 - p2) */   to {     transform: translate3d(q1.x, q1.y, q1.z);   } }  @keyframes move2 {   to {     transform: translate3d(p2.x, p2.y, p2.z);   } }

Similar to the demo of Method 1, you can view or adjust the curve. Additionally, the demo also shows two more pieces of information:

  • The mathematical construction (gray moving parts)
  • The CSS animations (blue parts)

Both can be toggled using the checkboxes.

Cubic Bezier Curves

This method works for cubic Bezier curves as well. If the curve is defined by points p0, p1, p2, and p3. The animations should be defined like this:

/* pseudo-CSS code */ div {   animation-name: move1, move2, move3; }  @keyframes move1 {   from {     transform: translate3d(p0.x, p0.y, p0.z);   }   /* define q1 = (3 * p1 - 3 * p2 + p3) */   to {     transform: translate3d(q1.x, q1.y, q1.z);   } }  @keyframes move2 {   /* define q2 = (3 * p2 - 2 * p3) */   to {     transform: translate3d(q2.x, q2.y, q2.z);   } }  @keyframes move3 {   to {     transform: translate3d(p3.x, p3.y, p3.z);   } }

Extensions

What about 3D Bezier Curves? Actually, the truth is, all the previous examples were 3D curves, we just never bothered with the z values.

What about higher-order Bezier curves? I am 90% sure that the method can be naturally extended to higher orders. Please let me know if you have worked out the formula for fourth-order Bezier curves, or even better, a generic formula for Bezier curves of order N.

Method 3: Standard Bezier Curve Construction

The mathematical construction of Bezier Curves already gives us a good hint.

Bézier 3 big
Phil Tregoning, Public domain, via Wikimedia Commons

Step-by-step, we can determine the coordinates of all moving dots. First, we determine the location of the green dot that is moving between p0 and p1:

@keyframes green0 {   from {     --green0x: var(--p0x);     --green0y: var(--p0y);   }   to {     --green0x: var(--p1x);     --green0y: var(--p1y);   } }

Additional green dots can be constructed in a similar way.

Next, we can determine the location of a blue dot like this:

@keyframes blue0 {   from {     --blue0x: var(--green0x);     --blue0y: var(--green0y);   }   to {     --blue0x: var(--green1x);     --blue0y: var(--green1y);   } }

Rinse and repeat, eventually we will get the desired curve.

Similar to Method 2, with this method we can easily build a 3D Bezier Curve. It is also intuitive to extend the method for higher-order Bezier curves.

The only downside is the usage of @property, which is not supported by all browsers.

Animation Timing

All the examples so far have the “linear” timing, what about easing or other timing functions?

Note: By “linear” we mean the variable t of the curve linearly changes from 0 to 1. In other words, t is the same as animation progress.

animation-timing-function is never used in Method 2 and Method 3. Like other CSS animations, we can use any supported timing function here, but we need to apply the same function for all animations (move1, move2, and move3) at the same time.

Here’s an example of animation-timing-function: cubic-bezier(1, 0.1, 0, 0.9):

And here’s how it looks like with animation-timing-function: steps(18, end):

On the other hand, Method 1 is trickier, because it already uses a cubic-bezier(u1, v1, u2, v2) timing function. In the examples above we have u1=1/3 and u2=2/3. In fact we can tweak the timing by changing both parameters. Again, all animations (e.g., move-x and move-y) must have the same values of u1 and u2.

Here’s how it looks like when u1=1 and u2=0:

With Method 2, we can achieve exactly the same effect by setting animation-timing-function to cubic-bezier(1, 0.333, 0, 0.667):

In fact, it works in a more general way:

Suppose that we are given a cubic Bezier curve, and we created two animations for the curve with Method 1 and Method 2 respectively. For any valid values of u1 and u2, the following two setups have the same animation timing:

  • Method 1 with animation-timing-function: cubic-bezier(u1, *, u2, *).
  • Method 2 with animation-timing-function: cubic-bezier(u1, 1/3, u2, 2/3).

Now we see why Method 1 is “limited”: with Method 1 we can only cubic-bezier() with two parameters, but with Method 2 and Method 3 we can use any CSS animation-timing-function.

Conclusions

In this article, we discussed 3 different methods of moving elements precisely along a Bezier curve, using only CSS animations.

While all 3 methods are more or less practical, they have their own pros and cons:

  • Method 1 might be more intuitive for those familiar with the timing function hack. But it is less flexible with animation timing.
  • Method 2 has very simple CSS rules. Any CSS timing function can be applied directly. However, it could be hard to remember the formulas.
  • Method 3 make more sense for those familiar with the math construction of Bezier curves. Animation timing is also flexible. On the other hand, not all modern browsers are supported, due the usage of @property.

That’s all! I hope you find this article interesting. Please let me know your thoughts!


Pure CSS Bezier Curve Motion Paths originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

CSS-Tricks

, , , ,

No Motion Isn’t Always prefers-reduced-motion

There is a code snippet that I see all the time when the media query prefers-reduced-motion is talked about. Here it is:

@media (prefers-reduced-motion: reduce) {   * {     animation-duration: 0.01ms !important;     animation-iteration-count: 1 !important;     transition-duration: 0.01ms !important;     scroll-behavior: auto !important;   } }

This is CSS that attempts to obliterate any motion on a website under the condition that the user has specified a preference for reduced motion in the accessibility preferences of their operating system.

prefers-reduced-motion settings in MacOS.

Why prefers-reduced-motion matters

The reason this setting exists is that on-screen movement is an accessibility concern. Here’s Eric Bailey:

Vestibular disorders can cause your vestibular system to struggle to make sense of what is happening, resulting in loss of balance and vertigo, migraines, nausea, and hearing loss. Anyone who has spun around too quickly is familiar with a confused vestibular system.

Vestibular disorders can be caused by both genetic and environmental factors. It’s part of the larger spectrum of conditions that make up accessibility concerns and it affects more than 70 million people.

Here he is again in a follow-up article:

If you have a vestibular disorder or have certain kinds of migraine or seizure triggers, navigating the web can be a lot like walking through a minefield — you’re perpetually one click away from activating an unannounced animation. And that’s just for casual browsing.

Reduced motion vs. nuked motion

Knowing this, the temptation might be high to go nuclear on the motion and wipe it out entirely when a user has specified a reduced motion preference. The trouble with that is — to quote Eric again — “animation isn’t unnecessary.” Some of it might be, but animation can also help accessibility. For example, a “transitional interface” (e.g. a list that animates an opening for a new item to slide into it) can be very helpful:

Animation can be a great tool to help combat some forms of cognitive disability by using it to break down complicated concepts, or communicate the relationship between seemingly disparate objects. Val Head’s article on A List Apart highlights some other very well-researched benefits, including helping to increase problem-solving ability, recall, and skill acquisition, as well as reducing cognitive load and your susceptibility to change blindness.

In this case, you would lose the helpful contextual movement if you just nuked it all. You just might want to take a different approach when in a prefers-reduced-motion scenario. Perhaps less, slower, or removed motion while leaning harder on color and fading transitions.

Ban Nadel recently wrote “Applying Multiple Animation @keyframes To Support Prefers-Reduced-Motion In CSS” and covered a similar example. A modal entrance animation uses both a fade-in and scale-in effect by default. Then, in a prefers-reduced-motion scenario, it uses the fade-in but not the scaling. The scaling causes movement in a way the fading doesn’t.

/*    By default, we'll use the REDUCED MOTION version of the animation. */ @keyframes modal-enter {   from {     opacity: 0 ;   }   to {     opacity: 1 ;   } }  /*   Then, if the user has NO PREFERENCE for motion, we can OVERRIDE the   animation definition to include both the motion and non-motion properties. */ @media ( prefers-reduced-motion: no-preference ) {   @keyframes modal-enter {     from {       opacity: 0 ;       transform: scale(0.7) ;     }     to {       opacity: 1 ;       transform: scale(1.0) ;     }   } }

See the GIF demo on Ben’s site if you’d like to see a quick comparison.

I like how this style of approach is think about the problem and come up with a reduced motion solution, rather than screw it all, no movement period!!

But not all motion is driven by CSS

While we’re on the topic of that screw-all-motion CSS snippet, note that it’s only effective at doing what it sets out to do on sites where all the movement is entirely CSS-driven. If you’re using JavaScript-powered animations beware that this nuclear snippet might… well here’s Josh Comeau:

If your animations are entirely driven by CSS, this works great… But I’ve had weird issues when running animations in JS. Specifically, I’ve seen this reset have the opposite effect, and make animations super fast and dizzying.

That’s right: It might do quite literally the opposite of what you are trying to do.


No Motion Isn’t Always prefers-reduced-motion originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, , ,
[Top]

Weekly Platform News: Reduced Motion, CORS, WhiteHouse.gov, popups, and 100vw

In this week’s roundup, we highlight a proposal for a new <popup> element, check the use of prefers-reduced-motion on award-winning sites, learn how to opt into cross-origin isolation, see how WhiteHouse.gov approaches accessibility, and warn the dangers of 100vh.

Let’s get into the news!

The new HTML <popup> element is in development

On January 21, Melanie Richards from the Microsoft Edge web platform team posted an explainer for a proposed HTML <popup> element (the name is not final). A few hours later, Mason Freed from Google announced an intent to prototype this element in the Blink browser engine. Work on the implementation is taking place in Chromium issue #1168738.

A popup is a temporary (transient) and “light-dismissable” UI element that is displayed on the the “top layer” of all other elements. The goal for the <popup> element is to be fully stylable and accessible by default. A <popup> can be anchored to an activating element, such as a button, but it can also be a standalone element that is displayed on page load (e.g., a teaching UI).

Two use cases showing a white action menu with four gray menu links below a blue menu button, and another example of a blog button with a large dark blue tooltip beneath it with two paragraphs of text in white.

A <popup> is automatically hidden when the user presses the Esc key or moves focus to a different element (this is called a light dismissal). Unlike the <dialog> element, only one <popup> can be shown at a time, and unlike the deprecated <menu> element, a <popup> can contain arbitrary content, including interactive elements:

We imagine <popup> as being useful for various different types of popover UI. We’ve chosen to use an action menu as a primary example, but folks use popup-esque patterns for many different types of content.

Award-winning websites should honor the “reduce motion” preference

Earlier this week, AWWWARDS announced the winners of their annual awards for the best websites of 2020. The following websites were awarded:

All these websites are highly dynamic and show full-screen motion on load and during scroll. Unfortunately, such animations can cause dizziness and nausea in people with vestibular disorders. Websites are therefore advised to reduce or turn off non-essential animations via the prefers-reduced-motion media query, which evaluates to true for people who have expressed their preference for reduced motion (e.g., the “Reduce motion” option on Apple’s platforms). None of the winning websites do this.

/* (code from animal-crossing.com) */ @media (prefers-reduced-motion: reduce) {   .main-header__scene {     animation: none;   } }

An example of a website that does this correctly is the official site of last year’s Animal Crossing game. Not only does the website honor the user’s preference via prefers-reduced-motion, but it also provides its own “Reduce motion” toggle button at the very top of the page.

Screenshot of the animal crossing game website that is bright with a solid green header above a gold ribbon that displays menu items. Below is the main banner showing a still of the animated game with a wooden welcome to Animal Crossing sign in the foreground.

(via Eric Bailey)

Websites can now opt into cross-origin isolation

Cross-origin isolation is part of a “long-term security improvement.” Websites can opt into cross-origin isolation by adding the following two HTTP response headers, which are already supported in Chrome and Firefox:

Cross-Origin-Embedder-Policy: require-corp Cross-Origin-Opener-Policy: same-origin

A cross-origin-isolated page relinquishes its ability to load content from other origins without their explicit opt-in (via CORS headers), and in return, the page gains access to some powerful APIs, such as SharedArrayBuffer.

if (crossOriginIsolated) {   // post SharedArrayBuffer } else {   // do something else }

The White House recommits to accessibility

The new WhiteHouse.gov website of the Biden administration was built from scratch in just six weeks with accessibility as a top priority (“accessibility was top of mind”). Microsoft’s chief accessibility officer reviewed the site and gave it the thumbs up.

The website’s accessibility statement (linked from the site’s footer) declares a “commitment to accessibility” and directly references the latest version of the Web Content Accessibility Guidelines, WCAG 2.1 (2018). This is notable because federal agencies in the USA are only required to comply with WCAG 2.0 (2008).

The Web Content Accessibility Guidelines are the most widely accepted standards for internet accessibility. … By referencing WCAG 2.1 (the latest version of the guidelines), the Biden administration may be indicating a broader acceptance of the WCAG model.

The CSS 100vw value can cause a useless horizontal scrollbar

On Windows, when a web page has a vertical scrollbar, that scrollbar consumes space and reduces the width of the page’s <html> element; this is called a classic scrollbar. The same is not the case on macOS, which uses overlay scrollbars instead of classic scrollbars; a vertical overlay scrollbar does not affect the width of the <html> element.

macOS users can switch from overlay scrollbars to classic scrollbars by setting “Show scroll bars” to ”Always” in System preferences > General.

The CSS length value 100vw is equal to the width of the <html> element. However, if a classic vertical scrollbar is added to the page, the <html> element becomes narrower (as explained above), but 100vw stays the same. In other words, 100vw is wider than the page when a classic vertical scrollbar is present.

This can be a problem for web developers on macOS who use 100vw but are unaware of its peculiarity. For example, a website might set width: 100vw on its article header to make it full-width, but this will cause a useless horizontal scrollbar on Windows that some of the site’s visitors may find annoying.

Screenshot of an article on a white background with a large black post title, post date and red tag links above a paragraph of black text. A scrollbar is displayed on the right with two large red arrows illustrating the page width, which is larger than the 100 viewport width unit.

Web developers on macOS can switch to classic scrollbars to make sure that overflow bugs caused by 100vw don’t slip under their radar. In the meantime, I have asked the CSS Working Group for comment.


The post Weekly Platform News: Reduced Motion, CORS, WhiteHouse.gov, popups, and 100vw appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , , , ,
[Top]

How to Create a Realistic Motion Blur with CSS Transitions

Before we delve into making a realistic motion blur in CSS, it’s worth doing a quick dive into what motion blur is, so we can have a better idea of what we’re trying to reproduce.

Have you ever taken a photo of something moving quickly, especially under low light, and it turned into a blurry streak? Or maybe the whole camera shook and the whole shot became a series of streaks? This is motion blur, and it’s a byproduct of how a camera works.

Motion Blur 101

Imagine a camera. It’s got a shutter, a door that opens to let light in, and then closes to stop the light from coming in. From the time it opens, to when it closes, is a single photograph, or a single frame of a moving image.

A blurry man wearing a red shirt on a blue bike speeding through the forest.
Real motion blur in action (Photo: Kevin Erdvig, Unsplash)

If the subject of the frame is moving during the time the shutter is open, we end up taking a photo of an object moving through the frame. On film, this shows up as being a steady smear, with the subject being in an infinite number of places between its starting point to its end. The moving object also ends up being semi-transparent, with parts of the background visible behind it.

What computers do to fake this is model several subframes, and then composite them together at a fraction of the opacity. Putting lots of copies of the same object in slightly different places along the path of motion creates a pretty convincing facsimile of a motion blur.

Video compositing apps tend to have settings for how many subdivisions their motion blur should have. If you set this value really low, you can see exactly how the technique works, like this, a frame of an animation of a simple white dot at four samples per frame:

Four overlapping white opaque circles on a black background.
Four samples per frame.
Twelve overlapping white opaque circles on a black background.
Here is 12 samples per frame.
Thirty-two overlapping white opaque circles on a black background.
And by the time we’re at 32 samples per frame, it’s pretty close to fully real, especially when seen at multiple frames per second.

The number of samples it takes to make a convincing motion blur is entirely relative to the content. Something small with sharp edges that’s moving super fast will need a lot of subframes; but something blurry moving slowly might need only a few. In general, using more will create a more convincing effect.

Doing this in CSS

In order to approximate this effect in CSS, we need to create a ton of identical elements, make them semi-transparent, and offset their animations by a tiny fraction of a second.

First, we’ll set up the base with the animation we want using a CSS transition. We’ll go with a simple black dot, and assign it a transform on hover (or tap, if you’re on mobile). We’ll also animate the border radius and color to show the flexibility of this approach.

Here is the base animation without motion blur:

Now, let’s make 20 identical copies of the black dot, all placed in the exact same place with absolute positioning. Each copy has an opacity of 10%, which is a little more than might be mathematically correct, but I find we need to make them more opaque to look solid enough.

The next step is where the magic happens. We add a slightly-increasing transition-delay value for each clone of our dot object. They’ll all run the exact same animation, but they’ll each be offset by three milliseconds. 

The beauty of this approach is that it creates a pseudo-motion blur effect that works for a ton of different animations. We can throw color changes on there, scaling transitions, odd timings, and the motion blur effect still works.

Using 20 object clones will work for plenty of fast and slow animations, but fewer can still produce a reasonable sense of motion blur. You may need to tweak the number of cloned objects, their opacity, and the amount of transition delay to work with your particular animation. The demo we just looked at has the blur effect slightly overclocked to make it more prominent.


Eventually, with the progress of computer power, I expect that some of the major browsers might start offering this effect natively. Then we can do away with the ridiculousness of having 20 identical objects. In the meantime, this is a reasonable way to approximate a realistic motion blur.


The post How to Create a Realistic Motion Blur with CSS Transitions appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Different Approaches to Responsive CSS Motion Path

As a follow-up to Jhey’s recent post on responsive motion paths, Michelle Barker notes that another approach could be to just transform: scale() the whole dang element.

The trade-off there is that you’re scaling both the path and the element on the path at the same time; Jhey’s approach only makes path flexbile and the element stays the same size.

Calculating scale is a really cool trick I think and one we’ve also covered before.

Direct Link to ArticlePermalink

The post Different Approaches to Responsive CSS Motion Path appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

Create a Responsive CSS Motion Path? Sure We Can!

There was a discussion recently on the Animation at Work Slack: how could you make a CSS motion path responsive? What techniques would be work? This got me thinking.

A CSS motion path allows us to animate elements along custom user-defined paths. Those paths follow the same structure as SVG paths. We define a path for an element using offset-path.

.block {   offset-path: path('M20,20 C20,100 200,0 200,100'); }

These values appear relative at first and they would be if we were using SVG. But, when used in an offset-path, they behave like px units. This is exactly the problem. Pixel units aren’t really responsive. This path won’t flex as the element it is in gets smaller or larger. Let’s figure this out.

To set the stage, the offset-distance property dictates where an element should be on that path:

Not only can we define the distance an element is along a path, but we can also define an element’s rotation with offset-rotate. The default value is auto which results in our element following the path. Check out the property’s almanac article for more values.

To animate an element along the path, we animate the offset-distance:

OK, that catches up to speed on moving elements along a path. Now we have to answer…

Can we make responsive paths?

The sticking point with CSS motion paths is the hardcoded nature. It’s not flexible. We are stuck hardcoding paths for particular dimensions and viewport sizes. A path that animates an element 600px, will animate that element 600px regardless of whether the viewport is 300px or 3440px wide.

This differs from what we are familiar with when using SVG paths. They will scale with the size of the SVG viewbox.

Try resizing this next demo below and you’ll see:

  • The SVG will scale with the viewport size as will the contained path.
  • The offset-path does not scale and the element goes off course.

This could be okay for simpler paths. But once our paths become more complicated, it will be hard to maintain. Especially if we wish to use paths we’ve created in vector drawing applications.

For example, consider the path we worked with earlier:

.element {   --path: 'M20,20 C20,100 200,0 200,100';   offset-path: path(var(--path)); }

To scale that up to a different container size, we would need to work out the path ourselves, then apply that path at different breakpoints. But even with this “simple” path, is it a case of multiplying all the path values? Will that give us the right scaling?

@media(min-width: 768px) {   .element {     --path: 'M40,40 C40,200 400,0 400,200'; // ????   } }

A more complex path such as one drawn in a vector application is going to be trickier to maintain. It will need the developer to open the application, rescale the path, export it, and integrate it with the CSS. This will need to happen for all container size variations. It’s not the worst solution, but it does require a level of maintenance that we might not want to get ourselves into.

.element {   --path: 'M40,228.75L55.729166666666664,197.29166666666666C71.45833333333333,165.83333333333334,102.91666666666667,102.91666666666667,134.375,102.91666666666667C165.83333333333334,102.91666666666667,197.29166666666666,165.83333333333334,228.75,228.75C260.2083333333333,291.6666666666667,291.6666666666667,354.5833333333333,323.125,354.5833333333333C354.5833333333333,354.5833333333333,386.0416666666667,291.6666666666667,401.7708333333333,260.2083333333333L417.5,228.75';   offset-path: path(var(--path)); } 
 @media(min-width: 768px) {   .element {     --path: 'M40,223.875L55.322916666666664,193.22916666666666C70.64583333333333,162.58333333333334,101.29166666666667,101.29166666666667,131.9375,101.29166666666667C162.58333333333334,101.29166666666667,193.22916666666666,162.58333333333334,223.875,223.875C254.52083333333334,285.1666666666667,285.1666666666667,346.4583333333333,315.8125,346.4583333333333C346.4583333333333,346.4583333333333,377.1041666666667,285.1666666666667,392.4270833333333,254.52083333333334L407.75,223.875';   } } 
 @media(min-width: 992px) {   .element {     --path: 'M40,221.625L55.135416666666664,191.35416666666666C70.27083333333333,161.08333333333334,100.54166666666667,100.54166666666667,130.8125,100.54166666666667C161.08333333333334,100.54166666666667,191.35416666666666,161.08333333333334,221.625,221.625C251.89583333333334,282.1666666666667,282.1666666666667,342.7083333333333,312.4375,342.7083333333333C342.7083333333333,342.7083333333333,372.9791666666667,282.1666666666667,388.1145833333333,251.89583333333334L403.25,221.625';   } }

It feels like a JavaScript solution makes sense here. GreenSock is my first thought because its MotionPath plugin can scale SVG paths. But what if we want to animate outside of an SVG? Could we write a function that scales the paths for us? We could but it won’t be straightforward.

Trying different approaches

What tool allows us to define a path in some way without the mental overhead? A charting library! Something like D3.js allows us to pass in a set of coordinates and receive a generated path string. We can tailor that string to our needs with different curves, sizing, etc.

With a little tinkering, we can create a function that scales a path based on a defined coordinate system:

This definitely works, but it’s also less than ideal because it’s unlikely we are going to be declaring SVG paths using sets of coordinates. What we want to do is take a path straight out of a vector drawing application, optimize it, and drop it on a page. That way, we can invoke some JavaScript function and let that do the heavy lifting.

So that’s exactly what we are going to do.

First, we need to create a path. This one was thrown together quickly in Inkscape. Other vector drawing tools are available.

A path created in Inkscape on a 300×300 canvas

Next, let’s optimize the SVG. After saving the SVG file, we’ll run it through Jake Archibald’s brilliant SVGOMG tool. That gives us something along these lines:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 79.375 79.375" height="300" width="300"><path d="M10.362 18.996s-6.046 21.453 1.47 25.329c10.158 5.238 18.033-21.308 29.039-18.23 13.125 3.672 18.325 36.55 18.325 36.55l12.031-47.544" fill="none" stroke="#000" stroke-width=".265"/></svg>

The parts we’re interested are path and viewBox.

Expanding the JavaScript solution

Now we can create a JavaScript function to handle the rest. Earlier, we created a function that takes a set of data points and converts them into a scalable SVG path. But now we want to take that a step further and take the path string and work out the data set. This way our users never have to worry about trying to convert their paths into data sets.

There is one caveat to our function: Besides the path string, we also need some bounds by which to scale the path against. These bounds are likely to be the third and fourth values of the viewBox attribute in our optimized SVG.

const path = "M10.362 18.996s-6.046 21.453 1.47 25.329c10.158 5.238 18.033-21.308 29.039-18.23 13.125 3.672 18.325 36.55 18.325 36.55l12.031-47.544"; const height = 79.375 // equivalent to viewbox y2 const width = 79.375 // equivalent to viewbox x2 
 const motionPath = new ResponsiveMotionPath({   height,   width,   path, });

We won’t go through this function line-by-line. You can check it out in the demo! But we will highlight the important steps that make this possible.

First, we’re converting a path string into a data set

The biggest part of making this possible is being able to read the path segments. This is totally possible, thanks to the SVGGeometryElement API. We start by creating an SVG element with a path and assigning the path string to its d attribute.

// To convert the path data to points, we need an SVG path element. const svgContainer = document.createElement('div'); // To create one though, a quick way is to use innerHTML svgContainer.innerHTML = `   <svg xmlns="http://www.w3.org/2000/svg">     <path d="$ {path}" stroke-width="$ {strokeWidth}"/>   </svg>`; const pathElement = svgContainer.querySelector('path');

Then we can use the SVGGeometryElement API on that path element. All we need to do is iterate over the total length of the path and return the point at each length of the path.

convertPathToData = path => {   // To convert the path data to points, we need an SVG path element.   const svgContainer = document.createElement('div');   // To create one though, a quick way is to use innerHTML   svgContainer.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg">                               <path d="$ {path}"/>                             </svg>`;   const pathElement = svgContainer.querySelector('path');   // Now to gather up the path points.   const DATA = [];   // Iterate over the total length of the path pushing the x and y into   // a data set for d3 to handle 👍   for (let p = 0; p < pathElement.getTotalLength(); p++) {     const { x, y } = pathElement.getPointAtLength(p);     DATA.push([x, y]);   }   return DATA; }

Next, we generate scaling ratios

Remember how we said we’d need some bounds likely defined by the viewBox? This is why. We need some way to calculate a ratio of the motion path against its container. This ratio will be equal to that of the path against the SVG viewBox. We will then use these with D3.js scales.

We have two functions: one to grab the largest x and y values, and another to calculate the ratios in relation to the viewBox.

getMaximums = data => {   const X_POINTS = data.map(point => point[0])   const Y_POINTS = data.map(point => point[1])   return [     Math.max(...X_POINTS), // x2     Math.max(...Y_POINTS), // y2   ] } getRatios = (maxs, width, height) => [maxs[0] / width, maxs[1] / height]

Now we need to generate the path

The last piece of the puzzle is to actually generate the path for our element. This is where D3.js actually comes into play. Don’t worry if you haven’t used it before because we’re only using a couple of functions from it. Specifically, we are going to use D3 to generate a path string with the data set we generated earlier.

To create a line with our data set, we do this:

d3.line()(data); // M10.362000465393066,18.996000289916992L10.107386589050293, etc.

The issue is that those points aren’t scaled to our container. The cool thing with D3 is that it provides the ability to create scales. These act as interpolation functions. See where this is going? We can write one set of coordinates and then have D3 recalculate the path. We can do this based on our container size using the ratios we generated.

For example, here’s the scale for our x coordinates:

const xScale = d3   .scaleLinear()   .domain([     0,     maxWidth,   ])   .range([0, width * widthRatio]);

The domain is from 0 to our highest x value. The range in most cases will go from 0 to container width multiplied by our width ratio.

There are times where our range may differ and we need to scale it. This is when the aspect ratio of our container doesn’t match that of our path. For example, consider a path in an SVG with a viewBox of 0 0 100 200. That’s an aspect ratio of 1:2. But if we then draw this in a container that has a height and width of 20vmin, the aspect ratio of the container is 1:1. We need to pad the width range to keep the path centered and maintain the aspect ratio.

What we can do in these cases is calculate an offset so that our path will still be centered in our container. 

const widthRatio = (height - width) / height const widthOffset = (ratio * containerWidth) / 2 const xScale = d3   .scaleLinear()   .domain([0, maxWidth])   .range([widthOffset, containerWidth * widthRatio - widthOffset])

Once we have two scales, we can map our data points using the scales and generate a new line.

const SCALED_POINTS = data.map(POINT => [   xScale(POINT[0]),   yScale(POINT[1]), ]); d3.line()(SCALED_POINTS); // Scaled path string that is scaled to our container

We can apply that path to our element by passing it inline via a CSS property 👍

ELEMENT.style.setProperty('--path', `"$ {newPath}"`);

Then it’s our responsibility to decide when we want to generate and apply a new scaled path. Here’s one possible solution:

const setPath = () => {   const scaledPath = responsivePath.generatePath(     CONTAINER.offsetWidth,     CONTAINER.offsetHeight   )   ELEMENT.style.setProperty('--path', `"$ {scaledPath}"`) } const SizeObserver = new ResizeObserver(setPath) SizeObserver.observe(CONTAINER)

This demo (viewed best in full screen) shows three versions of the element using a motion path. The paths are present to easier see the scaling. The first version is the unscaled SVG. The second is a scaling container illustrating how the path doesn’t scale. The third is using our JavaScript solution to scale the path.

Phew, we did it!

This was a really cool challenge and I definitely learned a bunch from it! Here’s a couple of demos using the solution.

It should work as a proof of concept and looks promising! Feel free to drop your own optimized SVG files into this demo to try them out! — it should catch most aspect ratios.

I’ve created a package named “Meanderer” on GitHub and npm. You can also pull it down with unpkg CDN to play with it in CodePen, if you want to try it out.

I look forward to seeing where this might go and hope we might see some native way of handling this in the future. 🙏

The post Create a Responsive CSS Motion Path? Sure We Can! appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

Get Moving (or not) with CSS Motion Path

We just linked up the idea that offset-path can be cleverly used to set type on a path. Don’t miss Michelle Barker’s experimentation either, with drawing paths or animating text along a path.

Dan Wilson has also been following this tech for quite a while and points out why the sudden surge of interest in this:

With the release of Firefox 72 on January 7, 2020, CSS Motion Path is now in Firefox, new Edge (slated for a January 15, 2020 stable release), Chrome, and Opera (and other Blink-based browsers). That means each of these browsers supports a common baseline of offset-path: path()offset-distance, and offset-rotate.

Dan’s post does a great job of covering the basics, including some things you might not think of, like the fact that the path itself can be animated.

Direct Link to ArticlePermalink

The post Get Moving (or not) with CSS Motion Path appeared first on CSS-Tricks.

CSS-Tricks

, ,
[Top]

Motion Paths – Past, Present and Future

Cassie Evans has a great intro to motion paths. That is, being able to animate an element along a path. Not just up/down/left/right, but whatever curvy/wiggly/weird path you want.

It’s an interesting subject because there are so many different technologies helping to do it over time. SMIL, JavaScript-powered animation libraries, native JavaScript APIs, and even CSS via offset-path and friends. I think offset-path is funny – it was changed to that name from motion-path as you don’t technically have to apply motion to an element you place on a path in this way.

There’s no clear winner. I’m (perhaps obviously) a fan of doing stuff like this in CSS whenever possible, but the browser support there is essentially Chrome-only. Plus seeing SVG path values in CSS always feels a smidge uncomfortable because of the unitless numbers. SMIL feels like essentially dead technology, but at least then you’re in SVG-land and the paths make good sense in that context. If browser support is vital, you have to use a library.

I do think there is untapped cool design possibility in motion paths. It’s not just for landing space ships, but can be for practical stuff like how a modal enters a page.

Direct Link to ArticlePermalink

The post Motion Paths – Past, Present and Future appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

Reduced Motion Picture Technique, Take Two

Did you see that neat technique for using the <picture> element with <source media=""> to serve an animated image (or not) based on a prefers-reduced-motion media query?

After we shared that in our newsletter, we got an interesting reply from Michael Gale:

What about folks who love their animated GIFs, but just didn’t want the UI to be zooming all over the place? Are they now forced to make a choice between content and UI?

I thought that was a pretty interesting question.

Also, whenever I see <img src="gif.gif"> these days, my brain is triggered into WELL WHAT ABOUT MP4?! territory, as I’ve been properly convinced that videos are better-in-all-ways on the web than GIFs. Turns out, some browsers support videos right within the <img> element and, believe it or not, you can write fallbacks for that, with — drumroll, please — for the <picture> element as well!

Let’s take a crack at combining all this stuff.

Adding an MP4 source

The easy one is adding an additional <source> with the video. That means we’ll need three source media files:

  1. A fallback non-animated graphic when prefers-reduced-motion is reduce.
  2. An animated GIF as the default.
  3. An MP4 video to replace the GIF, if the fallback is supported.

For example:

<picture>   <source srcset="static.png" media="(prefers-reduced-motion: reduce)"></source>   <source srcset="animated.mp4" type="video/mp4">   <img srcset="animated.gif" alt="animated image" /> </picture>

Under default conditions in Chrome, only the GIF is downloaded and shown:

Chrome DevTools showing only gif downloaded

Under default conditions in Safari, only the MP4 is downloaded and shown:

Safari DevTools showing only mp4 downloaded

If you’ve activated prefers-reduced-motion: reduce in either Chrome or Safari (on my Mac, I go to System PreferencesAccessibilityDisplayReduce Motion), both browsers only download the static PNG file.

Chrome DevTools showing png downloaded

I tested Firefox and it doesn’t seem to work, instead continuing to download the GIF version. Firefox seems to support prefers-reduced-motion, but perhaps it’s just not supported on <source> elements yet? I’m not sure what’s up there.

Wouldn’t it be kinda cool to provide a single animated source and have a tool generate the others from it? I bet you could wire that up with something like Cloudinary.

Adding a toggle to show the animated version

Like Michael Gale mentioned, it seems a pity that you’re entirely locked out from seeing the animated version just because you’ve flipped on a reduced motion toggle.

It should be easy enough to have a <button> that would use JavaScript to yank out the media query and force the browser to show the animated version.

I’m fairly sure there is no practical way to do this declaratively in HTML. We also can’t put this button within the <picture> tag. Even though <picture> isn’t a replaced element, the browser still gets confused and doesn’t like it. Instead, it doesn’t render it at all. No big deal, we can use a wrapper.

<div class="picture-wrap">      <picture>      <!-- sources  -->   </picture>    <button class="animate-button">Animate</button>  </div>

We can position the button on top of the image somewhere. This is just an arbitrary choice — you could put it wherever you want, or even have the entire image be tappable as long as you think you could explain that to users. Remember to only show the button when the same media query matches:

.picture-wrap .animate-button {   display: none; }  @media (prefers-reduced-motion: reduce) {   .picture-wrap .animate-button {      display: block;   } }

When the button is clicked (or tapped), we need to remove the media query to start the animation by downloading an animated source.

let button = document.querySelector(".animate-button");  button.addEventListener("click", () => {   const parent = button.closest(".picture-wrap");   const picture = parent.querySelector("picture");   picture.querySelector("source[media]").remove(); });

Here’s that in action:

See the Pen
Prefers Reduction Motion Technique PLUS!
by Chris Coyier (@chriscoyier)
on CodePen.

Maybe this is a good component?

We could automatically include the button, the button styling, and the button functionality with a web component. Hey, why not?

See the Pen
Prefers Reduction Motion Technique as Web Component
by Chris Coyier (@chriscoyier)
on CodePen.

The post Reduced Motion Picture Technique, Take Two appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

Reducing motion with the picture element

Here’s a bonafide CSS/HTML trick from Brad Frost and Dave Rupert where they use the <picture> element to switch out a GIF file with an image if the user has reduced motion enabled. This is how Brad goes about implementing that:

<picture>   <!-- This image will be loaded if the media query is true  -->   <source srcset="no-motion.jpg" media="(prefers-reduced-motion: reduce)"></source>    <!--  Otherwise, load this gif -->   <img srcset="animated.gif alt="brick wall"/> </picture>

How nifty is this? It makes me wonder if there are other ways this image-switching technique can be used besides accessibility and responsive images…

Also it’s worth noting that Eric Bailey wrote about the reduced motion media query a while back where he digs into its history and various approaches to use it.

Direct Link to ArticlePermalink

The post Reducing motion with the picture element appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]