feat: snap scrolling with per-section background colors

Replace sticky-scroll layout with full-viewport snap-scrolling
sections. Each scroll gesture snaps to the next section like a
slide deck. Sections get progressively darker backgrounds
(white -> slate-300). Simplified FeatureScene to single
description per slide.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mudskipper
2026-04-21 11:15:42 +10:00
parent f827c7d90a
commit 300320fed2
4 changed files with 43 additions and 111 deletions

View File

@@ -1,4 +1,4 @@
<section class="bg-slate-100">
<section class="bg-slate-100 min-h-screen snap-start snap-always flex items-center">
<div class="max-w-screen-lg mx-6 sm:mx-16 xl:mx-auto py-16 md:py-24 flex flex-col items-center text-center gap-6">
<h2 class="text-slate-900">Ready to try Audacity 4?</h2>
<p class="text-lg text-slate-600 max-w-xl">

View File

@@ -1,100 +1,28 @@
import React, { useRef, useEffect, useState } from "react";
import { motion, useScroll, useInView, AnimatePresence } from "framer-motion";
function FeatureScene({ title, descriptions, imageSrc, imageAlt, mirrored = false }) {
const sectionRef = useRef(null);
const imageRef = useRef(null);
const imageInView = useInView(imageRef, { once: true, margin: "-50px" });
const [activeIndex, setActiveIndex] = useState(0);
const { scrollYProgress } = useScroll({
target: sectionRef,
offset: ["start start", "end end"],
});
useEffect(() => {
const unsubscribe = scrollYProgress.on("change", (v) => {
const stepCount = descriptions.length;
const index = Math.min(Math.floor(v * stepCount), stepCount - 1);
setActiveIndex(index);
});
return unsubscribe;
}, [scrollYProgress, descriptions.length]);
const sectionHeight = `${descriptions.length * 100 + 50}vh`;
import React from "react";
function FeatureScene({ title, description, imageSrc, imageAlt, mirrored = false, bgClass = "bg-white", textClass = "text-slate-900", bodyClass = "text-slate-600" }) {
const imageColumn = (
<div className="hidden md:block md:w-[55%]">
<div className="sticky top-[50vh] -translate-y-1/2 w-full">
<motion.div
ref={imageRef}
initial={{ opacity: 0, y: 20 }}
animate={imageInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
transition={{ duration: 0.5, ease: "easeOut" }}
>
<img
src={imageSrc}
alt={imageAlt}
className="w-full rounded-xl shadow-[0_25px_50px_rgba(0,0,0,0.12)] border border-gray-100"
loading="lazy"
/>
</motion.div>
</div>
<div className="w-full md:w-1/2 flex items-center justify-center px-4">
<img
src={imageSrc}
alt={imageAlt}
className="w-full max-w-lg rounded-xl shadow-[0_25px_50px_rgba(0,0,0,0.12)] border border-gray-100"
/>
</div>
);
const textColumn = (
<div className="w-full md:w-[40%]">
{/* Mobile: stacked layout, no sticky */}
<div className="md:hidden flex flex-col gap-8 py-8">
<img
src={imageSrc}
alt={imageAlt}
className="w-full rounded-xl shadow-[0_25px_50px_rgba(0,0,0,0.12)] border border-gray-100"
loading="lazy"
/>
<h2 className="text-slate-900">{title}</h2>
{descriptions.map((desc, index) => (
<p key={index} className="text-lg text-slate-600 leading-relaxed">{desc}</p>
))}
</div>
{/* Desktop: sticky text that crossfades */}
<div className="hidden md:block sticky top-[50vh] -translate-y-1/2">
<div className="relative w-full">
<AnimatePresence mode="wait">
<motion.div
key={activeIndex}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
<h2 className="text-slate-900 mb-4">{title}</h2>
<p className="text-lg text-slate-600 leading-relaxed">
{descriptions[activeIndex]}
</p>
{/* Progress dots */}
<div className="flex gap-2 mt-8">
{descriptions.map((_, index) => (
<div
key={index}
className={`w-2 h-2 rounded-full transition-colors duration-300 ${
index === activeIndex ? "bg-blue-700" : "bg-slate-300"
}`}
/>
))}
</div>
</motion.div>
</AnimatePresence>
</div>
<div className="w-full md:w-1/2 flex items-center px-4 md:px-8">
<div>
<h2 className={`${textClass} mb-4`}>{title}</h2>
<p className={`text-lg leading-relaxed ${bodyClass}`}>{description}</p>
</div>
</div>
);
return (
<section ref={sectionRef} className="bg-white">
<div className={`max-w-screen-lg mx-6 sm:mx-16 xl:mx-auto py-12 md:py-[50vh] flex flex-col md:flex-row ${mirrored ? "md:flex-row-reverse" : ""} gap-8 md:gap-12`} style={{ minHeight: sectionHeight }}>
<section className={`h-screen snap-start snap-always ${bgClass}`}>
<div className={`h-full max-w-screen-lg mx-auto px-6 sm:px-16 flex flex-col md:flex-row ${mirrored ? "md:flex-row-reverse" : ""} items-center gap-8 md:gap-12`}>
{imageColumn}
{textColumn}
</div>

View File

@@ -2,8 +2,8 @@
import PlaceholderUI from "../../assets/img/audacity4/placeholder-ui.svg";
---
<section id="main" class="bg-gradient-to-b from-slate-50 to-white min-h-screen">
<div class="max-w-screen-lg mx-6 sm:mx-16 xl:mx-auto py-16 md:py-24 flex flex-col items-center justify-center text-center gap-8 md:gap-12 min-h-screen">
<section id="main" class="bg-gradient-to-b from-slate-50 to-white h-screen snap-start snap-always">
<div class="max-w-screen-lg mx-6 sm:mx-16 xl:mx-auto py-16 md:py-24 flex flex-col items-center justify-center text-center gap-8 md:gap-12 h-full">
<div class="flex flex-col gap-4 max-w-2xl">
<h1 class="text-slate-900 leading-tight">
Meet Audacity 4

View File

@@ -20,64 +20,68 @@ import PlaceholderSplit from "../assets/img/audacity4/placeholder-split.svg";
<FeatureScene
client:load
title="A completely redesigned interface"
description="Audacity 4 introduces a ground-up redesign of the entire interface. Every toolbar, panel, and menu has been rethought for clarity and speed."
imageSrc={PlaceholderUI.src}
imageAlt="The new Audacity 4 interface with redesigned toolbar and track layout"
descriptions={[
"Audacity 4 introduces a ground-up redesign of the entire interface. Every toolbar, panel, and menu has been rethought for clarity and speed.",
"The new track panel gives you a cleaner overview of your project, with streamlined controls that stay out of the way until you need them.",
"A modernized toolbar puts your most-used tools front and center, with logical groupings that match the way you actually work.",
"The result is an interface that feels familiar to long-time users but dramatically more efficient for everyone."
]}
mirrored={false}
bgClass="bg-white"
textClass="text-slate-900"
bodyClass="text-slate-600"
/>
<FeatureScene
client:load
title="Select multiple clips at once"
description="For the first time in Audacity, you can select and manipulate multiple clips across tracks in a single action. Click, shift-click, or drag to select exactly what you need."
imageSrc={PlaceholderClips.src}
imageAlt="Multiple audio clips selected simultaneously across different tracks"
descriptions={[
"For the first time in Audacity, you can select and manipulate multiple clips across tracks in a single action. Click, shift-click, or drag to select exactly what you need.",
"Move, delete, copy, or apply effects to your entire selection at once. No more tedious one-clip-at-a-time editing."
]}
mirrored={true}
bgClass="bg-slate-50"
textClass="text-slate-900"
bodyClass="text-slate-600"
/>
<FeatureScene
client:load
title="Stretch clips with a new algorithm"
description="Grab the edge of any clip and drag to stretch or compress it in time. The new stretching algorithm preserves audio quality even at extreme ratios."
imageSrc={PlaceholderStretch.src}
imageAlt="An audio clip being stretched using drag handles on its edges"
descriptions={[
"Grab the edge of any clip and drag to stretch or compress it in time. The new stretching algorithm preserves audio quality even at extreme ratios.",
"Whether you're time-aligning dialogue, adjusting music loops, or syncing sound effects, stretch handles make it intuitive and fast."
]}
mirrored={false}
bgClass="bg-slate-100"
textClass="text-slate-900"
bodyClass="text-slate-600"
/>
<FeatureScene
client:load
title="Group clips together"
description="Select clips across tracks and group them into a single unit. Grouped clips move, copy, and delete together — keeping your carefully-arranged edits intact."
imageSrc={PlaceholderGroups.src}
imageAlt="Multiple clips grouped together with a visual bounding outline"
descriptions={[
"Select clips across tracks and group them into a single unit. Grouped clips move, copy, and delete together — keeping your carefully-arranged edits intact.",
"Groups are non-destructive and can be ungrouped at any time. Perfect for managing complex multi-track projects."
]}
mirrored={true}
bgClass="bg-slate-200"
textClass="text-slate-900"
bodyClass="text-slate-700"
/>
<FeatureScene
client:load
title="The new Split tool"
description="The dedicated Split tool gives you a precise way to divide clips exactly where you need them. Combined with multi-clip selection and clip groups, it completes a powerful new editing workflow."
imageSrc={PlaceholderSplit.src}
imageAlt="The split tool cursor positioned over an audio clip, ready to split it"
descriptions={[
"The dedicated Split tool gives you a precise way to divide clips exactly where you need them. Click anywhere on a clip to split it at that point.",
"Combined with multi-clip selection and clip groups, the Split tool completes a powerful new editing workflow that puts you in full control of your audio."
]}
mirrored={false}
bgClass="bg-slate-300"
textClass="text-slate-900"
bodyClass="text-slate-700"
/>
<CTAFooter />
</BaseLayout>
<style is:global>
html {
scroll-snap-type: y mandatory;
}
</style>