diff --git a/apps/web/app/docs/page.tsx b/apps/web/app/docs/page.tsx index 1d060f5..ad46dea 100644 --- a/apps/web/app/docs/page.tsx +++ b/apps/web/app/docs/page.tsx @@ -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 @@ -59,11 +59,9 @@ function getPreviewPosterSrc(previewVideo?: string) { function ComponentCard({ component, index, - asciiColors, }: { component: ComponentMetadata index: number - asciiColors?: string[] }) { const videoRef = useRef(null) const [isHovered, setIsHovered] = useState(false) @@ -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" && ( - + )} {previewPosterSrc && ( = [ - { 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("") - const [asciiPalette, setAsciiPalette] = useState(0) useEffect(() => { const observers = categoryOrder.map((cat) => { @@ -397,40 +385,6 @@ export default function DocsPage() {

{category}

- {category === "ASCII Effects" && ( -
- {asciiPalettes.map((palette, paletteIndex) => ( - - ))} -
- )}
{items.map((component, i) => ( @@ -438,7 +392,6 @@ export default function DocsPage() { key={component.slug} component={component} index={i} - asciiColors={category === "ASCII Effects" ? asciiPalettes[asciiPalette]?.colors : undefined} /> ))}
diff --git a/apps/web/components/docs/ascii-effect.tsx b/apps/web/components/docs/ascii-effect.tsx new file mode 100644 index 0000000..6a7ad9a --- /dev/null +++ b/apps/web/components/docs/ascii-effect.tsx @@ -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} + +
+ +
`; + +const flowCode = `${importCode} + +
+ +
`; + +const glitchCode = `${importCode} + +
+ +
`; + +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 ( + } + previewCode={imageCode} + installPackageName="ascii-effect" + installSourceCode={source ?? "// Unable to load source code"} + installSourceFilename="components/ui/ascii-effect.tsx" + usageCode={imageCode} + examples={[ + { title: "Flow", preview: , code: flowCode }, + { title: "Glitch", preview: , code: glitchCode }, + ]} + props={props} + fullWidthPreview + /> + ); +} diff --git a/apps/web/components/docs/ascii-effects.tsx b/apps/web/components/docs/ascii-effects.tsx deleted file mode 100644 index 3becce7..0000000 --- a/apps/web/components/docs/ascii-effects.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { DocsPageLayout, type PropItem } from "@/components/docs-page-layout" -import { - AsciiGlitchPreview, - AsciiImagePreview, - AsciiFlowPreview, -} from "@/components/docs/previews/ascii-effects-preview" -import { readComponentSource } from "@/lib/source-code" - -const props: PropItem[] = [ - { name: "imageSrc", type: "string", description: "Image URL rendered as ASCII characters." }, - { 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." }, - { 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 without distorting text." }, - { name: "colors", type: "string[]", description: "One or more colors used as a luminance gradient." }, - { name: "colorMode", type: '"gradient" | "source"', default: '"gradient"', description: "Use the 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." }, -] - -const entries = { - image: { - title: "ASCII Image", - description: "A clean, responsive image-to-ASCII renderer with custom character ramps, typography, luminance, and color controls.", - Preview: AsciiImagePreview, - usage: `import { AsciiImage } from "@/components/ui/ascii-effect" - -
- -
`, - }, - flow: { - title: "ASCII Flow", - description: "A directional ASCII flow field with configurable speed and a cursor-driven ripple.", - Preview: AsciiFlowPreview, - usage: `import { AsciiFlow } from "@/components/ui/ascii-effect" - -
- -
`, - }, - glitch: { - title: "ASCII Glitch", - description: "A cinematic ASCII reconstruction with a radial reveal and configurable signal displacement.", - Preview: AsciiGlitchPreview, - usage: `import { AsciiGlitch } from "@/components/ui/ascii-effect" - -
- -
`, - }, -} as const - -async function AsciiDocs({ kind }: { kind: keyof typeof entries }) { - const entry = entries[kind] - const source = await readComponentSource(`ascii-${kind}`) - const Preview = entry.Preview - - return ( - } - previewCode={entry.usage} - installPackageName={`ascii-${kind}`} - installSourceCode={source ?? "// Unable to load source code"} - installSourceFilename="components/ui/ascii-effect.tsx" - usageCode={entry.usage} - examples={[]} - props={props} - fullWidthPreview - /> - ) -} - -export async function AsciiImageDocs() { - return -} - -export async function AsciiFlowDocs() { - return -} - -export async function AsciiGlitchDocs() { - return -} diff --git a/apps/web/components/docs/lazy-registry.ts b/apps/web/components/docs/lazy-registry.ts index 61de595..dc852cb 100644 --- a/apps/web/components/docs/lazy-registry.ts +++ b/apps/web/components/docs/lazy-registry.ts @@ -10,17 +10,9 @@ const docsImportMap: Record< | { [key: string]: React.ComponentType> } > > = { - "ascii-image": () => - import("@/components/docs/ascii-effects").then((m) => ({ - default: m.AsciiImageDocs, - })), - "ascii-flow": () => - import("@/components/docs/ascii-effects").then((m) => ({ - default: m.AsciiFlowDocs, - })), - "ascii-glitch": () => - import("@/components/docs/ascii-effects").then((m) => ({ - default: m.AsciiGlitchDocs, + "ascii-effect": () => + import("@/components/docs/ascii-effect").then((m) => ({ + default: m.AsciiEffectDocs, })), "dithered-logo": () => import("@/components/docs/dithered-logo").then((m) => ({ diff --git a/apps/web/components/docs/previews/ascii-effect-preview.tsx b/apps/web/components/docs/previews/ascii-effect-preview.tsx new file mode 100644 index 0000000..597b9be --- /dev/null +++ b/apps/web/components/docs/previews/ascii-effect-preview.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { AsciiEffect } from "@workspace/ui/components/ascii-effect"; +import type { ReactNode } from "react"; + +const imageSrc = "/images/ascii-effects/chrome-bust.webp"; + +function PreviewFrame({ + children, + label, +}: { + children: ReactNode; + label: string; +}) { + return ( +
+ {children} +
+ {label} + Render / Live +
+
+ ); +} + +export function AsciiImagePreview() { + return ( + + + + ); +} + +export function AsciiFlowPreview() { + return ( + + + + ); +} + +export function AsciiGlitchPreview() { + return ( + + + + ); +} + +export function AsciiCatalogPreview() { + return ( + + ); +} diff --git a/apps/web/components/docs/previews/ascii-effects-preview.tsx b/apps/web/components/docs/previews/ascii-effects-preview.tsx deleted file mode 100644 index 01c6a38..0000000 --- a/apps/web/components/docs/previews/ascii-effects-preview.tsx +++ /dev/null @@ -1,81 +0,0 @@ -"use client" - -import { - AsciiGlitch, - AsciiImage, - AsciiFlow, -} from "@workspace/ui/components/ascii-effect" -import type { ReactNode } from "react" - -const imageSrc = "/images/ascii-effects/chrome-bust.webp" - -function PreviewFrame({ - children, - label, -}: { - children: ReactNode - label: string -}) { - return ( -
- {children} -
- {label} - Render / Live -
-
- ) -} - -export function AsciiImagePreview() { - return ( - - - - ) -} - -export function AsciiFlowPreview() { - return ( - - - - ) -} - -export function AsciiGlitchPreview() { - return ( - - - - ) -} - -export function AsciiCatalogPreview({ slug, colors }: { slug: string; colors?: string[] }) { - const common = { imageSrc, fontSize: 4.5, scale: 1, colors } - - if (slug === "ascii-flow") { - return - } - if (slug === "ascii-glitch") { - return - } - return -} diff --git a/apps/web/public/r/ascii-image.json b/apps/web/public/r/ascii-effect.json similarity index 98% rename from apps/web/public/r/ascii-image.json rename to apps/web/public/r/ascii-effect.json index bc9b09b..f72825f 100644 --- a/apps/web/public/r/ascii-image.json +++ b/apps/web/public/r/ascii-effect.json @@ -1,12 +1,12 @@ { "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "ascii-image", + "name": "ascii-effect", "type": "registry:ui", - "title": "Ascii Image", + "title": "ASCII Effect", "dependencies": [], "devDependencies": [], "registryDependencies": [], - "description": "Component for ascii-image", + "description": "Render images as responsive ASCII artwork with image, flow, and glitch variations.", "files": [ { "path": "components/ui/ascii-effect.tsx", @@ -14,4 +14,4 @@ "type": "registry:ui" } ] -} \ No newline at end of file +} diff --git a/apps/web/public/r/ascii-flow.json b/apps/web/public/r/ascii-flow.json deleted file mode 100644 index ba7e245..0000000 --- a/apps/web/public/r/ascii-flow.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "ascii-flow", - "type": "registry:ui", - "title": "Ascii Flow", - "dependencies": [], - "devDependencies": [], - "registryDependencies": [], - "description": "Component for ascii-flow", - "files": [ - { - "path": "components/ui/ascii-effect.tsx", - "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useEffect, useRef, type HTMLAttributes, type PointerEvent } from \"react\"\n\nexport interface AsciiEffectProps extends Omit, \"color\"> {\n imageSrc: string\n alt?: string\n variant?: \"image\" | \"flow\" | \"glitch\"\n chars?: string\n fontSize?: number\n fontFamily?: string\n fontWeight?: number | string\n lineHeight?: number\n characterSpacing?: number\n brightnessBoost?: number\n contrast?: number\n threshold?: number\n posterize?: number\n dither?: \"none\" | \"floyd-steinberg\" | \"bayer\"\n ditherStrength?: number\n flowSpeed?: number\n flowDirection?: number\n flowStrength?: number\n flowFrequency?: number\n mouseRadius?: number\n mouseStrength?: number\n mouseWaveSpeed?: number\n scale?: number\n fit?: \"cover\" | \"contain\" | \"stretch\"\n colors?: string[]\n colorMode?: \"gradient\" | \"source\"\n backgroundColor?: string\n invert?: boolean\n glitchIntensity?: number\n glitchFrequency?: number\n revealDuration?: number\n}\n\nexport type AsciiPresetProps = Omit\n\nconst IMAGE_COLORS = [\"#f4f4f5\", \"#a1a1aa\"]\nconst FLOW_COLORS = [\"#e2e8f0\", \"#67e8f9\", \"#818cf8\"]\nconst GLITCH_COLORS = [\"#ecfccb\", \"#a3e635\", \"#22d3ee\"]\n\nfunction clamp(value: number, min = 0, max = 1) {\n return Math.min(max, Math.max(min, value))\n}\n\nfunction parseHex(color: string) {\n const match = /^#([\\da-f]{2})([\\da-f]{2})([\\da-f]{2})$/i.exec(color)\n return match\n ? [Number.parseInt(match[1]!, 16), Number.parseInt(match[2]!, 16), Number.parseInt(match[3]!, 16)]\n : null\n}\n\nfunction gradientColor(colors: string[], amount: number) {\n if (colors.length < 2) return colors[0] ?? \"#ffffff\"\n\n const position = clamp(amount) * (colors.length - 1)\n const index = Math.min(Math.floor(position), colors.length - 2)\n const mix = position - index\n const from = parseHex(colors[index]!)\n const to = parseHex(colors[index + 1]!)\n if (!from || !to) return colors[Math.round(position)] ?? colors[0]!\n\n return `rgb(${from.map((channel, i) => Math.round(channel + (to[i]! - channel) * mix)).join(\", \")})`\n}\n\nexport function AsciiEffect({\n imageSrc,\n alt = \"ASCII rendering\",\n variant = \"image\",\n chars = \" .:-=+*#%@\",\n fontSize = 9,\n fontFamily = \"Arial, Helvetica, sans-serif\",\n fontWeight = 400,\n lineHeight = 1,\n characterSpacing = 1,\n brightnessBoost = 2.2,\n contrast = 1.1,\n threshold = 0.06,\n posterize = 32,\n dither = \"floyd-steinberg\",\n ditherStrength = 0.8,\n flowSpeed = 0.22,\n flowDirection = 0,\n flowStrength = 12,\n flowFrequency = 0.018,\n mouseRadius = 150,\n mouseStrength = 22,\n mouseWaveSpeed = 1.2,\n scale = 1.15,\n fit = \"cover\",\n colors = IMAGE_COLORS,\n colorMode = \"gradient\",\n backgroundColor = \"#07090d\",\n invert = false,\n glitchIntensity = 0.65,\n glitchFrequency = 1.4,\n revealDuration = 1400,\n className,\n onPointerMove,\n onPointerLeave,\n ...props\n}: AsciiEffectProps) {\n const containerRef = useRef(null)\n const canvasRef = useRef(null)\n const pointer = useRef({ x: 0, y: 0, targetX: 0, targetY: 0, active: false })\n\n useEffect(() => {\n const container = containerRef.current\n const canvas = canvasRef.current\n if (!container || !canvas || chars.length === 0) return\n\n const context = canvas.getContext(\"2d\")\n const sampleCanvas = document.createElement(\"canvas\")\n const sampleContext = sampleCanvas.getContext(\"2d\", { willReadFrequently: true })\n if (!context || !sampleContext) return\n\n const image = new Image()\n image.crossOrigin = \"anonymous\"\n let frame = 0\n let width = 0\n let height = 0\n let startedAt = 0\n let nextGlitchAt = 0\n let glitchUntil = 0\n let glitchBands = new Map()\n let loaded = false\n const reduceMotion = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n\n const resize = () => {\n const rect = container.getBoundingClientRect()\n const dpr = Math.min(window.devicePixelRatio || 1, 2)\n width = Math.max(1, rect.width)\n height = Math.max(1, rect.height)\n canvas.width = Math.round(width * dpr)\n canvas.height = Math.round(height * dpr)\n canvas.style.width = `${width}px`\n canvas.style.height = `${height}px`\n context.setTransform(dpr, 0, 0, dpr, 0, 0)\n if (loaded) draw(performance.now())\n }\n\n const updateGlitch = (now: number, rows: number) => {\n if (variant !== \"glitch\" || reduceMotion || glitchFrequency <= 0 || now < nextGlitchAt) return\n\n glitchBands = new Map()\n const bandCount = Math.max(1, Math.round(clamp(glitchIntensity) * 4))\n for (let band = 0; band < bandCount; band++) {\n const start = Math.floor(Math.random() * rows)\n const size = 1 + Math.floor(Math.random() * 3)\n const offset = (Math.random() - 0.5) * fontSize * 12 * clamp(glitchIntensity)\n for (let row = start; row < Math.min(rows, start + size); row++) glitchBands.set(row, offset)\n }\n glitchUntil = now + 70 + 90 * clamp(glitchIntensity)\n nextGlitchAt = now + 1000 / glitchFrequency\n }\n\n const draw = (now: number) => {\n if (!loaded || width === 0 || height === 0) return\n\n pointer.current.x += (pointer.current.targetX - pointer.current.x) * 0.08\n pointer.current.y += (pointer.current.targetY - pointer.current.y) * 0.08\n\n const cellHeight = Math.max(4, fontSize * Math.max(0.5, lineHeight))\n context.font = `${fontWeight} ${fontSize}px ${fontFamily}`\n const cellWidth = Math.max(2, context.measureText(\"M\").width * Math.max(0.5, characterSpacing))\n const columns = Math.ceil(width / cellWidth) + 2\n const rows = Math.ceil(height / cellHeight) + 2\n const radians = flowDirection * Math.PI / 180\n const directionX = Math.cos(radians)\n const directionY = Math.sin(radians)\n sampleCanvas.width = columns\n sampleCanvas.height = rows\n\n const imageScale = fit === \"stretch\"\n ? 1\n : (fit === \"contain\"\n ? Math.min(width / image.naturalWidth, height / image.naturalHeight)\n : Math.max(width / image.naturalWidth, height / image.naturalHeight)) * Math.max(0.1, scale)\n const drawWidth = fit === \"stretch\" ? columns : image.naturalWidth * imageScale / cellWidth\n const drawHeight = fit === \"stretch\" ? rows : image.naturalHeight * imageScale / cellHeight\n sampleContext.clearRect(0, 0, columns, rows)\n sampleContext.drawImage(\n image,\n (columns - drawWidth) / 2,\n (rows - drawHeight) / 2,\n drawWidth,\n drawHeight,\n )\n\n const pixels = sampleContext.getImageData(0, 0, columns, rows).data\n const steps = Math.max(2, Math.round(posterize))\n const luminanceField = new Float32Array(columns * rows)\n for (let index = 0; index < luminanceField.length; index++) {\n const pixel = index * 4\n const alpha = pixels[pixel + 3]! / 255\n let luminance = (pixels[pixel]! * 0.2126 + pixels[pixel + 1]! * 0.7152 + pixels[pixel + 2]! * 0.0722) / 255\n luminance = clamp((luminance - 0.5) * Math.max(0, contrast) + 0.5)\n luminance = clamp(luminance * brightnessBoost * alpha)\n luminanceField[index] = luminance <= threshold ? 0 : (luminance - threshold) / Math.max(0.001, 1 - threshold)\n }\n\n if (dither === \"floyd-steinberg\") {\n for (let row = 0; row < rows; row++) {\n for (let column = 0; column < columns; column++) {\n const index = row * columns + column\n const oldValue = clamp(luminanceField[index]!)\n const quantized = Math.round(oldValue * (steps - 1)) / (steps - 1)\n const value = oldValue + (quantized - oldValue) * clamp(ditherStrength)\n const error = oldValue - value\n luminanceField[index] = value\n if (column + 1 < columns) luminanceField[index + 1]! += error * 7 / 16\n if (row + 1 < rows) {\n if (column > 0) luminanceField[index + columns - 1]! += error * 3 / 16\n luminanceField[index + columns]! += error * 5 / 16\n if (column + 1 < columns) luminanceField[index + columns + 1]! += error / 16\n }\n }\n }\n } else if (dither === \"bayer\") {\n const matrix = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5]\n for (let row = 0; row < rows; row++) {\n for (let column = 0; column < columns; column++) {\n const index = row * columns + column\n const offset = (matrix[(row % 4) * 4 + column % 4]! / 16 - 0.5) * clamp(ditherStrength) / 4\n luminanceField[index] = clamp(luminanceField[index]! + offset)\n }\n }\n } else {\n for (let index = 0; index < luminanceField.length; index++) {\n luminanceField[index] = Math.round(clamp(luminanceField[index]!) * (steps - 1)) / (steps - 1)\n }\n }\n\n const reveal = variant === \"glitch\" && !reduceMotion && revealDuration > 0\n ? clamp((now - startedAt) / revealDuration)\n : 1\n\n updateGlitch(now, rows)\n if (now > glitchUntil) glitchBands.clear()\n\n context.fillStyle = backgroundColor\n context.fillRect(0, 0, width, height)\n context.textBaseline = \"top\"\n\n for (let row = 0; row < rows; row++) {\n const rowOffset = glitchBands.get(row) ?? 0\n for (let column = 0; column < columns; column++) {\n const dx = column / Math.max(1, columns - 1) - 0.5\n const dy = row / Math.max(1, rows - 1) - 0.5\n if (Math.hypot(dx, dy) > reveal * 0.72) continue\n\n let sourceColumn = column\n let sourceRow = row\n let mouseInfluence = 0\n if (variant === \"flow\" && !reduceMotion) {\n const x = column * cellWidth\n const y = row * cellHeight\n const phase = (x * directionX + y * directionY) * flowFrequency + now * flowSpeed * Math.PI * 0.002\n const crossPhase = (-x * directionY + y * directionX) * flowFrequency * 0.65\n const drift = (Math.sin(phase) + Math.sin(phase * 0.61 + crossPhase) * 0.45) * flowStrength\n let mouseDisplacement = 0\n\n if (pointer.current.active && mouseRadius > 0) {\n const mouseX = x - pointer.current.x\n const mouseY = y - pointer.current.y\n const distance = Math.hypot(mouseX, mouseY)\n mouseInfluence = clamp(1 - distance / mouseRadius)\n if (distance > 0 && mouseInfluence > 0) {\n const ripple = Math.sin(distance * 0.055 - now * mouseWaveSpeed * Math.PI * 0.002)\n mouseDisplacement = mouseInfluence ** 2 * mouseStrength * ripple\n sourceColumn -= mouseX / distance * mouseDisplacement / cellWidth\n sourceRow -= mouseY / distance * mouseDisplacement / cellHeight\n }\n }\n\n sourceColumn -= directionX * drift / cellWidth\n sourceRow -= directionY * drift / cellHeight\n }\n\n const sampledColumn = Math.round(clamp(sourceColumn, 0, columns - 1))\n const sampledRow = Math.round(clamp(sourceRow, 0, rows - 1))\n const sampleIndex = sampledRow * columns + sampledColumn\n const pixel = sampleIndex * 4\n let luminance = clamp(luminanceField[sampleIndex]! + mouseInfluence * 0.08)\n if (invert) luminance = 1 - luminance\n\n const character = chars[Math.min(chars.length - 1, Math.floor(luminance * (chars.length - 1)))]\n if (!character?.trim()) continue\n\n context.fillStyle = colorMode === \"source\"\n ? `rgb(${pixels[pixel]}, ${pixels[pixel + 1]}, ${pixels[pixel + 2]})`\n : gradientColor(colors, luminance)\n context.fillText(character, column * cellWidth - cellWidth + rowOffset, row * cellHeight - cellHeight)\n }\n }\n }\n\n const animate = (now: number) => {\n draw(now)\n if (!reduceMotion && variant !== \"image\") frame = requestAnimationFrame(animate)\n }\n\n const start = () => {\n if (loaded) return\n loaded = true\n startedAt = performance.now()\n resize()\n if (!reduceMotion && variant !== \"image\") frame = requestAnimationFrame(animate)\n }\n image.onload = start\n image.src = imageSrc\n if (image.complete) start()\n\n const observer = new ResizeObserver(resize)\n observer.observe(container)\n\n return () => {\n cancelAnimationFrame(frame)\n observer.disconnect()\n image.onload = null\n }\n }, [backgroundColor, brightnessBoost, characterSpacing, chars, colorMode, colors, contrast, dither, ditherStrength, fit, flowDirection, flowFrequency, flowSpeed, flowStrength, fontFamily, fontSize, fontWeight, glitchFrequency, glitchIntensity, imageSrc, invert, lineHeight, mouseRadius, mouseStrength, mouseWaveSpeed, posterize, revealDuration, scale, threshold, variant])\n\n const trackPointer = (event: PointerEvent) => {\n onPointerMove?.(event)\n if (variant !== \"flow\") return\n const rect = event.currentTarget.getBoundingClientRect()\n const x = event.clientX - rect.left\n const y = event.clientY - rect.top\n if (!pointer.current.active) {\n pointer.current.x = x\n pointer.current.y = y\n }\n pointer.current.active = true\n pointer.current.targetX = x\n pointer.current.targetY = y\n }\n\n const resetPointer = (event: PointerEvent) => {\n onPointerLeave?.(event)\n pointer.current.active = false\n }\n\n return (\n \n \n \n )\n}\n\nexport function AsciiImage(props: AsciiPresetProps) {\n return \n}\n\nexport function AsciiFlow(props: AsciiPresetProps) {\n return \n}\n\nexport function AsciiGlitch(props: AsciiPresetProps) {\n return \n}\n", - "type": "registry:ui" - } - ] -} \ No newline at end of file diff --git a/apps/web/public/r/ascii-glitch.json b/apps/web/public/r/ascii-glitch.json deleted file mode 100644 index 80dfc7f..0000000 --- a/apps/web/public/r/ascii-glitch.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "ascii-glitch", - "type": "registry:ui", - "title": "Ascii Glitch", - "dependencies": [], - "devDependencies": [], - "registryDependencies": [], - "description": "Component for ascii-glitch", - "files": [ - { - "path": "components/ui/ascii-effect.tsx", - "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useEffect, useRef, type HTMLAttributes, type PointerEvent } from \"react\"\n\nexport interface AsciiEffectProps extends Omit, \"color\"> {\n imageSrc: string\n alt?: string\n variant?: \"image\" | \"flow\" | \"glitch\"\n chars?: string\n fontSize?: number\n fontFamily?: string\n fontWeight?: number | string\n lineHeight?: number\n characterSpacing?: number\n brightnessBoost?: number\n contrast?: number\n threshold?: number\n posterize?: number\n dither?: \"none\" | \"floyd-steinberg\" | \"bayer\"\n ditherStrength?: number\n flowSpeed?: number\n flowDirection?: number\n flowStrength?: number\n flowFrequency?: number\n mouseRadius?: number\n mouseStrength?: number\n mouseWaveSpeed?: number\n scale?: number\n fit?: \"cover\" | \"contain\" | \"stretch\"\n colors?: string[]\n colorMode?: \"gradient\" | \"source\"\n backgroundColor?: string\n invert?: boolean\n glitchIntensity?: number\n glitchFrequency?: number\n revealDuration?: number\n}\n\nexport type AsciiPresetProps = Omit\n\nconst IMAGE_COLORS = [\"#f4f4f5\", \"#a1a1aa\"]\nconst FLOW_COLORS = [\"#e2e8f0\", \"#67e8f9\", \"#818cf8\"]\nconst GLITCH_COLORS = [\"#ecfccb\", \"#a3e635\", \"#22d3ee\"]\n\nfunction clamp(value: number, min = 0, max = 1) {\n return Math.min(max, Math.max(min, value))\n}\n\nfunction parseHex(color: string) {\n const match = /^#([\\da-f]{2})([\\da-f]{2})([\\da-f]{2})$/i.exec(color)\n return match\n ? [Number.parseInt(match[1]!, 16), Number.parseInt(match[2]!, 16), Number.parseInt(match[3]!, 16)]\n : null\n}\n\nfunction gradientColor(colors: string[], amount: number) {\n if (colors.length < 2) return colors[0] ?? \"#ffffff\"\n\n const position = clamp(amount) * (colors.length - 1)\n const index = Math.min(Math.floor(position), colors.length - 2)\n const mix = position - index\n const from = parseHex(colors[index]!)\n const to = parseHex(colors[index + 1]!)\n if (!from || !to) return colors[Math.round(position)] ?? colors[0]!\n\n return `rgb(${from.map((channel, i) => Math.round(channel + (to[i]! - channel) * mix)).join(\", \")})`\n}\n\nexport function AsciiEffect({\n imageSrc,\n alt = \"ASCII rendering\",\n variant = \"image\",\n chars = \" .:-=+*#%@\",\n fontSize = 9,\n fontFamily = \"Arial, Helvetica, sans-serif\",\n fontWeight = 400,\n lineHeight = 1,\n characterSpacing = 1,\n brightnessBoost = 2.2,\n contrast = 1.1,\n threshold = 0.06,\n posterize = 32,\n dither = \"floyd-steinberg\",\n ditherStrength = 0.8,\n flowSpeed = 0.22,\n flowDirection = 0,\n flowStrength = 12,\n flowFrequency = 0.018,\n mouseRadius = 150,\n mouseStrength = 22,\n mouseWaveSpeed = 1.2,\n scale = 1.15,\n fit = \"cover\",\n colors = IMAGE_COLORS,\n colorMode = \"gradient\",\n backgroundColor = \"#07090d\",\n invert = false,\n glitchIntensity = 0.65,\n glitchFrequency = 1.4,\n revealDuration = 1400,\n className,\n onPointerMove,\n onPointerLeave,\n ...props\n}: AsciiEffectProps) {\n const containerRef = useRef(null)\n const canvasRef = useRef(null)\n const pointer = useRef({ x: 0, y: 0, targetX: 0, targetY: 0, active: false })\n\n useEffect(() => {\n const container = containerRef.current\n const canvas = canvasRef.current\n if (!container || !canvas || chars.length === 0) return\n\n const context = canvas.getContext(\"2d\")\n const sampleCanvas = document.createElement(\"canvas\")\n const sampleContext = sampleCanvas.getContext(\"2d\", { willReadFrequently: true })\n if (!context || !sampleContext) return\n\n const image = new Image()\n image.crossOrigin = \"anonymous\"\n let frame = 0\n let width = 0\n let height = 0\n let startedAt = 0\n let nextGlitchAt = 0\n let glitchUntil = 0\n let glitchBands = new Map()\n let loaded = false\n const reduceMotion = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n\n const resize = () => {\n const rect = container.getBoundingClientRect()\n const dpr = Math.min(window.devicePixelRatio || 1, 2)\n width = Math.max(1, rect.width)\n height = Math.max(1, rect.height)\n canvas.width = Math.round(width * dpr)\n canvas.height = Math.round(height * dpr)\n canvas.style.width = `${width}px`\n canvas.style.height = `${height}px`\n context.setTransform(dpr, 0, 0, dpr, 0, 0)\n if (loaded) draw(performance.now())\n }\n\n const updateGlitch = (now: number, rows: number) => {\n if (variant !== \"glitch\" || reduceMotion || glitchFrequency <= 0 || now < nextGlitchAt) return\n\n glitchBands = new Map()\n const bandCount = Math.max(1, Math.round(clamp(glitchIntensity) * 4))\n for (let band = 0; band < bandCount; band++) {\n const start = Math.floor(Math.random() * rows)\n const size = 1 + Math.floor(Math.random() * 3)\n const offset = (Math.random() - 0.5) * fontSize * 12 * clamp(glitchIntensity)\n for (let row = start; row < Math.min(rows, start + size); row++) glitchBands.set(row, offset)\n }\n glitchUntil = now + 70 + 90 * clamp(glitchIntensity)\n nextGlitchAt = now + 1000 / glitchFrequency\n }\n\n const draw = (now: number) => {\n if (!loaded || width === 0 || height === 0) return\n\n pointer.current.x += (pointer.current.targetX - pointer.current.x) * 0.08\n pointer.current.y += (pointer.current.targetY - pointer.current.y) * 0.08\n\n const cellHeight = Math.max(4, fontSize * Math.max(0.5, lineHeight))\n context.font = `${fontWeight} ${fontSize}px ${fontFamily}`\n const cellWidth = Math.max(2, context.measureText(\"M\").width * Math.max(0.5, characterSpacing))\n const columns = Math.ceil(width / cellWidth) + 2\n const rows = Math.ceil(height / cellHeight) + 2\n const radians = flowDirection * Math.PI / 180\n const directionX = Math.cos(radians)\n const directionY = Math.sin(radians)\n sampleCanvas.width = columns\n sampleCanvas.height = rows\n\n const imageScale = fit === \"stretch\"\n ? 1\n : (fit === \"contain\"\n ? Math.min(width / image.naturalWidth, height / image.naturalHeight)\n : Math.max(width / image.naturalWidth, height / image.naturalHeight)) * Math.max(0.1, scale)\n const drawWidth = fit === \"stretch\" ? columns : image.naturalWidth * imageScale / cellWidth\n const drawHeight = fit === \"stretch\" ? rows : image.naturalHeight * imageScale / cellHeight\n sampleContext.clearRect(0, 0, columns, rows)\n sampleContext.drawImage(\n image,\n (columns - drawWidth) / 2,\n (rows - drawHeight) / 2,\n drawWidth,\n drawHeight,\n )\n\n const pixels = sampleContext.getImageData(0, 0, columns, rows).data\n const steps = Math.max(2, Math.round(posterize))\n const luminanceField = new Float32Array(columns * rows)\n for (let index = 0; index < luminanceField.length; index++) {\n const pixel = index * 4\n const alpha = pixels[pixel + 3]! / 255\n let luminance = (pixels[pixel]! * 0.2126 + pixels[pixel + 1]! * 0.7152 + pixels[pixel + 2]! * 0.0722) / 255\n luminance = clamp((luminance - 0.5) * Math.max(0, contrast) + 0.5)\n luminance = clamp(luminance * brightnessBoost * alpha)\n luminanceField[index] = luminance <= threshold ? 0 : (luminance - threshold) / Math.max(0.001, 1 - threshold)\n }\n\n if (dither === \"floyd-steinberg\") {\n for (let row = 0; row < rows; row++) {\n for (let column = 0; column < columns; column++) {\n const index = row * columns + column\n const oldValue = clamp(luminanceField[index]!)\n const quantized = Math.round(oldValue * (steps - 1)) / (steps - 1)\n const value = oldValue + (quantized - oldValue) * clamp(ditherStrength)\n const error = oldValue - value\n luminanceField[index] = value\n if (column + 1 < columns) luminanceField[index + 1]! += error * 7 / 16\n if (row + 1 < rows) {\n if (column > 0) luminanceField[index + columns - 1]! += error * 3 / 16\n luminanceField[index + columns]! += error * 5 / 16\n if (column + 1 < columns) luminanceField[index + columns + 1]! += error / 16\n }\n }\n }\n } else if (dither === \"bayer\") {\n const matrix = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5]\n for (let row = 0; row < rows; row++) {\n for (let column = 0; column < columns; column++) {\n const index = row * columns + column\n const offset = (matrix[(row % 4) * 4 + column % 4]! / 16 - 0.5) * clamp(ditherStrength) / 4\n luminanceField[index] = clamp(luminanceField[index]! + offset)\n }\n }\n } else {\n for (let index = 0; index < luminanceField.length; index++) {\n luminanceField[index] = Math.round(clamp(luminanceField[index]!) * (steps - 1)) / (steps - 1)\n }\n }\n\n const reveal = variant === \"glitch\" && !reduceMotion && revealDuration > 0\n ? clamp((now - startedAt) / revealDuration)\n : 1\n\n updateGlitch(now, rows)\n if (now > glitchUntil) glitchBands.clear()\n\n context.fillStyle = backgroundColor\n context.fillRect(0, 0, width, height)\n context.textBaseline = \"top\"\n\n for (let row = 0; row < rows; row++) {\n const rowOffset = glitchBands.get(row) ?? 0\n for (let column = 0; column < columns; column++) {\n const dx = column / Math.max(1, columns - 1) - 0.5\n const dy = row / Math.max(1, rows - 1) - 0.5\n if (Math.hypot(dx, dy) > reveal * 0.72) continue\n\n let sourceColumn = column\n let sourceRow = row\n let mouseInfluence = 0\n if (variant === \"flow\" && !reduceMotion) {\n const x = column * cellWidth\n const y = row * cellHeight\n const phase = (x * directionX + y * directionY) * flowFrequency + now * flowSpeed * Math.PI * 0.002\n const crossPhase = (-x * directionY + y * directionX) * flowFrequency * 0.65\n const drift = (Math.sin(phase) + Math.sin(phase * 0.61 + crossPhase) * 0.45) * flowStrength\n let mouseDisplacement = 0\n\n if (pointer.current.active && mouseRadius > 0) {\n const mouseX = x - pointer.current.x\n const mouseY = y - pointer.current.y\n const distance = Math.hypot(mouseX, mouseY)\n mouseInfluence = clamp(1 - distance / mouseRadius)\n if (distance > 0 && mouseInfluence > 0) {\n const ripple = Math.sin(distance * 0.055 - now * mouseWaveSpeed * Math.PI * 0.002)\n mouseDisplacement = mouseInfluence ** 2 * mouseStrength * ripple\n sourceColumn -= mouseX / distance * mouseDisplacement / cellWidth\n sourceRow -= mouseY / distance * mouseDisplacement / cellHeight\n }\n }\n\n sourceColumn -= directionX * drift / cellWidth\n sourceRow -= directionY * drift / cellHeight\n }\n\n const sampledColumn = Math.round(clamp(sourceColumn, 0, columns - 1))\n const sampledRow = Math.round(clamp(sourceRow, 0, rows - 1))\n const sampleIndex = sampledRow * columns + sampledColumn\n const pixel = sampleIndex * 4\n let luminance = clamp(luminanceField[sampleIndex]! + mouseInfluence * 0.08)\n if (invert) luminance = 1 - luminance\n\n const character = chars[Math.min(chars.length - 1, Math.floor(luminance * (chars.length - 1)))]\n if (!character?.trim()) continue\n\n context.fillStyle = colorMode === \"source\"\n ? `rgb(${pixels[pixel]}, ${pixels[pixel + 1]}, ${pixels[pixel + 2]})`\n : gradientColor(colors, luminance)\n context.fillText(character, column * cellWidth - cellWidth + rowOffset, row * cellHeight - cellHeight)\n }\n }\n }\n\n const animate = (now: number) => {\n draw(now)\n if (!reduceMotion && variant !== \"image\") frame = requestAnimationFrame(animate)\n }\n\n const start = () => {\n if (loaded) return\n loaded = true\n startedAt = performance.now()\n resize()\n if (!reduceMotion && variant !== \"image\") frame = requestAnimationFrame(animate)\n }\n image.onload = start\n image.src = imageSrc\n if (image.complete) start()\n\n const observer = new ResizeObserver(resize)\n observer.observe(container)\n\n return () => {\n cancelAnimationFrame(frame)\n observer.disconnect()\n image.onload = null\n }\n }, [backgroundColor, brightnessBoost, characterSpacing, chars, colorMode, colors, contrast, dither, ditherStrength, fit, flowDirection, flowFrequency, flowSpeed, flowStrength, fontFamily, fontSize, fontWeight, glitchFrequency, glitchIntensity, imageSrc, invert, lineHeight, mouseRadius, mouseStrength, mouseWaveSpeed, posterize, revealDuration, scale, threshold, variant])\n\n const trackPointer = (event: PointerEvent) => {\n onPointerMove?.(event)\n if (variant !== \"flow\") return\n const rect = event.currentTarget.getBoundingClientRect()\n const x = event.clientX - rect.left\n const y = event.clientY - rect.top\n if (!pointer.current.active) {\n pointer.current.x = x\n pointer.current.y = y\n }\n pointer.current.active = true\n pointer.current.targetX = x\n pointer.current.targetY = y\n }\n\n const resetPointer = (event: PointerEvent) => {\n onPointerLeave?.(event)\n pointer.current.active = false\n }\n\n return (\n \n \n \n )\n}\n\nexport function AsciiImage(props: AsciiPresetProps) {\n return \n}\n\nexport function AsciiFlow(props: AsciiPresetProps) {\n return \n}\n\nexport function AsciiGlitch(props: AsciiPresetProps) {\n return \n}\n", - "type": "registry:ui" - } - ] -} \ No newline at end of file diff --git a/apps/web/public/r/registry.json b/apps/web/public/r/registry.json index 8896d7e..898dcaf 100644 --- a/apps/web/public/r/registry.json +++ b/apps/web/public/r/registry.json @@ -2,27 +2,14 @@ "$schema": "https://ui.shadcn.com/schema/registry.json", "name": "componentry", "homepage": "https://componentry.fun", - "aliases": [ - "componentry", - "componentryui", - "ui", - "cmp" - ], + "aliases": ["componentry", "componentryui", "ui", "cmp"], "items": [ { "name": "animated-gradient", "type": "registry:ui" }, { - "name": "ascii-flow", - "type": "registry:ui" - }, - { - "name": "ascii-glitch", - "type": "registry:ui" - }, - { - "name": "ascii-image", + "name": "ascii-effect", "type": "registry:ui" }, { @@ -81,6 +68,10 @@ "name": "github-calendar", "type": "registry:ui" }, + { + "name": "gradient-hero-01", + "type": "registry:block" + }, { "name": "hero-geometric", "type": "registry:ui" @@ -236,10 +227,6 @@ { "name": "webgl-liquid", "type": "registry:ui" - }, - { - "name": "gradient-hero-01", - "type": "registry:block" } ] } diff --git a/apps/web/registry/index.ts b/apps/web/registry/index.ts index 6b78809..64d074b 100644 --- a/apps/web/registry/index.ts +++ b/apps/web/registry/index.ts @@ -309,25 +309,11 @@ export const components: Record = { "https://pub-a50e7f4ea75a4970a1738e50d53b6eb1.r2.dev/preview-videos/hero-backgrounds/liquidchrome.webm", }, // ASCII Effects - "ascii-image": { - title: "ASCII Image", - description: "Turn any image into crisp, responsive ASCII artwork.", + "ascii-effect": { + title: "ASCII Effect", + description: "Render images as responsive ASCII artwork with image, flow, and glitch variations.", category: "ASCII Effects", - slug: "ascii-image", - addedAt: "2026-07-21", - }, - "ascii-flow": { - title: "ASCII Flow", - description: "Directional ASCII motion with configurable speed and cursor ripples.", - category: "ASCII Effects", - slug: "ascii-flow", - addedAt: "2026-07-21", - }, - "ascii-glitch": { - title: "ASCII Glitch", - description: "A radial ASCII reveal punctuated by configurable signal glitches.", - category: "ASCII Effects", - slug: "ascii-glitch", + slug: "ascii-effect", addedAt: "2026-07-21", }, // Visual Effects diff --git a/docs/COMPONENT_CREATION_GUIDE.md b/docs/COMPONENT_CREATION_GUIDE.md index cfad3ed..784dc61 100644 --- a/docs/COMPONENT_CREATION_GUIDE.md +++ b/docs/COMPONENT_CREATION_GUIDE.md @@ -193,21 +193,6 @@ export const components: Record = { | `"Visual Effects"` | Decorative effects (Blobs, Particles, Noise) | | `"ASCII Effects"` | Image-to-ASCII renderers and animated ASCII effects | -### Shared Source Files - -Related registry entries may share one implementation file. Pass the shared source -name as the second argument when generating each entry: - -```bash -node scripts/generate-registry.js ascii-image ascii-effect -node scripts/generate-registry.js ascii-flow ascii-effect -node scripts/generate-registry.js ascii-glitch ascii-effect -``` - -When using this pattern, set `installSourceFilename` in every docs page to the -shared registry path (for example, `"components/ui/ascii-effect.tsx"`) so the -manual-install filename matches the import shown in usage examples. - --- ## 📝 Step 4: Create Documentation Component diff --git a/scripts/generate-registry.js b/scripts/generate-registry.js index 1d5f677..fbc1007 100644 --- a/scripts/generate-registry.js +++ b/scripts/generate-registry.js @@ -13,13 +13,12 @@ const WEBGL_ERROR_BOUNDARY_SOURCE_PATH = path.join(COMPONENT_DIR, WEBGL_ERROR_BO const args = process.argv.slice(2); if (args.length === 0) { - console.error('Please provide a component name and optional source name (e.g., ascii-image ascii-effect)'); + console.error('Please provide a component name (e.g., scroll-based-velocity)'); process.exit(1); } const componentName = args[0]; -const sourceComponentName = args[1] || componentName; -const componentFilename = `${sourceComponentName}.tsx`; +const componentFilename = `${componentName}.tsx`; const sourcePath = path.join(COMPONENT_DIR, componentFilename); const registryPath = path.join(REGISTRY_DIR, `${componentName}.json`);