From 3bba781f49a2af87db2eec67f7ffcf2c0eddc4c3 Mon Sep 17 00:00:00 2001 From: filemon Date: Sat, 18 Apr 2026 18:09:12 -0300 Subject: [PATCH] Fix open-eye flash on first sleep transition Clear stale cached DOM refs in useBlobbiEyes when entering sleep. The awake animation loop caches blink/gaze SVG elements, but dangerouslySetInnerHTML replaces the entire SVG when the sleeping recipe is applied. The old refs' open-eye clip geometry was being used to querySelector into the new sleeping SVG and reset the clip-paths back to the open position, causing both open eyes and closed-eye lines to render simultaneously. The sleeping recipe already sets clip rects to the closed position in the SVG string, so no JS-side clip-path reset is needed. Clearing the caches prevents stale operations and lets fresh caching happen naturally when Blobbi wakes up. --- src/blobbi/ui/lib/useBlobbiEyes.ts | 50 ++++++++++++------------------ 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/src/blobbi/ui/lib/useBlobbiEyes.ts b/src/blobbi/ui/lib/useBlobbiEyes.ts index b7cf3088..9ba8d45e 100644 --- a/src/blobbi/ui/lib/useBlobbiEyes.ts +++ b/src/blobbi/ui/lib/useBlobbiEyes.ts @@ -227,37 +227,25 @@ export function useBlobbiEyes( attachGlobalMouseListener(); if (isSleeping) { - // Reset eyes to center when sleeping (no blinking) - const resetEyes = () => { - // Reset tracking transforms - leftGazeRef.current.forEach((el) => { - el.setAttribute('transform', 'translate(0 0)'); - }); - rightGazeRef.current.forEach((el) => { - el.setAttribute('transform', 'translate(0 0)'); - }); - // Reset clip-paths to fully open (unless SMIL animations are controlling them) - [...leftBlinkRef.current, ...rightBlinkRef.current].forEach((el) => { - const clipId = el.getAttribute(EYE_DATA_ATTRS.clipId); - // Try new format first, fall back to legacy for old SVGs - const clipTopAttr = el.getAttribute(EYE_DATA_ATTRS.clipTop) ?? el.getAttribute(EYE_DATA_ATTRS.legacyEyeTop); - const clipHeightAttr = el.getAttribute(EYE_DATA_ATTRS.clipHeight); - - if (clipId && clipTopAttr && clipHeightAttr && containerRef.current) { - const clipRect = containerRef.current.querySelector(`#${clipId} rect`) as SVGRectElement | null; - if (clipRect) { - // Don't override SMIL animations (e.g., sleepy emotion) - const hasSmilAnimation = clipRect.querySelector('animate') !== null; - if (hasSmilAnimation) { - return; - } - clipRect.setAttribute('y', clipTopAttr); - clipRect.setAttribute('height', clipHeightAttr); - } - } - }); - }; - resetEyes(); + // Clear all cached refs immediately. When transitioning from awake to + // sleep the SVG DOM is replaced (dangerouslySetInnerHTML) so the old + // refs point at detached nodes whose data-attributes still carry the + // open-eye clip geometry. Using those stale values to querySelector + // into the *new* sleeping SVG would reset the clip-paths back to the + // open position — causing both open eyes and closed-eye lines to show + // simultaneously on the first sleep transition. + // + // The sleeping recipe (applySleepingClosedEyes) already sets clip rects + // to the closed position in the SVG string, so no clip-path reset is + // needed here. Clearing the caches ensures the awake animation loop + // won't run stale operations, and fresh caching will happen naturally + // when Blobbi wakes up and the effect re-runs. + leftGazeRef.current = []; + rightGazeRef.current = []; + leftBlinkRef.current = []; + rightBlinkRef.current = []; + lastSvgContentRef.current = ''; + // Reset blink state when sleeping blinkStateRef.current = null;