This is from last year when I tried to recreate Rach Smith's P5.js animation using CSS. The animation seemed like it could be implemented in a breeze, but there's an illusion of simplicity. The process of manually updating the positions and directions of each box was a bit tricky.
After some effort, I realized that I only needed to track the positions of a single box in keyframes,
the rest would follow and be evenly separated through different values of animation-delay.
Negative delays were used to start the animation immediately.
.box {
/* ... */
position: absolute;
animation: move 10s linear infinite;
animation-delay: calc(var(--index) / var(--total) * -10s);
}
@keyframes move {
0%, 100% { left: 25%; top: 25% }
25% { left: 75%; top: 25% }
50% { left: 75%; top: 75% }
75% { left: 25%; top: 75% }
}
A few days later, I found a simpler method and got it working
with the offset-path property.
The inset() function would create a square path and then the boxes would move along with it
by animating the offset-distance values.
I was excited because I had previously thought that offset-path only supported SVG paths.
.box {
/* ... */
offset-path: inset(25%);
animation: move 10s linear infinite;
animation-delay: calc(var(--index) / var(--total) * -10s);
}
@keyframes move {
to {
offset-distance: 100%;
}
}Linear scale
Since the boxes were evenly spaced,
the animation-delay values needed to be equally distributed proportionally according to each box's index.
The calculation process is referred to as linear scaling, or a simplified form of linear interpolation.
/* -0.5s, -1s, -1.5s, -2s, ... , -9.5s, -10s */
var(--index) / var(--total) * -10s
Browers are now shipping tree counting functions,
so that the predefined variables can be replaced with native
sibling-index() and sibling-count() functions.
sibling-index() / sibling-count() * -10sLinear scaling has many use cases, such as creating a range of opacities or colors.
opacity: calc(sibling-index() / sibling-count())
background: hsl(
calc(sibling-index() / sibling-count() * 360deg),
80%, 50%
)
Compared to calc() expressions, the @iI() function
in css-doodle provides a more concise and readable way to achieve the same result.
/* opacity: calc(sibling-index() / sibling-count()) */
opacity: @iI()
/* animation-delay: calc(sibling-index() / sibling-count() * -10s) */
animation-delay: @iI(*-10s)Rewrite with css-doodle
When using css-doodle, the code becomes even simpler.
The function @t can replace both animation and keyframes,
as explained in the post Time-based CSS Animations.
offset-distance: @t(/100%, +@iI(*100%));Below is the complete code used for the animation. (CodePen)
@grid: 20x1 / 240px / #f2f0e9 ß2;
@size: 6% auto 1;
border: solid 2px hsl(
@iI(*360deg), 100%, 34%
);
offset-distance: @t(/100%, +@iI(*100%));
offset-path: inset(25%);
Note that the inset() function supports a round keyword,
adding a rounded corner to the path will get the boxes a subtle flipping effect as they turn the corners.
/* ... */
offset-path: inset(25% round 1%);Offset-path with polygon()
I recently discovered that offset-path accepts polygon() and other shape functions.
I can't believe I hadn't tried it earlier -- there are so many shapes
that polygon() can create and apply to offset-path,
some of them may result in interesting animations.
The tool css-doodle also offers the @shape function to generate polygons with ease.
/* heart */
offset-path: @shape(heart);
/* hexagon */
offset-path: @shape(hexagon);
/* custom shapes */
offset-path: @shape(
points: 12;
r: seq(.5, 1);
)
A trailing effect will be produced when making a slight change to the offset-distance ratio.
(CodePen)
/* ... */
@size: @iI(*5%);
opacity: @iI();
offset-distance: @t(/50%, +@iI(*8%));
offset-path: @shape(
points: 180;
scale: .012;
r: 50*abs.sin(2.5t)-81;
);Another example of custom shapes with polygons. The performance is not as good as in canvas, but it's still fun to play with. (CodePen)
@grid: 20x1 / 240px / #051d1b;
@place: center;
@size: @iI(*50%, +50%);
filter: drop-shadow(0 0 5px #9bff9b);
background: @doodle(
@grid: 20x1 / 100%;
@size: @r(1%) @r(2%);
position: absolute;
background: @p(#93ffe4, #84ff92);
offset-rotate: auto 90deg;
offset-distance: @t(
/@r(50%, 500%),
+@iI(*100%)
);
offset-path: @shape(
points: 180;
r: cos(4t)
);
);Beesandbombs's spiral animation
I once saw a cool animation made by @beesandbombs.
I couldn't find a simple way to recreate it with CSS before,
but now it's possible to do it with offset-path.
Let's start by creating polygons with sides ranging from 3 to 13 and place them in the background.
@grid: 1 / 240px / #f5f5f5 @svg(
viewBox: -5 -5 10 10;
fill: none;
stroke-width: .035;
stroke: #000;
polygon*13 {
points: @Plot(
points: @n2;
r: @nN(*3.2);
rotate: @n(*90, /@n2);
);
}
);In the original animation, the length of each polygon side is the same, so the corresponding r should be adjusted based on the fixed side length.
@grid: 1 / 240px #f5f5f5 @svg(
viewBox: -5 -5 10 10;
fill: none;
stroke-width: .035;
stroke: #000;
polygon*13 {
points: @Plot(
points: @n2;
r: $(.7 / cos(@n(*π, /@n2, /2)));
rotate: @n(*90, /@n2);
);
}
);The painting order in SVG is determined by the order of the elements, after adding colors to the polygons, the smaller polygons appear to be covered by the larger ones. Therefore, the order of the polygons needs to be reversed.
@grid: 1 / 240px / #fff @svg(
viewBox: -5 -5 10 10;
fill: none;
stroke-width: .035;
polygon*13-1 {
stroke: hsl(@nN(*360), 80%, 50%);
points: @Plot(
points: @n2;
r: $(.7 / cos(@n(*π, /@n2, /2)));
rotate: @n(*90, /@n2);
);
}
);
The final step is to add small dots and create identical offset paths corresponding to the polygons in the background,
and then animate them using offset-distance.
(CodePen)
@grid: 1x13 / 240px / #f5f5f5 @svg(
viewBox: -5 -5 10 10;
fill: none;
stroke-width: .035;
polygon*13-1 {
stroke: hsl(@nN(*360), 80%, 50%);
points: @Plot(
points: @n2;
r: $(.7 / cos(@n(*π, /@n2, /2)));
rotate: @n(*90, /@n2);
);
}
);
@size: 2% auto 1;
border-radius: 50%;
background: #000;
offset-path: @shape(
points: @i2;
turn: -1;
r: $(.14 / cos(@i(*π, /@i2, /2)));
rotate: @i(*90, /@i2);
);
offset-distance: calc(
240px/@I3 + @t(/160%, *@I2(-@i))
);Another puzzle solved, just like every time, it's been a lot of fun :)