feat(cart): honey glow and drip when the drop lands in the cart
After the flying honey drop reaches the header cart icon, the basket now lights up twice in honey colour with a warm halo, and a small honey drop gathers at its lower edge, stretches and runs down. With reduced motion only a single calm glow remains, without the drip. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
3107a02588
commit
e19827d6af
2 files changed
+123
-1
No files matched your search
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { motion, useAnimationControls, useReducedMotion } from "motion/react";
|
||||
import { onCartLanded } from "@/components/cart-flight";
|
||||
|
||||
// Der Korb im Kopfzeilen-Symbol (20 px in einem 40-px-Knopf, viewBox 24):
|
||||
// seine Unterkante liegt bei y=15, die Mitte bei x=13.5. Dort bildet sich
|
||||
// der Tropfen — umgerechnet in Pixel des Knopfes.
|
||||
const DRIP_LEFT_PX = 10 + (13.5 / 24) * 20;
|
||||
const DRIP_TOP_PX = 10 + (15 / 24) * 20;
|
||||
const DRIP_WIDTH_PX = 6;
|
||||
const DRIP_DELAY_S = 0.3;
|
||||
|
||||
/**
|
||||
* Nachspiel der Landung eines Honigtropfens (components/cart-flight.tsx):
|
||||
* Der Korb leuchtet zweimal warm in Honigfarbe auf, dann sammelt sich an
|
||||
* seiner Unterkante ein kleiner Tropfen, zieht sich lang und perlt nach
|
||||
* unten ab — als waere der Honig durch den Korb gesickert.
|
||||
*
|
||||
* Liegt als Overlay im Warenkorb-Knopf (relative) und faengt keine Klicks.
|
||||
* Das Leuchten ist eine eigene, honigfarbene Kopie des Korbs, deren Deckkraft
|
||||
* blinkt — so stimmt die Farbe im hellen wie im dunklen Farbschema, ohne die
|
||||
* Strichfarbe des eigentlichen Symbols anzufassen. Bei reduzierter Bewegung
|
||||
* bleibt nur ein einzelnes, ruhiges Aufleuchten ohne Tropfen.
|
||||
*/
|
||||
export function CartLandingEffect() {
|
||||
const glowControls = useAnimationControls();
|
||||
const dripControls = useAnimationControls();
|
||||
const reduceMotion = useReducedMotion();
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
onCartLanded(() => {
|
||||
if (reduceMotion) {
|
||||
void glowControls.start({
|
||||
opacity: [0, 0.9, 0],
|
||||
transition: { duration: 0.8, ease: "easeInOut" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
void glowControls.start({
|
||||
opacity: [0, 1, 0.35, 1, 0],
|
||||
scale: [0.9, 1.08, 1, 1.05, 1],
|
||||
transition: { duration: 0.75, ease: "easeInOut", times: [0, 0.2, 0.45, 0.65, 1] },
|
||||
});
|
||||
void dripControls.start({
|
||||
// An der Kante sammeln, schwer werden und lang ziehen,
|
||||
// abloesen und nach unten perlen, verblassen.
|
||||
opacity: [0, 1, 1, 1, 0],
|
||||
scaleX: [0.3, 1, 0.85, 0.9, 0.7],
|
||||
scaleY: [0.2, 1, 1.35, 1, 0.8],
|
||||
y: [0, 0, 3, 13, 17],
|
||||
transition: {
|
||||
duration: 1.1,
|
||||
delay: DRIP_DELAY_S,
|
||||
ease: "easeIn",
|
||||
times: [0, 0.3, 0.55, 0.85, 1],
|
||||
},
|
||||
});
|
||||
}),
|
||||
[glowControls, dripControls, reduceMotion]
|
||||
);
|
||||
|
||||
return (
|
||||
<span aria-hidden="true" className="pointer-events-none absolute inset-0">
|
||||
<motion.span
|
||||
initial={{ opacity: 0 }}
|
||||
animate={glowControls}
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
>
|
||||
{/* Warmer Schein hinter dem Korb. */}
|
||||
<span className="absolute inset-1 rounded-full bg-honey/30 blur-md" />
|
||||
<svg
|
||||
className="relative text-honey drop-shadow-[0_0_4px_var(--color-honey)]"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
>
|
||||
<path d="M6 6h15l-1.5 9h-12z" fill="var(--color-honey-pale)" fillOpacity="0.55" />
|
||||
<path d="M6 6 5 3H2" />
|
||||
<circle cx="9" cy="20" r="1" />
|
||||
<circle cx="18" cy="20" r="1" />
|
||||
</svg>
|
||||
</motion.span>
|
||||
|
||||
{reduceMotion ? null : (
|
||||
<motion.span
|
||||
initial={{ opacity: 0 }}
|
||||
animate={dripControls}
|
||||
style={{
|
||||
left: DRIP_LEFT_PX - DRIP_WIDTH_PX / 2,
|
||||
top: DRIP_TOP_PX - 1,
|
||||
width: DRIP_WIDTH_PX,
|
||||
transformOrigin: "50% 0%",
|
||||
}}
|
||||
className="absolute"
|
||||
>
|
||||
{/* Gleiche Honigtoene wie der fliegende Tropfen in cart-flight.tsx. */}
|
||||
<svg viewBox="0 0 6 9" width={DRIP_WIDTH_PX} height="9">
|
||||
<defs>
|
||||
<radialGradient id="wh-drip" cx="40%" cy="65%" r="70%">
|
||||
<stop offset="0" stopColor="#e4cfa4" />
|
||||
<stop offset="0.5" stopColor="#b88a44" />
|
||||
<stop offset="1" stopColor="#8a6531" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<path d="M3 0C3 2.5 6 4.2 6 6.1A3 3 0 0 1 0 6.1C0 4.2 3 2.5 3 0Z" fill="url(#wh-drip)" />
|
||||
<ellipse cx="2" cy="5.8" rx="0.7" ry="1" fill="#fff" opacity="0.6" />
|
||||
</svg>
|
||||
</motion.span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { QuantityStepper } from "@/components/quantity-stepper";
|
||||
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||
import { useI18n } from "@/i18n/i18n-provider";
|
||||
import { CartFlight, onCartLanded } from "@/components/cart-flight";
|
||||
import { CartLandingEffect } from "@/components/cart-landing-effect";
|
||||
import { HexMark } from "@/components/honeycomb";
|
||||
import type { Dictionary } from "@/i18n/dictionary-type";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
@@ -285,7 +286,8 @@ function CartIconButton() {
|
||||
const basketControls = useAnimationControls();
|
||||
|
||||
// Landet ein Honigtropfen (components/cart-flight.tsx), faengt der Korb
|
||||
// ihn sichtbar auf: kurzes Einsinken und Nachwippen.
|
||||
// ihn sichtbar auf: kurzes Einsinken und Nachwippen. Leuchten und
|
||||
// abperlender Tropfen kommen aus CartLandingEffect.
|
||||
useEffect(
|
||||
() =>
|
||||
onCartLanded(() => {
|
||||
@@ -319,6 +321,7 @@ function CartIconButton() {
|
||||
<circle cx="9" cy="20" r="1" />
|
||||
<circle cx="18" cy="20" r="1" />
|
||||
</motion.svg>
|
||||
<CartLandingEffect />
|
||||
<AnimatePresence>
|
||||
{count > 0 ? (
|
||||
<motion.span
|
||||
|
||||
Reference in new issue
Block a user