Internal document · v1.0 · July 2026
Animation & Tech Brief
The complete engineering blueprint for LanternBRP's motion system: what animates, where, with which library, and the exact parameters. Everything below is live on this site.
Smooth scroll foundation: Lenis
Global · every page
Lenis provides inertial, momentum-based scrolling across the whole site. It runs inside GSAP's ticker so every scroll-driven animation (Framer Motion reveals, ScrollTrigger scrubs) samples the same clock, so there's no fighting between libraries.
// App.tsx: smooth momentum scrolling, synced to GSAP's ticker
import Lenis from "lenis";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
const lenis = new Lenis({ lerp: 0.09, smoothWheel: true });
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);Kinetic hero: masked line reveal
Home · hero (dark #030712)
The signature on-load moment: each headline line sits inside an overflow-hidden mask and rises from 115% below, staggered by 140ms, on a [0.22, 1, 0.36, 1] expo-style ease. The final line carries the sky→cyan→teal gradient as the brand payoff.
// Kinetic hero: masked line-by-line reveal (Framer Motion)
const EASE = [0.22, 1, 0.36, 1];
{lines.map((line, i) => (
<span className="block overflow-hidden" key={line}>
<motion.span
className="block"
initial={{ y: "115%" }}
animate={{ y: 0 }}
transition={{ duration: 0.95, delay: 0.25 + i * 0.14, ease: EASE }}
>
{line}
</motion.span>
</span>
))}Parallax depth: useScroll + useTransform
Home · hero blobs, showcase phones · About · image band
Three depth layers move at different scroll velocities: background glow blobs drift slowest, the app mockup floats at mid-speed, and floating status chips travel fastest, creating genuine foreground/background separation around the product UI.
// Parallax depth: background drifts slower than foreground UI
const { scrollYProgress } = useScroll({
target: sectionRef,
offset: ["start end", "end start"],
});
const phoneY = useTransform(scrollYProgress, [0, 1], [70, -70]);
const chipY = useTransform(scrollYProgress, [0, 1], [140, -60]);
const blobY = useTransform(scrollYProgress, [0, 1], [0, -110]);
<motion.div style={{ y: phoneY }}><PhoneMockup /></motion.div>Scroll-scrub route drawing: GSAP
Home · 'Watch a day get done' map section
The hero animation. An SVG street map hosts a gradient route path whose strokeDashoffset is scrubbed by scroll position via ScrollTrigger. A glowing GPS dot rides the same path using MotionPathPlugin with identical timing, so dot and line never separate. Stop pins spring in (back.out) at staggered scrub positions as the route reaches them.
// Scroll-scrub route drawing: GSAP ScrollTrigger + MotionPath
gsap.registerPlugin(ScrollTrigger, MotionPathPlugin);
const len = path.getTotalLength();
gsap.set(path, { strokeDasharray: len, strokeDashoffset: len });
const tl = gsap.timeline({
scrollTrigger: {
trigger: section, // the map section element
start: "top 72%", // begin when section top hits 72% viewport
end: "bottom 62%", // finish near the section bottom
scrub: 1, // 1s catch-up smoothing; butter, not jitter
},
});
tl.to(path, { strokeDashoffset: 0, ease: "none" }, 0);
tl.to(dot, {
motionPath: { path, align: path, alignOrigin: [0.5, 0.5] },
ease: "none",
}, 0);
tl.to(pins, { opacity: 1, scale: 1, stagger: 0.18, ease: "back.out(2.2)" }, 0.08);Looping explainers: Lottie + SVG scenes
How It Works · hero Lottie + 4 step scenes
The How It Works hero plays a self-hosted Lottie JSON (lottie-react, loop + autoplay) as an ambient 'always-on' visual. The four step panels are hand-built SVG + Framer Motion loops (pathLength draws, keyframed dot travel, stamp springs) chosen over stock Lotties for exact brand and content control. Swap any step scene by dropping a new JSON into /public/lottie and pointing the fetch at it.
// Looping explainer: lottie-react with a self-hosted JSON
import Lottie from "lottie-react";
const [data, setData] = useState(null);
useEffect(() => {
fetch("/lottie/delivery.json") // drop any LottieFiles JSON in /public/lottie
.then((r) => r.json())
.then(setData);
}, []);
<Lottie animationData={data} loop autoplay className="h-64 w-full" />
// Fallback pattern: pure SVG + Framer Motion loops (see src/components/StepAnims.tsx)
<motion.path
initial={{ pathLength: 0 }}
animate={{ pathLength: [0, 1, 1] }}
transition={{ duration: 3, repeat: Infinity, repeatDelay: 0.7 }}
/>Editorial marquee: CSS keyframes
Home, Features, About · section dividers
One slow (48s) infinite ribbon of mono-spaced field-ops vocabulary. Content is duplicated once and translated -50% for a seamless loop; it pauses on hover. Zero JavaScript on the scroll path.
/* Editorial marquee: pure CSS, pauses on hover */
@keyframes marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); } /* content is duplicated once */
}
.animate-marquee { animation: marquee 48s linear infinite; }
.marquee-paused:hover .animate-marquee { animation-play-state: paused; }Routing & SEO architecture
All 8 dedicated URLs
One React app, eight indexable routes: /, /features, /how-it-works, /about, /support, /privacy, /terms, /tech-brief, each served by React Router with its own <title> and meta description (see Seo.tsx), plus robots.txt and sitemap.xml in /public.
Google renders JavaScript SPAs, so every route is crawlable as-is. Two production notes: (1) configure the static host to fall back to index.html for unknown paths so deep links never 404; (2) for maximum first-visit crawl fidelity, add prerendering (react-snap or an SSR pass) later; the route structure above is already compatible with both.
Performance & accessibility guardrails
- Animations run on transform and opacity only, never layout-thrashing properties.
- ScrollTrigger contexts revert on unmount; Lenis destroys cleanly on teardown.
- All photography loads lazily; Lottie JSONs are self-hosted from /public to avoid third-party latency.
- Hybrid contrast: dark sections (#030712/#0B1528) pair with light content (#F8FAFC/#FFFFFF), and body text exceeds WCAG AA 4.5:1 on both.
- Every interactive element carries a kebab-case data-testid for automated flow testing.
- Marquee and scrub animations are non-essential: content remains fully readable without motion.