Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 2 additions & 49 deletions apps/web/app/docs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
landingGutterClass,
} from "@/components/landing/landing-frame"
import { cn } from "@/lib/utils"
import { AsciiCatalogPreview } from "@/components/docs/previews/ascii-effects-preview"
import { AsciiCatalogPreview } from "@/components/docs/previews/ascii-effect-preview"

type PreviewSources = {
mp4: string
Expand Down Expand Up @@ -59,11 +59,9 @@ function getPreviewPosterSrc(previewVideo?: string) {
function ComponentCard({
component,
index,
asciiColors,
}: {
component: ComponentMetadata
index: number
asciiColors?: string[]
}) {
const videoRef = useRef<HTMLVideoElement | null>(null)
const [isHovered, setIsHovered] = useState(false)
Expand Down Expand Up @@ -194,7 +192,7 @@ function ComponentCard({
className="relative w-full rounded-xl bg-zinc-50 dark:bg-zinc-900/80 group-hover:bg-zinc-100/50 dark:group-hover:bg-zinc-800/80 transition-colors border border-dashed border-border shadow-surface-inset overflow-hidden"
>
{component.category === "ASCII Effects" && (
<AsciiCatalogPreview slug={component.slug} colors={asciiColors} />
<AsciiCatalogPreview />
)}
{previewPosterSrc && (
<img
Expand Down Expand Up @@ -269,20 +267,10 @@ const categoryOrder: ComponentCategory[] = [
"ASCII Effects",
]

const asciiPalettes: Array<{ label: string; colors: string[] }> = [
{ label: "Graphite", colors: ["#171719", "#888b91", "#f4f4f5"] },
{ label: "Crimson", colors: ["#180407", "#821421", "#e34a55"] },
{ label: "Matrix", colors: ["#04180b", "#18a448", "#baffca"] },
{ label: "Cobalt", colors: ["#071426", "#2566ad", "#d1e5ff"] },
{ label: "Amber", colors: ["#211304", "#b96b13", "#ffe0a3"] },
{ label: "Ultraviolet", colors: ["#170923", "#7d2db3", "#e8caff"] },
]

// ─── Main Docs Page ─────────────────────────────────────────────────────────
export default function DocsPage() {
const allComponents = Object.values(components)
const [activeSection, setActiveSection] = useState<string>("")
const [asciiPalette, setAsciiPalette] = useState(0)

useEffect(() => {
const observers = categoryOrder.map((cat) => {
Expand Down Expand Up @@ -397,48 +385,13 @@ export default function DocsPage() {
<h2 className="text-xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">
{category}
</h2>
{category === "ASCII Effects" && (
<div
className="flex items-center gap-0.5 rounded-full border border-zinc-200/80 bg-white/70 p-1 shadow-sm backdrop-blur-sm dark:border-white/10 dark:bg-black/30"
role="group"
aria-label="ASCII color theme"
>
{asciiPalettes.map((palette, paletteIndex) => (
<button
key={palette.label}
type="button"
aria-label={palette.label}
aria-pressed={asciiPalette === paletteIndex}
title={palette.label}
onClick={() => setAsciiPalette(paletteIndex)}
className={cn(
"grid size-7 place-items-center rounded-full border border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400",
asciiPalette === paletteIndex
? "border-zinc-300 bg-zinc-100 dark:border-white/15 dark:bg-white/10"
: "hover:bg-zinc-100 dark:hover:bg-white/[0.06]"
)}
>
<span
className="size-3.5 rounded-full border border-white/15"
style={{
backgroundColor: palette.colors[1],
boxShadow: asciiPalette === paletteIndex
? `0 0 10px ${palette.colors[1]}`
: `0 0 0 1px ${palette.colors[0]}`,
}}
/>
</button>
))}
</div>
)}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{items.map((component, i) => (
<ComponentCard
key={component.slug}
component={component}
index={i}
asciiColors={category === "ASCII Effects" ? asciiPalettes[asciiPalette]?.colors : undefined}
/>
))}
</div>
Expand Down
260 changes: 260 additions & 0 deletions apps/web/components/docs/ascii-effect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import { DocsPageLayout, type PropItem } from "@/components/docs-page-layout";
import {
AsciiFlowPreview,
AsciiGlitchPreview,
AsciiImagePreview,
} from "@/components/docs/previews/ascii-effect-preview";
import { readComponentSource } from "@/lib/source-code";

const importCode = `import { AsciiEffect } from "@/components/ui/ascii-effect"`;

const imageCode = `${importCode}

<div className="h-[520px] overflow-hidden rounded-2xl">
<AsciiEffect
variant="image"
imageSrc="/images/portrait.jpg"
fontSize={9}
scale={1.15}
/>
</div>`;

const flowCode = `${importCode}

<div className="h-[520px] overflow-hidden rounded-2xl">
<AsciiEffect
variant="flow"
imageSrc="/images/portrait.jpg"
flowSpeed={0.22}
flowStrength={12}
mouseRadius={150}
mouseStrength={22}
/>
</div>`;

const glitchCode = `${importCode}

<div className="h-[520px] overflow-hidden rounded-2xl">
<AsciiEffect
variant="glitch"
imageSrc="/images/portrait.jpg"
glitchIntensity={0.65}
glitchFrequency={1.4}
revealDuration={1400}
/>
</div>`;

const props: PropItem[] = [
{
name: "imageSrc",
type: "string",
description: "Image URL rendered as ASCII characters.",
},
{
name: "variant",
type: '"image" | "flow" | "glitch"',
default: '"image"',
description: "Visual effect variation.",
},
{
name: "chars",
type: "string",
default: '" .:-=+*#%@"',
description: "Characters ordered from darkest to brightest.",
},
{
name: "fontSize",
type: "number",
default: "9",
description: "Character height in pixels.",
},
{
name: "fontFamily",
type: "string",
default: "Arial, Helvetica, sans-serif",
description: "Font stack used to draw the characters.",
},
{
name: "fontWeight",
type: "number | string",
default: "400",
description: "Canvas font weight.",
},
{
name: "lineHeight",
type: "number",
default: "1",
description: "Character row height multiplier.",
},
{
name: "characterSpacing",
type: "number",
default: "1",
description: "Horizontal character-cell spacing multiplier.",
},
{
name: "brightnessBoost",
type: "number",
default: "2.2",
description: "Multiplier applied to sampled image luminance.",
},
{
name: "contrast",
type: "number",
default: "1.1",
description: "Contrast applied before character selection.",
},
{
name: "threshold",
type: "number",
default: "0.06",
description: "Dark-pixel cutoff used to keep the background clean.",
},
{
name: "posterize",
type: "number",
default: "32",
description: "Number of luminance steps.",
},
{
name: "dither",
type: '"none" | "floyd-steinberg" | "bayer"',
default: '"floyd-steinberg"',
description: "Dithering algorithm used to preserve photographic detail.",
},
{
name: "ditherStrength",
type: "number",
default: "0.8",
description: "Amount of error diffusion or ordered dithering.",
},
{
name: "flowSpeed",
type: "number",
default: "0.22",
description: "Flow cycles per second for the flow variant.",
},
{
name: "flowDirection",
type: "number",
default: "0",
description: "Flow direction in degrees.",
},
{
name: "flowStrength",
type: "number",
default: "12",
description: "Directional displacement in pixels.",
},
{
name: "flowFrequency",
type: "number",
default: "0.018",
description: "Size of the flowing wave field.",
},
{
name: "mouseRadius",
type: "number",
default: "150",
description: "Radius of the pointer ripple in pixels.",
},
{
name: "mouseStrength",
type: "number",
default: "22",
description: "Pointer ripple displacement in pixels.",
},
{
name: "mouseWaveSpeed",
type: "number",
default: "1.2",
description: "Speed of the pointer ripple.",
},
{
name: "scale",
type: "number",
default: "1.15",
description: "Image zoom inside the ASCII field.",
},
{
name: "fit",
type: '"cover" | "contain" | "stretch"',
default: '"cover"',
description: "How the source image fits the canvas.",
},
{
name: "colors",
type: "string[]",
description: "One or more colors used as a luminance gradient.",
},
{
name: "colorMode",
type: '"gradient" | "source"',
default: '"gradient"',
description: "Use a supplied gradient or colors sampled from the image.",
},
{
name: "backgroundColor",
type: "string",
default: '"#07090d"',
description: "Canvas background color.",
},
{
name: "invert",
type: "boolean",
default: "false",
description: "Reverse the luminance-to-character mapping.",
},
{
name: "glitchIntensity",
type: "number",
default: "0.65",
description: "Strength and number of displaced signal bands.",
},
{
name: "glitchFrequency",
type: "number",
default: "1.4",
description: "Approximate glitches per second.",
},
{
name: "revealDuration",
type: "number",
default: "1400",
description: "Radial reveal duration in milliseconds.",
},
{
name: "alt",
type: "string",
default: '"ASCII rendering"',
description: "Accessible label for the canvas.",
},
{
name: "className",
type: "string",
description: "Additional classes for the container.",
},
];

export async function AsciiEffectDocs() {
const source = await readComponentSource("ascii-effect");

return (
<DocsPageLayout
title="ASCII Effect"
description="A responsive image-to-ASCII renderer with image, flowing ripple, and signal-glitch variations."
preview={<AsciiImagePreview />}
previewCode={imageCode}
installPackageName="ascii-effect"
installSourceCode={source ?? "// Unable to load source code"}
installSourceFilename="components/ui/ascii-effect.tsx"
usageCode={imageCode}
examples={[
{ title: "Flow", preview: <AsciiFlowPreview />, code: flowCode },
{ title: "Glitch", preview: <AsciiGlitchPreview />, code: glitchCode },
]}
props={props}
fullWidthPreview
/>
);
}
Loading
Loading