Give product screenshots a painterly atmospheric stage
Float each screenshot over an ambient wash painted from its own colours (a tiny blurred thumbnail), layered with the aurora, a designed green->teal gradient, and fine film grain — the "based on our real product screenshot" treatment. - ui/art-stage: reusable atmospheric stage (ambient LQIP + aurora + grain + a floating shot with depth) - hero + bento: swap the flat frames for ArtStage - public/images/ambient: per-screenshot colour-wash thumbnails (<250B each) - remove the now-unused media-frame e2e green (12 passed).
This commit is contained in:
1 parent
8f1a5f1f4e
commit
2e8121d8b9
10 files changed
+83
-59
No files matched your search
@@ -5,6 +5,7 @@ import { Container } from "../ui/container";
|
|||||||
import { Reveal } from "../ui/reveal";
|
import { Reveal } from "../ui/reveal";
|
||||||
import { Eyebrow } from "../ui/eyebrow";
|
import { Eyebrow } from "../ui/eyebrow";
|
||||||
import { SectionHeading } from "../ui/section-heading";
|
import { SectionHeading } from "../ui/section-heading";
|
||||||
|
import { ArtStage } from "../ui/art-stage";
|
||||||
import { featureSections, type FeatureSection } from "../../lib/landing-content";
|
import { featureSections, type FeatureSection } from "../../lib/landing-content";
|
||||||
|
|
||||||
// Explicit order + column spans so the cells pack into full 12-column rows.
|
// Explicit order + column spans so the cells pack into full 12-column rows.
|
||||||
@@ -39,17 +40,13 @@ function BentoCard({ feature, span }: { feature: FeatureSection; span: string })
|
|||||||
<h3 className="text-xl font-medium tracking-[-0.02em] text-[var(--text)]">{feature.title}</h3>
|
<h3 className="text-xl font-medium tracking-[-0.02em] text-[var(--text)]">{feature.title}</h3>
|
||||||
<p className="max-w-[46ch] text-sm leading-relaxed text-[var(--muted)]">{feature.description}</p>
|
<p className="max-w-[46ch] text-sm leading-relaxed text-[var(--muted)]">{feature.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative mt-6 overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--panel-illustration)] shadow-[0_18px_50px_-24px_rgba(10,29,8,0.28)]">
|
<ArtStage
|
||||||
<img
|
src={feature.image}
|
||||||
src={feature.image}
|
alt={feature.imageAlt}
|
||||||
alt={feature.imageAlt}
|
className="mt-6"
|
||||||
width={2400}
|
stageClassName="flex aspect-[16/10] items-center justify-center p-4 sm:p-5"
|
||||||
height={1500}
|
contentClassName="max-h-full w-auto max-w-full object-contain transition-transform duration-500 group-hover:scale-[1.02]"
|
||||||
loading="lazy"
|
/>
|
||||||
decoding="async"
|
|
||||||
className="block aspect-[16/10] w-full object-cover object-top transition-transform duration-500 group-hover:scale-[1.02]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Container } from "../ui/container";
|
import { Container } from "../ui/container";
|
||||||
import { Badge } from "../ui/badge";
|
import { Badge } from "../ui/badge";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { MediaFrame } from "../ui/media-frame";
|
import { ArtStage } from "../ui/art-stage";
|
||||||
import { landingHeroNarrative } from "../../lib/product-narrative";
|
import { landingHeroNarrative } from "../../lib/product-narrative";
|
||||||
|
|
||||||
export function Hero() {
|
export function Hero() {
|
||||||
@@ -52,25 +52,13 @@ export function Hero() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="enter-rise relative mt-14 md:mt-20" style={{ animationDelay: "0.3s" }}>
|
<div className="enter-rise mt-14 md:mt-20" style={{ animationDelay: "0.3s" }}>
|
||||||
<div
|
<ArtStage
|
||||||
aria-hidden="true"
|
src="/images/dashboard.webp"
|
||||||
className="pointer-events-none absolute -inset-x-8 -top-10 bottom-0"
|
alt="AgentCompany desktop dashboard showing the quarter goal, budget, agent workload, and recent runs"
|
||||||
style={{
|
priority
|
||||||
filter: "blur(52px)",
|
stageClassName="p-5 sm:p-8 md:p-12 lg:p-14"
|
||||||
background:
|
|
||||||
"radial-gradient(55% 45% at 50% 22%, var(--aurora-mint) 0%, transparent 70%)," +
|
|
||||||
"radial-gradient(45% 40% at 22% 65%, var(--aurora-teal) 0%, transparent 72%)," +
|
|
||||||
"radial-gradient(45% 42% at 80% 60%, var(--aurora-lime) 0%, transparent 72%)",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<div className="relative z-10">
|
|
||||||
<MediaFrame
|
|
||||||
src="/images/dashboard.webp"
|
|
||||||
alt="AgentCompany desktop dashboard showing the quarter goal, budget, agent workload, and recent runs"
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// Fractal-noise film grain (inline data URI, allowed by img-src 'self' data:).
|
||||||
|
const GRAIN =
|
||||||
|
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E\")";
|
||||||
|
|
||||||
|
const AURORA_WASH =
|
||||||
|
"radial-gradient(60% 55% at 26% 16%, var(--aurora-mint) 0%, transparent 62%)," +
|
||||||
|
"radial-gradient(52% 52% at 84% 84%, var(--aurora-teal) 0%, transparent 60%)," +
|
||||||
|
"radial-gradient(46% 48% at 60% 50%, var(--aurora-lime) 0%, transparent 66%)";
|
||||||
|
|
||||||
|
/** Ambient color wash derived from `ambientSrc`, given the src's own path. */
|
||||||
|
export function ambientFor(src: string): string {
|
||||||
|
return src.replace("/images/", "/images/ambient/");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A product screenshot floating over an atmospheric wash painted from its own
|
||||||
|
* colours (a heavily blurred thumbnail), plus the aurora and a fine film grain.
|
||||||
|
*/
|
||||||
|
export function ArtStage({
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
priority = false,
|
||||||
|
className = "",
|
||||||
|
stageClassName = "p-5 sm:p-8 md:p-10",
|
||||||
|
contentClassName = "w-full",
|
||||||
|
}: {
|
||||||
|
src: string;
|
||||||
|
alt: string;
|
||||||
|
priority?: boolean;
|
||||||
|
className?: string;
|
||||||
|
stageClassName?: string;
|
||||||
|
contentClassName?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={`relative isolate overflow-hidden rounded-2xl border border-[var(--border)] ${className}`}>
|
||||||
|
<img
|
||||||
|
src={ambientFor(src)}
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-0 -z-10 h-full w-full scale-125 object-cover opacity-75 blur-3xl saturate-[1.7]"
|
||||||
|
/>
|
||||||
|
<div aria-hidden="true" className="absolute inset-0 -z-10" style={{ background: AURORA_WASH }} />
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-0 -z-10"
|
||||||
|
style={{ background: "linear-gradient(150deg, rgba(94,236,151,0.16) 0%, transparent 42%, rgba(45,212,191,0.12) 100%)" }}
|
||||||
|
/>
|
||||||
|
<div aria-hidden="true" className="absolute inset-0 -z-10 bg-[var(--bg)]/28" />
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-0 -z-10 opacity-[0.14] mix-blend-overlay"
|
||||||
|
style={{ backgroundImage: GRAIN }}
|
||||||
|
/>
|
||||||
|
<div className={`relative ${stageClassName}`}>
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
width={2400}
|
||||||
|
height={1600}
|
||||||
|
loading={priority ? "eager" : "lazy"}
|
||||||
|
decoding="async"
|
||||||
|
fetchPriority={priority ? "high" : "auto"}
|
||||||
|
className={`block rounded-lg border border-black/5 shadow-[0_30px_80px_-28px_rgba(10,29,8,0.5)] md:rounded-xl ${contentClassName}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
/** Framed product screenshot panel: rounded, bordered, soft shadow on the page. */
|
|
||||||
export function MediaFrame({
|
|
||||||
src,
|
|
||||||
alt,
|
|
||||||
priority = false,
|
|
||||||
className = "",
|
|
||||||
}: {
|
|
||||||
src: string;
|
|
||||||
alt: string;
|
|
||||||
priority?: boolean;
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--panel-illustration)] shadow-[0_24px_70px_-28px_rgba(10,29,8,0.3)] md:rounded-2xl ${className}`}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={src}
|
|
||||||
alt={alt}
|
|
||||||
width={2400}
|
|
||||||
height={1600}
|
|
||||||
loading={priority ? "eager" : "lazy"}
|
|
||||||
decoding="async"
|
|
||||||
fetchPriority={priority ? "high" : "auto"}
|
|
||||||
className="block h-auto w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 96 B |
Binary file not shown.
|
After Width: | Height: | Size: 96 B |
Binary file not shown.
|
After Width: | Height: | Size: 118 B |
Binary file not shown.
|
After Width: | Height: | Size: 116 B |
Binary file not shown.
|
After Width: | Height: | Size: 232 B |
Binary file not shown.
|
After Width: | Height: | Size: 108 B |
Reference in new issue
Block a user