feat(snippets): add motion-path category (5)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Showing 5 changed files with +463 and −0
added
snippets/motion-path/bee-flight.html
+96 −0
@@ -0,0 +1,96 @@ | ||
| 1 | +<!-- | |
| 2 | + ============================================================ | |
| 3 | + SVGarden — https://www.svgarden.dev | |
| 4 | + Author : Simon-Pierre Boucher | |
| 5 | + Contact: contact@spboucher.ai | |
| 6 | + File : snippets/motion-path/bee-flight.html | |
| 7 | + Desc : Erratic bee flight — darts and hovers paced with keyPoints/keyTimes | |
| 8 | + ============================================================ | |
| 9 | +--> | |
| 10 | +<!--svgarden | |
| 11 | +title: Bee flight | |
| 12 | +slug: bee-flight | |
| 13 | +category: motion-path | |
| 14 | +tags: [motion, smil, bee, pacing] | |
| 15 | +difficulty: advanced | |
| 16 | +techniques: [animateMotion, keyPoints, keyTimes, rotate-auto] | |
| 17 | +how_it_works: > | |
| 18 | + A plain <animateMotion> moves at constant speed, which reads as mechanical — | |
| 19 | + real insects dart and hover. keyPoints/keyTimes decouple distance from time: | |
| 20 | + keyPoints says how far along the path (0–1) the bee is at each keyTimes | |
| 21 | + moment, so a big keyPoints jump inside a small keyTimes slice is a dart, and | |
| 22 | + a near-zero jump over a long slice is a hover at a flower. calcMode="linear" | |
| 23 | + keeps each segment steady so the contrast does the acting. The wings are a | |
| 24 | + separate 90 ms CSS flutter — two timescales layered on one element sell the | |
| 25 | + illusion. | |
| 26 | +customizable: | |
| 27 | + - { var: "--sg-color", label: "Bee", type: color, default: "#EFBB33" } | |
| 28 | + - { var: "--sg-flower", label: "Flowers", type: color, default: "#7F77DD" } | |
| 29 | + - { var: "--sg-size", label: "Size", type: range, min: 160, max: 440, default: 260, unit: px } | |
| 30 | +created: 2026-08-10 | |
| 31 | +--> | |
| 32 | +<div class="sg-bee-flight" style="--sg-color:#EFBB33; --sg-flower:#7F77DD; --sg-size:260px;"> | |
| 33 | + <svg viewBox="0 0 200 130" aria-hidden="true" focusable="false"> | |
| 34 | + <path id="sg-bee-flight-route" class="sg-bee-flight-route" | |
| 35 | + d="M 20 100 C 40 60, 70 40, 96 52 C 118 62, 118 84, 100 92 C 130 98, 160 84, 172 56 C 178 42, 160 30, 140 38 C 100 26, 46 34, 20 100" /> | |
| 36 | + <g class="sg-bee-flight-flower"><circle cx="97" cy="55" r="4" /><circle cx="171" cy="55" r="4" /></g> | |
| 37 | + <g class="sg-bee-flight-bee"> | |
| 38 | + <g class="sg-bee-flight-wings"> | |
| 39 | + <ellipse cx="-1.5" cy="-4.5" rx="3" ry="4" /> | |
| 40 | + <ellipse cx="2.5" cy="-4.5" rx="3" ry="4" /> | |
| 41 | + </g> | |
| 42 | + <ellipse class="sg-bee-flight-body" cx="0" cy="0" rx="6" ry="4" /> | |
| 43 | + <path class="sg-bee-flight-stripes" d="M-2 -3.8 V3.8 M2 -3.8 V3.8" /> | |
| 44 | + <circle class="sg-bee-flight-eye" cx="5" cy="-1.2" r="0.9" /> | |
| 45 | + <animateMotion | |
| 46 | + dur="9s" repeatCount="indefinite" rotate="auto" calcMode="linear" | |
| 47 | + keyPoints="0; 0.3; 0.34; 0.62; 0.66; 0.9; 1" | |
| 48 | + keyTimes="0; 0.1; 0.42; 0.52; 0.82; 0.92; 1"> | |
| 49 | + <mpath href="#sg-bee-flight-route" /> | |
| 50 | + </animateMotion> | |
| 51 | + </g> | |
| 52 | + </svg> | |
| 53 | +</div> | |
| 54 | +<style> | |
| 55 | + .sg-bee-flight { | |
| 56 | + width: var(--sg-size); | |
| 57 | + } | |
| 58 | + .sg-bee-flight svg { | |
| 59 | + display: block; | |
| 60 | + width: 100%; | |
| 61 | + } | |
| 62 | + .sg-bee-flight-route { | |
| 63 | + fill: none; | |
| 64 | + stroke: rgba(128, 128, 128, 0.3); | |
| 65 | + stroke-width: 1; | |
| 66 | + stroke-dasharray: 1.5 4; | |
| 67 | + } | |
| 68 | + .sg-bee-flight-flower circle { | |
| 69 | + fill: var(--sg-flower); | |
| 70 | + opacity: 0.8; | |
| 71 | + } | |
| 72 | + .sg-bee-flight-body { | |
| 73 | + fill: var(--sg-color); | |
| 74 | + } | |
| 75 | + .sg-bee-flight-stripes { | |
| 76 | + stroke: rgba(40, 40, 40, 0.75); | |
| 77 | + stroke-width: 1.4; | |
| 78 | + fill: none; | |
| 79 | + } | |
| 80 | + .sg-bee-flight-eye { | |
| 81 | + fill: rgba(40, 40, 40, 0.75); | |
| 82 | + } | |
| 83 | + .sg-bee-flight-wings ellipse { | |
| 84 | + fill: rgba(128, 128, 128, 0.4); | |
| 85 | + transform-box: fill-box; | |
| 86 | + transform-origin: center bottom; | |
| 87 | + animation: sg-bee-flight-flutter 0.09s ease-in-out infinite alternate; | |
| 88 | + } | |
| 89 | + .sg-bee-flight-wings ellipse:nth-of-type(2) { | |
| 90 | + animation-delay: 0.045s; | |
| 91 | + } | |
| 92 | + @keyframes sg-bee-flight-flutter { | |
| 93 | + from { transform: scaleY(1); } | |
| 94 | + to { transform: scaleY(0.45) rotate(8deg); } | |
| 95 | + } | |
| 96 | +</style> | |
added
snippets/motion-path/comet-trail.html
+90 −0
@@ -0,0 +1,90 @@ | ||
| 1 | +<!-- | |
| 2 | + ============================================================ | |
| 3 | + SVGarden — https://www.svgarden.dev | |
| 4 | + Author : Simon-Pierre Boucher | |
| 5 | + Contact: contact@spboucher.ai | |
| 6 | + File : snippets/motion-path/comet-trail.html | |
| 7 | + Desc : Comet sweeping an arc with a fading clone-particle tail | |
| 8 | + ============================================================ | |
| 9 | +--> | |
| 10 | +<!--svgarden | |
| 11 | +title: Comet trail | |
| 12 | +slug: comet-trail | |
| 13 | +category: motion-path | |
| 14 | +tags: [motion, smil, comet, particles] | |
| 15 | +difficulty: intermediate | |
| 16 | +techniques: [animateMotion, begin-offset-clones, opacity-falloff, mpath] | |
| 17 | +how_it_works: > | |
| 18 | + The tail is not drawn — it is five clones of the head flying the exact same | |
| 19 | + <animateMotion> route, each with a slightly less negative begin value, so | |
| 20 | + every clone runs a fixed fraction of a second behind the one before it. | |
| 21 | + Negative begins matter: they mean every particle is already mid-flight on | |
| 22 | + the first frame, so nothing pops in from the SVG origin. Shrinking radii and | |
| 23 | + falling opacity down the chain turn discrete circles into a convincing | |
| 24 | + streak, and two CSS-twinkling stars give the sky depth for almost free. | |
| 25 | +customizable: | |
| 26 | + - { var: "--sg-color", label: "Comet", type: color, default: "#7F77DD" } | |
| 27 | + - { var: "--sg-size", label: "Size", type: range, min: 160, max: 460, default: 280, unit: px } | |
| 28 | +created: 2026-08-10 | |
| 29 | +--> | |
| 30 | +<div class="sg-comet-trail" style="--sg-color:#7F77DD; --sg-size:280px;"> | |
| 31 | + <svg viewBox="0 0 200 110" aria-hidden="true" focusable="false"> | |
| 32 | + <path id="sg-comet-trail-arc" class="sg-comet-trail-arc" d="M -12 96 C 40 18, 130 6, 214 58" /> | |
| 33 | + <path class="sg-comet-trail-star" d="M40 78 l1.4 3 3 1.4 -3 1.4 -1.4 3 -1.4 -3 -3 -1.4 3 -1.4 Z" /> | |
| 34 | + <path class="sg-comet-trail-star sg-comet-trail-star2" d="M156 84 l1.2 2.6 2.6 1.2 -2.6 1.2 -1.2 2.6 -1.2 -2.6 -2.6 -1.2 2.6 -1.2 Z" /> | |
| 35 | + <circle class="sg-comet-trail-p5" r="0.8"> | |
| 36 | + <animateMotion dur="4s" begin="0s" repeatCount="indefinite"><mpath href="#sg-comet-trail-arc" /></animateMotion> | |
| 37 | + </circle> | |
| 38 | + <circle class="sg-comet-trail-p4" r="1.3"> | |
| 39 | + <animateMotion dur="4s" begin="-0.1s" repeatCount="indefinite"><mpath href="#sg-comet-trail-arc" /></animateMotion> | |
| 40 | + </circle> | |
| 41 | + <circle class="sg-comet-trail-p3" r="1.9"> | |
| 42 | + <animateMotion dur="4s" begin="-0.2s" repeatCount="indefinite"><mpath href="#sg-comet-trail-arc" /></animateMotion> | |
| 43 | + </circle> | |
| 44 | + <circle class="sg-comet-trail-p2" r="2.6"> | |
| 45 | + <animateMotion dur="4s" begin="-0.3s" repeatCount="indefinite"><mpath href="#sg-comet-trail-arc" /></animateMotion> | |
| 46 | + </circle> | |
| 47 | + <circle class="sg-comet-trail-halo" r="7"> | |
| 48 | + <animateMotion dur="4s" begin="-0.4s" repeatCount="indefinite"><mpath href="#sg-comet-trail-arc" /></animateMotion> | |
| 49 | + </circle> | |
| 50 | + <circle class="sg-comet-trail-head" r="4"> | |
| 51 | + <animateMotion dur="4s" begin="-0.4s" repeatCount="indefinite"><mpath href="#sg-comet-trail-arc" /></animateMotion> | |
| 52 | + </circle> | |
| 53 | + </svg> | |
| 54 | +</div> | |
| 55 | +<style> | |
| 56 | + .sg-comet-trail { | |
| 57 | + width: var(--sg-size); | |
| 58 | + } | |
| 59 | + .sg-comet-trail svg { | |
| 60 | + display: block; | |
| 61 | + width: 100%; | |
| 62 | + } | |
| 63 | + .sg-comet-trail-arc { | |
| 64 | + fill: none; | |
| 65 | + stroke: rgba(128, 128, 128, 0.18); | |
| 66 | + stroke-width: 1; | |
| 67 | + stroke-dasharray: 3 6; | |
| 68 | + } | |
| 69 | + .sg-comet-trail circle { | |
| 70 | + fill: var(--sg-color); | |
| 71 | + } | |
| 72 | + .sg-comet-trail-head { opacity: 1; } | |
| 73 | + .sg-comet-trail-halo { opacity: 0.18; } | |
| 74 | + .sg-comet-trail-p2 { opacity: 0.6; } | |
| 75 | + .sg-comet-trail-p3 { opacity: 0.4; } | |
| 76 | + .sg-comet-trail-p4 { opacity: 0.25; } | |
| 77 | + .sg-comet-trail-p5 { opacity: 0.12; } | |
| 78 | + .sg-comet-trail-star { | |
| 79 | + fill: var(--sg-color); | |
| 80 | + opacity: 0.5; | |
| 81 | + animation: sg-comet-trail-twinkle 2.6s ease-in-out infinite; | |
| 82 | + } | |
| 83 | + .sg-comet-trail-star2 { | |
| 84 | + animation-delay: -1.3s; | |
| 85 | + } | |
| 86 | + @keyframes sg-comet-trail-twinkle { | |
| 87 | + 0%, 100% { opacity: 0.5; transform: none; } | |
| 88 | + 50% { opacity: 0.1; } | |
| 89 | + } | |
| 90 | +</style> | |
added
snippets/motion-path/orbit-planets.html
+111 −0
@@ -0,0 +1,111 @@ | ||
| 1 | +<!-- | |
| 2 | + ============================================================ | |
| 3 | + SVGarden — https://www.svgarden.dev | |
| 4 | + Author : Simon-Pierre Boucher | |
| 5 | + Contact: contact@spboucher.ai | |
| 6 | + File : snippets/motion-path/orbit-planets.html | |
| 7 | + Desc : Mini solar system — planets on elliptical CSS offset-path orbits | |
| 8 | + ============================================================ | |
| 9 | +--> | |
| 10 | +<!--svgarden | |
| 11 | +title: Orbiting planets | |
| 12 | +slug: orbit-planets | |
| 13 | +category: motion-path | |
| 14 | +tags: [motion, orbit, planets, offset-path] | |
| 15 | +difficulty: advanced | |
| 16 | +techniques: [offset-path, offset-distance, "@supports", nested-transforms] | |
| 17 | +how_it_works: > | |
| 18 | + Each planet is pinned to an invisible ellipse with offset-path: path('…') and | |
| 19 | + flown around it by animating a single property, offset-distance, from 0% to | |
| 20 | + 100% — the browser handles all the trigonometry. The three orbits share one | |
| 21 | + keyframe block and differ only in animation-duration, so their alignments | |
| 22 | + drift like a real system. The moon is a nested transform: a carrier group | |
| 23 | + rotating about its local origin, which offset-path has already parked on the | |
| 24 | + planet. Everything sits inside an @supports guard, so browsers without | |
| 25 | + offset-path simply show the planets resting on their rails. | |
| 26 | +support: > | |
| 27 | + CSS offset-path on SVG children needs Chrome 116+, Safari 16+ or Firefox 122+. | |
| 28 | + Unsupported browsers show a static system (planets resting on their orbit | |
| 29 | + rails); the moon keeps spinning either way. | |
| 30 | +customizable: | |
| 31 | + - { var: "--sg-sun", label: "Sun", type: color, default: "#EFBB33" } | |
| 32 | + - { var: "--sg-color", label: "Planets", type: color, default: "#7F77DD" } | |
| 33 | + - { var: "--sg-size", label: "Size", type: range, min: 140, max: 420, default: 240, unit: px } | |
| 34 | + - { var: "--sg-duration", label: "Year", type: range, min: 2, max: 14, step: 0.5, default: 6, unit: s } | |
| 35 | +created: 2026-08-10 | |
| 36 | +--> | |
| 37 | +<div class="sg-orbit-planets" style="--sg-sun:#EFBB33; --sg-color:#7F77DD; --sg-size:240px; --sg-duration:6s;"> | |
| 38 | + <svg viewBox="0 0 200 140" aria-hidden="true" focusable="false"> | |
| 39 | + <ellipse class="sg-orbit-planets-rail" cx="100" cy="70" rx="38" ry="20" /> | |
| 40 | + <ellipse class="sg-orbit-planets-rail" cx="100" cy="70" rx="64" ry="36" /> | |
| 41 | + <ellipse class="sg-orbit-planets-rail" cx="100" cy="70" rx="88" ry="52" /> | |
| 42 | + <circle class="sg-orbit-planets-sun" cx="100" cy="70" r="9" /> | |
| 43 | + <circle class="sg-orbit-planets-p sg-orbit-planets-p1" r="3.4" /> | |
| 44 | + <g class="sg-orbit-planets-p sg-orbit-planets-p2"> | |
| 45 | + <circle class="sg-orbit-planets-body" r="4.6" /> | |
| 46 | + <g class="sg-orbit-planets-moon-orbit"> | |
| 47 | + <circle class="sg-orbit-planets-moon" cx="8.5" cy="0" r="1.6" /> | |
| 48 | + </g> | |
| 49 | + </g> | |
| 50 | + <circle class="sg-orbit-planets-p sg-orbit-planets-p3" r="2.6" /> | |
| 51 | + </svg> | |
| 52 | +</div> | |
| 53 | +<style> | |
| 54 | + .sg-orbit-planets { | |
| 55 | + width: var(--sg-size); | |
| 56 | + } | |
| 57 | + .sg-orbit-planets svg { | |
| 58 | + display: block; | |
| 59 | + width: 100%; | |
| 60 | + } | |
| 61 | + .sg-orbit-planets-rail { | |
| 62 | + fill: none; | |
| 63 | + stroke: rgba(128, 128, 128, 0.35); | |
| 64 | + stroke-width: 1; | |
| 65 | + stroke-dasharray: 2 3; | |
| 66 | + } | |
| 67 | + .sg-orbit-planets-sun { | |
| 68 | + fill: var(--sg-sun); | |
| 69 | + } | |
| 70 | + .sg-orbit-planets-p, | |
| 71 | + .sg-orbit-planets-body { | |
| 72 | + fill: var(--sg-color); | |
| 73 | + } | |
| 74 | + .sg-orbit-planets-p1 { opacity: 0.9; transform: translate(62px, 70px); } | |
| 75 | + .sg-orbit-planets-p2 { transform: translate(164px, 70px); } | |
| 76 | + .sg-orbit-planets-p3 { opacity: 0.55; transform: translate(100px, 122px); } | |
| 77 | + .sg-orbit-planets-moon { | |
| 78 | + fill: var(--sg-color); | |
| 79 | + opacity: 0.7; | |
| 80 | + } | |
| 81 | + .sg-orbit-planets-moon-orbit { | |
| 82 | + transform-origin: 0 0; | |
| 83 | + animation: sg-orbit-planets-spin 2.2s linear infinite; | |
| 84 | + } | |
| 85 | + @supports (offset-path: path('M0 0 L1 1')) { | |
| 86 | + .sg-orbit-planets-p { | |
| 87 | + transform: none; | |
| 88 | + offset-rotate: 0deg; | |
| 89 | + animation: sg-orbit-planets-fly var(--sg-duration) linear infinite; | |
| 90 | + } | |
| 91 | + .sg-orbit-planets-p1 { | |
| 92 | + offset-path: path('M62 70 a38 20 0 1 1 76 0 a38 20 0 1 1 -76 0'); | |
| 93 | + } | |
| 94 | + .sg-orbit-planets-p2 { | |
| 95 | + offset-path: path('M36 70 a64 36 0 1 1 128 0 a64 36 0 1 1 -128 0'); | |
| 96 | + animation-duration: calc(var(--sg-duration) * 1.9); | |
| 97 | + animation-delay: calc(var(--sg-duration) * -0.7); | |
| 98 | + } | |
| 99 | + .sg-orbit-planets-p3 { | |
| 100 | + offset-path: path('M12 70 a88 52 0 1 1 176 0 a88 52 0 1 1 -176 0'); | |
| 101 | + animation-duration: calc(var(--sg-duration) * 3.1); | |
| 102 | + animation-delay: calc(var(--sg-duration) * -1.4); | |
| 103 | + } | |
| 104 | + } | |
| 105 | + @keyframes sg-orbit-planets-fly { | |
| 106 | + to { offset-distance: 100%; } | |
| 107 | + } | |
| 108 | + @keyframes sg-orbit-planets-spin { | |
| 109 | + to { transform: rotate(360deg); } | |
| 110 | + } | |
| 111 | +</style> | |
added
snippets/motion-path/paper-plane-loop.html
+68 −0
@@ -0,0 +1,68 @@ | ||
| 1 | +<!-- | |
| 2 | + ============================================================ | |
| 3 | + SVGarden — https://www.svgarden.dev | |
| 4 | + Author : Simon-Pierre Boucher | |
| 5 | + Contact: contact@spboucher.ai | |
| 6 | + File : snippets/motion-path/paper-plane-loop.html | |
| 7 | + Desc : Paper plane flying a loop-the-loop, banking into the turns | |
| 8 | + ============================================================ | |
| 9 | +--> | |
| 10 | +<!--svgarden | |
| 11 | +title: Paper plane loop | |
| 12 | +slug: paper-plane-loop | |
| 13 | +category: motion-path | |
| 14 | +tags: [motion, smil, plane, loop] | |
| 15 | +difficulty: advanced | |
| 16 | +techniques: [animateMotion, rotate-auto, mpath, stroke-dashoffset] | |
| 17 | +how_it_works: > | |
| 18 | + SMIL's <animateMotion> drags the plane along a bézier flight path, and the | |
| 19 | + one attribute doing the heavy lifting is rotate="auto": it continuously | |
| 20 | + re-orients the element to the path's tangent, so the plane banks into the | |
| 21 | + loop and exits nose-first instead of sliding sideways like a fridge magnet. | |
| 22 | + The <mpath> child references the same path that is drawn as the dashed rail, | |
| 23 | + so flight and artwork can never drift apart. A slow dashoffset animation on | |
| 24 | + the rail suggests the airflow direction. Because the plane is drawn pointing | |
| 25 | + along +x, "auto" needs no extra angle correction. | |
| 26 | +customizable: | |
| 27 | + - { var: "--sg-color", label: "Plane", type: color, default: "#7F77DD" } | |
| 28 | + - { var: "--sg-size", label: "Width", type: range, min: 160, max: 460, default: 280, unit: px } | |
| 29 | +created: 2026-08-10 | |
| 30 | +--> | |
| 31 | +<div class="sg-paper-plane-loop" style="--sg-color:#7F77DD; --sg-size:280px;"> | |
| 32 | + <svg viewBox="0 0 200 120" aria-hidden="true" focusable="false"> | |
| 33 | + <path id="sg-paper-plane-loop-path" class="sg-paper-plane-loop-rail" | |
| 34 | + d="M 10 82 C 40 58, 68 46, 96 54 C 128 64, 132 96, 110 94 C 88 92, 90 62, 116 50 C 138 40, 166 44, 192 58" /> | |
| 35 | + <g class="sg-paper-plane-loop-plane"> | |
| 36 | + <path d="M2 0 L-9 5 L-5.5 0 L-9 -5 Z" /> | |
| 37 | + <animateMotion dur="5s" repeatCount="indefinite" rotate="auto"> | |
| 38 | + <mpath href="#sg-paper-plane-loop-path" /> | |
| 39 | + </animateMotion> | |
| 40 | + </g> | |
| 41 | + </svg> | |
| 42 | +</div> | |
| 43 | +<style> | |
| 44 | + .sg-paper-plane-loop { | |
| 45 | + width: var(--sg-size); | |
| 46 | + } | |
| 47 | + .sg-paper-plane-loop svg { | |
| 48 | + display: block; | |
| 49 | + width: 100%; | |
| 50 | + } | |
| 51 | + .sg-paper-plane-loop-rail { | |
| 52 | + fill: none; | |
| 53 | + stroke: rgba(128, 128, 128, 0.4); | |
| 54 | + stroke-width: 1.4; | |
| 55 | + stroke-linecap: round; | |
| 56 | + stroke-dasharray: 5 7; | |
| 57 | + animation: sg-paper-plane-loop-wake 1.6s linear infinite; | |
| 58 | + } | |
| 59 | + .sg-paper-plane-loop-plane path { | |
| 60 | + fill: var(--sg-color); | |
| 61 | + stroke: var(--sg-color); | |
| 62 | + stroke-width: 1; | |
| 63 | + stroke-linejoin: round; | |
| 64 | + } | |
| 65 | + @keyframes sg-paper-plane-loop-wake { | |
| 66 | + to { stroke-dashoffset: -12; } | |
| 67 | + } | |
| 68 | +</style> | |
added
snippets/motion-path/train-track.html
+98 −0
@@ -0,0 +1,98 @@ | ||
| 1 | +<!-- | |
| 2 | + ============================================================ | |
| 3 | + SVGarden — https://www.svgarden.dev | |
| 4 | + Author : Simon-Pierre Boucher | |
| 5 | + Contact: contact@spboucher.ai | |
| 6 | + File : snippets/motion-path/train-track.html | |
| 7 | + Desc : Toy train following its track, wheels spinning, car in tow | |
| 8 | + ============================================================ | |
| 9 | +--> | |
| 10 | +<!--svgarden | |
| 11 | +title: Train on track | |
| 12 | +slug: train-track | |
| 13 | +category: motion-path | |
| 14 | +tags: [motion, smil, train, mpath] | |
| 15 | +difficulty: intermediate | |
| 16 | +techniques: [animateMotion, mpath, begin-offset, transform-box] | |
| 17 | +how_it_works: > | |
| 18 | + One <path> is the single source of truth: it is drawn twice as artwork | |
| 19 | + (dashed sleepers under a solid rail) and referenced twice via <mpath> as the | |
| 20 | + motion route, so the train can never leave its own track. The engine and the | |
| 21 | + car run identical animateMotion timings, but the car starts with a negative | |
| 22 | + begin offset, keeping it a fixed phase behind — that's how you couple | |
| 23 | + carriages without maths. rotate="auto" turns both into the curves, while the | |
| 24 | + wheels spin on their own axes thanks to transform-box: fill-box, which makes | |
| 25 | + each wheel rotate around its own center instead of the SVG origin. | |
| 26 | +customizable: | |
| 27 | + - { var: "--sg-color", label: "Train", type: color, default: "#7F77DD" } | |
| 28 | + - { var: "--sg-car", label: "Car", type: color, default: "#EFBB33" } | |
| 29 | + - { var: "--sg-size", label: "Size", type: range, min: 160, max: 440, default: 260, unit: px } | |
| 30 | +created: 2026-08-10 | |
| 31 | +--> | |
| 32 | +<div class="sg-train-track" style="--sg-color:#7F77DD; --sg-car:#EFBB33; --sg-size:260px;"> | |
| 33 | + <svg viewBox="0 0 200 130" aria-hidden="true" focusable="false"> | |
| 34 | + <path id="sg-train-track-rail" class="sg-train-track-sleepers" | |
| 35 | + d="M 45 25 H 155 A 28 28 0 0 1 183 53 V 77 A 28 28 0 0 1 155 105 H 45 A 28 28 0 0 1 17 77 V 53 A 28 28 0 0 1 45 25 Z" /> | |
| 36 | + <use href="#sg-train-track-rail" class="sg-train-track-rails" /> | |
| 37 | + <g class="sg-train-track-car"> | |
| 38 | + <rect x="-8" y="-8" width="16" height="6.5" rx="1.5" /> | |
| 39 | + <g class="sg-train-track-wheel"><circle cx="-4" cy="0" r="2.4" /><path d="M-4 -2.4 V2.4 M-6.4 0 H-1.6" /></g> | |
| 40 | + <g class="sg-train-track-wheel"><circle cx="4" cy="0" r="2.4" /><path d="M4 -2.4 V2.4 M1.6 0 H6.4" /></g> | |
| 41 | + <animateMotion dur="10s" begin="-9.75s" repeatCount="indefinite" rotate="auto"> | |
| 42 | + <mpath href="#sg-train-track-rail" /> | |
| 43 | + </animateMotion> | |
| 44 | + </g> | |
| 45 | + <g class="sg-train-track-engine"> | |
| 46 | + <rect x="-9" y="-8" width="13" height="7" rx="1.5" /> | |
| 47 | + <rect x="3" y="-11" width="7" height="10" rx="1.5" /> | |
| 48 | + <rect x="-7" y="-11.5" width="3" height="4" rx="1" /> | |
| 49 | + <g class="sg-train-track-wheel"><circle cx="-5" cy="0" r="2.4" /><path d="M-5 -2.4 V2.4 M-7.4 0 H-2.6" /></g> | |
| 50 | + <g class="sg-train-track-wheel"><circle cx="5" cy="0" r="2.4" /><path d="M5 -2.4 V2.4 M2.6 0 H7.4" /></g> | |
| 51 | + <animateMotion dur="10s" repeatCount="indefinite" rotate="auto"> | |
| 52 | + <mpath href="#sg-train-track-rail" /> | |
| 53 | + </animateMotion> | |
| 54 | + </g> | |
| 55 | + </svg> | |
| 56 | +</div> | |
| 57 | +<style> | |
| 58 | + .sg-train-track { | |
| 59 | + width: var(--sg-size); | |
| 60 | + } | |
| 61 | + .sg-train-track svg { | |
| 62 | + display: block; | |
| 63 | + width: 100%; | |
| 64 | + } | |
| 65 | + .sg-train-track-sleepers { | |
| 66 | + fill: none; | |
| 67 | + stroke: rgba(128, 128, 128, 0.35); | |
| 68 | + stroke-width: 7; | |
| 69 | + stroke-dasharray: 1.6 5; | |
| 70 | + } | |
| 71 | + .sg-train-track-rails { | |
| 72 | + fill: none; | |
| 73 | + stroke: rgba(128, 128, 128, 0.55); | |
| 74 | + stroke-width: 1.6; | |
| 75 | + stroke-dasharray: none; | |
| 76 | + } | |
| 77 | + .sg-train-track-engine rect { | |
| 78 | + fill: var(--sg-color); | |
| 79 | + } | |
| 80 | + .sg-train-track-car rect { | |
| 81 | + fill: var(--sg-car); | |
| 82 | + } | |
| 83 | + .sg-train-track-wheel circle { | |
| 84 | + fill: rgba(60, 60, 60, 0.85); | |
| 85 | + } | |
| 86 | + .sg-train-track-wheel path { | |
| 87 | + stroke: rgba(200, 200, 200, 0.9); | |
| 88 | + stroke-width: 0.7; | |
| 89 | + } | |
| 90 | + .sg-train-track-wheel { | |
| 91 | + transform-box: fill-box; | |
| 92 | + transform-origin: center; | |
| 93 | + animation: sg-train-track-spin 0.9s linear infinite; | |
| 94 | + } | |
| 95 | + @keyframes sg-train-track-spin { | |
| 96 | + to { transform: rotate(360deg); } | |
| 97 | + } | |
| 98 | +</style> | |
| 99 | ||