Skip to content

Instantly share code, notes, and snippets.

@rgchris
Created March 25, 2026 12:46
Show Gist options
  • Select an option

  • Save rgchris/3d94601e5f4d8193a7cff85d40fd2bb5 to your computer and use it in GitHub Desktop.

Select an option

Save rgchris/3d94601e5f4d8193a7cff85d40fd2bb5 to your computer and use it in GitHub Desktop.
Whale (Animated SVG Example)

I came across this article, How Designers Should Think About SVG, and was taken by the little whale icon about a third of the way down showcasing the benefits of using SVG for animation. I noticed on clicking through to the CodePen editor that the animation portions were done in CSS. I was curious a) whether the animation could be done in pure SVG/SMIL, and b) whether my SVG module was up to the task of producing such a file in a streamlined fashion. Indeed they were, here is the result!

Some notes:

  • It's not well documented how the additive attribute is required to make multiple transform animations work on a single element. For each keyframe, additive="replace" is necessary to clear out any transform values active on the element, and additive="sum" is required on subsequent values so as not to clear out the prior one within the same keyframe.

  • The compounded values in the animate/animateTransform tags seem quite messy and further belie the idea that SVG is an XML format (it's a multitude of microformats wrapped in an XML envelope).

  • The keySplines attribute requires one less set of values than the values and keyTimes attributes. I'm not sure why, like the animation-timing-function CSS equivalent it can't extrapolate from one value.

  • The original CSS keyframes only required the intermediate stops (e.g. 50%) presumably taking the values for 0 and 100% from the initial attribute value(?). I wasn't able to get this to work, so I manually added these keyframes where needed. (I may be overlooking some way in which this and the additive attribute are supposed to work together.)

  • A general observation: I still don't feel that CSS and SVG go together all that well. SVG should have all the capabilities to do stuff like this without making it an SVG+CSS endeavour. In this case it does, though it can look awkward when pushed to the extreme.

  • I'm still developing the SVG module, though I believe some aspects are relatively intuitive. If it doesn't make creating SVG files in a more literate way that raw XML (or XML + CSS for that matter), then it needs further improvement.

Display the source blob
Display the rendered blob
Raw
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320" viewBox="0,0,320,320">
<!--
Monsieur Whale - Animating SVG Icon
This experiment uses ~CSS~ SVG Transforms and Animations on an SVG icon. These kinds
of techniques are presented in my icon design & SVG class on CreativeLive. To check
out the class or its resources, visit pnowell.com/resources
Animated and designed by: Peter Nowell
(note that the icon is currently enlarged for demonstration)
-->
<g transform="scale(4,4)" fill="none" stroke="#3279fd" stroke-linecap="round" stroke-linejoin="round">
<g>
<path d="M21.4 43.9c-.5 1.5-1.7 5-4.7 6.7c-3.1 1.8-4.9 1.1-8.4 1.5c5.9-5.8 3.9-8.8 6.6-11.3c-2.4-1.6-.5-3-1.9-6.5c1.9.1 5.5-1.4 6.7-.4c1.2 1 .8 2.4 1.4 4.4" transform-origin="14.835 42.795" transform="skewY(-15) translate(-.4,2.5) rotate(36)">
<animateTransform attributeName="transform" type="skewY" begin="1.5s" dur="4s" repeatCount="indefinite" values="-15;20;-15" keyTimes="0;.5;1" calcMode="spline" keySplines=".6,0,.4,1;.6,0,.4,1" additive="replace" />
<animateTransform attributeName="transform" type="translate" begin="1.5s" dur="4s" repeatCount="indefinite" values="-.4,2.5;.8,-4.3;-.4,2.5" keyTimes="0;.5;1" calcMode="spline" keySplines=".6,0,.4,1;.6,0,.4,1" additive="sum" />
<animateTransform attributeName="transform" type="rotate" begin="1.5s" dur="4s" repeatCount="indefinite" values="36;28;36" keyTimes="0;.5;1" calcMode="spline" keySplines=".6,0,.4,1;.6,0,.4,1" additive="sum" />
</path>
<path d="M78.5 46.1c-3 0-3.3 1.2-5 1.9c2-.6 4.5-.4 4.5-.4c-2.5 6.5-11.5 9.9-20.5 9.9c-4.4 0-7.2-.5-12.1-1.4" />
<path d="M46.7 56.4c-9-1.6-18.7-4.7-26.7-9.4" transform-origin="46.74 56.36" transform="rotate(-3)">
<animateTransform attributeName="transform" type="rotate" begin="1.5s" dur="4s" repeatCount="indefinite" values="-3;5;-3" keyTimes="0;.5;1" calcMode="spline" keySplines=".6,0,.4,1;.6,0,.4,1" additive="replace" />
</path>
<path d="M58.5 24.5c15 0 19 10 19 15c0 2 0 5.6 1 6.6" />
<path d="M21 42c13.5 0 20.5-17.5 37.5-17.5" transform-origin="58.5 24.5" transform="rotate(-3)">
<animateTransform attributeName="transform" type="rotate" begin="1.5s" dur="4s" repeatCount="indefinite" values="-3;2.5;-3" keyTimes="0;.5;1" calcMode="spline" keySplines=".6,0,.4,1;.6,0,.4,1" additive="replace" />
<animateTransform attributeName="transform" type="scale" begin="1.5s" dur="4s" repeatCount="indefinite" values="1,1;.92,1;1,1" keyTimes="0;.5;1" calcMode="spline" keySplines=".6,0,.4,1;.6,0,.4,1" additive="sum" />
</path>
<path d="M69.5 47.5c1.1 1.7 3.5.6 4.5.4" />
<path d="M52.5 48.6c-1.1 3.1-3.3 6.9-5.9 10c-2.6 3-7 4.4-12.2 2.5c-3.3-1.1-1.3-6 3.2-8.9c2.4-1.5 6.8-2.3 5.4-3.9" fill="#fff" transform-origin="48.544 48.32" stroke-width="1.2">
<animate attributeName="stroke-width" dur="12s" repeatCount="indefinite" values="1.2;1;1.2;1;1.1;1;1.1;1;1.1;1;1.1;1;1.1;1;1.1;1;1.1;1;1.1;1;1.2;1;1.2;1;1.2" keyTimes="0;.1;.2;.26;.27;.29;.3;.31;.33;.34;.36;.37;.39;.4;.41;.43;.45;.49;.52;.55;.6;.7;.8;.9;1" />
<animateTransform attributeName="transform" type="scale" dur="12s" repeatCount="indefinite" values="1 .7;1 1.2;1 .7;1 1.2;1 .85;1 1.2;1 .85;1 1.2;1 .85;1 1.2;1 .85;1 1.2;1 .85;1 1.2;1 .85;1 1.2;1 .85;1 1.2;1 .85;1 1.2;1 .7;1 1.2;1 .7;1 1.2;1 .7" keyTimes="0;.1;.2;.26;.27;.29;.3;.31;.33;.34;.36;.37;.39;.4;.41;.43;.45;.49;.52;.55;.6;.7;.8;.9;1" additive="replace" />
</path>
<circle cx="62.5" cy="42.5" r="4" />
<circle cx="64.25" cy="42.25" r="2.25" fill="#3279fd" stroke="none" transform-origin="62.5 42.5">
<animateTransform attributeName="transform" type="rotate" dur="12s" repeatCount="indefinite" values="5;5;100;10;-40;-50;20;5" keyTimes="0;.15;.25;.55;.6;.7;.85;1" calcMode="spline" keySplines=".6,0,.4,1;.6,0,.4,1;.6,0,.4,1;.6,0,.4,1;.6,0,.4,1;.6,0,.4,1;.6,0,.4,1" additive="replace" />
</circle>
<path d="M53 27.9c.8-1 2.5-.5 3-1" />
<animateTransform attributeName="transform" type="translate" dur="12s" repeatCount="indefinite" values="0,0;0,10;0,0;0,4;0,0" keyTimes="0;.3;.6;.8;1" calcMode="spline" keySplines=".6,0,.4,1;.6,0,.4,1;.6,0,.4,1;.6,0,.4,1" additive="replace" />
</g>
<g>
<path d="M54 27c-1-6-3-10.5-7.5-10.5c-3.6 0-4 3-4 3" stroke-dasharray="19.57">
<animate attributeName="stroke-dashoffset" dur="12s" repeatCount="indefinite" values="58.7125;58.7125;19.5708;19.5708" keyTimes="0;.62;.68;1" />
</path>
<path d="M53.5 27.5l0-9.5c0-9.5-2-14-6.5-14c-3 0-4.5 2.5-4.5 4.5c0 1.5 1 4 3.5 4c2 0 2.5-1.5 2.5-2.5c0-2.5-2.5-2.5-3-1.5" stroke-dasharray="48.2">
<animate attributeName="stroke-dashoffset" dur="12s" repeatCount="indefinite" values="144.6033;144.6033;48.2011;48.2011" keyTimes="0;.6;.7;1" />
</path>
<path d="M54.4 27.2c-.4-11.7 2.1-24.2-5.9-26.7" stroke-dasharray="29.06">
<animate attributeName="stroke-dashoffset" dur="12s" repeatCount="indefinite" values="87.1881;87.1881;29.0627;29.0627" keyTimes="0;.55;.65;1" />
</path>
<path d="M55.5 27c0 0 .5-12 1-17c.5-5 3-8.5 7-8.5c4 0 5 2 5 2" stroke-dasharray="34.79">
<animate attributeName="stroke-dashoffset" dur="12s" repeatCount="indefinite" values="104.382;104.382;34.794;34.794" keyTimes="0;.6;.68;1" />
</path>
<path d="M55.5 27c0 0 1-7 2-12.5c1-5.5 3.5-7 6-7c2.5 0 3.5 1.5 3.5 3c0 1.5-1 3-2.5 3c-1.5 0-2-1-2-1.5c0-1 .5-1.5 1-1.5" stroke-dasharray="37.06">
<animate attributeName="stroke-dashoffset" dur="12s" repeatCount="indefinite" values="111.19;111.19;37.0617;37.0617" keyTimes="0;.56;.72;1" />
</path>
<path d="M55.5 27c0-5.5 7-16 15-8" stroke-dasharray="22.1995">
<animate attributeName="stroke-dashoffset" dur="12s" repeatCount="indefinite" values="66.5985;66.5985;22.1995;22.1995" keyTimes="0;.56;.7;1" />
</path>
<path d="M55.5 27c0-5.5 6-8.5 10.5-6" stroke-dasharray="14.57">
<animate attributeName="stroke-dashoffset" dur="12s" repeatCount="indefinite" values="43.7218;43.7218;14.5739;14.5739" keyTimes="0;.62;.68;1" />
</path>
</g>
</g>
</svg>
Rebol [
Title: "Whale (Animated SVG Example)"
Author: "Christopher Ross-Gill"
Type: private
Needs: [
r3:rgchris:svg
]
Comment: [
https://medium.com/sketch-app-sources/how-designers-should-think-about-svg-b2b92efc4d77
https://codepen.io/pnowell/pen/QyZVro
"Original Whale Animation"
]
]
assets: #[
comment: {
Monsieur Whale - Animating SVG Icon
This experiment uses ~CSS~ SVG Transforms and Animations on an SVG icon. These kinds
of techniques are presented in my icon design & SVG class on CreativeLive. To check
out the class or its resources, visit pnowell.com/resources
Animated and designed by: Peter Nowell
(note that the icon is currently enlarged for demonstration)
}
color: #3279fd
paths: #[
tail: {
M21.4 43.9
C20.9 45.4 19.7 48.9 16.7 50.6
13.6 52.4 11.8 51.7 8.3 52.1
14.2 46.3 12.2 43.3 14.9 40.8
12.5 39.2 14.4 37.8 13 34.3
14.9 34.4 18.5 32.9 19.7 33.9
20.9 34.9 20.5 36.3 21.1 38.3
}
body-lower-right: {
M78.5 46.1
C75.5 46.1 75.2 47.3 73.5 48
75.5 47.4 78 47.6 78 47.6
75.5 54.1 66.5 57.5 57.5 57.5
53.1 57.5 50.3 57 45.4 56.1
}
body-lower-left: {
M46.7 56.4
C37.7 54.8 28 51.7 20 47
}
body-top-right: {
M58.5 24.5
C73.5 24.5 77.5 34.5 77.5 39.5
77.5 41.5 77.5 45.1 78.5 46.1
}
body-top-left: {
M21 42
C34.5 42 41.5 24.5 58.5 24.5
}
smile: {
M69.5 47.5
C70.6 49.2 73 48.1 74 47.9
}
flipper: {
M52.5 48.6
C51.4 51.7 49.2 55.5 46.6 58.6
44 61.6 39.6 63 34.4 61.1
31.1 60 33.1 55.1 37.6 52.2
40 50.7 44.4 49.9 43 48.3
}
eye: #[center: 62.5x42.5 radius: 4]
pupil: #[center: 64.25x42.25 radius: 2.25]
blow-hole: {
M53 27.9
C53.8 26.9 55.5 27.4 56 26.9
}
]
sprays: [
#[
path: {
M54 27
C53 21 51 16.5 46.5 16.5
42.9 16.5 42.5 19.5 42.5 19.5
}
length: 19.570466995239258
initial: 19.57
short: 19.5708
long: 58.7125
start: 62%
end: 68%
show?: yes
]
#[
path: {
M53.5 27.5
L53.5 18
C53.5 8.5 51.5 4 47 4
44 4 42.5 6.5 42.5 8.5
42.5 10 43.5 12.5 46 12.5
48 12.5 48.5 11 48.5 10
48.5 7.5 46 7.5 45.5 8.5
}
length: 48.20001983642578
initial: 48.2
short: 48.2011
long: 144.6033
start: 60%
end: 70%
show?: yes
]
#[
path: {
M54.4 27.2
C54 15.5 56.5 3 48.5 0.5
}
length: 29.062421798706055
initial: 29.06
short: 29.0627
long: 87.1881
start: 55%
end: 65%
show?: yes
]
#[
path: {
M55.5 27
C55.5 27 56 15 56.5 10
57 5 59.5 1.5 63.5 1.5
67.5 1.5 68.5 3.5 68.5 3.5
}
length: 34.793601989746094
initial: 34.79
short: 34.7940
long: 104.382
start: 60%
end: 68%
show?: yes
]
#[
path: {
M55.5 27
C55.5 27 56.5 20 57.5 14.5
58.5 9 61 7.5 63.5 7.5
66 7.5 67 9 67 10.5
67 12 66 13.5 64.5 13.5
63 13.5 62.5 12.5 62.5 12
62.5 11 63 10.5 63.5 10.5
}
length: 37.06087112426758
initial: 37.06
short: 37.0617
long: 111.19
start: 56%
end: 72%
show?: yes
]
#[
path: {
M55.5 27
C55.5 21.5 62.5 11 70.5 19
}
length: 22.199106216430664
initial: 22.1995
short: 22.1995
long: 66.598500000000001
start: 56%
end: 70%
show?: yes
]
#[
path: {
M55.5 27
C55.5 21.5 61.5 18.5 66 21
}
length: 14.573626518249512
initial: 14.57
short: 14.5739
long: 43.7218
start: 62%
end: 68%
show?: yes
]
]
flipper: #[
up: #[
stroke-width: 1.2
scale: 1x0.7
]
down: #[
stroke-width: 1
scale: 1x1.2
]
little-up: #[
stroke-width: 1.1
scale: 1x0.85
]
frames: #[
0% up
10% down
20% up
26% down
27% little-up
29% down
30% little-up
31% down
33% little-up
34% down
36% little-up
37% down
39% little-up
40% down
41% little-up
43% down
45% little-up
49% down
52% little-up
55% down
60% up
70% down
80% up
90% down
100% up
]
]
splines: [
.6 0 .4 1
]
]
drawing: svg/create 320x320 [
comment join newline trim/auto assets/comment
group #[
transform: [scale 4x4]
fill: none
stroke: (assets/color)
; class: "whale-container"
stroke-linecap: round
stroke-linejoin: round
][
animate group _ [
; Tail
;
animate path #[
transform-origin: 14.835x42.795
transform: [skewY -15 translate -.4 2.5 rotate 36]
]
assets/paths/tail
[
keyframes-for 'skewY 4 [
begin 1.5
splines assets/splines
at 0 -15
at 50% 20
at 100% -15
]
keyframes-for 'translate 4 [
begin 1.5
splines assets/splines
at 0 [-.4 2.5]
at 50% [.8 -4.3]
at 100% [-.4 2.5]
]
keyframes-for 'rotate 4 [
begin 1.5
splines assets/splines
at 0 36
at 50% 28
at 100% 36
]
]
; Main Body
;
path _ assets/paths/body-lower-right
animate path #[
transform-origin: 46.74x56.36
transform: [rotate -3]
]
assets/paths/body-lower-left
[
keyframes-for 'rotate 4 [
begin 1.5
splines assets/splines
at 0 -3
at 50% 5
at 1 -3
]
]
path _ assets/paths/body-top-right
animate path #[
transform-origin: 58.5x24.5
transform: [rotate -3]
]
assets/paths/body-top-left
[
keyframes-for 'rotate 4 [
begin 1.5
splines assets/splines
at 0 -3
at 50% 2.5
at 1 -3
]
keyframes-for 'scale 4 [
begin 1.5
splines assets/splines
at 0 [1 1]
at 50% [.92 1]
at 1 [1 1]
]
]
path _ assets/paths/smile
; Flipper
;
animate path #[
fill: #fff
transform-origin: 48.544x48.32
stroke-width: 1.2
]
assets/paths/flipper
[
for-each facet [stroke-width scale] [
keyframes-for :facet 12 [
for-each [time value] assets/flipper/frames [
at time assets/flipper/:value/:facet
]
]
]
]
; Eye
;
circle _ assets/paths/eye/center assets/paths/eye/radius
animate circle #[
fill: (assets/color)
stroke: none
transform-origin: 62.5x42.5
]
assets/paths/pupil/center
assets/paths/pupil/radius
[
keyframes-for 'rotate 12 [
splines assets/splines
at 0% 5
at 15% 5
at 25% 100
at 55% 10
at 60% -40
at 70% -50
at 85% 20
at 100% 5
]
]
; Blow Hole
;
path _ assets/paths/blow-hole
][
keyframes-for 'translate 12 [
splines assets/splines
at 0 [0 0]
at 30% [0 10]
at 60% [0 0]
at 80% [0 4]
at 100% [0 0]
]
]
; Spray
;
group _ [
for-each spray assets/sprays [
if spray/show? [
animate path #[
stroke-dasharray: (spray/initial)
stroke-dashoffset: (spray/long)
]
spray/path
[
keyframes-for 'stroke-dashoffset 12 [
at 0 spray/long
at spray/start spray/long
at spray/end spray/short
at 100% spray/short
]
]
]
]
]
]
]
encoding: svg/encode/pretty/precise drawing 0.0001
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment