From a48b7653dda493ca40e8818bc69d522506358c8b Mon Sep 17 00:00:00 2001 From: jis3r Date: Sun, 23 Aug 2026 21:28:58 +0200 Subject: [PATCH] fix(icons): make animate prop drive the imperative icon animations AudioLines, Bot, Eye, MessageCircleMore and MessageSquareMore drive their animation with requestAnimationFrame, started only from the mouseenter handler, so a parent-set animate prop did nothing. In Bot and the two message icons the guard also tested the animate union, which meant a parent holding animate=true blocked the hover path as well. Each icon now triggers its animation from the prop via an untracked effect (untrack keeps the effect depending on the prop alone, not on the frame state it reads), and the single-flight guards test the in-flight flag instead of the union. AudioLines, which loops continuously, starts and settles the loop from the prop and no longer settles back on mouseleave while the prop still asks for animation. Two tests cover the whole set: every icon must render differently when animate is set, and every icon with a requestAnimationFrame loop must move geometry (not just toggle a class - AudioLines and Bot toggle a class with no CSS rule behind it, which is why this looked wired up) both from the prop and on hover. Fixes #99 --- src/lib/icons.spec.ts | Bin 3121 -> 6653 bytes src/lib/icons/audio-lines.svelte | 33 +++++++++++++++++++---- src/lib/icons/bot.svelte | 10 ++++++- src/lib/icons/eye.svelte | 7 +++++ src/lib/icons/message-circle-more.svelte | 11 +++++++- src/lib/icons/message-square-more.svelte | 11 +++++++- static/r/audio-lines.json | 2 +- static/r/bot.json | 2 +- static/r/eye.json | 2 +- static/r/message-circle-more.json | 2 +- static/r/message-square-more.json | 2 +- 11 files changed, 69 insertions(+), 13 deletions(-) diff --git a/src/lib/icons.spec.ts b/src/lib/icons.spec.ts index 8402406bd00ac39807358a61ae8c64101e468608..c34dce4382dffc139678f18d67e0e4df58f883b6 100644 GIT binary patch literal 6653 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 + import { untrack } from 'svelte'; import type { IconProps } from './types.js'; let { @@ -214,15 +215,15 @@ } } - function handleMouseEnter() { - hoverAnimate = true; + function startLoop() { isAnimatingBack = false; startTime = null; - animationFrameId = requestAnimationFrame(animateFrame); + if (!animationFrameId) { + animationFrameId = requestAnimationFrame(animateFrame); + } } - function handleMouseLeave() { - hoverAnimate = false; + function settleBack() { startPositions = { line1: { y1: line1Y1, y2: line1Y2 }, line2: { y1: line2Y1, y2: line2Y2 }, @@ -237,6 +238,28 @@ animationFrameId = requestAnimationFrame(animateFrame); } } + + function handleMouseEnter() { + hoverAnimate = true; + startLoop(); + } + + function handleMouseLeave() { + hoverAnimate = false; + // A parent still asking for animation keeps the loop running. + if (!animateProp) settleBack(); + } + + // The loop is driven imperatively, so the prop needs an explicit start/stop. + // untrack keeps this effect depending on animateProp alone - reading the frame + // state as a dependency would re-run it on every frame. + $effect(() => { + const on = animateProp; + untrack(() => { + if (on) startLoop(); + else if (!hoverAnimate && animationFrameId) settleBack(); + }); + });
+ import { untrack } from 'svelte'; import type { IconProps } from './types.js'; let { @@ -49,7 +50,9 @@ } function handleMouseEnter() { - if (animate) return; + // hoverAnimate doubles as the in-flight flag: guard on it, not on the + // animate union, or a parent holding animate=true would block every blink. + if (hoverAnimate) return; hoverAnimate = true; animateEyes(13, 15, 14, 14, 250, 200).then(() => { animateEyes(14, 14, 13, 15, 250).then(() => { @@ -58,6 +61,11 @@ }); } + // The blink is driven imperatively, so the prop has to trigger it explicitly. + $effect(() => { + if (animateProp) untrack(handleMouseEnter); + }); + $effect(() => () => clearTimeout(entryTimer)); diff --git a/src/lib/icons/eye.svelte b/src/lib/icons/eye.svelte index ae1b5d12..98921599 100644 --- a/src/lib/icons/eye.svelte +++ b/src/lib/icons/eye.svelte @@ -1,4 +1,5 @@ diff --git a/src/lib/icons/message-circle-more.svelte b/src/lib/icons/message-circle-more.svelte index b8e1a742..d1d3d6d2 100644 --- a/src/lib/icons/message-circle-more.svelte +++ b/src/lib/icons/message-circle-more.svelte @@ -1,4 +1,5 @@ diff --git a/src/lib/icons/message-square-more.svelte b/src/lib/icons/message-square-more.svelte index ce9e6d93..9f3f42f1 100644 --- a/src/lib/icons/message-square-more.svelte +++ b/src/lib/icons/message-square-more.svelte @@ -1,4 +1,5 @@ diff --git a/static/r/audio-lines.json b/static/r/audio-lines.json index 98df4ab1..fd42a79a 100644 --- a/static/r/audio-lines.json +++ b/static/r/audio-lines.json @@ -9,7 +9,7 @@ { "type": "registry:component", "target": "~/src/lib/components/movingicons/audio-lines.svelte", - "content": "\n\n\n\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\n
\n\n\n" + "content": "\n\n\n\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\n\n\n\n" } ], "registryDependencies": [] diff --git a/static/r/bot.json b/static/r/bot.json index e7b1227f..1ebb8579 100644 --- a/static/r/bot.json +++ b/static/r/bot.json @@ -9,7 +9,7 @@ { "type": "registry:component", "target": "~/src/lib/components/movingicons/bot.svelte", - "content": "\n\n
\n\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\n
\n\n\n" + "content": "\n\n
\n\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\n
\n\n\n" } ], "registryDependencies": [] diff --git a/static/r/eye.json b/static/r/eye.json index b2dbd293..1bcf093c 100644 --- a/static/r/eye.json +++ b/static/r/eye.json @@ -9,7 +9,7 @@ { "type": "registry:component", "target": "~/src/lib/components/movingicons/eye.svelte", - "content": "\n\n
\n\t\n\t\t\n\t\t\n\n\t\t\n\t\n
\n\n\n" + "content": "\n\n
\n\t\n\t\t\n\t\t\n\n\t\t\n\t\n
\n\n\n" } ], "registryDependencies": [] diff --git a/static/r/message-circle-more.json b/static/r/message-circle-more.json index c9d962b0..136da641 100644 --- a/static/r/message-circle-more.json +++ b/static/r/message-circle-more.json @@ -9,7 +9,7 @@ { "type": "registry:component", "target": "~/src/lib/components/movingicons/message-circle-more.svelte", - "content": "\n\n
\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n
\n\n\n" + "content": "\n\n
\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n
\n\n\n" } ], "registryDependencies": [] diff --git a/static/r/message-square-more.json b/static/r/message-square-more.json index 6819da63..e405d279 100644 --- a/static/r/message-square-more.json +++ b/static/r/message-square-more.json @@ -9,7 +9,7 @@ { "type": "registry:component", "target": "~/src/lib/components/movingicons/message-square-more.svelte", - "content": "\n\n
\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n
\n\n\n" + "content": "\n\n
\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n
\n\n\n" } ], "registryDependencies": []