diff --git a/jsrepo.config.ts b/jsrepo.config.ts index e0fbd266..9c782a5e 100644 --- a/jsrepo.config.ts +++ b/jsrepo.config.ts @@ -1,4 +1,5 @@ -import { defineConfig, type RegistryItem } from 'jsrepo'; +import fs from 'node:fs'; +import { defineConfig, type RegistryItem, type RegistryItemFile } from 'jsrepo'; import { output } from '@jsrepo/shadcn'; import { type Category, componentMetadata, type Variant } from './src/constants/Information'; @@ -79,13 +80,23 @@ function defineComponent({ ...(title === 'Lanyard' ? { dependencyResolution: 'manual' as const } : {}) }; - const filesForVariant = (basePath: string, sourceFile: string, styleFile?: string) => - title === 'Lanyard' - ? [ - ...(styleFile ? [{ path: `${basePath}/${styleFile}` }] : []), - { path: `${basePath}/${sourceFile}` } - ] - : [{ path: basePath }]; + const filesForVariant = (basePath: string, sourceFile: string, styleFile?: string): RegistryItemFile[] => { + // Lanyard also ships binary assets (card.glb, lanyard.png) which can't go through the registry, + // so only its source files are listed instead of the whole folder. + if (title === 'Lanyard') { + return [...(styleFile ? [defineStylesheet(basePath, styleFile)] : []), { path: `${basePath}/${sourceFile}` }]; + } + + // Variants without a stylesheet can ship the whole folder as-is. + if (!styleFile || !fs.existsSync(`${basePath}/${styleFile}`)) return [{ path: basePath }]; + + // A folder can't declare a type per file, so variants that ship a stylesheet are listed file by + // file to give the stylesheet its own type. See defineStylesheet. + return fs + .readdirSync(basePath) + .sort() + .map(file => (file === styleFile ? defineStylesheet(basePath, file) : { path: `${basePath}/${file}` })); + }; // this might warrant a bit of explanation // basically we check if the variant is included in the variants array and if so we return the item as part of an array @@ -137,3 +148,24 @@ function defineComponent({ : []) ]; } + +/** + * Define a stylesheet to be exposed from the registry. + * + * The shadcn CLI parses every file it installs as JavaScript/TypeScript unless the file is typed as + * `registry:file`, so a stylesheet shipped as `registry:component` makes `shadcn add` fail with + * `Unexpected token (1:0)`. `registry:file` requires an explicit target, and pointing it at the + * `components` alias puts the stylesheet next to the component itself so the component's relative + * `./.css` import keeps resolving. + * + * @param basePath The path to the component folder. + * @param styleFile The name of the stylesheet inside that folder. + * @returns A RegistryItemFile object for the stylesheet. + */ +function defineStylesheet(basePath: string, styleFile: string): RegistryItemFile { + return { + path: `${basePath}/${styleFile}`, + type: 'file', + target: `@components/${styleFile}` + }; +} diff --git a/public/r/AccordionGallery-JS-CSS.json b/public/r/AccordionGallery-JS-CSS.json index 495a6db0..1bc9ab59 100644 --- a/public/r/AccordionGallery-JS-CSS.json +++ b/public/r/AccordionGallery-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "AccordionGallery/AccordionGallery.css", + "type": "registry:file", + "path": "AccordionGallery.css", + "target": "@components/AccordionGallery.css", "content": ".accordion-gallery {\n --ag-accent: #ffffff;\n --ag-overlay: #060010;\n --ag-text: #ffffff;\n --ag-gap: 10px;\n --ag-radius: 16px;\n --ag-media-size: 320px;\n\n display: flex;\n flex-direction: row;\n gap: var(--ag-gap);\n width: 100%;\n max-width: 100%;\n perspective: 1400px;\n perspective-origin: 50% 50%;\n}\n\n.accordion-gallery--vertical {\n flex-direction: column;\n}\n\n.ag-panel {\n position: relative;\n flex: 1 1 0;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n border-radius: var(--ag-radius);\n cursor: pointer;\n display: block;\n text-decoration: none;\n outline: none;\n transform-style: preserve-3d;\n transform-origin: center center;\n background: #0a0713;\n box-shadow: 0 10px 30px -18px rgba(0, 0, 0, 0.8);\n will-change: flex-grow, transform;\n -webkit-tap-highlight-color: transparent;\n}\n\n.ag-panel:focus-visible {\n box-shadow:\n 0 0 0 2px var(--ag-accent),\n 0 10px 30px -18px rgba(0, 0, 0, 0.8);\n}\n\n.ag-panel__frame {\n position: absolute;\n inset: 0;\n overflow: hidden;\n border-radius: inherit;\n}\n\n.ag-panel__media {\n --ag-gray: 1;\n --ag-dim: 0.35;\n position: absolute;\n top: 50%;\n left: 50%;\n width: var(--ag-media-size);\n height: 100%;\n filter: grayscale(var(--ag-gray));\n will-change: transform, filter;\n}\n\n.accordion-gallery--vertical .ag-panel__media {\n width: 100%;\n height: var(--ag-media-size);\n}\n\n.ag-panel__media img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n user-select: none;\n -webkit-user-drag: none;\n}\n\n.ag-panel__overlay {\n position: absolute;\n inset: 0;\n pointer-events: none;\n background:\n linear-gradient(180deg, transparent 45%, color-mix(in srgb, var(--ag-overlay) 78%, transparent) 100%),\n color-mix(in srgb, var(--ag-overlay) calc(var(--ag-dim, 0.35) * 100%), transparent);\n}\n\n.ag-panel__label {\n position: absolute;\n left: 20px;\n bottom: 20px;\n right: 20px;\n display: flex;\n align-items: center;\n gap: 12px;\n pointer-events: none;\n z-index: 2;\n}\n\n.ag-panel__bar {\n flex: 0 0 auto;\n width: 3px;\n height: 26px;\n border-radius: 3px;\n background: var(--ag-accent);\n opacity: 0;\n box-shadow: 0 0 12px color-mix(in srgb, var(--ag-accent) 60%, transparent);\n}\n\n.ag-panel__text {\n color: var(--ag-text);\n font-family: inherit;\n font-weight: 600;\n font-size: clamp(1rem, 1.4vw, 1.4rem);\n letter-spacing: 0.01em;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n opacity: 0;\n text-shadow: 0 2px 14px rgba(0, 0, 0, 0.55);\n}\n\n@media (max-width: 520px) {\n .accordion-gallery {\n flex-direction: column;\n perspective: none;\n height: auto !important;\n }\n .ag-panel {\n min-height: 84px;\n transform: none !important;\n }\n .accordion-gallery .ag-panel__media {\n width: 100%;\n height: var(--ag-media-size);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .ag-panel,\n .ag-panel__media {\n will-change: auto;\n }\n}\n" }, { "type": "registry:component", - "path": "AccordionGallery/AccordionGallery.jsx", + "path": "AccordionGallery.jsx", "content": "import { useRef, useEffect, useState, useCallback } from 'react';\nimport { gsap } from 'gsap';\n\nimport './AccordionGallery.css';\n\nconst DEFAULT_ITEMS = [\n { image: 'https://picsum.photos/id/1015/900/1200', label: 'Canyon', link: '#' },\n { image: 'https://picsum.photos/id/1018/900/1200', label: 'Ridgeline', link: '#' },\n { image: 'https://picsum.photos/id/1039/900/1200', label: 'Falls', link: '#' },\n { image: 'https://picsum.photos/id/1043/900/1200', label: 'Harbour', link: '#' },\n { image: 'https://picsum.photos/id/1044/900/1200', label: 'Skyline', link: '#' }\n];\n\nconst AccordionGallery = ({\n items = DEFAULT_ITEMS,\n defaultIndex = 2,\n accentColor = '#ffffff',\n overlayColor = '#060010',\n textColor = '#ffffff',\n height = 460,\n gap = 10,\n radius = 16,\n expandRatio = 0.52,\n orientation = 'horizontal',\n duration = 0.6,\n ease = 'power3.out',\n parallax = 0.5,\n tilt = 8,\n stagger = 0.06,\n trigger = 'hover',\n showLabels = true,\n grayscale = true,\n className = ''\n}) => {\n const rootRef = useRef(null);\n const panelRefs = useRef([]);\n const mediaRefs = useRef([]);\n const barRefs = useRef([]);\n const textRefs = useRef([]);\n const tlRef = useRef(null);\n const firstRunRef = useRef(true);\n const mediaSizeRef = useRef(320);\n\n const vertical = orientation === 'vertical';\n const count = items.length;\n const [active, setActive] = useState(Math.min(Math.max(defaultIndex, 0), count - 1));\n\n const prefersReduced =\n typeof window !== 'undefined' && window.matchMedia\n ? window.matchMedia('(prefers-reduced-motion: reduce)').matches\n : false;\n\n const applyLayout = useCallback(\n animate => {\n const panels = panelRefs.current;\n if (!panels.length) return;\n\n const r = Math.min(Math.max(expandRatio, 0.2), 0.9);\n const grow = count > 1 ? (r * (count - 1)) / (1 - r) : 1;\n const mediaSize = mediaSizeRef.current;\n\n tlRef.current?.kill();\n const dur = animate && !prefersReduced ? duration : 0;\n const tl = gsap.timeline();\n\n panels.forEach((panel, i) => {\n if (!panel) return;\n const isActive = i === active;\n const media = mediaRefs.current[i];\n const bar = barRefs.current[i];\n const text = textRefs.current[i];\n\n const rot = isActive ? 0 : i < active ? tilt : -tilt;\n const rotProp = vertical ? { rotateX: -rot } : { rotateY: rot };\n\n tl.to(panel, { flexGrow: isActive ? grow : 1, ...rotProp, duration: dur, ease }, 0);\n\n if (media) {\n const drift = Math.max(-1.5, Math.min(1.5, active - i));\n const shift = drift * parallax * mediaSize * 0.06;\n const gray = grayscale ? (isActive ? 0 : 1) : 0;\n tl.to(\n media,\n {\n xPercent: -50,\n yPercent: -50,\n x: vertical ? 0 : isActive ? 0 : shift,\n y: vertical ? (isActive ? 0 : shift) : 0,\n '--ag-gray': gray,\n '--ag-dim': isActive ? 0 : 0.35,\n duration: dur,\n ease\n },\n 0\n );\n }\n\n if (showLabels && bar && text) {\n if (isActive) {\n tl.to([bar, text], { opacity: 1, x: 0, duration: dur, ease, stagger: prefersReduced ? 0 : stagger }, 0);\n } else {\n tl.to([bar, text], { opacity: 0, x: -14, duration: dur * 0.6, ease }, 0);\n }\n }\n });\n\n tlRef.current = tl;\n },\n [\n active,\n count,\n expandRatio,\n duration,\n ease,\n vertical,\n tilt,\n parallax,\n grayscale,\n showLabels,\n stagger,\n prefersReduced\n ]\n );\n\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n\n const measure = () => {\n const rect = el.getBoundingClientRect();\n const total = vertical ? rect.height : rect.width;\n const usable = Math.max(total - gap * (count - 1), 120);\n const size = Math.max(140, usable * Math.min(Math.max(expandRatio, 0.2), 0.9) * 1.22);\n mediaSizeRef.current = size;\n el.style.setProperty('--ag-media-size', `${size}px`);\n applyLayout(!firstRunRef.current);\n };\n\n measure();\n const ro = new ResizeObserver(measure);\n ro.observe(el);\n return () => ro.disconnect();\n }, [applyLayout, gap, count, expandRatio, vertical]);\n\n useEffect(() => {\n applyLayout(!firstRunRef.current);\n firstRunRef.current = false;\n }, [applyLayout]);\n\n useEffect(\n () => () => {\n tlRef.current?.kill();\n },\n []\n );\n\n const handleEnter = i => {\n if (trigger === 'hover') setActive(i);\n };\n\n const handleClick = (i, e) => {\n if (i !== active) {\n e.preventDefault();\n setActive(i);\n }\n };\n\n const handleKeyDown = (i, e) => {\n if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {\n e.preventDefault();\n setActive((i + 1) % count);\n } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {\n e.preventDefault();\n setActive((i - 1 + count) % count);\n }\n };\n\n return (\n \n {items.map((item, i) => {\n const isActive = i === active;\n const Tag = item.link ? 'a' : 'div';\n return (\n (panelRefs.current[i] = el)}\n className={`ag-panel${isActive ? ' ag-panel--active' : ''}`}\n style={{ borderRadius: `${radius}px` }}\n href={item.link || undefined}\n onClick={e => handleClick(i, e)}\n onMouseEnter={() => handleEnter(i)}\n onFocus={() => setActive(i)}\n onKeyDown={e => handleKeyDown(i, e)}\n role=\"listitem\"\n tabIndex={0}\n aria-current={isActive ? 'true' : undefined}\n aria-label={item.label}\n >\n \n (mediaRefs.current[i] = el)}>\n {item.alt\n \n \n \n {showLabels && (\n \n (barRefs.current[i] = el)} />\n (textRefs.current[i] = el)}>\n {item.label}\n \n \n )}\n \n );\n })}\n \n );\n};\n\nexport default AccordionGallery;\n" } ], diff --git a/public/r/AccordionGallery-TS-CSS.json b/public/r/AccordionGallery-TS-CSS.json index 24fafaa6..11b9c231 100644 --- a/public/r/AccordionGallery-TS-CSS.json +++ b/public/r/AccordionGallery-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "AccordionGallery/AccordionGallery.css", + "type": "registry:file", + "path": "AccordionGallery.css", + "target": "@components/AccordionGallery.css", "content": ".accordion-gallery {\n --ag-accent: #ffffff;\n --ag-overlay: #060010;\n --ag-text: #ffffff;\n --ag-gap: 10px;\n --ag-radius: 16px;\n --ag-media-size: 320px;\n\n display: flex;\n flex-direction: row;\n gap: var(--ag-gap);\n width: 100%;\n max-width: 100%;\n perspective: 1400px;\n perspective-origin: 50% 50%;\n}\n\n.accordion-gallery--vertical {\n flex-direction: column;\n}\n\n.ag-panel {\n position: relative;\n flex: 1 1 0;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n border-radius: var(--ag-radius);\n cursor: pointer;\n display: block;\n text-decoration: none;\n outline: none;\n transform-style: preserve-3d;\n transform-origin: center center;\n background: #0a0713;\n box-shadow: 0 10px 30px -18px rgba(0, 0, 0, 0.8);\n will-change: flex-grow, transform;\n -webkit-tap-highlight-color: transparent;\n}\n\n.ag-panel:focus-visible {\n box-shadow:\n 0 0 0 2px var(--ag-accent),\n 0 10px 30px -18px rgba(0, 0, 0, 0.8);\n}\n\n.ag-panel__frame {\n position: absolute;\n inset: 0;\n overflow: hidden;\n border-radius: inherit;\n}\n\n.ag-panel__media {\n --ag-gray: 1;\n --ag-dim: 0.35;\n position: absolute;\n top: 50%;\n left: 50%;\n width: var(--ag-media-size);\n height: 100%;\n filter: grayscale(var(--ag-gray));\n will-change: transform, filter;\n}\n\n.accordion-gallery--vertical .ag-panel__media {\n width: 100%;\n height: var(--ag-media-size);\n}\n\n.ag-panel__media img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n user-select: none;\n -webkit-user-drag: none;\n}\n\n.ag-panel__overlay {\n position: absolute;\n inset: 0;\n pointer-events: none;\n background:\n linear-gradient(180deg, transparent 45%, color-mix(in srgb, var(--ag-overlay) 78%, transparent) 100%),\n color-mix(in srgb, var(--ag-overlay) calc(var(--ag-dim, 0.35) * 100%), transparent);\n}\n\n.ag-panel__label {\n position: absolute;\n left: 20px;\n bottom: 20px;\n right: 20px;\n display: flex;\n align-items: center;\n gap: 12px;\n pointer-events: none;\n z-index: 2;\n}\n\n.ag-panel__bar {\n flex: 0 0 auto;\n width: 3px;\n height: 26px;\n border-radius: 3px;\n background: var(--ag-accent);\n opacity: 0;\n box-shadow: 0 0 12px color-mix(in srgb, var(--ag-accent) 60%, transparent);\n}\n\n.ag-panel__text {\n color: var(--ag-text);\n font-family: inherit;\n font-weight: 600;\n font-size: clamp(1rem, 1.4vw, 1.4rem);\n letter-spacing: 0.01em;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n opacity: 0;\n text-shadow: 0 2px 14px rgba(0, 0, 0, 0.55);\n}\n\n@media (max-width: 520px) {\n .accordion-gallery {\n flex-direction: column;\n perspective: none;\n height: auto !important;\n }\n .ag-panel {\n min-height: 84px;\n transform: none !important;\n }\n .accordion-gallery .ag-panel__media {\n width: 100%;\n height: var(--ag-media-size);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .ag-panel,\n .ag-panel__media {\n will-change: auto;\n }\n}\n" }, { "type": "registry:component", - "path": "AccordionGallery/AccordionGallery.tsx", + "path": "AccordionGallery.tsx", "content": "import { useRef, useEffect, useState, useCallback, CSSProperties, KeyboardEvent, MouseEvent } from 'react';\nimport { gsap } from 'gsap';\n\nimport './AccordionGallery.css';\n\nexport interface AccordionGalleryItem {\n image: string;\n label?: string;\n link?: string;\n alt?: string;\n}\n\nexport interface AccordionGalleryProps {\n items?: AccordionGalleryItem[];\n defaultIndex?: number;\n accentColor?: string;\n overlayColor?: string;\n textColor?: string;\n height?: number;\n gap?: number;\n radius?: number;\n expandRatio?: number;\n orientation?: 'horizontal' | 'vertical';\n duration?: number;\n ease?: string;\n parallax?: number;\n tilt?: number;\n stagger?: number;\n trigger?: 'hover' | 'click';\n showLabels?: boolean;\n grayscale?: boolean;\n className?: string;\n}\n\nconst DEFAULT_ITEMS: AccordionGalleryItem[] = [\n { image: 'https://picsum.photos/id/1015/900/1200', label: 'Canyon', link: '#' },\n { image: 'https://picsum.photos/id/1018/900/1200', label: 'Ridgeline', link: '#' },\n { image: 'https://picsum.photos/id/1039/900/1200', label: 'Falls', link: '#' },\n { image: 'https://picsum.photos/id/1043/900/1200', label: 'Harbour', link: '#' },\n { image: 'https://picsum.photos/id/1044/900/1200', label: 'Skyline', link: '#' }\n];\n\nconst AccordionGallery = ({\n items = DEFAULT_ITEMS,\n defaultIndex = 2,\n accentColor = '#ffffff',\n overlayColor = '#060010',\n textColor = '#ffffff',\n height = 460,\n gap = 10,\n radius = 16,\n expandRatio = 0.52,\n orientation = 'horizontal',\n duration = 0.6,\n ease = 'power3.out',\n parallax = 0.5,\n tilt = 8,\n stagger = 0.06,\n trigger = 'hover',\n showLabels = true,\n grayscale = true,\n className = ''\n}: AccordionGalleryProps) => {\n const rootRef = useRef(null);\n const panelRefs = useRef<(HTMLElement | null)[]>([]);\n const mediaRefs = useRef<(HTMLElement | null)[]>([]);\n const barRefs = useRef<(HTMLElement | null)[]>([]);\n const textRefs = useRef<(HTMLElement | null)[]>([]);\n const tlRef = useRef(null);\n const firstRunRef = useRef(true);\n const mediaSizeRef = useRef(320);\n\n const vertical = orientation === 'vertical';\n const count = items.length;\n const [active, setActive] = useState(Math.min(Math.max(defaultIndex, 0), count - 1));\n\n const prefersReduced =\n typeof window !== 'undefined' && window.matchMedia\n ? window.matchMedia('(prefers-reduced-motion: reduce)').matches\n : false;\n\n const applyLayout = useCallback(\n (animate: boolean) => {\n const panels = panelRefs.current;\n if (!panels.length) return;\n\n const r = Math.min(Math.max(expandRatio, 0.2), 0.9);\n const grow = count > 1 ? (r * (count - 1)) / (1 - r) : 1;\n const mediaSize = mediaSizeRef.current;\n\n tlRef.current?.kill();\n const dur = animate && !prefersReduced ? duration : 0;\n const tl = gsap.timeline();\n\n panels.forEach((panel, i) => {\n if (!panel) return;\n const isActive = i === active;\n const media = mediaRefs.current[i];\n const bar = barRefs.current[i];\n const text = textRefs.current[i];\n\n const rot = isActive ? 0 : i < active ? tilt : -tilt;\n const rotProp = vertical ? { rotateX: -rot } : { rotateY: rot };\n\n tl.to(panel, { flexGrow: isActive ? grow : 1, ...rotProp, duration: dur, ease }, 0);\n\n if (media) {\n const drift = Math.max(-1.5, Math.min(1.5, active - i));\n const shift = drift * parallax * mediaSize * 0.06;\n const gray = grayscale ? (isActive ? 0 : 1) : 0;\n tl.to(\n media,\n {\n xPercent: -50,\n yPercent: -50,\n x: vertical ? 0 : isActive ? 0 : shift,\n y: vertical ? (isActive ? 0 : shift) : 0,\n '--ag-gray': gray,\n '--ag-dim': isActive ? 0 : 0.35,\n duration: dur,\n ease\n },\n 0\n );\n }\n\n if (showLabels && bar && text) {\n if (isActive) {\n tl.to([bar, text], { opacity: 1, x: 0, duration: dur, ease, stagger: prefersReduced ? 0 : stagger }, 0);\n } else {\n tl.to([bar, text], { opacity: 0, x: -14, duration: dur * 0.6, ease }, 0);\n }\n }\n });\n\n tlRef.current = tl;\n },\n [\n active,\n count,\n expandRatio,\n duration,\n ease,\n vertical,\n tilt,\n parallax,\n grayscale,\n showLabels,\n stagger,\n prefersReduced\n ]\n );\n\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n\n const measure = () => {\n const rect = el.getBoundingClientRect();\n const total = vertical ? rect.height : rect.width;\n const usable = Math.max(total - gap * (count - 1), 120);\n const size = Math.max(140, usable * Math.min(Math.max(expandRatio, 0.2), 0.9) * 1.22);\n mediaSizeRef.current = size;\n el.style.setProperty('--ag-media-size', `${size}px`);\n applyLayout(!firstRunRef.current);\n };\n\n measure();\n const ro = new ResizeObserver(measure);\n ro.observe(el);\n return () => ro.disconnect();\n }, [applyLayout, gap, count, expandRatio, vertical]);\n\n useEffect(() => {\n applyLayout(!firstRunRef.current);\n firstRunRef.current = false;\n }, [applyLayout]);\n\n useEffect(\n () => () => {\n tlRef.current?.kill();\n },\n []\n );\n\n const handleEnter = (i: number) => {\n if (trigger === 'hover') setActive(i);\n };\n\n const handleClick = (i: number, e: MouseEvent) => {\n if (i !== active) {\n e.preventDefault();\n setActive(i);\n }\n };\n\n const handleKeyDown = (i: number, e: KeyboardEvent) => {\n if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {\n e.preventDefault();\n setActive((i + 1) % count);\n } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {\n e.preventDefault();\n setActive((i - 1 + count) % count);\n }\n };\n\n const rootStyle = {\n '--ag-accent': accentColor,\n '--ag-overlay': overlayColor,\n '--ag-text': textColor,\n '--ag-gap': `${gap}px`,\n '--ag-radius': `${radius}px`,\n height: vertical ? `${Math.round(height * 1.6)}px` : `${height}px`\n } as CSSProperties;\n\n return (\n \n {items.map((item, i) => {\n const isActive = i === active;\n const Tag = (item.link ? 'a' : 'div') as 'a';\n return (\n {\n panelRefs.current[i] = el;\n }}\n className={`ag-panel${isActive ? ' ag-panel--active' : ''}`}\n style={{ borderRadius: `${radius}px` }}\n href={item.link || undefined}\n onClick={e => handleClick(i, e)}\n onMouseEnter={() => handleEnter(i)}\n onFocus={() => setActive(i)}\n onKeyDown={e => handleKeyDown(i, e)}\n role=\"listitem\"\n tabIndex={0}\n aria-current={isActive ? 'true' : undefined}\n aria-label={item.label}\n >\n \n {\n mediaRefs.current[i] = el;\n }}\n >\n {item.alt\n \n \n \n {showLabels && (\n \n {\n barRefs.current[i] = el;\n }}\n />\n {\n textRefs.current[i] = el;\n }}\n >\n {item.label}\n \n \n )}\n \n );\n })}\n \n );\n};\n\nexport default AccordionGallery;\n" } ], diff --git a/public/r/AcidSquares-JS-CSS.json b/public/r/AcidSquares-JS-CSS.json index 0f16a930..9ec3a8d4 100644 --- a/public/r/AcidSquares-JS-CSS.json +++ b/public/r/AcidSquares-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "AcidSquares/AcidSquares.css", + "type": "registry:file", + "path": "AcidSquares.css", + "target": "@components/AcidSquares.css", "content": ".acid-squares-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "AcidSquares/AcidSquares.jsx", + "path": "AcidSquares.jsx", "content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle, RenderTarget } from 'ogl';\nimport './AcidSquares.css';\n\nconst hexToRgb = hex => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 1, 1];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst DETAIL_STEPS = { low: 20, medium: 32, high: 48 };\nconst stepsFor = detail => DETAIL_STEPS[detail] || DETAIL_STEPS.medium;\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uSpeed;\nuniform float uWaveDepth;\nuniform float uZoom;\nuniform float uDensity;\nuniform float uSpread;\nuniform float uStepSize;\nuniform float uGlow;\nuniform float uExposure;\nuniform float uColorShift;\nuniform float uContrast;\nuniform float uBrightness;\nuniform float uOpacity;\nuniform float uSteps;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nuniform vec2 uMouse;\nuniform float uMouseStrength;\nuniform float uMouseRadius;\nuniform float uEnableMouse;\nuniform float uMouseActive;\nuniform float uGrain;\nuniform float uGrainIntensity;\nout vec4 fragColor;\n\nvoid main() {\n vec2 frag = gl_FragCoord.xy;\n float zoom = max(uZoom, 0.05);\n float aspect = iResolution.x / iResolution.y;\n vec2 ndc = (2.0 * frag - iResolution.xy) / iResolution.y;\n vec2 dir = ndc * (0.5 / zoom);\n\n vec2 mouseNdc = vec2(uMouse.x * aspect, uMouse.y);\n float mr = max(uMouseRadius, 0.01);\n vec2 md = ndc - mouseNdc;\n float dent = exp(-dot(md, md) / (mr * mr)) * (3.0 * uMouseStrength * uEnableMouse * uMouseActive);\n\n float travel = sin(iTime * uSpeed) * uWaveDepth;\n float density = max(uDensity, 1.0);\n float spread = clamp(uSpread, 0.05, 0.6);\n float stepSize = max(uStepSize, 0.0005);\n float glowGain = max(uGlow, 0.0);\n\n vec3 tOffset = vec3(0.0, dent, travel);\n vec3 p = vec3(0.0);\n float s = 0.0;\n float glow = 0.0;\n\n for (int i = 0; i < 64; i++) {\n if (float(i) >= uSteps) break;\n p += vec3(dir * s, s);\n vec3 q = p + tOffset;\n s += density - length(q.xz) + length(ceil(q).xy);\n s = stepSize + abs(s) * spread;\n glow += glowGain / s;\n }\n\n float e = glow / max(uExposure, 1.0);\n float shimmer = 0.5 + 0.5 * dot(cos(iTime * uColorShift + p), vec3(0.3333));\n float v = tanh(e * uBrightness * mix(0.7, 1.05, shimmer));\n v = clamp((v - 0.5) * uContrast + 0.5, 0.0, 1.0);\n\n vec3 col = mix(uColor1, uColor2, smoothstep(0.0, 0.55, v));\n col = mix(col, uColor3, smoothstep(0.55, 1.0, v));\n col *= v;\n\n float a = clamp(v, 0.0, 1.0) * uOpacity;\n vec3 outRgb = col * a;\n if (uGrain > 0.5) {\n float gv = (fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233)) + iTime) * 43758.5453) - 0.5) * uGrainIntensity;\n outRgb = clamp(outRgb + gv, 0.0, 1.0);\n a = clamp(a + gv, 0.0, 1.0);\n }\n fragColor = vec4(outRgb, a);\n}\n`;\n\nconst postFragment = `#version 300 es\nprecision highp float;\nuniform sampler2D tMap;\nuniform vec2 iResolution;\nuniform vec2 uDirection;\nuniform float uRadius;\nuniform float uGrain;\nuniform float uGrainIntensity;\nuniform float iTime;\nout vec4 fragColor;\n\nvec4 samp(vec2 uv) {\n return texture(tMap, uv);\n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / iResolution;\n vec2 texel = uDirection / iResolution;\n float st = uRadius * 0.25;\n vec4 sum = samp(uv) * 0.2026;\n sum += (samp(uv + texel * st) + samp(uv - texel * st)) * 0.179;\n sum += (samp(uv + texel * (st * 2.0)) + samp(uv - texel * (st * 2.0))) * 0.124;\n sum += (samp(uv + texel * (st * 3.0)) + samp(uv - texel * (st * 3.0))) * 0.0672;\n sum += (samp(uv + texel * (st * 4.0)) + samp(uv - texel * (st * 4.0))) * 0.0285;\n vec4 col = sum;\n if (uGrain > 0.5) {\n float gv = (fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233)) + iTime) * 43758.5453) - 0.5) * uGrainIntensity;\n col.rgb = clamp(col.rgb + gv, 0.0, 1.0);\n col.a = clamp(col.a + gv, 0.0, 1.0);\n }\n fragColor = col;\n}\n`;\n\nconst ctxMap = new WeakMap();\n\nconst AcidSquares = ({\n color1 = '#5227FF',\n color2 = '#A855F7',\n color3 = '#FFFFFF',\n detail = 'medium',\n speed = 0.7,\n waveDepth = 1,\n zoom = 1.3,\n density = 10.0,\n glow = 1.0,\n exposure = 2700,\n spread = 0.3,\n stepSize = 0.002,\n colorShift = 0,\n contrast = 1,\n brightness = 1.0,\n opacity = 1.0,\n mouseInteraction = true,\n mouseStrength = 0.1,\n mouseRadius = 0.35,\n blur = 0,\n grain = true,\n grainIntensity = 0.05,\n className = ''\n}) => {\n const containerRef = useRef(null);\n const mouseTarget = useRef([0, 0]);\n const mouseCurrent = useRef([0, 0]);\n const enableMouseRef = useRef(mouseInteraction);\n const mouseStrengthRef = useRef(mouseStrength);\n const mouseActive = useRef(0);\n const mouseActiveTarget = useRef(0);\n const blurRef = useRef(blur);\n const grainRef = useRef(grain);\n const grainIntensityRef = useRef(grainIntensity);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n webgl: 2,\n alpha: true,\n premultipliedAlpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n const canvas = gl.canvas;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uSpeed: { value: 0.7 },\n uWaveDepth: { value: 1 },\n uZoom: { value: 1.3 },\n uDensity: { value: 10.0 },\n uSpread: { value: 0.3 },\n uStepSize: { value: 0.002 },\n uGlow: { value: 1.0 },\n uExposure: { value: 2700 },\n uColorShift: { value: 0 },\n uContrast: { value: 1 },\n uBrightness: { value: 1.0 },\n uOpacity: { value: 1.0 },\n uSteps: { value: 32 },\n uColor1: { value: new Float32Array([1, 1, 1]) },\n uColor2: { value: new Float32Array([1, 1, 1]) },\n uColor3: { value: new Float32Array([1, 1, 1]) },\n uMouse: { value: new Float32Array([0, 0]) },\n uMouseStrength: { value: 0.1 },\n uMouseRadius: { value: 0.35 },\n uEnableMouse: { value: 1.0 },\n uMouseActive: { value: 0.0 },\n uGrain: { value: 1.0 },\n uGrainIntensity: { value: 0.05 }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n\n const postProgram = new Program(gl, {\n vertex,\n fragment: postFragment,\n uniforms: {\n tMap: { value: null },\n iResolution: { value: new Float32Array([1, 1]) },\n uDirection: { value: new Float32Array([1, 0]) },\n uRadius: { value: 0 },\n uGrain: { value: 0 },\n uGrainIntensity: { value: 0.05 },\n iTime: { value: 0 }\n }\n });\n const postMesh = new Mesh(gl, { geometry, program: postProgram });\n\n let rtA = null;\n let rtB = null;\n const ensureTargets = () => {\n if (!rtA) {\n const bw = gl.drawingBufferWidth;\n const bh = gl.drawingBufferHeight;\n rtA = new RenderTarget(gl, { width: bw, height: bh, depth: false });\n rtB = new RenderTarget(gl, { width: bw, height: bh, depth: false });\n }\n };\n\n const renderFrame = () => {\n const grainOn = grainRef.current ? 1.0 : 0.0;\n const grainAmt = grainIntensityRef.current;\n program.uniforms.uGrainIntensity.value = grainAmt;\n postProgram.uniforms.uGrainIntensity.value = grainAmt;\n if (blurRef.current > 0) {\n ensureTargets();\n program.uniforms.uGrain.value = 0.0;\n renderer.render({ scene: mesh, target: rtA });\n const pu = postProgram.uniforms;\n pu.uRadius.value = blurRef.current * 14.0;\n pu.tMap.value = rtA.texture;\n pu.uDirection.value[0] = 1;\n pu.uDirection.value[1] = 0;\n pu.uGrain.value = 0.0;\n renderer.render({ scene: postMesh, target: rtB });\n pu.tMap.value = rtB.texture;\n pu.uDirection.value[0] = 0;\n pu.uDirection.value[1] = 1;\n pu.uGrain.value = grainOn;\n renderer.render({ scene: postMesh });\n } else {\n program.uniforms.uGrain.value = grainOn;\n renderer.render({ scene: mesh });\n }\n };\n\n ctxMap.set(container, { renderer, program, mesh });\n\n const setSize = () => {\n const rect = container.getBoundingClientRect();\n const w = Math.max(1, Math.floor(rect.width));\n const h = Math.max(1, Math.floor(rect.height));\n renderer.setSize(w, h);\n const bw = gl.drawingBufferWidth;\n const bh = gl.drawingBufferHeight;\n const res = program.uniforms.iResolution.value;\n res[0] = bw;\n res[1] = bh;\n const pres = postProgram.uniforms.iResolution.value;\n pres[0] = bw;\n pres[1] = bh;\n if (rtA) {\n rtA.setSize(bw, bh);\n rtB.setSize(bw, bh);\n }\n renderFrame();\n };\n\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n setSize();\n\n const handleMouseMove = e => {\n const rect = container.getBoundingClientRect();\n const x = ((e.clientX - rect.left) / rect.width - 0.5) * 2.0;\n const y = -((e.clientY - rect.top) / rect.height - 0.5) * 2.0;\n mouseTarget.current = [x, y];\n mouseActiveTarget.current = 1;\n };\n const handleMouseLeave = () => {\n mouseActiveTarget.current = 0;\n };\n container.addEventListener('mousemove', handleMouseMove);\n container.addEventListener('mouseleave', handleMouseLeave);\n\n let raf = 0;\n let isVisible = true;\n let isPageVisible = !document.hidden;\n const t0 = performance.now();\n\n const loop = t => {\n program.uniforms.iTime.value = (t - t0) * 0.001;\n\n const cur = mouseCurrent.current;\n const tgt = mouseTarget.current;\n cur[0] += 0.05 * (tgt[0] - cur[0]);\n cur[1] += 0.05 * (tgt[1] - cur[1]);\n const m = program.uniforms.uMouse.value;\n m[0] = cur[0];\n m[1] = cur[1];\n const activeTarget = enableMouseRef.current ? mouseActiveTarget.current : 0;\n mouseActive.current += 0.05 * (activeTarget - mouseActive.current);\n program.uniforms.uMouseActive.value = mouseActive.current;\n program.uniforms.uEnableMouse.value = enableMouseRef.current ? 1.0 : 0.0;\n program.uniforms.uMouseStrength.value = mouseStrengthRef.current;\n\n postProgram.uniforms.iTime.value = program.uniforms.iTime.value;\n renderFrame();\n raf = requestAnimationFrame(loop);\n };\n\n const tryStart = () => {\n if (isVisible && isPageVisible && raf === 0) raf = requestAnimationFrame(loop);\n };\n const tryStop = () => {\n if (raf !== 0) {\n cancelAnimationFrame(raf);\n raf = 0;\n }\n };\n\n const io = new IntersectionObserver(\n ([entry]) => {\n isVisible = entry.isIntersecting;\n isVisible ? tryStart() : tryStop();\n },\n { threshold: 0 }\n );\n io.observe(container);\n\n const onVisibility = () => {\n isPageVisible = !document.hidden;\n isPageVisible ? tryStart() : tryStop();\n };\n document.addEventListener('visibilitychange', onVisibility);\n\n tryStart();\n\n return () => {\n tryStop();\n ro.disconnect();\n io.disconnect();\n document.removeEventListener('visibilitychange', onVisibility);\n container.removeEventListener('mousemove', handleMouseMove);\n container.removeEventListener('mouseleave', handleMouseLeave);\n ctxMap.delete(container);\n if (rtA) {\n gl.deleteFramebuffer(rtA.buffer);\n gl.deleteFramebuffer(rtB.buffer);\n rtA.textures.forEach(tex => gl.deleteTexture(tex.texture));\n rtB.textures.forEach(tex => gl.deleteTexture(tex.texture));\n }\n try {\n container.removeChild(canvas);\n } catch {}\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, []);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n const ctx = ctxMap.get(container);\n if (!ctx) return;\n const { program } = ctx;\n const u = program.uniforms;\n\n u.uSpeed.value = speed;\n u.uWaveDepth.value = waveDepth;\n u.uZoom.value = zoom;\n u.uDensity.value = density;\n u.uSpread.value = spread;\n u.uStepSize.value = stepSize;\n u.uGlow.value = glow;\n u.uExposure.value = exposure;\n u.uColorShift.value = colorShift;\n u.uContrast.value = contrast;\n u.uBrightness.value = brightness;\n u.uOpacity.value = opacity;\n u.uSteps.value = stepsFor(detail);\n u.uMouseRadius.value = mouseRadius;\n const c1 = hexToRgb(color1);\n const a1 = u.uColor1.value;\n a1[0] = c1[0];\n a1[1] = c1[1];\n a1[2] = c1[2];\n const c2 = hexToRgb(color2);\n const a2 = u.uColor2.value;\n a2[0] = c2[0];\n a2[1] = c2[1];\n a2[2] = c2[2];\n const c3 = hexToRgb(color3);\n const a3 = u.uColor3.value;\n a3[0] = c3[0];\n a3[1] = c3[1];\n a3[2] = c3[2];\n\n enableMouseRef.current = mouseInteraction;\n mouseStrengthRef.current = mouseStrength;\n blurRef.current = blur;\n grainRef.current = grain;\n grainIntensityRef.current = grainIntensity;\n }, [\n color1,\n color2,\n color3,\n detail,\n speed,\n waveDepth,\n zoom,\n density,\n glow,\n exposure,\n spread,\n stepSize,\n colorShift,\n contrast,\n brightness,\n opacity,\n mouseInteraction,\n mouseStrength,\n mouseRadius,\n blur,\n grain,\n grainIntensity\n ]);\n\n return
;\n};\n\nexport default AcidSquares;\n" } ], diff --git a/public/r/AcidSquares-TS-CSS.json b/public/r/AcidSquares-TS-CSS.json index 0142f076..aab479f1 100644 --- a/public/r/AcidSquares-TS-CSS.json +++ b/public/r/AcidSquares-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "AcidSquares/AcidSquares.css", + "type": "registry:file", + "path": "AcidSquares.css", + "target": "@components/AcidSquares.css", "content": ".acid-squares-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "AcidSquares/AcidSquares.tsx", + "path": "AcidSquares.tsx", "content": "import React, { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle, RenderTarget } from 'ogl';\nimport './AcidSquares.css';\n\nexport type AcidSquaresDetail = 'low' | 'medium' | 'high';\n\nexport interface AcidSquaresProps {\n color1?: string;\n color2?: string;\n color3?: string;\n detail?: AcidSquaresDetail;\n speed?: number;\n waveDepth?: number;\n zoom?: number;\n density?: number;\n glow?: number;\n exposure?: number;\n spread?: number;\n stepSize?: number;\n colorShift?: number;\n contrast?: number;\n brightness?: number;\n opacity?: number;\n mouseInteraction?: boolean;\n mouseStrength?: number;\n mouseRadius?: number;\n blur?: number;\n grain?: boolean;\n grainIntensity?: number;\n className?: string;\n}\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 1, 1];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst DETAIL_STEPS: Record = { low: 20, medium: 32, high: 48 };\nconst stepsFor = (detail: AcidSquaresDetail): number => DETAIL_STEPS[detail] || DETAIL_STEPS.medium;\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uSpeed;\nuniform float uWaveDepth;\nuniform float uZoom;\nuniform float uDensity;\nuniform float uSpread;\nuniform float uStepSize;\nuniform float uGlow;\nuniform float uExposure;\nuniform float uColorShift;\nuniform float uContrast;\nuniform float uBrightness;\nuniform float uOpacity;\nuniform float uSteps;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nuniform vec2 uMouse;\nuniform float uMouseStrength;\nuniform float uMouseRadius;\nuniform float uEnableMouse;\nuniform float uMouseActive;\nuniform float uGrain;\nuniform float uGrainIntensity;\nout vec4 fragColor;\n\nvoid main() {\n vec2 frag = gl_FragCoord.xy;\n float zoom = max(uZoom, 0.05);\n float aspect = iResolution.x / iResolution.y;\n vec2 ndc = (2.0 * frag - iResolution.xy) / iResolution.y;\n vec2 dir = ndc * (0.5 / zoom);\n\n vec2 mouseNdc = vec2(uMouse.x * aspect, uMouse.y);\n float mr = max(uMouseRadius, 0.01);\n vec2 md = ndc - mouseNdc;\n float dent = exp(-dot(md, md) / (mr * mr)) * (3.0 * uMouseStrength * uEnableMouse * uMouseActive);\n\n float travel = sin(iTime * uSpeed) * uWaveDepth;\n float density = max(uDensity, 1.0);\n float spread = clamp(uSpread, 0.05, 0.6);\n float stepSize = max(uStepSize, 0.0005);\n float glowGain = max(uGlow, 0.0);\n\n vec3 tOffset = vec3(0.0, dent, travel);\n vec3 p = vec3(0.0);\n float s = 0.0;\n float glow = 0.0;\n\n for (int i = 0; i < 64; i++) {\n if (float(i) >= uSteps) break;\n p += vec3(dir * s, s);\n vec3 q = p + tOffset;\n s += density - length(q.xz) + length(ceil(q).xy);\n s = stepSize + abs(s) * spread;\n glow += glowGain / s;\n }\n\n float e = glow / max(uExposure, 1.0);\n float shimmer = 0.5 + 0.5 * dot(cos(iTime * uColorShift + p), vec3(0.3333));\n float v = tanh(e * uBrightness * mix(0.7, 1.05, shimmer));\n v = clamp((v - 0.5) * uContrast + 0.5, 0.0, 1.0);\n\n vec3 col = mix(uColor1, uColor2, smoothstep(0.0, 0.55, v));\n col = mix(col, uColor3, smoothstep(0.55, 1.0, v));\n col *= v;\n\n float a = clamp(v, 0.0, 1.0) * uOpacity;\n vec3 outRgb = col * a;\n if (uGrain > 0.5) {\n float gv = (fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233)) + iTime) * 43758.5453) - 0.5) * uGrainIntensity;\n outRgb = clamp(outRgb + gv, 0.0, 1.0);\n a = clamp(a + gv, 0.0, 1.0);\n }\n fragColor = vec4(outRgb, a);\n}\n`;\n\nconst postFragment = `#version 300 es\nprecision highp float;\nuniform sampler2D tMap;\nuniform vec2 iResolution;\nuniform vec2 uDirection;\nuniform float uRadius;\nuniform float uGrain;\nuniform float uGrainIntensity;\nuniform float iTime;\nout vec4 fragColor;\n\nvec4 samp(vec2 uv) {\n return texture(tMap, uv);\n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / iResolution;\n vec2 texel = uDirection / iResolution;\n float st = uRadius * 0.25;\n vec4 sum = samp(uv) * 0.2026;\n sum += (samp(uv + texel * st) + samp(uv - texel * st)) * 0.179;\n sum += (samp(uv + texel * (st * 2.0)) + samp(uv - texel * (st * 2.0))) * 0.124;\n sum += (samp(uv + texel * (st * 3.0)) + samp(uv - texel * (st * 3.0))) * 0.0672;\n sum += (samp(uv + texel * (st * 4.0)) + samp(uv - texel * (st * 4.0))) * 0.0285;\n vec4 col = sum;\n if (uGrain > 0.5) {\n float gv = (fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233)) + iTime) * 43758.5453) - 0.5) * uGrainIntensity;\n col.rgb = clamp(col.rgb + gv, 0.0, 1.0);\n col.a = clamp(col.a + gv, 0.0, 1.0);\n }\n fragColor = col;\n}\n`;\n\ntype AcidSquaresCtx = {\n renderer: InstanceType;\n program: InstanceType;\n mesh: InstanceType;\n};\nconst ctxMap = new WeakMap();\n\nconst AcidSquares: React.FC = ({\n color1 = '#5227FF',\n color2 = '#A855F7',\n color3 = '#FFFFFF',\n detail = 'medium',\n speed = 0.7,\n waveDepth = 1,\n zoom = 1.3,\n density = 10.0,\n glow = 1.0,\n exposure = 2700,\n spread = 0.3,\n stepSize = 0.002,\n colorShift = 0,\n contrast = 1,\n brightness = 1.0,\n opacity = 1.0,\n mouseInteraction = true,\n mouseStrength = 0.1,\n mouseRadius = 0.35,\n blur = 0,\n grain = true,\n grainIntensity = 0.05,\n className = ''\n}) => {\n const containerRef = useRef(null);\n const mouseTarget = useRef<[number, number]>([0, 0]);\n const mouseCurrent = useRef<[number, number]>([0, 0]);\n const enableMouseRef = useRef(mouseInteraction);\n const mouseStrengthRef = useRef(mouseStrength);\n const mouseActive = useRef(0);\n const mouseActiveTarget = useRef(0);\n const blurRef = useRef(blur);\n const grainRef = useRef(grain);\n const grainIntensityRef = useRef(grainIntensity);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n webgl: 2,\n alpha: true,\n premultipliedAlpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n const canvas = gl.canvas as HTMLCanvasElement;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uSpeed: { value: 0.7 },\n uWaveDepth: { value: 1 },\n uZoom: { value: 1.3 },\n uDensity: { value: 10.0 },\n uSpread: { value: 0.3 },\n uStepSize: { value: 0.002 },\n uGlow: { value: 1.0 },\n uExposure: { value: 2700 },\n uColorShift: { value: 0 },\n uContrast: { value: 1 },\n uBrightness: { value: 1.0 },\n uOpacity: { value: 1.0 },\n uSteps: { value: 32 },\n uColor1: { value: new Float32Array([1, 1, 1]) },\n uColor2: { value: new Float32Array([1, 1, 1]) },\n uColor3: { value: new Float32Array([1, 1, 1]) },\n uMouse: { value: new Float32Array([0, 0]) },\n uMouseStrength: { value: 0.1 },\n uMouseRadius: { value: 0.35 },\n uEnableMouse: { value: 1.0 },\n uMouseActive: { value: 0.0 },\n uGrain: { value: 1.0 },\n uGrainIntensity: { value: 0.05 }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n\n const postProgram = new Program(gl, {\n vertex,\n fragment: postFragment,\n uniforms: {\n tMap: { value: null },\n iResolution: { value: new Float32Array([1, 1]) },\n uDirection: { value: new Float32Array([1, 0]) },\n uRadius: { value: 0 },\n uGrain: { value: 0 },\n uGrainIntensity: { value: 0.05 },\n iTime: { value: 0 }\n }\n });\n const postMesh = new Mesh(gl, { geometry, program: postProgram });\n const pu = postProgram.uniforms as Record;\n const mu = program.uniforms as Record;\n\n let rtA: InstanceType | null = null;\n let rtB: InstanceType | null = null;\n const ensureTargets = () => {\n if (!rtA) {\n const bw = gl.drawingBufferWidth;\n const bh = gl.drawingBufferHeight;\n rtA = new RenderTarget(gl, { width: bw, height: bh, depth: false });\n rtB = new RenderTarget(gl, { width: bw, height: bh, depth: false });\n }\n };\n\n const renderFrame = () => {\n const grainOn = grainRef.current ? 1.0 : 0.0;\n const grainAmt = grainIntensityRef.current;\n program.uniforms.uGrainIntensity.value = grainAmt;\n postProgram.uniforms.uGrainIntensity.value = grainAmt;\n if (blurRef.current > 0) {\n ensureTargets();\n mu.uGrain.value = 0.0;\n renderer.render({ scene: mesh, target: rtA });\n pu.uRadius.value = blurRef.current * 14.0;\n pu.tMap.value = rtA!.texture;\n pu.uDirection.value[0] = 1;\n pu.uDirection.value[1] = 0;\n pu.uGrain.value = 0.0;\n renderer.render({ scene: postMesh, target: rtB });\n pu.tMap.value = rtB!.texture;\n pu.uDirection.value[0] = 0;\n pu.uDirection.value[1] = 1;\n pu.uGrain.value = grainOn;\n renderer.render({ scene: postMesh });\n } else {\n mu.uGrain.value = grainOn;\n renderer.render({ scene: mesh });\n }\n };\n\n ctxMap.set(container, { renderer, program, mesh });\n\n const setSize = () => {\n const rect = container.getBoundingClientRect();\n const w = Math.max(1, Math.floor(rect.width));\n const h = Math.max(1, Math.floor(rect.height));\n renderer.setSize(w, h);\n const bw = gl.drawingBufferWidth;\n const bh = gl.drawingBufferHeight;\n const res = (program.uniforms.iResolution as { value: Float32Array }).value;\n res[0] = bw;\n res[1] = bh;\n const pres = pu.iResolution.value as Float32Array;\n pres[0] = bw;\n pres[1] = bh;\n if (rtA) {\n rtA.setSize(bw, bh);\n rtB!.setSize(bw, bh);\n }\n renderFrame();\n };\n\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n setSize();\n\n const handleMouseMove = (e: MouseEvent) => {\n const rect = container.getBoundingClientRect();\n const x = ((e.clientX - rect.left) / rect.width - 0.5) * 2.0;\n const y = -((e.clientY - rect.top) / rect.height - 0.5) * 2.0;\n mouseTarget.current = [x, y];\n mouseActiveTarget.current = 1;\n };\n const handleMouseLeave = () => {\n mouseActiveTarget.current = 0;\n };\n container.addEventListener('mousemove', handleMouseMove);\n container.addEventListener('mouseleave', handleMouseLeave);\n\n let raf = 0;\n let isVisible = true;\n let isPageVisible = !document.hidden;\n const t0 = performance.now();\n\n const loop = (t: number) => {\n (program.uniforms.iTime as { value: number }).value = (t - t0) * 0.001;\n\n const cur = mouseCurrent.current;\n const tgt = mouseTarget.current;\n cur[0] += 0.05 * (tgt[0] - cur[0]);\n cur[1] += 0.05 * (tgt[1] - cur[1]);\n const m = (program.uniforms.uMouse as { value: Float32Array }).value;\n m[0] = cur[0];\n m[1] = cur[1];\n const activeTarget = enableMouseRef.current ? mouseActiveTarget.current : 0;\n mouseActive.current += 0.05 * (activeTarget - mouseActive.current);\n (program.uniforms.uMouseActive as { value: number }).value = mouseActive.current;\n (program.uniforms.uEnableMouse as { value: number }).value = enableMouseRef.current ? 1.0 : 0.0;\n (program.uniforms.uMouseStrength as { value: number }).value = mouseStrengthRef.current;\n\n pu.iTime.value = (program.uniforms.iTime as { value: number }).value;\n renderFrame();\n raf = requestAnimationFrame(loop);\n };\n\n const tryStart = () => {\n if (isVisible && isPageVisible && raf === 0) raf = requestAnimationFrame(loop);\n };\n const tryStop = () => {\n if (raf !== 0) {\n cancelAnimationFrame(raf);\n raf = 0;\n }\n };\n\n const io = new IntersectionObserver(\n ([entry]) => {\n isVisible = entry.isIntersecting;\n isVisible ? tryStart() : tryStop();\n },\n { threshold: 0 }\n );\n io.observe(container);\n\n const onVisibility = () => {\n isPageVisible = !document.hidden;\n isPageVisible ? tryStart() : tryStop();\n };\n document.addEventListener('visibilitychange', onVisibility);\n\n tryStart();\n\n return () => {\n tryStop();\n ro.disconnect();\n io.disconnect();\n document.removeEventListener('visibilitychange', onVisibility);\n container.removeEventListener('mousemove', handleMouseMove);\n container.removeEventListener('mouseleave', handleMouseLeave);\n ctxMap.delete(container);\n if (rtA) {\n gl.deleteFramebuffer(rtA.buffer);\n gl.deleteFramebuffer(rtB!.buffer);\n rtA.textures.forEach(tex => gl.deleteTexture(tex.texture));\n rtB!.textures.forEach(tex => gl.deleteTexture(tex.texture));\n }\n try {\n container.removeChild(canvas);\n } catch {}\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, []);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n const ctx = ctxMap.get(container);\n if (!ctx) return;\n const { program } = ctx;\n const u = program.uniforms as Record;\n\n u.uSpeed.value = speed;\n u.uWaveDepth.value = waveDepth;\n u.uZoom.value = zoom;\n u.uDensity.value = density;\n u.uSpread.value = spread;\n u.uStepSize.value = stepSize;\n u.uGlow.value = glow;\n u.uExposure.value = exposure;\n u.uColorShift.value = colorShift;\n u.uContrast.value = contrast;\n u.uBrightness.value = brightness;\n u.uOpacity.value = opacity;\n u.uSteps.value = stepsFor(detail);\n u.uMouseRadius.value = mouseRadius;\n const c1 = hexToRgb(color1);\n const a1 = u.uColor1.value as Float32Array;\n a1[0] = c1[0];\n a1[1] = c1[1];\n a1[2] = c1[2];\n const c2 = hexToRgb(color2);\n const a2 = u.uColor2.value as Float32Array;\n a2[0] = c2[0];\n a2[1] = c2[1];\n a2[2] = c2[2];\n const c3 = hexToRgb(color3);\n const a3 = u.uColor3.value as Float32Array;\n a3[0] = c3[0];\n a3[1] = c3[1];\n a3[2] = c3[2];\n\n enableMouseRef.current = mouseInteraction;\n mouseStrengthRef.current = mouseStrength;\n blurRef.current = blur;\n grainRef.current = grain;\n grainIntensityRef.current = grainIntensity;\n }, [\n color1,\n color2,\n color3,\n detail,\n speed,\n waveDepth,\n zoom,\n density,\n glow,\n exposure,\n spread,\n stepSize,\n colorShift,\n contrast,\n brightness,\n opacity,\n mouseInteraction,\n mouseStrength,\n mouseRadius,\n blur,\n grain,\n grainIntensity\n ]);\n\n return
;\n};\n\nexport default AcidSquares;\n" } ], diff --git a/public/r/AnimatedList-JS-CSS.json b/public/r/AnimatedList-JS-CSS.json index f6155b66..de22f9bd 100644 --- a/public/r/AnimatedList-JS-CSS.json +++ b/public/r/AnimatedList-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "AnimatedList/AnimatedList.css", + "type": "registry:file", + "path": "AnimatedList.css", + "target": "@components/AnimatedList.css", "content": ".scroll-list-container {\n position: relative;\n width: 500px;\n}\n\n.scroll-list {\n max-height: 400px;\n overflow-y: auto;\n padding: 16px;\n}\n\n.scroll-list::-webkit-scrollbar {\n width: 8px;\n}\n\n.scroll-list::-webkit-scrollbar-track {\n background: #120F17;\n}\n\n.scroll-list::-webkit-scrollbar-thumb {\n background: #2F293A;\n border-radius: 4px;\n}\n\n.no-scrollbar::-webkit-scrollbar {\n display: none;\n}\n\n.no-scrollbar {\n -ms-overflow-style: none;\n scrollbar-width: none;\n}\n\n.item {\n padding: 16px;\n background-color: #2F293A;\n border-radius: 8px;\n margin-bottom: 1rem;\n}\n\n.item.selected {\n background-color: #2F293A;\n}\n\n.item-text {\n color: white;\n margin: 0;\n}\n\n.top-gradient {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 50px;\n background: linear-gradient(to bottom, #120F17, transparent);\n pointer-events: none;\n transition: opacity 0.3s ease;\n}\n\n.bottom-gradient {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 100px;\n background: linear-gradient(to top, #120F17, transparent);\n pointer-events: none;\n transition: opacity 0.3s ease;\n}\n" }, { "type": "registry:component", - "path": "AnimatedList/AnimatedList.jsx", + "path": "AnimatedList.jsx", "content": "import { useRef, useState, useEffect, useCallback } from 'react';\nimport { motion, useInView } from 'motion/react';\nimport './AnimatedList.css';\n\nconst AnimatedItem = ({ children, delay = 0, index, onMouseEnter, onClick }) => {\n const ref = useRef(null);\n const inView = useInView(ref, { amount: 0.5, triggerOnce: false });\n return (\n \n {children}\n \n );\n};\n\nconst AnimatedList = ({\n items = [\n 'Item 1',\n 'Item 2',\n 'Item 3',\n 'Item 4',\n 'Item 5',\n 'Item 6',\n 'Item 7',\n 'Item 8',\n 'Item 9',\n 'Item 10',\n 'Item 11',\n 'Item 12',\n 'Item 13',\n 'Item 14',\n 'Item 15'\n ],\n onItemSelect,\n showGradients = true,\n enableArrowNavigation = true,\n className = '',\n itemClassName = '',\n displayScrollbar = true,\n initialSelectedIndex = -1\n}) => {\n const listRef = useRef(null);\n const [selectedIndex, setSelectedIndex] = useState(initialSelectedIndex);\n const [keyboardNav, setKeyboardNav] = useState(false);\n const [topGradientOpacity, setTopGradientOpacity] = useState(0);\n const [bottomGradientOpacity, setBottomGradientOpacity] = useState(1);\n\n const handleItemMouseEnter = useCallback(index => {\n setSelectedIndex(index);\n }, []);\n\n const handleItemClick = useCallback(\n (item, index) => {\n setSelectedIndex(index);\n if (onItemSelect) {\n onItemSelect(item, index);\n }\n },\n [onItemSelect]\n );\n\n const handleScroll = useCallback(e => {\n const { scrollTop, scrollHeight, clientHeight } = e.target;\n setTopGradientOpacity(Math.min(scrollTop / 50, 1));\n const bottomDistance = scrollHeight - (scrollTop + clientHeight);\n setBottomGradientOpacity(scrollHeight <= clientHeight ? 0 : Math.min(bottomDistance / 50, 1));\n }, []);\n\n useEffect(() => {\n if (!enableArrowNavigation) return;\n const handleKeyDown = e => {\n if (e.key === 'ArrowDown' || (e.key === 'Tab' && !e.shiftKey)) {\n e.preventDefault();\n setKeyboardNav(true);\n setSelectedIndex(prev => Math.min(prev + 1, items.length - 1));\n } else if (e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey)) {\n e.preventDefault();\n setKeyboardNav(true);\n setSelectedIndex(prev => Math.max(prev - 1, 0));\n } else if (e.key === 'Enter') {\n if (selectedIndex >= 0 && selectedIndex < items.length) {\n e.preventDefault();\n if (onItemSelect) {\n onItemSelect(items[selectedIndex], selectedIndex);\n }\n }\n }\n };\n\n window.addEventListener('keydown', handleKeyDown);\n return () => window.removeEventListener('keydown', handleKeyDown);\n }, [items, selectedIndex, onItemSelect, enableArrowNavigation]);\n\n useEffect(() => {\n if (!keyboardNav || selectedIndex < 0 || !listRef.current) return;\n const container = listRef.current;\n const selectedItem = container.querySelector(`[data-index=\"${selectedIndex}\"]`);\n if (selectedItem) {\n const extraMargin = 50;\n const containerScrollTop = container.scrollTop;\n const containerHeight = container.clientHeight;\n const itemTop = selectedItem.offsetTop;\n const itemBottom = itemTop + selectedItem.offsetHeight;\n if (itemTop < containerScrollTop + extraMargin) {\n container.scrollTo({ top: itemTop - extraMargin, behavior: 'smooth' });\n } else if (itemBottom > containerScrollTop + containerHeight - extraMargin) {\n container.scrollTo({\n top: itemBottom - containerHeight + extraMargin,\n behavior: 'smooth'\n });\n }\n }\n setKeyboardNav(false);\n }, [selectedIndex, keyboardNav]);\n\n return (\n
\n
\n {items.map((item, index) => (\n handleItemMouseEnter(index)}\n onClick={() => handleItemClick(item, index)}\n >\n
\n

{item}

\n
\n \n ))}\n
\n {showGradients && (\n <>\n
\n
\n \n )}\n
\n );\n};\n\nexport default AnimatedList;\n" } ], diff --git a/public/r/AnimatedList-TS-CSS.json b/public/r/AnimatedList-TS-CSS.json index 62154084..e7de6290 100644 --- a/public/r/AnimatedList-TS-CSS.json +++ b/public/r/AnimatedList-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "AnimatedList/AnimatedList.css", + "type": "registry:file", + "path": "AnimatedList.css", + "target": "@components/AnimatedList.css", "content": ".scroll-list-container {\n position: relative;\n width: 500px;\n}\n\n.scroll-list {\n max-height: 400px;\n overflow-y: auto;\n padding: 16px;\n}\n\n.scroll-list::-webkit-scrollbar {\n width: 8px;\n}\n\n.scroll-list::-webkit-scrollbar-track {\n background: #060606;\n}\n\n.scroll-list::-webkit-scrollbar-thumb {\n background: #222;\n border-radius: 4px;\n}\n\n.no-scrollbar::-webkit-scrollbar {\n display: none;\n}\n\n.no-scrollbar {\n -ms-overflow-style: none;\n scrollbar-width: none;\n}\n\n.item {\n padding: 16px;\n background-color: #111;\n border-radius: 8px;\n margin-bottom: 1rem;\n}\n\n.item.selected {\n background-color: #222;\n}\n\n.item-text {\n color: white;\n margin: 0;\n}\n\n.top-gradient {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 50px;\n background: linear-gradient(to bottom, #120F17, transparent);\n pointer-events: none;\n transition: opacity 0.3s ease;\n}\n\n.bottom-gradient {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 100px;\n background: linear-gradient(to top, #120F17, transparent);\n pointer-events: none;\n transition: opacity 0.3s ease;\n}\n" }, { "type": "registry:component", - "path": "AnimatedList/AnimatedList.tsx", - "content": "import React, { useRef, useState, useEffect, useCallback, ReactNode, MouseEventHandler, UIEvent } from 'react';\nimport { motion, useInView } from 'motion/react';\nimport './AnimatedList.css';\n\ninterface AnimatedItemProps {\n children: ReactNode;\n delay?: number;\n index: number;\n onMouseEnter?: MouseEventHandler;\n onClick?: MouseEventHandler;\n}\n\nconst AnimatedItem: React.FC = ({ children, delay = 0, index, onMouseEnter, onClick }) => {\n const ref = useRef(null);\n const inView = useInView(ref, { amount: 0.5, once: false });\n return (\n \n {children}\n \n );\n};\n\ninterface AnimatedListProps {\n items?: string[];\n onItemSelect?: (item: string, index: number) => void;\n showGradients?: boolean;\n enableArrowNavigation?: boolean;\n className?: string;\n itemClassName?: string;\n displayScrollbar?: boolean;\n initialSelectedIndex?: number;\n}\n\nconst AnimatedList: React.FC = ({\n items = [\n 'Item 1',\n 'Item 2',\n 'Item 3',\n 'Item 4',\n 'Item 5',\n 'Item 6',\n 'Item 7',\n 'Item 8',\n 'Item 9',\n 'Item 10',\n 'Item 11',\n 'Item 12',\n 'Item 13',\n 'Item 14',\n 'Item 15'\n ],\n onItemSelect,\n showGradients = true,\n enableArrowNavigation = true,\n className = '',\n itemClassName = '',\n displayScrollbar = true,\n initialSelectedIndex = -1\n}) => {\n const listRef = useRef(null);\n const [selectedIndex, setSelectedIndex] = useState(initialSelectedIndex);\n const [keyboardNav, setKeyboardNav] = useState(false);\n const [topGradientOpacity, setTopGradientOpacity] = useState(0);\n const [bottomGradientOpacity, setBottomGradientOpacity] = useState(1);\n\n const handleItemMouseEnter = useCallback((index: number) => {\n setSelectedIndex(index);\n }, []);\n\n const handleItemClick = useCallback(\n (item: string, index: number) => {\n setSelectedIndex(index);\n if (onItemSelect) {\n onItemSelect(item, index);\n }\n },\n [onItemSelect]\n );\n\n const handleScroll = useCallback((e: UIEvent) => {\n const target = e.target as HTMLDivElement;\n const { scrollTop, scrollHeight, clientHeight } = target;\n setTopGradientOpacity(Math.min(scrollTop / 50, 1));\n const bottomDistance = scrollHeight - (scrollTop + clientHeight);\n setBottomGradientOpacity(scrollHeight <= clientHeight ? 0 : Math.min(bottomDistance / 50, 1));\n }, []);\n\n useEffect(() => {\n if (!enableArrowNavigation) return;\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'ArrowDown' || (e.key === 'Tab' && !e.shiftKey)) {\n e.preventDefault();\n setKeyboardNav(true);\n setSelectedIndex(prev => Math.min(prev + 1, items.length - 1));\n } else if (e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey)) {\n e.preventDefault();\n setKeyboardNav(true);\n setSelectedIndex(prev => Math.max(prev - 1, 0));\n } else if (e.key === 'Enter') {\n if (selectedIndex >= 0 && selectedIndex < items.length) {\n e.preventDefault();\n if (onItemSelect) {\n onItemSelect(items[selectedIndex], selectedIndex);\n }\n }\n }\n };\n\n window.addEventListener('keydown', handleKeyDown);\n return () => window.removeEventListener('keydown', handleKeyDown);\n }, [items, selectedIndex, onItemSelect, enableArrowNavigation]);\n\n useEffect(() => {\n if (!keyboardNav || selectedIndex < 0 || !listRef.current) return;\n const container = listRef.current;\n const selectedItem = container.querySelector(`[data-index=\"${selectedIndex}\"]`) as HTMLElement | null;\n if (selectedItem) {\n const extraMargin = 50;\n const containerScrollTop = container.scrollTop;\n const containerHeight = container.clientHeight;\n const itemTop = selectedItem.offsetTop;\n const itemBottom = itemTop + selectedItem.offsetHeight;\n if (itemTop < containerScrollTop + extraMargin) {\n container.scrollTo({ top: itemTop - extraMargin, behavior: 'smooth' });\n } else if (itemBottom > containerScrollTop + containerHeight - extraMargin) {\n container.scrollTo({\n top: itemBottom - containerHeight + extraMargin,\n behavior: 'smooth'\n });\n }\n }\n setKeyboardNav(false);\n }, [selectedIndex, keyboardNav]);\n\n return (\n
\n
\n {items.map((item, index) => (\n handleItemMouseEnter(index)}\n onClick={() => handleItemClick(item, index)}\n >\n
\n

{item}

\n
\n \n ))}\n
\n {showGradients && (\n <>\n
\n
\n \n )}\n
\n );\n};\n\nexport default AnimatedList;\n" + "path": "AnimatedList.tsx", + "content": "import React, {\n useRef,\n useState,\n useEffect,\n useCallback,\n type ReactNode,\n type MouseEventHandler,\n type UIEvent\n} from 'react';\nimport { motion, useInView } from 'motion/react';\nimport './AnimatedList.css';\n\ninterface AnimatedItemProps {\n children: ReactNode;\n delay?: number;\n index: number;\n onMouseEnter?: MouseEventHandler;\n onClick?: MouseEventHandler;\n}\n\nconst AnimatedItem: React.FC = ({ children, delay = 0, index, onMouseEnter, onClick }) => {\n const ref = useRef(null);\n const inView = useInView(ref, { amount: 0.5, once: false });\n return (\n \n {children}\n \n );\n};\n\ninterface AnimatedListProps {\n items?: string[];\n onItemSelect?: (item: string, index: number) => void;\n showGradients?: boolean;\n enableArrowNavigation?: boolean;\n className?: string;\n itemClassName?: string;\n displayScrollbar?: boolean;\n initialSelectedIndex?: number;\n}\n\nconst AnimatedList: React.FC = ({\n items = [\n 'Item 1',\n 'Item 2',\n 'Item 3',\n 'Item 4',\n 'Item 5',\n 'Item 6',\n 'Item 7',\n 'Item 8',\n 'Item 9',\n 'Item 10',\n 'Item 11',\n 'Item 12',\n 'Item 13',\n 'Item 14',\n 'Item 15'\n ],\n onItemSelect,\n showGradients = true,\n enableArrowNavigation = true,\n className = '',\n itemClassName = '',\n displayScrollbar = true,\n initialSelectedIndex = -1\n}) => {\n const listRef = useRef(null);\n const [selectedIndex, setSelectedIndex] = useState(initialSelectedIndex);\n const [keyboardNav, setKeyboardNav] = useState(false);\n const [topGradientOpacity, setTopGradientOpacity] = useState(0);\n const [bottomGradientOpacity, setBottomGradientOpacity] = useState(1);\n\n const handleItemMouseEnter = useCallback((index: number) => {\n setSelectedIndex(index);\n }, []);\n\n const handleItemClick = useCallback(\n (item: string, index: number) => {\n setSelectedIndex(index);\n if (onItemSelect) {\n onItemSelect(item, index);\n }\n },\n [onItemSelect]\n );\n\n const handleScroll = useCallback((e: UIEvent) => {\n const target = e.target as HTMLDivElement;\n const { scrollTop, scrollHeight, clientHeight } = target;\n setTopGradientOpacity(Math.min(scrollTop / 50, 1));\n const bottomDistance = scrollHeight - (scrollTop + clientHeight);\n setBottomGradientOpacity(scrollHeight <= clientHeight ? 0 : Math.min(bottomDistance / 50, 1));\n }, []);\n\n useEffect(() => {\n if (!enableArrowNavigation) return;\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'ArrowDown' || (e.key === 'Tab' && !e.shiftKey)) {\n e.preventDefault();\n setKeyboardNav(true);\n setSelectedIndex(prev => Math.min(prev + 1, items.length - 1));\n } else if (e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey)) {\n e.preventDefault();\n setKeyboardNav(true);\n setSelectedIndex(prev => Math.max(prev - 1, 0));\n } else if (e.key === 'Enter') {\n if (selectedIndex >= 0 && selectedIndex < items.length) {\n e.preventDefault();\n if (onItemSelect) {\n onItemSelect(items[selectedIndex], selectedIndex);\n }\n }\n }\n };\n\n window.addEventListener('keydown', handleKeyDown);\n return () => window.removeEventListener('keydown', handleKeyDown);\n }, [items, selectedIndex, onItemSelect, enableArrowNavigation]);\n\n useEffect(() => {\n if (!keyboardNav || selectedIndex < 0 || !listRef.current) return;\n const container = listRef.current;\n const selectedItem = container.querySelector(`[data-index=\"${selectedIndex}\"]`) as HTMLElement | null;\n if (selectedItem) {\n const extraMargin = 50;\n const containerScrollTop = container.scrollTop;\n const containerHeight = container.clientHeight;\n const itemTop = selectedItem.offsetTop;\n const itemBottom = itemTop + selectedItem.offsetHeight;\n if (itemTop < containerScrollTop + extraMargin) {\n container.scrollTo({ top: itemTop - extraMargin, behavior: 'smooth' });\n } else if (itemBottom > containerScrollTop + containerHeight - extraMargin) {\n container.scrollTo({\n top: itemBottom - containerHeight + extraMargin,\n behavior: 'smooth'\n });\n }\n }\n setKeyboardNav(false);\n }, [selectedIndex, keyboardNav]);\n\n return (\n
\n
\n {items.map((item, index) => (\n handleItemMouseEnter(index)}\n onClick={() => handleItemClick(item, index)}\n >\n
\n

{item}

\n
\n \n ))}\n
\n {showGradients && (\n <>\n
\n
\n \n )}\n
\n );\n};\n\nexport default AnimatedList;\n" } ], "registryDependencies": [], diff --git a/public/r/AnimatedList-TS-TW.json b/public/r/AnimatedList-TS-TW.json index f0e1950e..5132102d 100644 --- a/public/r/AnimatedList-TS-TW.json +++ b/public/r/AnimatedList-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "AnimatedList/AnimatedList.tsx", - "content": "import React, { useRef, useState, useEffect, useCallback, ReactNode, MouseEventHandler, UIEvent } from 'react';\nimport { motion, useInView } from 'motion/react';\n\ninterface AnimatedItemProps {\n children: ReactNode;\n delay?: number;\n index: number;\n onMouseEnter?: MouseEventHandler;\n onClick?: MouseEventHandler;\n}\n\nconst AnimatedItem: React.FC = ({ children, delay = 0, index, onMouseEnter, onClick }) => {\n const ref = useRef(null);\n const inView = useInView(ref, { amount: 0.5, once: false });\n return (\n \n {children}\n \n );\n};\n\ninterface AnimatedListProps {\n items?: string[];\n onItemSelect?: (item: string, index: number) => void;\n showGradients?: boolean;\n enableArrowNavigation?: boolean;\n className?: string;\n itemClassName?: string;\n displayScrollbar?: boolean;\n initialSelectedIndex?: number;\n}\n\nconst AnimatedList: React.FC = ({\n items = [\n 'Item 1',\n 'Item 2',\n 'Item 3',\n 'Item 4',\n 'Item 5',\n 'Item 6',\n 'Item 7',\n 'Item 8',\n 'Item 9',\n 'Item 10',\n 'Item 11',\n 'Item 12',\n 'Item 13',\n 'Item 14',\n 'Item 15'\n ],\n onItemSelect,\n showGradients = true,\n enableArrowNavigation = true,\n className = '',\n itemClassName = '',\n displayScrollbar = true,\n initialSelectedIndex = -1\n}) => {\n const listRef = useRef(null);\n const [selectedIndex, setSelectedIndex] = useState(initialSelectedIndex);\n const [keyboardNav, setKeyboardNav] = useState(false);\n const [topGradientOpacity, setTopGradientOpacity] = useState(0);\n const [bottomGradientOpacity, setBottomGradientOpacity] = useState(1);\n\n const handleItemMouseEnter = useCallback((index: number) => {\n setSelectedIndex(index);\n }, []);\n\n const handleItemClick = useCallback(\n (item: string, index: number) => {\n setSelectedIndex(index);\n if (onItemSelect) {\n onItemSelect(item, index);\n }\n },\n [onItemSelect]\n );\n\n const handleScroll = (e: UIEvent) => {\n const { scrollTop, scrollHeight, clientHeight } = e.target as HTMLDivElement;\n setTopGradientOpacity(Math.min(scrollTop / 50, 1));\n const bottomDistance = scrollHeight - (scrollTop + clientHeight);\n setBottomGradientOpacity(scrollHeight <= clientHeight ? 0 : Math.min(bottomDistance / 50, 1));\n };\n\n useEffect(() => {\n if (!enableArrowNavigation) return;\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'ArrowDown' || (e.key === 'Tab' && !e.shiftKey)) {\n e.preventDefault();\n setKeyboardNav(true);\n setSelectedIndex(prev => Math.min(prev + 1, items.length - 1));\n } else if (e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey)) {\n e.preventDefault();\n setKeyboardNav(true);\n setSelectedIndex(prev => Math.max(prev - 1, 0));\n } else if (e.key === 'Enter') {\n if (selectedIndex >= 0 && selectedIndex < items.length) {\n e.preventDefault();\n if (onItemSelect) {\n onItemSelect(items[selectedIndex], selectedIndex);\n }\n }\n }\n };\n\n window.addEventListener('keydown', handleKeyDown);\n return () => window.removeEventListener('keydown', handleKeyDown);\n }, [items, selectedIndex, onItemSelect, enableArrowNavigation]);\n\n useEffect(() => {\n if (!keyboardNav || selectedIndex < 0 || !listRef.current) return;\n const container = listRef.current;\n const selectedItem = container.querySelector(`[data-index=\"${selectedIndex}\"]`) as HTMLElement | null;\n if (selectedItem) {\n const extraMargin = 50;\n const containerScrollTop = container.scrollTop;\n const containerHeight = container.clientHeight;\n const itemTop = selectedItem.offsetTop;\n const itemBottom = itemTop + selectedItem.offsetHeight;\n if (itemTop < containerScrollTop + extraMargin) {\n container.scrollTo({ top: itemTop - extraMargin, behavior: 'smooth' });\n } else if (itemBottom > containerScrollTop + containerHeight - extraMargin) {\n container.scrollTo({\n top: itemBottom - containerHeight + extraMargin,\n behavior: 'smooth'\n });\n }\n }\n setKeyboardNav(false);\n }, [selectedIndex, keyboardNav]);\n\n return (\n
\n \n {items.map((item, index) => (\n handleItemMouseEnter(index)}\n onClick={() => handleItemClick(item, index)}\n >\n
\n

{item}

\n
\n \n ))}\n
\n {showGradients && (\n <>\n
\n
\n \n )}\n \n );\n};\n\nexport default AnimatedList;\n" + "content": "import React, {\n useRef,\n useState,\n useEffect,\n useCallback,\n type ReactNode,\n type MouseEventHandler,\n type UIEvent\n} from 'react';\nimport { motion, useInView } from 'motion/react';\n\ninterface AnimatedItemProps {\n children: ReactNode;\n delay?: number;\n index: number;\n onMouseEnter?: MouseEventHandler;\n onClick?: MouseEventHandler;\n}\n\nconst AnimatedItem: React.FC = ({ children, delay = 0, index, onMouseEnter, onClick }) => {\n const ref = useRef(null);\n const inView = useInView(ref, { amount: 0.5, once: false });\n return (\n \n {children}\n \n );\n};\n\ninterface AnimatedListProps {\n items?: string[];\n onItemSelect?: (item: string, index: number) => void;\n showGradients?: boolean;\n enableArrowNavigation?: boolean;\n className?: string;\n itemClassName?: string;\n displayScrollbar?: boolean;\n initialSelectedIndex?: number;\n}\n\nconst AnimatedList: React.FC = ({\n items = [\n 'Item 1',\n 'Item 2',\n 'Item 3',\n 'Item 4',\n 'Item 5',\n 'Item 6',\n 'Item 7',\n 'Item 8',\n 'Item 9',\n 'Item 10',\n 'Item 11',\n 'Item 12',\n 'Item 13',\n 'Item 14',\n 'Item 15'\n ],\n onItemSelect,\n showGradients = true,\n enableArrowNavigation = true,\n className = '',\n itemClassName = '',\n displayScrollbar = true,\n initialSelectedIndex = -1\n}) => {\n const listRef = useRef(null);\n const [selectedIndex, setSelectedIndex] = useState(initialSelectedIndex);\n const [keyboardNav, setKeyboardNav] = useState(false);\n const [topGradientOpacity, setTopGradientOpacity] = useState(0);\n const [bottomGradientOpacity, setBottomGradientOpacity] = useState(1);\n\n const handleItemMouseEnter = useCallback((index: number) => {\n setSelectedIndex(index);\n }, []);\n\n const handleItemClick = useCallback(\n (item: string, index: number) => {\n setSelectedIndex(index);\n if (onItemSelect) {\n onItemSelect(item, index);\n }\n },\n [onItemSelect]\n );\n\n const handleScroll = (e: UIEvent) => {\n const { scrollTop, scrollHeight, clientHeight } = e.target as HTMLDivElement;\n setTopGradientOpacity(Math.min(scrollTop / 50, 1));\n const bottomDistance = scrollHeight - (scrollTop + clientHeight);\n setBottomGradientOpacity(scrollHeight <= clientHeight ? 0 : Math.min(bottomDistance / 50, 1));\n };\n\n useEffect(() => {\n if (!enableArrowNavigation) return;\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'ArrowDown' || (e.key === 'Tab' && !e.shiftKey)) {\n e.preventDefault();\n setKeyboardNav(true);\n setSelectedIndex(prev => Math.min(prev + 1, items.length - 1));\n } else if (e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey)) {\n e.preventDefault();\n setKeyboardNav(true);\n setSelectedIndex(prev => Math.max(prev - 1, 0));\n } else if (e.key === 'Enter') {\n if (selectedIndex >= 0 && selectedIndex < items.length) {\n e.preventDefault();\n if (onItemSelect) {\n onItemSelect(items[selectedIndex], selectedIndex);\n }\n }\n }\n };\n\n window.addEventListener('keydown', handleKeyDown);\n return () => window.removeEventListener('keydown', handleKeyDown);\n }, [items, selectedIndex, onItemSelect, enableArrowNavigation]);\n\n useEffect(() => {\n if (!keyboardNav || selectedIndex < 0 || !listRef.current) return;\n const container = listRef.current;\n const selectedItem = container.querySelector(`[data-index=\"${selectedIndex}\"]`) as HTMLElement | null;\n if (selectedItem) {\n const extraMargin = 50;\n const containerScrollTop = container.scrollTop;\n const containerHeight = container.clientHeight;\n const itemTop = selectedItem.offsetTop;\n const itemBottom = itemTop + selectedItem.offsetHeight;\n if (itemTop < containerScrollTop + extraMargin) {\n container.scrollTo({ top: itemTop - extraMargin, behavior: 'smooth' });\n } else if (itemBottom > containerScrollTop + containerHeight - extraMargin) {\n container.scrollTo({\n top: itemBottom - containerHeight + extraMargin,\n behavior: 'smooth'\n });\n }\n }\n setKeyboardNav(false);\n }, [selectedIndex, keyboardNav]);\n\n return (\n
\n \n {items.map((item, index) => (\n handleItemMouseEnter(index)}\n onClick={() => handleItemClick(item, index)}\n >\n
\n

{item}

\n
\n \n ))}\n
\n {showGradients && (\n <>\n \n \n \n )}\n \n );\n};\n\nexport default AnimatedList;\n" } ], "registryDependencies": [], diff --git a/public/r/Aurora-JS-CSS.json b/public/r/Aurora-JS-CSS.json index 0521aa4a..0962f9b0 100644 --- a/public/r/Aurora-JS-CSS.json +++ b/public/r/Aurora-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Aurora/Aurora.css", + "type": "registry:file", + "path": "Aurora.css", + "target": "@components/Aurora.css", "content": ".aurora-container {\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "Aurora/Aurora.jsx", + "path": "Aurora.jsx", "content": "import { Renderer, Program, Mesh, Color, Triangle } from 'ogl';\nimport { useEffect, useRef } from 'react';\n\nimport './Aurora.css';\n\nconst VERT = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst FRAG = `#version 300 es\nprecision highp float;\n\nuniform float uTime;\nuniform float uAmplitude;\nuniform vec3 uColorStops[3];\nuniform vec2 uResolution;\nuniform float uBlend;\n\nout vec4 fragColor;\n\nvec3 permute(vec3 x) {\n return mod(((x * 34.0) + 1.0) * x, 289.0);\n}\n\nfloat snoise(vec2 v){\n const vec4 C = vec4(\n 0.211324865405187, 0.366025403784439,\n -0.577350269189626, 0.024390243902439\n );\n vec2 i = floor(v + dot(v, C.yy));\n vec2 x0 = v - i + dot(i, C.xx);\n vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);\n vec4 x12 = x0.xyxy + C.xxzz;\n x12.xy -= i1;\n i = mod(i, 289.0);\n\n vec3 p = permute(\n permute(i.y + vec3(0.0, i1.y, 1.0))\n + i.x + vec3(0.0, i1.x, 1.0)\n );\n\n vec3 m = max(\n 0.5 - vec3(\n dot(x0, x0),\n dot(x12.xy, x12.xy),\n dot(x12.zw, x12.zw)\n ), \n 0.0\n );\n m = m * m;\n m = m * m;\n\n vec3 x = 2.0 * fract(p * C.www) - 1.0;\n vec3 h = abs(x) - 0.5;\n vec3 ox = floor(x + 0.5);\n vec3 a0 = x - ox;\n m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);\n\n vec3 g;\n g.x = a0.x * x0.x + h.x * x0.y;\n g.yz = a0.yz * x12.xz + h.yz * x12.yw;\n return 130.0 * dot(m, g);\n}\n\nstruct ColorStop {\n vec3 color;\n float position;\n};\n\n#define COLOR_RAMP(colors, factor, finalColor) { \\\n int index = 0; \\\n for (int i = 0; i < 2; i++) { \\\n ColorStop currentColor = colors[i]; \\\n bool isInBetween = currentColor.position <= factor; \\\n index = int(mix(float(index), float(i), float(isInBetween))); \\\n } \\\n ColorStop currentColor = colors[index]; \\\n ColorStop nextColor = colors[index + 1]; \\\n float range = nextColor.position - currentColor.position; \\\n float lerpFactor = (factor - currentColor.position) / range; \\\n finalColor = mix(currentColor.color, nextColor.color, lerpFactor); \\\n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / uResolution;\n \n ColorStop colors[3];\n colors[0] = ColorStop(uColorStops[0], 0.0);\n colors[1] = ColorStop(uColorStops[1], 0.5);\n colors[2] = ColorStop(uColorStops[2], 1.0);\n \n vec3 rampColor;\n COLOR_RAMP(colors, uv.x, rampColor);\n \n float height = snoise(vec2(uv.x * 2.0 + uTime * 0.1, uTime * 0.25)) * 0.5 * uAmplitude;\n height = exp(height);\n height = (uv.y * 2.0 - height + 0.2);\n float intensity = 0.6 * height;\n \n float midPoint = 0.20;\n float auroraAlpha = smoothstep(midPoint - uBlend * 0.5, midPoint + uBlend * 0.5, intensity);\n \n vec3 auroraColor = intensity * rampColor;\n \n fragColor = vec4(auroraColor * auroraAlpha, auroraAlpha);\n}\n`;\n\nexport default function Aurora(props) {\n const { colorStops = ['#5227FF', '#7cff67', '#5227FF'], amplitude = 1.0, blend = 0.5 } = props;\n const propsRef = useRef(props);\n propsRef.current = props;\n\n const ctnDom = useRef(null);\n\n useEffect(() => {\n const ctn = ctnDom.current;\n if (!ctn) return;\n\n const renderer = new Renderer({\n alpha: true,\n premultipliedAlpha: true,\n antialias: true\n });\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n gl.enable(gl.BLEND);\n gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);\n gl.canvas.style.backgroundColor = 'transparent';\n\n let program;\n\n function resize() {\n if (!ctn) return;\n const width = ctn.offsetWidth;\n const height = ctn.offsetHeight;\n renderer.setSize(width, height);\n if (program) {\n program.uniforms.uResolution.value = [width, height];\n }\n }\n window.addEventListener('resize', resize);\n\n const geometry = new Triangle(gl);\n if (geometry.attributes.uv) {\n delete geometry.attributes.uv;\n }\n\n const colorStopsArray = colorStops.map(hex => {\n const c = new Color(hex);\n return [c.r, c.g, c.b];\n });\n\n program = new Program(gl, {\n vertex: VERT,\n fragment: FRAG,\n uniforms: {\n uTime: { value: 0 },\n uAmplitude: { value: amplitude },\n uColorStops: { value: colorStopsArray },\n uResolution: { value: [ctn.offsetWidth, ctn.offsetHeight] },\n uBlend: { value: blend }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n ctn.appendChild(gl.canvas);\n\n let animateId = 0;\n const update = t => {\n animateId = requestAnimationFrame(update);\n const { time = t * 0.01, speed = 1.0 } = propsRef.current;\n program.uniforms.uTime.value = time * speed * 0.1;\n program.uniforms.uAmplitude.value = propsRef.current.amplitude ?? 1.0;\n program.uniforms.uBlend.value = propsRef.current.blend ?? blend;\n const stops = propsRef.current.colorStops ?? colorStops;\n program.uniforms.uColorStops.value = stops.map(hex => {\n const c = new Color(hex);\n return [c.r, c.g, c.b];\n });\n renderer.render({ scene: mesh });\n };\n animateId = requestAnimationFrame(update);\n\n resize();\n\n return () => {\n cancelAnimationFrame(animateId);\n window.removeEventListener('resize', resize);\n if (ctn && gl.canvas.parentNode === ctn) {\n ctn.removeChild(gl.canvas);\n }\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [amplitude]);\n\n return
;\n}\n" } ], diff --git a/public/r/Aurora-TS-CSS.json b/public/r/Aurora-TS-CSS.json index 5e4d8e4e..4bb71881 100644 --- a/public/r/Aurora-TS-CSS.json +++ b/public/r/Aurora-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Aurora/Aurora.css", + "type": "registry:file", + "path": "Aurora.css", + "target": "@components/Aurora.css", "content": ".aurora-container {\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "Aurora/Aurora.tsx", + "path": "Aurora.tsx", "content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Color, Triangle } from 'ogl';\n\nimport './Aurora.css';\n\nconst VERT = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst FRAG = `#version 300 es\nprecision highp float;\n\nuniform float uTime;\nuniform float uAmplitude;\nuniform vec3 uColorStops[3];\nuniform vec2 uResolution;\nuniform float uBlend;\n\nout vec4 fragColor;\n\nvec3 permute(vec3 x) {\n return mod(((x * 34.0) + 1.0) * x, 289.0);\n}\n\nfloat snoise(vec2 v){\n const vec4 C = vec4(\n 0.211324865405187, 0.366025403784439,\n -0.577350269189626, 0.024390243902439\n );\n vec2 i = floor(v + dot(v, C.yy));\n vec2 x0 = v - i + dot(i, C.xx);\n vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);\n vec4 x12 = x0.xyxy + C.xxzz;\n x12.xy -= i1;\n i = mod(i, 289.0);\n\n vec3 p = permute(\n permute(i.y + vec3(0.0, i1.y, 1.0))\n + i.x + vec3(0.0, i1.x, 1.0)\n );\n\n vec3 m = max(\n 0.5 - vec3(\n dot(x0, x0),\n dot(x12.xy, x12.xy),\n dot(x12.zw, x12.zw)\n ), \n 0.0\n );\n m = m * m;\n m = m * m;\n\n vec3 x = 2.0 * fract(p * C.www) - 1.0;\n vec3 h = abs(x) - 0.5;\n vec3 ox = floor(x + 0.5);\n vec3 a0 = x - ox;\n m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);\n\n vec3 g;\n g.x = a0.x * x0.x + h.x * x0.y;\n g.yz = a0.yz * x12.xz + h.yz * x12.yw;\n return 130.0 * dot(m, g);\n}\n\nstruct ColorStop {\n vec3 color;\n float position;\n};\n\n#define COLOR_RAMP(colors, factor, finalColor) { \\\n int index = 0; \\\n for (int i = 0; i < 2; i++) { \\\n ColorStop currentColor = colors[i]; \\\n bool isInBetween = currentColor.position <= factor; \\\n index = int(mix(float(index), float(i), float(isInBetween))); \\\n } \\\n ColorStop currentColor = colors[index]; \\\n ColorStop nextColor = colors[index + 1]; \\\n float range = nextColor.position - currentColor.position; \\\n float lerpFactor = (factor - currentColor.position) / range; \\\n finalColor = mix(currentColor.color, nextColor.color, lerpFactor); \\\n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / uResolution;\n \n ColorStop colors[3];\n colors[0] = ColorStop(uColorStops[0], 0.0);\n colors[1] = ColorStop(uColorStops[1], 0.5);\n colors[2] = ColorStop(uColorStops[2], 1.0);\n \n vec3 rampColor;\n COLOR_RAMP(colors, uv.x, rampColor);\n \n float height = snoise(vec2(uv.x * 2.0 + uTime * 0.1, uTime * 0.25)) * 0.5 * uAmplitude;\n height = exp(height);\n height = (uv.y * 2.0 - height + 0.2);\n float intensity = 0.6 * height;\n \n float midPoint = 0.20;\n float auroraAlpha = smoothstep(midPoint - uBlend * 0.5, midPoint + uBlend * 0.5, intensity);\n \n vec3 auroraColor = intensity * rampColor;\n \n fragColor = vec4(auroraColor * auroraAlpha, auroraAlpha);\n}\n`;\n\ninterface AuroraProps {\n colorStops?: string[];\n amplitude?: number;\n blend?: number;\n time?: number;\n speed?: number;\n}\n\nexport default function Aurora(props: AuroraProps) {\n const { colorStops = ['#5227FF', '#7cff67', '#5227FF'], amplitude = 1.0, blend = 0.5 } = props;\n const propsRef = useRef(props);\n propsRef.current = props;\n\n const ctnDom = useRef(null);\n\n useEffect(() => {\n const ctn = ctnDom.current;\n if (!ctn) return;\n\n const renderer = new Renderer({\n alpha: true,\n premultipliedAlpha: true,\n antialias: true\n });\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n gl.enable(gl.BLEND);\n gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);\n gl.canvas.style.backgroundColor = 'transparent';\n\n let program: Program | undefined;\n\n function resize() {\n if (!ctn) return;\n const width = ctn.offsetWidth;\n const height = ctn.offsetHeight;\n renderer.setSize(width, height);\n if (program) {\n program.uniforms.uResolution.value = [width, height];\n }\n }\n window.addEventListener('resize', resize);\n\n const geometry = new Triangle(gl);\n if (geometry.attributes.uv) {\n delete geometry.attributes.uv;\n }\n\n const colorStopsArray = colorStops.map(hex => {\n const c = new Color(hex);\n return [c.r, c.g, c.b];\n });\n\n program = new Program(gl, {\n vertex: VERT,\n fragment: FRAG,\n uniforms: {\n uTime: { value: 0 },\n uAmplitude: { value: amplitude },\n uColorStops: { value: colorStopsArray },\n uResolution: { value: [ctn.offsetWidth, ctn.offsetHeight] },\n uBlend: { value: blend }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n ctn.appendChild(gl.canvas);\n\n let animateId = 0;\n const update = (t: number) => {\n animateId = requestAnimationFrame(update);\n const { time = t * 0.01, speed = 1.0 } = propsRef.current;\n if (program) {\n program.uniforms.uTime.value = time * speed * 0.1;\n program.uniforms.uAmplitude.value = propsRef.current.amplitude ?? 1.0;\n program.uniforms.uBlend.value = propsRef.current.blend ?? blend;\n const stops = propsRef.current.colorStops ?? colorStops;\n program.uniforms.uColorStops.value = stops.map((hex: string) => {\n const c = new Color(hex);\n return [c.r, c.g, c.b];\n });\n renderer.render({ scene: mesh });\n }\n };\n animateId = requestAnimationFrame(update);\n\n resize();\n\n return () => {\n cancelAnimationFrame(animateId);\n window.removeEventListener('resize', resize);\n if (ctn && gl.canvas.parentNode === ctn) {\n ctn.removeChild(gl.canvas);\n }\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, [amplitude]);\n\n return
;\n}\n" } ], diff --git a/public/r/Balatro-JS-CSS.json b/public/r/Balatro-JS-CSS.json index c03e2335..e5601901 100644 --- a/public/r/Balatro-JS-CSS.json +++ b/public/r/Balatro-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Balatro/Balatro.css", + "type": "registry:file", + "path": "Balatro.css", + "target": "@components/Balatro.css", "content": ".balatro-container {\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "Balatro/Balatro.jsx", + "path": "Balatro.jsx", "content": "import { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport { useEffect, useRef } from 'react';\n\nimport './Balatro.css';\n\nfunction hexToVec4(hex) {\n let hexStr = hex.replace('#', '');\n let r = 0,\n g = 0,\n b = 0,\n a = 1;\n if (hexStr.length === 6) {\n r = parseInt(hexStr.slice(0, 2), 16) / 255;\n g = parseInt(hexStr.slice(2, 4), 16) / 255;\n b = parseInt(hexStr.slice(4, 6), 16) / 255;\n } else if (hexStr.length === 8) {\n r = parseInt(hexStr.slice(0, 2), 16) / 255;\n g = parseInt(hexStr.slice(2, 4), 16) / 255;\n b = parseInt(hexStr.slice(4, 6), 16) / 255;\n a = parseInt(hexStr.slice(6, 8), 16) / 255;\n }\n return [r, g, b, a];\n}\n\nconst vertexShader = `\nattribute vec2 uv;\nattribute vec2 position;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0, 1);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\n#define PI 3.14159265359\n\nuniform float iTime;\nuniform vec3 iResolution;\nuniform float uSpinRotation;\nuniform float uSpinSpeed;\nuniform vec2 uOffset;\nuniform vec4 uColor1;\nuniform vec4 uColor2;\nuniform vec4 uColor3;\nuniform float uContrast;\nuniform float uLighting;\nuniform float uSpinAmount;\nuniform float uPixelFilter;\nuniform float uSpinEase;\nuniform bool uIsRotate;\nuniform vec2 uMouse;\n\nvarying vec2 vUv;\n\nvec4 effect(vec2 screenSize, vec2 screen_coords) {\n float pixel_size = length(screenSize.xy) / uPixelFilter;\n vec2 uv = (floor(screen_coords.xy * (1.0 / pixel_size)) * pixel_size - 0.5 * screenSize.xy) / length(screenSize.xy) - uOffset;\n float uv_len = length(uv);\n \n float speed = (uSpinRotation * uSpinEase * 0.2);\n if(uIsRotate){\n speed = iTime * speed;\n }\n speed += 302.2;\n \n float mouseInfluence = (uMouse.x * 2.0 - 1.0);\n speed += mouseInfluence * 0.1;\n \n float new_pixel_angle = atan(uv.y, uv.x) + speed - uSpinEase * 20.0 * (uSpinAmount * uv_len + (1.0 - uSpinAmount));\n vec2 mid = (screenSize.xy / length(screenSize.xy)) / 2.0;\n uv = (vec2(uv_len * cos(new_pixel_angle) + mid.x, uv_len * sin(new_pixel_angle) + mid.y) - mid);\n \n uv *= 30.0;\n float baseSpeed = iTime * uSpinSpeed;\n speed = baseSpeed + mouseInfluence * 2.0;\n \n vec2 uv2 = vec2(uv.x + uv.y);\n \n for(int i = 0; i < 5; i++) {\n uv2 += sin(max(uv.x, uv.y)) + uv;\n uv += 0.5 * vec2(\n cos(5.1123314 + 0.353 * uv2.y + speed * 0.131121),\n sin(uv2.x - 0.113 * speed)\n );\n uv -= cos(uv.x + uv.y) - sin(uv.x * 0.711 - uv.y);\n }\n \n float contrast_mod = (0.25 * uContrast + 0.5 * uSpinAmount + 1.2);\n float paint_res = min(2.0, max(0.0, length(uv) * 0.035 * contrast_mod));\n float c1p = max(0.0, 1.0 - contrast_mod * abs(1.0 - paint_res));\n float c2p = max(0.0, 1.0 - contrast_mod * abs(paint_res));\n float c3p = 1.0 - min(1.0, c1p + c2p);\n float light = (uLighting - 0.2) * max(c1p * 5.0 - 4.0, 0.0) + uLighting * max(c2p * 5.0 - 4.0, 0.0);\n \n return (0.3 / uContrast) * uColor1 + (1.0 - 0.3 / uContrast) * (uColor1 * c1p + uColor2 * c2p + vec4(c3p * uColor3.rgb, c3p * uColor1.a)) + light;\n}\n\nvoid main() {\n vec2 uv = vUv * iResolution.xy;\n gl_FragColor = effect(iResolution.xy, uv);\n}\n`;\n\nexport default function Balatro({\n spinRotation = -2.0,\n spinSpeed = 7.0,\n offset = [0.0, 0.0],\n color1 = '#DE443B',\n color2 = '#006BB4',\n color3 = '#162325',\n contrast = 3.5,\n lighting = 0.4,\n spinAmount = 0.25,\n pixelFilter = 745.0,\n spinEase = 1.0,\n isRotate = false,\n mouseInteraction = true\n}) {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n const container = containerRef.current;\n const renderer = new Renderer();\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 1);\n\n let program;\n\n function resize() {\n renderer.setSize(container.offsetWidth, container.offsetHeight);\n if (program) {\n program.uniforms.iResolution.value = [gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height];\n }\n }\n window.addEventListener('resize', resize);\n resize();\n\n const geometry = new Triangle(gl);\n program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n iTime: { value: 0 },\n iResolution: {\n value: [gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height]\n },\n uSpinRotation: { value: spinRotation },\n uSpinSpeed: { value: spinSpeed },\n uOffset: { value: offset },\n uColor1: { value: hexToVec4(color1) },\n uColor2: { value: hexToVec4(color2) },\n uColor3: { value: hexToVec4(color3) },\n uContrast: { value: contrast },\n uLighting: { value: lighting },\n uSpinAmount: { value: spinAmount },\n uPixelFilter: { value: pixelFilter },\n uSpinEase: { value: spinEase },\n uIsRotate: { value: isRotate },\n uMouse: { value: [0.5, 0.5] }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n let animationFrameId;\n\n function update(time) {\n animationFrameId = requestAnimationFrame(update);\n program.uniforms.iTime.value = time * 0.001;\n renderer.render({ scene: mesh });\n }\n animationFrameId = requestAnimationFrame(update);\n container.appendChild(gl.canvas);\n\n function handleMouseMove(e) {\n if (!mouseInteraction) return;\n const rect = container.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1.0 - (e.clientY - rect.top) / rect.height;\n program.uniforms.uMouse.value = [x, y];\n }\n container.addEventListener('mousemove', handleMouseMove);\n\n return () => {\n cancelAnimationFrame(animationFrameId);\n window.removeEventListener('resize', resize);\n container.removeEventListener('mousemove', handleMouseMove);\n container.removeChild(gl.canvas);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, [\n spinRotation,\n spinSpeed,\n offset,\n color1,\n color2,\n color3,\n contrast,\n lighting,\n spinAmount,\n pixelFilter,\n spinEase,\n isRotate,\n mouseInteraction,\n containerRef\n ]);\n\n return
;\n}\n" } ], diff --git a/public/r/Balatro-TS-CSS.json b/public/r/Balatro-TS-CSS.json index 8efd9b1e..d4b560ed 100644 --- a/public/r/Balatro-TS-CSS.json +++ b/public/r/Balatro-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Balatro/Balatro.css", + "type": "registry:file", + "path": "Balatro.css", + "target": "@components/Balatro.css", "content": ".balatro-container {\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "Balatro/Balatro.tsx", + "path": "Balatro.tsx", "content": "import { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport { useEffect, useRef } from 'react';\n\nimport './Balatro.css';\n\ninterface BalatroProps {\n spinRotation?: number;\n spinSpeed?: number;\n offset?: [number, number];\n color1?: string;\n color2?: string;\n color3?: string;\n contrast?: number;\n lighting?: number;\n spinAmount?: number;\n pixelFilter?: number;\n spinEase?: number;\n isRotate?: boolean;\n mouseInteraction?: boolean;\n}\n\nfunction hexToVec4(hex: string): [number, number, number, number] {\n let hexStr = hex.replace('#', '');\n let r = 0,\n g = 0,\n b = 0,\n a = 1;\n if (hexStr.length === 6) {\n r = parseInt(hexStr.slice(0, 2), 16) / 255;\n g = parseInt(hexStr.slice(2, 4), 16) / 255;\n b = parseInt(hexStr.slice(4, 6), 16) / 255;\n } else if (hexStr.length === 8) {\n r = parseInt(hexStr.slice(0, 2), 16) / 255;\n g = parseInt(hexStr.slice(2, 4), 16) / 255;\n b = parseInt(hexStr.slice(4, 6), 16) / 255;\n a = parseInt(hexStr.slice(6, 8), 16) / 255;\n }\n return [r, g, b, a];\n}\n\nconst vertexShader = `\nattribute vec2 uv;\nattribute vec2 position;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0, 1);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\n#define PI 3.14159265359\n\nuniform float iTime;\nuniform vec3 iResolution;\nuniform float uSpinRotation;\nuniform float uSpinSpeed;\nuniform vec2 uOffset;\nuniform vec4 uColor1;\nuniform vec4 uColor2;\nuniform vec4 uColor3;\nuniform float uContrast;\nuniform float uLighting;\nuniform float uSpinAmount;\nuniform float uPixelFilter;\nuniform float uSpinEase;\nuniform bool uIsRotate;\nuniform vec2 uMouse;\n\nvarying vec2 vUv;\n\nvec4 effect(vec2 screenSize, vec2 screen_coords) {\n float pixel_size = length(screenSize.xy) / uPixelFilter;\n vec2 uv = (floor(screen_coords.xy * (1.0 / pixel_size)) * pixel_size - 0.5 * screenSize.xy) / length(screenSize.xy) - uOffset;\n float uv_len = length(uv);\n \n float speed = (uSpinRotation * uSpinEase * 0.2);\n if(uIsRotate){\n speed = iTime * speed;\n }\n speed += 302.2;\n \n float mouseInfluence = (uMouse.x * 2.0 - 1.0);\n speed += mouseInfluence * 0.1;\n \n float new_pixel_angle = atan(uv.y, uv.x) + speed - uSpinEase * 20.0 * (uSpinAmount * uv_len + (1.0 - uSpinAmount));\n vec2 mid = (screenSize.xy / length(screenSize.xy)) / 2.0;\n uv = (vec2(uv_len * cos(new_pixel_angle) + mid.x, uv_len * sin(new_pixel_angle) + mid.y) - mid);\n \n uv *= 30.0;\n float baseSpeed = iTime * uSpinSpeed;\n speed = baseSpeed + mouseInfluence * 2.0;\n \n vec2 uv2 = vec2(uv.x + uv.y);\n \n for(int i = 0; i < 5; i++) {\n uv2 += sin(max(uv.x, uv.y)) + uv;\n uv += 0.5 * vec2(\n cos(5.1123314 + 0.353 * uv2.y + speed * 0.131121),\n sin(uv2.x - 0.113 * speed)\n );\n uv -= cos(uv.x + uv.y) - sin(uv.x * 0.711 - uv.y);\n }\n \n float contrast_mod = (0.25 * uContrast + 0.5 * uSpinAmount + 1.2);\n float paint_res = min(2.0, max(0.0, length(uv) * 0.035 * contrast_mod));\n float c1p = max(0.0, 1.0 - contrast_mod * abs(1.0 - paint_res));\n float c2p = max(0.0, 1.0 - contrast_mod * abs(paint_res));\n float c3p = 1.0 - min(1.0, c1p + c2p);\n float light = (uLighting - 0.2) * max(c1p * 5.0 - 4.0, 0.0) + uLighting * max(c2p * 5.0 - 4.0, 0.0);\n \n return (0.3 / uContrast) * uColor1 + (1.0 - 0.3 / uContrast) * (uColor1 * c1p + uColor2 * c2p + vec4(c3p * uColor3.rgb, c3p * uColor1.a)) + light;\n}\n\nvoid main() {\n vec2 uv = vUv * iResolution.xy;\n gl_FragColor = effect(iResolution.xy, uv);\n}\n`;\n\nexport default function Balatro({\n spinRotation = -2.0,\n spinSpeed = 7.0,\n offset = [0.0, 0.0],\n color1 = '#DE443B',\n color2 = '#006BB4',\n color3 = '#162325',\n contrast = 3.5,\n lighting = 0.4,\n spinAmount = 0.25,\n pixelFilter = 745.0,\n spinEase = 1.0,\n isRotate = false,\n mouseInteraction = true\n}: BalatroProps) {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n const container = containerRef.current;\n const renderer = new Renderer();\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 1);\n\n let program: Program;\n\n function resize() {\n renderer.setSize(container.offsetWidth, container.offsetHeight);\n if (program) {\n program.uniforms.iResolution.value = [gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height];\n }\n }\n window.addEventListener('resize', resize);\n resize();\n\n const geometry = new Triangle(gl);\n program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n iTime: { value: 0 },\n iResolution: {\n value: [gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height]\n },\n uSpinRotation: { value: spinRotation },\n uSpinSpeed: { value: spinSpeed },\n uOffset: { value: offset },\n uColor1: { value: hexToVec4(color1) },\n uColor2: { value: hexToVec4(color2) },\n uColor3: { value: hexToVec4(color3) },\n uContrast: { value: contrast },\n uLighting: { value: lighting },\n uSpinAmount: { value: spinAmount },\n uPixelFilter: { value: pixelFilter },\n uSpinEase: { value: spinEase },\n uIsRotate: { value: isRotate },\n uMouse: { value: [0.5, 0.5] }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n let animationFrameId: number;\n\n function update(time: number) {\n animationFrameId = requestAnimationFrame(update);\n program.uniforms.iTime.value = time * 0.001;\n renderer.render({ scene: mesh });\n }\n animationFrameId = requestAnimationFrame(update);\n container.appendChild(gl.canvas);\n\n function handleMouseMove(e: MouseEvent) {\n if (!mouseInteraction) return;\n const rect = container.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1.0 - (e.clientY - rect.top) / rect.height;\n program.uniforms.uMouse.value = [x, y];\n }\n container.addEventListener('mousemove', handleMouseMove);\n\n return () => {\n cancelAnimationFrame(animationFrameId);\n window.removeEventListener('resize', resize);\n container.removeEventListener('mousemove', handleMouseMove);\n container.removeChild(gl.canvas);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, [\n spinRotation,\n spinSpeed,\n offset,\n color1,\n color2,\n color3,\n contrast,\n lighting,\n spinAmount,\n pixelFilter,\n spinEase,\n isRotate,\n mouseInteraction\n ]);\n\n return
;\n}\n" } ], diff --git a/public/r/Ballpit-JS-CSS.json b/public/r/Ballpit-JS-CSS.json index fcadf27a..2e93d52b 100644 --- a/public/r/Ballpit-JS-CSS.json +++ b/public/r/Ballpit-JS-CSS.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "Ballpit/Ballpit.jsx", - "content": "import { useEffect, useRef } from 'react';\nimport {\n Vector3 as a,\n MeshPhysicalMaterial as c,\n InstancedMesh as d,\n Timer as e,\n AmbientLight as f,\n SphereGeometry as g,\n ShaderChunk as h,\n Scene as i,\n Color as l,\n Object3D as m,\n SRGBColorSpace as n,\n MathUtils as o,\n PMREMGenerator as p,\n Vector2 as r,\n WebGLRenderer as s,\n PerspectiveCamera as t,\n PointLight as u,\n ACESFilmicToneMapping as v,\n Plane as w,\n Raycaster as y\n} from 'three';\nimport { RoomEnvironment as z } from 'three/examples/jsm/environments/RoomEnvironment.js';\n\nclass x {\n #e;\n canvas;\n camera;\n cameraMinAspect;\n cameraMaxAspect;\n cameraFov;\n maxPixelRatio;\n minPixelRatio;\n scene;\n renderer;\n #t;\n size = { width: 0, height: 0, wWidth: 0, wHeight: 0, ratio: 0, pixelRatio: 0 };\n render = this.#i;\n onBeforeRender = () => {};\n onAfterRender = () => {};\n onAfterResize = () => {};\n #s = false;\n #n = false;\n isDisposed = false;\n #o;\n #r;\n #a;\n #c = new e();\n #h = { elapsed: 0, delta: 0 };\n #l;\n constructor(e) {\n this.#e = { ...e };\n this.#m();\n this.#d();\n this.#p();\n this.resize();\n this.#g();\n }\n #m() {\n this.camera = new t();\n this.cameraFov = this.camera.fov;\n }\n #d() {\n this.scene = new i();\n }\n #p() {\n if (this.#e.canvas) {\n this.canvas = this.#e.canvas;\n } else if (this.#e.id) {\n this.canvas = document.getElementById(this.#e.id);\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas.style.display = 'block';\n const e = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#e.rendererOptions ?? {})\n };\n this.renderer = new s(e);\n this.renderer.outputColorSpace = n;\n }\n #g() {\n if (!(this.#e.size instanceof Object)) {\n window.addEventListener('resize', this.#f.bind(this));\n if (this.#e.size === 'parent' && this.canvas.parentNode) {\n this.#r = new ResizeObserver(this.#f.bind(this));\n this.#r.observe(this.canvas.parentNode);\n }\n }\n this.#o = new IntersectionObserver(this.#u.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#o.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#v.bind(this));\n }\n #y() {\n window.removeEventListener('resize', this.#f.bind(this));\n this.#r?.disconnect();\n this.#o?.disconnect();\n document.removeEventListener('visibilitychange', this.#v.bind(this));\n }\n #u(e) {\n this.#s = e[0].isIntersecting;\n this.#s ? this.#w() : this.#z();\n }\n #v() {\n if (this.#s) {\n document.hidden ? this.#z() : this.#w();\n }\n }\n #f() {\n if (this.#a) clearTimeout(this.#a);\n this.#a = setTimeout(this.resize.bind(this), 100);\n }\n resize() {\n let e, t;\n if (this.#e.size instanceof Object) {\n e = this.#e.size.width;\n t = this.#e.size.height;\n } else if (this.#e.size === 'parent' && this.canvas.parentNode) {\n e = this.canvas.parentNode.offsetWidth;\n t = this.canvas.parentNode.offsetHeight;\n } else {\n e = window.innerWidth;\n t = window.innerHeight;\n }\n this.size.width = e;\n this.size.height = t;\n this.size.ratio = e / t;\n this.#x();\n this.#b();\n this.onAfterResize(this.size);\n }\n #x() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#A(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#A(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n #A(e) {\n const t = Math.tan(o.degToRad(this.cameraFov / 2)) / (this.camera.aspect / e);\n this.camera.fov = 2 * o.radToDeg(Math.atan(t));\n }\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const e = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(e / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if (this.camera.isOrthographicCamera) {\n this.size.wHeight = this.camera.top - this.camera.bottom;\n this.size.wWidth = this.camera.right - this.camera.left;\n }\n }\n #b() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#t?.setSize(this.size.width, this.size.height);\n let e = window.devicePixelRatio;\n if (this.maxPixelRatio && e > this.maxPixelRatio) {\n e = this.maxPixelRatio;\n } else if (this.minPixelRatio && e < this.minPixelRatio) {\n e = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(e);\n this.size.pixelRatio = e;\n }\n get postprocessing() {\n return this.#t;\n }\n set postprocessing(e) {\n this.#t = e;\n this.render = e.render.bind(e);\n }\n #w() {\n if (this.#n) return;\n const animate = () => {\n this.#l = requestAnimationFrame(animate);\n this.#c.update();\n this.#h.delta = this.#c.getDelta();\n this.#h.elapsed += this.#h.delta;\n this.onBeforeRender(this.#h);\n this.render();\n this.onAfterRender(this.#h);\n };\n this.#n = true;\n this.#c.reset();\n animate();\n }\n #z() {\n if (this.#n) {\n cancelAnimationFrame(this.#l);\n this.#n = false;\n }\n }\n #i() {\n this.renderer.render(this.scene, this.camera);\n }\n clear() {\n this.scene.traverse(e => {\n if (e.isMesh && typeof e.material === 'object' && e.material !== null) {\n Object.keys(e.material).forEach(t => {\n const i = e.material[t];\n if (i !== null && typeof i === 'object' && typeof i.dispose === 'function') {\n i.dispose();\n }\n });\n e.material.dispose();\n e.geometry.dispose();\n }\n });\n this.scene.clear();\n }\n dispose() {\n this.#y();\n this.#z();\n this.#c.dispose();\n this.clear();\n this.#t?.dispose();\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n this.isDisposed = true;\n }\n}\n\nconst b = new Map(),\n A = new r();\nlet R = false;\nfunction S(e) {\n const t = {\n position: new r(),\n nPosition: new r(),\n hover: false,\n touching: false,\n onEnter() {},\n onMove() {},\n onClick() {},\n onLeave() {},\n ...e\n };\n (function (e, t) {\n if (!b.has(e)) {\n b.set(e, t);\n if (!R) {\n document.body.addEventListener('pointermove', M);\n document.body.addEventListener('pointerleave', L);\n document.body.addEventListener('click', C);\n\n document.body.addEventListener('touchstart', TouchStart, { passive: false });\n document.body.addEventListener('touchmove', TouchMove, { passive: false });\n document.body.addEventListener('touchend', TouchEnd, { passive: false });\n document.body.addEventListener('touchcancel', TouchEnd, { passive: false });\n\n R = true;\n }\n }\n })(e.domElement, t);\n t.dispose = () => {\n const t = e.domElement;\n b.delete(t);\n if (b.size === 0) {\n document.body.removeEventListener('pointermove', M);\n document.body.removeEventListener('pointerleave', L);\n document.body.removeEventListener('click', C);\n\n document.body.removeEventListener('touchstart', TouchStart);\n document.body.removeEventListener('touchmove', TouchMove);\n document.body.removeEventListener('touchend', TouchEnd);\n document.body.removeEventListener('touchcancel', TouchEnd);\n\n R = false;\n }\n };\n return t;\n}\n\nfunction M(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n processInteraction();\n}\n\nfunction processInteraction() {\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n if (D(i)) {\n P(t, i);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && !t.touching) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction C(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n P(t, i);\n if (D(i)) t.onClick(t);\n }\n}\n\nfunction L() {\n for (const t of b.values()) {\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction TouchStart(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n if (D(rect)) {\n t.touching = true;\n P(t, rect);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchMove(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n P(t, rect);\n\n if (D(rect)) {\n if (!t.hover) {\n t.hover = true;\n t.touching = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && t.touching) {\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchEnd() {\n for (const [, t] of b) {\n if (t.touching) {\n t.touching = false;\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n }\n}\n\nfunction P(e, t) {\n const { position: i, nPosition: s } = e;\n i.x = A.x - t.left;\n i.y = A.y - t.top;\n s.x = (i.x / t.width) * 2 - 1;\n s.y = (-i.y / t.height) * 2 + 1;\n}\nfunction D(e) {\n const { x: t, y: i } = A;\n const { left: s, top: n, width: o, height: r } = e;\n return t >= s && t <= s + o && i >= n && i <= n + r;\n}\n\nconst { randFloat: k, randFloatSpread: E } = o;\nconst F = new a();\nconst I = new a();\nconst O = new a();\nconst V = new a();\nconst B = new a();\nconst N = new a();\nconst _ = new a();\nconst j = new a();\nconst H = new a();\nconst T = new a();\n\nclass W {\n constructor(e) {\n this.config = e;\n this.positionData = new Float32Array(3 * e.count).fill(0);\n this.velocityData = new Float32Array(3 * e.count).fill(0);\n this.sizeData = new Float32Array(e.count).fill(1);\n this.center = new a();\n this.#R();\n this.setSizes();\n }\n #R() {\n const { config: e, positionData: t } = this;\n this.center.toArray(t, 0);\n for (let i = 1; i < e.count; i++) {\n const s = 3 * i;\n t[s] = E(2 * e.maxX);\n t[s + 1] = E(2 * e.maxY);\n t[s + 2] = E(2 * e.maxZ);\n }\n }\n setSizes() {\n const { config: e, sizeData: t } = this;\n t[0] = e.size0;\n for (let i = 1; i < e.count; i++) {\n t[i] = k(e.minSize, e.maxSize);\n }\n }\n update(e) {\n const { config: t, center: i, positionData: s, sizeData: n, velocityData: o } = this;\n let r = 0;\n if (t.controlSphere0) {\n r = 1;\n F.fromArray(s, 0);\n F.lerp(i, 0.1).toArray(s, 0);\n V.set(0, 0, 0).toArray(o, 0);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n B.y -= e.delta * t.gravity * n[idx];\n B.multiplyScalar(t.friction);\n B.clampLength(0, t.maxVelocity);\n I.add(B);\n I.toArray(s, base);\n B.toArray(o, base);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n const radius = n[idx];\n for (let jdx = idx + 1; jdx < t.count; jdx++) {\n const otherBase = 3 * jdx;\n O.fromArray(s, otherBase);\n N.fromArray(o, otherBase);\n const otherRadius = n[jdx];\n _.copy(O).sub(I);\n const dist = _.length();\n const sumRadius = radius + otherRadius;\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n j.copy(_)\n .normalize()\n .multiplyScalar(0.5 * overlap);\n H.copy(j).multiplyScalar(Math.max(B.length(), 1));\n T.copy(j).multiplyScalar(Math.max(N.length(), 1));\n I.sub(j);\n B.sub(H);\n I.toArray(s, base);\n B.toArray(o, base);\n O.add(j);\n N.add(T);\n O.toArray(s, otherBase);\n N.toArray(o, otherBase);\n }\n }\n if (t.controlSphere0) {\n _.copy(F).sub(I);\n const dist = _.length();\n const sumRadius0 = radius + n[0];\n if (dist < sumRadius0) {\n const diff = sumRadius0 - dist;\n j.copy(_.normalize()).multiplyScalar(diff);\n H.copy(j).multiplyScalar(Math.max(B.length(), 2));\n I.sub(j);\n B.sub(H);\n }\n }\n if (Math.abs(I.x) + radius > t.maxX) {\n I.x = Math.sign(I.x) * (t.maxX - radius);\n B.x = -B.x * t.wallBounce;\n }\n if (t.gravity === 0) {\n if (Math.abs(I.y) + radius > t.maxY) {\n I.y = Math.sign(I.y) * (t.maxY - radius);\n B.y = -B.y * t.wallBounce;\n }\n } else if (I.y - radius < -t.maxY) {\n I.y = -t.maxY + radius;\n B.y = -B.y * t.wallBounce;\n }\n const maxBoundary = Math.max(t.maxZ, t.maxSize);\n if (Math.abs(I.z) + radius > maxBoundary) {\n I.z = Math.sign(I.z) * (t.maxZ - radius);\n B.z = -B.z * t.wallBounce;\n }\n I.toArray(s, base);\n B.toArray(o, base);\n }\n }\n}\n\nclass Y extends c {\n constructor(e) {\n super(e);\n this.uniforms = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n this.defines.USE_UV = '';\n this.onBeforeCompile = e => {\n Object.assign(e.uniforms, this.uniforms);\n e.fragmentShader =\n '\\n uniform float thicknessPower;\\n uniform float thicknessScale;\\n uniform float thicknessDistortion;\\n uniform float thicknessAmbient;\\n uniform float thicknessAttenuation;\\n ' +\n e.fragmentShader;\n e.fragmentShader = e.fragmentShader.replace(\n 'void main() {',\n '\\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\\n #ifdef USE_COLOR\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\\n #else\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\\n #endif\\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\\n }\\n\\n void main() {\\n '\n );\n const t = h.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n '\\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\\n '\n );\n e.fragmentShader = e.fragmentShader.replace('#include ', t);\n if (this.onBeforeCompile2) this.onBeforeCompile2(e);\n };\n }\n}\n\nconst X = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 16777215,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new m();\n\nclass Z extends d {\n constructor(e, t = {}) {\n const i = { ...X, ...t };\n const s = new z();\n const n = new p(e, 0.04).fromScene(s).texture;\n const o = new g();\n const r = new Y({ envMap: n, ...i.materialParams });\n r.envMapRotation.x = -Math.PI / 2;\n super(o, r, i.count);\n this.config = i;\n this.physics = new W(i);\n this.#S();\n this.setColors(i.colors);\n }\n #S() {\n this.ambientLight = new f(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new u(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n setColors(e) {\n if (Array.isArray(e) && e.length > 1) {\n const t = (function (e) {\n let t, i;\n function setColors(e) {\n t = e;\n i = [];\n t.forEach(col => {\n i.push(new l(col));\n });\n }\n setColors(e);\n return {\n setColors,\n getColorAt: function (ratio, out = new l()) {\n const scaled = Math.max(0, Math.min(1, ratio)) * (t.length - 1);\n const idx = Math.floor(scaled);\n const start = i[idx];\n if (idx >= t.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = i[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(e);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, t.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light.color.copy(t.getColorAt(idx / this.count));\n }\n }\n this.instanceColor.needsUpdate = true;\n }\n }\n update(e) {\n this.physics.update(e);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\nfunction createBallpit(e, t = {}) {\n const i = new x({\n canvas: e,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let s;\n i.renderer.toneMapping = v;\n i.camera.position.set(0, 0, 20);\n i.camera.lookAt(0, 0, 0);\n i.cameraMaxAspect = 1.5;\n i.resize();\n initialize(t);\n const n = new y();\n const o = new w(new a(0, 0, 1), 0);\n const r = new a();\n let c = false;\n\n e.style.touchAction = 'none';\n e.style.userSelect = 'none';\n e.style.webkitUserSelect = 'none';\n\n const h = S({\n domElement: e,\n onMove() {\n n.setFromCamera(h.nPosition, i.camera);\n i.camera.getWorldDirection(o.normal);\n n.ray.intersectPlane(o, r);\n s.physics.center.copy(r);\n s.config.controlSphere0 = true;\n },\n onLeave() {\n s.config.controlSphere0 = false;\n }\n });\n function initialize(e) {\n if (s) {\n i.clear();\n i.scene.remove(s);\n }\n s = new Z(i.renderer, e);\n i.scene.add(s);\n }\n i.onBeforeRender = e => {\n if (!c) s.update(e);\n };\n i.onAfterResize = e => {\n s.config.maxX = e.wWidth / 2;\n s.config.maxY = e.wHeight / 2;\n };\n return {\n three: i,\n get spheres() {\n return s;\n },\n setCount(e) {\n initialize({ ...s.config, count: e });\n },\n updateConfig(newProps) {\n if (newProps.count !== undefined && newProps.count !== s.config.count) {\n initialize({ ...s.config, ...newProps });\n } else {\n Object.assign(s.config, newProps);\n if (newProps.colors) {\n s.setColors(s.config.colors);\n }\n if (newProps.minSize !== undefined || newProps.maxSize !== undefined || newProps.size0 !== undefined) {\n s.physics.setSizes();\n }\n }\n },\n togglePause() {\n c = !c;\n },\n dispose() {\n h.dispose();\n i.dispose();\n }\n };\n}\n\nconst Ballpit = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n const isFirstRender = useRef(true);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, { followCursor, ...props });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n spheresInstanceRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (isFirstRender.current) {\n isFirstRender.current = false;\n return;\n }\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.updateConfig({ followCursor, ...props });\n }\n }, [props, followCursor]);\n\n return ;\n};\n\nexport default Ballpit;\n" + "content": "import { useEffect, useRef } from 'react';\nimport {\n Vector3 as a,\n MeshPhysicalMaterial as c,\n InstancedMesh as d,\n Timer as e,\n AmbientLight as f,\n SphereGeometry as g,\n ShaderChunk as h,\n Scene as i,\n Color as l,\n Object3D as m,\n SRGBColorSpace as n,\n MathUtils as o,\n PMREMGenerator as p,\n Vector2 as r,\n WebGLRenderer as s,\n PerspectiveCamera as t,\n PointLight as u,\n ACESFilmicToneMapping as v,\n Plane as w,\n Raycaster as y\n} from 'three';\nimport { RoomEnvironment as z } from 'three/examples/jsm/environments/RoomEnvironment.js';\n\nclass x {\n #e;\n canvas;\n camera;\n cameraMinAspect;\n cameraMaxAspect;\n cameraFov;\n maxPixelRatio;\n minPixelRatio;\n scene;\n renderer;\n #t;\n size = { width: 0, height: 0, wWidth: 0, wHeight: 0, ratio: 0, pixelRatio: 0 };\n render = this.#i;\n onBeforeRender = () => {};\n onAfterRender = () => {};\n onAfterResize = () => {};\n #s = false;\n #n = false;\n // Bind once: `.bind()` returns a new function on every call, so binding again\n // in the teardown would hand removeEventListener a function that was never\n // registered, leaving the listener attached for the lifetime of the page.\n #boundResize = this.#f.bind(this);\n #boundVisibilityChange = this.#v.bind(this);\n isDisposed = false;\n #o;\n #r;\n #a;\n #c = new e();\n #h = { elapsed: 0, delta: 0 };\n #l;\n constructor(e) {\n this.#e = { ...e };\n this.#m();\n this.#d();\n this.#p();\n this.resize();\n this.#g();\n }\n #m() {\n this.camera = new t();\n this.cameraFov = this.camera.fov;\n }\n #d() {\n this.scene = new i();\n }\n #p() {\n if (this.#e.canvas) {\n this.canvas = this.#e.canvas;\n } else if (this.#e.id) {\n this.canvas = document.getElementById(this.#e.id);\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas.style.display = 'block';\n const e = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#e.rendererOptions ?? {})\n };\n this.renderer = new s(e);\n this.renderer.outputColorSpace = n;\n }\n #g() {\n if (!(this.#e.size instanceof Object)) {\n window.addEventListener('resize', this.#boundResize);\n if (this.#e.size === 'parent' && this.canvas.parentNode) {\n this.#r = new ResizeObserver(this.#f.bind(this));\n this.#r.observe(this.canvas.parentNode);\n }\n }\n this.#o = new IntersectionObserver(this.#u.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#o.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#boundVisibilityChange);\n }\n #y() {\n window.removeEventListener('resize', this.#boundResize);\n this.#r?.disconnect();\n this.#o?.disconnect();\n document.removeEventListener('visibilitychange', this.#boundVisibilityChange);\n }\n #u(e) {\n this.#s = e[0].isIntersecting;\n this.#s ? this.#w() : this.#z();\n }\n #v() {\n if (this.#s) {\n document.hidden ? this.#z() : this.#w();\n }\n }\n #f() {\n if (this.#a) clearTimeout(this.#a);\n this.#a = setTimeout(this.resize.bind(this), 100);\n }\n resize() {\n let e, t;\n if (this.#e.size instanceof Object) {\n e = this.#e.size.width;\n t = this.#e.size.height;\n } else if (this.#e.size === 'parent' && this.canvas.parentNode) {\n e = this.canvas.parentNode.offsetWidth;\n t = this.canvas.parentNode.offsetHeight;\n } else {\n e = window.innerWidth;\n t = window.innerHeight;\n }\n this.size.width = e;\n this.size.height = t;\n this.size.ratio = e / t;\n this.#x();\n this.#b();\n this.onAfterResize(this.size);\n }\n #x() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#A(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#A(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n #A(e) {\n const t = Math.tan(o.degToRad(this.cameraFov / 2)) / (this.camera.aspect / e);\n this.camera.fov = 2 * o.radToDeg(Math.atan(t));\n }\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const e = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(e / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if (this.camera.isOrthographicCamera) {\n this.size.wHeight = this.camera.top - this.camera.bottom;\n this.size.wWidth = this.camera.right - this.camera.left;\n }\n }\n #b() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#t?.setSize(this.size.width, this.size.height);\n let e = window.devicePixelRatio;\n if (this.maxPixelRatio && e > this.maxPixelRatio) {\n e = this.maxPixelRatio;\n } else if (this.minPixelRatio && e < this.minPixelRatio) {\n e = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(e);\n this.size.pixelRatio = e;\n }\n get postprocessing() {\n return this.#t;\n }\n set postprocessing(e) {\n this.#t = e;\n this.render = e.render.bind(e);\n }\n #w() {\n if (this.#n) return;\n const animate = () => {\n this.#l = requestAnimationFrame(animate);\n this.#c.update();\n this.#h.delta = this.#c.getDelta();\n this.#h.elapsed += this.#h.delta;\n this.onBeforeRender(this.#h);\n this.render();\n this.onAfterRender(this.#h);\n };\n this.#n = true;\n this.#c.reset();\n animate();\n }\n #z() {\n if (this.#n) {\n cancelAnimationFrame(this.#l);\n this.#n = false;\n }\n }\n #i() {\n this.renderer.render(this.scene, this.camera);\n }\n clear() {\n this.scene.traverse(e => {\n if (e.isMesh && typeof e.material === 'object' && e.material !== null) {\n Object.keys(e.material).forEach(t => {\n const i = e.material[t];\n if (i !== null && typeof i === 'object' && typeof i.dispose === 'function') {\n i.dispose();\n }\n });\n e.material.dispose();\n e.geometry.dispose();\n }\n });\n this.scene.clear();\n }\n dispose() {\n this.#y();\n this.#z();\n this.#c.dispose();\n this.clear();\n this.#t?.dispose();\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n this.isDisposed = true;\n }\n}\n\nconst b = new Map(),\n A = new r();\nlet R = false;\nfunction S(e) {\n const t = {\n position: new r(),\n nPosition: new r(),\n hover: false,\n touching: false,\n onEnter() {},\n onMove() {},\n onClick() {},\n onLeave() {},\n ...e\n };\n (function (e, t) {\n if (!b.has(e)) {\n b.set(e, t);\n if (!R) {\n document.body.addEventListener('pointermove', M);\n document.body.addEventListener('pointerleave', L);\n document.body.addEventListener('click', C);\n\n document.body.addEventListener('touchstart', TouchStart, { passive: false });\n document.body.addEventListener('touchmove', TouchMove, { passive: false });\n document.body.addEventListener('touchend', TouchEnd, { passive: false });\n document.body.addEventListener('touchcancel', TouchEnd, { passive: false });\n\n R = true;\n }\n }\n })(e.domElement, t);\n t.dispose = () => {\n const t = e.domElement;\n b.delete(t);\n if (b.size === 0) {\n document.body.removeEventListener('pointermove', M);\n document.body.removeEventListener('pointerleave', L);\n document.body.removeEventListener('click', C);\n\n document.body.removeEventListener('touchstart', TouchStart);\n document.body.removeEventListener('touchmove', TouchMove);\n document.body.removeEventListener('touchend', TouchEnd);\n document.body.removeEventListener('touchcancel', TouchEnd);\n\n R = false;\n }\n };\n return t;\n}\n\nfunction M(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n processInteraction();\n}\n\nfunction processInteraction() {\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n if (D(i)) {\n P(t, i);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && !t.touching) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction C(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n P(t, i);\n if (D(i)) t.onClick(t);\n }\n}\n\nfunction L() {\n for (const t of b.values()) {\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction TouchStart(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n if (D(rect)) {\n t.touching = true;\n P(t, rect);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchMove(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n P(t, rect);\n\n if (D(rect)) {\n if (!t.hover) {\n t.hover = true;\n t.touching = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && t.touching) {\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchEnd() {\n for (const [, t] of b) {\n if (t.touching) {\n t.touching = false;\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n }\n}\n\nfunction P(e, t) {\n const { position: i, nPosition: s } = e;\n i.x = A.x - t.left;\n i.y = A.y - t.top;\n s.x = (i.x / t.width) * 2 - 1;\n s.y = (-i.y / t.height) * 2 + 1;\n}\nfunction D(e) {\n const { x: t, y: i } = A;\n const { left: s, top: n, width: o, height: r } = e;\n return t >= s && t <= s + o && i >= n && i <= n + r;\n}\n\nconst { randFloat: k, randFloatSpread: E } = o;\nconst F = new a();\nconst I = new a();\nconst O = new a();\nconst V = new a();\nconst B = new a();\nconst N = new a();\nconst _ = new a();\nconst j = new a();\nconst H = new a();\nconst T = new a();\n\nclass W {\n constructor(e) {\n this.config = e;\n this.positionData = new Float32Array(3 * e.count).fill(0);\n this.velocityData = new Float32Array(3 * e.count).fill(0);\n this.sizeData = new Float32Array(e.count).fill(1);\n this.center = new a();\n this.#R();\n this.setSizes();\n }\n #R() {\n const { config: e, positionData: t } = this;\n this.center.toArray(t, 0);\n for (let i = 1; i < e.count; i++) {\n const s = 3 * i;\n t[s] = E(2 * e.maxX);\n t[s + 1] = E(2 * e.maxY);\n t[s + 2] = E(2 * e.maxZ);\n }\n }\n setSizes() {\n const { config: e, sizeData: t } = this;\n t[0] = e.size0;\n for (let i = 1; i < e.count; i++) {\n t[i] = k(e.minSize, e.maxSize);\n }\n }\n update(e) {\n const { config: t, center: i, positionData: s, sizeData: n, velocityData: o } = this;\n let r = 0;\n if (t.controlSphere0) {\n r = 1;\n F.fromArray(s, 0);\n F.lerp(i, 0.1).toArray(s, 0);\n V.set(0, 0, 0).toArray(o, 0);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n B.y -= e.delta * t.gravity * n[idx];\n B.multiplyScalar(t.friction);\n B.clampLength(0, t.maxVelocity);\n I.add(B);\n I.toArray(s, base);\n B.toArray(o, base);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n const radius = n[idx];\n for (let jdx = idx + 1; jdx < t.count; jdx++) {\n const otherBase = 3 * jdx;\n O.fromArray(s, otherBase);\n N.fromArray(o, otherBase);\n const otherRadius = n[jdx];\n _.copy(O).sub(I);\n const dist = _.length();\n const sumRadius = radius + otherRadius;\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n j.copy(_)\n .normalize()\n .multiplyScalar(0.5 * overlap);\n H.copy(j).multiplyScalar(Math.max(B.length(), 1));\n T.copy(j).multiplyScalar(Math.max(N.length(), 1));\n I.sub(j);\n B.sub(H);\n I.toArray(s, base);\n B.toArray(o, base);\n O.add(j);\n N.add(T);\n O.toArray(s, otherBase);\n N.toArray(o, otherBase);\n }\n }\n if (t.controlSphere0) {\n _.copy(F).sub(I);\n const dist = _.length();\n const sumRadius0 = radius + n[0];\n if (dist < sumRadius0) {\n const diff = sumRadius0 - dist;\n j.copy(_.normalize()).multiplyScalar(diff);\n H.copy(j).multiplyScalar(Math.max(B.length(), 2));\n I.sub(j);\n B.sub(H);\n }\n }\n if (Math.abs(I.x) + radius > t.maxX) {\n I.x = Math.sign(I.x) * (t.maxX - radius);\n B.x = -B.x * t.wallBounce;\n }\n if (t.gravity === 0) {\n if (Math.abs(I.y) + radius > t.maxY) {\n I.y = Math.sign(I.y) * (t.maxY - radius);\n B.y = -B.y * t.wallBounce;\n }\n } else if (I.y - radius < -t.maxY) {\n I.y = -t.maxY + radius;\n B.y = -B.y * t.wallBounce;\n }\n const maxBoundary = Math.max(t.maxZ, t.maxSize);\n if (Math.abs(I.z) + radius > maxBoundary) {\n I.z = Math.sign(I.z) * (t.maxZ - radius);\n B.z = -B.z * t.wallBounce;\n }\n I.toArray(s, base);\n B.toArray(o, base);\n }\n }\n}\n\nclass Y extends c {\n constructor(e) {\n super(e);\n this.uniforms = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n this.defines.USE_UV = '';\n this.onBeforeCompile = e => {\n Object.assign(e.uniforms, this.uniforms);\n e.fragmentShader =\n '\\n uniform float thicknessPower;\\n uniform float thicknessScale;\\n uniform float thicknessDistortion;\\n uniform float thicknessAmbient;\\n uniform float thicknessAttenuation;\\n ' +\n e.fragmentShader;\n e.fragmentShader = e.fragmentShader.replace(\n 'void main() {',\n '\\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\\n #ifdef USE_COLOR\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\\n #else\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\\n #endif\\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\\n }\\n\\n void main() {\\n '\n );\n const t = h.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n '\\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\\n '\n );\n e.fragmentShader = e.fragmentShader.replace('#include ', t);\n if (this.onBeforeCompile2) this.onBeforeCompile2(e);\n };\n }\n}\n\nconst X = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 16777215,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new m();\n\nclass Z extends d {\n constructor(e, t = {}) {\n const i = { ...X, ...t };\n const s = new z();\n const n = new p(e, 0.04).fromScene(s).texture;\n const o = new g();\n const r = new Y({ envMap: n, ...i.materialParams });\n r.envMapRotation.x = -Math.PI / 2;\n super(o, r, i.count);\n this.config = i;\n this.physics = new W(i);\n this.#S();\n this.setColors(i.colors);\n }\n #S() {\n this.ambientLight = new f(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new u(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n setColors(e) {\n if (Array.isArray(e) && e.length > 1) {\n const t = (function (e) {\n let t, i;\n function setColors(e) {\n t = e;\n i = [];\n t.forEach(col => {\n i.push(new l(col));\n });\n }\n setColors(e);\n return {\n setColors,\n getColorAt: function (ratio, out = new l()) {\n const scaled = Math.max(0, Math.min(1, ratio)) * (t.length - 1);\n const idx = Math.floor(scaled);\n const start = i[idx];\n if (idx >= t.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = i[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(e);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, t.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light.color.copy(t.getColorAt(idx / this.count));\n }\n }\n this.instanceColor.needsUpdate = true;\n }\n }\n update(e) {\n this.physics.update(e);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\nfunction createBallpit(e, t = {}) {\n const i = new x({\n canvas: e,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let s;\n i.renderer.toneMapping = v;\n i.camera.position.set(0, 0, 20);\n i.camera.lookAt(0, 0, 0);\n i.cameraMaxAspect = 1.5;\n i.resize();\n initialize(t);\n const n = new y();\n const o = new w(new a(0, 0, 1), 0);\n const r = new a();\n let c = false;\n\n e.style.touchAction = 'none';\n e.style.userSelect = 'none';\n e.style.webkitUserSelect = 'none';\n\n const h = S({\n domElement: e,\n onMove() {\n n.setFromCamera(h.nPosition, i.camera);\n i.camera.getWorldDirection(o.normal);\n n.ray.intersectPlane(o, r);\n s.physics.center.copy(r);\n s.config.controlSphere0 = true;\n },\n onLeave() {\n s.config.controlSphere0 = false;\n }\n });\n function initialize(e) {\n if (s) {\n i.clear();\n i.scene.remove(s);\n }\n s = new Z(i.renderer, e);\n i.scene.add(s);\n }\n i.onBeforeRender = e => {\n if (!c) s.update(e);\n };\n i.onAfterResize = e => {\n s.config.maxX = e.wWidth / 2;\n s.config.maxY = e.wHeight / 2;\n };\n return {\n three: i,\n get spheres() {\n return s;\n },\n setCount(e) {\n initialize({ ...s.config, count: e });\n },\n updateConfig(newProps) {\n if (newProps.count !== undefined && newProps.count !== s.config.count) {\n initialize({ ...s.config, ...newProps });\n } else {\n Object.assign(s.config, newProps);\n if (newProps.colors) {\n s.setColors(s.config.colors);\n }\n if (newProps.minSize !== undefined || newProps.maxSize !== undefined || newProps.size0 !== undefined) {\n s.physics.setSizes();\n }\n }\n },\n togglePause() {\n c = !c;\n },\n dispose() {\n h.dispose();\n i.dispose();\n }\n };\n}\n\nconst Ballpit = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n const isFirstRender = useRef(true);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, { followCursor, ...props });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n spheresInstanceRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (isFirstRender.current) {\n isFirstRender.current = false;\n return;\n }\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.updateConfig({ followCursor, ...props });\n }\n }, [props, followCursor]);\n\n return ;\n};\n\nexport default Ballpit;\n" } ], "registryDependencies": [], diff --git a/public/r/Ballpit-JS-TW.json b/public/r/Ballpit-JS-TW.json index 894d7e5a..78291514 100644 --- a/public/r/Ballpit-JS-TW.json +++ b/public/r/Ballpit-JS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "Ballpit/Ballpit.jsx", - "content": "import { useEffect, useRef } from 'react';\nimport {\n Vector3 as a,\n MeshPhysicalMaterial as c,\n InstancedMesh as d,\n Timer as e,\n AmbientLight as f,\n SphereGeometry as g,\n ShaderChunk as h,\n Scene as i,\n Color as l,\n Object3D as m,\n SRGBColorSpace as n,\n MathUtils as o,\n PMREMGenerator as p,\n Vector2 as r,\n WebGLRenderer as s,\n PerspectiveCamera as t,\n PointLight as u,\n ACESFilmicToneMapping as v,\n Plane as w,\n Raycaster as y\n} from 'three';\nimport { RoomEnvironment as z } from 'three/examples/jsm/environments/RoomEnvironment.js';\n\nclass x {\n #e;\n canvas;\n camera;\n cameraMinAspect;\n cameraMaxAspect;\n cameraFov;\n maxPixelRatio;\n minPixelRatio;\n scene;\n renderer;\n #t;\n size = { width: 0, height: 0, wWidth: 0, wHeight: 0, ratio: 0, pixelRatio: 0 };\n render = this.#i;\n onBeforeRender = () => {};\n onAfterRender = () => {};\n onAfterResize = () => {};\n #s = false;\n #n = false;\n isDisposed = false;\n #o;\n #r;\n #a;\n #c = new e();\n #h = { elapsed: 0, delta: 0 };\n #l;\n constructor(e) {\n this.#e = { ...e };\n this.#m();\n this.#d();\n this.#p();\n this.resize();\n this.#g();\n }\n #m() {\n this.camera = new t();\n this.cameraFov = this.camera.fov;\n }\n #d() {\n this.scene = new i();\n }\n #p() {\n if (this.#e.canvas) {\n this.canvas = this.#e.canvas;\n } else if (this.#e.id) {\n this.canvas = document.getElementById(this.#e.id);\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas.style.display = 'block';\n const e = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#e.rendererOptions ?? {})\n };\n this.renderer = new s(e);\n this.renderer.outputColorSpace = n;\n }\n #g() {\n if (!(this.#e.size instanceof Object)) {\n window.addEventListener('resize', this.#f.bind(this));\n if (this.#e.size === 'parent' && this.canvas.parentNode) {\n this.#r = new ResizeObserver(this.#f.bind(this));\n this.#r.observe(this.canvas.parentNode);\n }\n }\n this.#o = new IntersectionObserver(this.#u.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#o.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#v.bind(this));\n }\n #y() {\n window.removeEventListener('resize', this.#f.bind(this));\n this.#r?.disconnect();\n this.#o?.disconnect();\n document.removeEventListener('visibilitychange', this.#v.bind(this));\n }\n #u(e) {\n this.#s = e[0].isIntersecting;\n this.#s ? this.#w() : this.#z();\n }\n #v() {\n if (this.#s) {\n document.hidden ? this.#z() : this.#w();\n }\n }\n #f() {\n if (this.#a) clearTimeout(this.#a);\n this.#a = setTimeout(this.resize.bind(this), 100);\n }\n resize() {\n let e, t;\n if (this.#e.size instanceof Object) {\n e = this.#e.size.width;\n t = this.#e.size.height;\n } else if (this.#e.size === 'parent' && this.canvas.parentNode) {\n e = this.canvas.parentNode.offsetWidth;\n t = this.canvas.parentNode.offsetHeight;\n } else {\n e = window.innerWidth;\n t = window.innerHeight;\n }\n this.size.width = e;\n this.size.height = t;\n this.size.ratio = e / t;\n this.#x();\n this.#b();\n this.onAfterResize(this.size);\n }\n #x() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#A(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#A(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n #A(e) {\n const t = Math.tan(o.degToRad(this.cameraFov / 2)) / (this.camera.aspect / e);\n this.camera.fov = 2 * o.radToDeg(Math.atan(t));\n }\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const e = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(e / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if (this.camera.isOrthographicCamera) {\n this.size.wHeight = this.camera.top - this.camera.bottom;\n this.size.wWidth = this.camera.right - this.camera.left;\n }\n }\n #b() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#t?.setSize(this.size.width, this.size.height);\n let e = window.devicePixelRatio;\n if (this.maxPixelRatio && e > this.maxPixelRatio) {\n e = this.maxPixelRatio;\n } else if (this.minPixelRatio && e < this.minPixelRatio) {\n e = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(e);\n this.size.pixelRatio = e;\n }\n get postprocessing() {\n return this.#t;\n }\n set postprocessing(e) {\n this.#t = e;\n this.render = e.render.bind(e);\n }\n #w() {\n if (this.#n) return;\n const animate = () => {\n this.#l = requestAnimationFrame(animate);\n this.#c.update();\n this.#h.delta = this.#c.getDelta();\n this.#h.elapsed += this.#h.delta;\n this.onBeforeRender(this.#h);\n this.render();\n this.onAfterRender(this.#h);\n };\n this.#n = true;\n this.#c.reset();\n animate();\n }\n #z() {\n if (this.#n) {\n cancelAnimationFrame(this.#l);\n this.#n = false;\n }\n }\n #i() {\n this.renderer.render(this.scene, this.camera);\n }\n clear() {\n this.scene.traverse(e => {\n if (e.isMesh && typeof e.material === 'object' && e.material !== null) {\n Object.keys(e.material).forEach(t => {\n const i = e.material[t];\n if (i !== null && typeof i === 'object' && typeof i.dispose === 'function') {\n i.dispose();\n }\n });\n e.material.dispose();\n e.geometry.dispose();\n }\n });\n this.scene.clear();\n }\n dispose() {\n this.#y();\n this.#z();\n this.#c.dispose();\n this.clear();\n this.#t?.dispose();\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n this.isDisposed = true;\n }\n}\n\nconst b = new Map(),\n A = new r();\nlet R = false;\nfunction S(e) {\n const t = {\n position: new r(),\n nPosition: new r(),\n hover: false,\n touching: false,\n onEnter() {},\n onMove() {},\n onClick() {},\n onLeave() {},\n ...e\n };\n (function (e, t) {\n if (!b.has(e)) {\n b.set(e, t);\n if (!R) {\n document.body.addEventListener('pointermove', M);\n document.body.addEventListener('pointerleave', L);\n document.body.addEventListener('click', C);\n\n document.body.addEventListener('touchstart', TouchStart, { passive: false });\n document.body.addEventListener('touchmove', TouchMove, { passive: false });\n document.body.addEventListener('touchend', TouchEnd, { passive: false });\n document.body.addEventListener('touchcancel', TouchEnd, { passive: false });\n\n R = true;\n }\n }\n })(e.domElement, t);\n t.dispose = () => {\n const t = e.domElement;\n b.delete(t);\n if (b.size === 0) {\n document.body.removeEventListener('pointermove', M);\n document.body.removeEventListener('pointerleave', L);\n document.body.removeEventListener('click', C);\n\n document.body.removeEventListener('touchstart', TouchStart);\n document.body.removeEventListener('touchmove', TouchMove);\n document.body.removeEventListener('touchend', TouchEnd);\n document.body.removeEventListener('touchcancel', TouchEnd);\n\n R = false;\n }\n };\n return t;\n}\n\nfunction M(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n processInteraction();\n}\n\nfunction processInteraction() {\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n if (D(i)) {\n P(t, i);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && !t.touching) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction C(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n P(t, i);\n if (D(i)) t.onClick(t);\n }\n}\n\nfunction L() {\n for (const t of b.values()) {\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction TouchStart(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n if (D(rect)) {\n t.touching = true;\n P(t, rect);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchMove(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n P(t, rect);\n\n if (D(rect)) {\n if (!t.hover) {\n t.hover = true;\n t.touching = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && t.touching) {\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchEnd() {\n for (const [, t] of b) {\n if (t.touching) {\n t.touching = false;\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n }\n}\n\nfunction P(e, t) {\n const { position: i, nPosition: s } = e;\n i.x = A.x - t.left;\n i.y = A.y - t.top;\n s.x = (i.x / t.width) * 2 - 1;\n s.y = (-i.y / t.height) * 2 + 1;\n}\nfunction D(e) {\n const { x: t, y: i } = A;\n const { left: s, top: n, width: o, height: r } = e;\n return t >= s && t <= s + o && i >= n && i <= n + r;\n}\n\nconst { randFloat: k, randFloatSpread: E } = o;\nconst F = new a();\nconst I = new a();\nconst O = new a();\nconst V = new a();\nconst B = new a();\nconst N = new a();\nconst _ = new a();\nconst j = new a();\nconst H = new a();\nconst T = new a();\n\nclass W {\n constructor(e) {\n this.config = e;\n this.positionData = new Float32Array(3 * e.count).fill(0);\n this.velocityData = new Float32Array(3 * e.count).fill(0);\n this.sizeData = new Float32Array(e.count).fill(1);\n this.center = new a();\n this.#R();\n this.setSizes();\n }\n #R() {\n const { config: e, positionData: t } = this;\n this.center.toArray(t, 0);\n for (let i = 1; i < e.count; i++) {\n const s = 3 * i;\n t[s] = E(2 * e.maxX);\n t[s + 1] = E(2 * e.maxY);\n t[s + 2] = E(2 * e.maxZ);\n }\n }\n setSizes() {\n const { config: e, sizeData: t } = this;\n t[0] = e.size0;\n for (let i = 1; i < e.count; i++) {\n t[i] = k(e.minSize, e.maxSize);\n }\n }\n update(e) {\n const { config: t, center: i, positionData: s, sizeData: n, velocityData: o } = this;\n let r = 0;\n if (t.controlSphere0) {\n r = 1;\n F.fromArray(s, 0);\n F.lerp(i, 0.1).toArray(s, 0);\n V.set(0, 0, 0).toArray(o, 0);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n B.y -= e.delta * t.gravity * n[idx];\n B.multiplyScalar(t.friction);\n B.clampLength(0, t.maxVelocity);\n I.add(B);\n I.toArray(s, base);\n B.toArray(o, base);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n const radius = n[idx];\n for (let jdx = idx + 1; jdx < t.count; jdx++) {\n const otherBase = 3 * jdx;\n O.fromArray(s, otherBase);\n N.fromArray(o, otherBase);\n const otherRadius = n[jdx];\n _.copy(O).sub(I);\n const dist = _.length();\n const sumRadius = radius + otherRadius;\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n j.copy(_)\n .normalize()\n .multiplyScalar(0.5 * overlap);\n H.copy(j).multiplyScalar(Math.max(B.length(), 1));\n T.copy(j).multiplyScalar(Math.max(N.length(), 1));\n I.sub(j);\n B.sub(H);\n I.toArray(s, base);\n B.toArray(o, base);\n O.add(j);\n N.add(T);\n O.toArray(s, otherBase);\n N.toArray(o, otherBase);\n }\n }\n if (t.controlSphere0) {\n _.copy(F).sub(I);\n const dist = _.length();\n const sumRadius0 = radius + n[0];\n if (dist < sumRadius0) {\n const diff = sumRadius0 - dist;\n j.copy(_.normalize()).multiplyScalar(diff);\n H.copy(j).multiplyScalar(Math.max(B.length(), 2));\n I.sub(j);\n B.sub(H);\n }\n }\n if (Math.abs(I.x) + radius > t.maxX) {\n I.x = Math.sign(I.x) * (t.maxX - radius);\n B.x = -B.x * t.wallBounce;\n }\n if (t.gravity === 0) {\n if (Math.abs(I.y) + radius > t.maxY) {\n I.y = Math.sign(I.y) * (t.maxY - radius);\n B.y = -B.y * t.wallBounce;\n }\n } else if (I.y - radius < -t.maxY) {\n I.y = -t.maxY + radius;\n B.y = -B.y * t.wallBounce;\n }\n const maxBoundary = Math.max(t.maxZ, t.maxSize);\n if (Math.abs(I.z) + radius > maxBoundary) {\n I.z = Math.sign(I.z) * (t.maxZ - radius);\n B.z = -B.z * t.wallBounce;\n }\n I.toArray(s, base);\n B.toArray(o, base);\n }\n }\n}\n\nclass Y extends c {\n constructor(e) {\n super(e);\n this.uniforms = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n this.defines.USE_UV = '';\n this.onBeforeCompile = e => {\n Object.assign(e.uniforms, this.uniforms);\n e.fragmentShader =\n '\\n uniform float thicknessPower;\\n uniform float thicknessScale;\\n uniform float thicknessDistortion;\\n uniform float thicknessAmbient;\\n uniform float thicknessAttenuation;\\n ' +\n e.fragmentShader;\n e.fragmentShader = e.fragmentShader.replace(\n 'void main() {',\n '\\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\\n #ifdef USE_COLOR\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\\n #else\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\\n #endif\\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\\n }\\n\\n void main() {\\n '\n );\n const t = h.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n '\\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\\n '\n );\n e.fragmentShader = e.fragmentShader.replace('#include ', t);\n if (this.onBeforeCompile2) this.onBeforeCompile2(e);\n };\n }\n}\n\nconst X = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 16777215,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new m();\n\nclass Z extends d {\n constructor(e, t = {}) {\n const i = { ...X, ...t };\n const s = new z();\n const n = new p(e, 0.04).fromScene(s).texture;\n const o = new g();\n const r = new Y({ envMap: n, ...i.materialParams });\n r.envMapRotation.x = -Math.PI / 2;\n super(o, r, i.count);\n this.config = i;\n this.physics = new W(i);\n this.#S();\n this.setColors(i.colors);\n }\n #S() {\n this.ambientLight = new f(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new u(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n setColors(e) {\n if (Array.isArray(e) && e.length > 1) {\n const t = (function (e) {\n let t, i;\n function setColors(e) {\n t = e;\n i = [];\n t.forEach(col => {\n i.push(new l(col));\n });\n }\n setColors(e);\n return {\n setColors,\n getColorAt: function (ratio, out = new l()) {\n const scaled = Math.max(0, Math.min(1, ratio)) * (t.length - 1);\n const idx = Math.floor(scaled);\n const start = i[idx];\n if (idx >= t.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = i[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(e);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, t.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light.color.copy(t.getColorAt(idx / this.count));\n }\n }\n this.instanceColor.needsUpdate = true;\n }\n }\n update(e) {\n this.physics.update(e);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\nfunction createBallpit(e, t = {}) {\n const i = new x({\n canvas: e,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let s;\n i.renderer.toneMapping = v;\n i.camera.position.set(0, 0, 20);\n i.camera.lookAt(0, 0, 0);\n i.cameraMaxAspect = 1.5;\n i.resize();\n initialize(t);\n const n = new y();\n const o = new w(new a(0, 0, 1), 0);\n const r = new a();\n let c = false;\n\n e.style.touchAction = 'none';\n e.style.userSelect = 'none';\n e.style.webkitUserSelect = 'none';\n\n const h = S({\n domElement: e,\n onMove() {\n n.setFromCamera(h.nPosition, i.camera);\n i.camera.getWorldDirection(o.normal);\n n.ray.intersectPlane(o, r);\n s.physics.center.copy(r);\n s.config.controlSphere0 = true;\n },\n onLeave() {\n s.config.controlSphere0 = false;\n }\n });\n function initialize(e) {\n if (s) {\n i.clear();\n i.scene.remove(s);\n }\n s = new Z(i.renderer, e);\n i.scene.add(s);\n }\n i.onBeforeRender = e => {\n if (!c) s.update(e);\n };\n i.onAfterResize = e => {\n s.config.maxX = e.wWidth / 2;\n s.config.maxY = e.wHeight / 2;\n };\n return {\n three: i,\n get spheres() {\n return s;\n },\n setCount(e) {\n initialize({ ...s.config, count: e });\n },\n updateConfig(newProps) {\n if (newProps.count !== undefined && newProps.count !== s.config.count) {\n initialize({ ...s.config, ...newProps });\n } else {\n Object.assign(s.config, newProps);\n if (newProps.colors) {\n s.setColors(s.config.colors);\n }\n if (newProps.minSize !== undefined || newProps.maxSize !== undefined || newProps.size0 !== undefined) {\n s.physics.setSizes();\n }\n }\n },\n togglePause() {\n c = !c;\n },\n dispose() {\n h.dispose();\n i.dispose();\n }\n };\n}\n\nconst Ballpit = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n const isFirstRender = useRef(true);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, { followCursor, ...props });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n spheresInstanceRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (isFirstRender.current) {\n isFirstRender.current = false;\n return;\n }\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.updateConfig({ followCursor, ...props });\n }\n }, [props, followCursor]);\n\n return ;\n};\n\nexport default Ballpit;\n" + "content": "import { useEffect, useRef } from 'react';\nimport {\n Vector3 as a,\n MeshPhysicalMaterial as c,\n InstancedMesh as d,\n Timer as e,\n AmbientLight as f,\n SphereGeometry as g,\n ShaderChunk as h,\n Scene as i,\n Color as l,\n Object3D as m,\n SRGBColorSpace as n,\n MathUtils as o,\n PMREMGenerator as p,\n Vector2 as r,\n WebGLRenderer as s,\n PerspectiveCamera as t,\n PointLight as u,\n ACESFilmicToneMapping as v,\n Plane as w,\n Raycaster as y\n} from 'three';\nimport { RoomEnvironment as z } from 'three/examples/jsm/environments/RoomEnvironment.js';\n\nclass x {\n #e;\n canvas;\n camera;\n cameraMinAspect;\n cameraMaxAspect;\n cameraFov;\n maxPixelRatio;\n minPixelRatio;\n scene;\n renderer;\n #t;\n size = { width: 0, height: 0, wWidth: 0, wHeight: 0, ratio: 0, pixelRatio: 0 };\n render = this.#i;\n onBeforeRender = () => {};\n onAfterRender = () => {};\n onAfterResize = () => {};\n #s = false;\n #n = false;\n // Bind once: `.bind()` returns a new function on every call, so binding again\n // in the teardown would hand removeEventListener a function that was never\n // registered, leaving the listener attached for the lifetime of the page.\n #boundResize = this.#f.bind(this);\n #boundVisibilityChange = this.#v.bind(this);\n isDisposed = false;\n #o;\n #r;\n #a;\n #c = new e();\n #h = { elapsed: 0, delta: 0 };\n #l;\n constructor(e) {\n this.#e = { ...e };\n this.#m();\n this.#d();\n this.#p();\n this.resize();\n this.#g();\n }\n #m() {\n this.camera = new t();\n this.cameraFov = this.camera.fov;\n }\n #d() {\n this.scene = new i();\n }\n #p() {\n if (this.#e.canvas) {\n this.canvas = this.#e.canvas;\n } else if (this.#e.id) {\n this.canvas = document.getElementById(this.#e.id);\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas.style.display = 'block';\n const e = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#e.rendererOptions ?? {})\n };\n this.renderer = new s(e);\n this.renderer.outputColorSpace = n;\n }\n #g() {\n if (!(this.#e.size instanceof Object)) {\n window.addEventListener('resize', this.#boundResize);\n if (this.#e.size === 'parent' && this.canvas.parentNode) {\n this.#r = new ResizeObserver(this.#f.bind(this));\n this.#r.observe(this.canvas.parentNode);\n }\n }\n this.#o = new IntersectionObserver(this.#u.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#o.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#boundVisibilityChange);\n }\n #y() {\n window.removeEventListener('resize', this.#boundResize);\n this.#r?.disconnect();\n this.#o?.disconnect();\n document.removeEventListener('visibilitychange', this.#boundVisibilityChange);\n }\n #u(e) {\n this.#s = e[0].isIntersecting;\n this.#s ? this.#w() : this.#z();\n }\n #v() {\n if (this.#s) {\n document.hidden ? this.#z() : this.#w();\n }\n }\n #f() {\n if (this.#a) clearTimeout(this.#a);\n this.#a = setTimeout(this.resize.bind(this), 100);\n }\n resize() {\n let e, t;\n if (this.#e.size instanceof Object) {\n e = this.#e.size.width;\n t = this.#e.size.height;\n } else if (this.#e.size === 'parent' && this.canvas.parentNode) {\n e = this.canvas.parentNode.offsetWidth;\n t = this.canvas.parentNode.offsetHeight;\n } else {\n e = window.innerWidth;\n t = window.innerHeight;\n }\n this.size.width = e;\n this.size.height = t;\n this.size.ratio = e / t;\n this.#x();\n this.#b();\n this.onAfterResize(this.size);\n }\n #x() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#A(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#A(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n #A(e) {\n const t = Math.tan(o.degToRad(this.cameraFov / 2)) / (this.camera.aspect / e);\n this.camera.fov = 2 * o.radToDeg(Math.atan(t));\n }\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const e = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(e / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if (this.camera.isOrthographicCamera) {\n this.size.wHeight = this.camera.top - this.camera.bottom;\n this.size.wWidth = this.camera.right - this.camera.left;\n }\n }\n #b() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#t?.setSize(this.size.width, this.size.height);\n let e = window.devicePixelRatio;\n if (this.maxPixelRatio && e > this.maxPixelRatio) {\n e = this.maxPixelRatio;\n } else if (this.minPixelRatio && e < this.minPixelRatio) {\n e = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(e);\n this.size.pixelRatio = e;\n }\n get postprocessing() {\n return this.#t;\n }\n set postprocessing(e) {\n this.#t = e;\n this.render = e.render.bind(e);\n }\n #w() {\n if (this.#n) return;\n const animate = () => {\n this.#l = requestAnimationFrame(animate);\n this.#c.update();\n this.#h.delta = this.#c.getDelta();\n this.#h.elapsed += this.#h.delta;\n this.onBeforeRender(this.#h);\n this.render();\n this.onAfterRender(this.#h);\n };\n this.#n = true;\n this.#c.reset();\n animate();\n }\n #z() {\n if (this.#n) {\n cancelAnimationFrame(this.#l);\n this.#n = false;\n }\n }\n #i() {\n this.renderer.render(this.scene, this.camera);\n }\n clear() {\n this.scene.traverse(e => {\n if (e.isMesh && typeof e.material === 'object' && e.material !== null) {\n Object.keys(e.material).forEach(t => {\n const i = e.material[t];\n if (i !== null && typeof i === 'object' && typeof i.dispose === 'function') {\n i.dispose();\n }\n });\n e.material.dispose();\n e.geometry.dispose();\n }\n });\n this.scene.clear();\n }\n dispose() {\n this.#y();\n this.#z();\n this.#c.dispose();\n this.clear();\n this.#t?.dispose();\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n this.isDisposed = true;\n }\n}\n\nconst b = new Map(),\n A = new r();\nlet R = false;\nfunction S(e) {\n const t = {\n position: new r(),\n nPosition: new r(),\n hover: false,\n touching: false,\n onEnter() {},\n onMove() {},\n onClick() {},\n onLeave() {},\n ...e\n };\n (function (e, t) {\n if (!b.has(e)) {\n b.set(e, t);\n if (!R) {\n document.body.addEventListener('pointermove', M);\n document.body.addEventListener('pointerleave', L);\n document.body.addEventListener('click', C);\n\n document.body.addEventListener('touchstart', TouchStart, { passive: false });\n document.body.addEventListener('touchmove', TouchMove, { passive: false });\n document.body.addEventListener('touchend', TouchEnd, { passive: false });\n document.body.addEventListener('touchcancel', TouchEnd, { passive: false });\n\n R = true;\n }\n }\n })(e.domElement, t);\n t.dispose = () => {\n const t = e.domElement;\n b.delete(t);\n if (b.size === 0) {\n document.body.removeEventListener('pointermove', M);\n document.body.removeEventListener('pointerleave', L);\n document.body.removeEventListener('click', C);\n\n document.body.removeEventListener('touchstart', TouchStart);\n document.body.removeEventListener('touchmove', TouchMove);\n document.body.removeEventListener('touchend', TouchEnd);\n document.body.removeEventListener('touchcancel', TouchEnd);\n\n R = false;\n }\n };\n return t;\n}\n\nfunction M(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n processInteraction();\n}\n\nfunction processInteraction() {\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n if (D(i)) {\n P(t, i);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && !t.touching) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction C(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n P(t, i);\n if (D(i)) t.onClick(t);\n }\n}\n\nfunction L() {\n for (const t of b.values()) {\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction TouchStart(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n if (D(rect)) {\n t.touching = true;\n P(t, rect);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchMove(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n P(t, rect);\n\n if (D(rect)) {\n if (!t.hover) {\n t.hover = true;\n t.touching = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && t.touching) {\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchEnd() {\n for (const [, t] of b) {\n if (t.touching) {\n t.touching = false;\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n }\n}\n\nfunction P(e, t) {\n const { position: i, nPosition: s } = e;\n i.x = A.x - t.left;\n i.y = A.y - t.top;\n s.x = (i.x / t.width) * 2 - 1;\n s.y = (-i.y / t.height) * 2 + 1;\n}\nfunction D(e) {\n const { x: t, y: i } = A;\n const { left: s, top: n, width: o, height: r } = e;\n return t >= s && t <= s + o && i >= n && i <= n + r;\n}\n\nconst { randFloat: k, randFloatSpread: E } = o;\nconst F = new a();\nconst I = new a();\nconst O = new a();\nconst V = new a();\nconst B = new a();\nconst N = new a();\nconst _ = new a();\nconst j = new a();\nconst H = new a();\nconst T = new a();\n\nclass W {\n constructor(e) {\n this.config = e;\n this.positionData = new Float32Array(3 * e.count).fill(0);\n this.velocityData = new Float32Array(3 * e.count).fill(0);\n this.sizeData = new Float32Array(e.count).fill(1);\n this.center = new a();\n this.#R();\n this.setSizes();\n }\n #R() {\n const { config: e, positionData: t } = this;\n this.center.toArray(t, 0);\n for (let i = 1; i < e.count; i++) {\n const s = 3 * i;\n t[s] = E(2 * e.maxX);\n t[s + 1] = E(2 * e.maxY);\n t[s + 2] = E(2 * e.maxZ);\n }\n }\n setSizes() {\n const { config: e, sizeData: t } = this;\n t[0] = e.size0;\n for (let i = 1; i < e.count; i++) {\n t[i] = k(e.minSize, e.maxSize);\n }\n }\n update(e) {\n const { config: t, center: i, positionData: s, sizeData: n, velocityData: o } = this;\n let r = 0;\n if (t.controlSphere0) {\n r = 1;\n F.fromArray(s, 0);\n F.lerp(i, 0.1).toArray(s, 0);\n V.set(0, 0, 0).toArray(o, 0);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n B.y -= e.delta * t.gravity * n[idx];\n B.multiplyScalar(t.friction);\n B.clampLength(0, t.maxVelocity);\n I.add(B);\n I.toArray(s, base);\n B.toArray(o, base);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n const radius = n[idx];\n for (let jdx = idx + 1; jdx < t.count; jdx++) {\n const otherBase = 3 * jdx;\n O.fromArray(s, otherBase);\n N.fromArray(o, otherBase);\n const otherRadius = n[jdx];\n _.copy(O).sub(I);\n const dist = _.length();\n const sumRadius = radius + otherRadius;\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n j.copy(_)\n .normalize()\n .multiplyScalar(0.5 * overlap);\n H.copy(j).multiplyScalar(Math.max(B.length(), 1));\n T.copy(j).multiplyScalar(Math.max(N.length(), 1));\n I.sub(j);\n B.sub(H);\n I.toArray(s, base);\n B.toArray(o, base);\n O.add(j);\n N.add(T);\n O.toArray(s, otherBase);\n N.toArray(o, otherBase);\n }\n }\n if (t.controlSphere0) {\n _.copy(F).sub(I);\n const dist = _.length();\n const sumRadius0 = radius + n[0];\n if (dist < sumRadius0) {\n const diff = sumRadius0 - dist;\n j.copy(_.normalize()).multiplyScalar(diff);\n H.copy(j).multiplyScalar(Math.max(B.length(), 2));\n I.sub(j);\n B.sub(H);\n }\n }\n if (Math.abs(I.x) + radius > t.maxX) {\n I.x = Math.sign(I.x) * (t.maxX - radius);\n B.x = -B.x * t.wallBounce;\n }\n if (t.gravity === 0) {\n if (Math.abs(I.y) + radius > t.maxY) {\n I.y = Math.sign(I.y) * (t.maxY - radius);\n B.y = -B.y * t.wallBounce;\n }\n } else if (I.y - radius < -t.maxY) {\n I.y = -t.maxY + radius;\n B.y = -B.y * t.wallBounce;\n }\n const maxBoundary = Math.max(t.maxZ, t.maxSize);\n if (Math.abs(I.z) + radius > maxBoundary) {\n I.z = Math.sign(I.z) * (t.maxZ - radius);\n B.z = -B.z * t.wallBounce;\n }\n I.toArray(s, base);\n B.toArray(o, base);\n }\n }\n}\n\nclass Y extends c {\n constructor(e) {\n super(e);\n this.uniforms = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n this.defines.USE_UV = '';\n this.onBeforeCompile = e => {\n Object.assign(e.uniforms, this.uniforms);\n e.fragmentShader =\n '\\n uniform float thicknessPower;\\n uniform float thicknessScale;\\n uniform float thicknessDistortion;\\n uniform float thicknessAmbient;\\n uniform float thicknessAttenuation;\\n ' +\n e.fragmentShader;\n e.fragmentShader = e.fragmentShader.replace(\n 'void main() {',\n '\\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\\n #ifdef USE_COLOR\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\\n #else\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\\n #endif\\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\\n }\\n\\n void main() {\\n '\n );\n const t = h.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n '\\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\\n '\n );\n e.fragmentShader = e.fragmentShader.replace('#include ', t);\n if (this.onBeforeCompile2) this.onBeforeCompile2(e);\n };\n }\n}\n\nconst X = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 16777215,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new m();\n\nclass Z extends d {\n constructor(e, t = {}) {\n const i = { ...X, ...t };\n const s = new z();\n const n = new p(e, 0.04).fromScene(s).texture;\n const o = new g();\n const r = new Y({ envMap: n, ...i.materialParams });\n r.envMapRotation.x = -Math.PI / 2;\n super(o, r, i.count);\n this.config = i;\n this.physics = new W(i);\n this.#S();\n this.setColors(i.colors);\n }\n #S() {\n this.ambientLight = new f(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new u(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n setColors(e) {\n if (Array.isArray(e) && e.length > 1) {\n const t = (function (e) {\n let t, i;\n function setColors(e) {\n t = e;\n i = [];\n t.forEach(col => {\n i.push(new l(col));\n });\n }\n setColors(e);\n return {\n setColors,\n getColorAt: function (ratio, out = new l()) {\n const scaled = Math.max(0, Math.min(1, ratio)) * (t.length - 1);\n const idx = Math.floor(scaled);\n const start = i[idx];\n if (idx >= t.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = i[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(e);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, t.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light.color.copy(t.getColorAt(idx / this.count));\n }\n }\n this.instanceColor.needsUpdate = true;\n }\n }\n update(e) {\n this.physics.update(e);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\nfunction createBallpit(e, t = {}) {\n const i = new x({\n canvas: e,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let s;\n i.renderer.toneMapping = v;\n i.camera.position.set(0, 0, 20);\n i.camera.lookAt(0, 0, 0);\n i.cameraMaxAspect = 1.5;\n i.resize();\n initialize(t);\n const n = new y();\n const o = new w(new a(0, 0, 1), 0);\n const r = new a();\n let c = false;\n\n e.style.touchAction = 'none';\n e.style.userSelect = 'none';\n e.style.webkitUserSelect = 'none';\n\n const h = S({\n domElement: e,\n onMove() {\n n.setFromCamera(h.nPosition, i.camera);\n i.camera.getWorldDirection(o.normal);\n n.ray.intersectPlane(o, r);\n s.physics.center.copy(r);\n s.config.controlSphere0 = true;\n },\n onLeave() {\n s.config.controlSphere0 = false;\n }\n });\n function initialize(e) {\n if (s) {\n i.clear();\n i.scene.remove(s);\n }\n s = new Z(i.renderer, e);\n i.scene.add(s);\n }\n i.onBeforeRender = e => {\n if (!c) s.update(e);\n };\n i.onAfterResize = e => {\n s.config.maxX = e.wWidth / 2;\n s.config.maxY = e.wHeight / 2;\n };\n return {\n three: i,\n get spheres() {\n return s;\n },\n setCount(e) {\n initialize({ ...s.config, count: e });\n },\n updateConfig(newProps) {\n if (newProps.count !== undefined && newProps.count !== s.config.count) {\n initialize({ ...s.config, ...newProps });\n } else {\n Object.assign(s.config, newProps);\n if (newProps.colors) {\n s.setColors(s.config.colors);\n }\n if (newProps.minSize !== undefined || newProps.maxSize !== undefined || newProps.size0 !== undefined) {\n s.physics.setSizes();\n }\n }\n },\n togglePause() {\n c = !c;\n },\n dispose() {\n h.dispose();\n i.dispose();\n }\n };\n}\n\nconst Ballpit = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n const isFirstRender = useRef(true);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, { followCursor, ...props });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n spheresInstanceRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (isFirstRender.current) {\n isFirstRender.current = false;\n return;\n }\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.updateConfig({ followCursor, ...props });\n }\n }, [props, followCursor]);\n\n return ;\n};\n\nexport default Ballpit;\n" } ], "registryDependencies": [], diff --git a/public/r/Ballpit-TS-CSS.json b/public/r/Ballpit-TS-CSS.json index e839042c..b8ca7849 100644 --- a/public/r/Ballpit-TS-CSS.json +++ b/public/r/Ballpit-TS-CSS.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "Ballpit/Ballpit.tsx", - "content": "import { gsap } from 'gsap';\nimport { Observer } from 'gsap/Observer';\nimport React, { useEffect, useRef } from 'react';\nimport {\n ACESFilmicToneMapping,\n AmbientLight,\n Color,\n InstancedMesh,\n MathUtils,\n MeshPhysicalMaterial,\n Object3D,\n PerspectiveCamera,\n Plane,\n PMREMGenerator,\n PointLight,\n Raycaster,\n Scene,\n ShaderChunk,\n SphereGeometry,\n SRGBColorSpace,\n Timer,\n Vector2,\n Vector3,\n WebGLRenderer,\n WebGLRendererParameters\n} from 'three';\nimport { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';\n\ngsap.registerPlugin(Observer);\n\ninterface XConfig {\n canvas?: HTMLCanvasElement;\n id?: string;\n rendererOptions?: Partial;\n size?: 'parent' | { width: number; height: number };\n}\n\ninterface SizeData {\n width: number;\n height: number;\n wWidth: number;\n wHeight: number;\n ratio: number;\n pixelRatio: number;\n}\n\nclass X {\n #config: XConfig;\n #postprocessing: any;\n #resizeObserver?: ResizeObserver;\n #intersectionObserver?: IntersectionObserver;\n #resizeTimer?: number;\n #animationFrameId: number = 0;\n #timer: Timer = new Timer();\n #animationState = { elapsed: 0, delta: 0 };\n #isAnimating: boolean = false;\n #isVisible: boolean = false;\n\n canvas!: HTMLCanvasElement;\n camera!: PerspectiveCamera;\n cameraMinAspect?: number;\n cameraMaxAspect?: number;\n cameraFov!: number;\n maxPixelRatio?: number;\n minPixelRatio?: number;\n scene!: Scene;\n renderer!: WebGLRenderer;\n size: SizeData = {\n width: 0,\n height: 0,\n wWidth: 0,\n wHeight: 0,\n ratio: 0,\n pixelRatio: 0\n };\n\n render: () => void = this.#render.bind(this);\n onBeforeRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterResize: (size: SizeData) => void = () => {};\n isDisposed: boolean = false;\n\n constructor(config: XConfig) {\n this.#config = { ...config };\n this.#initCamera();\n this.#initScene();\n this.#initRenderer();\n this.resize();\n this.#initObservers();\n }\n\n #initCamera() {\n this.camera = new PerspectiveCamera();\n this.cameraFov = this.camera.fov;\n }\n\n #initScene() {\n this.scene = new Scene();\n }\n\n #initRenderer() {\n if (this.#config.canvas) {\n this.canvas = this.#config.canvas;\n } else if (this.#config.id) {\n const elem = document.getElementById(this.#config.id);\n if (elem instanceof HTMLCanvasElement) {\n this.canvas = elem;\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas!.style.display = 'block';\n const rendererOptions: WebGLRendererParameters = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#config.rendererOptions ?? {})\n };\n this.renderer = new WebGLRenderer(rendererOptions);\n this.renderer.outputColorSpace = SRGBColorSpace;\n }\n\n #initObservers() {\n if (!(this.#config.size instanceof Object)) {\n window.addEventListener('resize', this.#onResize.bind(this));\n if (this.#config.size === 'parent' && this.canvas.parentNode) {\n this.#resizeObserver = new ResizeObserver(this.#onResize.bind(this));\n this.#resizeObserver.observe(this.canvas.parentNode as Element);\n }\n }\n this.#intersectionObserver = new IntersectionObserver(this.#onIntersection.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#intersectionObserver.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#onVisibilityChange.bind(this));\n }\n\n #onResize() {\n if (this.#resizeTimer) clearTimeout(this.#resizeTimer);\n this.#resizeTimer = window.setTimeout(this.resize.bind(this), 100);\n }\n\n resize() {\n let w: number, h: number;\n if (this.#config.size instanceof Object) {\n w = this.#config.size.width;\n h = this.#config.size.height;\n } else if (this.#config.size === 'parent' && this.canvas.parentNode) {\n w = (this.canvas.parentNode as HTMLElement).offsetWidth;\n h = (this.canvas.parentNode as HTMLElement).offsetHeight;\n } else {\n w = window.innerWidth;\n h = window.innerHeight;\n }\n this.size.width = w;\n this.size.height = h;\n this.size.ratio = w / h;\n this.#updateCamera();\n this.#updateRenderer();\n this.onAfterResize(this.size);\n }\n\n #updateCamera() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#adjustFov(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#adjustFov(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n\n #adjustFov(aspect: number) {\n const tanFov = Math.tan(MathUtils.degToRad(this.cameraFov / 2));\n const newTan = tanFov / (this.camera.aspect / aspect);\n this.camera.fov = 2 * MathUtils.radToDeg(Math.atan(newTan));\n }\n\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const fovRad = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(fovRad / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if ((this.camera as any).isOrthographicCamera) {\n const cam = this.camera as any;\n this.size.wHeight = cam.top - cam.bottom;\n this.size.wWidth = cam.right - cam.left;\n }\n }\n\n #updateRenderer() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#postprocessing?.setSize(this.size.width, this.size.height);\n let pr = window.devicePixelRatio;\n if (this.maxPixelRatio && pr > this.maxPixelRatio) {\n pr = this.maxPixelRatio;\n } else if (this.minPixelRatio && pr < this.minPixelRatio) {\n pr = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(pr);\n this.size.pixelRatio = pr;\n }\n\n get postprocessing() {\n return this.#postprocessing;\n }\n set postprocessing(value: any) {\n this.#postprocessing = value;\n this.render = value.render.bind(value);\n }\n\n #onIntersection(entries: IntersectionObserverEntry[]) {\n this.#isAnimating = entries[0].isIntersecting;\n this.#isAnimating ? this.#startAnimation() : this.#stopAnimation();\n }\n\n #onVisibilityChange() {\n if (this.#isAnimating) {\n document.hidden ? this.#stopAnimation() : this.#startAnimation();\n }\n }\n\n #startAnimation() {\n if (this.#isVisible) return;\n const animateFrame = () => {\n this.#animationFrameId = requestAnimationFrame(animateFrame);\n this.#timer.update();\n this.#animationState.delta = this.#timer.getDelta();\n this.#animationState.elapsed += this.#animationState.delta;\n this.onBeforeRender(this.#animationState);\n this.render();\n this.onAfterRender(this.#animationState);\n };\n this.#isVisible = true;\n this.#timer.reset();\n animateFrame();\n }\n\n #stopAnimation() {\n if (this.#isVisible) {\n cancelAnimationFrame(this.#animationFrameId);\n this.#isVisible = false;\n }\n }\n\n #render() {\n this.renderer.render(this.scene, this.camera);\n }\n\n clear() {\n this.scene.traverse(obj => {\n if ((obj as any).isMesh && typeof (obj as any).material === 'object' && (obj as any).material !== null) {\n Object.keys((obj as any).material).forEach(key => {\n const matProp = (obj as any).material[key];\n if (matProp && typeof matProp === 'object' && typeof matProp.dispose === 'function') {\n matProp.dispose();\n }\n });\n (obj as any).material.dispose();\n (obj as any).geometry.dispose();\n }\n });\n this.scene.clear();\n }\n\n dispose() {\n this.#onResizeCleanup();\n this.#stopAnimation();\n this.#timer.dispose();\n this.clear();\n this.#postprocessing?.dispose();\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n this.isDisposed = true;\n }\n\n #onResizeCleanup() {\n window.removeEventListener('resize', this.#onResize.bind(this));\n this.#resizeObserver?.disconnect();\n this.#intersectionObserver?.disconnect();\n document.removeEventListener('visibilitychange', this.#onVisibilityChange.bind(this));\n }\n}\n\ninterface WConfig {\n count: number;\n maxX: number;\n maxY: number;\n maxZ: number;\n maxSize: number;\n minSize: number;\n size0: number;\n gravity: number;\n friction: number;\n wallBounce: number;\n maxVelocity: number;\n controlSphere0?: boolean;\n followCursor?: boolean;\n}\n\nclass W {\n config: WConfig;\n positionData: Float32Array;\n velocityData: Float32Array;\n sizeData: Float32Array;\n center: Vector3 = new Vector3();\n\n constructor(config: WConfig) {\n this.config = config;\n this.positionData = new Float32Array(3 * config.count).fill(0);\n this.velocityData = new Float32Array(3 * config.count).fill(0);\n this.sizeData = new Float32Array(config.count).fill(1);\n this.center = new Vector3();\n this.#initializePositions();\n this.setSizes();\n }\n\n #initializePositions() {\n const { config, positionData } = this;\n this.center.toArray(positionData, 0);\n for (let i = 1; i < config.count; i++) {\n const idx = 3 * i;\n positionData[idx] = MathUtils.randFloatSpread(2 * config.maxX);\n positionData[idx + 1] = MathUtils.randFloatSpread(2 * config.maxY);\n positionData[idx + 2] = MathUtils.randFloatSpread(2 * config.maxZ);\n }\n }\n\n setSizes() {\n const { config, sizeData } = this;\n sizeData[0] = config.size0;\n for (let i = 1; i < config.count; i++) {\n sizeData[i] = MathUtils.randFloat(config.minSize, config.maxSize);\n }\n }\n\n update(deltaInfo: { delta: number }) {\n const { config, center, positionData, sizeData, velocityData } = this;\n let startIdx = 0;\n if (config.controlSphere0) {\n startIdx = 1;\n const firstVec = new Vector3().fromArray(positionData, 0);\n firstVec.lerp(center, 0.1).toArray(positionData, 0);\n new Vector3(0, 0, 0).toArray(velocityData, 0);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n vel.y -= deltaInfo.delta * config.gravity * sizeData[idx];\n vel.multiplyScalar(config.friction);\n vel.clampLength(0, config.maxVelocity);\n pos.add(vel);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n const radius = sizeData[idx];\n for (let jdx = idx + 1; jdx < config.count; jdx++) {\n const otherBase = 3 * jdx;\n const otherPos = new Vector3().fromArray(positionData, otherBase);\n const otherVel = new Vector3().fromArray(velocityData, otherBase);\n const diff = new Vector3().copy(otherPos).sub(pos);\n const dist = diff.length();\n const sumRadius = radius + sizeData[jdx];\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n const correction = diff.normalize().multiplyScalar(0.5 * overlap);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 1));\n pos.sub(correction);\n vel.sub(velCorrection);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n otherPos.add(correction);\n otherVel.add(correction.clone().multiplyScalar(Math.max(otherVel.length(), 1)));\n otherPos.toArray(positionData, otherBase);\n otherVel.toArray(velocityData, otherBase);\n }\n }\n if (config.controlSphere0) {\n const diff = new Vector3().copy(new Vector3().fromArray(positionData, 0)).sub(pos);\n const d = diff.length();\n const sumRadius0 = radius + sizeData[0];\n if (d < sumRadius0) {\n const correction = diff.normalize().multiplyScalar(sumRadius0 - d);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 2));\n pos.sub(correction);\n vel.sub(velCorrection);\n }\n }\n if (Math.abs(pos.x) + radius > config.maxX) {\n pos.x = Math.sign(pos.x) * (config.maxX - radius);\n vel.x = -vel.x * config.wallBounce;\n }\n if (config.gravity === 0) {\n if (Math.abs(pos.y) + radius > config.maxY) {\n pos.y = Math.sign(pos.y) * (config.maxY - radius);\n vel.y = -vel.y * config.wallBounce;\n }\n } else if (pos.y - radius < -config.maxY) {\n pos.y = -config.maxY + radius;\n vel.y = -vel.y * config.wallBounce;\n }\n const maxBoundary = Math.max(config.maxZ, config.maxSize);\n if (Math.abs(pos.z) + radius > maxBoundary) {\n pos.z = Math.sign(pos.z) * (config.maxZ - radius);\n vel.z = -vel.z * config.wallBounce;\n }\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n }\n}\n\nclass Y extends MeshPhysicalMaterial {\n uniforms: { [key: string]: { value: any } } = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n defines: { USE_UV: string };\n\n constructor(params: any) {\n super(params);\n this.defines = { USE_UV: '' };\n this.onBeforeCompile = shader => {\n Object.assign(shader.uniforms, this.uniforms);\n shader.fragmentShader =\n `\n uniform float thicknessPower;\n uniform float thicknessScale;\n uniform float thicknessDistortion;\n uniform float thicknessAmbient;\n uniform float thicknessAttenuation;\n ` + shader.fragmentShader;\n shader.fragmentShader = shader.fragmentShader.replace(\n 'void main() {',\n `\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\n #ifdef USE_COLOR\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\n #else\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\n #endif\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\n }\n\n void main() {\n `\n );\n const lightsChunk = ShaderChunk.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n `\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\n `\n );\n shader.fragmentShader = shader.fragmentShader.replace('#include ', lightsChunk);\n if (this.onBeforeCompile2) this.onBeforeCompile2(shader);\n };\n }\n onBeforeCompile2?: (shader: any) => void;\n}\n\nconst XConfig = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 0xffffff,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new Object3D();\n\nlet globalPointerActive = false;\nconst pointerPosition = new Vector2();\n\ninterface PointerData {\n position: Vector2;\n nPosition: Vector2;\n hover: boolean;\n touching: boolean;\n onEnter: (data: PointerData) => void;\n onMove: (data: PointerData) => void;\n onClick: (data: PointerData) => void;\n onLeave: (data: PointerData) => void;\n dispose?: () => void;\n}\n\nconst pointerMap = new Map();\n\nfunction createPointerData(options: Partial & { domElement: HTMLElement }): PointerData {\n const defaultData: PointerData = {\n position: new Vector2(),\n nPosition: new Vector2(),\n hover: false,\n touching: false,\n onEnter: () => {},\n onMove: () => {},\n onClick: () => {},\n onLeave: () => {},\n ...options\n };\n if (!pointerMap.has(options.domElement)) {\n pointerMap.set(options.domElement, defaultData);\n if (!globalPointerActive) {\n document.body.addEventListener('pointermove', onPointerMove as EventListener);\n document.body.addEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.addEventListener('click', onPointerClick as EventListener);\n\n document.body.addEventListener('touchstart', onTouchStart as EventListener, { passive: false });\n document.body.addEventListener('touchmove', onTouchMove as EventListener, { passive: false });\n document.body.addEventListener('touchend', onTouchEnd as EventListener, { passive: false });\n document.body.addEventListener('touchcancel', onTouchEnd as EventListener, { passive: false });\n globalPointerActive = true;\n }\n }\n defaultData.dispose = () => {\n pointerMap.delete(options.domElement);\n if (pointerMap.size === 0) {\n document.body.removeEventListener('pointermove', onPointerMove as EventListener);\n document.body.removeEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.removeEventListener('click', onPointerClick as EventListener);\n\n document.body.removeEventListener('touchstart', onTouchStart as EventListener);\n document.body.removeEventListener('touchmove', onTouchMove as EventListener);\n document.body.removeEventListener('touchend', onTouchEnd as EventListener);\n document.body.removeEventListener('touchcancel', onTouchEnd as EventListener);\n globalPointerActive = false;\n }\n };\n return defaultData;\n}\n\nfunction onPointerMove(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n processPointerInteraction();\n}\n\nfunction processPointerInteraction() {\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && !data.touching) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction onTouchStart(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n data.touching = true;\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchMove(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) {\n if (!data.hover) {\n data.hover = true;\n data.touching = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && data.touching) {\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchEnd() {\n for (const [, data] of pointerMap) {\n if (data.touching) {\n data.touching = false;\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n }\n}\n\nfunction onPointerClick(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) data.onClick(data);\n }\n}\n\nfunction onPointerLeave() {\n for (const data of pointerMap.values()) {\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction updatePointerData(data: PointerData, rect: DOMRect) {\n data.position.set(pointerPosition.x - rect.left, pointerPosition.y - rect.top);\n data.nPosition.set((data.position.x / rect.width) * 2 - 1, (-data.position.y / rect.height) * 2 + 1);\n}\n\nfunction isInside(rect: DOMRect) {\n return (\n pointerPosition.x >= rect.left &&\n pointerPosition.x <= rect.left + rect.width &&\n pointerPosition.y >= rect.top &&\n pointerPosition.y <= rect.top + rect.height\n );\n}\n\nconst { randFloat, randFloatSpread } = MathUtils;\nconst F = new Vector3();\nconst I = new Vector3();\nconst O = new Vector3();\nconst V = new Vector3();\nconst B = new Vector3();\nconst N = new Vector3();\nconst _ = new Vector3();\nconst j = new Vector3();\nconst H = new Vector3();\nconst T = new Vector3();\n\nclass Z extends InstancedMesh {\n config: typeof XConfig;\n physics: W;\n ambientLight: AmbientLight | undefined;\n light: PointLight | undefined;\n\n constructor(renderer: WebGLRenderer, params: Partial = {}) {\n const config = { ...XConfig, ...params };\n const roomEnv = new RoomEnvironment();\n const pmrem = new PMREMGenerator(renderer);\n const envTexture = pmrem.fromScene(roomEnv).texture;\n const geometry = new SphereGeometry();\n const material = new Y({ envMap: envTexture, ...config.materialParams });\n material.envMapRotation.x = -Math.PI / 2;\n super(geometry, material, config.count);\n this.config = config;\n this.physics = new W(config);\n this.#setupLights();\n this.setColors(config.colors);\n }\n\n #setupLights() {\n this.ambientLight = new AmbientLight(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new PointLight(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n\n setColors(colors: number[]) {\n if (Array.isArray(colors) && colors.length > 1) {\n const colorUtils = (function (colorsArr: number[]) {\n let baseColors: number[] = colorsArr;\n let colorObjects: Color[] = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n return {\n setColors: (cols: number[]) => {\n baseColors = cols;\n colorObjects = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n },\n getColorAt: (ratio: number, out: Color = new Color()) => {\n const clamped = Math.max(0, Math.min(1, ratio));\n const scaled = clamped * (baseColors.length - 1);\n const idx = Math.floor(scaled);\n const start = colorObjects[idx];\n if (idx >= baseColors.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = colorObjects[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(colors);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, colorUtils.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light!.color.copy(colorUtils.getColorAt(idx / this.count));\n }\n }\n\n if (!this.instanceColor) return;\n this.instanceColor.needsUpdate = true;\n }\n }\n\n update(deltaInfo: { delta: number }) {\n this.physics.update(deltaInfo);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light!.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\ninterface CreateBallpitReturn {\n three: X;\n spheres: Z;\n setCount: (count: number) => void;\n updateConfig: (newProps: { [key: string]: any }) => void;\n togglePause: () => void;\n dispose: () => void;\n}\n\nfunction createBallpit(canvas: HTMLCanvasElement, config: any = {}): CreateBallpitReturn {\n const threeInstance = new X({\n canvas,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let spheres: Z;\n threeInstance.renderer.toneMapping = ACESFilmicToneMapping;\n threeInstance.camera.position.set(0, 0, 20);\n threeInstance.camera.lookAt(0, 0, 0);\n threeInstance.cameraMaxAspect = 1.5;\n threeInstance.resize();\n initialize(config);\n const raycaster = new Raycaster();\n const plane = new Plane(new Vector3(0, 0, 1), 0);\n const intersectionPoint = new Vector3();\n let isPaused = false;\n\n canvas.style.touchAction = 'none';\n canvas.style.userSelect = 'none';\n (canvas.style as any).webkitUserSelect = 'none';\n\n const pointerData = createPointerData({\n domElement: canvas,\n onMove() {\n raycaster.setFromCamera(pointerData.nPosition, threeInstance.camera);\n threeInstance.camera.getWorldDirection(plane.normal);\n raycaster.ray.intersectPlane(plane, intersectionPoint);\n spheres.physics.center.copy(intersectionPoint);\n spheres.config.controlSphere0 = true;\n },\n onLeave() {\n spheres.config.controlSphere0 = false;\n }\n });\n function initialize(cfg: any) {\n if (spheres) {\n threeInstance.clear();\n threeInstance.scene.remove(spheres);\n }\n spheres = new Z(threeInstance.renderer, cfg);\n threeInstance.scene.add(spheres);\n }\n threeInstance.onBeforeRender = deltaInfo => {\n if (!isPaused) spheres.update(deltaInfo);\n };\n threeInstance.onAfterResize = size => {\n spheres.config.maxX = size.wWidth / 2;\n spheres.config.maxY = size.wHeight / 2;\n };\n return {\n three: threeInstance,\n get spheres() {\n return spheres;\n },\n setCount(count: number) {\n initialize({ ...spheres.config, count });\n },\n updateConfig(newProps: { [key: string]: any }) {\n if (newProps.count !== undefined && newProps.count !== spheres.config.count) {\n initialize({ ...spheres.config, ...newProps });\n } else {\n Object.assign(spheres.config, newProps);\n if (newProps.colors) {\n spheres.setColors(spheres.config.colors);\n }\n if (newProps.minSize !== undefined || newProps.maxSize !== undefined || newProps.size0 !== undefined) {\n spheres.physics.setSizes();\n }\n }\n },\n togglePause() {\n isPaused = !isPaused;\n },\n dispose() {\n pointerData.dispose?.();\n threeInstance.dispose();\n }\n };\n}\n\ninterface BallpitProps {\n className?: string;\n followCursor?: boolean;\n [key: string]: any;\n}\n\nconst Ballpit: React.FC = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n const isFirstRender = useRef(true);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, {\n followCursor,\n ...props\n });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n spheresInstanceRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (isFirstRender.current) {\n isFirstRender.current = false;\n return;\n }\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.updateConfig({ followCursor, ...props });\n }\n }, [props, followCursor]);\n\n return ;\n};\n\nexport default Ballpit;\n" + "content": "import { gsap } from 'gsap';\nimport { Observer } from 'gsap/Observer';\nimport React, { useEffect, useRef } from 'react';\nimport {\n ACESFilmicToneMapping,\n AmbientLight,\n Color,\n InstancedMesh,\n MathUtils,\n MeshPhysicalMaterial,\n Object3D,\n PerspectiveCamera,\n Plane,\n PMREMGenerator,\n PointLight,\n Raycaster,\n Scene,\n ShaderChunk,\n SphereGeometry,\n SRGBColorSpace,\n Timer,\n Vector2,\n Vector3,\n WebGLRenderer,\n type WebGLRendererParameters\n} from 'three';\nimport { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';\n\ngsap.registerPlugin(Observer);\n\ninterface XConfig {\n canvas?: HTMLCanvasElement;\n id?: string;\n rendererOptions?: Partial;\n size?: 'parent' | { width: number; height: number };\n}\n\ninterface SizeData {\n width: number;\n height: number;\n wWidth: number;\n wHeight: number;\n ratio: number;\n pixelRatio: number;\n}\n\nclass X {\n #config: XConfig;\n #postprocessing: any;\n #resizeObserver?: ResizeObserver;\n #intersectionObserver?: IntersectionObserver;\n #resizeTimer?: number;\n #animationFrameId: number = 0;\n #timer: Timer = new Timer();\n #animationState = { elapsed: 0, delta: 0 };\n #isAnimating: boolean = false;\n #isVisible: boolean = false;\n // Bind once: `.bind()` returns a new function on every call, so binding again\n // in the teardown would hand removeEventListener a function that was never\n // registered, leaving the listener attached for the lifetime of the page.\n #boundResize = this.#onResize.bind(this);\n #boundVisibilityChange = this.#onVisibilityChange.bind(this);\n\n canvas!: HTMLCanvasElement;\n camera!: PerspectiveCamera;\n cameraMinAspect?: number;\n cameraMaxAspect?: number;\n cameraFov!: number;\n maxPixelRatio?: number;\n minPixelRatio?: number;\n scene!: Scene;\n renderer!: WebGLRenderer;\n size: SizeData = {\n width: 0,\n height: 0,\n wWidth: 0,\n wHeight: 0,\n ratio: 0,\n pixelRatio: 0\n };\n\n render: () => void = this.#render.bind(this);\n onBeforeRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterResize: (size: SizeData) => void = () => {};\n isDisposed: boolean = false;\n\n constructor(config: XConfig) {\n this.#config = { ...config };\n this.#initCamera();\n this.#initScene();\n this.#initRenderer();\n this.resize();\n this.#initObservers();\n }\n\n #initCamera() {\n this.camera = new PerspectiveCamera();\n this.cameraFov = this.camera.fov;\n }\n\n #initScene() {\n this.scene = new Scene();\n }\n\n #initRenderer() {\n if (this.#config.canvas) {\n this.canvas = this.#config.canvas;\n } else if (this.#config.id) {\n const elem = document.getElementById(this.#config.id);\n if (elem instanceof HTMLCanvasElement) {\n this.canvas = elem;\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas!.style.display = 'block';\n const rendererOptions: WebGLRendererParameters = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#config.rendererOptions ?? {})\n };\n this.renderer = new WebGLRenderer(rendererOptions);\n this.renderer.outputColorSpace = SRGBColorSpace;\n }\n\n #initObservers() {\n if (!(this.#config.size instanceof Object)) {\n window.addEventListener('resize', this.#boundResize);\n if (this.#config.size === 'parent' && this.canvas.parentNode) {\n this.#resizeObserver = new ResizeObserver(this.#onResize.bind(this));\n this.#resizeObserver.observe(this.canvas.parentNode as Element);\n }\n }\n this.#intersectionObserver = new IntersectionObserver(this.#onIntersection.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#intersectionObserver.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#boundVisibilityChange);\n }\n\n #onResize() {\n if (this.#resizeTimer) clearTimeout(this.#resizeTimer);\n this.#resizeTimer = window.setTimeout(this.resize.bind(this), 100);\n }\n\n resize() {\n let w: number, h: number;\n if (this.#config.size instanceof Object) {\n w = this.#config.size.width;\n h = this.#config.size.height;\n } else if (this.#config.size === 'parent' && this.canvas.parentNode) {\n w = (this.canvas.parentNode as HTMLElement).offsetWidth;\n h = (this.canvas.parentNode as HTMLElement).offsetHeight;\n } else {\n w = window.innerWidth;\n h = window.innerHeight;\n }\n this.size.width = w;\n this.size.height = h;\n this.size.ratio = w / h;\n this.#updateCamera();\n this.#updateRenderer();\n this.onAfterResize(this.size);\n }\n\n #updateCamera() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#adjustFov(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#adjustFov(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n\n #adjustFov(aspect: number) {\n const tanFov = Math.tan(MathUtils.degToRad(this.cameraFov / 2));\n const newTan = tanFov / (this.camera.aspect / aspect);\n this.camera.fov = 2 * MathUtils.radToDeg(Math.atan(newTan));\n }\n\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const fovRad = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(fovRad / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if ((this.camera as any).isOrthographicCamera) {\n const cam = this.camera as any;\n this.size.wHeight = cam.top - cam.bottom;\n this.size.wWidth = cam.right - cam.left;\n }\n }\n\n #updateRenderer() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#postprocessing?.setSize(this.size.width, this.size.height);\n let pr = window.devicePixelRatio;\n if (this.maxPixelRatio && pr > this.maxPixelRatio) {\n pr = this.maxPixelRatio;\n } else if (this.minPixelRatio && pr < this.minPixelRatio) {\n pr = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(pr);\n this.size.pixelRatio = pr;\n }\n\n get postprocessing() {\n return this.#postprocessing;\n }\n set postprocessing(value: any) {\n this.#postprocessing = value;\n this.render = value.render.bind(value);\n }\n\n #onIntersection(entries: IntersectionObserverEntry[]) {\n this.#isAnimating = entries[0].isIntersecting;\n this.#isAnimating ? this.#startAnimation() : this.#stopAnimation();\n }\n\n #onVisibilityChange() {\n if (this.#isAnimating) {\n document.hidden ? this.#stopAnimation() : this.#startAnimation();\n }\n }\n\n #startAnimation() {\n if (this.#isVisible) return;\n const animateFrame = () => {\n this.#animationFrameId = requestAnimationFrame(animateFrame);\n this.#timer.update();\n this.#animationState.delta = this.#timer.getDelta();\n this.#animationState.elapsed += this.#animationState.delta;\n this.onBeforeRender(this.#animationState);\n this.render();\n this.onAfterRender(this.#animationState);\n };\n this.#isVisible = true;\n this.#timer.reset();\n animateFrame();\n }\n\n #stopAnimation() {\n if (this.#isVisible) {\n cancelAnimationFrame(this.#animationFrameId);\n this.#isVisible = false;\n }\n }\n\n #render() {\n this.renderer.render(this.scene, this.camera);\n }\n\n clear() {\n this.scene.traverse(obj => {\n if ((obj as any).isMesh && typeof (obj as any).material === 'object' && (obj as any).material !== null) {\n Object.keys((obj as any).material).forEach(key => {\n const matProp = (obj as any).material[key];\n if (matProp && typeof matProp === 'object' && typeof matProp.dispose === 'function') {\n matProp.dispose();\n }\n });\n (obj as any).material.dispose();\n (obj as any).geometry.dispose();\n }\n });\n this.scene.clear();\n }\n\n dispose() {\n this.#onResizeCleanup();\n this.#stopAnimation();\n this.#timer.dispose();\n this.clear();\n this.#postprocessing?.dispose();\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n this.isDisposed = true;\n }\n\n #onResizeCleanup() {\n window.removeEventListener('resize', this.#boundResize);\n this.#resizeObserver?.disconnect();\n this.#intersectionObserver?.disconnect();\n document.removeEventListener('visibilitychange', this.#boundVisibilityChange);\n }\n}\n\ninterface WConfig {\n count: number;\n maxX: number;\n maxY: number;\n maxZ: number;\n maxSize: number;\n minSize: number;\n size0: number;\n gravity: number;\n friction: number;\n wallBounce: number;\n maxVelocity: number;\n controlSphere0?: boolean;\n followCursor?: boolean;\n}\n\nclass W {\n config: WConfig;\n positionData: Float32Array;\n velocityData: Float32Array;\n sizeData: Float32Array;\n center: Vector3 = new Vector3();\n\n constructor(config: WConfig) {\n this.config = config;\n this.positionData = new Float32Array(3 * config.count).fill(0);\n this.velocityData = new Float32Array(3 * config.count).fill(0);\n this.sizeData = new Float32Array(config.count).fill(1);\n this.center = new Vector3();\n this.#initializePositions();\n this.setSizes();\n }\n\n #initializePositions() {\n const { config, positionData } = this;\n this.center.toArray(positionData, 0);\n for (let i = 1; i < config.count; i++) {\n const idx = 3 * i;\n positionData[idx] = MathUtils.randFloatSpread(2 * config.maxX);\n positionData[idx + 1] = MathUtils.randFloatSpread(2 * config.maxY);\n positionData[idx + 2] = MathUtils.randFloatSpread(2 * config.maxZ);\n }\n }\n\n setSizes() {\n const { config, sizeData } = this;\n sizeData[0] = config.size0;\n for (let i = 1; i < config.count; i++) {\n sizeData[i] = MathUtils.randFloat(config.minSize, config.maxSize);\n }\n }\n\n update(deltaInfo: { delta: number }) {\n const { config, center, positionData, sizeData, velocityData } = this;\n let startIdx = 0;\n if (config.controlSphere0) {\n startIdx = 1;\n const firstVec = new Vector3().fromArray(positionData, 0);\n firstVec.lerp(center, 0.1).toArray(positionData, 0);\n new Vector3(0, 0, 0).toArray(velocityData, 0);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n vel.y -= deltaInfo.delta * config.gravity * sizeData[idx];\n vel.multiplyScalar(config.friction);\n vel.clampLength(0, config.maxVelocity);\n pos.add(vel);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n const radius = sizeData[idx];\n for (let jdx = idx + 1; jdx < config.count; jdx++) {\n const otherBase = 3 * jdx;\n const otherPos = new Vector3().fromArray(positionData, otherBase);\n const otherVel = new Vector3().fromArray(velocityData, otherBase);\n const diff = new Vector3().copy(otherPos).sub(pos);\n const dist = diff.length();\n const sumRadius = radius + sizeData[jdx];\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n const correction = diff.normalize().multiplyScalar(0.5 * overlap);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 1));\n pos.sub(correction);\n vel.sub(velCorrection);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n otherPos.add(correction);\n otherVel.add(correction.clone().multiplyScalar(Math.max(otherVel.length(), 1)));\n otherPos.toArray(positionData, otherBase);\n otherVel.toArray(velocityData, otherBase);\n }\n }\n if (config.controlSphere0) {\n const diff = new Vector3().copy(new Vector3().fromArray(positionData, 0)).sub(pos);\n const d = diff.length();\n const sumRadius0 = radius + sizeData[0];\n if (d < sumRadius0) {\n const correction = diff.normalize().multiplyScalar(sumRadius0 - d);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 2));\n pos.sub(correction);\n vel.sub(velCorrection);\n }\n }\n if (Math.abs(pos.x) + radius > config.maxX) {\n pos.x = Math.sign(pos.x) * (config.maxX - radius);\n vel.x = -vel.x * config.wallBounce;\n }\n if (config.gravity === 0) {\n if (Math.abs(pos.y) + radius > config.maxY) {\n pos.y = Math.sign(pos.y) * (config.maxY - radius);\n vel.y = -vel.y * config.wallBounce;\n }\n } else if (pos.y - radius < -config.maxY) {\n pos.y = -config.maxY + radius;\n vel.y = -vel.y * config.wallBounce;\n }\n const maxBoundary = Math.max(config.maxZ, config.maxSize);\n if (Math.abs(pos.z) + radius > maxBoundary) {\n pos.z = Math.sign(pos.z) * (config.maxZ - radius);\n vel.z = -vel.z * config.wallBounce;\n }\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n }\n}\n\nclass Y extends MeshPhysicalMaterial {\n uniforms: { [key: string]: { value: any } } = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n defines: { USE_UV: string };\n\n constructor(params: any) {\n super(params);\n this.defines = { USE_UV: '' };\n this.onBeforeCompile = shader => {\n Object.assign(shader.uniforms, this.uniforms);\n shader.fragmentShader =\n `\n uniform float thicknessPower;\n uniform float thicknessScale;\n uniform float thicknessDistortion;\n uniform float thicknessAmbient;\n uniform float thicknessAttenuation;\n ` + shader.fragmentShader;\n shader.fragmentShader = shader.fragmentShader.replace(\n 'void main() {',\n `\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\n #ifdef USE_COLOR\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\n #else\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\n #endif\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\n }\n\n void main() {\n `\n );\n const lightsChunk = ShaderChunk.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n `\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\n `\n );\n shader.fragmentShader = shader.fragmentShader.replace('#include ', lightsChunk);\n if (this.onBeforeCompile2) this.onBeforeCompile2(shader);\n };\n }\n onBeforeCompile2?: (shader: any) => void;\n}\n\nconst XConfig = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 0xffffff,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new Object3D();\n\nlet globalPointerActive = false;\nconst pointerPosition = new Vector2();\n\ninterface PointerData {\n position: Vector2;\n nPosition: Vector2;\n hover: boolean;\n touching: boolean;\n onEnter: (data: PointerData) => void;\n onMove: (data: PointerData) => void;\n onClick: (data: PointerData) => void;\n onLeave: (data: PointerData) => void;\n dispose?: () => void;\n}\n\nconst pointerMap = new Map();\n\nfunction createPointerData(options: Partial & { domElement: HTMLElement }): PointerData {\n const defaultData: PointerData = {\n position: new Vector2(),\n nPosition: new Vector2(),\n hover: false,\n touching: false,\n onEnter: () => {},\n onMove: () => {},\n onClick: () => {},\n onLeave: () => {},\n ...options\n };\n if (!pointerMap.has(options.domElement)) {\n pointerMap.set(options.domElement, defaultData);\n if (!globalPointerActive) {\n document.body.addEventListener('pointermove', onPointerMove as EventListener);\n document.body.addEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.addEventListener('click', onPointerClick as EventListener);\n\n document.body.addEventListener('touchstart', onTouchStart as EventListener, { passive: false });\n document.body.addEventListener('touchmove', onTouchMove as EventListener, { passive: false });\n document.body.addEventListener('touchend', onTouchEnd as EventListener, { passive: false });\n document.body.addEventListener('touchcancel', onTouchEnd as EventListener, { passive: false });\n globalPointerActive = true;\n }\n }\n defaultData.dispose = () => {\n pointerMap.delete(options.domElement);\n if (pointerMap.size === 0) {\n document.body.removeEventListener('pointermove', onPointerMove as EventListener);\n document.body.removeEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.removeEventListener('click', onPointerClick as EventListener);\n\n document.body.removeEventListener('touchstart', onTouchStart as EventListener);\n document.body.removeEventListener('touchmove', onTouchMove as EventListener);\n document.body.removeEventListener('touchend', onTouchEnd as EventListener);\n document.body.removeEventListener('touchcancel', onTouchEnd as EventListener);\n globalPointerActive = false;\n }\n };\n return defaultData;\n}\n\nfunction onPointerMove(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n processPointerInteraction();\n}\n\nfunction processPointerInteraction() {\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && !data.touching) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction onTouchStart(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n data.touching = true;\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchMove(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) {\n if (!data.hover) {\n data.hover = true;\n data.touching = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && data.touching) {\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchEnd() {\n for (const [, data] of pointerMap) {\n if (data.touching) {\n data.touching = false;\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n }\n}\n\nfunction onPointerClick(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) data.onClick(data);\n }\n}\n\nfunction onPointerLeave() {\n for (const data of pointerMap.values()) {\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction updatePointerData(data: PointerData, rect: DOMRect) {\n data.position.set(pointerPosition.x - rect.left, pointerPosition.y - rect.top);\n data.nPosition.set((data.position.x / rect.width) * 2 - 1, (-data.position.y / rect.height) * 2 + 1);\n}\n\nfunction isInside(rect: DOMRect) {\n return (\n pointerPosition.x >= rect.left &&\n pointerPosition.x <= rect.left + rect.width &&\n pointerPosition.y >= rect.top &&\n pointerPosition.y <= rect.top + rect.height\n );\n}\n\nconst { randFloat, randFloatSpread } = MathUtils;\nconst F = new Vector3();\nconst I = new Vector3();\nconst O = new Vector3();\nconst V = new Vector3();\nconst B = new Vector3();\nconst N = new Vector3();\nconst _ = new Vector3();\nconst j = new Vector3();\nconst H = new Vector3();\nconst T = new Vector3();\n\nclass Z extends InstancedMesh {\n config: typeof XConfig;\n physics: W;\n ambientLight: AmbientLight | undefined;\n light: PointLight | undefined;\n\n constructor(renderer: WebGLRenderer, params: Partial = {}) {\n const config = { ...XConfig, ...params };\n const roomEnv = new RoomEnvironment();\n const pmrem = new PMREMGenerator(renderer);\n const envTexture = pmrem.fromScene(roomEnv).texture;\n const geometry = new SphereGeometry();\n const material = new Y({ envMap: envTexture, ...config.materialParams });\n material.envMapRotation.x = -Math.PI / 2;\n super(geometry, material, config.count);\n this.config = config;\n this.physics = new W(config);\n this.#setupLights();\n this.setColors(config.colors);\n }\n\n #setupLights() {\n this.ambientLight = new AmbientLight(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new PointLight(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n\n setColors(colors: number[]) {\n if (Array.isArray(colors) && colors.length > 1) {\n const colorUtils = (function (colorsArr: number[]) {\n let baseColors: number[] = colorsArr;\n let colorObjects: Color[] = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n return {\n setColors: (cols: number[]) => {\n baseColors = cols;\n colorObjects = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n },\n getColorAt: (ratio: number, out: Color = new Color()) => {\n const clamped = Math.max(0, Math.min(1, ratio));\n const scaled = clamped * (baseColors.length - 1);\n const idx = Math.floor(scaled);\n const start = colorObjects[idx];\n if (idx >= baseColors.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = colorObjects[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(colors);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, colorUtils.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light!.color.copy(colorUtils.getColorAt(idx / this.count));\n }\n }\n\n if (!this.instanceColor) return;\n this.instanceColor.needsUpdate = true;\n }\n }\n\n update(deltaInfo: { delta: number }) {\n this.physics.update(deltaInfo);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light!.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\ninterface CreateBallpitReturn {\n three: X;\n spheres: Z;\n setCount: (count: number) => void;\n updateConfig: (newProps: { [key: string]: any }) => void;\n togglePause: () => void;\n dispose: () => void;\n}\n\nfunction createBallpit(canvas: HTMLCanvasElement, config: any = {}): CreateBallpitReturn {\n const threeInstance = new X({\n canvas,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let spheres: Z;\n threeInstance.renderer.toneMapping = ACESFilmicToneMapping;\n threeInstance.camera.position.set(0, 0, 20);\n threeInstance.camera.lookAt(0, 0, 0);\n threeInstance.cameraMaxAspect = 1.5;\n threeInstance.resize();\n initialize(config);\n const raycaster = new Raycaster();\n const plane = new Plane(new Vector3(0, 0, 1), 0);\n const intersectionPoint = new Vector3();\n let isPaused = false;\n\n canvas.style.touchAction = 'none';\n canvas.style.userSelect = 'none';\n (canvas.style as any).webkitUserSelect = 'none';\n\n const pointerData = createPointerData({\n domElement: canvas,\n onMove() {\n raycaster.setFromCamera(pointerData.nPosition, threeInstance.camera);\n threeInstance.camera.getWorldDirection(plane.normal);\n raycaster.ray.intersectPlane(plane, intersectionPoint);\n spheres.physics.center.copy(intersectionPoint);\n spheres.config.controlSphere0 = true;\n },\n onLeave() {\n spheres.config.controlSphere0 = false;\n }\n });\n function initialize(cfg: any) {\n if (spheres) {\n threeInstance.clear();\n threeInstance.scene.remove(spheres);\n }\n spheres = new Z(threeInstance.renderer, cfg);\n threeInstance.scene.add(spheres);\n }\n threeInstance.onBeforeRender = deltaInfo => {\n if (!isPaused) spheres.update(deltaInfo);\n };\n threeInstance.onAfterResize = size => {\n spheres.config.maxX = size.wWidth / 2;\n spheres.config.maxY = size.wHeight / 2;\n };\n return {\n three: threeInstance,\n get spheres() {\n return spheres;\n },\n setCount(count: number) {\n initialize({ ...spheres.config, count });\n },\n updateConfig(newProps: { [key: string]: any }) {\n if (newProps.count !== undefined && newProps.count !== spheres.config.count) {\n initialize({ ...spheres.config, ...newProps });\n } else {\n Object.assign(spheres.config, newProps);\n if (newProps.colors) {\n spheres.setColors(spheres.config.colors);\n }\n if (newProps.minSize !== undefined || newProps.maxSize !== undefined || newProps.size0 !== undefined) {\n spheres.physics.setSizes();\n }\n }\n },\n togglePause() {\n isPaused = !isPaused;\n },\n dispose() {\n pointerData.dispose?.();\n threeInstance.dispose();\n }\n };\n}\n\ninterface BallpitProps {\n className?: string;\n followCursor?: boolean;\n [key: string]: any;\n}\n\nconst Ballpit: React.FC = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n const isFirstRender = useRef(true);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, {\n followCursor,\n ...props\n });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n spheresInstanceRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (isFirstRender.current) {\n isFirstRender.current = false;\n return;\n }\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.updateConfig({ followCursor, ...props });\n }\n }, [props, followCursor]);\n\n return ;\n};\n\nexport default Ballpit;\n" } ], "registryDependencies": [], diff --git a/public/r/Ballpit-TS-TW.json b/public/r/Ballpit-TS-TW.json index d52651c2..c16a9f61 100644 --- a/public/r/Ballpit-TS-TW.json +++ b/public/r/Ballpit-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "Ballpit/Ballpit.tsx", - "content": "import { gsap } from 'gsap';\nimport { Observer } from 'gsap/Observer';\nimport React, { useEffect, useRef } from 'react';\nimport {\n ACESFilmicToneMapping,\n AmbientLight,\n Color,\n InstancedMesh,\n MathUtils,\n MeshPhysicalMaterial,\n Object3D,\n PerspectiveCamera,\n Plane,\n PMREMGenerator,\n PointLight,\n Raycaster,\n Scene,\n ShaderChunk,\n SphereGeometry,\n SRGBColorSpace,\n Timer,\n Vector2,\n Vector3,\n WebGLRenderer,\n WebGLRendererParameters\n} from 'three';\nimport { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';\n\ngsap.registerPlugin(Observer);\n\ninterface XConfig {\n canvas?: HTMLCanvasElement;\n id?: string;\n rendererOptions?: Partial;\n size?: 'parent' | { width: number; height: number };\n}\n\ninterface SizeData {\n width: number;\n height: number;\n wWidth: number;\n wHeight: number;\n ratio: number;\n pixelRatio: number;\n}\n\nclass X {\n #config: XConfig;\n #postprocessing: any;\n #resizeObserver?: ResizeObserver;\n #intersectionObserver?: IntersectionObserver;\n #resizeTimer?: number;\n #animationFrameId: number = 0;\n #timer: Timer = new Timer();\n #animationState = { elapsed: 0, delta: 0 };\n #isAnimating: boolean = false;\n #isVisible: boolean = false;\n\n canvas!: HTMLCanvasElement;\n camera!: PerspectiveCamera;\n cameraMinAspect?: number;\n cameraMaxAspect?: number;\n cameraFov!: number;\n maxPixelRatio?: number;\n minPixelRatio?: number;\n scene!: Scene;\n renderer!: WebGLRenderer;\n size: SizeData = {\n width: 0,\n height: 0,\n wWidth: 0,\n wHeight: 0,\n ratio: 0,\n pixelRatio: 0\n };\n\n render: () => void = this.#render.bind(this);\n onBeforeRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterResize: (size: SizeData) => void = () => {};\n isDisposed: boolean = false;\n\n constructor(config: XConfig) {\n this.#config = { ...config };\n this.#initCamera();\n this.#initScene();\n this.#initRenderer();\n this.resize();\n this.#initObservers();\n }\n\n #initCamera() {\n this.camera = new PerspectiveCamera();\n this.cameraFov = this.camera.fov;\n }\n\n #initScene() {\n this.scene = new Scene();\n }\n\n #initRenderer() {\n if (this.#config.canvas) {\n this.canvas = this.#config.canvas;\n } else if (this.#config.id) {\n const elem = document.getElementById(this.#config.id);\n if (elem instanceof HTMLCanvasElement) {\n this.canvas = elem;\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas!.style.display = 'block';\n const rendererOptions: WebGLRendererParameters = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#config.rendererOptions ?? {})\n };\n this.renderer = new WebGLRenderer(rendererOptions);\n this.renderer.outputColorSpace = SRGBColorSpace;\n }\n\n #initObservers() {\n if (!(this.#config.size instanceof Object)) {\n window.addEventListener('resize', this.#onResize.bind(this));\n if (this.#config.size === 'parent' && this.canvas.parentNode) {\n this.#resizeObserver = new ResizeObserver(this.#onResize.bind(this));\n this.#resizeObserver.observe(this.canvas.parentNode as Element);\n }\n }\n this.#intersectionObserver = new IntersectionObserver(this.#onIntersection.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#intersectionObserver.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#onVisibilityChange.bind(this));\n }\n\n #onResize() {\n if (this.#resizeTimer) clearTimeout(this.#resizeTimer);\n this.#resizeTimer = window.setTimeout(this.resize.bind(this), 100);\n }\n\n resize() {\n let w: number, h: number;\n if (this.#config.size instanceof Object) {\n w = this.#config.size.width;\n h = this.#config.size.height;\n } else if (this.#config.size === 'parent' && this.canvas.parentNode) {\n w = (this.canvas.parentNode as HTMLElement).offsetWidth;\n h = (this.canvas.parentNode as HTMLElement).offsetHeight;\n } else {\n w = window.innerWidth;\n h = window.innerHeight;\n }\n this.size.width = w;\n this.size.height = h;\n this.size.ratio = w / h;\n this.#updateCamera();\n this.#updateRenderer();\n this.onAfterResize(this.size);\n }\n\n #updateCamera() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#adjustFov(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#adjustFov(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n\n #adjustFov(aspect: number) {\n const tanFov = Math.tan(MathUtils.degToRad(this.cameraFov / 2));\n const newTan = tanFov / (this.camera.aspect / aspect);\n this.camera.fov = 2 * MathUtils.radToDeg(Math.atan(newTan));\n }\n\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const fovRad = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(fovRad / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if ((this.camera as any).isOrthographicCamera) {\n const cam = this.camera as any;\n this.size.wHeight = cam.top - cam.bottom;\n this.size.wWidth = cam.right - cam.left;\n }\n }\n\n #updateRenderer() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#postprocessing?.setSize(this.size.width, this.size.height);\n let pr = window.devicePixelRatio;\n if (this.maxPixelRatio && pr > this.maxPixelRatio) {\n pr = this.maxPixelRatio;\n } else if (this.minPixelRatio && pr < this.minPixelRatio) {\n pr = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(pr);\n this.size.pixelRatio = pr;\n }\n\n get postprocessing() {\n return this.#postprocessing;\n }\n set postprocessing(value: any) {\n this.#postprocessing = value;\n this.render = value.render.bind(value);\n }\n\n #onIntersection(entries: IntersectionObserverEntry[]) {\n this.#isAnimating = entries[0].isIntersecting;\n this.#isAnimating ? this.#startAnimation() : this.#stopAnimation();\n }\n\n #onVisibilityChange() {\n if (this.#isAnimating) {\n document.hidden ? this.#stopAnimation() : this.#startAnimation();\n }\n }\n\n #startAnimation() {\n if (this.#isVisible) return;\n const animateFrame = () => {\n this.#animationFrameId = requestAnimationFrame(animateFrame);\n this.#timer.update();\n this.#animationState.delta = this.#timer.getDelta();\n this.#animationState.elapsed += this.#animationState.delta;\n this.onBeforeRender(this.#animationState);\n this.render();\n this.onAfterRender(this.#animationState);\n };\n this.#isVisible = true;\n this.#timer.reset();\n animateFrame();\n }\n\n #stopAnimation() {\n if (this.#isVisible) {\n cancelAnimationFrame(this.#animationFrameId);\n this.#isVisible = false;\n }\n }\n\n #render() {\n this.renderer.render(this.scene, this.camera);\n }\n\n clear() {\n this.scene.traverse(obj => {\n if ((obj as any).isMesh && typeof (obj as any).material === 'object' && (obj as any).material !== null) {\n Object.keys((obj as any).material).forEach(key => {\n const matProp = (obj as any).material[key];\n if (matProp && typeof matProp === 'object' && typeof matProp.dispose === 'function') {\n matProp.dispose();\n }\n });\n (obj as any).material.dispose();\n (obj as any).geometry.dispose();\n }\n });\n this.scene.clear();\n }\n\n dispose() {\n this.#onResizeCleanup();\n this.#stopAnimation();\n this.#timer.dispose();\n this.clear();\n this.#postprocessing?.dispose();\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n this.isDisposed = true;\n }\n\n #onResizeCleanup() {\n window.removeEventListener('resize', this.#onResize.bind(this));\n this.#resizeObserver?.disconnect();\n this.#intersectionObserver?.disconnect();\n document.removeEventListener('visibilitychange', this.#onVisibilityChange.bind(this));\n }\n}\n\ninterface WConfig {\n count: number;\n maxX: number;\n maxY: number;\n maxZ: number;\n maxSize: number;\n minSize: number;\n size0: number;\n gravity: number;\n friction: number;\n wallBounce: number;\n maxVelocity: number;\n controlSphere0?: boolean;\n followCursor?: boolean;\n}\n\nclass W {\n config: WConfig;\n positionData: Float32Array;\n velocityData: Float32Array;\n sizeData: Float32Array;\n center: Vector3 = new Vector3();\n\n constructor(config: WConfig) {\n this.config = config;\n this.positionData = new Float32Array(3 * config.count).fill(0);\n this.velocityData = new Float32Array(3 * config.count).fill(0);\n this.sizeData = new Float32Array(config.count).fill(1);\n this.center = new Vector3();\n this.#initializePositions();\n this.setSizes();\n }\n\n #initializePositions() {\n const { config, positionData } = this;\n this.center.toArray(positionData, 0);\n for (let i = 1; i < config.count; i++) {\n const idx = 3 * i;\n positionData[idx] = MathUtils.randFloatSpread(2 * config.maxX);\n positionData[idx + 1] = MathUtils.randFloatSpread(2 * config.maxY);\n positionData[idx + 2] = MathUtils.randFloatSpread(2 * config.maxZ);\n }\n }\n\n setSizes() {\n const { config, sizeData } = this;\n sizeData[0] = config.size0;\n for (let i = 1; i < config.count; i++) {\n sizeData[i] = MathUtils.randFloat(config.minSize, config.maxSize);\n }\n }\n\n update(deltaInfo: { delta: number }) {\n const { config, center, positionData, sizeData, velocityData } = this;\n let startIdx = 0;\n if (config.controlSphere0) {\n startIdx = 1;\n const firstVec = new Vector3().fromArray(positionData, 0);\n firstVec.lerp(center, 0.1).toArray(positionData, 0);\n new Vector3(0, 0, 0).toArray(velocityData, 0);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n vel.y -= deltaInfo.delta * config.gravity * sizeData[idx];\n vel.multiplyScalar(config.friction);\n vel.clampLength(0, config.maxVelocity);\n pos.add(vel);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n const radius = sizeData[idx];\n for (let jdx = idx + 1; jdx < config.count; jdx++) {\n const otherBase = 3 * jdx;\n const otherPos = new Vector3().fromArray(positionData, otherBase);\n const otherVel = new Vector3().fromArray(velocityData, otherBase);\n const diff = new Vector3().copy(otherPos).sub(pos);\n const dist = diff.length();\n const sumRadius = radius + sizeData[jdx];\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n const correction = diff.normalize().multiplyScalar(0.5 * overlap);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 1));\n pos.sub(correction);\n vel.sub(velCorrection);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n otherPos.add(correction);\n otherVel.add(correction.clone().multiplyScalar(Math.max(otherVel.length(), 1)));\n otherPos.toArray(positionData, otherBase);\n otherVel.toArray(velocityData, otherBase);\n }\n }\n if (config.controlSphere0) {\n const diff = new Vector3().copy(new Vector3().fromArray(positionData, 0)).sub(pos);\n const d = diff.length();\n const sumRadius0 = radius + sizeData[0];\n if (d < sumRadius0) {\n const correction = diff.normalize().multiplyScalar(sumRadius0 - d);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 2));\n pos.sub(correction);\n vel.sub(velCorrection);\n }\n }\n if (Math.abs(pos.x) + radius > config.maxX) {\n pos.x = Math.sign(pos.x) * (config.maxX - radius);\n vel.x = -vel.x * config.wallBounce;\n }\n if (config.gravity === 0) {\n if (Math.abs(pos.y) + radius > config.maxY) {\n pos.y = Math.sign(pos.y) * (config.maxY - radius);\n vel.y = -vel.y * config.wallBounce;\n }\n } else if (pos.y - radius < -config.maxY) {\n pos.y = -config.maxY + radius;\n vel.y = -vel.y * config.wallBounce;\n }\n const maxBoundary = Math.max(config.maxZ, config.maxSize);\n if (Math.abs(pos.z) + radius > maxBoundary) {\n pos.z = Math.sign(pos.z) * (config.maxZ - radius);\n vel.z = -vel.z * config.wallBounce;\n }\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n }\n}\n\nclass Y extends MeshPhysicalMaterial {\n uniforms: { [key: string]: { value: any } } = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n defines: { USE_UV: string };\n\n constructor(params: any) {\n super(params);\n this.defines = { USE_UV: '' };\n this.onBeforeCompile = shader => {\n Object.assign(shader.uniforms, this.uniforms);\n shader.fragmentShader =\n `\n uniform float thicknessPower;\n uniform float thicknessScale;\n uniform float thicknessDistortion;\n uniform float thicknessAmbient;\n uniform float thicknessAttenuation;\n ` + shader.fragmentShader;\n shader.fragmentShader = shader.fragmentShader.replace(\n 'void main() {',\n `\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\n #ifdef USE_COLOR\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\n #else\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\n #endif\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\n }\n\n void main() {\n `\n );\n const lightsChunk = ShaderChunk.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n `\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\n `\n );\n shader.fragmentShader = shader.fragmentShader.replace('#include ', lightsChunk);\n if (this.onBeforeCompile2) this.onBeforeCompile2(shader);\n };\n }\n onBeforeCompile2?: (shader: any) => void;\n}\n\nconst XConfig = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 0xffffff,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new Object3D();\n\nlet globalPointerActive = false;\nconst pointerPosition = new Vector2();\n\ninterface PointerData {\n position: Vector2;\n nPosition: Vector2;\n hover: boolean;\n touching: boolean;\n onEnter: (data: PointerData) => void;\n onMove: (data: PointerData) => void;\n onClick: (data: PointerData) => void;\n onLeave: (data: PointerData) => void;\n dispose?: () => void;\n}\n\nconst pointerMap = new Map();\n\nfunction createPointerData(options: Partial & { domElement: HTMLElement }): PointerData {\n const defaultData: PointerData = {\n position: new Vector2(),\n nPosition: new Vector2(),\n hover: false,\n touching: false,\n onEnter: () => {},\n onMove: () => {},\n onClick: () => {},\n onLeave: () => {},\n ...options\n };\n if (!pointerMap.has(options.domElement)) {\n pointerMap.set(options.domElement, defaultData);\n if (!globalPointerActive) {\n document.body.addEventListener('pointermove', onPointerMove as EventListener);\n document.body.addEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.addEventListener('click', onPointerClick as EventListener);\n\n document.body.addEventListener('touchstart', onTouchStart as EventListener, {\n passive: false\n });\n document.body.addEventListener('touchmove', onTouchMove as EventListener, {\n passive: false\n });\n document.body.addEventListener('touchend', onTouchEnd as EventListener, {\n passive: false\n });\n document.body.addEventListener('touchcancel', onTouchEnd as EventListener, {\n passive: false\n });\n globalPointerActive = true;\n }\n }\n defaultData.dispose = () => {\n pointerMap.delete(options.domElement);\n if (pointerMap.size === 0) {\n document.body.removeEventListener('pointermove', onPointerMove as EventListener);\n document.body.removeEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.removeEventListener('click', onPointerClick as EventListener);\n\n document.body.removeEventListener('touchstart', onTouchStart as EventListener);\n document.body.removeEventListener('touchmove', onTouchMove as EventListener);\n document.body.removeEventListener('touchend', onTouchEnd as EventListener);\n document.body.removeEventListener('touchcancel', onTouchEnd as EventListener);\n globalPointerActive = false;\n }\n };\n return defaultData;\n}\n\nfunction onPointerMove(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n processPointerInteraction();\n}\n\nfunction processPointerInteraction() {\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && !data.touching) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction onTouchStart(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n data.touching = true;\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchMove(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) {\n if (!data.hover) {\n data.hover = true;\n data.touching = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && data.touching) {\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchEnd() {\n for (const [, data] of pointerMap) {\n if (data.touching) {\n data.touching = false;\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n }\n}\n\nfunction onPointerClick(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) data.onClick(data);\n }\n}\n\nfunction onPointerLeave() {\n for (const data of pointerMap.values()) {\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction updatePointerData(data: PointerData, rect: DOMRect) {\n data.position.set(pointerPosition.x - rect.left, pointerPosition.y - rect.top);\n data.nPosition.set((data.position.x / rect.width) * 2 - 1, (-data.position.y / rect.height) * 2 + 1);\n}\n\nfunction isInside(rect: DOMRect) {\n return (\n pointerPosition.x >= rect.left &&\n pointerPosition.x <= rect.left + rect.width &&\n pointerPosition.y >= rect.top &&\n pointerPosition.y <= rect.top + rect.height\n );\n}\n\nclass Z extends InstancedMesh {\n config: typeof XConfig;\n physics: W;\n ambientLight: AmbientLight | undefined;\n light: PointLight | undefined;\n\n constructor(renderer: WebGLRenderer, params: Partial = {}) {\n const config = { ...XConfig, ...params };\n const roomEnv = new RoomEnvironment();\n const pmrem = new PMREMGenerator(renderer);\n const envTexture = pmrem.fromScene(roomEnv).texture;\n const geometry = new SphereGeometry();\n const material = new Y({ envMap: envTexture, ...config.materialParams });\n material.envMapRotation.x = -Math.PI / 2;\n super(geometry, material, config.count);\n this.config = config;\n this.physics = new W(config);\n this.#setupLights();\n this.setColors(config.colors);\n }\n\n #setupLights() {\n this.ambientLight = new AmbientLight(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new PointLight(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n\n setColors(colors: number[]) {\n if (Array.isArray(colors) && colors.length > 1) {\n const colorUtils = (function (colorsArr: number[]) {\n let baseColors: number[] = colorsArr;\n let colorObjects: Color[] = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n return {\n setColors: (cols: number[]) => {\n baseColors = cols;\n colorObjects = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n },\n getColorAt: (ratio: number, out: Color = new Color()) => {\n const clamped = Math.max(0, Math.min(1, ratio));\n const scaled = clamped * (baseColors.length - 1);\n const idx = Math.floor(scaled);\n const start = colorObjects[idx];\n if (idx >= baseColors.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = colorObjects[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(colors);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, colorUtils.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light!.color.copy(colorUtils.getColorAt(idx / this.count));\n }\n }\n\n if (!this.instanceColor) return;\n this.instanceColor.needsUpdate = true;\n }\n }\n\n update(deltaInfo: { delta: number }) {\n this.physics.update(deltaInfo);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light!.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\ninterface CreateBallpitReturn {\n three: X;\n spheres: Z;\n setCount: (count: number) => void;\n updateConfig: (newProps: { [key: string]: any }) => void;\n togglePause: () => void;\n dispose: () => void;\n}\n\nfunction createBallpit(canvas: HTMLCanvasElement, config: any = {}): CreateBallpitReturn {\n const threeInstance = new X({\n canvas,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let spheres: Z;\n threeInstance.renderer.toneMapping = ACESFilmicToneMapping;\n threeInstance.camera.position.set(0, 0, 20);\n threeInstance.camera.lookAt(0, 0, 0);\n threeInstance.cameraMaxAspect = 1.5;\n threeInstance.resize();\n initialize(config);\n const raycaster = new Raycaster();\n const plane = new Plane(new Vector3(0, 0, 1), 0);\n const intersectionPoint = new Vector3();\n let isPaused = false;\n\n canvas.style.touchAction = 'none';\n canvas.style.userSelect = 'none';\n (canvas.style as any).webkitUserSelect = 'none';\n\n const pointerData = createPointerData({\n domElement: canvas,\n onMove() {\n raycaster.setFromCamera(pointerData.nPosition, threeInstance.camera);\n threeInstance.camera.getWorldDirection(plane.normal);\n raycaster.ray.intersectPlane(plane, intersectionPoint);\n spheres.physics.center.copy(intersectionPoint);\n spheres.config.controlSphere0 = true;\n },\n onLeave() {\n spheres.config.controlSphere0 = false;\n }\n });\n function initialize(cfg: any) {\n if (spheres) {\n threeInstance.clear();\n threeInstance.scene.remove(spheres);\n }\n spheres = new Z(threeInstance.renderer, cfg);\n threeInstance.scene.add(spheres);\n }\n threeInstance.onBeforeRender = deltaInfo => {\n if (!isPaused) spheres.update(deltaInfo);\n };\n threeInstance.onAfterResize = size => {\n spheres.config.maxX = size.wWidth / 2;\n spheres.config.maxY = size.wHeight / 2;\n };\n return {\n three: threeInstance,\n get spheres() {\n return spheres;\n },\n setCount(count: number) {\n initialize({ ...spheres.config, count });\n },\n updateConfig(newProps: { [key: string]: any }) {\n if (newProps.count !== undefined && newProps.count !== spheres.config.count) {\n initialize({ ...spheres.config, ...newProps });\n } else {\n Object.assign(spheres.config, newProps);\n if (newProps.colors) {\n spheres.setColors(spheres.config.colors);\n }\n if (newProps.minSize !== undefined || newProps.maxSize !== undefined || newProps.size0 !== undefined) {\n spheres.physics.setSizes();\n }\n }\n },\n togglePause() {\n isPaused = !isPaused;\n },\n dispose() {\n pointerData.dispose?.();\n threeInstance.dispose();\n }\n };\n}\n\ninterface BallpitProps {\n className?: string;\n followCursor?: boolean;\n [key: string]: any;\n}\n\nconst Ballpit: React.FC = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n const isFirstRender = useRef(true);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, {\n followCursor,\n ...props\n });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n spheresInstanceRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (isFirstRender.current) {\n isFirstRender.current = false;\n return;\n }\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.updateConfig({ followCursor, ...props });\n }\n }, [props, followCursor]);\n\n return ;\n};\n\nexport default Ballpit;\n" + "content": "import { gsap } from 'gsap';\nimport { Observer } from 'gsap/Observer';\nimport React, { useEffect, useRef } from 'react';\nimport {\n ACESFilmicToneMapping,\n AmbientLight,\n Color,\n InstancedMesh,\n MathUtils,\n MeshPhysicalMaterial,\n Object3D,\n PerspectiveCamera,\n Plane,\n PMREMGenerator,\n PointLight,\n Raycaster,\n Scene,\n ShaderChunk,\n SphereGeometry,\n SRGBColorSpace,\n Timer,\n Vector2,\n Vector3,\n WebGLRenderer,\n type WebGLRendererParameters\n} from 'three';\nimport { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';\n\ngsap.registerPlugin(Observer);\n\ninterface XConfig {\n canvas?: HTMLCanvasElement;\n id?: string;\n rendererOptions?: Partial;\n size?: 'parent' | { width: number; height: number };\n}\n\ninterface SizeData {\n width: number;\n height: number;\n wWidth: number;\n wHeight: number;\n ratio: number;\n pixelRatio: number;\n}\n\nclass X {\n #config: XConfig;\n #postprocessing: any;\n #resizeObserver?: ResizeObserver;\n #intersectionObserver?: IntersectionObserver;\n #resizeTimer?: number;\n #animationFrameId: number = 0;\n #timer: Timer = new Timer();\n #animationState = { elapsed: 0, delta: 0 };\n #isAnimating: boolean = false;\n #isVisible: boolean = false;\n // Bind once: `.bind()` returns a new function on every call, so binding again\n // in the teardown would hand removeEventListener a function that was never\n // registered, leaving the listener attached for the lifetime of the page.\n #boundResize = this.#onResize.bind(this);\n #boundVisibilityChange = this.#onVisibilityChange.bind(this);\n\n canvas!: HTMLCanvasElement;\n camera!: PerspectiveCamera;\n cameraMinAspect?: number;\n cameraMaxAspect?: number;\n cameraFov!: number;\n maxPixelRatio?: number;\n minPixelRatio?: number;\n scene!: Scene;\n renderer!: WebGLRenderer;\n size: SizeData = {\n width: 0,\n height: 0,\n wWidth: 0,\n wHeight: 0,\n ratio: 0,\n pixelRatio: 0\n };\n\n render: () => void = this.#render.bind(this);\n onBeforeRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterResize: (size: SizeData) => void = () => {};\n isDisposed: boolean = false;\n\n constructor(config: XConfig) {\n this.#config = { ...config };\n this.#initCamera();\n this.#initScene();\n this.#initRenderer();\n this.resize();\n this.#initObservers();\n }\n\n #initCamera() {\n this.camera = new PerspectiveCamera();\n this.cameraFov = this.camera.fov;\n }\n\n #initScene() {\n this.scene = new Scene();\n }\n\n #initRenderer() {\n if (this.#config.canvas) {\n this.canvas = this.#config.canvas;\n } else if (this.#config.id) {\n const elem = document.getElementById(this.#config.id);\n if (elem instanceof HTMLCanvasElement) {\n this.canvas = elem;\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas!.style.display = 'block';\n const rendererOptions: WebGLRendererParameters = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#config.rendererOptions ?? {})\n };\n this.renderer = new WebGLRenderer(rendererOptions);\n this.renderer.outputColorSpace = SRGBColorSpace;\n }\n\n #initObservers() {\n if (!(this.#config.size instanceof Object)) {\n window.addEventListener('resize', this.#boundResize);\n if (this.#config.size === 'parent' && this.canvas.parentNode) {\n this.#resizeObserver = new ResizeObserver(this.#onResize.bind(this));\n this.#resizeObserver.observe(this.canvas.parentNode as Element);\n }\n }\n this.#intersectionObserver = new IntersectionObserver(this.#onIntersection.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#intersectionObserver.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#boundVisibilityChange);\n }\n\n #onResize() {\n if (this.#resizeTimer) clearTimeout(this.#resizeTimer);\n this.#resizeTimer = window.setTimeout(this.resize.bind(this), 100);\n }\n\n resize() {\n let w: number, h: number;\n if (this.#config.size instanceof Object) {\n w = this.#config.size.width;\n h = this.#config.size.height;\n } else if (this.#config.size === 'parent' && this.canvas.parentNode) {\n w = (this.canvas.parentNode as HTMLElement).offsetWidth;\n h = (this.canvas.parentNode as HTMLElement).offsetHeight;\n } else {\n w = window.innerWidth;\n h = window.innerHeight;\n }\n this.size.width = w;\n this.size.height = h;\n this.size.ratio = w / h;\n this.#updateCamera();\n this.#updateRenderer();\n this.onAfterResize(this.size);\n }\n\n #updateCamera() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#adjustFov(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#adjustFov(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n\n #adjustFov(aspect: number) {\n const tanFov = Math.tan(MathUtils.degToRad(this.cameraFov / 2));\n const newTan = tanFov / (this.camera.aspect / aspect);\n this.camera.fov = 2 * MathUtils.radToDeg(Math.atan(newTan));\n }\n\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const fovRad = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(fovRad / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if ((this.camera as any).isOrthographicCamera) {\n const cam = this.camera as any;\n this.size.wHeight = cam.top - cam.bottom;\n this.size.wWidth = cam.right - cam.left;\n }\n }\n\n #updateRenderer() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#postprocessing?.setSize(this.size.width, this.size.height);\n let pr = window.devicePixelRatio;\n if (this.maxPixelRatio && pr > this.maxPixelRatio) {\n pr = this.maxPixelRatio;\n } else if (this.minPixelRatio && pr < this.minPixelRatio) {\n pr = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(pr);\n this.size.pixelRatio = pr;\n }\n\n get postprocessing() {\n return this.#postprocessing;\n }\n set postprocessing(value: any) {\n this.#postprocessing = value;\n this.render = value.render.bind(value);\n }\n\n #onIntersection(entries: IntersectionObserverEntry[]) {\n this.#isAnimating = entries[0].isIntersecting;\n this.#isAnimating ? this.#startAnimation() : this.#stopAnimation();\n }\n\n #onVisibilityChange() {\n if (this.#isAnimating) {\n document.hidden ? this.#stopAnimation() : this.#startAnimation();\n }\n }\n\n #startAnimation() {\n if (this.#isVisible) return;\n const animateFrame = () => {\n this.#animationFrameId = requestAnimationFrame(animateFrame);\n this.#timer.update();\n this.#animationState.delta = this.#timer.getDelta();\n this.#animationState.elapsed += this.#animationState.delta;\n this.onBeforeRender(this.#animationState);\n this.render();\n this.onAfterRender(this.#animationState);\n };\n this.#isVisible = true;\n this.#timer.reset();\n animateFrame();\n }\n\n #stopAnimation() {\n if (this.#isVisible) {\n cancelAnimationFrame(this.#animationFrameId);\n this.#isVisible = false;\n }\n }\n\n #render() {\n this.renderer.render(this.scene, this.camera);\n }\n\n clear() {\n this.scene.traverse(obj => {\n if ((obj as any).isMesh && typeof (obj as any).material === 'object' && (obj as any).material !== null) {\n Object.keys((obj as any).material).forEach(key => {\n const matProp = (obj as any).material[key];\n if (matProp && typeof matProp === 'object' && typeof matProp.dispose === 'function') {\n matProp.dispose();\n }\n });\n (obj as any).material.dispose();\n (obj as any).geometry.dispose();\n }\n });\n this.scene.clear();\n }\n\n dispose() {\n this.#onResizeCleanup();\n this.#stopAnimation();\n this.#timer.dispose();\n this.clear();\n this.#postprocessing?.dispose();\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n this.isDisposed = true;\n }\n\n #onResizeCleanup() {\n window.removeEventListener('resize', this.#boundResize);\n this.#resizeObserver?.disconnect();\n this.#intersectionObserver?.disconnect();\n document.removeEventListener('visibilitychange', this.#boundVisibilityChange);\n }\n}\n\ninterface WConfig {\n count: number;\n maxX: number;\n maxY: number;\n maxZ: number;\n maxSize: number;\n minSize: number;\n size0: number;\n gravity: number;\n friction: number;\n wallBounce: number;\n maxVelocity: number;\n controlSphere0?: boolean;\n followCursor?: boolean;\n}\n\nclass W {\n config: WConfig;\n positionData: Float32Array;\n velocityData: Float32Array;\n sizeData: Float32Array;\n center: Vector3 = new Vector3();\n\n constructor(config: WConfig) {\n this.config = config;\n this.positionData = new Float32Array(3 * config.count).fill(0);\n this.velocityData = new Float32Array(3 * config.count).fill(0);\n this.sizeData = new Float32Array(config.count).fill(1);\n this.center = new Vector3();\n this.#initializePositions();\n this.setSizes();\n }\n\n #initializePositions() {\n const { config, positionData } = this;\n this.center.toArray(positionData, 0);\n for (let i = 1; i < config.count; i++) {\n const idx = 3 * i;\n positionData[idx] = MathUtils.randFloatSpread(2 * config.maxX);\n positionData[idx + 1] = MathUtils.randFloatSpread(2 * config.maxY);\n positionData[idx + 2] = MathUtils.randFloatSpread(2 * config.maxZ);\n }\n }\n\n setSizes() {\n const { config, sizeData } = this;\n sizeData[0] = config.size0;\n for (let i = 1; i < config.count; i++) {\n sizeData[i] = MathUtils.randFloat(config.minSize, config.maxSize);\n }\n }\n\n update(deltaInfo: { delta: number }) {\n const { config, center, positionData, sizeData, velocityData } = this;\n let startIdx = 0;\n if (config.controlSphere0) {\n startIdx = 1;\n const firstVec = new Vector3().fromArray(positionData, 0);\n firstVec.lerp(center, 0.1).toArray(positionData, 0);\n new Vector3(0, 0, 0).toArray(velocityData, 0);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n vel.y -= deltaInfo.delta * config.gravity * sizeData[idx];\n vel.multiplyScalar(config.friction);\n vel.clampLength(0, config.maxVelocity);\n pos.add(vel);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n const radius = sizeData[idx];\n for (let jdx = idx + 1; jdx < config.count; jdx++) {\n const otherBase = 3 * jdx;\n const otherPos = new Vector3().fromArray(positionData, otherBase);\n const otherVel = new Vector3().fromArray(velocityData, otherBase);\n const diff = new Vector3().copy(otherPos).sub(pos);\n const dist = diff.length();\n const sumRadius = radius + sizeData[jdx];\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n const correction = diff.normalize().multiplyScalar(0.5 * overlap);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 1));\n pos.sub(correction);\n vel.sub(velCorrection);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n otherPos.add(correction);\n otherVel.add(correction.clone().multiplyScalar(Math.max(otherVel.length(), 1)));\n otherPos.toArray(positionData, otherBase);\n otherVel.toArray(velocityData, otherBase);\n }\n }\n if (config.controlSphere0) {\n const diff = new Vector3().copy(new Vector3().fromArray(positionData, 0)).sub(pos);\n const d = diff.length();\n const sumRadius0 = radius + sizeData[0];\n if (d < sumRadius0) {\n const correction = diff.normalize().multiplyScalar(sumRadius0 - d);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 2));\n pos.sub(correction);\n vel.sub(velCorrection);\n }\n }\n if (Math.abs(pos.x) + radius > config.maxX) {\n pos.x = Math.sign(pos.x) * (config.maxX - radius);\n vel.x = -vel.x * config.wallBounce;\n }\n if (config.gravity === 0) {\n if (Math.abs(pos.y) + radius > config.maxY) {\n pos.y = Math.sign(pos.y) * (config.maxY - radius);\n vel.y = -vel.y * config.wallBounce;\n }\n } else if (pos.y - radius < -config.maxY) {\n pos.y = -config.maxY + radius;\n vel.y = -vel.y * config.wallBounce;\n }\n const maxBoundary = Math.max(config.maxZ, config.maxSize);\n if (Math.abs(pos.z) + radius > maxBoundary) {\n pos.z = Math.sign(pos.z) * (config.maxZ - radius);\n vel.z = -vel.z * config.wallBounce;\n }\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n }\n}\n\nclass Y extends MeshPhysicalMaterial {\n uniforms: { [key: string]: { value: any } } = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n defines: { USE_UV: string };\n\n constructor(params: any) {\n super(params);\n this.defines = { USE_UV: '' };\n this.onBeforeCompile = shader => {\n Object.assign(shader.uniforms, this.uniforms);\n shader.fragmentShader =\n `\n uniform float thicknessPower;\n uniform float thicknessScale;\n uniform float thicknessDistortion;\n uniform float thicknessAmbient;\n uniform float thicknessAttenuation;\n ` + shader.fragmentShader;\n shader.fragmentShader = shader.fragmentShader.replace(\n 'void main() {',\n `\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\n #ifdef USE_COLOR\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\n #else\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\n #endif\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\n }\n\n void main() {\n `\n );\n const lightsChunk = ShaderChunk.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n `\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\n `\n );\n shader.fragmentShader = shader.fragmentShader.replace('#include ', lightsChunk);\n if (this.onBeforeCompile2) this.onBeforeCompile2(shader);\n };\n }\n onBeforeCompile2?: (shader: any) => void;\n}\n\nconst XConfig = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 0xffffff,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new Object3D();\n\nlet globalPointerActive = false;\nconst pointerPosition = new Vector2();\n\ninterface PointerData {\n position: Vector2;\n nPosition: Vector2;\n hover: boolean;\n touching: boolean;\n onEnter: (data: PointerData) => void;\n onMove: (data: PointerData) => void;\n onClick: (data: PointerData) => void;\n onLeave: (data: PointerData) => void;\n dispose?: () => void;\n}\n\nconst pointerMap = new Map();\n\nfunction createPointerData(options: Partial & { domElement: HTMLElement }): PointerData {\n const defaultData: PointerData = {\n position: new Vector2(),\n nPosition: new Vector2(),\n hover: false,\n touching: false,\n onEnter: () => {},\n onMove: () => {},\n onClick: () => {},\n onLeave: () => {},\n ...options\n };\n if (!pointerMap.has(options.domElement)) {\n pointerMap.set(options.domElement, defaultData);\n if (!globalPointerActive) {\n document.body.addEventListener('pointermove', onPointerMove as EventListener);\n document.body.addEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.addEventListener('click', onPointerClick as EventListener);\n\n document.body.addEventListener('touchstart', onTouchStart as EventListener, {\n passive: false\n });\n document.body.addEventListener('touchmove', onTouchMove as EventListener, {\n passive: false\n });\n document.body.addEventListener('touchend', onTouchEnd as EventListener, {\n passive: false\n });\n document.body.addEventListener('touchcancel', onTouchEnd as EventListener, {\n passive: false\n });\n globalPointerActive = true;\n }\n }\n defaultData.dispose = () => {\n pointerMap.delete(options.domElement);\n if (pointerMap.size === 0) {\n document.body.removeEventListener('pointermove', onPointerMove as EventListener);\n document.body.removeEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.removeEventListener('click', onPointerClick as EventListener);\n\n document.body.removeEventListener('touchstart', onTouchStart as EventListener);\n document.body.removeEventListener('touchmove', onTouchMove as EventListener);\n document.body.removeEventListener('touchend', onTouchEnd as EventListener);\n document.body.removeEventListener('touchcancel', onTouchEnd as EventListener);\n globalPointerActive = false;\n }\n };\n return defaultData;\n}\n\nfunction onPointerMove(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n processPointerInteraction();\n}\n\nfunction processPointerInteraction() {\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && !data.touching) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction onTouchStart(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n data.touching = true;\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchMove(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) {\n if (!data.hover) {\n data.hover = true;\n data.touching = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && data.touching) {\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchEnd() {\n for (const [, data] of pointerMap) {\n if (data.touching) {\n data.touching = false;\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n }\n}\n\nfunction onPointerClick(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) data.onClick(data);\n }\n}\n\nfunction onPointerLeave() {\n for (const data of pointerMap.values()) {\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction updatePointerData(data: PointerData, rect: DOMRect) {\n data.position.set(pointerPosition.x - rect.left, pointerPosition.y - rect.top);\n data.nPosition.set((data.position.x / rect.width) * 2 - 1, (-data.position.y / rect.height) * 2 + 1);\n}\n\nfunction isInside(rect: DOMRect) {\n return (\n pointerPosition.x >= rect.left &&\n pointerPosition.x <= rect.left + rect.width &&\n pointerPosition.y >= rect.top &&\n pointerPosition.y <= rect.top + rect.height\n );\n}\n\nclass Z extends InstancedMesh {\n config: typeof XConfig;\n physics: W;\n ambientLight: AmbientLight | undefined;\n light: PointLight | undefined;\n\n constructor(renderer: WebGLRenderer, params: Partial = {}) {\n const config = { ...XConfig, ...params };\n const roomEnv = new RoomEnvironment();\n const pmrem = new PMREMGenerator(renderer);\n const envTexture = pmrem.fromScene(roomEnv).texture;\n const geometry = new SphereGeometry();\n const material = new Y({ envMap: envTexture, ...config.materialParams });\n material.envMapRotation.x = -Math.PI / 2;\n super(geometry, material, config.count);\n this.config = config;\n this.physics = new W(config);\n this.#setupLights();\n this.setColors(config.colors);\n }\n\n #setupLights() {\n this.ambientLight = new AmbientLight(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new PointLight(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n\n setColors(colors: number[]) {\n if (Array.isArray(colors) && colors.length > 1) {\n const colorUtils = (function (colorsArr: number[]) {\n let baseColors: number[] = colorsArr;\n let colorObjects: Color[] = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n return {\n setColors: (cols: number[]) => {\n baseColors = cols;\n colorObjects = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n },\n getColorAt: (ratio: number, out: Color = new Color()) => {\n const clamped = Math.max(0, Math.min(1, ratio));\n const scaled = clamped * (baseColors.length - 1);\n const idx = Math.floor(scaled);\n const start = colorObjects[idx];\n if (idx >= baseColors.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = colorObjects[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(colors);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, colorUtils.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light!.color.copy(colorUtils.getColorAt(idx / this.count));\n }\n }\n\n if (!this.instanceColor) return;\n this.instanceColor.needsUpdate = true;\n }\n }\n\n update(deltaInfo: { delta: number }) {\n this.physics.update(deltaInfo);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light!.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\ninterface CreateBallpitReturn {\n three: X;\n spheres: Z;\n setCount: (count: number) => void;\n updateConfig: (newProps: { [key: string]: any }) => void;\n togglePause: () => void;\n dispose: () => void;\n}\n\nfunction createBallpit(canvas: HTMLCanvasElement, config: any = {}): CreateBallpitReturn {\n const threeInstance = new X({\n canvas,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let spheres: Z;\n threeInstance.renderer.toneMapping = ACESFilmicToneMapping;\n threeInstance.camera.position.set(0, 0, 20);\n threeInstance.camera.lookAt(0, 0, 0);\n threeInstance.cameraMaxAspect = 1.5;\n threeInstance.resize();\n initialize(config);\n const raycaster = new Raycaster();\n const plane = new Plane(new Vector3(0, 0, 1), 0);\n const intersectionPoint = new Vector3();\n let isPaused = false;\n\n canvas.style.touchAction = 'none';\n canvas.style.userSelect = 'none';\n (canvas.style as any).webkitUserSelect = 'none';\n\n const pointerData = createPointerData({\n domElement: canvas,\n onMove() {\n raycaster.setFromCamera(pointerData.nPosition, threeInstance.camera);\n threeInstance.camera.getWorldDirection(plane.normal);\n raycaster.ray.intersectPlane(plane, intersectionPoint);\n spheres.physics.center.copy(intersectionPoint);\n spheres.config.controlSphere0 = true;\n },\n onLeave() {\n spheres.config.controlSphere0 = false;\n }\n });\n function initialize(cfg: any) {\n if (spheres) {\n threeInstance.clear();\n threeInstance.scene.remove(spheres);\n }\n spheres = new Z(threeInstance.renderer, cfg);\n threeInstance.scene.add(spheres);\n }\n threeInstance.onBeforeRender = deltaInfo => {\n if (!isPaused) spheres.update(deltaInfo);\n };\n threeInstance.onAfterResize = size => {\n spheres.config.maxX = size.wWidth / 2;\n spheres.config.maxY = size.wHeight / 2;\n };\n return {\n three: threeInstance,\n get spheres() {\n return spheres;\n },\n setCount(count: number) {\n initialize({ ...spheres.config, count });\n },\n updateConfig(newProps: { [key: string]: any }) {\n if (newProps.count !== undefined && newProps.count !== spheres.config.count) {\n initialize({ ...spheres.config, ...newProps });\n } else {\n Object.assign(spheres.config, newProps);\n if (newProps.colors) {\n spheres.setColors(spheres.config.colors);\n }\n if (newProps.minSize !== undefined || newProps.maxSize !== undefined || newProps.size0 !== undefined) {\n spheres.physics.setSizes();\n }\n }\n },\n togglePause() {\n isPaused = !isPaused;\n },\n dispose() {\n pointerData.dispose?.();\n threeInstance.dispose();\n }\n };\n}\n\ninterface BallpitProps {\n className?: string;\n followCursor?: boolean;\n [key: string]: any;\n}\n\nconst Ballpit: React.FC = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n const isFirstRender = useRef(true);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, {\n followCursor,\n ...props\n });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n spheresInstanceRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (isFirstRender.current) {\n isFirstRender.current = false;\n return;\n }\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.updateConfig({ followCursor, ...props });\n }\n }, [props, followCursor]);\n\n return ;\n};\n\nexport default Ballpit;\n" } ], "registryDependencies": [], diff --git a/public/r/Beams-JS-CSS.json b/public/r/Beams-JS-CSS.json index d1362e96..833aae0b 100644 --- a/public/r/Beams-JS-CSS.json +++ b/public/r/Beams-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Beams/Beams.css", + "type": "registry:file", + "path": "Beams.css", + "target": "@components/Beams.css", "content": ".beams-container {\n position: relative;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "Beams/Beams.jsx", + "path": "Beams.jsx", "content": "/* eslint-disable react/no-unknown-property */\nimport { forwardRef, useImperativeHandle, useEffect, useRef, useMemo } from 'react';\n\nimport * as THREE from 'three';\n\nimport { Canvas, useFrame } from '@react-three/fiber';\nimport { PerspectiveCamera } from '@react-three/drei';\nimport { degToRad } from 'three/src/math/MathUtils.js';\n\nimport './Beams.css';\n\nfunction extendMaterial(BaseMaterial, cfg) {\n const physical = THREE.ShaderLib.physical;\n const { vertexShader: baseVert, fragmentShader: baseFrag, uniforms: baseUniforms } = physical;\n const baseDefines = physical.defines ?? {};\n\n const uniforms = THREE.UniformsUtils.clone(baseUniforms);\n\n const defaults = new BaseMaterial(cfg.material || {});\n\n if (defaults.color) uniforms.diffuse.value = defaults.color;\n if ('roughness' in defaults) uniforms.roughness.value = defaults.roughness;\n if ('metalness' in defaults) uniforms.metalness.value = defaults.metalness;\n if ('envMap' in defaults) uniforms.envMap.value = defaults.envMap;\n if ('envMapIntensity' in defaults) uniforms.envMapIntensity.value = defaults.envMapIntensity;\n\n Object.entries(cfg.uniforms ?? {}).forEach(([key, u]) => {\n uniforms[key] = u !== null && typeof u === 'object' && 'value' in u ? u : { value: u };\n });\n\n let vert = `${cfg.header}\\n${cfg.vertexHeader ?? ''}\\n${baseVert}`;\n let frag = `${cfg.header}\\n${cfg.fragmentHeader ?? ''}\\n${baseFrag}`;\n\n for (const [inc, code] of Object.entries(cfg.vertex ?? {})) {\n vert = vert.replace(inc, `${inc}\\n${code}`);\n }\n for (const [inc, code] of Object.entries(cfg.fragment ?? {})) {\n frag = frag.replace(inc, `${inc}\\n${code}`);\n }\n\n const mat = new THREE.ShaderMaterial({\n defines: { ...baseDefines },\n uniforms,\n vertexShader: vert,\n fragmentShader: frag,\n lights: true,\n fog: !!cfg.material?.fog\n });\n\n return mat;\n}\n\nconst CanvasWrapper = ({ children }) => (\n \n {children}\n \n);\n\nconst hexToNormalizedRGB = hex => {\n const clean = hex.replace('#', '');\n const r = parseInt(clean.substring(0, 2), 16);\n const g = parseInt(clean.substring(2, 4), 16);\n const b = parseInt(clean.substring(4, 6), 16);\n return [r / 255, g / 255, b / 255];\n};\n\nconst noise = `\nfloat random (in vec2 st) {\n return fract(sin(dot(st.xy,\n vec2(12.9898,78.233)))*\n 43758.5453123);\n}\nfloat noise (in vec2 st) {\n vec2 i = floor(st);\n vec2 f = fract(st);\n float a = random(i);\n float b = random(i + vec2(1.0, 0.0));\n float c = random(i + vec2(0.0, 1.0));\n float d = random(i + vec2(1.0, 1.0));\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(a, b, u.x) +\n (c - a)* u.y * (1.0 - u.x) +\n (d - b) * u.x * u.y;\n}\nvec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}\nvec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}\nvec3 fade(vec3 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);}\nfloat cnoise(vec3 P){\n vec3 Pi0 = floor(P);\n vec3 Pi1 = Pi0 + vec3(1.0);\n Pi0 = mod(Pi0, 289.0);\n Pi1 = mod(Pi1, 289.0);\n vec3 Pf0 = fract(P);\n vec3 Pf1 = Pf0 - vec3(1.0);\n vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);\n vec4 iy = vec4(Pi0.yy, Pi1.yy);\n vec4 iz0 = Pi0.zzzz;\n vec4 iz1 = Pi1.zzzz;\n vec4 ixy = permute(permute(ix) + iy);\n vec4 ixy0 = permute(ixy + iz0);\n vec4 ixy1 = permute(ixy + iz1);\n vec4 gx0 = ixy0 / 7.0;\n vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5;\n gx0 = fract(gx0);\n vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);\n vec4 sz0 = step(gz0, vec4(0.0));\n gx0 -= sz0 * (step(0.0, gx0) - 0.5);\n gy0 -= sz0 * (step(0.0, gy0) - 0.5);\n vec4 gx1 = ixy1 / 7.0;\n vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5;\n gx1 = fract(gx1);\n vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);\n vec4 sz1 = step(gz1, vec4(0.0));\n gx1 -= sz1 * (step(0.0, gx1) - 0.5);\n gy1 -= sz1 * (step(0.0, gy1) - 0.5);\n vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);\n vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);\n vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);\n vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);\n vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);\n vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);\n vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);\n vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);\n vec4 norm0 = taylorInvSqrt(vec4(dot(g000,g000),dot(g010,g010),dot(g100,g100),dot(g110,g110)));\n g000 *= norm0.x; g010 *= norm0.y; g100 *= norm0.z; g110 *= norm0.w;\n vec4 norm1 = taylorInvSqrt(vec4(dot(g001,g001),dot(g011,g011),dot(g101,g101),dot(g111,g111)));\n g001 *= norm1.x; g011 *= norm1.y; g101 *= norm1.z; g111 *= norm1.w;\n float n000 = dot(g000, Pf0);\n float n100 = dot(g100, vec3(Pf1.x,Pf0.yz));\n float n010 = dot(g010, vec3(Pf0.x,Pf1.y,Pf0.z));\n float n110 = dot(g110, vec3(Pf1.xy,Pf0.z));\n float n001 = dot(g001, vec3(Pf0.xy,Pf1.z));\n float n101 = dot(g101, vec3(Pf1.x,Pf0.y,Pf1.z));\n float n011 = dot(g011, vec3(Pf0.x,Pf1.yz));\n float n111 = dot(g111, Pf1);\n vec3 fade_xyz = fade(Pf0);\n vec4 n_z = mix(vec4(n000,n100,n010,n110),vec4(n001,n101,n011,n111),fade_xyz.z);\n vec2 n_yz = mix(n_z.xy,n_z.zw,fade_xyz.y);\n float n_xyz = mix(n_yz.x,n_yz.y,fade_xyz.x);\n return 2.2 * n_xyz;\n}\n`;\n\nconst Beams = ({\n beamWidth = 2,\n beamHeight = 15,\n beamNumber = 12,\n lightColor = '#ffffff',\n speed = 2,\n noiseIntensity = 1.75,\n scale = 0.2,\n rotation = 0\n}) => {\n const meshRef = useRef(null);\n const beamMaterial = useMemo(\n () =>\n extendMaterial(THREE.MeshStandardMaterial, {\n header: `\n varying vec3 vEye;\n varying float vNoise;\n varying vec2 vUv;\n varying vec3 vPosition;\n uniform float time;\n uniform float uSpeed;\n uniform float uNoiseIntensity;\n uniform float uScale;\n ${noise}`,\n vertexHeader: `\n float getPos(vec3 pos) {\n vec3 noisePos =\n vec3(pos.x * 0., pos.y - uv.y, pos.z + time * uSpeed * 3.) * uScale;\n return cnoise(noisePos);\n }\n vec3 getCurrentPos(vec3 pos) {\n vec3 newpos = pos;\n newpos.z += getPos(pos);\n return newpos;\n }\n vec3 getNormal(vec3 pos) {\n vec3 curpos = getCurrentPos(pos);\n vec3 nextposX = getCurrentPos(pos + vec3(0.01, 0.0, 0.0));\n vec3 nextposZ = getCurrentPos(pos + vec3(0.0, -0.01, 0.0));\n vec3 tangentX = normalize(nextposX - curpos);\n vec3 tangentZ = normalize(nextposZ - curpos);\n return normalize(cross(tangentZ, tangentX));\n }`,\n fragmentHeader: '',\n vertex: {\n '#include ': `transformed.z += getPos(transformed.xyz);`,\n '#include ': `objectNormal = getNormal(position.xyz);`\n },\n fragment: {\n '#include ': `\n float randomNoise = noise(gl_FragCoord.xy);\n gl_FragColor.rgb -= randomNoise / 15. * uNoiseIntensity;`\n },\n material: { fog: true },\n uniforms: {\n diffuse: new THREE.Color(...hexToNormalizedRGB('#000000')),\n time: { shared: true, mixed: true, linked: true, value: 0 },\n roughness: 0.3,\n metalness: 0.3,\n uSpeed: { shared: true, mixed: true, linked: true, value: speed },\n envMapIntensity: 10,\n uNoiseIntensity: noiseIntensity,\n uScale: scale\n }\n }),\n [speed, noiseIntensity, scale]\n );\n\n return (\n \n \n \n \n \n \n \n \n \n );\n};\n\nfunction createStackedPlanesBufferGeometry(n, width, height, spacing, heightSegments) {\n const geometry = new THREE.BufferGeometry();\n const numVertices = n * (heightSegments + 1) * 2;\n const numFaces = n * heightSegments * 2;\n const positions = new Float32Array(numVertices * 3);\n const indices = new Uint32Array(numFaces * 3);\n const uvs = new Float32Array(numVertices * 2);\n\n let vertexOffset = 0;\n let indexOffset = 0;\n let uvOffset = 0;\n const totalWidth = n * width + (n - 1) * spacing;\n const xOffsetBase = -totalWidth / 2;\n\n for (let i = 0; i < n; i++) {\n const xOffset = xOffsetBase + i * (width + spacing);\n const uvXOffset = Math.random() * 300;\n const uvYOffset = Math.random() * 300;\n\n for (let j = 0; j <= heightSegments; j++) {\n const y = height * (j / heightSegments - 0.5);\n const v0 = [xOffset, y, 0];\n const v1 = [xOffset + width, y, 0];\n positions.set([...v0, ...v1], vertexOffset * 3);\n\n const uvY = j / heightSegments;\n uvs.set([uvXOffset, uvY + uvYOffset, uvXOffset + 1, uvY + uvYOffset], uvOffset);\n\n if (j < heightSegments) {\n const a = vertexOffset,\n b = vertexOffset + 1,\n c = vertexOffset + 2,\n d = vertexOffset + 3;\n indices.set([a, b, c, c, b, d], indexOffset);\n indexOffset += 6;\n }\n vertexOffset += 2;\n uvOffset += 4;\n }\n }\n\n geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));\n geometry.setIndex(new THREE.BufferAttribute(indices, 1));\n geometry.computeVertexNormals();\n return geometry;\n}\n\nconst MergedPlanes = forwardRef(({ material, width, count, height }, ref) => {\n const mesh = useRef(null);\n useImperativeHandle(ref, () => mesh.current);\n const geometry = useMemo(\n () => createStackedPlanesBufferGeometry(count, width, height, 0, 100),\n [count, width, height]\n );\n useFrame((_, delta) => {\n mesh.current.material.uniforms.time.value += 0.1 * delta;\n });\n return ;\n});\nMergedPlanes.displayName = 'MergedPlanes';\n\nconst PlaneNoise = forwardRef((props, ref) => (\n \n));\nPlaneNoise.displayName = 'PlaneNoise';\n\nconst DirLight = ({ position, color }) => {\n const dir = useRef(null);\n useEffect(() => {\n if (!dir.current) return;\n const cam = dir.current.shadow.camera;\n if (!cam) return;\n cam.top = 24;\n cam.bottom = -24;\n cam.left = -24;\n cam.right = 24;\n cam.far = 64;\n dir.current.shadow.bias = -0.004;\n }, []);\n return ;\n};\n\nexport default Beams;\n" } ], diff --git a/public/r/Beams-TS-CSS.json b/public/r/Beams-TS-CSS.json index 6e331334..ad877c17 100644 --- a/public/r/Beams-TS-CSS.json +++ b/public/r/Beams-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Beams/Beams.css", + "type": "registry:file", + "path": "Beams.css", + "target": "@components/Beams.css", "content": ".beams-container {\n position: relative;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "Beams/Beams.tsx", - "content": "import { forwardRef, useImperativeHandle, useEffect, useRef, useMemo, FC, ReactNode } from 'react';\n\nimport * as THREE from 'three';\n\nimport { Canvas, useFrame } from '@react-three/fiber';\nimport { PerspectiveCamera } from '@react-three/drei';\nimport { degToRad } from 'three/src/math/MathUtils.js';\n\nimport './Beams.css';\n\ntype UniformValue = THREE.IUniform | unknown;\n\ninterface ExtendMaterialConfig {\n header: string;\n vertexHeader?: string;\n fragmentHeader?: string;\n material?: THREE.MeshPhysicalMaterialParameters & { fog?: boolean };\n uniforms?: Record;\n vertex?: Record;\n fragment?: Record;\n}\n\ntype ShaderWithDefines = THREE.ShaderLibShader & {\n defines?: Record;\n};\n\nfunction extendMaterial(\n BaseMaterial: new (params?: THREE.MaterialParameters) => T,\n cfg: ExtendMaterialConfig\n): THREE.ShaderMaterial {\n const physical = THREE.ShaderLib.physical as ShaderWithDefines;\n const { vertexShader: baseVert, fragmentShader: baseFrag, uniforms: baseUniforms } = physical;\n const baseDefines = physical.defines ?? {};\n\n const uniforms: Record = THREE.UniformsUtils.clone(baseUniforms);\n\n const defaults = new BaseMaterial(cfg.material || {}) as T & {\n color?: THREE.Color;\n roughness?: number;\n metalness?: number;\n envMap?: THREE.Texture;\n envMapIntensity?: number;\n };\n\n if (defaults.color) uniforms.diffuse.value = defaults.color;\n if ('roughness' in defaults) uniforms.roughness.value = defaults.roughness;\n if ('metalness' in defaults) uniforms.metalness.value = defaults.metalness;\n if ('envMap' in defaults) uniforms.envMap.value = defaults.envMap;\n if ('envMapIntensity' in defaults) uniforms.envMapIntensity.value = defaults.envMapIntensity;\n\n Object.entries(cfg.uniforms ?? {}).forEach(([key, u]) => {\n uniforms[key] =\n u !== null && typeof u === 'object' && 'value' in u\n ? (u as THREE.IUniform)\n : ({ value: u } as THREE.IUniform);\n });\n\n let vert = `${cfg.header}\\n${cfg.vertexHeader ?? ''}\\n${baseVert}`;\n let frag = `${cfg.header}\\n${cfg.fragmentHeader ?? ''}\\n${baseFrag}`;\n\n for (const [inc, code] of Object.entries(cfg.vertex ?? {})) {\n vert = vert.replace(inc, `${inc}\\n${code}`);\n }\n for (const [inc, code] of Object.entries(cfg.fragment ?? {})) {\n frag = frag.replace(inc, `${inc}\\n${code}`);\n }\n\n const mat = new THREE.ShaderMaterial({\n defines: { ...baseDefines },\n uniforms,\n vertexShader: vert,\n fragmentShader: frag,\n lights: true,\n fog: !!cfg.material?.fog\n });\n\n return mat;\n}\n\nconst CanvasWrapper: FC<{ children: ReactNode }> = ({ children }) => (\n \n {children}\n \n);\n\nconst hexToNormalizedRGB = (hex: string): [number, number, number] => {\n const clean = hex.replace('#', '');\n const r = parseInt(clean.substring(0, 2), 16);\n const g = parseInt(clean.substring(2, 4), 16);\n const b = parseInt(clean.substring(4, 6), 16);\n return [r / 255, g / 255, b / 255];\n};\n\nconst noise = `\nfloat random (in vec2 st) {\n return fract(sin(dot(st.xy,\n vec2(12.9898,78.233)))*\n 43758.5453123);\n}\nfloat noise (in vec2 st) {\n vec2 i = floor(st);\n vec2 f = fract(st);\n float a = random(i);\n float b = random(i + vec2(1.0, 0.0));\n float c = random(i + vec2(0.0, 1.0));\n float d = random(i + vec2(1.0, 1.0));\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(a, b, u.x) +\n (c - a)* u.y * (1.0 - u.x) +\n (d - b) * u.x * u.y;\n}\nvec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}\nvec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}\nvec3 fade(vec3 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);}\nfloat cnoise(vec3 P){\n vec3 Pi0 = floor(P);\n vec3 Pi1 = Pi0 + vec3(1.0);\n Pi0 = mod(Pi0, 289.0);\n Pi1 = mod(Pi1, 289.0);\n vec3 Pf0 = fract(P);\n vec3 Pf1 = Pf0 - vec3(1.0);\n vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);\n vec4 iy = vec4(Pi0.yy, Pi1.yy);\n vec4 iz0 = Pi0.zzzz;\n vec4 iz1 = Pi1.zzzz;\n vec4 ixy = permute(permute(ix) + iy);\n vec4 ixy0 = permute(ixy + iz0);\n vec4 ixy1 = permute(ixy + iz1);\n vec4 gx0 = ixy0 / 7.0;\n vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5;\n gx0 = fract(gx0);\n vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);\n vec4 sz0 = step(gz0, vec4(0.0));\n gx0 -= sz0 * (step(0.0, gx0) - 0.5);\n gy0 -= sz0 * (step(0.0, gy0) - 0.5);\n vec4 gx1 = ixy1 / 7.0;\n vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5;\n gx1 = fract(gx1);\n vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);\n vec4 sz1 = step(gz1, vec4(0.0));\n gx1 -= sz1 * (step(0.0, gx1) - 0.5);\n gy1 -= sz1 * (step(0.0, gy1) - 0.5);\n vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);\n vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);\n vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);\n vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);\n vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);\n vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);\n vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);\n vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);\n vec4 norm0 = taylorInvSqrt(vec4(dot(g000,g000),dot(g010,g010),dot(g100,g100),dot(g110,g110)));\n g000 *= norm0.x; g010 *= norm0.y; g100 *= norm0.z; g110 *= norm0.w;\n vec4 norm1 = taylorInvSqrt(vec4(dot(g001,g001),dot(g011,g011),dot(g101,g101),dot(g111,g111)));\n g001 *= norm1.x; g011 *= norm1.y; g101 *= norm1.z; g111 *= norm1.w;\n float n000 = dot(g000, Pf0);\n float n100 = dot(g100, vec3(Pf1.x,Pf0.yz));\n float n010 = dot(g010, vec3(Pf0.x,Pf1.y,Pf0.z));\n float n110 = dot(g110, vec3(Pf1.xy,Pf0.z));\n float n001 = dot(g001, vec3(Pf0.xy,Pf1.z));\n float n101 = dot(g101, vec3(Pf1.x,Pf0.y,Pf1.z));\n float n011 = dot(g011, vec3(Pf0.x,Pf1.yz));\n float n111 = dot(g111, Pf1);\n vec3 fade_xyz = fade(Pf0);\n vec4 n_z = mix(vec4(n000,n100,n010,n110),vec4(n001,n101,n011,n111),fade_xyz.z);\n vec2 n_yz = mix(n_z.xy,n_z.zw,fade_xyz.y);\n float n_xyz = mix(n_yz.x,n_yz.y,fade_xyz.x);\n return 2.2 * n_xyz;\n}\n`;\n\ninterface BeamsProps {\n beamWidth?: number;\n beamHeight?: number;\n beamNumber?: number;\n lightColor?: string;\n speed?: number;\n noiseIntensity?: number;\n scale?: number;\n rotation?: number;\n}\n\nconst Beams: FC = ({\n beamWidth = 2,\n beamHeight = 15,\n beamNumber = 12,\n lightColor = '#ffffff',\n speed = 2,\n noiseIntensity = 1.75,\n scale = 0.2,\n rotation = 0\n}) => {\n const meshRef = useRef>(null!);\n\n const beamMaterial = useMemo(\n () =>\n extendMaterial(THREE.MeshStandardMaterial, {\n header: `\n varying vec3 vEye;\n varying float vNoise;\n varying vec2 vUv;\n varying vec3 vPosition;\n uniform float time;\n uniform float uSpeed;\n uniform float uNoiseIntensity;\n uniform float uScale;\n ${noise}`,\n vertexHeader: `\n float getPos(vec3 pos) {\n vec3 noisePos =\n vec3(pos.x * 0., pos.y - uv.y, pos.z + time * uSpeed * 3.) * uScale;\n return cnoise(noisePos);\n }\n vec3 getCurrentPos(vec3 pos) {\n vec3 newpos = pos;\n newpos.z += getPos(pos);\n return newpos;\n }\n vec3 getNormal(vec3 pos) {\n vec3 curpos = getCurrentPos(pos);\n vec3 nextposX = getCurrentPos(pos + vec3(0.01, 0.0, 0.0));\n vec3 nextposZ = getCurrentPos(pos + vec3(0.0, -0.01, 0.0));\n vec3 tangentX = normalize(nextposX - curpos);\n vec3 tangentZ = normalize(nextposZ - curpos);\n return normalize(cross(tangentZ, tangentX));\n }`,\n fragmentHeader: '',\n vertex: {\n '#include ': `transformed.z += getPos(transformed.xyz);`,\n '#include ': `objectNormal = getNormal(position.xyz);`\n },\n fragment: {\n '#include ': `\n float randomNoise = noise(gl_FragCoord.xy);\n gl_FragColor.rgb -= randomNoise / 15. * uNoiseIntensity;`\n },\n material: { fog: true },\n uniforms: {\n diffuse: new THREE.Color(...hexToNormalizedRGB('#000000')),\n time: { shared: true, mixed: true, linked: true, value: 0 },\n roughness: 0.3,\n metalness: 0.3,\n uSpeed: { shared: true, mixed: true, linked: true, value: speed },\n envMapIntensity: 10,\n uNoiseIntensity: noiseIntensity,\n uScale: scale\n }\n }),\n [speed, noiseIntensity, scale]\n );\n\n return (\n \n \n \n \n \n \n \n \n \n );\n};\n\nfunction createStackedPlanesBufferGeometry(\n n: number,\n width: number,\n height: number,\n spacing: number,\n heightSegments: number\n): THREE.BufferGeometry {\n const geometry = new THREE.BufferGeometry();\n const numVertices = n * (heightSegments + 1) * 2;\n const numFaces = n * heightSegments * 2;\n const positions = new Float32Array(numVertices * 3);\n const indices = new Uint32Array(numFaces * 3);\n const uvs = new Float32Array(numVertices * 2);\n\n let vertexOffset = 0;\n let indexOffset = 0;\n let uvOffset = 0;\n const totalWidth = n * width + (n - 1) * spacing;\n const xOffsetBase = -totalWidth / 2;\n\n for (let i = 0; i < n; i++) {\n const xOffset = xOffsetBase + i * (width + spacing);\n const uvXOffset = Math.random() * 300;\n const uvYOffset = Math.random() * 300;\n\n for (let j = 0; j <= heightSegments; j++) {\n const y = height * (j / heightSegments - 0.5);\n const v0 = [xOffset, y, 0];\n const v1 = [xOffset + width, y, 0];\n positions.set([...v0, ...v1], vertexOffset * 3);\n\n const uvY = j / heightSegments;\n uvs.set([uvXOffset, uvY + uvYOffset, uvXOffset + 1, uvY + uvYOffset], uvOffset);\n\n if (j < heightSegments) {\n const a = vertexOffset,\n b = vertexOffset + 1,\n c = vertexOffset + 2,\n d = vertexOffset + 3;\n indices.set([a, b, c, c, b, d], indexOffset);\n indexOffset += 6;\n }\n vertexOffset += 2;\n uvOffset += 4;\n }\n }\n\n geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));\n geometry.setIndex(new THREE.BufferAttribute(indices, 1));\n geometry.computeVertexNormals();\n return geometry;\n}\n\nconst MergedPlanes = forwardRef<\n THREE.Mesh,\n {\n material: THREE.ShaderMaterial;\n width: number;\n count: number;\n height: number;\n }\n>(({ material, width, count, height }, ref) => {\n const mesh = useRef>(null!);\n useImperativeHandle(ref, () => mesh.current);\n const geometry = useMemo(\n () => createStackedPlanesBufferGeometry(count, width, height, 0, 100),\n [count, width, height]\n );\n useFrame((_, delta) => {\n mesh.current.material.uniforms.time.value += 0.1 * delta;\n });\n return ;\n});\nMergedPlanes.displayName = 'MergedPlanes';\n\nconst PlaneNoise = forwardRef<\n THREE.Mesh,\n {\n material: THREE.ShaderMaterial;\n width: number;\n count: number;\n height: number;\n }\n>((props, ref) => (\n \n));\nPlaneNoise.displayName = 'PlaneNoise';\n\nconst DirLight: FC<{ position: [number, number, number]; color: string }> = ({ position, color }) => {\n const dir = useRef(null!);\n useEffect(() => {\n if (!dir.current) return;\n const cam = dir.current.shadow.camera as THREE.Camera & {\n top: number;\n bottom: number;\n left: number;\n right: number;\n far: number;\n };\n cam.top = 24;\n cam.bottom = -24;\n cam.left = -24;\n cam.right = 24;\n cam.far = 64;\n dir.current.shadow.bias = -0.004;\n }, []);\n return ;\n};\n\nexport default Beams;\n" + "path": "Beams.tsx", + "content": "import { forwardRef, useImperativeHandle, useEffect, useRef, useMemo, type FC, type ReactNode } from 'react';\n\nimport * as THREE from 'three';\n\nimport { Canvas, useFrame } from '@react-three/fiber';\nimport { PerspectiveCamera } from '@react-three/drei';\nimport { degToRad } from 'three/src/math/MathUtils.js';\n\nimport './Beams.css';\n\ntype UniformValue = THREE.IUniform | unknown;\n\ninterface ExtendMaterialConfig {\n header: string;\n vertexHeader?: string;\n fragmentHeader?: string;\n material?: THREE.MeshPhysicalMaterialParameters & { fog?: boolean };\n uniforms?: Record;\n vertex?: Record;\n fragment?: Record;\n}\n\ntype ShaderWithDefines = THREE.ShaderLibShader & {\n defines?: Record;\n};\n\nfunction extendMaterial(\n BaseMaterial: new (params?: THREE.MaterialParameters) => T,\n cfg: ExtendMaterialConfig\n): THREE.ShaderMaterial {\n const physical = THREE.ShaderLib.physical as ShaderWithDefines;\n const { vertexShader: baseVert, fragmentShader: baseFrag, uniforms: baseUniforms } = physical;\n const baseDefines = physical.defines ?? {};\n\n const uniforms: Record = THREE.UniformsUtils.clone(baseUniforms);\n\n const defaults = new BaseMaterial(cfg.material || {}) as T & {\n color?: THREE.Color;\n roughness?: number;\n metalness?: number;\n envMap?: THREE.Texture;\n envMapIntensity?: number;\n };\n\n if (defaults.color) uniforms.diffuse.value = defaults.color;\n if ('roughness' in defaults) uniforms.roughness.value = defaults.roughness;\n if ('metalness' in defaults) uniforms.metalness.value = defaults.metalness;\n if ('envMap' in defaults) uniforms.envMap.value = defaults.envMap;\n if ('envMapIntensity' in defaults) uniforms.envMapIntensity.value = defaults.envMapIntensity;\n\n Object.entries(cfg.uniforms ?? {}).forEach(([key, u]) => {\n uniforms[key] =\n u !== null && typeof u === 'object' && 'value' in u\n ? (u as THREE.IUniform)\n : ({ value: u } as THREE.IUniform);\n });\n\n let vert = `${cfg.header}\\n${cfg.vertexHeader ?? ''}\\n${baseVert}`;\n let frag = `${cfg.header}\\n${cfg.fragmentHeader ?? ''}\\n${baseFrag}`;\n\n for (const [inc, code] of Object.entries(cfg.vertex ?? {})) {\n vert = vert.replace(inc, `${inc}\\n${code}`);\n }\n for (const [inc, code] of Object.entries(cfg.fragment ?? {})) {\n frag = frag.replace(inc, `${inc}\\n${code}`);\n }\n\n const mat = new THREE.ShaderMaterial({\n defines: { ...baseDefines },\n uniforms,\n vertexShader: vert,\n fragmentShader: frag,\n lights: true,\n fog: !!cfg.material?.fog\n });\n\n return mat;\n}\n\nconst CanvasWrapper: FC<{ children: ReactNode }> = ({ children }) => (\n \n {children}\n \n);\n\nconst hexToNormalizedRGB = (hex: string): [number, number, number] => {\n const clean = hex.replace('#', '');\n const r = parseInt(clean.substring(0, 2), 16);\n const g = parseInt(clean.substring(2, 4), 16);\n const b = parseInt(clean.substring(4, 6), 16);\n return [r / 255, g / 255, b / 255];\n};\n\nconst noise = `\nfloat random (in vec2 st) {\n return fract(sin(dot(st.xy,\n vec2(12.9898,78.233)))*\n 43758.5453123);\n}\nfloat noise (in vec2 st) {\n vec2 i = floor(st);\n vec2 f = fract(st);\n float a = random(i);\n float b = random(i + vec2(1.0, 0.0));\n float c = random(i + vec2(0.0, 1.0));\n float d = random(i + vec2(1.0, 1.0));\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(a, b, u.x) +\n (c - a)* u.y * (1.0 - u.x) +\n (d - b) * u.x * u.y;\n}\nvec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}\nvec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}\nvec3 fade(vec3 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);}\nfloat cnoise(vec3 P){\n vec3 Pi0 = floor(P);\n vec3 Pi1 = Pi0 + vec3(1.0);\n Pi0 = mod(Pi0, 289.0);\n Pi1 = mod(Pi1, 289.0);\n vec3 Pf0 = fract(P);\n vec3 Pf1 = Pf0 - vec3(1.0);\n vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);\n vec4 iy = vec4(Pi0.yy, Pi1.yy);\n vec4 iz0 = Pi0.zzzz;\n vec4 iz1 = Pi1.zzzz;\n vec4 ixy = permute(permute(ix) + iy);\n vec4 ixy0 = permute(ixy + iz0);\n vec4 ixy1 = permute(ixy + iz1);\n vec4 gx0 = ixy0 / 7.0;\n vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5;\n gx0 = fract(gx0);\n vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);\n vec4 sz0 = step(gz0, vec4(0.0));\n gx0 -= sz0 * (step(0.0, gx0) - 0.5);\n gy0 -= sz0 * (step(0.0, gy0) - 0.5);\n vec4 gx1 = ixy1 / 7.0;\n vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5;\n gx1 = fract(gx1);\n vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);\n vec4 sz1 = step(gz1, vec4(0.0));\n gx1 -= sz1 * (step(0.0, gx1) - 0.5);\n gy1 -= sz1 * (step(0.0, gy1) - 0.5);\n vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);\n vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);\n vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);\n vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);\n vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);\n vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);\n vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);\n vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);\n vec4 norm0 = taylorInvSqrt(vec4(dot(g000,g000),dot(g010,g010),dot(g100,g100),dot(g110,g110)));\n g000 *= norm0.x; g010 *= norm0.y; g100 *= norm0.z; g110 *= norm0.w;\n vec4 norm1 = taylorInvSqrt(vec4(dot(g001,g001),dot(g011,g011),dot(g101,g101),dot(g111,g111)));\n g001 *= norm1.x; g011 *= norm1.y; g101 *= norm1.z; g111 *= norm1.w;\n float n000 = dot(g000, Pf0);\n float n100 = dot(g100, vec3(Pf1.x,Pf0.yz));\n float n010 = dot(g010, vec3(Pf0.x,Pf1.y,Pf0.z));\n float n110 = dot(g110, vec3(Pf1.xy,Pf0.z));\n float n001 = dot(g001, vec3(Pf0.xy,Pf1.z));\n float n101 = dot(g101, vec3(Pf1.x,Pf0.y,Pf1.z));\n float n011 = dot(g011, vec3(Pf0.x,Pf1.yz));\n float n111 = dot(g111, Pf1);\n vec3 fade_xyz = fade(Pf0);\n vec4 n_z = mix(vec4(n000,n100,n010,n110),vec4(n001,n101,n011,n111),fade_xyz.z);\n vec2 n_yz = mix(n_z.xy,n_z.zw,fade_xyz.y);\n float n_xyz = mix(n_yz.x,n_yz.y,fade_xyz.x);\n return 2.2 * n_xyz;\n}\n`;\n\ninterface BeamsProps {\n beamWidth?: number;\n beamHeight?: number;\n beamNumber?: number;\n lightColor?: string;\n speed?: number;\n noiseIntensity?: number;\n scale?: number;\n rotation?: number;\n}\n\nconst Beams: FC = ({\n beamWidth = 2,\n beamHeight = 15,\n beamNumber = 12,\n lightColor = '#ffffff',\n speed = 2,\n noiseIntensity = 1.75,\n scale = 0.2,\n rotation = 0\n}) => {\n const meshRef = useRef>(null!);\n\n const beamMaterial = useMemo(\n () =>\n extendMaterial(THREE.MeshStandardMaterial, {\n header: `\n varying vec3 vEye;\n varying float vNoise;\n varying vec2 vUv;\n varying vec3 vPosition;\n uniform float time;\n uniform float uSpeed;\n uniform float uNoiseIntensity;\n uniform float uScale;\n ${noise}`,\n vertexHeader: `\n float getPos(vec3 pos) {\n vec3 noisePos =\n vec3(pos.x * 0., pos.y - uv.y, pos.z + time * uSpeed * 3.) * uScale;\n return cnoise(noisePos);\n }\n vec3 getCurrentPos(vec3 pos) {\n vec3 newpos = pos;\n newpos.z += getPos(pos);\n return newpos;\n }\n vec3 getNormal(vec3 pos) {\n vec3 curpos = getCurrentPos(pos);\n vec3 nextposX = getCurrentPos(pos + vec3(0.01, 0.0, 0.0));\n vec3 nextposZ = getCurrentPos(pos + vec3(0.0, -0.01, 0.0));\n vec3 tangentX = normalize(nextposX - curpos);\n vec3 tangentZ = normalize(nextposZ - curpos);\n return normalize(cross(tangentZ, tangentX));\n }`,\n fragmentHeader: '',\n vertex: {\n '#include ': `transformed.z += getPos(transformed.xyz);`,\n '#include ': `objectNormal = getNormal(position.xyz);`\n },\n fragment: {\n '#include ': `\n float randomNoise = noise(gl_FragCoord.xy);\n gl_FragColor.rgb -= randomNoise / 15. * uNoiseIntensity;`\n },\n material: { fog: true },\n uniforms: {\n diffuse: new THREE.Color(...hexToNormalizedRGB('#000000')),\n time: { shared: true, mixed: true, linked: true, value: 0 },\n roughness: 0.3,\n metalness: 0.3,\n uSpeed: { shared: true, mixed: true, linked: true, value: speed },\n envMapIntensity: 10,\n uNoiseIntensity: noiseIntensity,\n uScale: scale\n }\n }),\n [speed, noiseIntensity, scale]\n );\n\n return (\n \n \n \n \n \n \n \n \n \n );\n};\n\nfunction createStackedPlanesBufferGeometry(\n n: number,\n width: number,\n height: number,\n spacing: number,\n heightSegments: number\n): THREE.BufferGeometry {\n const geometry = new THREE.BufferGeometry();\n const numVertices = n * (heightSegments + 1) * 2;\n const numFaces = n * heightSegments * 2;\n const positions = new Float32Array(numVertices * 3);\n const indices = new Uint32Array(numFaces * 3);\n const uvs = new Float32Array(numVertices * 2);\n\n let vertexOffset = 0;\n let indexOffset = 0;\n let uvOffset = 0;\n const totalWidth = n * width + (n - 1) * spacing;\n const xOffsetBase = -totalWidth / 2;\n\n for (let i = 0; i < n; i++) {\n const xOffset = xOffsetBase + i * (width + spacing);\n const uvXOffset = Math.random() * 300;\n const uvYOffset = Math.random() * 300;\n\n for (let j = 0; j <= heightSegments; j++) {\n const y = height * (j / heightSegments - 0.5);\n const v0 = [xOffset, y, 0];\n const v1 = [xOffset + width, y, 0];\n positions.set([...v0, ...v1], vertexOffset * 3);\n\n const uvY = j / heightSegments;\n uvs.set([uvXOffset, uvY + uvYOffset, uvXOffset + 1, uvY + uvYOffset], uvOffset);\n\n if (j < heightSegments) {\n const a = vertexOffset,\n b = vertexOffset + 1,\n c = vertexOffset + 2,\n d = vertexOffset + 3;\n indices.set([a, b, c, c, b, d], indexOffset);\n indexOffset += 6;\n }\n vertexOffset += 2;\n uvOffset += 4;\n }\n }\n\n geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));\n geometry.setIndex(new THREE.BufferAttribute(indices, 1));\n geometry.computeVertexNormals();\n return geometry;\n}\n\nconst MergedPlanes = forwardRef<\n THREE.Mesh,\n {\n material: THREE.ShaderMaterial;\n width: number;\n count: number;\n height: number;\n }\n>(({ material, width, count, height }, ref) => {\n const mesh = useRef>(null!);\n useImperativeHandle(ref, () => mesh.current);\n const geometry = useMemo(\n () => createStackedPlanesBufferGeometry(count, width, height, 0, 100),\n [count, width, height]\n );\n useFrame((_, delta) => {\n mesh.current.material.uniforms.time.value += 0.1 * delta;\n });\n return ;\n});\nMergedPlanes.displayName = 'MergedPlanes';\n\nconst PlaneNoise = forwardRef<\n THREE.Mesh,\n {\n material: THREE.ShaderMaterial;\n width: number;\n count: number;\n height: number;\n }\n>((props, ref) => (\n \n));\nPlaneNoise.displayName = 'PlaneNoise';\n\nconst DirLight: FC<{ position: [number, number, number]; color: string }> = ({ position, color }) => {\n const dir = useRef(null!);\n useEffect(() => {\n if (!dir.current) return;\n const cam = dir.current.shadow.camera as THREE.Camera & {\n top: number;\n bottom: number;\n left: number;\n right: number;\n far: number;\n };\n cam.top = 24;\n cam.bottom = -24;\n cam.left = -24;\n cam.right = 24;\n cam.far = 64;\n dir.current.shadow.bias = -0.004;\n }, []);\n return ;\n};\n\nexport default Beams;\n" } ], "registryDependencies": [], diff --git a/public/r/Beams-TS-TW.json b/public/r/Beams-TS-TW.json index 9b1dfd1c..df16c923 100644 --- a/public/r/Beams-TS-TW.json +++ b/public/r/Beams-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "Beams/Beams.tsx", - "content": "import { forwardRef, useImperativeHandle, useEffect, useRef, useMemo, FC, ReactNode } from 'react';\n\nimport * as THREE from 'three';\n\nimport { Canvas, useFrame } from '@react-three/fiber';\nimport { PerspectiveCamera } from '@react-three/drei';\nimport { degToRad } from 'three/src/math/MathUtils.js';\n\ntype UniformValue = THREE.IUniform | unknown;\n\ninterface ExtendMaterialConfig {\n header: string;\n vertexHeader?: string;\n fragmentHeader?: string;\n material?: THREE.MeshPhysicalMaterialParameters & { fog?: boolean };\n uniforms?: Record;\n vertex?: Record;\n fragment?: Record;\n}\n\ntype ShaderWithDefines = THREE.ShaderLibShader & {\n defines?: Record;\n};\n\nfunction extendMaterial(\n BaseMaterial: new (params?: THREE.MaterialParameters) => T,\n cfg: ExtendMaterialConfig\n): THREE.ShaderMaterial {\n const physical = THREE.ShaderLib.physical as ShaderWithDefines;\n const { vertexShader: baseVert, fragmentShader: baseFrag, uniforms: baseUniforms } = physical;\n const baseDefines = physical.defines ?? {};\n\n const uniforms: Record = THREE.UniformsUtils.clone(baseUniforms);\n\n const defaults = new BaseMaterial(cfg.material || {}) as T & {\n color?: THREE.Color;\n roughness?: number;\n metalness?: number;\n envMap?: THREE.Texture;\n envMapIntensity?: number;\n };\n\n if (defaults.color) uniforms.diffuse.value = defaults.color;\n if ('roughness' in defaults) uniforms.roughness.value = defaults.roughness;\n if ('metalness' in defaults) uniforms.metalness.value = defaults.metalness;\n if ('envMap' in defaults) uniforms.envMap.value = defaults.envMap;\n if ('envMapIntensity' in defaults) uniforms.envMapIntensity.value = defaults.envMapIntensity;\n\n Object.entries(cfg.uniforms ?? {}).forEach(([key, u]) => {\n uniforms[key] =\n u !== null && typeof u === 'object' && 'value' in u\n ? (u as THREE.IUniform)\n : ({ value: u } as THREE.IUniform);\n });\n\n let vert = `${cfg.header}\\n${cfg.vertexHeader ?? ''}\\n${baseVert}`;\n let frag = `${cfg.header}\\n${cfg.fragmentHeader ?? ''}\\n${baseFrag}`;\n\n for (const [inc, code] of Object.entries(cfg.vertex ?? {})) {\n vert = vert.replace(inc, `${inc}\\n${code}`);\n }\n for (const [inc, code] of Object.entries(cfg.fragment ?? {})) {\n frag = frag.replace(inc, `${inc}\\n${code}`);\n }\n\n const mat = new THREE.ShaderMaterial({\n defines: { ...baseDefines },\n uniforms,\n vertexShader: vert,\n fragmentShader: frag,\n lights: true,\n fog: !!cfg.material?.fog\n });\n\n return mat;\n}\n\nconst CanvasWrapper: FC<{ children: ReactNode }> = ({ children }) => (\n \n {children}\n \n);\n\nconst hexToNormalizedRGB = (hex: string): [number, number, number] => {\n const clean = hex.replace('#', '');\n const r = parseInt(clean.substring(0, 2), 16);\n const g = parseInt(clean.substring(2, 4), 16);\n const b = parseInt(clean.substring(4, 6), 16);\n return [r / 255, g / 255, b / 255];\n};\n\nconst noise = `\nfloat random (in vec2 st) {\n return fract(sin(dot(st.xy,\n vec2(12.9898,78.233)))*\n 43758.5453123);\n}\nfloat noise (in vec2 st) {\n vec2 i = floor(st);\n vec2 f = fract(st);\n float a = random(i);\n float b = random(i + vec2(1.0, 0.0));\n float c = random(i + vec2(0.0, 1.0));\n float d = random(i + vec2(1.0, 1.0));\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(a, b, u.x) +\n (c - a)* u.y * (1.0 - u.x) +\n (d - b) * u.x * u.y;\n}\nvec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}\nvec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}\nvec3 fade(vec3 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);}\nfloat cnoise(vec3 P){\n vec3 Pi0 = floor(P);\n vec3 Pi1 = Pi0 + vec3(1.0);\n Pi0 = mod(Pi0, 289.0);\n Pi1 = mod(Pi1, 289.0);\n vec3 Pf0 = fract(P);\n vec3 Pf1 = Pf0 - vec3(1.0);\n vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);\n vec4 iy = vec4(Pi0.yy, Pi1.yy);\n vec4 iz0 = Pi0.zzzz;\n vec4 iz1 = Pi1.zzzz;\n vec4 ixy = permute(permute(ix) + iy);\n vec4 ixy0 = permute(ixy + iz0);\n vec4 ixy1 = permute(ixy + iz1);\n vec4 gx0 = ixy0 / 7.0;\n vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5;\n gx0 = fract(gx0);\n vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);\n vec4 sz0 = step(gz0, vec4(0.0));\n gx0 -= sz0 * (step(0.0, gx0) - 0.5);\n gy0 -= sz0 * (step(0.0, gy0) - 0.5);\n vec4 gx1 = ixy1 / 7.0;\n vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5;\n gx1 = fract(gx1);\n vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);\n vec4 sz1 = step(gz1, vec4(0.0));\n gx1 -= sz1 * (step(0.0, gx1) - 0.5);\n gy1 -= sz1 * (step(0.0, gy1) - 0.5);\n vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);\n vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);\n vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);\n vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);\n vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);\n vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);\n vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);\n vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);\n vec4 norm0 = taylorInvSqrt(vec4(dot(g000,g000),dot(g010,g010),dot(g100,g100),dot(g110,g110)));\n g000 *= norm0.x; g010 *= norm0.y; g100 *= norm0.z; g110 *= norm0.w;\n vec4 norm1 = taylorInvSqrt(vec4(dot(g001,g001),dot(g011,g011),dot(g101,g101),dot(g111,g111)));\n g001 *= norm1.x; g011 *= norm1.y; g101 *= norm1.z; g111 *= norm1.w;\n float n000 = dot(g000, Pf0);\n float n100 = dot(g100, vec3(Pf1.x,Pf0.yz));\n float n010 = dot(g010, vec3(Pf0.x,Pf1.y,Pf0.z));\n float n110 = dot(g110, vec3(Pf1.xy,Pf0.z));\n float n001 = dot(g001, vec3(Pf0.xy,Pf1.z));\n float n101 = dot(g101, vec3(Pf1.x,Pf0.y,Pf1.z));\n float n011 = dot(g011, vec3(Pf0.x,Pf1.yz));\n float n111 = dot(g111, Pf1);\n vec3 fade_xyz = fade(Pf0);\n vec4 n_z = mix(vec4(n000,n100,n010,n110),vec4(n001,n101,n011,n111),fade_xyz.z);\n vec2 n_yz = mix(n_z.xy,n_z.zw,fade_xyz.y);\n float n_xyz = mix(n_yz.x,n_yz.y,fade_xyz.x);\n return 2.2 * n_xyz;\n}\n`;\n\ninterface BeamsProps {\n beamWidth?: number;\n beamHeight?: number;\n beamNumber?: number;\n lightColor?: string;\n speed?: number;\n noiseIntensity?: number;\n scale?: number;\n rotation?: number;\n}\n\nconst Beams: FC = ({\n beamWidth = 2,\n beamHeight = 15,\n beamNumber = 12,\n lightColor = '#ffffff',\n speed = 2,\n noiseIntensity = 1.75,\n scale = 0.2,\n rotation = 0\n}) => {\n const meshRef = useRef>(null!);\n\n const beamMaterial = useMemo(\n () =>\n extendMaterial(THREE.MeshStandardMaterial, {\n header: `\n varying vec3 vEye;\n varying float vNoise;\n varying vec2 vUv;\n varying vec3 vPosition;\n uniform float time;\n uniform float uSpeed;\n uniform float uNoiseIntensity;\n uniform float uScale;\n ${noise}`,\n vertexHeader: `\n float getPos(vec3 pos) {\n vec3 noisePos =\n vec3(pos.x * 0., pos.y - uv.y, pos.z + time * uSpeed * 3.) * uScale;\n return cnoise(noisePos);\n }\n vec3 getCurrentPos(vec3 pos) {\n vec3 newpos = pos;\n newpos.z += getPos(pos);\n return newpos;\n }\n vec3 getNormal(vec3 pos) {\n vec3 curpos = getCurrentPos(pos);\n vec3 nextposX = getCurrentPos(pos + vec3(0.01, 0.0, 0.0));\n vec3 nextposZ = getCurrentPos(pos + vec3(0.0, -0.01, 0.0));\n vec3 tangentX = normalize(nextposX - curpos);\n vec3 tangentZ = normalize(nextposZ - curpos);\n return normalize(cross(tangentZ, tangentX));\n }`,\n fragmentHeader: '',\n vertex: {\n '#include ': `transformed.z += getPos(transformed.xyz);`,\n '#include ': `objectNormal = getNormal(position.xyz);`\n },\n fragment: {\n '#include ': `\n float randomNoise = noise(gl_FragCoord.xy);\n gl_FragColor.rgb -= randomNoise / 15. * uNoiseIntensity;`\n },\n material: { fog: true },\n uniforms: {\n diffuse: new THREE.Color(...hexToNormalizedRGB('#000000')),\n time: { shared: true, mixed: true, linked: true, value: 0 },\n roughness: 0.3,\n metalness: 0.3,\n uSpeed: { shared: true, mixed: true, linked: true, value: speed },\n envMapIntensity: 10,\n uNoiseIntensity: noiseIntensity,\n uScale: scale\n }\n }),\n [speed, noiseIntensity, scale]\n );\n\n return (\n \n \n \n \n \n \n \n \n \n );\n};\n\nfunction createStackedPlanesBufferGeometry(\n n: number,\n width: number,\n height: number,\n spacing: number,\n heightSegments: number\n): THREE.BufferGeometry {\n const geometry = new THREE.BufferGeometry();\n const numVertices = n * (heightSegments + 1) * 2;\n const numFaces = n * heightSegments * 2;\n const positions = new Float32Array(numVertices * 3);\n const indices = new Uint32Array(numFaces * 3);\n const uvs = new Float32Array(numVertices * 2);\n\n let vertexOffset = 0;\n let indexOffset = 0;\n let uvOffset = 0;\n const totalWidth = n * width + (n - 1) * spacing;\n const xOffsetBase = -totalWidth / 2;\n\n for (let i = 0; i < n; i++) {\n const xOffset = xOffsetBase + i * (width + spacing);\n const uvXOffset = Math.random() * 300;\n const uvYOffset = Math.random() * 300;\n\n for (let j = 0; j <= heightSegments; j++) {\n const y = height * (j / heightSegments - 0.5);\n const v0 = [xOffset, y, 0];\n const v1 = [xOffset + width, y, 0];\n positions.set([...v0, ...v1], vertexOffset * 3);\n\n const uvY = j / heightSegments;\n uvs.set([uvXOffset, uvY + uvYOffset, uvXOffset + 1, uvY + uvYOffset], uvOffset);\n\n if (j < heightSegments) {\n const a = vertexOffset,\n b = vertexOffset + 1,\n c = vertexOffset + 2,\n d = vertexOffset + 3;\n indices.set([a, b, c, c, b, d], indexOffset);\n indexOffset += 6;\n }\n vertexOffset += 2;\n uvOffset += 4;\n }\n }\n\n geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));\n geometry.setIndex(new THREE.BufferAttribute(indices, 1));\n geometry.computeVertexNormals();\n return geometry;\n}\n\nconst MergedPlanes = forwardRef<\n THREE.Mesh,\n {\n material: THREE.ShaderMaterial;\n width: number;\n count: number;\n height: number;\n }\n>(({ material, width, count, height }, ref) => {\n const mesh = useRef>(null!);\n useImperativeHandle(ref, () => mesh.current);\n const geometry = useMemo(\n () => createStackedPlanesBufferGeometry(count, width, height, 0, 100),\n [count, width, height]\n );\n useFrame((_, delta) => {\n mesh.current.material.uniforms.time.value += 0.1 * delta;\n });\n return ;\n});\nMergedPlanes.displayName = 'MergedPlanes';\n\nconst PlaneNoise = forwardRef<\n THREE.Mesh,\n {\n material: THREE.ShaderMaterial;\n width: number;\n count: number;\n height: number;\n }\n>((props, ref) => (\n \n));\nPlaneNoise.displayName = 'PlaneNoise';\n\nconst DirLight: FC<{ position: [number, number, number]; color: string }> = ({ position, color }) => {\n const dir = useRef(null!);\n useEffect(() => {\n if (!dir.current) return;\n const cam = dir.current.shadow.camera as THREE.Camera & {\n top: number;\n bottom: number;\n left: number;\n right: number;\n far: number;\n };\n cam.top = 24;\n cam.bottom = -24;\n cam.left = -24;\n cam.right = 24;\n cam.far = 64;\n dir.current.shadow.bias = -0.004;\n }, []);\n return ;\n};\n\nexport default Beams;\n" + "content": "import { forwardRef, useImperativeHandle, useEffect, useRef, useMemo, type FC, type ReactNode } from 'react';\n\nimport * as THREE from 'three';\n\nimport { Canvas, useFrame } from '@react-three/fiber';\nimport { PerspectiveCamera } from '@react-three/drei';\nimport { degToRad } from 'three/src/math/MathUtils.js';\n\ntype UniformValue = THREE.IUniform | unknown;\n\ninterface ExtendMaterialConfig {\n header: string;\n vertexHeader?: string;\n fragmentHeader?: string;\n material?: THREE.MeshPhysicalMaterialParameters & { fog?: boolean };\n uniforms?: Record;\n vertex?: Record;\n fragment?: Record;\n}\n\ntype ShaderWithDefines = THREE.ShaderLibShader & {\n defines?: Record;\n};\n\nfunction extendMaterial(\n BaseMaterial: new (params?: THREE.MaterialParameters) => T,\n cfg: ExtendMaterialConfig\n): THREE.ShaderMaterial {\n const physical = THREE.ShaderLib.physical as ShaderWithDefines;\n const { vertexShader: baseVert, fragmentShader: baseFrag, uniforms: baseUniforms } = physical;\n const baseDefines = physical.defines ?? {};\n\n const uniforms: Record = THREE.UniformsUtils.clone(baseUniforms);\n\n const defaults = new BaseMaterial(cfg.material || {}) as T & {\n color?: THREE.Color;\n roughness?: number;\n metalness?: number;\n envMap?: THREE.Texture;\n envMapIntensity?: number;\n };\n\n if (defaults.color) uniforms.diffuse.value = defaults.color;\n if ('roughness' in defaults) uniforms.roughness.value = defaults.roughness;\n if ('metalness' in defaults) uniforms.metalness.value = defaults.metalness;\n if ('envMap' in defaults) uniforms.envMap.value = defaults.envMap;\n if ('envMapIntensity' in defaults) uniforms.envMapIntensity.value = defaults.envMapIntensity;\n\n Object.entries(cfg.uniforms ?? {}).forEach(([key, u]) => {\n uniforms[key] =\n u !== null && typeof u === 'object' && 'value' in u\n ? (u as THREE.IUniform)\n : ({ value: u } as THREE.IUniform);\n });\n\n let vert = `${cfg.header}\\n${cfg.vertexHeader ?? ''}\\n${baseVert}`;\n let frag = `${cfg.header}\\n${cfg.fragmentHeader ?? ''}\\n${baseFrag}`;\n\n for (const [inc, code] of Object.entries(cfg.vertex ?? {})) {\n vert = vert.replace(inc, `${inc}\\n${code}`);\n }\n for (const [inc, code] of Object.entries(cfg.fragment ?? {})) {\n frag = frag.replace(inc, `${inc}\\n${code}`);\n }\n\n const mat = new THREE.ShaderMaterial({\n defines: { ...baseDefines },\n uniforms,\n vertexShader: vert,\n fragmentShader: frag,\n lights: true,\n fog: !!cfg.material?.fog\n });\n\n return mat;\n}\n\nconst CanvasWrapper: FC<{ children: ReactNode }> = ({ children }) => (\n \n {children}\n \n);\n\nconst hexToNormalizedRGB = (hex: string): [number, number, number] => {\n const clean = hex.replace('#', '');\n const r = parseInt(clean.substring(0, 2), 16);\n const g = parseInt(clean.substring(2, 4), 16);\n const b = parseInt(clean.substring(4, 6), 16);\n return [r / 255, g / 255, b / 255];\n};\n\nconst noise = `\nfloat random (in vec2 st) {\n return fract(sin(dot(st.xy,\n vec2(12.9898,78.233)))*\n 43758.5453123);\n}\nfloat noise (in vec2 st) {\n vec2 i = floor(st);\n vec2 f = fract(st);\n float a = random(i);\n float b = random(i + vec2(1.0, 0.0));\n float c = random(i + vec2(0.0, 1.0));\n float d = random(i + vec2(1.0, 1.0));\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(a, b, u.x) +\n (c - a)* u.y * (1.0 - u.x) +\n (d - b) * u.x * u.y;\n}\nvec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}\nvec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}\nvec3 fade(vec3 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);}\nfloat cnoise(vec3 P){\n vec3 Pi0 = floor(P);\n vec3 Pi1 = Pi0 + vec3(1.0);\n Pi0 = mod(Pi0, 289.0);\n Pi1 = mod(Pi1, 289.0);\n vec3 Pf0 = fract(P);\n vec3 Pf1 = Pf0 - vec3(1.0);\n vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);\n vec4 iy = vec4(Pi0.yy, Pi1.yy);\n vec4 iz0 = Pi0.zzzz;\n vec4 iz1 = Pi1.zzzz;\n vec4 ixy = permute(permute(ix) + iy);\n vec4 ixy0 = permute(ixy + iz0);\n vec4 ixy1 = permute(ixy + iz1);\n vec4 gx0 = ixy0 / 7.0;\n vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5;\n gx0 = fract(gx0);\n vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);\n vec4 sz0 = step(gz0, vec4(0.0));\n gx0 -= sz0 * (step(0.0, gx0) - 0.5);\n gy0 -= sz0 * (step(0.0, gy0) - 0.5);\n vec4 gx1 = ixy1 / 7.0;\n vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5;\n gx1 = fract(gx1);\n vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);\n vec4 sz1 = step(gz1, vec4(0.0));\n gx1 -= sz1 * (step(0.0, gx1) - 0.5);\n gy1 -= sz1 * (step(0.0, gy1) - 0.5);\n vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);\n vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);\n vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);\n vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);\n vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);\n vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);\n vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);\n vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);\n vec4 norm0 = taylorInvSqrt(vec4(dot(g000,g000),dot(g010,g010),dot(g100,g100),dot(g110,g110)));\n g000 *= norm0.x; g010 *= norm0.y; g100 *= norm0.z; g110 *= norm0.w;\n vec4 norm1 = taylorInvSqrt(vec4(dot(g001,g001),dot(g011,g011),dot(g101,g101),dot(g111,g111)));\n g001 *= norm1.x; g011 *= norm1.y; g101 *= norm1.z; g111 *= norm1.w;\n float n000 = dot(g000, Pf0);\n float n100 = dot(g100, vec3(Pf1.x,Pf0.yz));\n float n010 = dot(g010, vec3(Pf0.x,Pf1.y,Pf0.z));\n float n110 = dot(g110, vec3(Pf1.xy,Pf0.z));\n float n001 = dot(g001, vec3(Pf0.xy,Pf1.z));\n float n101 = dot(g101, vec3(Pf1.x,Pf0.y,Pf1.z));\n float n011 = dot(g011, vec3(Pf0.x,Pf1.yz));\n float n111 = dot(g111, Pf1);\n vec3 fade_xyz = fade(Pf0);\n vec4 n_z = mix(vec4(n000,n100,n010,n110),vec4(n001,n101,n011,n111),fade_xyz.z);\n vec2 n_yz = mix(n_z.xy,n_z.zw,fade_xyz.y);\n float n_xyz = mix(n_yz.x,n_yz.y,fade_xyz.x);\n return 2.2 * n_xyz;\n}\n`;\n\ninterface BeamsProps {\n beamWidth?: number;\n beamHeight?: number;\n beamNumber?: number;\n lightColor?: string;\n speed?: number;\n noiseIntensity?: number;\n scale?: number;\n rotation?: number;\n}\n\nconst Beams: FC = ({\n beamWidth = 2,\n beamHeight = 15,\n beamNumber = 12,\n lightColor = '#ffffff',\n speed = 2,\n noiseIntensity = 1.75,\n scale = 0.2,\n rotation = 0\n}) => {\n const meshRef = useRef>(null!);\n\n const beamMaterial = useMemo(\n () =>\n extendMaterial(THREE.MeshStandardMaterial, {\n header: `\n varying vec3 vEye;\n varying float vNoise;\n varying vec2 vUv;\n varying vec3 vPosition;\n uniform float time;\n uniform float uSpeed;\n uniform float uNoiseIntensity;\n uniform float uScale;\n ${noise}`,\n vertexHeader: `\n float getPos(vec3 pos) {\n vec3 noisePos =\n vec3(pos.x * 0., pos.y - uv.y, pos.z + time * uSpeed * 3.) * uScale;\n return cnoise(noisePos);\n }\n vec3 getCurrentPos(vec3 pos) {\n vec3 newpos = pos;\n newpos.z += getPos(pos);\n return newpos;\n }\n vec3 getNormal(vec3 pos) {\n vec3 curpos = getCurrentPos(pos);\n vec3 nextposX = getCurrentPos(pos + vec3(0.01, 0.0, 0.0));\n vec3 nextposZ = getCurrentPos(pos + vec3(0.0, -0.01, 0.0));\n vec3 tangentX = normalize(nextposX - curpos);\n vec3 tangentZ = normalize(nextposZ - curpos);\n return normalize(cross(tangentZ, tangentX));\n }`,\n fragmentHeader: '',\n vertex: {\n '#include ': `transformed.z += getPos(transformed.xyz);`,\n '#include ': `objectNormal = getNormal(position.xyz);`\n },\n fragment: {\n '#include ': `\n float randomNoise = noise(gl_FragCoord.xy);\n gl_FragColor.rgb -= randomNoise / 15. * uNoiseIntensity;`\n },\n material: { fog: true },\n uniforms: {\n diffuse: new THREE.Color(...hexToNormalizedRGB('#000000')),\n time: { shared: true, mixed: true, linked: true, value: 0 },\n roughness: 0.3,\n metalness: 0.3,\n uSpeed: { shared: true, mixed: true, linked: true, value: speed },\n envMapIntensity: 10,\n uNoiseIntensity: noiseIntensity,\n uScale: scale\n }\n }),\n [speed, noiseIntensity, scale]\n );\n\n return (\n \n \n \n \n \n \n \n \n \n );\n};\n\nfunction createStackedPlanesBufferGeometry(\n n: number,\n width: number,\n height: number,\n spacing: number,\n heightSegments: number\n): THREE.BufferGeometry {\n const geometry = new THREE.BufferGeometry();\n const numVertices = n * (heightSegments + 1) * 2;\n const numFaces = n * heightSegments * 2;\n const positions = new Float32Array(numVertices * 3);\n const indices = new Uint32Array(numFaces * 3);\n const uvs = new Float32Array(numVertices * 2);\n\n let vertexOffset = 0;\n let indexOffset = 0;\n let uvOffset = 0;\n const totalWidth = n * width + (n - 1) * spacing;\n const xOffsetBase = -totalWidth / 2;\n\n for (let i = 0; i < n; i++) {\n const xOffset = xOffsetBase + i * (width + spacing);\n const uvXOffset = Math.random() * 300;\n const uvYOffset = Math.random() * 300;\n\n for (let j = 0; j <= heightSegments; j++) {\n const y = height * (j / heightSegments - 0.5);\n const v0 = [xOffset, y, 0];\n const v1 = [xOffset + width, y, 0];\n positions.set([...v0, ...v1], vertexOffset * 3);\n\n const uvY = j / heightSegments;\n uvs.set([uvXOffset, uvY + uvYOffset, uvXOffset + 1, uvY + uvYOffset], uvOffset);\n\n if (j < heightSegments) {\n const a = vertexOffset,\n b = vertexOffset + 1,\n c = vertexOffset + 2,\n d = vertexOffset + 3;\n indices.set([a, b, c, c, b, d], indexOffset);\n indexOffset += 6;\n }\n vertexOffset += 2;\n uvOffset += 4;\n }\n }\n\n geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));\n geometry.setIndex(new THREE.BufferAttribute(indices, 1));\n geometry.computeVertexNormals();\n return geometry;\n}\n\nconst MergedPlanes = forwardRef<\n THREE.Mesh,\n {\n material: THREE.ShaderMaterial;\n width: number;\n count: number;\n height: number;\n }\n>(({ material, width, count, height }, ref) => {\n const mesh = useRef>(null!);\n useImperativeHandle(ref, () => mesh.current);\n const geometry = useMemo(\n () => createStackedPlanesBufferGeometry(count, width, height, 0, 100),\n [count, width, height]\n );\n useFrame((_, delta) => {\n mesh.current.material.uniforms.time.value += 0.1 * delta;\n });\n return ;\n});\nMergedPlanes.displayName = 'MergedPlanes';\n\nconst PlaneNoise = forwardRef<\n THREE.Mesh,\n {\n material: THREE.ShaderMaterial;\n width: number;\n count: number;\n height: number;\n }\n>((props, ref) => (\n \n));\nPlaneNoise.displayName = 'PlaneNoise';\n\nconst DirLight: FC<{ position: [number, number, number]; color: string }> = ({ position, color }) => {\n const dir = useRef(null!);\n useEffect(() => {\n if (!dir.current) return;\n const cam = dir.current.shadow.camera as THREE.Camera & {\n top: number;\n bottom: number;\n left: number;\n right: number;\n far: number;\n };\n cam.top = 24;\n cam.bottom = -24;\n cam.left = -24;\n cam.right = 24;\n cam.far = 64;\n dir.current.shadow.bias = -0.004;\n }, []);\n return ;\n};\n\nexport default Beams;\n" } ], "registryDependencies": [], diff --git a/public/r/BlobCursor-JS-CSS.json b/public/r/BlobCursor-JS-CSS.json index 0a08c630..1a5d7300 100644 --- a/public/r/BlobCursor-JS-CSS.json +++ b/public/r/BlobCursor-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "BlobCursor/BlobCursor.css", + "type": "registry:file", + "path": "BlobCursor.css", + "target": "@components/BlobCursor.css", "content": ".blob-container {\n position: relative;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n}\n\n.blob-main {\n pointer-events: none;\n position: absolute;\n width: 100%;\n height: 100%;\n overflow: hidden;\n background: transparent;\n user-select: none;\n cursor: default;\n}\n\n.blob {\n position: absolute;\n will-change: transform;\n transform: translate(-50%, -50%);\n}\n\n.inner-dot {\n position: absolute;\n}\n" }, { "type": "registry:component", - "path": "BlobCursor/BlobCursor.jsx", + "path": "BlobCursor.jsx", "content": "'use client';\n\nimport { useRef, useEffect, useCallback } from 'react';\nimport gsap from 'gsap';\nimport './BlobCursor.css';\n\nexport default function BlobCursor({\n blobType = 'circle',\n fillColor = '#5227FF',\n trailCount = 3,\n sizes = [60, 125, 75],\n innerSizes = [20, 35, 25],\n innerColor = 'rgba(255,255,255,0.8)',\n opacities = [0.6, 0.6, 0.6],\n shadowColor = 'rgba(0,0,0,0.75)',\n shadowBlur = 5,\n shadowOffsetX = 10,\n shadowOffsetY = 10,\n filterId = 'blob',\n filterStdDeviation = 30,\n filterColorMatrixValues = '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 35 -10',\n useFilter = true,\n fastDuration = 0.1,\n slowDuration = 0.5,\n fastEase = 'power3.out',\n slowEase = 'power1.out',\n zIndex = 100\n}) {\n const containerRef = useRef(null);\n const blobsRef = useRef([]);\n\n const updateOffset = useCallback(() => {\n if (!containerRef.current) return { left: 0, top: 0 };\n const rect = containerRef.current.getBoundingClientRect();\n return { left: rect.left, top: rect.top };\n }, []);\n\n const handleMove = useCallback(\n e => {\n const { left, top } = updateOffset();\n const x = 'clientX' in e ? e.clientX : e.touches[0].clientX;\n const y = 'clientY' in e ? e.clientY : e.touches[0].clientY;\n\n blobsRef.current.forEach((el, i) => {\n if (!el) return;\n const isLead = i === 0;\n gsap.to(el, {\n x: x - left,\n y: y - top,\n duration: isLead ? fastDuration : slowDuration,\n ease: isLead ? fastEase : slowEase\n });\n });\n },\n [updateOffset, fastDuration, slowDuration, fastEase, slowEase]\n );\n\n useEffect(() => {\n const onResize = () => updateOffset();\n window.addEventListener('resize', onResize);\n return () => window.removeEventListener('resize', onResize);\n }, [updateOffset]);\n\n return (\n \n {useFilter && (\n \n \n \n \n \n \n )}\n\n
\n {Array.from({ length: trailCount }).map((_, i) => (\n {\n blobsRef.current[i] = el;\n }}\n className=\"blob\"\n style={{\n width: sizes[i],\n height: sizes[i],\n borderRadius: blobType === 'circle' ? '50%' : '0%',\n backgroundColor: fillColor,\n opacity: opacities[i],\n boxShadow: `${shadowOffsetX}px ${shadowOffsetY}px ${shadowBlur}px 0 ${shadowColor}`\n }}\n >\n \n
\n ))}\n
\n
\n );\n}\n" } ], diff --git a/public/r/BlobCursor-TS-CSS.json b/public/r/BlobCursor-TS-CSS.json index ea761f41..0e3abc5f 100644 --- a/public/r/BlobCursor-TS-CSS.json +++ b/public/r/BlobCursor-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "BlobCursor/BlobCursor.css", + "type": "registry:file", + "path": "BlobCursor.css", + "target": "@components/BlobCursor.css", "content": ".blob-container {\n position: relative;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n}\n\n.blob-main {\n pointer-events: none;\n position: absolute;\n width: 100%;\n height: 100%;\n overflow: hidden;\n background: transparent;\n user-select: none;\n cursor: default;\n}\n\n.blob {\n position: absolute;\n will-change: transform;\n transform: translate(-50%, -50%);\n}\n\n.inner-dot {\n position: absolute;\n}\n" }, { "type": "registry:component", - "path": "BlobCursor/BlobCursor.tsx", + "path": "BlobCursor.tsx", "content": "'use client';\n\nimport React, { useRef, useEffect, useCallback } from 'react';\nimport gsap from 'gsap';\nimport './BlobCursor.css';\n\nexport interface BlobCursorProps {\n blobType?: 'circle' | 'square';\n fillColor?: string;\n trailCount?: number;\n sizes?: number[];\n innerSizes?: number[];\n innerColor?: string;\n opacities?: number[];\n shadowColor?: string;\n shadowBlur?: number;\n shadowOffsetX?: number;\n shadowOffsetY?: number;\n filterId?: string;\n filterStdDeviation?: number;\n filterColorMatrixValues?: string;\n useFilter?: boolean;\n fastDuration?: number;\n slowDuration?: number;\n fastEase?: string;\n slowEase?: string;\n zIndex?: number;\n}\n\nexport default function BlobCursor({\n blobType = 'circle',\n fillColor = '#5227FF',\n trailCount = 3,\n sizes = [60, 125, 75],\n innerSizes = [20, 35, 25],\n innerColor = 'rgba(255,255,255,0.8)',\n opacities = [0.6, 0.6, 0.6],\n shadowColor = 'rgba(0,0,0,0.75)',\n shadowBlur = 5,\n shadowOffsetX = 10,\n shadowOffsetY = 10,\n filterId = 'blob',\n filterStdDeviation = 30,\n filterColorMatrixValues = '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 35 -10',\n useFilter = true,\n fastDuration = 0.1,\n slowDuration = 0.5,\n fastEase = 'power3.out',\n slowEase = 'power1.out',\n zIndex = 100\n}: BlobCursorProps) {\n const containerRef = useRef(null);\n const blobsRef = useRef<(HTMLDivElement | null)[]>([]);\n\n const updateOffset = useCallback(() => {\n if (!containerRef.current) return { left: 0, top: 0 };\n const rect = containerRef.current.getBoundingClientRect();\n return { left: rect.left, top: rect.top };\n }, []);\n\n const handleMove = useCallback(\n (e: React.MouseEvent | React.TouchEvent) => {\n const { left, top } = updateOffset();\n const x = 'clientX' in e ? e.clientX : e.touches[0].clientX;\n const y = 'clientY' in e ? e.clientY : e.touches[0].clientY;\n\n blobsRef.current.forEach((el, i) => {\n if (!el) return;\n const isLead = i === 0;\n gsap.to(el, {\n x: x - left,\n y: y - top,\n duration: isLead ? fastDuration : slowDuration,\n ease: isLead ? fastEase : slowEase\n });\n });\n },\n [updateOffset, fastDuration, slowDuration, fastEase, slowEase]\n );\n\n useEffect(() => {\n const onResize = () => updateOffset();\n window.addEventListener('resize', onResize);\n return () => window.removeEventListener('resize', onResize);\n }, [updateOffset]);\n\n return (\n \n {useFilter && (\n \n \n \n \n \n \n )}\n\n
\n {Array.from({ length: trailCount }).map((_, i) => (\n {\n blobsRef.current[i] = el;\n }}\n className=\"blob\"\n style={{\n width: sizes[i],\n height: sizes[i],\n borderRadius: blobType === 'circle' ? '50%' : '0%',\n backgroundColor: fillColor,\n opacity: opacities[i],\n boxShadow: `${shadowOffsetX}px ${shadowOffsetY}px ${shadowBlur}px 0 ${shadowColor}`\n }}\n >\n \n
\n ))}\n
\n
\n );\n}\n" } ], diff --git a/public/r/BlurText-TS-CSS.json b/public/r/BlurText-TS-CSS.json index cc0c432a..9ff049a6 100644 --- a/public/r/BlurText-TS-CSS.json +++ b/public/r/BlurText-TS-CSS.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "BlurText/BlurText.tsx", - "content": "import { motion, Transition } from 'motion/react';\nimport { useEffect, useRef, useState, useMemo } from 'react';\n\ntype BlurTextProps = {\n text?: string;\n delay?: number;\n className?: string;\n animateBy?: 'words' | 'letters';\n direction?: 'top' | 'bottom';\n threshold?: number;\n rootMargin?: string;\n animationFrom?: Record;\n animationTo?: Array>;\n easing?: (t: number) => number;\n onAnimationComplete?: () => void;\n stepDuration?: number;\n};\n\nconst buildKeyframes = (\n from: Record,\n steps: Array>\n): Record> => {\n const keys = new Set([...Object.keys(from), ...steps.flatMap(s => Object.keys(s))]);\n\n const keyframes: Record> = {};\n keys.forEach(k => {\n keyframes[k] = [from[k], ...steps.map(s => s[k])];\n });\n return keyframes;\n};\n\nconst BlurText: React.FC = ({\n text = '',\n delay = 200,\n className = '',\n animateBy = 'words',\n direction = 'top',\n threshold = 0.1,\n rootMargin = '0px',\n animationFrom,\n animationTo,\n easing = (t: number) => t,\n onAnimationComplete,\n stepDuration = 0.35\n}) => {\n const elements = animateBy === 'words' ? text.split(' ') : text.split('');\n const [inView, setInView] = useState(false);\n const ref = useRef(null);\n\n useEffect(() => {\n if (!ref.current) return;\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) {\n setInView(true);\n observer.unobserve(ref.current as Element);\n }\n },\n { threshold, rootMargin }\n );\n observer.observe(ref.current);\n return () => observer.disconnect();\n }, [threshold, rootMargin]);\n\n const defaultFrom = useMemo(\n () =>\n direction === 'top' ? { filter: 'blur(10px)', opacity: 0, y: -50 } : { filter: 'blur(10px)', opacity: 0, y: 50 },\n [direction]\n );\n\n const defaultTo = useMemo(\n () => [\n {\n filter: 'blur(5px)',\n opacity: 0.5,\n y: direction === 'top' ? 5 : -5\n },\n { filter: 'blur(0px)', opacity: 1, y: 0 }\n ],\n [direction]\n );\n\n const fromSnapshot = animationFrom ?? defaultFrom;\n const toSnapshots = animationTo ?? defaultTo;\n\n const stepCount = toSnapshots.length + 1;\n const totalDuration = stepDuration * (stepCount - 1);\n const times = Array.from({ length: stepCount }, (_, i) => (stepCount === 1 ? 0 : i / (stepCount - 1)));\n\n return (\n

\n {elements.map((segment, index) => {\n const animateKeyframes = buildKeyframes(fromSnapshot, toSnapshots);\n\n const spanTransition: Transition = {\n duration: totalDuration,\n times,\n delay: (index * delay) / 1000,\n ease: easing\n };\n\n return (\n \n {segment === ' ' ? '\\u00A0' : segment}\n {animateBy === 'words' && index < elements.length - 1 && '\\u00A0'}\n \n );\n })}\n

\n );\n};\n\nexport default BlurText;\n" + "content": "import { motion, type Transition } from 'motion/react';\nimport { useEffect, useRef, useState, useMemo } from 'react';\n\ntype BlurTextProps = {\n text?: string;\n delay?: number;\n className?: string;\n animateBy?: 'words' | 'letters';\n direction?: 'top' | 'bottom';\n threshold?: number;\n rootMargin?: string;\n animationFrom?: Record;\n animationTo?: Array>;\n easing?: (t: number) => number;\n onAnimationComplete?: () => void;\n stepDuration?: number;\n};\n\nconst buildKeyframes = (\n from: Record,\n steps: Array>\n): Record> => {\n const keys = new Set([...Object.keys(from), ...steps.flatMap(s => Object.keys(s))]);\n\n const keyframes: Record> = {};\n keys.forEach(k => {\n keyframes[k] = [from[k], ...steps.map(s => s[k])];\n });\n return keyframes;\n};\n\nconst BlurText: React.FC = ({\n text = '',\n delay = 200,\n className = '',\n animateBy = 'words',\n direction = 'top',\n threshold = 0.1,\n rootMargin = '0px',\n animationFrom,\n animationTo,\n easing = (t: number) => t,\n onAnimationComplete,\n stepDuration = 0.35\n}) => {\n const elements = animateBy === 'words' ? text.split(' ') : text.split('');\n const [inView, setInView] = useState(false);\n const ref = useRef(null);\n\n useEffect(() => {\n if (!ref.current) return;\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) {\n setInView(true);\n observer.unobserve(ref.current as Element);\n }\n },\n { threshold, rootMargin }\n );\n observer.observe(ref.current);\n return () => observer.disconnect();\n }, [threshold, rootMargin]);\n\n const defaultFrom = useMemo(\n () =>\n direction === 'top' ? { filter: 'blur(10px)', opacity: 0, y: -50 } : { filter: 'blur(10px)', opacity: 0, y: 50 },\n [direction]\n );\n\n const defaultTo = useMemo(\n () => [\n {\n filter: 'blur(5px)',\n opacity: 0.5,\n y: direction === 'top' ? 5 : -5\n },\n { filter: 'blur(0px)', opacity: 1, y: 0 }\n ],\n [direction]\n );\n\n const fromSnapshot = animationFrom ?? defaultFrom;\n const toSnapshots = animationTo ?? defaultTo;\n\n const stepCount = toSnapshots.length + 1;\n const totalDuration = stepDuration * (stepCount - 1);\n const times = Array.from({ length: stepCount }, (_, i) => (stepCount === 1 ? 0 : i / (stepCount - 1)));\n\n return (\n

\n {elements.map((segment, index) => {\n const animateKeyframes = buildKeyframes(fromSnapshot, toSnapshots);\n\n const spanTransition: Transition = {\n duration: totalDuration,\n times,\n delay: (index * delay) / 1000,\n ease: easing\n };\n\n return (\n \n {segment === ' ' ? '\\u00A0' : segment}\n {animateBy === 'words' && index < elements.length - 1 && '\\u00A0'}\n \n );\n })}\n

\n );\n};\n\nexport default BlurText;\n" } ], "registryDependencies": [], diff --git a/public/r/BlurText-TS-TW.json b/public/r/BlurText-TS-TW.json index a65b1758..a5c8e113 100644 --- a/public/r/BlurText-TS-TW.json +++ b/public/r/BlurText-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "BlurText/BlurText.tsx", - "content": "import { motion, Transition, Easing } from 'motion/react';\nimport { useEffect, useRef, useState, useMemo } from 'react';\n\ntype BlurTextProps = {\n text?: string;\n delay?: number;\n className?: string;\n animateBy?: 'words' | 'letters';\n direction?: 'top' | 'bottom';\n threshold?: number;\n rootMargin?: string;\n animationFrom?: Record;\n animationTo?: Array>;\n easing?: Easing | Easing[];\n onAnimationComplete?: () => void;\n stepDuration?: number;\n};\n\nconst buildKeyframes = (\n from: Record,\n steps: Array>\n): Record> => {\n const keys = new Set([...Object.keys(from), ...steps.flatMap(s => Object.keys(s))]);\n\n const keyframes: Record> = {};\n keys.forEach(k => {\n keyframes[k] = [from[k], ...steps.map(s => s[k])];\n });\n return keyframes;\n};\n\nconst BlurText: React.FC = ({\n text = '',\n delay = 200,\n className = '',\n animateBy = 'words',\n direction = 'top',\n threshold = 0.1,\n rootMargin = '0px',\n animationFrom,\n animationTo,\n easing = (t: number) => t,\n onAnimationComplete,\n stepDuration = 0.35\n}) => {\n const elements = animateBy === 'words' ? text.split(' ') : text.split('');\n const [inView, setInView] = useState(false);\n const ref = useRef(null);\n\n useEffect(() => {\n if (!ref.current) return;\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) {\n setInView(true);\n observer.unobserve(ref.current as Element);\n }\n },\n { threshold, rootMargin }\n );\n observer.observe(ref.current);\n return () => observer.disconnect();\n }, [threshold, rootMargin]);\n\n const defaultFrom = useMemo(\n () =>\n direction === 'top' ? { filter: 'blur(10px)', opacity: 0, y: -50 } : { filter: 'blur(10px)', opacity: 0, y: 50 },\n [direction]\n );\n\n const defaultTo = useMemo(\n () => [\n {\n filter: 'blur(5px)',\n opacity: 0.5,\n y: direction === 'top' ? 5 : -5\n },\n { filter: 'blur(0px)', opacity: 1, y: 0 }\n ],\n [direction]\n );\n\n const fromSnapshot = animationFrom ?? defaultFrom;\n const toSnapshots = animationTo ?? defaultTo;\n\n const stepCount = toSnapshots.length + 1;\n const totalDuration = stepDuration * (stepCount - 1);\n const times = Array.from({ length: stepCount }, (_, i) => (stepCount === 1 ? 0 : i / (stepCount - 1)));\n\n return (\n

\n {elements.map((segment, index) => {\n const animateKeyframes = buildKeyframes(fromSnapshot, toSnapshots);\n\n const spanTransition: Transition = {\n duration: totalDuration,\n times,\n delay: (index * delay) / 1000,\n ease: easing\n };\n\n return (\n \n {segment === ' ' ? '\\u00A0' : segment}\n {animateBy === 'words' && index < elements.length - 1 && '\\u00A0'}\n \n );\n })}\n

\n );\n};\n\nexport default BlurText;\n" + "content": "import { motion, type Transition, type Easing } from 'motion/react';\nimport { useEffect, useRef, useState, useMemo } from 'react';\n\ntype BlurTextProps = {\n text?: string;\n delay?: number;\n className?: string;\n animateBy?: 'words' | 'letters';\n direction?: 'top' | 'bottom';\n threshold?: number;\n rootMargin?: string;\n animationFrom?: Record;\n animationTo?: Array>;\n easing?: Easing | Easing[];\n onAnimationComplete?: () => void;\n stepDuration?: number;\n};\n\nconst buildKeyframes = (\n from: Record,\n steps: Array>\n): Record> => {\n const keys = new Set([...Object.keys(from), ...steps.flatMap(s => Object.keys(s))]);\n\n const keyframes: Record> = {};\n keys.forEach(k => {\n keyframes[k] = [from[k], ...steps.map(s => s[k])];\n });\n return keyframes;\n};\n\nconst BlurText: React.FC = ({\n text = '',\n delay = 200,\n className = '',\n animateBy = 'words',\n direction = 'top',\n threshold = 0.1,\n rootMargin = '0px',\n animationFrom,\n animationTo,\n easing = (t: number) => t,\n onAnimationComplete,\n stepDuration = 0.35\n}) => {\n const elements = animateBy === 'words' ? text.split(' ') : text.split('');\n const [inView, setInView] = useState(false);\n const ref = useRef(null);\n\n useEffect(() => {\n if (!ref.current) return;\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) {\n setInView(true);\n observer.unobserve(ref.current as Element);\n }\n },\n { threshold, rootMargin }\n );\n observer.observe(ref.current);\n return () => observer.disconnect();\n }, [threshold, rootMargin]);\n\n const defaultFrom = useMemo(\n () =>\n direction === 'top' ? { filter: 'blur(10px)', opacity: 0, y: -50 } : { filter: 'blur(10px)', opacity: 0, y: 50 },\n [direction]\n );\n\n const defaultTo = useMemo(\n () => [\n {\n filter: 'blur(5px)',\n opacity: 0.5,\n y: direction === 'top' ? 5 : -5\n },\n { filter: 'blur(0px)', opacity: 1, y: 0 }\n ],\n [direction]\n );\n\n const fromSnapshot = animationFrom ?? defaultFrom;\n const toSnapshots = animationTo ?? defaultTo;\n\n const stepCount = toSnapshots.length + 1;\n const totalDuration = stepDuration * (stepCount - 1);\n const times = Array.from({ length: stepCount }, (_, i) => (stepCount === 1 ? 0 : i / (stepCount - 1)));\n\n return (\n

\n {elements.map((segment, index) => {\n const animateKeyframes = buildKeyframes(fromSnapshot, toSnapshots);\n\n const spanTransition: Transition = {\n duration: totalDuration,\n times,\n delay: (index * delay) / 1000,\n ease: easing\n };\n\n return (\n \n {segment === ' ' ? '\\u00A0' : segment}\n {animateBy === 'words' && index < elements.length - 1 && '\\u00A0'}\n \n );\n })}\n

\n );\n};\n\nexport default BlurText;\n" } ], "registryDependencies": [], diff --git a/public/r/BorderGlow-JS-CSS.json b/public/r/BorderGlow-JS-CSS.json index d1475e3d..ceb4f8fc 100644 --- a/public/r/BorderGlow-JS-CSS.json +++ b/public/r/BorderGlow-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "BorderGlow/BorderGlow.css", + "type": "registry:file", + "path": "BorderGlow.css", + "target": "@components/BorderGlow.css", "content": ".border-glow-card {\n --edge-proximity: 0;\n --cursor-angle: 45deg;\n --edge-sensitivity: 30;\n --color-sensitivity: calc(var(--edge-sensitivity) + 20);\n --border-radius: 28px;\n --glow-padding: 40px;\n --cone-spread: 25;\n\n position: relative;\n border-radius: var(--border-radius);\n isolation: isolate;\n transform: translate3d(0, 0, 0.01px);\n display: grid;\n border: 1px solid rgb(255 255 255 / 15%);\n background: var(--card-bg, #120F17);\n overflow: visible;\n box-shadow:\n rgba(0, 0, 0, 0.1) 0px 1px 2px,\n rgba(0, 0, 0, 0.1) 0px 2px 4px,\n rgba(0, 0, 0, 0.1) 0px 4px 8px,\n rgba(0, 0, 0, 0.1) 0px 8px 16px,\n rgba(0, 0, 0, 0.1) 0px 16px 32px,\n rgba(0, 0, 0, 0.1) 0px 32px 64px;\n}\n\n.border-glow-card::before,\n.border-glow-card::after,\n.border-glow-card > .edge-light {\n content: \"\";\n position: absolute;\n inset: 0;\n border-radius: inherit;\n transition: opacity 0.25s ease-out;\n z-index: -1;\n}\n\n.border-glow-card:not(:hover):not(.sweep-active)::before,\n.border-glow-card:not(:hover):not(.sweep-active)::after,\n.border-glow-card:not(:hover):not(.sweep-active) > .edge-light {\n opacity: 0;\n transition: opacity 0.75s ease-in-out;\n}\n\n/* colored mesh-gradient border */\n.border-glow-card::before {\n border: 1px solid transparent;\n background:\n linear-gradient(var(--card-bg, #120F17) 0 100%) padding-box,\n linear-gradient(rgb(255 255 255 / 0%) 0% 100%) border-box,\n var(--gradient-one, radial-gradient(at 80% 55%, hsla(268, 100%, 76%, 1) 0px, transparent 50%)) border-box,\n var(--gradient-two, radial-gradient(at 69% 34%, hsla(349, 100%, 74%, 1) 0px, transparent 50%)) border-box,\n var(--gradient-three, radial-gradient(at 8% 6%, hsla(136, 100%, 78%, 1) 0px, transparent 50%)) border-box,\n var(--gradient-four, radial-gradient(at 41% 38%, hsla(192, 100%, 64%, 1) 0px, transparent 50%)) border-box,\n var(--gradient-five, radial-gradient(at 86% 85%, hsla(186, 100%, 74%, 1) 0px, transparent 50%)) border-box,\n var(--gradient-six, radial-gradient(at 82% 18%, hsla(52, 100%, 65%, 1) 0px, transparent 50%)) border-box,\n var(--gradient-seven, radial-gradient(at 51% 4%, hsla(12, 100%, 72%, 1) 0px, transparent 50%)) border-box,\n var(--gradient-base, linear-gradient(#c299ff 0 100%)) border-box;\n\n opacity: calc((var(--edge-proximity) - var(--color-sensitivity)) / (100 - var(--color-sensitivity)));\n\n mask-image:\n conic-gradient(\n from var(--cursor-angle) at center,\n black calc(var(--cone-spread) * 1%),\n transparent calc((var(--cone-spread) + 15) * 1%),\n transparent calc((100 - var(--cone-spread) - 15) * 1%),\n black calc((100 - var(--cone-spread)) * 1%)\n );\n}\n\n/* colored mesh-gradient background fill near edges */\n.border-glow-card::after {\n border: 1px solid transparent;\n background:\n var(--gradient-one, radial-gradient(at 80% 55%, hsla(268, 100%, 76%, 1) 0px, transparent 50%)) padding-box,\n var(--gradient-two, radial-gradient(at 69% 34%, hsla(349, 100%, 74%, 1) 0px, transparent 50%)) padding-box,\n var(--gradient-three, radial-gradient(at 8% 6%, hsla(136, 100%, 78%, 1) 0px, transparent 50%)) padding-box,\n var(--gradient-four, radial-gradient(at 41% 38%, hsla(192, 100%, 64%, 1) 0px, transparent 50%)) padding-box,\n var(--gradient-five, radial-gradient(at 86% 85%, hsla(186, 100%, 74%, 1) 0px, transparent 50%)) padding-box,\n var(--gradient-six, radial-gradient(at 82% 18%, hsla(52, 100%, 65%, 1) 0px, transparent 50%)) padding-box,\n var(--gradient-seven, radial-gradient(at 51% 4%, hsla(12, 100%, 72%, 1) 0px, transparent 50%)) padding-box,\n var(--gradient-base, linear-gradient(#c299ff 0 100%)) padding-box;\n\n mask-image:\n linear-gradient(to bottom, black, black),\n radial-gradient(ellipse at 50% 50%, black 40%, transparent 65%),\n radial-gradient(ellipse at 66% 66%, black 5%, transparent 40%),\n radial-gradient(ellipse at 33% 33%, black 5%, transparent 40%),\n radial-gradient(ellipse at 66% 33%, black 5%, transparent 40%),\n radial-gradient(ellipse at 33% 66%, black 5%, transparent 40%),\n conic-gradient(from var(--cursor-angle) at center, transparent 5%, black 15%, black 85%, transparent 95%);\n\n mask-composite: subtract, add, add, add, add, add;\n opacity: calc(var(--fill-opacity, 0.5) * (var(--edge-proximity) - var(--color-sensitivity)) / (100 - var(--color-sensitivity)));\n mix-blend-mode: soft-light;\n}\n\n/* outer glow layer */\n.border-glow-card > .edge-light {\n inset: calc(var(--glow-padding) * -1);\n pointer-events: none;\n z-index: 1;\n\n mask-image:\n conic-gradient(\n from var(--cursor-angle) at center, black 2.5%, transparent 10%, transparent 90%, black 97.5%\n );\n\n opacity: calc((var(--edge-proximity) - var(--edge-sensitivity)) / (100 - var(--edge-sensitivity)));\n mix-blend-mode: plus-lighter;\n}\n\n.border-glow-card > .edge-light::before {\n content: \"\";\n position: absolute;\n inset: var(--glow-padding);\n border-radius: inherit;\n box-shadow:\n inset 0 0 0 1px var(--glow-color, hsl(40deg 80% 80% / 100%)),\n inset 0 0 1px 0 var(--glow-color-60, hsl(40deg 80% 80% / 60%)),\n inset 0 0 3px 0 var(--glow-color-50, hsl(40deg 80% 80% / 50%)),\n inset 0 0 6px 0 var(--glow-color-40, hsl(40deg 80% 80% / 40%)),\n inset 0 0 15px 0 var(--glow-color-30, hsl(40deg 80% 80% / 30%)),\n inset 0 0 25px 2px var(--glow-color-20, hsl(40deg 80% 80% / 20%)),\n inset 0 0 50px 2px var(--glow-color-10, hsl(40deg 80% 80% / 10%)),\n 0 0 1px 0 var(--glow-color-60, hsl(40deg 80% 80% / 60%)),\n 0 0 3px 0 var(--glow-color-50, hsl(40deg 80% 80% / 50%)),\n 0 0 6px 0 var(--glow-color-40, hsl(40deg 80% 80% / 40%)),\n 0 0 15px 0 var(--glow-color-30, hsl(40deg 80% 80% / 30%)),\n 0 0 25px 2px var(--glow-color-20, hsl(40deg 80% 80% / 20%)),\n 0 0 50px 2px var(--glow-color-10, hsl(40deg 80% 80% / 10%));\n}\n\n.border-glow-inner {\n display: flex;\n flex-direction: column;\n position: relative;\n overflow: auto;\n z-index: 1;\n}\n" }, { "type": "registry:component", - "path": "BorderGlow/BorderGlow.jsx", + "path": "BorderGlow.jsx", "content": "import { useRef, useCallback, useEffect } from 'react';\nimport './BorderGlow.css';\n\nfunction parseHSL(hslStr) {\n const match = hslStr.match(/([\\d.]+)\\s*([\\d.]+)%?\\s*([\\d.]+)%?/);\n if (!match) return { h: 40, s: 80, l: 80 };\n return { h: parseFloat(match[1]), s: parseFloat(match[2]), l: parseFloat(match[3]) };\n}\n\nfunction buildGlowVars(glowColor, intensity) {\n const { h, s, l } = parseHSL(glowColor);\n const base = `${h}deg ${s}% ${l}%`;\n const opacities = [100, 60, 50, 40, 30, 20, 10];\n const keys = ['', '-60', '-50', '-40', '-30', '-20', '-10'];\n const vars = {};\n for (let i = 0; i < opacities.length; i++) {\n vars[`--glow-color${keys[i]}`] = `hsl(${base} / ${Math.min(opacities[i] * intensity, 100)}%)`;\n }\n return vars;\n}\n\nconst GRADIENT_POSITIONS = ['80% 55%', '69% 34%', '8% 6%', '41% 38%', '86% 85%', '82% 18%', '51% 4%'];\nconst GRADIENT_KEYS = ['--gradient-one', '--gradient-two', '--gradient-three', '--gradient-four', '--gradient-five', '--gradient-six', '--gradient-seven'];\nconst COLOR_MAP = [0, 1, 2, 0, 1, 2, 1];\n\nfunction buildGradientVars(colors) {\n const vars = {};\n for (let i = 0; i < 7; i++) {\n const c = colors[Math.min(COLOR_MAP[i], colors.length - 1)];\n vars[GRADIENT_KEYS[i]] = `radial-gradient(at ${GRADIENT_POSITIONS[i]}, ${c} 0px, transparent 50%)`;\n }\n vars['--gradient-base'] = `linear-gradient(${colors[0]} 0 100%)`;\n return vars;\n}\n\nfunction easeOutCubic(x) { return 1 - Math.pow(1 - x, 3); }\nfunction easeInCubic(x) { return x * x * x; }\n\nfunction animateValue({ start = 0, end = 100, duration = 1000, delay = 0, ease = easeOutCubic, onUpdate, onEnd }) {\n const t0 = performance.now() + delay;\n function tick() {\n const elapsed = performance.now() - t0;\n const t = Math.min(elapsed / duration, 1);\n onUpdate(start + (end - start) * ease(t));\n if (t < 1) requestAnimationFrame(tick);\n else if (onEnd) onEnd();\n }\n setTimeout(() => requestAnimationFrame(tick), delay);\n}\n\nconst BorderGlow = ({\n children,\n className = '',\n edgeSensitivity = 30,\n glowColor = '40 80 80',\n backgroundColor = '#120F17',\n borderRadius = 28,\n glowRadius = 40,\n glowIntensity = 1.0,\n coneSpread = 25,\n animated = false,\n colors = ['#c084fc', '#f472b6', '#38bdf8'],\n fillOpacity = 0.5,\n}) => {\n const cardRef = useRef(null);\n\n const getCenterOfElement = useCallback((el) => {\n const { width, height } = el.getBoundingClientRect();\n return [width / 2, height / 2];\n }, []);\n\n const getEdgeProximity = useCallback((el, x, y) => {\n const [cx, cy] = getCenterOfElement(el);\n const dx = x - cx;\n const dy = y - cy;\n let kx = Infinity;\n let ky = Infinity;\n if (dx !== 0) kx = cx / Math.abs(dx);\n if (dy !== 0) ky = cy / Math.abs(dy);\n return Math.min(Math.max(1 / Math.min(kx, ky), 0), 1);\n }, [getCenterOfElement]);\n\n const getCursorAngle = useCallback((el, x, y) => {\n const [cx, cy] = getCenterOfElement(el);\n const dx = x - cx;\n const dy = y - cy;\n if (dx === 0 && dy === 0) return 0;\n const radians = Math.atan2(dy, dx);\n let degrees = radians * (180 / Math.PI) + 90;\n if (degrees < 0) degrees += 360;\n return degrees;\n }, [getCenterOfElement]);\n\n const handlePointerMove = useCallback((e) => {\n const card = cardRef.current;\n if (!card) return;\n\n const rect = card.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n\n const edge = getEdgeProximity(card, x, y);\n const angle = getCursorAngle(card, x, y);\n\n card.style.setProperty('--edge-proximity', `${(edge * 100).toFixed(3)}`);\n card.style.setProperty('--cursor-angle', `${angle.toFixed(3)}deg`);\n }, [getEdgeProximity, getCursorAngle]);\n\n useEffect(() => {\n if (!animated || !cardRef.current) return;\n const card = cardRef.current;\n const angleStart = 110;\n const angleEnd = 465;\n card.classList.add('sweep-active');\n card.style.setProperty('--cursor-angle', `${angleStart}deg`);\n\n animateValue({ duration: 500, onUpdate: v => card.style.setProperty('--edge-proximity', v) });\n animateValue({ ease: easeInCubic, duration: 1500, end: 50, onUpdate: v => {\n card.style.setProperty('--cursor-angle', `${(angleEnd - angleStart) * (v / 100) + angleStart}deg`);\n }});\n animateValue({ ease: easeOutCubic, delay: 1500, duration: 2250, start: 50, end: 100, onUpdate: v => {\n card.style.setProperty('--cursor-angle', `${(angleEnd - angleStart) * (v / 100) + angleStart}deg`);\n }});\n animateValue({ ease: easeInCubic, delay: 2500, duration: 1500, start: 100, end: 0,\n onUpdate: v => card.style.setProperty('--edge-proximity', v),\n onEnd: () => card.classList.remove('sweep-active'),\n });\n }, [animated]);\n\n const glowVars = buildGlowVars(glowColor, glowIntensity);\n\n return (\n \n \n
\n {children}\n
\n \n );\n};\n\nexport default BorderGlow;\n" } ], diff --git a/public/r/BorderGlow-TS-CSS.json b/public/r/BorderGlow-TS-CSS.json index ced2c7f2..2a208693 100644 --- a/public/r/BorderGlow-TS-CSS.json +++ b/public/r/BorderGlow-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "BorderGlow/BorderGlow.css", + "type": "registry:file", + "path": "BorderGlow.css", + "target": "@components/BorderGlow.css", "content": ".border-glow-card {\n --edge-proximity: 0;\n --cursor-angle: 45deg;\n --edge-sensitivity: 30;\n --color-sensitivity: calc(var(--edge-sensitivity) + 20);\n --border-radius: 28px;\n --glow-padding: 40px;\n --cone-spread: 25;\n\n position: relative;\n border-radius: var(--border-radius);\n isolation: isolate;\n transform: translate3d(0, 0, 0.01px);\n display: grid;\n border: 1px solid rgb(255 255 255 / 15%);\n background: var(--card-bg, #120F17);\n overflow: visible;\n box-shadow:\n rgba(0, 0, 0, 0.1) 0px 1px 2px,\n rgba(0, 0, 0, 0.1) 0px 2px 4px,\n rgba(0, 0, 0, 0.1) 0px 4px 8px,\n rgba(0, 0, 0, 0.1) 0px 8px 16px,\n rgba(0, 0, 0, 0.1) 0px 16px 32px,\n rgba(0, 0, 0, 0.1) 0px 32px 64px;\n}\n\n.border-glow-card::before,\n.border-glow-card::after,\n.border-glow-card > .edge-light {\n content: \"\";\n position: absolute;\n inset: 0;\n border-radius: inherit;\n transition: opacity 0.25s ease-out;\n z-index: -1;\n}\n\n.border-glow-card:not(:hover):not(.sweep-active)::before,\n.border-glow-card:not(:hover):not(.sweep-active)::after,\n.border-glow-card:not(:hover):not(.sweep-active) > .edge-light {\n opacity: 0;\n transition: opacity 0.75s ease-in-out;\n}\n\n/* colored mesh-gradient border */\n.border-glow-card::before {\n border: 1px solid transparent;\n background:\n linear-gradient(var(--card-bg, #120F17) 0 100%) padding-box,\n linear-gradient(rgb(255 255 255 / 0%) 0% 100%) border-box,\n var(--gradient-one, radial-gradient(at 80% 55%, hsla(268, 100%, 76%, 1) 0px, transparent 50%)) border-box,\n var(--gradient-two, radial-gradient(at 69% 34%, hsla(349, 100%, 74%, 1) 0px, transparent 50%)) border-box,\n var(--gradient-three, radial-gradient(at 8% 6%, hsla(136, 100%, 78%, 1) 0px, transparent 50%)) border-box,\n var(--gradient-four, radial-gradient(at 41% 38%, hsla(192, 100%, 64%, 1) 0px, transparent 50%)) border-box,\n var(--gradient-five, radial-gradient(at 86% 85%, hsla(186, 100%, 74%, 1) 0px, transparent 50%)) border-box,\n var(--gradient-six, radial-gradient(at 82% 18%, hsla(52, 100%, 65%, 1) 0px, transparent 50%)) border-box,\n var(--gradient-seven, radial-gradient(at 51% 4%, hsla(12, 100%, 72%, 1) 0px, transparent 50%)) border-box,\n var(--gradient-base, linear-gradient(#c299ff 0 100%)) border-box;\n\n opacity: calc((var(--edge-proximity) - var(--color-sensitivity)) / (100 - var(--color-sensitivity)));\n\n mask-image:\n conic-gradient(\n from var(--cursor-angle) at center,\n black calc(var(--cone-spread) * 1%),\n transparent calc((var(--cone-spread) + 15) * 1%),\n transparent calc((100 - var(--cone-spread) - 15) * 1%),\n black calc((100 - var(--cone-spread)) * 1%)\n );\n}\n\n/* colored mesh-gradient background fill near edges */\n.border-glow-card::after {\n border: 1px solid transparent;\n background:\n var(--gradient-one, radial-gradient(at 80% 55%, hsla(268, 100%, 76%, 1) 0px, transparent 50%)) padding-box,\n var(--gradient-two, radial-gradient(at 69% 34%, hsla(349, 100%, 74%, 1) 0px, transparent 50%)) padding-box,\n var(--gradient-three, radial-gradient(at 8% 6%, hsla(136, 100%, 78%, 1) 0px, transparent 50%)) padding-box,\n var(--gradient-four, radial-gradient(at 41% 38%, hsla(192, 100%, 64%, 1) 0px, transparent 50%)) padding-box,\n var(--gradient-five, radial-gradient(at 86% 85%, hsla(186, 100%, 74%, 1) 0px, transparent 50%)) padding-box,\n var(--gradient-six, radial-gradient(at 82% 18%, hsla(52, 100%, 65%, 1) 0px, transparent 50%)) padding-box,\n var(--gradient-seven, radial-gradient(at 51% 4%, hsla(12, 100%, 72%, 1) 0px, transparent 50%)) padding-box,\n var(--gradient-base, linear-gradient(#c299ff 0 100%)) padding-box;\n\n mask-image:\n linear-gradient(to bottom, black, black),\n radial-gradient(ellipse at 50% 50%, black 40%, transparent 65%),\n radial-gradient(ellipse at 66% 66%, black 5%, transparent 40%),\n radial-gradient(ellipse at 33% 33%, black 5%, transparent 40%),\n radial-gradient(ellipse at 66% 33%, black 5%, transparent 40%),\n radial-gradient(ellipse at 33% 66%, black 5%, transparent 40%),\n conic-gradient(from var(--cursor-angle) at center, transparent 5%, black 15%, black 85%, transparent 95%);\n\n mask-composite: subtract, add, add, add, add, add;\n opacity: calc(var(--fill-opacity, 0.5) * (var(--edge-proximity) - var(--color-sensitivity)) / (100 - var(--color-sensitivity)));\n mix-blend-mode: soft-light;\n}\n\n/* outer glow layer */\n.border-glow-card > .edge-light {\n inset: calc(var(--glow-padding) * -1);\n pointer-events: none;\n z-index: 1;\n\n mask-image:\n conic-gradient(\n from var(--cursor-angle) at center, black 2.5%, transparent 10%, transparent 90%, black 97.5%\n );\n\n opacity: calc((var(--edge-proximity) - var(--edge-sensitivity)) / (100 - var(--edge-sensitivity)));\n mix-blend-mode: plus-lighter;\n}\n\n.border-glow-card > .edge-light::before {\n content: \"\";\n position: absolute;\n inset: var(--glow-padding);\n border-radius: inherit;\n box-shadow:\n inset 0 0 0 1px var(--glow-color, hsl(40deg 80% 80% / 100%)),\n inset 0 0 1px 0 var(--glow-color-60, hsl(40deg 80% 80% / 60%)),\n inset 0 0 3px 0 var(--glow-color-50, hsl(40deg 80% 80% / 50%)),\n inset 0 0 6px 0 var(--glow-color-40, hsl(40deg 80% 80% / 40%)),\n inset 0 0 15px 0 var(--glow-color-30, hsl(40deg 80% 80% / 30%)),\n inset 0 0 25px 2px var(--glow-color-20, hsl(40deg 80% 80% / 20%)),\n inset 0 0 50px 2px var(--glow-color-10, hsl(40deg 80% 80% / 10%)),\n 0 0 1px 0 var(--glow-color-60, hsl(40deg 80% 80% / 60%)),\n 0 0 3px 0 var(--glow-color-50, hsl(40deg 80% 80% / 50%)),\n 0 0 6px 0 var(--glow-color-40, hsl(40deg 80% 80% / 40%)),\n 0 0 15px 0 var(--glow-color-30, hsl(40deg 80% 80% / 30%)),\n 0 0 25px 2px var(--glow-color-20, hsl(40deg 80% 80% / 20%)),\n 0 0 50px 2px var(--glow-color-10, hsl(40deg 80% 80% / 10%));\n}\n\n.border-glow-inner {\n display: flex;\n flex-direction: column;\n position: relative;\n overflow: auto;\n z-index: 1;\n}\n" }, { "type": "registry:component", - "path": "BorderGlow/BorderGlow.tsx", + "path": "BorderGlow.tsx", "content": "import { useRef, useCallback, useEffect, type ReactNode } from 'react';\nimport './BorderGlow.css';\n\ninterface BorderGlowProps {\n children?: ReactNode;\n className?: string;\n edgeSensitivity?: number;\n glowColor?: string;\n backgroundColor?: string;\n borderRadius?: number;\n glowRadius?: number;\n glowIntensity?: number;\n coneSpread?: number;\n animated?: boolean;\n colors?: string[];\n fillOpacity?: number;\n}\n\nfunction parseHSL(hslStr: string): { h: number; s: number; l: number } {\n const match = hslStr.match(/([\\d.]+)\\s*([\\d.]+)%?\\s*([\\d.]+)%?/);\n if (!match) return { h: 40, s: 80, l: 80 };\n return { h: parseFloat(match[1]), s: parseFloat(match[2]), l: parseFloat(match[3]) };\n}\n\nfunction buildGlowVars(glowColor: string, intensity: number): Record {\n const { h, s, l } = parseHSL(glowColor);\n const base = `${h}deg ${s}% ${l}%`;\n const opacities = [100, 60, 50, 40, 30, 20, 10];\n const keys = ['', '-60', '-50', '-40', '-30', '-20', '-10'];\n const vars: Record = {};\n for (let i = 0; i < opacities.length; i++) {\n vars[`--glow-color${keys[i]}`] = `hsl(${base} / ${Math.min(opacities[i] * intensity, 100)}%)`;\n }\n return vars;\n}\n\nconst GRADIENT_POSITIONS = ['80% 55%', '69% 34%', '8% 6%', '41% 38%', '86% 85%', '82% 18%', '51% 4%'];\nconst GRADIENT_KEYS = ['--gradient-one', '--gradient-two', '--gradient-three', '--gradient-four', '--gradient-five', '--gradient-six', '--gradient-seven'];\nconst COLOR_MAP = [0, 1, 2, 0, 1, 2, 1];\n\nfunction buildGradientVars(colors: string[]): Record {\n const vars: Record = {};\n for (let i = 0; i < 7; i++) {\n const c = colors[Math.min(COLOR_MAP[i], colors.length - 1)];\n vars[GRADIENT_KEYS[i]] = `radial-gradient(at ${GRADIENT_POSITIONS[i]}, ${c} 0px, transparent 50%)`;\n }\n vars['--gradient-base'] = `linear-gradient(${colors[0]} 0 100%)`;\n return vars;\n}\n\nfunction easeOutCubic(x: number) { return 1 - Math.pow(1 - x, 3); }\nfunction easeInCubic(x: number) { return x * x * x; }\n\ninterface AnimateOpts {\n start?: number; end?: number; duration?: number; delay?: number;\n ease?: (t: number) => number; onUpdate: (v: number) => void; onEnd?: () => void;\n}\n\nfunction animateValue({ start = 0, end = 100, duration = 1000, delay = 0, ease = easeOutCubic, onUpdate, onEnd }: AnimateOpts) {\n const t0 = performance.now() + delay;\n function tick() {\n const elapsed = performance.now() - t0;\n const t = Math.min(elapsed / duration, 1);\n onUpdate(start + (end - start) * ease(t));\n if (t < 1) requestAnimationFrame(tick);\n else if (onEnd) onEnd();\n }\n setTimeout(() => requestAnimationFrame(tick), delay);\n}\n\nconst BorderGlow: React.FC = ({\n children,\n className = '',\n edgeSensitivity = 30,\n glowColor = '40 80 80',\n backgroundColor = '#120F17',\n borderRadius = 28,\n glowRadius = 40,\n glowIntensity = 1.0,\n coneSpread = 25,\n animated = false,\n colors = ['#c084fc', '#f472b6', '#38bdf8'],\n fillOpacity = 0.5,\n}) => {\n const cardRef = useRef(null);\n\n const getCenterOfElement = useCallback((el: HTMLElement) => {\n const { width, height } = el.getBoundingClientRect();\n return [width / 2, height / 2];\n }, []);\n\n const getEdgeProximity = useCallback((el: HTMLElement, x: number, y: number) => {\n const [cx, cy] = getCenterOfElement(el);\n const dx = x - cx;\n const dy = y - cy;\n let kx = Infinity;\n let ky = Infinity;\n if (dx !== 0) kx = cx / Math.abs(dx);\n if (dy !== 0) ky = cy / Math.abs(dy);\n return Math.min(Math.max(1 / Math.min(kx, ky), 0), 1);\n }, [getCenterOfElement]);\n\n const getCursorAngle = useCallback((el: HTMLElement, x: number, y: number) => {\n const [cx, cy] = getCenterOfElement(el);\n const dx = x - cx;\n const dy = y - cy;\n if (dx === 0 && dy === 0) return 0;\n const radians = Math.atan2(dy, dx);\n let degrees = radians * (180 / Math.PI) + 90;\n if (degrees < 0) degrees += 360;\n return degrees;\n }, [getCenterOfElement]);\n\n const handlePointerMove = useCallback((e: React.PointerEvent) => {\n const card = cardRef.current;\n if (!card) return;\n\n const rect = card.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n\n const edge = getEdgeProximity(card, x, y);\n const angle = getCursorAngle(card, x, y);\n\n card.style.setProperty('--edge-proximity', `${(edge * 100).toFixed(3)}`);\n card.style.setProperty('--cursor-angle', `${angle.toFixed(3)}deg`);\n }, [getEdgeProximity, getCursorAngle]);\n\n useEffect(() => {\n if (!animated || !cardRef.current) return;\n const card = cardRef.current;\n const angleStart = 110;\n const angleEnd = 465;\n card.classList.add('sweep-active');\n card.style.setProperty('--cursor-angle', `${angleStart}deg`);\n\n animateValue({ duration: 500, onUpdate: v => card.style.setProperty('--edge-proximity', `${v}`) });\n animateValue({ ease: easeInCubic, duration: 1500, end: 50, onUpdate: v => {\n card.style.setProperty('--cursor-angle', `${(angleEnd - angleStart) * (v / 100) + angleStart}deg`);\n }});\n animateValue({ ease: easeOutCubic, delay: 1500, duration: 2250, start: 50, end: 100, onUpdate: v => {\n card.style.setProperty('--cursor-angle', `${(angleEnd - angleStart) * (v / 100) + angleStart}deg`);\n }});\n animateValue({ ease: easeInCubic, delay: 2500, duration: 1500, start: 100, end: 0,\n onUpdate: v => card.style.setProperty('--edge-proximity', `${v}`),\n onEnd: () => card.classList.remove('sweep-active'),\n });\n }, [animated]);\n\n const glowVars = buildGlowVars(glowColor, glowIntensity);\n\n return (\n \n \n
\n {children}\n
\n \n );\n};\n\nexport default BorderGlow;\n" } ], diff --git a/public/r/BounceCards-JS-CSS.json b/public/r/BounceCards-JS-CSS.json index efe89a06..640b0e6e 100644 --- a/public/r/BounceCards-JS-CSS.json +++ b/public/r/BounceCards-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "BounceCards/BounceCards.css", + "type": "registry:file", + "path": "BounceCards.css", + "target": "@components/BounceCards.css", "content": ".bounceCardsContainer {\n position: relative;\n display: flex;\n justify-content: center;\n align-items: center;\n width: 400px;\n height: 400px;\n}\n\n.card {\n position: absolute;\n width: 200px;\n aspect-ratio: 1;\n border: 5px solid #fff;\n border-radius: 25px;\n overflow: hidden;\n box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);\n}\n\n.card .image {\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n" }, { "type": "registry:component", - "path": "BounceCards/BounceCards.jsx", + "path": "BounceCards.jsx", "content": "import { useEffect, useRef } from 'react';\nimport { gsap } from 'gsap';\nimport './BounceCards.css';\n\nexport default function BounceCards({\n className = '',\n images = [],\n containerWidth = 400,\n containerHeight = 400,\n animationDelay = 0.5,\n animationStagger = 0.06,\n easeType = 'elastic.out(1, 0.8)',\n transformStyles = [\n 'rotate(10deg) translate(-170px)',\n 'rotate(5deg) translate(-85px)',\n 'rotate(-3deg)',\n 'rotate(-10deg) translate(85px)',\n 'rotate(2deg) translate(170px)'\n ],\n enableHover = true\n}) {\n const containerRef = useRef(null);\n useEffect(() => {\n const ctx = gsap.context(() => {\n gsap.fromTo(\n '.card',\n { scale: 0 },\n {\n scale: 1,\n stagger: animationStagger,\n ease: easeType,\n delay: animationDelay\n }\n );\n }, containerRef);\n return () => ctx.revert();\n }, [animationStagger, easeType, animationDelay]);\n\n const getNoRotationTransform = transformStr => {\n const hasRotate = /rotate\\([\\s\\S]*?\\)/.test(transformStr);\n if (hasRotate) {\n return transformStr.replace(/rotate\\([\\s\\S]*?\\)/, 'rotate(0deg)');\n } else if (transformStr === 'none') {\n return 'rotate(0deg)';\n } else {\n return `${transformStr} rotate(0deg)`;\n }\n };\n\n const getPushedTransform = (baseTransform, offsetX) => {\n const translateRegex = /translate\\(([-0-9.]+)px\\)/;\n const match = baseTransform.match(translateRegex);\n if (match) {\n const currentX = parseFloat(match[1]);\n const newX = currentX + offsetX;\n return baseTransform.replace(translateRegex, `translate(${newX}px)`);\n } else {\n return baseTransform === 'none' ? `translate(${offsetX}px)` : `${baseTransform} translate(${offsetX}px)`;\n }\n };\n\n const pushSiblings = hoveredIdx => {\n if (!enableHover || !containerRef.current) return;\n\n const q = gsap.utils.selector(containerRef);\n\n images.forEach((_, i) => {\n const target = q(`.card-${i}`);\n gsap.killTweensOf(target);\n\n const baseTransform = transformStyles[i] || 'none';\n\n if (i === hoveredIdx) {\n const noRotationTransform = getNoRotationTransform(baseTransform);\n gsap.to(target, {\n transform: noRotationTransform,\n duration: 0.4,\n ease: 'back.out(1.4)',\n overwrite: 'auto'\n });\n } else {\n const offsetX = i < hoveredIdx ? -160 : 160;\n const pushedTransform = getPushedTransform(baseTransform, offsetX);\n\n const distance = Math.abs(hoveredIdx - i);\n const delay = distance * 0.05;\n\n gsap.to(target, {\n transform: pushedTransform,\n duration: 0.4,\n ease: 'back.out(1.4)',\n delay,\n overwrite: 'auto'\n });\n }\n });\n };\n\n const resetSiblings = () => {\n if (!enableHover || !containerRef.current) return;\n\n const q = gsap.utils.selector(containerRef);\n\n images.forEach((_, i) => {\n const target = q(`.card-${i}`);\n gsap.killTweensOf(target);\n const baseTransform = transformStyles[i] || 'none';\n gsap.to(target, {\n transform: baseTransform,\n duration: 0.4,\n ease: 'back.out(1.4)',\n overwrite: 'auto'\n });\n });\n };\n\n return (\n \n {images.map((src, idx) => (\n pushSiblings(idx)}\n onMouseLeave={resetSiblings}\n >\n {`card-${idx}`}\n \n ))}\n \n );\n}\n" } ], diff --git a/public/r/BounceCards-TS-CSS.json b/public/r/BounceCards-TS-CSS.json index 0f157565..c7ad50a7 100644 --- a/public/r/BounceCards-TS-CSS.json +++ b/public/r/BounceCards-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "BounceCards/BounceCards.css", + "type": "registry:file", + "path": "BounceCards.css", + "target": "@components/BounceCards.css", "content": ".bounceCardsContainer {\n position: relative;\n display: flex;\n justify-content: center;\n align-items: center;\n width: 400px;\n height: 400px;\n}\n\n.card {\n position: absolute;\n width: 200px;\n aspect-ratio: 1;\n border: 5px solid #fff;\n border-radius: 25px;\n overflow: hidden;\n box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);\n}\n\n.card .image {\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n" }, { "type": "registry:component", - "path": "BounceCards/BounceCards.tsx", + "path": "BounceCards.tsx", "content": "import { useEffect, useRef } from 'react';\nimport { gsap } from 'gsap';\nimport './BounceCards.css';\n\ninterface BounceCardsProps {\n className?: string;\n images?: string[];\n containerWidth?: number;\n containerHeight?: number;\n animationDelay?: number;\n animationStagger?: number;\n easeType?: string;\n transformStyles?: string[];\n enableHover?: boolean;\n}\n\nexport default function BounceCards({\n className = '',\n images = [],\n containerWidth = 400,\n containerHeight = 400,\n animationDelay = 0.5,\n animationStagger = 0.06,\n easeType = 'elastic.out(1, 0.8)',\n transformStyles = [\n 'rotate(10deg) translate(-170px)',\n 'rotate(5deg) translate(-85px)',\n 'rotate(-3deg)',\n 'rotate(-10deg) translate(85px)',\n 'rotate(2deg) translate(170px)'\n ],\n enableHover = false\n}: BounceCardsProps) {\n const containerRef = useRef(null);\n\n useEffect(() => {\n const ctx = gsap.context(() => {\n gsap.fromTo(\n '.card',\n { scale: 0 },\n {\n scale: 1,\n stagger: animationStagger,\n ease: easeType,\n delay: animationDelay\n }\n );\n }, containerRef);\n return () => ctx.revert();\n }, [animationStagger, easeType, animationDelay]);\n\n const getNoRotationTransform = (transformStr: string): string => {\n const hasRotate = /rotate\\([\\s\\S]*?\\)/.test(transformStr);\n if (hasRotate) {\n return transformStr.replace(/rotate\\([\\s\\S]*?\\)/, 'rotate(0deg)');\n } else if (transformStr === 'none') {\n return 'rotate(0deg)';\n } else {\n return `${transformStr} rotate(0deg)`;\n }\n };\n\n const getPushedTransform = (baseTransform: string, offsetX: number): string => {\n const translateRegex = /translate\\(([-0-9.]+)px\\)/;\n const match = baseTransform.match(translateRegex);\n if (match) {\n const currentX = parseFloat(match[1]);\n const newX = currentX + offsetX;\n return baseTransform.replace(translateRegex, `translate(${newX}px)`);\n } else {\n return baseTransform === 'none' ? `translate(${offsetX}px)` : `${baseTransform} translate(${offsetX}px)`;\n }\n };\n\n const pushSiblings = (hoveredIdx: number) => {\n if (!enableHover || !containerRef.current) return;\n const q = gsap.utils.selector(containerRef);\n images.forEach((_, i) => {\n const selector = q(`.card-${i}`);\n gsap.killTweensOf(selector);\n\n const baseTransform = transformStyles[i] || 'none';\n\n if (i === hoveredIdx) {\n const noRotation = getNoRotationTransform(baseTransform);\n gsap.to(selector, {\n transform: noRotation,\n duration: 0.4,\n ease: 'back.out(1.4)',\n overwrite: 'auto'\n });\n } else {\n const offsetX = i < hoveredIdx ? -160 : 160;\n const pushedTransform = getPushedTransform(baseTransform, offsetX);\n\n const distance = Math.abs(hoveredIdx - i);\n const delay = distance * 0.05;\n\n gsap.to(selector, {\n transform: pushedTransform,\n duration: 0.4,\n ease: 'back.out(1.4)',\n delay,\n overwrite: 'auto'\n });\n }\n });\n };\n\n const resetSiblings = () => {\n if (!enableHover || !containerRef.current) return;\n const q = gsap.utils.selector(containerRef);\n images.forEach((_, i) => {\n const selector = q(`.card-${i}`);\n gsap.killTweensOf(selector);\n const baseTransform = transformStyles[i] || 'none';\n gsap.to(selector, {\n transform: baseTransform,\n duration: 0.4,\n ease: 'back.out(1.4)',\n overwrite: 'auto'\n });\n });\n };\n\n return (\n \n {images.map((src, idx) => (\n pushSiblings(idx)}\n onMouseLeave={resetSiblings}\n >\n {`card-${idx}`}\n \n ))}\n \n );\n}\n" } ], diff --git a/public/r/BubbleMenu-JS-CSS.json b/public/r/BubbleMenu-JS-CSS.json index f7b22bde..494ec220 100644 --- a/public/r/BubbleMenu-JS-CSS.json +++ b/public/r/BubbleMenu-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "BubbleMenu/BubbleMenu.css", + "type": "registry:file", + "path": "BubbleMenu.css", + "target": "@components/BubbleMenu.css", "content": ".bubble-menu {\n left: 0;\n right: 0;\n top: 2em;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n padding: 0 2em;\n pointer-events: none;\n z-index: 99;\n}\n\n.bubble-menu.fixed {\n position: fixed;\n}\n\n.bubble-menu.absolute {\n position: absolute;\n}\n\n.bubble-menu .bubble {\n --bubble-size: 48px;\n width: var(--bubble-size);\n height: var(--bubble-size);\n border-radius: 50%;\n background: #fff;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n pointer-events: auto;\n}\n\n.bubble-menu .logo-bubble,\n.bubble-menu .toggle-bubble {\n will-change: transform;\n}\n\n.bubble-menu .logo-bubble {\n width: auto;\n min-height: var(--bubble-size);\n height: var(--bubble-size);\n padding: 0 16px;\n border-radius: calc(var(--bubble-size) / 2);\n gap: 8px;\n}\n\n.bubble-menu .toggle-bubble {\n width: var(--bubble-size);\n height: var(--bubble-size);\n}\n\n.bubble-menu .bubble-logo {\n max-height: 60%;\n max-width: 100%;\n object-fit: contain;\n display: block;\n}\n\n.bubble-menu .logo-content {\n --logo-max-height: 60%;\n --logo-max-width: 100%;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 120px;\n height: 100%;\n}\n\n.bubble-menu .logo-content > .bubble-logo,\n.bubble-menu .logo-content > img,\n.bubble-menu .logo-content > svg {\n max-height: var(--logo-max-height);\n max-width: var(--logo-max-width);\n}\n\n.bubble-menu .menu-btn {\n border: none;\n background: #fff;\n cursor: pointer;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 0;\n}\n\n.bubble-menu .menu-line {\n width: 26px;\n height: 2px;\n background: #111;\n border-radius: 2px;\n display: block;\n margin: 0 auto;\n transition:\n transform 0.3s ease,\n opacity 0.3s ease;\n transform-origin: center;\n}\n\n.bubble-menu .menu-line + .menu-line {\n margin-top: 6px;\n}\n\n.bubble-menu .menu-btn.open .menu-line:first-child {\n transform: translateY(4px) rotate(45deg);\n}\n\n.bubble-menu .menu-btn.open .menu-line:last-child {\n transform: translateY(-4px) rotate(-45deg);\n}\n\n@media (min-width: 768px) {\n .bubble-menu .bubble {\n --bubble-size: 56px;\n }\n\n .bubble-menu .logo-bubble {\n padding: 0 16px;\n }\n}\n\n.bubble-menu-items {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n pointer-events: none;\n z-index: 98;\n}\n\n.bubble-menu-items.fixed {\n position: fixed;\n}\n\n.bubble-menu-items.absolute {\n position: absolute;\n}\n\n.bubble-menu-items .pill-list {\n list-style: none;\n margin: 0;\n padding: 0 24px;\n display: flex;\n flex-wrap: wrap;\n gap: 0;\n row-gap: 4px;\n width: 100%;\n max-width: 1600px;\n margin-left: auto;\n margin-right: auto;\n pointer-events: auto;\n justify-content: stretch;\n}\n\n.bubble-menu-items .pill-list .pill-spacer {\n width: 100%;\n height: 0;\n pointer-events: none;\n}\n\n.bubble-menu-items .pill-list .pill-col {\n display: flex;\n justify-content: center;\n align-items: stretch;\n flex: 0 0 calc(100% / 3);\n box-sizing: border-box;\n}\n\n.bubble-menu-items .pill-list .pill-col:nth-child(4):nth-last-child(2) {\n margin-left: calc(100% / 6);\n}\n\n.bubble-menu-items .pill-list .pill-col:nth-child(4):last-child {\n margin-left: calc(100% / 3);\n}\n\n.bubble-menu-items .pill-link {\n --pill-bg: #ffffff;\n --pill-color: #111;\n --pill-border: rgba(0, 0, 0, 0.12);\n --item-rot: 0deg;\n --pill-min-h: 160px;\n --hover-bg: #f3f4f6;\n --hover-color: #111;\n width: 100%;\n min-height: var(--pill-min-h);\n padding: clamp(1.5rem, 3vw, 8rem) 0;\n font-size: clamp(1.5rem, 4vw, 4rem);\n font-weight: 400;\n line-height: 0;\n border-radius: 999px;\n background: var(--pill-bg);\n color: var(--pill-color);\n text-decoration: none;\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.1);\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n transition:\n background 0.3s ease,\n color 0.3s ease;\n will-change: transform;\n box-sizing: border-box;\n white-space: nowrap;\n overflow: hidden;\n height: 10px;\n}\n\n@media (min-width: 900px) {\n .bubble-menu-items .pill-link {\n transform: rotate(var(--item-rot));\n }\n\n .bubble-menu-items .pill-link:hover {\n transform: rotate(var(--item-rot)) scale(1.06);\n background: var(--hover-bg);\n color: var(--hover-color);\n }\n\n .bubble-menu-items .pill-link:active {\n transform: rotate(var(--item-rot)) scale(0.94);\n }\n}\n\n.bubble-menu-items .pill-link .pill-label {\n display: inline-block;\n will-change: transform, opacity;\n height: 1.2em;\n line-height: 1.2;\n}\n\n@media (max-width: 899px) {\n .bubble-menu-items {\n padding-top: 0px;\n align-items: flex-start;\n padding-top: 120px;\n }\n\n .bubble-menu-items .pill-list {\n row-gap: 16px;\n }\n\n .bubble-menu-items .pill-list .pill-col {\n flex: 0 0 100%;\n margin-left: 0 !important;\n overflow: visible;\n }\n\n .bubble-menu-items .pill-link {\n font-size: clamp(1.2rem, 3vw, 4rem);\n padding: clamp(1rem, 2vw, 2rem) 0;\n min-height: 80px;\n }\n\n .bubble-menu-items .pill-link:hover {\n transform: scale(1.06);\n background: var(--hover-bg);\n color: var(--hover-color);\n }\n\n .bubble-menu-items .pill-link:active {\n transform: scale(0.94);\n }\n}\n" }, { "type": "registry:component", - "path": "BubbleMenu/BubbleMenu.jsx", + "path": "BubbleMenu.jsx", "content": "import { useState, useRef, useEffect } from 'react';\nimport { gsap } from 'gsap';\n\nimport './BubbleMenu.css';\n\nconst DEFAULT_ITEMS = [\n {\n label: 'home',\n href: '#',\n ariaLabel: 'Home',\n rotation: -8,\n hoverStyles: { bgColor: '#3b82f6', textColor: '#ffffff' }\n },\n {\n label: 'about',\n href: '#',\n ariaLabel: 'About',\n rotation: 8,\n hoverStyles: { bgColor: '#10b981', textColor: '#ffffff' }\n },\n {\n label: 'projects',\n href: '#',\n ariaLabel: 'Documentation',\n rotation: 8,\n hoverStyles: { bgColor: '#f59e0b', textColor: '#ffffff' }\n },\n {\n label: 'blog',\n href: '#',\n ariaLabel: 'Blog',\n rotation: 8,\n hoverStyles: { bgColor: '#ef4444', textColor: '#ffffff' }\n },\n {\n label: 'contact',\n href: '#',\n ariaLabel: 'Contact',\n rotation: -8,\n hoverStyles: { bgColor: '#8b5cf6', textColor: '#ffffff' }\n }\n];\n\nexport default function BubbleMenu({\n logo,\n onMenuClick,\n className,\n style,\n menuAriaLabel = 'Toggle menu',\n menuBg = '#fff',\n menuContentColor = '#111',\n useFixedPosition = false,\n items,\n animationEase = 'back.out(1.5)',\n animationDuration = 0.5,\n staggerDelay = 0.12\n}) {\n const [isMenuOpen, setIsMenuOpen] = useState(false);\n const [showOverlay, setShowOverlay] = useState(false);\n\n const overlayRef = useRef(null);\n const bubblesRef = useRef([]);\n const labelRefs = useRef([]);\n\n const menuItems = items?.length ? items : DEFAULT_ITEMS;\n const containerClassName = ['bubble-menu', useFixedPosition ? 'fixed' : 'absolute', className]\n .filter(Boolean)\n .join(' ');\n\n const handleToggle = () => {\n const nextState = !isMenuOpen;\n if (nextState) setShowOverlay(true);\n setIsMenuOpen(nextState);\n onMenuClick?.(nextState);\n };\n\n useEffect(() => {\n const overlay = overlayRef.current;\n const bubbles = bubblesRef.current.filter(Boolean);\n const labels = labelRefs.current.filter(Boolean);\n\n if (!overlay || !bubbles.length) return;\n\n if (isMenuOpen) {\n gsap.set(overlay, { display: 'flex' });\n gsap.killTweensOf([...bubbles, ...labels]);\n gsap.set(bubbles, { scale: 0, transformOrigin: '50% 50%' });\n gsap.set(labels, { y: 24, autoAlpha: 0 });\n\n bubbles.forEach((bubble, i) => {\n const delay = i * staggerDelay + gsap.utils.random(-0.05, 0.05);\n const tl = gsap.timeline({ delay });\n\n tl.to(bubble, {\n scale: 1,\n duration: animationDuration,\n ease: animationEase\n });\n if (labels[i]) {\n tl.to(\n labels[i],\n {\n y: 0,\n autoAlpha: 1,\n duration: animationDuration,\n ease: 'power3.out'\n },\n `-=${animationDuration * 0.9}`\n );\n }\n });\n } else if (showOverlay) {\n gsap.killTweensOf([...bubbles, ...labels]);\n gsap.to(labels, {\n y: 24,\n autoAlpha: 0,\n duration: 0.2,\n ease: 'power3.in'\n });\n gsap.to(bubbles, {\n scale: 0,\n duration: 0.2,\n ease: 'power3.in',\n onComplete: () => {\n gsap.set(overlay, { display: 'none' });\n setShowOverlay(false);\n }\n });\n }\n }, [isMenuOpen, showOverlay, animationEase, animationDuration, staggerDelay]);\n\n useEffect(() => {\n const handleResize = () => {\n if (isMenuOpen) {\n const bubbles = bubblesRef.current.filter(Boolean);\n const isDesktop = window.innerWidth >= 900;\n\n bubbles.forEach((bubble, i) => {\n const item = menuItems[i];\n if (bubble && item) {\n const rotation = isDesktop ? (item.rotation ?? 0) : 0;\n gsap.set(bubble, { rotation });\n }\n });\n }\n };\n\n window.addEventListener('resize', handleResize);\n return () => window.removeEventListener('resize', handleResize);\n }, [isMenuOpen, menuItems]);\n\n return (\n <>\n \n {showOverlay && (\n \n
    \n {menuItems.map((item, idx) => (\n
  • \n {\n if (el) bubblesRef.current[idx] = el;\n }}\n >\n {\n if (el) labelRefs.current[idx] = el;\n }}\n >\n {item.label}\n \n \n
  • \n ))}\n
\n \n )}\n \n );\n}\n" } ], diff --git a/public/r/BubbleMenu-TS-CSS.json b/public/r/BubbleMenu-TS-CSS.json index 60f502ae..f7da51ff 100644 --- a/public/r/BubbleMenu-TS-CSS.json +++ b/public/r/BubbleMenu-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "BubbleMenu/BubbleMenu.css", + "type": "registry:file", + "path": "BubbleMenu.css", + "target": "@components/BubbleMenu.css", "content": ".bubble-menu {\n left: 0;\n right: 0;\n top: 2em;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n padding: 0 2em;\n pointer-events: none;\n z-index: 99;\n}\n\n.bubble-menu.fixed {\n position: fixed;\n}\n\n.bubble-menu.absolute {\n position: absolute;\n}\n\n.bubble-menu .bubble {\n --bubble-size: 48px;\n width: var(--bubble-size);\n height: var(--bubble-size);\n border-radius: 50%;\n background: #fff;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n pointer-events: auto;\n}\n\n.bubble-menu .logo-bubble,\n.bubble-menu .toggle-bubble {\n will-change: transform;\n}\n\n.bubble-menu .logo-bubble {\n width: auto;\n min-height: var(--bubble-size);\n height: var(--bubble-size);\n padding: 0 16px;\n border-radius: calc(var(--bubble-size) / 2);\n gap: 8px;\n}\n\n.bubble-menu .toggle-bubble {\n width: var(--bubble-size);\n height: var(--bubble-size);\n}\n\n.bubble-menu .bubble-logo {\n max-height: 60%;\n max-width: 100%;\n object-fit: contain;\n display: block;\n}\n\n.bubble-menu .logo-content {\n --logo-max-height: 60%;\n --logo-max-width: 100%;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 120px;\n height: 100%;\n}\n\n.bubble-menu .logo-content > .bubble-logo,\n.bubble-menu .logo-content > img,\n.bubble-menu .logo-content > svg {\n max-height: var(--logo-max-height);\n max-width: var(--logo-max-width);\n}\n\n.bubble-menu .menu-btn {\n border: none;\n background: #fff;\n cursor: pointer;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 0;\n}\n\n.bubble-menu .menu-line {\n width: 26px;\n height: 2px;\n background: #111;\n border-radius: 2px;\n display: block;\n margin: 0 auto;\n transition:\n transform 0.3s ease,\n opacity 0.3s ease;\n transform-origin: center;\n}\n\n.bubble-menu .menu-line + .menu-line {\n margin-top: 6px;\n}\n\n.bubble-menu .menu-btn.open .menu-line:first-child {\n transform: translateY(4px) rotate(45deg);\n}\n\n.bubble-menu .menu-btn.open .menu-line:last-child {\n transform: translateY(-4px) rotate(-45deg);\n}\n\n@media (min-width: 768px) {\n .bubble-menu .bubble {\n --bubble-size: 56px;\n }\n\n .bubble-menu .logo-bubble {\n padding: 0 16px;\n }\n}\n\n.bubble-menu-items {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n pointer-events: none;\n z-index: 98;\n}\n\n.bubble-menu-items.fixed {\n position: fixed;\n}\n\n.bubble-menu-items.absolute {\n position: absolute;\n}\n\n.bubble-menu-items .pill-list {\n list-style: none;\n margin: 0;\n padding: 0 24px;\n display: flex;\n flex-wrap: wrap;\n gap: 0;\n row-gap: 4px;\n width: 100%;\n max-width: 1600px;\n margin-left: auto;\n margin-right: auto;\n pointer-events: auto;\n justify-content: stretch;\n}\n\n.bubble-menu-items .pill-list .pill-spacer {\n width: 100%;\n height: 0;\n pointer-events: none;\n}\n\n.bubble-menu-items .pill-list .pill-col {\n display: flex;\n justify-content: center;\n align-items: stretch;\n flex: 0 0 calc(100% / 3);\n box-sizing: border-box;\n}\n\n.bubble-menu-items .pill-list .pill-col:nth-child(4):nth-last-child(2) {\n margin-left: calc(100% / 6);\n}\n\n.bubble-menu-items .pill-list .pill-col:nth-child(4):last-child {\n margin-left: calc(100% / 3);\n}\n\n.bubble-menu-items .pill-link {\n --pill-bg: #ffffff;\n --pill-color: #111;\n --pill-border: rgba(0, 0, 0, 0.12);\n --item-rot: 0deg;\n --pill-min-h: 160px;\n --hover-bg: #f3f4f6;\n --hover-color: #111;\n width: 100%;\n min-height: var(--pill-min-h);\n padding: clamp(1.5rem, 3vw, 8rem) 0;\n font-size: clamp(1.5rem, 4vw, 4rem);\n font-weight: 400;\n line-height: 0;\n border-radius: 999px;\n background: var(--pill-bg);\n color: var(--pill-color);\n text-decoration: none;\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.1);\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n transition:\n background 0.3s ease,\n color 0.3s ease;\n will-change: transform;\n box-sizing: border-box;\n white-space: nowrap;\n overflow: hidden;\n height: 10px;\n}\n\n@media (min-width: 900px) {\n .bubble-menu-items .pill-link {\n transform: rotate(var(--item-rot));\n }\n\n .bubble-menu-items .pill-link:hover {\n transform: rotate(var(--item-rot)) scale(1.06);\n background: var(--hover-bg);\n color: var(--hover-color);\n }\n\n .bubble-menu-items .pill-link:active {\n transform: rotate(var(--item-rot)) scale(0.94);\n }\n}\n\n.bubble-menu-items .pill-link .pill-label {\n display: inline-block;\n will-change: transform, opacity;\n height: 1.2em;\n line-height: 1.2;\n}\n\n@media (max-width: 899px) {\n .bubble-menu-items {\n padding-top: 0px;\n align-items: flex-start;\n padding-top: 120px;\n }\n\n .bubble-menu-items .pill-list {\n row-gap: 16px;\n }\n\n .bubble-menu-items .pill-list .pill-col {\n flex: 0 0 100%;\n margin-left: 0 !important;\n overflow: visible;\n }\n\n .bubble-menu-items .pill-link {\n font-size: clamp(1.2rem, 3vw, 4rem);\n padding: clamp(1rem, 2vw, 2rem) 0;\n min-height: 80px;\n }\n\n .bubble-menu-items .pill-link:hover {\n transform: scale(1.06);\n background: var(--hover-bg);\n color: var(--hover-color);\n }\n\n .bubble-menu-items .pill-link:active {\n transform: scale(0.94);\n }\n}\n" }, { "type": "registry:component", - "path": "BubbleMenu/BubbleMenu.tsx", + "path": "BubbleMenu.tsx", "content": "import type { CSSProperties, ReactNode } from 'react';\nimport { useState, useRef, useEffect } from 'react';\nimport { gsap } from 'gsap';\n\nimport './BubbleMenu.css';\n\ntype MenuItem = {\n label: string;\n href: string;\n ariaLabel?: string;\n rotation?: number;\n hoverStyles?: {\n bgColor?: string;\n textColor?: string;\n };\n};\n\nexport type BubbleMenuProps = {\n logo: ReactNode | string;\n onMenuClick?: (open: boolean) => void;\n className?: string;\n style?: CSSProperties;\n menuAriaLabel?: string;\n menuBg?: string;\n menuContentColor?: string;\n useFixedPosition?: boolean;\n items?: MenuItem[];\n animationEase?: string;\n animationDuration?: number;\n staggerDelay?: number;\n};\n\nconst DEFAULT_ITEMS: MenuItem[] = [\n {\n label: 'home',\n href: '#',\n ariaLabel: 'Home',\n rotation: -8,\n hoverStyles: { bgColor: '#3b82f6', textColor: '#ffffff' }\n },\n {\n label: 'about',\n href: '#',\n ariaLabel: 'About',\n rotation: 8,\n hoverStyles: { bgColor: '#10b981', textColor: '#ffffff' }\n },\n {\n label: 'projects',\n href: '#',\n ariaLabel: 'Documentation',\n rotation: 8,\n hoverStyles: { bgColor: '#f59e0b', textColor: '#ffffff' }\n },\n {\n label: 'blog',\n href: '#',\n ariaLabel: 'Blog',\n rotation: 8,\n hoverStyles: { bgColor: '#ef4444', textColor: '#ffffff' }\n },\n {\n label: 'contact',\n href: '#',\n ariaLabel: 'Contact',\n rotation: -8,\n hoverStyles: { bgColor: '#8b5cf6', textColor: '#ffffff' }\n }\n];\n\nexport default function BubbleMenu({\n logo,\n onMenuClick,\n className,\n style,\n menuAriaLabel = 'Toggle menu',\n menuBg = '#fff',\n menuContentColor = '#111',\n useFixedPosition = false,\n items,\n animationEase = 'back.out(1.5)',\n animationDuration = 0.5,\n staggerDelay = 0.12\n}: BubbleMenuProps) {\n const [isMenuOpen, setIsMenuOpen] = useState(false);\n const [showOverlay, setShowOverlay] = useState(false);\n\n const overlayRef = useRef(null);\n const bubblesRef = useRef([]);\n const labelRefs = useRef([]);\n\n const menuItems = items?.length ? items : DEFAULT_ITEMS;\n const containerClassName = ['bubble-menu', useFixedPosition ? 'fixed' : 'absolute', className]\n .filter(Boolean)\n .join(' ');\n\n const handleToggle = () => {\n const nextState = !isMenuOpen;\n if (nextState) setShowOverlay(true);\n setIsMenuOpen(nextState);\n onMenuClick?.(nextState);\n };\n\n useEffect(() => {\n const overlay = overlayRef.current;\n const bubbles = bubblesRef.current.filter(Boolean);\n const labels = labelRefs.current.filter(Boolean);\n\n if (!overlay || !bubbles.length) return;\n\n if (isMenuOpen) {\n gsap.set(overlay, { display: 'flex' });\n gsap.killTweensOf([...bubbles, ...labels]);\n gsap.set(bubbles, { scale: 0, transformOrigin: '50% 50%' });\n gsap.set(labels, { y: 24, autoAlpha: 0 });\n\n bubbles.forEach((bubble, i) => {\n const delay = i * staggerDelay + gsap.utils.random(-0.05, 0.05);\n const tl = gsap.timeline({ delay });\n\n tl.to(bubble, {\n scale: 1,\n duration: animationDuration,\n ease: animationEase\n });\n if (labels[i]) {\n tl.to(\n labels[i],\n {\n y: 0,\n autoAlpha: 1,\n duration: animationDuration,\n ease: 'power3.out'\n },\n `-=${animationDuration * 0.9}`\n );\n }\n });\n } else if (showOverlay) {\n gsap.killTweensOf([...bubbles, ...labels]);\n gsap.to(labels, {\n y: 24,\n autoAlpha: 0,\n duration: 0.2,\n ease: 'power3.in'\n });\n gsap.to(bubbles, {\n scale: 0,\n duration: 0.2,\n ease: 'power3.in',\n onComplete: () => {\n gsap.set(overlay, { display: 'none' });\n setShowOverlay(false);\n }\n });\n }\n }, [isMenuOpen, showOverlay, animationEase, animationDuration, staggerDelay]);\n\n useEffect(() => {\n const handleResize = () => {\n if (isMenuOpen) {\n const bubbles = bubblesRef.current.filter(Boolean);\n const isDesktop = window.innerWidth >= 900;\n\n bubbles.forEach((bubble, i) => {\n const item = menuItems[i];\n if (bubble && item) {\n const rotation = isDesktop ? (item.rotation ?? 0) : 0;\n gsap.set(bubble, { rotation });\n }\n });\n }\n };\n\n window.addEventListener('resize', handleResize);\n return () => window.removeEventListener('resize', handleResize);\n }, [isMenuOpen, menuItems]);\n\n return (\n <>\n \n {showOverlay && (\n \n
    \n {menuItems.map((item, idx) => (\n
  • \n {\n if (el) bubblesRef.current[idx] = el;\n }}\n >\n {\n if (el) labelRefs.current[idx] = el;\n }}\n >\n {item.label}\n \n \n
  • \n ))}\n
\n \n )}\n \n );\n}\n" } ], diff --git a/public/r/CardNav-JS-CSS.json b/public/r/CardNav-JS-CSS.json index 19feded2..a4be34b4 100644 --- a/public/r/CardNav-JS-CSS.json +++ b/public/r/CardNav-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "CardNav/CardNav.css", + "type": "registry:file", + "path": "CardNav.css", + "target": "@components/CardNav.css", "content": ".card-nav-container {\n position: absolute;\n top: 2em;\n left: 50%;\n transform: translateX(-50%);\n width: 90%;\n max-width: 800px;\n z-index: 99;\n box-sizing: border-box;\n}\n\n.card-nav {\n display: block;\n height: 60px;\n padding: 0;\n background-color: white;\n border: 0.5px solid rgba(255, 255, 255, 0.1);\n border-radius: 0.75rem;\n box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n position: relative;\n overflow: hidden;\n will-change: height;\n}\n\n.card-nav-top {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 60px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.5rem 0.45rem 0.55rem 1.1rem;\n z-index: 2;\n}\n\n.hamburger-menu {\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n gap: 6px;\n}\n\n.hamburger-menu:hover .hamburger-line {\n opacity: 0.75;\n}\n\n.hamburger-line {\n width: 30px;\n height: 2px;\n background-color: currentColor;\n transition:\n transform 0.25s ease,\n opacity 0.2s ease,\n margin 0.3s ease;\n transform-origin: 50% 50%;\n}\n\n.hamburger-menu.open .hamburger-line:first-child {\n transform: translateY(4px) rotate(45deg);\n}\n\n.hamburger-menu.open .hamburger-line:last-child {\n transform: translateY(-4px) rotate(-45deg);\n}\n\n.logo-container {\n display: flex;\n align-items: center;\n position: absolute;\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n}\n\n.logo {\n height: 28px;\n}\n\n.card-nav-cta-button {\n background-color: #111;\n color: white;\n border: none;\n border-radius: calc(0.75rem - 0.35rem);\n padding: 0 1rem;\n height: 100%;\n font-weight: 500;\n cursor: pointer;\n transition: background-color 0.3s ease;\n align-items: center;\n}\n\n.card-nav-cta-button:hover {\n background-color: #333;\n}\n\n.card-nav-content {\n position: absolute;\n left: 0;\n right: 0;\n top: 60px;\n bottom: 0;\n padding: 0.5rem;\n display: flex;\n align-items: flex-end;\n gap: 12px;\n visibility: hidden;\n pointer-events: none;\n z-index: 1;\n}\n\n.card-nav.open .card-nav-content {\n visibility: visible;\n pointer-events: auto;\n}\n\n.nav-card {\n height: 100%;\n flex: 1 1 0;\n min-width: 0;\n border-radius: calc(0.75rem - 0.2rem);\n position: relative;\n display: flex;\n flex-direction: column;\n padding: 12px 16px;\n gap: 8px;\n user-select: none;\n}\n\n.nav-card-label {\n font-weight: 400;\n font-size: 22px;\n letter-spacing: -0.5px;\n}\n\n.nav-card-links {\n margin-top: auto;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.nav-card-link {\n font-size: 16px;\n cursor: pointer;\n text-decoration: none;\n transition: opacity 0.3s ease;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.nav-card-link:hover {\n opacity: 0.75;\n}\n\n@media (max-width: 768px) {\n .card-nav-container {\n width: 90%;\n top: 1.2em;\n }\n\n .card-nav-top {\n padding: 0.5rem 1rem;\n justify-content: space-between;\n }\n\n .hamburger-menu {\n order: 2;\n }\n\n .logo-container {\n position: static;\n transform: none;\n order: 1;\n }\n\n .card-nav-cta-button {\n display: none;\n }\n\n .card-nav-content {\n flex-direction: column;\n align-items: stretch;\n gap: 8px;\n padding: 0.5rem;\n bottom: 0;\n justify-content: flex-start;\n }\n\n .nav-card {\n height: auto;\n min-height: 60px;\n flex: 1 1 auto;\n max-height: none;\n }\n\n .nav-card-label {\n font-size: 18px;\n }\n\n .nav-card-link {\n font-size: 15px;\n }\n}\n" }, { "type": "registry:component", - "path": "CardNav/CardNav.jsx", + "path": "CardNav.jsx", "content": "import { useLayoutEffect, useRef, useState } from 'react';\nimport { gsap } from 'gsap';\n// use your own icon import if react-icons is not available\nimport { GoArrowUpRight } from 'react-icons/go';\nimport './CardNav.css';\n\nconst CardNav = ({\n logo,\n logoAlt = 'Logo',\n items,\n className = '',\n ease = 'power3.out',\n baseColor = '#fff',\n menuColor,\n buttonBgColor,\n buttonTextColor\n}) => {\n const [isHamburgerOpen, setIsHamburgerOpen] = useState(false);\n const [isExpanded, setIsExpanded] = useState(false);\n const navRef = useRef(null);\n const cardsRef = useRef([]);\n const tlRef = useRef(null);\n\n const calculateHeight = () => {\n const navEl = navRef.current;\n if (!navEl) return 260;\n\n const isMobile = window.matchMedia('(max-width: 768px)').matches;\n if (isMobile) {\n const contentEl = navEl.querySelector('.card-nav-content');\n if (contentEl) {\n const wasVisible = contentEl.style.visibility;\n const wasPointerEvents = contentEl.style.pointerEvents;\n const wasPosition = contentEl.style.position;\n const wasHeight = contentEl.style.height;\n\n contentEl.style.visibility = 'visible';\n contentEl.style.pointerEvents = 'auto';\n contentEl.style.position = 'static';\n contentEl.style.height = 'auto';\n\n contentEl.offsetHeight;\n\n const topBar = 60;\n const padding = 16;\n const contentHeight = contentEl.scrollHeight;\n\n contentEl.style.visibility = wasVisible;\n contentEl.style.pointerEvents = wasPointerEvents;\n contentEl.style.position = wasPosition;\n contentEl.style.height = wasHeight;\n\n return topBar + contentHeight + padding;\n }\n }\n return 260;\n };\n\n const createTimeline = () => {\n const navEl = navRef.current;\n if (!navEl) return null;\n\n gsap.set(navEl, { height: 60, overflow: 'hidden' });\n gsap.set(cardsRef.current, { y: 50, opacity: 0 });\n\n const tl = gsap.timeline({ paused: true });\n\n tl.to(navEl, {\n height: calculateHeight,\n duration: 0.4,\n ease\n });\n\n tl.to(cardsRef.current, { y: 0, opacity: 1, duration: 0.4, ease, stagger: 0.08 }, '-=0.1');\n\n return tl;\n };\n\n useLayoutEffect(() => {\n const tl = createTimeline();\n tlRef.current = tl;\n\n return () => {\n tl?.kill();\n tlRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [ease, items]);\n\n useLayoutEffect(() => {\n const handleResize = () => {\n if (!tlRef.current) return;\n\n if (isExpanded) {\n const newHeight = calculateHeight();\n gsap.set(navRef.current, { height: newHeight });\n\n tlRef.current.kill();\n const newTl = createTimeline();\n if (newTl) {\n newTl.progress(1);\n tlRef.current = newTl;\n }\n } else {\n tlRef.current.kill();\n const newTl = createTimeline();\n if (newTl) {\n tlRef.current = newTl;\n }\n }\n };\n\n window.addEventListener('resize', handleResize);\n return () => window.removeEventListener('resize', handleResize);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isExpanded]);\n\n const toggleMenu = () => {\n const tl = tlRef.current;\n if (!tl) return;\n if (!isExpanded) {\n setIsHamburgerOpen(true);\n setIsExpanded(true);\n tl.play(0);\n } else {\n setIsHamburgerOpen(false);\n tl.eventCallback('onReverseComplete', () => setIsExpanded(false));\n tl.reverse();\n }\n };\n\n const setCardRef = i => el => {\n if (el) cardsRef.current[i] = el;\n };\n\n return (\n
\n \n
\n );\n};\n\nexport default CardNav;\n" } ], diff --git a/public/r/CardNav-TS-CSS.json b/public/r/CardNav-TS-CSS.json index a32518d5..1824a4cd 100644 --- a/public/r/CardNav-TS-CSS.json +++ b/public/r/CardNav-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "CardNav/CardNav.css", + "type": "registry:file", + "path": "CardNav.css", + "target": "@components/CardNav.css", "content": ".card-nav-container {\n position: absolute;\n top: 2em;\n left: 50%;\n transform: translateX(-50%);\n width: 90%;\n max-width: 800px;\n z-index: 99;\n box-sizing: border-box;\n}\n\n.card-nav {\n display: block;\n height: 60px;\n padding: 0;\n background-color: white;\n border: 0.5px solid rgba(255, 255, 255, 0.1);\n border-radius: 0.75rem;\n box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n position: relative;\n overflow: hidden;\n will-change: height;\n}\n\n.card-nav-top {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 60px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.5rem 0.45rem 0.55rem 1.1rem;\n z-index: 2;\n}\n\n.hamburger-menu {\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n gap: 6px;\n}\n\n.hamburger-menu:hover .hamburger-line {\n opacity: 0.75;\n}\n\n.hamburger-line {\n width: 30px;\n height: 2px;\n background-color: currentColor;\n transition:\n transform 0.25s ease,\n opacity 0.2s ease,\n margin 0.3s ease;\n transform-origin: 50% 50%;\n}\n\n.hamburger-menu.open .hamburger-line:first-child {\n transform: translateY(4px) rotate(45deg);\n}\n\n.hamburger-menu.open .hamburger-line:last-child {\n transform: translateY(-4px) rotate(-45deg);\n}\n\n.logo-container {\n display: flex;\n align-items: center;\n position: absolute;\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n}\n\n.logo {\n height: 28px;\n}\n\n.card-nav-cta-button {\n background-color: #111;\n color: white;\n border: none;\n border-radius: calc(0.75rem - 0.35rem);\n padding: 0 1rem;\n height: 100%;\n font-weight: 500;\n cursor: pointer;\n transition: background-color 0.3s ease;\n align-items: center;\n}\n\n.card-nav-cta-button:hover {\n background-color: #333;\n}\n\n.card-nav-content {\n position: absolute;\n left: 0;\n right: 0;\n top: 60px;\n bottom: 0;\n padding: 0.5rem;\n display: flex;\n align-items: flex-end;\n gap: 12px;\n visibility: hidden;\n pointer-events: none;\n z-index: 1;\n}\n\n.card-nav.open .card-nav-content {\n visibility: visible;\n pointer-events: auto;\n}\n\n.nav-card {\n height: 100%;\n flex: 1 1 0;\n min-width: 0;\n border-radius: calc(0.75rem - 0.2rem);\n position: relative;\n display: flex;\n flex-direction: column;\n padding: 12px 16px;\n gap: 8px;\n user-select: none;\n}\n\n.nav-card-label {\n font-weight: 400;\n font-size: 22px;\n letter-spacing: -0.5px;\n}\n\n.nav-card-links {\n margin-top: auto;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.nav-card-link {\n font-size: 16px;\n cursor: pointer;\n text-decoration: none;\n transition: opacity 0.3s ease;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.nav-card-link:hover {\n opacity: 0.75;\n}\n\n@media (max-width: 768px) {\n .card-nav-container {\n width: 90%;\n top: 1.2em;\n }\n\n .card-nav-top {\n padding: 0.5rem 1rem;\n justify-content: space-between;\n }\n\n .hamburger-menu {\n order: 2;\n }\n\n .logo-container {\n position: static;\n transform: none;\n order: 1;\n }\n\n .card-nav-cta-button {\n display: none;\n }\n\n .card-nav-content {\n flex-direction: column;\n align-items: stretch;\n gap: 8px;\n padding: 0.5rem;\n bottom: 0;\n justify-content: flex-start;\n }\n\n .nav-card {\n height: auto;\n min-height: 60px;\n flex: 1 1 auto;\n max-height: none;\n }\n\n .nav-card-label {\n font-size: 18px;\n }\n\n .nav-card-link {\n font-size: 15px;\n }\n}\n" }, { "type": "registry:component", - "path": "CardNav/CardNav.tsx", + "path": "CardNav.tsx", "content": "import React, { useLayoutEffect, useRef, useState } from 'react';\nimport { gsap } from 'gsap';\n// use your own icon import if react-icons is not available\nimport { GoArrowUpRight } from 'react-icons/go';\nimport './CardNav.css';\n\ntype CardNavLink = {\n label: string;\n href: string;\n ariaLabel: string;\n};\n\nexport type CardNavItem = {\n label: string;\n bgColor: string;\n textColor: string;\n links: CardNavLink[];\n};\n\nexport interface CardNavProps {\n logo: string;\n logoAlt?: string;\n items: CardNavItem[];\n className?: string;\n ease?: string;\n baseColor?: string;\n menuColor?: string;\n buttonBgColor?: string;\n buttonTextColor?: string;\n}\n\nconst CardNav: React.FC = ({\n logo,\n logoAlt = 'Logo',\n items,\n className = '',\n ease = 'power3.out',\n baseColor = '#fff',\n menuColor,\n buttonBgColor,\n buttonTextColor\n}) => {\n const [isHamburgerOpen, setIsHamburgerOpen] = useState(false);\n const [isExpanded, setIsExpanded] = useState(false);\n const navRef = useRef(null);\n const cardsRef = useRef([]);\n const tlRef = useRef(null);\n\n const calculateHeight = () => {\n const navEl = navRef.current;\n if (!navEl) return 260;\n\n const isMobile = window.matchMedia('(max-width: 768px)').matches;\n if (isMobile) {\n const contentEl = navEl.querySelector('.card-nav-content') as HTMLElement;\n if (contentEl) {\n const wasVisible = contentEl.style.visibility;\n const wasPointerEvents = contentEl.style.pointerEvents;\n const wasPosition = contentEl.style.position;\n const wasHeight = contentEl.style.height;\n\n contentEl.style.visibility = 'visible';\n contentEl.style.pointerEvents = 'auto';\n contentEl.style.position = 'static';\n contentEl.style.height = 'auto';\n\n contentEl.offsetHeight;\n\n const topBar = 60;\n const padding = 16;\n const contentHeight = contentEl.scrollHeight;\n\n contentEl.style.visibility = wasVisible;\n contentEl.style.pointerEvents = wasPointerEvents;\n contentEl.style.position = wasPosition;\n contentEl.style.height = wasHeight;\n\n return topBar + contentHeight + padding;\n }\n }\n return 260;\n };\n\n const createTimeline = () => {\n const navEl = navRef.current;\n if (!navEl) return null;\n\n gsap.set(navEl, { height: 60, overflow: 'hidden' });\n gsap.set(cardsRef.current, { y: 50, opacity: 0 });\n\n const tl = gsap.timeline({ paused: true });\n\n tl.to(navEl, {\n height: calculateHeight,\n duration: 0.4,\n ease\n });\n\n tl.to(cardsRef.current, { y: 0, opacity: 1, duration: 0.4, ease, stagger: 0.08 }, '-=0.1');\n\n return tl;\n };\n\n useLayoutEffect(() => {\n const tl = createTimeline();\n tlRef.current = tl;\n\n return () => {\n tl?.kill();\n tlRef.current = null;\n };\n }, [ease, items]);\n\n useLayoutEffect(() => {\n const handleResize = () => {\n if (!tlRef.current) return;\n\n if (isExpanded) {\n const newHeight = calculateHeight();\n gsap.set(navRef.current, { height: newHeight });\n\n tlRef.current.kill();\n const newTl = createTimeline();\n if (newTl) {\n newTl.progress(1);\n tlRef.current = newTl;\n }\n } else {\n tlRef.current.kill();\n const newTl = createTimeline();\n if (newTl) {\n tlRef.current = newTl;\n }\n }\n };\n\n window.addEventListener('resize', handleResize);\n return () => window.removeEventListener('resize', handleResize);\n }, [isExpanded]);\n\n const toggleMenu = () => {\n const tl = tlRef.current;\n if (!tl) return;\n if (!isExpanded) {\n setIsHamburgerOpen(true);\n setIsExpanded(true);\n tl.play(0);\n } else {\n setIsHamburgerOpen(false);\n tl.eventCallback('onReverseComplete', () => setIsExpanded(false));\n tl.reverse();\n }\n };\n\n const setCardRef = (i: number) => (el: HTMLDivElement | null) => {\n if (el) cardsRef.current[i] = el;\n };\n\n return (\n
\n \n
\n );\n};\n\nexport default CardNav;\n" } ], diff --git a/public/r/CardSwap-JS-CSS.json b/public/r/CardSwap-JS-CSS.json index 4d2bada4..b7b40f71 100644 --- a/public/r/CardSwap-JS-CSS.json +++ b/public/r/CardSwap-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "CardSwap/CardSwap.css", + "type": "registry:file", + "path": "CardSwap.css", + "target": "@components/CardSwap.css", "content": ".card-swap-container {\n position: absolute;\n bottom: 0;\n right: 0;\n transform: translate(5%, 20%);\n transform-origin: bottom right;\n\n perspective: 900px;\n overflow: visible;\n}\n\n.card {\n position: absolute;\n top: 50%;\n left: 50%;\n border-radius: 12px;\n border: 1px solid #fff;\n background: #000;\n\n transform-style: preserve-3d;\n will-change: transform;\n backface-visibility: hidden;\n -webkit-backface-visibility: hidden;\n}\n\n@media (max-width: 768px) {\n .card-swap-container {\n transform: scale(0.75) translate(25%, 25%);\n }\n}\n\n@media (max-width: 480px) {\n .card-swap-container {\n transform: scale(0.55) translate(25%, 25%);\n }\n}\n" }, { "type": "registry:component", - "path": "CardSwap/CardSwap.jsx", + "path": "CardSwap.jsx", "content": "import React, { Children, cloneElement, forwardRef, isValidElement, useEffect, useMemo, useRef } from 'react';\nimport gsap from 'gsap';\nimport './CardSwap.css';\n\nexport const Card = forwardRef(({ customClass, ...rest }, ref) => (\n
\n));\nCard.displayName = 'Card';\n\nconst makeSlot = (i, distX, distY, total) => ({\n x: i * distX,\n y: -i * distY,\n z: -i * distX * 1.5,\n zIndex: total - i\n});\nconst placeNow = (el, slot, skew) =>\n gsap.set(el, {\n x: slot.x,\n y: slot.y,\n z: slot.z,\n xPercent: -50,\n yPercent: -50,\n skewY: skew,\n transformOrigin: 'center center',\n zIndex: slot.zIndex,\n force3D: true\n });\n\nconst CardSwap = ({\n width = 500,\n height = 400,\n cardDistance = 60,\n verticalDistance = 70,\n delay = 5000,\n pauseOnHover = false,\n onCardClick,\n skewAmount = 6,\n easing = 'elastic',\n children\n}) => {\n const config =\n easing === 'elastic'\n ? {\n ease: 'elastic.out(0.6,0.9)',\n durDrop: 2,\n durMove: 2,\n durReturn: 2,\n promoteOverlap: 0.9,\n returnDelay: 0.05\n }\n : {\n ease: 'power1.inOut',\n durDrop: 0.8,\n durMove: 0.8,\n durReturn: 0.8,\n promoteOverlap: 0.45,\n returnDelay: 0.2\n };\n\n const childArr = useMemo(() => Children.toArray(children), [children]);\n const refs = useMemo(\n () => childArr.map(() => React.createRef()),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [childArr.length]\n );\n\n const order = useRef(Array.from({ length: childArr.length }, (_, i) => i));\n\n const tlRef = useRef(null);\n const intervalRef = useRef();\n const container = useRef(null);\n\n useEffect(() => {\n const total = refs.length;\n refs.forEach((r, i) => placeNow(r.current, makeSlot(i, cardDistance, verticalDistance, total), skewAmount));\n\n const swap = () => {\n if (order.current.length < 2) return;\n\n const [front, ...rest] = order.current;\n const elFront = refs[front].current;\n const tl = gsap.timeline();\n tlRef.current = tl;\n\n tl.to(elFront, {\n y: '+=500',\n duration: config.durDrop,\n ease: config.ease\n });\n\n tl.addLabel('promote', `-=${config.durDrop * config.promoteOverlap}`);\n rest.forEach((idx, i) => {\n const el = refs[idx].current;\n const slot = makeSlot(i, cardDistance, verticalDistance, refs.length);\n tl.set(el, { zIndex: slot.zIndex }, 'promote');\n tl.to(\n el,\n {\n x: slot.x,\n y: slot.y,\n z: slot.z,\n duration: config.durMove,\n ease: config.ease\n },\n `promote+=${i * 0.15}`\n );\n });\n\n const backSlot = makeSlot(refs.length - 1, cardDistance, verticalDistance, refs.length);\n tl.addLabel('return', `promote+=${config.durMove * config.returnDelay}`);\n tl.call(\n () => {\n gsap.set(elFront, { zIndex: backSlot.zIndex });\n },\n undefined,\n 'return'\n );\n tl.to(\n elFront,\n {\n x: backSlot.x,\n y: backSlot.y,\n z: backSlot.z,\n duration: config.durReturn,\n ease: config.ease\n },\n 'return'\n );\n\n tl.call(() => {\n order.current = [...rest, front];\n });\n };\n\n swap();\n intervalRef.current = window.setInterval(swap, delay);\n\n if (pauseOnHover) {\n const node = container.current;\n const pause = () => {\n tlRef.current?.pause();\n clearInterval(intervalRef.current);\n };\n const resume = () => {\n tlRef.current?.play();\n intervalRef.current = window.setInterval(swap, delay);\n };\n node.addEventListener('mouseenter', pause);\n node.addEventListener('mouseleave', resume);\n return () => {\n node.removeEventListener('mouseenter', pause);\n node.removeEventListener('mouseleave', resume);\n clearInterval(intervalRef.current);\n };\n }\n return () => clearInterval(intervalRef.current);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [cardDistance, verticalDistance, delay, pauseOnHover, skewAmount, easing]);\n\n const rendered = childArr.map((child, i) =>\n isValidElement(child)\n ? cloneElement(child, {\n key: i,\n ref: refs[i],\n style: { width, height, ...(child.props.style ?? {}) },\n onClick: e => {\n child.props.onClick?.(e);\n onCardClick?.(i);\n }\n })\n : child\n );\n\n return (\n
\n {rendered}\n
\n );\n};\n\nexport default CardSwap;\n" } ], diff --git a/public/r/CardSwap-TS-CSS.json b/public/r/CardSwap-TS-CSS.json index 1045020b..f726fe2a 100644 --- a/public/r/CardSwap-TS-CSS.json +++ b/public/r/CardSwap-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "CardSwap/CardSwap.css", + "type": "registry:file", + "path": "CardSwap.css", + "target": "@components/CardSwap.css", "content": ".card-swap-container {\n position: absolute;\n bottom: 0;\n right: 0;\n transform: translate(5%, 20%);\n transform-origin: bottom right;\n\n perspective: 900px;\n overflow: visible;\n}\n\n.card {\n position: absolute;\n top: 50%;\n left: 50%;\n border-radius: 12px;\n border: 1px solid #fff;\n background: #000;\n\n transform-style: preserve-3d;\n will-change: transform;\n backface-visibility: hidden;\n -webkit-backface-visibility: hidden;\n}\n\n@media (max-width: 768px) {\n .card-swap-container {\n transform: scale(0.75) translate(25%, 25%);\n }\n}\n\n@media (max-width: 480px) {\n .card-swap-container {\n transform: scale(0.55) translate(25%, 25%);\n }\n}\n" }, { "type": "registry:component", - "path": "CardSwap/CardSwap.tsx", - "content": "import React, {\n Children,\n cloneElement,\n forwardRef,\n isValidElement,\n ReactElement,\n ReactNode,\n RefObject,\n useEffect,\n useMemo,\n useRef\n} from 'react';\nimport gsap from 'gsap';\nimport './CardSwap.css';\n\nexport interface CardSwapProps {\n width?: number | string;\n height?: number | string;\n cardDistance?: number;\n verticalDistance?: number;\n delay?: number;\n pauseOnHover?: boolean;\n onCardClick?: (idx: number) => void;\n skewAmount?: number;\n easing?: 'linear' | 'elastic';\n children: ReactNode;\n}\n\nexport interface CardProps extends React.HTMLAttributes {\n customClass?: string;\n}\n\nexport const Card = forwardRef(({ customClass, ...rest }, ref) => (\n
\n));\nCard.displayName = 'Card';\n\ntype CardRef = RefObject;\ninterface Slot {\n x: number;\n y: number;\n z: number;\n zIndex: number;\n}\n\nconst makeSlot = (i: number, distX: number, distY: number, total: number): Slot => ({\n x: i * distX,\n y: -i * distY,\n z: -i * distX * 1.5,\n zIndex: total - i\n});\n\nconst placeNow = (el: HTMLElement, slot: Slot, skew: number) =>\n gsap.set(el, {\n x: slot.x,\n y: slot.y,\n z: slot.z,\n xPercent: -50,\n yPercent: -50,\n skewY: skew,\n transformOrigin: 'center center',\n zIndex: slot.zIndex,\n force3D: true\n });\n\nconst CardSwap: React.FC = ({\n width = 500,\n height = 400,\n cardDistance = 60,\n verticalDistance = 70,\n delay = 5000,\n pauseOnHover = false,\n onCardClick,\n skewAmount = 6,\n easing = 'elastic',\n children\n}) => {\n const config =\n easing === 'elastic'\n ? {\n ease: 'elastic.out(0.6,0.9)',\n durDrop: 2,\n durMove: 2,\n durReturn: 2,\n promoteOverlap: 0.9,\n returnDelay: 0.05\n }\n : {\n ease: 'power1.inOut',\n durDrop: 0.8,\n durMove: 0.8,\n durReturn: 0.8,\n promoteOverlap: 0.45,\n returnDelay: 0.2\n };\n\n const childArr = useMemo(() => Children.toArray(children) as ReactElement[], [children]);\n const refs = useMemo(() => childArr.map(() => React.createRef()), [childArr.length]);\n\n const order = useRef(Array.from({ length: childArr.length }, (_, i) => i));\n\n const tlRef = useRef(null);\n const intervalRef = useRef(0);\n const container = useRef(null);\n\n useEffect(() => {\n const total = refs.length;\n refs.forEach((r, i) => placeNow(r.current!, makeSlot(i, cardDistance, verticalDistance, total), skewAmount));\n\n const swap = () => {\n if (order.current.length < 2) return;\n\n const [front, ...rest] = order.current;\n const elFront = refs[front].current!;\n const tl = gsap.timeline();\n tlRef.current = tl;\n\n tl.to(elFront, {\n y: '+=500',\n duration: config.durDrop,\n ease: config.ease\n });\n\n tl.addLabel('promote', `-=${config.durDrop * config.promoteOverlap}`);\n rest.forEach((idx, i) => {\n const el = refs[idx].current!;\n const slot = makeSlot(i, cardDistance, verticalDistance, refs.length);\n tl.set(el, { zIndex: slot.zIndex }, 'promote');\n tl.to(\n el,\n {\n x: slot.x,\n y: slot.y,\n z: slot.z,\n duration: config.durMove,\n ease: config.ease\n },\n `promote+=${i * 0.15}`\n );\n });\n\n const backSlot = makeSlot(refs.length - 1, cardDistance, verticalDistance, refs.length);\n tl.addLabel('return', `promote+=${config.durMove * config.returnDelay}`);\n tl.call(\n () => {\n gsap.set(elFront, { zIndex: backSlot.zIndex });\n },\n undefined,\n 'return'\n );\n tl.to(\n elFront,\n {\n x: backSlot.x,\n y: backSlot.y,\n z: backSlot.z,\n duration: config.durReturn,\n ease: config.ease\n },\n 'return'\n );\n\n tl.call(() => {\n order.current = [...rest, front];\n });\n };\n\n swap();\n intervalRef.current = window.setInterval(swap, delay);\n\n if (pauseOnHover) {\n const node = container.current!;\n const pause = () => {\n tlRef.current?.pause();\n clearInterval(intervalRef.current);\n };\n const resume = () => {\n tlRef.current?.play();\n intervalRef.current = window.setInterval(swap, delay);\n };\n node.addEventListener('mouseenter', pause);\n node.addEventListener('mouseleave', resume);\n return () => {\n node.removeEventListener('mouseenter', pause);\n node.removeEventListener('mouseleave', resume);\n clearInterval(intervalRef.current);\n };\n }\n return () => clearInterval(intervalRef.current);\n }, [cardDistance, verticalDistance, delay, pauseOnHover, skewAmount, easing]);\n\n const rendered = childArr.map((child, i) =>\n isValidElement(child)\n ? cloneElement(child, {\n key: i,\n ref: refs[i],\n style: { width, height, ...(child.props.style ?? {}) },\n onClick: e => {\n child.props.onClick?.(e as React.MouseEvent);\n onCardClick?.(i);\n }\n } as CardProps & React.RefAttributes)\n : child\n );\n\n return (\n
\n {rendered}\n
\n );\n};\n\nexport default CardSwap;\n" + "path": "CardSwap.tsx", + "content": "import React, {\n Children,\n cloneElement,\n forwardRef,\n isValidElement,\n type ReactElement,\n type ReactNode,\n type RefObject,\n useEffect,\n useMemo,\n useRef\n} from 'react';\nimport gsap from 'gsap';\nimport './CardSwap.css';\n\nexport interface CardSwapProps {\n width?: number | string;\n height?: number | string;\n cardDistance?: number;\n verticalDistance?: number;\n delay?: number;\n pauseOnHover?: boolean;\n onCardClick?: (idx: number) => void;\n skewAmount?: number;\n easing?: 'linear' | 'elastic';\n children: ReactNode;\n}\n\nexport interface CardProps extends React.HTMLAttributes {\n customClass?: string;\n}\n\nexport const Card = forwardRef(({ customClass, ...rest }, ref) => (\n
\n));\nCard.displayName = 'Card';\n\ntype CardRef = RefObject;\ninterface Slot {\n x: number;\n y: number;\n z: number;\n zIndex: number;\n}\n\nconst makeSlot = (i: number, distX: number, distY: number, total: number): Slot => ({\n x: i * distX,\n y: -i * distY,\n z: -i * distX * 1.5,\n zIndex: total - i\n});\n\nconst placeNow = (el: HTMLElement, slot: Slot, skew: number) =>\n gsap.set(el, {\n x: slot.x,\n y: slot.y,\n z: slot.z,\n xPercent: -50,\n yPercent: -50,\n skewY: skew,\n transformOrigin: 'center center',\n zIndex: slot.zIndex,\n force3D: true\n });\n\nconst CardSwap: React.FC = ({\n width = 500,\n height = 400,\n cardDistance = 60,\n verticalDistance = 70,\n delay = 5000,\n pauseOnHover = false,\n onCardClick,\n skewAmount = 6,\n easing = 'elastic',\n children\n}) => {\n const config =\n easing === 'elastic'\n ? {\n ease: 'elastic.out(0.6,0.9)',\n durDrop: 2,\n durMove: 2,\n durReturn: 2,\n promoteOverlap: 0.9,\n returnDelay: 0.05\n }\n : {\n ease: 'power1.inOut',\n durDrop: 0.8,\n durMove: 0.8,\n durReturn: 0.8,\n promoteOverlap: 0.45,\n returnDelay: 0.2\n };\n\n const childArr = useMemo(() => Children.toArray(children) as ReactElement[], [children]);\n const refs = useMemo(() => childArr.map(() => React.createRef()), [childArr.length]);\n\n const order = useRef(Array.from({ length: childArr.length }, (_, i) => i));\n\n const tlRef = useRef(null);\n const intervalRef = useRef(0);\n const container = useRef(null);\n\n useEffect(() => {\n const total = refs.length;\n refs.forEach((r, i) => placeNow(r.current!, makeSlot(i, cardDistance, verticalDistance, total), skewAmount));\n\n const swap = () => {\n if (order.current.length < 2) return;\n\n const [front, ...rest] = order.current;\n const elFront = refs[front].current!;\n const tl = gsap.timeline();\n tlRef.current = tl;\n\n tl.to(elFront, {\n y: '+=500',\n duration: config.durDrop,\n ease: config.ease\n });\n\n tl.addLabel('promote', `-=${config.durDrop * config.promoteOverlap}`);\n rest.forEach((idx, i) => {\n const el = refs[idx].current!;\n const slot = makeSlot(i, cardDistance, verticalDistance, refs.length);\n tl.set(el, { zIndex: slot.zIndex }, 'promote');\n tl.to(\n el,\n {\n x: slot.x,\n y: slot.y,\n z: slot.z,\n duration: config.durMove,\n ease: config.ease\n },\n `promote+=${i * 0.15}`\n );\n });\n\n const backSlot = makeSlot(refs.length - 1, cardDistance, verticalDistance, refs.length);\n tl.addLabel('return', `promote+=${config.durMove * config.returnDelay}`);\n tl.call(\n () => {\n gsap.set(elFront, { zIndex: backSlot.zIndex });\n },\n undefined,\n 'return'\n );\n tl.to(\n elFront,\n {\n x: backSlot.x,\n y: backSlot.y,\n z: backSlot.z,\n duration: config.durReturn,\n ease: config.ease\n },\n 'return'\n );\n\n tl.call(() => {\n order.current = [...rest, front];\n });\n };\n\n swap();\n intervalRef.current = window.setInterval(swap, delay);\n\n if (pauseOnHover) {\n const node = container.current!;\n const pause = () => {\n tlRef.current?.pause();\n clearInterval(intervalRef.current);\n };\n const resume = () => {\n tlRef.current?.play();\n intervalRef.current = window.setInterval(swap, delay);\n };\n node.addEventListener('mouseenter', pause);\n node.addEventListener('mouseleave', resume);\n return () => {\n node.removeEventListener('mouseenter', pause);\n node.removeEventListener('mouseleave', resume);\n clearInterval(intervalRef.current);\n };\n }\n return () => clearInterval(intervalRef.current);\n }, [cardDistance, verticalDistance, delay, pauseOnHover, skewAmount, easing]);\n\n const rendered = childArr.map((child, i) =>\n isValidElement(child)\n ? cloneElement(child, {\n key: i,\n ref: refs[i],\n style: { width, height, ...(child.props.style ?? {}) },\n onClick: e => {\n child.props.onClick?.(e as React.MouseEvent);\n onCardClick?.(i);\n }\n } as CardProps & React.RefAttributes)\n : child\n );\n\n return (\n
\n {rendered}\n
\n );\n};\n\nexport default CardSwap;\n" } ], "registryDependencies": [], diff --git a/public/r/CardSwap-TS-TW.json b/public/r/CardSwap-TS-TW.json index ba085e11..bb388591 100644 --- a/public/r/CardSwap-TS-TW.json +++ b/public/r/CardSwap-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "CardSwap/CardSwap.tsx", - "content": "import React, {\n Children,\n cloneElement,\n forwardRef,\n isValidElement,\n ReactElement,\n ReactNode,\n RefObject,\n useEffect,\n useMemo,\n useRef\n} from 'react';\nimport gsap from 'gsap';\n\nexport interface CardSwapProps {\n width?: number | string;\n height?: number | string;\n cardDistance?: number;\n verticalDistance?: number;\n delay?: number;\n pauseOnHover?: boolean;\n onCardClick?: (idx: number) => void;\n skewAmount?: number;\n easing?: 'linear' | 'elastic';\n children: ReactNode;\n}\n\nexport interface CardProps extends React.HTMLAttributes {\n customClass?: string;\n}\n\nexport const Card = forwardRef(({ customClass, ...rest }, ref) => (\n \n));\nCard.displayName = 'Card';\n\ntype CardRef = RefObject;\ninterface Slot {\n x: number;\n y: number;\n z: number;\n zIndex: number;\n}\n\nconst makeSlot = (i: number, distX: number, distY: number, total: number): Slot => ({\n x: i * distX,\n y: -i * distY,\n z: -i * distX * 1.5,\n zIndex: total - i\n});\n\nconst placeNow = (el: HTMLElement, slot: Slot, skew: number) =>\n gsap.set(el, {\n x: slot.x,\n y: slot.y,\n z: slot.z,\n xPercent: -50,\n yPercent: -50,\n skewY: skew,\n transformOrigin: 'center center',\n zIndex: slot.zIndex,\n force3D: true\n });\n\nconst CardSwap: React.FC = ({\n width = 500,\n height = 400,\n cardDistance = 60,\n verticalDistance = 70,\n delay = 5000,\n pauseOnHover = false,\n onCardClick,\n skewAmount = 6,\n easing = 'elastic',\n children\n}) => {\n const config =\n easing === 'elastic'\n ? {\n ease: 'elastic.out(0.6,0.9)',\n durDrop: 2,\n durMove: 2,\n durReturn: 2,\n promoteOverlap: 0.9,\n returnDelay: 0.05\n }\n : {\n ease: 'power1.inOut',\n durDrop: 0.8,\n durMove: 0.8,\n durReturn: 0.8,\n promoteOverlap: 0.45,\n returnDelay: 0.2\n };\n\n const childArr = useMemo(() => Children.toArray(children) as ReactElement[], [children]);\n const refs = useMemo(() => childArr.map(() => React.createRef()), [childArr.length]);\n\n const order = useRef(Array.from({ length: childArr.length }, (_, i) => i));\n\n const tlRef = useRef(null);\n const intervalRef = useRef(0);\n const container = useRef(null);\n\n useEffect(() => {\n const total = refs.length;\n refs.forEach((r, i) => placeNow(r.current!, makeSlot(i, cardDistance, verticalDistance, total), skewAmount));\n\n const swap = () => {\n if (order.current.length < 2) return;\n\n const [front, ...rest] = order.current;\n const elFront = refs[front].current!;\n const tl = gsap.timeline();\n tlRef.current = tl;\n\n tl.to(elFront, {\n y: '+=500',\n duration: config.durDrop,\n ease: config.ease\n });\n\n tl.addLabel('promote', `-=${config.durDrop * config.promoteOverlap}`);\n rest.forEach((idx, i) => {\n const el = refs[idx].current!;\n const slot = makeSlot(i, cardDistance, verticalDistance, refs.length);\n tl.set(el, { zIndex: slot.zIndex }, 'promote');\n tl.to(\n el,\n {\n x: slot.x,\n y: slot.y,\n z: slot.z,\n duration: config.durMove,\n ease: config.ease\n },\n `promote+=${i * 0.15}`\n );\n });\n\n const backSlot = makeSlot(refs.length - 1, cardDistance, verticalDistance, refs.length);\n tl.addLabel('return', `promote+=${config.durMove * config.returnDelay}`);\n tl.call(\n () => {\n gsap.set(elFront, { zIndex: backSlot.zIndex });\n },\n undefined,\n 'return'\n );\n tl.to(\n elFront,\n {\n x: backSlot.x,\n y: backSlot.y,\n z: backSlot.z,\n duration: config.durReturn,\n ease: config.ease\n },\n 'return'\n );\n\n tl.call(() => {\n order.current = [...rest, front];\n });\n };\n\n swap();\n intervalRef.current = window.setInterval(swap, delay);\n\n if (pauseOnHover) {\n const node = container.current!;\n const pause = () => {\n tlRef.current?.pause();\n clearInterval(intervalRef.current);\n };\n const resume = () => {\n tlRef.current?.play();\n intervalRef.current = window.setInterval(swap, delay);\n };\n node.addEventListener('mouseenter', pause);\n node.addEventListener('mouseleave', resume);\n return () => {\n node.removeEventListener('mouseenter', pause);\n node.removeEventListener('mouseleave', resume);\n clearInterval(intervalRef.current);\n };\n }\n return () => clearInterval(intervalRef.current);\n }, [cardDistance, verticalDistance, delay, pauseOnHover, skewAmount, easing]);\n\n const rendered = childArr.map((child, i) =>\n isValidElement(child)\n ? cloneElement(child, {\n key: i,\n ref: refs[i],\n style: { width, height, ...(child.props.style ?? {}) },\n onClick: e => {\n child.props.onClick?.(e as React.MouseEvent);\n onCardClick?.(i);\n }\n } as CardProps & React.RefAttributes)\n : child\n );\n\n return (\n \n {rendered}\n
\n );\n};\n\nexport default CardSwap;\n" + "content": "import React, {\n Children,\n cloneElement,\n forwardRef,\n isValidElement,\n type ReactElement,\n type ReactNode,\n type RefObject,\n useEffect,\n useMemo,\n useRef\n} from 'react';\nimport gsap from 'gsap';\n\nexport interface CardSwapProps {\n width?: number | string;\n height?: number | string;\n cardDistance?: number;\n verticalDistance?: number;\n delay?: number;\n pauseOnHover?: boolean;\n onCardClick?: (idx: number) => void;\n skewAmount?: number;\n easing?: 'linear' | 'elastic';\n children: ReactNode;\n}\n\nexport interface CardProps extends React.HTMLAttributes {\n customClass?: string;\n}\n\nexport const Card = forwardRef(({ customClass, ...rest }, ref) => (\n \n));\nCard.displayName = 'Card';\n\ntype CardRef = RefObject;\ninterface Slot {\n x: number;\n y: number;\n z: number;\n zIndex: number;\n}\n\nconst makeSlot = (i: number, distX: number, distY: number, total: number): Slot => ({\n x: i * distX,\n y: -i * distY,\n z: -i * distX * 1.5,\n zIndex: total - i\n});\n\nconst placeNow = (el: HTMLElement, slot: Slot, skew: number) =>\n gsap.set(el, {\n x: slot.x,\n y: slot.y,\n z: slot.z,\n xPercent: -50,\n yPercent: -50,\n skewY: skew,\n transformOrigin: 'center center',\n zIndex: slot.zIndex,\n force3D: true\n });\n\nconst CardSwap: React.FC = ({\n width = 500,\n height = 400,\n cardDistance = 60,\n verticalDistance = 70,\n delay = 5000,\n pauseOnHover = false,\n onCardClick,\n skewAmount = 6,\n easing = 'elastic',\n children\n}) => {\n const config =\n easing === 'elastic'\n ? {\n ease: 'elastic.out(0.6,0.9)',\n durDrop: 2,\n durMove: 2,\n durReturn: 2,\n promoteOverlap: 0.9,\n returnDelay: 0.05\n }\n : {\n ease: 'power1.inOut',\n durDrop: 0.8,\n durMove: 0.8,\n durReturn: 0.8,\n promoteOverlap: 0.45,\n returnDelay: 0.2\n };\n\n const childArr = useMemo(() => Children.toArray(children) as ReactElement[], [children]);\n const refs = useMemo(() => childArr.map(() => React.createRef()), [childArr.length]);\n\n const order = useRef(Array.from({ length: childArr.length }, (_, i) => i));\n\n const tlRef = useRef(null);\n const intervalRef = useRef(0);\n const container = useRef(null);\n\n useEffect(() => {\n const total = refs.length;\n refs.forEach((r, i) => placeNow(r.current!, makeSlot(i, cardDistance, verticalDistance, total), skewAmount));\n\n const swap = () => {\n if (order.current.length < 2) return;\n\n const [front, ...rest] = order.current;\n const elFront = refs[front].current!;\n const tl = gsap.timeline();\n tlRef.current = tl;\n\n tl.to(elFront, {\n y: '+=500',\n duration: config.durDrop,\n ease: config.ease\n });\n\n tl.addLabel('promote', `-=${config.durDrop * config.promoteOverlap}`);\n rest.forEach((idx, i) => {\n const el = refs[idx].current!;\n const slot = makeSlot(i, cardDistance, verticalDistance, refs.length);\n tl.set(el, { zIndex: slot.zIndex }, 'promote');\n tl.to(\n el,\n {\n x: slot.x,\n y: slot.y,\n z: slot.z,\n duration: config.durMove,\n ease: config.ease\n },\n `promote+=${i * 0.15}`\n );\n });\n\n const backSlot = makeSlot(refs.length - 1, cardDistance, verticalDistance, refs.length);\n tl.addLabel('return', `promote+=${config.durMove * config.returnDelay}`);\n tl.call(\n () => {\n gsap.set(elFront, { zIndex: backSlot.zIndex });\n },\n undefined,\n 'return'\n );\n tl.to(\n elFront,\n {\n x: backSlot.x,\n y: backSlot.y,\n z: backSlot.z,\n duration: config.durReturn,\n ease: config.ease\n },\n 'return'\n );\n\n tl.call(() => {\n order.current = [...rest, front];\n });\n };\n\n swap();\n intervalRef.current = window.setInterval(swap, delay);\n\n if (pauseOnHover) {\n const node = container.current!;\n const pause = () => {\n tlRef.current?.pause();\n clearInterval(intervalRef.current);\n };\n const resume = () => {\n tlRef.current?.play();\n intervalRef.current = window.setInterval(swap, delay);\n };\n node.addEventListener('mouseenter', pause);\n node.addEventListener('mouseleave', resume);\n return () => {\n node.removeEventListener('mouseenter', pause);\n node.removeEventListener('mouseleave', resume);\n clearInterval(intervalRef.current);\n };\n }\n return () => clearInterval(intervalRef.current);\n }, [cardDistance, verticalDistance, delay, pauseOnHover, skewAmount, easing]);\n\n const rendered = childArr.map((child, i) =>\n isValidElement(child)\n ? cloneElement(child, {\n key: i,\n ref: refs[i],\n style: { width, height, ...(child.props.style ?? {}) },\n onClick: e => {\n child.props.onClick?.(e as React.MouseEvent);\n onCardClick?.(i);\n }\n } as CardProps & React.RefAttributes)\n : child\n );\n\n return (\n \n {rendered}\n
\n );\n};\n\nexport default CardSwap;\n" } ], "registryDependencies": [], diff --git a/public/r/Carousel-JS-CSS.json b/public/r/Carousel-JS-CSS.json index 42170fb7..ef51d34e 100644 --- a/public/r/Carousel-JS-CSS.json +++ b/public/r/Carousel-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Carousel/Carousel.css", + "type": "registry:file", + "path": "Carousel.css", + "target": "@components/Carousel.css", "content": ".carousel-container {\n position: relative;\n overflow: hidden;\n border: 1px solid #555;\n border-radius: 24px;\n padding: 16px;\n --outer-r: 24px;\n --p-distance: 12px;\n}\n\n.carousel-track {\n display: flex;\n}\n\n.carousel-item {\n position: relative;\n display: flex;\n flex-shrink: 0;\n flex-direction: column;\n align-items: flex-start;\n justify-content: space-between;\n border: 1px solid #555;\n border-radius: calc(var(--outer-r) - var(--p-distance));\n background-color: #1B1722;\n overflow: hidden;\n cursor: grab;\n}\n\n.carousel-item:active {\n cursor: grabbing;\n}\n\n.carousel-container.round {\n border: 1px solid #555;\n}\n\n.carousel-item.round {\n background-color: #1B1722;\n position: relative;\n bottom: 0.1em;\n border: 1px solid #555;\n justify-content: center;\n align-items: center;\n text-align: center;\n}\n\n.carousel-item-header.round {\n padding: 0;\n margin: 0;\n}\n\n.carousel-indicators-container.round {\n position: absolute;\n z-index: 2;\n bottom: 3em;\n left: 50%;\n transform: translateX(-50%);\n}\n\n.carousel-indicator.active {\n background-color: #333333;\n}\n\n.carousel-indicator.inactive {\n background-color: rgba(51, 51, 51, 0.4);\n}\n\n.carousel-item-header {\n margin-bottom: 16px;\n padding: 20px;\n padding-top: 20px;\n}\n\n.carousel-icon-container {\n display: flex;\n height: 28px;\n width: 28px;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n background-color: #fff;\n}\n\n.carousel-icon {\n height: 16px;\n width: 16px;\n color: #120F17;\n}\n\n.carousel-item-content {\n padding: 20px;\n padding-bottom: 20px;\n}\n\n.carousel-item-title {\n margin-bottom: 4px;\n font-weight: 900;\n font-size: 18px;\n color: #fff;\n}\n\n.carousel-item-description {\n font-size: 14px;\n color: #fff;\n}\n\n.carousel-indicators-container {\n display: flex;\n width: 100%;\n justify-content: center;\n}\n\n.carousel-indicators {\n margin-top: 16px;\n display: flex;\n width: 150px;\n justify-content: space-between;\n padding: 0 32px;\n}\n\n.carousel-indicator {\n height: 8px;\n width: 8px;\n border: none;\n padding: 0;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n transition: background-color 150ms;\n}\n\n.carousel-indicator:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n.carousel-indicator.active {\n background-color: #fff;\n}\n\n.carousel-indicator.inactive {\n background-color: #555;\n}\n" }, { "type": "registry:component", - "path": "Carousel/Carousel.jsx", + "path": "Carousel.jsx", "content": "import { useEffect, useMemo, useRef, useState } from 'react';\nimport { motion, useMotionValue, useTransform } from 'motion/react';\n// replace icons with your own if needed\nimport { FiCircle, FiCode, FiFileText, FiLayers, FiLayout } from 'react-icons/fi';\n\nimport './Carousel.css';\n\nconst DEFAULT_ITEMS = [\n {\n title: 'Text Animations',\n description: 'Cool text animations for your projects.',\n id: 1,\n icon: \n },\n {\n title: 'Animations',\n description: 'Smooth animations for your projects.',\n id: 2,\n icon: \n },\n {\n title: 'Components',\n description: 'Reusable components for your projects.',\n id: 3,\n icon: \n },\n {\n title: 'Backgrounds',\n description: 'Beautiful backgrounds and patterns for your projects.',\n id: 4,\n icon: \n },\n {\n title: 'Common UI',\n description: 'Common UI components are coming soon!',\n id: 5,\n icon: \n }\n];\n\nconst DRAG_BUFFER = 0;\nconst VELOCITY_THRESHOLD = 500;\nconst GAP = 16;\nconst SPRING_OPTIONS = { type: 'spring', stiffness: 300, damping: 30 };\n\nfunction CarouselItem({ item, index, itemWidth, round, trackItemOffset, x, transition }) {\n const range = [-(index + 1) * trackItemOffset, -index * trackItemOffset, -(index - 1) * trackItemOffset];\n const outputRange = [90, 0, -90];\n const rotateY = useTransform(x, range, outputRange, { clamp: false });\n\n return (\n \n
\n {item.icon}\n
\n
\n
{item.title}
\n

{item.description}

\n
\n \n );\n}\n\nexport default function Carousel({\n items = DEFAULT_ITEMS,\n baseWidth = 300,\n autoplay = false,\n autoplayDelay = 3000,\n pauseOnHover = false,\n loop = false,\n round = false\n}) {\n const containerPadding = 16;\n const itemWidth = baseWidth - containerPadding * 2;\n const trackItemOffset = itemWidth + GAP;\n const itemsForRender = useMemo(() => {\n if (!loop) return items;\n if (items.length === 0) return [];\n return [items[items.length - 1], ...items, items[0]];\n }, [items, loop]);\n\n const [position, setPosition] = useState(loop ? 1 : 0);\n const x = useMotionValue(0);\n const [isHovered, setIsHovered] = useState(false);\n const [isJumping, setIsJumping] = useState(false);\n const [isAnimating, setIsAnimating] = useState(false);\n\n const containerRef = useRef(null);\n useEffect(() => {\n if (pauseOnHover && containerRef.current) {\n const container = containerRef.current;\n const handleMouseEnter = () => setIsHovered(true);\n const handleMouseLeave = () => setIsHovered(false);\n container.addEventListener('mouseenter', handleMouseEnter);\n container.addEventListener('mouseleave', handleMouseLeave);\n return () => {\n container.removeEventListener('mouseenter', handleMouseEnter);\n container.removeEventListener('mouseleave', handleMouseLeave);\n };\n }\n }, [pauseOnHover]);\n\n useEffect(() => {\n if (!autoplay || itemsForRender.length <= 1) return undefined;\n if (pauseOnHover && isHovered) return undefined;\n\n const timer = setInterval(() => {\n setPosition(prev => Math.min(prev + 1, itemsForRender.length - 1));\n }, autoplayDelay);\n\n return () => clearInterval(timer);\n }, [autoplay, autoplayDelay, isHovered, pauseOnHover, itemsForRender.length]);\n\n useEffect(() => {\n const startingPosition = loop ? 1 : 0;\n setPosition(startingPosition);\n x.set(-startingPosition * trackItemOffset);\n }, [items.length, loop, trackItemOffset, x]);\n\n useEffect(() => {\n if (!loop && position > itemsForRender.length - 1) {\n setPosition(Math.max(0, itemsForRender.length - 1));\n }\n }, [itemsForRender.length, loop, position]);\n\n const effectiveTransition = isJumping ? { duration: 0 } : SPRING_OPTIONS;\n\n const handleAnimationStart = () => {\n setIsAnimating(true);\n };\n\n const handleAnimationComplete = () => {\n if (!loop || itemsForRender.length <= 1) {\n setIsAnimating(false);\n return;\n }\n const lastCloneIndex = itemsForRender.length - 1;\n\n if (position === lastCloneIndex) {\n setIsJumping(true);\n const target = 1;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n if (position === 0) {\n setIsJumping(true);\n const target = items.length;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n setIsAnimating(false);\n };\n\n const handleDragEnd = (_, info) => {\n const { offset, velocity } = info;\n const direction =\n offset.x < -DRAG_BUFFER || velocity.x < -VELOCITY_THRESHOLD\n ? 1\n : offset.x > DRAG_BUFFER || velocity.x > VELOCITY_THRESHOLD\n ? -1\n : 0;\n\n if (direction === 0) return;\n\n setPosition(prev => {\n const next = prev + direction;\n const max = itemsForRender.length - 1;\n return Math.max(0, Math.min(next, max));\n });\n };\n\n const dragProps = loop\n ? {}\n : {\n dragConstraints: {\n left: -trackItemOffset * Math.max(itemsForRender.length - 1, 0),\n right: 0\n }\n };\n\n const activeIndex =\n items.length === 0 ? 0 : loop ? (position - 1 + items.length) % items.length : Math.min(position, items.length - 1);\n\n return (\n \n \n {itemsForRender.map((item, index) => (\n \n ))}\n \n
\n
\n {items.map((_, index) => (\n setPosition(loop ? index + 1 : index)}\n transition={{ duration: 0.15 }}\n />\n ))}\n
\n
\n
\n );\n}\n" } ], diff --git a/public/r/Carousel-TS-CSS.json b/public/r/Carousel-TS-CSS.json index ccaefe56..db0ca0c0 100644 --- a/public/r/Carousel-TS-CSS.json +++ b/public/r/Carousel-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Carousel/Carousel.css", + "type": "registry:file", + "path": "Carousel.css", + "target": "@components/Carousel.css", "content": ".carousel-container {\n position: relative;\n overflow: hidden;\n border: 1px solid #555;\n border-radius: 24px;\n padding: 16px;\n --outer-r: 24px;\n --p-distance: 12px;\n}\n\n.carousel-track {\n display: flex;\n}\n\n.carousel-item {\n position: relative;\n display: flex;\n flex-shrink: 0;\n flex-direction: column;\n align-items: flex-start;\n justify-content: space-between;\n border: 1px solid #555;\n border-radius: calc(var(--outer-r) - var(--p-distance));\n background-color: #0d0d0d;\n overflow: hidden;\n cursor: grab;\n}\n\n.carousel-item:active {\n cursor: grabbing;\n}\n\n.carousel-container.round {\n border: 1px solid #555;\n}\n\n.carousel-item.round {\n background-color: #0d0d0d;\n position: relative;\n bottom: 0.1em;\n border: 1px solid #555;\n justify-content: center;\n align-items: center;\n text-align: center;\n}\n\n.carousel-item-header.round {\n padding: 0;\n margin: 0;\n}\n\n.carousel-indicators-container.round {\n position: absolute;\n z-index: 2;\n bottom: 3em;\n left: 50%;\n transform: translateX(-50%);\n}\n\n.carousel-indicator.active {\n background-color: #333333;\n}\n\n.carousel-indicator.inactive {\n background-color: rgba(51, 51, 51, 0.4);\n}\n\n.carousel-item-header {\n margin-bottom: 16px;\n padding: 20px;\n padding-top: 20px;\n}\n\n.carousel-icon-container {\n display: flex;\n height: 28px;\n width: 28px;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n background-color: #fff;\n}\n\n.carousel-icon {\n height: 16px;\n width: 16px;\n color: #120F17;\n}\n\n.carousel-item-content {\n padding: 20px;\n padding-bottom: 20px;\n}\n\n.carousel-item-title {\n margin-bottom: 4px;\n font-weight: 900;\n font-size: 18px;\n color: #fff;\n}\n\n.carousel-item-description {\n font-size: 14px;\n color: #fff;\n}\n\n.carousel-indicators-container {\n display: flex;\n width: 100%;\n justify-content: center;\n}\n\n.carousel-indicators {\n margin-top: 16px;\n display: flex;\n width: 150px;\n justify-content: space-between;\n padding: 0 32px;\n}\n\n.carousel-indicator {\n height: 8px;\n width: 8px;\n border: none;\n padding: 0;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n transition: background-color 150ms;\n}\n\n.carousel-indicator:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n.carousel-indicator.active {\n background-color: #fff;\n}\n\n.carousel-indicator.inactive {\n background-color: #555;\n}\n" }, { "type": "registry:component", - "path": "Carousel/Carousel.tsx", - "content": "import { useEffect, useMemo, useRef, useState } from 'react';\nimport { motion, PanInfo, useMotionValue, useTransform } from 'motion/react';\n// replace icons with your own if needed\nimport { FiCircle, FiCode, FiFileText, FiLayers, FiLayout } from 'react-icons/fi';\nimport './Carousel.css';\n\nexport interface CarouselItem {\n title: string;\n description: string;\n id: number;\n icon: React.ReactElement;\n}\n\nexport interface CarouselProps {\n items?: CarouselItem[];\n baseWidth?: number;\n autoplay?: boolean;\n autoplayDelay?: number;\n pauseOnHover?: boolean;\n loop?: boolean;\n round?: boolean;\n}\n\nconst DEFAULT_ITEMS: CarouselItem[] = [\n {\n title: 'Text Animations',\n description: 'Cool text animations for your projects.',\n id: 1,\n icon: \n },\n {\n title: 'Animations',\n description: 'Smooth animations for your projects.',\n id: 2,\n icon: \n },\n {\n title: 'Components',\n description: 'Reusable components for your projects.',\n id: 3,\n icon: \n },\n {\n title: 'Backgrounds',\n description: 'Beautiful backgrounds and patterns for your projects.',\n id: 4,\n icon: \n },\n {\n title: 'Common UI',\n description: 'Common UI components are coming soon!',\n id: 5,\n icon: \n }\n];\n\nconst DRAG_BUFFER = 0;\nconst VELOCITY_THRESHOLD = 500;\nconst GAP = 16;\nconst SPRING_OPTIONS = { type: 'spring' as const, stiffness: 300, damping: 30 };\n\ninterface CarouselItemProps {\n item: CarouselItem;\n index: number;\n itemWidth: number;\n round: boolean;\n trackItemOffset: number;\n x: any;\n transition: any;\n}\n\nfunction CarouselItem({ item, index, itemWidth, round, trackItemOffset, x, transition }: CarouselItemProps) {\n const range = [-(index + 1) * trackItemOffset, -index * trackItemOffset, -(index - 1) * trackItemOffset];\n const outputRange = [90, 0, -90];\n const rotateY = useTransform(x, range, outputRange, { clamp: false });\n\n return (\n \n
\n {item.icon}\n
\n
\n
{item.title}
\n

{item.description}

\n
\n \n );\n}\n\nexport default function Carousel({\n items = DEFAULT_ITEMS,\n baseWidth = 300,\n autoplay = false,\n autoplayDelay = 3000,\n pauseOnHover = false,\n loop = false,\n round = false\n}: CarouselProps): React.JSX.Element {\n const containerPadding = 16;\n const itemWidth = baseWidth - containerPadding * 2;\n const trackItemOffset = itemWidth + GAP;\n const itemsForRender = useMemo(() => {\n if (!loop) return items;\n if (items.length === 0) return [];\n return [items[items.length - 1], ...items, items[0]];\n }, [items, loop]);\n\n const [position, setPosition] = useState(loop ? 1 : 0);\n const x = useMotionValue(0);\n const [isHovered, setIsHovered] = useState(false);\n const [isJumping, setIsJumping] = useState(false);\n const [isAnimating, setIsAnimating] = useState(false);\n\n const containerRef = useRef(null);\n useEffect(() => {\n if (pauseOnHover && containerRef.current) {\n const container = containerRef.current;\n const handleMouseEnter = () => setIsHovered(true);\n const handleMouseLeave = () => setIsHovered(false);\n container.addEventListener('mouseenter', handleMouseEnter);\n container.addEventListener('mouseleave', handleMouseLeave);\n return () => {\n container.removeEventListener('mouseenter', handleMouseEnter);\n container.removeEventListener('mouseleave', handleMouseLeave);\n };\n }\n }, [pauseOnHover]);\n\n useEffect(() => {\n if (!autoplay || itemsForRender.length <= 1) return undefined;\n if (pauseOnHover && isHovered) return undefined;\n\n const timer = setInterval(() => {\n setPosition(prev => Math.min(prev + 1, itemsForRender.length - 1));\n }, autoplayDelay);\n\n return () => clearInterval(timer);\n }, [autoplay, autoplayDelay, isHovered, pauseOnHover, itemsForRender.length]);\n\n useEffect(() => {\n const startingPosition = loop ? 1 : 0;\n setPosition(startingPosition);\n x.set(-startingPosition * trackItemOffset);\n }, [items.length, loop, trackItemOffset, x]);\n\n useEffect(() => {\n if (!loop && position > itemsForRender.length - 1) {\n setPosition(Math.max(0, itemsForRender.length - 1));\n }\n }, [itemsForRender.length, loop, position]);\n\n const effectiveTransition = isJumping ? { duration: 0 } : SPRING_OPTIONS;\n\n const handleAnimationStart = () => {\n setIsAnimating(true);\n };\n\n const handleAnimationComplete = () => {\n if (!loop || itemsForRender.length <= 1) {\n setIsAnimating(false);\n return;\n }\n const lastCloneIndex = itemsForRender.length - 1;\n\n if (position === lastCloneIndex) {\n setIsJumping(true);\n const target = 1;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n if (position === 0) {\n setIsJumping(true);\n const target = items.length;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n setIsAnimating(false);\n };\n\n const handleDragEnd = (_: MouseEvent | TouchEvent | PointerEvent, info: PanInfo): void => {\n const { offset, velocity } = info;\n const direction =\n offset.x < -DRAG_BUFFER || velocity.x < -VELOCITY_THRESHOLD\n ? 1\n : offset.x > DRAG_BUFFER || velocity.x > VELOCITY_THRESHOLD\n ? -1\n : 0;\n\n if (direction === 0) return;\n\n setPosition(prev => {\n const next = prev + direction;\n const max = itemsForRender.length - 1;\n return Math.max(0, Math.min(next, max));\n });\n };\n\n const dragProps = loop\n ? {}\n : {\n dragConstraints: {\n left: -trackItemOffset * Math.max(itemsForRender.length - 1, 0),\n right: 0\n }\n };\n\n const activeIndex =\n items.length === 0 ? 0 : loop ? (position - 1 + items.length) % items.length : Math.min(position, items.length - 1);\n\n return (\n \n \n {itemsForRender.map((item, index) => (\n \n ))}\n \n
\n
\n {items.map((_, index) => (\n setPosition(loop ? index + 1 : index)}\n transition={{ duration: 0.15 }}\n />\n ))}\n
\n
\n \n );\n}\n" + "path": "Carousel.tsx", + "content": "import { useEffect, useMemo, useRef, useState } from 'react';\nimport { motion, type PanInfo, useMotionValue, useTransform } from 'motion/react';\n// replace icons with your own if needed\nimport { FiCircle, FiCode, FiFileText, FiLayers, FiLayout } from 'react-icons/fi';\nimport './Carousel.css';\n\nexport interface CarouselItem {\n title: string;\n description: string;\n id: number;\n icon: React.ReactElement;\n}\n\nexport interface CarouselProps {\n items?: CarouselItem[];\n baseWidth?: number;\n autoplay?: boolean;\n autoplayDelay?: number;\n pauseOnHover?: boolean;\n loop?: boolean;\n round?: boolean;\n}\n\nconst DEFAULT_ITEMS: CarouselItem[] = [\n {\n title: 'Text Animations',\n description: 'Cool text animations for your projects.',\n id: 1,\n icon: \n },\n {\n title: 'Animations',\n description: 'Smooth animations for your projects.',\n id: 2,\n icon: \n },\n {\n title: 'Components',\n description: 'Reusable components for your projects.',\n id: 3,\n icon: \n },\n {\n title: 'Backgrounds',\n description: 'Beautiful backgrounds and patterns for your projects.',\n id: 4,\n icon: \n },\n {\n title: 'Common UI',\n description: 'Common UI components are coming soon!',\n id: 5,\n icon: \n }\n];\n\nconst DRAG_BUFFER = 0;\nconst VELOCITY_THRESHOLD = 500;\nconst GAP = 16;\nconst SPRING_OPTIONS = { type: 'spring' as const, stiffness: 300, damping: 30 };\n\ninterface CarouselItemProps {\n item: CarouselItem;\n index: number;\n itemWidth: number;\n round: boolean;\n trackItemOffset: number;\n x: any;\n transition: any;\n}\n\nfunction CarouselItem({ item, index, itemWidth, round, trackItemOffset, x, transition }: CarouselItemProps) {\n const range = [-(index + 1) * trackItemOffset, -index * trackItemOffset, -(index - 1) * trackItemOffset];\n const outputRange = [90, 0, -90];\n const rotateY = useTransform(x, range, outputRange, { clamp: false });\n\n return (\n \n
\n {item.icon}\n
\n
\n
{item.title}
\n

{item.description}

\n
\n \n );\n}\n\nexport default function Carousel({\n items = DEFAULT_ITEMS,\n baseWidth = 300,\n autoplay = false,\n autoplayDelay = 3000,\n pauseOnHover = false,\n loop = false,\n round = false\n}: CarouselProps): React.JSX.Element {\n const containerPadding = 16;\n const itemWidth = baseWidth - containerPadding * 2;\n const trackItemOffset = itemWidth + GAP;\n const itemsForRender = useMemo(() => {\n if (!loop) return items;\n if (items.length === 0) return [];\n return [items[items.length - 1], ...items, items[0]];\n }, [items, loop]);\n\n const [position, setPosition] = useState(loop ? 1 : 0);\n const x = useMotionValue(0);\n const [isHovered, setIsHovered] = useState(false);\n const [isJumping, setIsJumping] = useState(false);\n const [isAnimating, setIsAnimating] = useState(false);\n\n const containerRef = useRef(null);\n useEffect(() => {\n if (pauseOnHover && containerRef.current) {\n const container = containerRef.current;\n const handleMouseEnter = () => setIsHovered(true);\n const handleMouseLeave = () => setIsHovered(false);\n container.addEventListener('mouseenter', handleMouseEnter);\n container.addEventListener('mouseleave', handleMouseLeave);\n return () => {\n container.removeEventListener('mouseenter', handleMouseEnter);\n container.removeEventListener('mouseleave', handleMouseLeave);\n };\n }\n }, [pauseOnHover]);\n\n useEffect(() => {\n if (!autoplay || itemsForRender.length <= 1) return undefined;\n if (pauseOnHover && isHovered) return undefined;\n\n const timer = setInterval(() => {\n setPosition(prev => Math.min(prev + 1, itemsForRender.length - 1));\n }, autoplayDelay);\n\n return () => clearInterval(timer);\n }, [autoplay, autoplayDelay, isHovered, pauseOnHover, itemsForRender.length]);\n\n useEffect(() => {\n const startingPosition = loop ? 1 : 0;\n setPosition(startingPosition);\n x.set(-startingPosition * trackItemOffset);\n }, [items.length, loop, trackItemOffset, x]);\n\n useEffect(() => {\n if (!loop && position > itemsForRender.length - 1) {\n setPosition(Math.max(0, itemsForRender.length - 1));\n }\n }, [itemsForRender.length, loop, position]);\n\n const effectiveTransition = isJumping ? { duration: 0 } : SPRING_OPTIONS;\n\n const handleAnimationStart = () => {\n setIsAnimating(true);\n };\n\n const handleAnimationComplete = () => {\n if (!loop || itemsForRender.length <= 1) {\n setIsAnimating(false);\n return;\n }\n const lastCloneIndex = itemsForRender.length - 1;\n\n if (position === lastCloneIndex) {\n setIsJumping(true);\n const target = 1;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n if (position === 0) {\n setIsJumping(true);\n const target = items.length;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n setIsAnimating(false);\n };\n\n const handleDragEnd = (_: MouseEvent | TouchEvent | PointerEvent, info: PanInfo): void => {\n const { offset, velocity } = info;\n const direction =\n offset.x < -DRAG_BUFFER || velocity.x < -VELOCITY_THRESHOLD\n ? 1\n : offset.x > DRAG_BUFFER || velocity.x > VELOCITY_THRESHOLD\n ? -1\n : 0;\n\n if (direction === 0) return;\n\n setPosition(prev => {\n const next = prev + direction;\n const max = itemsForRender.length - 1;\n return Math.max(0, Math.min(next, max));\n });\n };\n\n const dragProps = loop\n ? {}\n : {\n dragConstraints: {\n left: -trackItemOffset * Math.max(itemsForRender.length - 1, 0),\n right: 0\n }\n };\n\n const activeIndex =\n items.length === 0 ? 0 : loop ? (position - 1 + items.length) % items.length : Math.min(position, items.length - 1);\n\n return (\n \n \n {itemsForRender.map((item, index) => (\n \n ))}\n \n
\n
\n {items.map((_, index) => (\n setPosition(loop ? index + 1 : index)}\n transition={{ duration: 0.15 }}\n />\n ))}\n
\n
\n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/Carousel-TS-TW.json b/public/r/Carousel-TS-TW.json index 3561e974..1d616e4e 100644 --- a/public/r/Carousel-TS-TW.json +++ b/public/r/Carousel-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "Carousel/Carousel.tsx", - "content": "import { useEffect, useMemo, useRef, useState } from 'react';\nimport { motion, PanInfo, useMotionValue, useTransform } from 'motion/react';\nimport React, { JSX } from 'react';\n\n// replace icons with your own if needed\nimport { FiCircle, FiCode, FiFileText, FiLayers, FiLayout } from 'react-icons/fi';\nexport interface CarouselItem {\n title: string;\n description: string;\n id: number;\n icon: React.ReactNode;\n}\n\nexport interface CarouselProps {\n items?: CarouselItem[];\n baseWidth?: number;\n autoplay?: boolean;\n autoplayDelay?: number;\n pauseOnHover?: boolean;\n loop?: boolean;\n round?: boolean;\n}\n\nconst DEFAULT_ITEMS: CarouselItem[] = [\n {\n title: 'Text Animations',\n description: 'Cool text animations for your projects.',\n id: 1,\n icon: \n },\n {\n title: 'Animations',\n description: 'Smooth animations for your projects.',\n id: 2,\n icon: \n },\n {\n title: 'Components',\n description: 'Reusable components for your projects.',\n id: 3,\n icon: \n },\n {\n title: 'Backgrounds',\n description: 'Beautiful backgrounds and patterns for your projects.',\n id: 4,\n icon: \n },\n {\n title: 'Common UI',\n description: 'Common UI components are coming soon!',\n id: 5,\n icon: \n }\n];\n\nconst DRAG_BUFFER = 0;\nconst VELOCITY_THRESHOLD = 500;\nconst GAP = 16;\nconst SPRING_OPTIONS = { type: 'spring' as const, stiffness: 300, damping: 30 };\n\ninterface CarouselItemProps {\n item: CarouselItem;\n index: number;\n itemWidth: number;\n round: boolean;\n trackItemOffset: number;\n x: any;\n transition: any;\n}\n\nfunction CarouselItem({ item, index, itemWidth, round, trackItemOffset, x, transition }: CarouselItemProps) {\n const range = [-(index + 1) * trackItemOffset, -index * trackItemOffset, -(index - 1) * trackItemOffset];\n const outputRange = [90, 0, -90];\n const rotateY = useTransform(x, range, outputRange, { clamp: false });\n\n return (\n \n
\n \n {item.icon}\n \n
\n
\n
{item.title}
\n

{item.description}

\n
\n \n );\n}\n\nexport default function Carousel({\n items = DEFAULT_ITEMS,\n baseWidth = 300,\n autoplay = false,\n autoplayDelay = 3000,\n pauseOnHover = false,\n loop = false,\n round = false\n}: CarouselProps): JSX.Element {\n const containerPadding = 16;\n const itemWidth = baseWidth - containerPadding * 2;\n const trackItemOffset = itemWidth + GAP;\n const itemsForRender = useMemo(() => {\n if (!loop) return items;\n if (items.length === 0) return [];\n return [items[items.length - 1], ...items, items[0]];\n }, [items, loop]);\n\n const [position, setPosition] = useState(loop ? 1 : 0);\n const x = useMotionValue(0);\n const [isHovered, setIsHovered] = useState(false);\n const [isJumping, setIsJumping] = useState(false);\n const [isAnimating, setIsAnimating] = useState(false);\n\n const containerRef = useRef(null);\n useEffect(() => {\n if (pauseOnHover && containerRef.current) {\n const container = containerRef.current;\n const handleMouseEnter = () => setIsHovered(true);\n const handleMouseLeave = () => setIsHovered(false);\n container.addEventListener('mouseenter', handleMouseEnter);\n container.addEventListener('mouseleave', handleMouseLeave);\n return () => {\n container.removeEventListener('mouseenter', handleMouseEnter);\n container.removeEventListener('mouseleave', handleMouseLeave);\n };\n }\n }, [pauseOnHover]);\n\n useEffect(() => {\n if (!autoplay || itemsForRender.length <= 1) return undefined;\n if (pauseOnHover && isHovered) return undefined;\n\n const timer = setInterval(() => {\n setPosition(prev => Math.min(prev + 1, itemsForRender.length - 1));\n }, autoplayDelay);\n\n return () => clearInterval(timer);\n }, [autoplay, autoplayDelay, isHovered, pauseOnHover, itemsForRender.length]);\n\n useEffect(() => {\n const startingPosition = loop ? 1 : 0;\n setPosition(startingPosition);\n x.set(-startingPosition * trackItemOffset);\n }, [items.length, loop, trackItemOffset, x]);\n\n useEffect(() => {\n if (!loop && position > itemsForRender.length - 1) {\n setPosition(Math.max(0, itemsForRender.length - 1));\n }\n }, [itemsForRender.length, loop, position]);\n\n const effectiveTransition = isJumping ? { duration: 0 } : SPRING_OPTIONS;\n\n const handleAnimationStart = () => {\n setIsAnimating(true);\n };\n\n const handleAnimationComplete = () => {\n if (!loop || itemsForRender.length <= 1) {\n setIsAnimating(false);\n return;\n }\n const lastCloneIndex = itemsForRender.length - 1;\n\n if (position === lastCloneIndex) {\n setIsJumping(true);\n const target = 1;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n if (position === 0) {\n setIsJumping(true);\n const target = items.length;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n setIsAnimating(false);\n };\n\n const handleDragEnd = (_: MouseEvent | TouchEvent | PointerEvent, info: PanInfo): void => {\n const { offset, velocity } = info;\n const direction =\n offset.x < -DRAG_BUFFER || velocity.x < -VELOCITY_THRESHOLD\n ? 1\n : offset.x > DRAG_BUFFER || velocity.x > VELOCITY_THRESHOLD\n ? -1\n : 0;\n\n if (direction === 0) return;\n\n setPosition(prev => {\n const next = prev + direction;\n const max = itemsForRender.length - 1;\n return Math.max(0, Math.min(next, max));\n });\n };\n\n const dragProps = loop\n ? {}\n : {\n dragConstraints: {\n left: -trackItemOffset * Math.max(itemsForRender.length - 1, 0),\n right: 0\n }\n };\n\n const activeIndex =\n items.length === 0 ? 0 : loop ? (position - 1 + items.length) % items.length : Math.min(position, items.length - 1);\n\n return (\n \n \n {itemsForRender.map((item, index) => (\n \n ))}\n \n
\n
\n {items.map((_, index) => (\n setPosition(loop ? index + 1 : index)}\n transition={{ duration: 0.15 }}\n />\n ))}\n
\n
\n \n );\n}\n" + "content": "import { useEffect, useMemo, useRef, useState } from 'react';\nimport { motion, type PanInfo, useMotionValue, useTransform } from 'motion/react';\nimport React, { type JSX } from 'react';\n\n// replace icons with your own if needed\nimport { FiCircle, FiCode, FiFileText, FiLayers, FiLayout } from 'react-icons/fi';\nexport interface CarouselItem {\n title: string;\n description: string;\n id: number;\n icon: React.ReactNode;\n}\n\nexport interface CarouselProps {\n items?: CarouselItem[];\n baseWidth?: number;\n autoplay?: boolean;\n autoplayDelay?: number;\n pauseOnHover?: boolean;\n loop?: boolean;\n round?: boolean;\n}\n\nconst DEFAULT_ITEMS: CarouselItem[] = [\n {\n title: 'Text Animations',\n description: 'Cool text animations for your projects.',\n id: 1,\n icon: \n },\n {\n title: 'Animations',\n description: 'Smooth animations for your projects.',\n id: 2,\n icon: \n },\n {\n title: 'Components',\n description: 'Reusable components for your projects.',\n id: 3,\n icon: \n },\n {\n title: 'Backgrounds',\n description: 'Beautiful backgrounds and patterns for your projects.',\n id: 4,\n icon: \n },\n {\n title: 'Common UI',\n description: 'Common UI components are coming soon!',\n id: 5,\n icon: \n }\n];\n\nconst DRAG_BUFFER = 0;\nconst VELOCITY_THRESHOLD = 500;\nconst GAP = 16;\nconst SPRING_OPTIONS = { type: 'spring' as const, stiffness: 300, damping: 30 };\n\ninterface CarouselItemProps {\n item: CarouselItem;\n index: number;\n itemWidth: number;\n round: boolean;\n trackItemOffset: number;\n x: any;\n transition: any;\n}\n\nfunction CarouselItem({ item, index, itemWidth, round, trackItemOffset, x, transition }: CarouselItemProps) {\n const range = [-(index + 1) * trackItemOffset, -index * trackItemOffset, -(index - 1) * trackItemOffset];\n const outputRange = [90, 0, -90];\n const rotateY = useTransform(x, range, outputRange, { clamp: false });\n\n return (\n \n
\n \n {item.icon}\n \n
\n
\n
{item.title}
\n

{item.description}

\n
\n \n );\n}\n\nexport default function Carousel({\n items = DEFAULT_ITEMS,\n baseWidth = 300,\n autoplay = false,\n autoplayDelay = 3000,\n pauseOnHover = false,\n loop = false,\n round = false\n}: CarouselProps): JSX.Element {\n const containerPadding = 16;\n const itemWidth = baseWidth - containerPadding * 2;\n const trackItemOffset = itemWidth + GAP;\n const itemsForRender = useMemo(() => {\n if (!loop) return items;\n if (items.length === 0) return [];\n return [items[items.length - 1], ...items, items[0]];\n }, [items, loop]);\n\n const [position, setPosition] = useState(loop ? 1 : 0);\n const x = useMotionValue(0);\n const [isHovered, setIsHovered] = useState(false);\n const [isJumping, setIsJumping] = useState(false);\n const [isAnimating, setIsAnimating] = useState(false);\n\n const containerRef = useRef(null);\n useEffect(() => {\n if (pauseOnHover && containerRef.current) {\n const container = containerRef.current;\n const handleMouseEnter = () => setIsHovered(true);\n const handleMouseLeave = () => setIsHovered(false);\n container.addEventListener('mouseenter', handleMouseEnter);\n container.addEventListener('mouseleave', handleMouseLeave);\n return () => {\n container.removeEventListener('mouseenter', handleMouseEnter);\n container.removeEventListener('mouseleave', handleMouseLeave);\n };\n }\n }, [pauseOnHover]);\n\n useEffect(() => {\n if (!autoplay || itemsForRender.length <= 1) return undefined;\n if (pauseOnHover && isHovered) return undefined;\n\n const timer = setInterval(() => {\n setPosition(prev => Math.min(prev + 1, itemsForRender.length - 1));\n }, autoplayDelay);\n\n return () => clearInterval(timer);\n }, [autoplay, autoplayDelay, isHovered, pauseOnHover, itemsForRender.length]);\n\n useEffect(() => {\n const startingPosition = loop ? 1 : 0;\n setPosition(startingPosition);\n x.set(-startingPosition * trackItemOffset);\n }, [items.length, loop, trackItemOffset, x]);\n\n useEffect(() => {\n if (!loop && position > itemsForRender.length - 1) {\n setPosition(Math.max(0, itemsForRender.length - 1));\n }\n }, [itemsForRender.length, loop, position]);\n\n const effectiveTransition = isJumping ? { duration: 0 } : SPRING_OPTIONS;\n\n const handleAnimationStart = () => {\n setIsAnimating(true);\n };\n\n const handleAnimationComplete = () => {\n if (!loop || itemsForRender.length <= 1) {\n setIsAnimating(false);\n return;\n }\n const lastCloneIndex = itemsForRender.length - 1;\n\n if (position === lastCloneIndex) {\n setIsJumping(true);\n const target = 1;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n if (position === 0) {\n setIsJumping(true);\n const target = items.length;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n setIsAnimating(false);\n };\n\n const handleDragEnd = (_: MouseEvent | TouchEvent | PointerEvent, info: PanInfo): void => {\n const { offset, velocity } = info;\n const direction =\n offset.x < -DRAG_BUFFER || velocity.x < -VELOCITY_THRESHOLD\n ? 1\n : offset.x > DRAG_BUFFER || velocity.x > VELOCITY_THRESHOLD\n ? -1\n : 0;\n\n if (direction === 0) return;\n\n setPosition(prev => {\n const next = prev + direction;\n const max = itemsForRender.length - 1;\n return Math.max(0, Math.min(next, max));\n });\n };\n\n const dragProps = loop\n ? {}\n : {\n dragConstraints: {\n left: -trackItemOffset * Math.max(itemsForRender.length - 1, 0),\n right: 0\n }\n };\n\n const activeIndex =\n items.length === 0 ? 0 : loop ? (position - 1 + items.length) % items.length : Math.min(position, items.length - 1);\n\n return (\n \n \n {itemsForRender.map((item, index) => (\n \n ))}\n \n
\n
\n {items.map((_, index) => (\n setPosition(loop ? index + 1 : index)}\n transition={{ duration: 0.15 }}\n />\n ))}\n
\n
\n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/ChromaGrid-JS-CSS.json b/public/r/ChromaGrid-JS-CSS.json index a60dccb3..62183f5a 100644 --- a/public/r/ChromaGrid-JS-CSS.json +++ b/public/r/ChromaGrid-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "ChromaGrid/ChromaGrid.css", + "type": "registry:file", + "path": "ChromaGrid.css", + "target": "@components/ChromaGrid.css", "content": ".chroma-grid {\n position: relative;\n width: 100%;\n height: 100%;\n display: grid;\n grid-template-columns: repeat(var(--cols, 3), 320px);\n grid-auto-rows: auto;\n justify-content: center;\n gap: 0.75rem;\n max-width: 1200px;\n margin: 0 auto;\n padding: 1rem;\n box-sizing: border-box;\n\n --x: 50%;\n --y: 50%;\n --r: 220px;\n}\n\n@media (max-width: 1124px) {\n .chroma-grid {\n grid-template-columns: repeat(auto-fit, minmax(320px, 320px));\n gap: 0.5rem;\n padding: 0.5rem;\n }\n}\n\n@media (max-width: 480px) {\n .chroma-grid {\n grid-template-columns: 320px;\n gap: 0.75rem;\n padding: 1rem;\n }\n}\n\n.chroma-card {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 320px;\n height: auto;\n border-radius: 20px;\n overflow: hidden;\n border: 1px solid #333;\n transition: border-color 0.3s ease;\n background: var(--card-gradient);\n\n --mouse-x: 50%;\n --mouse-y: 50%;\n --spotlight-color: rgba(255, 255, 255, 0.3);\n}\n\n.chroma-card:hover {\n border-color: var(--card-border);\n}\n\n.chroma-card::before {\n content: '';\n position: absolute;\n inset: 0;\n background: radial-gradient(circle at var(--mouse-x) var(--mouse-y), var(--spotlight-color), transparent 70%);\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.5s ease;\n z-index: 2;\n}\n\n.chroma-card:hover::before {\n opacity: 1;\n}\n\n.chroma-img-wrapper {\n position: relative;\n z-index: 1;\n flex: 1;\n padding: 10px;\n box-sizing: border-box;\n background: transparent;\n transition: background 0.3s ease;\n}\n\n.chroma-img-wrapper img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n border-radius: 10px;\n display: block;\n}\n\n.chroma-info {\n position: relative;\n z-index: 1;\n padding: 0.75rem 1rem;\n color: #fff;\n font-family: system-ui, sans-serif;\n display: grid;\n grid-template-columns: 1fr auto;\n row-gap: 0.25rem;\n column-gap: 0.75rem;\n}\n\n.chroma-info .role,\n.chroma-info .handle {\n color: #aaa;\n}\n\n.chroma-overlay {\n position: absolute;\n inset: 0;\n pointer-events: none;\n z-index: 3;\n backdrop-filter: grayscale(1) brightness(0.78);\n -webkit-backdrop-filter: grayscale(1) brightness(0.78);\n background: rgba(0, 0, 0, 0.001);\n\n mask-image: radial-gradient(\n circle var(--r) at var(--x) var(--y),\n transparent 0%,\n transparent 15%,\n rgba(0, 0, 0, 0.1) 30%,\n rgba(0, 0, 0, 0.22) 45%,\n rgba(0, 0, 0, 0.35) 60%,\n rgba(0, 0, 0, 0.5) 75%,\n rgba(0, 0, 0, 0.68) 88%,\n white 100%\n );\n -webkit-mask-image: radial-gradient(\n circle var(--r) at var(--x) var(--y),\n transparent 0%,\n transparent 15%,\n rgba(0, 0, 0, 0.1) 30%,\n rgba(0, 0, 0, 0.22) 45%,\n rgba(0, 0, 0, 0.35) 60%,\n rgba(0, 0, 0, 0.5) 75%,\n rgba(0, 0, 0, 0.68) 88%,\n white 100%\n );\n}\n\n.chroma-fade {\n position: absolute;\n inset: 0;\n pointer-events: none;\n z-index: 4;\n backdrop-filter: grayscale(1) brightness(0.78);\n -webkit-backdrop-filter: grayscale(1) brightness(0.78);\n background: rgba(0, 0, 0, 0.001);\n\n mask-image: radial-gradient(\n circle var(--r) at var(--x) var(--y),\n white 0%,\n white 15%,\n rgba(255, 255, 255, 0.9) 30%,\n rgba(255, 255, 255, 0.78) 45%,\n rgba(255, 255, 255, 0.65) 60%,\n rgba(255, 255, 255, 0.5) 75%,\n rgba(255, 255, 255, 0.32) 88%,\n transparent 100%\n );\n -webkit-mask-image: radial-gradient(\n circle var(--r) at var(--x) var(--y),\n white 0%,\n white 15%,\n rgba(255, 255, 255, 0.9) 30%,\n rgba(255, 255, 255, 0.78) 45%,\n rgba(255, 255, 255, 0.65) 60%,\n rgba(255, 255, 255, 0.5) 75%,\n rgba(255, 255, 255, 0.32) 88%,\n transparent 100%\n );\n\n opacity: 1;\n transition: opacity 0.25s ease;\n}\n" }, { "type": "registry:component", - "path": "ChromaGrid/ChromaGrid.jsx", + "path": "ChromaGrid.jsx", "content": "import { useRef, useEffect } from 'react';\nimport { gsap } from 'gsap';\nimport './ChromaGrid.css';\n\nexport const ChromaGrid = ({\n items,\n className = '',\n radius = 300,\n columns = 3,\n rows = 2,\n damping = 0.45,\n fadeOut = 0.6,\n ease = 'power3.out'\n}) => {\n const rootRef = useRef(null);\n const fadeRef = useRef(null);\n const setX = useRef(null);\n const setY = useRef(null);\n const pos = useRef({ x: 0, y: 0 });\n\n const demo = [\n {\n image: 'https://i.pravatar.cc/300?img=8',\n title: 'Alex Rivera',\n subtitle: 'Full Stack Developer',\n handle: '@alexrivera',\n borderColor: '#4F46E5',\n gradient: 'linear-gradient(145deg, #4F46E5, #000)',\n url: 'https://github.com/'\n },\n {\n image: 'https://i.pravatar.cc/300?img=11',\n title: 'Jordan Chen',\n subtitle: 'DevOps Engineer',\n handle: '@jordanchen',\n borderColor: '#10B981',\n gradient: 'linear-gradient(210deg, #10B981, #000)',\n url: 'https://linkedin.com/in/'\n },\n {\n image: 'https://i.pravatar.cc/300?img=3',\n title: 'Morgan Blake',\n subtitle: 'UI/UX Designer',\n handle: '@morganblake',\n borderColor: '#F59E0B',\n gradient: 'linear-gradient(165deg, #F59E0B, #000)',\n url: 'https://dribbble.com/'\n },\n {\n image: 'https://i.pravatar.cc/300?img=16',\n title: 'Casey Park',\n subtitle: 'Data Scientist',\n handle: '@caseypark',\n borderColor: '#EF4444',\n gradient: 'linear-gradient(195deg, #EF4444, #000)',\n url: 'https://kaggle.com/'\n },\n {\n image: 'https://i.pravatar.cc/300?img=25',\n title: 'Sam Kim',\n subtitle: 'Mobile Developer',\n handle: '@thesamkim',\n borderColor: '#8B5CF6',\n gradient: 'linear-gradient(225deg, #8B5CF6, #000)',\n url: 'https://github.com/'\n },\n {\n image: 'https://i.pravatar.cc/300?img=60',\n title: 'Tyler Rodriguez',\n subtitle: 'Cloud Architect',\n handle: '@tylerrod',\n borderColor: '#06B6D4',\n gradient: 'linear-gradient(135deg, #06B6D4, #000)',\n url: 'https://aws.amazon.com/'\n }\n ];\n const data = items?.length ? items : demo;\n\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n setX.current = gsap.quickSetter(el, '--x', 'px');\n setY.current = gsap.quickSetter(el, '--y', 'px');\n const { width, height } = el.getBoundingClientRect();\n pos.current = { x: width / 2, y: height / 2 };\n setX.current(pos.current.x);\n setY.current(pos.current.y);\n }, []);\n\n const moveTo = (x, y) => {\n gsap.to(pos.current, {\n x,\n y,\n duration: damping,\n ease,\n onUpdate: () => {\n setX.current?.(pos.current.x);\n setY.current?.(pos.current.y);\n },\n overwrite: true\n });\n };\n\n const handleMove = e => {\n const r = rootRef.current.getBoundingClientRect();\n moveTo(e.clientX - r.left, e.clientY - r.top);\n gsap.to(fadeRef.current, { opacity: 0, duration: 0.25, overwrite: true });\n };\n\n const handleLeave = () => {\n gsap.to(fadeRef.current, {\n opacity: 1,\n duration: fadeOut,\n overwrite: true\n });\n };\n\n const handleCardClick = url => {\n if (url) {\n window.open(url, '_blank', 'noopener,noreferrer');\n }\n };\n\n const handleCardMove = e => {\n const card = e.currentTarget;\n const rect = card.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n card.style.setProperty('--mouse-x', `${x}px`);\n card.style.setProperty('--mouse-y', `${y}px`);\n };\n\n return (\n \n {data.map((c, i) => (\n handleCardClick(c.url)}\n style={{\n '--card-border': c.borderColor || 'transparent',\n '--card-gradient': c.gradient,\n cursor: c.url ? 'pointer' : 'default'\n }}\n >\n
\n {c.title}\n
\n
\n

{c.title}

\n {c.handle && {c.handle}}\n

{c.subtitle}

\n {c.location && {c.location}}\n
\n \n ))}\n
\n
\n
\n );\n};\n\nexport default ChromaGrid;\n" } ], diff --git a/public/r/ChromaGrid-TS-CSS.json b/public/r/ChromaGrid-TS-CSS.json index 1d9f97b6..920a77be 100644 --- a/public/r/ChromaGrid-TS-CSS.json +++ b/public/r/ChromaGrid-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "ChromaGrid/ChromaGrid.css", + "type": "registry:file", + "path": "ChromaGrid.css", + "target": "@components/ChromaGrid.css", "content": ".chroma-grid {\n position: relative;\n width: 100%;\n height: 100%;\n display: grid;\n grid-template-columns: repeat(var(--cols, 3), 320px);\n grid-auto-rows: auto;\n justify-content: center;\n gap: 0.75rem;\n max-width: 1200px;\n margin: 0 auto;\n padding: 1rem;\n box-sizing: border-box;\n\n --x: 50%;\n --y: 50%;\n --r: 220px;\n}\n\n@media (max-width: 1124px) {\n .chroma-grid {\n grid-template-columns: repeat(auto-fit, minmax(320px, 320px));\n gap: 0.5rem;\n padding: 0.5rem;\n }\n}\n\n@media (max-width: 480px) {\n .chroma-grid {\n grid-template-columns: 320px;\n gap: 0.75rem;\n padding: 1rem;\n }\n}\n\n.chroma-card {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 320px;\n height: auto;\n border-radius: 20px;\n overflow: hidden;\n border: 1px solid #333;\n transition: border-color 0.3s ease;\n background: var(--card-gradient);\n\n --mouse-x: 50%;\n --mouse-y: 50%;\n --spotlight-color: rgba(255, 255, 255, 0.3);\n}\n\n.chroma-card:hover {\n border-color: var(--card-border);\n}\n\n.chroma-card::before {\n content: '';\n position: absolute;\n inset: 0;\n background: radial-gradient(circle at var(--mouse-x) var(--mouse-y), var(--spotlight-color), transparent 70%);\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.5s ease;\n z-index: 2;\n}\n\n.chroma-card:hover::before {\n opacity: 1;\n}\n\n.chroma-img-wrapper {\n position: relative;\n z-index: 1;\n flex: 1;\n padding: 10px;\n box-sizing: border-box;\n background: transparent;\n transition: background 0.3s ease;\n}\n\n.chroma-img-wrapper img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n border-radius: 10px;\n display: block;\n}\n\n.chroma-info {\n position: relative;\n z-index: 1;\n padding: 0.75rem 1rem;\n color: #fff;\n font-family: system-ui, sans-serif;\n display: grid;\n grid-template-columns: 1fr auto;\n row-gap: 0.25rem;\n column-gap: 0.75rem;\n}\n\n.chroma-info .role,\n.chroma-info .handle {\n color: #aaa;\n}\n\n.chroma-overlay {\n position: absolute;\n inset: 0;\n pointer-events: none;\n z-index: 3;\n backdrop-filter: grayscale(1) brightness(0.78);\n -webkit-backdrop-filter: grayscale(1) brightness(0.78);\n background: rgba(0, 0, 0, 0.001);\n\n mask-image: radial-gradient(\n circle var(--r) at var(--x) var(--y),\n transparent 0%,\n transparent 15%,\n rgba(0, 0, 0, 0.1) 30%,\n rgba(0, 0, 0, 0.22) 45%,\n rgba(0, 0, 0, 0.35) 60%,\n rgba(0, 0, 0, 0.5) 75%,\n rgba(0, 0, 0, 0.68) 88%,\n white 100%\n );\n -webkit-mask-image: radial-gradient(\n circle var(--r) at var(--x) var(--y),\n transparent 0%,\n transparent 15%,\n rgba(0, 0, 0, 0.1) 30%,\n rgba(0, 0, 0, 0.22) 45%,\n rgba(0, 0, 0, 0.35) 60%,\n rgba(0, 0, 0, 0.5) 75%,\n rgba(0, 0, 0, 0.68) 88%,\n white 100%\n );\n}\n\n.chroma-fade {\n position: absolute;\n inset: 0;\n pointer-events: none;\n z-index: 4;\n backdrop-filter: grayscale(1) brightness(0.78);\n -webkit-backdrop-filter: grayscale(1) brightness(0.78);\n background: rgba(0, 0, 0, 0.001);\n\n mask-image: radial-gradient(\n circle var(--r) at var(--x) var(--y),\n white 0%,\n white 15%,\n rgba(255, 255, 255, 0.9) 30%,\n rgba(255, 255, 255, 0.78) 45%,\n rgba(255, 255, 255, 0.65) 60%,\n rgba(255, 255, 255, 0.5) 75%,\n rgba(255, 255, 255, 0.32) 88%,\n transparent 100%\n );\n -webkit-mask-image: radial-gradient(\n circle var(--r) at var(--x) var(--y),\n white 0%,\n white 15%,\n rgba(255, 255, 255, 0.9) 30%,\n rgba(255, 255, 255, 0.78) 45%,\n rgba(255, 255, 255, 0.65) 60%,\n rgba(255, 255, 255, 0.5) 75%,\n rgba(255, 255, 255, 0.32) 88%,\n transparent 100%\n );\n\n opacity: 1;\n transition: opacity 0.25s ease;\n}\n" }, { "type": "registry:component", - "path": "ChromaGrid/ChromaGrid.tsx", + "path": "ChromaGrid.tsx", "content": "import React, { useRef, useEffect } from 'react';\nimport { gsap } from 'gsap';\nimport './ChromaGrid.css';\n\nexport interface ChromaItem {\n image: string;\n title: string;\n subtitle: string;\n handle?: string;\n location?: string;\n borderColor?: string;\n gradient?: string;\n url?: string;\n}\n\nexport interface ChromaGridProps {\n items?: ChromaItem[];\n className?: string;\n radius?: number;\n columns?: number;\n rows?: number;\n damping?: number;\n fadeOut?: number;\n ease?: string;\n}\n\ntype SetterFn = (v: number | string) => void;\n\nexport const ChromaGrid: React.FC = ({\n items,\n className = '',\n radius = 300,\n columns = 3,\n rows = 2,\n damping = 0.45,\n fadeOut = 0.6,\n ease = 'power3.out'\n}) => {\n const rootRef = useRef(null);\n const fadeRef = useRef(null);\n const setX = useRef(null);\n const setY = useRef(null);\n const pos = useRef({ x: 0, y: 0 });\n\n const demo: ChromaItem[] = [\n {\n image: 'https://i.pravatar.cc/300?img=8',\n title: 'Alex Rivera',\n subtitle: 'Full Stack Developer',\n handle: '@alexrivera',\n borderColor: '#4F46E5',\n gradient: 'linear-gradient(145deg, #4F46E5, #000)',\n url: 'https://github.com/'\n },\n {\n image: 'https://i.pravatar.cc/300?img=11',\n title: 'Jordan Chen',\n subtitle: 'DevOps Engineer',\n handle: '@jordanchen',\n borderColor: '#10B981',\n gradient: 'linear-gradient(210deg, #10B981, #000)',\n url: 'https://linkedin.com/in/'\n },\n {\n image: 'https://i.pravatar.cc/300?img=3',\n title: 'Morgan Blake',\n subtitle: 'UI/UX Designer',\n handle: '@morganblake',\n borderColor: '#F59E0B',\n gradient: 'linear-gradient(165deg, #F59E0B, #000)',\n url: 'https://dribbble.com/'\n },\n {\n image: 'https://i.pravatar.cc/300?img=16',\n title: 'Casey Park',\n subtitle: 'Data Scientist',\n handle: '@caseypark',\n borderColor: '#EF4444',\n gradient: 'linear-gradient(195deg, #EF4444, #000)',\n url: 'https://kaggle.com/'\n },\n {\n image: 'https://i.pravatar.cc/300?img=25',\n title: 'Sam Kim',\n subtitle: 'Mobile Developer',\n handle: '@thesamkim',\n borderColor: '#8B5CF6',\n gradient: 'linear-gradient(225deg, #8B5CF6, #000)',\n url: 'https://github.com/'\n },\n {\n image: 'https://i.pravatar.cc/300?img=60',\n title: 'Tyler Rodriguez',\n subtitle: 'Cloud Architect',\n handle: '@tylerrod',\n borderColor: '#06B6D4',\n gradient: 'linear-gradient(135deg, #06B6D4, #000)',\n url: 'https://aws.amazon.com/'\n }\n ];\n const data = items?.length ? items : demo;\n\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n setX.current = gsap.quickSetter(el, '--x', 'px') as SetterFn;\n setY.current = gsap.quickSetter(el, '--y', 'px') as SetterFn;\n const { width, height } = el.getBoundingClientRect();\n pos.current = { x: width / 2, y: height / 2 };\n setX.current(pos.current.x);\n setY.current(pos.current.y);\n }, []);\n\n const moveTo = (x: number, y: number) => {\n gsap.to(pos.current, {\n x,\n y,\n duration: damping,\n ease,\n onUpdate: () => {\n setX.current?.(pos.current.x);\n setY.current?.(pos.current.y);\n },\n overwrite: true\n });\n };\n\n const handleMove = (e: React.PointerEvent) => {\n const r = rootRef.current!.getBoundingClientRect();\n moveTo(e.clientX - r.left, e.clientY - r.top);\n gsap.to(fadeRef.current, { opacity: 0, duration: 0.25, overwrite: true });\n };\n\n const handleLeave = () => {\n gsap.to(fadeRef.current, {\n opacity: 1,\n duration: fadeOut,\n overwrite: true\n });\n };\n\n const handleCardClick = (url?: string) => {\n if (url) {\n window.open(url, '_blank', 'noopener,noreferrer');\n }\n };\n\n const handleCardMove: React.MouseEventHandler = e => {\n const card = e.currentTarget as HTMLElement;\n const rect = card.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n card.style.setProperty('--mouse-x', `${x}px`);\n card.style.setProperty('--mouse-y', `${y}px`);\n };\n\n return (\n \n {data.map((c, i) => (\n handleCardClick(c.url)}\n style={\n {\n '--card-border': c.borderColor || 'transparent',\n '--card-gradient': c.gradient,\n cursor: c.url ? 'pointer' : 'default'\n } as React.CSSProperties\n }\n >\n
\n {c.title}\n
\n
\n

{c.title}

\n {c.handle && {c.handle}}\n

{c.subtitle}

\n {c.location && {c.location}}\n
\n \n ))}\n
\n
\n
\n );\n};\n\nexport default ChromaGrid;\n" } ], diff --git a/public/r/CircularGallery-JS-CSS.json b/public/r/CircularGallery-JS-CSS.json index 63a9aaf9..e90339ea 100644 --- a/public/r/CircularGallery-JS-CSS.json +++ b/public/r/CircularGallery-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "CircularGallery/CircularGallery.css", + "type": "registry:file", + "path": "CircularGallery.css", + "target": "@components/CircularGallery.css", "content": ".circular-gallery {\n width: 100%;\n height: 100%;\n overflow: hidden;\n cursor: grab;\n}\n\n.circular-gallery:active {\n cursor: grabbing;\n}\n\n.circular-gallery:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 4px;\n}\n" }, { "type": "registry:component", - "path": "CircularGallery/CircularGallery.jsx", + "path": "CircularGallery.jsx", "content": "import { Camera, Mesh, Plane, Program, Renderer, Texture, Transform } from 'ogl';\nimport { useEffect, useRef } from 'react';\n\nimport './CircularGallery.css';\n\nfunction debounce(func, wait) {\n let timeout;\n return function (...args) {\n clearTimeout(timeout);\n timeout = setTimeout(() => func.apply(this, args), wait);\n };\n}\n\nfunction lerp(p1, p2, t) {\n return p1 + (p2 - p1) * t;\n}\n\nfunction autoBind(instance) {\n const proto = Object.getPrototypeOf(instance);\n Object.getOwnPropertyNames(proto).forEach(key => {\n if (key !== 'constructor' && typeof instance[key] === 'function') {\n instance[key] = instance[key].bind(instance);\n }\n });\n}\n\nconst DEFAULT_FONT = 'bold 30px Figtree';\n// Figtree is not guaranteed to be available on the host page, so the component\n// loads it on demand whenever the default font is used.\nconst DEFAULT_FONT_URL = 'https://fonts.googleapis.com/css2?family=Figtree:wght@400;700&display=swap';\n\nfunction deriveFontFamilyFromUrl(url) {\n const fileName = (url.split('/').pop() || 'custom-font').split('?')[0];\n const base = fileName.replace(/\\.(woff2?|ttf|otf|eot)$/i, '');\n return base.replace(/[^a-zA-Z0-9-_ ]/g, '').trim() || 'CircularGalleryFont';\n}\n\nasync function loadFontFromStylesheet(url) {\n const response = await fetch(url);\n if (!response.ok) throw new Error(`Failed to fetch font stylesheet (${response.status})`);\n const cssText = await response.text();\n const faceBlocks = cssText.match(/@font-face\\s*{[^}]*}/g) || [];\n let family = null;\n const fontFaces = [];\n for (const block of faceBlocks) {\n const familyMatch = block.match(/font-family:\\s*['\"]?([^;'\"]+)['\"]?/);\n const urlMatch = block.match(/url\\(\\s*['\"]?([^'\")]+)['\"]?\\s*\\)/);\n if (!familyMatch || !urlMatch) continue;\n family = familyMatch[1].trim();\n const descriptors = {};\n const weightMatch = block.match(/font-weight:\\s*([^;]+);/);\n const styleMatch = block.match(/font-style:\\s*([^;]+);/);\n const rangeMatch = block.match(/unicode-range:\\s*([^;]+);/);\n if (weightMatch) descriptors.weight = weightMatch[1].trim();\n if (styleMatch) descriptors.style = styleMatch[1].trim();\n if (rangeMatch) descriptors.unicodeRange = rangeMatch[1].trim();\n fontFaces.push(new FontFace(family, `url(${urlMatch[1]})`, descriptors));\n }\n if (!family) throw new Error('No @font-face rule found in the stylesheet');\n await Promise.allSettled(\n fontFaces.map(async face => {\n await face.load();\n document.fonts.add(face);\n })\n );\n return family;\n}\n\nasync function loadFontFromFile(url) {\n const family = deriveFontFamilyFromUrl(url);\n const fontFace = new FontFace(family, `url(${url})`);\n await fontFace.load();\n document.fonts.add(fontFace);\n return family;\n}\n\nasync function loadCustomFont(fontUrl) {\n const isStylesheet = fontUrl.includes('fonts.googleapis.com') || /\\.css(\\?.*)?$/i.test(fontUrl);\n return isStylesheet ? loadFontFromStylesheet(fontUrl) : loadFontFromFile(fontUrl);\n}\n\n// Loads `fontUrl` (a stylesheet such as a Google Fonts URL, or a direct font\n// file) and returns a canvas-ready font string that keeps the size/weight from\n// `font` but swaps in the freshly loaded family. Falls back to `font` on error.\nasync function resolveFont(font, fontUrl) {\n // Use the bundled Figtree stylesheet when the caller relies on the default\n // font, otherwise honor the explicit `fontUrl`.\n const effectiveUrl = fontUrl || (font === DEFAULT_FONT ? DEFAULT_FONT_URL : null);\n if (!effectiveUrl) {\n // A custom family was supplied without a URL – make sure it is ready (in\n // case the host page declares it) before we draw it to the canvas,\n // otherwise the first paint silently falls back to a system font.\n if (document.fonts && document.fonts.load) {\n try {\n await document.fonts.load(font);\n await document.fonts.ready;\n } catch {\n // Ignore – fall back to whatever the browser provides.\n }\n }\n return font;\n }\n try {\n const family = await loadCustomFont(effectiveUrl);\n const sizeMatch = font.match(/^\\s*(.*?\\d+px)/);\n const prefix = sizeMatch ? sizeMatch[1].trim() : 'bold 30px';\n const resolved = `${prefix} \"${family}\"`;\n if (document.fonts && document.fonts.load) {\n try {\n await document.fonts.load(resolved);\n } catch {\n // Ignore – we still attempt to render with the requested font.\n }\n }\n return resolved;\n } catch (error) {\n console.error('CircularGallery: unable to load font from', fontUrl, error);\n return font;\n }\n}\n\nfunction getFontSize(font) {\n const match = font.match(/(\\d+)px/);\n return match ? parseInt(match[1], 10) : 30;\n}\n\nfunction createTextTexture(gl, text, font = 'bold 30px monospace', color = 'black') {\n const canvas = document.createElement('canvas');\n const context = canvas.getContext('2d');\n context.font = font;\n const metrics = context.measureText(text);\n const textWidth = Math.ceil(metrics.width);\n const textHeight = Math.ceil(getFontSize(font) * 1.2);\n canvas.width = textWidth + 20;\n canvas.height = textHeight + 20;\n context.font = font;\n context.fillStyle = color;\n context.textBaseline = 'middle';\n context.textAlign = 'center';\n context.clearRect(0, 0, canvas.width, canvas.height);\n context.fillText(text, canvas.width / 2, canvas.height / 2);\n const texture = new Texture(gl, { generateMipmaps: false });\n texture.image = canvas;\n return { texture, width: canvas.width, height: canvas.height };\n}\n\nclass Title {\n constructor({ gl, plane, renderer, text, textColor = '#545050', font = '30px sans-serif' }) {\n autoBind(this);\n this.gl = gl;\n this.plane = plane;\n this.renderer = renderer;\n this.text = text;\n this.textColor = textColor;\n this.font = font;\n this.createMesh();\n }\n createMesh() {\n const { texture, width, height } = createTextTexture(this.gl, this.text, this.font, this.textColor);\n const geometry = new Plane(this.gl);\n const program = new Program(this.gl, {\n vertex: `\n attribute vec3 position;\n attribute vec2 uv;\n uniform mat4 modelViewMatrix;\n uniform mat4 projectionMatrix;\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragment: `\n precision highp float;\n uniform sampler2D tMap;\n varying vec2 vUv;\n void main() {\n vec4 color = texture2D(tMap, vUv);\n if (color.a < 0.1) discard;\n gl_FragColor = color;\n }\n `,\n uniforms: { tMap: { value: texture } },\n transparent: true\n });\n this.mesh = new Mesh(this.gl, { geometry, program });\n const aspect = width / height;\n const textHeight = this.plane.scale.y * 0.15;\n const textWidth = textHeight * aspect;\n this.mesh.scale.set(textWidth, textHeight, 1);\n this.mesh.position.y = -this.plane.scale.y * 0.5 - textHeight * 0.5 - 0.05;\n this.mesh.setParent(this.plane);\n }\n}\n\nclass Media {\n constructor({\n geometry,\n gl,\n image,\n index,\n length,\n renderer,\n scene,\n screen,\n text,\n viewport,\n bend,\n textColor,\n borderRadius = 0,\n font\n }) {\n this.extra = 0;\n this.geometry = geometry;\n this.gl = gl;\n this.image = image;\n this.index = index;\n this.length = length;\n this.renderer = renderer;\n this.scene = scene;\n this.screen = screen;\n this.text = text;\n this.viewport = viewport;\n this.bend = bend;\n this.textColor = textColor;\n this.borderRadius = borderRadius;\n this.font = font;\n this.createShader();\n this.createMesh();\n this.createTitle();\n this.onResize();\n }\n createShader() {\n const texture = new Texture(this.gl, {\n generateMipmaps: true\n });\n this.program = new Program(this.gl, {\n depthTest: false,\n depthWrite: false,\n vertex: `\n precision highp float;\n attribute vec3 position;\n attribute vec2 uv;\n uniform mat4 modelViewMatrix;\n uniform mat4 projectionMatrix;\n uniform float uTime;\n uniform float uSpeed;\n varying vec2 vUv;\n void main() {\n vUv = uv;\n vec3 p = position;\n p.z = (sin(p.x * 4.0 + uTime) * 1.5 + cos(p.y * 2.0 + uTime) * 1.5) * (0.1 + uSpeed * 0.5);\n gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);\n }\n `,\n fragment: `\n precision highp float;\n uniform vec2 uImageSizes;\n uniform vec2 uPlaneSizes;\n uniform sampler2D tMap;\n uniform float uBorderRadius;\n varying vec2 vUv;\n \n float roundedBoxSDF(vec2 p, vec2 b, float r) {\n vec2 d = abs(p) - b;\n return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0) - r;\n }\n \n void main() {\n vec2 ratio = vec2(\n min((uPlaneSizes.x / uPlaneSizes.y) / (uImageSizes.x / uImageSizes.y), 1.0),\n min((uPlaneSizes.y / uPlaneSizes.x) / (uImageSizes.y / uImageSizes.x), 1.0)\n );\n vec2 uv = vec2(\n vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,\n vUv.y * ratio.y + (1.0 - ratio.y) * 0.5\n );\n vec4 color = texture2D(tMap, uv);\n \n float d = roundedBoxSDF(vUv - 0.5, vec2(0.5 - uBorderRadius), uBorderRadius);\n \n // Smooth antialiasing for edges\n float edgeSmooth = 0.002;\n float alpha = 1.0 - smoothstep(-edgeSmooth, edgeSmooth, d);\n \n gl_FragColor = vec4(color.rgb, alpha);\n }\n `,\n uniforms: {\n tMap: { value: texture },\n uPlaneSizes: { value: [0, 0] },\n uImageSizes: { value: [0, 0] },\n uSpeed: { value: 0 },\n uTime: { value: 100 * Math.random() },\n uBorderRadius: { value: this.borderRadius }\n },\n transparent: true\n });\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.src = this.image;\n img.onload = () => {\n texture.image = img;\n this.program.uniforms.uImageSizes.value = [img.naturalWidth, img.naturalHeight];\n };\n }\n createMesh() {\n this.plane = new Mesh(this.gl, {\n geometry: this.geometry,\n program: this.program\n });\n this.plane.setParent(this.scene);\n }\n createTitle() {\n this.title = new Title({\n gl: this.gl,\n plane: this.plane,\n renderer: this.renderer,\n text: this.text,\n textColor: this.textColor,\n font: this.font\n });\n }\n update(scroll, direction) {\n this.plane.position.x = this.x - scroll.current - this.extra;\n\n const x = this.plane.position.x;\n const H = this.viewport.width / 2;\n\n if (this.bend === 0) {\n this.plane.position.y = 0;\n this.plane.rotation.z = 0;\n } else {\n const B_abs = Math.abs(this.bend);\n const R = (H * H + B_abs * B_abs) / (2 * B_abs);\n const effectiveX = Math.min(Math.abs(x), H);\n\n const arc = R - Math.sqrt(R * R - effectiveX * effectiveX);\n if (this.bend > 0) {\n this.plane.position.y = -arc;\n this.plane.rotation.z = -Math.sign(x) * Math.asin(effectiveX / R);\n } else {\n this.plane.position.y = arc;\n this.plane.rotation.z = Math.sign(x) * Math.asin(effectiveX / R);\n }\n }\n\n this.speed = scroll.current - scroll.last;\n this.program.uniforms.uTime.value += 0.04;\n this.program.uniforms.uSpeed.value = this.speed;\n\n const planeOffset = this.plane.scale.x / 2;\n const viewportOffset = this.viewport.width / 2;\n this.isBefore = this.plane.position.x + planeOffset < -viewportOffset;\n this.isAfter = this.plane.position.x - planeOffset > viewportOffset;\n if (direction === 'right' && this.isBefore) {\n this.extra -= this.widthTotal;\n this.isBefore = this.isAfter = false;\n }\n if (direction === 'left' && this.isAfter) {\n this.extra += this.widthTotal;\n this.isBefore = this.isAfter = false;\n }\n }\n onResize({ screen, viewport } = {}) {\n if (screen) this.screen = screen;\n if (viewport) {\n this.viewport = viewport;\n if (this.plane.program.uniforms.uViewportSizes) {\n this.plane.program.uniforms.uViewportSizes.value = [this.viewport.width, this.viewport.height];\n }\n }\n this.scale = this.screen.height / 1500;\n this.plane.scale.y = (this.viewport.height * (900 * this.scale)) / this.screen.height;\n this.plane.scale.x = (this.viewport.width * (700 * this.scale)) / this.screen.width;\n this.plane.program.uniforms.uPlaneSizes.value = [this.plane.scale.x, this.plane.scale.y];\n this.padding = 2;\n this.width = this.plane.scale.x + this.padding;\n this.widthTotal = this.width * this.length;\n this.x = this.width * this.index;\n }\n}\n\nclass App {\n constructor(\n container,\n {\n items,\n bend,\n textColor = '#ffffff',\n borderRadius = 0,\n font = 'bold 30px Figtree',\n scrollSpeed = 2,\n scrollEase = 0.05\n } = {}\n ) {\n document.documentElement.classList.remove('no-js');\n this.container = container;\n this.scrollSpeed = scrollSpeed;\n this.scroll = { ease: scrollEase, current: 0, target: 0, last: 0 };\n this.onCheckDebounce = debounce(this.onCheck, 200);\n this.createRenderer();\n this.createCamera();\n this.createScene();\n this.onResize();\n this.createGeometry();\n this.createMedias(items, bend, textColor, borderRadius, font);\n this.update();\n this.addEventListeners();\n }\n createRenderer() {\n this.renderer = new Renderer({\n alpha: true,\n antialias: true,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n this.gl = this.renderer.gl;\n this.gl.clearColor(0, 0, 0, 0);\n this.container.appendChild(this.gl.canvas);\n }\n createCamera() {\n this.camera = new Camera(this.gl);\n this.camera.fov = 45;\n this.camera.position.z = 20;\n }\n createScene() {\n this.scene = new Transform();\n }\n createGeometry() {\n this.planeGeometry = new Plane(this.gl, {\n heightSegments: 50,\n widthSegments: 100\n });\n }\n createMedias(items, bend = 1, textColor, borderRadius, font) {\n const defaultItems = [\n { image: `https://picsum.photos/seed/1/800/600?grayscale`, text: 'Bridge' },\n { image: `https://picsum.photos/seed/2/800/600?grayscale`, text: 'Desk Setup' },\n { image: `https://picsum.photos/seed/3/800/600?grayscale`, text: 'Waterfall' },\n { image: `https://picsum.photos/seed/4/800/600?grayscale`, text: 'Strawberries' },\n { image: `https://picsum.photos/seed/5/800/600?grayscale`, text: 'Deep Diving' },\n { image: `https://picsum.photos/seed/16/800/600?grayscale`, text: 'Train Track' },\n { image: `https://picsum.photos/seed/17/800/600?grayscale`, text: 'Santorini' },\n { image: `https://picsum.photos/seed/8/800/600?grayscale`, text: 'Blurry Lights' },\n { image: `https://picsum.photos/seed/9/800/600?grayscale`, text: 'New York' },\n { image: `https://picsum.photos/seed/10/800/600?grayscale`, text: 'Good Boy' },\n { image: `https://picsum.photos/seed/21/800/600?grayscale`, text: 'Coastline' },\n { image: `https://picsum.photos/seed/12/800/600?grayscale`, text: 'Palm Trees' }\n ];\n const galleryItems = items && items.length ? items : defaultItems;\n this.mediasImages = galleryItems.concat(galleryItems);\n this.medias = this.mediasImages.map((data, index) => {\n return new Media({\n geometry: this.planeGeometry,\n gl: this.gl,\n image: data.image,\n index,\n length: this.mediasImages.length,\n renderer: this.renderer,\n scene: this.scene,\n screen: this.screen,\n text: data.text,\n viewport: this.viewport,\n bend,\n textColor,\n borderRadius,\n font\n });\n });\n }\n onTouchDown(e) {\n this.isDown = true;\n this.scroll.position = this.scroll.current;\n this.start = e.touches ? e.touches[0].clientX : e.clientX;\n }\n onTouchMove(e) {\n if (!this.isDown) return;\n const x = e.touches ? e.touches[0].clientX : e.clientX;\n const distance = (this.start - x) * (this.scrollSpeed * 0.025);\n this.scroll.target = this.scroll.position + distance;\n }\n onTouchUp() {\n this.isDown = false;\n this.onCheck();\n }\n onWheel(e) {\n const delta = e.deltaY || e.wheelDelta || e.detail;\n this.scroll.target += (delta > 0 ? this.scrollSpeed : -this.scrollSpeed) * 0.2;\n this.onCheckDebounce();\n }\n onKeyDown(e) {\n switch (e.key) {\n case 'ArrowRight':\n e.preventDefault();\n this.scroll.target += this.scrollSpeed * 5;\n this.onCheckDebounce();\n break;\n\n case 'ArrowLeft':\n e.preventDefault();\n this.scroll.target -= this.scrollSpeed * 5;\n this.onCheckDebounce();\n break;\n\n case 'Home':\n e.preventDefault();\n this.scroll.target = 0;\n this.onCheckDebounce();\n break;\n\n default:\n break;\n }\n }\n\n onCheck() {\n if (!this.medias || !this.medias[0]) return;\n const width = this.medias[0].width;\n const itemIndex = Math.round(Math.abs(this.scroll.target) / width);\n const item = width * itemIndex;\n this.scroll.target = this.scroll.target < 0 ? -item : item;\n }\n onResize() {\n this.screen = {\n width: this.container.clientWidth,\n height: this.container.clientHeight\n };\n this.renderer.setSize(this.screen.width, this.screen.height);\n this.camera.perspective({\n aspect: this.screen.width / this.screen.height\n });\n const fov = (this.camera.fov * Math.PI) / 180;\n const height = 2 * Math.tan(fov / 2) * this.camera.position.z;\n const width = height * this.camera.aspect;\n this.viewport = { width, height };\n if (this.medias) {\n this.medias.forEach(media => media.onResize({ screen: this.screen, viewport: this.viewport }));\n }\n }\n update() {\n this.scroll.current = lerp(this.scroll.current, this.scroll.target, this.scroll.ease);\n const direction = this.scroll.current > this.scroll.last ? 'right' : 'left';\n if (this.medias) {\n this.medias.forEach(media => media.update(this.scroll, direction));\n }\n this.renderer.render({ scene: this.scene, camera: this.camera });\n this.scroll.last = this.scroll.current;\n this.raf = window.requestAnimationFrame(this.update.bind(this));\n }\n addEventListeners() {\n this.boundOnResize = this.onResize.bind(this);\n this.boundOnWheel = this.onWheel.bind(this);\n this.boundOnTouchDown = this.onTouchDown.bind(this);\n this.boundOnTouchMove = this.onTouchMove.bind(this);\n this.boundOnTouchUp = this.onTouchUp.bind(this);\n this.boundOnKeyDown = this.onKeyDown.bind(this);\n\n window.addEventListener('resize', this.boundOnResize);\n window.addEventListener('mousewheel', this.boundOnWheel);\n window.addEventListener('wheel', this.boundOnWheel);\n window.addEventListener('mousedown', this.boundOnTouchDown);\n window.addEventListener('mousemove', this.boundOnTouchMove);\n window.addEventListener('mouseup', this.boundOnTouchUp);\n window.addEventListener('touchstart', this.boundOnTouchDown);\n window.addEventListener('touchmove', this.boundOnTouchMove);\n window.addEventListener('touchend', this.boundOnTouchUp);\n\n this.container?.addEventListener('keydown', this.boundOnKeyDown);\n }\n destroy() {\n window.cancelAnimationFrame(this.raf);\n window.removeEventListener('resize', this.boundOnResize);\n window.removeEventListener('mousewheel', this.boundOnWheel);\n window.removeEventListener('wheel', this.boundOnWheel);\n window.removeEventListener('mousedown', this.boundOnTouchDown);\n window.removeEventListener('mousemove', this.boundOnTouchMove);\n window.removeEventListener('mouseup', this.boundOnTouchUp);\n window.removeEventListener('touchstart', this.boundOnTouchDown);\n window.removeEventListener('touchmove', this.boundOnTouchMove);\n window.removeEventListener('touchend', this.boundOnTouchUp);\n if (this.renderer && this.renderer.gl && this.renderer.gl.canvas.parentNode) {\n this.renderer.gl.canvas.parentNode.removeChild(this.renderer.gl.canvas);\n }\n\n if (this.container) {\n this.container.removeEventListener('keydown', this.boundOnKeyDown);\n }\n }\n}\n\nexport default function CircularGallery({\n items,\n bend = 3,\n textColor = '#ffffff',\n borderRadius = 0.05,\n font = 'bold 30px Figtree',\n fontUrl,\n scrollSpeed = 2,\n scrollEase = 0.05\n}) {\n const containerRef = useRef(null);\n useEffect(() => {\n if (!containerRef.current) return;\n let app;\n let isMounted = true;\n resolveFont(font, fontUrl).then(resolvedFont => {\n if (!isMounted || !containerRef.current) return;\n app = new App(containerRef.current, {\n items,\n bend,\n textColor,\n borderRadius,\n font: resolvedFont,\n scrollSpeed,\n scrollEase\n });\n });\n\n return () => {\n isMounted = false;\n if (app) app.destroy();\n };\n }, [items, bend, textColor, borderRadius, font, fontUrl, scrollSpeed, scrollEase]);\n return (\n \n );\n}\n" } ], diff --git a/public/r/CircularGallery-TS-CSS.json b/public/r/CircularGallery-TS-CSS.json index 8d14d1ae..e8ef0464 100644 --- a/public/r/CircularGallery-TS-CSS.json +++ b/public/r/CircularGallery-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "CircularGallery/CircularGallery.css", + "type": "registry:file", + "path": "CircularGallery.css", + "target": "@components/CircularGallery.css", "content": ".circular-gallery {\n width: 100%;\n height: 100%;\n overflow: hidden;\n cursor: grab;\n}\n\n.circular-gallery:active {\n cursor: grabbing;\n}\n\n.circular-gallery:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 4px;\n}\n" }, { "type": "registry:component", - "path": "CircularGallery/CircularGallery.tsx", + "path": "CircularGallery.tsx", "content": "import { Camera, Mesh, Plane, Program, Renderer, Texture, Transform } from 'ogl';\nimport { useEffect, useRef } from 'react';\n\nimport './CircularGallery.css';\n\ntype GL = Renderer['gl'];\n\nfunction debounce void>(func: T, wait: number) {\n let timeout: number;\n return function (this: any, ...args: Parameters) {\n window.clearTimeout(timeout);\n timeout = window.setTimeout(() => func.apply(this, args), wait);\n };\n}\n\nfunction lerp(p1: number, p2: number, t: number): number {\n return p1 + (p2 - p1) * t;\n}\n\nfunction autoBind(instance: any): void {\n const proto = Object.getPrototypeOf(instance);\n Object.getOwnPropertyNames(proto).forEach(key => {\n if (key !== 'constructor' && typeof instance[key] === 'function') {\n instance[key] = instance[key].bind(instance);\n }\n });\n}\n\nconst DEFAULT_FONT = 'bold 30px Figtree';\n// Figtree is not guaranteed to be available on the host page, so the component\n// loads it on demand whenever the default font is used.\nconst DEFAULT_FONT_URL = 'https://fonts.googleapis.com/css2?family=Figtree:wght@400;700&display=swap';\n\nfunction deriveFontFamilyFromUrl(url: string): string {\n const fileName = (url.split('/').pop() || 'custom-font').split('?')[0];\n const base = fileName.replace(/\\.(woff2?|ttf|otf|eot)$/i, '');\n return base.replace(/[^a-zA-Z0-9-_ ]/g, '').trim() || 'CircularGalleryFont';\n}\n\nasync function loadFontFromStylesheet(url: string): Promise {\n const response = await fetch(url);\n if (!response.ok) throw new Error(`Failed to fetch font stylesheet (${response.status})`);\n const cssText = await response.text();\n const faceBlocks = cssText.match(/@font-face\\s*{[^}]*}/g) || [];\n let family: string | null = null;\n const fontFaces: FontFace[] = [];\n for (const block of faceBlocks) {\n const familyMatch = block.match(/font-family:\\s*['\"]?([^;'\"]+)['\"]?/);\n const urlMatch = block.match(/url\\(\\s*['\"]?([^'\")]+)['\"]?\\s*\\)/);\n if (!familyMatch || !urlMatch) continue;\n family = familyMatch[1].trim();\n const descriptors: FontFaceDescriptors = {};\n const weightMatch = block.match(/font-weight:\\s*([^;]+);/);\n const styleMatch = block.match(/font-style:\\s*([^;]+);/);\n const rangeMatch = block.match(/unicode-range:\\s*([^;]+);/);\n if (weightMatch) descriptors.weight = weightMatch[1].trim();\n if (styleMatch) descriptors.style = styleMatch[1].trim();\n if (rangeMatch) descriptors.unicodeRange = rangeMatch[1].trim();\n fontFaces.push(new FontFace(family, `url(${urlMatch[1]})`, descriptors));\n }\n if (!family) throw new Error('No @font-face rule found in the stylesheet');\n await Promise.allSettled(\n fontFaces.map(async face => {\n await face.load();\n document.fonts.add(face);\n })\n );\n return family;\n}\n\nasync function loadFontFromFile(url: string): Promise {\n const family = deriveFontFamilyFromUrl(url);\n const fontFace = new FontFace(family, `url(${url})`);\n await fontFace.load();\n document.fonts.add(fontFace);\n return family;\n}\n\nasync function loadCustomFont(fontUrl: string): Promise {\n const isStylesheet = fontUrl.includes('fonts.googleapis.com') || /\\.css(\\?.*)?$/i.test(fontUrl);\n return isStylesheet ? loadFontFromStylesheet(fontUrl) : loadFontFromFile(fontUrl);\n}\n\n// Loads `fontUrl` (a stylesheet such as a Google Fonts URL, or a direct font\n// file) and returns a canvas-ready font string that keeps the size/weight from\n// `font` but swaps in the freshly loaded family. Falls back to `font` on error.\nasync function resolveFont(font: string, fontUrl?: string): Promise {\n // Use the bundled Figtree stylesheet when the caller relies on the default\n // font, otherwise honor the explicit `fontUrl`.\n const effectiveUrl = fontUrl || (font === DEFAULT_FONT ? DEFAULT_FONT_URL : null);\n if (!effectiveUrl) {\n // A custom family was supplied without a URL – make sure it is ready (in\n // case the host page declares it) before we draw it to the canvas,\n // otherwise the first paint silently falls back to a system font.\n if (document.fonts && document.fonts.load) {\n try {\n await document.fonts.load(font);\n await document.fonts.ready;\n } catch {\n // Ignore – fall back to whatever the browser provides.\n }\n }\n return font;\n }\n try {\n const family = await loadCustomFont(effectiveUrl);\n const sizeMatch = font.match(/^\\s*(.*?\\d+px)/);\n const prefix = sizeMatch ? sizeMatch[1].trim() : 'bold 30px';\n const resolved = `${prefix} \"${family}\"`;\n if (document.fonts && document.fonts.load) {\n try {\n await document.fonts.load(resolved);\n } catch {\n // Ignore – we still attempt to render with the requested font.\n }\n }\n return resolved;\n } catch (error) {\n console.error('CircularGallery: unable to load font from', fontUrl, error);\n return font;\n }\n}\n\nfunction getFontSize(font: string): number {\n const match = font.match(/(\\d+)px/);\n return match ? parseInt(match[1], 10) : 30;\n}\n\nfunction createTextTexture(\n gl: GL,\n text: string,\n font: string = 'bold 30px monospace',\n color: string = 'black'\n): { texture: Texture; width: number; height: number } {\n const canvas = document.createElement('canvas');\n const context = canvas.getContext('2d');\n if (!context) throw new Error('Could not get 2d context');\n\n context.font = font;\n const metrics = context.measureText(text);\n const textWidth = Math.ceil(metrics.width);\n const fontSize = getFontSize(font);\n const textHeight = Math.ceil(fontSize * 1.2);\n\n canvas.width = textWidth + 20;\n canvas.height = textHeight + 20;\n\n context.font = font;\n context.fillStyle = color;\n context.textBaseline = 'middle';\n context.textAlign = 'center';\n context.clearRect(0, 0, canvas.width, canvas.height);\n context.fillText(text, canvas.width / 2, canvas.height / 2);\n\n const texture = new Texture(gl, { generateMipmaps: false });\n texture.image = canvas;\n return { texture, width: canvas.width, height: canvas.height };\n}\n\ninterface TitleProps {\n gl: GL;\n plane: Mesh;\n renderer: Renderer;\n text: string;\n textColor?: string;\n font?: string;\n}\n\nclass Title {\n gl: GL;\n plane: Mesh;\n renderer: Renderer;\n text: string;\n textColor: string;\n font: string;\n mesh!: Mesh;\n\n constructor({ gl, plane, renderer, text, textColor = '#545050', font = '30px sans-serif' }: TitleProps) {\n autoBind(this);\n this.gl = gl;\n this.plane = plane;\n this.renderer = renderer;\n this.text = text;\n this.textColor = textColor;\n this.font = font;\n this.createMesh();\n }\n\n createMesh() {\n const { texture, width, height } = createTextTexture(this.gl, this.text, this.font, this.textColor);\n const geometry = new Plane(this.gl);\n const program = new Program(this.gl, {\n vertex: `\n attribute vec3 position;\n attribute vec2 uv;\n uniform mat4 modelViewMatrix;\n uniform mat4 projectionMatrix;\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragment: `\n precision highp float;\n uniform sampler2D tMap;\n varying vec2 vUv;\n void main() {\n vec4 color = texture2D(tMap, vUv);\n if (color.a < 0.1) discard;\n gl_FragColor = color;\n }\n `,\n uniforms: { tMap: { value: texture } },\n transparent: true\n });\n this.mesh = new Mesh(this.gl, { geometry, program });\n const aspect = width / height;\n const textHeightScaled = this.plane.scale.y * 0.15;\n const textWidthScaled = textHeightScaled * aspect;\n this.mesh.scale.set(textWidthScaled, textHeightScaled, 1);\n this.mesh.position.y = -this.plane.scale.y * 0.5 - textHeightScaled * 0.5 - 0.05;\n this.mesh.setParent(this.plane);\n }\n}\n\ninterface ScreenSize {\n width: number;\n height: number;\n}\n\ninterface Viewport {\n width: number;\n height: number;\n}\n\ninterface MediaProps {\n geometry: Plane;\n gl: GL;\n image: string;\n index: number;\n length: number;\n renderer: Renderer;\n scene: Transform;\n screen: ScreenSize;\n text: string;\n viewport: Viewport;\n bend: number;\n textColor: string;\n borderRadius?: number;\n font?: string;\n}\n\nclass Media {\n extra: number = 0;\n geometry: Plane;\n gl: GL;\n image: string;\n index: number;\n length: number;\n renderer: Renderer;\n scene: Transform;\n screen: ScreenSize;\n text: string;\n viewport: Viewport;\n bend: number;\n textColor: string;\n borderRadius: number;\n font?: string;\n program!: Program;\n plane!: Mesh;\n title!: Title;\n scale!: number;\n padding!: number;\n width!: number;\n widthTotal!: number;\n x!: number;\n speed: number = 0;\n isBefore: boolean = false;\n isAfter: boolean = false;\n\n constructor({\n geometry,\n gl,\n image,\n index,\n length,\n renderer,\n scene,\n screen,\n text,\n viewport,\n bend,\n textColor,\n borderRadius = 0,\n font\n }: MediaProps) {\n this.geometry = geometry;\n this.gl = gl;\n this.image = image;\n this.index = index;\n this.length = length;\n this.renderer = renderer;\n this.scene = scene;\n this.screen = screen;\n this.text = text;\n this.viewport = viewport;\n this.bend = bend;\n this.textColor = textColor;\n this.borderRadius = borderRadius;\n this.font = font;\n this.createShader();\n this.createMesh();\n this.createTitle();\n this.onResize();\n }\n\n createShader() {\n const texture = new Texture(this.gl, {\n generateMipmaps: true\n });\n this.program = new Program(this.gl, {\n depthTest: false,\n depthWrite: false,\n vertex: `\n precision highp float;\n attribute vec3 position;\n attribute vec2 uv;\n uniform mat4 modelViewMatrix;\n uniform mat4 projectionMatrix;\n uniform float uTime;\n uniform float uSpeed;\n varying vec2 vUv;\n void main() {\n vUv = uv;\n vec3 p = position;\n p.z = (sin(p.x * 4.0 + uTime) * 1.5 + cos(p.y * 2.0 + uTime) * 1.5) * (0.1 + uSpeed * 0.5);\n gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);\n }\n `,\n fragment: `\n precision highp float;\n uniform vec2 uImageSizes;\n uniform vec2 uPlaneSizes;\n uniform sampler2D tMap;\n uniform float uBorderRadius;\n varying vec2 vUv;\n \n float roundedBoxSDF(vec2 p, vec2 b, float r) {\n vec2 d = abs(p) - b;\n return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0) - r;\n }\n \n void main() {\n vec2 ratio = vec2(\n min((uPlaneSizes.x / uPlaneSizes.y) / (uImageSizes.x / uImageSizes.y), 1.0),\n min((uPlaneSizes.y / uPlaneSizes.x) / (uImageSizes.y / uImageSizes.x), 1.0)\n );\n vec2 uv = vec2(\n vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,\n vUv.y * ratio.y + (1.0 - ratio.y) * 0.5\n );\n vec4 color = texture2D(tMap, uv);\n \n float d = roundedBoxSDF(vUv - 0.5, vec2(0.5 - uBorderRadius), uBorderRadius);\n \n float edgeSmooth = 0.002;\n float alpha = 1.0 - smoothstep(-edgeSmooth, edgeSmooth, d);\n \n gl_FragColor = vec4(color.rgb, alpha);\n }\n `,\n uniforms: {\n tMap: { value: texture },\n uPlaneSizes: { value: [0, 0] },\n uImageSizes: { value: [0, 0] },\n uSpeed: { value: 0 },\n uTime: { value: 100 * Math.random() },\n uBorderRadius: { value: this.borderRadius }\n },\n transparent: true\n });\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.src = this.image;\n img.onload = () => {\n texture.image = img;\n this.program.uniforms.uImageSizes.value = [img.naturalWidth, img.naturalHeight];\n };\n }\n\n createMesh() {\n this.plane = new Mesh(this.gl, {\n geometry: this.geometry,\n program: this.program\n });\n this.plane.setParent(this.scene);\n }\n\n createTitle() {\n this.title = new Title({\n gl: this.gl,\n plane: this.plane,\n renderer: this.renderer,\n text: this.text,\n textColor: this.textColor,\n font: this.font\n });\n }\n\n update(scroll: { current: number; last: number }, direction: 'right' | 'left') {\n this.plane.position.x = this.x - scroll.current - this.extra;\n\n const x = this.plane.position.x;\n const H = this.viewport.width / 2;\n\n if (this.bend === 0) {\n this.plane.position.y = 0;\n this.plane.rotation.z = 0;\n } else {\n const B_abs = Math.abs(this.bend);\n const R = (H * H + B_abs * B_abs) / (2 * B_abs);\n const effectiveX = Math.min(Math.abs(x), H);\n\n const arc = R - Math.sqrt(R * R - effectiveX * effectiveX);\n if (this.bend > 0) {\n this.plane.position.y = -arc;\n this.plane.rotation.z = -Math.sign(x) * Math.asin(effectiveX / R);\n } else {\n this.plane.position.y = arc;\n this.plane.rotation.z = Math.sign(x) * Math.asin(effectiveX / R);\n }\n }\n\n this.speed = scroll.current - scroll.last;\n this.program.uniforms.uTime.value += 0.04;\n this.program.uniforms.uSpeed.value = this.speed;\n\n const planeOffset = this.plane.scale.x / 2;\n const viewportOffset = this.viewport.width / 2;\n this.isBefore = this.plane.position.x + planeOffset < -viewportOffset;\n this.isAfter = this.plane.position.x - planeOffset > viewportOffset;\n if (direction === 'right' && this.isBefore) {\n this.extra -= this.widthTotal;\n this.isBefore = this.isAfter = false;\n }\n if (direction === 'left' && this.isAfter) {\n this.extra += this.widthTotal;\n this.isBefore = this.isAfter = false;\n }\n }\n\n onResize({ screen, viewport }: { screen?: ScreenSize; viewport?: Viewport } = {}) {\n if (screen) this.screen = screen;\n if (viewport) {\n this.viewport = viewport;\n if (this.plane.program.uniforms.uViewportSizes) {\n this.plane.program.uniforms.uViewportSizes.value = [this.viewport.width, this.viewport.height];\n }\n }\n this.scale = this.screen.height / 1500;\n this.plane.scale.y = (this.viewport.height * (900 * this.scale)) / this.screen.height;\n this.plane.scale.x = (this.viewport.width * (700 * this.scale)) / this.screen.width;\n this.plane.program.uniforms.uPlaneSizes.value = [this.plane.scale.x, this.plane.scale.y];\n this.padding = 2;\n this.width = this.plane.scale.x + this.padding;\n this.widthTotal = this.width * this.length;\n this.x = this.width * this.index;\n }\n}\n\ninterface AppConfig {\n items?: { image: string; text: string }[];\n bend?: number;\n textColor?: string;\n borderRadius?: number;\n font?: string;\n scrollSpeed?: number;\n scrollEase?: number;\n}\n\nclass App {\n container: HTMLElement;\n scrollSpeed: number;\n scroll: {\n ease: number;\n current: number;\n target: number;\n last: number;\n position?: number;\n };\n onCheckDebounce: (...args: any[]) => void;\n renderer!: Renderer;\n gl!: GL;\n camera!: Camera;\n scene!: Transform;\n planeGeometry!: Plane;\n medias: Media[] = [];\n mediasImages: { image: string; text: string }[] = [];\n screen!: { width: number; height: number };\n viewport!: { width: number; height: number };\n raf: number = 0;\n\n boundOnResize!: () => void;\n boundOnWheel!: (e: Event) => void;\n boundOnTouchDown!: (e: MouseEvent | TouchEvent) => void;\n boundOnTouchMove!: (e: MouseEvent | TouchEvent) => void;\n boundOnTouchUp!: () => void;\n boundOnKeyDown!: (e: KeyboardEvent) => void;\n\n isDown: boolean = false;\n start: number = 0;\n\n constructor(\n container: HTMLElement,\n {\n items,\n bend = 1,\n textColor = '#ffffff',\n borderRadius = 0,\n font = 'bold 30px Figtree',\n scrollSpeed = 2,\n scrollEase = 0.05\n }: AppConfig\n ) {\n document.documentElement.classList.remove('no-js');\n this.container = container;\n this.scrollSpeed = scrollSpeed;\n this.scroll = { ease: scrollEase, current: 0, target: 0, last: 0 };\n this.onCheckDebounce = debounce(this.onCheck.bind(this), 200);\n this.createRenderer();\n this.createCamera();\n this.createScene();\n this.onResize();\n this.createGeometry();\n this.createMedias(items, bend, textColor, borderRadius, font);\n this.update();\n this.addEventListeners();\n }\n\n createRenderer() {\n this.renderer = new Renderer({\n alpha: true,\n antialias: true,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n this.gl = this.renderer.gl;\n this.gl.clearColor(0, 0, 0, 0);\n this.container.appendChild(this.renderer.gl.canvas as HTMLCanvasElement);\n }\n\n createCamera() {\n this.camera = new Camera(this.gl);\n this.camera.fov = 45;\n this.camera.position.z = 20;\n }\n\n createScene() {\n this.scene = new Transform();\n }\n\n createGeometry() {\n this.planeGeometry = new Plane(this.gl, {\n heightSegments: 50,\n widthSegments: 100\n });\n }\n\n createMedias(\n items: { image: string; text: string }[] | undefined,\n bend: number = 1,\n textColor: string,\n borderRadius: number,\n font: string\n ) {\n const defaultItems = [\n {\n image: `https://picsum.photos/seed/1/800/600?grayscale`,\n text: 'Bridge'\n },\n {\n image: `https://picsum.photos/seed/2/800/600?grayscale`,\n text: 'Desk Setup'\n },\n {\n image: `https://picsum.photos/seed/3/800/600?grayscale`,\n text: 'Waterfall'\n },\n {\n image: `https://picsum.photos/seed/4/800/600?grayscale`,\n text: 'Strawberries'\n },\n {\n image: `https://picsum.photos/seed/5/800/600?grayscale`,\n text: 'Deep Diving'\n },\n {\n image: `https://picsum.photos/seed/16/800/600?grayscale`,\n text: 'Train Track'\n },\n {\n image: `https://picsum.photos/seed/17/800/600?grayscale`,\n text: 'Santorini'\n },\n {\n image: `https://picsum.photos/seed/8/800/600?grayscale`,\n text: 'Blurry Lights'\n },\n {\n image: `https://picsum.photos/seed/9/800/600?grayscale`,\n text: 'New York'\n },\n {\n image: `https://picsum.photos/seed/10/800/600?grayscale`,\n text: 'Good Boy'\n },\n {\n image: `https://picsum.photos/seed/21/800/600?grayscale`,\n text: 'Coastline'\n },\n {\n image: `https://picsum.photos/seed/12/800/600?grayscale`,\n text: 'Palm Trees'\n }\n ];\n const galleryItems = items && items.length ? items : defaultItems;\n this.mediasImages = galleryItems.concat(galleryItems);\n this.medias = this.mediasImages.map((data, index) => {\n return new Media({\n geometry: this.planeGeometry,\n gl: this.gl,\n image: data.image,\n index,\n length: this.mediasImages.length,\n renderer: this.renderer,\n scene: this.scene,\n screen: this.screen,\n text: data.text,\n viewport: this.viewport,\n bend,\n textColor,\n borderRadius,\n font\n });\n });\n }\n\n onTouchDown(e: MouseEvent | TouchEvent) {\n this.isDown = true;\n this.scroll.position = this.scroll.current;\n this.start = 'touches' in e ? e.touches[0].clientX : e.clientX;\n }\n\n onTouchMove(e: MouseEvent | TouchEvent) {\n if (!this.isDown) return;\n const x = 'touches' in e ? e.touches[0].clientX : e.clientX;\n const distance = (this.start - x) * (this.scrollSpeed * 0.025);\n this.scroll.target = (this.scroll.position ?? 0) + distance;\n }\n\n onTouchUp() {\n this.isDown = false;\n this.onCheck();\n }\n\n onWheel(e: Event) {\n const wheelEvent = e as WheelEvent;\n const delta = wheelEvent.deltaY || (wheelEvent as any).wheelDelta || (wheelEvent as any).detail;\n this.scroll.target += (delta > 0 ? this.scrollSpeed : -this.scrollSpeed) * 0.2;\n this.onCheckDebounce();\n }\n\n onKeyDown(e: KeyboardEvent) {\n switch (e.key) {\n case 'ArrowRight':\n e.preventDefault();\n this.scroll.target += this.scrollSpeed * 5;\n this.onCheckDebounce();\n break;\n\n case 'ArrowLeft':\n e.preventDefault();\n this.scroll.target -= this.scrollSpeed * 5;\n this.onCheckDebounce();\n break;\n\n default:\n break;\n }\n }\n\n onCheck() {\n if (!this.medias || !this.medias[0]) return;\n const width = this.medias[0].width;\n const itemIndex = Math.round(Math.abs(this.scroll.target) / width);\n const item = width * itemIndex;\n this.scroll.target = this.scroll.target < 0 ? -item : item;\n }\n\n onResize() {\n this.screen = {\n width: this.container.clientWidth,\n height: this.container.clientHeight\n };\n this.renderer.setSize(this.screen.width, this.screen.height);\n this.camera.perspective({\n aspect: this.screen.width / this.screen.height\n });\n const fov = (this.camera.fov * Math.PI) / 180;\n const height = 2 * Math.tan(fov / 2) * this.camera.position.z;\n const width = height * this.camera.aspect;\n this.viewport = { width, height };\n if (this.medias) {\n this.medias.forEach(media => media.onResize({ screen: this.screen, viewport: this.viewport }));\n }\n }\n\n update() {\n this.scroll.current = lerp(this.scroll.current, this.scroll.target, this.scroll.ease);\n const direction = this.scroll.current > this.scroll.last ? 'right' : 'left';\n if (this.medias) {\n this.medias.forEach(media => media.update(this.scroll, direction));\n }\n this.renderer.render({ scene: this.scene, camera: this.camera });\n this.scroll.last = this.scroll.current;\n this.raf = window.requestAnimationFrame(this.update.bind(this));\n }\n\n addEventListeners() {\n this.boundOnResize = this.onResize.bind(this);\n this.boundOnWheel = this.onWheel.bind(this);\n this.boundOnTouchDown = this.onTouchDown.bind(this);\n this.boundOnTouchMove = this.onTouchMove.bind(this);\n this.boundOnTouchUp = this.onTouchUp.bind(this);\n this.boundOnKeyDown = this.onKeyDown.bind(this);\n\n window.addEventListener('resize', this.boundOnResize);\n window.addEventListener('mousewheel', this.boundOnWheel);\n window.addEventListener('wheel', this.boundOnWheel);\n window.addEventListener('mousedown', this.boundOnTouchDown);\n window.addEventListener('mousemove', this.boundOnTouchMove);\n window.addEventListener('mouseup', this.boundOnTouchUp);\n window.addEventListener('touchstart', this.boundOnTouchDown);\n window.addEventListener('touchmove', this.boundOnTouchMove);\n window.addEventListener('touchend', this.boundOnTouchUp);\n\n this.container?.addEventListener('keydown', this.boundOnKeyDown);\n }\n\n destroy() {\n window.cancelAnimationFrame(this.raf);\n window.removeEventListener('resize', this.boundOnResize);\n window.removeEventListener('mousewheel', this.boundOnWheel);\n window.removeEventListener('wheel', this.boundOnWheel);\n window.removeEventListener('mousedown', this.boundOnTouchDown);\n window.removeEventListener('mousemove', this.boundOnTouchMove);\n window.removeEventListener('mouseup', this.boundOnTouchUp);\n window.removeEventListener('touchstart', this.boundOnTouchDown);\n window.removeEventListener('touchmove', this.boundOnTouchMove);\n window.removeEventListener('touchend', this.boundOnTouchUp);\n if (this.renderer && this.renderer.gl && this.renderer.gl.canvas.parentNode) {\n this.renderer.gl.canvas.parentNode.removeChild(this.renderer.gl.canvas as HTMLCanvasElement);\n }\n if (this.container) {\n this.container.removeEventListener('keydown', this.boundOnKeyDown);\n }\n }\n}\n\ninterface CircularGalleryProps {\n items?: { image: string; text: string }[];\n bend?: number;\n textColor?: string;\n borderRadius?: number;\n font?: string;\n fontUrl?: string;\n scrollSpeed?: number;\n scrollEase?: number;\n}\n\nexport default function CircularGallery({\n items,\n bend = 3,\n textColor = '#ffffff',\n borderRadius = 0.05,\n font = 'bold 30px Figtree',\n fontUrl,\n scrollSpeed = 2,\n scrollEase = 0.05\n}: CircularGalleryProps) {\n const containerRef = useRef(null);\n useEffect(() => {\n if (!containerRef.current) return;\n let app: App | undefined;\n let isMounted = true;\n resolveFont(font, fontUrl).then(resolvedFont => {\n if (!isMounted || !containerRef.current) return;\n app = new App(containerRef.current, {\n items,\n bend,\n textColor,\n borderRadius,\n font: resolvedFont,\n scrollSpeed,\n scrollEase\n });\n });\n return () => {\n isMounted = false;\n if (app) app.destroy();\n };\n }, [items, bend, textColor, borderRadius, font, fontUrl, scrollSpeed, scrollEase]);\n return (\n \n );\n}\n" } ], diff --git a/public/r/CircularText-JS-CSS.json b/public/r/CircularText-JS-CSS.json index 3c6ae7bd..77406be5 100644 --- a/public/r/CircularText-JS-CSS.json +++ b/public/r/CircularText-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "CircularText/CircularText.css", + "type": "registry:file", + "path": "CircularText.css", + "target": "@components/CircularText.css", "content": ".circular-text {\n margin: 0 auto;\n border-radius: 50%;\n width: 200px;\n position: relative;\n height: 200px;\n font-weight: bold;\n color: #fff;\n font-weight: 900;\n text-align: center;\n cursor: pointer;\n transform-origin: 50% 50%;\n -webkit-transform-origin: 50% 50%;\n}\n\n.circular-text span {\n position: absolute;\n display: inline-block;\n left: 0;\n right: 0;\n top: 0;\n bottom: 0;\n font-size: 24px;\n transition: all 0.5s cubic-bezier(0, 0, 0, 1);\n}\n" }, { "type": "registry:component", - "path": "CircularText/CircularText.jsx", + "path": "CircularText.jsx", "content": "import { useEffect } from 'react';\nimport { motion, useAnimation, useMotionValue } from 'motion/react';\n\nimport './CircularText.css';\n\nconst getRotationTransition = (duration, from, loop = true) => ({\n from,\n to: from + 360,\n ease: 'linear',\n duration,\n type: 'tween',\n repeat: loop ? Infinity : 0\n});\n\nconst getTransition = (duration, from) => ({\n rotate: getRotationTransition(duration, from),\n scale: {\n type: 'spring',\n damping: 20,\n stiffness: 300\n }\n});\n\nconst CircularText = ({ text, spinDuration = 20, onHover = 'speedUp', className = '' }) => {\n const letters = Array.from(text);\n const controls = useAnimation();\n const rotation = useMotionValue(0);\n\n useEffect(() => {\n const start = rotation.get();\n controls.start({\n rotate: start + 360,\n scale: 1,\n transition: getTransition(spinDuration, start)\n });\n }, [spinDuration, text, onHover, controls, rotation]);\n\n const handleHoverStart = () => {\n const start = rotation.get();\n if (!onHover) return;\n\n let transitionConfig;\n let scaleVal = 1;\n\n switch (onHover) {\n case 'slowDown':\n transitionConfig = getTransition(spinDuration * 2, start);\n break;\n case 'speedUp':\n transitionConfig = getTransition(spinDuration / 4, start);\n break;\n case 'pause':\n transitionConfig = {\n rotate: { type: 'spring', damping: 20, stiffness: 300 },\n scale: { type: 'spring', damping: 20, stiffness: 300 }\n };\n scaleVal = 1;\n break;\n case 'goBonkers':\n transitionConfig = getTransition(spinDuration / 20, start);\n scaleVal = 0.8;\n break;\n default:\n transitionConfig = getTransition(spinDuration, start);\n }\n\n controls.start({\n rotate: start + 360,\n scale: scaleVal,\n transition: transitionConfig\n });\n };\n\n const handleHoverEnd = () => {\n const start = rotation.get();\n controls.start({\n rotate: start + 360,\n scale: 1,\n transition: getTransition(spinDuration, start)\n });\n };\n\n return (\n \n {letters.map((letter, i) => {\n const rotationDeg = (360 / letters.length) * i;\n const factor = Math.PI / letters.length;\n const x = factor * i;\n const y = factor * i;\n const transform = `rotateZ(${rotationDeg}deg) translate3d(${x}px, ${y}px, 0)`;\n\n return (\n \n {letter}\n \n );\n })}\n \n );\n};\n\nexport default CircularText;\n" } ], diff --git a/public/r/CircularText-TS-CSS.json b/public/r/CircularText-TS-CSS.json index f6b4b407..51380b08 100644 --- a/public/r/CircularText-TS-CSS.json +++ b/public/r/CircularText-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "CircularText/CircularText.css", + "type": "registry:file", + "path": "CircularText.css", + "target": "@components/CircularText.css", "content": ".circular-text {\n margin: 0 auto;\n border-radius: 50%;\n width: 200px;\n height: 200px;\n font-weight: bold;\n color: #fff;\n font-weight: 900;\n text-align: center;\n cursor: pointer;\n transform-origin: 50% 50%;\n -webkit-transform-origin: 50% 50%;\n}\n\n.circular-text span {\n position: absolute;\n display: inline-block;\n left: 0;\n right: 0;\n top: 0;\n bottom: 0;\n font-size: 24px;\n transition: all 0.5s cubic-bezier(0, 0, 0, 1);\n}\n" }, { "type": "registry:component", - "path": "CircularText/CircularText.tsx", - "content": "import React, { useEffect } from 'react';\nimport { motion, useAnimation, useMotionValue, MotionValue, Transition } from 'motion/react';\n\nimport './CircularText.css';\ninterface CircularTextProps {\n text: string;\n spinDuration?: number;\n onHover?: 'slowDown' | 'speedUp' | 'pause' | 'goBonkers';\n className?: string;\n}\n\nconst getRotationTransition = (duration: number, from: number, loop: boolean = true) => ({\n from,\n to: from + 360,\n ease: 'linear' as const,\n duration,\n type: 'tween' as const,\n repeat: loop ? Infinity : 0\n});\n\nconst getTransition = (duration: number, from: number) => ({\n rotate: getRotationTransition(duration, from),\n scale: {\n type: 'spring' as const,\n damping: 20,\n stiffness: 300\n }\n});\n\nconst CircularText: React.FC = ({\n text,\n spinDuration = 20,\n onHover = 'speedUp',\n className = ''\n}) => {\n const letters = Array.from(text);\n const controls = useAnimation();\n const rotation: MotionValue = useMotionValue(0);\n\n useEffect(() => {\n const start = rotation.get();\n controls.start({\n rotate: start + 360,\n scale: 1,\n transition: getTransition(spinDuration, start)\n });\n }, [spinDuration, text, onHover, controls]);\n\n const handleHoverStart = () => {\n const start = rotation.get();\n\n if (!onHover) return;\n\n let transitionConfig: ReturnType | Transition;\n let scaleVal = 1;\n\n switch (onHover) {\n case 'slowDown':\n transitionConfig = getTransition(spinDuration * 2, start);\n break;\n case 'speedUp':\n transitionConfig = getTransition(spinDuration / 4, start);\n break;\n case 'pause':\n transitionConfig = {\n rotate: { type: 'spring', damping: 20, stiffness: 300 },\n scale: { type: 'spring', damping: 20, stiffness: 300 }\n };\n break;\n case 'goBonkers':\n transitionConfig = getTransition(spinDuration / 20, start);\n scaleVal = 0.8;\n break;\n default:\n transitionConfig = getTransition(spinDuration, start);\n }\n\n controls.start({\n rotate: start + 360,\n scale: scaleVal,\n transition: transitionConfig\n });\n };\n\n const handleHoverEnd = () => {\n const start = rotation.get();\n controls.start({\n rotate: start + 360,\n scale: 1,\n transition: getTransition(spinDuration, start)\n });\n };\n\n return (\n \n {letters.map((letter, i) => {\n const rotationDeg = (360 / letters.length) * i;\n const factor = Math.PI / letters.length;\n const x = factor * i;\n const y = factor * i;\n const transform = `rotateZ(${rotationDeg}deg) translate3d(${x}px, ${y}px, 0)`;\n\n return (\n \n {letter}\n \n );\n })}\n \n );\n};\n\nexport default CircularText;\n" + "path": "CircularText.tsx", + "content": "import React, { useEffect } from 'react';\nimport { motion, useAnimation, useMotionValue, MotionValue, type Transition } from 'motion/react';\n\nimport './CircularText.css';\ninterface CircularTextProps {\n text: string;\n spinDuration?: number;\n onHover?: 'slowDown' | 'speedUp' | 'pause' | 'goBonkers';\n className?: string;\n}\n\nconst getRotationTransition = (duration: number, from: number, loop: boolean = true) => ({\n from,\n to: from + 360,\n ease: 'linear' as const,\n duration,\n type: 'tween' as const,\n repeat: loop ? Infinity : 0\n});\n\nconst getTransition = (duration: number, from: number) => ({\n rotate: getRotationTransition(duration, from),\n scale: {\n type: 'spring' as const,\n damping: 20,\n stiffness: 300\n }\n});\n\nconst CircularText: React.FC = ({\n text,\n spinDuration = 20,\n onHover = 'speedUp',\n className = ''\n}) => {\n const letters = Array.from(text);\n const controls = useAnimation();\n const rotation: MotionValue = useMotionValue(0);\n\n useEffect(() => {\n const start = rotation.get();\n controls.start({\n rotate: start + 360,\n scale: 1,\n transition: getTransition(spinDuration, start)\n });\n }, [spinDuration, text, onHover, controls]);\n\n const handleHoverStart = () => {\n const start = rotation.get();\n\n if (!onHover) return;\n\n let transitionConfig: ReturnType | Transition;\n let scaleVal = 1;\n\n switch (onHover) {\n case 'slowDown':\n transitionConfig = getTransition(spinDuration * 2, start);\n break;\n case 'speedUp':\n transitionConfig = getTransition(spinDuration / 4, start);\n break;\n case 'pause':\n transitionConfig = {\n rotate: { type: 'spring', damping: 20, stiffness: 300 },\n scale: { type: 'spring', damping: 20, stiffness: 300 }\n };\n break;\n case 'goBonkers':\n transitionConfig = getTransition(spinDuration / 20, start);\n scaleVal = 0.8;\n break;\n default:\n transitionConfig = getTransition(spinDuration, start);\n }\n\n controls.start({\n rotate: start + 360,\n scale: scaleVal,\n transition: transitionConfig\n });\n };\n\n const handleHoverEnd = () => {\n const start = rotation.get();\n controls.start({\n rotate: start + 360,\n scale: 1,\n transition: getTransition(spinDuration, start)\n });\n };\n\n return (\n \n {letters.map((letter, i) => {\n const rotationDeg = (360 / letters.length) * i;\n const factor = Math.PI / letters.length;\n const x = factor * i;\n const y = factor * i;\n const transform = `rotateZ(${rotationDeg}deg) translate3d(${x}px, ${y}px, 0)`;\n\n return (\n \n {letter}\n \n );\n })}\n \n );\n};\n\nexport default CircularText;\n" } ], "registryDependencies": [], diff --git a/public/r/CircularText-TS-TW.json b/public/r/CircularText-TS-TW.json index ee4b59d5..0a4b0ab7 100644 --- a/public/r/CircularText-TS-TW.json +++ b/public/r/CircularText-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "CircularText/CircularText.tsx", - "content": "import React, { useEffect } from 'react';\nimport { motion, useAnimation, useMotionValue, MotionValue, Transition } from 'motion/react';\ninterface CircularTextProps {\n text: string;\n spinDuration?: number;\n onHover?: 'slowDown' | 'speedUp' | 'pause' | 'goBonkers';\n className?: string;\n}\n\nconst getRotationTransition = (duration: number, from: number, loop: boolean = true) => ({\n from,\n to: from + 360,\n ease: 'linear' as const,\n duration,\n type: 'tween' as const,\n repeat: loop ? Infinity : 0\n});\n\nconst getTransition = (duration: number, from: number) => ({\n rotate: getRotationTransition(duration, from),\n scale: {\n type: 'spring' as const,\n damping: 20,\n stiffness: 300\n }\n});\n\nconst CircularText: React.FC = ({\n text,\n spinDuration = 20,\n onHover = 'speedUp',\n className = ''\n}) => {\n const letters = Array.from(text);\n const controls = useAnimation();\n const rotation: MotionValue = useMotionValue(0);\n\n useEffect(() => {\n const start = rotation.get();\n controls.start({\n rotate: start + 360,\n scale: 1,\n transition: getTransition(spinDuration, start)\n });\n }, [spinDuration, text, onHover, controls]);\n\n const handleHoverStart = () => {\n const start = rotation.get();\n\n if (!onHover) return;\n\n let transitionConfig: ReturnType | Transition;\n let scaleVal = 1;\n\n switch (onHover) {\n case 'slowDown':\n transitionConfig = getTransition(spinDuration * 2, start);\n break;\n case 'speedUp':\n transitionConfig = getTransition(spinDuration / 4, start);\n break;\n case 'pause':\n transitionConfig = {\n rotate: { type: 'spring', damping: 20, stiffness: 300 },\n scale: { type: 'spring', damping: 20, stiffness: 300 }\n };\n break;\n case 'goBonkers':\n transitionConfig = getTransition(spinDuration / 20, start);\n scaleVal = 0.8;\n break;\n default:\n transitionConfig = getTransition(spinDuration, start);\n }\n\n controls.start({\n rotate: start + 360,\n scale: scaleVal,\n transition: transitionConfig\n });\n };\n\n const handleHoverEnd = () => {\n const start = rotation.get();\n controls.start({\n rotate: start + 360,\n scale: 1,\n transition: getTransition(spinDuration, start)\n });\n };\n\n return (\n \n {letters.map((letter, i) => {\n const rotationDeg = (360 / letters.length) * i;\n const factor = Math.PI / letters.length;\n const x = factor * i;\n const y = factor * i;\n const transform = `rotateZ(${rotationDeg}deg) translate3d(${x}px, ${y}px, 0)`;\n\n return (\n \n {letter}\n \n );\n })}\n \n );\n};\n\nexport default CircularText;\n" + "content": "import React, { useEffect } from 'react';\nimport { motion, useAnimation, useMotionValue, MotionValue, type Transition } from 'motion/react';\ninterface CircularTextProps {\n text: string;\n spinDuration?: number;\n onHover?: 'slowDown' | 'speedUp' | 'pause' | 'goBonkers';\n className?: string;\n}\n\nconst getRotationTransition = (duration: number, from: number, loop: boolean = true) => ({\n from,\n to: from + 360,\n ease: 'linear' as const,\n duration,\n type: 'tween' as const,\n repeat: loop ? Infinity : 0\n});\n\nconst getTransition = (duration: number, from: number) => ({\n rotate: getRotationTransition(duration, from),\n scale: {\n type: 'spring' as const,\n damping: 20,\n stiffness: 300\n }\n});\n\nconst CircularText: React.FC = ({\n text,\n spinDuration = 20,\n onHover = 'speedUp',\n className = ''\n}) => {\n const letters = Array.from(text);\n const controls = useAnimation();\n const rotation: MotionValue = useMotionValue(0);\n\n useEffect(() => {\n const start = rotation.get();\n controls.start({\n rotate: start + 360,\n scale: 1,\n transition: getTransition(spinDuration, start)\n });\n }, [spinDuration, text, onHover, controls]);\n\n const handleHoverStart = () => {\n const start = rotation.get();\n\n if (!onHover) return;\n\n let transitionConfig: ReturnType | Transition;\n let scaleVal = 1;\n\n switch (onHover) {\n case 'slowDown':\n transitionConfig = getTransition(spinDuration * 2, start);\n break;\n case 'speedUp':\n transitionConfig = getTransition(spinDuration / 4, start);\n break;\n case 'pause':\n transitionConfig = {\n rotate: { type: 'spring', damping: 20, stiffness: 300 },\n scale: { type: 'spring', damping: 20, stiffness: 300 }\n };\n break;\n case 'goBonkers':\n transitionConfig = getTransition(spinDuration / 20, start);\n scaleVal = 0.8;\n break;\n default:\n transitionConfig = getTransition(spinDuration, start);\n }\n\n controls.start({\n rotate: start + 360,\n scale: scaleVal,\n transition: transitionConfig\n });\n };\n\n const handleHoverEnd = () => {\n const start = rotation.get();\n controls.start({\n rotate: start + 360,\n scale: 1,\n transition: getTransition(spinDuration, start)\n });\n };\n\n return (\n \n {letters.map((letter, i) => {\n const rotationDeg = (360 / letters.length) * i;\n const factor = Math.PI / letters.length;\n const x = factor * i;\n const y = factor * i;\n const transform = `rotateZ(${rotationDeg}deg) translate3d(${x}px, ${y}px, 0)`;\n\n return (\n \n {letter}\n \n );\n })}\n \n );\n};\n\nexport default CircularText;\n" } ], "registryDependencies": [], diff --git a/public/r/ColorBends-JS-CSS.json b/public/r/ColorBends-JS-CSS.json index c88ed3e4..08b2c325 100644 --- a/public/r/ColorBends-JS-CSS.json +++ b/public/r/ColorBends-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "ColorBends/ColorBends.css", + "type": "registry:file", + "path": "ColorBends.css", + "target": "@components/ColorBends.css", "content": ".color-bends-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "ColorBends/ColorBends.jsx", + "path": "ColorBends.jsx", "content": "import { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\nimport './ColorBends.css';\n\nconst MAX_COLORS = 8;\n\nconst frag = `\n#define MAX_COLORS ${MAX_COLORS}\nuniform vec2 uCanvas;\nuniform float uTime;\nuniform float uSpeed;\nuniform vec2 uRot;\nuniform int uColorCount;\nuniform vec3 uColors[MAX_COLORS];\nuniform int uTransparent;\nuniform float uScale;\nuniform float uFrequency;\nuniform float uWarpStrength;\nuniform vec2 uPointer; // in NDC [-1,1]\nuniform float uMouseInfluence;\nuniform float uParallax;\nuniform float uNoise;\nuniform int uIterations;\nuniform float uIntensity;\nuniform float uBandWidth;\nvarying vec2 vUv;\n\nvoid main() {\n float t = uTime * uSpeed;\n vec2 p = vUv * 2.0 - 1.0;\n p += uPointer * uParallax * 0.1;\n vec2 rp = vec2(p.x * uRot.x - p.y * uRot.y, p.x * uRot.y + p.y * uRot.x);\n vec2 q = vec2(rp.x * (uCanvas.x / uCanvas.y), rp.y);\n q /= max(uScale, 0.0001);\n q /= 0.5 + 0.2 * dot(q, q);\n q += 0.2 * cos(t) - 7.56;\n vec2 toward = (uPointer - rp);\n q += toward * uMouseInfluence * 0.2;\n\n for (int j = 0; j < 5; j++) {\n if (j >= uIterations - 1) break;\n vec2 rr = sin(1.5 * (q.yx * uFrequency) + 2.0 * cos(q * uFrequency));\n q += (rr - q) * 0.15;\n }\n\n vec3 col = vec3(0.0);\n float a = 1.0;\n\n if (uColorCount > 0) {\n vec2 s = q;\n vec3 sumCol = vec3(0.0);\n float cover = 0.0;\n for (int i = 0; i < MAX_COLORS; ++i) {\n if (i >= uColorCount) break;\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3); // strong response across 0..1\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0); // allow >1 to amplify displacement\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float m = mix(m0, m1, kMix);\n float w = 1.0 - exp(-uBandWidth / exp(uBandWidth * m));\n sumCol += uColors[i] * w;\n cover = max(cover, w);\n }\n col = clamp(sumCol, 0.0, 1.0);\n a = uTransparent > 0 ? cover : 1.0;\n } else {\n vec2 s = q;\n for (int k = 0; k < 3; ++k) {\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3);\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0);\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float m = mix(m0, m1, kMix);\n col[k] = 1.0 - exp(-uBandWidth / exp(uBandWidth * m));\n }\n a = uTransparent > 0 ? max(max(col.r, col.g), col.b) : 1.0;\n }\n\n col *= uIntensity;\n\n if (uNoise > 0.0001) {\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(uTime), vec2(12.9898, 78.233))) * 43758.5453123);\n col += (n - 0.5) * uNoise;\n col = clamp(col, 0.0, 1.0);\n }\n\n vec3 rgb = (uTransparent > 0) ? col * a : col;\n gl_FragColor = vec4(rgb, a);\n}\n`;\n\nconst vert = `\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n}\n`;\n\nexport default function ColorBends({\n className,\n style,\n rotation = 90,\n speed = 0.2,\n colors = [],\n transparent = true,\n autoRotate = 0,\n scale = 1,\n frequency = 1,\n warpStrength = 1,\n mouseInfluence = 1,\n parallax = 0.5,\n noise = 0.15,\n iterations = 1,\n intensity = 1.5,\n bandWidth = 6\n}) {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const rafRef = useRef(null);\n const materialRef = useRef(null);\n const resizeObserverRef = useRef(null);\n const rotationRef = useRef(rotation);\n const autoRotateRef = useRef(autoRotate);\n const pointerTargetRef = useRef(new THREE.Vector2(0, 0));\n const pointerCurrentRef = useRef(new THREE.Vector2(0, 0));\n const pointerSmoothRef = useRef(8);\n\n useEffect(() => {\n const container = containerRef.current;\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geometry = new THREE.PlaneGeometry(2, 2);\n const uColorsArray = Array.from({ length: MAX_COLORS }, () => new THREE.Vector3(0, 0, 0));\n const material = new THREE.ShaderMaterial({\n vertexShader: vert,\n fragmentShader: frag,\n uniforms: {\n uCanvas: { value: new THREE.Vector2(1, 1) },\n uTime: { value: 0 },\n uSpeed: { value: speed },\n uRot: { value: new THREE.Vector2(1, 0) },\n uColorCount: { value: 0 },\n uColors: { value: uColorsArray },\n uTransparent: { value: transparent ? 1 : 0 },\n uScale: { value: scale },\n uFrequency: { value: frequency },\n uWarpStrength: { value: warpStrength },\n uPointer: { value: new THREE.Vector2(0, 0) },\n uMouseInfluence: { value: mouseInfluence },\n uParallax: { value: parallax },\n uNoise: { value: noise },\n uIterations: { value: iterations },\n uIntensity: { value: intensity },\n uBandWidth: { value: bandWidth }\n },\n premultipliedAlpha: true,\n transparent: true\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geometry, material);\n scene.add(mesh);\n\n const renderer = new THREE.WebGLRenderer({\n antialias: false,\n powerPreference: 'high-performance',\n alpha: true\n });\n rendererRef.current = renderer;\n renderer.outputColorSpace = THREE.SRGBColorSpace;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setClearColor(0x000000, transparent ? 0 : 1);\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n renderer.domElement.style.display = 'block';\n container.appendChild(renderer.domElement);\n\n const clock = new THREE.Clock();\n\n const handleResize = () => {\n const w = container.clientWidth || 1;\n const h = container.clientHeight || 1;\n renderer.setSize(w, h, false);\n material.uniforms.uCanvas.value.set(w, h);\n };\n\n handleResize();\n\n if ('ResizeObserver' in window) {\n const ro = new ResizeObserver(handleResize);\n ro.observe(container);\n resizeObserverRef.current = ro;\n } else {\n window.addEventListener('resize', handleResize);\n }\n\n const loop = () => {\n const dt = clock.getDelta();\n const elapsed = clock.elapsedTime;\n material.uniforms.uTime.value = elapsed;\n\n const deg = (rotationRef.current % 360) + autoRotateRef.current * elapsed;\n const rad = (deg * Math.PI) / 180;\n const c = Math.cos(rad);\n const s = Math.sin(rad);\n material.uniforms.uRot.value.set(c, s);\n\n const cur = pointerCurrentRef.current;\n const tgt = pointerTargetRef.current;\n const amt = Math.min(1, dt * pointerSmoothRef.current);\n cur.lerp(tgt, amt);\n material.uniforms.uPointer.value.copy(cur);\n renderer.render(scene, camera);\n rafRef.current = requestAnimationFrame(loop);\n };\n rafRef.current = requestAnimationFrame(loop);\n\n return () => {\n if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);\n if (resizeObserverRef.current) resizeObserverRef.current.disconnect();\n else window.removeEventListener('resize', handleResize);\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n renderer.forceContextLoss();\n if (renderer.domElement && renderer.domElement.parentElement === container) {\n container.removeChild(renderer.domElement);\n }\n };\n }, [bandWidth, frequency, intensity, iterations, mouseInfluence, noise, parallax, scale, speed, transparent, warpStrength]);\n\n useEffect(() => {\n const material = materialRef.current;\n const renderer = rendererRef.current;\n if (!material) return;\n\n rotationRef.current = rotation;\n autoRotateRef.current = autoRotate;\n material.uniforms.uSpeed.value = speed;\n material.uniforms.uScale.value = scale;\n material.uniforms.uFrequency.value = frequency;\n material.uniforms.uWarpStrength.value = warpStrength;\n material.uniforms.uMouseInfluence.value = mouseInfluence;\n material.uniforms.uParallax.value = parallax;\n material.uniforms.uNoise.value = noise;\n material.uniforms.uIterations.value = iterations;\n material.uniforms.uIntensity.value = intensity;\n material.uniforms.uBandWidth.value = bandWidth;\n\n const toVec3 = hex => {\n const h = hex.replace('#', '').trim();\n const v =\n h.length === 3\n ? [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)]\n : [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];\n return new THREE.Vector3(v[0] / 255, v[1] / 255, v[2] / 255);\n };\n\n const arr = (colors || []).filter(Boolean).slice(0, MAX_COLORS).map(toVec3);\n for (let i = 0; i < MAX_COLORS; i++) {\n const vec = material.uniforms.uColors.value[i];\n if (i < arr.length) vec.copy(arr[i]);\n else vec.set(0, 0, 0);\n }\n material.uniforms.uColorCount.value = arr.length;\n\n material.uniforms.uTransparent.value = transparent ? 1 : 0;\n if (renderer) renderer.setClearColor(0x000000, transparent ? 0 : 1);\n }, [\n rotation,\n autoRotate,\n speed,\n scale,\n frequency,\n warpStrength,\n mouseInfluence,\n parallax,\n noise,\n iterations,\n intensity,\n bandWidth,\n colors,\n transparent\n ]);\n\n useEffect(() => {\n const material = materialRef.current;\n const container = containerRef.current;\n if (!material || !container) return;\n\n const handlePointerMove = e => {\n const rect = container.getBoundingClientRect();\n const x = ((e.clientX - rect.left) / (rect.width || 1)) * 2 - 1;\n const y = -(((e.clientY - rect.top) / (rect.height || 1)) * 2 - 1);\n pointerTargetRef.current.set(x, y);\n };\n\n container.addEventListener('pointermove', handlePointerMove);\n return () => {\n container.removeEventListener('pointermove', handlePointerMove);\n };\n }, []);\n\n return
;\n}\n" } ], diff --git a/public/r/ColorBends-TS-CSS.json b/public/r/ColorBends-TS-CSS.json index 483aa1c5..c85b41d6 100644 --- a/public/r/ColorBends-TS-CSS.json +++ b/public/r/ColorBends-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "ColorBends/ColorBends.css", + "type": "registry:file", + "path": "ColorBends.css", + "target": "@components/ColorBends.css", "content": ".color-bends-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "ColorBends/ColorBends.tsx", + "path": "ColorBends.tsx", "content": "import React, { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\nimport './ColorBends.css';\n\ntype ColorBendsProps = {\n className?: string;\n style?: React.CSSProperties;\n rotation?: number;\n speed?: number;\n colors?: string[];\n transparent?: boolean;\n autoRotate?: number;\n scale?: number;\n frequency?: number;\n warpStrength?: number;\n mouseInfluence?: number;\n parallax?: number;\n noise?: number;\n iterations?: number;\n intensity?: number;\n bandWidth?: number;\n};\n\nconst MAX_COLORS = 8 as const;\n\nconst frag = `\n#define MAX_COLORS ${MAX_COLORS}\nuniform vec2 uCanvas;\nuniform float uTime;\nuniform float uSpeed;\nuniform vec2 uRot;\nuniform int uColorCount;\nuniform vec3 uColors[MAX_COLORS];\nuniform int uTransparent;\nuniform float uScale;\nuniform float uFrequency;\nuniform float uWarpStrength;\nuniform vec2 uPointer; // in NDC [-1,1]\nuniform float uMouseInfluence;\nuniform float uParallax;\nuniform float uNoise;\nuniform int uIterations;\nuniform float uIntensity;\nuniform float uBandWidth;\nvarying vec2 vUv;\n\nvoid main() {\n float t = uTime * uSpeed;\n vec2 p = vUv * 2.0 - 1.0;\n p += uPointer * uParallax * 0.1;\n vec2 rp = vec2(p.x * uRot.x - p.y * uRot.y, p.x * uRot.y + p.y * uRot.x);\n vec2 q = vec2(rp.x * (uCanvas.x / uCanvas.y), rp.y);\n q /= max(uScale, 0.0001);\n q /= 0.5 + 0.2 * dot(q, q);\n q += 0.2 * cos(t) - 7.56;\n vec2 toward = (uPointer - rp);\n q += toward * uMouseInfluence * 0.2;\n\n for (int j = 0; j < 5; j++) {\n if (j >= uIterations - 1) break;\n vec2 rr = sin(1.5 * (q.yx * uFrequency) + 2.0 * cos(q * uFrequency));\n q += (rr - q) * 0.15;\n }\n\n vec3 col = vec3(0.0);\n float a = 1.0;\n\n if (uColorCount > 0) {\n vec2 s = q;\n vec3 sumCol = vec3(0.0);\n float cover = 0.0;\n for (int i = 0; i < MAX_COLORS; ++i) {\n if (i >= uColorCount) break;\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3); // strong response across 0..1\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0); // allow >1 to amplify displacement\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float m = mix(m0, m1, kMix);\n float w = 1.0 - exp(-uBandWidth / exp(uBandWidth * m));\n sumCol += uColors[i] * w;\n cover = max(cover, w);\n }\n col = clamp(sumCol, 0.0, 1.0);\n a = uTransparent > 0 ? cover : 1.0;\n } else {\n vec2 s = q;\n for (int k = 0; k < 3; ++k) {\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3);\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0);\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float m = mix(m0, m1, kMix);\n col[k] = 1.0 - exp(-uBandWidth / exp(uBandWidth * m));\n }\n a = uTransparent > 0 ? max(max(col.r, col.g), col.b) : 1.0;\n }\n\n col *= uIntensity;\n\n if (uNoise > 0.0001) {\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(uTime), vec2(12.9898, 78.233))) * 43758.5453123);\n col += (n - 0.5) * uNoise;\n col = clamp(col, 0.0, 1.0);\n }\n\n vec3 rgb = (uTransparent > 0) ? col * a : col;\n gl_FragColor = vec4(rgb, a);\n}\n`;\n\nconst vert = `\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n}\n`;\n\nexport default function ColorBends({\n className,\n style,\n rotation = 90,\n speed = 0.2,\n colors = [],\n transparent = true,\n autoRotate = 0,\n scale = 1,\n frequency = 1,\n warpStrength = 1,\n mouseInfluence = 1,\n parallax = 0.5,\n noise = 0.15,\n iterations = 1,\n intensity = 1.5,\n bandWidth = 6\n}: ColorBendsProps) {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const rafRef = useRef(null);\n const materialRef = useRef(null);\n const resizeObserverRef = useRef(null);\n const rotationRef = useRef(rotation);\n const autoRotateRef = useRef(autoRotate);\n const pointerTargetRef = useRef(new THREE.Vector2(0, 0));\n const pointerCurrentRef = useRef(new THREE.Vector2(0, 0));\n const pointerSmoothRef = useRef(8);\n\n useEffect(() => {\n const container = containerRef.current!;\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geometry = new THREE.PlaneGeometry(2, 2);\n const uColorsArray = Array.from({ length: MAX_COLORS }, () => new THREE.Vector3(0, 0, 0));\n const material = new THREE.ShaderMaterial({\n vertexShader: vert,\n fragmentShader: frag,\n uniforms: {\n uCanvas: { value: new THREE.Vector2(1, 1) },\n uTime: { value: 0 },\n uSpeed: { value: speed },\n uRot: { value: new THREE.Vector2(1, 0) },\n uColorCount: { value: 0 },\n uColors: { value: uColorsArray },\n uTransparent: { value: transparent ? 1 : 0 },\n uScale: { value: scale },\n uFrequency: { value: frequency },\n uWarpStrength: { value: warpStrength },\n uPointer: { value: new THREE.Vector2(0, 0) },\n uMouseInfluence: { value: mouseInfluence },\n uParallax: { value: parallax },\n uNoise: { value: noise },\n uIterations: { value: iterations },\n uIntensity: { value: intensity },\n uBandWidth: { value: bandWidth }\n },\n premultipliedAlpha: true,\n transparent: true\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geometry, material);\n scene.add(mesh);\n\n const renderer = new THREE.WebGLRenderer({\n antialias: false,\n powerPreference: 'high-performance',\n alpha: true\n });\n rendererRef.current = renderer;\n (renderer as any).outputColorSpace = (THREE as any).SRGBColorSpace;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setClearColor(0x000000, transparent ? 0 : 1);\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n renderer.domElement.style.display = 'block';\n container.appendChild(renderer.domElement);\n\n const clock = new THREE.Clock();\n\n const handleResize = () => {\n const w = container.clientWidth || 1;\n const h = container.clientHeight || 1;\n renderer.setSize(w, h, false);\n (material.uniforms.uCanvas.value as THREE.Vector2).set(w, h);\n };\n\n handleResize();\n\n if ('ResizeObserver' in window) {\n const ro = new ResizeObserver(handleResize);\n ro.observe(container);\n resizeObserverRef.current = ro;\n } else {\n (window as Window).addEventListener('resize', handleResize);\n }\n\n const loop = () => {\n const dt = clock.getDelta();\n const elapsed = clock.elapsedTime;\n material.uniforms.uTime.value = elapsed;\n\n const deg = (rotationRef.current % 360) + autoRotateRef.current * elapsed;\n const rad = (deg * Math.PI) / 180;\n const c = Math.cos(rad);\n const s = Math.sin(rad);\n (material.uniforms.uRot.value as THREE.Vector2).set(c, s);\n\n const cur = pointerCurrentRef.current;\n const tgt = pointerTargetRef.current;\n const amt = Math.min(1, dt * pointerSmoothRef.current);\n cur.lerp(tgt, amt);\n (material.uniforms.uPointer.value as THREE.Vector2).copy(cur);\n renderer.render(scene, camera);\n rafRef.current = requestAnimationFrame(loop);\n };\n rafRef.current = requestAnimationFrame(loop);\n\n return () => {\n if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);\n if (resizeObserverRef.current) resizeObserverRef.current.disconnect();\n else (window as Window).removeEventListener('resize', handleResize);\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n renderer.forceContextLoss();\n if (renderer.domElement && renderer.domElement.parentElement === container) {\n container.removeChild(renderer.domElement);\n }\n };\n }, []);\n\n useEffect(() => {\n const material = materialRef.current;\n const renderer = rendererRef.current;\n if (!material) return;\n\n rotationRef.current = rotation;\n autoRotateRef.current = autoRotate;\n material.uniforms.uSpeed.value = speed;\n material.uniforms.uScale.value = scale;\n material.uniforms.uFrequency.value = frequency;\n material.uniforms.uWarpStrength.value = warpStrength;\n material.uniforms.uMouseInfluence.value = mouseInfluence;\n material.uniforms.uParallax.value = parallax;\n material.uniforms.uNoise.value = noise;\n material.uniforms.uIterations.value = iterations;\n material.uniforms.uIntensity.value = intensity;\n material.uniforms.uBandWidth.value = bandWidth;\n\n const toVec3 = (hex: string) => {\n const h = hex.replace('#', '').trim();\n const v =\n h.length === 3\n ? [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)]\n : [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];\n return new THREE.Vector3(v[0] / 255, v[1] / 255, v[2] / 255);\n };\n\n const arr = (colors || []).filter(Boolean).slice(0, MAX_COLORS).map(toVec3);\n for (let i = 0; i < MAX_COLORS; i++) {\n const vec = (material.uniforms.uColors.value as THREE.Vector3[])[i];\n if (i < arr.length) vec.copy(arr[i]);\n else vec.set(0, 0, 0);\n }\n material.uniforms.uColorCount.value = arr.length;\n\n material.uniforms.uTransparent.value = transparent ? 1 : 0;\n if (renderer) renderer.setClearColor(0x000000, transparent ? 0 : 1);\n }, [\n rotation,\n autoRotate,\n speed,\n scale,\n frequency,\n warpStrength,\n mouseInfluence,\n parallax,\n noise,\n iterations,\n intensity,\n bandWidth,\n colors,\n transparent\n ]);\n\n useEffect(() => {\n const material = materialRef.current;\n const container = containerRef.current;\n if (!material || !container) return;\n\n const handlePointerMove = (e: PointerEvent) => {\n const rect = container.getBoundingClientRect();\n const x = ((e.clientX - rect.left) / (rect.width || 1)) * 2 - 1;\n const y = -(((e.clientY - rect.top) / (rect.height || 1)) * 2 - 1);\n pointerTargetRef.current.set(x, y);\n };\n\n container.addEventListener('pointermove', handlePointerMove);\n return () => {\n container.removeEventListener('pointermove', handlePointerMove);\n };\n }, []);\n\n return
;\n}\n" } ], diff --git a/public/r/Counter-JS-CSS.json b/public/r/Counter-JS-CSS.json index 6f8769a1..aef2143a 100644 --- a/public/r/Counter-JS-CSS.json +++ b/public/r/Counter-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Counter/Counter.css", + "type": "registry:file", + "path": "Counter.css", + "target": "@components/Counter.css", "content": ".counter-container {\n position: relative;\n display: inline-block;\n}\n\n.counter-counter {\n display: flex;\n overflow: hidden;\n line-height: 1;\n}\n\n.counter-digit {\n position: relative;\n width: 1ch;\n font-variant-numeric: tabular-nums;\n}\n\n.counter-number {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.gradient-container {\n pointer-events: none;\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n right: 0;\n}\n\n.bottom-gradient {\n position: absolute;\n bottom: 0;\n width: 100%;\n}\n" }, { "type": "registry:component", - "path": "Counter/Counter.jsx", + "path": "Counter.jsx", "content": "import { motion, useSpring, useTransform } from 'motion/react';\nimport { useEffect } from 'react';\n\nimport './Counter.css';\n\nfunction Number({ mv, number, height }) {\n let y = useTransform(mv, latest => {\n let placeValue = latest % 10;\n let offset = (10 + number - placeValue) % 10;\n let memo = offset * height;\n if (offset > 5) {\n memo -= 10 * height;\n }\n return memo;\n });\n return (\n \n {number}\n \n );\n}\n\nfunction normalizeNearInteger(num) {\n const nearest = Math.round(num);\n const tolerance = 1e-9 * Math.max(1, Math.abs(num));\n return Math.abs(num - nearest) < tolerance ? nearest : num;\n}\n\nfunction getValueRoundedToPlace(value, place) {\n const scaled = value / place;\n return Math.floor(normalizeNearInteger(scaled));\n}\n\nfunction Digit({ place, value, height, digitStyle }) {\n const isDecimal = place === '.';\n const valueRoundedToPlace = isDecimal ? 0 : getValueRoundedToPlace(value, place);\n const animatedValue = useSpring(valueRoundedToPlace);\n\n useEffect(() => {\n if (!isDecimal) {\n animatedValue.set(valueRoundedToPlace);\n }\n }, [animatedValue, valueRoundedToPlace, isDecimal]);\n\n if (isDecimal) {\n return (\n \n .\n \n );\n }\n\n return (\n \n {Array.from({ length: 10 }, (_, i) => (\n \n ))}\n \n );\n}\n\nexport default function Counter({\n value,\n fontSize = 100,\n padding = 0,\n places = [...value.toString()].map((ch, i, a) => {\n ch == '.';\n if (ch === '.') {\n return '.';\n } else {\n return (\n 10 **\n (a.indexOf('.') === -1 ? a.length - i - 1 : i < a.indexOf('.') ? a.indexOf('.') - i - 1 : -(i - a.indexOf('.')))\n );\n }\n }),\n gap = 8,\n borderRadius = 4,\n horizontalPadding = 8,\n textColor = 'inherit',\n fontWeight = 'inherit',\n containerStyle,\n counterStyle,\n digitStyle,\n gradientHeight = 16,\n gradientFrom = 'black',\n gradientTo = 'transparent',\n topGradientStyle,\n bottomGradientStyle\n}) {\n const height = fontSize + padding;\n const defaultCounterStyle = {\n fontSize,\n gap: gap,\n borderRadius: borderRadius,\n paddingLeft: horizontalPadding,\n paddingRight: horizontalPadding,\n color: textColor,\n fontWeight: fontWeight,\n direction: \"ltr\"\n };\n const defaultTopGradientStyle = {\n height: gradientHeight,\n background: `linear-gradient(to bottom, ${gradientFrom}, ${gradientTo})`\n };\n const defaultBottomGradientStyle = {\n height: gradientHeight,\n background: `linear-gradient(to top, ${gradientFrom}, ${gradientTo})`\n };\n return (\n \n \n {places.map(place => (\n \n ))}\n \n \n \n \n \n \n );\n}\n" } ], diff --git a/public/r/Counter-TS-CSS.json b/public/r/Counter-TS-CSS.json index cead910a..fdeb2b79 100644 --- a/public/r/Counter-TS-CSS.json +++ b/public/r/Counter-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Counter/Counter.css", + "type": "registry:file", + "path": "Counter.css", + "target": "@components/Counter.css", "content": ".counter-container {\n position: relative;\n display: inline-block;\n}\n\n.counter-counter {\n display: flex;\n overflow: hidden;\n line-height: 1;\n}\n\n.counter-digit {\n position: relative;\n width: 1ch;\n font-variant-numeric: tabular-nums;\n}\n\n.counter-number {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.gradient-container {\n pointer-events: none;\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n right: 0;\n}\n\n.bottom-gradient {\n position: absolute;\n bottom: 0;\n width: 100%;\n}\n" }, { "type": "registry:component", - "path": "Counter/Counter.tsx", + "path": "Counter.tsx", "content": "import { MotionValue, motion, useSpring, useTransform } from 'motion/react';\nimport type React from 'react';\nimport { useEffect } from 'react';\n\nimport './Counter.css';\n\ntype PlaceValue = number | '.';\n\ninterface NumberProps {\n mv: MotionValue;\n number: number;\n height: number;\n}\n\nfunction Number({ mv, number, height }: NumberProps) {\n const y = useTransform(mv, latest => {\n const placeValue = latest % 10;\n const offset = (10 + number - placeValue) % 10;\n let memo = offset * height;\n if (offset > 5) {\n memo -= 10 * height;\n }\n return memo;\n });\n\n return (\n \n {number}\n \n );\n}\n\nfunction normalizeNearInteger(num: number): number {\n const nearest = Math.round(num);\n const tolerance = 1e-9 * Math.max(1, Math.abs(num));\n return Math.abs(num - nearest) < tolerance ? nearest : num;\n}\n\nfunction getValueRoundedToPlace(value: number, place: number): number {\n const scaled = value / place;\n return Math.floor(normalizeNearInteger(scaled));\n}\n\ninterface DigitProps {\n place: PlaceValue;\n value: number;\n height: number;\n digitStyle?: React.CSSProperties;\n}\n\nfunction Digit({ place, value, height, digitStyle }: DigitProps) {\n if (place === '.') {\n return (\n \n .\n \n );\n }\n\n const valueRoundedToPlace = getValueRoundedToPlace(value, place);\n const animatedValue = useSpring(valueRoundedToPlace);\n\n useEffect(() => {\n animatedValue.set(valueRoundedToPlace);\n }, [animatedValue, valueRoundedToPlace]);\n\n return (\n \n {Array.from({ length: 10 }, (_, i) => (\n \n ))}\n \n );\n}\n\ninterface CounterProps {\n value: number;\n fontSize?: number;\n padding?: number;\n /**\n * An array of place values that determines which digit positions\n * should be displayed. For decimal places, use \".\" to represent\n * the decimal point. Leave this prop empty to enable automatic\n * detection based on the current value.\n */\n places?: PlaceValue[];\n gap?: number;\n borderRadius?: number;\n horizontalPadding?: number;\n textColor?: string;\n fontWeight?: React.CSSProperties['fontWeight'];\n containerStyle?: React.CSSProperties;\n counterStyle?: React.CSSProperties;\n digitStyle?: React.CSSProperties;\n gradientHeight?: number;\n gradientFrom?: string;\n gradientTo?: string;\n topGradientStyle?: React.CSSProperties;\n bottomGradientStyle?: React.CSSProperties;\n}\n\nexport default function Counter({\n value,\n fontSize = 100,\n padding = 0,\n places = [...value.toString()].map((ch, i, a) => {\n if (ch === '.') {\n return '.';\n }\n const dotIndex = a.indexOf('.');\n const isInteger = dotIndex === -1;\n\n const exponent = isInteger ? a.length - i - 1 : i < dotIndex ? dotIndex - i - 1 : -(i - dotIndex);\n\n return 10 ** exponent;\n }),\n gap = 8,\n borderRadius = 4,\n horizontalPadding = 8,\n textColor = 'inherit',\n fontWeight = 'inherit',\n containerStyle,\n counterStyle,\n digitStyle,\n gradientHeight = 16,\n gradientFrom = 'black',\n gradientTo = 'transparent',\n topGradientStyle,\n bottomGradientStyle\n}: CounterProps) {\n const height = fontSize + padding;\n\n const defaultCounterStyle: React.CSSProperties = {\n fontSize,\n gap,\n borderRadius,\n paddingLeft: horizontalPadding,\n paddingRight: horizontalPadding,\n color: textColor,\n fontWeight,\n direction: \"ltr\"\n };\n\n const defaultTopGradientStyle: React.CSSProperties = {\n height: gradientHeight,\n background: `linear-gradient(to bottom, ${gradientFrom}, ${gradientTo})`\n };\n\n const defaultBottomGradientStyle: React.CSSProperties = {\n height: gradientHeight,\n background: `linear-gradient(to top, ${gradientFrom}, ${gradientTo})`\n };\n\n return (\n \n \n {places.map(place => (\n \n ))}\n \n \n \n \n \n \n );\n}\n" } ], diff --git a/public/r/Crosshair-TS-CSS.json b/public/r/Crosshair-TS-CSS.json index 568e7d62..02e1a876 100644 --- a/public/r/Crosshair-TS-CSS.json +++ b/public/r/Crosshair-TS-CSS.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "Crosshair/Crosshair.tsx", - "content": "import React, { useEffect, useRef, RefObject } from 'react';\nimport { gsap } from 'gsap';\n\nconst lerp = (a: number, b: number, n: number): number => (1 - n) * a + n * b;\n\nconst getMousePos = (e: Event, container?: HTMLElement | null): { x: number; y: number } => {\n const mouseEvent = e as MouseEvent;\n if (container) {\n const bounds = container.getBoundingClientRect();\n return {\n x: mouseEvent.clientX - bounds.left,\n y: mouseEvent.clientY - bounds.top\n };\n }\n return { x: mouseEvent.clientX, y: mouseEvent.clientY };\n};\n\ninterface CrosshairProps {\n color?: string;\n containerRef?: RefObject;\n}\n\nconst Crosshair: React.FC = ({ color = 'white', containerRef = null }) => {\n const cursorRef = useRef(null);\n const lineHorizontalRef = useRef(null);\n const lineVerticalRef = useRef(null);\n const filterXRef = useRef(null);\n const filterYRef = useRef(null);\n\n let mouse = { x: 0, y: 0 };\n\n useEffect(() => {\n const handleMouseMove = (ev: Event) => {\n const mouseEvent = ev as MouseEvent;\n mouse = getMousePos(mouseEvent, containerRef?.current);\n if (containerRef?.current) {\n const bounds = containerRef.current.getBoundingClientRect();\n if (\n mouseEvent.clientX < bounds.left ||\n mouseEvent.clientX > bounds.right ||\n mouseEvent.clientY < bounds.top ||\n mouseEvent.clientY > bounds.bottom\n ) {\n gsap.to([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), { opacity: 0 });\n } else {\n gsap.to([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), { opacity: 1 });\n }\n }\n };\n\n const target: HTMLElement | Window = containerRef?.current || window;\n target.addEventListener('mousemove', handleMouseMove);\n\n const renderedStyles: {\n [key: string]: { previous: number; current: number; amt: number };\n } = {\n tx: { previous: 0, current: 0, amt: 0.15 },\n ty: { previous: 0, current: 0, amt: 0.15 }\n };\n\n gsap.set([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), { opacity: 0 });\n\n const onMouseMove = (_ev: Event) => {\n renderedStyles.tx.previous = renderedStyles.tx.current = mouse.x;\n renderedStyles.ty.previous = renderedStyles.ty.current = mouse.y;\n\n gsap.to([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), {\n duration: 0.9,\n ease: 'Power3.easeOut',\n opacity: 1\n });\n\n requestAnimationFrame(render);\n\n target.removeEventListener('mousemove', onMouseMove);\n };\n\n target.addEventListener('mousemove', onMouseMove);\n\n const primitiveValues = { turbulence: 0 };\n\n const tl = gsap\n .timeline({\n paused: true,\n onStart: () => {\n if (lineHorizontalRef.current) {\n lineHorizontalRef.current.style.filter = 'url(#filter-noise-x)';\n }\n if (lineVerticalRef.current) {\n lineVerticalRef.current.style.filter = 'url(#filter-noise-y)';\n }\n },\n onUpdate: () => {\n if (filterXRef.current && filterYRef.current) {\n filterXRef.current.setAttribute('baseFrequency', primitiveValues.turbulence.toString());\n filterYRef.current.setAttribute('baseFrequency', primitiveValues.turbulence.toString());\n }\n },\n onComplete: () => {\n if (lineHorizontalRef.current && lineVerticalRef.current) {\n lineHorizontalRef.current.style.filter = 'none';\n lineVerticalRef.current.style.filter = 'none';\n }\n }\n })\n .to(primitiveValues, {\n duration: 0.5,\n ease: 'power1',\n startAt: { turbulence: 1 },\n turbulence: 0\n });\n\n const enter = () => tl.restart();\n const leave = () => {\n tl.progress(1).kill();\n };\n\n const render = () => {\n renderedStyles.tx.current = mouse.x;\n renderedStyles.ty.current = mouse.y;\n\n for (const key in renderedStyles) {\n const style = renderedStyles[key];\n style.previous = lerp(style.previous, style.current, style.amt);\n }\n\n if (lineHorizontalRef.current && lineVerticalRef.current) {\n gsap.set(lineVerticalRef.current, { x: renderedStyles.tx.previous });\n gsap.set(lineHorizontalRef.current, { y: renderedStyles.ty.previous });\n }\n\n requestAnimationFrame(render);\n };\n\n const links: NodeListOf = containerRef?.current\n ? containerRef.current.querySelectorAll('a')\n : document.querySelectorAll('a');\n\n links.forEach(link => {\n link.addEventListener('mouseenter', enter);\n link.addEventListener('mouseleave', leave);\n });\n\n return () => {\n target.removeEventListener('mousemove', handleMouseMove);\n target.removeEventListener('mousemove', onMouseMove);\n links.forEach(link => {\n link.removeEventListener('mouseenter', enter);\n link.removeEventListener('mouseleave', leave);\n });\n };\n }, [containerRef]);\n\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n );\n};\n\nexport default Crosshair;\n" + "content": "import React, { useEffect, useRef, type RefObject } from 'react';\nimport { gsap } from 'gsap';\n\nconst lerp = (a: number, b: number, n: number): number => (1 - n) * a + n * b;\n\nconst getMousePos = (e: Event, container?: HTMLElement | null): { x: number; y: number } => {\n const mouseEvent = e as MouseEvent;\n if (container) {\n const bounds = container.getBoundingClientRect();\n return {\n x: mouseEvent.clientX - bounds.left,\n y: mouseEvent.clientY - bounds.top\n };\n }\n return { x: mouseEvent.clientX, y: mouseEvent.clientY };\n};\n\ninterface CrosshairProps {\n color?: string;\n containerRef?: RefObject;\n}\n\nconst Crosshair: React.FC = ({ color = 'white', containerRef = null }) => {\n const cursorRef = useRef(null);\n const lineHorizontalRef = useRef(null);\n const lineVerticalRef = useRef(null);\n const filterXRef = useRef(null);\n const filterYRef = useRef(null);\n\n let mouse = { x: 0, y: 0 };\n\n useEffect(() => {\n const handleMouseMove = (ev: Event) => {\n const mouseEvent = ev as MouseEvent;\n mouse = getMousePos(mouseEvent, containerRef?.current);\n if (containerRef?.current) {\n const bounds = containerRef.current.getBoundingClientRect();\n if (\n mouseEvent.clientX < bounds.left ||\n mouseEvent.clientX > bounds.right ||\n mouseEvent.clientY < bounds.top ||\n mouseEvent.clientY > bounds.bottom\n ) {\n gsap.to([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), { opacity: 0 });\n } else {\n gsap.to([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), { opacity: 1 });\n }\n }\n };\n\n const target: HTMLElement | Window = containerRef?.current || window;\n target.addEventListener('mousemove', handleMouseMove);\n\n const renderedStyles: {\n [key: string]: { previous: number; current: number; amt: number };\n } = {\n tx: { previous: 0, current: 0, amt: 0.15 },\n ty: { previous: 0, current: 0, amt: 0.15 }\n };\n\n gsap.set([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), { opacity: 0 });\n\n const onMouseMove = (_ev: Event) => {\n renderedStyles.tx.previous = renderedStyles.tx.current = mouse.x;\n renderedStyles.ty.previous = renderedStyles.ty.current = mouse.y;\n\n gsap.to([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), {\n duration: 0.9,\n ease: 'Power3.easeOut',\n opacity: 1\n });\n\n requestAnimationFrame(render);\n\n target.removeEventListener('mousemove', onMouseMove);\n };\n\n target.addEventListener('mousemove', onMouseMove);\n\n const primitiveValues = { turbulence: 0 };\n\n const tl = gsap\n .timeline({\n paused: true,\n onStart: () => {\n if (lineHorizontalRef.current) {\n lineHorizontalRef.current.style.filter = 'url(#filter-noise-x)';\n }\n if (lineVerticalRef.current) {\n lineVerticalRef.current.style.filter = 'url(#filter-noise-y)';\n }\n },\n onUpdate: () => {\n if (filterXRef.current && filterYRef.current) {\n filterXRef.current.setAttribute('baseFrequency', primitiveValues.turbulence.toString());\n filterYRef.current.setAttribute('baseFrequency', primitiveValues.turbulence.toString());\n }\n },\n onComplete: () => {\n if (lineHorizontalRef.current && lineVerticalRef.current) {\n lineHorizontalRef.current.style.filter = 'none';\n lineVerticalRef.current.style.filter = 'none';\n }\n }\n })\n .to(primitiveValues, {\n duration: 0.5,\n ease: 'power1',\n startAt: { turbulence: 1 },\n turbulence: 0\n });\n\n const enter = () => tl.restart();\n const leave = () => {\n tl.progress(1).kill();\n };\n\n const render = () => {\n renderedStyles.tx.current = mouse.x;\n renderedStyles.ty.current = mouse.y;\n\n for (const key in renderedStyles) {\n const style = renderedStyles[key];\n style.previous = lerp(style.previous, style.current, style.amt);\n }\n\n if (lineHorizontalRef.current && lineVerticalRef.current) {\n gsap.set(lineVerticalRef.current, { x: renderedStyles.tx.previous });\n gsap.set(lineHorizontalRef.current, { y: renderedStyles.ty.previous });\n }\n\n requestAnimationFrame(render);\n };\n\n const links: NodeListOf = containerRef?.current\n ? containerRef.current.querySelectorAll('a')\n : document.querySelectorAll('a');\n\n links.forEach(link => {\n link.addEventListener('mouseenter', enter);\n link.addEventListener('mouseleave', leave);\n });\n\n return () => {\n target.removeEventListener('mousemove', handleMouseMove);\n target.removeEventListener('mousemove', onMouseMove);\n links.forEach(link => {\n link.removeEventListener('mouseenter', enter);\n link.removeEventListener('mouseleave', leave);\n });\n };\n }, [containerRef]);\n\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n );\n};\n\nexport default Crosshair;\n" } ], "registryDependencies": [], diff --git a/public/r/Crosshair-TS-TW.json b/public/r/Crosshair-TS-TW.json index d02176cb..f8e5c5ac 100644 --- a/public/r/Crosshair-TS-TW.json +++ b/public/r/Crosshair-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "Crosshair/Crosshair.tsx", - "content": "import React, { useEffect, useRef, RefObject } from 'react';\nimport { gsap } from 'gsap';\n\nconst lerp = (a: number, b: number, n: number): number => (1 - n) * a + n * b;\n\nconst getMousePos = (e: Event, container?: HTMLElement | null): { x: number; y: number } => {\n const mouseEvent = e as MouseEvent;\n if (container) {\n const bounds = container.getBoundingClientRect();\n return {\n x: mouseEvent.clientX - bounds.left,\n y: mouseEvent.clientY - bounds.top\n };\n }\n return { x: mouseEvent.clientX, y: mouseEvent.clientY };\n};\n\ninterface CrosshairProps {\n color?: string;\n containerRef?: RefObject;\n}\n\nconst Crosshair: React.FC = ({ color = 'white', containerRef = null }) => {\n const cursorRef = useRef(null);\n const lineHorizontalRef = useRef(null);\n const lineVerticalRef = useRef(null);\n const filterXRef = useRef(null);\n const filterYRef = useRef(null);\n\n let mouse = { x: 0, y: 0 };\n\n useEffect(() => {\n const handleMouseMove = (ev: Event) => {\n const mouseEvent = ev as MouseEvent;\n mouse = getMousePos(mouseEvent, containerRef?.current);\n if (containerRef?.current) {\n const bounds = containerRef.current.getBoundingClientRect();\n if (\n mouseEvent.clientX < bounds.left ||\n mouseEvent.clientX > bounds.right ||\n mouseEvent.clientY < bounds.top ||\n mouseEvent.clientY > bounds.bottom\n ) {\n gsap.to([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), { opacity: 0 });\n } else {\n gsap.to([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), { opacity: 1 });\n }\n }\n };\n\n const target: HTMLElement | Window = containerRef?.current || window;\n target.addEventListener('mousemove', handleMouseMove);\n\n const renderedStyles: {\n [key: string]: { previous: number; current: number; amt: number };\n } = {\n tx: { previous: 0, current: 0, amt: 0.15 },\n ty: { previous: 0, current: 0, amt: 0.15 }\n };\n\n gsap.set([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), { opacity: 0 });\n\n const onMouseMove = (_ev: Event) => {\n renderedStyles.tx.previous = renderedStyles.tx.current = mouse.x;\n renderedStyles.ty.previous = renderedStyles.ty.current = mouse.y;\n\n gsap.to([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), {\n duration: 0.9,\n ease: 'Power3.easeOut',\n opacity: 1\n });\n\n requestAnimationFrame(render);\n\n target.removeEventListener('mousemove', onMouseMove);\n };\n\n target.addEventListener('mousemove', onMouseMove);\n\n const primitiveValues = { turbulence: 0 };\n\n const tl = gsap\n .timeline({\n paused: true,\n onStart: () => {\n if (lineHorizontalRef.current) {\n lineHorizontalRef.current.style.filter = 'url(#filter-noise-x)';\n }\n if (lineVerticalRef.current) {\n lineVerticalRef.current.style.filter = 'url(#filter-noise-y)';\n }\n },\n onUpdate: () => {\n if (filterXRef.current && filterYRef.current) {\n filterXRef.current.setAttribute('baseFrequency', primitiveValues.turbulence.toString());\n filterYRef.current.setAttribute('baseFrequency', primitiveValues.turbulence.toString());\n }\n },\n onComplete: () => {\n if (lineHorizontalRef.current && lineVerticalRef.current) {\n lineHorizontalRef.current.style.filter = 'none';\n lineVerticalRef.current.style.filter = 'none';\n }\n }\n })\n .to(primitiveValues, {\n duration: 0.5,\n ease: 'power1',\n startAt: { turbulence: 1 },\n turbulence: 0\n });\n\n const enter = () => tl.restart();\n const leave = () => {\n tl.progress(1).kill();\n };\n\n const render = () => {\n renderedStyles.tx.current = mouse.x;\n renderedStyles.ty.current = mouse.y;\n\n for (const key in renderedStyles) {\n const style = renderedStyles[key];\n style.previous = lerp(style.previous, style.current, style.amt);\n }\n\n if (lineHorizontalRef.current && lineVerticalRef.current) {\n gsap.set(lineVerticalRef.current, { x: renderedStyles.tx.previous });\n gsap.set(lineHorizontalRef.current, { y: renderedStyles.ty.previous });\n }\n\n requestAnimationFrame(render);\n };\n\n const links: NodeListOf = containerRef?.current\n ? containerRef.current.querySelectorAll('a')\n : document.querySelectorAll('a');\n\n links.forEach(link => {\n link.addEventListener('mouseenter', enter);\n link.addEventListener('mouseleave', leave);\n });\n\n return () => {\n target.removeEventListener('mousemove', handleMouseMove);\n target.removeEventListener('mousemove', onMouseMove);\n links.forEach(link => {\n link.removeEventListener('mouseenter', enter);\n link.removeEventListener('mouseleave', leave);\n });\n };\n }, [containerRef]);\n\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n
\n \n );\n};\n\nexport default Crosshair;\n" + "content": "import React, { useEffect, useRef, type RefObject } from 'react';\nimport { gsap } from 'gsap';\n\nconst lerp = (a: number, b: number, n: number): number => (1 - n) * a + n * b;\n\nconst getMousePos = (e: Event, container?: HTMLElement | null): { x: number; y: number } => {\n const mouseEvent = e as MouseEvent;\n if (container) {\n const bounds = container.getBoundingClientRect();\n return {\n x: mouseEvent.clientX - bounds.left,\n y: mouseEvent.clientY - bounds.top\n };\n }\n return { x: mouseEvent.clientX, y: mouseEvent.clientY };\n};\n\ninterface CrosshairProps {\n color?: string;\n containerRef?: RefObject;\n}\n\nconst Crosshair: React.FC = ({ color = 'white', containerRef = null }) => {\n const cursorRef = useRef(null);\n const lineHorizontalRef = useRef(null);\n const lineVerticalRef = useRef(null);\n const filterXRef = useRef(null);\n const filterYRef = useRef(null);\n\n let mouse = { x: 0, y: 0 };\n\n useEffect(() => {\n const handleMouseMove = (ev: Event) => {\n const mouseEvent = ev as MouseEvent;\n mouse = getMousePos(mouseEvent, containerRef?.current);\n if (containerRef?.current) {\n const bounds = containerRef.current.getBoundingClientRect();\n if (\n mouseEvent.clientX < bounds.left ||\n mouseEvent.clientX > bounds.right ||\n mouseEvent.clientY < bounds.top ||\n mouseEvent.clientY > bounds.bottom\n ) {\n gsap.to([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), { opacity: 0 });\n } else {\n gsap.to([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), { opacity: 1 });\n }\n }\n };\n\n const target: HTMLElement | Window = containerRef?.current || window;\n target.addEventListener('mousemove', handleMouseMove);\n\n const renderedStyles: {\n [key: string]: { previous: number; current: number; amt: number };\n } = {\n tx: { previous: 0, current: 0, amt: 0.15 },\n ty: { previous: 0, current: 0, amt: 0.15 }\n };\n\n gsap.set([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), { opacity: 0 });\n\n const onMouseMove = (_ev: Event) => {\n renderedStyles.tx.previous = renderedStyles.tx.current = mouse.x;\n renderedStyles.ty.previous = renderedStyles.ty.current = mouse.y;\n\n gsap.to([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), {\n duration: 0.9,\n ease: 'Power3.easeOut',\n opacity: 1\n });\n\n requestAnimationFrame(render);\n\n target.removeEventListener('mousemove', onMouseMove);\n };\n\n target.addEventListener('mousemove', onMouseMove);\n\n const primitiveValues = { turbulence: 0 };\n\n const tl = gsap\n .timeline({\n paused: true,\n onStart: () => {\n if (lineHorizontalRef.current) {\n lineHorizontalRef.current.style.filter = 'url(#filter-noise-x)';\n }\n if (lineVerticalRef.current) {\n lineVerticalRef.current.style.filter = 'url(#filter-noise-y)';\n }\n },\n onUpdate: () => {\n if (filterXRef.current && filterYRef.current) {\n filterXRef.current.setAttribute('baseFrequency', primitiveValues.turbulence.toString());\n filterYRef.current.setAttribute('baseFrequency', primitiveValues.turbulence.toString());\n }\n },\n onComplete: () => {\n if (lineHorizontalRef.current && lineVerticalRef.current) {\n lineHorizontalRef.current.style.filter = 'none';\n lineVerticalRef.current.style.filter = 'none';\n }\n }\n })\n .to(primitiveValues, {\n duration: 0.5,\n ease: 'power1',\n startAt: { turbulence: 1 },\n turbulence: 0\n });\n\n const enter = () => tl.restart();\n const leave = () => {\n tl.progress(1).kill();\n };\n\n const render = () => {\n renderedStyles.tx.current = mouse.x;\n renderedStyles.ty.current = mouse.y;\n\n for (const key in renderedStyles) {\n const style = renderedStyles[key];\n style.previous = lerp(style.previous, style.current, style.amt);\n }\n\n if (lineHorizontalRef.current && lineVerticalRef.current) {\n gsap.set(lineVerticalRef.current, { x: renderedStyles.tx.previous });\n gsap.set(lineHorizontalRef.current, { y: renderedStyles.ty.previous });\n }\n\n requestAnimationFrame(render);\n };\n\n const links: NodeListOf = containerRef?.current\n ? containerRef.current.querySelectorAll('a')\n : document.querySelectorAll('a');\n\n links.forEach(link => {\n link.addEventListener('mouseenter', enter);\n link.addEventListener('mouseleave', leave);\n });\n\n return () => {\n target.removeEventListener('mousemove', handleMouseMove);\n target.removeEventListener('mousemove', onMouseMove);\n links.forEach(link => {\n link.removeEventListener('mouseenter', enter);\n link.removeEventListener('mouseleave', leave);\n });\n };\n }, [containerRef]);\n\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n};\n\nexport default Crosshair;\n" } ], "registryDependencies": [], diff --git a/public/r/Cubes-JS-CSS.json b/public/r/Cubes-JS-CSS.json index d2fe3eb5..03e21b4a 100644 --- a/public/r/Cubes-JS-CSS.json +++ b/public/r/Cubes-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Cubes/Cubes.css", + "type": "registry:file", + "path": "Cubes.css", + "target": "@components/Cubes.css", "content": ":root {\n --col-gap: 5%;\n --row-gap: 5%;\n --cube-perspective: 99999999px;\n --cube-face-border: 1px solid #fff;\n --cube-face-bg: #120F17;\n}\n\n.default-animation {\n position: relative;\n width: 50%;\n aspect-ratio: 1 / 1;\n height: auto;\n}\n\n.default-animation--scene {\n display: grid;\n width: 100%;\n height: 100%;\n column-gap: var(--col-gap);\n row-gap: var(--row-gap);\n perspective: var(--cube-perspective);\n grid-auto-rows: 1fr;\n}\n\n.cube {\n position: relative;\n width: 100%;\n height: 100%;\n aspect-ratio: 1 / 1;\n transform-style: preserve-3d;\n}\n\n.cube::before {\n content: '';\n position: absolute;\n top: -36px;\n right: -36px;\n bottom: -36px;\n left: -36px;\n}\n\n.default-animation .cube-face {\n position: absolute;\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--cube-face-bg);\n border: var(--cube-face-border);\n opacity: 1;\n}\n\n.default-animation .cube-face--top {\n transform: translateY(-50%) rotateX(90deg);\n}\n\n.default-animation .cube-face--bottom {\n transform: translateY(50%) rotateX(-90deg);\n}\n\n.default-animation .cube-face--left {\n transform: translateX(-50%) rotateY(-90deg);\n}\n\n.default-animation .cube-face--right {\n transform: translateX(50%) rotateY(90deg);\n}\n\n.default-animation .cube-face--back,\n.default-animation .cube-face--front {\n transform: rotateY(-90deg) translateX(50%) rotateY(90deg);\n}\n\n@media (max-width: 768px) {\n .default-animation {\n width: 90%;\n }\n}\n" }, { "type": "registry:component", - "path": "Cubes/Cubes.jsx", + "path": "Cubes.jsx", "content": "import { useCallback, useEffect, useRef } from 'react';\nimport gsap from 'gsap';\nimport './Cubes.css';\n\nconst Cubes = ({\n gridSize = 10,\n cubeSize,\n maxAngle = 45,\n radius = 3,\n easing = 'power3.out',\n duration = { enter: 0.3, leave: 0.6 },\n cellGap,\n borderStyle = '1px solid #fff',\n faceColor = '#120F17',\n shadow = false,\n autoAnimate = true,\n rippleOnClick = true,\n rippleColor = '#fff',\n rippleSpeed = 2\n}) => {\n const sceneRef = useRef(null);\n const rafRef = useRef(null);\n const idleTimerRef = useRef(null);\n const userActiveRef = useRef(false);\n const simPosRef = useRef({ x: 0, y: 0 });\n const simTargetRef = useRef({ x: 0, y: 0 });\n const simRAFRef = useRef(null);\n\n const colGap = typeof cellGap === 'number' ? `${cellGap}px` : cellGap?.col !== undefined ? `${cellGap.col}px` : '5%';\n const rowGap = typeof cellGap === 'number' ? `${cellGap}px` : cellGap?.row !== undefined ? `${cellGap.row}px` : '5%';\n\n const enterDur = duration.enter;\n const leaveDur = duration.leave;\n\n const tiltAt = useCallback(\n (rowCenter, colCenter) => {\n if (!sceneRef.current) return;\n sceneRef.current.querySelectorAll('.cube').forEach(cube => {\n const r = +cube.dataset.row;\n const c = +cube.dataset.col;\n const dist = Math.hypot(r - rowCenter, c - colCenter);\n if (dist <= radius) {\n const pct = 1 - dist / radius;\n const angle = pct * maxAngle;\n gsap.to(cube, {\n duration: enterDur,\n ease: easing,\n overwrite: true,\n rotateX: -angle,\n rotateY: angle\n });\n } else {\n gsap.to(cube, {\n duration: leaveDur,\n ease: 'power3.out',\n overwrite: true,\n rotateX: 0,\n rotateY: 0\n });\n }\n });\n },\n [radius, maxAngle, enterDur, leaveDur, easing]\n );\n\n const onPointerMove = useCallback(\n e => {\n userActiveRef.current = true;\n if (idleTimerRef.current) clearTimeout(idleTimerRef.current);\n\n const rect = sceneRef.current.getBoundingClientRect();\n const cellW = rect.width / gridSize;\n const cellH = rect.height / gridSize;\n const colCenter = (e.clientX - rect.left) / cellW;\n const rowCenter = (e.clientY - rect.top) / cellH;\n\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n rafRef.current = requestAnimationFrame(() => tiltAt(rowCenter, colCenter));\n\n idleTimerRef.current = setTimeout(() => {\n userActiveRef.current = false;\n }, 3000);\n },\n [gridSize, tiltAt]\n );\n\n const resetAll = useCallback(() => {\n if (!sceneRef.current) return;\n sceneRef.current.querySelectorAll('.cube').forEach(cube =>\n gsap.to(cube, {\n duration: leaveDur,\n rotateX: 0,\n rotateY: 0,\n ease: 'power3.out'\n })\n );\n }, [leaveDur]);\n\n const onTouchMove = useCallback(\n e => {\n e.preventDefault();\n userActiveRef.current = true;\n if (idleTimerRef.current) clearTimeout(idleTimerRef.current);\n\n const rect = sceneRef.current.getBoundingClientRect();\n const cellW = rect.width / gridSize;\n const cellH = rect.height / gridSize;\n\n const touch = e.touches[0];\n const colCenter = (touch.clientX - rect.left) / cellW;\n const rowCenter = (touch.clientY - rect.top) / cellH;\n\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n rafRef.current = requestAnimationFrame(() => tiltAt(rowCenter, colCenter));\n\n idleTimerRef.current = setTimeout(() => {\n userActiveRef.current = false;\n }, 3000);\n },\n [gridSize, tiltAt]\n );\n\n const onTouchStart = useCallback(() => {\n userActiveRef.current = true;\n }, []);\n\n const onTouchEnd = useCallback(() => {\n if (!sceneRef.current) return;\n resetAll();\n }, [resetAll]);\n\n const onClick = useCallback(\n e => {\n if (!rippleOnClick || !sceneRef.current) return;\n const rect = sceneRef.current.getBoundingClientRect();\n const cellW = rect.width / gridSize;\n const cellH = rect.height / gridSize;\n\n const clientX = e.clientX || (e.touches && e.touches[0].clientX);\n const clientY = e.clientY || (e.touches && e.touches[0].clientY);\n\n const colHit = Math.floor((clientX - rect.left) / cellW);\n const rowHit = Math.floor((clientY - rect.top) / cellH);\n\n const baseRingDelay = 0.15;\n const baseAnimDur = 0.3;\n const baseHold = 0.6;\n\n const spreadDelay = baseRingDelay / rippleSpeed;\n const animDuration = baseAnimDur / rippleSpeed;\n const holdTime = baseHold / rippleSpeed;\n\n const rings = {};\n sceneRef.current.querySelectorAll('.cube').forEach(cube => {\n const r = +cube.dataset.row;\n const c = +cube.dataset.col;\n const dist = Math.hypot(r - rowHit, c - colHit);\n const ring = Math.round(dist);\n if (!rings[ring]) rings[ring] = [];\n rings[ring].push(cube);\n });\n\n Object.keys(rings)\n .map(Number)\n .sort((a, b) => a - b)\n .forEach(ring => {\n const delay = ring * spreadDelay;\n const faces = rings[ring].flatMap(cube => Array.from(cube.querySelectorAll('.cube-face')));\n\n gsap.to(faces, {\n backgroundColor: rippleColor,\n duration: animDuration,\n delay,\n ease: 'power3.out'\n });\n gsap.to(faces, {\n backgroundColor: faceColor,\n duration: animDuration,\n delay: delay + animDuration + holdTime,\n ease: 'power3.out'\n });\n });\n },\n [rippleOnClick, gridSize, faceColor, rippleColor, rippleSpeed]\n );\n\n useEffect(() => {\n if (!autoAnimate || !sceneRef.current) return;\n simPosRef.current = {\n x: Math.random() * gridSize,\n y: Math.random() * gridSize\n };\n simTargetRef.current = {\n x: Math.random() * gridSize,\n y: Math.random() * gridSize\n };\n const speed = 0.02;\n const loop = () => {\n if (!userActiveRef.current) {\n const pos = simPosRef.current;\n const tgt = simTargetRef.current;\n pos.x += (tgt.x - pos.x) * speed;\n pos.y += (tgt.y - pos.y) * speed;\n tiltAt(pos.y, pos.x);\n if (Math.hypot(pos.x - tgt.x, pos.y - tgt.y) < 0.1) {\n simTargetRef.current = {\n x: Math.random() * gridSize,\n y: Math.random() * gridSize\n };\n }\n }\n simRAFRef.current = requestAnimationFrame(loop);\n };\n simRAFRef.current = requestAnimationFrame(loop);\n return () => {\n if (simRAFRef.current != null) {\n cancelAnimationFrame(simRAFRef.current);\n }\n };\n }, [autoAnimate, gridSize, tiltAt]);\n\n useEffect(() => {\n const el = sceneRef.current;\n if (!el) return;\n\n el.addEventListener('pointermove', onPointerMove);\n el.addEventListener('pointerleave', resetAll);\n el.addEventListener('click', onClick);\n\n el.addEventListener('touchmove', onTouchMove, { passive: false });\n el.addEventListener('touchstart', onTouchStart, { passive: true });\n el.addEventListener('touchend', onTouchEnd, { passive: true });\n\n return () => {\n el.removeEventListener('pointermove', onPointerMove);\n el.removeEventListener('pointerleave', resetAll);\n el.removeEventListener('click', onClick);\n\n el.removeEventListener('touchmove', onTouchMove);\n el.removeEventListener('touchstart', onTouchStart);\n el.removeEventListener('touchend', onTouchEnd);\n\n rafRef.current != null && cancelAnimationFrame(rafRef.current);\n idleTimerRef.current && clearTimeout(idleTimerRef.current);\n };\n }, [onPointerMove, resetAll, onClick, onTouchMove, onTouchStart, onTouchEnd]);\n\n const cells = Array.from({ length: gridSize });\n const sceneStyle = {\n gridTemplateColumns: cubeSize ? `repeat(${gridSize}, ${cubeSize}px)` : `repeat(${gridSize}, 1fr)`,\n gridTemplateRows: cubeSize ? `repeat(${gridSize}, ${cubeSize}px)` : `repeat(${gridSize}, 1fr)`,\n columnGap: colGap,\n rowGap: rowGap\n };\n const wrapperStyle = {\n '--cube-face-border': borderStyle,\n '--cube-face-bg': faceColor,\n '--cube-face-shadow': shadow === true ? '0 0 6px rgba(0,0,0,.5)' : shadow || 'none',\n ...(cubeSize\n ? {\n width: `${gridSize * cubeSize}px`,\n height: `${gridSize * cubeSize}px`\n }\n : {})\n };\n\n return (\n
\n
\n {cells.map((_, r) =>\n cells.map((__, c) => (\n
\n
\n
\n
\n
\n
\n
\n
\n ))\n )}\n
\n
\n );\n};\n\nexport default Cubes;\n" } ], diff --git a/public/r/Cubes-TS-CSS.json b/public/r/Cubes-TS-CSS.json index 47e947af..1689010f 100644 --- a/public/r/Cubes-TS-CSS.json +++ b/public/r/Cubes-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Cubes/Cubes.css", + "type": "registry:file", + "path": "Cubes.css", + "target": "@components/Cubes.css", "content": ":root {\n --col-gap: 5%;\n --row-gap: 5%;\n --cube-perspective: 99999999px;\n --cube-face-border: 1px solid #fff;\n --cube-face-bg: #120F17;\n}\n\n.default-animation {\n position: relative;\n width: 50%;\n aspect-ratio: 1 / 1;\n height: auto;\n}\n\n.default-animation--scene {\n display: grid;\n width: 100%;\n height: 100%;\n column-gap: var(--col-gap);\n row-gap: var(--row-gap);\n perspective: var(--cube-perspective);\n grid-auto-rows: 1fr;\n}\n\n.cube {\n position: relative;\n width: 100%;\n height: 100%;\n aspect-ratio: 1 / 1;\n transform-style: preserve-3d;\n}\n\n.cube::before {\n content: '';\n position: absolute;\n top: -36px;\n right: -36px;\n bottom: -36px;\n left: -36px;\n}\n\n.default-animation .cube-face {\n position: absolute;\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--cube-face-bg);\n border: var(--cube-face-border);\n opacity: 1;\n}\n\n.default-animation .cube-face--top {\n transform: translateY(-50%) rotateX(90deg);\n}\n\n.default-animation .cube-face--bottom {\n transform: translateY(50%) rotateX(-90deg);\n}\n\n.default-animation .cube-face--left {\n transform: translateX(-50%) rotateY(-90deg);\n}\n\n.default-animation .cube-face--right {\n transform: translateX(50%) rotateY(90deg);\n}\n\n.default-animation .cube-face--back,\n.default-animation .cube-face--front {\n transform: rotateY(-90deg) translateX(50%) rotateY(90deg);\n}\n\n@media (max-width: 768px) {\n .default-animation {\n width: 90%;\n }\n}\n" }, { "type": "registry:component", - "path": "Cubes/Cubes.tsx", + "path": "Cubes.tsx", "content": "import React, { useCallback, useEffect, useRef } from 'react';\nimport gsap from 'gsap';\nimport './Cubes.css';\n\ninterface Gap {\n row: number;\n col: number;\n}\ninterface Duration {\n enter: number;\n leave: number;\n}\n\nexport interface CubesProps {\n gridSize?: number;\n cubeSize?: number;\n maxAngle?: number;\n radius?: number;\n easing?: gsap.EaseString;\n duration?: Duration;\n cellGap?: number | Gap;\n borderStyle?: string;\n faceColor?: string;\n shadow?: boolean | string;\n autoAnimate?: boolean;\n rippleOnClick?: boolean;\n rippleColor?: string;\n rippleSpeed?: number;\n}\n\nconst Cubes: React.FC = ({\n gridSize = 10,\n cubeSize,\n maxAngle = 45,\n radius = 3,\n easing = 'power3.out',\n duration = { enter: 0.3, leave: 0.6 },\n cellGap,\n borderStyle = '1px solid #fff',\n faceColor = '#120F17',\n shadow = false,\n autoAnimate = true,\n rippleOnClick = true,\n rippleColor = '#fff',\n rippleSpeed = 2\n}) => {\n const sceneRef = useRef(null);\n const rafRef = useRef(null);\n const idleTimerRef = useRef | null>(null);\n const userActiveRef = useRef(false);\n const simPosRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });\n const simTargetRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });\n const simRAFRef = useRef(null);\n\n const colGap =\n typeof cellGap === 'number'\n ? `${cellGap}px`\n : (cellGap as Gap)?.col !== undefined\n ? `${(cellGap as Gap).col}px`\n : '5%';\n const rowGap =\n typeof cellGap === 'number'\n ? `${cellGap}px`\n : (cellGap as Gap)?.row !== undefined\n ? `${(cellGap as Gap).row}px`\n : '5%';\n\n const enterDur = duration.enter;\n const leaveDur = duration.leave;\n\n const tiltAt = useCallback(\n (rowCenter: number, colCenter: number) => {\n if (!sceneRef.current) return;\n sceneRef.current.querySelectorAll('.cube').forEach(cube => {\n const r = +cube.dataset.row!;\n const c = +cube.dataset.col!;\n const dist = Math.hypot(r - rowCenter, c - colCenter);\n if (dist <= radius) {\n const pct = 1 - dist / radius;\n const angle = pct * maxAngle;\n gsap.to(cube, {\n duration: enterDur,\n ease: easing,\n overwrite: true,\n rotateX: -angle,\n rotateY: angle\n });\n } else {\n gsap.to(cube, {\n duration: leaveDur,\n ease: 'power3.out',\n overwrite: true,\n rotateX: 0,\n rotateY: 0\n });\n }\n });\n },\n [radius, maxAngle, enterDur, leaveDur, easing]\n );\n\n const onPointerMove = useCallback(\n (e: PointerEvent) => {\n userActiveRef.current = true;\n if (idleTimerRef.current) clearTimeout(idleTimerRef.current);\n\n const rect = sceneRef.current!.getBoundingClientRect();\n const cellW = rect.width / gridSize;\n const cellH = rect.height / gridSize;\n const colCenter = (e.clientX - rect.left) / cellW;\n const rowCenter = (e.clientY - rect.top) / cellH;\n\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n rafRef.current = requestAnimationFrame(() => tiltAt(rowCenter, colCenter));\n\n idleTimerRef.current = setTimeout(() => {\n userActiveRef.current = false;\n }, 3000);\n },\n [gridSize, tiltAt]\n );\n\n const resetAll = useCallback(() => {\n if (!sceneRef.current) return;\n sceneRef.current.querySelectorAll('.cube').forEach(cube =>\n gsap.to(cube, {\n duration: leaveDur,\n rotateX: 0,\n rotateY: 0,\n ease: 'power3.out'\n })\n );\n }, [leaveDur]);\n\n const onTouchMove = useCallback(\n (e: TouchEvent) => {\n e.preventDefault();\n userActiveRef.current = true;\n if (idleTimerRef.current) clearTimeout(idleTimerRef.current);\n\n const rect = sceneRef.current!.getBoundingClientRect();\n const cellW = rect.width / gridSize;\n const cellH = rect.height / gridSize;\n\n const touch = e.touches[0];\n const colCenter = (touch.clientX - rect.left) / cellW;\n const rowCenter = (touch.clientY - rect.top) / cellH;\n\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n rafRef.current = requestAnimationFrame(() => tiltAt(rowCenter, colCenter));\n\n idleTimerRef.current = setTimeout(() => {\n userActiveRef.current = false;\n }, 3000);\n },\n [gridSize, tiltAt]\n );\n\n const onTouchStart = useCallback(() => {\n userActiveRef.current = true;\n }, []);\n\n const onTouchEnd = useCallback(() => {\n if (!sceneRef.current) return;\n resetAll();\n }, [resetAll]);\n\n const onClick = useCallback(\n (e: MouseEvent | TouchEvent) => {\n if (!rippleOnClick || !sceneRef.current) return;\n const rect = sceneRef.current.getBoundingClientRect();\n const cellW = rect.width / gridSize;\n const cellH = rect.height / gridSize;\n\n const clientX = (e as MouseEvent).clientX || ((e as TouchEvent).touches && (e as TouchEvent).touches[0].clientX);\n const clientY = (e as MouseEvent).clientY || ((e as TouchEvent).touches && (e as TouchEvent).touches[0].clientY);\n\n const colHit = Math.floor((clientX - rect.left) / cellW);\n const rowHit = Math.floor((clientY - rect.top) / cellH);\n\n const baseRingDelay = 0.15;\n const baseAnimDur = 0.3;\n const baseHold = 0.6;\n\n const spreadDelay = baseRingDelay / rippleSpeed;\n const animDuration = baseAnimDur / rippleSpeed;\n const holdTime = baseHold / rippleSpeed;\n\n const rings: Record = {};\n sceneRef.current.querySelectorAll('.cube').forEach(cube => {\n const r = +cube.dataset.row!;\n const c = +cube.dataset.col!;\n const dist = Math.hypot(r - rowHit, c - colHit);\n const ring = Math.round(dist);\n if (!rings[ring]) rings[ring] = [];\n rings[ring].push(cube);\n });\n\n Object.keys(rings)\n .map(Number)\n .sort((a, b) => a - b)\n .forEach(ring => {\n const delay = ring * spreadDelay;\n const faces = rings[ring].flatMap(cube => Array.from(cube.querySelectorAll('.cube-face')));\n\n gsap.to(faces, {\n backgroundColor: rippleColor,\n duration: animDuration,\n delay,\n ease: 'power3.out'\n });\n gsap.to(faces, {\n backgroundColor: faceColor,\n duration: animDuration,\n delay: delay + animDuration + holdTime,\n ease: 'power3.out'\n });\n });\n },\n [rippleOnClick, gridSize, faceColor, rippleColor, rippleSpeed]\n );\n\n useEffect(() => {\n if (!autoAnimate || !sceneRef.current) return;\n simPosRef.current = {\n x: Math.random() * gridSize,\n y: Math.random() * gridSize\n };\n simTargetRef.current = {\n x: Math.random() * gridSize,\n y: Math.random() * gridSize\n };\n const speed = 0.02;\n const loop = () => {\n if (!userActiveRef.current) {\n const pos = simPosRef.current;\n const tgt = simTargetRef.current;\n pos.x += (tgt.x - pos.x) * speed;\n pos.y += (tgt.y - pos.y) * speed;\n tiltAt(pos.y, pos.x);\n if (Math.hypot(pos.x - tgt.x, pos.y - tgt.y) < 0.1) {\n simTargetRef.current = {\n x: Math.random() * gridSize,\n y: Math.random() * gridSize\n };\n }\n }\n simRAFRef.current = requestAnimationFrame(loop);\n };\n simRAFRef.current = requestAnimationFrame(loop);\n return () => {\n if (simRAFRef.current != null) {\n cancelAnimationFrame(simRAFRef.current);\n }\n };\n }, [autoAnimate, gridSize, tiltAt]);\n\n useEffect(() => {\n const el = sceneRef.current;\n if (!el) return;\n el.addEventListener('pointermove', onPointerMove);\n el.addEventListener('pointerleave', resetAll);\n el.addEventListener('click', onClick);\n\n el.addEventListener('touchmove', onTouchMove, { passive: false });\n el.addEventListener('touchstart', onTouchStart, { passive: true });\n el.addEventListener('touchend', onTouchEnd, { passive: true });\n\n return () => {\n el.removeEventListener('pointermove', onPointerMove);\n el.removeEventListener('pointerleave', resetAll);\n el.removeEventListener('click', onClick);\n\n el.removeEventListener('touchmove', onTouchMove);\n el.removeEventListener('touchstart', onTouchStart);\n el.removeEventListener('touchend', onTouchEnd);\n\n rafRef.current != null && cancelAnimationFrame(rafRef.current);\n idleTimerRef.current && clearTimeout(idleTimerRef.current);\n };\n }, [onPointerMove, resetAll, onClick, onTouchMove, onTouchStart, onTouchEnd]);\n\n const cells = Array.from({ length: gridSize });\n const sceneStyle: React.CSSProperties = {\n gridTemplateColumns: cubeSize ? `repeat(${gridSize}, ${cubeSize}px)` : `repeat(${gridSize}, 1fr)`,\n gridTemplateRows: cubeSize ? `repeat(${gridSize}, ${cubeSize}px)` : `repeat(${gridSize}, 1fr)`,\n columnGap: colGap,\n rowGap: rowGap\n };\n const wrapperStyle = {\n '--cube-face-border': borderStyle,\n '--cube-face-bg': faceColor,\n '--cube-face-shadow': shadow === true ? '0 0 6px rgba(0,0,0,.5)' : shadow || 'none',\n ...(cubeSize\n ? {\n width: `${gridSize * cubeSize}px`,\n height: `${gridSize * cubeSize}px`\n }\n : {})\n } as React.CSSProperties;\n\n return (\n
\n
\n {cells.map((_, r) =>\n cells.map((__, c) => (\n
\n
\n
\n
\n
\n
\n
\n
\n ))\n )}\n
\n
\n );\n};\n\nexport default Cubes;\n" } ], diff --git a/public/r/CursorGrid-JS-CSS.json b/public/r/CursorGrid-JS-CSS.json index 86aaaf5e..e5bab3ca 100644 --- a/public/r/CursorGrid-JS-CSS.json +++ b/public/r/CursorGrid-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "CursorGrid/CursorGrid.css", + "type": "registry:file", + "path": "CursorGrid.css", + "target": "@components/CursorGrid.css", "content": ".cursor-grid {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n\n.cursor-grid__canvas {\n display: block;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "CursorGrid/CursorGrid.jsx", + "path": "CursorGrid.jsx", "content": "import { useRef, useEffect } from 'react';\nimport './CursorGrid.css';\n\nconst FALLOFF_CURVES = {\n linear: t => t,\n smooth: t => t * t * (3 - 2 * t),\n sharp: t => t * t * t\n};\n\nconst hexToRgb = hex => {\n const h = hex.replace('#', '');\n const v = h.length === 3 ? h.split('').map(c => c + c).join('') : h;\n const num = parseInt(v.slice(0, 6), 16);\n return [(num >> 16) & 255, (num >> 8) & 255, num & 255];\n};\n\nconst CursorGrid = ({\n cellSize = 70,\n color = '#D946EF',\n radius = 140,\n falloff = 'smooth',\n holdTime = 400,\n fadeDuration = 800,\n lineWidth = 1.2,\n maxOpacity = 1,\n fillOpacity = 0,\n gridOpacity = 0,\n cellRadius = 0,\n clickPulse = true,\n pulseSpeed = 600,\n className = ''\n}) => {\n const containerRef = useRef(null);\n const canvasRef = useRef(null);\n const propsRef = useRef({});\n const wakeRef = useRef(null);\n\n propsRef.current = {\n cellSize,\n color,\n radius,\n falloff,\n holdTime,\n fadeDuration,\n lineWidth,\n maxOpacity,\n fillOpacity,\n gridOpacity,\n cellRadius,\n clickPulse,\n pulseSpeed\n };\n\n useEffect(() => {\n const container = containerRef.current;\n const canvas = canvasRef.current;\n if (!container || !canvas) return;\n\n const ctx = canvas.getContext('2d');\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n\n // Grid state: one alpha + timestamp pair per cell, indexed row-major.\n let cols = 0;\n let rows = 0;\n let offX = 0;\n let offY = 0;\n let alphas = new Float32Array(0);\n let touched = new Float64Array(0);\n let w = 0;\n let h = 0;\n const pulses = [];\n let raf = 0;\n let running = false;\n let lastFrame = 0;\n\n const rebuild = () => {\n const p = propsRef.current;\n w = container.offsetWidth;\n h = container.offsetHeight;\n canvas.width = Math.max(1, Math.round(w * dpr));\n canvas.height = Math.max(1, Math.round(h * dpr));\n canvas.style.width = `${w}px`;\n canvas.style.height = `${h}px`;\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n cols = Math.ceil(w / p.cellSize) + 1;\n rows = Math.ceil(h / p.cellSize) + 1;\n // Center the lattice so edge cells crop evenly on both sides\n offX = (w - cols * p.cellSize) / 2;\n offY = (h - rows * p.cellSize) / 2;\n alphas = new Float32Array(cols * rows);\n touched = new Float64Array(cols * rows);\n };\n\n const cellCenter = i => {\n const p = propsRef.current;\n const cx = offX + (i % cols) * p.cellSize + p.cellSize / 2;\n const cy = offY + Math.floor(i / cols) * p.cellSize + p.cellSize / 2;\n return [cx, cy];\n };\n\n // Light up every cell whose center falls inside the radius, with the\n // configured falloff curve mapping distance to brightness.\n const energize = (x, y, boost) => {\n const p = propsRef.current;\n const r = Math.max(p.radius, 1);\n const ease = FALLOFF_CURVES[p.falloff] ?? FALLOFF_CURVES.linear;\n const now = performance.now();\n const minCol = Math.max(0, Math.floor((x - r - offX) / p.cellSize));\n const maxCol = Math.min(cols - 1, Math.floor((x + r - offX) / p.cellSize));\n const minRow = Math.max(0, Math.floor((y - r - offY) / p.cellSize));\n const maxRow = Math.min(rows - 1, Math.floor((y + r - offY) / p.cellSize));\n for (let cRow = minRow; cRow <= maxRow; cRow++) {\n for (let cCol = minCol; cCol <= maxCol; cCol++) {\n const i = cRow * cols + cCol;\n const [cx, cy] = cellCenter(i);\n const dist = Math.hypot(cx - x, cy - y);\n if (dist > r) continue;\n const level = ease(1 - dist / r) * p.maxOpacity * (boost ?? 1);\n if (level > alphas[i]) {\n alphas[i] = level;\n touched[i] = now;\n } else if (level > 0) {\n touched[i] = now;\n }\n }\n }\n };\n\n const draw = now => {\n const p = propsRef.current;\n const dt = Math.min(now - lastFrame, 50);\n lastFrame = now;\n ctx.clearRect(0, 0, w, h);\n const [cr, cg, cb] = hexToRgb(p.color);\n\n // Optional faint static lattice\n if (p.gridOpacity > 0) {\n ctx.strokeStyle = `rgba(${cr}, ${cg}, ${cb}, ${p.gridOpacity})`;\n ctx.lineWidth = 1;\n ctx.beginPath();\n for (let cCol = 0; cCol <= cols; cCol++) {\n const x = Math.round(offX + cCol * p.cellSize) + 0.5;\n ctx.moveTo(x, 0);\n ctx.lineTo(x, h);\n }\n for (let cRow = 0; cRow <= rows; cRow++) {\n const y = Math.round(offY + cRow * p.cellSize) + 0.5;\n ctx.moveTo(0, y);\n ctx.lineTo(w, y);\n }\n ctx.stroke();\n }\n\n // Expanding click pulses hand their energy to cells as they pass\n for (let pi = pulses.length - 1; pi >= 0; pi--) {\n const pulse = pulses[pi];\n const age = (now - pulse.t0) / 1000;\n const ringR = age * p.pulseSpeed;\n if (ringR > Math.hypot(w, h)) {\n pulses.splice(pi, 1);\n continue;\n }\n const band = p.cellSize;\n const minCol = Math.max(0, Math.floor((pulse.x - ringR - band - offX) / p.cellSize));\n const maxCol = Math.min(cols - 1, Math.floor((pulse.x + ringR + band - offX) / p.cellSize));\n const minRow = Math.max(0, Math.floor((pulse.y - ringR - band - offY) / p.cellSize));\n const maxRow = Math.min(rows - 1, Math.floor((pulse.y + ringR + band - offY) / p.cellSize));\n for (let cRow = minRow; cRow <= maxRow; cRow++) {\n for (let cCol = minCol; cCol <= maxCol; cCol++) {\n const i = cRow * cols + cCol;\n const [cx, cy] = cellCenter(i);\n const dist = Math.hypot(cx - pulse.x, cy - pulse.y);\n if (Math.abs(dist - ringR) < band / 2 && p.maxOpacity > alphas[i]) {\n alphas[i] = p.maxOpacity;\n touched[i] = now;\n }\n }\n }\n }\n\n let anyVisible = pulses.length > 0;\n const fadeStep = dt / Math.max(p.fadeDuration, 16);\n const half = p.cellSize / 2;\n\n for (let i = 0; i < alphas.length; i++) {\n let a = alphas[i];\n if (a <= 0) continue;\n if (now - touched[i] > p.holdTime) {\n a = Math.max(0, a - fadeStep);\n alphas[i] = a;\n if (a <= 0) continue;\n }\n anyVisible = true;\n\n const [cx, cy] = cellCenter(i);\n const gradient = ctx.createRadialGradient(cx, cy, half * 0.1, cx, cy, p.cellSize);\n gradient.addColorStop(0, `rgba(${cr}, ${cg}, ${cb}, ${a})`);\n gradient.addColorStop(1, `rgba(${cr}, ${cg}, ${cb}, 0)`);\n\n const x = cx - half + 0.5;\n const y = cy - half + 0.5;\n const s = p.cellSize - 1;\n\n ctx.beginPath();\n if (p.cellRadius > 0) {\n ctx.roundRect(x, y, s, s, p.cellRadius);\n } else {\n ctx.rect(x, y, s, s);\n }\n if (p.fillOpacity > 0) {\n ctx.fillStyle = `rgba(${cr}, ${cg}, ${cb}, ${a * p.fillOpacity})`;\n ctx.fill();\n }\n ctx.strokeStyle = gradient;\n ctx.lineWidth = p.lineWidth;\n ctx.stroke();\n }\n\n if (anyVisible) {\n raf = requestAnimationFrame(draw);\n } else {\n running = false;\n if (propsRef.current.gridOpacity <= 0) ctx.clearRect(0, 0, w, h);\n }\n };\n\n const wake = () => {\n if (running) return;\n running = true;\n lastFrame = performance.now();\n raf = requestAnimationFrame(draw);\n };\n wakeRef.current = wake;\n\n const toLocal = e => {\n const rect = canvas.getBoundingClientRect();\n return [e.clientX - rect.left, e.clientY - rect.top];\n };\n\n const onPointerMove = e => {\n const [x, y] = toLocal(e);\n energize(x, y);\n wake();\n };\n\n const onPointerDown = e => {\n if (!propsRef.current.clickPulse) return;\n const [x, y] = toLocal(e);\n pulses.push({ x, y, t0: performance.now() });\n wake();\n };\n\n const ro = new ResizeObserver(() => {\n rebuild();\n wake();\n });\n ro.observe(container);\n rebuild();\n wake();\n\n container.addEventListener('pointermove', onPointerMove);\n container.addEventListener('pointerdown', onPointerDown);\n\n return () => {\n cancelAnimationFrame(raf);\n ro.disconnect();\n container.removeEventListener('pointermove', onPointerMove);\n container.removeEventListener('pointerdown', onPointerDown);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [cellSize]);\n\n // Repaint static layers when visual props change while idle\n useEffect(() => {\n wakeRef.current?.();\n }, [gridOpacity, color, lineWidth, maxOpacity, fillOpacity, cellRadius]);\n\n return (\n
\n \n
\n );\n};\n\nexport default CursorGrid;\n" } ], diff --git a/public/r/CursorGrid-TS-CSS.json b/public/r/CursorGrid-TS-CSS.json index 793e40f4..515922b8 100644 --- a/public/r/CursorGrid-TS-CSS.json +++ b/public/r/CursorGrid-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "CursorGrid/CursorGrid.css", + "type": "registry:file", + "path": "CursorGrid.css", + "target": "@components/CursorGrid.css", "content": ".cursor-grid {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n\n.cursor-grid__canvas {\n display: block;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "CursorGrid/CursorGrid.tsx", + "path": "CursorGrid.tsx", "content": "import { useRef, useEffect } from 'react';\nimport './CursorGrid.css';\n\ntype Falloff = 'linear' | 'smooth' | 'sharp';\n\nexport interface CursorGridProps {\n cellSize?: number;\n color?: string;\n radius?: number;\n falloff?: Falloff;\n holdTime?: number;\n fadeDuration?: number;\n lineWidth?: number;\n maxOpacity?: number;\n fillOpacity?: number;\n gridOpacity?: number;\n cellRadius?: number;\n clickPulse?: boolean;\n pulseSpeed?: number;\n className?: string;\n}\n\ninterface GridConfig {\n cellSize: number;\n color: string;\n radius: number;\n falloff: Falloff;\n holdTime: number;\n fadeDuration: number;\n lineWidth: number;\n maxOpacity: number;\n fillOpacity: number;\n gridOpacity: number;\n cellRadius: number;\n clickPulse: boolean;\n pulseSpeed: number;\n}\n\ninterface Pulse {\n x: number;\n y: number;\n t0: number;\n}\n\nconst FALLOFF_CURVES: Record number> = {\n linear: t => t,\n smooth: t => t * t * (3 - 2 * t),\n sharp: t => t * t * t\n};\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n const h = hex.replace('#', '');\n const v = h.length === 3 ? h.split('').map(c => c + c).join('') : h;\n const num = parseInt(v.slice(0, 6), 16);\n return [(num >> 16) & 255, (num >> 8) & 255, num & 255];\n};\n\nconst CursorGrid = ({\n cellSize = 70,\n color = '#D946EF',\n radius = 140,\n falloff = 'smooth',\n holdTime = 400,\n fadeDuration = 800,\n lineWidth = 1.2,\n maxOpacity = 1,\n fillOpacity = 0,\n gridOpacity = 0,\n cellRadius = 0,\n clickPulse = true,\n pulseSpeed = 600,\n className = ''\n}: CursorGridProps) => {\n const containerRef = useRef(null);\n const canvasRef = useRef(null);\n const propsRef = useRef({} as GridConfig);\n const wakeRef = useRef<(() => void) | null>(null);\n\n propsRef.current = {\n cellSize,\n color,\n radius,\n falloff,\n holdTime,\n fadeDuration,\n lineWidth,\n maxOpacity,\n fillOpacity,\n gridOpacity,\n cellRadius,\n clickPulse,\n pulseSpeed\n };\n\n useEffect(() => {\n const container = containerRef.current;\n const canvas = canvasRef.current;\n if (!container || !canvas) return;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n\n // Grid state: one alpha + timestamp pair per cell, indexed row-major.\n let cols = 0;\n let rows = 0;\n let offX = 0;\n let offY = 0;\n let alphas = new Float32Array(0);\n let touched = new Float64Array(0);\n let w = 0;\n let h = 0;\n const pulses: Pulse[] = [];\n let raf = 0;\n let running = false;\n let lastFrame = 0;\n\n const rebuild = () => {\n const p = propsRef.current;\n w = container.offsetWidth;\n h = container.offsetHeight;\n canvas.width = Math.max(1, Math.round(w * dpr));\n canvas.height = Math.max(1, Math.round(h * dpr));\n canvas.style.width = `${w}px`;\n canvas.style.height = `${h}px`;\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n cols = Math.ceil(w / p.cellSize) + 1;\n rows = Math.ceil(h / p.cellSize) + 1;\n // Center the lattice so edge cells crop evenly on both sides\n offX = (w - cols * p.cellSize) / 2;\n offY = (h - rows * p.cellSize) / 2;\n alphas = new Float32Array(cols * rows);\n touched = new Float64Array(cols * rows);\n };\n\n const cellCenter = (i: number): [number, number] => {\n const p = propsRef.current;\n const cx = offX + (i % cols) * p.cellSize + p.cellSize / 2;\n const cy = offY + Math.floor(i / cols) * p.cellSize + p.cellSize / 2;\n return [cx, cy];\n };\n\n // Light up every cell whose center falls inside the radius, with the\n // configured falloff curve mapping distance to brightness.\n const energize = (x: number, y: number, boost?: number) => {\n const p = propsRef.current;\n const r = Math.max(p.radius, 1);\n const ease = FALLOFF_CURVES[p.falloff] ?? FALLOFF_CURVES.linear;\n const now = performance.now();\n const minCol = Math.max(0, Math.floor((x - r - offX) / p.cellSize));\n const maxCol = Math.min(cols - 1, Math.floor((x + r - offX) / p.cellSize));\n const minRow = Math.max(0, Math.floor((y - r - offY) / p.cellSize));\n const maxRow = Math.min(rows - 1, Math.floor((y + r - offY) / p.cellSize));\n for (let cRow = minRow; cRow <= maxRow; cRow++) {\n for (let cCol = minCol; cCol <= maxCol; cCol++) {\n const i = cRow * cols + cCol;\n const [cx, cy] = cellCenter(i);\n const dist = Math.hypot(cx - x, cy - y);\n if (dist > r) continue;\n const level = ease(1 - dist / r) * p.maxOpacity * (boost ?? 1);\n if (level > alphas[i]) {\n alphas[i] = level;\n touched[i] = now;\n } else if (level > 0) {\n touched[i] = now;\n }\n }\n }\n };\n\n const draw = (now: number) => {\n const p = propsRef.current;\n const dt = Math.min(now - lastFrame, 50);\n lastFrame = now;\n ctx.clearRect(0, 0, w, h);\n const [cr, cg, cb] = hexToRgb(p.color);\n\n // Optional faint static lattice\n if (p.gridOpacity > 0) {\n ctx.strokeStyle = `rgba(${cr}, ${cg}, ${cb}, ${p.gridOpacity})`;\n ctx.lineWidth = 1;\n ctx.beginPath();\n for (let cCol = 0; cCol <= cols; cCol++) {\n const x = Math.round(offX + cCol * p.cellSize) + 0.5;\n ctx.moveTo(x, 0);\n ctx.lineTo(x, h);\n }\n for (let cRow = 0; cRow <= rows; cRow++) {\n const y = Math.round(offY + cRow * p.cellSize) + 0.5;\n ctx.moveTo(0, y);\n ctx.lineTo(w, y);\n }\n ctx.stroke();\n }\n\n // Expanding click pulses hand their energy to cells as they pass\n for (let pi = pulses.length - 1; pi >= 0; pi--) {\n const pulse = pulses[pi];\n const age = (now - pulse.t0) / 1000;\n const ringR = age * p.pulseSpeed;\n if (ringR > Math.hypot(w, h)) {\n pulses.splice(pi, 1);\n continue;\n }\n const band = p.cellSize;\n const minCol = Math.max(0, Math.floor((pulse.x - ringR - band - offX) / p.cellSize));\n const maxCol = Math.min(cols - 1, Math.floor((pulse.x + ringR + band - offX) / p.cellSize));\n const minRow = Math.max(0, Math.floor((pulse.y - ringR - band - offY) / p.cellSize));\n const maxRow = Math.min(rows - 1, Math.floor((pulse.y + ringR + band - offY) / p.cellSize));\n for (let cRow = minRow; cRow <= maxRow; cRow++) {\n for (let cCol = minCol; cCol <= maxCol; cCol++) {\n const i = cRow * cols + cCol;\n const [cx, cy] = cellCenter(i);\n const dist = Math.hypot(cx - pulse.x, cy - pulse.y);\n if (Math.abs(dist - ringR) < band / 2 && p.maxOpacity > alphas[i]) {\n alphas[i] = p.maxOpacity;\n touched[i] = now;\n }\n }\n }\n }\n\n let anyVisible = pulses.length > 0;\n const fadeStep = dt / Math.max(p.fadeDuration, 16);\n const half = p.cellSize / 2;\n\n for (let i = 0; i < alphas.length; i++) {\n let a = alphas[i];\n if (a <= 0) continue;\n if (now - touched[i] > p.holdTime) {\n a = Math.max(0, a - fadeStep);\n alphas[i] = a;\n if (a <= 0) continue;\n }\n anyVisible = true;\n\n const [cx, cy] = cellCenter(i);\n const gradient = ctx.createRadialGradient(cx, cy, half * 0.1, cx, cy, p.cellSize);\n gradient.addColorStop(0, `rgba(${cr}, ${cg}, ${cb}, ${a})`);\n gradient.addColorStop(1, `rgba(${cr}, ${cg}, ${cb}, 0)`);\n\n const x = cx - half + 0.5;\n const y = cy - half + 0.5;\n const s = p.cellSize - 1;\n\n ctx.beginPath();\n if (p.cellRadius > 0) {\n ctx.roundRect(x, y, s, s, p.cellRadius);\n } else {\n ctx.rect(x, y, s, s);\n }\n if (p.fillOpacity > 0) {\n ctx.fillStyle = `rgba(${cr}, ${cg}, ${cb}, ${a * p.fillOpacity})`;\n ctx.fill();\n }\n ctx.strokeStyle = gradient;\n ctx.lineWidth = p.lineWidth;\n ctx.stroke();\n }\n\n if (anyVisible) {\n raf = requestAnimationFrame(draw);\n } else {\n running = false;\n if (propsRef.current.gridOpacity <= 0) ctx.clearRect(0, 0, w, h);\n }\n };\n\n const wake = () => {\n if (running) return;\n running = true;\n lastFrame = performance.now();\n raf = requestAnimationFrame(draw);\n };\n wakeRef.current = wake;\n\n const toLocal = (e: PointerEvent): [number, number] => {\n const rect = canvas.getBoundingClientRect();\n return [e.clientX - rect.left, e.clientY - rect.top];\n };\n\n const onPointerMove = (e: PointerEvent) => {\n const [x, y] = toLocal(e);\n energize(x, y);\n wake();\n };\n\n const onPointerDown = (e: PointerEvent) => {\n if (!propsRef.current.clickPulse) return;\n const [x, y] = toLocal(e);\n pulses.push({ x, y, t0: performance.now() });\n wake();\n };\n\n const ro = new ResizeObserver(() => {\n rebuild();\n wake();\n });\n ro.observe(container);\n rebuild();\n wake();\n\n container.addEventListener('pointermove', onPointerMove);\n container.addEventListener('pointerdown', onPointerDown);\n\n return () => {\n cancelAnimationFrame(raf);\n ro.disconnect();\n container.removeEventListener('pointermove', onPointerMove);\n container.removeEventListener('pointerdown', onPointerDown);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [cellSize]);\n\n // Repaint static layers when visual props change while idle\n useEffect(() => {\n wakeRef.current?.();\n }, [gridOpacity, color, lineWidth, maxOpacity, fillOpacity, cellRadius]);\n\n return (\n
\n \n
\n );\n};\n\nexport default CursorGrid;\n" } ], diff --git a/public/r/CurvedInput-JS-CSS.json b/public/r/CurvedInput-JS-CSS.json index ccc5603d..f5f93ad3 100644 --- a/public/r/CurvedInput-JS-CSS.json +++ b/public/r/CurvedInput-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "CurvedInput/CurvedInput.css", + "type": "registry:file", + "path": "CurvedInput.css", + "target": "@components/CurvedInput.css", "content": ".curved-input {\n position: relative;\n display: block;\n width: 100%;\n max-width: 100%;\n margin: 0;\n}\n\n.curved-input__svg {\n display: block;\n overflow: visible;\n width: 100%;\n height: auto;\n cursor: text;\n user-select: none;\n -webkit-user-select: none;\n -webkit-tap-highlight-color: transparent;\n}\n\n.curved-input__svg text {\n font-family: inherit;\n}\n\n.curved-input__ring {\n opacity: 0;\n transition: opacity 0.25s ease;\n}\n\n.curved-input--focused .curved-input__ring {\n opacity: 0.28;\n}\n\n.curved-input__button {\n cursor: pointer;\n outline: none;\n}\n\n.curved-input__button-bg {\n transition:\n filter 0.2s ease,\n opacity 0.2s ease;\n}\n\n.curved-input__button:hover .curved-input__button-bg {\n filter: brightness(1.12);\n}\n\n.curved-input__button:active .curved-input__button-bg {\n filter: brightness(0.94);\n}\n\n.curved-input__button:focus-visible .curved-input__button-bg {\n filter: brightness(1.18);\n}\n\n.curved-input__field {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n opacity: 0;\n border: 0;\n padding: 0;\n margin: 0;\n background: transparent;\n color: transparent;\n caret-color: transparent;\n font-size: 16px;\n pointer-events: none;\n outline: none;\n}\n" }, { "type": "registry:component", - "path": "CurvedInput/CurvedInput.jsx", + "path": "CurvedInput.jsx", "content": "import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport './CurvedInput.css';\n\nconst DEG = 180 / Math.PI;\n\nconst round2 = n => Math.round(n * 100) / 100;\n\nconst hexToRgba = (hex, alpha) => {\n let h = String(hex).replace('#', '');\n if (h.length === 3)\n h = h\n .split('')\n .map(c => c + c)\n .join('');\n const n = parseInt(h.slice(0, 6), 16);\n if (Number.isNaN(n)) return hex;\n return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;\n};\n\nconst SHADOWS = { sm: [5, 12, 0.3], md: [10, 24, 0.4], lg: [16, 40, 0.52] };\n\nconst THEMES = {\n dark: {\n backgroundColor: '#1B1722',\n textColor: '#f5f5f5',\n placeholderColor: '#a1a1aa',\n borderColor: '#392e4e',\n buttonColor: '#A855F7',\n buttonTextColor: '#ffffff',\n shadowColor: '#000000'\n },\n light: {\n backgroundColor: '#ffffff',\n textColor: '#1d2050',\n placeholderColor: '#9aa0b6',\n borderColor: '#262a56',\n buttonColor: '#4763eb',\n buttonTextColor: '#ffffff',\n shadowColor: '#0b0e2a'\n }\n};\n\n// Maps the flat coordinate space (u: 0..W along the bar, v: offset from the\n// centerline, positive down) onto a circular arc with the given sagitta\n// (`bend`, in px). Positive bend arches up, negative sags down, 0 is flat.\nconst buildGeometry = (width, bend, thickness, pad) => {\n const W = width;\n const T = thickness;\n const s = Math.max(-W * 0.35, Math.min(bend, W * 0.35));\n const a = Math.abs(s);\n const dir = s >= 0 ? 1 : -1;\n const svgH = T + a + pad * 2;\n\n if (a < 0.75) {\n const midY = pad + T / 2;\n return {\n straight: true,\n W,\n T,\n svgH,\n uPerLen: 1,\n point: (u, v) => [u, midY + v],\n angleAt: () => 0,\n uFromPoint: x => x\n };\n }\n\n const R = (W * W * 0.25 + a * a) / (2 * a);\n const cx = W / 2;\n const apexY = pad + T / 2 + (dir > 0 ? 0 : a);\n const cy = apexY + dir * R;\n const phi = Math.asin(Math.min(1, W / (2 * R)));\n\n return {\n straight: false,\n W,\n T,\n svgH,\n R,\n dir,\n uPerLen: W / (2 * R * phi),\n point: (u, v) => {\n const th = ((u - cx) / cx) * phi;\n const rho = R - dir * v;\n return [cx + rho * Math.sin(th), cy - dir * rho * Math.cos(th)];\n },\n angleAt: u => dir * ((u - cx) / cx) * phi * DEG,\n uFromPoint: (x, y) => {\n const th = Math.atan2(x - cx, dir * (cy - y));\n return cx + (th / phi) * cx;\n }\n };\n};\n\nconst fmt = (g, u, v) => {\n const [x, y] = g.point(u, v);\n return `${round2(x)} ${round2(y)}`;\n};\n\n// Segment along a constant-v edge, as a circular arc (or a line when flat)\nconst edgeSeg = (g, uTo, v, ltr) => {\n if (g.straight) return `L ${fmt(g, uTo, v)}`;\n const rho = round2(g.R - g.dir * v);\n const sweep = ltr === g.dir > 0 ? 1 : 0;\n return `A ${rho} ${rho} 0 0 ${sweep} ${fmt(g, uTo, v)}`;\n};\n\n// A rectangle bent along the arc: circular top/bottom edges, radial end caps\n// and quadratic rounded corners.\nconst bentRectPath = (g, u0, u1, vTop, vBot, radius) => {\n const rc = Math.max(0, Math.min(radius, (vBot - vTop) / 2, (u1 - u0) / 2));\n return [\n `M ${fmt(g, u0 + rc, vTop)}`,\n edgeSeg(g, u1 - rc, vTop, true),\n `Q ${fmt(g, u1, vTop)} ${fmt(g, u1, vTop + rc)}`,\n `L ${fmt(g, u1, vBot - rc)}`,\n `Q ${fmt(g, u1, vBot)} ${fmt(g, u1 - rc, vBot)}`,\n edgeSeg(g, u0 + rc, vBot, false),\n `Q ${fmt(g, u0, vBot)} ${fmt(g, u0, vBot - rc)}`,\n `L ${fmt(g, u0, vTop + rc)}`,\n `Q ${fmt(g, u0, vTop)} ${fmt(g, u0 + rc, vTop)}`,\n 'Z'\n ].join(' ');\n};\n\nconst bentLinePath = (g, u0, u1, v) => `M ${fmt(g, u0, v)} ${edgeSeg(g, u1, v, true)}`;\n\nconst SELECTABLE_TYPES = ['text', 'search', 'tel', 'url', 'password'];\n\nconst CurvedInput = ({\n value,\n defaultValue = '',\n onChange,\n onSubmit,\n placeholder = 'Enter your email',\n buttonText = 'Get Started',\n type = 'email',\n name,\n ariaLabel,\n theme = 'dark',\n width = 450,\n bend = 28,\n height = 64,\n cornerRadius = 18,\n borderWidth = 1.5,\n fontSize = 16,\n backgroundColor,\n textColor,\n placeholderColor,\n borderColor,\n buttonColor,\n buttonTextColor,\n iconColor,\n shadowSize = 'md',\n shadowColor,\n showButton = true,\n showIcon = true,\n icon,\n className = '',\n style\n}) => {\n const uid = useId().replace(/:/g, '');\n const layoutPathId = `ci-text-${uid}`;\n const buttonPathId = `ci-btn-${uid}`;\n const clipId = `ci-clip-${uid}`;\n\n const rootRef = useRef(null);\n const svgRef = useRef(null);\n const inputRef = useRef(null);\n const textRef = useRef(null);\n const btnMeasureRef = useRef(null);\n const scrollRef = useRef(0);\n\n const [w, setW] = useState(0);\n const [innerValue, setInnerValue] = useState(defaultValue);\n const [caretIndex, setCaretIndex] = useState(defaultValue.length);\n const [focused, setFocused] = useState(false);\n const [caretU, setCaretU] = useState(0);\n const [scrollLen, setScrollLen] = useState(0);\n const [btnTextW, setBtnTextW] = useState(0);\n const [, setFontTick] = useState(0);\n\n const val = value !== undefined ? value : innerValue;\n const display = type === 'password' ? '•'.repeat(val.length) : val;\n\n const palette = THEMES[theme] || THEMES.dark;\n const bgColor = backgroundColor ?? palette.backgroundColor;\n const fgColor = textColor ?? palette.textColor;\n const phColor = placeholderColor ?? palette.placeholderColor;\n const strokeColor = borderColor ?? palette.borderColor;\n const accentColor = buttonColor ?? palette.buttonColor;\n const btnFgColor = buttonTextColor ?? palette.buttonTextColor;\n const shColor = shadowColor ?? palette.shadowColor;\n\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const ro = new ResizeObserver(entries => {\n const cw = entries[0]?.contentRect?.width ?? el.clientWidth;\n setW(Math.round(cw));\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n // Re-measure once webfonts finish loading\n useEffect(() => {\n let alive = true;\n if (document.fonts?.ready) {\n document.fonts.ready.then(() => {\n if (alive) setFontTick(t => t + 1);\n });\n }\n return () => {\n alive = false;\n };\n }, []);\n\n const pad = Math.ceil(borderWidth / 2) + 6;\n const geom = useMemo(() => (w > 2 ? buildGeometry(w, bend, height, pad) : null), [w, bend, height, pad]);\n\n const layout = useMemo(() => {\n if (!geom) return null;\n const T = height;\n const btnInset = Math.max(5, borderWidth + 4);\n const chipH = Math.min(34, Math.max(16, T * 0.34));\n const chipW = chipH * 1.25;\n const iconU = 22 + chipW / 2;\n const textStartU = showIcon ? 22 + chipW + 13 : 24;\n const btnW = showButton ? Math.max(btnTextW + fontSize * 2.7, T * 1.35) : 0;\n const btnU1 = geom.W - btnInset;\n const btnU0 = btnU1 - btnW;\n const textEndU = Math.max(textStartU + 20, showButton ? btnU0 - 14 : geom.W - 24);\n const winLen = (textEndU - textStartU) / geom.uPerLen;\n return { btnInset, chipH, chipW, iconU, textStartU, textEndU, btnU0, btnU1, winLen };\n }, [geom, height, borderWidth, btnTextW, fontSize, showIcon, showButton]);\n\n // Measure rendered text to keep the caret on the curve and scroll long\n // values along the arc, exactly like a native input would.\n useLayoutEffect(() => {\n if (btnMeasureRef.current) {\n const bw = btnMeasureRef.current.getComputedTextLength();\n setBtnTextW(prev => (Math.abs(prev - bw) > 0.5 ? bw : prev));\n }\n if (!geom || !layout) return;\n const textEl = textRef.current;\n const caret = Math.min(caretIndex, display.length);\n let caretLen = 0;\n let totalLen = 0;\n if (textEl && display.length) {\n try {\n totalLen = textEl.getSubStringLength(0, display.length);\n caretLen = caret > 0 ? textEl.getSubStringLength(0, caret) : 0;\n } catch {\n totalLen = 0;\n caretLen = 0;\n }\n }\n let next = scrollRef.current;\n if (caretLen - next > layout.winLen - 2) next = caretLen - layout.winLen + 2;\n if (caretLen - next < 0) next = caretLen;\n if (totalLen - next < layout.winLen) next = Math.max(0, totalLen - layout.winLen);\n next = Math.max(0, next);\n if (Math.abs(next - scrollRef.current) > 0.5) {\n scrollRef.current = next;\n setScrollLen(next);\n }\n setCaretU(layout.textStartU + (caretLen - next) * geom.uPerLen);\n });\n\n const commitValue = v => {\n if (value === undefined) setInnerValue(v);\n onChange?.(v);\n };\n\n const handleInputChange = e => {\n commitValue(e.target.value);\n setCaretIndex(e.target.selectionStart ?? e.target.value.length);\n };\n\n const handleSelect = e => {\n setCaretIndex(e.target.selectionStart ?? e.target.value.length);\n };\n\n const handleSubmit = e => {\n if (e?.preventDefault) e.preventDefault();\n if (onSubmit) onSubmit(val);\n };\n\n // Click on the curve: focus the hidden input and drop the caret on the\n // character closest to the click, measured in arc length.\n const handleSurfaceClick = e => {\n const input = inputRef.current;\n if (!input) return;\n let idx = display.length;\n const svg = svgRef.current;\n const textEl = textRef.current;\n if (svg && geom && layout && textEl && display.length) {\n try {\n const pt = new DOMPoint(e.clientX, e.clientY).matrixTransform(svg.getScreenCTM().inverse());\n const target = scrollRef.current + (geom.uFromPoint(pt.x, pt.y) - layout.textStartU) / geom.uPerLen;\n let best = 0;\n let bestDist = Infinity;\n for (let i = 0; i <= display.length; i++) {\n const li = i === 0 ? 0 : textEl.getSubStringLength(0, i);\n const d = Math.abs(li - target);\n if (d < bestDist) {\n bestDist = d;\n best = i;\n }\n }\n idx = best;\n } catch {\n idx = display.length;\n }\n }\n input.focus();\n try {\n input.setSelectionRange(idx, idx);\n } catch {\n /* selection API unavailable for this input type */\n }\n setCaretIndex(idx);\n };\n\n const safeType = SELECTABLE_TYPES.includes(type) ? type : 'text';\n const inputMode = type === 'email' ? 'email' : type === 'number' ? 'decimal' : undefined;\n\n const shadow = SHADOWS[shadowSize];\n const svgStyle = shadow\n ? { filter: `drop-shadow(0 ${shadow[0]}px ${shadow[1]}px ${hexToRgba(shColor, shadow[2])})` }\n : undefined;\n\n let content = null;\n if (geom && layout) {\n const T = height;\n const vBase = fontSize * 0.34;\n const scrollU = scrollLen * geom.uPerLen;\n const bandPath = bentRectPath(geom, 0, geom.W, -T / 2, T / 2, cornerRadius);\n const layoutPath = bentLinePath(geom, layout.textStartU - scrollU, geom.W, vBase);\n const clipPath = bentRectPath(geom, layout.textStartU - 6, layout.textEndU + 8, -T / 2, T / 2, 0);\n\n const chipFill = iconColor || accentColor;\n const { chipW, chipH } = layout;\n const ew = chipW * 0.5;\n const eh = chipH * 0.5;\n const sw = Math.max(1.1, chipH * 0.075);\n const [ix, iy] = geom.point(layout.iconU, 0);\n const iconAngle = geom.angleAt(layout.iconU);\n\n const [caretX, caretY] = geom.point(caretU, 0);\n const caretAngle = geom.angleAt(caretU);\n const caretH = Math.min(T * 0.58, fontSize * 1.45);\n\n const btnH = T - layout.btnInset * 2;\n const buttonPath = showButton\n ? bentRectPath(geom, layout.btnU0, layout.btnU1, -T / 2 + layout.btnInset, T / 2 - layout.btnInset, Math.min(cornerRadius * 0.72, btnH / 2))\n : '';\n const buttonTextPath = showButton ? bentLinePath(geom, layout.btnU0, layout.btnU1, vBase) : '';\n\n content = (\n e.preventDefault()}\n onClick={handleSurfaceClick}\n >\n \n \n \n \n \n\n \n \n\n \n\n {showIcon && (\n \n {icon || (\n <>\n \n \n \n \n )}\n \n )}\n\n \n \n {display}\n \n {!display && placeholder && (\n \n {placeholder}\n \n )}\n {focused && (\n \n \n \n \n \n )}\n \n\n {showButton && (\n {\n e.stopPropagation();\n handleSubmit();\n }}\n onPointerDown={e => e.stopPropagation()}\n onKeyDown={e => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n handleSubmit();\n }\n }}\n >\n \n \n \n \n {buttonText}\n \n \n \n )}\n\n \n {buttonText}\n \n \n );\n }\n\n return (\n \n {content}\n setFocused(true)}\n onBlur={() => setFocused(false)}\n aria-label={ariaLabel || placeholder || 'Curved input'}\n autoComplete=\"off\"\n autoCapitalize=\"none\"\n autoCorrect=\"off\"\n spellCheck={false}\n />\n \n );\n};\n\nexport default CurvedInput;\n" } ], diff --git a/public/r/CurvedInput-TS-CSS.json b/public/r/CurvedInput-TS-CSS.json index 0d141bfa..b0c246d3 100644 --- a/public/r/CurvedInput-TS-CSS.json +++ b/public/r/CurvedInput-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "CurvedInput/CurvedInput.css", + "type": "registry:file", + "path": "CurvedInput.css", + "target": "@components/CurvedInput.css", "content": ".curved-input {\n position: relative;\n display: block;\n width: 100%;\n max-width: 100%;\n margin: 0;\n}\n\n.curved-input__svg {\n display: block;\n overflow: visible;\n width: 100%;\n height: auto;\n cursor: text;\n user-select: none;\n -webkit-user-select: none;\n -webkit-tap-highlight-color: transparent;\n}\n\n.curved-input__svg text {\n font-family: inherit;\n}\n\n.curved-input__ring {\n opacity: 0;\n transition: opacity 0.25s ease;\n}\n\n.curved-input--focused .curved-input__ring {\n opacity: 0.28;\n}\n\n.curved-input__button {\n cursor: pointer;\n outline: none;\n}\n\n.curved-input__button-bg {\n transition:\n filter 0.2s ease,\n opacity 0.2s ease;\n}\n\n.curved-input__button:hover .curved-input__button-bg {\n filter: brightness(1.12);\n}\n\n.curved-input__button:active .curved-input__button-bg {\n filter: brightness(0.94);\n}\n\n.curved-input__button:focus-visible .curved-input__button-bg {\n filter: brightness(1.18);\n}\n\n.curved-input__field {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n opacity: 0;\n border: 0;\n padding: 0;\n margin: 0;\n background: transparent;\n color: transparent;\n caret-color: transparent;\n font-size: 16px;\n pointer-events: none;\n outline: none;\n}\n" }, { "type": "registry:component", - "path": "CurvedInput/CurvedInput.tsx", + "path": "CurvedInput.tsx", "content": "import {\n useEffect,\n useId,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n type ChangeEvent,\n type FormEvent,\n type KeyboardEvent,\n type MouseEvent as ReactMouseEvent,\n type PointerEvent as ReactPointerEvent,\n type SyntheticEvent\n} from 'react';\nimport './CurvedInput.css';\n\nconst DEG = 180 / Math.PI;\n\nconst round2 = (n: number): number => Math.round(n * 100) / 100;\n\nconst hexToRgba = (hex: string, alpha: number): string => {\n let h = String(hex).replace('#', '');\n if (h.length === 3)\n h = h\n .split('')\n .map(c => c + c)\n .join('');\n const n = parseInt(h.slice(0, 6), 16);\n if (Number.isNaN(n)) return hex;\n return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;\n};\n\ntype ShadowSize = 'sm' | 'md' | 'lg';\ntype Theme = 'dark' | 'light';\n\nconst SHADOWS: Record = {\n sm: [5, 12, 0.3],\n md: [10, 24, 0.4],\n lg: [16, 40, 0.52]\n};\n\ninterface ThemePalette {\n backgroundColor: string;\n textColor: string;\n placeholderColor: string;\n borderColor: string;\n buttonColor: string;\n buttonTextColor: string;\n shadowColor: string;\n}\n\nconst THEMES: Record = {\n dark: {\n backgroundColor: '#1B1722',\n textColor: '#f5f5f5',\n placeholderColor: '#a1a1aa',\n borderColor: '#392e4e',\n buttonColor: '#A855F7',\n buttonTextColor: '#ffffff',\n shadowColor: '#000000'\n },\n light: {\n backgroundColor: '#ffffff',\n textColor: '#1d2050',\n placeholderColor: '#9aa0b6',\n borderColor: '#262a56',\n buttonColor: '#4763eb',\n buttonTextColor: '#ffffff',\n shadowColor: '#0b0e2a'\n }\n};\n\ninterface Geometry {\n straight: boolean;\n W: number;\n T: number;\n svgH: number;\n R?: number;\n dir?: number;\n uPerLen: number;\n point: (u: number, v: number) => [number, number];\n angleAt: (u: number) => number;\n uFromPoint: (x: number, y?: number) => number;\n}\n\n// Maps the flat coordinate space (u: 0..W along the bar, v: offset from the\n// centerline, positive down) onto a circular arc with the given sagitta\n// (`bend`, in px). Positive bend arches up, negative sags down, 0 is flat.\nconst buildGeometry = (width: number, bend: number, thickness: number, pad: number): Geometry => {\n const W = width;\n const T = thickness;\n const s = Math.max(-W * 0.35, Math.min(bend, W * 0.35));\n const a = Math.abs(s);\n const dir = s >= 0 ? 1 : -1;\n const svgH = T + a + pad * 2;\n\n if (a < 0.75) {\n const midY = pad + T / 2;\n return {\n straight: true,\n W,\n T,\n svgH,\n uPerLen: 1,\n point: (u, v) => [u, midY + v],\n angleAt: () => 0,\n uFromPoint: x => x\n };\n }\n\n const R = (W * W * 0.25 + a * a) / (2 * a);\n const cx = W / 2;\n const apexY = pad + T / 2 + (dir > 0 ? 0 : a);\n const cy = apexY + dir * R;\n const phi = Math.asin(Math.min(1, W / (2 * R)));\n\n return {\n straight: false,\n W,\n T,\n svgH,\n R,\n dir,\n uPerLen: W / (2 * R * phi),\n point: (u, v) => {\n const th = ((u - cx) / cx) * phi;\n const rho = R - dir * v;\n return [cx + rho * Math.sin(th), cy - dir * rho * Math.cos(th)];\n },\n angleAt: u => dir * ((u - cx) / cx) * phi * DEG,\n uFromPoint: (x, y = 0) => {\n const th = Math.atan2(x - cx, dir * (cy - y));\n return cx + (th / phi) * cx;\n }\n };\n};\n\nconst fmt = (g: Geometry, u: number, v: number): string => {\n const [x, y] = g.point(u, v);\n return `${round2(x)} ${round2(y)}`;\n};\n\n// Segment along a constant-v edge, as a circular arc (or a line when flat)\nconst edgeSeg = (g: Geometry, uTo: number, v: number, ltr: boolean): string => {\n if (g.straight) return `L ${fmt(g, uTo, v)}`;\n const rho = round2(g.R! - g.dir! * v);\n const sweep = ltr === g.dir! > 0 ? 1 : 0;\n return `A ${rho} ${rho} 0 0 ${sweep} ${fmt(g, uTo, v)}`;\n};\n\n// A rectangle bent along the arc: circular top/bottom edges, radial end caps\n// and quadratic rounded corners.\nconst bentRectPath = (g: Geometry, u0: number, u1: number, vTop: number, vBot: number, radius: number): string => {\n const rc = Math.max(0, Math.min(radius, (vBot - vTop) / 2, (u1 - u0) / 2));\n return [\n `M ${fmt(g, u0 + rc, vTop)}`,\n edgeSeg(g, u1 - rc, vTop, true),\n `Q ${fmt(g, u1, vTop)} ${fmt(g, u1, vTop + rc)}`,\n `L ${fmt(g, u1, vBot - rc)}`,\n `Q ${fmt(g, u1, vBot)} ${fmt(g, u1 - rc, vBot)}`,\n edgeSeg(g, u0 + rc, vBot, false),\n `Q ${fmt(g, u0, vBot)} ${fmt(g, u0, vBot - rc)}`,\n `L ${fmt(g, u0, vTop + rc)}`,\n `Q ${fmt(g, u0, vTop)} ${fmt(g, u0 + rc, vTop)}`,\n 'Z'\n ].join(' ');\n};\n\nconst bentLinePath = (g: Geometry, u0: number, u1: number, v: number): string =>\n `M ${fmt(g, u0, v)} ${edgeSeg(g, u1, v, true)}`;\n\nconst SELECTABLE_TYPES = ['text', 'search', 'tel', 'url', 'password'];\n\ninterface CurvedInputProps {\n value?: string;\n defaultValue?: string;\n onChange?: (value: string) => void;\n onSubmit?: (value: string) => void;\n placeholder?: string;\n buttonText?: string;\n type?: string;\n name?: string;\n ariaLabel?: string;\n theme?: Theme;\n width?: number | string;\n bend?: number;\n height?: number;\n cornerRadius?: number;\n borderWidth?: number;\n fontSize?: number;\n backgroundColor?: string;\n textColor?: string;\n placeholderColor?: string;\n borderColor?: string;\n buttonColor?: string;\n buttonTextColor?: string;\n iconColor?: string;\n shadowSize?: ShadowSize;\n shadowColor?: string;\n showButton?: boolean;\n showIcon?: boolean;\n icon?: ReactNode;\n className?: string;\n style?: CSSProperties;\n}\n\nconst CurvedInput = ({\n value,\n defaultValue = '',\n onChange,\n onSubmit,\n placeholder = 'Enter your email',\n buttonText = 'Get Started',\n type = 'email',\n name,\n ariaLabel,\n theme = 'dark',\n width = 450,\n bend = 28,\n height = 64,\n cornerRadius = 18,\n borderWidth = 1.5,\n fontSize = 16,\n backgroundColor,\n textColor,\n placeholderColor,\n borderColor,\n buttonColor,\n buttonTextColor,\n iconColor,\n shadowSize = 'md',\n shadowColor,\n showButton = true,\n showIcon = true,\n icon,\n className = '',\n style\n}: CurvedInputProps) => {\n const uid = useId().replace(/:/g, '');\n const layoutPathId = `ci-text-${uid}`;\n const buttonPathId = `ci-btn-${uid}`;\n const clipId = `ci-clip-${uid}`;\n\n const rootRef = useRef(null);\n const svgRef = useRef(null);\n const inputRef = useRef(null);\n const textRef = useRef(null);\n const btnMeasureRef = useRef(null);\n const scrollRef = useRef(0);\n\n const [w, setW] = useState(0);\n const [innerValue, setInnerValue] = useState(defaultValue);\n const [caretIndex, setCaretIndex] = useState(defaultValue.length);\n const [focused, setFocused] = useState(false);\n const [caretU, setCaretU] = useState(0);\n const [scrollLen, setScrollLen] = useState(0);\n const [btnTextW, setBtnTextW] = useState(0);\n const [, setFontTick] = useState(0);\n\n const val = value !== undefined ? value : innerValue;\n const display = type === 'password' ? '•'.repeat(val.length) : val;\n\n const palette = THEMES[theme] || THEMES.dark;\n const bgColor = backgroundColor ?? palette.backgroundColor;\n const fgColor = textColor ?? palette.textColor;\n const phColor = placeholderColor ?? palette.placeholderColor;\n const strokeColor = borderColor ?? palette.borderColor;\n const accentColor = buttonColor ?? palette.buttonColor;\n const btnFgColor = buttonTextColor ?? palette.buttonTextColor;\n const shColor = shadowColor ?? palette.shadowColor;\n\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const ro = new ResizeObserver(entries => {\n const cw = entries[0]?.contentRect?.width ?? el.clientWidth;\n setW(Math.round(cw));\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n // Re-measure once webfonts finish loading\n useEffect(() => {\n let alive = true;\n if (document.fonts?.ready) {\n document.fonts.ready.then(() => {\n if (alive) setFontTick(t => t + 1);\n });\n }\n return () => {\n alive = false;\n };\n }, []);\n\n const pad = Math.ceil(borderWidth / 2) + 6;\n const geom = useMemo(\n () => (w > 2 ? buildGeometry(w, bend, height, pad) : null),\n [w, bend, height, pad]\n );\n\n const layout = useMemo(() => {\n if (!geom) return null;\n const T = height;\n const btnInset = Math.max(5, borderWidth + 4);\n const chipH = Math.min(34, Math.max(16, T * 0.34));\n const chipW = chipH * 1.25;\n const iconU = 22 + chipW / 2;\n const textStartU = showIcon ? 22 + chipW + 13 : 24;\n const btnW = showButton ? Math.max(btnTextW + fontSize * 2.7, T * 1.35) : 0;\n const btnU1 = geom.W - btnInset;\n const btnU0 = btnU1 - btnW;\n const textEndU = Math.max(textStartU + 20, showButton ? btnU0 - 14 : geom.W - 24);\n const winLen = (textEndU - textStartU) / geom.uPerLen;\n return { btnInset, chipH, chipW, iconU, textStartU, textEndU, btnU0, btnU1, winLen };\n }, [geom, height, borderWidth, btnTextW, fontSize, showIcon, showButton]);\n\n // Measure rendered text to keep the caret on the curve and scroll long\n // values along the arc, exactly like a native input would.\n useLayoutEffect(() => {\n if (btnMeasureRef.current) {\n const bw = btnMeasureRef.current.getComputedTextLength();\n setBtnTextW(prev => (Math.abs(prev - bw) > 0.5 ? bw : prev));\n }\n if (!geom || !layout) return;\n const textEl = textRef.current;\n const caret = Math.min(caretIndex, display.length);\n let caretLen = 0;\n let totalLen = 0;\n if (textEl && display.length) {\n try {\n totalLen = textEl.getSubStringLength(0, display.length);\n caretLen = caret > 0 ? textEl.getSubStringLength(0, caret) : 0;\n } catch {\n totalLen = 0;\n caretLen = 0;\n }\n }\n let next = scrollRef.current;\n if (caretLen - next > layout.winLen - 2) next = caretLen - layout.winLen + 2;\n if (caretLen - next < 0) next = caretLen;\n if (totalLen - next < layout.winLen) next = Math.max(0, totalLen - layout.winLen);\n next = Math.max(0, next);\n if (Math.abs(next - scrollRef.current) > 0.5) {\n scrollRef.current = next;\n setScrollLen(next);\n }\n setCaretU(layout.textStartU + (caretLen - next) * geom.uPerLen);\n });\n\n const commitValue = (v: string) => {\n if (value === undefined) setInnerValue(v);\n onChange?.(v);\n };\n\n const handleInputChange = (e: ChangeEvent) => {\n commitValue(e.target.value);\n setCaretIndex(e.target.selectionStart ?? e.target.value.length);\n };\n\n const handleSelect = (e: SyntheticEvent) => {\n const target = e.currentTarget;\n setCaretIndex(target.selectionStart ?? target.value.length);\n };\n\n const handleSubmit = (e?: FormEvent) => {\n if (e?.preventDefault) e.preventDefault();\n if (onSubmit) onSubmit(val);\n };\n\n // Click on the curve: focus the hidden input and drop the caret on the\n // character closest to the click, measured in arc length.\n const handleSurfaceClick = (e: ReactMouseEvent) => {\n const input = inputRef.current;\n if (!input) return;\n let idx = display.length;\n const svg = svgRef.current;\n const textEl = textRef.current;\n if (svg && geom && layout && textEl && display.length) {\n try {\n const ctm = svg.getScreenCTM();\n if (!ctm) throw new Error('missing screen CTM');\n const pt = new DOMPoint(e.clientX, e.clientY).matrixTransform(ctm.inverse());\n const target = scrollRef.current + (geom.uFromPoint(pt.x, pt.y) - layout.textStartU) / geom.uPerLen;\n let best = 0;\n let bestDist = Infinity;\n for (let i = 0; i <= display.length; i++) {\n const li = i === 0 ? 0 : textEl.getSubStringLength(0, i);\n const d = Math.abs(li - target);\n if (d < bestDist) {\n bestDist = d;\n best = i;\n }\n }\n idx = best;\n } catch {\n idx = display.length;\n }\n }\n input.focus();\n try {\n input.setSelectionRange(idx, idx);\n } catch {\n /* selection API unavailable for this input type */\n }\n setCaretIndex(idx);\n };\n\n const safeType = SELECTABLE_TYPES.includes(type) ? type : 'text';\n const inputMode = type === 'email' ? 'email' : type === 'number' ? 'decimal' : undefined;\n\n const shadow = SHADOWS[shadowSize];\n const svgStyle: CSSProperties | undefined = shadow\n ? { filter: `drop-shadow(0 ${shadow[0]}px ${shadow[1]}px ${hexToRgba(shColor, shadow[2])})` }\n : undefined;\n\n let content: ReactNode = null;\n if (geom && layout) {\n const T = height;\n const vBase = fontSize * 0.34;\n const scrollU = scrollLen * geom.uPerLen;\n const bandPath = bentRectPath(geom, 0, geom.W, -T / 2, T / 2, cornerRadius);\n const layoutPath = bentLinePath(geom, layout.textStartU - scrollU, geom.W, vBase);\n const clipPath = bentRectPath(geom, layout.textStartU - 6, layout.textEndU + 8, -T / 2, T / 2, 0);\n\n const chipFill = iconColor || accentColor;\n const { chipW, chipH } = layout;\n const ew = chipW * 0.5;\n const eh = chipH * 0.5;\n const sw = Math.max(1.1, chipH * 0.075);\n const [ix, iy] = geom.point(layout.iconU, 0);\n const iconAngle = geom.angleAt(layout.iconU);\n\n const [caretX, caretY] = geom.point(caretU, 0);\n const caretAngle = geom.angleAt(caretU);\n const caretH = Math.min(T * 0.58, fontSize * 1.45);\n\n const btnH = T - layout.btnInset * 2;\n const buttonPath = showButton\n ? bentRectPath(\n geom,\n layout.btnU0,\n layout.btnU1,\n -T / 2 + layout.btnInset,\n T / 2 - layout.btnInset,\n Math.min(cornerRadius * 0.72, btnH / 2)\n )\n : '';\n const buttonTextPath = showButton ? bentLinePath(geom, layout.btnU0, layout.btnU1, vBase) : '';\n\n content = (\n e.preventDefault()}\n onClick={handleSurfaceClick}\n >\n \n \n \n \n \n\n \n \n\n \n\n {showIcon && (\n \n {icon || (\n <>\n \n \n \n \n )}\n \n )}\n\n \n \n {display}\n \n {!display && placeholder && (\n \n {placeholder}\n \n )}\n {focused && (\n \n \n \n \n \n )}\n \n\n {showButton && (\n {\n e.stopPropagation();\n handleSubmit();\n }}\n onPointerDown={(e: ReactPointerEvent) => e.stopPropagation()}\n onKeyDown={(e: KeyboardEvent) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n handleSubmit();\n }\n }}\n >\n \n \n \n \n {buttonText}\n \n \n \n )}\n\n \n {buttonText}\n \n \n );\n }\n\n return (\n \n {content}\n setFocused(true)}\n onBlur={() => setFocused(false)}\n aria-label={ariaLabel || placeholder || 'Curved input'}\n autoComplete=\"off\"\n autoCapitalize=\"none\"\n autoCorrect=\"off\"\n spellCheck={false}\n />\n \n );\n};\n\nexport default CurvedInput;\n" } ], diff --git a/public/r/CurvedLoop-JS-CSS.json b/public/r/CurvedLoop-JS-CSS.json index 29701623..804b550a 100644 --- a/public/r/CurvedLoop-JS-CSS.json +++ b/public/r/CurvedLoop-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "CurvedLoop/CurvedLoop.css", + "type": "registry:file", + "path": "CurvedLoop.css", + "target": "@components/CurvedLoop.css", "content": ".curved-loop-jacket {\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n}\n\n.curved-loop-svg {\n user-select: none;\n width: 100%;\n aspect-ratio: 100 / 12;\n overflow: visible;\n display: block;\n font-size: 6rem;\n fill: #ffffff;\n user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n font-weight: 700;\n text-transform: uppercase;\n line-height: 1;\n}\n" }, { "type": "registry:component", - "path": "CurvedLoop/CurvedLoop.jsx", + "path": "CurvedLoop.jsx", "content": "import { useRef, useEffect, useState, useMemo, useId } from 'react';\nimport './CurvedLoop.css';\n\nconst CurvedLoop = ({\n marqueeText = '',\n speed = 2,\n className,\n curveAmount = 400,\n direction = 'left',\n interactive = true\n}) => {\n const text = useMemo(() => {\n const hasTrailing = /\\s|\\u00A0$/.test(marqueeText);\n return (hasTrailing ? marqueeText.replace(/\\s+$/, '') : marqueeText) + '\\u00A0';\n }, [marqueeText]);\n\n const measureRef = useRef(null);\n const textPathRef = useRef(null);\n const pathRef = useRef(null);\n const [spacing, setSpacing] = useState(0);\n const [offset, setOffset] = useState(0);\n const uid = useId();\n const pathId = `curve-${uid}`;\n const pathD = `M-100,40 Q500,${40 + curveAmount} 1540,40`;\n\n const dragRef = useRef(false);\n const lastXRef = useRef(0);\n const dirRef = useRef(direction);\n const velRef = useRef(0);\n\n const textLength = spacing;\n const totalText = textLength\n ? Array(Math.ceil(1800 / textLength) + 2)\n .fill(text)\n .join('')\n : text;\n const ready = spacing > 0;\n\n useEffect(() => {\n if (measureRef.current) setSpacing(measureRef.current.getComputedTextLength());\n }, [text, className]);\n\n useEffect(() => {\n if (!spacing) return;\n if (textPathRef.current) {\n const initial = -spacing;\n textPathRef.current.setAttribute('startOffset', initial + 'px');\n setOffset(initial);\n }\n }, [spacing]);\n\n useEffect(() => {\n if (!spacing || !ready) return;\n let frame = 0;\n const step = () => {\n if (!dragRef.current && textPathRef.current) {\n const delta = dirRef.current === 'right' ? speed : -speed;\n const currentOffset = parseFloat(textPathRef.current.getAttribute('startOffset') || '0');\n let newOffset = currentOffset + delta;\n\n const wrapPoint = spacing;\n if (newOffset <= -wrapPoint) newOffset += wrapPoint;\n if (newOffset > 0) newOffset -= wrapPoint;\n\n textPathRef.current.setAttribute('startOffset', newOffset + 'px');\n setOffset(newOffset);\n }\n frame = requestAnimationFrame(step);\n };\n frame = requestAnimationFrame(step);\n return () => cancelAnimationFrame(frame);\n }, [spacing, speed, ready]);\n\n const onPointerDown = e => {\n if (!interactive) return;\n dragRef.current = true;\n lastXRef.current = e.clientX;\n velRef.current = 0;\n e.target.setPointerCapture(e.pointerId);\n };\n\n const onPointerMove = e => {\n if (!interactive || !dragRef.current || !textPathRef.current) return;\n const dx = e.clientX - lastXRef.current;\n lastXRef.current = e.clientX;\n velRef.current = dx;\n\n const currentOffset = parseFloat(textPathRef.current.getAttribute('startOffset') || '0');\n let newOffset = currentOffset + dx;\n\n const wrapPoint = spacing;\n if (newOffset <= -wrapPoint) newOffset += wrapPoint;\n if (newOffset > 0) newOffset -= wrapPoint;\n\n textPathRef.current.setAttribute('startOffset', newOffset + 'px');\n setOffset(newOffset);\n };\n\n const endDrag = () => {\n if (!interactive) return;\n dragRef.current = false;\n dirRef.current = velRef.current > 0 ? 'right' : 'left';\n };\n\n const cursorStyle = interactive ? (dragRef.current ? 'grabbing' : 'grab') : 'auto';\n\n return (\n \n \n \n {text}\n \n \n \n \n {ready && (\n \n \n {totalText}\n \n \n )}\n \n
\n );\n};\n\nexport default CurvedLoop;\n" } ], diff --git a/public/r/CurvedLoop-TS-CSS.json b/public/r/CurvedLoop-TS-CSS.json index 195eeec2..e1176216 100644 --- a/public/r/CurvedLoop-TS-CSS.json +++ b/public/r/CurvedLoop-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "CurvedLoop/CurvedLoop.css", + "type": "registry:file", + "path": "CurvedLoop.css", + "target": "@components/CurvedLoop.css", "content": ".curved-loop-jacket {\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n}\n\n.curved-loop-svg {\n user-select: none;\n width: 100%;\n aspect-ratio: 100 / 12;\n overflow: visible;\n display: block;\n font-size: 6rem;\n fill: #ffffff;\n user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n font-weight: 700;\n text-transform: uppercase;\n line-height: 1;\n}\n" }, { "type": "registry:component", - "path": "CurvedLoop/CurvedLoop.tsx", - "content": "import { useRef, useEffect, useState, useMemo, useId, FC, PointerEvent } from 'react';\nimport './CurvedLoop.css';\n\ninterface CurvedLoopProps {\n marqueeText?: string;\n speed?: number;\n className?: string;\n curveAmount?: number;\n direction?: 'left' | 'right';\n interactive?: boolean;\n}\n\nconst CurvedLoop: FC = ({\n marqueeText = '',\n speed = 2,\n className,\n curveAmount = 400,\n direction = 'left',\n interactive = true\n}) => {\n const text = useMemo(() => {\n const hasTrailing = /\\s|\\u00A0$/.test(marqueeText);\n return (hasTrailing ? marqueeText.replace(/\\s+$/, '') : marqueeText) + '\\u00A0';\n }, [marqueeText]);\n\n const measureRef = useRef(null);\n const textPathRef = useRef(null);\n const pathRef = useRef(null);\n const [spacing, setSpacing] = useState(0);\n const [offset, setOffset] = useState(0);\n const uid = useId();\n const pathId = `curve-${uid}`;\n const pathD = `M-100,40 Q500,${40 + curveAmount} 1540,40`;\n\n const dragRef = useRef(false);\n const lastXRef = useRef(0);\n const dirRef = useRef<'left' | 'right'>(direction);\n const velRef = useRef(0);\n\n const textLength = spacing;\n const totalText = textLength\n ? Array(Math.ceil(1800 / textLength) + 2)\n .fill(text)\n .join('')\n : text;\n const ready = spacing > 0;\n\n useEffect(() => {\n if (measureRef.current) setSpacing(measureRef.current.getComputedTextLength());\n }, [text, className]);\n\n useEffect(() => {\n if (!spacing) return;\n if (textPathRef.current) {\n const initial = -spacing;\n textPathRef.current.setAttribute('startOffset', initial + 'px');\n setOffset(initial);\n }\n }, [spacing]);\n\n useEffect(() => {\n if (!spacing || !ready) return;\n let frame = 0;\n const step = () => {\n if (!dragRef.current && textPathRef.current) {\n const delta = dirRef.current === 'right' ? speed : -speed;\n const currentOffset = parseFloat(textPathRef.current.getAttribute('startOffset') || '0');\n let newOffset = currentOffset + delta;\n const wrapPoint = spacing;\n if (newOffset <= -wrapPoint) newOffset += wrapPoint;\n if (newOffset > 0) newOffset -= wrapPoint;\n textPathRef.current.setAttribute('startOffset', newOffset + 'px');\n setOffset(newOffset);\n }\n frame = requestAnimationFrame(step);\n };\n frame = requestAnimationFrame(step);\n return () => cancelAnimationFrame(frame);\n }, [spacing, speed, ready]);\n\n const onPointerDown = (e: PointerEvent) => {\n if (!interactive) return;\n dragRef.current = true;\n lastXRef.current = e.clientX;\n velRef.current = 0;\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n };\n\n const onPointerMove = (e: PointerEvent) => {\n if (!interactive || !dragRef.current || !textPathRef.current) return;\n const dx = e.clientX - lastXRef.current;\n lastXRef.current = e.clientX;\n velRef.current = dx;\n const currentOffset = parseFloat(textPathRef.current.getAttribute('startOffset') || '0');\n let newOffset = currentOffset + dx;\n const wrapPoint = spacing;\n if (newOffset <= -wrapPoint) newOffset += wrapPoint;\n if (newOffset > 0) newOffset -= wrapPoint;\n textPathRef.current.setAttribute('startOffset', newOffset + 'px');\n setOffset(newOffset);\n };\n\n const endDrag = () => {\n if (!interactive) return;\n dragRef.current = false;\n dirRef.current = velRef.current > 0 ? 'right' : 'left';\n };\n\n const cursorStyle = interactive ? (dragRef.current ? 'grabbing' : 'grab') : 'auto';\n\n return (\n \n \n \n {text}\n \n \n \n \n {ready && (\n \n \n {totalText}\n \n \n )}\n \n
\n );\n};\n\nexport default CurvedLoop;\n" + "path": "CurvedLoop.tsx", + "content": "import { useRef, useEffect, useState, useMemo, useId, type FC, type PointerEvent } from 'react';\nimport './CurvedLoop.css';\n\ninterface CurvedLoopProps {\n marqueeText?: string;\n speed?: number;\n className?: string;\n curveAmount?: number;\n direction?: 'left' | 'right';\n interactive?: boolean;\n}\n\nconst CurvedLoop: FC = ({\n marqueeText = '',\n speed = 2,\n className,\n curveAmount = 400,\n direction = 'left',\n interactive = true\n}) => {\n const text = useMemo(() => {\n const hasTrailing = /\\s|\\u00A0$/.test(marqueeText);\n return (hasTrailing ? marqueeText.replace(/\\s+$/, '') : marqueeText) + '\\u00A0';\n }, [marqueeText]);\n\n const measureRef = useRef(null);\n const textPathRef = useRef(null);\n const pathRef = useRef(null);\n const [spacing, setSpacing] = useState(0);\n const [offset, setOffset] = useState(0);\n const uid = useId();\n const pathId = `curve-${uid}`;\n const pathD = `M-100,40 Q500,${40 + curveAmount} 1540,40`;\n\n const dragRef = useRef(false);\n const lastXRef = useRef(0);\n const dirRef = useRef<'left' | 'right'>(direction);\n const velRef = useRef(0);\n\n const textLength = spacing;\n const totalText = textLength\n ? Array(Math.ceil(1800 / textLength) + 2)\n .fill(text)\n .join('')\n : text;\n const ready = spacing > 0;\n\n useEffect(() => {\n if (measureRef.current) setSpacing(measureRef.current.getComputedTextLength());\n }, [text, className]);\n\n useEffect(() => {\n if (!spacing) return;\n if (textPathRef.current) {\n const initial = -spacing;\n textPathRef.current.setAttribute('startOffset', initial + 'px');\n setOffset(initial);\n }\n }, [spacing]);\n\n useEffect(() => {\n if (!spacing || !ready) return;\n let frame = 0;\n const step = () => {\n if (!dragRef.current && textPathRef.current) {\n const delta = dirRef.current === 'right' ? speed : -speed;\n const currentOffset = parseFloat(textPathRef.current.getAttribute('startOffset') || '0');\n let newOffset = currentOffset + delta;\n const wrapPoint = spacing;\n if (newOffset <= -wrapPoint) newOffset += wrapPoint;\n if (newOffset > 0) newOffset -= wrapPoint;\n textPathRef.current.setAttribute('startOffset', newOffset + 'px');\n setOffset(newOffset);\n }\n frame = requestAnimationFrame(step);\n };\n frame = requestAnimationFrame(step);\n return () => cancelAnimationFrame(frame);\n }, [spacing, speed, ready]);\n\n const onPointerDown = (e: PointerEvent) => {\n if (!interactive) return;\n dragRef.current = true;\n lastXRef.current = e.clientX;\n velRef.current = 0;\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n };\n\n const onPointerMove = (e: PointerEvent) => {\n if (!interactive || !dragRef.current || !textPathRef.current) return;\n const dx = e.clientX - lastXRef.current;\n lastXRef.current = e.clientX;\n velRef.current = dx;\n const currentOffset = parseFloat(textPathRef.current.getAttribute('startOffset') || '0');\n let newOffset = currentOffset + dx;\n const wrapPoint = spacing;\n if (newOffset <= -wrapPoint) newOffset += wrapPoint;\n if (newOffset > 0) newOffset -= wrapPoint;\n textPathRef.current.setAttribute('startOffset', newOffset + 'px');\n setOffset(newOffset);\n };\n\n const endDrag = () => {\n if (!interactive) return;\n dragRef.current = false;\n dirRef.current = velRef.current > 0 ? 'right' : 'left';\n };\n\n const cursorStyle = interactive ? (dragRef.current ? 'grabbing' : 'grab') : 'auto';\n\n return (\n \n \n \n {text}\n \n \n \n \n {ready && (\n \n \n {totalText}\n \n \n )}\n \n
\n );\n};\n\nexport default CurvedLoop;\n" } ], "registryDependencies": [], diff --git a/public/r/CurvedLoop-TS-TW.json b/public/r/CurvedLoop-TS-TW.json index cad61d22..94eab234 100644 --- a/public/r/CurvedLoop-TS-TW.json +++ b/public/r/CurvedLoop-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "CurvedLoop/CurvedLoop.tsx", - "content": "import { useRef, useEffect, useState, useMemo, useId, FC, PointerEvent } from 'react';\n\ninterface CurvedLoopProps {\n marqueeText?: string;\n speed?: number;\n className?: string;\n curveAmount?: number;\n direction?: 'left' | 'right';\n interactive?: boolean;\n}\n\nconst CurvedLoop: FC = ({\n marqueeText = '',\n speed = 2,\n className,\n curveAmount = 400,\n direction = 'left',\n interactive = true\n}) => {\n const text = useMemo(() => {\n const hasTrailing = /\\s|\\u00A0$/.test(marqueeText);\n return (hasTrailing ? marqueeText.replace(/\\s+$/, '') : marqueeText) + '\\u00A0';\n }, [marqueeText]);\n\n const measureRef = useRef(null);\n const textPathRef = useRef(null);\n const pathRef = useRef(null);\n const [spacing, setSpacing] = useState(0);\n const [offset, setOffset] = useState(0);\n const uid = useId();\n const pathId = `curve-${uid}`;\n const pathD = `M-100,40 Q500,${40 + curveAmount} 1540,40`;\n\n const dragRef = useRef(false);\n const lastXRef = useRef(0);\n const dirRef = useRef<'left' | 'right'>(direction);\n const velRef = useRef(0);\n\n const textLength = spacing;\n const totalText = textLength\n ? Array(Math.ceil(1800 / textLength) + 2)\n .fill(text)\n .join('')\n : text;\n const ready = spacing > 0;\n\n useEffect(() => {\n if (measureRef.current) setSpacing(measureRef.current.getComputedTextLength());\n }, [text, className]);\n\n useEffect(() => {\n if (!spacing) return;\n if (textPathRef.current) {\n const initial = -spacing;\n textPathRef.current.setAttribute('startOffset', initial + 'px');\n setOffset(initial);\n }\n }, [spacing]);\n\n useEffect(() => {\n if (!spacing || !ready) return;\n let frame = 0;\n const step = () => {\n if (!dragRef.current && textPathRef.current) {\n const delta = dirRef.current === 'right' ? speed : -speed;\n const currentOffset = parseFloat(textPathRef.current.getAttribute('startOffset') || '0');\n let newOffset = currentOffset + delta;\n const wrapPoint = spacing;\n if (newOffset <= -wrapPoint) newOffset += wrapPoint;\n if (newOffset > 0) newOffset -= wrapPoint;\n textPathRef.current.setAttribute('startOffset', newOffset + 'px');\n setOffset(newOffset);\n }\n frame = requestAnimationFrame(step);\n };\n frame = requestAnimationFrame(step);\n return () => cancelAnimationFrame(frame);\n }, [spacing, speed, ready]);\n\n const onPointerDown = (e: PointerEvent) => {\n if (!interactive) return;\n dragRef.current = true;\n lastXRef.current = e.clientX;\n velRef.current = 0;\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n };\n\n const onPointerMove = (e: PointerEvent) => {\n if (!interactive || !dragRef.current || !textPathRef.current) return;\n const dx = e.clientX - lastXRef.current;\n lastXRef.current = e.clientX;\n velRef.current = dx;\n const currentOffset = parseFloat(textPathRef.current.getAttribute('startOffset') || '0');\n let newOffset = currentOffset + dx;\n const wrapPoint = spacing;\n if (newOffset <= -wrapPoint) newOffset += wrapPoint;\n if (newOffset > 0) newOffset -= wrapPoint;\n textPathRef.current.setAttribute('startOffset', newOffset + 'px');\n setOffset(newOffset);\n };\n\n const endDrag = () => {\n if (!interactive) return;\n dragRef.current = false;\n dirRef.current = velRef.current > 0 ? 'right' : 'left';\n };\n\n const cursorStyle = interactive ? (dragRef.current ? 'grabbing' : 'grab') : 'auto';\n\n return (\n \n \n \n {text}\n \n \n \n \n {ready && (\n \n \n {totalText}\n \n \n )}\n \n
\n );\n};\n\nexport default CurvedLoop;\n" + "content": "import { useRef, useEffect, useState, useMemo, useId, type FC, type PointerEvent } from 'react';\n\ninterface CurvedLoopProps {\n marqueeText?: string;\n speed?: number;\n className?: string;\n curveAmount?: number;\n direction?: 'left' | 'right';\n interactive?: boolean;\n}\n\nconst CurvedLoop: FC = ({\n marqueeText = '',\n speed = 2,\n className,\n curveAmount = 400,\n direction = 'left',\n interactive = true\n}) => {\n const text = useMemo(() => {\n const hasTrailing = /\\s|\\u00A0$/.test(marqueeText);\n return (hasTrailing ? marqueeText.replace(/\\s+$/, '') : marqueeText) + '\\u00A0';\n }, [marqueeText]);\n\n const measureRef = useRef(null);\n const textPathRef = useRef(null);\n const pathRef = useRef(null);\n const [spacing, setSpacing] = useState(0);\n const [offset, setOffset] = useState(0);\n const uid = useId();\n const pathId = `curve-${uid}`;\n const pathD = `M-100,40 Q500,${40 + curveAmount} 1540,40`;\n\n const dragRef = useRef(false);\n const lastXRef = useRef(0);\n const dirRef = useRef<'left' | 'right'>(direction);\n const velRef = useRef(0);\n\n const textLength = spacing;\n const totalText = textLength\n ? Array(Math.ceil(1800 / textLength) + 2)\n .fill(text)\n .join('')\n : text;\n const ready = spacing > 0;\n\n useEffect(() => {\n if (measureRef.current) setSpacing(measureRef.current.getComputedTextLength());\n }, [text, className]);\n\n useEffect(() => {\n if (!spacing) return;\n if (textPathRef.current) {\n const initial = -spacing;\n textPathRef.current.setAttribute('startOffset', initial + 'px');\n setOffset(initial);\n }\n }, [spacing]);\n\n useEffect(() => {\n if (!spacing || !ready) return;\n let frame = 0;\n const step = () => {\n if (!dragRef.current && textPathRef.current) {\n const delta = dirRef.current === 'right' ? speed : -speed;\n const currentOffset = parseFloat(textPathRef.current.getAttribute('startOffset') || '0');\n let newOffset = currentOffset + delta;\n const wrapPoint = spacing;\n if (newOffset <= -wrapPoint) newOffset += wrapPoint;\n if (newOffset > 0) newOffset -= wrapPoint;\n textPathRef.current.setAttribute('startOffset', newOffset + 'px');\n setOffset(newOffset);\n }\n frame = requestAnimationFrame(step);\n };\n frame = requestAnimationFrame(step);\n return () => cancelAnimationFrame(frame);\n }, [spacing, speed, ready]);\n\n const onPointerDown = (e: PointerEvent) => {\n if (!interactive) return;\n dragRef.current = true;\n lastXRef.current = e.clientX;\n velRef.current = 0;\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n };\n\n const onPointerMove = (e: PointerEvent) => {\n if (!interactive || !dragRef.current || !textPathRef.current) return;\n const dx = e.clientX - lastXRef.current;\n lastXRef.current = e.clientX;\n velRef.current = dx;\n const currentOffset = parseFloat(textPathRef.current.getAttribute('startOffset') || '0');\n let newOffset = currentOffset + dx;\n const wrapPoint = spacing;\n if (newOffset <= -wrapPoint) newOffset += wrapPoint;\n if (newOffset > 0) newOffset -= wrapPoint;\n textPathRef.current.setAttribute('startOffset', newOffset + 'px');\n setOffset(newOffset);\n };\n\n const endDrag = () => {\n if (!interactive) return;\n dragRef.current = false;\n dirRef.current = velRef.current > 0 ? 'right' : 'left';\n };\n\n const cursorStyle = interactive ? (dragRef.current ? 'grabbing' : 'grab') : 'auto';\n\n return (\n \n \n \n {text}\n \n \n \n \n {ready && (\n \n \n {totalText}\n \n \n )}\n \n
\n );\n};\n\nexport default CurvedLoop;\n" } ], "registryDependencies": [], diff --git a/public/r/DarkVeil-JS-CSS.json b/public/r/DarkVeil-JS-CSS.json index 12a0f0fd..900b4006 100644 --- a/public/r/DarkVeil-JS-CSS.json +++ b/public/r/DarkVeil-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "DarkVeil/DarkVeil.css", + "type": "registry:file", + "path": "DarkVeil.css", + "target": "@components/DarkVeil.css", "content": ".darkveil-canvas {\n width: 100%;\n height: 100%;\n display: block;\n}\n" }, { "type": "registry:component", - "path": "DarkVeil/DarkVeil.jsx", + "path": "DarkVeil.jsx", "content": "import { useRef, useEffect } from 'react';\nimport { Renderer, Program, Mesh, Triangle, Vec2 } from 'ogl';\nimport './DarkVeil.css';\n\nconst vertex = `\nattribute vec2 position;\nvoid main(){gl_Position=vec4(position,0.0,1.0);}\n`;\n\nconst fragment = `\n#ifdef GL_ES\nprecision lowp float;\n#endif\nuniform vec2 uResolution;\nuniform float uTime;\nuniform float uHueShift;\nuniform float uNoise;\nuniform float uScan;\nuniform float uScanFreq;\nuniform float uWarp;\n#define iTime uTime\n#define iResolution uResolution\n\nvec4 buf[8];\nfloat rand(vec2 c){return fract(sin(dot(c,vec2(12.9898,78.233)))*43758.5453);}\n\nmat3 rgb2yiq=mat3(0.299,0.587,0.114,0.596,-0.274,-0.322,0.211,-0.523,0.312);\nmat3 yiq2rgb=mat3(1.0,0.956,0.621,1.0,-0.272,-0.647,1.0,-1.106,1.703);\n\nvec3 hueShiftRGB(vec3 col,float deg){\n vec3 yiq=rgb2yiq*col;\n float rad=radians(deg);\n float cosh=cos(rad),sinh=sin(rad);\n vec3 yiqShift=vec3(yiq.x,yiq.y*cosh-yiq.z*sinh,yiq.y*sinh+yiq.z*cosh);\n return clamp(yiq2rgb*yiqShift,0.0,1.0);\n}\n\nvec4 sigmoid(vec4 x){return 1./(1.+exp(-x));}\n\nvec4 cppn_fn(vec2 coordinate,float in0,float in1,float in2){\n buf[6]=vec4(coordinate.x,coordinate.y,0.3948333106474662+in0,0.36+in1);\n buf[7]=vec4(0.14+in2,sqrt(coordinate.x*coordinate.x+coordinate.y*coordinate.y),0.,0.);\n buf[0]=mat4(vec4(6.5404263,-3.6126034,0.7590882,-1.13613),vec4(2.4582713,3.1660357,1.2219609,0.06276096),vec4(-5.478085,-6.159632,1.8701609,-4.7742867),vec4(6.039214,-5.542865,-0.90925294,3.251348))*buf[6]+mat4(vec4(0.8473259,-5.722911,3.975766,1.6522468),vec4(-0.24321538,0.5839259,-1.7661959,-5.350116),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(0.21808943,1.1243913,-1.7969975,5.0294676);\n buf[1]=mat4(vec4(-3.3522482,-6.0612736,0.55641043,-4.4719114),vec4(0.8631464,1.7432913,5.643898,1.6106541),vec4(2.4941394,-3.5012043,1.7184316,6.357333),vec4(3.310376,8.209261,1.1355612,-1.165539))*buf[6]+mat4(vec4(5.24046,-13.034365,0.009859298,15.870829),vec4(2.987511,3.129433,-0.89023495,-1.6822904),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-5.9457836,-6.573602,-0.8812491,1.5436668);\n buf[0]=sigmoid(buf[0]);buf[1]=sigmoid(buf[1]);\n buf[2]=mat4(vec4(-15.219568,8.095543,-2.429353,-1.9381982),vec4(-5.951362,4.3115187,2.6393783,1.274315),vec4(-7.3145227,6.7297835,5.2473326,5.9411426),vec4(5.0796127,8.979051,-1.7278991,-1.158976))*buf[6]+mat4(vec4(-11.967154,-11.608155,6.1486754,11.237008),vec4(2.124141,-6.263192,-1.7050359,-0.7021966),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-4.17164,-3.2281182,-4.576417,-3.6401186);\n buf[3]=mat4(vec4(3.1832156,-13.738922,1.879223,3.233465),vec4(0.64300746,12.768129,1.9141049,0.50990224),vec4(-0.049295485,4.4807224,1.4733979,1.801449),vec4(5.0039253,13.000481,3.3991797,-4.5561905))*buf[6]+mat4(vec4(-0.1285731,7.720628,-3.1425676,4.742367),vec4(0.6393625,3.714393,-0.8108378,-0.39174938),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-1.1811101,-21.621881,0.7851888,1.2329718);\n buf[2]=sigmoid(buf[2]);buf[3]=sigmoid(buf[3]);\n buf[4]=mat4(vec4(5.214916,-7.183024,2.7228765,2.6592617),vec4(-5.601878,-25.3591,4.067988,0.4602802),vec4(-10.57759,24.286327,21.102104,37.546658),vec4(4.3024497,-1.9625226,2.3458803,-1.372816))*buf[0]+mat4(vec4(-17.6526,-10.507558,2.2587414,12.462782),vec4(6.265566,-502.75443,-12.642513,0.9112289),vec4(-10.983244,20.741234,-9.701768,-0.7635988),vec4(5.383626,1.4819539,-4.1911616,-4.8444734))*buf[1]+mat4(vec4(12.785233,-16.345072,-0.39901125,1.7955981),vec4(-30.48365,-1.8345358,1.4542528,-1.1118771),vec4(19.872723,-7.337935,-42.941723,-98.52709),vec4(8.337645,-2.7312303,-2.2927687,-36.142323))*buf[2]+mat4(vec4(-16.298317,3.5471997,-0.44300047,-9.444417),vec4(57.5077,-35.609753,16.163465,-4.1534753),vec4(-0.07470326,-3.8656476,-7.0901804,3.1523974),vec4(-12.559385,-7.077619,1.490437,-0.8211543))*buf[3]+vec4(-7.67914,15.927437,1.3207729,-1.6686112);\n buf[5]=mat4(vec4(-1.4109162,-0.372762,-3.770383,-21.367174),vec4(-6.2103205,-9.35908,0.92529047,8.82561),vec4(11.460242,-22.348068,13.625772,-18.693201),vec4(-0.3429052,-3.9905605,-2.4626114,-0.45033523))*buf[0]+mat4(vec4(7.3481627,-4.3661838,-6.3037653,-3.868115),vec4(1.5462853,6.5488915,1.9701879,-0.58291394),vec4(6.5858274,-2.2180402,3.7127688,-1.3730392),vec4(-5.7973905,10.134961,-2.3395722,-5.965605))*buf[1]+mat4(vec4(-2.5132585,-6.6685553,-1.4029363,-0.16285264),vec4(-0.37908727,0.53738135,4.389061,-1.3024765),vec4(-0.70647055,2.0111287,-5.1659346,-3.728635),vec4(-13.562562,10.487719,-0.9173751,-2.6487076))*buf[2]+mat4(vec4(-8.645013,6.5546675,-6.3944063,-5.5933375),vec4(-0.57783127,-1.077275,36.91025,5.736769),vec4(14.283112,3.7146652,7.1452246,-4.5958776),vec4(2.7192075,3.6021907,-4.366337,-2.3653464))*buf[3]+vec4(-5.9000807,-4.329569,1.2427121,8.59503);\n buf[4]=sigmoid(buf[4]);buf[5]=sigmoid(buf[5]);\n buf[6]=mat4(vec4(-1.61102,0.7970257,1.4675229,0.20917463),vec4(-28.793737,-7.1390953,1.5025433,4.656581),vec4(-10.94861,39.66238,0.74318546,-10.095605),vec4(-0.7229728,-1.5483948,0.7301322,2.1687684))*buf[0]+mat4(vec4(3.2547753,21.489103,-1.0194173,-3.3100595),vec4(-3.7316632,-3.3792162,-7.223193,-0.23685838),vec4(13.1804495,0.7916005,5.338587,5.687114),vec4(-4.167605,-17.798311,-6.815736,-1.6451967))*buf[1]+mat4(vec4(0.604885,-7.800309,-7.213122,-2.741014),vec4(-3.522382,-0.12359311,-0.5258442,0.43852118),vec4(9.6752825,-22.853785,2.062431,0.099892326),vec4(-4.3196306,-17.730087,2.5184598,5.30267))*buf[2]+mat4(vec4(-6.545563,-15.790176,-6.0438633,-5.415399),vec4(-43.591583,28.551912,-16.00161,18.84728),vec4(4.212382,8.394307,3.0958717,8.657522),vec4(-5.0237565,-4.450633,-4.4768,-5.5010443))*buf[3]+mat4(vec4(1.6985557,-67.05806,6.897715,1.9004834),vec4(1.8680354,2.3915145,2.5231109,4.081538),vec4(11.158006,1.7294737,2.0738268,7.386411),vec4(-4.256034,-306.24686,8.258898,-17.132736))*buf[4]+mat4(vec4(1.6889864,-4.5852966,3.8534803,-6.3482175),vec4(1.3543309,-1.2640043,9.932754,2.9079645),vec4(-5.2770967,0.07150358,-0.13962056,3.3269649),vec4(28.34703,-4.918278,6.1044083,4.085355))*buf[5]+vec4(6.6818056,12.522166,-3.7075126,-4.104386);\n buf[7]=mat4(vec4(-8.265602,-4.7027016,5.098234,0.7509808),vec4(8.6507845,-17.15949,16.51939,-8.884479),vec4(-4.036479,-2.3946867,-2.6055532,-1.9866527),vec4(-2.2167742,-1.8135649,-5.9759874,4.8846445))*buf[0]+mat4(vec4(6.7790847,3.5076547,-2.8191125,-2.7028968),vec4(-5.743024,-0.27844876,1.4958696,-5.0517144),vec4(13.122226,15.735168,-2.9397483,-4.101023),vec4(-14.375265,-5.030483,-6.2599335,2.9848232))*buf[1]+mat4(vec4(4.0950394,-0.94011575,-5.674733,4.755022),vec4(4.3809423,4.8310084,1.7425908,-3.437416),vec4(2.117492,0.16342592,-104.56341,16.949184),vec4(-5.22543,-2.994248,3.8350096,-1.9364246))*buf[2]+mat4(vec4(-5.900337,1.7946124,-13.604192,-3.8060522),vec4(6.6583457,31.911177,25.164474,91.81147),vec4(11.840538,4.1503043,-0.7314397,6.768467),vec4(-6.3967767,4.034772,6.1714606,-0.32874924))*buf[3]+mat4(vec4(3.4992442,-196.91893,-8.923708,2.8142626),vec4(3.4806502,-3.1846354,5.1725626,5.1804223),vec4(-2.4009497,15.585794,1.2863957,2.0252278),vec4(-71.25271,-62.441242,-8.138444,0.50670296))*buf[4]+mat4(vec4(-12.291733,-11.176166,-7.3474145,4.390294),vec4(10.805477,5.6337385,-0.9385842,-4.7348723),vec4(-12.869276,-7.039391,5.3029537,7.5436664),vec4(1.4593618,8.91898,3.5101583,5.840625))*buf[5]+vec4(2.2415268,-6.705987,-0.98861027,-2.117676);\n buf[6]=sigmoid(buf[6]);buf[7]=sigmoid(buf[7]);\n buf[0]=mat4(vec4(1.6794263,1.3817469,2.9625452,0.),vec4(-1.8834411,-1.4806935,-3.5924516,0.),vec4(-1.3279216,-1.0918057,-2.3124623,0.),vec4(0.2662234,0.23235129,0.44178495,0.))*buf[0]+mat4(vec4(-0.6299101,-0.5945583,-0.9125601,0.),vec4(0.17828953,0.18300213,0.18182953,0.),vec4(-2.96544,-2.5819945,-4.9001055,0.),vec4(1.4195864,1.1868085,2.5176322,0.))*buf[1]+mat4(vec4(-1.2584374,-1.0552157,-2.1688404,0.),vec4(-0.7200217,-0.52666044,-1.438251,0.),vec4(0.15345335,0.15196142,0.272854,0.),vec4(0.945728,0.8861938,1.2766753,0.))*buf[2]+mat4(vec4(-2.4218085,-1.968602,-4.35166,0.),vec4(-22.683098,-18.0544,-41.954372,0.),vec4(0.63792,0.5470648,1.1078634,0.),vec4(-1.5489894,-1.3075932,-2.6444845,0.))*buf[3]+mat4(vec4(-0.49252132,-0.39877754,-0.91366625,0.),vec4(0.95609266,0.7923952,1.640221,0.),vec4(0.30616966,0.15693925,0.8639857,0.),vec4(1.1825981,0.94504964,2.176963,0.))*buf[4]+mat4(vec4(0.35446745,0.3293795,0.59547555,0.),vec4(-0.58784515,-0.48177817,-1.0614829,0.),vec4(2.5271258,1.9991658,4.6846647,0.),vec4(0.13042648,0.08864098,0.30187556,0.))*buf[5]+mat4(vec4(-1.7718065,-1.4033192,-3.3355875,0.),vec4(3.1664357,2.638297,5.378702,0.),vec4(-3.1724713,-2.6107926,-5.549295,0.),vec4(-2.851368,-2.249092,-5.3013067,0.))*buf[6]+mat4(vec4(1.5203838,1.2212278,2.8404984,0.),vec4(1.5210563,1.2651345,2.683903,0.),vec4(2.9789467,2.4364579,5.2347264,0.),vec4(2.2270417,1.8825914,3.8028636,0.))*buf[7]+vec4(-1.5468478,-3.6171484,0.24762098,0.);\n buf[0]=sigmoid(buf[0]);\n return vec4(buf[0].x,buf[0].y,buf[0].z,1.);\n}\n\nvoid mainImage(out vec4 fragColor,in vec2 fragCoord){\n vec2 uv=fragCoord/uResolution.xy*2.-1.;\n uv.x *= uResolution.x / uResolution.y;\n uv.y*=-1.;\n uv+=uWarp*vec2(sin(uv.y*6.283+uTime*0.5),cos(uv.x*6.283+uTime*0.5))*0.05;\n fragColor=cppn_fn(uv,0.1*sin(0.3*uTime),0.1*sin(0.69*uTime),0.1*sin(0.44*uTime));\n}\n\nvoid main(){\n vec4 col;mainImage(col,gl_FragCoord.xy);\n col.rgb=hueShiftRGB(col.rgb,uHueShift);\n float scanline_val=sin(gl_FragCoord.y*uScanFreq)*0.5+0.5;\n col.rgb*=1.-(scanline_val*scanline_val)*uScan;\n col.rgb+=(rand(gl_FragCoord.xy+uTime)-0.5)*uNoise;\n gl_FragColor=vec4(clamp(col.rgb,0.0,1.0),1.0);\n}\n`;\n\nexport default function DarkVeil({\n hueShift = 0,\n noiseIntensity = 0,\n scanlineIntensity = 0,\n speed = 0.5,\n scanlineFrequency = 0,\n warpAmount = 0,\n resolutionScale = 1\n }) {\n const ref = useRef(null);\n useEffect(() => {\n const canvas = ref.current;\n const parent = canvas.parentElement;\n\n const renderer = new Renderer({\n dpr: Math.min(window.devicePixelRatio, 2),\n canvas\n });\n\n const gl = renderer.gl;\n const geometry = new Triangle(gl);\n\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n uTime: { value: 0 },\n uResolution: { value: new Vec2() },\n uHueShift: { value: hueShift },\n uNoise: { value: noiseIntensity },\n uScan: { value: scanlineIntensity },\n uScanFreq: { value: scanlineFrequency },\n uWarp: { value: warpAmount }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n\n const resize = () => {\n const w = parent.clientWidth,\n h = parent.clientHeight;\n renderer.setSize(w * resolutionScale, h * resolutionScale);\n program.uniforms.uResolution.value.set(w, h);\n };\n\n window.addEventListener('resize', resize);\n resize();\n\n const start = performance.now();\n let frame = 0;\n\n const loop = () => {\n program.uniforms.uTime.value = ((performance.now() - start) / 1000) * speed;\n program.uniforms.uHueShift.value = hueShift;\n program.uniforms.uNoise.value = noiseIntensity;\n program.uniforms.uScan.value = scanlineIntensity;\n program.uniforms.uScanFreq.value = scanlineFrequency;\n program.uniforms.uWarp.value = warpAmount;\n renderer.render({ scene: mesh });\n frame = requestAnimationFrame(loop);\n };\n\n loop();\n\n return () => {\n cancelAnimationFrame(frame);\n window.removeEventListener('resize', resize);\n };\n }, [hueShift, noiseIntensity, scanlineIntensity, speed, scanlineFrequency, warpAmount, resolutionScale]);\n\n return ;\n}" } ], diff --git a/public/r/DarkVeil-TS-CSS.json b/public/r/DarkVeil-TS-CSS.json index c1b4f12d..9934974e 100644 --- a/public/r/DarkVeil-TS-CSS.json +++ b/public/r/DarkVeil-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "DarkVeil/DarkVeil.css", + "type": "registry:file", + "path": "DarkVeil.css", + "target": "@components/DarkVeil.css", "content": ".darkveil-canvas {\n width: 100%;\n height: 100%;\n display: block;\n}\n" }, { "type": "registry:component", - "path": "DarkVeil/DarkVeil.tsx", + "path": "DarkVeil.tsx", "content": "import { useRef, useEffect } from 'react';\nimport { Renderer, Program, Mesh, Triangle, Vec2 } from 'ogl';\nimport './DarkVeil.css';\n\nconst vertex = `\nattribute vec2 position;\nvoid main(){gl_Position=vec4(position,0.0,1.0);}\n`;\n\nconst fragment = `\n#ifdef GL_ES\nprecision lowp float;\n#endif\nuniform vec2 uResolution;\nuniform float uTime;\nuniform float uHueShift;\nuniform float uNoise;\nuniform float uScan;\nuniform float uScanFreq;\nuniform float uWarp;\n#define iTime uTime\n#define iResolution uResolution\n\nvec4 buf[8];\nfloat rand(vec2 c){return fract(sin(dot(c,vec2(12.9898,78.233)))*43758.5453);}\n\nmat3 rgb2yiq=mat3(0.299,0.587,0.114,0.596,-0.274,-0.322,0.211,-0.523,0.312);\nmat3 yiq2rgb=mat3(1.0,0.956,0.621,1.0,-0.272,-0.647,1.0,-1.106,1.703);\n\nvec3 hueShiftRGB(vec3 col,float deg){\n vec3 yiq=rgb2yiq*col;\n float rad=radians(deg);\n float cosh=cos(rad),sinh=sin(rad);\n vec3 yiqShift=vec3(yiq.x,yiq.y*cosh-yiq.z*sinh,yiq.y*sinh+yiq.z*cosh);\n return clamp(yiq2rgb*yiqShift,0.0,1.0);\n}\n\nvec4 sigmoid(vec4 x){return 1./(1.+exp(-x));}\n\nvec4 cppn_fn(vec2 coordinate,float in0,float in1,float in2){\n buf[6]=vec4(coordinate.x,coordinate.y,0.3948333106474662+in0,0.36+in1);\n buf[7]=vec4(0.14+in2,sqrt(coordinate.x*coordinate.x+coordinate.y*coordinate.y),0.,0.);\n buf[0]=mat4(vec4(6.5404263,-3.6126034,0.7590882,-1.13613),vec4(2.4582713,3.1660357,1.2219609,0.06276096),vec4(-5.478085,-6.159632,1.8701609,-4.7742867),vec4(6.039214,-5.542865,-0.90925294,3.251348))*buf[6]+mat4(vec4(0.8473259,-5.722911,3.975766,1.6522468),vec4(-0.24321538,0.5839259,-1.7661959,-5.350116),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(0.21808943,1.1243913,-1.7969975,5.0294676);\n buf[1]=mat4(vec4(-3.3522482,-6.0612736,0.55641043,-4.4719114),vec4(0.8631464,1.7432913,5.643898,1.6106541),vec4(2.4941394,-3.5012043,1.7184316,6.357333),vec4(3.310376,8.209261,1.1355612,-1.165539))*buf[6]+mat4(vec4(5.24046,-13.034365,0.009859298,15.870829),vec4(2.987511,3.129433,-0.89023495,-1.6822904),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-5.9457836,-6.573602,-0.8812491,1.5436668);\n buf[0]=sigmoid(buf[0]);buf[1]=sigmoid(buf[1]);\n buf[2]=mat4(vec4(-15.219568,8.095543,-2.429353,-1.9381982),vec4(-5.951362,4.3115187,2.6393783,1.274315),vec4(-7.3145227,6.7297835,5.2473326,5.9411426),vec4(5.0796127,8.979051,-1.7278991,-1.158976))*buf[6]+mat4(vec4(-11.967154,-11.608155,6.1486754,11.237008),vec4(2.124141,-6.263192,-1.7050359,-0.7021966),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-4.17164,-3.2281182,-4.576417,-3.6401186);\n buf[3]=mat4(vec4(3.1832156,-13.738922,1.879223,3.233465),vec4(0.64300746,12.768129,1.9141049,0.50990224),vec4(-0.049295485,4.4807224,1.4733979,1.801449),vec4(5.0039253,13.000481,3.3991797,-4.5561905))*buf[6]+mat4(vec4(-0.1285731,7.720628,-3.1425676,4.742367),vec4(0.6393625,3.714393,-0.8108378,-0.39174938),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-1.1811101,-21.621881,0.7851888,1.2329718);\n buf[2]=sigmoid(buf[2]);buf[3]=sigmoid(buf[3]);\n buf[4]=mat4(vec4(5.214916,-7.183024,2.7228765,2.6592617),vec4(-5.601878,-25.3591,4.067988,0.4602802),vec4(-10.57759,24.286327,21.102104,37.546658),vec4(4.3024497,-1.9625226,2.3458803,-1.372816))*buf[0]+mat4(vec4(-17.6526,-10.507558,2.2587414,12.462782),vec4(6.265566,-502.75443,-12.642513,0.9112289),vec4(-10.983244,20.741234,-9.701768,-0.7635988),vec4(5.383626,1.4819539,-4.1911616,-4.8444734))*buf[1]+mat4(vec4(12.785233,-16.345072,-0.39901125,1.7955981),vec4(-30.48365,-1.8345358,1.4542528,-1.1118771),vec4(19.872723,-7.337935,-42.941723,-98.52709),vec4(8.337645,-2.7312303,-2.2927687,-36.142323))*buf[2]+mat4(vec4(-16.298317,3.5471997,-0.44300047,-9.444417),vec4(57.5077,-35.609753,16.163465,-4.1534753),vec4(-0.07470326,-3.8656476,-7.0901804,3.1523974),vec4(-12.559385,-7.077619,1.490437,-0.8211543))*buf[3]+vec4(-7.67914,15.927437,1.3207729,-1.6686112);\n buf[5]=mat4(vec4(-1.4109162,-0.372762,-3.770383,-21.367174),vec4(-6.2103205,-9.35908,0.92529047,8.82561),vec4(11.460242,-22.348068,13.625772,-18.693201),vec4(-0.3429052,-3.9905605,-2.4626114,-0.45033523))*buf[0]+mat4(vec4(7.3481627,-4.3661838,-6.3037653,-3.868115),vec4(1.5462853,6.5488915,1.9701879,-0.58291394),vec4(6.5858274,-2.2180402,3.7127688,-1.3730392),vec4(-5.7973905,10.134961,-2.3395722,-5.965605))*buf[1]+mat4(vec4(-2.5132585,-6.6685553,-1.4029363,-0.16285264),vec4(-0.37908727,0.53738135,4.389061,-1.3024765),vec4(-0.70647055,2.0111287,-5.1659346,-3.728635),vec4(-13.562562,10.487719,-0.9173751,-2.6487076))*buf[2]+mat4(vec4(-8.645013,6.5546675,-6.3944063,-5.5933375),vec4(-0.57783127,-1.077275,36.91025,5.736769),vec4(14.283112,3.7146652,7.1452246,-4.5958776),vec4(2.7192075,3.6021907,-4.366337,-2.3653464))*buf[3]+vec4(-5.9000807,-4.329569,1.2427121,8.59503);\n buf[4]=sigmoid(buf[4]);buf[5]=sigmoid(buf[5]);\n buf[6]=mat4(vec4(-1.61102,0.7970257,1.4675229,0.20917463),vec4(-28.793737,-7.1390953,1.5025433,4.656581),vec4(-10.94861,39.66238,0.74318546,-10.095605),vec4(-0.7229728,-1.5483948,0.7301322,2.1687684))*buf[0]+mat4(vec4(3.2547753,21.489103,-1.0194173,-3.3100595),vec4(-3.7316632,-3.3792162,-7.223193,-0.23685838),vec4(13.1804495,0.7916005,5.338587,5.687114),vec4(-4.167605,-17.798311,-6.815736,-1.6451967))*buf[1]+mat4(vec4(0.604885,-7.800309,-7.213122,-2.741014),vec4(-3.522382,-0.12359311,-0.5258442,0.43852118),vec4(9.6752825,-22.853785,2.062431,0.099892326),vec4(-4.3196306,-17.730087,2.5184598,5.30267))*buf[2]+mat4(vec4(-6.545563,-15.790176,-6.0438633,-5.415399),vec4(-43.591583,28.551912,-16.00161,18.84728),vec4(4.212382,8.394307,3.0958717,8.657522),vec4(-5.0237565,-4.450633,-4.4768,-5.5010443))*buf[3]+mat4(vec4(1.6985557,-67.05806,6.897715,1.9004834),vec4(1.8680354,2.3915145,2.5231109,4.081538),vec4(11.158006,1.7294737,2.0738268,7.386411),vec4(-4.256034,-306.24686,8.258898,-17.132736))*buf[4]+mat4(vec4(1.6889864,-4.5852966,3.8534803,-6.3482175),vec4(1.3543309,-1.2640043,9.932754,2.9079645),vec4(-5.2770967,0.07150358,-0.13962056,3.3269649),vec4(28.34703,-4.918278,6.1044083,4.085355))*buf[5]+vec4(6.6818056,12.522166,-3.7075126,-4.104386);\n buf[7]=mat4(vec4(-8.265602,-4.7027016,5.098234,0.7509808),vec4(8.6507845,-17.15949,16.51939,-8.884479),vec4(-4.036479,-2.3946867,-2.6055532,-1.9866527),vec4(-2.2167742,-1.8135649,-5.9759874,4.8846445))*buf[0]+mat4(vec4(6.7790847,3.5076547,-2.8191125,-2.7028968),vec4(-5.743024,-0.27844876,1.4958696,-5.0517144),vec4(13.122226,15.735168,-2.9397483,-4.101023),vec4(-14.375265,-5.030483,-6.2599335,2.9848232))*buf[1]+mat4(vec4(4.0950394,-0.94011575,-5.674733,4.755022),vec4(4.3809423,4.8310084,1.7425908,-3.437416),vec4(2.117492,0.16342592,-104.56341,16.949184),vec4(-5.22543,-2.994248,3.8350096,-1.9364246))*buf[2]+mat4(vec4(-5.900337,1.7946124,-13.604192,-3.8060522),vec4(6.6583457,31.911177,25.164474,91.81147),vec4(11.840538,4.1503043,-0.7314397,6.768467),vec4(-6.3967767,4.034772,6.1714606,-0.32874924))*buf[3]+mat4(vec4(3.4992442,-196.91893,-8.923708,2.8142626),vec4(3.4806502,-3.1846354,5.1725626,5.1804223),vec4(-2.4009497,15.585794,1.2863957,2.0252278),vec4(-71.25271,-62.441242,-8.138444,0.50670296))*buf[4]+mat4(vec4(-12.291733,-11.176166,-7.3474145,4.390294),vec4(10.805477,5.6337385,-0.9385842,-4.7348723),vec4(-12.869276,-7.039391,5.3029537,7.5436664),vec4(1.4593618,8.91898,3.5101583,5.840625))*buf[5]+vec4(2.2415268,-6.705987,-0.98861027,-2.117676);\n buf[6]=sigmoid(buf[6]);buf[7]=sigmoid(buf[7]);\n buf[0]=mat4(vec4(1.6794263,1.3817469,2.9625452,0.),vec4(-1.8834411,-1.4806935,-3.5924516,0.),vec4(-1.3279216,-1.0918057,-2.3124623,0.),vec4(0.2662234,0.23235129,0.44178495,0.))*buf[0]+mat4(vec4(-0.6299101,-0.5945583,-0.9125601,0.),vec4(0.17828953,0.18300213,0.18182953,0.),vec4(-2.96544,-2.5819945,-4.9001055,0.),vec4(1.4195864,1.1868085,2.5176322,0.))*buf[1]+mat4(vec4(-1.2584374,-1.0552157,-2.1688404,0.),vec4(-0.7200217,-0.52666044,-1.438251,0.),vec4(0.15345335,0.15196142,0.272854,0.),vec4(0.945728,0.8861938,1.2766753,0.))*buf[2]+mat4(vec4(-2.4218085,-1.968602,-4.35166,0.),vec4(-22.683098,-18.0544,-41.954372,0.),vec4(0.63792,0.5470648,1.1078634,0.),vec4(-1.5489894,-1.3075932,-2.6444845,0.))*buf[3]+mat4(vec4(-0.49252132,-0.39877754,-0.91366625,0.),vec4(0.95609266,0.7923952,1.640221,0.),vec4(0.30616966,0.15693925,0.8639857,0.),vec4(1.1825981,0.94504964,2.176963,0.))*buf[4]+mat4(vec4(0.35446745,0.3293795,0.59547555,0.),vec4(-0.58784515,-0.48177817,-1.0614829,0.),vec4(2.5271258,1.9991658,4.6846647,0.),vec4(0.13042648,0.08864098,0.30187556,0.))*buf[5]+mat4(vec4(-1.7718065,-1.4033192,-3.3355875,0.),vec4(3.1664357,2.638297,5.378702,0.),vec4(-3.1724713,-2.6107926,-5.549295,0.),vec4(-2.851368,-2.249092,-5.3013067,0.))*buf[6]+mat4(vec4(1.5203838,1.2212278,2.8404984,0.),vec4(1.5210563,1.2651345,2.683903,0.),vec4(2.9789467,2.4364579,5.2347264,0.),vec4(2.2270417,1.8825914,3.8028636,0.))*buf[7]+vec4(-1.5468478,-3.6171484,0.24762098,0.);\n buf[0]=sigmoid(buf[0]);\n return vec4(buf[0].x,buf[0].y,buf[0].z,1.);\n}\n\nvoid mainImage(out vec4 fragColor,in vec2 fragCoord){\n vec2 uv=fragCoord/uResolution.xy*2.-1.;\n uv.x*=uResolution.x/uResolution.y;\n uv.y*=-1.;\n uv+=uWarp*vec2(sin(uv.y*6.283+uTime*0.5),cos(uv.x*6.283+uTime*0.5))*0.05;\n fragColor=cppn_fn(uv,0.1*sin(0.3*uTime),0.1*sin(0.69*uTime),0.1*sin(0.44*uTime));\n}\n\nvoid main(){\n vec4 col;mainImage(col,gl_FragCoord.xy);\n col.rgb=hueShiftRGB(col.rgb,uHueShift);\n float scanline_val=sin(gl_FragCoord.y*uScanFreq)*0.5+0.5;\n col.rgb*=1.-(scanline_val*scanline_val)*uScan;\n col.rgb+=(rand(gl_FragCoord.xy+uTime)-0.5)*uNoise;\n gl_FragColor=vec4(clamp(col.rgb,0.0,1.0),1.0);\n}\n`;\n\ntype Props = {\n hueShift?: number;\n noiseIntensity?: number;\n scanlineIntensity?: number;\n speed?: number;\n scanlineFrequency?: number;\n warpAmount?: number;\n resolutionScale?: number;\n};\n\nexport default function DarkVeil({\n hueShift = 0,\n noiseIntensity = 0,\n scanlineIntensity = 0,\n speed = 0.5,\n scanlineFrequency = 0,\n warpAmount = 0,\n resolutionScale = 1\n }: Props) {\n const ref = useRef(null);\n useEffect(() => {\n const canvas = ref.current as HTMLCanvasElement;\n const parent = canvas.parentElement as HTMLElement;\n\n const renderer = new Renderer({\n dpr: Math.min(window.devicePixelRatio, 2),\n canvas\n });\n\n const gl = renderer.gl;\n const geometry = new Triangle(gl);\n\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n uTime: { value: 0 },\n uResolution: { value: new Vec2() },\n uHueShift: { value: hueShift },\n uNoise: { value: noiseIntensity },\n uScan: { value: scanlineIntensity },\n uScanFreq: { value: scanlineFrequency },\n uWarp: { value: warpAmount }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n\n const resize = () => {\n const w = parent.clientWidth,\n h = parent.clientHeight;\n renderer.setSize(w * resolutionScale, h * resolutionScale);\n program.uniforms.uResolution.value.set(w, h);\n };\n\n window.addEventListener('resize', resize);\n resize();\n\n const start = performance.now();\n let frame = 0;\n\n const loop = () => {\n program.uniforms.uTime.value = ((performance.now() - start) / 1000) * speed;\n program.uniforms.uHueShift.value = hueShift;\n program.uniforms.uNoise.value = noiseIntensity;\n program.uniforms.uScan.value = scanlineIntensity;\n program.uniforms.uScanFreq.value = scanlineFrequency;\n program.uniforms.uWarp.value = warpAmount;\n renderer.render({ scene: mesh });\n frame = requestAnimationFrame(loop);\n };\n\n loop();\n\n return () => {\n cancelAnimationFrame(frame);\n window.removeEventListener('resize', resize);\n };\n }, [hueShift, noiseIntensity, scanlineIntensity, speed, scanlineFrequency, warpAmount, resolutionScale]);\n return ;\n}" } ], diff --git a/public/r/DecayCard-JS-CSS.json b/public/r/DecayCard-JS-CSS.json index bcc7301f..5cf85204 100644 --- a/public/r/DecayCard-JS-CSS.json +++ b/public/r/DecayCard-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "DecayCard/DecayCard.css", + "type": "registry:file", + "path": "DecayCard.css", + "target": "@components/DecayCard.css", "content": ".content {\n position: relative;\n}\n\n.svg {\n position: relative;\n width: 100%;\n height: 100%;\n display: block;\n will-change: transform;\n}\n\n.card-text {\n position: absolute;\n bottom: 1.2em;\n letter-spacing: -0.5px;\n font-weight: 900;\n left: 1em;\n font-size: 2.5rem;\n line-height: 1.5em;\n}\n\n.card-text::first-line {\n font-size: 4rem;\n}\n" }, { "type": "registry:component", - "path": "DecayCard/DecayCard.jsx", + "path": "DecayCard.jsx", "content": "import { useEffect, useRef } from 'react';\nimport { gsap } from 'gsap';\n\nimport './DecayCard.css';\n\nconst DecayCard = ({\n width = 300,\n height = 400,\n image = 'https://picsum.photos/300/400?grayscale',\n baseFrequency = 0.015,\n numOctaves = 5,\n seed = 4,\n maxDisplacement = 400,\n movementBound = 50,\n children\n}) => {\n const svgRef = useRef(null);\n const displacementMapRef = useRef(null);\n const cursor = useRef({\n x: typeof window !== 'undefined' ? window.innerWidth / 2 : 0,\n y: typeof window !== 'undefined' ? window.innerHeight / 2 : 0\n });\n const cachedCursor = useRef({ ...cursor.current });\n const winsize = useRef({\n width: typeof window !== 'undefined' ? window.innerWidth : 0,\n height: typeof window !== 'undefined' ? window.innerHeight : 0\n });\n\n useEffect(() => {\n const lerp = (a, b, n) => (1 - n) * a + n * b;\n\n const map = (x, a, b, c, d) => ((x - a) * (d - c)) / (b - a) + c;\n\n const distance = (x1, x2, y1, y2) => {\n const a = x1 - x2;\n const b = y1 - y2;\n return Math.hypot(a, b);\n };\n\n const handleResize = () => {\n winsize.current = { width: window.innerWidth, height: window.innerHeight };\n };\n\n const handleMouseMove = ev => {\n cursor.current = { x: ev.clientX, y: ev.clientY };\n };\n\n window.addEventListener('resize', handleResize);\n window.addEventListener('mousemove', handleMouseMove);\n\n const imgValues = {\n imgTransforms: { x: 0, y: 0, rz: 0 },\n displacementScale: 0\n };\n\n const render = () => {\n let targetX = lerp(imgValues.imgTransforms.x, map(cursor.current.x, 0, winsize.current.width, -120, 120), 0.1);\n let targetY = lerp(imgValues.imgTransforms.y, map(cursor.current.y, 0, winsize.current.height, -120, 120), 0.1);\n let targetRz = lerp(imgValues.imgTransforms.rz, map(cursor.current.x, 0, winsize.current.width, -10, 10), 0.1);\n\n if (targetX > movementBound) targetX = movementBound + (targetX - movementBound) * 0.2;\n if (targetX < -movementBound) targetX = -movementBound + (targetX + movementBound) * 0.2;\n if (targetY > movementBound) targetY = movementBound + (targetY - movementBound) * 0.2;\n if (targetY < -movementBound) targetY = -movementBound + (targetY + movementBound) * 0.2;\n\n imgValues.imgTransforms.x = targetX;\n imgValues.imgTransforms.y = targetY;\n imgValues.imgTransforms.rz = targetRz;\n\n if (svgRef.current) {\n gsap.set(svgRef.current, {\n x: imgValues.imgTransforms.x,\n y: imgValues.imgTransforms.y,\n rotateZ: imgValues.imgTransforms.rz\n });\n }\n\n const cursorTravelledDistance = distance(\n cachedCursor.current.x,\n cursor.current.x,\n cachedCursor.current.y,\n cursor.current.y\n );\n imgValues.displacementScale = lerp(\n imgValues.displacementScale,\n map(cursorTravelledDistance, 0, 200, 0, maxDisplacement),\n 0.06\n );\n\n if (displacementMapRef.current) {\n gsap.set(displacementMapRef.current, { attr: { scale: imgValues.displacementScale } });\n }\n\n cachedCursor.current = { ...cursor.current };\n\n rafId = requestAnimationFrame(render);\n };\n\n let rafId = requestAnimationFrame(render);\n\n return () => {\n cancelAnimationFrame(rafId);\n window.removeEventListener('resize', handleResize);\n window.removeEventListener('mousemove', handleMouseMove);\n };\n }, [maxDisplacement, movementBound]);\n\n return (\n
\n \n \n \n \n \n \n \n \n \n
{children}
\n
\n );\n};\n\nexport default DecayCard;\n" } ], diff --git a/public/r/DecayCard-TS-CSS.json b/public/r/DecayCard-TS-CSS.json index 977803df..9e2e172e 100644 --- a/public/r/DecayCard-TS-CSS.json +++ b/public/r/DecayCard-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "DecayCard/DecayCard.css", + "type": "registry:file", + "path": "DecayCard.css", + "target": "@components/DecayCard.css", "content": ".content {\n position: relative;\n}\n\n.svg {\n position: relative;\n width: 100%;\n height: 100%;\n display: block;\n will-change: transform;\n}\n\n.card-text {\n position: absolute;\n bottom: 1.2em;\n letter-spacing: -0.5px;\n font-weight: 900;\n left: 1em;\n font-size: 2.5rem;\n line-height: 1.5em;\n}\n\n.card-text::first-line {\n font-size: 6rem;\n}\n" }, { "type": "registry:component", - "path": "DecayCard/DecayCard.tsx", - "content": "import React, { useEffect, useRef, ReactNode } from 'react';\nimport { gsap } from 'gsap';\nimport './DecayCard.css';\n\ninterface DecayCardProps {\n width?: number;\n height?: number;\n image?: string;\n baseFrequency?: number;\n numOctaves?: number;\n seed?: number;\n maxDisplacement?: number;\n movementBound?: number;\n children?: ReactNode;\n}\n\nconst DecayCard: React.FC = ({\n width = 300,\n height = 400,\n image = 'https://picsum.photos/300/400?grayscale',\n baseFrequency = 0.015,\n numOctaves = 5,\n seed = 4,\n maxDisplacement = 400,\n movementBound = 50,\n children\n}) => {\n const svgRef = useRef(null);\n const displacementMapRef = useRef(null);\n const cursor = useRef<{ x: number; y: number }>({\n x: typeof window !== 'undefined' ? window.innerWidth / 2 : 0,\n y: typeof window !== 'undefined' ? window.innerHeight / 2 : 0\n });\n const cachedCursor = useRef<{ x: number; y: number }>({ ...cursor.current });\n const winsize = useRef<{ width: number; height: number }>({\n width: typeof window !== 'undefined' ? window.innerWidth : 0,\n height: typeof window !== 'undefined' ? window.innerHeight : 0\n });\n\n useEffect(() => {\n const lerp = (a: number, b: number, n: number): number => (1 - n) * a + n * b;\n\n const map = (x: number, a: number, b: number, c: number, d: number): number => ((x - a) * (d - c)) / (b - a) + c;\n\n const distance = (x1: number, x2: number, y1: number, y2: number): number => {\n const a = x1 - x2;\n const b = y1 - y2;\n return Math.hypot(a, b);\n };\n\n const handleResize = (): void => {\n winsize.current = {\n width: window.innerWidth,\n height: window.innerHeight\n };\n };\n\n const handleMouseMove = (ev: MouseEvent): void => {\n cursor.current = { x: ev.clientX, y: ev.clientY };\n };\n\n window.addEventListener('resize', handleResize);\n window.addEventListener('mousemove', handleMouseMove);\n\n const imgValues = {\n imgTransforms: { x: 0, y: 0, rz: 0 },\n displacementScale: 0\n };\n\n const render = () => {\n let targetX = lerp(imgValues.imgTransforms.x, map(cursor.current.x, 0, winsize.current.width, -120, 120), 0.1);\n let targetY = lerp(imgValues.imgTransforms.y, map(cursor.current.y, 0, winsize.current.height, -120, 120), 0.1);\n let targetRz = lerp(imgValues.imgTransforms.rz, map(cursor.current.x, 0, winsize.current.width, -10, 10), 0.1);\n\n if (targetX > movementBound) targetX = movementBound + (targetX - movementBound) * 0.2;\n if (targetX < -movementBound) targetX = -movementBound + (targetX + movementBound) * 0.2;\n if (targetY > movementBound) targetY = movementBound + (targetY - movementBound) * 0.2;\n if (targetY < -movementBound) targetY = -movementBound + (targetY + movementBound) * 0.2;\n\n imgValues.imgTransforms.x = targetX;\n imgValues.imgTransforms.y = targetY;\n imgValues.imgTransforms.rz = targetRz;\n\n if (svgRef.current) {\n gsap.set(svgRef.current, {\n x: imgValues.imgTransforms.x,\n y: imgValues.imgTransforms.y,\n rotateZ: imgValues.imgTransforms.rz\n });\n }\n\n const cursorTravelledDistance = distance(\n cachedCursor.current.x,\n cursor.current.x,\n cachedCursor.current.y,\n cursor.current.y\n );\n imgValues.displacementScale = lerp(\n imgValues.displacementScale,\n map(cursorTravelledDistance, 0, 200, 0, maxDisplacement),\n 0.06\n );\n\n if (displacementMapRef.current) {\n gsap.set(displacementMapRef.current, {\n attr: { scale: imgValues.displacementScale }\n });\n }\n\n cachedCursor.current = { ...cursor.current };\n\n rafId = requestAnimationFrame(render);\n };\n\n let rafId = requestAnimationFrame(render);\n\n return () => {\n cancelAnimationFrame(rafId);\n window.removeEventListener('resize', handleResize);\n window.removeEventListener('mousemove', handleMouseMove);\n };\n }, [maxDisplacement, movementBound]);\n\n return (\n
\n \n \n \n \n \n \n \n \n \n
{children}
\n
\n );\n};\n\nexport default DecayCard;\n" + "path": "DecayCard.tsx", + "content": "import React, { useEffect, useRef, type ReactNode } from 'react';\nimport { gsap } from 'gsap';\nimport './DecayCard.css';\n\ninterface DecayCardProps {\n width?: number;\n height?: number;\n image?: string;\n baseFrequency?: number;\n numOctaves?: number;\n seed?: number;\n maxDisplacement?: number;\n movementBound?: number;\n children?: ReactNode;\n}\n\nconst DecayCard: React.FC = ({\n width = 300,\n height = 400,\n image = 'https://picsum.photos/300/400?grayscale',\n baseFrequency = 0.015,\n numOctaves = 5,\n seed = 4,\n maxDisplacement = 400,\n movementBound = 50,\n children\n}) => {\n const svgRef = useRef(null);\n const displacementMapRef = useRef(null);\n const cursor = useRef<{ x: number; y: number }>({\n x: typeof window !== 'undefined' ? window.innerWidth / 2 : 0,\n y: typeof window !== 'undefined' ? window.innerHeight / 2 : 0\n });\n const cachedCursor = useRef<{ x: number; y: number }>({ ...cursor.current });\n const winsize = useRef<{ width: number; height: number }>({\n width: typeof window !== 'undefined' ? window.innerWidth : 0,\n height: typeof window !== 'undefined' ? window.innerHeight : 0\n });\n\n useEffect(() => {\n const lerp = (a: number, b: number, n: number): number => (1 - n) * a + n * b;\n\n const map = (x: number, a: number, b: number, c: number, d: number): number => ((x - a) * (d - c)) / (b - a) + c;\n\n const distance = (x1: number, x2: number, y1: number, y2: number): number => {\n const a = x1 - x2;\n const b = y1 - y2;\n return Math.hypot(a, b);\n };\n\n const handleResize = (): void => {\n winsize.current = {\n width: window.innerWidth,\n height: window.innerHeight\n };\n };\n\n const handleMouseMove = (ev: MouseEvent): void => {\n cursor.current = { x: ev.clientX, y: ev.clientY };\n };\n\n window.addEventListener('resize', handleResize);\n window.addEventListener('mousemove', handleMouseMove);\n\n const imgValues = {\n imgTransforms: { x: 0, y: 0, rz: 0 },\n displacementScale: 0\n };\n\n const render = () => {\n let targetX = lerp(imgValues.imgTransforms.x, map(cursor.current.x, 0, winsize.current.width, -120, 120), 0.1);\n let targetY = lerp(imgValues.imgTransforms.y, map(cursor.current.y, 0, winsize.current.height, -120, 120), 0.1);\n let targetRz = lerp(imgValues.imgTransforms.rz, map(cursor.current.x, 0, winsize.current.width, -10, 10), 0.1);\n\n if (targetX > movementBound) targetX = movementBound + (targetX - movementBound) * 0.2;\n if (targetX < -movementBound) targetX = -movementBound + (targetX + movementBound) * 0.2;\n if (targetY > movementBound) targetY = movementBound + (targetY - movementBound) * 0.2;\n if (targetY < -movementBound) targetY = -movementBound + (targetY + movementBound) * 0.2;\n\n imgValues.imgTransforms.x = targetX;\n imgValues.imgTransforms.y = targetY;\n imgValues.imgTransforms.rz = targetRz;\n\n if (svgRef.current) {\n gsap.set(svgRef.current, {\n x: imgValues.imgTransforms.x,\n y: imgValues.imgTransforms.y,\n rotateZ: imgValues.imgTransforms.rz\n });\n }\n\n const cursorTravelledDistance = distance(\n cachedCursor.current.x,\n cursor.current.x,\n cachedCursor.current.y,\n cursor.current.y\n );\n imgValues.displacementScale = lerp(\n imgValues.displacementScale,\n map(cursorTravelledDistance, 0, 200, 0, maxDisplacement),\n 0.06\n );\n\n if (displacementMapRef.current) {\n gsap.set(displacementMapRef.current, {\n attr: { scale: imgValues.displacementScale }\n });\n }\n\n cachedCursor.current = { ...cursor.current };\n\n rafId = requestAnimationFrame(render);\n };\n\n let rafId = requestAnimationFrame(render);\n\n return () => {\n cancelAnimationFrame(rafId);\n window.removeEventListener('resize', handleResize);\n window.removeEventListener('mousemove', handleMouseMove);\n };\n }, [maxDisplacement, movementBound]);\n\n return (\n
\n \n \n \n \n \n \n \n \n \n
{children}
\n
\n );\n};\n\nexport default DecayCard;\n" } ], "registryDependencies": [], diff --git a/public/r/DecayCard-TS-TW.json b/public/r/DecayCard-TS-TW.json index c42e4fe4..c34dc7fc 100644 --- a/public/r/DecayCard-TS-TW.json +++ b/public/r/DecayCard-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "DecayCard/DecayCard.tsx", - "content": "import React, { useEffect, useRef, ReactNode } from 'react';\nimport { gsap } from 'gsap';\n\ninterface DecayCardProps {\n width?: number;\n height?: number;\n image?: string;\n baseFrequency?: number;\n numOctaves?: number;\n seed?: number;\n maxDisplacement?: number;\n movementBound?: number;\n children?: ReactNode;\n}\n\nconst DecayCard: React.FC = ({\n width = 300,\n height = 400,\n image = 'https://picsum.photos/300/400?grayscale',\n baseFrequency = 0.015,\n numOctaves = 5,\n seed = 4,\n maxDisplacement = 400,\n movementBound = 50,\n children\n}) => {\n const svgRef = useRef(null);\n const displacementMapRef = useRef(null);\n const cursor = useRef<{ x: number; y: number }>({\n x: typeof window !== 'undefined' ? window.innerWidth / 2 : 0,\n y: typeof window !== 'undefined' ? window.innerHeight / 2 : 0\n });\n const cachedCursor = useRef<{ x: number; y: number }>({ ...cursor.current });\n const winsize = useRef<{ width: number; height: number }>({\n width: typeof window !== 'undefined' ? window.innerWidth : 0,\n height: typeof window !== 'undefined' ? window.innerHeight : 0\n });\n\n useEffect(() => {\n const lerp = (a: number, b: number, n: number): number => (1 - n) * a + n * b;\n const map = (x: number, a: number, b: number, c: number, d: number): number => ((x - a) * (d - c)) / (b - a) + c;\n const distance = (x1: number, x2: number, y1: number, y2: number): number => Math.hypot(x1 - x2, y1 - y2);\n\n const handleResize = (): void => {\n winsize.current = {\n width: window.innerWidth,\n height: window.innerHeight\n };\n };\n\n const handleMouseMove = (ev: MouseEvent): void => {\n cursor.current = { x: ev.clientX, y: ev.clientY };\n };\n\n window.addEventListener('resize', handleResize);\n window.addEventListener('mousemove', handleMouseMove);\n\n const imgValues = {\n imgTransforms: { x: 0, y: 0, rz: 0 },\n displacementScale: 0\n };\n\n const render = () => {\n let targetX = lerp(imgValues.imgTransforms.x, map(cursor.current.x, 0, winsize.current.width, -120, 120), 0.1);\n let targetY = lerp(imgValues.imgTransforms.y, map(cursor.current.y, 0, winsize.current.height, -120, 120), 0.1);\n let targetRz = lerp(imgValues.imgTransforms.rz, map(cursor.current.x, 0, winsize.current.width, -10, 10), 0.1);\n\n if (targetX > movementBound) targetX = movementBound + (targetX - movementBound) * 0.2;\n if (targetX < -movementBound) targetX = -movementBound + (targetX + movementBound) * 0.2;\n if (targetY > movementBound) targetY = movementBound + (targetY - movementBound) * 0.2;\n if (targetY < -movementBound) targetY = -movementBound + (targetY + movementBound) * 0.2;\n\n imgValues.imgTransforms.x = targetX;\n imgValues.imgTransforms.y = targetY;\n imgValues.imgTransforms.rz = targetRz;\n\n if (svgRef.current) {\n gsap.set(svgRef.current, {\n x: imgValues.imgTransforms.x,\n y: imgValues.imgTransforms.y,\n rotateZ: imgValues.imgTransforms.rz\n });\n }\n\n const cursorTravelledDistance = distance(\n cachedCursor.current.x,\n cursor.current.x,\n cachedCursor.current.y,\n cursor.current.y\n );\n imgValues.displacementScale = lerp(\n imgValues.displacementScale,\n map(cursorTravelledDistance, 0, 200, 0, maxDisplacement),\n 0.06\n );\n\n if (displacementMapRef.current) {\n gsap.set(displacementMapRef.current, {\n attr: { scale: imgValues.displacementScale }\n });\n }\n\n cachedCursor.current = { ...cursor.current };\n\n rafId = requestAnimationFrame(render);\n };\n\n let rafId = requestAnimationFrame(render);\n\n return () => {\n cancelAnimationFrame(rafId);\n window.removeEventListener('resize', handleResize);\n window.removeEventListener('mousemove', handleMouseMove);\n };\n }, [maxDisplacement, movementBound]);\n\n return (\n
\n \n \n \n \n \n \n \n \n \n
\n {children}\n
\n
\n );\n};\n\nexport default DecayCard;\n" + "content": "import React, { useEffect, useRef, type ReactNode } from 'react';\nimport { gsap } from 'gsap';\n\ninterface DecayCardProps {\n width?: number;\n height?: number;\n image?: string;\n baseFrequency?: number;\n numOctaves?: number;\n seed?: number;\n maxDisplacement?: number;\n movementBound?: number;\n children?: ReactNode;\n}\n\nconst DecayCard: React.FC = ({\n width = 300,\n height = 400,\n image = 'https://picsum.photos/300/400?grayscale',\n baseFrequency = 0.015,\n numOctaves = 5,\n seed = 4,\n maxDisplacement = 400,\n movementBound = 50,\n children\n}) => {\n const svgRef = useRef(null);\n const displacementMapRef = useRef(null);\n const cursor = useRef<{ x: number; y: number }>({\n x: typeof window !== 'undefined' ? window.innerWidth / 2 : 0,\n y: typeof window !== 'undefined' ? window.innerHeight / 2 : 0\n });\n const cachedCursor = useRef<{ x: number; y: number }>({ ...cursor.current });\n const winsize = useRef<{ width: number; height: number }>({\n width: typeof window !== 'undefined' ? window.innerWidth : 0,\n height: typeof window !== 'undefined' ? window.innerHeight : 0\n });\n\n useEffect(() => {\n const lerp = (a: number, b: number, n: number): number => (1 - n) * a + n * b;\n const map = (x: number, a: number, b: number, c: number, d: number): number => ((x - a) * (d - c)) / (b - a) + c;\n const distance = (x1: number, x2: number, y1: number, y2: number): number => Math.hypot(x1 - x2, y1 - y2);\n\n const handleResize = (): void => {\n winsize.current = {\n width: window.innerWidth,\n height: window.innerHeight\n };\n };\n\n const handleMouseMove = (ev: MouseEvent): void => {\n cursor.current = { x: ev.clientX, y: ev.clientY };\n };\n\n window.addEventListener('resize', handleResize);\n window.addEventListener('mousemove', handleMouseMove);\n\n const imgValues = {\n imgTransforms: { x: 0, y: 0, rz: 0 },\n displacementScale: 0\n };\n\n const render = () => {\n let targetX = lerp(imgValues.imgTransforms.x, map(cursor.current.x, 0, winsize.current.width, -120, 120), 0.1);\n let targetY = lerp(imgValues.imgTransforms.y, map(cursor.current.y, 0, winsize.current.height, -120, 120), 0.1);\n let targetRz = lerp(imgValues.imgTransforms.rz, map(cursor.current.x, 0, winsize.current.width, -10, 10), 0.1);\n\n if (targetX > movementBound) targetX = movementBound + (targetX - movementBound) * 0.2;\n if (targetX < -movementBound) targetX = -movementBound + (targetX + movementBound) * 0.2;\n if (targetY > movementBound) targetY = movementBound + (targetY - movementBound) * 0.2;\n if (targetY < -movementBound) targetY = -movementBound + (targetY + movementBound) * 0.2;\n\n imgValues.imgTransforms.x = targetX;\n imgValues.imgTransforms.y = targetY;\n imgValues.imgTransforms.rz = targetRz;\n\n if (svgRef.current) {\n gsap.set(svgRef.current, {\n x: imgValues.imgTransforms.x,\n y: imgValues.imgTransforms.y,\n rotateZ: imgValues.imgTransforms.rz\n });\n }\n\n const cursorTravelledDistance = distance(\n cachedCursor.current.x,\n cursor.current.x,\n cachedCursor.current.y,\n cursor.current.y\n );\n imgValues.displacementScale = lerp(\n imgValues.displacementScale,\n map(cursorTravelledDistance, 0, 200, 0, maxDisplacement),\n 0.06\n );\n\n if (displacementMapRef.current) {\n gsap.set(displacementMapRef.current, {\n attr: { scale: imgValues.displacementScale }\n });\n }\n\n cachedCursor.current = { ...cursor.current };\n\n rafId = requestAnimationFrame(render);\n };\n\n let rafId = requestAnimationFrame(render);\n\n return () => {\n cancelAnimationFrame(rafId);\n window.removeEventListener('resize', handleResize);\n window.removeEventListener('mousemove', handleMouseMove);\n };\n }, [maxDisplacement, movementBound]);\n\n return (\n
\n \n \n \n \n \n \n \n \n \n
\n {children}\n
\n
\n );\n};\n\nexport default DecayCard;\n" } ], "registryDependencies": [], diff --git a/public/r/DepthCarousel-JS-CSS.json b/public/r/DepthCarousel-JS-CSS.json index 5f799ef7..45a4f88b 100644 --- a/public/r/DepthCarousel-JS-CSS.json +++ b/public/r/DepthCarousel-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "DepthCarousel/DepthCarousel.css", + "type": "registry:file", + "path": "DepthCarousel.css", + "target": "@components/DepthCarousel.css", "content": ".depth-carousel {\n position: relative;\n width: 100%;\n height: 100%;\n min-height: 320px;\n display: flex;\n align-items: center;\n justify-content: center;\n perspective: var(--dc-perspective, 1400px);\n perspective-origin: 50% 50%;\n touch-action: pan-y;\n outline: none;\n user-select: none;\n -webkit-user-select: none;\n cursor: grab;\n}\n\n.depth-carousel:active {\n cursor: grabbing;\n}\n\n.depth-carousel:focus-visible {\n outline: 2px solid rgba(255, 255, 255, 0.5);\n outline-offset: 4px;\n border-radius: 12px;\n}\n\n.depth-carousel__stage {\n position: absolute;\n inset: 0;\n transform-style: preserve-3d;\n}\n\n.depth-carousel__card {\n position: absolute;\n top: 50%;\n left: 50%;\n transform-origin: center center;\n overflow: hidden;\n background: #0b0d12;\n box-shadow:\n 0 30px 60px -20px rgba(0, 0, 0, 0.65),\n 0 8px 20px -10px rgba(0, 0, 0, 0.5);\n will-change: transform, opacity, filter;\n cursor: pointer;\n transform: translate(-50%, -50%);\n}\n\n.depth-carousel__img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n pointer-events: none;\n -webkit-user-drag: none;\n}\n\n.depth-carousel__tint {\n position: absolute;\n inset: 0;\n opacity: 0;\n pointer-events: none;\n mix-blend-mode: multiply;\n}\n\n.depth-carousel__arrow {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n z-index: 3000;\n width: 42px;\n height: 42px;\n display: grid;\n place-items: center;\n border: 1px solid rgba(255, 255, 255, 0.18);\n border-radius: 999px;\n background: rgba(18, 20, 26, 0.55);\n backdrop-filter: blur(8px);\n color: #fff;\n cursor: pointer;\n transition:\n background 0.2s ease,\n border-color 0.2s ease,\n transform 0.2s ease;\n}\n\n.depth-carousel__arrow:hover {\n background: rgba(28, 31, 40, 0.85);\n border-color: rgba(255, 255, 255, 0.4);\n}\n\n.depth-carousel__arrow:active {\n transform: translateY(-50%) scale(0.94);\n}\n\n.depth-carousel__arrow--prev {\n left: 16px;\n}\n\n.depth-carousel__arrow--next {\n right: 16px;\n}\n\n.depth-carousel__dots {\n position: absolute;\n bottom: 16px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 3000;\n display: flex;\n gap: 8px;\n padding: 8px 12px;\n border-radius: 999px;\n background: rgba(14, 16, 22, 0.4);\n backdrop-filter: blur(6px);\n}\n\n.depth-carousel__dot {\n width: 7px;\n height: 7px;\n padding: 0;\n border: none;\n border-radius: 999px;\n background: rgba(255, 255, 255, 0.32);\n cursor: pointer;\n transition:\n width 0.25s ease,\n background 0.25s ease;\n}\n\n.depth-carousel__dot.is-active {\n width: 20px;\n background: #fff;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .depth-carousel__card {\n will-change: auto;\n }\n .depth-carousel__arrow,\n .depth-carousel__dot {\n transition: none;\n }\n}\n" }, { "type": "registry:component", - "path": "DepthCarousel/DepthCarousel.jsx", + "path": "DepthCarousel.jsx", "content": "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport gsap from 'gsap';\nimport './DepthCarousel.css';\n\nconst DEFAULT_ITEMS = [\n { image: 'https://picsum.photos/seed/depth1/800/1000', alt: 'Slide 1' },\n { image: 'https://picsum.photos/seed/depth2/800/1000', alt: 'Slide 2' },\n { image: 'https://picsum.photos/seed/depth3/800/1000', alt: 'Slide 3' },\n { image: 'https://picsum.photos/seed/depth4/800/1000', alt: 'Slide 4' },\n { image: 'https://picsum.photos/seed/depth5/800/1000', alt: 'Slide 5' },\n { image: 'https://picsum.photos/seed/depth6/800/1000', alt: 'Slide 6' }\n];\n\nconst clamp = (v, min, max) => Math.min(Math.max(v, min), max);\nconst normalizeItem = it => (typeof it === 'string' ? { image: it, alt: '' } : it);\n\nconst DepthCarousel = ({\n items = DEFAULT_ITEMS,\n cardWidth = 300,\n cardHeight = 380,\n radius = 18,\n tint = '#05060a',\n depth = 220,\n spread = 90,\n tilt = 22,\n tiltDirection = 'right',\n perspective = 1400,\n visibleCards = 4,\n falloff = 0.2,\n blur = 6,\n duration = 700,\n ease = 'power3.out',\n autoplay = false,\n autoplayDelay = 3200,\n loop = true,\n showControls = true,\n showIndicators = true,\n onChange,\n className = ''\n}) => {\n const data = useMemo(() => (Array.isArray(items) ? items : []).map(normalizeItem), [items]);\n const count = data.length;\n\n const rootRef = useRef(null);\n const stageRef = useRef(null);\n const cardRefs = useRef([]);\n const overlayRefs = useRef([]);\n\n const posRef = useRef(0);\n const focusRef = useRef(0);\n const tweenRef = useRef(null);\n const scaleRef = useRef(1);\n const cfgRef = useRef({});\n const onChangeRef = useRef(onChange);\n\n const dragRef = useRef(null);\n const wheelTimerRef = useRef(null);\n const autoTimerRef = useRef(null);\n const reducedRef = useRef(false);\n\n const [active, setActive] = useState(0);\n\n onChangeRef.current = onChange;\n cfgRef.current = {\n count,\n depth,\n spread,\n tilt,\n tiltDirection,\n visibleCards,\n falloff,\n blur,\n duration,\n ease,\n loop,\n cardWidth,\n autoplayDelay\n };\n\n const layout = useCallback(pos => {\n const cfg = cfgRef.current;\n const n = cfg.count;\n if (!n) return;\n const dir = cfg.tiltDirection === 'left' ? -1 : 1;\n const sc = scaleRef.current;\n\n for (let i = 0; i < n; i++) {\n const el = cardRefs.current[i];\n if (!el) continue;\n\n let d = i - pos;\n if (cfg.loop && n > 1) {\n d = ((d % n) + n) % n;\n if (d > n / 2) d -= n;\n }\n\n const back = Math.max(0, d);\n const az = Math.abs(d);\n const shown = az <= cfg.visibleCards + 0.5;\n\n const tz = -cfg.depth * d;\n const tx = dir * cfg.spread * d;\n const ry = dir * cfg.tilt * clamp(d, 0, 1);\n\n let opacity = d < 0 ? Math.max(0, 1 + d) : 1;\n if (!shown) opacity = 0;\n\n const brightness = Math.max(0.15, 1 - back * cfg.falloff);\n const blurPx = cfg.blur > 0 ? Math.min(cfg.blur, (back / Math.max(1, cfg.visibleCards)) * cfg.blur) : 0;\n const zi = Math.round(2000 - d * 20);\n\n el.style.transform = `translate(-50%, -50%) scale(${sc}) translateX(${tx.toFixed(2)}px) translateZ(${tz.toFixed(2)}px) rotateY(${ry.toFixed(3)}deg)`;\n el.style.opacity = opacity.toFixed(3);\n el.style.filter = `brightness(${brightness.toFixed(3)}) blur(${blurPx.toFixed(2)}px)`;\n el.style.zIndex = String(zi);\n el.style.pointerEvents = shown && opacity > 0.05 ? 'auto' : 'none';\n\n const ov = overlayRefs.current[i];\n if (ov) ov.style.opacity = clamp(back * cfg.falloff * 1.25, 0, 0.86).toFixed(3);\n }\n }, []);\n\n const notify = useCallback(\n idx => {\n setActive(idx);\n onChangeRef.current?.(idx, data[idx]);\n },\n [data]\n );\n\n const tweenTo = useCallback(\n (target, animate) => {\n tweenRef.current?.kill();\n const cfg = cfgRef.current;\n const proxy = { p: posRef.current };\n const dur = animate && !reducedRef.current ? cfg.duration / 1000 : 0;\n tweenRef.current = gsap.to(proxy, {\n p: target,\n duration: dur,\n ease: cfg.ease,\n onUpdate: () => {\n posRef.current = proxy.p;\n layout(proxy.p);\n },\n onComplete: () => {\n const n = cfg.count;\n if (n > 0) posRef.current = ((posRef.current % n) + n) % n;\n layout(posRef.current);\n }\n });\n },\n [layout]\n );\n\n const setFocus = useCallback(\n (rawIndex, animate = true) => {\n const cfg = cfgRef.current;\n const n = cfg.count;\n if (!n) return;\n const idx = cfg.loop ? ((rawIndex % n) + n) % n : clamp(rawIndex, 0, n - 1);\n let delta = idx - posRef.current;\n if (cfg.loop && n > 1) {\n delta = ((delta % n) + n) % n;\n if (delta > n / 2) delta -= n;\n }\n tweenTo(posRef.current + delta, animate);\n if (idx !== focusRef.current) {\n focusRef.current = idx;\n notify(idx);\n }\n },\n [tweenTo, notify]\n );\n\n const navigateBy = useCallback(step => setFocus(focusRef.current + step, true), [setFocus]);\n\n useEffect(() => {\n const root = rootRef.current;\n if (!root) return;\n const ro = new ResizeObserver(entries => {\n const w = entries[0].contentRect.width;\n const cfg = cfgRef.current;\n const needed = cfg.cardWidth + Math.abs(cfg.spread) * 2 + 120;\n scaleRef.current = clamp(w / needed, 0.4, 1);\n layout(posRef.current);\n });\n ro.observe(root);\n return () => ro.disconnect();\n }, [layout]);\n\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const onWheel = e => {\n const cfg = cfgRef.current;\n if (cfg.count < 2) return;\n e.preventDefault();\n tweenRef.current?.kill();\n const raw = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;\n const delta = e.deltaMode === 1 ? raw * 24 : raw;\n const step = clamp(delta / (cfg.cardWidth * 0.9), -0.6, 0.6);\n posRef.current += step;\n layout(posRef.current);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n wheelTimerRef.current = setTimeout(() => setFocus(Math.round(posRef.current), true), 130);\n };\n el.addEventListener('wheel', onWheel, { passive: false });\n return () => {\n el.removeEventListener('wheel', onWheel);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n };\n }, [layout, setFocus]);\n\n const onPointerDown = useCallback(e => {\n const cfg = cfgRef.current;\n if (cfg.count < 2) return;\n tweenRef.current?.kill();\n dragRef.current = {\n x: e.clientX,\n startPos: posRef.current,\n lastX: e.clientX,\n lastT: performance.now(),\n v: 0,\n moved: false,\n id: e.pointerId\n };\n }, []);\n\n const onPointerMove = useCallback(\n e => {\n const drag = dragRef.current;\n if (!drag) return;\n const cfg = cfgRef.current;\n const stepPx = Math.max(cfg.cardWidth * 0.55 * scaleRef.current, 40);\n const dx = e.clientX - drag.x;\n if (!drag.moved && Math.abs(dx) > 4) {\n drag.moved = true;\n rootRef.current?.setPointerCapture(drag.id);\n }\n if (!drag.moved) return;\n const now = performance.now();\n const dt = Math.max(now - drag.lastT, 1);\n drag.v = (e.clientX - drag.lastX) / dt;\n drag.lastX = e.clientX;\n drag.lastT = now;\n posRef.current = drag.startPos - dx / stepPx;\n layout(posRef.current);\n },\n [layout]\n );\n\n const onPointerEnd = useCallback(() => {\n const drag = dragRef.current;\n if (!drag) return;\n dragRef.current = null;\n if (!drag.moved) return;\n const cfg = cfgRef.current;\n const stepPx = Math.max(cfg.cardWidth * 0.55 * scaleRef.current, 40);\n const projected = posRef.current - (drag.v * 180) / stepPx;\n setFocus(Math.round(projected), true);\n }, [setFocus]);\n\n const onKeyDown = useCallback(\n e => {\n if (e.key === 'ArrowLeft') {\n e.preventDefault();\n navigateBy(-1);\n } else if (e.key === 'ArrowRight') {\n e.preventDefault();\n navigateBy(1);\n }\n },\n [navigateBy]\n );\n\n const onCardClick = useCallback(\n index => {\n if (dragRef.current?.moved) return;\n setFocus(index, true);\n },\n [setFocus]\n );\n\n useEffect(() => {\n reducedRef.current = typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n if (!autoplay || reducedRef.current || count < 2) return;\n const root = rootRef.current;\n let hovered = false;\n let focused = false;\n const stop = () => {\n if (autoTimerRef.current) clearInterval(autoTimerRef.current);\n autoTimerRef.current = null;\n };\n const start = () => {\n stop();\n autoTimerRef.current = window.setInterval(\n () => {\n if (!hovered && !focused) navigateBy(1);\n },\n Math.max(cfgRef.current.autoplayDelay, 600)\n );\n };\n const onEnter = () => {\n hovered = true;\n };\n const onLeave = () => {\n hovered = false;\n };\n const onFocusIn = () => {\n focused = true;\n };\n const onFocusOut = () => {\n focused = false;\n };\n root?.addEventListener('mouseenter', onEnter);\n root?.addEventListener('mouseleave', onLeave);\n root?.addEventListener('focusin', onFocusIn);\n root?.addEventListener('focusout', onFocusOut);\n start();\n return () => {\n stop();\n root?.removeEventListener('mouseenter', onEnter);\n root?.removeEventListener('mouseleave', onLeave);\n root?.removeEventListener('focusin', onFocusIn);\n root?.removeEventListener('focusout', onFocusOut);\n };\n }, [autoplay, autoplayDelay, count, navigateBy]);\n\n useEffect(() => {\n layout(posRef.current);\n }, [layout, depth, spread, tilt, tiltDirection, visibleCards, falloff, blur, cardWidth, cardHeight, radius, count]);\n\n useEffect(\n () => () => {\n tweenRef.current?.kill();\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n if (autoTimerRef.current) clearInterval(autoTimerRef.current);\n },\n []\n );\n\n return (\n \n
\n {data.map((item, i) => (\n (cardRefs.current[i] = el)}\n style={{ width: cardWidth, height: cardHeight, borderRadius: radius }}\n aria-roledescription=\"slide\"\n aria-label={`${i + 1} of ${count}`}\n aria-hidden={active !== i}\n onClick={() => onCardClick(i)}\n >\n {item.alt\n (overlayRefs.current[i] = el)}\n style={{ background: tint }}\n />\n
\n ))}\n
\n\n {showControls && count > 1 && (\n <>\n navigateBy(-1)}\n >\n \n \n \n \n navigateBy(1)}\n >\n \n \n \n \n \n )}\n\n {showIndicators && count > 1 && (\n
\n {data.map((_, i) => (\n setFocus(i, true)}\n />\n ))}\n
\n )}\n
\n );\n};\n\nexport default DepthCarousel;\n" } ], diff --git a/public/r/DepthCarousel-TS-CSS.json b/public/r/DepthCarousel-TS-CSS.json index d81b0a3d..76ccdea3 100644 --- a/public/r/DepthCarousel-TS-CSS.json +++ b/public/r/DepthCarousel-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "DepthCarousel/DepthCarousel.css", + "type": "registry:file", + "path": "DepthCarousel.css", + "target": "@components/DepthCarousel.css", "content": ".depth-carousel {\n position: relative;\n width: 100%;\n height: 100%;\n min-height: 320px;\n display: flex;\n align-items: center;\n justify-content: center;\n perspective: var(--dc-perspective, 1400px);\n perspective-origin: 50% 50%;\n touch-action: pan-y;\n outline: none;\n user-select: none;\n -webkit-user-select: none;\n cursor: grab;\n}\n\n.depth-carousel:active {\n cursor: grabbing;\n}\n\n.depth-carousel:focus-visible {\n outline: 2px solid rgba(255, 255, 255, 0.5);\n outline-offset: 4px;\n border-radius: 12px;\n}\n\n.depth-carousel__stage {\n position: absolute;\n inset: 0;\n transform-style: preserve-3d;\n}\n\n.depth-carousel__card {\n position: absolute;\n top: 50%;\n left: 50%;\n transform-origin: center center;\n overflow: hidden;\n background: #0b0d12;\n box-shadow:\n 0 30px 60px -20px rgba(0, 0, 0, 0.65),\n 0 8px 20px -10px rgba(0, 0, 0, 0.5);\n will-change: transform, opacity, filter;\n cursor: pointer;\n transform: translate(-50%, -50%);\n}\n\n.depth-carousel__img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n pointer-events: none;\n -webkit-user-drag: none;\n}\n\n.depth-carousel__tint {\n position: absolute;\n inset: 0;\n opacity: 0;\n pointer-events: none;\n mix-blend-mode: multiply;\n}\n\n.depth-carousel__arrow {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n z-index: 3000;\n width: 42px;\n height: 42px;\n display: grid;\n place-items: center;\n border: 1px solid rgba(255, 255, 255, 0.18);\n border-radius: 999px;\n background: rgba(18, 20, 26, 0.55);\n backdrop-filter: blur(8px);\n color: #fff;\n cursor: pointer;\n transition:\n background 0.2s ease,\n border-color 0.2s ease,\n transform 0.2s ease;\n}\n\n.depth-carousel__arrow:hover {\n background: rgba(28, 31, 40, 0.85);\n border-color: rgba(255, 255, 255, 0.4);\n}\n\n.depth-carousel__arrow:active {\n transform: translateY(-50%) scale(0.94);\n}\n\n.depth-carousel__arrow--prev {\n left: 16px;\n}\n\n.depth-carousel__arrow--next {\n right: 16px;\n}\n\n.depth-carousel__dots {\n position: absolute;\n bottom: 16px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 3000;\n display: flex;\n gap: 8px;\n padding: 8px 12px;\n border-radius: 999px;\n background: rgba(14, 16, 22, 0.4);\n backdrop-filter: blur(6px);\n}\n\n.depth-carousel__dot {\n width: 7px;\n height: 7px;\n padding: 0;\n border: none;\n border-radius: 999px;\n background: rgba(255, 255, 255, 0.32);\n cursor: pointer;\n transition:\n width 0.25s ease,\n background 0.25s ease;\n}\n\n.depth-carousel__dot.is-active {\n width: 20px;\n background: #fff;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .depth-carousel__card {\n will-change: auto;\n }\n .depth-carousel__arrow,\n .depth-carousel__dot {\n transition: none;\n }\n}\n" }, { "type": "registry:component", - "path": "DepthCarousel/DepthCarousel.tsx", + "path": "DepthCarousel.tsx", "content": "import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n CSSProperties,\n PointerEvent as ReactPointerEvent,\n KeyboardEvent as ReactKeyboardEvent\n} from 'react';\nimport gsap from 'gsap';\nimport './DepthCarousel.css';\n\nexport type DepthCarouselItem = string | { image: string; alt?: string };\ntype TiltDirection = 'left' | 'right';\n\nexport interface DepthCarouselProps {\n items?: DepthCarouselItem[];\n cardWidth?: number;\n cardHeight?: number;\n radius?: number;\n tint?: string;\n depth?: number;\n spread?: number;\n tilt?: number;\n tiltDirection?: TiltDirection;\n perspective?: number;\n visibleCards?: number;\n falloff?: number;\n blur?: number;\n duration?: number;\n ease?: string;\n autoplay?: boolean;\n autoplayDelay?: number;\n loop?: boolean;\n showControls?: boolean;\n showIndicators?: boolean;\n onChange?: (index: number, item: { image: string; alt?: string }) => void;\n className?: string;\n}\n\ninterface CarouselConfig {\n count: number;\n depth: number;\n spread: number;\n tilt: number;\n tiltDirection: TiltDirection;\n visibleCards: number;\n falloff: number;\n blur: number;\n duration: number;\n ease: string;\n loop: boolean;\n cardWidth: number;\n autoplayDelay: number;\n}\n\ninterface DragState {\n x: number;\n startPos: number;\n lastX: number;\n lastT: number;\n v: number;\n moved: boolean;\n id: number;\n}\n\nconst DEFAULT_ITEMS: DepthCarouselItem[] = [\n { image: 'https://picsum.photos/seed/depth1/800/1000', alt: 'Slide 1' },\n { image: 'https://picsum.photos/seed/depth2/800/1000', alt: 'Slide 2' },\n { image: 'https://picsum.photos/seed/depth3/800/1000', alt: 'Slide 3' },\n { image: 'https://picsum.photos/seed/depth4/800/1000', alt: 'Slide 4' },\n { image: 'https://picsum.photos/seed/depth5/800/1000', alt: 'Slide 5' },\n { image: 'https://picsum.photos/seed/depth6/800/1000', alt: 'Slide 6' }\n];\n\nconst clamp = (v: number, min: number, max: number) => Math.min(Math.max(v, min), max);\nconst normalizeItem = (it: DepthCarouselItem) => (typeof it === 'string' ? { image: it, alt: '' } : it);\n\nconst DepthCarousel = ({\n items = DEFAULT_ITEMS,\n cardWidth = 300,\n cardHeight = 380,\n radius = 18,\n tint = '#05060a',\n depth = 220,\n spread = 90,\n tilt = 22,\n tiltDirection = 'right',\n perspective = 1400,\n visibleCards = 4,\n falloff = 0.2,\n blur = 6,\n duration = 700,\n ease = 'power3.out',\n autoplay = false,\n autoplayDelay = 3200,\n loop = true,\n showControls = true,\n showIndicators = true,\n onChange,\n className = ''\n}: DepthCarouselProps) => {\n const data = useMemo(() => (Array.isArray(items) ? items : []).map(normalizeItem), [items]);\n const count = data.length;\n\n const rootRef = useRef(null);\n const stageRef = useRef(null);\n const cardRefs = useRef<(HTMLDivElement | null)[]>([]);\n const overlayRefs = useRef<(HTMLSpanElement | null)[]>([]);\n\n const posRef = useRef(0);\n const focusRef = useRef(0);\n const tweenRef = useRef(null);\n const scaleRef = useRef(1);\n const cfgRef = useRef({} as CarouselConfig);\n const onChangeRef = useRef(onChange);\n\n const dragRef = useRef(null);\n const wheelTimerRef = useRef | null>(null);\n const autoTimerRef = useRef | null>(null);\n const reducedRef = useRef(false);\n\n const [active, setActive] = useState(0);\n\n onChangeRef.current = onChange;\n cfgRef.current = {\n count,\n depth,\n spread,\n tilt,\n tiltDirection,\n visibleCards,\n falloff,\n blur,\n duration,\n ease,\n loop,\n cardWidth,\n autoplayDelay\n };\n\n const layout = useCallback((pos: number) => {\n const cfg = cfgRef.current;\n const n = cfg.count;\n if (!n) return;\n const dir = cfg.tiltDirection === 'left' ? -1 : 1;\n const sc = scaleRef.current;\n\n for (let i = 0; i < n; i++) {\n const el = cardRefs.current[i];\n if (!el) continue;\n\n let d = i - pos;\n if (cfg.loop && n > 1) {\n d = ((d % n) + n) % n;\n if (d > n / 2) d -= n;\n }\n\n const back = Math.max(0, d);\n const az = Math.abs(d);\n const shown = az <= cfg.visibleCards + 0.5;\n\n const tz = -cfg.depth * d;\n const tx = dir * cfg.spread * d;\n const ry = dir * cfg.tilt * clamp(d, 0, 1);\n\n let opacity = d < 0 ? Math.max(0, 1 + d) : 1;\n if (!shown) opacity = 0;\n\n const brightness = Math.max(0.15, 1 - back * cfg.falloff);\n const blurPx = cfg.blur > 0 ? Math.min(cfg.blur, (back / Math.max(1, cfg.visibleCards)) * cfg.blur) : 0;\n const zi = Math.round(2000 - d * 20);\n\n el.style.transform = `translate(-50%, -50%) scale(${sc}) translateX(${tx.toFixed(2)}px) translateZ(${tz.toFixed(2)}px) rotateY(${ry.toFixed(3)}deg)`;\n el.style.opacity = opacity.toFixed(3);\n el.style.filter = `brightness(${brightness.toFixed(3)}) blur(${blurPx.toFixed(2)}px)`;\n el.style.zIndex = String(zi);\n el.style.pointerEvents = shown && opacity > 0.05 ? 'auto' : 'none';\n\n const ov = overlayRefs.current[i];\n if (ov) ov.style.opacity = clamp(back * cfg.falloff * 1.25, 0, 0.86).toFixed(3);\n }\n }, []);\n\n const notify = useCallback(\n (idx: number) => {\n setActive(idx);\n onChangeRef.current?.(idx, data[idx]);\n },\n [data]\n );\n\n const tweenTo = useCallback(\n (target: number, animate: boolean) => {\n tweenRef.current?.kill();\n const cfg = cfgRef.current;\n const proxy = { p: posRef.current };\n const dur = animate && !reducedRef.current ? cfg.duration / 1000 : 0;\n tweenRef.current = gsap.to(proxy, {\n p: target,\n duration: dur,\n ease: cfg.ease,\n onUpdate: () => {\n posRef.current = proxy.p;\n layout(proxy.p);\n },\n onComplete: () => {\n const n = cfg.count;\n if (n > 0) posRef.current = ((posRef.current % n) + n) % n;\n layout(posRef.current);\n }\n });\n },\n [layout]\n );\n\n const setFocus = useCallback(\n (rawIndex: number, animate = true) => {\n const cfg = cfgRef.current;\n const n = cfg.count;\n if (!n) return;\n const idx = cfg.loop ? ((rawIndex % n) + n) % n : clamp(rawIndex, 0, n - 1);\n let delta = idx - posRef.current;\n if (cfg.loop && n > 1) {\n delta = ((delta % n) + n) % n;\n if (delta > n / 2) delta -= n;\n }\n tweenTo(posRef.current + delta, animate);\n if (idx !== focusRef.current) {\n focusRef.current = idx;\n notify(idx);\n }\n },\n [tweenTo, notify]\n );\n\n const navigateBy = useCallback((step: number) => setFocus(focusRef.current + step, true), [setFocus]);\n\n useEffect(() => {\n const root = rootRef.current;\n if (!root) return;\n const ro = new ResizeObserver(entries => {\n const w = entries[0].contentRect.width;\n const cfg = cfgRef.current;\n const needed = cfg.cardWidth + Math.abs(cfg.spread) * 2 + 120;\n scaleRef.current = clamp(w / needed, 0.4, 1);\n layout(posRef.current);\n });\n ro.observe(root);\n return () => ro.disconnect();\n }, [layout]);\n\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const onWheel = (e: WheelEvent) => {\n const cfg = cfgRef.current;\n if (cfg.count < 2) return;\n e.preventDefault();\n tweenRef.current?.kill();\n const raw = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;\n const delta = e.deltaMode === 1 ? raw * 24 : raw;\n const step = clamp(delta / (cfg.cardWidth * 0.9), -0.6, 0.6);\n posRef.current += step;\n layout(posRef.current);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n wheelTimerRef.current = setTimeout(() => setFocus(Math.round(posRef.current), true), 130);\n };\n el.addEventListener('wheel', onWheel, { passive: false });\n return () => {\n el.removeEventListener('wheel', onWheel);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n };\n }, [layout, setFocus]);\n\n const onPointerDown = useCallback((e: ReactPointerEvent) => {\n const cfg = cfgRef.current;\n if (cfg.count < 2) return;\n tweenRef.current?.kill();\n dragRef.current = {\n x: e.clientX,\n startPos: posRef.current,\n lastX: e.clientX,\n lastT: performance.now(),\n v: 0,\n moved: false,\n id: e.pointerId\n };\n }, []);\n\n const onPointerMove = useCallback(\n (e: ReactPointerEvent) => {\n const drag = dragRef.current;\n if (!drag) return;\n const cfg = cfgRef.current;\n const stepPx = Math.max(cfg.cardWidth * 0.55 * scaleRef.current, 40);\n const dx = e.clientX - drag.x;\n if (!drag.moved && Math.abs(dx) > 4) {\n drag.moved = true;\n rootRef.current?.setPointerCapture(drag.id);\n }\n if (!drag.moved) return;\n const now = performance.now();\n const dt = Math.max(now - drag.lastT, 1);\n drag.v = (e.clientX - drag.lastX) / dt;\n drag.lastX = e.clientX;\n drag.lastT = now;\n posRef.current = drag.startPos - dx / stepPx;\n layout(posRef.current);\n },\n [layout]\n );\n\n const onPointerEnd = useCallback(() => {\n const drag = dragRef.current;\n if (!drag) return;\n dragRef.current = null;\n if (!drag.moved) return;\n const cfg = cfgRef.current;\n const stepPx = Math.max(cfg.cardWidth * 0.55 * scaleRef.current, 40);\n const projected = posRef.current - (drag.v * 180) / stepPx;\n setFocus(Math.round(projected), true);\n }, [setFocus]);\n\n const onKeyDown = useCallback(\n (e: ReactKeyboardEvent) => {\n if (e.key === 'ArrowLeft') {\n e.preventDefault();\n navigateBy(-1);\n } else if (e.key === 'ArrowRight') {\n e.preventDefault();\n navigateBy(1);\n }\n },\n [navigateBy]\n );\n\n const onCardClick = useCallback(\n (index: number) => {\n if (dragRef.current?.moved) return;\n setFocus(index, true);\n },\n [setFocus]\n );\n\n useEffect(() => {\n reducedRef.current = typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n if (!autoplay || reducedRef.current || count < 2) return;\n const root = rootRef.current;\n let hovered = false;\n let focused = false;\n const stop = () => {\n if (autoTimerRef.current) clearInterval(autoTimerRef.current);\n autoTimerRef.current = null;\n };\n const start = () => {\n stop();\n autoTimerRef.current = setInterval(\n () => {\n if (!hovered && !focused) navigateBy(1);\n },\n Math.max(cfgRef.current.autoplayDelay, 600)\n );\n };\n const onEnter = () => {\n hovered = true;\n };\n const onLeave = () => {\n hovered = false;\n };\n const onFocusIn = () => {\n focused = true;\n };\n const onFocusOut = () => {\n focused = false;\n };\n root?.addEventListener('mouseenter', onEnter);\n root?.addEventListener('mouseleave', onLeave);\n root?.addEventListener('focusin', onFocusIn);\n root?.addEventListener('focusout', onFocusOut);\n start();\n return () => {\n stop();\n root?.removeEventListener('mouseenter', onEnter);\n root?.removeEventListener('mouseleave', onLeave);\n root?.removeEventListener('focusin', onFocusIn);\n root?.removeEventListener('focusout', onFocusOut);\n };\n }, [autoplay, autoplayDelay, count, navigateBy]);\n\n useEffect(() => {\n layout(posRef.current);\n }, [layout, depth, spread, tilt, tiltDirection, visibleCards, falloff, blur, cardWidth, cardHeight, radius, count]);\n\n useEffect(\n () => () => {\n tweenRef.current?.kill();\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n if (autoTimerRef.current) clearInterval(autoTimerRef.current);\n },\n []\n );\n\n return (\n \n
\n {data.map((item, i) => (\n {\n cardRefs.current[i] = el;\n }}\n style={{ width: cardWidth, height: cardHeight, borderRadius: radius }}\n aria-roledescription=\"slide\"\n aria-label={`${i + 1} of ${count}`}\n aria-hidden={active !== i}\n onClick={() => onCardClick(i)}\n >\n {item.alt\n {\n overlayRefs.current[i] = el;\n }}\n style={{ background: tint }}\n />\n
\n ))}\n
\n\n {showControls && count > 1 && (\n <>\n navigateBy(-1)}\n >\n \n \n \n \n navigateBy(1)}\n >\n \n \n \n \n \n )}\n\n {showIndicators && count > 1 && (\n
\n {data.map((_, i) => (\n setFocus(i, true)}\n />\n ))}\n
\n )}\n
\n );\n};\n\nexport default DepthCarousel;\n" } ], diff --git a/public/r/DepthText-JS-CSS.json b/public/r/DepthText-JS-CSS.json index 5ebab51d..87b824f6 100644 --- a/public/r/DepthText-JS-CSS.json +++ b/public/r/DepthText-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "DepthText/DepthText.css", + "type": "registry:file", + "path": "DepthText.css", + "target": "@components/DepthText.css", "content": ".depth-text {\n display: inline-block;\n perspective: var(--depth-text-perspective);\n perspective-origin: 50% 48%;\n isolation: isolate;\n}\n\n.depth-text__stage {\n position: relative;\n display: inline-grid;\n place-items: center;\n transform-style: preserve-3d;\n transform: rotateX(-2.4deg) rotateY(3.15deg);\n transform-origin: 50% 50%;\n will-change: transform;\n}\n\n.depth-text__layer,\n.depth-text__face {\n grid-area: 1 / 1;\n display: inline-block;\n font-size: var(--depth-text-font-size);\n font-weight: var(--depth-text-font-weight);\n line-height: 0.86;\n letter-spacing: -0.065em;\n white-space: nowrap;\n user-select: none;\n transform-style: preserve-3d;\n backface-visibility: hidden;\n font-kerning: normal;\n text-rendering: geometricPrecision;\n}\n\n.depth-text__layer {\n position: absolute;\n inset: 0;\n z-index: 0;\n filter: saturate(0.95) brightness(0.92);\n pointer-events: none;\n}\n\n.depth-text__face {\n position: relative;\n z-index: 1;\n color: var(--depth-text-face-color);\n text-shadow: var(--depth-text-shadow);\n transform: translateZ(0.6px);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .depth-text {\n cursor: default;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .depth-text__stage {\n will-change: auto;\n }\n}\n" }, { "type": "registry:component", - "path": "DepthText/DepthText.jsx", + "path": "DepthText.jsx", "content": "import { useEffect, useMemo, useRef } from 'react';\nimport './DepthText.css';\n\nconst MAX_LAYERS = 64;\n\nconst clamp = (value, min, max) => Math.min(Math.max(value, min), max);\n\nconst getLayerColor = (faceColor, depthColor, index, total) => {\n const progress = total <= 1 ? 1 : index / total;\n const eased = progress * progress;\n const faceMix = Math.round((1 - eased) * 72 + 4);\n return `color-mix(in srgb, ${faceColor} ${faceMix}%, ${depthColor})`;\n};\n\nconst getTransform = (rotateX, rotateY) => `rotateX(${rotateX.toFixed(3)}deg) rotateY(${rotateY.toFixed(3)}deg)`;\n\nconst DepthText = ({\n text = 'Elevate',\n layers = 34,\n depth = 2.4,\n faceColor = '#f8fafc',\n depthColor = '#7c3aed',\n tilt = 7.5,\n pointerTracking = true,\n smoothing = 0.14,\n perspective = 900,\n autoOrbit = true,\n orbitSpeed = 0.35,\n fontSize = 'clamp(3rem, 12vw, 7rem)',\n fontWeight = 900,\n shadow = true,\n className = '',\n style = {}\n}) => {\n const rootRef = useRef(null);\n const stageRef = useRef(null);\n\n const safeLayers = clamp(Math.round(Number(layers) || 1), 2, MAX_LAYERS);\n const safeDepth = clamp(Number(depth) || 0, 0, 12);\n const safeTilt = clamp(Number(tilt) || 0, 0, 12);\n const safeSmoothing = clamp(Number(smoothing) || 0.14, 0.02, 0.35);\n const safePerspective = clamp(Number(perspective) || 900, 300, 2000);\n const safeOrbitSpeed = clamp(Number(orbitSpeed) || 0, 0, 2);\n\n const baseRotation = useMemo(() => ({ x: -safeTilt * 0.32, y: safeTilt * 0.42 }), [safeTilt]);\n\n const depthLayers = useMemo(\n () =>\n Array.from({ length: safeLayers }, (_, layerIndex) => {\n const index = safeLayers - layerIndex;\n return {\n index,\n color: getLayerColor(faceColor, depthColor, index, safeLayers),\n transform: `translateZ(${-index * safeDepth}px)`\n };\n }),\n [safeLayers, safeDepth, faceColor, depthColor]\n );\n\n useEffect(() => {\n const root = rootRef.current;\n const stage = stageRef.current;\n if (!root || !stage || typeof window === 'undefined') return undefined;\n\n const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n const finePointer = window.matchMedia('(hover: hover) and (pointer: fine)').matches;\n const canTrackPointer = pointerTracking && finePointer && !reducedMotion;\n\n let frameId = 0;\n let activePointer = false;\n let startTime = performance.now();\n const current = { ...baseRotation };\n const target = { ...baseRotation };\n\n const applyTransform = () => {\n stage.style.transform = getTransform(current.x, current.y);\n };\n\n if (reducedMotion) {\n stage.style.transform = getTransform(baseRotation.x, baseRotation.y);\n return undefined;\n }\n\n const handlePointerMove = event => {\n const rect = root.getBoundingClientRect();\n if (!rect.width || !rect.height) return;\n\n activePointer = true;\n const x = clamp((event.clientX - (rect.left + rect.width / 2)) / (rect.width * 0.8), -1, 1);\n const y = clamp((event.clientY - (rect.top + rect.height / 2)) / (rect.height * 0.8), -1, 1);\n\n target.x = baseRotation.x - y * safeTilt;\n target.y = baseRotation.y + x * safeTilt;\n };\n\n const handlePointerLeave = () => {\n activePointer = false;\n target.x = baseRotation.x;\n target.y = baseRotation.y;\n };\n\n if (canTrackPointer) {\n window.addEventListener('pointermove', handlePointerMove);\n window.addEventListener('pointerleave', handlePointerLeave);\n window.addEventListener('blur', handlePointerLeave);\n }\n\n const tick = now => {\n if ((!canTrackPointer || !activePointer) && autoOrbit) {\n const elapsed = (now - startTime) / 1000;\n const orbit = elapsed * safeOrbitSpeed * Math.PI * 2;\n const fallbackAmount = canTrackPointer ? 0.18 : 0.55;\n target.x = baseRotation.x + Math.sin(orbit) * safeTilt * fallbackAmount;\n target.y = baseRotation.y + Math.cos(orbit * 0.85) * safeTilt * fallbackAmount;\n }\n\n current.x += (target.x - current.x) * safeSmoothing;\n current.y += (target.y - current.y) * safeSmoothing;\n applyTransform();\n frameId = requestAnimationFrame(tick);\n };\n\n applyTransform();\n frameId = requestAnimationFrame(tick);\n\n return () => {\n if (canTrackPointer) {\n window.removeEventListener('pointermove', handlePointerMove);\n window.removeEventListener('pointerleave', handlePointerLeave);\n window.removeEventListener('blur', handlePointerLeave);\n }\n cancelAnimationFrame(frameId);\n startTime = 0;\n };\n }, [autoOrbit, baseRotation, pointerTracking, safeOrbitSpeed, safeSmoothing, safeTilt]);\n\n const rootStyle = {\n ...style,\n '--depth-text-perspective': `${safePerspective}px`,\n '--depth-text-font-size': fontSize,\n '--depth-text-font-weight': fontWeight,\n '--depth-text-face-color': faceColor,\n '--depth-text-depth-color': depthColor,\n '--depth-text-shadow': shadow\n ? `0 22px 34px color-mix(in srgb, ${depthColor} 36%, transparent), 0 4px 8px rgba(0, 0, 0, 0.28)`\n : 'none'\n };\n\n return (\n \n \n {depthLayers.map(layer => (\n \n {text}\n \n ))}\n {text}\n \n \n );\n};\n\nexport default DepthText;\n" } ], diff --git a/public/r/DepthText-TS-CSS.json b/public/r/DepthText-TS-CSS.json index 55baf906..ee2e83ed 100644 --- a/public/r/DepthText-TS-CSS.json +++ b/public/r/DepthText-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "DepthText/DepthText.css", + "type": "registry:file", + "path": "DepthText.css", + "target": "@components/DepthText.css", "content": ".depth-text {\n display: inline-block;\n perspective: var(--depth-text-perspective);\n perspective-origin: 50% 48%;\n isolation: isolate;\n}\n\n.depth-text__stage {\n position: relative;\n display: inline-grid;\n place-items: center;\n transform-style: preserve-3d;\n transform: rotateX(-2.4deg) rotateY(3.15deg);\n transform-origin: 50% 50%;\n will-change: transform;\n}\n\n.depth-text__layer,\n.depth-text__face {\n grid-area: 1 / 1;\n display: inline-block;\n font-size: var(--depth-text-font-size);\n font-weight: var(--depth-text-font-weight);\n line-height: 0.86;\n letter-spacing: -0.065em;\n white-space: nowrap;\n user-select: none;\n transform-style: preserve-3d;\n backface-visibility: hidden;\n font-kerning: normal;\n text-rendering: geometricPrecision;\n}\n\n.depth-text__layer {\n position: absolute;\n inset: 0;\n z-index: 0;\n filter: saturate(0.95) brightness(0.92);\n pointer-events: none;\n}\n\n.depth-text__face {\n position: relative;\n z-index: 1;\n color: var(--depth-text-face-color);\n text-shadow: var(--depth-text-shadow);\n transform: translateZ(0.6px);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .depth-text {\n cursor: default;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .depth-text__stage {\n will-change: auto;\n }\n}\n" }, { "type": "registry:component", - "path": "DepthText/DepthText.tsx", + "path": "DepthText.tsx", "content": "import { useEffect, useMemo, useRef, type CSSProperties } from 'react';\nimport './DepthText.css';\n\nexport interface DepthTextProps {\n text?: string;\n layers?: number;\n depth?: number;\n faceColor?: string;\n depthColor?: string;\n tilt?: number;\n pointerTracking?: boolean;\n smoothing?: number;\n perspective?: number;\n autoOrbit?: boolean;\n orbitSpeed?: number;\n fontSize?: string;\n fontWeight?: number | string;\n shadow?: boolean;\n className?: string;\n style?: CSSProperties;\n}\n\ninterface DepthLayer {\n index: number;\n color: string;\n transform: string;\n}\n\nconst MAX_LAYERS = 64;\n\nconst clamp = (value: number, min: number, max: number): number => Math.min(Math.max(value, min), max);\n\nconst getLayerColor = (faceColor: string, depthColor: string, index: number, total: number): string => {\n const progress = total <= 1 ? 1 : index / total;\n const eased = progress * progress;\n const faceMix = Math.round((1 - eased) * 72 + 4);\n return `color-mix(in srgb, ${faceColor} ${faceMix}%, ${depthColor})`;\n};\n\nconst getTransform = (rotateX: number, rotateY: number): string =>\n `rotateX(${rotateX.toFixed(3)}deg) rotateY(${rotateY.toFixed(3)}deg)`;\n\nconst DepthText = ({\n text = 'Elevate',\n layers = 34,\n depth = 2.4,\n faceColor = '#f8fafc',\n depthColor = '#7c3aed',\n tilt = 7.5,\n pointerTracking = true,\n smoothing = 0.14,\n perspective = 900,\n autoOrbit = true,\n orbitSpeed = 0.35,\n fontSize = 'clamp(3rem, 12vw, 7rem)',\n fontWeight = 900,\n shadow = true,\n className = '',\n style = {}\n}: DepthTextProps) => {\n const rootRef = useRef(null);\n const stageRef = useRef(null);\n\n const safeLayers = clamp(Math.round(Number(layers) || 1), 2, MAX_LAYERS);\n const safeDepth = clamp(Number(depth) || 0, 0, 12);\n const safeTilt = clamp(Number(tilt) || 0, 0, 12);\n const safeSmoothing = clamp(Number(smoothing) || 0.14, 0.02, 0.35);\n const safePerspective = clamp(Number(perspective) || 900, 300, 2000);\n const safeOrbitSpeed = clamp(Number(orbitSpeed) || 0, 0, 2);\n\n const baseRotation = useMemo(() => ({ x: -safeTilt * 0.32, y: safeTilt * 0.42 }), [safeTilt]);\n\n const depthLayers = useMemo(\n () =>\n Array.from({ length: safeLayers }, (_, layerIndex) => {\n const index = safeLayers - layerIndex;\n return {\n index,\n color: getLayerColor(faceColor, depthColor, index, safeLayers),\n transform: `translateZ(${-index * safeDepth}px)`\n };\n }),\n [safeLayers, safeDepth, faceColor, depthColor]\n );\n\n useEffect(() => {\n const root = rootRef.current;\n const stage = stageRef.current;\n if (!root || !stage || typeof window === 'undefined') return undefined;\n\n const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n const finePointer = window.matchMedia('(hover: hover) and (pointer: fine)').matches;\n const canTrackPointer = pointerTracking && finePointer && !reducedMotion;\n\n let frameId = 0;\n let activePointer = false;\n let startTime = performance.now();\n const current = { ...baseRotation };\n const target = { ...baseRotation };\n\n const applyTransform = () => {\n stage.style.transform = getTransform(current.x, current.y);\n };\n\n if (reducedMotion) {\n stage.style.transform = getTransform(baseRotation.x, baseRotation.y);\n return undefined;\n }\n\n const handlePointerMove = (event: PointerEvent) => {\n const rect = root.getBoundingClientRect();\n if (!rect.width || !rect.height) return;\n\n activePointer = true;\n const x = clamp((event.clientX - (rect.left + rect.width / 2)) / (rect.width * 0.8), -1, 1);\n const y = clamp((event.clientY - (rect.top + rect.height / 2)) / (rect.height * 0.8), -1, 1);\n\n target.x = baseRotation.x - y * safeTilt;\n target.y = baseRotation.y + x * safeTilt;\n };\n\n const handlePointerLeave = () => {\n activePointer = false;\n target.x = baseRotation.x;\n target.y = baseRotation.y;\n };\n\n if (canTrackPointer) {\n window.addEventListener('pointermove', handlePointerMove);\n window.addEventListener('pointerleave', handlePointerLeave);\n window.addEventListener('blur', handlePointerLeave);\n }\n\n const tick = (now: number) => {\n if ((!canTrackPointer || !activePointer) && autoOrbit) {\n const elapsed = (now - startTime) / 1000;\n const orbit = elapsed * safeOrbitSpeed * Math.PI * 2;\n const fallbackAmount = canTrackPointer ? 0.18 : 0.55;\n target.x = baseRotation.x + Math.sin(orbit) * safeTilt * fallbackAmount;\n target.y = baseRotation.y + Math.cos(orbit * 0.85) * safeTilt * fallbackAmount;\n }\n\n current.x += (target.x - current.x) * safeSmoothing;\n current.y += (target.y - current.y) * safeSmoothing;\n applyTransform();\n frameId = requestAnimationFrame(tick);\n };\n\n applyTransform();\n frameId = requestAnimationFrame(tick);\n\n return () => {\n if (canTrackPointer) {\n window.removeEventListener('pointermove', handlePointerMove);\n window.removeEventListener('pointerleave', handlePointerLeave);\n window.removeEventListener('blur', handlePointerLeave);\n }\n cancelAnimationFrame(frameId);\n startTime = 0;\n };\n }, [autoOrbit, baseRotation, pointerTracking, safeOrbitSpeed, safeSmoothing, safeTilt]);\n\n const rootStyle = {\n ...style,\n '--depth-text-perspective': `${safePerspective}px`,\n '--depth-text-font-size': fontSize,\n '--depth-text-font-weight': fontWeight,\n '--depth-text-face-color': faceColor,\n '--depth-text-depth-color': depthColor,\n '--depth-text-shadow': shadow\n ? `0 22px 34px color-mix(in srgb, ${depthColor} 36%, transparent), 0 4px 8px rgba(0, 0, 0, 0.28)`\n : 'none'\n } as CSSProperties;\n\n return (\n \n \n {depthLayers.map(layer => (\n \n {text}\n \n ))}\n {text}\n \n \n );\n};\n\nexport default DepthText;\n" } ], diff --git a/public/r/Dither-JS-CSS.json b/public/r/Dither-JS-CSS.json index d463bfbd..0b30c058 100644 --- a/public/r/Dither-JS-CSS.json +++ b/public/r/Dither-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Dither/Dither.css", + "type": "registry:file", + "path": "Dither.css", + "target": "@components/Dither.css", "content": ".dither-container {\n width: 100%;\n height: 100%;\n position: relative;\n}\n" }, { "type": "registry:component", - "path": "Dither/Dither.jsx", + "path": "Dither.jsx", "content": "/* eslint-disable react/no-unknown-property */\nimport { useRef, useEffect, forwardRef } from 'react';\nimport { Canvas, useFrame, useThree } from '@react-three/fiber';\nimport { EffectComposer, wrapEffect } from '@react-three/postprocessing';\nimport { Effect } from 'postprocessing';\nimport * as THREE from 'three';\n\nimport './Dither.css';\n\nconst waveVertexShader = `\nprecision highp float;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n vec4 modelPosition = modelMatrix * vec4(position, 1.0);\n vec4 viewPosition = viewMatrix * modelPosition;\n gl_Position = projectionMatrix * viewPosition;\n}\n`;\n\nconst waveFragmentShader = `\nprecision highp float;\nuniform vec2 resolution;\nuniform float time;\nuniform float waveSpeed;\nuniform float waveFrequency;\nuniform float waveAmplitude;\nuniform vec3 waveColor;\nuniform vec2 mousePos;\nuniform int enableMouseInteraction;\nuniform float mouseRadius;\n\nvec4 mod289(vec4 x) { return x - floor(x * (1.0/289.0)) * 289.0; }\nvec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }\nvec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\nvec2 fade(vec2 t) { return t*t*t*(t*(t*6.0-15.0)+10.0); }\n\nfloat cnoise(vec2 P) {\n vec4 Pi = floor(P.xyxy) + vec4(0.0,0.0,1.0,1.0);\n vec4 Pf = fract(P.xyxy) - vec4(0.0,0.0,1.0,1.0);\n Pi = mod289(Pi);\n vec4 ix = Pi.xzxz;\n vec4 iy = Pi.yyww;\n vec4 fx = Pf.xzxz;\n vec4 fy = Pf.yyww;\n vec4 i = permute(permute(ix) + iy);\n vec4 gx = fract(i * (1.0/41.0)) * 2.0 - 1.0;\n vec4 gy = abs(gx) - 0.5;\n vec4 tx = floor(gx + 0.5);\n gx = gx - tx;\n vec2 g00 = vec2(gx.x, gy.x);\n vec2 g10 = vec2(gx.y, gy.y);\n vec2 g01 = vec2(gx.z, gy.z);\n vec2 g11 = vec2(gx.w, gy.w);\n vec4 norm = taylorInvSqrt(vec4(dot(g00,g00), dot(g01,g01), dot(g10,g10), dot(g11,g11)));\n g00 *= norm.x; g01 *= norm.y; g10 *= norm.z; g11 *= norm.w;\n float n00 = dot(g00, vec2(fx.x, fy.x));\n float n10 = dot(g10, vec2(fx.y, fy.y));\n float n01 = dot(g01, vec2(fx.z, fy.z));\n float n11 = dot(g11, vec2(fx.w, fy.w));\n vec2 fade_xy = fade(Pf.xy);\n vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);\n return 2.3 * mix(n_x.x, n_x.y, fade_xy.y);\n}\n\nconst int OCTAVES = 4;\nfloat fbm(vec2 p) {\n float value = 0.0;\n float amp = 1.0;\n float freq = waveFrequency;\n for (int i = 0; i < OCTAVES; i++) {\n value += amp * abs(cnoise(p));\n p *= freq;\n amp *= waveAmplitude;\n }\n return value;\n}\n\nfloat pattern(vec2 p) {\n vec2 p2 = p - time * waveSpeed;\n return fbm(p + fbm(p2)); \n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / resolution.xy;\n uv -= 0.5;\n uv.x *= resolution.x / resolution.y;\n float f = pattern(uv);\n if (enableMouseInteraction == 1) {\n vec2 mouseNDC = (mousePos / resolution - 0.5) * vec2(1.0, -1.0);\n mouseNDC.x *= resolution.x / resolution.y;\n float dist = length(uv - mouseNDC);\n float effect = 1.0 - smoothstep(0.0, mouseRadius, dist);\n f -= 0.5 * effect;\n }\n vec3 col = mix(vec3(0.0), waveColor, f);\n gl_FragColor = vec4(col, 1.0);\n}\n`;\n\nconst ditherFragmentShader = `\nprecision highp float;\nuniform float colorNum;\nuniform float pixelSize;\nconst float bayerMatrix8x8[64] = float[64](\n 0.0/64.0, 48.0/64.0, 12.0/64.0, 60.0/64.0, 3.0/64.0, 51.0/64.0, 15.0/64.0, 63.0/64.0,\n 32.0/64.0,16.0/64.0, 44.0/64.0, 28.0/64.0, 35.0/64.0,19.0/64.0, 47.0/64.0, 31.0/64.0,\n 8.0/64.0, 56.0/64.0, 4.0/64.0, 52.0/64.0, 11.0/64.0,59.0/64.0, 7.0/64.0, 55.0/64.0,\n 40.0/64.0,24.0/64.0, 36.0/64.0, 20.0/64.0, 43.0/64.0,27.0/64.0, 39.0/64.0, 23.0/64.0,\n 2.0/64.0, 50.0/64.0, 14.0/64.0, 62.0/64.0, 1.0/64.0,49.0/64.0, 13.0/64.0, 61.0/64.0,\n 34.0/64.0,18.0/64.0, 46.0/64.0, 30.0/64.0, 33.0/64.0,17.0/64.0, 45.0/64.0, 29.0/64.0,\n 10.0/64.0,58.0/64.0, 6.0/64.0, 54.0/64.0, 9.0/64.0,57.0/64.0, 5.0/64.0, 53.0/64.0,\n 42.0/64.0,26.0/64.0, 38.0/64.0, 22.0/64.0, 41.0/64.0,25.0/64.0, 37.0/64.0, 21.0/64.0\n);\n\nvec3 dither(vec2 uv, vec3 color) {\n vec2 scaledCoord = floor(uv * resolution / pixelSize);\n int x = int(mod(scaledCoord.x, 8.0));\n int y = int(mod(scaledCoord.y, 8.0));\n float threshold = bayerMatrix8x8[y * 8 + x] - 0.25;\n float step = 1.0 / (colorNum - 1.0);\n color += threshold * step;\n float bias = 0.2;\n color = clamp(color - bias, 0.0, 1.0);\n return floor(color * (colorNum - 1.0) + 0.5) / (colorNum - 1.0);\n}\n\nvoid mainImage(in vec4 inputColor, in vec2 uv, out vec4 outputColor) {\n vec2 normalizedPixelSize = pixelSize / resolution;\n vec2 uvPixel = normalizedPixelSize * floor(uv / normalizedPixelSize);\n vec4 color = texture2D(inputBuffer, uvPixel);\n color.rgb = dither(uv, color.rgb);\n outputColor = color;\n}\n`;\n\nclass RetroEffectImpl extends Effect {\n constructor() {\n const uniforms = new Map([\n ['colorNum', new THREE.Uniform(4.0)],\n ['pixelSize', new THREE.Uniform(2.0)]\n ]);\n super('RetroEffect', ditherFragmentShader, { uniforms });\n this.uniforms = uniforms;\n }\n set colorNum(v) {\n this.uniforms.get('colorNum').value = v;\n }\n get colorNum() {\n return this.uniforms.get('colorNum').value;\n }\n set pixelSize(v) {\n this.uniforms.get('pixelSize').value = v;\n }\n get pixelSize() {\n return this.uniforms.get('pixelSize').value;\n }\n}\n\nconst WrappedRetro = wrapEffect(RetroEffectImpl);\n\nconst RetroEffect = forwardRef((props, ref) => {\n const { colorNum, pixelSize } = props;\n return ;\n});\nRetroEffect.displayName = 'RetroEffect';\n\nfunction DitheredWaves({\n waveSpeed,\n waveFrequency,\n waveAmplitude,\n waveColor,\n colorNum,\n pixelSize,\n disableAnimation,\n enableMouseInteraction,\n mouseRadius\n}) {\n const mesh = useRef(null);\n const mouseRef = useRef(new THREE.Vector2());\n const { viewport, size, gl } = useThree();\n\n const waveUniformsRef = useRef({\n time: new THREE.Uniform(0),\n resolution: new THREE.Uniform(new THREE.Vector2(0, 0)),\n waveSpeed: new THREE.Uniform(waveSpeed),\n waveFrequency: new THREE.Uniform(waveFrequency),\n waveAmplitude: new THREE.Uniform(waveAmplitude),\n waveColor: new THREE.Uniform(new THREE.Color(...waveColor)),\n mousePos: new THREE.Uniform(new THREE.Vector2(0, 0)),\n enableMouseInteraction: new THREE.Uniform(enableMouseInteraction ? 1 : 0),\n mouseRadius: new THREE.Uniform(mouseRadius)\n });\n\n useEffect(() => {\n const dpr = gl.getPixelRatio();\n const w = Math.floor(size.width * dpr),\n h = Math.floor(size.height * dpr);\n const res = waveUniformsRef.current.resolution.value;\n if (res.x !== w || res.y !== h) {\n res.set(w, h);\n }\n }, [size, gl]);\n\n const prevColor = useRef([...waveColor]);\n useFrame(({ clock }) => {\n const u = waveUniformsRef.current;\n\n if (!disableAnimation) {\n u.time.value = clock.getElapsedTime();\n }\n\n if (u.waveSpeed.value !== waveSpeed) u.waveSpeed.value = waveSpeed;\n if (u.waveFrequency.value !== waveFrequency) u.waveFrequency.value = waveFrequency;\n if (u.waveAmplitude.value !== waveAmplitude) u.waveAmplitude.value = waveAmplitude;\n\n if (!prevColor.current.every((v, i) => v === waveColor[i])) {\n u.waveColor.value.set(...waveColor);\n prevColor.current = [...waveColor];\n }\n\n u.enableMouseInteraction.value = enableMouseInteraction ? 1 : 0;\n u.mouseRadius.value = mouseRadius;\n\n if (enableMouseInteraction) {\n u.mousePos.value.copy(mouseRef.current);\n }\n });\n\n const handlePointerMove = e => {\n if (!enableMouseInteraction) return;\n const rect = gl.domElement.getBoundingClientRect();\n const dpr = gl.getPixelRatio();\n mouseRef.current.set((e.clientX - rect.left) * dpr, (e.clientY - rect.top) * dpr);\n };\n\n return (\n <>\n \n \n \n \n\n \n \n \n\n \n \n \n \n \n );\n}\n\nexport default function Dither({\n waveSpeed = 0.05,\n waveFrequency = 3,\n waveAmplitude = 0.3,\n waveColor = [0.5, 0.5, 0.5],\n colorNum = 4,\n pixelSize = 2,\n disableAnimation = false,\n enableMouseInteraction = true,\n mouseRadius = 1\n}) {\n return (\n \n \n \n );\n}\n" } ], diff --git a/public/r/Dither-TS-CSS.json b/public/r/Dither-TS-CSS.json index d3b9dc5b..3dd3409c 100644 --- a/public/r/Dither-TS-CSS.json +++ b/public/r/Dither-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Dither/Dither.css", + "type": "registry:file", + "path": "Dither.css", + "target": "@components/Dither.css", "content": ".dither-container {\n width: 100%;\n height: 100%;\n position: relative;\n}\n" }, { "type": "registry:component", - "path": "Dither/Dither.tsx", - "content": "/* eslint-disable react/no-unknown-property */\nimport { useRef, useEffect, forwardRef } from 'react';\nimport { Canvas, useFrame, useThree, ThreeEvent } from '@react-three/fiber';\nimport { EffectComposer, wrapEffect } from '@react-three/postprocessing';\nimport { Effect } from 'postprocessing';\nimport * as THREE from 'three';\n\nimport './Dither.css';\n\nconst waveVertexShader = `\nprecision highp float;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n vec4 modelPosition = modelMatrix * vec4(position, 1.0);\n vec4 viewPosition = viewMatrix * modelPosition;\n gl_Position = projectionMatrix * viewPosition;\n}\n`;\n\nconst waveFragmentShader = `\nprecision highp float;\nuniform vec2 resolution;\nuniform float time;\nuniform float waveSpeed;\nuniform float waveFrequency;\nuniform float waveAmplitude;\nuniform vec3 waveColor;\nuniform vec2 mousePos;\nuniform int enableMouseInteraction;\nuniform float mouseRadius;\n\nvec4 mod289(vec4 x) { return x - floor(x * (1.0/289.0)) * 289.0; }\nvec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }\nvec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\nvec2 fade(vec2 t) { return t*t*t*(t*(t*6.0-15.0)+10.0); }\n\nfloat cnoise(vec2 P) {\n vec4 Pi = floor(P.xyxy) + vec4(0.0,0.0,1.0,1.0);\n vec4 Pf = fract(P.xyxy) - vec4(0.0,0.0,1.0,1.0);\n Pi = mod289(Pi);\n vec4 ix = Pi.xzxz;\n vec4 iy = Pi.yyww;\n vec4 fx = Pf.xzxz;\n vec4 fy = Pf.yyww;\n vec4 i = permute(permute(ix) + iy);\n vec4 gx = fract(i * (1.0/41.0)) * 2.0 - 1.0;\n vec4 gy = abs(gx) - 0.5;\n vec4 tx = floor(gx + 0.5);\n gx = gx - tx;\n vec2 g00 = vec2(gx.x, gy.x);\n vec2 g10 = vec2(gx.y, gy.y);\n vec2 g01 = vec2(gx.z, gy.z);\n vec2 g11 = vec2(gx.w, gy.w);\n vec4 norm = taylorInvSqrt(vec4(dot(g00,g00), dot(g01,g01), dot(g10,g10), dot(g11,g11)));\n g00 *= norm.x; g01 *= norm.y; g10 *= norm.z; g11 *= norm.w;\n float n00 = dot(g00, vec2(fx.x, fy.x));\n float n10 = dot(g10, vec2(fx.y, fy.y));\n float n01 = dot(g01, vec2(fx.z, fy.z));\n float n11 = dot(g11, vec2(fx.w, fy.w));\n vec2 fade_xy = fade(Pf.xy);\n vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);\n return 2.3 * mix(n_x.x, n_x.y, fade_xy.y);\n}\n\nconst int OCTAVES = 4;\nfloat fbm(vec2 p) {\n float value = 0.0;\n float amp = 1.0;\n float freq = waveFrequency;\n for (int i = 0; i < OCTAVES; i++) {\n value += amp * abs(cnoise(p));\n p *= freq;\n amp *= waveAmplitude;\n }\n return value;\n}\n\nfloat pattern(vec2 p) {\n vec2 p2 = p - time * waveSpeed;\n return fbm(p + fbm(p2)); \n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / resolution.xy;\n uv -= 0.5;\n uv.x *= resolution.x / resolution.y;\n float f = pattern(uv);\n if (enableMouseInteraction == 1) {\n vec2 mouseNDC = (mousePos / resolution - 0.5) * vec2(1.0, -1.0);\n mouseNDC.x *= resolution.x / resolution.y;\n float dist = length(uv - mouseNDC);\n float effect = 1.0 - smoothstep(0.0, mouseRadius, dist);\n f -= 0.5 * effect;\n }\n vec3 col = mix(vec3(0.0), waveColor, f);\n gl_FragColor = vec4(col, 1.0);\n}\n`;\n\nconst ditherFragmentShader = `\nprecision highp float;\nuniform float colorNum;\nuniform float pixelSize;\nconst float bayerMatrix8x8[64] = float[64](\n 0.0/64.0, 48.0/64.0, 12.0/64.0, 60.0/64.0, 3.0/64.0, 51.0/64.0, 15.0/64.0, 63.0/64.0,\n 32.0/64.0,16.0/64.0, 44.0/64.0, 28.0/64.0, 35.0/64.0,19.0/64.0, 47.0/64.0, 31.0/64.0,\n 8.0/64.0, 56.0/64.0, 4.0/64.0, 52.0/64.0, 11.0/64.0,59.0/64.0, 7.0/64.0, 55.0/64.0,\n 40.0/64.0,24.0/64.0, 36.0/64.0, 20.0/64.0, 43.0/64.0,27.0/64.0, 39.0/64.0, 23.0/64.0,\n 2.0/64.0, 50.0/64.0, 14.0/64.0, 62.0/64.0, 1.0/64.0,49.0/64.0, 13.0/64.0, 61.0/64.0,\n 34.0/64.0,18.0/64.0, 46.0/64.0, 30.0/64.0, 33.0/64.0,17.0/64.0, 45.0/64.0, 29.0/64.0,\n 10.0/64.0,58.0/64.0, 6.0/64.0, 54.0/64.0, 9.0/64.0,57.0/64.0, 5.0/64.0, 53.0/64.0,\n 42.0/64.0,26.0/64.0, 38.0/64.0, 22.0/64.0, 41.0/64.0,25.0/64.0, 37.0/64.0, 21.0/64.0\n);\n\nvec3 dither(vec2 uv, vec3 color) {\n vec2 scaledCoord = floor(uv * resolution / pixelSize);\n int x = int(mod(scaledCoord.x, 8.0));\n int y = int(mod(scaledCoord.y, 8.0));\n float threshold = bayerMatrix8x8[y * 8 + x] - 0.25;\n float step = 1.0 / (colorNum - 1.0);\n color += threshold * step;\n float bias = 0.2;\n color = clamp(color - bias, 0.0, 1.0);\n return floor(color * (colorNum - 1.0) + 0.5) / (colorNum - 1.0);\n}\n\nvoid mainImage(in vec4 inputColor, in vec2 uv, out vec4 outputColor) {\n vec2 normalizedPixelSize = pixelSize / resolution;\n vec2 uvPixel = normalizedPixelSize * floor(uv / normalizedPixelSize);\n vec4 color = texture2D(inputBuffer, uvPixel);\n color.rgb = dither(uv, color.rgb);\n outputColor = color;\n}\n`;\n\nclass RetroEffectImpl extends Effect {\n public uniforms: Map>;\n constructor() {\n const uniforms = new Map>([\n ['colorNum', new THREE.Uniform(4.0)],\n ['pixelSize', new THREE.Uniform(2.0)]\n ]);\n super('RetroEffect', ditherFragmentShader, { uniforms });\n this.uniforms = uniforms;\n }\n set colorNum(value: number) {\n this.uniforms.get('colorNum')!.value = value;\n }\n get colorNum(): number {\n return this.uniforms.get('colorNum')!.value;\n }\n set pixelSize(value: number) {\n this.uniforms.get('pixelSize')!.value = value;\n }\n get pixelSize(): number {\n return this.uniforms.get('pixelSize')!.value;\n }\n}\n\nconst RetroEffect = forwardRef((props, ref) => {\n const { colorNum, pixelSize } = props;\n const WrappedRetroEffect = wrapEffect(RetroEffectImpl);\n return ;\n});\n\nRetroEffect.displayName = 'RetroEffect';\n\ninterface WaveUniforms {\n [key: string]: THREE.Uniform;\n time: THREE.Uniform;\n resolution: THREE.Uniform;\n waveSpeed: THREE.Uniform;\n waveFrequency: THREE.Uniform;\n waveAmplitude: THREE.Uniform;\n waveColor: THREE.Uniform;\n mousePos: THREE.Uniform;\n enableMouseInteraction: THREE.Uniform;\n mouseRadius: THREE.Uniform;\n}\n\ninterface DitheredWavesProps {\n waveSpeed: number;\n waveFrequency: number;\n waveAmplitude: number;\n waveColor: [number, number, number];\n colorNum: number;\n pixelSize: number;\n disableAnimation: boolean;\n enableMouseInteraction: boolean;\n mouseRadius: number;\n}\n\nfunction DitheredWaves({\n waveSpeed,\n waveFrequency,\n waveAmplitude,\n waveColor,\n colorNum,\n pixelSize,\n disableAnimation,\n enableMouseInteraction,\n mouseRadius\n}: DitheredWavesProps) {\n const mesh = useRef(null);\n const mouseRef = useRef(new THREE.Vector2());\n const { viewport, size, gl } = useThree();\n\n const waveUniformsRef = useRef({\n time: new THREE.Uniform(0),\n resolution: new THREE.Uniform(new THREE.Vector2(0, 0)),\n waveSpeed: new THREE.Uniform(waveSpeed),\n waveFrequency: new THREE.Uniform(waveFrequency),\n waveAmplitude: new THREE.Uniform(waveAmplitude),\n waveColor: new THREE.Uniform(new THREE.Color(...waveColor)),\n mousePos: new THREE.Uniform(new THREE.Vector2(0, 0)),\n enableMouseInteraction: new THREE.Uniform(enableMouseInteraction ? 1 : 0),\n mouseRadius: new THREE.Uniform(mouseRadius)\n });\n\n useEffect(() => {\n const dpr = gl.getPixelRatio();\n const newWidth = Math.floor(size.width * dpr);\n const newHeight = Math.floor(size.height * dpr);\n const currentRes = waveUniformsRef.current.resolution.value;\n if (currentRes.x !== newWidth || currentRes.y !== newHeight) {\n currentRes.set(newWidth, newHeight);\n }\n }, [size, gl]);\n\n const prevColor = useRef([...waveColor]);\n useFrame(({ clock }) => {\n const u = waveUniformsRef.current;\n\n if (!disableAnimation) {\n u.time.value = clock.getElapsedTime();\n }\n\n if (u.waveSpeed.value !== waveSpeed) u.waveSpeed.value = waveSpeed;\n if (u.waveFrequency.value !== waveFrequency) u.waveFrequency.value = waveFrequency;\n if (u.waveAmplitude.value !== waveAmplitude) u.waveAmplitude.value = waveAmplitude;\n\n if (!prevColor.current.every((v, i) => v === waveColor[i])) {\n u.waveColor.value.set(...waveColor);\n prevColor.current = [...waveColor];\n }\n\n u.enableMouseInteraction.value = enableMouseInteraction ? 1 : 0;\n u.mouseRadius.value = mouseRadius;\n\n if (enableMouseInteraction) {\n u.mousePos.value.copy(mouseRef.current);\n }\n });\n\n const handlePointerMove = (e: ThreeEvent) => {\n if (!enableMouseInteraction) return;\n const rect = gl.domElement.getBoundingClientRect();\n const dpr = gl.getPixelRatio();\n mouseRef.current.set((e.clientX - rect.left) * dpr, (e.clientY - rect.top) * dpr);\n };\n\n return (\n <>\n \n \n \n \n\n \n \n \n\n \n \n \n \n \n );\n}\n\ninterface DitherProps {\n waveSpeed?: number;\n waveFrequency?: number;\n waveAmplitude?: number;\n waveColor?: [number, number, number];\n colorNum?: number;\n pixelSize?: number;\n disableAnimation?: boolean;\n enableMouseInteraction?: boolean;\n mouseRadius?: number;\n}\n\nexport default function Dither({\n waveSpeed = 0.05,\n waveFrequency = 3,\n waveAmplitude = 0.3,\n waveColor = [0.5, 0.5, 0.5],\n colorNum = 4,\n pixelSize = 2,\n disableAnimation = false,\n enableMouseInteraction = true,\n mouseRadius = 1\n}: DitherProps) {\n return (\n \n \n \n );\n}\n" + "path": "Dither.tsx", + "content": "/* eslint-disable react/no-unknown-property */\nimport { useRef, useEffect, forwardRef } from 'react';\nimport { Canvas, useFrame, useThree, type ThreeEvent } from '@react-three/fiber';\nimport { EffectComposer, wrapEffect } from '@react-three/postprocessing';\nimport { Effect } from 'postprocessing';\nimport * as THREE from 'three';\n\nimport './Dither.css';\n\nconst waveVertexShader = `\nprecision highp float;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n vec4 modelPosition = modelMatrix * vec4(position, 1.0);\n vec4 viewPosition = viewMatrix * modelPosition;\n gl_Position = projectionMatrix * viewPosition;\n}\n`;\n\nconst waveFragmentShader = `\nprecision highp float;\nuniform vec2 resolution;\nuniform float time;\nuniform float waveSpeed;\nuniform float waveFrequency;\nuniform float waveAmplitude;\nuniform vec3 waveColor;\nuniform vec2 mousePos;\nuniform int enableMouseInteraction;\nuniform float mouseRadius;\n\nvec4 mod289(vec4 x) { return x - floor(x * (1.0/289.0)) * 289.0; }\nvec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }\nvec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\nvec2 fade(vec2 t) { return t*t*t*(t*(t*6.0-15.0)+10.0); }\n\nfloat cnoise(vec2 P) {\n vec4 Pi = floor(P.xyxy) + vec4(0.0,0.0,1.0,1.0);\n vec4 Pf = fract(P.xyxy) - vec4(0.0,0.0,1.0,1.0);\n Pi = mod289(Pi);\n vec4 ix = Pi.xzxz;\n vec4 iy = Pi.yyww;\n vec4 fx = Pf.xzxz;\n vec4 fy = Pf.yyww;\n vec4 i = permute(permute(ix) + iy);\n vec4 gx = fract(i * (1.0/41.0)) * 2.0 - 1.0;\n vec4 gy = abs(gx) - 0.5;\n vec4 tx = floor(gx + 0.5);\n gx = gx - tx;\n vec2 g00 = vec2(gx.x, gy.x);\n vec2 g10 = vec2(gx.y, gy.y);\n vec2 g01 = vec2(gx.z, gy.z);\n vec2 g11 = vec2(gx.w, gy.w);\n vec4 norm = taylorInvSqrt(vec4(dot(g00,g00), dot(g01,g01), dot(g10,g10), dot(g11,g11)));\n g00 *= norm.x; g01 *= norm.y; g10 *= norm.z; g11 *= norm.w;\n float n00 = dot(g00, vec2(fx.x, fy.x));\n float n10 = dot(g10, vec2(fx.y, fy.y));\n float n01 = dot(g01, vec2(fx.z, fy.z));\n float n11 = dot(g11, vec2(fx.w, fy.w));\n vec2 fade_xy = fade(Pf.xy);\n vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);\n return 2.3 * mix(n_x.x, n_x.y, fade_xy.y);\n}\n\nconst int OCTAVES = 4;\nfloat fbm(vec2 p) {\n float value = 0.0;\n float amp = 1.0;\n float freq = waveFrequency;\n for (int i = 0; i < OCTAVES; i++) {\n value += amp * abs(cnoise(p));\n p *= freq;\n amp *= waveAmplitude;\n }\n return value;\n}\n\nfloat pattern(vec2 p) {\n vec2 p2 = p - time * waveSpeed;\n return fbm(p + fbm(p2)); \n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / resolution.xy;\n uv -= 0.5;\n uv.x *= resolution.x / resolution.y;\n float f = pattern(uv);\n if (enableMouseInteraction == 1) {\n vec2 mouseNDC = (mousePos / resolution - 0.5) * vec2(1.0, -1.0);\n mouseNDC.x *= resolution.x / resolution.y;\n float dist = length(uv - mouseNDC);\n float effect = 1.0 - smoothstep(0.0, mouseRadius, dist);\n f -= 0.5 * effect;\n }\n vec3 col = mix(vec3(0.0), waveColor, f);\n gl_FragColor = vec4(col, 1.0);\n}\n`;\n\nconst ditherFragmentShader = `\nprecision highp float;\nuniform float colorNum;\nuniform float pixelSize;\nconst float bayerMatrix8x8[64] = float[64](\n 0.0/64.0, 48.0/64.0, 12.0/64.0, 60.0/64.0, 3.0/64.0, 51.0/64.0, 15.0/64.0, 63.0/64.0,\n 32.0/64.0,16.0/64.0, 44.0/64.0, 28.0/64.0, 35.0/64.0,19.0/64.0, 47.0/64.0, 31.0/64.0,\n 8.0/64.0, 56.0/64.0, 4.0/64.0, 52.0/64.0, 11.0/64.0,59.0/64.0, 7.0/64.0, 55.0/64.0,\n 40.0/64.0,24.0/64.0, 36.0/64.0, 20.0/64.0, 43.0/64.0,27.0/64.0, 39.0/64.0, 23.0/64.0,\n 2.0/64.0, 50.0/64.0, 14.0/64.0, 62.0/64.0, 1.0/64.0,49.0/64.0, 13.0/64.0, 61.0/64.0,\n 34.0/64.0,18.0/64.0, 46.0/64.0, 30.0/64.0, 33.0/64.0,17.0/64.0, 45.0/64.0, 29.0/64.0,\n 10.0/64.0,58.0/64.0, 6.0/64.0, 54.0/64.0, 9.0/64.0,57.0/64.0, 5.0/64.0, 53.0/64.0,\n 42.0/64.0,26.0/64.0, 38.0/64.0, 22.0/64.0, 41.0/64.0,25.0/64.0, 37.0/64.0, 21.0/64.0\n);\n\nvec3 dither(vec2 uv, vec3 color) {\n vec2 scaledCoord = floor(uv * resolution / pixelSize);\n int x = int(mod(scaledCoord.x, 8.0));\n int y = int(mod(scaledCoord.y, 8.0));\n float threshold = bayerMatrix8x8[y * 8 + x] - 0.25;\n float step = 1.0 / (colorNum - 1.0);\n color += threshold * step;\n float bias = 0.2;\n color = clamp(color - bias, 0.0, 1.0);\n return floor(color * (colorNum - 1.0) + 0.5) / (colorNum - 1.0);\n}\n\nvoid mainImage(in vec4 inputColor, in vec2 uv, out vec4 outputColor) {\n vec2 normalizedPixelSize = pixelSize / resolution;\n vec2 uvPixel = normalizedPixelSize * floor(uv / normalizedPixelSize);\n vec4 color = texture2D(inputBuffer, uvPixel);\n color.rgb = dither(uv, color.rgb);\n outputColor = color;\n}\n`;\n\nclass RetroEffectImpl extends Effect {\n public uniforms: Map>;\n constructor() {\n const uniforms = new Map>([\n ['colorNum', new THREE.Uniform(4.0)],\n ['pixelSize', new THREE.Uniform(2.0)]\n ]);\n super('RetroEffect', ditherFragmentShader, { uniforms });\n this.uniforms = uniforms;\n }\n set colorNum(value: number) {\n this.uniforms.get('colorNum')!.value = value;\n }\n get colorNum(): number {\n return this.uniforms.get('colorNum')!.value;\n }\n set pixelSize(value: number) {\n this.uniforms.get('pixelSize')!.value = value;\n }\n get pixelSize(): number {\n return this.uniforms.get('pixelSize')!.value;\n }\n}\n\nconst RetroEffect = forwardRef((props, ref) => {\n const { colorNum, pixelSize } = props;\n const WrappedRetroEffect = wrapEffect(RetroEffectImpl);\n return ;\n});\n\nRetroEffect.displayName = 'RetroEffect';\n\ninterface WaveUniforms {\n [key: string]: THREE.Uniform;\n time: THREE.Uniform;\n resolution: THREE.Uniform;\n waveSpeed: THREE.Uniform;\n waveFrequency: THREE.Uniform;\n waveAmplitude: THREE.Uniform;\n waveColor: THREE.Uniform;\n mousePos: THREE.Uniform;\n enableMouseInteraction: THREE.Uniform;\n mouseRadius: THREE.Uniform;\n}\n\ninterface DitheredWavesProps {\n waveSpeed: number;\n waveFrequency: number;\n waveAmplitude: number;\n waveColor: [number, number, number];\n colorNum: number;\n pixelSize: number;\n disableAnimation: boolean;\n enableMouseInteraction: boolean;\n mouseRadius: number;\n}\n\nfunction DitheredWaves({\n waveSpeed,\n waveFrequency,\n waveAmplitude,\n waveColor,\n colorNum,\n pixelSize,\n disableAnimation,\n enableMouseInteraction,\n mouseRadius\n}: DitheredWavesProps) {\n const mesh = useRef(null);\n const mouseRef = useRef(new THREE.Vector2());\n const { viewport, size, gl } = useThree();\n\n const waveUniformsRef = useRef({\n time: new THREE.Uniform(0),\n resolution: new THREE.Uniform(new THREE.Vector2(0, 0)),\n waveSpeed: new THREE.Uniform(waveSpeed),\n waveFrequency: new THREE.Uniform(waveFrequency),\n waveAmplitude: new THREE.Uniform(waveAmplitude),\n waveColor: new THREE.Uniform(new THREE.Color(...waveColor)),\n mousePos: new THREE.Uniform(new THREE.Vector2(0, 0)),\n enableMouseInteraction: new THREE.Uniform(enableMouseInteraction ? 1 : 0),\n mouseRadius: new THREE.Uniform(mouseRadius)\n });\n\n useEffect(() => {\n const dpr = gl.getPixelRatio();\n const newWidth = Math.floor(size.width * dpr);\n const newHeight = Math.floor(size.height * dpr);\n const currentRes = waveUniformsRef.current.resolution.value;\n if (currentRes.x !== newWidth || currentRes.y !== newHeight) {\n currentRes.set(newWidth, newHeight);\n }\n }, [size, gl]);\n\n const prevColor = useRef([...waveColor]);\n useFrame(({ clock }) => {\n const u = waveUniformsRef.current;\n\n if (!disableAnimation) {\n u.time.value = clock.getElapsedTime();\n }\n\n if (u.waveSpeed.value !== waveSpeed) u.waveSpeed.value = waveSpeed;\n if (u.waveFrequency.value !== waveFrequency) u.waveFrequency.value = waveFrequency;\n if (u.waveAmplitude.value !== waveAmplitude) u.waveAmplitude.value = waveAmplitude;\n\n if (!prevColor.current.every((v, i) => v === waveColor[i])) {\n u.waveColor.value.set(...waveColor);\n prevColor.current = [...waveColor];\n }\n\n u.enableMouseInteraction.value = enableMouseInteraction ? 1 : 0;\n u.mouseRadius.value = mouseRadius;\n\n if (enableMouseInteraction) {\n u.mousePos.value.copy(mouseRef.current);\n }\n });\n\n const handlePointerMove = (e: ThreeEvent) => {\n if (!enableMouseInteraction) return;\n const rect = gl.domElement.getBoundingClientRect();\n const dpr = gl.getPixelRatio();\n mouseRef.current.set((e.clientX - rect.left) * dpr, (e.clientY - rect.top) * dpr);\n };\n\n return (\n <>\n \n \n \n \n\n \n \n \n\n \n \n \n \n \n );\n}\n\ninterface DitherProps {\n waveSpeed?: number;\n waveFrequency?: number;\n waveAmplitude?: number;\n waveColor?: [number, number, number];\n colorNum?: number;\n pixelSize?: number;\n disableAnimation?: boolean;\n enableMouseInteraction?: boolean;\n mouseRadius?: number;\n}\n\nexport default function Dither({\n waveSpeed = 0.05,\n waveFrequency = 3,\n waveAmplitude = 0.3,\n waveColor = [0.5, 0.5, 0.5],\n colorNum = 4,\n pixelSize = 2,\n disableAnimation = false,\n enableMouseInteraction = true,\n mouseRadius = 1\n}: DitherProps) {\n return (\n \n \n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/Dither-TS-TW.json b/public/r/Dither-TS-TW.json index 6a103191..2712b7cc 100644 --- a/public/r/Dither-TS-TW.json +++ b/public/r/Dither-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "Dither/Dither.tsx", - "content": "/* eslint-disable react/no-unknown-property */\nimport { useRef, useState, useEffect, forwardRef } from 'react';\nimport { Canvas, useFrame, useThree, ThreeEvent } from '@react-three/fiber';\nimport { EffectComposer, wrapEffect } from '@react-three/postprocessing';\nimport { Effect } from 'postprocessing';\nimport * as THREE from 'three';\n\nconst waveVertexShader = `\nprecision highp float;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n vec4 modelPosition = modelMatrix * vec4(position, 1.0);\n vec4 viewPosition = viewMatrix * modelPosition;\n gl_Position = projectionMatrix * viewPosition;\n}\n`;\n\nconst waveFragmentShader = `\nprecision highp float;\nuniform vec2 resolution;\nuniform float time;\nuniform float waveSpeed;\nuniform float waveFrequency;\nuniform float waveAmplitude;\nuniform vec3 waveColor;\nuniform vec2 mousePos;\nuniform int enableMouseInteraction;\nuniform float mouseRadius;\n\nvec4 mod289(vec4 x) { return x - floor(x * (1.0/289.0)) * 289.0; }\nvec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }\nvec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\nvec2 fade(vec2 t) { return t*t*t*(t*(t*6.0-15.0)+10.0); }\n\nfloat cnoise(vec2 P) {\n vec4 Pi = floor(P.xyxy) + vec4(0.0,0.0,1.0,1.0);\n vec4 Pf = fract(P.xyxy) - vec4(0.0,0.0,1.0,1.0);\n Pi = mod289(Pi);\n vec4 ix = Pi.xzxz;\n vec4 iy = Pi.yyww;\n vec4 fx = Pf.xzxz;\n vec4 fy = Pf.yyww;\n vec4 i = permute(permute(ix) + iy);\n vec4 gx = fract(i * (1.0/41.0)) * 2.0 - 1.0;\n vec4 gy = abs(gx) - 0.5;\n vec4 tx = floor(gx + 0.5);\n gx = gx - tx;\n vec2 g00 = vec2(gx.x, gy.x);\n vec2 g10 = vec2(gx.y, gy.y);\n vec2 g01 = vec2(gx.z, gy.z);\n vec2 g11 = vec2(gx.w, gy.w);\n vec4 norm = taylorInvSqrt(vec4(dot(g00,g00), dot(g01,g01), dot(g10,g10), dot(g11,g11)));\n g00 *= norm.x; g01 *= norm.y; g10 *= norm.z; g11 *= norm.w;\n float n00 = dot(g00, vec2(fx.x, fy.x));\n float n10 = dot(g10, vec2(fx.y, fy.y));\n float n01 = dot(g01, vec2(fx.z, fy.z));\n float n11 = dot(g11, vec2(fx.w, fy.w));\n vec2 fade_xy = fade(Pf.xy);\n vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);\n return 2.3 * mix(n_x.x, n_x.y, fade_xy.y);\n}\n\nconst int OCTAVES = 4;\nfloat fbm(vec2 p) {\n float value = 0.0;\n float amp = 1.0;\n float freq = waveFrequency;\n for (int i = 0; i < OCTAVES; i++) {\n value += amp * abs(cnoise(p));\n p *= freq;\n amp *= waveAmplitude;\n }\n return value;\n}\n\nfloat pattern(vec2 p) {\n vec2 p2 = p - time * waveSpeed;\n return fbm(p + fbm(p2)); \n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / resolution.xy;\n uv -= 0.5;\n uv.x *= resolution.x / resolution.y;\n float f = pattern(uv);\n if (enableMouseInteraction == 1) {\n vec2 mouseNDC = (mousePos / resolution - 0.5) * vec2(1.0, -1.0);\n mouseNDC.x *= resolution.x / resolution.y;\n float dist = length(uv - mouseNDC);\n float effect = 1.0 - smoothstep(0.0, mouseRadius, dist);\n f -= 0.5 * effect;\n }\n vec3 col = mix(vec3(0.0), waveColor, f);\n gl_FragColor = vec4(col, 1.0);\n}\n`;\n\nconst ditherFragmentShader = `\nprecision highp float;\nuniform float colorNum;\nuniform float pixelSize;\nconst float bayerMatrix8x8[64] = float[64](\n 0.0/64.0, 48.0/64.0, 12.0/64.0, 60.0/64.0, 3.0/64.0, 51.0/64.0, 15.0/64.0, 63.0/64.0,\n 32.0/64.0,16.0/64.0, 44.0/64.0, 28.0/64.0, 35.0/64.0,19.0/64.0, 47.0/64.0, 31.0/64.0,\n 8.0/64.0, 56.0/64.0, 4.0/64.0, 52.0/64.0, 11.0/64.0,59.0/64.0, 7.0/64.0, 55.0/64.0,\n 40.0/64.0,24.0/64.0, 36.0/64.0, 20.0/64.0, 43.0/64.0,27.0/64.0, 39.0/64.0, 23.0/64.0,\n 2.0/64.0, 50.0/64.0, 14.0/64.0, 62.0/64.0, 1.0/64.0,49.0/64.0, 13.0/64.0, 61.0/64.0,\n 34.0/64.0,18.0/64.0, 46.0/64.0, 30.0/64.0, 33.0/64.0,17.0/64.0, 45.0/64.0, 29.0/64.0,\n 10.0/64.0,58.0/64.0, 6.0/64.0, 54.0/64.0, 9.0/64.0,57.0/64.0, 5.0/64.0, 53.0/64.0,\n 42.0/64.0,26.0/64.0, 38.0/64.0, 22.0/64.0, 41.0/64.0,25.0/64.0, 37.0/64.0, 21.0/64.0\n);\n\nvec3 dither(vec2 uv, vec3 color) {\n vec2 scaledCoord = floor(uv * resolution / pixelSize);\n int x = int(mod(scaledCoord.x, 8.0));\n int y = int(mod(scaledCoord.y, 8.0));\n float threshold = bayerMatrix8x8[y * 8 + x] - 0.25;\n float step = 1.0 / (colorNum - 1.0);\n color += threshold * step;\n float bias = 0.2;\n color = clamp(color - bias, 0.0, 1.0);\n return floor(color * (colorNum - 1.0) + 0.5) / (colorNum - 1.0);\n}\n\nvoid mainImage(in vec4 inputColor, in vec2 uv, out vec4 outputColor) {\n vec2 normalizedPixelSize = pixelSize / resolution;\n vec2 uvPixel = normalizedPixelSize * floor(uv / normalizedPixelSize);\n vec4 color = texture2D(inputBuffer, uvPixel);\n color.rgb = dither(uv, color.rgb);\n outputColor = color;\n}\n`;\n\nclass RetroEffectImpl extends Effect {\n public uniforms: Map>;\n constructor() {\n const uniforms = new Map>([\n ['colorNum', new THREE.Uniform(4.0)],\n ['pixelSize', new THREE.Uniform(2.0)]\n ]);\n super('RetroEffect', ditherFragmentShader, { uniforms });\n this.uniforms = uniforms;\n }\n set colorNum(value: number) {\n this.uniforms.get('colorNum')!.value = value;\n }\n get colorNum(): number {\n return this.uniforms.get('colorNum')!.value;\n }\n set pixelSize(value: number) {\n this.uniforms.get('pixelSize')!.value = value;\n }\n get pixelSize(): number {\n return this.uniforms.get('pixelSize')!.value;\n }\n}\n\nconst RetroEffect = forwardRef((props, ref) => {\n const { colorNum, pixelSize } = props;\n const WrappedRetroEffect = wrapEffect(RetroEffectImpl);\n return ;\n});\n\nRetroEffect.displayName = 'RetroEffect';\n\ninterface WaveUniforms {\n [key: string]: THREE.Uniform;\n time: THREE.Uniform;\n resolution: THREE.Uniform;\n waveSpeed: THREE.Uniform;\n waveFrequency: THREE.Uniform;\n waveAmplitude: THREE.Uniform;\n waveColor: THREE.Uniform;\n mousePos: THREE.Uniform;\n enableMouseInteraction: THREE.Uniform;\n mouseRadius: THREE.Uniform;\n}\n\ninterface DitheredWavesProps {\n waveSpeed: number;\n waveFrequency: number;\n waveAmplitude: number;\n waveColor: [number, number, number];\n colorNum: number;\n pixelSize: number;\n disableAnimation: boolean;\n enableMouseInteraction: boolean;\n mouseRadius: number;\n}\n\nfunction DitheredWaves({\n waveSpeed,\n waveFrequency,\n waveAmplitude,\n waveColor,\n colorNum,\n pixelSize,\n disableAnimation,\n enableMouseInteraction,\n mouseRadius\n}: DitheredWavesProps) {\n const mesh = useRef(null);\n const mouseRef = useRef(new THREE.Vector2());\n const { viewport, size, gl } = useThree();\n\n const waveUniformsRef = useRef({\n time: new THREE.Uniform(0),\n resolution: new THREE.Uniform(new THREE.Vector2(0, 0)),\n waveSpeed: new THREE.Uniform(waveSpeed),\n waveFrequency: new THREE.Uniform(waveFrequency),\n waveAmplitude: new THREE.Uniform(waveAmplitude),\n waveColor: new THREE.Uniform(new THREE.Color(...waveColor)),\n mousePos: new THREE.Uniform(new THREE.Vector2(0, 0)),\n enableMouseInteraction: new THREE.Uniform(enableMouseInteraction ? 1 : 0),\n mouseRadius: new THREE.Uniform(mouseRadius)\n });\n\n useEffect(() => {\n const dpr = gl.getPixelRatio();\n const newWidth = Math.floor(size.width * dpr);\n const newHeight = Math.floor(size.height * dpr);\n const currentRes = waveUniformsRef.current.resolution.value;\n if (currentRes.x !== newWidth || currentRes.y !== newHeight) {\n currentRes.set(newWidth, newHeight);\n }\n }, [size, gl]);\n\n const prevColor = useRef([...waveColor]);\n useFrame(({ clock }) => {\n const u = waveUniformsRef.current;\n\n if (!disableAnimation) {\n u.time.value = clock.getElapsedTime();\n }\n\n if (u.waveSpeed.value !== waveSpeed) u.waveSpeed.value = waveSpeed;\n if (u.waveFrequency.value !== waveFrequency) u.waveFrequency.value = waveFrequency;\n if (u.waveAmplitude.value !== waveAmplitude) u.waveAmplitude.value = waveAmplitude;\n\n if (!prevColor.current.every((v, i) => v === waveColor[i])) {\n u.waveColor.value.set(...waveColor);\n prevColor.current = [...waveColor];\n }\n\n u.enableMouseInteraction.value = enableMouseInteraction ? 1 : 0;\n u.mouseRadius.value = mouseRadius;\n\n if (enableMouseInteraction) {\n u.mousePos.value.copy(mouseRef.current);\n }\n });\n\n const handlePointerMove = (e: ThreeEvent) => {\n if (!enableMouseInteraction) return;\n const rect = gl.domElement.getBoundingClientRect();\n const dpr = gl.getPixelRatio();\n mouseRef.current.set((e.clientX - rect.left) * dpr, (e.clientY - rect.top) * dpr);\n };\n\n return (\n <>\n \n \n \n \n\n \n \n \n\n \n \n \n \n \n );\n}\n\ninterface DitherProps {\n waveSpeed?: number;\n waveFrequency?: number;\n waveAmplitude?: number;\n waveColor?: [number, number, number];\n colorNum?: number;\n pixelSize?: number;\n disableAnimation?: boolean;\n enableMouseInteraction?: boolean;\n mouseRadius?: number;\n}\n\nexport default function Dither({\n waveSpeed = 0.05,\n waveFrequency = 3,\n waveAmplitude = 0.3,\n waveColor = [0.5, 0.5, 0.5],\n colorNum = 4,\n pixelSize = 2,\n disableAnimation = false,\n enableMouseInteraction = true,\n mouseRadius = 1\n}: DitherProps) {\n return (\n \n \n \n );\n}\n" + "content": "/* eslint-disable react/no-unknown-property */\nimport { useRef, useState, useEffect, forwardRef } from 'react';\nimport { Canvas, useFrame, useThree, type ThreeEvent } from '@react-three/fiber';\nimport { EffectComposer, wrapEffect } from '@react-three/postprocessing';\nimport { Effect } from 'postprocessing';\nimport * as THREE from 'three';\n\nconst waveVertexShader = `\nprecision highp float;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n vec4 modelPosition = modelMatrix * vec4(position, 1.0);\n vec4 viewPosition = viewMatrix * modelPosition;\n gl_Position = projectionMatrix * viewPosition;\n}\n`;\n\nconst waveFragmentShader = `\nprecision highp float;\nuniform vec2 resolution;\nuniform float time;\nuniform float waveSpeed;\nuniform float waveFrequency;\nuniform float waveAmplitude;\nuniform vec3 waveColor;\nuniform vec2 mousePos;\nuniform int enableMouseInteraction;\nuniform float mouseRadius;\n\nvec4 mod289(vec4 x) { return x - floor(x * (1.0/289.0)) * 289.0; }\nvec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }\nvec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\nvec2 fade(vec2 t) { return t*t*t*(t*(t*6.0-15.0)+10.0); }\n\nfloat cnoise(vec2 P) {\n vec4 Pi = floor(P.xyxy) + vec4(0.0,0.0,1.0,1.0);\n vec4 Pf = fract(P.xyxy) - vec4(0.0,0.0,1.0,1.0);\n Pi = mod289(Pi);\n vec4 ix = Pi.xzxz;\n vec4 iy = Pi.yyww;\n vec4 fx = Pf.xzxz;\n vec4 fy = Pf.yyww;\n vec4 i = permute(permute(ix) + iy);\n vec4 gx = fract(i * (1.0/41.0)) * 2.0 - 1.0;\n vec4 gy = abs(gx) - 0.5;\n vec4 tx = floor(gx + 0.5);\n gx = gx - tx;\n vec2 g00 = vec2(gx.x, gy.x);\n vec2 g10 = vec2(gx.y, gy.y);\n vec2 g01 = vec2(gx.z, gy.z);\n vec2 g11 = vec2(gx.w, gy.w);\n vec4 norm = taylorInvSqrt(vec4(dot(g00,g00), dot(g01,g01), dot(g10,g10), dot(g11,g11)));\n g00 *= norm.x; g01 *= norm.y; g10 *= norm.z; g11 *= norm.w;\n float n00 = dot(g00, vec2(fx.x, fy.x));\n float n10 = dot(g10, vec2(fx.y, fy.y));\n float n01 = dot(g01, vec2(fx.z, fy.z));\n float n11 = dot(g11, vec2(fx.w, fy.w));\n vec2 fade_xy = fade(Pf.xy);\n vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);\n return 2.3 * mix(n_x.x, n_x.y, fade_xy.y);\n}\n\nconst int OCTAVES = 4;\nfloat fbm(vec2 p) {\n float value = 0.0;\n float amp = 1.0;\n float freq = waveFrequency;\n for (int i = 0; i < OCTAVES; i++) {\n value += amp * abs(cnoise(p));\n p *= freq;\n amp *= waveAmplitude;\n }\n return value;\n}\n\nfloat pattern(vec2 p) {\n vec2 p2 = p - time * waveSpeed;\n return fbm(p + fbm(p2)); \n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / resolution.xy;\n uv -= 0.5;\n uv.x *= resolution.x / resolution.y;\n float f = pattern(uv);\n if (enableMouseInteraction == 1) {\n vec2 mouseNDC = (mousePos / resolution - 0.5) * vec2(1.0, -1.0);\n mouseNDC.x *= resolution.x / resolution.y;\n float dist = length(uv - mouseNDC);\n float effect = 1.0 - smoothstep(0.0, mouseRadius, dist);\n f -= 0.5 * effect;\n }\n vec3 col = mix(vec3(0.0), waveColor, f);\n gl_FragColor = vec4(col, 1.0);\n}\n`;\n\nconst ditherFragmentShader = `\nprecision highp float;\nuniform float colorNum;\nuniform float pixelSize;\nconst float bayerMatrix8x8[64] = float[64](\n 0.0/64.0, 48.0/64.0, 12.0/64.0, 60.0/64.0, 3.0/64.0, 51.0/64.0, 15.0/64.0, 63.0/64.0,\n 32.0/64.0,16.0/64.0, 44.0/64.0, 28.0/64.0, 35.0/64.0,19.0/64.0, 47.0/64.0, 31.0/64.0,\n 8.0/64.0, 56.0/64.0, 4.0/64.0, 52.0/64.0, 11.0/64.0,59.0/64.0, 7.0/64.0, 55.0/64.0,\n 40.0/64.0,24.0/64.0, 36.0/64.0, 20.0/64.0, 43.0/64.0,27.0/64.0, 39.0/64.0, 23.0/64.0,\n 2.0/64.0, 50.0/64.0, 14.0/64.0, 62.0/64.0, 1.0/64.0,49.0/64.0, 13.0/64.0, 61.0/64.0,\n 34.0/64.0,18.0/64.0, 46.0/64.0, 30.0/64.0, 33.0/64.0,17.0/64.0, 45.0/64.0, 29.0/64.0,\n 10.0/64.0,58.0/64.0, 6.0/64.0, 54.0/64.0, 9.0/64.0,57.0/64.0, 5.0/64.0, 53.0/64.0,\n 42.0/64.0,26.0/64.0, 38.0/64.0, 22.0/64.0, 41.0/64.0,25.0/64.0, 37.0/64.0, 21.0/64.0\n);\n\nvec3 dither(vec2 uv, vec3 color) {\n vec2 scaledCoord = floor(uv * resolution / pixelSize);\n int x = int(mod(scaledCoord.x, 8.0));\n int y = int(mod(scaledCoord.y, 8.0));\n float threshold = bayerMatrix8x8[y * 8 + x] - 0.25;\n float step = 1.0 / (colorNum - 1.0);\n color += threshold * step;\n float bias = 0.2;\n color = clamp(color - bias, 0.0, 1.0);\n return floor(color * (colorNum - 1.0) + 0.5) / (colorNum - 1.0);\n}\n\nvoid mainImage(in vec4 inputColor, in vec2 uv, out vec4 outputColor) {\n vec2 normalizedPixelSize = pixelSize / resolution;\n vec2 uvPixel = normalizedPixelSize * floor(uv / normalizedPixelSize);\n vec4 color = texture2D(inputBuffer, uvPixel);\n color.rgb = dither(uv, color.rgb);\n outputColor = color;\n}\n`;\n\nclass RetroEffectImpl extends Effect {\n public uniforms: Map>;\n constructor() {\n const uniforms = new Map>([\n ['colorNum', new THREE.Uniform(4.0)],\n ['pixelSize', new THREE.Uniform(2.0)]\n ]);\n super('RetroEffect', ditherFragmentShader, { uniforms });\n this.uniforms = uniforms;\n }\n set colorNum(value: number) {\n this.uniforms.get('colorNum')!.value = value;\n }\n get colorNum(): number {\n return this.uniforms.get('colorNum')!.value;\n }\n set pixelSize(value: number) {\n this.uniforms.get('pixelSize')!.value = value;\n }\n get pixelSize(): number {\n return this.uniforms.get('pixelSize')!.value;\n }\n}\n\nconst RetroEffect = forwardRef((props, ref) => {\n const { colorNum, pixelSize } = props;\n const WrappedRetroEffect = wrapEffect(RetroEffectImpl);\n return ;\n});\n\nRetroEffect.displayName = 'RetroEffect';\n\ninterface WaveUniforms {\n [key: string]: THREE.Uniform;\n time: THREE.Uniform;\n resolution: THREE.Uniform;\n waveSpeed: THREE.Uniform;\n waveFrequency: THREE.Uniform;\n waveAmplitude: THREE.Uniform;\n waveColor: THREE.Uniform;\n mousePos: THREE.Uniform;\n enableMouseInteraction: THREE.Uniform;\n mouseRadius: THREE.Uniform;\n}\n\ninterface DitheredWavesProps {\n waveSpeed: number;\n waveFrequency: number;\n waveAmplitude: number;\n waveColor: [number, number, number];\n colorNum: number;\n pixelSize: number;\n disableAnimation: boolean;\n enableMouseInteraction: boolean;\n mouseRadius: number;\n}\n\nfunction DitheredWaves({\n waveSpeed,\n waveFrequency,\n waveAmplitude,\n waveColor,\n colorNum,\n pixelSize,\n disableAnimation,\n enableMouseInteraction,\n mouseRadius\n}: DitheredWavesProps) {\n const mesh = useRef(null);\n const mouseRef = useRef(new THREE.Vector2());\n const { viewport, size, gl } = useThree();\n\n const waveUniformsRef = useRef({\n time: new THREE.Uniform(0),\n resolution: new THREE.Uniform(new THREE.Vector2(0, 0)),\n waveSpeed: new THREE.Uniform(waveSpeed),\n waveFrequency: new THREE.Uniform(waveFrequency),\n waveAmplitude: new THREE.Uniform(waveAmplitude),\n waveColor: new THREE.Uniform(new THREE.Color(...waveColor)),\n mousePos: new THREE.Uniform(new THREE.Vector2(0, 0)),\n enableMouseInteraction: new THREE.Uniform(enableMouseInteraction ? 1 : 0),\n mouseRadius: new THREE.Uniform(mouseRadius)\n });\n\n useEffect(() => {\n const dpr = gl.getPixelRatio();\n const newWidth = Math.floor(size.width * dpr);\n const newHeight = Math.floor(size.height * dpr);\n const currentRes = waveUniformsRef.current.resolution.value;\n if (currentRes.x !== newWidth || currentRes.y !== newHeight) {\n currentRes.set(newWidth, newHeight);\n }\n }, [size, gl]);\n\n const prevColor = useRef([...waveColor]);\n useFrame(({ clock }) => {\n const u = waveUniformsRef.current;\n\n if (!disableAnimation) {\n u.time.value = clock.getElapsedTime();\n }\n\n if (u.waveSpeed.value !== waveSpeed) u.waveSpeed.value = waveSpeed;\n if (u.waveFrequency.value !== waveFrequency) u.waveFrequency.value = waveFrequency;\n if (u.waveAmplitude.value !== waveAmplitude) u.waveAmplitude.value = waveAmplitude;\n\n if (!prevColor.current.every((v, i) => v === waveColor[i])) {\n u.waveColor.value.set(...waveColor);\n prevColor.current = [...waveColor];\n }\n\n u.enableMouseInteraction.value = enableMouseInteraction ? 1 : 0;\n u.mouseRadius.value = mouseRadius;\n\n if (enableMouseInteraction) {\n u.mousePos.value.copy(mouseRef.current);\n }\n });\n\n const handlePointerMove = (e: ThreeEvent) => {\n if (!enableMouseInteraction) return;\n const rect = gl.domElement.getBoundingClientRect();\n const dpr = gl.getPixelRatio();\n mouseRef.current.set((e.clientX - rect.left) * dpr, (e.clientY - rect.top) * dpr);\n };\n\n return (\n <>\n \n \n \n \n\n \n \n \n\n \n \n \n \n \n );\n}\n\ninterface DitherProps {\n waveSpeed?: number;\n waveFrequency?: number;\n waveAmplitude?: number;\n waveColor?: [number, number, number];\n colorNum?: number;\n pixelSize?: number;\n disableAnimation?: boolean;\n enableMouseInteraction?: boolean;\n mouseRadius?: number;\n}\n\nexport default function Dither({\n waveSpeed = 0.05,\n waveFrequency = 3,\n waveAmplitude = 0.3,\n waveColor = [0.5, 0.5, 0.5],\n colorNum = 4,\n pixelSize = 2,\n disableAnimation = false,\n enableMouseInteraction = true,\n mouseRadius = 1\n}: DitherProps) {\n return (\n \n \n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/Dock-JS-CSS.json b/public/r/Dock-JS-CSS.json index e5f73393..50cfcca5 100644 --- a/public/r/Dock-JS-CSS.json +++ b/public/r/Dock-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Dock/Dock.css", + "type": "registry:file", + "path": "Dock.css", + "target": "@components/Dock.css", "content": ".dock-outer {\n margin: 0 0.5rem;\n display: flex;\n max-width: 100%;\n align-items: center;\n}\n\n.dock-panel {\n position: absolute;\n bottom: 0.5rem;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n align-items: flex-end;\n width: fit-content;\n gap: 1rem;\n border-radius: 1rem;\n background-color: #120F17;\n border: 1px solid #222;\n padding: 0 0.5rem 0.5rem;\n}\n\n.dock-item {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 10px;\n background-color: #120F17;\n border: 1px solid #222;\n box-shadow:\n 0 4px 6px -1px rgba(0, 0, 0, 0.1),\n 0 2px 4px -1px rgba(0, 0, 0, 0.06);\n cursor: pointer;\n outline: none;\n}\n\n.dock-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.dock-label {\n position: absolute;\n top: -1.5rem;\n left: 50%;\n width: fit-content;\n white-space: pre;\n border-radius: 0.375rem;\n border: 1px solid #222;\n background-color: #120F17;\n padding: 0.125rem 0.5rem;\n font-size: 0.75rem;\n color: #fff;\n transform: translateX(-50%);\n}\n" }, { "type": "registry:component", - "path": "Dock/Dock.jsx", + "path": "Dock.jsx", "content": "'use client';\n\nimport { motion, useMotionValue, useSpring, useTransform, AnimatePresence } from 'motion/react';\nimport { Children, cloneElement, useEffect, useMemo, useRef, useState } from 'react';\n\nimport './Dock.css';\n\nfunction DockItem({ children, className = '', onClick, mouseX, spring, distance, magnification, baseItemSize, label }) {\n const ref = useRef(null);\n const isHovered = useMotionValue(0);\n\n const mouseDistance = useTransform(mouseX, val => {\n const rect = ref.current?.getBoundingClientRect() ?? {\n x: 0,\n width: baseItemSize\n };\n return val - rect.x - baseItemSize / 2;\n });\n\n const targetSize = useTransform(mouseDistance, [-distance, 0, distance], [baseItemSize, magnification, baseItemSize]);\n const size = useSpring(targetSize, spring);\n\n const handleKeyDown = e => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n };\n\n return (\n isHovered.set(1)}\n onHoverEnd={() => isHovered.set(0)}\n onFocus={() => isHovered.set(1)}\n onBlur={() => isHovered.set(0)}\n onClick={onClick}\n className={`dock-item ${className}`}\n tabIndex={0}\n role=\"button\"\n aria-haspopup=\"true\"\n aria-label={label}\n onKeyDown={handleKeyDown}\n >\n {Children.map(children, child => cloneElement(child, { isHovered }))}\n \n );\n}\n\nfunction DockLabel({ children, className = '', ...rest }) {\n const { isHovered } = rest;\n const [isVisible, setIsVisible] = useState(false);\n\n useEffect(() => {\n const unsubscribe = isHovered.on('change', latest => {\n setIsVisible(latest === 1);\n });\n return () => unsubscribe();\n }, [isHovered]);\n\n return (\n \n {isVisible && (\n \n {children}\n \n )}\n \n );\n}\n\nfunction DockIcon({ children, className = '' }) {\n return
{children}
;\n}\n\nexport default function Dock({\n items,\n className = '',\n spring = { mass: 0.1, stiffness: 150, damping: 12 },\n magnification = 70,\n distance = 200,\n panelHeight = 68,\n dockHeight = 256,\n baseItemSize = 50\n}) {\n const mouseX = useMotionValue(Infinity);\n const isHovered = useMotionValue(0);\n\n const maxHeight = useMemo(\n () => Math.max(dockHeight, magnification + magnification / 2 + 4),\n [magnification, dockHeight]\n );\n const heightRow = useTransform(isHovered, [0, 1], [panelHeight, maxHeight]);\n const height = useSpring(heightRow, spring);\n\n return (\n \n {\n isHovered.set(1);\n mouseX.set(pageX);\n }}\n onMouseLeave={() => {\n isHovered.set(0);\n mouseX.set(Infinity);\n }}\n className={`dock-panel ${className}`}\n style={{ height: panelHeight }}\n role=\"toolbar\"\n aria-label=\"Application dock\"\n >\n {items.map((item, index) => (\n \n {item.icon}\n {item.label}\n \n ))}\n \n \n );\n}\n" } ], diff --git a/public/r/Dock-TS-CSS.json b/public/r/Dock-TS-CSS.json index f3dbee4d..8ffa1d50 100644 --- a/public/r/Dock-TS-CSS.json +++ b/public/r/Dock-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Dock/Dock.css", + "type": "registry:file", + "path": "Dock.css", + "target": "@components/Dock.css", "content": ".dock-outer {\n margin: 0 0.5rem;\n display: flex;\n max-width: 100%;\n align-items: center;\n}\n\n.dock-panel {\n position: absolute;\n bottom: 0.5rem;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n align-items: flex-end;\n width: fit-content;\n gap: 1rem;\n border-radius: 1rem;\n background-color: #120F17;\n border: 1px solid #222;\n padding: 0 0.5rem 0.5rem;\n}\n\n.dock-item {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 10px;\n background-color: #120F17;\n border: 1px solid #222;\n box-shadow:\n 0 4px 6px -1px rgba(0, 0, 0, 0.1),\n 0 2px 4px -1px rgba(0, 0, 0, 0.06);\n cursor: pointer;\n outline: none;\n}\n\n.dock-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.dock-label {\n position: absolute;\n top: -1.5rem;\n left: 50%;\n width: fit-content;\n white-space: pre;\n border-radius: 0.375rem;\n border: 1px solid #222;\n background-color: #120F17;\n padding: 0.125rem 0.5rem;\n font-size: 0.75rem;\n color: #fff;\n transform: translateX(-50%);\n}\n" }, { "type": "registry:component", - "path": "Dock/Dock.tsx", + "path": "Dock.tsx", "content": "'use client';\n\nimport {\n motion,\n MotionValue,\n useMotionValue,\n useSpring,\n useTransform,\n type SpringOptions,\n AnimatePresence\n} from 'motion/react';\nimport React, { Children, cloneElement, useEffect, useMemo, useRef, useState } from 'react';\n\nimport './Dock.css';\n\nexport type DockItemData = {\n icon: React.ReactNode;\n label: React.ReactNode;\n onClick: () => void;\n className?: string;\n};\n\nexport type DockProps = {\n items: DockItemData[];\n className?: string;\n distance?: number;\n panelHeight?: number;\n baseItemSize?: number;\n dockHeight?: number;\n magnification?: number;\n spring?: SpringOptions;\n};\n\ntype DockItemProps = {\n className?: string;\n children: React.ReactNode;\n onClick?: () => void;\n mouseX: MotionValue;\n spring: SpringOptions;\n distance: number;\n baseItemSize: number;\n magnification: number;\n label?: React.ReactNode;\n};\n\nfunction DockItem({\n children,\n className = '',\n onClick,\n mouseX,\n spring,\n distance,\n magnification,\n baseItemSize,\n label\n}: DockItemProps) {\n const ref = useRef(null);\n const isHovered = useMotionValue(0);\n\n const mouseDistance = useTransform(mouseX, val => {\n const rect = ref.current?.getBoundingClientRect() ?? {\n x: 0,\n width: baseItemSize\n };\n return val - rect.x - baseItemSize / 2;\n });\n\n const targetSize = useTransform(mouseDistance, [-distance, 0, distance], [baseItemSize, magnification, baseItemSize]);\n const size = useSpring(targetSize, spring);\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n };\n\n return (\n isHovered.set(1)}\n onHoverEnd={() => isHovered.set(0)}\n onFocus={() => isHovered.set(1)}\n onBlur={() => isHovered.set(0)}\n onClick={onClick}\n onKeyDown={handleKeyDown}\n className={`dock-item ${className}`}\n tabIndex={0}\n role=\"button\"\n aria-haspopup=\"true\"\n aria-label={typeof label === 'string' ? label : undefined}\n >\n {Children.map(children, child =>\n React.isValidElement(child)\n ? cloneElement(child as React.ReactElement<{ isHovered?: MotionValue }>, { isHovered })\n : child\n )}\n \n );\n}\n\ntype DockLabelProps = {\n className?: string;\n children: React.ReactNode;\n isHovered?: MotionValue;\n};\n\nfunction DockLabel({ children, className = '', isHovered }: DockLabelProps) {\n const [isVisible, setIsVisible] = useState(false);\n\n useEffect(() => {\n if (!isHovered) return;\n const unsubscribe = isHovered.on('change', latest => {\n setIsVisible(latest === 1);\n });\n return () => unsubscribe();\n }, [isHovered]);\n\n return (\n \n {isVisible && (\n \n {children}\n \n )}\n \n );\n}\n\ntype DockIconProps = {\n className?: string;\n children: React.ReactNode;\n isHovered?: MotionValue;\n};\n\nfunction DockIcon({ children, className = '' }: DockIconProps) {\n return
{children}
;\n}\n\nexport default function Dock({\n items,\n className = '',\n spring = { mass: 0.1, stiffness: 150, damping: 12 },\n magnification = 70,\n distance = 200,\n panelHeight = 68,\n dockHeight = 256,\n baseItemSize = 50\n}: DockProps) {\n const mouseX = useMotionValue(Infinity);\n const isHovered = useMotionValue(0);\n\n const maxHeight = useMemo(\n () => Math.max(dockHeight, magnification + magnification / 2 + 4),\n [magnification, dockHeight]\n );\n const heightRow = useTransform(isHovered, [0, 1], [panelHeight, maxHeight]);\n const height = useSpring(heightRow, spring);\n\n return (\n \n {\n isHovered.set(1);\n mouseX.set(pageX);\n }}\n onMouseLeave={() => {\n isHovered.set(0);\n mouseX.set(Infinity);\n }}\n className={`dock-panel ${className}`}\n style={{ height: panelHeight }}\n role=\"toolbar\"\n aria-label=\"Application dock\"\n >\n {items.map((item, index) => (\n \n {item.icon}\n {item.label}\n \n ))}\n \n \n );\n}\n" } ], diff --git a/public/r/DomeGallery-JS-CSS.json b/public/r/DomeGallery-JS-CSS.json index a041c1d1..6f3e1a62 100644 --- a/public/r/DomeGallery-JS-CSS.json +++ b/public/r/DomeGallery-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "DomeGallery/DomeGallery.css", + "type": "registry:file", + "path": "DomeGallery.css", + "target": "@components/DomeGallery.css", "content": ".sphere-root {\n position: relative;\n width: 100%;\n height: 100%;\n --radius: 520px;\n --viewer-pad: 72px;\n --circ: calc(var(--radius) * 3.14);\n --rot-y: calc((360deg / var(--segments-x)) / 2);\n --rot-x: calc((360deg / var(--segments-y)) / 2);\n --item-width: calc(var(--circ) / var(--segments-x));\n --item-height: calc(var(--circ) / var(--segments-y));\n}\n\n.sphere-root * {\n box-sizing: border-box;\n}\n\n.sphere,\n.item,\n.item__image {\n transform-style: preserve-3d;\n}\n\nmain.sphere-main {\n position: absolute;\n inset: 0;\n display: grid;\n place-items: center;\n overflow: hidden;\n touch-action: none;\n user-select: none;\n -webkit-user-select: none;\n background: transparent;\n}\n\n.stage {\n width: 100%;\n height: 100%;\n display: grid;\n place-items: center;\n perspective: calc(var(--radius) * 2);\n perspective-origin: 50% 50%;\n contain: layout paint size;\n}\n\n.sphere {\n transform: translateZ(calc(var(--radius) * -1));\n will-change: transform;\n}\n\n.overlay,\n.overlay--blur {\n position: absolute;\n inset: 0;\n margin: auto;\n z-index: 3;\n pointer-events: none;\n}\n\n.overlay {\n background-image: radial-gradient(rgba(235, 235, 235, 0) 65%, var(--overlay-blur-color, #120F17) 100%);\n}\n\n.overlay--blur {\n -webkit-mask-image: radial-gradient(rgba(235, 235, 235, 0) 70%, var(--overlay-blur-color, #120F17) 90%);\n mask-image: radial-gradient(rgba(235, 235, 235, 0) 70%, var(--overlay-blur-color, #120F17) 90%);\n backdrop-filter: blur(3px);\n}\n\n.item {\n width: calc(var(--item-width) * var(--item-size-x));\n height: calc(var(--item-height) * var(--item-size-y));\n position: absolute;\n top: -999px;\n bottom: -999px;\n left: -999px;\n right: -999px;\n margin: auto;\n transform-origin: 50% 50%;\n backface-visibility: hidden;\n transition: transform 300ms;\n transform: rotateY(calc(var(--rot-y) * (var(--offset-x) + ((var(--item-size-x) - 1) / 2)) + var(--rot-y-delta, 0deg)))\n rotateX(calc(var(--rot-x) * (var(--offset-y) - ((var(--item-size-y) - 1) / 2)) + var(--rot-x-delta, 0deg)))\n translateZ(var(--radius));\n}\n\n.item__image {\n position: absolute;\n display: block;\n inset: 10px;\n border-radius: var(--tile-radius, 12px);\n background: transparent;\n overflow: hidden;\n backface-visibility: hidden;\n transition: transform 300ms;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n touch-action: manipulation;\n pointer-events: auto;\n -webkit-transform: translateZ(0);\n transform: translateZ(0);\n}\n\n.item__image:focus {\n outline: none;\n}\n\n.item__image img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n pointer-events: none;\n backface-visibility: hidden;\n filter: var(--image-filter, none);\n}\n\n.viewer {\n position: absolute;\n inset: 0;\n z-index: 20;\n pointer-events: none;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--viewer-pad);\n}\n\n.viewer .frame {\n height: 100%;\n aspect-ratio: 1;\n border-radius: var(--enlarge-radius, 32px);\n display: flex;\n}\n\n@media (max-aspect-ratio: 1/1) {\n .viewer .frame {\n height: auto;\n width: 100%;\n }\n}\n\n.viewer .scrim {\n position: absolute;\n inset: 0;\n z-index: 10;\n background: rgba(0, 0, 0, 0.4);\n pointer-events: none;\n opacity: 0;\n transition: opacity 500ms ease;\n backdrop-filter: blur(3px);\n}\n\n.sphere-root[data-enlarging='true'] .viewer .scrim {\n opacity: 1;\n pointer-events: all;\n}\n\n.viewer .enlarge {\n position: absolute;\n z-index: 30;\n border-radius: var(--enlarge-radius, 32px);\n overflow: hidden;\n transition:\n transform 500ms ease,\n opacity 500ms ease;\n transform-origin: top left;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);\n}\n\n.viewer .enlarge img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n filter: var(--image-filter, none);\n}\n\n.sphere-root .enlarge-closing img {\n filter: var(--image-filter, none);\n}\n\n.edge-fade {\n position: absolute;\n left: 0;\n right: 0;\n height: 120px;\n z-index: 5;\n pointer-events: none;\n background: linear-gradient(to bottom, transparent, var(--overlay-blur-color, #120F17));\n}\n\n.edge-fade--top {\n top: 0;\n transform: rotate(180deg);\n}\n\n.edge-fade--bottom {\n bottom: 0;\n}\n" }, { "type": "registry:component", - "path": "DomeGallery/DomeGallery.jsx", + "path": "DomeGallery.jsx", "content": "import { useEffect, useMemo, useRef, useCallback } from 'react';\nimport { useGesture } from '@use-gesture/react';\nimport './DomeGallery.css';\n\nconst DEFAULT_IMAGES = [\n {\n src: 'https://images.unsplash.com/photo-1755331039789-7e5680e26e8f?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n alt: 'Abstract art'\n },\n {\n src: 'https://images.unsplash.com/photo-1755569309049-98410b94f66d?q=80&w=772&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n alt: 'Modern sculpture'\n },\n {\n src: 'https://images.unsplash.com/photo-1755497595318-7e5e3523854f?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n alt: 'Digital artwork'\n },\n {\n src: 'https://images.unsplash.com/photo-1755353985163-c2a0fe5ac3d8?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n alt: 'Contemporary art'\n },\n {\n src: 'https://images.unsplash.com/photo-1745965976680-d00be7dc0377?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n alt: 'Geometric pattern'\n },\n {\n src: 'https://images.unsplash.com/photo-1752588975228-21f44630bb3c?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n alt: 'Textured surface'\n },\n { src: 'https://pbs.twimg.com/media/Gyla7NnXMAAXSo_?format=jpg&name=large', alt: 'Social media image' }\n];\n\nconst DEFAULTS = {\n maxVerticalRotationDeg: 5,\n dragSensitivity: 20,\n enlargeTransitionMs: 300,\n segments: 35\n};\n\nconst clamp = (v, min, max) => Math.min(Math.max(v, min), max);\nconst normalizeAngle = d => ((d % 360) + 360) % 360;\nconst wrapAngleSigned = deg => {\n const a = (((deg + 180) % 360) + 360) % 360;\n return a - 180;\n};\nconst getDataNumber = (el, name, fallback) => {\n const attr = el.dataset[name] ?? el.getAttribute(`data-${name}`);\n const n = attr == null ? NaN : parseFloat(attr);\n return Number.isFinite(n) ? n : fallback;\n};\n\nfunction buildItems(pool, seg) {\n const xCols = Array.from({ length: seg }, (_, i) => -37 + i * 2);\n const evenYs = [-4, -2, 0, 2, 4];\n const oddYs = [-3, -1, 1, 3, 5];\n\n const coords = xCols.flatMap((x, c) => {\n const ys = c % 2 === 0 ? evenYs : oddYs;\n return ys.map(y => ({ x, y, sizeX: 2, sizeY: 2 }));\n });\n\n const totalSlots = coords.length;\n if (pool.length === 0) {\n return coords.map(c => ({ ...c, src: '', alt: '' }));\n }\n if (pool.length > totalSlots) {\n console.warn(\n `[DomeGallery] Provided image count (${pool.length}) exceeds available tiles (${totalSlots}). Some images will not be shown.`\n );\n }\n\n const normalizedImages = pool.map(image => {\n if (typeof image === 'string') {\n return { src: image, alt: '' };\n }\n return { src: image.src || '', alt: image.alt || '' };\n });\n\n const usedImages = Array.from({ length: totalSlots }, (_, i) => normalizedImages[i % normalizedImages.length]);\n\n for (let i = 1; i < usedImages.length; i++) {\n if (usedImages[i].src === usedImages[i - 1].src) {\n for (let j = i + 1; j < usedImages.length; j++) {\n if (usedImages[j].src !== usedImages[i].src) {\n const tmp = usedImages[i];\n usedImages[i] = usedImages[j];\n usedImages[j] = tmp;\n break;\n }\n }\n }\n }\n\n return coords.map((c, i) => ({\n ...c,\n src: usedImages[i].src,\n alt: usedImages[i].alt\n }));\n}\n\nfunction computeItemBaseRotation(offsetX, offsetY, sizeX, sizeY, segments) {\n const unit = 360 / segments / 2;\n const rotateY = unit * (offsetX + (sizeX - 1) / 2);\n const rotateX = unit * (offsetY - (sizeY - 1) / 2);\n return { rotateX, rotateY };\n}\n\nexport default function DomeGallery({\n images = DEFAULT_IMAGES,\n fit = 0.5,\n fitBasis = 'auto',\n minRadius = 600,\n maxRadius = Infinity,\n padFactor = 0.25,\n overlayBlurColor = '#120F17',\n maxVerticalRotationDeg = DEFAULTS.maxVerticalRotationDeg,\n dragSensitivity = DEFAULTS.dragSensitivity,\n enlargeTransitionMs = DEFAULTS.enlargeTransitionMs,\n segments = DEFAULTS.segments,\n dragDampening = 2,\n openedImageWidth = '250px',\n openedImageHeight = '350px',\n imageBorderRadius = '30px',\n openedImageBorderRadius = '30px',\n grayscale = true\n}) {\n const rootRef = useRef(null);\n const mainRef = useRef(null);\n const sphereRef = useRef(null);\n const frameRef = useRef(null);\n const viewerRef = useRef(null);\n const scrimRef = useRef(null);\n const focusedElRef = useRef(null);\n const originalTilePositionRef = useRef(null);\n\n const rotationRef = useRef({ x: 0, y: 0 });\n const startRotRef = useRef({ x: 0, y: 0 });\n const startPosRef = useRef(null);\n const draggingRef = useRef(false);\n const movedRef = useRef(false);\n const inertiaRAF = useRef(null);\n const openingRef = useRef(false);\n const openStartedAtRef = useRef(0);\n const lastDragEndAt = useRef(0);\n\n const scrollLockedRef = useRef(false);\n const lockScroll = useCallback(() => {\n if (scrollLockedRef.current) return;\n scrollLockedRef.current = true;\n document.body.classList.add('dg-scroll-lock');\n }, []);\n const unlockScroll = useCallback(() => {\n if (!scrollLockedRef.current) return;\n if (rootRef.current?.getAttribute('data-enlarging') === 'true') return;\n scrollLockedRef.current = false;\n document.body.classList.remove('dg-scroll-lock');\n }, []);\n\n const items = useMemo(() => buildItems(images, segments), [images, segments]);\n\n const applyTransform = (xDeg, yDeg) => {\n const el = sphereRef.current;\n if (el) {\n el.style.transform = `translateZ(calc(var(--radius) * -1)) rotateX(${xDeg}deg) rotateY(${yDeg}deg)`;\n }\n };\n\n const lockedRadiusRef = useRef(null);\n\n useEffect(() => {\n const root = rootRef.current;\n if (!root) return;\n const ro = new ResizeObserver(entries => {\n const cr = entries[0].contentRect;\n const w = Math.max(1, cr.width),\n h = Math.max(1, cr.height);\n const minDim = Math.min(w, h),\n maxDim = Math.max(w, h),\n aspect = w / h;\n let basis;\n switch (fitBasis) {\n case 'min':\n basis = minDim;\n break;\n case 'max':\n basis = maxDim;\n break;\n case 'width':\n basis = w;\n break;\n case 'height':\n basis = h;\n break;\n default:\n basis = aspect >= 1.3 ? w : minDim;\n }\n let radius = basis * fit;\n const heightGuard = h * 1.35;\n radius = Math.min(radius, heightGuard);\n radius = clamp(radius, minRadius, maxRadius);\n lockedRadiusRef.current = Math.round(radius);\n\n const viewerPad = Math.max(8, Math.round(minDim * padFactor));\n root.style.setProperty('--radius', `${lockedRadiusRef.current}px`);\n root.style.setProperty('--viewer-pad', `${viewerPad}px`);\n root.style.setProperty('--overlay-blur-color', overlayBlurColor);\n root.style.setProperty('--tile-radius', imageBorderRadius);\n root.style.setProperty('--enlarge-radius', openedImageBorderRadius);\n root.style.setProperty('--image-filter', grayscale ? 'grayscale(1)' : 'none');\n applyTransform(rotationRef.current.x, rotationRef.current.y);\n\n const enlargedOverlay = viewerRef.current?.querySelector('.enlarge');\n if (enlargedOverlay && frameRef.current && mainRef.current) {\n const frameR = frameRef.current.getBoundingClientRect();\n const mainR = mainRef.current.getBoundingClientRect();\n\n const hasCustomSize = openedImageWidth && openedImageHeight;\n if (hasCustomSize) {\n const tempDiv = document.createElement('div');\n tempDiv.style.cssText = `position: absolute; width: ${openedImageWidth}; height: ${openedImageHeight}; visibility: hidden;`;\n document.body.appendChild(tempDiv);\n const tempRect = tempDiv.getBoundingClientRect();\n document.body.removeChild(tempDiv);\n\n const centeredLeft = frameR.left - mainR.left + (frameR.width - tempRect.width) / 2;\n const centeredTop = frameR.top - mainR.top + (frameR.height - tempRect.height) / 2;\n\n enlargedOverlay.style.left = `${centeredLeft}px`;\n enlargedOverlay.style.top = `${centeredTop}px`;\n } else {\n enlargedOverlay.style.left = `${frameR.left - mainR.left}px`;\n enlargedOverlay.style.top = `${frameR.top - mainR.top}px`;\n enlargedOverlay.style.width = `${frameR.width}px`;\n enlargedOverlay.style.height = `${frameR.height}px`;\n }\n }\n });\n ro.observe(root);\n return () => ro.disconnect();\n }, [\n fit,\n fitBasis,\n minRadius,\n maxRadius,\n padFactor,\n overlayBlurColor,\n grayscale,\n imageBorderRadius,\n openedImageBorderRadius,\n openedImageWidth,\n openedImageHeight\n ]);\n\n useEffect(() => {\n applyTransform(rotationRef.current.x, rotationRef.current.y);\n }, []);\n\n const stopInertia = useCallback(() => {\n if (inertiaRAF.current) {\n cancelAnimationFrame(inertiaRAF.current);\n inertiaRAF.current = null;\n }\n }, []);\n\n const startInertia = useCallback(\n (vx, vy) => {\n const MAX_V = 1.4;\n let vX = clamp(vx, -MAX_V, MAX_V) * 80;\n let vY = clamp(vy, -MAX_V, MAX_V) * 80;\n let frames = 0;\n const d = clamp(dragDampening ?? 0.6, 0, 1);\n const frictionMul = 0.94 + 0.055 * d;\n const stopThreshold = 0.015 - 0.01 * d;\n const maxFrames = Math.round(90 + 270 * d);\n const step = () => {\n vX *= frictionMul;\n vY *= frictionMul;\n if (Math.abs(vX) < stopThreshold && Math.abs(vY) < stopThreshold) {\n inertiaRAF.current = null;\n return;\n }\n if (++frames > maxFrames) {\n inertiaRAF.current = null;\n return;\n }\n const nextX = clamp(rotationRef.current.x - vY / 200, -maxVerticalRotationDeg, maxVerticalRotationDeg);\n const nextY = wrapAngleSigned(rotationRef.current.y + vX / 200);\n rotationRef.current = { x: nextX, y: nextY };\n applyTransform(nextX, nextY);\n inertiaRAF.current = requestAnimationFrame(step);\n };\n stopInertia();\n inertiaRAF.current = requestAnimationFrame(step);\n },\n [dragDampening, maxVerticalRotationDeg, stopInertia]\n );\n\n useGesture(\n {\n onDragStart: ({ event }) => {\n if (focusedElRef.current) return;\n stopInertia();\n const evt = event;\n draggingRef.current = true;\n movedRef.current = false;\n startRotRef.current = { ...rotationRef.current };\n startPosRef.current = { x: evt.clientX, y: evt.clientY };\n },\n onDrag: ({ event, last, velocity = [0, 0], direction = [0, 0], movement }) => {\n if (focusedElRef.current || !draggingRef.current || !startPosRef.current) return;\n const evt = event;\n const dxTotal = evt.clientX - startPosRef.current.x;\n const dyTotal = evt.clientY - startPosRef.current.y;\n if (!movedRef.current) {\n const dist2 = dxTotal * dxTotal + dyTotal * dyTotal;\n if (dist2 > 16) movedRef.current = true;\n }\n const nextX = clamp(\n startRotRef.current.x - dyTotal / dragSensitivity,\n -maxVerticalRotationDeg,\n maxVerticalRotationDeg\n );\n const nextY = wrapAngleSigned(startRotRef.current.y + dxTotal / dragSensitivity);\n if (rotationRef.current.x !== nextX || rotationRef.current.y !== nextY) {\n rotationRef.current = { x: nextX, y: nextY };\n applyTransform(nextX, nextY);\n }\n if (last) {\n draggingRef.current = false;\n let [vMagX, vMagY] = velocity;\n const [dirX, dirY] = direction;\n let vx = vMagX * dirX;\n let vy = vMagY * dirY;\n if (Math.abs(vx) < 0.001 && Math.abs(vy) < 0.001 && Array.isArray(movement)) {\n const [mx, my] = movement;\n vx = clamp((mx / dragSensitivity) * 0.02, -1.2, 1.2);\n vy = clamp((my / dragSensitivity) * 0.02, -1.2, 1.2);\n }\n if (Math.abs(vx) > 0.005 || Math.abs(vy) > 0.005) startInertia(vx, vy);\n if (movedRef.current) lastDragEndAt.current = performance.now();\n movedRef.current = false;\n }\n }\n },\n { target: mainRef, eventOptions: { passive: true } }\n );\n\n useEffect(() => {\n const scrim = scrimRef.current;\n if (!scrim) return;\n const close = () => {\n if (performance.now() - openStartedAtRef.current < 250) return;\n const el = focusedElRef.current;\n if (!el) return;\n const parent = el.parentElement;\n const overlay = viewerRef.current?.querySelector('.enlarge');\n if (!overlay) return;\n const refDiv = parent.querySelector('.item__image--reference');\n const originalPos = originalTilePositionRef.current;\n if (!originalPos) {\n overlay.remove();\n if (refDiv) refDiv.remove();\n parent.style.setProperty('--rot-y-delta', '0deg');\n parent.style.setProperty('--rot-x-delta', '0deg');\n el.style.visibility = '';\n el.style.zIndex = 0;\n focusedElRef.current = null;\n rootRef.current?.removeAttribute('data-enlarging');\n openingRef.current = false;\n unlockScroll();\n return;\n }\n const currentRect = overlay.getBoundingClientRect();\n const rootRect = rootRef.current.getBoundingClientRect();\n const originalPosRelativeToRoot = {\n left: originalPos.left - rootRect.left,\n top: originalPos.top - rootRect.top,\n width: originalPos.width,\n height: originalPos.height\n };\n const overlayRelativeToRoot = {\n left: currentRect.left - rootRect.left,\n top: currentRect.top - rootRect.top,\n width: currentRect.width,\n height: currentRect.height\n };\n const animatingOverlay = document.createElement('div');\n animatingOverlay.className = 'enlarge-closing';\n animatingOverlay.style.cssText = `position:absolute;left:${overlayRelativeToRoot.left}px;top:${overlayRelativeToRoot.top}px;width:${overlayRelativeToRoot.width}px;height:${overlayRelativeToRoot.height}px;z-index:9999;border-radius: var(--enlarge-radius, 32px);overflow:hidden;box-shadow:0 10px 30px rgba(0,0,0,.35);transition:all ${enlargeTransitionMs}ms ease-out;pointer-events:none;margin:0;transform:none;`;\n const originalImg = overlay.querySelector('img');\n if (originalImg) {\n const img = originalImg.cloneNode();\n img.style.cssText = 'width:100%;height:100%;object-fit:cover;';\n animatingOverlay.appendChild(img);\n }\n overlay.remove();\n rootRef.current.appendChild(animatingOverlay);\n void animatingOverlay.getBoundingClientRect();\n requestAnimationFrame(() => {\n animatingOverlay.style.left = originalPosRelativeToRoot.left + 'px';\n animatingOverlay.style.top = originalPosRelativeToRoot.top + 'px';\n animatingOverlay.style.width = originalPosRelativeToRoot.width + 'px';\n animatingOverlay.style.height = originalPosRelativeToRoot.height + 'px';\n animatingOverlay.style.opacity = '0';\n });\n const cleanup = () => {\n animatingOverlay.remove();\n originalTilePositionRef.current = null;\n if (refDiv) refDiv.remove();\n parent.style.transition = 'none';\n el.style.transition = 'none';\n parent.style.setProperty('--rot-y-delta', '0deg');\n parent.style.setProperty('--rot-x-delta', '0deg');\n requestAnimationFrame(() => {\n el.style.visibility = '';\n el.style.opacity = '0';\n el.style.zIndex = 0;\n focusedElRef.current = null;\n rootRef.current?.removeAttribute('data-enlarging');\n requestAnimationFrame(() => {\n parent.style.transition = '';\n el.style.transition = 'opacity 300ms ease-out';\n requestAnimationFrame(() => {\n el.style.opacity = '1';\n setTimeout(() => {\n el.style.transition = '';\n el.style.opacity = '';\n openingRef.current = false;\n if (!draggingRef.current && rootRef.current?.getAttribute('data-enlarging') !== 'true')\n document.body.classList.remove('dg-scroll-lock');\n }, 300);\n });\n });\n });\n };\n animatingOverlay.addEventListener('transitionend', cleanup, { once: true });\n };\n scrim.addEventListener('click', close);\n const onKey = e => {\n if (e.key === 'Escape') close();\n };\n window.addEventListener('keydown', onKey);\n return () => {\n scrim.removeEventListener('click', close);\n window.removeEventListener('keydown', onKey);\n };\n }, [enlargeTransitionMs, unlockScroll]);\n\n const openItemFromElement = useCallback(\n el => {\n if (openingRef.current) return;\n openingRef.current = true;\n openStartedAtRef.current = performance.now();\n lockScroll();\n const parent = el.parentElement;\n focusedElRef.current = el;\n el.setAttribute('data-focused', 'true');\n const offsetX = getDataNumber(parent, 'offsetX', 0);\n const offsetY = getDataNumber(parent, 'offsetY', 0);\n const sizeX = getDataNumber(parent, 'sizeX', 2);\n const sizeY = getDataNumber(parent, 'sizeY', 2);\n const parentRot = computeItemBaseRotation(offsetX, offsetY, sizeX, sizeY, segments);\n const parentY = normalizeAngle(parentRot.rotateY);\n const globalY = normalizeAngle(rotationRef.current.y);\n let rotY = -(parentY + globalY) % 360;\n if (rotY < -180) rotY += 360;\n const rotX = -parentRot.rotateX - rotationRef.current.x;\n parent.style.setProperty('--rot-y-delta', `${rotY}deg`);\n parent.style.setProperty('--rot-x-delta', `${rotX}deg`);\n const refDiv = document.createElement('div');\n refDiv.className = 'item__image item__image--reference';\n refDiv.style.opacity = '0';\n refDiv.style.transform = `rotateX(${-parentRot.rotateX}deg) rotateY(${-parentRot.rotateY}deg)`;\n parent.appendChild(refDiv);\n\n void refDiv.offsetHeight;\n\n const tileR = refDiv.getBoundingClientRect();\n const mainR = mainRef.current?.getBoundingClientRect();\n const frameR = frameRef.current?.getBoundingClientRect();\n\n if (!mainR || !frameR || tileR.width <= 0 || tileR.height <= 0) {\n openingRef.current = false;\n focusedElRef.current = null;\n parent.removeChild(refDiv);\n unlockScroll();\n return;\n }\n\n originalTilePositionRef.current = { left: tileR.left, top: tileR.top, width: tileR.width, height: tileR.height };\n el.style.visibility = 'hidden';\n el.style.zIndex = 0;\n const overlay = document.createElement('div');\n overlay.className = 'enlarge';\n overlay.style.position = 'absolute';\n overlay.style.left = frameR.left - mainR.left + 'px';\n overlay.style.top = frameR.top - mainR.top + 'px';\n overlay.style.width = frameR.width + 'px';\n overlay.style.height = frameR.height + 'px';\n overlay.style.opacity = '0';\n overlay.style.zIndex = '30';\n overlay.style.willChange = 'transform, opacity';\n overlay.style.transformOrigin = 'top left';\n overlay.style.transition = `transform ${enlargeTransitionMs}ms ease, opacity ${enlargeTransitionMs}ms ease`;\n const rawSrc = parent.dataset.src || el.querySelector('img')?.src || '';\n const img = document.createElement('img');\n img.src = rawSrc;\n overlay.appendChild(img);\n viewerRef.current.appendChild(overlay);\n const tx0 = tileR.left - frameR.left;\n const ty0 = tileR.top - frameR.top;\n const sx0 = tileR.width / frameR.width;\n const sy0 = tileR.height / frameR.height;\n\n const validSx0 = isFinite(sx0) && sx0 > 0 ? sx0 : 1;\n const validSy0 = isFinite(sy0) && sy0 > 0 ? sy0 : 1;\n\n overlay.style.transform = `translate(${tx0}px, ${ty0}px) scale(${validSx0}, ${validSy0})`;\n\n setTimeout(() => {\n if (!overlay.parentElement) return;\n overlay.style.opacity = '1';\n overlay.style.transform = 'translate(0px, 0px) scale(1, 1)';\n rootRef.current?.setAttribute('data-enlarging', 'true');\n }, 16);\n\n const wantsResize = openedImageWidth || openedImageHeight;\n if (wantsResize) {\n const onFirstEnd = ev => {\n if (ev.propertyName !== 'transform') return;\n overlay.removeEventListener('transitionend', onFirstEnd);\n const prevTransition = overlay.style.transition;\n overlay.style.transition = 'none';\n const tempWidth = openedImageWidth || `${frameR.width}px`;\n const tempHeight = openedImageHeight || `${frameR.height}px`;\n overlay.style.width = tempWidth;\n overlay.style.height = tempHeight;\n const newRect = overlay.getBoundingClientRect();\n overlay.style.width = frameR.width + 'px';\n overlay.style.height = frameR.height + 'px';\n void overlay.offsetWidth;\n overlay.style.transition = `left ${enlargeTransitionMs}ms ease, top ${enlargeTransitionMs}ms ease, width ${enlargeTransitionMs}ms ease, height ${enlargeTransitionMs}ms ease`;\n const centeredLeft = frameR.left - mainR.left + (frameR.width - newRect.width) / 2;\n const centeredTop = frameR.top - mainR.top + (frameR.height - newRect.height) / 2;\n requestAnimationFrame(() => {\n overlay.style.left = `${centeredLeft}px`;\n overlay.style.top = `${centeredTop}px`;\n overlay.style.width = tempWidth;\n overlay.style.height = tempHeight;\n });\n const cleanupSecond = () => {\n overlay.removeEventListener('transitionend', cleanupSecond);\n overlay.style.transition = prevTransition;\n };\n overlay.addEventListener('transitionend', cleanupSecond, { once: true });\n };\n overlay.addEventListener('transitionend', onFirstEnd);\n }\n },\n [enlargeTransitionMs, lockScroll, openedImageHeight, openedImageWidth, segments, unlockScroll]\n );\n\n const onTileClick = useCallback(\n e => {\n if (draggingRef.current) return;\n if (movedRef.current) return;\n if (performance.now() - lastDragEndAt.current < 80) return;\n if (openingRef.current) return;\n openItemFromElement(e.currentTarget);\n },\n [openItemFromElement]\n );\n\n const onTilePointerUp = useCallback(\n e => {\n if (e.pointerType !== 'touch') return;\n if (draggingRef.current) return;\n if (movedRef.current) return;\n if (performance.now() - lastDragEndAt.current < 80) return;\n if (openingRef.current) return;\n openItemFromElement(e.currentTarget);\n },\n [openItemFromElement]\n );\n\n useEffect(() => {\n return () => {\n document.body.classList.remove('dg-scroll-lock');\n };\n }, []);\n\n return (\n \n
\n
\n
\n {items.map((it, i) => (\n \n \n {it.alt}\n
\n
\n ))}\n
\n
\n\n
\n
\n
\n
\n\n
\n
\n
\n
\n \n
\n );\n}\n" } ], diff --git a/public/r/DomeGallery-TS-CSS.json b/public/r/DomeGallery-TS-CSS.json index f6d3f0d6..2f11d1f0 100644 --- a/public/r/DomeGallery-TS-CSS.json +++ b/public/r/DomeGallery-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "DomeGallery/DomeGallery.css", + "type": "registry:file", + "path": "DomeGallery.css", + "target": "@components/DomeGallery.css", "content": ".sphere-root {\n position: relative;\n width: 100%;\n height: 100%;\n --radius: 520px;\n --viewer-pad: 72px;\n --circ: calc(var(--radius) * 3.14);\n --rot-y: calc((360deg / var(--segments-x)) / 2);\n --rot-x: calc((360deg / var(--segments-y)) / 2);\n --item-width: calc(var(--circ) / var(--segments-x));\n --item-height: calc(var(--circ) / var(--segments-y));\n}\n\n.sphere-root * {\n box-sizing: border-box;\n}\n\n.sphere,\n.item,\n.item__image {\n transform-style: preserve-3d;\n}\n\nmain.sphere-main {\n position: absolute;\n inset: 0;\n display: grid;\n place-items: center;\n overflow: hidden;\n touch-action: none;\n user-select: none;\n -webkit-user-select: none;\n background: transparent;\n}\n\n.stage {\n width: 100%;\n height: 100%;\n display: grid;\n place-items: center;\n perspective: calc(var(--radius) * 2);\n perspective-origin: 50% 50%;\n contain: layout paint size;\n}\n\n.sphere {\n transform: translateZ(calc(var(--radius) * -1));\n will-change: transform;\n}\n\n.overlay,\n.overlay--blur {\n position: absolute;\n inset: 0;\n margin: auto;\n z-index: 3;\n pointer-events: none;\n}\n\n.overlay {\n background-image: radial-gradient(rgba(235, 235, 235, 0) 65%, var(--overlay-blur-color, #120F17) 100%);\n}\n\n.overlay--blur {\n -webkit-mask-image: radial-gradient(rgba(235, 235, 235, 0) 70%, var(--overlay-blur-color, #120F17) 90%);\n mask-image: radial-gradient(rgba(235, 235, 235, 0) 70%, var(--overlay-blur-color, #120F17) 90%);\n backdrop-filter: blur(3px);\n}\n\n.item {\n width: calc(var(--item-width) * var(--item-size-x));\n height: calc(var(--item-height) * var(--item-size-y));\n position: absolute;\n top: -999px;\n bottom: -999px;\n left: -999px;\n right: -999px;\n margin: auto;\n transform-origin: 50% 50%;\n backface-visibility: hidden;\n transition: transform 300ms;\n transform: rotateY(calc(var(--rot-y) * (var(--offset-x) + ((var(--item-size-x) - 1) / 2)) + var(--rot-y-delta, 0deg)))\n rotateX(calc(var(--rot-x) * (var(--offset-y) - ((var(--item-size-y) - 1) / 2)) + var(--rot-x-delta, 0deg)))\n translateZ(var(--radius));\n}\n\n.item__image {\n position: absolute;\n display: block;\n inset: 10px;\n border-radius: var(--tile-radius, 12px);\n background: transparent;\n overflow: hidden;\n backface-visibility: hidden;\n transition: transform 300ms;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n touch-action: manipulation;\n pointer-events: auto;\n -webkit-transform: translateZ(0);\n transform: translateZ(0);\n}\n\n.item__image:focus {\n outline: none;\n}\n\n.item__image img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n pointer-events: none;\n backface-visibility: hidden;\n filter: var(--image-filter, none);\n}\n\n.viewer {\n position: absolute;\n inset: 0;\n z-index: 20;\n pointer-events: none;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--viewer-pad);\n}\n\n.viewer .frame {\n height: 100%;\n aspect-ratio: 1;\n border-radius: var(--enlarge-radius, 32px);\n display: flex;\n}\n\n@media (max-aspect-ratio: 1/1) {\n .viewer .frame {\n height: auto;\n width: 100%;\n }\n}\n\n.viewer .scrim {\n position: absolute;\n inset: 0;\n z-index: 10;\n background: rgba(0, 0, 0, 0.4);\n pointer-events: none;\n opacity: 0;\n transition: opacity 500ms ease;\n backdrop-filter: blur(3px);\n}\n\n.sphere-root[data-enlarging='true'] .viewer .scrim {\n opacity: 1;\n pointer-events: all;\n}\n\n.viewer .enlarge {\n position: absolute;\n z-index: 30;\n border-radius: var(--enlarge-radius, 32px);\n overflow: hidden;\n transition:\n transform 500ms ease,\n opacity 500ms ease;\n transform-origin: top left;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);\n}\n\n.viewer .enlarge img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n filter: var(--image-filter, none);\n}\n\n.sphere-root .enlarge-closing img {\n filter: var(--image-filter, none);\n}\n\n.edge-fade {\n position: absolute;\n left: 0;\n right: 0;\n height: 120px;\n z-index: 5;\n pointer-events: none;\n background: linear-gradient(to bottom, transparent, var(--overlay-blur-color, #120F17));\n}\n\n.edge-fade--top {\n top: 0;\n transform: rotate(180deg);\n}\n\n.edge-fade--bottom {\n bottom: 0;\n}\n" }, { "type": "registry:component", - "path": "DomeGallery/DomeGallery.tsx", + "path": "DomeGallery.tsx", "content": "import { useEffect, useMemo, useRef, useCallback } from 'react';\nimport { useGesture } from '@use-gesture/react';\nimport './DomeGallery.css';\n\ntype ImageItem = string | { src: string; alt?: string };\n\ntype DomeGalleryProps = {\n images?: ImageItem[];\n fit?: number;\n fitBasis?: 'auto' | 'min' | 'max' | 'width' | 'height';\n minRadius?: number;\n maxRadius?: number;\n padFactor?: number;\n overlayBlurColor?: string;\n maxVerticalRotationDeg?: number;\n dragSensitivity?: number;\n enlargeTransitionMs?: number;\n segments?: number;\n dragDampening?: number;\n openedImageWidth?: string;\n openedImageHeight?: string;\n imageBorderRadius?: string;\n openedImageBorderRadius?: string;\n grayscale?: boolean;\n};\n\ntype ItemDef = {\n src: string;\n alt: string;\n x: number;\n y: number;\n sizeX: number;\n sizeY: number;\n};\n\nconst DEFAULT_IMAGES: ImageItem[] = [\n {\n src: 'https://images.unsplash.com/photo-1755331039789-7e5680e26e8f?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n alt: 'Abstract art'\n },\n {\n src: 'https://images.unsplash.com/photo-1755569309049-98410b94f66d?q=80&w=772&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n alt: 'Modern sculpture'\n },\n {\n src: 'https://images.unsplash.com/photo-1755497595318-7e5e3523854f?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n alt: 'Digital artwork'\n },\n {\n src: 'https://images.unsplash.com/photo-1755353985163-c2a0fe5ac3d8?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n alt: 'Contemporary art'\n },\n {\n src: 'https://images.unsplash.com/photo-1745965976680-d00be7dc0377?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n alt: 'Geometric pattern'\n },\n {\n src: 'https://images.unsplash.com/photo-1752588975228-21f44630bb3c?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n alt: 'Textured surface'\n },\n {\n src: 'https://pbs.twimg.com/media/Gyla7NnXMAAXSo_?format=jpg&name=large',\n alt: 'Social media image'\n }\n];\n\nconst DEFAULTS = {\n maxVerticalRotationDeg: 5,\n dragSensitivity: 20,\n enlargeTransitionMs: 300,\n segments: 35\n};\n\nconst clamp = (v: number, min: number, max: number) => Math.min(Math.max(v, min), max);\nconst normalizeAngle = (d: number) => ((d % 360) + 360) % 360;\nconst wrapAngleSigned = (deg: number) => {\n const a = (((deg + 180) % 360) + 360) % 360;\n return a - 180;\n};\nconst getDataNumber = (el: HTMLElement, name: string, fallback: number) => {\n const attr = el.dataset[name] ?? el.getAttribute(`data-${name}`);\n const n = attr == null ? NaN : parseFloat(attr);\n return Number.isFinite(n) ? n : fallback;\n};\n\nfunction buildItems(pool: ImageItem[], seg: number): ItemDef[] {\n const xCols = Array.from({ length: seg }, (_, i) => -37 + i * 2);\n const evenYs = [-4, -2, 0, 2, 4];\n const oddYs = [-3, -1, 1, 3, 5];\n\n const coords = xCols.flatMap((x, c) => {\n const ys = c % 2 === 0 ? evenYs : oddYs;\n return ys.map(y => ({ x, y, sizeX: 2, sizeY: 2 }));\n });\n\n const totalSlots = coords.length;\n if (pool.length === 0) {\n return coords.map(c => ({ ...c, src: '', alt: '' }));\n }\n if (pool.length > totalSlots) {\n console.warn(\n `[DomeGallery] Provided image count (${pool.length}) exceeds available tiles (${totalSlots}). Some images will not be shown.`\n );\n }\n\n const normalizedImages = pool.map(image => {\n if (typeof image === 'string') {\n return { src: image, alt: '' };\n }\n return { src: image.src || '', alt: image.alt || '' };\n });\n\n const usedImages = Array.from({ length: totalSlots }, (_, i) => normalizedImages[i % normalizedImages.length]);\n\n for (let i = 1; i < usedImages.length; i++) {\n if (usedImages[i].src === usedImages[i - 1].src) {\n for (let j = i + 1; j < usedImages.length; j++) {\n if (usedImages[j].src !== usedImages[i].src) {\n const tmp = usedImages[i];\n usedImages[i] = usedImages[j];\n usedImages[j] = tmp;\n break;\n }\n }\n }\n }\n\n return coords.map((c, i) => ({\n ...c,\n src: usedImages[i].src,\n alt: usedImages[i].alt\n }));\n}\n\nfunction computeItemBaseRotation(offsetX: number, offsetY: number, sizeX: number, sizeY: number, segments: number) {\n const unit = 360 / segments / 2;\n const rotateY = unit * (offsetX + (sizeX - 1) / 2);\n const rotateX = unit * (offsetY - (sizeY - 1) / 2);\n return { rotateX, rotateY };\n}\n\nexport default function DomeGallery({\n images = DEFAULT_IMAGES,\n fit = 0.5,\n fitBasis = 'auto',\n minRadius = 600,\n maxRadius = Infinity,\n padFactor = 0.25,\n overlayBlurColor = '#120F17',\n maxVerticalRotationDeg = DEFAULTS.maxVerticalRotationDeg,\n dragSensitivity = DEFAULTS.dragSensitivity,\n enlargeTransitionMs = DEFAULTS.enlargeTransitionMs,\n segments = DEFAULTS.segments,\n dragDampening = 2,\n openedImageWidth = '400px',\n openedImageHeight = '400px',\n imageBorderRadius = '30px',\n openedImageBorderRadius = '30px',\n grayscale = true\n}: DomeGalleryProps) {\n const rootRef = useRef(null);\n const mainRef = useRef(null);\n const sphereRef = useRef(null);\n const frameRef = useRef(null);\n const viewerRef = useRef(null);\n const scrimRef = useRef(null);\n const focusedElRef = useRef(null);\n const originalTilePositionRef = useRef<{\n left: number;\n top: number;\n width: number;\n height: number;\n } | null>(null);\n\n const rotationRef = useRef({ x: 0, y: 0 });\n const startRotRef = useRef({ x: 0, y: 0 });\n const startPosRef = useRef<{ x: number; y: number } | null>(null);\n const draggingRef = useRef(false);\n const movedRef = useRef(false);\n const inertiaRAF = useRef(null);\n\n const openingRef = useRef(false);\n const openStartedAtRef = useRef(0);\n const lastDragEndAt = useRef(0);\n\n const scrollLockedRef = useRef(false);\n const lockScroll = useCallback(() => {\n if (scrollLockedRef.current) return;\n scrollLockedRef.current = true;\n document.body.classList.add('dg-scroll-lock');\n }, []);\n const unlockScroll = useCallback(() => {\n if (!scrollLockedRef.current) return;\n if (rootRef.current?.getAttribute('data-enlarging') === 'true') return;\n scrollLockedRef.current = false;\n document.body.classList.remove('dg-scroll-lock');\n }, []);\n\n const items = useMemo(() => buildItems(images, segments), [images, segments]);\n\n const applyTransform = (xDeg: number, yDeg: number) => {\n const el = sphereRef.current;\n if (el) {\n el.style.transform = `translateZ(calc(var(--radius) * -1)) rotateX(${xDeg}deg) rotateY(${yDeg}deg)`;\n }\n };\n\n const lockedRadiusRef = useRef(null);\n\n useEffect(() => {\n const root = rootRef.current;\n if (!root) return;\n const ro = new ResizeObserver(entries => {\n const cr = entries[0].contentRect;\n const w = Math.max(1, cr.width),\n h = Math.max(1, cr.height);\n const minDim = Math.min(w, h),\n maxDim = Math.max(w, h),\n aspect = w / h;\n let basis: number;\n switch (fitBasis) {\n case 'min':\n basis = minDim;\n break;\n case 'max':\n basis = maxDim;\n break;\n case 'width':\n basis = w;\n break;\n case 'height':\n basis = h;\n break;\n default:\n basis = aspect >= 1.3 ? w : minDim;\n }\n let radius = basis * fit;\n const heightGuard = h * 1.35;\n radius = Math.min(radius, heightGuard);\n radius = clamp(radius, minRadius, maxRadius);\n lockedRadiusRef.current = Math.round(radius);\n\n const viewerPad = Math.max(8, Math.round(minDim * padFactor));\n root.style.setProperty('--radius', `${lockedRadiusRef.current}px`);\n root.style.setProperty('--viewer-pad', `${viewerPad}px`);\n root.style.setProperty('--overlay-blur-color', overlayBlurColor);\n root.style.setProperty('--tile-radius', imageBorderRadius);\n root.style.setProperty('--enlarge-radius', openedImageBorderRadius);\n root.style.setProperty('--image-filter', grayscale ? 'grayscale(1)' : 'none');\n applyTransform(rotationRef.current.x, rotationRef.current.y);\n\n const enlargedOverlay = viewerRef.current?.querySelector('.enlarge') as HTMLElement;\n if (enlargedOverlay && frameRef.current && mainRef.current) {\n const frameR = frameRef.current.getBoundingClientRect();\n const mainR = mainRef.current.getBoundingClientRect();\n\n const hasCustomSize = openedImageWidth && openedImageHeight;\n if (hasCustomSize) {\n const tempDiv = document.createElement('div');\n tempDiv.style.cssText = `position: absolute; width: ${openedImageWidth}; height: ${openedImageHeight}; visibility: hidden;`;\n document.body.appendChild(tempDiv);\n const tempRect = tempDiv.getBoundingClientRect();\n document.body.removeChild(tempDiv);\n\n const centeredLeft = frameR.left - mainR.left + (frameR.width - tempRect.width) / 2;\n const centeredTop = frameR.top - mainR.top + (frameR.height - tempRect.height) / 2;\n\n enlargedOverlay.style.left = `${centeredLeft}px`;\n enlargedOverlay.style.top = `${centeredTop}px`;\n } else {\n enlargedOverlay.style.left = `${frameR.left - mainR.left}px`;\n enlargedOverlay.style.top = `${frameR.top - mainR.top}px`;\n enlargedOverlay.style.width = `${frameR.width}px`;\n enlargedOverlay.style.height = `${frameR.height}px`;\n }\n }\n });\n ro.observe(root);\n return () => ro.disconnect();\n }, [\n fit,\n fitBasis,\n minRadius,\n maxRadius,\n padFactor,\n overlayBlurColor,\n grayscale,\n imageBorderRadius,\n openedImageBorderRadius,\n openedImageWidth,\n openedImageHeight\n ]);\n\n useEffect(() => {\n applyTransform(rotationRef.current.x, rotationRef.current.y);\n }, []);\n\n const stopInertia = useCallback(() => {\n if (inertiaRAF.current) {\n cancelAnimationFrame(inertiaRAF.current);\n inertiaRAF.current = null;\n }\n }, []);\n\n const startInertia = useCallback(\n (vx: number, vy: number) => {\n const MAX_V = 1.4;\n let vX = clamp(vx, -MAX_V, MAX_V) * 80;\n let vY = clamp(vy, -MAX_V, MAX_V) * 80;\n\n let frames = 0;\n const d = clamp(dragDampening ?? 0.6, 0, 1);\n const frictionMul = 0.94 + 0.055 * d;\n const stopThreshold = 0.015 - 0.01 * d;\n const maxFrames = Math.round(90 + 270 * d);\n\n const step = () => {\n vX *= frictionMul;\n vY *= frictionMul;\n if (Math.abs(vX) < stopThreshold && Math.abs(vY) < stopThreshold) {\n inertiaRAF.current = null;\n return;\n }\n if (++frames > maxFrames) {\n inertiaRAF.current = null;\n return;\n }\n const nextX = clamp(rotationRef.current.x - vY / 200, -maxVerticalRotationDeg, maxVerticalRotationDeg);\n const nextY = wrapAngleSigned(rotationRef.current.y + vX / 200);\n rotationRef.current = { x: nextX, y: nextY };\n applyTransform(nextX, nextY);\n inertiaRAF.current = requestAnimationFrame(step);\n };\n stopInertia();\n inertiaRAF.current = requestAnimationFrame(step);\n },\n [dragDampening, maxVerticalRotationDeg, stopInertia]\n );\n\n useGesture(\n {\n onDragStart: ({ event }) => {\n if (focusedElRef.current) return;\n stopInertia();\n const evt = event as PointerEvent;\n draggingRef.current = true;\n movedRef.current = false;\n startRotRef.current = { ...rotationRef.current };\n startPosRef.current = { x: evt.clientX, y: evt.clientY };\n },\n onDrag: ({ event, last, velocity = [0, 0], direction = [0, 0], movement }) => {\n if (focusedElRef.current || !draggingRef.current || !startPosRef.current) return;\n\n const evt = event as PointerEvent;\n const dxTotal = evt.clientX - startPosRef.current.x;\n const dyTotal = evt.clientY - startPosRef.current.y;\n\n if (!movedRef.current) {\n const dist2 = dxTotal * dxTotal + dyTotal * dyTotal;\n if (dist2 > 16) movedRef.current = true;\n }\n\n const nextX = clamp(\n startRotRef.current.x - dyTotal / dragSensitivity,\n -maxVerticalRotationDeg,\n maxVerticalRotationDeg\n );\n const nextY = wrapAngleSigned(startRotRef.current.y + dxTotal / dragSensitivity);\n\n if (rotationRef.current.x !== nextX || rotationRef.current.y !== nextY) {\n rotationRef.current = { x: nextX, y: nextY };\n applyTransform(nextX, nextY);\n }\n\n if (last) {\n draggingRef.current = false;\n\n let [vMagX, vMagY] = velocity;\n const [dirX, dirY] = direction;\n let vx = vMagX * dirX;\n let vy = vMagY * dirY;\n\n if (Math.abs(vx) < 0.001 && Math.abs(vy) < 0.001 && Array.isArray(movement)) {\n const [mx, my] = movement;\n vx = clamp((mx / dragSensitivity) * 0.02, -1.2, 1.2);\n vy = clamp((my / dragSensitivity) * 0.02, -1.2, 1.2);\n }\n\n if (Math.abs(vx) > 0.005 || Math.abs(vy) > 0.005) {\n startInertia(vx, vy);\n }\n\n if (movedRef.current) lastDragEndAt.current = performance.now();\n\n movedRef.current = false;\n }\n }\n },\n { target: mainRef, eventOptions: { passive: true } }\n );\n\n const openItemFromElement = (el: HTMLElement) => {\n if (openingRef.current) return;\n openingRef.current = true;\n openStartedAtRef.current = performance.now();\n lockScroll();\n\n const parent = el.parentElement as HTMLElement;\n focusedElRef.current = el;\n el.setAttribute('data-focused', 'true');\n\n const offsetX = getDataNumber(parent, 'offsetX', 0);\n const offsetY = getDataNumber(parent, 'offsetY', 0);\n const sizeX = getDataNumber(parent, 'sizeX', 2);\n const sizeY = getDataNumber(parent, 'sizeY', 2);\n\n const parentRot = computeItemBaseRotation(offsetX, offsetY, sizeX, sizeY, segments);\n const parentY = normalizeAngle(parentRot.rotateY);\n const globalY = normalizeAngle(rotationRef.current.y);\n let rotY = -(parentY + globalY) % 360;\n if (rotY < -180) rotY += 360;\n const rotX = -parentRot.rotateX - rotationRef.current.x;\n parent.style.setProperty('--rot-y-delta', `${rotY}deg`);\n parent.style.setProperty('--rot-x-delta', `${rotX}deg`);\n\n const refDiv = document.createElement('div');\n refDiv.className = 'item__image item__image--reference';\n refDiv.style.opacity = '0';\n refDiv.style.transform = `rotateX(${-parentRot.rotateX}deg) rotateY(${-parentRot.rotateY}deg)`;\n parent.appendChild(refDiv);\n\n void refDiv.offsetHeight;\n\n const tileR = refDiv.getBoundingClientRect();\n const mainR = mainRef.current?.getBoundingClientRect();\n const frameR = frameRef.current?.getBoundingClientRect();\n\n if (!mainR || !frameR || tileR.width <= 0 || tileR.height <= 0) {\n openingRef.current = false;\n focusedElRef.current = null;\n parent.removeChild(refDiv);\n unlockScroll();\n return;\n }\n\n originalTilePositionRef.current = {\n left: tileR.left,\n top: tileR.top,\n width: tileR.width,\n height: tileR.height\n };\n\n el.style.visibility = 'hidden';\n (el.style as any).zIndex = 0;\n\n const overlay = document.createElement('div');\n overlay.className = 'enlarge';\n overlay.style.position = 'absolute';\n overlay.style.left = frameR.left - mainR.left + 'px';\n overlay.style.top = frameR.top - mainR.top + 'px';\n overlay.style.width = frameR.width + 'px';\n overlay.style.height = frameR.height + 'px';\n overlay.style.opacity = '0';\n overlay.style.zIndex = '30';\n overlay.style.willChange = 'transform, opacity';\n overlay.style.transformOrigin = 'top left';\n overlay.style.transition = `transform ${enlargeTransitionMs}ms ease, opacity ${enlargeTransitionMs}ms ease`;\n\n const rawSrc = parent.dataset.src || (el.querySelector('img') as HTMLImageElement)?.src || '';\n const img = document.createElement('img');\n img.src = rawSrc;\n overlay.appendChild(img);\n viewerRef.current!.appendChild(overlay);\n\n const tx0 = tileR.left - frameR.left;\n const ty0 = tileR.top - frameR.top;\n const sx0 = tileR.width / frameR.width;\n const sy0 = tileR.height / frameR.height;\n\n const validSx0 = isFinite(sx0) && sx0 > 0 ? sx0 : 1;\n const validSy0 = isFinite(sy0) && sy0 > 0 ? sy0 : 1;\n\n overlay.style.transform = `translate(${tx0}px, ${ty0}px) scale(${validSx0}, ${validSy0})`;\n\n setTimeout(() => {\n if (!overlay.parentElement) return;\n overlay.style.opacity = '1';\n overlay.style.transform = 'translate(0px, 0px) scale(1, 1)';\n rootRef.current?.setAttribute('data-enlarging', 'true');\n }, 16);\n\n const wantsResize = openedImageWidth || openedImageHeight;\n if (wantsResize) {\n const onFirstEnd = (ev: TransitionEvent) => {\n if (ev.propertyName !== 'transform') return;\n overlay.removeEventListener('transitionend', onFirstEnd);\n const prevTransition = overlay.style.transition;\n overlay.style.transition = 'none';\n const tempWidth = openedImageWidth || `${frameR.width}px`;\n const tempHeight = openedImageHeight || `${frameR.height}px`;\n overlay.style.width = tempWidth;\n overlay.style.height = tempHeight;\n const newRect = overlay.getBoundingClientRect();\n overlay.style.width = frameR.width + 'px';\n overlay.style.height = frameR.height + 'px';\n void overlay.offsetWidth;\n overlay.style.transition = `left ${enlargeTransitionMs}ms ease, top ${enlargeTransitionMs}ms ease, width ${enlargeTransitionMs}ms ease, height ${enlargeTransitionMs}ms ease`;\n const centeredLeft = frameR.left - mainR.left + (frameR.width - newRect.width) / 2;\n const centeredTop = frameR.top - mainR.top + (frameR.height - newRect.height) / 2;\n requestAnimationFrame(() => {\n overlay.style.left = `${centeredLeft}px`;\n overlay.style.top = `${centeredTop}px`;\n overlay.style.width = tempWidth;\n overlay.style.height = tempHeight;\n });\n const cleanupSecond = () => {\n overlay.removeEventListener('transitionend', cleanupSecond);\n overlay.style.transition = prevTransition;\n };\n overlay.addEventListener('transitionend', cleanupSecond, {\n once: true\n });\n };\n overlay.addEventListener('transitionend', onFirstEnd);\n }\n };\n\n const onTileClick = useCallback(\n (e: React.MouseEvent) => {\n if (draggingRef.current) return;\n if (movedRef.current) return;\n if (performance.now() - lastDragEndAt.current < 80) return;\n if (openingRef.current) return;\n openItemFromElement(e.currentTarget);\n },\n [openItemFromElement]\n );\n\n const onTilePointerUp = useCallback(\n (e: React.PointerEvent) => {\n if (e.pointerType !== 'touch') return;\n if (draggingRef.current) return;\n if (movedRef.current) return;\n if (performance.now() - lastDragEndAt.current < 80) return;\n if (openingRef.current) return;\n openItemFromElement(e.currentTarget);\n },\n [openItemFromElement]\n );\n\n useEffect(() => {\n const scrim = scrimRef.current;\n if (!scrim) return;\n\n const close = () => {\n if (performance.now() - openStartedAtRef.current < 250) return;\n\n const el = focusedElRef.current;\n if (!el) return;\n const parent = el.parentElement as HTMLElement;\n const overlay = viewerRef.current?.querySelector('.enlarge') as HTMLElement | null;\n if (!overlay) return;\n\n const refDiv = parent.querySelector('.item__image--reference') as HTMLElement | null;\n\n const originalPos = originalTilePositionRef.current;\n if (!originalPos) {\n overlay.remove();\n if (refDiv) refDiv.remove();\n parent.style.setProperty('--rot-y-delta', `0deg`);\n parent.style.setProperty('--rot-x-delta', `0deg`);\n el.style.visibility = '';\n (el.style as any).zIndex = 0;\n focusedElRef.current = null;\n rootRef.current?.removeAttribute('data-enlarging');\n openingRef.current = false;\n unlockScroll();\n return;\n }\n\n const currentRect = overlay.getBoundingClientRect();\n const rootRect = rootRef.current!.getBoundingClientRect();\n\n const originalPosRelativeToRoot = {\n left: originalPos.left - rootRect.left,\n top: originalPos.top - rootRect.top,\n width: originalPos.width,\n height: originalPos.height\n };\n\n const overlayRelativeToRoot = {\n left: currentRect.left - rootRect.left,\n top: currentRect.top - rootRect.top,\n width: currentRect.width,\n height: currentRect.height\n };\n\n const animatingOverlay = document.createElement('div');\n animatingOverlay.className = 'enlarge-closing';\n animatingOverlay.style.cssText = `\n position: absolute;\n left: ${overlayRelativeToRoot.left}px;\n top: ${overlayRelativeToRoot.top}px;\n width: ${overlayRelativeToRoot.width}px;\n height: ${overlayRelativeToRoot.height}px;\n z-index: 9999;\n border-radius: var(--enlarge-radius, 32px);\n overflow: hidden;\n box-shadow: 0 10px 30px rgba(0,0,0,.35);\n transition: all ${enlargeTransitionMs}ms ease-out;\n pointer-events: none;\n margin: 0;\n transform: none;\n `;\n\n const originalImg = overlay.querySelector('img');\n if (originalImg) {\n const img = originalImg.cloneNode() as HTMLImageElement;\n img.style.cssText = 'width: 100%; height: 100%; object-fit: cover;';\n animatingOverlay.appendChild(img);\n }\n\n overlay.remove();\n rootRef.current!.appendChild(animatingOverlay);\n\n void animatingOverlay.getBoundingClientRect();\n\n requestAnimationFrame(() => {\n animatingOverlay.style.left = originalPosRelativeToRoot.left + 'px';\n animatingOverlay.style.top = originalPosRelativeToRoot.top + 'px';\n animatingOverlay.style.width = originalPosRelativeToRoot.width + 'px';\n animatingOverlay.style.height = originalPosRelativeToRoot.height + 'px';\n animatingOverlay.style.opacity = '0';\n });\n\n const cleanup = () => {\n animatingOverlay.remove();\n originalTilePositionRef.current = null;\n\n if (refDiv) refDiv.remove();\n parent.style.transition = 'none';\n el.style.transition = 'none';\n\n parent.style.setProperty('--rot-y-delta', `0deg`);\n parent.style.setProperty('--rot-x-delta', `0deg`);\n\n requestAnimationFrame(() => {\n el.style.visibility = '';\n el.style.opacity = '0';\n (el.style as any).zIndex = 0;\n focusedElRef.current = null;\n rootRef.current?.removeAttribute('data-enlarging');\n\n requestAnimationFrame(() => {\n parent.style.transition = '';\n el.style.transition = 'opacity 300ms ease-out';\n\n requestAnimationFrame(() => {\n el.style.opacity = '1';\n setTimeout(() => {\n el.style.transition = '';\n el.style.opacity = '';\n openingRef.current = false;\n if (!draggingRef.current && rootRef.current?.getAttribute('data-enlarging') !== 'true') {\n document.body.classList.remove('dg-scroll-lock');\n }\n }, 300);\n });\n });\n });\n };\n\n animatingOverlay.addEventListener('transitionend', cleanup, {\n once: true\n });\n };\n\n scrim.addEventListener('click', close);\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') close();\n };\n window.addEventListener('keydown', onKey);\n\n return () => {\n scrim.removeEventListener('click', close);\n window.removeEventListener('keydown', onKey);\n };\n }, [enlargeTransitionMs, unlockScroll]);\n\n useEffect(() => {\n return () => {\n document.body.classList.remove('dg-scroll-lock');\n };\n }, []);\n\n return (\n \n
\n
\n
\n {items.map((it, i) => (\n \n \n {it.alt}\n
\n
\n ))}\n
\n
\n\n
\n
\n
\n
\n\n
\n
\n
\n
\n \n
\n );\n}\n" } ], diff --git a/public/r/DotField-JS-CSS.json b/public/r/DotField-JS-CSS.json index ba1ddb4d..9326a7d3 100644 --- a/public/r/DotField-JS-CSS.json +++ b/public/r/DotField-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "DotField/DotField.css", + "type": "registry:file", + "path": "DotField.css", + "target": "@components/DotField.css", "content": ".dot-field-container {\n position: relative;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "DotField/DotField.jsx", + "path": "DotField.jsx", "content": "import { useEffect, useRef, memo } from 'react';\n\nimport './DotField.css';\n\nconst TWO_PI = Math.PI * 2;\n\nconst DotField = memo(({\n dotRadius = 1.5,\n dotSpacing = 14,\n cursorRadius = 500,\n cursorForce = 0.1,\n bulgeOnly = true,\n bulgeStrength = 67,\n glowRadius = 160,\n sparkle = false,\n waveAmplitude = 0,\n gradientFrom = 'rgba(168, 85, 247, 0.35)',\n gradientTo = 'rgba(180, 151, 207, 0.25)',\n glowColor = '#120F17',\n ...rest\n}) => {\n const canvasRef = useRef(null);\n const svgRef = useRef(null);\n const glowRef = useRef(null);\n const dotsRef = useRef([]);\n const mouseRef = useRef({ x: -9999, y: -9999, prevX: -9999, prevY: -9999, speed: 0 });\n const rafRef = useRef(null);\n const sizeRef = useRef({ w: 0, h: 0, offsetX: 0, offsetY: 0 });\n const glowOpacity = useRef(0);\n const engagement = useRef(0);\n const propsRef = useRef({});\n propsRef.current = { dotRadius, dotSpacing, cursorRadius, cursorForce, bulgeOnly, bulgeStrength, sparkle, waveAmplitude, gradientFrom, gradientTo };\n const rebuildRef = useRef(null);\n const glowIdRef = useRef(`dot-field-glow-${Math.random().toString(36).slice(2, 9)}`);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n const glowEl = glowRef.current;\n if (!canvas) return;\n const ctx = canvas.getContext('2d', { alpha: true });\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n let resizeTimer;\n\n function resize() {\n clearTimeout(resizeTimer);\n resizeTimer = setTimeout(doResize, 100);\n }\n\n function doResize() {\n const rect = canvas.parentElement.getBoundingClientRect();\n const w = rect.width;\n const h = rect.height;\n\n canvas.width = w * dpr;\n canvas.height = h * dpr;\n canvas.style.width = `${w}px`;\n canvas.style.height = `${h}px`;\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n sizeRef.current = {\n w,\n h,\n offsetX: rect.left + window.scrollX,\n offsetY: rect.top + window.scrollY,\n };\n\n buildDots(w, h);\n }\n\n function buildDots(w, h) {\n const p = propsRef.current;\n const step = p.dotRadius + p.dotSpacing;\n const cols = Math.floor(w / step);\n const rows = Math.floor(h / step);\n const padX = (w % step) / 2;\n const padY = (h % step) / 2;\n const dots = new Array(rows * cols);\n let idx = 0;\n\n for (let row = 0; row < rows; row++) {\n for (let col = 0; col < cols; col++) {\n const ax = padX + col * step + step / 2;\n const ay = padY + row * step + step / 2;\n dots[idx++] = { ax, ay, sx: ax, sy: ay, vx: 0, vy: 0, x: ax, y: ay };\n }\n }\n dotsRef.current = dots;\n }\n\n function onMouseMove(e) {\n const s = sizeRef.current;\n mouseRef.current.x = e.pageX - s.offsetX;\n mouseRef.current.y = e.pageY - s.offsetY;\n }\n\n function updateMouseSpeed() {\n const m = mouseRef.current;\n const dx = m.prevX - m.x;\n const dy = m.prevY - m.y;\n const dist = Math.sqrt(dx * dx + dy * dy);\n m.speed += (dist - m.speed) * 0.5;\n if (m.speed < 0.001) m.speed = 0;\n m.prevX = m.x;\n m.prevY = m.y;\n }\n\n const speedInterval = setInterval(updateMouseSpeed, 20);\n\n let frameCount = 0;\n\n function tick() {\n frameCount++;\n const dots = dotsRef.current;\n const m = mouseRef.current;\n const { w, h } = sizeRef.current;\n const p = propsRef.current;\n const len = dots.length;\n const t = frameCount * 0.02;\n\n const targetEngagement = Math.min(m.speed / 5, 1);\n engagement.current += (targetEngagement - engagement.current) * 0.06;\n if (engagement.current < 0.001) engagement.current = 0;\n const eng = engagement.current;\n\n glowOpacity.current += (eng - glowOpacity.current) * 0.08;\n\n if (glowEl) {\n glowEl.setAttribute('cx', m.x);\n glowEl.setAttribute('cy', m.y);\n glowEl.style.opacity = glowOpacity.current;\n }\n\n ctx.clearRect(0, 0, w, h);\n\n const grad = ctx.createLinearGradient(0, 0, w, h);\n grad.addColorStop(0, p.gradientFrom);\n grad.addColorStop(1, p.gradientTo);\n ctx.fillStyle = grad;\n\n const cr = p.cursorRadius;\n const crSq = cr * cr;\n const rad = p.dotRadius / 2;\n const isBulge = p.bulgeOnly;\n\n ctx.beginPath();\n\n for (let i = 0; i < len; i++) {\n const d = dots[i];\n const dx = m.x - d.ax;\n const dy = m.y - d.ay;\n const distSq = dx * dx + dy * dy;\n\n if (distSq < crSq && eng > 0.01) {\n const dist = Math.sqrt(distSq);\n if (isBulge) {\n const t = 1 - dist / cr;\n const push = t * t * p.bulgeStrength * eng;\n const angle = Math.atan2(dy, dx);\n d.sx += (d.ax - Math.cos(angle) * push - d.sx) * 0.15;\n d.sy += (d.ay - Math.sin(angle) * push - d.sy) * 0.15;\n } else {\n const angle = Math.atan2(dy, dx);\n const move = (500 / dist) * (m.speed * p.cursorForce);\n d.vx += Math.cos(angle) * -move;\n d.vy += Math.sin(angle) * -move;\n }\n } else if (isBulge) {\n d.sx += (d.ax - d.sx) * 0.1;\n d.sy += (d.ay - d.sy) * 0.1;\n }\n\n if (!isBulge) {\n d.vx *= 0.9;\n d.vy *= 0.9;\n d.x = d.ax + d.vx;\n d.y = d.ay + d.vy;\n d.sx += (d.x - d.sx) * 0.1;\n d.sy += (d.y - d.sy) * 0.1;\n }\n\n let drawX = d.sx;\n let drawY = d.sy;\n if (p.waveAmplitude > 0) {\n drawY += Math.sin(d.ax * 0.03 + t) * p.waveAmplitude;\n drawX += Math.cos(d.ay * 0.03 + t * 0.7) * p.waveAmplitude * 0.5;\n }\n\n if (p.sparkle) {\n const hash = ((i * 2654435761) ^ (frameCount >> 3)) >>> 0;\n if ((hash % 100) < 3) {\n ctx.moveTo(drawX + rad * 1.8, drawY);\n ctx.arc(drawX, drawY, rad * 1.8, 0, TWO_PI);\n } else {\n ctx.moveTo(drawX + rad, drawY);\n ctx.arc(drawX, drawY, rad, 0, TWO_PI);\n }\n } else {\n ctx.moveTo(drawX + rad, drawY);\n ctx.arc(drawX, drawY, rad, 0, TWO_PI);\n }\n }\n\n ctx.fill();\n\n rafRef.current = requestAnimationFrame(tick);\n }\n\n doResize();\n window.addEventListener('resize', resize);\n window.addEventListener('mousemove', onMouseMove, { passive: true });\n rafRef.current = requestAnimationFrame(tick);\n\n rebuildRef.current = () => {\n const { w, h } = sizeRef.current;\n if (w > 0 && h > 0) buildDots(w, h);\n };\n\n return () => {\n cancelAnimationFrame(rafRef.current);\n clearInterval(speedInterval);\n clearTimeout(resizeTimer);\n window.removeEventListener('resize', resize);\n window.removeEventListener('mousemove', onMouseMove);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n rebuildRef.current?.();\n }, [dotRadius, dotSpacing]);\n\n return (\n
\n \n \n \n \n \n \n \n \n \n \n
\n );\n});\n\nDotField.displayName = 'DotField';\n\nexport default DotField;\n" } ], diff --git a/public/r/DotField-TS-CSS.json b/public/r/DotField-TS-CSS.json index f0cf07f8..d7a06f80 100644 --- a/public/r/DotField-TS-CSS.json +++ b/public/r/DotField-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "DotField/DotField.css", + "type": "registry:file", + "path": "DotField.css", + "target": "@components/DotField.css", "content": ".dot-field-container {\n position: relative;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "DotField/DotField.tsx", + "path": "DotField.tsx", "content": "import { useEffect, useRef, memo } from 'react';\n\nimport './DotField.css';\n\nconst TWO_PI = Math.PI * 2;\n\ninterface Dot {\n ax: number;\n ay: number;\n sx: number;\n sy: number;\n vx: number;\n vy: number;\n x: number;\n y: number;\n}\n\ninterface DotFieldProps {\n dotRadius?: number;\n dotSpacing?: number;\n cursorRadius?: number;\n cursorForce?: number;\n bulgeOnly?: boolean;\n bulgeStrength?: number;\n glowRadius?: number;\n sparkle?: boolean;\n waveAmplitude?: number;\n gradientFrom?: string;\n gradientTo?: string;\n glowColor?: string;\n [key: string]: unknown;\n}\n\nconst DotField = memo(({\n dotRadius = 1.5,\n dotSpacing = 14,\n cursorRadius = 500,\n cursorForce = 0.1,\n bulgeOnly = true,\n bulgeStrength = 67,\n glowRadius = 160,\n sparkle = false,\n waveAmplitude = 0,\n gradientFrom = 'rgba(168, 85, 247, 0.35)',\n gradientTo = 'rgba(180, 151, 207, 0.25)',\n glowColor = '#120F17',\n ...rest\n}: DotFieldProps) => {\n const canvasRef = useRef(null);\n const svgRef = useRef(null);\n const glowRef = useRef(null);\n const dotsRef = useRef([]);\n const mouseRef = useRef({ x: -9999, y: -9999, prevX: -9999, prevY: -9999, speed: 0 });\n const rafRef = useRef(null);\n const sizeRef = useRef({ w: 0, h: 0, offsetX: 0, offsetY: 0 });\n const glowOpacity = useRef(0);\n const engagement = useRef(0);\n const propsRef = useRef>({});\n propsRef.current = { dotRadius, dotSpacing, cursorRadius, cursorForce, bulgeOnly, bulgeStrength, sparkle, waveAmplitude, gradientFrom, gradientTo };\n const rebuildRef = useRef<(() => void) | null>(null);\n const glowIdRef = useRef(`dot-field-glow-${Math.random().toString(36).slice(2, 9)}`);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n const glowEl = glowRef.current;\n if (!canvas) return;\n const ctx = canvas.getContext('2d', { alpha: true });\n if (!ctx) return;\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n let resizeTimer: ReturnType;\n\n function resize() {\n clearTimeout(resizeTimer);\n resizeTimer = setTimeout(doResize, 100);\n }\n\n function doResize() {\n const rect = canvas!.parentElement!.getBoundingClientRect();\n const w = rect.width;\n const h = rect.height;\n\n canvas!.width = w * dpr;\n canvas!.height = h * dpr;\n canvas!.style.width = `${w}px`;\n canvas!.style.height = `${h}px`;\n ctx!.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n sizeRef.current = {\n w,\n h,\n offsetX: rect.left + window.scrollX,\n offsetY: rect.top + window.scrollY,\n };\n\n buildDots(w, h);\n }\n\n function buildDots(w: number, h: number) {\n const p = propsRef.current;\n const step = (p.dotRadius as number) + (p.dotSpacing as number);\n const cols = Math.floor(w / step);\n const rows = Math.floor(h / step);\n const padX = (w % step) / 2;\n const padY = (h % step) / 2;\n const dots: Dot[] = new Array(rows * cols);\n let idx = 0;\n\n for (let row = 0; row < rows; row++) {\n for (let col = 0; col < cols; col++) {\n const ax = padX + col * step + step / 2;\n const ay = padY + row * step + step / 2;\n dots[idx++] = { ax, ay, sx: ax, sy: ay, vx: 0, vy: 0, x: ax, y: ay };\n }\n }\n dotsRef.current = dots;\n }\n\n function onMouseMove(e: MouseEvent) {\n const s = sizeRef.current;\n mouseRef.current.x = e.pageX - s.offsetX;\n mouseRef.current.y = e.pageY - s.offsetY;\n }\n\n function updateMouseSpeed() {\n const m = mouseRef.current;\n const dx = m.prevX - m.x;\n const dy = m.prevY - m.y;\n const dist = Math.sqrt(dx * dx + dy * dy);\n m.speed += (dist - m.speed) * 0.5;\n if (m.speed < 0.001) m.speed = 0;\n m.prevX = m.x;\n m.prevY = m.y;\n }\n\n const speedInterval = setInterval(updateMouseSpeed, 20);\n\n let frameCount = 0;\n\n function tick() {\n frameCount++;\n const dots = dotsRef.current;\n const m = mouseRef.current;\n const { w, h } = sizeRef.current;\n const p = propsRef.current;\n const len = dots.length;\n const t = frameCount * 0.02;\n\n const targetEngagement = Math.min(m.speed / 5, 1);\n engagement.current += (targetEngagement - engagement.current) * 0.06;\n if (engagement.current < 0.001) engagement.current = 0;\n const eng = engagement.current;\n\n glowOpacity.current += (eng - glowOpacity.current) * 0.08;\n\n if (glowEl) {\n glowEl.setAttribute('cx', String(m.x));\n glowEl.setAttribute('cy', String(m.y));\n glowEl.style.opacity = String(glowOpacity.current);\n }\n\n ctx!.clearRect(0, 0, w, h);\n\n const grad = ctx!.createLinearGradient(0, 0, w, h);\n grad.addColorStop(0, p.gradientFrom as string);\n grad.addColorStop(1, p.gradientTo as string);\n ctx!.fillStyle = grad;\n\n const cr = p.cursorRadius as number;\n const crSq = cr * cr;\n const rad = (p.dotRadius as number) / 2;\n const isBulge = p.bulgeOnly as boolean;\n\n ctx!.beginPath();\n\n for (let i = 0; i < len; i++) {\n const d = dots[i];\n const dx = m.x - d.ax;\n const dy = m.y - d.ay;\n const distSq = dx * dx + dy * dy;\n\n if (distSq < crSq && eng > 0.01) {\n const dist = Math.sqrt(distSq);\n if (isBulge) {\n const t = 1 - dist / cr;\n const push = t * t * (p.bulgeStrength as number) * eng;\n const angle = Math.atan2(dy, dx);\n d.sx += (d.ax - Math.cos(angle) * push - d.sx) * 0.15;\n d.sy += (d.ay - Math.sin(angle) * push - d.sy) * 0.15;\n } else {\n const angle = Math.atan2(dy, dx);\n const move = (500 / dist) * (m.speed * (p.cursorForce as number));\n d.vx += Math.cos(angle) * -move;\n d.vy += Math.sin(angle) * -move;\n }\n } else if (isBulge) {\n d.sx += (d.ax - d.sx) * 0.1;\n d.sy += (d.ay - d.sy) * 0.1;\n }\n\n if (!isBulge) {\n d.vx *= 0.9;\n d.vy *= 0.9;\n d.x = d.ax + d.vx;\n d.y = d.ay + d.vy;\n d.sx += (d.x - d.sx) * 0.1;\n d.sy += (d.y - d.sy) * 0.1;\n }\n\n let drawX = d.sx;\n let drawY = d.sy;\n if ((p.waveAmplitude as number) > 0) {\n drawY += Math.sin(d.ax * 0.03 + t) * (p.waveAmplitude as number);\n drawX += Math.cos(d.ay * 0.03 + t * 0.7) * (p.waveAmplitude as number) * 0.5;\n }\n\n if (p.sparkle) {\n const hash = ((i * 2654435761) ^ (frameCount >> 3)) >>> 0;\n if ((hash % 100) < 3) {\n ctx!.moveTo(drawX + rad * 1.8, drawY);\n ctx!.arc(drawX, drawY, rad * 1.8, 0, TWO_PI);\n } else {\n ctx!.moveTo(drawX + rad, drawY);\n ctx!.arc(drawX, drawY, rad, 0, TWO_PI);\n }\n } else {\n ctx!.moveTo(drawX + rad, drawY);\n ctx!.arc(drawX, drawY, rad, 0, TWO_PI);\n }\n }\n\n ctx!.fill();\n\n rafRef.current = requestAnimationFrame(tick);\n }\n\n doResize();\n window.addEventListener('resize', resize);\n window.addEventListener('mousemove', onMouseMove, { passive: true });\n rafRef.current = requestAnimationFrame(tick);\n\n rebuildRef.current = () => {\n const { w, h } = sizeRef.current;\n if (w > 0 && h > 0) buildDots(w, h);\n };\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n clearInterval(speedInterval);\n clearTimeout(resizeTimer);\n window.removeEventListener('resize', resize);\n window.removeEventListener('mousemove', onMouseMove);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n rebuildRef.current?.();\n }, [dotRadius, dotSpacing]);\n\n return (\n
\n \n \n \n \n \n \n \n \n \n \n
\n );\n});\n\nDotField.displayName = 'DotField';\n\nexport default DotField;\n" } ], diff --git a/public/r/DotGrid-JS-CSS.json b/public/r/DotGrid-JS-CSS.json index 72a223a2..9738aa4c 100644 --- a/public/r/DotGrid-JS-CSS.json +++ b/public/r/DotGrid-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "DotGrid/DotGrid.css", + "type": "registry:file", + "path": "DotGrid.css", + "target": "@components/DotGrid.css", "content": ".dot-grid {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n width: 100%;\n position: relative;\n}\n\n.dot-grid__wrap {\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n.dot-grid__canvas {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n}\n" }, { "type": "registry:component", - "path": "DotGrid/DotGrid.jsx", + "path": "DotGrid.jsx", "content": "'use client';\nimport { useRef, useEffect, useCallback, useMemo } from 'react';\nimport { gsap } from 'gsap';\nimport { InertiaPlugin } from 'gsap/InertiaPlugin';\n\nimport './DotGrid.css';\n\ngsap.registerPlugin(InertiaPlugin);\n\nconst throttle = (func, limit) => {\n let lastCall = 0;\n return function (...args) {\n const now = performance.now();\n if (now - lastCall >= limit) {\n lastCall = now;\n func.apply(this, args);\n }\n };\n};\n\nfunction hexToRgb(hex) {\n const m = hex.match(/^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i);\n if (!m) return { r: 0, g: 0, b: 0 };\n return {\n r: parseInt(m[1], 16),\n g: parseInt(m[2], 16),\n b: parseInt(m[3], 16)\n };\n}\n\nconst DotGrid = ({\n dotSize = 16,\n gap = 32,\n baseColor = '#5227FF',\n activeColor = '#5227FF',\n proximity = 150,\n speedTrigger = 100,\n shockRadius = 250,\n shockStrength = 5,\n maxSpeed = 5000,\n resistance = 750,\n returnDuration = 1.5,\n className = '',\n style\n}) => {\n const wrapperRef = useRef(null);\n const canvasRef = useRef(null);\n const dotsRef = useRef([]);\n const pointerRef = useRef({\n x: 0,\n y: 0,\n vx: 0,\n vy: 0,\n speed: 0,\n lastTime: 0,\n lastX: 0,\n lastY: 0\n });\n\n const baseRgb = useMemo(() => hexToRgb(baseColor), [baseColor]);\n const activeRgb = useMemo(() => hexToRgb(activeColor), [activeColor]);\n\n const circlePath = useMemo(() => {\n if (typeof window === 'undefined' || !window.Path2D) return null;\n\n const p = new window.Path2D();\n p.arc(0, 0, dotSize / 2, 0, Math.PI * 2);\n return p;\n }, [dotSize]);\n\n const buildGrid = useCallback(() => {\n const wrap = wrapperRef.current;\n const canvas = canvasRef.current;\n if (!wrap || !canvas) return;\n\n const { width, height } = wrap.getBoundingClientRect();\n const dpr = window.devicePixelRatio || 1;\n\n canvas.width = width * dpr;\n canvas.height = height * dpr;\n canvas.style.width = `${width}px`;\n canvas.style.height = `${height}px`;\n const ctx = canvas.getContext('2d');\n if (ctx) ctx.scale(dpr, dpr);\n\n const cols = Math.floor((width + gap) / (dotSize + gap));\n const rows = Math.floor((height + gap) / (dotSize + gap));\n const cell = dotSize + gap;\n\n const gridW = cell * cols - gap;\n const gridH = cell * rows - gap;\n\n const extraX = width - gridW;\n const extraY = height - gridH;\n\n const startX = extraX / 2 + dotSize / 2;\n const startY = extraY / 2 + dotSize / 2;\n\n const dots = [];\n for (let y = 0; y < rows; y++) {\n for (let x = 0; x < cols; x++) {\n const cx = startX + x * cell;\n const cy = startY + y * cell;\n dots.push({ cx, cy, xOffset: 0, yOffset: 0, _inertiaApplied: false });\n }\n }\n dotsRef.current = dots;\n }, [dotSize, gap]);\n\n useEffect(() => {\n if (!circlePath) return;\n\n let rafId;\n const proxSq = proximity * proximity;\n\n const draw = () => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n const { x: px, y: py } = pointerRef.current;\n\n for (const dot of dotsRef.current) {\n const ox = dot.cx + dot.xOffset;\n const oy = dot.cy + dot.yOffset;\n const dx = dot.cx - px;\n const dy = dot.cy - py;\n const dsq = dx * dx + dy * dy;\n\n let style = baseColor;\n if (dsq <= proxSq) {\n const dist = Math.sqrt(dsq);\n const t = 1 - dist / proximity;\n const r = Math.round(baseRgb.r + (activeRgb.r - baseRgb.r) * t);\n const g = Math.round(baseRgb.g + (activeRgb.g - baseRgb.g) * t);\n const b = Math.round(baseRgb.b + (activeRgb.b - baseRgb.b) * t);\n style = `rgb(${r},${g},${b})`;\n }\n\n ctx.save();\n ctx.translate(ox, oy);\n ctx.fillStyle = style;\n ctx.fill(circlePath);\n ctx.restore();\n }\n\n rafId = requestAnimationFrame(draw);\n };\n\n draw();\n return () => cancelAnimationFrame(rafId);\n }, [proximity, baseColor, activeRgb, baseRgb, circlePath]);\n\n useEffect(() => {\n buildGrid();\n let ro = null;\n if ('ResizeObserver' in window) {\n ro = new ResizeObserver(buildGrid);\n wrapperRef.current && ro.observe(wrapperRef.current);\n } else {\n window.addEventListener('resize', buildGrid);\n }\n return () => {\n if (ro) ro.disconnect();\n else window.removeEventListener('resize', buildGrid);\n };\n }, [buildGrid]);\n\n useEffect(() => {\n const onMove = e => {\n const now = performance.now();\n const pr = pointerRef.current;\n const dt = pr.lastTime ? now - pr.lastTime : 16;\n const dx = e.clientX - pr.lastX;\n const dy = e.clientY - pr.lastY;\n let vx = (dx / dt) * 1000;\n let vy = (dy / dt) * 1000;\n let speed = Math.hypot(vx, vy);\n if (speed > maxSpeed) {\n const scale = maxSpeed / speed;\n vx *= scale;\n vy *= scale;\n speed = maxSpeed;\n }\n pr.lastTime = now;\n pr.lastX = e.clientX;\n pr.lastY = e.clientY;\n pr.vx = vx;\n pr.vy = vy;\n pr.speed = speed;\n\n const rect = canvasRef.current.getBoundingClientRect();\n pr.x = e.clientX - rect.left;\n pr.y = e.clientY - rect.top;\n\n for (const dot of dotsRef.current) {\n const dist = Math.hypot(dot.cx - pr.x, dot.cy - pr.y);\n if (speed > speedTrigger && dist < proximity && !dot._inertiaApplied) {\n dot._inertiaApplied = true;\n gsap.killTweensOf(dot);\n const pushX = dot.cx - pr.x + vx * 0.005;\n const pushY = dot.cy - pr.y + vy * 0.005;\n gsap.to(dot, {\n inertia: { xOffset: pushX, yOffset: pushY, resistance },\n onComplete: () => {\n gsap.to(dot, {\n xOffset: 0,\n yOffset: 0,\n duration: returnDuration,\n ease: 'elastic.out(1,0.75)'\n });\n dot._inertiaApplied = false;\n }\n });\n }\n }\n };\n\n const onClick = e => {\n const rect = canvasRef.current.getBoundingClientRect();\n const cx = e.clientX - rect.left;\n const cy = e.clientY - rect.top;\n for (const dot of dotsRef.current) {\n const dist = Math.hypot(dot.cx - cx, dot.cy - cy);\n if (dist < shockRadius && !dot._inertiaApplied) {\n dot._inertiaApplied = true;\n gsap.killTweensOf(dot);\n const falloff = Math.max(0, 1 - dist / shockRadius);\n const pushX = (dot.cx - cx) * shockStrength * falloff;\n const pushY = (dot.cy - cy) * shockStrength * falloff;\n gsap.to(dot, {\n inertia: { xOffset: pushX, yOffset: pushY, resistance },\n onComplete: () => {\n gsap.to(dot, {\n xOffset: 0,\n yOffset: 0,\n duration: returnDuration,\n ease: 'elastic.out(1,0.75)'\n });\n dot._inertiaApplied = false;\n }\n });\n }\n }\n };\n\n const throttledMove = throttle(onMove, 50);\n window.addEventListener('mousemove', throttledMove, { passive: true });\n window.addEventListener('click', onClick);\n\n return () => {\n window.removeEventListener('mousemove', throttledMove);\n window.removeEventListener('click', onClick);\n };\n }, [maxSpeed, speedTrigger, proximity, resistance, returnDuration, shockRadius, shockStrength]);\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nexport default DotGrid;\n" } ], diff --git a/public/r/DotGrid-TS-CSS.json b/public/r/DotGrid-TS-CSS.json index 80dbdbaa..ec9106ce 100644 --- a/public/r/DotGrid-TS-CSS.json +++ b/public/r/DotGrid-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "DotGrid/DotGrid.css", + "type": "registry:file", + "path": "DotGrid.css", + "target": "@components/DotGrid.css", "content": ".dot-grid {\n padding: 1em;\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n width: 100%;\n position: relative;\n}\n\n.dot-grid__wrap {\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n.dot-grid__canvas {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n}\n" }, { "type": "registry:component", - "path": "DotGrid/DotGrid.tsx", + "path": "DotGrid.tsx", "content": "'use client';\nimport React, { useRef, useEffect, useCallback, useMemo } from 'react';\nimport { gsap } from 'gsap';\nimport { InertiaPlugin } from 'gsap/InertiaPlugin';\n\nimport './DotGrid.css';\n\ngsap.registerPlugin(InertiaPlugin);\n\nconst throttle = (func: (...args: any[]) => void, limit: number) => {\n let lastCall = 0;\n return function (this: any, ...args: any[]) {\n const now = performance.now();\n if (now - lastCall >= limit) {\n lastCall = now;\n func.apply(this, args);\n }\n };\n};\n\ninterface Dot {\n cx: number;\n cy: number;\n xOffset: number;\n yOffset: number;\n _inertiaApplied: boolean;\n}\n\nexport interface DotGridProps {\n dotSize?: number;\n gap?: number;\n baseColor?: string;\n activeColor?: string;\n proximity?: number;\n speedTrigger?: number;\n shockRadius?: number;\n shockStrength?: number;\n maxSpeed?: number;\n resistance?: number;\n returnDuration?: number;\n className?: string;\n style?: React.CSSProperties;\n}\n\nfunction hexToRgb(hex: string) {\n const m = hex.match(/^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i);\n if (!m) return { r: 0, g: 0, b: 0 };\n return {\n r: parseInt(m[1], 16),\n g: parseInt(m[2], 16),\n b: parseInt(m[3], 16)\n };\n}\n\nconst DotGrid: React.FC = ({\n dotSize = 16,\n gap = 32,\n baseColor = '#5227FF',\n activeColor = '#5227FF',\n proximity = 150,\n speedTrigger = 100,\n shockRadius = 250,\n shockStrength = 5,\n maxSpeed = 5000,\n resistance = 750,\n returnDuration = 1.5,\n className = '',\n style\n}) => {\n const wrapperRef = useRef(null);\n const canvasRef = useRef(null);\n const dotsRef = useRef([]);\n const pointerRef = useRef({\n x: 0,\n y: 0,\n vx: 0,\n vy: 0,\n speed: 0,\n lastTime: 0,\n lastX: 0,\n lastY: 0\n });\n\n const baseRgb = useMemo(() => hexToRgb(baseColor), [baseColor]);\n const activeRgb = useMemo(() => hexToRgb(activeColor), [activeColor]);\n\n const circlePath = useMemo(() => {\n if (typeof window === 'undefined' || !window.Path2D) return null;\n\n const p = new Path2D();\n p.arc(0, 0, dotSize / 2, 0, Math.PI * 2);\n return p;\n }, [dotSize]);\n\n const buildGrid = useCallback(() => {\n const wrap = wrapperRef.current;\n const canvas = canvasRef.current;\n if (!wrap || !canvas) return;\n\n const { width, height } = wrap.getBoundingClientRect();\n const dpr = window.devicePixelRatio || 1;\n\n canvas.width = width * dpr;\n canvas.height = height * dpr;\n canvas.style.width = `${width}px`;\n canvas.style.height = `${height}px`;\n const ctx = canvas.getContext('2d');\n if (ctx) ctx.scale(dpr, dpr);\n\n const cols = Math.floor((width + gap) / (dotSize + gap));\n const rows = Math.floor((height + gap) / (dotSize + gap));\n const cell = dotSize + gap;\n\n const gridW = cell * cols - gap;\n const gridH = cell * rows - gap;\n\n const extraX = width - gridW;\n const extraY = height - gridH;\n\n const startX = extraX / 2 + dotSize / 2;\n const startY = extraY / 2 + dotSize / 2;\n\n const dots: Dot[] = [];\n for (let y = 0; y < rows; y++) {\n for (let x = 0; x < cols; x++) {\n const cx = startX + x * cell;\n const cy = startY + y * cell;\n dots.push({ cx, cy, xOffset: 0, yOffset: 0, _inertiaApplied: false });\n }\n }\n dotsRef.current = dots;\n }, [dotSize, gap]);\n\n useEffect(() => {\n if (!circlePath) return;\n\n let rafId: number;\n const proxSq = proximity * proximity;\n\n const draw = () => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n const { x: px, y: py } = pointerRef.current;\n\n for (const dot of dotsRef.current) {\n const ox = dot.cx + dot.xOffset;\n const oy = dot.cy + dot.yOffset;\n const dx = dot.cx - px;\n const dy = dot.cy - py;\n const dsq = dx * dx + dy * dy;\n\n let style = baseColor;\n if (dsq <= proxSq) {\n const dist = Math.sqrt(dsq);\n const t = 1 - dist / proximity;\n const r = Math.round(baseRgb.r + (activeRgb.r - baseRgb.r) * t);\n const g = Math.round(baseRgb.g + (activeRgb.g - baseRgb.g) * t);\n const b = Math.round(baseRgb.b + (activeRgb.b - baseRgb.b) * t);\n style = `rgb(${r},${g},${b})`;\n }\n\n ctx.save();\n ctx.translate(ox, oy);\n ctx.fillStyle = style;\n ctx.fill(circlePath);\n ctx.restore();\n }\n\n rafId = requestAnimationFrame(draw);\n };\n\n draw();\n return () => cancelAnimationFrame(rafId);\n }, [proximity, baseColor, activeRgb, baseRgb, circlePath]);\n\n useEffect(() => {\n buildGrid();\n let ro: ResizeObserver | null = null;\n if ('ResizeObserver' in window) {\n ro = new ResizeObserver(buildGrid);\n wrapperRef.current && ro.observe(wrapperRef.current);\n } else {\n (window as Window).addEventListener('resize', buildGrid);\n }\n return () => {\n if (ro) ro.disconnect();\n else window.removeEventListener('resize', buildGrid);\n };\n }, [buildGrid]);\n\n useEffect(() => {\n const onMove = (e: MouseEvent) => {\n const now = performance.now();\n const pr = pointerRef.current;\n const dt = pr.lastTime ? now - pr.lastTime : 16;\n const dx = e.clientX - pr.lastX;\n const dy = e.clientY - pr.lastY;\n let vx = (dx / dt) * 1000;\n let vy = (dy / dt) * 1000;\n let speed = Math.hypot(vx, vy);\n if (speed > maxSpeed) {\n const scale = maxSpeed / speed;\n vx *= scale;\n vy *= scale;\n speed = maxSpeed;\n }\n pr.lastTime = now;\n pr.lastX = e.clientX;\n pr.lastY = e.clientY;\n pr.vx = vx;\n pr.vy = vy;\n pr.speed = speed;\n\n const rect = canvasRef.current!.getBoundingClientRect();\n pr.x = e.clientX - rect.left;\n pr.y = e.clientY - rect.top;\n\n for (const dot of dotsRef.current) {\n const dist = Math.hypot(dot.cx - pr.x, dot.cy - pr.y);\n if (speed > speedTrigger && dist < proximity && !dot._inertiaApplied) {\n dot._inertiaApplied = true;\n gsap.killTweensOf(dot);\n const pushX = dot.cx - pr.x + vx * 0.005;\n const pushY = dot.cy - pr.y + vy * 0.005;\n gsap.to(dot, {\n inertia: { xOffset: pushX, yOffset: pushY, resistance },\n onComplete: () => {\n gsap.to(dot, {\n xOffset: 0,\n yOffset: 0,\n duration: returnDuration,\n ease: 'elastic.out(1,0.75)'\n });\n dot._inertiaApplied = false;\n }\n });\n }\n }\n };\n\n const onClick = (e: MouseEvent) => {\n const rect = canvasRef.current!.getBoundingClientRect();\n const cx = e.clientX - rect.left;\n const cy = e.clientY - rect.top;\n for (const dot of dotsRef.current) {\n const dist = Math.hypot(dot.cx - cx, dot.cy - cy);\n if (dist < shockRadius && !dot._inertiaApplied) {\n dot._inertiaApplied = true;\n gsap.killTweensOf(dot);\n const falloff = Math.max(0, 1 - dist / shockRadius);\n const pushX = (dot.cx - cx) * shockStrength * falloff;\n const pushY = (dot.cy - cy) * shockStrength * falloff;\n gsap.to(dot, {\n inertia: { xOffset: pushX, yOffset: pushY, resistance },\n onComplete: () => {\n gsap.to(dot, {\n xOffset: 0,\n yOffset: 0,\n duration: returnDuration,\n ease: 'elastic.out(1,0.75)'\n });\n dot._inertiaApplied = false;\n }\n });\n }\n }\n };\n\n const throttledMove = throttle(onMove, 50);\n window.addEventListener('mousemove', throttledMove, { passive: true });\n window.addEventListener('click', onClick);\n\n return () => {\n window.removeEventListener('mousemove', throttledMove);\n window.removeEventListener('click', onClick);\n };\n }, [maxSpeed, speedTrigger, proximity, resistance, returnDuration, shockRadius, shockStrength]);\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nexport default DotGrid;\n" } ], diff --git a/public/r/DriftWall-JS-CSS.json b/public/r/DriftWall-JS-CSS.json index 6324d767..6affabcf 100644 --- a/public/r/DriftWall-JS-CSS.json +++ b/public/r/DriftWall-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "DriftWall/DriftWall.css", + "type": "registry:file", + "path": "DriftWall.css", + "target": "@components/DriftWall.css", "content": ".drift-wall {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n perspective: var(--dw-perspective, 1200px);\n perspective-origin: 50% 50%;\n --dw-tile-w: 200px;\n --dw-tile-h: 132px;\n --dw-gap: 18px;\n --dw-radius: 14px;\n --dw-lift: 64px;\n --dw-dim: 0.55;\n --dw-gray: 0;\n --dw-overlay: #060010;\n --dw-edge: 40%;\n -webkit-mask-image:\n radial-gradient(ellipse 78% 82% at 50% 46%, #000 var(--dw-edge), transparent 100%),\n linear-gradient(to top, #000 var(--dw-edge), transparent 100%);\n -webkit-mask-composite: source-in;\n mask-image:\n radial-gradient(ellipse 78% 82% at 50% 46%, #000 var(--dw-edge), transparent 100%),\n linear-gradient(to top, #000 var(--dw-edge), transparent 100%);\n mask-composite: intersect;\n}\n\n.drift-wall__plane {\n position: absolute;\n top: 50%;\n left: 50%;\n display: flex;\n flex-direction: row;\n transform-style: preserve-3d;\n cursor: pointer;\n transform-origin: 50% 50%;\n will-change: transform;\n}\n\n.drift-wall__col {\n position: relative;\n width: calc(var(--dw-tile-w) + var(--dw-gap));\n transform-style: preserve-3d;\n}\n\n.drift-wall__track {\n display: flex;\n flex-direction: column;\n will-change: transform;\n transform-style: preserve-3d;\n}\n\n.drift-wall__tile {\n position: relative;\n display: block;\n width: 100%;\n height: calc(var(--dw-tile-h) + var(--dw-gap));\n flex: 0 0 auto;\n outline: none;\n transform-style: preserve-3d;\n}\n\n.drift-wall__inner {\n position: absolute;\n inset: calc(var(--dw-gap) / 2);\n display: block;\n border-radius: var(--dw-radius);\n overflow: hidden;\n background: #0b0b12;\n opacity: var(--dw-dim);\n transform: translateZ(0);\n pointer-events: none;\n transition:\n transform 0.42s cubic-bezier(0.22, 1, 0.36, 1),\n opacity 0.42s cubic-bezier(0.22, 1, 0.36, 1),\n box-shadow 0.42s cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n.drift-wall__tile img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n filter: grayscale(var(--dw-gray)) saturate(0.92);\n transition: filter 0.42s cubic-bezier(0.22, 1, 0.36, 1);\n user-select: none;\n -webkit-user-drag: none;\n}\n\n.drift-wall__overlay {\n position: absolute;\n inset: 0;\n background: var(--dw-overlay);\n opacity: 0.42;\n pointer-events: none;\n transition: opacity 0.42s cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n.drift-wall__tile.is-active .drift-wall__inner,\n.drift-wall__tile:focus-visible .drift-wall__inner {\n opacity: 1;\n transform: translateZ(var(--dw-lift));\n box-shadow: 0 24px 60px -18px rgba(0, 0, 0, 0.7);\n}\n\n.drift-wall__tile.is-active img,\n.drift-wall__tile:focus-visible img {\n filter: grayscale(0) saturate(1.05);\n}\n\n.drift-wall__tile.is-active .drift-wall__overlay,\n.drift-wall__tile:focus-visible .drift-wall__overlay {\n opacity: 0;\n}\n\n.drift-wall__tile:focus-visible .drift-wall__inner {\n box-shadow:\n 0 24px 60px -18px rgba(0, 0, 0, 0.7),\n 0 0 0 2px rgba(255, 255, 255, 0.9);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .drift-wall__plane,\n .drift-wall__track {\n will-change: auto;\n }\n}\n" }, { "type": "registry:component", - "path": "DriftWall/DriftWall.jsx", + "path": "DriftWall.jsx", "content": "import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport './DriftWall.css';\n\nconst DEFAULT_ITEMS = Array.from({ length: 15 }, (_, i) => {\n const ids = [1015, 1025, 1039, 1043, 1044, 1050, 1062, 1069, 1074, 1080, 1084, 106, 110, 133, 164];\n return {\n image: `https://picsum.photos/id/${ids[i % ids.length]}/600/400`,\n title: `Tile ${i + 1}`,\n href: undefined\n };\n});\n\nconst prefersReducedMotion = () =>\n typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nconst columnFactor = (index, variance) => {\n const pseudo = ((index * 0.6180339887 + 0.35) % 1) * 2 - 1;\n return 1 + variance * pseudo;\n};\n\nconst DriftWall = ({\n items = DEFAULT_ITEMS,\n columns = 5,\n tileWidth = 200,\n tileHeight = 132,\n gap = 18,\n radius = 14,\n tilt = 16,\n turn = -14,\n roll = 0,\n perspective = 1200,\n depth = 120,\n speed = 42,\n direction = 'up',\n variance = 0.45,\n parallax = 0.6,\n pauseOnHover = false,\n lift = 64,\n fade = 0.6,\n dim = 0.55,\n grayscale = false,\n overlayColor = '#060010',\n className = '',\n style\n}) => {\n const containerRef = useRef(null);\n const planeRef = useRef(null);\n const trackRefs = useRef([]);\n const rafRef = useRef(null);\n\n const offsetsRef = useRef([]);\n const velocitiesRef = useRef([]);\n const hoveredColRef = useRef(-1);\n const wallHoveredRef = useRef(false);\n const pointerRef = useRef({ x: 0, y: 0 });\n const pointerDampedRef = useRef({ x: 0, y: 0 });\n const lastTsRef = useRef(null);\n\n const [containerHeight, setContainerHeight] = useState(600);\n const [activeId, setActiveId] = useState(null);\n const activeIdRef = useRef(null);\n const [reduced, setReduced] = useState(false);\n\n useEffect(() => {\n setReduced(prefersReducedMotion());\n const mq = window.matchMedia('(prefers-reduced-motion: reduce)');\n const onChange = e => setReduced(e.matches);\n mq.addEventListener('change', onChange);\n return () => mq.removeEventListener('change', onChange);\n }, []);\n\n const columnItems = useMemo(() => {\n const cols = Array.from({ length: columns }, () => []);\n items.forEach((item, i) => cols[i % columns].push(item));\n return cols.map(col => (col.length ? col : items.slice(0, 1)));\n }, [items, columns]);\n\n const columnMeta = useMemo(() => {\n const unit = tileHeight + gap;\n return columnItems.map(col => {\n const copyHeight = Math.max(unit, col.length * unit);\n const copies = Math.max(2, Math.ceil((containerHeight * 1.6) / copyHeight) + 1);\n return { copyHeight, copies };\n });\n }, [columnItems, tileHeight, gap, containerHeight]);\n\n useLayoutEffect(() => {\n if (!containerRef.current) return;\n const ro = new ResizeObserver(([entry]) => {\n setContainerHeight(entry.contentRect.height || 600);\n });\n ro.observe(containerRef.current);\n return () => ro.disconnect();\n }, []);\n\n const baseVelocities = useMemo(() => {\n const dirSign = direction === 'up' ? 1 : -1;\n return columnItems.map((_, c) => {\n const altSign = c % 2 === 0 ? 1 : -1;\n return speed * columnFactor(c, variance) * dirSign * altSign;\n });\n }, [columnItems, speed, direction, variance]);\n\n useEffect(() => {\n offsetsRef.current = columnMeta.map((meta, c) => meta.copyHeight * ((c * 0.37) % 1));\n velocitiesRef.current = columnItems.map(() => 0);\n }, [columnMeta, columnItems]);\n\n const applyPlaneTransform = useCallback(\n (px, py) => {\n const plane = planeRef.current;\n if (!plane) return;\n plane.style.transform =\n `translate(-50%, -50%) scale(1.18) ` +\n `rotateX(${tilt + py}deg) rotateY(${turn + px}deg) rotateZ(${roll}deg) ` +\n `translateZ(${-depth}px)`;\n },\n [tilt, turn, roll, depth]\n );\n\n useEffect(() => {\n const animate = ts => {\n if (lastTsRef.current === null) lastTsRef.current = ts;\n const dt = Math.min(0.05, Math.max(0, ts - lastTsRef.current) / 1000);\n lastTsRef.current = ts;\n\n const maxTilt = parallax * 8;\n const targetX = pointerRef.current.x * maxTilt;\n const targetY = -pointerRef.current.y * maxTilt;\n const damp = 1 - Math.exp(-dt / 0.12);\n pointerDampedRef.current.x += (targetX - pointerDampedRef.current.x) * damp;\n pointerDampedRef.current.y += (targetY - pointerDampedRef.current.y) * damp;\n applyPlaneTransform(pointerDampedRef.current.x, pointerDampedRef.current.y);\n\n if (!reduced) {\n for (let c = 0; c < trackRefs.current.length; c++) {\n const meta = columnMeta[c];\n if (!meta) continue;\n const paused = wallHoveredRef.current && pauseOnHover;\n const factor = paused || hoveredColRef.current === c ? 0 : 1;\n const target = baseVelocities[c] * factor;\n\n const ease = 1 - Math.exp(-dt / (target === 0 ? 0.16 : 0.28));\n velocitiesRef.current[c] += (target - velocitiesRef.current[c]) * ease;\n let next = (offsetsRef.current[c] ?? 0) + velocitiesRef.current[c] * dt;\n next = ((next % meta.copyHeight) + meta.copyHeight) % meta.copyHeight;\n offsetsRef.current[c] = next;\n\n const el = trackRefs.current[c];\n if (el) el.style.transform = `translate3d(0, ${-next}px, 0)`;\n }\n } else {\n for (let c = 0; c < trackRefs.current.length; c++) {\n const el = trackRefs.current[c];\n const meta = columnMeta[c];\n if (el && meta) el.style.transform = `translate3d(0, ${-(offsetsRef.current[c] ?? 0)}px, 0)`;\n }\n }\n\n rafRef.current = requestAnimationFrame(animate);\n };\n\n rafRef.current = requestAnimationFrame(animate);\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n lastTsRef.current = null;\n };\n }, [baseVelocities, columnMeta, pauseOnHover, parallax, reduced, applyPlaneTransform]);\n\n const activate = useCallback((id, index) => {\n activeIdRef.current = id;\n hoveredColRef.current = index;\n setActiveId(id);\n }, []);\n const release = useCallback(() => {\n activeIdRef.current = null;\n hoveredColRef.current = -1;\n setActiveId(null);\n }, []);\n\n const handlePointerMove = useCallback(\n e => {\n const rect = containerRef.current?.getBoundingClientRect();\n if (!rect) return;\n if (parallax > 0 && !reduced) {\n pointerRef.current = {\n x: (e.clientX - rect.left) / rect.width - 0.5,\n y: (e.clientY - rect.top) / rect.height - 0.5\n };\n }\n const hit = document.elementFromPoint(e.clientX, e.clientY);\n const tile = hit && hit.closest ? hit.closest('[data-tile-id]') : null;\n if (!tile) return;\n const id = tile.dataset.tileId;\n if (id === activeIdRef.current) return;\n activeIdRef.current = id;\n hoveredColRef.current = Number(tile.dataset.col);\n setActiveId(id);\n },\n [parallax, reduced]\n );\n\n const handlePointerLeaveWall = useCallback(() => {\n wallHoveredRef.current = false;\n pointerRef.current = { x: 0, y: 0 };\n release();\n }, [release]);\n\n const cssVars = useMemo(\n () => ({\n '--dw-tile-w': `${tileWidth}px`,\n '--dw-tile-h': `${tileHeight}px`,\n '--dw-gap': `${gap}px`,\n '--dw-radius': `${radius}px`,\n '--dw-perspective': `${perspective}px`,\n '--dw-lift': `${lift}px`,\n '--dw-dim': dim,\n '--dw-gray': grayscale ? 1 : 0,\n '--dw-overlay': overlayColor,\n '--dw-edge': `${Math.max(0, (1 - fade) * 100)}%`,\n ...style\n }),\n [tileWidth, tileHeight, gap, radius, perspective, lift, dim, grayscale, overlayColor, fade, style]\n );\n\n const renderTile = (item, id, colIndex) => {\n const inner = (\n \n {item.title\n \n \n );\n const commonProps = {\n className: `drift-wall__tile${activeId === id ? ' is-active' : ''}`,\n 'data-tile-id': id,\n 'data-col': colIndex,\n onFocus: () => activate(id, colIndex),\n onBlur: release\n };\n if (item.href) {\n return (\n \n {inner}\n \n );\n }\n return (\n
\n {inner}\n
\n );\n };\n\n const rootClass = ['drift-wall', reduced ? 'drift-wall--reduced' : '', className].filter(Boolean).join(' ');\n\n return (\n {\n wallHoveredRef.current = true;\n }}\n onPointerLeave={handlePointerLeaveWall}\n role=\"group\"\n aria-label=\"Drifting wall of tiles\"\n >\n
\n {columnItems.map((col, c) => {\n const meta = columnMeta[c];\n const copies = Array.from({ length: meta.copies });\n return (\n
\n
(trackRefs.current[c] = el)}>\n {copies.map((_, copyIndex) =>\n col.map((item, itemIndex) => renderTile(item, `${c}-${copyIndex}-${itemIndex}`, c))\n )}\n
\n
\n );\n })}\n
\n
\n );\n};\n\nexport default DriftWall;\n" } ], diff --git a/public/r/DriftWall-TS-CSS.json b/public/r/DriftWall-TS-CSS.json index 99ea95fc..e9d43458 100644 --- a/public/r/DriftWall-TS-CSS.json +++ b/public/r/DriftWall-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "DriftWall/DriftWall.css", + "type": "registry:file", + "path": "DriftWall.css", + "target": "@components/DriftWall.css", "content": ".drift-wall {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n perspective: var(--dw-perspective, 1200px);\n perspective-origin: 50% 50%;\n --dw-tile-w: 200px;\n --dw-tile-h: 132px;\n --dw-gap: 18px;\n --dw-radius: 14px;\n --dw-lift: 64px;\n --dw-dim: 0.55;\n --dw-gray: 0;\n --dw-overlay: #060010;\n --dw-edge: 40%;\n -webkit-mask-image:\n radial-gradient(ellipse 78% 82% at 50% 46%, #000 var(--dw-edge), transparent 100%),\n linear-gradient(to top, #000 var(--dw-edge), transparent 100%);\n -webkit-mask-composite: source-in;\n mask-image:\n radial-gradient(ellipse 78% 82% at 50% 46%, #000 var(--dw-edge), transparent 100%),\n linear-gradient(to top, #000 var(--dw-edge), transparent 100%);\n mask-composite: intersect;\n}\n\n.drift-wall__plane {\n position: absolute;\n top: 50%;\n left: 50%;\n display: flex;\n flex-direction: row;\n transform-style: preserve-3d;\n cursor: pointer;\n transform-origin: 50% 50%;\n will-change: transform;\n}\n\n.drift-wall__col {\n position: relative;\n width: calc(var(--dw-tile-w) + var(--dw-gap));\n transform-style: preserve-3d;\n}\n\n.drift-wall__track {\n display: flex;\n flex-direction: column;\n will-change: transform;\n transform-style: preserve-3d;\n}\n\n.drift-wall__tile {\n position: relative;\n display: block;\n width: 100%;\n height: calc(var(--dw-tile-h) + var(--dw-gap));\n flex: 0 0 auto;\n outline: none;\n transform-style: preserve-3d;\n}\n\n.drift-wall__inner {\n position: absolute;\n inset: calc(var(--dw-gap) / 2);\n display: block;\n border-radius: var(--dw-radius);\n overflow: hidden;\n background: #0b0b12;\n opacity: var(--dw-dim);\n transform: translateZ(0);\n pointer-events: none;\n transition:\n transform 0.42s cubic-bezier(0.22, 1, 0.36, 1),\n opacity 0.42s cubic-bezier(0.22, 1, 0.36, 1),\n box-shadow 0.42s cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n.drift-wall__tile img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n filter: grayscale(var(--dw-gray)) saturate(0.92);\n transition: filter 0.42s cubic-bezier(0.22, 1, 0.36, 1);\n user-select: none;\n -webkit-user-drag: none;\n}\n\n.drift-wall__overlay {\n position: absolute;\n inset: 0;\n background: var(--dw-overlay);\n opacity: 0.42;\n pointer-events: none;\n transition: opacity 0.42s cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n.drift-wall__tile.is-active .drift-wall__inner,\n.drift-wall__tile:focus-visible .drift-wall__inner {\n opacity: 1;\n transform: translateZ(var(--dw-lift));\n box-shadow: 0 24px 60px -18px rgba(0, 0, 0, 0.7);\n}\n\n.drift-wall__tile.is-active img,\n.drift-wall__tile:focus-visible img {\n filter: grayscale(0) saturate(1.05);\n}\n\n.drift-wall__tile.is-active .drift-wall__overlay,\n.drift-wall__tile:focus-visible .drift-wall__overlay {\n opacity: 0;\n}\n\n.drift-wall__tile:focus-visible .drift-wall__inner {\n box-shadow:\n 0 24px 60px -18px rgba(0, 0, 0, 0.7),\n 0 0 0 2px rgba(255, 255, 255, 0.9);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .drift-wall__plane,\n .drift-wall__track {\n will-change: auto;\n }\n}\n" }, { "type": "registry:component", - "path": "DriftWall/DriftWall.tsx", + "path": "DriftWall.tsx", "content": "import { CSSProperties, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport './DriftWall.css';\n\nexport interface DriftWallItem {\n image: string;\n title?: string;\n href?: string;\n}\n\nexport interface DriftWallProps {\n items?: DriftWallItem[];\n columns?: number;\n tileWidth?: number;\n tileHeight?: number;\n gap?: number;\n radius?: number;\n tilt?: number;\n turn?: number;\n roll?: number;\n perspective?: number;\n depth?: number;\n speed?: number;\n direction?: 'up' | 'down';\n variance?: number;\n parallax?: number;\n pauseOnHover?: boolean;\n lift?: number;\n fade?: number;\n dim?: number;\n grayscale?: boolean;\n overlayColor?: string;\n className?: string;\n style?: CSSProperties;\n}\n\ninterface ColumnMeta {\n copyHeight: number;\n copies: number;\n}\n\nconst DEFAULT_ITEMS: DriftWallItem[] = Array.from({ length: 15 }, (_, i) => {\n const ids = [1015, 1025, 1039, 1043, 1044, 1050, 1062, 1069, 1074, 1080, 1084, 106, 110, 133, 164];\n return {\n image: `https://picsum.photos/id/${ids[i % ids.length]}/600/400`,\n title: `Tile ${i + 1}`,\n href: undefined\n };\n});\n\nconst prefersReducedMotion = (): boolean =>\n typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nconst columnFactor = (index: number, variance: number): number => {\n const pseudo = ((index * 0.6180339887 + 0.35) % 1) * 2 - 1;\n return 1 + variance * pseudo;\n};\n\nconst DriftWall = ({\n items = DEFAULT_ITEMS,\n columns = 5,\n tileWidth = 200,\n tileHeight = 132,\n gap = 18,\n radius = 14,\n tilt = 16,\n turn = -14,\n roll = 0,\n perspective = 1200,\n depth = 120,\n speed = 42,\n direction = 'up',\n variance = 0.45,\n parallax = 0.6,\n pauseOnHover = false,\n lift = 64,\n fade = 0.6,\n dim = 0.55,\n grayscale = false,\n overlayColor = '#060010',\n className = '',\n style\n}: DriftWallProps) => {\n const containerRef = useRef(null);\n const planeRef = useRef(null);\n const trackRefs = useRef<(HTMLDivElement | null)[]>([]);\n const rafRef = useRef(null);\n\n const offsetsRef = useRef([]);\n const velocitiesRef = useRef([]);\n const hoveredColRef = useRef(-1);\n const wallHoveredRef = useRef(false);\n const pointerRef = useRef({ x: 0, y: 0 });\n const pointerDampedRef = useRef({ x: 0, y: 0 });\n const lastTsRef = useRef(null);\n\n const [containerHeight, setContainerHeight] = useState(600);\n const [activeId, setActiveId] = useState(null);\n const activeIdRef = useRef(null);\n const [reduced, setReduced] = useState(false);\n\n useEffect(() => {\n setReduced(prefersReducedMotion());\n const mq = window.matchMedia('(prefers-reduced-motion: reduce)');\n const onChange = (e: MediaQueryListEvent) => setReduced(e.matches);\n mq.addEventListener('change', onChange);\n return () => mq.removeEventListener('change', onChange);\n }, []);\n\n const columnItems = useMemo(() => {\n const cols: DriftWallItem[][] = Array.from({ length: columns }, () => []);\n items.forEach((item, i) => cols[i % columns].push(item));\n return cols.map(col => (col.length ? col : items.slice(0, 1)));\n }, [items, columns]);\n\n const columnMeta = useMemo(() => {\n const unit = tileHeight + gap;\n return columnItems.map(col => {\n const copyHeight = Math.max(unit, col.length * unit);\n const copies = Math.max(2, Math.ceil((containerHeight * 1.6) / copyHeight) + 1);\n return { copyHeight, copies };\n });\n }, [columnItems, tileHeight, gap, containerHeight]);\n\n useLayoutEffect(() => {\n if (!containerRef.current) return;\n const ro = new ResizeObserver(([entry]) => {\n setContainerHeight(entry.contentRect.height || 600);\n });\n ro.observe(containerRef.current);\n return () => ro.disconnect();\n }, []);\n\n const baseVelocities = useMemo(() => {\n const dirSign = direction === 'up' ? 1 : -1;\n return columnItems.map((_, c) => {\n const altSign = c % 2 === 0 ? 1 : -1;\n return speed * columnFactor(c, variance) * dirSign * altSign;\n });\n }, [columnItems, speed, direction, variance]);\n\n useEffect(() => {\n offsetsRef.current = columnMeta.map((meta, c) => meta.copyHeight * ((c * 0.37) % 1));\n velocitiesRef.current = columnItems.map(() => 0);\n }, [columnMeta, columnItems]);\n\n const applyPlaneTransform = useCallback(\n (px: number, py: number) => {\n const plane = planeRef.current;\n if (!plane) return;\n plane.style.transform =\n `translate(-50%, -50%) scale(1.18) ` +\n `rotateX(${tilt + py}deg) rotateY(${turn + px}deg) rotateZ(${roll}deg) ` +\n `translateZ(${-depth}px)`;\n },\n [tilt, turn, roll, depth]\n );\n\n useEffect(() => {\n const animate = (ts: number) => {\n if (lastTsRef.current === null) lastTsRef.current = ts;\n const dt = Math.min(0.05, Math.max(0, ts - lastTsRef.current) / 1000);\n lastTsRef.current = ts;\n\n const maxTilt = parallax * 8;\n const targetX = pointerRef.current.x * maxTilt;\n const targetY = -pointerRef.current.y * maxTilt;\n const damp = 1 - Math.exp(-dt / 0.12);\n pointerDampedRef.current.x += (targetX - pointerDampedRef.current.x) * damp;\n pointerDampedRef.current.y += (targetY - pointerDampedRef.current.y) * damp;\n applyPlaneTransform(pointerDampedRef.current.x, pointerDampedRef.current.y);\n\n if (!reduced) {\n for (let c = 0; c < trackRefs.current.length; c++) {\n const meta = columnMeta[c];\n if (!meta) continue;\n const paused = wallHoveredRef.current && pauseOnHover;\n const factor = paused || hoveredColRef.current === c ? 0 : 1;\n const target = baseVelocities[c] * factor;\n\n const ease = 1 - Math.exp(-dt / (target === 0 ? 0.16 : 0.28));\n velocitiesRef.current[c] += (target - velocitiesRef.current[c]) * ease;\n let next = (offsetsRef.current[c] ?? 0) + velocitiesRef.current[c] * dt;\n next = ((next % meta.copyHeight) + meta.copyHeight) % meta.copyHeight;\n offsetsRef.current[c] = next;\n\n const el = trackRefs.current[c];\n if (el) el.style.transform = `translate3d(0, ${-next}px, 0)`;\n }\n } else {\n for (let c = 0; c < trackRefs.current.length; c++) {\n const el = trackRefs.current[c];\n const meta = columnMeta[c];\n if (el && meta) el.style.transform = `translate3d(0, ${-(offsetsRef.current[c] ?? 0)}px, 0)`;\n }\n }\n\n rafRef.current = requestAnimationFrame(animate);\n };\n\n rafRef.current = requestAnimationFrame(animate);\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n lastTsRef.current = null;\n };\n }, [baseVelocities, columnMeta, pauseOnHover, parallax, reduced, applyPlaneTransform]);\n\n const activate = useCallback((id: string, index: number): void => {\n activeIdRef.current = id;\n hoveredColRef.current = index;\n setActiveId(id);\n }, []);\n const release = useCallback((): void => {\n activeIdRef.current = null;\n hoveredColRef.current = -1;\n setActiveId(null);\n }, []);\n\n const handlePointerMove = useCallback(\n (e: React.PointerEvent) => {\n const rect = containerRef.current?.getBoundingClientRect();\n if (!rect) return;\n if (parallax > 0 && !reduced) {\n pointerRef.current = {\n x: (e.clientX - rect.left) / rect.width - 0.5,\n y: (e.clientY - rect.top) / rect.height - 0.5\n };\n }\n const hit = document.elementFromPoint(e.clientX, e.clientY);\n const tile = hit && hit.closest ? (hit.closest('[data-tile-id]') as HTMLElement | null) : null;\n if (!tile) return;\n const id = tile.dataset.tileId ?? null;\n if (id === activeIdRef.current) return;\n activeIdRef.current = id;\n hoveredColRef.current = Number(tile.dataset.col);\n setActiveId(id);\n },\n [parallax, reduced]\n );\n\n const handlePointerLeaveWall = useCallback((): void => {\n wallHoveredRef.current = false;\n pointerRef.current = { x: 0, y: 0 };\n release();\n }, [release]);\n\n const cssVars = useMemo(\n () =>\n ({\n '--dw-tile-w': `${tileWidth}px`,\n '--dw-tile-h': `${tileHeight}px`,\n '--dw-gap': `${gap}px`,\n '--dw-radius': `${radius}px`,\n '--dw-perspective': `${perspective}px`,\n '--dw-lift': `${lift}px`,\n '--dw-dim': dim,\n '--dw-gray': grayscale ? 1 : 0,\n '--dw-overlay': overlayColor,\n '--dw-edge': `${Math.max(0, (1 - fade) * 100)}%`,\n ...style\n }) as CSSProperties,\n [tileWidth, tileHeight, gap, radius, perspective, lift, dim, grayscale, overlayColor, fade, style]\n );\n\n const renderTile = (item: DriftWallItem, id: string, colIndex: number) => {\n const inner = (\n \n {item.title\n \n \n );\n const commonProps = {\n className: `drift-wall__tile${activeId === id ? ' is-active' : ''}`,\n 'data-tile-id': id,\n 'data-col': colIndex,\n onFocus: () => activate(id, colIndex),\n onBlur: release\n };\n if (item.href) {\n return (\n \n {inner}\n \n );\n }\n return (\n
\n {inner}\n
\n );\n };\n\n const rootClass = ['drift-wall', reduced ? 'drift-wall--reduced' : '', className].filter(Boolean).join(' ');\n\n return (\n {\n wallHoveredRef.current = true;\n }}\n onPointerLeave={handlePointerLeaveWall}\n role=\"group\"\n aria-label=\"Drifting wall of tiles\"\n >\n
\n {columnItems.map((col, c) => {\n const meta = columnMeta[c];\n const copies = Array.from({ length: meta.copies });\n return (\n
\n {\n trackRefs.current[c] = el;\n }}\n >\n {copies.map((_, copyIndex) =>\n col.map((item, itemIndex) => renderTile(item, `${c}-${copyIndex}-${itemIndex}`, c))\n )}\n
\n
\n );\n })}\n
\n
\n );\n};\n\nexport default DriftWall;\n" } ], diff --git a/public/r/EchoText-JS-CSS.json b/public/r/EchoText-JS-CSS.json index 07a11717..d9efa446 100644 --- a/public/r/EchoText-JS-CSS.json +++ b/public/r/EchoText-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "EchoText/EchoText.css", + "type": "registry:file", + "path": "EchoText.css", + "target": "@components/EchoText.css", "content": ".echo-text {\n position: relative;\n display: inline-block;\n white-space: nowrap;\n line-height: 0.9;\n letter-spacing: -0.04em;\n user-select: none;\n contain: layout style;\n font-kerning: normal;\n text-rendering: geometricPrecision;\n}\n\n.echo-text__echo {\n position: absolute;\n inset: 0;\n display: block;\n pointer-events: none;\n transform: translate3d(0, 0, 0);\n transform-origin: 50% 50%;\n will-change: transform, opacity;\n backface-visibility: hidden;\n}\n\n.echo-text__echo--front {\n position: relative;\n z-index: 2;\n opacity: 1;\n filter: none;\n text-shadow: 0 0.035em 0 rgba(255, 255, 255, 0.04);\n will-change: transform;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .echo-text__echo:not(.echo-text__echo--front) {\n display: none;\n }\n\n .echo-text__echo--front {\n transform: none !important;\n }\n}\n" }, { "type": "registry:component", - "path": "EchoText/EchoText.jsx", + "path": "EchoText.jsx", "content": "import { useEffect, useMemo, useRef, useState } from 'react';\n\nimport './EchoText.css';\n\nconst clamp = (value, min, max) => Math.min(Math.max(value, min), max);\n\nconst directionVectors = {\n right: { x: 1, y: 0 },\n left: { x: -1, y: 0 },\n up: { x: 0, y: -1 },\n down: { x: 0, y: 1 },\n diagonal: { x: 0.72, y: 0.72 }\n};\n\nconst easing = {\n linear: t => t,\n 'ease-out': t => 1 - Math.pow(1 - t, 3),\n 'ease-in-out': t => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2),\n snappy: t => 1 - Math.pow(1 - t, 5)\n};\n\nconst EchoText = ({\n text = 'Motion Echo',\n echoes = 12,\n lag = 0.24,\n offset = 36,\n direction = 'right',\n fade = 0.72,\n blur = 3,\n tint = '#7dd3fc',\n mode = 'both',\n cursorRadius = 320,\n duration = 900,\n ease = 'ease-out',\n fontSize = 'clamp(3rem, 9vw, 7rem)',\n fontWeight = 800,\n color = '#f8fafc',\n className = '',\n style\n}) => {\n const rootRef = useRef(null);\n const copyRefs = useRef([]);\n const frameRef = useRef(null);\n const stateRef = useRef(null);\n const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);\n\n const echoCount = prefersReducedMotion ? 0 : clamp(Math.round(echoes), 0, 24);\n const copyIndexes = useMemo(() => Array.from({ length: echoCount + 1 }, (_, index) => index), [echoCount]);\n\n useEffect(() => {\n if (typeof window === 'undefined' || !window.matchMedia) return;\n\n const media = window.matchMedia('(prefers-reduced-motion: reduce)');\n const updateMotionPreference = () => setPrefersReducedMotion(media.matches);\n updateMotionPreference();\n\n media.addEventListener?.('change', updateMotionPreference);\n return () => media.removeEventListener?.('change', updateMotionPreference);\n }, []);\n\n useEffect(() => {\n const root = rootRef.current;\n if (!root || prefersReducedMotion) return;\n\n const vector = directionVectors[direction] || directionVectors.right;\n const safeOffset = clamp(Number(offset) || 0, 0, 120);\n const safeCursorRadius = clamp(Number(cursorRadius) || 320, 40, 1200);\n const safeLag = clamp(Number(lag) || 0.16, 0.02, 0.5);\n const safeFade = clamp(Number(fade) || 0.64, 0.1, 0.95);\n const safeBlur = clamp(Number(blur) || 0, 0, 16);\n const safeDuration = Math.max(0, Number(duration) || 0);\n const easeFn = easing[ease] || easing['ease-out'];\n const entranceEnabled = mode === 'entrance' || mode === 'both';\n const pointerEnabled = mode === 'pointer' || mode === 'both';\n const positions = Array.from({ length: echoCount + 1 }, (_, index) => {\n const entranceAmount = entranceEnabled ? safeOffset * (index + 0.35) : 0;\n return { x: vector.x * entranceAmount, y: vector.y * entranceAmount };\n });\n\n stateRef.current = {\n targetX: 0,\n targetY: 0,\n lastTargetX: 0,\n lastTargetY: 0,\n activity: entranceEnabled ? 1 : 0,\n positions,\n startTime: performance.now()\n };\n\n let canHover = false;\n let cleanupPointer = () => {};\n\n if (pointerEnabled && window.matchMedia) {\n const hoverMedia = window.matchMedia('(hover: hover) and (pointer: fine)');\n canHover = hoverMedia.matches;\n }\n\n const handlePointerMove = event => {\n const state = stateRef.current;\n if (!state) return;\n\n const rect = root.getBoundingClientRect();\n if (!rect.width || !rect.height) return;\n\n const centerX = rect.left + rect.width / 2;\n const centerY = rect.top + rect.height / 2;\n const deltaX = event.clientX - centerX;\n const deltaY = event.clientY - centerY;\n const distance = Math.hypot(deltaX, deltaY);\n const reach = distance > 0 ? clamp(distance / safeCursorRadius, 0, 1) : 0;\n const dirX = distance > 0 ? deltaX / distance : 0;\n const dirY = distance > 0 ? deltaY / distance : 0;\n\n state.targetX = dirX * reach * safeOffset;\n state.targetY = dirY * reach * safeOffset * 0.72;\n };\n\n const handlePointerLeave = () => {\n const state = stateRef.current;\n if (!state) return;\n state.targetX = 0;\n state.targetY = 0;\n };\n\n if (canHover) {\n window.addEventListener('pointermove', handlePointerMove, { passive: true });\n document.addEventListener('pointerleave', handlePointerLeave);\n cleanupPointer = () => {\n window.removeEventListener('pointermove', handlePointerMove);\n document.removeEventListener('pointerleave', handlePointerLeave);\n };\n }\n\n const renderFrame = now => {\n const state = stateRef.current;\n if (!state) return;\n\n const elapsed = now - state.startTime;\n const entranceProgress = entranceEnabled && safeDuration > 0 ? clamp(elapsed / safeDuration, 0, 1) : 1;\n const easedEntrance = easeFn(entranceProgress);\n const entranceRest = entranceEnabled ? 1 - easedEntrance : 0;\n const targetVelocity = Math.hypot(state.targetX - state.lastTargetX, state.targetY - state.lastTargetY);\n\n state.lastTargetX = state.targetX;\n state.lastTargetY = state.targetY;\n\n let maxSeparation = 0;\n\n for (let index = 0; index <= echoCount; index += 1) {\n const copy = copyRefs.current[index];\n const current = state.positions[index];\n if (!copy || !current) continue;\n\n const entranceAmount = entranceRest * safeOffset * (index + 0.35);\n const desiredX = state.targetX + vector.x * entranceAmount;\n const desiredY = state.targetY + vector.y * entranceAmount;\n const lerp = clamp(0.34 / (1 + index * safeLag * 4.2), 0.018, 0.36);\n\n current.x += (desiredX - current.x) * lerp;\n current.y += (desiredY - current.y) * lerp;\n\n copy.style.transform = `translate3d(${current.x.toFixed(3)}px, ${current.y.toFixed(3)}px, 0)`;\n\n if (index > 0) {\n const front = state.positions[0];\n const separation = front ? Math.hypot(current.x - front.x, current.y - front.y) : 0;\n maxSeparation = Math.max(maxSeparation, separation);\n const depth = echoCount ? index / echoCount : 0;\n copy.style.filter = safeBlur > 0 ? `blur(${(safeBlur * depth).toFixed(2)}px)` : 'none';\n }\n }\n\n const separationActivity = safeOffset > 0 ? clamp(maxSeparation / (safeOffset * 2.25), 0, 1) : 0;\n const targetActivity = safeOffset > 0 ? clamp(targetVelocity / (safeOffset * 0.35), 0, 1) : 0;\n const nextActivity = Math.max(entranceRest, separationActivity, targetActivity);\n state.activity += (nextActivity - state.activity) * 0.18;\n\n for (let index = 1; index <= echoCount; index += 1) {\n const copy = copyRefs.current[index];\n if (!copy) continue;\n copy.style.opacity = String(Math.pow(safeFade, index) * state.activity);\n }\n\n const stillMoving =\n state.activity > 0.002 ||\n Math.abs(state.targetX) > 0.01 ||\n Math.abs(state.targetY) > 0.01 ||\n entranceProgress < 1 ||\n canHover;\n\n if (stillMoving) {\n frameRef.current = requestAnimationFrame(renderFrame);\n } else {\n frameRef.current = null;\n }\n };\n\n frameRef.current = requestAnimationFrame(renderFrame);\n\n return () => {\n cleanupPointer();\n if (frameRef.current) cancelAnimationFrame(frameRef.current);\n frameRef.current = null;\n stateRef.current = null;\n };\n }, [blur, cursorRadius, direction, duration, ease, echoCount, fade, lag, mode, offset, prefersReducedMotion]);\n\n const rootStyle = {\n fontSize,\n fontWeight,\n color,\n ...style\n };\n\n return (\n \n {copyIndexes\n .slice(1)\n .reverse()\n .map(index => (\n {\n copyRefs.current[index] = element;\n }}\n style={{\n color: tint ? `color-mix(in srgb, ${tint} ${Math.min(72, 18 + index * 5)}%, ${color})` : color,\n opacity: 0\n }}\n >\n {text}\n \n ))}\n {\n copyRefs.current[0] = element;\n }}\n >\n {text}\n \n \n );\n};\n\nexport default EchoText;\n" } ], diff --git a/public/r/EchoText-TS-CSS.json b/public/r/EchoText-TS-CSS.json index 1d96771a..4e614e5b 100644 --- a/public/r/EchoText-TS-CSS.json +++ b/public/r/EchoText-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "EchoText/EchoText.css", + "type": "registry:file", + "path": "EchoText.css", + "target": "@components/EchoText.css", "content": ".echo-text {\n position: relative;\n display: inline-block;\n white-space: nowrap;\n line-height: 0.9;\n letter-spacing: -0.04em;\n user-select: none;\n contain: layout style;\n font-kerning: normal;\n text-rendering: geometricPrecision;\n}\n\n.echo-text__echo {\n position: absolute;\n inset: 0;\n display: block;\n pointer-events: none;\n transform: translate3d(0, 0, 0);\n transform-origin: 50% 50%;\n will-change: transform, opacity;\n backface-visibility: hidden;\n}\n\n.echo-text__echo--front {\n position: relative;\n z-index: 2;\n opacity: 1;\n filter: none;\n text-shadow: 0 0.035em 0 rgba(255, 255, 255, 0.04);\n will-change: transform;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .echo-text__echo:not(.echo-text__echo--front) {\n display: none;\n }\n\n .echo-text__echo--front {\n transform: none !important;\n }\n}\n" }, { "type": "registry:component", - "path": "EchoText/EchoText.tsx", + "path": "EchoText.tsx", "content": "import React, { CSSProperties, useEffect, useMemo, useRef, useState } from 'react';\n\nimport './EchoText.css';\n\ntype Direction = 'right' | 'left' | 'up' | 'down' | 'diagonal';\ntype Mode = 'entrance' | 'pointer' | 'both';\ntype Ease = 'linear' | 'ease-out' | 'ease-in-out' | 'snappy';\n\ntype Vector = { x: number; y: number };\ntype Position = { x: number; y: number };\n\ntype AnimationState = {\n targetX: number;\n targetY: number;\n lastTargetX: number;\n lastTargetY: number;\n activity: number;\n positions: Position[];\n startTime: number;\n};\n\nexport interface EchoTextProps {\n text?: string;\n echoes?: number;\n lag?: number;\n offset?: number;\n direction?: Direction;\n fade?: number;\n blur?: number;\n tint?: string | false;\n mode?: Mode;\n cursorRadius?: number;\n duration?: number;\n ease?: Ease;\n fontSize?: string | number;\n fontWeight?: string | number;\n color?: string;\n className?: string;\n style?: CSSProperties;\n}\n\nconst clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);\n\nconst directionVectors: Record = {\n right: { x: 1, y: 0 },\n left: { x: -1, y: 0 },\n up: { x: 0, y: -1 },\n down: { x: 0, y: 1 },\n diagonal: { x: 0.72, y: 0.72 }\n};\n\nconst easing: Record number> = {\n linear: t => t,\n 'ease-out': t => 1 - Math.pow(1 - t, 3),\n 'ease-in-out': t => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2),\n snappy: t => 1 - Math.pow(1 - t, 5)\n};\n\nconst EchoText: React.FC = ({\n text = 'Motion Echo',\n echoes = 12,\n lag = 0.24,\n offset = 36,\n direction = 'right',\n fade = 0.72,\n blur = 3,\n tint = '#7dd3fc',\n mode = 'both',\n cursorRadius = 320,\n duration = 900,\n ease = 'ease-out',\n fontSize = 'clamp(3rem, 9vw, 7rem)',\n fontWeight = 800,\n color = '#f8fafc',\n className = '',\n style\n}) => {\n const rootRef = useRef(null);\n const copyRefs = useRef>([]);\n const frameRef = useRef(null);\n const stateRef = useRef(null);\n const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);\n\n const echoCount = prefersReducedMotion ? 0 : clamp(Math.round(echoes), 0, 24);\n const copyIndexes = useMemo(() => Array.from({ length: echoCount + 1 }, (_, index) => index), [echoCount]);\n\n useEffect(() => {\n if (typeof window === 'undefined' || !window.matchMedia) return;\n\n const media = window.matchMedia('(prefers-reduced-motion: reduce)');\n const updateMotionPreference = () => setPrefersReducedMotion(media.matches);\n updateMotionPreference();\n\n media.addEventListener?.('change', updateMotionPreference);\n return () => media.removeEventListener?.('change', updateMotionPreference);\n }, []);\n\n useEffect(() => {\n const root = rootRef.current;\n if (!root || prefersReducedMotion) return;\n\n const vector = directionVectors[direction] || directionVectors.right;\n const safeOffset = clamp(Number(offset) || 0, 0, 120);\n const safeCursorRadius = clamp(Number(cursorRadius) || 320, 40, 1200);\n const safeLag = clamp(Number(lag) || 0.16, 0.02, 0.5);\n const safeFade = clamp(Number(fade) || 0.64, 0.1, 0.95);\n const safeBlur = clamp(Number(blur) || 0, 0, 16);\n const safeDuration = Math.max(0, Number(duration) || 0);\n const easeFn = easing[ease] || easing['ease-out'];\n const entranceEnabled = mode === 'entrance' || mode === 'both';\n const pointerEnabled = mode === 'pointer' || mode === 'both';\n const positions = Array.from({ length: echoCount + 1 }, (_, index) => {\n const entranceAmount = entranceEnabled ? safeOffset * (index + 0.35) : 0;\n return { x: vector.x * entranceAmount, y: vector.y * entranceAmount };\n });\n\n stateRef.current = {\n targetX: 0,\n targetY: 0,\n lastTargetX: 0,\n lastTargetY: 0,\n activity: entranceEnabled ? 1 : 0,\n positions,\n startTime: performance.now()\n };\n\n let canHover = false;\n let cleanupPointer = () => {};\n\n if (pointerEnabled && window.matchMedia) {\n const hoverMedia = window.matchMedia('(hover: hover) and (pointer: fine)');\n canHover = hoverMedia.matches;\n }\n\n const handlePointerMove = (event: PointerEvent) => {\n const state = stateRef.current;\n if (!state) return;\n\n const rect = root.getBoundingClientRect();\n if (!rect.width || !rect.height) return;\n\n const centerX = rect.left + rect.width / 2;\n const centerY = rect.top + rect.height / 2;\n const deltaX = event.clientX - centerX;\n const deltaY = event.clientY - centerY;\n const distance = Math.hypot(deltaX, deltaY);\n const reach = distance > 0 ? clamp(distance / safeCursorRadius, 0, 1) : 0;\n const dirX = distance > 0 ? deltaX / distance : 0;\n const dirY = distance > 0 ? deltaY / distance : 0;\n\n state.targetX = dirX * reach * safeOffset;\n state.targetY = dirY * reach * safeOffset * 0.72;\n };\n\n const handlePointerLeave = () => {\n const state = stateRef.current;\n if (!state) return;\n state.targetX = 0;\n state.targetY = 0;\n };\n\n if (canHover) {\n window.addEventListener('pointermove', handlePointerMove, { passive: true });\n document.addEventListener('pointerleave', handlePointerLeave);\n cleanupPointer = () => {\n window.removeEventListener('pointermove', handlePointerMove);\n document.removeEventListener('pointerleave', handlePointerLeave);\n };\n }\n\n const renderFrame = (now: number) => {\n const state = stateRef.current;\n if (!state) return;\n\n const elapsed = now - state.startTime;\n const entranceProgress = entranceEnabled && safeDuration > 0 ? clamp(elapsed / safeDuration, 0, 1) : 1;\n const easedEntrance = easeFn(entranceProgress);\n const entranceRest = entranceEnabled ? 1 - easedEntrance : 0;\n const targetVelocity = Math.hypot(state.targetX - state.lastTargetX, state.targetY - state.lastTargetY);\n\n state.lastTargetX = state.targetX;\n state.lastTargetY = state.targetY;\n\n let maxSeparation = 0;\n\n for (let index = 0; index <= echoCount; index += 1) {\n const copy = copyRefs.current[index];\n const current = state.positions[index];\n if (!copy || !current) continue;\n\n const entranceAmount = entranceRest * safeOffset * (index + 0.35);\n const desiredX = state.targetX + vector.x * entranceAmount;\n const desiredY = state.targetY + vector.y * entranceAmount;\n const lerp = clamp(0.34 / (1 + index * safeLag * 4.2), 0.018, 0.36);\n\n current.x += (desiredX - current.x) * lerp;\n current.y += (desiredY - current.y) * lerp;\n\n copy.style.transform = `translate3d(${current.x.toFixed(3)}px, ${current.y.toFixed(3)}px, 0)`;\n\n if (index > 0) {\n const front = state.positions[0];\n const separation = front ? Math.hypot(current.x - front.x, current.y - front.y) : 0;\n maxSeparation = Math.max(maxSeparation, separation);\n const depth = echoCount ? index / echoCount : 0;\n copy.style.filter = safeBlur > 0 ? `blur(${(safeBlur * depth).toFixed(2)}px)` : 'none';\n }\n }\n\n const separationActivity = safeOffset > 0 ? clamp(maxSeparation / (safeOffset * 2.25), 0, 1) : 0;\n const targetActivity = safeOffset > 0 ? clamp(targetVelocity / (safeOffset * 0.35), 0, 1) : 0;\n const nextActivity = Math.max(entranceRest, separationActivity, targetActivity);\n state.activity += (nextActivity - state.activity) * 0.18;\n\n for (let index = 1; index <= echoCount; index += 1) {\n const copy = copyRefs.current[index];\n if (!copy) continue;\n copy.style.opacity = String(Math.pow(safeFade, index) * state.activity);\n }\n\n const stillMoving =\n state.activity > 0.002 ||\n Math.abs(state.targetX) > 0.01 ||\n Math.abs(state.targetY) > 0.01 ||\n entranceProgress < 1 ||\n canHover;\n\n if (stillMoving) {\n frameRef.current = requestAnimationFrame(renderFrame);\n } else {\n frameRef.current = null;\n }\n };\n\n frameRef.current = requestAnimationFrame(renderFrame);\n\n return () => {\n cleanupPointer();\n if (frameRef.current) cancelAnimationFrame(frameRef.current);\n frameRef.current = null;\n stateRef.current = null;\n };\n }, [blur, cursorRadius, direction, duration, ease, echoCount, fade, lag, mode, offset, prefersReducedMotion]);\n\n const rootStyle: CSSProperties = {\n fontSize,\n fontWeight,\n color,\n ...style\n };\n\n return (\n \n {copyIndexes\n .slice(1)\n .reverse()\n .map(index => (\n {\n copyRefs.current[index] = element;\n }}\n style={{\n color: tint ? `color-mix(in srgb, ${tint} ${Math.min(72, 18 + index * 5)}%, ${color})` : color,\n opacity: 0\n }}\n >\n {text}\n \n ))}\n {\n copyRefs.current[0] = element;\n }}\n >\n {text}\n \n \n );\n};\n\nexport default EchoText;\n" } ], diff --git a/public/r/ElasticMesh-JS-CSS.json b/public/r/ElasticMesh-JS-CSS.json index 384f9bac..0f790de6 100644 --- a/public/r/ElasticMesh-JS-CSS.json +++ b/public/r/ElasticMesh-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "ElasticMesh/ElasticMesh.css", + "type": "registry:file", + "path": "ElasticMesh.css", + "target": "@components/ElasticMesh.css", "content": ".elastic-mesh {\n position: relative;\n width: 100%;\n height: 100%;\n min-height: 0;\n touch-action: none;\n}\n\n.elastic-mesh canvas {\n display: block;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "ElasticMesh/ElasticMesh.jsx", + "path": "ElasticMesh.jsx", "content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Geometry, Program, Mesh, Texture } from 'ogl';\n\nimport './ElasticMesh.css';\n\nconst DIST = 4.6;\nconst FIT = 0.82;\n\nconst VERT = `\nprecision highp float;\nattribute vec2 aGrid;\nattribute vec2 uv;\nattribute vec3 aOffset;\nattribute vec3 aNormal;\n\nuniform float uAspect;\nuniform float uTilt;\nuniform float uDist;\nuniform float uFit;\n\nvarying vec2 vUv;\nvarying vec3 vNormal;\nvarying float vDepth;\n\nvoid main() {\n vUv = uv;\n\n vec2 base = vec2((aGrid.x * 2.0 - 1.0) * uAspect, 1.0 - aGrid.y * 2.0);\n vec3 p = vec3(base + aOffset.xy, aOffset.z);\n\n float ct = cos(uTilt);\n float st = sin(uTilt);\n float ry = p.y * ct - p.z * st;\n float rz = p.y * st + p.z * ct;\n p.y = ry;\n p.z = rz;\n\n float persp = uDist / (uDist - p.z);\n vec2 clip = vec2(p.x / uAspect, p.y) * persp * uFit;\n\n vNormal = aNormal;\n vDepth = aOffset.z;\n gl_Position = vec4(clip, 0.0, 1.0);\n}\n`;\n\nconst FRAG = `\nprecision highp float;\n\nvarying vec2 vUv;\nvarying vec3 vNormal;\nvarying float vDepth;\n\nuniform sampler2D tMap;\nuniform float uHasImage;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uHighlight;\nuniform float uShading;\nuniform vec2 uRes;\nuniform float uRadius;\nuniform float uGrid;\nuniform float uGridDensity;\nuniform float uGridOpacity;\nuniform vec3 uGridColor;\n\nvoid main() {\n vec3 base;\n if (uHasImage > 0.5) {\n base = texture2D(tMap, vUv).rgb;\n } else {\n base = mix(uColor1, uColor2, clamp(vUv.y, 0.0, 1.0));\n }\n\n vec3 N = normalize(vNormal);\n vec3 L = normalize(vec3(-0.35, 0.55, 0.78));\n vec3 V = vec3(0.0, 0.0, 1.0);\n vec3 H = normalize(L + V);\n\n float diff = clamp(dot(N, L), 0.0, 1.0);\n float specRaw = pow(clamp(dot(N, H), 0.0, 1.0), 26.0);\n float specFlat = pow(clamp(H.z, 0.0, 1.0), 26.0);\n float spec = clamp((specRaw - specFlat) / (1.0 - specFlat), 0.0, 1.0);\n float ao = clamp(1.0 + vDepth * 0.45, 0.65, 1.25);\n\n vec3 lit = base * (1.0 - uShading * 0.28);\n lit += base * diff * uShading * 0.55;\n lit *= ao;\n lit += uHighlight * spec * uShading * 0.25;\n\n if (uGrid > 0.5) {\n vec2 g = vUv * uGridDensity;\n vec2 w = uGridDensity / max(uRes, vec2(1.0));\n vec2 d = abs(fract(g - 0.5) - 0.5) / max(w * 1.5, vec2(1e-4));\n float line = 1.0 - clamp(min(d.x, d.y), 0.0, 1.0);\n lit = mix(lit, uGridColor, line * uGridOpacity * (0.45 + diff * 0.55));\n }\n\n vec2 p = (vUv - 0.5) * uRes;\n vec2 halfRes = uRes * 0.5;\n float r = min(uRadius, min(halfRes.x, halfRes.y));\n vec2 q = abs(p) - (halfRes - r);\n float sd = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;\n float alpha = 1.0 - smoothstep(-1.25, 1.25, sd);\n if (alpha <= 0.002) discard;\n\n gl_FragColor = vec4(lit, alpha);\n}\n`;\n\nfunction hexToRgb(hex) {\n let h = (hex || '').replace('#', '').trim();\n if (h.length === 3)\n h = h\n .split('')\n .map(c => c + c)\n .join('');\n const n = parseInt(h || '000000', 16);\n return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\nconst ElasticMesh = ({\n image = '',\n color1 = '#5227FF',\n color2 = '#B19EEF',\n highlight = '#ffffff',\n showGrid = true,\n gridDensity = 20,\n gridOpacity = 0.28,\n gridColor = '#ffffff',\n borderRadius = 25,\n stiffness = 0.05,\n damping = 0.2,\n grabRadius = 0.6,\n pull = 0.4,\n wobble = 5,\n tilt = 14,\n shading = 0.5,\n resolution = 25,\n interaction = 'hover',\n enabled = true,\n className = '',\n style,\n ...rest\n}) => {\n const containerRef = useRef(null);\n\n const propsRef = useRef({});\n propsRef.current = {\n color1,\n color2,\n highlight,\n showGrid,\n gridDensity,\n gridOpacity,\n gridColor,\n borderRadius,\n stiffness,\n damping,\n grabRadius,\n pull,\n wobble,\n tilt,\n shading,\n interaction,\n enabled\n };\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n const renderer = new Renderer({ alpha: true, antialias: true, dpr: Math.min(window.devicePixelRatio || 1, 2) });\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n gl.enable(gl.BLEND);\n gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\n\n const N = Math.max(6, Math.min(40, Math.round(resolution)));\n const nodeCount = N * N;\n\n const aGrid = new Float32Array(nodeCount * 2);\n const uv = new Float32Array(nodeCount * 2);\n const aOffset = new Float32Array(nodeCount * 3);\n const aNormal = new Float32Array(nodeCount * 3);\n\n for (let j = 0; j < N; j++) {\n for (let i = 0; i < N; i++) {\n const idx = j * N + i;\n const u = i / (N - 1);\n const v = j / (N - 1);\n aGrid[idx * 2] = u;\n aGrid[idx * 2 + 1] = v;\n uv[idx * 2] = u;\n uv[idx * 2 + 1] = v;\n aNormal[idx * 3 + 2] = 1;\n }\n }\n\n const quads = (N - 1) * (N - 1);\n const index = new Uint16Array(quads * 6);\n let t = 0;\n for (let j = 0; j < N - 1; j++) {\n for (let i = 0; i < N - 1; i++) {\n const a = j * N + i;\n const b = a + 1;\n const c = a + N;\n const d = c + 1;\n index[t++] = a;\n index[t++] = c;\n index[t++] = b;\n index[t++] = b;\n index[t++] = c;\n index[t++] = d;\n }\n }\n\n const geometry = new Geometry(gl, {\n aGrid: { size: 2, data: aGrid },\n uv: { size: 2, data: uv },\n aOffset: { size: 3, data: aOffset },\n aNormal: { size: 3, data: aNormal },\n index: { data: index }\n });\n\n const texture = new Texture(gl, { generateMipmaps: false, flipY: false });\n let hasImage = 0;\n if (image) {\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.src = image;\n img.onload = () => {\n texture.image = img;\n program.uniforms.uHasImage.value = 1;\n };\n }\n\n const program = new Program(gl, {\n vertex: VERT,\n fragment: FRAG,\n transparent: true,\n cullFace: null,\n uniforms: {\n tMap: { value: texture },\n uHasImage: { value: hasImage },\n uColor1: { value: hexToRgb(color1) },\n uColor2: { value: hexToRgb(color2) },\n uHighlight: { value: hexToRgb(highlight) },\n uGrid: { value: showGrid ? 1 : 0 },\n uGridDensity: { value: gridDensity },\n uGridOpacity: { value: gridOpacity },\n uGridColor: { value: hexToRgb(gridColor) },\n uShading: { value: shading },\n uRes: { value: [1, 1] },\n uRadius: { value: borderRadius },\n uAspect: { value: 1 },\n uTilt: { value: (tilt * Math.PI) / 180 },\n uDist: { value: DIST },\n uFit: { value: FIT }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n\n const baseX = new Float32Array(nodeCount);\n const baseY = new Float32Array(nodeCount);\n const pos = new Float32Array(nodeCount * 3);\n const vel = new Float32Array(nodeCount * 3);\n const accel = new Float32Array(nodeCount * 3);\n\n let aspect = 1;\n function refreshBase() {\n for (let idx = 0; idx < nodeCount; idx++) {\n baseX[idx] = (aGrid[idx * 2] * 2 - 1) * aspect;\n baseY[idx] = 1 - aGrid[idx * 2 + 1] * 2;\n }\n }\n\n function resize() {\n const w = container.offsetWidth || 1;\n const h = container.offsetHeight || 1;\n renderer.setSize(w, h);\n aspect = w / h;\n program.uniforms.uAspect.value = aspect;\n program.uniforms.uRes.value = [w, h];\n refreshBase();\n }\n\n const ro = new ResizeObserver(resize);\n ro.observe(container);\n resize();\n\n const pointer = { x: 0, y: 0, tx: 0, ty: 0, active: false, targetActive: false };\n\n function toPlane(clientX, clientY) {\n const rect = container.getBoundingClientRect();\n const mx = (clientX - rect.left) / rect.width;\n const my = (clientY - rect.top) / rect.height;\n const clipX = mx * 2 - 1;\n const clipY = 1 - my * 2;\n const t = ((propsRef.current.tilt || 0) * Math.PI) / 180;\n const ct = Math.cos(t);\n const st = Math.sin(t);\n const a = clipY / (ct * FIT * DIST);\n const py = (a * DIST) / (1 + a * st);\n const persp = DIST / (DIST - py * st);\n pointer.tx = (clipX * aspect) / (persp * FIT);\n pointer.ty = py;\n }\n\n function onMove(e) {\n toPlane(e.clientX, e.clientY);\n if (propsRef.current.interaction === 'hover') pointer.targetActive = true;\n }\n function onEnter() {\n if (propsRef.current.interaction === 'hover') pointer.targetActive = true;\n }\n function onLeave() {\n pointer.targetActive = false;\n }\n function onDown(e) {\n if (propsRef.current.interaction === 'drag') {\n toPlane(e.clientX, e.clientY);\n pointer.x = pointer.tx;\n pointer.y = pointer.ty;\n pointer.targetActive = true;\n }\n }\n function onUp() {\n if (propsRef.current.interaction === 'drag') pointer.targetActive = false;\n }\n function onTouch(e) {\n if (e.touches.length) {\n toPlane(e.touches[0].clientX, e.touches[0].clientY);\n pointer.targetActive = true;\n }\n }\n\n container.addEventListener('mousemove', onMove);\n container.addEventListener('mouseenter', onEnter);\n container.addEventListener('mouseleave', onLeave);\n container.addEventListener('mousedown', onDown);\n window.addEventListener('mouseup', onUp);\n container.addEventListener('touchstart', onTouch, { passive: true });\n container.addEventListener('touchmove', onTouch, { passive: true });\n container.addEventListener('touchend', onLeave);\n\n const STEP = 1 / 120;\n const MAX_SUB = 5;\n let accTime = 0;\n let last = performance.now();\n let maxOffset = 0;\n let maxVel = 0;\n\n function substep() {\n const p = propsRef.current;\n const s = p.stiffness;\n const retain = 1 - p.damping;\n const coupling = 0.06 + p.wobble * 0.032;\n const active = pointer.active && p.enabled && !reduceMotion;\n const r = Math.max(0.08, p.grabRadius) * 1.4;\n const invR = 1 / r;\n const force = p.pull * 0.009;\n\n for (let j = 0; j < N; j++) {\n for (let i = 0; i < N; i++) {\n const idx = j * N + i;\n const o3 = idx * 3;\n const ox = pos[o3];\n const oy = pos[o3 + 1];\n const oz = pos[o3 + 2];\n\n let ax = -s * ox;\n let ay = -s * oy;\n let az = -s * oz;\n\n let sumx = 0;\n let sumy = 0;\n let sumz = 0;\n let cnt = 0;\n if (i > 0) {\n const n = (idx - 1) * 3;\n sumx += pos[n];\n sumy += pos[n + 1];\n sumz += pos[n + 2];\n cnt++;\n }\n if (i < N - 1) {\n const n = (idx + 1) * 3;\n sumx += pos[n];\n sumy += pos[n + 1];\n sumz += pos[n + 2];\n cnt++;\n }\n if (j > 0) {\n const n = (idx - N) * 3;\n sumx += pos[n];\n sumy += pos[n + 1];\n sumz += pos[n + 2];\n cnt++;\n }\n if (j < N - 1) {\n const n = (idx + N) * 3;\n sumx += pos[n];\n sumy += pos[n + 1];\n sumz += pos[n + 2];\n cnt++;\n }\n ax += coupling * (sumx - cnt * ox);\n ay += coupling * (sumy - cnt * oy);\n az += coupling * (sumz - cnt * oz);\n\n if (active) {\n const dx = pointer.x - (baseX[idx] + ox);\n const dy = pointer.y - (baseY[idx] + oy);\n const d = Math.sqrt(dx * dx + dy * dy);\n const tnorm = d * invR;\n if (tnorm < 1) {\n const zBump = 1 - tnorm * tnorm;\n az += force * zBump * zBump * 6.0;\n if (d > 1e-4) {\n const pinch = tnorm * (1 - tnorm) * (1 - tnorm) * 6.75;\n const dir = (force * pinch * 1.6) / d;\n ax += dx * dir;\n ay += dy * dir;\n }\n }\n }\n\n accel[o3] = ax;\n accel[o3 + 1] = ay;\n accel[o3 + 2] = az;\n }\n }\n\n for (let k = 0; k < nodeCount; k++) {\n const o3 = k * 3;\n const nvx = (vel[o3] + accel[o3]) * retain;\n const nvy = (vel[o3 + 1] + accel[o3 + 1]) * retain;\n const nvz = (vel[o3 + 2] + accel[o3 + 2]) * retain;\n vel[o3] = nvx;\n vel[o3 + 1] = nvy;\n vel[o3 + 2] = nvz;\n\n let px = pos[o3] + nvx;\n let py = pos[o3 + 1] + nvy;\n let pz = pos[o3 + 2] + nvz;\n if (px > 1.2) px = 1.2;\n else if (px < -1.2) px = -1.2;\n if (py > 1.2) py = 1.2;\n else if (py < -1.2) py = -1.2;\n if (pz > 1.2) pz = 1.2;\n else if (pz < -1.2) pz = -1.2;\n pos[o3] = px;\n pos[o3 + 1] = py;\n pos[o3 + 2] = pz;\n }\n }\n\n function commit() {\n maxOffset = 0;\n maxVel = 0;\n for (let j = 0; j < N; j++) {\n for (let i = 0; i < N; i++) {\n const idx = j * N + i;\n const o3 = idx * 3;\n const iL = i > 0 ? idx - 1 : idx;\n const iR = i < N - 1 ? idx + 1 : idx;\n const iD = j > 0 ? idx - N : idx;\n const iU = j < N - 1 ? idx + N : idx;\n\n const lx = baseX[iL] + pos[iL * 3];\n const ly = baseY[iL] + pos[iL * 3 + 1];\n const lz = pos[iL * 3 + 2];\n const rx = baseX[iR] + pos[iR * 3];\n const ry = baseY[iR] + pos[iR * 3 + 1];\n const rz = pos[iR * 3 + 2];\n const dx = baseX[iD] + pos[iD * 3];\n const dy = baseY[iD] + pos[iD * 3 + 1];\n const dz = pos[iD * 3 + 2];\n const ux = baseX[iU] + pos[iU * 3];\n const uy = baseY[iU] + pos[iU * 3 + 1];\n const uz = pos[iU * 3 + 2];\n\n const txx = rx - lx;\n const txy = ry - ly;\n const txz = rz - lz;\n const tyx = ux - dx;\n const tyy = uy - dy;\n const tyz = uz - dz;\n\n let nx = txy * tyz - txz * tyy;\n let ny = txz * tyx - txx * tyz;\n let nz = txx * tyy - txy * tyx;\n if (nz < 0) {\n nx = -nx;\n ny = -ny;\n nz = -nz;\n }\n const len = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;\n aNormal[o3] = nx / len;\n aNormal[o3 + 1] = ny / len;\n aNormal[o3 + 2] = nz / len;\n\n aOffset[o3] = pos[o3];\n aOffset[o3 + 1] = pos[o3 + 1];\n aOffset[o3 + 2] = pos[o3 + 2];\n\n const om = Math.abs(pos[o3]) + Math.abs(pos[o3 + 1]) + Math.abs(pos[o3 + 2]);\n if (om > maxOffset) maxOffset = om;\n const vm = Math.abs(vel[o3]) + Math.abs(vel[o3 + 1]) + Math.abs(vel[o3 + 2]);\n if (vm > maxVel) maxVel = vm;\n }\n }\n geometry.attributes.aOffset.needsUpdate = true;\n geometry.attributes.aNormal.needsUpdate = true;\n }\n\n let raf = 0;\n function frame(now) {\n raf = requestAnimationFrame(frame);\n const p = propsRef.current;\n\n program.uniforms.uShading.value = p.shading;\n program.uniforms.uRadius.value = p.borderRadius;\n program.uniforms.uTilt.value = (p.tilt * Math.PI) / 180;\n program.uniforms.uColor1.value = hexToRgb(p.color1);\n program.uniforms.uColor2.value = hexToRgb(p.color2);\n program.uniforms.uHighlight.value = hexToRgb(p.highlight);\n program.uniforms.uGrid.value = p.showGrid ? 1 : 0;\n program.uniforms.uGridDensity.value = p.gridDensity;\n program.uniforms.uGridOpacity.value = p.gridOpacity;\n program.uniforms.uGridColor.value = hexToRgb(p.gridColor);\n\n let dt = (now - last) / 1000;\n last = now;\n if (dt > 0.25) dt = 0.25;\n\n const tau = 0.06;\n const kLerp = 1 - Math.exp(-Math.max(dt, 1e-4) / tau);\n pointer.x += (pointer.tx - pointer.x) * kLerp;\n pointer.y += (pointer.ty - pointer.y) * kLerp;\n pointer.active = pointer.targetActive;\n\n accTime += dt;\n let sub = 0;\n while (accTime >= STEP && sub < MAX_SUB) {\n substep();\n accTime -= STEP;\n sub++;\n }\n if (accTime > STEP) accTime = 0;\n\n commit();\n renderer.render({ scene: mesh });\n }\n raf = requestAnimationFrame(frame);\n\n container.appendChild(gl.canvas);\n\n return () => {\n cancelAnimationFrame(raf);\n ro.disconnect();\n container.removeEventListener('mousemove', onMove);\n container.removeEventListener('mouseenter', onEnter);\n container.removeEventListener('mouseleave', onLeave);\n container.removeEventListener('mousedown', onDown);\n window.removeEventListener('mouseup', onUp);\n container.removeEventListener('touchstart', onTouch);\n container.removeEventListener('touchmove', onTouch);\n container.removeEventListener('touchend', onLeave);\n if (gl.canvas.parentElement === container) container.removeChild(gl.canvas);\n const lose = gl.getExtension('WEBGL_lose_context');\n if (lose) lose.loseContext();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [image, resolution]);\n\n return (\n
\n );\n};\n\nexport default ElasticMesh;\n" } ], diff --git a/public/r/ElasticMesh-TS-CSS.json b/public/r/ElasticMesh-TS-CSS.json index 157a1f6d..babadb62 100644 --- a/public/r/ElasticMesh-TS-CSS.json +++ b/public/r/ElasticMesh-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "ElasticMesh/ElasticMesh.css", + "type": "registry:file", + "path": "ElasticMesh.css", + "target": "@components/ElasticMesh.css", "content": ".elastic-mesh {\n position: relative;\n width: 100%;\n height: 100%;\n min-height: 0;\n touch-action: none;\n}\n\n.elastic-mesh canvas {\n display: block;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "ElasticMesh/ElasticMesh.tsx", + "path": "ElasticMesh.tsx", "content": "import { useEffect, useRef } from 'react';\nimport type { CSSProperties } from 'react';\nimport { Renderer, Geometry, Program, Mesh, Texture } from 'ogl';\n\nimport './ElasticMesh.css';\n\nconst DIST = 4.6;\nconst FIT = 0.82;\n\nconst VERT = `\nprecision highp float;\nattribute vec2 aGrid;\nattribute vec2 uv;\nattribute vec3 aOffset;\nattribute vec3 aNormal;\n\nuniform float uAspect;\nuniform float uTilt;\nuniform float uDist;\nuniform float uFit;\n\nvarying vec2 vUv;\nvarying vec3 vNormal;\nvarying float vDepth;\n\nvoid main() {\n vUv = uv;\n\n vec2 base = vec2((aGrid.x * 2.0 - 1.0) * uAspect, 1.0 - aGrid.y * 2.0);\n vec3 p = vec3(base + aOffset.xy, aOffset.z);\n\n float ct = cos(uTilt);\n float st = sin(uTilt);\n float ry = p.y * ct - p.z * st;\n float rz = p.y * st + p.z * ct;\n p.y = ry;\n p.z = rz;\n\n float persp = uDist / (uDist - p.z);\n vec2 clip = vec2(p.x / uAspect, p.y) * persp * uFit;\n\n vNormal = aNormal;\n vDepth = aOffset.z;\n gl_Position = vec4(clip, 0.0, 1.0);\n}\n`;\n\nconst FRAG = `\nprecision highp float;\n\nvarying vec2 vUv;\nvarying vec3 vNormal;\nvarying float vDepth;\n\nuniform sampler2D tMap;\nuniform float uHasImage;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uHighlight;\nuniform float uShading;\nuniform vec2 uRes;\nuniform float uRadius;\nuniform float uGrid;\nuniform float uGridDensity;\nuniform float uGridOpacity;\nuniform vec3 uGridColor;\n\nvoid main() {\n vec3 base;\n if (uHasImage > 0.5) {\n base = texture2D(tMap, vUv).rgb;\n } else {\n base = mix(uColor1, uColor2, clamp(vUv.y, 0.0, 1.0));\n }\n\n vec3 N = normalize(vNormal);\n vec3 L = normalize(vec3(-0.35, 0.55, 0.78));\n vec3 V = vec3(0.0, 0.0, 1.0);\n vec3 H = normalize(L + V);\n\n float diff = clamp(dot(N, L), 0.0, 1.0);\n float specRaw = pow(clamp(dot(N, H), 0.0, 1.0), 26.0);\n float specFlat = pow(clamp(H.z, 0.0, 1.0), 26.0);\n float spec = clamp((specRaw - specFlat) / (1.0 - specFlat), 0.0, 1.0);\n float ao = clamp(1.0 + vDepth * 0.45, 0.65, 1.25);\n\n vec3 lit = base * (1.0 - uShading * 0.28);\n lit += base * diff * uShading * 0.55;\n lit *= ao;\n lit += uHighlight * spec * uShading * 0.25;\n\n if (uGrid > 0.5) {\n vec2 g = vUv * uGridDensity;\n vec2 w = uGridDensity / max(uRes, vec2(1.0));\n vec2 d = abs(fract(g - 0.5) - 0.5) / max(w * 1.5, vec2(1e-4));\n float line = 1.0 - clamp(min(d.x, d.y), 0.0, 1.0);\n lit = mix(lit, uGridColor, line * uGridOpacity * (0.45 + diff * 0.55));\n }\n\n vec2 p = (vUv - 0.5) * uRes;\n vec2 halfRes = uRes * 0.5;\n float r = min(uRadius, min(halfRes.x, halfRes.y));\n vec2 q = abs(p) - (halfRes - r);\n float sd = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;\n float alpha = 1.0 - smoothstep(-1.25, 1.25, sd);\n if (alpha <= 0.002) discard;\n\n gl_FragColor = vec4(lit, alpha);\n}\n`;\n\nfunction hexToRgb(hex: string): [number, number, number] {\n let h = (hex || '').replace('#', '').trim();\n if (h.length === 3)\n h = h\n .split('')\n .map(c => c + c)\n .join('');\n const n = parseInt(h || '000000', 16);\n return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\nexport interface ElasticMeshProps {\n image?: string;\n color1?: string;\n color2?: string;\n highlight?: string;\n showGrid?: boolean;\n gridDensity?: number;\n gridOpacity?: number;\n gridColor?: string;\n borderRadius?: number;\n stiffness?: number;\n damping?: number;\n grabRadius?: number;\n pull?: number;\n wobble?: number;\n tilt?: number;\n shading?: number;\n resolution?: number;\n interaction?: 'hover' | 'drag';\n enabled?: boolean;\n className?: string;\n style?: CSSProperties;\n [key: string]: unknown;\n}\n\nconst ElasticMesh = ({\n image = '',\n color1 = '#5227FF',\n color2 = '#B19EEF',\n highlight = '#ffffff',\n showGrid = true,\n gridDensity = 20,\n gridOpacity = 0.28,\n gridColor = '#ffffff',\n borderRadius = 25,\n stiffness = 0.05,\n damping = 0.2,\n grabRadius = 0.6,\n pull = 0.4,\n wobble = 5,\n tilt = 14,\n shading = 0.5,\n resolution = 25,\n interaction = 'hover',\n enabled = true,\n className = '',\n style,\n ...rest\n}: ElasticMeshProps) => {\n const containerRef = useRef(null);\n\n const propsRef = useRef>({});\n propsRef.current = {\n color1,\n color2,\n highlight,\n showGrid,\n gridDensity,\n gridOpacity,\n gridColor,\n borderRadius,\n stiffness,\n damping,\n grabRadius,\n pull,\n wobble,\n tilt,\n shading,\n interaction,\n enabled\n };\n\n useEffect(() => {\n const container = containerRef.current as HTMLDivElement;\n if (!container) return;\n\n const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n const renderer = new Renderer({ alpha: true, antialias: true, dpr: Math.min(window.devicePixelRatio || 1, 2) });\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n gl.enable(gl.BLEND);\n gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\n\n const N = Math.max(6, Math.min(40, Math.round(resolution)));\n const nodeCount = N * N;\n\n const aGrid = new Float32Array(nodeCount * 2);\n const uv = new Float32Array(nodeCount * 2);\n const aOffset = new Float32Array(nodeCount * 3);\n const aNormal = new Float32Array(nodeCount * 3);\n\n for (let j = 0; j < N; j++) {\n for (let i = 0; i < N; i++) {\n const idx = j * N + i;\n const u = i / (N - 1);\n const v = j / (N - 1);\n aGrid[idx * 2] = u;\n aGrid[idx * 2 + 1] = v;\n uv[idx * 2] = u;\n uv[idx * 2 + 1] = v;\n aNormal[idx * 3 + 2] = 1;\n }\n }\n\n const quads = (N - 1) * (N - 1);\n const index = new Uint16Array(quads * 6);\n let t = 0;\n for (let j = 0; j < N - 1; j++) {\n for (let i = 0; i < N - 1; i++) {\n const a = j * N + i;\n const b = a + 1;\n const c = a + N;\n const d = c + 1;\n index[t++] = a;\n index[t++] = c;\n index[t++] = b;\n index[t++] = b;\n index[t++] = c;\n index[t++] = d;\n }\n }\n\n const geometry = new Geometry(gl, {\n aGrid: { size: 2, data: aGrid },\n uv: { size: 2, data: uv },\n aOffset: { size: 3, data: aOffset },\n aNormal: { size: 3, data: aNormal },\n index: { data: index }\n });\n\n const texture = new Texture(gl, { generateMipmaps: false, flipY: false });\n let hasImage = 0;\n if (image) {\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.src = image;\n img.onload = () => {\n texture.image = img;\n program.uniforms.uHasImage.value = 1;\n };\n }\n\n const program = new Program(gl, {\n vertex: VERT,\n fragment: FRAG,\n transparent: true,\n cullFace: null,\n uniforms: {\n tMap: { value: texture },\n uHasImage: { value: hasImage },\n uColor1: { value: hexToRgb(color1) },\n uColor2: { value: hexToRgb(color2) },\n uHighlight: { value: hexToRgb(highlight) },\n uGrid: { value: showGrid ? 1 : 0 },\n uGridDensity: { value: gridDensity },\n uGridOpacity: { value: gridOpacity },\n uGridColor: { value: hexToRgb(gridColor) },\n uShading: { value: shading },\n uRes: { value: [1, 1] },\n uRadius: { value: borderRadius },\n uAspect: { value: 1 },\n uTilt: { value: (tilt * Math.PI) / 180 },\n uDist: { value: DIST },\n uFit: { value: FIT }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n\n const baseX = new Float32Array(nodeCount);\n const baseY = new Float32Array(nodeCount);\n const pos = new Float32Array(nodeCount * 3);\n const vel = new Float32Array(nodeCount * 3);\n const accel = new Float32Array(nodeCount * 3);\n\n let aspect = 1;\n function refreshBase() {\n for (let idx = 0; idx < nodeCount; idx++) {\n baseX[idx] = (aGrid[idx * 2] * 2 - 1) * aspect;\n baseY[idx] = 1 - aGrid[idx * 2 + 1] * 2;\n }\n }\n\n function resize() {\n const w = container.offsetWidth || 1;\n const h = container.offsetHeight || 1;\n renderer.setSize(w, h);\n aspect = w / h;\n program.uniforms.uAspect.value = aspect;\n program.uniforms.uRes.value = [w, h];\n refreshBase();\n }\n\n const ro = new ResizeObserver(resize);\n ro.observe(container);\n resize();\n\n const pointer = { x: 0, y: 0, tx: 0, ty: 0, active: false, targetActive: false };\n\n function toPlane(clientX: number, clientY: number) {\n const rect = container.getBoundingClientRect();\n const mx = (clientX - rect.left) / rect.width;\n const my = (clientY - rect.top) / rect.height;\n const clipX = mx * 2 - 1;\n const clipY = 1 - my * 2;\n const t = ((propsRef.current.tilt || 0) * Math.PI) / 180;\n const ct = Math.cos(t);\n const st = Math.sin(t);\n const a = clipY / (ct * FIT * DIST);\n const py = (a * DIST) / (1 + a * st);\n const persp = DIST / (DIST - py * st);\n pointer.tx = (clipX * aspect) / (persp * FIT);\n pointer.ty = py;\n }\n\n function onMove(e: MouseEvent) {\n toPlane(e.clientX, e.clientY);\n if (propsRef.current.interaction === 'hover') pointer.targetActive = true;\n }\n function onEnter() {\n if (propsRef.current.interaction === 'hover') pointer.targetActive = true;\n }\n function onLeave() {\n pointer.targetActive = false;\n }\n function onDown(e: MouseEvent) {\n if (propsRef.current.interaction === 'drag') {\n toPlane(e.clientX, e.clientY);\n pointer.x = pointer.tx;\n pointer.y = pointer.ty;\n pointer.targetActive = true;\n }\n }\n function onUp() {\n if (propsRef.current.interaction === 'drag') pointer.targetActive = false;\n }\n function onTouch(e: TouchEvent) {\n if (e.touches.length) {\n toPlane(e.touches[0].clientX, e.touches[0].clientY);\n pointer.targetActive = true;\n }\n }\n\n container.addEventListener('mousemove', onMove);\n container.addEventListener('mouseenter', onEnter);\n container.addEventListener('mouseleave', onLeave);\n container.addEventListener('mousedown', onDown);\n window.addEventListener('mouseup', onUp);\n container.addEventListener('touchstart', onTouch, { passive: true });\n container.addEventListener('touchmove', onTouch, { passive: true });\n container.addEventListener('touchend', onLeave);\n\n const STEP = 1 / 120;\n const MAX_SUB = 5;\n let accTime = 0;\n let last = performance.now();\n let maxOffset = 0;\n let maxVel = 0;\n\n function substep() {\n const p = propsRef.current;\n const s = p.stiffness;\n const retain = 1 - p.damping;\n const coupling = 0.06 + p.wobble * 0.032;\n const active = pointer.active && p.enabled && !reduceMotion;\n const r = Math.max(0.08, p.grabRadius) * 1.4;\n const invR = 1 / r;\n const force = p.pull * 0.009;\n\n for (let j = 0; j < N; j++) {\n for (let i = 0; i < N; i++) {\n const idx = j * N + i;\n const o3 = idx * 3;\n const ox = pos[o3];\n const oy = pos[o3 + 1];\n const oz = pos[o3 + 2];\n\n let ax = -s * ox;\n let ay = -s * oy;\n let az = -s * oz;\n\n let sumx = 0;\n let sumy = 0;\n let sumz = 0;\n let cnt = 0;\n if (i > 0) {\n const n = (idx - 1) * 3;\n sumx += pos[n];\n sumy += pos[n + 1];\n sumz += pos[n + 2];\n cnt++;\n }\n if (i < N - 1) {\n const n = (idx + 1) * 3;\n sumx += pos[n];\n sumy += pos[n + 1];\n sumz += pos[n + 2];\n cnt++;\n }\n if (j > 0) {\n const n = (idx - N) * 3;\n sumx += pos[n];\n sumy += pos[n + 1];\n sumz += pos[n + 2];\n cnt++;\n }\n if (j < N - 1) {\n const n = (idx + N) * 3;\n sumx += pos[n];\n sumy += pos[n + 1];\n sumz += pos[n + 2];\n cnt++;\n }\n ax += coupling * (sumx - cnt * ox);\n ay += coupling * (sumy - cnt * oy);\n az += coupling * (sumz - cnt * oz);\n\n if (active) {\n const dx = pointer.x - (baseX[idx] + ox);\n const dy = pointer.y - (baseY[idx] + oy);\n const d = Math.sqrt(dx * dx + dy * dy);\n const tnorm = d * invR;\n if (tnorm < 1) {\n const zBump = 1 - tnorm * tnorm;\n az += force * zBump * zBump * 6.0;\n if (d > 1e-4) {\n const pinch = tnorm * (1 - tnorm) * (1 - tnorm) * 6.75;\n const dir = (force * pinch * 1.6) / d;\n ax += dx * dir;\n ay += dy * dir;\n }\n }\n }\n\n accel[o3] = ax;\n accel[o3 + 1] = ay;\n accel[o3 + 2] = az;\n }\n }\n\n for (let k = 0; k < nodeCount; k++) {\n const o3 = k * 3;\n const nvx = (vel[o3] + accel[o3]) * retain;\n const nvy = (vel[o3 + 1] + accel[o3 + 1]) * retain;\n const nvz = (vel[o3 + 2] + accel[o3 + 2]) * retain;\n vel[o3] = nvx;\n vel[o3 + 1] = nvy;\n vel[o3 + 2] = nvz;\n\n let px = pos[o3] + nvx;\n let py = pos[o3 + 1] + nvy;\n let pz = pos[o3 + 2] + nvz;\n if (px > 1.2) px = 1.2;\n else if (px < -1.2) px = -1.2;\n if (py > 1.2) py = 1.2;\n else if (py < -1.2) py = -1.2;\n if (pz > 1.2) pz = 1.2;\n else if (pz < -1.2) pz = -1.2;\n pos[o3] = px;\n pos[o3 + 1] = py;\n pos[o3 + 2] = pz;\n }\n }\n\n function commit() {\n maxOffset = 0;\n maxVel = 0;\n for (let j = 0; j < N; j++) {\n for (let i = 0; i < N; i++) {\n const idx = j * N + i;\n const o3 = idx * 3;\n const iL = i > 0 ? idx - 1 : idx;\n const iR = i < N - 1 ? idx + 1 : idx;\n const iD = j > 0 ? idx - N : idx;\n const iU = j < N - 1 ? idx + N : idx;\n\n const lx = baseX[iL] + pos[iL * 3];\n const ly = baseY[iL] + pos[iL * 3 + 1];\n const lz = pos[iL * 3 + 2];\n const rx = baseX[iR] + pos[iR * 3];\n const ry = baseY[iR] + pos[iR * 3 + 1];\n const rz = pos[iR * 3 + 2];\n const dx = baseX[iD] + pos[iD * 3];\n const dy = baseY[iD] + pos[iD * 3 + 1];\n const dz = pos[iD * 3 + 2];\n const ux = baseX[iU] + pos[iU * 3];\n const uy = baseY[iU] + pos[iU * 3 + 1];\n const uz = pos[iU * 3 + 2];\n\n const txx = rx - lx;\n const txy = ry - ly;\n const txz = rz - lz;\n const tyx = ux - dx;\n const tyy = uy - dy;\n const tyz = uz - dz;\n\n let nx = txy * tyz - txz * tyy;\n let ny = txz * tyx - txx * tyz;\n let nz = txx * tyy - txy * tyx;\n if (nz < 0) {\n nx = -nx;\n ny = -ny;\n nz = -nz;\n }\n const len = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;\n aNormal[o3] = nx / len;\n aNormal[o3 + 1] = ny / len;\n aNormal[o3 + 2] = nz / len;\n\n aOffset[o3] = pos[o3];\n aOffset[o3 + 1] = pos[o3 + 1];\n aOffset[o3 + 2] = pos[o3 + 2];\n\n const om = Math.abs(pos[o3]) + Math.abs(pos[o3 + 1]) + Math.abs(pos[o3 + 2]);\n if (om > maxOffset) maxOffset = om;\n const vm = Math.abs(vel[o3]) + Math.abs(vel[o3 + 1]) + Math.abs(vel[o3 + 2]);\n if (vm > maxVel) maxVel = vm;\n }\n }\n geometry.attributes.aOffset.needsUpdate = true;\n geometry.attributes.aNormal.needsUpdate = true;\n }\n\n let raf = 0;\n function frame(now: number) {\n raf = requestAnimationFrame(frame);\n const p = propsRef.current;\n\n program.uniforms.uShading.value = p.shading;\n program.uniforms.uRadius.value = p.borderRadius;\n program.uniforms.uTilt.value = (p.tilt * Math.PI) / 180;\n program.uniforms.uColor1.value = hexToRgb(p.color1);\n program.uniforms.uColor2.value = hexToRgb(p.color2);\n program.uniforms.uHighlight.value = hexToRgb(p.highlight);\n program.uniforms.uGrid.value = p.showGrid ? 1 : 0;\n program.uniforms.uGridDensity.value = p.gridDensity;\n program.uniforms.uGridOpacity.value = p.gridOpacity;\n program.uniforms.uGridColor.value = hexToRgb(p.gridColor);\n\n let dt = (now - last) / 1000;\n last = now;\n if (dt > 0.25) dt = 0.25;\n\n const tau = 0.06;\n const kLerp = 1 - Math.exp(-Math.max(dt, 1e-4) / tau);\n pointer.x += (pointer.tx - pointer.x) * kLerp;\n pointer.y += (pointer.ty - pointer.y) * kLerp;\n pointer.active = pointer.targetActive;\n\n accTime += dt;\n let sub = 0;\n while (accTime >= STEP && sub < MAX_SUB) {\n substep();\n accTime -= STEP;\n sub++;\n }\n if (accTime > STEP) accTime = 0;\n\n commit();\n renderer.render({ scene: mesh });\n }\n raf = requestAnimationFrame(frame);\n\n container.appendChild(gl.canvas);\n\n return () => {\n cancelAnimationFrame(raf);\n ro.disconnect();\n container.removeEventListener('mousemove', onMove);\n container.removeEventListener('mouseenter', onEnter);\n container.removeEventListener('mouseleave', onLeave);\n container.removeEventListener('mousedown', onDown);\n window.removeEventListener('mouseup', onUp);\n container.removeEventListener('touchstart', onTouch);\n container.removeEventListener('touchmove', onTouch);\n container.removeEventListener('touchend', onLeave);\n if (gl.canvas.parentElement === container) container.removeChild(gl.canvas);\n const lose = gl.getExtension('WEBGL_lose_context');\n if (lose) lose.loseContext();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [image, resolution]);\n\n return (\n
\n );\n};\n\nexport default ElasticMesh;\n" } ], diff --git a/public/r/ElasticSlider-JS-CSS.json b/public/r/ElasticSlider-JS-CSS.json index b6ec8f41..5cd236d2 100644 --- a/public/r/ElasticSlider-JS-CSS.json +++ b/public/r/ElasticSlider-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "ElasticSlider/ElasticSlider.css", + "type": "registry:file", + "path": "ElasticSlider.css", + "target": "@components/ElasticSlider.css", "content": ".slider-container {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 1rem;\n width: 12rem;\n}\n\n.slider-wrapper {\n display: flex;\n width: 100%;\n touch-action: none;\n user-select: none;\n align-items: center;\n justify-content: center;\n gap: 1rem;\n}\n\n.slider-root {\n position: relative;\n display: flex;\n width: 100%;\n max-width: 200px;\n flex-grow: 1;\n cursor: grab;\n touch-action: none;\n user-select: none;\n align-items: center;\n padding: 1rem 0;\n}\n\n.slider-root:active {\n cursor: grabbing;\n}\n\n.slider-track-wrapper {\n display: flex;\n flex-grow: 1;\n}\n\n.slider-track {\n position: relative;\n height: 100%;\n flex-grow: 1;\n overflow: hidden;\n border-radius: 9999px;\n background-color: rgba(128, 128, 128, 0.4);\n}\n\n.slider-range {\n position: absolute;\n height: 100%;\n background-color: #888;\n border-radius: 9999px;\n}\n\n.value-indicator {\n color: #808080;\n position: absolute;\n transform: translateY(-1rem);\n font-size: 0.75rem;\n font-weight: 500;\n letter-spacing: 0.05em;\n}\n\n.icon {\n width: 24px;\n height: 24px;\n color: #888;\n}\n\n.icon.dark {\n color: #ddd;\n}\n" }, { "type": "registry:component", - "path": "ElasticSlider/ElasticSlider.jsx", + "path": "ElasticSlider.jsx", "content": "import { animate, motion, useMotionValue, useMotionValueEvent, useTransform } from 'motion/react';\nimport { useEffect, useRef, useState } from 'react';\nimport { Icon } from '@chakra-ui/react';\nimport { RiVolumeDownFill, RiVolumeUpFill } from 'react-icons/ri';\n\nimport './ElasticSlider.css';\n\nconst MAX_OVERFLOW = 50;\n\nexport default function ElasticSlider({\n defaultValue = 50,\n startingValue = 0,\n maxValue = 100,\n className = '',\n isStepped = false,\n stepSize = 1,\n leftIcon = ,\n rightIcon = \n}) {\n return (\n
\n \n
\n );\n}\n\nfunction Slider({ defaultValue, startingValue, maxValue, isStepped, stepSize, leftIcon, rightIcon }) {\n const [value, setValue] = useState(defaultValue);\n const sliderRef = useRef(null);\n const [region, setRegion] = useState('middle');\n const clientX = useMotionValue(0);\n const overflow = useMotionValue(0);\n const scale = useMotionValue(1);\n\n useEffect(() => {\n setValue(defaultValue);\n }, [defaultValue]);\n\n useMotionValueEvent(clientX, 'change', latest => {\n if (sliderRef.current) {\n const { left, right } = sliderRef.current.getBoundingClientRect();\n let newValue;\n\n if (latest < left) {\n setRegion('left');\n newValue = left - latest;\n } else if (latest > right) {\n setRegion('right');\n newValue = latest - right;\n } else {\n setRegion('middle');\n newValue = 0;\n }\n\n overflow.jump(decay(newValue, MAX_OVERFLOW));\n }\n });\n\n const handlePointerMove = e => {\n if (e.buttons > 0 && sliderRef.current) {\n const { left, width } = sliderRef.current.getBoundingClientRect();\n let newValue = startingValue + ((e.clientX - left) / width) * (maxValue - startingValue);\n\n if (isStepped) {\n newValue = Math.round(newValue / stepSize) * stepSize;\n }\n\n newValue = Math.min(Math.max(newValue, startingValue), maxValue);\n setValue(newValue);\n clientX.jump(e.clientX);\n }\n };\n\n const handlePointerDown = e => {\n handlePointerMove(e);\n e.currentTarget.setPointerCapture(e.pointerId);\n };\n\n const handlePointerUp = () => {\n animate(overflow, 0, { type: 'spring', bounce: 0.5 });\n };\n\n const getRangePercentage = () => {\n const totalRange = maxValue - startingValue;\n if (totalRange === 0) return 0;\n\n return ((value - startingValue) / totalRange) * 100;\n };\n\n return (\n <>\n animate(scale, 1.2)}\n onHoverEnd={() => animate(scale, 1)}\n onTouchStart={() => animate(scale, 1.2)}\n onTouchEnd={() => animate(scale, 1)}\n style={{\n scale,\n opacity: useTransform(scale, [1, 1.2], [0.7, 1])\n }}\n className=\"slider-wrapper\"\n >\n (region === 'left' ? -overflow.get() / scale.get() : 0))\n }}\n >\n {leftIcon}\n \n\n \n {\n if (sliderRef.current) {\n const { width } = sliderRef.current.getBoundingClientRect();\n return 1 + overflow.get() / width;\n }\n }),\n scaleY: useTransform(overflow, [0, MAX_OVERFLOW], [1, 0.8]),\n transformOrigin: useTransform(() => {\n if (sliderRef.current) {\n const { left, width } = sliderRef.current.getBoundingClientRect();\n return clientX.get() < left + width / 2 ? 'right' : 'left';\n }\n }),\n height: useTransform(scale, [1, 1.2], [6, 12]),\n marginTop: useTransform(scale, [1, 1.2], [0, -3]),\n marginBottom: useTransform(scale, [1, 1.2], [0, -3])\n }}\n className=\"slider-track-wrapper\"\n >\n
\n
\n
\n \n
\n\n (region === 'right' ? overflow.get() / scale.get() : 0))\n }}\n >\n {rightIcon}\n \n \n

{Math.round(value)}

\n \n );\n}\n\nfunction decay(value, max) {\n if (max === 0) {\n return 0;\n }\n\n const entry = value / max;\n const sigmoid = 2 * (1 / (1 + Math.exp(-entry)) - 0.5);\n\n return sigmoid * max;\n}\n" } ], diff --git a/public/r/ElasticSlider-TS-CSS.json b/public/r/ElasticSlider-TS-CSS.json index b6a4d44b..7921e8de 100644 --- a/public/r/ElasticSlider-TS-CSS.json +++ b/public/r/ElasticSlider-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "ElasticSlider/ElasticSlider.css", + "type": "registry:file", + "path": "ElasticSlider.css", + "target": "@components/ElasticSlider.css", "content": ".slider-container {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 1rem;\n width: 12rem;\n}\n\n.slider-wrapper {\n display: flex;\n width: 100%;\n touch-action: none;\n user-select: none;\n align-items: center;\n justify-content: center;\n gap: 1rem;\n}\n\n.slider-root {\n position: relative;\n display: flex;\n width: 100%;\n max-width: 200px;\n flex-grow: 1;\n cursor: grab;\n touch-action: none;\n user-select: none;\n align-items: center;\n padding: 1rem 0;\n}\n\n.slider-root:active {\n cursor: grabbing;\n}\n\n.slider-track-wrapper {\n display: flex;\n flex-grow: 1;\n}\n\n.slider-track {\n position: relative;\n height: 100%;\n flex-grow: 1;\n overflow: hidden;\n border-radius: 9999px;\n background-color: rgba(128, 128, 128, 0.4);\n}\n\n.slider-range {\n position: absolute;\n height: 100%;\n background-color: #888;\n border-radius: 9999px;\n}\n\n.value-indicator {\n color: #808080;\n position: absolute;\n transform: translateY(-1rem);\n font-size: 0.75rem;\n font-weight: 500;\n letter-spacing: 0.05em;\n}\n\n.icon {\n width: 24px;\n height: 24px;\n color: #888;\n}\n\n.icon.dark {\n color: #ddd;\n}\n" }, { "type": "registry:component", - "path": "ElasticSlider/ElasticSlider.tsx", + "path": "ElasticSlider.tsx", "content": "import React, { useEffect, useRef, useState } from 'react';\nimport { animate, motion, useMotionValue, useMotionValueEvent, useTransform } from 'motion/react';\nimport { Icon } from '@chakra-ui/react';\nimport { RiVolumeDownFill, RiVolumeUpFill } from 'react-icons/ri';\n\nimport './ElasticSlider.css';\n\nconst MAX_OVERFLOW = 50;\n\ninterface ElasticSliderProps {\n defaultValue?: number;\n startingValue?: number;\n maxValue?: number;\n className?: string;\n isStepped?: boolean;\n stepSize?: number;\n leftIcon?: React.ReactNode;\n rightIcon?: React.ReactNode;\n}\n\nconst ElasticSlider: React.FC = ({\n defaultValue = 50,\n startingValue = 0,\n maxValue = 100,\n className = '',\n isStepped = false,\n stepSize = 1,\n leftIcon = ,\n rightIcon = \n}) => {\n return (\n
\n \n
\n );\n};\n\ninterface SliderProps {\n defaultValue: number;\n startingValue: number;\n maxValue: number;\n isStepped: boolean;\n stepSize: number;\n leftIcon: React.ReactNode;\n rightIcon: React.ReactNode;\n}\n\nconst Slider: React.FC = ({\n defaultValue,\n startingValue,\n maxValue,\n isStepped,\n stepSize,\n leftIcon,\n rightIcon\n}) => {\n const [value, setValue] = useState(defaultValue);\n const sliderRef = useRef(null);\n const [region, setRegion] = useState<'left' | 'middle' | 'right'>('middle');\n const clientX = useMotionValue(0);\n const overflow = useMotionValue(0);\n const scale = useMotionValue(1);\n\n useEffect(() => {\n setValue(defaultValue);\n }, [defaultValue]);\n\n useMotionValueEvent(clientX, 'change', (latest: number) => {\n if (sliderRef.current) {\n const { left, right } = sliderRef.current.getBoundingClientRect();\n let newValue: number;\n if (latest < left) {\n setRegion('left');\n newValue = left - latest;\n } else if (latest > right) {\n setRegion('right');\n newValue = latest - right;\n } else {\n setRegion('middle');\n newValue = 0;\n }\n overflow.jump(decay(newValue, MAX_OVERFLOW));\n }\n });\n\n const handlePointerMove = (e: React.PointerEvent) => {\n if (e.buttons > 0 && sliderRef.current) {\n const { left, width } = sliderRef.current.getBoundingClientRect();\n let newValue = startingValue + ((e.clientX - left) / width) * (maxValue - startingValue);\n if (isStepped) {\n newValue = Math.round(newValue / stepSize) * stepSize;\n }\n newValue = Math.min(Math.max(newValue, startingValue), maxValue);\n setValue(newValue);\n clientX.jump(e.clientX);\n }\n };\n\n const handlePointerDown = (e: React.PointerEvent) => {\n handlePointerMove(e);\n e.currentTarget.setPointerCapture(e.pointerId);\n };\n\n const handlePointerUp = () => {\n animate(overflow, 0, { type: 'spring', bounce: 0.5 });\n };\n\n const getRangePercentage = (): number => {\n const totalRange = maxValue - startingValue;\n if (totalRange === 0) return 0;\n return ((value - startingValue) / totalRange) * 100;\n };\n\n return (\n <>\n animate(scale, 1.2)}\n onHoverEnd={() => animate(scale, 1)}\n onTouchStart={() => animate(scale, 1.2)}\n onTouchEnd={() => animate(scale, 1)}\n style={{\n scale,\n opacity: useTransform(scale, [1, 1.2], [0.7, 1])\n }}\n className=\"slider-wrapper\"\n >\n (region === 'left' ? -overflow.get() / scale.get() : 0))\n }}\n >\n {leftIcon}\n \n\n \n {\n if (sliderRef.current) {\n const { width } = sliderRef.current.getBoundingClientRect();\n return 1 + overflow.get() / width;\n }\n return 1;\n }),\n scaleY: useTransform(overflow, [0, MAX_OVERFLOW], [1, 0.8]),\n transformOrigin: useTransform(() => {\n if (sliderRef.current) {\n const { left, width } = sliderRef.current.getBoundingClientRect();\n return clientX.get() < left + width / 2 ? 'right' : 'left';\n }\n return 'center';\n }),\n height: useTransform(scale, [1, 1.2], [6, 12]),\n marginTop: useTransform(scale, [1, 1.2], [0, -3]),\n marginBottom: useTransform(scale, [1, 1.2], [0, -3])\n }}\n className=\"slider-track-wrapper\"\n >\n
\n
\n
\n \n
\n\n (region === 'right' ? overflow.get() / scale.get() : 0))\n }}\n >\n {rightIcon}\n \n \n

{Math.round(value)}

\n \n );\n};\n\nfunction decay(value: number, max: number): number {\n if (max === 0) {\n return 0;\n }\n const entry = value / max;\n const sigmoid = 2 * (1 / (1 + Math.exp(-entry)) - 0.5);\n return sigmoid * max;\n}\n\nexport default ElasticSlider;\n" } ], diff --git a/public/r/ElectricBorder-JS-CSS.json b/public/r/ElectricBorder-JS-CSS.json index a9e0467a..5ddfcdd4 100644 --- a/public/r/ElectricBorder-JS-CSS.json +++ b/public/r/ElectricBorder-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "ElectricBorder/ElectricBorder.css", + "type": "registry:file", + "path": "ElectricBorder.css", + "target": "@components/ElectricBorder.css", "content": ".electric-border {\n --electric-light-color: oklch(from var(--electric-border-color) l c h);\n position: relative;\n border-radius: inherit;\n overflow: visible;\n isolation: isolate;\n}\n\n.eb-canvas-container {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n pointer-events: none;\n z-index: 2;\n}\n\n.eb-canvas {\n display: block;\n}\n\n.eb-content {\n position: relative;\n border-radius: inherit;\n z-index: 1;\n}\n\n.eb-layers {\n position: absolute;\n inset: 0;\n border-radius: inherit;\n pointer-events: none;\n z-index: 0;\n}\n\n.eb-glow-1,\n.eb-glow-2,\n.eb-background-glow {\n position: absolute;\n inset: 0;\n border-radius: inherit;\n pointer-events: none;\n box-sizing: border-box;\n}\n\n.eb-glow-1 {\n border: 2px solid oklch(from var(--electric-border-color) l c h / 0.6);\n filter: blur(1px);\n}\n\n.eb-glow-2 {\n border: 2px solid var(--electric-light-color);\n filter: blur(4px);\n}\n\n.eb-background-glow {\n z-index: -1;\n transform: scale(1.1);\n filter: blur(32px);\n opacity: 0.3;\n background: linear-gradient(-30deg, var(--electric-light-color), transparent, var(--electric-border-color));\n}\n" }, { "type": "registry:component", - "path": "ElectricBorder/ElectricBorder.jsx", + "path": "ElectricBorder.jsx", "content": "import { useEffect, useRef, useCallback } from 'react';\nimport './ElectricBorder.css';\n\nconst ElectricBorder = ({\n children,\n color = '#5227FF',\n speed = 1,\n chaos = 0.12,\n borderRadius = 24,\n className,\n style\n}) => {\n const canvasRef = useRef(null);\n const containerRef = useRef(null);\n const animationRef = useRef(null);\n const timeRef = useRef(0);\n const lastFrameTimeRef = useRef(0);\n\n // Noise functions\n const random = useCallback(x => {\n return (Math.sin(x * 12.9898) * 43758.5453) % 1;\n }, []);\n\n const noise2D = useCallback(\n (x, y) => {\n const i = Math.floor(x);\n const j = Math.floor(y);\n const fx = x - i;\n const fy = y - j;\n\n const a = random(i + j * 57);\n const b = random(i + 1 + j * 57);\n const c = random(i + (j + 1) * 57);\n const d = random(i + 1 + (j + 1) * 57);\n\n const ux = fx * fx * (3.0 - 2.0 * fx);\n const uy = fy * fy * (3.0 - 2.0 * fy);\n\n return a * (1 - ux) * (1 - uy) + b * ux * (1 - uy) + c * (1 - ux) * uy + d * ux * uy;\n },\n [random]\n );\n\n const octavedNoise = useCallback(\n (x, octaves, lacunarity, gain, baseAmplitude, baseFrequency, time, seed, baseFlatness) => {\n let y = 0;\n let amplitude = baseAmplitude;\n let frequency = baseFrequency;\n\n for (let i = 0; i < octaves; i++) {\n let octaveAmplitude = amplitude;\n if (i === 0) {\n octaveAmplitude *= baseFlatness;\n }\n y += octaveAmplitude * noise2D(frequency * x + seed * 100, time * frequency * 0.3);\n frequency *= lacunarity;\n amplitude *= gain;\n }\n\n return y;\n },\n [noise2D]\n );\n\n const getCornerPoint = useCallback((centerX, centerY, radius, startAngle, arcLength, progress) => {\n const angle = startAngle + progress * arcLength;\n return {\n x: centerX + radius * Math.cos(angle),\n y: centerY + radius * Math.sin(angle)\n };\n }, []);\n\n const getRoundedRectPoint = useCallback(\n (t, left, top, width, height, radius) => {\n const straightWidth = width - 2 * radius;\n const straightHeight = height - 2 * radius;\n const cornerArc = (Math.PI * radius) / 2;\n const totalPerimeter = 2 * straightWidth + 2 * straightHeight + 4 * cornerArc;\n const distance = t * totalPerimeter;\n\n let accumulated = 0;\n\n // Top edge\n if (distance <= accumulated + straightWidth) {\n const progress = (distance - accumulated) / straightWidth;\n return { x: left + radius + progress * straightWidth, y: top };\n }\n accumulated += straightWidth;\n\n // Top-right corner\n if (distance <= accumulated + cornerArc) {\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + width - radius, top + radius, radius, -Math.PI / 2, Math.PI / 2, progress);\n }\n accumulated += cornerArc;\n\n // Right edge\n if (distance <= accumulated + straightHeight) {\n const progress = (distance - accumulated) / straightHeight;\n return { x: left + width, y: top + radius + progress * straightHeight };\n }\n accumulated += straightHeight;\n\n // Bottom-right corner\n if (distance <= accumulated + cornerArc) {\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + width - radius, top + height - radius, radius, 0, Math.PI / 2, progress);\n }\n accumulated += cornerArc;\n\n // Bottom edge\n if (distance <= accumulated + straightWidth) {\n const progress = (distance - accumulated) / straightWidth;\n return { x: left + width - radius - progress * straightWidth, y: top + height };\n }\n accumulated += straightWidth;\n\n // Bottom-left corner\n if (distance <= accumulated + cornerArc) {\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + radius, top + height - radius, radius, Math.PI / 2, Math.PI / 2, progress);\n }\n accumulated += cornerArc;\n\n // Left edge\n if (distance <= accumulated + straightHeight) {\n const progress = (distance - accumulated) / straightHeight;\n return { x: left, y: top + height - radius - progress * straightHeight };\n }\n accumulated += straightHeight;\n\n // Top-left corner\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + radius, top + radius, radius, Math.PI, Math.PI / 2, progress);\n },\n [getCornerPoint]\n );\n\n useEffect(() => {\n const canvas = canvasRef.current;\n const container = containerRef.current;\n if (!canvas || !container) return;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n // Configuration\n const octaves = 10;\n const lacunarity = 1.6;\n const gain = 0.7;\n const amplitude = chaos;\n const frequency = 10;\n const baseFlatness = 0;\n const displacement = 60;\n const borderOffset = 60;\n\n const updateSize = () => {\n const rect = container.getBoundingClientRect();\n const width = rect.width + borderOffset * 2;\n const height = rect.height + borderOffset * 2;\n\n // Use device pixel ratio for sharp rendering\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n canvas.width = width * dpr;\n canvas.height = height * dpr;\n canvas.style.width = `${width}px`;\n canvas.style.height = `${height}px`;\n ctx.scale(dpr, dpr);\n\n return { width, height };\n };\n\n let { width, height } = updateSize();\n let lastDpr = Math.min(window.devicePixelRatio || 1, 2);\n\n const drawElectricBorder = currentTime => {\n if (!canvas || !ctx) return;\n\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n if (dpr !== lastDpr) {\n lastDpr = dpr;\n const newSize = updateSize();\n width = newSize.width;\n height = newSize.height;\n }\n\n const deltaTime = (currentTime - lastFrameTimeRef.current) / 1000;\n timeRef.current += deltaTime * speed;\n lastFrameTimeRef.current = currentTime;\n\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n ctx.scale(dpr, dpr);\n\n ctx.strokeStyle = color;\n ctx.lineWidth = 1;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n\n const scale = displacement;\n const left = borderOffset;\n const top = borderOffset;\n const borderWidth = width - 2 * borderOffset;\n const borderHeight = height - 2 * borderOffset;\n const maxRadius = Math.min(borderWidth, borderHeight) / 2;\n const radius = Math.min(borderRadius, maxRadius);\n\n const approximatePerimeter = 2 * (borderWidth + borderHeight) + 2 * Math.PI * radius;\n const sampleCount = Math.floor(approximatePerimeter / 2);\n\n ctx.beginPath();\n\n for (let i = 0; i <= sampleCount; i++) {\n const progress = i / sampleCount;\n\n const point = getRoundedRectPoint(progress, left, top, borderWidth, borderHeight, radius);\n\n const xNoise = octavedNoise(\n progress * 8,\n octaves,\n lacunarity,\n gain,\n amplitude,\n frequency,\n timeRef.current,\n 0,\n baseFlatness\n );\n\n const yNoise = octavedNoise(\n progress * 8,\n octaves,\n lacunarity,\n gain,\n amplitude,\n frequency,\n timeRef.current,\n 1,\n baseFlatness\n );\n\n const displacedX = point.x + xNoise * scale;\n const displacedY = point.y + yNoise * scale;\n\n if (i === 0) {\n ctx.moveTo(displacedX, displacedY);\n } else {\n ctx.lineTo(displacedX, displacedY);\n }\n }\n\n ctx.closePath();\n ctx.stroke();\n\n animationRef.current = requestAnimationFrame(drawElectricBorder);\n };\n\n // Handle resize\n const resizeObserver = new ResizeObserver(() => {\n const newSize = updateSize();\n width = newSize.width;\n height = newSize.height;\n });\n resizeObserver.observe(container);\n\n // Start animation\n animationRef.current = requestAnimationFrame(drawElectricBorder);\n\n return () => {\n if (animationRef.current) {\n cancelAnimationFrame(animationRef.current);\n }\n resizeObserver.disconnect();\n };\n }, [color, speed, chaos, borderRadius, octavedNoise, getRoundedRectPoint]);\n\n const vars = {\n '--electric-border-color': color,\n borderRadius: borderRadius\n };\n\n return (\n
\n
\n \n
\n
\n
\n
\n
\n
\n
{children}
\n
\n );\n};\n\nexport default ElectricBorder;\n" } ], diff --git a/public/r/ElectricBorder-TS-CSS.json b/public/r/ElectricBorder-TS-CSS.json index b9832b97..e24d73e7 100644 --- a/public/r/ElectricBorder-TS-CSS.json +++ b/public/r/ElectricBorder-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "ElectricBorder/ElectricBorder.css", + "type": "registry:file", + "path": "ElectricBorder.css", + "target": "@components/ElectricBorder.css", "content": ".electric-border {\n --electric-light-color: oklch(from var(--electric-border-color) l c h);\n position: relative;\n border-radius: inherit;\n overflow: visible;\n isolation: isolate;\n}\n\n.eb-canvas-container {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n pointer-events: none;\n z-index: 2;\n}\n\n.eb-canvas {\n display: block;\n}\n\n.eb-content {\n position: relative;\n border-radius: inherit;\n z-index: 1;\n}\n\n.eb-layers {\n position: absolute;\n inset: 0;\n border-radius: inherit;\n pointer-events: none;\n z-index: 0;\n}\n\n.eb-glow-1,\n.eb-glow-2,\n.eb-background-glow {\n position: absolute;\n inset: 0;\n border-radius: inherit;\n pointer-events: none;\n box-sizing: border-box;\n}\n\n.eb-glow-1 {\n border: 2px solid oklch(from var(--electric-border-color) l c h / 0.6);\n filter: blur(1px);\n}\n\n.eb-glow-2 {\n border: 2px solid var(--electric-light-color);\n filter: blur(4px);\n}\n\n.eb-background-glow {\n z-index: -1;\n transform: scale(1.1);\n filter: blur(32px);\n opacity: 0.3;\n background: linear-gradient(-30deg, var(--electric-light-color), transparent, var(--electric-border-color));\n}\n" }, { "type": "registry:component", - "path": "ElectricBorder/ElectricBorder.tsx", - "content": "import React, { useEffect, useRef, useCallback, CSSProperties, ReactNode } from 'react';\nimport './ElectricBorder.css';\n\ninterface ElectricBorderProps {\n children?: ReactNode;\n color?: string;\n speed?: number;\n chaos?: number;\n borderRadius?: number;\n className?: string;\n style?: CSSProperties;\n}\n\nconst ElectricBorder: React.FC = ({\n children,\n color = '#5227FF',\n speed = 1,\n chaos = 0.12,\n borderRadius = 24,\n className,\n style\n}) => {\n const canvasRef = useRef(null);\n const containerRef = useRef(null);\n const animationRef = useRef(null);\n const timeRef = useRef(0);\n const lastFrameTimeRef = useRef(0);\n\n const random = useCallback((x: number): number => {\n return (Math.sin(x * 12.9898) * 43758.5453) % 1;\n }, []);\n\n const noise2D = useCallback(\n (x: number, y: number): number => {\n const i = Math.floor(x);\n const j = Math.floor(y);\n const fx = x - i;\n const fy = y - j;\n\n const a = random(i + j * 57);\n const b = random(i + 1 + j * 57);\n const c = random(i + (j + 1) * 57);\n const d = random(i + 1 + (j + 1) * 57);\n\n const ux = fx * fx * (3.0 - 2.0 * fx);\n const uy = fy * fy * (3.0 - 2.0 * fy);\n\n return a * (1 - ux) * (1 - uy) + b * ux * (1 - uy) + c * (1 - ux) * uy + d * ux * uy;\n },\n [random]\n );\n\n const octavedNoise = useCallback(\n (\n x: number,\n octaves: number,\n lacunarity: number,\n gain: number,\n baseAmplitude: number,\n baseFrequency: number,\n time: number,\n seed: number,\n baseFlatness: number\n ): number => {\n let y = 0;\n let amplitude = baseAmplitude;\n let frequency = baseFrequency;\n\n for (let i = 0; i < octaves; i++) {\n let octaveAmplitude = amplitude;\n if (i === 0) {\n octaveAmplitude *= baseFlatness;\n }\n y += octaveAmplitude * noise2D(frequency * x + seed * 100, time * frequency * 0.3);\n frequency *= lacunarity;\n amplitude *= gain;\n }\n\n return y;\n },\n [noise2D]\n );\n\n const getCornerPoint = useCallback(\n (\n centerX: number,\n centerY: number,\n radius: number,\n startAngle: number,\n arcLength: number,\n progress: number\n ): { x: number; y: number } => {\n const angle = startAngle + progress * arcLength;\n return {\n x: centerX + radius * Math.cos(angle),\n y: centerY + radius * Math.sin(angle)\n };\n },\n []\n );\n\n const getRoundedRectPoint = useCallback(\n (t: number, left: number, top: number, width: number, height: number, radius: number): { x: number; y: number } => {\n const straightWidth = width - 2 * radius;\n const straightHeight = height - 2 * radius;\n const cornerArc = (Math.PI * radius) / 2;\n const totalPerimeter = 2 * straightWidth + 2 * straightHeight + 4 * cornerArc;\n const distance = t * totalPerimeter;\n\n let accumulated = 0;\n\n if (distance <= accumulated + straightWidth) {\n const progress = (distance - accumulated) / straightWidth;\n return { x: left + radius + progress * straightWidth, y: top };\n }\n accumulated += straightWidth;\n\n if (distance <= accumulated + cornerArc) {\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + width - radius, top + radius, radius, -Math.PI / 2, Math.PI / 2, progress);\n }\n accumulated += cornerArc;\n\n if (distance <= accumulated + straightHeight) {\n const progress = (distance - accumulated) / straightHeight;\n return { x: left + width, y: top + radius + progress * straightHeight };\n }\n accumulated += straightHeight;\n\n if (distance <= accumulated + cornerArc) {\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + width - radius, top + height - radius, radius, 0, Math.PI / 2, progress);\n }\n accumulated += cornerArc;\n\n if (distance <= accumulated + straightWidth) {\n const progress = (distance - accumulated) / straightWidth;\n return { x: left + width - radius - progress * straightWidth, y: top + height };\n }\n accumulated += straightWidth;\n\n if (distance <= accumulated + cornerArc) {\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + radius, top + height - radius, radius, Math.PI / 2, Math.PI / 2, progress);\n }\n accumulated += cornerArc;\n\n if (distance <= accumulated + straightHeight) {\n const progress = (distance - accumulated) / straightHeight;\n return { x: left, y: top + height - radius - progress * straightHeight };\n }\n accumulated += straightHeight;\n\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + radius, top + radius, radius, Math.PI, Math.PI / 2, progress);\n },\n [getCornerPoint]\n );\n\n useEffect(() => {\n const canvas = canvasRef.current;\n const container = containerRef.current;\n if (!canvas || !container) return;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n const octaves = 10;\n const lacunarity = 1.6;\n const gain = 0.7;\n const amplitude = chaos;\n const frequency = 10;\n const baseFlatness = 0;\n const displacement = 60;\n const borderOffset = 60;\n\n const updateSize = () => {\n const rect = container.getBoundingClientRect();\n const width = rect.width + borderOffset * 2;\n const height = rect.height + borderOffset * 2;\n\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n canvas.width = width * dpr;\n canvas.height = height * dpr;\n canvas.style.width = `${width}px`;\n canvas.style.height = `${height}px`;\n ctx.scale(dpr, dpr);\n\n return { width, height };\n };\n\n let { width, height } = updateSize();\n let lastDpr = Math.min(window.devicePixelRatio || 1, 2);\n\n const drawElectricBorder = (currentTime: number) => {\n if (!canvas || !ctx) return;\n\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n if (dpr !== lastDpr) {\n lastDpr = dpr;\n const newSize = updateSize();\n width = newSize.width;\n height = newSize.height;\n }\n\n const deltaTime = (currentTime - lastFrameTimeRef.current) / 1000;\n timeRef.current += deltaTime * speed;\n lastFrameTimeRef.current = currentTime;\n\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n ctx.scale(dpr, dpr);\n\n ctx.strokeStyle = color;\n ctx.lineWidth = 1;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n\n const scale = displacement;\n const left = borderOffset;\n const top = borderOffset;\n const borderWidth = width - 2 * borderOffset;\n const borderHeight = height - 2 * borderOffset;\n const maxRadius = Math.min(borderWidth, borderHeight) / 2;\n const radius = Math.min(borderRadius, maxRadius);\n\n const approximatePerimeter = 2 * (borderWidth + borderHeight) + 2 * Math.PI * radius;\n const sampleCount = Math.floor(approximatePerimeter / 2);\n\n ctx.beginPath();\n\n for (let i = 0; i <= sampleCount; i++) {\n const progress = i / sampleCount;\n\n const point = getRoundedRectPoint(progress, left, top, borderWidth, borderHeight, radius);\n\n const xNoise = octavedNoise(\n progress * 8,\n octaves,\n lacunarity,\n gain,\n amplitude,\n frequency,\n timeRef.current,\n 0,\n baseFlatness\n );\n const yNoise = octavedNoise(\n progress * 8,\n octaves,\n lacunarity,\n gain,\n amplitude,\n frequency,\n timeRef.current,\n 1,\n baseFlatness\n );\n\n const displacedX = point.x + xNoise * scale;\n const displacedY = point.y + yNoise * scale;\n\n if (i === 0) {\n ctx.moveTo(displacedX, displacedY);\n } else {\n ctx.lineTo(displacedX, displacedY);\n }\n }\n\n ctx.closePath();\n ctx.stroke();\n\n animationRef.current = requestAnimationFrame(drawElectricBorder);\n };\n\n const resizeObserver = new ResizeObserver(() => {\n const newSize = updateSize();\n width = newSize.width;\n height = newSize.height;\n });\n resizeObserver.observe(container);\n\n animationRef.current = requestAnimationFrame(drawElectricBorder);\n\n return () => {\n if (animationRef.current) {\n cancelAnimationFrame(animationRef.current);\n }\n resizeObserver.disconnect();\n };\n }, [color, speed, chaos, borderRadius, octavedNoise, getRoundedRectPoint]);\n\n const vars = {\n '--electric-border-color': color,\n borderRadius\n } as CSSProperties;\n\n return (\n
\n
\n \n
\n
\n
\n
\n
\n
\n
{children}
\n
\n );\n};\n\nexport default ElectricBorder;\n" + "path": "ElectricBorder.tsx", + "content": "import React, { useEffect, useRef, useCallback, type CSSProperties, type ReactNode } from 'react';\nimport './ElectricBorder.css';\n\ninterface ElectricBorderProps {\n children?: ReactNode;\n color?: string;\n speed?: number;\n chaos?: number;\n borderRadius?: number;\n className?: string;\n style?: CSSProperties;\n}\n\nconst ElectricBorder: React.FC = ({\n children,\n color = '#5227FF',\n speed = 1,\n chaos = 0.12,\n borderRadius = 24,\n className,\n style\n}) => {\n const canvasRef = useRef(null);\n const containerRef = useRef(null);\n const animationRef = useRef(null);\n const timeRef = useRef(0);\n const lastFrameTimeRef = useRef(0);\n\n const random = useCallback((x: number): number => {\n return (Math.sin(x * 12.9898) * 43758.5453) % 1;\n }, []);\n\n const noise2D = useCallback(\n (x: number, y: number): number => {\n const i = Math.floor(x);\n const j = Math.floor(y);\n const fx = x - i;\n const fy = y - j;\n\n const a = random(i + j * 57);\n const b = random(i + 1 + j * 57);\n const c = random(i + (j + 1) * 57);\n const d = random(i + 1 + (j + 1) * 57);\n\n const ux = fx * fx * (3.0 - 2.0 * fx);\n const uy = fy * fy * (3.0 - 2.0 * fy);\n\n return a * (1 - ux) * (1 - uy) + b * ux * (1 - uy) + c * (1 - ux) * uy + d * ux * uy;\n },\n [random]\n );\n\n const octavedNoise = useCallback(\n (\n x: number,\n octaves: number,\n lacunarity: number,\n gain: number,\n baseAmplitude: number,\n baseFrequency: number,\n time: number,\n seed: number,\n baseFlatness: number\n ): number => {\n let y = 0;\n let amplitude = baseAmplitude;\n let frequency = baseFrequency;\n\n for (let i = 0; i < octaves; i++) {\n let octaveAmplitude = amplitude;\n if (i === 0) {\n octaveAmplitude *= baseFlatness;\n }\n y += octaveAmplitude * noise2D(frequency * x + seed * 100, time * frequency * 0.3);\n frequency *= lacunarity;\n amplitude *= gain;\n }\n\n return y;\n },\n [noise2D]\n );\n\n const getCornerPoint = useCallback(\n (\n centerX: number,\n centerY: number,\n radius: number,\n startAngle: number,\n arcLength: number,\n progress: number\n ): { x: number; y: number } => {\n const angle = startAngle + progress * arcLength;\n return {\n x: centerX + radius * Math.cos(angle),\n y: centerY + radius * Math.sin(angle)\n };\n },\n []\n );\n\n const getRoundedRectPoint = useCallback(\n (t: number, left: number, top: number, width: number, height: number, radius: number): { x: number; y: number } => {\n const straightWidth = width - 2 * radius;\n const straightHeight = height - 2 * radius;\n const cornerArc = (Math.PI * radius) / 2;\n const totalPerimeter = 2 * straightWidth + 2 * straightHeight + 4 * cornerArc;\n const distance = t * totalPerimeter;\n\n let accumulated = 0;\n\n if (distance <= accumulated + straightWidth) {\n const progress = (distance - accumulated) / straightWidth;\n return { x: left + radius + progress * straightWidth, y: top };\n }\n accumulated += straightWidth;\n\n if (distance <= accumulated + cornerArc) {\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + width - radius, top + radius, radius, -Math.PI / 2, Math.PI / 2, progress);\n }\n accumulated += cornerArc;\n\n if (distance <= accumulated + straightHeight) {\n const progress = (distance - accumulated) / straightHeight;\n return { x: left + width, y: top + radius + progress * straightHeight };\n }\n accumulated += straightHeight;\n\n if (distance <= accumulated + cornerArc) {\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + width - radius, top + height - radius, radius, 0, Math.PI / 2, progress);\n }\n accumulated += cornerArc;\n\n if (distance <= accumulated + straightWidth) {\n const progress = (distance - accumulated) / straightWidth;\n return { x: left + width - radius - progress * straightWidth, y: top + height };\n }\n accumulated += straightWidth;\n\n if (distance <= accumulated + cornerArc) {\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + radius, top + height - radius, radius, Math.PI / 2, Math.PI / 2, progress);\n }\n accumulated += cornerArc;\n\n if (distance <= accumulated + straightHeight) {\n const progress = (distance - accumulated) / straightHeight;\n return { x: left, y: top + height - radius - progress * straightHeight };\n }\n accumulated += straightHeight;\n\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + radius, top + radius, radius, Math.PI, Math.PI / 2, progress);\n },\n [getCornerPoint]\n );\n\n useEffect(() => {\n const canvas = canvasRef.current;\n const container = containerRef.current;\n if (!canvas || !container) return;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n const octaves = 10;\n const lacunarity = 1.6;\n const gain = 0.7;\n const amplitude = chaos;\n const frequency = 10;\n const baseFlatness = 0;\n const displacement = 60;\n const borderOffset = 60;\n\n const updateSize = () => {\n const rect = container.getBoundingClientRect();\n const width = rect.width + borderOffset * 2;\n const height = rect.height + borderOffset * 2;\n\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n canvas.width = width * dpr;\n canvas.height = height * dpr;\n canvas.style.width = `${width}px`;\n canvas.style.height = `${height}px`;\n ctx.scale(dpr, dpr);\n\n return { width, height };\n };\n\n let { width, height } = updateSize();\n let lastDpr = Math.min(window.devicePixelRatio || 1, 2);\n\n const drawElectricBorder = (currentTime: number) => {\n if (!canvas || !ctx) return;\n\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n if (dpr !== lastDpr) {\n lastDpr = dpr;\n const newSize = updateSize();\n width = newSize.width;\n height = newSize.height;\n }\n\n const deltaTime = (currentTime - lastFrameTimeRef.current) / 1000;\n timeRef.current += deltaTime * speed;\n lastFrameTimeRef.current = currentTime;\n\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n ctx.scale(dpr, dpr);\n\n ctx.strokeStyle = color;\n ctx.lineWidth = 1;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n\n const scale = displacement;\n const left = borderOffset;\n const top = borderOffset;\n const borderWidth = width - 2 * borderOffset;\n const borderHeight = height - 2 * borderOffset;\n const maxRadius = Math.min(borderWidth, borderHeight) / 2;\n const radius = Math.min(borderRadius, maxRadius);\n\n const approximatePerimeter = 2 * (borderWidth + borderHeight) + 2 * Math.PI * radius;\n const sampleCount = Math.floor(approximatePerimeter / 2);\n\n ctx.beginPath();\n\n for (let i = 0; i <= sampleCount; i++) {\n const progress = i / sampleCount;\n\n const point = getRoundedRectPoint(progress, left, top, borderWidth, borderHeight, radius);\n\n const xNoise = octavedNoise(\n progress * 8,\n octaves,\n lacunarity,\n gain,\n amplitude,\n frequency,\n timeRef.current,\n 0,\n baseFlatness\n );\n const yNoise = octavedNoise(\n progress * 8,\n octaves,\n lacunarity,\n gain,\n amplitude,\n frequency,\n timeRef.current,\n 1,\n baseFlatness\n );\n\n const displacedX = point.x + xNoise * scale;\n const displacedY = point.y + yNoise * scale;\n\n if (i === 0) {\n ctx.moveTo(displacedX, displacedY);\n } else {\n ctx.lineTo(displacedX, displacedY);\n }\n }\n\n ctx.closePath();\n ctx.stroke();\n\n animationRef.current = requestAnimationFrame(drawElectricBorder);\n };\n\n const resizeObserver = new ResizeObserver(() => {\n const newSize = updateSize();\n width = newSize.width;\n height = newSize.height;\n });\n resizeObserver.observe(container);\n\n animationRef.current = requestAnimationFrame(drawElectricBorder);\n\n return () => {\n if (animationRef.current) {\n cancelAnimationFrame(animationRef.current);\n }\n resizeObserver.disconnect();\n };\n }, [color, speed, chaos, borderRadius, octavedNoise, getRoundedRectPoint]);\n\n const vars = {\n '--electric-border-color': color,\n borderRadius\n } as CSSProperties;\n\n return (\n
\n
\n \n
\n
\n
\n
\n
\n
\n
{children}
\n
\n );\n};\n\nexport default ElectricBorder;\n" } ], "registryDependencies": [], diff --git a/public/r/ElectricBorder-TS-TW.json b/public/r/ElectricBorder-TS-TW.json index a05aa84b..5df10325 100644 --- a/public/r/ElectricBorder-TS-TW.json +++ b/public/r/ElectricBorder-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "ElectricBorder/ElectricBorder.tsx", - "content": "import React, { useEffect, useRef, useCallback, CSSProperties, ReactNode } from 'react';\n\nfunction hexToRgba(hex: string, alpha: number = 1): string {\n if (!hex) return `rgba(0,0,0,${alpha})`;\n let h = hex.replace('#', '');\n if (h.length === 3) {\n h = h\n .split('')\n .map(c => c + c)\n .join('');\n }\n const int = parseInt(h.slice(0, 6), 16);\n const r = (int >> 16) & 255;\n const g = (int >> 8) & 255;\n const b = int & 255;\n return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\ninterface ElectricBorderProps {\n children?: ReactNode;\n color?: string;\n speed?: number;\n chaos?: number;\n borderRadius?: number;\n className?: string;\n style?: CSSProperties;\n}\n\nconst ElectricBorder: React.FC = ({\n children,\n color = '#5227FF',\n speed = 1,\n chaos = 0.12,\n borderRadius = 24,\n className,\n style\n}) => {\n const canvasRef = useRef(null);\n const containerRef = useRef(null);\n const animationRef = useRef(null);\n const timeRef = useRef(0);\n const lastFrameTimeRef = useRef(0);\n\n const random = useCallback((x: number): number => {\n return (Math.sin(x * 12.9898) * 43758.5453) % 1;\n }, []);\n\n const noise2D = useCallback(\n (x: number, y: number): number => {\n const i = Math.floor(x);\n const j = Math.floor(y);\n const fx = x - i;\n const fy = y - j;\n\n const a = random(i + j * 57);\n const b = random(i + 1 + j * 57);\n const c = random(i + (j + 1) * 57);\n const d = random(i + 1 + (j + 1) * 57);\n\n const ux = fx * fx * (3.0 - 2.0 * fx);\n const uy = fy * fy * (3.0 - 2.0 * fy);\n\n return a * (1 - ux) * (1 - uy) + b * ux * (1 - uy) + c * (1 - ux) * uy + d * ux * uy;\n },\n [random]\n );\n\n const octavedNoise = useCallback(\n (\n x: number,\n octaves: number,\n lacunarity: number,\n gain: number,\n baseAmplitude: number,\n baseFrequency: number,\n time: number,\n seed: number,\n baseFlatness: number\n ): number => {\n let y = 0;\n let amplitude = baseAmplitude;\n let frequency = baseFrequency;\n\n for (let i = 0; i < octaves; i++) {\n let octaveAmplitude = amplitude;\n if (i === 0) {\n octaveAmplitude *= baseFlatness;\n }\n y += octaveAmplitude * noise2D(frequency * x + seed * 100, time * frequency * 0.3);\n frequency *= lacunarity;\n amplitude *= gain;\n }\n\n return y;\n },\n [noise2D]\n );\n\n const getCornerPoint = useCallback(\n (\n centerX: number,\n centerY: number,\n radius: number,\n startAngle: number,\n arcLength: number,\n progress: number\n ): { x: number; y: number } => {\n const angle = startAngle + progress * arcLength;\n return {\n x: centerX + radius * Math.cos(angle),\n y: centerY + radius * Math.sin(angle)\n };\n },\n []\n );\n\n const getRoundedRectPoint = useCallback(\n (t: number, left: number, top: number, width: number, height: number, radius: number): { x: number; y: number } => {\n const straightWidth = width - 2 * radius;\n const straightHeight = height - 2 * radius;\n const cornerArc = (Math.PI * radius) / 2;\n const totalPerimeter = 2 * straightWidth + 2 * straightHeight + 4 * cornerArc;\n const distance = t * totalPerimeter;\n\n let accumulated = 0;\n\n if (distance <= accumulated + straightWidth) {\n const progress = (distance - accumulated) / straightWidth;\n return { x: left + radius + progress * straightWidth, y: top };\n }\n accumulated += straightWidth;\n\n if (distance <= accumulated + cornerArc) {\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + width - radius, top + radius, radius, -Math.PI / 2, Math.PI / 2, progress);\n }\n accumulated += cornerArc;\n\n if (distance <= accumulated + straightHeight) {\n const progress = (distance - accumulated) / straightHeight;\n return { x: left + width, y: top + radius + progress * straightHeight };\n }\n accumulated += straightHeight;\n\n if (distance <= accumulated + cornerArc) {\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + width - radius, top + height - radius, radius, 0, Math.PI / 2, progress);\n }\n accumulated += cornerArc;\n\n if (distance <= accumulated + straightWidth) {\n const progress = (distance - accumulated) / straightWidth;\n return { x: left + width - radius - progress * straightWidth, y: top + height };\n }\n accumulated += straightWidth;\n\n if (distance <= accumulated + cornerArc) {\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + radius, top + height - radius, radius, Math.PI / 2, Math.PI / 2, progress);\n }\n accumulated += cornerArc;\n\n if (distance <= accumulated + straightHeight) {\n const progress = (distance - accumulated) / straightHeight;\n return { x: left, y: top + height - radius - progress * straightHeight };\n }\n accumulated += straightHeight;\n\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + radius, top + radius, radius, Math.PI, Math.PI / 2, progress);\n },\n [getCornerPoint]\n );\n\n useEffect(() => {\n const canvas = canvasRef.current;\n const container = containerRef.current;\n if (!canvas || !container) return;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n const octaves = 10;\n const lacunarity = 1.6;\n const gain = 0.7;\n const amplitude = chaos;\n const frequency = 10;\n const baseFlatness = 0;\n const displacement = 60;\n const borderOffset = 60;\n\n const updateSize = () => {\n const rect = container.getBoundingClientRect();\n const width = rect.width + borderOffset * 2;\n const height = rect.height + borderOffset * 2;\n\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n canvas.width = width * dpr;\n canvas.height = height * dpr;\n canvas.style.width = `${width}px`;\n canvas.style.height = `${height}px`;\n ctx.scale(dpr, dpr);\n\n return { width, height };\n };\n\n let { width, height } = updateSize();\n let lastDpr = Math.min(window.devicePixelRatio || 1, 2);\n\n const drawElectricBorder = (currentTime: number) => {\n if (!canvas || !ctx) return;\n\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n if (dpr !== lastDpr) {\n lastDpr = dpr;\n const newSize = updateSize();\n width = newSize.width;\n height = newSize.height;\n }\n\n const deltaTime = (currentTime - lastFrameTimeRef.current) / 1000;\n timeRef.current += deltaTime * speed;\n lastFrameTimeRef.current = currentTime;\n\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n ctx.scale(dpr, dpr);\n\n ctx.strokeStyle = color;\n ctx.lineWidth = 1;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n\n const scale = displacement;\n const left = borderOffset;\n const top = borderOffset;\n const borderWidth = width - 2 * borderOffset;\n const borderHeight = height - 2 * borderOffset;\n const maxRadius = Math.min(borderWidth, borderHeight) / 2;\n const radius = Math.min(borderRadius, maxRadius);\n\n const approximatePerimeter = 2 * (borderWidth + borderHeight) + 2 * Math.PI * radius;\n const sampleCount = Math.floor(approximatePerimeter / 2);\n\n ctx.beginPath();\n\n for (let i = 0; i <= sampleCount; i++) {\n const progress = i / sampleCount;\n\n const point = getRoundedRectPoint(progress, left, top, borderWidth, borderHeight, radius);\n\n const xNoise = octavedNoise(\n progress * 8,\n octaves,\n lacunarity,\n gain,\n amplitude,\n frequency,\n timeRef.current,\n 0,\n baseFlatness\n );\n const yNoise = octavedNoise(\n progress * 8,\n octaves,\n lacunarity,\n gain,\n amplitude,\n frequency,\n timeRef.current,\n 1,\n baseFlatness\n );\n\n const displacedX = point.x + xNoise * scale;\n const displacedY = point.y + yNoise * scale;\n\n if (i === 0) {\n ctx.moveTo(displacedX, displacedY);\n } else {\n ctx.lineTo(displacedX, displacedY);\n }\n }\n\n ctx.closePath();\n ctx.stroke();\n\n animationRef.current = requestAnimationFrame(drawElectricBorder);\n };\n\n const resizeObserver = new ResizeObserver(() => {\n const newSize = updateSize();\n width = newSize.width;\n height = newSize.height;\n });\n resizeObserver.observe(container);\n\n animationRef.current = requestAnimationFrame(drawElectricBorder);\n\n return () => {\n if (animationRef.current) {\n cancelAnimationFrame(animationRef.current);\n }\n resizeObserver.disconnect();\n };\n }, [color, speed, chaos, borderRadius, octavedNoise, getRoundedRectPoint]);\n\n return (\n \n
\n \n
\n
\n \n \n \n
\n
{children}
\n
\n );\n};\n\nexport default ElectricBorder;\n" + "content": "import React, { useEffect, useRef, useCallback, type CSSProperties, type ReactNode } from 'react';\n\nfunction hexToRgba(hex: string, alpha: number = 1): string {\n if (!hex) return `rgba(0,0,0,${alpha})`;\n let h = hex.replace('#', '');\n if (h.length === 3) {\n h = h\n .split('')\n .map(c => c + c)\n .join('');\n }\n const int = parseInt(h.slice(0, 6), 16);\n const r = (int >> 16) & 255;\n const g = (int >> 8) & 255;\n const b = int & 255;\n return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\ninterface ElectricBorderProps {\n children?: ReactNode;\n color?: string;\n speed?: number;\n chaos?: number;\n borderRadius?: number;\n className?: string;\n style?: CSSProperties;\n}\n\nconst ElectricBorder: React.FC = ({\n children,\n color = '#5227FF',\n speed = 1,\n chaos = 0.12,\n borderRadius = 24,\n className,\n style\n}) => {\n const canvasRef = useRef(null);\n const containerRef = useRef(null);\n const animationRef = useRef(null);\n const timeRef = useRef(0);\n const lastFrameTimeRef = useRef(0);\n\n const random = useCallback((x: number): number => {\n return (Math.sin(x * 12.9898) * 43758.5453) % 1;\n }, []);\n\n const noise2D = useCallback(\n (x: number, y: number): number => {\n const i = Math.floor(x);\n const j = Math.floor(y);\n const fx = x - i;\n const fy = y - j;\n\n const a = random(i + j * 57);\n const b = random(i + 1 + j * 57);\n const c = random(i + (j + 1) * 57);\n const d = random(i + 1 + (j + 1) * 57);\n\n const ux = fx * fx * (3.0 - 2.0 * fx);\n const uy = fy * fy * (3.0 - 2.0 * fy);\n\n return a * (1 - ux) * (1 - uy) + b * ux * (1 - uy) + c * (1 - ux) * uy + d * ux * uy;\n },\n [random]\n );\n\n const octavedNoise = useCallback(\n (\n x: number,\n octaves: number,\n lacunarity: number,\n gain: number,\n baseAmplitude: number,\n baseFrequency: number,\n time: number,\n seed: number,\n baseFlatness: number\n ): number => {\n let y = 0;\n let amplitude = baseAmplitude;\n let frequency = baseFrequency;\n\n for (let i = 0; i < octaves; i++) {\n let octaveAmplitude = amplitude;\n if (i === 0) {\n octaveAmplitude *= baseFlatness;\n }\n y += octaveAmplitude * noise2D(frequency * x + seed * 100, time * frequency * 0.3);\n frequency *= lacunarity;\n amplitude *= gain;\n }\n\n return y;\n },\n [noise2D]\n );\n\n const getCornerPoint = useCallback(\n (\n centerX: number,\n centerY: number,\n radius: number,\n startAngle: number,\n arcLength: number,\n progress: number\n ): { x: number; y: number } => {\n const angle = startAngle + progress * arcLength;\n return {\n x: centerX + radius * Math.cos(angle),\n y: centerY + radius * Math.sin(angle)\n };\n },\n []\n );\n\n const getRoundedRectPoint = useCallback(\n (t: number, left: number, top: number, width: number, height: number, radius: number): { x: number; y: number } => {\n const straightWidth = width - 2 * radius;\n const straightHeight = height - 2 * radius;\n const cornerArc = (Math.PI * radius) / 2;\n const totalPerimeter = 2 * straightWidth + 2 * straightHeight + 4 * cornerArc;\n const distance = t * totalPerimeter;\n\n let accumulated = 0;\n\n if (distance <= accumulated + straightWidth) {\n const progress = (distance - accumulated) / straightWidth;\n return { x: left + radius + progress * straightWidth, y: top };\n }\n accumulated += straightWidth;\n\n if (distance <= accumulated + cornerArc) {\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + width - radius, top + radius, radius, -Math.PI / 2, Math.PI / 2, progress);\n }\n accumulated += cornerArc;\n\n if (distance <= accumulated + straightHeight) {\n const progress = (distance - accumulated) / straightHeight;\n return { x: left + width, y: top + radius + progress * straightHeight };\n }\n accumulated += straightHeight;\n\n if (distance <= accumulated + cornerArc) {\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + width - radius, top + height - radius, radius, 0, Math.PI / 2, progress);\n }\n accumulated += cornerArc;\n\n if (distance <= accumulated + straightWidth) {\n const progress = (distance - accumulated) / straightWidth;\n return { x: left + width - radius - progress * straightWidth, y: top + height };\n }\n accumulated += straightWidth;\n\n if (distance <= accumulated + cornerArc) {\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + radius, top + height - radius, radius, Math.PI / 2, Math.PI / 2, progress);\n }\n accumulated += cornerArc;\n\n if (distance <= accumulated + straightHeight) {\n const progress = (distance - accumulated) / straightHeight;\n return { x: left, y: top + height - radius - progress * straightHeight };\n }\n accumulated += straightHeight;\n\n const progress = (distance - accumulated) / cornerArc;\n return getCornerPoint(left + radius, top + radius, radius, Math.PI, Math.PI / 2, progress);\n },\n [getCornerPoint]\n );\n\n useEffect(() => {\n const canvas = canvasRef.current;\n const container = containerRef.current;\n if (!canvas || !container) return;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n const octaves = 10;\n const lacunarity = 1.6;\n const gain = 0.7;\n const amplitude = chaos;\n const frequency = 10;\n const baseFlatness = 0;\n const displacement = 60;\n const borderOffset = 60;\n\n const updateSize = () => {\n const rect = container.getBoundingClientRect();\n const width = rect.width + borderOffset * 2;\n const height = rect.height + borderOffset * 2;\n\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n canvas.width = width * dpr;\n canvas.height = height * dpr;\n canvas.style.width = `${width}px`;\n canvas.style.height = `${height}px`;\n ctx.scale(dpr, dpr);\n\n return { width, height };\n };\n\n let { width, height } = updateSize();\n let lastDpr = Math.min(window.devicePixelRatio || 1, 2);\n\n const drawElectricBorder = (currentTime: number) => {\n if (!canvas || !ctx) return;\n\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n if (dpr !== lastDpr) {\n lastDpr = dpr;\n const newSize = updateSize();\n width = newSize.width;\n height = newSize.height;\n }\n\n const deltaTime = (currentTime - lastFrameTimeRef.current) / 1000;\n timeRef.current += deltaTime * speed;\n lastFrameTimeRef.current = currentTime;\n\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n ctx.scale(dpr, dpr);\n\n ctx.strokeStyle = color;\n ctx.lineWidth = 1;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n\n const scale = displacement;\n const left = borderOffset;\n const top = borderOffset;\n const borderWidth = width - 2 * borderOffset;\n const borderHeight = height - 2 * borderOffset;\n const maxRadius = Math.min(borderWidth, borderHeight) / 2;\n const radius = Math.min(borderRadius, maxRadius);\n\n const approximatePerimeter = 2 * (borderWidth + borderHeight) + 2 * Math.PI * radius;\n const sampleCount = Math.floor(approximatePerimeter / 2);\n\n ctx.beginPath();\n\n for (let i = 0; i <= sampleCount; i++) {\n const progress = i / sampleCount;\n\n const point = getRoundedRectPoint(progress, left, top, borderWidth, borderHeight, radius);\n\n const xNoise = octavedNoise(\n progress * 8,\n octaves,\n lacunarity,\n gain,\n amplitude,\n frequency,\n timeRef.current,\n 0,\n baseFlatness\n );\n const yNoise = octavedNoise(\n progress * 8,\n octaves,\n lacunarity,\n gain,\n amplitude,\n frequency,\n timeRef.current,\n 1,\n baseFlatness\n );\n\n const displacedX = point.x + xNoise * scale;\n const displacedY = point.y + yNoise * scale;\n\n if (i === 0) {\n ctx.moveTo(displacedX, displacedY);\n } else {\n ctx.lineTo(displacedX, displacedY);\n }\n }\n\n ctx.closePath();\n ctx.stroke();\n\n animationRef.current = requestAnimationFrame(drawElectricBorder);\n };\n\n const resizeObserver = new ResizeObserver(() => {\n const newSize = updateSize();\n width = newSize.width;\n height = newSize.height;\n });\n resizeObserver.observe(container);\n\n animationRef.current = requestAnimationFrame(drawElectricBorder);\n\n return () => {\n if (animationRef.current) {\n cancelAnimationFrame(animationRef.current);\n }\n resizeObserver.disconnect();\n };\n }, [color, speed, chaos, borderRadius, octavedNoise, getRoundedRectPoint]);\n\n return (\n \n
\n \n
\n
\n \n \n \n
\n
{children}
\n
\n );\n};\n\nexport default ElectricBorder;\n" } ], "registryDependencies": [], diff --git a/public/r/EvilEye-JS-CSS.json b/public/r/EvilEye-JS-CSS.json index 2bf05ec0..291c8458 100644 --- a/public/r/EvilEye-JS-CSS.json +++ b/public/r/EvilEye-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "EvilEye/EvilEye.css", + "type": "registry:file", + "path": "EvilEye.css", + "target": "@components/EvilEye.css", "content": ".evil-eye-container {\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "EvilEye/EvilEye.jsx", + "path": "EvilEye.jsx", "content": "import { Renderer, Program, Mesh, Triangle, Texture } from 'ogl';\nimport { useEffect, useRef } from 'react';\nimport './EvilEye.css';\n\nfunction hexToVec3(hex) {\n const h = hex.replace('#', '');\n return [\n parseInt(h.slice(0, 2), 16) / 255,\n parseInt(h.slice(2, 4), 16) / 255,\n parseInt(h.slice(4, 6), 16) / 255\n ];\n}\n\nfunction generateNoiseTexture(size = 256) {\n const data = new Uint8Array(size * size * 4);\n\n function hash(x, y, s) {\n let n = x * 374761393 + y * 668265263 + s * 1274126177;\n n = Math.imul(n ^ (n >>> 13), 1274126177);\n return ((n ^ (n >>> 16)) >>> 0) / 4294967296;\n }\n\n function noise(px, py, freq, seed) {\n const fx = (px / size) * freq;\n const fy = (py / size) * freq;\n const ix = Math.floor(fx);\n const iy = Math.floor(fy);\n const tx = fx - ix;\n const ty = fy - iy;\n const w = freq | 0;\n const v00 = hash(((ix % w) + w) % w, ((iy % w) + w) % w, seed);\n const v10 = hash((((ix + 1) % w) + w) % w, ((iy % w) + w) % w, seed);\n const v01 = hash(((ix % w) + w) % w, (((iy + 1) % w) + w) % w, seed);\n const v11 = hash((((ix + 1) % w) + w) % w, (((iy + 1) % w) + w) % w, seed);\n return v00 * (1 - tx) * (1 - ty) + v10 * tx * (1 - ty) + v01 * (1 - tx) * ty + v11 * tx * ty;\n }\n\n for (let y = 0; y < size; y++) {\n for (let x = 0; x < size; x++) {\n let v = 0;\n let amp = 0.4;\n let totalAmp = 0;\n for (let o = 0; o < 8; o++) {\n const f = 32 * (1 << o);\n v += amp * noise(x, y, f, o * 31);\n totalAmp += amp;\n amp *= 0.65;\n }\n v /= totalAmp;\n v = (v - 0.5) * 2.2 + 0.5;\n v = Math.max(0, Math.min(1, v));\n const val = Math.round(v * 255);\n const i = (y * size + x) * 4;\n data[i] = val;\n data[i + 1] = val;\n data[i + 2] = val;\n data[i + 3] = 255;\n }\n }\n\n return data;\n}\n\nconst vertexShader = `\nattribute vec2 uv;\nattribute vec2 position;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0, 1);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float uTime;\nuniform vec3 uResolution;\nuniform sampler2D uNoiseTexture;\nuniform float uPupilSize;\nuniform float uIrisWidth;\nuniform float uGlowIntensity;\nuniform float uIntensity;\nuniform float uScale;\nuniform float uNoiseScale;\nuniform vec2 uMouse;\nuniform float uPupilFollow;\nuniform float uFlameSpeed;\nuniform vec3 uEyeColor;\nuniform vec3 uBgColor;\n\nvoid main() {\n vec2 uv = (gl_FragCoord.xy * 2.0 - uResolution.xy) / uResolution.y;\n uv /= uScale;\n float ft = uTime * uFlameSpeed;\n\n float polarRadius = length(uv) * 2.0;\n float polarAngle = (2.0 * atan(uv.x, uv.y)) / 6.28 * 0.3;\n vec2 polarUv = vec2(polarRadius, polarAngle);\n\n vec4 noiseA = texture2D(uNoiseTexture, polarUv * vec2(0.2, 7.0) * uNoiseScale + vec2(-ft * 0.1, 0.0));\n vec4 noiseB = texture2D(uNoiseTexture, polarUv * vec2(0.3, 4.0) * uNoiseScale + vec2(-ft * 0.2, 0.0));\n vec4 noiseC = texture2D(uNoiseTexture, polarUv * vec2(0.1, 5.0) * uNoiseScale + vec2(-ft * 0.1, 0.0));\n\n float distanceMask = 1.0 - length(uv);\n\n // Inner ring\n float innerRing = clamp(-1.0 * ((distanceMask - 0.7) / uIrisWidth), 0.0, 1.0);\n innerRing = (innerRing * distanceMask - 0.2) / 0.28;\n innerRing += noiseA.r - 0.5;\n innerRing *= 1.3;\n innerRing = clamp(innerRing, 0.0, 1.0);\n\n float outerRing = clamp(-1.0 * ((distanceMask - 0.5) / 0.2), 0.0, 1.0);\n outerRing = (outerRing * distanceMask - 0.1) / 0.38;\n outerRing += noiseC.r - 0.5;\n outerRing *= 1.3;\n outerRing = clamp(outerRing, 0.0, 1.0);\n\n innerRing += outerRing;\n\n // Inner eye\n float innerEye = distanceMask - 0.1 * 2.0;\n innerEye *= noiseB.r * 2.0;\n\n // Pupil with cursor tracking\n vec2 pupilOffset = uMouse * uPupilFollow * 0.12;\n vec2 pupilUv = uv - pupilOffset;\n float pupil = 1.0 - length(pupilUv * vec2(9.0, 2.3));\n pupil *= uPupilSize;\n pupil = clamp(pupil, 0.0, 1.0);\n pupil /= 0.35;\n\n // Outer eye\n float outerEyeGlow = 1.0 - length(uv * vec2(0.5, 1.5));\n outerEyeGlow = clamp(outerEyeGlow + 0.5, 0.0, 1.0);\n outerEyeGlow += noiseC.r - 0.5;\n float outerBgGlow = outerEyeGlow;\n outerEyeGlow = pow(outerEyeGlow, 2.0);\n outerEyeGlow += distanceMask;\n outerEyeGlow *= uGlowIntensity;\n outerEyeGlow = clamp(outerEyeGlow, 0.0, 1.0);\n outerEyeGlow *= pow(1.0 - distanceMask, 2.0) * 2.5;\n\n // Outer eye bg glow\n outerBgGlow += distanceMask;\n outerBgGlow = pow(outerBgGlow, 0.5);\n outerBgGlow *= 0.15;\n\n vec3 color = uEyeColor * uIntensity * clamp(max(innerRing + innerEye, outerEyeGlow + outerBgGlow) - pupil, 0.0, 3.0);\n color += uBgColor;\n\n gl_FragColor = vec4(color, 1.0);\n}\n`;\n\nexport default function EvilEye({\n eyeColor = '#FF6F37',\n intensity = 1.5,\n pupilSize = 0.6,\n irisWidth = 0.25,\n glowIntensity = 0.35,\n scale = 0.8,\n noiseScale = 1.0,\n pupilFollow = 1.0,\n flameSpeed = 1.0,\n backgroundColor = '#000000'\n}) {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n const container = containerRef.current;\n const renderer = new Renderer({ alpha: true, premultipliedAlpha: false });\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n\n const noiseData = generateNoiseTexture(256);\n const noiseTexture = new Texture(gl, {\n image: noiseData,\n width: 256,\n height: 256,\n generateMipmaps: false,\n flipY: false,\n });\n noiseTexture.minFilter = gl.LINEAR;\n noiseTexture.magFilter = gl.LINEAR;\n noiseTexture.wrapS = gl.REPEAT;\n noiseTexture.wrapT = gl.REPEAT;\n\n const mouse = { x: 0, y: 0, tx: 0, ty: 0 };\n\n function onMouseMove(e) {\n const rect = container.getBoundingClientRect();\n mouse.tx = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n mouse.ty = -(((e.clientY - rect.top) / rect.height) * 2 - 1);\n }\n\n function onMouseLeave() {\n mouse.tx = 0;\n mouse.ty = 0;\n }\n\n container.addEventListener('mousemove', onMouseMove);\n container.addEventListener('mouseleave', onMouseLeave);\n\n let program;\n\n function resize() {\n renderer.setSize(container.offsetWidth, container.offsetHeight);\n if (program) {\n program.uniforms.uResolution.value = [gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height];\n }\n }\n window.addEventListener('resize', resize);\n resize();\n\n const geometry = new Triangle(gl);\n program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uResolution: { value: [gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height] },\n uNoiseTexture: { value: noiseTexture },\n uPupilSize: { value: pupilSize },\n uIrisWidth: { value: irisWidth },\n uGlowIntensity: { value: glowIntensity },\n uIntensity: { value: intensity },\n uScale: { value: scale },\n uNoiseScale: { value: noiseScale },\n uMouse: { value: [0, 0] },\n uPupilFollow: { value: pupilFollow },\n uFlameSpeed: { value: flameSpeed },\n uEyeColor: { value: hexToVec3(eyeColor) },\n uBgColor: { value: hexToVec3(backgroundColor) }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n container.appendChild(gl.canvas);\n\n let animationFrameId;\n\n function update(time) {\n animationFrameId = requestAnimationFrame(update);\n mouse.x += (mouse.tx - mouse.x) * 0.05;\n mouse.y += (mouse.ty - mouse.y) * 0.05;\n program.uniforms.uMouse.value = [mouse.x, mouse.y];\n program.uniforms.uTime.value = time * 0.001;\n renderer.render({ scene: mesh });\n }\n animationFrameId = requestAnimationFrame(update);\n\n return () => {\n cancelAnimationFrame(animationFrameId);\n window.removeEventListener('resize', resize);\n container.removeEventListener('mousemove', onMouseMove);\n container.removeEventListener('mouseleave', onMouseLeave);\n container.removeChild(gl.canvas);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, [eyeColor, intensity, pupilSize, irisWidth, glowIntensity, scale, noiseScale, pupilFollow, flameSpeed, backgroundColor]);\n\n return
;\n}\n" } ], diff --git a/public/r/EvilEye-TS-CSS.json b/public/r/EvilEye-TS-CSS.json index cad2755c..665dfb5d 100644 --- a/public/r/EvilEye-TS-CSS.json +++ b/public/r/EvilEye-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "EvilEye/EvilEye.css", + "type": "registry:file", + "path": "EvilEye.css", + "target": "@components/EvilEye.css", "content": ".evil-eye-container {\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "EvilEye/EvilEye.tsx", + "path": "EvilEye.tsx", "content": "import { Renderer, Program, Mesh, Triangle, Texture } from 'ogl';\nimport { useEffect, useRef } from 'react';\nimport './EvilEye.css';\n\ninterface EvilEyeProps {\n eyeColor?: string;\n intensity?: number;\n pupilSize?: number;\n irisWidth?: number;\n glowIntensity?: number;\n scale?: number;\n noiseScale?: number;\n pupilFollow?: number;\n flameSpeed?: number;\n backgroundColor?: string;\n}\n\nfunction hexToVec3(hex: string): [number, number, number] {\n const h = hex.replace('#', '');\n return [\n parseInt(h.slice(0, 2), 16) / 255,\n parseInt(h.slice(2, 4), 16) / 255,\n parseInt(h.slice(4, 6), 16) / 255\n ];\n}\n\nfunction generateNoiseTexture(size = 256): Uint8Array {\n const data = new Uint8Array(size * size * 4);\n\n function hash(x: number, y: number, s: number): number {\n let n = x * 374761393 + y * 668265263 + s * 1274126177;\n n = Math.imul(n ^ (n >>> 13), 1274126177);\n return ((n ^ (n >>> 16)) >>> 0) / 4294967296;\n }\n\n function noise(px: number, py: number, freq: number, seed: number): number {\n const fx = (px / size) * freq;\n const fy = (py / size) * freq;\n const ix = Math.floor(fx);\n const iy = Math.floor(fy);\n const tx = fx - ix;\n const ty = fy - iy;\n const w = freq | 0;\n const v00 = hash(((ix % w) + w) % w, ((iy % w) + w) % w, seed);\n const v10 = hash((((ix + 1) % w) + w) % w, ((iy % w) + w) % w, seed);\n const v01 = hash(((ix % w) + w) % w, (((iy + 1) % w) + w) % w, seed);\n const v11 = hash((((ix + 1) % w) + w) % w, (((iy + 1) % w) + w) % w, seed);\n return v00 * (1 - tx) * (1 - ty) + v10 * tx * (1 - ty) + v01 * (1 - tx) * ty + v11 * tx * ty;\n }\n\n for (let y = 0; y < size; y++) {\n for (let x = 0; x < size; x++) {\n let v = 0;\n let amp = 0.4;\n let totalAmp = 0;\n for (let o = 0; o < 8; o++) {\n const f = 32 * (1 << o);\n v += amp * noise(x, y, f, o * 31);\n totalAmp += amp;\n amp *= 0.65;\n }\n v /= totalAmp;\n v = (v - 0.5) * 2.2 + 0.5;\n v = Math.max(0, Math.min(1, v));\n const val = Math.round(v * 255);\n const i = (y * size + x) * 4;\n data[i] = val;\n data[i + 1] = val;\n data[i + 2] = val;\n data[i + 3] = 255;\n }\n }\n\n return data;\n}\n\nconst vertexShader = `\nattribute vec2 uv;\nattribute vec2 position;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0, 1);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float uTime;\nuniform vec3 uResolution;\nuniform sampler2D uNoiseTexture;\nuniform float uPupilSize;\nuniform float uIrisWidth;\nuniform float uGlowIntensity;\nuniform float uIntensity;\nuniform float uScale;\nuniform float uNoiseScale;\nuniform vec2 uMouse;\nuniform float uPupilFollow;\nuniform float uFlameSpeed;\nuniform vec3 uEyeColor;\nuniform vec3 uBgColor;\n\nvoid main() {\n vec2 uv = (gl_FragCoord.xy * 2.0 - uResolution.xy) / uResolution.y;\n uv /= uScale;\n float ft = uTime * uFlameSpeed;\n\n float polarRadius = length(uv) * 2.0;\n float polarAngle = (2.0 * atan(uv.x, uv.y)) / 6.28 * 0.3;\n vec2 polarUv = vec2(polarRadius, polarAngle);\n\n vec4 noiseA = texture2D(uNoiseTexture, polarUv * vec2(0.2, 7.0) * uNoiseScale + vec2(-ft * 0.1, 0.0));\n vec4 noiseB = texture2D(uNoiseTexture, polarUv * vec2(0.3, 4.0) * uNoiseScale + vec2(-ft * 0.2, 0.0));\n vec4 noiseC = texture2D(uNoiseTexture, polarUv * vec2(0.1, 5.0) * uNoiseScale + vec2(-ft * 0.1, 0.0));\n\n float distanceMask = 1.0 - length(uv);\n\n // Inner ring\n float innerRing = clamp(-1.0 * ((distanceMask - 0.7) / uIrisWidth), 0.0, 1.0);\n innerRing = (innerRing * distanceMask - 0.2) / 0.28;\n innerRing += noiseA.r - 0.5;\n innerRing *= 1.3;\n innerRing = clamp(innerRing, 0.0, 1.0);\n\n float outerRing = clamp(-1.0 * ((distanceMask - 0.5) / 0.2), 0.0, 1.0);\n outerRing = (outerRing * distanceMask - 0.1) / 0.38;\n outerRing += noiseC.r - 0.5;\n outerRing *= 1.3;\n outerRing = clamp(outerRing, 0.0, 1.0);\n\n innerRing += outerRing;\n\n // Inner eye\n float innerEye = distanceMask - 0.1 * 2.0;\n innerEye *= noiseB.r * 2.0;\n\n // Pupil with cursor tracking\n vec2 pupilOffset = uMouse * uPupilFollow * 0.12;\n vec2 pupilUv = uv - pupilOffset;\n float pupil = 1.0 - length(pupilUv * vec2(9.0, 2.3));\n pupil *= uPupilSize;\n pupil = clamp(pupil, 0.0, 1.0);\n pupil /= 0.35;\n\n // Outer eye\n float outerEyeGlow = 1.0 - length(uv * vec2(0.5, 1.5));\n outerEyeGlow = clamp(outerEyeGlow + 0.5, 0.0, 1.0);\n outerEyeGlow += noiseC.r - 0.5;\n float outerBgGlow = outerEyeGlow;\n outerEyeGlow = pow(outerEyeGlow, 2.0);\n outerEyeGlow += distanceMask;\n outerEyeGlow *= uGlowIntensity;\n outerEyeGlow = clamp(outerEyeGlow, 0.0, 1.0);\n outerEyeGlow *= pow(1.0 - distanceMask, 2.0) * 2.5;\n\n // Outer eye bg glow\n outerBgGlow += distanceMask;\n outerBgGlow = pow(outerBgGlow, 0.5);\n outerBgGlow *= 0.15;\n\n vec3 color = uEyeColor * uIntensity * clamp(max(innerRing + innerEye, outerEyeGlow + outerBgGlow) - pupil, 0.0, 3.0);\n color += uBgColor;\n\n gl_FragColor = vec4(color, 1.0);\n}\n`;\n\nexport default function EvilEye({\n eyeColor = '#FF6F37',\n intensity = 1.5,\n pupilSize = 0.6,\n irisWidth = 0.25,\n glowIntensity = 0.35,\n scale = 0.8,\n noiseScale = 1.0,\n pupilFollow = 1.0,\n flameSpeed = 1.0,\n backgroundColor = '#000000'\n}: EvilEyeProps) {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n const container = containerRef.current;\n const renderer = new Renderer({ alpha: true, premultipliedAlpha: false });\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n\n const noiseData = generateNoiseTexture(256);\n const noiseTexture = new Texture(gl, {\n image: noiseData,\n width: 256,\n height: 256,\n generateMipmaps: false,\n flipY: false,\n });\n noiseTexture.minFilter = gl.LINEAR;\n noiseTexture.magFilter = gl.LINEAR;\n noiseTexture.wrapS = gl.REPEAT;\n noiseTexture.wrapT = gl.REPEAT;\n\n const mouse = { x: 0, y: 0, tx: 0, ty: 0 };\n\n function onMouseMove(e: MouseEvent) {\n const rect = container.getBoundingClientRect();\n mouse.tx = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n mouse.ty = -(((e.clientY - rect.top) / rect.height) * 2 - 1);\n }\n\n function onMouseLeave() {\n mouse.tx = 0;\n mouse.ty = 0;\n }\n\n container.addEventListener('mousemove', onMouseMove);\n container.addEventListener('mouseleave', onMouseLeave);\n\n let program: Program;\n\n function resize() {\n renderer.setSize(container.offsetWidth, container.offsetHeight);\n if (program) {\n program.uniforms.uResolution.value = [gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height];\n }\n }\n window.addEventListener('resize', resize);\n resize();\n\n const geometry = new Triangle(gl);\n program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uResolution: { value: [gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height] },\n uNoiseTexture: { value: noiseTexture },\n uPupilSize: { value: pupilSize },\n uIrisWidth: { value: irisWidth },\n uGlowIntensity: { value: glowIntensity },\n uIntensity: { value: intensity },\n uScale: { value: scale },\n uNoiseScale: { value: noiseScale },\n uMouse: { value: [0, 0] },\n uPupilFollow: { value: pupilFollow },\n uFlameSpeed: { value: flameSpeed },\n uEyeColor: { value: hexToVec3(eyeColor) },\n uBgColor: { value: hexToVec3(backgroundColor) }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n container.appendChild(gl.canvas);\n\n let animationFrameId: number;\n\n function update(time: number) {\n animationFrameId = requestAnimationFrame(update);\n mouse.x += (mouse.tx - mouse.x) * 0.05;\n mouse.y += (mouse.ty - mouse.y) * 0.05;\n program.uniforms.uMouse.value = [mouse.x, mouse.y];\n program.uniforms.uTime.value = time * 0.001;\n renderer.render({ scene: mesh });\n }\n animationFrameId = requestAnimationFrame(update);\n\n return () => {\n cancelAnimationFrame(animationFrameId);\n window.removeEventListener('resize', resize);\n container.removeEventListener('mousemove', onMouseMove);\n container.removeEventListener('mouseleave', onMouseLeave);\n container.removeChild(gl.canvas);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, [eyeColor, intensity, pupilSize, irisWidth, glowIntensity, scale, noiseScale, pupilFollow, flameSpeed, backgroundColor]);\n\n return
;\n}\n" } ], diff --git a/public/r/FallingText-JS-CSS.json b/public/r/FallingText-JS-CSS.json index 5ee5ca81..eeae469d 100644 --- a/public/r/FallingText-JS-CSS.json +++ b/public/r/FallingText-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "FallingText/FallingText.css", + "type": "registry:file", + "path": "FallingText.css", + "target": "@components/FallingText.css", "content": ".falling-text-container {\n position: relative;\n z-index: 1;\n width: 100%;\n height: 100%;\n cursor: pointer;\n text-align: center;\n padding-top: 2em;\n}\n\n.falling-text-target {\n display: inline-block;\n}\n\n.word {\n display: inline-block;\n margin: 0 2px;\n user-select: none;\n}\n\n.highlighted {\n color: cyan;\n font-weight: bold;\n}\n\n.falling-text-canvas {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 0;\n}\n" }, { "type": "registry:component", - "path": "FallingText/FallingText.jsx", + "path": "FallingText.jsx", "content": "import { useRef, useState, useEffect } from 'react';\nimport Matter from 'matter-js';\nimport './FallingText.css';\n\nconst FallingText = ({\n className = '',\n text = '',\n highlightWords = [],\n highlightClass = 'highlighted',\n trigger = 'auto',\n backgroundColor = 'transparent',\n wireframes = false,\n gravity = 1,\n mouseConstraintStiffness = 0.2,\n fontSize = '1rem'\n}) => {\n const containerRef = useRef(null);\n const textRef = useRef(null);\n const canvasContainerRef = useRef(null);\n\n const [effectStarted, setEffectStarted] = useState(false);\n\n useEffect(() => {\n if (!textRef.current) return;\n const words = text.split(' ');\n const newHTML = words\n .map(word => {\n const isHighlighted = highlightWords.some(hw => word.startsWith(hw));\n return `${word}`;\n })\n .join(' ');\n textRef.current.innerHTML = newHTML;\n }, [text, highlightWords, highlightClass]);\n\n useEffect(() => {\n if (trigger === 'auto') {\n setEffectStarted(true);\n return;\n }\n if (trigger === 'scroll' && containerRef.current) {\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) {\n setEffectStarted(true);\n observer.disconnect();\n }\n },\n { threshold: 0.1 }\n );\n observer.observe(containerRef.current);\n return () => observer.disconnect();\n }\n }, [trigger]);\n\n useEffect(() => {\n if (!effectStarted) return;\n\n const { Engine, Render, World, Bodies, Runner, Mouse, MouseConstraint } = Matter;\n\n const containerRect = containerRef.current.getBoundingClientRect();\n const width = containerRect.width;\n const height = containerRect.height;\n\n if (width <= 0 || height <= 0) {\n return;\n }\n\n const engine = Engine.create();\n engine.world.gravity.y = gravity;\n\n const render = Render.create({\n element: canvasContainerRef.current,\n engine,\n options: {\n width,\n height,\n background: backgroundColor,\n wireframes\n }\n });\n\n const boundaryOptions = {\n isStatic: true,\n render: { fillStyle: 'transparent' }\n };\n const floor = Bodies.rectangle(width / 2, height + 25, width, 50, boundaryOptions);\n const leftWall = Bodies.rectangle(-25, height / 2, 50, height, boundaryOptions);\n const rightWall = Bodies.rectangle(width + 25, height / 2, 50, height, boundaryOptions);\n const ceiling = Bodies.rectangle(width / 2, -25, width, 50, boundaryOptions);\n\n const wordSpans = textRef.current.querySelectorAll('.word');\n const wordBodies = [...wordSpans].map(elem => {\n const rect = elem.getBoundingClientRect();\n\n const x = rect.left - containerRect.left + rect.width / 2;\n const y = rect.top - containerRect.top + rect.height / 2;\n\n const body = Bodies.rectangle(x, y, rect.width, rect.height, {\n render: { fillStyle: 'transparent' },\n restitution: 0.8,\n frictionAir: 0.01,\n friction: 0.2\n });\n\n Matter.Body.setVelocity(body, {\n x: (Math.random() - 0.5) * 5,\n y: 0\n });\n Matter.Body.setAngularVelocity(body, (Math.random() - 0.5) * 0.05);\n return { elem, body };\n });\n\n wordBodies.forEach(({ elem, body }) => {\n elem.style.position = 'absolute';\n elem.style.left = `${body.position.x - body.bounds.max.x + body.bounds.min.x / 2}px`;\n elem.style.top = `${body.position.y - body.bounds.max.y + body.bounds.min.y / 2}px`;\n elem.style.transform = 'none';\n });\n\n const mouse = Mouse.create(containerRef.current);\n const mouseConstraint = MouseConstraint.create(engine, {\n mouse,\n constraint: {\n stiffness: mouseConstraintStiffness,\n render: { visible: false }\n }\n });\n render.mouse = mouse;\n\n World.add(engine.world, [floor, leftWall, rightWall, ceiling, mouseConstraint, ...wordBodies.map(wb => wb.body)]);\n\n const runner = Runner.create();\n Runner.run(runner, engine);\n Render.run(render);\n\n const updateLoop = () => {\n wordBodies.forEach(({ body, elem }) => {\n const { x, y } = body.position;\n elem.style.left = `${x}px`;\n elem.style.top = `${y}px`;\n elem.style.transform = `translate(-50%, -50%) rotate(${body.angle}rad)`;\n });\n Matter.Engine.update(engine);\n requestAnimationFrame(updateLoop);\n };\n updateLoop();\n\n return () => {\n Render.stop(render);\n Runner.stop(runner);\n if (render.canvas && canvasContainerRef.current) {\n // eslint-disable-next-line react-hooks/exhaustive-deps\n canvasContainerRef.current.removeChild(render.canvas);\n }\n World.clear(engine.world);\n Engine.clear(engine);\n };\n }, [effectStarted, gravity, wireframes, backgroundColor, mouseConstraintStiffness]);\n\n const handleTrigger = () => {\n if (!effectStarted && (trigger === 'click' || trigger === 'hover')) {\n setEffectStarted(true);\n }\n };\n\n return (\n \n \n
\n
\n );\n};\n\nexport default FallingText;\n" } ], diff --git a/public/r/FallingText-TS-CSS.json b/public/r/FallingText-TS-CSS.json index c62bc2d1..3969d21a 100644 --- a/public/r/FallingText-TS-CSS.json +++ b/public/r/FallingText-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "FallingText/FallingText.css", + "type": "registry:file", + "path": "FallingText.css", + "target": "@components/FallingText.css", "content": ".falling-text-container {\n position: relative;\n z-index: 1;\n width: 100%;\n height: 100%;\n cursor: pointer;\n text-align: center;\n padding-top: 2em;\n}\n\n.falling-text-target {\n display: inline-block;\n}\n\n.word {\n display: inline-block;\n margin: 0 2px;\n user-select: none;\n}\n\n.highlighted {\n color: cyan;\n font-weight: bold;\n}\n\n.falling-text-canvas {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 0;\n}\n" }, { "type": "registry:component", - "path": "FallingText/FallingText.tsx", + "path": "FallingText.tsx", "content": "import { useRef, useState, useEffect } from 'react';\nimport Matter from 'matter-js';\nimport './FallingText.css';\n\ninterface FallingTextProps {\n text?: string;\n highlightWords?: string[];\n highlightClass?: string;\n trigger?: 'auto' | 'scroll' | 'click' | 'hover';\n backgroundColor?: string;\n wireframes?: boolean;\n gravity?: number;\n mouseConstraintStiffness?: number;\n fontSize?: string;\n}\n\nconst FallingText: React.FC = ({\n text = '',\n highlightWords = [],\n highlightClass = 'highlighted',\n trigger = 'auto',\n backgroundColor = 'transparent',\n wireframes = false,\n gravity = 1,\n mouseConstraintStiffness = 0.2,\n fontSize = '1rem'\n}) => {\n const containerRef = useRef(null);\n const textRef = useRef(null);\n const canvasContainerRef = useRef(null);\n\n const [effectStarted, setEffectStarted] = useState(false);\n\n useEffect(() => {\n if (!textRef.current) return;\n const words = text.split(' ');\n const newHTML = words\n .map(word => {\n const isHighlighted = highlightWords.some(hw => word.startsWith(hw));\n return `${word}`;\n })\n .join(' ');\n textRef.current.innerHTML = newHTML;\n }, [text, highlightWords, highlightClass]);\n\n useEffect(() => {\n if (trigger === 'auto') {\n setEffectStarted(true);\n return;\n }\n if (trigger === 'scroll' && containerRef.current) {\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) {\n setEffectStarted(true);\n observer.disconnect();\n }\n },\n { threshold: 0.1 }\n );\n observer.observe(containerRef.current);\n return () => observer.disconnect();\n }\n }, [trigger]);\n\n useEffect(() => {\n if (!effectStarted) return;\n\n const { Engine, Render, World, Bodies, Runner, Mouse, MouseConstraint } = Matter;\n\n if (!containerRef.current || !canvasContainerRef.current || !textRef.current) return;\n\n const containerRect = containerRef.current.getBoundingClientRect();\n const width = containerRect.width;\n const height = containerRect.height;\n\n if (width <= 0 || height <= 0) {\n return;\n }\n\n const engine = Engine.create();\n engine.world.gravity.y = gravity;\n\n const render = Render.create({\n element: canvasContainerRef.current,\n engine,\n options: {\n width,\n height,\n background: backgroundColor,\n wireframes\n }\n });\n\n const boundaryOptions = {\n isStatic: true,\n render: { fillStyle: 'transparent' }\n };\n const floor = Bodies.rectangle(width / 2, height + 25, width, 50, boundaryOptions);\n const leftWall = Bodies.rectangle(-25, height / 2, 50, height, boundaryOptions);\n const rightWall = Bodies.rectangle(width + 25, height / 2, 50, height, boundaryOptions);\n const ceiling = Bodies.rectangle(width / 2, -25, width, 50, boundaryOptions);\n\n const wordSpans = textRef.current.querySelectorAll('.word');\n const wordBodies = Array.from(wordSpans).map(elem => {\n const rect = elem.getBoundingClientRect();\n\n const x = rect.left - containerRect.left + rect.width / 2;\n const y = rect.top - containerRect.top + rect.height / 2;\n\n const body = Bodies.rectangle(x, y, rect.width, rect.height, {\n render: { fillStyle: 'transparent' },\n restitution: 0.8,\n frictionAir: 0.01,\n friction: 0.2\n });\n\n Matter.Body.setVelocity(body, {\n x: (Math.random() - 0.5) * 5,\n y: 0\n });\n Matter.Body.setAngularVelocity(body, (Math.random() - 0.5) * 0.05);\n return { elem, body };\n });\n\n wordBodies.forEach(({ elem, body }) => {\n elem.style.position = 'absolute';\n elem.style.left = `${body.position.x - body.bounds.max.x + body.bounds.min.x / 2}px`;\n elem.style.top = `${body.position.y - body.bounds.max.y + body.bounds.min.y / 2}px`;\n elem.style.transform = 'none';\n });\n\n const mouse = Mouse.create(containerRef.current);\n const mouseConstraint = MouseConstraint.create(engine, {\n mouse,\n constraint: {\n stiffness: mouseConstraintStiffness,\n render: { visible: false }\n }\n });\n render.mouse = mouse;\n\n World.add(engine.world, [floor, leftWall, rightWall, ceiling, mouseConstraint, ...wordBodies.map(wb => wb.body)]);\n\n const runner = Runner.create();\n Runner.run(runner, engine);\n Render.run(render);\n\n const updateLoop = () => {\n wordBodies.forEach(({ body, elem }) => {\n const { x, y } = body.position;\n elem.style.left = `${x}px`;\n elem.style.top = `${y}px`;\n elem.style.transform = `translate(-50%, -50%) rotate(${body.angle}rad)`;\n });\n Matter.Engine.update(engine);\n requestAnimationFrame(updateLoop);\n };\n updateLoop();\n\n return () => {\n Render.stop(render);\n Runner.stop(runner);\n if (render.canvas && canvasContainerRef.current) {\n canvasContainerRef.current.removeChild(render.canvas);\n }\n World.clear(engine.world, false);\n Engine.clear(engine);\n };\n }, [effectStarted, gravity, wireframes, backgroundColor, mouseConstraintStiffness]);\n\n const handleTrigger = () => {\n if (!effectStarted && (trigger === 'click' || trigger === 'hover')) {\n setEffectStarted(true);\n }\n };\n\n return (\n \n \n
\n
\n );\n};\n\nexport default FallingText;\n" } ], diff --git a/public/r/FaultyTerminal-JS-CSS.json b/public/r/FaultyTerminal-JS-CSS.json index 00fafa26..42aa8bd9 100644 --- a/public/r/FaultyTerminal-JS-CSS.json +++ b/public/r/FaultyTerminal-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "FaultyTerminal/FaultyTerminal.css", + "type": "registry:file", + "path": "FaultyTerminal.css", + "target": "@components/FaultyTerminal.css", "content": ".faulty-terminal-container {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "FaultyTerminal/FaultyTerminal.jsx", + "path": "FaultyTerminal.jsx", "content": "import { Renderer, Program, Mesh, Color, Triangle } from 'ogl';\nimport { useEffect, useRef, useMemo, useCallback } from 'react';\nimport './FaultyTerminal.css';\n\nconst vertexShader = `\nattribute vec2 position;\nattribute vec2 uv;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision mediump float;\n\nvarying vec2 vUv;\n\nuniform float iTime;\nuniform vec3 iResolution;\nuniform float uScale;\n\nuniform vec2 uGridMul;\nuniform float uDigitSize;\nuniform float uScanlineIntensity;\nuniform float uGlitchAmount;\nuniform float uFlickerAmount;\nuniform float uNoiseAmp;\nuniform float uChromaticAberration;\nuniform float uDither;\nuniform float uCurvature;\nuniform vec3 uTint;\nuniform vec2 uMouse;\nuniform float uMouseStrength;\nuniform float uUseMouse;\nuniform float uPageLoadProgress;\nuniform float uUsePageLoadAnimation;\nuniform float uBrightness;\n\nfloat time;\n\nfloat hash21(vec2 p){\n p = fract(p * 234.56);\n p += dot(p, p + 34.56);\n return fract(p.x * p.y);\n}\n\nfloat noise(vec2 p)\n{\n return sin(p.x * 10.0) * sin(p.y * (3.0 + sin(time * 0.090909))) + 0.2; \n}\n\nmat2 rotate(float angle)\n{\n float c = cos(angle);\n float s = sin(angle);\n return mat2(c, -s, s, c);\n}\n\nfloat fbm(vec2 p)\n{\n p *= 1.1;\n float f = 0.0;\n float amp = 0.5 * uNoiseAmp;\n \n mat2 modify0 = rotate(time * 0.02);\n f += amp * noise(p);\n p = modify0 * p * 2.0;\n amp *= 0.454545;\n \n mat2 modify1 = rotate(time * 0.02);\n f += amp * noise(p);\n p = modify1 * p * 2.0;\n amp *= 0.454545;\n \n mat2 modify2 = rotate(time * 0.08);\n f += amp * noise(p);\n \n return f;\n}\n\nfloat pattern(vec2 p, out vec2 q, out vec2 r) {\n vec2 offset1 = vec2(1.0);\n vec2 offset0 = vec2(0.0);\n mat2 rot01 = rotate(0.1 * time);\n mat2 rot1 = rotate(0.1);\n \n q = vec2(fbm(p + offset1), fbm(rot01 * p + offset1));\n r = vec2(fbm(rot1 * q + offset0), fbm(q + offset0));\n return fbm(p + r);\n}\n\nfloat digit(vec2 p){\n vec2 grid = uGridMul * 15.0;\n vec2 s = floor(p * grid) / grid;\n p = p * grid;\n vec2 q, r;\n float intensity = pattern(s * 0.1, q, r) * 1.3 - 0.03;\n \n if(uUseMouse > 0.5){\n vec2 mouseWorld = uMouse * uScale;\n float distToMouse = distance(s, mouseWorld);\n float mouseInfluence = exp(-distToMouse * 8.0) * uMouseStrength * 10.0;\n intensity += mouseInfluence;\n \n float ripple = sin(distToMouse * 20.0 - iTime * 5.0) * 0.1 * mouseInfluence;\n intensity += ripple;\n }\n \n if(uUsePageLoadAnimation > 0.5){\n float cellRandom = fract(sin(dot(s, vec2(12.9898, 78.233))) * 43758.5453);\n float cellDelay = cellRandom * 0.8;\n float cellProgress = clamp((uPageLoadProgress - cellDelay) / 0.2, 0.0, 1.0);\n \n float fadeAlpha = smoothstep(0.0, 1.0, cellProgress);\n intensity *= fadeAlpha;\n }\n \n p = fract(p);\n p *= uDigitSize;\n \n float px5 = p.x * 5.0;\n float py5 = (1.0 - p.y) * 5.0;\n float x = fract(px5);\n float y = fract(py5);\n \n float i = floor(py5) - 2.0;\n float j = floor(px5) - 2.0;\n float n = i * i + j * j;\n float f = n * 0.0625;\n \n float isOn = step(0.1, intensity - f);\n float brightness = isOn * (0.2 + y * 0.8) * (0.75 + x * 0.25);\n \n return step(0.0, p.x) * step(p.x, 1.0) * step(0.0, p.y) * step(p.y, 1.0) * brightness;\n}\n\nfloat onOff(float a, float b, float c)\n{\n return step(c, sin(iTime + a * cos(iTime * b))) * uFlickerAmount;\n}\n\nfloat displace(vec2 look)\n{\n float y = look.y - mod(iTime * 0.25, 1.0);\n float window = 1.0 / (1.0 + 50.0 * y * y);\n return sin(look.y * 20.0 + iTime) * 0.0125 * onOff(4.0, 2.0, 0.8) * (1.0 + cos(iTime * 60.0)) * window;\n}\n\nvec3 getColor(vec2 p){\n \n float bar = step(mod(p.y + time * 20.0, 1.0), 0.2) * 0.4 + 1.0;\n bar *= uScanlineIntensity;\n \n float displacement = displace(p);\n p.x += displacement;\n\n if (uGlitchAmount != 1.0) {\n float extra = displacement * (uGlitchAmount - 1.0);\n p.x += extra;\n }\n\n float middle = digit(p);\n \n const float off = 0.002;\n float sum = digit(p + vec2(-off, -off)) + digit(p + vec2(0.0, -off)) + digit(p + vec2(off, -off)) +\n digit(p + vec2(-off, 0.0)) + digit(p + vec2(0.0, 0.0)) + digit(p + vec2(off, 0.0)) +\n digit(p + vec2(-off, off)) + digit(p + vec2(0.0, off)) + digit(p + vec2(off, off));\n \n vec3 baseColor = vec3(0.9) * middle + sum * 0.1 * vec3(1.0) * bar;\n return baseColor;\n}\n\nvec2 barrel(vec2 uv){\n vec2 c = uv * 2.0 - 1.0;\n float r2 = dot(c, c);\n c *= 1.0 + uCurvature * r2;\n return c * 0.5 + 0.5;\n}\n\nvoid main() {\n time = iTime * 0.333333;\n vec2 uv = vUv;\n\n if(uCurvature != 0.0){\n uv = barrel(uv);\n }\n \n vec2 p = uv * uScale;\n vec3 col = getColor(p);\n\n if(uChromaticAberration != 0.0){\n vec2 ca = vec2(uChromaticAberration) / iResolution.xy;\n col.r = getColor(p + ca).r;\n col.b = getColor(p - ca).b;\n }\n\n col *= uTint;\n col *= uBrightness;\n\n if(uDither > 0.0){\n float rnd = hash21(gl_FragCoord.xy);\n col += (rnd - 0.5) * (uDither * 0.003922);\n }\n\n gl_FragColor = vec4(col, 1.0);\n}\n`;\n\nfunction hexToRgb(hex) {\n let h = hex.replace('#', '').trim();\n if (h.length === 3)\n h = h\n .split('')\n .map(c => c + c)\n .join('');\n const num = parseInt(h.slice(0, 6), 16);\n return [((num >> 16) & 255) / 255, ((num >> 8) & 255) / 255, (num & 255) / 255];\n}\n\nexport default function FaultyTerminal({\n scale = 1,\n gridMul = [2, 1],\n digitSize = 1.5,\n timeScale = 0.3,\n pause = false,\n scanlineIntensity = 0.3,\n glitchAmount = 1,\n flickerAmount = 1,\n noiseAmp = 0,\n chromaticAberration = 0,\n dither = 0,\n curvature = 0.2,\n tint = '#ffffff',\n mouseReact = true,\n mouseStrength = 0.2,\n dpr = Math.min(window.devicePixelRatio || 1, 2),\n pageLoadAnimation = true,\n brightness = 1,\n className,\n style,\n ...rest\n}) {\n const containerRef = useRef(null);\n const programRef = useRef(null);\n const rendererRef = useRef(null);\n const mouseRef = useRef({ x: 0.5, y: 0.5 });\n const smoothMouseRef = useRef({ x: 0.5, y: 0.5 });\n const frozenTimeRef = useRef(0);\n const rafRef = useRef(0);\n const loadAnimationStartRef = useRef(0);\n const timeOffsetRef = useRef(Math.random() * 100);\n\n const tintVec = useMemo(() => hexToRgb(tint), [tint]);\n\n const ditherValue = useMemo(() => (typeof dither === 'boolean' ? (dither ? 1 : 0) : dither), [dither]);\n\n const handleMouseMove = useCallback(e => {\n const ctn = containerRef.current;\n if (!ctn) return;\n const rect = ctn.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1 - (e.clientY - rect.top) / rect.height;\n mouseRef.current = { x, y };\n }, []);\n\n useEffect(() => {\n const ctn = containerRef.current;\n if (!ctn) return;\n\n const renderer = new Renderer({ dpr });\n rendererRef.current = renderer;\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 1);\n\n const geometry = new Triangle(gl);\n\n const program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n iTime: { value: 0 },\n iResolution: {\n value: new Color(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height)\n },\n uScale: { value: scale },\n\n uGridMul: { value: new Float32Array(gridMul) },\n uDigitSize: { value: digitSize },\n uScanlineIntensity: { value: scanlineIntensity },\n uGlitchAmount: { value: glitchAmount },\n uFlickerAmount: { value: flickerAmount },\n uNoiseAmp: { value: noiseAmp },\n uChromaticAberration: { value: chromaticAberration },\n uDither: { value: ditherValue },\n uCurvature: { value: curvature },\n uTint: { value: new Color(tintVec[0], tintVec[1], tintVec[2]) },\n uMouse: {\n value: new Float32Array([smoothMouseRef.current.x, smoothMouseRef.current.y])\n },\n uMouseStrength: { value: mouseStrength },\n uUseMouse: { value: mouseReact ? 1 : 0 },\n uPageLoadProgress: { value: pageLoadAnimation ? 0 : 1 },\n uUsePageLoadAnimation: { value: pageLoadAnimation ? 1 : 0 },\n uBrightness: { value: brightness }\n }\n });\n programRef.current = program;\n\n const mesh = new Mesh(gl, { geometry, program });\n\n function resize() {\n if (!ctn || !renderer) return;\n renderer.setSize(ctn.offsetWidth, ctn.offsetHeight);\n program.uniforms.iResolution.value = new Color(\n gl.canvas.width,\n gl.canvas.height,\n gl.canvas.width / gl.canvas.height\n );\n }\n\n const resizeObserver = new ResizeObserver(() => resize());\n resizeObserver.observe(ctn);\n resize();\n\n const update = t => {\n rafRef.current = requestAnimationFrame(update);\n\n if (pageLoadAnimation && loadAnimationStartRef.current === 0) {\n loadAnimationStartRef.current = t;\n }\n\n if (!pause) {\n const elapsed = (t * 0.001 + timeOffsetRef.current) * timeScale;\n program.uniforms.iTime.value = elapsed;\n frozenTimeRef.current = elapsed;\n } else {\n program.uniforms.iTime.value = frozenTimeRef.current;\n }\n\n if (pageLoadAnimation && loadAnimationStartRef.current > 0) {\n const animationDuration = 2000;\n const animationElapsed = t - loadAnimationStartRef.current;\n const progress = Math.min(animationElapsed / animationDuration, 1);\n program.uniforms.uPageLoadProgress.value = progress;\n }\n\n if (mouseReact) {\n const dampingFactor = 0.08;\n const smoothMouse = smoothMouseRef.current;\n const mouse = mouseRef.current;\n smoothMouse.x += (mouse.x - smoothMouse.x) * dampingFactor;\n smoothMouse.y += (mouse.y - smoothMouse.y) * dampingFactor;\n\n const mouseUniform = program.uniforms.uMouse.value;\n mouseUniform[0] = smoothMouse.x;\n mouseUniform[1] = smoothMouse.y;\n }\n\n renderer.render({ scene: mesh });\n };\n rafRef.current = requestAnimationFrame(update);\n ctn.appendChild(gl.canvas);\n\n if (mouseReact) ctn.addEventListener('mousemove', handleMouseMove);\n\n return () => {\n cancelAnimationFrame(rafRef.current);\n resizeObserver.disconnect();\n if (mouseReact) ctn.removeEventListener('mousemove', handleMouseMove);\n if (gl.canvas.parentElement === ctn) ctn.removeChild(gl.canvas);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n loadAnimationStartRef.current = 0;\n timeOffsetRef.current = Math.random() * 100;\n };\n }, [\n dpr,\n pause,\n timeScale,\n scale,\n gridMul,\n digitSize,\n scanlineIntensity,\n glitchAmount,\n flickerAmount,\n noiseAmp,\n chromaticAberration,\n ditherValue,\n curvature,\n tintVec,\n mouseReact,\n mouseStrength,\n pageLoadAnimation,\n brightness,\n handleMouseMove\n ]);\n\n return
;\n}\n" } ], diff --git a/public/r/FaultyTerminal-TS-CSS.json b/public/r/FaultyTerminal-TS-CSS.json index eed7ee21..e04a58cd 100644 --- a/public/r/FaultyTerminal-TS-CSS.json +++ b/public/r/FaultyTerminal-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "FaultyTerminal/FaultyTerminal.css", + "type": "registry:file", + "path": "FaultyTerminal.css", + "target": "@components/FaultyTerminal.css", "content": ".faulty-terminal-container {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "FaultyTerminal/FaultyTerminal.tsx", + "path": "FaultyTerminal.tsx", "content": "import { Renderer, Program, Mesh, Color, Triangle } from 'ogl';\nimport React, { useEffect, useRef, useMemo, useCallback } from 'react';\nimport './FaultyTerminal.css';\n\ntype Vec2 = [number, number];\n\nexport interface FaultyTerminalProps extends React.HTMLAttributes {\n scale?: number;\n gridMul?: Vec2;\n digitSize?: number;\n timeScale?: number;\n pause?: boolean;\n scanlineIntensity?: number;\n glitchAmount?: number;\n flickerAmount?: number;\n noiseAmp?: number;\n chromaticAberration?: number;\n dither?: number | boolean;\n curvature?: number;\n tint?: string;\n mouseReact?: boolean;\n mouseStrength?: number;\n dpr?: number;\n pageLoadAnimation?: boolean;\n brightness?: number;\n}\n\nconst vertexShader = `\nattribute vec2 position;\nattribute vec2 uv;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision mediump float;\n\nvarying vec2 vUv;\n\nuniform float iTime;\nuniform vec3 iResolution;\nuniform float uScale;\n\nuniform vec2 uGridMul;\nuniform float uDigitSize;\nuniform float uScanlineIntensity;\nuniform float uGlitchAmount;\nuniform float uFlickerAmount;\nuniform float uNoiseAmp;\nuniform float uChromaticAberration;\nuniform float uDither;\nuniform float uCurvature;\nuniform vec3 uTint;\nuniform vec2 uMouse;\nuniform float uMouseStrength;\nuniform float uUseMouse;\nuniform float uPageLoadProgress;\nuniform float uUsePageLoadAnimation;\nuniform float uBrightness;\n\nfloat time;\n\nfloat hash21(vec2 p){\n p = fract(p * 234.56);\n p += dot(p, p + 34.56);\n return fract(p.x * p.y);\n}\n\nfloat noise(vec2 p)\n{\n return sin(p.x * 10.0) * sin(p.y * (3.0 + sin(time * 0.090909))) + 0.2; \n}\n\nmat2 rotate(float angle)\n{\n float c = cos(angle);\n float s = sin(angle);\n return mat2(c, -s, s, c);\n}\n\nfloat fbm(vec2 p)\n{\n p *= 1.1;\n float f = 0.0;\n float amp = 0.5 * uNoiseAmp;\n \n mat2 modify0 = rotate(time * 0.02);\n f += amp * noise(p);\n p = modify0 * p * 2.0;\n amp *= 0.454545;\n \n mat2 modify1 = rotate(time * 0.02);\n f += amp * noise(p);\n p = modify1 * p * 2.0;\n amp *= 0.454545;\n \n mat2 modify2 = rotate(time * 0.08);\n f += amp * noise(p);\n \n return f;\n}\n\nfloat pattern(vec2 p, out vec2 q, out vec2 r) {\n vec2 offset1 = vec2(1.0);\n vec2 offset0 = vec2(0.0);\n mat2 rot01 = rotate(0.1 * time);\n mat2 rot1 = rotate(0.1);\n \n q = vec2(fbm(p + offset1), fbm(rot01 * p + offset1));\n r = vec2(fbm(rot1 * q + offset0), fbm(q + offset0));\n return fbm(p + r);\n}\n\nfloat digit(vec2 p){\n vec2 grid = uGridMul * 15.0;\n vec2 s = floor(p * grid) / grid;\n p = p * grid;\n vec2 q, r;\n float intensity = pattern(s * 0.1, q, r) * 1.3 - 0.03;\n \n if(uUseMouse > 0.5){\n vec2 mouseWorld = uMouse * uScale;\n float distToMouse = distance(s, mouseWorld);\n float mouseInfluence = exp(-distToMouse * 8.0) * uMouseStrength * 10.0;\n intensity += mouseInfluence;\n \n float ripple = sin(distToMouse * 20.0 - iTime * 5.0) * 0.1 * mouseInfluence;\n intensity += ripple;\n }\n \n if(uUsePageLoadAnimation > 0.5){\n float cellRandom = fract(sin(dot(s, vec2(12.9898, 78.233))) * 43758.5453);\n float cellDelay = cellRandom * 0.8;\n float cellProgress = clamp((uPageLoadProgress - cellDelay) / 0.2, 0.0, 1.0);\n \n float fadeAlpha = smoothstep(0.0, 1.0, cellProgress);\n intensity *= fadeAlpha;\n }\n \n p = fract(p);\n p *= uDigitSize;\n \n float px5 = p.x * 5.0;\n float py5 = (1.0 - p.y) * 5.0;\n float x = fract(px5);\n float y = fract(py5);\n \n float i = floor(py5) - 2.0;\n float j = floor(px5) - 2.0;\n float n = i * i + j * j;\n float f = n * 0.0625;\n \n float isOn = step(0.1, intensity - f);\n float brightness = isOn * (0.2 + y * 0.8) * (0.75 + x * 0.25);\n \n return step(0.0, p.x) * step(p.x, 1.0) * step(0.0, p.y) * step(p.y, 1.0) * brightness;\n}\n\nfloat onOff(float a, float b, float c)\n{\n\treturn step(c, sin(iTime + a * cos(iTime * b))) * uFlickerAmount;\n}\n\nfloat displace(vec2 look)\n{\n float y = look.y - mod(iTime * 0.25, 1.0);\n float window = 1.0 / (1.0 + 50.0 * y * y);\n\t return sin(look.y * 20.0 + iTime) * 0.0125 * onOff(4.0, 2.0, 0.8) * (1.0 + cos(iTime * 60.0)) * window;\n}\n\nvec3 getColor(vec2 p){\n \n float bar = step(mod(p.y + time * 20.0, 1.0), 0.2) * 0.4 + 1.0;\n bar *= uScanlineIntensity;\n \n float displacement = displace(p);\n p.x += displacement;\n\n if (uGlitchAmount != 1.0) {\n float extra = displacement * (uGlitchAmount - 1.0);\n p.x += extra;\n }\n\n float middle = digit(p);\n \n const float off = 0.002;\n float sum = digit(p + vec2(-off, -off)) + digit(p + vec2(0.0, -off)) + digit(p + vec2(off, -off)) +\n digit(p + vec2(-off, 0.0)) + digit(p + vec2(0.0, 0.0)) + digit(p + vec2(off, 0.0)) +\n digit(p + vec2(-off, off)) + digit(p + vec2(0.0, off)) + digit(p + vec2(off, off));\n \n vec3 baseColor = vec3(0.9) * middle + sum * 0.1 * vec3(1.0) * bar;\n return baseColor;\n}\n\nvec2 barrel(vec2 uv){\n vec2 c = uv * 2.0 - 1.0;\n float r2 = dot(c, c);\n c *= 1.0 + uCurvature * r2;\n return c * 0.5 + 0.5;\n}\n\nvoid main() {\n time = iTime * 0.333333;\n vec2 uv = vUv;\n\n if(uCurvature != 0.0){\n uv = barrel(uv);\n }\n \n vec2 p = uv * uScale;\n vec3 col = getColor(p);\n\n if(uChromaticAberration != 0.0){\n vec2 ca = vec2(uChromaticAberration) / iResolution.xy;\n col.r = getColor(p + ca).r;\n col.b = getColor(p - ca).b;\n }\n\n col *= uTint;\n col *= uBrightness;\n\n if(uDither > 0.0){\n float rnd = hash21(gl_FragCoord.xy);\n col += (rnd - 0.5) * (uDither * 0.003922);\n }\n\n gl_FragColor = vec4(col, 1.0);\n}\n`;\n\nfunction hexToRgb(hex: string): [number, number, number] {\n let h = hex.replace('#', '').trim();\n if (h.length === 3)\n h = h\n .split('')\n .map(c => c + c)\n .join('');\n const num = parseInt(h.slice(0, 6), 16);\n return [((num >> 16) & 255) / 255, ((num >> 8) & 255) / 255, (num & 255) / 255];\n}\n\nexport default function FaultyTerminal({\n scale = 1,\n gridMul = [2, 1],\n digitSize = 1.5,\n timeScale = 0.3,\n pause = false,\n scanlineIntensity = 0.3,\n glitchAmount = 1,\n flickerAmount = 1,\n noiseAmp = 1,\n chromaticAberration = 0,\n dither = 0,\n curvature = 0.2,\n tint = '#ffffff',\n mouseReact = true,\n mouseStrength = 0.2,\n dpr = Math.min(window.devicePixelRatio || 1, 2),\n pageLoadAnimation = true,\n brightness = 1,\n className,\n style,\n ...rest\n}: FaultyTerminalProps) {\n const containerRef = useRef(null);\n const programRef = useRef(null);\n const rendererRef = useRef(null);\n const mouseRef = useRef({ x: 0.5, y: 0.5 });\n const smoothMouseRef = useRef({ x: 0.5, y: 0.5 });\n const frozenTimeRef = useRef(0);\n const rafRef = useRef(0);\n const loadAnimationStartRef = useRef(0);\n const timeOffsetRef = useRef(Math.random() * 100);\n\n const tintVec = useMemo(() => hexToRgb(tint), [tint]);\n\n const ditherValue = useMemo(() => (typeof dither === 'boolean' ? (dither ? 1 : 0) : dither), [dither]);\n\n const handleMouseMove = useCallback((e: MouseEvent) => {\n const ctn = containerRef.current;\n if (!ctn) return;\n const rect = ctn.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1 - (e.clientY - rect.top) / rect.height;\n mouseRef.current = { x, y };\n }, []);\n\n useEffect(() => {\n const ctn = containerRef.current;\n if (!ctn) return;\n\n const renderer = new Renderer({ dpr });\n rendererRef.current = renderer;\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 1);\n\n const geometry = new Triangle(gl);\n\n const program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n iTime: { value: 0 },\n iResolution: {\n value: new Color(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height)\n },\n uScale: { value: scale },\n\n uGridMul: { value: new Float32Array(gridMul) },\n uDigitSize: { value: digitSize },\n uScanlineIntensity: { value: scanlineIntensity },\n uGlitchAmount: { value: glitchAmount },\n uFlickerAmount: { value: flickerAmount },\n uNoiseAmp: { value: noiseAmp },\n uChromaticAberration: { value: chromaticAberration },\n uDither: { value: ditherValue },\n uCurvature: { value: curvature },\n uTint: { value: new Color(tintVec[0], tintVec[1], tintVec[2]) },\n uMouse: {\n value: new Float32Array([smoothMouseRef.current.x, smoothMouseRef.current.y])\n },\n uMouseStrength: { value: mouseStrength },\n uUseMouse: { value: mouseReact ? 1 : 0 },\n uPageLoadProgress: { value: pageLoadAnimation ? 0 : 1 },\n uUsePageLoadAnimation: { value: pageLoadAnimation ? 1 : 0 },\n uBrightness: { value: brightness }\n }\n });\n programRef.current = program;\n\n const mesh = new Mesh(gl, { geometry, program });\n\n function resize() {\n if (!ctn || !renderer) return;\n renderer.setSize(ctn.offsetWidth, ctn.offsetHeight);\n program.uniforms.iResolution.value = new Color(\n gl.canvas.width,\n gl.canvas.height,\n gl.canvas.width / gl.canvas.height\n );\n }\n\n const resizeObserver = new ResizeObserver(() => resize());\n resizeObserver.observe(ctn);\n resize();\n\n const update = (t: number) => {\n rafRef.current = requestAnimationFrame(update);\n\n if (pageLoadAnimation && loadAnimationStartRef.current === 0) {\n loadAnimationStartRef.current = t;\n }\n\n if (!pause) {\n const elapsed = (t * 0.001 + timeOffsetRef.current) * timeScale;\n program.uniforms.iTime.value = elapsed;\n frozenTimeRef.current = elapsed;\n } else {\n program.uniforms.iTime.value = frozenTimeRef.current;\n }\n\n if (pageLoadAnimation && loadAnimationStartRef.current > 0) {\n const animationDuration = 2000;\n const animationElapsed = t - loadAnimationStartRef.current;\n const progress = Math.min(animationElapsed / animationDuration, 1);\n program.uniforms.uPageLoadProgress.value = progress;\n }\n\n if (mouseReact) {\n const dampingFactor = 0.08;\n const smoothMouse = smoothMouseRef.current;\n const mouse = mouseRef.current;\n smoothMouse.x += (mouse.x - smoothMouse.x) * dampingFactor;\n smoothMouse.y += (mouse.y - smoothMouse.y) * dampingFactor;\n\n const mouseUniform = program.uniforms.uMouse.value as Float32Array;\n mouseUniform[0] = smoothMouse.x;\n mouseUniform[1] = smoothMouse.y;\n }\n\n renderer.render({ scene: mesh });\n };\n rafRef.current = requestAnimationFrame(update);\n ctn.appendChild(gl.canvas);\n\n if (mouseReact) ctn.addEventListener('mousemove', handleMouseMove);\n\n return () => {\n cancelAnimationFrame(rafRef.current);\n resizeObserver.disconnect();\n if (mouseReact) ctn.removeEventListener('mousemove', handleMouseMove);\n if (gl.canvas.parentElement === ctn) ctn.removeChild(gl.canvas);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n loadAnimationStartRef.current = 0;\n timeOffsetRef.current = Math.random() * 100;\n };\n }, [\n dpr,\n pause,\n timeScale,\n scale,\n gridMul,\n digitSize,\n scanlineIntensity,\n glitchAmount,\n flickerAmount,\n noiseAmp,\n chromaticAberration,\n ditherValue,\n curvature,\n tintVec,\n mouseReact,\n mouseStrength,\n pageLoadAnimation,\n brightness,\n handleMouseMove\n ]);\n\n return
;\n}\n" } ], diff --git a/public/r/Ferrofluid-JS-CSS.json b/public/r/Ferrofluid-JS-CSS.json index b7918033..d041ccda 100644 --- a/public/r/Ferrofluid-JS-CSS.json +++ b/public/r/Ferrofluid-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Ferrofluid/Ferrofluid.css", + "type": "registry:file", + "path": "Ferrofluid.css", + "target": "@components/Ferrofluid.css", "content": ".ferrofluid-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "Ferrofluid/Ferrofluid.jsx", + "path": "Ferrofluid.jsx", "content": "import React, { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './Ferrofluid.css';\n\nconst MAX_COLORS = 8;\n\nconst hexToRGB = hex => {\n const c = hex.replace('#', '').padEnd(6, '0');\n const r = parseInt(c.slice(0, 2), 16) / 255;\n const g = parseInt(c.slice(2, 4), 16) / 255;\n const b = parseInt(c.slice(4, 6), 16) / 255;\n return [r, g, b];\n};\n\nconst prepColors = input => {\n const base = (input && input.length ? input : ['#4F46E5', '#06B6D4', '#E0F2FE']).slice(0, MAX_COLORS);\n const count = base.length;\n const arr = [];\n for (let i = 0; i < MAX_COLORS; i++) arr.push(hexToRGB(base[Math.min(i, base.length - 1)]));\n const avg = [0, 0, 0];\n for (let i = 0; i < count; i++) {\n avg[0] += arr[i][0];\n avg[1] += arr[i][1];\n avg[2] += arr[i][2];\n }\n avg[0] /= count;\n avg[1] /= count;\n avg[2] /= count;\n return { arr, count, avg };\n};\n\nconst flowVec = d => {\n switch (d) {\n case 'up':\n return [0, 1];\n case 'down':\n return [0, -1];\n case 'left':\n return [-1, 0];\n case 'right':\n return [1, 0];\n default:\n return [0, -1];\n }\n};\n\nconst vertex = `\nattribute vec2 position;\nattribute vec2 uv;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `\nprecision highp float;\n\nuniform vec3 iResolution;\nuniform vec2 iMouse;\nuniform float iTime;\n\nuniform vec3 uColor0;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nuniform vec3 uColor4;\nuniform vec3 uColor5;\nuniform vec3 uColor6;\nuniform vec3 uColor7;\nuniform int uColorCount;\n\nuniform vec3 uMouseColor;\nuniform vec2 uFlow;\nuniform float uSpeed;\nuniform float uScale;\nuniform float uTurbulence;\nuniform float uFluidity;\nuniform float uRimWidth;\nuniform float uSharpness;\nuniform float uShimmer;\nuniform float uGlow;\nuniform float uOpacity;\nuniform float uMouseEnabled;\nuniform float uMouseStrength;\nuniform float uMouseRadius;\n\nvarying vec2 vUv;\n\n#define PI 3.14159265\n\nvec3 palette(float h) {\n int count = uColorCount;\n if (count < 1) count = 1;\n int idx = int(floor(clamp(h, 0.0, 0.999999) * float(count)));\n if (idx <= 0) return uColor0;\n if (idx == 1) return uColor1;\n if (idx == 2) return uColor2;\n if (idx == 3) return uColor3;\n if (idx == 4) return uColor4;\n if (idx == 5) return uColor5;\n if (idx == 6) return uColor6;\n return uColor7;\n}\n\nfloat hash(vec3 p3) {\n p3 = fract(p3 * 0.1031);\n p3 += dot(p3, p3.zyx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfloat smin(float a, float b, float k) {\n float r = exp2(-a / k) + exp2(-b / k);\n return -k * log2(r);\n}\n\nfloat sinlerp(float a, float b, float w) {\n return mix(a, b, (sin(w * PI - PI / 2.0) + 1.0) / 2.0);\n}\n\nfloat vn(vec2 p, float s, float seed) {\n vec2 cellp = floor(p / s);\n vec2 relp = mod(p, s);\n float g1 = hash(vec3(cellp, seed));\n float g2 = hash(vec3(cellp.x + 1.0, cellp.y, seed));\n float g3 = hash(vec3(cellp.x + 1.0, cellp.y + 1.0, seed));\n float g4 = hash(vec3(cellp.x, cellp.y + 1.0, seed));\n float bx = sinlerp(g1, g2, relp.x / s);\n float tx = sinlerp(g4, g3, relp.x / s);\n return sinlerp(bx, tx, relp.y / s);\n}\n\nfloat dbn(vec2 p, float s, float seed) {\n float o = s / 2.0;\n float n0 = vn(p, s, seed);\n float n1 = vn(p + vec2(o, o), s, seed + 0.1);\n float n2 = vn(p + vec2(-o, o), s, seed + 0.2);\n float n3 = vn(p + vec2(o, -o), s, seed + 0.3);\n float n4 = vn(p + vec2(-o, -o), s, seed + 0.4);\n return (2.0 * n0 + 1.5 * n1 + 1.25 * n2 + 1.125 * n3 + n4) / 7.0;\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n float ref = 700.0 / max(uScale, 0.05);\n vec2 p = fragCoord / iResolution.y * ref;\n\n float spd = 200.0 * uSpeed;\n float t = iTime;\n\n vec2 dir = uFlow;\n vec2 perp = vec2(-dir.y, dir.x);\n\n float distort1 = vn(p + perp * (t * spd), 60.0, 10.0) * 50.0 * uTurbulence;\n float distort2 = vn(p - perp * (t * spd), 120.0, 15.0) * 100.0 * uTurbulence;\n\n float peaks = dbn(p + distort1 + dir * (t * spd * 0.5), 40.0, 1.0);\n float peaks2 = dbn(p + distort2 - dir * (t * spd * 0.5), 40.0, 0.0);\n\n float mapeaks = smin(peaks, peaks2, max(uFluidity, 0.001));\n\n float mGlow = 0.0;\n if (uMouseEnabled > 0.5) {\n vec2 mp = iMouse / iResolution.y * ref;\n float md = length(p - mp) / ref;\n float rr = max(uMouseRadius, 0.02);\n mGlow = exp(-md * md / (rr * rr)) * uMouseStrength;\n }\n\n float band = (uRimWidth - abs((mapeaks - 0.4) * 2.0)) * 5.0;\n float ltn = clamp(band - vn(p + dir * (t * spd * 0.5), 60.0, 12.0) * uShimmer, 0.0, 1.0);\n ltn = pow(ltn, uSharpness) * uGlow;\n ltn *= clamp(1.0 - mGlow, 0.0, 1.0);\n\n float h = clamp(0.5 + (peaks - peaks2) * 0.8, 0.0, 1.0);\n vec3 col = palette(h);\n\n vec3 outc = col * ltn;\n float a = clamp(max(outc.r, max(outc.g, outc.b)), 0.0, 1.0);\n fragColor = vec4(outc, a * uOpacity);\n}\n\nvoid main() {\n vec4 color;\n mainImage(color, vUv * iResolution.xy);\n gl_FragColor = color;\n}\n`;\n\nconst Ferrofluid = ({\n className,\n dpr,\n paused = false,\n colors = ['#ffffff', '#ffffff', '#ffffff'],\n speed = 0.5,\n scale = 1.6,\n turbulence = 1,\n fluidity = 0.1,\n rimWidth = 0.2,\n sharpness = 2.5,\n shimmer = 1.5,\n glow = 2,\n flowDirection = 'down',\n opacity = 1,\n mouseInteraction = true,\n mouseStrength = 1,\n mouseRadius = 0.35,\n mouseDampening = 0.15,\n mixBlendMode\n}) => {\n const containerRef = useRef(null);\n const rafRef = useRef(null);\n const programRef = useRef(null);\n const meshRef = useRef(null);\n const geometryRef = useRef(null);\n const rendererRef = useRef(null);\n const mouseTargetRef = useRef([0, 0]);\n const lastTimeRef = useRef(0);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n dpr: dpr ?? (typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1),\n alpha: true,\n antialias: true\n });\n rendererRef.current = renderer;\n const gl = renderer.gl;\n const canvas = gl.canvas;\n gl.clearColor(0, 0, 0, 0);\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const { arr, count, avg } = prepColors(colors);\n\n const uniforms = {\n iResolution: { value: [gl.drawingBufferWidth, gl.drawingBufferHeight, 1] },\n iMouse: { value: [0, 0] },\n iTime: { value: 0 },\n uColor0: { value: arr[0] },\n uColor1: { value: arr[1] },\n uColor2: { value: arr[2] },\n uColor3: { value: arr[3] },\n uColor4: { value: arr[4] },\n uColor5: { value: arr[5] },\n uColor6: { value: arr[6] },\n uColor7: { value: arr[7] },\n uColorCount: { value: count },\n uMouseColor: { value: avg },\n uFlow: { value: flowVec(flowDirection) },\n uSpeed: { value: speed },\n uScale: { value: scale },\n uTurbulence: { value: turbulence },\n uFluidity: { value: fluidity },\n uRimWidth: { value: rimWidth },\n uSharpness: { value: sharpness },\n uShimmer: { value: shimmer },\n uGlow: { value: glow },\n uOpacity: { value: opacity },\n uMouseEnabled: { value: mouseInteraction ? 1 : 0 },\n uMouseStrength: { value: mouseStrength },\n uMouseRadius: { value: mouseRadius }\n };\n\n const program = new Program(gl, { vertex, fragment, uniforms });\n programRef.current = program;\n\n const geometry = new Triangle(gl);\n geometryRef.current = geometry;\n const mesh = new Mesh(gl, { geometry, program });\n meshRef.current = mesh;\n\n const resize = () => {\n const rect = container.getBoundingClientRect();\n renderer.setSize(rect.width, rect.height);\n uniforms.iResolution.value = [gl.drawingBufferWidth, gl.drawingBufferHeight, 1];\n };\n\n resize();\n const ro = new ResizeObserver(resize);\n ro.observe(container);\n\n const onPointerMove = e => {\n const rect = canvas.getBoundingClientRect();\n const sc = renderer.dpr || 1;\n const x = (e.clientX - rect.left) * sc;\n const y = (rect.height - (e.clientY - rect.top)) * sc;\n mouseTargetRef.current = [x, y];\n if (mouseDampening <= 0) {\n uniforms.iMouse.value = [x, y];\n }\n };\n if (mouseInteraction) {\n canvas.addEventListener('pointermove', onPointerMove);\n }\n\n const loop = t => {\n rafRef.current = requestAnimationFrame(loop);\n uniforms.iTime.value = t * 0.001;\n if (mouseDampening > 0) {\n if (!lastTimeRef.current) lastTimeRef.current = t;\n const dt = (t - lastTimeRef.current) / 1000;\n lastTimeRef.current = t;\n const tau = Math.max(1e-4, mouseDampening);\n let factor = 1 - Math.exp(-dt / tau);\n if (factor > 1) factor = 1;\n const target = mouseTargetRef.current;\n const cur = uniforms.iMouse.value;\n cur[0] += (target[0] - cur[0]) * factor;\n cur[1] += (target[1] - cur[1]) * factor;\n } else {\n lastTimeRef.current = t;\n }\n if (!paused && programRef.current && meshRef.current) {\n try {\n renderer.render({ scene: meshRef.current });\n } catch (e) {\n console.error(e);\n }\n }\n };\n rafRef.current = requestAnimationFrame(loop);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n if (mouseInteraction) canvas.removeEventListener('pointermove', onPointerMove);\n ro.disconnect();\n if (canvas.parentElement === container) {\n container.removeChild(canvas);\n }\n const callIfFn = (obj, key) => {\n const fn = obj && obj[key];\n if (typeof fn === 'function') {\n fn.call(obj);\n }\n };\n callIfFn(programRef.current, 'remove');\n callIfFn(geometryRef.current, 'remove');\n callIfFn(meshRef.current, 'remove');\n callIfFn(rendererRef.current, 'destroy');\n programRef.current = null;\n geometryRef.current = null;\n meshRef.current = null;\n rendererRef.current = null;\n };\n }, [\n dpr,\n paused,\n colors,\n speed,\n scale,\n turbulence,\n fluidity,\n rimWidth,\n sharpness,\n shimmer,\n glow,\n flowDirection,\n opacity,\n mouseInteraction,\n mouseStrength,\n mouseRadius,\n mouseDampening\n ]);\n\n return (\n \n );\n};\n\nexport default Ferrofluid;\n" } ], diff --git a/public/r/Ferrofluid-TS-CSS.json b/public/r/Ferrofluid-TS-CSS.json index 285034ac..e9fabdf2 100644 --- a/public/r/Ferrofluid-TS-CSS.json +++ b/public/r/Ferrofluid-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Ferrofluid/Ferrofluid.css", + "type": "registry:file", + "path": "Ferrofluid.css", + "target": "@components/Ferrofluid.css", "content": ".ferrofluid-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "Ferrofluid/Ferrofluid.tsx", + "path": "Ferrofluid.tsx", "content": "import React, { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './Ferrofluid.css';\n\nexport interface FerrofluidProps {\n className?: string;\n dpr?: number;\n paused?: boolean;\n colors?: string[];\n speed?: number;\n scale?: number;\n turbulence?: number;\n fluidity?: number;\n rimWidth?: number;\n sharpness?: number;\n shimmer?: number;\n glow?: number;\n flowDirection?: 'up' | 'down' | 'left' | 'right';\n opacity?: number;\n mouseInteraction?: boolean;\n mouseStrength?: number;\n mouseRadius?: number;\n mouseDampening?: number;\n mixBlendMode?: string;\n}\n\ntype RGB = [number, number, number];\n\nconst MAX_COLORS = 8;\n\nconst hexToRGB = (hex: string): RGB => {\n const c = hex.replace('#', '').padEnd(6, '0');\n const r = parseInt(c.slice(0, 2), 16) / 255;\n const g = parseInt(c.slice(2, 4), 16) / 255;\n const b = parseInt(c.slice(4, 6), 16) / 255;\n return [r, g, b];\n};\n\nconst prepColors = (input?: string[]) => {\n const base = (input && input.length ? input : ['#4F46E5', '#06B6D4', '#E0F2FE']).slice(0, MAX_COLORS);\n const count = base.length;\n const arr: RGB[] = [];\n for (let i = 0; i < MAX_COLORS; i++) arr.push(hexToRGB(base[Math.min(i, base.length - 1)]));\n const avg: RGB = [0, 0, 0];\n for (let i = 0; i < count; i++) {\n avg[0] += arr[i][0];\n avg[1] += arr[i][1];\n avg[2] += arr[i][2];\n }\n avg[0] /= count;\n avg[1] /= count;\n avg[2] /= count;\n return { arr, count, avg };\n};\n\nconst flowVec = (d?: string): [number, number] => {\n switch (d) {\n case 'up':\n return [0, 1];\n case 'down':\n return [0, -1];\n case 'left':\n return [-1, 0];\n case 'right':\n return [1, 0];\n default:\n return [0, -1];\n }\n};\n\nconst vertex = `\nattribute vec2 position;\nattribute vec2 uv;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `\nprecision highp float;\n\nuniform vec3 iResolution;\nuniform vec2 iMouse;\nuniform float iTime;\n\nuniform vec3 uColor0;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nuniform vec3 uColor4;\nuniform vec3 uColor5;\nuniform vec3 uColor6;\nuniform vec3 uColor7;\nuniform int uColorCount;\n\nuniform vec3 uMouseColor;\nuniform vec2 uFlow;\nuniform float uSpeed;\nuniform float uScale;\nuniform float uTurbulence;\nuniform float uFluidity;\nuniform float uRimWidth;\nuniform float uSharpness;\nuniform float uShimmer;\nuniform float uGlow;\nuniform float uOpacity;\nuniform float uMouseEnabled;\nuniform float uMouseStrength;\nuniform float uMouseRadius;\n\nvarying vec2 vUv;\n\n#define PI 3.14159265\n\nvec3 palette(float h) {\n int count = uColorCount;\n if (count < 1) count = 1;\n int idx = int(floor(clamp(h, 0.0, 0.999999) * float(count)));\n if (idx <= 0) return uColor0;\n if (idx == 1) return uColor1;\n if (idx == 2) return uColor2;\n if (idx == 3) return uColor3;\n if (idx == 4) return uColor4;\n if (idx == 5) return uColor5;\n if (idx == 6) return uColor6;\n return uColor7;\n}\n\nfloat hash(vec3 p3) {\n p3 = fract(p3 * 0.1031);\n p3 += dot(p3, p3.zyx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfloat smin(float a, float b, float k) {\n float r = exp2(-a / k) + exp2(-b / k);\n return -k * log2(r);\n}\n\nfloat sinlerp(float a, float b, float w) {\n return mix(a, b, (sin(w * PI - PI / 2.0) + 1.0) / 2.0);\n}\n\nfloat vn(vec2 p, float s, float seed) {\n vec2 cellp = floor(p / s);\n vec2 relp = mod(p, s);\n float g1 = hash(vec3(cellp, seed));\n float g2 = hash(vec3(cellp.x + 1.0, cellp.y, seed));\n float g3 = hash(vec3(cellp.x + 1.0, cellp.y + 1.0, seed));\n float g4 = hash(vec3(cellp.x, cellp.y + 1.0, seed));\n float bx = sinlerp(g1, g2, relp.x / s);\n float tx = sinlerp(g4, g3, relp.x / s);\n return sinlerp(bx, tx, relp.y / s);\n}\n\nfloat dbn(vec2 p, float s, float seed) {\n float o = s / 2.0;\n float n0 = vn(p, s, seed);\n float n1 = vn(p + vec2(o, o), s, seed + 0.1);\n float n2 = vn(p + vec2(-o, o), s, seed + 0.2);\n float n3 = vn(p + vec2(o, -o), s, seed + 0.3);\n float n4 = vn(p + vec2(-o, -o), s, seed + 0.4);\n return (2.0 * n0 + 1.5 * n1 + 1.25 * n2 + 1.125 * n3 + n4) / 7.0;\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n float ref = 700.0 / max(uScale, 0.05);\n vec2 p = fragCoord / iResolution.y * ref;\n\n float spd = 200.0 * uSpeed;\n float t = iTime;\n\n vec2 dir = uFlow;\n vec2 perp = vec2(-dir.y, dir.x);\n\n float distort1 = vn(p + perp * (t * spd), 60.0, 10.0) * 50.0 * uTurbulence;\n float distort2 = vn(p - perp * (t * spd), 120.0, 15.0) * 100.0 * uTurbulence;\n\n float peaks = dbn(p + distort1 + dir * (t * spd * 0.5), 40.0, 1.0);\n float peaks2 = dbn(p + distort2 - dir * (t * spd * 0.5), 40.0, 0.0);\n\n float mapeaks = smin(peaks, peaks2, max(uFluidity, 0.001));\n\n float mGlow = 0.0;\n if (uMouseEnabled > 0.5) {\n vec2 mp = iMouse / iResolution.y * ref;\n float md = length(p - mp) / ref;\n float rr = max(uMouseRadius, 0.02);\n mGlow = exp(-md * md / (rr * rr)) * uMouseStrength;\n }\n\n float band = (uRimWidth - abs((mapeaks - 0.4) * 2.0)) * 5.0;\n float ltn = clamp(band - vn(p + dir * (t * spd * 0.5), 60.0, 12.0) * uShimmer, 0.0, 1.0);\n ltn = pow(ltn, uSharpness) * uGlow;\n ltn *= clamp(1.0 - mGlow, 0.0, 1.0);\n\n float h = clamp(0.5 + (peaks - peaks2) * 0.8, 0.0, 1.0);\n vec3 col = palette(h);\n\n vec3 outc = col * ltn;\n float a = clamp(max(outc.r, max(outc.g, outc.b)), 0.0, 1.0);\n fragColor = vec4(outc, a * uOpacity);\n}\n\nvoid main() {\n vec4 color;\n mainImage(color, vUv * iResolution.xy);\n gl_FragColor = color;\n}\n`;\n\nconst Ferrofluid: React.FC = ({\n className,\n dpr,\n paused = false,\n colors = ['#ffffff', '#ffffff', '#ffffff'],\n speed = 0.5,\n scale = 1.6,\n turbulence = 1,\n fluidity = 0.1,\n rimWidth = 0.2,\n sharpness = 2.5,\n shimmer = 1.5,\n glow = 2,\n flowDirection = 'down',\n opacity = 1,\n mouseInteraction = true,\n mouseStrength = 1,\n mouseRadius = 0.35,\n mouseDampening = 0.15,\n mixBlendMode\n}) => {\n const containerRef = useRef(null);\n const rafRef = useRef(null);\n const programRef = useRef(null);\n const meshRef = useRef(null);\n const geometryRef = useRef(null);\n const rendererRef = useRef(null);\n const mouseTargetRef = useRef<[number, number]>([0, 0]);\n const lastTimeRef = useRef(0);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n dpr: dpr ?? (typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1),\n alpha: true,\n antialias: true\n });\n rendererRef.current = renderer;\n const gl = renderer.gl;\n const canvas = gl.canvas as HTMLCanvasElement;\n gl.clearColor(0, 0, 0, 0);\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const { arr, count, avg } = prepColors(colors);\n\n const uniforms = {\n iResolution: { value: [gl.drawingBufferWidth, gl.drawingBufferHeight, 1] },\n iMouse: { value: [0, 0] },\n iTime: { value: 0 },\n uColor0: { value: arr[0] },\n uColor1: { value: arr[1] },\n uColor2: { value: arr[2] },\n uColor3: { value: arr[3] },\n uColor4: { value: arr[4] },\n uColor5: { value: arr[5] },\n uColor6: { value: arr[6] },\n uColor7: { value: arr[7] },\n uColorCount: { value: count },\n uMouseColor: { value: avg },\n uFlow: { value: flowVec(flowDirection) },\n uSpeed: { value: speed },\n uScale: { value: scale },\n uTurbulence: { value: turbulence },\n uFluidity: { value: fluidity },\n uRimWidth: { value: rimWidth },\n uSharpness: { value: sharpness },\n uShimmer: { value: shimmer },\n uGlow: { value: glow },\n uOpacity: { value: opacity },\n uMouseEnabled: { value: mouseInteraction ? 1 : 0 },\n uMouseStrength: { value: mouseStrength },\n uMouseRadius: { value: mouseRadius }\n };\n\n const program = new Program(gl, { vertex, fragment, uniforms });\n programRef.current = program;\n\n const geometry = new Triangle(gl);\n geometryRef.current = geometry;\n const mesh = new Mesh(gl, { geometry, program });\n meshRef.current = mesh;\n\n const resize = () => {\n const rect = container.getBoundingClientRect();\n renderer.setSize(rect.width, rect.height);\n uniforms.iResolution.value = [gl.drawingBufferWidth, gl.drawingBufferHeight, 1];\n };\n\n resize();\n const ro = new ResizeObserver(resize);\n ro.observe(container);\n\n const onPointerMove = (e: PointerEvent) => {\n const rect = canvas.getBoundingClientRect();\n const sc = renderer.dpr || 1;\n const x = (e.clientX - rect.left) * sc;\n const y = (rect.height - (e.clientY - rect.top)) * sc;\n mouseTargetRef.current = [x, y];\n if (mouseDampening <= 0) {\n uniforms.iMouse.value = [x, y];\n }\n };\n if (mouseInteraction) {\n canvas.addEventListener('pointermove', onPointerMove);\n }\n\n const loop = (t: number) => {\n rafRef.current = requestAnimationFrame(loop);\n uniforms.iTime.value = t * 0.001;\n if (mouseDampening > 0) {\n if (!lastTimeRef.current) lastTimeRef.current = t;\n const dt = (t - lastTimeRef.current) / 1000;\n lastTimeRef.current = t;\n const tau = Math.max(1e-4, mouseDampening);\n let factor = 1 - Math.exp(-dt / tau);\n if (factor > 1) factor = 1;\n const target = mouseTargetRef.current;\n const cur = uniforms.iMouse.value as number[];\n cur[0] += (target[0] - cur[0]) * factor;\n cur[1] += (target[1] - cur[1]) * factor;\n } else {\n lastTimeRef.current = t;\n }\n if (!paused && programRef.current && meshRef.current) {\n try {\n renderer.render({ scene: meshRef.current });\n } catch (e) {\n console.error(e);\n }\n }\n };\n rafRef.current = requestAnimationFrame(loop);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n if (mouseInteraction) canvas.removeEventListener('pointermove', onPointerMove);\n ro.disconnect();\n if (canvas.parentElement === container) {\n container.removeChild(canvas);\n }\n const callIfFn = (obj: unknown, key: string) => {\n const fn = obj && (obj as Record)[key];\n if (typeof fn === 'function') {\n (fn as () => void).call(obj);\n }\n };\n callIfFn(programRef.current, 'remove');\n callIfFn(geometryRef.current, 'remove');\n callIfFn(meshRef.current, 'remove');\n callIfFn(rendererRef.current, 'destroy');\n programRef.current = null;\n geometryRef.current = null;\n meshRef.current = null;\n rendererRef.current = null;\n };\n }, [\n dpr,\n paused,\n colors,\n speed,\n scale,\n turbulence,\n fluidity,\n rimWidth,\n sharpness,\n shimmer,\n glow,\n flowDirection,\n opacity,\n mouseInteraction,\n mouseStrength,\n mouseRadius,\n mouseDampening\n ]);\n\n return (\n \n );\n};\n\nexport default Ferrofluid;\n" } ], diff --git a/public/r/FloatingLines-JS-CSS.json b/public/r/FloatingLines-JS-CSS.json index be8add66..f1a8f31b 100644 --- a/public/r/FloatingLines-JS-CSS.json +++ b/public/r/FloatingLines-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "FloatingLines/FloatingLines.css", + "type": "registry:file", + "path": "FloatingLines.css", + "target": "@components/FloatingLines.css", "content": ".floating-lines-container {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "FloatingLines/FloatingLines.jsx", + "path": "FloatingLines.jsx", "content": "import { useEffect, useRef } from 'react';\nimport {\n Clock,\n Mesh,\n OrthographicCamera,\n PlaneGeometry,\n Scene,\n ShaderMaterial,\n Vector2,\n Vector3,\n WebGLRenderer\n} from 'three';\n\nimport './FloatingLines.css';\n\nconst vertexShader = `\nprecision highp float;\n\nvoid main() {\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float iTime;\nuniform vec3 iResolution;\nuniform float animationSpeed;\n\nuniform bool enableTop;\nuniform bool enableMiddle;\nuniform bool enableBottom;\n\nuniform int topLineCount;\nuniform int middleLineCount;\nuniform int bottomLineCount;\n\nuniform float topLineDistance;\nuniform float middleLineDistance;\nuniform float bottomLineDistance;\n\nuniform vec3 topWavePosition;\nuniform vec3 middleWavePosition;\nuniform vec3 bottomWavePosition;\n\nuniform vec2 iMouse;\nuniform bool interactive;\nuniform float bendRadius;\nuniform float bendStrength;\nuniform float bendInfluence;\n\nuniform bool parallax;\nuniform float parallaxStrength;\nuniform vec2 parallaxOffset;\n\nuniform vec3 lineGradient[8];\nuniform int lineGradientCount;\n\nconst vec3 BLACK = vec3(0.0);\nconst vec3 PINK = vec3(233.0, 71.0, 245.0) / 255.0;\nconst vec3 BLUE = vec3(47.0, 75.0, 162.0) / 255.0;\n\nmat2 rotate(float r) {\n return mat2(cos(r), sin(r), -sin(r), cos(r));\n}\n\nvec3 background_color(vec2 uv) {\n vec3 col = vec3(0.0);\n\n float y = sin(uv.x - 0.2) * 0.3 - 0.1;\n float m = uv.y - y;\n\n col += mix(BLUE, BLACK, smoothstep(0.0, 1.0, abs(m)));\n col += mix(PINK, BLACK, smoothstep(0.0, 1.0, abs(m - 0.8)));\n return col * 0.5;\n}\n\nvec3 getLineColor(float t, vec3 baseColor) {\n if (lineGradientCount <= 0) {\n return baseColor;\n }\n\n vec3 gradientColor;\n \n if (lineGradientCount == 1) {\n gradientColor = lineGradient[0];\n } else {\n float clampedT = clamp(t, 0.0, 0.9999);\n float scaled = clampedT * float(lineGradientCount - 1);\n int idx = int(floor(scaled));\n float f = fract(scaled);\n int idx2 = min(idx + 1, lineGradientCount - 1);\n\n vec3 c1 = lineGradient[idx];\n vec3 c2 = lineGradient[idx2];\n \n gradientColor = mix(c1, c2, f);\n }\n \n return gradientColor * 0.5;\n}\n\n float wave(vec2 uv, float offset, vec2 screenUv, vec2 mouseUv, bool shouldBend) {\n float time = iTime * animationSpeed;\n\n float x_offset = offset;\n float x_movement = time * 0.1;\n float amp = sin(offset + time * 0.2) * 0.3;\n float y = sin(uv.x + x_offset + x_movement) * amp;\n\n if (shouldBend) {\n vec2 d = screenUv - mouseUv;\n float influence = exp(-dot(d, d) * bendRadius); // radial falloff around cursor\n float bendOffset = (mouseUv.y - screenUv.y) * influence * bendStrength * bendInfluence;\n y += bendOffset;\n }\n\n float m = uv.y - y;\n return 0.0175 / max(abs(m) + 0.01, 1e-3) + 0.01;\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 baseUv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n baseUv.y *= -1.0;\n \n if (parallax) {\n baseUv += parallaxOffset;\n }\n\n vec3 col = vec3(0.0);\n\n vec3 b = lineGradientCount > 0 ? vec3(0.0) : background_color(baseUv);\n\n vec2 mouseUv = vec2(0.0);\n if (interactive) {\n mouseUv = (2.0 * iMouse - iResolution.xy) / iResolution.y;\n mouseUv.y *= -1.0;\n }\n \n if (enableBottom) {\n for (int i = 0; i < bottomLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(bottomLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = bottomWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(bottomLineDistance * fi + bottomWavePosition.x, bottomWavePosition.y),\n 1.5 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.2;\n }\n }\n\n if (enableMiddle) {\n for (int i = 0; i < middleLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(middleLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = middleWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(middleLineDistance * fi + middleWavePosition.x, middleWavePosition.y),\n 2.0 + 0.15 * fi,\n baseUv,\n mouseUv,\n interactive\n );\n }\n }\n\n if (enableTop) {\n for (int i = 0; i < topLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(topLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = topWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n ruv.x *= -1.0;\n col += lineCol * wave(\n ruv + vec2(topLineDistance * fi + topWavePosition.x, topWavePosition.y),\n 1.0 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.1;\n }\n }\n\n fragColor = vec4(col, 1.0);\n}\n\nvoid main() {\n vec4 color = vec4(0.0);\n mainImage(color, gl_FragCoord.xy);\n gl_FragColor = color;\n}\n`;\n\nconst MAX_GRADIENT_STOPS = 8;\n\nfunction hexToVec3(hex) {\n let value = hex.trim();\n\n if (value.startsWith('#')) {\n value = value.slice(1);\n }\n\n let r = 255;\n let g = 255;\n let b = 255;\n\n if (value.length === 3) {\n r = parseInt(value[0] + value[0], 16);\n g = parseInt(value[1] + value[1], 16);\n b = parseInt(value[2] + value[2], 16);\n } else if (value.length === 6) {\n r = parseInt(value.slice(0, 2), 16);\n g = parseInt(value.slice(2, 4), 16);\n b = parseInt(value.slice(4, 6), 16);\n }\n\n return new Vector3(r / 255, g / 255, b / 255);\n}\n\nexport default function FloatingLines({\n linesGradient,\n enabledWaves = ['top', 'middle', 'bottom'],\n lineCount = [6],\n lineDistance = [5],\n topWavePosition,\n middleWavePosition,\n bottomWavePosition = { x: 2.0, y: -0.7, rotate: -1 },\n animationSpeed = 1,\n interactive = true,\n bendRadius = 5.0,\n bendStrength = -0.5,\n mouseDamping = 0.05,\n parallax = true,\n parallaxStrength = 0.2,\n mixBlendMode = 'screen'\n}) {\n const containerRef = useRef(null);\n const targetMouseRef = useRef(new Vector2(-1000, -1000));\n const currentMouseRef = useRef(new Vector2(-1000, -1000));\n const targetInfluenceRef = useRef(0);\n const currentInfluenceRef = useRef(0);\n const targetParallaxRef = useRef(new Vector2(0, 0));\n const currentParallaxRef = useRef(new Vector2(0, 0));\n\n const getLineCount = waveType => {\n if (typeof lineCount === 'number') return lineCount;\n if (!enabledWaves.includes(waveType)) return 0;\n const index = enabledWaves.indexOf(waveType);\n return lineCount[index] ?? 6;\n };\n\n const getLineDistance = waveType => {\n if (typeof lineDistance === 'number') return lineDistance;\n if (!enabledWaves.includes(waveType)) return 0.1;\n const index = enabledWaves.indexOf(waveType);\n return lineDistance[index] ?? 0.1;\n };\n\n const topLineCount = enabledWaves.includes('top') ? getLineCount('top') : 0;\n const middleLineCount = enabledWaves.includes('middle') ? getLineCount('middle') : 0;\n const bottomLineCount = enabledWaves.includes('bottom') ? getLineCount('bottom') : 0;\n\n const topLineDistance = enabledWaves.includes('top') ? getLineDistance('top') * 0.01 : 0.01;\n const middleLineDistance = enabledWaves.includes('middle') ? getLineDistance('middle') * 0.01 : 0.01;\n const bottomLineDistance = enabledWaves.includes('bottom') ? getLineDistance('bottom') * 0.01 : 0.01;\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n let active = true;\n\n const scene = new Scene();\n\n const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);\n camera.position.z = 1;\n\n const renderer = new WebGLRenderer({ antialias: true, alpha: false });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n container.appendChild(renderer.domElement);\n\n const uniforms = {\n iTime: { value: 0 },\n iResolution: { value: new Vector3(1, 1, 1) },\n animationSpeed: { value: animationSpeed },\n\n enableTop: { value: enabledWaves.includes('top') },\n enableMiddle: { value: enabledWaves.includes('middle') },\n enableBottom: { value: enabledWaves.includes('bottom') },\n\n topLineCount: { value: topLineCount },\n middleLineCount: { value: middleLineCount },\n bottomLineCount: { value: bottomLineCount },\n\n topLineDistance: { value: topLineDistance },\n middleLineDistance: { value: middleLineDistance },\n bottomLineDistance: { value: bottomLineDistance },\n\n topWavePosition: {\n value: new Vector3(topWavePosition?.x ?? 10.0, topWavePosition?.y ?? 0.5, topWavePosition?.rotate ?? -0.4)\n },\n middleWavePosition: {\n value: new Vector3(\n middleWavePosition?.x ?? 5.0,\n middleWavePosition?.y ?? 0.0,\n middleWavePosition?.rotate ?? 0.2\n )\n },\n bottomWavePosition: {\n value: new Vector3(\n bottomWavePosition?.x ?? 2.0,\n bottomWavePosition?.y ?? -0.7,\n bottomWavePosition?.rotate ?? 0.4\n )\n },\n\n iMouse: { value: new Vector2(-1000, -1000) },\n interactive: { value: interactive },\n bendRadius: { value: bendRadius },\n bendStrength: { value: bendStrength },\n bendInfluence: { value: 0 },\n\n parallax: { value: parallax },\n parallaxStrength: { value: parallaxStrength },\n parallaxOffset: { value: new Vector2(0, 0) },\n\n lineGradient: {\n value: Array.from({ length: MAX_GRADIENT_STOPS }, () => new Vector3(1, 1, 1))\n },\n lineGradientCount: { value: 0 }\n };\n\n if (linesGradient && linesGradient.length > 0) {\n const stops = linesGradient.slice(0, MAX_GRADIENT_STOPS);\n uniforms.lineGradientCount.value = stops.length;\n\n stops.forEach((hex, i) => {\n const color = hexToVec3(hex);\n uniforms.lineGradient.value[i].set(color.x, color.y, color.z);\n });\n }\n\n const material = new ShaderMaterial({\n uniforms,\n vertexShader,\n fragmentShader\n });\n\n const geometry = new PlaneGeometry(2, 2);\n const mesh = new Mesh(geometry, material);\n scene.add(mesh);\n\n const clock = new Clock();\n\n const setSize = () => {\n if (!active) return;\n const width = container.clientWidth || 1;\n const height = container.clientHeight || 1;\n\n renderer.setSize(width, height, false);\n\n const canvasWidth = renderer.domElement.width;\n const canvasHeight = renderer.domElement.height;\n uniforms.iResolution.value.set(canvasWidth, canvasHeight, 1);\n };\n\n setSize();\n\n const ro =\n typeof ResizeObserver !== 'undefined'\n ? new ResizeObserver(() => {\n if (!active) return;\n setSize();\n })\n : null;\n\n if (ro) ro.observe(container);\n\n const handlePointerMove = event => {\n const rect = renderer.domElement.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const y = event.clientY - rect.top;\n const dpr = renderer.getPixelRatio();\n\n targetMouseRef.current.set(x * dpr, (rect.height - y) * dpr);\n targetInfluenceRef.current = 1.0;\n\n if (parallax) {\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const offsetX = (x - centerX) / rect.width;\n const offsetY = -(y - centerY) / rect.height;\n targetParallaxRef.current.set(offsetX * parallaxStrength, offsetY * parallaxStrength);\n }\n };\n\n const handlePointerLeave = () => {\n targetInfluenceRef.current = 0.0;\n };\n\n if (interactive) {\n renderer.domElement.addEventListener('pointermove', handlePointerMove);\n renderer.domElement.addEventListener('pointerleave', handlePointerLeave);\n }\n\n let raf = 0;\n const renderLoop = () => {\n if (!active) return;\n\n uniforms.iTime.value = clock.getElapsedTime();\n\n if (interactive) {\n currentMouseRef.current.lerp(targetMouseRef.current, mouseDamping);\n uniforms.iMouse.value.copy(currentMouseRef.current);\n\n currentInfluenceRef.current += (targetInfluenceRef.current - currentInfluenceRef.current) * mouseDamping;\n uniforms.bendInfluence.value = currentInfluenceRef.current;\n }\n\n if (parallax) {\n currentParallaxRef.current.lerp(targetParallaxRef.current, mouseDamping);\n uniforms.parallaxOffset.value.copy(currentParallaxRef.current);\n }\n\n renderer.render(scene, camera);\n raf = requestAnimationFrame(renderLoop);\n };\n renderLoop();\n\n return () => {\n active = false;\n\n cancelAnimationFrame(raf);\n\n if (ro) ro.disconnect();\n\n if (interactive) {\n renderer.domElement.removeEventListener('pointermove', handlePointerMove);\n renderer.domElement.removeEventListener('pointerleave', handlePointerLeave);\n }\n\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n renderer.forceContextLoss();\n if (renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n linesGradient,\n enabledWaves,\n lineCount,\n lineDistance,\n topWavePosition,\n middleWavePosition,\n bottomWavePosition,\n animationSpeed,\n interactive,\n bendRadius,\n bendStrength,\n mouseDamping,\n parallax,\n parallaxStrength\n ]);\n\n return (\n \n );\n}\n" } ], diff --git a/public/r/FloatingLines-TS-CSS.json b/public/r/FloatingLines-TS-CSS.json index 17baece3..dc80605d 100644 --- a/public/r/FloatingLines-TS-CSS.json +++ b/public/r/FloatingLines-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "FloatingLines/FloatingLines.css", + "type": "registry:file", + "path": "FloatingLines.css", + "target": "@components/FloatingLines.css", "content": ".floating-lines-container {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "FloatingLines/FloatingLines.tsx", + "path": "FloatingLines.tsx", "content": "import { useEffect, useRef } from 'react';\nimport {\n Clock,\n Mesh,\n OrthographicCamera,\n PlaneGeometry,\n Scene,\n ShaderMaterial,\n Vector2,\n Vector3,\n WebGLRenderer\n} from 'three';\n\nimport './FloatingLines.css';\n\nconst vertexShader = `\nprecision highp float;\n\nvoid main() {\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float iTime;\nuniform vec3 iResolution;\nuniform float animationSpeed;\n\nuniform bool enableTop;\nuniform bool enableMiddle;\nuniform bool enableBottom;\n\nuniform int topLineCount;\nuniform int middleLineCount;\nuniform int bottomLineCount;\n\nuniform float topLineDistance;\nuniform float middleLineDistance;\nuniform float bottomLineDistance;\n\nuniform vec3 topWavePosition;\nuniform vec3 middleWavePosition;\nuniform vec3 bottomWavePosition;\n\nuniform vec2 iMouse;\nuniform bool interactive;\nuniform float bendRadius;\nuniform float bendStrength;\nuniform float bendInfluence;\n\nuniform bool parallax;\nuniform float parallaxStrength;\nuniform vec2 parallaxOffset;\n\nuniform vec3 lineGradient[8];\nuniform int lineGradientCount;\n\nconst vec3 BLACK = vec3(0.0);\nconst vec3 PINK = vec3(233.0, 71.0, 245.0) / 255.0;\nconst vec3 BLUE = vec3(47.0, 75.0, 162.0) / 255.0;\n\nmat2 rotate(float r) {\n return mat2(cos(r), sin(r), -sin(r), cos(r));\n}\n\nvec3 background_color(vec2 uv) {\n vec3 col = vec3(0.0);\n\n float y = sin(uv.x - 0.2) * 0.3 - 0.1;\n float m = uv.y - y;\n\n col += mix(BLUE, BLACK, smoothstep(0.0, 1.0, abs(m)));\n col += mix(PINK, BLACK, smoothstep(0.0, 1.0, abs(m - 0.8)));\n return col * 0.5;\n}\n\nvec3 getLineColor(float t, vec3 baseColor) {\n if (lineGradientCount <= 0) {\n return baseColor;\n }\n\n vec3 gradientColor;\n \n if (lineGradientCount == 1) {\n gradientColor = lineGradient[0];\n } else {\n float clampedT = clamp(t, 0.0, 0.9999);\n float scaled = clampedT * float(lineGradientCount - 1);\n int idx = int(floor(scaled));\n float f = fract(scaled);\n int idx2 = min(idx + 1, lineGradientCount - 1);\n\n vec3 c1 = lineGradient[idx];\n vec3 c2 = lineGradient[idx2];\n \n gradientColor = mix(c1, c2, f);\n }\n \n return gradientColor * 0.5;\n}\n\n float wave(vec2 uv, float offset, vec2 screenUv, vec2 mouseUv, bool shouldBend) {\n float time = iTime * animationSpeed;\n\n float x_offset = offset;\n float x_movement = time * 0.1;\n float amp = sin(offset + time * 0.2) * 0.3;\n float y = sin(uv.x + x_offset + x_movement) * amp;\n\n if (shouldBend) {\n vec2 d = screenUv - mouseUv;\n float influence = exp(-dot(d, d) * bendRadius); // radial falloff around cursor\n float bendOffset = (mouseUv.y - screenUv.y) * influence * bendStrength * bendInfluence;\n y += bendOffset;\n }\n\n float m = uv.y - y;\n return 0.0175 / max(abs(m) + 0.01, 1e-3) + 0.01;\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 baseUv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n baseUv.y *= -1.0;\n \n if (parallax) {\n baseUv += parallaxOffset;\n }\n\n vec3 col = vec3(0.0);\n\n vec3 b = lineGradientCount > 0 ? vec3(0.0) : background_color(baseUv);\n\n vec2 mouseUv = vec2(0.0);\n if (interactive) {\n mouseUv = (2.0 * iMouse - iResolution.xy) / iResolution.y;\n mouseUv.y *= -1.0;\n }\n \n if (enableBottom) {\n for (int i = 0; i < bottomLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(bottomLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = bottomWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(bottomLineDistance * fi + bottomWavePosition.x, bottomWavePosition.y),\n 1.5 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.2;\n }\n }\n\n if (enableMiddle) {\n for (int i = 0; i < middleLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(middleLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = middleWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(middleLineDistance * fi + middleWavePosition.x, middleWavePosition.y),\n 2.0 + 0.15 * fi,\n baseUv,\n mouseUv,\n interactive\n );\n }\n }\n\n if (enableTop) {\n for (int i = 0; i < topLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(topLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = topWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n ruv.x *= -1.0;\n col += lineCol * wave(\n ruv + vec2(topLineDistance * fi + topWavePosition.x, topWavePosition.y),\n 1.0 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.1;\n }\n }\n\n fragColor = vec4(col, 1.0);\n}\n\nvoid main() {\n vec4 color = vec4(0.0);\n mainImage(color, gl_FragCoord.xy);\n gl_FragColor = color;\n}\n`;\n\nconst MAX_GRADIENT_STOPS = 8;\n\ntype WavePosition = {\n x: number;\n y: number;\n rotate: number;\n};\n\ntype FloatingLinesProps = {\n linesGradient?: string[];\n enabledWaves?: Array<'top' | 'middle' | 'bottom'>;\n lineCount?: number | number[];\n lineDistance?: number | number[];\n topWavePosition?: WavePosition;\n middleWavePosition?: WavePosition;\n bottomWavePosition?: WavePosition;\n animationSpeed?: number;\n interactive?: boolean;\n bendRadius?: number;\n bendStrength?: number;\n mouseDamping?: number;\n parallax?: boolean;\n parallaxStrength?: number;\n mixBlendMode?: React.CSSProperties['mixBlendMode'];\n};\n\nfunction hexToVec3(hex: string): Vector3 {\n let value = hex.trim();\n\n if (value.startsWith('#')) {\n value = value.slice(1);\n }\n\n let r = 255;\n let g = 255;\n let b = 255;\n\n if (value.length === 3) {\n r = parseInt(value[0] + value[0], 16);\n g = parseInt(value[1] + value[1], 16);\n b = parseInt(value[2] + value[2], 16);\n } else if (value.length === 6) {\n r = parseInt(value.slice(0, 2), 16);\n g = parseInt(value.slice(2, 4), 16);\n b = parseInt(value.slice(4, 6), 16);\n }\n\n return new Vector3(r / 255, g / 255, b / 255);\n}\n\nexport default function FloatingLines({\n linesGradient,\n enabledWaves = ['top', 'middle', 'bottom'],\n lineCount = [6],\n lineDistance = [5],\n topWavePosition,\n middleWavePosition,\n bottomWavePosition = { x: 2.0, y: -0.7, rotate: -1 },\n animationSpeed = 1,\n interactive = true,\n bendRadius = 5.0,\n bendStrength = -0.5,\n mouseDamping = 0.05,\n parallax = true,\n parallaxStrength = 0.2,\n mixBlendMode = 'screen'\n}: FloatingLinesProps) {\n const containerRef = useRef(null);\n const targetMouseRef = useRef(new Vector2(-1000, -1000));\n const currentMouseRef = useRef(new Vector2(-1000, -1000));\n const targetInfluenceRef = useRef(0);\n const currentInfluenceRef = useRef(0);\n const targetParallaxRef = useRef(new Vector2(0, 0));\n const currentParallaxRef = useRef(new Vector2(0, 0));\n\n const getLineCount = (waveType: 'top' | 'middle' | 'bottom'): number => {\n if (typeof lineCount === 'number') return lineCount;\n if (!enabledWaves.includes(waveType)) return 0;\n const index = enabledWaves.indexOf(waveType);\n return lineCount[index] ?? 6;\n };\n\n const getLineDistance = (waveType: 'top' | 'middle' | 'bottom'): number => {\n if (typeof lineDistance === 'number') return lineDistance;\n if (!enabledWaves.includes(waveType)) return 0.1;\n const index = enabledWaves.indexOf(waveType);\n return lineDistance[index] ?? 0.1;\n };\n\n const topLineCount = enabledWaves.includes('top') ? getLineCount('top') : 0;\n const middleLineCount = enabledWaves.includes('middle') ? getLineCount('middle') : 0;\n const bottomLineCount = enabledWaves.includes('bottom') ? getLineCount('bottom') : 0;\n\n const topLineDistance = enabledWaves.includes('top') ? getLineDistance('top') * 0.01 : 0.01;\n const middleLineDistance = enabledWaves.includes('middle') ? getLineDistance('middle') * 0.01 : 0.01;\n const bottomLineDistance = enabledWaves.includes('bottom') ? getLineDistance('bottom') * 0.01 : 0.01;\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n let active = true;\n\n const scene = new Scene();\n\n const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);\n camera.position.z = 1;\n\n const renderer = new WebGLRenderer({ antialias: true, alpha: false });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n container.appendChild(renderer.domElement);\n\n const uniforms = {\n iTime: { value: 0 },\n iResolution: { value: new Vector3(1, 1, 1) },\n animationSpeed: { value: animationSpeed },\n\n enableTop: { value: enabledWaves.includes('top') },\n enableMiddle: { value: enabledWaves.includes('middle') },\n enableBottom: { value: enabledWaves.includes('bottom') },\n\n topLineCount: { value: topLineCount },\n middleLineCount: { value: middleLineCount },\n bottomLineCount: { value: bottomLineCount },\n\n topLineDistance: { value: topLineDistance },\n middleLineDistance: { value: middleLineDistance },\n bottomLineDistance: { value: bottomLineDistance },\n\n topWavePosition: {\n value: new Vector3(topWavePosition?.x ?? 10.0, topWavePosition?.y ?? 0.5, topWavePosition?.rotate ?? -0.4)\n },\n middleWavePosition: {\n value: new Vector3(\n middleWavePosition?.x ?? 5.0,\n middleWavePosition?.y ?? 0.0,\n middleWavePosition?.rotate ?? 0.2\n )\n },\n bottomWavePosition: {\n value: new Vector3(\n bottomWavePosition?.x ?? 2.0,\n bottomWavePosition?.y ?? -0.7,\n bottomWavePosition?.rotate ?? 0.4\n )\n },\n\n iMouse: { value: new Vector2(-1000, -1000) },\n interactive: { value: interactive },\n bendRadius: { value: bendRadius },\n bendStrength: { value: bendStrength },\n bendInfluence: { value: 0 },\n\n parallax: { value: parallax },\n parallaxStrength: { value: parallaxStrength },\n parallaxOffset: { value: new Vector2(0, 0) },\n\n lineGradient: {\n value: Array.from({ length: MAX_GRADIENT_STOPS }, () => new Vector3(1, 1, 1))\n },\n lineGradientCount: { value: 0 }\n };\n\n if (linesGradient && linesGradient.length > 0) {\n const stops = linesGradient.slice(0, MAX_GRADIENT_STOPS);\n uniforms.lineGradientCount.value = stops.length;\n\n stops.forEach((hex, i) => {\n const color = hexToVec3(hex);\n uniforms.lineGradient.value[i].set(color.x, color.y, color.z);\n });\n }\n\n const material = new ShaderMaterial({\n uniforms,\n vertexShader,\n fragmentShader\n });\n\n const geometry = new PlaneGeometry(2, 2);\n const mesh = new Mesh(geometry, material);\n scene.add(mesh);\n\n const clock = new Clock();\n\n const setSize = () => {\n const el = containerRef.current!;\n const width = el.clientWidth || 1;\n const height = el.clientHeight || 1;\n\n renderer.setSize(width, height, false);\n\n const canvasWidth = renderer.domElement.width;\n const canvasHeight = renderer.domElement.height;\n uniforms.iResolution.value.set(canvasWidth, canvasHeight, 1);\n };\n\n setSize();\n\n const ro =\n typeof ResizeObserver !== 'undefined'\n ? new ResizeObserver(() => {\n if (!active) return;\n setSize();\n })\n : null;\n\n if (ro) ro.observe(container);\n\n const handlePointerMove = (event: PointerEvent) => {\n const rect = renderer.domElement.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const y = event.clientY - rect.top;\n const dpr = renderer.getPixelRatio();\n\n targetMouseRef.current.set(x * dpr, (rect.height - y) * dpr);\n targetInfluenceRef.current = 1.0;\n\n if (parallax) {\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const offsetX = (x - centerX) / rect.width;\n const offsetY = -(y - centerY) / rect.height;\n targetParallaxRef.current.set(offsetX * parallaxStrength, offsetY * parallaxStrength);\n }\n };\n\n const handlePointerLeave = () => {\n targetInfluenceRef.current = 0.0;\n };\n\n if (interactive) {\n renderer.domElement.addEventListener('pointermove', handlePointerMove);\n renderer.domElement.addEventListener('pointerleave', handlePointerLeave);\n }\n\n let raf = 0;\n const renderLoop = () => {\n if (!active) return;\n\n uniforms.iTime.value = clock.getElapsedTime();\n\n if (interactive) {\n currentMouseRef.current.lerp(targetMouseRef.current, mouseDamping);\n uniforms.iMouse.value.copy(currentMouseRef.current);\n\n currentInfluenceRef.current += (targetInfluenceRef.current - currentInfluenceRef.current) * mouseDamping;\n uniforms.bendInfluence.value = currentInfluenceRef.current;\n }\n\n if (parallax) {\n currentParallaxRef.current.lerp(targetParallaxRef.current, mouseDamping);\n uniforms.parallaxOffset.value.copy(currentParallaxRef.current);\n }\n\n renderer.render(scene, camera);\n raf = requestAnimationFrame(renderLoop);\n };\n renderLoop();\n\n return () => {\n active = false;\n\n cancelAnimationFrame(raf);\n\n if (ro) ro.disconnect();\n\n if (interactive) {\n renderer.domElement.removeEventListener('pointermove', handlePointerMove);\n renderer.domElement.removeEventListener('pointerleave', handlePointerLeave);\n }\n\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n renderer.forceContextLoss();\n if (renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n };\n }, [\n linesGradient,\n enabledWaves,\n lineCount,\n lineDistance,\n topWavePosition,\n middleWavePosition,\n bottomWavePosition,\n animationSpeed,\n interactive,\n bendRadius,\n bendStrength,\n mouseDamping,\n parallax,\n parallaxStrength\n ]);\n\n return (\n \n );\n}\n" } ], diff --git a/public/r/FlowingMenu-JS-CSS.json b/public/r/FlowingMenu-JS-CSS.json index 9c88e606..c35601b8 100644 --- a/public/r/FlowingMenu-JS-CSS.json +++ b/public/r/FlowingMenu-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "FlowingMenu/FlowingMenu.css", + "type": "registry:file", + "path": "FlowingMenu.css", + "target": "@components/FlowingMenu.css", "content": ".menu-wrap {\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n\n.menu {\n display: flex;\n flex-direction: column;\n height: 100%;\n margin: 0;\n padding: 0;\n}\n\n.menu__item {\n flex: 1;\n position: relative;\n overflow: hidden;\n text-align: center;\n border-top: 1px solid;\n}\n\n.menu__item:first-child {\n border-top: none;\n}\n\n.menu__item-link {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n position: relative;\n cursor: pointer;\n text-transform: uppercase;\n text-decoration: none;\n white-space: nowrap;\n font-weight: 600;\n font-size: 4vh;\n}\n\n.menu__item-link:hover {\n color: inherit;\n}\n\n.menu__item-link:focus:not(:focus-visible) {\n color: inherit;\n}\n\n.marquee {\n position: absolute;\n top: 0;\n left: 0;\n overflow: hidden;\n width: 100%;\n height: 100%;\n pointer-events: none;\n transform: translate3d(0, 101%, 0);\n}\n\n.marquee__inner-wrap {\n height: 100%;\n width: 100%;\n overflow: hidden;\n}\n\n.marquee__inner {\n display: flex;\n align-items: center;\n position: relative;\n height: 100%;\n width: fit-content;\n will-change: transform;\n}\n\n.marquee__part {\n display: flex;\n align-items: center;\n flex-shrink: 0;\n}\n\n.marquee span {\n white-space: nowrap;\n text-transform: uppercase;\n font-weight: 400;\n font-size: 4vh;\n line-height: 1;\n padding: 0 1vw;\n}\n\n.marquee__img {\n width: 200px;\n height: 7vh;\n margin: 2em 2vw;\n padding: 1em 0;\n border-radius: 50px;\n background-size: cover;\n background-position: 50% 50%;\n}\n" }, { "type": "registry:component", - "path": "FlowingMenu/FlowingMenu.jsx", + "path": "FlowingMenu.jsx", "content": "import { useRef, useEffect, useState } from 'react';\nimport { gsap } from 'gsap';\n\nimport './FlowingMenu.css';\n\nfunction FlowingMenu({\n items = [],\n speed = 15,\n textColor = '#fff',\n bgColor = '#120F17',\n marqueeBgColor = '#fff',\n marqueeTextColor = '#120F17',\n borderColor = '#fff'\n}) {\n return (\n
\n \n
\n );\n}\n\nfunction MenuItem({ link, text, image, speed, textColor, marqueeBgColor, marqueeTextColor, borderColor }) {\n const itemRef = useRef(null);\n const marqueeRef = useRef(null);\n const marqueeInnerRef = useRef(null);\n const animationRef = useRef(null);\n const [repetitions, setRepetitions] = useState(4);\n\n const animationDefaults = { duration: 0.6, ease: 'expo' };\n\n const findClosestEdge = (mouseX, mouseY, width, height) => {\n const topEdgeDist = distMetric(mouseX, mouseY, width / 2, 0);\n const bottomEdgeDist = distMetric(mouseX, mouseY, width / 2, height);\n return topEdgeDist < bottomEdgeDist ? 'top' : 'bottom';\n };\n\n const distMetric = (x, y, x2, y2) => {\n const xDiff = x - x2;\n const yDiff = y - y2;\n return xDiff * xDiff + yDiff * yDiff;\n };\n\n useEffect(() => {\n const calculateRepetitions = () => {\n if (!marqueeInnerRef.current) return;\n\n // Get the first marquee part to measure content width\n const marqueeContent = marqueeInnerRef.current.querySelector('.marquee__part');\n if (!marqueeContent) return;\n\n const contentWidth = marqueeContent.offsetWidth;\n const viewportWidth = window.innerWidth;\n\n // Calculate how many copies we need to fill viewport + extra for seamless loop\n // We need at least 2, but calculate based on content vs viewport\n const needed = Math.ceil(viewportWidth / contentWidth) + 2;\n setRepetitions(Math.max(4, needed));\n };\n\n calculateRepetitions();\n window.addEventListener('resize', calculateRepetitions);\n return () => window.removeEventListener('resize', calculateRepetitions);\n }, [text, image]);\n\n useEffect(() => {\n const setupMarquee = () => {\n if (!marqueeInnerRef.current) return;\n\n const marqueeContent = marqueeInnerRef.current.querySelector('.marquee__part');\n if (!marqueeContent) return;\n\n const contentWidth = marqueeContent.offsetWidth;\n if (contentWidth === 0) return;\n\n if (animationRef.current) {\n animationRef.current.kill();\n }\n\n // Animate exactly one content width for seamless loop\n animationRef.current = gsap.to(marqueeInnerRef.current, {\n x: -contentWidth,\n duration: speed,\n ease: 'none',\n repeat: -1\n });\n };\n\n // Small delay to ensure DOM is ready after repetitions update\n const timer = setTimeout(setupMarquee, 50);\n\n return () => {\n clearTimeout(timer);\n if (animationRef.current) {\n animationRef.current.kill();\n }\n };\n }, [text, image, repetitions, speed]);\n\n const handleMouseEnter = ev => {\n if (!itemRef.current || !marqueeRef.current || !marqueeInnerRef.current) return;\n const rect = itemRef.current.getBoundingClientRect();\n const x = ev.clientX - rect.left;\n const y = ev.clientY - rect.top;\n const edge = findClosestEdge(x, y, rect.width, rect.height);\n\n gsap\n .timeline({ defaults: animationDefaults })\n .set(marqueeRef.current, { y: edge === 'top' ? '-101%' : '101%' }, 0)\n .set(marqueeInnerRef.current, { y: edge === 'top' ? '101%' : '-101%' }, 0)\n .to([marqueeRef.current, marqueeInnerRef.current], { y: '0%' }, 0);\n };\n\n const handleMouseLeave = ev => {\n if (!itemRef.current || !marqueeRef.current || !marqueeInnerRef.current) return;\n const rect = itemRef.current.getBoundingClientRect();\n const x = ev.clientX - rect.left;\n const y = ev.clientY - rect.top;\n const edge = findClosestEdge(x, y, rect.width, rect.height);\n\n gsap\n .timeline({ defaults: animationDefaults })\n .to(marqueeRef.current, { y: edge === 'top' ? '-101%' : '101%' }, 0)\n .to(marqueeInnerRef.current, { y: edge === 'top' ? '101%' : '-101%' }, 0);\n };\n\n return (\n
\n \n {text}\n \n
\n
\n
\n {[...Array(repetitions)].map((_, idx) => (\n
\n {text}\n
\n
\n ))}\n
\n
\n
\n
\n );\n}\n\nexport default FlowingMenu;\n" } ], diff --git a/public/r/FlowingMenu-TS-CSS.json b/public/r/FlowingMenu-TS-CSS.json index 1855efa7..a246afcc 100644 --- a/public/r/FlowingMenu-TS-CSS.json +++ b/public/r/FlowingMenu-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "FlowingMenu/FlowingMenu.css", + "type": "registry:file", + "path": "FlowingMenu.css", + "target": "@components/FlowingMenu.css", "content": ".menu-wrap {\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n\n.menu {\n display: flex;\n flex-direction: column;\n height: 100%;\n margin: 0;\n padding: 0;\n}\n\n.menu__item {\n flex: 1;\n position: relative;\n overflow: hidden;\n text-align: center;\n border-top: 1px solid;\n}\n\n.menu__item:first-child {\n border-top: none;\n}\n\n.menu__item-link {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n position: relative;\n cursor: pointer;\n text-transform: uppercase;\n text-decoration: none;\n white-space: nowrap;\n font-weight: 600;\n font-size: 4vh;\n}\n\n.menu__item-link:hover {\n color: inherit;\n}\n\n.menu__item-link:focus:not(:focus-visible) {\n color: inherit;\n}\n\n.marquee {\n position: absolute;\n top: 0;\n left: 0;\n overflow: hidden;\n width: 100%;\n height: 100%;\n pointer-events: none;\n transform: translate3d(0, 101%, 0);\n}\n\n.marquee__inner-wrap {\n height: 100%;\n width: 100%;\n overflow: hidden;\n}\n\n.marquee__inner {\n display: flex;\n align-items: center;\n position: relative;\n height: 100%;\n width: fit-content;\n will-change: transform;\n}\n\n.marquee__part {\n display: flex;\n align-items: center;\n flex-shrink: 0;\n}\n\n.marquee span {\n white-space: nowrap;\n text-transform: uppercase;\n font-weight: 400;\n font-size: 4vh;\n line-height: 1;\n padding: 0 1vw;\n}\n\n.marquee__img {\n width: 200px;\n height: 7vh;\n margin: 2em 2vw;\n padding: 1em 0;\n border-radius: 50px;\n background-size: cover;\n background-position: 50% 50%;\n}\n" }, { "type": "registry:component", - "path": "FlowingMenu/FlowingMenu.tsx", + "path": "FlowingMenu.tsx", "content": "import React, { useRef, useEffect, useState } from 'react';\nimport { gsap } from 'gsap';\n\nimport './FlowingMenu.css';\n\ninterface MenuItemData {\n link: string;\n text: string;\n image: string;\n}\n\ninterface FlowingMenuProps {\n items?: MenuItemData[];\n speed?: number;\n textColor?: string;\n bgColor?: string;\n marqueeBgColor?: string;\n marqueeTextColor?: string;\n borderColor?: string;\n}\n\ninterface MenuItemProps extends MenuItemData {\n speed: number;\n textColor: string;\n marqueeBgColor: string;\n marqueeTextColor: string;\n borderColor: string;\n isFirst: boolean;\n}\n\nconst FlowingMenu: React.FC = ({\n items = [],\n speed = 15,\n textColor = '#fff',\n bgColor = '#120F17',\n marqueeBgColor = '#fff',\n marqueeTextColor = '#120F17',\n borderColor = '#fff'\n}) => {\n return (\n
\n \n
\n );\n};\n\nconst MenuItem: React.FC = ({\n link,\n text,\n image,\n speed,\n textColor,\n marqueeBgColor,\n marqueeTextColor,\n borderColor,\n isFirst\n}) => {\n const itemRef = useRef(null);\n const marqueeRef = useRef(null);\n const marqueeInnerRef = useRef(null);\n const animationRef = useRef(null);\n const [repetitions, setRepetitions] = useState(4);\n\n const animationDefaults: gsap.TweenVars = { duration: 0.6, ease: 'expo' };\n\n const distMetric = (x: number, y: number, x2: number, y2: number): number => {\n const xDiff = x - x2;\n const yDiff = y - y2;\n return xDiff * xDiff + yDiff * yDiff;\n };\n\n const findClosestEdge = (mouseX: number, mouseY: number, width: number, height: number): 'top' | 'bottom' => {\n const topEdgeDist = distMetric(mouseX, mouseY, width / 2, 0);\n const bottomEdgeDist = distMetric(mouseX, mouseY, width / 2, height);\n return topEdgeDist < bottomEdgeDist ? 'top' : 'bottom';\n };\n\n useEffect(() => {\n const calculateRepetitions = () => {\n if (!marqueeInnerRef.current) return;\n const marqueeContent = marqueeInnerRef.current.querySelector('.marquee__part') as HTMLElement;\n if (!marqueeContent) return;\n const contentWidth = marqueeContent.offsetWidth;\n const viewportWidth = window.innerWidth;\n const needed = Math.ceil(viewportWidth / contentWidth) + 2;\n setRepetitions(Math.max(4, needed));\n };\n\n calculateRepetitions();\n window.addEventListener('resize', calculateRepetitions);\n return () => window.removeEventListener('resize', calculateRepetitions);\n }, [text, image]);\n\n useEffect(() => {\n const setupMarquee = () => {\n if (!marqueeInnerRef.current) return;\n const marqueeContent = marqueeInnerRef.current.querySelector('.marquee__part') as HTMLElement;\n if (!marqueeContent) return;\n const contentWidth = marqueeContent.offsetWidth;\n if (contentWidth === 0) return;\n\n if (animationRef.current) {\n animationRef.current.kill();\n }\n\n animationRef.current = gsap.to(marqueeInnerRef.current, {\n x: -contentWidth,\n duration: speed,\n ease: 'none',\n repeat: -1\n });\n };\n\n const timer = setTimeout(setupMarquee, 50);\n return () => {\n clearTimeout(timer);\n if (animationRef.current) {\n animationRef.current.kill();\n }\n };\n }, [text, image, repetitions, speed]);\n\n const handleMouseEnter = (ev: React.MouseEvent) => {\n if (!itemRef.current || !marqueeRef.current || !marqueeInnerRef.current) return;\n const rect = itemRef.current.getBoundingClientRect();\n const x = ev.clientX - rect.left;\n const y = ev.clientY - rect.top;\n const edge = findClosestEdge(x, y, rect.width, rect.height);\n\n gsap\n .timeline({ defaults: animationDefaults })\n .set(marqueeRef.current, { y: edge === 'top' ? '-101%' : '101%' }, 0)\n .set(marqueeInnerRef.current, { y: edge === 'top' ? '101%' : '-101%' }, 0)\n .to([marqueeRef.current, marqueeInnerRef.current], { y: '0%' }, 0);\n };\n\n const handleMouseLeave = (ev: React.MouseEvent) => {\n if (!itemRef.current || !marqueeRef.current || !marqueeInnerRef.current) return;\n const rect = itemRef.current.getBoundingClientRect();\n const x = ev.clientX - rect.left;\n const y = ev.clientY - rect.top;\n const edge = findClosestEdge(x, y, rect.width, rect.height);\n\n gsap\n .timeline({ defaults: animationDefaults })\n .to(marqueeRef.current, { y: edge === 'top' ? '-101%' : '101%' }, 0)\n .to(marqueeInnerRef.current, { y: edge === 'top' ? '101%' : '-101%' }, 0);\n };\n\n return (\n
\n \n {text}\n \n
\n
\n
\n {[...Array(repetitions)].map((_, idx) => (\n
\n {text}\n
\n
\n ))}\n
\n
\n
\n
\n );\n};\n\nexport default FlowingMenu;\n" } ], diff --git a/public/r/FluidGlass-TS-CSS.json b/public/r/FluidGlass-TS-CSS.json index 489e9181..0fda9498 100644 --- a/public/r/FluidGlass-TS-CSS.json +++ b/public/r/FluidGlass-TS-CSS.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "FluidGlass/FluidGlass.tsx", - "content": "/* eslint-disable react/no-unknown-property */\nimport * as THREE from 'three';\nimport { useRef, useState, useEffect, memo, ReactNode } from 'react';\nimport { Canvas, createPortal, useFrame, useThree, ThreeElements } from '@react-three/fiber';\nimport {\n useFBO,\n useGLTF,\n useScroll,\n Image,\n Scroll,\n Preload,\n ScrollControls,\n MeshTransmissionMaterial,\n Text\n} from '@react-three/drei';\nimport { easing } from 'maath';\n\ntype Mode = 'lens' | 'bar' | 'cube';\n\ninterface NavItem {\n label: string;\n link: string;\n}\n\ntype ModeProps = Record;\n\ninterface FluidGlassProps {\n mode?: Mode;\n lensProps?: ModeProps;\n barProps?: ModeProps;\n cubeProps?: ModeProps;\n}\n\nexport default function FluidGlass({ mode = 'lens', lensProps = {}, barProps = {}, cubeProps = {} }: FluidGlassProps) {\n const Wrapper = mode === 'bar' ? Bar : mode === 'cube' ? Cube : Lens;\n const rawOverrides = mode === 'bar' ? barProps : mode === 'cube' ? cubeProps : lensProps;\n\n const {\n navItems = [\n { label: 'Home', link: '' },\n { label: 'About', link: '' },\n { label: 'Contact', link: '' }\n ],\n ...modeProps\n } = rawOverrides;\n\n return (\n \n \n {mode === 'bar' && }\n \n \n \n \n \n \n \n \n \n \n );\n}\n\ntype MeshProps = ThreeElements['mesh'];\n\ninterface ModeWrapperProps extends MeshProps {\n children?: ReactNode;\n glb: string;\n geometryKey: string;\n lockToBottom?: boolean;\n followPointer?: boolean;\n modeProps?: ModeProps;\n}\n\ninterface ZoomMaterial extends THREE.Material {\n zoom: number;\n}\n\ninterface ZoomMesh extends THREE.Mesh {}\n\ntype ZoomGroup = THREE.Group & { children: ZoomMesh[] };\n\nconst ModeWrapper = memo(function ModeWrapper({\n children,\n glb,\n geometryKey,\n lockToBottom = false,\n followPointer = true,\n modeProps = {},\n ...props\n}: ModeWrapperProps) {\n const ref = useRef(null!);\n const { nodes } = useGLTF(glb);\n const buffer = useFBO();\n const { viewport: vp } = useThree();\n const [scene] = useState(() => new THREE.Scene());\n const geoWidthRef = useRef(1);\n\n useEffect(() => {\n const geo = (nodes[geometryKey] as THREE.Mesh)?.geometry;\n geo.computeBoundingBox();\n geoWidthRef.current = geo.boundingBox!.max.x - geo.boundingBox!.min.x || 1;\n }, [nodes, geometryKey]);\n\n useFrame((state, delta) => {\n const { gl, viewport, pointer, camera } = state;\n const v = viewport.getCurrentViewport(camera, [0, 0, 15]);\n\n const destX = followPointer ? (pointer.x * v.width) / 2 : 0;\n const destY = lockToBottom ? -v.height / 2 + 0.2 : followPointer ? (pointer.y * v.height) / 2 : 0;\n easing.damp3(ref.current.position, [destX, destY, 15], 0.15, delta);\n\n if ((modeProps as { scale?: number }).scale == null) {\n const maxWorld = v.width * 0.9;\n const desired = maxWorld / geoWidthRef.current;\n ref.current.scale.setScalar(Math.min(0.15, desired));\n }\n\n gl.setRenderTarget(buffer);\n gl.render(scene, camera);\n gl.setRenderTarget(null);\n gl.setClearColor(0x5227ff, 1);\n });\n\n const { scale, ior, thickness, anisotropy, chromaticAberration, ...extraMat } = modeProps as {\n scale?: number;\n ior?: number;\n thickness?: number;\n anisotropy?: number;\n chromaticAberration?: number;\n [key: string]: unknown;\n };\n\n return (\n <>\n {createPortal(children, scene)}\n \n \n \n \n \n \n \n \n );\n});\n\nfunction Lens({ modeProps, ...p }: { modeProps?: ModeProps } & MeshProps) {\n return ;\n}\n\nfunction Cube({ modeProps, ...p }: { modeProps?: ModeProps } & MeshProps) {\n return ;\n}\n\nfunction Bar({ modeProps = {}, ...p }: { modeProps?: ModeProps } & MeshProps) {\n const defaultMat = {\n transmission: 1,\n roughness: 0,\n thickness: 10,\n ior: 1.15,\n color: '#ffffff',\n attenuationColor: '#ffffff',\n attenuationDistance: 0.25\n };\n\n return (\n \n );\n}\n\nfunction NavItems({ items }: { items: NavItem[] }) {\n const group = useRef(null!);\n const { viewport, camera } = useThree();\n\n const DEVICE = {\n mobile: { max: 639, spacing: 0.2, fontSize: 0.035 },\n tablet: { max: 1023, spacing: 0.24, fontSize: 0.045 },\n desktop: { max: Infinity, spacing: 0.3, fontSize: 0.045 }\n };\n const getDevice = () => {\n const w = window.innerWidth;\n return w <= DEVICE.mobile.max ? 'mobile' : w <= DEVICE.tablet.max ? 'tablet' : 'desktop';\n };\n\n const [device, setDevice] = useState(getDevice());\n\n useEffect(() => {\n const onResize = () => setDevice(getDevice());\n window.addEventListener('resize', onResize);\n return () => window.removeEventListener('resize', onResize);\n }, []);\n\n const { spacing, fontSize } = DEVICE[device];\n\n useFrame(() => {\n if (!group.current) return;\n const v = viewport.getCurrentViewport(camera, [0, 0, 15]);\n group.current.position.set(0, -v.height / 2 + 0.2, 15.1);\n\n group.current.children.forEach((child, i) => {\n child.position.x = (i - (items.length - 1) / 2) * spacing;\n });\n });\n\n const handleNavigate = (link: string) => {\n if (!link) return;\n link.startsWith('#') ? (window.location.hash = link) : (window.location.href = link);\n };\n\n return (\n \n {items.map(({ label, link }) => (\n {\n e.stopPropagation();\n handleNavigate(link);\n }}\n onPointerOver={() => (document.body.style.cursor = 'pointer')}\n onPointerOut={() => (document.body.style.cursor = 'auto')}\n >\n {label}\n \n ))}\n \n );\n}\n\nfunction Images() {\n const group = useRef(null!);\n const data = useScroll();\n const { height } = useThree(s => s.viewport);\n\n useFrame(() => {\n group.current.children[0].material.zoom = 1 + data.range(0, 1 / 3) / 3;\n group.current.children[1].material.zoom = 1 + data.range(0, 1 / 3) / 3;\n group.current.children[2].material.zoom = 1 + data.range(1.15 / 3, 1 / 3) / 2;\n group.current.children[3].material.zoom = 1 + data.range(1.15 / 3, 1 / 3) / 2;\n group.current.children[4].material.zoom = 1 + data.range(1.15 / 3, 1 / 3) / 2;\n });\n\n return (\n \n \n \n \n \n \n \n );\n}\n\nfunction Typography() {\n const DEVICE = {\n mobile: { fontSize: 0.2 },\n tablet: { fontSize: 0.4 },\n desktop: { fontSize: 0.6 }\n };\n const getDevice = () => {\n const w = window.innerWidth;\n return w <= 639 ? 'mobile' : w <= 1023 ? 'tablet' : 'desktop';\n };\n\n const [device, setDevice] = useState(getDevice());\n\n useEffect(() => {\n const onResize = () => setDevice(getDevice());\n window.addEventListener('resize', onResize);\n return () => window.removeEventListener('resize', onResize);\n }, []);\n\n const { fontSize } = DEVICE[device];\n\n return (\n \n React Bits\n \n );\n}\n" + "content": "/* eslint-disable react/no-unknown-property */\nimport * as THREE from 'three';\nimport { useRef, useState, useEffect, memo, type ReactNode } from 'react';\nimport { Canvas, createPortal, useFrame, useThree, type ThreeElements } from '@react-three/fiber';\nimport {\n useFBO,\n useGLTF,\n useScroll,\n Image,\n Scroll,\n Preload,\n ScrollControls,\n MeshTransmissionMaterial,\n Text\n} from '@react-three/drei';\nimport { easing } from 'maath';\n\ntype Mode = 'lens' | 'bar' | 'cube';\n\ninterface NavItem {\n label: string;\n link: string;\n}\n\ntype ModeProps = Record;\n\ninterface FluidGlassProps {\n mode?: Mode;\n lensProps?: ModeProps;\n barProps?: ModeProps;\n cubeProps?: ModeProps;\n}\n\nexport default function FluidGlass({ mode = 'lens', lensProps = {}, barProps = {}, cubeProps = {} }: FluidGlassProps) {\n const Wrapper = mode === 'bar' ? Bar : mode === 'cube' ? Cube : Lens;\n const rawOverrides = mode === 'bar' ? barProps : mode === 'cube' ? cubeProps : lensProps;\n\n const {\n navItems = [\n { label: 'Home', link: '' },\n { label: 'About', link: '' },\n { label: 'Contact', link: '' }\n ],\n ...modeProps\n } = rawOverrides;\n\n return (\n \n \n {mode === 'bar' && }\n \n \n \n \n \n \n \n \n \n \n );\n}\n\ntype MeshProps = ThreeElements['mesh'];\n\ninterface ModeWrapperProps extends MeshProps {\n children?: ReactNode;\n glb: string;\n geometryKey: string;\n lockToBottom?: boolean;\n followPointer?: boolean;\n modeProps?: ModeProps;\n}\n\ninterface ZoomMaterial extends THREE.Material {\n zoom: number;\n}\n\ninterface ZoomMesh extends THREE.Mesh {}\n\ntype ZoomGroup = THREE.Group & { children: ZoomMesh[] };\n\nconst ModeWrapper = memo(function ModeWrapper({\n children,\n glb,\n geometryKey,\n lockToBottom = false,\n followPointer = true,\n modeProps = {},\n ...props\n}: ModeWrapperProps) {\n const ref = useRef(null!);\n const { nodes } = useGLTF(glb);\n const buffer = useFBO();\n const { viewport: vp } = useThree();\n const [scene] = useState(() => new THREE.Scene());\n const geoWidthRef = useRef(1);\n\n useEffect(() => {\n const geo = (nodes[geometryKey] as THREE.Mesh)?.geometry;\n geo.computeBoundingBox();\n geoWidthRef.current = geo.boundingBox!.max.x - geo.boundingBox!.min.x || 1;\n }, [nodes, geometryKey]);\n\n useFrame((state, delta) => {\n const { gl, viewport, pointer, camera } = state;\n const v = viewport.getCurrentViewport(camera, [0, 0, 15]);\n\n const destX = followPointer ? (pointer.x * v.width) / 2 : 0;\n const destY = lockToBottom ? -v.height / 2 + 0.2 : followPointer ? (pointer.y * v.height) / 2 : 0;\n easing.damp3(ref.current.position, [destX, destY, 15], 0.15, delta);\n\n if ((modeProps as { scale?: number }).scale == null) {\n const maxWorld = v.width * 0.9;\n const desired = maxWorld / geoWidthRef.current;\n ref.current.scale.setScalar(Math.min(0.15, desired));\n }\n\n gl.setRenderTarget(buffer);\n gl.render(scene, camera);\n gl.setRenderTarget(null);\n gl.setClearColor(0x5227ff, 1);\n });\n\n const { scale, ior, thickness, anisotropy, chromaticAberration, ...extraMat } = modeProps as {\n scale?: number;\n ior?: number;\n thickness?: number;\n anisotropy?: number;\n chromaticAberration?: number;\n [key: string]: unknown;\n };\n\n return (\n <>\n {createPortal(children, scene)}\n \n \n \n \n \n \n \n \n );\n});\n\nfunction Lens({ modeProps, ...p }: { modeProps?: ModeProps } & MeshProps) {\n return ;\n}\n\nfunction Cube({ modeProps, ...p }: { modeProps?: ModeProps } & MeshProps) {\n return ;\n}\n\nfunction Bar({ modeProps = {}, ...p }: { modeProps?: ModeProps } & MeshProps) {\n const defaultMat = {\n transmission: 1,\n roughness: 0,\n thickness: 10,\n ior: 1.15,\n color: '#ffffff',\n attenuationColor: '#ffffff',\n attenuationDistance: 0.25\n };\n\n return (\n \n );\n}\n\nfunction NavItems({ items }: { items: NavItem[] }) {\n const group = useRef(null!);\n const { viewport, camera } = useThree();\n\n const DEVICE = {\n mobile: { max: 639, spacing: 0.2, fontSize: 0.035 },\n tablet: { max: 1023, spacing: 0.24, fontSize: 0.045 },\n desktop: { max: Infinity, spacing: 0.3, fontSize: 0.045 }\n };\n const getDevice = () => {\n const w = window.innerWidth;\n return w <= DEVICE.mobile.max ? 'mobile' : w <= DEVICE.tablet.max ? 'tablet' : 'desktop';\n };\n\n const [device, setDevice] = useState(getDevice());\n\n useEffect(() => {\n const onResize = () => setDevice(getDevice());\n window.addEventListener('resize', onResize);\n return () => window.removeEventListener('resize', onResize);\n }, []);\n\n const { spacing, fontSize } = DEVICE[device];\n\n useFrame(() => {\n if (!group.current) return;\n const v = viewport.getCurrentViewport(camera, [0, 0, 15]);\n group.current.position.set(0, -v.height / 2 + 0.2, 15.1);\n\n group.current.children.forEach((child, i) => {\n child.position.x = (i - (items.length - 1) / 2) * spacing;\n });\n });\n\n const handleNavigate = (link: string) => {\n if (!link) return;\n link.startsWith('#') ? (window.location.hash = link) : (window.location.href = link);\n };\n\n return (\n \n {items.map(({ label, link }) => (\n {\n e.stopPropagation();\n handleNavigate(link);\n }}\n onPointerOver={() => (document.body.style.cursor = 'pointer')}\n onPointerOut={() => (document.body.style.cursor = 'auto')}\n >\n {label}\n \n ))}\n \n );\n}\n\nfunction Images() {\n const group = useRef(null!);\n const data = useScroll();\n const { height } = useThree(s => s.viewport);\n\n useFrame(() => {\n group.current.children[0].material.zoom = 1 + data.range(0, 1 / 3) / 3;\n group.current.children[1].material.zoom = 1 + data.range(0, 1 / 3) / 3;\n group.current.children[2].material.zoom = 1 + data.range(1.15 / 3, 1 / 3) / 2;\n group.current.children[3].material.zoom = 1 + data.range(1.15 / 3, 1 / 3) / 2;\n group.current.children[4].material.zoom = 1 + data.range(1.15 / 3, 1 / 3) / 2;\n });\n\n return (\n \n \n \n \n \n \n \n );\n}\n\nfunction Typography() {\n const DEVICE = {\n mobile: { fontSize: 0.2 },\n tablet: { fontSize: 0.4 },\n desktop: { fontSize: 0.6 }\n };\n const getDevice = () => {\n const w = window.innerWidth;\n return w <= 639 ? 'mobile' : w <= 1023 ? 'tablet' : 'desktop';\n };\n\n const [device, setDevice] = useState(getDevice());\n\n useEffect(() => {\n const onResize = () => setDevice(getDevice());\n window.addEventListener('resize', onResize);\n return () => window.removeEventListener('resize', onResize);\n }, []);\n\n const { fontSize } = DEVICE[device];\n\n return (\n \n React Bits\n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/FluidGlass-TS-TW.json b/public/r/FluidGlass-TS-TW.json index 8c01777b..36e58eab 100644 --- a/public/r/FluidGlass-TS-TW.json +++ b/public/r/FluidGlass-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "FluidGlass/FluidGlass.tsx", - "content": "/* eslint-disable react/no-unknown-property */\nimport * as THREE from 'three';\nimport { useRef, useState, useEffect, memo, ReactNode } from 'react';\nimport { Canvas, createPortal, useFrame, useThree, ThreeElements } from '@react-three/fiber';\nimport {\n useFBO,\n useGLTF,\n useScroll,\n Image,\n Scroll,\n Preload,\n ScrollControls,\n MeshTransmissionMaterial,\n Text\n} from '@react-three/drei';\nimport { easing } from 'maath';\n\ntype Mode = 'lens' | 'bar' | 'cube';\n\ninterface NavItem {\n label: string;\n link: string;\n}\n\ntype ModeProps = Record;\n\ninterface FluidGlassProps {\n mode?: Mode;\n lensProps?: ModeProps;\n barProps?: ModeProps;\n cubeProps?: ModeProps;\n}\n\nexport default function FluidGlass({ mode = 'lens', lensProps = {}, barProps = {}, cubeProps = {} }: FluidGlassProps) {\n const Wrapper = mode === 'bar' ? Bar : mode === 'cube' ? Cube : Lens;\n const rawOverrides = mode === 'bar' ? barProps : mode === 'cube' ? cubeProps : lensProps;\n\n const {\n navItems = [\n { label: 'Home', link: '' },\n { label: 'About', link: '' },\n { label: 'Contact', link: '' }\n ],\n ...modeProps\n } = rawOverrides;\n\n return (\n \n \n {mode === 'bar' && }\n \n \n \n \n \n \n \n \n \n \n );\n}\n\ntype MeshProps = ThreeElements['mesh'];\n\ninterface ModeWrapperProps extends MeshProps {\n children?: ReactNode;\n glb: string;\n geometryKey: string;\n lockToBottom?: boolean;\n followPointer?: boolean;\n modeProps?: ModeProps;\n}\n\ninterface ZoomMaterial extends THREE.Material {\n zoom: number;\n}\n\ninterface ZoomMesh extends THREE.Mesh {}\n\ntype ZoomGroup = THREE.Group & { children: ZoomMesh[] };\n\nconst ModeWrapper = memo(function ModeWrapper({\n children,\n glb,\n geometryKey,\n lockToBottom = false,\n followPointer = true,\n modeProps = {},\n ...props\n}: ModeWrapperProps) {\n const ref = useRef(null!);\n const { nodes } = useGLTF(glb);\n const buffer = useFBO();\n const { viewport: vp } = useThree();\n const [scene] = useState(() => new THREE.Scene());\n const geoWidthRef = useRef(1);\n\n useEffect(() => {\n const geo = (nodes[geometryKey] as THREE.Mesh)?.geometry;\n geo.computeBoundingBox();\n geoWidthRef.current = geo.boundingBox!.max.x - geo.boundingBox!.min.x || 1;\n }, [nodes, geometryKey]);\n\n useFrame((state, delta) => {\n const { gl, viewport, pointer, camera } = state;\n const v = viewport.getCurrentViewport(camera, [0, 0, 15]);\n\n const destX = followPointer ? (pointer.x * v.width) / 2 : 0;\n const destY = lockToBottom ? -v.height / 2 + 0.2 : followPointer ? (pointer.y * v.height) / 2 : 0;\n easing.damp3(ref.current.position, [destX, destY, 15], 0.15, delta);\n\n if ((modeProps as { scale?: number }).scale == null) {\n const maxWorld = v.width * 0.9;\n const desired = maxWorld / geoWidthRef.current;\n ref.current.scale.setScalar(Math.min(0.15, desired));\n }\n\n gl.setRenderTarget(buffer);\n gl.render(scene, camera);\n gl.setRenderTarget(null);\n gl.setClearColor(0x5227ff, 1);\n });\n\n const { scale, ior, thickness, anisotropy, chromaticAberration, ...extraMat } = modeProps as {\n scale?: number;\n ior?: number;\n thickness?: number;\n anisotropy?: number;\n chromaticAberration?: number;\n [key: string]: unknown;\n };\n\n return (\n <>\n {createPortal(children, scene)}\n \n \n \n \n \n \n \n \n );\n});\n\nfunction Lens({ modeProps, ...p }: { modeProps?: ModeProps } & MeshProps) {\n return ;\n}\n\nfunction Cube({ modeProps, ...p }: { modeProps?: ModeProps } & MeshProps) {\n return ;\n}\n\nfunction Bar({ modeProps = {}, ...p }: { modeProps?: ModeProps } & MeshProps) {\n const defaultMat = {\n transmission: 1,\n roughness: 0,\n thickness: 10,\n ior: 1.15,\n color: '#ffffff',\n attenuationColor: '#ffffff',\n attenuationDistance: 0.25\n };\n\n return (\n \n );\n}\n\nfunction NavItems({ items }: { items: NavItem[] }) {\n const group = useRef(null!);\n const { viewport, camera } = useThree();\n\n const DEVICE = {\n mobile: { max: 639, spacing: 0.2, fontSize: 0.035 },\n tablet: { max: 1023, spacing: 0.24, fontSize: 0.045 },\n desktop: { max: Infinity, spacing: 0.3, fontSize: 0.045 }\n };\n const getDevice = () => {\n const w = window.innerWidth;\n return w <= DEVICE.mobile.max ? 'mobile' : w <= DEVICE.tablet.max ? 'tablet' : 'desktop';\n };\n\n const [device, setDevice] = useState(getDevice());\n\n useEffect(() => {\n const onResize = () => setDevice(getDevice());\n window.addEventListener('resize', onResize);\n return () => window.removeEventListener('resize', onResize);\n }, []);\n\n const { spacing, fontSize } = DEVICE[device];\n\n useFrame(() => {\n if (!group.current) return;\n const v = viewport.getCurrentViewport(camera, [0, 0, 15]);\n group.current.position.set(0, -v.height / 2 + 0.2, 15.1);\n\n group.current.children.forEach((child, i) => {\n child.position.x = (i - (items.length - 1) / 2) * spacing;\n });\n });\n\n const handleNavigate = (link: string) => {\n if (!link) return;\n link.startsWith('#') ? (window.location.hash = link) : (window.location.href = link);\n };\n\n return (\n \n {items.map(({ label, link }) => (\n {\n e.stopPropagation();\n handleNavigate(link);\n }}\n onPointerOver={() => (document.body.style.cursor = 'pointer')}\n onPointerOut={() => (document.body.style.cursor = 'auto')}\n >\n {label}\n \n ))}\n \n );\n}\n\nfunction Images() {\n const group = useRef(null!);\n const data = useScroll();\n const { height } = useThree(s => s.viewport);\n\n useFrame(() => {\n group.current.children[0].material.zoom = 1 + data.range(0, 1 / 3) / 3;\n group.current.children[1].material.zoom = 1 + data.range(0, 1 / 3) / 3;\n group.current.children[2].material.zoom = 1 + data.range(1.15 / 3, 1 / 3) / 2;\n group.current.children[3].material.zoom = 1 + data.range(1.15 / 3, 1 / 3) / 2;\n group.current.children[4].material.zoom = 1 + data.range(1.15 / 3, 1 / 3) / 2;\n });\n\n return (\n \n \n \n \n \n \n \n );\n}\n\nfunction Typography() {\n const DEVICE = {\n mobile: { fontSize: 0.2 },\n tablet: { fontSize: 0.4 },\n desktop: { fontSize: 0.6 }\n };\n const getDevice = () => {\n const w = window.innerWidth;\n return w <= 639 ? 'mobile' : w <= 1023 ? 'tablet' : 'desktop';\n };\n\n const [device, setDevice] = useState(getDevice());\n\n useEffect(() => {\n const onResize = () => setDevice(getDevice());\n window.addEventListener('resize', onResize);\n return () => window.removeEventListener('resize', onResize);\n }, []);\n\n const { fontSize } = DEVICE[device];\n\n return (\n \n React Bits\n \n );\n}\n" + "content": "/* eslint-disable react/no-unknown-property */\nimport * as THREE from 'three';\nimport { useRef, useState, useEffect, memo, type ReactNode } from 'react';\nimport { Canvas, createPortal, useFrame, useThree, type ThreeElements } from '@react-three/fiber';\nimport {\n useFBO,\n useGLTF,\n useScroll,\n Image,\n Scroll,\n Preload,\n ScrollControls,\n MeshTransmissionMaterial,\n Text\n} from '@react-three/drei';\nimport { easing } from 'maath';\n\ntype Mode = 'lens' | 'bar' | 'cube';\n\ninterface NavItem {\n label: string;\n link: string;\n}\n\ntype ModeProps = Record;\n\ninterface FluidGlassProps {\n mode?: Mode;\n lensProps?: ModeProps;\n barProps?: ModeProps;\n cubeProps?: ModeProps;\n}\n\nexport default function FluidGlass({ mode = 'lens', lensProps = {}, barProps = {}, cubeProps = {} }: FluidGlassProps) {\n const Wrapper = mode === 'bar' ? Bar : mode === 'cube' ? Cube : Lens;\n const rawOverrides = mode === 'bar' ? barProps : mode === 'cube' ? cubeProps : lensProps;\n\n const {\n navItems = [\n { label: 'Home', link: '' },\n { label: 'About', link: '' },\n { label: 'Contact', link: '' }\n ],\n ...modeProps\n } = rawOverrides;\n\n return (\n \n \n {mode === 'bar' && }\n \n \n \n \n \n \n \n \n \n \n );\n}\n\ntype MeshProps = ThreeElements['mesh'];\n\ninterface ModeWrapperProps extends MeshProps {\n children?: ReactNode;\n glb: string;\n geometryKey: string;\n lockToBottom?: boolean;\n followPointer?: boolean;\n modeProps?: ModeProps;\n}\n\ninterface ZoomMaterial extends THREE.Material {\n zoom: number;\n}\n\ninterface ZoomMesh extends THREE.Mesh {}\n\ntype ZoomGroup = THREE.Group & { children: ZoomMesh[] };\n\nconst ModeWrapper = memo(function ModeWrapper({\n children,\n glb,\n geometryKey,\n lockToBottom = false,\n followPointer = true,\n modeProps = {},\n ...props\n}: ModeWrapperProps) {\n const ref = useRef(null!);\n const { nodes } = useGLTF(glb);\n const buffer = useFBO();\n const { viewport: vp } = useThree();\n const [scene] = useState(() => new THREE.Scene());\n const geoWidthRef = useRef(1);\n\n useEffect(() => {\n const geo = (nodes[geometryKey] as THREE.Mesh)?.geometry;\n geo.computeBoundingBox();\n geoWidthRef.current = geo.boundingBox!.max.x - geo.boundingBox!.min.x || 1;\n }, [nodes, geometryKey]);\n\n useFrame((state, delta) => {\n const { gl, viewport, pointer, camera } = state;\n const v = viewport.getCurrentViewport(camera, [0, 0, 15]);\n\n const destX = followPointer ? (pointer.x * v.width) / 2 : 0;\n const destY = lockToBottom ? -v.height / 2 + 0.2 : followPointer ? (pointer.y * v.height) / 2 : 0;\n easing.damp3(ref.current.position, [destX, destY, 15], 0.15, delta);\n\n if ((modeProps as { scale?: number }).scale == null) {\n const maxWorld = v.width * 0.9;\n const desired = maxWorld / geoWidthRef.current;\n ref.current.scale.setScalar(Math.min(0.15, desired));\n }\n\n gl.setRenderTarget(buffer);\n gl.render(scene, camera);\n gl.setRenderTarget(null);\n gl.setClearColor(0x5227ff, 1);\n });\n\n const { scale, ior, thickness, anisotropy, chromaticAberration, ...extraMat } = modeProps as {\n scale?: number;\n ior?: number;\n thickness?: number;\n anisotropy?: number;\n chromaticAberration?: number;\n [key: string]: unknown;\n };\n\n return (\n <>\n {createPortal(children, scene)}\n \n \n \n \n \n \n \n \n );\n});\n\nfunction Lens({ modeProps, ...p }: { modeProps?: ModeProps } & MeshProps) {\n return ;\n}\n\nfunction Cube({ modeProps, ...p }: { modeProps?: ModeProps } & MeshProps) {\n return ;\n}\n\nfunction Bar({ modeProps = {}, ...p }: { modeProps?: ModeProps } & MeshProps) {\n const defaultMat = {\n transmission: 1,\n roughness: 0,\n thickness: 10,\n ior: 1.15,\n color: '#ffffff',\n attenuationColor: '#ffffff',\n attenuationDistance: 0.25\n };\n\n return (\n \n );\n}\n\nfunction NavItems({ items }: { items: NavItem[] }) {\n const group = useRef(null!);\n const { viewport, camera } = useThree();\n\n const DEVICE = {\n mobile: { max: 639, spacing: 0.2, fontSize: 0.035 },\n tablet: { max: 1023, spacing: 0.24, fontSize: 0.045 },\n desktop: { max: Infinity, spacing: 0.3, fontSize: 0.045 }\n };\n const getDevice = () => {\n const w = window.innerWidth;\n return w <= DEVICE.mobile.max ? 'mobile' : w <= DEVICE.tablet.max ? 'tablet' : 'desktop';\n };\n\n const [device, setDevice] = useState(getDevice());\n\n useEffect(() => {\n const onResize = () => setDevice(getDevice());\n window.addEventListener('resize', onResize);\n return () => window.removeEventListener('resize', onResize);\n }, []);\n\n const { spacing, fontSize } = DEVICE[device];\n\n useFrame(() => {\n if (!group.current) return;\n const v = viewport.getCurrentViewport(camera, [0, 0, 15]);\n group.current.position.set(0, -v.height / 2 + 0.2, 15.1);\n\n group.current.children.forEach((child, i) => {\n child.position.x = (i - (items.length - 1) / 2) * spacing;\n });\n });\n\n const handleNavigate = (link: string) => {\n if (!link) return;\n link.startsWith('#') ? (window.location.hash = link) : (window.location.href = link);\n };\n\n return (\n \n {items.map(({ label, link }) => (\n {\n e.stopPropagation();\n handleNavigate(link);\n }}\n onPointerOver={() => (document.body.style.cursor = 'pointer')}\n onPointerOut={() => (document.body.style.cursor = 'auto')}\n >\n {label}\n \n ))}\n \n );\n}\n\nfunction Images() {\n const group = useRef(null!);\n const data = useScroll();\n const { height } = useThree(s => s.viewport);\n\n useFrame(() => {\n group.current.children[0].material.zoom = 1 + data.range(0, 1 / 3) / 3;\n group.current.children[1].material.zoom = 1 + data.range(0, 1 / 3) / 3;\n group.current.children[2].material.zoom = 1 + data.range(1.15 / 3, 1 / 3) / 2;\n group.current.children[3].material.zoom = 1 + data.range(1.15 / 3, 1 / 3) / 2;\n group.current.children[4].material.zoom = 1 + data.range(1.15 / 3, 1 / 3) / 2;\n });\n\n return (\n \n \n \n \n \n \n \n );\n}\n\nfunction Typography() {\n const DEVICE = {\n mobile: { fontSize: 0.2 },\n tablet: { fontSize: 0.4 },\n desktop: { fontSize: 0.6 }\n };\n const getDevice = () => {\n const w = window.innerWidth;\n return w <= 639 ? 'mobile' : w <= 1023 ? 'tablet' : 'desktop';\n };\n\n const [device, setDevice] = useState(getDevice());\n\n useEffect(() => {\n const onResize = () => setDevice(getDevice());\n window.addEventListener('resize', onResize);\n return () => window.removeEventListener('resize', onResize);\n }, []);\n\n const { fontSize } = DEVICE[device];\n\n return (\n \n React Bits\n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/FlyingPosters-JS-CSS.json b/public/r/FlyingPosters-JS-CSS.json index 14719d2d..902d642a 100644 --- a/public/r/FlyingPosters-JS-CSS.json +++ b/public/r/FlyingPosters-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "FlyingPosters/FlyingPosters.css", + "type": "registry:file", + "path": "FlyingPosters.css", + "target": "@components/FlyingPosters.css", "content": ".posters-container {\n width: 100%;\n height: 100%;\n overflow: hidden;\n position: relative;\n z-index: 2;\n}\n\n.posters-canvas {\n display: block;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "FlyingPosters/FlyingPosters.jsx", + "path": "FlyingPosters.jsx", "content": "import { useRef, useEffect } from 'react';\nimport { Renderer, Camera, Transform, Plane, Program, Mesh, Texture } from 'ogl';\n\nimport './FlyingPosters.css';\n\nconst vertexShader = `\nprecision highp float;\n\nattribute vec3 position;\nattribute vec2 uv;\nattribute vec3 normal;\n\nuniform mat4 modelViewMatrix;\nuniform mat4 projectionMatrix;\nuniform mat3 normalMatrix;\n\nuniform float uPosition;\nuniform float uTime;\nuniform float uSpeed;\nuniform vec3 distortionAxis;\nuniform vec3 rotationAxis;\nuniform float uDistortion;\n\nvarying vec2 vUv;\nvarying vec3 vNormal;\n\nfloat PI = 3.141592653589793238;\nmat4 rotationMatrix(vec3 axis, float angle) {\n axis = normalize(axis);\n float s = sin(angle);\n float c = cos(angle);\n float oc = 1.0 - c;\n \n return mat4(\n oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 0.0,\n oc * axis.x * axis.y + axis.z * s,oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, 0.0,\n oc * axis.z * axis.x - axis.y * s,oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c, 0.0,\n 0.0, 0.0, 0.0, 1.0\n );\n}\n\nvec3 rotate(vec3 v, vec3 axis, float angle) {\n mat4 m = rotationMatrix(axis, angle);\n return (m * vec4(v, 1.0)).xyz;\n}\n\nfloat qinticInOut(float t) {\n return t < 0.5\n ? 16.0 * pow(t, 5.0)\n : -0.5 * abs(pow(2.0 * t - 2.0, 5.0)) + 1.0;\n}\n\nvoid main() {\n vUv = uv;\n \n float norm = 0.5;\n vec3 newpos = position;\n float offset = (dot(distortionAxis, position) + norm / 2.) / norm;\n float localprogress = clamp(\n (fract(uPosition * 5.0 * 0.01) - 0.01 * uDistortion * offset) / (1. - 0.01 * uDistortion),\n 0.,\n 2.\n );\n localprogress = qinticInOut(localprogress) * PI;\n newpos = rotate(newpos, rotationAxis, localprogress);\n\n gl_Position = projectionMatrix * modelViewMatrix * vec4(newpos, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform vec2 uImageSize;\nuniform vec2 uPlaneSize;\nuniform sampler2D tMap;\n\nvarying vec2 vUv;\n\nvoid main() {\n vec2 imageSize = uImageSize;\n vec2 planeSize = uPlaneSize;\n\n float imageAspect = imageSize.x / imageSize.y;\n float planeAspect = planeSize.x / planeSize.y;\n vec2 scale = vec2(1.0, 1.0);\n\n if (planeAspect > imageAspect) {\n scale.x = imageAspect / planeAspect;\n } else {\n scale.y = planeAspect / imageAspect;\n }\n\n vec2 uv = vUv * scale + (1.0 - scale) * 0.5;\n\n gl_FragColor = texture2D(tMap, uv);\n}\n`;\n\nfunction AutoBind(self, { include, exclude } = {}) {\n const getAllProperties = object => {\n const properties = new Set();\n do {\n for (const key of Reflect.ownKeys(object)) {\n properties.add([object, key]);\n }\n } while ((object = Reflect.getPrototypeOf(object)) && object !== Object.prototype);\n return properties;\n };\n\n const filter = key => {\n const match = pattern => (typeof pattern === 'string' ? key === pattern : pattern.test(key));\n\n if (include) return include.some(match);\n if (exclude) return !exclude.some(match);\n return true;\n };\n\n for (const [object, key] of getAllProperties(self.constructor.prototype)) {\n if (key === 'constructor' || !filter(key)) continue;\n const descriptor = Reflect.getOwnPropertyDescriptor(object, key);\n if (descriptor && typeof descriptor.value === 'function') {\n self[key] = self[key].bind(self);\n }\n }\n return self;\n}\n\nfunction lerp(p1, p2, t) {\n return p1 + (p2 - p1) * t;\n}\n\nfunction map(num, min1, max1, min2, max2, round = false) {\n const num1 = (num - min1) / (max1 - min1);\n const num2 = num1 * (max2 - min2) + min2;\n return round ? Math.round(num2) : num2;\n}\n\nclass Media {\n constructor({ gl, geometry, scene, screen, viewport, image, length, index, planeWidth, planeHeight, distortion }) {\n this.extra = 0;\n this.gl = gl;\n this.geometry = geometry;\n this.scene = scene;\n this.screen = screen;\n this.viewport = viewport;\n this.image = image;\n this.length = length;\n this.index = index;\n this.planeWidth = planeWidth;\n this.planeHeight = planeHeight;\n this.distortion = distortion;\n\n this.createShader();\n this.createMesh();\n this.onResize();\n }\n\n createShader() {\n const texture = new Texture(this.gl, {\n generateMipmaps: false\n });\n\n this.program = new Program(this.gl, {\n depthTest: false,\n depthWrite: false,\n fragment: fragmentShader,\n vertex: vertexShader,\n uniforms: {\n tMap: { value: texture },\n uPosition: { value: 0 },\n uPlaneSize: { value: [0, 0] },\n uImageSize: { value: [0, 0] },\n uSpeed: { value: 0 },\n rotationAxis: { value: [0, 1, 0] },\n distortionAxis: { value: [1, 1, 0] },\n uDistortion: { value: this.distortion },\n uViewportSize: { value: [this.viewport.width, this.viewport.height] },\n uTime: { value: 0 }\n },\n cullFace: false\n });\n\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.src = this.image;\n img.onload = () => {\n texture.image = img;\n this.program.uniforms.uImageSize.value = [img.naturalWidth, img.naturalHeight];\n };\n }\n\n createMesh() {\n this.plane = new Mesh(this.gl, {\n geometry: this.geometry,\n program: this.program\n });\n this.plane.setParent(this.scene);\n }\n\n setScale() {\n this.plane.scale.x = (this.viewport.width * this.planeWidth) / this.screen.width;\n this.plane.scale.y = (this.viewport.height * this.planeHeight) / this.screen.height;\n\n this.plane.position.x = 0;\n this.plane.program.uniforms.uPlaneSize.value = [this.plane.scale.x, this.plane.scale.y];\n }\n\n onResize({ screen, viewport } = {}) {\n if (screen) this.screen = screen;\n if (viewport) {\n this.viewport = viewport;\n this.plane.program.uniforms.uViewportSize.value = [this.viewport.width, this.viewport.height];\n }\n this.setScale();\n\n this.padding = 5;\n this.height = this.plane.scale.y + this.padding;\n this.heightTotal = this.height * this.length;\n\n this.y = -this.heightTotal / 2 + (this.index + 0.5) * this.height;\n }\n\n update(scroll) {\n this.plane.position.y = this.y - scroll.current - this.extra;\n\n const position = map(this.plane.position.y, -this.viewport.height, this.viewport.height, 5, 15);\n\n this.program.uniforms.uPosition.value = position;\n this.program.uniforms.uTime.value += 0.04;\n this.program.uniforms.uSpeed.value = scroll.current;\n\n const planeHeight = this.plane.scale.y;\n const viewportHeight = this.viewport.height;\n\n const topEdge = this.plane.position.y + planeHeight / 2;\n const bottomEdge = this.plane.position.y - planeHeight / 2;\n\n if (topEdge < -viewportHeight / 2) {\n this.extra -= this.heightTotal;\n } else if (bottomEdge > viewportHeight / 2) {\n this.extra += this.heightTotal;\n }\n }\n}\n\nclass Canvas {\n constructor({ container, canvas, items, planeWidth, planeHeight, distortion, scrollEase, cameraFov, cameraZ }) {\n this.container = container;\n this.canvas = canvas;\n this.items = items;\n this.planeWidth = planeWidth;\n this.planeHeight = planeHeight;\n this.distortion = distortion;\n this.scroll = {\n ease: scrollEase,\n current: 0,\n target: 0,\n last: 0\n };\n this.cameraFov = cameraFov;\n this.cameraZ = cameraZ;\n\n AutoBind(this);\n\n this.createRenderer();\n this.createCamera();\n this.createScene();\n this.onResize();\n\n this.createGeometry();\n this.createMedias();\n this.update();\n this.addEventListeners();\n this.createPreloader();\n }\n\n createRenderer() {\n this.renderer = new Renderer({\n canvas: this.canvas,\n alpha: true,\n antialias: true,\n dpr: Math.min(window.devicePixelRatio, 2)\n });\n this.gl = this.renderer.gl;\n }\n\n createCamera() {\n this.camera = new Camera(this.gl);\n this.camera.fov = this.cameraFov;\n this.camera.position.z = this.cameraZ;\n }\n\n createScene() {\n this.scene = new Transform();\n }\n\n createGeometry() {\n this.planeGeometry = new Plane(this.gl, {\n heightSegments: 1,\n widthSegments: 100\n });\n }\n\n createMedias() {\n this.medias = this.items.map((image, index) => {\n return new Media({\n gl: this.gl,\n geometry: this.planeGeometry,\n scene: this.scene,\n screen: this.screen,\n viewport: this.viewport,\n image,\n length: this.items.length,\n index,\n planeWidth: this.planeWidth,\n planeHeight: this.planeHeight,\n distortion: this.distortion\n });\n });\n }\n\n createPreloader() {\n this.loaded = 0;\n if (!this.items.length) return;\n\n this.items.forEach(src => {\n const image = new Image();\n image.crossOrigin = 'anonymous';\n image.src = src;\n image.onload = () => {\n this.loaded += 1;\n if (this.loaded === this.items.length) {\n document.documentElement.classList.remove('loading');\n document.documentElement.classList.add('loaded');\n }\n };\n });\n }\n\n onResize() {\n const rect = this.container.getBoundingClientRect();\n this.screen = {\n width: rect.width,\n height: rect.height\n };\n\n this.renderer.setSize(this.screen.width, this.screen.height);\n\n this.camera.perspective({\n aspect: this.gl.canvas.width / this.gl.canvas.height\n });\n\n const fov = (this.camera.fov * Math.PI) / 180;\n const height = 2 * Math.tan(fov / 2) * this.camera.position.z;\n const width = height * this.camera.aspect;\n\n this.viewport = { height, width };\n\n if (this.medias) {\n this.medias.forEach(media => media.onResize({ screen: this.screen, viewport: this.viewport }));\n }\n }\n\n onTouchDown(e) {\n this.isDown = true;\n this.scroll.position = this.scroll.current;\n this.start = e.touches ? e.touches[0].clientY : e.clientY;\n }\n\n onTouchMove(e) {\n if (!this.isDown) return;\n const y = e.touches ? e.touches[0].clientY : e.clientY;\n const distance = (this.start - y) * 0.1;\n this.scroll.target = this.scroll.position + distance;\n }\n\n onTouchUp() {\n this.isDown = false;\n }\n\n onWheel(e) {\n const speed = e.deltaY;\n this.scroll.target += speed * 0.005;\n }\n\n update() {\n this.scroll.current = lerp(this.scroll.current, this.scroll.target, this.scroll.ease);\n\n if (this.medias) {\n this.medias.forEach(media => media.update(this.scroll));\n }\n this.renderer.render({ scene: this.scene, camera: this.camera });\n this.scroll.last = this.scroll.current;\n requestAnimationFrame(this.update);\n }\n\n addEventListeners() {\n window.addEventListener('resize', this.onResize);\n window.addEventListener('wheel', this.onWheel);\n window.addEventListener('mousewheel', this.onWheel);\n\n window.addEventListener('mousedown', this.onTouchDown);\n window.addEventListener('mousemove', this.onTouchMove);\n window.addEventListener('mouseup', this.onTouchUp);\n\n window.addEventListener('touchstart', this.onTouchDown);\n window.addEventListener('touchmove', this.onTouchMove);\n window.addEventListener('touchend', this.onTouchUp);\n }\n\n destroy() {\n window.removeEventListener('resize', this.onResize);\n window.removeEventListener('wheel', this.onWheel);\n window.removeEventListener('mousewheel', this.onWheel);\n\n window.removeEventListener('mousedown', this.onTouchDown);\n window.removeEventListener('mousemove', this.onTouchMove);\n window.removeEventListener('mouseup', this.onTouchUp);\n\n window.removeEventListener('touchstart', this.onTouchDown);\n window.removeEventListener('touchmove', this.onTouchMove);\n window.removeEventListener('touchend', this.onTouchUp);\n }\n}\n\nexport default function FlyingPosters({\n items = [],\n planeWidth = 320,\n planeHeight = 320,\n distortion = 3,\n scrollEase = 0.01,\n cameraFov = 45,\n cameraZ = 20,\n className,\n ...props\n}) {\n const containerRef = useRef(null);\n const canvasRef = useRef(null);\n const instanceRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n instanceRef.current = new Canvas({\n container: containerRef.current,\n canvas: canvasRef.current,\n items,\n planeWidth,\n planeHeight,\n distortion,\n scrollEase,\n cameraFov,\n cameraZ\n });\n\n return () => {\n if (instanceRef.current) {\n instanceRef.current.destroy();\n instanceRef.current = null;\n }\n };\n }, [items, planeWidth, planeHeight, distortion, scrollEase, cameraFov, cameraZ]);\n\n useEffect(() => {\n if (!canvasRef.current) return;\n\n const canvasEl = canvasRef.current;\n\n const handleWheel = e => {\n e.preventDefault();\n if (instanceRef.current) {\n instanceRef.current.onWheel(e);\n }\n };\n\n const handleTouchMove = e => {\n e.preventDefault();\n };\n\n canvasEl.addEventListener('wheel', handleWheel, { passive: false });\n canvasEl.addEventListener('touchmove', handleTouchMove, { passive: false });\n\n return () => {\n canvasEl.removeEventListener('wheel', handleWheel);\n canvasEl.removeEventListener('touchmove', handleTouchMove);\n };\n }, []);\n\n return (\n
\n \n
\n );\n}\n" } ], diff --git a/public/r/FlyingPosters-TS-CSS.json b/public/r/FlyingPosters-TS-CSS.json index a5eec93a..600a9572 100644 --- a/public/r/FlyingPosters-TS-CSS.json +++ b/public/r/FlyingPosters-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "FlyingPosters/FlyingPosters.css", + "type": "registry:file", + "path": "FlyingPosters.css", + "target": "@components/FlyingPosters.css", "content": ".posters-container {\n width: 100%;\n height: 100%;\n overflow: hidden;\n position: relative;\n z-index: 2;\n}\n\n.posters-canvas {\n display: block;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "FlyingPosters/FlyingPosters.tsx", + "path": "FlyingPosters.tsx", "content": "import { useRef, useEffect } from 'react';\nimport { Renderer, Camera, Transform, Plane, Program, Mesh, Texture, type OGLRenderingContext } from 'ogl';\n\nimport './FlyingPosters.css';\n\ntype GL = OGLRenderingContext;\ntype OGLProgram = Program;\ntype OGLMesh = Mesh;\ntype OGLTransform = Transform;\ntype OGLPlane = Plane;\n\ninterface ScreenSize {\n width: number;\n height: number;\n}\n\ninterface ViewportSize {\n width: number;\n height: number;\n}\n\ninterface ScrollState {\n position?: number;\n ease: number;\n current: number;\n target: number;\n last: number;\n}\n\ninterface AutoBindOptions {\n include?: Array;\n exclude?: Array;\n}\n\ninterface MediaParams {\n gl: GL;\n geometry: OGLPlane;\n scene: OGLTransform;\n screen: ScreenSize;\n viewport: ViewportSize;\n image: string;\n length: number;\n index: number;\n planeWidth: number;\n planeHeight: number;\n distortion: number;\n}\n\ninterface CanvasParams {\n container: HTMLElement;\n canvas: HTMLCanvasElement;\n items: string[];\n planeWidth: number;\n planeHeight: number;\n distortion: number;\n scrollEase: number;\n cameraFov: number;\n cameraZ: number;\n}\n\nconst vertexShader = `\nprecision highp float;\n\nattribute vec3 position;\nattribute vec2 uv;\nattribute vec3 normal;\n\nuniform mat4 modelViewMatrix;\nuniform mat4 projectionMatrix;\nuniform mat3 normalMatrix;\n\nuniform float uPosition;\nuniform float uTime;\nuniform float uSpeed;\nuniform vec3 distortionAxis;\nuniform vec3 rotationAxis;\nuniform float uDistortion;\n\nvarying vec2 vUv;\nvarying vec3 vNormal;\n\nfloat PI = 3.141592653589793238;\nmat4 rotationMatrix(vec3 axis, float angle) {\n axis = normalize(axis);\n float s = sin(angle);\n float c = cos(angle);\n float oc = 1.0 - c;\n \n return mat4(\n oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 0.0,\n oc * axis.x * axis.y + axis.z * s,oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, 0.0,\n oc * axis.z * axis.x - axis.y * s,oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c, 0.0,\n 0.0, 0.0, 0.0, 1.0\n );\n}\n\nvec3 rotate(vec3 v, vec3 axis, float angle) {\n mat4 m = rotationMatrix(axis, angle);\n return (m * vec4(v, 1.0)).xyz;\n}\n\nfloat qinticInOut(float t) {\n return t < 0.5\n ? 16.0 * pow(t, 5.0)\n : -0.5 * abs(pow(2.0 * t - 2.0, 5.0)) + 1.0;\n}\n\nvoid main() {\n vUv = uv;\n \n float norm = 0.5;\n vec3 newpos = position;\n float offset = (dot(distortionAxis, position) + norm / 2.) / norm;\n float localprogress = clamp(\n (fract(uPosition * 5.0 * 0.01) - 0.01 * uDistortion * offset) / (1. - 0.01 * uDistortion),\n 0.,\n 2.\n );\n localprogress = qinticInOut(localprogress) * PI;\n newpos = rotate(newpos, rotationAxis, localprogress);\n\n gl_Position = projectionMatrix * modelViewMatrix * vec4(newpos, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform vec2 uImageSize;\nuniform vec2 uPlaneSize;\nuniform sampler2D tMap;\n\nvarying vec2 vUv;\n\nvoid main() {\n vec2 imageSize = uImageSize;\n vec2 planeSize = uPlaneSize;\n\n float imageAspect = imageSize.x / imageSize.y;\n float planeAspect = planeSize.x / planeSize.y;\n vec2 scale = vec2(1.0, 1.0);\n\n if (planeAspect > imageAspect) {\n scale.x = imageAspect / planeAspect;\n } else {\n scale.y = planeAspect / imageAspect;\n }\n\n vec2 uv = vUv * scale + (1.0 - scale) * 0.5;\n\n gl_FragColor = texture2D(tMap, uv);\n}\n`;\n\nfunction AutoBind(self: any, { include, exclude }: AutoBindOptions = {}) {\n const getAllProperties = (object: any): Set<[any, string | symbol]> => {\n const properties = new Set<[any, string | symbol]>();\n do {\n for (const key of Reflect.ownKeys(object)) {\n properties.add([object, key]);\n }\n } while ((object = Reflect.getPrototypeOf(object)) && object !== Object.prototype);\n return properties;\n };\n\n const filter = (key: string | symbol) => {\n const match = (pattern: string | RegExp) =>\n typeof pattern === 'string' ? key === pattern : (pattern as RegExp).test(key.toString());\n\n if (include) return include.some(match);\n if (exclude) return !exclude.some(match);\n return true;\n };\n\n for (const [object, key] of getAllProperties(self.constructor.prototype)) {\n if (key === 'constructor' || !filter(key)) continue;\n const descriptor = Reflect.getOwnPropertyDescriptor(object, key);\n if (descriptor && typeof descriptor.value === 'function') {\n self[key] = self[key].bind(self);\n }\n }\n return self;\n}\n\nfunction lerp(p1: number, p2: number, t: number): number {\n return p1 + (p2 - p1) * t;\n}\n\nfunction map(num: number, min1: number, max1: number, min2: number, max2: number, round = false): number {\n const num1 = (num - min1) / (max1 - min1);\n const num2 = num1 * (max2 - min2) + min2;\n return round ? Math.round(num2) : num2;\n}\n\nclass Media {\n gl: GL;\n geometry: OGLPlane;\n scene: OGLTransform;\n screen: ScreenSize;\n viewport: ViewportSize;\n image: string;\n length: number;\n index: number;\n planeWidth: number;\n planeHeight: number;\n distortion: number;\n\n program!: OGLProgram;\n plane!: OGLMesh;\n extra = 0;\n padding = 0;\n height = 0;\n heightTotal = 0;\n y = 0;\n\n constructor({\n gl,\n geometry,\n scene,\n screen,\n viewport,\n image,\n length,\n index,\n planeWidth,\n planeHeight,\n distortion\n }: MediaParams) {\n this.gl = gl;\n this.geometry = geometry;\n this.scene = scene;\n this.screen = screen;\n this.viewport = viewport;\n this.image = image;\n this.length = length;\n this.index = index;\n this.planeWidth = planeWidth;\n this.planeHeight = planeHeight;\n this.distortion = distortion;\n\n this.createShader();\n this.createMesh();\n this.onResize();\n }\n\n createShader() {\n const texture = new Texture(this.gl, { generateMipmaps: false });\n this.program = new Program(this.gl, {\n depthTest: false,\n depthWrite: false,\n fragment: fragmentShader,\n vertex: vertexShader,\n uniforms: {\n tMap: { value: texture },\n uPosition: { value: 0 },\n uPlaneSize: { value: [0, 0] },\n uImageSize: { value: [0, 0] },\n uSpeed: { value: 0 },\n rotationAxis: { value: [0, 1, 0] },\n distortionAxis: { value: [1, 1, 0] },\n uDistortion: { value: this.distortion },\n uViewportSize: { value: [this.viewport.width, this.viewport.height] },\n uTime: { value: 0 }\n },\n cullFace: false\n });\n\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.src = this.image;\n img.onload = () => {\n texture.image = img;\n this.program.uniforms.uImageSize.value = [img.naturalWidth, img.naturalHeight];\n };\n }\n\n createMesh() {\n this.plane = new Mesh(this.gl, {\n geometry: this.geometry,\n program: this.program\n });\n this.plane.setParent(this.scene);\n }\n\n setScale() {\n this.plane.scale.x = (this.viewport.width * this.planeWidth) / this.screen.width;\n this.plane.scale.y = (this.viewport.height * this.planeHeight) / this.screen.height;\n this.plane.position.x = 0;\n this.program.uniforms.uPlaneSize.value = [this.plane.scale.x, this.plane.scale.y];\n }\n\n onResize({ screen, viewport }: { screen?: ScreenSize; viewport?: ViewportSize } = {}) {\n if (screen) this.screen = screen;\n if (viewport) {\n this.viewport = viewport;\n this.program.uniforms.uViewportSize.value = [viewport.width, viewport.height];\n }\n this.setScale();\n\n this.padding = 5;\n this.height = this.plane.scale.y + this.padding;\n this.heightTotal = this.height * this.length;\n this.y = -this.heightTotal / 2 + (this.index + 0.5) * this.height;\n }\n\n update(scroll: ScrollState) {\n this.plane.position.y = this.y - scroll.current - this.extra;\n const position = map(this.plane.position.y, -this.viewport.height, this.viewport.height, 5, 15);\n\n this.program.uniforms.uPosition.value = position;\n this.program.uniforms.uTime.value += 0.04;\n this.program.uniforms.uSpeed.value = scroll.current;\n\n const planeHeight = this.plane.scale.y;\n const viewportHeight = this.viewport.height;\n const topEdge = this.plane.position.y + planeHeight / 2;\n const bottomEdge = this.plane.position.y - planeHeight / 2;\n\n if (topEdge < -viewportHeight / 2) {\n this.extra -= this.heightTotal;\n } else if (bottomEdge > viewportHeight / 2) {\n this.extra += this.heightTotal;\n }\n }\n}\n\nclass Canvas {\n container: HTMLElement;\n canvas: HTMLCanvasElement;\n items: string[];\n planeWidth: number;\n planeHeight: number;\n distortion: number;\n scroll: ScrollState;\n cameraFov: number;\n cameraZ: number;\n\n renderer!: Renderer;\n gl!: GL;\n camera!: Camera;\n scene!: OGLTransform;\n planeGeometry!: OGLPlane;\n medias!: Media[];\n screen!: ScreenSize;\n viewport!: ViewportSize;\n isDown = false;\n start = 0;\n loaded = 0;\n\n constructor({\n container,\n canvas,\n items,\n planeWidth,\n planeHeight,\n distortion,\n scrollEase,\n cameraFov,\n cameraZ\n }: CanvasParams) {\n this.container = container;\n this.canvas = canvas;\n this.items = items;\n this.planeWidth = planeWidth;\n this.planeHeight = planeHeight;\n this.distortion = distortion;\n this.scroll = {\n ease: scrollEase,\n current: 0,\n target: 0,\n last: 0\n };\n this.cameraFov = cameraFov;\n this.cameraZ = cameraZ;\n\n AutoBind(this);\n this.createRenderer();\n this.createCamera();\n this.createScene();\n this.onResize();\n this.createGeometry();\n this.createMedias();\n this.update();\n this.addEventListeners();\n this.createPreloader();\n }\n\n createRenderer() {\n this.renderer = new Renderer({\n canvas: this.canvas,\n alpha: true,\n antialias: true,\n dpr: Math.min(window.devicePixelRatio, 2)\n });\n this.gl = this.renderer.gl;\n }\n\n createCamera() {\n this.camera = new Camera(this.gl);\n this.camera.fov = this.cameraFov;\n this.camera.position.z = this.cameraZ;\n }\n\n createScene() {\n this.scene = new Transform();\n }\n\n createGeometry() {\n this.planeGeometry = new Plane(this.gl, {\n heightSegments: 1,\n widthSegments: 100\n });\n }\n\n createMedias() {\n this.medias = this.items.map(\n (image, index) =>\n new Media({\n gl: this.gl,\n geometry: this.planeGeometry,\n scene: this.scene,\n screen: this.screen,\n viewport: this.viewport,\n image,\n length: this.items.length,\n index,\n planeWidth: this.planeWidth,\n planeHeight: this.planeHeight,\n distortion: this.distortion\n })\n );\n }\n\n createPreloader() {\n this.loaded = 0;\n this.items.forEach(src => {\n const image = new Image();\n image.crossOrigin = 'anonymous';\n image.src = src;\n image.onload = () => {\n if (++this.loaded === this.items.length) {\n document.documentElement.classList.remove('loading');\n document.documentElement.classList.add('loaded');\n }\n };\n });\n }\n\n onResize() {\n const rect = this.container.getBoundingClientRect();\n this.screen = { width: rect.width, height: rect.height };\n this.renderer.setSize(this.screen.width, this.screen.height);\n\n this.camera.perspective({\n aspect: this.gl.canvas.width / this.gl.canvas.height\n });\n\n const fov = (this.camera.fov * Math.PI) / 180;\n const height = 2 * Math.tan(fov / 2) * this.camera.position.z;\n const width = height * this.camera.aspect;\n this.viewport = { width, height };\n\n this.medias?.forEach(media => media.onResize({ screen: this.screen, viewport: this.viewport }));\n }\n\n onTouchDown(e: MouseEvent | TouchEvent) {\n this.isDown = true;\n this.scroll.position = this.scroll.current;\n this.start = e instanceof TouchEvent ? e.touches[0].clientY : e.clientY;\n }\n\n onTouchMove(e: MouseEvent | TouchEvent) {\n if (!this.isDown || !this.scroll.position) return;\n const y = e instanceof TouchEvent ? e.touches[0].clientY : e.clientY;\n const distance = (this.start - y) * 0.1;\n this.scroll.target = this.scroll.position + distance;\n }\n\n onTouchUp() {\n this.isDown = false;\n }\n\n onWheel(e: WheelEvent) {\n this.scroll.target += e.deltaY * 0.005;\n }\n\n update() {\n this.scroll.current = lerp(this.scroll.current, this.scroll.target, this.scroll.ease);\n this.medias?.forEach(media => media.update(this.scroll));\n this.renderer.render({ scene: this.scene, camera: this.camera });\n this.scroll.last = this.scroll.current;\n requestAnimationFrame(this.update);\n }\n\n addEventListeners() {\n window.addEventListener('resize', this.onResize);\n window.addEventListener('wheel', this.onWheel);\n window.addEventListener('mousedown', this.onTouchDown);\n window.addEventListener('mousemove', this.onTouchMove);\n window.addEventListener('mouseup', this.onTouchUp);\n window.addEventListener('touchstart', this.onTouchDown as EventListener);\n window.addEventListener('touchmove', this.onTouchMove as EventListener);\n window.addEventListener('touchend', this.onTouchUp as EventListener);\n }\n\n destroy() {\n window.removeEventListener('resize', this.onResize);\n window.removeEventListener('wheel', this.onWheel);\n window.removeEventListener('mousedown', this.onTouchDown);\n window.removeEventListener('mousemove', this.onTouchMove);\n window.removeEventListener('mouseup', this.onTouchUp);\n window.removeEventListener('touchstart', this.onTouchDown as EventListener);\n window.removeEventListener('touchmove', this.onTouchMove as EventListener);\n window.removeEventListener('touchend', this.onTouchUp as EventListener);\n }\n}\n\ninterface FlyingPostersProps extends React.HTMLAttributes {\n items?: string[];\n planeWidth?: number;\n planeHeight?: number;\n distortion?: number;\n scrollEase?: number;\n cameraFov?: number;\n cameraZ?: number;\n}\n\nexport default function FlyingPosters({\n items = [],\n planeWidth = 320,\n planeHeight = 320,\n distortion = 3,\n scrollEase = 0.01,\n cameraFov = 45,\n cameraZ = 20,\n className,\n ...props\n}: FlyingPostersProps) {\n const containerRef = useRef(null);\n const canvasRef = useRef(null);\n const instanceRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current || !canvasRef.current) return;\n\n instanceRef.current = new Canvas({\n container: containerRef.current,\n canvas: canvasRef.current,\n items,\n planeWidth,\n planeHeight,\n distortion,\n scrollEase,\n cameraFov,\n cameraZ\n });\n\n return () => {\n instanceRef.current?.destroy();\n instanceRef.current = null;\n };\n }, [items, planeWidth, planeHeight, distortion, scrollEase, cameraFov, cameraZ]);\n\n useEffect(() => {\n if (!canvasRef.current) return;\n\n const canvasEl = canvasRef.current;\n\n const handleWheel = (e: WheelEvent) => {\n e.preventDefault();\n if (instanceRef.current) {\n instanceRef.current.onWheel(e);\n }\n };\n\n const handleTouchMove = (e: TouchEvent) => {\n e.preventDefault();\n };\n\n canvasEl.addEventListener('wheel', handleWheel, { passive: false });\n canvasEl.addEventListener('touchmove', handleTouchMove, { passive: false });\n\n return () => {\n canvasEl.removeEventListener('wheel', handleWheel);\n canvasEl.removeEventListener('touchmove', handleTouchMove);\n };\n }, []);\n\n return (\n
\n \n
\n );\n}\n" } ], diff --git a/public/r/FoldText-JS-CSS.json b/public/r/FoldText-JS-CSS.json index 2e67ebe2..19d78a8e 100644 --- a/public/r/FoldText-JS-CSS.json +++ b/public/r/FoldText-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "FoldText/FoldText.css", + "type": "registry:file", + "path": "FoldText.css", + "target": "@components/FoldText.css", "content": ".fold-text {\n display: inline-block;\n color: var(--fold-text-color, currentColor);\n font-size: var(--fold-text-font-size, inherit);\n font-weight: var(--fold-text-font-weight, inherit);\n line-height: 0.95;\n letter-spacing: -0.04em;\n white-space: pre-wrap;\n user-select: text;\n}\n\n.fold-text-sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n.fold-text-visual {\n display: inline;\n}\n\n.fold-text-line {\n display: block;\n}\n\n.fold-text-whitespace {\n display: inline;\n}\n\n.fold-text-segment {\n display: inline-block;\n line-height: inherit;\n perspective: var(--fold-perspective, 700px);\n transform-style: preserve-3d;\n vertical-align: baseline;\n}\n\n.fold-text-segment[data-fold-split='line'] {\n display: block;\n}\n\n.fold-text-piece {\n position: relative;\n display: inline-block;\n color: inherit;\n line-height: inherit;\n transform-style: preserve-3d;\n backface-visibility: hidden;\n will-change: transform, opacity;\n}\n\n.fold-text-piece::after {\n content: '';\n position: absolute;\n inset: -0.08em -0.02em;\n pointer-events: none;\n opacity: var(--fold-crease, 0);\n mix-blend-mode: multiply;\n border-radius: 0.08em;\n}\n\n.fold-text-piece[data-fold-hinge='top']::after {\n background: linear-gradient(180deg, rgba(0, 0, 0, 0.58) 0%, rgba(0, 0, 0, 0.22) 42%, rgba(255, 255, 255, 0.26) 100%);\n}\n\n.fold-text-piece[data-fold-hinge='bottom']::after {\n background: linear-gradient(0deg, rgba(0, 0, 0, 0.58) 0%, rgba(0, 0, 0, 0.22) 42%, rgba(255, 255, 255, 0.26) 100%);\n}\n\n.fold-text-piece[data-fold-hinge='left']::after {\n background: linear-gradient(90deg, rgba(0, 0, 0, 0.58) 0%, rgba(0, 0, 0, 0.22) 42%, rgba(255, 255, 255, 0.26) 100%);\n}\n\n.fold-text-piece[data-fold-hinge='right']::after {\n background: linear-gradient(270deg, rgba(0, 0, 0, 0.58) 0%, rgba(0, 0, 0, 0.22) 42%, rgba(255, 255, 255, 0.26) 100%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fold-text-piece {\n transform: none !important;\n }\n\n .fold-text-piece::after {\n opacity: 0 !important;\n }\n}\n" }, { "type": "registry:component", - "path": "FoldText/FoldText.jsx", + "path": "FoldText.jsx", "content": "import { useEffect, useMemo, useRef } from 'react';\nimport { gsap } from 'gsap';\nimport { ScrollTrigger } from 'gsap/ScrollTrigger';\n\nimport './FoldText.css';\n\ngsap.registerPlugin(ScrollTrigger);\n\nconst HINGE_CONFIG = {\n top: { origin: '50% 0%', rotateX: -92, rotateY: 0 },\n bottom: { origin: '50% 100%', rotateX: 92, rotateY: 0 },\n left: { origin: '0% 50%', rotateX: 0, rotateY: 92 },\n right: { origin: '100% 50%', rotateX: 0, rotateY: -92 }\n};\n\nconst clamp = (value, min, max) => Math.min(max, Math.max(min, value));\n\nconst renderWhitespace = (value, key) =>\n value.split(/(\\n)/).map((part, index) => {\n if (part === '\\n') return
;\n if (!part) return null;\n\n return (\n \n {part.replace(/ /g, '\\u00A0')}\n \n );\n });\n\nconst FoldText = ({\n text = 'Design unfolds',\n splitBy = 'char',\n hinge = 'top',\n duration = 0.65,\n stagger = 0.045,\n ease = 'power3.out',\n perspective = 700,\n creaseShading = 0.55,\n trigger = 'mount',\n fontSize = 80,\n fontWeight = 800,\n color = '#f7f2e8',\n className = '',\n style = {}\n}) => {\n const rootRef = useRef(null);\n const timelineRef = useRef(null);\n const hingeConfig = HINGE_CONFIG[hinge] || HINGE_CONFIG.top;\n const safeCrease = clamp(creaseShading, 0, 1);\n const safePerspective = Math.max(120, perspective);\n\n const segments = useMemo(() => {\n let segmentIndex = 0;\n\n const renderSegment = (content, key, split = splitBy) => {\n segmentIndex += 1;\n return (\n \n \n {content || '\\u00A0'}\n \n \n );\n };\n\n if (splitBy === 'line') {\n return text.split('\\n').map((line, index) => (\n \n {renderSegment(line || '\\u00A0', `segment-line-${index}`, 'line')}\n \n ));\n }\n\n if (splitBy === 'word') {\n return text.split(/(\\s+)/).flatMap((part, index) => {\n if (!part) return [];\n if (/^\\s+$/.test(part)) return renderWhitespace(part, `ws-${index}`);\n return renderSegment(part, `segment-word-${segmentIndex}`);\n });\n }\n\n return Array.from(text).map((char, index) => {\n if (char === '\\n') return
;\n return renderSegment(char === ' ' ? '\\u00A0' : char, `segment-char-${index}`);\n });\n }, [text, splitBy, hinge, hingeConfig.origin, safePerspective]);\n\n useEffect(() => {\n if (typeof window === 'undefined') return undefined;\n\n const root = rootRef.current;\n if (!root) return undefined;\n\n const pieces = Array.from(root.querySelectorAll('.fold-text-piece'));\n if (!pieces.length) return undefined;\n\n const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;\n const activeDuration = reduceMotion ? Math.min(duration, 0.22) : duration;\n const activeStagger = reduceMotion ? Math.min(stagger, 0.02) : stagger;\n const fromVars = {\n opacity: 0,\n rotateX: reduceMotion ? 0 : hingeConfig.rotateX,\n rotateY: reduceMotion ? 0 : hingeConfig.rotateY,\n '--fold-crease': reduceMotion ? 0 : safeCrease,\n transformOrigin: hingeConfig.origin,\n force3D: true\n };\n const toVars = {\n opacity: 1,\n rotateX: 0,\n rotateY: 0,\n '--fold-crease': 0,\n duration: activeDuration,\n ease: reduceMotion ? 'power1.out' : ease,\n stagger: activeStagger,\n clearProps: 'willChange'\n };\n\n const killTimeline = () => {\n timelineRef.current?.kill();\n timelineRef.current = null;\n gsap.killTweensOf(pieces);\n };\n\n const play = repeat => {\n killTimeline();\n timelineRef.current = gsap.timeline({ repeat: repeat ? -1 : 0, repeatDelay: repeat ? 0.75 : 0 });\n timelineRef.current.fromTo(pieces, fromVars, toVars);\n return timelineRef.current;\n };\n\n let scrollTrigger;\n let hoverHandler;\n\n if (trigger === 'hover') {\n gsap.set(pieces, { opacity: 1, rotateX: 0, rotateY: 0, '--fold-crease': 0, transformOrigin: hingeConfig.origin });\n hoverHandler = () => play(false);\n root.addEventListener('mouseenter', hoverHandler);\n } else if (trigger === 'scroll') {\n gsap.set(pieces, fromVars);\n scrollTrigger = ScrollTrigger.create({\n trigger: root,\n start: 'top 82%',\n once: true,\n onEnter: () => play(false)\n });\n } else if (trigger === 'loop') {\n play(true);\n } else {\n play(false);\n }\n\n return () => {\n if (hoverHandler) root.removeEventListener('mouseenter', hoverHandler);\n scrollTrigger?.kill();\n killTimeline();\n };\n }, [\n text,\n splitBy,\n hinge,\n duration,\n stagger,\n ease,\n perspective,\n safeCrease,\n trigger,\n hingeConfig.origin,\n hingeConfig.rotateX,\n hingeConfig.rotateY\n ]);\n\n const rootStyle = {\n '--fold-text-font-size': typeof fontSize === 'number' ? `${fontSize}px` : fontSize,\n '--fold-text-font-weight': fontWeight,\n '--fold-text-color': color,\n ...style\n };\n\n return (\n \n {text}\n \n {segments}\n \n \n );\n};\n\nexport default FoldText;\n" } ], diff --git a/public/r/FoldText-TS-CSS.json b/public/r/FoldText-TS-CSS.json index eed1cb0c..328eaad0 100644 --- a/public/r/FoldText-TS-CSS.json +++ b/public/r/FoldText-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "FoldText/FoldText.css", + "type": "registry:file", + "path": "FoldText.css", + "target": "@components/FoldText.css", "content": ".fold-text {\n display: inline-block;\n color: var(--fold-text-color, currentColor);\n font-size: var(--fold-text-font-size, inherit);\n font-weight: var(--fold-text-font-weight, inherit);\n line-height: 0.95;\n letter-spacing: -0.04em;\n white-space: pre-wrap;\n user-select: text;\n}\n\n.fold-text-sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n.fold-text-visual {\n display: inline;\n}\n\n.fold-text-line {\n display: block;\n}\n\n.fold-text-whitespace {\n display: inline;\n}\n\n.fold-text-segment {\n display: inline-block;\n line-height: inherit;\n perspective: var(--fold-perspective, 700px);\n transform-style: preserve-3d;\n vertical-align: baseline;\n}\n\n.fold-text-segment[data-fold-split='line'] {\n display: block;\n}\n\n.fold-text-piece {\n position: relative;\n display: inline-block;\n color: inherit;\n line-height: inherit;\n transform-style: preserve-3d;\n backface-visibility: hidden;\n will-change: transform, opacity;\n}\n\n.fold-text-piece::after {\n content: '';\n position: absolute;\n inset: -0.08em -0.02em;\n pointer-events: none;\n opacity: var(--fold-crease, 0);\n mix-blend-mode: multiply;\n border-radius: 0.08em;\n}\n\n.fold-text-piece[data-fold-hinge='top']::after {\n background: linear-gradient(180deg, rgba(0, 0, 0, 0.58) 0%, rgba(0, 0, 0, 0.22) 42%, rgba(255, 255, 255, 0.26) 100%);\n}\n\n.fold-text-piece[data-fold-hinge='bottom']::after {\n background: linear-gradient(0deg, rgba(0, 0, 0, 0.58) 0%, rgba(0, 0, 0, 0.22) 42%, rgba(255, 255, 255, 0.26) 100%);\n}\n\n.fold-text-piece[data-fold-hinge='left']::after {\n background: linear-gradient(90deg, rgba(0, 0, 0, 0.58) 0%, rgba(0, 0, 0, 0.22) 42%, rgba(255, 255, 255, 0.26) 100%);\n}\n\n.fold-text-piece[data-fold-hinge='right']::after {\n background: linear-gradient(270deg, rgba(0, 0, 0, 0.58) 0%, rgba(0, 0, 0, 0.22) 42%, rgba(255, 255, 255, 0.26) 100%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fold-text-piece {\n transform: none !important;\n }\n\n .fold-text-piece::after {\n opacity: 0 !important;\n }\n}\n" }, { "type": "registry:component", - "path": "FoldText/FoldText.tsx", + "path": "FoldText.tsx", "content": "import { useEffect, useMemo, useRef, type CSSProperties, type ReactNode } from 'react';\nimport { gsap } from 'gsap';\nimport { ScrollTrigger } from 'gsap/ScrollTrigger';\n\nimport './FoldText.css';\n\ngsap.registerPlugin(ScrollTrigger);\n\ntype SplitBy = 'char' | 'word' | 'line';\ntype Hinge = 'top' | 'bottom' | 'left' | 'right';\ntype Trigger = 'mount' | 'hover' | 'scroll' | 'loop';\n\nexport interface FoldTextProps {\n text?: string;\n splitBy?: SplitBy;\n hinge?: Hinge;\n duration?: number;\n stagger?: number;\n ease?: string;\n perspective?: number;\n creaseShading?: number;\n trigger?: Trigger;\n fontSize?: string | number;\n fontWeight?: string | number;\n color?: string;\n className?: string;\n style?: CSSProperties;\n}\n\ntype HingeConfig = {\n origin: string;\n rotateX: number;\n rotateY: number;\n};\n\nconst HINGE_CONFIG: Record = {\n top: { origin: '50% 0%', rotateX: -92, rotateY: 0 },\n bottom: { origin: '50% 100%', rotateX: 92, rotateY: 0 },\n left: { origin: '0% 50%', rotateX: 0, rotateY: 92 },\n right: { origin: '100% 50%', rotateX: 0, rotateY: -92 }\n};\n\nconst clamp = (value: number, min: number, max: number): number => Math.min(max, Math.max(min, value));\n\nconst renderWhitespace = (value: string, key: string): ReactNode[] =>\n value.split(/(\\n)/).map((part, index) => {\n if (part === '\\n') return
;\n if (!part) return null;\n\n return (\n \n {part.replace(/ /g, '\\u00A0')}\n \n );\n });\n\nconst FoldText = ({\n text = 'Design unfolds',\n splitBy = 'char',\n hinge = 'top',\n duration = 0.65,\n stagger = 0.045,\n ease = 'power3.out',\n perspective = 700,\n creaseShading = 0.55,\n trigger = 'mount',\n fontSize = 80,\n fontWeight = 800,\n color = '#f7f2e8',\n className = '',\n style = {}\n}: FoldTextProps) => {\n const rootRef = useRef(null);\n const timelineRef = useRef(null);\n const hingeConfig = HINGE_CONFIG[hinge] || HINGE_CONFIG.top;\n const safeCrease = clamp(creaseShading, 0, 1);\n const safePerspective = Math.max(120, perspective);\n\n const segments = useMemo(() => {\n let segmentIndex = 0;\n\n const renderSegment = (content: string, key: string, split: SplitBy = splitBy): ReactNode => {\n segmentIndex += 1;\n return (\n \n \n {content || '\\u00A0'}\n \n \n );\n };\n\n if (splitBy === 'line') {\n return text.split('\\n').map((line, index) => (\n \n {renderSegment(line || '\\u00A0', `segment-line-${index}`, 'line')}\n \n ));\n }\n\n if (splitBy === 'word') {\n return text.split(/(\\s+)/).flatMap((part, index) => {\n if (!part) return [];\n if (/^\\s+$/.test(part)) return renderWhitespace(part, `ws-${index}`);\n return renderSegment(part, `segment-word-${segmentIndex}`);\n });\n }\n\n return Array.from(text).map((char, index) => {\n if (char === '\\n') return
;\n return renderSegment(char === ' ' ? '\\u00A0' : char, `segment-char-${index}`);\n });\n }, [text, splitBy, hinge, hingeConfig.origin, safePerspective]);\n\n useEffect(() => {\n if (typeof window === 'undefined') return undefined;\n\n const root = rootRef.current;\n if (!root) return undefined;\n\n const pieces = Array.from(root.querySelectorAll('.fold-text-piece'));\n if (!pieces.length) return undefined;\n\n const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;\n const activeDuration = reduceMotion ? Math.min(duration, 0.22) : duration;\n const activeStagger = reduceMotion ? Math.min(stagger, 0.02) : stagger;\n const fromVars = {\n opacity: 0,\n rotateX: reduceMotion ? 0 : hingeConfig.rotateX,\n rotateY: reduceMotion ? 0 : hingeConfig.rotateY,\n '--fold-crease': reduceMotion ? 0 : safeCrease,\n transformOrigin: hingeConfig.origin,\n force3D: true\n };\n const toVars = {\n opacity: 1,\n rotateX: 0,\n rotateY: 0,\n '--fold-crease': 0,\n duration: activeDuration,\n ease: reduceMotion ? 'power1.out' : ease,\n stagger: activeStagger,\n clearProps: 'willChange'\n };\n\n const killTimeline = () => {\n timelineRef.current?.kill();\n timelineRef.current = null;\n gsap.killTweensOf(pieces);\n };\n\n const play = (repeat: boolean): gsap.core.Timeline => {\n killTimeline();\n timelineRef.current = gsap.timeline({ repeat: repeat ? -1 : 0, repeatDelay: repeat ? 0.75 : 0 });\n timelineRef.current.fromTo(pieces, fromVars, toVars);\n return timelineRef.current;\n };\n\n let scrollTrigger: ReturnType | undefined;\n let hoverHandler: (() => void) | undefined;\n\n if (trigger === 'hover') {\n gsap.set(pieces, { opacity: 1, rotateX: 0, rotateY: 0, '--fold-crease': 0, transformOrigin: hingeConfig.origin });\n hoverHandler = () => play(false);\n root.addEventListener('mouseenter', hoverHandler);\n } else if (trigger === 'scroll') {\n gsap.set(pieces, fromVars);\n scrollTrigger = ScrollTrigger.create({\n trigger: root,\n start: 'top 82%',\n once: true,\n onEnter: () => play(false)\n });\n } else if (trigger === 'loop') {\n play(true);\n } else {\n play(false);\n }\n\n return () => {\n if (hoverHandler) root.removeEventListener('mouseenter', hoverHandler);\n scrollTrigger?.kill();\n killTimeline();\n };\n }, [\n text,\n splitBy,\n hinge,\n duration,\n stagger,\n ease,\n perspective,\n safeCrease,\n trigger,\n hingeConfig.origin,\n hingeConfig.rotateX,\n hingeConfig.rotateY\n ]);\n\n const rootStyle: CSSProperties = {\n '--fold-text-font-size': typeof fontSize === 'number' ? `${fontSize}px` : fontSize,\n '--fold-text-font-weight': fontWeight,\n '--fold-text-color': color,\n ...style\n } as CSSProperties;\n\n return (\n \n {text}\n \n {segments}\n \n \n );\n};\n\nexport default FoldText;\n" } ], diff --git a/public/r/Folder-JS-CSS.json b/public/r/Folder-JS-CSS.json index 19199101..41a576f8 100644 --- a/public/r/Folder-JS-CSS.json +++ b/public/r/Folder-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Folder/Folder.css", + "type": "registry:file", + "path": "Folder.css", + "target": "@components/Folder.css", "content": ":root {\n --folder-color: #70a1ff;\n --folder-back-color: #4785ff;\n --paper-1: #e6e6e6;\n --paper-2: #f2f2f2;\n --paper-3: #ffffff;\n}\n\n.folder {\n transition: all 0.2s ease-in;\n cursor: pointer;\n}\n\n.folder:not(.folder--click):hover {\n transform: translateY(-8px);\n}\n\n.folder:not(.folder--click):hover .paper {\n transform: translate(-50%, 0%);\n}\n\n.folder:not(.folder--click):hover .folder__front {\n transform: skew(15deg) scaleY(0.6);\n}\n\n.folder:not(.folder--click):hover .right {\n transform: skew(-15deg) scaleY(0.6);\n}\n\n.folder.open {\n transform: translateY(-8px);\n}\n\n.folder.open .paper:nth-child(1) {\n transform: translate(-120%, -70%) rotateZ(-15deg);\n}\n\n.folder.open .paper:nth-child(1):hover {\n transform: translate(-120%, -70%) rotateZ(-15deg) scale(1.1);\n}\n\n.folder.open .paper:nth-child(2) {\n transform: translate(10%, -70%) rotateZ(15deg);\n height: 80%;\n}\n\n.folder.open .paper:nth-child(2):hover {\n transform: translate(10%, -70%) rotateZ(15deg) scale(1.1);\n}\n\n.folder.open .paper:nth-child(3) {\n transform: translate(-50%, -100%) rotateZ(5deg);\n height: 80%;\n}\n\n.folder.open .paper:nth-child(3):hover {\n transform: translate(-50%, -100%) rotateZ(5deg) scale(1.1);\n}\n\n.folder.open .folder__front {\n transform: skew(15deg) scaleY(0.6);\n}\n\n.folder.open .right {\n transform: skew(-15deg) scaleY(0.6);\n}\n\n.folder__back {\n position: relative;\n width: 100px;\n height: 80px;\n background: var(--folder-back-color);\n border-radius: 0px 10px 10px 10px;\n}\n\n.folder__back::after {\n position: absolute;\n z-index: 0;\n bottom: 98%;\n left: 0;\n content: '';\n width: 30px;\n height: 10px;\n background: var(--folder-back-color);\n border-radius: 5px 5px 0 0;\n}\n\n.paper {\n position: absolute;\n z-index: 2;\n bottom: 10%;\n left: 50%;\n transform: translate(-50%, 10%);\n width: 70%;\n height: 80%;\n background: var(--paper-1);\n border-radius: 10px;\n transition: all 0.3s ease-in-out;\n}\n\n.paper:nth-child(2) {\n background: var(--paper-2);\n width: 80%;\n height: 70%;\n}\n\n.paper:nth-child(3) {\n background: var(--paper-3);\n width: 90%;\n height: 60%;\n}\n\n.folder__front {\n position: absolute;\n z-index: 3;\n width: 100%;\n height: 100%;\n background: var(--folder-color);\n border-radius: 5px 10px 10px 10px;\n transform-origin: bottom;\n transition: all 0.3s ease-in-out;\n}\n\n.folder:focus-visible {\n outline: 2px solid #ffffff;\n outline-offset: 4px;\n border-radius: 10px;\n}\n" }, { "type": "registry:component", - "path": "Folder/Folder.jsx", + "path": "Folder.jsx", "content": "import { useState } from 'react';\nimport './Folder.css';\n\nconst darkenColor = (hex, percent) => {\n let color = hex.startsWith('#') ? hex.slice(1) : hex;\n if (color.length === 3) {\n color = color\n .split('')\n .map(c => c + c)\n .join('');\n }\n const num = parseInt(color.slice(0, 6), 16);\n let r = (num >> 16) & 0xff;\n let g = (num >> 8) & 0xff;\n let b = num & 0xff;\n r = Math.max(0, Math.min(255, Math.floor(r * (1 - percent))));\n g = Math.max(0, Math.min(255, Math.floor(g * (1 - percent))));\n b = Math.max(0, Math.min(255, Math.floor(b * (1 - percent))));\n return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();\n};\n\nconst Folder = ({ color = '#5227FF', size = 1, items = [], className = '' }) => {\n const maxItems = 3;\n const papers = items.slice(0, maxItems);\n while (papers.length < maxItems) {\n papers.push(null);\n }\n\n const [open, setOpen] = useState(false);\n const [paperOffsets, setPaperOffsets] = useState(Array.from({ length: maxItems }, () => ({ x: 0, y: 0 })));\n\n const folderBackColor = darkenColor(color, 0.08);\n const paper1 = darkenColor('#ffffff', 0.1);\n const paper2 = darkenColor('#ffffff', 0.05);\n const paper3 = '#ffffff';\n\n const handleClick = () => {\n setOpen(prev => !prev);\n if (open) {\n setPaperOffsets(Array.from({ length: maxItems }, () => ({ x: 0, y: 0 })));\n }\n };\n\n const handlePaperMouseMove = (e, index) => {\n if (!open) return;\n const rect = e.currentTarget.getBoundingClientRect();\n const centerX = rect.left + rect.width / 2;\n const centerY = rect.top + rect.height / 2;\n const offsetX = (e.clientX - centerX) * 0.15;\n const offsetY = (e.clientY - centerY) * 0.15;\n setPaperOffsets(prev => {\n const newOffsets = [...prev];\n newOffsets[index] = { x: offsetX, y: offsetY };\n return newOffsets;\n });\n };\n\n const handlePaperMouseLeave = (e, index) => {\n setPaperOffsets(prev => {\n const newOffsets = [...prev];\n newOffsets[index] = { x: 0, y: 0 };\n return newOffsets;\n });\n };\n\n const folderStyle = {\n '--folder-color': color,\n '--folder-back-color': folderBackColor,\n '--paper-1': paper1,\n '--paper-2': paper2,\n '--paper-3': paper3\n };\n\n const folderClassName = `folder ${open ? 'open' : ''}`.trim();\n const scaleStyle = { transform: `scale(${size})` };\n\n return (\n
\n {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n\n handleClick();\n }\n }}\n tabIndex={0}\n role=\"button\"\n aria-expanded={open}\n aria-label={open ? 'Close folder' : 'Open folder'}\n >\n
\n {papers.map((item, i) => (\n handlePaperMouseMove(e, i)}\n onMouseLeave={e => handlePaperMouseLeave(e, i)}\n style={\n open\n ? {\n '--magnet-x': `${paperOffsets[i]?.x || 0}px`,\n '--magnet-y': `${paperOffsets[i]?.y || 0}px`\n }\n : {}\n }\n >\n {item}\n
\n ))}\n
\n
\n
\n
\n
\n );\n};\n\nexport default Folder;\n" } ], diff --git a/public/r/Folder-TS-CSS.json b/public/r/Folder-TS-CSS.json index 8a95c4e8..fb7e0b82 100644 --- a/public/r/Folder-TS-CSS.json +++ b/public/r/Folder-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Folder/Folder.css", + "type": "registry:file", + "path": "Folder.css", + "target": "@components/Folder.css", "content": ":root {\n --folder-color: #70a1ff;\n --folder-back-color: #4785ff;\n --paper-1: #e6e6e6;\n --paper-2: #f2f2f2;\n --paper-3: #ffffff;\n}\n\n.folder {\n transition: all 0.2s ease-in;\n cursor: pointer;\n}\n\n.folder:not(.folder--click):hover {\n transform: translateY(-8px);\n}\n\n.folder:not(.folder--click):hover .paper {\n transform: translate(-50%, 0%);\n}\n\n.folder:not(.folder--click):hover .folder__front {\n transform: skew(15deg) scaleY(0.6);\n}\n\n.folder:not(.folder--click):hover .right {\n transform: skew(-15deg) scaleY(0.6);\n}\n\n.folder.open {\n transform: translateY(-8px);\n}\n\n.folder.open .paper:nth-child(1) {\n transform: translate(-120%, -70%) rotateZ(-15deg);\n}\n\n.folder.open .paper:nth-child(1):hover {\n transform: translate(-120%, -70%) rotateZ(-15deg) scale(1.1);\n}\n\n.folder.open .paper:nth-child(2) {\n transform: translate(10%, -70%) rotateZ(15deg);\n height: 80%;\n}\n\n.folder.open .paper:nth-child(2):hover {\n transform: translate(10%, -70%) rotateZ(15deg) scale(1.1);\n}\n\n.folder.open .paper:nth-child(3) {\n transform: translate(-50%, -100%) rotateZ(5deg);\n height: 80%;\n}\n\n.folder.open .paper:nth-child(3):hover {\n transform: translate(-50%, -100%) rotateZ(5deg) scale(1.1);\n}\n\n.folder.open .folder__front {\n transform: skew(15deg) scaleY(0.6);\n}\n\n.folder.open .right {\n transform: skew(-15deg) scaleY(0.6);\n}\n\n.folder__back {\n position: relative;\n width: 100px;\n height: 80px;\n background: var(--folder-back-color);\n border-radius: 0px 10px 10px 10px;\n}\n\n.folder__back::after {\n position: absolute;\n z-index: 0;\n bottom: 98%;\n left: 0;\n content: '';\n width: 30px;\n height: 10px;\n background: var(--folder-back-color);\n border-radius: 5px 5px 0 0;\n}\n\n.paper {\n position: absolute;\n z-index: 2;\n bottom: 10%;\n left: 50%;\n transform: translate(-50%, 10%);\n width: 70%;\n height: 80%;\n background: var(--paper-1);\n border-radius: 10px;\n transition: all 0.3s ease-in-out;\n}\n\n.paper:nth-child(2) {\n background: var(--paper-2);\n width: 80%;\n height: 70%;\n}\n\n.paper:nth-child(3) {\n background: var(--paper-3);\n width: 90%;\n height: 60%;\n}\n\n.folder__front {\n position: absolute;\n z-index: 3;\n width: 100%;\n height: 100%;\n background: var(--folder-color);\n border-radius: 5px 10px 10px 10px;\n transform-origin: bottom;\n transition: all 0.3s ease-in-out;\n}\n\n.folder:focus-visible {\n outline: 2px solid #ffffff;\n outline-offset: 4px;\n border-radius: 10px;\n}\n" }, { "type": "registry:component", - "path": "Folder/Folder.tsx", + "path": "Folder.tsx", "content": "import React, { useState } from 'react';\nimport './Folder.css';\n\ninterface FolderProps {\n color?: string;\n size?: number;\n items?: React.ReactNode[];\n className?: string;\n}\n\nconst darkenColor = (hex: string, percent: number): string => {\n let color = hex.startsWith('#') ? hex.slice(1) : hex;\n if (color.length === 3) {\n color = color\n .split('')\n .map(c => c + c)\n .join('');\n }\n const num = parseInt(color.slice(0, 6), 16);\n let r = (num >> 16) & 0xff;\n let g = (num >> 8) & 0xff;\n let b = num & 0xff;\n r = Math.max(0, Math.min(255, Math.floor(r * (1 - percent))));\n g = Math.max(0, Math.min(255, Math.floor(g * (1 - percent))));\n b = Math.max(0, Math.min(255, Math.floor(b * (1 - percent))));\n return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();\n};\n\nconst Folder: React.FC = ({ color = '#5227FF', size = 1, items = [], className = '' }) => {\n const maxItems = 3;\n const papers = items.slice(0, maxItems);\n while (papers.length < maxItems) {\n papers.push(null);\n }\n\n const [open, setOpen] = useState(false);\n const [paperOffsets, setPaperOffsets] = useState<{ x: number; y: number }[]>(\n Array.from({ length: maxItems }, () => ({ x: 0, y: 0 }))\n );\n\n const folderBackColor = darkenColor(color, 0.08);\n const paper1 = darkenColor('#ffffff', 0.1);\n const paper2 = darkenColor('#ffffff', 0.05);\n const paper3 = '#ffffff';\n\n const handleClick = () => {\n setOpen(prev => !prev);\n if (open) {\n setPaperOffsets(Array.from({ length: maxItems }, () => ({ x: 0, y: 0 })));\n }\n };\n\n const handlePaperMouseMove = (e: React.MouseEvent, index: number) => {\n if (!open) return;\n const rect = e.currentTarget.getBoundingClientRect();\n const centerX = rect.left + rect.width / 2;\n const centerY = rect.top + rect.height / 2;\n const offsetX = (e.clientX - centerX) * 0.15;\n const offsetY = (e.clientY - centerY) * 0.15;\n setPaperOffsets(prev => {\n const newOffsets = [...prev];\n newOffsets[index] = { x: offsetX, y: offsetY };\n return newOffsets;\n });\n };\n\n const handlePaperMouseLeave = (e: React.MouseEvent, index: number) => {\n setPaperOffsets(prev => {\n const newOffsets = [...prev];\n newOffsets[index] = { x: 0, y: 0 };\n return newOffsets;\n });\n };\n\n const folderStyle: React.CSSProperties = {\n '--folder-color': color,\n '--folder-back-color': folderBackColor,\n '--paper-1': paper1,\n '--paper-2': paper2,\n '--paper-3': paper3\n } as React.CSSProperties;\n\n const folderClassName = `folder ${open ? 'open' : ''}`.trim();\n const scaleStyle = { transform: `scale(${size})` };\n\n return (\n
\n {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n handleClick();\n }\n }}\n tabIndex={0}\n role=\"button\"\n aria-expanded={open}\n aria-label={open ? 'Close folder' : 'Open folder'}\n >\n
\n {papers.map((item, i) => (\n handlePaperMouseMove(e, i)}\n onMouseLeave={e => handlePaperMouseLeave(e, i)}\n style={\n open\n ? ({\n '--magnet-x': `${paperOffsets[i]?.x || 0}px`,\n '--magnet-y': `${paperOffsets[i]?.y || 0}px`\n } as React.CSSProperties)\n : {}\n }\n >\n {item}\n
\n ))}\n
\n
\n
\n
\n
\n );\n};\n\nexport default Folder;\n" } ], diff --git a/public/r/Galaxy-JS-CSS.json b/public/r/Galaxy-JS-CSS.json index b242b474..5813a1df 100644 --- a/public/r/Galaxy-JS-CSS.json +++ b/public/r/Galaxy-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Galaxy/Galaxy.css", + "type": "registry:file", + "path": "Galaxy.css", + "target": "@components/Galaxy.css", "content": ".galaxy-container {\n width: 100%;\n height: 100%;\n position: relative;\n}\n" }, { "type": "registry:component", - "path": "Galaxy/Galaxy.jsx", + "path": "Galaxy.jsx", "content": "import { Renderer, Program, Mesh, Color, Triangle } from 'ogl';\nimport { useEffect, useRef } from 'react';\nimport './Galaxy.css';\n\nconst vertexShader = `\nattribute vec2 uv;\nattribute vec2 position;\n\nvarying vec2 vUv;\n\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0, 1);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float uTime;\nuniform vec3 uResolution;\nuniform vec2 uFocal;\nuniform vec2 uRotation;\nuniform float uStarSpeed;\nuniform float uDensity;\nuniform float uHueShift;\nuniform float uSpeed;\nuniform vec2 uMouse;\nuniform float uGlowIntensity;\nuniform float uSaturation;\nuniform bool uMouseRepulsion;\nuniform float uTwinkleIntensity;\nuniform float uRotationSpeed;\nuniform float uRepulsionStrength;\nuniform float uMouseActiveFactor;\nuniform float uAutoCenterRepulsion;\nuniform bool uTransparent;\n\nvarying vec2 vUv;\n\n#define NUM_LAYER 4.0\n#define STAR_COLOR_CUTOFF 0.2\n#define MAT45 mat2(0.7071, -0.7071, 0.7071, 0.7071)\n#define PERIOD 3.0\n\nfloat Hash21(vec2 p) {\n p = fract(p * vec2(123.34, 456.21));\n p += dot(p, p + 45.32);\n return fract(p.x * p.y);\n}\n\nfloat tri(float x) {\n return abs(fract(x) * 2.0 - 1.0);\n}\n\nfloat tris(float x) {\n float t = fract(x);\n return 1.0 - smoothstep(0.0, 1.0, abs(2.0 * t - 1.0));\n}\n\nfloat trisn(float x) {\n float t = fract(x);\n return 2.0 * (1.0 - smoothstep(0.0, 1.0, abs(2.0 * t - 1.0))) - 1.0;\n}\n\nvec3 hsv2rgb(vec3 c) {\n vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);\n vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);\n return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);\n}\n\nfloat Star(vec2 uv, float flare) {\n float d = length(uv);\n float m = (0.05 * uGlowIntensity) / d;\n float rays = smoothstep(0.0, 1.0, 1.0 - abs(uv.x * uv.y * 1000.0));\n m += rays * flare * uGlowIntensity;\n uv *= MAT45;\n rays = smoothstep(0.0, 1.0, 1.0 - abs(uv.x * uv.y * 1000.0));\n m += rays * 0.3 * flare * uGlowIntensity;\n m *= smoothstep(1.0, 0.2, d);\n return m;\n}\n\nvec3 StarLayer(vec2 uv) {\n vec3 col = vec3(0.0);\n\n vec2 gv = fract(uv) - 0.5; \n vec2 id = floor(uv);\n\n for (int y = -1; y <= 1; y++) {\n for (int x = -1; x <= 1; x++) {\n vec2 offset = vec2(float(x), float(y));\n vec2 si = id + vec2(float(x), float(y));\n float seed = Hash21(si);\n float size = fract(seed * 345.32);\n float glossLocal = tri(uStarSpeed / (PERIOD * seed + 1.0));\n float flareSize = smoothstep(0.9, 1.0, size) * glossLocal;\n\n float red = smoothstep(STAR_COLOR_CUTOFF, 1.0, Hash21(si + 1.0)) + STAR_COLOR_CUTOFF;\n float blu = smoothstep(STAR_COLOR_CUTOFF, 1.0, Hash21(si + 3.0)) + STAR_COLOR_CUTOFF;\n float grn = min(red, blu) * seed;\n vec3 base = vec3(red, grn, blu);\n \n float hue = atan(base.g - base.r, base.b - base.r) / (2.0 * 3.14159) + 0.5;\n hue = fract(hue + uHueShift / 360.0);\n float sat = length(base - vec3(dot(base, vec3(0.299, 0.587, 0.114)))) * uSaturation;\n float val = max(max(base.r, base.g), base.b);\n base = hsv2rgb(vec3(hue, sat, val));\n\n vec2 pad = vec2(tris(seed * 34.0 + uTime * uSpeed / 10.0), tris(seed * 38.0 + uTime * uSpeed / 30.0)) - 0.5;\n\n float star = Star(gv - offset - pad, flareSize);\n vec3 color = base;\n\n float twinkle = trisn(uTime * uSpeed + seed * 6.2831) * 0.5 + 1.0;\n twinkle = mix(1.0, twinkle, uTwinkleIntensity);\n star *= twinkle;\n \n col += star * size * color;\n }\n }\n\n return col;\n}\n\nvoid main() {\n vec2 focalPx = uFocal * uResolution.xy;\n vec2 uv = (vUv * uResolution.xy - focalPx) / uResolution.y;\n\n vec2 mouseNorm = uMouse - vec2(0.5);\n \n if (uAutoCenterRepulsion > 0.0) {\n vec2 centerUV = vec2(0.0, 0.0);\n float centerDist = length(uv - centerUV);\n vec2 repulsion = normalize(uv - centerUV) * (uAutoCenterRepulsion / (centerDist + 0.1));\n uv += repulsion * 0.05;\n } else if (uMouseRepulsion) {\n vec2 mousePosUV = (uMouse * uResolution.xy - focalPx) / uResolution.y;\n float mouseDist = length(uv - mousePosUV);\n vec2 repulsion = normalize(uv - mousePosUV) * (uRepulsionStrength / (mouseDist + 0.1));\n uv += repulsion * 0.05 * uMouseActiveFactor;\n } else {\n vec2 mouseOffset = mouseNorm * 0.1 * uMouseActiveFactor;\n uv += mouseOffset;\n }\n\n float autoRotAngle = uTime * uRotationSpeed;\n mat2 autoRot = mat2(cos(autoRotAngle), -sin(autoRotAngle), sin(autoRotAngle), cos(autoRotAngle));\n uv = autoRot * uv;\n\n uv = mat2(uRotation.x, -uRotation.y, uRotation.y, uRotation.x) * uv;\n\n vec3 col = vec3(0.0);\n\n for (float i = 0.0; i < 1.0; i += 1.0 / NUM_LAYER) {\n float depth = fract(i + uStarSpeed * uSpeed);\n float scale = mix(20.0 * uDensity, 0.5 * uDensity, depth);\n float fade = depth * smoothstep(1.0, 0.9, depth);\n col += StarLayer(uv * scale + i * 453.32) * fade;\n }\n\n if (uTransparent) {\n float alpha = length(col);\n alpha = smoothstep(0.0, 0.3, alpha);\n alpha = min(alpha, 1.0);\n gl_FragColor = vec4(col, alpha);\n } else {\n gl_FragColor = vec4(col, 1.0);\n }\n}\n`;\n\nexport default function Galaxy({\n focal = [0.5, 0.5],\n rotation = [1.0, 0.0],\n starSpeed = 0.5,\n density = 1,\n hueShift = 140,\n disableAnimation = false,\n speed = 1.0,\n mouseInteraction = true,\n glowIntensity = 0.3,\n saturation = 0.0,\n mouseRepulsion = true,\n repulsionStrength = 2,\n twinkleIntensity = 0.3,\n rotationSpeed = 0.1,\n autoCenterRepulsion = 0,\n transparent = true,\n ...rest\n}) {\n const ctnDom = useRef(null);\n const targetMousePos = useRef({ x: 0.5, y: 0.5 });\n const smoothMousePos = useRef({ x: 0.5, y: 0.5 });\n const targetMouseActive = useRef(0.0);\n const smoothMouseActive = useRef(0.0);\n\n useEffect(() => {\n if (!ctnDom.current) return;\n const ctn = ctnDom.current;\n const renderer = new Renderer({\n alpha: transparent,\n premultipliedAlpha: false\n });\n const gl = renderer.gl;\n\n if (transparent) {\n gl.enable(gl.BLEND);\n gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\n gl.clearColor(0, 0, 0, 0);\n } else {\n gl.clearColor(0, 0, 0, 1);\n }\n\n let program;\n\n function resize() {\n const scale = 1;\n renderer.setSize(ctn.offsetWidth * scale, ctn.offsetHeight * scale);\n if (program) {\n program.uniforms.uResolution.value = new Color(\n gl.canvas.width,\n gl.canvas.height,\n gl.canvas.width / gl.canvas.height\n );\n }\n }\n window.addEventListener('resize', resize, false);\n resize();\n\n const geometry = new Triangle(gl);\n program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uResolution: {\n value: new Color(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height)\n },\n uFocal: { value: new Float32Array(focal) },\n uRotation: { value: new Float32Array(rotation) },\n uStarSpeed: { value: starSpeed },\n uDensity: { value: density },\n uHueShift: { value: hueShift },\n uSpeed: { value: speed },\n uMouse: {\n value: new Float32Array([smoothMousePos.current.x, smoothMousePos.current.y])\n },\n uGlowIntensity: { value: glowIntensity },\n uSaturation: { value: saturation },\n uMouseRepulsion: { value: mouseRepulsion },\n uTwinkleIntensity: { value: twinkleIntensity },\n uRotationSpeed: { value: rotationSpeed },\n uRepulsionStrength: { value: repulsionStrength },\n uMouseActiveFactor: { value: 0.0 },\n uAutoCenterRepulsion: { value: autoCenterRepulsion },\n uTransparent: { value: transparent }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n let animateId;\n\n function update(t) {\n animateId = requestAnimationFrame(update);\n if (!disableAnimation) {\n program.uniforms.uTime.value = t * 0.001;\n program.uniforms.uStarSpeed.value = (t * 0.001 * starSpeed) / 10.0;\n }\n\n const lerpFactor = 0.05;\n smoothMousePos.current.x += (targetMousePos.current.x - smoothMousePos.current.x) * lerpFactor;\n smoothMousePos.current.y += (targetMousePos.current.y - smoothMousePos.current.y) * lerpFactor;\n\n smoothMouseActive.current += (targetMouseActive.current - smoothMouseActive.current) * lerpFactor;\n\n program.uniforms.uMouse.value[0] = smoothMousePos.current.x;\n program.uniforms.uMouse.value[1] = smoothMousePos.current.y;\n program.uniforms.uMouseActiveFactor.value = smoothMouseActive.current;\n\n renderer.render({ scene: mesh });\n }\n animateId = requestAnimationFrame(update);\n ctn.appendChild(gl.canvas);\n\n function handleMouseMove(e) {\n const rect = ctn.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1.0 - (e.clientY - rect.top) / rect.height;\n targetMousePos.current = { x, y };\n targetMouseActive.current = 1.0;\n }\n\n function handleMouseLeave() {\n targetMouseActive.current = 0.0;\n }\n\n if (mouseInteraction) {\n ctn.addEventListener('mousemove', handleMouseMove);\n ctn.addEventListener('mouseleave', handleMouseLeave);\n }\n\n return () => {\n cancelAnimationFrame(animateId);\n window.removeEventListener('resize', resize);\n if (mouseInteraction) {\n ctn.removeEventListener('mousemove', handleMouseMove);\n ctn.removeEventListener('mouseleave', handleMouseLeave);\n }\n ctn.removeChild(gl.canvas);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, [\n focal,\n rotation,\n starSpeed,\n density,\n hueShift,\n disableAnimation,\n speed,\n mouseInteraction,\n glowIntensity,\n saturation,\n mouseRepulsion,\n twinkleIntensity,\n rotationSpeed,\n repulsionStrength,\n autoCenterRepulsion,\n transparent\n ]);\n\n return
;\n}\n" } ], diff --git a/public/r/Galaxy-TS-CSS.json b/public/r/Galaxy-TS-CSS.json index 24f822d6..97135d19 100644 --- a/public/r/Galaxy-TS-CSS.json +++ b/public/r/Galaxy-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Galaxy/Galaxy.css", + "type": "registry:file", + "path": "Galaxy.css", + "target": "@components/Galaxy.css", "content": ".galaxy-container {\n width: 100%;\n height: 100%;\n position: relative;\n}\n" }, { "type": "registry:component", - "path": "Galaxy/Galaxy.tsx", + "path": "Galaxy.tsx", "content": "import { Renderer, Program, Mesh, Color, Triangle } from 'ogl';\nimport { useEffect, useRef } from 'react';\nimport './Galaxy.css';\n\nconst vertexShader = `\nattribute vec2 uv;\nattribute vec2 position;\n\nvarying vec2 vUv;\n\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0, 1);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float uTime;\nuniform vec3 uResolution;\nuniform vec2 uFocal;\nuniform vec2 uRotation;\nuniform float uStarSpeed;\nuniform float uDensity;\nuniform float uHueShift;\nuniform float uSpeed;\nuniform vec2 uMouse;\nuniform float uGlowIntensity;\nuniform float uSaturation;\nuniform bool uMouseRepulsion;\nuniform float uTwinkleIntensity;\nuniform float uRotationSpeed;\nuniform float uRepulsionStrength;\nuniform float uMouseActiveFactor;\nuniform float uAutoCenterRepulsion;\nuniform bool uTransparent;\n\nvarying vec2 vUv;\n\n#define NUM_LAYER 4.0\n#define STAR_COLOR_CUTOFF 0.2\n#define MAT45 mat2(0.7071, -0.7071, 0.7071, 0.7071)\n#define PERIOD 3.0\n\nfloat Hash21(vec2 p) {\n p = fract(p * vec2(123.34, 456.21));\n p += dot(p, p + 45.32);\n return fract(p.x * p.y);\n}\n\nfloat tri(float x) {\n return abs(fract(x) * 2.0 - 1.0);\n}\n\nfloat tris(float x) {\n float t = fract(x);\n return 1.0 - smoothstep(0.0, 1.0, abs(2.0 * t - 1.0));\n}\n\nfloat trisn(float x) {\n float t = fract(x);\n return 2.0 * (1.0 - smoothstep(0.0, 1.0, abs(2.0 * t - 1.0))) - 1.0;\n}\n\nvec3 hsv2rgb(vec3 c) {\n vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);\n vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);\n return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);\n}\n\nfloat Star(vec2 uv, float flare) {\n float d = length(uv);\n float m = (0.05 * uGlowIntensity) / d;\n float rays = smoothstep(0.0, 1.0, 1.0 - abs(uv.x * uv.y * 1000.0));\n m += rays * flare * uGlowIntensity;\n uv *= MAT45;\n rays = smoothstep(0.0, 1.0, 1.0 - abs(uv.x * uv.y * 1000.0));\n m += rays * 0.3 * flare * uGlowIntensity;\n m *= smoothstep(1.0, 0.2, d);\n return m;\n}\n\nvec3 StarLayer(vec2 uv) {\n vec3 col = vec3(0.0);\n\n vec2 gv = fract(uv) - 0.5; \n vec2 id = floor(uv);\n\n for (int y = -1; y <= 1; y++) {\n for (int x = -1; x <= 1; x++) {\n vec2 offset = vec2(float(x), float(y));\n vec2 si = id + vec2(float(x), float(y));\n float seed = Hash21(si);\n float size = fract(seed * 345.32);\n float glossLocal = tri(uStarSpeed / (PERIOD * seed + 1.0));\n float flareSize = smoothstep(0.9, 1.0, size) * glossLocal;\n\n float red = smoothstep(STAR_COLOR_CUTOFF, 1.0, Hash21(si + 1.0)) + STAR_COLOR_CUTOFF;\n float blu = smoothstep(STAR_COLOR_CUTOFF, 1.0, Hash21(si + 3.0)) + STAR_COLOR_CUTOFF;\n float grn = min(red, blu) * seed;\n vec3 base = vec3(red, grn, blu);\n \n float hue = atan(base.g - base.r, base.b - base.r) / (2.0 * 3.14159) + 0.5;\n hue = fract(hue + uHueShift / 360.0);\n float sat = length(base - vec3(dot(base, vec3(0.299, 0.587, 0.114)))) * uSaturation;\n float val = max(max(base.r, base.g), base.b);\n base = hsv2rgb(vec3(hue, sat, val));\n\n vec2 pad = vec2(tris(seed * 34.0 + uTime * uSpeed / 10.0), tris(seed * 38.0 + uTime * uSpeed / 30.0)) - 0.5;\n\n float star = Star(gv - offset - pad, flareSize);\n vec3 color = base;\n\n float twinkle = trisn(uTime * uSpeed + seed * 6.2831) * 0.5 + 1.0;\n twinkle = mix(1.0, twinkle, uTwinkleIntensity);\n star *= twinkle;\n \n col += star * size * color;\n }\n }\n\n return col;\n}\n\nvoid main() {\n vec2 focalPx = uFocal * uResolution.xy;\n vec2 uv = (vUv * uResolution.xy - focalPx) / uResolution.y;\n\n vec2 mouseNorm = uMouse - vec2(0.5);\n \n if (uAutoCenterRepulsion > 0.0) {\n vec2 centerUV = vec2(0.0, 0.0);\n float centerDist = length(uv - centerUV);\n vec2 repulsion = normalize(uv - centerUV) * (uAutoCenterRepulsion / (centerDist + 0.1));\n uv += repulsion * 0.05;\n } else if (uMouseRepulsion) {\n vec2 mousePosUV = (uMouse * uResolution.xy - focalPx) / uResolution.y;\n float mouseDist = length(uv - mousePosUV);\n vec2 repulsion = normalize(uv - mousePosUV) * (uRepulsionStrength / (mouseDist + 0.1));\n uv += repulsion * 0.05 * uMouseActiveFactor;\n } else {\n vec2 mouseOffset = mouseNorm * 0.1 * uMouseActiveFactor;\n uv += mouseOffset;\n }\n\n float autoRotAngle = uTime * uRotationSpeed;\n mat2 autoRot = mat2(cos(autoRotAngle), -sin(autoRotAngle), sin(autoRotAngle), cos(autoRotAngle));\n uv = autoRot * uv;\n\n uv = mat2(uRotation.x, -uRotation.y, uRotation.y, uRotation.x) * uv;\n\n vec3 col = vec3(0.0);\n\n for (float i = 0.0; i < 1.0; i += 1.0 / NUM_LAYER) {\n float depth = fract(i + uStarSpeed * uSpeed);\n float scale = mix(20.0 * uDensity, 0.5 * uDensity, depth);\n float fade = depth * smoothstep(1.0, 0.9, depth);\n col += StarLayer(uv * scale + i * 453.32) * fade;\n }\n\n if (uTransparent) {\n float alpha = length(col);\n alpha = smoothstep(0.0, 0.3, alpha);\n alpha = min(alpha, 1.0);\n gl_FragColor = vec4(col, alpha);\n } else {\n gl_FragColor = vec4(col, 1.0);\n }\n}\n`;\n\ninterface GalaxyProps {\n focal?: [number, number];\n rotation?: [number, number];\n starSpeed?: number;\n density?: number;\n hueShift?: number;\n disableAnimation?: boolean;\n speed?: number;\n mouseInteraction?: boolean;\n glowIntensity?: number;\n saturation?: number;\n mouseRepulsion?: boolean;\n twinkleIntensity?: number;\n rotationSpeed?: number;\n repulsionStrength?: number;\n autoCenterRepulsion?: number;\n transparent?: boolean;\n}\n\nexport default function Galaxy({\n focal = [0.5, 0.5],\n rotation = [1.0, 0.0],\n starSpeed = 0.5,\n density = 1,\n hueShift = 140,\n disableAnimation = false,\n speed = 1.0,\n mouseInteraction = true,\n glowIntensity = 0.3,\n saturation = 0.0,\n mouseRepulsion = true,\n repulsionStrength = 2,\n twinkleIntensity = 0.3,\n rotationSpeed = 0.1,\n autoCenterRepulsion = 0,\n transparent = true,\n ...rest\n}: GalaxyProps) {\n const ctnDom = useRef(null);\n const targetMousePos = useRef({ x: 0.5, y: 0.5 });\n const smoothMousePos = useRef({ x: 0.5, y: 0.5 });\n const targetMouseActive = useRef(0.0);\n const smoothMouseActive = useRef(0.0);\n\n useEffect(() => {\n if (!ctnDom.current) return;\n const ctn = ctnDom.current;\n const renderer = new Renderer({\n alpha: transparent,\n premultipliedAlpha: false\n });\n const gl = renderer.gl;\n\n if (transparent) {\n gl.enable(gl.BLEND);\n gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\n gl.clearColor(0, 0, 0, 0);\n } else {\n gl.clearColor(0, 0, 0, 1);\n }\n\n let program: Program;\n\n function resize() {\n const scale = 1;\n renderer.setSize(ctn.offsetWidth * scale, ctn.offsetHeight * scale);\n if (program) {\n program.uniforms.uResolution.value = new Color(\n gl.canvas.width,\n gl.canvas.height,\n gl.canvas.width / gl.canvas.height\n );\n }\n }\n window.addEventListener('resize', resize, false);\n resize();\n\n const geometry = new Triangle(gl);\n program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uResolution: {\n value: new Color(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height)\n },\n uFocal: { value: new Float32Array(focal) },\n uRotation: { value: new Float32Array(rotation) },\n uStarSpeed: { value: starSpeed },\n uDensity: { value: density },\n uHueShift: { value: hueShift },\n uSpeed: { value: speed },\n uMouse: {\n value: new Float32Array([smoothMousePos.current.x, smoothMousePos.current.y])\n },\n uGlowIntensity: { value: glowIntensity },\n uSaturation: { value: saturation },\n uMouseRepulsion: { value: mouseRepulsion },\n uTwinkleIntensity: { value: twinkleIntensity },\n uRotationSpeed: { value: rotationSpeed },\n uRepulsionStrength: { value: repulsionStrength },\n uMouseActiveFactor: { value: 0.0 },\n uAutoCenterRepulsion: { value: autoCenterRepulsion },\n uTransparent: { value: transparent }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n let animateId: number;\n\n function update(t: number) {\n animateId = requestAnimationFrame(update);\n if (!disableAnimation) {\n program.uniforms.uTime.value = t * 0.001;\n program.uniforms.uStarSpeed.value = (t * 0.001 * starSpeed) / 10.0;\n }\n\n const lerpFactor = 0.05;\n smoothMousePos.current.x += (targetMousePos.current.x - smoothMousePos.current.x) * lerpFactor;\n smoothMousePos.current.y += (targetMousePos.current.y - smoothMousePos.current.y) * lerpFactor;\n\n smoothMouseActive.current += (targetMouseActive.current - smoothMouseActive.current) * lerpFactor;\n\n program.uniforms.uMouse.value[0] = smoothMousePos.current.x;\n program.uniforms.uMouse.value[1] = smoothMousePos.current.y;\n program.uniforms.uMouseActiveFactor.value = smoothMouseActive.current;\n\n renderer.render({ scene: mesh });\n }\n animateId = requestAnimationFrame(update);\n ctn.appendChild(gl.canvas);\n\n function handleMouseMove(e: MouseEvent) {\n const rect = ctn.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1.0 - (e.clientY - rect.top) / rect.height;\n targetMousePos.current = { x, y };\n targetMouseActive.current = 1.0;\n }\n\n function handleMouseLeave() {\n targetMouseActive.current = 0.0;\n }\n\n if (mouseInteraction) {\n ctn.addEventListener('mousemove', handleMouseMove);\n ctn.addEventListener('mouseleave', handleMouseLeave);\n }\n\n return () => {\n cancelAnimationFrame(animateId);\n window.removeEventListener('resize', resize);\n if (mouseInteraction) {\n ctn.removeEventListener('mousemove', handleMouseMove);\n ctn.removeEventListener('mouseleave', handleMouseLeave);\n }\n ctn.removeChild(gl.canvas);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, [\n focal,\n rotation,\n starSpeed,\n density,\n hueShift,\n disableAnimation,\n speed,\n mouseInteraction,\n glowIntensity,\n saturation,\n mouseRepulsion,\n twinkleIntensity,\n rotationSpeed,\n repulsionStrength,\n autoCenterRepulsion,\n transparent\n ]);\n\n return
;\n}\n" } ], diff --git a/public/r/GhostCursor-JS-CSS.json b/public/r/GhostCursor-JS-CSS.json index fc24e142..8d11dd91 100644 --- a/public/r/GhostCursor-JS-CSS.json +++ b/public/r/GhostCursor-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GhostCursor/GhostCursor.css", + "type": "registry:file", + "path": "GhostCursor.css", + "target": "@components/GhostCursor.css", "content": ".ghost-cursor {\n position: absolute;\n inset: 0;\n pointer-events: none;\n}\n\n.ghost-cursor > canvas {\n display: block;\n width: 100%;\n height: 100%;\n background: transparent;\n}\n" }, { "type": "registry:component", - "path": "GhostCursor/GhostCursor.jsx", + "path": "GhostCursor.jsx", "content": "import { useEffect, useMemo, useRef } from 'react';\nimport * as THREE from 'three';\nimport { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';\nimport { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';\nimport { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';\nimport { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';\nimport './GhostCursor.css';\n\nconst GhostCursor = ({\n className,\n style,\n trailLength = 50,\n inertia = 0.5,\n grainIntensity = 0.05,\n bloomStrength = 0.1,\n bloomRadius = 1.0,\n bloomThreshold = 0.025,\n\n brightness = 1,\n color = '#B497CF',\n mixBlendMode = 'screen',\n edgeIntensity = 0,\n\n maxDevicePixelRatio = 0.5,\n targetPixels,\n\n fadeDelayMs,\n fadeDurationMs,\n zIndex = 10\n}) => {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const composerRef = useRef(null);\n const materialRef = useRef(null);\n const bloomPassRef = useRef(null);\n const filmPassRef = useRef(null);\n\n const trailBufRef = useRef([]);\n const headRef = useRef(0);\n\n const rafRef = useRef(null);\n const resizeObsRef = useRef(null);\n const currentMouseRef = useRef(new THREE.Vector2(0.5, 0.5));\n const velocityRef = useRef(new THREE.Vector2(0, 0));\n const fadeOpacityRef = useRef(1.0);\n const lastMoveTimeRef = useRef(typeof performance !== 'undefined' ? performance.now() : Date.now());\n const pointerActiveRef = useRef(false);\n const runningRef = useRef(false);\n const hasValidSizeRef = useRef(false);\n\n const isTouch = useMemo(\n () => typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0),\n []\n );\n\n const pixelBudget = targetPixels ?? (isTouch ? 0.9e6 : 1.3e6);\n const fadeDelay = fadeDelayMs ?? (isTouch ? 500 : 1000);\n const fadeDuration = fadeDurationMs ?? (isTouch ? 1000 : 1500);\n\n const baseVertexShader = `\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n }\n `;\n\n const fragmentShader = `\n uniform float iTime;\n uniform vec3 iResolution;\n uniform vec2 iMouse;\n uniform vec2 iPrevMouse[MAX_TRAIL_LENGTH];\n uniform float iOpacity;\n uniform float iScale;\n uniform vec3 iBaseColor;\n uniform float iBrightness;\n uniform float iEdgeIntensity;\n varying vec2 vUv;\n\n float hash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7))) * 43758.5453123); }\n float noise(vec2 p){\n vec2 i = floor(p), f = fract(p);\n f *= f * (3. - 2. * f);\n return mix(mix(hash(i + vec2(0.,0.)), hash(i + vec2(1.,0.)), f.x),\n mix(hash(i + vec2(0.,1.)), hash(i + vec2(1.,1.)), f.x), f.y);\n }\n float fbm(vec2 p){\n float v = 0.0;\n float a = 0.5;\n mat2 m = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));\n for(int i=0;i<5;i++){\n v += a * noise(p);\n p = m * p * 2.0;\n a *= 0.5;\n }\n return v;\n }\n vec3 tint1(vec3 base){ return mix(base, vec3(1.0), 0.15); }\n vec3 tint2(vec3 base){ return mix(base, vec3(0.8, 0.9, 1.0), 0.25); }\n\n vec4 blob(vec2 p, vec2 mousePos, float intensity, float activity) {\n vec2 q = vec2(fbm(p * iScale + iTime * 0.1), fbm(p * iScale + vec2(5.2,1.3) + iTime * 0.1));\n vec2 r = vec2(fbm(p * iScale + q * 1.5 + iTime * 0.15), fbm(p * iScale + q * 1.5 + vec2(8.3,2.8) + iTime * 0.15));\n\n float smoke = fbm(p * iScale + r * 0.8);\n float radius = 0.5 + 0.3 * (1.0 / iScale);\n float distFactor = 1.0 - smoothstep(0.0, radius * activity, length(p - mousePos));\n float alpha = pow(smoke, 2.5) * distFactor;\n\n vec3 c1 = tint1(iBaseColor);\n vec3 c2 = tint2(iBaseColor);\n vec3 color = mix(c1, c2, sin(iTime * 0.5) * 0.5 + 0.5);\n\n return vec4(color * alpha * intensity, alpha * intensity);\n }\n\n void main() {\n vec2 uv = (gl_FragCoord.xy / iResolution.xy * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n vec2 mouse = (iMouse * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n\n vec3 colorAcc = vec3(0.0);\n float alphaAcc = 0.0;\n\n vec4 b = blob(uv, mouse, 1.0, iOpacity);\n colorAcc += b.rgb;\n alphaAcc += b.a;\n\n for (int i = 0; i < MAX_TRAIL_LENGTH; i++) {\n vec2 pm = (iPrevMouse[i] * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n float t = 1.0 - float(i) / float(MAX_TRAIL_LENGTH);\n t = pow(t, 2.0);\n if (t > 0.01) {\n vec4 bt = blob(uv, pm, t * 0.8, iOpacity);\n colorAcc += bt.rgb;\n alphaAcc += bt.a;\n }\n }\n\n colorAcc *= iBrightness;\n\n vec2 uv01 = gl_FragCoord.xy / iResolution.xy;\n float edgeDist = min(min(uv01.x, 1.0 - uv01.x), min(uv01.y, 1.0 - uv01.y));\n float distFromEdge = clamp(edgeDist * 2.0, 0.0, 1.0);\n float k = clamp(iEdgeIntensity, 0.0, 1.0);\n float edgeMask = mix(1.0 - k, 1.0, distFromEdge);\n\n float outAlpha = clamp(alphaAcc * iOpacity * edgeMask, 0.0, 1.0);\n gl_FragColor = vec4(colorAcc, outAlpha);\n }\n `;\n\n const FilmGrainShader = useMemo(() => {\n return {\n uniforms: {\n tDiffuse: { value: null },\n iTime: { value: 0 },\n intensity: { value: grainIntensity }\n },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float iTime;\n uniform float intensity;\n varying vec2 vUv;\n\n float hash1(float n){ return fract(sin(n)*43758.5453); }\n\n void main(){\n vec4 color = texture2D(tDiffuse, vUv);\n float n = hash1(vUv.x*1000.0 + vUv.y*2000.0 + iTime) * 2.0 - 1.0;\n color.rgb += n * intensity * color.rgb;\n gl_FragColor = color;\n }\n `\n };\n }, [grainIntensity]);\n\n const UnpremultiplyPass = useMemo(\n () =>\n new ShaderPass({\n uniforms: { tDiffuse: { value: null } },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n varying vec2 vUv;\n void main(){\n vec4 c = texture2D(tDiffuse, vUv);\n float a = max(c.a, 1e-5);\n vec3 straight = c.rgb / a;\n gl_FragColor = vec4(clamp(straight, 0.0, 1.0), c.a);\n }\n `\n }),\n []\n );\n\n function calculateScale(el) {\n const r = el.getBoundingClientRect();\n const base = 600;\n const current = Math.min(Math.max(1, r.width), Math.max(1, r.height));\n return Math.max(0.5, Math.min(2.0, current / base));\n }\n\n useEffect(() => {\n const host = containerRef.current;\n const parent = host?.parentElement;\n if (!host || !parent) return;\n\n let active = true;\n\n const prevParentPos = parent.style.position;\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = 'relative';\n }\n\n const renderer = new THREE.WebGLRenderer({\n antialias: !isTouch,\n alpha: true,\n depth: false,\n stencil: false,\n powerPreference: isTouch ? 'low-power' : 'high-performance',\n premultipliedAlpha: false,\n preserveDrawingBuffer: false\n });\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n renderer.domElement.style.pointerEvents = 'none';\n if (mixBlendMode) {\n renderer.domElement.style.mixBlendMode = String(mixBlendMode);\n } else {\n renderer.domElement.style.removeProperty('mix-blend-mode');\n }\n\n host.appendChild(renderer.domElement);\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geom = new THREE.PlaneGeometry(2, 2);\n\n const maxTrail = Math.max(1, Math.floor(trailLength));\n trailBufRef.current = Array.from({ length: maxTrail }, () => new THREE.Vector2(0.5, 0.5));\n headRef.current = 0;\n\n const baseColor = new THREE.Color(color);\n\n const material = new THREE.ShaderMaterial({\n defines: { MAX_TRAIL_LENGTH: maxTrail },\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new THREE.Vector3(1, 1, 1) },\n iMouse: { value: new THREE.Vector2(0.5, 0.5) },\n iPrevMouse: { value: trailBufRef.current.map(v => v.clone()) },\n iOpacity: { value: 1.0 },\n iScale: { value: 1.0 },\n iBaseColor: { value: new THREE.Vector3(baseColor.r, baseColor.g, baseColor.b) },\n iBrightness: { value: brightness },\n iEdgeIntensity: { value: edgeIntensity }\n },\n vertexShader: baseVertexShader,\n fragmentShader,\n transparent: true,\n depthTest: false,\n depthWrite: false\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geom, material);\n scene.add(mesh);\n\n const composer = new EffectComposer(renderer);\n composerRef.current = composer;\n\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloomPass = new UnrealBloomPass(new THREE.Vector2(1, 1), bloomStrength, bloomRadius, bloomThreshold);\n bloomPassRef.current = bloomPass;\n composer.addPass(bloomPass);\n\n const filmPass = new ShaderPass(FilmGrainShader);\n filmPassRef.current = filmPass;\n composer.addPass(filmPass);\n\n composer.addPass(UnpremultiplyPass);\n\n const resize = () => {\n if (!active) return;\n\n const rect = host.getBoundingClientRect();\n const cssW = Math.floor(rect.width);\n const cssH = Math.floor(rect.height);\n\n if (cssW <= 0 || cssH <= 0) {\n hasValidSizeRef.current = false;\n return;\n }\n\n const currentDPR = Math.min(\n typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,\n maxDevicePixelRatio\n );\n const need = cssW * cssH * currentDPR * currentDPR;\n const scale = need <= pixelBudget ? 1 : Math.max(0.5, Math.min(1, Math.sqrt(pixelBudget / Math.max(1, need))));\n const pixelRatio = currentDPR * scale;\n\n renderer.setPixelRatio(pixelRatio);\n renderer.setSize(cssW, cssH, false);\n\n composer.setPixelRatio?.(pixelRatio);\n composer.setSize(cssW, cssH);\n\n const wpx = Math.max(1, Math.floor(cssW * pixelRatio));\n const hpx = Math.max(1, Math.floor(cssH * pixelRatio));\n material.uniforms.iResolution.value.set(wpx, hpx, 1);\n material.uniforms.iScale.value = calculateScale(host);\n bloomPass.setSize(wpx, hpx);\n\n hasValidSizeRef.current = true;\n };\n\n resize();\n const ro = new ResizeObserver(() => {\n if (!active) return;\n resize();\n });\n resizeObsRef.current = ro;\n ro.observe(parent);\n ro.observe(host);\n\n const start = typeof performance !== 'undefined' ? performance.now() : Date.now();\n const animate = () => {\n if (!active) return;\n\n if (!hasValidSizeRef.current) {\n rafRef.current = requestAnimationFrame(animate);\n return;\n }\n\n const now = performance.now();\n const t = (now - start) / 1000;\n\n const mat = materialRef.current;\n const comp = composerRef.current;\n\n if (pointerActiveRef.current) {\n velocityRef.current.set(\n currentMouseRef.current.x - mat.uniforms.iMouse.value.x,\n currentMouseRef.current.y - mat.uniforms.iMouse.value.y\n );\n mat.uniforms.iMouse.value.copy(currentMouseRef.current);\n fadeOpacityRef.current = 1.0;\n } else {\n velocityRef.current.multiplyScalar(inertia);\n if (velocityRef.current.lengthSq() > 1e-6) {\n mat.uniforms.iMouse.value.add(velocityRef.current);\n }\n const dt = now - lastMoveTimeRef.current;\n if (dt > fadeDelay) {\n const k = Math.min(1, (dt - fadeDelay) / fadeDuration);\n fadeOpacityRef.current = Math.max(0, 1 - k);\n }\n }\n\n const N = trailBufRef.current.length;\n headRef.current = (headRef.current + 1) % N;\n trailBufRef.current[headRef.current].copy(mat.uniforms.iMouse.value);\n const arr = mat.uniforms.iPrevMouse.value;\n for (let i = 0; i < N; i++) {\n const srcIdx = (headRef.current - i + N) % N;\n arr[i].copy(trailBufRef.current[srcIdx]);\n }\n\n mat.uniforms.iOpacity.value = fadeOpacityRef.current;\n mat.uniforms.iTime.value = t;\n\n if (filmPassRef.current?.uniforms?.iTime) {\n filmPassRef.current.uniforms.iTime.value = t;\n }\n\n comp.render();\n\n if (!pointerActiveRef.current && fadeOpacityRef.current <= 0.001) {\n runningRef.current = false;\n rafRef.current = null;\n return;\n }\n\n rafRef.current = requestAnimationFrame(animate);\n };\n\n const ensureLoop = () => {\n if (!runningRef.current) {\n runningRef.current = true;\n rafRef.current = requestAnimationFrame(animate);\n }\n };\n\n const onPointerMove = e => {\n const rect = parent.getBoundingClientRect();\n const x = THREE.MathUtils.clamp((e.clientX - rect.left) / Math.max(1, rect.width), 0, 1);\n const y = THREE.MathUtils.clamp(1 - (e.clientY - rect.top) / Math.max(1, rect.height), 0, 1);\n currentMouseRef.current.set(x, y);\n pointerActiveRef.current = true;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n const onPointerEnter = () => {\n pointerActiveRef.current = true;\n ensureLoop();\n };\n const onPointerLeave = () => {\n pointerActiveRef.current = false;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n\n parent.addEventListener('pointermove', onPointerMove, { passive: true });\n parent.addEventListener('pointerenter', onPointerEnter, { passive: true });\n parent.addEventListener('pointerleave', onPointerLeave, { passive: true });\n\n ensureLoop();\n\n return () => {\n active = false;\n hasValidSizeRef.current = false;\n\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n runningRef.current = false;\n rafRef.current = null;\n\n parent.removeEventListener('pointermove', onPointerMove);\n parent.removeEventListener('pointerenter', onPointerEnter);\n parent.removeEventListener('pointerleave', onPointerLeave);\n resizeObsRef.current?.disconnect();\n\n scene.clear();\n geom.dispose();\n material.dispose();\n materialRef.current = null;\n composer.dispose();\n composerRef.current = null;\n renderer.dispose();\n renderer.forceContextLoss();\n rendererRef.current = null;\n\n if (renderer.domElement && renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = prevParentPos;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n trailLength,\n inertia,\n grainIntensity,\n bloomStrength,\n bloomRadius,\n bloomThreshold,\n pixelBudget,\n fadeDelay,\n fadeDuration,\n isTouch,\n color,\n brightness,\n mixBlendMode,\n edgeIntensity\n ]);\n\n useEffect(() => {\n if (materialRef.current) {\n const c = new THREE.Color(color);\n materialRef.current.uniforms.iBaseColor.value.set(c.r, c.g, c.b);\n }\n }, [color]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iBrightness.value = brightness;\n }\n }, [brightness]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iEdgeIntensity.value = edgeIntensity;\n }\n }, [edgeIntensity]);\n\n useEffect(() => {\n if (filmPassRef.current?.uniforms?.intensity) {\n filmPassRef.current.uniforms.intensity.value = grainIntensity;\n }\n }, [grainIntensity]);\n\n useEffect(() => {\n const el = rendererRef.current?.domElement;\n if (!el) return;\n if (mixBlendMode) {\n el.style.mixBlendMode = String(mixBlendMode);\n } else {\n el.style.removeProperty('mix-blend-mode');\n }\n }, [mixBlendMode]);\n\n const mergedStyle = useMemo(() => ({ zIndex, ...style }), [zIndex, style]);\n\n return
;\n};\n\nexport default GhostCursor;\n" } ], diff --git a/public/r/GhostCursor-TS-CSS.json b/public/r/GhostCursor-TS-CSS.json index f8f3a2ef..1596035d 100644 --- a/public/r/GhostCursor-TS-CSS.json +++ b/public/r/GhostCursor-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GhostCursor/GhostCursor.css", + "type": "registry:file", + "path": "GhostCursor.css", + "target": "@components/GhostCursor.css", "content": ".ghost-cursor {\n position: absolute;\n inset: 0;\n pointer-events: none;\n}\n\n.ghost-cursor > canvas {\n display: block;\n width: 100%;\n height: 100%;\n background: transparent;\n}\n" }, { "type": "registry:component", - "path": "GhostCursor/GhostCursor.tsx", + "path": "GhostCursor.tsx", "content": "import React, { useEffect, useMemo, useRef } from 'react';\nimport * as THREE from 'three';\nimport { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';\nimport { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';\nimport { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';\nimport { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';\nimport './GhostCursor.css';\n\ntype GhostCursorProps = {\n className?: string;\n style?: React.CSSProperties;\n\n trailLength?: number;\n inertia?: number;\n grainIntensity?: number;\n bloomStrength?: number;\n bloomRadius?: number;\n bloomThreshold?: number;\n\n brightness?: number;\n color?: string;\n mixBlendMode?: React.CSSProperties['mixBlendMode'];\n edgeIntensity?: number;\n\n maxDevicePixelRatio?: number;\n targetPixels?: number;\n fadeDelayMs?: number;\n fadeDurationMs?: number;\n zIndex?: number;\n};\n\nconst GhostCursor: React.FC = ({\n className,\n style,\n trailLength = 50,\n inertia = 0.5,\n grainIntensity = 0.05,\n bloomStrength = 0.1,\n bloomRadius = 1.0,\n bloomThreshold = 0.025,\n\n brightness = 1,\n color = '#B497CF',\n mixBlendMode = 'screen',\n edgeIntensity = 0,\n\n maxDevicePixelRatio = 0.5,\n targetPixels,\n\n fadeDelayMs,\n fadeDurationMs,\n zIndex = 10\n}) => {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const composerRef = useRef(null);\n const materialRef = useRef(null);\n const bloomPassRef = useRef(null);\n const filmPassRef = useRef(null);\n\n // Trail circular buffer\n const trailBufRef = useRef([]);\n const headRef = useRef(0);\n\n const rafRef = useRef(null);\n const resizeObsRef = useRef(null);\n const currentMouseRef = useRef(new THREE.Vector2(0.5, 0.5));\n const velocityRef = useRef(new THREE.Vector2(0, 0));\n const fadeOpacityRef = useRef(1.0);\n const lastMoveTimeRef = useRef(typeof performance !== 'undefined' ? performance.now() : Date.now());\n const pointerActiveRef = useRef(false);\n const runningRef = useRef(false);\n const hasValidSizeRef = useRef(false);\n\n const isTouch = useMemo(\n () => typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0),\n []\n );\n\n const pixelBudget = targetPixels ?? (isTouch ? 0.9e6 : 1.3e6);\n const fadeDelay = fadeDelayMs ?? (isTouch ? 500 : 1000);\n const fadeDuration = fadeDurationMs ?? (isTouch ? 1000 : 1500);\n\n const baseVertexShader = `\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n }\n `;\n\n const fragmentShader = `\n uniform float iTime;\n uniform vec3 iResolution;\n uniform vec2 iMouse;\n uniform vec2 iPrevMouse[MAX_TRAIL_LENGTH];\n uniform float iOpacity;\n uniform float iScale;\n uniform vec3 iBaseColor;\n uniform float iBrightness;\n uniform float iEdgeIntensity;\n varying vec2 vUv;\n\n float hash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7))) * 43758.5453123); }\n float noise(vec2 p){\n vec2 i = floor(p), f = fract(p);\n f *= f * (3. - 2. * f);\n return mix(mix(hash(i + vec2(0.,0.)), hash(i + vec2(1.,0.)), f.x),\n mix(hash(i + vec2(0.,1.)), hash(i + vec2(1.,1.)), f.x), f.y);\n }\n float fbm(vec2 p){\n float v = 0.0;\n float a = 0.5;\n mat2 m = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));\n for(int i=0;i<5;i++){\n v += a * noise(p);\n p = m * p * 2.0;\n a *= 0.5;\n }\n return v;\n }\n vec3 tint1(vec3 base){ return mix(base, vec3(1.0), 0.15); }\n vec3 tint2(vec3 base){ return mix(base, vec3(0.8, 0.9, 1.0), 0.25); }\n\n vec4 blob(vec2 p, vec2 mousePos, float intensity, float activity) {\n vec2 q = vec2(fbm(p * iScale + iTime * 0.1), fbm(p * iScale + vec2(5.2,1.3) + iTime * 0.1));\n vec2 r = vec2(fbm(p * iScale + q * 1.5 + iTime * 0.15), fbm(p * iScale + q * 1.5 + vec2(8.3,2.8) + iTime * 0.15));\n\n float smoke = fbm(p * iScale + r * 0.8);\n float radius = 0.5 + 0.3 * (1.0 / iScale);\n float distFactor = 1.0 - smoothstep(0.0, radius * activity, length(p - mousePos));\n float alpha = pow(smoke, 2.5) * distFactor;\n\n vec3 c1 = tint1(iBaseColor);\n vec3 c2 = tint2(iBaseColor);\n vec3 color = mix(c1, c2, sin(iTime * 0.5) * 0.5 + 0.5);\n\n return vec4(color * alpha * intensity, alpha * intensity);\n }\n\n void main() {\n vec2 uv = (gl_FragCoord.xy / iResolution.xy * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n vec2 mouse = (iMouse * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n\n vec3 colorAcc = vec3(0.0);\n float alphaAcc = 0.0;\n\n vec4 b = blob(uv, mouse, 1.0, iOpacity);\n colorAcc += b.rgb;\n alphaAcc += b.a;\n\n for (int i = 0; i < MAX_TRAIL_LENGTH; i++) {\n vec2 pm = (iPrevMouse[i] * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n float t = 1.0 - float(i) / float(MAX_TRAIL_LENGTH);\n t = pow(t, 2.0);\n if (t > 0.01) {\n vec4 bt = blob(uv, pm, t * 0.8, iOpacity);\n colorAcc += bt.rgb;\n alphaAcc += bt.a;\n }\n }\n\n colorAcc *= iBrightness;\n\n vec2 uv01 = gl_FragCoord.xy / iResolution.xy;\n float edgeDist = min(min(uv01.x, 1.0 - uv01.x), min(uv01.y, 1.0 - uv01.y));\n float distFromEdge = clamp(edgeDist * 2.0, 0.0, 1.0);\n float k = clamp(iEdgeIntensity, 0.0, 1.0);\n float edgeMask = mix(1.0 - k, 1.0, distFromEdge);\n\n float outAlpha = clamp(alphaAcc * iOpacity * edgeMask, 0.0, 1.0);\n gl_FragColor = vec4(colorAcc, outAlpha);\n }\n `;\n\n const FilmGrainShader = useMemo(() => {\n return {\n uniforms: {\n tDiffuse: { value: null },\n iTime: { value: 0 },\n intensity: { value: grainIntensity }\n },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float iTime;\n uniform float intensity;\n varying vec2 vUv;\n\n float hash1(float n){ return fract(sin(n)*43758.5453); }\n\n void main(){\n vec4 color = texture2D(tDiffuse, vUv);\n float n = hash1(vUv.x*1000.0 + vUv.y*2000.0 + iTime) * 2.0 - 1.0;\n color.rgb += n * intensity * color.rgb;\n gl_FragColor = color;\n }\n `\n };\n }, [grainIntensity]);\n\n const UnpremultiplyPass = useMemo(\n () =>\n new ShaderPass({\n uniforms: { tDiffuse: { value: null } },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n varying vec2 vUv;\n void main(){\n vec4 c = texture2D(tDiffuse, vUv);\n float a = max(c.a, 1e-5);\n vec3 straight = c.rgb / a;\n gl_FragColor = vec4(clamp(straight, 0.0, 1.0), c.a);\n }\n `\n }),\n []\n );\n\n function calculateScale(el: HTMLElement) {\n const r = el.getBoundingClientRect();\n const base = 600;\n const current = Math.min(Math.max(1, r.width), Math.max(1, r.height));\n return Math.max(0.5, Math.min(2.0, current / base));\n }\n\n useEffect(() => {\n const host = containerRef.current;\n const parent = host?.parentElement;\n if (!host || !parent) return;\n\n let active = true;\n\n const prevParentPos = parent.style.position;\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = 'relative';\n }\n\n const renderer = new THREE.WebGLRenderer({\n antialias: !isTouch,\n alpha: true,\n depth: false,\n stencil: false,\n powerPreference: isTouch ? 'low-power' : 'high-performance',\n premultipliedAlpha: false,\n preserveDrawingBuffer: false\n });\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n renderer.domElement.style.pointerEvents = 'none';\n if (mixBlendMode) {\n renderer.domElement.style.mixBlendMode = String(mixBlendMode);\n } else {\n renderer.domElement.style.removeProperty('mix-blend-mode');\n }\n\n host.appendChild(renderer.domElement);\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geom = new THREE.PlaneGeometry(2, 2);\n\n const maxTrail = Math.max(1, Math.floor(trailLength));\n trailBufRef.current = Array.from({ length: maxTrail }, () => new THREE.Vector2(0.5, 0.5));\n headRef.current = 0;\n\n const baseColor = new THREE.Color(color);\n\n const material = new THREE.ShaderMaterial({\n defines: { MAX_TRAIL_LENGTH: maxTrail },\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new THREE.Vector3(1, 1, 1) },\n iMouse: { value: new THREE.Vector2(0.5, 0.5) },\n iPrevMouse: { value: trailBufRef.current.map(v => v.clone()) },\n iOpacity: { value: 1.0 },\n iScale: { value: 1.0 },\n iBaseColor: { value: new THREE.Vector3(baseColor.r, baseColor.g, baseColor.b) },\n iBrightness: { value: brightness },\n iEdgeIntensity: { value: edgeIntensity }\n },\n vertexShader: baseVertexShader,\n fragmentShader,\n transparent: true,\n depthTest: false,\n depthWrite: false\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geom, material);\n scene.add(mesh);\n\n const composer = new EffectComposer(renderer);\n composerRef.current = composer;\n\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloomPass = new UnrealBloomPass(new THREE.Vector2(1, 1), bloomStrength, bloomRadius, bloomThreshold);\n bloomPassRef.current = bloomPass;\n composer.addPass(bloomPass);\n\n const filmPass = new ShaderPass(FilmGrainShader as any);\n filmPassRef.current = filmPass;\n composer.addPass(filmPass);\n\n composer.addPass(UnpremultiplyPass);\n\n const resize = () => {\n if (!active) return;\n\n const rect = host.getBoundingClientRect();\n const cssW = Math.floor(rect.width);\n const cssH = Math.floor(rect.height);\n\n if (cssW <= 0 || cssH <= 0) {\n hasValidSizeRef.current = false;\n return;\n }\n\n const currentDPR = Math.min(\n typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,\n maxDevicePixelRatio\n );\n const need = cssW * cssH * currentDPR * currentDPR;\n const scale = need <= pixelBudget ? 1 : Math.max(0.5, Math.min(1, Math.sqrt(pixelBudget / Math.max(1, need))));\n const pixelRatio = currentDPR * scale;\n\n renderer.setPixelRatio(pixelRatio);\n renderer.setSize(cssW, cssH, false);\n\n composer.setPixelRatio?.(pixelRatio);\n composer.setSize(cssW, cssH);\n\n const wpx = Math.max(1, Math.floor(cssW * pixelRatio));\n const hpx = Math.max(1, Math.floor(cssH * pixelRatio));\n material.uniforms.iResolution.value.set(wpx, hpx, 1);\n material.uniforms.iScale.value = calculateScale(host);\n bloomPass.setSize(wpx, hpx);\n\n hasValidSizeRef.current = true;\n };\n\n resize();\n const ro = new ResizeObserver(resize);\n resizeObsRef.current = ro;\n ro.observe(parent);\n ro.observe(host);\n\n const start = typeof performance !== 'undefined' ? performance.now() : Date.now();\n const animate = () => {\n if (!active) return;\n\n if (!hasValidSizeRef.current) {\n rafRef.current = requestAnimationFrame(animate);\n return;\n }\n\n const now = performance.now();\n const t = (now - start) / 1000;\n\n const mat = materialRef.current!;\n const comp = composerRef.current!;\n\n if (pointerActiveRef.current) {\n velocityRef.current.set(\n currentMouseRef.current.x - mat.uniforms.iMouse.value.x,\n currentMouseRef.current.y - mat.uniforms.iMouse.value.y\n );\n mat.uniforms.iMouse.value.copy(currentMouseRef.current);\n fadeOpacityRef.current = 1.0;\n } else {\n velocityRef.current.multiplyScalar(inertia);\n if (velocityRef.current.lengthSq() > 1e-6) {\n mat.uniforms.iMouse.value.add(velocityRef.current);\n }\n const dt = now - lastMoveTimeRef.current;\n if (dt > fadeDelay) {\n const k = Math.min(1, (dt - fadeDelay) / fadeDuration);\n fadeOpacityRef.current = Math.max(0, 1 - k);\n }\n }\n\n const N = trailBufRef.current.length;\n headRef.current = (headRef.current + 1) % N;\n trailBufRef.current[headRef.current].copy(mat.uniforms.iMouse.value);\n const arr = mat.uniforms.iPrevMouse.value as THREE.Vector2[];\n for (let i = 0; i < N; i++) {\n const srcIdx = (headRef.current - i + N) % N;\n arr[i].copy(trailBufRef.current[srcIdx]);\n }\n\n mat.uniforms.iOpacity.value = fadeOpacityRef.current;\n mat.uniforms.iTime.value = t;\n\n if (filmPassRef.current?.uniforms?.iTime) {\n filmPassRef.current.uniforms.iTime.value = t;\n }\n\n comp.render();\n\n if (!pointerActiveRef.current && fadeOpacityRef.current <= 0.001) {\n runningRef.current = false;\n rafRef.current = null;\n return;\n }\n\n rafRef.current = requestAnimationFrame(animate);\n };\n\n const ensureLoop = () => {\n if (!runningRef.current) {\n runningRef.current = true;\n rafRef.current = requestAnimationFrame(animate);\n }\n };\n\n const onPointerMove = (e: PointerEvent) => {\n const rect = parent.getBoundingClientRect();\n const x = THREE.MathUtils.clamp((e.clientX - rect.left) / Math.max(1, rect.width), 0, 1);\n const y = THREE.MathUtils.clamp(1 - (e.clientY - rect.top) / Math.max(1, rect.height), 0, 1);\n currentMouseRef.current.set(x, y);\n pointerActiveRef.current = true;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n const onPointerEnter = () => {\n pointerActiveRef.current = true;\n ensureLoop();\n };\n const onPointerLeave = () => {\n pointerActiveRef.current = false;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n\n parent.addEventListener('pointermove', onPointerMove, { passive: true });\n parent.addEventListener('pointerenter', onPointerEnter, { passive: true });\n parent.addEventListener('pointerleave', onPointerLeave, { passive: true });\n\n ensureLoop();\n\n return () => {\n active = false;\n hasValidSizeRef.current = false;\n\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n runningRef.current = false;\n rafRef.current = null;\n\n parent.removeEventListener('pointermove', onPointerMove);\n parent.removeEventListener('pointerenter', onPointerEnter);\n parent.removeEventListener('pointerleave', onPointerLeave);\n resizeObsRef.current?.disconnect();\n\n scene.clear();\n geom.dispose();\n material.dispose();\n materialRef.current = null;\n composer.dispose();\n composerRef.current = null;\n renderer.dispose();\n renderer.forceContextLoss();\n rendererRef.current = null;\n\n if (renderer.domElement && renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = prevParentPos;\n }\n };\n }, [\n trailLength,\n inertia,\n grainIntensity,\n bloomStrength,\n bloomRadius,\n bloomThreshold,\n pixelBudget,\n fadeDelay,\n fadeDuration,\n isTouch,\n color,\n brightness,\n mixBlendMode,\n edgeIntensity\n ]);\n\n useEffect(() => {\n if (materialRef.current) {\n const c = new THREE.Color(color);\n (materialRef.current.uniforms.iBaseColor.value as THREE.Vector3).set(c.r, c.g, c.b);\n }\n }, [color]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iBrightness.value = brightness;\n }\n }, [brightness]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iEdgeIntensity.value = edgeIntensity;\n }\n }, [edgeIntensity]);\n\n useEffect(() => {\n if (filmPassRef.current?.uniforms?.intensity) {\n filmPassRef.current.uniforms.intensity.value = grainIntensity;\n }\n }, [grainIntensity]);\n\n useEffect(() => {\n const el = rendererRef.current?.domElement;\n if (!el) return;\n if (mixBlendMode) {\n el.style.mixBlendMode = String(mixBlendMode);\n } else {\n el.style.removeProperty('mix-blend-mode');\n }\n }, [mixBlendMode]);\n\n const mergedStyle = useMemo(() => ({ zIndex, ...style }), [zIndex, style]);\n\n return
;\n};\n\nexport default GhostCursor;\n" } ], diff --git a/public/r/GlareHover-JS-CSS.json b/public/r/GlareHover-JS-CSS.json index 71d4b921..9c6a723a 100644 --- a/public/r/GlareHover-JS-CSS.json +++ b/public/r/GlareHover-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GlareHover/GlareHover.css", + "type": "registry:file", + "path": "GlareHover.css", + "target": "@components/GlareHover.css", "content": ".glare-hover {\n width: var(--gh-width);\n height: var(--gh-height);\n background: var(--gh-bg);\n border-radius: var(--gh-br);\n border: 1px solid var(--gh-border);\n overflow: hidden;\n position: relative;\n display: grid;\n place-items: center;\n}\n\n.glare-hover::before {\n content: '';\n position: absolute;\n inset: 0;\n background: linear-gradient(\n var(--gh-angle),\n hsla(0, 0%, 0%, 0) 60%,\n var(--gh-rgba) 70%,\n hsla(0, 0%, 0%, 0),\n hsla(0, 0%, 0%, 0) 100%\n );\n transition: var(--gh-duration) ease;\n background-size:\n var(--gh-size) var(--gh-size),\n 100% 100%;\n background-repeat: no-repeat;\n background-position:\n -100% -100%,\n 0 0;\n}\n\n.glare-hover:hover {\n cursor: pointer;\n}\n\n.glare-hover:hover::before {\n background-position:\n 100% 100%,\n 0 0;\n}\n\n.glare-hover--play-once::before {\n transition: none;\n}\n\n.glare-hover--play-once:hover::before {\n transition: var(--gh-duration) ease;\n background-position:\n 100% 100%,\n 0 0;\n}\n" }, { "type": "registry:component", - "path": "GlareHover/GlareHover.jsx", + "path": "GlareHover.jsx", "content": "import './GlareHover.css';\n\nconst GlareHover = ({\n width = '500px',\n height = '500px',\n background = '#000',\n borderRadius = '10px',\n borderColor = '#333',\n children,\n glareColor = '#ffffff',\n glareOpacity = 0.5,\n glareAngle = -45,\n glareSize = 250,\n transitionDuration = 650,\n playOnce = false,\n className = '',\n style = {}\n}) => {\n const hex = glareColor.replace('#', '');\n let rgba = glareColor;\n if (/^[0-9A-Fa-f]{6}$/.test(hex)) {\n const r = parseInt(hex.slice(0, 2), 16);\n const g = parseInt(hex.slice(2, 4), 16);\n const b = parseInt(hex.slice(4, 6), 16);\n rgba = `rgba(${r}, ${g}, ${b}, ${glareOpacity})`;\n } else if (/^[0-9A-Fa-f]{3}$/.test(hex)) {\n const r = parseInt(hex[0] + hex[0], 16);\n const g = parseInt(hex[1] + hex[1], 16);\n const b = parseInt(hex[2] + hex[2], 16);\n rgba = `rgba(${r}, ${g}, ${b}, ${glareOpacity})`;\n }\n\n const vars = {\n '--gh-width': width,\n '--gh-height': height,\n '--gh-bg': background,\n '--gh-br': borderRadius,\n '--gh-angle': `${glareAngle}deg`,\n '--gh-duration': `${transitionDuration}ms`,\n '--gh-size': `${glareSize}%`,\n '--gh-rgba': rgba,\n '--gh-border': borderColor\n };\n\n return (\n \n {children}\n
\n );\n};\n\nexport default GlareHover;\n" } ], diff --git a/public/r/GlareHover-TS-CSS.json b/public/r/GlareHover-TS-CSS.json index cdb31b38..824d8973 100644 --- a/public/r/GlareHover-TS-CSS.json +++ b/public/r/GlareHover-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GlareHover/GlareHover.css", + "type": "registry:file", + "path": "GlareHover.css", + "target": "@components/GlareHover.css", "content": ".glare-hover {\n width: var(--gh-width);\n height: var(--gh-height);\n background: var(--gh-bg);\n border-radius: var(--gh-br);\n border: 1px solid var(--gh-border);\n overflow: hidden;\n position: relative;\n display: grid;\n place-items: center;\n}\n\n.glare-hover::before {\n content: '';\n position: absolute;\n inset: 0;\n background: linear-gradient(\n var(--gh-angle),\n hsla(0, 0%, 0%, 0) 60%,\n var(--gh-rgba) 70%,\n hsla(0, 0%, 0%, 0),\n hsla(0, 0%, 0%, 0) 100%\n );\n transition: var(--gh-duration) ease;\n background-size:\n var(--gh-size) var(--gh-size),\n 100% 100%;\n background-repeat: no-repeat;\n background-position:\n -100% -100%,\n 0 0;\n}\n\n.glare-hover:hover {\n cursor: pointer;\n}\n\n.glare-hover:hover::before {\n background-position:\n 100% 100%,\n 0 0;\n}\n\n.glare-hover--play-once::before {\n transition: none;\n}\n\n.glare-hover--play-once:hover::before {\n transition: var(--gh-duration) ease;\n background-position:\n 100% 100%,\n 0 0;\n}\n" }, { "type": "registry:component", - "path": "GlareHover/GlareHover.tsx", + "path": "GlareHover.tsx", "content": "import React from 'react';\nimport './GlareHover.css';\n\ninterface GlareHoverProps {\n width?: string;\n height?: string;\n background?: string;\n borderRadius?: string;\n borderColor?: string;\n children?: React.ReactNode;\n glareColor?: string;\n glareOpacity?: number;\n glareAngle?: number;\n glareSize?: number;\n transitionDuration?: number;\n playOnce?: boolean;\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst GlareHover: React.FC = ({\n width = '500px',\n height = '500px',\n background = '#000',\n borderRadius = '10px',\n borderColor = '#333',\n children,\n glareColor = '#ffffff',\n glareOpacity = 0.5,\n glareAngle = -45,\n glareSize = 250,\n transitionDuration = 650,\n playOnce = false,\n className = '',\n style = {}\n}) => {\n const hex = glareColor.replace('#', '');\n let rgba = glareColor;\n if (/^[0-9A-Fa-f]{6}$/.test(hex)) {\n const r = parseInt(hex.slice(0, 2), 16);\n const g = parseInt(hex.slice(2, 4), 16);\n const b = parseInt(hex.slice(4, 6), 16);\n rgba = `rgba(${r}, ${g}, ${b}, ${glareOpacity})`;\n } else if (/^[0-9A-Fa-f]{3}$/.test(hex)) {\n const r = parseInt(hex[0] + hex[0], 16);\n const g = parseInt(hex[1] + hex[1], 16);\n const b = parseInt(hex[2] + hex[2], 16);\n rgba = `rgba(${r}, ${g}, ${b}, ${glareOpacity})`;\n }\n\n const vars: React.CSSProperties & { [k: string]: string } = {\n '--gh-width': width,\n '--gh-height': height,\n '--gh-bg': background,\n '--gh-br': borderRadius,\n '--gh-angle': `${glareAngle}deg`,\n '--gh-duration': `${transitionDuration}ms`,\n '--gh-size': `${glareSize}%`,\n '--gh-rgba': rgba,\n '--gh-border': borderColor\n };\n\n return (\n \n {children}\n
\n );\n};\n\nexport default GlareHover;\n" } ], diff --git a/public/r/GlassIcons-JS-CSS.json b/public/r/GlassIcons-JS-CSS.json index 1094638c..14202166 100644 --- a/public/r/GlassIcons-JS-CSS.json +++ b/public/r/GlassIcons-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GlassIcons/GlassIcons.css", + "type": "registry:file", + "path": "GlassIcons.css", + "target": "@components/GlassIcons.css", "content": ".icon-btns {\n display: grid;\n grid-gap: 5em;\n grid-template-columns: repeat(2, 1fr);\n margin: auto;\n padding: 3em 0;\n overflow: visible;\n}\n\n.icon-btn {\n background-color: transparent;\n outline: none;\n position: relative;\n width: 4.5em;\n height: 4.5em;\n perspective: 24em;\n transform-style: preserve-3d;\n -webkit-tap-highlight-color: transparent;\n border: none;\n cursor: pointer;\n}\n\n.icon-btn__back,\n.icon-btn__front,\n.icon-btn__label {\n transition:\n opacity 0.3s cubic-bezier(0.83, 0, 0.17, 1),\n transform 0.3s cubic-bezier(0.83, 0, 0.17, 1);\n}\n\n.icon-btn__back,\n.icon-btn__front {\n border-radius: 1.25em;\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n}\n\n.icon-btn__back {\n box-shadow: 0.5em -0.5em 0.75em hsla(223, 10%, 10%, 0.15);\n display: block;\n transform: rotate(15deg);\n transform-origin: 100% 100%;\n will-change: transform;\n}\n\n.icon-btn__front {\n background-color: hsla(0, 0%, 100%, 0.15);\n box-shadow: 0 0 0 0.1em hsla(0, 0%, 100%, 0.3) inset;\n backdrop-filter: blur(0.75em);\n -webkit-backdrop-filter: blur(0.75em);\n -moz-backdrop-filter: blur(0.75em);\n display: flex;\n transform-origin: 80% 50%;\n will-change: transform;\n}\n\n.icon-btn__icon {\n margin: auto;\n width: 1.5em;\n height: 1.5em;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.icon-btn__label {\n font-size: 1em;\n white-space: nowrap;\n text-align: center;\n line-height: 2;\n opacity: 0;\n position: absolute;\n top: 100%;\n right: 0;\n left: 0;\n transform: translateY(0);\n}\n\n.icon-btn:focus-visible .icon-btn__back,\n.icon-btn:hover .icon-btn__back {\n transform: rotate(25deg) translate3d(-0.5em, -0.5em, 0.5em);\n}\n\n.icon-btn:focus-visible .icon-btn__front,\n.icon-btn:hover .icon-btn__front {\n transform: translate3d(0, 0, 2em);\n}\n\n.icon-btn:focus-visible .icon-btn__label,\n.icon-btn:hover .icon-btn__label {\n opacity: 1;\n transform: translateY(20%);\n}\n\n@media (min-width: 768px) {\n .icon-btns {\n grid-template-columns: repeat(3, 1fr);\n }\n}\n" }, { "type": "registry:component", - "path": "GlassIcons/GlassIcons.jsx", + "path": "GlassIcons.jsx", "content": "import './GlassIcons.css';\n\nconst gradientMapping = {\n blue: 'linear-gradient(hsl(223, 90%, 50%), hsl(208, 90%, 50%))',\n purple: 'linear-gradient(hsl(283, 90%, 50%), hsl(268, 90%, 50%))',\n red: 'linear-gradient(hsl(3, 90%, 50%), hsl(348, 90%, 50%))',\n indigo: 'linear-gradient(hsl(253, 90%, 50%), hsl(238, 90%, 50%))',\n orange: 'linear-gradient(hsl(43, 90%, 50%), hsl(28, 90%, 50%))',\n green: 'linear-gradient(hsl(123, 90%, 40%), hsl(108, 90%, 40%))'\n};\n\nconst GlassIcons = ({ items, className }) => {\n const getBackgroundStyle = color => {\n if (gradientMapping[color]) {\n return { background: gradientMapping[color] };\n }\n return { background: color };\n };\n\n return (\n
\n {items.map((item, index) => (\n \n ))}\n
\n );\n};\n\nexport default GlassIcons;\n" } ], diff --git a/public/r/GlassIcons-TS-CSS.json b/public/r/GlassIcons-TS-CSS.json index 42358b98..7035d9c3 100644 --- a/public/r/GlassIcons-TS-CSS.json +++ b/public/r/GlassIcons-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GlassIcons/GlassIcons.css", + "type": "registry:file", + "path": "GlassIcons.css", + "target": "@components/GlassIcons.css", "content": ".icon-btns {\n display: grid;\n grid-gap: 5em;\n grid-template-columns: repeat(2, 1fr);\n margin: auto;\n padding: 3em 0;\n overflow: visible;\n}\n\n.icon-btn {\n background-color: transparent;\n outline: none;\n position: relative;\n width: 4.5em;\n height: 4.5em;\n perspective: 24em;\n transform-style: preserve-3d;\n -webkit-tap-highlight-color: transparent;\n border: none;\n cursor: pointer;\n}\n\n.icon-btn__back,\n.icon-btn__front,\n.icon-btn__label {\n transition:\n opacity 0.3s cubic-bezier(0.83, 0, 0.17, 1),\n transform 0.3s cubic-bezier(0.83, 0, 0.17, 1);\n}\n\n.icon-btn__back,\n.icon-btn__front {\n border-radius: 1.25em;\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n}\n\n.icon-btn__back {\n box-shadow: 0.5em -0.5em 0.75em hsla(223, 10%, 10%, 0.15);\n display: block;\n transform: rotate(15deg);\n transform-origin: 100% 100%;\n will-change: transform;\n}\n\n.icon-btn__front {\n background-color: hsla(0, 0%, 100%, 0.15);\n box-shadow: 0 0 0 0.1em hsla(0, 0%, 100%, 0.3) inset;\n backdrop-filter: blur(0.75em);\n -webkit-backdrop-filter: blur(0.75em);\n -moz-backdrop-filter: blur(0.75em);\n display: flex;\n transform-origin: 80% 50%;\n will-change: transform;\n}\n\n.icon-btn__icon {\n margin: auto;\n width: 1.5em;\n height: 1.5em;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.icon-btn__label {\n font-size: 1em;\n white-space: nowrap;\n text-align: center;\n line-height: 2;\n opacity: 0;\n position: absolute;\n top: 100%;\n right: 0;\n left: 0;\n transform: translateY(0);\n}\n\n.icon-btn:focus-visible .icon-btn__back,\n.icon-btn:hover .icon-btn__back {\n transform: rotate(25deg) translate3d(-0.5em, -0.5em, 0.5em);\n}\n\n.icon-btn:focus-visible .icon-btn__front,\n.icon-btn:hover .icon-btn__front {\n transform: translate3d(0, 0, 2em);\n}\n\n.icon-btn:focus-visible .icon-btn__label,\n.icon-btn:hover .icon-btn__label {\n opacity: 1;\n transform: translateY(20%);\n}\n\n@media (min-width: 768px) {\n .icon-btns {\n grid-template-columns: repeat(3, 1fr);\n }\n}\n" }, { "type": "registry:component", - "path": "GlassIcons/GlassIcons.tsx", + "path": "GlassIcons.tsx", "content": "import React from 'react';\nimport './GlassIcons.css';\n\nexport interface GlassIconsItem {\n icon: React.ReactElement;\n color: string;\n label: string;\n customClass?: string;\n}\n\nexport interface GlassIconsProps {\n items: GlassIconsItem[];\n className?: string;\n}\n\nconst gradientMapping: Record = {\n blue: 'linear-gradient(hsl(223, 90%, 50%), hsl(208, 90%, 50%))',\n purple: 'linear-gradient(hsl(283, 90%, 50%), hsl(268, 90%, 50%))',\n red: 'linear-gradient(hsl(3, 90%, 50%), hsl(348, 90%, 50%))',\n indigo: 'linear-gradient(hsl(253, 90%, 50%), hsl(238, 90%, 50%))',\n orange: 'linear-gradient(hsl(43, 90%, 50%), hsl(28, 90%, 50%))',\n green: 'linear-gradient(hsl(123, 90%, 40%), hsl(108, 90%, 40%))'\n};\n\nconst GlassIcons: React.FC = ({ items, className }) => {\n const getBackgroundStyle = (color: string): React.CSSProperties => {\n if (gradientMapping[color]) {\n return { background: gradientMapping[color] };\n }\n return { background: color };\n };\n\n return (\n
\n {items.map((item, index) => (\n \n ))}\n
\n );\n};\n\nexport default GlassIcons;\n" } ], diff --git a/public/r/GlassSurface-JS-CSS.json b/public/r/GlassSurface-JS-CSS.json index 016ee530..2c3c1957 100644 --- a/public/r/GlassSurface-JS-CSS.json +++ b/public/r/GlassSurface-JS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GlassSurface/GlassSurface.css", + "type": "registry:file", + "path": "GlassSurface.css", + "target": "@components/GlassSurface.css", "content": ".glass-surface {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n overflow: hidden;\n transition: opacity 0.26s ease-out;\n}\n\n.glass-surface__filter {\n width: 100%;\n height: 100%;\n pointer-events: none;\n position: absolute;\n inset: 0;\n opacity: 0;\n z-index: -1;\n}\n\n.glass-surface__content {\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0.5rem;\n border-radius: inherit;\n position: relative;\n z-index: 1;\n}\n\n.glass-surface--svg {\n background: light-dark(hsl(0 0% 100% / var(--glass-frost, 0)), hsl(0 0% 0% / var(--glass-frost, 0)));\n backdrop-filter: var(--filter-id, url(#glass-filter)) saturate(var(--glass-saturation, 1));\n box-shadow:\n 0 0 2px 1px light-dark(color-mix(in oklch, black, transparent 85%), color-mix(in oklch, white, transparent 65%))\n inset,\n 0 0 10px 4px light-dark(color-mix(in oklch, black, transparent 90%), color-mix(in oklch, white, transparent 85%))\n inset,\n 0px 4px 16px rgba(17, 17, 26, 0.05),\n 0px 8px 24px rgba(17, 17, 26, 0.05),\n 0px 16px 56px rgba(17, 17, 26, 0.05),\n 0px 4px 16px rgba(17, 17, 26, 0.05) inset,\n 0px 8px 24px rgba(17, 17, 26, 0.05) inset,\n 0px 16px 56px rgba(17, 17, 26, 0.05) inset;\n}\n\n.glass-surface--fallback {\n background: rgba(255, 255, 255, 0.25);\n backdrop-filter: blur(12px) saturate(1.8) brightness(1.1);\n -webkit-backdrop-filter: blur(12px) saturate(1.8) brightness(1.1);\n border: 1px solid rgba(255, 255, 255, 0.3);\n box-shadow:\n 0 8px 32px 0 rgba(31, 38, 135, 0.2),\n 0 2px 16px 0 rgba(31, 38, 135, 0.1),\n inset 0 1px 0 0 rgba(255, 255, 255, 0.4),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.2);\n}\n\n@media (prefers-color-scheme: dark) {\n .glass-surface--fallback {\n background: rgba(255, 255, 255, 0.1);\n backdrop-filter: blur(12px) saturate(1.8) brightness(1.2);\n -webkit-backdrop-filter: blur(12px) saturate(1.8) brightness(1.2);\n border: 1px solid rgba(255, 255, 255, 0.2);\n box-shadow:\n inset 0 1px 0 0 rgba(255, 255, 255, 0.2),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.1);\n }\n}\n\n@supports not (backdrop-filter: blur(10px)) {\n .glass-surface--fallback {\n background: rgba(255, 255, 255, 0.4);\n box-shadow:\n inset 0 1px 0 0 rgba(255, 255, 255, 0.5),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.3);\n }\n\n .glass-surface--fallback::before {\n content: '';\n position: absolute;\n inset: 0;\n background: rgba(255, 255, 255, 0.15);\n border-radius: inherit;\n z-index: -1;\n }\n}\n\n@supports not (backdrop-filter: blur(10px)) {\n @media (prefers-color-scheme: dark) {\n .glass-surface--fallback {\n background: rgba(0, 0, 0, 0.4);\n }\n\n .glass-surface--fallback::before {\n background: rgba(255, 255, 255, 0.05);\n }\n }\n}\n\n.glass-surface:focus-visible {\n outline: 2px solid light-dark(#007aff, #0a84ff);\n outline-offset: 2px;\n}\n" }, { "type": "registry:component", - "path": "GlassSurface/GlassSurface.jsx", - "content": "/* eslint-disable react-hooks/exhaustive-deps */\nimport { useEffect, useState, useRef, useId } from 'react';\nimport './GlassSurface.css';\n\nconst GlassSurface = ({\n children,\n width = 200,\n height = 80,\n borderRadius = 20,\n borderWidth = 0.07,\n brightness = 50,\n opacity = 0.93,\n blur = 11,\n displace = 0,\n backgroundOpacity = 0,\n saturation = 1,\n distortionScale = -180,\n redOffset = 0,\n greenOffset = 10,\n blueOffset = 20,\n xChannel = 'R',\n yChannel = 'G',\n mixBlendMode = 'difference',\n className = '',\n style = {}\n}) => {\n const uniqueId = useId().replace(/:/g, '-');\n const filterId = `glass-filter-${uniqueId}`;\n const redGradId = `red-grad-${uniqueId}`;\n const blueGradId = `blue-grad-${uniqueId}`;\n\n const [svgSupported, setSvgSupported] = useState(false);\n\n const containerRef = useRef(null);\n const feImageRef = useRef(null);\n const redChannelRef = useRef(null);\n const greenChannelRef = useRef(null);\n const blueChannelRef = useRef(null);\n const gaussianBlurRef = useRef(null);\n\n const generateDisplacementMap = () => {\n const rect = containerRef.current?.getBoundingClientRect();\n const actualWidth = rect?.width || 400;\n const actualHeight = rect?.height || 200;\n const edgeSize = Math.min(actualWidth, actualHeight) * (borderWidth * 0.5);\n\n const svgContent = `\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n `;\n\n return `data:image/svg+xml,${encodeURIComponent(svgContent)}`;\n };\n\n const updateDisplacementMap = () => {\n feImageRef.current?.setAttribute('href', generateDisplacementMap());\n };\n\n useEffect(() => {\n updateDisplacementMap();\n [\n { ref: redChannelRef, offset: redOffset },\n { ref: greenChannelRef, offset: greenOffset },\n { ref: blueChannelRef, offset: blueOffset }\n ].forEach(({ ref, offset }) => {\n if (ref.current) {\n ref.current.setAttribute('scale', (distortionScale + offset).toString());\n ref.current.setAttribute('xChannelSelector', xChannel);\n ref.current.setAttribute('yChannelSelector', yChannel);\n }\n });\n\n gaussianBlurRef.current?.setAttribute('stdDeviation', displace.toString());\n }, [\n width,\n height,\n borderRadius,\n borderWidth,\n brightness,\n opacity,\n blur,\n displace,\n distortionScale,\n redOffset,\n greenOffset,\n blueOffset,\n xChannel,\n yChannel,\n mixBlendMode\n ]);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const resizeObserver = new ResizeObserver(() => {\n setTimeout(updateDisplacementMap, 0);\n });\n\n resizeObserver.observe(containerRef.current);\n\n return () => {\n resizeObserver.disconnect();\n };\n }, []);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const resizeObserver = new ResizeObserver(() => {\n setTimeout(updateDisplacementMap, 0);\n });\n\n resizeObserver.observe(containerRef.current);\n\n return () => {\n resizeObserver.disconnect();\n };\n }, []);\n\n useEffect(() => {\n setTimeout(updateDisplacementMap, 0);\n }, [width, height]);\n\n useEffect(() => {\n setSvgSupported(supportsSVGFilters());\n }, []);\n\n const supportsSVGFilters = () => {\n if (typeof window === 'undefined' || typeof document === 'undefined') {\n return false;\n }\n\n const isWebkit = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);\n const isFirefox = /Firefox/.test(navigator.userAgent);\n\n if (isWebkit || isFirefox) {\n return false;\n }\n\n const div = document.createElement('div');\n div.style.backdropFilter = `url(#${filterId})`;\n\n return div.style.backdropFilter !== '';\n };\n\n const containerStyle = {\n ...style,\n width: typeof width === 'number' ? `${width}px` : width,\n height: typeof height === 'number' ? `${height}px` : height,\n borderRadius: `${borderRadius}px`,\n '--glass-frost': backgroundOpacity,\n '--glass-saturation': saturation,\n '--filter-id': `url(#${filterId})`\n };\n\n return (\n \n \n \n \n \n\n \n \n\n \n \n\n \n \n\n \n \n \n \n \n \n\n
{children}
\n
\n );\n};\n\nexport default GlassSurface;\n" + "path": "GlassSurface.jsx", + "content": "/* eslint-disable react-hooks/exhaustive-deps */\nimport { useEffect, useState, useRef, useId } from 'react';\nimport './GlassSurface.css';\n\nconst GlassSurface = ({\n children,\n width = 200,\n height = 80,\n borderRadius = 20,\n borderWidth = 0.07,\n brightness = 50,\n opacity = 0.93,\n blur = 11,\n displace = 0,\n backgroundOpacity = 0,\n saturation = 1,\n distortionScale = -180,\n redOffset = 0,\n greenOffset = 10,\n blueOffset = 20,\n xChannel = 'R',\n yChannel = 'G',\n mixBlendMode = 'difference',\n className = '',\n style = {}\n}) => {\n const uniqueId = useId().replace(/:/g, '-');\n const filterId = `glass-filter-${uniqueId}`;\n const redGradId = `red-grad-${uniqueId}`;\n const blueGradId = `blue-grad-${uniqueId}`;\n\n const [svgSupported, setSvgSupported] = useState(false);\n\n const containerRef = useRef(null);\n const feImageRef = useRef(null);\n const redChannelRef = useRef(null);\n const greenChannelRef = useRef(null);\n const blueChannelRef = useRef(null);\n const gaussianBlurRef = useRef(null);\n\n const generateDisplacementMap = () => {\n const rect = containerRef.current?.getBoundingClientRect();\n const actualWidth = rect?.width || 400;\n const actualHeight = rect?.height || 200;\n const edgeSize = Math.min(actualWidth, actualHeight) * (borderWidth * 0.5);\n\n const svgContent = `\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n `;\n\n return `data:image/svg+xml,${encodeURIComponent(svgContent)}`;\n };\n\n const updateDisplacementMap = () => {\n feImageRef.current?.setAttribute('href', generateDisplacementMap());\n };\n\n useEffect(() => {\n updateDisplacementMap();\n [\n { ref: redChannelRef, offset: redOffset },\n { ref: greenChannelRef, offset: greenOffset },\n { ref: blueChannelRef, offset: blueOffset }\n ].forEach(({ ref, offset }) => {\n if (ref.current) {\n ref.current.setAttribute('scale', (distortionScale + offset).toString());\n ref.current.setAttribute('xChannelSelector', xChannel);\n ref.current.setAttribute('yChannelSelector', yChannel);\n }\n });\n\n gaussianBlurRef.current?.setAttribute('stdDeviation', displace.toString());\n }, [\n width,\n height,\n borderRadius,\n borderWidth,\n brightness,\n opacity,\n blur,\n displace,\n distortionScale,\n redOffset,\n greenOffset,\n blueOffset,\n xChannel,\n yChannel,\n mixBlendMode\n ]);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const resizeObserver = new ResizeObserver(() => {\n setTimeout(updateDisplacementMap, 0);\n });\n\n resizeObserver.observe(containerRef.current);\n\n return () => {\n resizeObserver.disconnect();\n };\n }, []);\n\n useEffect(() => {\n setTimeout(updateDisplacementMap, 0);\n }, [width, height]);\n\n useEffect(() => {\n setSvgSupported(supportsSVGFilters());\n }, []);\n\n const supportsSVGFilters = () => {\n if (typeof window === 'undefined' || typeof document === 'undefined') {\n return false;\n }\n\n const isWebkit = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);\n const isFirefox = /Firefox/.test(navigator.userAgent);\n\n if (isWebkit || isFirefox) {\n return false;\n }\n\n const div = document.createElement('div');\n div.style.backdropFilter = `url(#${filterId})`;\n\n return div.style.backdropFilter !== '';\n };\n\n const containerStyle = {\n ...style,\n width: typeof width === 'number' ? `${width}px` : width,\n height: typeof height === 'number' ? `${height}px` : height,\n borderRadius: `${borderRadius}px`,\n '--glass-frost': backgroundOpacity,\n '--glass-saturation': saturation,\n '--filter-id': `url(#${filterId})`\n };\n\n return (\n \n \n \n \n \n\n \n \n\n \n \n\n \n \n\n \n \n \n \n \n \n\n
{children}
\n
\n );\n};\n\nexport default GlassSurface;\n" } ], "registryDependencies": [], diff --git a/public/r/GlassSurface-JS-TW.json b/public/r/GlassSurface-JS-TW.json index f9aa138d..db8fa935 100644 --- a/public/r/GlassSurface-JS-TW.json +++ b/public/r/GlassSurface-JS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "GlassSurface/GlassSurface.jsx", - "content": "/* eslint-disable react-hooks/exhaustive-deps */\nimport { useEffect, useRef, useState, useId } from 'react';\n\nconst useDarkMode = () => {\n const [isDark, setIsDark] = useState(false);\n\n useEffect(() => {\n if (typeof window === 'undefined') return;\n\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n setIsDark(mediaQuery.matches);\n\n const handler = e => setIsDark(e.matches);\n mediaQuery.addEventListener('change', handler);\n return () => mediaQuery.removeEventListener('change', handler);\n }, []);\n\n return isDark;\n};\n\nconst GlassSurface = ({\n children,\n width = 200,\n height = 80,\n borderRadius = 20,\n borderWidth = 0.07,\n brightness = 50,\n opacity = 0.93,\n blur = 11,\n displace = 0,\n backgroundOpacity = 0,\n saturation = 1,\n distortionScale = -180,\n redOffset = 0,\n greenOffset = 10,\n blueOffset = 20,\n xChannel = 'R',\n yChannel = 'G',\n mixBlendMode = 'difference',\n className = '',\n style = {}\n}) => {\n const uniqueId = useId().replace(/:/g, '-');\n const filterId = `glass-filter-${uniqueId}`;\n const redGradId = `red-grad-${uniqueId}`;\n const blueGradId = `blue-grad-${uniqueId}`;\n\n const [svgSupported, setSvgSupported] = useState(false);\n\n const containerRef = useRef < HTMLDivElement > null;\n const feImageRef = useRef < SVGFEImageElement > null;\n const redChannelRef = useRef < SVGFEDisplacementMapElement > null;\n const greenChannelRef = useRef < SVGFEDisplacementMapElement > null;\n const blueChannelRef = useRef < SVGFEDisplacementMapElement > null;\n const gaussianBlurRef = useRef < SVGFEGaussianBlurElement > null;\n\n const isDarkMode = useDarkMode();\n\n const generateDisplacementMap = () => {\n const rect = containerRef.current?.getBoundingClientRect();\n const actualWidth = rect?.width || 400;\n const actualHeight = rect?.height || 200;\n const edgeSize = Math.min(actualWidth, actualHeight) * (borderWidth * 0.5);\n\n const svgContent = `\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n `;\n\n return `data:image/svg+xml,${encodeURIComponent(svgContent)}`;\n };\n\n const updateDisplacementMap = () => {\n feImageRef.current?.setAttribute('href', generateDisplacementMap());\n };\n\n useEffect(() => {\n updateDisplacementMap();\n [\n { ref: redChannelRef, offset: redOffset },\n { ref: greenChannelRef, offset: greenOffset },\n { ref: blueChannelRef, offset: blueOffset }\n ].forEach(({ ref, offset }) => {\n if (ref.current) {\n ref.current.setAttribute('scale', (distortionScale + offset).toString());\n ref.current.setAttribute('xChannelSelector', xChannel);\n ref.current.setAttribute('yChannelSelector', yChannel);\n }\n });\n\n gaussianBlurRef.current?.setAttribute('stdDeviation', displace.toString());\n }, [\n width,\n height,\n borderRadius,\n borderWidth,\n brightness,\n opacity,\n blur,\n displace,\n distortionScale,\n redOffset,\n greenOffset,\n blueOffset,\n xChannel,\n yChannel,\n mixBlendMode\n ]);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const resizeObserver = new ResizeObserver(() => {\n setTimeout(updateDisplacementMap, 0);\n });\n\n resizeObserver.observe(containerRef.current);\n\n return () => {\n resizeObserver.disconnect();\n };\n }, []);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const resizeObserver = new ResizeObserver(() => {\n setTimeout(updateDisplacementMap, 0);\n });\n\n resizeObserver.observe(containerRef.current);\n\n return () => {\n resizeObserver.disconnect();\n };\n }, []);\n\n useEffect(() => {\n setTimeout(updateDisplacementMap, 0);\n }, [width, height]);\n\n useEffect(() => {\n setSvgSupported(supportsSVGFilters());\n }, []);\n\n const supportsSVGFilters = () => {\n if (typeof window === 'undefined' || typeof document === 'undefined') {\n return false;\n }\n\n const isWebkit = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);\n const isFirefox = /Firefox/.test(navigator.userAgent);\n\n if (isWebkit || isFirefox) {\n return false;\n }\n\n const div = document.createElement('div');\n div.style.backdropFilter = `url(#${filterId})`;\n\n return div.style.backdropFilter !== '';\n };\n\n const supportsBackdropFilter = () => {\n if (typeof window === 'undefined') return false;\n return CSS.supports('backdrop-filter', 'blur(10px)');\n };\n\n const getContainerStyles = () => {\n const baseStyles = {\n ...style,\n width: typeof width === 'number' ? `${width}px` : width,\n height: typeof height === 'number' ? `${height}px` : height,\n borderRadius: `${borderRadius}px`,\n '--glass-frost': backgroundOpacity,\n '--glass-saturation': saturation\n };\n\n const backdropFilterSupported = supportsBackdropFilter();\n\n if (svgSupported) {\n return {\n ...baseStyles,\n background: isDarkMode ? `hsl(0 0% 0% / ${backgroundOpacity})` : `hsl(0 0% 100% / ${backgroundOpacity})`,\n backdropFilter: `url(#${filterId}) saturate(${saturation})`,\n boxShadow: isDarkMode\n ? `0 0 2px 1px color-mix(in oklch, white, transparent 65%) inset,\n 0 0 10px 4px color-mix(in oklch, white, transparent 85%) inset,\n 0px 4px 16px rgba(17, 17, 26, 0.05),\n 0px 8px 24px rgba(17, 17, 26, 0.05),\n 0px 16px 56px rgba(17, 17, 26, 0.05),\n 0px 4px 16px rgba(17, 17, 26, 0.05) inset,\n 0px 8px 24px rgba(17, 17, 26, 0.05) inset,\n 0px 16px 56px rgba(17, 17, 26, 0.05) inset`\n : `0 0 2px 1px color-mix(in oklch, black, transparent 85%) inset,\n 0 0 10px 4px color-mix(in oklch, black, transparent 90%) inset,\n 0px 4px 16px rgba(17, 17, 26, 0.05),\n 0px 8px 24px rgba(17, 17, 26, 0.05),\n 0px 16px 56px rgba(17, 17, 26, 0.05),\n 0px 4px 16px rgba(17, 17, 26, 0.05) inset,\n 0px 8px 24px rgba(17, 17, 26, 0.05) inset,\n 0px 16px 56px rgba(17, 17, 26, 0.05) inset`\n };\n } else {\n if (isDarkMode) {\n if (!backdropFilterSupported) {\n return {\n ...baseStyles,\n background: 'rgba(0, 0, 0, 0.4)',\n border: '1px solid rgba(255, 255, 255, 0.2)',\n boxShadow: `inset 0 1px 0 0 rgba(255, 255, 255, 0.2),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.1)`\n };\n } else {\n return {\n ...baseStyles,\n background: 'rgba(255, 255, 255, 0.1)',\n backdropFilter: 'blur(12px) saturate(1.8) brightness(1.2)',\n WebkitBackdropFilter: 'blur(12px) saturate(1.8) brightness(1.2)',\n border: '1px solid rgba(255, 255, 255, 0.2)',\n boxShadow: `inset 0 1px 0 0 rgba(255, 255, 255, 0.2),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.1)`\n };\n }\n } else {\n if (!backdropFilterSupported) {\n return {\n ...baseStyles,\n background: 'rgba(255, 255, 255, 0.4)',\n border: '1px solid rgba(255, 255, 255, 0.3)',\n boxShadow: `inset 0 1px 0 0 rgba(255, 255, 255, 0.5),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.3)`\n };\n } else {\n return {\n ...baseStyles,\n background: 'rgba(255, 255, 255, 0.25)',\n backdropFilter: 'blur(12px) saturate(1.8) brightness(1.1)',\n WebkitBackdropFilter: 'blur(12px) saturate(1.8) brightness(1.1)',\n border: '1px solid rgba(255, 255, 255, 0.3)',\n boxShadow: `0 8px 32px 0 rgba(31, 38, 135, 0.2),\n 0 2px 16px 0 rgba(31, 38, 135, 0.1),\n inset 0 1px 0 0 rgba(255, 255, 255, 0.4),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.2)`\n };\n }\n }\n }\n };\n\n const glassSurfaceClasses =\n 'relative flex items-center justify-center overflow-hidden transition-opacity duration-[260ms] ease-out';\n\n const focusVisibleClasses = isDarkMode\n ? 'focus-visible:outline-2 focus-visible:outline-[#0A84FF] focus-visible:outline-offset-2'\n : 'focus-visible:outline-2 focus-visible:outline-[#007AFF] focus-visible:outline-offset-2';\n\n return (\n \n \n \n \n \n\n \n \n\n \n \n\n \n \n\n \n \n \n \n \n \n\n
\n {children}\n
\n
\n );\n};\n\nexport default GlassSurface;\n" + "content": "/* eslint-disable react-hooks/exhaustive-deps */\nimport { useEffect, useRef, useState, useId } from 'react';\n\nconst useDarkMode = () => {\n const [isDark, setIsDark] = useState(false);\n\n useEffect(() => {\n if (typeof window === 'undefined') return;\n\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n setIsDark(mediaQuery.matches);\n\n const handler = e => setIsDark(e.matches);\n mediaQuery.addEventListener('change', handler);\n return () => mediaQuery.removeEventListener('change', handler);\n }, []);\n\n return isDark;\n};\n\nconst GlassSurface = ({\n children,\n width = 200,\n height = 80,\n borderRadius = 20,\n borderWidth = 0.07,\n brightness = 50,\n opacity = 0.93,\n blur = 11,\n displace = 0,\n backgroundOpacity = 0,\n saturation = 1,\n distortionScale = -180,\n redOffset = 0,\n greenOffset = 10,\n blueOffset = 20,\n xChannel = 'R',\n yChannel = 'G',\n mixBlendMode = 'difference',\n className = '',\n style = {}\n}) => {\n const uniqueId = useId().replace(/:/g, '-');\n const filterId = `glass-filter-${uniqueId}`;\n const redGradId = `red-grad-${uniqueId}`;\n const blueGradId = `blue-grad-${uniqueId}`;\n\n const [svgSupported, setSvgSupported] = useState(false);\n\n const containerRef = useRef < HTMLDivElement > null;\n const feImageRef = useRef < SVGFEImageElement > null;\n const redChannelRef = useRef < SVGFEDisplacementMapElement > null;\n const greenChannelRef = useRef < SVGFEDisplacementMapElement > null;\n const blueChannelRef = useRef < SVGFEDisplacementMapElement > null;\n const gaussianBlurRef = useRef < SVGFEGaussianBlurElement > null;\n\n const isDarkMode = useDarkMode();\n\n const generateDisplacementMap = () => {\n const rect = containerRef.current?.getBoundingClientRect();\n const actualWidth = rect?.width || 400;\n const actualHeight = rect?.height || 200;\n const edgeSize = Math.min(actualWidth, actualHeight) * (borderWidth * 0.5);\n\n const svgContent = `\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n `;\n\n return `data:image/svg+xml,${encodeURIComponent(svgContent)}`;\n };\n\n const updateDisplacementMap = () => {\n feImageRef.current?.setAttribute('href', generateDisplacementMap());\n };\n\n useEffect(() => {\n updateDisplacementMap();\n [\n { ref: redChannelRef, offset: redOffset },\n { ref: greenChannelRef, offset: greenOffset },\n { ref: blueChannelRef, offset: blueOffset }\n ].forEach(({ ref, offset }) => {\n if (ref.current) {\n ref.current.setAttribute('scale', (distortionScale + offset).toString());\n ref.current.setAttribute('xChannelSelector', xChannel);\n ref.current.setAttribute('yChannelSelector', yChannel);\n }\n });\n\n gaussianBlurRef.current?.setAttribute('stdDeviation', displace.toString());\n }, [\n width,\n height,\n borderRadius,\n borderWidth,\n brightness,\n opacity,\n blur,\n displace,\n distortionScale,\n redOffset,\n greenOffset,\n blueOffset,\n xChannel,\n yChannel,\n mixBlendMode\n ]);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const resizeObserver = new ResizeObserver(() => {\n setTimeout(updateDisplacementMap, 0);\n });\n\n resizeObserver.observe(containerRef.current);\n\n return () => {\n resizeObserver.disconnect();\n };\n }, []);\n\n useEffect(() => {\n setTimeout(updateDisplacementMap, 0);\n }, [width, height]);\n\n useEffect(() => {\n setSvgSupported(supportsSVGFilters());\n }, []);\n\n const supportsSVGFilters = () => {\n if (typeof window === 'undefined' || typeof document === 'undefined') {\n return false;\n }\n\n const isWebkit = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);\n const isFirefox = /Firefox/.test(navigator.userAgent);\n\n if (isWebkit || isFirefox) {\n return false;\n }\n\n const div = document.createElement('div');\n div.style.backdropFilter = `url(#${filterId})`;\n\n return div.style.backdropFilter !== '';\n };\n\n const supportsBackdropFilter = () => {\n if (typeof window === 'undefined') return false;\n return CSS.supports('backdrop-filter', 'blur(10px)');\n };\n\n const getContainerStyles = () => {\n const baseStyles = {\n ...style,\n width: typeof width === 'number' ? `${width}px` : width,\n height: typeof height === 'number' ? `${height}px` : height,\n borderRadius: `${borderRadius}px`,\n '--glass-frost': backgroundOpacity,\n '--glass-saturation': saturation\n };\n\n const backdropFilterSupported = supportsBackdropFilter();\n\n if (svgSupported) {\n return {\n ...baseStyles,\n background: isDarkMode ? `hsl(0 0% 0% / ${backgroundOpacity})` : `hsl(0 0% 100% / ${backgroundOpacity})`,\n backdropFilter: `url(#${filterId}) saturate(${saturation})`,\n boxShadow: isDarkMode\n ? `0 0 2px 1px color-mix(in oklch, white, transparent 65%) inset,\n 0 0 10px 4px color-mix(in oklch, white, transparent 85%) inset,\n 0px 4px 16px rgba(17, 17, 26, 0.05),\n 0px 8px 24px rgba(17, 17, 26, 0.05),\n 0px 16px 56px rgba(17, 17, 26, 0.05),\n 0px 4px 16px rgba(17, 17, 26, 0.05) inset,\n 0px 8px 24px rgba(17, 17, 26, 0.05) inset,\n 0px 16px 56px rgba(17, 17, 26, 0.05) inset`\n : `0 0 2px 1px color-mix(in oklch, black, transparent 85%) inset,\n 0 0 10px 4px color-mix(in oklch, black, transparent 90%) inset,\n 0px 4px 16px rgba(17, 17, 26, 0.05),\n 0px 8px 24px rgba(17, 17, 26, 0.05),\n 0px 16px 56px rgba(17, 17, 26, 0.05),\n 0px 4px 16px rgba(17, 17, 26, 0.05) inset,\n 0px 8px 24px rgba(17, 17, 26, 0.05) inset,\n 0px 16px 56px rgba(17, 17, 26, 0.05) inset`\n };\n } else {\n if (isDarkMode) {\n if (!backdropFilterSupported) {\n return {\n ...baseStyles,\n background: 'rgba(0, 0, 0, 0.4)',\n border: '1px solid rgba(255, 255, 255, 0.2)',\n boxShadow: `inset 0 1px 0 0 rgba(255, 255, 255, 0.2),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.1)`\n };\n } else {\n return {\n ...baseStyles,\n background: 'rgba(255, 255, 255, 0.1)',\n backdropFilter: 'blur(12px) saturate(1.8) brightness(1.2)',\n WebkitBackdropFilter: 'blur(12px) saturate(1.8) brightness(1.2)',\n border: '1px solid rgba(255, 255, 255, 0.2)',\n boxShadow: `inset 0 1px 0 0 rgba(255, 255, 255, 0.2),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.1)`\n };\n }\n } else {\n if (!backdropFilterSupported) {\n return {\n ...baseStyles,\n background: 'rgba(255, 255, 255, 0.4)',\n border: '1px solid rgba(255, 255, 255, 0.3)',\n boxShadow: `inset 0 1px 0 0 rgba(255, 255, 255, 0.5),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.3)`\n };\n } else {\n return {\n ...baseStyles,\n background: 'rgba(255, 255, 255, 0.25)',\n backdropFilter: 'blur(12px) saturate(1.8) brightness(1.1)',\n WebkitBackdropFilter: 'blur(12px) saturate(1.8) brightness(1.1)',\n border: '1px solid rgba(255, 255, 255, 0.3)',\n boxShadow: `0 8px 32px 0 rgba(31, 38, 135, 0.2),\n 0 2px 16px 0 rgba(31, 38, 135, 0.1),\n inset 0 1px 0 0 rgba(255, 255, 255, 0.4),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.2)`\n };\n }\n }\n }\n };\n\n const glassSurfaceClasses =\n 'relative flex items-center justify-center overflow-hidden transition-opacity duration-[260ms] ease-out';\n\n const focusVisibleClasses = isDarkMode\n ? 'focus-visible:outline-2 focus-visible:outline-[#0A84FF] focus-visible:outline-offset-2'\n : 'focus-visible:outline-2 focus-visible:outline-[#007AFF] focus-visible:outline-offset-2';\n\n return (\n \n \n \n \n \n\n \n \n\n \n \n\n \n \n\n \n \n \n \n \n \n\n
\n {children}\n
\n
\n );\n};\n\nexport default GlassSurface;\n" } ], "registryDependencies": [], diff --git a/public/r/GlassSurface-TS-CSS.json b/public/r/GlassSurface-TS-CSS.json index ea2faeb8..ae3afc0c 100644 --- a/public/r/GlassSurface-TS-CSS.json +++ b/public/r/GlassSurface-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GlassSurface/GlassSurface.css", + "type": "registry:file", + "path": "GlassSurface.css", + "target": "@components/GlassSurface.css", "content": ".glass-surface {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n overflow: hidden;\n transition: opacity 0.26s ease-out;\n}\n\n.glass-surface__filter {\n width: 100%;\n height: 100%;\n pointer-events: none;\n position: absolute;\n inset: 0;\n opacity: 0;\n z-index: -1;\n}\n\n.glass-surface__content {\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0.5rem;\n border-radius: inherit;\n position: relative;\n z-index: 1;\n}\n\n.glass-surface--svg {\n background: light-dark(hsl(0 0% 100% / var(--glass-frost, 0)), hsl(0 0% 0% / var(--glass-frost, 0)));\n backdrop-filter: var(--filter-id) saturate(var(--glass-saturation, 1));\n box-shadow:\n 0 0 2px 1px light-dark(color-mix(in oklch, black, transparent 85%), color-mix(in oklch, white, transparent 65%))\n inset,\n 0 0 10px 4px light-dark(color-mix(in oklch, black, transparent 90%), color-mix(in oklch, white, transparent 85%))\n inset,\n 0px 4px 16px rgba(17, 17, 26, 0.05),\n 0px 8px 24px rgba(17, 17, 26, 0.05),\n 0px 16px 56px rgba(17, 17, 26, 0.05),\n 0px 4px 16px rgba(17, 17, 26, 0.05) inset,\n 0px 8px 24px rgba(17, 17, 26, 0.05) inset,\n 0px 16px 56px rgba(17, 17, 26, 0.05) inset;\n}\n\n.glass-surface--fallback {\n background: rgba(255, 255, 255, 0.25);\n backdrop-filter: blur(12px) saturate(1.8) brightness(1.1);\n -webkit-backdrop-filter: blur(12px) saturate(1.8) brightness(1.1);\n border: 1px solid rgba(255, 255, 255, 0.3);\n box-shadow:\n 0 8px 32px 0 rgba(31, 38, 135, 0.2),\n 0 2px 16px 0 rgba(31, 38, 135, 0.1),\n inset 0 1px 0 0 rgba(255, 255, 255, 0.4),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.2);\n}\n\n@media (prefers-color-scheme: dark) {\n .glass-surface--fallback {\n background: rgba(255, 255, 255, 0.1);\n backdrop-filter: blur(12px) saturate(1.8) brightness(1.2);\n -webkit-backdrop-filter: blur(12px) saturate(1.8) brightness(1.2);\n border: 1px solid rgba(255, 255, 255, 0.2);\n box-shadow:\n inset 0 1px 0 0 rgba(255, 255, 255, 0.2),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.1);\n }\n}\n\n@supports not (backdrop-filter: blur(10px)) {\n .glass-surface--fallback {\n background: rgba(255, 255, 255, 0.4);\n box-shadow:\n inset 0 1px 0 0 rgba(255, 255, 255, 0.5),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.3);\n }\n\n .glass-surface--fallback::before {\n content: '';\n position: absolute;\n inset: 0;\n background: rgba(255, 255, 255, 0.15);\n border-radius: inherit;\n z-index: -1;\n }\n}\n\n@supports not (backdrop-filter: blur(10px)) {\n @media (prefers-color-scheme: dark) {\n .glass-surface--fallback {\n background: rgba(0, 0, 0, 0.4);\n }\n\n .glass-surface--fallback::before {\n background: rgba(255, 255, 255, 0.05);\n }\n }\n}\n\n.glass-surface:focus-visible {\n outline: 2px solid light-dark(#007aff, #0a84ff);\n outline-offset: 2px;\n}\n" }, { "type": "registry:component", - "path": "GlassSurface/GlassSurface.tsx", - "content": "import React, { useEffect, useRef, useState, useId } from 'react';\nimport './GlassSurface.css';\n\nexport interface GlassSurfaceProps {\n children?: React.ReactNode;\n width?: number | string;\n height?: number | string;\n borderRadius?: number;\n borderWidth?: number;\n brightness?: number;\n opacity?: number;\n blur?: number;\n displace?: number;\n backgroundOpacity?: number;\n saturation?: number;\n distortionScale?: number;\n redOffset?: number;\n greenOffset?: number;\n blueOffset?: number;\n xChannel?: 'R' | 'G' | 'B';\n yChannel?: 'R' | 'G' | 'B';\n mixBlendMode?:\n | 'normal'\n | 'multiply'\n | 'screen'\n | 'overlay'\n | 'darken'\n | 'lighten'\n | 'color-dodge'\n | 'color-burn'\n | 'hard-light'\n | 'soft-light'\n | 'difference'\n | 'exclusion'\n | 'hue'\n | 'saturation'\n | 'color'\n | 'luminosity'\n | 'plus-darker'\n | 'plus-lighter';\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst GlassSurface: React.FC = ({\n children,\n width = 200,\n height = 80,\n borderRadius = 20,\n borderWidth = 0.07,\n brightness = 50,\n opacity = 0.93,\n blur = 11,\n displace = 0,\n backgroundOpacity = 0,\n saturation = 1,\n distortionScale = -180,\n redOffset = 0,\n greenOffset = 10,\n blueOffset = 20,\n xChannel = 'R',\n yChannel = 'G',\n mixBlendMode = 'difference',\n className = '',\n style = {}\n}) => {\n const id = useId();\n const filterId = `glass-filter-${id}`;\n const redGradId = `red-grad-${id}`;\n const blueGradId = `blue-grad-${id}`;\n\n const [svgSupported, setSvgSupported] = useState(false);\n\n const containerRef = useRef(null);\n const feImageRef = useRef(null);\n const redChannelRef = useRef(null);\n const greenChannelRef = useRef(null);\n const blueChannelRef = useRef(null);\n const gaussianBlurRef = useRef(null);\n\n const generateDisplacementMap = () => {\n const rect = containerRef.current?.getBoundingClientRect();\n const actualWidth = rect?.width || 400;\n const actualHeight = rect?.height || 200;\n const edgeSize = Math.min(actualWidth, actualHeight) * (borderWidth * 0.5);\n\n const svgContent = `\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n `;\n\n return `data:image/svg+xml,${encodeURIComponent(svgContent)}`;\n };\n\n const updateDisplacementMap = () => {\n feImageRef.current?.setAttribute('href', generateDisplacementMap());\n };\n\n useEffect(() => {\n updateDisplacementMap();\n [\n { ref: redChannelRef, offset: redOffset },\n { ref: greenChannelRef, offset: greenOffset },\n { ref: blueChannelRef, offset: blueOffset }\n ].forEach(({ ref, offset }) => {\n if (ref.current) {\n ref.current.setAttribute('scale', (distortionScale + offset).toString());\n ref.current.setAttribute('xChannelSelector', xChannel);\n ref.current.setAttribute('yChannelSelector', yChannel);\n }\n });\n\n gaussianBlurRef.current?.setAttribute('stdDeviation', displace.toString());\n }, [\n width,\n height,\n borderRadius,\n borderWidth,\n brightness,\n opacity,\n blur,\n displace,\n distortionScale,\n redOffset,\n greenOffset,\n blueOffset,\n xChannel,\n yChannel,\n mixBlendMode\n ]);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const resizeObserver = new ResizeObserver(() => {\n setTimeout(updateDisplacementMap, 0);\n });\n\n resizeObserver.observe(containerRef.current);\n\n return () => {\n resizeObserver.disconnect();\n };\n }, []);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const resizeObserver = new ResizeObserver(() => {\n setTimeout(updateDisplacementMap, 0);\n });\n\n resizeObserver.observe(containerRef.current);\n\n return () => {\n resizeObserver.disconnect();\n };\n }, []);\n\n useEffect(() => {\n setTimeout(updateDisplacementMap, 0);\n }, [width, height]);\n\n useEffect(() => {\n setSvgSupported(supportsSVGFilters());\n }, []);\n\n const supportsSVGFilters = () => {\n if (typeof window === 'undefined' || typeof document === 'undefined') {\n return false;\n }\n\n const isWebkit = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);\n const isFirefox = /Firefox/.test(navigator.userAgent);\n\n if (isWebkit || isFirefox) {\n return false;\n }\n\n const div = document.createElement('div');\n div.style.backdropFilter = `url(#${filterId})`;\n\n return div.style.backdropFilter !== '';\n };\n\n const containerStyle: React.CSSProperties = {\n ...style,\n width: typeof width === 'number' ? `${width}px` : width,\n height: typeof height === 'number' ? `${height}px` : height,\n borderRadius: `${borderRadius}px`,\n '--glass-frost': backgroundOpacity,\n '--glass-saturation': saturation,\n '--filter-id': `url(#${filterId})`\n } as React.CSSProperties;\n\n return (\n \n \n \n \n \n\n \n \n\n \n \n\n \n \n\n \n \n \n \n \n \n\n
{children}
\n
\n );\n};\n\nexport default GlassSurface;\n" + "path": "GlassSurface.tsx", + "content": "import React, { useEffect, useRef, useState, useId } from 'react';\nimport './GlassSurface.css';\n\nexport interface GlassSurfaceProps {\n children?: React.ReactNode;\n width?: number | string;\n height?: number | string;\n borderRadius?: number;\n borderWidth?: number;\n brightness?: number;\n opacity?: number;\n blur?: number;\n displace?: number;\n backgroundOpacity?: number;\n saturation?: number;\n distortionScale?: number;\n redOffset?: number;\n greenOffset?: number;\n blueOffset?: number;\n xChannel?: 'R' | 'G' | 'B';\n yChannel?: 'R' | 'G' | 'B';\n mixBlendMode?:\n | 'normal'\n | 'multiply'\n | 'screen'\n | 'overlay'\n | 'darken'\n | 'lighten'\n | 'color-dodge'\n | 'color-burn'\n | 'hard-light'\n | 'soft-light'\n | 'difference'\n | 'exclusion'\n | 'hue'\n | 'saturation'\n | 'color'\n | 'luminosity'\n | 'plus-darker'\n | 'plus-lighter';\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst GlassSurface: React.FC = ({\n children,\n width = 200,\n height = 80,\n borderRadius = 20,\n borderWidth = 0.07,\n brightness = 50,\n opacity = 0.93,\n blur = 11,\n displace = 0,\n backgroundOpacity = 0,\n saturation = 1,\n distortionScale = -180,\n redOffset = 0,\n greenOffset = 10,\n blueOffset = 20,\n xChannel = 'R',\n yChannel = 'G',\n mixBlendMode = 'difference',\n className = '',\n style = {}\n}) => {\n const id = useId();\n const filterId = `glass-filter-${id}`;\n const redGradId = `red-grad-${id}`;\n const blueGradId = `blue-grad-${id}`;\n\n const [svgSupported, setSvgSupported] = useState(false);\n\n const containerRef = useRef(null);\n const feImageRef = useRef(null);\n const redChannelRef = useRef(null);\n const greenChannelRef = useRef(null);\n const blueChannelRef = useRef(null);\n const gaussianBlurRef = useRef(null);\n\n const generateDisplacementMap = () => {\n const rect = containerRef.current?.getBoundingClientRect();\n const actualWidth = rect?.width || 400;\n const actualHeight = rect?.height || 200;\n const edgeSize = Math.min(actualWidth, actualHeight) * (borderWidth * 0.5);\n\n const svgContent = `\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n `;\n\n return `data:image/svg+xml,${encodeURIComponent(svgContent)}`;\n };\n\n const updateDisplacementMap = () => {\n feImageRef.current?.setAttribute('href', generateDisplacementMap());\n };\n\n useEffect(() => {\n updateDisplacementMap();\n [\n { ref: redChannelRef, offset: redOffset },\n { ref: greenChannelRef, offset: greenOffset },\n { ref: blueChannelRef, offset: blueOffset }\n ].forEach(({ ref, offset }) => {\n if (ref.current) {\n ref.current.setAttribute('scale', (distortionScale + offset).toString());\n ref.current.setAttribute('xChannelSelector', xChannel);\n ref.current.setAttribute('yChannelSelector', yChannel);\n }\n });\n\n gaussianBlurRef.current?.setAttribute('stdDeviation', displace.toString());\n }, [\n width,\n height,\n borderRadius,\n borderWidth,\n brightness,\n opacity,\n blur,\n displace,\n distortionScale,\n redOffset,\n greenOffset,\n blueOffset,\n xChannel,\n yChannel,\n mixBlendMode\n ]);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const resizeObserver = new ResizeObserver(() => {\n setTimeout(updateDisplacementMap, 0);\n });\n\n resizeObserver.observe(containerRef.current);\n\n return () => {\n resizeObserver.disconnect();\n };\n }, []);\n\n useEffect(() => {\n setTimeout(updateDisplacementMap, 0);\n }, [width, height]);\n\n useEffect(() => {\n setSvgSupported(supportsSVGFilters());\n }, []);\n\n const supportsSVGFilters = () => {\n if (typeof window === 'undefined' || typeof document === 'undefined') {\n return false;\n }\n\n const isWebkit = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);\n const isFirefox = /Firefox/.test(navigator.userAgent);\n\n if (isWebkit || isFirefox) {\n return false;\n }\n\n const div = document.createElement('div');\n div.style.backdropFilter = `url(#${filterId})`;\n\n return div.style.backdropFilter !== '';\n };\n\n const containerStyle: React.CSSProperties = {\n ...style,\n width: typeof width === 'number' ? `${width}px` : width,\n height: typeof height === 'number' ? `${height}px` : height,\n borderRadius: `${borderRadius}px`,\n '--glass-frost': backgroundOpacity,\n '--glass-saturation': saturation,\n '--filter-id': `url(#${filterId})`\n } as React.CSSProperties;\n\n return (\n \n \n \n \n \n\n \n \n\n \n \n\n \n \n\n \n \n \n \n \n \n\n
{children}
\n
\n );\n};\n\nexport default GlassSurface;\n" } ], "registryDependencies": [], diff --git a/public/r/GlassSurface-TS-TW.json b/public/r/GlassSurface-TS-TW.json index 1a8a3f88..338bc422 100644 --- a/public/r/GlassSurface-TS-TW.json +++ b/public/r/GlassSurface-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "GlassSurface/GlassSurface.tsx", - "content": "import React, { useEffect, useRef, useState, useId } from 'react';\n\nexport interface GlassSurfaceProps {\n children?: React.ReactNode;\n width?: number | string;\n height?: number | string;\n borderRadius?: number;\n borderWidth?: number;\n brightness?: number;\n opacity?: number;\n blur?: number;\n displace?: number;\n backgroundOpacity?: number;\n saturation?: number;\n distortionScale?: number;\n redOffset?: number;\n greenOffset?: number;\n blueOffset?: number;\n xChannel?: 'R' | 'G' | 'B';\n yChannel?: 'R' | 'G' | 'B';\n mixBlendMode?:\n | 'normal'\n | 'multiply'\n | 'screen'\n | 'overlay'\n | 'darken'\n | 'lighten'\n | 'color-dodge'\n | 'color-burn'\n | 'hard-light'\n | 'soft-light'\n | 'difference'\n | 'exclusion'\n | 'hue'\n | 'saturation'\n | 'color'\n | 'luminosity'\n | 'plus-darker'\n | 'plus-lighter';\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst useDarkMode = () => {\n const [isDark, setIsDark] = useState(false);\n\n useEffect(() => {\n if (typeof window === 'undefined') return;\n\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n setIsDark(mediaQuery.matches);\n\n const handler = (e: MediaQueryListEvent) => setIsDark(e.matches);\n mediaQuery.addEventListener('change', handler);\n return () => mediaQuery.removeEventListener('change', handler);\n }, []);\n\n return isDark;\n};\n\nconst GlassSurface: React.FC = ({\n children,\n width = 200,\n height = 80,\n borderRadius = 20,\n borderWidth = 0.07,\n brightness = 50,\n opacity = 0.93,\n blur = 11,\n displace = 0,\n backgroundOpacity = 0,\n saturation = 1,\n distortionScale = -180,\n redOffset = 0,\n greenOffset = 10,\n blueOffset = 20,\n xChannel = 'R',\n yChannel = 'G',\n mixBlendMode = 'difference',\n className = '',\n style = {}\n}) => {\n const uniqueId = useId().replace(/:/g, '-');\n const filterId = `glass-filter-${uniqueId}`;\n const redGradId = `red-grad-${uniqueId}`;\n const blueGradId = `blue-grad-${uniqueId}`;\n\n const [svgSupported, setSvgSupported] = useState(false);\n\n const containerRef = useRef(null);\n const feImageRef = useRef(null);\n const redChannelRef = useRef(null);\n const greenChannelRef = useRef(null);\n const blueChannelRef = useRef(null);\n const gaussianBlurRef = useRef(null);\n\n const isDarkMode = useDarkMode();\n\n const generateDisplacementMap = () => {\n const rect = containerRef.current?.getBoundingClientRect();\n const actualWidth = rect?.width || 400;\n const actualHeight = rect?.height || 200;\n const edgeSize = Math.min(actualWidth, actualHeight) * (borderWidth * 0.5);\n\n const svgContent = `\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n `;\n\n return `data:image/svg+xml,${encodeURIComponent(svgContent)}`;\n };\n\n const updateDisplacementMap = () => {\n feImageRef.current?.setAttribute('href', generateDisplacementMap());\n };\n\n useEffect(() => {\n updateDisplacementMap();\n [\n { ref: redChannelRef, offset: redOffset },\n { ref: greenChannelRef, offset: greenOffset },\n { ref: blueChannelRef, offset: blueOffset }\n ].forEach(({ ref, offset }) => {\n if (ref.current) {\n ref.current.setAttribute('scale', (distortionScale + offset).toString());\n ref.current.setAttribute('xChannelSelector', xChannel);\n ref.current.setAttribute('yChannelSelector', yChannel);\n }\n });\n\n gaussianBlurRef.current?.setAttribute('stdDeviation', displace.toString());\n }, [\n width,\n height,\n borderRadius,\n borderWidth,\n brightness,\n opacity,\n blur,\n displace,\n distortionScale,\n redOffset,\n greenOffset,\n blueOffset,\n xChannel,\n yChannel,\n mixBlendMode\n ]);\n\n useEffect(() => {\n setSvgSupported(supportsSVGFilters());\n }, []);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const resizeObserver = new ResizeObserver(() => {\n setTimeout(updateDisplacementMap, 0);\n });\n\n resizeObserver.observe(containerRef.current);\n\n return () => {\n resizeObserver.disconnect();\n };\n }, []);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const resizeObserver = new ResizeObserver(() => {\n setTimeout(updateDisplacementMap, 0);\n });\n\n resizeObserver.observe(containerRef.current);\n\n return () => {\n resizeObserver.disconnect();\n };\n }, []);\n\n useEffect(() => {\n setTimeout(updateDisplacementMap, 0);\n }, [width, height]);\n\n const supportsSVGFilters = () => {\n if (typeof window === 'undefined' || typeof document === 'undefined') {\n return false;\n }\n\n const isWebkit = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);\n const isFirefox = /Firefox/.test(navigator.userAgent);\n\n if (isWebkit || isFirefox) {\n return false;\n }\n\n const div = document.createElement('div');\n div.style.backdropFilter = `url(#${filterId})`;\n\n return div.style.backdropFilter !== '';\n };\n\n const supportsBackdropFilter = () => {\n if (typeof window === 'undefined') return false;\n return CSS.supports('backdrop-filter', 'blur(10px)');\n };\n\n const getContainerStyles = (): React.CSSProperties => {\n const baseStyles: React.CSSProperties = {\n ...style,\n width: typeof width === 'number' ? `${width}px` : width,\n height: typeof height === 'number' ? `${height}px` : height,\n borderRadius: `${borderRadius}px`,\n '--glass-frost': backgroundOpacity,\n '--glass-saturation': saturation\n } as React.CSSProperties;\n\n const backdropFilterSupported = supportsBackdropFilter();\n\n if (svgSupported) {\n return {\n ...baseStyles,\n background: isDarkMode ? `hsl(0 0% 0% / ${backgroundOpacity})` : `hsl(0 0% 100% / ${backgroundOpacity})`,\n backdropFilter: `url(#${filterId}) saturate(${saturation})`,\n boxShadow: isDarkMode\n ? `0 0 2px 1px color-mix(in oklch, white, transparent 65%) inset,\n 0 0 10px 4px color-mix(in oklch, white, transparent 85%) inset,\n 0px 4px 16px rgba(17, 17, 26, 0.05),\n 0px 8px 24px rgba(17, 17, 26, 0.05),\n 0px 16px 56px rgba(17, 17, 26, 0.05),\n 0px 4px 16px rgba(17, 17, 26, 0.05) inset,\n 0px 8px 24px rgba(17, 17, 26, 0.05) inset,\n 0px 16px 56px rgba(17, 17, 26, 0.05) inset`\n : `0 0 2px 1px color-mix(in oklch, black, transparent 85%) inset,\n 0 0 10px 4px color-mix(in oklch, black, transparent 90%) inset,\n 0px 4px 16px rgba(17, 17, 26, 0.05),\n 0px 8px 24px rgba(17, 17, 26, 0.05),\n 0px 16px 56px rgba(17, 17, 26, 0.05),\n 0px 4px 16px rgba(17, 17, 26, 0.05) inset,\n 0px 8px 24px rgba(17, 17, 26, 0.05) inset,\n 0px 16px 56px rgba(17, 17, 26, 0.05) inset`\n };\n } else {\n if (isDarkMode) {\n if (!backdropFilterSupported) {\n return {\n ...baseStyles,\n background: 'rgba(0, 0, 0, 0.4)',\n border: '1px solid rgba(255, 255, 255, 0.2)',\n boxShadow: `inset 0 1px 0 0 rgba(255, 255, 255, 0.2),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.1)`\n };\n } else {\n return {\n ...baseStyles,\n background: 'rgba(255, 255, 255, 0.1)',\n backdropFilter: 'blur(12px) saturate(1.8) brightness(1.2)',\n WebkitBackdropFilter: 'blur(12px) saturate(1.8) brightness(1.2)',\n border: '1px solid rgba(255, 255, 255, 0.2)',\n boxShadow: `inset 0 1px 0 0 rgba(255, 255, 255, 0.2),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.1)`\n };\n }\n } else {\n if (!backdropFilterSupported) {\n return {\n ...baseStyles,\n background: 'rgba(255, 255, 255, 0.4)',\n border: '1px solid rgba(255, 255, 255, 0.3)',\n boxShadow: `inset 0 1px 0 0 rgba(255, 255, 255, 0.5),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.3)`\n };\n } else {\n return {\n ...baseStyles,\n background: 'rgba(255, 255, 255, 0.25)',\n backdropFilter: 'blur(12px) saturate(1.8) brightness(1.1)',\n WebkitBackdropFilter: 'blur(12px) saturate(1.8) brightness(1.1)',\n border: '1px solid rgba(255, 255, 255, 0.3)',\n boxShadow: `0 8px 32px 0 rgba(31, 38, 135, 0.2),\n 0 2px 16px 0 rgba(31, 38, 135, 0.1),\n inset 0 1px 0 0 rgba(255, 255, 255, 0.4),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.2)`\n };\n }\n }\n }\n };\n\n const glassSurfaceClasses =\n 'relative flex items-center justify-center overflow-hidden transition-opacity duration-[260ms] ease-out';\n\n const focusVisibleClasses = isDarkMode\n ? 'focus-visible:outline-2 focus-visible:outline-[#0A84FF] focus-visible:outline-offset-2'\n : 'focus-visible:outline-2 focus-visible:outline-[#007AFF] focus-visible:outline-offset-2';\n\n return (\n \n \n \n \n \n\n \n \n\n \n \n\n \n \n\n \n \n \n \n \n \n\n
\n {children}\n
\n
\n );\n};\n\nexport default GlassSurface;\n" + "content": "import React, { useEffect, useRef, useState, useId } from 'react';\n\nexport interface GlassSurfaceProps {\n children?: React.ReactNode;\n width?: number | string;\n height?: number | string;\n borderRadius?: number;\n borderWidth?: number;\n brightness?: number;\n opacity?: number;\n blur?: number;\n displace?: number;\n backgroundOpacity?: number;\n saturation?: number;\n distortionScale?: number;\n redOffset?: number;\n greenOffset?: number;\n blueOffset?: number;\n xChannel?: 'R' | 'G' | 'B';\n yChannel?: 'R' | 'G' | 'B';\n mixBlendMode?:\n | 'normal'\n | 'multiply'\n | 'screen'\n | 'overlay'\n | 'darken'\n | 'lighten'\n | 'color-dodge'\n | 'color-burn'\n | 'hard-light'\n | 'soft-light'\n | 'difference'\n | 'exclusion'\n | 'hue'\n | 'saturation'\n | 'color'\n | 'luminosity'\n | 'plus-darker'\n | 'plus-lighter';\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst useDarkMode = () => {\n const [isDark, setIsDark] = useState(false);\n\n useEffect(() => {\n if (typeof window === 'undefined') return;\n\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n setIsDark(mediaQuery.matches);\n\n const handler = (e: MediaQueryListEvent) => setIsDark(e.matches);\n mediaQuery.addEventListener('change', handler);\n return () => mediaQuery.removeEventListener('change', handler);\n }, []);\n\n return isDark;\n};\n\nconst GlassSurface: React.FC = ({\n children,\n width = 200,\n height = 80,\n borderRadius = 20,\n borderWidth = 0.07,\n brightness = 50,\n opacity = 0.93,\n blur = 11,\n displace = 0,\n backgroundOpacity = 0,\n saturation = 1,\n distortionScale = -180,\n redOffset = 0,\n greenOffset = 10,\n blueOffset = 20,\n xChannel = 'R',\n yChannel = 'G',\n mixBlendMode = 'difference',\n className = '',\n style = {}\n}) => {\n const uniqueId = useId().replace(/:/g, '-');\n const filterId = `glass-filter-${uniqueId}`;\n const redGradId = `red-grad-${uniqueId}`;\n const blueGradId = `blue-grad-${uniqueId}`;\n\n const [svgSupported, setSvgSupported] = useState(false);\n\n const containerRef = useRef(null);\n const feImageRef = useRef(null);\n const redChannelRef = useRef(null);\n const greenChannelRef = useRef(null);\n const blueChannelRef = useRef(null);\n const gaussianBlurRef = useRef(null);\n\n const isDarkMode = useDarkMode();\n\n const generateDisplacementMap = () => {\n const rect = containerRef.current?.getBoundingClientRect();\n const actualWidth = rect?.width || 400;\n const actualHeight = rect?.height || 200;\n const edgeSize = Math.min(actualWidth, actualHeight) * (borderWidth * 0.5);\n\n const svgContent = `\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n `;\n\n return `data:image/svg+xml,${encodeURIComponent(svgContent)}`;\n };\n\n const updateDisplacementMap = () => {\n feImageRef.current?.setAttribute('href', generateDisplacementMap());\n };\n\n useEffect(() => {\n updateDisplacementMap();\n [\n { ref: redChannelRef, offset: redOffset },\n { ref: greenChannelRef, offset: greenOffset },\n { ref: blueChannelRef, offset: blueOffset }\n ].forEach(({ ref, offset }) => {\n if (ref.current) {\n ref.current.setAttribute('scale', (distortionScale + offset).toString());\n ref.current.setAttribute('xChannelSelector', xChannel);\n ref.current.setAttribute('yChannelSelector', yChannel);\n }\n });\n\n gaussianBlurRef.current?.setAttribute('stdDeviation', displace.toString());\n }, [\n width,\n height,\n borderRadius,\n borderWidth,\n brightness,\n opacity,\n blur,\n displace,\n distortionScale,\n redOffset,\n greenOffset,\n blueOffset,\n xChannel,\n yChannel,\n mixBlendMode\n ]);\n\n useEffect(() => {\n setSvgSupported(supportsSVGFilters());\n }, []);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const resizeObserver = new ResizeObserver(() => {\n setTimeout(updateDisplacementMap, 0);\n });\n\n resizeObserver.observe(containerRef.current);\n\n return () => {\n resizeObserver.disconnect();\n };\n }, []);\n\n useEffect(() => {\n setTimeout(updateDisplacementMap, 0);\n }, [width, height]);\n\n const supportsSVGFilters = () => {\n if (typeof window === 'undefined' || typeof document === 'undefined') {\n return false;\n }\n\n const isWebkit = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);\n const isFirefox = /Firefox/.test(navigator.userAgent);\n\n if (isWebkit || isFirefox) {\n return false;\n }\n\n const div = document.createElement('div');\n div.style.backdropFilter = `url(#${filterId})`;\n\n return div.style.backdropFilter !== '';\n };\n\n const supportsBackdropFilter = () => {\n if (typeof window === 'undefined') return false;\n return CSS.supports('backdrop-filter', 'blur(10px)');\n };\n\n const getContainerStyles = (): React.CSSProperties => {\n const baseStyles: React.CSSProperties = {\n ...style,\n width: typeof width === 'number' ? `${width}px` : width,\n height: typeof height === 'number' ? `${height}px` : height,\n borderRadius: `${borderRadius}px`,\n '--glass-frost': backgroundOpacity,\n '--glass-saturation': saturation\n } as React.CSSProperties;\n\n const backdropFilterSupported = supportsBackdropFilter();\n\n if (svgSupported) {\n return {\n ...baseStyles,\n background: isDarkMode ? `hsl(0 0% 0% / ${backgroundOpacity})` : `hsl(0 0% 100% / ${backgroundOpacity})`,\n backdropFilter: `url(#${filterId}) saturate(${saturation})`,\n boxShadow: isDarkMode\n ? `0 0 2px 1px color-mix(in oklch, white, transparent 65%) inset,\n 0 0 10px 4px color-mix(in oklch, white, transparent 85%) inset,\n 0px 4px 16px rgba(17, 17, 26, 0.05),\n 0px 8px 24px rgba(17, 17, 26, 0.05),\n 0px 16px 56px rgba(17, 17, 26, 0.05),\n 0px 4px 16px rgba(17, 17, 26, 0.05) inset,\n 0px 8px 24px rgba(17, 17, 26, 0.05) inset,\n 0px 16px 56px rgba(17, 17, 26, 0.05) inset`\n : `0 0 2px 1px color-mix(in oklch, black, transparent 85%) inset,\n 0 0 10px 4px color-mix(in oklch, black, transparent 90%) inset,\n 0px 4px 16px rgba(17, 17, 26, 0.05),\n 0px 8px 24px rgba(17, 17, 26, 0.05),\n 0px 16px 56px rgba(17, 17, 26, 0.05),\n 0px 4px 16px rgba(17, 17, 26, 0.05) inset,\n 0px 8px 24px rgba(17, 17, 26, 0.05) inset,\n 0px 16px 56px rgba(17, 17, 26, 0.05) inset`\n };\n } else {\n if (isDarkMode) {\n if (!backdropFilterSupported) {\n return {\n ...baseStyles,\n background: 'rgba(0, 0, 0, 0.4)',\n border: '1px solid rgba(255, 255, 255, 0.2)',\n boxShadow: `inset 0 1px 0 0 rgba(255, 255, 255, 0.2),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.1)`\n };\n } else {\n return {\n ...baseStyles,\n background: 'rgba(255, 255, 255, 0.1)',\n backdropFilter: 'blur(12px) saturate(1.8) brightness(1.2)',\n WebkitBackdropFilter: 'blur(12px) saturate(1.8) brightness(1.2)',\n border: '1px solid rgba(255, 255, 255, 0.2)',\n boxShadow: `inset 0 1px 0 0 rgba(255, 255, 255, 0.2),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.1)`\n };\n }\n } else {\n if (!backdropFilterSupported) {\n return {\n ...baseStyles,\n background: 'rgba(255, 255, 255, 0.4)',\n border: '1px solid rgba(255, 255, 255, 0.3)',\n boxShadow: `inset 0 1px 0 0 rgba(255, 255, 255, 0.5),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.3)`\n };\n } else {\n return {\n ...baseStyles,\n background: 'rgba(255, 255, 255, 0.25)',\n backdropFilter: 'blur(12px) saturate(1.8) brightness(1.1)',\n WebkitBackdropFilter: 'blur(12px) saturate(1.8) brightness(1.1)',\n border: '1px solid rgba(255, 255, 255, 0.3)',\n boxShadow: `0 8px 32px 0 rgba(31, 38, 135, 0.2),\n 0 2px 16px 0 rgba(31, 38, 135, 0.1),\n inset 0 1px 0 0 rgba(255, 255, 255, 0.4),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.2)`\n };\n }\n }\n }\n };\n\n const glassSurfaceClasses =\n 'relative flex items-center justify-center overflow-hidden transition-opacity duration-[260ms] ease-out';\n\n const focusVisibleClasses = isDarkMode\n ? 'focus-visible:outline-2 focus-visible:outline-[#0A84FF] focus-visible:outline-offset-2'\n : 'focus-visible:outline-2 focus-visible:outline-[#007AFF] focus-visible:outline-offset-2';\n\n return (\n \n \n \n \n \n\n \n \n\n \n \n\n \n \n\n \n \n \n \n \n \n\n
\n {children}\n
\n
\n );\n};\n\nexport default GlassSurface;\n" } ], "registryDependencies": [], diff --git a/public/r/GlitchText-JS-CSS.json b/public/r/GlitchText-JS-CSS.json index 384df7b0..34da9cdf 100644 --- a/public/r/GlitchText-JS-CSS.json +++ b/public/r/GlitchText-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GlitchText/GlitchText.css", + "type": "registry:file", + "path": "GlitchText.css", + "target": "@components/GlitchText.css", "content": ".glitch {\n color: #fff;\n font-size: clamp(2rem, 10vw, 8rem);\n white-space: nowrap;\n font-weight: 900;\n position: relative;\n margin: 0 auto;\n user-select: none;\n cursor: pointer;\n}\n\n.glitch::after,\n.glitch::before {\n content: attr(data-text);\n position: absolute;\n top: 0;\n color: #fff;\n background-color: #120F17;\n overflow: hidden;\n clip-path: inset(0 0 0 0);\n}\n\n.glitch:not(.enable-on-hover)::after {\n left: 10px;\n text-shadow: var(--after-shadow, -10px 0 red);\n animation: animate-glitch var(--after-duration, 3s) infinite linear alternate-reverse;\n}\n.glitch:not(.enable-on-hover)::before {\n left: -10px;\n text-shadow: var(--before-shadow, 10px 0 cyan);\n animation: animate-glitch var(--before-duration, 2s) infinite linear alternate-reverse;\n}\n\n.glitch.enable-on-hover::after,\n.glitch.enable-on-hover::before {\n content: '';\n opacity: 0;\n animation: none;\n}\n\n.glitch.enable-on-hover:hover::after {\n content: attr(data-text);\n opacity: 1;\n left: 10px;\n text-shadow: var(--after-shadow, -10px 0 red);\n animation: animate-glitch var(--after-duration, 3s) infinite linear alternate-reverse;\n}\n.glitch.enable-on-hover:hover::before {\n content: attr(data-text);\n opacity: 1;\n left: -10px;\n text-shadow: var(--before-shadow, 10px 0 cyan);\n animation: animate-glitch var(--before-duration, 2s) infinite linear alternate-reverse;\n}\n\n@keyframes animate-glitch {\n 0% {\n clip-path: inset(20% 0 50% 0);\n }\n 5% {\n clip-path: inset(10% 0 60% 0);\n }\n 10% {\n clip-path: inset(15% 0 55% 0);\n }\n 15% {\n clip-path: inset(25% 0 35% 0);\n }\n 20% {\n clip-path: inset(30% 0 40% 0);\n }\n 25% {\n clip-path: inset(40% 0 20% 0);\n }\n 30% {\n clip-path: inset(10% 0 60% 0);\n }\n 35% {\n clip-path: inset(15% 0 55% 0);\n }\n 40% {\n clip-path: inset(25% 0 35% 0);\n }\n 45% {\n clip-path: inset(30% 0 40% 0);\n }\n 50% {\n clip-path: inset(20% 0 50% 0);\n }\n 55% {\n clip-path: inset(10% 0 60% 0);\n }\n 60% {\n clip-path: inset(15% 0 55% 0);\n }\n 65% {\n clip-path: inset(25% 0 35% 0);\n }\n 70% {\n clip-path: inset(30% 0 40% 0);\n }\n 75% {\n clip-path: inset(40% 0 20% 0);\n }\n 80% {\n clip-path: inset(20% 0 50% 0);\n }\n 85% {\n clip-path: inset(10% 0 60% 0);\n }\n 90% {\n clip-path: inset(15% 0 55% 0);\n }\n 95% {\n clip-path: inset(25% 0 35% 0);\n }\n 100% {\n clip-path: inset(30% 0 40% 0);\n }\n}\n" }, { "type": "registry:component", - "path": "GlitchText/GlitchText.jsx", + "path": "GlitchText.jsx", "content": "import './GlitchText.css';\n\nconst GlitchText = ({ children, speed = 1, enableShadows = true, enableOnHover = true, className = '' }) => {\n const inlineStyles = {\n '--after-duration': `${speed * 3}s`,\n '--before-duration': `${speed * 2}s`,\n '--after-shadow': enableShadows ? '-5px 0 red' : 'none',\n '--before-shadow': enableShadows ? '5px 0 cyan' : 'none'\n };\n\n const hoverClass = enableOnHover ? 'enable-on-hover' : '';\n\n return (\n
\n {children}\n
\n );\n};\n\nexport default GlitchText;\n" } ], diff --git a/public/r/GlitchText-TS-CSS.json b/public/r/GlitchText-TS-CSS.json index f614f7f1..418f5397 100644 --- a/public/r/GlitchText-TS-CSS.json +++ b/public/r/GlitchText-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GlitchText/GlitchText.css", + "type": "registry:file", + "path": "GlitchText.css", + "target": "@components/GlitchText.css", "content": ".glitch {\n color: #fff;\n font-size: clamp(2rem, 10vw, 8rem);\n font-weight: 900;\n position: relative;\n white-space: nowrap;\n margin: 0 auto;\n user-select: none;\n cursor: pointer;\n}\n\n.glitch::after,\n.glitch::before {\n content: attr(data-text);\n position: absolute;\n top: 0;\n color: #fff;\n background-color: #120F17;\n overflow: hidden;\n clip-path: inset(0 0 0 0);\n}\n\n.glitch:not(.enable-on-hover)::after {\n left: 10px;\n text-shadow: var(--after-shadow, -10px 0 red);\n animation: animate-glitch var(--after-duration, 3s) infinite linear alternate-reverse;\n}\n.glitch:not(.enable-on-hover)::before {\n left: -10px;\n text-shadow: var(--before-shadow, 10px 0 cyan);\n animation: animate-glitch var(--before-duration, 2s) infinite linear alternate-reverse;\n}\n\n.glitch.enable-on-hover::after,\n.glitch.enable-on-hover::before {\n content: '';\n opacity: 0;\n animation: none;\n}\n\n.glitch.enable-on-hover:hover::after {\n content: attr(data-text);\n opacity: 1;\n left: 10px;\n text-shadow: var(--after-shadow, -10px 0 red);\n animation: animate-glitch var(--after-duration, 3s) infinite linear alternate-reverse;\n}\n.glitch.enable-on-hover:hover::before {\n content: attr(data-text);\n opacity: 1;\n left: -10px;\n text-shadow: var(--before-shadow, 10px 0 cyan);\n animation: animate-glitch var(--before-duration, 2s) infinite linear alternate-reverse;\n}\n\n@keyframes animate-glitch {\n 0% {\n clip-path: inset(20% 0 50% 0);\n }\n 5% {\n clip-path: inset(10% 0 60% 0);\n }\n 10% {\n clip-path: inset(15% 0 55% 0);\n }\n 15% {\n clip-path: inset(25% 0 35% 0);\n }\n 20% {\n clip-path: inset(30% 0 40% 0);\n }\n 25% {\n clip-path: inset(40% 0 20% 0);\n }\n 30% {\n clip-path: inset(10% 0 60% 0);\n }\n 35% {\n clip-path: inset(15% 0 55% 0);\n }\n 40% {\n clip-path: inset(25% 0 35% 0);\n }\n 45% {\n clip-path: inset(30% 0 40% 0);\n }\n 50% {\n clip-path: inset(20% 0 50% 0);\n }\n 55% {\n clip-path: inset(10% 0 60% 0);\n }\n 60% {\n clip-path: inset(15% 0 55% 0);\n }\n 65% {\n clip-path: inset(25% 0 35% 0);\n }\n 70% {\n clip-path: inset(30% 0 40% 0);\n }\n 75% {\n clip-path: inset(40% 0 20% 0);\n }\n 80% {\n clip-path: inset(20% 0 50% 0);\n }\n 85% {\n clip-path: inset(10% 0 60% 0);\n }\n 90% {\n clip-path: inset(15% 0 55% 0);\n }\n 95% {\n clip-path: inset(25% 0 35% 0);\n }\n 100% {\n clip-path: inset(30% 0 40% 0);\n }\n}\n" }, { "type": "registry:component", - "path": "GlitchText/GlitchText.tsx", - "content": "import { FC, CSSProperties } from 'react';\nimport './GlitchText.css';\n\ninterface GlitchTextProps {\n children: string;\n speed?: number;\n enableShadows?: boolean;\n enableOnHover?: boolean;\n className?: string;\n}\n\ninterface CustomCSSProperties extends CSSProperties {\n '--after-duration': string;\n '--before-duration': string;\n '--after-shadow': string;\n '--before-shadow': string;\n}\n\nconst GlitchText: FC = ({\n children,\n speed = 0.5,\n enableShadows = true,\n enableOnHover = false,\n className = ''\n}) => {\n const inlineStyles: CustomCSSProperties = {\n '--after-duration': `${speed * 3}s`,\n '--before-duration': `${speed * 2}s`,\n '--after-shadow': enableShadows ? '-5px 0 red' : 'none',\n '--before-shadow': enableShadows ? '5px 0 cyan' : 'none'\n };\n\n const hoverClass = enableOnHover ? 'enable-on-hover' : '';\n\n return (\n
\n {children}\n
\n );\n};\n\nexport default GlitchText;\n" + "path": "GlitchText.tsx", + "content": "import { type FC, type CSSProperties } from 'react';\nimport './GlitchText.css';\n\ninterface GlitchTextProps {\n children: string;\n speed?: number;\n enableShadows?: boolean;\n enableOnHover?: boolean;\n className?: string;\n}\n\ninterface CustomCSSProperties extends CSSProperties {\n '--after-duration': string;\n '--before-duration': string;\n '--after-shadow': string;\n '--before-shadow': string;\n}\n\nconst GlitchText: FC = ({\n children,\n speed = 0.5,\n enableShadows = true,\n enableOnHover = false,\n className = ''\n}) => {\n const inlineStyles: CustomCSSProperties = {\n '--after-duration': `${speed * 3}s`,\n '--before-duration': `${speed * 2}s`,\n '--after-shadow': enableShadows ? '-5px 0 red' : 'none',\n '--before-shadow': enableShadows ? '5px 0 cyan' : 'none'\n };\n\n const hoverClass = enableOnHover ? 'enable-on-hover' : '';\n\n return (\n
\n {children}\n
\n );\n};\n\nexport default GlitchText;\n" } ], "registryDependencies": [], diff --git a/public/r/GlitchText-TS-TW.json b/public/r/GlitchText-TS-TW.json index 47ff73b9..e1685719 100644 --- a/public/r/GlitchText-TS-TW.json +++ b/public/r/GlitchText-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "GlitchText/GlitchText.tsx", - "content": "import { FC, CSSProperties } from 'react';\n\ninterface GlitchTextProps {\n children: string;\n speed?: number;\n enableShadows?: boolean;\n enableOnHover?: boolean;\n className?: string;\n}\n\ninterface CustomCSSProperties extends CSSProperties {\n '--after-duration': string;\n '--before-duration': string;\n '--after-shadow': string;\n '--before-shadow': string;\n}\n\nconst GlitchText: FC = ({\n children,\n speed = 0.5,\n enableShadows = true,\n enableOnHover = false,\n className = ''\n}) => {\n const inlineStyles: CustomCSSProperties = {\n '--after-duration': `${speed * 3}s`,\n '--before-duration': `${speed * 2}s`,\n '--after-shadow': enableShadows ? '-5px 0 red' : 'none',\n '--before-shadow': enableShadows ? '5px 0 cyan' : 'none'\n };\n\n const baseClasses = 'text-white text-[clamp(2rem,10vw,8rem)] font-black relative mx-auto select-none cursor-pointer';\n\n const pseudoClasses = !enableOnHover\n ? 'after:content-[attr(data-text)] after:absolute after:top-0 after:left-[10px] after:text-white after:bg-[#120F17] after:overflow-hidden after:[clip-path:inset(0_0_0_0)] after:[text-shadow:var(--after-shadow)] after:animate-glitch-after ' +\n 'before:content-[attr(data-text)] before:absolute before:top-0 before:left-[-10px] before:text-white before:bg-[#120F17] before:overflow-hidden before:[clip-path:inset(0_0_0_0)] before:[text-shadow:var(--before-shadow)] before:animate-glitch-before'\n : \"after:content-[''] after:absolute after:top-0 after:left-[10px] after:text-white after:bg-[#120F17] after:overflow-hidden after:[clip-path:inset(0_0_0_0)] after:opacity-0 \" +\n \"before:content-[''] before:absolute before:top-0 before:left-[-10px] before:text-white before:bg-[#120F17] before:overflow-hidden before:[clip-path:inset(0_0_0_0)] before:opacity-0 \" +\n 'hover:after:content-[attr(data-text)] hover:after:opacity-100 hover:after:[text-shadow:var(--after-shadow)] hover:after:animate-glitch-after ' +\n 'hover:before:content-[attr(data-text)] hover:before:opacity-100 hover:before:[text-shadow:var(--before-shadow)] hover:before:animate-glitch-before';\n\n const combinedClasses = `${baseClasses} ${pseudoClasses} ${className}`;\n\n return (\n
\n {children}\n
\n );\n};\n\nexport default GlitchText;\n\n// tailwind.config.js\n// module.exports = {\n// theme: {\n// extend: {\n// keyframes: {\n// glitch: {\n// \"0%\": { \"clip-path\": \"inset(20% 0 50% 0)\" },\n// \"5%\": { \"clip-path\": \"inset(10% 0 60% 0)\" },\n// \"10%\": { \"clip-path\": \"inset(15% 0 55% 0)\" },\n// \"15%\": { \"clip-path\": \"inset(25% 0 35% 0)\" },\n// \"20%\": { \"clip-path\": \"inset(30% 0 40% 0)\" },\n// \"25%\": { \"clip-path\": \"inset(40% 0 20% 0)\" },\n// \"30%\": { \"clip-path\": \"inset(10% 0 60% 0)\" },\n// \"35%\": { \"clip-path\": \"inset(15% 0 55% 0)\" },\n// \"40%\": { \"clip-path\": \"inset(25% 0 35% 0)\" },\n// \"45%\": { \"clip-path\": \"inset(30% 0 40% 0)\" },\n// \"50%\": { \"clip-path\": \"inset(20% 0 50% 0)\" },\n// \"55%\": { \"clip-path\": \"inset(10% 0 60% 0)\" },\n// \"60%\": { \"clip-path\": \"inset(15% 0 55% 0)\" },\n// \"65%\": { \"clip-path\": \"inset(25% 0 35% 0)\" },\n// \"70%\": { \"clip-path\": \"inset(30% 0 40% 0)\" },\n// \"75%\": { \"clip-path\": \"inset(40% 0 20% 0)\" },\n// \"80%\": { \"clip-path\": \"inset(20% 0 50% 0)\" },\n// \"85%\": { \"clip-path\": \"inset(10% 0 60% 0)\" },\n// \"90%\": { \"clip-path\": \"inset(15% 0 55% 0)\" },\n// \"95%\": { \"clip-path\": \"inset(25% 0 35% 0)\" },\n// \"100%\": { \"clip-path\": \"inset(30% 0 40% 0)\" },\n// },\n// },\n// animation: {\n// \"glitch-after\": \"glitch var(--after-duration) infinite linear alternate-reverse\",\n// \"glitch-before\": \"glitch var(--before-duration) infinite linear alternate-reverse\",\n// },\n// },\n// },\n// plugins: [],\n// };\n" + "content": "import { type FC, type CSSProperties } from 'react';\n\ninterface GlitchTextProps {\n children: string;\n speed?: number;\n enableShadows?: boolean;\n enableOnHover?: boolean;\n className?: string;\n}\n\ninterface CustomCSSProperties extends CSSProperties {\n '--after-duration': string;\n '--before-duration': string;\n '--after-shadow': string;\n '--before-shadow': string;\n}\n\nconst GlitchText: FC = ({\n children,\n speed = 0.5,\n enableShadows = true,\n enableOnHover = false,\n className = ''\n}) => {\n const inlineStyles: CustomCSSProperties = {\n '--after-duration': `${speed * 3}s`,\n '--before-duration': `${speed * 2}s`,\n '--after-shadow': enableShadows ? '-5px 0 red' : 'none',\n '--before-shadow': enableShadows ? '5px 0 cyan' : 'none'\n };\n\n const baseClasses = 'text-white text-[clamp(2rem,10vw,8rem)] font-black relative mx-auto select-none cursor-pointer';\n\n const pseudoClasses = !enableOnHover\n ? 'after:content-[attr(data-text)] after:absolute after:top-0 after:left-[10px] after:text-white after:bg-[#120F17] after:overflow-hidden after:[clip-path:inset(0_0_0_0)] after:[text-shadow:var(--after-shadow)] after:animate-glitch-after ' +\n 'before:content-[attr(data-text)] before:absolute before:top-0 before:left-[-10px] before:text-white before:bg-[#120F17] before:overflow-hidden before:[clip-path:inset(0_0_0_0)] before:[text-shadow:var(--before-shadow)] before:animate-glitch-before'\n : \"after:content-[''] after:absolute after:top-0 after:left-[10px] after:text-white after:bg-[#120F17] after:overflow-hidden after:[clip-path:inset(0_0_0_0)] after:opacity-0 \" +\n \"before:content-[''] before:absolute before:top-0 before:left-[-10px] before:text-white before:bg-[#120F17] before:overflow-hidden before:[clip-path:inset(0_0_0_0)] before:opacity-0 \" +\n 'hover:after:content-[attr(data-text)] hover:after:opacity-100 hover:after:[text-shadow:var(--after-shadow)] hover:after:animate-glitch-after ' +\n 'hover:before:content-[attr(data-text)] hover:before:opacity-100 hover:before:[text-shadow:var(--before-shadow)] hover:before:animate-glitch-before';\n\n const combinedClasses = `${baseClasses} ${pseudoClasses} ${className}`;\n\n return (\n
\n {children}\n
\n );\n};\n\nexport default GlitchText;\n\n// tailwind.config.js\n// module.exports = {\n// theme: {\n// extend: {\n// keyframes: {\n// glitch: {\n// \"0%\": { \"clip-path\": \"inset(20% 0 50% 0)\" },\n// \"5%\": { \"clip-path\": \"inset(10% 0 60% 0)\" },\n// \"10%\": { \"clip-path\": \"inset(15% 0 55% 0)\" },\n// \"15%\": { \"clip-path\": \"inset(25% 0 35% 0)\" },\n// \"20%\": { \"clip-path\": \"inset(30% 0 40% 0)\" },\n// \"25%\": { \"clip-path\": \"inset(40% 0 20% 0)\" },\n// \"30%\": { \"clip-path\": \"inset(10% 0 60% 0)\" },\n// \"35%\": { \"clip-path\": \"inset(15% 0 55% 0)\" },\n// \"40%\": { \"clip-path\": \"inset(25% 0 35% 0)\" },\n// \"45%\": { \"clip-path\": \"inset(30% 0 40% 0)\" },\n// \"50%\": { \"clip-path\": \"inset(20% 0 50% 0)\" },\n// \"55%\": { \"clip-path\": \"inset(10% 0 60% 0)\" },\n// \"60%\": { \"clip-path\": \"inset(15% 0 55% 0)\" },\n// \"65%\": { \"clip-path\": \"inset(25% 0 35% 0)\" },\n// \"70%\": { \"clip-path\": \"inset(30% 0 40% 0)\" },\n// \"75%\": { \"clip-path\": \"inset(40% 0 20% 0)\" },\n// \"80%\": { \"clip-path\": \"inset(20% 0 50% 0)\" },\n// \"85%\": { \"clip-path\": \"inset(10% 0 60% 0)\" },\n// \"90%\": { \"clip-path\": \"inset(15% 0 55% 0)\" },\n// \"95%\": { \"clip-path\": \"inset(25% 0 35% 0)\" },\n// \"100%\": { \"clip-path\": \"inset(30% 0 40% 0)\" },\n// },\n// },\n// animation: {\n// \"glitch-after\": \"glitch var(--after-duration) infinite linear alternate-reverse\",\n// \"glitch-before\": \"glitch var(--before-duration) infinite linear alternate-reverse\",\n// },\n// },\n// },\n// plugins: [],\n// };\n" } ], "registryDependencies": [], diff --git a/public/r/GooeyNav-JS-CSS.json b/public/r/GooeyNav-JS-CSS.json index 7d99b58c..fbd0a9cf 100644 --- a/public/r/GooeyNav-JS-CSS.json +++ b/public/r/GooeyNav-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GooeyNav/GooeyNav.css", + "type": "registry:file", + "path": "GooeyNav.css", + "target": "@components/GooeyNav.css", "content": ":root {\n --linear-ease: linear(\n 0,\n 0.068,\n 0.19 2.7%,\n 0.804 8.1%,\n 1.037,\n 1.199 13.2%,\n 1.245,\n 1.27 15.8%,\n 1.274,\n 1.272 17.4%,\n 1.249 19.1%,\n 0.996 28%,\n 0.949,\n 0.928 33.3%,\n 0.926,\n 0.933 36.8%,\n 1.001 45.6%,\n 1.013,\n 1.019 50.8%,\n 1.018 54.4%,\n 1 63.1%,\n 0.995 68%,\n 1.001 85%,\n 1\n );\n}\n\n.gooey-nav-container {\n position: relative;\n}\n\n.gooey-nav-container nav {\n display: flex;\n position: relative;\n transform: translate3d(0, 0, 0.01px);\n}\n\n.gooey-nav-container nav ul {\n display: flex;\n gap: 2em;\n list-style: none;\n padding: 0 1em;\n margin: 0;\n position: relative;\n z-index: 3;\n color: white;\n text-shadow: 0 1px 1px hsl(205deg 30% 10% / 0.2);\n}\n\n.gooey-nav-container nav ul li {\n border-radius: 100vw;\n position: relative;\n cursor: pointer;\n transition:\n background-color 0.3s ease,\n color 0.3s ease,\n box-shadow 0.3s ease;\n box-shadow: 0 0 0.5px 1.5px transparent;\n color: white;\n}\n\n.gooey-nav-container nav ul li a {\n display: inline-block;\n padding: 0.6em 1em;\n}\n\n.gooey-nav-container nav ul li:focus-within:has(:focus-visible) {\n box-shadow: 0 0 0.5px 1.5px white;\n}\n\n.gooey-nav-container nav ul li::after {\n content: '';\n position: absolute;\n inset: 0;\n border-radius: 10px;\n background: white;\n opacity: 0;\n transform: scale(0);\n transition: all 0.3s ease;\n z-index: -1;\n}\n\n.gooey-nav-container nav ul li.active {\n color: black;\n text-shadow: none;\n}\n\n.gooey-nav-container nav ul li.active::after {\n opacity: 1;\n transform: scale(1);\n}\n\n.gooey-nav-container .effect {\n position: absolute;\n left: 0;\n top: 0;\n width: 0;\n height: 0;\n opacity: 1;\n pointer-events: none;\n display: grid;\n place-items: center;\n z-index: 1;\n}\n\n.gooey-nav-container .effect.text {\n color: white;\n transition: color 0.3s ease;\n}\n\n.gooey-nav-container .effect.text.active {\n color: black;\n}\n\n.gooey-nav-container .effect.filter {\n filter: blur(7px) contrast(100) blur(0);\n mix-blend-mode: lighten;\n}\n\n.gooey-nav-container .effect.filter::before {\n content: '';\n position: absolute;\n inset: -75px;\n z-index: -2;\n background: black;\n}\n\n.gooey-nav-container .effect.filter::after {\n content: '';\n position: absolute;\n inset: 0;\n background: white;\n transform: scale(0);\n opacity: 0;\n z-index: -1;\n border-radius: 100vw;\n}\n\n.gooey-nav-container .effect.active::after {\n animation: pill 0.3s ease both;\n}\n\n@keyframes pill {\n to {\n transform: scale(1);\n opacity: 1;\n }\n}\n\n.particle,\n.point {\n display: block;\n opacity: 0;\n width: 20px;\n height: 20px;\n border-radius: 100%;\n transform-origin: center;\n}\n\n.particle {\n --time: 5s;\n position: absolute;\n top: calc(50% - 8px);\n left: calc(50% - 8px);\n animation: particle calc(var(--time)) ease 1 -350ms;\n}\n\n.point {\n background: var(--color);\n opacity: 1;\n animation: point calc(var(--time)) ease 1 -350ms;\n}\n\n@keyframes particle {\n 0% {\n transform: rotate(0deg) translate(calc(var(--start-x)), calc(var(--start-y)));\n opacity: 1;\n animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);\n }\n\n 70% {\n transform: rotate(calc(var(--rotate) * 0.5)) translate(calc(var(--end-x) * 1.2), calc(var(--end-y) * 1.2));\n opacity: 1;\n animation-timing-function: ease;\n }\n\n 85% {\n transform: rotate(calc(var(--rotate) * 0.66)) translate(calc(var(--end-x)), calc(var(--end-y)));\n opacity: 1;\n }\n\n 100% {\n transform: rotate(calc(var(--rotate) * 1.2)) translate(calc(var(--end-x) * 0.5), calc(var(--end-y) * 0.5));\n opacity: 1;\n }\n}\n\n@keyframes point {\n 0% {\n transform: scale(0);\n opacity: 0;\n animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);\n }\n\n 25% {\n transform: scale(calc(var(--scale) * 0.25));\n }\n\n 38% {\n opacity: 1;\n }\n\n 65% {\n transform: scale(var(--scale));\n opacity: 1;\n animation-timing-function: ease;\n }\n\n 85% {\n transform: scale(var(--scale));\n opacity: 1;\n }\n\n 100% {\n transform: scale(0);\n opacity: 0;\n }\n}\n" }, { "type": "registry:component", - "path": "GooeyNav/GooeyNav.jsx", + "path": "GooeyNav.jsx", "content": "import { useRef, useEffect, useState } from 'react';\nimport './GooeyNav.css';\n\nconst GooeyNav = ({\n items,\n animationTime = 600,\n particleCount = 15,\n particleDistances = [90, 10],\n particleR = 100,\n timeVariance = 300,\n colors = [1, 2, 3, 1, 2, 3, 1, 4],\n initialActiveIndex = 0\n}) => {\n const containerRef = useRef(null);\n const navRef = useRef(null);\n const filterRef = useRef(null);\n const textRef = useRef(null);\n const [activeIndex, setActiveIndex] = useState(initialActiveIndex);\n\n const noise = (n = 1) => n / 2 - Math.random() * n;\n\n const getXY = (distance, pointIndex, totalPoints) => {\n const angle = ((360 + noise(8)) / totalPoints) * pointIndex * (Math.PI / 180);\n return [distance * Math.cos(angle), distance * Math.sin(angle)];\n };\n\n const createParticle = (i, t, d, r) => {\n let rotate = noise(r / 10);\n return {\n start: getXY(d[0], particleCount - i, particleCount),\n end: getXY(d[1] + noise(7), particleCount - i, particleCount),\n time: t,\n scale: 1 + noise(0.2),\n color: colors[Math.floor(Math.random() * colors.length)],\n rotate: rotate > 0 ? (rotate + r / 20) * 10 : (rotate - r / 20) * 10\n };\n };\n\n const makeParticles = element => {\n const d = particleDistances;\n const r = particleR;\n const bubbleTime = animationTime * 2 + timeVariance;\n element.style.setProperty('--time', `${bubbleTime}ms`);\n\n for (let i = 0; i < particleCount; i++) {\n const t = animationTime * 2 + noise(timeVariance * 2);\n const p = createParticle(i, t, d, r);\n element.classList.remove('active');\n\n setTimeout(() => {\n const particle = document.createElement('span');\n const point = document.createElement('span');\n particle.classList.add('particle');\n particle.style.setProperty('--start-x', `${p.start[0]}px`);\n particle.style.setProperty('--start-y', `${p.start[1]}px`);\n particle.style.setProperty('--end-x', `${p.end[0]}px`);\n particle.style.setProperty('--end-y', `${p.end[1]}px`);\n particle.style.setProperty('--time', `${p.time}ms`);\n particle.style.setProperty('--scale', `${p.scale}`);\n particle.style.setProperty('--color', `var(--color-${p.color}, white)`);\n particle.style.setProperty('--rotate', `${p.rotate}deg`);\n\n point.classList.add('point');\n particle.appendChild(point);\n element.appendChild(particle);\n requestAnimationFrame(() => {\n element.classList.add('active');\n });\n setTimeout(() => {\n try {\n element.removeChild(particle);\n } catch {\n // Do nothing\n }\n }, t);\n }, 30);\n }\n };\n\n const updateEffectPosition = element => {\n if (!containerRef.current || !filterRef.current || !textRef.current) return;\n const containerRect = containerRef.current.getBoundingClientRect();\n const pos = element.getBoundingClientRect();\n\n const styles = {\n left: `${pos.x - containerRect.x}px`,\n top: `${pos.y - containerRect.y}px`,\n width: `${pos.width}px`,\n height: `${pos.height}px`\n };\n Object.assign(filterRef.current.style, styles);\n Object.assign(textRef.current.style, styles);\n textRef.current.innerText = element.innerText;\n };\n\n const handleClick = (e, index) => {\n const liEl = e.currentTarget;\n if (activeIndex === index) return;\n\n setActiveIndex(index);\n updateEffectPosition(liEl);\n\n if (filterRef.current) {\n const particles = filterRef.current.querySelectorAll('.particle');\n particles.forEach(p => filterRef.current.removeChild(p));\n }\n\n if (textRef.current) {\n textRef.current.classList.remove('active');\n\n void textRef.current.offsetWidth;\n textRef.current.classList.add('active');\n }\n\n if (filterRef.current) {\n makeParticles(filterRef.current);\n }\n };\n\n const handleKeyDown = (e, index) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n const liEl = e.currentTarget.parentElement;\n if (liEl) {\n handleClick({ currentTarget: liEl }, index);\n }\n }\n };\n\n useEffect(() => {\n if (!navRef.current || !containerRef.current) return;\n const activeLi = navRef.current.querySelectorAll('li')[activeIndex];\n if (activeLi) {\n updateEffectPosition(activeLi);\n textRef.current?.classList.add('active');\n }\n\n const resizeObserver = new ResizeObserver(() => {\n const currentActiveLi = navRef.current?.querySelectorAll('li')[activeIndex];\n if (currentActiveLi) {\n updateEffectPosition(currentActiveLi);\n }\n });\n\n resizeObserver.observe(containerRef.current);\n return () => resizeObserver.disconnect();\n }, [activeIndex]);\n\n return (\n
\n \n \n \n
\n );\n};\n\nexport default GooeyNav;\n" } ], diff --git a/public/r/GooeyNav-TS-CSS.json b/public/r/GooeyNav-TS-CSS.json index b03fcd82..9ed008f4 100644 --- a/public/r/GooeyNav-TS-CSS.json +++ b/public/r/GooeyNav-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GooeyNav/GooeyNav.css", + "type": "registry:file", + "path": "GooeyNav.css", + "target": "@components/GooeyNav.css", "content": ":root {\n --linear-ease: linear(\n 0,\n 0.068,\n 0.19 2.7%,\n 0.804 8.1%,\n 1.037,\n 1.199 13.2%,\n 1.245,\n 1.27 15.8%,\n 1.274,\n 1.272 17.4%,\n 1.249 19.1%,\n 0.996 28%,\n 0.949,\n 0.928 33.3%,\n 0.926,\n 0.933 36.8%,\n 1.001 45.6%,\n 1.013,\n 1.019 50.8%,\n 1.018 54.4%,\n 1 63.1%,\n 0.995 68%,\n 1.001 85%,\n 1\n );\n}\n\n.gooey-nav-container {\n position: relative;\n}\n\n.gooey-nav-container nav {\n display: flex;\n position: relative;\n transform: translate3d(0, 0, 0.01px);\n}\n\n.gooey-nav-container nav ul {\n display: flex;\n gap: 2em;\n list-style: none;\n padding: 0 1em;\n margin: 0;\n position: relative;\n z-index: 3;\n color: white;\n text-shadow: 0 1px 1px hsl(205deg 30% 10% / 0.2);\n}\n\n.gooey-nav-container nav ul li {\n border-radius: 100vw;\n position: relative;\n cursor: pointer;\n transition:\n background-color 0.3s ease,\n color 0.3s ease,\n box-shadow 0.3s ease;\n box-shadow: 0 0 0.5px 1.5px transparent;\n color: white;\n}\n\n.gooey-nav-container nav ul li a {\n display: inline-block;\n padding: 0.6em 1em;\n}\n\n.gooey-nav-container nav ul li:focus-within:has(:focus-visible) {\n box-shadow: 0 0 0.5px 1.5px white;\n}\n\n.gooey-nav-container nav ul li::after {\n content: '';\n position: absolute;\n inset: 0;\n border-radius: 10px;\n background: white;\n opacity: 0;\n transform: scale(0);\n transition: all 0.3s ease;\n z-index: -1;\n}\n\n.gooey-nav-container nav ul li.active {\n color: black;\n text-shadow: none;\n}\n\n.gooey-nav-container nav ul li.active::after {\n opacity: 1;\n transform: scale(1);\n}\n\n.gooey-nav-container .effect {\n position: absolute;\n left: 0;\n top: 0;\n width: 0;\n height: 0;\n opacity: 1;\n pointer-events: none;\n display: grid;\n place-items: center;\n z-index: 1;\n}\n\n.gooey-nav-container .effect.text {\n color: white;\n transition: color 0.3s ease;\n}\n\n.gooey-nav-container .effect.text.active {\n color: black;\n}\n\n.gooey-nav-container .effect.filter {\n filter: blur(7px) contrast(100) blur(0);\n mix-blend-mode: lighten;\n}\n\n.gooey-nav-container .effect.filter::before {\n content: '';\n position: absolute;\n inset: -75px;\n z-index: -2;\n background: black;\n}\n\n.gooey-nav-container .effect.filter::after {\n content: '';\n position: absolute;\n inset: 0;\n background: white;\n transform: scale(0);\n opacity: 0;\n z-index: -1;\n border-radius: 100vw;\n}\n\n.gooey-nav-container .effect.active::after {\n animation: pill 0.3s ease both;\n}\n\n@keyframes pill {\n to {\n transform: scale(1);\n opacity: 1;\n }\n}\n\n.particle,\n.point {\n display: block;\n opacity: 0;\n width: 20px;\n height: 20px;\n border-radius: 100%;\n transform-origin: center;\n}\n\n.particle {\n --time: 5s;\n position: absolute;\n top: calc(50% - 8px);\n left: calc(50% - 8px);\n animation: particle calc(var(--time)) ease 1 -350ms;\n}\n\n.point {\n background: var(--color);\n opacity: 1;\n animation: point calc(var(--time)) ease 1 -350ms;\n}\n\n@keyframes particle {\n 0% {\n transform: rotate(0deg) translate(calc(var(--start-x)), calc(var(--start-y)));\n opacity: 1;\n animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);\n }\n\n 70% {\n transform: rotate(calc(var(--rotate) * 0.5)) translate(calc(var(--end-x) * 1.2), calc(var(--end-y) * 1.2));\n opacity: 1;\n animation-timing-function: ease;\n }\n\n 85% {\n transform: rotate(calc(var(--rotate) * 0.66)) translate(calc(var(--end-x)), calc(var(--end-y)));\n opacity: 1;\n }\n\n 100% {\n transform: rotate(calc(var(--rotate) * 1.2)) translate(calc(var(--end-x) * 0.5), calc(var(--end-y) * 0.5));\n opacity: 1;\n }\n}\n\n@keyframes point {\n 0% {\n transform: scale(0);\n opacity: 0;\n animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);\n }\n\n 25% {\n transform: scale(calc(var(--scale) * 0.25));\n }\n\n 38% {\n opacity: 1;\n }\n\n 65% {\n transform: scale(var(--scale));\n opacity: 1;\n animation-timing-function: ease;\n }\n\n 85% {\n transform: scale(var(--scale));\n opacity: 1;\n }\n\n 100% {\n transform: scale(0);\n opacity: 0;\n }\n}\n" }, { "type": "registry:component", - "path": "GooeyNav/GooeyNav.tsx", + "path": "GooeyNav.tsx", "content": "import React, { useRef, useEffect, useState } from 'react';\nimport './GooeyNav.css';\n\ninterface GooeyNavItem {\n label: string;\n href: string;\n}\n\nexport interface GooeyNavProps {\n items: GooeyNavItem[];\n animationTime?: number;\n particleCount?: number;\n particleDistances?: [number, number];\n particleR?: number;\n timeVariance?: number;\n colors?: number[];\n initialActiveIndex?: number;\n}\n\nconst GooeyNav: React.FC = ({\n items,\n animationTime = 600,\n particleCount = 15,\n particleDistances = [90, 10],\n particleR = 100,\n timeVariance = 300,\n colors = [1, 2, 3, 1, 2, 3, 1, 4],\n initialActiveIndex = 0\n}) => {\n const containerRef = useRef(null);\n const navRef = useRef(null);\n const filterRef = useRef(null);\n const textRef = useRef(null);\n const [activeIndex, setActiveIndex] = useState(initialActiveIndex);\n\n const noise = (n = 1) => n / 2 - Math.random() * n;\n\n const getXY = (distance: number, pointIndex: number, totalPoints: number): [number, number] => {\n const angle = ((360 + noise(8)) / totalPoints) * pointIndex * (Math.PI / 180);\n return [distance * Math.cos(angle), distance * Math.sin(angle)];\n };\n\n const createParticle = (i: number, t: number, d: [number, number], r: number) => {\n let rotate = noise(r / 10);\n return {\n start: getXY(d[0], particleCount - i, particleCount),\n end: getXY(d[1] + noise(7), particleCount - i, particleCount),\n time: t,\n scale: 1 + noise(0.2),\n color: colors[Math.floor(Math.random() * colors.length)],\n rotate: rotate > 0 ? (rotate + r / 20) * 10 : (rotate - r / 20) * 10\n };\n };\n\n const makeParticles = (element: HTMLElement) => {\n const d: [number, number] = particleDistances;\n const r = particleR;\n const bubbleTime = animationTime * 2 + timeVariance;\n element.style.setProperty('--time', `${bubbleTime}ms`);\n\n for (let i = 0; i < particleCount; i++) {\n const t = animationTime * 2 + noise(timeVariance * 2);\n const p = createParticle(i, t, d, r);\n element.classList.remove('active');\n\n setTimeout(() => {\n const particle = document.createElement('span');\n const point = document.createElement('span');\n particle.classList.add('particle');\n particle.style.setProperty('--start-x', `${p.start[0]}px`);\n particle.style.setProperty('--start-y', `${p.start[1]}px`);\n particle.style.setProperty('--end-x', `${p.end[0]}px`);\n particle.style.setProperty('--end-y', `${p.end[1]}px`);\n particle.style.setProperty('--time', `${p.time}ms`);\n particle.style.setProperty('--scale', `${p.scale}`);\n particle.style.setProperty('--color', `var(--color-${p.color}, white)`);\n particle.style.setProperty('--rotate', `${p.rotate}deg`);\n\n point.classList.add('point');\n particle.appendChild(point);\n element.appendChild(particle);\n requestAnimationFrame(() => {\n element.classList.add('active');\n });\n setTimeout(() => {\n try {\n element.removeChild(particle);\n } catch {\n // Do nothing\n }\n }, t);\n }, 30);\n }\n };\n\n const updateEffectPosition = (element: HTMLElement) => {\n if (!containerRef.current || !filterRef.current || !textRef.current) return;\n const containerRect = containerRef.current.getBoundingClientRect();\n const pos = element.getBoundingClientRect();\n\n const styles = {\n left: `${pos.x - containerRect.x}px`,\n top: `${pos.y - containerRect.y}px`,\n width: `${pos.width}px`,\n height: `${pos.height}px`\n };\n Object.assign(filterRef.current.style, styles);\n Object.assign(textRef.current.style, styles);\n textRef.current.innerText = element.innerText;\n };\n\n const handleClick = (e: React.MouseEvent, index: number) => {\n const liEl = e.currentTarget;\n if (activeIndex === index) return;\n\n setActiveIndex(index);\n updateEffectPosition(liEl);\n\n if (filterRef.current) {\n const particles = filterRef.current.querySelectorAll('.particle');\n particles.forEach(p => filterRef.current!.removeChild(p));\n }\n\n if (textRef.current) {\n textRef.current.classList.remove('active');\n\n void textRef.current.offsetWidth;\n textRef.current.classList.add('active');\n }\n\n if (filterRef.current) {\n makeParticles(filterRef.current);\n }\n };\n\n const handleKeyDown = (e: React.KeyboardEvent, index: number) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n const liEl = e.currentTarget.parentElement;\n if (liEl) {\n handleClick(\n {\n currentTarget: liEl\n } as React.MouseEvent,\n index\n );\n }\n }\n };\n\n useEffect(() => {\n if (!navRef.current || !containerRef.current) return;\n const activeLi = navRef.current.querySelectorAll('li')[activeIndex] as HTMLElement;\n if (activeLi) {\n updateEffectPosition(activeLi);\n textRef.current?.classList.add('active');\n }\n\n const resizeObserver = new ResizeObserver(() => {\n const currentActiveLi = navRef.current?.querySelectorAll('li')[activeIndex] as HTMLElement;\n if (currentActiveLi) {\n updateEffectPosition(currentActiveLi);\n }\n });\n\n resizeObserver.observe(containerRef.current);\n return () => resizeObserver.disconnect();\n }, [activeIndex]);\n\n return (\n
\n \n \n \n
\n );\n};\n\nexport default GooeyNav;\n" } ], diff --git a/public/r/GradientBlinds-JS-CSS.json b/public/r/GradientBlinds-JS-CSS.json index cf5d3def..6f00c5c7 100644 --- a/public/r/GradientBlinds-JS-CSS.json +++ b/public/r/GradientBlinds-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GradientBlinds/GradientBlinds.css", + "type": "registry:file", + "path": "GradientBlinds.css", + "target": "@components/GradientBlinds.css", "content": ".gradient-blinds-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "GradientBlinds/GradientBlinds.jsx", + "path": "GradientBlinds.jsx", "content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './GradientBlinds.css';\n\nconst MAX_COLORS = 8;\nconst hexToRGB = hex => {\n const c = hex.replace('#', '').padEnd(6, '0');\n const r = parseInt(c.slice(0, 2), 16) / 255;\n const g = parseInt(c.slice(2, 4), 16) / 255;\n const b = parseInt(c.slice(4, 6), 16) / 255;\n return [r, g, b];\n};\nconst prepStops = stops => {\n const base = (stops && stops.length ? stops : ['#FF9FFC', '#5227FF']).slice(0, MAX_COLORS);\n if (base.length === 1) base.push(base[0]);\n while (base.length < MAX_COLORS) base.push(base[base.length - 1]);\n const arr = [];\n for (let i = 0; i < MAX_COLORS; i++) arr.push(hexToRGB(base[i]));\n const count = Math.max(2, Math.min(MAX_COLORS, stops?.length ?? 2));\n return { arr, count };\n};\n\nconst GradientBlinds = ({\n className,\n dpr,\n paused = false,\n gradientColors,\n angle = 0,\n noise = 0.3,\n blindCount = 16,\n blindMinWidth = 60,\n mouseDampening = 0.15,\n mirrorGradient = false,\n spotlightRadius = 0.5,\n spotlightSoftness = 1,\n spotlightOpacity = 1,\n distortAmount = 0,\n shineDirection = 'left',\n mixBlendMode = 'lighten'\n}) => {\n const containerRef = useRef(null);\n const rafRef = useRef(null);\n const programRef = useRef(null);\n const meshRef = useRef(null);\n const geometryRef = useRef(null);\n const rendererRef = useRef(null);\n const mouseTargetRef = useRef([0, 0]);\n const lastTimeRef = useRef(0);\n const firstResizeRef = useRef(true);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n dpr: dpr ?? (typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1),\n alpha: true,\n antialias: true\n });\n rendererRef.current = renderer;\n const gl = renderer.gl;\n const canvas = gl.canvas;\n\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const vertex = `\nattribute vec2 position;\nattribute vec2 uv;\nvarying vec2 vUv;\n\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\n const fragment = `\n#ifdef GL_ES\nprecision mediump float;\n#endif\n\nuniform vec3 iResolution;\nuniform vec2 iMouse;\nuniform float iTime;\n\nuniform float uAngle;\nuniform float uNoise;\nuniform float uBlindCount;\nuniform float uSpotlightRadius;\nuniform float uSpotlightSoftness;\nuniform float uSpotlightOpacity;\nuniform float uMirror;\nuniform float uDistort;\nuniform float uShineFlip;\nuniform vec3 uColor0;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nuniform vec3 uColor4;\nuniform vec3 uColor5;\nuniform vec3 uColor6;\nuniform vec3 uColor7;\nuniform int uColorCount;\n\nvarying vec2 vUv;\n\nfloat rand(vec2 co){\n return fract(sin(dot(co, vec2(12.9898,78.233))) * 43758.5453);\n}\n\nvec2 rotate2D(vec2 p, float a){\n float c = cos(a);\n float s = sin(a);\n return mat2(c, -s, s, c) * p;\n}\n\nvec3 getGradientColor(float t){\n float tt = clamp(t, 0.0, 1.0);\n int count = uColorCount;\n if (count < 2) count = 2;\n float scaled = tt * float(count - 1);\n float seg = floor(scaled);\n float f = fract(scaled);\n\n if (seg < 1.0) return mix(uColor0, uColor1, f);\n if (seg < 2.0 && count > 2) return mix(uColor1, uColor2, f);\n if (seg < 3.0 && count > 3) return mix(uColor2, uColor3, f);\n if (seg < 4.0 && count > 4) return mix(uColor3, uColor4, f);\n if (seg < 5.0 && count > 5) return mix(uColor4, uColor5, f);\n if (seg < 6.0 && count > 6) return mix(uColor5, uColor6, f);\n if (seg < 7.0 && count > 7) return mix(uColor6, uColor7, f);\n if (count > 7) return uColor7;\n if (count > 6) return uColor6;\n if (count > 5) return uColor5;\n if (count > 4) return uColor4;\n if (count > 3) return uColor3;\n if (count > 2) return uColor2;\n return uColor1;\n}\n\nvoid mainImage( out vec4 fragColor, in vec2 fragCoord )\n{\n vec2 uv0 = fragCoord.xy / iResolution.xy;\n\n float aspect = iResolution.x / iResolution.y;\n vec2 p = uv0 * 2.0 - 1.0;\n p.x *= aspect;\n vec2 pr = rotate2D(p, uAngle);\n pr.x /= aspect;\n vec2 uv = pr * 0.5 + 0.5;\n\n vec2 uvMod = uv;\n if (uDistort > 0.0) {\n float a = uvMod.y * 6.0;\n float b = uvMod.x * 6.0;\n float w = 0.01 * uDistort;\n uvMod.x += sin(a) * w;\n uvMod.y += cos(b) * w;\n }\n float t = uvMod.x;\n if (uMirror > 0.5) {\n t = 1.0 - abs(1.0 - 2.0 * fract(t));\n }\n vec3 base = getGradientColor(t);\n\n vec2 offset = vec2(iMouse.x/iResolution.x, iMouse.y/iResolution.y);\n float d = length(uv0 - offset);\n float r = max(uSpotlightRadius, 1e-4);\n float dn = d / r;\n float spot = (1.0 - 2.0 * pow(dn, uSpotlightSoftness)) * uSpotlightOpacity;\n vec3 cir = vec3(spot);\n float stripe = fract(uvMod.x * max(uBlindCount, 1.0));\n if (uShineFlip > 0.5) stripe = 1.0 - stripe;\n vec3 ran = vec3(stripe);\n\n vec3 col = cir + base - ran;\n col += (rand(gl_FragCoord.xy + iTime) - 0.5) * uNoise;\n\n fragColor = vec4(col, 1.0);\n}\n\nvoid main() {\n vec4 color;\n mainImage(color, vUv * iResolution.xy);\n gl_FragColor = color;\n}\n`;\n\n const { arr: colorArr, count: colorCount } = prepStops(gradientColors);\n const uniforms = {\n iResolution: {\n value: [gl.drawingBufferWidth, gl.drawingBufferHeight, 1]\n },\n iMouse: { value: [0, 0] },\n iTime: { value: 0 },\n uAngle: { value: (angle * Math.PI) / 180 },\n uNoise: { value: noise },\n uBlindCount: { value: Math.max(1, blindCount) },\n uSpotlightRadius: { value: spotlightRadius },\n uSpotlightSoftness: { value: spotlightSoftness },\n uSpotlightOpacity: { value: spotlightOpacity },\n uMirror: { value: mirrorGradient ? 1 : 0 },\n uDistort: { value: distortAmount },\n uShineFlip: { value: shineDirection === 'right' ? 1 : 0 },\n uColor0: { value: colorArr[0] },\n uColor1: { value: colorArr[1] },\n uColor2: { value: colorArr[2] },\n uColor3: { value: colorArr[3] },\n uColor4: { value: colorArr[4] },\n uColor5: { value: colorArr[5] },\n uColor6: { value: colorArr[6] },\n uColor7: { value: colorArr[7] },\n uColorCount: { value: colorCount }\n };\n\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms\n });\n programRef.current = program;\n\n const geometry = new Triangle(gl);\n geometryRef.current = geometry;\n const mesh = new Mesh(gl, { geometry, program });\n meshRef.current = mesh;\n\n const resize = () => {\n const rect = container.getBoundingClientRect();\n renderer.setSize(rect.width, rect.height);\n uniforms.iResolution.value = [gl.drawingBufferWidth, gl.drawingBufferHeight, 1];\n\n if (blindMinWidth && blindMinWidth > 0) {\n const maxByMinWidth = Math.max(1, Math.floor(rect.width / blindMinWidth));\n\n const effective = blindCount ? Math.min(blindCount, maxByMinWidth) : maxByMinWidth;\n uniforms.uBlindCount.value = Math.max(1, effective);\n } else {\n uniforms.uBlindCount.value = Math.max(1, blindCount);\n }\n\n if (firstResizeRef.current) {\n firstResizeRef.current = false;\n const cx = gl.drawingBufferWidth / 2;\n const cy = gl.drawingBufferHeight / 2;\n uniforms.iMouse.value = [cx, cy];\n mouseTargetRef.current = [cx, cy];\n }\n };\n\n resize();\n const ro = new ResizeObserver(resize);\n ro.observe(container);\n\n const onPointerMove = e => {\n const rect = canvas.getBoundingClientRect();\n const scale = renderer.dpr || 1;\n const x = (e.clientX - rect.left) * scale;\n const y = (rect.height - (e.clientY - rect.top)) * scale;\n mouseTargetRef.current = [x, y];\n if (mouseDampening <= 0) {\n uniforms.iMouse.value = [x, y];\n }\n };\n canvas.addEventListener('pointermove', onPointerMove);\n\n const loop = t => {\n rafRef.current = requestAnimationFrame(loop);\n uniforms.iTime.value = t * 0.001;\n if (mouseDampening > 0) {\n if (!lastTimeRef.current) lastTimeRef.current = t;\n const dt = (t - lastTimeRef.current) / 1000;\n lastTimeRef.current = t;\n const tau = Math.max(1e-4, mouseDampening);\n let factor = 1 - Math.exp(-dt / tau);\n if (factor > 1) factor = 1;\n const target = mouseTargetRef.current;\n const cur = uniforms.iMouse.value;\n cur[0] += (target[0] - cur[0]) * factor;\n cur[1] += (target[1] - cur[1]) * factor;\n } else {\n lastTimeRef.current = t;\n }\n if (!paused && programRef.current && meshRef.current) {\n try {\n renderer.render({ scene: meshRef.current });\n } catch (e) {\n console.error(e);\n }\n }\n };\n rafRef.current = requestAnimationFrame(loop);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n canvas.removeEventListener('pointermove', onPointerMove);\n ro.disconnect();\n if (canvas.parentElement === container) {\n container.removeChild(canvas);\n }\n const callIfFn = (obj, key) => {\n if (obj && typeof obj[key] === 'function') {\n obj[key].call(obj);\n }\n };\n callIfFn(programRef.current, 'remove');\n callIfFn(geometryRef.current, 'remove');\n callIfFn(meshRef.current, 'remove');\n callIfFn(rendererRef.current, 'destroy');\n programRef.current = null;\n geometryRef.current = null;\n meshRef.current = null;\n rendererRef.current = null;\n };\n }, [\n dpr,\n paused,\n gradientColors,\n angle,\n noise,\n blindCount,\n blindMinWidth,\n mouseDampening,\n mirrorGradient,\n spotlightRadius,\n spotlightSoftness,\n spotlightOpacity,\n distortAmount,\n shineDirection\n ]);\n\n return (\n \n );\n};\n\nexport default GradientBlinds;\n" } ], diff --git a/public/r/GradientBlinds-TS-CSS.json b/public/r/GradientBlinds-TS-CSS.json index 89967a75..6e49d853 100644 --- a/public/r/GradientBlinds-TS-CSS.json +++ b/public/r/GradientBlinds-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GradientBlinds/GradientBlinds.css", + "type": "registry:file", + "path": "GradientBlinds.css", + "target": "@components/GradientBlinds.css", "content": ".gradient-blinds-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "GradientBlinds/GradientBlinds.tsx", + "path": "GradientBlinds.tsx", "content": "import React, { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './GradientBlinds.css';\n\nexport interface GradientBlindsProps {\n className?: string;\n dpr?: number;\n paused?: boolean;\n gradientColors?: string[];\n angle?: number;\n noise?: number;\n blindCount?: number;\n blindMinWidth?: number;\n mouseDampening?: number;\n mirrorGradient?: boolean;\n spotlightRadius?: number;\n spotlightSoftness?: number;\n spotlightOpacity?: number;\n distortAmount?: number;\n shineDirection?: 'left' | 'right';\n mixBlendMode?: string;\n}\n\nconst MAX_COLORS = 8;\nconst hexToRGB = (hex: string): [number, number, number] => {\n const c = hex.replace('#', '').padEnd(6, '0');\n const r = parseInt(c.slice(0, 2), 16) / 255;\n const g = parseInt(c.slice(2, 4), 16) / 255;\n const b = parseInt(c.slice(4, 6), 16) / 255;\n return [r, g, b];\n};\nconst prepStops = (stops?: string[]) => {\n const base = (stops && stops.length ? stops : ['#FF9FFC', '#5227FF']).slice(0, MAX_COLORS);\n if (base.length === 1) base.push(base[0]);\n while (base.length < MAX_COLORS) base.push(base[base.length - 1]);\n const arr: [number, number, number][] = [];\n for (let i = 0; i < MAX_COLORS; i++) arr.push(hexToRGB(base[i]));\n const count = Math.max(2, Math.min(MAX_COLORS, stops?.length ?? 2));\n return { arr, count };\n};\n\nconst GradientBlinds: React.FC = ({\n className,\n dpr,\n paused = false,\n gradientColors,\n angle = 0,\n noise = 0.3,\n blindCount = 16,\n blindMinWidth = 60,\n mouseDampening = 0.15,\n mirrorGradient = false,\n spotlightRadius = 0.5,\n spotlightSoftness = 1,\n spotlightOpacity = 1,\n distortAmount = 0,\n shineDirection = 'left',\n mixBlendMode = 'lighten'\n}) => {\n const containerRef = useRef(null);\n const rafRef = useRef(null);\n const programRef = useRef(null);\n const meshRef = useRef | null>(null);\n const geometryRef = useRef(null);\n const rendererRef = useRef(null);\n const mouseTargetRef = useRef<[number, number]>([0, 0]);\n const lastTimeRef = useRef(0);\n const firstResizeRef = useRef(true);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n dpr: dpr ?? (typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1),\n alpha: true,\n antialias: true\n });\n rendererRef.current = renderer;\n const gl = renderer.gl;\n const canvas = gl.canvas as HTMLCanvasElement;\n\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const vertex = `\nattribute vec2 position;\nattribute vec2 uv;\nvarying vec2 vUv;\n\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\n const fragment = `\n#ifdef GL_ES\nprecision mediump float;\n#endif\n\nuniform vec3 iResolution;\nuniform vec2 iMouse;\nuniform float iTime;\n\nuniform float uAngle;\nuniform float uNoise;\nuniform float uBlindCount;\nuniform float uSpotlightRadius;\nuniform float uSpotlightSoftness;\nuniform float uSpotlightOpacity;\nuniform float uMirror;\nuniform float uDistort;\nuniform float uShineFlip;\nuniform vec3 uColor0;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nuniform vec3 uColor4;\nuniform vec3 uColor5;\nuniform vec3 uColor6;\nuniform vec3 uColor7;\nuniform int uColorCount;\n\nvarying vec2 vUv;\n\nfloat rand(vec2 co){\n return fract(sin(dot(co, vec2(12.9898,78.233))) * 43758.5453);\n}\n\nvec2 rotate2D(vec2 p, float a){\n float c = cos(a);\n float s = sin(a);\n return mat2(c, -s, s, c) * p;\n}\n\nvec3 getGradientColor(float t){\n float tt = clamp(t, 0.0, 1.0);\n int count = uColorCount;\n if (count < 2) count = 2;\n float scaled = tt * float(count - 1);\n float seg = floor(scaled);\n float f = fract(scaled);\n\n if (seg < 1.0) return mix(uColor0, uColor1, f);\n if (seg < 2.0 && count > 2) return mix(uColor1, uColor2, f);\n if (seg < 3.0 && count > 3) return mix(uColor2, uColor3, f);\n if (seg < 4.0 && count > 4) return mix(uColor3, uColor4, f);\n if (seg < 5.0 && count > 5) return mix(uColor4, uColor5, f);\n if (seg < 6.0 && count > 6) return mix(uColor5, uColor6, f);\n if (seg < 7.0 && count > 7) return mix(uColor6, uColor7, f);\n if (count > 7) return uColor7;\n if (count > 6) return uColor6;\n if (count > 5) return uColor5;\n if (count > 4) return uColor4;\n if (count > 3) return uColor3;\n if (count > 2) return uColor2;\n return uColor1;\n}\n\nvoid mainImage( out vec4 fragColor, in vec2 fragCoord )\n{\n vec2 uv0 = fragCoord.xy / iResolution.xy;\n\n float aspect = iResolution.x / iResolution.y;\n vec2 p = uv0 * 2.0 - 1.0;\n p.x *= aspect;\n vec2 pr = rotate2D(p, uAngle);\n pr.x /= aspect;\n vec2 uv = pr * 0.5 + 0.5;\n\n vec2 uvMod = uv;\n if (uDistort > 0.0) {\n float a = uvMod.y * 6.0;\n float b = uvMod.x * 6.0;\n float w = 0.01 * uDistort;\n uvMod.x += sin(a) * w;\n uvMod.y += cos(b) * w;\n }\n float t = uvMod.x;\n if (uMirror > 0.5) {\n t = 1.0 - abs(1.0 - 2.0 * fract(t));\n }\n vec3 base = getGradientColor(t);\n\n vec2 offset = vec2(iMouse.x/iResolution.x, iMouse.y/iResolution.y);\n float d = length(uv0 - offset);\n float r = max(uSpotlightRadius, 1e-4);\n float dn = d / r;\n float spot = (1.0 - 2.0 * pow(dn, uSpotlightSoftness)) * uSpotlightOpacity;\n vec3 cir = vec3(spot);\n float stripe = fract(uvMod.x * max(uBlindCount, 1.0));\n if (uShineFlip > 0.5) stripe = 1.0 - stripe;\n vec3 ran = vec3(stripe);\n\n vec3 col = cir + base - ran;\n col += (rand(gl_FragCoord.xy + iTime) - 0.5) * uNoise;\n\n fragColor = vec4(col, 1.0);\n}\n\nvoid main() {\n vec4 color;\n mainImage(color, vUv * iResolution.xy);\n gl_FragColor = color;\n}\n`;\n\n const { arr: colorArr, count: colorCount } = prepStops(gradientColors);\n const uniforms: {\n iResolution: { value: [number, number, number] };\n iMouse: { value: [number, number] };\n iTime: { value: number };\n uAngle: { value: number };\n uNoise: { value: number };\n uBlindCount: { value: number };\n uSpotlightRadius: { value: number };\n uSpotlightSoftness: { value: number };\n uSpotlightOpacity: { value: number };\n uMirror: { value: number };\n uDistort: { value: number };\n uShineFlip: { value: number };\n uColor0: { value: [number, number, number] };\n uColor1: { value: [number, number, number] };\n uColor2: { value: [number, number, number] };\n uColor3: { value: [number, number, number] };\n uColor4: { value: [number, number, number] };\n uColor5: { value: [number, number, number] };\n uColor6: { value: [number, number, number] };\n uColor7: { value: [number, number, number] };\n uColorCount: { value: number };\n } = {\n iResolution: {\n value: [gl.drawingBufferWidth, gl.drawingBufferHeight, 1]\n },\n iMouse: { value: [0, 0] },\n iTime: { value: 0 },\n uAngle: { value: (angle * Math.PI) / 180 },\n uNoise: { value: noise },\n uBlindCount: { value: Math.max(1, blindCount) },\n uSpotlightRadius: { value: spotlightRadius },\n uSpotlightSoftness: { value: spotlightSoftness },\n uSpotlightOpacity: { value: spotlightOpacity },\n uMirror: { value: mirrorGradient ? 1 : 0 },\n uDistort: { value: distortAmount },\n uShineFlip: { value: shineDirection === 'right' ? 1 : 0 },\n uColor0: { value: colorArr[0] },\n uColor1: { value: colorArr[1] },\n uColor2: { value: colorArr[2] },\n uColor3: { value: colorArr[3] },\n uColor4: { value: colorArr[4] },\n uColor5: { value: colorArr[5] },\n uColor6: { value: colorArr[6] },\n uColor7: { value: colorArr[7] },\n uColorCount: { value: colorCount }\n };\n\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms\n });\n programRef.current = program;\n\n const geometry = new Triangle(gl);\n geometryRef.current = geometry;\n const mesh = new Mesh(gl, { geometry, program });\n meshRef.current = mesh;\n\n const resize = () => {\n const rect = container.getBoundingClientRect();\n renderer.setSize(rect.width, rect.height);\n uniforms.iResolution.value = [gl.drawingBufferWidth, gl.drawingBufferHeight, 1];\n\n if (blindMinWidth && blindMinWidth > 0) {\n const maxByMinWidth = Math.max(1, Math.floor(rect.width / blindMinWidth));\n\n const effective = blindCount ? Math.min(blindCount, maxByMinWidth) : maxByMinWidth;\n uniforms.uBlindCount.value = Math.max(1, effective);\n } else {\n uniforms.uBlindCount.value = Math.max(1, blindCount);\n }\n\n if (firstResizeRef.current) {\n firstResizeRef.current = false;\n const cx = gl.drawingBufferWidth / 2;\n const cy = gl.drawingBufferHeight / 2;\n uniforms.iMouse.value = [cx, cy];\n mouseTargetRef.current = [cx, cy];\n }\n };\n\n resize();\n const ro = new ResizeObserver(resize);\n ro.observe(container);\n\n const onPointerMove = (e: PointerEvent) => {\n const rect = canvas.getBoundingClientRect();\n const scale = (renderer as unknown as { dpr?: number }).dpr || 1;\n const x = (e.clientX - rect.left) * scale;\n const y = (rect.height - (e.clientY - rect.top)) * scale;\n mouseTargetRef.current = [x, y];\n if (mouseDampening <= 0) {\n uniforms.iMouse.value = [x, y];\n }\n };\n canvas.addEventListener('pointermove', onPointerMove);\n\n const loop = (t: number) => {\n rafRef.current = requestAnimationFrame(loop);\n uniforms.iTime.value = t * 0.001;\n if (mouseDampening > 0) {\n if (!lastTimeRef.current) lastTimeRef.current = t;\n const dt = (t - lastTimeRef.current) / 1000;\n lastTimeRef.current = t;\n const tau = Math.max(1e-4, mouseDampening);\n let factor = 1 - Math.exp(-dt / tau);\n if (factor > 1) factor = 1;\n const target = mouseTargetRef.current;\n const cur = uniforms.iMouse.value;\n cur[0] += (target[0] - cur[0]) * factor;\n cur[1] += (target[1] - cur[1]) * factor;\n } else {\n lastTimeRef.current = t;\n }\n if (!paused && programRef.current && meshRef.current) {\n try {\n renderer.render({ scene: meshRef.current });\n } catch (e) {\n console.error(e);\n }\n }\n };\n rafRef.current = requestAnimationFrame(loop);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n canvas.removeEventListener('pointermove', onPointerMove);\n ro.disconnect();\n if (canvas.parentElement === container) {\n container.removeChild(canvas);\n }\n const callIfFn = (obj: T | null, key: K) => {\n if (obj && typeof obj[key] === 'function') {\n (obj[key] as unknown as () => void).call(obj);\n }\n };\n callIfFn(programRef.current, 'remove');\n callIfFn(geometryRef.current, 'remove');\n callIfFn(meshRef.current as unknown as { remove?: () => void }, 'remove');\n callIfFn(rendererRef.current as unknown as { destroy?: () => void }, 'destroy');\n programRef.current = null;\n geometryRef.current = null;\n meshRef.current = null;\n rendererRef.current = null;\n };\n }, [\n dpr,\n paused,\n gradientColors,\n angle,\n noise,\n blindCount,\n blindMinWidth,\n mouseDampening,\n mirrorGradient,\n spotlightRadius,\n spotlightSoftness,\n spotlightOpacity,\n distortAmount,\n shineDirection\n ]);\n\n return (\n \n );\n};\n\nexport default GradientBlinds;\n" } ], diff --git a/public/r/GradientText-JS-CSS.json b/public/r/GradientText-JS-CSS.json index 098edb51..c5b82d3c 100644 --- a/public/r/GradientText-JS-CSS.json +++ b/public/r/GradientText-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GradientText/GradientText.css", + "type": "registry:file", + "path": "GradientText.css", + "target": "@components/GradientText.css", "content": ".animated-gradient-text {\n position: relative;\n margin: 0 auto;\n display: flex;\n max-width: fit-content;\n flex-direction: row;\n align-items: center;\n justify-content: center;\n border-radius: 1.25rem;\n font-weight: 500;\n backdrop-filter: blur(10px);\n transition: box-shadow 0.5s ease-out;\n overflow: hidden;\n cursor: pointer;\n}\n\n.animated-gradient-text.with-border {\n padding: 0.35rem 0.75rem;\n}\n\n.gradient-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n border-radius: inherit;\n z-index: 0;\n pointer-events: none;\n}\n\n.gradient-overlay::before {\n content: '';\n position: absolute;\n left: 0;\n top: 0;\n border-radius: inherit;\n width: calc(100% - 2px);\n height: calc(100% - 2px);\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n background-color: #120F17;\n z-index: -1;\n}\n\n.text-content {\n display: inline-block;\n position: relative;\n z-index: 2;\n background-clip: text;\n -webkit-background-clip: text;\n color: transparent;\n}\n" }, { "type": "registry:component", - "path": "GradientText/GradientText.jsx", + "path": "GradientText.jsx", "content": "import { useState, useCallback, useEffect, useRef } from 'react';\nimport { motion, useMotionValue, useAnimationFrame, useTransform } from 'motion/react';\nimport './GradientText.css';\n\nexport default function GradientText({\n children,\n className = '',\n colors = ['#5227FF', '#FF9FFC', '#B497CF'],\n animationSpeed = 8,\n showBorder = false,\n direction = 'horizontal',\n pauseOnHover = false,\n yoyo = true\n}) {\n const [isPaused, setIsPaused] = useState(false);\n const progress = useMotionValue(0);\n const elapsedRef = useRef(0);\n const lastTimeRef = useRef(null);\n\n const animationDuration = animationSpeed * 1000;\n\n useAnimationFrame(time => {\n if (isPaused) {\n lastTimeRef.current = null;\n return;\n }\n\n if (lastTimeRef.current === null) {\n lastTimeRef.current = time;\n return;\n }\n\n const deltaTime = time - lastTimeRef.current;\n lastTimeRef.current = time;\n elapsedRef.current += deltaTime;\n\n if (yoyo) {\n const fullCycle = animationDuration * 2;\n const cycleTime = elapsedRef.current % fullCycle;\n\n if (cycleTime < animationDuration) {\n progress.set((cycleTime / animationDuration) * 100);\n } else {\n progress.set(100 - ((cycleTime - animationDuration) / animationDuration) * 100);\n }\n } else {\n // Continuously increase position for seamless looping\n progress.set((elapsedRef.current / animationDuration) * 100);\n }\n });\n\n useEffect(() => {\n elapsedRef.current = 0;\n progress.set(0);\n }, [animationSpeed, progress, yoyo]);\n\n const backgroundPosition = useTransform(progress, p => {\n if (direction === 'horizontal') {\n return `${p}% 50%`;\n } else if (direction === 'vertical') {\n return `50% ${p}%`;\n } else {\n // For diagonal, move only horizontally to avoid interference patterns\n return `${p}% 50%`;\n }\n });\n\n const handleMouseEnter = useCallback(() => {\n if (pauseOnHover) setIsPaused(true);\n }, [pauseOnHover]);\n\n const handleMouseLeave = useCallback(() => {\n if (pauseOnHover) setIsPaused(false);\n }, [pauseOnHover]);\n\n const gradientAngle =\n direction === 'horizontal' ? 'to right' : direction === 'vertical' ? 'to bottom' : 'to bottom right';\n // Duplicate first color at the end for seamless looping\n const gradientColors = [...colors, colors[0]].join(', ');\n\n const gradientStyle = {\n backgroundImage: `linear-gradient(${gradientAngle}, ${gradientColors})`,\n backgroundSize: direction === 'horizontal' ? '300% 100%' : direction === 'vertical' ? '100% 300%' : '300% 300%',\n backgroundRepeat: 'repeat'\n };\n\n return (\n \n {showBorder && }\n \n {children}\n \n \n );\n}\n" } ], diff --git a/public/r/GradientText-TS-CSS.json b/public/r/GradientText-TS-CSS.json index d3dceac2..63201bdd 100644 --- a/public/r/GradientText-TS-CSS.json +++ b/public/r/GradientText-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GradientText/GradientText.css", + "type": "registry:file", + "path": "GradientText.css", + "target": "@components/GradientText.css", "content": ".animated-gradient-text {\n position: relative;\n margin: 0 auto;\n display: flex;\n max-width: fit-content;\n flex-direction: row;\n align-items: center;\n justify-content: center;\n border-radius: 1.25rem;\n font-weight: 500;\n backdrop-filter: blur(10px);\n transition: box-shadow 0.5s ease-out;\n overflow: hidden;\n cursor: pointer;\n}\n\n.gradient-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background-size: 300% 100%;\n animation: gradient linear infinite;\n border-radius: inherit;\n z-index: 0;\n pointer-events: none;\n}\n\n.gradient-overlay::before {\n content: '';\n position: absolute;\n left: 0;\n top: 0;\n border-radius: inherit;\n width: calc(100% - 2px);\n height: calc(100% - 2px);\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n background-color: #120F17;\n z-index: -1;\n}\n\n@keyframes gradient {\n 0% {\n background-position: 0% 50%;\n }\n\n 50% {\n background-position: 100% 50%;\n }\n\n 100% {\n background-position: 0% 50%;\n }\n}\n\n.text-content {\n display: inline-block;\n position: relative;\n z-index: 2;\n background-size: 300% 100%;\n background-clip: text;\n -webkit-background-clip: text;\n color: transparent;\n animation: gradient linear infinite;\n}\n" }, { "type": "registry:component", - "path": "GradientText/GradientText.tsx", - "content": "import { useState, useCallback, useEffect, useRef, ReactNode } from 'react';\nimport { motion, useMotionValue, useAnimationFrame, useTransform } from 'motion/react';\nimport './GradientText.css';\n\ninterface GradientTextProps {\n children: ReactNode;\n className?: string;\n colors?: string[];\n animationSpeed?: number;\n showBorder?: boolean;\n direction?: 'horizontal' | 'vertical' | 'diagonal';\n pauseOnHover?: boolean;\n yoyo?: boolean;\n}\n\nexport default function GradientText({\n children,\n className = '',\n colors = ['#5227FF', '#FF9FFC', '#B497CF'],\n animationSpeed = 8,\n showBorder = false,\n direction = 'horizontal',\n pauseOnHover = false,\n yoyo = true\n}: GradientTextProps) {\n const [isPaused, setIsPaused] = useState(false);\n const progress = useMotionValue(0);\n const elapsedRef = useRef(0);\n const lastTimeRef = useRef(null);\n\n const animationDuration = animationSpeed * 1000;\n\n useAnimationFrame(time => {\n if (isPaused) {\n lastTimeRef.current = null;\n return;\n }\n\n if (lastTimeRef.current === null) {\n lastTimeRef.current = time;\n return;\n }\n\n const deltaTime = time - lastTimeRef.current;\n lastTimeRef.current = time;\n elapsedRef.current += deltaTime;\n\n if (yoyo) {\n const fullCycle = animationDuration * 2;\n const cycleTime = elapsedRef.current % fullCycle;\n\n if (cycleTime < animationDuration) {\n progress.set((cycleTime / animationDuration) * 100);\n } else {\n progress.set(100 - ((cycleTime - animationDuration) / animationDuration) * 100);\n }\n } else {\n // Continuously increase position for seamless looping\n progress.set((elapsedRef.current / animationDuration) * 100);\n }\n });\n\n useEffect(() => {\n elapsedRef.current = 0;\n progress.set(0);\n }, [animationSpeed, yoyo]);\n\n const backgroundPosition = useTransform(progress, p => {\n if (direction === 'horizontal') {\n return `${p}% 50%`;\n } else if (direction === 'vertical') {\n return `50% ${p}%`;\n } else {\n // For diagonal, move only horizontally to avoid interference patterns\n return `${p}% 50%`;\n }\n });\n\n const handleMouseEnter = useCallback(() => {\n if (pauseOnHover) setIsPaused(true);\n }, [pauseOnHover]);\n\n const handleMouseLeave = useCallback(() => {\n if (pauseOnHover) setIsPaused(false);\n }, [pauseOnHover]);\n\n const gradientAngle =\n direction === 'horizontal' ? 'to right' : direction === 'vertical' ? 'to bottom' : 'to bottom right';\n // Duplicate first color at the end for seamless looping\n const gradientColors = [...colors, colors[0]].join(', ');\n\n const gradientStyle = {\n backgroundImage: `linear-gradient(${gradientAngle}, ${gradientColors})`,\n backgroundSize: direction === 'horizontal' ? '300% 100%' : direction === 'vertical' ? '100% 300%' : '300% 300%',\n backgroundRepeat: 'repeat'\n };\n\n return (\n \n {showBorder && }\n \n {children}\n \n \n );\n}\n" + "path": "GradientText.tsx", + "content": "import { useState, useCallback, useEffect, useRef, type ReactNode } from 'react';\nimport { motion, useMotionValue, useAnimationFrame, useTransform } from 'motion/react';\nimport './GradientText.css';\n\ninterface GradientTextProps {\n children: ReactNode;\n className?: string;\n colors?: string[];\n animationSpeed?: number;\n showBorder?: boolean;\n direction?: 'horizontal' | 'vertical' | 'diagonal';\n pauseOnHover?: boolean;\n yoyo?: boolean;\n}\n\nexport default function GradientText({\n children,\n className = '',\n colors = ['#5227FF', '#FF9FFC', '#B497CF'],\n animationSpeed = 8,\n showBorder = false,\n direction = 'horizontal',\n pauseOnHover = false,\n yoyo = true\n}: GradientTextProps) {\n const [isPaused, setIsPaused] = useState(false);\n const progress = useMotionValue(0);\n const elapsedRef = useRef(0);\n const lastTimeRef = useRef(null);\n\n const animationDuration = animationSpeed * 1000;\n\n useAnimationFrame(time => {\n if (isPaused) {\n lastTimeRef.current = null;\n return;\n }\n\n if (lastTimeRef.current === null) {\n lastTimeRef.current = time;\n return;\n }\n\n const deltaTime = time - lastTimeRef.current;\n lastTimeRef.current = time;\n elapsedRef.current += deltaTime;\n\n if (yoyo) {\n const fullCycle = animationDuration * 2;\n const cycleTime = elapsedRef.current % fullCycle;\n\n if (cycleTime < animationDuration) {\n progress.set((cycleTime / animationDuration) * 100);\n } else {\n progress.set(100 - ((cycleTime - animationDuration) / animationDuration) * 100);\n }\n } else {\n // Continuously increase position for seamless looping\n progress.set((elapsedRef.current / animationDuration) * 100);\n }\n });\n\n useEffect(() => {\n elapsedRef.current = 0;\n progress.set(0);\n }, [animationSpeed, yoyo]);\n\n const backgroundPosition = useTransform(progress, p => {\n if (direction === 'horizontal') {\n return `${p}% 50%`;\n } else if (direction === 'vertical') {\n return `50% ${p}%`;\n } else {\n // For diagonal, move only horizontally to avoid interference patterns\n return `${p}% 50%`;\n }\n });\n\n const handleMouseEnter = useCallback(() => {\n if (pauseOnHover) setIsPaused(true);\n }, [pauseOnHover]);\n\n const handleMouseLeave = useCallback(() => {\n if (pauseOnHover) setIsPaused(false);\n }, [pauseOnHover]);\n\n const gradientAngle =\n direction === 'horizontal' ? 'to right' : direction === 'vertical' ? 'to bottom' : 'to bottom right';\n // Duplicate first color at the end for seamless looping\n const gradientColors = [...colors, colors[0]].join(', ');\n\n const gradientStyle = {\n backgroundImage: `linear-gradient(${gradientAngle}, ${gradientColors})`,\n backgroundSize: direction === 'horizontal' ? '300% 100%' : direction === 'vertical' ? '100% 300%' : '300% 300%',\n backgroundRepeat: 'repeat'\n };\n\n return (\n \n {showBorder && }\n \n {children}\n \n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/GradientText-TS-TW.json b/public/r/GradientText-TS-TW.json index d6c7900f..f525a2b3 100644 --- a/public/r/GradientText-TS-TW.json +++ b/public/r/GradientText-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "GradientText/GradientText.tsx", - "content": "import { useState, useCallback, useEffect, useRef, ReactNode } from 'react';\nimport { motion, useMotionValue, useAnimationFrame, useTransform } from 'motion/react';\n\ninterface GradientTextProps {\n children: ReactNode;\n className?: string;\n colors?: string[];\n animationSpeed?: number;\n showBorder?: boolean;\n direction?: 'horizontal' | 'vertical' | 'diagonal';\n pauseOnHover?: boolean;\n yoyo?: boolean;\n}\n\nexport default function GradientText({\n children,\n className = '',\n colors = ['#5227FF', '#FF9FFC', '#B497CF'],\n animationSpeed = 8,\n showBorder = false,\n direction = 'horizontal',\n pauseOnHover = false,\n yoyo = true\n}: GradientTextProps) {\n const [isPaused, setIsPaused] = useState(false);\n const progress = useMotionValue(0);\n const elapsedRef = useRef(0);\n const lastTimeRef = useRef(null);\n\n const animationDuration = animationSpeed * 1000;\n\n useAnimationFrame(time => {\n if (isPaused) {\n lastTimeRef.current = null;\n return;\n }\n\n if (lastTimeRef.current === null) {\n lastTimeRef.current = time;\n return;\n }\n\n const deltaTime = time - lastTimeRef.current;\n lastTimeRef.current = time;\n elapsedRef.current += deltaTime;\n\n if (yoyo) {\n const fullCycle = animationDuration * 2;\n const cycleTime = elapsedRef.current % fullCycle;\n\n if (cycleTime < animationDuration) {\n progress.set((cycleTime / animationDuration) * 100);\n } else {\n progress.set(100 - ((cycleTime - animationDuration) / animationDuration) * 100);\n }\n } else {\n // Continuously increase position for seamless looping\n progress.set((elapsedRef.current / animationDuration) * 100);\n }\n });\n\n useEffect(() => {\n elapsedRef.current = 0;\n progress.set(0);\n }, [animationSpeed, yoyo]);\n\n const backgroundPosition = useTransform(progress, p => {\n if (direction === 'horizontal') {\n return `${p}% 50%`;\n } else if (direction === 'vertical') {\n return `50% ${p}%`;\n } else {\n // For diagonal, move only horizontally to avoid interference patterns\n return `${p}% 50%`;\n }\n });\n\n const handleMouseEnter = useCallback(() => {\n if (pauseOnHover) setIsPaused(true);\n }, [pauseOnHover]);\n\n const handleMouseLeave = useCallback(() => {\n if (pauseOnHover) setIsPaused(false);\n }, [pauseOnHover]);\n\n const gradientAngle =\n direction === 'horizontal' ? 'to right' : direction === 'vertical' ? 'to bottom' : 'to bottom right';\n // Duplicate first color at the end for seamless looping\n const gradientColors = [...colors, colors[0]].join(', ');\n\n const gradientStyle = {\n backgroundImage: `linear-gradient(${gradientAngle}, ${gradientColors})`,\n backgroundSize: direction === 'horizontal' ? '300% 100%' : direction === 'vertical' ? '100% 300%' : '300% 300%',\n backgroundRepeat: 'repeat'\n };\n\n return (\n \n {showBorder && (\n \n \n \n )}\n \n {children}\n \n \n );\n}\n" + "content": "import { useState, useCallback, useEffect, useRef, type ReactNode } from 'react';\nimport { motion, useMotionValue, useAnimationFrame, useTransform } from 'motion/react';\n\ninterface GradientTextProps {\n children: ReactNode;\n className?: string;\n colors?: string[];\n animationSpeed?: number;\n showBorder?: boolean;\n direction?: 'horizontal' | 'vertical' | 'diagonal';\n pauseOnHover?: boolean;\n yoyo?: boolean;\n}\n\nexport default function GradientText({\n children,\n className = '',\n colors = ['#5227FF', '#FF9FFC', '#B497CF'],\n animationSpeed = 8,\n showBorder = false,\n direction = 'horizontal',\n pauseOnHover = false,\n yoyo = true\n}: GradientTextProps) {\n const [isPaused, setIsPaused] = useState(false);\n const progress = useMotionValue(0);\n const elapsedRef = useRef(0);\n const lastTimeRef = useRef(null);\n\n const animationDuration = animationSpeed * 1000;\n\n useAnimationFrame(time => {\n if (isPaused) {\n lastTimeRef.current = null;\n return;\n }\n\n if (lastTimeRef.current === null) {\n lastTimeRef.current = time;\n return;\n }\n\n const deltaTime = time - lastTimeRef.current;\n lastTimeRef.current = time;\n elapsedRef.current += deltaTime;\n\n if (yoyo) {\n const fullCycle = animationDuration * 2;\n const cycleTime = elapsedRef.current % fullCycle;\n\n if (cycleTime < animationDuration) {\n progress.set((cycleTime / animationDuration) * 100);\n } else {\n progress.set(100 - ((cycleTime - animationDuration) / animationDuration) * 100);\n }\n } else {\n // Continuously increase position for seamless looping\n progress.set((elapsedRef.current / animationDuration) * 100);\n }\n });\n\n useEffect(() => {\n elapsedRef.current = 0;\n progress.set(0);\n }, [animationSpeed, yoyo]);\n\n const backgroundPosition = useTransform(progress, p => {\n if (direction === 'horizontal') {\n return `${p}% 50%`;\n } else if (direction === 'vertical') {\n return `50% ${p}%`;\n } else {\n // For diagonal, move only horizontally to avoid interference patterns\n return `${p}% 50%`;\n }\n });\n\n const handleMouseEnter = useCallback(() => {\n if (pauseOnHover) setIsPaused(true);\n }, [pauseOnHover]);\n\n const handleMouseLeave = useCallback(() => {\n if (pauseOnHover) setIsPaused(false);\n }, [pauseOnHover]);\n\n const gradientAngle =\n direction === 'horizontal' ? 'to right' : direction === 'vertical' ? 'to bottom' : 'to bottom right';\n // Duplicate first color at the end for seamless looping\n const gradientColors = [...colors, colors[0]].join(', ');\n\n const gradientStyle = {\n backgroundImage: `linear-gradient(${gradientAngle}, ${gradientColors})`,\n backgroundSize: direction === 'horizontal' ? '300% 100%' : direction === 'vertical' ? '100% 300%' : '300% 300%',\n backgroundRepeat: 'repeat'\n };\n\n return (\n \n {showBorder && (\n \n \n \n )}\n \n {children}\n \n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/GradientWaves-JS-CSS.json b/public/r/GradientWaves-JS-CSS.json index 0ced6da4..072d1049 100644 --- a/public/r/GradientWaves-JS-CSS.json +++ b/public/r/GradientWaves-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GradientWaves/GradientWaves.css", + "type": "registry:file", + "path": "GradientWaves.css", + "target": "@components/GradientWaves.css", "content": ".gradient-waves-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "GradientWaves/GradientWaves.jsx", + "path": "GradientWaves.jsx", "content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './GradientWaves.css';\n\nconst hexToRgb = hex => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 1, 1];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst detailToSteps = detail => {\n if (detail === 'low') return 40.0;\n if (detail === 'high') return 110.0;\n return 70.0;\n};\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uSpeed;\nuniform float uAmplitude;\nuniform float uWaveScale;\nuniform float uWaveRatio;\nuniform float uSwell;\nuniform float uTurbulence;\nuniform float uTilt;\nuniform float uZoom;\nuniform float uHeight;\nuniform float uFogDepth;\nuniform float uSteps;\nuniform float uBrightness;\nuniform float uOpacity;\nuniform float uGrain;\nuniform float uGrainIntensity;\nuniform vec2 uMouse;\nuniform float uParallax;\nuniform bool uEnableMouse;\nuniform vec3 uHorizonColor;\nuniform vec3 uWaveColor;\nuniform vec3 uCrestColor;\nout vec4 fragColor;\n\nconst float MAX_DIST = 20000.0;\n\nfloat hash21(vec2 p) {\n vec3 p3 = fract(vec3(p.xyx) * 0.1031);\n p3 += dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfloat plasma(vec3 r, vec2 freq, vec4 tc) {\n float mx = r.x + tc.x;\n mx += uSwell * sin((r.y + mx) / 20.0 + tc.y);\n float my = r.y - tc.z;\n my += uTurbulence * cos(r.x / 23.0 + tc.w);\n return r.z - (sin(mx * freq.x) * uAmplitude + sin(my * freq.y) * uAmplitude + uHeight);\n}\n\nfloat raymarch(vec3 pos, vec3 dir, vec2 freq, vec4 tc) {\n float dist = 0.0;\n for (int i = 0; i < 128; i++) {\n if (float(i) >= uSteps) break;\n float dscene = plasma(pos + dist * dir, freq, tc);\n if (abs(dscene) < 0.1) break;\n dist += 0.9 * dscene;\n if (!(abs(dist) < MAX_DIST)) return MAX_DIST;\n }\n return dist;\n}\n\nvoid main() {\n float T = iTime * uSpeed;\n vec2 freq = vec2(uWaveScale / 7.0, (uWaveScale * uWaveRatio) / 3.0);\n vec4 tc = vec4(T / 0.130, T / 0.810, T / 0.200, T / 0.710);\n float c, s;\n float vfov = (3.14159 / 2.3) / max(uZoom, 0.05);\n vec3 cam = vec3(0.0, 0.0, 30.0);\n vec2 uv = (gl_FragCoord.xy / iResolution.xy) - 0.5;\n uv.x *= iResolution.x / iResolution.y;\n uv.y *= -1.0;\n\n vec3 dir = vec3(0.0, 0.0, -1.0);\n float ulen = length(uv);\n float xrot = vfov * ulen;\n c = cos(xrot); s = sin(xrot);\n dir = mat3(1.0, 0.0, 0.0, 0.0, c, -s, 0.0, s, c) * dir;\n vec2 nuv = ulen > 1e-5 ? uv / ulen : vec2(1.0, 0.0);\n c = nuv.x; s = nuv.y;\n dir = mat3(c, -s, 0.0, s, c, 0.0, 0.0, 0.0, 1.0) * dir;\n c = cos(uTilt); s = sin(uTilt);\n dir = mat3(c, 0.0, s, 0.0, 1.0, 0.0, -s, 0.0, c) * dir;\n\n if (uEnableMouse) {\n float yaw = (uMouse.x - 0.5) * uParallax * 0.4;\n float pitch = (uMouse.y - 0.5) * uParallax * 0.4;\n c = cos(yaw); s = sin(yaw);\n dir = mat3(c, 0.0, s, 0.0, 1.0, 0.0, -s, 0.0, c) * dir;\n c = cos(pitch); s = sin(pitch);\n dir = mat3(1.0, 0.0, 0.0, 0.0, c, -s, 0.0, s, c) * dir;\n }\n\n float dist = raymarch(cam, dir, freq, tc);\n vec3 pos = cam + dist * dir;\n\n float t = clamp(uFogDepth / max(dist, 0.001), 0.0, 1.0);\n vec3 body = mix(uWaveColor, uCrestColor, clamp(pos.z * 0.08 + 0.5, 0.0, 1.0));\n vec3 col = mix(uHorizonColor, body, t);\n col *= uBrightness;\n col = clamp(col, 0.0, 1.0);\n\n float alpha = clamp(t, 0.0, 1.0) * uOpacity;\n if (uGrain > 0.5) {\n float g = hash21(gl_FragCoord.xy + mod(iTime, 64.0) * 11.0);\n alpha += (g - 0.5) * uGrainIntensity;\n }\n alpha = clamp(alpha, 0.0, 1.0);\n fragColor = vec4(col * alpha, alpha);\n}\n`;\n\nconst ctxMap = new WeakMap();\n\nconst GradientWaves = ({\n horizonColor = '#5227FF',\n waveColor = '#FF9FFC',\n crestColor = '#FFFFFF',\n speed = 0.4,\n amplitude = 2.5,\n waveScale = 0.6,\n waveRatio = 0.9,\n swell = 35,\n turbulence = 20,\n tilt = 1.11,\n zoom = 1.0,\n height = 5.5,\n fogDepth = 15,\n detail = 'medium',\n brightness = 1.0,\n opacity = 1.0,\n mouseInteraction = true,\n parallaxStrength = 0.5,\n grain = true,\n grainIntensity = 0.05,\n className = ''\n}) => {\n const containerRef = useRef(null);\n const enableMouseRef = useRef(mouseInteraction);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n webgl: 2,\n alpha: true,\n premultipliedAlpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n const canvas = gl.canvas;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uSpeed: { value: 0.4 },\n uAmplitude: { value: 2.5 },\n uWaveScale: { value: 0.6 },\n uWaveRatio: { value: 0.9 },\n uSwell: { value: 35 },\n uTurbulence: { value: 20 },\n uTilt: { value: 1.11 },\n uZoom: { value: 1.0 },\n uHeight: { value: 5.5 },\n uFogDepth: { value: 15 },\n uSteps: { value: 70.0 },\n uBrightness: { value: 1.0 },\n uOpacity: { value: 1.0 },\n uGrain: { value: 1.0 },\n uGrainIntensity: { value: 0.05 },\n uMouse: { value: new Float32Array([0.5, 0.5]) },\n uParallax: { value: 0.5 },\n uEnableMouse: { value: true },\n uHorizonColor: { value: new Float32Array([1, 1, 1]) },\n uWaveColor: { value: new Float32Array([1, 1, 1]) },\n uCrestColor: { value: new Float32Array([1, 1, 1]) }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n ctxMap.set(container, { renderer, program, mesh });\n\n const setSize = () => {\n const rect = container.getBoundingClientRect();\n const w = Math.max(1, Math.floor(rect.width));\n const h = Math.max(1, Math.floor(rect.height));\n renderer.setSize(w, h);\n const res = program.uniforms.iResolution.value;\n res[0] = gl.drawingBufferWidth;\n res[1] = gl.drawingBufferHeight;\n renderer.render({ scene: mesh });\n };\n\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n setSize();\n\n const currentMouse = [0.5, 0.5];\n const targetMouse = [0.5, 0.5];\n\n const onPointerMove = e => {\n const rect = canvas.getBoundingClientRect();\n targetMouse[0] = (e.clientX - rect.left) / rect.width;\n targetMouse[1] = 1.0 - (e.clientY - rect.top) / rect.height;\n };\n const onPointerLeave = () => {\n targetMouse[0] = 0.5;\n targetMouse[1] = 0.5;\n };\n canvas.addEventListener('pointermove', onPointerMove);\n canvas.addEventListener('pointerleave', onPointerLeave);\n\n let raf = 0;\n let isVisible = true;\n let isPageVisible = !document.hidden;\n const t0 = performance.now();\n\n const loop = t => {\n program.uniforms.iTime.value = (t - t0) * 0.001;\n const tx = enableMouseRef.current ? targetMouse[0] : 0.5;\n const ty = enableMouseRef.current ? targetMouse[1] : 0.5;\n currentMouse[0] += 0.05 * (tx - currentMouse[0]);\n currentMouse[1] += 0.05 * (ty - currentMouse[1]);\n program.uniforms.uMouse.value[0] = currentMouse[0];\n program.uniforms.uMouse.value[1] = currentMouse[1];\n renderer.render({ scene: mesh });\n raf = requestAnimationFrame(loop);\n };\n\n const tryStart = () => {\n if (isVisible && isPageVisible && raf === 0) raf = requestAnimationFrame(loop);\n };\n const tryStop = () => {\n if (raf !== 0) {\n cancelAnimationFrame(raf);\n raf = 0;\n }\n };\n\n const io = new IntersectionObserver(\n ([entry]) => {\n isVisible = entry.isIntersecting;\n isVisible ? tryStart() : tryStop();\n },\n { threshold: 0 }\n );\n io.observe(container);\n\n const onVisibility = () => {\n isPageVisible = !document.hidden;\n isPageVisible ? tryStart() : tryStop();\n };\n document.addEventListener('visibilitychange', onVisibility);\n\n tryStart();\n\n return () => {\n tryStop();\n ro.disconnect();\n io.disconnect();\n document.removeEventListener('visibilitychange', onVisibility);\n canvas.removeEventListener('pointermove', onPointerMove);\n canvas.removeEventListener('pointerleave', onPointerLeave);\n ctxMap.delete(container);\n try {\n container.removeChild(canvas);\n } catch {}\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, []);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n const ctx = ctxMap.get(container);\n if (!ctx) return;\n const { program } = ctx;\n const u = program.uniforms;\n\n enableMouseRef.current = mouseInteraction;\n\n u.uSpeed.value = speed;\n u.uAmplitude.value = amplitude;\n u.uWaveScale.value = waveScale;\n u.uWaveRatio.value = waveRatio;\n u.uSwell.value = swell;\n u.uTurbulence.value = turbulence;\n u.uTilt.value = tilt;\n u.uZoom.value = zoom;\n u.uHeight.value = height;\n u.uFogDepth.value = fogDepth;\n u.uSteps.value = detailToSteps(detail);\n u.uBrightness.value = brightness;\n u.uOpacity.value = opacity;\n u.uGrain.value = grain ? 1.0 : 0.0;\n u.uGrainIntensity.value = grainIntensity;\n u.uParallax.value = parallaxStrength;\n u.uEnableMouse.value = mouseInteraction;\n const hc = u.uHorizonColor.value;\n const wc = u.uWaveColor.value;\n const cc = u.uCrestColor.value;\n const h = hexToRgb(horizonColor);\n const w = hexToRgb(waveColor);\n const cr = hexToRgb(crestColor);\n hc[0] = h[0];\n hc[1] = h[1];\n hc[2] = h[2];\n wc[0] = w[0];\n wc[1] = w[1];\n wc[2] = w[2];\n cc[0] = cr[0];\n cc[1] = cr[1];\n cc[2] = cr[2];\n }, [\n horizonColor,\n waveColor,\n crestColor,\n speed,\n amplitude,\n waveScale,\n waveRatio,\n swell,\n turbulence,\n tilt,\n zoom,\n height,\n fogDepth,\n detail,\n brightness,\n opacity,\n grain,\n grainIntensity,\n mouseInteraction,\n parallaxStrength\n ]);\n\n return
;\n};\n\nexport default GradientWaves;\n" } ], diff --git a/public/r/GradientWaves-TS-CSS.json b/public/r/GradientWaves-TS-CSS.json index d4a22c45..37504ba0 100644 --- a/public/r/GradientWaves-TS-CSS.json +++ b/public/r/GradientWaves-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GradientWaves/GradientWaves.css", + "type": "registry:file", + "path": "GradientWaves.css", + "target": "@components/GradientWaves.css", "content": ".gradient-waves-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "GradientWaves/GradientWaves.tsx", + "path": "GradientWaves.tsx", "content": "import React, { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './GradientWaves.css';\n\nexport type GradientWavesDetail = 'low' | 'medium' | 'high';\n\nexport interface GradientWavesProps {\n horizonColor?: string;\n waveColor?: string;\n crestColor?: string;\n speed?: number;\n amplitude?: number;\n waveScale?: number;\n waveRatio?: number;\n swell?: number;\n turbulence?: number;\n tilt?: number;\n zoom?: number;\n height?: number;\n fogDepth?: number;\n detail?: GradientWavesDetail;\n brightness?: number;\n opacity?: number;\n mouseInteraction?: boolean;\n parallaxStrength?: number;\n grain?: boolean;\n grainIntensity?: number;\n className?: string;\n}\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 1, 1];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst detailToSteps = (detail: GradientWavesDetail): number => {\n if (detail === 'low') return 40.0;\n if (detail === 'high') return 110.0;\n return 70.0;\n};\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uSpeed;\nuniform float uAmplitude;\nuniform float uWaveScale;\nuniform float uWaveRatio;\nuniform float uSwell;\nuniform float uTurbulence;\nuniform float uTilt;\nuniform float uZoom;\nuniform float uHeight;\nuniform float uFogDepth;\nuniform float uSteps;\nuniform float uBrightness;\nuniform float uOpacity;\nuniform float uGrain;\nuniform float uGrainIntensity;\nuniform vec2 uMouse;\nuniform float uParallax;\nuniform bool uEnableMouse;\nuniform vec3 uHorizonColor;\nuniform vec3 uWaveColor;\nuniform vec3 uCrestColor;\nout vec4 fragColor;\n\nconst float MAX_DIST = 20000.0;\n\nfloat hash21(vec2 p) {\n vec3 p3 = fract(vec3(p.xyx) * 0.1031);\n p3 += dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfloat plasma(vec3 r, vec2 freq, vec4 tc) {\n float mx = r.x + tc.x;\n mx += uSwell * sin((r.y + mx) / 20.0 + tc.y);\n float my = r.y - tc.z;\n my += uTurbulence * cos(r.x / 23.0 + tc.w);\n return r.z - (sin(mx * freq.x) * uAmplitude + sin(my * freq.y) * uAmplitude + uHeight);\n}\n\nfloat raymarch(vec3 pos, vec3 dir, vec2 freq, vec4 tc) {\n float dist = 0.0;\n for (int i = 0; i < 128; i++) {\n if (float(i) >= uSteps) break;\n float dscene = plasma(pos + dist * dir, freq, tc);\n if (abs(dscene) < 0.1) break;\n dist += 0.9 * dscene;\n if (!(abs(dist) < MAX_DIST)) return MAX_DIST;\n }\n return dist;\n}\n\nvoid main() {\n float T = iTime * uSpeed;\n vec2 freq = vec2(uWaveScale / 7.0, (uWaveScale * uWaveRatio) / 3.0);\n vec4 tc = vec4(T / 0.130, T / 0.810, T / 0.200, T / 0.710);\n float c, s;\n float vfov = (3.14159 / 2.3) / max(uZoom, 0.05);\n vec3 cam = vec3(0.0, 0.0, 30.0);\n vec2 uv = (gl_FragCoord.xy / iResolution.xy) - 0.5;\n uv.x *= iResolution.x / iResolution.y;\n uv.y *= -1.0;\n\n vec3 dir = vec3(0.0, 0.0, -1.0);\n float ulen = length(uv);\n float xrot = vfov * ulen;\n c = cos(xrot); s = sin(xrot);\n dir = mat3(1.0, 0.0, 0.0, 0.0, c, -s, 0.0, s, c) * dir;\n vec2 nuv = ulen > 1e-5 ? uv / ulen : vec2(1.0, 0.0);\n c = nuv.x; s = nuv.y;\n dir = mat3(c, -s, 0.0, s, c, 0.0, 0.0, 0.0, 1.0) * dir;\n c = cos(uTilt); s = sin(uTilt);\n dir = mat3(c, 0.0, s, 0.0, 1.0, 0.0, -s, 0.0, c) * dir;\n\n if (uEnableMouse) {\n float yaw = (uMouse.x - 0.5) * uParallax * 0.4;\n float pitch = (uMouse.y - 0.5) * uParallax * 0.4;\n c = cos(yaw); s = sin(yaw);\n dir = mat3(c, 0.0, s, 0.0, 1.0, 0.0, -s, 0.0, c) * dir;\n c = cos(pitch); s = sin(pitch);\n dir = mat3(1.0, 0.0, 0.0, 0.0, c, -s, 0.0, s, c) * dir;\n }\n\n float dist = raymarch(cam, dir, freq, tc);\n vec3 pos = cam + dist * dir;\n\n float t = clamp(uFogDepth / max(dist, 0.001), 0.0, 1.0);\n vec3 body = mix(uWaveColor, uCrestColor, clamp(pos.z * 0.08 + 0.5, 0.0, 1.0));\n vec3 col = mix(uHorizonColor, body, t);\n col *= uBrightness;\n col = clamp(col, 0.0, 1.0);\n\n float alpha = clamp(t, 0.0, 1.0) * uOpacity;\n if (uGrain > 0.5) {\n float g = hash21(gl_FragCoord.xy + mod(iTime, 64.0) * 11.0);\n alpha += (g - 0.5) * uGrainIntensity;\n }\n alpha = clamp(alpha, 0.0, 1.0);\n fragColor = vec4(col * alpha, alpha);\n}\n`;\n\ntype GradientWavesCtx = {\n renderer: InstanceType;\n program: InstanceType;\n mesh: InstanceType;\n};\nconst ctxMap = new WeakMap();\n\nconst GradientWaves: React.FC = ({\n horizonColor = '#5227FF',\n waveColor = '#FF9FFC',\n crestColor = '#FFFFFF',\n speed = 0.4,\n amplitude = 2.5,\n waveScale = 0.6,\n waveRatio = 0.9,\n swell = 35,\n turbulence = 20,\n tilt = 1.11,\n zoom = 1.0,\n height = 5.5,\n fogDepth = 15,\n detail = 'medium',\n brightness = 1.0,\n opacity = 1.0,\n mouseInteraction = true,\n parallaxStrength = 0.5,\n grain = true,\n grainIntensity = 0.05,\n className = ''\n}) => {\n const containerRef = useRef(null);\n const enableMouseRef = useRef(mouseInteraction);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n webgl: 2,\n alpha: true,\n premultipliedAlpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n const canvas = gl.canvas as HTMLCanvasElement;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uSpeed: { value: 0.4 },\n uAmplitude: { value: 2.5 },\n uWaveScale: { value: 0.6 },\n uWaveRatio: { value: 0.9 },\n uSwell: { value: 35 },\n uTurbulence: { value: 20 },\n uTilt: { value: 1.11 },\n uZoom: { value: 1.0 },\n uHeight: { value: 5.5 },\n uFogDepth: { value: 15 },\n uSteps: { value: 70.0 },\n uBrightness: { value: 1.0 },\n uOpacity: { value: 1.0 },\n uGrain: { value: 1.0 },\n uGrainIntensity: { value: 0.05 },\n uMouse: { value: new Float32Array([0.5, 0.5]) },\n uParallax: { value: 0.5 },\n uEnableMouse: { value: true },\n uHorizonColor: { value: new Float32Array([1, 1, 1]) },\n uWaveColor: { value: new Float32Array([1, 1, 1]) },\n uCrestColor: { value: new Float32Array([1, 1, 1]) }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n ctxMap.set(container, { renderer, program, mesh });\n\n const setSize = () => {\n const rect = container.getBoundingClientRect();\n const w = Math.max(1, Math.floor(rect.width));\n const h = Math.max(1, Math.floor(rect.height));\n renderer.setSize(w, h);\n const res = (program.uniforms.iResolution as { value: Float32Array }).value;\n res[0] = gl.drawingBufferWidth;\n res[1] = gl.drawingBufferHeight;\n renderer.render({ scene: mesh });\n };\n\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n setSize();\n\n const currentMouse: [number, number] = [0.5, 0.5];\n const targetMouse: [number, number] = [0.5, 0.5];\n\n const onPointerMove = (e: PointerEvent) => {\n const rect = canvas.getBoundingClientRect();\n targetMouse[0] = (e.clientX - rect.left) / rect.width;\n targetMouse[1] = 1.0 - (e.clientY - rect.top) / rect.height;\n };\n const onPointerLeave = () => {\n targetMouse[0] = 0.5;\n targetMouse[1] = 0.5;\n };\n canvas.addEventListener('pointermove', onPointerMove);\n canvas.addEventListener('pointerleave', onPointerLeave);\n\n let raf = 0;\n let isVisible = true;\n let isPageVisible = !document.hidden;\n const t0 = performance.now();\n\n const loop = (t: number) => {\n (program.uniforms.iTime as { value: number }).value = (t - t0) * 0.001;\n const tx = enableMouseRef.current ? targetMouse[0] : 0.5;\n const ty = enableMouseRef.current ? targetMouse[1] : 0.5;\n currentMouse[0] += 0.05 * (tx - currentMouse[0]);\n currentMouse[1] += 0.05 * (ty - currentMouse[1]);\n const m = (program.uniforms.uMouse as { value: Float32Array }).value;\n m[0] = currentMouse[0];\n m[1] = currentMouse[1];\n renderer.render({ scene: mesh });\n raf = requestAnimationFrame(loop);\n };\n\n const tryStart = () => {\n if (isVisible && isPageVisible && raf === 0) raf = requestAnimationFrame(loop);\n };\n const tryStop = () => {\n if (raf !== 0) {\n cancelAnimationFrame(raf);\n raf = 0;\n }\n };\n\n const io = new IntersectionObserver(\n ([entry]) => {\n isVisible = entry.isIntersecting;\n isVisible ? tryStart() : tryStop();\n },\n { threshold: 0 }\n );\n io.observe(container);\n\n const onVisibility = () => {\n isPageVisible = !document.hidden;\n isPageVisible ? tryStart() : tryStop();\n };\n document.addEventListener('visibilitychange', onVisibility);\n\n tryStart();\n\n return () => {\n tryStop();\n ro.disconnect();\n io.disconnect();\n document.removeEventListener('visibilitychange', onVisibility);\n canvas.removeEventListener('pointermove', onPointerMove);\n canvas.removeEventListener('pointerleave', onPointerLeave);\n ctxMap.delete(container);\n try {\n container.removeChild(canvas);\n } catch {}\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, []);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n const ctx = ctxMap.get(container);\n if (!ctx) return;\n const { program } = ctx;\n const u = program.uniforms as Record;\n\n enableMouseRef.current = mouseInteraction;\n\n u.uSpeed.value = speed;\n u.uAmplitude.value = amplitude;\n u.uWaveScale.value = waveScale;\n u.uWaveRatio.value = waveRatio;\n u.uSwell.value = swell;\n u.uTurbulence.value = turbulence;\n u.uTilt.value = tilt;\n u.uZoom.value = zoom;\n u.uHeight.value = height;\n u.uFogDepth.value = fogDepth;\n u.uSteps.value = detailToSteps(detail);\n u.uBrightness.value = brightness;\n u.uOpacity.value = opacity;\n u.uGrain.value = grain ? 1.0 : 0.0;\n u.uGrainIntensity.value = grainIntensity;\n u.uParallax.value = parallaxStrength;\n u.uEnableMouse.value = mouseInteraction;\n const hc = u.uHorizonColor.value as Float32Array;\n const wc = u.uWaveColor.value as Float32Array;\n const cc = u.uCrestColor.value as Float32Array;\n const h = hexToRgb(horizonColor);\n const w = hexToRgb(waveColor);\n const cr = hexToRgb(crestColor);\n hc[0] = h[0];\n hc[1] = h[1];\n hc[2] = h[2];\n wc[0] = w[0];\n wc[1] = w[1];\n wc[2] = w[2];\n cc[0] = cr[0];\n cc[1] = cr[1];\n cc[2] = cr[2];\n }, [\n horizonColor,\n waveColor,\n crestColor,\n speed,\n amplitude,\n waveScale,\n waveRatio,\n swell,\n turbulence,\n tilt,\n zoom,\n height,\n fogDepth,\n detail,\n brightness,\n opacity,\n grain,\n grainIntensity,\n mouseInteraction,\n parallaxStrength\n ]);\n\n return
;\n};\n\nexport default GradientWaves;\n" } ], diff --git a/public/r/GradualBlur-JS-CSS.json b/public/r/GradualBlur-JS-CSS.json index bc113899..bd213cf3 100644 --- a/public/r/GradualBlur-JS-CSS.json +++ b/public/r/GradualBlur-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GradualBlur/GradualBlur.css", + "type": "registry:file", + "path": "GradualBlur.css", + "target": "@components/GradualBlur.css", "content": ".gradual-blur-inner {\n position: relative;\n width: 100%;\n height: 100%;\n}\n\n.gradual-blur-inner > div {\n -webkit-backdrop-filter: inherit;\n backdrop-filter: inherit;\n}\n\n.gradual-blur {\n isolation: isolate;\n}\n\n@supports not (backdrop-filter: blur(1px)) {\n .gradual-blur-inner > div {\n background: rgba(0, 0, 0, 0.3);\n opacity: 0.5;\n }\n}\n\n.gradual-blur-fixed {\n position: fixed !important;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n pointer-events: none;\n z-index: 1000;\n}\n" }, { "type": "registry:component", - "path": "GradualBlur/GradualBlur.jsx", + "path": "GradualBlur.jsx", "content": "import React, { useEffect, useRef, useState, useMemo } from 'react';\n\nimport './GradualBlur.css';\n\nconst DEFAULT_CONFIG = {\n position: 'bottom',\n strength: 2,\n height: '6rem',\n divCount: 5,\n exponential: false,\n zIndex: 1000,\n animated: false,\n duration: '0.3s',\n easing: 'ease-out',\n opacity: 1,\n curve: 'linear',\n responsive: false,\n target: 'parent',\n className: '',\n style: {}\n};\n\nconst PRESETS = {\n top: { position: 'top', height: '6rem' },\n bottom: { position: 'bottom', height: '6rem' },\n left: { position: 'left', height: '6rem' },\n right: { position: 'right', height: '6rem' },\n subtle: { height: '4rem', strength: 1, opacity: 0.8, divCount: 3 },\n intense: { height: '10rem', strength: 4, divCount: 8, exponential: true },\n smooth: { height: '8rem', curve: 'bezier', divCount: 10 },\n sharp: { height: '5rem', curve: 'linear', divCount: 4 },\n header: { position: 'top', height: '8rem', curve: 'ease-out' },\n footer: { position: 'bottom', height: '8rem', curve: 'ease-out' },\n sidebar: { position: 'left', height: '6rem', strength: 2.5 },\n 'page-header': { position: 'top', height: '10rem', target: 'page', strength: 3 },\n 'page-footer': { position: 'bottom', height: '10rem', target: 'page', strength: 3 }\n};\n\nconst CURVE_FUNCTIONS = {\n linear: p => p,\n bezier: p => p * p * (3 - 2 * p),\n 'ease-in': p => p * p,\n 'ease-out': p => 1 - Math.pow(1 - p, 2),\n 'ease-in-out': p => (p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2)\n};\n\nconst mergeConfigs = (...configs) => configs.reduce((acc, c) => ({ ...acc, ...c }), {});\nconst getGradientDirection = position =>\n ({\n top: 'to top',\n bottom: 'to bottom',\n left: 'to left',\n right: 'to right'\n })[position] || 'to bottom';\n\nconst debounce = (fn, wait) => {\n let t;\n return (...a) => {\n clearTimeout(t);\n t = setTimeout(() => fn(...a), wait);\n };\n};\n\nconst useResponsiveDimension = (responsive, config, key) => {\n const [value, setValue] = useState(config[key]);\n useEffect(() => {\n if (!responsive) return;\n const calc = () => {\n const w = window.innerWidth;\n let v = config[key];\n if (w <= 480 && config[`mobile${key[0].toUpperCase() + key.slice(1)}`])\n v = config[`mobile${key[0].toUpperCase() + key.slice(1)}`];\n else if (w <= 768 && config[`tablet${key[0].toUpperCase() + key.slice(1)}`])\n v = config[`tablet${key[0].toUpperCase() + key.slice(1)}`];\n else if (w <= 1024 && config[`desktop${key[0].toUpperCase() + key.slice(1)}`])\n v = config[`desktop${key[0].toUpperCase() + key.slice(1)}`];\n setValue(v);\n };\n const debounced = debounce(calc, 100);\n calc();\n window.addEventListener('resize', debounced);\n return () => window.removeEventListener('resize', debounced);\n }, [responsive, config, key]);\n return responsive ? value : config[key];\n};\n\nconst useIntersectionObserver = (ref, shouldObserve = false) => {\n const [isVisible, setIsVisible] = useState(!shouldObserve);\n\n useEffect(() => {\n if (!shouldObserve || !ref.current) return;\n\n const observer = new IntersectionObserver(([entry]) => setIsVisible(entry.isIntersecting), { threshold: 0.1 });\n\n observer.observe(ref.current);\n return () => observer.disconnect();\n }, [ref, shouldObserve]);\n\n return isVisible;\n};\n\nfunction GradualBlur(props) {\n const containerRef = useRef(null);\n const [isHovered, setIsHovered] = useState(false);\n\n const config = useMemo(() => {\n const presetConfig = props.preset && PRESETS[props.preset] ? PRESETS[props.preset] : {};\n return mergeConfigs(DEFAULT_CONFIG, presetConfig, props);\n }, [props]);\n\n const responsiveHeight = useResponsiveDimension(config.responsive, config, 'height');\n const responsiveWidth = useResponsiveDimension(config.responsive, config, 'width');\n\n const isVisible = useIntersectionObserver(containerRef, config.animated === 'scroll');\n\n const blurDivs = useMemo(() => {\n const divs = [];\n const increment = 100 / config.divCount;\n const currentStrength =\n isHovered && config.hoverIntensity ? config.strength * config.hoverIntensity : config.strength;\n\n const curveFunc = CURVE_FUNCTIONS[config.curve] || CURVE_FUNCTIONS.linear;\n\n for (let i = 1; i <= config.divCount; i++) {\n let progress = i / config.divCount;\n progress = curveFunc(progress);\n\n let blurValue;\n if (config.exponential) {\n blurValue = Math.pow(2, progress * 4) * 0.0625 * currentStrength;\n } else {\n blurValue = 0.0625 * (progress * config.divCount + 1) * currentStrength;\n }\n\n const p1 = Math.round((increment * i - increment) * 10) / 10;\n const p2 = Math.round(increment * i * 10) / 10;\n const p3 = Math.round((increment * i + increment) * 10) / 10;\n const p4 = Math.round((increment * i + increment * 2) * 10) / 10;\n\n let gradient = `transparent ${p1}%, black ${p2}%`;\n if (p3 <= 100) gradient += `, black ${p3}%`;\n if (p4 <= 100) gradient += `, transparent ${p4}%`;\n\n const direction = getGradientDirection(config.position);\n\n const divStyle = {\n position: 'absolute',\n inset: '0',\n maskImage: `linear-gradient(${direction}, ${gradient})`,\n WebkitMaskImage: `linear-gradient(${direction}, ${gradient})`,\n backdropFilter: `blur(${blurValue.toFixed(3)}rem)`,\n WebkitBackdropFilter: `blur(${blurValue.toFixed(3)}rem)`,\n opacity: config.opacity,\n transition:\n config.animated && config.animated !== 'scroll'\n ? `backdrop-filter ${config.duration} ${config.easing}`\n : undefined\n };\n\n divs.push(
);\n }\n\n return divs;\n }, [config, isHovered]);\n\n const containerStyle = useMemo(() => {\n const isVertical = ['top', 'bottom'].includes(config.position);\n const isHorizontal = ['left', 'right'].includes(config.position);\n const isPageTarget = config.target === 'page';\n\n const baseStyle = {\n position: isPageTarget ? 'fixed' : 'absolute',\n pointerEvents: config.hoverIntensity ? 'auto' : 'none',\n opacity: isVisible ? 1 : 0,\n transition: config.animated ? `opacity ${config.duration} ${config.easing}` : undefined,\n zIndex: isPageTarget ? config.zIndex + 100 : config.zIndex,\n ...config.style\n };\n\n if (isVertical) {\n baseStyle.height = responsiveHeight;\n baseStyle.width = responsiveWidth || '100%';\n baseStyle[config.position] = 0;\n baseStyle.left = 0;\n baseStyle.right = 0;\n } else if (isHorizontal) {\n baseStyle.width = responsiveWidth || responsiveHeight;\n baseStyle.height = '100%';\n baseStyle[config.position] = 0;\n baseStyle.top = 0;\n baseStyle.bottom = 0;\n }\n\n return baseStyle;\n }, [config, responsiveHeight, responsiveWidth, isVisible]);\n\n const { hoverIntensity, animated, onAnimationComplete, duration } = config;\n\n useEffect(() => {\n if (isVisible && animated === 'scroll' && onAnimationComplete) {\n const ms = parseFloat(duration) * 1000;\n const t = setTimeout(() => onAnimationComplete(), ms);\n return () => clearTimeout(t);\n }\n }, [isVisible, animated, onAnimationComplete, duration]);\n\n return (\n setIsHovered(true) : undefined}\n onMouseLeave={hoverIntensity ? () => setIsHovered(false) : undefined}\n >\n \n {blurDivs}\n
\n
\n );\n}\n\nconst GradualBlurMemo = React.memo(GradualBlur);\nGradualBlurMemo.displayName = 'GradualBlur';\nGradualBlurMemo.PRESETS = PRESETS;\nGradualBlurMemo.CURVE_FUNCTIONS = CURVE_FUNCTIONS;\nexport default GradualBlurMemo;\n\nconst injectStyles = () => {\n if (typeof document === 'undefined') return;\n\n const styleId = 'gradual-blur-styles';\n if (document.getElementById(styleId)) return;\n\n const styleElement = document.createElement('style');\n styleElement.id = styleId;\n styleElement.textContent = `\n .gradual-blur { pointer-events: none; transition: opacity 0.3s ease-out; }\n .gradual-blur-parent { overflow: hidden; }\n .gradual-blur-inner { pointer-events: none; }`;\n\n document.head.appendChild(styleElement);\n};\n\nif (typeof document !== 'undefined') {\n injectStyles();\n}\n" } ], diff --git a/public/r/GradualBlur-TS-CSS.json b/public/r/GradualBlur-TS-CSS.json index 2ae76ce8..4dc3dd2a 100644 --- a/public/r/GradualBlur-TS-CSS.json +++ b/public/r/GradualBlur-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GradualBlur/GradualBlur.css", + "type": "registry:file", + "path": "GradualBlur.css", + "target": "@components/GradualBlur.css", "content": ".gradual-blur-inner {\n position: relative;\n width: 100%;\n height: 100%;\n}\n\n.gradual-blur-inner > div {\n -webkit-backdrop-filter: inherit;\n backdrop-filter: inherit;\n}\n\n.gradual-blur {\n isolation: isolate;\n}\n\n@supports not (backdrop-filter: blur(1px)) {\n .gradual-blur-inner > div {\n background: rgba(0, 0, 0, 0.3);\n opacity: 0.5;\n }\n}\n\n.gradual-blur-fixed {\n position: fixed !important;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n pointer-events: none;\n z-index: 1000;\n}\n" }, { "type": "registry:component", - "path": "GradualBlur/GradualBlur.tsx", - "content": "import React, { CSSProperties, useEffect, useRef, useState, useMemo, PropsWithChildren } from 'react';\n\nimport './GradualBlur.css';\n\ntype GradualBlurProps = {\n position?: 'top' | 'bottom' | 'left' | 'right';\n strength?: number;\n height?: string;\n width?: string;\n divCount?: number;\n exponential?: boolean;\n zIndex?: number;\n animated?: boolean | 'scroll';\n duration?: string;\n easing?: string;\n opacity?: number;\n curve?: 'linear' | 'bezier' | 'ease-in' | 'ease-out' | 'ease-in-out';\n responsive?: boolean;\n mobileHeight?: string;\n tabletHeight?: string;\n desktopHeight?: string;\n mobileWidth?: string;\n tabletWidth?: string;\n desktopWidth?: string;\n preset?:\n | 'top'\n | 'bottom'\n | 'left'\n | 'right'\n | 'subtle'\n | 'intense'\n | 'smooth'\n | 'sharp'\n | 'header'\n | 'footer'\n | 'sidebar'\n | 'page-header'\n | 'page-footer';\n gpuOptimized?: boolean;\n hoverIntensity?: number;\n target?: 'parent' | 'page';\n onAnimationComplete?: () => void;\n className?: string;\n style?: CSSProperties;\n};\n\nconst DEFAULT_CONFIG: Partial = {\n position: 'bottom',\n strength: 2,\n height: '6rem',\n divCount: 5,\n exponential: false,\n zIndex: 1000,\n animated: false,\n duration: '0.3s',\n easing: 'ease-out',\n opacity: 1,\n curve: 'linear',\n responsive: false,\n target: 'parent',\n className: '',\n style: {}\n};\n\nconst PRESETS: Record> = {\n top: { position: 'top', height: '6rem' },\n bottom: { position: 'bottom', height: '6rem' },\n left: { position: 'left', height: '6rem' },\n right: { position: 'right', height: '6rem' },\n subtle: { height: '4rem', strength: 1, opacity: 0.8, divCount: 3 },\n intense: { height: '10rem', strength: 4, divCount: 8, exponential: true },\n smooth: { height: '8rem', curve: 'bezier', divCount: 10 },\n sharp: { height: '5rem', curve: 'linear', divCount: 4 },\n header: { position: 'top', height: '8rem', curve: 'ease-out' },\n footer: { position: 'bottom', height: '8rem', curve: 'ease-out' },\n sidebar: { position: 'left', height: '6rem', strength: 2.5 },\n 'page-header': {\n position: 'top',\n height: '10rem',\n target: 'page',\n strength: 3\n },\n 'page-footer': {\n position: 'bottom',\n height: '10rem',\n target: 'page',\n strength: 3\n }\n};\n\nconst CURVE_FUNCTIONS: Record number> = {\n linear: p => p,\n bezier: p => p * p * (3 - 2 * p),\n 'ease-in': p => p * p,\n 'ease-out': p => 1 - Math.pow(1 - p, 2),\n 'ease-in-out': p => (p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2)\n};\n\nconst mergeConfigs = (...configs: Partial[]): Partial => {\n return configs.reduce((acc, config) => ({ ...acc, ...config }), {});\n};\n\nconst getGradientDirection = (position: string): string => {\n const directions: Record = {\n top: 'to top',\n bottom: 'to bottom',\n left: 'to left',\n right: 'to right'\n };\n return directions[position] || 'to bottom';\n};\n\nconst debounce = void>(fn: T, wait: number) => {\n let t: ReturnType;\n return (...a: Parameters) => {\n clearTimeout(t);\n t = setTimeout(() => fn(...a), wait);\n };\n};\n\nconst useResponsiveDimension = (\n responsive: boolean | undefined,\n config: Partial,\n key: keyof GradualBlurProps\n) => {\n const [val, setVal] = useState(config[key]);\n useEffect(() => {\n if (!responsive) return;\n const calc = () => {\n const w = window.innerWidth;\n let v: any = config[key];\n const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);\n const k = cap(key as string);\n if (w <= 480 && (config as any)['mobile' + k]) v = (config as any)['mobile' + k];\n else if (w <= 768 && (config as any)['tablet' + k]) v = (config as any)['tablet' + k];\n else if (w <= 1024 && (config as any)['desktop' + k]) v = (config as any)['desktop' + k];\n setVal(v);\n };\n const deb = debounce(calc, 100);\n calc();\n window.addEventListener('resize', deb);\n return () => window.removeEventListener('resize', deb);\n }, [responsive, config, key]);\n return responsive ? val : (config as any)[key];\n};\n\nconst useIntersectionObserver = (ref: React.RefObject, shouldObserve: boolean = false) => {\n const [isVisible, setIsVisible] = useState(!shouldObserve);\n\n useEffect(() => {\n if (!shouldObserve || !ref.current) return;\n\n const observer = new IntersectionObserver(([entry]) => setIsVisible(entry.isIntersecting), { threshold: 0.1 });\n\n observer.observe(ref.current);\n return () => observer.disconnect();\n }, [ref, shouldObserve]);\n\n return isVisible;\n};\n\nconst GradualBlur: React.FC> = props => {\n const containerRef = useRef(null);\n const [isHovered, setIsHovered] = useState(false);\n\n const config = useMemo(() => {\n const presetConfig = props.preset && PRESETS[props.preset] ? PRESETS[props.preset] : {};\n return mergeConfigs(DEFAULT_CONFIG, presetConfig, props) as Required;\n }, [props]);\n\n const responsiveHeight = useResponsiveDimension(config.responsive, config, 'height');\n const responsiveWidth = useResponsiveDimension(config.responsive, config, 'width');\n\n const isVisible = useIntersectionObserver(containerRef, config.animated === 'scroll');\n\n const blurDivs = useMemo(() => {\n const divs: React.ReactNode[] = [];\n const increment = 100 / config.divCount;\n const currentStrength =\n isHovered && config.hoverIntensity ? config.strength * config.hoverIntensity : config.strength;\n\n const curveFunc = CURVE_FUNCTIONS[config.curve] || CURVE_FUNCTIONS.linear;\n\n for (let i = 1; i <= config.divCount; i++) {\n let progress = i / config.divCount;\n progress = curveFunc(progress);\n\n let blurValue: number;\n if (config.exponential) {\n blurValue = Number(Math.pow(2, progress * 4)) * 0.0625 * currentStrength;\n } else {\n blurValue = 0.0625 * (progress * config.divCount + 1) * currentStrength;\n }\n\n const p1 = Math.round((increment * i - increment) * 10) / 10;\n const p2 = Math.round(increment * i * 10) / 10;\n const p3 = Math.round((increment * i + increment) * 10) / 10;\n const p4 = Math.round((increment * i + increment * 2) * 10) / 10;\n\n let gradient = `transparent ${p1}%, black ${p2}%`;\n if (p3 <= 100) gradient += `, black ${p3}%`;\n if (p4 <= 100) gradient += `, transparent ${p4}%`;\n\n const direction = getGradientDirection(config.position);\n\n const divStyle: CSSProperties = {\n position: 'absolute',\n inset: '0',\n maskImage: `linear-gradient(${direction}, ${gradient})`,\n WebkitMaskImage: `linear-gradient(${direction}, ${gradient})`,\n backdropFilter: `blur(${blurValue.toFixed(3)}rem)`,\n WebkitBackdropFilter: `blur(${blurValue.toFixed(3)}rem)`,\n opacity: config.opacity,\n transition:\n config.animated && config.animated !== 'scroll'\n ? `backdrop-filter ${config.duration} ${config.easing}`\n : undefined\n };\n\n divs.push(
);\n }\n\n return divs;\n }, [config, isHovered]);\n\n const containerStyle: CSSProperties = useMemo(() => {\n const isVertical = ['top', 'bottom'].includes(config.position);\n const isHorizontal = ['left', 'right'].includes(config.position);\n const isPageTarget = config.target === 'page';\n\n const baseStyle: CSSProperties = {\n position: isPageTarget ? 'fixed' : 'absolute',\n pointerEvents: config.hoverIntensity ? 'auto' : 'none',\n opacity: isVisible ? 1 : 0,\n transition: config.animated ? `opacity ${config.duration} ${config.easing}` : undefined,\n zIndex: isPageTarget ? config.zIndex + 100 : config.zIndex,\n ...config.style\n };\n\n if (isVertical) {\n baseStyle.height = responsiveHeight;\n baseStyle.width = responsiveWidth || '100%';\n baseStyle[config.position] = 0;\n baseStyle.left = 0;\n baseStyle.right = 0;\n } else if (isHorizontal) {\n baseStyle.width = responsiveWidth || responsiveHeight;\n baseStyle.height = '100%';\n baseStyle[config.position] = 0;\n baseStyle.top = 0;\n baseStyle.bottom = 0;\n }\n\n return baseStyle;\n }, [config, responsiveHeight, responsiveWidth, isVisible]);\n\n const { hoverIntensity, animated, onAnimationComplete, duration } = config as any;\n useEffect(() => {\n if (isVisible && animated === 'scroll' && onAnimationComplete) {\n const t = setTimeout(() => onAnimationComplete(), parseFloat(duration) * 1000);\n return () => clearTimeout(t);\n }\n }, [isVisible, animated, onAnimationComplete, duration]);\n\n return (\n setIsHovered(true) : undefined}\n onMouseLeave={hoverIntensity ? () => setIsHovered(false) : undefined}\n >\n \n {blurDivs}\n
\n
\n );\n};\n\nconst GradualBlurMemo = React.memo(GradualBlur);\nGradualBlurMemo.displayName = 'GradualBlur';\n(GradualBlurMemo as any).PRESETS = PRESETS;\n(GradualBlurMemo as any).CURVE_FUNCTIONS = CURVE_FUNCTIONS;\nexport default GradualBlurMemo;\n\nconst injectStyles = () => {\n if (typeof document === 'undefined') return;\n const styleId = 'gradual-blur-styles';\n if (document.getElementById(styleId)) return;\n const styleElement = document.createElement('style');\n styleElement.id = styleId;\n styleElement.textContent = `.gradual-blur{pointer-events:none;transition:opacity 0.3s ease-out}.gradual-blur-inner{pointer-events:none}`;\n document.head.appendChild(styleElement);\n};\n\nif (typeof document !== 'undefined') {\n injectStyles();\n}\n" + "path": "GradualBlur.tsx", + "content": "import React, { type CSSProperties, useEffect, useRef, useState, useMemo, type PropsWithChildren } from 'react';\n\nimport './GradualBlur.css';\n\ntype GradualBlurProps = {\n position?: 'top' | 'bottom' | 'left' | 'right';\n strength?: number;\n height?: string;\n width?: string;\n divCount?: number;\n exponential?: boolean;\n zIndex?: number;\n animated?: boolean | 'scroll';\n duration?: string;\n easing?: string;\n opacity?: number;\n curve?: 'linear' | 'bezier' | 'ease-in' | 'ease-out' | 'ease-in-out';\n responsive?: boolean;\n mobileHeight?: string;\n tabletHeight?: string;\n desktopHeight?: string;\n mobileWidth?: string;\n tabletWidth?: string;\n desktopWidth?: string;\n preset?:\n | 'top'\n | 'bottom'\n | 'left'\n | 'right'\n | 'subtle'\n | 'intense'\n | 'smooth'\n | 'sharp'\n | 'header'\n | 'footer'\n | 'sidebar'\n | 'page-header'\n | 'page-footer';\n gpuOptimized?: boolean;\n hoverIntensity?: number;\n target?: 'parent' | 'page';\n onAnimationComplete?: () => void;\n className?: string;\n style?: CSSProperties;\n};\n\nconst DEFAULT_CONFIG: Partial = {\n position: 'bottom',\n strength: 2,\n height: '6rem',\n divCount: 5,\n exponential: false,\n zIndex: 1000,\n animated: false,\n duration: '0.3s',\n easing: 'ease-out',\n opacity: 1,\n curve: 'linear',\n responsive: false,\n target: 'parent',\n className: '',\n style: {}\n};\n\nconst PRESETS: Record> = {\n top: { position: 'top', height: '6rem' },\n bottom: { position: 'bottom', height: '6rem' },\n left: { position: 'left', height: '6rem' },\n right: { position: 'right', height: '6rem' },\n subtle: { height: '4rem', strength: 1, opacity: 0.8, divCount: 3 },\n intense: { height: '10rem', strength: 4, divCount: 8, exponential: true },\n smooth: { height: '8rem', curve: 'bezier', divCount: 10 },\n sharp: { height: '5rem', curve: 'linear', divCount: 4 },\n header: { position: 'top', height: '8rem', curve: 'ease-out' },\n footer: { position: 'bottom', height: '8rem', curve: 'ease-out' },\n sidebar: { position: 'left', height: '6rem', strength: 2.5 },\n 'page-header': {\n position: 'top',\n height: '10rem',\n target: 'page',\n strength: 3\n },\n 'page-footer': {\n position: 'bottom',\n height: '10rem',\n target: 'page',\n strength: 3\n }\n};\n\nconst CURVE_FUNCTIONS: Record number> = {\n linear: p => p,\n bezier: p => p * p * (3 - 2 * p),\n 'ease-in': p => p * p,\n 'ease-out': p => 1 - Math.pow(1 - p, 2),\n 'ease-in-out': p => (p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2)\n};\n\nconst mergeConfigs = (...configs: Partial[]): Partial => {\n return configs.reduce((acc, config) => ({ ...acc, ...config }), {});\n};\n\nconst getGradientDirection = (position: string): string => {\n const directions: Record = {\n top: 'to top',\n bottom: 'to bottom',\n left: 'to left',\n right: 'to right'\n };\n return directions[position] || 'to bottom';\n};\n\nconst debounce = void>(fn: T, wait: number) => {\n let t: ReturnType;\n return (...a: Parameters) => {\n clearTimeout(t);\n t = setTimeout(() => fn(...a), wait);\n };\n};\n\nconst useResponsiveDimension = (\n responsive: boolean | undefined,\n config: Partial,\n key: keyof GradualBlurProps\n) => {\n const [val, setVal] = useState(config[key]);\n useEffect(() => {\n if (!responsive) return;\n const calc = () => {\n const w = window.innerWidth;\n let v: any = config[key];\n const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);\n const k = cap(key as string);\n if (w <= 480 && (config as any)['mobile' + k]) v = (config as any)['mobile' + k];\n else if (w <= 768 && (config as any)['tablet' + k]) v = (config as any)['tablet' + k];\n else if (w <= 1024 && (config as any)['desktop' + k]) v = (config as any)['desktop' + k];\n setVal(v);\n };\n const deb = debounce(calc, 100);\n calc();\n window.addEventListener('resize', deb);\n return () => window.removeEventListener('resize', deb);\n }, [responsive, config, key]);\n return responsive ? val : (config as any)[key];\n};\n\nconst useIntersectionObserver = (ref: React.RefObject, shouldObserve: boolean = false) => {\n const [isVisible, setIsVisible] = useState(!shouldObserve);\n\n useEffect(() => {\n if (!shouldObserve || !ref.current) return;\n\n const observer = new IntersectionObserver(([entry]) => setIsVisible(entry.isIntersecting), { threshold: 0.1 });\n\n observer.observe(ref.current);\n return () => observer.disconnect();\n }, [ref, shouldObserve]);\n\n return isVisible;\n};\n\nconst GradualBlur: React.FC> = props => {\n const containerRef = useRef(null);\n const [isHovered, setIsHovered] = useState(false);\n\n const config = useMemo(() => {\n const presetConfig = props.preset && PRESETS[props.preset] ? PRESETS[props.preset] : {};\n return mergeConfigs(DEFAULT_CONFIG, presetConfig, props) as Required;\n }, [props]);\n\n const responsiveHeight = useResponsiveDimension(config.responsive, config, 'height');\n const responsiveWidth = useResponsiveDimension(config.responsive, config, 'width');\n\n const isVisible = useIntersectionObserver(containerRef, config.animated === 'scroll');\n\n const blurDivs = useMemo(() => {\n const divs: React.ReactNode[] = [];\n const increment = 100 / config.divCount;\n const currentStrength =\n isHovered && config.hoverIntensity ? config.strength * config.hoverIntensity : config.strength;\n\n const curveFunc = CURVE_FUNCTIONS[config.curve] || CURVE_FUNCTIONS.linear;\n\n for (let i = 1; i <= config.divCount; i++) {\n let progress = i / config.divCount;\n progress = curveFunc(progress);\n\n let blurValue: number;\n if (config.exponential) {\n blurValue = Number(Math.pow(2, progress * 4)) * 0.0625 * currentStrength;\n } else {\n blurValue = 0.0625 * (progress * config.divCount + 1) * currentStrength;\n }\n\n const p1 = Math.round((increment * i - increment) * 10) / 10;\n const p2 = Math.round(increment * i * 10) / 10;\n const p3 = Math.round((increment * i + increment) * 10) / 10;\n const p4 = Math.round((increment * i + increment * 2) * 10) / 10;\n\n let gradient = `transparent ${p1}%, black ${p2}%`;\n if (p3 <= 100) gradient += `, black ${p3}%`;\n if (p4 <= 100) gradient += `, transparent ${p4}%`;\n\n const direction = getGradientDirection(config.position);\n\n const divStyle: CSSProperties = {\n position: 'absolute',\n inset: '0',\n maskImage: `linear-gradient(${direction}, ${gradient})`,\n WebkitMaskImage: `linear-gradient(${direction}, ${gradient})`,\n backdropFilter: `blur(${blurValue.toFixed(3)}rem)`,\n WebkitBackdropFilter: `blur(${blurValue.toFixed(3)}rem)`,\n opacity: config.opacity,\n transition:\n config.animated && config.animated !== 'scroll'\n ? `backdrop-filter ${config.duration} ${config.easing}`\n : undefined\n };\n\n divs.push(
);\n }\n\n return divs;\n }, [config, isHovered]);\n\n const containerStyle: CSSProperties = useMemo(() => {\n const isVertical = ['top', 'bottom'].includes(config.position);\n const isHorizontal = ['left', 'right'].includes(config.position);\n const isPageTarget = config.target === 'page';\n\n const baseStyle: CSSProperties = {\n position: isPageTarget ? 'fixed' : 'absolute',\n pointerEvents: config.hoverIntensity ? 'auto' : 'none',\n opacity: isVisible ? 1 : 0,\n transition: config.animated ? `opacity ${config.duration} ${config.easing}` : undefined,\n zIndex: isPageTarget ? config.zIndex + 100 : config.zIndex,\n ...config.style\n };\n\n if (isVertical) {\n baseStyle.height = responsiveHeight;\n baseStyle.width = responsiveWidth || '100%';\n baseStyle[config.position] = 0;\n baseStyle.left = 0;\n baseStyle.right = 0;\n } else if (isHorizontal) {\n baseStyle.width = responsiveWidth || responsiveHeight;\n baseStyle.height = '100%';\n baseStyle[config.position] = 0;\n baseStyle.top = 0;\n baseStyle.bottom = 0;\n }\n\n return baseStyle;\n }, [config, responsiveHeight, responsiveWidth, isVisible]);\n\n const { hoverIntensity, animated, onAnimationComplete, duration } = config as any;\n useEffect(() => {\n if (isVisible && animated === 'scroll' && onAnimationComplete) {\n const t = setTimeout(() => onAnimationComplete(), parseFloat(duration) * 1000);\n return () => clearTimeout(t);\n }\n }, [isVisible, animated, onAnimationComplete, duration]);\n\n return (\n setIsHovered(true) : undefined}\n onMouseLeave={hoverIntensity ? () => setIsHovered(false) : undefined}\n >\n \n {blurDivs}\n
\n
\n );\n};\n\nconst GradualBlurMemo = React.memo(GradualBlur);\nGradualBlurMemo.displayName = 'GradualBlur';\n(GradualBlurMemo as any).PRESETS = PRESETS;\n(GradualBlurMemo as any).CURVE_FUNCTIONS = CURVE_FUNCTIONS;\nexport default GradualBlurMemo;\n\nconst injectStyles = () => {\n if (typeof document === 'undefined') return;\n const styleId = 'gradual-blur-styles';\n if (document.getElementById(styleId)) return;\n const styleElement = document.createElement('style');\n styleElement.id = styleId;\n styleElement.textContent = `.gradual-blur{pointer-events:none;transition:opacity 0.3s ease-out}.gradual-blur-inner{pointer-events:none}`;\n document.head.appendChild(styleElement);\n};\n\nif (typeof document !== 'undefined') {\n injectStyles();\n}\n" } ], "registryDependencies": [], diff --git a/public/r/GradualBlur-TS-TW.json b/public/r/GradualBlur-TS-TW.json index ae07e735..d4d94924 100644 --- a/public/r/GradualBlur-TS-TW.json +++ b/public/r/GradualBlur-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "GradualBlur/GradualBlur.tsx", - "content": "import React, { CSSProperties, useEffect, useRef, useState, useMemo, PropsWithChildren } from 'react';\n\ntype GradualBlurProps = PropsWithChildren<{\n position?: 'top' | 'bottom' | 'left' | 'right';\n strength?: number;\n height?: string;\n width?: string;\n divCount?: number;\n exponential?: boolean;\n zIndex?: number;\n animated?: boolean | 'scroll';\n duration?: string;\n easing?: string;\n opacity?: number;\n curve?: 'linear' | 'bezier' | 'ease-in' | 'ease-out' | 'ease-in-out';\n responsive?: boolean;\n mobileHeight?: string;\n tabletHeight?: string;\n desktopHeight?: string;\n mobileWidth?: string;\n tabletWidth?: string;\n desktopWidth?: string;\n\n preset?:\n | 'top'\n | 'bottom'\n | 'left'\n | 'right'\n | 'subtle'\n | 'intense'\n | 'smooth'\n | 'sharp'\n | 'header'\n | 'footer'\n | 'sidebar'\n | 'page-header'\n | 'page-footer';\n gpuOptimized?: boolean;\n hoverIntensity?: number;\n target?: 'parent' | 'page';\n\n onAnimationComplete?: () => void;\n className?: string;\n style?: CSSProperties;\n}>;\n\nconst DEFAULT_CONFIG: Partial = {\n position: 'bottom',\n strength: 2,\n height: '6rem',\n divCount: 5,\n exponential: false,\n zIndex: 1000,\n animated: false,\n duration: '0.3s',\n easing: 'ease-out',\n opacity: 1,\n curve: 'linear',\n responsive: false,\n target: 'parent',\n className: '',\n style: {}\n};\n\nconst PRESETS: Record> = {\n top: { position: 'top', height: '6rem' },\n bottom: { position: 'bottom', height: '6rem' },\n left: { position: 'left', height: '6rem' },\n right: { position: 'right', height: '6rem' },\n\n subtle: { height: '4rem', strength: 1, opacity: 0.8, divCount: 3 },\n intense: { height: '10rem', strength: 4, divCount: 8, exponential: true },\n\n smooth: { height: '8rem', curve: 'bezier', divCount: 10 },\n sharp: { height: '5rem', curve: 'linear', divCount: 4 },\n\n header: { position: 'top', height: '8rem', curve: 'ease-out' },\n footer: { position: 'bottom', height: '8rem', curve: 'ease-out' },\n sidebar: { position: 'left', height: '6rem', strength: 2.5 },\n\n 'page-header': {\n position: 'top',\n height: '10rem',\n target: 'page',\n strength: 3\n },\n 'page-footer': {\n position: 'bottom',\n height: '10rem',\n target: 'page',\n strength: 3\n }\n};\n\nconst CURVE_FUNCTIONS: Record number> = {\n linear: p => p,\n bezier: p => p * p * (3 - 2 * p),\n 'ease-in': p => p * p,\n 'ease-out': p => 1 - Math.pow(1 - p, 2),\n 'ease-in-out': p => (p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2)\n};\n\nconst mergeConfigs = (...configs: Partial[]): Partial => {\n return configs.reduce((acc, config) => ({ ...acc, ...config }), {});\n};\n\nconst getGradientDirection = (position: string): string => {\n const directions: Record = {\n top: 'to top',\n bottom: 'to bottom',\n left: 'to left',\n right: 'to right'\n };\n return directions[position] || 'to bottom';\n};\n\nconst debounce = void>(fn: T, wait: number) => {\n let t: ReturnType;\n return (...a: Parameters) => {\n clearTimeout(t);\n t = setTimeout(() => fn(...a), wait);\n };\n};\nconst useResponsiveDimension = (\n responsive: boolean | undefined,\n config: Partial,\n key: keyof GradualBlurProps\n) => {\n const [val, setVal] = useState(config[key]);\n useEffect(() => {\n if (!responsive) return;\n const calc = () => {\n const w = window.innerWidth;\n let v: any = config[key];\n const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);\n const k = cap(key as string);\n if (w <= 480 && (config as any)['mobile' + k]) v = (config as any)['mobile' + k];\n else if (w <= 768 && (config as any)['tablet' + k]) v = (config as any)['tablet' + k];\n else if (w <= 1024 && (config as any)['desktop' + k]) v = (config as any)['desktop' + k];\n setVal(v);\n };\n const deb = debounce(calc, 100);\n calc();\n window.addEventListener('resize', deb);\n return () => window.removeEventListener('resize', deb);\n }, [responsive, config, key]);\n return responsive ? val : (config as any)[key];\n};\n\nconst useIntersectionObserver = (ref: React.RefObject, shouldObserve: boolean = false) => {\n const [isVisible, setIsVisible] = useState(!shouldObserve);\n\n useEffect(() => {\n if (!shouldObserve || !ref.current) return;\n\n const observer = new IntersectionObserver(([entry]) => setIsVisible(entry.isIntersecting), { threshold: 0.1 });\n\n observer.observe(ref.current);\n return () => observer.disconnect();\n }, [ref, shouldObserve]);\n\n return isVisible;\n};\n\nconst GradualBlur: React.FC = props => {\n const containerRef = useRef(null) as React.RefObject;\n const [isHovered, setIsHovered] = useState(false);\n\n const config = useMemo(() => {\n const presetConfig = props.preset && PRESETS[props.preset] ? PRESETS[props.preset] : {};\n return mergeConfigs(DEFAULT_CONFIG, presetConfig, props) as Required;\n }, [props]);\n\n const responsiveHeight = useResponsiveDimension(config.responsive, config, 'height');\n const responsiveWidth = useResponsiveDimension(config.responsive, config, 'width');\n\n const isVisible = useIntersectionObserver(containerRef, config.animated === 'scroll');\n\n const blurDivs = useMemo(() => {\n const divs: React.ReactNode[] = [];\n const increment = 100 / config.divCount;\n const currentStrength =\n isHovered && config.hoverIntensity ? config.strength * config.hoverIntensity : config.strength;\n\n const curveFunc = CURVE_FUNCTIONS[config.curve] || CURVE_FUNCTIONS.linear;\n\n for (let i = 1; i <= config.divCount; i++) {\n let progress = i / config.divCount;\n progress = curveFunc(progress);\n\n let blurValue: number;\n if (config.exponential) {\n blurValue = Number(Math.pow(2, progress * 4)) * 0.0625 * currentStrength;\n } else {\n blurValue = 0.0625 * (progress * config.divCount + 1) * currentStrength;\n }\n\n const p1 = Math.round((increment * i - increment) * 10) / 10;\n const p2 = Math.round(increment * i * 10) / 10;\n const p3 = Math.round((increment * i + increment) * 10) / 10;\n const p4 = Math.round((increment * i + increment * 2) * 10) / 10;\n\n let gradient = `transparent ${p1}%, black ${p2}%`;\n if (p3 <= 100) gradient += `, black ${p3}%`;\n if (p4 <= 100) gradient += `, transparent ${p4}%`;\n\n const direction = getGradientDirection(config.position);\n\n const divStyle: CSSProperties = {\n maskImage: `linear-gradient(${direction}, ${gradient})`,\n WebkitMaskImage: `linear-gradient(${direction}, ${gradient})`,\n backdropFilter: `blur(${blurValue.toFixed(3)}rem)`,\n opacity: config.opacity,\n transition:\n config.animated && config.animated !== 'scroll'\n ? `backdrop-filter ${config.duration} ${config.easing}`\n : undefined\n };\n\n divs.push(
);\n }\n\n return divs;\n }, [config, isHovered]);\n\n const containerStyle: CSSProperties = useMemo(() => {\n const isVertical = ['top', 'bottom'].includes(config.position);\n const isHorizontal = ['left', 'right'].includes(config.position);\n const isPageTarget = config.target === 'page';\n\n const baseStyle: CSSProperties = {\n position: isPageTarget ? 'fixed' : 'absolute',\n pointerEvents: config.hoverIntensity ? 'auto' : 'none',\n opacity: isVisible ? 1 : 0,\n transition: config.animated ? `opacity ${config.duration} ${config.easing}` : undefined,\n zIndex: isPageTarget ? config.zIndex + 100 : config.zIndex,\n ...config.style\n };\n\n if (isVertical) {\n baseStyle.height = responsiveHeight;\n baseStyle.width = responsiveWidth || '100%';\n baseStyle[config.position] = 0;\n baseStyle.left = 0;\n baseStyle.right = 0;\n } else if (isHorizontal) {\n baseStyle.width = responsiveWidth || responsiveHeight;\n baseStyle.height = '100%';\n baseStyle[config.position] = 0;\n baseStyle.top = 0;\n baseStyle.bottom = 0;\n }\n\n return baseStyle;\n }, [config, responsiveHeight, responsiveWidth, isVisible]);\n\n const { hoverIntensity, animated, onAnimationComplete, duration } = config as any;\n useEffect(() => {\n if (isVisible && animated === 'scroll' && onAnimationComplete) {\n const t = setTimeout(() => onAnimationComplete(), parseFloat(duration) * 1000);\n return () => clearTimeout(t);\n }\n }, [isVisible, animated, onAnimationComplete, duration]);\n\n return (\n setIsHovered(true) : undefined}\n onMouseLeave={hoverIntensity ? () => setIsHovered(false) : undefined}\n >\n
{blurDivs}
\n {props.children &&
{props.children}
}\n
\n );\n};\n\nconst GradualBlurMemo = React.memo(GradualBlur);\nGradualBlurMemo.displayName = 'GradualBlur';\n(GradualBlurMemo as any).PRESETS = PRESETS;\n(GradualBlurMemo as any).CURVE_FUNCTIONS = CURVE_FUNCTIONS;\nexport default GradualBlurMemo;\n\nconst injectStyles = () => {\n if (typeof document === 'undefined') return;\n const id = 'gradual-blur-styles';\n if (document.getElementById(id)) return;\n const el = document.createElement('style');\n el.id = id;\n el.textContent = `.gradual-blur{pointer-events:none;transition:opacity .3s ease-out}.gradual-blur-inner{pointer-events:none}`;\n document.head.appendChild(el);\n};\nif (typeof document !== 'undefined') {\n injectStyles();\n}\n" + "content": "import React, { type CSSProperties, useEffect, useRef, useState, useMemo, type PropsWithChildren } from 'react';\n\ntype GradualBlurProps = PropsWithChildren<{\n position?: 'top' | 'bottom' | 'left' | 'right';\n strength?: number;\n height?: string;\n width?: string;\n divCount?: number;\n exponential?: boolean;\n zIndex?: number;\n animated?: boolean | 'scroll';\n duration?: string;\n easing?: string;\n opacity?: number;\n curve?: 'linear' | 'bezier' | 'ease-in' | 'ease-out' | 'ease-in-out';\n responsive?: boolean;\n mobileHeight?: string;\n tabletHeight?: string;\n desktopHeight?: string;\n mobileWidth?: string;\n tabletWidth?: string;\n desktopWidth?: string;\n\n preset?:\n | 'top'\n | 'bottom'\n | 'left'\n | 'right'\n | 'subtle'\n | 'intense'\n | 'smooth'\n | 'sharp'\n | 'header'\n | 'footer'\n | 'sidebar'\n | 'page-header'\n | 'page-footer';\n gpuOptimized?: boolean;\n hoverIntensity?: number;\n target?: 'parent' | 'page';\n\n onAnimationComplete?: () => void;\n className?: string;\n style?: CSSProperties;\n}>;\n\nconst DEFAULT_CONFIG: Partial = {\n position: 'bottom',\n strength: 2,\n height: '6rem',\n divCount: 5,\n exponential: false,\n zIndex: 1000,\n animated: false,\n duration: '0.3s',\n easing: 'ease-out',\n opacity: 1,\n curve: 'linear',\n responsive: false,\n target: 'parent',\n className: '',\n style: {}\n};\n\nconst PRESETS: Record> = {\n top: { position: 'top', height: '6rem' },\n bottom: { position: 'bottom', height: '6rem' },\n left: { position: 'left', height: '6rem' },\n right: { position: 'right', height: '6rem' },\n\n subtle: { height: '4rem', strength: 1, opacity: 0.8, divCount: 3 },\n intense: { height: '10rem', strength: 4, divCount: 8, exponential: true },\n\n smooth: { height: '8rem', curve: 'bezier', divCount: 10 },\n sharp: { height: '5rem', curve: 'linear', divCount: 4 },\n\n header: { position: 'top', height: '8rem', curve: 'ease-out' },\n footer: { position: 'bottom', height: '8rem', curve: 'ease-out' },\n sidebar: { position: 'left', height: '6rem', strength: 2.5 },\n\n 'page-header': {\n position: 'top',\n height: '10rem',\n target: 'page',\n strength: 3\n },\n 'page-footer': {\n position: 'bottom',\n height: '10rem',\n target: 'page',\n strength: 3\n }\n};\n\nconst CURVE_FUNCTIONS: Record number> = {\n linear: p => p,\n bezier: p => p * p * (3 - 2 * p),\n 'ease-in': p => p * p,\n 'ease-out': p => 1 - Math.pow(1 - p, 2),\n 'ease-in-out': p => (p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2)\n};\n\nconst mergeConfigs = (...configs: Partial[]): Partial => {\n return configs.reduce((acc, config) => ({ ...acc, ...config }), {});\n};\n\nconst getGradientDirection = (position: string): string => {\n const directions: Record = {\n top: 'to top',\n bottom: 'to bottom',\n left: 'to left',\n right: 'to right'\n };\n return directions[position] || 'to bottom';\n};\n\nconst debounce = void>(fn: T, wait: number) => {\n let t: ReturnType;\n return (...a: Parameters) => {\n clearTimeout(t);\n t = setTimeout(() => fn(...a), wait);\n };\n};\nconst useResponsiveDimension = (\n responsive: boolean | undefined,\n config: Partial,\n key: keyof GradualBlurProps\n) => {\n const [val, setVal] = useState(config[key]);\n useEffect(() => {\n if (!responsive) return;\n const calc = () => {\n const w = window.innerWidth;\n let v: any = config[key];\n const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);\n const k = cap(key as string);\n if (w <= 480 && (config as any)['mobile' + k]) v = (config as any)['mobile' + k];\n else if (w <= 768 && (config as any)['tablet' + k]) v = (config as any)['tablet' + k];\n else if (w <= 1024 && (config as any)['desktop' + k]) v = (config as any)['desktop' + k];\n setVal(v);\n };\n const deb = debounce(calc, 100);\n calc();\n window.addEventListener('resize', deb);\n return () => window.removeEventListener('resize', deb);\n }, [responsive, config, key]);\n return responsive ? val : (config as any)[key];\n};\n\nconst useIntersectionObserver = (ref: React.RefObject, shouldObserve: boolean = false) => {\n const [isVisible, setIsVisible] = useState(!shouldObserve);\n\n useEffect(() => {\n if (!shouldObserve || !ref.current) return;\n\n const observer = new IntersectionObserver(([entry]) => setIsVisible(entry.isIntersecting), { threshold: 0.1 });\n\n observer.observe(ref.current);\n return () => observer.disconnect();\n }, [ref, shouldObserve]);\n\n return isVisible;\n};\n\nconst GradualBlur: React.FC = props => {\n const containerRef = useRef(null) as React.RefObject;\n const [isHovered, setIsHovered] = useState(false);\n\n const config = useMemo(() => {\n const presetConfig = props.preset && PRESETS[props.preset] ? PRESETS[props.preset] : {};\n return mergeConfigs(DEFAULT_CONFIG, presetConfig, props) as Required;\n }, [props]);\n\n const responsiveHeight = useResponsiveDimension(config.responsive, config, 'height');\n const responsiveWidth = useResponsiveDimension(config.responsive, config, 'width');\n\n const isVisible = useIntersectionObserver(containerRef, config.animated === 'scroll');\n\n const blurDivs = useMemo(() => {\n const divs: React.ReactNode[] = [];\n const increment = 100 / config.divCount;\n const currentStrength =\n isHovered && config.hoverIntensity ? config.strength * config.hoverIntensity : config.strength;\n\n const curveFunc = CURVE_FUNCTIONS[config.curve] || CURVE_FUNCTIONS.linear;\n\n for (let i = 1; i <= config.divCount; i++) {\n let progress = i / config.divCount;\n progress = curveFunc(progress);\n\n let blurValue: number;\n if (config.exponential) {\n blurValue = Number(Math.pow(2, progress * 4)) * 0.0625 * currentStrength;\n } else {\n blurValue = 0.0625 * (progress * config.divCount + 1) * currentStrength;\n }\n\n const p1 = Math.round((increment * i - increment) * 10) / 10;\n const p2 = Math.round(increment * i * 10) / 10;\n const p3 = Math.round((increment * i + increment) * 10) / 10;\n const p4 = Math.round((increment * i + increment * 2) * 10) / 10;\n\n let gradient = `transparent ${p1}%, black ${p2}%`;\n if (p3 <= 100) gradient += `, black ${p3}%`;\n if (p4 <= 100) gradient += `, transparent ${p4}%`;\n\n const direction = getGradientDirection(config.position);\n\n const divStyle: CSSProperties = {\n maskImage: `linear-gradient(${direction}, ${gradient})`,\n WebkitMaskImage: `linear-gradient(${direction}, ${gradient})`,\n backdropFilter: `blur(${blurValue.toFixed(3)}rem)`,\n opacity: config.opacity,\n transition:\n config.animated && config.animated !== 'scroll'\n ? `backdrop-filter ${config.duration} ${config.easing}`\n : undefined\n };\n\n divs.push(
);\n }\n\n return divs;\n }, [config, isHovered]);\n\n const containerStyle: CSSProperties = useMemo(() => {\n const isVertical = ['top', 'bottom'].includes(config.position);\n const isHorizontal = ['left', 'right'].includes(config.position);\n const isPageTarget = config.target === 'page';\n\n const baseStyle: CSSProperties = {\n position: isPageTarget ? 'fixed' : 'absolute',\n pointerEvents: config.hoverIntensity ? 'auto' : 'none',\n opacity: isVisible ? 1 : 0,\n transition: config.animated ? `opacity ${config.duration} ${config.easing}` : undefined,\n zIndex: isPageTarget ? config.zIndex + 100 : config.zIndex,\n ...config.style\n };\n\n if (isVertical) {\n baseStyle.height = responsiveHeight;\n baseStyle.width = responsiveWidth || '100%';\n baseStyle[config.position] = 0;\n baseStyle.left = 0;\n baseStyle.right = 0;\n } else if (isHorizontal) {\n baseStyle.width = responsiveWidth || responsiveHeight;\n baseStyle.height = '100%';\n baseStyle[config.position] = 0;\n baseStyle.top = 0;\n baseStyle.bottom = 0;\n }\n\n return baseStyle;\n }, [config, responsiveHeight, responsiveWidth, isVisible]);\n\n const { hoverIntensity, animated, onAnimationComplete, duration } = config as any;\n useEffect(() => {\n if (isVisible && animated === 'scroll' && onAnimationComplete) {\n const t = setTimeout(() => onAnimationComplete(), parseFloat(duration) * 1000);\n return () => clearTimeout(t);\n }\n }, [isVisible, animated, onAnimationComplete, duration]);\n\n return (\n setIsHovered(true) : undefined}\n onMouseLeave={hoverIntensity ? () => setIsHovered(false) : undefined}\n >\n
{blurDivs}
\n {props.children &&
{props.children}
}\n
\n );\n};\n\nconst GradualBlurMemo = React.memo(GradualBlur);\nGradualBlurMemo.displayName = 'GradualBlur';\n(GradualBlurMemo as any).PRESETS = PRESETS;\n(GradualBlurMemo as any).CURVE_FUNCTIONS = CURVE_FUNCTIONS;\nexport default GradualBlurMemo;\n\nconst injectStyles = () => {\n if (typeof document === 'undefined') return;\n const id = 'gradual-blur-styles';\n if (document.getElementById(id)) return;\n const el = document.createElement('style');\n el.id = id;\n el.textContent = `.gradual-blur{pointer-events:none;transition:opacity .3s ease-out}.gradual-blur-inner{pointer-events:none}`;\n document.head.appendChild(el);\n};\nif (typeof document !== 'undefined') {\n injectStyles();\n}\n" } ], "registryDependencies": [], diff --git a/public/r/Grainient-JS-CSS.json b/public/r/Grainient-JS-CSS.json index 98b301f9..c6b26572 100644 --- a/public/r/Grainient-JS-CSS.json +++ b/public/r/Grainient-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Grainient/Grainient.css", + "type": "registry:file", + "path": "Grainient.css", + "target": "@components/Grainient.css", "content": ".grainient-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "Grainient/Grainient.jsx", + "path": "Grainient.jsx", "content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './Grainient.css';\n\nconst hexToRgb = hex => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 1, 1];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uTimeSpeed;\nuniform float uColorBalance;\nuniform float uWarpStrength;\nuniform float uWarpFrequency;\nuniform float uWarpSpeed;\nuniform float uWarpAmplitude;\nuniform float uBlendAngle;\nuniform float uBlendSoftness;\nuniform float uRotationAmount;\nuniform float uNoiseScale;\nuniform float uGrainAmount;\nuniform float uGrainScale;\nuniform float uGrainAnimated;\nuniform float uContrast;\nuniform float uGamma;\nuniform float uSaturation;\nuniform vec2 uCenterOffset;\nuniform float uZoom;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nout vec4 fragColor;\n#define S(a,b,t) smoothstep(a,b,t)\nmat2 Rot(float a){float s=sin(a),c=cos(a);return mat2(c,-s,s,c);} \nvec2 hash(vec2 p){p=vec2(dot(p,vec2(2127.1,81.17)),dot(p,vec2(1269.5,283.37)));return fract(sin(p)*43758.5453);} \nfloat noise(vec2 p){vec2 i=floor(p),f=fract(p),u=f*f*(3.0-2.0*f);float n=mix(mix(dot(-1.0+2.0*hash(i+vec2(0.0,0.0)),f-vec2(0.0,0.0)),dot(-1.0+2.0*hash(i+vec2(1.0,0.0)),f-vec2(1.0,0.0)),u.x),mix(dot(-1.0+2.0*hash(i+vec2(0.0,1.0)),f-vec2(0.0,1.0)),dot(-1.0+2.0*hash(i+vec2(1.0,1.0)),f-vec2(1.0,1.0)),u.x),u.y);return 0.5+0.5*n;}\nvoid mainImage(out vec4 o, vec2 C){\n float t=iTime*uTimeSpeed;\n vec2 uv=C/iResolution.xy;\n float ratio=iResolution.x/iResolution.y;\n vec2 tuv=uv-0.5+uCenterOffset;\n tuv/=max(uZoom,0.001);\n\n float degree=noise(vec2(t*0.1,tuv.x*tuv.y)*uNoiseScale);\n tuv.y*=1.0/ratio;\n tuv*=Rot(radians((degree-0.5)*uRotationAmount+180.0));\n tuv.y*=ratio;\n\n float frequency=uWarpFrequency;\n float ws=max(uWarpStrength,0.001);\n float amplitude=uWarpAmplitude/ws;\n float warpTime=t*uWarpSpeed;\n tuv.x+=sin(tuv.y*frequency+warpTime)/amplitude;\n tuv.y+=sin(tuv.x*(frequency*1.5)+warpTime)/(amplitude*0.5);\n\n vec3 colLav=uColor1;\n vec3 colOrg=uColor2;\n vec3 colDark=uColor3;\n float b=uColorBalance;\n float s=max(uBlendSoftness,0.0);\n mat2 blendRot=Rot(radians(uBlendAngle));\n float blendX=(tuv*blendRot).x;\n float edge0=-0.3-b-s;\n float edge1=0.2-b+s;\n float v0=0.5-b+s;\n float v1=-0.3-b-s;\n vec3 layer1=mix(colDark,colOrg,S(edge0,edge1,blendX));\n vec3 layer2=mix(colOrg,colLav,S(edge0,edge1,blendX));\n vec3 col=mix(layer1,layer2,S(v0,v1,tuv.y));\n\n vec2 grainUv=uv*max(uGrainScale,0.001);\n if(uGrainAnimated>0.5){grainUv+=vec2(iTime*0.05);} \n float grain=fract(sin(dot(grainUv,vec2(12.9898,78.233)))*43758.5453);\n col+=(grain-0.5)*uGrainAmount;\n\n col=(col-0.5)*uContrast+0.5;\n float luma=dot(col,vec3(0.2126,0.7152,0.0722));\n col=mix(vec3(luma),col,uSaturation);\n col=pow(max(col,0.0),vec3(1.0/max(uGamma,0.001)));\n col=clamp(col,0.0,1.0);\n\n o=vec4(col,1.0);\n}\nvoid main(){\n vec4 o=vec4(0.0);\n mainImage(o,gl_FragCoord.xy);\n fragColor=o;\n}\n`;\n\n\n// Keep renderer/program alive across re-renders so Effect 2 can update\n// uniforms without ever rebuilding the WebGL context.\nconst ctxMap = new WeakMap();\n\nconst Grainient = ({\n timeSpeed = 0.25,\n colorBalance = 0.0,\n warpStrength = 1.0,\n warpFrequency = 5.0,\n warpSpeed = 2.0,\n warpAmplitude = 50.0,\n blendAngle = 0.0,\n blendSoftness = 0.05,\n rotationAmount = 500.0,\n noiseScale = 2.0,\n grainAmount = 0.1,\n grainScale = 2.0,\n grainAnimated = false,\n contrast = 1.5,\n gamma = 1.0,\n saturation = 1.0,\n centerX = 0.0,\n centerY = 0.0,\n zoom = 0.9,\n color1 = '#FF9FFC',\n color2 = '#5227FF',\n color3 = '#B497CF',\n className = ''\n}) => {\n const containerRef = useRef(null);\n\n // Effect 1: build WebGL context once, pause when offscreen / tab hidden\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n webgl: 2,\n alpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n\n const gl = renderer.gl;\n const canvas = gl.canvas;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uTimeSpeed: { value: 0.25 },\n uColorBalance: { value: 0.0 },\n uWarpStrength: { value: 1.0 },\n uWarpFrequency: { value: 5.0 },\n uWarpSpeed: { value: 2.0 },\n uWarpAmplitude: { value: 50.0 },\n uBlendAngle: { value: 0.0 },\n uBlendSoftness: { value: 0.05 },\n uRotationAmount: { value: 500.0 },\n uNoiseScale: { value: 2.0 },\n uGrainAmount: { value: 0.1 },\n uGrainScale: { value: 2.0 },\n uGrainAnimated: { value: 0.0 },\n uContrast: { value: 1.5 },\n uGamma: { value: 1.0 },\n uSaturation: { value: 1.0 },\n uCenterOffset: { value: new Float32Array([0, 0]) },\n uZoom: { value: 0.9 },\n uColor1: { value: new Float32Array([1, 1, 1]) },\n uColor2: { value: new Float32Array([1, 1, 1]) },\n uColor3: { value: new Float32Array([1, 1, 1]) }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n ctxMap.set(container, { renderer, program, mesh });\n\n const setSize = () => {\n const rect = container.getBoundingClientRect();\n const w = Math.max(1, Math.floor(rect.width));\n const h = Math.max(1, Math.floor(rect.height));\n renderer.setSize(w, h);\n const res = program.uniforms.iResolution.value;\n res[0] = gl.drawingBufferWidth;\n res[1] = gl.drawingBufferHeight;\n renderer.render({ scene: mesh });\n };\n\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n setSize();\n\n let raf = 0;\n let isVisible = true;\n let isPageVisible = !document.hidden;\n const t0 = performance.now();\n\n const loop = t => {\n program.uniforms.iTime.value = (t - t0) * 0.001;\n renderer.render({ scene: mesh });\n raf = requestAnimationFrame(loop);\n };\n\n const tryStart = () => {\n if (isVisible && isPageVisible && raf === 0) raf = requestAnimationFrame(loop);\n };\n const tryStop = () => {\n if (raf !== 0) { cancelAnimationFrame(raf); raf = 0; }\n };\n\n const io = new IntersectionObserver(\n ([entry]) => { isVisible = entry.isIntersecting; isVisible ? tryStart() : tryStop(); },\n { threshold: 0 }\n );\n io.observe(container);\n\n const onVisibility = () => {\n isPageVisible = !document.hidden;\n isPageVisible ? tryStart() : tryStop();\n };\n document.addEventListener('visibilitychange', onVisibility);\n\n tryStart();\n\n return () => {\n tryStop();\n ro.disconnect();\n io.disconnect();\n document.removeEventListener('visibilitychange', onVisibility);\n ctxMap.delete(container);\n try { container.removeChild(canvas); } catch { /* ignore */ }\n };\n }, []); // renderer created once\n\n // Effect 2: sync props to uniforms — zero GPU cost, no teardown\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n const ctx = ctxMap.get(container);\n if (!ctx) return;\n const { program } = ctx;\n const u = program.uniforms;\n\n u.uTimeSpeed.value = timeSpeed;\n u.uColorBalance.value = colorBalance;\n u.uWarpStrength.value = warpStrength;\n u.uWarpFrequency.value = warpFrequency;\n u.uWarpSpeed.value = warpSpeed;\n u.uWarpAmplitude.value = warpAmplitude;\n u.uBlendAngle.value = blendAngle;\n u.uBlendSoftness.value = blendSoftness;\n u.uRotationAmount.value = rotationAmount;\n u.uNoiseScale.value = noiseScale;\n u.uGrainAmount.value = grainAmount;\n u.uGrainScale.value = grainScale;\n u.uGrainAnimated.value = grainAnimated ? 1.0 : 0.0;\n u.uContrast.value = contrast;\n u.uGamma.value = gamma;\n u.uSaturation.value = saturation;\n u.uCenterOffset.value = new Float32Array([centerX, centerY]);\n u.uZoom.value = zoom;\n u.uColor1.value = new Float32Array(hexToRgb(color1));\n u.uColor2.value = new Float32Array(hexToRgb(color2));\n u.uColor3.value = new Float32Array(hexToRgb(color3));\n }, [\n timeSpeed, colorBalance, warpStrength, warpFrequency, warpSpeed,\n warpAmplitude, blendAngle, blendSoftness, rotationAmount, noiseScale,\n grainAmount, grainScale, grainAnimated, contrast, gamma, saturation,\n centerX, centerY, zoom, color1, color2, color3\n ]);\n\n\n return
;\n};\n\nexport default Grainient;" } ], diff --git a/public/r/Grainient-TS-CSS.json b/public/r/Grainient-TS-CSS.json index 1cf3948a..85f120a4 100644 --- a/public/r/Grainient-TS-CSS.json +++ b/public/r/Grainient-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Grainient/Grainient.css", + "type": "registry:file", + "path": "Grainient.css", + "target": "@components/Grainient.css", "content": ".grainient-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "Grainient/Grainient.tsx", + "path": "Grainient.tsx", "content": "import React, { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './Grainient.css';\n\ninterface GrainientProps {\n timeSpeed?: number;\n colorBalance?: number;\n warpStrength?: number;\n warpFrequency?: number;\n warpSpeed?: number;\n warpAmplitude?: number;\n blendAngle?: number;\n blendSoftness?: number;\n rotationAmount?: number;\n noiseScale?: number;\n grainAmount?: number;\n grainScale?: number;\n grainAnimated?: boolean;\n contrast?: number;\n gamma?: number;\n saturation?: number;\n centerX?: number;\n centerY?: number;\n zoom?: number;\n color1?: string;\n color2?: string;\n color3?: string;\n className?: string;\n}\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 1, 1];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uTimeSpeed;\nuniform float uColorBalance;\nuniform float uWarpStrength;\nuniform float uWarpFrequency;\nuniform float uWarpSpeed;\nuniform float uWarpAmplitude;\nuniform float uBlendAngle;\nuniform float uBlendSoftness;\nuniform float uRotationAmount;\nuniform float uNoiseScale;\nuniform float uGrainAmount;\nuniform float uGrainScale;\nuniform float uGrainAnimated;\nuniform float uContrast;\nuniform float uGamma;\nuniform float uSaturation;\nuniform vec2 uCenterOffset;\nuniform float uZoom;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nout vec4 fragColor;\n#define S(a,b,t) smoothstep(a,b,t)\nmat2 Rot(float a){float s=sin(a),c=cos(a);return mat2(c,-s,s,c);} \nvec2 hash(vec2 p){p=vec2(dot(p,vec2(2127.1,81.17)),dot(p,vec2(1269.5,283.37)));return fract(sin(p)*43758.5453);} \nfloat noise(vec2 p){vec2 i=floor(p),f=fract(p),u=f*f*(3.0-2.0*f);float n=mix(mix(dot(-1.0+2.0*hash(i+vec2(0.0,0.0)),f-vec2(0.0,0.0)),dot(-1.0+2.0*hash(i+vec2(1.0,0.0)),f-vec2(1.0,0.0)),u.x),mix(dot(-1.0+2.0*hash(i+vec2(0.0,1.0)),f-vec2(0.0,1.0)),dot(-1.0+2.0*hash(i+vec2(1.0,1.0)),f-vec2(1.0,1.0)),u.x),u.y);return 0.5+0.5*n;}\nvoid mainImage(out vec4 o, vec2 C){\n float t=iTime*uTimeSpeed;\n vec2 uv=C/iResolution.xy;\n float ratio=iResolution.x/iResolution.y;\n vec2 tuv=uv-0.5+uCenterOffset;\n tuv/=max(uZoom,0.001);\n\n float degree=noise(vec2(t*0.1,tuv.x*tuv.y)*uNoiseScale);\n tuv.y*=1.0/ratio;\n tuv*=Rot(radians((degree-0.5)*uRotationAmount+180.0));\n tuv.y*=ratio;\n\n float frequency=uWarpFrequency;\n float ws=max(uWarpStrength,0.001);\n float amplitude=uWarpAmplitude/ws;\n float warpTime=t*uWarpSpeed;\n tuv.x+=sin(tuv.y*frequency+warpTime)/amplitude;\n tuv.y+=sin(tuv.x*(frequency*1.5)+warpTime)/(amplitude*0.5);\n\n vec3 colLav=uColor1;\n vec3 colOrg=uColor2;\n vec3 colDark=uColor3;\n float b=uColorBalance;\n float s=max(uBlendSoftness,0.0);\n mat2 blendRot=Rot(radians(uBlendAngle));\n float blendX=(tuv*blendRot).x;\n float edge0=-0.3-b-s;\n float edge1=0.2-b+s;\n float v0=0.5-b+s;\n float v1=-0.3-b-s;\n vec3 layer1=mix(colDark,colOrg,S(edge0,edge1,blendX));\n vec3 layer2=mix(colOrg,colLav,S(edge0,edge1,blendX));\n vec3 col=mix(layer1,layer2,S(v0,v1,tuv.y));\n\n vec2 grainUv=uv*max(uGrainScale,0.001);\n if(uGrainAnimated>0.5){grainUv+=vec2(iTime*0.05);} \n float grain=fract(sin(dot(grainUv,vec2(12.9898,78.233)))*43758.5453);\n col+=(grain-0.5)*uGrainAmount;\n\n col=(col-0.5)*uContrast+0.5;\n float luma=dot(col,vec3(0.2126,0.7152,0.0722));\n col=mix(vec3(luma),col,uSaturation);\n col=pow(max(col,0.0),vec3(1.0/max(uGamma,0.001)));\n col=clamp(col,0.0,1.0);\n\n o=vec4(col,1.0);\n}\nvoid main(){\n vec4 o=vec4(0.0);\n mainImage(o,gl_FragCoord.xy);\n fragColor=o;\n}\n`;\n\n\n// Keep renderer/program alive across re-renders so Effect 2 can update\n// uniforms without ever rebuilding the WebGL context.\ntype GrainientCtx = {\n renderer: InstanceType;\n program: InstanceType;\n mesh: InstanceType;\n};\nconst ctxMap = new WeakMap();\n\nconst Grainient: React.FC = ({\n timeSpeed = 0.25,\n colorBalance = 0.0,\n warpStrength = 1.0,\n warpFrequency = 5.0,\n warpSpeed = 2.0,\n warpAmplitude = 50.0,\n blendAngle = 0.0,\n blendSoftness = 0.05,\n rotationAmount = 500.0,\n noiseScale = 2.0,\n grainAmount = 0.1,\n grainScale = 2.0,\n grainAnimated = false,\n contrast = 1.5,\n gamma = 1.0,\n saturation = 1.0,\n centerX = 0.0,\n centerY = 0.0,\n zoom = 0.9,\n color1 = '#FF9FFC',\n color2 = '#5227FF',\n color3 = '#B497CF',\n className = ''\n}) => {\n const containerRef = useRef(null);\n\n // Effect 1: build WebGL context once, pause when offscreen / tab hidden\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n webgl: 2,\n alpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n\n const gl = renderer.gl;\n const canvas = gl.canvas as HTMLCanvasElement;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uTimeSpeed: { value: 0.25 },\n uColorBalance: { value: 0.0 },\n uWarpStrength: { value: 1.0 },\n uWarpFrequency: { value: 5.0 },\n uWarpSpeed: { value: 2.0 },\n uWarpAmplitude: { value: 50.0 },\n uBlendAngle: { value: 0.0 },\n uBlendSoftness: { value: 0.05 },\n uRotationAmount: { value: 500.0 },\n uNoiseScale: { value: 2.0 },\n uGrainAmount: { value: 0.1 },\n uGrainScale: { value: 2.0 },\n uGrainAnimated: { value: 0.0 },\n uContrast: { value: 1.5 },\n uGamma: { value: 1.0 },\n uSaturation: { value: 1.0 },\n uCenterOffset: { value: new Float32Array([0, 0]) },\n uZoom: { value: 0.9 },\n uColor1: { value: new Float32Array([1, 1, 1]) },\n uColor2: { value: new Float32Array([1, 1, 1]) },\n uColor3: { value: new Float32Array([1, 1, 1]) }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n ctxMap.set(container, { renderer, program, mesh });\n\n const setSize = () => {\n const rect = container.getBoundingClientRect();\n const w = Math.max(1, Math.floor(rect.width));\n const h = Math.max(1, Math.floor(rect.height));\n renderer.setSize(w, h);\n const res = (program.uniforms.iResolution as { value: Float32Array }).value;\n res[0] = gl.drawingBufferWidth;\n res[1] = gl.drawingBufferHeight;\n renderer.render({ scene: mesh });\n };\n\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n setSize();\n\n let raf = 0;\n let isVisible = true;\n let isPageVisible = !document.hidden;\n const t0 = performance.now();\n\n const loop = (t: number) => {\n (program.uniforms.iTime as { value: number }).value = (t - t0) * 0.001;\n renderer.render({ scene: mesh });\n raf = requestAnimationFrame(loop);\n };\n\n const tryStart = () => {\n if (isVisible && isPageVisible && raf === 0) raf = requestAnimationFrame(loop);\n };\n const tryStop = () => {\n if (raf !== 0) { cancelAnimationFrame(raf); raf = 0; }\n };\n\n const io = new IntersectionObserver(\n ([entry]) => { isVisible = entry.isIntersecting; isVisible ? tryStart() : tryStop(); },\n { threshold: 0 }\n );\n io.observe(container);\n\n const onVisibility = () => {\n isPageVisible = !document.hidden;\n isPageVisible ? tryStart() : tryStop();\n };\n document.addEventListener('visibilitychange', onVisibility);\n\n tryStart();\n\n return () => {\n tryStop();\n ro.disconnect();\n io.disconnect();\n document.removeEventListener('visibilitychange', onVisibility);\n ctxMap.delete(container);\n try { container.removeChild(canvas); } catch { /* ignore */ }\n };\n }, []); // renderer created once\n\n // Effect 2: sync props to uniforms — zero GPU cost, no teardown\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n const ctx = ctxMap.get(container);\n if (!ctx) return;\n const { program } = ctx;\n const u = program.uniforms as Record;\n\n u.uTimeSpeed.value = timeSpeed;\n u.uColorBalance.value = colorBalance;\n u.uWarpStrength.value = warpStrength;\n u.uWarpFrequency.value = warpFrequency;\n u.uWarpSpeed.value = warpSpeed;\n u.uWarpAmplitude.value = warpAmplitude;\n u.uBlendAngle.value = blendAngle;\n u.uBlendSoftness.value = blendSoftness;\n u.uRotationAmount.value = rotationAmount;\n u.uNoiseScale.value = noiseScale;\n u.uGrainAmount.value = grainAmount;\n u.uGrainScale.value = grainScale;\n u.uGrainAnimated.value = grainAnimated ? 1.0 : 0.0;\n u.uContrast.value = contrast;\n u.uGamma.value = gamma;\n u.uSaturation.value = saturation;\n u.uCenterOffset.value = new Float32Array([centerX, centerY]);\n u.uZoom.value = zoom;\n u.uColor1.value = new Float32Array(hexToRgb(color1));\n u.uColor2.value = new Float32Array(hexToRgb(color2));\n u.uColor3.value = new Float32Array(hexToRgb(color3));\n }, [\n timeSpeed, colorBalance, warpStrength, warpFrequency, warpSpeed,\n warpAmplitude, blendAngle, blendSoftness, rotationAmount, noiseScale,\n grainAmount, grainScale, grainAnimated, contrast, gamma, saturation,\n centerX, centerY, zoom, color1, color2, color3\n ]);\n\n\n return
;\n};\n\nexport default Grainient;\n" } ], diff --git a/public/r/GridDistortion-JS-CSS.json b/public/r/GridDistortion-JS-CSS.json index 5e42d5b4..27c4c9f0 100644 --- a/public/r/GridDistortion-JS-CSS.json +++ b/public/r/GridDistortion-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GridDistortion/GridDistortion.css", + "type": "registry:file", + "path": "GridDistortion.css", + "target": "@components/GridDistortion.css", "content": ".distortion-container {\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "GridDistortion/GridDistortion.jsx", + "path": "GridDistortion.jsx", "content": "import { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\nimport './GridDistortion.css';\n\nconst vertexShader = `\nuniform float time;\nvarying vec2 vUv;\nvarying vec3 vPosition;\n\nvoid main() {\n vUv = uv;\n vPosition = position;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}`;\n\nconst fragmentShader = `\nuniform sampler2D uDataTexture;\nuniform sampler2D uTexture;\nuniform vec4 resolution;\nvarying vec2 vUv;\n\nvoid main() {\n vec2 uv = vUv;\n vec4 offset = texture2D(uDataTexture, vUv);\n gl_FragColor = texture2D(uTexture, uv - 0.02 * offset.rg);\n}`;\n\nconst GridDistortion = ({ grid = 15, mouse = 0.1, strength = 0.15, relaxation = 0.9, imageSrc, className = '' }) => {\n const containerRef = useRef(null);\n const sceneRef = useRef(null);\n const rendererRef = useRef(null);\n const cameraRef = useRef(null);\n const planeRef = useRef(null);\n const imageAspectRef = useRef(1);\n const animationIdRef = useRef(null);\n const resizeObserverRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const container = containerRef.current;\n\n const scene = new THREE.Scene();\n sceneRef.current = scene;\n\n const renderer = new THREE.WebGLRenderer({\n antialias: true,\n alpha: true,\n powerPreference: 'high-performance'\n });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n container.innerHTML = '';\n container.appendChild(renderer.domElement);\n\n const camera = new THREE.OrthographicCamera(0, 0, 0, 0, -1000, 1000);\n camera.position.z = 2;\n cameraRef.current = camera;\n\n const uniforms = {\n time: { value: 0 },\n resolution: { value: new THREE.Vector4() },\n uTexture: { value: null },\n uDataTexture: { value: null }\n };\n\n const textureLoader = new THREE.TextureLoader();\n textureLoader.load(imageSrc, texture => {\n texture.minFilter = THREE.LinearFilter;\n texture.magFilter = THREE.LinearFilter;\n texture.wrapS = THREE.ClampToEdgeWrapping;\n texture.wrapT = THREE.ClampToEdgeWrapping;\n imageAspectRef.current = texture.image.width / texture.image.height;\n uniforms.uTexture.value = texture;\n handleResize();\n });\n\n const size = grid;\n const data = new Float32Array(4 * size * size);\n for (let i = 0; i < size * size; i++) {\n data[i * 4] = Math.random() * 255 - 125;\n data[i * 4 + 1] = Math.random() * 255 - 125;\n }\n\n const dataTexture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat, THREE.FloatType);\n dataTexture.needsUpdate = true;\n uniforms.uDataTexture.value = dataTexture;\n\n const material = new THREE.ShaderMaterial({\n side: THREE.DoubleSide,\n uniforms,\n vertexShader,\n fragmentShader,\n transparent: true\n });\n\n const geometry = new THREE.PlaneGeometry(1, 1, size - 1, size - 1);\n const plane = new THREE.Mesh(geometry, material);\n planeRef.current = plane;\n scene.add(plane);\n\n const handleResize = () => {\n if (!container || !renderer || !camera) return;\n\n const rect = container.getBoundingClientRect();\n const width = rect.width;\n const height = rect.height;\n\n if (width === 0 || height === 0) return;\n\n const containerAspect = width / height;\n\n renderer.setSize(width, height);\n\n if (plane) {\n plane.scale.set(containerAspect, 1, 1);\n }\n\n const frustumHeight = 1;\n const frustumWidth = frustumHeight * containerAspect;\n camera.left = -frustumWidth / 2;\n camera.right = frustumWidth / 2;\n camera.top = frustumHeight / 2;\n camera.bottom = -frustumHeight / 2;\n camera.updateProjectionMatrix();\n\n uniforms.resolution.value.set(width, height, 1, 1);\n };\n\n if (window.ResizeObserver) {\n const resizeObserver = new ResizeObserver(() => {\n handleResize();\n });\n resizeObserver.observe(container);\n resizeObserverRef.current = resizeObserver;\n } else {\n window.addEventListener('resize', handleResize);\n }\n\n const mouseState = {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n };\n\n const handleMouseMove = e => {\n const rect = container.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1 - (e.clientY - rect.top) / rect.height;\n mouseState.vX = x - mouseState.prevX;\n mouseState.vY = y - mouseState.prevY;\n Object.assign(mouseState, { x, y, prevX: x, prevY: y });\n };\n\n const handleMouseLeave = () => {\n if (dataTexture) {\n dataTexture.needsUpdate = true;\n }\n Object.assign(mouseState, {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n });\n };\n\n container.addEventListener('mousemove', handleMouseMove);\n container.addEventListener('mouseleave', handleMouseLeave);\n\n handleResize();\n\n const animate = () => {\n animationIdRef.current = requestAnimationFrame(animate);\n\n if (!renderer || !scene || !camera) return;\n\n uniforms.time.value += 0.05;\n\n const data = dataTexture.image.data;\n for (let i = 0; i < size * size; i++) {\n data[i * 4] *= relaxation;\n data[i * 4 + 1] *= relaxation;\n }\n\n const gridMouseX = size * mouseState.x;\n const gridMouseY = size * mouseState.y;\n const maxDist = size * mouse;\n\n for (let i = 0; i < size; i++) {\n for (let j = 0; j < size; j++) {\n const distSq = Math.pow(gridMouseX - i, 2) + Math.pow(gridMouseY - j, 2);\n if (distSq < maxDist * maxDist) {\n const index = 4 * (i + size * j);\n const power = Math.min(maxDist / Math.sqrt(distSq), 10);\n data[index] += strength * 100 * mouseState.vX * power;\n data[index + 1] -= strength * 100 * mouseState.vY * power;\n }\n }\n }\n\n dataTexture.needsUpdate = true;\n renderer.render(scene, camera);\n };\n\n animate();\n\n return () => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n }\n\n if (resizeObserverRef.current) {\n resizeObserverRef.current.disconnect();\n } else {\n window.removeEventListener('resize', handleResize);\n }\n\n container.removeEventListener('mousemove', handleMouseMove);\n container.removeEventListener('mouseleave', handleMouseLeave);\n\n if (renderer) {\n renderer.dispose();\n renderer.forceContextLoss();\n if (container.contains(renderer.domElement)) {\n container.removeChild(renderer.domElement);\n }\n }\n\n if (geometry) geometry.dispose();\n if (material) material.dispose();\n if (dataTexture) dataTexture.dispose();\n if (uniforms.uTexture.value) uniforms.uTexture.value.dispose();\n\n sceneRef.current = null;\n rendererRef.current = null;\n cameraRef.current = null;\n planeRef.current = null;\n };\n }, [grid, mouse, strength, relaxation, imageSrc]);\n\n return (\n \n );\n};\n\nexport default GridDistortion;\n" } ], diff --git a/public/r/GridDistortion-TS-CSS.json b/public/r/GridDistortion-TS-CSS.json index cb07d43b..0b320068 100644 --- a/public/r/GridDistortion-TS-CSS.json +++ b/public/r/GridDistortion-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GridDistortion/GridDistortion.css", + "type": "registry:file", + "path": "GridDistortion.css", + "target": "@components/GridDistortion.css", "content": ".distortion-container {\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "GridDistortion/GridDistortion.tsx", + "path": "GridDistortion.tsx", "content": "import React, { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\nimport './GridDistortion.css';\n\ninterface GridDistortionProps {\n grid?: number;\n mouse?: number;\n strength?: number;\n relaxation?: number;\n imageSrc: string;\n className?: string;\n}\n\nconst vertexShader = `\nuniform float time;\nvarying vec2 vUv;\nvarying vec3 vPosition;\n\nvoid main() {\n vUv = uv;\n vPosition = position;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nuniform sampler2D uDataTexture;\nuniform sampler2D uTexture;\nuniform vec4 resolution;\nvarying vec2 vUv;\n\nvoid main() {\n vec2 uv = vUv;\n vec4 offset = texture2D(uDataTexture, vUv);\n gl_FragColor = texture2D(uTexture, uv - 0.02 * offset.rg);\n}\n`;\n\nconst GridDistortion: React.FC = ({\n grid = 15,\n mouse = 0.1,\n strength = 0.15,\n relaxation = 0.9,\n imageSrc,\n className = ''\n}) => {\n const containerRef = useRef(null);\n const sceneRef = useRef(null);\n const rendererRef = useRef(null);\n const cameraRef = useRef(null);\n const planeRef = useRef(null);\n const imageAspectRef = useRef(1);\n const animationIdRef = useRef(null);\n const resizeObserverRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const container = containerRef.current;\n\n const scene = new THREE.Scene();\n sceneRef.current = scene;\n\n const renderer = new THREE.WebGLRenderer({\n antialias: true,\n alpha: true,\n powerPreference: 'high-performance'\n });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n container.innerHTML = '';\n container.appendChild(renderer.domElement);\n\n const camera = new THREE.OrthographicCamera(0, 0, 0, 0, -1000, 1000);\n camera.position.z = 2;\n cameraRef.current = camera;\n\n const uniforms = {\n time: { value: 0 },\n resolution: { value: new THREE.Vector4() },\n uTexture: { value: null as THREE.Texture | null },\n uDataTexture: { value: null as THREE.DataTexture | null }\n };\n\n const textureLoader = new THREE.TextureLoader();\n textureLoader.load(imageSrc, texture => {\n texture.minFilter = THREE.LinearFilter;\n texture.magFilter = THREE.LinearFilter;\n texture.wrapS = THREE.ClampToEdgeWrapping;\n texture.wrapT = THREE.ClampToEdgeWrapping;\n imageAspectRef.current = texture.image.width / texture.image.height;\n uniforms.uTexture.value = texture;\n handleResize();\n });\n\n const size = grid;\n const data = new Float32Array(4 * size * size);\n for (let i = 0; i < size * size; i++) {\n data[i * 4] = Math.random() * 255 - 125;\n data[i * 4 + 1] = Math.random() * 255 - 125;\n }\n\n const dataTexture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat, THREE.FloatType);\n dataTexture.needsUpdate = true;\n uniforms.uDataTexture.value = dataTexture;\n\n const material = new THREE.ShaderMaterial({\n side: THREE.DoubleSide,\n uniforms,\n vertexShader,\n fragmentShader,\n transparent: true\n });\n\n const geometry = new THREE.PlaneGeometry(1, 1, size - 1, size - 1);\n const plane = new THREE.Mesh(geometry, material);\n planeRef.current = plane;\n scene.add(plane);\n\n const handleResize = () => {\n if (!container || !renderer || !camera) return;\n\n const rect = container.getBoundingClientRect();\n const width = rect.width;\n const height = rect.height;\n\n if (width === 0 || height === 0) return;\n\n const containerAspect = width / height;\n\n renderer.setSize(width, height);\n\n if (plane) {\n plane.scale.set(containerAspect, 1, 1);\n }\n\n const frustumHeight = 1;\n const frustumWidth = frustumHeight * containerAspect;\n camera.left = -frustumWidth / 2;\n camera.right = frustumWidth / 2;\n camera.top = frustumHeight / 2;\n camera.bottom = -frustumHeight / 2;\n camera.updateProjectionMatrix();\n\n uniforms.resolution.value.set(width, height, 1, 1);\n };\n\n if (window.ResizeObserver) {\n const resizeObserver = new ResizeObserver(() => {\n handleResize();\n });\n resizeObserver.observe(container);\n resizeObserverRef.current = resizeObserver;\n } else {\n window.addEventListener('resize', handleResize);\n }\n\n const mouseState = {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n };\n\n const handleMouseMove = (e: MouseEvent) => {\n const rect = container.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1 - (e.clientY - rect.top) / rect.height;\n mouseState.vX = x - mouseState.prevX;\n mouseState.vY = y - mouseState.prevY;\n Object.assign(mouseState, { x, y, prevX: x, prevY: y });\n };\n\n const handleMouseLeave = () => {\n if (dataTexture) {\n dataTexture.needsUpdate = true;\n }\n Object.assign(mouseState, {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n });\n };\n\n container.addEventListener('mousemove', handleMouseMove);\n container.addEventListener('mouseleave', handleMouseLeave);\n\n handleResize();\n\n const animate = () => {\n animationIdRef.current = requestAnimationFrame(animate);\n\n if (!renderer || !scene || !camera) return;\n\n uniforms.time.value += 0.05;\n\n if (!(dataTexture.image.data instanceof Float32Array)) {\n console.error('dataTexture.image.data is not a Float32Array');\n return;\n }\n const data: Float32Array = dataTexture.image.data;\n for (let i = 0; i < size * size; i++) {\n data[i * 4] *= relaxation;\n data[i * 4 + 1] *= relaxation;\n }\n\n const gridMouseX = size * mouseState.x;\n const gridMouseY = size * mouseState.y;\n const maxDist = size * mouse;\n\n for (let i = 0; i < size; i++) {\n for (let j = 0; j < size; j++) {\n const distSq = Math.pow(gridMouseX - i, 2) + Math.pow(gridMouseY - j, 2);\n if (distSq < maxDist * maxDist) {\n const index = 4 * (i + size * j);\n const power = Math.min(maxDist / Math.sqrt(distSq), 10);\n data[index] += strength * 100 * mouseState.vX * power;\n data[index + 1] -= strength * 100 * mouseState.vY * power;\n }\n }\n }\n\n dataTexture.needsUpdate = true;\n renderer.render(scene, camera);\n };\n\n animate();\n\n return () => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n }\n\n if (resizeObserverRef.current) {\n resizeObserverRef.current.disconnect();\n } else {\n window.removeEventListener('resize', handleResize);\n }\n\n container.removeEventListener('mousemove', handleMouseMove);\n container.removeEventListener('mouseleave', handleMouseLeave);\n\n if (renderer) {\n renderer.dispose();\n renderer.forceContextLoss();\n if (container.contains(renderer.domElement)) {\n container.removeChild(renderer.domElement);\n }\n }\n\n if (geometry) geometry.dispose();\n if (material) material.dispose();\n if (dataTexture) dataTexture.dispose();\n if (uniforms.uTexture.value) uniforms.uTexture.value.dispose();\n\n sceneRef.current = null;\n rendererRef.current = null;\n cameraRef.current = null;\n planeRef.current = null;\n };\n }, [grid, mouse, strength, relaxation, imageSrc]);\n\n return (\n \n );\n};\n\nexport default GridDistortion;\n" } ], diff --git a/public/r/GridMotion-JS-CSS.json b/public/r/GridMotion-JS-CSS.json index a5444d96..792ee3ab 100644 --- a/public/r/GridMotion-JS-CSS.json +++ b/public/r/GridMotion-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GridMotion/GridMotion.css", + "type": "registry:file", + "path": "GridMotion.css", + "target": "@components/GridMotion.css", "content": ".noscroll {\n height: 100%;\n width: 100%;\n overflow: hidden;\n}\n\n.intro {\n width: 100%;\n height: 100vh;\n overflow: hidden;\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.intro::after {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-size: 250px;\n pointer-events: none;\n z-index: 4;\n}\n\n.gridMotion-container {\n gap: 1rem;\n flex: none;\n position: relative;\n width: 150vw;\n height: 150vh;\n display: grid;\n grid-template-rows: repeat(4, 1fr);\n grid-template-columns: 100%;\n transform: rotate(-15deg);\n transform-origin: center center;\n z-index: 2;\n}\n\n.row {\n display: grid;\n gap: 1rem;\n grid-template-columns: repeat(7, 1fr);\n will-change: transform, filter;\n}\n\n.row__item {\n position: relative;\n}\n\n.row__item-inner {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n border-radius: 10px;\n background-color: #111;\n display: flex;\n align-items: center;\n justify-content: center;\n color: white;\n font-size: 1.5rem;\n}\n\n.row__item-img {\n width: 100%;\n height: 100%;\n background-size: cover;\n background-position: 50% 50%;\n position: absolute;\n top: 0;\n left: 0;\n}\n\n.row__item-content {\n padding: 1rem;\n text-align: center;\n z-index: 1;\n}\n\n.fullview {\n position: relative;\n width: 100%;\n height: 100%;\n top: 0;\n left: 0;\n pointer-events: none;\n}\n\n.fullview .row__item-inner {\n border-radius: 0px;\n}\n" }, { "type": "registry:component", - "path": "GridMotion/GridMotion.jsx", + "path": "GridMotion.jsx", "content": "import { useEffect, useRef } from 'react';\nimport { gsap } from 'gsap';\nimport './GridMotion.css';\n\nconst GridMotion = ({ items = [], gradientColor = 'black' }) => {\n const gridRef = useRef(null);\n const rowRefs = useRef([]);\n const mouseXRef = useRef(window.innerWidth / 2);\n\n const totalItems = 28;\n const defaultItems = Array.from({ length: totalItems }, (_, index) => `Item ${index + 1}`);\n const combinedItems = items.length > 0 ? items.slice(0, totalItems) : defaultItems;\n\n useEffect(() => {\n gsap.ticker.lagSmoothing(0);\n\n const handleMouseMove = e => {\n mouseXRef.current = e.clientX;\n };\n\n const updateMotion = () => {\n const maxMoveAmount = 300;\n const baseDuration = 0.8;\n const inertiaFactors = [0.6, 0.4, 0.3, 0.2];\n\n rowRefs.current.forEach((row, index) => {\n if (row) {\n const direction = index % 2 === 0 ? 1 : -1;\n const moveAmount = ((mouseXRef.current / window.innerWidth) * maxMoveAmount - maxMoveAmount / 2) * direction;\n\n gsap.to(row, {\n x: moveAmount,\n duration: baseDuration + inertiaFactors[index % inertiaFactors.length],\n ease: 'power3.out',\n overwrite: 'auto'\n });\n }\n });\n };\n\n const removeAnimationLoop = gsap.ticker.add(updateMotion);\n\n window.addEventListener('mousemove', handleMouseMove);\n\n return () => {\n window.removeEventListener('mousemove', handleMouseMove);\n removeAnimationLoop();\n };\n }, []);\n\n return (\n
\n \n
\n {[...Array(4)].map((_, rowIndex) => (\n
{\n rowRefs.current[rowIndex] = el;\n }}>\n {[...Array(7)].map((_, itemIndex) => {\n const content = combinedItems[rowIndex * 7 + itemIndex];\n return (\n
\n
\n {typeof content === 'string' && content.startsWith('http') ? (\n
\n ) : (\n
{content}
\n )}\n
\n
\n );\n })}\n
\n ))}\n
\n
\n \n
\n );\n};\n\nexport default GridMotion;\n" } ], diff --git a/public/r/GridMotion-TS-CSS.json b/public/r/GridMotion-TS-CSS.json index 2c1adbe6..1705e9c1 100644 --- a/public/r/GridMotion-TS-CSS.json +++ b/public/r/GridMotion-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GridMotion/GridMotion.css", + "type": "registry:file", + "path": "GridMotion.css", + "target": "@components/GridMotion.css", "content": ".noscroll {\n height: 100%;\n width: 100%;\n overflow: hidden;\n}\n\n.intro {\n width: 100%;\n height: 100vh;\n overflow: hidden;\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.intro::after {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-size: 250px;\n pointer-events: none;\n z-index: 4;\n}\n\n.gridMotion-container {\n gap: 1rem;\n flex: none;\n position: relative;\n width: 150vw;\n height: 150vh;\n display: grid;\n grid-template-rows: repeat(4, 1fr);\n grid-template-columns: 100%;\n transform: rotate(-15deg);\n transform-origin: center center;\n z-index: 2;\n}\n\n.row {\n display: grid;\n gap: 1rem;\n grid-template-columns: repeat(7, 1fr);\n will-change: transform, filter;\n}\n\n.row__item {\n position: relative;\n}\n\n.row__item-inner {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n border-radius: 10px;\n background-color: #111;\n display: flex;\n align-items: center;\n justify-content: center;\n color: white;\n font-size: 1.5rem;\n}\n\n.row__item-img {\n width: 100%;\n height: 100%;\n background-size: cover;\n background-position: 50% 50%;\n position: absolute;\n top: 0;\n left: 0;\n}\n\n.row__item-content {\n padding: 1rem;\n text-align: center;\n z-index: 1;\n}\n\n.fullview {\n position: relative;\n width: 100%;\n height: 100%;\n top: 0;\n left: 0;\n pointer-events: none;\n}\n\n.fullview .row__item-inner {\n border-radius: 0px;\n}\n" }, { "type": "registry:component", - "path": "GridMotion/GridMotion.tsx", - "content": "import { useEffect, useRef, FC, ReactNode } from 'react';\nimport { gsap } from 'gsap';\nimport './GridMotion.css';\n\ninterface GridMotionProps {\n items?: (string | ReactNode)[];\n gradientColor?: string;\n}\n\nconst GridMotion: FC = ({ items = [], gradientColor = 'black' }) => {\n const gridRef = useRef(null);\n const rowRefs = useRef<(HTMLDivElement | null)[]>([]);\n const mouseXRef = useRef(window.innerWidth / 2);\n\n const totalItems = 28;\n const defaultItems = Array.from({ length: totalItems }, (_, index) => `Item ${index + 1}`);\n const combinedItems = items.length > 0 ? items.slice(0, totalItems) : defaultItems;\n\n useEffect(() => {\n gsap.ticker.lagSmoothing(0);\n\n const handleMouseMove = (e: MouseEvent): void => {\n mouseXRef.current = e.clientX;\n };\n\n const updateMotion = (): void => {\n const maxMoveAmount = 300;\n const baseDuration = 0.8;\n const inertiaFactors = [0.6, 0.4, 0.3, 0.2];\n\n rowRefs.current.forEach((row, index) => {\n if (row) {\n const direction = index % 2 === 0 ? 1 : -1;\n const moveAmount = ((mouseXRef.current / window.innerWidth) * maxMoveAmount - maxMoveAmount / 2) * direction;\n\n gsap.to(row, {\n x: moveAmount,\n duration: baseDuration + inertiaFactors[index % inertiaFactors.length],\n ease: 'power3.out',\n overwrite: 'auto'\n });\n }\n });\n };\n\n const removeAnimationLoop = gsap.ticker.add(updateMotion);\n window.addEventListener('mousemove', handleMouseMove);\n\n return () => {\n window.removeEventListener('mousemove', handleMouseMove);\n removeAnimationLoop();\n };\n }, []);\n\n return (\n
\n \n
\n {Array.from({ length: 4 }, (_, rowIndex) => (\n {\n rowRefs.current[rowIndex] = el;\n }}\n >\n {Array.from({ length: 7 }, (_, itemIndex) => {\n const content = combinedItems[rowIndex * 7 + itemIndex];\n return (\n
\n
\n {typeof content === 'string' && content.startsWith('http') ? (\n
\n ) : (\n
{content}
\n )}\n
\n
\n );\n })}\n
\n ))}\n
\n
\n \n
\n );\n};\n\nexport default GridMotion;\n" + "path": "GridMotion.tsx", + "content": "import { useEffect, useRef, type FC, type ReactNode } from 'react';\nimport { gsap } from 'gsap';\nimport './GridMotion.css';\n\ninterface GridMotionProps {\n items?: (string | ReactNode)[];\n gradientColor?: string;\n}\n\nconst GridMotion: FC = ({ items = [], gradientColor = 'black' }) => {\n const gridRef = useRef(null);\n const rowRefs = useRef<(HTMLDivElement | null)[]>([]);\n const mouseXRef = useRef(window.innerWidth / 2);\n\n const totalItems = 28;\n const defaultItems = Array.from({ length: totalItems }, (_, index) => `Item ${index + 1}`);\n const combinedItems = items.length > 0 ? items.slice(0, totalItems) : defaultItems;\n\n useEffect(() => {\n gsap.ticker.lagSmoothing(0);\n\n const handleMouseMove = (e: MouseEvent): void => {\n mouseXRef.current = e.clientX;\n };\n\n const updateMotion = (): void => {\n const maxMoveAmount = 300;\n const baseDuration = 0.8;\n const inertiaFactors = [0.6, 0.4, 0.3, 0.2];\n\n rowRefs.current.forEach((row, index) => {\n if (row) {\n const direction = index % 2 === 0 ? 1 : -1;\n const moveAmount = ((mouseXRef.current / window.innerWidth) * maxMoveAmount - maxMoveAmount / 2) * direction;\n\n gsap.to(row, {\n x: moveAmount,\n duration: baseDuration + inertiaFactors[index % inertiaFactors.length],\n ease: 'power3.out',\n overwrite: 'auto'\n });\n }\n });\n };\n\n const removeAnimationLoop = gsap.ticker.add(updateMotion);\n window.addEventListener('mousemove', handleMouseMove);\n\n return () => {\n window.removeEventListener('mousemove', handleMouseMove);\n removeAnimationLoop();\n };\n }, []);\n\n return (\n
\n \n
\n {Array.from({ length: 4 }, (_, rowIndex) => (\n {\n rowRefs.current[rowIndex] = el;\n }}\n >\n {Array.from({ length: 7 }, (_, itemIndex) => {\n const content = combinedItems[rowIndex * 7 + itemIndex];\n return (\n
\n
\n {typeof content === 'string' && content.startsWith('http') ? (\n
\n ) : (\n
{content}
\n )}\n
\n
\n );\n })}\n
\n ))}\n
\n
\n \n
\n );\n};\n\nexport default GridMotion;\n" } ], "registryDependencies": [], diff --git a/public/r/GridMotion-TS-TW.json b/public/r/GridMotion-TS-TW.json index 76d7afee..2c8e1ec7 100644 --- a/public/r/GridMotion-TS-TW.json +++ b/public/r/GridMotion-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "GridMotion/GridMotion.tsx", - "content": "import { useEffect, useRef, FC, ReactNode } from 'react';\nimport { gsap } from 'gsap';\n\ninterface GridMotionProps {\n items?: (string | ReactNode)[];\n gradientColor?: string;\n}\n\nconst GridMotion: FC = ({ items = [], gradientColor = 'black' }) => {\n const gridRef = useRef(null);\n const rowRefs = useRef<(HTMLDivElement | null)[]>([]);\n const mouseXRef = useRef(window.innerWidth / 2);\n\n const totalItems = 28;\n const defaultItems = Array.from({ length: totalItems }, (_, index) => `Item ${index + 1}`);\n const combinedItems = items.length > 0 ? items.slice(0, totalItems) : defaultItems;\n\n useEffect(() => {\n gsap.ticker.lagSmoothing(0);\n\n const handleMouseMove = (e: MouseEvent): void => {\n mouseXRef.current = e.clientX;\n };\n\n const updateMotion = (): void => {\n const maxMoveAmount = 300;\n const baseDuration = 0.8;\n const inertiaFactors = [0.6, 0.4, 0.3, 0.2];\n\n rowRefs.current.forEach((row, index) => {\n if (row) {\n const direction = index % 2 === 0 ? 1 : -1;\n const moveAmount = ((mouseXRef.current / window.innerWidth) * maxMoveAmount - maxMoveAmount / 2) * direction;\n\n gsap.to(row, {\n x: moveAmount,\n duration: baseDuration + inertiaFactors[index % inertiaFactors.length],\n ease: 'power3.out',\n overwrite: 'auto'\n });\n }\n });\n };\n\n const removeAnimationLoop = gsap.ticker.add(updateMotion);\n window.addEventListener('mousemove', handleMouseMove);\n\n return () => {\n window.removeEventListener('mousemove', handleMouseMove);\n removeAnimationLoop();\n };\n }, []);\n\n return (\n
\n \n
\n
\n {Array.from({ length: 4 }, (_, rowIndex) => (\n {\n if (el) rowRefs.current[rowIndex] = el;\n }}\n >\n {Array.from({ length: 7 }, (_, itemIndex) => {\n const content = combinedItems[rowIndex * 7 + itemIndex];\n return (\n
\n
\n {typeof content === 'string' && content.startsWith('http') ? (\n
\n ) : (\n
{content}
\n )}\n
\n
\n );\n })}\n
\n ))}\n
\n
\n \n
\n );\n};\n\nexport default GridMotion;\n" + "content": "import { useEffect, useRef, type FC, type ReactNode } from 'react';\nimport { gsap } from 'gsap';\n\ninterface GridMotionProps {\n items?: (string | ReactNode)[];\n gradientColor?: string;\n}\n\nconst GridMotion: FC = ({ items = [], gradientColor = 'black' }) => {\n const gridRef = useRef(null);\n const rowRefs = useRef<(HTMLDivElement | null)[]>([]);\n const mouseXRef = useRef(window.innerWidth / 2);\n\n const totalItems = 28;\n const defaultItems = Array.from({ length: totalItems }, (_, index) => `Item ${index + 1}`);\n const combinedItems = items.length > 0 ? items.slice(0, totalItems) : defaultItems;\n\n useEffect(() => {\n gsap.ticker.lagSmoothing(0);\n\n const handleMouseMove = (e: MouseEvent): void => {\n mouseXRef.current = e.clientX;\n };\n\n const updateMotion = (): void => {\n const maxMoveAmount = 300;\n const baseDuration = 0.8;\n const inertiaFactors = [0.6, 0.4, 0.3, 0.2];\n\n rowRefs.current.forEach((row, index) => {\n if (row) {\n const direction = index % 2 === 0 ? 1 : -1;\n const moveAmount = ((mouseXRef.current / window.innerWidth) * maxMoveAmount - maxMoveAmount / 2) * direction;\n\n gsap.to(row, {\n x: moveAmount,\n duration: baseDuration + inertiaFactors[index % inertiaFactors.length],\n ease: 'power3.out',\n overwrite: 'auto'\n });\n }\n });\n };\n\n const removeAnimationLoop = gsap.ticker.add(updateMotion);\n window.addEventListener('mousemove', handleMouseMove);\n\n return () => {\n window.removeEventListener('mousemove', handleMouseMove);\n removeAnimationLoop();\n };\n }, []);\n\n return (\n
\n \n
\n
\n {Array.from({ length: 4 }, (_, rowIndex) => (\n {\n if (el) rowRefs.current[rowIndex] = el;\n }}\n >\n {Array.from({ length: 7 }, (_, itemIndex) => {\n const content = combinedItems[rowIndex * 7 + itemIndex];\n return (\n
\n
\n {typeof content === 'string' && content.startsWith('http') ? (\n
\n ) : (\n
{content}
\n )}\n
\n
\n );\n })}\n
\n ))}\n
\n
\n \n
\n );\n};\n\nexport default GridMotion;\n" } ], "registryDependencies": [], diff --git a/public/r/GridScan-JS-CSS.json b/public/r/GridScan-JS-CSS.json index 25d79484..2857729d 100644 --- a/public/r/GridScan-JS-CSS.json +++ b/public/r/GridScan-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GridScan/GridScan.css", + "type": "registry:file", + "path": "GridScan.css", + "target": "@components/GridScan.css", "content": ".gridscan {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n\n.gridscan__preview {\n position: absolute;\n right: 12px;\n bottom: 12px;\n width: 220px;\n height: 132px;\n border-radius: 8px;\n overflow: hidden;\n border: 1px solid rgba(255, 255, 255, 0.25);\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);\n background: #000;\n color: #fff;\n font:\n 12px/1.2 system-ui,\n -apple-system,\n Segoe UI,\n Roboto,\n sans-serif;\n pointer-events: none;\n}\n\n.gridscan__video {\n width: 100%;\n height: 100%;\n object-fit: cover;\n transform: scaleX(-1);\n}\n\n.gridscan__badge {\n position: absolute;\n left: 8px;\n top: 8px;\n padding: 2px 6px;\n background: rgba(0, 0, 0, 0.5);\n border-radius: 6px;\n backdrop-filter: blur(4px);\n}\n" }, { "type": "registry:component", - "path": "GridScan/GridScan.jsx", + "path": "GridScan.jsx", "content": "import * as faceapi from 'face-api.js';\nimport { BloomEffect, ChromaticAberrationEffect, EffectComposer, EffectPass, RenderPass } from 'postprocessing';\nimport { useEffect, useRef, useState } from 'react';\nimport * as THREE from 'three';\nimport './GridScan.css';\n\nconst vert = `\nvarying vec2 vUv;\nvoid main(){\n vUv = uv;\n gl_Position = vec4(position.xy, 0.0, 1.0);\n}\n`;\n\nconst frag = `\nprecision highp float;\nuniform vec3 iResolution;\nuniform float iTime;\nuniform vec2 uSkew;\nuniform float uTilt;\nuniform float uYaw;\nuniform float uLineThickness;\nuniform vec3 uLinesColor;\nuniform vec3 uScanColor;\nuniform float uGridScale;\nuniform float uLineStyle;\nuniform float uLineJitter;\nuniform float uScanOpacity;\nuniform float uScanDirection;\nuniform float uNoise;\nuniform float uBloomOpacity;\nuniform float uScanGlow;\nuniform float uScanSoftness;\nuniform float uPhaseTaper;\nuniform float uScanDuration;\nuniform float uScanDelay;\nvarying vec2 vUv;\n\nuniform float uScanStarts[8];\nuniform float uScanCount;\n\nconst int MAX_SCANS = 8;\n\nfloat smoother01(float a, float b, float x){\n float t = clamp((x - a) / max(1e-5, (b - a)), 0.0, 1.0);\n return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord)\n{\n vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n\n vec3 ro = vec3(0.0);\n vec3 rd = normalize(vec3(p, 2.0));\n\n float cR = cos(uTilt), sR = sin(uTilt);\n rd.xy = mat2(cR, -sR, sR, cR) * rd.xy;\n\n float cY = cos(uYaw), sY = sin(uYaw);\n rd.xz = mat2(cY, -sY, sY, cY) * rd.xz;\n\n vec2 skew = clamp(uSkew, vec2(-0.7), vec2(0.7));\n rd.xy += skew * rd.z;\n\n vec3 color = vec3(0.0);\n float minT = 1e20;\n float gridScale = max(1e-5, uGridScale);\n float fadeStrength = 2.0;\n vec2 gridUV = vec2(0.0);\n\n float hitIsY = 1.0;\n for (int i = 0; i < 4; i++)\n {\n float isY = float(i < 2);\n float pos = mix(-0.2, 0.2, float(i)) * isY + mix(-0.5, 0.5, float(i - 2)) * (1.0 - isY);\n float num = pos - (isY * ro.y + (1.0 - isY) * ro.x);\n float den = isY * rd.y + (1.0 - isY) * rd.x;\n float t = num / den;\n vec3 h = ro + rd * t;\n\n float depthBoost = smoothstep(0.0, 3.0, h.z);\n h.xy += skew * 0.15 * depthBoost;\n\n bool use = t > 0.0 && t < minT;\n gridUV = use ? mix(h.zy, h.xz, isY) / gridScale : gridUV;\n minT = use ? t : minT;\n hitIsY = use ? isY : hitIsY;\n }\n\n vec3 hit = ro + rd * minT;\n float dist = length(hit - ro);\n\n float jitterAmt = clamp(uLineJitter, 0.0, 1.0);\n if (jitterAmt > 0.0) {\n vec2 j = vec2(\n sin(gridUV.y * 2.7 + iTime * 1.8),\n cos(gridUV.x * 2.3 - iTime * 1.6)\n ) * (0.15 * jitterAmt);\n gridUV += j;\n }\n float fx = fract(gridUV.x);\n float fy = fract(gridUV.y);\n float ax = min(fx, 1.0 - fx);\n float ay = min(fy, 1.0 - fy);\n float wx = fwidth(gridUV.x);\n float wy = fwidth(gridUV.y);\n float halfPx = max(0.0, uLineThickness) * 0.5;\n\n float tx = halfPx * wx;\n float ty = halfPx * wy;\n\n float aax = wx;\n float aay = wy;\n\n float lineX = 1.0 - smoothstep(tx, tx + aax, ax);\n float lineY = 1.0 - smoothstep(ty, ty + aay, ay);\n if (uLineStyle > 0.5) {\n float dashRepeat = 4.0;\n float dashDuty = 0.5;\n float vy = fract(gridUV.y * dashRepeat);\n float vx = fract(gridUV.x * dashRepeat);\n float dashMaskY = step(vy, dashDuty);\n float dashMaskX = step(vx, dashDuty);\n if (uLineStyle < 1.5) {\n lineX *= dashMaskY;\n lineY *= dashMaskX;\n } else {\n float dotRepeat = 6.0;\n float dotWidth = 0.18;\n float cy = abs(fract(gridUV.y * dotRepeat) - 0.5);\n float cx = abs(fract(gridUV.x * dotRepeat) - 0.5);\n float dotMaskY = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.y * dotRepeat), cy);\n float dotMaskX = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.x * dotRepeat), cx);\n lineX *= dotMaskY;\n lineY *= dotMaskX;\n }\n }\n float primaryMask = max(lineX, lineY);\n\n vec2 gridUV2 = (hitIsY > 0.5 ? hit.xz : hit.zy) / gridScale;\n if (jitterAmt > 0.0) {\n vec2 j2 = vec2(\n cos(gridUV2.y * 2.1 - iTime * 1.4),\n sin(gridUV2.x * 2.5 + iTime * 1.7)\n ) * (0.15 * jitterAmt);\n gridUV2 += j2;\n }\n float fx2 = fract(gridUV2.x);\n float fy2 = fract(gridUV2.y);\n float ax2 = min(fx2, 1.0 - fx2);\n float ay2 = min(fy2, 1.0 - fy2);\n float wx2 = fwidth(gridUV2.x);\n float wy2 = fwidth(gridUV2.y);\n float tx2 = halfPx * wx2;\n float ty2 = halfPx * wy2;\n float aax2 = wx2;\n float aay2 = wy2;\n float lineX2 = 1.0 - smoothstep(tx2, tx2 + aax2, ax2);\n float lineY2 = 1.0 - smoothstep(ty2, ty2 + aay2, ay2);\n if (uLineStyle > 0.5) {\n float dashRepeat2 = 4.0;\n float dashDuty2 = 0.5;\n float vy2m = fract(gridUV2.y * dashRepeat2);\n float vx2m = fract(gridUV2.x * dashRepeat2);\n float dashMaskY2 = step(vy2m, dashDuty2);\n float dashMaskX2 = step(vx2m, dashDuty2);\n if (uLineStyle < 1.5) {\n lineX2 *= dashMaskY2;\n lineY2 *= dashMaskX2;\n } else {\n float dotRepeat2 = 6.0;\n float dotWidth2 = 0.18;\n float cy2 = abs(fract(gridUV2.y * dotRepeat2) - 0.5);\n float cx2 = abs(fract(gridUV2.x * dotRepeat2) - 0.5);\n float dotMaskY2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.y * dotRepeat2), cy2);\n float dotMaskX2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.x * dotRepeat2), cx2);\n lineX2 *= dotMaskY2;\n lineY2 *= dotMaskX2;\n }\n }\n float altMask = max(lineX2, lineY2);\n\n float edgeDistX = min(abs(hit.x - (-0.5)), abs(hit.x - 0.5));\n float edgeDistY = min(abs(hit.y - (-0.2)), abs(hit.y - 0.2));\n float edgeDist = mix(edgeDistY, edgeDistX, hitIsY);\n float edgeGate = 1.0 - smoothstep(gridScale * 0.5, gridScale * 2.0, edgeDist);\n altMask *= edgeGate;\n\n float lineMask = max(primaryMask, altMask);\n\n float fade = exp(-dist * fadeStrength);\n\n float dur = max(0.05, uScanDuration);\n float del = max(0.0, uScanDelay);\n float scanZMax = 2.0;\n float widthScale = max(0.1, uScanGlow);\n float sigma = max(0.001, 0.18 * widthScale * uScanSoftness);\n float sigmaA = sigma * 2.0;\n\n float combinedPulse = 0.0;\n float combinedAura = 0.0;\n\n float cycle = dur + del;\n float tCycle = mod(iTime, cycle);\n float scanPhase = clamp((tCycle - del) / dur, 0.0, 1.0);\n float phase = scanPhase;\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phase = 1.0 - phase;\n } else if (uScanDirection > 1.5) {\n float t2 = mod(max(0.0, iTime - del), 2.0 * dur);\n phase = (t2 < dur) ? (t2 / dur) : (1.0 - (t2 - dur) / dur);\n }\n float scanZ = phase * scanZMax;\n float dz = abs(hit.z - scanZ);\n float lineBand = exp(-0.5 * (dz * dz) / (sigma * sigma));\n float taper = clamp(uPhaseTaper, 0.0, 0.49);\n float headW = taper;\n float tailW = taper;\n float headFade = smoother01(0.0, headW, phase);\n float tailFade = 1.0 - smoother01(1.0 - tailW, 1.0, phase);\n float phaseWindow = headFade * tailFade;\n float pulseBase = lineBand * phaseWindow;\n combinedPulse += pulseBase * clamp(uScanOpacity, 0.0, 1.0);\n float auraBand = exp(-0.5 * (dz * dz) / (sigmaA * sigmaA));\n combinedAura += (auraBand * 0.25) * phaseWindow * clamp(uScanOpacity, 0.0, 1.0);\n\n for (int i = 0; i < MAX_SCANS; i++) {\n if (float(i) >= uScanCount) break;\n float tActiveI = iTime - uScanStarts[i];\n float phaseI = clamp(tActiveI / dur, 0.0, 1.0);\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phaseI = 1.0 - phaseI;\n } else if (uScanDirection > 1.5) {\n phaseI = (phaseI < 0.5) ? (phaseI * 2.0) : (1.0 - (phaseI - 0.5) * 2.0);\n }\n float scanZI = phaseI * scanZMax;\n float dzI = abs(hit.z - scanZI);\n float lineBandI = exp(-0.5 * (dzI * dzI) / (sigma * sigma));\n float headFadeI = smoother01(0.0, headW, phaseI);\n float tailFadeI = 1.0 - smoother01(1.0 - tailW, 1.0, phaseI);\n float phaseWindowI = headFadeI * tailFadeI;\n combinedPulse += lineBandI * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n float auraBandI = exp(-0.5 * (dzI * dzI) / (sigmaA * sigmaA));\n combinedAura += (auraBandI * 0.25) * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n }\n\n float lineVis = lineMask;\n vec3 gridCol = uLinesColor * lineVis * fade;\n vec3 scanCol = uScanColor * combinedPulse;\n vec3 scanAura = uScanColor * combinedAura;\n\n color = gridCol + scanCol + scanAura;\n\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(iTime * 123.4), vec2(12.9898,78.233))) * 43758.5453123);\n color += (n - 0.5) * uNoise;\n color = clamp(color, 0.0, 1.0);\n float alpha = clamp(max(lineVis, combinedPulse), 0.0, 1.0);\n float gx = 1.0 - smoothstep(tx * 2.0, tx * 2.0 + aax * 2.0, ax);\n float gy = 1.0 - smoothstep(ty * 2.0, ty * 2.0 + aay * 2.0, ay);\n float halo = max(gx, gy) * fade;\n alpha = max(alpha, halo * clamp(uBloomOpacity, 0.0, 1.0));\n fragColor = vec4(color, alpha);\n}\n\nvoid main(){\n vec4 c;\n mainImage(c, vUv * iResolution.xy);\n gl_FragColor = c;\n}\n`;\n\nexport const GridScan = ({\n enableWebcam = false,\n showPreview = false,\n modelsPath = 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@0.22.2/weights',\n sensitivity = 0.55,\n lineThickness = 1,\n linesColor = '#2F293A',\n scanColor = '#FF9FFC',\n scanOpacity = 0.4,\n gridScale = 0.1,\n lineStyle = 'solid',\n lineJitter = 0.1,\n scanDirection = 'pingpong',\n enablePost = true,\n bloomIntensity = 0,\n bloomThreshold = 0,\n bloomSmoothing = 0,\n chromaticAberration = 0.002,\n noiseIntensity = 0.01,\n scanGlow = 0.5,\n scanSoftness = 2,\n scanPhaseTaper = 0.9,\n scanDuration = 2.0,\n scanDelay = 2.0,\n enableGyro = false,\n scanOnClick = false,\n snapBackDelay = 250,\n className,\n style\n}) => {\n const containerRef = useRef(null);\n const videoRef = useRef(null);\n\n const rendererRef = useRef(null);\n const materialRef = useRef(null);\n const composerRef = useRef(null);\n const bloomRef = useRef(null);\n const chromaRef = useRef(null);\n const rafRef = useRef(null);\n\n const [modelsReady, setModelsReady] = useState(false);\n const [uiFaceActive, setUiFaceActive] = useState(false);\n\n const lookTarget = useRef(new THREE.Vector2(0, 0));\n const tiltTarget = useRef(0);\n const yawTarget = useRef(0);\n\n const lookCurrent = useRef(new THREE.Vector2(0, 0));\n const lookVel = useRef(new THREE.Vector2(0, 0));\n const tiltCurrent = useRef(0);\n const tiltVel = useRef(0);\n const yawCurrent = useRef(0);\n const yawVel = useRef(0);\n\n const MAX_SCANS = 8;\n const scanStartsRef = useRef([]);\n\n const pushScan = t => {\n const arr = scanStartsRef.current.slice();\n if (arr.length >= MAX_SCANS) arr.shift();\n arr.push(t);\n scanStartsRef.current = arr;\n if (materialRef.current) {\n const u = materialRef.current.uniforms;\n const buf = new Array(MAX_SCANS).fill(0);\n for (let i = 0; i < arr.length && i < MAX_SCANS; i++) buf[i] = arr[i];\n u.uScanStarts.value = buf;\n u.uScanCount.value = arr.length;\n }\n };\n\n const bufX = useRef([]);\n const bufY = useRef([]);\n const bufT = useRef([]);\n const bufYaw = useRef([]);\n\n const s = THREE.MathUtils.clamp(sensitivity, 0, 1);\n const skewScale = THREE.MathUtils.lerp(0.06, 0.2, s);\n const tiltScale = THREE.MathUtils.lerp(0.12, 0.3, s);\n const yawScale = THREE.MathUtils.lerp(0.1, 0.28, s);\n const depthResponse = THREE.MathUtils.lerp(0.25, 0.45, s);\n const smoothTime = THREE.MathUtils.lerp(0.45, 0.12, s);\n const maxSpeed = Infinity;\n\n const yBoost = THREE.MathUtils.lerp(1.2, 1.6, s);\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n let leaveTimer = null;\n const onMove = e => {\n if (uiFaceActive) return;\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n const rect = el.getBoundingClientRect();\n const nx = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n const ny = -(((e.clientY - rect.top) / rect.height) * 2 - 1);\n lookTarget.current.set(nx, ny);\n };\n const onClick = async () => {\n const nowSec = performance.now() / 1000;\n if (scanOnClick) pushScan(nowSec);\n if (\n enableGyro &&\n typeof window !== 'undefined' &&\n window.DeviceOrientationEvent &&\n DeviceOrientationEvent.requestPermission\n ) {\n try {\n await DeviceOrientationEvent.requestPermission();\n } catch {\n // noop\n }\n }\n };\n const onEnter = () => {\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n };\n const onLeave = () => {\n if (uiFaceActive) return;\n if (leaveTimer) clearTimeout(leaveTimer);\n leaveTimer = window.setTimeout(\n () => {\n lookTarget.current.set(0, 0);\n tiltTarget.current = 0;\n yawTarget.current = 0;\n },\n Math.max(0, snapBackDelay || 0)\n );\n };\n el.addEventListener('mousemove', onMove);\n el.addEventListener('mouseenter', onEnter);\n if (scanOnClick) el.addEventListener('click', onClick);\n el.addEventListener('mouseleave', onLeave);\n return () => {\n el.removeEventListener('mousemove', onMove);\n el.removeEventListener('mouseenter', onEnter);\n el.removeEventListener('mouseleave', onLeave);\n if (scanOnClick) el.removeEventListener('click', onClick);\n if (leaveTimer) clearTimeout(leaveTimer);\n };\n }, [uiFaceActive, snapBackDelay, scanOnClick, enableGyro]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });\n rendererRef.current = renderer;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setSize(container.clientWidth, container.clientHeight);\n renderer.outputColorSpace = THREE.SRGBColorSpace;\n renderer.toneMapping = THREE.NoToneMapping;\n renderer.autoClear = false;\n renderer.setClearColor(0x000000, 0);\n container.appendChild(renderer.domElement);\n\n const uniforms = {\n iResolution: {\n value: new THREE.Vector3(container.clientWidth, container.clientHeight, renderer.getPixelRatio())\n },\n iTime: { value: 0 },\n uSkew: { value: new THREE.Vector2(0, 0) },\n uTilt: { value: 0 },\n uYaw: { value: 0 },\n uLineThickness: { value: lineThickness },\n uLinesColor: { value: srgbColor(linesColor) },\n uScanColor: { value: srgbColor(scanColor) },\n uGridScale: { value: gridScale },\n uLineStyle: { value: lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0 },\n uLineJitter: { value: Math.max(0, Math.min(1, lineJitter || 0)) },\n uScanOpacity: { value: scanOpacity },\n uNoise: { value: noiseIntensity },\n uBloomOpacity: { value: bloomIntensity },\n uScanGlow: { value: scanGlow },\n uScanSoftness: { value: scanSoftness },\n uPhaseTaper: { value: scanPhaseTaper },\n uScanDuration: { value: scanDuration },\n uScanDelay: { value: scanDelay },\n uScanDirection: { value: scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0 },\n uScanStarts: { value: new Array(MAX_SCANS).fill(0) },\n uScanCount: { value: 0 }\n };\n\n const material = new THREE.ShaderMaterial({\n uniforms,\n vertexShader: vert,\n fragmentShader: frag,\n transparent: true,\n depthWrite: false,\n depthTest: false\n });\n materialRef.current = material;\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);\n scene.add(quad);\n\n let composer = null;\n if (enablePost) {\n composer = new EffectComposer(renderer);\n composerRef.current = composer;\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloom = new BloomEffect({\n intensity: 1.0,\n luminanceThreshold: bloomThreshold,\n luminanceSmoothing: bloomSmoothing\n });\n bloom.blendMode.opacity.value = Math.max(0, bloomIntensity);\n bloomRef.current = bloom;\n\n const chroma = new ChromaticAberrationEffect({\n offset: new THREE.Vector2(chromaticAberration, chromaticAberration),\n radialModulation: true,\n modulationOffset: 0.0\n });\n chromaRef.current = chroma;\n\n const effectPass = new EffectPass(camera, bloom, chroma);\n effectPass.renderToScreen = true;\n composer.addPass(effectPass);\n }\n\n const onResize = () => {\n renderer.setSize(container.clientWidth, container.clientHeight);\n material.uniforms.iResolution.value.set(container.clientWidth, container.clientHeight, renderer.getPixelRatio());\n if (composerRef.current) composerRef.current.setSize(container.clientWidth, container.clientHeight);\n };\n window.addEventListener('resize', onResize);\n\n let last = performance.now();\n const tick = () => {\n const now = performance.now();\n const dt = Math.max(0, Math.min(0.1, (now - last) / 1000));\n last = now;\n\n lookCurrent.current.copy(\n smoothDampVec2(lookCurrent.current, lookTarget.current, lookVel.current, smoothTime, maxSpeed, dt)\n );\n\n const tiltSm = smoothDampFloat(\n tiltCurrent.current,\n tiltTarget.current,\n { v: tiltVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n tiltCurrent.current = tiltSm.value;\n tiltVel.current = tiltSm.v;\n\n const yawSm = smoothDampFloat(\n yawCurrent.current,\n yawTarget.current,\n { v: yawVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n yawCurrent.current = yawSm.value;\n yawVel.current = yawSm.v;\n\n const skew = new THREE.Vector2(lookCurrent.current.x * skewScale, -lookCurrent.current.y * yBoost * skewScale);\n material.uniforms.uSkew.value.set(skew.x, skew.y);\n material.uniforms.uTilt.value = tiltCurrent.current * tiltScale;\n material.uniforms.uYaw.value = THREE.MathUtils.clamp(yawCurrent.current * yawScale, -0.6, 0.6);\n\n material.uniforms.iTime.value = now / 1000;\n renderer.clear(true, true, true);\n if (composerRef.current) {\n composerRef.current.render(dt);\n } else {\n renderer.render(scene, camera);\n }\n rafRef.current = requestAnimationFrame(tick);\n };\n rafRef.current = requestAnimationFrame(tick);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n window.removeEventListener('resize', onResize);\n material.dispose();\n quad.geometry.dispose();\n\n if (composerRef.current) {\n composerRef.current.dispose();\n composerRef.current = null;\n }\n renderer.dispose();\n renderer.forceContextLoss();\n container.removeChild(renderer.domElement);\n };\n }, [\n sensitivity,\n lineThickness,\n linesColor,\n scanColor,\n scanOpacity,\n gridScale,\n lineStyle,\n lineJitter,\n scanDirection,\n enablePost,\n noiseIntensity,\n bloomIntensity,\n scanGlow,\n scanSoftness,\n scanPhaseTaper,\n scanDuration,\n scanDelay,\n bloomThreshold,\n bloomSmoothing,\n chromaticAberration,\n smoothTime,\n maxSpeed,\n skewScale,\n yBoost,\n tiltScale,\n yawScale\n ]);\n\n useEffect(() => {\n const m = materialRef.current;\n if (m) {\n const u = m.uniforms;\n u.uLineThickness.value = lineThickness;\n u.uLinesColor.value.copy(srgbColor(linesColor));\n u.uScanColor.value.copy(srgbColor(scanColor));\n u.uGridScale.value = gridScale;\n u.uLineStyle.value = lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0;\n u.uLineJitter.value = Math.max(0, Math.min(1, lineJitter || 0));\n u.uBloomOpacity.value = Math.max(0, bloomIntensity);\n u.uNoise.value = Math.max(0, noiseIntensity);\n u.uScanGlow.value = scanGlow;\n u.uScanOpacity.value = Math.max(0, Math.min(1, scanOpacity));\n u.uScanDirection.value = scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0;\n u.uScanSoftness.value = scanSoftness;\n u.uPhaseTaper.value = scanPhaseTaper;\n u.uScanDuration.value = Math.max(0.05, scanDuration);\n u.uScanDelay.value = Math.max(0.0, scanDelay);\n }\n if (bloomRef.current) {\n bloomRef.current.blendMode.opacity.value = Math.max(0, bloomIntensity);\n bloomRef.current.luminanceMaterial.threshold = bloomThreshold;\n bloomRef.current.luminanceMaterial.smoothing = bloomSmoothing;\n }\n if (chromaRef.current) {\n chromaRef.current.offset.set(chromaticAberration, chromaticAberration);\n }\n }, [\n lineThickness,\n linesColor,\n scanColor,\n gridScale,\n lineStyle,\n lineJitter,\n bloomIntensity,\n bloomThreshold,\n bloomSmoothing,\n chromaticAberration,\n noiseIntensity,\n scanGlow,\n scanOpacity,\n scanDirection,\n scanSoftness,\n scanPhaseTaper,\n scanDuration,\n scanDelay\n ]);\n\n useEffect(() => {\n if (!enableGyro) return;\n const handler = e => {\n if (uiFaceActive) return;\n const gamma = e.gamma ?? 0;\n const beta = e.beta ?? 0;\n const nx = THREE.MathUtils.clamp(gamma / 45, -1, 1);\n const ny = THREE.MathUtils.clamp(-beta / 30, -1, 1);\n lookTarget.current.set(nx, ny);\n tiltTarget.current = THREE.MathUtils.degToRad(gamma) * 0.4;\n };\n window.addEventListener('deviceorientation', handler);\n return () => {\n window.removeEventListener('deviceorientation', handler);\n };\n }, [enableGyro, uiFaceActive]);\n\n useEffect(() => {\n let canceled = false;\n const load = async () => {\n try {\n await Promise.all([\n faceapi.nets.tinyFaceDetector.loadFromUri(modelsPath),\n faceapi.nets.faceLandmark68TinyNet.loadFromUri(modelsPath)\n ]);\n if (!canceled) setModelsReady(true);\n } catch {\n if (!canceled) setModelsReady(false);\n }\n };\n load();\n return () => {\n canceled = true;\n };\n }, [modelsPath]);\n\n useEffect(() => {\n let stop = false;\n let lastDetect = 0;\n const video = videoRef.current;\n\n const start = async () => {\n if (!enableWebcam || !modelsReady) return;\n if (!video) return;\n\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 } },\n audio: false\n });\n video.srcObject = stream;\n await video.play();\n } catch {\n return;\n }\n\n const opts = new faceapi.TinyFaceDetectorOptions({ inputSize: 320, scoreThreshold: 0.5 });\n\n const detect = async ts => {\n if (stop) return;\n\n if (ts - lastDetect >= 33) {\n lastDetect = ts;\n try {\n const res = await faceapi.detectSingleFace(video, opts).withFaceLandmarks(true);\n if (res && res.detection) {\n const det = res.detection;\n const box = det.box;\n const vw = video.videoWidth || 1;\n const vh = video.videoHeight || 1;\n\n const cx = box.x + box.width * 0.5;\n const cy = box.y + box.height * 0.5;\n const nx = (cx / vw) * 2 - 1;\n const ny = (cy / vh) * 2 - 1;\n medianPush(bufX.current, nx, 5);\n medianPush(bufY.current, ny, 5);\n const nxm = median(bufX.current);\n const nym = median(bufY.current);\n\n const look = new THREE.Vector2(Math.tanh(nxm), Math.tanh(nym));\n\n const faceSize = Math.min(1, Math.hypot(box.width / vw, box.height / vh));\n const depthScale = 1 + depthResponse * (faceSize - 0.25);\n lookTarget.current.copy(look.multiplyScalar(depthScale));\n\n const leftEye = res.landmarks.getLeftEye();\n const rightEye = res.landmarks.getRightEye();\n const lc = centroid(leftEye);\n const rc = centroid(rightEye);\n const tilt = Math.atan2(rc.y - lc.y, rc.x - lc.x);\n medianPush(bufT.current, tilt, 5);\n tiltTarget.current = median(bufT.current);\n\n const nose = res.landmarks.getNose();\n const tip = nose[nose.length - 1] || nose[Math.floor(nose.length / 2)];\n const jaw = res.landmarks.getJawOutline();\n const leftCheek = jaw[3] || jaw[2];\n const rightCheek = jaw[13] || jaw[14];\n const dL = dist2(tip, leftCheek);\n const dR = dist2(tip, rightCheek);\n const eyeDist = Math.hypot(rc.x - lc.x, rc.y - lc.y) + 1e-6;\n let yawSignal = THREE.MathUtils.clamp((dR - dL) / (eyeDist * 1.6), -1, 1);\n yawSignal = Math.tanh(yawSignal);\n medianPush(bufYaw.current, yawSignal, 5);\n yawTarget.current = median(bufYaw.current);\n\n setUiFaceActive(true);\n } else {\n setUiFaceActive(false);\n }\n } catch {\n setUiFaceActive(false);\n }\n }\n\n if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {\n video.requestVideoFrameCallback(() => detect(performance.now()));\n } else {\n requestAnimationFrame(detect);\n }\n };\n\n requestAnimationFrame(detect);\n };\n\n start();\n\n return () => {\n stop = true;\n if (video) {\n const stream = video.srcObject;\n if (stream) stream.getTracks().forEach(t => t.stop());\n video.pause();\n video.srcObject = null;\n }\n };\n }, [enableWebcam, modelsReady, depthResponse]);\n\n return (\n
\n {showPreview && (\n
\n
\n )}\n
\n );\n};\n\nfunction srgbColor(hex) {\n const c = new THREE.Color(hex);\n return c.convertSRGBToLinear();\n}\n\nfunction smoothDampVec2(current, target, currentVelocity, smoothTime, maxSpeed, deltaTime) {\n const out = current.clone();\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current.clone().sub(target);\n const originalTo = target.clone();\n\n const maxChange = maxSpeed * smoothTime;\n if (change.length() > maxChange) change.setLength(maxChange);\n\n target = current.clone().sub(change);\n const temp = currentVelocity.clone().addScaledVector(change, omega).multiplyScalar(deltaTime);\n currentVelocity.sub(temp.clone().multiplyScalar(omega));\n currentVelocity.multiplyScalar(exp);\n\n out.copy(target.clone().add(change.add(temp).multiplyScalar(exp)));\n\n const origMinusCurrent = originalTo.clone().sub(current);\n const outMinusOrig = out.clone().sub(originalTo);\n if (origMinusCurrent.dot(outMinusOrig) > 0) {\n out.copy(originalTo);\n currentVelocity.set(0, 0);\n }\n return out;\n}\n\nfunction smoothDampFloat(current, target, velRef, smoothTime, maxSpeed, deltaTime) {\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current - target;\n const originalTo = target;\n\n const maxChange = maxSpeed * smoothTime;\n change = Math.sign(change) * Math.min(Math.abs(change), maxChange);\n\n target = current - change;\n const temp = (velRef.v + omega * change) * deltaTime;\n velRef.v = (velRef.v - omega * temp) * exp;\n\n let out = target + (change + temp) * exp;\n\n const origMinusCurrent = originalTo - current;\n const outMinusOrig = out - originalTo;\n if (origMinusCurrent * outMinusOrig > 0) {\n out = originalTo;\n velRef.v = 0;\n }\n return { value: out, v: velRef.v };\n}\n\nfunction medianPush(buf, v, maxLen) {\n buf.push(v);\n if (buf.length > maxLen) buf.shift();\n}\n\nfunction median(buf) {\n if (buf.length === 0) return 0;\n const a = [...buf].sort((x, y) => x - y);\n const mid = Math.floor(a.length / 2);\n return a.length % 2 ? a[mid] : (a[mid - 1] + a[mid]) * 0.5;\n}\n\nfunction centroid(points) {\n let x = 0,\n y = 0;\n const n = points.length || 1;\n for (const p of points) {\n x += p.x;\n y += p.y;\n }\n return { x: x / n, y: y / n };\n}\n\nfunction dist2(a, b) {\n return Math.hypot(a.x - b.x, a.y - b.y);\n}\n" } ], diff --git a/public/r/GridScan-TS-CSS.json b/public/r/GridScan-TS-CSS.json index 153165bf..3de5906f 100644 --- a/public/r/GridScan-TS-CSS.json +++ b/public/r/GridScan-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "GridScan/GridScan.css", + "type": "registry:file", + "path": "GridScan.css", + "target": "@components/GridScan.css", "content": ".gridscan {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n\n.gridscan__preview {\n position: absolute;\n right: 12px;\n bottom: 12px;\n width: 220px;\n height: 132px;\n border-radius: 8px;\n overflow: hidden;\n border: 1px solid rgba(255, 255, 255, 0.25);\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);\n background: #000;\n color: #fff;\n font:\n 12px/1.2 system-ui,\n -apple-system,\n Segoe UI,\n Roboto,\n sans-serif;\n pointer-events: none;\n}\n\n.gridscan__video {\n width: 100%;\n height: 100%;\n object-fit: cover;\n transform: scaleX(-1);\n}\n\n.gridscan__badge {\n position: absolute;\n left: 8px;\n top: 8px;\n padding: 2px 6px;\n background: rgba(0, 0, 0, 0.5);\n border-radius: 6px;\n backdrop-filter: blur(4px);\n}\n" }, { "type": "registry:component", - "path": "GridScan/GridScan.tsx", + "path": "GridScan.tsx", "content": "import * as faceapi from 'face-api.js';\nimport { BloomEffect, ChromaticAberrationEffect, EffectComposer, EffectPass, RenderPass } from 'postprocessing';\nimport React, { useEffect, useRef, useState } from 'react';\nimport * as THREE from 'three';\nimport './GridScan.css';\n\ntype GridScanProps = {\n enableWebcam?: boolean;\n showPreview?: boolean;\n modelsPath?: string;\n sensitivity?: number;\n\n lineThickness?: number;\n linesColor?: string;\n\n gridScale?: number;\n lineStyle?: 'solid' | 'dashed' | 'dotted';\n lineJitter?: number;\n\n enablePost?: boolean;\n bloomIntensity?: number;\n bloomThreshold?: number;\n bloomSmoothing?: number;\n chromaticAberration?: number;\n noiseIntensity?: number;\n\n scanColor?: string;\n scanOpacity?: number;\n scanDirection?: 'forward' | 'backward' | 'pingpong';\n scanSoftness?: number;\n scanGlow?: number;\n scanPhaseTaper?: number;\n scanDuration?: number;\n scanDelay?: number;\n enableGyro?: boolean;\n scanOnClick?: boolean;\n snapBackDelay?: number;\n className?: string;\n style?: React.CSSProperties;\n};\n\nconst vert = `\nvarying vec2 vUv;\nvoid main(){\n vUv = uv;\n gl_Position = vec4(position.xy, 0.0, 1.0);\n}\n`;\n\nconst frag = `\nprecision highp float;\nuniform vec3 iResolution;\nuniform float iTime;\nuniform vec2 uSkew;\nuniform float uTilt;\nuniform float uYaw;\nuniform float uLineThickness;\nuniform vec3 uLinesColor;\nuniform vec3 uScanColor;\nuniform float uGridScale;\nuniform float uLineStyle;\nuniform float uLineJitter;\nuniform float uScanOpacity;\nuniform float uScanDirection;\nuniform float uNoise;\nuniform float uBloomOpacity;\nuniform float uScanGlow;\nuniform float uScanSoftness;\nuniform float uPhaseTaper;\nuniform float uScanDuration;\nuniform float uScanDelay;\nvarying vec2 vUv;\n\nuniform float uScanStarts[8];\nuniform float uScanCount;\n\nconst int MAX_SCANS = 8;\n\nfloat smoother01(float a, float b, float x){\n float t = clamp((x - a) / max(1e-5, (b - a)), 0.0, 1.0);\n return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord)\n{\n vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n\n vec3 ro = vec3(0.0);\n vec3 rd = normalize(vec3(p, 2.0));\n\n float cR = cos(uTilt), sR = sin(uTilt);\n rd.xy = mat2(cR, -sR, sR, cR) * rd.xy;\n\n float cY = cos(uYaw), sY = sin(uYaw);\n rd.xz = mat2(cY, -sY, sY, cY) * rd.xz;\n\n vec2 skew = clamp(uSkew, vec2(-0.7), vec2(0.7));\n rd.xy += skew * rd.z;\n\n vec3 color = vec3(0.0);\n float minT = 1e20;\n float gridScale = max(1e-5, uGridScale);\n float fadeStrength = 2.0;\n vec2 gridUV = vec2(0.0);\n\n float hitIsY = 1.0;\n for (int i = 0; i < 4; i++)\n {\n float isY = float(i < 2);\n float pos = mix(-0.2, 0.2, float(i)) * isY + mix(-0.5, 0.5, float(i - 2)) * (1.0 - isY);\n float num = pos - (isY * ro.y + (1.0 - isY) * ro.x);\n float den = isY * rd.y + (1.0 - isY) * rd.x;\n float t = num / den;\n vec3 h = ro + rd * t;\n\n float depthBoost = smoothstep(0.0, 3.0, h.z);\n h.xy += skew * 0.15 * depthBoost;\n\n bool use = t > 0.0 && t < minT;\n gridUV = use ? mix(h.zy, h.xz, isY) / gridScale : gridUV;\n minT = use ? t : minT;\n hitIsY = use ? isY : hitIsY;\n }\n\n vec3 hit = ro + rd * minT;\n float dist = length(hit - ro);\n\n float jitterAmt = clamp(uLineJitter, 0.0, 1.0);\n if (jitterAmt > 0.0) {\n vec2 j = vec2(\n sin(gridUV.y * 2.7 + iTime * 1.8),\n cos(gridUV.x * 2.3 - iTime * 1.6)\n ) * (0.15 * jitterAmt);\n gridUV += j;\n }\n float fx = fract(gridUV.x);\n float fy = fract(gridUV.y);\n float ax = min(fx, 1.0 - fx);\n float ay = min(fy, 1.0 - fy);\n float wx = fwidth(gridUV.x);\n float wy = fwidth(gridUV.y);\n float halfPx = max(0.0, uLineThickness) * 0.5;\n\n float tx = halfPx * wx;\n float ty = halfPx * wy;\n\n float aax = wx;\n float aay = wy;\n\n float lineX = 1.0 - smoothstep(tx, tx + aax, ax);\n float lineY = 1.0 - smoothstep(ty, ty + aay, ay);\n if (uLineStyle > 0.5) {\n float dashRepeat = 4.0;\n float dashDuty = 0.5;\n float vy = fract(gridUV.y * dashRepeat);\n float vx = fract(gridUV.x * dashRepeat);\n float dashMaskY = step(vy, dashDuty);\n float dashMaskX = step(vx, dashDuty);\n if (uLineStyle < 1.5) {\n lineX *= dashMaskY;\n lineY *= dashMaskX;\n } else {\n float dotRepeat = 6.0;\n float dotWidth = 0.18;\n float cy = abs(fract(gridUV.y * dotRepeat) - 0.5);\n float cx = abs(fract(gridUV.x * dotRepeat) - 0.5);\n float dotMaskY = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.y * dotRepeat), cy);\n float dotMaskX = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.x * dotRepeat), cx);\n lineX *= dotMaskY;\n lineY *= dotMaskX;\n }\n }\n float primaryMask = max(lineX, lineY);\n\n vec2 gridUV2 = (hitIsY > 0.5 ? hit.xz : hit.zy) / gridScale;\n if (jitterAmt > 0.0) {\n vec2 j2 = vec2(\n cos(gridUV2.y * 2.1 - iTime * 1.4),\n sin(gridUV2.x * 2.5 + iTime * 1.7)\n ) * (0.15 * jitterAmt);\n gridUV2 += j2;\n }\n float fx2 = fract(gridUV2.x);\n float fy2 = fract(gridUV2.y);\n float ax2 = min(fx2, 1.0 - fx2);\n float ay2 = min(fy2, 1.0 - fy2);\n float wx2 = fwidth(gridUV2.x);\n float wy2 = fwidth(gridUV2.y);\n float tx2 = halfPx * wx2;\n float ty2 = halfPx * wy2;\n float aax2 = wx2;\n float aay2 = wy2;\n float lineX2 = 1.0 - smoothstep(tx2, tx2 + aax2, ax2);\n float lineY2 = 1.0 - smoothstep(ty2, ty2 + aay2, ay2);\n if (uLineStyle > 0.5) {\n float dashRepeat2 = 4.0;\n float dashDuty2 = 0.5;\n float vy2m = fract(gridUV2.y * dashRepeat2);\n float vx2m = fract(gridUV2.x * dashRepeat2);\n float dashMaskY2 = step(vy2m, dashDuty2);\n float dashMaskX2 = step(vx2m, dashDuty2);\n if (uLineStyle < 1.5) {\n lineX2 *= dashMaskY2;\n lineY2 *= dashMaskX2;\n } else {\n float dotRepeat2 = 6.0;\n float dotWidth2 = 0.18;\n float cy2 = abs(fract(gridUV2.y * dotRepeat2) - 0.5);\n float cx2 = abs(fract(gridUV2.x * dotRepeat2) - 0.5);\n float dotMaskY2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.y * dotRepeat2), cy2);\n float dotMaskX2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.x * dotRepeat2), cx2);\n lineX2 *= dotMaskY2;\n lineY2 *= dotMaskX2;\n }\n }\n float altMask = max(lineX2, lineY2);\n\n float edgeDistX = min(abs(hit.x - (-0.5)), abs(hit.x - 0.5));\n float edgeDistY = min(abs(hit.y - (-0.2)), abs(hit.y - 0.2));\n float edgeDist = mix(edgeDistY, edgeDistX, hitIsY);\n float edgeGate = 1.0 - smoothstep(gridScale * 0.5, gridScale * 2.0, edgeDist);\n altMask *= edgeGate;\n\n float lineMask = max(primaryMask, altMask);\n\n float fade = exp(-dist * fadeStrength);\n\n float dur = max(0.05, uScanDuration);\n float del = max(0.0, uScanDelay);\n float scanZMax = 2.0;\n float widthScale = max(0.1, uScanGlow);\n float sigma = max(0.001, 0.18 * widthScale * uScanSoftness);\n float sigmaA = sigma * 2.0;\n\n float combinedPulse = 0.0;\n float combinedAura = 0.0;\n\n float cycle = dur + del;\n float tCycle = mod(iTime, cycle);\n float scanPhase = clamp((tCycle - del) / dur, 0.0, 1.0);\n float phase = scanPhase;\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phase = 1.0 - phase;\n } else if (uScanDirection > 1.5) {\n float t2 = mod(max(0.0, iTime - del), 2.0 * dur);\n phase = (t2 < dur) ? (t2 / dur) : (1.0 - (t2 - dur) / dur);\n }\n float scanZ = phase * scanZMax;\n float dz = abs(hit.z - scanZ);\n float lineBand = exp(-0.5 * (dz * dz) / (sigma * sigma));\n float taper = clamp(uPhaseTaper, 0.0, 0.49);\n float headW = taper;\n float tailW = taper;\n float headFade = smoother01(0.0, headW, phase);\n float tailFade = 1.0 - smoother01(1.0 - tailW, 1.0, phase);\n float phaseWindow = headFade * tailFade;\n float pulseBase = lineBand * phaseWindow;\n combinedPulse += pulseBase * clamp(uScanOpacity, 0.0, 1.0);\n float auraBand = exp(-0.5 * (dz * dz) / (sigmaA * sigmaA));\n combinedAura += (auraBand * 0.25) * phaseWindow * clamp(uScanOpacity, 0.0, 1.0);\n\n for (int i = 0; i < MAX_SCANS; i++) {\n if (float(i) >= uScanCount) break;\n float tActiveI = iTime - uScanStarts[i];\n float phaseI = clamp(tActiveI / dur, 0.0, 1.0);\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phaseI = 1.0 - phaseI;\n } else if (uScanDirection > 1.5) {\n phaseI = (phaseI < 0.5) ? (phaseI * 2.0) : (1.0 - (phaseI - 0.5) * 2.0);\n }\n float scanZI = phaseI * scanZMax;\n float dzI = abs(hit.z - scanZI);\n float lineBandI = exp(-0.5 * (dzI * dzI) / (sigma * sigma));\n float headFadeI = smoother01(0.0, headW, phaseI);\n float tailFadeI = 1.0 - smoother01(1.0 - tailW, 1.0, phaseI);\n float phaseWindowI = headFadeI * tailFadeI;\n combinedPulse += lineBandI * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n float auraBandI = exp(-0.5 * (dzI * dzI) / (sigmaA * sigmaA));\n combinedAura += (auraBandI * 0.25) * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n }\n\n float lineVis = lineMask;\n vec3 gridCol = uLinesColor * lineVis * fade;\n vec3 scanCol = uScanColor * combinedPulse;\n vec3 scanAura = uScanColor * combinedAura;\n\n color = gridCol + scanCol + scanAura;\n\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(iTime * 123.4), vec2(12.9898,78.233))) * 43758.5453123);\n color += (n - 0.5) * uNoise;\n color = clamp(color, 0.0, 1.0);\n float alpha = clamp(max(lineVis, combinedPulse), 0.0, 1.0);\n float gx = 1.0 - smoothstep(tx * 2.0, tx * 2.0 + aax * 2.0, ax);\n float gy = 1.0 - smoothstep(ty * 2.0, ty * 2.0 + aay * 2.0, ay);\n float halo = max(gx, gy) * fade;\n alpha = max(alpha, halo * clamp(uBloomOpacity, 0.0, 1.0));\n fragColor = vec4(color, alpha);\n}\n\nvoid main(){\n vec4 c;\n mainImage(c, vUv * iResolution.xy);\n gl_FragColor = c;\n}\n`;\n\nexport const GridScan: React.FC = ({\n enableWebcam = false,\n showPreview = false,\n modelsPath = 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@0.22.2/weights',\n sensitivity = 0.55,\n lineThickness = 1,\n linesColor = '#2F293A',\n scanColor = '#FF9FFC',\n scanOpacity = 0.4,\n gridScale = 0.1,\n lineStyle = 'solid',\n lineJitter = 0.1,\n scanDirection = 'pingpong',\n enablePost = true,\n bloomIntensity = 0,\n bloomThreshold = 0,\n bloomSmoothing = 0,\n chromaticAberration = 0.002,\n noiseIntensity = 0.01,\n scanGlow = 0.5,\n scanSoftness = 2,\n scanPhaseTaper = 0.9,\n scanDuration = 2.0,\n scanDelay = 2.0,\n enableGyro = false,\n scanOnClick = false,\n snapBackDelay = 250,\n className,\n style\n}) => {\n const containerRef = useRef(null);\n const videoRef = useRef(null);\n\n const rendererRef = useRef(null);\n const materialRef = useRef(null);\n const composerRef = useRef(null);\n const bloomRef = useRef(null);\n const chromaRef = useRef(null);\n const rafRef = useRef(null);\n\n const [modelsReady, setModelsReady] = useState(false);\n const [uiFaceActive, setUiFaceActive] = useState(false);\n\n const lookTarget = useRef(new THREE.Vector2(0, 0));\n const tiltTarget = useRef(0);\n const yawTarget = useRef(0);\n\n const lookCurrent = useRef(new THREE.Vector2(0, 0));\n const lookVel = useRef(new THREE.Vector2(0, 0));\n const tiltCurrent = useRef(0);\n const tiltVel = useRef(0);\n const yawCurrent = useRef(0);\n const yawVel = useRef(0);\n\n const MAX_SCANS = 8;\n const scanStartsRef = useRef([]);\n\n const pushScan = (t: number) => {\n const arr = scanStartsRef.current.slice();\n if (arr.length >= MAX_SCANS) arr.shift();\n arr.push(t);\n scanStartsRef.current = arr;\n if (materialRef.current) {\n const u = materialRef.current.uniforms;\n const buf = new Array(MAX_SCANS).fill(0);\n for (let i = 0; i < arr.length && i < MAX_SCANS; i++) buf[i] = arr[i];\n u.uScanStarts.value = buf;\n u.uScanCount.value = arr.length;\n }\n };\n\n const bufX = useRef([]);\n const bufY = useRef([]);\n const bufT = useRef([]);\n const bufYaw = useRef([]);\n\n const s = THREE.MathUtils.clamp(sensitivity, 0, 1);\n const skewScale = THREE.MathUtils.lerp(0.06, 0.2, s);\n const tiltScale = THREE.MathUtils.lerp(0.12, 0.3, s);\n const yawScale = THREE.MathUtils.lerp(0.1, 0.28, s);\n const depthResponse = THREE.MathUtils.lerp(0.25, 0.45, s);\n const smoothTime = THREE.MathUtils.lerp(0.45, 0.12, s);\n const maxSpeed = Infinity;\n\n const yBoost = THREE.MathUtils.lerp(1.2, 1.6, s);\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n let leaveTimer: number | null = null;\n const onMove = (e: MouseEvent) => {\n if (uiFaceActive) return;\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n const rect = el.getBoundingClientRect();\n const nx = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n const ny = -(((e.clientY - rect.top) / rect.height) * 2 - 1);\n lookTarget.current.set(nx, ny);\n };\n const onClick = async () => {\n const nowSec = performance.now() / 1000;\n if (scanOnClick) pushScan(nowSec);\n if (\n enableGyro &&\n typeof window !== 'undefined' &&\n (window as any).DeviceOrientationEvent &&\n (DeviceOrientationEvent as any).requestPermission\n ) {\n try {\n await (DeviceOrientationEvent as any).requestPermission();\n } catch {}\n }\n };\n const onEnter = () => {\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n };\n const onLeave = () => {\n if (uiFaceActive) return;\n if (leaveTimer) clearTimeout(leaveTimer);\n leaveTimer = window.setTimeout(\n () => {\n lookTarget.current.set(0, 0);\n tiltTarget.current = 0;\n yawTarget.current = 0;\n },\n Math.max(0, snapBackDelay || 0)\n );\n };\n el.addEventListener('mousemove', onMove);\n el.addEventListener('mouseenter', onEnter);\n if (scanOnClick) el.addEventListener('click', onClick);\n el.addEventListener('mouseleave', onLeave);\n return () => {\n el.removeEventListener('mousemove', onMove);\n el.removeEventListener('mouseenter', onEnter);\n el.removeEventListener('mouseleave', onLeave);\n if (scanOnClick) el.removeEventListener('click', onClick);\n if (leaveTimer) clearTimeout(leaveTimer);\n };\n }, [uiFaceActive, snapBackDelay, scanOnClick, enableGyro]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });\n rendererRef.current = renderer;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setSize(container.clientWidth, container.clientHeight);\n renderer.outputColorSpace = THREE.SRGBColorSpace;\n renderer.toneMapping = THREE.NoToneMapping;\n renderer.autoClear = false;\n renderer.setClearColor(0x000000, 0);\n container.appendChild(renderer.domElement);\n\n const uniforms = {\n iResolution: {\n value: new THREE.Vector3(container.clientWidth, container.clientHeight, renderer.getPixelRatio())\n },\n iTime: { value: 0 },\n uSkew: { value: new THREE.Vector2(0, 0) },\n uTilt: { value: 0 },\n uYaw: { value: 0 },\n uLineThickness: { value: lineThickness },\n uLinesColor: { value: srgbColor(linesColor) },\n uScanColor: { value: srgbColor(scanColor) },\n uGridScale: { value: gridScale },\n uLineStyle: { value: lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0 },\n uLineJitter: { value: Math.max(0, Math.min(1, lineJitter || 0)) },\n uScanOpacity: { value: scanOpacity },\n uNoise: { value: noiseIntensity },\n uBloomOpacity: { value: bloomIntensity },\n uScanGlow: { value: scanGlow },\n uScanSoftness: { value: scanSoftness },\n uPhaseTaper: { value: scanPhaseTaper },\n uScanDuration: { value: scanDuration },\n uScanDelay: { value: scanDelay },\n uScanDirection: { value: scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0 },\n uScanStarts: { value: new Array(MAX_SCANS).fill(0) },\n uScanCount: { value: 0 }\n };\n\n const material = new THREE.ShaderMaterial({\n uniforms,\n vertexShader: vert,\n fragmentShader: frag,\n transparent: true,\n depthWrite: false,\n depthTest: false\n });\n materialRef.current = material;\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);\n scene.add(quad);\n\n let composer: EffectComposer | null = null;\n if (enablePost) {\n composer = new EffectComposer(renderer);\n composerRef.current = composer;\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloom = new BloomEffect({\n intensity: 1.0,\n luminanceThreshold: bloomThreshold,\n luminanceSmoothing: bloomSmoothing\n });\n bloom.blendMode.opacity.value = Math.max(0, bloomIntensity);\n bloomRef.current = bloom;\n\n const chroma = new ChromaticAberrationEffect({\n offset: new THREE.Vector2(chromaticAberration, chromaticAberration),\n radialModulation: true,\n modulationOffset: 0.0\n });\n chromaRef.current = chroma;\n\n const effectPass = new EffectPass(camera, bloom, chroma);\n effectPass.renderToScreen = true;\n composer.addPass(effectPass);\n }\n\n const onResize = () => {\n renderer.setSize(container.clientWidth, container.clientHeight);\n material.uniforms.iResolution.value.set(container.clientWidth, container.clientHeight, renderer.getPixelRatio());\n if (composerRef.current) composerRef.current.setSize(container.clientWidth, container.clientHeight);\n };\n window.addEventListener('resize', onResize);\n\n let last = performance.now();\n const tick = () => {\n const now = performance.now();\n const dt = Math.max(0, Math.min(0.1, (now - last) / 1000));\n last = now;\n\n lookCurrent.current.copy(\n smoothDampVec2(lookCurrent.current, lookTarget.current, lookVel.current, smoothTime, maxSpeed, dt)\n );\n\n const tiltSm = smoothDampFloat(\n tiltCurrent.current,\n tiltTarget.current,\n { v: tiltVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n tiltCurrent.current = tiltSm.value;\n tiltVel.current = tiltSm.v;\n\n const yawSm = smoothDampFloat(\n yawCurrent.current,\n yawTarget.current,\n { v: yawVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n yawCurrent.current = yawSm.value;\n yawVel.current = yawSm.v;\n\n const skew = new THREE.Vector2(lookCurrent.current.x * skewScale, -lookCurrent.current.y * yBoost * skewScale);\n material.uniforms.uSkew.value.set(skew.x, skew.y);\n material.uniforms.uTilt.value = tiltCurrent.current * tiltScale;\n material.uniforms.uYaw.value = THREE.MathUtils.clamp(yawCurrent.current * yawScale, -0.6, 0.6);\n\n material.uniforms.iTime.value = now / 1000;\n renderer.clear(true, true, true);\n if (composerRef.current) {\n composerRef.current.render(dt);\n } else {\n renderer.render(scene, camera);\n }\n rafRef.current = requestAnimationFrame(tick);\n };\n rafRef.current = requestAnimationFrame(tick);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n window.removeEventListener('resize', onResize);\n material.dispose();\n (quad.geometry as THREE.BufferGeometry).dispose();\n if (composerRef.current) {\n composerRef.current.dispose();\n composerRef.current = null;\n }\n renderer.dispose();\n renderer.forceContextLoss();\n container.removeChild(renderer.domElement);\n };\n }, [\n sensitivity,\n lineThickness,\n linesColor,\n scanColor,\n scanOpacity,\n gridScale,\n lineStyle,\n lineJitter,\n scanDirection,\n enablePost\n ]);\n\n useEffect(() => {\n const m = materialRef.current;\n if (m) {\n const u = m.uniforms;\n u.uLineThickness.value = lineThickness;\n (u.uLinesColor.value as THREE.Color).copy(srgbColor(linesColor));\n (u.uScanColor.value as THREE.Color).copy(srgbColor(scanColor));\n u.uGridScale.value = gridScale;\n u.uLineStyle.value = lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0;\n u.uLineJitter.value = Math.max(0, Math.min(1, lineJitter || 0));\n u.uBloomOpacity.value = Math.max(0, bloomIntensity);\n u.uNoise.value = Math.max(0, noiseIntensity);\n u.uScanGlow.value = scanGlow;\n u.uScanOpacity.value = Math.max(0, Math.min(1, scanOpacity));\n u.uScanDirection.value = scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0;\n u.uScanSoftness.value = scanSoftness;\n u.uPhaseTaper.value = scanPhaseTaper;\n u.uScanDuration.value = Math.max(0.05, scanDuration);\n u.uScanDelay.value = Math.max(0.0, scanDelay);\n }\n if (bloomRef.current) {\n bloomRef.current.blendMode.opacity.value = Math.max(0, bloomIntensity);\n (bloomRef.current as any).luminanceMaterial.threshold = bloomThreshold;\n (bloomRef.current as any).luminanceMaterial.smoothing = bloomSmoothing;\n }\n if (chromaRef.current) {\n chromaRef.current.offset.set(chromaticAberration, chromaticAberration);\n }\n }, [\n lineThickness,\n linesColor,\n scanColor,\n gridScale,\n lineStyle,\n lineJitter,\n bloomIntensity,\n bloomThreshold,\n bloomSmoothing,\n chromaticAberration,\n noiseIntensity,\n scanGlow,\n scanOpacity,\n scanDirection,\n scanSoftness,\n scanPhaseTaper,\n scanDuration,\n scanDelay\n ]);\n\n useEffect(() => {\n if (!enableGyro) return;\n const handler = (e: DeviceOrientationEvent) => {\n if (uiFaceActive) return;\n const gamma = e.gamma ?? 0;\n const beta = e.beta ?? 0;\n const nx = THREE.MathUtils.clamp(gamma / 45, -1, 1);\n const ny = THREE.MathUtils.clamp(-beta / 30, -1, 1);\n lookTarget.current.set(nx, ny);\n tiltTarget.current = THREE.MathUtils.degToRad(gamma) * 0.4;\n };\n window.addEventListener('deviceorientation', handler);\n return () => {\n window.removeEventListener('deviceorientation', handler);\n };\n }, [enableGyro, uiFaceActive]);\n\n useEffect(() => {\n let canceled = false;\n const load = async () => {\n try {\n await Promise.all([\n faceapi.nets.tinyFaceDetector.loadFromUri(modelsPath),\n faceapi.nets.faceLandmark68TinyNet.loadFromUri(modelsPath)\n ]);\n if (!canceled) setModelsReady(true);\n } catch {\n if (!canceled) setModelsReady(false);\n }\n };\n load();\n return () => {\n canceled = true;\n };\n }, [modelsPath]);\n\n useEffect(() => {\n let stop = false;\n let lastDetect = 0;\n\n const start = async () => {\n if (!enableWebcam || !modelsReady) return;\n const video = videoRef.current;\n if (!video) return;\n\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 } },\n audio: false\n });\n video.srcObject = stream;\n await video.play();\n } catch {\n return;\n }\n\n const opts = new faceapi.TinyFaceDetectorOptions({ inputSize: 320, scoreThreshold: 0.5 });\n\n const detect = async (ts: number) => {\n if (stop) return;\n\n if (ts - lastDetect >= 33) {\n lastDetect = ts;\n try {\n const res = await faceapi.detectSingleFace(video, opts).withFaceLandmarks(true);\n if (res && res.detection) {\n const det = res.detection;\n const box = det.box;\n const vw = video.videoWidth || 1;\n const vh = video.videoHeight || 1;\n\n const cx = box.x + box.width * 0.5;\n const cy = box.y + box.height * 0.5;\n const nx = (cx / vw) * 2 - 1;\n const ny = (cy / vh) * 2 - 1;\n medianPush(bufX.current, nx, 5);\n medianPush(bufY.current, ny, 5);\n const nxm = median(bufX.current);\n const nym = median(bufY.current);\n\n const look = new THREE.Vector2(Math.tanh(nxm), Math.tanh(nym));\n\n const faceSize = Math.min(1, Math.hypot(box.width / vw, box.height / vh));\n const depthScale = 1 + depthResponse * (faceSize - 0.25);\n lookTarget.current.copy(look.multiplyScalar(depthScale));\n\n const leftEye = res.landmarks.getLeftEye();\n const rightEye = res.landmarks.getRightEye();\n const lc = centroid(leftEye);\n const rc = centroid(rightEye);\n const tilt = Math.atan2(rc.y - lc.y, rc.x - lc.x);\n medianPush(bufT.current, tilt, 5);\n tiltTarget.current = median(bufT.current);\n\n const nose = res.landmarks.getNose();\n const tip = nose[nose.length - 1] || nose[Math.floor(nose.length / 2)];\n const jaw = res.landmarks.getJawOutline();\n const leftCheek = jaw[3] || jaw[2];\n const rightCheek = jaw[13] || jaw[14];\n const dL = dist2(tip, leftCheek);\n const dR = dist2(tip, rightCheek);\n const eyeDist = Math.hypot(rc.x - lc.x, rc.y - lc.y) + 1e-6;\n let yawSignal = THREE.MathUtils.clamp((dR - dL) / (eyeDist * 1.6), -1, 1);\n yawSignal = Math.tanh(yawSignal);\n medianPush(bufYaw.current, yawSignal, 5);\n yawTarget.current = median(bufYaw.current);\n\n setUiFaceActive(true);\n } else {\n setUiFaceActive(false);\n }\n } catch {\n setUiFaceActive(false);\n }\n }\n\n if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {\n (video as any).requestVideoFrameCallback(() => detect(performance.now()));\n } else {\n requestAnimationFrame(detect);\n }\n };\n\n requestAnimationFrame(detect);\n };\n\n start();\n\n return () => {\n stop = true;\n const video = videoRef.current;\n if (video) {\n const stream = video.srcObject as MediaStream | null;\n if (stream) stream.getTracks().forEach(t => t.stop());\n video.pause();\n video.srcObject = null;\n }\n };\n }, [enableWebcam, modelsReady, depthResponse]);\n\n return (\n
\n {showPreview && (\n
\n
\n )}\n
\n );\n};\n\nfunction srgbColor(hex: string) {\n const c = new THREE.Color(hex);\n return c.convertSRGBToLinear();\n}\n\nfunction smoothDampVec2(\n current: THREE.Vector2,\n target: THREE.Vector2,\n currentVelocity: THREE.Vector2,\n smoothTime: number,\n maxSpeed: number,\n deltaTime: number\n): THREE.Vector2 {\n const out = current.clone();\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current.clone().sub(target);\n const originalTo = target.clone();\n\n const maxChange = maxSpeed * smoothTime;\n if (change.length() > maxChange) change.setLength(maxChange);\n\n target = current.clone().sub(change);\n const temp = currentVelocity.clone().addScaledVector(change, omega).multiplyScalar(deltaTime);\n currentVelocity.sub(temp.clone().multiplyScalar(omega));\n currentVelocity.multiplyScalar(exp);\n\n out.copy(target.clone().add(change.add(temp).multiplyScalar(exp)));\n\n const origMinusCurrent = originalTo.clone().sub(current);\n const outMinusOrig = out.clone().sub(originalTo);\n if (origMinusCurrent.dot(outMinusOrig) > 0) {\n out.copy(originalTo);\n currentVelocity.set(0, 0);\n }\n return out;\n}\n\nfunction smoothDampFloat(\n current: number,\n target: number,\n velRef: { v: number },\n smoothTime: number,\n maxSpeed: number,\n deltaTime: number\n): { value: number; v: number } {\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current - target;\n const originalTo = target;\n\n const maxChange = maxSpeed * smoothTime;\n change = Math.sign(change) * Math.min(Math.abs(change), maxChange);\n\n target = current - change;\n const temp = (velRef.v + omega * change) * deltaTime;\n velRef.v = (velRef.v - omega * temp) * exp;\n\n let out = target + (change + temp) * exp;\n\n const origMinusCurrent = originalTo - current;\n const outMinusOrig = out - originalTo;\n if (origMinusCurrent * outMinusOrig > 0) {\n out = originalTo;\n velRef.v = 0;\n }\n return { value: out, v: velRef.v };\n}\n\nfunction medianPush(buf: number[], v: number, maxLen: number) {\n buf.push(v);\n if (buf.length > maxLen) buf.shift();\n}\n\nfunction median(buf: number[]) {\n if (buf.length === 0) return 0;\n const a = [...buf].sort((x, y) => x - y);\n const mid = Math.floor(a.length / 2);\n return a.length % 2 ? a[mid] : (a[mid - 1] + a[mid]) * 0.5;\n}\n\nfunction centroid(points: { x: number; y: number }[]) {\n let x = 0,\n y = 0;\n const n = points.length || 1;\n for (const p of points) {\n x += p.x;\n y += p.y;\n }\n return { x: x / n, y: y / n };\n}\n\nfunction dist2(a: { x: number; y: number }, b: { x: number; y: number }) {\n return Math.hypot(a.x - b.x, a.y - b.y);\n}\n" } ], diff --git a/public/r/HalftoneReveal-JS-CSS.json b/public/r/HalftoneReveal-JS-CSS.json index 0f4d2f8f..c57d0aa5 100644 --- a/public/r/HalftoneReveal-JS-CSS.json +++ b/public/r/HalftoneReveal-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "HalftoneReveal/HalftoneReveal.css", + "type": "registry:file", + "path": "HalftoneReveal.css", + "target": "@components/HalftoneReveal.css", "content": ".halftone-reveal {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n touch-action: none;\n cursor: crosshair;\n}\n\n.halftone-reveal canvas {\n display: block;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "HalftoneReveal/HalftoneReveal.jsx", + "path": "HalftoneReveal.jsx", "content": "import { useRef, useEffect } from 'react';\nimport { Renderer, Program, Triangle, Mesh, Texture } from 'ogl';\n\nimport './HalftoneReveal.css';\n\nconst DEFAULT_SRC = 'https://picsum.photos/seed/halftone-reveal/1200/800';\n\nconst hexToRgb = hex => {\n const m = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex || '');\n return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [0, 0, 0];\n};\n\nconst MODES = { mono: 0, duotone: 1, color: 2 };\nconst SHAPES = { circle: 0, square: 1, diamond: 2, line: 3 };\nconst TRIGGERS = { off: 0, hover: 1, always: 2 };\n\nconst vertex = `#version 300 es\nin vec2 position;\nout vec2 vUv;\nvoid main() {\n vUv = position * 0.5 + 0.5;\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\n\nuniform sampler2D tMap;\nuniform vec2 iResolution;\nuniform vec2 uImageSize;\nuniform vec2 uMouse;\nuniform float uActivity;\n\nuniform float uDotSize;\nuniform float uDensity;\nuniform float uAngle;\nuniform int uShape;\nuniform vec3 uInk;\nuniform vec3 uPaper;\nuniform int uMode;\nuniform float uContrast;\nuniform float uInvert;\n\nuniform float uRevealRadius;\nuniform float uEdge;\nuniform float uIdleReveal;\nuniform int uTrigger;\n\nin vec2 vUv;\nout vec4 fragColor;\n\nvec2 uAspect() {\n return vec2(iResolution.x / max(iResolution.y, 1.0), 1.0);\n}\n\nvec2 coverUv(vec2 uv) {\n float ia = uImageSize.x / max(uImageSize.y, 1.0);\n float pa = iResolution.x / max(iResolution.y, 1.0);\n vec2 s = pa > ia ? vec2(1.0, ia / pa) : vec2(pa / ia, 1.0);\n return (uv - 0.5) * s + 0.5;\n}\n\nvec3 gradeRGB(vec3 c) {\n c = clamp((c - 0.5) * uContrast + 0.5, 0.0, 1.0);\n return mix(c, 1.0 - c, uInvert);\n}\n\nfloat shapeDist(vec2 f) {\n if (uShape == 1) return max(abs(f.x), abs(f.y));\n if (uShape == 2) return abs(f.x) + abs(f.y);\n if (uShape == 3) return abs(f.y);\n return length(f);\n}\n\nmat2 rot(float a) {\n float c = cos(a);\n float s = sin(a);\n return mat2(c, -s, s, c);\n}\n\nvec4 sampleCell(vec2 st, float dens, float ang) {\n vec2 rp = rot(ang) * st * dens;\n vec2 center = floor(rp) + 0.5;\n vec2 stC = rot(-ang) * (center / dens);\n vec2 uvC = stC / uAspect();\n return texture(tMap, clamp(coverUv(uvC), 0.0, 1.0));\n}\n\nfloat coverage(vec2 st, float dens, float ang, float ink, float rscale) {\n vec2 rp = rot(ang) * st * dens;\n vec2 f = fract(rp) - 0.5;\n float d = shapeDist(f);\n float r = sqrt(clamp(ink, 0.0, 1.0)) * 0.72 * rscale * uDotSize;\n float w = length(fwidth(rp)) * 0.6 + 1e-4;\n return smoothstep(r + w, r - w, d);\n}\n\nvoid main() {\n vec2 aspect = uAspect();\n vec2 st = vUv * aspect;\n float ang = radians(uAngle);\n\n vec2 duv = (vUv - uMouse) * aspect;\n float dist = length(duv);\n\n float act = uTrigger == 2 ? 1.0 : (uTrigger == 0 ? 0.0 : uActivity);\n float radius = max(uRevealRadius, 1e-4) * mix(0.4, 1.0, act);\n\n float px = 1.4 / max(iResolution.y, 1.0);\n float band = max(px, radius * (1.0 - clamp(uEdge, 0.0, 1.0)) * 0.45);\n float loupe = 1.0 - smoothstep(radius - band, radius + band, dist);\n float focus = clamp(max(loupe * act, uIdleReveal), 0.0, 1.0);\n\n float dens = uDensity;\n\n vec3 print;\n if (uMode == 2) {\n vec3 gc = gradeRGB(sampleCell(st, dens, ang + radians(15.0)).rgb);\n vec3 gm = gradeRGB(sampleCell(st, dens, ang + radians(75.0)).rgb);\n vec3 gy = gradeRGB(sampleCell(st, dens, ang).rgb);\n vec3 gk = gradeRGB(sampleCell(st, dens, ang + radians(45.0)).rgb);\n float c = 1.0 - gc.r;\n float m = 1.0 - gm.g;\n float y = 1.0 - gy.b;\n float k = 1.0 - dot(gk, vec3(0.299, 0.587, 0.114));\n float gcr = min(min(c, m), y) * 0.5;\n c = clamp(c - gcr, 0.0, 1.0);\n m = clamp(m - gcr, 0.0, 1.0);\n y = clamp(y - gcr, 0.0, 1.0);\n k = clamp(max(gcr, k * k * 0.9), 0.0, 1.0);\n float covC = coverage(st, dens, ang + radians(15.0), c, 0.82);\n float covM = coverage(st, dens, ang + radians(75.0), m, 0.82);\n float covY = coverage(st, dens, ang, y, 0.82);\n float covK = coverage(st, dens, ang + radians(45.0), k, 0.78);\n print = uPaper;\n print = mix(print, print * vec3(0.10, 0.72, 0.90), covC);\n print = mix(print, print * vec3(0.92, 0.10, 0.52), covM);\n print = mix(print, print * vec3(0.98, 0.86, 0.10), covY);\n print = mix(print, print * vec3(0.08), covK);\n } else if (uMode == 1) {\n vec3 ink2 = mix(uInk.gbr, vec3(0.90, 0.24, 0.30), 0.7);\n float lumA = dot(gradeRGB(sampleCell(st, dens, ang).rgb), vec3(0.299, 0.587, 0.114));\n float lumB = dot(gradeRGB(sampleCell(st, dens, ang + radians(38.0)).rgb), vec3(0.299, 0.587, 0.114));\n float covA = coverage(st, dens, ang, 1.0 - lumA, 1.0);\n float covB = coverage(st, dens, ang + radians(38.0), pow(1.0 - lumB, 1.4), 0.92);\n print = uPaper;\n print = mix(print, ink2, covB * 0.85);\n print = mix(print, uInk, covA);\n } else {\n float lum = dot(gradeRGB(sampleCell(st, dens, ang).rgb), vec3(0.299, 0.587, 0.114));\n float cov = coverage(st, dens, ang, 1.0 - lum, 1.0);\n print = mix(uPaper, uInk, cov);\n }\n\n float t = clamp(dist / radius, 0.0, 1.0);\n float bend = t * t * t * t;\n vec2 dir = dist > 1e-5 ? duv / dist : vec2(0.0);\n vec2 off = dir * bend * radius * 0.22 / aspect;\n vec2 ca = dir * bend * 0.0045 / aspect;\n vec3 sharp = gradeRGB(vec3(\n texture(tMap, clamp(coverUv(vUv - off - ca), 0.0, 1.0)).r,\n texture(tMap, clamp(coverUv(vUv - off), 0.0, 1.0)).g,\n texture(tMap, clamp(coverUv(vUv - off + ca), 0.0, 1.0)).b\n ));\n\n vec3 col = mix(print, sharp, focus);\n fragColor = vec4(col, 1.0);\n}\n`;\n\nconst HalftoneReveal = ({\n src = DEFAULT_SRC,\n inkColor = '#141414',\n paperColor = '#fff7e6',\n mode = 'mono',\n dotSize = 1,\n dotDensity = 71,\n angle = 45,\n shape = 'circle',\n contrast = 1.15,\n invert = false,\n revealRadius = 0.4,\n edge = 0.8,\n follow = 0.37,\n idleReveal = 0,\n trigger = 'hover',\n borderRadius = '16px',\n className = '',\n style\n}) => {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const uniformsRef = useRef(null);\n const rafRef = useRef(null);\n const followRef = useRef(follow);\n const mouseRef = useRef({ x: 0.5, y: 0.5, sx: 0.5, sy: 0.5, active: 0, target: 0 });\n\n useEffect(() => {\n followRef.current = follow;\n }, [follow]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const reduced =\n typeof window !== 'undefined' &&\n window.matchMedia &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n const renderer = new Renderer({\n dpr: Math.min(window.devicePixelRatio || 1, 2),\n alpha: false,\n antialias: true\n });\n rendererRef.current = renderer;\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 1);\n gl.canvas.style.width = '100%';\n gl.canvas.style.height = '100%';\n gl.canvas.style.display = 'block';\n container.appendChild(gl.canvas);\n\n const texture = new Texture(gl, { generateMipmaps: false });\n\n const uniforms = {\n tMap: { value: texture },\n iResolution: { value: [1, 1] },\n uImageSize: { value: [1, 1] },\n uMouse: { value: [0.5, 0.5] },\n uActivity: { value: 0 },\n uDotSize: { value: dotSize },\n uDensity: { value: dotDensity },\n uAngle: { value: angle },\n uShape: { value: SHAPES[shape] ?? 0 },\n uInk: { value: hexToRgb(inkColor) },\n uPaper: { value: hexToRgb(paperColor) },\n uMode: { value: MODES[mode] ?? 0 },\n uContrast: { value: contrast },\n uInvert: { value: invert ? 1 : 0 },\n uRevealRadius: { value: revealRadius },\n uEdge: { value: edge },\n uIdleReveal: { value: idleReveal },\n uTrigger: { value: TRIGGERS[trigger] ?? 1 }\n };\n uniformsRef.current = uniforms;\n\n const program = new Program(gl, { vertex, fragment, uniforms });\n const mesh = new Mesh(gl, { geometry: new Triangle(gl), program });\n\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.src = src;\n img.onload = () => {\n texture.image = img;\n uniforms.uImageSize.value = [img.naturalWidth, img.naturalHeight];\n };\n\n const resize = () => {\n const w = container.clientWidth || 1;\n const h = container.clientHeight || 1;\n renderer.setSize(w, h);\n uniforms.iResolution.value = [gl.canvas.width, gl.canvas.height];\n };\n resize();\n const ro = new ResizeObserver(resize);\n ro.observe(container);\n\n const onMove = e => {\n const rect = container.getBoundingClientRect();\n mouseRef.current.x = (e.clientX - rect.left) / rect.width;\n mouseRef.current.y = 1 - (e.clientY - rect.top) / rect.height;\n mouseRef.current.target = reduced ? 0 : 1;\n };\n const onLeave = () => {\n mouseRef.current.target = 0;\n };\n container.addEventListener('pointermove', onMove, { passive: true });\n container.addEventListener('pointerenter', onMove, { passive: true });\n container.addEventListener('pointerleave', onLeave, { passive: true });\n\n let prev = performance.now();\n const loop = now => {\n rafRef.current = requestAnimationFrame(loop);\n const dt = Math.min(0.05, Math.max(0.001, (now - prev) / 1000));\n prev = now;\n\n const m = mouseRef.current;\n const a = 1 - Math.exp(-dt / Math.max(0.001, followRef.current));\n m.sx += (m.x - m.sx) * a;\n m.sy += (m.y - m.sy) * a;\n const ba = 1 - Math.exp(-dt / 0.18);\n m.active += (m.target - m.active) * ba;\n\n uniforms.uMouse.value[0] = m.sx;\n uniforms.uMouse.value[1] = m.sy;\n uniforms.uActivity.value = m.active;\n\n renderer.render({ scene: mesh });\n };\n rafRef.current = requestAnimationFrame(loop);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n ro.disconnect();\n container.removeEventListener('pointermove', onMove);\n container.removeEventListener('pointerenter', onMove);\n container.removeEventListener('pointerleave', onLeave);\n const ext = gl.getExtension('WEBGL_lose_context');\n if (ext) ext.loseContext();\n if (gl.canvas.parentNode) gl.canvas.parentNode.removeChild(gl.canvas);\n rendererRef.current = null;\n uniformsRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [src]);\n\n useEffect(() => {\n const u = uniformsRef.current;\n if (!u) return;\n u.uDotSize.value = dotSize;\n u.uDensity.value = dotDensity;\n u.uAngle.value = angle;\n u.uShape.value = SHAPES[shape] ?? 0;\n u.uInk.value = hexToRgb(inkColor);\n u.uPaper.value = hexToRgb(paperColor);\n u.uMode.value = MODES[mode] ?? 0;\n u.uContrast.value = contrast;\n u.uInvert.value = invert ? 1 : 0;\n u.uRevealRadius.value = revealRadius;\n u.uEdge.value = edge;\n u.uIdleReveal.value = idleReveal;\n u.uTrigger.value = TRIGGERS[trigger] ?? 1;\n }, [\n dotSize,\n dotDensity,\n angle,\n shape,\n inkColor,\n paperColor,\n mode,\n contrast,\n invert,\n revealRadius,\n edge,\n idleReveal,\n trigger\n ]);\n\n return (\n
\n );\n};\n\nexport default HalftoneReveal;\n" } ], diff --git a/public/r/HalftoneReveal-TS-CSS.json b/public/r/HalftoneReveal-TS-CSS.json index 592c1ce0..fc05b880 100644 --- a/public/r/HalftoneReveal-TS-CSS.json +++ b/public/r/HalftoneReveal-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "HalftoneReveal/HalftoneReveal.css", + "type": "registry:file", + "path": "HalftoneReveal.css", + "target": "@components/HalftoneReveal.css", "content": ".halftone-reveal {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n touch-action: none;\n cursor: crosshair;\n}\n\n.halftone-reveal canvas {\n display: block;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "HalftoneReveal/HalftoneReveal.tsx", + "path": "HalftoneReveal.tsx", "content": "import { useRef, useEffect, CSSProperties } from 'react';\nimport { Renderer, Program, Triangle, Mesh, Texture } from 'ogl';\n\nimport './HalftoneReveal.css';\n\nconst DEFAULT_SRC = 'https://picsum.photos/seed/halftone-reveal/1200/800';\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n const m = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex || '');\n return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [0, 0, 0];\n};\n\ntype Mode = 'mono' | 'duotone' | 'color';\ntype Shape = 'circle' | 'square' | 'diamond' | 'line';\ntype Trigger = 'off' | 'hover' | 'always';\n\nconst MODES: Record = { mono: 0, duotone: 1, color: 2 };\nconst SHAPES: Record = { circle: 0, square: 1, diamond: 2, line: 3 };\nconst TRIGGERS: Record = { off: 0, hover: 1, always: 2 };\n\nexport interface HalftoneRevealProps {\n src?: string;\n inkColor?: string;\n paperColor?: string;\n mode?: Mode;\n dotSize?: number;\n dotDensity?: number;\n angle?: number;\n shape?: Shape;\n contrast?: number;\n invert?: boolean;\n revealRadius?: number;\n edge?: number;\n follow?: number;\n idleReveal?: number;\n trigger?: Trigger;\n borderRadius?: string;\n className?: string;\n style?: CSSProperties;\n}\n\nconst vertex = `#version 300 es\nin vec2 position;\nout vec2 vUv;\nvoid main() {\n vUv = position * 0.5 + 0.5;\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\n\nuniform sampler2D tMap;\nuniform vec2 iResolution;\nuniform vec2 uImageSize;\nuniform vec2 uMouse;\nuniform float uActivity;\n\nuniform float uDotSize;\nuniform float uDensity;\nuniform float uAngle;\nuniform int uShape;\nuniform vec3 uInk;\nuniform vec3 uPaper;\nuniform int uMode;\nuniform float uContrast;\nuniform float uInvert;\n\nuniform float uRevealRadius;\nuniform float uEdge;\nuniform float uIdleReveal;\nuniform int uTrigger;\n\nin vec2 vUv;\nout vec4 fragColor;\n\nvec2 uAspect() {\n return vec2(iResolution.x / max(iResolution.y, 1.0), 1.0);\n}\n\nvec2 coverUv(vec2 uv) {\n float ia = uImageSize.x / max(uImageSize.y, 1.0);\n float pa = iResolution.x / max(iResolution.y, 1.0);\n vec2 s = pa > ia ? vec2(1.0, ia / pa) : vec2(pa / ia, 1.0);\n return (uv - 0.5) * s + 0.5;\n}\n\nvec3 gradeRGB(vec3 c) {\n c = clamp((c - 0.5) * uContrast + 0.5, 0.0, 1.0);\n return mix(c, 1.0 - c, uInvert);\n}\n\nfloat shapeDist(vec2 f) {\n if (uShape == 1) return max(abs(f.x), abs(f.y));\n if (uShape == 2) return abs(f.x) + abs(f.y);\n if (uShape == 3) return abs(f.y);\n return length(f);\n}\n\nmat2 rot(float a) {\n float c = cos(a);\n float s = sin(a);\n return mat2(c, -s, s, c);\n}\n\nvec4 sampleCell(vec2 st, float dens, float ang) {\n vec2 rp = rot(ang) * st * dens;\n vec2 center = floor(rp) + 0.5;\n vec2 stC = rot(-ang) * (center / dens);\n vec2 uvC = stC / uAspect();\n return texture(tMap, clamp(coverUv(uvC), 0.0, 1.0));\n}\n\nfloat coverage(vec2 st, float dens, float ang, float ink, float rscale) {\n vec2 rp = rot(ang) * st * dens;\n vec2 f = fract(rp) - 0.5;\n float d = shapeDist(f);\n float r = sqrt(clamp(ink, 0.0, 1.0)) * 0.72 * rscale * uDotSize;\n float w = length(fwidth(rp)) * 0.6 + 1e-4;\n return smoothstep(r + w, r - w, d);\n}\n\nvoid main() {\n vec2 aspect = uAspect();\n vec2 st = vUv * aspect;\n float ang = radians(uAngle);\n\n vec2 duv = (vUv - uMouse) * aspect;\n float dist = length(duv);\n\n float act = uTrigger == 2 ? 1.0 : (uTrigger == 0 ? 0.0 : uActivity);\n float radius = max(uRevealRadius, 1e-4) * mix(0.4, 1.0, act);\n\n float px = 1.4 / max(iResolution.y, 1.0);\n float band = max(px, radius * (1.0 - clamp(uEdge, 0.0, 1.0)) * 0.45);\n float loupe = 1.0 - smoothstep(radius - band, radius + band, dist);\n float focus = clamp(max(loupe * act, uIdleReveal), 0.0, 1.0);\n\n float dens = uDensity;\n\n vec3 print;\n if (uMode == 2) {\n vec3 gc = gradeRGB(sampleCell(st, dens, ang + radians(15.0)).rgb);\n vec3 gm = gradeRGB(sampleCell(st, dens, ang + radians(75.0)).rgb);\n vec3 gy = gradeRGB(sampleCell(st, dens, ang).rgb);\n vec3 gk = gradeRGB(sampleCell(st, dens, ang + radians(45.0)).rgb);\n float c = 1.0 - gc.r;\n float m = 1.0 - gm.g;\n float y = 1.0 - gy.b;\n float k = 1.0 - dot(gk, vec3(0.299, 0.587, 0.114));\n float gcr = min(min(c, m), y) * 0.5;\n c = clamp(c - gcr, 0.0, 1.0);\n m = clamp(m - gcr, 0.0, 1.0);\n y = clamp(y - gcr, 0.0, 1.0);\n k = clamp(max(gcr, k * k * 0.9), 0.0, 1.0);\n float covC = coverage(st, dens, ang + radians(15.0), c, 0.82);\n float covM = coverage(st, dens, ang + radians(75.0), m, 0.82);\n float covY = coverage(st, dens, ang, y, 0.82);\n float covK = coverage(st, dens, ang + radians(45.0), k, 0.78);\n print = uPaper;\n print = mix(print, print * vec3(0.10, 0.72, 0.90), covC);\n print = mix(print, print * vec3(0.92, 0.10, 0.52), covM);\n print = mix(print, print * vec3(0.98, 0.86, 0.10), covY);\n print = mix(print, print * vec3(0.08), covK);\n } else if (uMode == 1) {\n vec3 ink2 = mix(uInk.gbr, vec3(0.90, 0.24, 0.30), 0.7);\n float lumA = dot(gradeRGB(sampleCell(st, dens, ang).rgb), vec3(0.299, 0.587, 0.114));\n float lumB = dot(gradeRGB(sampleCell(st, dens, ang + radians(38.0)).rgb), vec3(0.299, 0.587, 0.114));\n float covA = coverage(st, dens, ang, 1.0 - lumA, 1.0);\n float covB = coverage(st, dens, ang + radians(38.0), pow(1.0 - lumB, 1.4), 0.92);\n print = uPaper;\n print = mix(print, ink2, covB * 0.85);\n print = mix(print, uInk, covA);\n } else {\n float lum = dot(gradeRGB(sampleCell(st, dens, ang).rgb), vec3(0.299, 0.587, 0.114));\n float cov = coverage(st, dens, ang, 1.0 - lum, 1.0);\n print = mix(uPaper, uInk, cov);\n }\n\n float t = clamp(dist / radius, 0.0, 1.0);\n float bend = t * t * t * t;\n vec2 dir = dist > 1e-5 ? duv / dist : vec2(0.0);\n vec2 off = dir * bend * radius * 0.22 / aspect;\n vec2 ca = dir * bend * 0.0045 / aspect;\n vec3 sharp = gradeRGB(vec3(\n texture(tMap, clamp(coverUv(vUv - off - ca), 0.0, 1.0)).r,\n texture(tMap, clamp(coverUv(vUv - off), 0.0, 1.0)).g,\n texture(tMap, clamp(coverUv(vUv - off + ca), 0.0, 1.0)).b\n ));\n\n vec3 col = mix(print, sharp, focus);\n fragColor = vec4(col, 1.0);\n}\n`;\n\nconst HalftoneReveal = ({\n src = DEFAULT_SRC,\n inkColor = '#141414',\n paperColor = '#fff7e6',\n mode = 'mono',\n dotSize = 1,\n dotDensity = 71,\n angle = 45,\n shape = 'circle',\n contrast = 1.15,\n invert = false,\n revealRadius = 0.4,\n edge = 0.8,\n follow = 0.37,\n idleReveal = 0,\n trigger = 'hover',\n borderRadius = '16px',\n className = '',\n style\n}: HalftoneRevealProps) => {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const uniformsRef = useRef | null>(null);\n const rafRef = useRef(null);\n const followRef = useRef(follow);\n const mouseRef = useRef({ x: 0.5, y: 0.5, sx: 0.5, sy: 0.5, active: 0, target: 0 });\n\n useEffect(() => {\n followRef.current = follow;\n }, [follow]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const reduced =\n typeof window !== 'undefined' &&\n window.matchMedia &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n const renderer = new Renderer({\n dpr: Math.min(window.devicePixelRatio || 1, 2),\n alpha: false,\n antialias: true\n });\n rendererRef.current = renderer;\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 1);\n gl.canvas.style.width = '100%';\n gl.canvas.style.height = '100%';\n gl.canvas.style.display = 'block';\n container.appendChild(gl.canvas);\n\n const texture = new Texture(gl, { generateMipmaps: false });\n\n const uniforms = {\n tMap: { value: texture },\n iResolution: { value: [1, 1] },\n uImageSize: { value: [1, 1] },\n uMouse: { value: [0.5, 0.5] },\n uActivity: { value: 0 },\n uDotSize: { value: dotSize },\n uDensity: { value: dotDensity },\n uAngle: { value: angle },\n uShape: { value: SHAPES[shape] ?? 0 },\n uInk: { value: hexToRgb(inkColor) },\n uPaper: { value: hexToRgb(paperColor) },\n uMode: { value: MODES[mode] ?? 0 },\n uContrast: { value: contrast },\n uInvert: { value: invert ? 1 : 0 },\n uRevealRadius: { value: revealRadius },\n uEdge: { value: edge },\n uIdleReveal: { value: idleReveal },\n uTrigger: { value: TRIGGERS[trigger] ?? 1 }\n };\n uniformsRef.current = uniforms;\n\n const program = new Program(gl, { vertex, fragment, uniforms });\n const mesh = new Mesh(gl, { geometry: new Triangle(gl), program });\n\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.src = src;\n img.onload = () => {\n texture.image = img;\n uniforms.uImageSize.value = [img.naturalWidth, img.naturalHeight];\n };\n\n const resize = () => {\n const w = container.clientWidth || 1;\n const h = container.clientHeight || 1;\n renderer.setSize(w, h);\n uniforms.iResolution.value = [gl.canvas.width, gl.canvas.height];\n };\n resize();\n const ro = new ResizeObserver(resize);\n ro.observe(container);\n\n const onMove = (e: PointerEvent) => {\n const rect = container.getBoundingClientRect();\n mouseRef.current.x = (e.clientX - rect.left) / rect.width;\n mouseRef.current.y = 1 - (e.clientY - rect.top) / rect.height;\n mouseRef.current.target = reduced ? 0 : 1;\n };\n const onLeave = () => {\n mouseRef.current.target = 0;\n };\n container.addEventListener('pointermove', onMove, { passive: true });\n container.addEventListener('pointerenter', onMove, { passive: true });\n container.addEventListener('pointerleave', onLeave, { passive: true });\n\n let prev = performance.now();\n const loop = (now: number) => {\n rafRef.current = requestAnimationFrame(loop);\n const dt = Math.min(0.05, Math.max(0.001, (now - prev) / 1000));\n prev = now;\n\n const m = mouseRef.current;\n const a = 1 - Math.exp(-dt / Math.max(0.001, followRef.current));\n m.sx += (m.x - m.sx) * a;\n m.sy += (m.y - m.sy) * a;\n const ba = 1 - Math.exp(-dt / 0.18);\n m.active += (m.target - m.active) * ba;\n\n uniforms.uMouse.value[0] = m.sx;\n uniforms.uMouse.value[1] = m.sy;\n uniforms.uActivity.value = m.active;\n\n renderer.render({ scene: mesh });\n };\n rafRef.current = requestAnimationFrame(loop);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n ro.disconnect();\n container.removeEventListener('pointermove', onMove);\n container.removeEventListener('pointerenter', onMove);\n container.removeEventListener('pointerleave', onLeave);\n const ext = gl.getExtension('WEBGL_lose_context');\n if (ext) ext.loseContext();\n if (gl.canvas.parentNode) gl.canvas.parentNode.removeChild(gl.canvas);\n rendererRef.current = null;\n uniformsRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [src]);\n\n useEffect(() => {\n const u = uniformsRef.current;\n if (!u) return;\n u.uDotSize.value = dotSize;\n u.uDensity.value = dotDensity;\n u.uAngle.value = angle;\n u.uShape.value = SHAPES[shape] ?? 0;\n u.uInk.value = hexToRgb(inkColor);\n u.uPaper.value = hexToRgb(paperColor);\n u.uMode.value = MODES[mode] ?? 0;\n u.uContrast.value = contrast;\n u.uInvert.value = invert ? 1 : 0;\n u.uRevealRadius.value = revealRadius;\n u.uEdge.value = edge;\n u.uIdleReveal.value = idleReveal;\n u.uTrigger.value = TRIGGERS[trigger] ?? 1;\n }, [\n dotSize,\n dotDensity,\n angle,\n shape,\n inkColor,\n paperColor,\n mode,\n contrast,\n invert,\n revealRadius,\n edge,\n idleReveal,\n trigger\n ]);\n\n return (\n
\n );\n};\n\nexport default HalftoneReveal;\n" } ], diff --git a/public/r/Hyperspeed-JS-CSS.json b/public/r/Hyperspeed-JS-CSS.json index 9cfa2756..7096a1f2 100644 --- a/public/r/Hyperspeed-JS-CSS.json +++ b/public/r/Hyperspeed-JS-CSS.json @@ -7,17 +7,18 @@ "files": [ { "type": "registry:component", - "path": "Hyperspeed/HyperSpeedPresets.js", + "path": "HyperSpeedPresets.js", "content": "export const hyperspeedPresets = {\n one: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 20,\n lightPairsPerRoadWay: 40,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.03, 400 * 0.2],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.8, 0.8],\n carFloorSeparation: [0, 5],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xd856bf, 0x6750a2, 0xc247ac],\n rightCars: [0x03b3c3, 0x0e5ea5, 0x324555],\n sticks: 0x03b3c3\n }\n },\n two: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'mountainDistortion',\n length: 400,\n roadWidth: 9,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 50,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xff102a, 0xeb383e, 0xff102a],\n rightCars: [0xdadafa, 0xbebae3, 0x8f97e4],\n sticks: 0xdadafa\n }\n },\n three: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'xyDistortion',\n length: 400,\n roadWidth: 9,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 3,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 30,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.02, 0.05],\n lightStickHeight: [0.3, 0.7],\n movingAwaySpeed: [20, 50],\n movingCloserSpeed: [-150, -230],\n carLightsLength: [400 * 0.05, 400 * 0.2],\n carLightsRadius: [0.03, 0.08],\n carWidthPercentage: [0.1, 0.5],\n carShiftX: [-0.5, 0.5],\n carFloorSeparation: [0, 0.1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0x7d0d1b, 0xa90519, 0xff102a],\n rightCars: [0xf1eece, 0xe6e2b1, 0xdfd98a],\n sticks: 0xf1eece\n }\n },\n four: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'LongRaceDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 5,\n lanesPerRoad: 2,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 70,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xff5f73, 0xe74d60, 0xff102a],\n rightCars: [0xa4e3e6, 0x80d1d4, 0x53c2c6],\n sticks: 0xa4e3e6\n }\n },\n five: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 9,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 50,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xdc5b20, 0xdca320, 0xdc2020],\n rightCars: [0x334bf7, 0xe5e6ed, 0xbfc6f3],\n sticks: 0xc5e8eb\n }\n },\n six: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'deepDistortion',\n length: 400,\n roadWidth: 18,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 50,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xff322f, 0xa33010, 0xa81508],\n rightCars: [0xfdfdf0, 0xf3dea0, 0xe2bb88],\n sticks: 0xfdfdf0\n }\n }\n};\n" }, { - "type": "registry:component", - "path": "Hyperspeed/Hyperspeed.css", + "type": "registry:file", + "path": "Hyperspeed.css", + "target": "@components/Hyperspeed.css", "content": "#lights {\n width: 100%;\n height: 100%;\n overflow: hidden;\n position: absolute;\n}\n\ncanvas {\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "Hyperspeed/Hyperspeed.jsx", + "path": "Hyperspeed.jsx", "content": "import { BloomEffect, EffectComposer, EffectPass, RenderPass, SMAAEffect, SMAAPreset } from 'postprocessing';\nimport { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\nimport './Hyperspeed.css';\n\nconst DEFAULT_EFFECT_OPTIONS = {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 2,\n lanesPerRoad: 4,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 20,\n lightPairsPerRoadWay: 40,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.03, 400 * 0.2],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.8, 0.8],\n carFloorSeparation: [0, 5],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0xffffff,\n brokenLines: 0xffffff,\n leftCars: [0xd856bf, 0x6750a2, 0xc247ac],\n rightCars: [0x03b3c3, 0x0e5ea5, 0x324555],\n sticks: 0x03b3c3\n }\n};\n\nconst Hyperspeed = ({ effectOptions = DEFAULT_EFFECT_OPTIONS }) => {\n const hyperspeed = useRef(null);\n const appRef = useRef(null);\n\n useEffect(() => {\n if (appRef.current) {\n appRef.current.dispose();\n appRef.current = null;\n const container = hyperspeed.current;\n if (container) {\n while (container.firstChild) {\n container.removeChild(container.firstChild);\n }\n }\n }\n const mountainUniforms = {\n uFreq: { value: new THREE.Vector3(3, 6, 10) },\n uAmp: { value: new THREE.Vector3(30, 30, 20) }\n };\n\n const xyUniforms = {\n uFreq: { value: new THREE.Vector2(5, 2) },\n uAmp: { value: new THREE.Vector2(25, 15) }\n };\n\n const LongRaceUniforms = {\n uFreq: { value: new THREE.Vector2(2, 3) },\n uAmp: { value: new THREE.Vector2(35, 10) }\n };\n\n const turbulentUniforms = {\n uFreq: { value: new THREE.Vector4(4, 8, 8, 1) },\n uAmp: { value: new THREE.Vector4(25, 5, 10, 10) }\n };\n\n const deepUniforms = {\n uFreq: { value: new THREE.Vector2(4, 8) },\n uAmp: { value: new THREE.Vector2(10, 20) },\n uPowY: { value: new THREE.Vector2(20, 2) }\n };\n\n let nsin = val => Math.sin(val) * 0.5 + 0.5;\n\n const distortions = {\n mountainDistortion: {\n uniforms: mountainUniforms,\n getDistortion: `\n uniform vec3 uAmp;\n uniform vec3 uFreq;\n #define PI 3.14159265358979\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n vec3 getDistortion(float progress){\n float movementProgressFix = 0.02;\n return vec3( \n cos(progress * PI * uFreq.x + uTime) * uAmp.x - cos(movementProgressFix * PI * uFreq.x + uTime) * uAmp.x,\n nsin(progress * PI * uFreq.y + uTime) * uAmp.y - nsin(movementProgressFix * PI * uFreq.y + uTime) * uAmp.y,\n nsin(progress * PI * uFreq.z + uTime) * uAmp.z - nsin(movementProgressFix * PI * uFreq.z + uTime) * uAmp.z\n );\n }\n `,\n getJS: (progress, time) => {\n let movementProgressFix = 0.02;\n let uFreq = mountainUniforms.uFreq.value;\n let uAmp = mountainUniforms.uAmp.value;\n let distortion = new THREE.Vector3(\n Math.cos(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.cos(movementProgressFix * Math.PI * uFreq.x + time) * uAmp.x,\n nsin(progress * Math.PI * uFreq.y + time) * uAmp.y -\n nsin(movementProgressFix * Math.PI * uFreq.y + time) * uAmp.y,\n nsin(progress * Math.PI * uFreq.z + time) * uAmp.z -\n nsin(movementProgressFix * Math.PI * uFreq.z + time) * uAmp.z\n );\n let lookAtAmp = new THREE.Vector3(2, 2, 2);\n let lookAtOffset = new THREE.Vector3(0, 0, -5);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n xyDistortion: {\n uniforms: xyUniforms,\n getDistortion: `\n uniform vec2 uFreq;\n uniform vec2 uAmp;\n #define PI 3.14159265358979\n vec3 getDistortion(float progress){\n float movementProgressFix = 0.02;\n return vec3( \n cos(progress * PI * uFreq.x + uTime) * uAmp.x - cos(movementProgressFix * PI * uFreq.x + uTime) * uAmp.x,\n sin(progress * PI * uFreq.y + PI/2. + uTime) * uAmp.y - sin(movementProgressFix * PI * uFreq.y + PI/2. + uTime) * uAmp.y,\n 0.\n );\n }\n `,\n getJS: (progress, time) => {\n let movementProgressFix = 0.02;\n let uFreq = xyUniforms.uFreq.value;\n let uAmp = xyUniforms.uAmp.value;\n let distortion = new THREE.Vector3(\n Math.cos(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.cos(movementProgressFix * Math.PI * uFreq.x + time) * uAmp.x,\n Math.sin(progress * Math.PI * uFreq.y + time + Math.PI / 2) * uAmp.y -\n Math.sin(movementProgressFix * Math.PI * uFreq.y + time + Math.PI / 2) * uAmp.y,\n 0\n );\n let lookAtAmp = new THREE.Vector3(2, 0.4, 1);\n let lookAtOffset = new THREE.Vector3(0, 0, -3);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n LongRaceDistortion: {\n uniforms: LongRaceUniforms,\n getDistortion: `\n uniform vec2 uFreq;\n uniform vec2 uAmp;\n #define PI 3.14159265358979\n vec3 getDistortion(float progress){\n float camProgress = 0.0125;\n return vec3( \n sin(progress * PI * uFreq.x + uTime) * uAmp.x - sin(camProgress * PI * uFreq.x + uTime) * uAmp.x,\n sin(progress * PI * uFreq.y + uTime) * uAmp.y - sin(camProgress * PI * uFreq.y + uTime) * uAmp.y,\n 0.\n );\n }\n `,\n getJS: (progress, time) => {\n let camProgress = 0.0125;\n let uFreq = LongRaceUniforms.uFreq.value;\n let uAmp = LongRaceUniforms.uAmp.value;\n let distortion = new THREE.Vector3(\n Math.sin(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.sin(camProgress * Math.PI * uFreq.x + time) * uAmp.x,\n Math.sin(progress * Math.PI * uFreq.y + time) * uAmp.y -\n Math.sin(camProgress * Math.PI * uFreq.y + time) * uAmp.y,\n 0\n );\n let lookAtAmp = new THREE.Vector3(1, 1, 0);\n let lookAtOffset = new THREE.Vector3(0, 0, -5);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n turbulentDistortion: {\n uniforms: turbulentUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n cos(PI * progress * uFreq.r + uTime) * uAmp.r +\n pow(cos(PI * progress * uFreq.g + uTime * (uFreq.g / uFreq.r)), 2. ) * uAmp.g\n );\n }\n float getDistortionY(float progress){\n return (\n -nsin(PI * progress * uFreq.b + uTime) * uAmp.b +\n -pow(nsin(PI * progress * uFreq.a + uTime / (uFreq.b / uFreq.a)), 5.) * uAmp.a\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.0125),\n getDistortionY(progress) - getDistortionY(0.0125),\n 0.\n );\n }\n `,\n getJS: (progress, time) => {\n const uFreq = turbulentUniforms.uFreq.value;\n const uAmp = turbulentUniforms.uAmp.value;\n\n const getX = p =>\n Math.cos(Math.PI * p * uFreq.x + time) * uAmp.x +\n Math.pow(Math.cos(Math.PI * p * uFreq.y + time * (uFreq.y / uFreq.x)), 2) * uAmp.y;\n\n const getY = p =>\n -nsin(Math.PI * p * uFreq.z + time) * uAmp.z -\n Math.pow(nsin(Math.PI * p * uFreq.w + time / (uFreq.z / uFreq.w)), 5) * uAmp.w;\n\n let distortion = new THREE.Vector3(\n getX(progress) - getX(progress + 0.007),\n getY(progress) - getY(progress + 0.007),\n 0\n );\n let lookAtAmp = new THREE.Vector3(-2, -5, 0);\n let lookAtOffset = new THREE.Vector3(0, 0, -10);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n turbulentDistortionStill: {\n uniforms: turbulentUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n cos(PI * progress * uFreq.r) * uAmp.r +\n pow(cos(PI * progress * uFreq.g * (uFreq.g / uFreq.r)), 2. ) * uAmp.g\n );\n }\n float getDistortionY(float progress){\n return (\n -nsin(PI * progress * uFreq.b) * uAmp.b +\n -pow(nsin(PI * progress * uFreq.a / (uFreq.b / uFreq.a)), 5.) * uAmp.a\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.02),\n 0.\n );\n }\n `\n },\n deepDistortionStill: {\n uniforms: deepUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n uniform vec2 uPowY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n sin(progress * PI * uFreq.x) * uAmp.x * 2.\n );\n }\n float getDistortionY(float progress){\n return (\n pow(abs(progress * uPowY.x), uPowY.y) + sin(progress * PI * uFreq.y) * uAmp.y\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.05),\n 0.\n );\n }\n `\n },\n deepDistortion: {\n uniforms: deepUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n uniform vec2 uPowY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n sin(progress * PI * uFreq.x + uTime) * uAmp.x\n );\n }\n float getDistortionY(float progress){\n return (\n pow(abs(progress * uPowY.x), uPowY.y) + sin(progress * PI * uFreq.y + uTime) * uAmp.y\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.02),\n 0.\n );\n }\n `,\n getJS: (progress, time) => {\n const uFreq = deepUniforms.uFreq.value;\n const uAmp = deepUniforms.uAmp.value;\n const uPowY = deepUniforms.uPowY.value;\n\n const getX = p => Math.sin(p * Math.PI * uFreq.x + time) * uAmp.x;\n const getY = p => Math.pow(p * uPowY.x, uPowY.y) + Math.sin(p * Math.PI * uFreq.y + time) * uAmp.y;\n\n let distortion = new THREE.Vector3(\n getX(progress) - getX(progress + 0.01),\n getY(progress) - getY(progress + 0.01),\n 0\n );\n let lookAtAmp = new THREE.Vector3(-2, -4, 0);\n let lookAtOffset = new THREE.Vector3(0, 0, -10);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n }\n };\n\n class App {\n constructor(container, options = {}) {\n this.options = options;\n if (this.options.distortion == null) {\n this.options.distortion = {\n uniforms: distortion_uniforms,\n getDistortion: distortion_vertex\n };\n }\n this.container = container;\n this.hasValidSize = false;\n\n const initW = Math.max(1, container.offsetWidth);\n const initH = Math.max(1, container.offsetHeight);\n\n this.renderer = new THREE.WebGLRenderer({\n antialias: false,\n alpha: true\n });\n this.renderer.setSize(initW, initH, false);\n this.renderer.setPixelRatio(window.devicePixelRatio);\n this.composer = new EffectComposer(this.renderer);\n container.append(this.renderer.domElement);\n\n this.camera = new THREE.PerspectiveCamera(options.fov, initW / initH, 0.1, 10000);\n this.camera.position.z = -5;\n this.camera.position.y = 8;\n this.camera.position.x = 0;\n this.scene = new THREE.Scene();\n this.scene.background = null;\n\n let fog = new THREE.Fog(options.colors.background, options.length * 0.2, options.length * 500);\n this.scene.fog = fog;\n this.fogUniforms = {\n fogColor: { value: fog.color },\n fogNear: { value: fog.near },\n fogFar: { value: fog.far }\n };\n this.clock = new THREE.Clock();\n this.assets = {};\n this.disposed = false;\n\n this.road = new Road(this, options);\n this.leftCarLights = new CarLights(\n this,\n options,\n options.colors.leftCars,\n options.movingAwaySpeed,\n new THREE.Vector2(0, 1 - options.carLightsFade)\n );\n this.rightCarLights = new CarLights(\n this,\n options,\n options.colors.rightCars,\n options.movingCloserSpeed,\n new THREE.Vector2(1, 0 + options.carLightsFade)\n );\n this.leftSticks = new LightsSticks(this, options);\n\n this.fovTarget = options.fov;\n this.speedUpTarget = 0;\n this.speedUp = 0;\n this.timeOffset = 0;\n\n this.tick = this.tick.bind(this);\n this.init = this.init.bind(this);\n this.setSize = this.setSize.bind(this);\n this.onMouseDown = this.onMouseDown.bind(this);\n this.onMouseUp = this.onMouseUp.bind(this);\n\n this.onTouchStart = this.onTouchStart.bind(this);\n this.onTouchEnd = this.onTouchEnd.bind(this);\n this.onContextMenu = this.onContextMenu.bind(this);\n\n this.onWindowResize = this.onWindowResize.bind(this);\n window.addEventListener('resize', this.onWindowResize);\n\n if (container.offsetWidth > 0 && container.offsetHeight > 0) {\n this.hasValidSize = true;\n }\n }\n\n onWindowResize() {\n const width = this.container.offsetWidth;\n const height = this.container.offsetHeight;\n\n if (width <= 0 || height <= 0) {\n this.hasValidSize = false;\n return;\n }\n\n this.renderer.setSize(width, height);\n this.camera.aspect = width / height;\n this.camera.updateProjectionMatrix();\n this.composer.setSize(width, height);\n this.hasValidSize = true;\n }\n\n initPasses() {\n this.renderPass = new RenderPass(this.scene, this.camera);\n this.bloomPass = new EffectPass(\n this.camera,\n new BloomEffect({\n luminanceThreshold: 0.2,\n luminanceSmoothing: 0,\n resolutionScale: 1\n })\n );\n\n const smaaPass = new EffectPass(\n this.camera,\n new SMAAEffect({\n preset: SMAAPreset.MEDIUM,\n searchImage: SMAAEffect.searchImageDataURL,\n areaImage: SMAAEffect.areaImageDataURL\n })\n );\n this.renderPass.renderToScreen = false;\n this.bloomPass.renderToScreen = false;\n smaaPass.renderToScreen = true;\n this.composer.addPass(this.renderPass);\n this.composer.addPass(this.bloomPass);\n this.composer.addPass(smaaPass);\n }\n\n loadAssets() {\n const assets = this.assets;\n return new Promise(resolve => {\n const manager = new THREE.LoadingManager(resolve);\n\n const searchImage = new Image();\n const areaImage = new Image();\n assets.smaa = {};\n searchImage.addEventListener('load', function () {\n assets.smaa.search = this;\n manager.itemEnd('smaa-search');\n });\n\n areaImage.addEventListener('load', function () {\n assets.smaa.area = this;\n manager.itemEnd('smaa-area');\n });\n manager.itemStart('smaa-search');\n manager.itemStart('smaa-area');\n\n searchImage.src = SMAAEffect.searchImageDataURL;\n areaImage.src = SMAAEffect.areaImageDataURL;\n });\n }\n\n init() {\n this.initPasses();\n const options = this.options;\n this.road.init();\n this.leftCarLights.init();\n\n this.leftCarLights.mesh.position.setX(-options.roadWidth / 2 - options.islandWidth / 2);\n this.rightCarLights.init();\n this.rightCarLights.mesh.position.setX(options.roadWidth / 2 + options.islandWidth / 2);\n this.leftSticks.init();\n this.leftSticks.mesh.position.setX(-(options.roadWidth + options.islandWidth / 2));\n\n this.container.addEventListener('mousedown', this.onMouseDown);\n this.container.addEventListener('mouseup', this.onMouseUp);\n this.container.addEventListener('mouseout', this.onMouseUp);\n\n this.container.addEventListener('touchstart', this.onTouchStart, { passive: true });\n this.container.addEventListener('touchend', this.onTouchEnd, { passive: true });\n this.container.addEventListener('touchcancel', this.onTouchEnd, { passive: true });\n\n this.container.addEventListener('contextmenu', this.onContextMenu);\n\n this.tick();\n }\n\n onMouseDown(ev) {\n if (this.options.onSpeedUp) this.options.onSpeedUp(ev);\n this.fovTarget = this.options.fovSpeedUp;\n this.speedUpTarget = this.options.speedUp;\n }\n\n onMouseUp(ev) {\n if (this.options.onSlowDown) this.options.onSlowDown(ev);\n this.fovTarget = this.options.fov;\n this.speedUpTarget = 0;\n }\n\n onTouchStart(ev) {\n if (this.options.onSpeedUp) this.options.onSpeedUp(ev);\n this.fovTarget = this.options.fovSpeedUp;\n this.speedUpTarget = this.options.speedUp;\n }\n\n onTouchEnd(ev) {\n if (this.options.onSlowDown) this.options.onSlowDown(ev);\n this.fovTarget = this.options.fov;\n this.speedUpTarget = 0;\n }\n\n onContextMenu(ev) {\n ev.preventDefault();\n }\n\n update(delta) {\n let lerpPercentage = Math.exp(-(-60 * Math.log2(1 - 0.1)) * delta);\n this.speedUp += lerp(this.speedUp, this.speedUpTarget, lerpPercentage, 0.00001);\n this.timeOffset += this.speedUp * delta;\n\n let time = this.clock.elapsedTime + this.timeOffset;\n\n this.rightCarLights.update(time);\n this.leftCarLights.update(time);\n this.leftSticks.update(time);\n this.road.update(time);\n\n let updateCamera = false;\n let fovChange = lerp(this.camera.fov, this.fovTarget, lerpPercentage);\n if (fovChange !== 0) {\n this.camera.fov += fovChange * delta * 6;\n updateCamera = true;\n }\n\n if (this.options.distortion.getJS) {\n const distortion = this.options.distortion.getJS(0.025, time);\n\n this.camera.lookAt(\n new THREE.Vector3(\n this.camera.position.x + distortion.x,\n this.camera.position.y + distortion.y,\n this.camera.position.z + distortion.z\n )\n );\n updateCamera = true;\n }\n if (updateCamera) {\n this.camera.updateProjectionMatrix();\n }\n }\n\n render(delta) {\n this.composer.render(delta);\n }\n\n dispose() {\n this.disposed = true;\n\n if (this.scene) {\n this.scene.traverse(object => {\n const obj = object;\n if (!obj.isMesh) return;\n\n if (obj.geometry) obj.geometry.dispose();\n\n if (obj.material) {\n if (Array.isArray(obj.material)) {\n obj.material.forEach(material => material.dispose());\n } else {\n obj.material.dispose();\n }\n }\n });\n this.scene.clear();\n }\n\n if (this.renderer) {\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n if (this.renderer.domElement && this.renderer.domElement.parentNode) {\n this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);\n }\n }\n if (this.composer) {\n this.composer.dispose();\n }\n\n window.removeEventListener('resize', this.onWindowResize);\n if (this.container) {\n this.container.removeEventListener('mousedown', this.onMouseDown);\n this.container.removeEventListener('mouseup', this.onMouseUp);\n this.container.removeEventListener('mouseout', this.onMouseUp);\n\n this.container.removeEventListener('touchstart', this.onTouchStart);\n this.container.removeEventListener('touchend', this.onTouchEnd);\n this.container.removeEventListener('touchcancel', this.onTouchEnd);\n this.container.removeEventListener('contextmenu', this.onContextMenu);\n }\n }\n\n setSize(width, height, updateStyles) {\n if (width <= 0 || height <= 0) {\n this.hasValidSize = false;\n return;\n }\n this.composer.setSize(width, height, updateStyles);\n this.hasValidSize = true;\n }\n\n tick() {\n if (this.disposed) return;\n\n if (!this.hasValidSize) {\n const w = this.container.offsetWidth;\n const h = this.container.offsetHeight;\n if (w > 0 && h > 0) {\n this.renderer.setSize(w, h, false);\n this.camera.aspect = w / h;\n this.camera.updateProjectionMatrix();\n this.composer.setSize(w, h);\n this.hasValidSize = true;\n } else {\n requestAnimationFrame(this.tick);\n return;\n }\n }\n\n if (resizeRendererToDisplaySize(this.renderer, this.setSize)) {\n const canvas = this.renderer.domElement;\n if (this.hasValidSize) {\n this.camera.aspect = canvas.clientWidth / canvas.clientHeight;\n this.camera.updateProjectionMatrix();\n }\n }\n\n if (this.hasValidSize) {\n const delta = this.clock.getDelta();\n this.render(delta);\n this.update(delta);\n }\n\n requestAnimationFrame(this.tick);\n }\n }\n\n const distortion_uniforms = {\n uDistortionX: { value: new THREE.Vector2(80, 3) },\n uDistortionY: { value: new THREE.Vector2(-40, 2.5) }\n };\n\n const distortion_vertex = `\n #define PI 3.14159265358979\n uniform vec2 uDistortionX;\n uniform vec2 uDistortionY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n vec3 getDistortion(float progress){\n progress = clamp(progress, 0., 1.);\n float xAmp = uDistortionX.r;\n float xFreq = uDistortionX.g;\n float yAmp = uDistortionY.r;\n float yFreq = uDistortionY.g;\n return vec3( \n xAmp * nsin(progress * PI * xFreq - PI / 2.),\n yAmp * nsin(progress * PI * yFreq - PI / 2.),\n 0.\n );\n }\n `;\n\n const random = base => {\n if (Array.isArray(base)) return Math.random() * (base[1] - base[0]) + base[0];\n return Math.random() * base;\n };\n\n const pickRandom = arr => {\n if (Array.isArray(arr)) return arr[Math.floor(Math.random() * arr.length)];\n return arr;\n };\n\n function lerp(current, target, speed = 0.1, limit = 0.001) {\n let change = (target - current) * speed;\n if (Math.abs(change) < limit) {\n change = target - current;\n }\n return change;\n }\n\n class CarLights {\n constructor(webgl, options, colors, speed, fade) {\n this.webgl = webgl;\n this.options = options;\n this.colors = colors;\n this.speed = speed;\n this.fade = fade;\n }\n\n init() {\n const options = this.options;\n let curve = new THREE.LineCurve3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -1));\n let geometry = new THREE.TubeGeometry(curve, 40, 1, 8, false);\n\n let instanced = new THREE.InstancedBufferGeometry().copy(geometry);\n instanced.instanceCount = options.lightPairsPerRoadWay * 2;\n\n let laneWidth = options.roadWidth / options.lanesPerRoad;\n\n let aOffset = [];\n let aMetrics = [];\n let aColor = [];\n\n let colors = this.colors;\n if (Array.isArray(colors)) {\n colors = colors.map(c => new THREE.Color(c));\n } else {\n colors = new THREE.Color(colors);\n }\n\n for (let i = 0; i < options.lightPairsPerRoadWay; i++) {\n let radius = random(options.carLightsRadius);\n let length = random(options.carLightsLength);\n let speed = random(this.speed);\n\n let carLane = i % options.lanesPerRoad;\n let laneX = carLane * laneWidth - options.roadWidth / 2 + laneWidth / 2;\n\n let carWidth = random(options.carWidthPercentage) * laneWidth;\n let carShiftX = random(options.carShiftX) * laneWidth;\n laneX += carShiftX;\n\n let offsetY = random(options.carFloorSeparation) + radius * 1.3;\n\n let offsetZ = -random(options.length);\n\n aOffset.push(laneX - carWidth / 2);\n aOffset.push(offsetY);\n aOffset.push(offsetZ);\n\n aOffset.push(laneX + carWidth / 2);\n aOffset.push(offsetY);\n aOffset.push(offsetZ);\n\n aMetrics.push(radius);\n aMetrics.push(length);\n aMetrics.push(speed);\n\n aMetrics.push(radius);\n aMetrics.push(length);\n aMetrics.push(speed);\n\n let color = pickRandom(colors);\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n }\n\n instanced.setAttribute('aOffset', new THREE.InstancedBufferAttribute(new Float32Array(aOffset), 3, false));\n instanced.setAttribute('aMetrics', new THREE.InstancedBufferAttribute(new Float32Array(aMetrics), 3, false));\n instanced.setAttribute('aColor', new THREE.InstancedBufferAttribute(new Float32Array(aColor), 3, false));\n\n let material = new THREE.ShaderMaterial({\n fragmentShader: carLightsFragment,\n vertexShader: carLightsVertex,\n transparent: true,\n uniforms: Object.assign(\n {\n uTime: { value: 0 },\n uTravelLength: { value: options.length },\n uFade: { value: this.fade }\n },\n this.webgl.fogUniforms,\n options.distortion.uniforms\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n options.distortion.getDistortion\n );\n };\n\n let mesh = new THREE.Mesh(instanced, material);\n mesh.frustumCulled = false;\n this.webgl.scene.add(mesh);\n this.mesh = mesh;\n }\n\n update(time) {\n this.mesh.material.uniforms.uTime.value = time;\n }\n }\n\n const carLightsFragment = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_fragment']}\n varying vec3 vColor;\n varying vec2 vUv; \n uniform vec2 uFade;\n void main() {\n vec3 color = vec3(vColor);\n float alpha = smoothstep(uFade.x, uFade.y, vUv.x);\n gl_FragColor = vec4(color, alpha);\n if (gl_FragColor.a < 0.0001) discard;\n ${THREE.ShaderChunk['fog_fragment']}\n }\n `;\n\n const carLightsVertex = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n attribute vec3 aOffset;\n attribute vec3 aMetrics;\n attribute vec3 aColor;\n uniform float uTravelLength;\n uniform float uTime;\n varying vec2 vUv; \n varying vec3 vColor; \n #include \n void main() {\n vec3 transformed = position.xyz;\n float radius = aMetrics.r;\n float myLength = aMetrics.g;\n float speed = aMetrics.b;\n\n transformed.xy *= radius;\n transformed.z *= myLength;\n\n transformed.z += myLength - mod(uTime * speed + aOffset.z, uTravelLength);\n transformed.xy += aOffset.xy;\n\n float progress = abs(transformed.z / uTravelLength);\n transformed.xyz += getDistortion(progress);\n\n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vUv = uv;\n vColor = aColor;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n `;\n\n class LightsSticks {\n constructor(webgl, options) {\n this.webgl = webgl;\n this.options = options;\n }\n\n init() {\n const options = this.options;\n const geometry = new THREE.PlaneGeometry(1, 1);\n let instanced = new THREE.InstancedBufferGeometry().copy(geometry);\n let totalSticks = options.totalSideLightSticks;\n instanced.instanceCount = totalSticks;\n\n let stickoffset = options.length / (totalSticks - 1);\n const aOffset = [];\n const aColor = [];\n const aMetrics = [];\n\n let colors = options.colors.sticks;\n if (Array.isArray(colors)) {\n colors = colors.map(c => new THREE.Color(c));\n } else {\n colors = new THREE.Color(colors);\n }\n\n for (let i = 0; i < totalSticks; i++) {\n let width = random(options.lightStickWidth);\n let height = random(options.lightStickHeight);\n aOffset.push((i - 1) * stickoffset * 2 + stickoffset * Math.random());\n\n let color = pickRandom(colors);\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n\n aMetrics.push(width);\n aMetrics.push(height);\n }\n\n instanced.setAttribute('aOffset', new THREE.InstancedBufferAttribute(new Float32Array(aOffset), 1, false));\n instanced.setAttribute('aColor', new THREE.InstancedBufferAttribute(new Float32Array(aColor), 3, false));\n instanced.setAttribute('aMetrics', new THREE.InstancedBufferAttribute(new Float32Array(aMetrics), 2, false));\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: sideSticksFragment,\n vertexShader: sideSticksVertex,\n side: THREE.DoubleSide,\n uniforms: Object.assign(\n {\n uTravelLength: { value: options.length },\n uTime: { value: 0 }\n },\n this.webgl.fogUniforms,\n options.distortion.uniforms\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n options.distortion.getDistortion\n );\n };\n\n const mesh = new THREE.Mesh(instanced, material);\n mesh.frustumCulled = false;\n this.webgl.scene.add(mesh);\n this.mesh = mesh;\n }\n\n update(time) {\n this.mesh.material.uniforms.uTime.value = time;\n }\n }\n\n const sideSticksVertex = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n attribute float aOffset;\n attribute vec3 aColor;\n attribute vec2 aMetrics;\n uniform float uTravelLength;\n uniform float uTime;\n varying vec3 vColor;\n mat4 rotationY( in float angle ) {\n return mat4(\tcos(angle),\t\t0,\t\tsin(angle),\t0,\n 0,\t\t1.0,\t\t\t 0,\t0,\n -sin(angle),\t0,\t\tcos(angle),\t0,\n 0, \t\t0,\t\t\t\t0,\t1);\n }\n #include \n void main(){\n vec3 transformed = position.xyz;\n float width = aMetrics.x;\n float height = aMetrics.y;\n\n transformed.xy *= vec2(width, height);\n float time = mod(uTime * 60. * 2. + aOffset, uTravelLength);\n\n transformed = (rotationY(3.14/2.) * vec4(transformed,1.)).xyz;\n\n transformed.z += - uTravelLength + time;\n\n float progress = abs(transformed.z / uTravelLength);\n transformed.xyz += getDistortion(progress);\n\n transformed.y += height / 2.;\n transformed.x += -width / 2.;\n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vColor = aColor;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n `;\n\n const sideSticksFragment = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_fragment']}\n varying vec3 vColor;\n void main(){\n vec3 color = vec3(vColor);\n gl_FragColor = vec4(color,1.);\n ${THREE.ShaderChunk['fog_fragment']}\n }\n `;\n\n class Road {\n constructor(webgl, options) {\n this.webgl = webgl;\n this.options = options;\n this.uTime = { value: 0 };\n }\n\n createPlane(side, width, isRoad) {\n const options = this.options;\n let segments = 100;\n const geometry = new THREE.PlaneGeometry(\n isRoad ? options.roadWidth : options.islandWidth,\n options.length,\n 20,\n segments\n );\n let uniforms = {\n uTravelLength: { value: options.length },\n uColor: { value: new THREE.Color(isRoad ? options.colors.roadColor : options.colors.islandColor) },\n uTime: this.uTime\n };\n\n if (isRoad) {\n uniforms = Object.assign(uniforms, {\n uLanes: { value: options.lanesPerRoad },\n uBrokenLinesColor: { value: new THREE.Color(options.colors.brokenLines) },\n uShoulderLinesColor: { value: new THREE.Color(options.colors.shoulderLines) },\n uShoulderLinesWidthPercentage: { value: options.shoulderLinesWidthPercentage },\n uBrokenLinesLengthPercentage: { value: options.brokenLinesLengthPercentage },\n uBrokenLinesWidthPercentage: { value: options.brokenLinesWidthPercentage }\n });\n }\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: isRoad ? roadFragment : islandFragment,\n vertexShader: roadVertex,\n side: THREE.DoubleSide,\n uniforms: Object.assign(uniforms, this.webgl.fogUniforms, options.distortion.uniforms)\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n options.distortion.getDistortion\n );\n };\n\n const mesh = new THREE.Mesh(geometry, material);\n mesh.rotation.x = -Math.PI / 2;\n mesh.position.z = -options.length / 2;\n mesh.position.x += (this.options.islandWidth / 2 + options.roadWidth / 2) * side;\n this.webgl.scene.add(mesh);\n\n return mesh;\n }\n\n init() {\n this.leftRoadWay = this.createPlane(-1, this.options.roadWidth, true);\n this.rightRoadWay = this.createPlane(1, this.options.roadWidth, true);\n this.island = this.createPlane(0, this.options.islandWidth, false);\n }\n\n update(time) {\n this.uTime.value = time;\n }\n }\n\n const roadBaseFragment = `\n #define USE_FOG;\n varying vec2 vUv; \n uniform vec3 uColor;\n uniform float uTime;\n #include \n ${THREE.ShaderChunk['fog_pars_fragment']}\n void main() {\n vec2 uv = vUv;\n vec3 color = vec3(uColor);\n #include \n gl_FragColor = vec4(color, 1.);\n ${THREE.ShaderChunk['fog_fragment']}\n }\n `;\n\n const islandFragment = roadBaseFragment\n .replace('#include ', '')\n .replace('#include ', '');\n\n const roadMarkings_vars = `\n uniform float uLanes;\n uniform vec3 uBrokenLinesColor;\n uniform vec3 uShoulderLinesColor;\n uniform float uShoulderLinesWidthPercentage;\n uniform float uBrokenLinesWidthPercentage;\n uniform float uBrokenLinesLengthPercentage;\n highp float random(vec2 co) {\n highp float a = 12.9898;\n highp float b = 78.233;\n highp float c = 43758.5453;\n highp float dt = dot(co.xy, vec2(a, b));\n highp float sn = mod(dt, 3.14);\n return fract(sin(sn) * c);\n }\n `;\n\n const roadMarkings_fragment = `\n uv.y = mod(uv.y + uTime * 0.05, 1.);\n float laneWidth = 1.0 / uLanes;\n float brokenLineWidth = laneWidth * uBrokenLinesWidthPercentage;\n float laneEmptySpace = 1. - uBrokenLinesLengthPercentage;\n\n float brokenLines = step(1.0 - brokenLineWidth, fract(uv.x * 2.0)) * step(laneEmptySpace, fract(uv.y * 10.0));\n float sideLines = step(1.0 - brokenLineWidth, fract((uv.x - laneWidth * (uLanes - 1.0)) * 2.0)) + step(brokenLineWidth, uv.x);\n\n brokenLines = mix(brokenLines, sideLines, uv.x);\n `;\n\n const roadFragment = roadBaseFragment\n .replace('#include ', roadMarkings_fragment)\n .replace('#include ', roadMarkings_vars);\n\n const roadVertex = `\n #define USE_FOG;\n uniform float uTime;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n uniform float uTravelLength;\n varying vec2 vUv; \n #include \n void main() {\n vec3 transformed = position.xyz;\n vec3 distortion = getDistortion((transformed.y + uTravelLength / 2.) / uTravelLength);\n transformed.x += distortion.x;\n transformed.z += distortion.y;\n transformed.y += -1. * distortion.z; \n \n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vUv = uv;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n `;\n\n function resizeRendererToDisplaySize(renderer, setSize) {\n const canvas = renderer.domElement;\n const width = canvas.clientWidth;\n const height = canvas.clientHeight;\n if (width <= 0 || height <= 0) return false;\n const needResize = canvas.width !== width || canvas.height !== height;\n if (needResize) {\n setSize(width, height, false);\n }\n return needResize;\n }\n\n const container = hyperspeed.current;\n if (!container) return;\n\n const options = {\n ...DEFAULT_EFFECT_OPTIONS,\n ...effectOptions,\n colors: { ...DEFAULT_EFFECT_OPTIONS.colors, ...effectOptions.colors }\n };\n options.distortion = distortions[options.distortion];\n\n const myApp = new App(container, options);\n appRef.current = myApp;\n myApp.loadAssets().then(myApp.init);\n\n return () => {\n if (appRef.current) {\n appRef.current.dispose();\n appRef.current = null;\n }\n };\n }, [effectOptions]);\n\n return
;\n};\n\nexport default Hyperspeed;\n" } ], diff --git a/public/r/Hyperspeed-JS-TW.json b/public/r/Hyperspeed-JS-TW.json index cd676f5e..3fd98e9e 100644 --- a/public/r/Hyperspeed-JS-TW.json +++ b/public/r/Hyperspeed-JS-TW.json @@ -7,13 +7,13 @@ "files": [ { "type": "registry:component", - "path": "Hyperspeed/HyperSpeedPresets.js", - "content": "export const hyperspeedPresets = {\n one: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 20,\n lightPairsPerRoadWay: 40,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.03, 400 * 0.2],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.8, 0.8],\n carFloorSeparation: [0, 5],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xd856bf, 0x6750a2, 0xc247ac],\n rightCars: [0x03b3c3, 0x0e5ea5, 0x324555],\n sticks: 0x03b3c3\n }\n },\n two: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'mountainDistortion',\n length: 400,\n roadWidth: 9,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 50,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xff102a, 0xeb383e, 0xff102a],\n rightCars: [0xdadafa, 0xbebae3, 0x8f97e4],\n sticks: 0xdadafa\n }\n },\n three: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'xyDistortion',\n length: 400,\n roadWidth: 9,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 3,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 30,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.02, 0.05],\n lightStickHeight: [0.3, 0.7],\n movingAwaySpeed: [20, 50],\n movingCloserSpeed: [-150, -230],\n carLightsLength: [400 * 0.05, 400 * 0.2],\n carLightsRadius: [0.03, 0.08],\n carWidthPercentage: [0.1, 0.5],\n carShiftX: [-0.5, 0.5],\n carFloorSeparation: [0, 0.1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0x7d0d1b, 0xa90519, 0xff102a],\n rightCars: [0xf1eece, 0xe6e2b1, 0xdfd98a],\n sticks: 0xf1eece\n }\n },\n four: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'LongRaceDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 5,\n lanesPerRoad: 2,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 70,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xff5f73, 0xe74d60, 0xff102a],\n rightCars: [0xa4e3e6, 0x80d1d4, 0x53c2c6],\n sticks: 0xa4e3e6\n }\n },\n five: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 9,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 50,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xdc5b20, 0xdca320, 0xdc2020],\n rightCars: [0x334bf7, 0xe5e6ed, 0xbfc6f3],\n sticks: 0xc5e8eb\n }\n },\n six: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'deepDistortion',\n length: 400,\n roadWidth: 18,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 50,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xff322f, 0xa33010, 0xa81508],\n rightCars: [0xfdfdf0, 0xf3dea0, 0xe2bb88],\n sticks: 0xfdfdf0\n }\n }\n};\n" + "path": "Hyperspeed/Hyperspeed.jsx", + "content": "import { BloomEffect, EffectComposer, EffectPass, RenderPass, SMAAEffect, SMAAPreset } from 'postprocessing';\nimport { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\nconst DEFAULT_EFFECT_OPTIONS = {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 2,\n lanesPerRoad: 4,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 20,\n lightPairsPerRoadWay: 40,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.03, 400 * 0.2],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.8, 0.8],\n carFloorSeparation: [0, 5],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0xffffff,\n brokenLines: 0xffffff,\n leftCars: [0xd856bf, 0x6750a2, 0xc247ac],\n rightCars: [0x03b3c3, 0x0e5ea5, 0x324555],\n sticks: 0x03b3c3\n }\n};\n\nconst Hyperspeed = ({ effectOptions = DEFAULT_EFFECT_OPTIONS }) => {\n const hyperspeed = useRef(null);\n const appRef = useRef(null);\n\n useEffect(() => {\n if (appRef.current) {\n appRef.current.dispose();\n appRef.current = null;\n const container = hyperspeed.current;\n if (container) {\n while (container.firstChild) {\n container.removeChild(container.firstChild);\n }\n }\n }\n\n const mountainUniforms = {\n uFreq: { value: new THREE.Vector3(3, 6, 10) },\n uAmp: { value: new THREE.Vector3(30, 30, 20) }\n };\n\n const xyUniforms = {\n uFreq: { value: new THREE.Vector2(5, 2) },\n uAmp: { value: new THREE.Vector2(25, 15) }\n };\n\n const LongRaceUniforms = {\n uFreq: { value: new THREE.Vector2(2, 3) },\n uAmp: { value: new THREE.Vector2(35, 10) }\n };\n\n const turbulentUniforms = {\n uFreq: { value: new THREE.Vector4(4, 8, 8, 1) },\n uAmp: { value: new THREE.Vector4(25, 5, 10, 10) }\n };\n\n const deepUniforms = {\n uFreq: { value: new THREE.Vector2(4, 8) },\n uAmp: { value: new THREE.Vector2(10, 20) },\n uPowY: { value: new THREE.Vector2(20, 2) }\n };\n\n let nsin = val => Math.sin(val) * 0.5 + 0.5;\n\n const distortions = {\n mountainDistortion: {\n uniforms: mountainUniforms,\n getDistortion: `\n uniform vec3 uAmp;\n uniform vec3 uFreq;\n #define PI 3.14159265358979\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n vec3 getDistortion(float progress){\n float movementProgressFix = 0.02;\n return vec3( \n cos(progress * PI * uFreq.x + uTime) * uAmp.x - cos(movementProgressFix * PI * uFreq.x + uTime) * uAmp.x,\n nsin(progress * PI * uFreq.y + uTime) * uAmp.y - nsin(movementProgressFix * PI * uFreq.y + uTime) * uAmp.y,\n nsin(progress * PI * uFreq.z + uTime) * uAmp.z - nsin(movementProgressFix * PI * uFreq.z + uTime) * uAmp.z\n );\n }\n `,\n getJS: (progress, time) => {\n let movementProgressFix = 0.02;\n let uFreq = mountainUniforms.uFreq.value;\n let uAmp = mountainUniforms.uAmp.value;\n let distortion = new THREE.Vector3(\n Math.cos(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.cos(movementProgressFix * Math.PI * uFreq.x + time) * uAmp.x,\n nsin(progress * Math.PI * uFreq.y + time) * uAmp.y -\n nsin(movementProgressFix * Math.PI * uFreq.y + time) * uAmp.y,\n nsin(progress * Math.PI * uFreq.z + time) * uAmp.z -\n nsin(movementProgressFix * Math.PI * uFreq.z + time) * uAmp.z\n );\n let lookAtAmp = new THREE.Vector3(2, 2, 2);\n let lookAtOffset = new THREE.Vector3(0, 0, -5);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n xyDistortion: {\n uniforms: xyUniforms,\n getDistortion: `\n uniform vec2 uFreq;\n uniform vec2 uAmp;\n #define PI 3.14159265358979\n vec3 getDistortion(float progress){\n float movementProgressFix = 0.02;\n return vec3( \n cos(progress * PI * uFreq.x + uTime) * uAmp.x - cos(movementProgressFix * PI * uFreq.x + uTime) * uAmp.x,\n sin(progress * PI * uFreq.y + PI/2. + uTime) * uAmp.y - sin(movementProgressFix * PI * uFreq.y + PI/2. + uTime) * uAmp.y,\n 0.\n );\n }\n `,\n getJS: (progress, time) => {\n let movementProgressFix = 0.02;\n let uFreq = xyUniforms.uFreq.value;\n let uAmp = xyUniforms.uAmp.value;\n let distortion = new THREE.Vector3(\n Math.cos(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.cos(movementProgressFix * Math.PI * uFreq.x + time) * uAmp.x,\n Math.sin(progress * Math.PI * uFreq.y + time + Math.PI / 2) * uAmp.y -\n Math.sin(movementProgressFix * Math.PI * uFreq.y + time + Math.PI / 2) * uAmp.y,\n 0\n );\n let lookAtAmp = new THREE.Vector3(2, 0.4, 1);\n let lookAtOffset = new THREE.Vector3(0, 0, -3);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n LongRaceDistortion: {\n uniforms: LongRaceUniforms,\n getDistortion: `\n uniform vec2 uFreq;\n uniform vec2 uAmp;\n #define PI 3.14159265358979\n vec3 getDistortion(float progress){\n float camProgress = 0.0125;\n return vec3( \n sin(progress * PI * uFreq.x + uTime) * uAmp.x - sin(camProgress * PI * uFreq.x + uTime) * uAmp.x,\n sin(progress * PI * uFreq.y + uTime) * uAmp.y - sin(camProgress * PI * uFreq.y + uTime) * uAmp.y,\n 0.\n );\n }\n `,\n getJS: (progress, time) => {\n let camProgress = 0.0125;\n let uFreq = LongRaceUniforms.uFreq.value;\n let uAmp = LongRaceUniforms.uAmp.value;\n let distortion = new THREE.Vector3(\n Math.sin(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.sin(camProgress * Math.PI * uFreq.x + time) * uAmp.x,\n Math.sin(progress * Math.PI * uFreq.y + time) * uAmp.y -\n Math.sin(camProgress * Math.PI * uFreq.y + time) * uAmp.y,\n 0\n );\n let lookAtAmp = new THREE.Vector3(1, 1, 0);\n let lookAtOffset = new THREE.Vector3(0, 0, -5);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n turbulentDistortion: {\n uniforms: turbulentUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n cos(PI * progress * uFreq.r + uTime) * uAmp.r +\n pow(cos(PI * progress * uFreq.g + uTime * (uFreq.g / uFreq.r)), 2. ) * uAmp.g\n );\n }\n float getDistortionY(float progress){\n return (\n -nsin(PI * progress * uFreq.b + uTime) * uAmp.b +\n -pow(nsin(PI * progress * uFreq.a + uTime / (uFreq.b / uFreq.a)), 5.) * uAmp.a\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.0125),\n getDistortionY(progress) - getDistortionY(0.0125),\n 0.\n );\n }\n `,\n getJS: (progress, time) => {\n const uFreq = turbulentUniforms.uFreq.value;\n const uAmp = turbulentUniforms.uAmp.value;\n\n const getX = p =>\n Math.cos(Math.PI * p * uFreq.x + time) * uAmp.x +\n Math.pow(Math.cos(Math.PI * p * uFreq.y + time * (uFreq.y / uFreq.x)), 2) * uAmp.y;\n\n const getY = p =>\n -nsin(Math.PI * p * uFreq.z + time) * uAmp.z -\n Math.pow(nsin(Math.PI * p * uFreq.w + time / (uFreq.z / uFreq.w)), 5) * uAmp.w;\n\n let distortion = new THREE.Vector3(\n getX(progress) - getX(progress + 0.007),\n getY(progress) - getY(progress + 0.007),\n 0\n );\n let lookAtAmp = new THREE.Vector3(-2, -5, 0);\n let lookAtOffset = new THREE.Vector3(0, 0, -10);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n turbulentDistortionStill: {\n uniforms: turbulentUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n cos(PI * progress * uFreq.r) * uAmp.r +\n pow(cos(PI * progress * uFreq.g * (uFreq.g / uFreq.r)), 2. ) * uAmp.g\n );\n }\n float getDistortionY(float progress){\n return (\n -nsin(PI * progress * uFreq.b) * uAmp.b +\n -pow(nsin(PI * progress * uFreq.a / (uFreq.b / uFreq.a)), 5.) * uAmp.a\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.02),\n 0.\n );\n }\n `\n },\n deepDistortionStill: {\n uniforms: deepUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n uniform vec2 uPowY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n sin(progress * PI * uFreq.x) * uAmp.x * 2.\n );\n }\n float getDistortionY(float progress){\n return (\n pow(abs(progress * uPowY.x), uPowY.y) + sin(progress * PI * uFreq.y) * uAmp.y\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.05),\n 0.\n );\n }\n `\n },\n deepDistortion: {\n uniforms: deepUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n uniform vec2 uPowY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n sin(progress * PI * uFreq.x + uTime) * uAmp.x\n );\n }\n float getDistortionY(float progress){\n return (\n pow(abs(progress * uPowY.x), uPowY.y) + sin(progress * PI * uFreq.y + uTime) * uAmp.y\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.02),\n 0.\n );\n }\n `,\n getJS: (progress, time) => {\n const uFreq = deepUniforms.uFreq.value;\n const uAmp = deepUniforms.uAmp.value;\n const uPowY = deepUniforms.uPowY.value;\n\n const getX = p => Math.sin(p * Math.PI * uFreq.x + time) * uAmp.x;\n const getY = p => Math.pow(p * uPowY.x, uPowY.y) + Math.sin(p * Math.PI * uFreq.y + time) * uAmp.y;\n\n let distortion = new THREE.Vector3(\n getX(progress) - getX(progress + 0.01),\n getY(progress) - getY(progress + 0.01),\n 0\n );\n let lookAtAmp = new THREE.Vector3(-2, -4, 0);\n let lookAtOffset = new THREE.Vector3(0, 0, -10);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n }\n };\n\n class App {\n constructor(container, options = {}) {\n this.options = options;\n if (this.options.distortion == null) {\n this.options.distortion = {\n uniforms: distortion_uniforms,\n getDistortion: distortion_vertex\n };\n }\n this.container = container;\n this.hasValidSize = false;\n\n const initW = Math.max(1, container.offsetWidth);\n const initH = Math.max(1, container.offsetHeight);\n\n this.renderer = new THREE.WebGLRenderer({\n antialias: false,\n alpha: true\n });\n this.renderer.setSize(initW, initH, false);\n this.renderer.setPixelRatio(window.devicePixelRatio);\n this.composer = new EffectComposer(this.renderer);\n container.append(this.renderer.domElement);\n\n this.camera = new THREE.PerspectiveCamera(options.fov, initW / initH, 0.1, 10000);\n this.camera.position.z = -5;\n this.camera.position.y = 8;\n this.camera.position.x = 0;\n this.scene = new THREE.Scene();\n this.scene.background = null;\n\n let fog = new THREE.Fog(options.colors.background, options.length * 0.2, options.length * 500);\n this.scene.fog = fog;\n this.fogUniforms = {\n fogColor: { value: fog.color },\n fogNear: { value: fog.near },\n fogFar: { value: fog.far }\n };\n this.clock = new THREE.Clock();\n this.assets = {};\n this.disposed = false;\n\n this.road = new Road(this, options);\n this.leftCarLights = new CarLights(\n this,\n options,\n options.colors.leftCars,\n options.movingAwaySpeed,\n new THREE.Vector2(0, 1 - options.carLightsFade)\n );\n this.rightCarLights = new CarLights(\n this,\n options,\n options.colors.rightCars,\n options.movingCloserSpeed,\n new THREE.Vector2(1, 0 + options.carLightsFade)\n );\n this.leftSticks = new LightsSticks(this, options);\n\n this.fovTarget = options.fov;\n this.speedUpTarget = 0;\n this.speedUp = 0;\n this.timeOffset = 0;\n\n this.tick = this.tick.bind(this);\n this.init = this.init.bind(this);\n this.setSize = this.setSize.bind(this);\n this.onMouseDown = this.onMouseDown.bind(this);\n this.onMouseUp = this.onMouseUp.bind(this);\n\n this.onTouchStart = this.onTouchStart.bind(this);\n this.onTouchEnd = this.onTouchEnd.bind(this);\n this.onContextMenu = this.onContextMenu.bind(this);\n\n this.onWindowResize = this.onWindowResize.bind(this);\n window.addEventListener('resize', this.onWindowResize);\n\n if (container.offsetWidth > 0 && container.offsetHeight > 0) {\n this.hasValidSize = true;\n }\n }\n\n onWindowResize() {\n const width = this.container.offsetWidth;\n const height = this.container.offsetHeight;\n\n if (width <= 0 || height <= 0) {\n this.hasValidSize = false;\n return;\n }\n\n this.renderer.setSize(width, height);\n this.camera.aspect = width / height;\n this.camera.updateProjectionMatrix();\n this.composer.setSize(width, height);\n this.hasValidSize = true;\n }\n\n initPasses() {\n this.renderPass = new RenderPass(this.scene, this.camera);\n this.bloomPass = new EffectPass(\n this.camera,\n new BloomEffect({\n luminanceThreshold: 0.2,\n luminanceSmoothing: 0,\n resolutionScale: 1\n })\n );\n\n const smaaPass = new EffectPass(\n this.camera,\n new SMAAEffect({\n preset: SMAAPreset.MEDIUM,\n searchImage: SMAAEffect.searchImageDataURL,\n areaImage: SMAAEffect.areaImageDataURL\n })\n );\n this.renderPass.renderToScreen = false;\n this.bloomPass.renderToScreen = false;\n smaaPass.renderToScreen = true;\n this.composer.addPass(this.renderPass);\n this.composer.addPass(this.bloomPass);\n this.composer.addPass(smaaPass);\n }\n\n loadAssets() {\n const assets = this.assets;\n return new Promise(resolve => {\n const manager = new THREE.LoadingManager(resolve);\n\n const searchImage = new Image();\n const areaImage = new Image();\n assets.smaa = {};\n searchImage.addEventListener('load', function () {\n assets.smaa.search = this;\n manager.itemEnd('smaa-search');\n });\n\n areaImage.addEventListener('load', function () {\n assets.smaa.area = this;\n manager.itemEnd('smaa-area');\n });\n manager.itemStart('smaa-search');\n manager.itemStart('smaa-area');\n\n searchImage.src = SMAAEffect.searchImageDataURL;\n areaImage.src = SMAAEffect.areaImageDataURL;\n });\n }\n\n init() {\n this.initPasses();\n const options = this.options;\n this.road.init();\n this.leftCarLights.init();\n\n this.leftCarLights.mesh.position.setX(-options.roadWidth / 2 - options.islandWidth / 2);\n this.rightCarLights.init();\n this.rightCarLights.mesh.position.setX(options.roadWidth / 2 + options.islandWidth / 2);\n this.leftSticks.init();\n this.leftSticks.mesh.position.setX(-(options.roadWidth + options.islandWidth / 2));\n\n this.container.addEventListener('mousedown', this.onMouseDown);\n this.container.addEventListener('mouseup', this.onMouseUp);\n this.container.addEventListener('mouseout', this.onMouseUp);\n\n this.container.addEventListener('touchstart', this.onTouchStart, { passive: true });\n this.container.addEventListener('touchend', this.onTouchEnd, { passive: true });\n this.container.addEventListener('touchcancel', this.onTouchEnd, { passive: true });\n this.container.addEventListener('contextmenu', this.onContextMenu);\n\n this.tick();\n }\n\n onMouseDown(ev) {\n if (this.options.onSpeedUp) this.options.onSpeedUp(ev);\n this.fovTarget = this.options.fovSpeedUp;\n this.speedUpTarget = this.options.speedUp;\n }\n\n onMouseUp(ev) {\n if (this.options.onSlowDown) this.options.onSlowDown(ev);\n this.fovTarget = this.options.fov;\n this.speedUpTarget = 0;\n }\n\n onTouchStart(ev) {\n if (this.options.onSpeedUp) this.options.onSpeedUp(ev);\n this.fovTarget = this.options.fovSpeedUp;\n this.speedUpTarget = this.options.speedUp;\n }\n\n onTouchEnd(ev) {\n if (this.options.onSlowDown) this.options.onSlowDown(ev);\n this.fovTarget = this.options.fov;\n this.speedUpTarget = 0;\n }\n\n onContextMenu(ev) {\n ev.preventDefault();\n }\n\n update(delta) {\n let lerpPercentage = Math.exp(-(-60 * Math.log2(1 - 0.1)) * delta);\n this.speedUp += lerp(this.speedUp, this.speedUpTarget, lerpPercentage, 0.00001);\n this.timeOffset += this.speedUp * delta;\n\n let time = this.clock.elapsedTime + this.timeOffset;\n\n this.rightCarLights.update(time);\n this.leftCarLights.update(time);\n this.leftSticks.update(time);\n this.road.update(time);\n\n let updateCamera = false;\n let fovChange = lerp(this.camera.fov, this.fovTarget, lerpPercentage);\n if (fovChange !== 0) {\n this.camera.fov += fovChange * delta * 6;\n updateCamera = true;\n }\n\n if (this.options.distortion.getJS) {\n const distortion = this.options.distortion.getJS(0.025, time);\n\n this.camera.lookAt(\n new THREE.Vector3(\n this.camera.position.x + distortion.x,\n this.camera.position.y + distortion.y,\n this.camera.position.z + distortion.z\n )\n );\n updateCamera = true;\n }\n if (updateCamera) {\n this.camera.updateProjectionMatrix();\n }\n }\n\n render(delta) {\n this.composer.render(delta);\n }\n\n dispose() {\n this.disposed = true;\n\n if (this.scene) {\n this.scene.traverse(object => {\n const obj = object;\n if (!obj.isMesh) return;\n\n if (obj.geometry) obj.geometry.dispose();\n\n if (obj.material) {\n if (Array.isArray(obj.material)) {\n obj.material.forEach(material => material.dispose());\n } else {\n obj.material.dispose();\n }\n }\n });\n this.scene.clear();\n }\n\n if (this.renderer) {\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n if (this.renderer.domElement && this.renderer.domElement.parentNode) {\n this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);\n }\n }\n if (this.composer) {\n this.composer.dispose();\n }\n\n window.removeEventListener('resize', this.onWindowResize);\n if (this.container) {\n this.container.removeEventListener('mousedown', this.onMouseDown);\n this.container.removeEventListener('mouseup', this.onMouseUp);\n this.container.removeEventListener('mouseout', this.onMouseUp);\n\n this.container.removeEventListener('touchstart', this.onTouchStart);\n this.container.removeEventListener('touchend', this.onTouchEnd);\n this.container.removeEventListener('touchcancel', this.onTouchEnd);\n this.container.removeEventListener('contextmenu', this.onContextMenu);\n }\n }\n\n setSize(width, height, updateStyles) {\n if (width <= 0 || height <= 0) {\n this.hasValidSize = false;\n return;\n }\n this.composer.setSize(width, height, updateStyles);\n this.hasValidSize = true;\n }\n\n tick() {\n if (this.disposed) return;\n\n if (!this.hasValidSize) {\n const w = this.container.offsetWidth;\n const h = this.container.offsetHeight;\n if (w > 0 && h > 0) {\n this.renderer.setSize(w, h, false);\n this.camera.aspect = w / h;\n this.camera.updateProjectionMatrix();\n this.composer.setSize(w, h);\n this.hasValidSize = true;\n } else {\n requestAnimationFrame(this.tick);\n return;\n }\n }\n\n if (resizeRendererToDisplaySize(this.renderer, this.setSize)) {\n const canvas = this.renderer.domElement;\n if (this.hasValidSize) {\n this.camera.aspect = canvas.clientWidth / canvas.clientHeight;\n this.camera.updateProjectionMatrix();\n }\n }\n\n if (this.hasValidSize) {\n const delta = this.clock.getDelta();\n this.render(delta);\n this.update(delta);\n }\n\n requestAnimationFrame(this.tick);\n }\n }\n\n const distortion_uniforms = {\n uDistortionX: { value: new THREE.Vector2(80, 3) },\n uDistortionY: { value: new THREE.Vector2(-40, 2.5) }\n };\n\n const distortion_vertex = `\n #define PI 3.14159265358979\n uniform vec2 uDistortionX;\n uniform vec2 uDistortionY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n vec3 getDistortion(float progress){\n progress = clamp(progress, 0., 1.);\n float xAmp = uDistortionX.r;\n float xFreq = uDistortionX.g;\n float yAmp = uDistortionY.r;\n float yFreq = uDistortionY.g;\n return vec3( \n xAmp * nsin(progress * PI * xFreq - PI / 2.),\n yAmp * nsin(progress * PI * yFreq - PI / 2.),\n 0.\n );\n }\n `;\n\n const random = base => {\n if (Array.isArray(base)) return Math.random() * (base[1] - base[0]) + base[0];\n return Math.random() * base;\n };\n\n const pickRandom = arr => {\n if (Array.isArray(arr)) return arr[Math.floor(Math.random() * arr.length)];\n return arr;\n };\n\n function lerp(current, target, speed = 0.1, limit = 0.001) {\n let change = (target - current) * speed;\n if (Math.abs(change) < limit) {\n change = target - current;\n }\n return change;\n }\n\n class CarLights {\n constructor(webgl, options, colors, speed, fade) {\n this.webgl = webgl;\n this.options = options;\n this.colors = colors;\n this.speed = speed;\n this.fade = fade;\n }\n\n init() {\n const options = this.options;\n let curve = new THREE.LineCurve3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -1));\n let geometry = new THREE.TubeGeometry(curve, 40, 1, 8, false);\n\n let instanced = new THREE.InstancedBufferGeometry().copy(geometry);\n instanced.instanceCount = options.lightPairsPerRoadWay * 2;\n\n let laneWidth = options.roadWidth / options.lanesPerRoad;\n\n let aOffset = [];\n let aMetrics = [];\n let aColor = [];\n\n let colors = this.colors;\n if (Array.isArray(colors)) {\n colors = colors.map(c => new THREE.Color(c));\n } else {\n colors = new THREE.Color(colors);\n }\n\n for (let i = 0; i < options.lightPairsPerRoadWay; i++) {\n let radius = random(options.carLightsRadius);\n let length = random(options.carLightsLength);\n let speed = random(this.speed);\n\n let carLane = i % options.lanesPerRoad;\n let laneX = carLane * laneWidth - options.roadWidth / 2 + laneWidth / 2;\n\n let carWidth = random(options.carWidthPercentage) * laneWidth;\n let carShiftX = random(options.carShiftX) * laneWidth;\n laneX += carShiftX;\n\n let offsetY = random(options.carFloorSeparation) + radius * 1.3;\n\n let offsetZ = -random(options.length);\n\n aOffset.push(laneX - carWidth / 2);\n aOffset.push(offsetY);\n aOffset.push(offsetZ);\n\n aOffset.push(laneX + carWidth / 2);\n aOffset.push(offsetY);\n aOffset.push(offsetZ);\n\n aMetrics.push(radius);\n aMetrics.push(length);\n aMetrics.push(speed);\n\n aMetrics.push(radius);\n aMetrics.push(length);\n aMetrics.push(speed);\n\n let color = pickRandom(colors);\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n }\n\n instanced.setAttribute('aOffset', new THREE.InstancedBufferAttribute(new Float32Array(aOffset), 3, false));\n instanced.setAttribute('aMetrics', new THREE.InstancedBufferAttribute(new Float32Array(aMetrics), 3, false));\n instanced.setAttribute('aColor', new THREE.InstancedBufferAttribute(new Float32Array(aColor), 3, false));\n\n let material = new THREE.ShaderMaterial({\n fragmentShader: carLightsFragment,\n vertexShader: carLightsVertex,\n transparent: true,\n uniforms: Object.assign(\n {\n uTime: { value: 0 },\n uTravelLength: { value: options.length },\n uFade: { value: this.fade }\n },\n this.webgl.fogUniforms,\n options.distortion.uniforms\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n options.distortion.getDistortion\n );\n };\n\n let mesh = new THREE.Mesh(instanced, material);\n mesh.frustumCulled = false;\n this.webgl.scene.add(mesh);\n this.mesh = mesh;\n }\n\n update(time) {\n this.mesh.material.uniforms.uTime.value = time;\n }\n }\n\n const carLightsFragment = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_fragment']}\n varying vec3 vColor;\n varying vec2 vUv; \n uniform vec2 uFade;\n void main() {\n vec3 color = vec3(vColor);\n float alpha = smoothstep(uFade.x, uFade.y, vUv.x);\n gl_FragColor = vec4(color, alpha);\n if (gl_FragColor.a < 0.0001) discard;\n ${THREE.ShaderChunk['fog_fragment']}\n }\n `;\n\n const carLightsVertex = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n attribute vec3 aOffset;\n attribute vec3 aMetrics;\n attribute vec3 aColor;\n uniform float uTravelLength;\n uniform float uTime;\n varying vec2 vUv; \n varying vec3 vColor; \n #include \n void main() {\n vec3 transformed = position.xyz;\n float radius = aMetrics.r;\n float myLength = aMetrics.g;\n float speed = aMetrics.b;\n\n transformed.xy *= radius;\n transformed.z *= myLength;\n\n transformed.z += myLength - mod(uTime * speed + aOffset.z, uTravelLength);\n transformed.xy += aOffset.xy;\n\n float progress = abs(transformed.z / uTravelLength);\n transformed.xyz += getDistortion(progress);\n\n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vUv = uv;\n vColor = aColor;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n `;\n\n class LightsSticks {\n constructor(webgl, options) {\n this.webgl = webgl;\n this.options = options;\n }\n\n init() {\n const options = this.options;\n const geometry = new THREE.PlaneGeometry(1, 1);\n let instanced = new THREE.InstancedBufferGeometry().copy(geometry);\n let totalSticks = options.totalSideLightSticks;\n instanced.instanceCount = totalSticks;\n\n let stickoffset = options.length / (totalSticks - 1);\n const aOffset = [];\n const aColor = [];\n const aMetrics = [];\n\n let colors = options.colors.sticks;\n if (Array.isArray(colors)) {\n colors = colors.map(c => new THREE.Color(c));\n } else {\n colors = new THREE.Color(colors);\n }\n\n for (let i = 0; i < totalSticks; i++) {\n let width = random(options.lightStickWidth);\n let height = random(options.lightStickHeight);\n aOffset.push((i - 1) * stickoffset * 2 + stickoffset * Math.random());\n\n let color = pickRandom(colors);\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n\n aMetrics.push(width);\n aMetrics.push(height);\n }\n\n instanced.setAttribute('aOffset', new THREE.InstancedBufferAttribute(new Float32Array(aOffset), 1, false));\n instanced.setAttribute('aColor', new THREE.InstancedBufferAttribute(new Float32Array(aColor), 3, false));\n instanced.setAttribute('aMetrics', new THREE.InstancedBufferAttribute(new Float32Array(aMetrics), 2, false));\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: sideSticksFragment,\n vertexShader: sideSticksVertex,\n side: THREE.DoubleSide,\n uniforms: Object.assign(\n {\n uTravelLength: { value: options.length },\n uTime: { value: 0 }\n },\n this.webgl.fogUniforms,\n options.distortion.uniforms\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n options.distortion.getDistortion\n );\n };\n\n const mesh = new THREE.Mesh(instanced, material);\n mesh.frustumCulled = false;\n this.webgl.scene.add(mesh);\n this.mesh = mesh;\n }\n\n update(time) {\n this.mesh.material.uniforms.uTime.value = time;\n }\n }\n\n const sideSticksVertex = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n attribute float aOffset;\n attribute vec3 aColor;\n attribute vec2 aMetrics;\n uniform float uTravelLength;\n uniform float uTime;\n varying vec3 vColor;\n mat4 rotationY( in float angle ) {\n return mat4(\tcos(angle),\t\t0,\t\tsin(angle),\t0,\n 0,\t\t1.0,\t\t\t 0,\t0,\n -sin(angle),\t0,\t\tcos(angle),\t0,\n 0, \t\t0,\t\t\t\t0,\t1);\n }\n #include \n void main(){\n vec3 transformed = position.xyz;\n float width = aMetrics.x;\n float height = aMetrics.y;\n\n transformed.xy *= vec2(width, height);\n float time = mod(uTime * 60. * 2. + aOffset, uTravelLength);\n\n transformed = (rotationY(3.14/2.) * vec4(transformed,1.)).xyz;\n\n transformed.z += - uTravelLength + time;\n\n float progress = abs(transformed.z / uTravelLength);\n transformed.xyz += getDistortion(progress);\n\n transformed.y += height / 2.;\n transformed.x += -width / 2.;\n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vColor = aColor;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n `;\n\n const sideSticksFragment = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_fragment']}\n varying vec3 vColor;\n void main(){\n vec3 color = vec3(vColor);\n gl_FragColor = vec4(color,1.);\n ${THREE.ShaderChunk['fog_fragment']}\n }\n `;\n\n class Road {\n constructor(webgl, options) {\n this.webgl = webgl;\n this.options = options;\n this.uTime = { value: 0 };\n }\n\n createPlane(side, width, isRoad) {\n const options = this.options;\n let segments = 100;\n const geometry = new THREE.PlaneGeometry(\n isRoad ? options.roadWidth : options.islandWidth,\n options.length,\n 20,\n segments\n );\n let uniforms = {\n uTravelLength: { value: options.length },\n uColor: { value: new THREE.Color(isRoad ? options.colors.roadColor : options.colors.islandColor) },\n uTime: this.uTime\n };\n\n if (isRoad) {\n uniforms = Object.assign(uniforms, {\n uLanes: { value: options.lanesPerRoad },\n uBrokenLinesColor: { value: new THREE.Color(options.colors.brokenLines) },\n uShoulderLinesColor: { value: new THREE.Color(options.colors.shoulderLines) },\n uShoulderLinesWidthPercentage: { value: options.shoulderLinesWidthPercentage },\n uBrokenLinesLengthPercentage: { value: options.brokenLinesLengthPercentage },\n uBrokenLinesWidthPercentage: { value: options.brokenLinesWidthPercentage }\n });\n }\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: isRoad ? roadFragment : islandFragment,\n vertexShader: roadVertex,\n side: THREE.DoubleSide,\n uniforms: Object.assign(uniforms, this.webgl.fogUniforms, options.distortion.uniforms)\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n options.distortion.getDistortion\n );\n };\n\n const mesh = new THREE.Mesh(geometry, material);\n mesh.rotation.x = -Math.PI / 2;\n mesh.position.z = -options.length / 2;\n mesh.position.x += (this.options.islandWidth / 2 + options.roadWidth / 2) * side;\n this.webgl.scene.add(mesh);\n\n return mesh;\n }\n\n init() {\n this.leftRoadWay = this.createPlane(-1, this.options.roadWidth, true);\n this.rightRoadWay = this.createPlane(1, this.options.roadWidth, true);\n this.island = this.createPlane(0, this.options.islandWidth, false);\n }\n\n update(time) {\n this.uTime.value = time;\n }\n }\n\n const roadBaseFragment = `\n #define USE_FOG;\n varying vec2 vUv; \n uniform vec3 uColor;\n uniform float uTime;\n #include \n ${THREE.ShaderChunk['fog_pars_fragment']}\n void main() {\n vec2 uv = vUv;\n vec3 color = vec3(uColor);\n #include \n gl_FragColor = vec4(color, 1.);\n ${THREE.ShaderChunk['fog_fragment']}\n }\n `;\n\n const islandFragment = roadBaseFragment\n .replace('#include ', '')\n .replace('#include ', '');\n\n const roadMarkings_vars = `\n uniform float uLanes;\n uniform vec3 uBrokenLinesColor;\n uniform vec3 uShoulderLinesColor;\n uniform float uShoulderLinesWidthPercentage;\n uniform float uBrokenLinesWidthPercentage;\n uniform float uBrokenLinesLengthPercentage;\n highp float random(vec2 co) {\n highp float a = 12.9898;\n highp float b = 78.233;\n highp float c = 43758.5453;\n highp float dt = dot(co.xy, vec2(a, b));\n highp float sn = mod(dt, 3.14);\n return fract(sin(sn) * c);\n }\n `;\n\n const roadMarkings_fragment = `\n uv.y = mod(uv.y + uTime * 0.05, 1.);\n float laneWidth = 1.0 / uLanes;\n float brokenLineWidth = laneWidth * uBrokenLinesWidthPercentage;\n float laneEmptySpace = 1. - uBrokenLinesLengthPercentage;\n\n float brokenLines = step(1.0 - brokenLineWidth, fract(uv.x * 2.0)) * step(laneEmptySpace, fract(uv.y * 10.0));\n float sideLines = step(1.0 - brokenLineWidth, fract((uv.x - laneWidth * (uLanes - 1.0)) * 2.0)) + step(brokenLineWidth, uv.x);\n\n brokenLines = mix(brokenLines, sideLines, uv.x);\n `;\n\n const roadFragment = roadBaseFragment\n .replace('#include ', roadMarkings_fragment)\n .replace('#include ', roadMarkings_vars);\n\n const roadVertex = `\n #define USE_FOG;\n uniform float uTime;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n uniform float uTravelLength;\n varying vec2 vUv; \n #include \n void main() {\n vec3 transformed = position.xyz;\n vec3 distortion = getDistortion((transformed.y + uTravelLength / 2.) / uTravelLength);\n transformed.x += distortion.x;\n transformed.z += distortion.y;\n transformed.y += -1. * distortion.z; \n \n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vUv = uv;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n `;\n\n function resizeRendererToDisplaySize(renderer, setSize) {\n const canvas = renderer.domElement;\n const width = canvas.clientWidth;\n const height = canvas.clientHeight;\n if (width <= 0 || height <= 0) return false;\n const needResize = canvas.width !== width || canvas.height !== height;\n if (needResize) {\n setSize(width, height, false);\n }\n return needResize;\n }\n\n const container = hyperspeed.current;\n if (!container) return;\n\n const options = {\n ...DEFAULT_EFFECT_OPTIONS,\n ...effectOptions,\n colors: { ...DEFAULT_EFFECT_OPTIONS.colors, ...effectOptions.colors }\n };\n options.distortion = distortions[options.distortion];\n\n const myApp = new App(container, options);\n appRef.current = myApp;\n myApp.loadAssets().then(myApp.init);\n\n return () => {\n if (appRef.current) {\n appRef.current.dispose();\n }\n };\n }, [effectOptions]);\n\n return
;\n};\n\nexport default Hyperspeed;\n" }, { "type": "registry:component", - "path": "Hyperspeed/Hyperspeed.jsx", - "content": "import { BloomEffect, EffectComposer, EffectPass, RenderPass, SMAAEffect, SMAAPreset } from 'postprocessing';\nimport { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\nconst DEFAULT_EFFECT_OPTIONS = {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 2,\n lanesPerRoad: 4,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 20,\n lightPairsPerRoadWay: 40,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.03, 400 * 0.2],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.8, 0.8],\n carFloorSeparation: [0, 5],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0xffffff,\n brokenLines: 0xffffff,\n leftCars: [0xd856bf, 0x6750a2, 0xc247ac],\n rightCars: [0x03b3c3, 0x0e5ea5, 0x324555],\n sticks: 0x03b3c3\n }\n};\n\nconst Hyperspeed = ({ effectOptions = DEFAULT_EFFECT_OPTIONS }) => {\n const hyperspeed = useRef(null);\n const appRef = useRef(null);\n\n useEffect(() => {\n if (appRef.current) {\n appRef.current.dispose();\n appRef.current = null;\n const container = hyperspeed.current;\n if (container) {\n while (container.firstChild) {\n container.removeChild(container.firstChild);\n }\n }\n }\n\n const mountainUniforms = {\n uFreq: { value: new THREE.Vector3(3, 6, 10) },\n uAmp: { value: new THREE.Vector3(30, 30, 20) }\n };\n\n const xyUniforms = {\n uFreq: { value: new THREE.Vector2(5, 2) },\n uAmp: { value: new THREE.Vector2(25, 15) }\n };\n\n const LongRaceUniforms = {\n uFreq: { value: new THREE.Vector2(2, 3) },\n uAmp: { value: new THREE.Vector2(35, 10) }\n };\n\n const turbulentUniforms = {\n uFreq: { value: new THREE.Vector4(4, 8, 8, 1) },\n uAmp: { value: new THREE.Vector4(25, 5, 10, 10) }\n };\n\n const deepUniforms = {\n uFreq: { value: new THREE.Vector2(4, 8) },\n uAmp: { value: new THREE.Vector2(10, 20) },\n uPowY: { value: new THREE.Vector2(20, 2) }\n };\n\n let nsin = val => Math.sin(val) * 0.5 + 0.5;\n\n const distortions = {\n mountainDistortion: {\n uniforms: mountainUniforms,\n getDistortion: `\n uniform vec3 uAmp;\n uniform vec3 uFreq;\n #define PI 3.14159265358979\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n vec3 getDistortion(float progress){\n float movementProgressFix = 0.02;\n return vec3( \n cos(progress * PI * uFreq.x + uTime) * uAmp.x - cos(movementProgressFix * PI * uFreq.x + uTime) * uAmp.x,\n nsin(progress * PI * uFreq.y + uTime) * uAmp.y - nsin(movementProgressFix * PI * uFreq.y + uTime) * uAmp.y,\n nsin(progress * PI * uFreq.z + uTime) * uAmp.z - nsin(movementProgressFix * PI * uFreq.z + uTime) * uAmp.z\n );\n }\n `,\n getJS: (progress, time) => {\n let movementProgressFix = 0.02;\n let uFreq = mountainUniforms.uFreq.value;\n let uAmp = mountainUniforms.uAmp.value;\n let distortion = new THREE.Vector3(\n Math.cos(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.cos(movementProgressFix * Math.PI * uFreq.x + time) * uAmp.x,\n nsin(progress * Math.PI * uFreq.y + time) * uAmp.y -\n nsin(movementProgressFix * Math.PI * uFreq.y + time) * uAmp.y,\n nsin(progress * Math.PI * uFreq.z + time) * uAmp.z -\n nsin(movementProgressFix * Math.PI * uFreq.z + time) * uAmp.z\n );\n let lookAtAmp = new THREE.Vector3(2, 2, 2);\n let lookAtOffset = new THREE.Vector3(0, 0, -5);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n xyDistortion: {\n uniforms: xyUniforms,\n getDistortion: `\n uniform vec2 uFreq;\n uniform vec2 uAmp;\n #define PI 3.14159265358979\n vec3 getDistortion(float progress){\n float movementProgressFix = 0.02;\n return vec3( \n cos(progress * PI * uFreq.x + uTime) * uAmp.x - cos(movementProgressFix * PI * uFreq.x + uTime) * uAmp.x,\n sin(progress * PI * uFreq.y + PI/2. + uTime) * uAmp.y - sin(movementProgressFix * PI * uFreq.y + PI/2. + uTime) * uAmp.y,\n 0.\n );\n }\n `,\n getJS: (progress, time) => {\n let movementProgressFix = 0.02;\n let uFreq = xyUniforms.uFreq.value;\n let uAmp = xyUniforms.uAmp.value;\n let distortion = new THREE.Vector3(\n Math.cos(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.cos(movementProgressFix * Math.PI * uFreq.x + time) * uAmp.x,\n Math.sin(progress * Math.PI * uFreq.y + time + Math.PI / 2) * uAmp.y -\n Math.sin(movementProgressFix * Math.PI * uFreq.y + time + Math.PI / 2) * uAmp.y,\n 0\n );\n let lookAtAmp = new THREE.Vector3(2, 0.4, 1);\n let lookAtOffset = new THREE.Vector3(0, 0, -3);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n LongRaceDistortion: {\n uniforms: LongRaceUniforms,\n getDistortion: `\n uniform vec2 uFreq;\n uniform vec2 uAmp;\n #define PI 3.14159265358979\n vec3 getDistortion(float progress){\n float camProgress = 0.0125;\n return vec3( \n sin(progress * PI * uFreq.x + uTime) * uAmp.x - sin(camProgress * PI * uFreq.x + uTime) * uAmp.x,\n sin(progress * PI * uFreq.y + uTime) * uAmp.y - sin(camProgress * PI * uFreq.y + uTime) * uAmp.y,\n 0.\n );\n }\n `,\n getJS: (progress, time) => {\n let camProgress = 0.0125;\n let uFreq = LongRaceUniforms.uFreq.value;\n let uAmp = LongRaceUniforms.uAmp.value;\n let distortion = new THREE.Vector3(\n Math.sin(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.sin(camProgress * Math.PI * uFreq.x + time) * uAmp.x,\n Math.sin(progress * Math.PI * uFreq.y + time) * uAmp.y -\n Math.sin(camProgress * Math.PI * uFreq.y + time) * uAmp.y,\n 0\n );\n let lookAtAmp = new THREE.Vector3(1, 1, 0);\n let lookAtOffset = new THREE.Vector3(0, 0, -5);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n turbulentDistortion: {\n uniforms: turbulentUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n cos(PI * progress * uFreq.r + uTime) * uAmp.r +\n pow(cos(PI * progress * uFreq.g + uTime * (uFreq.g / uFreq.r)), 2. ) * uAmp.g\n );\n }\n float getDistortionY(float progress){\n return (\n -nsin(PI * progress * uFreq.b + uTime) * uAmp.b +\n -pow(nsin(PI * progress * uFreq.a + uTime / (uFreq.b / uFreq.a)), 5.) * uAmp.a\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.0125),\n getDistortionY(progress) - getDistortionY(0.0125),\n 0.\n );\n }\n `,\n getJS: (progress, time) => {\n const uFreq = turbulentUniforms.uFreq.value;\n const uAmp = turbulentUniforms.uAmp.value;\n\n const getX = p =>\n Math.cos(Math.PI * p * uFreq.x + time) * uAmp.x +\n Math.pow(Math.cos(Math.PI * p * uFreq.y + time * (uFreq.y / uFreq.x)), 2) * uAmp.y;\n\n const getY = p =>\n -nsin(Math.PI * p * uFreq.z + time) * uAmp.z -\n Math.pow(nsin(Math.PI * p * uFreq.w + time / (uFreq.z / uFreq.w)), 5) * uAmp.w;\n\n let distortion = new THREE.Vector3(\n getX(progress) - getX(progress + 0.007),\n getY(progress) - getY(progress + 0.007),\n 0\n );\n let lookAtAmp = new THREE.Vector3(-2, -5, 0);\n let lookAtOffset = new THREE.Vector3(0, 0, -10);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n turbulentDistortionStill: {\n uniforms: turbulentUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n cos(PI * progress * uFreq.r) * uAmp.r +\n pow(cos(PI * progress * uFreq.g * (uFreq.g / uFreq.r)), 2. ) * uAmp.g\n );\n }\n float getDistortionY(float progress){\n return (\n -nsin(PI * progress * uFreq.b) * uAmp.b +\n -pow(nsin(PI * progress * uFreq.a / (uFreq.b / uFreq.a)), 5.) * uAmp.a\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.02),\n 0.\n );\n }\n `\n },\n deepDistortionStill: {\n uniforms: deepUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n uniform vec2 uPowY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n sin(progress * PI * uFreq.x) * uAmp.x * 2.\n );\n }\n float getDistortionY(float progress){\n return (\n pow(abs(progress * uPowY.x), uPowY.y) + sin(progress * PI * uFreq.y) * uAmp.y\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.05),\n 0.\n );\n }\n `\n },\n deepDistortion: {\n uniforms: deepUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n uniform vec2 uPowY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n sin(progress * PI * uFreq.x + uTime) * uAmp.x\n );\n }\n float getDistortionY(float progress){\n return (\n pow(abs(progress * uPowY.x), uPowY.y) + sin(progress * PI * uFreq.y + uTime) * uAmp.y\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.02),\n 0.\n );\n }\n `,\n getJS: (progress, time) => {\n const uFreq = deepUniforms.uFreq.value;\n const uAmp = deepUniforms.uAmp.value;\n const uPowY = deepUniforms.uPowY.value;\n\n const getX = p => Math.sin(p * Math.PI * uFreq.x + time) * uAmp.x;\n const getY = p => Math.pow(p * uPowY.x, uPowY.y) + Math.sin(p * Math.PI * uFreq.y + time) * uAmp.y;\n\n let distortion = new THREE.Vector3(\n getX(progress) - getX(progress + 0.01),\n getY(progress) - getY(progress + 0.01),\n 0\n );\n let lookAtAmp = new THREE.Vector3(-2, -4, 0);\n let lookAtOffset = new THREE.Vector3(0, 0, -10);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n }\n };\n\n class App {\n constructor(container, options = {}) {\n this.options = options;\n if (this.options.distortion == null) {\n this.options.distortion = {\n uniforms: distortion_uniforms,\n getDistortion: distortion_vertex\n };\n }\n this.container = container;\n this.hasValidSize = false;\n\n const initW = Math.max(1, container.offsetWidth);\n const initH = Math.max(1, container.offsetHeight);\n\n this.renderer = new THREE.WebGLRenderer({\n antialias: false,\n alpha: true\n });\n this.renderer.setSize(initW, initH, false);\n this.renderer.setPixelRatio(window.devicePixelRatio);\n this.composer = new EffectComposer(this.renderer);\n container.append(this.renderer.domElement);\n\n this.camera = new THREE.PerspectiveCamera(options.fov, initW / initH, 0.1, 10000);\n this.camera.position.z = -5;\n this.camera.position.y = 8;\n this.camera.position.x = 0;\n this.scene = new THREE.Scene();\n this.scene.background = null;\n\n let fog = new THREE.Fog(options.colors.background, options.length * 0.2, options.length * 500);\n this.scene.fog = fog;\n this.fogUniforms = {\n fogColor: { value: fog.color },\n fogNear: { value: fog.near },\n fogFar: { value: fog.far }\n };\n this.clock = new THREE.Clock();\n this.assets = {};\n this.disposed = false;\n\n this.road = new Road(this, options);\n this.leftCarLights = new CarLights(\n this,\n options,\n options.colors.leftCars,\n options.movingAwaySpeed,\n new THREE.Vector2(0, 1 - options.carLightsFade)\n );\n this.rightCarLights = new CarLights(\n this,\n options,\n options.colors.rightCars,\n options.movingCloserSpeed,\n new THREE.Vector2(1, 0 + options.carLightsFade)\n );\n this.leftSticks = new LightsSticks(this, options);\n\n this.fovTarget = options.fov;\n this.speedUpTarget = 0;\n this.speedUp = 0;\n this.timeOffset = 0;\n\n this.tick = this.tick.bind(this);\n this.init = this.init.bind(this);\n this.setSize = this.setSize.bind(this);\n this.onMouseDown = this.onMouseDown.bind(this);\n this.onMouseUp = this.onMouseUp.bind(this);\n\n this.onTouchStart = this.onTouchStart.bind(this);\n this.onTouchEnd = this.onTouchEnd.bind(this);\n this.onContextMenu = this.onContextMenu.bind(this);\n\n this.onWindowResize = this.onWindowResize.bind(this);\n window.addEventListener('resize', this.onWindowResize);\n\n if (container.offsetWidth > 0 && container.offsetHeight > 0) {\n this.hasValidSize = true;\n }\n }\n\n onWindowResize() {\n const width = this.container.offsetWidth;\n const height = this.container.offsetHeight;\n\n if (width <= 0 || height <= 0) {\n this.hasValidSize = false;\n return;\n }\n\n this.renderer.setSize(width, height);\n this.camera.aspect = width / height;\n this.camera.updateProjectionMatrix();\n this.composer.setSize(width, height);\n this.hasValidSize = true;\n }\n\n initPasses() {\n this.renderPass = new RenderPass(this.scene, this.camera);\n this.bloomPass = new EffectPass(\n this.camera,\n new BloomEffect({\n luminanceThreshold: 0.2,\n luminanceSmoothing: 0,\n resolutionScale: 1\n })\n );\n\n const smaaPass = new EffectPass(\n this.camera,\n new SMAAEffect({\n preset: SMAAPreset.MEDIUM,\n searchImage: SMAAEffect.searchImageDataURL,\n areaImage: SMAAEffect.areaImageDataURL\n })\n );\n this.renderPass.renderToScreen = false;\n this.bloomPass.renderToScreen = false;\n smaaPass.renderToScreen = true;\n this.composer.addPass(this.renderPass);\n this.composer.addPass(this.bloomPass);\n this.composer.addPass(smaaPass);\n }\n\n loadAssets() {\n const assets = this.assets;\n return new Promise(resolve => {\n const manager = new THREE.LoadingManager(resolve);\n\n const searchImage = new Image();\n const areaImage = new Image();\n assets.smaa = {};\n searchImage.addEventListener('load', function () {\n assets.smaa.search = this;\n manager.itemEnd('smaa-search');\n });\n\n areaImage.addEventListener('load', function () {\n assets.smaa.area = this;\n manager.itemEnd('smaa-area');\n });\n manager.itemStart('smaa-search');\n manager.itemStart('smaa-area');\n\n searchImage.src = SMAAEffect.searchImageDataURL;\n areaImage.src = SMAAEffect.areaImageDataURL;\n });\n }\n\n init() {\n this.initPasses();\n const options = this.options;\n this.road.init();\n this.leftCarLights.init();\n\n this.leftCarLights.mesh.position.setX(-options.roadWidth / 2 - options.islandWidth / 2);\n this.rightCarLights.init();\n this.rightCarLights.mesh.position.setX(options.roadWidth / 2 + options.islandWidth / 2);\n this.leftSticks.init();\n this.leftSticks.mesh.position.setX(-(options.roadWidth + options.islandWidth / 2));\n\n this.container.addEventListener('mousedown', this.onMouseDown);\n this.container.addEventListener('mouseup', this.onMouseUp);\n this.container.addEventListener('mouseout', this.onMouseUp);\n\n this.container.addEventListener('touchstart', this.onTouchStart, { passive: true });\n this.container.addEventListener('touchend', this.onTouchEnd, { passive: true });\n this.container.addEventListener('touchcancel', this.onTouchEnd, { passive: true });\n this.container.addEventListener('contextmenu', this.onContextMenu);\n\n this.tick();\n }\n\n onMouseDown(ev) {\n if (this.options.onSpeedUp) this.options.onSpeedUp(ev);\n this.fovTarget = this.options.fovSpeedUp;\n this.speedUpTarget = this.options.speedUp;\n }\n\n onMouseUp(ev) {\n if (this.options.onSlowDown) this.options.onSlowDown(ev);\n this.fovTarget = this.options.fov;\n this.speedUpTarget = 0;\n }\n\n onTouchStart(ev) {\n if (this.options.onSpeedUp) this.options.onSpeedUp(ev);\n this.fovTarget = this.options.fovSpeedUp;\n this.speedUpTarget = this.options.speedUp;\n }\n\n onTouchEnd(ev) {\n if (this.options.onSlowDown) this.options.onSlowDown(ev);\n this.fovTarget = this.options.fov;\n this.speedUpTarget = 0;\n }\n\n onContextMenu(ev) {\n ev.preventDefault();\n }\n\n update(delta) {\n let lerpPercentage = Math.exp(-(-60 * Math.log2(1 - 0.1)) * delta);\n this.speedUp += lerp(this.speedUp, this.speedUpTarget, lerpPercentage, 0.00001);\n this.timeOffset += this.speedUp * delta;\n\n let time = this.clock.elapsedTime + this.timeOffset;\n\n this.rightCarLights.update(time);\n this.leftCarLights.update(time);\n this.leftSticks.update(time);\n this.road.update(time);\n\n let updateCamera = false;\n let fovChange = lerp(this.camera.fov, this.fovTarget, lerpPercentage);\n if (fovChange !== 0) {\n this.camera.fov += fovChange * delta * 6;\n updateCamera = true;\n }\n\n if (this.options.distortion.getJS) {\n const distortion = this.options.distortion.getJS(0.025, time);\n\n this.camera.lookAt(\n new THREE.Vector3(\n this.camera.position.x + distortion.x,\n this.camera.position.y + distortion.y,\n this.camera.position.z + distortion.z\n )\n );\n updateCamera = true;\n }\n if (updateCamera) {\n this.camera.updateProjectionMatrix();\n }\n }\n\n render(delta) {\n this.composer.render(delta);\n }\n\n dispose() {\n this.disposed = true;\n\n if (this.scene) {\n this.scene.traverse(object => {\n const obj = object;\n if (!obj.isMesh) return;\n\n if (obj.geometry) obj.geometry.dispose();\n\n if (obj.material) {\n if (Array.isArray(obj.material)) {\n obj.material.forEach(material => material.dispose());\n } else {\n obj.material.dispose();\n }\n }\n });\n this.scene.clear();\n }\n\n if (this.renderer) {\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n if (this.renderer.domElement && this.renderer.domElement.parentNode) {\n this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);\n }\n }\n if (this.composer) {\n this.composer.dispose();\n }\n\n window.removeEventListener('resize', this.onWindowResize);\n if (this.container) {\n this.container.removeEventListener('mousedown', this.onMouseDown);\n this.container.removeEventListener('mouseup', this.onMouseUp);\n this.container.removeEventListener('mouseout', this.onMouseUp);\n\n this.container.removeEventListener('touchstart', this.onTouchStart);\n this.container.removeEventListener('touchend', this.onTouchEnd);\n this.container.removeEventListener('touchcancel', this.onTouchEnd);\n this.container.removeEventListener('contextmenu', this.onContextMenu);\n }\n }\n\n setSize(width, height, updateStyles) {\n if (width <= 0 || height <= 0) {\n this.hasValidSize = false;\n return;\n }\n this.composer.setSize(width, height, updateStyles);\n this.hasValidSize = true;\n }\n\n tick() {\n if (this.disposed) return;\n\n if (!this.hasValidSize) {\n const w = this.container.offsetWidth;\n const h = this.container.offsetHeight;\n if (w > 0 && h > 0) {\n this.renderer.setSize(w, h, false);\n this.camera.aspect = w / h;\n this.camera.updateProjectionMatrix();\n this.composer.setSize(w, h);\n this.hasValidSize = true;\n } else {\n requestAnimationFrame(this.tick);\n return;\n }\n }\n\n if (resizeRendererToDisplaySize(this.renderer, this.setSize)) {\n const canvas = this.renderer.domElement;\n if (this.hasValidSize) {\n this.camera.aspect = canvas.clientWidth / canvas.clientHeight;\n this.camera.updateProjectionMatrix();\n }\n }\n\n if (this.hasValidSize) {\n const delta = this.clock.getDelta();\n this.render(delta);\n this.update(delta);\n }\n\n requestAnimationFrame(this.tick);\n }\n }\n\n const distortion_uniforms = {\n uDistortionX: { value: new THREE.Vector2(80, 3) },\n uDistortionY: { value: new THREE.Vector2(-40, 2.5) }\n };\n\n const distortion_vertex = `\n #define PI 3.14159265358979\n uniform vec2 uDistortionX;\n uniform vec2 uDistortionY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n vec3 getDistortion(float progress){\n progress = clamp(progress, 0., 1.);\n float xAmp = uDistortionX.r;\n float xFreq = uDistortionX.g;\n float yAmp = uDistortionY.r;\n float yFreq = uDistortionY.g;\n return vec3( \n xAmp * nsin(progress * PI * xFreq - PI / 2.),\n yAmp * nsin(progress * PI * yFreq - PI / 2.),\n 0.\n );\n }\n `;\n\n const random = base => {\n if (Array.isArray(base)) return Math.random() * (base[1] - base[0]) + base[0];\n return Math.random() * base;\n };\n\n const pickRandom = arr => {\n if (Array.isArray(arr)) return arr[Math.floor(Math.random() * arr.length)];\n return arr;\n };\n\n function lerp(current, target, speed = 0.1, limit = 0.001) {\n let change = (target - current) * speed;\n if (Math.abs(change) < limit) {\n change = target - current;\n }\n return change;\n }\n\n class CarLights {\n constructor(webgl, options, colors, speed, fade) {\n this.webgl = webgl;\n this.options = options;\n this.colors = colors;\n this.speed = speed;\n this.fade = fade;\n }\n\n init() {\n const options = this.options;\n let curve = new THREE.LineCurve3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -1));\n let geometry = new THREE.TubeGeometry(curve, 40, 1, 8, false);\n\n let instanced = new THREE.InstancedBufferGeometry().copy(geometry);\n instanced.instanceCount = options.lightPairsPerRoadWay * 2;\n\n let laneWidth = options.roadWidth / options.lanesPerRoad;\n\n let aOffset = [];\n let aMetrics = [];\n let aColor = [];\n\n let colors = this.colors;\n if (Array.isArray(colors)) {\n colors = colors.map(c => new THREE.Color(c));\n } else {\n colors = new THREE.Color(colors);\n }\n\n for (let i = 0; i < options.lightPairsPerRoadWay; i++) {\n let radius = random(options.carLightsRadius);\n let length = random(options.carLightsLength);\n let speed = random(this.speed);\n\n let carLane = i % options.lanesPerRoad;\n let laneX = carLane * laneWidth - options.roadWidth / 2 + laneWidth / 2;\n\n let carWidth = random(options.carWidthPercentage) * laneWidth;\n let carShiftX = random(options.carShiftX) * laneWidth;\n laneX += carShiftX;\n\n let offsetY = random(options.carFloorSeparation) + radius * 1.3;\n\n let offsetZ = -random(options.length);\n\n aOffset.push(laneX - carWidth / 2);\n aOffset.push(offsetY);\n aOffset.push(offsetZ);\n\n aOffset.push(laneX + carWidth / 2);\n aOffset.push(offsetY);\n aOffset.push(offsetZ);\n\n aMetrics.push(radius);\n aMetrics.push(length);\n aMetrics.push(speed);\n\n aMetrics.push(radius);\n aMetrics.push(length);\n aMetrics.push(speed);\n\n let color = pickRandom(colors);\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n }\n\n instanced.setAttribute('aOffset', new THREE.InstancedBufferAttribute(new Float32Array(aOffset), 3, false));\n instanced.setAttribute('aMetrics', new THREE.InstancedBufferAttribute(new Float32Array(aMetrics), 3, false));\n instanced.setAttribute('aColor', new THREE.InstancedBufferAttribute(new Float32Array(aColor), 3, false));\n\n let material = new THREE.ShaderMaterial({\n fragmentShader: carLightsFragment,\n vertexShader: carLightsVertex,\n transparent: true,\n uniforms: Object.assign(\n {\n uTime: { value: 0 },\n uTravelLength: { value: options.length },\n uFade: { value: this.fade }\n },\n this.webgl.fogUniforms,\n options.distortion.uniforms\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n options.distortion.getDistortion\n );\n };\n\n let mesh = new THREE.Mesh(instanced, material);\n mesh.frustumCulled = false;\n this.webgl.scene.add(mesh);\n this.mesh = mesh;\n }\n\n update(time) {\n this.mesh.material.uniforms.uTime.value = time;\n }\n }\n\n const carLightsFragment = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_fragment']}\n varying vec3 vColor;\n varying vec2 vUv; \n uniform vec2 uFade;\n void main() {\n vec3 color = vec3(vColor);\n float alpha = smoothstep(uFade.x, uFade.y, vUv.x);\n gl_FragColor = vec4(color, alpha);\n if (gl_FragColor.a < 0.0001) discard;\n ${THREE.ShaderChunk['fog_fragment']}\n }\n `;\n\n const carLightsVertex = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n attribute vec3 aOffset;\n attribute vec3 aMetrics;\n attribute vec3 aColor;\n uniform float uTravelLength;\n uniform float uTime;\n varying vec2 vUv; \n varying vec3 vColor; \n #include \n void main() {\n vec3 transformed = position.xyz;\n float radius = aMetrics.r;\n float myLength = aMetrics.g;\n float speed = aMetrics.b;\n\n transformed.xy *= radius;\n transformed.z *= myLength;\n\n transformed.z += myLength - mod(uTime * speed + aOffset.z, uTravelLength);\n transformed.xy += aOffset.xy;\n\n float progress = abs(transformed.z / uTravelLength);\n transformed.xyz += getDistortion(progress);\n\n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vUv = uv;\n vColor = aColor;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n `;\n\n class LightsSticks {\n constructor(webgl, options) {\n this.webgl = webgl;\n this.options = options;\n }\n\n init() {\n const options = this.options;\n const geometry = new THREE.PlaneGeometry(1, 1);\n let instanced = new THREE.InstancedBufferGeometry().copy(geometry);\n let totalSticks = options.totalSideLightSticks;\n instanced.instanceCount = totalSticks;\n\n let stickoffset = options.length / (totalSticks - 1);\n const aOffset = [];\n const aColor = [];\n const aMetrics = [];\n\n let colors = options.colors.sticks;\n if (Array.isArray(colors)) {\n colors = colors.map(c => new THREE.Color(c));\n } else {\n colors = new THREE.Color(colors);\n }\n\n for (let i = 0; i < totalSticks; i++) {\n let width = random(options.lightStickWidth);\n let height = random(options.lightStickHeight);\n aOffset.push((i - 1) * stickoffset * 2 + stickoffset * Math.random());\n\n let color = pickRandom(colors);\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n\n aMetrics.push(width);\n aMetrics.push(height);\n }\n\n instanced.setAttribute('aOffset', new THREE.InstancedBufferAttribute(new Float32Array(aOffset), 1, false));\n instanced.setAttribute('aColor', new THREE.InstancedBufferAttribute(new Float32Array(aColor), 3, false));\n instanced.setAttribute('aMetrics', new THREE.InstancedBufferAttribute(new Float32Array(aMetrics), 2, false));\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: sideSticksFragment,\n vertexShader: sideSticksVertex,\n side: THREE.DoubleSide,\n uniforms: Object.assign(\n {\n uTravelLength: { value: options.length },\n uTime: { value: 0 }\n },\n this.webgl.fogUniforms,\n options.distortion.uniforms\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n options.distortion.getDistortion\n );\n };\n\n const mesh = new THREE.Mesh(instanced, material);\n mesh.frustumCulled = false;\n this.webgl.scene.add(mesh);\n this.mesh = mesh;\n }\n\n update(time) {\n this.mesh.material.uniforms.uTime.value = time;\n }\n }\n\n const sideSticksVertex = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n attribute float aOffset;\n attribute vec3 aColor;\n attribute vec2 aMetrics;\n uniform float uTravelLength;\n uniform float uTime;\n varying vec3 vColor;\n mat4 rotationY( in float angle ) {\n return mat4(\tcos(angle),\t\t0,\t\tsin(angle),\t0,\n 0,\t\t1.0,\t\t\t 0,\t0,\n -sin(angle),\t0,\t\tcos(angle),\t0,\n 0, \t\t0,\t\t\t\t0,\t1);\n }\n #include \n void main(){\n vec3 transformed = position.xyz;\n float width = aMetrics.x;\n float height = aMetrics.y;\n\n transformed.xy *= vec2(width, height);\n float time = mod(uTime * 60. * 2. + aOffset, uTravelLength);\n\n transformed = (rotationY(3.14/2.) * vec4(transformed,1.)).xyz;\n\n transformed.z += - uTravelLength + time;\n\n float progress = abs(transformed.z / uTravelLength);\n transformed.xyz += getDistortion(progress);\n\n transformed.y += height / 2.;\n transformed.x += -width / 2.;\n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vColor = aColor;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n `;\n\n const sideSticksFragment = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_fragment']}\n varying vec3 vColor;\n void main(){\n vec3 color = vec3(vColor);\n gl_FragColor = vec4(color,1.);\n ${THREE.ShaderChunk['fog_fragment']}\n }\n `;\n\n class Road {\n constructor(webgl, options) {\n this.webgl = webgl;\n this.options = options;\n this.uTime = { value: 0 };\n }\n\n createPlane(side, width, isRoad) {\n const options = this.options;\n let segments = 100;\n const geometry = new THREE.PlaneGeometry(\n isRoad ? options.roadWidth : options.islandWidth,\n options.length,\n 20,\n segments\n );\n let uniforms = {\n uTravelLength: { value: options.length },\n uColor: { value: new THREE.Color(isRoad ? options.colors.roadColor : options.colors.islandColor) },\n uTime: this.uTime\n };\n\n if (isRoad) {\n uniforms = Object.assign(uniforms, {\n uLanes: { value: options.lanesPerRoad },\n uBrokenLinesColor: { value: new THREE.Color(options.colors.brokenLines) },\n uShoulderLinesColor: { value: new THREE.Color(options.colors.shoulderLines) },\n uShoulderLinesWidthPercentage: { value: options.shoulderLinesWidthPercentage },\n uBrokenLinesLengthPercentage: { value: options.brokenLinesLengthPercentage },\n uBrokenLinesWidthPercentage: { value: options.brokenLinesWidthPercentage }\n });\n }\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: isRoad ? roadFragment : islandFragment,\n vertexShader: roadVertex,\n side: THREE.DoubleSide,\n uniforms: Object.assign(uniforms, this.webgl.fogUniforms, options.distortion.uniforms)\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n options.distortion.getDistortion\n );\n };\n\n const mesh = new THREE.Mesh(geometry, material);\n mesh.rotation.x = -Math.PI / 2;\n mesh.position.z = -options.length / 2;\n mesh.position.x += (this.options.islandWidth / 2 + options.roadWidth / 2) * side;\n this.webgl.scene.add(mesh);\n\n return mesh;\n }\n\n init() {\n this.leftRoadWay = this.createPlane(-1, this.options.roadWidth, true);\n this.rightRoadWay = this.createPlane(1, this.options.roadWidth, true);\n this.island = this.createPlane(0, this.options.islandWidth, false);\n }\n\n update(time) {\n this.uTime.value = time;\n }\n }\n\n const roadBaseFragment = `\n #define USE_FOG;\n varying vec2 vUv; \n uniform vec3 uColor;\n uniform float uTime;\n #include \n ${THREE.ShaderChunk['fog_pars_fragment']}\n void main() {\n vec2 uv = vUv;\n vec3 color = vec3(uColor);\n #include \n gl_FragColor = vec4(color, 1.);\n ${THREE.ShaderChunk['fog_fragment']}\n }\n `;\n\n const islandFragment = roadBaseFragment\n .replace('#include ', '')\n .replace('#include ', '');\n\n const roadMarkings_vars = `\n uniform float uLanes;\n uniform vec3 uBrokenLinesColor;\n uniform vec3 uShoulderLinesColor;\n uniform float uShoulderLinesWidthPercentage;\n uniform float uBrokenLinesWidthPercentage;\n uniform float uBrokenLinesLengthPercentage;\n highp float random(vec2 co) {\n highp float a = 12.9898;\n highp float b = 78.233;\n highp float c = 43758.5453;\n highp float dt = dot(co.xy, vec2(a, b));\n highp float sn = mod(dt, 3.14);\n return fract(sin(sn) * c);\n }\n `;\n\n const roadMarkings_fragment = `\n uv.y = mod(uv.y + uTime * 0.05, 1.);\n float laneWidth = 1.0 / uLanes;\n float brokenLineWidth = laneWidth * uBrokenLinesWidthPercentage;\n float laneEmptySpace = 1. - uBrokenLinesLengthPercentage;\n\n float brokenLines = step(1.0 - brokenLineWidth, fract(uv.x * 2.0)) * step(laneEmptySpace, fract(uv.y * 10.0));\n float sideLines = step(1.0 - brokenLineWidth, fract((uv.x - laneWidth * (uLanes - 1.0)) * 2.0)) + step(brokenLineWidth, uv.x);\n\n brokenLines = mix(brokenLines, sideLines, uv.x);\n `;\n\n const roadFragment = roadBaseFragment\n .replace('#include ', roadMarkings_fragment)\n .replace('#include ', roadMarkings_vars);\n\n const roadVertex = `\n #define USE_FOG;\n uniform float uTime;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n uniform float uTravelLength;\n varying vec2 vUv; \n #include \n void main() {\n vec3 transformed = position.xyz;\n vec3 distortion = getDistortion((transformed.y + uTravelLength / 2.) / uTravelLength);\n transformed.x += distortion.x;\n transformed.z += distortion.y;\n transformed.y += -1. * distortion.z; \n \n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vUv = uv;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n `;\n\n function resizeRendererToDisplaySize(renderer, setSize) {\n const canvas = renderer.domElement;\n const width = canvas.clientWidth;\n const height = canvas.clientHeight;\n if (width <= 0 || height <= 0) return false;\n const needResize = canvas.width !== width || canvas.height !== height;\n if (needResize) {\n setSize(width, height, false);\n }\n return needResize;\n }\n\n const container = hyperspeed.current;\n if (!container) return;\n\n const options = {\n ...DEFAULT_EFFECT_OPTIONS,\n ...effectOptions,\n colors: { ...DEFAULT_EFFECT_OPTIONS.colors, ...effectOptions.colors }\n };\n options.distortion = distortions[options.distortion];\n\n const myApp = new App(container, options);\n appRef.current = myApp;\n myApp.loadAssets().then(myApp.init);\n\n return () => {\n if (appRef.current) {\n appRef.current.dispose();\n }\n };\n }, [effectOptions]);\n\n return
;\n};\n\nexport default Hyperspeed;\n" + "path": "Hyperspeed/HyperSpeedPresets.js", + "content": "export const hyperspeedPresets = {\n one: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 20,\n lightPairsPerRoadWay: 40,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.03, 400 * 0.2],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.8, 0.8],\n carFloorSeparation: [0, 5],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xd856bf, 0x6750a2, 0xc247ac],\n rightCars: [0x03b3c3, 0x0e5ea5, 0x324555],\n sticks: 0x03b3c3\n }\n },\n two: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'mountainDistortion',\n length: 400,\n roadWidth: 9,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 50,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xff102a, 0xeb383e, 0xff102a],\n rightCars: [0xdadafa, 0xbebae3, 0x8f97e4],\n sticks: 0xdadafa\n }\n },\n three: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'xyDistortion',\n length: 400,\n roadWidth: 9,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 3,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 30,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.02, 0.05],\n lightStickHeight: [0.3, 0.7],\n movingAwaySpeed: [20, 50],\n movingCloserSpeed: [-150, -230],\n carLightsLength: [400 * 0.05, 400 * 0.2],\n carLightsRadius: [0.03, 0.08],\n carWidthPercentage: [0.1, 0.5],\n carShiftX: [-0.5, 0.5],\n carFloorSeparation: [0, 0.1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0x7d0d1b, 0xa90519, 0xff102a],\n rightCars: [0xf1eece, 0xe6e2b1, 0xdfd98a],\n sticks: 0xf1eece\n }\n },\n four: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'LongRaceDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 5,\n lanesPerRoad: 2,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 70,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xff5f73, 0xe74d60, 0xff102a],\n rightCars: [0xa4e3e6, 0x80d1d4, 0x53c2c6],\n sticks: 0xa4e3e6\n }\n },\n five: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 9,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 50,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xdc5b20, 0xdca320, 0xdc2020],\n rightCars: [0x334bf7, 0xe5e6ed, 0xbfc6f3],\n sticks: 0xc5e8eb\n }\n },\n six: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'deepDistortion',\n length: 400,\n roadWidth: 18,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 50,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xff322f, 0xa33010, 0xa81508],\n rightCars: [0xfdfdf0, 0xf3dea0, 0xe2bb88],\n sticks: 0xfdfdf0\n }\n }\n};\n" } ], "registryDependencies": [], diff --git a/public/r/Hyperspeed-TS-CSS.json b/public/r/Hyperspeed-TS-CSS.json index a9feafec..29544b83 100644 --- a/public/r/Hyperspeed-TS-CSS.json +++ b/public/r/Hyperspeed-TS-CSS.json @@ -7,18 +7,19 @@ "files": [ { "type": "registry:component", - "path": "Hyperspeed/HyperSpeedPresets.ts", + "path": "HyperSpeedPresets.ts", "content": "export const hyperspeedPresets = {\n one: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 20,\n lightPairsPerRoadWay: 40,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.03, 400 * 0.2],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.8, 0.8],\n carFloorSeparation: [0, 5],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xd856bf, 0x6750a2, 0xc247ac],\n rightCars: [0x03b3c3, 0x0e5ea5, 0x324555],\n sticks: 0x03b3c3\n }\n },\n two: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'mountainDistortion',\n length: 400,\n roadWidth: 9,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 50,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xff102a, 0xeb383e, 0xff102a],\n rightCars: [0xdadafa, 0xbebae3, 0x8f97e4],\n sticks: 0xdadafa\n }\n },\n three: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'xyDistortion',\n length: 400,\n roadWidth: 9,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 3,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 30,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.02, 0.05],\n lightStickHeight: [0.3, 0.7],\n movingAwaySpeed: [20, 50],\n movingCloserSpeed: [-150, -230],\n carLightsLength: [400 * 0.05, 400 * 0.2],\n carLightsRadius: [0.03, 0.08],\n carWidthPercentage: [0.1, 0.5],\n carShiftX: [-0.5, 0.5],\n carFloorSeparation: [0, 0.1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0x7d0d1b, 0xa90519, 0xff102a],\n rightCars: [0xf1eece, 0xe6e2b1, 0xdfd98a],\n sticks: 0xf1eece\n }\n },\n four: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'LongRaceDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 5,\n lanesPerRoad: 2,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 70,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xff5f73, 0xe74d60, 0xff102a],\n rightCars: [0xa4e3e6, 0x80d1d4, 0x53c2c6],\n sticks: 0xa4e3e6\n }\n },\n five: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 9,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 50,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xdc5b20, 0xdca320, 0xdc2020],\n rightCars: [0x334bf7, 0xe5e6ed, 0xbfc6f3],\n sticks: 0xc5e8eb\n }\n },\n six: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'deepDistortion',\n length: 400,\n roadWidth: 18,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 50,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xff322f, 0xa33010, 0xa81508],\n rightCars: [0xfdfdf0, 0xf3dea0, 0xe2bb88],\n sticks: 0xfdfdf0\n }\n }\n};\n" }, { - "type": "registry:component", - "path": "Hyperspeed/Hyperspeed.css", + "type": "registry:file", + "path": "Hyperspeed.css", + "target": "@components/Hyperspeed.css", "content": "#lights {\n width: 100%;\n height: 100%;\n overflow: hidden;\n position: absolute;\n}\n\ncanvas {\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "Hyperspeed/Hyperspeed.tsx", - "content": "import { BloomEffect, EffectComposer, EffectPass, RenderPass, SMAAEffect, SMAAPreset } from 'postprocessing';\nimport { FC, useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\nimport './Hyperspeed.css';\n\ninterface Distortion {\n uniforms: Record;\n getDistortion: string;\n getJS?: (progress: number, time: number) => THREE.Vector3;\n}\n\ninterface Distortions {\n [key: string]: Distortion;\n}\n\ninterface Colors {\n roadColor: number;\n islandColor: number;\n background: number;\n shoulderLines: number;\n brokenLines: number;\n leftCars: number[];\n rightCars: number[];\n sticks: number;\n}\n\ninterface HyperspeedOptions {\n onSpeedUp?: (ev: MouseEvent | TouchEvent) => void;\n onSlowDown?: (ev: MouseEvent | TouchEvent) => void;\n distortion?: string | Distortion;\n length: number;\n roadWidth: number;\n islandWidth: number;\n lanesPerRoad: number;\n fov: number;\n fovSpeedUp: number;\n speedUp: number;\n carLightsFade: number;\n totalSideLightSticks: number;\n lightPairsPerRoadWay: number;\n shoulderLinesWidthPercentage: number;\n brokenLinesWidthPercentage: number;\n brokenLinesLengthPercentage: number;\n lightStickWidth: [number, number];\n lightStickHeight: [number, number];\n movingAwaySpeed: [number, number];\n movingCloserSpeed: [number, number];\n carLightsLength: [number, number];\n carLightsRadius: [number, number];\n carWidthPercentage: [number, number];\n carShiftX: [number, number];\n carFloorSeparation: [number, number];\n colors: Colors;\n isHyper?: boolean;\n}\n\ninterface HyperspeedProps {\n effectOptions?: Partial;\n}\n\nconst defaultOptions: HyperspeedOptions = {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 2,\n lanesPerRoad: 4,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 20,\n lightPairsPerRoadWay: 40,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.03, 400 * 0.2],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.8, 0.8],\n carFloorSeparation: [0, 5],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0xffffff,\n brokenLines: 0xffffff,\n leftCars: [0xd856bf, 0x6750a2, 0xc247ac],\n rightCars: [0x03b3c3, 0x0e5ea5, 0x324555],\n sticks: 0x03b3c3\n }\n};\n\nfunction nsin(val: number) {\n return Math.sin(val) * 0.5 + 0.5;\n}\n\nconst mountainUniforms = {\n uFreq: { value: new THREE.Vector3(3, 6, 10) },\n uAmp: { value: new THREE.Vector3(30, 30, 20) }\n};\n\nconst xyUniforms = {\n uFreq: { value: new THREE.Vector2(5, 2) },\n uAmp: { value: new THREE.Vector2(25, 15) }\n};\n\nconst LongRaceUniforms = {\n uFreq: { value: new THREE.Vector2(2, 3) },\n uAmp: { value: new THREE.Vector2(35, 10) }\n};\n\nconst turbulentUniforms = {\n uFreq: { value: new THREE.Vector4(4, 8, 8, 1) },\n uAmp: { value: new THREE.Vector4(25, 5, 10, 10) }\n};\n\nconst deepUniforms = {\n uFreq: { value: new THREE.Vector2(4, 8) },\n uAmp: { value: new THREE.Vector2(10, 20) },\n uPowY: { value: new THREE.Vector2(20, 2) }\n};\n\nconst distortions: Distortions = {\n mountainDistortion: {\n uniforms: mountainUniforms,\n getDistortion: `\n uniform vec3 uAmp;\n uniform vec3 uFreq;\n #define PI 3.14159265358979\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n vec3 getDistortion(float progress){\n float movementProgressFix = 0.02;\n return vec3( \n cos(progress * PI * uFreq.x + uTime) * uAmp.x - cos(movementProgressFix * PI * uFreq.x + uTime) * uAmp.x,\n nsin(progress * PI * uFreq.y + uTime) * uAmp.y - nsin(movementProgressFix * PI * uFreq.y + uTime) * uAmp.y,\n nsin(progress * PI * uFreq.z + uTime) * uAmp.z - nsin(movementProgressFix * PI * uFreq.z + uTime) * uAmp.z\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const movementProgressFix = 0.02;\n const uFreq = mountainUniforms.uFreq.value;\n const uAmp = mountainUniforms.uAmp.value;\n const distortion = new THREE.Vector3(\n Math.cos(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.cos(movementProgressFix * Math.PI * uFreq.x + time) * uAmp.x,\n nsin(progress * Math.PI * uFreq.y + time) * uAmp.y -\n nsin(movementProgressFix * Math.PI * uFreq.y + time) * uAmp.y,\n nsin(progress * Math.PI * uFreq.z + time) * uAmp.z -\n nsin(movementProgressFix * Math.PI * uFreq.z + time) * uAmp.z\n );\n const lookAtAmp = new THREE.Vector3(2, 2, 2);\n const lookAtOffset = new THREE.Vector3(0, 0, -5);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n xyDistortion: {\n uniforms: xyUniforms,\n getDistortion: `\n uniform vec2 uFreq;\n uniform vec2 uAmp;\n #define PI 3.14159265358979\n vec3 getDistortion(float progress){\n float movementProgressFix = 0.02;\n return vec3( \n cos(progress * PI * uFreq.x + uTime) * uAmp.x - cos(movementProgressFix * PI * uFreq.x + uTime) * uAmp.x,\n sin(progress * PI * uFreq.y + PI/2. + uTime) * uAmp.y - sin(movementProgressFix * PI * uFreq.y + PI/2. + uTime) * uAmp.y,\n 0.\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const movementProgressFix = 0.02;\n const uFreq = xyUniforms.uFreq.value;\n const uAmp = xyUniforms.uAmp.value;\n const distortion = new THREE.Vector3(\n Math.cos(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.cos(movementProgressFix * Math.PI * uFreq.x + time) * uAmp.x,\n Math.sin(progress * Math.PI * uFreq.y + time + Math.PI / 2) * uAmp.y -\n Math.sin(movementProgressFix * Math.PI * uFreq.y + time + Math.PI / 2) * uAmp.y,\n 0\n );\n const lookAtAmp = new THREE.Vector3(2, 0.4, 1);\n const lookAtOffset = new THREE.Vector3(0, 0, -3);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n LongRaceDistortion: {\n uniforms: LongRaceUniforms,\n getDistortion: `\n uniform vec2 uFreq;\n uniform vec2 uAmp;\n #define PI 3.14159265358979\n vec3 getDistortion(float progress){\n float camProgress = 0.0125;\n return vec3( \n sin(progress * PI * uFreq.x + uTime) * uAmp.x - sin(camProgress * PI * uFreq.x + uTime) * uAmp.x,\n sin(progress * PI * uFreq.y + uTime) * uAmp.y - sin(camProgress * PI * uFreq.y + uTime) * uAmp.y,\n 0.\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const camProgress = 0.0125;\n const uFreq = LongRaceUniforms.uFreq.value;\n const uAmp = LongRaceUniforms.uAmp.value;\n const distortion = new THREE.Vector3(\n Math.sin(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.sin(camProgress * Math.PI * uFreq.x + time) * uAmp.x,\n Math.sin(progress * Math.PI * uFreq.y + time) * uAmp.y -\n Math.sin(camProgress * Math.PI * uFreq.y + time) * uAmp.y,\n 0\n );\n const lookAtAmp = new THREE.Vector3(1, 1, 0);\n const lookAtOffset = new THREE.Vector3(0, 0, -5);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n turbulentDistortion: {\n uniforms: turbulentUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n cos(PI * progress * uFreq.r + uTime) * uAmp.r +\n pow(cos(PI * progress * uFreq.g + uTime * (uFreq.g / uFreq.r)), 2. ) * uAmp.g\n );\n }\n float getDistortionY(float progress){\n return (\n -nsin(PI * progress * uFreq.b + uTime) * uAmp.b +\n -pow(nsin(PI * progress * uFreq.a + uTime / (uFreq.b / uFreq.a)), 5.) * uAmp.a\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.0125),\n getDistortionY(progress) - getDistortionY(0.0125),\n 0.\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const uFreq = turbulentUniforms.uFreq.value;\n const uAmp = turbulentUniforms.uAmp.value;\n\n const getX = (p: number) =>\n Math.cos(Math.PI * p * uFreq.x + time) * uAmp.x +\n Math.pow(Math.cos(Math.PI * p * uFreq.y + time * (uFreq.y / uFreq.x)), 2) * uAmp.y;\n\n const getY = (p: number) =>\n -nsin(Math.PI * p * uFreq.z + time) * uAmp.z -\n Math.pow(nsin(Math.PI * p * uFreq.w + time / (uFreq.z / uFreq.w)), 5) * uAmp.w;\n\n const distortion = new THREE.Vector3(\n getX(progress) - getX(progress + 0.007),\n getY(progress) - getY(progress + 0.007),\n 0\n );\n const lookAtAmp = new THREE.Vector3(-2, -5, 0);\n const lookAtOffset = new THREE.Vector3(0, 0, -10);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n turbulentDistortionStill: {\n uniforms: turbulentUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n cos(PI * progress * uFreq.r) * uAmp.r +\n pow(cos(PI * progress * uFreq.g * (uFreq.g / uFreq.r)), 2. ) * uAmp.g\n );\n }\n float getDistortionY(float progress){\n return (\n -nsin(PI * progress * uFreq.b) * uAmp.b +\n -pow(nsin(PI * progress * uFreq.a / (uFreq.b / uFreq.a)), 5.) * uAmp.a\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.02),\n 0.\n );\n }\n `\n },\n deepDistortionStill: {\n uniforms: deepUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n uniform vec2 uPowY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n sin(progress * PI * uFreq.x) * uAmp.x * 2.\n );\n }\n float getDistortionY(float progress){\n return (\n pow(abs(progress * uPowY.x), uPowY.y) + sin(progress * PI * uFreq.y) * uAmp.y\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.05),\n 0.\n );\n }\n `\n },\n deepDistortion: {\n uniforms: deepUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n uniform vec2 uPowY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n sin(progress * PI * uFreq.x + uTime) * uAmp.x\n );\n }\n float getDistortionY(float progress){\n return (\n pow(abs(progress * uPowY.x), uPowY.y) + sin(progress * PI * uFreq.y + uTime) * uAmp.y\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.02),\n 0.\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const uFreq = deepUniforms.uFreq.value;\n const uAmp = deepUniforms.uAmp.value;\n const uPowY = deepUniforms.uPowY.value;\n\n const getX = (p: number) => Math.sin(p * Math.PI * uFreq.x + time) * uAmp.x;\n const getY = (p: number) => Math.pow(p * uPowY.x, uPowY.y) + Math.sin(p * Math.PI * uFreq.y + time) * uAmp.y;\n\n const distortion = new THREE.Vector3(\n getX(progress) - getX(progress + 0.01),\n getY(progress) - getY(progress + 0.01),\n 0\n );\n const lookAtAmp = new THREE.Vector3(-2, -4, 0);\n const lookAtOffset = new THREE.Vector3(0, 0, -10);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n }\n};\n\nconst distortion_uniforms = {\n uDistortionX: { value: new THREE.Vector2(80, 3) },\n uDistortionY: { value: new THREE.Vector2(-40, 2.5) }\n};\n\nconst distortion_vertex = `\n #define PI 3.14159265358979\n uniform vec2 uDistortionX;\n uniform vec2 uDistortionY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n vec3 getDistortion(float progress){\n progress = clamp(progress, 0., 1.);\n float xAmp = uDistortionX.r;\n float xFreq = uDistortionX.g;\n float yAmp = uDistortionY.r;\n float yFreq = uDistortionY.g;\n return vec3( \n xAmp * nsin(progress * PI * xFreq - PI / 2.),\n yAmp * nsin(progress * PI * yFreq - PI / 2.),\n 0.\n );\n }\n`;\n\nfunction random(base: number | [number, number]): number {\n if (Array.isArray(base)) {\n return Math.random() * (base[1] - base[0]) + base[0];\n }\n return Math.random() * base;\n}\n\nfunction pickRandom(arr: T | T[]): T {\n if (Array.isArray(arr)) {\n return arr[Math.floor(Math.random() * arr.length)];\n }\n return arr;\n}\n\nfunction lerp(current: number, target: number, speed = 0.1, limit = 0.001): number {\n let change = (target - current) * speed;\n if (Math.abs(change) < limit) {\n change = target - current;\n }\n return change;\n}\n\nclass CarLights {\n webgl: App;\n options: HyperspeedOptions;\n colors: number[] | THREE.Color;\n speed: [number, number];\n fade: THREE.Vector2;\n mesh!: THREE.Mesh;\n\n constructor(\n webgl: App,\n options: HyperspeedOptions,\n colors: number[] | THREE.Color,\n speed: [number, number],\n fade: THREE.Vector2\n ) {\n this.webgl = webgl;\n this.options = options;\n this.colors = colors;\n this.speed = speed;\n this.fade = fade;\n }\n\n init() {\n const options = this.options;\n const curve = new THREE.LineCurve3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -1));\n const geometry = new THREE.TubeGeometry(curve, 40, 1, 8, false);\n\n const instanced = new THREE.InstancedBufferGeometry().copy(geometry as any) as THREE.InstancedBufferGeometry;\n instanced.instanceCount = options.lightPairsPerRoadWay * 2;\n\n const laneWidth = options.roadWidth / options.lanesPerRoad;\n\n const aOffset: number[] = [];\n const aMetrics: number[] = [];\n const aColor: number[] = [];\n\n let colorArray: THREE.Color[];\n if (Array.isArray(this.colors)) {\n colorArray = this.colors.map(c => new THREE.Color(c));\n } else {\n colorArray = [new THREE.Color(this.colors)];\n }\n\n for (let i = 0; i < options.lightPairsPerRoadWay; i++) {\n const radius = random(options.carLightsRadius);\n const length = random(options.carLightsLength);\n const spd = random(this.speed);\n\n const carLane = i % options.lanesPerRoad;\n let laneX = carLane * laneWidth - options.roadWidth / 2 + laneWidth / 2;\n\n const carWidth = random(options.carWidthPercentage) * laneWidth;\n const carShiftX = random(options.carShiftX) * laneWidth;\n laneX += carShiftX;\n\n const offsetY = random(options.carFloorSeparation) + radius * 1.3;\n const offsetZ = -random(options.length);\n\n aOffset.push(laneX - carWidth / 2);\n aOffset.push(offsetY);\n aOffset.push(offsetZ);\n\n aOffset.push(laneX + carWidth / 2);\n aOffset.push(offsetY);\n aOffset.push(offsetZ);\n\n aMetrics.push(radius);\n aMetrics.push(length);\n aMetrics.push(spd);\n\n aMetrics.push(radius);\n aMetrics.push(length);\n aMetrics.push(spd);\n\n const color = pickRandom(colorArray);\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n }\n\n instanced.setAttribute('aOffset', new THREE.InstancedBufferAttribute(new Float32Array(aOffset), 3, false));\n instanced.setAttribute('aMetrics', new THREE.InstancedBufferAttribute(new Float32Array(aMetrics), 3, false));\n instanced.setAttribute('aColor', new THREE.InstancedBufferAttribute(new Float32Array(aColor), 3, false));\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: carLightsFragment,\n vertexShader: carLightsVertex,\n transparent: true,\n uniforms: Object.assign(\n {\n uTime: { value: 0 },\n uTravelLength: { value: options.length },\n uFade: { value: this.fade }\n },\n this.webgl.fogUniforms,\n (typeof this.options.distortion === 'object' ? this.options.distortion.uniforms : {}) || {}\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n typeof this.options.distortion === 'object' ? this.options.distortion.getDistortion : ''\n );\n };\n\n const mesh = new THREE.Mesh(instanced, material);\n mesh.frustumCulled = false;\n this.webgl.scene.add(mesh);\n this.mesh = mesh;\n }\n\n update(time: number) {\n if (this.mesh.material.uniforms.uTime) {\n this.mesh.material.uniforms.uTime.value = time;\n }\n }\n}\n\nconst carLightsFragment = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_fragment']}\n varying vec3 vColor;\n varying vec2 vUv; \n uniform vec2 uFade;\n void main() {\n vec3 color = vec3(vColor);\n float alpha = smoothstep(uFade.x, uFade.y, vUv.x);\n gl_FragColor = vec4(color, alpha);\n if (gl_FragColor.a < 0.0001) discard;\n ${THREE.ShaderChunk['fog_fragment']}\n }\n`;\n\nconst carLightsVertex = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n attribute vec3 aOffset;\n attribute vec3 aMetrics;\n attribute vec3 aColor;\n uniform float uTravelLength;\n uniform float uTime;\n varying vec2 vUv; \n varying vec3 vColor; \n #include \n void main() {\n vec3 transformed = position.xyz;\n float radius = aMetrics.r;\n float myLength = aMetrics.g;\n float speed = aMetrics.b;\n\n transformed.xy *= radius;\n transformed.z *= myLength;\n\n transformed.z += myLength - mod(uTime * speed + aOffset.z, uTravelLength);\n transformed.xy += aOffset.xy;\n\n float progress = abs(transformed.z / uTravelLength);\n transformed.xyz += getDistortion(progress);\n\n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vUv = uv;\n vColor = aColor;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n`;\n\nclass LightsSticks {\n webgl: App;\n options: HyperspeedOptions;\n mesh!: THREE.Mesh;\n\n constructor(webgl: App, options: HyperspeedOptions) {\n this.webgl = webgl;\n this.options = options;\n }\n\n init() {\n const options = this.options;\n const geometry = new THREE.PlaneGeometry(1, 1);\n const instanced = new THREE.InstancedBufferGeometry().copy(geometry as any) as THREE.InstancedBufferGeometry;\n const totalSticks = options.totalSideLightSticks;\n instanced.instanceCount = totalSticks;\n\n const stickoffset = options.length / (totalSticks - 1);\n const aOffset: number[] = [];\n const aColor: number[] = [];\n const aMetrics: number[] = [];\n\n let colorArray: THREE.Color[];\n if (Array.isArray(options.colors.sticks)) {\n colorArray = options.colors.sticks.map(c => new THREE.Color(c));\n } else {\n colorArray = [new THREE.Color(options.colors.sticks)];\n }\n\n for (let i = 0; i < totalSticks; i++) {\n const width = random(options.lightStickWidth);\n const height = random(options.lightStickHeight);\n aOffset.push((i - 1) * stickoffset * 2 + stickoffset * Math.random());\n\n const color = pickRandom(colorArray);\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n\n aMetrics.push(width);\n aMetrics.push(height);\n }\n\n instanced.setAttribute('aOffset', new THREE.InstancedBufferAttribute(new Float32Array(aOffset), 1, false));\n instanced.setAttribute('aColor', new THREE.InstancedBufferAttribute(new Float32Array(aColor), 3, false));\n instanced.setAttribute('aMetrics', new THREE.InstancedBufferAttribute(new Float32Array(aMetrics), 2, false));\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: sideSticksFragment,\n vertexShader: sideSticksVertex,\n side: THREE.DoubleSide,\n uniforms: Object.assign(\n {\n uTravelLength: { value: options.length },\n uTime: { value: 0 }\n },\n this.webgl.fogUniforms,\n (typeof options.distortion === 'object' ? options.distortion.uniforms : {}) || {}\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n typeof this.options.distortion === 'object' ? this.options.distortion.getDistortion : ''\n );\n };\n\n const mesh = new THREE.Mesh(instanced, material);\n mesh.frustumCulled = false;\n this.webgl.scene.add(mesh);\n this.mesh = mesh;\n }\n\n update(time: number) {\n if (this.mesh.material.uniforms.uTime) {\n this.mesh.material.uniforms.uTime.value = time;\n }\n }\n}\n\nconst sideSticksVertex = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n attribute float aOffset;\n attribute vec3 aColor;\n attribute vec2 aMetrics;\n uniform float uTravelLength;\n uniform float uTime;\n varying vec3 vColor;\n mat4 rotationY( in float angle ) {\n return mat4(\n cos(angle),\t\t0,\t\tsin(angle),\t0,\n 0,\t\t 1.0,\t0,\t\t\t0,\n -sin(angle),\t 0,\t\tcos(angle),\t0,\n 0, \t\t 0,\t\t0,\t\t\t1\n );\n }\n #include \n void main(){\n vec3 transformed = position.xyz;\n float width = aMetrics.x;\n float height = aMetrics.y;\n\n transformed.xy *= vec2(width, height);\n float time = mod(uTime * 60. * 2. + aOffset, uTravelLength);\n\n transformed = (rotationY(3.14/2.) * vec4(transformed,1.)).xyz;\n transformed.z += - uTravelLength + time;\n\n float progress = abs(transformed.z / uTravelLength);\n transformed.xyz += getDistortion(progress);\n\n transformed.y += height / 2.;\n transformed.x += -width / 2.;\n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vColor = aColor;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n`;\n\nconst sideSticksFragment = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_fragment']}\n varying vec3 vColor;\n void main(){\n vec3 color = vec3(vColor);\n gl_FragColor = vec4(color,1.);\n ${THREE.ShaderChunk['fog_fragment']}\n }\n`;\n\nclass Road {\n webgl: App;\n options: HyperspeedOptions;\n uTime: { value: number };\n leftRoadWay!: THREE.Mesh;\n rightRoadWay!: THREE.Mesh;\n island!: THREE.Mesh;\n\n constructor(webgl: App, options: HyperspeedOptions) {\n this.webgl = webgl;\n this.options = options;\n this.uTime = { value: 0 };\n }\n\n createPlane(side: number, width: number, isRoad: boolean) {\n const options = this.options;\n const segments = 100;\n const geometry = new THREE.PlaneGeometry(\n isRoad ? options.roadWidth : options.islandWidth,\n options.length,\n 20,\n segments\n );\n\n let uniforms: Record = {\n uTravelLength: { value: options.length },\n uColor: {\n value: new THREE.Color(isRoad ? options.colors.roadColor : options.colors.islandColor)\n },\n uTime: this.uTime\n };\n\n if (isRoad) {\n uniforms = Object.assign(uniforms, {\n uLanes: { value: options.lanesPerRoad },\n uBrokenLinesColor: {\n value: new THREE.Color(options.colors.brokenLines)\n },\n uShoulderLinesColor: {\n value: new THREE.Color(options.colors.shoulderLines)\n },\n uShoulderLinesWidthPercentage: {\n value: options.shoulderLinesWidthPercentage\n },\n uBrokenLinesLengthPercentage: {\n value: options.brokenLinesLengthPercentage\n },\n uBrokenLinesWidthPercentage: {\n value: options.brokenLinesWidthPercentage\n }\n });\n }\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: isRoad ? roadFragment : islandFragment,\n vertexShader: roadVertex,\n side: THREE.DoubleSide,\n uniforms: Object.assign(\n uniforms,\n this.webgl.fogUniforms,\n (typeof options.distortion === 'object' ? options.distortion.uniforms : {}) || {}\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n typeof this.options.distortion === 'object' ? this.options.distortion.getDistortion : ''\n );\n };\n\n const mesh = new THREE.Mesh(geometry, material);\n mesh.rotation.x = -Math.PI / 2;\n mesh.position.z = -options.length / 2;\n mesh.position.x += (this.options.islandWidth / 2 + options.roadWidth / 2) * side;\n\n this.webgl.scene.add(mesh);\n return mesh;\n }\n\n init() {\n this.leftRoadWay = this.createPlane(-1, this.options.roadWidth, true);\n this.rightRoadWay = this.createPlane(1, this.options.roadWidth, true);\n this.island = this.createPlane(0, this.options.islandWidth, false);\n }\n\n update(time: number) {\n this.uTime.value = time;\n }\n}\n\nconst roadBaseFragment = `\n #define USE_FOG;\n varying vec2 vUv; \n uniform vec3 uColor;\n uniform float uTime;\n #include \n ${THREE.ShaderChunk['fog_pars_fragment']}\n void main() {\n vec2 uv = vUv;\n vec3 color = vec3(uColor);\n #include \n gl_FragColor = vec4(color, 1.);\n ${THREE.ShaderChunk['fog_fragment']}\n }\n`;\n\nconst islandFragment = roadBaseFragment\n .replace('#include ', '')\n .replace('#include ', '');\n\nconst roadMarkings_vars = `\n uniform float uLanes;\n uniform vec3 uBrokenLinesColor;\n uniform vec3 uShoulderLinesColor;\n uniform float uShoulderLinesWidthPercentage;\n uniform float uBrokenLinesWidthPercentage;\n uniform float uBrokenLinesLengthPercentage;\n highp float random(vec2 co) {\n highp float a = 12.9898;\n highp float b = 78.233;\n highp float c = 43758.5453;\n highp float dt = dot(co.xy, vec2(a, b));\n highp float sn = mod(dt, 3.14);\n return fract(sin(sn) * c);\n }\n`;\n\nconst roadMarkings_fragment = `\n uv.y = mod(uv.y + uTime * 0.05, 1.);\n float laneWidth = 1.0 / uLanes;\n float brokenLineWidth = laneWidth * uBrokenLinesWidthPercentage;\n float laneEmptySpace = 1. - uBrokenLinesLengthPercentage;\n\n float brokenLines = step(1.0 - brokenLineWidth, fract(uv.x * 2.0)) * step(laneEmptySpace, fract(uv.y * 10.0));\n float sideLines = step(1.0 - brokenLineWidth, fract((uv.x - laneWidth * (uLanes - 1.0)) * 2.0)) + step(brokenLineWidth, uv.x);\n\n brokenLines = mix(brokenLines, sideLines, uv.x);\n`;\n\nconst roadFragment = roadBaseFragment\n .replace('#include ', roadMarkings_fragment)\n .replace('#include ', roadMarkings_vars);\n\nconst roadVertex = `\n #define USE_FOG;\n uniform float uTime;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n uniform float uTravelLength;\n varying vec2 vUv; \n #include \n void main() {\n vec3 transformed = position.xyz;\n vec3 distortion = getDistortion((transformed.y + uTravelLength / 2.) / uTravelLength);\n transformed.x += distortion.x;\n transformed.z += distortion.y;\n transformed.y += -1. * distortion.z; \n \n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vUv = uv;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n`;\n\nfunction resizeRendererToDisplaySize(\n renderer: THREE.WebGLRenderer,\n setSize: (width: number, height: number, updateStyle: boolean) => void\n) {\n const canvas = renderer.domElement;\n const width = canvas.clientWidth;\n const height = canvas.clientHeight;\n if (width <= 0 || height <= 0) return false;\n const needResize = canvas.width !== width || canvas.height !== height;\n if (needResize) {\n setSize(width, height, false);\n }\n return needResize;\n}\n\nclass App {\n container: HTMLElement;\n options: HyperspeedOptions;\n renderer: THREE.WebGLRenderer;\n composer: EffectComposer;\n camera: THREE.PerspectiveCamera;\n scene: THREE.Scene;\n renderPass!: RenderPass;\n bloomPass!: EffectPass;\n clock: THREE.Clock;\n assets: Record;\n disposed: boolean;\n road: Road;\n leftCarLights: CarLights;\n rightCarLights: CarLights;\n leftSticks: LightsSticks;\n fogUniforms: Record;\n fovTarget: number;\n speedUpTarget: number;\n speedUp: number;\n timeOffset: number;\n hasValidSize: boolean;\n\n constructor(container: HTMLElement, options: HyperspeedOptions) {\n this.options = options;\n if (!this.options.distortion) {\n this.options.distortion = {\n uniforms: distortion_uniforms,\n getDistortion: distortion_vertex\n };\n }\n this.container = container;\n this.hasValidSize = false;\n\n const initW = Math.max(1, container.offsetWidth);\n const initH = Math.max(1, container.offsetHeight);\n\n this.renderer = new THREE.WebGLRenderer({\n antialias: false,\n alpha: true\n });\n this.renderer.setSize(initW, initH, false);\n this.renderer.setPixelRatio(window.devicePixelRatio);\n\n this.composer = new EffectComposer(this.renderer);\n container.appendChild(this.renderer.domElement);\n\n this.camera = new THREE.PerspectiveCamera(options.fov, initW / initH, 0.1, 10000);\n this.camera.position.z = -5;\n this.camera.position.y = 8;\n this.camera.position.x = 0;\n\n this.scene = new THREE.Scene();\n this.scene.background = null;\n\n const fog = new THREE.Fog(options.colors.background, options.length * 0.2, options.length * 500);\n this.scene.fog = fog;\n\n this.fogUniforms = {\n fogColor: { value: fog.color },\n fogNear: { value: fog.near },\n fogFar: { value: fog.far }\n };\n\n this.clock = new THREE.Clock();\n this.assets = {};\n this.disposed = false;\n\n this.road = new Road(this, options);\n this.leftCarLights = new CarLights(\n this,\n options,\n options.colors.leftCars,\n options.movingAwaySpeed,\n new THREE.Vector2(0, 1 - options.carLightsFade)\n );\n this.rightCarLights = new CarLights(\n this,\n options,\n options.colors.rightCars,\n options.movingCloserSpeed,\n new THREE.Vector2(1, 0 + options.carLightsFade)\n );\n this.leftSticks = new LightsSticks(this, options);\n\n this.fovTarget = options.fov;\n this.speedUpTarget = 0;\n this.speedUp = 0;\n this.timeOffset = 0;\n\n this.tick = this.tick.bind(this);\n this.init = this.init.bind(this);\n this.setSize = this.setSize.bind(this);\n this.onMouseDown = this.onMouseDown.bind(this);\n this.onMouseUp = this.onMouseUp.bind(this);\n\n this.onTouchStart = this.onTouchStart.bind(this);\n this.onTouchEnd = this.onTouchEnd.bind(this);\n this.onContextMenu = this.onContextMenu.bind(this);\n\n this.onWindowResize = this.onWindowResize.bind(this);\n window.addEventListener('resize', this.onWindowResize);\n\n if (container.offsetWidth > 0 && container.offsetHeight > 0) {\n this.hasValidSize = true;\n }\n }\n\n onWindowResize() {\n const width = this.container.offsetWidth;\n const height = this.container.offsetHeight;\n\n if (width <= 0 || height <= 0) {\n this.hasValidSize = false;\n return;\n }\n\n this.renderer.setSize(width, height);\n this.camera.aspect = width / height;\n this.camera.updateProjectionMatrix();\n this.composer.setSize(width, height);\n this.hasValidSize = true;\n }\n\n initPasses() {\n this.renderPass = new RenderPass(this.scene, this.camera);\n this.bloomPass = new EffectPass(\n this.camera,\n new BloomEffect({\n luminanceThreshold: 0.2,\n luminanceSmoothing: 0,\n resolutionScale: 1\n })\n );\n\n const smaaPass = new EffectPass(\n this.camera,\n new SMAAEffect({\n preset: SMAAPreset.MEDIUM\n })\n );\n this.renderPass.renderToScreen = false;\n this.bloomPass.renderToScreen = false;\n smaaPass.renderToScreen = true;\n\n this.composer.addPass(this.renderPass);\n this.composer.addPass(this.bloomPass);\n this.composer.addPass(smaaPass);\n }\n\n loadAssets(): Promise {\n const assets = this.assets;\n return new Promise(resolve => {\n const manager = new THREE.LoadingManager(resolve);\n\n const searchImage = new Image();\n const areaImage = new Image();\n assets.smaa = {};\n\n searchImage.addEventListener('load', function () {\n assets.smaa.search = this;\n manager.itemEnd('smaa-search');\n });\n\n areaImage.addEventListener('load', function () {\n assets.smaa.area = this;\n manager.itemEnd('smaa-area');\n });\n\n manager.itemStart('smaa-search');\n manager.itemStart('smaa-area');\n\n searchImage.src = SMAAEffect.searchImageDataURL;\n areaImage.src = SMAAEffect.areaImageDataURL;\n });\n }\n\n init() {\n this.initPasses();\n const options = this.options;\n this.road.init();\n this.leftCarLights.init();\n this.leftCarLights.mesh.position.setX(-options.roadWidth / 2 - options.islandWidth / 2);\n\n this.rightCarLights.init();\n this.rightCarLights.mesh.position.setX(options.roadWidth / 2 + options.islandWidth / 2);\n\n this.leftSticks.init();\n this.leftSticks.mesh.position.setX(-(options.roadWidth + options.islandWidth / 2));\n\n this.container.addEventListener('mousedown', this.onMouseDown);\n this.container.addEventListener('mouseup', this.onMouseUp);\n this.container.addEventListener('mouseout', this.onMouseUp);\n\n this.container.addEventListener('touchstart', this.onTouchStart, { passive: true });\n this.container.addEventListener('touchend', this.onTouchEnd, { passive: true });\n this.container.addEventListener('touchcancel', this.onTouchEnd, { passive: true });\n this.container.addEventListener('contextmenu', this.onContextMenu);\n\n this.tick();\n }\n\n onMouseDown(ev: MouseEvent) {\n if (this.options.onSpeedUp) this.options.onSpeedUp(ev);\n this.fovTarget = this.options.fovSpeedUp;\n this.speedUpTarget = this.options.speedUp;\n }\n\n onMouseUp(ev: MouseEvent) {\n if (this.options.onSlowDown) this.options.onSlowDown(ev);\n this.fovTarget = this.options.fov;\n this.speedUpTarget = 0;\n }\n\n onTouchStart(ev: TouchEvent) {\n if (this.options.onSpeedUp) this.options.onSpeedUp(ev);\n this.fovTarget = this.options.fovSpeedUp;\n this.speedUpTarget = this.options.speedUp;\n }\n\n onTouchEnd(ev: TouchEvent) {\n if (this.options.onSlowDown) this.options.onSlowDown(ev);\n this.fovTarget = this.options.fov;\n this.speedUpTarget = 0;\n }\n\n onContextMenu(ev: MouseEvent) {\n ev.preventDefault();\n }\n\n update(delta: number) {\n const lerpPercentage = Math.exp(-(-60 * Math.log2(1 - 0.1)) * delta);\n this.speedUp += lerp(this.speedUp, this.speedUpTarget, lerpPercentage, 0.00001);\n this.timeOffset += this.speedUp * delta;\n const time = this.clock.elapsedTime + this.timeOffset;\n\n this.rightCarLights.update(time);\n this.leftCarLights.update(time);\n this.leftSticks.update(time);\n this.road.update(time);\n\n let updateCamera = false;\n const fovChange = lerp(this.camera.fov, this.fovTarget, lerpPercentage);\n if (fovChange !== 0) {\n this.camera.fov += fovChange * delta * 6;\n updateCamera = true;\n }\n\n if (typeof this.options.distortion === 'object' && this.options.distortion.getJS) {\n const distortion = this.options.distortion.getJS(0.025, time);\n this.camera.lookAt(\n new THREE.Vector3(\n this.camera.position.x + distortion.x,\n this.camera.position.y + distortion.y,\n this.camera.position.z + distortion.z\n )\n );\n updateCamera = true;\n }\n\n if (updateCamera) {\n this.camera.updateProjectionMatrix();\n }\n }\n\n render(delta: number) {\n this.composer.render(delta);\n }\n\n dispose() {\n this.disposed = true;\n\n if (this.scene) {\n this.scene.traverse(object => {\n const obj = object as unknown as THREE.Mesh;\n if (!obj.isMesh) return;\n\n if (obj.geometry) obj.geometry.dispose();\n\n if (obj.material) {\n if (Array.isArray(obj.material)) {\n obj.material.forEach(material => material.dispose());\n } else {\n obj.material.dispose();\n }\n }\n });\n this.scene.clear();\n }\n\n if (this.renderer) {\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n if (this.renderer.domElement && this.renderer.domElement.parentNode) {\n this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);\n }\n }\n if (this.composer) {\n this.composer.dispose();\n }\n\n window.removeEventListener('resize', this.onWindowResize);\n if (this.container) {\n this.container.removeEventListener('mousedown', this.onMouseDown);\n this.container.removeEventListener('mouseup', this.onMouseUp);\n this.container.removeEventListener('mouseout', this.onMouseUp);\n\n this.container.removeEventListener('touchstart', this.onTouchStart);\n this.container.removeEventListener('touchend', this.onTouchEnd);\n this.container.removeEventListener('touchcancel', this.onTouchEnd);\n this.container.removeEventListener('contextmenu', this.onContextMenu);\n }\n }\n\n setSize(width: number, height: number, updateStyles: boolean) {\n this.composer.setSize(width, height, updateStyles);\n }\n\n tick() {\n if (this.disposed) return;\n\n if (!this.hasValidSize) {\n const w = this.container.offsetWidth;\n const h = this.container.offsetHeight;\n if (w > 0 && h > 0) {\n this.renderer.setSize(w, h, false);\n this.camera.aspect = w / h;\n this.camera.updateProjectionMatrix();\n this.composer.setSize(w, h);\n this.hasValidSize = true;\n } else {\n requestAnimationFrame(this.tick);\n return;\n }\n }\n\n if (resizeRendererToDisplaySize(this.renderer, this.setSize)) {\n const canvas = this.renderer.domElement;\n if (this.hasValidSize) {\n this.camera.aspect = canvas.clientWidth / canvas.clientHeight;\n this.camera.updateProjectionMatrix();\n }\n }\n\n if (this.hasValidSize) {\n const delta = this.clock.getDelta();\n this.render(delta);\n this.update(delta);\n }\n\n requestAnimationFrame(this.tick);\n }\n}\n\nconst DEFAULT_EFFECT_OPTIONS: Partial = {};\n\nconst Hyperspeed: FC = ({ effectOptions = DEFAULT_EFFECT_OPTIONS }) => {\n const hyperspeed = useRef(null);\n const appRef = useRef(null);\n\n useEffect(() => {\n if (appRef.current) {\n appRef.current.dispose();\n appRef.current = null;\n const container = hyperspeed.current;\n if (container) {\n while (container.firstChild) {\n container.removeChild(container.firstChild);\n }\n }\n }\n\n const container = hyperspeed.current;\n if (!container) return;\n\n const options: HyperspeedOptions = {\n ...defaultOptions,\n ...effectOptions,\n colors: { ...defaultOptions.colors, ...effectOptions.colors }\n };\n if (typeof options.distortion === 'string') {\n options.distortion = distortions[options.distortion];\n }\n\n const myApp = new App(container, options);\n appRef.current = myApp;\n myApp.loadAssets().then(myApp.init);\n\n return () => {\n if (appRef.current) {\n appRef.current.dispose();\n }\n };\n }, [effectOptions]);\n\n return
;\n};\n\nexport default Hyperspeed;\n" + "path": "Hyperspeed.tsx", + "content": "import { BloomEffect, EffectComposer, EffectPass, RenderPass, SMAAEffect, SMAAPreset } from 'postprocessing';\nimport { type FC, useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\nimport './Hyperspeed.css';\n\ninterface Distortion {\n uniforms: Record;\n getDistortion: string;\n getJS?: (progress: number, time: number) => THREE.Vector3;\n}\n\ninterface Distortions {\n [key: string]: Distortion;\n}\n\ninterface Colors {\n roadColor: number;\n islandColor: number;\n background: number;\n shoulderLines: number;\n brokenLines: number;\n leftCars: number[];\n rightCars: number[];\n sticks: number;\n}\n\ninterface HyperspeedOptions {\n onSpeedUp?: (ev: MouseEvent | TouchEvent) => void;\n onSlowDown?: (ev: MouseEvent | TouchEvent) => void;\n distortion?: string | Distortion;\n length: number;\n roadWidth: number;\n islandWidth: number;\n lanesPerRoad: number;\n fov: number;\n fovSpeedUp: number;\n speedUp: number;\n carLightsFade: number;\n totalSideLightSticks: number;\n lightPairsPerRoadWay: number;\n shoulderLinesWidthPercentage: number;\n brokenLinesWidthPercentage: number;\n brokenLinesLengthPercentage: number;\n lightStickWidth: [number, number];\n lightStickHeight: [number, number];\n movingAwaySpeed: [number, number];\n movingCloserSpeed: [number, number];\n carLightsLength: [number, number];\n carLightsRadius: [number, number];\n carWidthPercentage: [number, number];\n carShiftX: [number, number];\n carFloorSeparation: [number, number];\n colors: Colors;\n isHyper?: boolean;\n}\n\ninterface HyperspeedProps {\n effectOptions?: Partial;\n}\n\nconst defaultOptions: HyperspeedOptions = {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 2,\n lanesPerRoad: 4,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 20,\n lightPairsPerRoadWay: 40,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.03, 400 * 0.2],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.8, 0.8],\n carFloorSeparation: [0, 5],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0xffffff,\n brokenLines: 0xffffff,\n leftCars: [0xd856bf, 0x6750a2, 0xc247ac],\n rightCars: [0x03b3c3, 0x0e5ea5, 0x324555],\n sticks: 0x03b3c3\n }\n};\n\nfunction nsin(val: number) {\n return Math.sin(val) * 0.5 + 0.5;\n}\n\nconst mountainUniforms = {\n uFreq: { value: new THREE.Vector3(3, 6, 10) },\n uAmp: { value: new THREE.Vector3(30, 30, 20) }\n};\n\nconst xyUniforms = {\n uFreq: { value: new THREE.Vector2(5, 2) },\n uAmp: { value: new THREE.Vector2(25, 15) }\n};\n\nconst LongRaceUniforms = {\n uFreq: { value: new THREE.Vector2(2, 3) },\n uAmp: { value: new THREE.Vector2(35, 10) }\n};\n\nconst turbulentUniforms = {\n uFreq: { value: new THREE.Vector4(4, 8, 8, 1) },\n uAmp: { value: new THREE.Vector4(25, 5, 10, 10) }\n};\n\nconst deepUniforms = {\n uFreq: { value: new THREE.Vector2(4, 8) },\n uAmp: { value: new THREE.Vector2(10, 20) },\n uPowY: { value: new THREE.Vector2(20, 2) }\n};\n\nconst distortions: Distortions = {\n mountainDistortion: {\n uniforms: mountainUniforms,\n getDistortion: `\n uniform vec3 uAmp;\n uniform vec3 uFreq;\n #define PI 3.14159265358979\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n vec3 getDistortion(float progress){\n float movementProgressFix = 0.02;\n return vec3( \n cos(progress * PI * uFreq.x + uTime) * uAmp.x - cos(movementProgressFix * PI * uFreq.x + uTime) * uAmp.x,\n nsin(progress * PI * uFreq.y + uTime) * uAmp.y - nsin(movementProgressFix * PI * uFreq.y + uTime) * uAmp.y,\n nsin(progress * PI * uFreq.z + uTime) * uAmp.z - nsin(movementProgressFix * PI * uFreq.z + uTime) * uAmp.z\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const movementProgressFix = 0.02;\n const uFreq = mountainUniforms.uFreq.value;\n const uAmp = mountainUniforms.uAmp.value;\n const distortion = new THREE.Vector3(\n Math.cos(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.cos(movementProgressFix * Math.PI * uFreq.x + time) * uAmp.x,\n nsin(progress * Math.PI * uFreq.y + time) * uAmp.y -\n nsin(movementProgressFix * Math.PI * uFreq.y + time) * uAmp.y,\n nsin(progress * Math.PI * uFreq.z + time) * uAmp.z -\n nsin(movementProgressFix * Math.PI * uFreq.z + time) * uAmp.z\n );\n const lookAtAmp = new THREE.Vector3(2, 2, 2);\n const lookAtOffset = new THREE.Vector3(0, 0, -5);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n xyDistortion: {\n uniforms: xyUniforms,\n getDistortion: `\n uniform vec2 uFreq;\n uniform vec2 uAmp;\n #define PI 3.14159265358979\n vec3 getDistortion(float progress){\n float movementProgressFix = 0.02;\n return vec3( \n cos(progress * PI * uFreq.x + uTime) * uAmp.x - cos(movementProgressFix * PI * uFreq.x + uTime) * uAmp.x,\n sin(progress * PI * uFreq.y + PI/2. + uTime) * uAmp.y - sin(movementProgressFix * PI * uFreq.y + PI/2. + uTime) * uAmp.y,\n 0.\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const movementProgressFix = 0.02;\n const uFreq = xyUniforms.uFreq.value;\n const uAmp = xyUniforms.uAmp.value;\n const distortion = new THREE.Vector3(\n Math.cos(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.cos(movementProgressFix * Math.PI * uFreq.x + time) * uAmp.x,\n Math.sin(progress * Math.PI * uFreq.y + time + Math.PI / 2) * uAmp.y -\n Math.sin(movementProgressFix * Math.PI * uFreq.y + time + Math.PI / 2) * uAmp.y,\n 0\n );\n const lookAtAmp = new THREE.Vector3(2, 0.4, 1);\n const lookAtOffset = new THREE.Vector3(0, 0, -3);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n LongRaceDistortion: {\n uniforms: LongRaceUniforms,\n getDistortion: `\n uniform vec2 uFreq;\n uniform vec2 uAmp;\n #define PI 3.14159265358979\n vec3 getDistortion(float progress){\n float camProgress = 0.0125;\n return vec3( \n sin(progress * PI * uFreq.x + uTime) * uAmp.x - sin(camProgress * PI * uFreq.x + uTime) * uAmp.x,\n sin(progress * PI * uFreq.y + uTime) * uAmp.y - sin(camProgress * PI * uFreq.y + uTime) * uAmp.y,\n 0.\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const camProgress = 0.0125;\n const uFreq = LongRaceUniforms.uFreq.value;\n const uAmp = LongRaceUniforms.uAmp.value;\n const distortion = new THREE.Vector3(\n Math.sin(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.sin(camProgress * Math.PI * uFreq.x + time) * uAmp.x,\n Math.sin(progress * Math.PI * uFreq.y + time) * uAmp.y -\n Math.sin(camProgress * Math.PI * uFreq.y + time) * uAmp.y,\n 0\n );\n const lookAtAmp = new THREE.Vector3(1, 1, 0);\n const lookAtOffset = new THREE.Vector3(0, 0, -5);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n turbulentDistortion: {\n uniforms: turbulentUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n cos(PI * progress * uFreq.r + uTime) * uAmp.r +\n pow(cos(PI * progress * uFreq.g + uTime * (uFreq.g / uFreq.r)), 2. ) * uAmp.g\n );\n }\n float getDistortionY(float progress){\n return (\n -nsin(PI * progress * uFreq.b + uTime) * uAmp.b +\n -pow(nsin(PI * progress * uFreq.a + uTime / (uFreq.b / uFreq.a)), 5.) * uAmp.a\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.0125),\n getDistortionY(progress) - getDistortionY(0.0125),\n 0.\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const uFreq = turbulentUniforms.uFreq.value;\n const uAmp = turbulentUniforms.uAmp.value;\n\n const getX = (p: number) =>\n Math.cos(Math.PI * p * uFreq.x + time) * uAmp.x +\n Math.pow(Math.cos(Math.PI * p * uFreq.y + time * (uFreq.y / uFreq.x)), 2) * uAmp.y;\n\n const getY = (p: number) =>\n -nsin(Math.PI * p * uFreq.z + time) * uAmp.z -\n Math.pow(nsin(Math.PI * p * uFreq.w + time / (uFreq.z / uFreq.w)), 5) * uAmp.w;\n\n const distortion = new THREE.Vector3(\n getX(progress) - getX(progress + 0.007),\n getY(progress) - getY(progress + 0.007),\n 0\n );\n const lookAtAmp = new THREE.Vector3(-2, -5, 0);\n const lookAtOffset = new THREE.Vector3(0, 0, -10);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n turbulentDistortionStill: {\n uniforms: turbulentUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n cos(PI * progress * uFreq.r) * uAmp.r +\n pow(cos(PI * progress * uFreq.g * (uFreq.g / uFreq.r)), 2. ) * uAmp.g\n );\n }\n float getDistortionY(float progress){\n return (\n -nsin(PI * progress * uFreq.b) * uAmp.b +\n -pow(nsin(PI * progress * uFreq.a / (uFreq.b / uFreq.a)), 5.) * uAmp.a\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.02),\n 0.\n );\n }\n `\n },\n deepDistortionStill: {\n uniforms: deepUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n uniform vec2 uPowY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n sin(progress * PI * uFreq.x) * uAmp.x * 2.\n );\n }\n float getDistortionY(float progress){\n return (\n pow(abs(progress * uPowY.x), uPowY.y) + sin(progress * PI * uFreq.y) * uAmp.y\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.05),\n 0.\n );\n }\n `\n },\n deepDistortion: {\n uniforms: deepUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n uniform vec2 uPowY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n sin(progress * PI * uFreq.x + uTime) * uAmp.x\n );\n }\n float getDistortionY(float progress){\n return (\n pow(abs(progress * uPowY.x), uPowY.y) + sin(progress * PI * uFreq.y + uTime) * uAmp.y\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.02),\n 0.\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const uFreq = deepUniforms.uFreq.value;\n const uAmp = deepUniforms.uAmp.value;\n const uPowY = deepUniforms.uPowY.value;\n\n const getX = (p: number) => Math.sin(p * Math.PI * uFreq.x + time) * uAmp.x;\n const getY = (p: number) => Math.pow(p * uPowY.x, uPowY.y) + Math.sin(p * Math.PI * uFreq.y + time) * uAmp.y;\n\n const distortion = new THREE.Vector3(\n getX(progress) - getX(progress + 0.01),\n getY(progress) - getY(progress + 0.01),\n 0\n );\n const lookAtAmp = new THREE.Vector3(-2, -4, 0);\n const lookAtOffset = new THREE.Vector3(0, 0, -10);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n }\n};\n\nconst distortion_uniforms = {\n uDistortionX: { value: new THREE.Vector2(80, 3) },\n uDistortionY: { value: new THREE.Vector2(-40, 2.5) }\n};\n\nconst distortion_vertex = `\n #define PI 3.14159265358979\n uniform vec2 uDistortionX;\n uniform vec2 uDistortionY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n vec3 getDistortion(float progress){\n progress = clamp(progress, 0., 1.);\n float xAmp = uDistortionX.r;\n float xFreq = uDistortionX.g;\n float yAmp = uDistortionY.r;\n float yFreq = uDistortionY.g;\n return vec3( \n xAmp * nsin(progress * PI * xFreq - PI / 2.),\n yAmp * nsin(progress * PI * yFreq - PI / 2.),\n 0.\n );\n }\n`;\n\nfunction random(base: number | [number, number]): number {\n if (Array.isArray(base)) {\n return Math.random() * (base[1] - base[0]) + base[0];\n }\n return Math.random() * base;\n}\n\nfunction pickRandom(arr: T | T[]): T {\n if (Array.isArray(arr)) {\n return arr[Math.floor(Math.random() * arr.length)];\n }\n return arr;\n}\n\nfunction lerp(current: number, target: number, speed = 0.1, limit = 0.001): number {\n let change = (target - current) * speed;\n if (Math.abs(change) < limit) {\n change = target - current;\n }\n return change;\n}\n\nclass CarLights {\n webgl: App;\n options: HyperspeedOptions;\n colors: number[] | THREE.Color;\n speed: [number, number];\n fade: THREE.Vector2;\n mesh!: THREE.Mesh;\n\n constructor(\n webgl: App,\n options: HyperspeedOptions,\n colors: number[] | THREE.Color,\n speed: [number, number],\n fade: THREE.Vector2\n ) {\n this.webgl = webgl;\n this.options = options;\n this.colors = colors;\n this.speed = speed;\n this.fade = fade;\n }\n\n init() {\n const options = this.options;\n const curve = new THREE.LineCurve3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -1));\n const geometry = new THREE.TubeGeometry(curve, 40, 1, 8, false);\n\n const instanced = new THREE.InstancedBufferGeometry().copy(geometry as any) as THREE.InstancedBufferGeometry;\n instanced.instanceCount = options.lightPairsPerRoadWay * 2;\n\n const laneWidth = options.roadWidth / options.lanesPerRoad;\n\n const aOffset: number[] = [];\n const aMetrics: number[] = [];\n const aColor: number[] = [];\n\n let colorArray: THREE.Color[];\n if (Array.isArray(this.colors)) {\n colorArray = this.colors.map(c => new THREE.Color(c));\n } else {\n colorArray = [new THREE.Color(this.colors)];\n }\n\n for (let i = 0; i < options.lightPairsPerRoadWay; i++) {\n const radius = random(options.carLightsRadius);\n const length = random(options.carLightsLength);\n const spd = random(this.speed);\n\n const carLane = i % options.lanesPerRoad;\n let laneX = carLane * laneWidth - options.roadWidth / 2 + laneWidth / 2;\n\n const carWidth = random(options.carWidthPercentage) * laneWidth;\n const carShiftX = random(options.carShiftX) * laneWidth;\n laneX += carShiftX;\n\n const offsetY = random(options.carFloorSeparation) + radius * 1.3;\n const offsetZ = -random(options.length);\n\n aOffset.push(laneX - carWidth / 2);\n aOffset.push(offsetY);\n aOffset.push(offsetZ);\n\n aOffset.push(laneX + carWidth / 2);\n aOffset.push(offsetY);\n aOffset.push(offsetZ);\n\n aMetrics.push(radius);\n aMetrics.push(length);\n aMetrics.push(spd);\n\n aMetrics.push(radius);\n aMetrics.push(length);\n aMetrics.push(spd);\n\n const color = pickRandom(colorArray);\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n }\n\n instanced.setAttribute('aOffset', new THREE.InstancedBufferAttribute(new Float32Array(aOffset), 3, false));\n instanced.setAttribute('aMetrics', new THREE.InstancedBufferAttribute(new Float32Array(aMetrics), 3, false));\n instanced.setAttribute('aColor', new THREE.InstancedBufferAttribute(new Float32Array(aColor), 3, false));\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: carLightsFragment,\n vertexShader: carLightsVertex,\n transparent: true,\n uniforms: Object.assign(\n {\n uTime: { value: 0 },\n uTravelLength: { value: options.length },\n uFade: { value: this.fade }\n },\n this.webgl.fogUniforms,\n (typeof this.options.distortion === 'object' ? this.options.distortion.uniforms : {}) || {}\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n typeof this.options.distortion === 'object' ? this.options.distortion.getDistortion : ''\n );\n };\n\n const mesh = new THREE.Mesh(instanced, material);\n mesh.frustumCulled = false;\n this.webgl.scene.add(mesh);\n this.mesh = mesh;\n }\n\n update(time: number) {\n if (this.mesh.material.uniforms.uTime) {\n this.mesh.material.uniforms.uTime.value = time;\n }\n }\n}\n\nconst carLightsFragment = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_fragment']}\n varying vec3 vColor;\n varying vec2 vUv; \n uniform vec2 uFade;\n void main() {\n vec3 color = vec3(vColor);\n float alpha = smoothstep(uFade.x, uFade.y, vUv.x);\n gl_FragColor = vec4(color, alpha);\n if (gl_FragColor.a < 0.0001) discard;\n ${THREE.ShaderChunk['fog_fragment']}\n }\n`;\n\nconst carLightsVertex = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n attribute vec3 aOffset;\n attribute vec3 aMetrics;\n attribute vec3 aColor;\n uniform float uTravelLength;\n uniform float uTime;\n varying vec2 vUv; \n varying vec3 vColor; \n #include \n void main() {\n vec3 transformed = position.xyz;\n float radius = aMetrics.r;\n float myLength = aMetrics.g;\n float speed = aMetrics.b;\n\n transformed.xy *= radius;\n transformed.z *= myLength;\n\n transformed.z += myLength - mod(uTime * speed + aOffset.z, uTravelLength);\n transformed.xy += aOffset.xy;\n\n float progress = abs(transformed.z / uTravelLength);\n transformed.xyz += getDistortion(progress);\n\n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vUv = uv;\n vColor = aColor;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n`;\n\nclass LightsSticks {\n webgl: App;\n options: HyperspeedOptions;\n mesh!: THREE.Mesh;\n\n constructor(webgl: App, options: HyperspeedOptions) {\n this.webgl = webgl;\n this.options = options;\n }\n\n init() {\n const options = this.options;\n const geometry = new THREE.PlaneGeometry(1, 1);\n const instanced = new THREE.InstancedBufferGeometry().copy(geometry as any) as THREE.InstancedBufferGeometry;\n const totalSticks = options.totalSideLightSticks;\n instanced.instanceCount = totalSticks;\n\n const stickoffset = options.length / (totalSticks - 1);\n const aOffset: number[] = [];\n const aColor: number[] = [];\n const aMetrics: number[] = [];\n\n let colorArray: THREE.Color[];\n if (Array.isArray(options.colors.sticks)) {\n colorArray = options.colors.sticks.map(c => new THREE.Color(c));\n } else {\n colorArray = [new THREE.Color(options.colors.sticks)];\n }\n\n for (let i = 0; i < totalSticks; i++) {\n const width = random(options.lightStickWidth);\n const height = random(options.lightStickHeight);\n aOffset.push((i - 1) * stickoffset * 2 + stickoffset * Math.random());\n\n const color = pickRandom(colorArray);\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n\n aMetrics.push(width);\n aMetrics.push(height);\n }\n\n instanced.setAttribute('aOffset', new THREE.InstancedBufferAttribute(new Float32Array(aOffset), 1, false));\n instanced.setAttribute('aColor', new THREE.InstancedBufferAttribute(new Float32Array(aColor), 3, false));\n instanced.setAttribute('aMetrics', new THREE.InstancedBufferAttribute(new Float32Array(aMetrics), 2, false));\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: sideSticksFragment,\n vertexShader: sideSticksVertex,\n side: THREE.DoubleSide,\n uniforms: Object.assign(\n {\n uTravelLength: { value: options.length },\n uTime: { value: 0 }\n },\n this.webgl.fogUniforms,\n (typeof options.distortion === 'object' ? options.distortion.uniforms : {}) || {}\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n typeof this.options.distortion === 'object' ? this.options.distortion.getDistortion : ''\n );\n };\n\n const mesh = new THREE.Mesh(instanced, material);\n mesh.frustumCulled = false;\n this.webgl.scene.add(mesh);\n this.mesh = mesh;\n }\n\n update(time: number) {\n if (this.mesh.material.uniforms.uTime) {\n this.mesh.material.uniforms.uTime.value = time;\n }\n }\n}\n\nconst sideSticksVertex = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n attribute float aOffset;\n attribute vec3 aColor;\n attribute vec2 aMetrics;\n uniform float uTravelLength;\n uniform float uTime;\n varying vec3 vColor;\n mat4 rotationY( in float angle ) {\n return mat4(\n cos(angle),\t\t0,\t\tsin(angle),\t0,\n 0,\t\t 1.0,\t0,\t\t\t0,\n -sin(angle),\t 0,\t\tcos(angle),\t0,\n 0, \t\t 0,\t\t0,\t\t\t1\n );\n }\n #include \n void main(){\n vec3 transformed = position.xyz;\n float width = aMetrics.x;\n float height = aMetrics.y;\n\n transformed.xy *= vec2(width, height);\n float time = mod(uTime * 60. * 2. + aOffset, uTravelLength);\n\n transformed = (rotationY(3.14/2.) * vec4(transformed,1.)).xyz;\n transformed.z += - uTravelLength + time;\n\n float progress = abs(transformed.z / uTravelLength);\n transformed.xyz += getDistortion(progress);\n\n transformed.y += height / 2.;\n transformed.x += -width / 2.;\n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vColor = aColor;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n`;\n\nconst sideSticksFragment = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_fragment']}\n varying vec3 vColor;\n void main(){\n vec3 color = vec3(vColor);\n gl_FragColor = vec4(color,1.);\n ${THREE.ShaderChunk['fog_fragment']}\n }\n`;\n\nclass Road {\n webgl: App;\n options: HyperspeedOptions;\n uTime: { value: number };\n leftRoadWay!: THREE.Mesh;\n rightRoadWay!: THREE.Mesh;\n island!: THREE.Mesh;\n\n constructor(webgl: App, options: HyperspeedOptions) {\n this.webgl = webgl;\n this.options = options;\n this.uTime = { value: 0 };\n }\n\n createPlane(side: number, width: number, isRoad: boolean) {\n const options = this.options;\n const segments = 100;\n const geometry = new THREE.PlaneGeometry(\n isRoad ? options.roadWidth : options.islandWidth,\n options.length,\n 20,\n segments\n );\n\n let uniforms: Record = {\n uTravelLength: { value: options.length },\n uColor: {\n value: new THREE.Color(isRoad ? options.colors.roadColor : options.colors.islandColor)\n },\n uTime: this.uTime\n };\n\n if (isRoad) {\n uniforms = Object.assign(uniforms, {\n uLanes: { value: options.lanesPerRoad },\n uBrokenLinesColor: {\n value: new THREE.Color(options.colors.brokenLines)\n },\n uShoulderLinesColor: {\n value: new THREE.Color(options.colors.shoulderLines)\n },\n uShoulderLinesWidthPercentage: {\n value: options.shoulderLinesWidthPercentage\n },\n uBrokenLinesLengthPercentage: {\n value: options.brokenLinesLengthPercentage\n },\n uBrokenLinesWidthPercentage: {\n value: options.brokenLinesWidthPercentage\n }\n });\n }\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: isRoad ? roadFragment : islandFragment,\n vertexShader: roadVertex,\n side: THREE.DoubleSide,\n uniforms: Object.assign(\n uniforms,\n this.webgl.fogUniforms,\n (typeof options.distortion === 'object' ? options.distortion.uniforms : {}) || {}\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n typeof this.options.distortion === 'object' ? this.options.distortion.getDistortion : ''\n );\n };\n\n const mesh = new THREE.Mesh(geometry, material);\n mesh.rotation.x = -Math.PI / 2;\n mesh.position.z = -options.length / 2;\n mesh.position.x += (this.options.islandWidth / 2 + options.roadWidth / 2) * side;\n\n this.webgl.scene.add(mesh);\n return mesh;\n }\n\n init() {\n this.leftRoadWay = this.createPlane(-1, this.options.roadWidth, true);\n this.rightRoadWay = this.createPlane(1, this.options.roadWidth, true);\n this.island = this.createPlane(0, this.options.islandWidth, false);\n }\n\n update(time: number) {\n this.uTime.value = time;\n }\n}\n\nconst roadBaseFragment = `\n #define USE_FOG;\n varying vec2 vUv; \n uniform vec3 uColor;\n uniform float uTime;\n #include \n ${THREE.ShaderChunk['fog_pars_fragment']}\n void main() {\n vec2 uv = vUv;\n vec3 color = vec3(uColor);\n #include \n gl_FragColor = vec4(color, 1.);\n ${THREE.ShaderChunk['fog_fragment']}\n }\n`;\n\nconst islandFragment = roadBaseFragment\n .replace('#include ', '')\n .replace('#include ', '');\n\nconst roadMarkings_vars = `\n uniform float uLanes;\n uniform vec3 uBrokenLinesColor;\n uniform vec3 uShoulderLinesColor;\n uniform float uShoulderLinesWidthPercentage;\n uniform float uBrokenLinesWidthPercentage;\n uniform float uBrokenLinesLengthPercentage;\n highp float random(vec2 co) {\n highp float a = 12.9898;\n highp float b = 78.233;\n highp float c = 43758.5453;\n highp float dt = dot(co.xy, vec2(a, b));\n highp float sn = mod(dt, 3.14);\n return fract(sin(sn) * c);\n }\n`;\n\nconst roadMarkings_fragment = `\n uv.y = mod(uv.y + uTime * 0.05, 1.);\n float laneWidth = 1.0 / uLanes;\n float brokenLineWidth = laneWidth * uBrokenLinesWidthPercentage;\n float laneEmptySpace = 1. - uBrokenLinesLengthPercentage;\n\n float brokenLines = step(1.0 - brokenLineWidth, fract(uv.x * 2.0)) * step(laneEmptySpace, fract(uv.y * 10.0));\n float sideLines = step(1.0 - brokenLineWidth, fract((uv.x - laneWidth * (uLanes - 1.0)) * 2.0)) + step(brokenLineWidth, uv.x);\n\n brokenLines = mix(brokenLines, sideLines, uv.x);\n`;\n\nconst roadFragment = roadBaseFragment\n .replace('#include ', roadMarkings_fragment)\n .replace('#include ', roadMarkings_vars);\n\nconst roadVertex = `\n #define USE_FOG;\n uniform float uTime;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n uniform float uTravelLength;\n varying vec2 vUv; \n #include \n void main() {\n vec3 transformed = position.xyz;\n vec3 distortion = getDistortion((transformed.y + uTravelLength / 2.) / uTravelLength);\n transformed.x += distortion.x;\n transformed.z += distortion.y;\n transformed.y += -1. * distortion.z; \n \n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vUv = uv;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n`;\n\nfunction resizeRendererToDisplaySize(\n renderer: THREE.WebGLRenderer,\n setSize: (width: number, height: number, updateStyle: boolean) => void\n) {\n const canvas = renderer.domElement;\n const width = canvas.clientWidth;\n const height = canvas.clientHeight;\n if (width <= 0 || height <= 0) return false;\n const needResize = canvas.width !== width || canvas.height !== height;\n if (needResize) {\n setSize(width, height, false);\n }\n return needResize;\n}\n\nclass App {\n container: HTMLElement;\n options: HyperspeedOptions;\n renderer: THREE.WebGLRenderer;\n composer: EffectComposer;\n camera: THREE.PerspectiveCamera;\n scene: THREE.Scene;\n renderPass!: RenderPass;\n bloomPass!: EffectPass;\n clock: THREE.Clock;\n assets: Record;\n disposed: boolean;\n road: Road;\n leftCarLights: CarLights;\n rightCarLights: CarLights;\n leftSticks: LightsSticks;\n fogUniforms: Record;\n fovTarget: number;\n speedUpTarget: number;\n speedUp: number;\n timeOffset: number;\n hasValidSize: boolean;\n\n constructor(container: HTMLElement, options: HyperspeedOptions) {\n this.options = options;\n if (!this.options.distortion) {\n this.options.distortion = {\n uniforms: distortion_uniforms,\n getDistortion: distortion_vertex\n };\n }\n this.container = container;\n this.hasValidSize = false;\n\n const initW = Math.max(1, container.offsetWidth);\n const initH = Math.max(1, container.offsetHeight);\n\n this.renderer = new THREE.WebGLRenderer({\n antialias: false,\n alpha: true\n });\n this.renderer.setSize(initW, initH, false);\n this.renderer.setPixelRatio(window.devicePixelRatio);\n\n this.composer = new EffectComposer(this.renderer);\n container.appendChild(this.renderer.domElement);\n\n this.camera = new THREE.PerspectiveCamera(options.fov, initW / initH, 0.1, 10000);\n this.camera.position.z = -5;\n this.camera.position.y = 8;\n this.camera.position.x = 0;\n\n this.scene = new THREE.Scene();\n this.scene.background = null;\n\n const fog = new THREE.Fog(options.colors.background, options.length * 0.2, options.length * 500);\n this.scene.fog = fog;\n\n this.fogUniforms = {\n fogColor: { value: fog.color },\n fogNear: { value: fog.near },\n fogFar: { value: fog.far }\n };\n\n this.clock = new THREE.Clock();\n this.assets = {};\n this.disposed = false;\n\n this.road = new Road(this, options);\n this.leftCarLights = new CarLights(\n this,\n options,\n options.colors.leftCars,\n options.movingAwaySpeed,\n new THREE.Vector2(0, 1 - options.carLightsFade)\n );\n this.rightCarLights = new CarLights(\n this,\n options,\n options.colors.rightCars,\n options.movingCloserSpeed,\n new THREE.Vector2(1, 0 + options.carLightsFade)\n );\n this.leftSticks = new LightsSticks(this, options);\n\n this.fovTarget = options.fov;\n this.speedUpTarget = 0;\n this.speedUp = 0;\n this.timeOffset = 0;\n\n this.tick = this.tick.bind(this);\n this.init = this.init.bind(this);\n this.setSize = this.setSize.bind(this);\n this.onMouseDown = this.onMouseDown.bind(this);\n this.onMouseUp = this.onMouseUp.bind(this);\n\n this.onTouchStart = this.onTouchStart.bind(this);\n this.onTouchEnd = this.onTouchEnd.bind(this);\n this.onContextMenu = this.onContextMenu.bind(this);\n\n this.onWindowResize = this.onWindowResize.bind(this);\n window.addEventListener('resize', this.onWindowResize);\n\n if (container.offsetWidth > 0 && container.offsetHeight > 0) {\n this.hasValidSize = true;\n }\n }\n\n onWindowResize() {\n const width = this.container.offsetWidth;\n const height = this.container.offsetHeight;\n\n if (width <= 0 || height <= 0) {\n this.hasValidSize = false;\n return;\n }\n\n this.renderer.setSize(width, height);\n this.camera.aspect = width / height;\n this.camera.updateProjectionMatrix();\n this.composer.setSize(width, height);\n this.hasValidSize = true;\n }\n\n initPasses() {\n this.renderPass = new RenderPass(this.scene, this.camera);\n this.bloomPass = new EffectPass(\n this.camera,\n new BloomEffect({\n luminanceThreshold: 0.2,\n luminanceSmoothing: 0,\n resolutionScale: 1\n })\n );\n\n const smaaPass = new EffectPass(\n this.camera,\n new SMAAEffect({\n preset: SMAAPreset.MEDIUM\n })\n );\n this.renderPass.renderToScreen = false;\n this.bloomPass.renderToScreen = false;\n smaaPass.renderToScreen = true;\n\n this.composer.addPass(this.renderPass);\n this.composer.addPass(this.bloomPass);\n this.composer.addPass(smaaPass);\n }\n\n loadAssets(): Promise {\n const assets = this.assets;\n return new Promise(resolve => {\n const manager = new THREE.LoadingManager(resolve);\n\n const searchImage = new Image();\n const areaImage = new Image();\n assets.smaa = {};\n\n searchImage.addEventListener('load', function () {\n assets.smaa.search = this;\n manager.itemEnd('smaa-search');\n });\n\n areaImage.addEventListener('load', function () {\n assets.smaa.area = this;\n manager.itemEnd('smaa-area');\n });\n\n manager.itemStart('smaa-search');\n manager.itemStart('smaa-area');\n\n searchImage.src = SMAAEffect.searchImageDataURL;\n areaImage.src = SMAAEffect.areaImageDataURL;\n });\n }\n\n init() {\n this.initPasses();\n const options = this.options;\n this.road.init();\n this.leftCarLights.init();\n this.leftCarLights.mesh.position.setX(-options.roadWidth / 2 - options.islandWidth / 2);\n\n this.rightCarLights.init();\n this.rightCarLights.mesh.position.setX(options.roadWidth / 2 + options.islandWidth / 2);\n\n this.leftSticks.init();\n this.leftSticks.mesh.position.setX(-(options.roadWidth + options.islandWidth / 2));\n\n this.container.addEventListener('mousedown', this.onMouseDown);\n this.container.addEventListener('mouseup', this.onMouseUp);\n this.container.addEventListener('mouseout', this.onMouseUp);\n\n this.container.addEventListener('touchstart', this.onTouchStart, { passive: true });\n this.container.addEventListener('touchend', this.onTouchEnd, { passive: true });\n this.container.addEventListener('touchcancel', this.onTouchEnd, { passive: true });\n this.container.addEventListener('contextmenu', this.onContextMenu);\n\n this.tick();\n }\n\n onMouseDown(ev: MouseEvent) {\n if (this.options.onSpeedUp) this.options.onSpeedUp(ev);\n this.fovTarget = this.options.fovSpeedUp;\n this.speedUpTarget = this.options.speedUp;\n }\n\n onMouseUp(ev: MouseEvent) {\n if (this.options.onSlowDown) this.options.onSlowDown(ev);\n this.fovTarget = this.options.fov;\n this.speedUpTarget = 0;\n }\n\n onTouchStart(ev: TouchEvent) {\n if (this.options.onSpeedUp) this.options.onSpeedUp(ev);\n this.fovTarget = this.options.fovSpeedUp;\n this.speedUpTarget = this.options.speedUp;\n }\n\n onTouchEnd(ev: TouchEvent) {\n if (this.options.onSlowDown) this.options.onSlowDown(ev);\n this.fovTarget = this.options.fov;\n this.speedUpTarget = 0;\n }\n\n onContextMenu(ev: MouseEvent) {\n ev.preventDefault();\n }\n\n update(delta: number) {\n const lerpPercentage = Math.exp(-(-60 * Math.log2(1 - 0.1)) * delta);\n this.speedUp += lerp(this.speedUp, this.speedUpTarget, lerpPercentage, 0.00001);\n this.timeOffset += this.speedUp * delta;\n const time = this.clock.elapsedTime + this.timeOffset;\n\n this.rightCarLights.update(time);\n this.leftCarLights.update(time);\n this.leftSticks.update(time);\n this.road.update(time);\n\n let updateCamera = false;\n const fovChange = lerp(this.camera.fov, this.fovTarget, lerpPercentage);\n if (fovChange !== 0) {\n this.camera.fov += fovChange * delta * 6;\n updateCamera = true;\n }\n\n if (typeof this.options.distortion === 'object' && this.options.distortion.getJS) {\n const distortion = this.options.distortion.getJS(0.025, time);\n this.camera.lookAt(\n new THREE.Vector3(\n this.camera.position.x + distortion.x,\n this.camera.position.y + distortion.y,\n this.camera.position.z + distortion.z\n )\n );\n updateCamera = true;\n }\n\n if (updateCamera) {\n this.camera.updateProjectionMatrix();\n }\n }\n\n render(delta: number) {\n this.composer.render(delta);\n }\n\n dispose() {\n this.disposed = true;\n\n if (this.scene) {\n this.scene.traverse(object => {\n const obj = object as unknown as THREE.Mesh;\n if (!obj.isMesh) return;\n\n if (obj.geometry) obj.geometry.dispose();\n\n if (obj.material) {\n if (Array.isArray(obj.material)) {\n obj.material.forEach(material => material.dispose());\n } else {\n obj.material.dispose();\n }\n }\n });\n this.scene.clear();\n }\n\n if (this.renderer) {\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n if (this.renderer.domElement && this.renderer.domElement.parentNode) {\n this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);\n }\n }\n if (this.composer) {\n this.composer.dispose();\n }\n\n window.removeEventListener('resize', this.onWindowResize);\n if (this.container) {\n this.container.removeEventListener('mousedown', this.onMouseDown);\n this.container.removeEventListener('mouseup', this.onMouseUp);\n this.container.removeEventListener('mouseout', this.onMouseUp);\n\n this.container.removeEventListener('touchstart', this.onTouchStart);\n this.container.removeEventListener('touchend', this.onTouchEnd);\n this.container.removeEventListener('touchcancel', this.onTouchEnd);\n this.container.removeEventListener('contextmenu', this.onContextMenu);\n }\n }\n\n setSize(width: number, height: number, updateStyles: boolean) {\n this.composer.setSize(width, height, updateStyles);\n }\n\n tick() {\n if (this.disposed) return;\n\n if (!this.hasValidSize) {\n const w = this.container.offsetWidth;\n const h = this.container.offsetHeight;\n if (w > 0 && h > 0) {\n this.renderer.setSize(w, h, false);\n this.camera.aspect = w / h;\n this.camera.updateProjectionMatrix();\n this.composer.setSize(w, h);\n this.hasValidSize = true;\n } else {\n requestAnimationFrame(this.tick);\n return;\n }\n }\n\n if (resizeRendererToDisplaySize(this.renderer, this.setSize)) {\n const canvas = this.renderer.domElement;\n if (this.hasValidSize) {\n this.camera.aspect = canvas.clientWidth / canvas.clientHeight;\n this.camera.updateProjectionMatrix();\n }\n }\n\n if (this.hasValidSize) {\n const delta = this.clock.getDelta();\n this.render(delta);\n this.update(delta);\n }\n\n requestAnimationFrame(this.tick);\n }\n}\n\nconst DEFAULT_EFFECT_OPTIONS: Partial = {};\n\nconst Hyperspeed: FC = ({ effectOptions = DEFAULT_EFFECT_OPTIONS }) => {\n const hyperspeed = useRef(null);\n const appRef = useRef(null);\n\n useEffect(() => {\n if (appRef.current) {\n appRef.current.dispose();\n appRef.current = null;\n const container = hyperspeed.current;\n if (container) {\n while (container.firstChild) {\n container.removeChild(container.firstChild);\n }\n }\n }\n\n const container = hyperspeed.current;\n if (!container) return;\n\n const options: HyperspeedOptions = {\n ...defaultOptions,\n ...effectOptions,\n colors: { ...defaultOptions.colors, ...effectOptions.colors }\n };\n if (typeof options.distortion === 'string') {\n options.distortion = distortions[options.distortion];\n }\n\n const myApp = new App(container, options);\n appRef.current = myApp;\n myApp.loadAssets().then(myApp.init);\n\n return () => {\n if (appRef.current) {\n appRef.current.dispose();\n }\n };\n }, [effectOptions]);\n\n return
;\n};\n\nexport default Hyperspeed;\n" } ], "registryDependencies": [], diff --git a/public/r/Hyperspeed-TS-TW.json b/public/r/Hyperspeed-TS-TW.json index c8dc49ef..0d4c285b 100644 --- a/public/r/Hyperspeed-TS-TW.json +++ b/public/r/Hyperspeed-TS-TW.json @@ -7,13 +7,13 @@ "files": [ { "type": "registry:component", - "path": "Hyperspeed/HyperSpeedPresets.ts", - "content": "export const hyperspeedPresets = {\n one: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 20,\n lightPairsPerRoadWay: 40,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.03, 400 * 0.2],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.8, 0.8],\n carFloorSeparation: [0, 5],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xd856bf, 0x6750a2, 0xc247ac],\n rightCars: [0x03b3c3, 0x0e5ea5, 0x324555],\n sticks: 0x03b3c3\n }\n },\n two: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'mountainDistortion',\n length: 400,\n roadWidth: 9,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 50,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xff102a, 0xeb383e, 0xff102a],\n rightCars: [0xdadafa, 0xbebae3, 0x8f97e4],\n sticks: 0xdadafa\n }\n },\n three: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'xyDistortion',\n length: 400,\n roadWidth: 9,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 3,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 30,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.02, 0.05],\n lightStickHeight: [0.3, 0.7],\n movingAwaySpeed: [20, 50],\n movingCloserSpeed: [-150, -230],\n carLightsLength: [400 * 0.05, 400 * 0.2],\n carLightsRadius: [0.03, 0.08],\n carWidthPercentage: [0.1, 0.5],\n carShiftX: [-0.5, 0.5],\n carFloorSeparation: [0, 0.1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0x7d0d1b, 0xa90519, 0xff102a],\n rightCars: [0xf1eece, 0xe6e2b1, 0xdfd98a],\n sticks: 0xf1eece\n }\n },\n four: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'LongRaceDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 5,\n lanesPerRoad: 2,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 70,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xff5f73, 0xe74d60, 0xff102a],\n rightCars: [0xa4e3e6, 0x80d1d4, 0x53c2c6],\n sticks: 0xa4e3e6\n }\n },\n five: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 9,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 50,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xdc5b20, 0xdca320, 0xdc2020],\n rightCars: [0x334bf7, 0xe5e6ed, 0xbfc6f3],\n sticks: 0xc5e8eb\n }\n },\n six: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'deepDistortion',\n length: 400,\n roadWidth: 18,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 50,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xff322f, 0xa33010, 0xa81508],\n rightCars: [0xfdfdf0, 0xf3dea0, 0xe2bb88],\n sticks: 0xfdfdf0\n }\n }\n};\n" + "path": "Hyperspeed/Hyperspeed.tsx", + "content": "import { BloomEffect, EffectComposer, EffectPass, RenderPass, SMAAEffect, SMAAPreset } from 'postprocessing';\nimport { type FC, useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\ninterface Distortion {\n uniforms: Record;\n getDistortion: string;\n getJS?: (progress: number, time: number) => THREE.Vector3;\n}\n\ninterface Distortions {\n [key: string]: Distortion;\n}\n\ninterface Colors {\n roadColor: number;\n islandColor: number;\n background: number;\n shoulderLines: number;\n brokenLines: number;\n leftCars: number[];\n rightCars: number[];\n sticks: number;\n}\n\ninterface HyperspeedOptions {\n onSpeedUp?: (ev: MouseEvent | TouchEvent) => void;\n onSlowDown?: (ev: MouseEvent | TouchEvent) => void;\n distortion?: string | Distortion;\n length: number;\n roadWidth: number;\n islandWidth: number;\n lanesPerRoad: number;\n fov: number;\n fovSpeedUp: number;\n speedUp: number;\n carLightsFade: number;\n totalSideLightSticks: number;\n lightPairsPerRoadWay: number;\n shoulderLinesWidthPercentage: number;\n brokenLinesWidthPercentage: number;\n brokenLinesLengthPercentage: number;\n lightStickWidth: [number, number];\n lightStickHeight: [number, number];\n movingAwaySpeed: [number, number];\n movingCloserSpeed: [number, number];\n carLightsLength: [number, number];\n carLightsRadius: [number, number];\n carWidthPercentage: [number, number];\n carShiftX: [number, number];\n carFloorSeparation: [number, number];\n colors: Colors;\n isHyper?: boolean;\n}\n\ninterface HyperspeedProps {\n effectOptions?: Partial;\n}\n\nconst defaultOptions: HyperspeedOptions = {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 2,\n lanesPerRoad: 4,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 20,\n lightPairsPerRoadWay: 40,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.03, 400 * 0.2],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.8, 0.8],\n carFloorSeparation: [0, 5],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0xffffff,\n brokenLines: 0xffffff,\n leftCars: [0xd856bf, 0x6750a2, 0xc247ac],\n rightCars: [0x03b3c3, 0x0e5ea5, 0x324555],\n sticks: 0x03b3c3\n }\n};\n\nfunction nsin(val: number) {\n return Math.sin(val) * 0.5 + 0.5;\n}\n\nconst mountainUniforms = {\n uFreq: { value: new THREE.Vector3(3, 6, 10) },\n uAmp: { value: new THREE.Vector3(30, 30, 20) }\n};\n\nconst xyUniforms = {\n uFreq: { value: new THREE.Vector2(5, 2) },\n uAmp: { value: new THREE.Vector2(25, 15) }\n};\n\nconst LongRaceUniforms = {\n uFreq: { value: new THREE.Vector2(2, 3) },\n uAmp: { value: new THREE.Vector2(35, 10) }\n};\n\nconst turbulentUniforms = {\n uFreq: { value: new THREE.Vector4(4, 8, 8, 1) },\n uAmp: { value: new THREE.Vector4(25, 5, 10, 10) }\n};\n\nconst deepUniforms = {\n uFreq: { value: new THREE.Vector2(4, 8) },\n uAmp: { value: new THREE.Vector2(10, 20) },\n uPowY: { value: new THREE.Vector2(20, 2) }\n};\n\nconst distortions: Distortions = {\n mountainDistortion: {\n uniforms: mountainUniforms,\n getDistortion: `\n uniform vec3 uAmp;\n uniform vec3 uFreq;\n #define PI 3.14159265358979\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n vec3 getDistortion(float progress){\n float movementProgressFix = 0.02;\n return vec3( \n cos(progress * PI * uFreq.x + uTime) * uAmp.x - cos(movementProgressFix * PI * uFreq.x + uTime) * uAmp.x,\n nsin(progress * PI * uFreq.y + uTime) * uAmp.y - nsin(movementProgressFix * PI * uFreq.y + uTime) * uAmp.y,\n nsin(progress * PI * uFreq.z + uTime) * uAmp.z - nsin(movementProgressFix * PI * uFreq.z + uTime) * uAmp.z\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const movementProgressFix = 0.02;\n const uFreq = mountainUniforms.uFreq.value;\n const uAmp = mountainUniforms.uAmp.value;\n const distortion = new THREE.Vector3(\n Math.cos(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.cos(movementProgressFix * Math.PI * uFreq.x + time) * uAmp.x,\n nsin(progress * Math.PI * uFreq.y + time) * uAmp.y -\n nsin(movementProgressFix * Math.PI * uFreq.y + time) * uAmp.y,\n nsin(progress * Math.PI * uFreq.z + time) * uAmp.z -\n nsin(movementProgressFix * Math.PI * uFreq.z + time) * uAmp.z\n );\n const lookAtAmp = new THREE.Vector3(2, 2, 2);\n const lookAtOffset = new THREE.Vector3(0, 0, -5);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n xyDistortion: {\n uniforms: xyUniforms,\n getDistortion: `\n uniform vec2 uFreq;\n uniform vec2 uAmp;\n #define PI 3.14159265358979\n vec3 getDistortion(float progress){\n float movementProgressFix = 0.02;\n return vec3( \n cos(progress * PI * uFreq.x + uTime) * uAmp.x - cos(movementProgressFix * PI * uFreq.x + uTime) * uAmp.x,\n sin(progress * PI * uFreq.y + PI/2. + uTime) * uAmp.y - sin(movementProgressFix * PI * uFreq.y + PI/2. + uTime) * uAmp.y,\n 0.\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const movementProgressFix = 0.02;\n const uFreq = xyUniforms.uFreq.value;\n const uAmp = xyUniforms.uAmp.value;\n const distortion = new THREE.Vector3(\n Math.cos(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.cos(movementProgressFix * Math.PI * uFreq.x + time) * uAmp.x,\n Math.sin(progress * Math.PI * uFreq.y + time + Math.PI / 2) * uAmp.y -\n Math.sin(movementProgressFix * Math.PI * uFreq.y + time + Math.PI / 2) * uAmp.y,\n 0\n );\n const lookAtAmp = new THREE.Vector3(2, 0.4, 1);\n const lookAtOffset = new THREE.Vector3(0, 0, -3);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n LongRaceDistortion: {\n uniforms: LongRaceUniforms,\n getDistortion: `\n uniform vec2 uFreq;\n uniform vec2 uAmp;\n #define PI 3.14159265358979\n vec3 getDistortion(float progress){\n float camProgress = 0.0125;\n return vec3( \n sin(progress * PI * uFreq.x + uTime) * uAmp.x - sin(camProgress * PI * uFreq.x + uTime) * uAmp.x,\n sin(progress * PI * uFreq.y + uTime) * uAmp.y - sin(camProgress * PI * uFreq.y + uTime) * uAmp.y,\n 0.\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const camProgress = 0.0125;\n const uFreq = LongRaceUniforms.uFreq.value;\n const uAmp = LongRaceUniforms.uAmp.value;\n const distortion = new THREE.Vector3(\n Math.sin(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.sin(camProgress * Math.PI * uFreq.x + time) * uAmp.x,\n Math.sin(progress * Math.PI * uFreq.y + time) * uAmp.y -\n Math.sin(camProgress * Math.PI * uFreq.y + time) * uAmp.y,\n 0\n );\n const lookAtAmp = new THREE.Vector3(1, 1, 0);\n const lookAtOffset = new THREE.Vector3(0, 0, -5);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n turbulentDistortion: {\n uniforms: turbulentUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n cos(PI * progress * uFreq.r + uTime) * uAmp.r +\n pow(cos(PI * progress * uFreq.g + uTime * (uFreq.g / uFreq.r)), 2. ) * uAmp.g\n );\n }\n float getDistortionY(float progress){\n return (\n -nsin(PI * progress * uFreq.b + uTime) * uAmp.b +\n -pow(nsin(PI * progress * uFreq.a + uTime / (uFreq.b / uFreq.a)), 5.) * uAmp.a\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.0125),\n getDistortionY(progress) - getDistortionY(0.0125),\n 0.\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const uFreq = turbulentUniforms.uFreq.value;\n const uAmp = turbulentUniforms.uAmp.value;\n\n const getX = (p: number) =>\n Math.cos(Math.PI * p * uFreq.x + time) * uAmp.x +\n Math.pow(Math.cos(Math.PI * p * uFreq.y + time * (uFreq.y / uFreq.x)), 2) * uAmp.y;\n\n const getY = (p: number) =>\n -nsin(Math.PI * p * uFreq.z + time) * uAmp.z -\n Math.pow(nsin(Math.PI * p * uFreq.w + time / (uFreq.z / uFreq.w)), 5) * uAmp.w;\n\n const distortion = new THREE.Vector3(\n getX(progress) - getX(progress + 0.007),\n getY(progress) - getY(progress + 0.007),\n 0\n );\n const lookAtAmp = new THREE.Vector3(-2, -5, 0);\n const lookAtOffset = new THREE.Vector3(0, 0, -10);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n turbulentDistortionStill: {\n uniforms: turbulentUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n cos(PI * progress * uFreq.r) * uAmp.r +\n pow(cos(PI * progress * uFreq.g * (uFreq.g / uFreq.r)), 2. ) * uAmp.g\n );\n }\n float getDistortionY(float progress){\n return (\n -nsin(PI * progress * uFreq.b) * uAmp.b +\n -pow(nsin(PI * progress * uFreq.a / (uFreq.b / uFreq.a)), 5.) * uAmp.a\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.02),\n 0.\n );\n }\n `\n },\n deepDistortionStill: {\n uniforms: deepUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n uniform vec2 uPowY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n sin(progress * PI * uFreq.x) * uAmp.x * 2.\n );\n }\n float getDistortionY(float progress){\n return (\n pow(abs(progress * uPowY.x), uPowY.y) + sin(progress * PI * uFreq.y) * uAmp.y\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.05),\n 0.\n );\n }\n `\n },\n deepDistortion: {\n uniforms: deepUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n uniform vec2 uPowY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n sin(progress * PI * uFreq.x + uTime) * uAmp.x\n );\n }\n float getDistortionY(float progress){\n return (\n pow(abs(progress * uPowY.x), uPowY.y) + sin(progress * PI * uFreq.y + uTime) * uAmp.y\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.02),\n 0.\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const uFreq = deepUniforms.uFreq.value;\n const uAmp = deepUniforms.uAmp.value;\n const uPowY = deepUniforms.uPowY.value;\n\n const getX = (p: number) => Math.sin(p * Math.PI * uFreq.x + time) * uAmp.x;\n const getY = (p: number) => Math.pow(p * uPowY.x, uPowY.y) + Math.sin(p * Math.PI * uFreq.y + time) * uAmp.y;\n\n const distortion = new THREE.Vector3(\n getX(progress) - getX(progress + 0.01),\n getY(progress) - getY(progress + 0.01),\n 0\n );\n const lookAtAmp = new THREE.Vector3(-2, -4, 0);\n const lookAtOffset = new THREE.Vector3(0, 0, -10);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n }\n};\n\nconst distortion_uniforms = {\n uDistortionX: { value: new THREE.Vector2(80, 3) },\n uDistortionY: { value: new THREE.Vector2(-40, 2.5) }\n};\n\nconst distortion_vertex = `\n #define PI 3.14159265358979\n uniform vec2 uDistortionX;\n uniform vec2 uDistortionY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n vec3 getDistortion(float progress){\n progress = clamp(progress, 0., 1.);\n float xAmp = uDistortionX.r;\n float xFreq = uDistortionX.g;\n float yAmp = uDistortionY.r;\n float yFreq = uDistortionY.g;\n return vec3( \n xAmp * nsin(progress * PI * xFreq - PI / 2.),\n yAmp * nsin(progress * PI * yFreq - PI / 2.),\n 0.\n );\n }\n`;\n\nfunction random(base: number | [number, number]): number {\n if (Array.isArray(base)) {\n return Math.random() * (base[1] - base[0]) + base[0];\n }\n return Math.random() * base;\n}\n\nfunction pickRandom(arr: T | T[]): T {\n if (Array.isArray(arr)) {\n return arr[Math.floor(Math.random() * arr.length)];\n }\n return arr;\n}\n\nfunction lerp(current: number, target: number, speed = 0.1, limit = 0.001): number {\n let change = (target - current) * speed;\n if (Math.abs(change) < limit) {\n change = target - current;\n }\n return change;\n}\n\nclass CarLights {\n webgl: App;\n options: HyperspeedOptions;\n colors: number[] | THREE.Color;\n speed: [number, number];\n fade: THREE.Vector2;\n mesh!: THREE.Mesh;\n\n constructor(\n webgl: App,\n options: HyperspeedOptions,\n colors: number[] | THREE.Color,\n speed: [number, number],\n fade: THREE.Vector2\n ) {\n this.webgl = webgl;\n this.options = options;\n this.colors = colors;\n this.speed = speed;\n this.fade = fade;\n }\n\n init() {\n const options = this.options;\n const curve = new THREE.LineCurve3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -1));\n const geometry = new THREE.TubeGeometry(curve, 40, 1, 8, false);\n\n const instanced = new THREE.InstancedBufferGeometry().copy(geometry as any) as THREE.InstancedBufferGeometry;\n instanced.instanceCount = options.lightPairsPerRoadWay * 2;\n\n const laneWidth = options.roadWidth / options.lanesPerRoad;\n\n const aOffset: number[] = [];\n const aMetrics: number[] = [];\n const aColor: number[] = [];\n\n let colorArray: THREE.Color[];\n if (Array.isArray(this.colors)) {\n colorArray = this.colors.map(c => new THREE.Color(c));\n } else {\n colorArray = [new THREE.Color(this.colors)];\n }\n\n for (let i = 0; i < options.lightPairsPerRoadWay; i++) {\n const radius = random(options.carLightsRadius);\n const length = random(options.carLightsLength);\n const spd = random(this.speed);\n\n const carLane = i % options.lanesPerRoad;\n let laneX = carLane * laneWidth - options.roadWidth / 2 + laneWidth / 2;\n\n const carWidth = random(options.carWidthPercentage) * laneWidth;\n const carShiftX = random(options.carShiftX) * laneWidth;\n laneX += carShiftX;\n\n const offsetY = random(options.carFloorSeparation) + radius * 1.3;\n const offsetZ = -random(options.length);\n\n aOffset.push(laneX - carWidth / 2);\n aOffset.push(offsetY);\n aOffset.push(offsetZ);\n\n aOffset.push(laneX + carWidth / 2);\n aOffset.push(offsetY);\n aOffset.push(offsetZ);\n\n aMetrics.push(radius);\n aMetrics.push(length);\n aMetrics.push(spd);\n\n aMetrics.push(radius);\n aMetrics.push(length);\n aMetrics.push(spd);\n\n const color = pickRandom(colorArray);\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n }\n\n instanced.setAttribute('aOffset', new THREE.InstancedBufferAttribute(new Float32Array(aOffset), 3, false));\n instanced.setAttribute('aMetrics', new THREE.InstancedBufferAttribute(new Float32Array(aMetrics), 3, false));\n instanced.setAttribute('aColor', new THREE.InstancedBufferAttribute(new Float32Array(aColor), 3, false));\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: carLightsFragment,\n vertexShader: carLightsVertex,\n transparent: true,\n uniforms: Object.assign(\n {\n uTime: { value: 0 },\n uTravelLength: { value: options.length },\n uFade: { value: this.fade }\n },\n this.webgl.fogUniforms,\n (typeof this.options.distortion === 'object' ? this.options.distortion.uniforms : {}) || {}\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n typeof this.options.distortion === 'object' ? this.options.distortion.getDistortion : ''\n );\n };\n\n const mesh = new THREE.Mesh(instanced, material);\n mesh.frustumCulled = false;\n this.webgl.scene.add(mesh);\n this.mesh = mesh;\n }\n\n update(time: number) {\n if (this.mesh.material.uniforms.uTime) {\n this.mesh.material.uniforms.uTime.value = time;\n }\n }\n}\n\nconst carLightsFragment = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_fragment']}\n varying vec3 vColor;\n varying vec2 vUv; \n uniform vec2 uFade;\n void main() {\n vec3 color = vec3(vColor);\n float alpha = smoothstep(uFade.x, uFade.y, vUv.x);\n gl_FragColor = vec4(color, alpha);\n if (gl_FragColor.a < 0.0001) discard;\n ${THREE.ShaderChunk['fog_fragment']}\n }\n`;\n\nconst carLightsVertex = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n attribute vec3 aOffset;\n attribute vec3 aMetrics;\n attribute vec3 aColor;\n uniform float uTravelLength;\n uniform float uTime;\n varying vec2 vUv; \n varying vec3 vColor; \n #include \n void main() {\n vec3 transformed = position.xyz;\n float radius = aMetrics.r;\n float myLength = aMetrics.g;\n float speed = aMetrics.b;\n\n transformed.xy *= radius;\n transformed.z *= myLength;\n\n transformed.z += myLength - mod(uTime * speed + aOffset.z, uTravelLength);\n transformed.xy += aOffset.xy;\n\n float progress = abs(transformed.z / uTravelLength);\n transformed.xyz += getDistortion(progress);\n\n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vUv = uv;\n vColor = aColor;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n`;\n\nclass LightsSticks {\n webgl: App;\n options: HyperspeedOptions;\n mesh!: THREE.Mesh;\n\n constructor(webgl: App, options: HyperspeedOptions) {\n this.webgl = webgl;\n this.options = options;\n }\n\n init() {\n const options = this.options;\n const geometry = new THREE.PlaneGeometry(1, 1);\n const instanced = new THREE.InstancedBufferGeometry().copy(geometry as any) as THREE.InstancedBufferGeometry;\n const totalSticks = options.totalSideLightSticks;\n instanced.instanceCount = totalSticks;\n\n const stickoffset = options.length / (totalSticks - 1);\n const aOffset: number[] = [];\n const aColor: number[] = [];\n const aMetrics: number[] = [];\n\n let colorArray: THREE.Color[];\n if (Array.isArray(options.colors.sticks)) {\n colorArray = options.colors.sticks.map(c => new THREE.Color(c));\n } else {\n colorArray = [new THREE.Color(options.colors.sticks)];\n }\n\n for (let i = 0; i < totalSticks; i++) {\n const width = random(options.lightStickWidth);\n const height = random(options.lightStickHeight);\n aOffset.push((i - 1) * stickoffset * 2 + stickoffset * Math.random());\n\n const color = pickRandom(colorArray);\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n\n aMetrics.push(width);\n aMetrics.push(height);\n }\n\n instanced.setAttribute('aOffset', new THREE.InstancedBufferAttribute(new Float32Array(aOffset), 1, false));\n instanced.setAttribute('aColor', new THREE.InstancedBufferAttribute(new Float32Array(aColor), 3, false));\n instanced.setAttribute('aMetrics', new THREE.InstancedBufferAttribute(new Float32Array(aMetrics), 2, false));\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: sideSticksFragment,\n vertexShader: sideSticksVertex,\n side: THREE.DoubleSide,\n uniforms: Object.assign(\n {\n uTravelLength: { value: options.length },\n uTime: { value: 0 }\n },\n this.webgl.fogUniforms,\n (typeof options.distortion === 'object' ? options.distortion.uniforms : {}) || {}\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n typeof this.options.distortion === 'object' ? this.options.distortion.getDistortion : ''\n );\n };\n\n const mesh = new THREE.Mesh(instanced, material);\n mesh.frustumCulled = false;\n this.webgl.scene.add(mesh);\n this.mesh = mesh;\n }\n\n update(time: number) {\n if (this.mesh.material.uniforms.uTime) {\n this.mesh.material.uniforms.uTime.value = time;\n }\n }\n}\n\nconst sideSticksVertex = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n attribute float aOffset;\n attribute vec3 aColor;\n attribute vec2 aMetrics;\n uniform float uTravelLength;\n uniform float uTime;\n varying vec3 vColor;\n mat4 rotationY( in float angle ) {\n return mat4(\n cos(angle),\t\t0,\t\tsin(angle),\t0,\n 0,\t\t 1.0,\t0,\t\t\t0,\n -sin(angle),\t 0,\t\tcos(angle),\t0,\n 0, \t\t 0,\t\t0,\t\t\t1\n );\n }\n #include \n void main(){\n vec3 transformed = position.xyz;\n float width = aMetrics.x;\n float height = aMetrics.y;\n\n transformed.xy *= vec2(width, height);\n float time = mod(uTime * 60. * 2. + aOffset, uTravelLength);\n\n transformed = (rotationY(3.14/2.) * vec4(transformed,1.)).xyz;\n transformed.z += - uTravelLength + time;\n\n float progress = abs(transformed.z / uTravelLength);\n transformed.xyz += getDistortion(progress);\n\n transformed.y += height / 2.;\n transformed.x += -width / 2.;\n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vColor = aColor;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n`;\n\nconst sideSticksFragment = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_fragment']}\n varying vec3 vColor;\n void main(){\n vec3 color = vec3(vColor);\n gl_FragColor = vec4(color,1.);\n ${THREE.ShaderChunk['fog_fragment']}\n }\n`;\n\nclass Road {\n webgl: App;\n options: HyperspeedOptions;\n uTime: { value: number };\n leftRoadWay!: THREE.Mesh;\n rightRoadWay!: THREE.Mesh;\n island!: THREE.Mesh;\n\n constructor(webgl: App, options: HyperspeedOptions) {\n this.webgl = webgl;\n this.options = options;\n this.uTime = { value: 0 };\n }\n\n createPlane(side: number, width: number, isRoad: boolean) {\n const options = this.options;\n const segments = 100;\n const geometry = new THREE.PlaneGeometry(\n isRoad ? options.roadWidth : options.islandWidth,\n options.length,\n 20,\n segments\n );\n\n let uniforms: Record = {\n uTravelLength: { value: options.length },\n uColor: {\n value: new THREE.Color(isRoad ? options.colors.roadColor : options.colors.islandColor)\n },\n uTime: this.uTime\n };\n\n if (isRoad) {\n uniforms = Object.assign(uniforms, {\n uLanes: { value: options.lanesPerRoad },\n uBrokenLinesColor: {\n value: new THREE.Color(options.colors.brokenLines)\n },\n uShoulderLinesColor: {\n value: new THREE.Color(options.colors.shoulderLines)\n },\n uShoulderLinesWidthPercentage: {\n value: options.shoulderLinesWidthPercentage\n },\n uBrokenLinesLengthPercentage: {\n value: options.brokenLinesLengthPercentage\n },\n uBrokenLinesWidthPercentage: {\n value: options.brokenLinesWidthPercentage\n }\n });\n }\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: isRoad ? roadFragment : islandFragment,\n vertexShader: roadVertex,\n side: THREE.DoubleSide,\n uniforms: Object.assign(\n uniforms,\n this.webgl.fogUniforms,\n (typeof options.distortion === 'object' ? options.distortion.uniforms : {}) || {}\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n typeof this.options.distortion === 'object' ? this.options.distortion.getDistortion : ''\n );\n };\n\n const mesh = new THREE.Mesh(geometry, material);\n mesh.rotation.x = -Math.PI / 2;\n mesh.position.z = -options.length / 2;\n mesh.position.x += (this.options.islandWidth / 2 + options.roadWidth / 2) * side;\n\n this.webgl.scene.add(mesh);\n return mesh;\n }\n\n init() {\n this.leftRoadWay = this.createPlane(-1, this.options.roadWidth, true);\n this.rightRoadWay = this.createPlane(1, this.options.roadWidth, true);\n this.island = this.createPlane(0, this.options.islandWidth, false);\n }\n\n update(time: number) {\n this.uTime.value = time;\n }\n}\n\nconst roadBaseFragment = `\n #define USE_FOG;\n varying vec2 vUv; \n uniform vec3 uColor;\n uniform float uTime;\n #include \n ${THREE.ShaderChunk['fog_pars_fragment']}\n void main() {\n vec2 uv = vUv;\n vec3 color = vec3(uColor);\n #include \n gl_FragColor = vec4(color, 1.);\n ${THREE.ShaderChunk['fog_fragment']}\n }\n`;\n\nconst islandFragment = roadBaseFragment\n .replace('#include ', '')\n .replace('#include ', '');\n\nconst roadMarkings_vars = `\n uniform float uLanes;\n uniform vec3 uBrokenLinesColor;\n uniform vec3 uShoulderLinesColor;\n uniform float uShoulderLinesWidthPercentage;\n uniform float uBrokenLinesWidthPercentage;\n uniform float uBrokenLinesLengthPercentage;\n highp float random(vec2 co) {\n highp float a = 12.9898;\n highp float b = 78.233;\n highp float c = 43758.5453;\n highp float dt = dot(co.xy, vec2(a, b));\n highp float sn = mod(dt, 3.14);\n return fract(sin(sn) * c);\n }\n`;\n\nconst roadMarkings_fragment = `\n uv.y = mod(uv.y + uTime * 0.05, 1.);\n float laneWidth = 1.0 / uLanes;\n float brokenLineWidth = laneWidth * uBrokenLinesWidthPercentage;\n float laneEmptySpace = 1. - uBrokenLinesLengthPercentage;\n\n float brokenLines = step(1.0 - brokenLineWidth, fract(uv.x * 2.0)) * step(laneEmptySpace, fract(uv.y * 10.0));\n float sideLines = step(1.0 - brokenLineWidth, fract((uv.x - laneWidth * (uLanes - 1.0)) * 2.0)) + step(brokenLineWidth, uv.x);\n\n brokenLines = mix(brokenLines, sideLines, uv.x);\n`;\n\nconst roadFragment = roadBaseFragment\n .replace('#include ', roadMarkings_fragment)\n .replace('#include ', roadMarkings_vars);\n\nconst roadVertex = `\n #define USE_FOG;\n uniform float uTime;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n uniform float uTravelLength;\n varying vec2 vUv; \n #include \n void main() {\n vec3 transformed = position.xyz;\n vec3 distortion = getDistortion((transformed.y + uTravelLength / 2.) / uTravelLength);\n transformed.x += distortion.x;\n transformed.z += distortion.y;\n transformed.y += -1. * distortion.z; \n \n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vUv = uv;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n`;\n\nfunction resizeRendererToDisplaySize(\n renderer: THREE.WebGLRenderer,\n setSize: (width: number, height: number, updateStyle: boolean) => void\n) {\n const canvas = renderer.domElement;\n const width = canvas.clientWidth;\n const height = canvas.clientHeight;\n if (width <= 0 || height <= 0) return false;\n const needResize = canvas.width !== width || canvas.height !== height;\n if (needResize) {\n setSize(width, height, false);\n }\n return needResize;\n}\n\nclass App {\n container: HTMLElement;\n options: HyperspeedOptions;\n renderer: THREE.WebGLRenderer;\n composer: EffectComposer;\n camera: THREE.PerspectiveCamera;\n scene: THREE.Scene;\n renderPass!: RenderPass;\n bloomPass!: EffectPass;\n clock: THREE.Clock;\n assets: Record;\n disposed: boolean;\n road: Road;\n leftCarLights: CarLights;\n rightCarLights: CarLights;\n leftSticks: LightsSticks;\n fogUniforms: Record;\n fovTarget: number;\n speedUpTarget: number;\n speedUp: number;\n timeOffset: number;\n hasValidSize: boolean;\n\n constructor(container: HTMLElement, options: HyperspeedOptions) {\n this.options = options;\n if (!this.options.distortion) {\n this.options.distortion = {\n uniforms: distortion_uniforms,\n getDistortion: distortion_vertex\n };\n }\n this.container = container;\n this.hasValidSize = false;\n\n const initW = Math.max(1, container.offsetWidth);\n const initH = Math.max(1, container.offsetHeight);\n\n this.renderer = new THREE.WebGLRenderer({\n antialias: false,\n alpha: true\n });\n this.renderer.setSize(initW, initH, false);\n this.renderer.setPixelRatio(window.devicePixelRatio);\n\n this.composer = new EffectComposer(this.renderer);\n container.appendChild(this.renderer.domElement);\n\n this.camera = new THREE.PerspectiveCamera(options.fov, initW / initH, 0.1, 10000);\n this.camera.position.z = -5;\n this.camera.position.y = 8;\n this.camera.position.x = 0;\n\n this.scene = new THREE.Scene();\n this.scene.background = null;\n\n const fog = new THREE.Fog(options.colors.background, options.length * 0.2, options.length * 500);\n this.scene.fog = fog;\n\n this.fogUniforms = {\n fogColor: { value: fog.color },\n fogNear: { value: fog.near },\n fogFar: { value: fog.far }\n };\n\n this.clock = new THREE.Clock();\n this.assets = {};\n this.disposed = false;\n\n this.road = new Road(this, options);\n this.leftCarLights = new CarLights(\n this,\n options,\n options.colors.leftCars,\n options.movingAwaySpeed,\n new THREE.Vector2(0, 1 - options.carLightsFade)\n );\n this.rightCarLights = new CarLights(\n this,\n options,\n options.colors.rightCars,\n options.movingCloserSpeed,\n new THREE.Vector2(1, 0 + options.carLightsFade)\n );\n this.leftSticks = new LightsSticks(this, options);\n\n this.fovTarget = options.fov;\n this.speedUpTarget = 0;\n this.speedUp = 0;\n this.timeOffset = 0;\n\n this.tick = this.tick.bind(this);\n this.init = this.init.bind(this);\n this.setSize = this.setSize.bind(this);\n this.onMouseDown = this.onMouseDown.bind(this);\n this.onMouseUp = this.onMouseUp.bind(this);\n\n this.onTouchStart = this.onTouchStart.bind(this);\n this.onTouchEnd = this.onTouchEnd.bind(this);\n this.onContextMenu = this.onContextMenu.bind(this);\n\n this.onWindowResize = this.onWindowResize.bind(this);\n window.addEventListener('resize', this.onWindowResize);\n\n if (container.offsetWidth > 0 && container.offsetHeight > 0) {\n this.hasValidSize = true;\n }\n }\n\n onWindowResize() {\n const width = this.container.offsetWidth;\n const height = this.container.offsetHeight;\n\n if (width <= 0 || height <= 0) {\n this.hasValidSize = false;\n return;\n }\n\n this.renderer.setSize(width, height);\n this.camera.aspect = width / height;\n this.camera.updateProjectionMatrix();\n this.composer.setSize(width, height);\n this.hasValidSize = true;\n }\n\n initPasses() {\n this.renderPass = new RenderPass(this.scene, this.camera);\n this.bloomPass = new EffectPass(\n this.camera,\n new BloomEffect({\n luminanceThreshold: 0.2,\n luminanceSmoothing: 0,\n resolutionScale: 1\n })\n );\n\n const smaaPass = new EffectPass(\n this.camera,\n new SMAAEffect({\n preset: SMAAPreset.MEDIUM\n })\n );\n this.renderPass.renderToScreen = false;\n this.bloomPass.renderToScreen = false;\n smaaPass.renderToScreen = true;\n\n this.composer.addPass(this.renderPass);\n this.composer.addPass(this.bloomPass);\n this.composer.addPass(smaaPass);\n }\n\n loadAssets(): Promise {\n const assets = this.assets;\n return new Promise(resolve => {\n const manager = new THREE.LoadingManager(resolve);\n\n const searchImage = new Image();\n const areaImage = new Image();\n assets.smaa = {};\n\n searchImage.addEventListener('load', function () {\n assets.smaa.search = this;\n manager.itemEnd('smaa-search');\n });\n\n areaImage.addEventListener('load', function () {\n assets.smaa.area = this;\n manager.itemEnd('smaa-area');\n });\n\n manager.itemStart('smaa-search');\n manager.itemStart('smaa-area');\n\n searchImage.src = SMAAEffect.searchImageDataURL;\n areaImage.src = SMAAEffect.areaImageDataURL;\n });\n }\n\n init() {\n this.initPasses();\n const options = this.options;\n this.road.init();\n this.leftCarLights.init();\n this.leftCarLights.mesh.position.setX(-options.roadWidth / 2 - options.islandWidth / 2);\n\n this.rightCarLights.init();\n this.rightCarLights.mesh.position.setX(options.roadWidth / 2 + options.islandWidth / 2);\n\n this.leftSticks.init();\n this.leftSticks.mesh.position.setX(-(options.roadWidth + options.islandWidth / 2));\n\n this.container.addEventListener('mousedown', this.onMouseDown);\n this.container.addEventListener('mouseup', this.onMouseUp);\n this.container.addEventListener('mouseout', this.onMouseUp);\n\n this.container.addEventListener('touchstart', this.onTouchStart, { passive: true });\n this.container.addEventListener('touchend', this.onTouchEnd, { passive: true });\n this.container.addEventListener('touchcancel', this.onTouchEnd, { passive: true });\n this.container.addEventListener('contextmenu', this.onContextMenu);\n\n this.tick();\n }\n\n onMouseDown(ev: MouseEvent) {\n if (this.options.onSpeedUp) this.options.onSpeedUp(ev);\n this.fovTarget = this.options.fovSpeedUp;\n this.speedUpTarget = this.options.speedUp;\n }\n\n onMouseUp(ev: MouseEvent) {\n if (this.options.onSlowDown) this.options.onSlowDown(ev);\n this.fovTarget = this.options.fov;\n this.speedUpTarget = 0;\n }\n\n onTouchStart(ev: TouchEvent) {\n if (this.options.onSpeedUp) this.options.onSpeedUp(ev);\n this.fovTarget = this.options.fovSpeedUp;\n this.speedUpTarget = this.options.speedUp;\n }\n\n onTouchEnd(ev: TouchEvent) {\n if (this.options.onSlowDown) this.options.onSlowDown(ev);\n this.fovTarget = this.options.fov;\n this.speedUpTarget = 0;\n }\n\n onContextMenu(ev: MouseEvent) {\n ev.preventDefault();\n }\n\n update(delta: number) {\n const lerpPercentage = Math.exp(-(-60 * Math.log2(1 - 0.1)) * delta);\n this.speedUp += lerp(this.speedUp, this.speedUpTarget, lerpPercentage, 0.00001);\n this.timeOffset += this.speedUp * delta;\n const time = this.clock.elapsedTime + this.timeOffset;\n\n this.rightCarLights.update(time);\n this.leftCarLights.update(time);\n this.leftSticks.update(time);\n this.road.update(time);\n\n let updateCamera = false;\n const fovChange = lerp(this.camera.fov, this.fovTarget, lerpPercentage);\n if (fovChange !== 0) {\n this.camera.fov += fovChange * delta * 6;\n updateCamera = true;\n }\n\n if (typeof this.options.distortion === 'object' && this.options.distortion.getJS) {\n const distortion = this.options.distortion.getJS(0.025, time);\n this.camera.lookAt(\n new THREE.Vector3(\n this.camera.position.x + distortion.x,\n this.camera.position.y + distortion.y,\n this.camera.position.z + distortion.z\n )\n );\n updateCamera = true;\n }\n\n if (updateCamera) {\n this.camera.updateProjectionMatrix();\n }\n }\n\n render(delta: number) {\n this.composer.render(delta);\n }\n\n dispose() {\n this.disposed = true;\n\n if (this.scene) {\n this.scene.traverse(object => {\n const obj = object as unknown as THREE.Mesh;\n if (!obj.isMesh) return;\n\n if (obj.geometry) obj.geometry.dispose();\n\n if (obj.material) {\n if (Array.isArray(obj.material)) {\n obj.material.forEach(material => material.dispose());\n } else {\n obj.material.dispose();\n }\n }\n });\n this.scene.clear();\n }\n\n if (this.renderer) {\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n if (this.renderer.domElement && this.renderer.domElement.parentNode) {\n this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);\n }\n }\n if (this.composer) {\n this.composer.dispose();\n }\n\n window.removeEventListener('resize', this.onWindowResize);\n if (this.container) {\n this.container.removeEventListener('mousedown', this.onMouseDown);\n this.container.removeEventListener('mouseup', this.onMouseUp);\n this.container.removeEventListener('mouseout', this.onMouseUp);\n\n this.container.removeEventListener('touchstart', this.onTouchStart);\n this.container.removeEventListener('touchend', this.onTouchEnd);\n this.container.removeEventListener('touchcancel', this.onTouchEnd);\n this.container.removeEventListener('contextmenu', this.onContextMenu);\n }\n }\n\n setSize(width: number, height: number, updateStyles: boolean) {\n this.composer.setSize(width, height, updateStyles);\n }\n\n tick() {\n if (this.disposed) return;\n\n if (!this.hasValidSize) {\n const w = this.container.offsetWidth;\n const h = this.container.offsetHeight;\n if (w > 0 && h > 0) {\n this.renderer.setSize(w, h, false);\n this.camera.aspect = w / h;\n this.camera.updateProjectionMatrix();\n this.composer.setSize(w, h);\n this.hasValidSize = true;\n } else {\n requestAnimationFrame(this.tick);\n return;\n }\n }\n\n if (resizeRendererToDisplaySize(this.renderer, this.setSize)) {\n const canvas = this.renderer.domElement;\n if (this.hasValidSize) {\n this.camera.aspect = canvas.clientWidth / canvas.clientHeight;\n this.camera.updateProjectionMatrix();\n }\n }\n\n if (this.hasValidSize) {\n const delta = this.clock.getDelta();\n this.render(delta);\n this.update(delta);\n }\n\n requestAnimationFrame(this.tick);\n }\n}\n\nconst DEFAULT_EFFECT_OPTIONS: Partial = {};\n\nconst Hyperspeed: FC = ({ effectOptions = DEFAULT_EFFECT_OPTIONS }) => {\n const hyperspeed = useRef(null);\n const appRef = useRef(null);\n\n useEffect(() => {\n if (appRef.current) {\n appRef.current.dispose();\n appRef.current = null;\n const container = hyperspeed.current;\n if (container) {\n while (container.firstChild) {\n container.removeChild(container.firstChild);\n }\n }\n }\n\n const container = hyperspeed.current;\n if (!container) return;\n\n const options: HyperspeedOptions = {\n ...defaultOptions,\n ...effectOptions,\n colors: { ...defaultOptions.colors, ...effectOptions.colors }\n };\n if (typeof options.distortion === 'string') {\n options.distortion = distortions[options.distortion];\n }\n\n const myApp = new App(container, options);\n appRef.current = myApp;\n myApp.loadAssets().then(myApp.init);\n\n return () => {\n if (appRef.current) {\n appRef.current.dispose();\n }\n };\n }, [effectOptions]);\n\n return
;\n};\n\nexport default Hyperspeed;\n" }, { "type": "registry:component", - "path": "Hyperspeed/Hyperspeed.tsx", - "content": "import { BloomEffect, EffectComposer, EffectPass, RenderPass, SMAAEffect, SMAAPreset } from 'postprocessing';\nimport { FC, useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\ninterface Distortion {\n uniforms: Record;\n getDistortion: string;\n getJS?: (progress: number, time: number) => THREE.Vector3;\n}\n\ninterface Distortions {\n [key: string]: Distortion;\n}\n\ninterface Colors {\n roadColor: number;\n islandColor: number;\n background: number;\n shoulderLines: number;\n brokenLines: number;\n leftCars: number[];\n rightCars: number[];\n sticks: number;\n}\n\ninterface HyperspeedOptions {\n onSpeedUp?: (ev: MouseEvent | TouchEvent) => void;\n onSlowDown?: (ev: MouseEvent | TouchEvent) => void;\n distortion?: string | Distortion;\n length: number;\n roadWidth: number;\n islandWidth: number;\n lanesPerRoad: number;\n fov: number;\n fovSpeedUp: number;\n speedUp: number;\n carLightsFade: number;\n totalSideLightSticks: number;\n lightPairsPerRoadWay: number;\n shoulderLinesWidthPercentage: number;\n brokenLinesWidthPercentage: number;\n brokenLinesLengthPercentage: number;\n lightStickWidth: [number, number];\n lightStickHeight: [number, number];\n movingAwaySpeed: [number, number];\n movingCloserSpeed: [number, number];\n carLightsLength: [number, number];\n carLightsRadius: [number, number];\n carWidthPercentage: [number, number];\n carShiftX: [number, number];\n carFloorSeparation: [number, number];\n colors: Colors;\n isHyper?: boolean;\n}\n\ninterface HyperspeedProps {\n effectOptions?: Partial;\n}\n\nconst defaultOptions: HyperspeedOptions = {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 2,\n lanesPerRoad: 4,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 20,\n lightPairsPerRoadWay: 40,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.03, 400 * 0.2],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.8, 0.8],\n carFloorSeparation: [0, 5],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0xffffff,\n brokenLines: 0xffffff,\n leftCars: [0xd856bf, 0x6750a2, 0xc247ac],\n rightCars: [0x03b3c3, 0x0e5ea5, 0x324555],\n sticks: 0x03b3c3\n }\n};\n\nfunction nsin(val: number) {\n return Math.sin(val) * 0.5 + 0.5;\n}\n\nconst mountainUniforms = {\n uFreq: { value: new THREE.Vector3(3, 6, 10) },\n uAmp: { value: new THREE.Vector3(30, 30, 20) }\n};\n\nconst xyUniforms = {\n uFreq: { value: new THREE.Vector2(5, 2) },\n uAmp: { value: new THREE.Vector2(25, 15) }\n};\n\nconst LongRaceUniforms = {\n uFreq: { value: new THREE.Vector2(2, 3) },\n uAmp: { value: new THREE.Vector2(35, 10) }\n};\n\nconst turbulentUniforms = {\n uFreq: { value: new THREE.Vector4(4, 8, 8, 1) },\n uAmp: { value: new THREE.Vector4(25, 5, 10, 10) }\n};\n\nconst deepUniforms = {\n uFreq: { value: new THREE.Vector2(4, 8) },\n uAmp: { value: new THREE.Vector2(10, 20) },\n uPowY: { value: new THREE.Vector2(20, 2) }\n};\n\nconst distortions: Distortions = {\n mountainDistortion: {\n uniforms: mountainUniforms,\n getDistortion: `\n uniform vec3 uAmp;\n uniform vec3 uFreq;\n #define PI 3.14159265358979\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n vec3 getDistortion(float progress){\n float movementProgressFix = 0.02;\n return vec3( \n cos(progress * PI * uFreq.x + uTime) * uAmp.x - cos(movementProgressFix * PI * uFreq.x + uTime) * uAmp.x,\n nsin(progress * PI * uFreq.y + uTime) * uAmp.y - nsin(movementProgressFix * PI * uFreq.y + uTime) * uAmp.y,\n nsin(progress * PI * uFreq.z + uTime) * uAmp.z - nsin(movementProgressFix * PI * uFreq.z + uTime) * uAmp.z\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const movementProgressFix = 0.02;\n const uFreq = mountainUniforms.uFreq.value;\n const uAmp = mountainUniforms.uAmp.value;\n const distortion = new THREE.Vector3(\n Math.cos(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.cos(movementProgressFix * Math.PI * uFreq.x + time) * uAmp.x,\n nsin(progress * Math.PI * uFreq.y + time) * uAmp.y -\n nsin(movementProgressFix * Math.PI * uFreq.y + time) * uAmp.y,\n nsin(progress * Math.PI * uFreq.z + time) * uAmp.z -\n nsin(movementProgressFix * Math.PI * uFreq.z + time) * uAmp.z\n );\n const lookAtAmp = new THREE.Vector3(2, 2, 2);\n const lookAtOffset = new THREE.Vector3(0, 0, -5);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n xyDistortion: {\n uniforms: xyUniforms,\n getDistortion: `\n uniform vec2 uFreq;\n uniform vec2 uAmp;\n #define PI 3.14159265358979\n vec3 getDistortion(float progress){\n float movementProgressFix = 0.02;\n return vec3( \n cos(progress * PI * uFreq.x + uTime) * uAmp.x - cos(movementProgressFix * PI * uFreq.x + uTime) * uAmp.x,\n sin(progress * PI * uFreq.y + PI/2. + uTime) * uAmp.y - sin(movementProgressFix * PI * uFreq.y + PI/2. + uTime) * uAmp.y,\n 0.\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const movementProgressFix = 0.02;\n const uFreq = xyUniforms.uFreq.value;\n const uAmp = xyUniforms.uAmp.value;\n const distortion = new THREE.Vector3(\n Math.cos(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.cos(movementProgressFix * Math.PI * uFreq.x + time) * uAmp.x,\n Math.sin(progress * Math.PI * uFreq.y + time + Math.PI / 2) * uAmp.y -\n Math.sin(movementProgressFix * Math.PI * uFreq.y + time + Math.PI / 2) * uAmp.y,\n 0\n );\n const lookAtAmp = new THREE.Vector3(2, 0.4, 1);\n const lookAtOffset = new THREE.Vector3(0, 0, -3);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n LongRaceDistortion: {\n uniforms: LongRaceUniforms,\n getDistortion: `\n uniform vec2 uFreq;\n uniform vec2 uAmp;\n #define PI 3.14159265358979\n vec3 getDistortion(float progress){\n float camProgress = 0.0125;\n return vec3( \n sin(progress * PI * uFreq.x + uTime) * uAmp.x - sin(camProgress * PI * uFreq.x + uTime) * uAmp.x,\n sin(progress * PI * uFreq.y + uTime) * uAmp.y - sin(camProgress * PI * uFreq.y + uTime) * uAmp.y,\n 0.\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const camProgress = 0.0125;\n const uFreq = LongRaceUniforms.uFreq.value;\n const uAmp = LongRaceUniforms.uAmp.value;\n const distortion = new THREE.Vector3(\n Math.sin(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.sin(camProgress * Math.PI * uFreq.x + time) * uAmp.x,\n Math.sin(progress * Math.PI * uFreq.y + time) * uAmp.y -\n Math.sin(camProgress * Math.PI * uFreq.y + time) * uAmp.y,\n 0\n );\n const lookAtAmp = new THREE.Vector3(1, 1, 0);\n const lookAtOffset = new THREE.Vector3(0, 0, -5);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n turbulentDistortion: {\n uniforms: turbulentUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n cos(PI * progress * uFreq.r + uTime) * uAmp.r +\n pow(cos(PI * progress * uFreq.g + uTime * (uFreq.g / uFreq.r)), 2. ) * uAmp.g\n );\n }\n float getDistortionY(float progress){\n return (\n -nsin(PI * progress * uFreq.b + uTime) * uAmp.b +\n -pow(nsin(PI * progress * uFreq.a + uTime / (uFreq.b / uFreq.a)), 5.) * uAmp.a\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.0125),\n getDistortionY(progress) - getDistortionY(0.0125),\n 0.\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const uFreq = turbulentUniforms.uFreq.value;\n const uAmp = turbulentUniforms.uAmp.value;\n\n const getX = (p: number) =>\n Math.cos(Math.PI * p * uFreq.x + time) * uAmp.x +\n Math.pow(Math.cos(Math.PI * p * uFreq.y + time * (uFreq.y / uFreq.x)), 2) * uAmp.y;\n\n const getY = (p: number) =>\n -nsin(Math.PI * p * uFreq.z + time) * uAmp.z -\n Math.pow(nsin(Math.PI * p * uFreq.w + time / (uFreq.z / uFreq.w)), 5) * uAmp.w;\n\n const distortion = new THREE.Vector3(\n getX(progress) - getX(progress + 0.007),\n getY(progress) - getY(progress + 0.007),\n 0\n );\n const lookAtAmp = new THREE.Vector3(-2, -5, 0);\n const lookAtOffset = new THREE.Vector3(0, 0, -10);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n turbulentDistortionStill: {\n uniforms: turbulentUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n cos(PI * progress * uFreq.r) * uAmp.r +\n pow(cos(PI * progress * uFreq.g * (uFreq.g / uFreq.r)), 2. ) * uAmp.g\n );\n }\n float getDistortionY(float progress){\n return (\n -nsin(PI * progress * uFreq.b) * uAmp.b +\n -pow(nsin(PI * progress * uFreq.a / (uFreq.b / uFreq.a)), 5.) * uAmp.a\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.02),\n 0.\n );\n }\n `\n },\n deepDistortionStill: {\n uniforms: deepUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n uniform vec2 uPowY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n sin(progress * PI * uFreq.x) * uAmp.x * 2.\n );\n }\n float getDistortionY(float progress){\n return (\n pow(abs(progress * uPowY.x), uPowY.y) + sin(progress * PI * uFreq.y) * uAmp.y\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.05),\n 0.\n );\n }\n `\n },\n deepDistortion: {\n uniforms: deepUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n uniform vec2 uPowY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n sin(progress * PI * uFreq.x + uTime) * uAmp.x\n );\n }\n float getDistortionY(float progress){\n return (\n pow(abs(progress * uPowY.x), uPowY.y) + sin(progress * PI * uFreq.y + uTime) * uAmp.y\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.02),\n 0.\n );\n }\n `,\n getJS: (progress: number, time: number) => {\n const uFreq = deepUniforms.uFreq.value;\n const uAmp = deepUniforms.uAmp.value;\n const uPowY = deepUniforms.uPowY.value;\n\n const getX = (p: number) => Math.sin(p * Math.PI * uFreq.x + time) * uAmp.x;\n const getY = (p: number) => Math.pow(p * uPowY.x, uPowY.y) + Math.sin(p * Math.PI * uFreq.y + time) * uAmp.y;\n\n const distortion = new THREE.Vector3(\n getX(progress) - getX(progress + 0.01),\n getY(progress) - getY(progress + 0.01),\n 0\n );\n const lookAtAmp = new THREE.Vector3(-2, -4, 0);\n const lookAtOffset = new THREE.Vector3(0, 0, -10);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n }\n};\n\nconst distortion_uniforms = {\n uDistortionX: { value: new THREE.Vector2(80, 3) },\n uDistortionY: { value: new THREE.Vector2(-40, 2.5) }\n};\n\nconst distortion_vertex = `\n #define PI 3.14159265358979\n uniform vec2 uDistortionX;\n uniform vec2 uDistortionY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n vec3 getDistortion(float progress){\n progress = clamp(progress, 0., 1.);\n float xAmp = uDistortionX.r;\n float xFreq = uDistortionX.g;\n float yAmp = uDistortionY.r;\n float yFreq = uDistortionY.g;\n return vec3( \n xAmp * nsin(progress * PI * xFreq - PI / 2.),\n yAmp * nsin(progress * PI * yFreq - PI / 2.),\n 0.\n );\n }\n`;\n\nfunction random(base: number | [number, number]): number {\n if (Array.isArray(base)) {\n return Math.random() * (base[1] - base[0]) + base[0];\n }\n return Math.random() * base;\n}\n\nfunction pickRandom(arr: T | T[]): T {\n if (Array.isArray(arr)) {\n return arr[Math.floor(Math.random() * arr.length)];\n }\n return arr;\n}\n\nfunction lerp(current: number, target: number, speed = 0.1, limit = 0.001): number {\n let change = (target - current) * speed;\n if (Math.abs(change) < limit) {\n change = target - current;\n }\n return change;\n}\n\nclass CarLights {\n webgl: App;\n options: HyperspeedOptions;\n colors: number[] | THREE.Color;\n speed: [number, number];\n fade: THREE.Vector2;\n mesh!: THREE.Mesh;\n\n constructor(\n webgl: App,\n options: HyperspeedOptions,\n colors: number[] | THREE.Color,\n speed: [number, number],\n fade: THREE.Vector2\n ) {\n this.webgl = webgl;\n this.options = options;\n this.colors = colors;\n this.speed = speed;\n this.fade = fade;\n }\n\n init() {\n const options = this.options;\n const curve = new THREE.LineCurve3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -1));\n const geometry = new THREE.TubeGeometry(curve, 40, 1, 8, false);\n\n const instanced = new THREE.InstancedBufferGeometry().copy(geometry as any) as THREE.InstancedBufferGeometry;\n instanced.instanceCount = options.lightPairsPerRoadWay * 2;\n\n const laneWidth = options.roadWidth / options.lanesPerRoad;\n\n const aOffset: number[] = [];\n const aMetrics: number[] = [];\n const aColor: number[] = [];\n\n let colorArray: THREE.Color[];\n if (Array.isArray(this.colors)) {\n colorArray = this.colors.map(c => new THREE.Color(c));\n } else {\n colorArray = [new THREE.Color(this.colors)];\n }\n\n for (let i = 0; i < options.lightPairsPerRoadWay; i++) {\n const radius = random(options.carLightsRadius);\n const length = random(options.carLightsLength);\n const spd = random(this.speed);\n\n const carLane = i % options.lanesPerRoad;\n let laneX = carLane * laneWidth - options.roadWidth / 2 + laneWidth / 2;\n\n const carWidth = random(options.carWidthPercentage) * laneWidth;\n const carShiftX = random(options.carShiftX) * laneWidth;\n laneX += carShiftX;\n\n const offsetY = random(options.carFloorSeparation) + radius * 1.3;\n const offsetZ = -random(options.length);\n\n aOffset.push(laneX - carWidth / 2);\n aOffset.push(offsetY);\n aOffset.push(offsetZ);\n\n aOffset.push(laneX + carWidth / 2);\n aOffset.push(offsetY);\n aOffset.push(offsetZ);\n\n aMetrics.push(radius);\n aMetrics.push(length);\n aMetrics.push(spd);\n\n aMetrics.push(radius);\n aMetrics.push(length);\n aMetrics.push(spd);\n\n const color = pickRandom(colorArray);\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n }\n\n instanced.setAttribute('aOffset', new THREE.InstancedBufferAttribute(new Float32Array(aOffset), 3, false));\n instanced.setAttribute('aMetrics', new THREE.InstancedBufferAttribute(new Float32Array(aMetrics), 3, false));\n instanced.setAttribute('aColor', new THREE.InstancedBufferAttribute(new Float32Array(aColor), 3, false));\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: carLightsFragment,\n vertexShader: carLightsVertex,\n transparent: true,\n uniforms: Object.assign(\n {\n uTime: { value: 0 },\n uTravelLength: { value: options.length },\n uFade: { value: this.fade }\n },\n this.webgl.fogUniforms,\n (typeof this.options.distortion === 'object' ? this.options.distortion.uniforms : {}) || {}\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n typeof this.options.distortion === 'object' ? this.options.distortion.getDistortion : ''\n );\n };\n\n const mesh = new THREE.Mesh(instanced, material);\n mesh.frustumCulled = false;\n this.webgl.scene.add(mesh);\n this.mesh = mesh;\n }\n\n update(time: number) {\n if (this.mesh.material.uniforms.uTime) {\n this.mesh.material.uniforms.uTime.value = time;\n }\n }\n}\n\nconst carLightsFragment = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_fragment']}\n varying vec3 vColor;\n varying vec2 vUv; \n uniform vec2 uFade;\n void main() {\n vec3 color = vec3(vColor);\n float alpha = smoothstep(uFade.x, uFade.y, vUv.x);\n gl_FragColor = vec4(color, alpha);\n if (gl_FragColor.a < 0.0001) discard;\n ${THREE.ShaderChunk['fog_fragment']}\n }\n`;\n\nconst carLightsVertex = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n attribute vec3 aOffset;\n attribute vec3 aMetrics;\n attribute vec3 aColor;\n uniform float uTravelLength;\n uniform float uTime;\n varying vec2 vUv; \n varying vec3 vColor; \n #include \n void main() {\n vec3 transformed = position.xyz;\n float radius = aMetrics.r;\n float myLength = aMetrics.g;\n float speed = aMetrics.b;\n\n transformed.xy *= radius;\n transformed.z *= myLength;\n\n transformed.z += myLength - mod(uTime * speed + aOffset.z, uTravelLength);\n transformed.xy += aOffset.xy;\n\n float progress = abs(transformed.z / uTravelLength);\n transformed.xyz += getDistortion(progress);\n\n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vUv = uv;\n vColor = aColor;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n`;\n\nclass LightsSticks {\n webgl: App;\n options: HyperspeedOptions;\n mesh!: THREE.Mesh;\n\n constructor(webgl: App, options: HyperspeedOptions) {\n this.webgl = webgl;\n this.options = options;\n }\n\n init() {\n const options = this.options;\n const geometry = new THREE.PlaneGeometry(1, 1);\n const instanced = new THREE.InstancedBufferGeometry().copy(geometry as any) as THREE.InstancedBufferGeometry;\n const totalSticks = options.totalSideLightSticks;\n instanced.instanceCount = totalSticks;\n\n const stickoffset = options.length / (totalSticks - 1);\n const aOffset: number[] = [];\n const aColor: number[] = [];\n const aMetrics: number[] = [];\n\n let colorArray: THREE.Color[];\n if (Array.isArray(options.colors.sticks)) {\n colorArray = options.colors.sticks.map(c => new THREE.Color(c));\n } else {\n colorArray = [new THREE.Color(options.colors.sticks)];\n }\n\n for (let i = 0; i < totalSticks; i++) {\n const width = random(options.lightStickWidth);\n const height = random(options.lightStickHeight);\n aOffset.push((i - 1) * stickoffset * 2 + stickoffset * Math.random());\n\n const color = pickRandom(colorArray);\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n\n aMetrics.push(width);\n aMetrics.push(height);\n }\n\n instanced.setAttribute('aOffset', new THREE.InstancedBufferAttribute(new Float32Array(aOffset), 1, false));\n instanced.setAttribute('aColor', new THREE.InstancedBufferAttribute(new Float32Array(aColor), 3, false));\n instanced.setAttribute('aMetrics', new THREE.InstancedBufferAttribute(new Float32Array(aMetrics), 2, false));\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: sideSticksFragment,\n vertexShader: sideSticksVertex,\n side: THREE.DoubleSide,\n uniforms: Object.assign(\n {\n uTravelLength: { value: options.length },\n uTime: { value: 0 }\n },\n this.webgl.fogUniforms,\n (typeof options.distortion === 'object' ? options.distortion.uniforms : {}) || {}\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n typeof this.options.distortion === 'object' ? this.options.distortion.getDistortion : ''\n );\n };\n\n const mesh = new THREE.Mesh(instanced, material);\n mesh.frustumCulled = false;\n this.webgl.scene.add(mesh);\n this.mesh = mesh;\n }\n\n update(time: number) {\n if (this.mesh.material.uniforms.uTime) {\n this.mesh.material.uniforms.uTime.value = time;\n }\n }\n}\n\nconst sideSticksVertex = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n attribute float aOffset;\n attribute vec3 aColor;\n attribute vec2 aMetrics;\n uniform float uTravelLength;\n uniform float uTime;\n varying vec3 vColor;\n mat4 rotationY( in float angle ) {\n return mat4(\n cos(angle),\t\t0,\t\tsin(angle),\t0,\n 0,\t\t 1.0,\t0,\t\t\t0,\n -sin(angle),\t 0,\t\tcos(angle),\t0,\n 0, \t\t 0,\t\t0,\t\t\t1\n );\n }\n #include \n void main(){\n vec3 transformed = position.xyz;\n float width = aMetrics.x;\n float height = aMetrics.y;\n\n transformed.xy *= vec2(width, height);\n float time = mod(uTime * 60. * 2. + aOffset, uTravelLength);\n\n transformed = (rotationY(3.14/2.) * vec4(transformed,1.)).xyz;\n transformed.z += - uTravelLength + time;\n\n float progress = abs(transformed.z / uTravelLength);\n transformed.xyz += getDistortion(progress);\n\n transformed.y += height / 2.;\n transformed.x += -width / 2.;\n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vColor = aColor;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n`;\n\nconst sideSticksFragment = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_fragment']}\n varying vec3 vColor;\n void main(){\n vec3 color = vec3(vColor);\n gl_FragColor = vec4(color,1.);\n ${THREE.ShaderChunk['fog_fragment']}\n }\n`;\n\nclass Road {\n webgl: App;\n options: HyperspeedOptions;\n uTime: { value: number };\n leftRoadWay!: THREE.Mesh;\n rightRoadWay!: THREE.Mesh;\n island!: THREE.Mesh;\n\n constructor(webgl: App, options: HyperspeedOptions) {\n this.webgl = webgl;\n this.options = options;\n this.uTime = { value: 0 };\n }\n\n createPlane(side: number, width: number, isRoad: boolean) {\n const options = this.options;\n const segments = 100;\n const geometry = new THREE.PlaneGeometry(\n isRoad ? options.roadWidth : options.islandWidth,\n options.length,\n 20,\n segments\n );\n\n let uniforms: Record = {\n uTravelLength: { value: options.length },\n uColor: {\n value: new THREE.Color(isRoad ? options.colors.roadColor : options.colors.islandColor)\n },\n uTime: this.uTime\n };\n\n if (isRoad) {\n uniforms = Object.assign(uniforms, {\n uLanes: { value: options.lanesPerRoad },\n uBrokenLinesColor: {\n value: new THREE.Color(options.colors.brokenLines)\n },\n uShoulderLinesColor: {\n value: new THREE.Color(options.colors.shoulderLines)\n },\n uShoulderLinesWidthPercentage: {\n value: options.shoulderLinesWidthPercentage\n },\n uBrokenLinesLengthPercentage: {\n value: options.brokenLinesLengthPercentage\n },\n uBrokenLinesWidthPercentage: {\n value: options.brokenLinesWidthPercentage\n }\n });\n }\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: isRoad ? roadFragment : islandFragment,\n vertexShader: roadVertex,\n side: THREE.DoubleSide,\n uniforms: Object.assign(\n uniforms,\n this.webgl.fogUniforms,\n (typeof options.distortion === 'object' ? options.distortion.uniforms : {}) || {}\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n typeof this.options.distortion === 'object' ? this.options.distortion.getDistortion : ''\n );\n };\n\n const mesh = new THREE.Mesh(geometry, material);\n mesh.rotation.x = -Math.PI / 2;\n mesh.position.z = -options.length / 2;\n mesh.position.x += (this.options.islandWidth / 2 + options.roadWidth / 2) * side;\n\n this.webgl.scene.add(mesh);\n return mesh;\n }\n\n init() {\n this.leftRoadWay = this.createPlane(-1, this.options.roadWidth, true);\n this.rightRoadWay = this.createPlane(1, this.options.roadWidth, true);\n this.island = this.createPlane(0, this.options.islandWidth, false);\n }\n\n update(time: number) {\n this.uTime.value = time;\n }\n}\n\nconst roadBaseFragment = `\n #define USE_FOG;\n varying vec2 vUv; \n uniform vec3 uColor;\n uniform float uTime;\n #include \n ${THREE.ShaderChunk['fog_pars_fragment']}\n void main() {\n vec2 uv = vUv;\n vec3 color = vec3(uColor);\n #include \n gl_FragColor = vec4(color, 1.);\n ${THREE.ShaderChunk['fog_fragment']}\n }\n`;\n\nconst islandFragment = roadBaseFragment\n .replace('#include ', '')\n .replace('#include ', '');\n\nconst roadMarkings_vars = `\n uniform float uLanes;\n uniform vec3 uBrokenLinesColor;\n uniform vec3 uShoulderLinesColor;\n uniform float uShoulderLinesWidthPercentage;\n uniform float uBrokenLinesWidthPercentage;\n uniform float uBrokenLinesLengthPercentage;\n highp float random(vec2 co) {\n highp float a = 12.9898;\n highp float b = 78.233;\n highp float c = 43758.5453;\n highp float dt = dot(co.xy, vec2(a, b));\n highp float sn = mod(dt, 3.14);\n return fract(sin(sn) * c);\n }\n`;\n\nconst roadMarkings_fragment = `\n uv.y = mod(uv.y + uTime * 0.05, 1.);\n float laneWidth = 1.0 / uLanes;\n float brokenLineWidth = laneWidth * uBrokenLinesWidthPercentage;\n float laneEmptySpace = 1. - uBrokenLinesLengthPercentage;\n\n float brokenLines = step(1.0 - brokenLineWidth, fract(uv.x * 2.0)) * step(laneEmptySpace, fract(uv.y * 10.0));\n float sideLines = step(1.0 - brokenLineWidth, fract((uv.x - laneWidth * (uLanes - 1.0)) * 2.0)) + step(brokenLineWidth, uv.x);\n\n brokenLines = mix(brokenLines, sideLines, uv.x);\n`;\n\nconst roadFragment = roadBaseFragment\n .replace('#include ', roadMarkings_fragment)\n .replace('#include ', roadMarkings_vars);\n\nconst roadVertex = `\n #define USE_FOG;\n uniform float uTime;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n uniform float uTravelLength;\n varying vec2 vUv; \n #include \n void main() {\n vec3 transformed = position.xyz;\n vec3 distortion = getDistortion((transformed.y + uTravelLength / 2.) / uTravelLength);\n transformed.x += distortion.x;\n transformed.z += distortion.y;\n transformed.y += -1. * distortion.z; \n \n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vUv = uv;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n`;\n\nfunction resizeRendererToDisplaySize(\n renderer: THREE.WebGLRenderer,\n setSize: (width: number, height: number, updateStyle: boolean) => void\n) {\n const canvas = renderer.domElement;\n const width = canvas.clientWidth;\n const height = canvas.clientHeight;\n if (width <= 0 || height <= 0) return false;\n const needResize = canvas.width !== width || canvas.height !== height;\n if (needResize) {\n setSize(width, height, false);\n }\n return needResize;\n}\n\nclass App {\n container: HTMLElement;\n options: HyperspeedOptions;\n renderer: THREE.WebGLRenderer;\n composer: EffectComposer;\n camera: THREE.PerspectiveCamera;\n scene: THREE.Scene;\n renderPass!: RenderPass;\n bloomPass!: EffectPass;\n clock: THREE.Clock;\n assets: Record;\n disposed: boolean;\n road: Road;\n leftCarLights: CarLights;\n rightCarLights: CarLights;\n leftSticks: LightsSticks;\n fogUniforms: Record;\n fovTarget: number;\n speedUpTarget: number;\n speedUp: number;\n timeOffset: number;\n hasValidSize: boolean;\n\n constructor(container: HTMLElement, options: HyperspeedOptions) {\n this.options = options;\n if (!this.options.distortion) {\n this.options.distortion = {\n uniforms: distortion_uniforms,\n getDistortion: distortion_vertex\n };\n }\n this.container = container;\n this.hasValidSize = false;\n\n const initW = Math.max(1, container.offsetWidth);\n const initH = Math.max(1, container.offsetHeight);\n\n this.renderer = new THREE.WebGLRenderer({\n antialias: false,\n alpha: true\n });\n this.renderer.setSize(initW, initH, false);\n this.renderer.setPixelRatio(window.devicePixelRatio);\n\n this.composer = new EffectComposer(this.renderer);\n container.appendChild(this.renderer.domElement);\n\n this.camera = new THREE.PerspectiveCamera(options.fov, initW / initH, 0.1, 10000);\n this.camera.position.z = -5;\n this.camera.position.y = 8;\n this.camera.position.x = 0;\n\n this.scene = new THREE.Scene();\n this.scene.background = null;\n\n const fog = new THREE.Fog(options.colors.background, options.length * 0.2, options.length * 500);\n this.scene.fog = fog;\n\n this.fogUniforms = {\n fogColor: { value: fog.color },\n fogNear: { value: fog.near },\n fogFar: { value: fog.far }\n };\n\n this.clock = new THREE.Clock();\n this.assets = {};\n this.disposed = false;\n\n this.road = new Road(this, options);\n this.leftCarLights = new CarLights(\n this,\n options,\n options.colors.leftCars,\n options.movingAwaySpeed,\n new THREE.Vector2(0, 1 - options.carLightsFade)\n );\n this.rightCarLights = new CarLights(\n this,\n options,\n options.colors.rightCars,\n options.movingCloserSpeed,\n new THREE.Vector2(1, 0 + options.carLightsFade)\n );\n this.leftSticks = new LightsSticks(this, options);\n\n this.fovTarget = options.fov;\n this.speedUpTarget = 0;\n this.speedUp = 0;\n this.timeOffset = 0;\n\n this.tick = this.tick.bind(this);\n this.init = this.init.bind(this);\n this.setSize = this.setSize.bind(this);\n this.onMouseDown = this.onMouseDown.bind(this);\n this.onMouseUp = this.onMouseUp.bind(this);\n\n this.onTouchStart = this.onTouchStart.bind(this);\n this.onTouchEnd = this.onTouchEnd.bind(this);\n this.onContextMenu = this.onContextMenu.bind(this);\n\n this.onWindowResize = this.onWindowResize.bind(this);\n window.addEventListener('resize', this.onWindowResize);\n\n if (container.offsetWidth > 0 && container.offsetHeight > 0) {\n this.hasValidSize = true;\n }\n }\n\n onWindowResize() {\n const width = this.container.offsetWidth;\n const height = this.container.offsetHeight;\n\n if (width <= 0 || height <= 0) {\n this.hasValidSize = false;\n return;\n }\n\n this.renderer.setSize(width, height);\n this.camera.aspect = width / height;\n this.camera.updateProjectionMatrix();\n this.composer.setSize(width, height);\n this.hasValidSize = true;\n }\n\n initPasses() {\n this.renderPass = new RenderPass(this.scene, this.camera);\n this.bloomPass = new EffectPass(\n this.camera,\n new BloomEffect({\n luminanceThreshold: 0.2,\n luminanceSmoothing: 0,\n resolutionScale: 1\n })\n );\n\n const smaaPass = new EffectPass(\n this.camera,\n new SMAAEffect({\n preset: SMAAPreset.MEDIUM\n })\n );\n this.renderPass.renderToScreen = false;\n this.bloomPass.renderToScreen = false;\n smaaPass.renderToScreen = true;\n\n this.composer.addPass(this.renderPass);\n this.composer.addPass(this.bloomPass);\n this.composer.addPass(smaaPass);\n }\n\n loadAssets(): Promise {\n const assets = this.assets;\n return new Promise(resolve => {\n const manager = new THREE.LoadingManager(resolve);\n\n const searchImage = new Image();\n const areaImage = new Image();\n assets.smaa = {};\n\n searchImage.addEventListener('load', function () {\n assets.smaa.search = this;\n manager.itemEnd('smaa-search');\n });\n\n areaImage.addEventListener('load', function () {\n assets.smaa.area = this;\n manager.itemEnd('smaa-area');\n });\n\n manager.itemStart('smaa-search');\n manager.itemStart('smaa-area');\n\n searchImage.src = SMAAEffect.searchImageDataURL;\n areaImage.src = SMAAEffect.areaImageDataURL;\n });\n }\n\n init() {\n this.initPasses();\n const options = this.options;\n this.road.init();\n this.leftCarLights.init();\n this.leftCarLights.mesh.position.setX(-options.roadWidth / 2 - options.islandWidth / 2);\n\n this.rightCarLights.init();\n this.rightCarLights.mesh.position.setX(options.roadWidth / 2 + options.islandWidth / 2);\n\n this.leftSticks.init();\n this.leftSticks.mesh.position.setX(-(options.roadWidth + options.islandWidth / 2));\n\n this.container.addEventListener('mousedown', this.onMouseDown);\n this.container.addEventListener('mouseup', this.onMouseUp);\n this.container.addEventListener('mouseout', this.onMouseUp);\n\n this.container.addEventListener('touchstart', this.onTouchStart, { passive: true });\n this.container.addEventListener('touchend', this.onTouchEnd, { passive: true });\n this.container.addEventListener('touchcancel', this.onTouchEnd, { passive: true });\n this.container.addEventListener('contextmenu', this.onContextMenu);\n\n this.tick();\n }\n\n onMouseDown(ev: MouseEvent) {\n if (this.options.onSpeedUp) this.options.onSpeedUp(ev);\n this.fovTarget = this.options.fovSpeedUp;\n this.speedUpTarget = this.options.speedUp;\n }\n\n onMouseUp(ev: MouseEvent) {\n if (this.options.onSlowDown) this.options.onSlowDown(ev);\n this.fovTarget = this.options.fov;\n this.speedUpTarget = 0;\n }\n\n onTouchStart(ev: TouchEvent) {\n if (this.options.onSpeedUp) this.options.onSpeedUp(ev);\n this.fovTarget = this.options.fovSpeedUp;\n this.speedUpTarget = this.options.speedUp;\n }\n\n onTouchEnd(ev: TouchEvent) {\n if (this.options.onSlowDown) this.options.onSlowDown(ev);\n this.fovTarget = this.options.fov;\n this.speedUpTarget = 0;\n }\n\n onContextMenu(ev: MouseEvent) {\n ev.preventDefault();\n }\n\n update(delta: number) {\n const lerpPercentage = Math.exp(-(-60 * Math.log2(1 - 0.1)) * delta);\n this.speedUp += lerp(this.speedUp, this.speedUpTarget, lerpPercentage, 0.00001);\n this.timeOffset += this.speedUp * delta;\n const time = this.clock.elapsedTime + this.timeOffset;\n\n this.rightCarLights.update(time);\n this.leftCarLights.update(time);\n this.leftSticks.update(time);\n this.road.update(time);\n\n let updateCamera = false;\n const fovChange = lerp(this.camera.fov, this.fovTarget, lerpPercentage);\n if (fovChange !== 0) {\n this.camera.fov += fovChange * delta * 6;\n updateCamera = true;\n }\n\n if (typeof this.options.distortion === 'object' && this.options.distortion.getJS) {\n const distortion = this.options.distortion.getJS(0.025, time);\n this.camera.lookAt(\n new THREE.Vector3(\n this.camera.position.x + distortion.x,\n this.camera.position.y + distortion.y,\n this.camera.position.z + distortion.z\n )\n );\n updateCamera = true;\n }\n\n if (updateCamera) {\n this.camera.updateProjectionMatrix();\n }\n }\n\n render(delta: number) {\n this.composer.render(delta);\n }\n\n dispose() {\n this.disposed = true;\n\n if (this.scene) {\n this.scene.traverse(object => {\n const obj = object as unknown as THREE.Mesh;\n if (!obj.isMesh) return;\n\n if (obj.geometry) obj.geometry.dispose();\n\n if (obj.material) {\n if (Array.isArray(obj.material)) {\n obj.material.forEach(material => material.dispose());\n } else {\n obj.material.dispose();\n }\n }\n });\n this.scene.clear();\n }\n\n if (this.renderer) {\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n if (this.renderer.domElement && this.renderer.domElement.parentNode) {\n this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);\n }\n }\n if (this.composer) {\n this.composer.dispose();\n }\n\n window.removeEventListener('resize', this.onWindowResize);\n if (this.container) {\n this.container.removeEventListener('mousedown', this.onMouseDown);\n this.container.removeEventListener('mouseup', this.onMouseUp);\n this.container.removeEventListener('mouseout', this.onMouseUp);\n\n this.container.removeEventListener('touchstart', this.onTouchStart);\n this.container.removeEventListener('touchend', this.onTouchEnd);\n this.container.removeEventListener('touchcancel', this.onTouchEnd);\n this.container.removeEventListener('contextmenu', this.onContextMenu);\n }\n }\n\n setSize(width: number, height: number, updateStyles: boolean) {\n this.composer.setSize(width, height, updateStyles);\n }\n\n tick() {\n if (this.disposed) return;\n\n if (!this.hasValidSize) {\n const w = this.container.offsetWidth;\n const h = this.container.offsetHeight;\n if (w > 0 && h > 0) {\n this.renderer.setSize(w, h, false);\n this.camera.aspect = w / h;\n this.camera.updateProjectionMatrix();\n this.composer.setSize(w, h);\n this.hasValidSize = true;\n } else {\n requestAnimationFrame(this.tick);\n return;\n }\n }\n\n if (resizeRendererToDisplaySize(this.renderer, this.setSize)) {\n const canvas = this.renderer.domElement;\n if (this.hasValidSize) {\n this.camera.aspect = canvas.clientWidth / canvas.clientHeight;\n this.camera.updateProjectionMatrix();\n }\n }\n\n if (this.hasValidSize) {\n const delta = this.clock.getDelta();\n this.render(delta);\n this.update(delta);\n }\n\n requestAnimationFrame(this.tick);\n }\n}\n\nconst DEFAULT_EFFECT_OPTIONS: Partial = {};\n\nconst Hyperspeed: FC = ({ effectOptions = DEFAULT_EFFECT_OPTIONS }) => {\n const hyperspeed = useRef(null);\n const appRef = useRef(null);\n\n useEffect(() => {\n if (appRef.current) {\n appRef.current.dispose();\n appRef.current = null;\n const container = hyperspeed.current;\n if (container) {\n while (container.firstChild) {\n container.removeChild(container.firstChild);\n }\n }\n }\n\n const container = hyperspeed.current;\n if (!container) return;\n\n const options: HyperspeedOptions = {\n ...defaultOptions,\n ...effectOptions,\n colors: { ...defaultOptions.colors, ...effectOptions.colors }\n };\n if (typeof options.distortion === 'string') {\n options.distortion = distortions[options.distortion];\n }\n\n const myApp = new App(container, options);\n appRef.current = myApp;\n myApp.loadAssets().then(myApp.init);\n\n return () => {\n if (appRef.current) {\n appRef.current.dispose();\n }\n };\n }, [effectOptions]);\n\n return
;\n};\n\nexport default Hyperspeed;\n" + "path": "Hyperspeed/HyperSpeedPresets.ts", + "content": "export const hyperspeedPresets = {\n one: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 20,\n lightPairsPerRoadWay: 40,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.03, 400 * 0.2],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.8, 0.8],\n carFloorSeparation: [0, 5],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xd856bf, 0x6750a2, 0xc247ac],\n rightCars: [0x03b3c3, 0x0e5ea5, 0x324555],\n sticks: 0x03b3c3\n }\n },\n two: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'mountainDistortion',\n length: 400,\n roadWidth: 9,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 50,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xff102a, 0xeb383e, 0xff102a],\n rightCars: [0xdadafa, 0xbebae3, 0x8f97e4],\n sticks: 0xdadafa\n }\n },\n three: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'xyDistortion',\n length: 400,\n roadWidth: 9,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 3,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 30,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.02, 0.05],\n lightStickHeight: [0.3, 0.7],\n movingAwaySpeed: [20, 50],\n movingCloserSpeed: [-150, -230],\n carLightsLength: [400 * 0.05, 400 * 0.2],\n carLightsRadius: [0.03, 0.08],\n carWidthPercentage: [0.1, 0.5],\n carShiftX: [-0.5, 0.5],\n carFloorSeparation: [0, 0.1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0x7d0d1b, 0xa90519, 0xff102a],\n rightCars: [0xf1eece, 0xe6e2b1, 0xdfd98a],\n sticks: 0xf1eece\n }\n },\n four: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'LongRaceDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 5,\n lanesPerRoad: 2,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 70,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xff5f73, 0xe74d60, 0xff102a],\n rightCars: [0xa4e3e6, 0x80d1d4, 0x53c2c6],\n sticks: 0xa4e3e6\n }\n },\n five: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 9,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 50,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xdc5b20, 0xdca320, 0xdc2020],\n rightCars: [0x334bf7, 0xe5e6ed, 0xbfc6f3],\n sticks: 0xc5e8eb\n }\n },\n six: {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'deepDistortion',\n length: 400,\n roadWidth: 18,\n islandWidth: 2,\n lanesPerRoad: 3,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 50,\n lightPairsPerRoadWay: 50,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.05, 400 * 0.15],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.2, 0.2],\n carFloorSeparation: [0.05, 1],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0x131318,\n brokenLines: 0x131318,\n leftCars: [0xff322f, 0xa33010, 0xa81508],\n rightCars: [0xfdfdf0, 0xf3dea0, 0xe2bb88],\n sticks: 0xfdfdf0\n }\n }\n};\n" } ], "registryDependencies": [], diff --git a/public/r/ImageTrail-JS-CSS.json b/public/r/ImageTrail-JS-CSS.json index 6e4f3b15..4a4ccfb0 100644 --- a/public/r/ImageTrail-JS-CSS.json +++ b/public/r/ImageTrail-JS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "ImageTrail/ImageTrail.css", + "type": "registry:file", + "path": "ImageTrail.css", + "target": "@components/ImageTrail.css", "content": ".content {\n width: 100%;\n height: 100%;\n position: relative;\n z-index: 100;\n border-radius: 8px;\n background: transparent;\n overflow: visible;\n}\n\n.content__img {\n width: 190px;\n aspect-ratio: 1.1;\n border-radius: 15px;\n position: absolute;\n top: 0;\n left: 0;\n opacity: 0;\n overflow: hidden;\n will-change: transform, filter;\n}\n\n.content__img-inner {\n background-position: 50% 50%;\n width: calc(100% + 20px);\n height: calc(100% + 20px);\n background-size: cover;\n position: absolute;\n top: calc(-1 * 20px / 2);\n left: calc(-1 * 20px / 2);\n}\n" }, { "type": "registry:component", - "path": "ImageTrail/ImageTrail.jsx", - "content": "import { useRef, useEffect } from 'react';\nimport { gsap } from 'gsap';\n\nimport './ImageTrail.css';\n\nfunction lerp(a, b, n) {\n return (1 - n) * a + n * b;\n}\n\nfunction getLocalPointerPos(e, rect) {\n let clientX = 0,\n clientY = 0;\n if (e.touches && e.touches.length > 0) {\n clientX = e.touches[0].clientX;\n clientY = e.touches[0].clientY;\n } else {\n clientX = e.clientX;\n clientY = e.clientY;\n }\n return {\n x: clientX - rect.left,\n y: clientY - rect.top\n };\n}\nfunction getMouseDistance(p1, p2) {\n const dx = p1.x - p2.x;\n const dy = p1.y - p2.y;\n return Math.hypot(dx, dy);\n}\n\nclass ImageItem {\n DOM = { el: null, inner: null };\n defaultStyle = { scale: 1, x: 0, y: 0, opacity: 0 };\n rect = null;\n\n constructor(DOM_el) {\n this.DOM.el = DOM_el;\n this.DOM.inner = this.DOM.el.querySelector('.content__img-inner');\n this.getRect();\n this.initEvents();\n }\n initEvents() {\n this.resize = () => {\n gsap.set(this.DOM.el, this.defaultStyle);\n this.getRect();\n };\n window.addEventListener('resize', this.resize);\n }\n getRect() {\n this.rect = this.DOM.el.getBoundingClientRect();\n }\n}\n\nclass ImageTrailVariant1 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...this.DOM.el.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n requestAnimationFrame(() => this.render());\n\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3',\n opacity: 0,\n scale: 0.2\n },\n 0.4\n );\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant2 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n requestAnimationFrame(() => this.render());\n\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2.8,\n filter: 'brightness(250%)'\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n filter: 'brightness(100%)'\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power2',\n opacity: 0,\n scale: 0.2\n },\n 0.45\n );\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant3 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n xPercent: 0,\n yPercent: 0,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 1.2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.6,\n ease: 'power2',\n opacity: 0,\n scale: 0.2,\n xPercent: () => gsap.utils.random(-30, 30),\n yPercent: -200\n },\n 0.6\n );\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant4 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let distance = Math.sqrt(dx * dx + dy * dy);\n if (distance !== 0) {\n dx /= distance;\n dy /= distance;\n }\n dx *= distance / 100;\n dy *= distance / 100;\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2,\n filter: `brightness(${Math.max((400 * distance) / 100, 100)}%) contrast(${Math.max((400 * distance) / 100, 100)}%)`\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n filter: 'brightness(100%) contrast(100%)'\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3',\n opacity: 0\n },\n 0.4\n )\n .to(\n img.DOM.el,\n {\n duration: 1.5,\n ease: 'power4',\n x: `+=${dx * 110}`,\n y: `+=${dy * 110}`\n },\n 0.05\n );\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant5 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n this.lastAngle = 0;\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let angle = Math.atan2(dy, dx) * (180 / Math.PI);\n if (angle < 0) angle += 360;\n if (angle > 90 && angle <= 270) angle += 180;\n const isMovingClockwise = angle >= this.lastAngle;\n this.lastAngle = angle;\n let startAngle = isMovingClockwise ? angle - 10 : angle + 10;\n let distance = Math.sqrt(dx * dx + dy * dy);\n if (distance !== 0) {\n dx /= distance;\n dy /= distance;\n }\n dx *= distance / 150;\n dy *= distance / 150;\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n filter: 'brightness(80%)',\n scale: 0.1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2,\n rotation: startAngle\n },\n {\n duration: 1,\n ease: 'power2',\n scale: 1,\n filter: 'brightness(100%)',\n x: this.mousePos.x - img.rect.width / 2 + dx * 70,\n y: this.mousePos.y - img.rect.height / 2 + dy * 70,\n rotation: this.lastAngle\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'expo',\n opacity: 0\n },\n 0.5\n )\n .to(\n img.DOM.el,\n {\n duration: 1.5,\n ease: 'power4',\n x: `+=${dx * 120}`,\n y: `+=${dy * 120}`\n },\n 0.05\n );\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant6 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n mapSpeedToSize(speed, minSize, maxSize) {\n const maxSpeed = 200;\n return minSize + (maxSize - minSize) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToBrightness(speed, minB, maxB) {\n const maxSpeed = 70;\n return minB + (maxB - minB) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToBlur(speed, minBlur, maxBlur) {\n const maxSpeed = 90;\n return minBlur + (maxBlur - minBlur) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToGrayscale(speed, minG, maxG) {\n const maxSpeed = 90;\n return minG + (maxG - minG) * Math.min(speed / maxSpeed, 1);\n }\n\n showNextImage() {\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let speed = Math.sqrt(dx * dx + dy * dy);\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n let scaleFactor = this.mapSpeedToSize(speed, 0.3, 2);\n let brightnessValue = this.mapSpeedToBrightness(speed, 0, 1.3);\n let blurValue = this.mapSpeedToBlur(speed, 20, 0);\n let grayscaleValue = this.mapSpeedToGrayscale(speed, 600, 0);\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.8,\n ease: 'power3',\n scale: scaleFactor,\n filter: `grayscale(${grayscaleValue * 100}%) brightness(${brightnessValue * 100}%) blur(${blurValue}px)`,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2\n },\n {\n duration: 0.8,\n ease: 'power3',\n scale: 1\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3.in',\n opacity: 0,\n scale: 0.2\n },\n 0.45\n );\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nfunction getNewPosition(position, offset, arr) {\n const realOffset = Math.abs(offset) % arr.length;\n if (position - realOffset >= 0) {\n return position - realOffset;\n } else {\n return arr.length - (realOffset - position);\n }\n}\nclass ImageTrailVariant7 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n this.visibleImagesCount = 0;\n this.visibleImagesTotal = 9;\n this.visibleImagesTotal = Math.min(this.visibleImagesTotal, this.imagesTotal - 1);\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n\n requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n ++this.visibleImagesCount;\n\n gsap.killTweensOf(img.DOM.el);\n const scaleValue = gsap.utils.random(0.5, 1.6);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n scale: scaleValue - Math.max(gsap.utils.random(0.2, 0.6), 0),\n rotationZ: 0,\n opacity: 1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power3',\n scale: scaleValue,\n rotationZ: gsap.utils.random(-3, 3),\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n );\n\n if (this.visibleImagesCount >= this.visibleImagesTotal) {\n const lastInQueue = getNewPosition(this.imgPosition, this.visibleImagesTotal, this.images);\n const oldImg = this.images[lastInQueue];\n gsap.to(oldImg.DOM.el, {\n duration: 0.4,\n ease: 'power4',\n opacity: 0,\n scale: 1.3,\n onComplete: () => {\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n });\n }\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n }\n}\n\nclass ImageTrailVariant8 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n this.rotation = { x: 0, y: 0 };\n this.cachedRotation = { x: 0, y: 0 };\n this.zValue = 0;\n this.cachedZValue = 0;\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n const rect = this.container.getBoundingClientRect();\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const relX = this.mousePos.x - centerX;\n const relY = this.mousePos.y - centerY;\n\n this.rotation.x = -(relY / centerY) * 30;\n this.rotation.y = (relX / centerX) * 30;\n this.cachedRotation = { ...this.rotation };\n\n const distanceFromCenter = Math.sqrt(relX * relX + relY * relY);\n const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);\n const proportion = distanceFromCenter / maxDistance;\n this.zValue = proportion * 1200 - 600;\n this.cachedZValue = this.zValue;\n const normalizedZ = (this.zValue + 600) / 1200;\n const brightness = 0.2 + normalizedZ * 2.3;\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .set(this.DOM.el, { perspective: 1000 }, 0)\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n z: 0,\n scale: 1 + this.cachedZValue / 1000,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2,\n rotationX: this.cachedRotation.x,\n rotationY: this.cachedRotation.y,\n filter: `brightness(${brightness})`\n },\n {\n duration: 1,\n ease: 'expo',\n scale: 1 + this.zValue / 1000,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2,\n rotationX: this.rotation.x,\n rotationY: this.rotation.y\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power2',\n opacity: 0,\n z: -800\n },\n 0.3\n );\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nconst variantMap = {\n 1: ImageTrailVariant1,\n 2: ImageTrailVariant2,\n 3: ImageTrailVariant3,\n 4: ImageTrailVariant4,\n 5: ImageTrailVariant5,\n 6: ImageTrailVariant6,\n 7: ImageTrailVariant7,\n 8: ImageTrailVariant8\n};\n\nexport default function ImageTrail({ items = [], variant = 1 }) {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const Cls = variantMap[variant] || variantMap[1];\n new Cls(containerRef.current);\n }, [variant, items]);\n\n return (\n
\n {items.map((url, i) => (\n
\n
\n
\n ))}\n
\n );\n}\n" + "path": "ImageTrail.jsx", + "content": "import { useRef, useEffect } from 'react';\nimport { gsap } from 'gsap';\n\nimport './ImageTrail.css';\n\nfunction lerp(a, b, n) {\n return (1 - n) * a + n * b;\n}\n\nfunction getLocalPointerPos(e, rect) {\n let clientX = 0,\n clientY = 0;\n if (e.touches && e.touches.length > 0) {\n clientX = e.touches[0].clientX;\n clientY = e.touches[0].clientY;\n } else {\n clientX = e.clientX;\n clientY = e.clientY;\n }\n return {\n x: clientX - rect.left,\n y: clientY - rect.top\n };\n}\nfunction getMouseDistance(p1, p2) {\n const dx = p1.x - p2.x;\n const dy = p1.y - p2.y;\n return Math.hypot(dx, dy);\n}\n\nclass ImageItem {\n DOM = { el: null, inner: null };\n defaultStyle = { scale: 1, x: 0, y: 0, opacity: 0 };\n rect = null;\n\n constructor(DOM_el) {\n this.DOM.el = DOM_el;\n this.DOM.inner = this.DOM.el.querySelector('.content__img-inner');\n this.getRect();\n this.initEvents();\n }\n initEvents() {\n this.resize = () => {\n gsap.set(this.DOM.el, this.defaultStyle);\n this.getRect();\n };\n window.addEventListener('resize', this.resize);\n }\n getRect() {\n this.rect = this.DOM.el.getBoundingClientRect();\n }\n\n destroy() {\n window.removeEventListener('resize', this.resize);\n }\n}\n\nclass ImageTrailVariant1 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...this.DOM.el.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n this.rafId = requestAnimationFrame(() => this.render());\n\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3',\n opacity: 0,\n scale: 0.2\n },\n 0.4\n );\n }\n\n destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant2 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n this.rafId = requestAnimationFrame(() => this.render());\n\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2.8,\n filter: 'brightness(250%)'\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n filter: 'brightness(100%)'\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power2',\n opacity: 0,\n scale: 0.2\n },\n 0.45\n );\n }\n\n destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant3 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n xPercent: 0,\n yPercent: 0,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 1.2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.6,\n ease: 'power2',\n opacity: 0,\n scale: 0.2,\n xPercent: () => gsap.utils.random(-30, 30),\n yPercent: -200\n },\n 0.6\n );\n }\n\n destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant4 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let distance = Math.sqrt(dx * dx + dy * dy);\n if (distance !== 0) {\n dx /= distance;\n dy /= distance;\n }\n dx *= distance / 100;\n dy *= distance / 100;\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2,\n filter: `brightness(${Math.max((400 * distance) / 100, 100)}%) contrast(${Math.max((400 * distance) / 100, 100)}%)`\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n filter: 'brightness(100%) contrast(100%)'\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3',\n opacity: 0\n },\n 0.4\n )\n .to(\n img.DOM.el,\n {\n duration: 1.5,\n ease: 'power4',\n x: `+=${dx * 110}`,\n y: `+=${dy * 110}`\n },\n 0.05\n );\n }\n\n destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant5 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n this.lastAngle = 0;\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let angle = Math.atan2(dy, dx) * (180 / Math.PI);\n if (angle < 0) angle += 360;\n if (angle > 90 && angle <= 270) angle += 180;\n const isMovingClockwise = angle >= this.lastAngle;\n this.lastAngle = angle;\n let startAngle = isMovingClockwise ? angle - 10 : angle + 10;\n let distance = Math.sqrt(dx * dx + dy * dy);\n if (distance !== 0) {\n dx /= distance;\n dy /= distance;\n }\n dx *= distance / 150;\n dy *= distance / 150;\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n filter: 'brightness(80%)',\n scale: 0.1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2,\n rotation: startAngle\n },\n {\n duration: 1,\n ease: 'power2',\n scale: 1,\n filter: 'brightness(100%)',\n x: this.mousePos.x - img.rect.width / 2 + dx * 70,\n y: this.mousePos.y - img.rect.height / 2 + dy * 70,\n rotation: this.lastAngle\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'expo',\n opacity: 0\n },\n 0.5\n )\n .to(\n img.DOM.el,\n {\n duration: 1.5,\n ease: 'power4',\n x: `+=${dx * 120}`,\n y: `+=${dy * 120}`\n },\n 0.05\n );\n }\n\n destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant6 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n mapSpeedToSize(speed, minSize, maxSize) {\n const maxSpeed = 200;\n return minSize + (maxSize - minSize) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToBrightness(speed, minB, maxB) {\n const maxSpeed = 70;\n return minB + (maxB - minB) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToBlur(speed, minBlur, maxBlur) {\n const maxSpeed = 90;\n return minBlur + (maxBlur - minBlur) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToGrayscale(speed, minG, maxG) {\n const maxSpeed = 90;\n return minG + (maxG - minG) * Math.min(speed / maxSpeed, 1);\n }\n\n showNextImage() {\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let speed = Math.sqrt(dx * dx + dy * dy);\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n let scaleFactor = this.mapSpeedToSize(speed, 0.3, 2);\n let brightnessValue = this.mapSpeedToBrightness(speed, 0, 1.3);\n let blurValue = this.mapSpeedToBlur(speed, 20, 0);\n let grayscaleValue = this.mapSpeedToGrayscale(speed, 600, 0);\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.8,\n ease: 'power3',\n scale: scaleFactor,\n filter: `grayscale(${grayscaleValue * 100}%) brightness(${brightnessValue * 100}%) blur(${blurValue}px)`,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2\n },\n {\n duration: 0.8,\n ease: 'power3',\n scale: 1\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3.in',\n opacity: 0,\n scale: 0.2\n },\n 0.45\n );\n }\n\n destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nfunction getNewPosition(position, offset, arr) {\n const realOffset = Math.abs(offset) % arr.length;\n if (position - realOffset >= 0) {\n return position - realOffset;\n } else {\n return arr.length - (realOffset - position);\n }\n}\nclass ImageTrailVariant7 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n this.visibleImagesCount = 0;\n this.visibleImagesTotal = 9;\n this.visibleImagesTotal = Math.min(this.visibleImagesTotal, this.imagesTotal - 1);\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n ++this.visibleImagesCount;\n\n gsap.killTweensOf(img.DOM.el);\n const scaleValue = gsap.utils.random(0.5, 1.6);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n scale: scaleValue - Math.max(gsap.utils.random(0.2, 0.6), 0),\n rotationZ: 0,\n opacity: 1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power3',\n scale: scaleValue,\n rotationZ: gsap.utils.random(-3, 3),\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n );\n\n if (this.visibleImagesCount >= this.visibleImagesTotal) {\n const lastInQueue = getNewPosition(this.imgPosition, this.visibleImagesTotal, this.images);\n const oldImg = this.images[lastInQueue];\n gsap.to(oldImg.DOM.el, {\n duration: 0.4,\n ease: 'power4',\n opacity: 0,\n scale: 1.3,\n onComplete: () => {\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n });\n }\n }\n\n destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n }\n}\n\nclass ImageTrailVariant8 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n this.rotation = { x: 0, y: 0 };\n this.cachedRotation = { x: 0, y: 0 };\n this.zValue = 0;\n this.cachedZValue = 0;\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n const rect = this.container.getBoundingClientRect();\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const relX = this.mousePos.x - centerX;\n const relY = this.mousePos.y - centerY;\n\n this.rotation.x = -(relY / centerY) * 30;\n this.rotation.y = (relX / centerX) * 30;\n this.cachedRotation = { ...this.rotation };\n\n const distanceFromCenter = Math.sqrt(relX * relX + relY * relY);\n const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);\n const proportion = distanceFromCenter / maxDistance;\n this.zValue = proportion * 1200 - 600;\n this.cachedZValue = this.zValue;\n const normalizedZ = (this.zValue + 600) / 1200;\n const brightness = 0.2 + normalizedZ * 2.3;\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .set(this.DOM.el, { perspective: 1000 }, 0)\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n z: 0,\n scale: 1 + this.cachedZValue / 1000,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2,\n rotationX: this.cachedRotation.x,\n rotationY: this.cachedRotation.y,\n filter: `brightness(${brightness})`\n },\n {\n duration: 1,\n ease: 'expo',\n scale: 1 + this.zValue / 1000,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2,\n rotationX: this.rotation.x,\n rotationY: this.rotation.y\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power2',\n opacity: 0,\n z: -800\n },\n 0.3\n );\n }\n\n destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nconst variantMap = {\n 1: ImageTrailVariant1,\n 2: ImageTrailVariant2,\n 3: ImageTrailVariant3,\n 4: ImageTrailVariant4,\n 5: ImageTrailVariant5,\n 6: ImageTrailVariant6,\n 7: ImageTrailVariant7,\n 8: ImageTrailVariant8\n};\n\nexport default function ImageTrail({ items = [], variant = 1 }) {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const Cls = variantMap[variant] || variantMap[1];\n const instance = new Cls(containerRef.current);\n\n return () => {\n instance.destroy();\n };\n }, [variant, items]);\n\n return (\n
\n {items.map((url, i) => (\n
\n
\n
\n ))}\n
\n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/ImageTrail-JS-TW.json b/public/r/ImageTrail-JS-TW.json index 94c95d5b..d1298662 100644 --- a/public/r/ImageTrail-JS-TW.json +++ b/public/r/ImageTrail-JS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "ImageTrail/ImageTrail.jsx", - "content": "import { useRef, useEffect } from 'react';\nimport { gsap } from 'gsap';\n\nfunction lerp(a, b, n) {\n return (1 - n) * a + n * b;\n}\n\nfunction getLocalPointerPos(e, rect) {\n let clientX = 0,\n clientY = 0;\n if (e.touches && e.touches.length > 0) {\n clientX = e.touches[0].clientX;\n clientY = e.touches[0].clientY;\n } else {\n clientX = e.clientX;\n clientY = e.clientY;\n }\n return {\n x: clientX - rect.left,\n y: clientY - rect.top\n };\n}\nfunction getMouseDistance(p1, p2) {\n const dx = p1.x - p2.x;\n const dy = p1.y - p2.y;\n return Math.hypot(dx, dy);\n}\n\nclass ImageItem {\n DOM = { el: null, inner: null };\n defaultStyle = { scale: 1, x: 0, y: 0, opacity: 0 };\n rect = null;\n\n constructor(DOM_el) {\n this.DOM.el = DOM_el;\n this.DOM.inner = this.DOM.el.querySelector('.content__img-inner');\n this.getRect();\n this.initEvents();\n }\n initEvents() {\n this.resize = () => {\n gsap.set(this.DOM.el, this.defaultStyle);\n this.getRect();\n };\n window.addEventListener('resize', this.resize);\n }\n getRect() {\n this.rect = this.DOM.el.getBoundingClientRect();\n }\n}\n\nclass ImageTrailVariant1 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...this.DOM.el.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n requestAnimationFrame(() => this.render());\n\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3',\n opacity: 0,\n scale: 0.2\n },\n 0.4\n );\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant2 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n requestAnimationFrame(() => this.render());\n\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2.8,\n filter: 'brightness(250%)'\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n filter: 'brightness(100%)'\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power2',\n opacity: 0,\n scale: 0.2\n },\n 0.45\n );\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant3 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n xPercent: 0,\n yPercent: 0,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 1.2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.6,\n ease: 'power2',\n opacity: 0,\n scale: 0.2,\n xPercent: () => gsap.utils.random(-30, 30),\n yPercent: -200\n },\n 0.6\n );\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant4 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let distance = Math.sqrt(dx * dx + dy * dy);\n if (distance !== 0) {\n dx /= distance;\n dy /= distance;\n }\n dx *= distance / 100;\n dy *= distance / 100;\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2,\n filter: `brightness(${Math.max((400 * distance) / 100, 100)}%) contrast(${Math.max((400 * distance) / 100, 100)}%)`\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n filter: 'brightness(100%) contrast(100%)'\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3',\n opacity: 0\n },\n 0.4\n )\n .to(\n img.DOM.el,\n {\n duration: 1.5,\n ease: 'power4',\n x: `+=${dx * 110}`,\n y: `+=${dy * 110}`\n },\n 0.05\n );\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant5 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n this.lastAngle = 0;\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let angle = Math.atan2(dy, dx) * (180 / Math.PI);\n if (angle < 0) angle += 360;\n if (angle > 90 && angle <= 270) angle += 180;\n const isMovingClockwise = angle >= this.lastAngle;\n this.lastAngle = angle;\n let startAngle = isMovingClockwise ? angle - 10 : angle + 10;\n let distance = Math.sqrt(dx * dx + dy * dy);\n if (distance !== 0) {\n dx /= distance;\n dy /= distance;\n }\n dx *= distance / 150;\n dy *= distance / 150;\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n filter: 'brightness(80%)',\n scale: 0.1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2,\n rotation: startAngle\n },\n {\n duration: 1,\n ease: 'power2',\n scale: 1,\n filter: 'brightness(100%)',\n x: this.mousePos.x - img.rect.width / 2 + dx * 70,\n y: this.mousePos.y - img.rect.height / 2 + dy * 70,\n rotation: this.lastAngle\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'expo',\n opacity: 0\n },\n 0.5\n )\n .to(\n img.DOM.el,\n {\n duration: 1.5,\n ease: 'power4',\n x: `+=${dx * 120}`,\n y: `+=${dy * 120}`\n },\n 0.05\n );\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant6 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n mapSpeedToSize(speed, minSize, maxSize) {\n const maxSpeed = 200;\n return minSize + (maxSize - minSize) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToBrightness(speed, minB, maxB) {\n const maxSpeed = 70;\n return minB + (maxB - minB) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToBlur(speed, minBlur, maxBlur) {\n const maxSpeed = 90;\n return minBlur + (maxBlur - minBlur) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToGrayscale(speed, minG, maxG) {\n const maxSpeed = 90;\n return minG + (maxG - minG) * Math.min(speed / maxSpeed, 1);\n }\n\n showNextImage() {\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let speed = Math.sqrt(dx * dx + dy * dy);\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n let scaleFactor = this.mapSpeedToSize(speed, 0.3, 2);\n let brightnessValue = this.mapSpeedToBrightness(speed, 0, 1.3);\n let blurValue = this.mapSpeedToBlur(speed, 20, 0);\n let grayscaleValue = this.mapSpeedToGrayscale(speed, 600, 0);\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.8,\n ease: 'power3',\n scale: scaleFactor,\n filter: `grayscale(${grayscaleValue * 100}%) brightness(${brightnessValue * 100}%) blur(${blurValue}px)`,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2\n },\n {\n duration: 0.8,\n ease: 'power3',\n scale: 1\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3.in',\n opacity: 0,\n scale: 0.2\n },\n 0.45\n );\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nfunction getNewPosition(position, offset, arr) {\n const realOffset = Math.abs(offset) % arr.length;\n if (position - realOffset >= 0) {\n return position - realOffset;\n } else {\n return arr.length - (realOffset - position);\n }\n}\nclass ImageTrailVariant7 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n this.visibleImagesCount = 0;\n this.visibleImagesTotal = 9;\n this.visibleImagesTotal = Math.min(this.visibleImagesTotal, this.imagesTotal - 1);\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n\n requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n ++this.visibleImagesCount;\n\n gsap.killTweensOf(img.DOM.el);\n const scaleValue = gsap.utils.random(0.5, 1.6);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n scale: scaleValue - Math.max(gsap.utils.random(0.2, 0.6), 0),\n rotationZ: 0,\n opacity: 1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power3',\n scale: scaleValue,\n rotationZ: gsap.utils.random(-3, 3),\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n );\n\n if (this.visibleImagesCount >= this.visibleImagesTotal) {\n const lastInQueue = getNewPosition(this.imgPosition, this.visibleImagesTotal, this.images);\n const oldImg = this.images[lastInQueue];\n gsap.to(oldImg.DOM.el, {\n duration: 0.4,\n ease: 'power4',\n opacity: 0,\n scale: 1.3,\n onComplete: () => {\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n });\n }\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n }\n}\n\nclass ImageTrailVariant8 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n this.rotation = { x: 0, y: 0 };\n this.cachedRotation = { x: 0, y: 0 };\n this.zValue = 0;\n this.cachedZValue = 0;\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n const rect = this.container.getBoundingClientRect();\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const relX = this.mousePos.x - centerX;\n const relY = this.mousePos.y - centerY;\n\n this.rotation.x = -(relY / centerY) * 30;\n this.rotation.y = (relX / centerX) * 30;\n this.cachedRotation = { ...this.rotation };\n\n const distanceFromCenter = Math.sqrt(relX * relX + relY * relY);\n const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);\n const proportion = distanceFromCenter / maxDistance;\n this.zValue = proportion * 1200 - 600;\n this.cachedZValue = this.zValue;\n const normalizedZ = (this.zValue + 600) / 1200;\n const brightness = 0.2 + normalizedZ * 2.3;\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .set(this.DOM.el, { perspective: 1000 }, 0)\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n z: 0,\n scale: 1 + this.cachedZValue / 1000,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2,\n rotationX: this.cachedRotation.x,\n rotationY: this.cachedRotation.y,\n filter: `brightness(${brightness})`\n },\n {\n duration: 1,\n ease: 'expo',\n scale: 1 + this.zValue / 1000,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2,\n rotationX: this.rotation.x,\n rotationY: this.rotation.y\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power2',\n opacity: 0,\n z: -800\n },\n 0.3\n );\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nconst variantMap = {\n 1: ImageTrailVariant1,\n 2: ImageTrailVariant2,\n 3: ImageTrailVariant3,\n 4: ImageTrailVariant4,\n 5: ImageTrailVariant5,\n 6: ImageTrailVariant6,\n 7: ImageTrailVariant7,\n 8: ImageTrailVariant8\n};\n\nexport default function ImageTrail({ items = [], variant = 1 }) {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const Cls = variantMap[variant] || variantMap[1];\n new Cls(containerRef.current);\n }, [variant, items]);\n\n return (\n
\n {items.map((url, i) => (\n \n \n
\n ))}\n
\n );\n}\n" + "content": "import { useRef, useEffect } from 'react';\nimport { gsap } from 'gsap';\n\nfunction lerp(a, b, n) {\n return (1 - n) * a + n * b;\n}\n\nfunction getLocalPointerPos(e, rect) {\n let clientX = 0,\n clientY = 0;\n if (e.touches && e.touches.length > 0) {\n clientX = e.touches[0].clientX;\n clientY = e.touches[0].clientY;\n } else {\n clientX = e.clientX;\n clientY = e.clientY;\n }\n return {\n x: clientX - rect.left,\n y: clientY - rect.top\n };\n}\nfunction getMouseDistance(p1, p2) {\n const dx = p1.x - p2.x;\n const dy = p1.y - p2.y;\n return Math.hypot(dx, dy);\n}\n\nclass ImageItem {\n DOM = { el: null, inner: null };\n defaultStyle = { scale: 1, x: 0, y: 0, opacity: 0 };\n rect = null;\n\n constructor(DOM_el) {\n this.DOM.el = DOM_el;\n this.DOM.inner = this.DOM.el.querySelector('.content__img-inner');\n this.getRect();\n this.initEvents();\n }\n initEvents() {\n this.resize = () => {\n gsap.set(this.DOM.el, this.defaultStyle);\n this.getRect();\n };\n window.addEventListener('resize', this.resize);\n }\n getRect() {\n this.rect = this.DOM.el.getBoundingClientRect();\n }\n\n destroy() {\n window.removeEventListener('resize', this.resize);\n }\n}\n\nclass ImageTrailVariant1 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...this.DOM.el.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n this.rafId = requestAnimationFrame(() => this.render());\n\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3',\n opacity: 0,\n scale: 0.2\n },\n 0.4\n );\n }\n\n destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant2 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n this.rafId = requestAnimationFrame(() => this.render());\n\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2.8,\n filter: 'brightness(250%)'\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n filter: 'brightness(100%)'\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power2',\n opacity: 0,\n scale: 0.2\n },\n 0.45\n );\n }\n\n destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant3 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n xPercent: 0,\n yPercent: 0,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 1.2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.6,\n ease: 'power2',\n opacity: 0,\n scale: 0.2,\n xPercent: () => gsap.utils.random(-30, 30),\n yPercent: -200\n },\n 0.6\n );\n }\n\n destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant4 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let distance = Math.sqrt(dx * dx + dy * dy);\n if (distance !== 0) {\n dx /= distance;\n dy /= distance;\n }\n dx *= distance / 100;\n dy *= distance / 100;\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2,\n filter: `brightness(${Math.max((400 * distance) / 100, 100)}%) contrast(${Math.max((400 * distance) / 100, 100)}%)`\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n filter: 'brightness(100%) contrast(100%)'\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3',\n opacity: 0\n },\n 0.4\n )\n .to(\n img.DOM.el,\n {\n duration: 1.5,\n ease: 'power4',\n x: `+=${dx * 110}`,\n y: `+=${dy * 110}`\n },\n 0.05\n );\n }\n\n destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant5 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n this.lastAngle = 0;\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let angle = Math.atan2(dy, dx) * (180 / Math.PI);\n if (angle < 0) angle += 360;\n if (angle > 90 && angle <= 270) angle += 180;\n const isMovingClockwise = angle >= this.lastAngle;\n this.lastAngle = angle;\n let startAngle = isMovingClockwise ? angle - 10 : angle + 10;\n let distance = Math.sqrt(dx * dx + dy * dy);\n if (distance !== 0) {\n dx /= distance;\n dy /= distance;\n }\n dx *= distance / 150;\n dy *= distance / 150;\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n filter: 'brightness(80%)',\n scale: 0.1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2,\n rotation: startAngle\n },\n {\n duration: 1,\n ease: 'power2',\n scale: 1,\n filter: 'brightness(100%)',\n x: this.mousePos.x - img.rect.width / 2 + dx * 70,\n y: this.mousePos.y - img.rect.height / 2 + dy * 70,\n rotation: this.lastAngle\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'expo',\n opacity: 0\n },\n 0.5\n )\n .to(\n img.DOM.el,\n {\n duration: 1.5,\n ease: 'power4',\n x: `+=${dx * 120}`,\n y: `+=${dy * 120}`\n },\n 0.05\n );\n }\n\n destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant6 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n mapSpeedToSize(speed, minSize, maxSize) {\n const maxSpeed = 200;\n return minSize + (maxSize - minSize) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToBrightness(speed, minB, maxB) {\n const maxSpeed = 70;\n return minB + (maxB - minB) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToBlur(speed, minBlur, maxBlur) {\n const maxSpeed = 90;\n return minBlur + (maxBlur - minBlur) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToGrayscale(speed, minG, maxG) {\n const maxSpeed = 90;\n return minG + (maxG - minG) * Math.min(speed / maxSpeed, 1);\n }\n\n showNextImage() {\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let speed = Math.sqrt(dx * dx + dy * dy);\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n let scaleFactor = this.mapSpeedToSize(speed, 0.3, 2);\n let brightnessValue = this.mapSpeedToBrightness(speed, 0, 1.3);\n let blurValue = this.mapSpeedToBlur(speed, 20, 0);\n let grayscaleValue = this.mapSpeedToGrayscale(speed, 600, 0);\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.8,\n ease: 'power3',\n scale: scaleFactor,\n filter: `grayscale(${grayscaleValue * 100}%) brightness(${brightnessValue * 100}%) blur(${blurValue}px)`,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2\n },\n {\n duration: 0.8,\n ease: 'power3',\n scale: 1\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3.in',\n opacity: 0,\n scale: 0.2\n },\n 0.45\n );\n }\n\n destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nfunction getNewPosition(position, offset, arr) {\n const realOffset = Math.abs(offset) % arr.length;\n if (position - realOffset >= 0) {\n return position - realOffset;\n } else {\n return arr.length - (realOffset - position);\n }\n}\nclass ImageTrailVariant7 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n this.visibleImagesCount = 0;\n this.visibleImagesTotal = 9;\n this.visibleImagesTotal = Math.min(this.visibleImagesTotal, this.imagesTotal - 1);\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n ++this.visibleImagesCount;\n\n gsap.killTweensOf(img.DOM.el);\n const scaleValue = gsap.utils.random(0.5, 1.6);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n scale: scaleValue - Math.max(gsap.utils.random(0.2, 0.6), 0),\n rotationZ: 0,\n opacity: 1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power3',\n scale: scaleValue,\n rotationZ: gsap.utils.random(-3, 3),\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n );\n\n if (this.visibleImagesCount >= this.visibleImagesTotal) {\n const lastInQueue = getNewPosition(this.imgPosition, this.visibleImagesTotal, this.images);\n const oldImg = this.images[lastInQueue];\n gsap.to(oldImg.DOM.el, {\n duration: 0.4,\n ease: 'power4',\n opacity: 0,\n scale: 1.3,\n onComplete: () => {\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n });\n }\n }\n\n destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n }\n}\n\nclass ImageTrailVariant8 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n this.rotation = { x: 0, y: 0 };\n this.cachedRotation = { x: 0, y: 0 };\n this.zValue = 0;\n this.cachedZValue = 0;\n\n const handlePointerMove = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = ev => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n showNextImage() {\n const rect = this.container.getBoundingClientRect();\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const relX = this.mousePos.x - centerX;\n const relY = this.mousePos.y - centerY;\n\n this.rotation.x = -(relY / centerY) * 30;\n this.rotation.y = (relX / centerX) * 30;\n this.cachedRotation = { ...this.rotation };\n\n const distanceFromCenter = Math.sqrt(relX * relX + relY * relY);\n const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);\n const proportion = distanceFromCenter / maxDistance;\n this.zValue = proportion * 1200 - 600;\n this.cachedZValue = this.zValue;\n const normalizedZ = (this.zValue + 600) / 1200;\n const brightness = 0.2 + normalizedZ * 2.3;\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .set(this.DOM.el, { perspective: 1000 }, 0)\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n z: 0,\n scale: 1 + this.cachedZValue / 1000,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - img.rect.width / 2,\n y: this.cacheMousePos.y - img.rect.height / 2,\n rotationX: this.cachedRotation.x,\n rotationY: this.cachedRotation.y,\n filter: `brightness(${brightness})`\n },\n {\n duration: 1,\n ease: 'expo',\n scale: 1 + this.zValue / 1000,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2,\n rotationX: this.rotation.x,\n rotationY: this.rotation.y\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power2',\n opacity: 0,\n z: -800\n },\n 0.3\n );\n }\n\n destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nconst variantMap = {\n 1: ImageTrailVariant1,\n 2: ImageTrailVariant2,\n 3: ImageTrailVariant3,\n 4: ImageTrailVariant4,\n 5: ImageTrailVariant5,\n 6: ImageTrailVariant6,\n 7: ImageTrailVariant7,\n 8: ImageTrailVariant8\n};\n\nexport default function ImageTrail({ items = [], variant = 1 }) {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const Cls = variantMap[variant] || variantMap[1];\n const instance = new Cls(containerRef.current);\n\n return () => {\n instance.destroy();\n };\n }, [variant, items]);\n\n return (\n
\n {items.map((url, i) => (\n \n \n
\n ))}\n
\n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/ImageTrail-TS-CSS.json b/public/r/ImageTrail-TS-CSS.json index b6125e55..c3ae906e 100644 --- a/public/r/ImageTrail-TS-CSS.json +++ b/public/r/ImageTrail-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "ImageTrail/ImageTrail.css", + "type": "registry:file", + "path": "ImageTrail.css", + "target": "@components/ImageTrail.css", "content": ".content {\n width: 100%;\n height: 100%;\n position: relative;\n z-index: 100;\n border-radius: 8px;\n background: transparent;\n overflow: visible;\n}\n\n.content__img {\n width: 190px;\n aspect-ratio: 1.1;\n border-radius: 15px;\n position: absolute;\n top: 0;\n left: 0;\n opacity: 0;\n overflow: hidden;\n will-change: transform, filter;\n}\n\n.content__img-inner {\n background-position: 50% 50%;\n width: calc(100% + 20px);\n height: calc(100% + 20px);\n background-size: cover;\n position: absolute;\n top: calc(-1 * 20px / 2);\n left: calc(-1 * 20px / 2);\n}\n" }, { "type": "registry:component", - "path": "ImageTrail/ImageTrail.tsx", - "content": "import { gsap } from 'gsap';\nimport { JSX, useEffect, useRef } from 'react';\nimport './ImageTrail.css';\n\nfunction lerp(a: number, b: number, n: number): number {\n return (1 - n) * a + n * b;\n}\n\nfunction getLocalPointerPos(e: MouseEvent | TouchEvent, rect: DOMRect): { x: number; y: number } {\n let clientX = 0,\n clientY = 0;\n if ('touches' in e && e.touches.length > 0) {\n clientX = e.touches[0].clientX;\n clientY = e.touches[0].clientY;\n } else if ('clientX' in e) {\n clientX = e.clientX;\n clientY = e.clientY;\n }\n return {\n x: clientX - rect.left,\n y: clientY - rect.top\n };\n}\n\nfunction getMouseDistance(p1: { x: number; y: number }, p2: { x: number; y: number }): number {\n const dx = p1.x - p2.x;\n const dy = p1.y - p2.y;\n return Math.hypot(dx, dy);\n}\n\nclass ImageItem {\n public DOM: { el: HTMLDivElement; inner: HTMLDivElement | null } = {\n el: null as unknown as HTMLDivElement,\n inner: null\n };\n public defaultStyle: gsap.TweenVars = { scale: 1, x: 0, y: 0, opacity: 0 };\n public rect: DOMRect | null = null;\n private resize!: () => void;\n\n constructor(DOM_el: HTMLDivElement) {\n this.DOM.el = DOM_el;\n this.DOM.inner = this.DOM.el.querySelector('.content__img-inner');\n this.getRect();\n this.initEvents();\n }\n\n private initEvents() {\n this.resize = () => {\n gsap.set(this.DOM.el, this.defaultStyle);\n this.getRect();\n };\n window.addEventListener('resize', this.resize);\n }\n\n private getRect() {\n this.rect = this.DOM.el.getBoundingClientRect();\n }\n}\n\nclass ImageTrailVariant1 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3',\n opacity: 0,\n scale: 0.2\n },\n 0.4\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant2 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n { scale: 2.8, filter: 'brightness(250%)' },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n filter: 'brightness(100%)'\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power2',\n opacity: 0,\n scale: 0.2\n },\n 0.45\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant3 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n xPercent: 0,\n yPercent: 0,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n { scale: 1.2 },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.6,\n ease: 'power2',\n opacity: 0,\n scale: 0.2,\n xPercent: () => gsap.utils.random(-30, 30),\n yPercent: -200\n },\n 0.6\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant4 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let distance = Math.sqrt(dx * dx + dy * dy);\n if (distance !== 0) {\n dx /= distance;\n dy /= distance;\n }\n dx *= distance / 100;\n dy *= distance / 100;\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2,\n filter: `brightness(${Math.max((400 * distance) / 100, 100)}%) contrast(${Math.max(\n (400 * distance) / 100,\n 100\n )}%)`\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n filter: 'brightness(100%) contrast(100%)'\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3',\n opacity: 0\n },\n 0.4\n )\n .to(\n img.DOM.el,\n {\n duration: 1.5,\n ease: 'power4',\n x: `+=${dx * 110}`,\n y: `+=${dy * 110}`\n },\n 0.05\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant5 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private lastAngle: number;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n this.lastAngle = 0;\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let angle = Math.atan2(dy, dx) * (180 / Math.PI);\n if (angle < 0) angle += 360;\n if (angle > 90 && angle <= 270) angle += 180;\n const isMovingClockwise = angle >= this.lastAngle;\n this.lastAngle = angle;\n let startAngle = isMovingClockwise ? angle - 10 : angle + 10;\n const distance = Math.sqrt(dx * dx + dy * dy);\n if (distance !== 0) {\n dx /= distance;\n dy /= distance;\n }\n dx *= distance / 150;\n dy *= distance / 150;\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n filter: 'brightness(80%)',\n scale: 0.1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,\n rotation: startAngle\n },\n {\n duration: 1,\n ease: 'power2',\n scale: 1,\n filter: 'brightness(100%)',\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2 + dx * 70,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2 + dy * 70,\n rotation: this.lastAngle\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'expo',\n opacity: 0\n },\n 0.5\n )\n .to(\n img.DOM.el,\n {\n duration: 1.5,\n ease: 'power4',\n x: `+=${dx * 120}`,\n y: `+=${dy * 120}`\n },\n 0.05\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant6 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n private mapSpeedToSize(speed: number, minSize: number, maxSize: number) {\n const maxSpeed = 200;\n return minSize + (maxSize - minSize) * Math.min(speed / maxSpeed, 1);\n }\n\n private mapSpeedToBrightness(speed: number, minB: number, maxB: number) {\n const maxSpeed = 70;\n return minB + (maxB - minB) * Math.min(speed / maxSpeed, 1);\n }\n\n private mapSpeedToBlur(speed: number, minBlur: number, maxBlur: number) {\n const maxSpeed = 90;\n return minBlur + (maxBlur - minBlur) * Math.min(speed / maxSpeed, 1);\n }\n\n private mapSpeedToGrayscale(speed: number, minG: number, maxG: number) {\n const maxSpeed = 90;\n return minG + (maxG - minG) * Math.min(speed / maxSpeed, 1);\n }\n\n private showNextImage() {\n const dx = this.mousePos.x - this.cacheMousePos.x;\n const dy = this.mousePos.y - this.cacheMousePos.y;\n const speed = Math.sqrt(dx * dx + dy * dy);\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n const scaleFactor = this.mapSpeedToSize(speed, 0.3, 2);\n const brightnessValue = this.mapSpeedToBrightness(speed, 0, 1.3);\n const blurValue = this.mapSpeedToBlur(speed, 20, 0);\n const grayscaleValue = this.mapSpeedToGrayscale(speed, 600, 0);\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.8,\n ease: 'power3',\n scale: scaleFactor,\n filter: `grayscale(${grayscaleValue * 100}%) brightness(${brightnessValue * 100}%) blur(${blurValue}px)`,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n { scale: 2 },\n {\n duration: 0.8,\n ease: 'power3',\n scale: 1\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3.in',\n opacity: 0,\n scale: 0.2\n },\n 0.45\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nfunction getNewPosition(position: number, offset: number, arr: ImageItem[]) {\n const realOffset = Math.abs(offset) % arr.length;\n if (position - realOffset >= 0) {\n return position - realOffset;\n } else {\n return arr.length - (realOffset - position);\n }\n}\n\nclass ImageTrailVariant7 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private visibleImagesCount: number;\n private visibleImagesTotal: number;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n this.visibleImagesCount = 0;\n this.visibleImagesTotal = 9;\n this.visibleImagesTotal = Math.min(this.visibleImagesTotal, this.imagesTotal - 1);\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n ++this.visibleImagesCount;\n\n gsap.killTweensOf(img.DOM.el);\n const scaleValue = gsap.utils.random(0.5, 1.6);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n scale: scaleValue - Math.max(gsap.utils.random(0.2, 0.6), 0),\n rotationZ: 0,\n opacity: 1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power3',\n scale: scaleValue,\n rotationZ: gsap.utils.random(-3, 3),\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n );\n\n if (this.visibleImagesCount >= this.visibleImagesTotal) {\n const lastInQueue = getNewPosition(this.imgPosition, this.visibleImagesTotal, this.images);\n const oldImg = this.images[lastInQueue];\n gsap.to(oldImg.DOM.el, {\n duration: 0.4,\n ease: 'power4',\n opacity: 0,\n scale: 1.3,\n onComplete: () => {\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n });\n }\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n }\n}\n\nclass ImageTrailVariant8 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private rotation: { x: number; y: number };\n private cachedRotation: { x: number; y: number };\n private zValue: number;\n private cachedZValue: number;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n this.rotation = { x: 0, y: 0 };\n this.cachedRotation = { x: 0, y: 0 };\n this.zValue = 0;\n this.cachedZValue = 0;\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n const rect = this.container.getBoundingClientRect();\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const relX = this.mousePos.x - centerX;\n const relY = this.mousePos.y - centerY;\n\n this.rotation.x = -(relY / centerY) * 30;\n this.rotation.y = (relX / centerX) * 30;\n this.cachedRotation = { ...this.rotation };\n\n const distanceFromCenter = Math.sqrt(relX * relX + relY * relY);\n const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);\n const proportion = distanceFromCenter / maxDistance;\n this.zValue = proportion * 1200 - 600;\n this.cachedZValue = this.zValue;\n const normalizedZ = (this.zValue + 600) / 1200;\n const brightness = 0.2 + normalizedZ * 2.3;\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .set(this.DOM.el, { perspective: 1000 }, 0)\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n z: 0,\n scale: 1 + this.cachedZValue / 1000,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,\n rotationX: this.cachedRotation.x,\n rotationY: this.cachedRotation.y,\n filter: `brightness(${brightness})`\n },\n {\n duration: 1,\n ease: 'expo',\n scale: 1 + this.zValue / 1000,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2,\n rotationX: this.rotation.x,\n rotationY: this.rotation.y\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power2',\n opacity: 0,\n z: -800\n },\n 0.3\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\ntype ImageTrailConstructor =\n | typeof ImageTrailVariant1\n | typeof ImageTrailVariant2\n | typeof ImageTrailVariant3\n | typeof ImageTrailVariant4\n | typeof ImageTrailVariant5\n | typeof ImageTrailVariant6\n | typeof ImageTrailVariant7\n | typeof ImageTrailVariant8;\n\nconst variantMap: Record = {\n 1: ImageTrailVariant1,\n 2: ImageTrailVariant2,\n 3: ImageTrailVariant3,\n 4: ImageTrailVariant4,\n 5: ImageTrailVariant5,\n 6: ImageTrailVariant6,\n 7: ImageTrailVariant7,\n 8: ImageTrailVariant8\n};\n\ninterface ImageTrailProps {\n items?: string[];\n variant?: number;\n}\n\nexport default function ImageTrail({ items = [], variant = 1 }: ImageTrailProps): JSX.Element {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n const Cls = variantMap[variant] || variantMap[1];\n new Cls(containerRef.current);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [variant, items]);\n\n return (\n
\n {items.map((url, i) => (\n
\n
\n
\n ))}\n
\n );\n}\n" + "path": "ImageTrail.tsx", + "content": "import { gsap } from 'gsap';\nimport { type JSX, useEffect, useRef } from 'react';\nimport './ImageTrail.css';\n\nfunction lerp(a: number, b: number, n: number): number {\n return (1 - n) * a + n * b;\n}\n\nfunction getLocalPointerPos(e: MouseEvent | TouchEvent, rect: DOMRect): { x: number; y: number } {\n let clientX = 0,\n clientY = 0;\n if ('touches' in e && e.touches.length > 0) {\n clientX = e.touches[0].clientX;\n clientY = e.touches[0].clientY;\n } else if ('clientX' in e) {\n clientX = e.clientX;\n clientY = e.clientY;\n }\n return {\n x: clientX - rect.left,\n y: clientY - rect.top\n };\n}\n\nfunction getMouseDistance(p1: { x: number; y: number }, p2: { x: number; y: number }): number {\n const dx = p1.x - p2.x;\n const dy = p1.y - p2.y;\n return Math.hypot(dx, dy);\n}\n\nclass ImageItem {\n public DOM: { el: HTMLDivElement; inner: HTMLDivElement | null } = {\n el: null as unknown as HTMLDivElement,\n inner: null\n };\n public defaultStyle: gsap.TweenVars = { scale: 1, x: 0, y: 0, opacity: 0 };\n public rect: DOMRect | null = null;\n private resize!: () => void;\n\n constructor(DOM_el: HTMLDivElement) {\n this.DOM.el = DOM_el;\n this.DOM.inner = this.DOM.el.querySelector('.content__img-inner');\n this.getRect();\n this.initEvents();\n }\n\n private initEvents() {\n this.resize = () => {\n gsap.set(this.DOM.el, this.defaultStyle);\n this.getRect();\n };\n window.addEventListener('resize', this.resize);\n }\n\n private getRect() {\n this.rect = this.DOM.el.getBoundingClientRect();\n }\n\n public destroy() {\n window.removeEventListener('resize', this.resize);\n }\n}\n\nclass ImageTrailVariant1 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3',\n opacity: 0,\n scale: 0.2\n },\n 0.4\n );\n }\n\n public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant2 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n { scale: 2.8, filter: 'brightness(250%)' },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n filter: 'brightness(100%)'\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power2',\n opacity: 0,\n scale: 0.2\n },\n 0.45\n );\n }\n\n public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant3 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n xPercent: 0,\n yPercent: 0,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n { scale: 1.2 },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.6,\n ease: 'power2',\n opacity: 0,\n scale: 0.2,\n xPercent: () => gsap.utils.random(-30, 30),\n yPercent: -200\n },\n 0.6\n );\n }\n\n public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant4 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let distance = Math.sqrt(dx * dx + dy * dy);\n if (distance !== 0) {\n dx /= distance;\n dy /= distance;\n }\n dx *= distance / 100;\n dy *= distance / 100;\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2,\n filter: `brightness(${Math.max((400 * distance) / 100, 100)}%) contrast(${Math.max(\n (400 * distance) / 100,\n 100\n )}%)`\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n filter: 'brightness(100%) contrast(100%)'\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3',\n opacity: 0\n },\n 0.4\n )\n .to(\n img.DOM.el,\n {\n duration: 1.5,\n ease: 'power4',\n x: `+=${dx * 110}`,\n y: `+=${dy * 110}`\n },\n 0.05\n );\n }\n\n public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant5 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\n private lastAngle: number;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n this.lastAngle = 0;\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let angle = Math.atan2(dy, dx) * (180 / Math.PI);\n if (angle < 0) angle += 360;\n if (angle > 90 && angle <= 270) angle += 180;\n const isMovingClockwise = angle >= this.lastAngle;\n this.lastAngle = angle;\n let startAngle = isMovingClockwise ? angle - 10 : angle + 10;\n const distance = Math.sqrt(dx * dx + dy * dy);\n if (distance !== 0) {\n dx /= distance;\n dy /= distance;\n }\n dx *= distance / 150;\n dy *= distance / 150;\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n filter: 'brightness(80%)',\n scale: 0.1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,\n rotation: startAngle\n },\n {\n duration: 1,\n ease: 'power2',\n scale: 1,\n filter: 'brightness(100%)',\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2 + dx * 70,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2 + dy * 70,\n rotation: this.lastAngle\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'expo',\n opacity: 0\n },\n 0.5\n )\n .to(\n img.DOM.el,\n {\n duration: 1.5,\n ease: 'power4',\n x: `+=${dx * 120}`,\n y: `+=${dy * 120}`\n },\n 0.05\n );\n }\n\n public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant6 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n private mapSpeedToSize(speed: number, minSize: number, maxSize: number) {\n const maxSpeed = 200;\n return minSize + (maxSize - minSize) * Math.min(speed / maxSpeed, 1);\n }\n\n private mapSpeedToBrightness(speed: number, minB: number, maxB: number) {\n const maxSpeed = 70;\n return minB + (maxB - minB) * Math.min(speed / maxSpeed, 1);\n }\n\n private mapSpeedToBlur(speed: number, minBlur: number, maxBlur: number) {\n const maxSpeed = 90;\n return minBlur + (maxBlur - minBlur) * Math.min(speed / maxSpeed, 1);\n }\n\n private mapSpeedToGrayscale(speed: number, minG: number, maxG: number) {\n const maxSpeed = 90;\n return minG + (maxG - minG) * Math.min(speed / maxSpeed, 1);\n }\n\n private showNextImage() {\n const dx = this.mousePos.x - this.cacheMousePos.x;\n const dy = this.mousePos.y - this.cacheMousePos.y;\n const speed = Math.sqrt(dx * dx + dy * dy);\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n const scaleFactor = this.mapSpeedToSize(speed, 0.3, 2);\n const brightnessValue = this.mapSpeedToBrightness(speed, 0, 1.3);\n const blurValue = this.mapSpeedToBlur(speed, 20, 0);\n const grayscaleValue = this.mapSpeedToGrayscale(speed, 600, 0);\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.8,\n ease: 'power3',\n scale: scaleFactor,\n filter: `grayscale(${grayscaleValue * 100}%) brightness(${brightnessValue * 100}%) blur(${blurValue}px)`,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n { scale: 2 },\n {\n duration: 0.8,\n ease: 'power3',\n scale: 1\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3.in',\n opacity: 0,\n scale: 0.2\n },\n 0.45\n );\n }\n\n public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nfunction getNewPosition(position: number, offset: number, arr: ImageItem[]) {\n const realOffset = Math.abs(offset) % arr.length;\n if (position - realOffset >= 0) {\n return position - realOffset;\n } else {\n return arr.length - (realOffset - position);\n }\n}\n\nclass ImageTrailVariant7 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\n private visibleImagesCount: number;\n private visibleImagesTotal: number;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n this.visibleImagesCount = 0;\n this.visibleImagesTotal = 9;\n this.visibleImagesTotal = Math.min(this.visibleImagesTotal, this.imagesTotal - 1);\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n ++this.visibleImagesCount;\n\n gsap.killTweensOf(img.DOM.el);\n const scaleValue = gsap.utils.random(0.5, 1.6);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n scale: scaleValue - Math.max(gsap.utils.random(0.2, 0.6), 0),\n rotationZ: 0,\n opacity: 1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power3',\n scale: scaleValue,\n rotationZ: gsap.utils.random(-3, 3),\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n );\n\n if (this.visibleImagesCount >= this.visibleImagesTotal) {\n const lastInQueue = getNewPosition(this.imgPosition, this.visibleImagesTotal, this.images);\n const oldImg = this.images[lastInQueue];\n gsap.to(oldImg.DOM.el, {\n duration: 0.4,\n ease: 'power4',\n opacity: 0,\n scale: 1.3,\n onComplete: () => {\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n });\n }\n }\n\n public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n }\n}\n\nclass ImageTrailVariant8 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\n private rotation: { x: number; y: number };\n private cachedRotation: { x: number; y: number };\n private zValue: number;\n private cachedZValue: number;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n this.rotation = { x: 0, y: 0 };\n this.cachedRotation = { x: 0, y: 0 };\n this.zValue = 0;\n this.cachedZValue = 0;\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n const rect = this.container.getBoundingClientRect();\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const relX = this.mousePos.x - centerX;\n const relY = this.mousePos.y - centerY;\n\n this.rotation.x = -(relY / centerY) * 30;\n this.rotation.y = (relX / centerX) * 30;\n this.cachedRotation = { ...this.rotation };\n\n const distanceFromCenter = Math.sqrt(relX * relX + relY * relY);\n const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);\n const proportion = distanceFromCenter / maxDistance;\n this.zValue = proportion * 1200 - 600;\n this.cachedZValue = this.zValue;\n const normalizedZ = (this.zValue + 600) / 1200;\n const brightness = 0.2 + normalizedZ * 2.3;\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .set(this.DOM.el, { perspective: 1000 }, 0)\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n z: 0,\n scale: 1 + this.cachedZValue / 1000,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,\n rotationX: this.cachedRotation.x,\n rotationY: this.cachedRotation.y,\n filter: `brightness(${brightness})`\n },\n {\n duration: 1,\n ease: 'expo',\n scale: 1 + this.zValue / 1000,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2,\n rotationX: this.rotation.x,\n rotationY: this.rotation.y\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power2',\n opacity: 0,\n z: -800\n },\n 0.3\n );\n }\n\n public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\ntype ImageTrailConstructor =\n | typeof ImageTrailVariant1\n | typeof ImageTrailVariant2\n | typeof ImageTrailVariant3\n | typeof ImageTrailVariant4\n | typeof ImageTrailVariant5\n | typeof ImageTrailVariant6\n | typeof ImageTrailVariant7\n | typeof ImageTrailVariant8;\n\nconst variantMap: Record = {\n 1: ImageTrailVariant1,\n 2: ImageTrailVariant2,\n 3: ImageTrailVariant3,\n 4: ImageTrailVariant4,\n 5: ImageTrailVariant5,\n 6: ImageTrailVariant6,\n 7: ImageTrailVariant7,\n 8: ImageTrailVariant8\n};\n\ninterface ImageTrailProps {\n items?: string[];\n variant?: number;\n}\n\nexport default function ImageTrail({ items = [], variant = 1 }: ImageTrailProps): JSX.Element {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n const Cls = variantMap[variant] || variantMap[1];\n const instance = new Cls(containerRef.current);\n\n return () => {\n instance.destroy();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [variant, items]);\n\n return (\n
\n {items.map((url, i) => (\n
\n
\n
\n ))}\n
\n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/ImageTrail-TS-TW.json b/public/r/ImageTrail-TS-TW.json index fe00260a..2035044d 100644 --- a/public/r/ImageTrail-TS-TW.json +++ b/public/r/ImageTrail-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "ImageTrail/ImageTrail.tsx", - "content": "import { gsap } from 'gsap';\nimport { JSX, useEffect, useRef } from 'react';\n\nfunction lerp(a: number, b: number, n: number): number {\n return (1 - n) * a + n * b;\n}\n\nfunction getLocalPointerPos(e: MouseEvent | TouchEvent, rect: DOMRect): { x: number; y: number } {\n let clientX = 0,\n clientY = 0;\n if ('touches' in e && e.touches.length > 0) {\n clientX = e.touches[0].clientX;\n clientY = e.touches[0].clientY;\n } else if ('clientX' in e) {\n clientX = e.clientX;\n clientY = e.clientY;\n }\n return {\n x: clientX - rect.left,\n y: clientY - rect.top\n };\n}\n\nfunction getMouseDistance(p1: { x: number; y: number }, p2: { x: number; y: number }): number {\n const dx = p1.x - p2.x;\n const dy = p1.y - p2.y;\n return Math.hypot(dx, dy);\n}\n\nclass ImageItem {\n public DOM: { el: HTMLDivElement; inner: HTMLDivElement | null } = {\n el: null as unknown as HTMLDivElement,\n inner: null\n };\n public defaultStyle: gsap.TweenVars = { scale: 1, x: 0, y: 0, opacity: 0 };\n public rect: DOMRect | null = null;\n private resize!: () => void;\n\n constructor(DOM_el: HTMLDivElement) {\n this.DOM.el = DOM_el;\n this.DOM.inner = this.DOM.el.querySelector('.content__img-inner');\n this.getRect();\n this.initEvents();\n }\n\n private initEvents() {\n this.resize = () => {\n gsap.set(this.DOM.el, this.defaultStyle);\n this.getRect();\n };\n window.addEventListener('resize', this.resize);\n }\n\n private getRect() {\n this.rect = this.DOM.el.getBoundingClientRect();\n }\n}\n\nclass ImageTrailVariant1 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3',\n opacity: 0,\n scale: 0.2\n },\n 0.4\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant2 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n { scale: 2.8, filter: 'brightness(250%)' },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n filter: 'brightness(100%)'\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power2',\n opacity: 0,\n scale: 0.2\n },\n 0.45\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant3 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n xPercent: 0,\n yPercent: 0,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n { scale: 1.2 },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.6,\n ease: 'power2',\n opacity: 0,\n scale: 0.2,\n xPercent: () => gsap.utils.random(-30, 30),\n yPercent: -200\n },\n 0.6\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant4 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let distance = Math.sqrt(dx * dx + dy * dy);\n if (distance !== 0) {\n dx /= distance;\n dy /= distance;\n }\n dx *= distance / 100;\n dy *= distance / 100;\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2,\n filter: `brightness(${Math.max((400 * distance) / 100, 100)}%) contrast(${Math.max(\n (400 * distance) / 100,\n 100\n )}%)`\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n filter: 'brightness(100%) contrast(100%)'\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3',\n opacity: 0\n },\n 0.4\n )\n .to(\n img.DOM.el,\n {\n duration: 1.5,\n ease: 'power4',\n x: `+=${dx * 110}`,\n y: `+=${dy * 110}`\n },\n 0.05\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant5 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private lastAngle: number;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n this.lastAngle = 0;\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let angle = Math.atan2(dy, dx) * (180 / Math.PI);\n if (angle < 0) angle += 360;\n if (angle > 90 && angle <= 270) angle += 180;\n const isMovingClockwise = angle >= this.lastAngle;\n this.lastAngle = angle;\n let startAngle = isMovingClockwise ? angle - 10 : angle + 10;\n const distance = Math.sqrt(dx * dx + dy * dy);\n if (distance !== 0) {\n dx /= distance;\n dy /= distance;\n }\n dx *= distance / 150;\n dy *= distance / 150;\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n filter: 'brightness(80%)',\n scale: 0.1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,\n rotation: startAngle\n },\n {\n duration: 1,\n ease: 'power2',\n scale: 1,\n filter: 'brightness(100%)',\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2 + dx * 70,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2 + dy * 70,\n rotation: this.lastAngle\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'expo',\n opacity: 0\n },\n 0.5\n )\n .to(\n img.DOM.el,\n {\n duration: 1.5,\n ease: 'power4',\n x: `+=${dx * 120}`,\n y: `+=${dy * 120}`\n },\n 0.05\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant6 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n private mapSpeedToSize(speed: number, minSize: number, maxSize: number) {\n const maxSpeed = 200;\n return minSize + (maxSize - minSize) * Math.min(speed / maxSpeed, 1);\n }\n\n private mapSpeedToBrightness(speed: number, minB: number, maxB: number) {\n const maxSpeed = 70;\n return minB + (maxB - minB) * Math.min(speed / maxSpeed, 1);\n }\n\n private mapSpeedToBlur(speed: number, minBlur: number, maxBlur: number) {\n const maxSpeed = 90;\n return minBlur + (maxBlur - minBlur) * Math.min(speed / maxSpeed, 1);\n }\n\n private mapSpeedToGrayscale(speed: number, minG: number, maxG: number) {\n const maxSpeed = 90;\n return minG + (maxG - minG) * Math.min(speed / maxSpeed, 1);\n }\n\n private showNextImage() {\n const dx = this.mousePos.x - this.cacheMousePos.x;\n const dy = this.mousePos.y - this.cacheMousePos.y;\n const speed = Math.sqrt(dx * dx + dy * dy);\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n const scaleFactor = this.mapSpeedToSize(speed, 0.3, 2);\n const brightnessValue = this.mapSpeedToBrightness(speed, 0, 1.3);\n const blurValue = this.mapSpeedToBlur(speed, 20, 0);\n const grayscaleValue = this.mapSpeedToGrayscale(speed, 600, 0);\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.8,\n ease: 'power3',\n scale: scaleFactor,\n filter: `grayscale(${grayscaleValue * 100}%) brightness(${brightnessValue * 100}%) blur(${blurValue}px)`,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n { scale: 2 },\n {\n duration: 0.8,\n ease: 'power3',\n scale: 1\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3.in',\n opacity: 0,\n scale: 0.2\n },\n 0.45\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nfunction getNewPosition(position: number, offset: number, arr: ImageItem[]) {\n const realOffset = Math.abs(offset) % arr.length;\n if (position - realOffset >= 0) {\n return position - realOffset;\n } else {\n return arr.length - (realOffset - position);\n }\n}\n\nclass ImageTrailVariant7 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private visibleImagesCount: number;\n private visibleImagesTotal: number;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n this.visibleImagesCount = 0;\n this.visibleImagesTotal = 9;\n this.visibleImagesTotal = Math.min(this.visibleImagesTotal, this.imagesTotal - 1);\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n ++this.visibleImagesCount;\n\n gsap.killTweensOf(img.DOM.el);\n const scaleValue = gsap.utils.random(0.5, 1.6);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n scale: scaleValue - Math.max(gsap.utils.random(0.2, 0.6), 0),\n rotationZ: 0,\n opacity: 1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power3',\n scale: scaleValue,\n rotationZ: gsap.utils.random(-3, 3),\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n );\n\n if (this.visibleImagesCount >= this.visibleImagesTotal) {\n const lastInQueue = getNewPosition(this.imgPosition, this.visibleImagesTotal, this.images);\n const oldImg = this.images[lastInQueue];\n gsap.to(oldImg.DOM.el, {\n duration: 0.4,\n ease: 'power4',\n opacity: 0,\n scale: 1.3,\n onComplete: () => {\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n });\n }\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n }\n}\n\nclass ImageTrailVariant8 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private rotation: { x: number; y: number };\n private cachedRotation: { x: number; y: number };\n private zValue: number;\n private cachedZValue: number;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n this.rotation = { x: 0, y: 0 };\n this.cachedRotation = { x: 0, y: 0 };\n this.zValue = 0;\n this.cachedZValue = 0;\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n const rect = this.container.getBoundingClientRect();\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const relX = this.mousePos.x - centerX;\n const relY = this.mousePos.y - centerY;\n\n this.rotation.x = -(relY / centerY) * 30;\n this.rotation.y = (relX / centerX) * 30;\n this.cachedRotation = { ...this.rotation };\n\n const distanceFromCenter = Math.sqrt(relX * relX + relY * relY);\n const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);\n const proportion = distanceFromCenter / maxDistance;\n this.zValue = proportion * 1200 - 600;\n this.cachedZValue = this.zValue;\n const normalizedZ = (this.zValue + 600) / 1200;\n const brightness = 0.2 + normalizedZ * 2.3;\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .set(this.DOM.el, { perspective: 1000 }, 0)\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n z: 0,\n scale: 1 + this.cachedZValue / 1000,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,\n rotationX: this.cachedRotation.x,\n rotationY: this.cachedRotation.y,\n filter: `brightness(${brightness})`\n },\n {\n duration: 1,\n ease: 'expo',\n scale: 1 + this.zValue / 1000,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2,\n rotationX: this.rotation.x,\n rotationY: this.rotation.y\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power2',\n opacity: 0,\n z: -800\n },\n 0.3\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\ntype ImageTrailConstructor =\n | typeof ImageTrailVariant1\n | typeof ImageTrailVariant2\n | typeof ImageTrailVariant3\n | typeof ImageTrailVariant4\n | typeof ImageTrailVariant5\n | typeof ImageTrailVariant6\n | typeof ImageTrailVariant7\n | typeof ImageTrailVariant8;\n\nconst variantMap: Record = {\n 1: ImageTrailVariant1,\n 2: ImageTrailVariant2,\n 3: ImageTrailVariant3,\n 4: ImageTrailVariant4,\n 5: ImageTrailVariant5,\n 6: ImageTrailVariant6,\n 7: ImageTrailVariant7,\n 8: ImageTrailVariant8\n};\n\ninterface ImageTrailProps {\n items?: string[];\n variant?: number;\n}\n\nexport default function ImageTrail({ items = [], variant = 1 }: ImageTrailProps): JSX.Element {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n const Cls = variantMap[variant] || variantMap[1];\n new Cls(containerRef.current);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [variant, items]);\n\n return (\n
\n {items.map((url, i) => (\n \n \n
\n ))}\n
\n );\n}\n" + "content": "import { gsap } from 'gsap';\nimport { type JSX, useEffect, useRef } from 'react';\n\nfunction lerp(a: number, b: number, n: number): number {\n return (1 - n) * a + n * b;\n}\n\nfunction getLocalPointerPos(e: MouseEvent | TouchEvent, rect: DOMRect): { x: number; y: number } {\n let clientX = 0,\n clientY = 0;\n if ('touches' in e && e.touches.length > 0) {\n clientX = e.touches[0].clientX;\n clientY = e.touches[0].clientY;\n } else if ('clientX' in e) {\n clientX = e.clientX;\n clientY = e.clientY;\n }\n return {\n x: clientX - rect.left,\n y: clientY - rect.top\n };\n}\n\nfunction getMouseDistance(p1: { x: number; y: number }, p2: { x: number; y: number }): number {\n const dx = p1.x - p2.x;\n const dy = p1.y - p2.y;\n return Math.hypot(dx, dy);\n}\n\nclass ImageItem {\n public DOM: { el: HTMLDivElement; inner: HTMLDivElement | null } = {\n el: null as unknown as HTMLDivElement,\n inner: null\n };\n public defaultStyle: gsap.TweenVars = { scale: 1, x: 0, y: 0, opacity: 0 };\n public rect: DOMRect | null = null;\n private resize!: () => void;\n\n constructor(DOM_el: HTMLDivElement) {\n this.DOM.el = DOM_el;\n this.DOM.inner = this.DOM.el.querySelector('.content__img-inner');\n this.getRect();\n this.initEvents();\n }\n\n private initEvents() {\n this.resize = () => {\n gsap.set(this.DOM.el, this.defaultStyle);\n this.getRect();\n };\n window.addEventListener('resize', this.resize);\n }\n\n private getRect() {\n this.rect = this.DOM.el.getBoundingClientRect();\n }\n\n public destroy() {\n window.removeEventListener('resize', this.resize);\n }\n}\n\nclass ImageTrailVariant1 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3',\n opacity: 0,\n scale: 0.2\n },\n 0.4\n );\n }\n\n public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant2 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n { scale: 2.8, filter: 'brightness(250%)' },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n filter: 'brightness(100%)'\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power2',\n opacity: 0,\n scale: 0.2\n },\n 0.45\n );\n }\n\n public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant3 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n xPercent: 0,\n yPercent: 0,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n { scale: 1.2 },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.6,\n ease: 'power2',\n opacity: 0,\n scale: 0.2,\n xPercent: () => gsap.utils.random(-30, 30),\n yPercent: -200\n },\n 0.6\n );\n }\n\n public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant4 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let distance = Math.sqrt(dx * dx + dy * dy);\n if (distance !== 0) {\n dx /= distance;\n dy /= distance;\n }\n dx *= distance / 100;\n dy *= distance / 100;\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2,\n filter: `brightness(${Math.max((400 * distance) / 100, 100)}%) contrast(${Math.max(\n (400 * distance) / 100,\n 100\n )}%)`\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n filter: 'brightness(100%) contrast(100%)'\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3',\n opacity: 0\n },\n 0.4\n )\n .to(\n img.DOM.el,\n {\n duration: 1.5,\n ease: 'power4',\n x: `+=${dx * 110}`,\n y: `+=${dy * 110}`\n },\n 0.05\n );\n }\n\n public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant5 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\n private lastAngle: number;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n this.lastAngle = 0;\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let angle = Math.atan2(dy, dx) * (180 / Math.PI);\n if (angle < 0) angle += 360;\n if (angle > 90 && angle <= 270) angle += 180;\n const isMovingClockwise = angle >= this.lastAngle;\n this.lastAngle = angle;\n let startAngle = isMovingClockwise ? angle - 10 : angle + 10;\n const distance = Math.sqrt(dx * dx + dy * dy);\n if (distance !== 0) {\n dx /= distance;\n dy /= distance;\n }\n dx *= distance / 150;\n dy *= distance / 150;\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n filter: 'brightness(80%)',\n scale: 0.1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,\n rotation: startAngle\n },\n {\n duration: 1,\n ease: 'power2',\n scale: 1,\n filter: 'brightness(100%)',\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2 + dx * 70,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2 + dy * 70,\n rotation: this.lastAngle\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'expo',\n opacity: 0\n },\n 0.5\n )\n .to(\n img.DOM.el,\n {\n duration: 1.5,\n ease: 'power4',\n x: `+=${dx * 120}`,\n y: `+=${dy * 120}`\n },\n 0.05\n );\n }\n\n public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant6 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n private mapSpeedToSize(speed: number, minSize: number, maxSize: number) {\n const maxSpeed = 200;\n return minSize + (maxSize - minSize) * Math.min(speed / maxSpeed, 1);\n }\n\n private mapSpeedToBrightness(speed: number, minB: number, maxB: number) {\n const maxSpeed = 70;\n return minB + (maxB - minB) * Math.min(speed / maxSpeed, 1);\n }\n\n private mapSpeedToBlur(speed: number, minBlur: number, maxBlur: number) {\n const maxSpeed = 90;\n return minBlur + (maxBlur - minBlur) * Math.min(speed / maxSpeed, 1);\n }\n\n private mapSpeedToGrayscale(speed: number, minG: number, maxG: number) {\n const maxSpeed = 90;\n return minG + (maxG - minG) * Math.min(speed / maxSpeed, 1);\n }\n\n private showNextImage() {\n const dx = this.mousePos.x - this.cacheMousePos.x;\n const dy = this.mousePos.y - this.cacheMousePos.y;\n const speed = Math.sqrt(dx * dx + dy * dy);\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n const scaleFactor = this.mapSpeedToSize(speed, 0.3, 2);\n const brightnessValue = this.mapSpeedToBrightness(speed, 0, 1.3);\n const blurValue = this.mapSpeedToBlur(speed, 20, 0);\n const grayscaleValue = this.mapSpeedToGrayscale(speed, 600, 0);\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.8,\n ease: 'power3',\n scale: scaleFactor,\n filter: `grayscale(${grayscaleValue * 100}%) brightness(${brightnessValue * 100}%) blur(${blurValue}px)`,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n { scale: 2 },\n {\n duration: 0.8,\n ease: 'power3',\n scale: 1\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3.in',\n opacity: 0,\n scale: 0.2\n },\n 0.45\n );\n }\n\n public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nfunction getNewPosition(position: number, offset: number, arr: ImageItem[]) {\n const realOffset = Math.abs(offset) % arr.length;\n if (position - realOffset >= 0) {\n return position - realOffset;\n } else {\n return arr.length - (realOffset - position);\n }\n}\n\nclass ImageTrailVariant7 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\n private visibleImagesCount: number;\n private visibleImagesTotal: number;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n this.visibleImagesCount = 0;\n this.visibleImagesTotal = 9;\n this.visibleImagesTotal = Math.min(this.visibleImagesTotal, this.imagesTotal - 1);\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n ++this.visibleImagesCount;\n\n gsap.killTweensOf(img.DOM.el);\n const scaleValue = gsap.utils.random(0.5, 1.6);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n scale: scaleValue - Math.max(gsap.utils.random(0.2, 0.6), 0),\n rotationZ: 0,\n opacity: 1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power3',\n scale: scaleValue,\n rotationZ: gsap.utils.random(-3, 3),\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n );\n\n if (this.visibleImagesCount >= this.visibleImagesTotal) {\n const lastInQueue = getNewPosition(this.imgPosition, this.visibleImagesTotal, this.images);\n const oldImg = this.images[lastInQueue];\n gsap.to(oldImg.DOM.el, {\n duration: 0.4,\n ease: 'power4',\n opacity: 0,\n scale: 1.3,\n onComplete: () => {\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n });\n }\n }\n\n public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n }\n}\n\nclass ImageTrailVariant8 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\n private rotation: { x: number; y: number };\n private cachedRotation: { x: number; y: number };\n private zValue: number;\n private cachedZValue: number;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n this.rotation = { x: 0, y: 0 };\n this.cachedRotation = { x: 0, y: 0 };\n this.zValue = 0;\n this.cachedZValue = 0;\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n this.rafId = requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n const rect = this.container.getBoundingClientRect();\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const relX = this.mousePos.x - centerX;\n const relY = this.mousePos.y - centerY;\n\n this.rotation.x = -(relY / centerY) * 30;\n this.rotation.y = (relX / centerX) * 30;\n this.cachedRotation = { ...this.rotation };\n\n const distanceFromCenter = Math.sqrt(relX * relX + relY * relY);\n const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);\n const proportion = distanceFromCenter / maxDistance;\n this.zValue = proportion * 1200 - 600;\n this.cachedZValue = this.zValue;\n const normalizedZ = (this.zValue + 600) / 1200;\n const brightness = 0.2 + normalizedZ * 2.3;\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .set(this.DOM.el, { perspective: 1000 }, 0)\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n z: 0,\n scale: 1 + this.cachedZValue / 1000,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,\n rotationX: this.cachedRotation.x,\n rotationY: this.cachedRotation.y,\n filter: `brightness(${brightness})`\n },\n {\n duration: 1,\n ease: 'expo',\n scale: 1 + this.zValue / 1000,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2,\n rotationX: this.rotation.x,\n rotationY: this.rotation.y\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power2',\n opacity: 0,\n z: -800\n },\n 0.3\n );\n }\n\n public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\ntype ImageTrailConstructor =\n | typeof ImageTrailVariant1\n | typeof ImageTrailVariant2\n | typeof ImageTrailVariant3\n | typeof ImageTrailVariant4\n | typeof ImageTrailVariant5\n | typeof ImageTrailVariant6\n | typeof ImageTrailVariant7\n | typeof ImageTrailVariant8;\n\nconst variantMap: Record = {\n 1: ImageTrailVariant1,\n 2: ImageTrailVariant2,\n 3: ImageTrailVariant3,\n 4: ImageTrailVariant4,\n 5: ImageTrailVariant5,\n 6: ImageTrailVariant6,\n 7: ImageTrailVariant7,\n 8: ImageTrailVariant8\n};\n\ninterface ImageTrailProps {\n items?: string[];\n variant?: number;\n}\n\nexport default function ImageTrail({ items = [], variant = 1 }: ImageTrailProps): JSX.Element {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n const Cls = variantMap[variant] || variantMap[1];\n const instance = new Cls(containerRef.current);\n\n return () => {\n instance.destroy();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [variant, items]);\n\n return (\n
\n {items.map((url, i) => (\n \n \n
\n ))}\n
\n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/InfiniteMenu-JS-CSS.json b/public/r/InfiniteMenu-JS-CSS.json index 2b56964f..34f659dd 100644 --- a/public/r/InfiniteMenu-JS-CSS.json +++ b/public/r/InfiniteMenu-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "InfiniteMenu/InfiniteMenu.css", + "type": "registry:file", + "path": "InfiniteMenu.css", + "target": "@components/InfiniteMenu.css", "content": "/* Note: this CSS is only an example, you can overlay whatever you want using the activeItem logic */\n\n#infinite-grid-menu-canvas {\n cursor: grab;\n width: 100%;\n height: 100%;\n overflow: hidden;\n position: relative;\n outline: none;\n}\n\n#infinite-grid-menu-canvas:active {\n cursor: grabbing;\n}\n\n.action-button {\n position: absolute;\n left: 50%;\n z-index: 10;\n width: 60px;\n height: 60px;\n display: grid;\n place-items: center;\n background: #5227ff;\n border: none;\n border-radius: 50%;\n cursor: pointer;\n border: 5px solid #000;\n}\n\n.face-title {\n user-select: none;\n position: absolute;\n font-weight: 900;\n font-size: 3rem;\n left: 1.6em;\n top: 50%;\n}\n\n.action-button-icon {\n user-select: none;\n position: relative;\n color: #fff;\n top: 2px;\n font-size: 26px;\n}\n\n.face-title {\n position: absolute;\n top: 50%;\n transform: translate(20%, -50%);\n}\n\n.face-title.active {\n opacity: 1;\n transform: translate(20%, -50%);\n pointer-events: auto;\n transition: 0.5s ease;\n}\n\n.face-title.inactive {\n pointer-events: none;\n opacity: 0;\n transition: 0.1s ease;\n}\n\n.face-description {\n user-select: none;\n position: absolute;\n max-width: 10ch;\n top: 50%;\n font-size: 1.2rem;\n right: 1%;\n transform: translate(0, -50%);\n}\n\n.face-description.active {\n opacity: 1;\n transform: translate(-90%, -50%);\n pointer-events: auto;\n transition: 0.5s ease;\n}\n\n.face-description.inactive {\n pointer-events: none;\n transform: translate(-60%, -50%);\n opacity: 0;\n transition: 0.1s ease;\n}\n\n.action-button {\n position: absolute;\n left: 50%;\n}\n\n.action-button.active {\n bottom: 3.8em;\n transform: translateX(-50%) scale(1);\n opacity: 1;\n pointer-events: auto;\n transition: 0.5s ease;\n}\n\n.action-button.inactive {\n bottom: -80px;\n transform: translateX(-50%) scale(0);\n opacity: 0;\n pointer-events: none;\n transition: 0.1s ease;\n}\n\n@media (max-width: 1500px) {\n .face-title,\n .face-description {\n display: none;\n }\n}\n" }, { "type": "registry:component", - "path": "InfiniteMenu/InfiniteMenu.jsx", + "path": "InfiniteMenu.jsx", "content": "import { useEffect, useRef, useState } from 'react';\nimport { mat4, quat, vec2, vec3 } from 'gl-matrix';\nimport './InfiniteMenu.css';\n\nconst discVertShaderSource = `#version 300 es\n\nuniform mat4 uWorldMatrix;\nuniform mat4 uViewMatrix;\nuniform mat4 uProjectionMatrix;\nuniform vec3 uCameraPosition;\nuniform vec4 uRotationAxisVelocity;\n\nin vec3 aModelPosition;\nin vec3 aModelNormal;\nin vec2 aModelUvs;\nin mat4 aInstanceMatrix;\n\nout vec2 vUvs;\nout float vAlpha;\nflat out int vInstanceId;\n\n#define PI 3.141593\n\nvoid main() {\n vec4 worldPosition = uWorldMatrix * aInstanceMatrix * vec4(aModelPosition, 1.);\n\n vec3 centerPos = (uWorldMatrix * aInstanceMatrix * vec4(0., 0., 0., 1.)).xyz;\n float radius = length(centerPos.xyz);\n\n if (gl_VertexID > 0) {\n vec3 rotationAxis = uRotationAxisVelocity.xyz;\n float rotationVelocity = min(.15, uRotationAxisVelocity.w * 15.);\n vec3 stretchDir = normalize(cross(centerPos, rotationAxis));\n vec3 relativeVertexPos = normalize(worldPosition.xyz - centerPos);\n float strength = dot(stretchDir, relativeVertexPos);\n float invAbsStrength = min(0., abs(strength) - 1.);\n strength = rotationVelocity * sign(strength) * abs(invAbsStrength * invAbsStrength * invAbsStrength + 1.);\n worldPosition.xyz += stretchDir * strength;\n }\n\n worldPosition.xyz = radius * normalize(worldPosition.xyz);\n\n gl_Position = uProjectionMatrix * uViewMatrix * worldPosition;\n\n vAlpha = smoothstep(0.5, 1., normalize(worldPosition.xyz).z) * .9 + .1;\n vUvs = aModelUvs;\n vInstanceId = gl_InstanceID;\n}\n`;\n\nconst discFragShaderSource = `#version 300 es\nprecision highp float;\n\nuniform sampler2D uTex;\nuniform int uItemCount;\nuniform int uAtlasSize;\n\nout vec4 outColor;\n\nin vec2 vUvs;\nin float vAlpha;\nflat in int vInstanceId;\n\nvoid main() {\n int itemIndex = vInstanceId % uItemCount;\n int cellsPerRow = uAtlasSize;\n int cellX = itemIndex % cellsPerRow;\n int cellY = itemIndex / cellsPerRow;\n vec2 cellSize = vec2(1.0) / vec2(float(cellsPerRow));\n vec2 cellOffset = vec2(float(cellX), float(cellY)) * cellSize;\n\n ivec2 texSize = textureSize(uTex, 0);\n float imageAspect = float(texSize.x) / float(texSize.y);\n float containerAspect = 1.0;\n \n float scale = max(imageAspect / containerAspect, \n containerAspect / imageAspect);\n \n vec2 st = vec2(vUvs.x, 1.0 - vUvs.y);\n st = (st - 0.5) * scale + 0.5;\n \n st = clamp(st, 0.0, 1.0);\n \n st = st * cellSize + cellOffset;\n \n outColor = texture(uTex, st);\n outColor.a *= vAlpha;\n}\n`;\n\nclass Face {\n constructor(a, b, c) {\n this.a = a;\n this.b = b;\n this.c = c;\n }\n}\n\nclass Vertex {\n constructor(x, y, z) {\n this.position = vec3.fromValues(x, y, z);\n this.normal = vec3.create();\n this.uv = vec2.create();\n }\n}\n\nclass Geometry {\n constructor() {\n this.vertices = [];\n this.faces = [];\n }\n\n addVertex(...args) {\n for (let i = 0; i < args.length; i += 3) {\n this.vertices.push(new Vertex(args[i], args[i + 1], args[i + 2]));\n }\n return this;\n }\n\n addFace(...args) {\n for (let i = 0; i < args.length; i += 3) {\n this.faces.push(new Face(args[i], args[i + 1], args[i + 2]));\n }\n return this;\n }\n\n get lastVertex() {\n return this.vertices[this.vertices.length - 1];\n }\n\n subdivide(divisions = 1) {\n const midPointCache = {};\n let f = this.faces;\n\n for (let div = 0; div < divisions; ++div) {\n const newFaces = new Array(f.length * 4);\n\n f.forEach((face, ndx) => {\n const mAB = this.getMidPoint(face.a, face.b, midPointCache);\n const mBC = this.getMidPoint(face.b, face.c, midPointCache);\n const mCA = this.getMidPoint(face.c, face.a, midPointCache);\n\n const i = ndx * 4;\n newFaces[i + 0] = new Face(face.a, mAB, mCA);\n newFaces[i + 1] = new Face(face.b, mBC, mAB);\n newFaces[i + 2] = new Face(face.c, mCA, mBC);\n newFaces[i + 3] = new Face(mAB, mBC, mCA);\n });\n\n f = newFaces;\n }\n\n this.faces = f;\n return this;\n }\n\n spherize(radius = 1) {\n this.vertices.forEach(vertex => {\n vec3.normalize(vertex.normal, vertex.position);\n vec3.scale(vertex.position, vertex.normal, radius);\n });\n return this;\n }\n\n get data() {\n return {\n vertices: this.vertexData,\n indices: this.indexData,\n normals: this.normalData,\n uvs: this.uvData\n };\n }\n\n get vertexData() {\n return new Float32Array(this.vertices.flatMap(v => Array.from(v.position)));\n }\n\n get normalData() {\n return new Float32Array(this.vertices.flatMap(v => Array.from(v.normal)));\n }\n\n get uvData() {\n return new Float32Array(this.vertices.flatMap(v => Array.from(v.uv)));\n }\n\n get indexData() {\n return new Uint16Array(this.faces.flatMap(f => [f.a, f.b, f.c]));\n }\n\n getMidPoint(ndxA, ndxB, cache) {\n const cacheKey = ndxA < ndxB ? `k_${ndxB}_${ndxA}` : `k_${ndxA}_${ndxB}`;\n if (Object.prototype.hasOwnProperty.call(cache, cacheKey)) {\n return cache[cacheKey];\n }\n const a = this.vertices[ndxA].position;\n const b = this.vertices[ndxB].position;\n const ndx = this.vertices.length;\n cache[cacheKey] = ndx;\n this.addVertex((a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5, (a[2] + b[2]) * 0.5);\n return ndx;\n }\n}\n\nclass IcosahedronGeometry extends Geometry {\n constructor() {\n super();\n const t = Math.sqrt(5) * 0.5 + 0.5;\n this.addVertex(\n -1,\n t,\n 0,\n 1,\n t,\n 0,\n -1,\n -t,\n 0,\n 1,\n -t,\n 0,\n 0,\n -1,\n t,\n 0,\n 1,\n t,\n 0,\n -1,\n -t,\n 0,\n 1,\n -t,\n t,\n 0,\n -1,\n t,\n 0,\n 1,\n -t,\n 0,\n -1,\n -t,\n 0,\n 1\n ).addFace(\n 0,\n 11,\n 5,\n 0,\n 5,\n 1,\n 0,\n 1,\n 7,\n 0,\n 7,\n 10,\n 0,\n 10,\n 11,\n 1,\n 5,\n 9,\n 5,\n 11,\n 4,\n 11,\n 10,\n 2,\n 10,\n 7,\n 6,\n 7,\n 1,\n 8,\n 3,\n 9,\n 4,\n 3,\n 4,\n 2,\n 3,\n 2,\n 6,\n 3,\n 6,\n 8,\n 3,\n 8,\n 9,\n 4,\n 9,\n 5,\n 2,\n 4,\n 11,\n 6,\n 2,\n 10,\n 8,\n 6,\n 7,\n 9,\n 8,\n 1\n );\n }\n}\n\nclass DiscGeometry extends Geometry {\n constructor(steps = 4, radius = 1) {\n super();\n steps = Math.max(4, steps);\n\n const alpha = (2 * Math.PI) / steps;\n\n this.addVertex(0, 0, 0);\n this.lastVertex.uv[0] = 0.5;\n this.lastVertex.uv[1] = 0.5;\n\n for (let i = 0; i < steps; ++i) {\n const x = Math.cos(alpha * i);\n const y = Math.sin(alpha * i);\n this.addVertex(radius * x, radius * y, 0);\n this.lastVertex.uv[0] = x * 0.5 + 0.5;\n this.lastVertex.uv[1] = y * 0.5 + 0.5;\n\n if (i > 0) {\n this.addFace(0, i, i + 1);\n }\n }\n this.addFace(0, steps, 1);\n }\n}\n\nfunction createShader(gl, type, source) {\n const shader = gl.createShader(type);\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);\n\n if (success) {\n return shader;\n }\n\n console.error(gl.getShaderInfoLog(shader));\n gl.deleteShader(shader);\n return null;\n}\n\nfunction createProgram(gl, shaderSources, transformFeedbackVaryings, attribLocations) {\n const program = gl.createProgram();\n\n [gl.VERTEX_SHADER, gl.FRAGMENT_SHADER].forEach((type, ndx) => {\n const shader = createShader(gl, type, shaderSources[ndx]);\n if (shader) gl.attachShader(program, shader);\n });\n\n if (transformFeedbackVaryings) {\n gl.transformFeedbackVaryings(program, transformFeedbackVaryings, gl.SEPARATE_ATTRIBS);\n }\n\n if (attribLocations) {\n for (const attrib in attribLocations) {\n gl.bindAttribLocation(program, attribLocations[attrib], attrib);\n }\n }\n\n gl.linkProgram(program);\n const success = gl.getProgramParameter(program, gl.LINK_STATUS);\n\n if (success) {\n return program;\n }\n\n console.error(gl.getProgramInfoLog(program));\n gl.deleteProgram(program);\n return null;\n}\n\nfunction makeVertexArray(gl, bufLocNumElmPairs, indices) {\n const va = gl.createVertexArray();\n gl.bindVertexArray(va);\n\n for (const [buffer, loc, numElem] of bufLocNumElmPairs) {\n if (loc === -1) continue;\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.enableVertexAttribArray(loc);\n gl.vertexAttribPointer(loc, numElem, gl.FLOAT, false, 0, 0);\n }\n\n if (indices) {\n const indexBuffer = gl.createBuffer();\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);\n gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);\n }\n\n gl.bindVertexArray(null);\n return va;\n}\n\nfunction resizeCanvasToDisplaySize(canvas) {\n const dpr = Math.min(2, window.devicePixelRatio);\n const displayWidth = Math.round(canvas.clientWidth * dpr);\n const displayHeight = Math.round(canvas.clientHeight * dpr);\n const needResize = canvas.width !== displayWidth || canvas.height !== displayHeight;\n if (needResize) {\n canvas.width = displayWidth;\n canvas.height = displayHeight;\n }\n return needResize;\n}\n\nfunction makeBuffer(gl, sizeOrData, usage) {\n const buf = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n gl.bufferData(gl.ARRAY_BUFFER, sizeOrData, usage);\n gl.bindBuffer(gl.ARRAY_BUFFER, null);\n return buf;\n}\n\nfunction createAndSetupTexture(gl, minFilter, magFilter, wrapS, wrapT) {\n const texture = gl.createTexture();\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapS);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapT);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, magFilter);\n return texture;\n}\n\nclass ArcballControl {\n isPointerDown = false;\n orientation = quat.create();\n pointerRotation = quat.create();\n rotationVelocity = 0;\n rotationAxis = vec3.fromValues(1, 0, 0);\n snapDirection = vec3.fromValues(0, 0, -1);\n snapTargetDirection;\n EPSILON = 0.1;\n IDENTITY_QUAT = quat.create();\n\n constructor(canvas, updateCallback) {\n this.canvas = canvas;\n this.updateCallback = updateCallback || (() => null);\n\n this.pointerPos = vec2.create();\n this.previousPointerPos = vec2.create();\n this._rotationVelocity = 0;\n this._combinedQuat = quat.create();\n\n canvas.addEventListener('pointerdown', e => {\n vec2.set(this.pointerPos, e.clientX, e.clientY);\n vec2.copy(this.previousPointerPos, this.pointerPos);\n this.isPointerDown = true;\n });\n canvas.addEventListener('pointerup', () => {\n this.isPointerDown = false;\n });\n canvas.addEventListener('pointerleave', () => {\n this.isPointerDown = false;\n });\n canvas.addEventListener('pointermove', e => {\n if (this.isPointerDown) {\n vec2.set(this.pointerPos, e.clientX, e.clientY);\n }\n });\n\n canvas.style.touchAction = 'none';\n }\n\n update(deltaTime, targetFrameDuration = 16) {\n const timeScale = deltaTime / targetFrameDuration + 0.00001;\n let angleFactor = timeScale;\n let snapRotation = quat.create();\n\n if (this.isPointerDown) {\n const INTENSITY = 0.3 * timeScale;\n const ANGLE_AMPLIFICATION = 5 / timeScale;\n\n const midPointerPos = vec2.sub(vec2.create(), this.pointerPos, this.previousPointerPos);\n vec2.scale(midPointerPos, midPointerPos, INTENSITY);\n\n if (vec2.sqrLen(midPointerPos) > this.EPSILON) {\n vec2.add(midPointerPos, this.previousPointerPos, midPointerPos);\n\n const p = this.#project(midPointerPos);\n const q = this.#project(this.previousPointerPos);\n const a = vec3.normalize(vec3.create(), p);\n const b = vec3.normalize(vec3.create(), q);\n\n vec2.copy(this.previousPointerPos, midPointerPos);\n\n angleFactor *= ANGLE_AMPLIFICATION;\n\n this.quatFromVectors(a, b, this.pointerRotation, angleFactor);\n } else {\n quat.slerp(this.pointerRotation, this.pointerRotation, this.IDENTITY_QUAT, INTENSITY);\n }\n } else {\n const INTENSITY = 0.1 * timeScale;\n quat.slerp(this.pointerRotation, this.pointerRotation, this.IDENTITY_QUAT, INTENSITY);\n\n if (this.snapTargetDirection) {\n const SNAPPING_INTENSITY = 0.2;\n const a = this.snapTargetDirection;\n const b = this.snapDirection;\n const sqrDist = vec3.squaredDistance(a, b);\n const distanceFactor = Math.max(0.1, 1 - sqrDist * 10);\n angleFactor *= SNAPPING_INTENSITY * distanceFactor;\n this.quatFromVectors(a, b, snapRotation, angleFactor);\n }\n }\n\n const combinedQuat = quat.multiply(quat.create(), snapRotation, this.pointerRotation);\n this.orientation = quat.multiply(quat.create(), combinedQuat, this.orientation);\n quat.normalize(this.orientation, this.orientation);\n\n const RA_INTENSITY = 0.8 * timeScale;\n quat.slerp(this._combinedQuat, this._combinedQuat, combinedQuat, RA_INTENSITY);\n quat.normalize(this._combinedQuat, this._combinedQuat);\n\n const rad = Math.acos(this._combinedQuat[3]) * 2.0;\n const s = Math.sin(rad / 2.0);\n let rv = 0;\n if (s > 0.000001) {\n rv = rad / (2 * Math.PI);\n this.rotationAxis[0] = this._combinedQuat[0] / s;\n this.rotationAxis[1] = this._combinedQuat[1] / s;\n this.rotationAxis[2] = this._combinedQuat[2] / s;\n }\n\n const RV_INTENSITY = 0.5 * timeScale;\n this._rotationVelocity += (rv - this._rotationVelocity) * RV_INTENSITY;\n this.rotationVelocity = this._rotationVelocity / timeScale;\n\n this.updateCallback(deltaTime);\n }\n\n quatFromVectors(a, b, out, angleFactor = 1) {\n const axis = vec3.cross(vec3.create(), a, b);\n vec3.normalize(axis, axis);\n const d = Math.max(-1, Math.min(1, vec3.dot(a, b)));\n const angle = Math.acos(d) * angleFactor;\n quat.setAxisAngle(out, axis, angle);\n return { q: out, axis, angle };\n }\n\n #project(pos) {\n const r = 2;\n const w = this.canvas.clientWidth;\n const h = this.canvas.clientHeight;\n const s = Math.max(w, h) - 1;\n\n const x = (2 * pos[0] - w - 1) / s;\n const y = (2 * pos[1] - h - 1) / s;\n let z = 0;\n const xySq = x * x + y * y;\n const rSq = r * r;\n\n if (xySq <= rSq / 2.0) {\n z = Math.sqrt(rSq - xySq);\n } else {\n z = rSq / Math.sqrt(xySq);\n }\n return vec3.fromValues(-x, y, z);\n }\n}\n\nclass InfiniteGridMenu {\n TARGET_FRAME_DURATION = 1000 / 60;\n SPHERE_RADIUS = 2;\n\n #time = 0;\n #deltaTime = 0;\n #deltaFrames = 0;\n #frames = 0;\n\n camera = {\n matrix: mat4.create(),\n near: 0.1,\n far: 40,\n fov: Math.PI / 4,\n aspect: 1,\n position: vec3.fromValues(0, 0, 3),\n up: vec3.fromValues(0, 1, 0),\n matrices: {\n view: mat4.create(),\n projection: mat4.create(),\n inversProjection: mat4.create()\n }\n };\n\n nearestVertexIndex = null;\n smoothRotationVelocity = 0;\n scaleFactor = 1.0;\n movementActive = false;\n\n constructor(canvas, items, onActiveItemChange, onMovementChange, onInit = null, scale = 1.0) {\n this.canvas = canvas;\n this.items = items || [];\n this.onActiveItemChange = onActiveItemChange || (() => {});\n this.onMovementChange = onMovementChange || (() => {});\n this.scaleFactor = scale;\n this.camera.position[2] = 3 * scale;\n this.#init(onInit);\n }\n\n resize() {\n this.viewportSize = vec2.set(this.viewportSize || vec2.create(), this.canvas.clientWidth, this.canvas.clientHeight);\n\n const gl = this.gl;\n const needsResize = resizeCanvasToDisplaySize(gl.canvas);\n if (needsResize) {\n gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);\n }\n\n this.#updateProjectionMatrix(gl);\n }\n\n run(time = 0) {\n this.#deltaTime = Math.min(32, time - this.#time);\n this.#time = time;\n this.#deltaFrames = this.#deltaTime / this.TARGET_FRAME_DURATION;\n this.#frames += this.#deltaFrames;\n\n this.#animate(this.#deltaTime);\n this.#render();\n\n requestAnimationFrame(t => this.run(t));\n }\n\n #init(onInit) {\n this.gl = this.canvas.getContext('webgl2', { antialias: true, alpha: false });\n const gl = this.gl;\n if (!gl) {\n throw new Error('No WebGL 2 context!');\n }\n\n this.viewportSize = vec2.fromValues(this.canvas.clientWidth, this.canvas.clientHeight);\n this.drawBufferSize = vec2.clone(this.viewportSize);\n\n this.discProgram = createProgram(gl, [discVertShaderSource, discFragShaderSource], null, {\n aModelPosition: 0,\n aModelNormal: 1,\n aModelUvs: 2,\n aInstanceMatrix: 3\n });\n\n this.discLocations = {\n aModelPosition: gl.getAttribLocation(this.discProgram, 'aModelPosition'),\n aModelUvs: gl.getAttribLocation(this.discProgram, 'aModelUvs'),\n aInstanceMatrix: gl.getAttribLocation(this.discProgram, 'aInstanceMatrix'),\n uWorldMatrix: gl.getUniformLocation(this.discProgram, 'uWorldMatrix'),\n uViewMatrix: gl.getUniformLocation(this.discProgram, 'uViewMatrix'),\n uProjectionMatrix: gl.getUniformLocation(this.discProgram, 'uProjectionMatrix'),\n uCameraPosition: gl.getUniformLocation(this.discProgram, 'uCameraPosition'),\n uScaleFactor: gl.getUniformLocation(this.discProgram, 'uScaleFactor'),\n uRotationAxisVelocity: gl.getUniformLocation(this.discProgram, 'uRotationAxisVelocity'),\n uTex: gl.getUniformLocation(this.discProgram, 'uTex'),\n uFrames: gl.getUniformLocation(this.discProgram, 'uFrames'),\n uItemCount: gl.getUniformLocation(this.discProgram, 'uItemCount'),\n uAtlasSize: gl.getUniformLocation(this.discProgram, 'uAtlasSize')\n };\n\n this.discGeo = new DiscGeometry(56, 1);\n this.discBuffers = this.discGeo.data;\n this.discVAO = makeVertexArray(\n gl,\n [\n [makeBuffer(gl, this.discBuffers.vertices, gl.STATIC_DRAW), this.discLocations.aModelPosition, 3],\n [makeBuffer(gl, this.discBuffers.uvs, gl.STATIC_DRAW), this.discLocations.aModelUvs, 2]\n ],\n this.discBuffers.indices\n );\n\n this.icoGeo = new IcosahedronGeometry();\n this.icoGeo.subdivide(1).spherize(this.SPHERE_RADIUS);\n this.instancePositions = this.icoGeo.vertices.map(v => v.position);\n this.DISC_INSTANCE_COUNT = this.icoGeo.vertices.length;\n this.#initDiscInstances(this.DISC_INSTANCE_COUNT);\n\n this.worldMatrix = mat4.create();\n this.#initTexture();\n\n this.control = new ArcballControl(this.canvas, deltaTime => this.#onControlUpdate(deltaTime));\n\n this.#updateCameraMatrix();\n this.#updateProjectionMatrix(gl);\n this.resize();\n\n if (onInit) onInit(this);\n }\n\n #initTexture() {\n const gl = this.gl;\n this.tex = createAndSetupTexture(gl, gl.LINEAR, gl.LINEAR, gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE);\n\n const itemCount = Math.max(1, this.items.length);\n this.atlasSize = Math.ceil(Math.sqrt(itemCount));\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d');\n const cellSize = 512;\n\n canvas.width = this.atlasSize * cellSize;\n canvas.height = this.atlasSize * cellSize;\n\n Promise.all(\n this.items.map(\n item =>\n new Promise(resolve => {\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => resolve(img);\n img.src = item.image;\n })\n )\n ).then(images => {\n images.forEach((img, i) => {\n const x = (i % this.atlasSize) * cellSize;\n const y = Math.floor(i / this.atlasSize) * cellSize;\n ctx.drawImage(img, x, y, cellSize, cellSize);\n });\n\n gl.bindTexture(gl.TEXTURE_2D, this.tex);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);\n gl.generateMipmap(gl.TEXTURE_2D);\n });\n }\n\n #initDiscInstances(count) {\n const gl = this.gl;\n this.discInstances = {\n matricesArray: new Float32Array(count * 16),\n matrices: [],\n buffer: gl.createBuffer()\n };\n for (let i = 0; i < count; ++i) {\n const instanceMatrixArray = new Float32Array(this.discInstances.matricesArray.buffer, i * 16 * 4, 16);\n instanceMatrixArray.set(mat4.create());\n this.discInstances.matrices.push(instanceMatrixArray);\n }\n gl.bindVertexArray(this.discVAO);\n gl.bindBuffer(gl.ARRAY_BUFFER, this.discInstances.buffer);\n gl.bufferData(gl.ARRAY_BUFFER, this.discInstances.matricesArray.byteLength, gl.DYNAMIC_DRAW);\n const mat4AttribSlotCount = 4;\n const bytesPerMatrix = 16 * 4;\n for (let j = 0; j < mat4AttribSlotCount; ++j) {\n const loc = this.discLocations.aInstanceMatrix + j;\n gl.enableVertexAttribArray(loc);\n gl.vertexAttribPointer(loc, 4, gl.FLOAT, false, bytesPerMatrix, j * 4 * 4);\n gl.vertexAttribDivisor(loc, 1);\n }\n gl.bindBuffer(gl.ARRAY_BUFFER, null);\n gl.bindVertexArray(null);\n }\n\n #animate(deltaTime) {\n const gl = this.gl;\n this.control.update(deltaTime, this.TARGET_FRAME_DURATION);\n\n let positions = this.instancePositions.map(p => vec3.transformQuat(vec3.create(), p, this.control.orientation));\n const scale = 0.25;\n const SCALE_INTENSITY = 0.6;\n positions.forEach((p, ndx) => {\n const s = (Math.abs(p[2]) / this.SPHERE_RADIUS) * SCALE_INTENSITY + (1 - SCALE_INTENSITY);\n const finalScale = s * scale;\n const matrix = mat4.create();\n mat4.multiply(matrix, matrix, mat4.fromTranslation(mat4.create(), vec3.negate(vec3.create(), p)));\n mat4.multiply(matrix, matrix, mat4.targetTo(mat4.create(), [0, 0, 0], p, [0, 1, 0]));\n mat4.multiply(matrix, matrix, mat4.fromScaling(mat4.create(), [finalScale, finalScale, finalScale]));\n mat4.multiply(matrix, matrix, mat4.fromTranslation(mat4.create(), [0, 0, -this.SPHERE_RADIUS]));\n\n mat4.copy(this.discInstances.matrices[ndx], matrix);\n });\n\n gl.bindBuffer(gl.ARRAY_BUFFER, this.discInstances.buffer);\n gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.discInstances.matricesArray);\n gl.bindBuffer(gl.ARRAY_BUFFER, null);\n\n this.smoothRotationVelocity = this.control.rotationVelocity;\n }\n\n #render() {\n const gl = this.gl;\n gl.useProgram(this.discProgram);\n\n gl.enable(gl.CULL_FACE);\n gl.enable(gl.DEPTH_TEST);\n\n gl.clearColor(0, 0, 0, 0);\n gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);\n\n gl.uniformMatrix4fv(this.discLocations.uWorldMatrix, false, this.worldMatrix);\n gl.uniformMatrix4fv(this.discLocations.uViewMatrix, false, this.camera.matrices.view);\n gl.uniformMatrix4fv(this.discLocations.uProjectionMatrix, false, this.camera.matrices.projection);\n gl.uniform3f(\n this.discLocations.uCameraPosition,\n this.camera.position[0],\n this.camera.position[1],\n this.camera.position[2]\n );\n gl.uniform4f(\n this.discLocations.uRotationAxisVelocity,\n this.control.rotationAxis[0],\n this.control.rotationAxis[1],\n this.control.rotationAxis[2],\n this.smoothRotationVelocity * 1.1\n );\n\n gl.uniform1i(this.discLocations.uItemCount, this.items.length);\n gl.uniform1i(this.discLocations.uAtlasSize, this.atlasSize);\n\n gl.uniform1f(this.discLocations.uFrames, this.#frames);\n gl.uniform1f(this.discLocations.uScaleFactor, this.scaleFactor);\n gl.uniform1i(this.discLocations.uTex, 0);\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, this.tex);\n\n gl.bindVertexArray(this.discVAO);\n gl.drawElementsInstanced(\n gl.TRIANGLES,\n this.discBuffers.indices.length,\n gl.UNSIGNED_SHORT,\n 0,\n this.DISC_INSTANCE_COUNT\n );\n }\n\n #updateCameraMatrix() {\n mat4.targetTo(this.camera.matrix, this.camera.position, [0, 0, 0], this.camera.up);\n mat4.invert(this.camera.matrices.view, this.camera.matrix);\n }\n\n #updateProjectionMatrix(gl) {\n this.camera.aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;\n const height = this.SPHERE_RADIUS * 0.35;\n const distance = this.camera.position[2];\n if (this.camera.aspect > 1) {\n this.camera.fov = 2 * Math.atan(height / distance);\n } else {\n this.camera.fov = 2 * Math.atan(height / this.camera.aspect / distance);\n }\n mat4.perspective(\n this.camera.matrices.projection,\n this.camera.fov,\n this.camera.aspect,\n this.camera.near,\n this.camera.far\n );\n mat4.invert(this.camera.matrices.inversProjection, this.camera.matrices.projection);\n }\n\n #onControlUpdate(deltaTime) {\n const timeScale = deltaTime / this.TARGET_FRAME_DURATION + 0.0001;\n let damping = 5 / timeScale;\n let cameraTargetZ = 3 * this.scaleFactor;\n\n const isMoving = this.control.isPointerDown || Math.abs(this.smoothRotationVelocity) > 0.01;\n\n if (isMoving !== this.movementActive) {\n this.movementActive = isMoving;\n this.onMovementChange(isMoving);\n }\n\n if (!this.control.isPointerDown) {\n const nearestVertexIndex = this.#findNearestVertexIndex();\n const itemIndex = nearestVertexIndex % Math.max(1, this.items.length);\n this.onActiveItemChange(itemIndex);\n const snapDirection = vec3.normalize(vec3.create(), this.#getVertexWorldPosition(nearestVertexIndex));\n this.control.snapTargetDirection = snapDirection;\n } else {\n cameraTargetZ += this.control.rotationVelocity * 80 + 2.5;\n damping = 7 / timeScale;\n }\n\n this.camera.position[2] += (cameraTargetZ - this.camera.position[2]) / damping;\n this.#updateCameraMatrix();\n }\n\n #findNearestVertexIndex() {\n const n = this.control.snapDirection;\n const inversOrientation = quat.conjugate(quat.create(), this.control.orientation);\n const nt = vec3.transformQuat(vec3.create(), n, inversOrientation);\n\n let maxD = -1;\n let nearestVertexIndex;\n for (let i = 0; i < this.instancePositions.length; ++i) {\n const d = vec3.dot(nt, this.instancePositions[i]);\n if (d > maxD) {\n maxD = d;\n nearestVertexIndex = i;\n }\n }\n return nearestVertexIndex;\n }\n\n #getVertexWorldPosition(index) {\n const nearestVertexPos = this.instancePositions[index];\n return vec3.transformQuat(vec3.create(), nearestVertexPos, this.control.orientation);\n }\n}\n\nconst defaultItems = [\n {\n image:\n 'https://images.unsplash.com/photo-1782977389500-dd7adad33ebe?q=80&w=600&h=600&fit=crop&sat=-100&auto=format',\n link: 'https://google.com/',\n title: '',\n description: ''\n }\n];\n\nexport default function InfiniteMenu({ items = [], scale = 1.0 }) {\n const canvasRef = useRef(null);\n const [activeItem, setActiveItem] = useState(null);\n const [isMoving, setIsMoving] = useState(false);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n let sketch;\n\n const handleActiveItem = index => {\n const itemIndex = index % items.length;\n setActiveItem(items[itemIndex]);\n };\n\n if (canvas) {\n sketch = new InfiniteGridMenu(\n canvas,\n items.length ? items : defaultItems,\n handleActiveItem,\n setIsMoving,\n sk => sk.run(),\n scale\n );\n }\n\n const handleResize = () => {\n if (sketch) {\n sketch.resize();\n }\n };\n\n window.addEventListener('resize', handleResize);\n handleResize();\n\n return () => {\n window.removeEventListener('resize', handleResize);\n };\n }, [items, scale]);\n\n const handleButtonClick = () => {\n if (!activeItem?.link) return;\n if (activeItem.link.startsWith('http')) {\n window.open(activeItem.link, '_blank');\n } else {\n console.log('Internal route:', activeItem.link);\n }\n };\n\n return (\n
\n \n\n {activeItem && (\n <>\n

{activeItem.title}

\n\n

{activeItem.description}

\n\n
\n

\n
\n \n )}\n
\n );\n}\n" } ], diff --git a/public/r/InfiniteMenu-TS-CSS.json b/public/r/InfiniteMenu-TS-CSS.json index a448a693..0bd0fb9a 100644 --- a/public/r/InfiniteMenu-TS-CSS.json +++ b/public/r/InfiniteMenu-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "InfiniteMenu/InfiniteMenu.css", + "type": "registry:file", + "path": "InfiniteMenu.css", + "target": "@components/InfiniteMenu.css", "content": "/* Note: this CSS is only an example, you can overlay whatever you want using the\n * activeItem logic\n */\n\n#infinite-grid-menu-canvas {\n cursor: grab;\n width: 100%;\n height: 100%;\n overflow: hidden;\n position: relative;\n outline: none;\n}\n\n#infinite-grid-menu-canvas:active {\n cursor: grabbing;\n}\n\n.action-button {\n position: absolute;\n left: 50%;\n z-index: 10;\n width: 60px;\n height: 60px;\n display: grid;\n place-items: center;\n background: cyan;\n border: none;\n border-radius: 50%;\n cursor: pointer;\n border: 5px solid #000;\n}\n\n.face-title {\n user-select: none;\n position: absolute;\n font-weight: 900;\n font-size: 4rem;\n left: 1.6em;\n top: 50%;\n}\n\n.action-button-icon {\n user-select: none;\n position: relative;\n color: #120F17;\n top: 2px;\n font-size: 26px;\n}\n\n.face-title {\n position: absolute;\n top: 50%;\n transform: translate(20%, -50%);\n}\n\n.face-title.active {\n opacity: 1;\n transform: translate(20%, -50%);\n pointer-events: auto;\n transition: 0.5s ease;\n}\n\n.face-title.inactive {\n pointer-events: none;\n opacity: 0;\n transition: 0.1s ease;\n}\n\n.face-description {\n user-select: none;\n position: absolute;\n max-width: 10ch;\n top: 50%;\n font-size: 1.5rem;\n right: 1%;\n transform: translate(0, -50%);\n}\n\n.face-description.active {\n opacity: 1;\n transform: translate(-90%, -50%);\n pointer-events: auto;\n transition: 0.5s ease;\n}\n\n.face-description.inactive {\n pointer-events: none;\n transform: translate(-60%, -50%);\n opacity: 0;\n transition: 0.1s ease;\n}\n\n.action-button {\n position: absolute;\n left: 50%;\n}\n\n.action-button.active {\n bottom: 3.8em;\n transform: translateX(-50%) scale(1);\n opacity: 1;\n pointer-events: auto;\n transition: 0.5s ease;\n}\n\n.action-button.inactive {\n bottom: -80px;\n transform: translateX(-50%) scale(0);\n opacity: 0;\n pointer-events: none;\n transition: 0.1s ease;\n}\n" }, { "type": "registry:component", - "path": "InfiniteMenu/InfiniteMenu.tsx", - "content": "import { FC, useRef, useState, useEffect, MutableRefObject } from 'react';\nimport { mat4, quat, vec2, vec3 } from 'gl-matrix';\nimport './InfiniteMenu.css';\n\nconst discVertShaderSource = `#version 300 es\n\nuniform mat4 uWorldMatrix;\nuniform mat4 uViewMatrix;\nuniform mat4 uProjectionMatrix;\nuniform vec3 uCameraPosition;\nuniform vec4 uRotationAxisVelocity;\n\nin vec3 aModelPosition;\nin vec3 aModelNormal;\nin vec2 aModelUvs;\nin mat4 aInstanceMatrix;\n\nout vec2 vUvs;\nout float vAlpha;\nflat out int vInstanceId;\n\n#define PI 3.141593\n\nvoid main() {\n vec4 worldPosition = uWorldMatrix * aInstanceMatrix * vec4(aModelPosition, 1.);\n\n vec3 centerPos = (uWorldMatrix * aInstanceMatrix * vec4(0., 0., 0., 1.)).xyz;\n float radius = length(centerPos.xyz);\n\n if (gl_VertexID > 0) {\n vec3 rotationAxis = uRotationAxisVelocity.xyz;\n float rotationVelocity = min(.15, uRotationAxisVelocity.w * 15.);\n vec3 stretchDir = normalize(cross(centerPos, rotationAxis));\n vec3 relativeVertexPos = normalize(worldPosition.xyz - centerPos);\n float strength = dot(stretchDir, relativeVertexPos);\n float invAbsStrength = min(0., abs(strength) - 1.);\n strength = rotationVelocity * sign(strength) * abs(invAbsStrength * invAbsStrength * invAbsStrength + 1.);\n worldPosition.xyz += stretchDir * strength;\n }\n\n worldPosition.xyz = radius * normalize(worldPosition.xyz);\n\n gl_Position = uProjectionMatrix * uViewMatrix * worldPosition;\n\n vAlpha = smoothstep(0.5, 1., normalize(worldPosition.xyz).z) * .9 + .1;\n vUvs = aModelUvs;\n vInstanceId = gl_InstanceID;\n}\n`;\n\nconst discFragShaderSource = `#version 300 es\nprecision highp float;\n\nuniform sampler2D uTex;\nuniform int uItemCount;\nuniform int uAtlasSize;\n\nout vec4 outColor;\n\nin vec2 vUvs;\nin float vAlpha;\nflat in int vInstanceId;\n\nvoid main() {\n int itemIndex = vInstanceId % uItemCount;\n int cellsPerRow = uAtlasSize;\n int cellX = itemIndex % cellsPerRow;\n int cellY = itemIndex / cellsPerRow;\n vec2 cellSize = vec2(1.0) / vec2(float(cellsPerRow));\n vec2 cellOffset = vec2(float(cellX), float(cellY)) * cellSize;\n\n ivec2 texSize = textureSize(uTex, 0);\n float imageAspect = float(texSize.x) / float(texSize.y);\n float containerAspect = 1.0;\n \n float scale = max(imageAspect / containerAspect, \n containerAspect / imageAspect);\n \n vec2 st = vec2(vUvs.x, 1.0 - vUvs.y);\n st = (st - 0.5) * scale + 0.5;\n \n st = clamp(st, 0.0, 1.0);\n \n st = st * cellSize + cellOffset;\n \n outColor = texture(uTex, st);\n outColor.a *= vAlpha;\n}\n`;\n\nclass Face {\n public a: number;\n public b: number;\n public c: number;\n\n constructor(a: number, b: number, c: number) {\n this.a = a;\n this.b = b;\n this.c = c;\n }\n}\n\nclass Vertex {\n public position: vec3;\n public normal: vec3;\n public uv: vec2;\n\n constructor(x: number, y: number, z: number) {\n this.position = vec3.fromValues(x, y, z);\n this.normal = vec3.create();\n this.uv = vec2.create();\n }\n}\n\nclass Geometry {\n public vertices: Vertex[];\n public faces: Face[];\n\n constructor() {\n this.vertices = [];\n this.faces = [];\n }\n\n public addVertex(...args: number[]): this {\n for (let i = 0; i < args.length; i += 3) {\n this.vertices.push(new Vertex(args[i], args[i + 1], args[i + 2]));\n }\n return this;\n }\n\n public addFace(...args: number[]): this {\n for (let i = 0; i < args.length; i += 3) {\n this.faces.push(new Face(args[i], args[i + 1], args[i + 2]));\n }\n return this;\n }\n\n public get lastVertex(): Vertex {\n return this.vertices[this.vertices.length - 1];\n }\n\n public subdivide(divisions = 1): this {\n const midPointCache: Record = {};\n let f = this.faces;\n\n for (let div = 0; div < divisions; ++div) {\n const newFaces = new Array(f.length * 4);\n\n f.forEach((face, ndx) => {\n const mAB = this.getMidPoint(face.a, face.b, midPointCache);\n const mBC = this.getMidPoint(face.b, face.c, midPointCache);\n const mCA = this.getMidPoint(face.c, face.a, midPointCache);\n\n const i = ndx * 4;\n newFaces[i + 0] = new Face(face.a, mAB, mCA);\n newFaces[i + 1] = new Face(face.b, mBC, mAB);\n newFaces[i + 2] = new Face(face.c, mCA, mBC);\n newFaces[i + 3] = new Face(mAB, mBC, mCA);\n });\n\n f = newFaces;\n }\n\n this.faces = f;\n return this;\n }\n\n public spherize(radius = 1): this {\n this.vertices.forEach(vertex => {\n vec3.normalize(vertex.normal, vertex.position);\n vec3.scale(vertex.position, vertex.normal, radius);\n });\n return this;\n }\n\n public get data(): {\n vertices: Float32Array;\n indices: Uint16Array;\n normals: Float32Array;\n uvs: Float32Array;\n } {\n return {\n vertices: this.vertexData,\n indices: this.indexData,\n normals: this.normalData,\n uvs: this.uvData\n };\n }\n\n public get vertexData(): Float32Array {\n return new Float32Array(this.vertices.flatMap(v => Array.from(v.position)));\n }\n\n public get normalData(): Float32Array {\n return new Float32Array(this.vertices.flatMap(v => Array.from(v.normal)));\n }\n\n public get uvData(): Float32Array {\n return new Float32Array(this.vertices.flatMap(v => Array.from(v.uv)));\n }\n\n public get indexData(): Uint16Array {\n return new Uint16Array(this.faces.flatMap(f => [f.a, f.b, f.c]));\n }\n\n public getMidPoint(ndxA: number, ndxB: number, cache: Record): number {\n const cacheKey = ndxA < ndxB ? `k_${ndxB}_${ndxA}` : `k_${ndxA}_${ndxB}`;\n if (Object.prototype.hasOwnProperty.call(cache, cacheKey)) {\n return cache[cacheKey];\n }\n const a = this.vertices[ndxA].position;\n const b = this.vertices[ndxB].position;\n const ndx = this.vertices.length;\n cache[cacheKey] = ndx;\n this.addVertex((a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5, (a[2] + b[2]) * 0.5);\n return ndx;\n }\n}\n\nclass IcosahedronGeometry extends Geometry {\n constructor() {\n super();\n const t = Math.sqrt(5) * 0.5 + 0.5;\n this.addVertex(\n -1,\n t,\n 0,\n 1,\n t,\n 0,\n -1,\n -t,\n 0,\n 1,\n -t,\n 0,\n 0,\n -1,\n t,\n 0,\n 1,\n t,\n 0,\n -1,\n -t,\n 0,\n 1,\n -t,\n t,\n 0,\n -1,\n t,\n 0,\n 1,\n -t,\n 0,\n -1,\n -t,\n 0,\n 1\n ).addFace(\n 0,\n 11,\n 5,\n 0,\n 5,\n 1,\n 0,\n 1,\n 7,\n 0,\n 7,\n 10,\n 0,\n 10,\n 11,\n 1,\n 5,\n 9,\n 5,\n 11,\n 4,\n 11,\n 10,\n 2,\n 10,\n 7,\n 6,\n 7,\n 1,\n 8,\n 3,\n 9,\n 4,\n 3,\n 4,\n 2,\n 3,\n 2,\n 6,\n 3,\n 6,\n 8,\n 3,\n 8,\n 9,\n 4,\n 9,\n 5,\n 2,\n 4,\n 11,\n 6,\n 2,\n 10,\n 8,\n 6,\n 7,\n 9,\n 8,\n 1\n );\n }\n}\n\nclass DiscGeometry extends Geometry {\n constructor(steps = 4, radius = 1) {\n super();\n const safeSteps = Math.max(4, steps);\n const alpha = (2 * Math.PI) / safeSteps;\n\n this.addVertex(0, 0, 0);\n this.lastVertex.uv[0] = 0.5;\n this.lastVertex.uv[1] = 0.5;\n\n for (let i = 0; i < safeSteps; ++i) {\n const x = Math.cos(alpha * i);\n const y = Math.sin(alpha * i);\n this.addVertex(radius * x, radius * y, 0);\n this.lastVertex.uv[0] = x * 0.5 + 0.5;\n this.lastVertex.uv[1] = y * 0.5 + 0.5;\n\n if (i > 0) {\n this.addFace(0, i, i + 1);\n }\n }\n this.addFace(0, safeSteps, 1);\n }\n}\n\nfunction createShader(gl: WebGL2RenderingContext, type: number, source: string): WebGLShader | null {\n const shader = gl.createShader(type);\n if (!shader) return null;\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);\n\n if (success) {\n return shader;\n }\n\n console.error(gl.getShaderInfoLog(shader));\n gl.deleteShader(shader);\n return null;\n}\n\nfunction createProgram(\n gl: WebGL2RenderingContext,\n shaderSources: [string, string],\n transformFeedbackVaryings?: string[] | null,\n attribLocations?: Record\n): WebGLProgram | null {\n const program = gl.createProgram();\n if (!program) return null;\n\n [gl.VERTEX_SHADER, gl.FRAGMENT_SHADER].forEach((type, ndx) => {\n const shader = createShader(gl, type, shaderSources[ndx]);\n if (shader) {\n gl.attachShader(program, shader);\n }\n });\n\n if (transformFeedbackVaryings) {\n gl.transformFeedbackVaryings(program, transformFeedbackVaryings, gl.SEPARATE_ATTRIBS);\n }\n\n if (attribLocations) {\n for (const attrib in attribLocations) {\n if (Object.prototype.hasOwnProperty.call(attribLocations, attrib)) {\n gl.bindAttribLocation(program, attribLocations[attrib], attrib);\n }\n }\n }\n\n gl.linkProgram(program);\n const success = gl.getProgramParameter(program, gl.LINK_STATUS);\n\n if (success) {\n return program;\n }\n\n console.error(gl.getProgramInfoLog(program));\n gl.deleteProgram(program);\n return null;\n}\n\nfunction makeVertexArray(\n gl: WebGL2RenderingContext,\n bufLocNumElmPairs: Array<[WebGLBuffer, number, number]>,\n indices?: Uint16Array\n): WebGLVertexArrayObject | null {\n const va = gl.createVertexArray();\n if (!va) return null;\n\n gl.bindVertexArray(va);\n\n for (const [buffer, loc, numElem] of bufLocNumElmPairs) {\n if (loc === -1) continue;\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.enableVertexAttribArray(loc);\n gl.vertexAttribPointer(loc, numElem, gl.FLOAT, false, 0, 0);\n }\n\n if (indices) {\n const indexBuffer = gl.createBuffer();\n if (indexBuffer) {\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);\n gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);\n }\n }\n\n gl.bindVertexArray(null);\n return va;\n}\n\nfunction resizeCanvasToDisplaySize(canvas: HTMLCanvasElement): boolean {\n const dpr = Math.min(2, window.devicePixelRatio || 1);\n const displayWidth = Math.round(canvas.clientWidth * dpr);\n const displayHeight = Math.round(canvas.clientHeight * dpr);\n const needResize = canvas.width !== displayWidth || canvas.height !== displayHeight;\n if (needResize) {\n canvas.width = displayWidth;\n canvas.height = displayHeight;\n }\n return needResize;\n}\n\nfunction makeBuffer(gl: WebGL2RenderingContext, sizeOrData: number | ArrayBufferView, usage: number): WebGLBuffer {\n const buf = gl.createBuffer();\n if (!buf) {\n throw new Error('Failed to create WebGL buffer.');\n }\n gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n\n if (typeof sizeOrData === 'number') {\n gl.bufferData(gl.ARRAY_BUFFER, sizeOrData, usage);\n } else {\n gl.bufferData(gl.ARRAY_BUFFER, sizeOrData, usage);\n }\n\n gl.bindBuffer(gl.ARRAY_BUFFER, null);\n return buf;\n}\n\nfunction createAndSetupTexture(\n gl: WebGL2RenderingContext,\n minFilter: number,\n magFilter: number,\n wrapS: number,\n wrapT: number\n): WebGLTexture {\n const texture = gl.createTexture();\n if (!texture) {\n throw new Error('Failed to create WebGL texture.');\n }\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapS);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapT);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, magFilter);\n return texture;\n}\n\ntype UpdateCallback = (deltaTime: number) => void;\n\nclass ArcballControl {\n private canvas: HTMLCanvasElement;\n private updateCallback: UpdateCallback;\n\n public isPointerDown = false;\n public orientation = quat.create();\n public pointerRotation = quat.create();\n public rotationVelocity = 0;\n public rotationAxis = vec3.fromValues(1, 0, 0);\n\n public snapDirection = vec3.fromValues(0, 0, -1);\n public snapTargetDirection: vec3 | null = null;\n\n private pointerPos = vec2.create();\n private previousPointerPos = vec2.create();\n private _rotationVelocity = 0;\n private _combinedQuat = quat.create();\n\n private readonly EPSILON = 0.1;\n private readonly IDENTITY_QUAT = quat.create();\n\n constructor(canvas: HTMLCanvasElement, updateCallback?: UpdateCallback) {\n this.canvas = canvas;\n this.updateCallback = updateCallback || (() => undefined);\n\n canvas.addEventListener('pointerdown', (e: PointerEvent) => {\n vec2.set(this.pointerPos, e.clientX, e.clientY);\n vec2.copy(this.previousPointerPos, this.pointerPos);\n this.isPointerDown = true;\n });\n canvas.addEventListener('pointerup', () => {\n this.isPointerDown = false;\n });\n canvas.addEventListener('pointerleave', () => {\n this.isPointerDown = false;\n });\n canvas.addEventListener('pointermove', (e: PointerEvent) => {\n if (this.isPointerDown) {\n vec2.set(this.pointerPos, e.clientX, e.clientY);\n }\n });\n\n canvas.style.touchAction = 'none';\n }\n\n public update(deltaTime: number, targetFrameDuration = 16): void {\n const timeScale = deltaTime / targetFrameDuration + 0.00001;\n let angleFactor = timeScale;\n const snapRotation = quat.create();\n\n if (this.isPointerDown) {\n const INTENSITY = 0.3 * timeScale;\n const ANGLE_AMPLIFICATION = 5 / timeScale;\n\n const midPointerPos = vec2.sub(vec2.create(), this.pointerPos, this.previousPointerPos);\n vec2.scale(midPointerPos, midPointerPos, INTENSITY);\n\n if (vec2.sqrLen(midPointerPos) > this.EPSILON) {\n vec2.add(midPointerPos, this.previousPointerPos, midPointerPos);\n\n const p = this.project(midPointerPos);\n const q = this.project(this.previousPointerPos);\n const a = vec3.normalize(vec3.create(), p);\n const b = vec3.normalize(vec3.create(), q);\n\n vec2.copy(this.previousPointerPos, midPointerPos);\n\n angleFactor *= ANGLE_AMPLIFICATION;\n\n this.quatFromVectors(a, b, this.pointerRotation, angleFactor);\n } else {\n quat.slerp(this.pointerRotation, this.pointerRotation, this.IDENTITY_QUAT, INTENSITY);\n }\n } else {\n const INTENSITY = 0.1 * timeScale;\n quat.slerp(this.pointerRotation, this.pointerRotation, this.IDENTITY_QUAT, INTENSITY);\n\n if (this.snapTargetDirection) {\n const SNAPPING_INTENSITY = 0.2;\n const a = this.snapTargetDirection;\n const b = this.snapDirection;\n const sqrDist = vec3.squaredDistance(a, b);\n const distanceFactor = Math.max(0.1, 1 - sqrDist * 10);\n angleFactor *= SNAPPING_INTENSITY * distanceFactor;\n this.quatFromVectors(a, b, snapRotation, angleFactor);\n }\n }\n\n const combinedQuat = quat.multiply(quat.create(), snapRotation, this.pointerRotation);\n this.orientation = quat.multiply(quat.create(), combinedQuat, this.orientation);\n quat.normalize(this.orientation, this.orientation);\n\n const RA_INTENSITY = 0.8 * timeScale;\n quat.slerp(this._combinedQuat, this._combinedQuat, combinedQuat, RA_INTENSITY);\n quat.normalize(this._combinedQuat, this._combinedQuat);\n\n const rad = Math.acos(this._combinedQuat[3]) * 2.0;\n const s = Math.sin(rad / 2.0);\n let rv = 0;\n if (s > 0.000001) {\n rv = rad / (2 * Math.PI);\n this.rotationAxis[0] = this._combinedQuat[0] / s;\n this.rotationAxis[1] = this._combinedQuat[1] / s;\n this.rotationAxis[2] = this._combinedQuat[2] / s;\n }\n\n const RV_INTENSITY = 0.5 * timeScale;\n this._rotationVelocity += (rv - this._rotationVelocity) * RV_INTENSITY;\n this.rotationVelocity = this._rotationVelocity / timeScale;\n\n this.updateCallback(deltaTime);\n }\n\n private quatFromVectors(a: vec3, b: vec3, out: quat, angleFactor = 1): { q: quat; axis: vec3; angle: number } {\n const axis = vec3.cross(vec3.create(), a, b);\n vec3.normalize(axis, axis);\n const d = Math.max(-1, Math.min(1, vec3.dot(a, b)));\n const angle = Math.acos(d) * angleFactor;\n quat.setAxisAngle(out, axis, angle);\n return { q: out, axis, angle };\n }\n\n private project(pos: vec2): vec3 {\n const r = 2;\n const w = this.canvas.clientWidth;\n const h = this.canvas.clientHeight;\n const s = Math.max(w, h) - 1;\n\n const x = (2 * pos[0] - w - 1) / s;\n const y = (2 * pos[1] - h - 1) / s;\n let z = 0;\n const xySq = x * x + y * y;\n const rSq = r * r;\n\n if (xySq <= rSq / 2.0) {\n z = Math.sqrt(rSq - xySq);\n } else {\n z = rSq / Math.sqrt(xySq);\n }\n return vec3.fromValues(-x, y, z);\n }\n}\n\ninterface MenuItem {\n image: string;\n link: string;\n title: string;\n description: string;\n}\n\ntype ActiveItemCallback = (index: number) => void;\ntype MovementChangeCallback = (isMoving: boolean) => void;\ntype InitCallback = (instance: InfiniteGridMenu) => void;\n\ninterface Camera {\n matrix: mat4;\n near: number;\n far: number;\n fov: number;\n aspect: number;\n position: vec3;\n up: vec3;\n matrices: {\n view: mat4;\n projection: mat4;\n inversProjection: mat4;\n };\n}\n\nclass InfiniteGridMenu {\n private gl: WebGL2RenderingContext | null = null;\n private discProgram: WebGLProgram | null = null;\n private discVAO: WebGLVertexArrayObject | null = null;\n private discBuffers!: {\n vertices: Float32Array;\n indices: Uint16Array;\n normals: Float32Array;\n uvs: Float32Array;\n };\n private icoGeo!: IcosahedronGeometry;\n private discGeo!: DiscGeometry;\n private worldMatrix = mat4.create();\n private tex: WebGLTexture | null = null;\n private control!: ArcballControl;\n\n private discLocations!: {\n aModelPosition: number;\n aModelUvs: number;\n aInstanceMatrix: number;\n uWorldMatrix: WebGLUniformLocation | null;\n uViewMatrix: WebGLUniformLocation | null;\n uProjectionMatrix: WebGLUniformLocation | null;\n uCameraPosition: WebGLUniformLocation | null;\n uScaleFactor: WebGLUniformLocation | null;\n uRotationAxisVelocity: WebGLUniformLocation | null;\n uTex: WebGLUniformLocation | null;\n uFrames: WebGLUniformLocation | null;\n uItemCount: WebGLUniformLocation | null;\n uAtlasSize: WebGLUniformLocation | null;\n };\n\n private viewportSize = vec2.create();\n private drawBufferSize = vec2.create();\n\n private discInstances!: {\n matricesArray: Float32Array;\n matrices: Float32Array[];\n buffer: WebGLBuffer | null;\n };\n\n private instancePositions: vec3[] = [];\n private DISC_INSTANCE_COUNT = 0;\n private atlasSize = 1;\n\n private _time = 0;\n private _deltaTime = 0;\n private _deltaFrames = 0;\n private _frames = 0;\n\n private movementActive = false;\n\n private TARGET_FRAME_DURATION = 1000 / 60;\n private SPHERE_RADIUS = 2;\n\n public camera: Camera = {\n matrix: mat4.create(),\n near: 0.1,\n far: 40,\n fov: Math.PI / 4,\n aspect: 1,\n position: vec3.fromValues(0, 0, 3),\n up: vec3.fromValues(0, 1, 0),\n matrices: {\n view: mat4.create(),\n projection: mat4.create(),\n inversProjection: mat4.create()\n }\n };\n\n public smoothRotationVelocity = 0;\n public scaleFactor = 1.0;\n\n constructor(\n private canvas: HTMLCanvasElement,\n private items: MenuItem[],\n private onActiveItemChange: ActiveItemCallback,\n private onMovementChange: MovementChangeCallback,\n onInit?: InitCallback,\n scale: number = 1.0\n ) {\n this.scaleFactor = scale;\n this.camera.position[2] = 3 * scale;\n this.init(onInit);\n }\n\n public resize(): void {\n const needsResize = resizeCanvasToDisplaySize(this.canvas);\n if (!this.gl) return;\n if (needsResize) {\n this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight);\n }\n this.updateProjectionMatrix();\n }\n\n public run(time = 0): void {\n this._deltaTime = Math.min(32, time - this._time);\n this._time = time;\n this._deltaFrames = this._deltaTime / this.TARGET_FRAME_DURATION;\n this._frames += this._deltaFrames;\n\n this.animate(this._deltaTime);\n this.render();\n\n requestAnimationFrame(t => this.run(t));\n }\n\n private init(onInit?: InitCallback): void {\n const gl = this.canvas.getContext('webgl2', {\n antialias: true,\n alpha: false\n });\n if (!gl) {\n throw new Error('No WebGL 2 context!');\n }\n this.gl = gl;\n\n vec2.set(this.viewportSize, this.canvas.clientWidth, this.canvas.clientHeight);\n vec2.clone(this.drawBufferSize);\n\n this.discProgram = createProgram(gl, [discVertShaderSource, discFragShaderSource], null, {\n aModelPosition: 0,\n aModelNormal: 1,\n aModelUvs: 2,\n aInstanceMatrix: 3\n });\n\n this.discLocations = {\n aModelPosition: gl.getAttribLocation(this.discProgram!, 'aModelPosition'),\n aModelUvs: gl.getAttribLocation(this.discProgram!, 'aModelUvs'),\n aInstanceMatrix: gl.getAttribLocation(this.discProgram!, 'aInstanceMatrix'),\n uWorldMatrix: gl.getUniformLocation(this.discProgram!, 'uWorldMatrix'),\n uViewMatrix: gl.getUniformLocation(this.discProgram!, 'uViewMatrix'),\n uProjectionMatrix: gl.getUniformLocation(this.discProgram!, 'uProjectionMatrix'),\n uCameraPosition: gl.getUniformLocation(this.discProgram!, 'uCameraPosition'),\n uScaleFactor: gl.getUniformLocation(this.discProgram!, 'uScaleFactor'),\n uRotationAxisVelocity: gl.getUniformLocation(this.discProgram!, 'uRotationAxisVelocity'),\n uTex: gl.getUniformLocation(this.discProgram!, 'uTex'),\n uFrames: gl.getUniformLocation(this.discProgram!, 'uFrames'),\n uItemCount: gl.getUniformLocation(this.discProgram!, 'uItemCount'),\n uAtlasSize: gl.getUniformLocation(this.discProgram!, 'uAtlasSize')\n };\n\n this.discGeo = new DiscGeometry(56, 1);\n this.discBuffers = this.discGeo.data;\n this.discVAO = makeVertexArray(\n gl,\n [\n [makeBuffer(gl, this.discBuffers.vertices, gl.STATIC_DRAW), this.discLocations.aModelPosition, 3],\n [makeBuffer(gl, this.discBuffers.uvs, gl.STATIC_DRAW), this.discLocations.aModelUvs, 2]\n ],\n this.discBuffers.indices\n );\n\n this.icoGeo = new IcosahedronGeometry();\n this.icoGeo.subdivide(1).spherize(this.SPHERE_RADIUS);\n this.instancePositions = this.icoGeo.vertices.map(v => v.position);\n this.DISC_INSTANCE_COUNT = this.icoGeo.vertices.length;\n this.initDiscInstances(this.DISC_INSTANCE_COUNT);\n this.initTexture();\n\n this.control = new ArcballControl(this.canvas, deltaTime => this.onControlUpdate(deltaTime));\n\n this.updateCameraMatrix();\n this.updateProjectionMatrix();\n\n this.resize();\n\n if (onInit) {\n onInit(this);\n }\n }\n\n private initTexture(): void {\n if (!this.gl) return;\n const gl = this.gl;\n this.tex = createAndSetupTexture(gl, gl.LINEAR, gl.LINEAR, gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE);\n\n const itemCount = Math.max(1, this.items.length);\n this.atlasSize = Math.ceil(Math.sqrt(itemCount));\n const cellSize = 512;\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d')!;\n canvas.width = this.atlasSize * cellSize;\n canvas.height = this.atlasSize * cellSize;\n\n Promise.all(\n this.items.map(\n item =>\n new Promise(resolve => {\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => resolve(img);\n img.src = item.image;\n })\n )\n ).then(images => {\n images.forEach((img, i) => {\n const x = (i % this.atlasSize) * cellSize;\n const y = Math.floor(i / this.atlasSize) * cellSize;\n ctx.drawImage(img, x, y, cellSize, cellSize);\n });\n\n gl.bindTexture(gl.TEXTURE_2D, this.tex);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);\n gl.generateMipmap(gl.TEXTURE_2D);\n });\n }\n\n private initDiscInstances(count: number): void {\n if (!this.gl || !this.discVAO) return;\n const gl = this.gl;\n\n const matricesArray = new Float32Array(count * 16);\n const matrices: Float32Array[] = [];\n for (let i = 0; i < count; ++i) {\n const instanceMatrixArray = new Float32Array(matricesArray.buffer, i * 16 * 4, 16);\n mat4.identity(instanceMatrixArray as unknown as mat4);\n matrices.push(instanceMatrixArray);\n }\n\n this.discInstances = {\n matricesArray,\n matrices,\n buffer: gl.createBuffer()\n };\n\n gl.bindVertexArray(this.discVAO);\n gl.bindBuffer(gl.ARRAY_BUFFER, this.discInstances.buffer);\n gl.bufferData(gl.ARRAY_BUFFER, this.discInstances.matricesArray.byteLength, gl.DYNAMIC_DRAW);\n\n const mat4AttribSlotCount = 4;\n const bytesPerMatrix = 16 * 4;\n for (let j = 0; j < mat4AttribSlotCount; ++j) {\n const loc = this.discLocations.aInstanceMatrix + j;\n gl.enableVertexAttribArray(loc);\n gl.vertexAttribPointer(loc, 4, gl.FLOAT, false, bytesPerMatrix, j * 4 * 4);\n gl.vertexAttribDivisor(loc, 1);\n }\n gl.bindBuffer(gl.ARRAY_BUFFER, null);\n gl.bindVertexArray(null);\n }\n\n private animate(deltaTime: number): void {\n if (!this.gl) return;\n this.control.update(deltaTime, this.TARGET_FRAME_DURATION);\n\n const positions = this.instancePositions.map(p => vec3.transformQuat(vec3.create(), p, this.control.orientation));\n const scale = 0.25;\n const SCALE_INTENSITY = 0.6;\n\n positions.forEach((p, ndx) => {\n const s = (Math.abs(p[2]) / this.SPHERE_RADIUS) * SCALE_INTENSITY + (1 - SCALE_INTENSITY);\n const finalScale = s * scale;\n const matrix = mat4.create();\n\n mat4.multiply(matrix, matrix, mat4.fromTranslation(mat4.create(), vec3.negate(vec3.create(), p)));\n mat4.multiply(matrix, matrix, mat4.targetTo(mat4.create(), [0, 0, 0], p, [0, 1, 0]));\n mat4.multiply(matrix, matrix, mat4.fromScaling(mat4.create(), [finalScale, finalScale, finalScale]));\n mat4.multiply(matrix, matrix, mat4.fromTranslation(mat4.create(), [0, 0, -this.SPHERE_RADIUS]));\n\n mat4.copy(this.discInstances.matrices[ndx], matrix);\n });\n\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.discInstances.buffer);\n this.gl.bufferSubData(this.gl.ARRAY_BUFFER, 0, this.discInstances.matricesArray);\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);\n\n this.smoothRotationVelocity = this.control.rotationVelocity;\n }\n\n private render(): void {\n if (!this.gl || !this.discProgram) return;\n const gl = this.gl;\n\n gl.useProgram(this.discProgram);\n gl.enable(gl.CULL_FACE);\n gl.enable(gl.DEPTH_TEST);\n\n gl.clearColor(0, 0, 0, 0);\n gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);\n\n gl.uniformMatrix4fv(this.discLocations.uWorldMatrix, false, this.worldMatrix);\n gl.uniformMatrix4fv(this.discLocations.uViewMatrix, false, this.camera.matrices.view);\n gl.uniformMatrix4fv(this.discLocations.uProjectionMatrix, false, this.camera.matrices.projection);\n gl.uniform3f(\n this.discLocations.uCameraPosition,\n this.camera.position[0],\n this.camera.position[1],\n this.camera.position[2]\n );\n gl.uniform4f(\n this.discLocations.uRotationAxisVelocity,\n this.control.rotationAxis[0],\n this.control.rotationAxis[1],\n this.control.rotationAxis[2],\n this.smoothRotationVelocity * 1.1\n );\n\n gl.uniform1i(this.discLocations.uItemCount, this.items.length);\n gl.uniform1i(this.discLocations.uAtlasSize, this.atlasSize);\n\n gl.uniform1f(this.discLocations.uFrames, this._frames);\n gl.uniform1f(this.discLocations.uScaleFactor, this.scaleFactor);\n\n gl.uniform1i(this.discLocations.uTex, 0);\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, this.tex);\n\n gl.bindVertexArray(this.discVAO);\n gl.drawElementsInstanced(\n gl.TRIANGLES,\n this.discBuffers.indices.length,\n gl.UNSIGNED_SHORT,\n 0,\n this.DISC_INSTANCE_COUNT\n );\n gl.bindVertexArray(null);\n }\n\n private updateCameraMatrix(): void {\n mat4.targetTo(this.camera.matrix, this.camera.position, [0, 0, 0], this.camera.up);\n mat4.invert(this.camera.matrices.view, this.camera.matrix);\n }\n\n private updateProjectionMatrix(): void {\n if (!this.gl) return;\n const canvasEl = this.gl.canvas as HTMLCanvasElement;\n this.camera.aspect = canvasEl.clientWidth / canvasEl.clientHeight;\n const height = this.SPHERE_RADIUS * 0.35;\n const distance = this.camera.position[2];\n if (this.camera.aspect > 1) {\n this.camera.fov = 2 * Math.atan(height / distance);\n } else {\n this.camera.fov = 2 * Math.atan(height / this.camera.aspect / distance);\n }\n mat4.perspective(\n this.camera.matrices.projection,\n this.camera.fov,\n this.camera.aspect,\n this.camera.near,\n this.camera.far\n );\n mat4.invert(this.camera.matrices.inversProjection, this.camera.matrices.projection);\n }\n\n private onControlUpdate(deltaTime: number): void {\n const timeScale = deltaTime / this.TARGET_FRAME_DURATION + 0.0001;\n let damping = 5 / timeScale;\n let cameraTargetZ = 3 * this.scaleFactor;\n\n const isMoving = this.control.isPointerDown || Math.abs(this.smoothRotationVelocity) > 0.01;\n\n if (isMoving !== this.movementActive) {\n this.movementActive = isMoving;\n this.onMovementChange(isMoving);\n }\n\n if (!this.control.isPointerDown) {\n const nearestVertexIndex = this.findNearestVertexIndex();\n const itemIndex = nearestVertexIndex % Math.max(1, this.items.length);\n this.onActiveItemChange(itemIndex);\n const snapDirection = vec3.normalize(vec3.create(), this.getVertexWorldPosition(nearestVertexIndex));\n this.control.snapTargetDirection = snapDirection;\n } else {\n cameraTargetZ += this.control.rotationVelocity * 80 + 2.5;\n damping = 7 / timeScale;\n }\n\n this.camera.position[2] += (cameraTargetZ - this.camera.position[2]) / damping;\n this.updateCameraMatrix();\n }\n\n private findNearestVertexIndex(): number {\n const n = this.control.snapDirection;\n const inversOrientation = quat.conjugate(quat.create(), this.control.orientation);\n const nt = vec3.transformQuat(vec3.create(), n, inversOrientation);\n\n let maxD = -1;\n let nearestVertexIndex = 0;\n for (let i = 0; i < this.instancePositions.length; ++i) {\n const d = vec3.dot(nt, this.instancePositions[i]);\n if (d > maxD) {\n maxD = d;\n nearestVertexIndex = i;\n }\n }\n return nearestVertexIndex;\n }\n\n private getVertexWorldPosition(index: number): vec3 {\n const nearestVertexPos = this.instancePositions[index];\n return vec3.transformQuat(vec3.create(), nearestVertexPos, this.control.orientation);\n }\n}\n\nconst defaultItems: MenuItem[] = [\n {\n image:\n 'https://images.unsplash.com/photo-1782977389500-dd7adad33ebe?q=80&w=600&h=600&fit=crop&sat=-100&auto=format',\n link: 'https://google.com/',\n title: '',\n description: ''\n }\n];\n\ninterface InfiniteMenuProps {\n items?: MenuItem[];\n scale?: number;\n}\n\nconst InfiniteMenu: FC = ({ items = [], scale = 1.0 }) => {\n const canvasRef = useRef(null) as MutableRefObject;\n const [activeItem, setActiveItem] = useState(null);\n const [isMoving, setIsMoving] = useState(false);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n let sketch: InfiniteGridMenu | null = null;\n\n const handleActiveItem = (index: number) => {\n if (!items.length) return;\n const itemIndex = index % items.length;\n setActiveItem(items[itemIndex]);\n };\n\n if (canvas) {\n sketch = new InfiniteGridMenu(\n canvas,\n items.length ? items : defaultItems,\n handleActiveItem,\n setIsMoving,\n sk => sk.run(),\n scale\n );\n }\n\n const handleResize = () => {\n if (sketch) {\n sketch.resize();\n }\n };\n\n window.addEventListener('resize', handleResize);\n handleResize();\n\n return () => {\n window.removeEventListener('resize', handleResize);\n };\n }, [items, scale]);\n\n const handleButtonClick = () => {\n if (!activeItem?.link) return;\n if (activeItem.link.startsWith('http')) {\n window.open(activeItem.link, '_blank');\n } else {\n console.log('Internal route:', activeItem.link);\n }\n };\n\n return (\n
\n \n\n {activeItem && (\n <>\n

{activeItem.title}

\n\n

{activeItem.description}

\n\n
\n

\n
\n \n )}\n
\n );\n};\n\nexport default InfiniteMenu;\n" + "path": "InfiniteMenu.tsx", + "content": "import { type FC, useRef, useState, useEffect, type MutableRefObject } from 'react';\nimport { mat4, quat, vec2, vec3 } from 'gl-matrix';\nimport './InfiniteMenu.css';\n\nconst discVertShaderSource = `#version 300 es\n\nuniform mat4 uWorldMatrix;\nuniform mat4 uViewMatrix;\nuniform mat4 uProjectionMatrix;\nuniform vec3 uCameraPosition;\nuniform vec4 uRotationAxisVelocity;\n\nin vec3 aModelPosition;\nin vec3 aModelNormal;\nin vec2 aModelUvs;\nin mat4 aInstanceMatrix;\n\nout vec2 vUvs;\nout float vAlpha;\nflat out int vInstanceId;\n\n#define PI 3.141593\n\nvoid main() {\n vec4 worldPosition = uWorldMatrix * aInstanceMatrix * vec4(aModelPosition, 1.);\n\n vec3 centerPos = (uWorldMatrix * aInstanceMatrix * vec4(0., 0., 0., 1.)).xyz;\n float radius = length(centerPos.xyz);\n\n if (gl_VertexID > 0) {\n vec3 rotationAxis = uRotationAxisVelocity.xyz;\n float rotationVelocity = min(.15, uRotationAxisVelocity.w * 15.);\n vec3 stretchDir = normalize(cross(centerPos, rotationAxis));\n vec3 relativeVertexPos = normalize(worldPosition.xyz - centerPos);\n float strength = dot(stretchDir, relativeVertexPos);\n float invAbsStrength = min(0., abs(strength) - 1.);\n strength = rotationVelocity * sign(strength) * abs(invAbsStrength * invAbsStrength * invAbsStrength + 1.);\n worldPosition.xyz += stretchDir * strength;\n }\n\n worldPosition.xyz = radius * normalize(worldPosition.xyz);\n\n gl_Position = uProjectionMatrix * uViewMatrix * worldPosition;\n\n vAlpha = smoothstep(0.5, 1., normalize(worldPosition.xyz).z) * .9 + .1;\n vUvs = aModelUvs;\n vInstanceId = gl_InstanceID;\n}\n`;\n\nconst discFragShaderSource = `#version 300 es\nprecision highp float;\n\nuniform sampler2D uTex;\nuniform int uItemCount;\nuniform int uAtlasSize;\n\nout vec4 outColor;\n\nin vec2 vUvs;\nin float vAlpha;\nflat in int vInstanceId;\n\nvoid main() {\n int itemIndex = vInstanceId % uItemCount;\n int cellsPerRow = uAtlasSize;\n int cellX = itemIndex % cellsPerRow;\n int cellY = itemIndex / cellsPerRow;\n vec2 cellSize = vec2(1.0) / vec2(float(cellsPerRow));\n vec2 cellOffset = vec2(float(cellX), float(cellY)) * cellSize;\n\n ivec2 texSize = textureSize(uTex, 0);\n float imageAspect = float(texSize.x) / float(texSize.y);\n float containerAspect = 1.0;\n \n float scale = max(imageAspect / containerAspect, \n containerAspect / imageAspect);\n \n vec2 st = vec2(vUvs.x, 1.0 - vUvs.y);\n st = (st - 0.5) * scale + 0.5;\n \n st = clamp(st, 0.0, 1.0);\n \n st = st * cellSize + cellOffset;\n \n outColor = texture(uTex, st);\n outColor.a *= vAlpha;\n}\n`;\n\nclass Face {\n public a: number;\n public b: number;\n public c: number;\n\n constructor(a: number, b: number, c: number) {\n this.a = a;\n this.b = b;\n this.c = c;\n }\n}\n\nclass Vertex {\n public position: vec3;\n public normal: vec3;\n public uv: vec2;\n\n constructor(x: number, y: number, z: number) {\n this.position = vec3.fromValues(x, y, z);\n this.normal = vec3.create();\n this.uv = vec2.create();\n }\n}\n\nclass Geometry {\n public vertices: Vertex[];\n public faces: Face[];\n\n constructor() {\n this.vertices = [];\n this.faces = [];\n }\n\n public addVertex(...args: number[]): this {\n for (let i = 0; i < args.length; i += 3) {\n this.vertices.push(new Vertex(args[i], args[i + 1], args[i + 2]));\n }\n return this;\n }\n\n public addFace(...args: number[]): this {\n for (let i = 0; i < args.length; i += 3) {\n this.faces.push(new Face(args[i], args[i + 1], args[i + 2]));\n }\n return this;\n }\n\n public get lastVertex(): Vertex {\n return this.vertices[this.vertices.length - 1];\n }\n\n public subdivide(divisions = 1): this {\n const midPointCache: Record = {};\n let f = this.faces;\n\n for (let div = 0; div < divisions; ++div) {\n const newFaces = new Array(f.length * 4);\n\n f.forEach((face, ndx) => {\n const mAB = this.getMidPoint(face.a, face.b, midPointCache);\n const mBC = this.getMidPoint(face.b, face.c, midPointCache);\n const mCA = this.getMidPoint(face.c, face.a, midPointCache);\n\n const i = ndx * 4;\n newFaces[i + 0] = new Face(face.a, mAB, mCA);\n newFaces[i + 1] = new Face(face.b, mBC, mAB);\n newFaces[i + 2] = new Face(face.c, mCA, mBC);\n newFaces[i + 3] = new Face(mAB, mBC, mCA);\n });\n\n f = newFaces;\n }\n\n this.faces = f;\n return this;\n }\n\n public spherize(radius = 1): this {\n this.vertices.forEach(vertex => {\n vec3.normalize(vertex.normal, vertex.position);\n vec3.scale(vertex.position, vertex.normal, radius);\n });\n return this;\n }\n\n public get data(): {\n vertices: Float32Array;\n indices: Uint16Array;\n normals: Float32Array;\n uvs: Float32Array;\n } {\n return {\n vertices: this.vertexData,\n indices: this.indexData,\n normals: this.normalData,\n uvs: this.uvData\n };\n }\n\n public get vertexData(): Float32Array {\n return new Float32Array(this.vertices.flatMap(v => Array.from(v.position)));\n }\n\n public get normalData(): Float32Array {\n return new Float32Array(this.vertices.flatMap(v => Array.from(v.normal)));\n }\n\n public get uvData(): Float32Array {\n return new Float32Array(this.vertices.flatMap(v => Array.from(v.uv)));\n }\n\n public get indexData(): Uint16Array {\n return new Uint16Array(this.faces.flatMap(f => [f.a, f.b, f.c]));\n }\n\n public getMidPoint(ndxA: number, ndxB: number, cache: Record): number {\n const cacheKey = ndxA < ndxB ? `k_${ndxB}_${ndxA}` : `k_${ndxA}_${ndxB}`;\n if (Object.prototype.hasOwnProperty.call(cache, cacheKey)) {\n return cache[cacheKey];\n }\n const a = this.vertices[ndxA].position;\n const b = this.vertices[ndxB].position;\n const ndx = this.vertices.length;\n cache[cacheKey] = ndx;\n this.addVertex((a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5, (a[2] + b[2]) * 0.5);\n return ndx;\n }\n}\n\nclass IcosahedronGeometry extends Geometry {\n constructor() {\n super();\n const t = Math.sqrt(5) * 0.5 + 0.5;\n this.addVertex(\n -1,\n t,\n 0,\n 1,\n t,\n 0,\n -1,\n -t,\n 0,\n 1,\n -t,\n 0,\n 0,\n -1,\n t,\n 0,\n 1,\n t,\n 0,\n -1,\n -t,\n 0,\n 1,\n -t,\n t,\n 0,\n -1,\n t,\n 0,\n 1,\n -t,\n 0,\n -1,\n -t,\n 0,\n 1\n ).addFace(\n 0,\n 11,\n 5,\n 0,\n 5,\n 1,\n 0,\n 1,\n 7,\n 0,\n 7,\n 10,\n 0,\n 10,\n 11,\n 1,\n 5,\n 9,\n 5,\n 11,\n 4,\n 11,\n 10,\n 2,\n 10,\n 7,\n 6,\n 7,\n 1,\n 8,\n 3,\n 9,\n 4,\n 3,\n 4,\n 2,\n 3,\n 2,\n 6,\n 3,\n 6,\n 8,\n 3,\n 8,\n 9,\n 4,\n 9,\n 5,\n 2,\n 4,\n 11,\n 6,\n 2,\n 10,\n 8,\n 6,\n 7,\n 9,\n 8,\n 1\n );\n }\n}\n\nclass DiscGeometry extends Geometry {\n constructor(steps = 4, radius = 1) {\n super();\n const safeSteps = Math.max(4, steps);\n const alpha = (2 * Math.PI) / safeSteps;\n\n this.addVertex(0, 0, 0);\n this.lastVertex.uv[0] = 0.5;\n this.lastVertex.uv[1] = 0.5;\n\n for (let i = 0; i < safeSteps; ++i) {\n const x = Math.cos(alpha * i);\n const y = Math.sin(alpha * i);\n this.addVertex(radius * x, radius * y, 0);\n this.lastVertex.uv[0] = x * 0.5 + 0.5;\n this.lastVertex.uv[1] = y * 0.5 + 0.5;\n\n if (i > 0) {\n this.addFace(0, i, i + 1);\n }\n }\n this.addFace(0, safeSteps, 1);\n }\n}\n\nfunction createShader(gl: WebGL2RenderingContext, type: number, source: string): WebGLShader | null {\n const shader = gl.createShader(type);\n if (!shader) return null;\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);\n\n if (success) {\n return shader;\n }\n\n console.error(gl.getShaderInfoLog(shader));\n gl.deleteShader(shader);\n return null;\n}\n\nfunction createProgram(\n gl: WebGL2RenderingContext,\n shaderSources: [string, string],\n transformFeedbackVaryings?: string[] | null,\n attribLocations?: Record\n): WebGLProgram | null {\n const program = gl.createProgram();\n if (!program) return null;\n\n [gl.VERTEX_SHADER, gl.FRAGMENT_SHADER].forEach((type, ndx) => {\n const shader = createShader(gl, type, shaderSources[ndx]);\n if (shader) {\n gl.attachShader(program, shader);\n }\n });\n\n if (transformFeedbackVaryings) {\n gl.transformFeedbackVaryings(program, transformFeedbackVaryings, gl.SEPARATE_ATTRIBS);\n }\n\n if (attribLocations) {\n for (const attrib in attribLocations) {\n if (Object.prototype.hasOwnProperty.call(attribLocations, attrib)) {\n gl.bindAttribLocation(program, attribLocations[attrib], attrib);\n }\n }\n }\n\n gl.linkProgram(program);\n const success = gl.getProgramParameter(program, gl.LINK_STATUS);\n\n if (success) {\n return program;\n }\n\n console.error(gl.getProgramInfoLog(program));\n gl.deleteProgram(program);\n return null;\n}\n\nfunction makeVertexArray(\n gl: WebGL2RenderingContext,\n bufLocNumElmPairs: Array<[WebGLBuffer, number, number]>,\n indices?: Uint16Array\n): WebGLVertexArrayObject | null {\n const va = gl.createVertexArray();\n if (!va) return null;\n\n gl.bindVertexArray(va);\n\n for (const [buffer, loc, numElem] of bufLocNumElmPairs) {\n if (loc === -1) continue;\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.enableVertexAttribArray(loc);\n gl.vertexAttribPointer(loc, numElem, gl.FLOAT, false, 0, 0);\n }\n\n if (indices) {\n const indexBuffer = gl.createBuffer();\n if (indexBuffer) {\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);\n gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);\n }\n }\n\n gl.bindVertexArray(null);\n return va;\n}\n\nfunction resizeCanvasToDisplaySize(canvas: HTMLCanvasElement): boolean {\n const dpr = Math.min(2, window.devicePixelRatio || 1);\n const displayWidth = Math.round(canvas.clientWidth * dpr);\n const displayHeight = Math.round(canvas.clientHeight * dpr);\n const needResize = canvas.width !== displayWidth || canvas.height !== displayHeight;\n if (needResize) {\n canvas.width = displayWidth;\n canvas.height = displayHeight;\n }\n return needResize;\n}\n\nfunction makeBuffer(gl: WebGL2RenderingContext, sizeOrData: number | ArrayBufferView, usage: number): WebGLBuffer {\n const buf = gl.createBuffer();\n if (!buf) {\n throw new Error('Failed to create WebGL buffer.');\n }\n gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n\n if (typeof sizeOrData === 'number') {\n gl.bufferData(gl.ARRAY_BUFFER, sizeOrData, usage);\n } else {\n gl.bufferData(gl.ARRAY_BUFFER, sizeOrData, usage);\n }\n\n gl.bindBuffer(gl.ARRAY_BUFFER, null);\n return buf;\n}\n\nfunction createAndSetupTexture(\n gl: WebGL2RenderingContext,\n minFilter: number,\n magFilter: number,\n wrapS: number,\n wrapT: number\n): WebGLTexture {\n const texture = gl.createTexture();\n if (!texture) {\n throw new Error('Failed to create WebGL texture.');\n }\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapS);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapT);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, magFilter);\n return texture;\n}\n\ntype UpdateCallback = (deltaTime: number) => void;\n\nclass ArcballControl {\n private canvas: HTMLCanvasElement;\n private updateCallback: UpdateCallback;\n\n public isPointerDown = false;\n public orientation = quat.create();\n public pointerRotation = quat.create();\n public rotationVelocity = 0;\n public rotationAxis = vec3.fromValues(1, 0, 0);\n\n public snapDirection = vec3.fromValues(0, 0, -1);\n public snapTargetDirection: vec3 | null = null;\n\n private pointerPos = vec2.create();\n private previousPointerPos = vec2.create();\n private _rotationVelocity = 0;\n private _combinedQuat = quat.create();\n\n private readonly EPSILON = 0.1;\n private readonly IDENTITY_QUAT = quat.create();\n\n constructor(canvas: HTMLCanvasElement, updateCallback?: UpdateCallback) {\n this.canvas = canvas;\n this.updateCallback = updateCallback || (() => undefined);\n\n canvas.addEventListener('pointerdown', (e: PointerEvent) => {\n vec2.set(this.pointerPos, e.clientX, e.clientY);\n vec2.copy(this.previousPointerPos, this.pointerPos);\n this.isPointerDown = true;\n });\n canvas.addEventListener('pointerup', () => {\n this.isPointerDown = false;\n });\n canvas.addEventListener('pointerleave', () => {\n this.isPointerDown = false;\n });\n canvas.addEventListener('pointermove', (e: PointerEvent) => {\n if (this.isPointerDown) {\n vec2.set(this.pointerPos, e.clientX, e.clientY);\n }\n });\n\n canvas.style.touchAction = 'none';\n }\n\n public update(deltaTime: number, targetFrameDuration = 16): void {\n const timeScale = deltaTime / targetFrameDuration + 0.00001;\n let angleFactor = timeScale;\n const snapRotation = quat.create();\n\n if (this.isPointerDown) {\n const INTENSITY = 0.3 * timeScale;\n const ANGLE_AMPLIFICATION = 5 / timeScale;\n\n const midPointerPos = vec2.sub(vec2.create(), this.pointerPos, this.previousPointerPos);\n vec2.scale(midPointerPos, midPointerPos, INTENSITY);\n\n if (vec2.sqrLen(midPointerPos) > this.EPSILON) {\n vec2.add(midPointerPos, this.previousPointerPos, midPointerPos);\n\n const p = this.project(midPointerPos);\n const q = this.project(this.previousPointerPos);\n const a = vec3.normalize(vec3.create(), p);\n const b = vec3.normalize(vec3.create(), q);\n\n vec2.copy(this.previousPointerPos, midPointerPos);\n\n angleFactor *= ANGLE_AMPLIFICATION;\n\n this.quatFromVectors(a, b, this.pointerRotation, angleFactor);\n } else {\n quat.slerp(this.pointerRotation, this.pointerRotation, this.IDENTITY_QUAT, INTENSITY);\n }\n } else {\n const INTENSITY = 0.1 * timeScale;\n quat.slerp(this.pointerRotation, this.pointerRotation, this.IDENTITY_QUAT, INTENSITY);\n\n if (this.snapTargetDirection) {\n const SNAPPING_INTENSITY = 0.2;\n const a = this.snapTargetDirection;\n const b = this.snapDirection;\n const sqrDist = vec3.squaredDistance(a, b);\n const distanceFactor = Math.max(0.1, 1 - sqrDist * 10);\n angleFactor *= SNAPPING_INTENSITY * distanceFactor;\n this.quatFromVectors(a, b, snapRotation, angleFactor);\n }\n }\n\n const combinedQuat = quat.multiply(quat.create(), snapRotation, this.pointerRotation);\n this.orientation = quat.multiply(quat.create(), combinedQuat, this.orientation);\n quat.normalize(this.orientation, this.orientation);\n\n const RA_INTENSITY = 0.8 * timeScale;\n quat.slerp(this._combinedQuat, this._combinedQuat, combinedQuat, RA_INTENSITY);\n quat.normalize(this._combinedQuat, this._combinedQuat);\n\n const rad = Math.acos(this._combinedQuat[3]) * 2.0;\n const s = Math.sin(rad / 2.0);\n let rv = 0;\n if (s > 0.000001) {\n rv = rad / (2 * Math.PI);\n this.rotationAxis[0] = this._combinedQuat[0] / s;\n this.rotationAxis[1] = this._combinedQuat[1] / s;\n this.rotationAxis[2] = this._combinedQuat[2] / s;\n }\n\n const RV_INTENSITY = 0.5 * timeScale;\n this._rotationVelocity += (rv - this._rotationVelocity) * RV_INTENSITY;\n this.rotationVelocity = this._rotationVelocity / timeScale;\n\n this.updateCallback(deltaTime);\n }\n\n private quatFromVectors(a: vec3, b: vec3, out: quat, angleFactor = 1): { q: quat; axis: vec3; angle: number } {\n const axis = vec3.cross(vec3.create(), a, b);\n vec3.normalize(axis, axis);\n const d = Math.max(-1, Math.min(1, vec3.dot(a, b)));\n const angle = Math.acos(d) * angleFactor;\n quat.setAxisAngle(out, axis, angle);\n return { q: out, axis, angle };\n }\n\n private project(pos: vec2): vec3 {\n const r = 2;\n const w = this.canvas.clientWidth;\n const h = this.canvas.clientHeight;\n const s = Math.max(w, h) - 1;\n\n const x = (2 * pos[0] - w - 1) / s;\n const y = (2 * pos[1] - h - 1) / s;\n let z = 0;\n const xySq = x * x + y * y;\n const rSq = r * r;\n\n if (xySq <= rSq / 2.0) {\n z = Math.sqrt(rSq - xySq);\n } else {\n z = rSq / Math.sqrt(xySq);\n }\n return vec3.fromValues(-x, y, z);\n }\n}\n\ninterface MenuItem {\n image: string;\n link: string;\n title: string;\n description: string;\n}\n\ntype ActiveItemCallback = (index: number) => void;\ntype MovementChangeCallback = (isMoving: boolean) => void;\ntype InitCallback = (instance: InfiniteGridMenu) => void;\n\ninterface Camera {\n matrix: mat4;\n near: number;\n far: number;\n fov: number;\n aspect: number;\n position: vec3;\n up: vec3;\n matrices: {\n view: mat4;\n projection: mat4;\n inversProjection: mat4;\n };\n}\n\nclass InfiniteGridMenu {\n private gl: WebGL2RenderingContext | null = null;\n private discProgram: WebGLProgram | null = null;\n private discVAO: WebGLVertexArrayObject | null = null;\n private discBuffers!: {\n vertices: Float32Array;\n indices: Uint16Array;\n normals: Float32Array;\n uvs: Float32Array;\n };\n private icoGeo!: IcosahedronGeometry;\n private discGeo!: DiscGeometry;\n private worldMatrix = mat4.create();\n private tex: WebGLTexture | null = null;\n private control!: ArcballControl;\n\n private discLocations!: {\n aModelPosition: number;\n aModelUvs: number;\n aInstanceMatrix: number;\n uWorldMatrix: WebGLUniformLocation | null;\n uViewMatrix: WebGLUniformLocation | null;\n uProjectionMatrix: WebGLUniformLocation | null;\n uCameraPosition: WebGLUniformLocation | null;\n uScaleFactor: WebGLUniformLocation | null;\n uRotationAxisVelocity: WebGLUniformLocation | null;\n uTex: WebGLUniformLocation | null;\n uFrames: WebGLUniformLocation | null;\n uItemCount: WebGLUniformLocation | null;\n uAtlasSize: WebGLUniformLocation | null;\n };\n\n private viewportSize = vec2.create();\n private drawBufferSize = vec2.create();\n\n private discInstances!: {\n matricesArray: Float32Array;\n matrices: Float32Array[];\n buffer: WebGLBuffer | null;\n };\n\n private instancePositions: vec3[] = [];\n private DISC_INSTANCE_COUNT = 0;\n private atlasSize = 1;\n\n private _time = 0;\n private _deltaTime = 0;\n private _deltaFrames = 0;\n private _frames = 0;\n\n private movementActive = false;\n\n private TARGET_FRAME_DURATION = 1000 / 60;\n private SPHERE_RADIUS = 2;\n\n public camera: Camera = {\n matrix: mat4.create(),\n near: 0.1,\n far: 40,\n fov: Math.PI / 4,\n aspect: 1,\n position: vec3.fromValues(0, 0, 3),\n up: vec3.fromValues(0, 1, 0),\n matrices: {\n view: mat4.create(),\n projection: mat4.create(),\n inversProjection: mat4.create()\n }\n };\n\n public smoothRotationVelocity = 0;\n public scaleFactor = 1.0;\n\n constructor(\n private canvas: HTMLCanvasElement,\n private items: MenuItem[],\n private onActiveItemChange: ActiveItemCallback,\n private onMovementChange: MovementChangeCallback,\n onInit?: InitCallback,\n scale: number = 1.0\n ) {\n this.scaleFactor = scale;\n this.camera.position[2] = 3 * scale;\n this.init(onInit);\n }\n\n public resize(): void {\n const needsResize = resizeCanvasToDisplaySize(this.canvas);\n if (!this.gl) return;\n if (needsResize) {\n this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight);\n }\n this.updateProjectionMatrix();\n }\n\n public run(time = 0): void {\n this._deltaTime = Math.min(32, time - this._time);\n this._time = time;\n this._deltaFrames = this._deltaTime / this.TARGET_FRAME_DURATION;\n this._frames += this._deltaFrames;\n\n this.animate(this._deltaTime);\n this.render();\n\n requestAnimationFrame(t => this.run(t));\n }\n\n private init(onInit?: InitCallback): void {\n const gl = this.canvas.getContext('webgl2', {\n antialias: true,\n alpha: false\n });\n if (!gl) {\n throw new Error('No WebGL 2 context!');\n }\n this.gl = gl;\n\n vec2.set(this.viewportSize, this.canvas.clientWidth, this.canvas.clientHeight);\n vec2.clone(this.drawBufferSize);\n\n this.discProgram = createProgram(gl, [discVertShaderSource, discFragShaderSource], null, {\n aModelPosition: 0,\n aModelNormal: 1,\n aModelUvs: 2,\n aInstanceMatrix: 3\n });\n\n this.discLocations = {\n aModelPosition: gl.getAttribLocation(this.discProgram!, 'aModelPosition'),\n aModelUvs: gl.getAttribLocation(this.discProgram!, 'aModelUvs'),\n aInstanceMatrix: gl.getAttribLocation(this.discProgram!, 'aInstanceMatrix'),\n uWorldMatrix: gl.getUniformLocation(this.discProgram!, 'uWorldMatrix'),\n uViewMatrix: gl.getUniformLocation(this.discProgram!, 'uViewMatrix'),\n uProjectionMatrix: gl.getUniformLocation(this.discProgram!, 'uProjectionMatrix'),\n uCameraPosition: gl.getUniformLocation(this.discProgram!, 'uCameraPosition'),\n uScaleFactor: gl.getUniformLocation(this.discProgram!, 'uScaleFactor'),\n uRotationAxisVelocity: gl.getUniformLocation(this.discProgram!, 'uRotationAxisVelocity'),\n uTex: gl.getUniformLocation(this.discProgram!, 'uTex'),\n uFrames: gl.getUniformLocation(this.discProgram!, 'uFrames'),\n uItemCount: gl.getUniformLocation(this.discProgram!, 'uItemCount'),\n uAtlasSize: gl.getUniformLocation(this.discProgram!, 'uAtlasSize')\n };\n\n this.discGeo = new DiscGeometry(56, 1);\n this.discBuffers = this.discGeo.data;\n this.discVAO = makeVertexArray(\n gl,\n [\n [makeBuffer(gl, this.discBuffers.vertices, gl.STATIC_DRAW), this.discLocations.aModelPosition, 3],\n [makeBuffer(gl, this.discBuffers.uvs, gl.STATIC_DRAW), this.discLocations.aModelUvs, 2]\n ],\n this.discBuffers.indices\n );\n\n this.icoGeo = new IcosahedronGeometry();\n this.icoGeo.subdivide(1).spherize(this.SPHERE_RADIUS);\n this.instancePositions = this.icoGeo.vertices.map(v => v.position);\n this.DISC_INSTANCE_COUNT = this.icoGeo.vertices.length;\n this.initDiscInstances(this.DISC_INSTANCE_COUNT);\n this.initTexture();\n\n this.control = new ArcballControl(this.canvas, deltaTime => this.onControlUpdate(deltaTime));\n\n this.updateCameraMatrix();\n this.updateProjectionMatrix();\n\n this.resize();\n\n if (onInit) {\n onInit(this);\n }\n }\n\n private initTexture(): void {\n if (!this.gl) return;\n const gl = this.gl;\n this.tex = createAndSetupTexture(gl, gl.LINEAR, gl.LINEAR, gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE);\n\n const itemCount = Math.max(1, this.items.length);\n this.atlasSize = Math.ceil(Math.sqrt(itemCount));\n const cellSize = 512;\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d')!;\n canvas.width = this.atlasSize * cellSize;\n canvas.height = this.atlasSize * cellSize;\n\n Promise.all(\n this.items.map(\n item =>\n new Promise(resolve => {\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => resolve(img);\n img.src = item.image;\n })\n )\n ).then(images => {\n images.forEach((img, i) => {\n const x = (i % this.atlasSize) * cellSize;\n const y = Math.floor(i / this.atlasSize) * cellSize;\n ctx.drawImage(img, x, y, cellSize, cellSize);\n });\n\n gl.bindTexture(gl.TEXTURE_2D, this.tex);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);\n gl.generateMipmap(gl.TEXTURE_2D);\n });\n }\n\n private initDiscInstances(count: number): void {\n if (!this.gl || !this.discVAO) return;\n const gl = this.gl;\n\n const matricesArray = new Float32Array(count * 16);\n const matrices: Float32Array[] = [];\n for (let i = 0; i < count; ++i) {\n const instanceMatrixArray = new Float32Array(matricesArray.buffer, i * 16 * 4, 16);\n mat4.identity(instanceMatrixArray as unknown as mat4);\n matrices.push(instanceMatrixArray);\n }\n\n this.discInstances = {\n matricesArray,\n matrices,\n buffer: gl.createBuffer()\n };\n\n gl.bindVertexArray(this.discVAO);\n gl.bindBuffer(gl.ARRAY_BUFFER, this.discInstances.buffer);\n gl.bufferData(gl.ARRAY_BUFFER, this.discInstances.matricesArray.byteLength, gl.DYNAMIC_DRAW);\n\n const mat4AttribSlotCount = 4;\n const bytesPerMatrix = 16 * 4;\n for (let j = 0; j < mat4AttribSlotCount; ++j) {\n const loc = this.discLocations.aInstanceMatrix + j;\n gl.enableVertexAttribArray(loc);\n gl.vertexAttribPointer(loc, 4, gl.FLOAT, false, bytesPerMatrix, j * 4 * 4);\n gl.vertexAttribDivisor(loc, 1);\n }\n gl.bindBuffer(gl.ARRAY_BUFFER, null);\n gl.bindVertexArray(null);\n }\n\n private animate(deltaTime: number): void {\n if (!this.gl) return;\n this.control.update(deltaTime, this.TARGET_FRAME_DURATION);\n\n const positions = this.instancePositions.map(p => vec3.transformQuat(vec3.create(), p, this.control.orientation));\n const scale = 0.25;\n const SCALE_INTENSITY = 0.6;\n\n positions.forEach((p, ndx) => {\n const s = (Math.abs(p[2]) / this.SPHERE_RADIUS) * SCALE_INTENSITY + (1 - SCALE_INTENSITY);\n const finalScale = s * scale;\n const matrix = mat4.create();\n\n mat4.multiply(matrix, matrix, mat4.fromTranslation(mat4.create(), vec3.negate(vec3.create(), p)));\n mat4.multiply(matrix, matrix, mat4.targetTo(mat4.create(), [0, 0, 0], p, [0, 1, 0]));\n mat4.multiply(matrix, matrix, mat4.fromScaling(mat4.create(), [finalScale, finalScale, finalScale]));\n mat4.multiply(matrix, matrix, mat4.fromTranslation(mat4.create(), [0, 0, -this.SPHERE_RADIUS]));\n\n mat4.copy(this.discInstances.matrices[ndx], matrix);\n });\n\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.discInstances.buffer);\n this.gl.bufferSubData(this.gl.ARRAY_BUFFER, 0, this.discInstances.matricesArray);\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);\n\n this.smoothRotationVelocity = this.control.rotationVelocity;\n }\n\n private render(): void {\n if (!this.gl || !this.discProgram) return;\n const gl = this.gl;\n\n gl.useProgram(this.discProgram);\n gl.enable(gl.CULL_FACE);\n gl.enable(gl.DEPTH_TEST);\n\n gl.clearColor(0, 0, 0, 0);\n gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);\n\n gl.uniformMatrix4fv(this.discLocations.uWorldMatrix, false, this.worldMatrix);\n gl.uniformMatrix4fv(this.discLocations.uViewMatrix, false, this.camera.matrices.view);\n gl.uniformMatrix4fv(this.discLocations.uProjectionMatrix, false, this.camera.matrices.projection);\n gl.uniform3f(\n this.discLocations.uCameraPosition,\n this.camera.position[0],\n this.camera.position[1],\n this.camera.position[2]\n );\n gl.uniform4f(\n this.discLocations.uRotationAxisVelocity,\n this.control.rotationAxis[0],\n this.control.rotationAxis[1],\n this.control.rotationAxis[2],\n this.smoothRotationVelocity * 1.1\n );\n\n gl.uniform1i(this.discLocations.uItemCount, this.items.length);\n gl.uniform1i(this.discLocations.uAtlasSize, this.atlasSize);\n\n gl.uniform1f(this.discLocations.uFrames, this._frames);\n gl.uniform1f(this.discLocations.uScaleFactor, this.scaleFactor);\n\n gl.uniform1i(this.discLocations.uTex, 0);\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, this.tex);\n\n gl.bindVertexArray(this.discVAO);\n gl.drawElementsInstanced(\n gl.TRIANGLES,\n this.discBuffers.indices.length,\n gl.UNSIGNED_SHORT,\n 0,\n this.DISC_INSTANCE_COUNT\n );\n gl.bindVertexArray(null);\n }\n\n private updateCameraMatrix(): void {\n mat4.targetTo(this.camera.matrix, this.camera.position, [0, 0, 0], this.camera.up);\n mat4.invert(this.camera.matrices.view, this.camera.matrix);\n }\n\n private updateProjectionMatrix(): void {\n if (!this.gl) return;\n const canvasEl = this.gl.canvas as HTMLCanvasElement;\n this.camera.aspect = canvasEl.clientWidth / canvasEl.clientHeight;\n const height = this.SPHERE_RADIUS * 0.35;\n const distance = this.camera.position[2];\n if (this.camera.aspect > 1) {\n this.camera.fov = 2 * Math.atan(height / distance);\n } else {\n this.camera.fov = 2 * Math.atan(height / this.camera.aspect / distance);\n }\n mat4.perspective(\n this.camera.matrices.projection,\n this.camera.fov,\n this.camera.aspect,\n this.camera.near,\n this.camera.far\n );\n mat4.invert(this.camera.matrices.inversProjection, this.camera.matrices.projection);\n }\n\n private onControlUpdate(deltaTime: number): void {\n const timeScale = deltaTime / this.TARGET_FRAME_DURATION + 0.0001;\n let damping = 5 / timeScale;\n let cameraTargetZ = 3 * this.scaleFactor;\n\n const isMoving = this.control.isPointerDown || Math.abs(this.smoothRotationVelocity) > 0.01;\n\n if (isMoving !== this.movementActive) {\n this.movementActive = isMoving;\n this.onMovementChange(isMoving);\n }\n\n if (!this.control.isPointerDown) {\n const nearestVertexIndex = this.findNearestVertexIndex();\n const itemIndex = nearestVertexIndex % Math.max(1, this.items.length);\n this.onActiveItemChange(itemIndex);\n const snapDirection = vec3.normalize(vec3.create(), this.getVertexWorldPosition(nearestVertexIndex));\n this.control.snapTargetDirection = snapDirection;\n } else {\n cameraTargetZ += this.control.rotationVelocity * 80 + 2.5;\n damping = 7 / timeScale;\n }\n\n this.camera.position[2] += (cameraTargetZ - this.camera.position[2]) / damping;\n this.updateCameraMatrix();\n }\n\n private findNearestVertexIndex(): number {\n const n = this.control.snapDirection;\n const inversOrientation = quat.conjugate(quat.create(), this.control.orientation);\n const nt = vec3.transformQuat(vec3.create(), n, inversOrientation);\n\n let maxD = -1;\n let nearestVertexIndex = 0;\n for (let i = 0; i < this.instancePositions.length; ++i) {\n const d = vec3.dot(nt, this.instancePositions[i]);\n if (d > maxD) {\n maxD = d;\n nearestVertexIndex = i;\n }\n }\n return nearestVertexIndex;\n }\n\n private getVertexWorldPosition(index: number): vec3 {\n const nearestVertexPos = this.instancePositions[index];\n return vec3.transformQuat(vec3.create(), nearestVertexPos, this.control.orientation);\n }\n}\n\nconst defaultItems: MenuItem[] = [\n {\n image:\n 'https://images.unsplash.com/photo-1782977389500-dd7adad33ebe?q=80&w=600&h=600&fit=crop&sat=-100&auto=format',\n link: 'https://google.com/',\n title: '',\n description: ''\n }\n];\n\ninterface InfiniteMenuProps {\n items?: MenuItem[];\n scale?: number;\n}\n\nconst InfiniteMenu: FC = ({ items = [], scale = 1.0 }) => {\n const canvasRef = useRef(null) as MutableRefObject;\n const [activeItem, setActiveItem] = useState(null);\n const [isMoving, setIsMoving] = useState(false);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n let sketch: InfiniteGridMenu | null = null;\n\n const handleActiveItem = (index: number) => {\n if (!items.length) return;\n const itemIndex = index % items.length;\n setActiveItem(items[itemIndex]);\n };\n\n if (canvas) {\n sketch = new InfiniteGridMenu(\n canvas,\n items.length ? items : defaultItems,\n handleActiveItem,\n setIsMoving,\n sk => sk.run(),\n scale\n );\n }\n\n const handleResize = () => {\n if (sketch) {\n sketch.resize();\n }\n };\n\n window.addEventListener('resize', handleResize);\n handleResize();\n\n return () => {\n window.removeEventListener('resize', handleResize);\n };\n }, [items, scale]);\n\n const handleButtonClick = () => {\n if (!activeItem?.link) return;\n if (activeItem.link.startsWith('http')) {\n window.open(activeItem.link, '_blank');\n } else {\n console.log('Internal route:', activeItem.link);\n }\n };\n\n return (\n
\n \n\n {activeItem && (\n <>\n

{activeItem.title}

\n\n

{activeItem.description}

\n\n
\n

\n
\n \n )}\n
\n );\n};\n\nexport default InfiniteMenu;\n" } ], "registryDependencies": [], diff --git a/public/r/InfiniteMenu-TS-TW.json b/public/r/InfiniteMenu-TS-TW.json index a7ccb00f..173b0596 100644 --- a/public/r/InfiniteMenu-TS-TW.json +++ b/public/r/InfiniteMenu-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "InfiniteMenu/InfiniteMenu.tsx", - "content": "import { FC, useRef, useState, useEffect, MutableRefObject } from 'react';\nimport { mat4, quat, vec2, vec3 } from 'gl-matrix';\n\nconst discVertShaderSource = `#version 300 es\n\nuniform mat4 uWorldMatrix;\nuniform mat4 uViewMatrix;\nuniform mat4 uProjectionMatrix;\nuniform vec3 uCameraPosition;\nuniform vec4 uRotationAxisVelocity;\n\nin vec3 aModelPosition;\nin vec3 aModelNormal;\nin vec2 aModelUvs;\nin mat4 aInstanceMatrix;\n\nout vec2 vUvs;\nout float vAlpha;\nflat out int vInstanceId;\n\n#define PI 3.141593\n\nvoid main() {\n vec4 worldPosition = uWorldMatrix * aInstanceMatrix * vec4(aModelPosition, 1.);\n\n vec3 centerPos = (uWorldMatrix * aInstanceMatrix * vec4(0., 0., 0., 1.)).xyz;\n float radius = length(centerPos.xyz);\n\n if (gl_VertexID > 0) {\n vec3 rotationAxis = uRotationAxisVelocity.xyz;\n float rotationVelocity = min(.15, uRotationAxisVelocity.w * 15.);\n vec3 stretchDir = normalize(cross(centerPos, rotationAxis));\n vec3 relativeVertexPos = normalize(worldPosition.xyz - centerPos);\n float strength = dot(stretchDir, relativeVertexPos);\n float invAbsStrength = min(0., abs(strength) - 1.);\n strength = rotationVelocity * sign(strength) * abs(invAbsStrength * invAbsStrength * invAbsStrength + 1.);\n worldPosition.xyz += stretchDir * strength;\n }\n\n worldPosition.xyz = radius * normalize(worldPosition.xyz);\n\n gl_Position = uProjectionMatrix * uViewMatrix * worldPosition;\n\n vAlpha = smoothstep(0.5, 1., normalize(worldPosition.xyz).z) * .9 + .1;\n vUvs = aModelUvs;\n vInstanceId = gl_InstanceID;\n}\n`;\n\nconst discFragShaderSource = `#version 300 es\nprecision highp float;\n\nuniform sampler2D uTex;\nuniform int uItemCount;\nuniform int uAtlasSize;\n\nout vec4 outColor;\n\nin vec2 vUvs;\nin float vAlpha;\nflat in int vInstanceId;\n\nvoid main() {\n int itemIndex = vInstanceId % uItemCount;\n int cellsPerRow = uAtlasSize;\n int cellX = itemIndex % cellsPerRow;\n int cellY = itemIndex / cellsPerRow;\n vec2 cellSize = vec2(1.0) / vec2(float(cellsPerRow));\n vec2 cellOffset = vec2(float(cellX), float(cellY)) * cellSize;\n\n ivec2 texSize = textureSize(uTex, 0);\n float imageAspect = float(texSize.x) / float(texSize.y);\n float containerAspect = 1.0;\n \n float scale = max(imageAspect / containerAspect, \n containerAspect / imageAspect);\n \n vec2 st = vec2(vUvs.x, 1.0 - vUvs.y);\n st = (st - 0.5) * scale + 0.5;\n \n st = clamp(st, 0.0, 1.0);\n st = st * cellSize + cellOffset;\n \n outColor = texture(uTex, st);\n outColor.a *= vAlpha;\n}\n`;\n\nclass Face {\n public a: number;\n public b: number;\n public c: number;\n\n constructor(a: number, b: number, c: number) {\n this.a = a;\n this.b = b;\n this.c = c;\n }\n}\n\nclass Vertex {\n public position: vec3;\n public normal: vec3;\n public uv: vec2;\n\n constructor(x: number, y: number, z: number) {\n this.position = vec3.fromValues(x, y, z);\n this.normal = vec3.create();\n this.uv = vec2.create();\n }\n}\n\nclass Geometry {\n public vertices: Vertex[];\n public faces: Face[];\n\n constructor() {\n this.vertices = [];\n this.faces = [];\n }\n\n public addVertex(...args: number[]): this {\n for (let i = 0; i < args.length; i += 3) {\n this.vertices.push(new Vertex(args[i], args[i + 1], args[i + 2]));\n }\n return this;\n }\n\n public addFace(...args: number[]): this {\n for (let i = 0; i < args.length; i += 3) {\n this.faces.push(new Face(args[i], args[i + 1], args[i + 2]));\n }\n return this;\n }\n\n public get lastVertex(): Vertex {\n return this.vertices[this.vertices.length - 1];\n }\n\n public subdivide(divisions = 1): this {\n const midPointCache: Record = {};\n let f = this.faces;\n\n for (let div = 0; div < divisions; ++div) {\n const newFaces = new Array(f.length * 4);\n\n f.forEach((face, ndx) => {\n const mAB = this.getMidPoint(face.a, face.b, midPointCache);\n const mBC = this.getMidPoint(face.b, face.c, midPointCache);\n const mCA = this.getMidPoint(face.c, face.a, midPointCache);\n\n const i = ndx * 4;\n newFaces[i + 0] = new Face(face.a, mAB, mCA);\n newFaces[i + 1] = new Face(face.b, mBC, mAB);\n newFaces[i + 2] = new Face(face.c, mCA, mBC);\n newFaces[i + 3] = new Face(mAB, mBC, mCA);\n });\n\n f = newFaces;\n }\n\n this.faces = f;\n return this;\n }\n\n public spherize(radius = 1): this {\n this.vertices.forEach(vertex => {\n vec3.normalize(vertex.normal, vertex.position);\n vec3.scale(vertex.position, vertex.normal, radius);\n });\n return this;\n }\n\n public get data(): {\n vertices: Float32Array;\n indices: Uint16Array;\n normals: Float32Array;\n uvs: Float32Array;\n } {\n return {\n vertices: this.vertexData,\n indices: this.indexData,\n normals: this.normalData,\n uvs: this.uvData\n };\n }\n\n public get vertexData(): Float32Array {\n return new Float32Array(this.vertices.flatMap(v => Array.from(v.position)));\n }\n\n public get normalData(): Float32Array {\n return new Float32Array(this.vertices.flatMap(v => Array.from(v.normal)));\n }\n\n public get uvData(): Float32Array {\n return new Float32Array(this.vertices.flatMap(v => Array.from(v.uv)));\n }\n\n public get indexData(): Uint16Array {\n return new Uint16Array(this.faces.flatMap(f => [f.a, f.b, f.c]));\n }\n\n public getMidPoint(ndxA: number, ndxB: number, cache: Record): number {\n const cacheKey = ndxA < ndxB ? `k_${ndxB}_${ndxA}` : `k_${ndxA}_${ndxB}`;\n if (Object.prototype.hasOwnProperty.call(cache, cacheKey)) {\n return cache[cacheKey];\n }\n const a = this.vertices[ndxA].position;\n const b = this.vertices[ndxB].position;\n const ndx = this.vertices.length;\n cache[cacheKey] = ndx;\n this.addVertex((a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5, (a[2] + b[2]) * 0.5);\n return ndx;\n }\n}\n\nclass IcosahedronGeometry extends Geometry {\n constructor() {\n super();\n const t = Math.sqrt(5) * 0.5 + 0.5;\n this.addVertex(\n -1,\n t,\n 0,\n 1,\n t,\n 0,\n -1,\n -t,\n 0,\n 1,\n -t,\n 0,\n 0,\n -1,\n t,\n 0,\n 1,\n t,\n 0,\n -1,\n -t,\n 0,\n 1,\n -t,\n t,\n 0,\n -1,\n t,\n 0,\n 1,\n -t,\n 0,\n -1,\n -t,\n 0,\n 1\n ).addFace(\n 0,\n 11,\n 5,\n 0,\n 5,\n 1,\n 0,\n 1,\n 7,\n 0,\n 7,\n 10,\n 0,\n 10,\n 11,\n 1,\n 5,\n 9,\n 5,\n 11,\n 4,\n 11,\n 10,\n 2,\n 10,\n 7,\n 6,\n 7,\n 1,\n 8,\n 3,\n 9,\n 4,\n 3,\n 4,\n 2,\n 3,\n 2,\n 6,\n 3,\n 6,\n 8,\n 3,\n 8,\n 9,\n 4,\n 9,\n 5,\n 2,\n 4,\n 11,\n 6,\n 2,\n 10,\n 8,\n 6,\n 7,\n 9,\n 8,\n 1\n );\n }\n}\n\nclass DiscGeometry extends Geometry {\n constructor(steps = 4, radius = 1) {\n super();\n const safeSteps = Math.max(4, steps);\n const alpha = (2 * Math.PI) / safeSteps;\n\n this.addVertex(0, 0, 0);\n this.lastVertex.uv[0] = 0.5;\n this.lastVertex.uv[1] = 0.5;\n\n for (let i = 0; i < safeSteps; ++i) {\n const x = Math.cos(alpha * i);\n const y = Math.sin(alpha * i);\n this.addVertex(radius * x, radius * y, 0);\n this.lastVertex.uv[0] = x * 0.5 + 0.5;\n this.lastVertex.uv[1] = y * 0.5 + 0.5;\n\n if (i > 0) {\n this.addFace(0, i, i + 1);\n }\n }\n this.addFace(0, safeSteps, 1);\n }\n}\n\nfunction createShader(gl: WebGL2RenderingContext, type: number, source: string): WebGLShader | null {\n const shader = gl.createShader(type);\n if (!shader) return null;\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);\n\n if (success) {\n return shader;\n }\n\n console.error(gl.getShaderInfoLog(shader));\n gl.deleteShader(shader);\n return null;\n}\n\nfunction createProgram(\n gl: WebGL2RenderingContext,\n shaderSources: [string, string],\n transformFeedbackVaryings?: string[] | null,\n attribLocations?: Record\n): WebGLProgram | null {\n const program = gl.createProgram();\n if (!program) return null;\n\n [gl.VERTEX_SHADER, gl.FRAGMENT_SHADER].forEach((type, ndx) => {\n const shader = createShader(gl, type, shaderSources[ndx]);\n if (shader) {\n gl.attachShader(program, shader);\n }\n });\n\n if (transformFeedbackVaryings) {\n gl.transformFeedbackVaryings(program, transformFeedbackVaryings, gl.SEPARATE_ATTRIBS);\n }\n\n if (attribLocations) {\n for (const attrib in attribLocations) {\n if (Object.prototype.hasOwnProperty.call(attribLocations, attrib)) {\n gl.bindAttribLocation(program, attribLocations[attrib], attrib);\n }\n }\n }\n\n gl.linkProgram(program);\n const success = gl.getProgramParameter(program, gl.LINK_STATUS);\n\n if (success) {\n return program;\n }\n\n console.error(gl.getProgramInfoLog(program));\n gl.deleteProgram(program);\n return null;\n}\n\nfunction makeVertexArray(\n gl: WebGL2RenderingContext,\n bufLocNumElmPairs: Array<[WebGLBuffer, number, number]>,\n indices?: Uint16Array\n): WebGLVertexArrayObject | null {\n const va = gl.createVertexArray();\n if (!va) return null;\n\n gl.bindVertexArray(va);\n\n for (const [buffer, loc, numElem] of bufLocNumElmPairs) {\n if (loc === -1) continue;\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.enableVertexAttribArray(loc);\n gl.vertexAttribPointer(loc, numElem, gl.FLOAT, false, 0, 0);\n }\n\n if (indices) {\n const indexBuffer = gl.createBuffer();\n if (indexBuffer) {\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);\n gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);\n }\n }\n\n gl.bindVertexArray(null);\n return va;\n}\n\nfunction resizeCanvasToDisplaySize(canvas: HTMLCanvasElement): boolean {\n const dpr = Math.min(2, window.devicePixelRatio || 1);\n const displayWidth = Math.round(canvas.clientWidth * dpr);\n const displayHeight = Math.round(canvas.clientHeight * dpr);\n const needResize = canvas.width !== displayWidth || canvas.height !== displayHeight;\n if (needResize) {\n canvas.width = displayWidth;\n canvas.height = displayHeight;\n }\n return needResize;\n}\n\nfunction makeBuffer(gl: WebGL2RenderingContext, sizeOrData: number | ArrayBufferView, usage: number): WebGLBuffer {\n const buf = gl.createBuffer();\n if (!buf) {\n throw new Error('Failed to create WebGL buffer.');\n }\n gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n\n if (typeof sizeOrData === 'number') {\n gl.bufferData(gl.ARRAY_BUFFER, sizeOrData, usage);\n } else {\n gl.bufferData(gl.ARRAY_BUFFER, sizeOrData, usage);\n }\n\n gl.bindBuffer(gl.ARRAY_BUFFER, null);\n return buf;\n}\n\nfunction createAndSetupTexture(\n gl: WebGL2RenderingContext,\n minFilter: number,\n magFilter: number,\n wrapS: number,\n wrapT: number\n): WebGLTexture {\n const texture = gl.createTexture();\n if (!texture) {\n throw new Error('Failed to create WebGL texture.');\n }\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapS);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapT);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, magFilter);\n return texture;\n}\n\ntype UpdateCallback = (deltaTime: number) => void;\n\nclass ArcballControl {\n private canvas: HTMLCanvasElement;\n private updateCallback: UpdateCallback;\n\n public isPointerDown = false;\n public orientation = quat.create();\n public pointerRotation = quat.create();\n public rotationVelocity = 0;\n public rotationAxis = vec3.fromValues(1, 0, 0);\n\n public snapDirection = vec3.fromValues(0, 0, -1);\n public snapTargetDirection: vec3 | null = null;\n\n private pointerPos = vec2.create();\n private previousPointerPos = vec2.create();\n private _rotationVelocity = 0;\n private _combinedQuat = quat.create();\n\n private readonly EPSILON = 0.1;\n private readonly IDENTITY_QUAT = quat.create();\n\n constructor(canvas: HTMLCanvasElement, updateCallback?: UpdateCallback) {\n this.canvas = canvas;\n this.updateCallback = updateCallback || (() => undefined);\n\n canvas.addEventListener('pointerdown', (e: PointerEvent) => {\n vec2.set(this.pointerPos, e.clientX, e.clientY);\n vec2.copy(this.previousPointerPos, this.pointerPos);\n this.isPointerDown = true;\n });\n canvas.addEventListener('pointerup', () => {\n this.isPointerDown = false;\n });\n canvas.addEventListener('pointerleave', () => {\n this.isPointerDown = false;\n });\n canvas.addEventListener('pointermove', (e: PointerEvent) => {\n if (this.isPointerDown) {\n vec2.set(this.pointerPos, e.clientX, e.clientY);\n }\n });\n canvas.style.touchAction = 'none';\n }\n\n public update(deltaTime: number, targetFrameDuration = 16): void {\n const timeScale = deltaTime / targetFrameDuration + 0.00001;\n let angleFactor = timeScale;\n const snapRotation = quat.create();\n\n if (this.isPointerDown) {\n const INTENSITY = 0.3 * timeScale;\n const ANGLE_AMPLIFICATION = 5 / timeScale;\n const midPointerPos = vec2.sub(vec2.create(), this.pointerPos, this.previousPointerPos);\n vec2.scale(midPointerPos, midPointerPos, INTENSITY);\n\n if (vec2.sqrLen(midPointerPos) > this.EPSILON) {\n vec2.add(midPointerPos, this.previousPointerPos, midPointerPos);\n\n const p = this.project(midPointerPos);\n const q = this.project(this.previousPointerPos);\n const a = vec3.normalize(vec3.create(), p);\n const b = vec3.normalize(vec3.create(), q);\n\n vec2.copy(this.previousPointerPos, midPointerPos);\n\n angleFactor *= ANGLE_AMPLIFICATION;\n\n this.quatFromVectors(a, b, this.pointerRotation, angleFactor);\n } else {\n quat.slerp(this.pointerRotation, this.pointerRotation, this.IDENTITY_QUAT, INTENSITY);\n }\n } else {\n const INTENSITY = 0.1 * timeScale;\n quat.slerp(this.pointerRotation, this.pointerRotation, this.IDENTITY_QUAT, INTENSITY);\n\n if (this.snapTargetDirection) {\n const SNAPPING_INTENSITY = 0.2;\n const a = this.snapTargetDirection;\n const b = this.snapDirection;\n const sqrDist = vec3.squaredDistance(a, b);\n const distanceFactor = Math.max(0.1, 1 - sqrDist * 10);\n angleFactor *= SNAPPING_INTENSITY * distanceFactor;\n this.quatFromVectors(a, b, snapRotation, angleFactor);\n }\n }\n\n const combinedQuat = quat.multiply(quat.create(), snapRotation, this.pointerRotation);\n this.orientation = quat.multiply(quat.create(), combinedQuat, this.orientation);\n quat.normalize(this.orientation, this.orientation);\n\n const RA_INTENSITY = 0.8 * timeScale;\n quat.slerp(this._combinedQuat, this._combinedQuat, combinedQuat, RA_INTENSITY);\n quat.normalize(this._combinedQuat, this._combinedQuat);\n\n const rad = Math.acos(this._combinedQuat[3]) * 2.0;\n const s = Math.sin(rad / 2.0);\n let rv = 0;\n if (s > 0.000001) {\n rv = rad / (2 * Math.PI);\n this.rotationAxis[0] = this._combinedQuat[0] / s;\n this.rotationAxis[1] = this._combinedQuat[1] / s;\n this.rotationAxis[2] = this._combinedQuat[2] / s;\n }\n\n const RV_INTENSITY = 0.5 * timeScale;\n this._rotationVelocity += (rv - this._rotationVelocity) * RV_INTENSITY;\n this.rotationVelocity = this._rotationVelocity / timeScale;\n\n this.updateCallback(deltaTime);\n }\n\n private quatFromVectors(a: vec3, b: vec3, out: quat, angleFactor = 1): { q: quat; axis: vec3; angle: number } {\n const axis = vec3.cross(vec3.create(), a, b);\n vec3.normalize(axis, axis);\n const d = Math.max(-1, Math.min(1, vec3.dot(a, b)));\n const angle = Math.acos(d) * angleFactor;\n quat.setAxisAngle(out, axis, angle);\n return { q: out, axis, angle };\n }\n\n private project(pos: vec2): vec3 {\n const r = 2;\n const w = this.canvas.clientWidth;\n const h = this.canvas.clientHeight;\n const s = Math.max(w, h) - 1;\n\n const x = (2 * pos[0] - w - 1) / s;\n const y = (2 * pos[1] - h - 1) / s;\n let z = 0;\n const xySq = x * x + y * y;\n const rSq = r * r;\n\n if (xySq <= rSq / 2.0) {\n z = Math.sqrt(rSq - xySq);\n } else {\n z = rSq / Math.sqrt(xySq);\n }\n return vec3.fromValues(-x, y, z);\n }\n}\n\ninterface MenuItem {\n image: string;\n link: string;\n title: string;\n description: string;\n}\n\ntype ActiveItemCallback = (index: number) => void;\ntype MovementChangeCallback = (isMoving: boolean) => void;\ntype InitCallback = (instance: InfiniteGridMenu) => void;\n\ninterface Camera {\n matrix: mat4;\n near: number;\n far: number;\n fov: number;\n aspect: number;\n position: vec3;\n up: vec3;\n matrices: {\n view: mat4;\n projection: mat4;\n inversProjection: mat4;\n };\n}\n\nclass InfiniteGridMenu {\n private gl: WebGL2RenderingContext | null = null;\n private discProgram: WebGLProgram | null = null;\n private discVAO: WebGLVertexArrayObject | null = null;\n private discBuffers!: {\n vertices: Float32Array;\n indices: Uint16Array;\n normals: Float32Array;\n uvs: Float32Array;\n };\n private icoGeo!: IcosahedronGeometry;\n private discGeo!: DiscGeometry;\n private worldMatrix = mat4.create();\n private tex: WebGLTexture | null = null;\n private control!: ArcballControl;\n\n private discLocations!: {\n aModelPosition: number;\n aModelUvs: number;\n aInstanceMatrix: number;\n uWorldMatrix: WebGLUniformLocation | null;\n uViewMatrix: WebGLUniformLocation | null;\n uProjectionMatrix: WebGLUniformLocation | null;\n uCameraPosition: WebGLUniformLocation | null;\n uScaleFactor: WebGLUniformLocation | null;\n uRotationAxisVelocity: WebGLUniformLocation | null;\n uTex: WebGLUniformLocation | null;\n uFrames: WebGLUniformLocation | null;\n uItemCount: WebGLUniformLocation | null;\n uAtlasSize: WebGLUniformLocation | null;\n };\n\n private viewportSize = vec2.create();\n private drawBufferSize = vec2.create();\n\n private discInstances!: {\n matricesArray: Float32Array;\n matrices: Float32Array[];\n buffer: WebGLBuffer | null;\n };\n\n private instancePositions: vec3[] = [];\n private DISC_INSTANCE_COUNT = 0;\n private atlasSize = 1;\n\n private _time = 0;\n private _deltaTime = 0;\n private _deltaFrames = 0;\n private _frames = 0;\n\n private movementActive = false;\n\n private TARGET_FRAME_DURATION = 1000 / 60;\n private SPHERE_RADIUS = 2;\n\n public camera: Camera = {\n matrix: mat4.create(),\n near: 0.1,\n far: 40,\n fov: Math.PI / 4,\n aspect: 1,\n position: vec3.fromValues(0, 0, 3),\n up: vec3.fromValues(0, 1, 0),\n matrices: {\n view: mat4.create(),\n projection: mat4.create(),\n inversProjection: mat4.create()\n }\n };\n\n public smoothRotationVelocity = 0;\n public scaleFactor = 1.0;\n\n constructor(\n private canvas: HTMLCanvasElement,\n private items: MenuItem[],\n private onActiveItemChange: ActiveItemCallback,\n private onMovementChange: MovementChangeCallback,\n onInit?: InitCallback,\n scale: number = 1.0\n ) {\n this.scaleFactor = scale;\n this.camera.position[2] = 3 * scale;\n this.init(onInit);\n }\n\n public resize(): void {\n const needsResize = resizeCanvasToDisplaySize(this.canvas);\n if (!this.gl) return;\n if (needsResize) {\n this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight);\n }\n this.updateProjectionMatrix();\n }\n\n public run(time = 0): void {\n this._deltaTime = Math.min(32, time - this._time);\n this._time = time;\n this._deltaFrames = this._deltaTime / this.TARGET_FRAME_DURATION;\n this._frames += this._deltaFrames;\n\n this.animate(this._deltaTime);\n this.render();\n\n requestAnimationFrame(t => this.run(t));\n }\n\n private init(onInit?: InitCallback): void {\n const gl = this.canvas.getContext('webgl2', {\n antialias: true,\n alpha: false\n });\n if (!gl) {\n throw new Error('No WebGL 2 context!');\n }\n this.gl = gl;\n\n vec2.set(this.viewportSize, this.canvas.clientWidth, this.canvas.clientHeight);\n vec2.clone(this.drawBufferSize);\n\n this.discProgram = createProgram(gl, [discVertShaderSource, discFragShaderSource], null, {\n aModelPosition: 0,\n aModelNormal: 1,\n aModelUvs: 2,\n aInstanceMatrix: 3\n });\n\n this.discLocations = {\n aModelPosition: gl.getAttribLocation(this.discProgram!, 'aModelPosition'),\n aModelUvs: gl.getAttribLocation(this.discProgram!, 'aModelUvs'),\n aInstanceMatrix: gl.getAttribLocation(this.discProgram!, 'aInstanceMatrix'),\n uWorldMatrix: gl.getUniformLocation(this.discProgram!, 'uWorldMatrix'),\n uViewMatrix: gl.getUniformLocation(this.discProgram!, 'uViewMatrix'),\n uProjectionMatrix: gl.getUniformLocation(this.discProgram!, 'uProjectionMatrix'),\n uCameraPosition: gl.getUniformLocation(this.discProgram!, 'uCameraPosition'),\n uScaleFactor: gl.getUniformLocation(this.discProgram!, 'uScaleFactor'),\n uRotationAxisVelocity: gl.getUniformLocation(this.discProgram!, 'uRotationAxisVelocity'),\n uTex: gl.getUniformLocation(this.discProgram!, 'uTex'),\n uFrames: gl.getUniformLocation(this.discProgram!, 'uFrames'),\n uItemCount: gl.getUniformLocation(this.discProgram!, 'uItemCount'),\n uAtlasSize: gl.getUniformLocation(this.discProgram!, 'uAtlasSize')\n };\n\n this.discGeo = new DiscGeometry(56, 1);\n this.discBuffers = this.discGeo.data;\n this.discVAO = makeVertexArray(\n gl,\n [\n [makeBuffer(gl, this.discBuffers.vertices, gl.STATIC_DRAW), this.discLocations.aModelPosition, 3],\n [makeBuffer(gl, this.discBuffers.uvs, gl.STATIC_DRAW), this.discLocations.aModelUvs, 2]\n ],\n this.discBuffers.indices\n );\n\n this.icoGeo = new IcosahedronGeometry();\n this.icoGeo.subdivide(1).spherize(this.SPHERE_RADIUS);\n this.instancePositions = this.icoGeo.vertices.map(v => v.position);\n this.DISC_INSTANCE_COUNT = this.icoGeo.vertices.length;\n this.initDiscInstances(this.DISC_INSTANCE_COUNT);\n this.initTexture();\n this.control = new ArcballControl(this.canvas, deltaTime => this.onControlUpdate(deltaTime));\n\n this.updateCameraMatrix();\n this.updateProjectionMatrix();\n\n this.resize();\n\n if (onInit) {\n onInit(this);\n }\n }\n\n private initTexture(): void {\n if (!this.gl) return;\n const gl = this.gl;\n this.tex = createAndSetupTexture(gl, gl.LINEAR, gl.LINEAR, gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE);\n\n const itemCount = Math.max(1, this.items.length);\n this.atlasSize = Math.ceil(Math.sqrt(itemCount));\n const cellSize = 512;\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d')!;\n canvas.width = this.atlasSize * cellSize;\n canvas.height = this.atlasSize * cellSize;\n\n Promise.all(\n this.items.map(\n item =>\n new Promise(resolve => {\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => resolve(img);\n img.src = item.image;\n })\n )\n ).then(images => {\n images.forEach((img, i) => {\n const x = (i % this.atlasSize) * cellSize;\n const y = Math.floor(i / this.atlasSize) * cellSize;\n ctx.drawImage(img, x, y, cellSize, cellSize);\n });\n\n gl.bindTexture(gl.TEXTURE_2D, this.tex);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);\n gl.generateMipmap(gl.TEXTURE_2D);\n });\n }\n\n private initDiscInstances(count: number): void {\n if (!this.gl || !this.discVAO) return;\n const gl = this.gl;\n\n const matricesArray = new Float32Array(count * 16);\n const matrices: Float32Array[] = [];\n for (let i = 0; i < count; ++i) {\n const instanceMatrixArray = new Float32Array(matricesArray.buffer, i * 16 * 4, 16);\n mat4.identity(instanceMatrixArray as unknown as mat4);\n matrices.push(instanceMatrixArray);\n }\n\n this.discInstances = {\n matricesArray,\n matrices,\n buffer: gl.createBuffer()\n };\n\n gl.bindVertexArray(this.discVAO);\n gl.bindBuffer(gl.ARRAY_BUFFER, this.discInstances.buffer);\n gl.bufferData(gl.ARRAY_BUFFER, this.discInstances.matricesArray.byteLength, gl.DYNAMIC_DRAW);\n\n const mat4AttribSlotCount = 4;\n const bytesPerMatrix = 16 * 4;\n for (let j = 0; j < mat4AttribSlotCount; ++j) {\n const loc = this.discLocations.aInstanceMatrix + j;\n gl.enableVertexAttribArray(loc);\n gl.vertexAttribPointer(loc, 4, gl.FLOAT, false, bytesPerMatrix, j * 4 * 4);\n gl.vertexAttribDivisor(loc, 1);\n }\n gl.bindBuffer(gl.ARRAY_BUFFER, null);\n gl.bindVertexArray(null);\n }\n\n private animate(deltaTime: number): void {\n if (!this.gl) return;\n this.control.update(deltaTime, this.TARGET_FRAME_DURATION);\n\n const positions = this.instancePositions.map(p => vec3.transformQuat(vec3.create(), p, this.control.orientation));\n const scale = 0.25;\n const SCALE_INTENSITY = 0.6;\n\n positions.forEach((p, ndx) => {\n const s = (Math.abs(p[2]) / this.SPHERE_RADIUS) * SCALE_INTENSITY + (1 - SCALE_INTENSITY);\n const finalScale = s * scale;\n const matrix = mat4.create();\n\n mat4.multiply(matrix, matrix, mat4.fromTranslation(mat4.create(), vec3.negate(vec3.create(), p)));\n mat4.multiply(matrix, matrix, mat4.targetTo(mat4.create(), [0, 0, 0], p, [0, 1, 0]));\n mat4.multiply(matrix, matrix, mat4.fromScaling(mat4.create(), [finalScale, finalScale, finalScale]));\n mat4.multiply(matrix, matrix, mat4.fromTranslation(mat4.create(), [0, 0, -this.SPHERE_RADIUS]));\n\n mat4.copy(this.discInstances.matrices[ndx], matrix);\n });\n\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.discInstances.buffer);\n this.gl.bufferSubData(this.gl.ARRAY_BUFFER, 0, this.discInstances.matricesArray);\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);\n\n this.smoothRotationVelocity = this.control.rotationVelocity;\n }\n\n private render(): void {\n if (!this.gl || !this.discProgram) return;\n const gl = this.gl;\n\n gl.useProgram(this.discProgram);\n gl.enable(gl.CULL_FACE);\n gl.enable(gl.DEPTH_TEST);\n\n gl.clearColor(0, 0, 0, 0);\n gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);\n\n gl.uniformMatrix4fv(this.discLocations.uWorldMatrix, false, this.worldMatrix);\n gl.uniformMatrix4fv(this.discLocations.uViewMatrix, false, this.camera.matrices.view);\n gl.uniformMatrix4fv(this.discLocations.uProjectionMatrix, false, this.camera.matrices.projection);\n gl.uniform3f(\n this.discLocations.uCameraPosition,\n this.camera.position[0],\n this.camera.position[1],\n this.camera.position[2]\n );\n gl.uniform4f(\n this.discLocations.uRotationAxisVelocity,\n this.control.rotationAxis[0],\n this.control.rotationAxis[1],\n this.control.rotationAxis[2],\n this.smoothRotationVelocity * 1.1\n );\n\n gl.uniform1i(this.discLocations.uItemCount, this.items.length);\n gl.uniform1i(this.discLocations.uAtlasSize, this.atlasSize);\n\n gl.uniform1f(this.discLocations.uFrames, this._frames);\n gl.uniform1f(this.discLocations.uScaleFactor, this.scaleFactor);\n\n gl.uniform1i(this.discLocations.uTex, 0);\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, this.tex);\n\n gl.bindVertexArray(this.discVAO);\n gl.drawElementsInstanced(\n gl.TRIANGLES,\n this.discBuffers.indices.length,\n gl.UNSIGNED_SHORT,\n 0,\n this.DISC_INSTANCE_COUNT\n );\n gl.bindVertexArray(null);\n }\n\n private updateCameraMatrix(): void {\n mat4.targetTo(this.camera.matrix, this.camera.position, [0, 0, 0], this.camera.up);\n mat4.invert(this.camera.matrices.view, this.camera.matrix);\n }\n\n private updateProjectionMatrix(): void {\n if (!this.gl) return;\n const canvasEl = this.gl.canvas as HTMLCanvasElement;\n this.camera.aspect = canvasEl.clientWidth / canvasEl.clientHeight;\n const height = this.SPHERE_RADIUS * 0.35;\n const distance = this.camera.position[2];\n if (this.camera.aspect > 1) {\n this.camera.fov = 2 * Math.atan(height / distance);\n } else {\n this.camera.fov = 2 * Math.atan(height / this.camera.aspect / distance);\n }\n mat4.perspective(\n this.camera.matrices.projection,\n this.camera.fov,\n this.camera.aspect,\n this.camera.near,\n this.camera.far\n );\n mat4.invert(this.camera.matrices.inversProjection, this.camera.matrices.projection);\n }\n\n private onControlUpdate(deltaTime: number): void {\n const timeScale = deltaTime / this.TARGET_FRAME_DURATION + 0.0001;\n let damping = 5 / timeScale;\n let cameraTargetZ = 3 * this.scaleFactor;\n\n const isMoving = this.control.isPointerDown || Math.abs(this.smoothRotationVelocity) > 0.01;\n\n if (isMoving !== this.movementActive) {\n this.movementActive = isMoving;\n this.onMovementChange(isMoving);\n }\n\n if (!this.control.isPointerDown) {\n const nearestVertexIndex = this.findNearestVertexIndex();\n const itemIndex = nearestVertexIndex % Math.max(1, this.items.length);\n this.onActiveItemChange(itemIndex);\n const snapDirection = vec3.normalize(vec3.create(), this.getVertexWorldPosition(nearestVertexIndex));\n this.control.snapTargetDirection = snapDirection;\n } else {\n cameraTargetZ += this.control.rotationVelocity * 80 + 2.5;\n damping = 7 / timeScale;\n }\n\n this.camera.position[2] += (cameraTargetZ - this.camera.position[2]) / damping;\n this.updateCameraMatrix();\n }\n\n private findNearestVertexIndex(): number {\n const n = this.control.snapDirection;\n const inversOrientation = quat.conjugate(quat.create(), this.control.orientation);\n const nt = vec3.transformQuat(vec3.create(), n, inversOrientation);\n\n let maxD = -1;\n let nearestVertexIndex = 0;\n for (let i = 0; i < this.instancePositions.length; ++i) {\n const d = vec3.dot(nt, this.instancePositions[i]);\n if (d > maxD) {\n maxD = d;\n nearestVertexIndex = i;\n }\n }\n return nearestVertexIndex;\n }\n\n private getVertexWorldPosition(index: number): vec3 {\n const nearestVertexPos = this.instancePositions[index];\n return vec3.transformQuat(vec3.create(), nearestVertexPos, this.control.orientation);\n }\n}\n\nconst defaultItems: MenuItem[] = [\n {\n image:\n 'https://images.unsplash.com/photo-1782977389500-dd7adad33ebe?q=80&w=600&h=600&fit=crop&sat=-100&auto=format',\n link: 'https://google.com/',\n title: '',\n description: ''\n }\n];\n\ninterface InfiniteMenuProps {\n items?: MenuItem[];\n scale?: number;\n}\n\nconst InfiniteMenu: FC = ({ items = [], scale = 1.0 }) => {\n const canvasRef = useRef(null) as MutableRefObject;\n const [activeItem, setActiveItem] = useState(null);\n const [isMoving, setIsMoving] = useState(false);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n let sketch: InfiniteGridMenu | null = null;\n\n const handleActiveItem = (index: number) => {\n if (!items.length) return;\n const itemIndex = index % items.length;\n setActiveItem(items[itemIndex]);\n };\n\n if (canvas) {\n sketch = new InfiniteGridMenu(\n canvas,\n items.length ? items : defaultItems,\n handleActiveItem,\n setIsMoving,\n sk => sk.run(),\n scale\n );\n }\n\n const handleResize = () => {\n if (sketch) {\n sketch.resize();\n }\n };\n\n window.addEventListener('resize', handleResize);\n handleResize();\n\n return () => {\n window.removeEventListener('resize', handleResize);\n };\n }, [items, scale]);\n\n const handleButtonClick = () => {\n if (!activeItem?.link) return;\n if (activeItem.link.startsWith('http')) {\n window.open(activeItem.link, '_blank');\n } else {\n console.log('Internal route:', activeItem.link);\n }\n };\n\n return (\n
\n \n\n {activeItem && (\n <>\n \n {activeItem.title}\n \n\n \n {activeItem.description}\n

\n\n \n

\n
\n \n )}\n
\n );\n};\n\nexport default InfiniteMenu;\n" + "content": "import { type FC, useRef, useState, useEffect, type MutableRefObject } from 'react';\nimport { mat4, quat, vec2, vec3 } from 'gl-matrix';\n\nconst discVertShaderSource = `#version 300 es\n\nuniform mat4 uWorldMatrix;\nuniform mat4 uViewMatrix;\nuniform mat4 uProjectionMatrix;\nuniform vec3 uCameraPosition;\nuniform vec4 uRotationAxisVelocity;\n\nin vec3 aModelPosition;\nin vec3 aModelNormal;\nin vec2 aModelUvs;\nin mat4 aInstanceMatrix;\n\nout vec2 vUvs;\nout float vAlpha;\nflat out int vInstanceId;\n\n#define PI 3.141593\n\nvoid main() {\n vec4 worldPosition = uWorldMatrix * aInstanceMatrix * vec4(aModelPosition, 1.);\n\n vec3 centerPos = (uWorldMatrix * aInstanceMatrix * vec4(0., 0., 0., 1.)).xyz;\n float radius = length(centerPos.xyz);\n\n if (gl_VertexID > 0) {\n vec3 rotationAxis = uRotationAxisVelocity.xyz;\n float rotationVelocity = min(.15, uRotationAxisVelocity.w * 15.);\n vec3 stretchDir = normalize(cross(centerPos, rotationAxis));\n vec3 relativeVertexPos = normalize(worldPosition.xyz - centerPos);\n float strength = dot(stretchDir, relativeVertexPos);\n float invAbsStrength = min(0., abs(strength) - 1.);\n strength = rotationVelocity * sign(strength) * abs(invAbsStrength * invAbsStrength * invAbsStrength + 1.);\n worldPosition.xyz += stretchDir * strength;\n }\n\n worldPosition.xyz = radius * normalize(worldPosition.xyz);\n\n gl_Position = uProjectionMatrix * uViewMatrix * worldPosition;\n\n vAlpha = smoothstep(0.5, 1., normalize(worldPosition.xyz).z) * .9 + .1;\n vUvs = aModelUvs;\n vInstanceId = gl_InstanceID;\n}\n`;\n\nconst discFragShaderSource = `#version 300 es\nprecision highp float;\n\nuniform sampler2D uTex;\nuniform int uItemCount;\nuniform int uAtlasSize;\n\nout vec4 outColor;\n\nin vec2 vUvs;\nin float vAlpha;\nflat in int vInstanceId;\n\nvoid main() {\n int itemIndex = vInstanceId % uItemCount;\n int cellsPerRow = uAtlasSize;\n int cellX = itemIndex % cellsPerRow;\n int cellY = itemIndex / cellsPerRow;\n vec2 cellSize = vec2(1.0) / vec2(float(cellsPerRow));\n vec2 cellOffset = vec2(float(cellX), float(cellY)) * cellSize;\n\n ivec2 texSize = textureSize(uTex, 0);\n float imageAspect = float(texSize.x) / float(texSize.y);\n float containerAspect = 1.0;\n \n float scale = max(imageAspect / containerAspect, \n containerAspect / imageAspect);\n \n vec2 st = vec2(vUvs.x, 1.0 - vUvs.y);\n st = (st - 0.5) * scale + 0.5;\n \n st = clamp(st, 0.0, 1.0);\n st = st * cellSize + cellOffset;\n \n outColor = texture(uTex, st);\n outColor.a *= vAlpha;\n}\n`;\n\nclass Face {\n public a: number;\n public b: number;\n public c: number;\n\n constructor(a: number, b: number, c: number) {\n this.a = a;\n this.b = b;\n this.c = c;\n }\n}\n\nclass Vertex {\n public position: vec3;\n public normal: vec3;\n public uv: vec2;\n\n constructor(x: number, y: number, z: number) {\n this.position = vec3.fromValues(x, y, z);\n this.normal = vec3.create();\n this.uv = vec2.create();\n }\n}\n\nclass Geometry {\n public vertices: Vertex[];\n public faces: Face[];\n\n constructor() {\n this.vertices = [];\n this.faces = [];\n }\n\n public addVertex(...args: number[]): this {\n for (let i = 0; i < args.length; i += 3) {\n this.vertices.push(new Vertex(args[i], args[i + 1], args[i + 2]));\n }\n return this;\n }\n\n public addFace(...args: number[]): this {\n for (let i = 0; i < args.length; i += 3) {\n this.faces.push(new Face(args[i], args[i + 1], args[i + 2]));\n }\n return this;\n }\n\n public get lastVertex(): Vertex {\n return this.vertices[this.vertices.length - 1];\n }\n\n public subdivide(divisions = 1): this {\n const midPointCache: Record = {};\n let f = this.faces;\n\n for (let div = 0; div < divisions; ++div) {\n const newFaces = new Array(f.length * 4);\n\n f.forEach((face, ndx) => {\n const mAB = this.getMidPoint(face.a, face.b, midPointCache);\n const mBC = this.getMidPoint(face.b, face.c, midPointCache);\n const mCA = this.getMidPoint(face.c, face.a, midPointCache);\n\n const i = ndx * 4;\n newFaces[i + 0] = new Face(face.a, mAB, mCA);\n newFaces[i + 1] = new Face(face.b, mBC, mAB);\n newFaces[i + 2] = new Face(face.c, mCA, mBC);\n newFaces[i + 3] = new Face(mAB, mBC, mCA);\n });\n\n f = newFaces;\n }\n\n this.faces = f;\n return this;\n }\n\n public spherize(radius = 1): this {\n this.vertices.forEach(vertex => {\n vec3.normalize(vertex.normal, vertex.position);\n vec3.scale(vertex.position, vertex.normal, radius);\n });\n return this;\n }\n\n public get data(): {\n vertices: Float32Array;\n indices: Uint16Array;\n normals: Float32Array;\n uvs: Float32Array;\n } {\n return {\n vertices: this.vertexData,\n indices: this.indexData,\n normals: this.normalData,\n uvs: this.uvData\n };\n }\n\n public get vertexData(): Float32Array {\n return new Float32Array(this.vertices.flatMap(v => Array.from(v.position)));\n }\n\n public get normalData(): Float32Array {\n return new Float32Array(this.vertices.flatMap(v => Array.from(v.normal)));\n }\n\n public get uvData(): Float32Array {\n return new Float32Array(this.vertices.flatMap(v => Array.from(v.uv)));\n }\n\n public get indexData(): Uint16Array {\n return new Uint16Array(this.faces.flatMap(f => [f.a, f.b, f.c]));\n }\n\n public getMidPoint(ndxA: number, ndxB: number, cache: Record): number {\n const cacheKey = ndxA < ndxB ? `k_${ndxB}_${ndxA}` : `k_${ndxA}_${ndxB}`;\n if (Object.prototype.hasOwnProperty.call(cache, cacheKey)) {\n return cache[cacheKey];\n }\n const a = this.vertices[ndxA].position;\n const b = this.vertices[ndxB].position;\n const ndx = this.vertices.length;\n cache[cacheKey] = ndx;\n this.addVertex((a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5, (a[2] + b[2]) * 0.5);\n return ndx;\n }\n}\n\nclass IcosahedronGeometry extends Geometry {\n constructor() {\n super();\n const t = Math.sqrt(5) * 0.5 + 0.5;\n this.addVertex(\n -1,\n t,\n 0,\n 1,\n t,\n 0,\n -1,\n -t,\n 0,\n 1,\n -t,\n 0,\n 0,\n -1,\n t,\n 0,\n 1,\n t,\n 0,\n -1,\n -t,\n 0,\n 1,\n -t,\n t,\n 0,\n -1,\n t,\n 0,\n 1,\n -t,\n 0,\n -1,\n -t,\n 0,\n 1\n ).addFace(\n 0,\n 11,\n 5,\n 0,\n 5,\n 1,\n 0,\n 1,\n 7,\n 0,\n 7,\n 10,\n 0,\n 10,\n 11,\n 1,\n 5,\n 9,\n 5,\n 11,\n 4,\n 11,\n 10,\n 2,\n 10,\n 7,\n 6,\n 7,\n 1,\n 8,\n 3,\n 9,\n 4,\n 3,\n 4,\n 2,\n 3,\n 2,\n 6,\n 3,\n 6,\n 8,\n 3,\n 8,\n 9,\n 4,\n 9,\n 5,\n 2,\n 4,\n 11,\n 6,\n 2,\n 10,\n 8,\n 6,\n 7,\n 9,\n 8,\n 1\n );\n }\n}\n\nclass DiscGeometry extends Geometry {\n constructor(steps = 4, radius = 1) {\n super();\n const safeSteps = Math.max(4, steps);\n const alpha = (2 * Math.PI) / safeSteps;\n\n this.addVertex(0, 0, 0);\n this.lastVertex.uv[0] = 0.5;\n this.lastVertex.uv[1] = 0.5;\n\n for (let i = 0; i < safeSteps; ++i) {\n const x = Math.cos(alpha * i);\n const y = Math.sin(alpha * i);\n this.addVertex(radius * x, radius * y, 0);\n this.lastVertex.uv[0] = x * 0.5 + 0.5;\n this.lastVertex.uv[1] = y * 0.5 + 0.5;\n\n if (i > 0) {\n this.addFace(0, i, i + 1);\n }\n }\n this.addFace(0, safeSteps, 1);\n }\n}\n\nfunction createShader(gl: WebGL2RenderingContext, type: number, source: string): WebGLShader | null {\n const shader = gl.createShader(type);\n if (!shader) return null;\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);\n\n if (success) {\n return shader;\n }\n\n console.error(gl.getShaderInfoLog(shader));\n gl.deleteShader(shader);\n return null;\n}\n\nfunction createProgram(\n gl: WebGL2RenderingContext,\n shaderSources: [string, string],\n transformFeedbackVaryings?: string[] | null,\n attribLocations?: Record\n): WebGLProgram | null {\n const program = gl.createProgram();\n if (!program) return null;\n\n [gl.VERTEX_SHADER, gl.FRAGMENT_SHADER].forEach((type, ndx) => {\n const shader = createShader(gl, type, shaderSources[ndx]);\n if (shader) {\n gl.attachShader(program, shader);\n }\n });\n\n if (transformFeedbackVaryings) {\n gl.transformFeedbackVaryings(program, transformFeedbackVaryings, gl.SEPARATE_ATTRIBS);\n }\n\n if (attribLocations) {\n for (const attrib in attribLocations) {\n if (Object.prototype.hasOwnProperty.call(attribLocations, attrib)) {\n gl.bindAttribLocation(program, attribLocations[attrib], attrib);\n }\n }\n }\n\n gl.linkProgram(program);\n const success = gl.getProgramParameter(program, gl.LINK_STATUS);\n\n if (success) {\n return program;\n }\n\n console.error(gl.getProgramInfoLog(program));\n gl.deleteProgram(program);\n return null;\n}\n\nfunction makeVertexArray(\n gl: WebGL2RenderingContext,\n bufLocNumElmPairs: Array<[WebGLBuffer, number, number]>,\n indices?: Uint16Array\n): WebGLVertexArrayObject | null {\n const va = gl.createVertexArray();\n if (!va) return null;\n\n gl.bindVertexArray(va);\n\n for (const [buffer, loc, numElem] of bufLocNumElmPairs) {\n if (loc === -1) continue;\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.enableVertexAttribArray(loc);\n gl.vertexAttribPointer(loc, numElem, gl.FLOAT, false, 0, 0);\n }\n\n if (indices) {\n const indexBuffer = gl.createBuffer();\n if (indexBuffer) {\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);\n gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);\n }\n }\n\n gl.bindVertexArray(null);\n return va;\n}\n\nfunction resizeCanvasToDisplaySize(canvas: HTMLCanvasElement): boolean {\n const dpr = Math.min(2, window.devicePixelRatio || 1);\n const displayWidth = Math.round(canvas.clientWidth * dpr);\n const displayHeight = Math.round(canvas.clientHeight * dpr);\n const needResize = canvas.width !== displayWidth || canvas.height !== displayHeight;\n if (needResize) {\n canvas.width = displayWidth;\n canvas.height = displayHeight;\n }\n return needResize;\n}\n\nfunction makeBuffer(gl: WebGL2RenderingContext, sizeOrData: number | ArrayBufferView, usage: number): WebGLBuffer {\n const buf = gl.createBuffer();\n if (!buf) {\n throw new Error('Failed to create WebGL buffer.');\n }\n gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n\n if (typeof sizeOrData === 'number') {\n gl.bufferData(gl.ARRAY_BUFFER, sizeOrData, usage);\n } else {\n gl.bufferData(gl.ARRAY_BUFFER, sizeOrData, usage);\n }\n\n gl.bindBuffer(gl.ARRAY_BUFFER, null);\n return buf;\n}\n\nfunction createAndSetupTexture(\n gl: WebGL2RenderingContext,\n minFilter: number,\n magFilter: number,\n wrapS: number,\n wrapT: number\n): WebGLTexture {\n const texture = gl.createTexture();\n if (!texture) {\n throw new Error('Failed to create WebGL texture.');\n }\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapS);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapT);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, magFilter);\n return texture;\n}\n\ntype UpdateCallback = (deltaTime: number) => void;\n\nclass ArcballControl {\n private canvas: HTMLCanvasElement;\n private updateCallback: UpdateCallback;\n\n public isPointerDown = false;\n public orientation = quat.create();\n public pointerRotation = quat.create();\n public rotationVelocity = 0;\n public rotationAxis = vec3.fromValues(1, 0, 0);\n\n public snapDirection = vec3.fromValues(0, 0, -1);\n public snapTargetDirection: vec3 | null = null;\n\n private pointerPos = vec2.create();\n private previousPointerPos = vec2.create();\n private _rotationVelocity = 0;\n private _combinedQuat = quat.create();\n\n private readonly EPSILON = 0.1;\n private readonly IDENTITY_QUAT = quat.create();\n\n constructor(canvas: HTMLCanvasElement, updateCallback?: UpdateCallback) {\n this.canvas = canvas;\n this.updateCallback = updateCallback || (() => undefined);\n\n canvas.addEventListener('pointerdown', (e: PointerEvent) => {\n vec2.set(this.pointerPos, e.clientX, e.clientY);\n vec2.copy(this.previousPointerPos, this.pointerPos);\n this.isPointerDown = true;\n });\n canvas.addEventListener('pointerup', () => {\n this.isPointerDown = false;\n });\n canvas.addEventListener('pointerleave', () => {\n this.isPointerDown = false;\n });\n canvas.addEventListener('pointermove', (e: PointerEvent) => {\n if (this.isPointerDown) {\n vec2.set(this.pointerPos, e.clientX, e.clientY);\n }\n });\n canvas.style.touchAction = 'none';\n }\n\n public update(deltaTime: number, targetFrameDuration = 16): void {\n const timeScale = deltaTime / targetFrameDuration + 0.00001;\n let angleFactor = timeScale;\n const snapRotation = quat.create();\n\n if (this.isPointerDown) {\n const INTENSITY = 0.3 * timeScale;\n const ANGLE_AMPLIFICATION = 5 / timeScale;\n const midPointerPos = vec2.sub(vec2.create(), this.pointerPos, this.previousPointerPos);\n vec2.scale(midPointerPos, midPointerPos, INTENSITY);\n\n if (vec2.sqrLen(midPointerPos) > this.EPSILON) {\n vec2.add(midPointerPos, this.previousPointerPos, midPointerPos);\n\n const p = this.project(midPointerPos);\n const q = this.project(this.previousPointerPos);\n const a = vec3.normalize(vec3.create(), p);\n const b = vec3.normalize(vec3.create(), q);\n\n vec2.copy(this.previousPointerPos, midPointerPos);\n\n angleFactor *= ANGLE_AMPLIFICATION;\n\n this.quatFromVectors(a, b, this.pointerRotation, angleFactor);\n } else {\n quat.slerp(this.pointerRotation, this.pointerRotation, this.IDENTITY_QUAT, INTENSITY);\n }\n } else {\n const INTENSITY = 0.1 * timeScale;\n quat.slerp(this.pointerRotation, this.pointerRotation, this.IDENTITY_QUAT, INTENSITY);\n\n if (this.snapTargetDirection) {\n const SNAPPING_INTENSITY = 0.2;\n const a = this.snapTargetDirection;\n const b = this.snapDirection;\n const sqrDist = vec3.squaredDistance(a, b);\n const distanceFactor = Math.max(0.1, 1 - sqrDist * 10);\n angleFactor *= SNAPPING_INTENSITY * distanceFactor;\n this.quatFromVectors(a, b, snapRotation, angleFactor);\n }\n }\n\n const combinedQuat = quat.multiply(quat.create(), snapRotation, this.pointerRotation);\n this.orientation = quat.multiply(quat.create(), combinedQuat, this.orientation);\n quat.normalize(this.orientation, this.orientation);\n\n const RA_INTENSITY = 0.8 * timeScale;\n quat.slerp(this._combinedQuat, this._combinedQuat, combinedQuat, RA_INTENSITY);\n quat.normalize(this._combinedQuat, this._combinedQuat);\n\n const rad = Math.acos(this._combinedQuat[3]) * 2.0;\n const s = Math.sin(rad / 2.0);\n let rv = 0;\n if (s > 0.000001) {\n rv = rad / (2 * Math.PI);\n this.rotationAxis[0] = this._combinedQuat[0] / s;\n this.rotationAxis[1] = this._combinedQuat[1] / s;\n this.rotationAxis[2] = this._combinedQuat[2] / s;\n }\n\n const RV_INTENSITY = 0.5 * timeScale;\n this._rotationVelocity += (rv - this._rotationVelocity) * RV_INTENSITY;\n this.rotationVelocity = this._rotationVelocity / timeScale;\n\n this.updateCallback(deltaTime);\n }\n\n private quatFromVectors(a: vec3, b: vec3, out: quat, angleFactor = 1): { q: quat; axis: vec3; angle: number } {\n const axis = vec3.cross(vec3.create(), a, b);\n vec3.normalize(axis, axis);\n const d = Math.max(-1, Math.min(1, vec3.dot(a, b)));\n const angle = Math.acos(d) * angleFactor;\n quat.setAxisAngle(out, axis, angle);\n return { q: out, axis, angle };\n }\n\n private project(pos: vec2): vec3 {\n const r = 2;\n const w = this.canvas.clientWidth;\n const h = this.canvas.clientHeight;\n const s = Math.max(w, h) - 1;\n\n const x = (2 * pos[0] - w - 1) / s;\n const y = (2 * pos[1] - h - 1) / s;\n let z = 0;\n const xySq = x * x + y * y;\n const rSq = r * r;\n\n if (xySq <= rSq / 2.0) {\n z = Math.sqrt(rSq - xySq);\n } else {\n z = rSq / Math.sqrt(xySq);\n }\n return vec3.fromValues(-x, y, z);\n }\n}\n\ninterface MenuItem {\n image: string;\n link: string;\n title: string;\n description: string;\n}\n\ntype ActiveItemCallback = (index: number) => void;\ntype MovementChangeCallback = (isMoving: boolean) => void;\ntype InitCallback = (instance: InfiniteGridMenu) => void;\n\ninterface Camera {\n matrix: mat4;\n near: number;\n far: number;\n fov: number;\n aspect: number;\n position: vec3;\n up: vec3;\n matrices: {\n view: mat4;\n projection: mat4;\n inversProjection: mat4;\n };\n}\n\nclass InfiniteGridMenu {\n private gl: WebGL2RenderingContext | null = null;\n private discProgram: WebGLProgram | null = null;\n private discVAO: WebGLVertexArrayObject | null = null;\n private discBuffers!: {\n vertices: Float32Array;\n indices: Uint16Array;\n normals: Float32Array;\n uvs: Float32Array;\n };\n private icoGeo!: IcosahedronGeometry;\n private discGeo!: DiscGeometry;\n private worldMatrix = mat4.create();\n private tex: WebGLTexture | null = null;\n private control!: ArcballControl;\n\n private discLocations!: {\n aModelPosition: number;\n aModelUvs: number;\n aInstanceMatrix: number;\n uWorldMatrix: WebGLUniformLocation | null;\n uViewMatrix: WebGLUniformLocation | null;\n uProjectionMatrix: WebGLUniformLocation | null;\n uCameraPosition: WebGLUniformLocation | null;\n uScaleFactor: WebGLUniformLocation | null;\n uRotationAxisVelocity: WebGLUniformLocation | null;\n uTex: WebGLUniformLocation | null;\n uFrames: WebGLUniformLocation | null;\n uItemCount: WebGLUniformLocation | null;\n uAtlasSize: WebGLUniformLocation | null;\n };\n\n private viewportSize = vec2.create();\n private drawBufferSize = vec2.create();\n\n private discInstances!: {\n matricesArray: Float32Array;\n matrices: Float32Array[];\n buffer: WebGLBuffer | null;\n };\n\n private instancePositions: vec3[] = [];\n private DISC_INSTANCE_COUNT = 0;\n private atlasSize = 1;\n\n private _time = 0;\n private _deltaTime = 0;\n private _deltaFrames = 0;\n private _frames = 0;\n\n private movementActive = false;\n\n private TARGET_FRAME_DURATION = 1000 / 60;\n private SPHERE_RADIUS = 2;\n\n public camera: Camera = {\n matrix: mat4.create(),\n near: 0.1,\n far: 40,\n fov: Math.PI / 4,\n aspect: 1,\n position: vec3.fromValues(0, 0, 3),\n up: vec3.fromValues(0, 1, 0),\n matrices: {\n view: mat4.create(),\n projection: mat4.create(),\n inversProjection: mat4.create()\n }\n };\n\n public smoothRotationVelocity = 0;\n public scaleFactor = 1.0;\n\n constructor(\n private canvas: HTMLCanvasElement,\n private items: MenuItem[],\n private onActiveItemChange: ActiveItemCallback,\n private onMovementChange: MovementChangeCallback,\n onInit?: InitCallback,\n scale: number = 1.0\n ) {\n this.scaleFactor = scale;\n this.camera.position[2] = 3 * scale;\n this.init(onInit);\n }\n\n public resize(): void {\n const needsResize = resizeCanvasToDisplaySize(this.canvas);\n if (!this.gl) return;\n if (needsResize) {\n this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight);\n }\n this.updateProjectionMatrix();\n }\n\n public run(time = 0): void {\n this._deltaTime = Math.min(32, time - this._time);\n this._time = time;\n this._deltaFrames = this._deltaTime / this.TARGET_FRAME_DURATION;\n this._frames += this._deltaFrames;\n\n this.animate(this._deltaTime);\n this.render();\n\n requestAnimationFrame(t => this.run(t));\n }\n\n private init(onInit?: InitCallback): void {\n const gl = this.canvas.getContext('webgl2', {\n antialias: true,\n alpha: false\n });\n if (!gl) {\n throw new Error('No WebGL 2 context!');\n }\n this.gl = gl;\n\n vec2.set(this.viewportSize, this.canvas.clientWidth, this.canvas.clientHeight);\n vec2.clone(this.drawBufferSize);\n\n this.discProgram = createProgram(gl, [discVertShaderSource, discFragShaderSource], null, {\n aModelPosition: 0,\n aModelNormal: 1,\n aModelUvs: 2,\n aInstanceMatrix: 3\n });\n\n this.discLocations = {\n aModelPosition: gl.getAttribLocation(this.discProgram!, 'aModelPosition'),\n aModelUvs: gl.getAttribLocation(this.discProgram!, 'aModelUvs'),\n aInstanceMatrix: gl.getAttribLocation(this.discProgram!, 'aInstanceMatrix'),\n uWorldMatrix: gl.getUniformLocation(this.discProgram!, 'uWorldMatrix'),\n uViewMatrix: gl.getUniformLocation(this.discProgram!, 'uViewMatrix'),\n uProjectionMatrix: gl.getUniformLocation(this.discProgram!, 'uProjectionMatrix'),\n uCameraPosition: gl.getUniformLocation(this.discProgram!, 'uCameraPosition'),\n uScaleFactor: gl.getUniformLocation(this.discProgram!, 'uScaleFactor'),\n uRotationAxisVelocity: gl.getUniformLocation(this.discProgram!, 'uRotationAxisVelocity'),\n uTex: gl.getUniformLocation(this.discProgram!, 'uTex'),\n uFrames: gl.getUniformLocation(this.discProgram!, 'uFrames'),\n uItemCount: gl.getUniformLocation(this.discProgram!, 'uItemCount'),\n uAtlasSize: gl.getUniformLocation(this.discProgram!, 'uAtlasSize')\n };\n\n this.discGeo = new DiscGeometry(56, 1);\n this.discBuffers = this.discGeo.data;\n this.discVAO = makeVertexArray(\n gl,\n [\n [makeBuffer(gl, this.discBuffers.vertices, gl.STATIC_DRAW), this.discLocations.aModelPosition, 3],\n [makeBuffer(gl, this.discBuffers.uvs, gl.STATIC_DRAW), this.discLocations.aModelUvs, 2]\n ],\n this.discBuffers.indices\n );\n\n this.icoGeo = new IcosahedronGeometry();\n this.icoGeo.subdivide(1).spherize(this.SPHERE_RADIUS);\n this.instancePositions = this.icoGeo.vertices.map(v => v.position);\n this.DISC_INSTANCE_COUNT = this.icoGeo.vertices.length;\n this.initDiscInstances(this.DISC_INSTANCE_COUNT);\n this.initTexture();\n this.control = new ArcballControl(this.canvas, deltaTime => this.onControlUpdate(deltaTime));\n\n this.updateCameraMatrix();\n this.updateProjectionMatrix();\n\n this.resize();\n\n if (onInit) {\n onInit(this);\n }\n }\n\n private initTexture(): void {\n if (!this.gl) return;\n const gl = this.gl;\n this.tex = createAndSetupTexture(gl, gl.LINEAR, gl.LINEAR, gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE);\n\n const itemCount = Math.max(1, this.items.length);\n this.atlasSize = Math.ceil(Math.sqrt(itemCount));\n const cellSize = 512;\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d')!;\n canvas.width = this.atlasSize * cellSize;\n canvas.height = this.atlasSize * cellSize;\n\n Promise.all(\n this.items.map(\n item =>\n new Promise(resolve => {\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => resolve(img);\n img.src = item.image;\n })\n )\n ).then(images => {\n images.forEach((img, i) => {\n const x = (i % this.atlasSize) * cellSize;\n const y = Math.floor(i / this.atlasSize) * cellSize;\n ctx.drawImage(img, x, y, cellSize, cellSize);\n });\n\n gl.bindTexture(gl.TEXTURE_2D, this.tex);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);\n gl.generateMipmap(gl.TEXTURE_2D);\n });\n }\n\n private initDiscInstances(count: number): void {\n if (!this.gl || !this.discVAO) return;\n const gl = this.gl;\n\n const matricesArray = new Float32Array(count * 16);\n const matrices: Float32Array[] = [];\n for (let i = 0; i < count; ++i) {\n const instanceMatrixArray = new Float32Array(matricesArray.buffer, i * 16 * 4, 16);\n mat4.identity(instanceMatrixArray as unknown as mat4);\n matrices.push(instanceMatrixArray);\n }\n\n this.discInstances = {\n matricesArray,\n matrices,\n buffer: gl.createBuffer()\n };\n\n gl.bindVertexArray(this.discVAO);\n gl.bindBuffer(gl.ARRAY_BUFFER, this.discInstances.buffer);\n gl.bufferData(gl.ARRAY_BUFFER, this.discInstances.matricesArray.byteLength, gl.DYNAMIC_DRAW);\n\n const mat4AttribSlotCount = 4;\n const bytesPerMatrix = 16 * 4;\n for (let j = 0; j < mat4AttribSlotCount; ++j) {\n const loc = this.discLocations.aInstanceMatrix + j;\n gl.enableVertexAttribArray(loc);\n gl.vertexAttribPointer(loc, 4, gl.FLOAT, false, bytesPerMatrix, j * 4 * 4);\n gl.vertexAttribDivisor(loc, 1);\n }\n gl.bindBuffer(gl.ARRAY_BUFFER, null);\n gl.bindVertexArray(null);\n }\n\n private animate(deltaTime: number): void {\n if (!this.gl) return;\n this.control.update(deltaTime, this.TARGET_FRAME_DURATION);\n\n const positions = this.instancePositions.map(p => vec3.transformQuat(vec3.create(), p, this.control.orientation));\n const scale = 0.25;\n const SCALE_INTENSITY = 0.6;\n\n positions.forEach((p, ndx) => {\n const s = (Math.abs(p[2]) / this.SPHERE_RADIUS) * SCALE_INTENSITY + (1 - SCALE_INTENSITY);\n const finalScale = s * scale;\n const matrix = mat4.create();\n\n mat4.multiply(matrix, matrix, mat4.fromTranslation(mat4.create(), vec3.negate(vec3.create(), p)));\n mat4.multiply(matrix, matrix, mat4.targetTo(mat4.create(), [0, 0, 0], p, [0, 1, 0]));\n mat4.multiply(matrix, matrix, mat4.fromScaling(mat4.create(), [finalScale, finalScale, finalScale]));\n mat4.multiply(matrix, matrix, mat4.fromTranslation(mat4.create(), [0, 0, -this.SPHERE_RADIUS]));\n\n mat4.copy(this.discInstances.matrices[ndx], matrix);\n });\n\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.discInstances.buffer);\n this.gl.bufferSubData(this.gl.ARRAY_BUFFER, 0, this.discInstances.matricesArray);\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);\n\n this.smoothRotationVelocity = this.control.rotationVelocity;\n }\n\n private render(): void {\n if (!this.gl || !this.discProgram) return;\n const gl = this.gl;\n\n gl.useProgram(this.discProgram);\n gl.enable(gl.CULL_FACE);\n gl.enable(gl.DEPTH_TEST);\n\n gl.clearColor(0, 0, 0, 0);\n gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);\n\n gl.uniformMatrix4fv(this.discLocations.uWorldMatrix, false, this.worldMatrix);\n gl.uniformMatrix4fv(this.discLocations.uViewMatrix, false, this.camera.matrices.view);\n gl.uniformMatrix4fv(this.discLocations.uProjectionMatrix, false, this.camera.matrices.projection);\n gl.uniform3f(\n this.discLocations.uCameraPosition,\n this.camera.position[0],\n this.camera.position[1],\n this.camera.position[2]\n );\n gl.uniform4f(\n this.discLocations.uRotationAxisVelocity,\n this.control.rotationAxis[0],\n this.control.rotationAxis[1],\n this.control.rotationAxis[2],\n this.smoothRotationVelocity * 1.1\n );\n\n gl.uniform1i(this.discLocations.uItemCount, this.items.length);\n gl.uniform1i(this.discLocations.uAtlasSize, this.atlasSize);\n\n gl.uniform1f(this.discLocations.uFrames, this._frames);\n gl.uniform1f(this.discLocations.uScaleFactor, this.scaleFactor);\n\n gl.uniform1i(this.discLocations.uTex, 0);\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, this.tex);\n\n gl.bindVertexArray(this.discVAO);\n gl.drawElementsInstanced(\n gl.TRIANGLES,\n this.discBuffers.indices.length,\n gl.UNSIGNED_SHORT,\n 0,\n this.DISC_INSTANCE_COUNT\n );\n gl.bindVertexArray(null);\n }\n\n private updateCameraMatrix(): void {\n mat4.targetTo(this.camera.matrix, this.camera.position, [0, 0, 0], this.camera.up);\n mat4.invert(this.camera.matrices.view, this.camera.matrix);\n }\n\n private updateProjectionMatrix(): void {\n if (!this.gl) return;\n const canvasEl = this.gl.canvas as HTMLCanvasElement;\n this.camera.aspect = canvasEl.clientWidth / canvasEl.clientHeight;\n const height = this.SPHERE_RADIUS * 0.35;\n const distance = this.camera.position[2];\n if (this.camera.aspect > 1) {\n this.camera.fov = 2 * Math.atan(height / distance);\n } else {\n this.camera.fov = 2 * Math.atan(height / this.camera.aspect / distance);\n }\n mat4.perspective(\n this.camera.matrices.projection,\n this.camera.fov,\n this.camera.aspect,\n this.camera.near,\n this.camera.far\n );\n mat4.invert(this.camera.matrices.inversProjection, this.camera.matrices.projection);\n }\n\n private onControlUpdate(deltaTime: number): void {\n const timeScale = deltaTime / this.TARGET_FRAME_DURATION + 0.0001;\n let damping = 5 / timeScale;\n let cameraTargetZ = 3 * this.scaleFactor;\n\n const isMoving = this.control.isPointerDown || Math.abs(this.smoothRotationVelocity) > 0.01;\n\n if (isMoving !== this.movementActive) {\n this.movementActive = isMoving;\n this.onMovementChange(isMoving);\n }\n\n if (!this.control.isPointerDown) {\n const nearestVertexIndex = this.findNearestVertexIndex();\n const itemIndex = nearestVertexIndex % Math.max(1, this.items.length);\n this.onActiveItemChange(itemIndex);\n const snapDirection = vec3.normalize(vec3.create(), this.getVertexWorldPosition(nearestVertexIndex));\n this.control.snapTargetDirection = snapDirection;\n } else {\n cameraTargetZ += this.control.rotationVelocity * 80 + 2.5;\n damping = 7 / timeScale;\n }\n\n this.camera.position[2] += (cameraTargetZ - this.camera.position[2]) / damping;\n this.updateCameraMatrix();\n }\n\n private findNearestVertexIndex(): number {\n const n = this.control.snapDirection;\n const inversOrientation = quat.conjugate(quat.create(), this.control.orientation);\n const nt = vec3.transformQuat(vec3.create(), n, inversOrientation);\n\n let maxD = -1;\n let nearestVertexIndex = 0;\n for (let i = 0; i < this.instancePositions.length; ++i) {\n const d = vec3.dot(nt, this.instancePositions[i]);\n if (d > maxD) {\n maxD = d;\n nearestVertexIndex = i;\n }\n }\n return nearestVertexIndex;\n }\n\n private getVertexWorldPosition(index: number): vec3 {\n const nearestVertexPos = this.instancePositions[index];\n return vec3.transformQuat(vec3.create(), nearestVertexPos, this.control.orientation);\n }\n}\n\nconst defaultItems: MenuItem[] = [\n {\n image:\n 'https://images.unsplash.com/photo-1782977389500-dd7adad33ebe?q=80&w=600&h=600&fit=crop&sat=-100&auto=format',\n link: 'https://google.com/',\n title: '',\n description: ''\n }\n];\n\ninterface InfiniteMenuProps {\n items?: MenuItem[];\n scale?: number;\n}\n\nconst InfiniteMenu: FC = ({ items = [], scale = 1.0 }) => {\n const canvasRef = useRef(null) as MutableRefObject;\n const [activeItem, setActiveItem] = useState(null);\n const [isMoving, setIsMoving] = useState(false);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n let sketch: InfiniteGridMenu | null = null;\n\n const handleActiveItem = (index: number) => {\n if (!items.length) return;\n const itemIndex = index % items.length;\n setActiveItem(items[itemIndex]);\n };\n\n if (canvas) {\n sketch = new InfiniteGridMenu(\n canvas,\n items.length ? items : defaultItems,\n handleActiveItem,\n setIsMoving,\n sk => sk.run(),\n scale\n );\n }\n\n const handleResize = () => {\n if (sketch) {\n sketch.resize();\n }\n };\n\n window.addEventListener('resize', handleResize);\n handleResize();\n\n return () => {\n window.removeEventListener('resize', handleResize);\n };\n }, [items, scale]);\n\n const handleButtonClick = () => {\n if (!activeItem?.link) return;\n if (activeItem.link.startsWith('http')) {\n window.open(activeItem.link, '_blank');\n } else {\n console.log('Internal route:', activeItem.link);\n }\n };\n\n return (\n
\n \n\n {activeItem && (\n <>\n \n {activeItem.title}\n \n\n \n {activeItem.description}\n

\n\n \n

\n
\n \n )}\n
\n );\n};\n\nexport default InfiniteMenu;\n" } ], "registryDependencies": [], diff --git a/public/r/Iridescence-JS-CSS.json b/public/r/Iridescence-JS-CSS.json index 9b032d96..15b61596 100644 --- a/public/r/Iridescence-JS-CSS.json +++ b/public/r/Iridescence-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Iridescence/Iridescence.css", + "type": "registry:file", + "path": "Iridescence.css", + "target": "@components/Iridescence.css", "content": ".iridescence-container {\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "Iridescence/Iridescence.jsx", + "path": "Iridescence.jsx", "content": "import { Renderer, Program, Mesh, Color, Triangle } from 'ogl';\nimport { useEffect, useRef } from 'react';\n\nimport './Iridescence.css';\n\nconst vertexShader = `\nattribute vec2 uv;\nattribute vec2 position;\n\nvarying vec2 vUv;\n\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0, 1);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float uTime;\nuniform vec3 uColor;\nuniform vec3 uResolution;\nuniform vec2 uMouse;\nuniform float uAmplitude;\nuniform float uSpeed;\n\nvarying vec2 vUv;\n\nvoid main() {\n float mr = min(uResolution.x, uResolution.y);\n vec2 uv = (vUv.xy * 2.0 - 1.0) * uResolution.xy / mr;\n\n uv += (uMouse - vec2(0.5)) * uAmplitude;\n\n float d = -uTime * 0.5 * uSpeed;\n float a = 0.0;\n for (float i = 0.0; i < 8.0; ++i) {\n a += cos(i - d - a * uv.x);\n d += sin(uv.y * i + a);\n }\n d += uTime * 0.5 * uSpeed;\n vec3 col = vec3(cos(uv * vec2(d, a)) * 0.6 + 0.4, cos(a + d) * 0.5 + 0.5);\n col = cos(col * cos(vec3(d, a, 2.5)) * 0.5 + 0.5) * uColor;\n gl_FragColor = vec4(col, 1.0);\n}\n`;\n\nexport default function Iridescence({ color = [1, 1, 1], speed = 1.0, amplitude = 0.1, mouseReact = true, ...rest }) {\n const ctnDom = useRef(null);\n const mousePos = useRef({ x: 0.5, y: 0.5 });\n\n useEffect(() => {\n if (!ctnDom.current) return;\n const ctn = ctnDom.current;\n const renderer = new Renderer();\n const gl = renderer.gl;\n gl.clearColor(1, 1, 1, 1);\n\n let program;\n\n function resize() {\n const scale = 1;\n renderer.setSize(ctn.offsetWidth * scale, ctn.offsetHeight * scale);\n if (program) {\n program.uniforms.uResolution.value = new Color(\n gl.canvas.width,\n gl.canvas.height,\n gl.canvas.width / gl.canvas.height\n );\n }\n }\n window.addEventListener('resize', resize, false);\n resize();\n\n const geometry = new Triangle(gl);\n program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uColor: { value: new Color(...color) },\n uResolution: {\n value: new Color(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height)\n },\n uMouse: { value: new Float32Array([mousePos.current.x, mousePos.current.y]) },\n uAmplitude: { value: amplitude },\n uSpeed: { value: speed }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n let animateId;\n\n function update(t) {\n animateId = requestAnimationFrame(update);\n program.uniforms.uTime.value = t * 0.001;\n renderer.render({ scene: mesh });\n }\n animateId = requestAnimationFrame(update);\n ctn.appendChild(gl.canvas);\n\n function handleMouseMove(e) {\n const rect = ctn.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1.0 - (e.clientY - rect.top) / rect.height;\n mousePos.current = { x, y };\n program.uniforms.uMouse.value[0] = x;\n program.uniforms.uMouse.value[1] = y;\n }\n if (mouseReact) {\n ctn.addEventListener('mousemove', handleMouseMove);\n }\n\n return () => {\n cancelAnimationFrame(animateId);\n window.removeEventListener('resize', resize);\n if (mouseReact) {\n ctn.removeEventListener('mousemove', handleMouseMove);\n }\n ctn.removeChild(gl.canvas);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, [color, speed, amplitude, mouseReact]);\n\n return
;\n}\n" } ], diff --git a/public/r/Iridescence-TS-CSS.json b/public/r/Iridescence-TS-CSS.json index 4fa6e995..14c46072 100644 --- a/public/r/Iridescence-TS-CSS.json +++ b/public/r/Iridescence-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Iridescence/Iridescence.css", + "type": "registry:file", + "path": "Iridescence.css", + "target": "@components/Iridescence.css", "content": ".iridescence-container {\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "Iridescence/Iridescence.tsx", + "path": "Iridescence.tsx", "content": "import { Renderer, Program, Mesh, Color, Triangle } from 'ogl';\nimport { useEffect, useRef } from 'react';\n\nimport './Iridescence.css';\n\nconst vertexShader = `\nattribute vec2 uv;\nattribute vec2 position;\n\nvarying vec2 vUv;\n\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0, 1);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float uTime;\nuniform vec3 uColor;\nuniform vec3 uResolution;\nuniform vec2 uMouse;\nuniform float uAmplitude;\nuniform float uSpeed;\n\nvarying vec2 vUv;\n\nvoid main() {\n float mr = min(uResolution.x, uResolution.y);\n vec2 uv = (vUv.xy * 2.0 - 1.0) * uResolution.xy / mr;\n\n uv += (uMouse - vec2(0.5)) * uAmplitude;\n\n float d = -uTime * 0.5 * uSpeed;\n float a = 0.0;\n for (float i = 0.0; i < 8.0; ++i) {\n a += cos(i - d - a * uv.x);\n d += sin(uv.y * i + a);\n }\n d += uTime * 0.5 * uSpeed;\n vec3 col = vec3(cos(uv * vec2(d, a)) * 0.6 + 0.4, cos(a + d) * 0.5 + 0.5);\n col = cos(col * cos(vec3(d, a, 2.5)) * 0.5 + 0.5) * uColor;\n gl_FragColor = vec4(col, 1.0);\n}\n`;\n\ninterface IridescenceProps {\n color?: [number, number, number];\n speed?: number;\n amplitude?: number;\n mouseReact?: boolean;\n}\n\nexport default function Iridescence({\n color = [1, 1, 1],\n speed = 1.0,\n amplitude = 0.1,\n mouseReact = true,\n ...rest\n}: IridescenceProps) {\n const ctnDom = useRef(null);\n const mousePos = useRef({ x: 0.5, y: 0.5 });\n\n useEffect(() => {\n if (!ctnDom.current) return;\n const ctn = ctnDom.current;\n const renderer = new Renderer();\n const gl = renderer.gl;\n gl.clearColor(1, 1, 1, 1);\n\n let program: Program;\n\n function resize() {\n const scale = 1;\n renderer.setSize(ctn.offsetWidth * scale, ctn.offsetHeight * scale);\n if (program) {\n program.uniforms.uResolution.value = new Color(\n gl.canvas.width,\n gl.canvas.height,\n gl.canvas.width / gl.canvas.height\n );\n }\n }\n window.addEventListener('resize', resize, false);\n resize();\n\n const geometry = new Triangle(gl);\n program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uColor: { value: new Color(...color) },\n uResolution: {\n value: new Color(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height)\n },\n uMouse: { value: new Float32Array([mousePos.current.x, mousePos.current.y]) },\n uAmplitude: { value: amplitude },\n uSpeed: { value: speed }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n let animateId: number;\n\n function update(t: number) {\n animateId = requestAnimationFrame(update);\n program.uniforms.uTime.value = t * 0.001;\n renderer.render({ scene: mesh });\n }\n animateId = requestAnimationFrame(update);\n ctn.appendChild(gl.canvas);\n\n function handleMouseMove(e: MouseEvent) {\n const rect = ctn.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1.0 - (e.clientY - rect.top) / rect.height;\n mousePos.current = { x, y };\n program.uniforms.uMouse.value[0] = x;\n program.uniforms.uMouse.value[1] = y;\n }\n if (mouseReact) {\n ctn.addEventListener('mousemove', handleMouseMove);\n }\n\n return () => {\n cancelAnimationFrame(animateId);\n window.removeEventListener('resize', resize);\n if (mouseReact) {\n ctn.removeEventListener('mousemove', handleMouseMove);\n }\n ctn.removeChild(gl.canvas);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, [color, speed, amplitude, mouseReact]);\n\n return
;\n}\n" } ], diff --git a/public/r/Lanyard-JS-CSS.json b/public/r/Lanyard-JS-CSS.json index ad9afa3b..68087759 100644 --- a/public/r/Lanyard-JS-CSS.json +++ b/public/r/Lanyard-JS-CSS.json @@ -6,8 +6,9 @@ "type": "registry:component", "files": [ { - "type": "registry:component", + "type": "registry:file", "path": "Lanyard.css", + "target": "@components/Lanyard.css", "content": ".lanyard-wrapper {\n position: relative;\n z-index: 0;\n width: 100%;\n height: 100vh;\n display: flex;\n justify-content: center;\n align-items: center;\n transform: scale(1);\n transform-origin: center;\n}\n" }, { diff --git a/public/r/Lanyard-TS-CSS.json b/public/r/Lanyard-TS-CSS.json index 7e3391bb..b387804a 100644 --- a/public/r/Lanyard-TS-CSS.json +++ b/public/r/Lanyard-TS-CSS.json @@ -6,8 +6,9 @@ "type": "registry:component", "files": [ { - "type": "registry:component", + "type": "registry:file", "path": "Lanyard.css", + "target": "@components/Lanyard.css", "content": ".lanyard-wrapper {\n position: relative;\n z-index: 0;\n width: 100%;\n height: 100vh;\n display: flex;\n justify-content: center;\n align-items: center;\n transform: scale(1);\n transform-origin: center;\n}\n" }, { diff --git a/public/r/LaserFlow-JS-CSS.json b/public/r/LaserFlow-JS-CSS.json index a9b1f7b1..3024eb52 100644 --- a/public/r/LaserFlow-JS-CSS.json +++ b/public/r/LaserFlow-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "LaserFlow/LaserFlow.css", + "type": "registry:file", + "path": "LaserFlow.css", + "target": "@components/LaserFlow.css", "content": ".laser-flow-container {\n width: 100%;\n height: 100%;\n position: relative;\n pointer-events: none;\n}\n" }, { "type": "registry:component", - "path": "LaserFlow/LaserFlow.jsx", + "path": "LaserFlow.jsx", "content": "import { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\nimport './LaserFlow.css';\n\nconst VERT = `\nprecision highp float;\nattribute vec3 position;\nvoid main(){\n gl_Position = vec4(position, 1.0);\n}\n`;\n\nconst FRAG = `\n#ifdef GL_ES\n#extension GL_OES_standard_derivatives : enable\n#endif\nprecision highp float;\nprecision mediump int;\n\nuniform float iTime;\nuniform vec3 iResolution;\nuniform vec4 iMouse;\nuniform float uWispDensity;\nuniform float uTiltScale;\nuniform float uFlowTime;\nuniform float uFogTime;\nuniform float uBeamXFrac;\nuniform float uBeamYFrac;\nuniform float uFlowSpeed;\nuniform float uVLenFactor;\nuniform float uHLenFactor;\nuniform float uFogIntensity;\nuniform float uFogScale;\nuniform float uWSpeed;\nuniform float uWIntensity;\nuniform float uFlowStrength;\nuniform float uDecay;\nuniform float uFalloffStart;\nuniform float uFogFallSpeed;\nuniform vec3 uColor;\nuniform float uFade;\n\n// Core beam/flare shaping and dynamics\n#define PI 3.14159265359\n#define TWO_PI 6.28318530718\n#define EPS 1e-6\n#define EDGE_SOFT (DT_LOCAL*4.0)\n#define DT_LOCAL 0.0038\n#define TAP_RADIUS 6\n#define R_H 150.0\n#define R_V 150.0\n#define FLARE_HEIGHT 16.0\n#define FLARE_AMOUNT 8.0\n#define FLARE_EXP 2.0\n#define TOP_FADE_START 0.1\n#define TOP_FADE_EXP 1.0\n#define FLOW_PERIOD 0.5\n#define FLOW_SHARPNESS 1.5\n\n// Wisps (animated micro-streaks) that travel along the beam\n#define W_BASE_X 1.5\n#define W_LAYER_GAP 0.25\n#define W_LANES 10\n#define W_SIDE_DECAY 0.5\n#define W_HALF 0.01\n#define W_AA 0.15\n#define W_CELL 20.0\n#define W_SEG_MIN 0.01\n#define W_SEG_MAX 0.55\n#define W_CURVE_AMOUNT 15.0\n#define W_CURVE_RANGE (FLARE_HEIGHT - 3.0)\n#define W_BOTTOM_EXP 10.0\n\n// Volumetric fog controls\n#define FOG_ON 1\n#define FOG_CONTRAST 1.2\n#define FOG_SPEED_U 0.1\n#define FOG_SPEED_V -0.1\n#define FOG_OCTAVES 5\n#define FOG_BOTTOM_BIAS 0.8\n#define FOG_TILT_TO_MOUSE 0.05\n#define FOG_TILT_DEADZONE 0.01\n#define FOG_TILT_MAX_X 0.35\n#define FOG_TILT_SHAPE 1.5\n#define FOG_BEAM_MIN 0.0\n#define FOG_BEAM_MAX 0.75\n#define FOG_MASK_GAMMA 0.5\n#define FOG_EXPAND_SHAPE 12.2\n#define FOG_EDGE_MIX 0.5\n\n// Horizontal vignette for the fog volume\n#define HFOG_EDGE_START 0.20\n#define HFOG_EDGE_END 0.98\n#define HFOG_EDGE_GAMMA 1.4\n#define HFOG_Y_RADIUS 25.0\n#define HFOG_Y_SOFT 60.0\n\n// Beam extents and edge masking\n#define EDGE_X0 0.22\n#define EDGE_X1 0.995\n#define EDGE_X_GAMMA 1.25\n#define EDGE_LUMA_T0 0.0\n#define EDGE_LUMA_T1 2.0\n#define DITHER_STRENGTH 1.0\n\n float g(float x){return x<=0.00031308?12.92*x:1.055*pow(x,1.0/2.4)-0.055;}\n float bs(vec2 p,vec2 q,float powr){\n float d=distance(p,q),f=powr*uFalloffStart,r=(f*f)/(d*d+EPS);\n return powr*min(1.0,r);\n }\n float bsa(vec2 p,vec2 q,float powr,vec2 s){\n vec2 d=p-q; float dd=(d.x*d.x)/(s.x*s.x)+(d.y*d.y)/(s.y*s.y),f=powr*uFalloffStart,r=(f*f)/(dd+EPS);\n return powr*min(1.0,r);\n }\n float tri01(float x){float f=fract(x);return 1.0-abs(f*2.0-1.0);}\n float tauWf(float t,float tmin,float tmax){float a=smoothstep(tmin,tmin+EDGE_SOFT,t),b=1.0-smoothstep(tmax-EDGE_SOFT,tmax,t);return max(0.0,a*b);} \n float h21(vec2 p){p=fract(p*vec2(123.34,456.21));p+=dot(p,p+34.123);return fract(p.x*p.y);}\n float vnoise(vec2 p){\n vec2 i=floor(p),f=fract(p);\n float a=h21(i),b=h21(i+vec2(1,0)),c=h21(i+vec2(0,1)),d=h21(i+vec2(1,1));\n vec2 u=f*f*(3.0-2.0*f);\n return mix(mix(a,b,u.x),mix(c,d,u.x),u.y);\n }\n float fbm2(vec2 p){\n float v=0.0,amp=0.6; mat2 m=mat2(0.86,0.5,-0.5,0.86);\n for(int i=0;i=lanes) break;\n float off=W_BASE_X+float(i)*W_LAYER_GAP,xc=sgn*(off*xS);\n float dx=abs(uv.x-xc),lat=1.0-smoothstep(W_HALF,W_HALF+W_AA,dx),amp=exp(-off*W_SIDE_DECAY);\n float seed=h21(vec2(off,sgn*17.0)),yf2=yf+seed*7.0,ci=floor(yf2),fy=fract(yf2);\n float seg=mix(W_SEG_MIN,W_SEG_MAX,h21(vec2(ci,off*2.3)));\n float spR=h21(vec2(ci,off+sgn*31.0)),seg1=rGate(fy,seg)*step(spR,sp);\n if(ep>0.0){float spR2=h21(vec2(ci*3.1+7.0,off*5.3+sgn*13.0)); float f2=fract(fy+0.5); seg1+=rGate(f2,seg*0.9)*step(spR2,ep);}\n sum+=amp*lat*seg1;\n }\n }\n float span=smoothstep(-3.0,0.0,y)*(1.0-smoothstep(R_V-6.0,R_V,y));\n return uWIntensity*sum*topF*bGain*span;\n}\n\nvoid mainImage(out vec4 fc,in vec2 frag){\n vec2 C=iResolution.xy*.5; float invW=1.0/max(C.x,1.0);\n vec2 sc=(512.0/iResolution.xy)*.4;\n vec2 uv=(frag-C)*sc,off=vec2(uBeamXFrac*iResolution.x*sc.x,uBeamYFrac*iResolution.y*sc.y);\n vec2 uvc = uv - off;\n float a=0.0,b=0.0;\n float basePhase=1.5*PI+uDecay*.5; float tauMin=basePhase-uDecay; float tauMax=basePhase;\n float cx=clamp(uvc.x/(R_H*uHLenFactor),-1.0,1.0),tH=clamp(TWO_PI-acos(cx),tauMin,tauMax);\n for(int k=-TAP_RADIUS;k<=TAP_RADIUS;++k){\n float tu=tH+float(k)*DT_LOCAL,wt=tauWf(tu,tauMin,tauMax); if(wt<=0.0) continue;\n float spd=max(abs(sin(tu)),0.02),u=clamp((basePhase-tu)/max(uDecay,EPS),0.0,1.0),env=pow(1.0-abs(u*2.0-1.0),0.8);\n vec2 p=vec2((R_H*uHLenFactor)*cos(tu),0.0);\n a+=wt*bs(uvc,p,env*spd);\n }\n float yPix=uvc.y,cy=clamp(-yPix/(R_V*uVLenFactor),-1.0,1.0),tV=clamp(TWO_PI-acos(cy),tauMin,tauMax);\n for(int k=-TAP_RADIUS;k<=TAP_RADIUS;++k){\n float tu=tV+float(k)*DT_LOCAL,wt=tauWf(tu,tauMin,tauMax); if(wt<=0.0) continue;\n float yb=(-R_V)*cos(tu),s=clamp(yb/R_V,0.0,1.0),spd=max(abs(sin(tu)),0.02);\n float env=pow(1.0-s,0.6)*spd;\n float cap=1.0-smoothstep(TOP_FADE_START,1.0,s); cap=pow(cap,TOP_FADE_EXP); env*=cap;\n float ph=s/max(FLOW_PERIOD,EPS)+uFlowTime*uFlowSpeed;\n float fl=pow(tri01(ph),FLOW_SHARPNESS);\n env*=mix(1.0-uFlowStrength,1.0,fl);\n float yp=(-R_V*uVLenFactor)*cos(tu),m=pow(smoothstep(FLARE_HEIGHT,0.0,yp),FLARE_EXP),wx=1.0+FLARE_AMOUNT*m;\n vec2 sig=vec2(wx,1.0),p=vec2(0.0,yp);\n float mask=step(0.0,yp);\n b+=wt*bsa(uvc,p,mask*env,sig);\n }\n float sPix=clamp(yPix/R_V,0.0,1.0),topA=pow(1.0-smoothstep(TOP_FADE_START,1.0,sPix),TOP_FADE_EXP);\n float L=a+b*topA;\n float w=vWisps(vec2(uvc.x,yPix),topA);\n float fog=0.0;\n#if FOG_ON\n vec2 fuv=uvc*uFogScale;\n float mAct=step(1.0,length(iMouse.xy)),nx=((iMouse.x-C.x)*invW)*mAct;\n float ax = abs(nx);\n float stMag = mix(ax, pow(ax, FOG_TILT_SHAPE), 0.35);\n float st = sign(nx) * stMag * uTiltScale;\n st = clamp(st, -FOG_TILT_MAX_X, FOG_TILT_MAX_X);\n vec2 dir=normalize(vec2(st,1.0));\n fuv+=uFogTime*uFogFallSpeed*dir;\n vec2 prp=vec2(-dir.y,dir.x);\n fuv+=prp*(0.08*sin(dot(uvc,prp)*0.08+uFogTime*0.9));\n float n=fbm2(fuv+vec2(fbm2(fuv+vec2(7.3,2.1)),fbm2(fuv+vec2(-3.7,5.9)))*0.6);\n n=pow(clamp(n,0.0,1.0),FOG_CONTRAST);\n float pixW = 1.0 / max(iResolution.y, 1.0);\n#ifdef GL_OES_standard_derivatives\n float wL = max(fwidth(L), pixW);\n#else\n float wL = pixW;\n#endif\n float m0=pow(smoothstep(FOG_BEAM_MIN - wL, FOG_BEAM_MAX + wL, L),FOG_MASK_GAMMA);\n float bm=1.0-pow(1.0-m0,FOG_EXPAND_SHAPE); bm=mix(bm*m0,bm,FOG_EDGE_MIX);\n float yP=1.0-smoothstep(HFOG_Y_RADIUS,HFOG_Y_RADIUS+HFOG_Y_SOFT,abs(yPix));\n float nxF=abs((frag.x-C.x)*invW),hE=1.0-smoothstep(HFOG_EDGE_START,HFOG_EDGE_END,nxF); hE=pow(clamp(hE,0.0,1.0),HFOG_EDGE_GAMMA);\n float hW=mix(1.0,hE,clamp(yP,0.0,1.0));\n float bBias=mix(1.0,1.0-sPix,FOG_BOTTOM_BIAS);\n float browserFogIntensity = uFogIntensity;\n browserFogIntensity *= 1.8;\n float radialFade = 1.0 - smoothstep(0.0, 0.7, length(uvc) / 120.0);\n float safariFog = n * browserFogIntensity * bBias * bm * hW * radialFade;\n fog = safariFog;\n#endif\n float LF=L+fog;\n float dith=(h21(frag)-0.5)*(DITHER_STRENGTH/255.0);\n float tone=g(LF+w);\n vec3 col=tone*uColor+dith;\n float alpha=clamp(g(L+w*0.6)+dith*0.6,0.0,1.0);\n float nxE=abs((frag.x-C.x)*invW),xF=pow(clamp(1.0-smoothstep(EDGE_X0,EDGE_X1,nxE),0.0,1.0),EDGE_X_GAMMA);\n float scene=LF+max(0.0,w)*0.5,hi=smoothstep(EDGE_LUMA_T0,EDGE_LUMA_T1,scene);\n float eM=mix(xF,1.0,hi);\n col*=eM; alpha*=eM;\n col*=uFade; alpha*=uFade;\n fc=vec4(col,alpha);\n}\n\nvoid main(){\n vec4 fc;\n mainImage(fc, gl_FragCoord.xy);\n gl_FragColor = fc;\n}\n`;\n\nexport const LaserFlow = ({\n className,\n style,\n wispDensity = 1,\n dpr,\n mouseSmoothTime = 0.0,\n mouseTiltStrength = 0.01,\n horizontalBeamOffset = 0.1,\n verticalBeamOffset = 0.0,\n flowSpeed = 0.35,\n verticalSizing = 2.0,\n horizontalSizing = 0.5,\n fogIntensity = 0.45,\n fogScale = 0.3,\n wispSpeed = 15.0,\n wispIntensity = 5.0,\n flowStrength = 0.25,\n decay = 1.1,\n falloffStart = 1.2,\n fogFallSpeed = 0.6,\n color = '#FF79C6'\n}) => {\n const mountRef = useRef(null);\n const rendererRef = useRef(null);\n const uniformsRef = useRef(null);\n const hasFadedRef = useRef(false);\n const rectRef = useRef(null);\n const baseDprRef = useRef(1);\n const currentDprRef = useRef(1);\n const lastSizeRef = useRef({ width: 0, height: 0, dpr: 0 });\n const fpsSamplesRef = useRef([]);\n const lastFpsCheckRef = useRef(performance.now());\n const emaDtRef = useRef(16.7);\n const pausedRef = useRef(false);\n const inViewRef = useRef(true);\n\n const hexToRGB = hex => {\n let c = hex.trim();\n if (c[0] === '#') c = c.slice(1);\n if (c.length === 3)\n c = c\n .split('')\n .map(x => x + x)\n .join('');\n const n = parseInt(c.slice(0, 6), 16) || 0xffffff;\n return { r: ((n >> 16) & 255) / 255, g: ((n >> 8) & 255) / 255, b: (n & 255) / 255 };\n };\n\n useEffect(() => {\n const mount = mountRef.current;\n const renderer = new THREE.WebGLRenderer({\n antialias: false,\n alpha: false,\n depth: false,\n stencil: false,\n powerPreference: 'high-performance',\n premultipliedAlpha: false,\n preserveDrawingBuffer: false,\n failIfMajorPerformanceCaveat: false,\n logarithmicDepthBuffer: false\n });\n rendererRef.current = renderer;\n\n baseDprRef.current = Math.min(dpr ?? (window.devicePixelRatio || 1), 2);\n currentDprRef.current = baseDprRef.current;\n\n renderer.setPixelRatio(currentDprRef.current);\n renderer.shadowMap.enabled = false;\n renderer.outputColorSpace = THREE.SRGBColorSpace;\n renderer.setClearColor(0x000000, 1);\n const canvas = renderer.domElement;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n mount.appendChild(canvas);\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geometry = new THREE.BufferGeometry();\n geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array([-1, -1, 0, 3, -1, 0, -1, 3, 0]), 3));\n\n const uniforms = {\n iTime: { value: 0 },\n iResolution: { value: new THREE.Vector3(1, 1, 1) },\n iMouse: { value: new THREE.Vector4(0, 0, 0, 0) },\n uWispDensity: { value: wispDensity },\n uTiltScale: { value: mouseTiltStrength },\n uFlowTime: { value: 0 },\n uFogTime: { value: 0 },\n uBeamXFrac: { value: horizontalBeamOffset },\n uBeamYFrac: { value: verticalBeamOffset },\n uFlowSpeed: { value: flowSpeed },\n uVLenFactor: { value: verticalSizing },\n uHLenFactor: { value: horizontalSizing },\n uFogIntensity: { value: fogIntensity },\n uFogScale: { value: fogScale },\n uWSpeed: { value: wispSpeed },\n uWIntensity: { value: wispIntensity },\n uFlowStrength: { value: flowStrength },\n uDecay: { value: decay },\n uFalloffStart: { value: falloffStart },\n uFogFallSpeed: { value: fogFallSpeed },\n uColor: { value: new THREE.Vector3(1, 1, 1) },\n uFade: { value: hasFadedRef.current ? 1 : 0 }\n };\n uniformsRef.current = uniforms;\n\n const material = new THREE.RawShaderMaterial({\n vertexShader: VERT,\n fragmentShader: FRAG,\n uniforms,\n transparent: false,\n depthTest: false,\n depthWrite: false,\n blending: THREE.NormalBlending\n });\n\n const mesh = new THREE.Mesh(geometry, material);\n mesh.frustumCulled = false;\n scene.add(mesh);\n\n const clock = new THREE.Clock();\n let prevTime = 0;\n let fade = hasFadedRef.current ? 1 : 0;\n\n const mouseTarget = new THREE.Vector2(0, 0);\n const mouseSmooth = new THREE.Vector2(0, 0);\n\n const setSizeNow = () => {\n const w = mount.clientWidth || 1;\n const h = mount.clientHeight || 1;\n const pr = currentDprRef.current;\n\n const last = lastSizeRef.current;\n const sizeChanged = Math.abs(w - last.width) > 0.5 || Math.abs(h - last.height) > 0.5;\n const dprChanged = Math.abs(pr - last.dpr) > 0.01;\n if (!sizeChanged && !dprChanged) {\n return;\n }\n\n lastSizeRef.current = { width: w, height: h, dpr: pr };\n renderer.setPixelRatio(pr);\n renderer.setSize(w, h, false);\n uniforms.iResolution.value.set(w * pr, h * pr, pr);\n rectRef.current = canvas.getBoundingClientRect();\n\n if (!pausedRef.current) {\n renderer.render(scene, camera);\n }\n };\n\n let resizeRaf = 0;\n const scheduleResize = () => {\n if (resizeRaf) cancelAnimationFrame(resizeRaf);\n resizeRaf = requestAnimationFrame(setSizeNow);\n };\n\n setSizeNow();\n const ro = new ResizeObserver(scheduleResize);\n ro.observe(mount);\n\n const io = new IntersectionObserver(\n entries => {\n inViewRef.current = entries[0]?.isIntersecting ?? true;\n },\n { root: null, threshold: 0 }\n );\n io.observe(mount);\n\n const onVis = () => {\n pausedRef.current = document.hidden;\n };\n document.addEventListener('visibilitychange', onVis, { passive: true });\n\n const updateMouse = (clientX, clientY) => {\n const rect = rectRef.current;\n if (!rect) return;\n const x = clientX - rect.left;\n const y = clientY - rect.top;\n const ratio = currentDprRef.current;\n const hb = rect.height * ratio;\n mouseTarget.set(x * ratio, hb - y * ratio);\n };\n const onMove = ev => updateMouse(ev.clientX, ev.clientY);\n const onLeave = () => mouseTarget.set(0, 0);\n canvas.addEventListener('pointermove', onMove, { passive: true });\n canvas.addEventListener('pointerdown', onMove, { passive: true });\n canvas.addEventListener('pointerenter', onMove, { passive: true });\n canvas.addEventListener('pointerleave', onLeave, { passive: true });\n\n const onCtxLost = e => {\n e.preventDefault();\n pausedRef.current = true;\n };\n const onCtxRestored = () => {\n pausedRef.current = false;\n scheduleResize();\n };\n canvas.addEventListener('webglcontextlost', onCtxLost, false);\n canvas.addEventListener('webglcontextrestored', onCtxRestored, false);\n\n let raf = 0;\n\n const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));\n const dprFloor = 0.6;\n const lowerThresh = 50;\n const upperThresh = 58;\n let lastDprChangeRef = 0;\n const dprChangeCooldown = 2000;\n\n const adjustDprIfNeeded = now => {\n const elapsed = now - lastFpsCheckRef.current;\n if (elapsed < 750) return;\n\n const samples = fpsSamplesRef.current;\n if (samples.length === 0) {\n lastFpsCheckRef.current = now;\n return;\n }\n const avgFps = samples.reduce((a, b) => a + b, 0) / samples.length;\n\n let next = currentDprRef.current;\n const base = baseDprRef.current;\n\n if (avgFps < lowerThresh) {\n next = clamp(currentDprRef.current * 0.85, dprFloor, base);\n } else if (avgFps > upperThresh && currentDprRef.current < base) {\n next = clamp(currentDprRef.current * 1.1, dprFloor, base);\n }\n\n if (Math.abs(next - currentDprRef.current) > 0.01 && now - lastDprChangeRef > dprChangeCooldown) {\n currentDprRef.current = next;\n lastDprChangeRef = now;\n setSizeNow();\n }\n\n fpsSamplesRef.current = [];\n lastFpsCheckRef.current = now;\n };\n\n const animate = () => {\n raf = requestAnimationFrame(animate);\n if (pausedRef.current || !inViewRef.current) return;\n\n const t = clock.getElapsedTime();\n const dt = Math.max(0, t - prevTime);\n prevTime = t;\n\n const dtMs = dt * 1000;\n emaDtRef.current = emaDtRef.current * 0.9 + dtMs * 0.1;\n const instFps = 1000 / Math.max(1, emaDtRef.current);\n fpsSamplesRef.current.push(instFps);\n\n uniforms.iTime.value = t;\n\n const cdt = Math.min(0.033, Math.max(0.001, dt));\n uniforms.uFlowTime.value += cdt;\n uniforms.uFogTime.value += cdt;\n\n if (!hasFadedRef.current) {\n const fadeDur = 1.0;\n fade = Math.min(1, fade + cdt / fadeDur);\n uniforms.uFade.value = fade;\n if (fade >= 1) hasFadedRef.current = true;\n }\n\n const tau = Math.max(1e-3, mouseSmoothTime);\n const alpha = 1 - Math.exp(-cdt / tau);\n mouseSmooth.lerp(mouseTarget, alpha);\n uniforms.iMouse.value.set(mouseSmooth.x, mouseSmooth.y, 0, 0);\n\n renderer.render(scene, camera);\n\n adjustDprIfNeeded(performance.now());\n };\n\n animate();\n\n return () => {\n cancelAnimationFrame(raf);\n ro.disconnect();\n io.disconnect();\n document.removeEventListener('visibilitychange', onVis);\n canvas.removeEventListener('pointermove', onMove);\n canvas.removeEventListener('pointerdown', onMove);\n canvas.removeEventListener('pointerenter', onMove);\n canvas.removeEventListener('pointerleave', onLeave);\n canvas.removeEventListener('webglcontextlost', onCtxLost);\n canvas.removeEventListener('webglcontextrestored', onCtxRestored);\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n renderer.forceContextLoss();\n if (mount.contains(canvas)) mount.removeChild(canvas);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [dpr]);\n\n useEffect(() => {\n const uniforms = uniformsRef.current;\n if (!uniforms) return;\n\n uniforms.uWispDensity.value = wispDensity;\n uniforms.uTiltScale.value = mouseTiltStrength;\n uniforms.uBeamXFrac.value = horizontalBeamOffset;\n uniforms.uBeamYFrac.value = verticalBeamOffset;\n uniforms.uFlowSpeed.value = flowSpeed;\n uniforms.uVLenFactor.value = verticalSizing;\n uniforms.uHLenFactor.value = horizontalSizing;\n uniforms.uFogIntensity.value = fogIntensity;\n uniforms.uFogScale.value = fogScale;\n uniforms.uWSpeed.value = wispSpeed;\n uniforms.uWIntensity.value = wispIntensity;\n uniforms.uFlowStrength.value = flowStrength;\n uniforms.uDecay.value = decay;\n uniforms.uFalloffStart.value = falloffStart;\n uniforms.uFogFallSpeed.value = fogFallSpeed;\n\n const { r, g, b } = hexToRGB(color || '#FFFFFF');\n uniforms.uColor.value.set(r, g, b);\n }, [\n wispDensity,\n mouseTiltStrength,\n horizontalBeamOffset,\n verticalBeamOffset,\n flowSpeed,\n verticalSizing,\n horizontalSizing,\n fogIntensity,\n fogScale,\n wispSpeed,\n wispIntensity,\n flowStrength,\n decay,\n falloffStart,\n fogFallSpeed,\n color\n ]);\n\n return
;\n};\n\nexport default LaserFlow;\n" } ], diff --git a/public/r/LaserFlow-TS-CSS.json b/public/r/LaserFlow-TS-CSS.json index db45d133..e8213b27 100644 --- a/public/r/LaserFlow-TS-CSS.json +++ b/public/r/LaserFlow-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "LaserFlow/LaserFlow.css", + "type": "registry:file", + "path": "LaserFlow.css", + "target": "@components/LaserFlow.css", "content": ".laser-flow-container {\n width: 100%;\n height: 100%;\n position: relative;\n pointer-events: none;\n}\n" }, { "type": "registry:component", - "path": "LaserFlow/LaserFlow.tsx", + "path": "LaserFlow.tsx", "content": "import React, { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\nimport './LaserFlow.css';\n\ntype Props = {\n className?: string;\n style?: React.CSSProperties;\n wispDensity?: number;\n dpr?: number;\n mouseSmoothTime?: number;\n mouseTiltStrength?: number;\n horizontalBeamOffset?: number;\n verticalBeamOffset?: number;\n flowSpeed?: number;\n verticalSizing?: number;\n horizontalSizing?: number;\n fogIntensity?: number;\n fogScale?: number;\n wispSpeed?: number;\n wispIntensity?: number;\n flowStrength?: number;\n decay?: number;\n falloffStart?: number;\n fogFallSpeed?: number;\n color?: string;\n};\n\nconst VERT = `\nprecision highp float;\nattribute vec3 position;\nvoid main(){\n gl_Position = vec4(position, 1.0);\n}\n`;\n\nconst FRAG = `\n#ifdef GL_ES\n#extension GL_OES_standard_derivatives : enable\n#endif\nprecision highp float;\nprecision mediump int;\n\nuniform float iTime;\nuniform vec3 iResolution;\nuniform vec4 iMouse;\nuniform float uWispDensity;\nuniform float uTiltScale;\nuniform float uFlowTime;\nuniform float uFogTime;\nuniform float uBeamXFrac;\nuniform float uBeamYFrac;\nuniform float uFlowSpeed;\nuniform float uVLenFactor;\nuniform float uHLenFactor;\nuniform float uFogIntensity;\nuniform float uFogScale;\nuniform float uWSpeed;\nuniform float uWIntensity;\nuniform float uFlowStrength;\nuniform float uDecay;\nuniform float uFalloffStart;\nuniform float uFogFallSpeed;\nuniform vec3 uColor;\nuniform float uFade;\n\n// Core beam/flare shaping and dynamics\n#define PI 3.14159265359\n#define TWO_PI 6.28318530718\n#define EPS 1e-6\n#define EDGE_SOFT (DT_LOCAL*4.0)\n#define DT_LOCAL 0.0038\n#define TAP_RADIUS 6\n#define R_H 150.0\n#define R_V 150.0\n#define FLARE_HEIGHT 16.0\n#define FLARE_AMOUNT 8.0\n#define FLARE_EXP 2.0\n#define TOP_FADE_START 0.1\n#define TOP_FADE_EXP 1.0\n#define FLOW_PERIOD 0.5\n#define FLOW_SHARPNESS 1.5\n\n// Wisps (animated micro-streaks) that travel along the beam\n#define W_BASE_X 1.5\n#define W_LAYER_GAP 0.25\n#define W_LANES 10\n#define W_SIDE_DECAY 0.5\n#define W_HALF 0.01\n#define W_AA 0.15\n#define W_CELL 20.0\n#define W_SEG_MIN 0.01\n#define W_SEG_MAX 0.55\n#define W_CURVE_AMOUNT 15.0\n#define W_CURVE_RANGE (FLARE_HEIGHT - 3.0)\n#define W_BOTTOM_EXP 10.0\n\n// Volumetric fog controls\n#define FOG_ON 1\n#define FOG_CONTRAST 1.2\n#define FOG_SPEED_U 0.1\n#define FOG_SPEED_V -0.1\n#define FOG_OCTAVES 5\n#define FOG_BOTTOM_BIAS 0.8\n#define FOG_TILT_TO_MOUSE 0.05\n#define FOG_TILT_DEADZONE 0.01\n#define FOG_TILT_MAX_X 0.35\n#define FOG_TILT_SHAPE 1.5\n#define FOG_BEAM_MIN 0.0\n#define FOG_BEAM_MAX 0.75\n#define FOG_MASK_GAMMA 0.5\n#define FOG_EXPAND_SHAPE 12.2\n#define FOG_EDGE_MIX 0.5\n\n// Horizontal vignette for the fog volume\n#define HFOG_EDGE_START 0.20\n#define HFOG_EDGE_END 0.98\n#define HFOG_EDGE_GAMMA 1.4\n#define HFOG_Y_RADIUS 25.0\n#define HFOG_Y_SOFT 60.0\n\n// Beam extents and edge masking\n#define EDGE_X0 0.22\n#define EDGE_X1 0.995\n#define EDGE_X_GAMMA 1.25\n#define EDGE_LUMA_T0 0.0\n#define EDGE_LUMA_T1 2.0\n#define DITHER_STRENGTH 1.0\n\n float g(float x){return x<=0.00031308?12.92*x:1.055*pow(x,1.0/2.4)-0.055;}\n float bs(vec2 p,vec2 q,float powr){\n float d=distance(p,q),f=powr*uFalloffStart,r=(f*f)/(d*d+EPS);\n return powr*min(1.0,r);\n }\n float bsa(vec2 p,vec2 q,float powr,vec2 s){\n vec2 d=p-q; float dd=(d.x*d.x)/(s.x*s.x)+(d.y*d.y)/(s.y*s.y),f=powr*uFalloffStart,r=(f*f)/(dd+EPS);\n return powr*min(1.0,r);\n }\n float tri01(float x){float f=fract(x);return 1.0-abs(f*2.0-1.0);}\n float tauWf(float t,float tmin,float tmax){float a=smoothstep(tmin,tmin+EDGE_SOFT,t),b=1.0-smoothstep(tmax-EDGE_SOFT,tmax,t);return max(0.0,a*b);} \n float h21(vec2 p){p=fract(p*vec2(123.34,456.21));p+=dot(p,p+34.123);return fract(p.x*p.y);}\n float vnoise(vec2 p){\n vec2 i=floor(p),f=fract(p);\n float a=h21(i),b=h21(i+vec2(1,0)),c=h21(i+vec2(0,1)),d=h21(i+vec2(1,1));\n vec2 u=f*f*(3.0-2.0*f);\n return mix(mix(a,b,u.x),mix(c,d,u.x),u.y);\n }\n float fbm2(vec2 p){\n float v=0.0,amp=0.6; mat2 m=mat2(0.86,0.5,-0.5,0.86);\n for(int i=0;i=lanes) break;\n float off=W_BASE_X+float(i)*W_LAYER_GAP,xc=sgn*(off*xS);\n float dx=abs(uv.x-xc),lat=1.0-smoothstep(W_HALF,W_HALF+W_AA,dx),amp=exp(-off*W_SIDE_DECAY);\n float seed=h21(vec2(off,sgn*17.0)),yf2=yf+seed*7.0,ci=floor(yf2),fy=fract(yf2);\n float seg=mix(W_SEG_MIN,W_SEG_MAX,h21(vec2(ci,off*2.3)));\n float spR=h21(vec2(ci,off+sgn*31.0)),seg1=rGate(fy,seg)*step(spR,sp);\n if(ep>0.0){float spR2=h21(vec2(ci*3.1+7.0,off*5.3+sgn*13.0)); float f2=fract(fy+0.5); seg1+=rGate(f2,seg*0.9)*step(spR2,ep);}\n sum+=amp*lat*seg1;\n }\n }\n float span=smoothstep(-3.0,0.0,y)*(1.0-smoothstep(R_V-6.0,R_V,y));\n return uWIntensity*sum*topF*bGain*span;\n}\n\nvoid mainImage(out vec4 fc,in vec2 frag){\n vec2 C=iResolution.xy*.5; float invW=1.0/max(C.x,1.0);\n vec2 sc=(512.0/iResolution.xy)*.4;\n vec2 uv=(frag-C)*sc,off=vec2(uBeamXFrac*iResolution.x*sc.x,uBeamYFrac*iResolution.y*sc.y);\n vec2 uvc = uv - off;\n float a=0.0,b=0.0;\n float basePhase=1.5*PI+uDecay*.5; float tauMin=basePhase-uDecay; float tauMax=basePhase;\n float cx=clamp(uvc.x/(R_H*uHLenFactor),-1.0,1.0),tH=clamp(TWO_PI-acos(cx),tauMin,tauMax);\n for(int k=-TAP_RADIUS;k<=TAP_RADIUS;++k){\n float tu=tH+float(k)*DT_LOCAL,wt=tauWf(tu,tauMin,tauMax); if(wt<=0.0) continue;\n float spd=max(abs(sin(tu)),0.02),u=clamp((basePhase-tu)/max(uDecay,EPS),0.0,1.0),env=pow(1.0-abs(u*2.0-1.0),0.8);\n vec2 p=vec2((R_H*uHLenFactor)*cos(tu),0.0);\n a+=wt*bs(uvc,p,env*spd);\n }\n float yPix=uvc.y,cy=clamp(-yPix/(R_V*uVLenFactor),-1.0,1.0),tV=clamp(TWO_PI-acos(cy),tauMin,tauMax);\n for(int k=-TAP_RADIUS;k<=TAP_RADIUS;++k){\n float tu=tV+float(k)*DT_LOCAL,wt=tauWf(tu,tauMin,tauMax); if(wt<=0.0) continue;\n float yb=(-R_V)*cos(tu),s=clamp(yb/R_V,0.0,1.0),spd=max(abs(sin(tu)),0.02);\n float env=pow(1.0-s,0.6)*spd;\n float cap=1.0-smoothstep(TOP_FADE_START,1.0,s); cap=pow(cap,TOP_FADE_EXP); env*=cap;\n float ph=s/max(FLOW_PERIOD,EPS)+uFlowTime*uFlowSpeed;\n float fl=pow(tri01(ph),FLOW_SHARPNESS);\n env*=mix(1.0-uFlowStrength,1.0,fl);\n float yp=(-R_V*uVLenFactor)*cos(tu),m=pow(smoothstep(FLARE_HEIGHT,0.0,yp),FLARE_EXP),wx=1.0+FLARE_AMOUNT*m;\n vec2 sig=vec2(wx,1.0),p=vec2(0.0,yp);\n float mask=step(0.0,yp);\n b+=wt*bsa(uvc,p,mask*env,sig);\n }\n float sPix=clamp(yPix/R_V,0.0,1.0),topA=pow(1.0-smoothstep(TOP_FADE_START,1.0,sPix),TOP_FADE_EXP);\n float L=a+b*topA;\n float w=vWisps(vec2(uvc.x,yPix),topA);\n float fog=0.0;\n#if FOG_ON\n vec2 fuv=uvc*uFogScale;\n float mAct=step(1.0,length(iMouse.xy)),nx=((iMouse.x-C.x)*invW)*mAct;\n float ax = abs(nx);\n float stMag = mix(ax, pow(ax, FOG_TILT_SHAPE), 0.35);\n float st = sign(nx) * stMag * uTiltScale;\n st = clamp(st, -FOG_TILT_MAX_X, FOG_TILT_MAX_X);\n vec2 dir=normalize(vec2(st,1.0));\n fuv+=uFogTime*uFogFallSpeed*dir;\n vec2 prp=vec2(-dir.y,dir.x);\n fuv+=prp*(0.08*sin(dot(uvc,prp)*0.08+uFogTime*0.9));\n float n=fbm2(fuv+vec2(fbm2(fuv+vec2(7.3,2.1)),fbm2(fuv+vec2(-3.7,5.9)))*0.6);\n n=pow(clamp(n,0.0,1.0),FOG_CONTRAST);\n float pixW = 1.0 / max(iResolution.y, 1.0);\n#ifdef GL_OES_standard_derivatives\n float wL = max(fwidth(L), pixW);\n#else\n float wL = pixW;\n#endif\n float m0=pow(smoothstep(FOG_BEAM_MIN - wL, FOG_BEAM_MAX + wL, L),FOG_MASK_GAMMA);\n float bm=1.0-pow(1.0-m0,FOG_EXPAND_SHAPE); bm=mix(bm*m0,bm,FOG_EDGE_MIX);\n float yP=1.0-smoothstep(HFOG_Y_RADIUS,HFOG_Y_RADIUS+HFOG_Y_SOFT,abs(yPix));\n float nxF=abs((frag.x-C.x)*invW),hE=1.0-smoothstep(HFOG_EDGE_START,HFOG_EDGE_END,nxF); hE=pow(clamp(hE,0.0,1.0),HFOG_EDGE_GAMMA);\n float hW=mix(1.0,hE,clamp(yP,0.0,1.0));\n float bBias=mix(1.0,1.0-sPix,FOG_BOTTOM_BIAS);\n float browserFogIntensity = uFogIntensity;\n browserFogIntensity *= 1.8;\n float radialFade = 1.0 - smoothstep(0.0, 0.7, length(uvc) / 120.0);\n float safariFog = n * browserFogIntensity * bBias * bm * hW * radialFade;\n fog = safariFog;\n#endif\n float LF=L+fog;\n float dith=(h21(frag)-0.5)*(DITHER_STRENGTH/255.0);\n float tone=g(LF+w);\n vec3 col=tone*uColor+dith;\n float alpha=clamp(g(L+w*0.6)+dith*0.6,0.0,1.0);\n float nxE=abs((frag.x-C.x)*invW),xF=pow(clamp(1.0-smoothstep(EDGE_X0,EDGE_X1,nxE),0.0,1.0),EDGE_X_GAMMA);\n float scene=LF+max(0.0,w)*0.5,hi=smoothstep(EDGE_LUMA_T0,EDGE_LUMA_T1,scene);\n float eM=mix(xF,1.0,hi);\n col*=eM; alpha*=eM;\n col*=uFade; alpha*=uFade;\n fc=vec4(col,alpha);\n}\n\nvoid main(){\n vec4 fc;\n mainImage(fc, gl_FragCoord.xy);\n gl_FragColor = fc;\n}\n`;\n\nexport const LaserFlow: React.FC = ({\n className,\n style,\n wispDensity = 1,\n dpr,\n mouseSmoothTime = 0.0,\n mouseTiltStrength = 0.01,\n horizontalBeamOffset = 0.1,\n verticalBeamOffset = 0.0,\n flowSpeed = 0.35,\n verticalSizing = 2.0,\n horizontalSizing = 0.5,\n fogIntensity = 0.45,\n fogScale = 0.3,\n wispSpeed = 15.0,\n wispIntensity = 5.0,\n flowStrength = 0.25,\n decay = 1.1,\n falloffStart = 1.2,\n fogFallSpeed = 0.6,\n color = '#FF79C6'\n}) => {\n const mountRef = useRef(null);\n const rendererRef = useRef(null);\n const uniformsRef = useRef(null);\n const hasFadedRef = useRef(false);\n const rectRef = useRef(null);\n const baseDprRef = useRef(1);\n const currentDprRef = useRef(1);\n const lastSizeRef = useRef({ width: 0, height: 0, dpr: 0 });\n const fpsSamplesRef = useRef([]);\n const lastFpsCheckRef = useRef(performance.now());\n const emaDtRef = useRef(16.7); // ms\n const pausedRef = useRef(false);\n const inViewRef = useRef(true);\n\n const hexToRGB = (hex: string) => {\n let c = hex.trim();\n if (c[0] === '#') c = c.slice(1);\n if (c.length === 3)\n c = c\n .split('')\n .map(x => x + x)\n .join('');\n const n = parseInt(c.slice(0, 6), 16) || 0xffffff;\n return { r: ((n >> 16) & 255) / 255, g: ((n >> 8) & 255) / 255, b: (n & 255) / 255 };\n };\n\n useEffect(() => {\n const mount = mountRef.current!;\n const renderer = new THREE.WebGLRenderer({\n antialias: false,\n alpha: false,\n depth: false,\n stencil: false,\n powerPreference: 'high-performance',\n premultipliedAlpha: false,\n preserveDrawingBuffer: false,\n failIfMajorPerformanceCaveat: false,\n logarithmicDepthBuffer: false\n });\n rendererRef.current = renderer;\n\n baseDprRef.current = Math.min(dpr ?? (window.devicePixelRatio || 1), 2);\n currentDprRef.current = baseDprRef.current;\n\n renderer.setPixelRatio(currentDprRef.current);\n renderer.shadowMap.enabled = false;\n renderer.outputColorSpace = THREE.SRGBColorSpace;\n renderer.setClearColor(0x000000, 1);\n const canvas = renderer.domElement;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n mount.appendChild(canvas);\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geometry = new THREE.BufferGeometry();\n geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array([-1, -1, 0, 3, -1, 0, -1, 3, 0]), 3));\n\n const uniforms = {\n iTime: { value: 0 },\n iResolution: { value: new THREE.Vector3(1, 1, 1) },\n iMouse: { value: new THREE.Vector4(0, 0, 0, 0) },\n uWispDensity: { value: wispDensity },\n uTiltScale: { value: mouseTiltStrength },\n uFlowTime: { value: 0 },\n uFogTime: { value: 0 },\n uBeamXFrac: { value: horizontalBeamOffset },\n uBeamYFrac: { value: verticalBeamOffset },\n uFlowSpeed: { value: flowSpeed },\n uVLenFactor: { value: verticalSizing },\n uHLenFactor: { value: horizontalSizing },\n uFogIntensity: { value: fogIntensity },\n uFogScale: { value: fogScale },\n uWSpeed: { value: wispSpeed },\n uWIntensity: { value: wispIntensity },\n uFlowStrength: { value: flowStrength },\n uDecay: { value: decay },\n uFalloffStart: { value: falloffStart },\n uFogFallSpeed: { value: fogFallSpeed },\n uColor: { value: new THREE.Vector3(1, 1, 1) },\n uFade: { value: hasFadedRef.current ? 1 : 0 }\n };\n uniformsRef.current = uniforms;\n\n const material = new THREE.RawShaderMaterial({\n vertexShader: VERT,\n fragmentShader: FRAG,\n uniforms,\n transparent: false,\n depthTest: false,\n depthWrite: false,\n blending: THREE.NormalBlending\n });\n\n const mesh = new THREE.Mesh(geometry, material);\n mesh.frustumCulled = false;\n scene.add(mesh);\n\n const clock = new THREE.Clock();\n let prevTime = 0;\n let fade = hasFadedRef.current ? 1 : 0;\n\n const mouseTarget = new THREE.Vector2(0, 0);\n const mouseSmooth = new THREE.Vector2(0, 0);\n\n const setSizeNow = () => {\n const w = mount.clientWidth || 1;\n const h = mount.clientHeight || 1;\n const pr = currentDprRef.current;\n\n const last = lastSizeRef.current;\n const sizeChanged = Math.abs(w - last.width) > 0.5 || Math.abs(h - last.height) > 0.5;\n const dprChanged = Math.abs(pr - last.dpr) > 0.01;\n if (!sizeChanged && !dprChanged) {\n return;\n }\n\n lastSizeRef.current = { width: w, height: h, dpr: pr };\n renderer.setPixelRatio(pr);\n renderer.setSize(w, h, false);\n uniforms.iResolution.value.set(w * pr, h * pr, pr);\n rectRef.current = canvas.getBoundingClientRect();\n\n if (!pausedRef.current) {\n renderer.render(scene, camera);\n }\n };\n\n let resizeRaf = 0;\n const scheduleResize = () => {\n if (resizeRaf) cancelAnimationFrame(resizeRaf);\n resizeRaf = requestAnimationFrame(setSizeNow);\n };\n\n setSizeNow();\n const ro = new ResizeObserver(scheduleResize);\n ro.observe(mount);\n\n const io = new IntersectionObserver(\n entries => {\n inViewRef.current = entries[0]?.isIntersecting ?? true;\n },\n { root: null, threshold: 0 }\n );\n io.observe(mount);\n\n const onVis = () => {\n pausedRef.current = document.hidden;\n };\n document.addEventListener('visibilitychange', onVis, { passive: true });\n\n const updateMouse = (clientX: number, clientY: number) => {\n const rect = rectRef.current;\n if (!rect) return;\n const x = clientX - rect.left;\n const y = clientY - rect.top;\n const ratio = currentDprRef.current;\n const hb = rect.height * ratio;\n mouseTarget.set(x * ratio, hb - y * ratio);\n };\n const onMove = (ev: PointerEvent | MouseEvent) => updateMouse(ev.clientX, ev.clientY);\n const onLeave = () => mouseTarget.set(0, 0);\n canvas.addEventListener('pointermove', onMove as any, { passive: true });\n canvas.addEventListener('pointerdown', onMove as any, { passive: true });\n canvas.addEventListener('pointerenter', onMove as any, { passive: true });\n canvas.addEventListener('pointerleave', onLeave as any, { passive: true });\n\n const onCtxLost = (e: Event) => {\n e.preventDefault();\n pausedRef.current = true;\n };\n const onCtxRestored = () => {\n pausedRef.current = false;\n scheduleResize();\n };\n canvas.addEventListener('webglcontextlost', onCtxLost, false);\n canvas.addEventListener('webglcontextrestored', onCtxRestored, false);\n\n let raf = 0;\n\n const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v));\n const dprFloor = 0.6;\n const lowerThresh = 50;\n const upperThresh = 58;\n let lastDprChangeRef = 0;\n const dprChangeCooldown = 2000;\n\n const adjustDprIfNeeded = (now: number) => {\n const elapsed = now - lastFpsCheckRef.current;\n if (elapsed < 750) return;\n\n const samples = fpsSamplesRef.current;\n if (samples.length === 0) {\n lastFpsCheckRef.current = now;\n return;\n }\n const avgFps = samples.reduce((a, b) => a + b, 0) / samples.length;\n\n let next = currentDprRef.current;\n const base = baseDprRef.current;\n\n if (avgFps < lowerThresh) {\n next = clamp(currentDprRef.current * 0.85, dprFloor, base);\n } else if (avgFps > upperThresh && currentDprRef.current < base) {\n next = clamp(currentDprRef.current * 1.1, dprFloor, base);\n }\n\n if (Math.abs(next - currentDprRef.current) > 0.01 && now - lastDprChangeRef > dprChangeCooldown) {\n currentDprRef.current = next;\n lastDprChangeRef = now;\n setSizeNow();\n }\n\n fpsSamplesRef.current = [];\n lastFpsCheckRef.current = now;\n };\n\n const animate = () => {\n raf = requestAnimationFrame(animate);\n if (pausedRef.current || !inViewRef.current) return;\n\n const t = clock.getElapsedTime();\n const dt = Math.max(0, t - prevTime);\n prevTime = t;\n\n const dtMs = dt * 1000;\n emaDtRef.current = emaDtRef.current * 0.9 + dtMs * 0.1;\n const instFps = 1000 / Math.max(1, emaDtRef.current);\n fpsSamplesRef.current.push(instFps);\n\n uniforms.iTime.value = t;\n\n const cdt = Math.min(0.033, Math.max(0.001, dt));\n (uniforms.uFlowTime.value as number) += cdt;\n (uniforms.uFogTime.value as number) += cdt;\n\n if (!hasFadedRef.current) {\n const fadeDur = 1.0;\n fade = Math.min(1, fade + cdt / fadeDur);\n uniforms.uFade.value = fade;\n if (fade >= 1) hasFadedRef.current = true;\n }\n\n const tau = Math.max(1e-3, mouseSmoothTime);\n const alpha = 1 - Math.exp(-cdt / tau);\n mouseSmooth.lerp(mouseTarget, alpha);\n uniforms.iMouse.value.set(mouseSmooth.x, mouseSmooth.y, 0, 0);\n\n renderer.render(scene, camera);\n\n adjustDprIfNeeded(performance.now());\n };\n\n animate();\n\n return () => {\n cancelAnimationFrame(raf);\n ro.disconnect();\n io.disconnect();\n document.removeEventListener('visibilitychange', onVis);\n canvas.removeEventListener('pointermove', onMove as any);\n canvas.removeEventListener('pointerdown', onMove as any);\n canvas.removeEventListener('pointerenter', onMove as any);\n canvas.removeEventListener('pointerleave', onLeave as any);\n canvas.removeEventListener('webglcontextlost', onCtxLost);\n canvas.removeEventListener('webglcontextrestored', onCtxRestored);\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n renderer.forceContextLoss();\n if (mount.contains(canvas)) mount.removeChild(canvas);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [dpr]);\n\n useEffect(() => {\n const uniforms = uniformsRef.current;\n if (!uniforms) return;\n\n uniforms.uWispDensity.value = wispDensity;\n uniforms.uTiltScale.value = mouseTiltStrength;\n uniforms.uBeamXFrac.value = horizontalBeamOffset;\n uniforms.uBeamYFrac.value = verticalBeamOffset;\n uniforms.uFlowSpeed.value = flowSpeed;\n uniforms.uVLenFactor.value = verticalSizing;\n uniforms.uHLenFactor.value = horizontalSizing;\n uniforms.uFogIntensity.value = fogIntensity;\n uniforms.uFogScale.value = fogScale;\n uniforms.uWSpeed.value = wispSpeed;\n uniforms.uWIntensity.value = wispIntensity;\n uniforms.uFlowStrength.value = flowStrength;\n uniforms.uDecay.value = decay;\n uniforms.uFalloffStart.value = falloffStart;\n uniforms.uFogFallSpeed.value = fogFallSpeed;\n\n const { r, g, b } = hexToRGB(color || '#FFFFFF');\n uniforms.uColor.value.set(r, g, b);\n }, [\n wispDensity,\n mouseTiltStrength,\n horizontalBeamOffset,\n verticalBeamOffset,\n flowSpeed,\n verticalSizing,\n horizontalSizing,\n fogIntensity,\n fogScale,\n wispSpeed,\n wispIntensity,\n flowStrength,\n decay,\n falloffStart,\n fogFallSpeed,\n color\n ]);\n\n return
;\n};\n\nexport default LaserFlow;\n" } ], diff --git a/public/r/LightPillar-JS-CSS.json b/public/r/LightPillar-JS-CSS.json index 46df2ab4..0c7b5961 100644 --- a/public/r/LightPillar-JS-CSS.json +++ b/public/r/LightPillar-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "LightPillar/LightPillar.css", + "type": "registry:file", + "path": "LightPillar.css", + "target": "@components/LightPillar.css", "content": ".light-pillar-fallback {\n width: 100%;\n height: 100%;\n position: absolute;\n top: 0;\n left: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: rgba(0, 0, 0, 0.1);\n color: #888;\n font-size: 14px;\n}\n\n.light-pillar-container {\n width: 100%;\n height: 100%;\n position: absolute;\n top: 0;\n left: 0;\n}\n" }, { "type": "registry:component", - "path": "LightPillar/LightPillar.jsx", + "path": "LightPillar.jsx", "content": "import { useRef, useEffect, useState } from 'react';\nimport * as THREE from 'three';\nimport './LightPillar.css';\n\nconst LightPillar = ({\n topColor = '#5227FF',\n bottomColor = '#FF9FFC',\n intensity = 1.0,\n rotationSpeed = 0.3,\n interactive = false,\n className = '',\n glowAmount = 0.005,\n pillarWidth = 3.0,\n pillarHeight = 0.4,\n noiseIntensity = 0.5,\n mixBlendMode = 'screen',\n pillarRotation = 0,\n quality = 'high'\n}) => {\n const containerRef = useRef(null);\n const rafRef = useRef(null);\n const rendererRef = useRef(null);\n const materialRef = useRef(null);\n const sceneRef = useRef(null);\n const cameraRef = useRef(null);\n const geometryRef = useRef(null);\n const mouseRef = useRef(new THREE.Vector2(0, 0));\n const timeRef = useRef(0);\n const rotationSpeedRef = useRef(rotationSpeed);\n const [webGLSupported, setWebGLSupported] = useState(true);\n\n useEffect(() => {\n const canvas = document.createElement('canvas');\n const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');\n if (!gl) {\n setWebGLSupported(false);\n }\n }, []);\n\n useEffect(() => {\n if (!containerRef.current || !webGLSupported) return;\n\n const container = containerRef.current;\n const width = container.clientWidth;\n const height = container.clientHeight;\n\n const scene = new THREE.Scene();\n sceneRef.current = scene;\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n cameraRef.current = camera;\n\n const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);\n const isLowEndDevice = isMobile || (navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 4);\n\n let effectiveQuality = quality;\n if (isLowEndDevice && quality === 'high') effectiveQuality = 'medium';\n if (isMobile && quality !== 'low') effectiveQuality = 'low';\n\n const qualitySettings = {\n low: { iterations: 24, waveIterations: 1, pixelRatio: 0.5, precision: 'mediump', stepMultiplier: 1.5 },\n medium: { iterations: 40, waveIterations: 2, pixelRatio: 0.65, precision: 'mediump', stepMultiplier: 1.2 },\n high: {\n iterations: 80,\n waveIterations: 4,\n pixelRatio: Math.min(window.devicePixelRatio, 2),\n precision: 'highp',\n stepMultiplier: 1.0\n }\n };\n\n const settings = qualitySettings[effectiveQuality] || qualitySettings.medium;\n\n let renderer;\n try {\n renderer = new THREE.WebGLRenderer({\n antialias: false,\n alpha: true,\n powerPreference: effectiveQuality === 'high' ? 'high-performance' : 'low-power',\n precision: settings.precision,\n stencil: false,\n depth: false\n });\n } catch (error) {\n setWebGLSupported(false);\n return;\n }\n\n renderer.setSize(width, height);\n renderer.setPixelRatio(settings.pixelRatio);\n container.appendChild(renderer.domElement);\n rendererRef.current = renderer;\n\n const parseColor = hex => {\n const color = new THREE.Color(hex);\n return new THREE.Vector3(color.r, color.g, color.b);\n };\n\n const vertexShader = `\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n }\n `;\n\n const fragmentShader = `\n precision ${settings.precision} float;\n\n uniform float uTime;\n uniform vec2 uResolution;\n uniform vec2 uMouse;\n uniform vec3 uTopColor;\n uniform vec3 uBottomColor;\n uniform float uIntensity;\n uniform bool uInteractive;\n uniform float uGlowAmount;\n uniform float uPillarWidth;\n uniform float uPillarHeight;\n uniform float uNoiseIntensity;\n uniform float uRotCos;\n uniform float uRotSin;\n uniform float uPillarRotCos;\n uniform float uPillarRotSin;\n uniform float uWaveSin;\n uniform float uWaveCos;\n varying vec2 vUv;\n\n const float STEP_MULT = ${settings.stepMultiplier.toFixed(1)};\n const int MAX_ITER = ${settings.iterations};\n const int WAVE_ITER = ${settings.waveIterations};\n\n void main() {\n vec2 uv = (vUv * 2.0 - 1.0) * vec2(uResolution.x / uResolution.y, 1.0);\n uv = vec2(uPillarRotCos * uv.x - uPillarRotSin * uv.y, uPillarRotSin * uv.x + uPillarRotCos * uv.y);\n\n vec3 ro = vec3(0.0, 0.0, -10.0);\n vec3 rd = normalize(vec3(uv, 1.0));\n\n float rotC = uRotCos;\n float rotS = uRotSin;\n if(uInteractive && (uMouse.x != 0.0 || uMouse.y != 0.0)) {\n float a = uMouse.x * 6.283185;\n rotC = cos(a);\n rotS = sin(a);\n }\n\n vec3 col = vec3(0.0);\n float t = 0.1;\n \n for(int i = 0; i < MAX_ITER; i++) {\n vec3 p = ro + rd * t;\n p.xz = vec2(rotC * p.x - rotS * p.z, rotS * p.x + rotC * p.z);\n\n vec3 q = p;\n q.y = p.y * uPillarHeight + uTime;\n \n float freq = 1.0;\n float amp = 1.0;\n for(int j = 0; j < WAVE_ITER; j++) {\n q.xz = vec2(uWaveCos * q.x - uWaveSin * q.z, uWaveSin * q.x + uWaveCos * q.z);\n q += cos(q.zxy * freq - uTime * float(j) * 2.0) * amp;\n freq *= 2.0;\n amp *= 0.5;\n }\n \n float d = length(cos(q.xz)) - 0.2;\n float bound = length(p.xz) - uPillarWidth;\n float k = 4.0;\n float h = max(k - abs(d - bound), 0.0);\n d = max(d, bound) + h * h * 0.0625 / k;\n d = abs(d) * 0.15 + 0.01;\n\n float grad = clamp((15.0 - p.y) / 30.0, 0.0, 1.0);\n col += mix(uBottomColor, uTopColor, grad) / d;\n\n t += d * STEP_MULT;\n if(t > 50.0) break;\n }\n\n float widthNorm = uPillarWidth / 3.0;\n col = tanh(col * uGlowAmount / widthNorm);\n \n col -= fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) / 15.0 * uNoiseIntensity;\n \n gl_FragColor = vec4(col * uIntensity, 1.0);\n }\n `;\n\n const pillarRotRad = (pillarRotation * Math.PI) / 180;\n const waveSin = Math.sin(0.4);\n const waveCos = Math.cos(0.4);\n\n const material = new THREE.ShaderMaterial({\n vertexShader,\n fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uResolution: { value: new THREE.Vector2(width, height) },\n uMouse: { value: mouseRef.current },\n uTopColor: { value: parseColor(topColor) },\n uBottomColor: { value: parseColor(bottomColor) },\n uIntensity: { value: intensity },\n uInteractive: { value: interactive },\n uGlowAmount: { value: glowAmount },\n uPillarWidth: { value: pillarWidth },\n uPillarHeight: { value: pillarHeight },\n uNoiseIntensity: { value: noiseIntensity },\n uRotCos: { value: 1.0 },\n uRotSin: { value: 0.0 },\n uPillarRotCos: { value: Math.cos(pillarRotRad) },\n uPillarRotSin: { value: Math.sin(pillarRotRad) },\n uWaveSin: { value: waveSin },\n uWaveCos: { value: waveCos }\n },\n transparent: true,\n depthWrite: false,\n depthTest: false\n });\n materialRef.current = material;\n\n const geometry = new THREE.PlaneGeometry(2, 2);\n geometryRef.current = geometry;\n const mesh = new THREE.Mesh(geometry, material);\n scene.add(mesh);\n\n let mouseMoveTimeout = null;\n const handleMouseMove = event => {\n if (!interactive) return;\n if (mouseMoveTimeout) return;\n mouseMoveTimeout = window.setTimeout(() => {\n mouseMoveTimeout = null;\n }, 16);\n const rect = container.getBoundingClientRect();\n const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;\n const y = -((event.clientY - rect.top) / rect.height) * 2 + 1;\n mouseRef.current.set(x, y);\n };\n\n if (interactive) {\n container.addEventListener('mousemove', handleMouseMove, { passive: true });\n }\n\n let lastTime = performance.now();\n const targetFPS = effectiveQuality === 'low' ? 30 : 60;\n const frameTime = 1000 / targetFPS;\n\n const animate = currentTime => {\n if (!materialRef.current || !rendererRef.current || !sceneRef.current || !cameraRef.current) return;\n\n const deltaTime = currentTime - lastTime;\n\n if (deltaTime >= frameTime) {\n timeRef.current += 0.016 * rotationSpeedRef.current;\n const t = timeRef.current;\n materialRef.current.uniforms.uTime.value = t;\n materialRef.current.uniforms.uRotCos.value = Math.cos(t * 0.3);\n materialRef.current.uniforms.uRotSin.value = Math.sin(t * 0.3);\n rendererRef.current.render(sceneRef.current, cameraRef.current);\n lastTime = currentTime - (deltaTime % frameTime);\n }\n\n rafRef.current = requestAnimationFrame(animate);\n };\n rafRef.current = requestAnimationFrame(animate);\n\n let resizeTimeout = null;\n const handleResize = () => {\n if (resizeTimeout) {\n clearTimeout(resizeTimeout);\n }\n\n resizeTimeout = window.setTimeout(() => {\n if (!rendererRef.current || !materialRef.current || !containerRef.current) return;\n const newWidth = containerRef.current.clientWidth;\n const newHeight = containerRef.current.clientHeight;\n rendererRef.current.setSize(newWidth, newHeight);\n materialRef.current.uniforms.uResolution.value.set(newWidth, newHeight);\n }, 150);\n };\n\n window.addEventListener('resize', handleResize, { passive: true });\n\n return () => {\n window.removeEventListener('resize', handleResize);\n if (interactive) {\n container.removeEventListener('mousemove', handleMouseMove);\n }\n if (rafRef.current) {\n cancelAnimationFrame(rafRef.current);\n }\n if (rendererRef.current) {\n rendererRef.current.dispose();\n rendererRef.current.forceContextLoss();\n if (container.contains(rendererRef.current.domElement)) {\n container.removeChild(rendererRef.current.domElement);\n }\n }\n if (materialRef.current) materialRef.current.dispose();\n if (geometryRef.current) geometryRef.current.dispose();\n\n rendererRef.current = null;\n materialRef.current = null;\n sceneRef.current = null;\n cameraRef.current = null;\n geometryRef.current = null;\n rafRef.current = null;\n };\n }, [webGLSupported, quality]);\n\n useEffect(() => {\n rotationSpeedRef.current = rotationSpeed;\n }, [rotationSpeed]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n const parseColor = hex => {\n const color = new THREE.Color(hex);\n return new THREE.Vector3(color.r, color.g, color.b);\n };\n materialRef.current.uniforms.uTopColor.value = parseColor(topColor);\n }, [topColor]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n const parseColor = hex => {\n const color = new THREE.Color(hex);\n return new THREE.Vector3(color.r, color.g, color.b);\n };\n materialRef.current.uniforms.uBottomColor.value = parseColor(bottomColor);\n }, [bottomColor]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n materialRef.current.uniforms.uIntensity.value = intensity;\n }, [intensity]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n materialRef.current.uniforms.uInteractive.value = interactive;\n }, [interactive]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n materialRef.current.uniforms.uGlowAmount.value = glowAmount;\n }, [glowAmount]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n materialRef.current.uniforms.uPillarWidth.value = pillarWidth;\n }, [pillarWidth]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n materialRef.current.uniforms.uPillarHeight.value = pillarHeight;\n }, [pillarHeight]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n materialRef.current.uniforms.uNoiseIntensity.value = noiseIntensity;\n }, [noiseIntensity]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n const pillarRotRad = (pillarRotation * Math.PI) / 180;\n materialRef.current.uniforms.uPillarRotCos.value = Math.cos(pillarRotRad);\n materialRef.current.uniforms.uPillarRotSin.value = Math.sin(pillarRotRad);\n }, [pillarRotation]);\n\n if (!webGLSupported) {\n return (\n
\n WebGL not supported\n
\n );\n }\n\n return
;\n};\n\nexport default LightPillar;\n" } ], diff --git a/public/r/LightPillar-TS-CSS.json b/public/r/LightPillar-TS-CSS.json index c3a94d90..145093e3 100644 --- a/public/r/LightPillar-TS-CSS.json +++ b/public/r/LightPillar-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "LightPillar/LightPillar.css", + "type": "registry:file", + "path": "LightPillar.css", + "target": "@components/LightPillar.css", "content": ".light-pillar-fallback {\n width: 100%;\n height: 100%;\n position: absolute;\n top: 0;\n left: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: rgba(0, 0, 0, 0.1);\n color: #888;\n font-size: 14px;\n}\n\n.light-pillar-container {\n width: 100%;\n height: 100%;\n position: absolute;\n top: 0;\n left: 0;\n}\n" }, { "type": "registry:component", - "path": "LightPillar/LightPillar.tsx", + "path": "LightPillar.tsx", "content": "import React, { useRef, useEffect, useState } from 'react';\nimport * as THREE from 'three';\nimport './LightPillar.css';\n\ninterface LightPillarProps {\n topColor?: string;\n bottomColor?: string;\n intensity?: number;\n rotationSpeed?: number;\n interactive?: boolean;\n className?: string;\n glowAmount?: number;\n pillarWidth?: number;\n pillarHeight?: number;\n noiseIntensity?: number;\n mixBlendMode?: React.CSSProperties['mixBlendMode'];\n pillarRotation?: number;\n quality?: 'low' | 'medium' | 'high';\n}\n\nconst LightPillar: React.FC = ({\n topColor = '#5227FF',\n bottomColor = '#FF9FFC',\n intensity = 1.0,\n rotationSpeed = 0.3,\n interactive = false,\n className = '',\n glowAmount = 0.005,\n pillarWidth = 3.0,\n pillarHeight = 0.4,\n noiseIntensity = 0.5,\n mixBlendMode = 'screen',\n pillarRotation = 0,\n quality = 'high'\n}) => {\n const containerRef = useRef(null);\n const rafRef = useRef(null);\n const rendererRef = useRef(null);\n const materialRef = useRef(null);\n const sceneRef = useRef(null);\n const cameraRef = useRef(null);\n const geometryRef = useRef(null);\n const mouseRef = useRef(new THREE.Vector2(0, 0));\n const timeRef = useRef(0);\n const rotationSpeedRef = useRef(rotationSpeed);\n const [webGLSupported, setWebGLSupported] = useState(true);\n\n // Check WebGL support\n useEffect(() => {\n const canvas = document.createElement('canvas');\n const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');\n if (!gl) {\n setWebGLSupported(false);\n }\n }, []);\n\n useEffect(() => {\n if (!containerRef.current || !webGLSupported) return;\n\n const container = containerRef.current;\n const width = container.clientWidth;\n const height = container.clientHeight;\n\n const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);\n const isLowEndDevice = isMobile || (navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 4);\n\n let effectiveQuality = quality;\n if (isLowEndDevice && quality === 'high') effectiveQuality = 'medium';\n if (isMobile && quality !== 'low') effectiveQuality = 'low';\n\n const qualitySettings = {\n low: { iterations: 24, waveIterations: 1, pixelRatio: 0.5, precision: 'mediump', stepMultiplier: 1.5 },\n medium: { iterations: 40, waveIterations: 2, pixelRatio: 0.65, precision: 'mediump', stepMultiplier: 1.2 },\n high: {\n iterations: 80,\n waveIterations: 4,\n pixelRatio: Math.min(window.devicePixelRatio, 2),\n precision: 'highp',\n stepMultiplier: 1.0\n }\n };\n\n const settings = qualitySettings[effectiveQuality] || qualitySettings.medium;\n\n const scene = new THREE.Scene();\n sceneRef.current = scene;\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n cameraRef.current = camera;\n\n let renderer: THREE.WebGLRenderer;\n try {\n renderer = new THREE.WebGLRenderer({\n antialias: false,\n alpha: true,\n powerPreference: effectiveQuality === 'high' ? 'high-performance' : 'low-power',\n precision: settings.precision as 'highp' | 'mediump' | 'lowp',\n stencil: false,\n depth: false\n });\n } catch (error) {\n setWebGLSupported(false);\n return;\n }\n\n renderer.setSize(width, height);\n renderer.setPixelRatio(settings.pixelRatio);\n container.appendChild(renderer.domElement);\n rendererRef.current = renderer;\n\n const parseColor = (hex: string): THREE.Vector3 => {\n const color = new THREE.Color(hex);\n return new THREE.Vector3(color.r, color.g, color.b);\n };\n\n const vertexShader = `\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n }\n `;\n\n const fragmentShader = `\n precision ${settings.precision} float;\n\n uniform float uTime;\n uniform vec2 uResolution;\n uniform vec2 uMouse;\n uniform vec3 uTopColor;\n uniform vec3 uBottomColor;\n uniform float uIntensity;\n uniform bool uInteractive;\n uniform float uGlowAmount;\n uniform float uPillarWidth;\n uniform float uPillarHeight;\n uniform float uNoiseIntensity;\n uniform float uRotCos;\n uniform float uRotSin;\n uniform float uPillarRotCos;\n uniform float uPillarRotSin;\n uniform float uWaveSin;\n uniform float uWaveCos;\n varying vec2 vUv;\n\n const float STEP_MULT = ${settings.stepMultiplier.toFixed(1)};\n const int MAX_ITER = ${settings.iterations};\n const int WAVE_ITER = ${settings.waveIterations};\n\n void main() {\n vec2 uv = (vUv * 2.0 - 1.0) * vec2(uResolution.x / uResolution.y, 1.0);\n uv = vec2(uPillarRotCos * uv.x - uPillarRotSin * uv.y, uPillarRotSin * uv.x + uPillarRotCos * uv.y);\n\n vec3 ro = vec3(0.0, 0.0, -10.0);\n vec3 rd = normalize(vec3(uv, 1.0));\n\n float rotC = uRotCos;\n float rotS = uRotSin;\n if(uInteractive && (uMouse.x != 0.0 || uMouse.y != 0.0)) {\n float a = uMouse.x * 6.283185;\n rotC = cos(a);\n rotS = sin(a);\n }\n\n vec3 col = vec3(0.0);\n float t = 0.1;\n \n for(int i = 0; i < MAX_ITER; i++) {\n vec3 p = ro + rd * t;\n p.xz = vec2(rotC * p.x - rotS * p.z, rotS * p.x + rotC * p.z);\n\n vec3 q = p;\n q.y = p.y * uPillarHeight + uTime;\n \n float freq = 1.0;\n float amp = 1.0;\n for(int j = 0; j < WAVE_ITER; j++) {\n q.xz = vec2(uWaveCos * q.x - uWaveSin * q.z, uWaveSin * q.x + uWaveCos * q.z);\n q += cos(q.zxy * freq - uTime * float(j) * 2.0) * amp;\n freq *= 2.0;\n amp *= 0.5;\n }\n \n float d = length(cos(q.xz)) - 0.2;\n float bound = length(p.xz) - uPillarWidth;\n float k = 4.0;\n float h = max(k - abs(d - bound), 0.0);\n d = max(d, bound) + h * h * 0.0625 / k;\n d = abs(d) * 0.15 + 0.01;\n\n float grad = clamp((15.0 - p.y) / 30.0, 0.0, 1.0);\n col += mix(uBottomColor, uTopColor, grad) / d;\n\n t += d * STEP_MULT;\n if(t > 50.0) break;\n }\n\n float widthNorm = uPillarWidth / 3.0;\n col = tanh(col * uGlowAmount / widthNorm);\n \n col -= fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) / 15.0 * uNoiseIntensity;\n \n gl_FragColor = vec4(col * uIntensity, 1.0);\n }\n `;\n\n const pillarRotRad = (pillarRotation * Math.PI) / 180;\n const waveSin = Math.sin(0.4);\n const waveCos = Math.cos(0.4);\n\n const material = new THREE.ShaderMaterial({\n vertexShader,\n fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uResolution: { value: new THREE.Vector2(width, height) },\n uMouse: { value: mouseRef.current },\n uTopColor: { value: parseColor(topColor) },\n uBottomColor: { value: parseColor(bottomColor) },\n uIntensity: { value: intensity },\n uInteractive: { value: interactive },\n uGlowAmount: { value: glowAmount },\n uPillarWidth: { value: pillarWidth },\n uPillarHeight: { value: pillarHeight },\n uNoiseIntensity: { value: noiseIntensity },\n uRotCos: { value: 1.0 },\n uRotSin: { value: 0.0 },\n uPillarRotCos: { value: Math.cos(pillarRotRad) },\n uPillarRotSin: { value: Math.sin(pillarRotRad) },\n uWaveSin: { value: waveSin },\n uWaveCos: { value: waveCos }\n },\n transparent: true,\n depthWrite: false,\n depthTest: false\n });\n materialRef.current = material;\n\n const geometry = new THREE.PlaneGeometry(2, 2);\n geometryRef.current = geometry;\n const mesh = new THREE.Mesh(geometry, material);\n scene.add(mesh);\n\n let mouseMoveTimeout: number | null = null;\n const handleMouseMove = (event: MouseEvent) => {\n if (!interactive) return;\n\n if (mouseMoveTimeout) return;\n\n mouseMoveTimeout = window.setTimeout(() => {\n mouseMoveTimeout = null;\n }, 16);\n\n const rect = container.getBoundingClientRect();\n const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;\n const y = -((event.clientY - rect.top) / rect.height) * 2 + 1;\n mouseRef.current.set(x, y);\n };\n\n if (interactive) {\n container.addEventListener('mousemove', handleMouseMove, { passive: true });\n }\n\n let lastTime = performance.now();\n const targetFPS = effectiveQuality === 'low' ? 30 : 60;\n const frameTime = 1000 / targetFPS;\n\n const animate = (currentTime: number) => {\n if (!materialRef.current || !rendererRef.current || !sceneRef.current || !cameraRef.current) return;\n\n const deltaTime = currentTime - lastTime;\n\n if (deltaTime >= frameTime) {\n timeRef.current += 0.016 * rotationSpeedRef.current;\n const t = timeRef.current;\n materialRef.current.uniforms.uTime.value = t;\n materialRef.current.uniforms.uRotCos.value = Math.cos(t * 0.3);\n materialRef.current.uniforms.uRotSin.value = Math.sin(t * 0.3);\n rendererRef.current.render(sceneRef.current, cameraRef.current);\n lastTime = currentTime - (deltaTime % frameTime);\n }\n\n rafRef.current = requestAnimationFrame(animate);\n };\n rafRef.current = requestAnimationFrame(animate);\n\n let resizeTimeout: number | null = null;\n const handleResize = () => {\n if (resizeTimeout) {\n clearTimeout(resizeTimeout);\n }\n\n resizeTimeout = window.setTimeout(() => {\n if (!rendererRef.current || !materialRef.current || !containerRef.current) return;\n const newWidth = containerRef.current.clientWidth;\n const newHeight = containerRef.current.clientHeight;\n rendererRef.current.setSize(newWidth, newHeight);\n materialRef.current.uniforms.uResolution.value.set(newWidth, newHeight);\n }, 150);\n };\n\n window.addEventListener('resize', handleResize, { passive: true });\n\n return () => {\n window.removeEventListener('resize', handleResize);\n if (interactive) {\n container.removeEventListener('mousemove', handleMouseMove);\n }\n if (rafRef.current) {\n cancelAnimationFrame(rafRef.current);\n }\n if (rendererRef.current) {\n rendererRef.current.dispose();\n rendererRef.current.forceContextLoss();\n if (container.contains(rendererRef.current.domElement)) {\n container.removeChild(rendererRef.current.domElement);\n }\n }\n if (materialRef.current) {\n materialRef.current.dispose();\n }\n if (geometryRef.current) {\n geometryRef.current.dispose();\n }\n\n rendererRef.current = null;\n materialRef.current = null;\n sceneRef.current = null;\n cameraRef.current = null;\n geometryRef.current = null;\n rafRef.current = null;\n };\n }, [webGLSupported, quality]);\n\n useEffect(() => {\n rotationSpeedRef.current = rotationSpeed;\n }, [rotationSpeed]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n const parseColor = (hex: string) => {\n const color = new THREE.Color(hex);\n return new THREE.Vector3(color.r, color.g, color.b);\n };\n materialRef.current.uniforms.uTopColor.value = parseColor(topColor);\n }, [topColor]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n const parseColor = (hex: string) => {\n const color = new THREE.Color(hex);\n return new THREE.Vector3(color.r, color.g, color.b);\n };\n materialRef.current.uniforms.uBottomColor.value = parseColor(bottomColor);\n }, [bottomColor]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n materialRef.current.uniforms.uIntensity.value = intensity;\n }, [intensity]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n materialRef.current.uniforms.uInteractive.value = interactive;\n }, [interactive]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n materialRef.current.uniforms.uGlowAmount.value = glowAmount;\n }, [glowAmount]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n materialRef.current.uniforms.uPillarWidth.value = pillarWidth;\n }, [pillarWidth]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n materialRef.current.uniforms.uPillarHeight.value = pillarHeight;\n }, [pillarHeight]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n materialRef.current.uniforms.uNoiseIntensity.value = noiseIntensity;\n }, [noiseIntensity]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n const pillarRotRad = (pillarRotation * Math.PI) / 180;\n materialRef.current.uniforms.uPillarRotCos.value = Math.cos(pillarRotRad);\n materialRef.current.uniforms.uPillarRotSin.value = Math.sin(pillarRotRad);\n }, [pillarRotation]);\n\n if (!webGLSupported) {\n return (\n
\n WebGL not supported\n
\n );\n }\n\n return
;\n};\n\nexport default LightPillar;\n" } ], diff --git a/public/r/LightRays-JS-CSS.json b/public/r/LightRays-JS-CSS.json index 4c603b4a..ab044360 100644 --- a/public/r/LightRays-JS-CSS.json +++ b/public/r/LightRays-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "LightRays/LightRays.css", + "type": "registry:file", + "path": "LightRays.css", + "target": "@components/LightRays.css", "content": ".light-rays-container {\n width: 100%;\n height: 100%;\n position: relative;\n pointer-events: none;\n z-index: 3;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "LightRays/LightRays.jsx", + "path": "LightRays.jsx", "content": "import { useRef, useEffect, useState } from 'react';\nimport { Renderer, Program, Triangle, Mesh } from 'ogl';\nimport './LightRays.css';\n\nconst DEFAULT_COLOR = '#ffffff';\n\nconst hexToRgb = hex => {\n const m = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [1, 1, 1];\n};\n\nconst getAnchorAndDir = (origin, w, h) => {\n const outside = 0.2;\n switch (origin) {\n case 'top-left':\n return { anchor: [0, -outside * h], dir: [0, 1] };\n case 'top-right':\n return { anchor: [w, -outside * h], dir: [0, 1] };\n case 'left':\n return { anchor: [-outside * w, 0.5 * h], dir: [1, 0] };\n case 'right':\n return { anchor: [(1 + outside) * w, 0.5 * h], dir: [-1, 0] };\n case 'bottom-left':\n return { anchor: [0, (1 + outside) * h], dir: [0, -1] };\n case 'bottom-center':\n return { anchor: [0.5 * w, (1 + outside) * h], dir: [0, -1] };\n case 'bottom-right':\n return { anchor: [w, (1 + outside) * h], dir: [0, -1] };\n default: // \"top-center\"\n return { anchor: [0.5 * w, -outside * h], dir: [0, 1] };\n }\n};\n\nconst LightRays = ({\n raysOrigin = 'top-center',\n raysColor = DEFAULT_COLOR,\n raysSpeed = 1,\n lightSpread = 1,\n rayLength = 2,\n pulsating = false,\n fadeDistance = 1.0,\n saturation = 1.0,\n followMouse = true,\n mouseInfluence = 0.1,\n noiseAmount = 0.0,\n distortion = 0.0,\n className = ''\n}) => {\n const containerRef = useRef(null);\n const uniformsRef = useRef(null);\n const rendererRef = useRef(null);\n const mouseRef = useRef({ x: 0.5, y: 0.5 });\n const smoothMouseRef = useRef({ x: 0.5, y: 0.5 });\n const animationIdRef = useRef(null);\n const meshRef = useRef(null);\n const cleanupFunctionRef = useRef(null);\n const [isVisible, setIsVisible] = useState(false);\n const observerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n observerRef.current = new IntersectionObserver(\n entries => {\n const entry = entries[0];\n setIsVisible(entry.isIntersecting);\n },\n { threshold: 0.1 }\n );\n\n observerRef.current.observe(containerRef.current);\n\n return () => {\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n };\n }, []);\n\n useEffect(() => {\n if (!isVisible || !containerRef.current) return;\n\n if (cleanupFunctionRef.current) {\n cleanupFunctionRef.current();\n cleanupFunctionRef.current = null;\n }\n\n const initializeWebGL = async () => {\n if (!containerRef.current) return;\n\n await new Promise(resolve => setTimeout(resolve, 10));\n\n if (!containerRef.current) return;\n\n const renderer = new Renderer({\n dpr: Math.min(window.devicePixelRatio, 2),\n alpha: true\n });\n rendererRef.current = renderer;\n\n const gl = renderer.gl;\n gl.canvas.style.width = '100%';\n gl.canvas.style.height = '100%';\n\n while (containerRef.current.firstChild) {\n containerRef.current.removeChild(containerRef.current.firstChild);\n }\n containerRef.current.appendChild(gl.canvas);\n\n const vert = `\nattribute vec2 position;\nvarying vec2 vUv;\nvoid main() {\n vUv = position * 0.5 + 0.5;\n gl_Position = vec4(position, 0.0, 1.0);\n}`;\n\n const frag = `precision highp float;\n\nuniform float iTime;\nuniform vec2 iResolution;\n\nuniform vec2 rayPos;\nuniform vec2 rayDir;\nuniform vec3 raysColor;\nuniform float raysSpeed;\nuniform float lightSpread;\nuniform float rayLength;\nuniform float pulsating;\nuniform float fadeDistance;\nuniform float saturation;\nuniform vec2 mousePos;\nuniform float mouseInfluence;\nuniform float noiseAmount;\nuniform float distortion;\n\nvarying vec2 vUv;\n\nfloat noise(vec2 st) {\n return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);\n}\n\nfloat rayStrength(vec2 raySource, vec2 rayRefDirection, vec2 coord,\n float seedA, float seedB, float speed) {\n vec2 sourceToCoord = coord - raySource;\n vec2 dirNorm = normalize(sourceToCoord);\n float cosAngle = dot(dirNorm, rayRefDirection);\n\n float distortedAngle = cosAngle + distortion * sin(iTime * 2.0 + length(sourceToCoord) * 0.01) * 0.2;\n \n float spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / max(lightSpread, 0.001));\n\n float distance = length(sourceToCoord);\n float maxDistance = iResolution.x * rayLength;\n float lengthFalloff = clamp((maxDistance - distance) / maxDistance, 0.0, 1.0);\n \n float fadeFalloff = clamp((iResolution.x * fadeDistance - distance) / (iResolution.x * fadeDistance), 0.5, 1.0);\n float pulse = pulsating > 0.5 ? (0.8 + 0.2 * sin(iTime * speed * 3.0)) : 1.0;\n\n float baseStrength = clamp(\n (0.45 + 0.15 * sin(distortedAngle * seedA + iTime * speed)) +\n (0.3 + 0.2 * cos(-distortedAngle * seedB + iTime * speed)),\n 0.0, 1.0\n );\n\n return baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse;\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 coord = vec2(fragCoord.x, iResolution.y - fragCoord.y);\n \n vec2 finalRayDir = rayDir;\n if (mouseInfluence > 0.0) {\n vec2 mouseScreenPos = mousePos * iResolution.xy;\n vec2 mouseDirection = normalize(mouseScreenPos - rayPos);\n finalRayDir = normalize(mix(rayDir, mouseDirection, mouseInfluence));\n }\n\n vec4 rays1 = vec4(1.0) *\n rayStrength(rayPos, finalRayDir, coord, 36.2214, 21.11349,\n 1.5 * raysSpeed);\n vec4 rays2 = vec4(1.0) *\n rayStrength(rayPos, finalRayDir, coord, 22.3991, 18.0234,\n 1.1 * raysSpeed);\n\n fragColor = rays1 * 0.5 + rays2 * 0.4;\n\n if (noiseAmount > 0.0) {\n float n = noise(coord * 0.01 + iTime * 0.1);\n fragColor.rgb *= (1.0 - noiseAmount + noiseAmount * n);\n }\n\n float brightness = 1.0 - (coord.y / iResolution.y);\n fragColor.x *= 0.1 + brightness * 0.8;\n fragColor.y *= 0.3 + brightness * 0.6;\n fragColor.z *= 0.5 + brightness * 0.5;\n\n if (saturation != 1.0) {\n float gray = dot(fragColor.rgb, vec3(0.299, 0.587, 0.114));\n fragColor.rgb = mix(vec3(gray), fragColor.rgb, saturation);\n }\n\n fragColor.rgb *= raysColor;\n}\n\nvoid main() {\n vec4 color;\n mainImage(color, gl_FragCoord.xy);\n gl_FragColor = color;\n}`;\n\n const uniforms = {\n iTime: { value: 0 },\n iResolution: { value: [1, 1] },\n\n rayPos: { value: [0, 0] },\n rayDir: { value: [0, 1] },\n\n raysColor: { value: hexToRgb(raysColor) },\n raysSpeed: { value: raysSpeed },\n lightSpread: { value: lightSpread },\n rayLength: { value: rayLength },\n pulsating: { value: pulsating ? 1.0 : 0.0 },\n fadeDistance: { value: fadeDistance },\n saturation: { value: saturation },\n mousePos: { value: [0.5, 0.5] },\n mouseInfluence: { value: mouseInfluence },\n noiseAmount: { value: noiseAmount },\n distortion: { value: distortion }\n };\n uniformsRef.current = uniforms;\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex: vert,\n fragment: frag,\n uniforms\n });\n const mesh = new Mesh(gl, { geometry, program });\n meshRef.current = mesh;\n\n const updatePlacement = () => {\n if (!containerRef.current || !renderer) return;\n\n renderer.dpr = Math.min(window.devicePixelRatio, 2);\n\n const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.current;\n renderer.setSize(wCSS, hCSS);\n\n const dpr = renderer.dpr;\n const w = wCSS * dpr;\n const h = hCSS * dpr;\n\n uniforms.iResolution.value = [w, h];\n\n const { anchor, dir } = getAnchorAndDir(raysOrigin, w, h);\n uniforms.rayPos.value = anchor;\n uniforms.rayDir.value = dir;\n };\n\n const loop = t => {\n if (!rendererRef.current || !uniformsRef.current || !meshRef.current) {\n return;\n }\n\n uniforms.iTime.value = t * 0.001;\n\n if (followMouse && mouseInfluence > 0.0) {\n const smoothing = 0.92;\n\n smoothMouseRef.current.x = smoothMouseRef.current.x * smoothing + mouseRef.current.x * (1 - smoothing);\n smoothMouseRef.current.y = smoothMouseRef.current.y * smoothing + mouseRef.current.y * (1 - smoothing);\n\n uniforms.mousePos.value = [smoothMouseRef.current.x, smoothMouseRef.current.y];\n }\n\n try {\n renderer.render({ scene: mesh });\n animationIdRef.current = requestAnimationFrame(loop);\n } catch (error) {\n console.warn('WebGL rendering error:', error);\n return;\n }\n };\n\n window.addEventListener('resize', updatePlacement);\n updatePlacement();\n animationIdRef.current = requestAnimationFrame(loop);\n\n cleanupFunctionRef.current = () => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n animationIdRef.current = null;\n }\n\n window.removeEventListener('resize', updatePlacement);\n\n if (renderer) {\n try {\n const canvas = renderer.gl.canvas;\n const loseContextExt = renderer.gl.getExtension('WEBGL_lose_context');\n if (loseContextExt) {\n loseContextExt.loseContext();\n }\n\n if (canvas && canvas.parentNode) {\n canvas.parentNode.removeChild(canvas);\n }\n } catch (error) {\n console.warn('Error during WebGL cleanup:', error);\n }\n }\n\n rendererRef.current = null;\n uniformsRef.current = null;\n meshRef.current = null;\n };\n };\n\n initializeWebGL();\n\n return () => {\n if (cleanupFunctionRef.current) {\n cleanupFunctionRef.current();\n cleanupFunctionRef.current = null;\n }\n };\n }, [\n isVisible,\n raysOrigin,\n raysColor,\n raysSpeed,\n lightSpread,\n rayLength,\n pulsating,\n fadeDistance,\n saturation,\n followMouse,\n mouseInfluence,\n noiseAmount,\n distortion\n ]);\n\n useEffect(() => {\n if (!uniformsRef.current || !containerRef.current || !rendererRef.current) return;\n\n const u = uniformsRef.current;\n const renderer = rendererRef.current;\n\n u.raysColor.value = hexToRgb(raysColor);\n u.raysSpeed.value = raysSpeed;\n u.lightSpread.value = lightSpread;\n u.rayLength.value = rayLength;\n u.pulsating.value = pulsating ? 1.0 : 0.0;\n u.fadeDistance.value = fadeDistance;\n u.saturation.value = saturation;\n u.mouseInfluence.value = mouseInfluence;\n u.noiseAmount.value = noiseAmount;\n u.distortion.value = distortion;\n\n const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.current;\n const dpr = renderer.dpr;\n const { anchor, dir } = getAnchorAndDir(raysOrigin, wCSS * dpr, hCSS * dpr);\n u.rayPos.value = anchor;\n u.rayDir.value = dir;\n }, [\n raysColor,\n raysSpeed,\n lightSpread,\n raysOrigin,\n rayLength,\n pulsating,\n fadeDistance,\n saturation,\n mouseInfluence,\n noiseAmount,\n distortion\n ]);\n\n useEffect(() => {\n const handleMouseMove = e => {\n if (!containerRef.current || !rendererRef.current) return;\n const rect = containerRef.current.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = (e.clientY - rect.top) / rect.height;\n mouseRef.current = { x, y };\n };\n\n if (followMouse) {\n window.addEventListener('mousemove', handleMouseMove);\n return () => window.removeEventListener('mousemove', handleMouseMove);\n }\n }, [followMouse]);\n\n return
;\n};\n\nexport default LightRays;\n" } ], diff --git a/public/r/LightRays-TS-CSS.json b/public/r/LightRays-TS-CSS.json index ea330422..b549a8cd 100644 --- a/public/r/LightRays-TS-CSS.json +++ b/public/r/LightRays-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "LightRays/LightRays.css", + "type": "registry:file", + "path": "LightRays.css", + "target": "@components/LightRays.css", "content": ".light-rays-container {\n width: 100%;\n height: 100%;\n position: relative;\n pointer-events: none;\n z-index: 3;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "LightRays/LightRays.tsx", + "path": "LightRays.tsx", "content": "import { useRef, useEffect, useState } from 'react';\nimport { Renderer, Program, Triangle, Mesh } from 'ogl';\nimport './LightRays.css';\n\nexport type RaysOrigin =\n | 'top-center'\n | 'top-left'\n | 'top-right'\n | 'right'\n | 'left'\n | 'bottom-center'\n | 'bottom-right'\n | 'bottom-left';\n\ninterface LightRaysProps {\n raysOrigin?: RaysOrigin;\n raysColor?: string;\n raysSpeed?: number;\n lightSpread?: number;\n rayLength?: number;\n pulsating?: boolean;\n fadeDistance?: number;\n saturation?: number;\n followMouse?: boolean;\n mouseInfluence?: number;\n noiseAmount?: number;\n distortion?: number;\n className?: string;\n}\n\nconst DEFAULT_COLOR = '#ffffff';\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n const m = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [1, 1, 1];\n};\n\nconst getAnchorAndDir = (\n origin: RaysOrigin,\n w: number,\n h: number\n): { anchor: [number, number]; dir: [number, number] } => {\n const outside = 0.2;\n switch (origin) {\n case 'top-left':\n return { anchor: [0, -outside * h], dir: [0, 1] };\n case 'top-right':\n return { anchor: [w, -outside * h], dir: [0, 1] };\n case 'left':\n return { anchor: [-outside * w, 0.5 * h], dir: [1, 0] };\n case 'right':\n return { anchor: [(1 + outside) * w, 0.5 * h], dir: [-1, 0] };\n case 'bottom-left':\n return { anchor: [0, (1 + outside) * h], dir: [0, -1] };\n case 'bottom-center':\n return { anchor: [0.5 * w, (1 + outside) * h], dir: [0, -1] };\n case 'bottom-right':\n return { anchor: [w, (1 + outside) * h], dir: [0, -1] };\n default: // \"top-center\"\n return { anchor: [0.5 * w, -outside * h], dir: [0, 1] };\n }\n};\n\ntype Vec2 = [number, number];\ntype Vec3 = [number, number, number];\n\ninterface Uniforms {\n iTime: { value: number };\n iResolution: { value: Vec2 };\n rayPos: { value: Vec2 };\n rayDir: { value: Vec2 };\n raysColor: { value: Vec3 };\n raysSpeed: { value: number };\n lightSpread: { value: number };\n rayLength: { value: number };\n pulsating: { value: number };\n fadeDistance: { value: number };\n saturation: { value: number };\n mousePos: { value: Vec2 };\n mouseInfluence: { value: number };\n noiseAmount: { value: number };\n distortion: { value: number };\n}\n\nconst LightRays: React.FC = ({\n raysOrigin = 'top-center',\n raysColor = DEFAULT_COLOR,\n raysSpeed = 1,\n lightSpread = 1,\n rayLength = 2,\n pulsating = false,\n fadeDistance = 1.0,\n saturation = 1.0,\n followMouse = true,\n mouseInfluence = 0.1,\n noiseAmount = 0.0,\n distortion = 0.0,\n className = ''\n}) => {\n const containerRef = useRef(null);\n const uniformsRef = useRef(null);\n const rendererRef = useRef(null);\n const mouseRef = useRef({ x: 0.5, y: 0.5 });\n const smoothMouseRef = useRef({ x: 0.5, y: 0.5 });\n const animationIdRef = useRef(null);\n const meshRef = useRef(null);\n const cleanupFunctionRef = useRef<(() => void) | null>(null);\n const [isVisible, setIsVisible] = useState(false);\n const observerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n observerRef.current = new IntersectionObserver(\n entries => {\n const entry = entries[0];\n setIsVisible(entry.isIntersecting);\n },\n { threshold: 0.1 }\n );\n\n observerRef.current.observe(containerRef.current);\n\n return () => {\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n };\n }, []);\n\n useEffect(() => {\n if (!isVisible || !containerRef.current) return;\n\n if (cleanupFunctionRef.current) {\n cleanupFunctionRef.current();\n cleanupFunctionRef.current = null;\n }\n\n const initializeWebGL = async () => {\n if (!containerRef.current) return;\n\n await new Promise(resolve => setTimeout(resolve, 10));\n\n if (!containerRef.current) return;\n\n const renderer = new Renderer({\n dpr: Math.min(window.devicePixelRatio, 2),\n alpha: true\n });\n rendererRef.current = renderer;\n\n const gl = renderer.gl;\n gl.canvas.style.width = '100%';\n gl.canvas.style.height = '100%';\n\n while (containerRef.current.firstChild) {\n containerRef.current.removeChild(containerRef.current.firstChild);\n }\n containerRef.current.appendChild(gl.canvas);\n\n const vert = `\nattribute vec2 position;\nvarying vec2 vUv;\nvoid main() {\n vUv = position * 0.5 + 0.5;\n gl_Position = vec4(position, 0.0, 1.0);\n}`;\n\n const frag = `precision highp float;\n\nuniform float iTime;\nuniform vec2 iResolution;\n\nuniform vec2 rayPos;\nuniform vec2 rayDir;\nuniform vec3 raysColor;\nuniform float raysSpeed;\nuniform float lightSpread;\nuniform float rayLength;\nuniform float pulsating;\nuniform float fadeDistance;\nuniform float saturation;\nuniform vec2 mousePos;\nuniform float mouseInfluence;\nuniform float noiseAmount;\nuniform float distortion;\n\nvarying vec2 vUv;\n\nfloat noise(vec2 st) {\n return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);\n}\n\nfloat rayStrength(vec2 raySource, vec2 rayRefDirection, vec2 coord,\n float seedA, float seedB, float speed) {\n vec2 sourceToCoord = coord - raySource;\n vec2 dirNorm = normalize(sourceToCoord);\n float cosAngle = dot(dirNorm, rayRefDirection);\n\n float distortedAngle = cosAngle + distortion * sin(iTime * 2.0 + length(sourceToCoord) * 0.01) * 0.2;\n \n float spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / max(lightSpread, 0.001));\n\n float distance = length(sourceToCoord);\n float maxDistance = iResolution.x * rayLength;\n float lengthFalloff = clamp((maxDistance - distance) / maxDistance, 0.0, 1.0);\n \n float fadeFalloff = clamp((iResolution.x * fadeDistance - distance) / (iResolution.x * fadeDistance), 0.5, 1.0);\n float pulse = pulsating > 0.5 ? (0.8 + 0.2 * sin(iTime * speed * 3.0)) : 1.0;\n\n float baseStrength = clamp(\n (0.45 + 0.15 * sin(distortedAngle * seedA + iTime * speed)) +\n (0.3 + 0.2 * cos(-distortedAngle * seedB + iTime * speed)),\n 0.0, 1.0\n );\n\n return baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse;\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 coord = vec2(fragCoord.x, iResolution.y - fragCoord.y);\n \n vec2 finalRayDir = rayDir;\n if (mouseInfluence > 0.0) {\n vec2 mouseScreenPos = mousePos * iResolution.xy;\n vec2 mouseDirection = normalize(mouseScreenPos - rayPos);\n finalRayDir = normalize(mix(rayDir, mouseDirection, mouseInfluence));\n }\n\n vec4 rays1 = vec4(1.0) *\n rayStrength(rayPos, finalRayDir, coord, 36.2214, 21.11349,\n 1.5 * raysSpeed);\n vec4 rays2 = vec4(1.0) *\n rayStrength(rayPos, finalRayDir, coord, 22.3991, 18.0234,\n 1.1 * raysSpeed);\n\n fragColor = rays1 * 0.5 + rays2 * 0.4;\n\n if (noiseAmount > 0.0) {\n float n = noise(coord * 0.01 + iTime * 0.1);\n fragColor.rgb *= (1.0 - noiseAmount + noiseAmount * n);\n }\n\n float brightness = 1.0 - (coord.y / iResolution.y);\n fragColor.x *= 0.1 + brightness * 0.8;\n fragColor.y *= 0.3 + brightness * 0.6;\n fragColor.z *= 0.5 + brightness * 0.5;\n\n if (saturation != 1.0) {\n float gray = dot(fragColor.rgb, vec3(0.299, 0.587, 0.114));\n fragColor.rgb = mix(vec3(gray), fragColor.rgb, saturation);\n }\n\n fragColor.rgb *= raysColor;\n}\n\nvoid main() {\n vec4 color;\n mainImage(color, gl_FragCoord.xy);\n gl_FragColor = color;\n}`;\n\n const uniforms: Uniforms = {\n iTime: { value: 0 },\n iResolution: { value: [1, 1] },\n\n rayPos: { value: [0, 0] },\n rayDir: { value: [0, 1] },\n\n raysColor: { value: hexToRgb(raysColor) },\n raysSpeed: { value: raysSpeed },\n lightSpread: { value: lightSpread },\n rayLength: { value: rayLength },\n pulsating: { value: pulsating ? 1.0 : 0.0 },\n fadeDistance: { value: fadeDistance },\n saturation: { value: saturation },\n mousePos: { value: [0.5, 0.5] },\n mouseInfluence: { value: mouseInfluence },\n noiseAmount: { value: noiseAmount },\n distortion: { value: distortion }\n };\n uniformsRef.current = uniforms;\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex: vert,\n fragment: frag,\n uniforms\n });\n const mesh = new Mesh(gl, { geometry, program });\n meshRef.current = mesh;\n\n const updatePlacement = () => {\n if (!containerRef.current || !renderer) return;\n\n renderer.dpr = Math.min(window.devicePixelRatio, 2);\n\n const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.current;\n renderer.setSize(wCSS, hCSS);\n\n const dpr = renderer.dpr;\n const w = wCSS * dpr;\n const h = hCSS * dpr;\n\n uniforms.iResolution.value = [w, h];\n\n const { anchor, dir } = getAnchorAndDir(raysOrigin, w, h);\n uniforms.rayPos.value = anchor;\n uniforms.rayDir.value = dir;\n };\n\n const loop = (t: number) => {\n if (!rendererRef.current || !uniformsRef.current || !meshRef.current) {\n return;\n }\n\n uniforms.iTime.value = t * 0.001;\n\n if (followMouse && mouseInfluence > 0.0) {\n const smoothing = 0.92;\n\n smoothMouseRef.current.x = smoothMouseRef.current.x * smoothing + mouseRef.current.x * (1 - smoothing);\n smoothMouseRef.current.y = smoothMouseRef.current.y * smoothing + mouseRef.current.y * (1 - smoothing);\n\n uniforms.mousePos.value = [smoothMouseRef.current.x, smoothMouseRef.current.y];\n }\n\n try {\n renderer.render({ scene: mesh });\n animationIdRef.current = requestAnimationFrame(loop);\n } catch (error) {\n console.warn('WebGL rendering error:', error);\n return;\n }\n };\n\n window.addEventListener('resize', updatePlacement);\n updatePlacement();\n animationIdRef.current = requestAnimationFrame(loop);\n\n cleanupFunctionRef.current = () => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n animationIdRef.current = null;\n }\n\n window.removeEventListener('resize', updatePlacement);\n\n if (renderer) {\n try {\n const canvas = renderer.gl.canvas;\n const loseContextExt = renderer.gl.getExtension('WEBGL_lose_context');\n if (loseContextExt) {\n loseContextExt.loseContext();\n }\n\n if (canvas && canvas.parentNode) {\n canvas.parentNode.removeChild(canvas);\n }\n } catch (error) {\n console.warn('Error during WebGL cleanup:', error);\n }\n }\n\n rendererRef.current = null;\n uniformsRef.current = null;\n meshRef.current = null;\n };\n };\n\n initializeWebGL();\n\n return () => {\n if (cleanupFunctionRef.current) {\n cleanupFunctionRef.current();\n cleanupFunctionRef.current = null;\n }\n };\n }, [\n isVisible,\n raysOrigin,\n raysColor,\n raysSpeed,\n lightSpread,\n rayLength,\n pulsating,\n fadeDistance,\n saturation,\n followMouse,\n mouseInfluence,\n noiseAmount,\n distortion\n ]);\n\n useEffect(() => {\n if (!uniformsRef.current || !containerRef.current || !rendererRef.current) return;\n\n const u = uniformsRef.current;\n const renderer = rendererRef.current;\n\n u.raysColor.value = hexToRgb(raysColor);\n u.raysSpeed.value = raysSpeed;\n u.lightSpread.value = lightSpread;\n u.rayLength.value = rayLength;\n u.pulsating.value = pulsating ? 1.0 : 0.0;\n u.fadeDistance.value = fadeDistance;\n u.saturation.value = saturation;\n u.mouseInfluence.value = mouseInfluence;\n u.noiseAmount.value = noiseAmount;\n u.distortion.value = distortion;\n\n const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.current;\n const dpr = renderer.dpr;\n const { anchor, dir } = getAnchorAndDir(raysOrigin, wCSS * dpr, hCSS * dpr);\n u.rayPos.value = anchor;\n u.rayDir.value = dir;\n }, [\n raysColor,\n raysSpeed,\n lightSpread,\n raysOrigin,\n rayLength,\n pulsating,\n fadeDistance,\n saturation,\n mouseInfluence,\n noiseAmount,\n distortion\n ]);\n\n useEffect(() => {\n const handleMouseMove = (e: MouseEvent) => {\n if (!containerRef.current || !rendererRef.current) return;\n const rect = containerRef.current.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = (e.clientY - rect.top) / rect.height;\n mouseRef.current = { x, y };\n };\n\n if (followMouse) {\n window.addEventListener('mousemove', handleMouseMove);\n return () => window.removeEventListener('mousemove', handleMouseMove);\n }\n }, [followMouse]);\n\n return
;\n};\n\nexport default LightRays;\n" } ], diff --git a/public/r/LightTunnel-JS-CSS.json b/public/r/LightTunnel-JS-CSS.json index bb0ba8f3..70e9f769 100644 --- a/public/r/LightTunnel-JS-CSS.json +++ b/public/r/LightTunnel-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "LightTunnel/LightTunnel.css", + "type": "registry:file", + "path": "LightTunnel.css", + "target": "@components/LightTunnel.css", "content": ".light-tunnel-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "LightTunnel/LightTunnel.jsx", + "path": "LightTunnel.jsx", "content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './LightTunnel.css';\n\nconst hexToRgb = hex => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 1, 1];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uSpeed;\nuniform float uFlowDir;\nuniform float uPulseSpeed;\nuniform float uPulseLength;\nuniform float uPulseBlend;\nuniform float uPulseWidth;\nuniform float uCableCount;\nuniform float uThickness;\nuniform float uRimWidth;\nuniform float uWaviness;\nuniform float uSway;\nuniform float uSize;\nuniform vec2 uCenter;\nuniform vec2 uMouseOffset;\nuniform float uGlow;\nuniform float uFadeNear;\nuniform float uFadeFar;\nuniform float uBrightness;\nuniform float uColorVariance;\nuniform float uOpacity;\nuniform vec3 uCableColor;\nuniform vec3 uPulseColor;\nuniform vec3 uTunnelColor;\nuniform float uTunnelOpacity;\nuniform float uGrain;\nuniform float uGrainIntensity;\nout vec4 fragColor;\n\nvoid mainImage(out vec4 o, in vec2 fragCoord) {\n float size = uSize * 2.0;\n float flowDir = uFlowDir;\n float speedBase = uSpeed * 4.0 * flowDir;\n float waviness = uWaviness * 0.15;\n float rotationOsc = uSway * 0.5;\n float baseThick = uThickness * 0.35 + 0.05;\n float borderWeight = uRimWidth * 0.15 + 0.01;\n float cablesCount = floor(uCableCount);\n\n vec2 res = iResolution.xy;\n vec2 uv = (fragCoord - 0.5 * res) / min(res.y, res.x);\n uv -= (uCenter + uMouseOffset);\n uv /= (size + 0.0001);\n\n float r = length(uv);\n float angle = atan(uv.y, uv.x);\n float depth = -log(r + 0.0001);\n\n float swing = sin(iTime * (uSpeed * 0.5 + 0.1)) * rotationOsc;\n float waveOffset = sin(depth * 1.2 + iTime * speedBase * 0.25) * waviness;\n\n float angleNormalized = (angle / 6.2831853) + 0.5;\n float finalAngle = fract(angleNormalized + waveOffset + swing);\n\n float cableID = floor(finalAngle * cablesCount);\n float gvX = (fract(finalAngle * cablesCount) - 0.5);\n\n float rand = fract(sin(cableID * 12.9898) * 43758.5453);\n float randSpeed = (0.4 + rand * 0.6) * speedBase * uPulseSpeed;\n float cableThick = baseThick * (0.6 + rand * 0.4);\n\n vec3 cableCol = uCableColor;\n cableCol *= 1.0 + (rand - 0.5) * 0.4 * uColorVariance;\n cableCol = mix(cableCol, uPulseColor, rand * 0.25 * uColorVariance);\n\n float scroll = depth + (iTime * randSpeed);\n float pulseFact = fract(scroll);\n\n float distToCore = abs(gvX);\n float wireMask = smoothstep(cableThick, cableThick - 0.05, distToCore);\n float rimGlow = smoothstep(borderWeight, 0.0, abs(distToCore - cableThick));\n\n float pulseThick = cableThick * uPulseWidth;\n float pulseMask = smoothstep(pulseThick, pulseThick - 0.05 * uPulseWidth, distToCore);\n\n float pulseDist = abs(pulseFact - 0.5);\n float pulseTotal = uPulseLength;\n float pulseCore = pulseTotal * (1.0 - uPulseBlend);\n float pulseLo = min(pulseCore, pulseTotal - max(fwidth(scroll), 1e-4));\n float dataPulse = 1.0 - smoothstep(pulseLo, pulseTotal, pulseDist);\n\n float aBody = wireMask * uTunnelOpacity;\n float aRim = rimGlow;\n float aPulse = clamp(dataPulse * pulseMask, 0.0, 1.0);\n\n vec3 fiberCol = uTunnelColor * aBody\n + cableCol * aRim * 1.3 * uGlow\n + uPulseColor * dataPulse * 3.0 * pulseMask;\n\n float distFade = smoothstep(0.0, uFadeNear, r) * smoothstep(uFadeFar, uFadeFar - 0.9, r);\n float inten = clamp(aBody + aRim + aPulse, 0.0, 1.0) * distFade;\n\n vec3 finalCol = fiberCol * uBrightness;\n float alpha = clamp(inten, 0.0, 1.0) * uOpacity;\n vec3 outRgb = finalCol * alpha;\n\n if (uGrain > 0.5) {\n float gv = (fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233)) + iTime) * 43758.5453) - 0.5) * uGrainIntensity;\n outRgb = clamp(outRgb + gv, 0.0, 1.0);\n alpha = clamp(alpha + gv, 0.0, 1.0);\n }\n\n o = vec4(outRgb, alpha);\n}\n\nvoid main() {\n vec4 o = vec4(0.0);\n mainImage(o, gl_FragCoord.xy);\n fragColor = o;\n}\n`;\n\nconst ctxMap = new WeakMap();\n\nconst LightTunnel = ({\n cableColor = '#A855F7',\n pulseColor = '#A855F7',\n tunnelColor = '#5227FF',\n tunnelOpacity = 0,\n speed = 0.1,\n flowDirection = 'outward',\n pulseSpeed = 2,\n pulseLength = 0.28,\n pulseBlend = 1,\n pulseWidth = 1,\n cableCount = 20,\n thickness = 0.35,\n rimWidth = 0.15,\n waviness = 0.3,\n sway = 0.5,\n size = 1.0,\n centerX = 0.0,\n centerY = 0.0,\n glow = 1.0,\n fadeNear = 0.5,\n fadeFar = 2,\n brightness = 1.0,\n colorVariance = true,\n grain = true,\n grainIntensity = 0.05,\n opacity = 1.0,\n mouseInteraction = true,\n mouseStrength = 0.1,\n className = ''\n}) => {\n const containerRef = useRef(null);\n const mouseEnabledRef = useRef(mouseInteraction);\n const mouseStrengthRef = useRef(mouseStrength);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n webgl: 2,\n alpha: true,\n premultipliedAlpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n const canvas = gl.canvas;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uSpeed: { value: 0.1 },\n uFlowDir: { value: -1.0 },\n uPulseSpeed: { value: 2.0 },\n uPulseLength: { value: 0.28 },\n uPulseBlend: { value: 1.0 },\n uPulseWidth: { value: 1.0 },\n uCableCount: { value: 20 },\n uThickness: { value: 0.35 },\n uRimWidth: { value: 0.15 },\n uWaviness: { value: 0.3 },\n uSway: { value: 0.5 },\n uSize: { value: 1.0 },\n uCenter: { value: new Float32Array([0, 0]) },\n uMouseOffset: { value: new Float32Array([0, 0]) },\n uGlow: { value: 1.0 },\n uFadeNear: { value: 0.5 },\n uFadeFar: { value: 2.0 },\n uBrightness: { value: 1.0 },\n uColorVariance: { value: 1.0 },\n uOpacity: { value: 1.0 },\n uCableColor: { value: new Float32Array([0.65882353, 0.33333333, 0.96862745]) },\n uPulseColor: { value: new Float32Array([0.65882353, 0.33333333, 0.96862745]) },\n uTunnelColor: { value: new Float32Array([0.32156863, 0.15294118, 1]) },\n uTunnelOpacity: { value: 0.0 },\n uGrain: { value: 1.0 },\n uGrainIntensity: { value: 0.05 }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n ctxMap.set(container, { renderer, program, mesh });\n\n const setSize = () => {\n const rect = container.getBoundingClientRect();\n const w = Math.max(1, Math.floor(rect.width));\n const h = Math.max(1, Math.floor(rect.height));\n renderer.setSize(w, h);\n const res = program.uniforms.iResolution.value;\n res[0] = gl.drawingBufferWidth;\n res[1] = gl.drawingBufferHeight;\n renderer.render({ scene: mesh });\n };\n\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n setSize();\n\n let currentMouse = [0.5, 0.5];\n let targetMouse = [0.5, 0.5];\n\n const handleMouseMove = e => {\n const rect = canvas.getBoundingClientRect();\n targetMouse = [(e.clientX - rect.left) / rect.width, 1.0 - (e.clientY - rect.top) / rect.height];\n };\n const handleMouseLeave = () => {\n targetMouse = [0.5, 0.5];\n };\n canvas.addEventListener('mousemove', handleMouseMove);\n canvas.addEventListener('mouseleave', handleMouseLeave);\n\n let raf = 0;\n let isVisible = true;\n let isPageVisible = !document.hidden;\n const t0 = performance.now();\n\n const loop = t => {\n program.uniforms.iTime.value = (t - t0) * 0.001;\n\n if (mouseEnabledRef.current) {\n currentMouse[0] += 0.05 * (targetMouse[0] - currentMouse[0]);\n currentMouse[1] += 0.05 * (targetMouse[1] - currentMouse[1]);\n } else {\n currentMouse[0] += 0.05 * (0.5 - currentMouse[0]);\n currentMouse[1] += 0.05 * (0.5 - currentMouse[1]);\n }\n const off = program.uniforms.uMouseOffset.value;\n off[0] = (currentMouse[0] - 0.5) * mouseStrengthRef.current;\n off[1] = (currentMouse[1] - 0.5) * mouseStrengthRef.current;\n\n renderer.render({ scene: mesh });\n raf = requestAnimationFrame(loop);\n };\n\n const tryStart = () => {\n if (isVisible && isPageVisible && raf === 0) raf = requestAnimationFrame(loop);\n };\n const tryStop = () => {\n if (raf !== 0) {\n cancelAnimationFrame(raf);\n raf = 0;\n }\n };\n\n const io = new IntersectionObserver(\n ([entry]) => {\n isVisible = entry.isIntersecting;\n isVisible ? tryStart() : tryStop();\n },\n { threshold: 0 }\n );\n io.observe(container);\n\n const onVisibility = () => {\n isPageVisible = !document.hidden;\n isPageVisible ? tryStart() : tryStop();\n };\n document.addEventListener('visibilitychange', onVisibility);\n\n tryStart();\n\n return () => {\n tryStop();\n ro.disconnect();\n io.disconnect();\n document.removeEventListener('visibilitychange', onVisibility);\n canvas.removeEventListener('mousemove', handleMouseMove);\n canvas.removeEventListener('mouseleave', handleMouseLeave);\n ctxMap.delete(container);\n try {\n container.removeChild(canvas);\n } catch {}\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, []);\n\n useEffect(() => {\n mouseEnabledRef.current = mouseInteraction;\n mouseStrengthRef.current = mouseStrength;\n\n const container = containerRef.current;\n if (!container) return;\n const ctx = ctxMap.get(container);\n if (!ctx) return;\n const { program } = ctx;\n const u = program.uniforms;\n\n u.uSpeed.value = speed;\n u.uFlowDir.value = flowDirection === 'outward' ? -1.0 : 1.0;\n u.uPulseSpeed.value = pulseSpeed;\n u.uPulseLength.value = pulseLength;\n u.uPulseBlend.value = pulseBlend;\n u.uPulseWidth.value = pulseWidth;\n u.uCableCount.value = cableCount;\n u.uThickness.value = thickness;\n u.uRimWidth.value = rimWidth;\n u.uWaviness.value = waviness;\n u.uSway.value = sway;\n u.uSize.value = size;\n const center = u.uCenter.value;\n center[0] = centerX;\n center[1] = centerY;\n u.uGlow.value = glow;\n u.uFadeNear.value = fadeNear;\n u.uFadeFar.value = fadeFar;\n u.uBrightness.value = brightness;\n u.uColorVariance.value = colorVariance ? 1.0 : 0.0;\n u.uGrain.value = grain ? 1.0 : 0.0;\n u.uGrainIntensity.value = grainIntensity;\n u.uOpacity.value = opacity;\n const cable = hexToRgb(cableColor);\n const cableU = u.uCableColor.value;\n cableU[0] = cable[0];\n cableU[1] = cable[1];\n cableU[2] = cable[2];\n const pulse = hexToRgb(pulseColor);\n const pulseU = u.uPulseColor.value;\n pulseU[0] = pulse[0];\n pulseU[1] = pulse[1];\n pulseU[2] = pulse[2];\n const tunnel = hexToRgb(tunnelColor);\n const tunnelU = u.uTunnelColor.value;\n tunnelU[0] = tunnel[0];\n tunnelU[1] = tunnel[1];\n tunnelU[2] = tunnel[2];\n u.uTunnelOpacity.value = tunnelOpacity;\n }, [\n cableColor,\n pulseColor,\n tunnelColor,\n tunnelOpacity,\n speed,\n flowDirection,\n pulseSpeed,\n pulseLength,\n pulseBlend,\n pulseWidth,\n cableCount,\n thickness,\n rimWidth,\n waviness,\n sway,\n size,\n centerX,\n centerY,\n glow,\n fadeNear,\n fadeFar,\n brightness,\n colorVariance,\n grain,\n grainIntensity,\n opacity,\n mouseInteraction,\n mouseStrength\n ]);\n\n return
;\n};\n\nexport default LightTunnel;\n" } ], diff --git a/public/r/LightTunnel-TS-CSS.json b/public/r/LightTunnel-TS-CSS.json index 644db025..79ab6a5c 100644 --- a/public/r/LightTunnel-TS-CSS.json +++ b/public/r/LightTunnel-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "LightTunnel/LightTunnel.css", + "type": "registry:file", + "path": "LightTunnel.css", + "target": "@components/LightTunnel.css", "content": ".light-tunnel-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "LightTunnel/LightTunnel.tsx", + "path": "LightTunnel.tsx", "content": "import React, { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './LightTunnel.css';\n\nexport type FlowDirection = 'inward' | 'outward';\n\nexport interface LightTunnelProps {\n cableColor?: string;\n pulseColor?: string;\n tunnelColor?: string;\n tunnelOpacity?: number;\n speed?: number;\n flowDirection?: FlowDirection;\n pulseSpeed?: number;\n pulseLength?: number;\n pulseBlend?: number;\n pulseWidth?: number;\n cableCount?: number;\n thickness?: number;\n rimWidth?: number;\n waviness?: number;\n sway?: number;\n size?: number;\n centerX?: number;\n centerY?: number;\n glow?: number;\n fadeNear?: number;\n fadeFar?: number;\n brightness?: number;\n colorVariance?: boolean;\n grain?: boolean;\n grainIntensity?: number;\n opacity?: number;\n mouseInteraction?: boolean;\n mouseStrength?: number;\n className?: string;\n}\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 1, 1];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uSpeed;\nuniform float uFlowDir;\nuniform float uPulseSpeed;\nuniform float uPulseLength;\nuniform float uPulseBlend;\nuniform float uPulseWidth;\nuniform float uCableCount;\nuniform float uThickness;\nuniform float uRimWidth;\nuniform float uWaviness;\nuniform float uSway;\nuniform float uSize;\nuniform vec2 uCenter;\nuniform vec2 uMouseOffset;\nuniform float uGlow;\nuniform float uFadeNear;\nuniform float uFadeFar;\nuniform float uBrightness;\nuniform float uColorVariance;\nuniform float uOpacity;\nuniform vec3 uCableColor;\nuniform vec3 uPulseColor;\nuniform vec3 uTunnelColor;\nuniform float uTunnelOpacity;\nuniform float uGrain;\nuniform float uGrainIntensity;\nout vec4 fragColor;\n\nvoid mainImage(out vec4 o, in vec2 fragCoord) {\n float size = uSize * 2.0;\n float flowDir = uFlowDir;\n float speedBase = uSpeed * 4.0 * flowDir;\n float waviness = uWaviness * 0.15;\n float rotationOsc = uSway * 0.5;\n float baseThick = uThickness * 0.35 + 0.05;\n float borderWeight = uRimWidth * 0.15 + 0.01;\n float cablesCount = floor(uCableCount);\n\n vec2 res = iResolution.xy;\n vec2 uv = (fragCoord - 0.5 * res) / min(res.y, res.x);\n uv -= (uCenter + uMouseOffset);\n uv /= (size + 0.0001);\n\n float r = length(uv);\n float angle = atan(uv.y, uv.x);\n float depth = -log(r + 0.0001);\n\n float swing = sin(iTime * (uSpeed * 0.5 + 0.1)) * rotationOsc;\n float waveOffset = sin(depth * 1.2 + iTime * speedBase * 0.25) * waviness;\n\n float angleNormalized = (angle / 6.2831853) + 0.5;\n float finalAngle = fract(angleNormalized + waveOffset + swing);\n\n float cableID = floor(finalAngle * cablesCount);\n float gvX = (fract(finalAngle * cablesCount) - 0.5);\n\n float rand = fract(sin(cableID * 12.9898) * 43758.5453);\n float randSpeed = (0.4 + rand * 0.6) * speedBase * uPulseSpeed;\n float cableThick = baseThick * (0.6 + rand * 0.4);\n\n vec3 cableCol = uCableColor;\n cableCol *= 1.0 + (rand - 0.5) * 0.4 * uColorVariance;\n cableCol = mix(cableCol, uPulseColor, rand * 0.25 * uColorVariance);\n\n float scroll = depth + (iTime * randSpeed);\n float pulseFact = fract(scroll);\n\n float distToCore = abs(gvX);\n float wireMask = smoothstep(cableThick, cableThick - 0.05, distToCore);\n float rimGlow = smoothstep(borderWeight, 0.0, abs(distToCore - cableThick));\n\n float pulseThick = cableThick * uPulseWidth;\n float pulseMask = smoothstep(pulseThick, pulseThick - 0.05 * uPulseWidth, distToCore);\n\n float pulseDist = abs(pulseFact - 0.5);\n float pulseTotal = uPulseLength;\n float pulseCore = pulseTotal * (1.0 - uPulseBlend);\n float pulseLo = min(pulseCore, pulseTotal - max(fwidth(scroll), 1e-4));\n float dataPulse = 1.0 - smoothstep(pulseLo, pulseTotal, pulseDist);\n\n float aBody = wireMask * uTunnelOpacity;\n float aRim = rimGlow;\n float aPulse = clamp(dataPulse * pulseMask, 0.0, 1.0);\n\n vec3 fiberCol = uTunnelColor * aBody\n + cableCol * aRim * 1.3 * uGlow\n + uPulseColor * dataPulse * 3.0 * pulseMask;\n\n float distFade = smoothstep(0.0, uFadeNear, r) * smoothstep(uFadeFar, uFadeFar - 0.9, r);\n float inten = clamp(aBody + aRim + aPulse, 0.0, 1.0) * distFade;\n\n vec3 finalCol = fiberCol * uBrightness;\n float alpha = clamp(inten, 0.0, 1.0) * uOpacity;\n vec3 outRgb = finalCol * alpha;\n\n if (uGrain > 0.5) {\n float gv = (fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233)) + iTime) * 43758.5453) - 0.5) * uGrainIntensity;\n outRgb = clamp(outRgb + gv, 0.0, 1.0);\n alpha = clamp(alpha + gv, 0.0, 1.0);\n }\n\n o = vec4(outRgb, alpha);\n}\n\nvoid main() {\n vec4 o = vec4(0.0);\n mainImage(o, gl_FragCoord.xy);\n fragColor = o;\n}\n`;\n\ntype LightTunnelCtx = {\n renderer: InstanceType;\n program: InstanceType;\n mesh: InstanceType;\n};\nconst ctxMap = new WeakMap();\n\nconst LightTunnel: React.FC = ({\n cableColor = '#A855F7',\n pulseColor = '#A855F7',\n tunnelColor = '#5227FF',\n tunnelOpacity = 0,\n speed = 0.1,\n flowDirection = 'outward',\n pulseSpeed = 2,\n pulseLength = 0.28,\n pulseBlend = 1,\n pulseWidth = 1,\n cableCount = 20,\n thickness = 0.35,\n rimWidth = 0.15,\n waviness = 0.3,\n sway = 0.5,\n size = 1.0,\n centerX = 0.0,\n centerY = 0.0,\n glow = 1.0,\n fadeNear = 0.5,\n fadeFar = 2,\n brightness = 1.0,\n colorVariance = true,\n grain = true,\n grainIntensity = 0.05,\n opacity = 1.0,\n mouseInteraction = true,\n mouseStrength = 0.1,\n className = ''\n}) => {\n const containerRef = useRef(null);\n const mouseEnabledRef = useRef(mouseInteraction);\n const mouseStrengthRef = useRef(mouseStrength);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n webgl: 2,\n alpha: true,\n premultipliedAlpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n const canvas = gl.canvas as HTMLCanvasElement;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uSpeed: { value: 0.1 },\n uFlowDir: { value: -1.0 },\n uPulseSpeed: { value: 2.0 },\n uPulseLength: { value: 0.28 },\n uPulseBlend: { value: 1.0 },\n uPulseWidth: { value: 1.0 },\n uCableCount: { value: 20 },\n uThickness: { value: 0.35 },\n uRimWidth: { value: 0.15 },\n uWaviness: { value: 0.3 },\n uSway: { value: 0.5 },\n uSize: { value: 1.0 },\n uCenter: { value: new Float32Array([0, 0]) },\n uMouseOffset: { value: new Float32Array([0, 0]) },\n uGlow: { value: 1.0 },\n uFadeNear: { value: 0.5 },\n uFadeFar: { value: 2.0 },\n uBrightness: { value: 1.0 },\n uColorVariance: { value: 1.0 },\n uOpacity: { value: 1.0 },\n uCableColor: { value: new Float32Array([0.65882353, 0.33333333, 0.96862745]) },\n uPulseColor: { value: new Float32Array([0.65882353, 0.33333333, 0.96862745]) },\n uTunnelColor: { value: new Float32Array([0.32156863, 0.15294118, 1]) },\n uTunnelOpacity: { value: 0.0 },\n uGrain: { value: 1.0 },\n uGrainIntensity: { value: 0.05 }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n ctxMap.set(container, { renderer, program, mesh });\n\n const setSize = () => {\n const rect = container.getBoundingClientRect();\n const w = Math.max(1, Math.floor(rect.width));\n const h = Math.max(1, Math.floor(rect.height));\n renderer.setSize(w, h);\n const res = (program.uniforms.iResolution as { value: Float32Array }).value;\n res[0] = gl.drawingBufferWidth;\n res[1] = gl.drawingBufferHeight;\n renderer.render({ scene: mesh });\n };\n\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n setSize();\n\n let currentMouse: [number, number] = [0.5, 0.5];\n let targetMouse: [number, number] = [0.5, 0.5];\n\n const handleMouseMove = (e: MouseEvent) => {\n const rect = canvas.getBoundingClientRect();\n targetMouse = [(e.clientX - rect.left) / rect.width, 1.0 - (e.clientY - rect.top) / rect.height];\n };\n const handleMouseLeave = () => {\n targetMouse = [0.5, 0.5];\n };\n canvas.addEventListener('mousemove', handleMouseMove);\n canvas.addEventListener('mouseleave', handleMouseLeave);\n\n let raf = 0;\n let isVisible = true;\n let isPageVisible = !document.hidden;\n const t0 = performance.now();\n\n const loop = (t: number) => {\n (program.uniforms.iTime as { value: number }).value = (t - t0) * 0.001;\n\n if (mouseEnabledRef.current) {\n currentMouse[0] += 0.05 * (targetMouse[0] - currentMouse[0]);\n currentMouse[1] += 0.05 * (targetMouse[1] - currentMouse[1]);\n } else {\n currentMouse[0] += 0.05 * (0.5 - currentMouse[0]);\n currentMouse[1] += 0.05 * (0.5 - currentMouse[1]);\n }\n const off = (program.uniforms.uMouseOffset as { value: Float32Array }).value;\n off[0] = (currentMouse[0] - 0.5) * mouseStrengthRef.current;\n off[1] = (currentMouse[1] - 0.5) * mouseStrengthRef.current;\n\n renderer.render({ scene: mesh });\n raf = requestAnimationFrame(loop);\n };\n\n const tryStart = () => {\n if (isVisible && isPageVisible && raf === 0) raf = requestAnimationFrame(loop);\n };\n const tryStop = () => {\n if (raf !== 0) {\n cancelAnimationFrame(raf);\n raf = 0;\n }\n };\n\n const io = new IntersectionObserver(\n ([entry]) => {\n isVisible = entry.isIntersecting;\n isVisible ? tryStart() : tryStop();\n },\n { threshold: 0 }\n );\n io.observe(container);\n\n const onVisibility = () => {\n isPageVisible = !document.hidden;\n isPageVisible ? tryStart() : tryStop();\n };\n document.addEventListener('visibilitychange', onVisibility);\n\n tryStart();\n\n return () => {\n tryStop();\n ro.disconnect();\n io.disconnect();\n document.removeEventListener('visibilitychange', onVisibility);\n canvas.removeEventListener('mousemove', handleMouseMove);\n canvas.removeEventListener('mouseleave', handleMouseLeave);\n ctxMap.delete(container);\n try {\n container.removeChild(canvas);\n } catch {}\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, []);\n\n useEffect(() => {\n mouseEnabledRef.current = mouseInteraction;\n mouseStrengthRef.current = mouseStrength;\n\n const container = containerRef.current;\n if (!container) return;\n const ctx = ctxMap.get(container);\n if (!ctx) return;\n const { program } = ctx;\n const u = program.uniforms as Record;\n\n u.uSpeed.value = speed;\n u.uFlowDir.value = flowDirection === 'outward' ? -1.0 : 1.0;\n u.uPulseSpeed.value = pulseSpeed;\n u.uPulseLength.value = pulseLength;\n u.uPulseBlend.value = pulseBlend;\n u.uPulseWidth.value = pulseWidth;\n u.uCableCount.value = cableCount;\n u.uThickness.value = thickness;\n u.uRimWidth.value = rimWidth;\n u.uWaviness.value = waviness;\n u.uSway.value = sway;\n u.uSize.value = size;\n const center = u.uCenter.value as Float32Array;\n center[0] = centerX;\n center[1] = centerY;\n u.uGlow.value = glow;\n u.uFadeNear.value = fadeNear;\n u.uFadeFar.value = fadeFar;\n u.uBrightness.value = brightness;\n u.uColorVariance.value = colorVariance ? 1.0 : 0.0;\n u.uGrain.value = grain ? 1.0 : 0.0;\n u.uGrainIntensity.value = grainIntensity;\n u.uOpacity.value = opacity;\n const cable = hexToRgb(cableColor);\n const cableU = u.uCableColor.value as Float32Array;\n cableU[0] = cable[0];\n cableU[1] = cable[1];\n cableU[2] = cable[2];\n const pulse = hexToRgb(pulseColor);\n const pulseU = u.uPulseColor.value as Float32Array;\n pulseU[0] = pulse[0];\n pulseU[1] = pulse[1];\n pulseU[2] = pulse[2];\n const tunnel = hexToRgb(tunnelColor);\n const tunnelU = u.uTunnelColor.value as Float32Array;\n tunnelU[0] = tunnel[0];\n tunnelU[1] = tunnel[1];\n tunnelU[2] = tunnel[2];\n u.uTunnelOpacity.value = tunnelOpacity;\n }, [\n cableColor,\n pulseColor,\n tunnelColor,\n tunnelOpacity,\n speed,\n flowDirection,\n pulseSpeed,\n pulseLength,\n pulseBlend,\n pulseWidth,\n cableCount,\n thickness,\n rimWidth,\n waviness,\n sway,\n size,\n centerX,\n centerY,\n glow,\n fadeNear,\n fadeFar,\n brightness,\n colorVariance,\n grain,\n grainIntensity,\n opacity,\n mouseInteraction,\n mouseStrength\n ]);\n\n return
;\n};\n\nexport default LightTunnel;\n" } ], diff --git a/public/r/Lightfall-JS-CSS.json b/public/r/Lightfall-JS-CSS.json index 61f66cac..d2d5787d 100644 --- a/public/r/Lightfall-JS-CSS.json +++ b/public/r/Lightfall-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Lightfall/Lightfall.css", + "type": "registry:file", + "path": "Lightfall.css", + "target": "@components/Lightfall.css", "content": ".lightfall-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "Lightfall/Lightfall.jsx", + "path": "Lightfall.jsx", "content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './Lightfall.css';\n\nconst MAX_COLORS = 8;\n\nconst hexToRGB = hex => {\n const c = hex.replace('#', '').padEnd(6, '0');\n const r = parseInt(c.slice(0, 2), 16) / 255;\n const g = parseInt(c.slice(2, 4), 16) / 255;\n const b = parseInt(c.slice(4, 6), 16) / 255;\n return [r, g, b];\n};\n\nconst prepColors = input => {\n const base = (input && input.length ? input : ['#A6C8FF', '#5227FF', '#FF9FFC']).slice(0, MAX_COLORS);\n const count = base.length;\n const arr = [];\n for (let i = 0; i < MAX_COLORS; i++) arr.push(hexToRGB(base[Math.min(i, base.length - 1)]));\n const avg = [0, 0, 0];\n for (let i = 0; i < count; i++) {\n avg[0] += arr[i][0];\n avg[1] += arr[i][1];\n avg[2] += arr[i][2];\n }\n avg[0] /= count;\n avg[1] /= count;\n avg[2] /= count;\n return { arr, count, avg };\n};\n\nconst vertex = `\nattribute vec2 position;\nattribute vec2 uv;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `\nprecision highp float;\n\nuniform vec3 iResolution;\nuniform vec2 iMouse;\nuniform float iTime;\n\nuniform vec3 uColor0;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nuniform vec3 uColor4;\nuniform vec3 uColor5;\nuniform vec3 uColor6;\nuniform vec3 uColor7;\nuniform int uColorCount;\n\nuniform vec3 uBgColor;\nuniform vec3 uMouseColor;\nuniform float uSpeed;\nuniform int uStreakCount;\nuniform float uStreakWidth;\nuniform float uStreakLength;\nuniform float uGlow;\nuniform float uDensity;\nuniform float uTwinkle;\nuniform float uZoom;\nuniform float uBgGlow;\nuniform float uOpacity;\nuniform float uMouseEnabled;\nuniform float uMouseStrength;\nuniform float uMouseRadius;\n\nvarying vec2 vUv;\n\nvec3 palette(float h) {\n int count = uColorCount;\n if (count < 1) count = 1;\n int idx = int(floor(clamp(h, 0.0, 0.999999) * float(count)));\n if (idx <= 0) return uColor0;\n if (idx == 1) return uColor1;\n if (idx == 2) return uColor2;\n if (idx == 3) return uColor3;\n if (idx == 4) return uColor4;\n if (idx == 5) return uColor5;\n if (idx == 6) return uColor6;\n return uColor7;\n}\n\nvec3 tanhv(vec3 x) {\n vec3 e = exp(-2.0 * x);\n return (1.0 - e) / (1.0 + e);\n}\n\nvec2 sceneC(vec2 frag, vec2 r) {\n vec2 P = (frag + frag - r) / r.x;\n float z = 0.0;\n float d = 1e3;\n vec4 O = vec4(0.0);\n for (int k = 0; k < 39; k++) {\n if (d <= 1e-4) break;\n O = z * normalize(vec4(P, uZoom, 0.0)) - vec4(0.0, 4.0, 1.0, 0.0) / 4.5;\n d = 1.0 - sqrt(length(O * O));\n z += d;\n }\n return vec2(O.x, atan(O.z, O.y));\n}\n\nvoid mainImage(out vec4 o, vec2 C) {\n vec2 r = iResolution.xy;\n vec2 uv0 = (C + C - r) / r.x;\n float T = 0.1 * iTime * uSpeed + 9.0;\n float angRings = max(1.0, floor(6.28318530718 * max(uDensity, 0.05) + 0.5));\n vec2 Y = vec2(5e-3, 6.28318530718 / angRings);\n\n vec2 c0 = sceneC(C, r);\n vec2 cdx = sceneC(C + vec2(1.0, 0.0), r);\n vec2 cdy = sceneC(C + vec2(0.0, 1.0), r);\n vec2 dCx = cdx - c0;\n vec2 dCy = cdy - c0;\n dCx.y -= 6.28318530718 * floor(dCx.y / 6.28318530718 + 0.5);\n dCy.y -= 6.28318530718 * floor(dCy.y / 6.28318530718 + 0.5);\n vec2 fw = abs(dCx) + abs(dCy);\n C = c0;\n\n vec2 P = vec2(2.0, 1.0) * uv0 - (r / r.x) * vec2(0.0, 1.0);\n vec4 O = vec4(uBgColor * 90.0 * uBgGlow / (1e3 * dot(P, P) + 6.0), 0.0);\n\n float mGlow = 0.0;\n if (uMouseEnabled > 0.5) {\n vec2 mN = (iMouse + iMouse - r) / r.x;\n float md = length(uv0 - mN);\n mGlow = exp(-md * md / max(uMouseRadius * uMouseRadius, 1e-4)) * uMouseStrength;\n O.rgb += uMouseColor * mGlow * 0.25;\n }\n\n float zr = 5e-4 * uStreakWidth;\n vec2 rr = vec2(max(length(fw), 1e-5));\n float tail = 19.0 / max(uStreakLength, 0.05);\n\n for (int m = 0; m < 16; m++) {\n if (m >= uStreakCount) break;\n float jf = float(m) + 1.0;\n float ic = fract(sin(dot(vec2(jf, floor(C.x / Y.x + 0.5)), vec2(7.0, 11.0)) * 73.0));\n vec2 Pp = C - (T + T * ic) * vec2(0.0, 1.0);\n Pp -= floor(Pp / Y + 0.5) * Y;\n float h = fract(8663.0 * ic);\n vec3 col = palette(h);\n float weight = mix(1.5, 1.0 + sin(T + 7.0 * h + 4.0), uTwinkle);\n weight *= (1.0 + mGlow * 2.0);\n vec2 inner = vec2(length(max(Pp, vec2(-1.0, 0.0))), length(Pp) - zr) - zr;\n vec2 sm = vec2(1.0) - smoothstep(-rr, rr, inner);\n O.rgb += dot(sm, vec2(exp(tail * Pp.y), 3.0)) * col * weight;\n C.x += Y.x / 8.0;\n }\n\n vec3 colr = sqrt(tanhv(max(O.rgb * uGlow - vec3(0.04, 0.08, 0.02), 0.0)));\n o = vec4(colr, uOpacity);\n}\n\nvoid main() {\n vec4 color;\n mainImage(color, vUv * iResolution.xy);\n gl_FragColor = color;\n}\n`;\n\nconst Lightfall = ({\n className,\n dpr,\n paused = false,\n colors = ['#A6C8FF', '#5227FF', '#FF9FFC'],\n backgroundColor = '#0A29FF',\n speed = 0.5,\n streakCount = 2,\n streakWidth = 1,\n streakLength = 1,\n glow = 1,\n density = 0.6,\n twinkle = 1,\n zoom = 3,\n backgroundGlow = 0.5,\n opacity = 1,\n mouseInteraction = true,\n mouseStrength = 0.5,\n mouseRadius = 1,\n mouseDampening = 0.15,\n mixBlendMode\n}) => {\n const containerRef = useRef(null);\n const rafRef = useRef(null);\n const programRef = useRef(null);\n const meshRef = useRef(null);\n const geometryRef = useRef(null);\n const rendererRef = useRef(null);\n const mouseTargetRef = useRef([0, 0]);\n const lastTimeRef = useRef(0);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n dpr: dpr ?? (typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1),\n alpha: true,\n antialias: true\n });\n rendererRef.current = renderer;\n const gl = renderer.gl;\n const canvas = gl.canvas;\n\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const { arr, count, avg } = prepColors(colors);\n\n const uniforms = {\n iResolution: { value: [gl.drawingBufferWidth, gl.drawingBufferHeight, 1] },\n iMouse: { value: [0, 0] },\n iTime: { value: 0 },\n uColor0: { value: arr[0] },\n uColor1: { value: arr[1] },\n uColor2: { value: arr[2] },\n uColor3: { value: arr[3] },\n uColor4: { value: arr[4] },\n uColor5: { value: arr[5] },\n uColor6: { value: arr[6] },\n uColor7: { value: arr[7] },\n uColorCount: { value: count },\n uBgColor: { value: hexToRGB(backgroundColor) },\n uMouseColor: { value: avg },\n uSpeed: { value: speed },\n uStreakCount: { value: Math.max(1, Math.min(16, Math.round(streakCount))) },\n uStreakWidth: { value: streakWidth },\n uStreakLength: { value: streakLength },\n uGlow: { value: glow },\n uDensity: { value: density },\n uTwinkle: { value: twinkle },\n uZoom: { value: zoom },\n uBgGlow: { value: backgroundGlow },\n uOpacity: { value: opacity },\n uMouseEnabled: { value: mouseInteraction ? 1 : 0 },\n uMouseStrength: { value: mouseStrength },\n uMouseRadius: { value: mouseRadius }\n };\n\n const program = new Program(gl, { vertex, fragment, uniforms });\n programRef.current = program;\n\n const geometry = new Triangle(gl);\n geometryRef.current = geometry;\n const mesh = new Mesh(gl, { geometry, program });\n meshRef.current = mesh;\n\n const resize = () => {\n const rect = container.getBoundingClientRect();\n renderer.setSize(rect.width, rect.height);\n uniforms.iResolution.value = [gl.drawingBufferWidth, gl.drawingBufferHeight, 1];\n };\n\n resize();\n const ro = new ResizeObserver(resize);\n ro.observe(container);\n\n const onPointerMove = e => {\n const rect = canvas.getBoundingClientRect();\n const scale = renderer.dpr || 1;\n const x = (e.clientX - rect.left) * scale;\n const y = (rect.height - (e.clientY - rect.top)) * scale;\n mouseTargetRef.current = [x, y];\n if (mouseDampening <= 0) {\n uniforms.iMouse.value = [x, y];\n }\n };\n if (mouseInteraction) {\n canvas.addEventListener('pointermove', onPointerMove);\n }\n\n const loop = t => {\n rafRef.current = requestAnimationFrame(loop);\n uniforms.iTime.value = t * 0.001;\n if (mouseDampening > 0) {\n if (!lastTimeRef.current) lastTimeRef.current = t;\n const dt = (t - lastTimeRef.current) / 1000;\n lastTimeRef.current = t;\n const tau = Math.max(1e-4, mouseDampening);\n let factor = 1 - Math.exp(-dt / tau);\n if (factor > 1) factor = 1;\n const target = mouseTargetRef.current;\n const cur = uniforms.iMouse.value;\n cur[0] += (target[0] - cur[0]) * factor;\n cur[1] += (target[1] - cur[1]) * factor;\n } else {\n lastTimeRef.current = t;\n }\n if (!paused && programRef.current && meshRef.current) {\n try {\n renderer.render({ scene: meshRef.current });\n } catch (e) {\n console.error(e);\n }\n }\n };\n rafRef.current = requestAnimationFrame(loop);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n if (mouseInteraction) canvas.removeEventListener('pointermove', onPointerMove);\n ro.disconnect();\n if (canvas.parentElement === container) {\n container.removeChild(canvas);\n }\n const callIfFn = (obj, key) => {\n if (obj && typeof obj[key] === 'function') {\n obj[key].call(obj);\n }\n };\n callIfFn(programRef.current, 'remove');\n callIfFn(geometryRef.current, 'remove');\n callIfFn(meshRef.current, 'remove');\n callIfFn(rendererRef.current, 'destroy');\n programRef.current = null;\n geometryRef.current = null;\n meshRef.current = null;\n rendererRef.current = null;\n };\n }, [\n dpr,\n paused,\n colors,\n backgroundColor,\n speed,\n streakCount,\n streakWidth,\n streakLength,\n glow,\n density,\n twinkle,\n zoom,\n backgroundGlow,\n opacity,\n mouseInteraction,\n mouseStrength,\n mouseRadius,\n mouseDampening\n ]);\n\n return (\n \n );\n};\n\nexport default Lightfall;\n" } ], diff --git a/public/r/Lightfall-TS-CSS.json b/public/r/Lightfall-TS-CSS.json index 39cc1032..1e4e75b4 100644 --- a/public/r/Lightfall-TS-CSS.json +++ b/public/r/Lightfall-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Lightfall/Lightfall.css", + "type": "registry:file", + "path": "Lightfall.css", + "target": "@components/Lightfall.css", "content": ".lightfall-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "Lightfall/Lightfall.tsx", + "path": "Lightfall.tsx", "content": "import React, { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './Lightfall.css';\n\nexport interface LightfallProps {\n className?: string;\n dpr?: number;\n paused?: boolean;\n colors?: string[];\n backgroundColor?: string;\n speed?: number;\n streakCount?: number;\n streakWidth?: number;\n streakLength?: number;\n glow?: number;\n density?: number;\n twinkle?: number;\n zoom?: number;\n backgroundGlow?: number;\n opacity?: number;\n mouseInteraction?: boolean;\n mouseStrength?: number;\n mouseRadius?: number;\n mouseDampening?: number;\n mixBlendMode?: string;\n}\n\ntype RGB = [number, number, number];\n\nconst MAX_COLORS = 8;\n\nconst hexToRGB = (hex: string): RGB => {\n const c = hex.replace('#', '').padEnd(6, '0');\n const r = parseInt(c.slice(0, 2), 16) / 255;\n const g = parseInt(c.slice(2, 4), 16) / 255;\n const b = parseInt(c.slice(4, 6), 16) / 255;\n return [r, g, b];\n};\n\nconst prepColors = (input?: string[]) => {\n const base = (input && input.length ? input : ['#A6C8FF', '#5227FF', '#FF9FFC']).slice(0, MAX_COLORS);\n const count = base.length;\n const arr: RGB[] = [];\n for (let i = 0; i < MAX_COLORS; i++) arr.push(hexToRGB(base[Math.min(i, base.length - 1)]));\n const avg: RGB = [0, 0, 0];\n for (let i = 0; i < count; i++) {\n avg[0] += arr[i][0];\n avg[1] += arr[i][1];\n avg[2] += arr[i][2];\n }\n avg[0] /= count;\n avg[1] /= count;\n avg[2] /= count;\n return { arr, count, avg };\n};\n\nconst vertex = `\nattribute vec2 position;\nattribute vec2 uv;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `\nprecision highp float;\n\nuniform vec3 iResolution;\nuniform vec2 iMouse;\nuniform float iTime;\n\nuniform vec3 uColor0;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nuniform vec3 uColor4;\nuniform vec3 uColor5;\nuniform vec3 uColor6;\nuniform vec3 uColor7;\nuniform int uColorCount;\n\nuniform vec3 uBgColor;\nuniform vec3 uMouseColor;\nuniform float uSpeed;\nuniform int uStreakCount;\nuniform float uStreakWidth;\nuniform float uStreakLength;\nuniform float uGlow;\nuniform float uDensity;\nuniform float uTwinkle;\nuniform float uZoom;\nuniform float uBgGlow;\nuniform float uOpacity;\nuniform float uMouseEnabled;\nuniform float uMouseStrength;\nuniform float uMouseRadius;\n\nvarying vec2 vUv;\n\nvec3 palette(float h) {\n int count = uColorCount;\n if (count < 1) count = 1;\n int idx = int(floor(clamp(h, 0.0, 0.999999) * float(count)));\n if (idx <= 0) return uColor0;\n if (idx == 1) return uColor1;\n if (idx == 2) return uColor2;\n if (idx == 3) return uColor3;\n if (idx == 4) return uColor4;\n if (idx == 5) return uColor5;\n if (idx == 6) return uColor6;\n return uColor7;\n}\n\nvec3 tanhv(vec3 x) {\n vec3 e = exp(-2.0 * x);\n return (1.0 - e) / (1.0 + e);\n}\n\nvec2 sceneC(vec2 frag, vec2 r) {\n vec2 P = (frag + frag - r) / r.x;\n float z = 0.0;\n float d = 1e3;\n vec4 O = vec4(0.0);\n for (int k = 0; k < 39; k++) {\n if (d <= 1e-4) break;\n O = z * normalize(vec4(P, uZoom, 0.0)) - vec4(0.0, 4.0, 1.0, 0.0) / 4.5;\n d = 1.0 - sqrt(length(O * O));\n z += d;\n }\n return vec2(O.x, atan(O.z, O.y));\n}\n\nvoid mainImage(out vec4 o, vec2 C) {\n vec2 r = iResolution.xy;\n vec2 uv0 = (C + C - r) / r.x;\n float T = 0.1 * iTime * uSpeed + 9.0;\n float angRings = max(1.0, floor(6.28318530718 * max(uDensity, 0.05) + 0.5));\n vec2 Y = vec2(5e-3, 6.28318530718 / angRings);\n\n vec2 c0 = sceneC(C, r);\n vec2 cdx = sceneC(C + vec2(1.0, 0.0), r);\n vec2 cdy = sceneC(C + vec2(0.0, 1.0), r);\n vec2 dCx = cdx - c0;\n vec2 dCy = cdy - c0;\n dCx.y -= 6.28318530718 * floor(dCx.y / 6.28318530718 + 0.5);\n dCy.y -= 6.28318530718 * floor(dCy.y / 6.28318530718 + 0.5);\n vec2 fw = abs(dCx) + abs(dCy);\n C = c0;\n\n vec2 P = vec2(2.0, 1.0) * uv0 - (r / r.x) * vec2(0.0, 1.0);\n vec4 O = vec4(uBgColor * 90.0 * uBgGlow / (1e3 * dot(P, P) + 6.0), 0.0);\n\n float mGlow = 0.0;\n if (uMouseEnabled > 0.5) {\n vec2 mN = (iMouse + iMouse - r) / r.x;\n float md = length(uv0 - mN);\n mGlow = exp(-md * md / max(uMouseRadius * uMouseRadius, 1e-4)) * uMouseStrength;\n O.rgb += uMouseColor * mGlow * 0.25;\n }\n\n float zr = 5e-4 * uStreakWidth;\n vec2 rr = vec2(max(length(fw), 1e-5));\n float tail = 19.0 / max(uStreakLength, 0.05);\n\n for (int m = 0; m < 16; m++) {\n if (m >= uStreakCount) break;\n float jf = float(m) + 1.0;\n float ic = fract(sin(dot(vec2(jf, floor(C.x / Y.x + 0.5)), vec2(7.0, 11.0)) * 73.0));\n vec2 Pp = C - (T + T * ic) * vec2(0.0, 1.0);\n Pp -= floor(Pp / Y + 0.5) * Y;\n float h = fract(8663.0 * ic);\n vec3 col = palette(h);\n float weight = mix(1.5, 1.0 + sin(T + 7.0 * h + 4.0), uTwinkle);\n weight *= (1.0 + mGlow * 2.0);\n vec2 inner = vec2(length(max(Pp, vec2(-1.0, 0.0))), length(Pp) - zr) - zr;\n vec2 sm = vec2(1.0) - smoothstep(-rr, rr, inner);\n O.rgb += dot(sm, vec2(exp(tail * Pp.y), 3.0)) * col * weight;\n C.x += Y.x / 8.0;\n }\n\n vec3 colr = sqrt(tanhv(max(O.rgb * uGlow - vec3(0.04, 0.08, 0.02), 0.0)));\n o = vec4(colr, uOpacity);\n}\n\nvoid main() {\n vec4 color;\n mainImage(color, vUv * iResolution.xy);\n gl_FragColor = color;\n}\n`;\n\nconst Lightfall: React.FC = ({\n className,\n dpr,\n paused = false,\n colors = ['#A6C8FF', '#5227FF', '#FF9FFC'],\n backgroundColor = '#0A29FF',\n speed = 0.5,\n streakCount = 2,\n streakWidth = 1,\n streakLength = 1,\n glow = 1,\n density = 0.6,\n twinkle = 1,\n zoom = 3,\n backgroundGlow = 0.5,\n opacity = 1,\n mouseInteraction = true,\n mouseStrength = 0.5,\n mouseRadius = 1,\n mouseDampening = 0.15,\n mixBlendMode\n}) => {\n const containerRef = useRef(null);\n const rafRef = useRef(null);\n const programRef = useRef(null);\n const meshRef = useRef(null);\n const geometryRef = useRef(null);\n const rendererRef = useRef(null);\n const mouseTargetRef = useRef<[number, number]>([0, 0]);\n const lastTimeRef = useRef(0);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n dpr: dpr ?? (typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1),\n alpha: true,\n antialias: true\n });\n rendererRef.current = renderer;\n const gl = renderer.gl;\n const canvas = gl.canvas as HTMLCanvasElement;\n\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const { arr, count, avg } = prepColors(colors);\n\n const uniforms = {\n iResolution: { value: [gl.drawingBufferWidth, gl.drawingBufferHeight, 1] },\n iMouse: { value: [0, 0] },\n iTime: { value: 0 },\n uColor0: { value: arr[0] },\n uColor1: { value: arr[1] },\n uColor2: { value: arr[2] },\n uColor3: { value: arr[3] },\n uColor4: { value: arr[4] },\n uColor5: { value: arr[5] },\n uColor6: { value: arr[6] },\n uColor7: { value: arr[7] },\n uColorCount: { value: count },\n uBgColor: { value: hexToRGB(backgroundColor) },\n uMouseColor: { value: avg },\n uSpeed: { value: speed },\n uStreakCount: { value: Math.max(1, Math.min(16, Math.round(streakCount))) },\n uStreakWidth: { value: streakWidth },\n uStreakLength: { value: streakLength },\n uGlow: { value: glow },\n uDensity: { value: density },\n uTwinkle: { value: twinkle },\n uZoom: { value: zoom },\n uBgGlow: { value: backgroundGlow },\n uOpacity: { value: opacity },\n uMouseEnabled: { value: mouseInteraction ? 1 : 0 },\n uMouseStrength: { value: mouseStrength },\n uMouseRadius: { value: mouseRadius }\n };\n\n const program = new Program(gl, { vertex, fragment, uniforms });\n programRef.current = program;\n\n const geometry = new Triangle(gl);\n geometryRef.current = geometry;\n const mesh = new Mesh(gl, { geometry, program });\n meshRef.current = mesh;\n\n const resize = () => {\n const rect = container.getBoundingClientRect();\n renderer.setSize(rect.width, rect.height);\n uniforms.iResolution.value = [gl.drawingBufferWidth, gl.drawingBufferHeight, 1];\n };\n\n resize();\n const ro = new ResizeObserver(resize);\n ro.observe(container);\n\n const onPointerMove = (e: PointerEvent) => {\n const rect = canvas.getBoundingClientRect();\n const scale = renderer.dpr || 1;\n const x = (e.clientX - rect.left) * scale;\n const y = (rect.height - (e.clientY - rect.top)) * scale;\n mouseTargetRef.current = [x, y];\n if (mouseDampening <= 0) {\n uniforms.iMouse.value = [x, y];\n }\n };\n if (mouseInteraction) {\n canvas.addEventListener('pointermove', onPointerMove);\n }\n\n const loop = (t: number) => {\n rafRef.current = requestAnimationFrame(loop);\n uniforms.iTime.value = t * 0.001;\n if (mouseDampening > 0) {\n if (!lastTimeRef.current) lastTimeRef.current = t;\n const dt = (t - lastTimeRef.current) / 1000;\n lastTimeRef.current = t;\n const tau = Math.max(1e-4, mouseDampening);\n let factor = 1 - Math.exp(-dt / tau);\n if (factor > 1) factor = 1;\n const target = mouseTargetRef.current;\n const cur = uniforms.iMouse.value as number[];\n cur[0] += (target[0] - cur[0]) * factor;\n cur[1] += (target[1] - cur[1]) * factor;\n } else {\n lastTimeRef.current = t;\n }\n if (!paused && programRef.current && meshRef.current) {\n try {\n renderer.render({ scene: meshRef.current });\n } catch (e) {\n console.error(e);\n }\n }\n };\n rafRef.current = requestAnimationFrame(loop);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n if (mouseInteraction) canvas.removeEventListener('pointermove', onPointerMove);\n ro.disconnect();\n if (canvas.parentElement === container) {\n container.removeChild(canvas);\n }\n const callIfFn = (obj: unknown, key: string) => {\n const fn = obj && (obj as Record)[key];\n if (typeof fn === 'function') {\n (fn as () => void).call(obj);\n }\n };\n callIfFn(programRef.current, 'remove');\n callIfFn(geometryRef.current, 'remove');\n callIfFn(meshRef.current, 'remove');\n callIfFn(rendererRef.current, 'destroy');\n programRef.current = null;\n geometryRef.current = null;\n meshRef.current = null;\n rendererRef.current = null;\n };\n }, [\n dpr,\n paused,\n colors,\n backgroundColor,\n speed,\n streakCount,\n streakWidth,\n streakLength,\n glow,\n density,\n twinkle,\n zoom,\n backgroundGlow,\n opacity,\n mouseInteraction,\n mouseStrength,\n mouseRadius,\n mouseDampening\n ]);\n\n return (\n \n );\n};\n\nexport default Lightfall;\n" } ], diff --git a/public/r/Lightning-JS-CSS.json b/public/r/Lightning-JS-CSS.json index b96123d8..bbe92a86 100644 --- a/public/r/Lightning-JS-CSS.json +++ b/public/r/Lightning-JS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Lightning/Lightning.css", + "type": "registry:file", + "path": "Lightning.css", + "target": "@components/Lightning.css", "content": ".lightning-container {\n width: 100%;\n height: 100%;\n position: relative;\n}\n" }, { "type": "registry:component", - "path": "Lightning/Lightning.jsx", - "content": "import { useRef, useEffect } from 'react';\nimport './Lightning.css';\n\nconst Lightning = ({ hue = 230, xOffset = 0, speed = 1, intensity = 1, size = 1 }) => {\n const canvasRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const resizeCanvas = () => {\n canvas.width = canvas.clientWidth;\n canvas.height = canvas.clientHeight;\n };\n resizeCanvas();\n window.addEventListener('resize', resizeCanvas);\n\n const gl = canvas.getContext('webgl', { alpha: true, premultipliedAlpha: false });\n if (!gl) {\n console.error('WebGL not supported');\n return;\n }\n\n const vertexShaderSource = `\n attribute vec2 aPosition;\n void main() {\n gl_Position = vec4(aPosition, 0.0, 1.0);\n }\n `;\n\n const fragmentShaderSource = `\n precision mediump float;\n uniform vec2 iResolution;\n uniform float iTime;\n uniform float uHue;\n uniform float uXOffset;\n uniform float uSpeed;\n uniform float uIntensity;\n uniform float uSize;\n \n #define OCTAVE_COUNT 10\n\n vec3 hsv2rgb(vec3 c) {\n vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0,4.0,2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);\n return c.z * mix(vec3(1.0), rgb, c.y);\n }\n\n float hash11(float p) {\n p = fract(p * .1031);\n p *= p + 33.33;\n p *= p + p;\n return fract(p);\n }\n\n float hash12(vec2 p) {\n vec3 p3 = fract(vec3(p.xyx) * .1031);\n p3 += dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n }\n\n mat2 rotate2d(float theta) {\n float c = cos(theta);\n float s = sin(theta);\n return mat2(c, -s, s, c);\n }\n\n float noise(vec2 p) {\n vec2 ip = floor(p);\n vec2 fp = fract(p);\n float a = hash12(ip);\n float b = hash12(ip + vec2(1.0, 0.0));\n float c = hash12(ip + vec2(0.0, 1.0));\n float d = hash12(ip + vec2(1.0, 1.0));\n \n vec2 t = smoothstep(0.0, 1.0, fp);\n return mix(mix(a, b, t.x), mix(c, d, t.x), t.y);\n }\n\n float fbm(vec2 p) {\n float value = 0.0;\n float amplitude = 0.5;\n for (int i = 0; i < OCTAVE_COUNT; ++i) {\n value += amplitude * noise(p);\n p *= rotate2d(0.45);\n p *= 2.0;\n amplitude *= 0.5;\n }\n return value;\n }\n\n void mainImage( out vec4 fragColor, in vec2 fragCoord ) {\n vec2 uv = fragCoord / iResolution.xy;\n uv = 2.0 * uv - 1.0;\n uv.x *= iResolution.x / iResolution.y;\n uv.x += uXOffset;\n \n uv += 2.0 * fbm(uv * uSize + 0.8 * iTime * uSpeed) - 1.0;\n \n float dist = abs(uv.x);\n vec3 baseColor = hsv2rgb(vec3(uHue / 360.0, 0.7, 0.8));\n vec3 col = baseColor * pow(mix(0.0, 0.07, hash11(iTime * uSpeed)) / dist, 1.0) * uIntensity;\n col = pow(col, vec3(1.0));\n float a = clamp(max(col.r, max(col.g, col.b)), 0.0, 1.0);\n fragColor = vec4(col, a);\n }\n\n void main() {\n mainImage(gl_FragColor, gl_FragCoord.xy);\n }\n `;\n\n const compileShader = (source, type) => {\n const shader = gl.createShader(type);\n if (!shader) return null;\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n console.error('Shader compile error:', gl.getShaderInfoLog(shader));\n gl.deleteShader(shader);\n return null;\n }\n return shader;\n };\n\n const vertexShader = compileShader(vertexShaderSource, gl.VERTEX_SHADER);\n const fragmentShader = compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER);\n if (!vertexShader || !fragmentShader) return;\n\n const program = gl.createProgram();\n if (!program) return;\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.linkProgram(program);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n console.error('Program linking error:', gl.getProgramInfoLog(program));\n return;\n }\n gl.useProgram(program);\n\n const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]);\n const vertexBuffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);\n gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);\n\n const aPosition = gl.getAttribLocation(program, 'aPosition');\n gl.enableVertexAttribArray(aPosition);\n gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);\n\n const iResolutionLocation = gl.getUniformLocation(program, 'iResolution');\n const iTimeLocation = gl.getUniformLocation(program, 'iTime');\n const uHueLocation = gl.getUniformLocation(program, 'uHue');\n const uXOffsetLocation = gl.getUniformLocation(program, 'uXOffset');\n const uSpeedLocation = gl.getUniformLocation(program, 'uSpeed');\n const uIntensityLocation = gl.getUniformLocation(program, 'uIntensity');\n const uSizeLocation = gl.getUniformLocation(program, 'uSize');\n\n const startTime = performance.now();\n const render = () => {\n resizeCanvas();\n gl.viewport(0, 0, canvas.width, canvas.height);\n gl.uniform2f(iResolutionLocation, canvas.width, canvas.height);\n const currentTime = performance.now();\n gl.uniform1f(iTimeLocation, (currentTime - startTime) / 1000.0);\n gl.uniform1f(uHueLocation, hue);\n gl.uniform1f(uXOffsetLocation, xOffset);\n gl.uniform1f(uSpeedLocation, speed);\n gl.uniform1f(uIntensityLocation, intensity);\n gl.uniform1f(uSizeLocation, size);\n gl.drawArrays(gl.TRIANGLES, 0, 6);\n requestAnimationFrame(render);\n };\n requestAnimationFrame(render);\n\n return () => {\n window.removeEventListener('resize', resizeCanvas);\n };\n }, [hue, xOffset, speed, intensity, size]);\n\n return ;\n};\n\nexport default Lightning;\n" + "path": "Lightning.jsx", + "content": "import { useRef, useEffect } from 'react';\nimport './Lightning.css';\n\nconst Lightning = ({ hue = 230, xOffset = 0, speed = 1, intensity = 1, size = 1 }) => {\n const canvasRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const resizeCanvas = () => {\n canvas.width = canvas.clientWidth;\n canvas.height = canvas.clientHeight;\n };\n resizeCanvas();\n window.addEventListener('resize', resizeCanvas);\n\n const gl = canvas.getContext('webgl', { alpha: true, premultipliedAlpha: false });\n if (!gl) {\n console.error('WebGL not supported');\n return;\n }\n\n const vertexShaderSource = `\n attribute vec2 aPosition;\n void main() {\n gl_Position = vec4(aPosition, 0.0, 1.0);\n }\n `;\n\n const fragmentShaderSource = `\n precision mediump float;\n uniform vec2 iResolution;\n uniform float iTime;\n uniform float uHue;\n uniform float uXOffset;\n uniform float uSpeed;\n uniform float uIntensity;\n uniform float uSize;\n \n #define OCTAVE_COUNT 10\n\n vec3 hsv2rgb(vec3 c) {\n vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0,4.0,2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);\n return c.z * mix(vec3(1.0), rgb, c.y);\n }\n\n float hash11(float p) {\n p = fract(p * .1031);\n p *= p + 33.33;\n p *= p + p;\n return fract(p);\n }\n\n float hash12(vec2 p) {\n vec3 p3 = fract(vec3(p.xyx) * .1031);\n p3 += dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n }\n\n mat2 rotate2d(float theta) {\n float c = cos(theta);\n float s = sin(theta);\n return mat2(c, -s, s, c);\n }\n\n float noise(vec2 p) {\n vec2 ip = floor(p);\n vec2 fp = fract(p);\n float a = hash12(ip);\n float b = hash12(ip + vec2(1.0, 0.0));\n float c = hash12(ip + vec2(0.0, 1.0));\n float d = hash12(ip + vec2(1.0, 1.0));\n \n vec2 t = smoothstep(0.0, 1.0, fp);\n return mix(mix(a, b, t.x), mix(c, d, t.x), t.y);\n }\n\n float fbm(vec2 p) {\n float value = 0.0;\n float amplitude = 0.5;\n for (int i = 0; i < OCTAVE_COUNT; ++i) {\n value += amplitude * noise(p);\n p *= rotate2d(0.45);\n p *= 2.0;\n amplitude *= 0.5;\n }\n return value;\n }\n\n void mainImage( out vec4 fragColor, in vec2 fragCoord ) {\n vec2 uv = fragCoord / iResolution.xy;\n uv = 2.0 * uv - 1.0;\n uv.x *= iResolution.x / iResolution.y;\n uv.x += uXOffset;\n \n uv += 2.0 * fbm(uv * uSize + 0.8 * iTime * uSpeed) - 1.0;\n \n float dist = abs(uv.x);\n vec3 baseColor = hsv2rgb(vec3(uHue / 360.0, 0.7, 0.8));\n vec3 col = baseColor * pow(mix(0.0, 0.07, hash11(iTime * uSpeed)) / dist, 1.0) * uIntensity;\n col = pow(col, vec3(1.0));\n float a = clamp(max(col.r, max(col.g, col.b)), 0.0, 1.0);\n fragColor = vec4(col, a);\n }\n\n void main() {\n mainImage(gl_FragColor, gl_FragCoord.xy);\n }\n `;\n\n const compileShader = (source, type) => {\n const shader = gl.createShader(type);\n if (!shader) return null;\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n console.error('Shader compile error:', gl.getShaderInfoLog(shader));\n gl.deleteShader(shader);\n return null;\n }\n return shader;\n };\n\n const vertexShader = compileShader(vertexShaderSource, gl.VERTEX_SHADER);\n const fragmentShader = compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER);\n if (!vertexShader || !fragmentShader) return;\n\n const program = gl.createProgram();\n if (!program) return;\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.linkProgram(program);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n console.error('Program linking error:', gl.getProgramInfoLog(program));\n return;\n }\n gl.useProgram(program);\n\n const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]);\n const vertexBuffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);\n gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);\n\n const aPosition = gl.getAttribLocation(program, 'aPosition');\n gl.enableVertexAttribArray(aPosition);\n gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);\n\n const iResolutionLocation = gl.getUniformLocation(program, 'iResolution');\n const iTimeLocation = gl.getUniformLocation(program, 'iTime');\n const uHueLocation = gl.getUniformLocation(program, 'uHue');\n const uXOffsetLocation = gl.getUniformLocation(program, 'uXOffset');\n const uSpeedLocation = gl.getUniformLocation(program, 'uSpeed');\n const uIntensityLocation = gl.getUniformLocation(program, 'uIntensity');\n const uSizeLocation = gl.getUniformLocation(program, 'uSize');\n\n let animationFrameId;\n const startTime = performance.now();\n const render = () => {\n resizeCanvas();\n gl.viewport(0, 0, canvas.width, canvas.height);\n gl.uniform2f(iResolutionLocation, canvas.width, canvas.height);\n const currentTime = performance.now();\n gl.uniform1f(iTimeLocation, (currentTime - startTime) / 1000.0);\n gl.uniform1f(uHueLocation, hue);\n gl.uniform1f(uXOffsetLocation, xOffset);\n gl.uniform1f(uSpeedLocation, speed);\n gl.uniform1f(uIntensityLocation, intensity);\n gl.uniform1f(uSizeLocation, size);\n gl.drawArrays(gl.TRIANGLES, 0, 6);\n animationFrameId = requestAnimationFrame(render);\n };\n animationFrameId = requestAnimationFrame(render);\n\n return () => {\n cancelAnimationFrame(animationFrameId);\n window.removeEventListener('resize', resizeCanvas);\n };\n }, [hue, xOffset, speed, intensity, size]);\n\n return ;\n};\n\nexport default Lightning;\n" } ], "registryDependencies": [], diff --git a/public/r/Lightning-JS-TW.json b/public/r/Lightning-JS-TW.json index dcdbf4f5..161fa0a3 100644 --- a/public/r/Lightning-JS-TW.json +++ b/public/r/Lightning-JS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "Lightning/Lightning.jsx", - "content": "import { useRef, useEffect } from 'react';\n\nconst Lightning = ({ hue = 230, xOffset = 0, speed = 1, intensity = 1, size = 1 }) => {\n const canvasRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const resizeCanvas = () => {\n canvas.width = canvas.clientWidth;\n canvas.height = canvas.clientHeight;\n };\n resizeCanvas();\n window.addEventListener('resize', resizeCanvas);\n\n const gl = canvas.getContext('webgl', { alpha: true, premultipliedAlpha: false });\n if (!gl) {\n console.error('WebGL not supported');\n return;\n }\n\n const vertexShaderSource = `\n attribute vec2 aPosition;\n void main() {\n gl_Position = vec4(aPosition, 0.0, 1.0);\n }\n `;\n\n const fragmentShaderSource = `\n precision mediump float;\n uniform vec2 iResolution;\n uniform float iTime;\n uniform float uHue;\n uniform float uXOffset;\n uniform float uSpeed;\n uniform float uIntensity;\n uniform float uSize;\n \n #define OCTAVE_COUNT 10\n\n vec3 hsv2rgb(vec3 c) {\n vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0,4.0,2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);\n return c.z * mix(vec3(1.0), rgb, c.y);\n }\n\n float hash11(float p) {\n p = fract(p * .1031);\n p *= p + 33.33;\n p *= p + p;\n return fract(p);\n }\n\n float hash12(vec2 p) {\n vec3 p3 = fract(vec3(p.xyx) * .1031);\n p3 += dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n }\n\n mat2 rotate2d(float theta) {\n float c = cos(theta);\n float s = sin(theta);\n return mat2(c, -s, s, c);\n }\n\n float noise(vec2 p) {\n vec2 ip = floor(p);\n vec2 fp = fract(p);\n float a = hash12(ip);\n float b = hash12(ip + vec2(1.0, 0.0));\n float c = hash12(ip + vec2(0.0, 1.0));\n float d = hash12(ip + vec2(1.0, 1.0));\n \n vec2 t = smoothstep(0.0, 1.0, fp);\n return mix(mix(a, b, t.x), mix(c, d, t.x), t.y);\n }\n\n float fbm(vec2 p) {\n float value = 0.0;\n float amplitude = 0.5;\n for (int i = 0; i < OCTAVE_COUNT; ++i) {\n value += amplitude * noise(p);\n p *= rotate2d(0.45);\n p *= 2.0;\n amplitude *= 0.5;\n }\n return value;\n }\n\n void mainImage( out vec4 fragColor, in vec2 fragCoord ) {\n vec2 uv = fragCoord / iResolution.xy;\n uv = 2.0 * uv - 1.0;\n uv.x *= iResolution.x / iResolution.y;\n uv.x += uXOffset;\n \n uv += 2.0 * fbm(uv * uSize + 0.8 * iTime * uSpeed) - 1.0;\n \n float dist = abs(uv.x);\n vec3 baseColor = hsv2rgb(vec3(uHue / 360.0, 0.7, 0.8));\n vec3 col = baseColor * pow(mix(0.0, 0.07, hash11(iTime * uSpeed)) / dist, 1.0) * uIntensity;\n col = pow(col, vec3(1.0));\n float a = clamp(max(col.r, max(col.g, col.b)), 0.0, 1.0);\n fragColor = vec4(col, a);\n }\n\n void main() {\n mainImage(gl_FragColor, gl_FragCoord.xy);\n }\n `;\n\n const compileShader = (source, type) => {\n const shader = gl.createShader(type);\n if (!shader) return null;\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n console.error('Shader compile error:', gl.getShaderInfoLog(shader));\n gl.deleteShader(shader);\n return null;\n }\n return shader;\n };\n\n const vertexShader = compileShader(vertexShaderSource, gl.VERTEX_SHADER);\n const fragmentShader = compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER);\n if (!vertexShader || !fragmentShader) return;\n\n const program = gl.createProgram();\n if (!program) return;\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.linkProgram(program);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n console.error('Program linking error:', gl.getProgramInfoLog(program));\n return;\n }\n gl.useProgram(program);\n\n const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]);\n const vertexBuffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);\n gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);\n\n const aPosition = gl.getAttribLocation(program, 'aPosition');\n gl.enableVertexAttribArray(aPosition);\n gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);\n\n const iResolutionLocation = gl.getUniformLocation(program, 'iResolution');\n const iTimeLocation = gl.getUniformLocation(program, 'iTime');\n const uHueLocation = gl.getUniformLocation(program, 'uHue');\n const uXOffsetLocation = gl.getUniformLocation(program, 'uXOffset');\n const uSpeedLocation = gl.getUniformLocation(program, 'uSpeed');\n const uIntensityLocation = gl.getUniformLocation(program, 'uIntensity');\n const uSizeLocation = gl.getUniformLocation(program, 'uSize');\n\n const startTime = performance.now();\n const render = () => {\n resizeCanvas();\n gl.viewport(0, 0, canvas.width, canvas.height);\n gl.uniform2f(iResolutionLocation, canvas.width, canvas.height);\n const currentTime = performance.now();\n gl.uniform1f(iTimeLocation, (currentTime - startTime) / 1000.0);\n gl.uniform1f(uHueLocation, hue);\n gl.uniform1f(uXOffsetLocation, xOffset);\n gl.uniform1f(uSpeedLocation, speed);\n gl.uniform1f(uIntensityLocation, intensity);\n gl.uniform1f(uSizeLocation, size);\n gl.drawArrays(gl.TRIANGLES, 0, 6);\n requestAnimationFrame(render);\n };\n requestAnimationFrame(render);\n\n return () => {\n window.removeEventListener('resize', resizeCanvas);\n };\n }, [hue, xOffset, speed, intensity, size]);\n\n return ;\n};\n\nexport default Lightning;\n" + "content": "import { useRef, useEffect } from 'react';\n\nconst Lightning = ({ hue = 230, xOffset = 0, speed = 1, intensity = 1, size = 1 }) => {\n const canvasRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const resizeCanvas = () => {\n canvas.width = canvas.clientWidth;\n canvas.height = canvas.clientHeight;\n };\n resizeCanvas();\n window.addEventListener('resize', resizeCanvas);\n\n const gl = canvas.getContext('webgl', { alpha: true, premultipliedAlpha: false });\n if (!gl) {\n console.error('WebGL not supported');\n return;\n }\n\n const vertexShaderSource = `\n attribute vec2 aPosition;\n void main() {\n gl_Position = vec4(aPosition, 0.0, 1.0);\n }\n `;\n\n const fragmentShaderSource = `\n precision mediump float;\n uniform vec2 iResolution;\n uniform float iTime;\n uniform float uHue;\n uniform float uXOffset;\n uniform float uSpeed;\n uniform float uIntensity;\n uniform float uSize;\n \n #define OCTAVE_COUNT 10\n\n vec3 hsv2rgb(vec3 c) {\n vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0,4.0,2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);\n return c.z * mix(vec3(1.0), rgb, c.y);\n }\n\n float hash11(float p) {\n p = fract(p * .1031);\n p *= p + 33.33;\n p *= p + p;\n return fract(p);\n }\n\n float hash12(vec2 p) {\n vec3 p3 = fract(vec3(p.xyx) * .1031);\n p3 += dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n }\n\n mat2 rotate2d(float theta) {\n float c = cos(theta);\n float s = sin(theta);\n return mat2(c, -s, s, c);\n }\n\n float noise(vec2 p) {\n vec2 ip = floor(p);\n vec2 fp = fract(p);\n float a = hash12(ip);\n float b = hash12(ip + vec2(1.0, 0.0));\n float c = hash12(ip + vec2(0.0, 1.0));\n float d = hash12(ip + vec2(1.0, 1.0));\n \n vec2 t = smoothstep(0.0, 1.0, fp);\n return mix(mix(a, b, t.x), mix(c, d, t.x), t.y);\n }\n\n float fbm(vec2 p) {\n float value = 0.0;\n float amplitude = 0.5;\n for (int i = 0; i < OCTAVE_COUNT; ++i) {\n value += amplitude * noise(p);\n p *= rotate2d(0.45);\n p *= 2.0;\n amplitude *= 0.5;\n }\n return value;\n }\n\n void mainImage( out vec4 fragColor, in vec2 fragCoord ) {\n vec2 uv = fragCoord / iResolution.xy;\n uv = 2.0 * uv - 1.0;\n uv.x *= iResolution.x / iResolution.y;\n uv.x += uXOffset;\n \n uv += 2.0 * fbm(uv * uSize + 0.8 * iTime * uSpeed) - 1.0;\n \n float dist = abs(uv.x);\n vec3 baseColor = hsv2rgb(vec3(uHue / 360.0, 0.7, 0.8));\n vec3 col = baseColor * pow(mix(0.0, 0.07, hash11(iTime * uSpeed)) / dist, 1.0) * uIntensity;\n col = pow(col, vec3(1.0));\n float a = clamp(max(col.r, max(col.g, col.b)), 0.0, 1.0);\n fragColor = vec4(col, a);\n }\n\n void main() {\n mainImage(gl_FragColor, gl_FragCoord.xy);\n }\n `;\n\n const compileShader = (source, type) => {\n const shader = gl.createShader(type);\n if (!shader) return null;\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n console.error('Shader compile error:', gl.getShaderInfoLog(shader));\n gl.deleteShader(shader);\n return null;\n }\n return shader;\n };\n\n const vertexShader = compileShader(vertexShaderSource, gl.VERTEX_SHADER);\n const fragmentShader = compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER);\n if (!vertexShader || !fragmentShader) return;\n\n const program = gl.createProgram();\n if (!program) return;\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.linkProgram(program);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n console.error('Program linking error:', gl.getProgramInfoLog(program));\n return;\n }\n gl.useProgram(program);\n\n const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]);\n const vertexBuffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);\n gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);\n\n const aPosition = gl.getAttribLocation(program, 'aPosition');\n gl.enableVertexAttribArray(aPosition);\n gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);\n\n const iResolutionLocation = gl.getUniformLocation(program, 'iResolution');\n const iTimeLocation = gl.getUniformLocation(program, 'iTime');\n const uHueLocation = gl.getUniformLocation(program, 'uHue');\n const uXOffsetLocation = gl.getUniformLocation(program, 'uXOffset');\n const uSpeedLocation = gl.getUniformLocation(program, 'uSpeed');\n const uIntensityLocation = gl.getUniformLocation(program, 'uIntensity');\n const uSizeLocation = gl.getUniformLocation(program, 'uSize');\n\n let animationFrameId;\n const startTime = performance.now();\n const render = () => {\n resizeCanvas();\n gl.viewport(0, 0, canvas.width, canvas.height);\n gl.uniform2f(iResolutionLocation, canvas.width, canvas.height);\n const currentTime = performance.now();\n gl.uniform1f(iTimeLocation, (currentTime - startTime) / 1000.0);\n gl.uniform1f(uHueLocation, hue);\n gl.uniform1f(uXOffsetLocation, xOffset);\n gl.uniform1f(uSpeedLocation, speed);\n gl.uniform1f(uIntensityLocation, intensity);\n gl.uniform1f(uSizeLocation, size);\n gl.drawArrays(gl.TRIANGLES, 0, 6);\n animationFrameId = requestAnimationFrame(render);\n };\n animationFrameId = requestAnimationFrame(render);\n\n return () => {\n cancelAnimationFrame(animationFrameId);\n window.removeEventListener('resize', resizeCanvas);\n };\n }, [hue, xOffset, speed, intensity, size]);\n\n return ;\n};\n\nexport default Lightning;\n" } ], "registryDependencies": [], diff --git a/public/r/Lightning-TS-CSS.json b/public/r/Lightning-TS-CSS.json index 009e38b5..8037dd13 100644 --- a/public/r/Lightning-TS-CSS.json +++ b/public/r/Lightning-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Lightning/Lightning.css", + "type": "registry:file", + "path": "Lightning.css", + "target": "@components/Lightning.css", "content": ".lightning-container {\n width: 100%;\n height: 100%;\n position: relative;\n}\n" }, { "type": "registry:component", - "path": "Lightning/Lightning.tsx", - "content": "import React, { useRef, useEffect } from 'react';\nimport './Lightning.css';\n\ninterface LightningProps {\n hue?: number;\n xOffset?: number;\n speed?: number;\n intensity?: number;\n size?: number;\n}\n\nconst Lightning: React.FC = ({ hue = 230, xOffset = 0, speed = 1, intensity = 1, size = 1 }) => {\n const canvasRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const resizeCanvas = () => {\n canvas.width = canvas.clientWidth;\n canvas.height = canvas.clientHeight;\n };\n resizeCanvas();\n window.addEventListener('resize', resizeCanvas);\n\n const gl = canvas.getContext('webgl', { alpha: true, premultipliedAlpha: false });\n if (!gl) {\n console.error('WebGL not supported');\n return;\n }\n\n const vertexShaderSource = `\n attribute vec2 aPosition;\n void main() {\n gl_Position = vec4(aPosition, 0.0, 1.0);\n }\n `;\n\n const fragmentShaderSource = `\n precision mediump float;\n uniform vec2 iResolution;\n uniform float iTime;\n uniform float uHue;\n uniform float uXOffset;\n uniform float uSpeed;\n uniform float uIntensity;\n uniform float uSize;\n \n #define OCTAVE_COUNT 10\n\n vec3 hsv2rgb(vec3 c) {\n vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0,4.0,2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);\n return c.z * mix(vec3(1.0), rgb, c.y);\n }\n\n float hash11(float p) {\n p = fract(p * .1031);\n p *= p + 33.33;\n p *= p + p;\n return fract(p);\n }\n\n float hash12(vec2 p) {\n vec3 p3 = fract(vec3(p.xyx) * .1031);\n p3 += dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n }\n\n mat2 rotate2d(float theta) {\n float c = cos(theta);\n float s = sin(theta);\n return mat2(c, -s, s, c);\n }\n\n float noise(vec2 p) {\n vec2 ip = floor(p);\n vec2 fp = fract(p);\n float a = hash12(ip);\n float b = hash12(ip + vec2(1.0, 0.0));\n float c = hash12(ip + vec2(0.0, 1.0));\n float d = hash12(ip + vec2(1.0, 1.0));\n \n vec2 t = smoothstep(0.0, 1.0, fp);\n return mix(mix(a, b, t.x), mix(c, d, t.x), t.y);\n }\n\n float fbm(vec2 p) {\n float value = 0.0;\n float amplitude = 0.5;\n for (int i = 0; i < OCTAVE_COUNT; ++i) {\n value += amplitude * noise(p);\n p *= rotate2d(0.45);\n p *= 2.0;\n amplitude *= 0.5;\n }\n return value;\n }\n\n void mainImage( out vec4 fragColor, in vec2 fragCoord ) {\n vec2 uv = fragCoord / iResolution.xy;\n uv = 2.0 * uv - 1.0;\n uv.x *= iResolution.x / iResolution.y;\n uv.x += uXOffset;\n \n uv += 2.0 * fbm(uv * uSize + 0.8 * iTime * uSpeed) - 1.0;\n \n float dist = abs(uv.x);\n vec3 baseColor = hsv2rgb(vec3(uHue / 360.0, 0.7, 0.8));\n vec3 col = baseColor * pow(mix(0.0, 0.07, hash11(iTime * uSpeed)) / dist, 1.0) * uIntensity;\n col = pow(col, vec3(1.0));\n float a = clamp(max(col.r, max(col.g, col.b)), 0.0, 1.0);\n fragColor = vec4(col, a);\n }\n\n void main() {\n mainImage(gl_FragColor, gl_FragCoord.xy);\n }\n `;\n\n const compileShader = (source: string, type: number): WebGLShader | null => {\n const shader = gl.createShader(type);\n if (!shader) return null;\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n console.error('Shader compile error:', gl.getShaderInfoLog(shader));\n gl.deleteShader(shader);\n return null;\n }\n return shader;\n };\n\n const vertexShader = compileShader(vertexShaderSource, gl.VERTEX_SHADER);\n const fragmentShader = compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER);\n if (!vertexShader || !fragmentShader) return;\n\n const program = gl.createProgram();\n if (!program) return;\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.linkProgram(program);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n console.error('Program linking error:', gl.getProgramInfoLog(program));\n return;\n }\n gl.useProgram(program);\n\n const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]);\n const vertexBuffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);\n gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);\n\n const aPosition = gl.getAttribLocation(program, 'aPosition');\n gl.enableVertexAttribArray(aPosition);\n gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);\n\n const iResolutionLocation = gl.getUniformLocation(program, 'iResolution');\n const iTimeLocation = gl.getUniformLocation(program, 'iTime');\n const uHueLocation = gl.getUniformLocation(program, 'uHue');\n const uXOffsetLocation = gl.getUniformLocation(program, 'uXOffset');\n const uSpeedLocation = gl.getUniformLocation(program, 'uSpeed');\n const uIntensityLocation = gl.getUniformLocation(program, 'uIntensity');\n const uSizeLocation = gl.getUniformLocation(program, 'uSize');\n\n const startTime = performance.now();\n const render = () => {\n resizeCanvas();\n gl.viewport(0, 0, canvas.width, canvas.height);\n gl.uniform2f(iResolutionLocation, canvas.width, canvas.height);\n const currentTime = performance.now();\n gl.uniform1f(iTimeLocation, (currentTime - startTime) / 1000.0);\n gl.uniform1f(uHueLocation, hue);\n gl.uniform1f(uXOffsetLocation, xOffset);\n gl.uniform1f(uSpeedLocation, speed);\n gl.uniform1f(uIntensityLocation, intensity);\n gl.uniform1f(uSizeLocation, size);\n gl.drawArrays(gl.TRIANGLES, 0, 6);\n requestAnimationFrame(render);\n };\n requestAnimationFrame(render);\n\n return () => {\n window.removeEventListener('resize', resizeCanvas);\n };\n }, [hue, xOffset, speed, intensity, size]);\n\n return ;\n};\n\nexport default Lightning;\n" + "path": "Lightning.tsx", + "content": "import React, { useRef, useEffect } from 'react';\nimport './Lightning.css';\n\ninterface LightningProps {\n hue?: number;\n xOffset?: number;\n speed?: number;\n intensity?: number;\n size?: number;\n}\n\nconst Lightning: React.FC = ({ hue = 230, xOffset = 0, speed = 1, intensity = 1, size = 1 }) => {\n const canvasRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const resizeCanvas = () => {\n canvas.width = canvas.clientWidth;\n canvas.height = canvas.clientHeight;\n };\n resizeCanvas();\n window.addEventListener('resize', resizeCanvas);\n\n const gl = canvas.getContext('webgl', { alpha: true, premultipliedAlpha: false });\n if (!gl) {\n console.error('WebGL not supported');\n return;\n }\n\n const vertexShaderSource = `\n attribute vec2 aPosition;\n void main() {\n gl_Position = vec4(aPosition, 0.0, 1.0);\n }\n `;\n\n const fragmentShaderSource = `\n precision mediump float;\n uniform vec2 iResolution;\n uniform float iTime;\n uniform float uHue;\n uniform float uXOffset;\n uniform float uSpeed;\n uniform float uIntensity;\n uniform float uSize;\n \n #define OCTAVE_COUNT 10\n\n vec3 hsv2rgb(vec3 c) {\n vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0,4.0,2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);\n return c.z * mix(vec3(1.0), rgb, c.y);\n }\n\n float hash11(float p) {\n p = fract(p * .1031);\n p *= p + 33.33;\n p *= p + p;\n return fract(p);\n }\n\n float hash12(vec2 p) {\n vec3 p3 = fract(vec3(p.xyx) * .1031);\n p3 += dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n }\n\n mat2 rotate2d(float theta) {\n float c = cos(theta);\n float s = sin(theta);\n return mat2(c, -s, s, c);\n }\n\n float noise(vec2 p) {\n vec2 ip = floor(p);\n vec2 fp = fract(p);\n float a = hash12(ip);\n float b = hash12(ip + vec2(1.0, 0.0));\n float c = hash12(ip + vec2(0.0, 1.0));\n float d = hash12(ip + vec2(1.0, 1.0));\n \n vec2 t = smoothstep(0.0, 1.0, fp);\n return mix(mix(a, b, t.x), mix(c, d, t.x), t.y);\n }\n\n float fbm(vec2 p) {\n float value = 0.0;\n float amplitude = 0.5;\n for (int i = 0; i < OCTAVE_COUNT; ++i) {\n value += amplitude * noise(p);\n p *= rotate2d(0.45);\n p *= 2.0;\n amplitude *= 0.5;\n }\n return value;\n }\n\n void mainImage( out vec4 fragColor, in vec2 fragCoord ) {\n vec2 uv = fragCoord / iResolution.xy;\n uv = 2.0 * uv - 1.0;\n uv.x *= iResolution.x / iResolution.y;\n uv.x += uXOffset;\n \n uv += 2.0 * fbm(uv * uSize + 0.8 * iTime * uSpeed) - 1.0;\n \n float dist = abs(uv.x);\n vec3 baseColor = hsv2rgb(vec3(uHue / 360.0, 0.7, 0.8));\n vec3 col = baseColor * pow(mix(0.0, 0.07, hash11(iTime * uSpeed)) / dist, 1.0) * uIntensity;\n col = pow(col, vec3(1.0));\n float a = clamp(max(col.r, max(col.g, col.b)), 0.0, 1.0);\n fragColor = vec4(col, a);\n }\n\n void main() {\n mainImage(gl_FragColor, gl_FragCoord.xy);\n }\n `;\n\n const compileShader = (source: string, type: number): WebGLShader | null => {\n const shader = gl.createShader(type);\n if (!shader) return null;\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n console.error('Shader compile error:', gl.getShaderInfoLog(shader));\n gl.deleteShader(shader);\n return null;\n }\n return shader;\n };\n\n const vertexShader = compileShader(vertexShaderSource, gl.VERTEX_SHADER);\n const fragmentShader = compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER);\n if (!vertexShader || !fragmentShader) return;\n\n const program = gl.createProgram();\n if (!program) return;\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.linkProgram(program);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n console.error('Program linking error:', gl.getProgramInfoLog(program));\n return;\n }\n gl.useProgram(program);\n\n const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]);\n const vertexBuffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);\n gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);\n\n const aPosition = gl.getAttribLocation(program, 'aPosition');\n gl.enableVertexAttribArray(aPosition);\n gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);\n\n const iResolutionLocation = gl.getUniformLocation(program, 'iResolution');\n const iTimeLocation = gl.getUniformLocation(program, 'iTime');\n const uHueLocation = gl.getUniformLocation(program, 'uHue');\n const uXOffsetLocation = gl.getUniformLocation(program, 'uXOffset');\n const uSpeedLocation = gl.getUniformLocation(program, 'uSpeed');\n const uIntensityLocation = gl.getUniformLocation(program, 'uIntensity');\n const uSizeLocation = gl.getUniformLocation(program, 'uSize');\n\n let animationFrameId: number;\n const startTime = performance.now();\n const render = () => {\n resizeCanvas();\n gl.viewport(0, 0, canvas.width, canvas.height);\n gl.uniform2f(iResolutionLocation, canvas.width, canvas.height);\n const currentTime = performance.now();\n gl.uniform1f(iTimeLocation, (currentTime - startTime) / 1000.0);\n gl.uniform1f(uHueLocation, hue);\n gl.uniform1f(uXOffsetLocation, xOffset);\n gl.uniform1f(uSpeedLocation, speed);\n gl.uniform1f(uIntensityLocation, intensity);\n gl.uniform1f(uSizeLocation, size);\n gl.drawArrays(gl.TRIANGLES, 0, 6);\n animationFrameId = requestAnimationFrame(render);\n };\n animationFrameId = requestAnimationFrame(render);\n\n return () => {\n cancelAnimationFrame(animationFrameId);\n window.removeEventListener('resize', resizeCanvas);\n };\n }, [hue, xOffset, speed, intensity, size]);\n\n return ;\n};\n\nexport default Lightning;\n" } ], "registryDependencies": [], diff --git a/public/r/Lightning-TS-TW.json b/public/r/Lightning-TS-TW.json index 91e58d45..9937cec6 100644 --- a/public/r/Lightning-TS-TW.json +++ b/public/r/Lightning-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "Lightning/Lightning.tsx", - "content": "import React, { useRef, useEffect } from 'react';\n\ninterface LightningProps {\n hue?: number;\n xOffset?: number;\n speed?: number;\n intensity?: number;\n size?: number;\n}\n\nconst Lightning: React.FC = ({ hue = 230, xOffset = 0, speed = 1, intensity = 1, size = 1 }) => {\n const canvasRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const resizeCanvas = () => {\n canvas.width = canvas.clientWidth;\n canvas.height = canvas.clientHeight;\n };\n resizeCanvas();\n window.addEventListener('resize', resizeCanvas);\n\n const gl = canvas.getContext('webgl', { alpha: true, premultipliedAlpha: false });\n if (!gl) {\n console.error('WebGL not supported');\n return;\n }\n\n const vertexShaderSource = `\n attribute vec2 aPosition;\n void main() {\n gl_Position = vec4(aPosition, 0.0, 1.0);\n }\n `;\n\n const fragmentShaderSource = `\n precision mediump float;\n uniform vec2 iResolution;\n uniform float iTime;\n uniform float uHue;\n uniform float uXOffset;\n uniform float uSpeed;\n uniform float uIntensity;\n uniform float uSize;\n \n #define OCTAVE_COUNT 10\n\n vec3 hsv2rgb(vec3 c) {\n vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0,4.0,2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);\n return c.z * mix(vec3(1.0), rgb, c.y);\n }\n\n float hash11(float p) {\n p = fract(p * .1031);\n p *= p + 33.33;\n p *= p + p;\n return fract(p);\n }\n\n float hash12(vec2 p) {\n vec3 p3 = fract(vec3(p.xyx) * .1031);\n p3 += dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n }\n\n mat2 rotate2d(float theta) {\n float c = cos(theta);\n float s = sin(theta);\n return mat2(c, -s, s, c);\n }\n\n float noise(vec2 p) {\n vec2 ip = floor(p);\n vec2 fp = fract(p);\n float a = hash12(ip);\n float b = hash12(ip + vec2(1.0, 0.0));\n float c = hash12(ip + vec2(0.0, 1.0));\n float d = hash12(ip + vec2(1.0, 1.0));\n \n vec2 t = smoothstep(0.0, 1.0, fp);\n return mix(mix(a, b, t.x), mix(c, d, t.x), t.y);\n }\n\n float fbm(vec2 p) {\n float value = 0.0;\n float amplitude = 0.5;\n for (int i = 0; i < OCTAVE_COUNT; ++i) {\n value += amplitude * noise(p);\n p *= rotate2d(0.45);\n p *= 2.0;\n amplitude *= 0.5;\n }\n return value;\n }\n\n void mainImage( out vec4 fragColor, in vec2 fragCoord ) {\n vec2 uv = fragCoord / iResolution.xy;\n uv = 2.0 * uv - 1.0;\n uv.x *= iResolution.x / iResolution.y;\n uv.x += uXOffset;\n \n uv += 2.0 * fbm(uv * uSize + 0.8 * iTime * uSpeed) - 1.0;\n \n float dist = abs(uv.x);\n vec3 baseColor = hsv2rgb(vec3(uHue / 360.0, 0.7, 0.8));\n vec3 col = baseColor * pow(mix(0.0, 0.07, hash11(iTime * uSpeed)) / dist, 1.0) * uIntensity;\n col = pow(col, vec3(1.0));\n float a = clamp(max(col.r, max(col.g, col.b)), 0.0, 1.0);\n fragColor = vec4(col, a);\n }\n\n void main() {\n mainImage(gl_FragColor, gl_FragCoord.xy);\n }\n `;\n\n const compileShader = (source: string, type: number): WebGLShader | null => {\n const shader = gl.createShader(type);\n if (!shader) return null;\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n console.error('Shader compile error:', gl.getShaderInfoLog(shader));\n gl.deleteShader(shader);\n return null;\n }\n return shader;\n };\n\n const vertexShader = compileShader(vertexShaderSource, gl.VERTEX_SHADER);\n const fragmentShader = compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER);\n if (!vertexShader || !fragmentShader) return;\n\n const program = gl.createProgram();\n if (!program) return;\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.linkProgram(program);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n console.error('Program linking error:', gl.getProgramInfoLog(program));\n return;\n }\n gl.useProgram(program);\n\n const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]);\n const vertexBuffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);\n gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);\n\n const aPosition = gl.getAttribLocation(program, 'aPosition');\n gl.enableVertexAttribArray(aPosition);\n gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);\n\n const iResolutionLocation = gl.getUniformLocation(program, 'iResolution');\n const iTimeLocation = gl.getUniformLocation(program, 'iTime');\n const uHueLocation = gl.getUniformLocation(program, 'uHue');\n const uXOffsetLocation = gl.getUniformLocation(program, 'uXOffset');\n const uSpeedLocation = gl.getUniformLocation(program, 'uSpeed');\n const uIntensityLocation = gl.getUniformLocation(program, 'uIntensity');\n const uSizeLocation = gl.getUniformLocation(program, 'uSize');\n\n const startTime = performance.now();\n const render = () => {\n resizeCanvas();\n gl.viewport(0, 0, canvas.width, canvas.height);\n gl.uniform2f(iResolutionLocation, canvas.width, canvas.height);\n const currentTime = performance.now();\n gl.uniform1f(iTimeLocation, (currentTime - startTime) / 1000.0);\n gl.uniform1f(uHueLocation, hue);\n gl.uniform1f(uXOffsetLocation, xOffset);\n gl.uniform1f(uSpeedLocation, speed);\n gl.uniform1f(uIntensityLocation, intensity);\n gl.uniform1f(uSizeLocation, size);\n gl.drawArrays(gl.TRIANGLES, 0, 6);\n requestAnimationFrame(render);\n };\n requestAnimationFrame(render);\n\n return () => {\n window.removeEventListener('resize', resizeCanvas);\n };\n }, [hue, xOffset, speed, intensity, size]);\n\n return ;\n};\n\nexport default Lightning;\n" + "content": "import React, { useRef, useEffect } from 'react';\n\ninterface LightningProps {\n hue?: number;\n xOffset?: number;\n speed?: number;\n intensity?: number;\n size?: number;\n}\n\nconst Lightning: React.FC = ({ hue = 230, xOffset = 0, speed = 1, intensity = 1, size = 1 }) => {\n const canvasRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const resizeCanvas = () => {\n canvas.width = canvas.clientWidth;\n canvas.height = canvas.clientHeight;\n };\n resizeCanvas();\n window.addEventListener('resize', resizeCanvas);\n\n const gl = canvas.getContext('webgl', { alpha: true, premultipliedAlpha: false });\n if (!gl) {\n console.error('WebGL not supported');\n return;\n }\n\n const vertexShaderSource = `\n attribute vec2 aPosition;\n void main() {\n gl_Position = vec4(aPosition, 0.0, 1.0);\n }\n `;\n\n const fragmentShaderSource = `\n precision mediump float;\n uniform vec2 iResolution;\n uniform float iTime;\n uniform float uHue;\n uniform float uXOffset;\n uniform float uSpeed;\n uniform float uIntensity;\n uniform float uSize;\n \n #define OCTAVE_COUNT 10\n\n vec3 hsv2rgb(vec3 c) {\n vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0,4.0,2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);\n return c.z * mix(vec3(1.0), rgb, c.y);\n }\n\n float hash11(float p) {\n p = fract(p * .1031);\n p *= p + 33.33;\n p *= p + p;\n return fract(p);\n }\n\n float hash12(vec2 p) {\n vec3 p3 = fract(vec3(p.xyx) * .1031);\n p3 += dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n }\n\n mat2 rotate2d(float theta) {\n float c = cos(theta);\n float s = sin(theta);\n return mat2(c, -s, s, c);\n }\n\n float noise(vec2 p) {\n vec2 ip = floor(p);\n vec2 fp = fract(p);\n float a = hash12(ip);\n float b = hash12(ip + vec2(1.0, 0.0));\n float c = hash12(ip + vec2(0.0, 1.0));\n float d = hash12(ip + vec2(1.0, 1.0));\n \n vec2 t = smoothstep(0.0, 1.0, fp);\n return mix(mix(a, b, t.x), mix(c, d, t.x), t.y);\n }\n\n float fbm(vec2 p) {\n float value = 0.0;\n float amplitude = 0.5;\n for (int i = 0; i < OCTAVE_COUNT; ++i) {\n value += amplitude * noise(p);\n p *= rotate2d(0.45);\n p *= 2.0;\n amplitude *= 0.5;\n }\n return value;\n }\n\n void mainImage( out vec4 fragColor, in vec2 fragCoord ) {\n vec2 uv = fragCoord / iResolution.xy;\n uv = 2.0 * uv - 1.0;\n uv.x *= iResolution.x / iResolution.y;\n uv.x += uXOffset;\n \n uv += 2.0 * fbm(uv * uSize + 0.8 * iTime * uSpeed) - 1.0;\n \n float dist = abs(uv.x);\n vec3 baseColor = hsv2rgb(vec3(uHue / 360.0, 0.7, 0.8));\n vec3 col = baseColor * pow(mix(0.0, 0.07, hash11(iTime * uSpeed)) / dist, 1.0) * uIntensity;\n col = pow(col, vec3(1.0));\n float a = clamp(max(col.r, max(col.g, col.b)), 0.0, 1.0);\n fragColor = vec4(col, a);\n }\n\n void main() {\n mainImage(gl_FragColor, gl_FragCoord.xy);\n }\n `;\n\n const compileShader = (source: string, type: number): WebGLShader | null => {\n const shader = gl.createShader(type);\n if (!shader) return null;\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n console.error('Shader compile error:', gl.getShaderInfoLog(shader));\n gl.deleteShader(shader);\n return null;\n }\n return shader;\n };\n\n const vertexShader = compileShader(vertexShaderSource, gl.VERTEX_SHADER);\n const fragmentShader = compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER);\n if (!vertexShader || !fragmentShader) return;\n\n const program = gl.createProgram();\n if (!program) return;\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.linkProgram(program);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n console.error('Program linking error:', gl.getProgramInfoLog(program));\n return;\n }\n gl.useProgram(program);\n\n const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]);\n const vertexBuffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);\n gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);\n\n const aPosition = gl.getAttribLocation(program, 'aPosition');\n gl.enableVertexAttribArray(aPosition);\n gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);\n\n const iResolutionLocation = gl.getUniformLocation(program, 'iResolution');\n const iTimeLocation = gl.getUniformLocation(program, 'iTime');\n const uHueLocation = gl.getUniformLocation(program, 'uHue');\n const uXOffsetLocation = gl.getUniformLocation(program, 'uXOffset');\n const uSpeedLocation = gl.getUniformLocation(program, 'uSpeed');\n const uIntensityLocation = gl.getUniformLocation(program, 'uIntensity');\n const uSizeLocation = gl.getUniformLocation(program, 'uSize');\n\n let animationFrameId: number;\n const startTime = performance.now();\n const render = () => {\n resizeCanvas();\n gl.viewport(0, 0, canvas.width, canvas.height);\n gl.uniform2f(iResolutionLocation, canvas.width, canvas.height);\n const currentTime = performance.now();\n gl.uniform1f(iTimeLocation, (currentTime - startTime) / 1000.0);\n gl.uniform1f(uHueLocation, hue);\n gl.uniform1f(uXOffsetLocation, xOffset);\n gl.uniform1f(uSpeedLocation, speed);\n gl.uniform1f(uIntensityLocation, intensity);\n gl.uniform1f(uSizeLocation, size);\n gl.drawArrays(gl.TRIANGLES, 0, 6);\n animationFrameId = requestAnimationFrame(render);\n };\n animationFrameId = requestAnimationFrame(render);\n\n return () => {\n cancelAnimationFrame(animationFrameId);\n window.removeEventListener('resize', resizeCanvas);\n };\n }, [hue, xOffset, speed, intensity, size]);\n\n return ;\n};\n\nexport default Lightning;\n" } ], "registryDependencies": [], diff --git a/public/r/LineSidebar-JS-CSS.json b/public/r/LineSidebar-JS-CSS.json index 3600321d..01438802 100644 --- a/public/r/LineSidebar-JS-CSS.json +++ b/public/r/LineSidebar-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "LineSidebar/LineSidebar.css", + "type": "registry:file", + "path": "LineSidebar.css", + "target": "@components/LineSidebar.css", "content": ".line-sidebar {\n --accent-color: #a855f7;\n --text-color: #c4c4c4;\n --marker-color: #6c6c6c;\n --marker-length: 60px;\n --marker-gap: 0px;\n --tick-scale: 0.5;\n --max-shift: 30px;\n --item-gap: 20px;\n --font-size: 1.1rem;\n --smoothing: 100ms;\n\n position: relative;\n display: flex;\n justify-content: flex-start;\n}\n\n.line-sidebar--markers {\n padding-left: calc(var(--marker-length) + var(--marker-gap));\n}\n\n.line-sidebar__list {\n list-style: none;\n margin: 0;\n padding: 1rem 0;\n display: flex;\n flex-direction: column;\n gap: var(--item-gap);\n}\n\n/* --effect (0..1) is driven per item by a rAF lerp in JS, so every derived\n property below reads the same continuously-animating value and stays in\n step, with no CSS transitions to stagger. */\n.line-sidebar__item {\n position: relative;\n cursor: pointer;\n}\n\n/* Widen the pointer target so items react a touch before the cursor arrives */\n.line-sidebar__item::before {\n content: '';\n position: absolute;\n inset: -6px -48px;\n}\n\n.line-sidebar__label {\n position: relative;\n display: inline-flex;\n align-items: baseline;\n font-size: var(--font-size);\n line-height: 1.2;\n color: color-mix(in srgb, var(--accent-color) calc(var(--effect, 0) * 100%), var(--text-color));\n transform: translateX(calc(var(--effect, 0) * var(--max-shift)));\n}\n\n.line-sidebar__index {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n margin-right: 0.6rem;\n font-size: 0.85em;\n opacity: calc(0.55 + var(--effect, 0) * 0.45);\n}\n\n.line-sidebar__marker {\n position: absolute;\n top: 50%;\n left: calc(-1 * var(--marker-length) - var(--marker-gap));\n height: 1px;\n width: var(--marker-length);\n background-color: color-mix(in srgb, var(--accent-color) calc(var(--effect, 0) * 100%), var(--marker-color));\n transform-origin: left center;\n transform: translateY(-50%) scaleX(calc(0.7 + var(--effect, 0) * 0.5));\n}\n\n/* Short static tick centered in the gap between two menu items */\n.line-sidebar--markers .line-sidebar__item:not(:last-child)::after {\n content: '';\n position: absolute;\n top: calc(100% + var(--item-gap) / 2);\n left: calc(-1 * var(--marker-length) - var(--marker-gap));\n height: 1px;\n width: calc(var(--marker-length) * var(--tick-scale));\n background-color: var(--marker-color);\n opacity: 0.5;\n transform: translateY(-50%);\n}\n\n/* When enabled, the in-between ticks grow with cursor proximity too */\n.line-sidebar--scale-tick .line-sidebar__item:not(:last-child)::after {\n transform-origin: left center;\n transform: translateY(-50%) scaleX(calc(0.7 + var(--effect, 0) * 0.6));\n}\n" }, { "type": "registry:component", - "path": "LineSidebar/LineSidebar.jsx", + "path": "LineSidebar.jsx", "content": "import { useRef, useState, useCallback, useEffect } from 'react';\nimport './LineSidebar.css';\n\nconst FALLOFF_CURVES = {\n linear: p => p,\n smooth: p => p * p * (3 - 2 * p),\n sharp: p => p * p * p\n};\n\nconst DEFAULT_ITEMS = [\n 'Overview',\n 'Components',\n 'Animations',\n 'Backgrounds',\n 'Showcase',\n 'Playground',\n 'Templates',\n 'Changelog',\n 'Community',\n 'Resources',\n 'Documentation',\n 'Support'\n];\n\nconst LineSidebar = ({\n items = DEFAULT_ITEMS,\n accentColor = '#A855F7',\n textColor = '#c4c4c4',\n markerColor = '#6c6c6c',\n showIndex = true,\n showMarker = true,\n proximityRadius = 100,\n maxShift = 30,\n falloff = 'smooth',\n markerLength = 60,\n markerGap = 0,\n tickScale = 0.5,\n scaleTick = true,\n itemGap = 20,\n fontSize = 1.1,\n smoothing = 100,\n defaultActive = null,\n onItemClick,\n className = ''\n}) => {\n const listRef = useRef(null);\n const itemRefs = useRef([]);\n const targetsRef = useRef([]);\n const currentRef = useRef([]);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const activeRef = useRef(defaultActive);\n const smoothingRef = useRef(smoothing);\n const [activeIndex, setActiveIndex] = useState(defaultActive);\n\n activeRef.current = activeIndex;\n smoothingRef.current = smoothing;\n\n // Single rAF loop that eases every item's --effect toward its target using\n // frame-rate independent exponential smoothing, so color, shift and scale\n // all move together without staggering CSS transitions.\n const runFrame = useCallback(now => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const tau = Math.max(smoothingRef.current, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n let moving = false;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const target = Math.max(targetsRef.current[i] || 0, activeRef.current === i ? 1 : 0);\n const cur = currentRef.current[i] || 0;\n const next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.0015;\n const value = settled ? target : next;\n currentRef.current[i] = value;\n el.style.setProperty('--effect', value.toFixed(4));\n if (!settled) moving = true;\n }\n\n rafRef.current = moving ? requestAnimationFrame(runFrame) : null;\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) {\n cancelAnimationFrame(rafRef.current);\n }\n\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n const handlePointerMove = useCallback(\n e => {\n const list = listRef.current;\n if (!list) return;\n const rect = list.getBoundingClientRect();\n const pointerY = e.clientY - rect.top;\n const ease = FALLOFF_CURVES[falloff] ?? FALLOFF_CURVES.linear;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const center = el.offsetTop + el.offsetHeight / 2;\n const distance = Math.abs(pointerY - center);\n targetsRef.current[i] = ease(Math.max(0, 1 - distance / proximityRadius));\n }\n startLoop();\n },\n [falloff, proximityRadius, startLoop]\n );\n\n const handlePointerLeave = useCallback(() => {\n targetsRef.current = targetsRef.current.map(() => 0);\n startLoop();\n }, [startLoop]);\n\n const handleClick = useCallback(\n (index, label) => {\n setActiveIndex(index);\n onItemClick?.(index, label);\n },\n [onItemClick]\n );\n\n useEffect(() => {\n startLoop();\n }, [activeIndex, startLoop]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n },\n []\n );\n\n return (\n \n
    \n {items.map((label, index) => (\n {\n itemRefs.current[index] = el;\n }}\n className=\"line-sidebar__item\"\n aria-current={activeIndex === index ? 'true' : undefined}\n onClick={() => handleClick(index, label)}\n >\n {showMarker && }\n \n {showIndex && {String(index + 1).padStart(2, '0')}}\n {label}\n \n \n ))}\n
\n \n );\n};\n\nexport default LineSidebar;\n" } ], diff --git a/public/r/LineSidebar-TS-CSS.json b/public/r/LineSidebar-TS-CSS.json index f43b2478..94b50c16 100644 --- a/public/r/LineSidebar-TS-CSS.json +++ b/public/r/LineSidebar-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "LineSidebar/LineSidebar.css", + "type": "registry:file", + "path": "LineSidebar.css", + "target": "@components/LineSidebar.css", "content": ".line-sidebar {\n --accent-color: #a855f7;\n --text-color: #c4c4c4;\n --marker-color: #6c6c6c;\n --marker-length: 60px;\n --marker-gap: 0px;\n --tick-scale: 0.5;\n --max-shift: 30px;\n --item-gap: 20px;\n --font-size: 1.1rem;\n --smoothing: 100ms;\n\n position: relative;\n display: flex;\n justify-content: flex-start;\n}\n\n.line-sidebar--markers {\n padding-left: calc(var(--marker-length) + var(--marker-gap));\n}\n\n.line-sidebar__list {\n list-style: none;\n margin: 0;\n padding: 1rem 0;\n display: flex;\n flex-direction: column;\n gap: var(--item-gap);\n}\n\n/* --effect (0..1) is driven per item by a rAF lerp in JS, so every derived\n property below reads the same continuously-animating value and stays in\n step, with no CSS transitions to stagger. */\n.line-sidebar__item {\n position: relative;\n cursor: pointer;\n}\n\n/* Widen the pointer target so items react a touch before the cursor arrives */\n.line-sidebar__item::before {\n content: '';\n position: absolute;\n inset: -6px -48px;\n}\n\n.line-sidebar__label {\n position: relative;\n display: inline-flex;\n align-items: baseline;\n font-size: var(--font-size);\n line-height: 1.2;\n color: color-mix(in srgb, var(--accent-color) calc(var(--effect, 0) * 100%), var(--text-color));\n transform: translateX(calc(var(--effect, 0) * var(--max-shift)));\n}\n\n.line-sidebar__index {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n margin-right: 0.6rem;\n font-size: 0.85em;\n opacity: calc(0.55 + var(--effect, 0) * 0.45);\n}\n\n.line-sidebar__marker {\n position: absolute;\n top: 50%;\n left: calc(-1 * var(--marker-length) - var(--marker-gap));\n height: 1px;\n width: var(--marker-length);\n background-color: color-mix(in srgb, var(--accent-color) calc(var(--effect, 0) * 100%), var(--marker-color));\n transform-origin: left center;\n transform: translateY(-50%) scaleX(calc(0.7 + var(--effect, 0) * 0.5));\n}\n\n/* Short static tick centered in the gap between two menu items */\n.line-sidebar--markers .line-sidebar__item:not(:last-child)::after {\n content: '';\n position: absolute;\n top: calc(100% + var(--item-gap) / 2);\n left: calc(-1 * var(--marker-length) - var(--marker-gap));\n height: 1px;\n width: calc(var(--marker-length) * var(--tick-scale));\n background-color: var(--marker-color);\n opacity: 0.5;\n transform: translateY(-50%);\n}\n\n/* When enabled, the in-between ticks grow with cursor proximity too */\n.line-sidebar--scale-tick .line-sidebar__item:not(:last-child)::after {\n transform-origin: left center;\n transform: translateY(-50%) scaleX(calc(0.7 + var(--effect, 0) * 0.6));\n}\n" }, { "type": "registry:component", - "path": "LineSidebar/LineSidebar.tsx", - "content": "import { useRef, useState, useCallback, useEffect, CSSProperties } from 'react';\nimport './LineSidebar.css';\n\ntype Falloff = 'linear' | 'smooth' | 'sharp';\n\nexport interface LineSidebarProps {\n items?: string[];\n accentColor?: string;\n textColor?: string;\n markerColor?: string;\n showIndex?: boolean;\n showMarker?: boolean;\n proximityRadius?: number;\n maxShift?: number;\n falloff?: Falloff;\n markerLength?: number;\n markerGap?: number;\n tickScale?: number;\n scaleTick?: boolean;\n itemGap?: number;\n fontSize?: number;\n smoothing?: number;\n defaultActive?: number | null;\n onItemClick?: (index: number, label: string) => void;\n className?: string;\n}\n\nconst FALLOFF_CURVES: Record number> = {\n linear: p => p,\n smooth: p => p * p * (3 - 2 * p),\n sharp: p => p * p * p\n};\n\nconst DEFAULT_ITEMS = [\n 'Overview',\n 'Components',\n 'Animations',\n 'Backgrounds',\n 'Showcase',\n 'Playground',\n 'Templates',\n 'Changelog',\n 'Community',\n 'Resources',\n 'Documentation',\n 'Support'\n];\n\nconst LineSidebar = ({\n items = DEFAULT_ITEMS,\n accentColor = '#A855F7',\n textColor = '#c4c4c4',\n markerColor = '#6c6c6c',\n showIndex = true,\n showMarker = true,\n proximityRadius = 100,\n maxShift = 30,\n falloff = 'smooth',\n markerLength = 60,\n markerGap = 0,\n tickScale = 0.5,\n scaleTick = true,\n itemGap = 20,\n fontSize = 1.1,\n smoothing = 100,\n defaultActive = null,\n onItemClick,\n className = ''\n}: LineSidebarProps) => {\n const listRef = useRef(null);\n const itemRefs = useRef<(HTMLLIElement | null)[]>([]);\n const targetsRef = useRef([]);\n const currentRef = useRef([]);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const activeRef = useRef(defaultActive);\n const smoothingRef = useRef(smoothing);\n const [activeIndex, setActiveIndex] = useState(defaultActive);\n\n activeRef.current = activeIndex;\n smoothingRef.current = smoothing;\n\n // Single rAF loop that eases every item's --effect toward its target using\n // frame-rate independent exponential smoothing, so color, shift and scale\n // all move together without staggering CSS transitions.\n const runFrame = useCallback((now: number) => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const tau = Math.max(smoothingRef.current, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n let moving = false;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const target = Math.max(targetsRef.current[i] || 0, activeRef.current === i ? 1 : 0);\n const cur = currentRef.current[i] || 0;\n const next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.0015;\n const value = settled ? target : next;\n currentRef.current[i] = value;\n el.style.setProperty('--effect', value.toFixed(4));\n if (!settled) moving = true;\n }\n\n rafRef.current = moving ? requestAnimationFrame(runFrame) : null;\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) {\n cancelAnimationFrame(rafRef.current);\n }\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n const handlePointerMove = useCallback(\n (e: React.PointerEvent) => {\n const list = listRef.current;\n if (!list) return;\n const rect = list.getBoundingClientRect();\n const pointerY = e.clientY - rect.top;\n const ease = FALLOFF_CURVES[falloff] ?? FALLOFF_CURVES.linear;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const center = el.offsetTop + el.offsetHeight / 2;\n const distance = Math.abs(pointerY - center);\n targetsRef.current[i] = ease(Math.max(0, 1 - distance / proximityRadius));\n }\n startLoop();\n },\n [falloff, proximityRadius, startLoop]\n );\n\n const handlePointerLeave = useCallback(() => {\n targetsRef.current = targetsRef.current.map(() => 0);\n startLoop();\n }, [startLoop]);\n\n const handleClick = useCallback(\n (index: number, label: string) => {\n setActiveIndex(index);\n onItemClick?.(index, label);\n },\n [onItemClick]\n );\n\n useEffect(() => {\n startLoop();\n }, [activeIndex, startLoop]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n },\n []\n );\n\n return (\n \n
    \n {items.map((label, index) => (\n {\n itemRefs.current[index] = el;\n }}\n className=\"line-sidebar__item\"\n aria-current={activeIndex === index ? 'true' : undefined}\n onClick={() => handleClick(index, label)}\n >\n {showMarker && }\n \n {showIndex && {String(index + 1).padStart(2, '0')}}\n {label}\n \n \n ))}\n
\n \n );\n};\n\nexport default LineSidebar;\n" + "path": "LineSidebar.tsx", + "content": "import { useRef, useState, useCallback, useEffect, type CSSProperties } from 'react';\nimport './LineSidebar.css';\n\ntype Falloff = 'linear' | 'smooth' | 'sharp';\n\nexport interface LineSidebarProps {\n items?: string[];\n accentColor?: string;\n textColor?: string;\n markerColor?: string;\n showIndex?: boolean;\n showMarker?: boolean;\n proximityRadius?: number;\n maxShift?: number;\n falloff?: Falloff;\n markerLength?: number;\n markerGap?: number;\n tickScale?: number;\n scaleTick?: boolean;\n itemGap?: number;\n fontSize?: number;\n smoothing?: number;\n defaultActive?: number | null;\n onItemClick?: (index: number, label: string) => void;\n className?: string;\n}\n\nconst FALLOFF_CURVES: Record number> = {\n linear: p => p,\n smooth: p => p * p * (3 - 2 * p),\n sharp: p => p * p * p\n};\n\nconst DEFAULT_ITEMS = [\n 'Overview',\n 'Components',\n 'Animations',\n 'Backgrounds',\n 'Showcase',\n 'Playground',\n 'Templates',\n 'Changelog',\n 'Community',\n 'Resources',\n 'Documentation',\n 'Support'\n];\n\nconst LineSidebar = ({\n items = DEFAULT_ITEMS,\n accentColor = '#A855F7',\n textColor = '#c4c4c4',\n markerColor = '#6c6c6c',\n showIndex = true,\n showMarker = true,\n proximityRadius = 100,\n maxShift = 30,\n falloff = 'smooth',\n markerLength = 60,\n markerGap = 0,\n tickScale = 0.5,\n scaleTick = true,\n itemGap = 20,\n fontSize = 1.1,\n smoothing = 100,\n defaultActive = null,\n onItemClick,\n className = ''\n}: LineSidebarProps) => {\n const listRef = useRef(null);\n const itemRefs = useRef<(HTMLLIElement | null)[]>([]);\n const targetsRef = useRef([]);\n const currentRef = useRef([]);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const activeRef = useRef(defaultActive);\n const smoothingRef = useRef(smoothing);\n const [activeIndex, setActiveIndex] = useState(defaultActive);\n\n activeRef.current = activeIndex;\n smoothingRef.current = smoothing;\n\n // Single rAF loop that eases every item's --effect toward its target using\n // frame-rate independent exponential smoothing, so color, shift and scale\n // all move together without staggering CSS transitions.\n const runFrame = useCallback((now: number) => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const tau = Math.max(smoothingRef.current, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n let moving = false;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const target = Math.max(targetsRef.current[i] || 0, activeRef.current === i ? 1 : 0);\n const cur = currentRef.current[i] || 0;\n const next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.0015;\n const value = settled ? target : next;\n currentRef.current[i] = value;\n el.style.setProperty('--effect', value.toFixed(4));\n if (!settled) moving = true;\n }\n\n rafRef.current = moving ? requestAnimationFrame(runFrame) : null;\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) {\n cancelAnimationFrame(rafRef.current);\n }\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n const handlePointerMove = useCallback(\n (e: React.PointerEvent) => {\n const list = listRef.current;\n if (!list) return;\n const rect = list.getBoundingClientRect();\n const pointerY = e.clientY - rect.top;\n const ease = FALLOFF_CURVES[falloff] ?? FALLOFF_CURVES.linear;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const center = el.offsetTop + el.offsetHeight / 2;\n const distance = Math.abs(pointerY - center);\n targetsRef.current[i] = ease(Math.max(0, 1 - distance / proximityRadius));\n }\n startLoop();\n },\n [falloff, proximityRadius, startLoop]\n );\n\n const handlePointerLeave = useCallback(() => {\n targetsRef.current = targetsRef.current.map(() => 0);\n startLoop();\n }, [startLoop]);\n\n const handleClick = useCallback(\n (index: number, label: string) => {\n setActiveIndex(index);\n onItemClick?.(index, label);\n },\n [onItemClick]\n );\n\n useEffect(() => {\n startLoop();\n }, [activeIndex, startLoop]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n },\n []\n );\n\n return (\n \n
    \n {items.map((label, index) => (\n {\n itemRefs.current[index] = el;\n }}\n className=\"line-sidebar__item\"\n aria-current={activeIndex === index ? 'true' : undefined}\n onClick={() => handleClick(index, label)}\n >\n {showMarker && }\n \n {showIndex && {String(index + 1).padStart(2, '0')}}\n {label}\n \n \n ))}\n
\n \n );\n};\n\nexport default LineSidebar;\n" } ], "registryDependencies": [], diff --git a/public/r/LineSidebar-TS-TW.json b/public/r/LineSidebar-TS-TW.json index ad98a2dc..dd7a02b8 100644 --- a/public/r/LineSidebar-TS-TW.json +++ b/public/r/LineSidebar-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "LineSidebar/LineSidebar.tsx", - "content": "import { useRef, useState, useCallback, useEffect, CSSProperties } from 'react';\n\ntype Falloff = 'linear' | 'smooth' | 'sharp';\n\nexport interface LineSidebarProps {\n items?: string[];\n accentColor?: string;\n textColor?: string;\n markerColor?: string;\n showIndex?: boolean;\n showMarker?: boolean;\n proximityRadius?: number;\n maxShift?: number;\n falloff?: Falloff;\n markerLength?: number;\n markerGap?: number;\n tickScale?: number;\n scaleTick?: boolean;\n itemGap?: number;\n fontSize?: number;\n smoothing?: number;\n defaultActive?: number | null;\n onItemClick?: (index: number, label: string) => void;\n className?: string;\n}\n\nconst FALLOFF_CURVES: Record number> = {\n linear: p => p,\n smooth: p => p * p * (3 - 2 * p),\n sharp: p => p * p * p\n};\n\nconst DEFAULT_ITEMS = [\n 'Overview',\n 'Components',\n 'Animations',\n 'Backgrounds',\n 'Showcase',\n 'Playground',\n 'Templates',\n 'Changelog',\n 'Community',\n 'Resources',\n 'Documentation',\n 'Support'\n];\n\nconst LineSidebar = ({\n items = DEFAULT_ITEMS,\n accentColor = '#A855F7',\n textColor = '#c4c4c4',\n markerColor = '#6c6c6c',\n showIndex = true,\n showMarker = true,\n proximityRadius = 100,\n maxShift = 30,\n falloff = 'smooth',\n markerLength = 60,\n markerGap = 0,\n tickScale = 0.5,\n scaleTick = true,\n itemGap = 20,\n fontSize = 1.1,\n smoothing = 100,\n defaultActive = null,\n onItemClick,\n className = ''\n}: LineSidebarProps) => {\n const listRef = useRef(null);\n const itemRefs = useRef<(HTMLLIElement | null)[]>([]);\n const targetsRef = useRef([]);\n const currentRef = useRef([]);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const activeRef = useRef(defaultActive);\n const smoothingRef = useRef(smoothing);\n const [activeIndex, setActiveIndex] = useState(defaultActive);\n\n activeRef.current = activeIndex;\n smoothingRef.current = smoothing;\n\n // Single rAF loop that eases every item's --effect toward its target using\n // frame-rate independent exponential smoothing, so color, shift and scale\n // all move together without staggering CSS transitions.\n const runFrame = useCallback((now: number) => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const tau = Math.max(smoothingRef.current, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n let moving = false;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const target = Math.max(targetsRef.current[i] || 0, activeRef.current === i ? 1 : 0);\n const cur = currentRef.current[i] || 0;\n const next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.0015;\n const value = settled ? target : next;\n currentRef.current[i] = value;\n el.style.setProperty('--effect', value.toFixed(4));\n if (!settled) moving = true;\n }\n\n rafRef.current = moving ? requestAnimationFrame(runFrame) : null;\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) {\n cancelAnimationFrame(rafRef.current);\n }\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n const handlePointerMove = useCallback(\n (e: React.PointerEvent) => {\n const list = listRef.current;\n if (!list) return;\n const rect = list.getBoundingClientRect();\n const pointerY = e.clientY - rect.top;\n const ease = FALLOFF_CURVES[falloff] ?? FALLOFF_CURVES.linear;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const center = el.offsetTop + el.offsetHeight / 2;\n const distance = Math.abs(pointerY - center);\n targetsRef.current[i] = ease(Math.max(0, 1 - distance / proximityRadius));\n }\n startLoop();\n },\n [falloff, proximityRadius, startLoop]\n );\n\n const handlePointerLeave = useCallback(() => {\n targetsRef.current = targetsRef.current.map(() => 0);\n startLoop();\n }, [startLoop]);\n\n const handleClick = useCallback(\n (index: number, label: string) => {\n setActiveIndex(index);\n onItemClick?.(index, label);\n },\n [onItemClick]\n );\n\n useEffect(() => {\n startLoop();\n }, [activeIndex, startLoop]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n },\n []\n );\n\n const tickClass = showMarker\n ? `after:absolute after:left-[calc(-1*var(--marker-length)-var(--marker-gap))] after:top-[calc(100%+var(--item-gap)/2)] after:h-px after:opacity-50 after:content-[''] last:after:content-none after:[background-color:var(--marker-color)] after:[width:calc(var(--marker-length)*var(--tick-scale))] ${\n scaleTick\n ? \"after:origin-left after:[transform:translateY(-50%)_scaleX(calc(0.7+var(--effect,0)*0.6))]\"\n : 'after:-translate-y-1/2'\n }`\n : '';\n\n return (\n \n \n {items.map((label, index) => (\n {\n itemRefs.current[index] = el;\n }}\n aria-current={activeIndex === index ? 'true' : undefined}\n onClick={() => handleClick(index, label)}\n className={`relative cursor-pointer before:absolute before:-inset-x-12 before:-inset-y-[6px] before:content-[''] ${tickClass}`}\n >\n {showMarker && (\n \n )}\n \n {showIndex && (\n \n {String(index + 1).padStart(2, '0')}\n \n )}\n {label}\n \n \n ))}\n \n \n );\n};\n\nexport default LineSidebar;\n" + "content": "import { useRef, useState, useCallback, useEffect, type CSSProperties } from 'react';\n\ntype Falloff = 'linear' | 'smooth' | 'sharp';\n\nexport interface LineSidebarProps {\n items?: string[];\n accentColor?: string;\n textColor?: string;\n markerColor?: string;\n showIndex?: boolean;\n showMarker?: boolean;\n proximityRadius?: number;\n maxShift?: number;\n falloff?: Falloff;\n markerLength?: number;\n markerGap?: number;\n tickScale?: number;\n scaleTick?: boolean;\n itemGap?: number;\n fontSize?: number;\n smoothing?: number;\n defaultActive?: number | null;\n onItemClick?: (index: number, label: string) => void;\n className?: string;\n}\n\nconst FALLOFF_CURVES: Record number> = {\n linear: p => p,\n smooth: p => p * p * (3 - 2 * p),\n sharp: p => p * p * p\n};\n\nconst DEFAULT_ITEMS = [\n 'Overview',\n 'Components',\n 'Animations',\n 'Backgrounds',\n 'Showcase',\n 'Playground',\n 'Templates',\n 'Changelog',\n 'Community',\n 'Resources',\n 'Documentation',\n 'Support'\n];\n\nconst LineSidebar = ({\n items = DEFAULT_ITEMS,\n accentColor = '#A855F7',\n textColor = '#c4c4c4',\n markerColor = '#6c6c6c',\n showIndex = true,\n showMarker = true,\n proximityRadius = 100,\n maxShift = 30,\n falloff = 'smooth',\n markerLength = 60,\n markerGap = 0,\n tickScale = 0.5,\n scaleTick = true,\n itemGap = 20,\n fontSize = 1.1,\n smoothing = 100,\n defaultActive = null,\n onItemClick,\n className = ''\n}: LineSidebarProps) => {\n const listRef = useRef(null);\n const itemRefs = useRef<(HTMLLIElement | null)[]>([]);\n const targetsRef = useRef([]);\n const currentRef = useRef([]);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const activeRef = useRef(defaultActive);\n const smoothingRef = useRef(smoothing);\n const [activeIndex, setActiveIndex] = useState(defaultActive);\n\n activeRef.current = activeIndex;\n smoothingRef.current = smoothing;\n\n // Single rAF loop that eases every item's --effect toward its target using\n // frame-rate independent exponential smoothing, so color, shift and scale\n // all move together without staggering CSS transitions.\n const runFrame = useCallback((now: number) => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const tau = Math.max(smoothingRef.current, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n let moving = false;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const target = Math.max(targetsRef.current[i] || 0, activeRef.current === i ? 1 : 0);\n const cur = currentRef.current[i] || 0;\n const next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.0015;\n const value = settled ? target : next;\n currentRef.current[i] = value;\n el.style.setProperty('--effect', value.toFixed(4));\n if (!settled) moving = true;\n }\n\n rafRef.current = moving ? requestAnimationFrame(runFrame) : null;\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) {\n cancelAnimationFrame(rafRef.current);\n }\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n const handlePointerMove = useCallback(\n (e: React.PointerEvent) => {\n const list = listRef.current;\n if (!list) return;\n const rect = list.getBoundingClientRect();\n const pointerY = e.clientY - rect.top;\n const ease = FALLOFF_CURVES[falloff] ?? FALLOFF_CURVES.linear;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const center = el.offsetTop + el.offsetHeight / 2;\n const distance = Math.abs(pointerY - center);\n targetsRef.current[i] = ease(Math.max(0, 1 - distance / proximityRadius));\n }\n startLoop();\n },\n [falloff, proximityRadius, startLoop]\n );\n\n const handlePointerLeave = useCallback(() => {\n targetsRef.current = targetsRef.current.map(() => 0);\n startLoop();\n }, [startLoop]);\n\n const handleClick = useCallback(\n (index: number, label: string) => {\n setActiveIndex(index);\n onItemClick?.(index, label);\n },\n [onItemClick]\n );\n\n useEffect(() => {\n startLoop();\n }, [activeIndex, startLoop]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n },\n []\n );\n\n const tickClass = showMarker\n ? `after:absolute after:left-[calc(-1*var(--marker-length)-var(--marker-gap))] after:top-[calc(100%+var(--item-gap)/2)] after:h-px after:opacity-50 after:content-[''] last:after:content-none after:[background-color:var(--marker-color)] after:[width:calc(var(--marker-length)*var(--tick-scale))] ${\n scaleTick\n ? \"after:origin-left after:[transform:translateY(-50%)_scaleX(calc(0.7+var(--effect,0)*0.6))]\"\n : 'after:-translate-y-1/2'\n }`\n : '';\n\n return (\n \n \n {items.map((label, index) => (\n {\n itemRefs.current[index] = el;\n }}\n aria-current={activeIndex === index ? 'true' : undefined}\n onClick={() => handleClick(index, label)}\n className={`relative cursor-pointer before:absolute before:-inset-x-12 before:-inset-y-[6px] before:content-[''] ${tickClass}`}\n >\n {showMarker && (\n \n )}\n \n {showIndex && (\n \n {String(index + 1).padStart(2, '0')}\n \n )}\n {label}\n \n \n ))}\n \n \n );\n};\n\nexport default LineSidebar;\n" } ], "registryDependencies": [], diff --git a/public/r/LineWaves-JS-CSS.json b/public/r/LineWaves-JS-CSS.json index 04c59203..543ebdcf 100644 --- a/public/r/LineWaves-JS-CSS.json +++ b/public/r/LineWaves-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "LineWaves/LineWaves.css", + "type": "registry:file", + "path": "LineWaves.css", + "target": "@components/LineWaves.css", "content": ".line-waves-container {\n width: 100%;\n height: 100%;\n}" }, { "type": "registry:component", - "path": "LineWaves/LineWaves.jsx", + "path": "LineWaves.jsx", "content": "import { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport { useEffect, useRef } from 'react';\n\nimport './LineWaves.css';\n\nfunction hexToVec3(hex) {\n const h = hex.replace('#', '');\n return [\n parseInt(h.slice(0, 2), 16) / 255,\n parseInt(h.slice(2, 4), 16) / 255,\n parseInt(h.slice(4, 6), 16) / 255\n ];\n}\n\nconst vertexShader = `\nattribute vec2 uv;\nattribute vec2 position;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0, 1);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float uTime;\nuniform vec3 uResolution;\nuniform float uSpeed;\nuniform float uInnerLines;\nuniform float uOuterLines;\nuniform float uWarpIntensity;\nuniform float uRotation;\nuniform float uEdgeFadeWidth;\nuniform float uColorCycleSpeed;\nuniform float uBrightness;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nuniform vec2 uMouse;\nuniform float uMouseInfluence;\nuniform bool uEnableMouse;\n\n#define HALF_PI 1.5707963\n\nfloat hashF(float n) {\n return fract(sin(n * 127.1) * 43758.5453123);\n}\n\nfloat smoothNoise(float x) {\n float i = floor(x);\n float f = fract(x);\n float u = f * f * (3.0 - 2.0 * f);\n return mix(hashF(i), hashF(i + 1.0), u);\n}\n\nfloat displaceA(float coord, float t) {\n float result = sin(coord * 2.123) * 0.2;\n result += sin(coord * 3.234 + t * 4.345) * 0.1;\n result += sin(coord * 0.589 + t * 0.934) * 0.5;\n return result;\n}\n\nfloat displaceB(float coord, float t) {\n float result = sin(coord * 1.345) * 0.3;\n result += sin(coord * 2.734 + t * 3.345) * 0.2;\n result += sin(coord * 0.189 + t * 0.934) * 0.3;\n return result;\n}\n\nvec2 rotate2D(vec2 p, float angle) {\n float c = cos(angle);\n float s = sin(angle);\n return vec2(p.x * c - p.y * s, p.x * s + p.y * c);\n}\n\nvoid main() {\n vec2 coords = gl_FragCoord.xy / uResolution.xy;\n coords = coords * 2.0 - 1.0;\n coords = rotate2D(coords, uRotation);\n\n float halfT = uTime * uSpeed * 0.5;\n float fullT = uTime * uSpeed;\n\n float mouseWarp = 0.0;\n if (uEnableMouse) {\n vec2 mPos = rotate2D(uMouse * 2.0 - 1.0, uRotation);\n float mDist = length(coords - mPos);\n mouseWarp = uMouseInfluence * exp(-mDist * mDist * 4.0);\n }\n\n float warpAx = coords.x + displaceA(coords.y, halfT) * uWarpIntensity + mouseWarp;\n float warpAy = coords.y - displaceA(coords.x * cos(fullT) * 1.235, halfT) * uWarpIntensity;\n float warpBx = coords.x + displaceB(coords.y, halfT) * uWarpIntensity + mouseWarp;\n float warpBy = coords.y - displaceB(coords.x * sin(fullT) * 1.235, halfT) * uWarpIntensity;\n\n vec2 fieldA = vec2(warpAx, warpAy);\n vec2 fieldB = vec2(warpBx, warpBy);\n vec2 blended = mix(fieldA, fieldB, mix(fieldA, fieldB, 0.5));\n\n float fadeTop = smoothstep(uEdgeFadeWidth, uEdgeFadeWidth + 0.4, blended.y);\n float fadeBottom = smoothstep(-uEdgeFadeWidth, -(uEdgeFadeWidth + 0.4), blended.y);\n float vMask = 1.0 - max(fadeTop, fadeBottom);\n\n float tileCount = mix(uOuterLines, uInnerLines, vMask);\n float scaledY = blended.y * tileCount;\n float nY = smoothNoise(abs(scaledY));\n\n float ridge = pow(\n step(abs(nY - blended.x) * 2.0, HALF_PI) * cos(2.0 * (nY - blended.x)),\n 5.0\n );\n\n float lines = 0.0;\n for (float i = 1.0; i < 3.0; i += 1.0) {\n lines += pow(max(fract(scaledY), fract(-scaledY)), i * 2.0);\n }\n\n float pattern = vMask * lines;\n\n float cycleT = fullT * uColorCycleSpeed;\n float rChannel = (pattern + lines * ridge) * (cos(blended.y + cycleT * 0.234) * 0.5 + 1.0);\n float gChannel = (pattern + vMask * ridge) * (sin(blended.x + cycleT * 1.745) * 0.5 + 1.0);\n float bChannel = (pattern + lines * ridge) * (cos(blended.x + cycleT * 0.534) * 0.5 + 1.0);\n\n vec3 col = (rChannel * uColor1 + gChannel * uColor2 + bChannel * uColor3) * uBrightness;\n float alpha = clamp(length(col), 0.0, 1.0);\n\n gl_FragColor = vec4(col, alpha);\n}\n`;\n\nexport default function LineWaves({\n speed = 0.3,\n innerLineCount = 32.0,\n outerLineCount = 36.0,\n warpIntensity = 1.0,\n rotation = -45,\n edgeFadeWidth = 0.0,\n colorCycleSpeed = 1.0,\n brightness = 0.2,\n color1 = '#ffffff',\n color2 = '#ffffff',\n color3 = '#ffffff',\n enableMouseInteraction = true,\n mouseInfluence = 2.0\n}) {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n const container = containerRef.current;\n const renderer = new Renderer({ alpha: true, premultipliedAlpha: false });\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n\n let program;\n let currentMouse = [0.5, 0.5];\n let targetMouse = [0.5, 0.5];\n\n function handleMouseMove(e) {\n const rect = gl.canvas.getBoundingClientRect();\n targetMouse = [\n (e.clientX - rect.left) / rect.width,\n 1.0 - (e.clientY - rect.top) / rect.height\n ];\n }\n\n function handleMouseLeave() {\n targetMouse = [0.5, 0.5];\n }\n\n function resize() {\n renderer.setSize(container.offsetWidth, container.offsetHeight);\n if (program) {\n program.uniforms.uResolution.value = [gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height];\n }\n }\n window.addEventListener('resize', resize);\n resize();\n\n const geometry = new Triangle(gl);\n const rotationRad = (rotation * Math.PI) / 180;\n program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uResolution: { value: [gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height] },\n uSpeed: { value: speed },\n uInnerLines: { value: innerLineCount },\n uOuterLines: { value: outerLineCount },\n uWarpIntensity: { value: warpIntensity },\n uRotation: { value: rotationRad },\n uEdgeFadeWidth: { value: edgeFadeWidth },\n uColorCycleSpeed: { value: colorCycleSpeed },\n uBrightness: { value: brightness },\n uColor1: { value: hexToVec3(color1) },\n uColor2: { value: hexToVec3(color2) },\n uColor3: { value: hexToVec3(color3) },\n uMouse: { value: new Float32Array([0.5, 0.5]) },\n uMouseInfluence: { value: mouseInfluence },\n uEnableMouse: { value: enableMouseInteraction }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n container.appendChild(gl.canvas);\n\n if (enableMouseInteraction) {\n gl.canvas.addEventListener('mousemove', handleMouseMove);\n gl.canvas.addEventListener('mouseleave', handleMouseLeave);\n }\n\n let animationFrameId;\n\n function update(time) {\n animationFrameId = requestAnimationFrame(update);\n program.uniforms.uTime.value = time * 0.001;\n\n if (enableMouseInteraction) {\n currentMouse[0] += 0.05 * (targetMouse[0] - currentMouse[0]);\n currentMouse[1] += 0.05 * (targetMouse[1] - currentMouse[1]);\n program.uniforms.uMouse.value[0] = currentMouse[0];\n program.uniforms.uMouse.value[1] = currentMouse[1];\n } else {\n program.uniforms.uMouse.value[0] = 0.5;\n program.uniforms.uMouse.value[1] = 0.5;\n }\n\n renderer.render({ scene: mesh });\n }\n animationFrameId = requestAnimationFrame(update);\n\n return () => {\n cancelAnimationFrame(animationFrameId);\n window.removeEventListener('resize', resize);\n if (enableMouseInteraction) {\n gl.canvas.removeEventListener('mousemove', handleMouseMove);\n gl.canvas.removeEventListener('mouseleave', handleMouseLeave);\n }\n container.removeChild(gl.canvas);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, [speed, innerLineCount, outerLineCount, warpIntensity, rotation, edgeFadeWidth, colorCycleSpeed, brightness, color1, color2, color3, enableMouseInteraction, mouseInfluence]);\n\n return
;\n}\n" } ], diff --git a/public/r/LineWaves-TS-CSS.json b/public/r/LineWaves-TS-CSS.json index 0047b304..9986f20f 100644 --- a/public/r/LineWaves-TS-CSS.json +++ b/public/r/LineWaves-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "LineWaves/LineWaves.css", + "type": "registry:file", + "path": "LineWaves.css", + "target": "@components/LineWaves.css", "content": ".line-waves-container {\n width: 100%;\n height: 100%;\n}" }, { "type": "registry:component", - "path": "LineWaves/LineWaves.tsx", + "path": "LineWaves.tsx", "content": "import { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport { useEffect, useRef } from 'react';\n\nimport './LineWaves.css';\n\ninterface LineWavesProps {\n speed?: number;\n innerLineCount?: number;\n outerLineCount?: number;\n warpIntensity?: number;\n rotation?: number;\n edgeFadeWidth?: number;\n colorCycleSpeed?: number;\n brightness?: number;\n color1?: string;\n color2?: string;\n color3?: string;\n enableMouseInteraction?: boolean;\n mouseInfluence?: number;\n}\n\nfunction hexToVec3(hex: string): [number, number, number] {\n const h = hex.replace('#', '');\n return [\n parseInt(h.slice(0, 2), 16) / 255,\n parseInt(h.slice(2, 4), 16) / 255,\n parseInt(h.slice(4, 6), 16) / 255\n ];\n}\n\nconst vertexShader = `\nattribute vec2 uv;\nattribute vec2 position;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0, 1);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float uTime;\nuniform vec3 uResolution;\nuniform float uSpeed;\nuniform float uInnerLines;\nuniform float uOuterLines;\nuniform float uWarpIntensity;\nuniform float uRotation;\nuniform float uEdgeFadeWidth;\nuniform float uColorCycleSpeed;\nuniform float uBrightness;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nuniform vec2 uMouse;\nuniform float uMouseInfluence;\nuniform bool uEnableMouse;\n\n#define HALF_PI 1.5707963\n\nfloat hashF(float n) {\n return fract(sin(n * 127.1) * 43758.5453123);\n}\n\nfloat smoothNoise(float x) {\n float i = floor(x);\n float f = fract(x);\n float u = f * f * (3.0 - 2.0 * f);\n return mix(hashF(i), hashF(i + 1.0), u);\n}\n\nfloat displaceA(float coord, float t) {\n float result = sin(coord * 2.123) * 0.2;\n result += sin(coord * 3.234 + t * 4.345) * 0.1;\n result += sin(coord * 0.589 + t * 0.934) * 0.5;\n return result;\n}\n\nfloat displaceB(float coord, float t) {\n float result = sin(coord * 1.345) * 0.3;\n result += sin(coord * 2.734 + t * 3.345) * 0.2;\n result += sin(coord * 0.189 + t * 0.934) * 0.3;\n return result;\n}\n\nvec2 rotate2D(vec2 p, float angle) {\n float c = cos(angle);\n float s = sin(angle);\n return vec2(p.x * c - p.y * s, p.x * s + p.y * c);\n}\n\nvoid main() {\n vec2 coords = gl_FragCoord.xy / uResolution.xy;\n coords = coords * 2.0 - 1.0;\n coords = rotate2D(coords, uRotation);\n\n float halfT = uTime * uSpeed * 0.5;\n float fullT = uTime * uSpeed;\n\n float mouseWarp = 0.0;\n if (uEnableMouse) {\n vec2 mPos = rotate2D(uMouse * 2.0 - 1.0, uRotation);\n float mDist = length(coords - mPos);\n mouseWarp = uMouseInfluence * exp(-mDist * mDist * 4.0);\n }\n\n float warpAx = coords.x + displaceA(coords.y, halfT) * uWarpIntensity + mouseWarp;\n float warpAy = coords.y - displaceA(coords.x * cos(fullT) * 1.235, halfT) * uWarpIntensity;\n float warpBx = coords.x + displaceB(coords.y, halfT) * uWarpIntensity + mouseWarp;\n float warpBy = coords.y - displaceB(coords.x * sin(fullT) * 1.235, halfT) * uWarpIntensity;\n\n vec2 fieldA = vec2(warpAx, warpAy);\n vec2 fieldB = vec2(warpBx, warpBy);\n vec2 blended = mix(fieldA, fieldB, mix(fieldA, fieldB, 0.5));\n\n float fadeTop = smoothstep(uEdgeFadeWidth, uEdgeFadeWidth + 0.4, blended.y);\n float fadeBottom = smoothstep(-uEdgeFadeWidth, -(uEdgeFadeWidth + 0.4), blended.y);\n float vMask = 1.0 - max(fadeTop, fadeBottom);\n\n float tileCount = mix(uOuterLines, uInnerLines, vMask);\n float scaledY = blended.y * tileCount;\n float nY = smoothNoise(abs(scaledY));\n\n float ridge = pow(\n step(abs(nY - blended.x) * 2.0, HALF_PI) * cos(2.0 * (nY - blended.x)),\n 5.0\n );\n\n float lines = 0.0;\n for (float i = 1.0; i < 3.0; i += 1.0) {\n lines += pow(max(fract(scaledY), fract(-scaledY)), i * 2.0);\n }\n\n float pattern = vMask * lines;\n\n float cycleT = fullT * uColorCycleSpeed;\n float rChannel = (pattern + lines * ridge) * (cos(blended.y + cycleT * 0.234) * 0.5 + 1.0);\n float gChannel = (pattern + vMask * ridge) * (sin(blended.x + cycleT * 1.745) * 0.5 + 1.0);\n float bChannel = (pattern + lines * ridge) * (cos(blended.x + cycleT * 0.534) * 0.5 + 1.0);\n\n vec3 col = (rChannel * uColor1 + gChannel * uColor2 + bChannel * uColor3) * uBrightness;\n float alpha = clamp(length(col), 0.0, 1.0);\n\n gl_FragColor = vec4(col, alpha);\n}\n`;\n\nexport default function LineWaves({\n speed = 0.3,\n innerLineCount = 32.0,\n outerLineCount = 36.0,\n warpIntensity = 1.0,\n rotation = -45,\n edgeFadeWidth = 0.0,\n colorCycleSpeed = 1.0,\n brightness = 0.2,\n color1 = '#ffffff',\n color2 = '#ffffff',\n color3 = '#ffffff',\n enableMouseInteraction = true,\n mouseInfluence = 2.0\n}: LineWavesProps) {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n const container = containerRef.current;\n const renderer = new Renderer({ alpha: true, premultipliedAlpha: false });\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n\n let program: Program;\n let currentMouse = [0.5, 0.5];\n let targetMouse = [0.5, 0.5];\n\n function handleMouseMove(e: MouseEvent) {\n const rect = gl.canvas.getBoundingClientRect();\n targetMouse = [\n (e.clientX - rect.left) / rect.width,\n 1.0 - (e.clientY - rect.top) / rect.height\n ];\n }\n\n function handleMouseLeave() {\n targetMouse = [0.5, 0.5];\n }\n\n function resize() {\n renderer.setSize(container.offsetWidth, container.offsetHeight);\n if (program) {\n program.uniforms.uResolution.value = [gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height];\n }\n }\n window.addEventListener('resize', resize);\n\n resize();\n\n const geometry = new Triangle(gl);\n const rotationRad = (rotation * Math.PI) / 180;\n program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uResolution: { value: [gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height] },\n uSpeed: { value: speed },\n uInnerLines: { value: innerLineCount },\n uOuterLines: { value: outerLineCount },\n uWarpIntensity: { value: warpIntensity },\n uRotation: { value: rotationRad },\n uEdgeFadeWidth: { value: edgeFadeWidth },\n uColorCycleSpeed: { value: colorCycleSpeed },\n uBrightness: { value: brightness },\n uColor1: { value: hexToVec3(color1) },\n uColor2: { value: hexToVec3(color2) },\n uColor3: { value: hexToVec3(color3) },\n uMouse: { value: new Float32Array([0.5, 0.5]) },\n uMouseInfluence: { value: mouseInfluence },\n uEnableMouse: { value: enableMouseInteraction }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n container.appendChild(gl.canvas);\n\n if (enableMouseInteraction) {\n gl.canvas.addEventListener('mousemove', handleMouseMove);\n gl.canvas.addEventListener('mouseleave', handleMouseLeave);\n }\n\n let animationFrameId: number;\n\n function update(time: number) {\n animationFrameId = requestAnimationFrame(update);\n program.uniforms.uTime.value = time * 0.001;\n\n if (enableMouseInteraction) {\n currentMouse[0] += 0.05 * (targetMouse[0] - currentMouse[0]);\n currentMouse[1] += 0.05 * (targetMouse[1] - currentMouse[1]);\n program.uniforms.uMouse.value[0] = currentMouse[0];\n program.uniforms.uMouse.value[1] = currentMouse[1];\n } else {\n program.uniforms.uMouse.value[0] = 0.5;\n program.uniforms.uMouse.value[1] = 0.5;\n }\n\n renderer.render({ scene: mesh });\n }\n animationFrameId = requestAnimationFrame(update);\n\n return () => {\n cancelAnimationFrame(animationFrameId);\n window.removeEventListener('resize', resize);\n if (enableMouseInteraction) {\n gl.canvas.removeEventListener('mousemove', handleMouseMove);\n gl.canvas.removeEventListener('mouseleave', handleMouseLeave);\n }\n container.removeChild(gl.canvas);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, [speed, innerLineCount, outerLineCount, warpIntensity, rotation, edgeFadeWidth, colorCycleSpeed, brightness, color1, color2, color3, enableMouseInteraction, mouseInfluence]);\n\n return
;\n}\n" } ], diff --git a/public/r/LiquidChrome-JS-CSS.json b/public/r/LiquidChrome-JS-CSS.json index 76f8a624..f4eb23c6 100644 --- a/public/r/LiquidChrome-JS-CSS.json +++ b/public/r/LiquidChrome-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "LiquidChrome/LiquidChrome.css", + "type": "registry:file", + "path": "LiquidChrome.css", + "target": "@components/LiquidChrome.css", "content": ".liquidChrome-container {\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "LiquidChrome/LiquidChrome.jsx", + "path": "LiquidChrome.jsx", "content": "import { useRef, useEffect } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\n\nimport './LiquidChrome.css';\n\nexport const LiquidChrome = ({\n baseColor = [0.1, 0.1, 0.1],\n speed = 0.2,\n amplitude = 0.3,\n frequencyX = 3,\n frequencyY = 3,\n interactive = true,\n ...props\n}) => {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const container = containerRef.current;\n const renderer = new Renderer({ antialias: true });\n const gl = renderer.gl;\n gl.clearColor(1, 1, 1, 1);\n\n const vertexShader = `\n attribute vec2 position;\n attribute vec2 uv;\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = vec4(position, 0.0, 1.0);\n }\n `;\n\n const fragmentShader = `\n precision highp float;\n uniform float uTime;\n uniform vec3 uResolution;\n uniform vec3 uBaseColor;\n uniform float uAmplitude;\n uniform float uFrequencyX;\n uniform float uFrequencyY;\n uniform vec2 uMouse;\n varying vec2 vUv;\n\n vec4 renderImage(vec2 uvCoord) {\n vec2 fragCoord = uvCoord * uResolution.xy;\n vec2 uv = (2.0 * fragCoord - uResolution.xy) / min(uResolution.x, uResolution.y);\n\n for (float i = 1.0; i < 10.0; i++){\n uv.x += uAmplitude / i * cos(i * uFrequencyX * uv.y + uTime + uMouse.x * 3.14159);\n uv.y += uAmplitude / i * cos(i * uFrequencyY * uv.x + uTime + uMouse.y * 3.14159);\n }\n\n vec2 diff = (uvCoord - uMouse);\n float dist = length(diff);\n float falloff = exp(-dist * 20.0);\n float ripple = sin(10.0 * dist - uTime * 2.0) * 0.03;\n uv += (diff / (dist + 0.0001)) * ripple * falloff;\n\n vec3 color = uBaseColor / abs(sin(uTime - uv.y - uv.x));\n return vec4(color, 1.0);\n }\n\n void main() {\n vec4 col = vec4(0.0);\n int samples = 0;\n for (int i = -1; i <= 1; i++){\n for (int j = -1; j <= 1; j++){\n vec2 offset = vec2(float(i), float(j)) * (1.0 / min(uResolution.x, uResolution.y));\n col += renderImage(vUv + offset);\n samples++;\n }\n }\n gl_FragColor = col / float(samples);\n }\n `;\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uResolution: {\n value: new Float32Array([gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height])\n },\n uBaseColor: { value: new Float32Array(baseColor) },\n uAmplitude: { value: amplitude },\n uFrequencyX: { value: frequencyX },\n uFrequencyY: { value: frequencyY },\n uMouse: { value: new Float32Array([0, 0]) }\n }\n });\n const mesh = new Mesh(gl, { geometry, program });\n\n function resize() {\n const scale = 1;\n renderer.setSize(container.offsetWidth * scale, container.offsetHeight * scale);\n const resUniform = program.uniforms.uResolution.value;\n resUniform[0] = gl.canvas.width;\n resUniform[1] = gl.canvas.height;\n resUniform[2] = gl.canvas.width / gl.canvas.height;\n }\n window.addEventListener('resize', resize);\n resize();\n\n function handleMouseMove(event) {\n const rect = container.getBoundingClientRect();\n const x = (event.clientX - rect.left) / rect.width;\n const y = 1 - (event.clientY - rect.top) / rect.height;\n const mouseUniform = program.uniforms.uMouse.value;\n mouseUniform[0] = x;\n mouseUniform[1] = y;\n }\n\n function handleTouchMove(event) {\n if (event.touches.length > 0) {\n const touch = event.touches[0];\n const rect = container.getBoundingClientRect();\n const x = (touch.clientX - rect.left) / rect.width;\n const y = 1 - (touch.clientY - rect.top) / rect.height;\n const mouseUniform = program.uniforms.uMouse.value;\n mouseUniform[0] = x;\n mouseUniform[1] = y;\n }\n }\n\n if (interactive) {\n container.addEventListener('mousemove', handleMouseMove);\n container.addEventListener('touchmove', handleTouchMove);\n }\n\n let animationId;\n function update(t) {\n animationId = requestAnimationFrame(update);\n program.uniforms.uTime.value = t * 0.001 * speed;\n renderer.render({ scene: mesh });\n }\n animationId = requestAnimationFrame(update);\n\n container.appendChild(gl.canvas);\n\n return () => {\n cancelAnimationFrame(animationId);\n window.removeEventListener('resize', resize);\n if (interactive) {\n container.removeEventListener('mousemove', handleMouseMove);\n container.removeEventListener('touchmove', handleTouchMove);\n }\n if (gl.canvas.parentElement) {\n gl.canvas.parentElement.removeChild(gl.canvas);\n }\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, [baseColor, speed, amplitude, frequencyX, frequencyY, interactive]);\n\n return
;\n};\n\nexport default LiquidChrome;\n" } ], diff --git a/public/r/LiquidChrome-TS-CSS.json b/public/r/LiquidChrome-TS-CSS.json index 5a051a30..26cb52a9 100644 --- a/public/r/LiquidChrome-TS-CSS.json +++ b/public/r/LiquidChrome-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "LiquidChrome/LiquidChrome.css", + "type": "registry:file", + "path": "LiquidChrome.css", + "target": "@components/LiquidChrome.css", "content": ".liquidChrome-container {\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "LiquidChrome/LiquidChrome.tsx", + "path": "LiquidChrome.tsx", "content": "import React, { useRef, useEffect } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\n\nimport './LiquidChrome.css';\n\ninterface LiquidChromeProps extends React.HTMLAttributes {\n baseColor?: [number, number, number];\n speed?: number;\n amplitude?: number;\n frequencyX?: number;\n frequencyY?: number;\n interactive?: boolean;\n}\n\nexport const LiquidChrome: React.FC = ({\n baseColor = [0.1, 0.1, 0.1],\n speed = 0.2,\n amplitude = 0.5,\n frequencyX = 3,\n frequencyY = 2,\n interactive = true,\n ...props\n}) => {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const container = containerRef.current;\n const renderer = new Renderer({ antialias: true });\n const gl = renderer.gl;\n gl.clearColor(1, 1, 1, 1);\n\n const vertexShader = `\n attribute vec2 position;\n attribute vec2 uv;\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = vec4(position, 0.0, 1.0);\n }\n `;\n\n const fragmentShader = `\n precision highp float;\n uniform float uTime;\n uniform vec3 uResolution;\n uniform vec3 uBaseColor;\n uniform float uAmplitude;\n uniform float uFrequencyX;\n uniform float uFrequencyY;\n uniform vec2 uMouse;\n varying vec2 vUv;\n\n vec4 renderImage(vec2 uvCoord) {\n vec2 fragCoord = uvCoord * uResolution.xy;\n vec2 uv = (2.0 * fragCoord - uResolution.xy) / min(uResolution.x, uResolution.y);\n\n for (float i = 1.0; i < 10.0; i++){\n uv.x += uAmplitude / i * cos(i * uFrequencyX * uv.y + uTime + uMouse.x * 3.14159);\n uv.y += uAmplitude / i * cos(i * uFrequencyY * uv.x + uTime + uMouse.y * 3.14159);\n }\n\n vec2 diff = (uvCoord - uMouse);\n float dist = length(diff);\n float falloff = exp(-dist * 20.0);\n float ripple = sin(10.0 * dist - uTime * 2.0) * 0.03;\n uv += (diff / (dist + 0.0001)) * ripple * falloff;\n\n vec3 color = uBaseColor / abs(sin(uTime - uv.y - uv.x));\n return vec4(color, 1.0);\n }\n\n void main() {\n vec4 col = vec4(0.0);\n int samples = 0;\n for (int i = -1; i <= 1; i++){\n for (int j = -1; j <= 1; j++){\n vec2 offset = vec2(float(i), float(j)) * (1.0 / min(uResolution.x, uResolution.y));\n col += renderImage(vUv + offset);\n samples++;\n }\n }\n gl_FragColor = col / float(samples);\n }\n `;\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uResolution: {\n value: new Float32Array([gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height])\n },\n uBaseColor: { value: new Float32Array(baseColor) },\n uAmplitude: { value: amplitude },\n uFrequencyX: { value: frequencyX },\n uFrequencyY: { value: frequencyY },\n uMouse: { value: new Float32Array([0, 0]) }\n }\n });\n const mesh = new Mesh(gl, { geometry, program });\n\n function resize() {\n const scale = 1;\n renderer.setSize(container.offsetWidth * scale, container.offsetHeight * scale);\n const resUniform = program.uniforms.uResolution.value as Float32Array;\n resUniform[0] = gl.canvas.width;\n resUniform[1] = gl.canvas.height;\n resUniform[2] = gl.canvas.width / gl.canvas.height;\n }\n window.addEventListener('resize', resize);\n resize();\n\n function handleMouseMove(event: MouseEvent) {\n const rect = container.getBoundingClientRect();\n const x = (event.clientX - rect.left) / rect.width;\n const y = 1 - (event.clientY - rect.top) / rect.height;\n const mouseUniform = program.uniforms.uMouse.value as Float32Array;\n mouseUniform[0] = x;\n mouseUniform[1] = y;\n }\n\n function handleTouchMove(event: TouchEvent) {\n if (event.touches.length > 0) {\n const touch = event.touches[0];\n const rect = container.getBoundingClientRect();\n const x = (touch.clientX - rect.left) / rect.width;\n const y = 1 - (touch.clientY - rect.top) / rect.height;\n const mouseUniform = program.uniforms.uMouse.value as Float32Array;\n mouseUniform[0] = x;\n mouseUniform[1] = y;\n }\n }\n\n if (interactive) {\n container.addEventListener('mousemove', handleMouseMove);\n container.addEventListener('touchmove', handleTouchMove);\n }\n\n let animationId: number;\n function update(t: number) {\n animationId = requestAnimationFrame(update);\n program.uniforms.uTime.value = t * 0.001 * speed;\n renderer.render({ scene: mesh });\n }\n animationId = requestAnimationFrame(update);\n\n container.appendChild(gl.canvas);\n\n return () => {\n cancelAnimationFrame(animationId);\n window.removeEventListener('resize', resize);\n if (interactive) {\n container.removeEventListener('mousemove', handleMouseMove);\n container.removeEventListener('touchmove', handleTouchMove);\n }\n if (gl.canvas.parentElement) {\n gl.canvas.parentElement.removeChild(gl.canvas);\n }\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, [baseColor, speed, amplitude, frequencyX, frequencyY, interactive]);\n\n return
;\n};\n\nexport default LiquidChrome;\n" } ], diff --git a/public/r/LiquidEther-JS-CSS.json b/public/r/LiquidEther-JS-CSS.json index 4ad13ce0..ff689fd6 100644 --- a/public/r/LiquidEther-JS-CSS.json +++ b/public/r/LiquidEther-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "LiquidEther/LiquidEther.css", + "type": "registry:file", + "path": "LiquidEther.css", + "target": "@components/LiquidEther.css", "content": ".liquid-ether-container {\n position: relative;\n overflow: hidden;\n width: 100%;\n height: 100%;\n touch-action: none;\n}\n" }, { "type": "registry:component", - "path": "LiquidEther/LiquidEther.jsx", + "path": "LiquidEther.jsx", "content": "import { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\nimport './LiquidEther.css';\n\nexport default function LiquidEther({\n mouseForce = 20,\n cursorSize = 100,\n isViscous = false,\n viscous = 30,\n iterationsViscous = 32,\n iterationsPoisson = 32,\n dt = 0.014,\n BFECC = true,\n resolution = 0.5,\n isBounce = false,\n colors = ['#5227FF', '#FF9FFC', '#B497CF'],\n style = {},\n className = '',\n autoDemo = true,\n autoSpeed = 0.5,\n autoIntensity = 2.2,\n takeoverDuration = 0.25,\n autoResumeDelay = 1000,\n autoRampDuration = 0.6\n}) {\n const mountRef = useRef(null);\n const webglRef = useRef(null);\n const resizeObserverRef = useRef(null);\n const rafRef = useRef(null);\n const intersectionObserverRef = useRef(null);\n const isVisibleRef = useRef(true);\n const resizeRafRef = useRef(null);\n\n useEffect(() => {\n if (!mountRef.current) return;\n\n function makePaletteTexture(stops) {\n let arr;\n if (Array.isArray(stops) && stops.length > 0) {\n if (stops.length === 1) {\n arr = [stops[0], stops[0]];\n } else {\n arr = stops;\n }\n } else {\n arr = ['#ffffff', '#ffffff'];\n }\n const w = arr.length;\n const data = new Uint8Array(w * 4);\n for (let i = 0; i < w; i++) {\n const c = new THREE.Color(arr[i]);\n data[i * 4 + 0] = Math.round(c.r * 255);\n data[i * 4 + 1] = Math.round(c.g * 255);\n data[i * 4 + 2] = Math.round(c.b * 255);\n data[i * 4 + 3] = 255;\n }\n const tex = new THREE.DataTexture(data, w, 1, THREE.RGBAFormat);\n tex.magFilter = THREE.LinearFilter;\n tex.minFilter = THREE.LinearFilter;\n tex.wrapS = THREE.ClampToEdgeWrapping;\n tex.wrapT = THREE.ClampToEdgeWrapping;\n tex.generateMipmaps = false;\n tex.needsUpdate = true;\n return tex;\n }\n\n const paletteTex = makePaletteTexture(colors);\n const bgVec4 = new THREE.Vector4(0, 0, 0, 0); // always transparent\n\n class CommonClass {\n constructor() {\n this.width = 0;\n this.height = 0;\n this.aspect = 1;\n this.pixelRatio = 1;\n this.isMobile = false;\n this.breakpoint = 768;\n this.fboWidth = null;\n this.fboHeight = null;\n this.time = 0;\n this.delta = 0;\n this.container = null;\n this.renderer = null;\n this.clock = null;\n }\n init(container) {\n this.container = container;\n this.pixelRatio = Math.min(window.devicePixelRatio || 1, 2);\n this.resize();\n this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });\n this.renderer.autoClear = false;\n this.renderer.setClearColor(new THREE.Color(0x000000), 0);\n this.renderer.setPixelRatio(this.pixelRatio);\n this.renderer.setSize(this.width, this.height);\n this.renderer.domElement.style.width = '100%';\n this.renderer.domElement.style.height = '100%';\n this.renderer.domElement.style.display = 'block';\n this.clock = new THREE.Clock();\n this.clock.start();\n }\n resize() {\n if (!this.container) return;\n const rect = this.container.getBoundingClientRect();\n this.width = Math.max(1, Math.floor(rect.width));\n this.height = Math.max(1, Math.floor(rect.height));\n this.aspect = this.width / this.height;\n if (this.renderer) this.renderer.setSize(this.width, this.height, false);\n }\n update() {\n this.delta = this.clock.getDelta();\n this.time += this.delta;\n }\n }\n const Common = new CommonClass();\n\n class MouseClass {\n constructor() {\n this.mouseMoved = false;\n this.coords = new THREE.Vector2();\n this.coords_old = new THREE.Vector2();\n this.diff = new THREE.Vector2();\n this.timer = null;\n this.container = null;\n this.docTarget = null;\n this.listenerTarget = null;\n this.isHoverInside = false;\n this.hasUserControl = false;\n this.isAutoActive = false;\n this.autoIntensity = 2.0;\n this.takeoverActive = false;\n this.takeoverStartTime = 0;\n this.takeoverDuration = 0.25;\n this.takeoverFrom = new THREE.Vector2();\n this.takeoverTo = new THREE.Vector2();\n this.onInteract = null;\n this._onMouseMove = this.onDocumentMouseMove.bind(this);\n this._onTouchStart = this.onDocumentTouchStart.bind(this);\n this._onTouchMove = this.onDocumentTouchMove.bind(this);\n this._onTouchEnd = this.onTouchEnd.bind(this);\n this._onDocumentLeave = this.onDocumentLeave.bind(this);\n }\n init(container) {\n this.container = container;\n this.docTarget = container.ownerDocument || null;\n const defaultView =\n (this.docTarget && this.docTarget.defaultView) || (typeof window !== 'undefined' ? window : null);\n if (!defaultView) return;\n this.listenerTarget = defaultView;\n this.listenerTarget.addEventListener('mousemove', this._onMouseMove);\n this.listenerTarget.addEventListener('touchstart', this._onTouchStart, { passive: true });\n this.listenerTarget.addEventListener('touchmove', this._onTouchMove, { passive: true });\n this.listenerTarget.addEventListener('touchend', this._onTouchEnd);\n if (this.docTarget) {\n this.docTarget.addEventListener('mouseleave', this._onDocumentLeave);\n }\n }\n dispose() {\n if (this.listenerTarget) {\n this.listenerTarget.removeEventListener('mousemove', this._onMouseMove);\n this.listenerTarget.removeEventListener('touchstart', this._onTouchStart);\n this.listenerTarget.removeEventListener('touchmove', this._onTouchMove);\n this.listenerTarget.removeEventListener('touchend', this._onTouchEnd);\n }\n if (this.docTarget) {\n this.docTarget.removeEventListener('mouseleave', this._onDocumentLeave);\n }\n this.listenerTarget = null;\n this.docTarget = null;\n this.container = null;\n }\n isPointInside(clientX, clientY) {\n if (!this.container) return false;\n const rect = this.container.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return false;\n return clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom;\n }\n updateHoverState(clientX, clientY) {\n this.isHoverInside = this.isPointInside(clientX, clientY);\n return this.isHoverInside;\n }\n setCoords(x, y) {\n if (!this.container) return;\n if (this.timer) window.clearTimeout(this.timer);\n const rect = this.container.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return;\n const nx = (x - rect.left) / rect.width;\n const ny = (y - rect.top) / rect.height;\n this.coords.set(nx * 2 - 1, -(ny * 2 - 1));\n this.mouseMoved = true;\n this.timer = window.setTimeout(() => {\n this.mouseMoved = false;\n }, 100);\n }\n setNormalized(nx, ny) {\n this.coords.set(nx, ny);\n this.mouseMoved = true;\n }\n onDocumentMouseMove(event) {\n if (!this.updateHoverState(event.clientX, event.clientY)) return;\n if (this.onInteract) this.onInteract();\n if (this.isAutoActive && !this.hasUserControl && !this.takeoverActive) {\n if (!this.container) return;\n const rect = this.container.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return;\n const nx = (event.clientX - rect.left) / rect.width;\n const ny = (event.clientY - rect.top) / rect.height;\n this.takeoverFrom.copy(this.coords);\n this.takeoverTo.set(nx * 2 - 1, -(ny * 2 - 1));\n this.takeoverStartTime = performance.now();\n this.takeoverActive = true;\n this.hasUserControl = true;\n this.isAutoActive = false;\n return;\n }\n this.setCoords(event.clientX, event.clientY);\n this.hasUserControl = true;\n }\n onDocumentTouchStart(event) {\n if (event.touches.length !== 1) return;\n const t = event.touches[0];\n if (!this.updateHoverState(t.clientX, t.clientY)) return;\n if (this.onInteract) this.onInteract();\n this.setCoords(t.clientX, t.clientY);\n this.hasUserControl = true;\n }\n onDocumentTouchMove(event) {\n if (event.touches.length !== 1) return;\n const t = event.touches[0];\n if (!this.updateHoverState(t.clientX, t.clientY)) return;\n if (this.onInteract) this.onInteract();\n this.setCoords(t.clientX, t.clientY);\n }\n onTouchEnd() {\n this.isHoverInside = false;\n }\n onDocumentLeave() {\n this.isHoverInside = false;\n }\n update() {\n if (this.takeoverActive) {\n const t = (performance.now() - this.takeoverStartTime) / (this.takeoverDuration * 1000);\n if (t >= 1) {\n this.takeoverActive = false;\n this.coords.copy(this.takeoverTo);\n this.coords_old.copy(this.coords);\n this.diff.set(0, 0);\n } else {\n const k = t * t * (3 - 2 * t);\n this.coords.copy(this.takeoverFrom).lerp(this.takeoverTo, k);\n }\n }\n this.diff.subVectors(this.coords, this.coords_old);\n this.coords_old.copy(this.coords);\n if (this.coords_old.x === 0 && this.coords_old.y === 0) this.diff.set(0, 0);\n if (this.isAutoActive && !this.takeoverActive) this.diff.multiplyScalar(this.autoIntensity);\n }\n }\n const Mouse = new MouseClass();\n\n class AutoDriver {\n constructor(mouse, manager, opts) {\n this.mouse = mouse;\n this.manager = manager;\n this.enabled = opts.enabled;\n this.speed = opts.speed; // normalized units/sec\n this.resumeDelay = opts.resumeDelay || 3000; // ms\n this.rampDurationMs = (opts.rampDuration || 0) * 1000;\n this.active = false;\n this.current = new THREE.Vector2(0, 0);\n this.target = new THREE.Vector2();\n this.lastTime = performance.now();\n this.activationTime = 0;\n this.margin = 0.2;\n this._tmpDir = new THREE.Vector2(); // reuse temp vector to avoid per-frame alloc\n this.pickNewTarget();\n }\n pickNewTarget() {\n const r = Math.random;\n this.target.set((r() * 2 - 1) * (1 - this.margin), (r() * 2 - 1) * (1 - this.margin));\n }\n forceStop() {\n this.active = false;\n this.mouse.isAutoActive = false;\n }\n update() {\n if (!this.enabled) return;\n const now = performance.now();\n const idle = now - this.manager.lastUserInteraction;\n if (idle < this.resumeDelay) {\n if (this.active) this.forceStop();\n return;\n }\n if (this.mouse.isHoverInside) {\n if (this.active) this.forceStop();\n return;\n }\n if (!this.active) {\n this.active = true;\n this.current.copy(this.mouse.coords);\n this.lastTime = now;\n this.activationTime = now;\n }\n if (!this.active) return;\n this.mouse.isAutoActive = true;\n let dtSec = (now - this.lastTime) / 1000;\n this.lastTime = now;\n if (dtSec > 0.2) dtSec = 0.016;\n const dir = this._tmpDir.subVectors(this.target, this.current);\n const dist = dir.length();\n if (dist < 0.01) {\n this.pickNewTarget();\n return;\n }\n dir.normalize();\n let ramp = 1;\n if (this.rampDurationMs > 0) {\n const t = Math.min(1, (now - this.activationTime) / this.rampDurationMs);\n ramp = t * t * (3 - 2 * t);\n }\n const step = this.speed * dtSec * ramp;\n const move = Math.min(step, dist);\n this.current.addScaledVector(dir, move);\n this.mouse.setNormalized(this.current.x, this.current.y);\n }\n }\n\n const face_vert = `\n attribute vec3 position;\n uniform vec2 px;\n uniform vec2 boundarySpace;\n varying vec2 uv;\n precision highp float;\n void main(){\n vec3 pos = position;\n vec2 scale = 1.0 - boundarySpace * 2.0;\n pos.xy = pos.xy * scale;\n uv = vec2(0.5)+(pos.xy)*0.5;\n gl_Position = vec4(pos, 1.0);\n}\n`;\n const line_vert = `\n attribute vec3 position;\n uniform vec2 px;\n precision highp float;\n varying vec2 uv;\n void main(){\n vec3 pos = position;\n uv = 0.5 + pos.xy * 0.5;\n vec2 n = sign(pos.xy);\n pos.xy = abs(pos.xy) - px * 1.0;\n pos.xy *= n;\n gl_Position = vec4(pos, 1.0);\n}\n`;\n const mouse_vert = `\n precision highp float;\n attribute vec3 position;\n attribute vec2 uv;\n uniform vec2 center;\n uniform vec2 scale;\n uniform vec2 px;\n varying vec2 vUv;\n void main(){\n vec2 pos = position.xy * scale * 2.0 * px + center;\n vUv = uv;\n gl_Position = vec4(pos, 0.0, 1.0);\n}\n`;\n const advection_frag = `\n precision highp float;\n uniform sampler2D velocity;\n uniform float dt;\n uniform bool isBFECC;\n uniform vec2 fboSize;\n uniform vec2 px;\n varying vec2 uv;\n void main(){\n vec2 ratio = max(fboSize.x, fboSize.y) / fboSize;\n if(isBFECC == false){\n vec2 vel = texture2D(velocity, uv).xy;\n vec2 uv2 = uv - vel * dt * ratio;\n vec2 newVel = texture2D(velocity, uv2).xy;\n gl_FragColor = vec4(newVel, 0.0, 0.0);\n } else {\n vec2 spot_new = uv;\n vec2 vel_old = texture2D(velocity, uv).xy;\n vec2 spot_old = spot_new - vel_old * dt * ratio;\n vec2 vel_new1 = texture2D(velocity, spot_old).xy;\n vec2 spot_new2 = spot_old + vel_new1 * dt * ratio;\n vec2 error = spot_new2 - spot_new;\n vec2 spot_new3 = spot_new - error / 2.0;\n vec2 vel_2 = texture2D(velocity, spot_new3).xy;\n vec2 spot_old2 = spot_new3 - vel_2 * dt * ratio;\n vec2 newVel2 = texture2D(velocity, spot_old2).xy; \n gl_FragColor = vec4(newVel2, 0.0, 0.0);\n }\n}\n`;\n const color_frag = `\n precision highp float;\n uniform sampler2D velocity;\n uniform sampler2D palette;\n uniform vec4 bgColor;\n varying vec2 uv;\n void main(){\n vec2 vel = texture2D(velocity, uv).xy;\n float lenv = clamp(length(vel), 0.0, 1.0);\n vec3 c = texture2D(palette, vec2(lenv, 0.5)).rgb;\n vec3 outRGB = mix(bgColor.rgb, c, lenv);\n float outA = mix(bgColor.a, 1.0, lenv);\n gl_FragColor = vec4(outRGB, outA);\n}\n`;\n const divergence_frag = `\n precision highp float;\n uniform sampler2D velocity;\n uniform float dt;\n uniform vec2 px;\n varying vec2 uv;\n void main(){\n float x0 = texture2D(velocity, uv-vec2(px.x, 0.0)).x;\n float x1 = texture2D(velocity, uv+vec2(px.x, 0.0)).x;\n float y0 = texture2D(velocity, uv-vec2(0.0, px.y)).y;\n float y1 = texture2D(velocity, uv+vec2(0.0, px.y)).y;\n float divergence = (x1 - x0 + y1 - y0) / 2.0;\n gl_FragColor = vec4(divergence / dt);\n}\n`;\n const externalForce_frag = `\n precision highp float;\n uniform vec2 force;\n uniform vec2 center;\n uniform vec2 scale;\n uniform vec2 px;\n varying vec2 vUv;\n void main(){\n vec2 circle = (vUv - 0.5) * 2.0;\n float d = 1.0 - min(length(circle), 1.0);\n d *= d;\n gl_FragColor = vec4(force * d, 0.0, 1.0);\n}\n`;\n const poisson_frag = `\n precision highp float;\n uniform sampler2D pressure;\n uniform sampler2D divergence;\n uniform vec2 px;\n varying vec2 uv;\n void main(){\n float p0 = texture2D(pressure, uv + vec2(px.x * 2.0, 0.0)).r;\n float p1 = texture2D(pressure, uv - vec2(px.x * 2.0, 0.0)).r;\n float p2 = texture2D(pressure, uv + vec2(0.0, px.y * 2.0)).r;\n float p3 = texture2D(pressure, uv - vec2(0.0, px.y * 2.0)).r;\n float div = texture2D(divergence, uv).r;\n float newP = (p0 + p1 + p2 + p3) / 4.0 - div;\n gl_FragColor = vec4(newP);\n}\n`;\n const pressure_frag = `\n precision highp float;\n uniform sampler2D pressure;\n uniform sampler2D velocity;\n uniform vec2 px;\n uniform float dt;\n varying vec2 uv;\n void main(){\n float step = 1.0;\n float p0 = texture2D(pressure, uv + vec2(px.x * step, 0.0)).r;\n float p1 = texture2D(pressure, uv - vec2(px.x * step, 0.0)).r;\n float p2 = texture2D(pressure, uv + vec2(0.0, px.y * step)).r;\n float p3 = texture2D(pressure, uv - vec2(0.0, px.y * step)).r;\n vec2 v = texture2D(velocity, uv).xy;\n vec2 gradP = vec2(p0 - p1, p2 - p3) * 0.5;\n v = v - gradP * dt;\n gl_FragColor = vec4(v, 0.0, 1.0);\n}\n`;\n const viscous_frag = `\n precision highp float;\n uniform sampler2D velocity;\n uniform sampler2D velocity_new;\n uniform float v;\n uniform vec2 px;\n uniform float dt;\n varying vec2 uv;\n void main(){\n vec2 old = texture2D(velocity, uv).xy;\n vec2 new0 = texture2D(velocity_new, uv + vec2(px.x * 2.0, 0.0)).xy;\n vec2 new1 = texture2D(velocity_new, uv - vec2(px.x * 2.0, 0.0)).xy;\n vec2 new2 = texture2D(velocity_new, uv + vec2(0.0, px.y * 2.0)).xy;\n vec2 new3 = texture2D(velocity_new, uv - vec2(0.0, px.y * 2.0)).xy;\n vec2 newv = 4.0 * old + v * dt * (new0 + new1 + new2 + new3);\n newv /= 4.0 * (1.0 + v * dt);\n gl_FragColor = vec4(newv, 0.0, 0.0);\n}\n`;\n\n class ShaderPass {\n constructor(props) {\n this.props = props || {};\n this.uniforms = this.props.material?.uniforms;\n this.scene = null;\n this.camera = null;\n this.material = null;\n this.geometry = null;\n this.plane = null;\n }\n init() {\n this.scene = new THREE.Scene();\n this.camera = new THREE.Camera();\n if (this.uniforms) {\n this.material = new THREE.RawShaderMaterial(this.props.material);\n this.geometry = new THREE.PlaneGeometry(2.0, 2.0);\n this.plane = new THREE.Mesh(this.geometry, this.material);\n this.scene.add(this.plane);\n }\n }\n update() {\n Common.renderer.setRenderTarget(this.props.output || null);\n Common.renderer.render(this.scene, this.camera);\n Common.renderer.setRenderTarget(null);\n }\n }\n\n class Advection extends ShaderPass {\n constructor(simProps) {\n super({\n material: {\n vertexShader: face_vert,\n fragmentShader: advection_frag,\n uniforms: {\n boundarySpace: { value: simProps.cellScale },\n px: { value: simProps.cellScale },\n fboSize: { value: simProps.fboSize },\n velocity: { value: simProps.src.texture },\n dt: { value: simProps.dt },\n isBFECC: { value: true }\n }\n },\n output: simProps.dst\n });\n this.uniforms = this.props.material.uniforms;\n this.init();\n }\n init() {\n super.init();\n this.createBoundary();\n }\n createBoundary() {\n const boundaryG = new THREE.BufferGeometry();\n const vertices_boundary = new Float32Array([\n -1, -1, 0, -1, 1, 0, -1, 1, 0, 1, 1, 0, 1, 1, 0, 1, -1, 0, 1, -1, 0, -1, -1, 0\n ]);\n boundaryG.setAttribute('position', new THREE.BufferAttribute(vertices_boundary, 3));\n const boundaryM = new THREE.RawShaderMaterial({\n vertexShader: line_vert,\n fragmentShader: advection_frag,\n uniforms: this.uniforms\n });\n this.line = new THREE.LineSegments(boundaryG, boundaryM);\n this.scene.add(this.line);\n }\n update({ dt, isBounce, BFECC }) {\n this.uniforms.dt.value = dt;\n this.line.visible = isBounce;\n this.uniforms.isBFECC.value = BFECC;\n super.update();\n }\n }\n\n class ExternalForce extends ShaderPass {\n constructor(simProps) {\n super({ output: simProps.dst });\n this.init(simProps);\n }\n init(simProps) {\n super.init();\n const mouseG = new THREE.PlaneGeometry(1, 1);\n const mouseM = new THREE.RawShaderMaterial({\n vertexShader: mouse_vert,\n fragmentShader: externalForce_frag,\n blending: THREE.AdditiveBlending,\n depthWrite: false,\n uniforms: {\n px: { value: simProps.cellScale },\n force: { value: new THREE.Vector2(0.0, 0.0) },\n center: { value: new THREE.Vector2(0.0, 0.0) },\n scale: { value: new THREE.Vector2(simProps.cursor_size, simProps.cursor_size) }\n }\n });\n this.mouse = new THREE.Mesh(mouseG, mouseM);\n this.scene.add(this.mouse);\n }\n update(props) {\n const forceX = (Mouse.diff.x / 2) * props.mouse_force;\n const forceY = (Mouse.diff.y / 2) * props.mouse_force;\n const cursorSizeX = props.cursor_size * props.cellScale.x;\n const cursorSizeY = props.cursor_size * props.cellScale.y;\n const centerX = Math.min(\n Math.max(Mouse.coords.x, -1 + cursorSizeX + props.cellScale.x * 2),\n 1 - cursorSizeX - props.cellScale.x * 2\n );\n const centerY = Math.min(\n Math.max(Mouse.coords.y, -1 + cursorSizeY + props.cellScale.y * 2),\n 1 - cursorSizeY - props.cellScale.y * 2\n );\n const uniforms = this.mouse.material.uniforms;\n uniforms.force.value.set(forceX, forceY);\n uniforms.center.value.set(centerX, centerY);\n uniforms.scale.value.set(props.cursor_size, props.cursor_size);\n super.update();\n }\n }\n\n class Viscous extends ShaderPass {\n constructor(simProps) {\n super({\n material: {\n vertexShader: face_vert,\n fragmentShader: viscous_frag,\n uniforms: {\n boundarySpace: { value: simProps.boundarySpace },\n velocity: { value: simProps.src.texture },\n velocity_new: { value: simProps.dst_.texture },\n v: { value: simProps.viscous },\n px: { value: simProps.cellScale },\n dt: { value: simProps.dt }\n }\n },\n output: simProps.dst,\n output0: simProps.dst_,\n output1: simProps.dst\n });\n this.init();\n }\n update({ viscous, iterations, dt }) {\n let fbo_in, fbo_out;\n this.uniforms.v.value = viscous;\n for (let i = 0; i < iterations; i++) {\n if (i % 2 === 0) {\n fbo_in = this.props.output0;\n fbo_out = this.props.output1;\n } else {\n fbo_in = this.props.output1;\n fbo_out = this.props.output0;\n }\n this.uniforms.velocity_new.value = fbo_in.texture;\n this.props.output = fbo_out;\n this.uniforms.dt.value = dt;\n super.update();\n }\n return fbo_out;\n }\n }\n\n class Divergence extends ShaderPass {\n constructor(simProps) {\n super({\n material: {\n vertexShader: face_vert,\n fragmentShader: divergence_frag,\n uniforms: {\n boundarySpace: { value: simProps.boundarySpace },\n velocity: { value: simProps.src.texture },\n px: { value: simProps.cellScale },\n dt: { value: simProps.dt }\n }\n },\n output: simProps.dst\n });\n this.init();\n }\n update({ vel }) {\n this.uniforms.velocity.value = vel.texture;\n super.update();\n }\n }\n\n class Poisson extends ShaderPass {\n constructor(simProps) {\n super({\n material: {\n vertexShader: face_vert,\n fragmentShader: poisson_frag,\n uniforms: {\n boundarySpace: { value: simProps.boundarySpace },\n pressure: { value: simProps.dst_.texture },\n divergence: { value: simProps.src.texture },\n px: { value: simProps.cellScale }\n }\n },\n output: simProps.dst,\n output0: simProps.dst_,\n output1: simProps.dst\n });\n this.init();\n }\n update({ iterations }) {\n let p_in, p_out;\n for (let i = 0; i < iterations; i++) {\n if (i % 2 === 0) {\n p_in = this.props.output0;\n p_out = this.props.output1;\n } else {\n p_in = this.props.output1;\n p_out = this.props.output0;\n }\n this.uniforms.pressure.value = p_in.texture;\n this.props.output = p_out;\n super.update();\n }\n return p_out;\n }\n }\n\n class Pressure extends ShaderPass {\n constructor(simProps) {\n super({\n material: {\n vertexShader: face_vert,\n fragmentShader: pressure_frag,\n uniforms: {\n boundarySpace: { value: simProps.boundarySpace },\n pressure: { value: simProps.src_p.texture },\n velocity: { value: simProps.src_v.texture },\n px: { value: simProps.cellScale },\n dt: { value: simProps.dt }\n }\n },\n output: simProps.dst\n });\n this.init();\n }\n update({ vel, pressure }) {\n this.uniforms.velocity.value = vel.texture;\n this.uniforms.pressure.value = pressure.texture;\n super.update();\n }\n }\n\n class Simulation {\n constructor(options) {\n this.options = {\n iterations_poisson: 32,\n iterations_viscous: 32,\n mouse_force: 20,\n resolution: 0.5,\n cursor_size: 100,\n viscous: 30,\n isBounce: false,\n dt: 0.014,\n isViscous: false,\n BFECC: true,\n ...options\n };\n this.fbos = {\n vel_0: null,\n vel_1: null,\n vel_viscous0: null,\n vel_viscous1: null,\n div: null,\n pressure_0: null,\n pressure_1: null\n };\n this.fboSize = new THREE.Vector2();\n this.cellScale = new THREE.Vector2();\n this.boundarySpace = new THREE.Vector2();\n this.init();\n }\n init() {\n this.calcSize();\n this.createAllFBO();\n this.createShaderPass();\n }\n getFloatType() {\n const isIOS = /(iPad|iPhone|iPod)/i.test(navigator.userAgent);\n return isIOS ? THREE.HalfFloatType : THREE.FloatType;\n }\n createAllFBO() {\n const type = this.getFloatType();\n const opts = {\n type,\n depthBuffer: false,\n stencilBuffer: false,\n minFilter: THREE.LinearFilter,\n magFilter: THREE.LinearFilter,\n wrapS: THREE.ClampToEdgeWrapping,\n wrapT: THREE.ClampToEdgeWrapping\n };\n for (let key in this.fbos) {\n this.fbos[key] = new THREE.WebGLRenderTarget(this.fboSize.x, this.fboSize.y, opts);\n }\n }\n createShaderPass() {\n this.advection = new Advection({\n cellScale: this.cellScale,\n fboSize: this.fboSize,\n dt: this.options.dt,\n src: this.fbos.vel_0,\n dst: this.fbos.vel_1\n });\n this.externalForce = new ExternalForce({\n cellScale: this.cellScale,\n cursor_size: this.options.cursor_size,\n dst: this.fbos.vel_1\n });\n this.viscous = new Viscous({\n cellScale: this.cellScale,\n boundarySpace: this.boundarySpace,\n viscous: this.options.viscous,\n src: this.fbos.vel_1,\n dst: this.fbos.vel_viscous1,\n dst_: this.fbos.vel_viscous0,\n dt: this.options.dt\n });\n this.divergence = new Divergence({\n cellScale: this.cellScale,\n boundarySpace: this.boundarySpace,\n src: this.fbos.vel_viscous0,\n dst: this.fbos.div,\n dt: this.options.dt\n });\n this.poisson = new Poisson({\n cellScale: this.cellScale,\n boundarySpace: this.boundarySpace,\n src: this.fbos.div,\n dst: this.fbos.pressure_1,\n dst_: this.fbos.pressure_0\n });\n this.pressure = new Pressure({\n cellScale: this.cellScale,\n boundarySpace: this.boundarySpace,\n src_p: this.fbos.pressure_0,\n src_v: this.fbos.vel_viscous0,\n dst: this.fbos.vel_0,\n dt: this.options.dt\n });\n }\n calcSize() {\n const width = Math.max(1, Math.round(this.options.resolution * Common.width));\n const height = Math.max(1, Math.round(this.options.resolution * Common.height));\n const px_x = 1.0 / width;\n const px_y = 1.0 / height;\n this.cellScale.set(px_x, px_y);\n this.fboSize.set(width, height);\n }\n resize() {\n this.calcSize();\n for (let key in this.fbos) {\n this.fbos[key].setSize(this.fboSize.x, this.fboSize.y);\n }\n }\n update() {\n if (this.options.isBounce) {\n this.boundarySpace.set(0, 0);\n } else {\n this.boundarySpace.copy(this.cellScale);\n }\n this.advection.update({\n dt: this.options.dt,\n isBounce: this.options.isBounce,\n BFECC: this.options.BFECC\n });\n this.externalForce.update({\n cursor_size: this.options.cursor_size,\n mouse_force: this.options.mouse_force,\n cellScale: this.cellScale\n });\n let vel = this.fbos.vel_1;\n if (this.options.isViscous) {\n vel = this.viscous.update({\n viscous: this.options.viscous,\n iterations: this.options.iterations_viscous,\n dt: this.options.dt\n });\n }\n this.divergence.update({ vel });\n const pressure = this.poisson.update({\n iterations: this.options.iterations_poisson\n });\n this.pressure.update({ vel, pressure });\n }\n }\n\n class Output {\n constructor() {\n this.init();\n }\n init() {\n this.simulation = new Simulation();\n this.scene = new THREE.Scene();\n this.camera = new THREE.Camera();\n this.output = new THREE.Mesh(\n new THREE.PlaneGeometry(2, 2),\n new THREE.RawShaderMaterial({\n vertexShader: face_vert,\n fragmentShader: color_frag,\n transparent: true,\n depthWrite: false,\n uniforms: {\n velocity: { value: this.simulation.fbos.vel_0.texture },\n boundarySpace: { value: new THREE.Vector2() },\n palette: { value: paletteTex },\n bgColor: { value: bgVec4 }\n }\n })\n );\n this.scene.add(this.output);\n }\n addScene(mesh) {\n this.scene.add(mesh);\n }\n resize() {\n this.simulation.resize();\n }\n render() {\n Common.renderer.setRenderTarget(null);\n Common.renderer.render(this.scene, this.camera);\n }\n update() {\n this.simulation.update();\n this.render();\n }\n }\n\n class WebGLManager {\n constructor(props) {\n this.props = props;\n Common.init(props.$wrapper);\n Mouse.init(props.$wrapper);\n Mouse.autoIntensity = props.autoIntensity;\n Mouse.takeoverDuration = props.takeoverDuration;\n this.lastUserInteraction = performance.now();\n Mouse.onInteract = () => {\n this.lastUserInteraction = performance.now();\n if (this.autoDriver) this.autoDriver.forceStop();\n };\n this.autoDriver = new AutoDriver(Mouse, this, {\n enabled: props.autoDemo,\n speed: props.autoSpeed,\n resumeDelay: props.autoResumeDelay,\n rampDuration: props.autoRampDuration\n });\n this.init();\n this._loop = this.loop.bind(this);\n this._resize = this.resize.bind(this);\n window.addEventListener('resize', this._resize);\n this._onVisibility = () => {\n const hidden = document.hidden;\n if (hidden) {\n this.pause();\n } else if (isVisibleRef.current) {\n this.start();\n }\n };\n document.addEventListener('visibilitychange', this._onVisibility);\n this.running = false;\n }\n init() {\n this.props.$wrapper.prepend(Common.renderer.domElement);\n this.output = new Output();\n }\n resize() {\n Common.resize();\n this.output.resize();\n }\n render() {\n if (this.autoDriver) this.autoDriver.update();\n Mouse.update();\n Common.update();\n this.output.update();\n }\n loop() {\n if (!this.running) return; // safety\n this.render();\n rafRef.current = requestAnimationFrame(this._loop);\n }\n start() {\n if (this.running) return;\n this.running = true;\n this._loop();\n }\n pause() {\n this.running = false;\n if (rafRef.current) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n }\n dispose() {\n try {\n window.removeEventListener('resize', this._resize);\n document.removeEventListener('visibilitychange', this._onVisibility);\n Mouse.dispose();\n if (Common.renderer) {\n const canvas = Common.renderer.domElement;\n if (canvas && canvas.parentNode) canvas.parentNode.removeChild(canvas);\n Common.renderer.dispose();\n Common.renderer.forceContextLoss();\n }\n } catch (e) {\n void 0;\n }\n }\n }\n\n const container = mountRef.current;\n container.style.position = container.style.position || 'relative';\n container.style.overflow = container.style.overflow || 'hidden';\n\n const webgl = new WebGLManager({\n $wrapper: container,\n autoDemo,\n autoSpeed,\n autoIntensity,\n takeoverDuration,\n autoResumeDelay,\n autoRampDuration\n });\n webglRef.current = webgl;\n\n const applyOptionsFromProps = () => {\n if (!webglRef.current) return;\n const sim = webglRef.current.output?.simulation;\n if (!sim) return;\n const prevRes = sim.options.resolution;\n Object.assign(sim.options, {\n mouse_force: mouseForce,\n cursor_size: cursorSize,\n isViscous,\n viscous,\n iterations_viscous: iterationsViscous,\n iterations_poisson: iterationsPoisson,\n dt,\n BFECC,\n resolution,\n isBounce\n });\n if (resolution !== prevRes) {\n sim.resize();\n }\n };\n applyOptionsFromProps();\n\n webgl.start();\n\n // IntersectionObserver to pause rendering when not visible\n const io = new IntersectionObserver(\n entries => {\n const entry = entries[0];\n const isVisible = entry.isIntersecting && entry.intersectionRatio > 0;\n isVisibleRef.current = isVisible;\n if (!webglRef.current) return;\n if (isVisible && !document.hidden) {\n webglRef.current.start();\n } else {\n webglRef.current.pause();\n }\n },\n { threshold: [0, 0.01, 0.1] }\n );\n io.observe(container);\n intersectionObserverRef.current = io;\n\n const ro = new ResizeObserver(() => {\n if (!webglRef.current) return;\n if (resizeRafRef.current) cancelAnimationFrame(resizeRafRef.current);\n resizeRafRef.current = requestAnimationFrame(() => {\n if (!webglRef.current) return;\n webglRef.current.resize();\n });\n });\n ro.observe(container);\n resizeObserverRef.current = ro;\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n if (resizeObserverRef.current) {\n try {\n resizeObserverRef.current.disconnect();\n } catch (e) {\n void 0;\n }\n }\n if (intersectionObserverRef.current) {\n try {\n intersectionObserverRef.current.disconnect();\n } catch (e) {\n void 0;\n }\n }\n if (webglRef.current) {\n webglRef.current.dispose();\n }\n webglRef.current = null;\n };\n }, [\n BFECC,\n cursorSize,\n dt,\n isBounce,\n isViscous,\n iterationsPoisson,\n iterationsViscous,\n mouseForce,\n resolution,\n viscous,\n colors,\n autoDemo,\n autoSpeed,\n autoIntensity,\n takeoverDuration,\n autoResumeDelay,\n autoRampDuration\n ]);\n\n useEffect(() => {\n const webgl = webglRef.current;\n if (!webgl) return;\n const sim = webgl.output?.simulation;\n if (!sim) return;\n const prevRes = sim.options.resolution;\n Object.assign(sim.options, {\n mouse_force: mouseForce,\n cursor_size: cursorSize,\n isViscous,\n viscous,\n iterations_viscous: iterationsViscous,\n iterations_poisson: iterationsPoisson,\n dt,\n BFECC,\n resolution,\n isBounce\n });\n if (webgl.autoDriver) {\n webgl.autoDriver.enabled = autoDemo;\n webgl.autoDriver.speed = autoSpeed;\n webgl.autoDriver.resumeDelay = autoResumeDelay;\n webgl.autoDriver.rampDurationMs = autoRampDuration * 1000;\n if (webgl.autoDriver.mouse) {\n webgl.autoDriver.mouse.autoIntensity = autoIntensity;\n webgl.autoDriver.mouse.takeoverDuration = takeoverDuration;\n }\n }\n if (resolution !== prevRes) {\n sim.resize();\n }\n }, [\n mouseForce,\n cursorSize,\n isViscous,\n viscous,\n iterationsViscous,\n iterationsPoisson,\n dt,\n BFECC,\n resolution,\n isBounce,\n autoDemo,\n autoSpeed,\n autoIntensity,\n takeoverDuration,\n autoResumeDelay,\n autoRampDuration\n ]);\n\n return
;\n}\n" } ], diff --git a/public/r/LiquidEther-TS-CSS.json b/public/r/LiquidEther-TS-CSS.json index 0caf2786..cdd32312 100644 --- a/public/r/LiquidEther-TS-CSS.json +++ b/public/r/LiquidEther-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "LiquidEther/LiquidEther.css", + "type": "registry:file", + "path": "LiquidEther.css", + "target": "@components/LiquidEther.css", "content": ".liquid-ether-container {\n position: relative;\n overflow: hidden;\n width: 100%;\n height: 100%;\n touch-action: none;\n}\n" }, { "type": "registry:component", - "path": "LiquidEther/LiquidEther.tsx", + "path": "LiquidEther.tsx", "content": "import React, { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\nimport './LiquidEther.css';\n\nexport interface LiquidEtherProps {\n mouseForce?: number;\n cursorSize?: number;\n isViscous?: boolean;\n viscous?: number;\n iterationsViscous?: number;\n iterationsPoisson?: number;\n dt?: number;\n BFECC?: boolean;\n resolution?: number;\n isBounce?: boolean;\n colors?: string[];\n style?: React.CSSProperties;\n className?: string;\n autoDemo?: boolean;\n autoSpeed?: number;\n autoIntensity?: number;\n takeoverDuration?: number;\n autoResumeDelay?: number;\n autoRampDuration?: number;\n}\n\ninterface SimOptions {\n iterations_poisson: number;\n iterations_viscous: number;\n mouse_force: number;\n resolution: number;\n cursor_size: number;\n viscous: number;\n isBounce: boolean;\n dt: number;\n isViscous: boolean;\n BFECC: boolean;\n}\n\ninterface LiquidEtherWebGL {\n output?: { simulation?: { options: SimOptions; resize: () => void } };\n autoDriver?: {\n enabled: boolean;\n speed: number;\n resumeDelay: number;\n rampDurationMs: number;\n mouse?: { autoIntensity: number; takeoverDuration: number };\n forceStop: () => void;\n };\n resize: () => void;\n start: () => void;\n pause: () => void;\n dispose: () => void;\n}\n\nconst defaultColors = ['#5227FF', '#FF9FFC', '#B497CF'];\n\nexport default function LiquidEther({\n mouseForce = 20,\n cursorSize = 100,\n isViscous = false,\n viscous = 30,\n iterationsViscous = 32,\n iterationsPoisson = 32,\n dt = 0.014,\n BFECC = true,\n resolution = 0.5,\n isBounce = false,\n colors = defaultColors,\n style = {},\n className = '',\n autoDemo = true,\n autoSpeed = 0.5,\n autoIntensity = 2.2,\n takeoverDuration = 0.25,\n autoResumeDelay = 1000,\n autoRampDuration = 0.6\n}: LiquidEtherProps): React.ReactElement {\n const mountRef = useRef(null);\n const webglRef = useRef(null);\n const resizeObserverRef = useRef(null);\n const rafRef = useRef(null);\n const intersectionObserverRef = useRef(null);\n const isVisibleRef = useRef(true);\n const resizeRafRef = useRef(null);\n\n useEffect(() => {\n if (!mountRef.current) return;\n\n function makePaletteTexture(stops: string[]): THREE.DataTexture {\n let arr: string[];\n if (Array.isArray(stops) && stops.length > 0) {\n arr = stops.length === 1 ? [stops[0], stops[0]] : stops;\n } else {\n arr = ['#ffffff', '#ffffff'];\n }\n const w = arr.length;\n const data = new Uint8Array(w * 4);\n for (let i = 0; i < w; i++) {\n const c = new THREE.Color(arr[i]);\n data[i * 4 + 0] = Math.round(c.r * 255);\n data[i * 4 + 1] = Math.round(c.g * 255);\n data[i * 4 + 2] = Math.round(c.b * 255);\n data[i * 4 + 3] = 255;\n }\n const tex = new THREE.DataTexture(data, w, 1, THREE.RGBAFormat);\n tex.magFilter = THREE.LinearFilter;\n tex.minFilter = THREE.LinearFilter;\n tex.wrapS = THREE.ClampToEdgeWrapping;\n tex.wrapT = THREE.ClampToEdgeWrapping;\n tex.generateMipmaps = false;\n tex.needsUpdate = true;\n return tex;\n }\n\n const paletteTex = makePaletteTexture(colors);\n // Hard-code transparent background vector (alpha 0)\n const bgVec4 = new THREE.Vector4(0, 0, 0, 0);\n\n class CommonClass {\n width = 0;\n height = 0;\n aspect = 1;\n pixelRatio = 1;\n isMobile = false;\n breakpoint = 768;\n fboWidth: number | null = null;\n fboHeight: number | null = null;\n time = 0;\n delta = 0;\n container: HTMLElement | null = null;\n renderer: THREE.WebGLRenderer | null = null;\n clock: THREE.Clock | null = null;\n init(container: HTMLElement) {\n this.container = container;\n this.pixelRatio = Math.min(window.devicePixelRatio || 1, 2);\n this.resize();\n this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });\n // Always transparent\n this.renderer.autoClear = false;\n this.renderer.setClearColor(new THREE.Color(0x000000), 0);\n this.renderer.setPixelRatio(this.pixelRatio);\n this.renderer.setSize(this.width, this.height);\n const el = this.renderer.domElement;\n el.style.width = '100%';\n el.style.height = '100%';\n el.style.display = 'block';\n this.clock = new THREE.Clock();\n this.clock.start();\n }\n resize() {\n if (!this.container) return;\n const rect = this.container.getBoundingClientRect();\n this.width = Math.max(1, Math.floor(rect.width));\n this.height = Math.max(1, Math.floor(rect.height));\n this.aspect = this.width / this.height;\n if (this.renderer) this.renderer.setSize(this.width, this.height, false);\n }\n update() {\n if (!this.clock) return;\n this.delta = this.clock.getDelta();\n this.time += this.delta;\n }\n }\n const Common = new CommonClass();\n\n class MouseClass {\n mouseMoved = false;\n coords = new THREE.Vector2();\n coords_old = new THREE.Vector2();\n diff = new THREE.Vector2();\n timer: number | null = null;\n container: HTMLElement | null = null;\n docTarget: Document | null = null;\n listenerTarget: Window | null = null;\n isHoverInside = false;\n hasUserControl = false;\n isAutoActive = false;\n autoIntensity = 2.0;\n takeoverActive = false;\n takeoverStartTime = 0;\n takeoverDuration = 0.25;\n takeoverFrom = new THREE.Vector2();\n takeoverTo = new THREE.Vector2();\n onInteract: (() => void) | null = null;\n private _onMouseMove = this.onDocumentMouseMove.bind(this);\n private _onTouchStart = this.onDocumentTouchStart.bind(this);\n private _onTouchMove = this.onDocumentTouchMove.bind(this);\n private _onTouchEnd = this.onTouchEnd.bind(this);\n private _onDocumentLeave = this.onDocumentLeave.bind(this);\n init(container: HTMLElement) {\n this.container = container;\n this.docTarget = container.ownerDocument || null;\n const defaultView = this.docTarget?.defaultView || (typeof window !== 'undefined' ? window : null);\n if (!defaultView) return;\n this.listenerTarget = defaultView;\n this.listenerTarget.addEventListener('mousemove', this._onMouseMove);\n this.listenerTarget.addEventListener('touchstart', this._onTouchStart, {\n passive: true\n });\n this.listenerTarget.addEventListener('touchmove', this._onTouchMove, {\n passive: true\n });\n this.listenerTarget.addEventListener('touchend', this._onTouchEnd);\n this.docTarget?.addEventListener('mouseleave', this._onDocumentLeave);\n }\n dispose() {\n if (this.listenerTarget) {\n this.listenerTarget.removeEventListener('mousemove', this._onMouseMove);\n this.listenerTarget.removeEventListener('touchstart', this._onTouchStart);\n this.listenerTarget.removeEventListener('touchmove', this._onTouchMove);\n this.listenerTarget.removeEventListener('touchend', this._onTouchEnd);\n }\n if (this.docTarget) {\n this.docTarget.removeEventListener('mouseleave', this._onDocumentLeave);\n }\n this.listenerTarget = null;\n this.docTarget = null;\n this.container = null;\n }\n private isPointInside(clientX: number, clientY: number) {\n if (!this.container) return false;\n const rect = this.container.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return false;\n return clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom;\n }\n private updateHoverState(clientX: number, clientY: number) {\n this.isHoverInside = this.isPointInside(clientX, clientY);\n return this.isHoverInside;\n }\n setCoords(x: number, y: number) {\n if (!this.container) return;\n if (this.timer) window.clearTimeout(this.timer);\n const rect = this.container.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return;\n const nx = (x - rect.left) / rect.width;\n const ny = (y - rect.top) / rect.height;\n this.coords.set(nx * 2 - 1, -(ny * 2 - 1));\n this.mouseMoved = true;\n this.timer = window.setTimeout(() => {\n this.mouseMoved = false;\n }, 100);\n }\n setNormalized(nx: number, ny: number) {\n this.coords.set(nx, ny);\n this.mouseMoved = true;\n }\n onDocumentMouseMove(event: MouseEvent) {\n if (!this.updateHoverState(event.clientX, event.clientY)) return;\n if (this.onInteract) this.onInteract();\n if (this.isAutoActive && !this.hasUserControl && !this.takeoverActive) {\n if (!this.container) return;\n const rect = this.container.getBoundingClientRect();\n const nx = (event.clientX - rect.left) / rect.width;\n const ny = (event.clientY - rect.top) / rect.height;\n this.takeoverFrom.copy(this.coords);\n this.takeoverTo.set(nx * 2 - 1, -(ny * 2 - 1));\n this.takeoverStartTime = performance.now();\n this.takeoverActive = true;\n this.hasUserControl = true;\n this.isAutoActive = false;\n return;\n }\n this.setCoords(event.clientX, event.clientY);\n this.hasUserControl = true;\n }\n onDocumentTouchStart(event: TouchEvent) {\n if (event.touches.length !== 1) return;\n const t = event.touches[0];\n if (!this.updateHoverState(t.clientX, t.clientY)) return;\n if (this.onInteract) this.onInteract();\n this.setCoords(t.clientX, t.clientY);\n this.hasUserControl = true;\n }\n onDocumentTouchMove(event: TouchEvent) {\n if (event.touches.length !== 1) return;\n const t = event.touches[0];\n if (!this.updateHoverState(t.clientX, t.clientY)) return;\n if (this.onInteract) this.onInteract();\n this.setCoords(t.clientX, t.clientY);\n }\n onTouchEnd() {\n this.isHoverInside = false;\n }\n onDocumentLeave() {\n this.isHoverInside = false;\n }\n update() {\n if (this.takeoverActive) {\n const t = (performance.now() - this.takeoverStartTime) / (this.takeoverDuration * 1000);\n if (t >= 1) {\n this.takeoverActive = false;\n this.coords.copy(this.takeoverTo);\n this.coords_old.copy(this.coords);\n this.diff.set(0, 0);\n } else {\n const k = t * t * (3 - 2 * t);\n this.coords.copy(this.takeoverFrom).lerp(this.takeoverTo, k);\n }\n }\n this.diff.subVectors(this.coords, this.coords_old);\n this.coords_old.copy(this.coords);\n if (this.coords_old.x === 0 && this.coords_old.y === 0) this.diff.set(0, 0);\n if (this.isAutoActive && !this.takeoverActive) this.diff.multiplyScalar(this.autoIntensity);\n }\n }\n const Mouse = new MouseClass();\n\n class AutoDriver {\n mouse: MouseClass;\n manager: WebGLManager;\n enabled: boolean;\n speed: number;\n resumeDelay: number;\n rampDurationMs: number;\n active = false;\n current = new THREE.Vector2(0, 0);\n target = new THREE.Vector2();\n lastTime = performance.now();\n activationTime = 0;\n margin = 0.2;\n private _tmpDir = new THREE.Vector2();\n constructor(\n mouse: MouseClass,\n manager: WebGLManager,\n opts: { enabled: boolean; speed: number; resumeDelay: number; rampDuration: number }\n ) {\n this.mouse = mouse;\n this.manager = manager;\n this.enabled = opts.enabled;\n this.speed = opts.speed;\n this.resumeDelay = opts.resumeDelay || 3000;\n this.rampDurationMs = (opts.rampDuration || 0) * 1000;\n this.pickNewTarget();\n }\n pickNewTarget() {\n const r = Math.random;\n this.target.set((r() * 2 - 1) * (1 - this.margin), (r() * 2 - 1) * (1 - this.margin));\n }\n forceStop() {\n this.active = false;\n this.mouse.isAutoActive = false;\n }\n update() {\n if (!this.enabled) return;\n const now = performance.now();\n const idle = now - this.manager.lastUserInteraction;\n if (idle < this.resumeDelay) {\n if (this.active) this.forceStop();\n return;\n }\n if (this.mouse.isHoverInside) {\n if (this.active) this.forceStop();\n return;\n }\n if (!this.active) {\n this.active = true;\n this.current.copy(this.mouse.coords);\n this.lastTime = now;\n this.activationTime = now;\n }\n if (!this.active) return;\n this.mouse.isAutoActive = true;\n let dtSec = (now - this.lastTime) / 1000;\n this.lastTime = now;\n if (dtSec > 0.2) dtSec = 0.016;\n const dir = this._tmpDir.subVectors(this.target, this.current);\n const dist = dir.length();\n if (dist < 0.01) {\n this.pickNewTarget();\n return;\n }\n dir.normalize();\n let ramp = 1;\n if (this.rampDurationMs > 0) {\n const t = Math.min(1, (now - this.activationTime) / this.rampDurationMs);\n ramp = t * t * (3 - 2 * t);\n }\n const step = this.speed * dtSec * ramp;\n const move = Math.min(step, dist);\n this.current.addScaledVector(dir, move);\n this.mouse.setNormalized(this.current.x, this.current.y);\n }\n }\n\n const face_vert = `\n\tattribute vec3 position;\n\tuniform vec2 px;\n\tuniform vec2 boundarySpace;\n\tvarying vec2 uv;\n\tprecision highp float;\n\tvoid main(){\n\tvec3 pos = position;\n\tvec2 scale = 1.0 - boundarySpace * 2.0;\n\tpos.xy = pos.xy * scale;\n\tuv = vec2(0.5)+(pos.xy)*0.5;\n\tgl_Position = vec4(pos, 1.0);\n}\n`;\n const line_vert = `\n\tattribute vec3 position;\n\tuniform vec2 px;\n\tprecision highp float;\n\tvarying vec2 uv;\n\tvoid main(){\n\tvec3 pos = position;\n\tuv = 0.5 + pos.xy * 0.5;\n\tvec2 n = sign(pos.xy);\n\tpos.xy = abs(pos.xy) - px * 1.0;\n\tpos.xy *= n;\n\tgl_Position = vec4(pos, 1.0);\n}\n`;\n const mouse_vert = `\n\t\tprecision highp float;\n\t\tattribute vec3 position;\n\t\tattribute vec2 uv;\n\t\tuniform vec2 center;\n\t\tuniform vec2 scale;\n\t\tuniform vec2 px;\n\t\tvarying vec2 vUv;\n\t\tvoid main(){\n\t\tvec2 pos = position.xy * scale * 2.0 * px + center;\n\t\tvUv = uv;\n\t\tgl_Position = vec4(pos, 0.0, 1.0);\n}\n`;\n const advection_frag = `\n\t\tprecision highp float;\n\t\tuniform sampler2D velocity;\n\t\tuniform float dt;\n\t\tuniform bool isBFECC;\n\t\tuniform vec2 fboSize;\n\t\tuniform vec2 px;\n\t\tvarying vec2 uv;\n\t\tvoid main(){\n\t\tvec2 ratio = max(fboSize.x, fboSize.y) / fboSize;\n\t\tif(isBFECC == false){\n\t\t\t\tvec2 vel = texture2D(velocity, uv).xy;\n\t\t\t\tvec2 uv2 = uv - vel * dt * ratio;\n\t\t\t\tvec2 newVel = texture2D(velocity, uv2).xy;\n\t\t\t\tgl_FragColor = vec4(newVel, 0.0, 0.0);\n\t\t} else {\n\t\t\t\tvec2 spot_new = uv;\n\t\t\t\tvec2 vel_old = texture2D(velocity, uv).xy;\n\t\t\t\tvec2 spot_old = spot_new - vel_old * dt * ratio;\n\t\t\t\tvec2 vel_new1 = texture2D(velocity, spot_old).xy;\n\t\t\t\tvec2 spot_new2 = spot_old + vel_new1 * dt * ratio;\n\t\t\t\tvec2 error = spot_new2 - spot_new;\n\t\t\t\tvec2 spot_new3 = spot_new - error / 2.0;\n\t\t\t\tvec2 vel_2 = texture2D(velocity, spot_new3).xy;\n\t\t\t\tvec2 spot_old2 = spot_new3 - vel_2 * dt * ratio;\n\t\t\t\tvec2 newVel2 = texture2D(velocity, spot_old2).xy; \n\t\t\t\tgl_FragColor = vec4(newVel2, 0.0, 0.0);\n\t\t}\n}\n`;\n const color_frag = `\n\t\tprecision highp float;\n\t\tuniform sampler2D velocity;\n\t\tuniform sampler2D palette;\n\t\tuniform vec4 bgColor;\n\t\tvarying vec2 uv;\n\t\tvoid main(){\n\t\tvec2 vel = texture2D(velocity, uv).xy;\n\t\tfloat lenv = clamp(length(vel), 0.0, 1.0);\n\t\tvec3 c = texture2D(palette, vec2(lenv, 0.5)).rgb;\n\t\tvec3 outRGB = mix(bgColor.rgb, c, lenv);\n\t\tfloat outA = mix(bgColor.a, 1.0, lenv);\n\t\tgl_FragColor = vec4(outRGB, outA);\n}\n`;\n const divergence_frag = `\n\t\tprecision highp float;\n\t\tuniform sampler2D velocity;\n\t\tuniform float dt;\n\t\tuniform vec2 px;\n\t\tvarying vec2 uv;\n\t\tvoid main(){\n\t\tfloat x0 = texture2D(velocity, uv-vec2(px.x, 0.0)).x;\n\t\tfloat x1 = texture2D(velocity, uv+vec2(px.x, 0.0)).x;\n\t\tfloat y0 = texture2D(velocity, uv-vec2(0.0, px.y)).y;\n\t\tfloat y1 = texture2D(velocity, uv+vec2(0.0, px.y)).y;\n\t\tfloat divergence = (x1 - x0 + y1 - y0) / 2.0;\n\t\tgl_FragColor = vec4(divergence / dt);\n}\n`;\n const externalForce_frag = `\n\t\tprecision highp float;\n\t\tuniform vec2 force;\n\t\tuniform vec2 center;\n\t\tuniform vec2 scale;\n\t\tuniform vec2 px;\n\t\tvarying vec2 vUv;\n\t\tvoid main(){\n\t\tvec2 circle = (vUv - 0.5) * 2.0;\n\t\tfloat d = 1.0 - min(length(circle), 1.0);\n\t\td *= d;\n\t\tgl_FragColor = vec4(force * d, 0.0, 1.0);\n}\n`;\n const poisson_frag = `\n\t\tprecision highp float;\n\t\tuniform sampler2D pressure;\n\t\tuniform sampler2D divergence;\n\t\tuniform vec2 px;\n\t\tvarying vec2 uv;\n\t\tvoid main(){\n\t\tfloat p0 = texture2D(pressure, uv + vec2(px.x * 2.0, 0.0)).r;\n\t\tfloat p1 = texture2D(pressure, uv - vec2(px.x * 2.0, 0.0)).r;\n\t\tfloat p2 = texture2D(pressure, uv + vec2(0.0, px.y * 2.0)).r;\n\t\tfloat p3 = texture2D(pressure, uv - vec2(0.0, px.y * 2.0)).r;\n\t\tfloat div = texture2D(divergence, uv).r;\n\t\tfloat newP = (p0 + p1 + p2 + p3) / 4.0 - div;\n\t\tgl_FragColor = vec4(newP);\n}\n`;\n const pressure_frag = `\n\t\tprecision highp float;\n\t\tuniform sampler2D pressure;\n\t\tuniform sampler2D velocity;\n\t\tuniform vec2 px;\n\t\tuniform float dt;\n\t\tvarying vec2 uv;\n\t\tvoid main(){\n\t\tfloat step = 1.0;\n\t\tfloat p0 = texture2D(pressure, uv + vec2(px.x * step, 0.0)).r;\n\t\tfloat p1 = texture2D(pressure, uv - vec2(px.x * step, 0.0)).r;\n\t\tfloat p2 = texture2D(pressure, uv + vec2(0.0, px.y * step)).r;\n\t\tfloat p3 = texture2D(pressure, uv - vec2(0.0, px.y * step)).r;\n\t\tvec2 v = texture2D(velocity, uv).xy;\n\t\tvec2 gradP = vec2(p0 - p1, p2 - p3) * 0.5;\n\t\tv = v - gradP * dt;\n\t\tgl_FragColor = vec4(v, 0.0, 1.0);\n}\n`;\n const viscous_frag = `\n\t\tprecision highp float;\n\t\tuniform sampler2D velocity;\n\t\tuniform sampler2D velocity_new;\n\t\tuniform float v;\n\t\tuniform vec2 px;\n\t\tuniform float dt;\n\t\tvarying vec2 uv;\n\t\tvoid main(){\n\t\tvec2 old = texture2D(velocity, uv).xy;\n\t\tvec2 new0 = texture2D(velocity_new, uv + vec2(px.x * 2.0, 0.0)).xy;\n\t\tvec2 new1 = texture2D(velocity_new, uv - vec2(px.x * 2.0, 0.0)).xy;\n\t\tvec2 new2 = texture2D(velocity_new, uv + vec2(0.0, px.y * 2.0)).xy;\n\t\tvec2 new3 = texture2D(velocity_new, uv - vec2(0.0, px.y * 2.0)).xy;\n\t\tvec2 newv = 4.0 * old + v * dt * (new0 + new1 + new2 + new3);\n\t\tnewv /= 4.0 * (1.0 + v * dt);\n\t\tgl_FragColor = vec4(newv, 0.0, 0.0);\n}\n`;\n\n type Uniforms = Record;\n\n class ShaderPass {\n props: any;\n uniforms?: Uniforms;\n scene: THREE.Scene | null = null;\n camera: THREE.Camera | null = null;\n material: THREE.RawShaderMaterial | null = null;\n geometry: THREE.BufferGeometry | null = null;\n plane: THREE.Mesh | null = null;\n constructor(props: any) {\n this.props = props || {};\n this.uniforms = this.props.material?.uniforms;\n }\n init(..._args: any[]) {\n this.scene = new THREE.Scene();\n this.camera = new THREE.Camera();\n if (this.uniforms) {\n this.material = new THREE.RawShaderMaterial(this.props.material);\n this.geometry = new THREE.PlaneGeometry(2, 2);\n this.plane = new THREE.Mesh(this.geometry, this.material);\n this.scene.add(this.plane);\n }\n }\n update(..._args: any[]) {\n if (!Common.renderer || !this.scene || !this.camera) return;\n Common.renderer.setRenderTarget(this.props.output || null);\n Common.renderer.render(this.scene, this.camera);\n Common.renderer.setRenderTarget(null);\n }\n }\n\n class Advection extends ShaderPass {\n line!: THREE.LineSegments;\n constructor(simProps: any) {\n super({\n material: {\n vertexShader: face_vert,\n fragmentShader: advection_frag,\n uniforms: {\n boundarySpace: { value: simProps.cellScale },\n px: { value: simProps.cellScale },\n fboSize: { value: simProps.fboSize },\n velocity: { value: simProps.src.texture },\n dt: { value: simProps.dt },\n isBFECC: { value: true }\n }\n },\n output: simProps.dst\n });\n this.uniforms = this.props.material.uniforms;\n this.init();\n }\n init() {\n super.init();\n this.createBoundary();\n }\n createBoundary() {\n const boundaryG = new THREE.BufferGeometry();\n const vertices_boundary = new Float32Array([\n -1, -1, 0, -1, 1, 0, -1, 1, 0, 1, 1, 0, 1, 1, 0, 1, -1, 0, 1, -1, 0, -1, -1, 0\n ]);\n boundaryG.setAttribute('position', new THREE.BufferAttribute(vertices_boundary, 3));\n const boundaryM = new THREE.RawShaderMaterial({\n vertexShader: line_vert,\n fragmentShader: advection_frag,\n uniforms: this.uniforms!\n });\n this.line = new THREE.LineSegments(boundaryG, boundaryM);\n this.scene!.add(this.line);\n }\n update(...args: any[]) {\n const { dt, isBounce, BFECC } = (args[0] || {}) as { dt?: number; isBounce?: boolean; BFECC?: boolean };\n if (!this.uniforms) return;\n if (typeof dt === 'number') this.uniforms.dt.value = dt;\n if (typeof isBounce === 'boolean') this.line.visible = isBounce;\n if (typeof BFECC === 'boolean') this.uniforms.isBFECC.value = BFECC;\n super.update();\n }\n }\n\n class ExternalForce extends ShaderPass {\n mouse!: THREE.Mesh;\n constructor(simProps: any) {\n super({ output: simProps.dst });\n this.init(simProps);\n }\n init(simProps: any) {\n super.init();\n const mouseG = new THREE.PlaneGeometry(1, 1);\n const mouseM = new THREE.RawShaderMaterial({\n vertexShader: mouse_vert,\n fragmentShader: externalForce_frag,\n blending: THREE.AdditiveBlending,\n depthWrite: false,\n uniforms: {\n px: { value: simProps.cellScale },\n force: { value: new THREE.Vector2(0, 0) },\n center: { value: new THREE.Vector2(0, 0) },\n scale: { value: new THREE.Vector2(simProps.cursor_size, simProps.cursor_size) }\n }\n });\n this.mouse = new THREE.Mesh(mouseG, mouseM);\n this.scene!.add(this.mouse);\n }\n update(...args: any[]) {\n const props = args[0] || {};\n const forceX = (Mouse.diff.x / 2) * (props.mouse_force || 0);\n const forceY = (Mouse.diff.y / 2) * (props.mouse_force || 0);\n const cellScale = props.cellScale || { x: 1, y: 1 };\n const cursorSize = props.cursor_size || 0;\n const cursorSizeX = cursorSize * cellScale.x;\n const cursorSizeY = cursorSize * cellScale.y;\n const centerX = Math.min(\n Math.max(Mouse.coords.x, -1 + cursorSizeX + cellScale.x * 2),\n 1 - cursorSizeX - cellScale.x * 2\n );\n const centerY = Math.min(\n Math.max(Mouse.coords.y, -1 + cursorSizeY + cellScale.y * 2),\n 1 - cursorSizeY - cellScale.y * 2\n );\n const uniforms = (this.mouse.material as THREE.RawShaderMaterial).uniforms;\n uniforms.force.value.set(forceX, forceY);\n uniforms.center.value.set(centerX, centerY);\n uniforms.scale.value.set(cursorSize, cursorSize);\n super.update();\n }\n }\n\n class Viscous extends ShaderPass {\n constructor(simProps: any) {\n super({\n material: {\n vertexShader: face_vert,\n fragmentShader: viscous_frag,\n uniforms: {\n boundarySpace: { value: simProps.boundarySpace },\n velocity: { value: simProps.src.texture },\n velocity_new: { value: simProps.dst_.texture },\n v: { value: simProps.viscous },\n px: { value: simProps.cellScale },\n dt: { value: simProps.dt }\n }\n },\n output: simProps.dst,\n output0: simProps.dst_,\n output1: simProps.dst\n });\n this.init();\n }\n update(...args: any[]) {\n const { viscous, iterations, dt } = (args[0] || {}) as { viscous?: number; iterations?: number; dt?: number };\n if (!this.uniforms) return;\n let fbo_in: any, fbo_out: any;\n if (typeof viscous === 'number') this.uniforms.v.value = viscous;\n const iter = iterations ?? 0;\n for (let i = 0; i < iter; i++) {\n if (i % 2 === 0) {\n fbo_in = this.props.output0;\n fbo_out = this.props.output1;\n } else {\n fbo_in = this.props.output1;\n fbo_out = this.props.output0;\n }\n this.uniforms.velocity_new.value = fbo_in.texture;\n this.props.output = fbo_out;\n if (typeof dt === 'number') this.uniforms.dt.value = dt;\n super.update();\n }\n return fbo_out;\n }\n }\n\n class Divergence extends ShaderPass {\n constructor(simProps: any) {\n super({\n material: {\n vertexShader: face_vert,\n fragmentShader: divergence_frag,\n uniforms: {\n boundarySpace: { value: simProps.boundarySpace },\n velocity: { value: simProps.src.texture },\n px: { value: simProps.cellScale },\n dt: { value: simProps.dt }\n }\n },\n output: simProps.dst\n });\n this.init();\n }\n update(...args: any[]) {\n const { vel } = (args[0] || {}) as { vel?: any };\n if (this.uniforms && vel) {\n this.uniforms.velocity.value = vel.texture;\n }\n super.update();\n }\n }\n\n class Poisson extends ShaderPass {\n constructor(simProps: any) {\n super({\n material: {\n vertexShader: face_vert,\n fragmentShader: poisson_frag,\n uniforms: {\n boundarySpace: { value: simProps.boundarySpace },\n pressure: { value: simProps.dst_.texture },\n divergence: { value: simProps.src.texture },\n px: { value: simProps.cellScale }\n }\n },\n output: simProps.dst,\n output0: simProps.dst_,\n output1: simProps.dst\n });\n this.init();\n }\n update(...args: any[]) {\n const { iterations } = (args[0] || {}) as { iterations?: number };\n let p_in: any, p_out: any;\n const iter = iterations ?? 0;\n for (let i = 0; i < iter; i++) {\n if (i % 2 === 0) {\n p_in = this.props.output0;\n p_out = this.props.output1;\n } else {\n p_in = this.props.output1;\n p_out = this.props.output0;\n }\n if (this.uniforms) this.uniforms.pressure.value = p_in.texture;\n this.props.output = p_out;\n super.update();\n }\n return p_out;\n }\n }\n\n class Pressure extends ShaderPass {\n constructor(simProps: any) {\n super({\n material: {\n vertexShader: face_vert,\n fragmentShader: pressure_frag,\n uniforms: {\n boundarySpace: { value: simProps.boundarySpace },\n pressure: { value: simProps.src_p.texture },\n velocity: { value: simProps.src_v.texture },\n px: { value: simProps.cellScale },\n dt: { value: simProps.dt }\n }\n },\n output: simProps.dst\n });\n this.init();\n }\n update(...args: any[]) {\n const { vel, pressure } = (args[0] || {}) as { vel?: any; pressure?: any };\n if (this.uniforms && vel && pressure) {\n this.uniforms.velocity.value = vel.texture;\n this.uniforms.pressure.value = pressure.texture;\n }\n super.update();\n }\n }\n\n class Simulation {\n options: SimOptions;\n fbos: Record = {\n vel_0: null,\n vel_1: null,\n vel_viscous0: null,\n vel_viscous1: null,\n div: null,\n pressure_0: null,\n pressure_1: null\n };\n fboSize = new THREE.Vector2();\n cellScale = new THREE.Vector2();\n boundarySpace = new THREE.Vector2();\n advection!: Advection;\n externalForce!: ExternalForce;\n viscous!: Viscous;\n divergence!: Divergence;\n poisson!: Poisson;\n pressure!: Pressure;\n constructor(options?: Partial) {\n this.options = {\n iterations_poisson: 32,\n iterations_viscous: 32,\n mouse_force: 20,\n resolution: 0.5,\n cursor_size: 100,\n viscous: 30,\n isBounce: false,\n dt: 0.014,\n isViscous: false,\n BFECC: true,\n ...options\n };\n this.init();\n }\n init() {\n this.calcSize();\n this.createAllFBO();\n this.createShaderPass();\n }\n getFloatType() {\n const isIOS = /(iPad|iPhone|iPod)/i.test(navigator.userAgent);\n return isIOS ? THREE.HalfFloatType : THREE.FloatType;\n }\n createAllFBO() {\n const type = this.getFloatType();\n const opts = {\n type,\n depthBuffer: false,\n stencilBuffer: false,\n minFilter: THREE.LinearFilter,\n magFilter: THREE.LinearFilter,\n wrapS: THREE.ClampToEdgeWrapping,\n wrapT: THREE.ClampToEdgeWrapping\n } as const;\n for (const key in this.fbos) {\n this.fbos[key] = new THREE.WebGLRenderTarget(this.fboSize.x, this.fboSize.y, opts);\n }\n }\n createShaderPass() {\n this.advection = new Advection({\n cellScale: this.cellScale,\n fboSize: this.fboSize,\n dt: this.options.dt,\n src: this.fbos.vel_0,\n dst: this.fbos.vel_1\n });\n this.externalForce = new ExternalForce({\n cellScale: this.cellScale,\n cursor_size: this.options.cursor_size,\n dst: this.fbos.vel_1\n });\n this.viscous = new Viscous({\n cellScale: this.cellScale,\n boundarySpace: this.boundarySpace,\n viscous: this.options.viscous,\n src: this.fbos.vel_1,\n dst: this.fbos.vel_viscous1,\n dst_: this.fbos.vel_viscous0,\n dt: this.options.dt\n });\n this.divergence = new Divergence({\n cellScale: this.cellScale,\n boundarySpace: this.boundarySpace,\n src: this.fbos.vel_viscous0,\n dst: this.fbos.div,\n dt: this.options.dt\n });\n this.poisson = new Poisson({\n cellScale: this.cellScale,\n boundarySpace: this.boundarySpace,\n src: this.fbos.div,\n dst: this.fbos.pressure_1,\n dst_: this.fbos.pressure_0\n });\n this.pressure = new Pressure({\n cellScale: this.cellScale,\n boundarySpace: this.boundarySpace,\n src_p: this.fbos.pressure_0,\n src_v: this.fbos.vel_viscous0,\n dst: this.fbos.vel_0,\n dt: this.options.dt\n });\n }\n calcSize() {\n const width = Math.max(1, Math.round(this.options.resolution * Common.width));\n const height = Math.max(1, Math.round(this.options.resolution * Common.height));\n this.cellScale.set(1 / width, 1 / height);\n this.fboSize.set(width, height);\n }\n resize() {\n this.calcSize();\n for (const key in this.fbos) {\n this.fbos[key]!.setSize(this.fboSize.x, this.fboSize.y);\n }\n }\n update() {\n if (this.options.isBounce) this.boundarySpace.set(0, 0);\n else this.boundarySpace.copy(this.cellScale);\n this.advection.update({ dt: this.options.dt, isBounce: this.options.isBounce, BFECC: this.options.BFECC });\n this.externalForce.update({\n cursor_size: this.options.cursor_size,\n mouse_force: this.options.mouse_force,\n cellScale: this.cellScale\n });\n let vel: any = this.fbos.vel_1;\n if (this.options.isViscous) {\n vel = this.viscous.update({\n viscous: this.options.viscous,\n iterations: this.options.iterations_viscous,\n dt: this.options.dt\n });\n }\n this.divergence.update({ vel });\n const pressure = this.poisson.update({ iterations: this.options.iterations_poisson });\n this.pressure.update({ vel, pressure });\n }\n }\n\n class Output {\n simulation: Simulation;\n scene: THREE.Scene;\n camera: THREE.Camera;\n output: THREE.Mesh;\n constructor() {\n this.simulation = new Simulation();\n this.scene = new THREE.Scene();\n this.camera = new THREE.Camera();\n this.output = new THREE.Mesh(\n new THREE.PlaneGeometry(2, 2),\n new THREE.RawShaderMaterial({\n vertexShader: face_vert,\n fragmentShader: color_frag,\n transparent: true,\n depthWrite: false,\n uniforms: {\n velocity: { value: this.simulation.fbos.vel_0!.texture },\n boundarySpace: { value: new THREE.Vector2() },\n palette: { value: paletteTex },\n bgColor: { value: bgVec4 }\n }\n })\n );\n this.scene.add(this.output);\n }\n resize() {\n this.simulation.resize();\n }\n render() {\n if (!Common.renderer) return;\n Common.renderer.setRenderTarget(null);\n Common.renderer.render(this.scene, this.camera);\n }\n update() {\n this.simulation.update();\n this.render();\n }\n }\n\n class WebGLManager implements LiquidEtherWebGL {\n props: any;\n output!: Output;\n autoDriver?: AutoDriver;\n lastUserInteraction = performance.now();\n running = false;\n private _loop = this.loop.bind(this);\n private _resize = this.resize.bind(this);\n private _onVisibility?: () => void;\n constructor(props: any) {\n this.props = props;\n Common.init(props.$wrapper);\n Mouse.init(props.$wrapper);\n Mouse.autoIntensity = props.autoIntensity;\n Mouse.takeoverDuration = props.takeoverDuration;\n Mouse.onInteract = () => {\n this.lastUserInteraction = performance.now();\n if (this.autoDriver) this.autoDriver.forceStop();\n };\n this.autoDriver = new AutoDriver(Mouse, this as any, {\n enabled: props.autoDemo,\n speed: props.autoSpeed,\n resumeDelay: props.autoResumeDelay,\n rampDuration: props.autoRampDuration\n });\n this.init();\n window.addEventListener('resize', this._resize);\n this._onVisibility = () => {\n const hidden = document.hidden;\n if (hidden) {\n this.pause();\n } else if (isVisibleRef.current) {\n this.start();\n }\n };\n document.addEventListener('visibilitychange', this._onVisibility);\n }\n init() {\n if (!Common.renderer) return;\n this.props.$wrapper.prepend(Common.renderer.domElement);\n this.output = new Output();\n }\n resize() {\n Common.resize();\n this.output.resize();\n }\n render() {\n if (this.autoDriver) this.autoDriver.update();\n Mouse.update();\n Common.update();\n this.output.update();\n }\n loop() {\n if (!this.running) return;\n this.render();\n rafRef.current = requestAnimationFrame(this._loop);\n }\n start() {\n if (this.running) return;\n this.running = true;\n this._loop();\n }\n pause() {\n this.running = false;\n if (rafRef.current) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n }\n dispose() {\n try {\n window.removeEventListener('resize', this._resize);\n if (this._onVisibility) document.removeEventListener('visibilitychange', this._onVisibility);\n Mouse.dispose();\n if (Common.renderer) {\n const canvas = Common.renderer.domElement;\n if (canvas && canvas.parentNode) canvas.parentNode.removeChild(canvas);\n Common.renderer.dispose();\n Common.renderer.forceContextLoss();\n }\n } catch {\n /* noop */\n }\n }\n }\n\n const container = mountRef.current;\n container.style.position = container.style.position || 'relative';\n container.style.overflow = container.style.overflow || 'hidden';\n\n const webgl = new WebGLManager({\n $wrapper: container,\n autoDemo,\n autoSpeed,\n autoIntensity,\n takeoverDuration,\n autoResumeDelay,\n autoRampDuration\n });\n webglRef.current = webgl;\n\n const applyOptionsFromProps = () => {\n if (!webglRef.current) return;\n const sim = webglRef.current.output?.simulation;\n if (!sim) return;\n const prevRes = sim.options.resolution;\n Object.assign(sim.options, {\n mouse_force: mouseForce,\n cursor_size: cursorSize,\n isViscous,\n viscous,\n iterations_viscous: iterationsViscous,\n iterations_poisson: iterationsPoisson,\n dt,\n BFECC,\n resolution,\n isBounce\n });\n if (resolution !== prevRes) sim.resize();\n };\n applyOptionsFromProps();\n webgl.start();\n\n const io = new IntersectionObserver(\n entries => {\n const entry = entries[0];\n const isVisible = entry.isIntersecting && entry.intersectionRatio > 0;\n isVisibleRef.current = isVisible;\n if (!webglRef.current) return;\n if (isVisible && !document.hidden) {\n webglRef.current.start();\n } else {\n webglRef.current.pause();\n }\n },\n { threshold: [0, 0.01, 0.1] }\n );\n io.observe(container);\n intersectionObserverRef.current = io;\n\n const ro = new ResizeObserver(() => {\n if (!webglRef.current) return;\n if (resizeRafRef.current) cancelAnimationFrame(resizeRafRef.current);\n resizeRafRef.current = requestAnimationFrame(() => {\n if (!webglRef.current) return;\n webglRef.current.resize();\n });\n });\n ro.observe(container);\n resizeObserverRef.current = ro;\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n if (resizeObserverRef.current) {\n try {\n resizeObserverRef.current.disconnect();\n } catch {\n /* noop */\n }\n }\n if (intersectionObserverRef.current) {\n try {\n intersectionObserverRef.current.disconnect();\n } catch {\n /* noop */\n }\n }\n if (webglRef.current) {\n webglRef.current.dispose();\n }\n webglRef.current = null;\n };\n }, [\n BFECC,\n cursorSize,\n dt,\n isBounce,\n isViscous,\n iterationsPoisson,\n iterationsViscous,\n mouseForce,\n resolution,\n viscous,\n colors,\n autoDemo,\n autoSpeed,\n autoIntensity,\n takeoverDuration,\n autoResumeDelay,\n autoRampDuration\n ]);\n\n useEffect(() => {\n const webgl = webglRef.current;\n if (!webgl) return;\n const sim = webgl.output?.simulation;\n if (!sim) return;\n const prevRes = sim.options.resolution;\n Object.assign(sim.options, {\n mouse_force: mouseForce,\n cursor_size: cursorSize,\n isViscous,\n viscous,\n iterations_viscous: iterationsViscous,\n iterations_poisson: iterationsPoisson,\n dt,\n BFECC,\n resolution,\n isBounce\n });\n if (webgl.autoDriver) {\n webgl.autoDriver.enabled = autoDemo;\n webgl.autoDriver.speed = autoSpeed;\n webgl.autoDriver.resumeDelay = autoResumeDelay;\n webgl.autoDriver.rampDurationMs = autoRampDuration * 1000;\n if (webgl.autoDriver.mouse) {\n webgl.autoDriver.mouse.autoIntensity = autoIntensity;\n webgl.autoDriver.mouse.takeoverDuration = takeoverDuration;\n }\n }\n if (resolution !== prevRes) sim.resize();\n }, [\n mouseForce,\n cursorSize,\n isViscous,\n viscous,\n iterationsViscous,\n iterationsPoisson,\n dt,\n BFECC,\n resolution,\n isBounce,\n autoDemo,\n autoSpeed,\n autoIntensity,\n takeoverDuration,\n autoResumeDelay,\n autoRampDuration\n ]);\n\n return
;\n}\n" } ], diff --git a/public/r/LogoLoop-JS-CSS.json b/public/r/LogoLoop-JS-CSS.json index e7c89eeb..1f808686 100644 --- a/public/r/LogoLoop-JS-CSS.json +++ b/public/r/LogoLoop-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "LogoLoop/LogoLoop.css", + "type": "registry:file", + "path": "LogoLoop.css", + "target": "@components/LogoLoop.css", "content": ".logoloop {\n position: relative;\n\n --logoloop-gap: 32px;\n --logoloop-logoHeight: 28px;\n --logoloop-fadeColorAuto: #ffffff;\n}\n\n.logoloop--vertical {\n height: 100%;\n display: inline-block;\n}\n\n.logoloop--scale-hover {\n padding-top: calc(var(--logoloop-logoHeight) * 0.1);\n padding-bottom: calc(var(--logoloop-logoHeight) * 0.1);\n}\n\n@media (prefers-color-scheme: dark) {\n .logoloop {\n --logoloop-fadeColorAuto: #0b0b0b;\n }\n}\n\n.logoloop__track {\n display: flex;\n width: max-content;\n will-change: transform;\n user-select: none;\n position: relative;\n z-index: 0;\n}\n\n.logoloop--vertical .logoloop__track {\n flex-direction: column;\n height: max-content;\n width: 100%;\n}\n\n.logoloop__list {\n display: flex;\n align-items: center;\n}\n\n.logoloop--vertical .logoloop__list {\n flex-direction: column;\n}\n\n.logoloop__item {\n flex: 0 0 auto;\n margin-right: var(--logoloop-gap);\n font-size: var(--logoloop-logoHeight);\n line-height: 1;\n}\n\n.logoloop--vertical .logoloop__item {\n margin-right: 0;\n margin-bottom: var(--logoloop-gap);\n}\n\n.logoloop__item:last-child {\n margin-right: var(--logoloop-gap);\n}\n\n.logoloop--vertical .logoloop__item:last-child {\n margin-right: 0;\n margin-bottom: var(--logoloop-gap);\n}\n\n.logoloop__node {\n display: inline-flex;\n align-items: center;\n}\n\n.logoloop__item img {\n height: var(--logoloop-logoHeight);\n width: auto;\n display: block;\n object-fit: contain;\n image-rendering: -webkit-optimize-contrast;\n -webkit-user-drag: none;\n pointer-events: none;\n transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.logoloop--scale-hover .logoloop__item {\n overflow: visible;\n}\n\n.logoloop--scale-hover .logoloop__item:hover img,\n.logoloop--scale-hover .logoloop__item:hover .logoloop__node {\n transform: scale(1.2);\n transform-origin: center center;\n}\n\n.logoloop--scale-hover .logoloop__node {\n transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.logoloop__link {\n display: inline-flex;\n align-items: center;\n text-decoration: none;\n border-radius: 4px;\n transition: opacity 0.2s ease;\n}\n\n.logoloop__link:hover {\n opacity: 0.8;\n}\n\n.logoloop__link:focus-visible {\n outline: 2px solid currentColor;\n outline-offset: 2px;\n}\n\n.logoloop--fade::before,\n.logoloop--fade::after {\n content: '';\n position: absolute;\n top: 0;\n bottom: 0;\n width: clamp(24px, 8%, 120px);\n pointer-events: none;\n z-index: 10;\n}\n\n.logoloop--fade::before {\n left: 0;\n background: linear-gradient(\n to right,\n var(--logoloop-fadeColor, var(--logoloop-fadeColorAuto)) 0%,\n rgba(0, 0, 0, 0) 100%\n );\n}\n\n.logoloop--fade::after {\n right: 0;\n background: linear-gradient(\n to left,\n var(--logoloop-fadeColor, var(--logoloop-fadeColorAuto)) 0%,\n rgba(0, 0, 0, 0) 100%\n );\n}\n\n.logoloop--vertical.logoloop--fade::before,\n.logoloop--vertical.logoloop--fade::after {\n left: 0;\n right: 0;\n width: 100%;\n height: clamp(24px, 8%, 120px);\n}\n\n.logoloop--vertical.logoloop--fade::before {\n top: 0;\n bottom: auto;\n background: linear-gradient(\n to bottom,\n var(--logoloop-fadeColor, var(--logoloop-fadeColorAuto)) 0%,\n rgba(0, 0, 0, 0) 100%\n );\n}\n\n.logoloop--vertical.logoloop--fade::after {\n bottom: 0;\n top: auto;\n background: linear-gradient(\n to top,\n var(--logoloop-fadeColor, var(--logoloop-fadeColorAuto)) 0%,\n rgba(0, 0, 0, 0) 100%\n );\n}\n\n@media (prefers-reduced-motion: reduce) {\n .logoloop__track {\n transform: translate3d(0, 0, 0) !important;\n }\n\n .logoloop__item img,\n .logoloop__node {\n transition: none !important;\n }\n}\n" }, { "type": "registry:component", - "path": "LogoLoop/LogoLoop.jsx", + "path": "LogoLoop.jsx", "content": "import { useCallback, useEffect, useMemo, useRef, useState, memo } from 'react';\nimport './LogoLoop.css';\n\nconst ANIMATION_CONFIG = { SMOOTH_TAU: 0.25, MIN_COPIES: 2, COPY_HEADROOM: 2 };\n\nconst toCssLength = value => (typeof value === 'number' ? `${value}px` : (value ?? undefined));\n\nconst useResizeObserver = (callback, elements, dependencies) => {\n useEffect(() => {\n if (!window.ResizeObserver) {\n const handleResize = () => callback();\n window.addEventListener('resize', handleResize);\n callback();\n return () => window.removeEventListener('resize', handleResize);\n }\n const observers = elements.map(ref => {\n if (!ref.current) return null;\n const observer = new ResizeObserver(callback);\n observer.observe(ref.current);\n return observer;\n });\n callback();\n return () => {\n observers.forEach(observer => observer?.disconnect());\n };\n }, [callback, elements, dependencies]);\n};\n\nconst useImageLoader = (seqRef, onLoad, dependencies) => {\n useEffect(() => {\n const images = seqRef.current?.querySelectorAll('img') ?? [];\n if (images.length === 0) {\n onLoad();\n return;\n }\n let remainingImages = images.length;\n const handleImageLoad = () => {\n remainingImages -= 1;\n if (remainingImages === 0) onLoad();\n };\n images.forEach(img => {\n const htmlImg = img;\n if (htmlImg.complete) {\n handleImageLoad();\n } else {\n htmlImg.addEventListener('load', handleImageLoad, { once: true });\n htmlImg.addEventListener('error', handleImageLoad, { once: true });\n }\n });\n return () => {\n images.forEach(img => {\n img.removeEventListener('load', handleImageLoad);\n img.removeEventListener('error', handleImageLoad);\n });\n };\n }, [onLoad, seqRef, dependencies]);\n};\n\nconst useAnimationLoop = (trackRef, targetVelocity, seqWidth, seqHeight, isHovered, hoverSpeed, isVertical) => {\n const rafRef = useRef(null);\n const lastTimestampRef = useRef(null);\n const offsetRef = useRef(0);\n const velocityRef = useRef(0);\n\n useEffect(() => {\n const track = trackRef.current;\n if (!track) return;\n\n const seqSize = isVertical ? seqHeight : seqWidth;\n\n if (seqSize > 0) {\n offsetRef.current = ((offsetRef.current % seqSize) + seqSize) % seqSize;\n const transformValue = isVertical\n ? `translate3d(0, ${-offsetRef.current}px, 0)`\n : `translate3d(${-offsetRef.current}px, 0, 0)`;\n track.style.transform = transformValue;\n }\n\n const animate = timestamp => {\n if (lastTimestampRef.current === null) {\n lastTimestampRef.current = timestamp;\n }\n\n const deltaTime = Math.max(0, timestamp - lastTimestampRef.current) / 1000;\n lastTimestampRef.current = timestamp;\n\n const target = isHovered && hoverSpeed !== undefined ? hoverSpeed : targetVelocity;\n\n const easingFactor = 1 - Math.exp(-deltaTime / ANIMATION_CONFIG.SMOOTH_TAU);\n velocityRef.current += (target - velocityRef.current) * easingFactor;\n\n if (seqSize > 0) {\n let nextOffset = offsetRef.current + velocityRef.current * deltaTime;\n nextOffset = ((nextOffset % seqSize) + seqSize) % seqSize;\n offsetRef.current = nextOffset;\n\n const transformValue = isVertical\n ? `translate3d(0, ${-offsetRef.current}px, 0)`\n : `translate3d(${-offsetRef.current}px, 0, 0)`;\n track.style.transform = transformValue;\n }\n\n rafRef.current = requestAnimationFrame(animate);\n };\n\n rafRef.current = requestAnimationFrame(animate);\n\n return () => {\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n lastTimestampRef.current = null;\n };\n }, [targetVelocity, seqWidth, seqHeight, isHovered, hoverSpeed, isVertical, trackRef]);\n};\n\nexport const LogoLoop = memo(\n ({\n logos,\n speed = 120,\n direction = 'left',\n width = '100%',\n logoHeight = 28,\n gap = 32,\n pauseOnHover,\n hoverSpeed,\n fadeOut = false,\n fadeOutColor,\n scaleOnHover = false,\n renderItem,\n ariaLabel = 'Partner logos',\n className,\n style\n }) => {\n const containerRef = useRef(null);\n const trackRef = useRef(null);\n const seqRef = useRef(null);\n\n const [seqWidth, setSeqWidth] = useState(0);\n const [seqHeight, setSeqHeight] = useState(0);\n const [copyCount, setCopyCount] = useState(ANIMATION_CONFIG.MIN_COPIES);\n const [isHovered, setIsHovered] = useState(false);\n\n const effectiveHoverSpeed = useMemo(() => {\n if (hoverSpeed !== undefined) return hoverSpeed;\n if (pauseOnHover === true) return 0;\n if (pauseOnHover === false) return undefined;\n return 0;\n }, [hoverSpeed, pauseOnHover]);\n\n const isVertical = direction === 'up' || direction === 'down';\n\n const targetVelocity = useMemo(() => {\n const magnitude = Math.abs(speed);\n let directionMultiplier;\n if (isVertical) {\n directionMultiplier = direction === 'up' ? 1 : -1;\n } else {\n directionMultiplier = direction === 'left' ? 1 : -1;\n }\n const speedMultiplier = speed < 0 ? -1 : 1;\n return magnitude * directionMultiplier * speedMultiplier;\n }, [speed, direction, isVertical]);\n\n const updateDimensions = useCallback(() => {\n const containerWidth = containerRef.current?.clientWidth ?? 0;\n const sequenceRect = seqRef.current?.getBoundingClientRect?.();\n const sequenceWidth = sequenceRect?.width ?? 0;\n const sequenceHeight = sequenceRect?.height ?? 0;\n if (isVertical) {\n const parentHeight = containerRef.current?.parentElement?.clientHeight ?? 0;\n if (containerRef.current && parentHeight > 0) {\n const targetHeight = Math.ceil(parentHeight);\n if (containerRef.current.style.height !== `${targetHeight}px`)\n containerRef.current.style.height = `${targetHeight}px`;\n }\n if (sequenceHeight > 0) {\n setSeqHeight(Math.ceil(sequenceHeight));\n const viewport = containerRef.current?.clientHeight ?? parentHeight ?? sequenceHeight;\n const copiesNeeded = Math.ceil(viewport / sequenceHeight) + ANIMATION_CONFIG.COPY_HEADROOM;\n setCopyCount(Math.max(ANIMATION_CONFIG.MIN_COPIES, copiesNeeded));\n }\n } else if (sequenceWidth > 0) {\n setSeqWidth(Math.ceil(sequenceWidth));\n const copiesNeeded = Math.ceil(containerWidth / sequenceWidth) + ANIMATION_CONFIG.COPY_HEADROOM;\n setCopyCount(Math.max(ANIMATION_CONFIG.MIN_COPIES, copiesNeeded));\n }\n }, [isVertical]);\n\n useResizeObserver(updateDimensions, [containerRef, seqRef], [logos, gap, logoHeight, isVertical]);\n\n useImageLoader(seqRef, updateDimensions, [logos, gap, logoHeight, isVertical]);\n\n useAnimationLoop(trackRef, targetVelocity, seqWidth, seqHeight, isHovered, effectiveHoverSpeed, isVertical);\n\n const cssVariables = useMemo(\n () => ({\n '--logoloop-gap': `${gap}px`,\n '--logoloop-logoHeight': `${logoHeight}px`,\n ...(fadeOutColor && { '--logoloop-fadeColor': fadeOutColor })\n }),\n [gap, logoHeight, fadeOutColor]\n );\n\n const rootClassName = useMemo(\n () =>\n [\n 'logoloop',\n isVertical ? 'logoloop--vertical' : 'logoloop--horizontal',\n fadeOut && 'logoloop--fade',\n scaleOnHover && 'logoloop--scale-hover',\n className\n ]\n .filter(Boolean)\n .join(' '),\n [isVertical, fadeOut, scaleOnHover, className]\n );\n\n const handleMouseEnter = useCallback(() => {\n if (effectiveHoverSpeed !== undefined) setIsHovered(true);\n }, [effectiveHoverSpeed]);\n const handleMouseLeave = useCallback(() => {\n if (effectiveHoverSpeed !== undefined) setIsHovered(false);\n }, [effectiveHoverSpeed]);\n\n const renderLogoItem = useCallback(\n (item, key) => {\n if (renderItem) {\n return (\n
  • \n {renderItem(item, key)}\n
  • \n );\n }\n const isNodeItem = 'node' in item;\n const content = isNodeItem ? (\n \n {item.node}\n \n ) : (\n \n );\n const itemAriaLabel = isNodeItem ? (item.ariaLabel ?? item.title) : (item.alt ?? item.title);\n const itemContent = item.href ? (\n \n {content}\n \n ) : (\n content\n );\n return (\n
  • \n {itemContent}\n
  • \n );\n },\n [renderItem]\n );\n\n const logoLists = useMemo(\n () =>\n Array.from({ length: copyCount }, (_, copyIndex) => (\n 0}\n ref={copyIndex === 0 ? seqRef : undefined}\n >\n {logos.map((item, itemIndex) => renderLogoItem(item, `${copyIndex}-${itemIndex}`))}\n \n )),\n [copyCount, logos, renderLogoItem]\n );\n\n const containerStyle = useMemo(\n () => ({\n width: isVertical\n ? toCssLength(width) === '100%'\n ? undefined\n : toCssLength(width)\n : (toCssLength(width) ?? '100%'),\n ...cssVariables,\n ...style\n }),\n [width, cssVariables, style, isVertical]\n );\n\n return (\n
    \n
    \n {logoLists}\n
    \n
    \n );\n }\n);\n\nLogoLoop.displayName = 'LogoLoop';\n\nexport default LogoLoop;\n" } ], diff --git a/public/r/LogoLoop-TS-CSS.json b/public/r/LogoLoop-TS-CSS.json index bfa2af4e..4f0db047 100644 --- a/public/r/LogoLoop-TS-CSS.json +++ b/public/r/LogoLoop-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "LogoLoop/LogoLoop.css", + "type": "registry:file", + "path": "LogoLoop.css", + "target": "@components/LogoLoop.css", "content": ".logoloop {\n position: relative;\n overflow-x: hidden;\n\n --logoloop-gap: 32px;\n --logoloop-logoHeight: 28px;\n --logoloop-fadeColorAuto: #ffffff;\n}\n\n.logoloop--vertical {\n overflow: hidden;\n height: 100%;\n display: inline-block;\n}\n\n.logoloop--scale-hover {\n padding-top: calc(var(--logoloop-logoHeight) * 0.1);\n padding-bottom: calc(var(--logoloop-logoHeight) * 0.1);\n}\n\n@media (prefers-color-scheme: dark) {\n .logoloop {\n --logoloop-fadeColorAuto: #0b0b0b;\n }\n}\n\n.logoloop__track {\n display: flex;\n width: max-content;\n will-change: transform;\n user-select: none;\n position: relative;\n z-index: 0;\n}\n\n.logoloop--vertical .logoloop__track {\n flex-direction: column;\n height: max-content;\n width: 100%;\n}\n\n.logoloop__list {\n display: flex;\n align-items: center;\n}\n\n.logoloop--vertical .logoloop__list {\n flex-direction: column;\n}\n\n.logoloop__item {\n flex: 0 0 auto;\n margin-right: var(--logoloop-gap);\n font-size: var(--logoloop-logoHeight);\n line-height: 1;\n}\n\n.logoloop--vertical .logoloop__item {\n margin-right: 0;\n margin-bottom: var(--logoloop-gap);\n}\n\n.logoloop__item:last-child {\n margin-right: var(--logoloop-gap);\n}\n\n.logoloop--vertical .logoloop__item:last-child {\n margin-right: 0;\n margin-bottom: var(--logoloop-gap);\n}\n\n.logoloop__node {\n display: inline-flex;\n align-items: center;\n}\n\n.logoloop__item img {\n height: var(--logoloop-logoHeight);\n width: auto;\n display: block;\n object-fit: contain;\n image-rendering: -webkit-optimize-contrast;\n -webkit-user-drag: none;\n pointer-events: none;\n transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.logoloop--scale-hover .logoloop__item {\n overflow: visible;\n}\n\n.logoloop--scale-hover .logoloop__item:hover img,\n.logoloop--scale-hover .logoloop__item:hover .logoloop__node {\n transform: scale(1.2);\n transform-origin: center center;\n}\n\n.logoloop--scale-hover .logoloop__node {\n transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.logoloop__link {\n display: inline-flex;\n align-items: center;\n text-decoration: none;\n border-radius: 4px;\n transition: opacity 0.2s ease;\n}\n\n.logoloop__link:hover {\n opacity: 0.8;\n}\n\n.logoloop__link:focus-visible {\n outline: 2px solid currentColor;\n outline-offset: 2px;\n}\n\n.logoloop--fade::before,\n.logoloop--fade::after {\n content: '';\n position: absolute;\n top: 0;\n bottom: 0;\n width: clamp(24px, 8%, 120px);\n pointer-events: none;\n z-index: 10;\n}\n\n.logoloop--fade::before {\n left: 0;\n background: linear-gradient(\n to right,\n var(--logoloop-fadeColor, var(--logoloop-fadeColorAuto)) 0%,\n rgba(0, 0, 0, 0) 100%\n );\n}\n\n.logoloop--fade::after {\n right: 0;\n background: linear-gradient(\n to left,\n var(--logoloop-fadeColor, var(--logoloop-fadeColorAuto)) 0%,\n rgba(0, 0, 0, 0) 100%\n );\n}\n\n.logoloop--vertical.logoloop--fade::before,\n.logoloop--vertical.logoloop--fade::after {\n left: 0;\n right: 0;\n width: 100%;\n height: clamp(24px, 8%, 120px);\n}\n\n.logoloop--vertical.logoloop--fade::before {\n top: 0;\n bottom: auto;\n background: linear-gradient(\n to bottom,\n var(--logoloop-fadeColor, var(--logoloop-fadeColorAuto)) 0%,\n rgba(0, 0, 0, 0) 100%\n );\n}\n\n.logoloop--vertical.logoloop--fade::after {\n bottom: 0;\n top: auto;\n background: linear-gradient(\n to top,\n var(--logoloop-fadeColor, var(--logoloop-fadeColorAuto)) 0%,\n rgba(0, 0, 0, 0) 100%\n );\n}\n\n@media (prefers-reduced-motion: reduce) {\n .logoloop__track {\n transform: translate3d(0, 0, 0) !important;\n }\n\n .logoloop__item img,\n .logoloop__node {\n transition: none !important;\n }\n}\n" }, { "type": "registry:component", - "path": "LogoLoop/LogoLoop.tsx", + "path": "LogoLoop.tsx", "content": "import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport './LogoLoop.css';\n\nexport type LogoItem =\n | {\n node: React.ReactNode;\n href?: string;\n title?: string;\n ariaLabel?: string;\n }\n | {\n src: string;\n alt?: string;\n href?: string;\n title?: string;\n srcSet?: string;\n sizes?: string;\n width?: number;\n height?: number;\n };\n\nexport interface LogoLoopProps {\n logos: LogoItem[];\n speed?: number;\n direction?: 'left' | 'right' | 'up' | 'down';\n width?: number | string;\n logoHeight?: number;\n gap?: number;\n pauseOnHover?: boolean;\n hoverSpeed?: number;\n fadeOut?: boolean;\n fadeOutColor?: string;\n scaleOnHover?: boolean;\n renderItem?: (item: LogoItem, key: React.Key) => React.ReactNode;\n ariaLabel?: string;\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst ANIMATION_CONFIG = {\n SMOOTH_TAU: 0.25,\n MIN_COPIES: 2,\n COPY_HEADROOM: 2\n} as const;\n\nconst toCssLength = (value?: number | string): string | undefined =>\n typeof value === 'number' ? `${value}px` : (value ?? undefined);\n\nconst useResizeObserver = (\n callback: () => void,\n elements: Array>,\n dependencies: React.DependencyList\n) => {\n useEffect(() => {\n if (!window.ResizeObserver) {\n const handleResize = () => callback();\n window.addEventListener('resize', handleResize);\n callback();\n return () => window.removeEventListener('resize', handleResize);\n }\n\n const observers = elements.map(ref => {\n if (!ref.current) return null;\n const observer = new ResizeObserver(callback);\n observer.observe(ref.current);\n return observer;\n });\n\n callback();\n\n return () => {\n observers.forEach(observer => observer?.disconnect());\n };\n }, dependencies);\n};\n\nconst useImageLoader = (\n seqRef: React.RefObject,\n onLoad: () => void,\n dependencies: React.DependencyList\n) => {\n useEffect(() => {\n const images = seqRef.current?.querySelectorAll('img') ?? [];\n\n if (images.length === 0) {\n onLoad();\n return;\n }\n\n let remainingImages = images.length;\n const handleImageLoad = () => {\n remainingImages -= 1;\n if (remainingImages === 0) {\n onLoad();\n }\n };\n\n images.forEach(img => {\n const htmlImg = img as HTMLImageElement;\n if (htmlImg.complete) {\n handleImageLoad();\n } else {\n htmlImg.addEventListener('load', handleImageLoad, { once: true });\n htmlImg.addEventListener('error', handleImageLoad, { once: true });\n }\n });\n\n return () => {\n images.forEach(img => {\n img.removeEventListener('load', handleImageLoad);\n img.removeEventListener('error', handleImageLoad);\n });\n };\n }, dependencies);\n};\n\nconst useAnimationLoop = (\n trackRef: React.RefObject,\n targetVelocity: number,\n seqWidth: number,\n seqHeight: number,\n isHovered: boolean,\n hoverSpeed: number | undefined,\n isVertical: boolean\n) => {\n const rafRef = useRef(null);\n const lastTimestampRef = useRef(null);\n const offsetRef = useRef(0);\n const velocityRef = useRef(0);\n\n useEffect(() => {\n const track = trackRef.current;\n if (!track) return;\n\n const seqSize = isVertical ? seqHeight : seqWidth;\n\n if (seqSize > 0) {\n offsetRef.current = ((offsetRef.current % seqSize) + seqSize) % seqSize;\n const transformValue = isVertical\n ? `translate3d(0, ${-offsetRef.current}px, 0)`\n : `translate3d(${-offsetRef.current}px, 0, 0)`;\n track.style.transform = transformValue;\n }\n\n const animate = (timestamp: number) => {\n if (lastTimestampRef.current === null) {\n lastTimestampRef.current = timestamp;\n }\n\n const deltaTime = Math.max(0, timestamp - lastTimestampRef.current) / 1000;\n lastTimestampRef.current = timestamp;\n\n const target = isHovered && hoverSpeed !== undefined ? hoverSpeed : targetVelocity;\n\n const easingFactor = 1 - Math.exp(-deltaTime / ANIMATION_CONFIG.SMOOTH_TAU);\n velocityRef.current += (target - velocityRef.current) * easingFactor;\n\n if (seqSize > 0) {\n let nextOffset = offsetRef.current + velocityRef.current * deltaTime;\n nextOffset = ((nextOffset % seqSize) + seqSize) % seqSize;\n offsetRef.current = nextOffset;\n\n const transformValue = isVertical\n ? `translate3d(0, ${-offsetRef.current}px, 0)`\n : `translate3d(${-offsetRef.current}px, 0, 0)`;\n track.style.transform = transformValue;\n }\n\n rafRef.current = requestAnimationFrame(animate);\n };\n\n rafRef.current = requestAnimationFrame(animate);\n\n return () => {\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n lastTimestampRef.current = null;\n };\n }, [targetVelocity, seqWidth, seqHeight, isHovered, hoverSpeed, isVertical]);\n};\n\nexport const LogoLoop = React.memo(\n ({\n logos,\n speed = 120,\n direction = 'left',\n width = '100%',\n logoHeight = 28,\n gap = 32,\n pauseOnHover,\n hoverSpeed,\n fadeOut = false,\n fadeOutColor,\n scaleOnHover = false,\n renderItem,\n ariaLabel = 'Partner logos',\n className,\n style\n }) => {\n const containerRef = useRef(null);\n const trackRef = useRef(null);\n const seqRef = useRef(null);\n\n const [seqWidth, setSeqWidth] = useState(0);\n const [seqHeight, setSeqHeight] = useState(0);\n const [copyCount, setCopyCount] = useState(ANIMATION_CONFIG.MIN_COPIES);\n const [isHovered, setIsHovered] = useState(false);\n\n const effectiveHoverSpeed = useMemo(() => {\n if (hoverSpeed !== undefined) return hoverSpeed;\n if (pauseOnHover === true) return 0;\n if (pauseOnHover === false) return undefined;\n return 0;\n }, [hoverSpeed, pauseOnHover]);\n\n const isVertical = direction === 'up' || direction === 'down';\n\n const targetVelocity = useMemo(() => {\n const magnitude = Math.abs(speed);\n let directionMultiplier: number;\n if (isVertical) {\n directionMultiplier = direction === 'up' ? 1 : -1;\n } else {\n directionMultiplier = direction === 'left' ? 1 : -1;\n }\n const speedMultiplier = speed < 0 ? -1 : 1;\n return magnitude * directionMultiplier * speedMultiplier;\n }, [speed, direction, isVertical]);\n\n const updateDimensions = useCallback(() => {\n const containerWidth = containerRef.current?.clientWidth ?? 0;\n const sequenceRect = seqRef.current?.getBoundingClientRect?.();\n const sequenceWidth = sequenceRect?.width ?? 0;\n const sequenceHeight = sequenceRect?.height ?? 0;\n if (isVertical) {\n const parentHeight = containerRef.current?.parentElement?.clientHeight ?? 0;\n if (containerRef.current && parentHeight > 0) {\n const targetHeight = Math.ceil(parentHeight);\n if (containerRef.current.style.height !== `${targetHeight}px`)\n containerRef.current.style.height = `${targetHeight}px`;\n }\n if (sequenceHeight > 0) {\n setSeqHeight(Math.ceil(sequenceHeight));\n const viewport = containerRef.current?.clientHeight ?? parentHeight ?? sequenceHeight;\n const copiesNeeded = Math.ceil(viewport / sequenceHeight) + ANIMATION_CONFIG.COPY_HEADROOM;\n setCopyCount(Math.max(ANIMATION_CONFIG.MIN_COPIES, copiesNeeded));\n }\n } else if (sequenceWidth > 0) {\n setSeqWidth(Math.ceil(sequenceWidth));\n const copiesNeeded = Math.ceil(containerWidth / sequenceWidth) + ANIMATION_CONFIG.COPY_HEADROOM;\n setCopyCount(Math.max(ANIMATION_CONFIG.MIN_COPIES, copiesNeeded));\n }\n }, [isVertical]);\n\n useResizeObserver(updateDimensions, [containerRef, seqRef], [logos, gap, logoHeight, isVertical]);\n\n useImageLoader(seqRef, updateDimensions, [logos, gap, logoHeight, isVertical]);\n\n useAnimationLoop(trackRef, targetVelocity, seqWidth, seqHeight, isHovered, effectiveHoverSpeed, isVertical);\n\n const cssVariables = useMemo(\n () =>\n ({\n '--logoloop-gap': `${gap}px`,\n '--logoloop-logoHeight': `${logoHeight}px`,\n ...(fadeOutColor && { '--logoloop-fadeColor': fadeOutColor })\n }) as React.CSSProperties,\n [gap, logoHeight, fadeOutColor]\n );\n\n const rootClassName = useMemo(\n () =>\n [\n 'logoloop',\n isVertical ? 'logoloop--vertical' : 'logoloop--horizontal',\n fadeOut && 'logoloop--fade',\n scaleOnHover && 'logoloop--scale-hover',\n className\n ]\n .filter(Boolean)\n .join(' '),\n [isVertical, fadeOut, scaleOnHover, className]\n );\n\n const handleMouseEnter = useCallback(() => {\n if (effectiveHoverSpeed !== undefined) setIsHovered(true);\n }, [effectiveHoverSpeed]);\n const handleMouseLeave = useCallback(() => {\n if (effectiveHoverSpeed !== undefined) setIsHovered(false);\n }, [effectiveHoverSpeed]);\n\n const renderLogoItem = useCallback(\n (item: LogoItem, key: React.Key) => {\n if (renderItem) {\n return (\n
  • \n {renderItem(item, key)}\n
  • \n );\n }\n const isNodeItem = 'node' in item;\n const content = isNodeItem ? (\n \n {(item as any).node}\n \n ) : (\n \n );\n const itemAriaLabel = isNodeItem\n ? ((item as any).ariaLabel ?? (item as any).title)\n : ((item as any).alt ?? (item as any).title);\n const itemContent = (item as any).href ? (\n \n {content}\n \n ) : (\n content\n );\n return (\n
  • \n {itemContent}\n
  • \n );\n },\n [renderItem]\n );\n\n const logoLists = useMemo(\n () =>\n Array.from({ length: copyCount }, (_, copyIndex) => (\n 0}\n ref={copyIndex === 0 ? seqRef : undefined}\n >\n {logos.map((item, itemIndex) => renderLogoItem(item, `${copyIndex}-${itemIndex}`))}\n \n )),\n [copyCount, logos, renderLogoItem]\n );\n\n const containerStyle = useMemo(\n (): React.CSSProperties => ({\n width: isVertical\n ? toCssLength(width) === '100%'\n ? undefined\n : toCssLength(width)\n : (toCssLength(width) ?? '100%'),\n ...cssVariables,\n ...style\n }),\n [width, cssVariables, style, isVertical]\n );\n\n return (\n
    \n
    \n {logoLists}\n
    \n
    \n );\n }\n);\n\nLogoLoop.displayName = 'LogoLoop';\n\nexport default LogoLoop;\n" } ], diff --git a/public/r/MagicBento-JS-CSS.json b/public/r/MagicBento-JS-CSS.json index fc310153..7fd76e35 100644 --- a/public/r/MagicBento-JS-CSS.json +++ b/public/r/MagicBento-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "MagicBento/MagicBento.css", + "type": "registry:file", + "path": "MagicBento.css", + "target": "@components/MagicBento.css", "content": ":root {\n --hue: 27;\n --sat: 69%;\n --white: hsl(0, 0%, 100%);\n --purple-primary: rgba(132, 0, 255, 1);\n --purple-glow: rgba(132, 0, 255, 0.2);\n --purple-border: rgba(132, 0, 255, 0.8);\n --border-color: #2F293A;\n --background-dark: #120F17;\n color-scheme: light dark;\n}\n\n.card-grid {\n display: grid;\n gap: 0.5em;\n padding: 0.75em;\n max-width: 54em;\n font-size: clamp(1rem, 0.9rem + 0.5vw, 1.5rem);\n}\n\n.magic-bento-card {\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n position: relative;\n aspect-ratio: 4/3;\n min-height: 200px;\n width: 100%;\n max-width: 100%;\n padding: 1.25em;\n border-radius: 20px;\n border: 1px solid var(--border-color);\n background: var(--background-dark);\n font-weight: 300;\n overflow: hidden;\n transition: all 0.3s ease;\n\n --glow-x: 50%;\n --glow-y: 50%;\n --glow-intensity: 0;\n --glow-radius: 200px;\n}\n\n.magic-bento-card:hover {\n transform: translateY(-2px);\n box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);\n}\n\n.magic-bento-card__header,\n.magic-bento-card__content {\n display: flex;\n position: relative;\n color: var(--white);\n}\n\n.magic-bento-card__header {\n gap: 0.75em;\n justify-content: space-between;\n}\n\n.magic-bento-card__content {\n flex-direction: column;\n}\n\n.magic-bento-card__label {\n font-size: 16px;\n}\n\n.magic-bento-card__title,\n.magic-bento-card__description {\n --clamp-title: 1;\n --clamp-desc: 2;\n}\n\n.magic-bento-card__title {\n font-weight: 400;\n font-size: 16px;\n margin: 0 0 0.25em;\n}\n\n.magic-bento-card__description {\n font-size: 12px;\n line-height: 1.2;\n opacity: 0.9;\n}\n\n.magic-bento-card--text-autohide .magic-bento-card__title,\n.magic-bento-card--text-autohide .magic-bento-card__description {\n display: -webkit-box;\n -webkit-box-orient: vertical;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.magic-bento-card--text-autohide .magic-bento-card__title {\n -webkit-line-clamp: var(--clamp-title);\n line-clamp: var(--clamp-title);\n}\n\n.magic-bento-card--text-autohide .magic-bento-card__description {\n -webkit-line-clamp: var(--clamp-desc);\n line-clamp: var(--clamp-desc);\n}\n\n@media (max-width: 599px) {\n .card-grid {\n grid-template-columns: 1fr;\n width: 90%;\n margin: 0 auto;\n padding: 0.5em;\n }\n\n .magic-bento-card {\n width: 100%;\n min-height: 180px;\n }\n}\n\n@media (min-width: 600px) {\n .card-grid {\n grid-template-columns: repeat(2, 1fr);\n }\n}\n\n@media (min-width: 1024px) {\n .card-grid {\n grid-template-columns: repeat(4, 1fr);\n }\n\n .magic-bento-card:nth-child(3) {\n grid-column: span 2;\n grid-row: span 2;\n }\n\n .magic-bento-card:nth-child(4) {\n grid-column: 1 / span 2;\n grid-row: 2 / span 2;\n }\n\n .magic-bento-card:nth-child(6) {\n grid-column: 4;\n grid-row: 3;\n }\n}\n\n/* Border glow effect */\n.magic-bento-card--border-glow::after {\n content: '';\n position: absolute;\n inset: 0;\n padding: 6px;\n background: radial-gradient(\n var(--glow-radius) circle at var(--glow-x) var(--glow-y),\n rgba(132, 0, 255, calc(var(--glow-intensity) * 0.8)) 0%,\n rgba(132, 0, 255, calc(var(--glow-intensity) * 0.4)) 30%,\n transparent 60%\n );\n border-radius: inherit;\n -webkit-mask:\n linear-gradient(#fff 0 0) content-box,\n linear-gradient(#fff 0 0);\n -webkit-mask-composite: xor;\n mask:\n linear-gradient(#fff 0 0) content-box,\n linear-gradient(#fff 0 0);\n mask-composite: exclude;\n pointer-events: none;\n opacity: 1;\n transition: opacity 0.3s ease;\n z-index: 1;\n}\n\n.magic-bento-card--border-glow:hover::after {\n opacity: 1;\n}\n\n.magic-bento-card--border-glow:hover {\n box-shadow:\n 0 4px 20px rgba(46, 24, 78, 0.4),\n 0 0 30px var(--purple-glow);\n}\n\n.particle-container {\n position: relative;\n overflow: hidden;\n}\n\n.particle::before {\n content: '';\n position: absolute;\n top: -2px;\n left: -2px;\n right: -2px;\n bottom: -2px;\n background: rgba(132, 0, 255, 0.2);\n border-radius: 50%;\n z-index: -1;\n}\n\n.particle-container:hover {\n box-shadow:\n 0 4px 20px rgba(46, 24, 78, 0.2),\n 0 0 30px var(--purple-glow);\n}\n\n/* Global spotlight styles */\n.global-spotlight {\n mix-blend-mode: screen;\n will-change: transform, opacity;\n z-index: 200 !important;\n pointer-events: none;\n}\n\n.bento-section {\n position: relative;\n user-select: none;\n}\n" }, { "type": "registry:component", - "path": "MagicBento/MagicBento.jsx", + "path": "MagicBento.jsx", "content": "import { useRef, useEffect, useCallback, useState } from 'react';\nimport { gsap } from 'gsap';\nimport './MagicBento.css';\n\nconst DEFAULT_PARTICLE_COUNT = 12;\nconst DEFAULT_SPOTLIGHT_RADIUS = 300;\nconst DEFAULT_GLOW_COLOR = '132, 0, 255';\nconst MOBILE_BREAKPOINT = 768;\n\nconst cardData = [\n {\n color: '#120F17',\n title: 'Analytics',\n description: 'Track user behavior',\n label: 'Insights'\n },\n {\n color: '#120F17',\n title: 'Dashboard',\n description: 'Centralized data view',\n label: 'Overview'\n },\n {\n color: '#120F17',\n title: 'Collaboration',\n description: 'Work together seamlessly',\n label: 'Teamwork'\n },\n {\n color: '#120F17',\n title: 'Automation',\n description: 'Streamline workflows',\n label: 'Efficiency'\n },\n {\n color: '#120F17',\n title: 'Integration',\n description: 'Connect favorite tools',\n label: 'Connectivity'\n },\n {\n color: '#120F17',\n title: 'Security',\n description: 'Enterprise-grade protection',\n label: 'Protection'\n }\n];\n\nconst createParticleElement = (x, y, color = DEFAULT_GLOW_COLOR) => {\n const el = document.createElement('div');\n el.className = 'particle';\n el.style.cssText = `\n position: absolute;\n width: 4px;\n height: 4px;\n border-radius: 50%;\n background: rgba(${color}, 1);\n box-shadow: 0 0 6px rgba(${color}, 0.6);\n pointer-events: none;\n z-index: 100;\n left: ${x}px;\n top: ${y}px;\n `;\n return el;\n};\n\nconst calculateSpotlightValues = radius => ({\n proximity: radius * 0.5,\n fadeDistance: radius * 0.75\n});\n\nconst updateCardGlowProperties = (card, mouseX, mouseY, glow, radius) => {\n const rect = card.getBoundingClientRect();\n const relativeX = ((mouseX - rect.left) / rect.width) * 100;\n const relativeY = ((mouseY - rect.top) / rect.height) * 100;\n\n card.style.setProperty('--glow-x', `${relativeX}%`);\n card.style.setProperty('--glow-y', `${relativeY}%`);\n card.style.setProperty('--glow-intensity', glow.toString());\n card.style.setProperty('--glow-radius', `${radius}px`);\n};\n\nconst ParticleCard = ({\n children,\n className = '',\n disableAnimations = false,\n style,\n particleCount = DEFAULT_PARTICLE_COUNT,\n glowColor = DEFAULT_GLOW_COLOR,\n enableTilt = true,\n clickEffect = false,\n enableMagnetism = false\n}) => {\n const cardRef = useRef(null);\n const particlesRef = useRef([]);\n const timeoutsRef = useRef([]);\n const isHoveredRef = useRef(false);\n const memoizedParticles = useRef([]);\n const particlesInitialized = useRef(false);\n const magnetismAnimationRef = useRef(null);\n\n const initializeParticles = useCallback(() => {\n if (particlesInitialized.current || !cardRef.current) return;\n\n const { width, height } = cardRef.current.getBoundingClientRect();\n memoizedParticles.current = Array.from({ length: particleCount }, () =>\n createParticleElement(Math.random() * width, Math.random() * height, glowColor)\n );\n particlesInitialized.current = true;\n }, [particleCount, glowColor]);\n\n const clearAllParticles = useCallback(() => {\n timeoutsRef.current.forEach(clearTimeout);\n timeoutsRef.current = [];\n magnetismAnimationRef.current?.kill();\n\n particlesRef.current.forEach(particle => {\n gsap.to(particle, {\n scale: 0,\n opacity: 0,\n duration: 0.3,\n ease: 'back.in(1.7)',\n onComplete: () => {\n particle.parentNode?.removeChild(particle);\n }\n });\n });\n particlesRef.current = [];\n }, []);\n\n const animateParticles = useCallback(() => {\n if (!cardRef.current || !isHoveredRef.current) return;\n\n if (!particlesInitialized.current) {\n initializeParticles();\n }\n\n memoizedParticles.current.forEach((particle, index) => {\n const timeoutId = setTimeout(() => {\n if (!isHoveredRef.current || !cardRef.current) return;\n\n const clone = particle.cloneNode(true);\n cardRef.current.appendChild(clone);\n particlesRef.current.push(clone);\n\n gsap.fromTo(clone, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.3, ease: 'back.out(1.7)' });\n\n gsap.to(clone, {\n x: (Math.random() - 0.5) * 100,\n y: (Math.random() - 0.5) * 100,\n rotation: Math.random() * 360,\n duration: 2 + Math.random() * 2,\n ease: 'none',\n repeat: -1,\n yoyo: true\n });\n\n gsap.to(clone, {\n opacity: 0.3,\n duration: 1.5,\n ease: 'power2.inOut',\n repeat: -1,\n yoyo: true\n });\n }, index * 100);\n\n timeoutsRef.current.push(timeoutId);\n });\n }, [initializeParticles]);\n\n useEffect(() => {\n if (disableAnimations || !cardRef.current) return;\n\n const element = cardRef.current;\n\n const handleMouseEnter = () => {\n isHoveredRef.current = true;\n animateParticles();\n\n if (enableTilt) {\n gsap.to(element, {\n rotateX: 5,\n rotateY: 5,\n duration: 0.3,\n ease: 'power2.out',\n transformPerspective: 1000\n });\n }\n };\n\n const handleMouseLeave = () => {\n isHoveredRef.current = false;\n clearAllParticles();\n\n if (enableTilt) {\n gsap.to(element, {\n rotateX: 0,\n rotateY: 0,\n duration: 0.3,\n ease: 'power2.out'\n });\n }\n\n if (enableMagnetism) {\n gsap.to(element, {\n x: 0,\n y: 0,\n duration: 0.3,\n ease: 'power2.out'\n });\n }\n };\n\n const handleMouseMove = e => {\n if (!enableTilt && !enableMagnetism) return;\n\n const rect = element.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n\n if (enableTilt) {\n const rotateX = ((y - centerY) / centerY) * -10;\n const rotateY = ((x - centerX) / centerX) * 10;\n\n gsap.to(element, {\n rotateX,\n rotateY,\n duration: 0.1,\n ease: 'power2.out',\n transformPerspective: 1000\n });\n }\n\n if (enableMagnetism) {\n const magnetX = (x - centerX) * 0.05;\n const magnetY = (y - centerY) * 0.05;\n\n magnetismAnimationRef.current = gsap.to(element, {\n x: magnetX,\n y: magnetY,\n duration: 0.3,\n ease: 'power2.out'\n });\n }\n };\n\n const handleClick = e => {\n if (!clickEffect) return;\n\n const rect = element.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n\n const maxDistance = Math.max(\n Math.hypot(x, y),\n Math.hypot(x - rect.width, y),\n Math.hypot(x, y - rect.height),\n Math.hypot(x - rect.width, y - rect.height)\n );\n\n const ripple = document.createElement('div');\n ripple.style.cssText = `\n position: absolute;\n width: ${maxDistance * 2}px;\n height: ${maxDistance * 2}px;\n border-radius: 50%;\n background: radial-gradient(circle, rgba(${glowColor}, 0.4) 0%, rgba(${glowColor}, 0.2) 30%, transparent 70%);\n left: ${x - maxDistance}px;\n top: ${y - maxDistance}px;\n pointer-events: none;\n z-index: 1000;\n `;\n\n element.appendChild(ripple);\n\n gsap.fromTo(\n ripple,\n {\n scale: 0,\n opacity: 1\n },\n {\n scale: 1,\n opacity: 0,\n duration: 0.8,\n ease: 'power2.out',\n onComplete: () => ripple.remove()\n }\n );\n };\n\n element.addEventListener('mouseenter', handleMouseEnter);\n element.addEventListener('mouseleave', handleMouseLeave);\n element.addEventListener('mousemove', handleMouseMove);\n element.addEventListener('click', handleClick);\n\n return () => {\n isHoveredRef.current = false;\n element.removeEventListener('mouseenter', handleMouseEnter);\n element.removeEventListener('mouseleave', handleMouseLeave);\n element.removeEventListener('mousemove', handleMouseMove);\n element.removeEventListener('click', handleClick);\n clearAllParticles();\n };\n }, [animateParticles, clearAllParticles, disableAnimations, enableTilt, enableMagnetism, clickEffect, glowColor]);\n\n return (\n \n {children}\n
    \n );\n};\n\nconst GlobalSpotlight = ({\n gridRef,\n disableAnimations = false,\n enabled = true,\n spotlightRadius = DEFAULT_SPOTLIGHT_RADIUS,\n glowColor = DEFAULT_GLOW_COLOR\n}) => {\n const spotlightRef = useRef(null);\n const isInsideSection = useRef(false);\n\n useEffect(() => {\n if (disableAnimations || !gridRef?.current || !enabled) return;\n\n const spotlight = document.createElement('div');\n spotlight.className = 'global-spotlight';\n spotlight.style.cssText = `\n position: fixed;\n width: 800px;\n height: 800px;\n border-radius: 50%;\n pointer-events: none;\n background: radial-gradient(circle,\n rgba(${glowColor}, 0.15) 0%,\n rgba(${glowColor}, 0.08) 15%,\n rgba(${glowColor}, 0.04) 25%,\n rgba(${glowColor}, 0.02) 40%,\n rgba(${glowColor}, 0.01) 65%,\n transparent 70%\n );\n z-index: 200;\n opacity: 0;\n transform: translate(-50%, -50%);\n mix-blend-mode: screen;\n `;\n document.body.appendChild(spotlight);\n spotlightRef.current = spotlight;\n\n const handleMouseMove = e => {\n if (!spotlightRef.current || !gridRef.current) return;\n\n const section = gridRef.current.closest('.bento-section');\n const rect = section?.getBoundingClientRect();\n const mouseInside =\n rect && e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;\n\n isInsideSection.current = mouseInside || false;\n const cards = gridRef.current.querySelectorAll('.magic-bento-card');\n\n if (!mouseInside) {\n gsap.to(spotlightRef.current, {\n opacity: 0,\n duration: 0.3,\n ease: 'power2.out'\n });\n cards.forEach(card => {\n card.style.setProperty('--glow-intensity', '0');\n });\n return;\n }\n\n const { proximity, fadeDistance } = calculateSpotlightValues(spotlightRadius);\n let minDistance = Infinity;\n\n cards.forEach(card => {\n const cardElement = card;\n const cardRect = cardElement.getBoundingClientRect();\n const centerX = cardRect.left + cardRect.width / 2;\n const centerY = cardRect.top + cardRect.height / 2;\n const distance =\n Math.hypot(e.clientX - centerX, e.clientY - centerY) - Math.max(cardRect.width, cardRect.height) / 2;\n const effectiveDistance = Math.max(0, distance);\n\n minDistance = Math.min(minDistance, effectiveDistance);\n\n let glowIntensity = 0;\n if (effectiveDistance <= proximity) {\n glowIntensity = 1;\n } else if (effectiveDistance <= fadeDistance) {\n glowIntensity = (fadeDistance - effectiveDistance) / (fadeDistance - proximity);\n }\n\n updateCardGlowProperties(cardElement, e.clientX, e.clientY, glowIntensity, spotlightRadius);\n });\n\n gsap.to(spotlightRef.current, {\n left: e.clientX,\n top: e.clientY,\n duration: 0.1,\n ease: 'power2.out'\n });\n\n const targetOpacity =\n minDistance <= proximity\n ? 0.8\n : minDistance <= fadeDistance\n ? ((fadeDistance - minDistance) / (fadeDistance - proximity)) * 0.8\n : 0;\n\n gsap.to(spotlightRef.current, {\n opacity: targetOpacity,\n duration: targetOpacity > 0 ? 0.2 : 0.5,\n ease: 'power2.out'\n });\n };\n\n const handleMouseLeave = () => {\n isInsideSection.current = false;\n gridRef.current?.querySelectorAll('.magic-bento-card').forEach(card => {\n card.style.setProperty('--glow-intensity', '0');\n });\n if (spotlightRef.current) {\n gsap.to(spotlightRef.current, {\n opacity: 0,\n duration: 0.3,\n ease: 'power2.out'\n });\n }\n };\n\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseleave', handleMouseLeave);\n\n return () => {\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseleave', handleMouseLeave);\n spotlightRef.current?.parentNode?.removeChild(spotlightRef.current);\n };\n }, [gridRef, disableAnimations, enabled, spotlightRadius, glowColor]);\n\n return null;\n};\n\nconst BentoCardGrid = ({ children, gridRef }) => (\n
    \n {children}\n
    \n);\n\nconst useMobileDetection = () => {\n const [isMobile, setIsMobile] = useState(false);\n\n useEffect(() => {\n const checkMobile = () => setIsMobile(window.innerWidth <= MOBILE_BREAKPOINT);\n\n checkMobile();\n window.addEventListener('resize', checkMobile);\n\n return () => window.removeEventListener('resize', checkMobile);\n }, []);\n\n return isMobile;\n};\n\nconst MagicBento = ({\n textAutoHide = true,\n enableStars = true,\n enableSpotlight = true,\n enableBorderGlow = true,\n disableAnimations = false,\n spotlightRadius = DEFAULT_SPOTLIGHT_RADIUS,\n particleCount = DEFAULT_PARTICLE_COUNT,\n enableTilt = false,\n glowColor = DEFAULT_GLOW_COLOR,\n clickEffect = true,\n enableMagnetism = true\n}) => {\n const gridRef = useRef(null);\n const isMobile = useMobileDetection();\n const shouldDisableAnimations = disableAnimations || isMobile;\n\n return (\n <>\n {enableSpotlight && (\n \n )}\n\n \n {cardData.map((card, index) => {\n const baseClassName = `magic-bento-card ${textAutoHide ? 'magic-bento-card--text-autohide' : ''} ${enableBorderGlow ? 'magic-bento-card--border-glow' : ''}`;\n const cardProps = {\n className: baseClassName,\n style: {\n backgroundColor: card.color,\n '--glow-color': glowColor\n }\n };\n\n if (enableStars) {\n return (\n \n
    \n
    {card.label}
    \n
    \n
    \n

    {card.title}

    \n

    {card.description}

    \n
    \n \n );\n }\n\n return (\n {\n if (!el) return;\n\n const handleMouseMove = e => {\n if (shouldDisableAnimations) return;\n\n const rect = el.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n\n if (enableTilt) {\n const rotateX = ((y - centerY) / centerY) * -10;\n const rotateY = ((x - centerX) / centerX) * 10;\n gsap.to(el, {\n rotateX,\n rotateY,\n duration: 0.1,\n ease: 'power2.out',\n transformPerspective: 1000\n });\n }\n\n if (enableMagnetism) {\n const magnetX = (x - centerX) * 0.05;\n const magnetY = (y - centerY) * 0.05;\n gsap.to(el, {\n x: magnetX,\n y: magnetY,\n duration: 0.3,\n ease: 'power2.out'\n });\n }\n };\n\n const handleMouseLeave = () => {\n if (shouldDisableAnimations) return;\n\n if (enableTilt) {\n gsap.to(el, {\n rotateX: 0,\n rotateY: 0,\n duration: 0.3,\n ease: 'power2.out'\n });\n }\n\n if (enableMagnetism) {\n gsap.to(el, {\n x: 0,\n y: 0,\n duration: 0.3,\n ease: 'power2.out'\n });\n }\n };\n\n const handleClick = e => {\n if (!clickEffect || shouldDisableAnimations) return;\n\n const rect = el.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n\n const maxDistance = Math.max(\n Math.hypot(x, y),\n Math.hypot(x - rect.width, y),\n Math.hypot(x, y - rect.height),\n Math.hypot(x - rect.width, y - rect.height)\n );\n\n const ripple = document.createElement('div');\n ripple.style.cssText = `\n position: absolute;\n width: ${maxDistance * 2}px;\n height: ${maxDistance * 2}px;\n border-radius: 50%;\n background: radial-gradient(circle, rgba(${glowColor}, 0.4) 0%, rgba(${glowColor}, 0.2) 30%, transparent 70%);\n left: ${x - maxDistance}px;\n top: ${y - maxDistance}px;\n pointer-events: none;\n z-index: 1000;\n `;\n\n el.appendChild(ripple);\n\n gsap.fromTo(\n ripple,\n {\n scale: 0,\n opacity: 1\n },\n {\n scale: 1,\n opacity: 0,\n duration: 0.8,\n ease: 'power2.out',\n onComplete: () => ripple.remove()\n }\n );\n };\n\n el.addEventListener('mousemove', handleMouseMove);\n el.addEventListener('mouseleave', handleMouseLeave);\n el.addEventListener('click', handleClick);\n }}\n >\n
    \n
    {card.label}
    \n
    \n
    \n

    {card.title}

    \n

    {card.description}

    \n
    \n
    \n );\n })}\n \n \n );\n};\n\nexport default MagicBento;\n" } ], diff --git a/public/r/MagicBento-TS-CSS.json b/public/r/MagicBento-TS-CSS.json index 2f056287..1a70df63 100644 --- a/public/r/MagicBento-TS-CSS.json +++ b/public/r/MagicBento-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "MagicBento/MagicBento.css", + "type": "registry:file", + "path": "MagicBento.css", + "target": "@components/MagicBento.css", "content": ":root {\n --hue: 27;\n --sat: 69%;\n --white: hsl(0, 0%, 100%);\n --purple-primary: rgba(132, 0, 255, 1);\n --purple-glow: rgba(132, 0, 255, 0.2);\n --purple-border: rgba(132, 0, 255, 0.8);\n --border-color: #2F293A;\n --background-dark: #120F17;\n color-scheme: light dark;\n}\n\n.card-grid {\n display: grid;\n gap: 0.5em;\n padding: 0.75em;\n max-width: 54em;\n font-size: clamp(1rem, 0.9rem + 0.5vw, 1.5rem);\n}\n\n.magic-bento-card {\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n position: relative;\n aspect-ratio: 4/3;\n min-height: 200px;\n width: 100%;\n max-width: 100%;\n padding: 1.25em;\n border-radius: 20px;\n border: 1px solid var(--border-color);\n background: var(--background-dark);\n font-weight: 300;\n overflow: hidden;\n transition: all 0.3s ease;\n\n --glow-x: 50%;\n --glow-y: 50%;\n --glow-intensity: 0;\n --glow-radius: 200px;\n}\n\n.magic-bento-card:hover {\n transform: translateY(-2px);\n box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);\n}\n\n.magic-bento-card__header,\n.magic-bento-card__content {\n display: flex;\n position: relative;\n color: var(--white);\n}\n\n.magic-bento-card__header {\n gap: 0.75em;\n justify-content: space-between;\n}\n\n.magic-bento-card__content {\n flex-direction: column;\n}\n\n.magic-bento-card__label {\n font-size: 16px;\n}\n\n.magic-bento-card__title,\n.magic-bento-card__description {\n --clamp-title: 1;\n --clamp-desc: 2;\n}\n\n.magic-bento-card__title {\n font-weight: 400;\n font-size: 16px;\n margin: 0 0 0.25em;\n}\n\n.magic-bento-card__description {\n font-size: 12px;\n line-height: 1.2;\n opacity: 0.9;\n}\n\n.magic-bento-card--text-autohide .magic-bento-card__title,\n.magic-bento-card--text-autohide .magic-bento-card__description {\n display: -webkit-box;\n -webkit-box-orient: vertical;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.magic-bento-card--text-autohide .magic-bento-card__title {\n -webkit-line-clamp: var(--clamp-title);\n line-clamp: var(--clamp-title);\n}\n\n.magic-bento-card--text-autohide .magic-bento-card__description {\n -webkit-line-clamp: var(--clamp-desc);\n line-clamp: var(--clamp-desc);\n}\n\n@media (max-width: 599px) {\n .card-grid {\n grid-template-columns: 1fr;\n width: 90%;\n margin: 0 auto;\n padding: 0.5em;\n }\n\n .magic-bento-card {\n width: 100%;\n min-height: 180px;\n }\n}\n\n@media (min-width: 600px) {\n .card-grid {\n grid-template-columns: repeat(2, 1fr);\n }\n}\n\n@media (min-width: 1024px) {\n .card-grid {\n grid-template-columns: repeat(4, 1fr);\n }\n\n .magic-bento-card:nth-child(3) {\n grid-column: span 2;\n grid-row: span 2;\n }\n\n .magic-bento-card:nth-child(4) {\n grid-column: 1 / span 2;\n grid-row: 2 / span 2;\n }\n\n .magic-bento-card:nth-child(6) {\n grid-column: 4;\n grid-row: 3;\n }\n}\n\n/* Border glow effect */\n.magic-bento-card--border-glow::after {\n content: '';\n position: absolute;\n inset: 0;\n padding: 6px;\n background: radial-gradient(\n var(--glow-radius) circle at var(--glow-x) var(--glow-y),\n rgba(132, 0, 255, calc(var(--glow-intensity) * 0.8)) 0%,\n rgba(132, 0, 255, calc(var(--glow-intensity) * 0.4)) 30%,\n transparent 60%\n );\n border-radius: inherit;\n -webkit-mask:\n linear-gradient(#fff 0 0) content-box,\n linear-gradient(#fff 0 0);\n -webkit-mask-composite: xor;\n mask:\n linear-gradient(#fff 0 0) content-box,\n linear-gradient(#fff 0 0);\n mask-composite: exclude;\n pointer-events: none;\n opacity: 1;\n transition: opacity 0.3s ease;\n z-index: 1;\n}\n\n.magic-bento-card--border-glow:hover::after {\n opacity: 1;\n}\n\n.magic-bento-card--border-glow:hover {\n box-shadow:\n 0 4px 20px rgba(46, 24, 78, 0.4),\n 0 0 30px var(--purple-glow);\n}\n\n.particle-container {\n position: relative;\n overflow: hidden;\n}\n\n.particle::before {\n content: '';\n position: absolute;\n top: -2px;\n left: -2px;\n right: -2px;\n bottom: -2px;\n background: rgba(132, 0, 255, 0.2);\n border-radius: 50%;\n z-index: -1;\n}\n\n.particle-container:hover {\n box-shadow:\n 0 4px 20px rgba(46, 24, 78, 0.2),\n 0 0 30px var(--purple-glow);\n}\n\n/* Global spotlight styles */\n.global-spotlight {\n mix-blend-mode: screen;\n will-change: transform, opacity;\n z-index: 200 !important;\n pointer-events: none;\n}\n\n.bento-section {\n position: relative;\n user-select: none;\n}\n" }, { "type": "registry:component", - "path": "MagicBento/MagicBento.tsx", + "path": "MagicBento.tsx", "content": "import React, { useRef, useEffect, useCallback, useState } from 'react';\nimport { gsap } from 'gsap';\nimport './MagicBento.css';\n\nexport interface BentoCardProps {\n color?: string;\n title?: string;\n description?: string;\n label?: string;\n textAutoHide?: boolean;\n disableAnimations?: boolean;\n}\n\nexport interface BentoProps {\n textAutoHide?: boolean;\n enableStars?: boolean;\n enableSpotlight?: boolean;\n enableBorderGlow?: boolean;\n disableAnimations?: boolean;\n spotlightRadius?: number;\n particleCount?: number;\n enableTilt?: boolean;\n glowColor?: string;\n clickEffect?: boolean;\n enableMagnetism?: boolean;\n}\n\nconst DEFAULT_PARTICLE_COUNT = 12;\nconst DEFAULT_SPOTLIGHT_RADIUS = 300;\nconst DEFAULT_GLOW_COLOR = '132, 0, 255';\nconst MOBILE_BREAKPOINT = 768;\n\nconst cardData: BentoCardProps[] = [\n {\n color: '#120F17',\n title: 'Analytics',\n description: 'Track user behavior',\n label: 'Insights'\n },\n {\n color: '#120F17',\n title: 'Dashboard',\n description: 'Centralized data view',\n label: 'Overview'\n },\n {\n color: '#120F17',\n title: 'Collaboration',\n description: 'Work together seamlessly',\n label: 'Teamwork'\n },\n {\n color: '#120F17',\n title: 'Automation',\n description: 'Streamline workflows',\n label: 'Efficiency'\n },\n {\n color: '#120F17',\n title: 'Integration',\n description: 'Connect favorite tools',\n label: 'Connectivity'\n },\n {\n color: '#120F17',\n title: 'Security',\n description: 'Enterprise-grade protection',\n label: 'Protection'\n }\n];\n\nconst createParticleElement = (x: number, y: number, color: string = DEFAULT_GLOW_COLOR): HTMLDivElement => {\n const el = document.createElement('div');\n el.className = 'particle';\n el.style.cssText = `\n position: absolute;\n width: 4px;\n height: 4px;\n border-radius: 50%;\n background: rgba(${color}, 1);\n box-shadow: 0 0 6px rgba(${color}, 0.6);\n pointer-events: none;\n z-index: 100;\n left: ${x}px;\n top: ${y}px;\n `;\n return el;\n};\n\nconst calculateSpotlightValues = (radius: number) => ({\n proximity: radius * 0.5,\n fadeDistance: radius * 0.75\n});\n\nconst updateCardGlowProperties = (card: HTMLElement, mouseX: number, mouseY: number, glow: number, radius: number) => {\n const rect = card.getBoundingClientRect();\n const relativeX = ((mouseX - rect.left) / rect.width) * 100;\n const relativeY = ((mouseY - rect.top) / rect.height) * 100;\n\n card.style.setProperty('--glow-x', `${relativeX}%`);\n card.style.setProperty('--glow-y', `${relativeY}%`);\n card.style.setProperty('--glow-intensity', glow.toString());\n card.style.setProperty('--glow-radius', `${radius}px`);\n};\n\nconst ParticleCard: React.FC<{\n children: React.ReactNode;\n className?: string;\n disableAnimations?: boolean;\n style?: React.CSSProperties;\n particleCount?: number;\n glowColor?: string;\n enableTilt?: boolean;\n clickEffect?: boolean;\n enableMagnetism?: boolean;\n}> = ({\n children,\n className = '',\n disableAnimations = false,\n style,\n particleCount = DEFAULT_PARTICLE_COUNT,\n glowColor = DEFAULT_GLOW_COLOR,\n enableTilt = true,\n clickEffect = false,\n enableMagnetism = false\n}) => {\n const cardRef = useRef(null);\n const particlesRef = useRef([]);\n const timeoutsRef = useRef[]>([]);\n const isHoveredRef = useRef(false);\n const memoizedParticles = useRef([]);\n const particlesInitialized = useRef(false);\n const magnetismAnimationRef = useRef(null);\n\n const initializeParticles = useCallback(() => {\n if (particlesInitialized.current || !cardRef.current) return;\n\n const { width, height } = cardRef.current.getBoundingClientRect();\n memoizedParticles.current = Array.from({ length: particleCount }, () =>\n createParticleElement(Math.random() * width, Math.random() * height, glowColor)\n );\n particlesInitialized.current = true;\n }, [particleCount, glowColor]);\n\n const clearAllParticles = useCallback(() => {\n timeoutsRef.current.forEach(clearTimeout);\n timeoutsRef.current = [];\n magnetismAnimationRef.current?.kill();\n\n particlesRef.current.forEach(particle => {\n gsap.to(particle, {\n scale: 0,\n opacity: 0,\n duration: 0.3,\n ease: 'back.in(1.7)',\n onComplete: () => {\n particle.parentNode?.removeChild(particle);\n }\n });\n });\n particlesRef.current = [];\n }, []);\n\n const animateParticles = useCallback(() => {\n if (!cardRef.current || !isHoveredRef.current) return;\n\n if (!particlesInitialized.current) {\n initializeParticles();\n }\n\n memoizedParticles.current.forEach((particle, index) => {\n const timeoutId = setTimeout(() => {\n if (!isHoveredRef.current || !cardRef.current) return;\n\n const clone = particle.cloneNode(true) as HTMLDivElement;\n cardRef.current.appendChild(clone);\n particlesRef.current.push(clone);\n\n gsap.fromTo(clone, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.3, ease: 'back.out(1.7)' });\n\n gsap.to(clone, {\n x: (Math.random() - 0.5) * 100,\n y: (Math.random() - 0.5) * 100,\n rotation: Math.random() * 360,\n duration: 2 + Math.random() * 2,\n ease: 'none',\n repeat: -1,\n yoyo: true\n });\n\n gsap.to(clone, {\n opacity: 0.3,\n duration: 1.5,\n ease: 'power2.inOut',\n repeat: -1,\n yoyo: true\n });\n }, index * 100);\n\n timeoutsRef.current.push(timeoutId);\n });\n }, [initializeParticles]);\n\n useEffect(() => {\n if (disableAnimations || !cardRef.current) return;\n\n const element = cardRef.current;\n\n const handleMouseEnter = () => {\n isHoveredRef.current = true;\n animateParticles();\n\n if (enableTilt) {\n gsap.to(element, {\n rotateX: 5,\n rotateY: 5,\n duration: 0.3,\n ease: 'power2.out',\n transformPerspective: 1000\n });\n }\n };\n\n const handleMouseLeave = () => {\n isHoveredRef.current = false;\n clearAllParticles();\n\n if (enableTilt) {\n gsap.to(element, {\n rotateX: 0,\n rotateY: 0,\n duration: 0.3,\n ease: 'power2.out'\n });\n }\n\n if (enableMagnetism) {\n gsap.to(element, {\n x: 0,\n y: 0,\n duration: 0.3,\n ease: 'power2.out'\n });\n }\n };\n\n const handleMouseMove = (e: MouseEvent) => {\n if (!enableTilt && !enableMagnetism) return;\n\n const rect = element.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n\n if (enableTilt) {\n const rotateX = ((y - centerY) / centerY) * -10;\n const rotateY = ((x - centerX) / centerX) * 10;\n\n gsap.to(element, {\n rotateX,\n rotateY,\n duration: 0.1,\n ease: 'power2.out',\n transformPerspective: 1000\n });\n }\n\n if (enableMagnetism) {\n const magnetX = (x - centerX) * 0.05;\n const magnetY = (y - centerY) * 0.05;\n\n magnetismAnimationRef.current = gsap.to(element, {\n x: magnetX,\n y: magnetY,\n duration: 0.3,\n ease: 'power2.out'\n });\n }\n };\n\n const handleClick = (e: MouseEvent) => {\n if (!clickEffect) return;\n\n const rect = element.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n\n const maxDistance = Math.max(\n Math.hypot(x, y),\n Math.hypot(x - rect.width, y),\n Math.hypot(x, y - rect.height),\n Math.hypot(x - rect.width, y - rect.height)\n );\n\n const ripple = document.createElement('div');\n ripple.style.cssText = `\n position: absolute;\n width: ${maxDistance * 2}px;\n height: ${maxDistance * 2}px;\n border-radius: 50%;\n background: radial-gradient(circle, rgba(${glowColor}, 0.4) 0%, rgba(${glowColor}, 0.2) 30%, transparent 70%);\n left: ${x - maxDistance}px;\n top: ${y - maxDistance}px;\n pointer-events: none;\n z-index: 1000;\n `;\n\n element.appendChild(ripple);\n\n gsap.fromTo(\n ripple,\n {\n scale: 0,\n opacity: 1\n },\n {\n scale: 1,\n opacity: 0,\n duration: 0.8,\n ease: 'power2.out',\n onComplete: () => ripple.remove()\n }\n );\n };\n\n element.addEventListener('mouseenter', handleMouseEnter);\n element.addEventListener('mouseleave', handleMouseLeave);\n element.addEventListener('mousemove', handleMouseMove);\n element.addEventListener('click', handleClick);\n\n return () => {\n isHoveredRef.current = false;\n element.removeEventListener('mouseenter', handleMouseEnter);\n element.removeEventListener('mouseleave', handleMouseLeave);\n element.removeEventListener('mousemove', handleMouseMove);\n element.removeEventListener('click', handleClick);\n clearAllParticles();\n };\n }, [animateParticles, clearAllParticles, disableAnimations, enableTilt, enableMagnetism, clickEffect, glowColor]);\n\n return (\n \n {children}\n
    \n );\n};\n\nconst GlobalSpotlight: React.FC<{\n gridRef: React.RefObject;\n disableAnimations?: boolean;\n enabled?: boolean;\n spotlightRadius?: number;\n glowColor?: string;\n}> = ({\n gridRef,\n disableAnimations = false,\n enabled = true,\n spotlightRadius = DEFAULT_SPOTLIGHT_RADIUS,\n glowColor = DEFAULT_GLOW_COLOR\n}) => {\n const spotlightRef = useRef(null);\n const isInsideSection = useRef(false);\n\n useEffect(() => {\n if (disableAnimations || !gridRef?.current || !enabled) return;\n\n const spotlight = document.createElement('div');\n spotlight.className = 'global-spotlight';\n spotlight.style.cssText = `\n position: fixed;\n width: 800px;\n height: 800px;\n border-radius: 50%;\n pointer-events: none;\n background: radial-gradient(circle,\n rgba(${glowColor}, 0.15) 0%,\n rgba(${glowColor}, 0.08) 15%,\n rgba(${glowColor}, 0.04) 25%,\n rgba(${glowColor}, 0.02) 40%,\n rgba(${glowColor}, 0.01) 65%,\n transparent 70%\n );\n z-index: 200;\n opacity: 0;\n transform: translate(-50%, -50%);\n mix-blend-mode: screen;\n `;\n document.body.appendChild(spotlight);\n spotlightRef.current = spotlight;\n\n const handleMouseMove = (e: MouseEvent) => {\n if (!spotlightRef.current || !gridRef.current) return;\n\n const section = gridRef.current.closest('.bento-section');\n const rect = section?.getBoundingClientRect();\n const mouseInside =\n rect && e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;\n\n isInsideSection.current = mouseInside || false;\n const cards = gridRef.current.querySelectorAll('.magic-bento-card');\n\n if (!mouseInside) {\n gsap.to(spotlightRef.current, {\n opacity: 0,\n duration: 0.3,\n ease: 'power2.out'\n });\n cards.forEach(card => {\n (card as HTMLElement).style.setProperty('--glow-intensity', '0');\n });\n return;\n }\n\n const { proximity, fadeDistance } = calculateSpotlightValues(spotlightRadius);\n let minDistance = Infinity;\n\n cards.forEach(card => {\n const cardElement = card as HTMLElement;\n const cardRect = cardElement.getBoundingClientRect();\n const centerX = cardRect.left + cardRect.width / 2;\n const centerY = cardRect.top + cardRect.height / 2;\n const distance =\n Math.hypot(e.clientX - centerX, e.clientY - centerY) - Math.max(cardRect.width, cardRect.height) / 2;\n const effectiveDistance = Math.max(0, distance);\n\n minDistance = Math.min(minDistance, effectiveDistance);\n\n let glowIntensity = 0;\n if (effectiveDistance <= proximity) {\n glowIntensity = 1;\n } else if (effectiveDistance <= fadeDistance) {\n glowIntensity = (fadeDistance - effectiveDistance) / (fadeDistance - proximity);\n }\n\n updateCardGlowProperties(cardElement, e.clientX, e.clientY, glowIntensity, spotlightRadius);\n });\n\n gsap.to(spotlightRef.current, {\n left: e.clientX,\n top: e.clientY,\n duration: 0.1,\n ease: 'power2.out'\n });\n\n const targetOpacity =\n minDistance <= proximity\n ? 0.8\n : minDistance <= fadeDistance\n ? ((fadeDistance - minDistance) / (fadeDistance - proximity)) * 0.8\n : 0;\n\n gsap.to(spotlightRef.current, {\n opacity: targetOpacity,\n duration: targetOpacity > 0 ? 0.2 : 0.5,\n ease: 'power2.out'\n });\n };\n\n const handleMouseLeave = () => {\n isInsideSection.current = false;\n gridRef.current?.querySelectorAll('.magic-bento-card').forEach(card => {\n (card as HTMLElement).style.setProperty('--glow-intensity', '0');\n });\n if (spotlightRef.current) {\n gsap.to(spotlightRef.current, {\n opacity: 0,\n duration: 0.3,\n ease: 'power2.out'\n });\n }\n };\n\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseleave', handleMouseLeave);\n\n return () => {\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseleave', handleMouseLeave);\n spotlightRef.current?.parentNode?.removeChild(spotlightRef.current);\n };\n }, [gridRef, disableAnimations, enabled, spotlightRadius, glowColor]);\n\n return null;\n};\n\nconst BentoCardGrid: React.FC<{\n children: React.ReactNode;\n gridRef?: React.RefObject;\n}> = ({ children, gridRef }) => (\n
    \n {children}\n
    \n);\n\nconst useMobileDetection = () => {\n const [isMobile, setIsMobile] = useState(false);\n\n useEffect(() => {\n const checkMobile = () => setIsMobile(window.innerWidth <= MOBILE_BREAKPOINT);\n\n checkMobile();\n window.addEventListener('resize', checkMobile);\n\n return () => window.removeEventListener('resize', checkMobile);\n }, []);\n\n return isMobile;\n};\n\nconst MagicBento: React.FC = ({\n textAutoHide = true,\n enableStars = true,\n enableSpotlight = true,\n enableBorderGlow = true,\n disableAnimations = false,\n spotlightRadius = DEFAULT_SPOTLIGHT_RADIUS,\n particleCount = DEFAULT_PARTICLE_COUNT,\n enableTilt = false,\n glowColor = DEFAULT_GLOW_COLOR,\n clickEffect = true,\n enableMagnetism = true\n}) => {\n const gridRef = useRef(null);\n const isMobile = useMobileDetection();\n const shouldDisableAnimations = disableAnimations || isMobile;\n\n return (\n <>\n {enableSpotlight && (\n \n )}\n\n \n {cardData.map((card, index) => {\n const baseClassName = `magic-bento-card ${textAutoHide ? 'magic-bento-card--text-autohide' : ''} ${enableBorderGlow ? 'magic-bento-card--border-glow' : ''}`;\n const cardProps = {\n className: baseClassName,\n style: {\n backgroundColor: card.color,\n '--glow-color': glowColor\n } as React.CSSProperties\n };\n\n if (enableStars) {\n return (\n \n
    \n
    {card.label}
    \n
    \n
    \n

    {card.title}

    \n

    {card.description}

    \n
    \n \n );\n }\n\n return (\n {\n if (!el) return;\n\n const handleMouseMove = (e: MouseEvent) => {\n if (shouldDisableAnimations) return;\n\n const rect = el.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n\n if (enableTilt) {\n const rotateX = ((y - centerY) / centerY) * -10;\n const rotateY = ((x - centerX) / centerX) * 10;\n gsap.to(el, {\n rotateX,\n rotateY,\n duration: 0.1,\n ease: 'power2.out',\n transformPerspective: 1000\n });\n }\n\n if (enableMagnetism) {\n const magnetX = (x - centerX) * 0.05;\n const magnetY = (y - centerY) * 0.05;\n gsap.to(el, {\n x: magnetX,\n y: magnetY,\n duration: 0.3,\n ease: 'power2.out'\n });\n }\n };\n\n const handleMouseLeave = () => {\n if (shouldDisableAnimations) return;\n\n if (enableTilt) {\n gsap.to(el, {\n rotateX: 0,\n rotateY: 0,\n duration: 0.3,\n ease: 'power2.out'\n });\n }\n\n if (enableMagnetism) {\n gsap.to(el, {\n x: 0,\n y: 0,\n duration: 0.3,\n ease: 'power2.out'\n });\n }\n };\n\n const handleClick = (e: MouseEvent) => {\n if (!clickEffect || shouldDisableAnimations) return;\n\n const rect = el.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n\n // Calculate the maximum distance from click point to any corner\n const maxDistance = Math.max(\n Math.hypot(x, y),\n Math.hypot(x - rect.width, y),\n Math.hypot(x, y - rect.height),\n Math.hypot(x - rect.width, y - rect.height)\n );\n\n const ripple = document.createElement('div');\n ripple.style.cssText = `\n position: absolute;\n width: ${maxDistance * 2}px;\n height: ${maxDistance * 2}px;\n border-radius: 50%;\n background: radial-gradient(circle, rgba(${glowColor}, 0.4) 0%, rgba(${glowColor}, 0.2) 30%, transparent 70%);\n left: ${x - maxDistance}px;\n top: ${y - maxDistance}px;\n pointer-events: none;\n z-index: 1000;\n `;\n\n el.appendChild(ripple);\n\n gsap.fromTo(\n ripple,\n {\n scale: 0,\n opacity: 1\n },\n {\n scale: 1,\n opacity: 0,\n duration: 0.8,\n ease: 'power2.out',\n onComplete: () => ripple.remove()\n }\n );\n };\n\n el.addEventListener('mousemove', handleMouseMove);\n el.addEventListener('mouseleave', handleMouseLeave);\n el.addEventListener('click', handleClick);\n }}\n >\n
    \n
    {card.label}
    \n
    \n
    \n

    {card.title}

    \n

    {card.description}

    \n
    \n
    \n );\n })}\n \n \n );\n};\n\nexport default MagicBento;\n" } ], diff --git a/public/r/MagicRings-JS-CSS.json b/public/r/MagicRings-JS-CSS.json index 9ea38186..d6eaa8ec 100644 --- a/public/r/MagicRings-JS-CSS.json +++ b/public/r/MagicRings-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "MagicRings/MagicRings.css", + "type": "registry:file", + "path": "MagicRings.css", + "target": "@components/MagicRings.css", "content": ".magic-rings-container {\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "MagicRings/MagicRings.jsx", + "path": "MagicRings.jsx", "content": "import { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\nimport './MagicRings.css';\n\nconst vertexShader = `\nvoid main() {\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float uTime, uAttenuation, uLineThickness;\nuniform float uBaseRadius, uRadiusStep, uScaleRate;\nuniform float uOpacity, uNoiseAmount, uRotation, uRingGap;\nuniform float uFadeIn, uFadeOut;\nuniform float uMouseInfluence, uHoverAmount, uHoverScale, uParallax, uBurst;\nuniform vec2 uResolution, uMouse;\nuniform vec3 uColor, uColorTwo;\nuniform int uRingCount;\n\nconst float HP = 1.5707963;\nconst float CYCLE = 3.45;\n\nfloat fade(float t) {\n return t < uFadeIn ? smoothstep(0.0, uFadeIn, t) : 1.0 - smoothstep(uFadeOut, CYCLE - 0.2, t);\n}\n\nfloat ring(vec2 p, float ri, float cut, float t0, float px) {\n float t = mod(uTime + t0, CYCLE);\n float r = ri + t / CYCLE * uScaleRate;\n float d = abs(length(p) - r);\n float a = atan(abs(p.y), abs(p.x)) / HP;\n float th = max(1.0 - a, 0.5) * px * uLineThickness;\n float h = (1.0 - smoothstep(th, th * 1.5, d)) + 1.0;\n d += pow(cut * a, 3.0) * r;\n return h * exp(-uAttenuation * d) * fade(t);\n}\n\nvoid main() {\n float px = 1.0 / min(uResolution.x, uResolution.y);\n vec2 p = (gl_FragCoord.xy - 0.5 * uResolution.xy) * px;\n float cr = cos(uRotation), sr = sin(uRotation);\n p = mat2(cr, -sr, sr, cr) * p;\n p -= uMouse * uMouseInfluence;\n float sc = mix(1.0, uHoverScale, uHoverAmount) + uBurst * 0.3;\n p /= sc;\n vec3 c = vec3(0.0);\n float rcf = max(float(uRingCount) - 1.0, 1.0);\n for (int i = 0; i < 10; i++) {\n if (i >= uRingCount) break;\n float fi = float(i);\n vec2 pr = p - fi * uParallax * uMouse;\n vec3 rc = mix(uColor, uColorTwo, fi / rcf);\n c = mix(c, rc, vec3(ring(pr, uBaseRadius + fi * uRadiusStep, pow(uRingGap, fi), i == 0 ? 0.0 : 2.95 * fi, px)));\n }\n c *= 1.0 + uBurst * 2.0;\n float n = fract(sin(dot(gl_FragCoord.xy + uTime * 100.0, vec2(12.9898, 78.233))) * 43758.5453);\n c += (n - 0.5) * uNoiseAmount;\n gl_FragColor = vec4(c, max(c.r, max(c.g, c.b)) * uOpacity);\n}\n`;\n\nexport default function MagicRings({\n color = '#fc42ff',\n colorTwo = '#42fcff',\n speed = 1,\n ringCount = 6,\n attenuation = 10,\n lineThickness = 2,\n baseRadius = 0.35,\n radiusStep = 0.1,\n scaleRate = 0.1,\n opacity = 1,\n blur = 0,\n noiseAmount = 0.1,\n rotation = 0,\n ringGap = 1.5,\n fadeIn = 0.7,\n fadeOut = 0.5,\n followMouse = false,\n mouseInfluence = 0.2,\n hoverScale = 1.2,\n parallax = 0.05,\n clickBurst = false,\n}) {\n const mountRef = useRef(null);\n const propsRef = useRef(null);\n const mouseRef = useRef([0, 0]);\n const smoothMouseRef = useRef([0, 0]);\n const hoverAmountRef = useRef(0);\n const isHoveredRef = useRef(false);\n const burstRef = useRef(0);\n\n propsRef.current = {\n color, colorTwo, speed, ringCount, attenuation, lineThickness,\n baseRadius, radiusStep, scaleRate, opacity, noiseAmount,\n rotation, ringGap, fadeIn, fadeOut, followMouse, mouseInfluence,\n hoverScale, parallax, clickBurst,\n };\n\n useEffect(() => {\n const mount = mountRef.current;\n if (!mount) return;\n\n let renderer;\n try {\n renderer = new THREE.WebGLRenderer({ alpha: true });\n } catch {\n return;\n }\n\n if (!renderer.capabilities.isWebGL2) {\n renderer.dispose();\n return;\n }\n\n renderer.setClearColor(0x000000, 0);\n mount.appendChild(renderer.domElement);\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-0.5, 0.5, 0.5, -0.5, 0.1, 10);\n camera.position.z = 1;\n\n const uniforms = {\n uTime: { value: 0 },\n uAttenuation: { value: 0 },\n uResolution: { value: new THREE.Vector2() },\n uColor: { value: new THREE.Color() },\n uColorTwo: { value: new THREE.Color() },\n uLineThickness: { value: 0 },\n uBaseRadius: { value: 0 },\n uRadiusStep: { value: 0 },\n uScaleRate: { value: 0 },\n uRingCount: { value: 0 },\n uOpacity: { value: 1 },\n uNoiseAmount: { value: 0 },\n uRotation: { value: 0 },\n uRingGap: { value: 1.6 },\n uFadeIn: { value: 0.5 },\n uFadeOut: { value: 0.75 },\n uMouse: { value: new THREE.Vector2() },\n uMouseInfluence: { value: 0 },\n uHoverAmount: { value: 0 },\n uHoverScale: { value: 1 },\n uParallax: { value: 0 },\n uBurst: { value: 0 },\n };\n\n const material = new THREE.ShaderMaterial({ vertexShader, fragmentShader, uniforms, transparent: true });\n const quad = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material);\n scene.add(quad);\n\n const resize = () => {\n const w = mount.clientWidth;\n const h = mount.clientHeight;\n const dpr = Math.min(window.devicePixelRatio, 2);\n renderer.setSize(w, h);\n renderer.setPixelRatio(dpr);\n uniforms.uResolution.value.set(w * dpr, h * dpr);\n };\n resize();\n window.addEventListener('resize', resize);\n\n const ro = new ResizeObserver(resize);\n ro.observe(mount);\n\n const onMouseMove = (e) => {\n const rect = mount.getBoundingClientRect();\n mouseRef.current[0] = (e.clientX - rect.left) / rect.width - 0.5;\n mouseRef.current[1] = -((e.clientY - rect.top) / rect.height - 0.5);\n };\n const onMouseEnter = () => { isHoveredRef.current = true; };\n const onMouseLeave = () => {\n isHoveredRef.current = false;\n mouseRef.current[0] = 0;\n mouseRef.current[1] = 0;\n };\n const onClick = () => { burstRef.current = 1; };\n\n mount.addEventListener('mousemove', onMouseMove);\n mount.addEventListener('mouseenter', onMouseEnter);\n mount.addEventListener('mouseleave', onMouseLeave);\n mount.addEventListener('click', onClick);\n\n let frameId = 0;\n let isVisible = false;\n let isPageVisible = !document.hidden;\n let elapsed = 0;\n let lastT = 0;\n const animate = (t) => {\n frameId = requestAnimationFrame(animate);\n const p = propsRef.current;\n\n const dt = lastT === 0 ? 0 : Math.min(t - lastT, 100);\n lastT = t;\n elapsed += dt * 0.001 * p.speed;\n\n smoothMouseRef.current[0] += (mouseRef.current[0] - smoothMouseRef.current[0]) * 0.08;\n smoothMouseRef.current[1] += (mouseRef.current[1] - smoothMouseRef.current[1]) * 0.08;\n hoverAmountRef.current += ((isHoveredRef.current ? 1 : 0) - hoverAmountRef.current) * 0.08;\n burstRef.current *= 0.95;\n if (burstRef.current < 0.001) burstRef.current = 0;\n\n uniforms.uTime.value = elapsed;\n uniforms.uAttenuation.value = p.attenuation;\n uniforms.uColor.value.set(p.color);\n uniforms.uColorTwo.value.set(p.colorTwo);\n uniforms.uLineThickness.value = p.lineThickness;\n uniforms.uBaseRadius.value = p.baseRadius;\n uniforms.uRadiusStep.value = p.radiusStep;\n uniforms.uScaleRate.value = p.scaleRate;\n uniforms.uRingCount.value = p.ringCount;\n uniforms.uOpacity.value = p.opacity;\n uniforms.uNoiseAmount.value = p.noiseAmount;\n uniforms.uRotation.value = (p.rotation * Math.PI) / 180;\n uniforms.uRingGap.value = p.ringGap;\n uniforms.uFadeIn.value = p.fadeIn;\n uniforms.uFadeOut.value = p.fadeOut;\n uniforms.uMouse.value.set(smoothMouseRef.current[0], smoothMouseRef.current[1]);\n uniforms.uMouseInfluence.value = p.followMouse ? p.mouseInfluence : 0;\n uniforms.uHoverAmount.value = hoverAmountRef.current;\n uniforms.uHoverScale.value = p.hoverScale;\n uniforms.uParallax.value = p.parallax;\n uniforms.uBurst.value = p.clickBurst ? burstRef.current : 0;\n\n renderer.render(scene, camera);\n };\n frameId = 0;\n\n const tryStart = () => {\n if (isVisible && isPageVisible && frameId === 0) {\n lastT = 0;\n frameId = requestAnimationFrame(animate);\n }\n };\n const tryStop = () => {\n if (frameId !== 0) {\n cancelAnimationFrame(frameId);\n frameId = 0;\n }\n };\n\n const io = new IntersectionObserver(\n ([entry]) => {\n isVisible = entry.isIntersecting;\n isVisible ? tryStart() : tryStop();\n },\n { threshold: 0 }\n );\n io.observe(mount);\n\n const onVisibility = () => {\n isPageVisible = !document.hidden;\n isPageVisible ? tryStart() : tryStop();\n };\n document.addEventListener('visibilitychange', onVisibility);\n\n tryStart();\n\n return () => {\n tryStop();\n io.disconnect();\n document.removeEventListener('visibilitychange', onVisibility);\n window.removeEventListener('resize', resize);\n ro.disconnect();\n mount.removeEventListener('mousemove', onMouseMove);\n mount.removeEventListener('mouseenter', onMouseEnter);\n mount.removeEventListener('mouseleave', onMouseLeave);\n mount.removeEventListener('click', onClick);\n mount.removeChild(renderer.domElement);\n renderer.dispose();\n material.dispose();\n };\n }, []);\n\n return
    0 ? { filter: `blur(${blur}px)` } : undefined} />;\n}\n" } ], diff --git a/public/r/MagicRings-TS-CSS.json b/public/r/MagicRings-TS-CSS.json index 262a0bf8..88bd4513 100644 --- a/public/r/MagicRings-TS-CSS.json +++ b/public/r/MagicRings-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "MagicRings/MagicRings.css", + "type": "registry:file", + "path": "MagicRings.css", + "target": "@components/MagicRings.css", "content": ".magic-rings-container {\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "MagicRings/MagicRings.tsx", + "path": "MagicRings.tsx", "content": "import { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\nimport './MagicRings.css';\n\nconst vertexShader = `\nvoid main() {\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float uTime, uAttenuation, uLineThickness;\nuniform float uBaseRadius, uRadiusStep, uScaleRate;\nuniform float uOpacity, uNoiseAmount, uRotation, uRingGap;\nuniform float uFadeIn, uFadeOut;\nuniform float uMouseInfluence, uHoverAmount, uHoverScale, uParallax, uBurst;\nuniform vec2 uResolution, uMouse;\nuniform vec3 uColor, uColorTwo;\nuniform int uRingCount;\n\nconst float HP = 1.5707963;\nconst float CYCLE = 3.45;\n\nfloat fade(float t) {\n return t < uFadeIn ? smoothstep(0.0, uFadeIn, t) : 1.0 - smoothstep(uFadeOut, CYCLE - 0.2, t);\n}\n\nfloat ring(vec2 p, float ri, float cut, float t0, float px) {\n float t = mod(uTime + t0, CYCLE);\n float r = ri + t / CYCLE * uScaleRate;\n float d = abs(length(p) - r);\n float a = atan(abs(p.y), abs(p.x)) / HP;\n float th = max(1.0 - a, 0.5) * px * uLineThickness;\n float h = (1.0 - smoothstep(th, th * 1.5, d)) + 1.0;\n d += pow(cut * a, 3.0) * r;\n return h * exp(-uAttenuation * d) * fade(t);\n}\n\nvoid main() {\n float px = 1.0 / min(uResolution.x, uResolution.y);\n vec2 p = (gl_FragCoord.xy - 0.5 * uResolution.xy) * px;\n float cr = cos(uRotation), sr = sin(uRotation);\n p = mat2(cr, -sr, sr, cr) * p;\n p -= uMouse * uMouseInfluence;\n float sc = mix(1.0, uHoverScale, uHoverAmount) + uBurst * 0.3;\n p /= sc;\n vec3 c = vec3(0.0);\n float rcf = max(float(uRingCount) - 1.0, 1.0);\n for (int i = 0; i < 10; i++) {\n if (i >= uRingCount) break;\n float fi = float(i);\n vec2 pr = p - fi * uParallax * uMouse;\n vec3 rc = mix(uColor, uColorTwo, fi / rcf);\n c = mix(c, rc, vec3(ring(pr, uBaseRadius + fi * uRadiusStep, pow(uRingGap, fi), i == 0 ? 0.0 : 2.95 * fi, px)));\n }\n c *= 1.0 + uBurst * 2.0;\n float n = fract(sin(dot(gl_FragCoord.xy + uTime * 100.0, vec2(12.9898, 78.233))) * 43758.5453);\n c += (n - 0.5) * uNoiseAmount;\n gl_FragColor = vec4(c, max(c.r, max(c.g, c.b)) * uOpacity);\n}\n`;\n\ninterface MagicRingsProps {\n color?: string;\n colorTwo?: string;\n speed?: number;\n ringCount?: number;\n attenuation?: number;\n lineThickness?: number;\n baseRadius?: number;\n radiusStep?: number;\n scaleRate?: number;\n opacity?: number;\n blur?: number;\n noiseAmount?: number;\n rotation?: number;\n ringGap?: number;\n fadeIn?: number;\n fadeOut?: number;\n followMouse?: boolean;\n mouseInfluence?: number;\n hoverScale?: number;\n parallax?: number;\n clickBurst?: boolean;\n}\n\nexport default function MagicRings({\n color = '#fc42ff',\n colorTwo = '#42fcff',\n speed = 1,\n ringCount = 6,\n attenuation = 10,\n lineThickness = 2,\n baseRadius = 0.35,\n radiusStep = 0.1,\n scaleRate = 0.1,\n opacity = 1,\n blur = 0,\n noiseAmount = 0.1,\n rotation = 0,\n ringGap = 1.5,\n fadeIn = 0.7,\n fadeOut = 0.5,\n followMouse = false,\n mouseInfluence = 0.2,\n hoverScale = 1.2,\n parallax = 0.05,\n clickBurst = false,\n}: MagicRingsProps) {\n const mountRef = useRef(null);\n const propsRef = useRef | null>(null);\n const mouseRef = useRef([0, 0]);\n const smoothMouseRef = useRef([0, 0]);\n const hoverAmountRef = useRef(0);\n const isHoveredRef = useRef(false);\n const burstRef = useRef(0);\n\n propsRef.current = {\n color, colorTwo, speed, ringCount, attenuation, lineThickness,\n baseRadius, radiusStep, scaleRate, opacity, blur, noiseAmount,\n rotation, ringGap, fadeIn, fadeOut, followMouse, mouseInfluence,\n hoverScale, parallax, clickBurst,\n };\n\n useEffect(() => {\n const mount = mountRef.current;\n if (!mount) return;\n\n let renderer: THREE.WebGLRenderer;\n try {\n renderer = new THREE.WebGLRenderer({ alpha: true });\n } catch {\n return;\n }\n\n if (!renderer.capabilities.isWebGL2) {\n renderer.dispose();\n return;\n }\n\n renderer.setClearColor(0x000000, 0);\n mount.appendChild(renderer.domElement);\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-0.5, 0.5, 0.5, -0.5, 0.1, 10);\n camera.position.z = 1;\n\n const uniforms = {\n uTime: { value: 0 },\n uAttenuation: { value: 0 },\n uResolution: { value: new THREE.Vector2() },\n uColor: { value: new THREE.Color() },\n uColorTwo: { value: new THREE.Color() },\n uLineThickness: { value: 0 },\n uBaseRadius: { value: 0 },\n uRadiusStep: { value: 0 },\n uScaleRate: { value: 0 },\n uRingCount: { value: 0 },\n uOpacity: { value: 1 },\n uNoiseAmount: { value: 0 },\n uRotation: { value: 0 },\n uRingGap: { value: 1.6 },\n uFadeIn: { value: 0.5 },\n uFadeOut: { value: 0.75 },\n uMouse: { value: new THREE.Vector2() },\n uMouseInfluence: { value: 0 },\n uHoverAmount: { value: 0 },\n uHoverScale: { value: 1 },\n uParallax: { value: 0 },\n uBurst: { value: 0 },\n };\n\n const material = new THREE.ShaderMaterial({ vertexShader, fragmentShader, uniforms, transparent: true });\n const quad = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material);\n scene.add(quad);\n\n const resize = () => {\n const w = mount.clientWidth;\n const h = mount.clientHeight;\n const dpr = Math.min(window.devicePixelRatio, 2);\n renderer.setSize(w, h);\n renderer.setPixelRatio(dpr);\n uniforms.uResolution.value.set(w * dpr, h * dpr);\n };\n resize();\n window.addEventListener('resize', resize);\n\n const ro = new ResizeObserver(resize);\n ro.observe(mount);\n\n const onMouseMove = (e: MouseEvent) => {\n const rect = mount.getBoundingClientRect();\n mouseRef.current[0] = (e.clientX - rect.left) / rect.width - 0.5;\n mouseRef.current[1] = -((e.clientY - rect.top) / rect.height - 0.5);\n };\n const onMouseEnter = () => { isHoveredRef.current = true; };\n const onMouseLeave = () => {\n isHoveredRef.current = false;\n mouseRef.current[0] = 0;\n mouseRef.current[1] = 0;\n };\n const onClick = () => { burstRef.current = 1; };\n\n mount.addEventListener('mousemove', onMouseMove);\n mount.addEventListener('mouseenter', onMouseEnter);\n mount.addEventListener('mouseleave', onMouseLeave);\n mount.addEventListener('click', onClick);\n\n let frameId = 0;\n let isVisible = false;\n let isPageVisible = !document.hidden;\n let elapsed = 0;\n let lastT = 0;\n const animate = (t: number) => {\n frameId = requestAnimationFrame(animate);\n const p = propsRef.current!;\n\n const dt = lastT === 0 ? 0 : Math.min(t - lastT, 100);\n lastT = t;\n elapsed += dt * 0.001 * p.speed;\n\n smoothMouseRef.current[0] += (mouseRef.current[0] - smoothMouseRef.current[0]) * 0.08;\n smoothMouseRef.current[1] += (mouseRef.current[1] - smoothMouseRef.current[1]) * 0.08;\n hoverAmountRef.current += ((isHoveredRef.current ? 1 : 0) - hoverAmountRef.current) * 0.08;\n burstRef.current *= 0.95;\n if (burstRef.current < 0.001) burstRef.current = 0;\n\n uniforms.uTime.value = elapsed;\n uniforms.uAttenuation.value = p.attenuation;\n uniforms.uColor.value.set(p.color);\n uniforms.uColorTwo.value.set(p.colorTwo);\n uniforms.uLineThickness.value = p.lineThickness;\n uniforms.uBaseRadius.value = p.baseRadius;\n uniforms.uRadiusStep.value = p.radiusStep;\n uniforms.uScaleRate.value = p.scaleRate;\n uniforms.uRingCount.value = p.ringCount;\n uniforms.uOpacity.value = p.opacity;\n uniforms.uNoiseAmount.value = p.noiseAmount;\n uniforms.uRotation.value = (p.rotation * Math.PI) / 180;\n uniforms.uRingGap.value = p.ringGap;\n uniforms.uFadeIn.value = p.fadeIn;\n uniforms.uFadeOut.value = p.fadeOut;\n uniforms.uMouse.value.set(smoothMouseRef.current[0], smoothMouseRef.current[1]);\n uniforms.uMouseInfluence.value = p.followMouse ? p.mouseInfluence : 0;\n uniforms.uHoverAmount.value = hoverAmountRef.current;\n uniforms.uHoverScale.value = p.hoverScale;\n uniforms.uParallax.value = p.parallax;\n uniforms.uBurst.value = p.clickBurst ? burstRef.current : 0;\n\n renderer.render(scene, camera);\n };\n frameId = 0;\n\n const tryStart = () => {\n if (isVisible && isPageVisible && frameId === 0) {\n lastT = 0;\n frameId = requestAnimationFrame(animate);\n }\n };\n const tryStop = () => {\n if (frameId !== 0) {\n cancelAnimationFrame(frameId);\n frameId = 0;\n }\n };\n\n const io = new IntersectionObserver(\n ([entry]) => {\n isVisible = entry.isIntersecting;\n isVisible ? tryStart() : tryStop();\n },\n { threshold: 0 }\n );\n io.observe(mount);\n\n const onVisibility = () => {\n isPageVisible = !document.hidden;\n isPageVisible ? tryStart() : tryStop();\n };\n document.addEventListener('visibilitychange', onVisibility);\n\n tryStart();\n\n return () => {\n tryStop();\n io.disconnect();\n document.removeEventListener('visibilitychange', onVisibility);\n window.removeEventListener('resize', resize);\n ro.disconnect();\n mount.removeEventListener('mousemove', onMouseMove);\n mount.removeEventListener('mouseenter', onMouseEnter);\n mount.removeEventListener('mouseleave', onMouseLeave);\n mount.removeEventListener('click', onClick);\n mount.removeChild(renderer.domElement);\n renderer.dispose();\n material.dispose();\n };\n }, []);\n\n return
    0 ? { filter: `blur(${blur}px)` } : undefined} />;\n}\n" } ], diff --git a/public/r/Magnet-TS-CSS.json b/public/r/Magnet-TS-CSS.json index 6ba7c873..4f804e3c 100644 --- a/public/r/Magnet-TS-CSS.json +++ b/public/r/Magnet-TS-CSS.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "Magnet/Magnet.tsx", - "content": "import React, { useState, useEffect, useRef, ReactNode, HTMLAttributes } from 'react';\n\ninterface MagnetProps extends HTMLAttributes {\n children: ReactNode;\n padding?: number;\n disabled?: boolean;\n magnetStrength?: number;\n activeTransition?: string;\n inactiveTransition?: string;\n wrapperClassName?: string;\n innerClassName?: string;\n}\n\nconst Magnet: React.FC = ({\n children,\n padding = 100,\n disabled = false,\n magnetStrength = 2,\n activeTransition = 'transform 0.3s ease-out',\n inactiveTransition = 'transform 0.5s ease-in-out',\n wrapperClassName = '',\n innerClassName = '',\n ...props\n}) => {\n const [isActive, setIsActive] = useState(false);\n const [position, setPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });\n const magnetRef = useRef(null);\n\n useEffect(() => {\n if (disabled) {\n setPosition({ x: 0, y: 0 });\n return;\n }\n\n const handleMouseMove = (e: MouseEvent) => {\n if (!magnetRef.current) return;\n\n const { left, top, width, height } = magnetRef.current.getBoundingClientRect();\n const centerX = left + width / 2;\n const centerY = top + height / 2;\n\n const distX = Math.abs(centerX - e.clientX);\n const distY = Math.abs(centerY - e.clientY);\n\n if (distX < width / 2 + padding && distY < height / 2 + padding) {\n setIsActive(true);\n const offsetX = (e.clientX - centerX) / magnetStrength;\n const offsetY = (e.clientY - centerY) / magnetStrength;\n setPosition({ x: offsetX, y: offsetY });\n } else {\n setIsActive(false);\n setPosition({ x: 0, y: 0 });\n }\n };\n\n window.addEventListener('mousemove', handleMouseMove);\n return () => {\n window.removeEventListener('mousemove', handleMouseMove);\n };\n }, [padding, disabled, magnetStrength]);\n\n const transitionStyle = isActive ? activeTransition : inactiveTransition;\n\n return (\n \n \n {children}\n
    \n
    \n );\n};\n\nexport default Magnet;\n" + "content": "import React, { useState, useEffect, useRef, type ReactNode, type HTMLAttributes } from 'react';\n\ninterface MagnetProps extends HTMLAttributes {\n children: ReactNode;\n padding?: number;\n disabled?: boolean;\n magnetStrength?: number;\n activeTransition?: string;\n inactiveTransition?: string;\n wrapperClassName?: string;\n innerClassName?: string;\n}\n\nconst Magnet: React.FC = ({\n children,\n padding = 100,\n disabled = false,\n magnetStrength = 2,\n activeTransition = 'transform 0.3s ease-out',\n inactiveTransition = 'transform 0.5s ease-in-out',\n wrapperClassName = '',\n innerClassName = '',\n ...props\n}) => {\n const [isActive, setIsActive] = useState(false);\n const [position, setPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });\n const magnetRef = useRef(null);\n\n useEffect(() => {\n if (disabled) {\n setPosition({ x: 0, y: 0 });\n return;\n }\n\n const handleMouseMove = (e: MouseEvent) => {\n if (!magnetRef.current) return;\n\n const { left, top, width, height } = magnetRef.current.getBoundingClientRect();\n const centerX = left + width / 2;\n const centerY = top + height / 2;\n\n const distX = Math.abs(centerX - e.clientX);\n const distY = Math.abs(centerY - e.clientY);\n\n if (distX < width / 2 + padding && distY < height / 2 + padding) {\n setIsActive(true);\n const offsetX = (e.clientX - centerX) / magnetStrength;\n const offsetY = (e.clientY - centerY) / magnetStrength;\n setPosition({ x: offsetX, y: offsetY });\n } else {\n setIsActive(false);\n setPosition({ x: 0, y: 0 });\n }\n };\n\n window.addEventListener('mousemove', handleMouseMove);\n return () => {\n window.removeEventListener('mousemove', handleMouseMove);\n };\n }, [padding, disabled, magnetStrength]);\n\n const transitionStyle = isActive ? activeTransition : inactiveTransition;\n\n return (\n \n \n {children}\n
    \n
    \n );\n};\n\nexport default Magnet;\n" } ], "registryDependencies": [], diff --git a/public/r/Magnet-TS-TW.json b/public/r/Magnet-TS-TW.json index 9d4b8975..f76ce1fd 100644 --- a/public/r/Magnet-TS-TW.json +++ b/public/r/Magnet-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "Magnet/Magnet.tsx", - "content": "import React, { useState, useEffect, useRef, ReactNode, HTMLAttributes } from 'react';\n\ninterface MagnetProps extends HTMLAttributes {\n children: ReactNode;\n padding?: number;\n disabled?: boolean;\n magnetStrength?: number;\n activeTransition?: string;\n inactiveTransition?: string;\n wrapperClassName?: string;\n innerClassName?: string;\n}\n\nconst Magnet: React.FC = ({\n children,\n padding = 100,\n disabled = false,\n magnetStrength = 2,\n activeTransition = 'transform 0.3s ease-out',\n inactiveTransition = 'transform 0.5s ease-in-out',\n wrapperClassName = '',\n innerClassName = '',\n ...props\n}) => {\n const [isActive, setIsActive] = useState(false);\n const [position, setPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });\n const magnetRef = useRef(null);\n\n useEffect(() => {\n if (disabled) {\n setPosition({ x: 0, y: 0 });\n return;\n }\n\n const handleMouseMove = (e: MouseEvent) => {\n if (!magnetRef.current) return;\n\n const { left, top, width, height } = magnetRef.current.getBoundingClientRect();\n const centerX = left + width / 2;\n const centerY = top + height / 2;\n\n const distX = Math.abs(centerX - e.clientX);\n const distY = Math.abs(centerY - e.clientY);\n\n if (distX < width / 2 + padding && distY < height / 2 + padding) {\n setIsActive(true);\n const offsetX = (e.clientX - centerX) / magnetStrength;\n const offsetY = (e.clientY - centerY) / magnetStrength;\n setPosition({ x: offsetX, y: offsetY });\n } else {\n setIsActive(false);\n setPosition({ x: 0, y: 0 });\n }\n };\n\n window.addEventListener('mousemove', handleMouseMove);\n return () => {\n window.removeEventListener('mousemove', handleMouseMove);\n };\n }, [padding, disabled, magnetStrength]);\n\n const transitionStyle = isActive ? activeTransition : inactiveTransition;\n\n return (\n \n \n {children}\n
    \n
    \n );\n};\n\nexport default Magnet;\n" + "content": "import React, { useState, useEffect, useRef, type ReactNode, type HTMLAttributes } from 'react';\n\ninterface MagnetProps extends HTMLAttributes {\n children: ReactNode;\n padding?: number;\n disabled?: boolean;\n magnetStrength?: number;\n activeTransition?: string;\n inactiveTransition?: string;\n wrapperClassName?: string;\n innerClassName?: string;\n}\n\nconst Magnet: React.FC = ({\n children,\n padding = 100,\n disabled = false,\n magnetStrength = 2,\n activeTransition = 'transform 0.3s ease-out',\n inactiveTransition = 'transform 0.5s ease-in-out',\n wrapperClassName = '',\n innerClassName = '',\n ...props\n}) => {\n const [isActive, setIsActive] = useState(false);\n const [position, setPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });\n const magnetRef = useRef(null);\n\n useEffect(() => {\n if (disabled) {\n setPosition({ x: 0, y: 0 });\n return;\n }\n\n const handleMouseMove = (e: MouseEvent) => {\n if (!magnetRef.current) return;\n\n const { left, top, width, height } = magnetRef.current.getBoundingClientRect();\n const centerX = left + width / 2;\n const centerY = top + height / 2;\n\n const distX = Math.abs(centerX - e.clientX);\n const distY = Math.abs(centerY - e.clientY);\n\n if (distX < width / 2 + padding && distY < height / 2 + padding) {\n setIsActive(true);\n const offsetX = (e.clientX - centerX) / magnetStrength;\n const offsetY = (e.clientY - centerY) / magnetStrength;\n setPosition({ x: offsetX, y: offsetY });\n } else {\n setIsActive(false);\n setPosition({ x: 0, y: 0 });\n }\n };\n\n window.addEventListener('mousemove', handleMouseMove);\n return () => {\n window.removeEventListener('mousemove', handleMouseMove);\n };\n }, [padding, disabled, magnetStrength]);\n\n const transitionStyle = isActive ? activeTransition : inactiveTransition;\n\n return (\n \n \n {children}\n
    \n
    \n );\n};\n\nexport default Magnet;\n" } ], "registryDependencies": [], diff --git a/public/r/MagnetLines-JS-CSS.json b/public/r/MagnetLines-JS-CSS.json index 2e90086a..6f4b735f 100644 --- a/public/r/MagnetLines-JS-CSS.json +++ b/public/r/MagnetLines-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "MagnetLines/MagnetLines.css", + "type": "registry:file", + "path": "MagnetLines.css", + "target": "@components/MagnetLines.css", "content": ".magnetLines-container {\n display: grid;\n grid-template-columns: repeat(var(--columns), 1fr);\n grid-template-rows: repeat(var(--rows), 1fr);\n\n justify-items: center;\n align-items: center;\n\n width: 80vmin;\n height: 80vmin;\n}\n\n.magnetLines-container span {\n display: block;\n transform-origin: center;\n will-change: transform;\n transform: rotate(var(--rotate));\n}\n" }, { "type": "registry:component", - "path": "MagnetLines/MagnetLines.jsx", + "path": "MagnetLines.jsx", "content": "import { useRef, useEffect } from 'react';\nimport './MagnetLines.css';\n\nexport default function MagnetLines({\n rows = 9,\n columns = 9,\n containerSize = '80vmin',\n lineColor = '#efefef',\n lineWidth = '1vmin',\n lineHeight = '6vmin',\n baseAngle = -10,\n className = '',\n style = {}\n}) {\n const containerRef = useRef(null);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const items = container.querySelectorAll('span');\n\n const onPointerMove = pointer => {\n items.forEach(item => {\n const rect = item.getBoundingClientRect();\n const centerX = rect.x + rect.width / 2;\n const centerY = rect.y + rect.height / 2;\n\n const b = pointer.x - centerX;\n const a = pointer.y - centerY;\n const c = Math.sqrt(a * a + b * b) || 1;\n const r = ((Math.acos(b / c) * 180) / Math.PI) * (pointer.y > centerY ? 1 : -1);\n\n item.style.setProperty('--rotate', `${r}deg`);\n });\n };\n\n window.addEventListener('pointermove', onPointerMove);\n\n if (items.length) {\n const middleIndex = Math.floor(items.length / 2);\n const rect = items[middleIndex].getBoundingClientRect();\n onPointerMove({ x: rect.x, y: rect.y });\n }\n\n return () => {\n window.removeEventListener('pointermove', onPointerMove);\n };\n }, [rows, columns]);\n\n const total = rows * columns;\n const spans = Array.from({ length: total }, (_, i) => (\n \n ));\n\n return (\n \n {spans}\n
    \n );\n}\n" } ], diff --git a/public/r/MagnetLines-TS-CSS.json b/public/r/MagnetLines-TS-CSS.json index 5898afca..e5a06e81 100644 --- a/public/r/MagnetLines-TS-CSS.json +++ b/public/r/MagnetLines-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "MagnetLines/MagnetLines.css", + "type": "registry:file", + "path": "MagnetLines.css", + "target": "@components/MagnetLines.css", "content": ".magnetLines-container {\n display: grid;\n grid-template-columns: repeat(var(--columns), 1fr);\n grid-template-rows: repeat(var(--rows), 1fr);\n\n justify-items: center;\n align-items: center;\n\n width: 80vmin;\n height: 80vmin;\n}\n\n.magnetLines-container span {\n display: block;\n transform-origin: center;\n will-change: transform;\n transform: rotate(var(--rotate));\n}\n" }, { "type": "registry:component", - "path": "MagnetLines/MagnetLines.tsx", - "content": "import React, { useRef, useEffect, CSSProperties } from 'react';\nimport './MagnetLines.css';\n\ninterface MagnetLinesProps {\n rows?: number;\n columns?: number;\n containerSize?: string;\n lineColor?: string;\n lineWidth?: string;\n lineHeight?: string;\n baseAngle?: number;\n className?: string;\n style?: CSSProperties;\n}\n\nconst MagnetLines: React.FC = ({\n rows = 9,\n columns = 9,\n containerSize = '80vmin',\n lineColor = '#efefef',\n lineWidth = '1vmin',\n lineHeight = '6vmin',\n baseAngle = -10,\n className = '',\n style = {}\n}) => {\n const containerRef = useRef(null);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const items = container.querySelectorAll('span');\n\n const onPointerMove = (pointer: { x: number; y: number }) => {\n items.forEach(item => {\n const rect = item.getBoundingClientRect();\n const centerX = rect.x + rect.width / 2;\n const centerY = rect.y + rect.height / 2;\n\n const b = pointer.x - centerX;\n const a = pointer.y - centerY;\n const c = Math.sqrt(a * a + b * b) || 1;\n const r = ((Math.acos(b / c) * 180) / Math.PI) * (pointer.y > centerY ? 1 : -1);\n\n item.style.setProperty('--rotate', `${r}deg`);\n });\n };\n\n const handlePointerMove = (e: PointerEvent) => {\n onPointerMove({ x: e.x, y: e.y });\n };\n\n window.addEventListener('pointermove', handlePointerMove);\n\n if (items.length) {\n const middleIndex = Math.floor(items.length / 2);\n const rect = items[middleIndex].getBoundingClientRect();\n onPointerMove({ x: rect.x, y: rect.y });\n }\n\n return () => {\n window.removeEventListener('pointermove', handlePointerMove);\n };\n }, [rows, columns]);\n\n const total = rows * columns;\n const spans = Array.from({ length: total }, (_, i) => (\n \n ));\n\n return (\n \n {spans}\n
    \n );\n};\n\nexport default MagnetLines;\n" + "path": "MagnetLines.tsx", + "content": "import React, { useRef, useEffect, type CSSProperties } from 'react';\nimport './MagnetLines.css';\n\ninterface MagnetLinesProps {\n rows?: number;\n columns?: number;\n containerSize?: string;\n lineColor?: string;\n lineWidth?: string;\n lineHeight?: string;\n baseAngle?: number;\n className?: string;\n style?: CSSProperties;\n}\n\nconst MagnetLines: React.FC = ({\n rows = 9,\n columns = 9,\n containerSize = '80vmin',\n lineColor = '#efefef',\n lineWidth = '1vmin',\n lineHeight = '6vmin',\n baseAngle = -10,\n className = '',\n style = {}\n}) => {\n const containerRef = useRef(null);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const items = container.querySelectorAll('span');\n\n const onPointerMove = (pointer: { x: number; y: number }) => {\n items.forEach(item => {\n const rect = item.getBoundingClientRect();\n const centerX = rect.x + rect.width / 2;\n const centerY = rect.y + rect.height / 2;\n\n const b = pointer.x - centerX;\n const a = pointer.y - centerY;\n const c = Math.sqrt(a * a + b * b) || 1;\n const r = ((Math.acos(b / c) * 180) / Math.PI) * (pointer.y > centerY ? 1 : -1);\n\n item.style.setProperty('--rotate', `${r}deg`);\n });\n };\n\n const handlePointerMove = (e: PointerEvent) => {\n onPointerMove({ x: e.x, y: e.y });\n };\n\n window.addEventListener('pointermove', handlePointerMove);\n\n if (items.length) {\n const middleIndex = Math.floor(items.length / 2);\n const rect = items[middleIndex].getBoundingClientRect();\n onPointerMove({ x: rect.x, y: rect.y });\n }\n\n return () => {\n window.removeEventListener('pointermove', handlePointerMove);\n };\n }, [rows, columns]);\n\n const total = rows * columns;\n const spans = Array.from({ length: total }, (_, i) => (\n \n ));\n\n return (\n \n {spans}\n
    \n );\n};\n\nexport default MagnetLines;\n" } ], "registryDependencies": [], diff --git a/public/r/MagnetLines-TS-TW.json b/public/r/MagnetLines-TS-TW.json index e144bc95..2e32f22b 100644 --- a/public/r/MagnetLines-TS-TW.json +++ b/public/r/MagnetLines-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "MagnetLines/MagnetLines.tsx", - "content": "import React, { useRef, useEffect, CSSProperties } from 'react';\n\ninterface MagnetLinesProps {\n rows?: number;\n columns?: number;\n containerSize?: string;\n lineColor?: string;\n lineWidth?: string;\n lineHeight?: string;\n baseAngle?: number;\n className?: string;\n style?: CSSProperties;\n}\n\nconst MagnetLines: React.FC = ({\n rows = 9,\n columns = 9,\n containerSize = '80vmin',\n lineColor = '#efefef',\n lineWidth = '1vmin',\n lineHeight = '6vmin',\n baseAngle = -10,\n className = '',\n style = {}\n}) => {\n const containerRef = useRef(null);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const items = container.querySelectorAll('span');\n\n const onPointerMove = (pointer: { x: number; y: number }) => {\n items.forEach(item => {\n const rect = item.getBoundingClientRect();\n const centerX = rect.x + rect.width / 2;\n const centerY = rect.y + rect.height / 2;\n\n const b = pointer.x - centerX;\n const a = pointer.y - centerY;\n const c = Math.sqrt(a * a + b * b) || 1;\n const r = ((Math.acos(b / c) * 180) / Math.PI) * (pointer.y > centerY ? 1 : -1);\n\n item.style.setProperty('--rotate', `${r}deg`);\n });\n };\n\n const handlePointerMove = (e: PointerEvent) => {\n onPointerMove({ x: e.x, y: e.y });\n };\n\n window.addEventListener('pointermove', handlePointerMove);\n\n if (items.length) {\n const middleIndex = Math.floor(items.length / 2);\n const rect = items[middleIndex].getBoundingClientRect();\n onPointerMove({ x: rect.x, y: rect.y });\n }\n\n return () => {\n window.removeEventListener('pointermove', handlePointerMove);\n };\n }, [rows, columns]);\n\n const total = rows * columns;\n const spans = Array.from({ length: total }, (_, i) => (\n \n ));\n\n return (\n \n {spans}\n
    \n );\n};\n\nexport default MagnetLines;\n" + "content": "import React, { useRef, useEffect, type CSSProperties } from 'react';\n\ninterface MagnetLinesProps {\n rows?: number;\n columns?: number;\n containerSize?: string;\n lineColor?: string;\n lineWidth?: string;\n lineHeight?: string;\n baseAngle?: number;\n className?: string;\n style?: CSSProperties;\n}\n\nconst MagnetLines: React.FC = ({\n rows = 9,\n columns = 9,\n containerSize = '80vmin',\n lineColor = '#efefef',\n lineWidth = '1vmin',\n lineHeight = '6vmin',\n baseAngle = -10,\n className = '',\n style = {}\n}) => {\n const containerRef = useRef(null);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const items = container.querySelectorAll('span');\n\n const onPointerMove = (pointer: { x: number; y: number }) => {\n items.forEach(item => {\n const rect = item.getBoundingClientRect();\n const centerX = rect.x + rect.width / 2;\n const centerY = rect.y + rect.height / 2;\n\n const b = pointer.x - centerX;\n const a = pointer.y - centerY;\n const c = Math.sqrt(a * a + b * b) || 1;\n const r = ((Math.acos(b / c) * 180) / Math.PI) * (pointer.y > centerY ? 1 : -1);\n\n item.style.setProperty('--rotate', `${r}deg`);\n });\n };\n\n const handlePointerMove = (e: PointerEvent) => {\n onPointerMove({ x: e.x, y: e.y });\n };\n\n window.addEventListener('pointermove', handlePointerMove);\n\n if (items.length) {\n const middleIndex = Math.floor(items.length / 2);\n const rect = items[middleIndex].getBoundingClientRect();\n onPointerMove({ x: rect.x, y: rect.y });\n }\n\n return () => {\n window.removeEventListener('pointermove', handlePointerMove);\n };\n }, [rows, columns]);\n\n const total = rows * columns;\n const spans = Array.from({ length: total }, (_, i) => (\n \n ));\n\n return (\n \n {spans}\n
    \n );\n};\n\nexport default MagnetLines;\n" } ], "registryDependencies": [], diff --git a/public/r/MaskedHeading-JS-CSS.json b/public/r/MaskedHeading-JS-CSS.json index 78e98baa..c79d53af 100644 --- a/public/r/MaskedHeading-JS-CSS.json +++ b/public/r/MaskedHeading-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "MaskedHeading/MaskedHeading.css", + "type": "registry:file", + "path": "MaskedHeading.css", + "target": "@components/MaskedHeading.css", "content": ".masked-heading {\n position: relative;\n width: 100%;\n margin: 0;\n padding: 0;\n text-wrap: balance;\n -webkit-font-smoothing: antialiased;\n}\n\n.masked-heading__measure {\n color: transparent;\n}\n\n.masked-heading__word {\n display: inline-block;\n white-space: pre;\n}\n\n.masked-heading__word:not(:last-child)::after {\n content: ' ';\n}\n\n.masked-heading__baseline {\n display: inline-block;\n width: 0;\n height: 0;\n}\n\n.masked-heading__defs {\n position: absolute;\n width: 0;\n height: 0;\n overflow: hidden;\n}\n\n.masked-heading__reveal {\n position: absolute;\n inset: 0;\n display: block;\n pointer-events: none;\n}\n\n.masked-heading__clip {\n position: absolute;\n inset: 0;\n display: block;\n}\n\n.masked-heading__media {\n position: absolute;\n inset: 0;\n display: block;\n will-change: transform, filter;\n}\n\n.masked-heading__source {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n user-select: none;\n -webkit-user-drag: none;\n}\n" }, { "type": "registry:component", - "path": "MaskedHeading/MaskedHeading.jsx", + "path": "MaskedHeading.jsx", "content": "import { useCallback, useEffect, useId, useMemo, useRef } from 'react';\nimport { gsap } from 'gsap';\n\nimport './MaskedHeading.css';\n\nconst clamp = (v, a, b) => (v < a ? a : v > b ? b : v);\n\nconst MaskedHeading = ({\n text = 'Designed in the details',\n tag = 'h2',\n mediaType = 'image',\n src = '',\n poster = '',\n fillScale = 1.25,\n parallax = 26,\n drift = 18,\n brightness = 1,\n saturation = 1,\n grayscale = false,\n reveal = 'rise',\n duration = 1.1,\n stagger = 0.09,\n trigger = 'view',\n align = 'center',\n weight = 700,\n tracking = -0.03,\n lineHeight = 1.06,\n textScale = 0.115,\n className = '',\n style,\n ...rest\n}) => {\n const rootRef = useRef(null);\n const measureRef = useRef(null);\n const revealRef = useRef(null);\n const mediaRef = useRef(null);\n const wordRefs = useRef([]);\n const baseRefs = useRef([]);\n const glyphRefs = useRef([]);\n const tweenRef = useRef(null);\n const offsetRef = useRef({ x: 0, y: 0, tx: 0, ty: 0 });\n\n const clipId = `mh-${useId().replace(/[^a-zA-Z0-9_-]/g, '')}`;\n const words = useMemo(() => String(text).split(/\\s+/).filter(Boolean), [text]);\n\n const settingsRef = useRef({});\n settingsRef.current = { fillScale, parallax, drift, brightness, saturation, grayscale, textScale };\n\n const place = useCallback(() => {\n const root = rootRef.current;\n const media = mediaRef.current;\n if (!root || !media) return;\n const s = settingsRef.current;\n const W = root.clientWidth;\n const H = root.clientHeight;\n const off = offsetRef.current;\n\n const maxX = Math.max(0, ((s.fillScale - 1) / 2) * W);\n const maxY = Math.max(0, ((s.fillScale - 1) / 2) * H);\n\n media.style.transform = `translate3d(${clamp(off.x, -maxX, maxX).toFixed(2)}px, ${clamp(off.y, -maxY, maxY).toFixed(2)}px, 0) scale(${s.fillScale})`;\n media.style.filter = `brightness(${s.brightness}) saturate(${s.saturation})${s.grayscale ? ' grayscale(1)' : ''}`;\n }, []);\n\n const sync = useCallback(() => {\n const root = rootRef.current;\n const measure = measureRef.current;\n if (!root || !measure) return;\n const s = settingsRef.current;\n\n root.style.fontSize = `${clamp(root.clientWidth * s.textScale, 20, 200).toFixed(1)}px`;\n\n const cs = window.getComputedStyle(measure);\n for (let i = 0; i < wordRefs.current.length; i += 1) {\n const box = wordRefs.current[i];\n const base = baseRefs.current[i];\n const glyph = glyphRefs.current[i];\n if (!box || !base || !glyph) continue;\n glyph.setAttribute('x', `${box.offsetLeft}`);\n glyph.setAttribute('y', `${base.offsetTop}`);\n glyph.style.fontFamily = cs.fontFamily;\n glyph.style.fontSize = cs.fontSize;\n glyph.style.fontWeight = cs.fontWeight;\n glyph.style.fontStyle = cs.fontStyle;\n glyph.style.letterSpacing = cs.letterSpacing;\n }\n place();\n }, [place]);\n\n useEffect(() => {\n const root = rootRef.current;\n if (!root) return;\n\n sync();\n const ro = new ResizeObserver(sync);\n ro.observe(root);\n if (document.fonts?.ready) document.fonts.ready.then(sync).catch(() => {});\n\n let raf = 0;\n let last = performance.now();\n let clock = 0;\n\n const frame = now => {\n const dt = Math.min(0.05, (now - last) / 1000);\n last = now;\n clock += dt;\n const s = settingsRef.current;\n const off = offsetRef.current;\n\n const dx = Math.sin(clock * 0.21) * s.drift;\n const dy = Math.cos(clock * 0.17) * s.drift * 0.6;\n\n const ease = 1 - Math.exp(-dt / 0.18);\n off.x += (off.tx + dx - off.x) * ease;\n off.y += (off.ty + dy - off.y) * ease;\n\n place();\n raf = requestAnimationFrame(frame);\n };\n\n const onMove = e => {\n const s = settingsRef.current;\n if (s.parallax <= 0) return;\n const r = root.getBoundingClientRect();\n const nx = ((e.clientX - r.left) / (r.width || 1)) * 2 - 1;\n const ny = ((e.clientY - r.top) / (r.height || 1)) * 2 - 1;\n offsetRef.current.tx = clamp(nx, -1, 1) * -s.parallax;\n offsetRef.current.ty = clamp(ny, -1, 1) * -s.parallax;\n };\n\n const onLeave = () => {\n offsetRef.current.tx = 0;\n offsetRef.current.ty = 0;\n };\n\n root.addEventListener('pointermove', onMove);\n root.addEventListener('pointerleave', onLeave);\n raf = requestAnimationFrame(frame);\n\n return () => {\n cancelAnimationFrame(raf);\n ro.disconnect();\n root.removeEventListener('pointermove', onMove);\n root.removeEventListener('pointerleave', onLeave);\n };\n }, [place, sync]);\n\n useEffect(() => {\n sync();\n }, [sync, words, tag, align, weight, tracking, lineHeight, textScale]);\n\n useEffect(() => {\n const root = rootRef.current;\n const layer = revealRef.current;\n if (!root || !layer) return;\n const glyphs = glyphRefs.current.filter(Boolean);\n if (!glyphs.length) return;\n\n const riseDistance = () => (parseFloat(window.getComputedStyle(root).fontSize) || 48) * 1.15;\n\n const settle = () => {\n gsap.set(glyphs, { y: 0 });\n gsap.set(layer, { opacity: 1, scale: 1, clipPath: 'inset(0% 0% 0% 0%)' });\n };\n\n const rest = () => {\n if (reveal === 'rise') {\n gsap.set(glyphs, { y: riseDistance() });\n } else if (reveal === 'wipe') {\n gsap.set(layer, { clipPath: 'inset(0% 100% 0% 0%)' });\n } else if (reveal === 'fade') {\n gsap.set(layer, { opacity: 0, scale: 1.08 });\n }\n };\n\n const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n if (reveal === 'none' || reduce) {\n settle();\n return;\n }\n\n const play = () => {\n tweenRef.current?.kill();\n if (reveal === 'rise') {\n gsap.set(layer, { opacity: 1, scale: 1, clipPath: 'inset(0% 0% 0% 0%)' });\n tweenRef.current = gsap.fromTo(\n glyphs,\n { y: riseDistance() },\n { y: 0, duration, stagger, ease: 'power4.out', overwrite: 'auto' }\n );\n } else if (reveal === 'wipe') {\n gsap.set(glyphs, { y: 0 });\n const state = { p: 100 };\n tweenRef.current = gsap.to(state, {\n p: 0,\n duration,\n ease: 'power3.inOut',\n overwrite: 'auto',\n onUpdate: () => {\n layer.style.clipPath = `inset(0% ${state.p}% 0% 0%)`;\n }\n });\n } else {\n gsap.set(glyphs, { y: 0 });\n tweenRef.current = gsap.fromTo(\n layer,\n { opacity: 0, scale: 1.08 },\n { opacity: 1, scale: 1, duration, ease: 'power3.out', overwrite: 'auto' }\n );\n }\n };\n\n if (trigger === 'hover') {\n settle();\n root.addEventListener('pointerenter', play);\n return () => {\n root.removeEventListener('pointerenter', play);\n tweenRef.current?.kill();\n };\n }\n\n if (trigger === 'view') {\n settle();\n rest();\n const io = new IntersectionObserver(\n entries => {\n if (entries.some(e => e.isIntersecting)) {\n play();\n io.disconnect();\n }\n },\n { threshold: 0.25 }\n );\n io.observe(root);\n return () => {\n io.disconnect();\n tweenRef.current?.kill();\n };\n }\n\n play();\n return () => tweenRef.current?.kill();\n }, [reveal, trigger, duration, stagger, words]);\n\n const Tag = tag;\n\n return (\n \n \n {words.map((word, i) => (\n {\n wordRefs.current[i] = el;\n }}\n className=\"masked-heading__word\"\n >\n {word}\n {\n baseRefs.current[i] = el;\n }}\n className=\"masked-heading__baseline\"\n />\n \n ))}\n \n\n \n \n \n {words.map((word, i) => (\n {\n glyphRefs.current[i] = el;\n }}\n >\n {word}\n \n ))}\n \n \n \n\n \n \n \n {mediaType === 'video' ? (\n \n \n \n \n );\n};\n\nexport default MaskedHeading;\n" } ], diff --git a/public/r/MaskedHeading-TS-CSS.json b/public/r/MaskedHeading-TS-CSS.json index ef29d6e5..84bafaea 100644 --- a/public/r/MaskedHeading-TS-CSS.json +++ b/public/r/MaskedHeading-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "MaskedHeading/MaskedHeading.css", + "type": "registry:file", + "path": "MaskedHeading.css", + "target": "@components/MaskedHeading.css", "content": ".masked-heading {\n position: relative;\n width: 100%;\n margin: 0;\n padding: 0;\n text-wrap: balance;\n -webkit-font-smoothing: antialiased;\n}\n\n.masked-heading__measure {\n color: transparent;\n}\n\n.masked-heading__word {\n display: inline-block;\n white-space: pre;\n}\n\n.masked-heading__word:not(:last-child)::after {\n content: ' ';\n}\n\n.masked-heading__baseline {\n display: inline-block;\n width: 0;\n height: 0;\n}\n\n.masked-heading__defs {\n position: absolute;\n width: 0;\n height: 0;\n overflow: hidden;\n}\n\n.masked-heading__reveal {\n position: absolute;\n inset: 0;\n display: block;\n pointer-events: none;\n}\n\n.masked-heading__clip {\n position: absolute;\n inset: 0;\n display: block;\n}\n\n.masked-heading__media {\n position: absolute;\n inset: 0;\n display: block;\n will-change: transform, filter;\n}\n\n.masked-heading__source {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n user-select: none;\n -webkit-user-drag: none;\n}\n" }, { "type": "registry:component", - "path": "MaskedHeading/MaskedHeading.tsx", + "path": "MaskedHeading.tsx", "content": "import { useCallback, useEffect, useId, useMemo, useRef } from 'react';\nimport type { CSSProperties, ElementType } from 'react';\nimport { gsap } from 'gsap';\n\nimport './MaskedHeading.css';\n\nconst clamp = (v: number, a: number, b: number): number => (v < a ? a : v > b ? b : v);\n\ntype Reveal = 'rise' | 'wipe' | 'fade' | 'none';\ntype Trigger = 'view' | 'mount' | 'hover';\n\nexport interface MaskedHeadingProps {\n text?: string;\n tag?: ElementType;\n mediaType?: 'image' | 'video';\n src?: string;\n poster?: string;\n fillScale?: number;\n parallax?: number;\n drift?: number;\n brightness?: number;\n saturation?: number;\n grayscale?: boolean;\n reveal?: Reveal;\n duration?: number;\n stagger?: number;\n trigger?: Trigger;\n align?: 'left' | 'center' | 'right';\n weight?: number;\n tracking?: number;\n lineHeight?: number;\n textScale?: number;\n className?: string;\n style?: CSSProperties;\n [key: string]: unknown;\n}\n\nconst MaskedHeading: React.FC = ({\n text = 'Designed in the details',\n tag = 'h2',\n mediaType = 'image',\n src = '',\n poster = '',\n fillScale = 1.25,\n parallax = 26,\n drift = 18,\n brightness = 1,\n saturation = 1,\n grayscale = false,\n reveal = 'rise',\n duration = 1.1,\n stagger = 0.09,\n trigger = 'view',\n align = 'center',\n weight = 700,\n tracking = -0.03,\n lineHeight = 1.06,\n textScale = 0.115,\n className = '',\n style,\n ...rest\n}: MaskedHeadingProps) => {\n const rootRef = useRef(null);\n const measureRef = useRef(null);\n const revealRef = useRef(null);\n const mediaRef = useRef(null);\n const wordRefs = useRef<(HTMLSpanElement | null)[]>([]);\n const baseRefs = useRef<(HTMLElement | null)[]>([]);\n const glyphRefs = useRef<(SVGTextElement | null)[]>([]);\n const tweenRef = useRef(null);\n const offsetRef = useRef<{ x: number; y: number; tx: number; ty: number }>({ x: 0, y: 0, tx: 0, ty: 0 });\n\n const clipId = `mh-${useId().replace(/[^a-zA-Z0-9_-]/g, '')}`;\n const words = useMemo(() => String(text).split(/\\s+/).filter(Boolean), [text]);\n\n const settingsRef = useRef<{\n fillScale: number;\n parallax: number;\n drift: number;\n brightness: number;\n saturation: number;\n grayscale: boolean;\n textScale: number;\n }>({ fillScale: 1, parallax: 0, drift: 0, brightness: 1, saturation: 1, grayscale: false, textScale: 0.115 });\n settingsRef.current = { fillScale, parallax, drift, brightness, saturation, grayscale, textScale };\n\n const place = useCallback(() => {\n const root = rootRef.current;\n const media = mediaRef.current;\n if (!root || !media) return;\n const s = settingsRef.current;\n const W = root.clientWidth;\n const H = root.clientHeight;\n const off = offsetRef.current;\n\n const maxX = Math.max(0, ((s.fillScale - 1) / 2) * W);\n const maxY = Math.max(0, ((s.fillScale - 1) / 2) * H);\n\n media.style.transform = `translate3d(${clamp(off.x, -maxX, maxX).toFixed(2)}px, ${clamp(off.y, -maxY, maxY).toFixed(2)}px, 0) scale(${s.fillScale})`;\n media.style.filter = `brightness(${s.brightness}) saturate(${s.saturation})${s.grayscale ? ' grayscale(1)' : ''}`;\n }, []);\n\n const sync = useCallback(() => {\n const root = rootRef.current;\n const measure = measureRef.current;\n if (!root || !measure) return;\n const s = settingsRef.current;\n\n root.style.fontSize = `${clamp(root.clientWidth * s.textScale, 20, 200).toFixed(1)}px`;\n\n const cs = window.getComputedStyle(measure);\n for (let i = 0; i < wordRefs.current.length; i += 1) {\n const box = wordRefs.current[i];\n const base = baseRefs.current[i];\n const glyph = glyphRefs.current[i];\n if (!box || !base || !glyph) continue;\n glyph.setAttribute('x', `${box.offsetLeft}`);\n glyph.setAttribute('y', `${base.offsetTop}`);\n glyph.style.fontFamily = cs.fontFamily;\n glyph.style.fontSize = cs.fontSize;\n glyph.style.fontWeight = cs.fontWeight;\n glyph.style.fontStyle = cs.fontStyle;\n glyph.style.letterSpacing = cs.letterSpacing;\n }\n place();\n }, [place]);\n\n useEffect(() => {\n const root = rootRef.current;\n if (!root) return;\n\n sync();\n const ro = new ResizeObserver(sync);\n ro.observe(root);\n if (document.fonts?.ready) document.fonts.ready.then(sync).catch(() => {});\n\n let raf = 0;\n let last = performance.now();\n let clock = 0;\n\n const frame = (now: number) => {\n const dt = Math.min(0.05, (now - last) / 1000);\n last = now;\n clock += dt;\n const s = settingsRef.current;\n const off = offsetRef.current;\n\n const dx = Math.sin(clock * 0.21) * s.drift;\n const dy = Math.cos(clock * 0.17) * s.drift * 0.6;\n\n const ease = 1 - Math.exp(-dt / 0.18);\n off.x += (off.tx + dx - off.x) * ease;\n off.y += (off.ty + dy - off.y) * ease;\n\n place();\n raf = requestAnimationFrame(frame);\n };\n\n const onMove = (e: PointerEvent) => {\n const s = settingsRef.current;\n if (s.parallax <= 0) return;\n const r = root.getBoundingClientRect();\n const nx = ((e.clientX - r.left) / (r.width || 1)) * 2 - 1;\n const ny = ((e.clientY - r.top) / (r.height || 1)) * 2 - 1;\n offsetRef.current.tx = clamp(nx, -1, 1) * -s.parallax;\n offsetRef.current.ty = clamp(ny, -1, 1) * -s.parallax;\n };\n\n const onLeave = () => {\n offsetRef.current.tx = 0;\n offsetRef.current.ty = 0;\n };\n\n root.addEventListener('pointermove', onMove);\n root.addEventListener('pointerleave', onLeave);\n raf = requestAnimationFrame(frame);\n\n return () => {\n cancelAnimationFrame(raf);\n ro.disconnect();\n root.removeEventListener('pointermove', onMove);\n root.removeEventListener('pointerleave', onLeave);\n };\n }, [place, sync]);\n\n useEffect(() => {\n sync();\n }, [sync, words, tag, align, weight, tracking, lineHeight, textScale]);\n\n useEffect(() => {\n const root = rootRef.current;\n const layer = revealRef.current;\n if (!root || !layer) return;\n const glyphs = glyphRefs.current.filter(Boolean);\n if (!glyphs.length) return;\n\n const riseDistance = () => (parseFloat(window.getComputedStyle(root).fontSize) || 48) * 1.15;\n\n const settle = () => {\n gsap.set(glyphs, { y: 0 });\n gsap.set(layer, { opacity: 1, scale: 1, clipPath: 'inset(0% 0% 0% 0%)' });\n };\n\n const rest = () => {\n if (reveal === 'rise') {\n gsap.set(glyphs, { y: riseDistance() });\n } else if (reveal === 'wipe') {\n gsap.set(layer, { clipPath: 'inset(0% 100% 0% 0%)' });\n } else if (reveal === 'fade') {\n gsap.set(layer, { opacity: 0, scale: 1.08 });\n }\n };\n\n const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n if (reveal === 'none' || reduce) {\n settle();\n return;\n }\n\n const play = () => {\n tweenRef.current?.kill();\n if (reveal === 'rise') {\n gsap.set(layer, { opacity: 1, scale: 1, clipPath: 'inset(0% 0% 0% 0%)' });\n tweenRef.current = gsap.fromTo(\n glyphs,\n { y: riseDistance() },\n { y: 0, duration, stagger, ease: 'power4.out', overwrite: 'auto' }\n );\n } else if (reveal === 'wipe') {\n gsap.set(glyphs, { y: 0 });\n const state = { p: 100 };\n tweenRef.current = gsap.to(state, {\n p: 0,\n duration,\n ease: 'power3.inOut',\n overwrite: 'auto',\n onUpdate: () => {\n layer.style.clipPath = `inset(0% ${state.p}% 0% 0%)`;\n }\n });\n } else {\n gsap.set(glyphs, { y: 0 });\n tweenRef.current = gsap.fromTo(\n layer,\n { opacity: 0, scale: 1.08 },\n { opacity: 1, scale: 1, duration, ease: 'power3.out', overwrite: 'auto' }\n );\n }\n };\n\n if (trigger === 'hover') {\n settle();\n root.addEventListener('pointerenter', play);\n return () => {\n root.removeEventListener('pointerenter', play);\n tweenRef.current?.kill();\n };\n }\n\n if (trigger === 'view') {\n settle();\n rest();\n const io = new IntersectionObserver(\n entries => {\n if (entries.some(e => e.isIntersecting)) {\n play();\n io.disconnect();\n }\n },\n { threshold: 0.25 }\n );\n io.observe(root);\n return () => {\n io.disconnect();\n tweenRef.current?.kill();\n };\n }\n\n play();\n return () => tweenRef.current?.kill();\n }, [reveal, trigger, duration, stagger, words]);\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const TagAny = tag as any;\n\n return (\n \n \n {words.map((word, i) => (\n {\n wordRefs.current[i] = el;\n }}\n className=\"masked-heading__word\"\n >\n {word}\n {\n baseRefs.current[i] = el;\n }}\n className=\"masked-heading__baseline\"\n />\n \n ))}\n \n\n \n \n \n {words.map((word, i) => (\n {\n glyphRefs.current[i] = el;\n }}\n >\n {word}\n \n ))}\n \n \n \n\n \n \n \n {mediaType === 'video' ? (\n \n \n \n \n );\n};\n\nexport default MaskedHeading;\n" } ], diff --git a/public/r/Masonry-JS-CSS.json b/public/r/Masonry-JS-CSS.json index ef6559f5..63b2cf4c 100644 --- a/public/r/Masonry-JS-CSS.json +++ b/public/r/Masonry-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Masonry/Masonry.css", + "type": "registry:file", + "path": "Masonry.css", + "target": "@components/Masonry.css", "content": ".list {\n position: relative;\n width: 100%;\n height: 100%;\n}\n\n.item-wrapper {\n position: absolute;\n will-change: transform, width, height, opacity;\n padding: 6px;\n cursor: pointer;\n top: 0;\n left: 0;\n}\n\n.item-wrapper > .item-img {\n position: relative;\n background-size: cover;\n background-position: center center;\n width: 100%;\n height: 100%;\n text-transform: uppercase;\n font-size: 10px;\n line-height: 10px;\n border-radius: 10px;\n box-shadow: 0px 10px 50px -10px rgba(0, 0, 0, 0.2);\n}\n" }, { "type": "registry:component", - "path": "Masonry/Masonry.jsx", + "path": "Masonry.jsx", "content": "import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport { gsap } from 'gsap';\n\nimport './Masonry.css';\n\nconst useMedia = (queries, values, defaultValue) => {\n const get = () => {\n if (typeof window === 'undefined') return defaultValue;\n return values[queries.findIndex(q => matchMedia(q).matches)] ?? defaultValue;\n };\n\n const [value, setValue] = useState(get);\n\n useEffect(() => {\n const handler = () => setValue(get);\n queries.forEach(q => matchMedia(q).addEventListener('change', handler));\n return () => queries.forEach(q => matchMedia(q).removeEventListener('change', handler));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [queries]);\n\n return value;\n};\n\nconst useMeasure = () => {\n const ref = useRef(null);\n const [size, setSize] = useState({ width: 0, height: 0 });\n\n useLayoutEffect(() => {\n if (!ref.current) return;\n const ro = new ResizeObserver(([entry]) => {\n const { width, height } = entry.contentRect;\n setSize({ width, height });\n });\n ro.observe(ref.current);\n return () => ro.disconnect();\n }, []);\n\n return [ref, size];\n};\n\nconst preloadImages = async urls => {\n await Promise.all(\n urls.map(\n src =>\n new Promise(resolve => {\n const img = new Image();\n img.src = src;\n img.onload = img.onerror = () => resolve();\n })\n )\n );\n};\n\nconst Masonry = ({\n items,\n ease = 'power3.out',\n duration = 0.6,\n stagger = 0.05,\n animateFrom = 'bottom',\n scaleOnHover = true,\n hoverScale = 0.95,\n blurToFocus = true,\n colorShiftOnHover = false\n}) => {\n const columns = useMedia(\n ['(min-width:1500px)', '(min-width:1000px)', '(min-width:600px)', '(min-width:400px)'],\n [5, 4, 3, 2],\n 1\n );\n\n const [containerRef, { width }] = useMeasure();\n const [imagesReady, setImagesReady] = useState(false);\n\n const getInitialPosition = item => {\n const containerRect = containerRef.current?.getBoundingClientRect();\n if (!containerRect) return { x: item.x, y: item.y };\n\n let direction = animateFrom;\n\n if (animateFrom === 'random') {\n const directions = ['top', 'bottom', 'left', 'right'];\n direction = directions[Math.floor(Math.random() * directions.length)];\n }\n\n switch (direction) {\n case 'top':\n return { x: item.x, y: -200 };\n case 'bottom':\n return { x: item.x, y: window.innerHeight + 200 };\n case 'left':\n return { x: -200, y: item.y };\n case 'right':\n return { x: window.innerWidth + 200, y: item.y };\n case 'center':\n return {\n x: containerRect.width / 2 - item.w / 2,\n y: containerRect.height / 2 - item.h / 2\n };\n default:\n return { x: item.x, y: item.y + 100 };\n }\n };\n\n useEffect(() => {\n preloadImages(items.map(i => i.img)).then(() => setImagesReady(true));\n }, [items]);\n\n const grid = useMemo(() => {\n if (!width) return [];\n\n const colHeights = new Array(columns).fill(0);\n const columnWidth = width / columns;\n\n return items.map(child => {\n const col = colHeights.indexOf(Math.min(...colHeights));\n const x = columnWidth * col;\n const height = child.height / 2;\n const y = colHeights[col];\n\n colHeights[col] += height;\n\n return { ...child, x, y, w: columnWidth, h: height };\n });\n }, [columns, items, width]);\n\n const hasMounted = useRef(false);\n\n useLayoutEffect(() => {\n if (!imagesReady) return;\n\n grid.forEach((item, index) => {\n const selector = `[data-key=\"${item.id}\"]`;\n const animationProps = {\n x: item.x,\n y: item.y,\n width: item.w,\n height: item.h\n };\n\n if (!hasMounted.current) {\n const initialPos = getInitialPosition(item, index);\n const initialState = {\n opacity: 0,\n x: initialPos.x,\n y: initialPos.y,\n width: item.w,\n height: item.h,\n ...(blurToFocus && { filter: 'blur(10px)' })\n };\n\n gsap.fromTo(selector, initialState, {\n opacity: 1,\n ...animationProps,\n ...(blurToFocus && { filter: 'blur(0px)' }),\n duration: 0.8,\n ease: 'power3.out',\n delay: index * stagger\n });\n } else {\n gsap.to(selector, {\n ...animationProps,\n duration: duration,\n ease: ease,\n overwrite: 'auto'\n });\n }\n });\n\n hasMounted.current = true;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [grid, imagesReady, stagger, animateFrom, blurToFocus, duration, ease]);\n\n const handleMouseEnter = (e, item) => {\n const element = e.currentTarget;\n const selector = `[data-key=\"${item.id}\"]`;\n\n if (scaleOnHover) {\n gsap.to(selector, {\n scale: hoverScale,\n duration: 0.3,\n ease: 'power2.out'\n });\n }\n\n if (colorShiftOnHover) {\n const overlay = element.querySelector('.color-overlay');\n if (overlay) {\n gsap.to(overlay, {\n opacity: 0.3,\n duration: 0.3\n });\n }\n }\n };\n\n const handleMouseLeave = (e, item) => {\n const element = e.currentTarget;\n const selector = `[data-key=\"${item.id}\"]`;\n\n if (scaleOnHover) {\n gsap.to(selector, {\n scale: 1,\n duration: 0.3,\n ease: 'power2.out'\n });\n }\n\n if (colorShiftOnHover) {\n const overlay = element.querySelector('.color-overlay');\n if (overlay) {\n gsap.to(overlay, {\n opacity: 0,\n duration: 0.3\n });\n }\n }\n };\n\n return (\n
    \n {grid.map(item => {\n return (\n window.open(item.url, '_blank', 'noopener')}\n onMouseEnter={e => handleMouseEnter(e, item)}\n onMouseLeave={e => handleMouseLeave(e, item)}\n >\n
    \n {colorShiftOnHover && (\n \n )}\n
    \n
    \n );\n })}\n
    \n );\n};\n\nexport default Masonry;\n" } ], diff --git a/public/r/Masonry-TS-CSS.json b/public/r/Masonry-TS-CSS.json index c3399588..84d37171 100644 --- a/public/r/Masonry-TS-CSS.json +++ b/public/r/Masonry-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Masonry/Masonry.css", + "type": "registry:file", + "path": "Masonry.css", + "target": "@components/Masonry.css", "content": ".list {\n position: relative;\n width: 100%;\n height: 100%;\n}\n\n.item-wrapper {\n position: absolute;\n will-change: transform, width, height, opacity;\n padding: 6px;\n cursor: pointer;\n top: 0;\n left: 0;\n}\n\n.item-wrapper > .item-img {\n position: relative;\n background-size: cover;\n background-position: center center;\n width: 100%;\n height: 100%;\n text-transform: uppercase;\n font-size: 10px;\n line-height: 10px;\n border-radius: 10px;\n box-shadow: 0px 10px 50px -10px rgba(0, 0, 0, 0.2);\n}\n" }, { "type": "registry:component", - "path": "Masonry/Masonry.tsx", + "path": "Masonry.tsx", "content": "import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport { gsap } from 'gsap';\n\nimport './Masonry.css';\n\nconst useMedia = (queries: string[], values: number[], defaultValue: number): number => {\n const get = () => {\n if (typeof window === 'undefined') return defaultValue;\n return values[queries.findIndex(q => matchMedia(q).matches)] ?? defaultValue;\n };\n\n const [value, setValue] = useState(get);\n\n useEffect(() => {\n const handler = () => setValue(get);\n queries.forEach(q => matchMedia(q).addEventListener('change', handler));\n return () => queries.forEach(q => matchMedia(q).removeEventListener('change', handler));\n }, [queries]);\n\n return value;\n};\n\nconst useMeasure = () => {\n const ref = useRef(null);\n const [size, setSize] = useState({ width: 0, height: 0 });\n\n useLayoutEffect(() => {\n if (!ref.current) return;\n const ro = new ResizeObserver(([entry]) => {\n const { width, height } = entry.contentRect;\n setSize({ width, height });\n });\n ro.observe(ref.current);\n return () => ro.disconnect();\n }, []);\n\n return [ref, size] as const;\n};\n\nconst preloadImages = async (urls: string[]): Promise => {\n await Promise.all(\n urls.map(\n src =>\n new Promise(resolve => {\n const img = new Image();\n img.src = src;\n img.onload = img.onerror = () => resolve();\n })\n )\n );\n};\n\ninterface Item {\n id: string;\n img: string;\n url: string;\n height: number;\n}\n\ninterface GridItem extends Item {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\ninterface MasonryProps {\n items: Item[];\n ease?: string;\n duration?: number;\n stagger?: number;\n animateFrom?: 'bottom' | 'top' | 'left' | 'right' | 'center' | 'random';\n scaleOnHover?: boolean;\n hoverScale?: number;\n blurToFocus?: boolean;\n colorShiftOnHover?: boolean;\n}\n\nconst Masonry: React.FC = ({\n items,\n ease = 'power3.out',\n duration = 0.6,\n stagger = 0.05,\n animateFrom = 'bottom',\n scaleOnHover = true,\n hoverScale = 0.95,\n blurToFocus = true,\n colorShiftOnHover = false\n}) => {\n const columns = useMedia(\n ['(min-width:1500px)', '(min-width:1000px)', '(min-width:600px)', '(min-width:400px)'],\n [5, 4, 3, 2],\n 1\n );\n\n const [containerRef, { width }] = useMeasure();\n const [imagesReady, setImagesReady] = useState(false);\n\n const getInitialPosition = (item: GridItem) => {\n const containerRect = containerRef.current?.getBoundingClientRect();\n if (!containerRect) return { x: item.x, y: item.y };\n\n let direction = animateFrom;\n\n if (animateFrom === 'random') {\n const directions = ['top', 'bottom', 'left', 'right'];\n direction = directions[Math.floor(Math.random() * directions.length)] as typeof animateFrom;\n }\n\n switch (direction) {\n case 'top':\n return { x: item.x, y: -200 };\n case 'bottom':\n return { x: item.x, y: window.innerHeight + 200 };\n case 'left':\n return { x: -200, y: item.y };\n case 'right':\n return { x: window.innerWidth + 200, y: item.y };\n case 'center':\n return {\n x: containerRect.width / 2 - item.w / 2,\n y: containerRect.height / 2 - item.h / 2\n };\n default:\n return { x: item.x, y: item.y + 100 };\n }\n };\n\n useEffect(() => {\n preloadImages(items.map(i => i.img)).then(() => setImagesReady(true));\n }, [items]);\n\n const grid = useMemo(() => {\n if (!width) return [];\n\n const colHeights = new Array(columns).fill(0);\n const columnWidth = width / columns;\n\n return items.map(child => {\n const col = colHeights.indexOf(Math.min(...colHeights));\n const x = columnWidth * col;\n const height = child.height / 2;\n const y = colHeights[col];\n\n colHeights[col] += height;\n\n return { ...child, x, y, w: columnWidth, h: height };\n });\n }, [columns, items, width]);\n\n const hasMounted = useRef(false);\n\n useLayoutEffect(() => {\n if (!imagesReady) return;\n\n grid.forEach((item, index) => {\n const selector = `[data-key=\"${item.id}\"]`;\n const animationProps = {\n x: item.x,\n y: item.y,\n width: item.w,\n height: item.h\n };\n\n if (!hasMounted.current) {\n const initialPos = getInitialPosition(item);\n const initialState = {\n opacity: 0,\n x: initialPos.x,\n y: initialPos.y,\n width: item.w,\n height: item.h,\n ...(blurToFocus && { filter: 'blur(10px)' })\n };\n\n gsap.fromTo(selector, initialState, {\n opacity: 1,\n ...animationProps,\n ...(blurToFocus && { filter: 'blur(0px)' }),\n duration: 0.8,\n ease: 'power3.out',\n delay: index * stagger\n });\n } else {\n gsap.to(selector, {\n ...animationProps,\n duration: duration,\n ease: ease,\n overwrite: 'auto'\n });\n }\n });\n\n hasMounted.current = true;\n }, [grid, imagesReady, stagger, animateFrom, blurToFocus, duration, ease]);\n\n const handleMouseEnter = (e: React.MouseEvent, item: GridItem) => {\n const element = e.currentTarget as HTMLElement;\n const selector = `[data-key=\"${item.id}\"]`;\n\n if (scaleOnHover) {\n gsap.to(selector, {\n scale: hoverScale,\n duration: 0.3,\n ease: 'power2.out'\n });\n }\n\n if (colorShiftOnHover) {\n const overlay = element.querySelector('.color-overlay') as HTMLElement;\n if (overlay) {\n gsap.to(overlay, {\n opacity: 0.3,\n duration: 0.3\n });\n }\n }\n };\n\n const handleMouseLeave = (e: React.MouseEvent, item: GridItem) => {\n const element = e.currentTarget as HTMLElement;\n const selector = `[data-key=\"${item.id}\"]`;\n\n if (scaleOnHover) {\n gsap.to(selector, {\n scale: 1,\n duration: 0.3,\n ease: 'power2.out'\n });\n }\n\n if (colorShiftOnHover) {\n const overlay = element.querySelector('.color-overlay') as HTMLElement;\n if (overlay) {\n gsap.to(overlay, {\n opacity: 0,\n duration: 0.3\n });\n }\n }\n };\n\n return (\n
    \n {grid.map(item => {\n return (\n window.open(item.url, '_blank', 'noopener')}\n onMouseEnter={e => handleMouseEnter(e, item)}\n onMouseLeave={e => handleMouseLeave(e, item)}\n >\n
    \n {colorShiftOnHover && (\n \n )}\n
    \n
    \n );\n })}\n
    \n );\n};\n\nexport default Masonry;\n" } ], diff --git a/public/r/MetaBalls-JS-CSS.json b/public/r/MetaBalls-JS-CSS.json index 7da5eb00..c05c3f50 100644 --- a/public/r/MetaBalls-JS-CSS.json +++ b/public/r/MetaBalls-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "MetaBalls/MetaBalls.css", + "type": "registry:file", + "path": "MetaBalls.css", + "target": "@components/MetaBalls.css", "content": ".metaballs-container {\n position: relative;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "MetaBalls/MetaBalls.jsx", + "path": "MetaBalls.jsx", "content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle, Transform, Vec3, Camera } from 'ogl';\n\nimport './MetaBalls.css';\n\nfunction parseHexColor(hex) {\n const c = hex.replace('#', '');\n const r = parseInt(c.substring(0, 2), 16) / 255;\n const g = parseInt(c.substring(2, 4), 16) / 255;\n const b = parseInt(c.substring(4, 6), 16) / 255;\n return [r, g, b];\n}\n\nfunction fract(x) {\n return x - Math.floor(x);\n}\n\nfunction hash31(p) {\n let r = [p * 0.1031, p * 0.103, p * 0.0973].map(fract);\n const r_yzx = [r[1], r[2], r[0]];\n const dotVal = r[0] * (r_yzx[0] + 33.33) + r[1] * (r_yzx[1] + 33.33) + r[2] * (r_yzx[2] + 33.33);\n for (let i = 0; i < 3; i++) {\n r[i] = fract(r[i] + dotVal);\n }\n return r;\n}\n\nfunction hash33(v) {\n let p = [v[0] * 0.1031, v[1] * 0.103, v[2] * 0.0973].map(fract);\n const p_yxz = [p[1], p[0], p[2]];\n const dotVal = p[0] * (p_yxz[0] + 33.33) + p[1] * (p_yxz[1] + 33.33) + p[2] * (p_yxz[2] + 33.33);\n for (let i = 0; i < 3; i++) {\n p[i] = fract(p[i] + dotVal);\n }\n const p_xxy = [p[0], p[0], p[1]];\n const p_yxx = [p[1], p[0], p[0]];\n const p_zyx = [p[2], p[1], p[0]];\n const result = [];\n for (let i = 0; i < 3; i++) {\n result[i] = fract((p_xxy[i] + p_yxx[i]) * p_zyx[i]);\n }\n return result;\n}\n\nconst vertex = `#version 300 es\nprecision highp float;\nlayout(location = 0) in vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec3 iResolution;\nuniform float iTime;\nuniform vec3 iMouse;\nuniform vec3 iColor;\nuniform vec3 iCursorColor;\nuniform float iAnimationSize;\nuniform int iBallCount;\nuniform float iCursorBallSize;\nuniform vec3 iMetaBalls[50];\nuniform float iClumpFactor;\nuniform bool enableTransparency;\nout vec4 outColor;\nconst float PI = 3.14159265359;\n\nfloat getMetaBallValue(vec2 c, float r, vec2 p) {\n vec2 d = p - c;\n float dist2 = dot(d, d);\n return (r * r) / dist2;\n}\n\nvoid main() {\n vec2 fc = gl_FragCoord.xy;\n float scale = iAnimationSize / iResolution.y;\n vec2 coord = (fc - iResolution.xy * 0.5) * scale;\n vec2 mouseW = (iMouse.xy - iResolution.xy * 0.5) * scale;\n float m1 = 0.0;\n for (int i = 0; i < 50; i++) {\n if (i >= iBallCount) break;\n m1 += getMetaBallValue(iMetaBalls[i].xy, iMetaBalls[i].z, coord);\n }\n float m2 = getMetaBallValue(mouseW, iCursorBallSize, coord);\n float total = m1 + m2;\n float f = smoothstep(-1.0, 1.0, (total - 1.3) / min(1.0, fwidth(total)));\n vec3 cFinal = vec3(0.0);\n if (total > 0.0) {\n float alpha1 = m1 / total;\n float alpha2 = m2 / total;\n cFinal = iColor * alpha1 + iCursorColor * alpha2;\n }\n outColor = vec4(cFinal * f, enableTransparency ? f : 1.0);\n}\n`;\n\nconst MetaBalls = ({\n className = '',\n color = '#ffffff',\n speed = 0.3,\n enableMouseInteraction = true,\n hoverSmoothness = 0.05,\n animationSize = 30,\n ballCount = 15,\n clumpFactor = 1,\n cursorBallSize = 3,\n cursorBallColor = '#ffffff',\n enableTransparency = true\n}) => {\n const containerRef = useRef(null);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const dpr = 1;\n const renderer = new Renderer({ dpr, alpha: true, premultipliedAlpha: false });\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, enableTransparency ? 0 : 1);\n container.appendChild(gl.canvas);\n\n const camera = new Camera(gl, {\n left: -1,\n right: 1,\n top: 1,\n bottom: -1,\n near: 0.1,\n far: 10\n });\n camera.position.z = 1;\n\n const geometry = new Triangle(gl);\n const [r1, g1, b1] = parseHexColor(color);\n const [r2, g2, b2] = parseHexColor(cursorBallColor);\n\n const metaBallsUniform = [];\n for (let i = 0; i < 50; i++) {\n metaBallsUniform.push(new Vec3(0, 0, 0));\n }\n\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Vec3(0, 0, 0) },\n iMouse: { value: new Vec3(0, 0, 0) },\n iColor: { value: new Vec3(r1, g1, b1) },\n iCursorColor: { value: new Vec3(r2, g2, b2) },\n iAnimationSize: { value: animationSize },\n iBallCount: { value: ballCount },\n iCursorBallSize: { value: cursorBallSize },\n iMetaBalls: { value: metaBallsUniform },\n iClumpFactor: { value: clumpFactor },\n enableTransparency: { value: enableTransparency }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n const scene = new Transform();\n mesh.setParent(scene);\n\n const maxBalls = 50;\n const effectiveBallCount = Math.min(ballCount, maxBalls);\n const ballParams = [];\n for (let i = 0; i < effectiveBallCount; i++) {\n const idx = i + 1;\n const h1 = hash31(idx);\n const st = h1[0] * (2 * Math.PI);\n const dtFactor = 0.1 * Math.PI + h1[1] * (0.4 * Math.PI - 0.1 * Math.PI);\n const baseScale = 5.0 + h1[1] * (10.0 - 5.0);\n const h2 = hash33(h1);\n const toggle = Math.floor(h2[0] * 2.0);\n const radiusVal = 0.5 + h2[2] * (2.0 - 0.5);\n ballParams.push({ st, dtFactor, baseScale, toggle, radius: radiusVal });\n }\n\n const mouseBallPos = { x: 0, y: 0 };\n let pointerInside = false;\n let pointerX = 0;\n let pointerY = 0;\n\n function resize() {\n if (!container) return;\n const width = container.clientWidth;\n const height = container.clientHeight;\n renderer.setSize(width * dpr, height * dpr);\n gl.canvas.style.width = width + 'px';\n gl.canvas.style.height = height + 'px';\n program.uniforms.iResolution.value.set(gl.canvas.width, gl.canvas.height, 0);\n }\n window.addEventListener('resize', resize);\n resize();\n\n function onPointerMove(e) {\n if (!enableMouseInteraction) return;\n const rect = container.getBoundingClientRect();\n const px = e.clientX - rect.left;\n const py = e.clientY - rect.top;\n pointerX = (px / rect.width) * gl.canvas.width;\n pointerY = (1 - py / rect.height) * gl.canvas.height;\n }\n function onPointerEnter() {\n if (!enableMouseInteraction) return;\n pointerInside = true;\n }\n function onPointerLeave() {\n if (!enableMouseInteraction) return;\n pointerInside = false;\n }\n container.addEventListener('pointermove', onPointerMove);\n container.addEventListener('pointerenter', onPointerEnter);\n container.addEventListener('pointerleave', onPointerLeave);\n\n const startTime = performance.now();\n let animationFrameId;\n function update(t) {\n animationFrameId = requestAnimationFrame(update);\n const elapsed = (t - startTime) * 0.001;\n program.uniforms.iTime.value = elapsed;\n\n for (let i = 0; i < effectiveBallCount; i++) {\n const p = ballParams[i];\n const dt = elapsed * speed * p.dtFactor;\n const th = p.st + dt;\n const x = Math.cos(th);\n const y = Math.sin(th + dt * p.toggle);\n const posX = x * p.baseScale * clumpFactor;\n const posY = y * p.baseScale * clumpFactor;\n metaBallsUniform[i].set(posX, posY, p.radius);\n }\n\n let targetX, targetY;\n if (pointerInside) {\n targetX = pointerX;\n targetY = pointerY;\n } else {\n const cx = gl.canvas.width * 0.5;\n const cy = gl.canvas.height * 0.5;\n const rx = gl.canvas.width * 0.15;\n const ry = gl.canvas.height * 0.15;\n targetX = cx + Math.cos(elapsed * speed) * rx;\n targetY = cy + Math.sin(elapsed * speed) * ry;\n }\n mouseBallPos.x += (targetX - mouseBallPos.x) * hoverSmoothness;\n mouseBallPos.y += (targetY - mouseBallPos.y) * hoverSmoothness;\n program.uniforms.iMouse.value.set(mouseBallPos.x, mouseBallPos.y, 0);\n\n renderer.render({ scene, camera });\n }\n animationFrameId = requestAnimationFrame(update);\n\n return () => {\n cancelAnimationFrame(animationFrameId);\n window.removeEventListener('resize', resize);\n container.removeEventListener('pointermove', onPointerMove);\n container.removeEventListener('pointerenter', onPointerEnter);\n container.removeEventListener('pointerleave', onPointerLeave);\n container.removeChild(gl.canvas);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, [\n color,\n cursorBallColor,\n speed,\n enableMouseInteraction,\n hoverSmoothness,\n animationSize,\n ballCount,\n clumpFactor,\n cursorBallSize,\n enableTransparency\n ]);\n\n return
    ;\n};\n\nexport default MetaBalls;\n" } ], diff --git a/public/r/MetaBalls-TS-CSS.json b/public/r/MetaBalls-TS-CSS.json index 7692fb72..84e37e0d 100644 --- a/public/r/MetaBalls-TS-CSS.json +++ b/public/r/MetaBalls-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "MetaBalls/MetaBalls.css", + "type": "registry:file", + "path": "MetaBalls.css", + "target": "@components/MetaBalls.css", "content": ".metaballs-container {\n position: relative;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "MetaBalls/MetaBalls.tsx", + "path": "MetaBalls.tsx", "content": "import React, { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle, Transform, Vec3, Camera } from 'ogl';\nimport './MetaBalls.css';\n\ntype MetaBallsProps = {\n color?: string;\n speed?: number;\n enableMouseInteraction?: boolean;\n hoverSmoothness?: number;\n animationSize?: number;\n ballCount?: number;\n clumpFactor?: number;\n cursorBallSize?: number;\n cursorBallColor?: string;\n enableTransparency?: boolean;\n};\n\nfunction parseHexColor(hex: string): [number, number, number] {\n const c = hex.replace('#', '');\n const r = parseInt(c.substring(0, 2), 16) / 255;\n const g = parseInt(c.substring(2, 4), 16) / 255;\n const b = parseInt(c.substring(4, 6), 16) / 255;\n return [r, g, b];\n}\n\nfunction fract(x: number): number {\n return x - Math.floor(x);\n}\n\nfunction hash31(p: number): number[] {\n let r = [p * 0.1031, p * 0.103, p * 0.0973].map(fract);\n const r_yzx = [r[1], r[2], r[0]];\n const dotVal = r[0] * (r_yzx[0] + 33.33) + r[1] * (r_yzx[1] + 33.33) + r[2] * (r_yzx[2] + 33.33);\n for (let i = 0; i < 3; i++) {\n r[i] = fract(r[i] + dotVal);\n }\n return r;\n}\n\nfunction hash33(v: number[]): number[] {\n let p = [v[0] * 0.1031, v[1] * 0.103, v[2] * 0.0973].map(fract);\n const p_yxz = [p[1], p[0], p[2]];\n const dotVal = p[0] * (p_yxz[0] + 33.33) + p[1] * (p_yxz[1] + 33.33) + p[2] * (p_yxz[2] + 33.33);\n for (let i = 0; i < 3; i++) {\n p[i] = fract(p[i] + dotVal);\n }\n const p_xxy = [p[0], p[0], p[1]];\n const p_yxx = [p[1], p[0], p[0]];\n const p_zyx = [p[2], p[1], p[0]];\n const result: number[] = [];\n for (let i = 0; i < 3; i++) {\n result[i] = fract((p_xxy[i] + p_yxx[i]) * p_zyx[i]);\n }\n return result;\n}\n\nconst vertex = `#version 300 es\nprecision highp float;\nlayout(location = 0) in vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec3 iResolution;\nuniform float iTime;\nuniform vec3 iMouse;\nuniform vec3 iColor;\nuniform vec3 iCursorColor;\nuniform float iAnimationSize;\nuniform int iBallCount;\nuniform float iCursorBallSize;\nuniform vec3 iMetaBalls[50];\nuniform float iClumpFactor;\nuniform bool enableTransparency;\nout vec4 outColor;\nconst float PI = 3.14159265359;\n \nfloat getMetaBallValue(vec2 c, float r, vec2 p) {\n vec2 d = p - c;\n float dist2 = dot(d, d);\n return (r * r) / dist2;\n}\n \nvoid main() {\n vec2 fc = gl_FragCoord.xy;\n float scale = iAnimationSize / iResolution.y;\n vec2 coord = (fc - iResolution.xy * 0.5) * scale;\n vec2 mouseW = (iMouse.xy - iResolution.xy * 0.5) * scale;\n float m1 = 0.0;\n for (int i = 0; i < 50; i++) {\n if (i >= iBallCount) break;\n m1 += getMetaBallValue(iMetaBalls[i].xy, iMetaBalls[i].z, coord);\n }\n float m2 = getMetaBallValue(mouseW, iCursorBallSize, coord);\n float total = m1 + m2;\n float f = smoothstep(-1.0, 1.0, (total - 1.3) / min(1.0, fwidth(total)));\n vec3 cFinal = vec3(0.0);\n if (total > 0.0) {\n float alpha1 = m1 / total;\n float alpha2 = m2 / total;\n cFinal = iColor * alpha1 + iCursorColor * alpha2;\n }\n outColor = vec4(cFinal * f, enableTransparency ? f : 1.0);\n}\n`;\n\ntype BallParams = {\n st: number;\n dtFactor: number;\n baseScale: number;\n toggle: number;\n radius: number;\n};\n\nconst MetaBalls: React.FC = ({\n color = '#ffffff',\n speed = 0.3,\n enableMouseInteraction = true,\n hoverSmoothness = 0.05,\n animationSize = 30,\n ballCount = 15,\n clumpFactor = 1,\n cursorBallSize = 3,\n cursorBallColor = '#ffffff',\n enableTransparency = false\n}) => {\n const containerRef = useRef(null);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const dpr = 1;\n const renderer = new Renderer({\n dpr,\n alpha: true,\n premultipliedAlpha: false\n });\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, enableTransparency ? 0 : 1);\n container.appendChild(gl.canvas);\n\n const camera = new Camera(gl, {\n left: -1,\n right: 1,\n top: 1,\n bottom: -1,\n near: 0.1,\n far: 10\n });\n camera.position.z = 1;\n\n const geometry = new Triangle(gl);\n const [r1, g1, b1] = parseHexColor(color);\n const [r2, g2, b2] = parseHexColor(cursorBallColor);\n\n const metaBallsUniform: Vec3[] = [];\n for (let i = 0; i < 50; i++) {\n metaBallsUniform.push(new Vec3(0, 0, 0));\n }\n\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Vec3(0, 0, 0) },\n iMouse: { value: new Vec3(0, 0, 0) },\n iColor: { value: new Vec3(r1, g1, b1) },\n iCursorColor: { value: new Vec3(r2, g2, b2) },\n iAnimationSize: { value: animationSize },\n iBallCount: { value: ballCount },\n iCursorBallSize: { value: cursorBallSize },\n iMetaBalls: { value: metaBallsUniform },\n iClumpFactor: { value: clumpFactor },\n enableTransparency: { value: enableTransparency }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n const scene = new Transform();\n mesh.setParent(scene);\n\n const maxBalls = 50;\n const effectiveBallCount = Math.min(ballCount, maxBalls);\n const ballParams: BallParams[] = [];\n for (let i = 0; i < effectiveBallCount; i++) {\n const idx = i + 1;\n const h1 = hash31(idx);\n const st = h1[0] * (2 * Math.PI);\n const dtFactor = 0.1 * Math.PI + h1[1] * (0.4 * Math.PI - 0.1 * Math.PI);\n const baseScale = 5.0 + h1[1] * (10.0 - 5.0);\n const h2 = hash33(h1);\n const toggle = Math.floor(h2[0] * 2.0);\n const radiusVal = 0.5 + h2[2] * (2.0 - 0.5);\n ballParams.push({ st, dtFactor, baseScale, toggle, radius: radiusVal });\n }\n\n const mouseBallPos = { x: 0, y: 0 };\n let pointerInside = false;\n let pointerX = 0;\n let pointerY = 0;\n\n function resize() {\n if (!container) return;\n const width = container.clientWidth;\n const height = container.clientHeight;\n renderer.setSize(width * dpr, height * dpr);\n gl.canvas.style.width = `${width}px`;\n gl.canvas.style.height = `${height}px`;\n program.uniforms.iResolution.value.set(gl.canvas.width, gl.canvas.height, 0);\n }\n window.addEventListener('resize', resize);\n resize();\n\n function onPointerMove(e: PointerEvent) {\n if (!enableMouseInteraction || !container) return;\n const rect = container.getBoundingClientRect();\n const px = e.clientX - rect.left;\n const py = e.clientY - rect.top;\n pointerX = (px / rect.width) * gl.canvas.width;\n pointerY = (1 - py / rect.height) * gl.canvas.height;\n }\n function onPointerEnter() {\n if (!enableMouseInteraction) return;\n pointerInside = true;\n }\n function onPointerLeave() {\n if (!enableMouseInteraction) return;\n pointerInside = false;\n }\n container.addEventListener('pointermove', onPointerMove);\n container.addEventListener('pointerenter', onPointerEnter);\n container.addEventListener('pointerleave', onPointerLeave);\n\n const startTime = performance.now();\n let animationFrameId: number;\n function update(t: number) {\n animationFrameId = requestAnimationFrame(update);\n const elapsed = (t - startTime) * 0.001;\n program.uniforms.iTime.value = elapsed;\n\n for (let i = 0; i < effectiveBallCount; i++) {\n const p = ballParams[i];\n const dt = elapsed * speed * p.dtFactor;\n const th = p.st + dt;\n const x = Math.cos(th);\n const y = Math.sin(th + dt * p.toggle);\n const posX = x * p.baseScale * clumpFactor;\n const posY = y * p.baseScale * clumpFactor;\n metaBallsUniform[i].set(posX, posY, p.radius);\n }\n\n let targetX: number, targetY: number;\n if (pointerInside) {\n targetX = pointerX;\n targetY = pointerY;\n } else {\n const cx = gl.canvas.width * 0.5;\n const cy = gl.canvas.height * 0.5;\n const rx = gl.canvas.width * 0.15;\n const ry = gl.canvas.height * 0.15;\n targetX = cx + Math.cos(elapsed * speed) * rx;\n targetY = cy + Math.sin(elapsed * speed) * ry;\n }\n mouseBallPos.x += (targetX - mouseBallPos.x) * hoverSmoothness;\n mouseBallPos.y += (targetY - mouseBallPos.y) * hoverSmoothness;\n program.uniforms.iMouse.value.set(mouseBallPos.x, mouseBallPos.y, 0);\n\n renderer.render({ scene, camera });\n }\n animationFrameId = requestAnimationFrame(update);\n\n return () => {\n cancelAnimationFrame(animationFrameId);\n window.removeEventListener('resize', resize);\n container.removeEventListener('pointermove', onPointerMove);\n container.removeEventListener('pointerenter', onPointerEnter);\n container.removeEventListener('pointerleave', onPointerLeave);\n container.removeChild(gl.canvas);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, [\n color,\n cursorBallColor,\n speed,\n enableMouseInteraction,\n hoverSmoothness,\n animationSize,\n ballCount,\n clumpFactor,\n cursorBallSize,\n enableTransparency\n ]);\n\n return
    ;\n};\n\nexport default MetaBalls;\n" } ], diff --git a/public/r/MetallicPaint-JS-CSS.json b/public/r/MetallicPaint-JS-CSS.json index 626ff981..e0ae6016 100644 --- a/public/r/MetallicPaint-JS-CSS.json +++ b/public/r/MetallicPaint-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "MetallicPaint/MetallicPaint.css", + "type": "registry:file", + "path": "MetallicPaint.css", + "target": "@components/MetallicPaint.css", "content": ".paint-container {\n display: block;\n height: 100%;\n width: 100%;\n object-fit: contain;\n}\n" }, { "type": "registry:component", - "path": "MetallicPaint/MetallicPaint.jsx", + "path": "MetallicPaint.jsx", "content": "'use client';\n\nimport { useEffect, useRef, useState, useCallback } from 'react';\nimport './MetallicPaint.css';\n\nconst vertexShader = `#version 300 es\nprecision highp float;\nin vec2 a_position;\nout vec2 vP;\nvoid main(){vP=a_position*.5+.5;gl_Position=vec4(a_position,0.,1.);}`;\n\nconst fragmentShader = `#version 300 es\nprecision highp float;\nin vec2 vP;\nout vec4 oC;\nuniform sampler2D u_tex;\nuniform float u_time,u_ratio,u_imgRatio,u_seed,u_scale,u_refract,u_blur,u_liquid;\nuniform float u_bright,u_contrast,u_angle,u_fresnel,u_sharp,u_wave,u_noise,u_chroma;\nuniform float u_distort,u_contour;\nuniform vec3 u_lightColor,u_darkColor,u_tint;\n\nvec3 sC,sM;\n\nvec3 pW(vec3 v){\n vec3 i=floor(v),f=fract(v),s=sign(fract(v*.5)-.5),h=fract(sM*i+i.yzx),c=f*(f-1.);\n return s*c*((h*16.-4.)*c-1.);\n}\n\nvec3 aF(vec3 b,vec3 c){return pW(b+c.zxy-pW(b.zxy+c.yzx)+pW(b.yzx+c.xyz));}\nvec3 lM(vec3 s,vec3 p){return(p+aF(s,p))*.5;}\n\nvec2 fA(){\n vec2 c=vP-.5;\n c.x*=u_ratio>u_imgRatio?u_ratio/u_imgRatio:1.;\n c.y*=u_ratio>u_imgRatio?1.:u_imgRatio/u_ratio;\n return vec2(c.x+.5,.5-c.y);\n}\n\nvec2 rot(vec2 p,float r){float c=cos(r),s=sin(r);return vec2(p.x*c+p.y*s,p.y*c-p.x*s);}\n\nfloat bM(vec2 c,float t){\n vec2 l=smoothstep(vec2(0.),vec2(t),c),u=smoothstep(vec2(0.),vec2(t),1.-c);\n return l.x*l.y*u.x*u.y;\n}\n\nfloat mG(float hi,float lo,float t,float sh,float cv){\n sh*=(2.-u_sharp);\n float ci=smoothstep(.15,.85,cv),r=lo;\n float e1=.08/u_scale;\n r=mix(r,hi,smoothstep(0.,sh*1.5,t));\n r=mix(r,lo,smoothstep(e1-sh,e1+sh,t));\n float e2=e1+.05/u_scale*(1.-ci*.35);\n r=mix(r,hi,smoothstep(e2-sh,e2+sh,t));\n float e3=e2+.025/u_scale*(1.-ci*.45);\n r=mix(r,lo,smoothstep(e3-sh,e3+sh,t));\n float e4=e1+.1/u_scale;\n r=mix(r,hi,smoothstep(e4-sh,e4+sh,t));\n float rm=1.-e4,gT=clamp((t-e4)/rm,0.,1.);\n r=mix(r,mix(hi,lo,smoothstep(0.,1.,gT)),smoothstep(e4-sh*.5,e4+sh*.5,t));\n return r;\n}\n\nvoid main(){\n sC=fract(vec3(.7548,.5698,.4154)*(u_seed+17.31))+.5;\n sM=fract(sC.zxy-sC.yzx*1.618);\n vec2 sc=vec2(vP.x*u_ratio,1.-vP.y);\n float angleRad=u_angle*3.14159/180.;\n sc=rot(sc-.5,angleRad)+.5;\n sc=clamp(sc,0.,1.);\n float sl=sc.x-sc.y,an=u_time*.001;\n vec2 iC=fA();\n vec4 texSample=texture(u_tex,iC);\n float dp=texSample.r;\n float shapeMask=texSample.a;\n vec3 hi=u_lightColor*u_bright;\n vec3 lo=u_darkColor*(2.-u_bright);\n lo.b+=smoothstep(.6,1.4,sc.x+sc.y)*.08;\n vec2 fC=sc-.5;\n float rd=length(fC+vec2(0.,sl*.15));\n vec2 ag=rot(fC,(.22-sl*.18)*3.14159);\n float cv=1.-pow(rd*1.65,1.15);\n cv*=pow(sc.y,.35);\n float vs=shapeMask;\n vs*=bM(iC,.01);\n float fr=pow(1.-cv,u_fresnel)*.3;\n vs=min(vs+fr*vs,1.);\n float mT=an*.0625;\n vec3 wO=vec3(-1.05,1.35,1.55);\n vec3 wA=aF(vec3(31.,73.,56.),mT+wO)*.22*u_wave;\n vec3 wB=aF(vec3(24.,64.,42.),mT-wO.yzx)*.22*u_wave;\n vec2 nC=sc*45.*u_noise;\n nC+=aF(sC.zxy,an*.17*sC.yzx-sc.yxy*.35).xy*18.*u_wave;\n vec3 tC=vec3(.00041,.00053,.00076)*mT+wB*nC.x+wA*nC.y;\n tC=lM(sC,tC);\n tC=lM(sC+1.618,tC);\n float tb=sin(tC.x*3.14159)*.5+.5;\n tb=tb*2.-1.;\n float noiseVal=pW(vec3(sc*8.+an,an*.5)).x;\n float edgeFactor=smoothstep(0.,.5,dp)*smoothstep(1.,.5,dp);\n float lD=dp+(1.-dp)*u_liquid*tb;\n lD+=noiseVal*u_distort*.15*edgeFactor;\n float rB=clamp(1.-cv,0.,1.);\n float fl=ag.x+sl;\n fl+=noiseVal*sl*u_distort*edgeFactor;\n fl*=mix(1.,1.-dp*.5,u_contour);\n fl-=dp*u_contour*.8;\n float eI=smoothstep(0.,1.,lD)*smoothstep(1.,0.,lD);\n fl-=tb*sl*1.8*eI;\n float cA=cv*clamp(pow(sc.y,.12),.25,1.);\n fl*=.12+(1.05-lD)*cA;\n fl*=smoothstep(1.,.65,lD);\n float vA1=smoothstep(.08,.18,sc.y)*smoothstep(.38,.18,sc.y);\n float vA2=smoothstep(.08,.18,1.-sc.y)*smoothstep(.38,.18,1.-sc.y);\n fl+=vA1*.16+vA2*.025;\n fl*=.45+pow(sc.y,2.)*.55;\n fl*=u_scale;\n fl-=an;\n float rO=rB+cv*tb*.025;\n float vM1=smoothstep(-.12,.18,sc.y)*smoothstep(.48,.08,sc.y);\n float cM1=smoothstep(.35,.55,cv)*smoothstep(.95,.35,cv);\n rO+=vM1*cM1*4.5;\n rO-=sl;\n float bO=rB*1.25;\n float vM2=smoothstep(-.02,.35,sc.y)*smoothstep(.75,.08,sc.y);\n float cM2=smoothstep(.35,.55,cv)*smoothstep(.75,.35,cv);\n bO+=vM2*cM2*.9;\n bO-=lD*.18;\n rO*=u_refract*u_chroma;\n bO*=u_refract*u_chroma;\n float sf=u_blur;\n float rP=fract(fl+rO);\n float rC=mG(hi.r,lo.r,rP,sf+.018+u_refract*cv*.025,cv);\n float gP=fract(fl);\n float gC=mG(hi.g,lo.g,gP,sf+.008/max(.01,1.-sl),cv);\n float bP=fract(fl-bO);\n float bC=mG(hi.b,lo.b,bP,sf+.008,cv);\n vec3 col=vec3(rC,gC,bC);\n col=(col-.5)*u_contrast+.5;\n col=clamp(col,0.,1.);\n col=mix(col,1.-min(vec3(1.),(1.-col)/max(u_tint,vec3(.001))),length(u_tint-1.)*.5);\n col=clamp(col,0.,1.);\n oC=vec4(col*vs,vs);\n}`;\n\nfunction processImage(img) {\n const MAX_SIZE = 1000;\n const MIN_SIZE = 500;\n let width = img.naturalWidth || img.width;\n let height = img.naturalHeight || img.height;\n\n if (width > MAX_SIZE || height > MAX_SIZE || width < MIN_SIZE || height < MIN_SIZE) {\n const scale =\n width > height\n ? width > MAX_SIZE\n ? MAX_SIZE / width\n : width < MIN_SIZE\n ? MIN_SIZE / width\n : 1\n : height > MAX_SIZE\n ? MAX_SIZE / height\n : height < MIN_SIZE\n ? MIN_SIZE / height\n : 1;\n width = Math.round(width * scale);\n height = Math.round(height * scale);\n }\n\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n const ctx = canvas.getContext('2d');\n ctx.drawImage(img, 0, 0, width, height);\n\n const imageData = ctx.getImageData(0, 0, width, height);\n const data = imageData.data;\n const size = width * height;\n const alphaValues = new Float32Array(size);\n const shapeMask = new Uint8Array(size);\n const boundaryMask = new Uint8Array(size);\n\n for (let i = 0; i < size; i++) {\n const idx = i * 4;\n const r = data[idx],\n g = data[idx + 1],\n b = data[idx + 2],\n a = data[idx + 3];\n const isBackground = (r > 250 && g > 250 && b > 250 && a === 255) || a < 5;\n alphaValues[i] = isBackground ? 0 : a / 255;\n shapeMask[i] = alphaValues[i] > 0.1 ? 1 : 0;\n }\n\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const idx = y * width + x;\n if (!shapeMask[idx]) continue;\n if (\n x === 0 ||\n x === width - 1 ||\n y === 0 ||\n y === height - 1 ||\n !shapeMask[idx - 1] ||\n !shapeMask[idx + 1] ||\n !shapeMask[idx - width] ||\n !shapeMask[idx + width]\n ) {\n boundaryMask[idx] = 1;\n }\n }\n }\n\n const u = new Float32Array(size);\n const ITERATIONS = 200;\n const C = 0.01;\n const omega = 1.85;\n\n for (let iter = 0; iter < ITERATIONS; iter++) {\n for (let y = 1; y < height - 1; y++) {\n for (let x = 1; x < width - 1; x++) {\n const idx = y * width + x;\n if (!shapeMask[idx] || boundaryMask[idx]) continue;\n const sum =\n (shapeMask[idx + 1] ? u[idx + 1] : 0) +\n (shapeMask[idx - 1] ? u[idx - 1] : 0) +\n (shapeMask[idx + width] ? u[idx + width] : 0) +\n (shapeMask[idx - width] ? u[idx - width] : 0);\n const newVal = (C + sum) / 4;\n u[idx] = omega * newVal + (1 - omega) * u[idx];\n }\n }\n }\n\n let maxVal = 0;\n for (let i = 0; i < size; i++) if (u[i] > maxVal) maxVal = u[i];\n if (maxVal === 0) maxVal = 1;\n\n const outData = ctx.createImageData(width, height);\n for (let i = 0; i < size; i++) {\n const px = i * 4;\n const depth = u[i] / maxVal;\n const gray = Math.round(255 * (1 - depth * depth));\n outData.data[px] = outData.data[px + 1] = outData.data[px + 2] = gray;\n outData.data[px + 3] = Math.round(alphaValues[i] * 255);\n }\n\n return outData;\n}\n\nfunction hexToRgb(hex) {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n return result\n ? [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255]\n : [1, 1, 1];\n}\n\nexport default function MetallicPaint({\n imageSrc,\n seed = 42,\n scale = 4,\n refraction = 0.01,\n blur = 0.015,\n liquid = 0.75,\n speed = 0.3,\n brightness = 2,\n contrast = 0.5,\n angle = 0,\n fresnel = 1,\n lightColor = '#ffffff',\n darkColor = '#000000',\n patternSharpness = 1,\n waveAmplitude = 1,\n noiseScale = 0.5,\n chromaticSpread = 2,\n mouseAnimation = false,\n distortion = 1,\n contour = 0.2,\n tintColor = '#feb3ff'\n}) {\n const canvasRef = useRef(null);\n const glRef = useRef(null);\n const programRef = useRef(null);\n const uniformsRef = useRef({});\n const textureRef = useRef(null);\n const animTimeRef = useRef(0);\n const lastTimeRef = useRef(0);\n const rafRef = useRef(null);\n const imgDataRef = useRef(null);\n const speedRef = useRef(speed);\n const mouseRef = useRef({ x: 0.5, y: 0.5, targetX: 0.5, targetY: 0.5 });\n const mouseAnimRef = useRef(mouseAnimation);\n\n const [ready, setReady] = useState(false);\n const [textureReady, setTextureReady] = useState(false);\n\n useEffect(() => {\n speedRef.current = speed;\n }, [speed]);\n useEffect(() => {\n mouseAnimRef.current = mouseAnimation;\n }, [mouseAnimation]);\n\n const initGL = useCallback(() => {\n const canvas = canvasRef.current;\n if (!canvas) return false;\n\n const gl = canvas.getContext('webgl2', { antialias: true, alpha: true });\n if (!gl) return false;\n\n const compile = (src, type) => {\n const s = gl.createShader(type);\n gl.shaderSource(s, src);\n gl.compileShader(s);\n if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {\n console.error(gl.getShaderInfoLog(s));\n return null;\n }\n return s;\n };\n\n const vs = compile(vertexShader, gl.VERTEX_SHADER);\n const fs = compile(fragmentShader, gl.FRAGMENT_SHADER);\n if (!vs || !fs) return false;\n\n const prog = gl.createProgram();\n gl.attachShader(prog, vs);\n gl.attachShader(prog, fs);\n gl.linkProgram(prog);\n if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {\n console.error(gl.getProgramInfoLog(prog));\n return false;\n }\n\n const uniforms = {};\n const count = gl.getProgramParameter(prog, gl.ACTIVE_UNIFORMS);\n for (let i = 0; i < count; i++) {\n const info = gl.getActiveUniform(prog, i);\n if (info) uniforms[info.name] = gl.getUniformLocation(prog, info.name);\n }\n\n const verts = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);\n const buf = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW);\n\n gl.useProgram(prog);\n const pos = gl.getAttribLocation(prog, 'a_position');\n gl.enableVertexAttribArray(pos);\n gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);\n\n glRef.current = gl;\n programRef.current = prog;\n uniformsRef.current = uniforms;\n\n return true;\n }, []);\n\n const uploadTexture = useCallback(imgData => {\n const gl = glRef.current;\n const uniforms = uniformsRef.current;\n if (!gl || !imgData) return;\n\n if (textureRef.current) gl.deleteTexture(textureRef.current);\n\n const tex = gl.createTexture();\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, tex);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, imgData.width, imgData.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, imgData.data);\n gl.uniform1i(uniforms.u_tex, 0);\n\n const ratio = imgData.width / imgData.height;\n gl.uniform1f(uniforms.u_imgRatio, ratio);\n gl.uniform1f(uniforms.u_ratio, 1);\n\n textureRef.current = tex;\n imgDataRef.current = imgData;\n }, []);\n\n useEffect(() => {\n if (!initGL()) return;\n\n const canvas = canvasRef.current;\n const gl = glRef.current;\n const side = 1000 * devicePixelRatio;\n canvas.width = side;\n canvas.height = side;\n gl.viewport(0, 0, side, side);\n\n setReady(true);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n if (textureRef.current && glRef.current) {\n glRef.current.deleteTexture(textureRef.current);\n }\n };\n }, [initGL]);\n\n useEffect(() => {\n if (!ready || !imageSrc) return;\n\n setTextureReady(false);\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => {\n const imgData = processImage(img);\n uploadTexture(imgData);\n setTextureReady(true);\n };\n img.src = imageSrc;\n }, [ready, imageSrc, uploadTexture]);\n\n useEffect(() => {\n const gl = glRef.current;\n const u = uniformsRef.current;\n if (!gl || !ready) return;\n\n gl.uniform1f(u.u_seed, seed);\n gl.uniform1f(u.u_scale, scale);\n gl.uniform1f(u.u_refract, refraction);\n gl.uniform1f(u.u_blur, blur);\n gl.uniform1f(u.u_liquid, liquid);\n gl.uniform1f(u.u_bright, brightness);\n gl.uniform1f(u.u_contrast, contrast);\n gl.uniform1f(u.u_angle, angle);\n gl.uniform1f(u.u_fresnel, fresnel);\n\n const light = hexToRgb(lightColor);\n const dark = hexToRgb(darkColor);\n const tint = hexToRgb(tintColor);\n gl.uniform3f(u.u_lightColor, light[0], light[1], light[2]);\n gl.uniform3f(u.u_darkColor, dark[0], dark[1], dark[2]);\n gl.uniform1f(u.u_sharp, patternSharpness);\n gl.uniform1f(u.u_wave, waveAmplitude);\n gl.uniform1f(u.u_noise, noiseScale);\n gl.uniform1f(u.u_chroma, chromaticSpread);\n gl.uniform1f(u.u_distort, distortion);\n gl.uniform1f(u.u_contour, contour);\n gl.uniform3f(u.u_tint, tint[0], tint[1], tint[2]);\n }, [\n ready,\n seed,\n scale,\n refraction,\n blur,\n liquid,\n brightness,\n contrast,\n angle,\n fresnel,\n lightColor,\n darkColor,\n patternSharpness,\n waveAmplitude,\n noiseScale,\n chromaticSpread,\n distortion,\n contour,\n tintColor\n ]);\n\n useEffect(() => {\n if (!ready || !textureReady) return;\n\n const gl = glRef.current;\n const u = uniformsRef.current;\n const canvas = canvasRef.current;\n const mouse = mouseRef.current;\n\n const handleMouseMove = e => {\n const rect = canvas.getBoundingClientRect();\n mouse.targetX = (e.clientX - rect.left) / rect.width;\n mouse.targetY = (e.clientY - rect.top) / rect.height;\n };\n\n canvas.addEventListener('mousemove', handleMouseMove);\n\n const render = time => {\n const delta = time - lastTimeRef.current;\n lastTimeRef.current = time;\n\n if (mouseAnimRef.current) {\n mouse.x += (mouse.targetX - mouse.x) * 0.08;\n mouse.y += (mouse.targetY - mouse.y) * 0.08;\n animTimeRef.current = mouse.x * 3000 + mouse.y * 1500;\n } else {\n animTimeRef.current += delta * speedRef.current;\n }\n\n gl.uniform1f(u.u_time, animTimeRef.current);\n gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n rafRef.current = requestAnimationFrame(render);\n };\n\n lastTimeRef.current = performance.now();\n rafRef.current = requestAnimationFrame(render);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n canvas.removeEventListener('mousemove', handleMouseMove);\n };\n }, [ready, textureReady]);\n\n return ;\n}\n" } ], diff --git a/public/r/MetallicPaint-TS-CSS.json b/public/r/MetallicPaint-TS-CSS.json index 752bc651..0f54eb03 100644 --- a/public/r/MetallicPaint-TS-CSS.json +++ b/public/r/MetallicPaint-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "MetallicPaint/MetallicPaint.css", + "type": "registry:file", + "path": "MetallicPaint.css", + "target": "@components/MetallicPaint.css", "content": ".paint-container {\n display: block;\n height: 100%;\n width: 100%;\n object-fit: contain;\n}\n" }, { "type": "registry:component", - "path": "MetallicPaint/MetallicPaint.tsx", + "path": "MetallicPaint.tsx", "content": "'use client';\n\nimport { useEffect, useRef, useState, useCallback } from 'react';\nimport './MetallicPaint.css';\n\nconst vertexShader = `#version 300 es\nprecision highp float;\nin vec2 a_position;\nout vec2 vP;\nvoid main(){vP=a_position*.5+.5;gl_Position=vec4(a_position,0.,1.);}`;\n\nconst fragmentShader = `#version 300 es\nprecision highp float;\nin vec2 vP;\nout vec4 oC;\nuniform sampler2D u_tex;\nuniform float u_time,u_ratio,u_imgRatio,u_seed,u_scale,u_refract,u_blur,u_liquid;\nuniform float u_bright,u_contrast,u_angle,u_fresnel,u_sharp,u_wave,u_noise,u_chroma;\nuniform float u_distort,u_contour;\nuniform vec3 u_lightColor,u_darkColor,u_tint;\n\nvec3 sC,sM;\n\nvec3 pW(vec3 v){\n vec3 i=floor(v),f=fract(v),s=sign(fract(v*.5)-.5),h=fract(sM*i+i.yzx),c=f*(f-1.);\n return s*c*((h*16.-4.)*c-1.);\n}\n\nvec3 aF(vec3 b,vec3 c){return pW(b+c.zxy-pW(b.zxy+c.yzx)+pW(b.yzx+c.xyz));}\nvec3 lM(vec3 s,vec3 p){return(p+aF(s,p))*.5;}\n\nvec2 fA(){\n vec2 c=vP-.5;\n c.x*=u_ratio>u_imgRatio?u_ratio/u_imgRatio:1.;\n c.y*=u_ratio>u_imgRatio?1.:u_imgRatio/u_ratio;\n return vec2(c.x+.5,.5-c.y);\n}\n\nvec2 rot(vec2 p,float r){float c=cos(r),s=sin(r);return vec2(p.x*c+p.y*s,p.y*c-p.x*s);}\n\nfloat bM(vec2 c,float t){\n vec2 l=smoothstep(vec2(0.),vec2(t),c),u=smoothstep(vec2(0.),vec2(t),1.-c);\n return l.x*l.y*u.x*u.y;\n}\n\nfloat mG(float hi,float lo,float t,float sh,float cv){\n sh*=(2.-u_sharp);\n float ci=smoothstep(.15,.85,cv),r=lo;\n float e1=.08/u_scale;\n r=mix(r,hi,smoothstep(0.,sh*1.5,t));\n r=mix(r,lo,smoothstep(e1-sh,e1+sh,t));\n float e2=e1+.05/u_scale*(1.-ci*.35);\n r=mix(r,hi,smoothstep(e2-sh,e2+sh,t));\n float e3=e2+.025/u_scale*(1.-ci*.45);\n r=mix(r,lo,smoothstep(e3-sh,e3+sh,t));\n float e4=e1+.1/u_scale;\n r=mix(r,hi,smoothstep(e4-sh,e4+sh,t));\n float rm=1.-e4,gT=clamp((t-e4)/rm,0.,1.);\n r=mix(r,mix(hi,lo,smoothstep(0.,1.,gT)),smoothstep(e4-sh*.5,e4+sh*.5,t));\n return r;\n}\n\nvoid main(){\n sC=fract(vec3(.7548,.5698,.4154)*(u_seed+17.31))+.5;\n sM=fract(sC.zxy-sC.yzx*1.618);\n vec2 sc=vec2(vP.x*u_ratio,1.-vP.y);\n float angleRad=u_angle*3.14159/180.;\n sc=rot(sc-.5,angleRad)+.5;\n sc=clamp(sc,0.,1.);\n float sl=sc.x-sc.y,an=u_time*.001;\n vec2 iC=fA();\n vec4 texSample=texture(u_tex,iC);\n float dp=texSample.r;\n float shapeMask=texSample.a;\n vec3 hi=u_lightColor*u_bright;\n vec3 lo=u_darkColor*(2.-u_bright);\n lo.b+=smoothstep(.6,1.4,sc.x+sc.y)*.08;\n vec2 fC=sc-.5;\n float rd=length(fC+vec2(0.,sl*.15));\n vec2 ag=rot(fC,(.22-sl*.18)*3.14159);\n float cv=1.-pow(rd*1.65,1.15);\n cv*=pow(sc.y,.35);\n float vs=shapeMask;\n vs*=bM(iC,.01);\n float fr=pow(1.-cv,u_fresnel)*.3;\n vs=min(vs+fr*vs,1.);\n float mT=an*.0625;\n vec3 wO=vec3(-1.05,1.35,1.55);\n vec3 wA=aF(vec3(31.,73.,56.),mT+wO)*.22*u_wave;\n vec3 wB=aF(vec3(24.,64.,42.),mT-wO.yzx)*.22*u_wave;\n vec2 nC=sc*45.*u_noise;\n nC+=aF(sC.zxy,an*.17*sC.yzx-sc.yxy*.35).xy*18.*u_wave;\n vec3 tC=vec3(.00041,.00053,.00076)*mT+wB*nC.x+wA*nC.y;\n tC=lM(sC,tC);\n tC=lM(sC+1.618,tC);\n float tb=sin(tC.x*3.14159)*.5+.5;\n tb=tb*2.-1.;\n float noiseVal=pW(vec3(sc*8.+an,an*.5)).x;\n float edgeFactor=smoothstep(0.,.5,dp)*smoothstep(1.,.5,dp);\n float lD=dp+(1.-dp)*u_liquid*tb;\n lD+=noiseVal*u_distort*.15*edgeFactor;\n float rB=clamp(1.-cv,0.,1.);\n float fl=ag.x+sl;\n fl+=noiseVal*sl*u_distort*edgeFactor;\n fl*=mix(1.,1.-dp*.5,u_contour);\n fl-=dp*u_contour*.8;\n float eI=smoothstep(0.,1.,lD)*smoothstep(1.,0.,lD);\n fl-=tb*sl*1.8*eI;\n float cA=cv*clamp(pow(sc.y,.12),.25,1.);\n fl*=.12+(1.05-lD)*cA;\n fl*=smoothstep(1.,.65,lD);\n float vA1=smoothstep(.08,.18,sc.y)*smoothstep(.38,.18,sc.y);\n float vA2=smoothstep(.08,.18,1.-sc.y)*smoothstep(.38,.18,1.-sc.y);\n fl+=vA1*.16+vA2*.025;\n fl*=.45+pow(sc.y,2.)*.55;\n fl*=u_scale;\n fl-=an;\n float rO=rB+cv*tb*.025;\n float vM1=smoothstep(-.12,.18,sc.y)*smoothstep(.48,.08,sc.y);\n float cM1=smoothstep(.35,.55,cv)*smoothstep(.95,.35,cv);\n rO+=vM1*cM1*4.5;\n rO-=sl;\n float bO=rB*1.25;\n float vM2=smoothstep(-.02,.35,sc.y)*smoothstep(.75,.08,sc.y);\n float cM2=smoothstep(.35,.55,cv)*smoothstep(.75,.35,cv);\n bO+=vM2*cM2*.9;\n bO-=lD*.18;\n rO*=u_refract*u_chroma;\n bO*=u_refract*u_chroma;\n float sf=u_blur;\n float rP=fract(fl+rO);\n float rC=mG(hi.r,lo.r,rP,sf+.018+u_refract*cv*.025,cv);\n float gP=fract(fl);\n float gC=mG(hi.g,lo.g,gP,sf+.008/max(.01,1.-sl),cv);\n float bP=fract(fl-bO);\n float bC=mG(hi.b,lo.b,bP,sf+.008,cv);\n vec3 col=vec3(rC,gC,bC);\n col=(col-.5)*u_contrast+.5;\n col=clamp(col,0.,1.);\n col=mix(col,1.-min(vec3(1.),(1.-col)/max(u_tint,vec3(.001))),length(u_tint-1.)*.5);\n col=clamp(col,0.,1.);\n oC=vec4(col*vs,vs);\n}`;\n\ninterface MetallicPaintProps {\n imageSrc: string;\n seed?: number;\n scale?: number;\n refraction?: number;\n blur?: number;\n liquid?: number;\n speed?: number;\n brightness?: number;\n contrast?: number;\n angle?: number;\n fresnel?: number;\n lightColor?: string;\n darkColor?: string;\n patternSharpness?: number;\n waveAmplitude?: number;\n noiseScale?: number;\n chromaticSpread?: number;\n mouseAnimation?: boolean;\n distortion?: number;\n contour?: number;\n tintColor?: string;\n}\n\nfunction processImage(img: HTMLImageElement): ImageData {\n const MAX_SIZE = 1000;\n const MIN_SIZE = 500;\n let width = img.naturalWidth || img.width;\n let height = img.naturalHeight || img.height;\n\n if (width > MAX_SIZE || height > MAX_SIZE || width < MIN_SIZE || height < MIN_SIZE) {\n const scale =\n width > height\n ? width > MAX_SIZE\n ? MAX_SIZE / width\n : width < MIN_SIZE\n ? MIN_SIZE / width\n : 1\n : height > MAX_SIZE\n ? MAX_SIZE / height\n : height < MIN_SIZE\n ? MIN_SIZE / height\n : 1;\n width = Math.round(width * scale);\n height = Math.round(height * scale);\n }\n\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n const ctx = canvas.getContext('2d')!;\n ctx.drawImage(img, 0, 0, width, height);\n\n const imageData = ctx.getImageData(0, 0, width, height);\n const data = imageData.data;\n const size = width * height;\n const alphaValues = new Float32Array(size);\n const shapeMask = new Uint8Array(size);\n const boundaryMask = new Uint8Array(size);\n\n for (let i = 0; i < size; i++) {\n const idx = i * 4;\n const r = data[idx],\n g = data[idx + 1],\n b = data[idx + 2],\n a = data[idx + 3];\n const isBackground = (r > 250 && g > 250 && b > 250 && a === 255) || a < 5;\n alphaValues[i] = isBackground ? 0 : a / 255;\n shapeMask[i] = alphaValues[i] > 0.1 ? 1 : 0;\n }\n\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const idx = y * width + x;\n if (!shapeMask[idx]) continue;\n if (\n x === 0 ||\n x === width - 1 ||\n y === 0 ||\n y === height - 1 ||\n !shapeMask[idx - 1] ||\n !shapeMask[idx + 1] ||\n !shapeMask[idx - width] ||\n !shapeMask[idx + width]\n ) {\n boundaryMask[idx] = 1;\n }\n }\n }\n\n const u = new Float32Array(size);\n const ITERATIONS = 200;\n const C = 0.01;\n const omega = 1.85;\n\n for (let iter = 0; iter < ITERATIONS; iter++) {\n for (let y = 1; y < height - 1; y++) {\n for (let x = 1; x < width - 1; x++) {\n const idx = y * width + x;\n if (!shapeMask[idx] || boundaryMask[idx]) continue;\n const sum =\n (shapeMask[idx + 1] ? u[idx + 1] : 0) +\n (shapeMask[idx - 1] ? u[idx - 1] : 0) +\n (shapeMask[idx + width] ? u[idx + width] : 0) +\n (shapeMask[idx - width] ? u[idx - width] : 0);\n const newVal = (C + sum) / 4;\n u[idx] = omega * newVal + (1 - omega) * u[idx];\n }\n }\n }\n\n let maxVal = 0;\n for (let i = 0; i < size; i++) if (u[i] > maxVal) maxVal = u[i];\n if (maxVal === 0) maxVal = 1;\n\n const outData = ctx.createImageData(width, height);\n for (let i = 0; i < size; i++) {\n const px = i * 4;\n const depth = u[i] / maxVal;\n const gray = Math.round(255 * (1 - depth * depth));\n outData.data[px] = outData.data[px + 1] = outData.data[px + 2] = gray;\n outData.data[px + 3] = Math.round(alphaValues[i] * 255);\n }\n\n return outData;\n}\n\nfunction hexToRgb(hex: string): [number, number, number] {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n return result\n ? [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255]\n : [1, 1, 1];\n}\n\nexport default function MetallicPaint({\n imageSrc,\n seed = 42,\n scale = 4,\n refraction = 0.01,\n blur = 0.015,\n liquid = 0.75,\n speed = 0.3,\n brightness = 2,\n contrast = 0.5,\n angle = 0,\n fresnel = 1,\n lightColor = '#ffffff',\n darkColor = '#000000',\n patternSharpness = 1,\n waveAmplitude = 1,\n noiseScale = 0.5,\n chromaticSpread = 2,\n mouseAnimation = false,\n distortion = 1,\n contour = 0.2,\n tintColor = '#feb3ff'\n}: MetallicPaintProps) {\n const canvasRef = useRef(null);\n const glRef = useRef(null);\n const programRef = useRef(null);\n const uniformsRef = useRef>({});\n const textureRef = useRef(null);\n const animTimeRef = useRef(0);\n const lastTimeRef = useRef(0);\n const rafRef = useRef(null);\n const imgDataRef = useRef(null);\n const speedRef = useRef(speed);\n const mouseRef = useRef({ x: 0.5, y: 0.5, targetX: 0.5, targetY: 0.5 });\n const mouseAnimRef = useRef(mouseAnimation);\n\n const [ready, setReady] = useState(false);\n const [textureReady, setTextureReady] = useState(false);\n\n useEffect(() => {\n speedRef.current = speed;\n }, [speed]);\n useEffect(() => {\n mouseAnimRef.current = mouseAnimation;\n }, [mouseAnimation]);\n\n const initGL = useCallback(() => {\n const canvas = canvasRef.current;\n if (!canvas) return false;\n\n const gl = canvas.getContext('webgl2', { antialias: true, alpha: true });\n if (!gl) return false;\n\n const compile = (src: string, type: number): WebGLShader | null => {\n const s = gl.createShader(type);\n if (!s) return null;\n gl.shaderSource(s, src);\n gl.compileShader(s);\n if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {\n console.error(gl.getShaderInfoLog(s));\n return null;\n }\n return s;\n };\n\n const vs = compile(vertexShader, gl.VERTEX_SHADER);\n const fs = compile(fragmentShader, gl.FRAGMENT_SHADER);\n if (!vs || !fs) return false;\n\n const prog = gl.createProgram();\n if (!prog) return false;\n gl.attachShader(prog, vs);\n gl.attachShader(prog, fs);\n gl.linkProgram(prog);\n if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {\n console.error(gl.getProgramInfoLog(prog));\n return false;\n }\n\n const uniforms: Record = {};\n const count = gl.getProgramParameter(prog, gl.ACTIVE_UNIFORMS);\n for (let i = 0; i < count; i++) {\n const info = gl.getActiveUniform(prog, i);\n if (info) uniforms[info.name] = gl.getUniformLocation(prog, info.name);\n }\n\n const verts = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);\n const buf = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW);\n\n gl.useProgram(prog);\n const pos = gl.getAttribLocation(prog, 'a_position');\n gl.enableVertexAttribArray(pos);\n gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);\n\n glRef.current = gl;\n programRef.current = prog;\n uniformsRef.current = uniforms;\n\n return true;\n }, []);\n\n const uploadTexture = useCallback((imgData: ImageData) => {\n const gl = glRef.current;\n const uniforms = uniformsRef.current;\n if (!gl || !imgData) return;\n\n if (textureRef.current) gl.deleteTexture(textureRef.current);\n\n const tex = gl.createTexture();\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, tex);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, imgData.width, imgData.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, imgData.data);\n gl.uniform1i(uniforms.u_tex, 0);\n\n const ratio = imgData.width / imgData.height;\n gl.uniform1f(uniforms.u_imgRatio, ratio);\n gl.uniform1f(uniforms.u_ratio, 1);\n\n textureRef.current = tex;\n imgDataRef.current = imgData;\n }, []);\n\n useEffect(() => {\n if (!initGL()) return;\n\n const canvas = canvasRef.current;\n const gl = glRef.current;\n if (!canvas || !gl) return;\n\n const side = 1000 * devicePixelRatio;\n canvas.width = side;\n canvas.height = side;\n gl.viewport(0, 0, side, side);\n\n setReady(true);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n if (textureRef.current && glRef.current) {\n glRef.current.deleteTexture(textureRef.current);\n }\n };\n }, [initGL]);\n\n useEffect(() => {\n if (!ready || !imageSrc) return;\n\n setTextureReady(false);\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => {\n const imgData = processImage(img);\n uploadTexture(imgData);\n setTextureReady(true);\n };\n img.src = imageSrc;\n }, [ready, imageSrc, uploadTexture]);\n\n useEffect(() => {\n const gl = glRef.current;\n const u = uniformsRef.current;\n if (!gl || !ready) return;\n\n gl.uniform1f(u.u_seed, seed);\n gl.uniform1f(u.u_scale, scale);\n gl.uniform1f(u.u_refract, refraction);\n gl.uniform1f(u.u_blur, blur);\n gl.uniform1f(u.u_liquid, liquid);\n gl.uniform1f(u.u_bright, brightness);\n gl.uniform1f(u.u_contrast, contrast);\n gl.uniform1f(u.u_angle, angle);\n gl.uniform1f(u.u_fresnel, fresnel);\n\n const light = hexToRgb(lightColor);\n const dark = hexToRgb(darkColor);\n const tint = hexToRgb(tintColor);\n gl.uniform3f(u.u_lightColor, light[0], light[1], light[2]);\n gl.uniform3f(u.u_darkColor, dark[0], dark[1], dark[2]);\n gl.uniform1f(u.u_sharp, patternSharpness);\n gl.uniform1f(u.u_wave, waveAmplitude);\n gl.uniform1f(u.u_noise, noiseScale);\n gl.uniform1f(u.u_chroma, chromaticSpread);\n gl.uniform1f(u.u_distort, distortion);\n gl.uniform1f(u.u_contour, contour);\n gl.uniform3f(u.u_tint, tint[0], tint[1], tint[2]);\n }, [\n ready,\n seed,\n scale,\n refraction,\n blur,\n liquid,\n brightness,\n contrast,\n angle,\n fresnel,\n lightColor,\n darkColor,\n patternSharpness,\n waveAmplitude,\n noiseScale,\n chromaticSpread,\n distortion,\n contour,\n tintColor\n ]);\n\n useEffect(() => {\n if (!ready || !textureReady) return;\n\n const gl = glRef.current;\n const u = uniformsRef.current;\n const canvas = canvasRef.current;\n const mouse = mouseRef.current;\n if (!gl || !canvas) return;\n\n const handleMouseMove = (e: MouseEvent) => {\n const rect = canvas.getBoundingClientRect();\n mouse.targetX = (e.clientX - rect.left) / rect.width;\n mouse.targetY = (e.clientY - rect.top) / rect.height;\n };\n\n canvas.addEventListener('mousemove', handleMouseMove);\n\n const render = (time: number) => {\n const delta = time - lastTimeRef.current;\n lastTimeRef.current = time;\n\n if (mouseAnimRef.current) {\n mouse.x += (mouse.targetX - mouse.x) * 0.08;\n mouse.y += (mouse.targetY - mouse.y) * 0.08;\n animTimeRef.current = mouse.x * 3000 + mouse.y * 1500;\n } else {\n animTimeRef.current += delta * speedRef.current;\n }\n\n gl.uniform1f(u.u_time, animTimeRef.current);\n gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n rafRef.current = requestAnimationFrame(render);\n };\n\n lastTimeRef.current = performance.now();\n rafRef.current = requestAnimationFrame(render);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n canvas.removeEventListener('mousemove', handleMouseMove);\n };\n }, [ready, textureReady]);\n\n return ;\n}\n" } ], diff --git a/public/r/ModelViewer-TS-CSS.json b/public/r/ModelViewer-TS-CSS.json index 29ac1db8..af72fcb7 100644 --- a/public/r/ModelViewer-TS-CSS.json +++ b/public/r/ModelViewer-TS-CSS.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "ModelViewer/ModelViewer.tsx", - "content": "import { FC, Suspense, useRef, useLayoutEffect, useEffect, useMemo } from 'react';\nimport { Canvas, useFrame, useLoader, useThree, invalidate } from '@react-three/fiber';\nimport { OrbitControls, useGLTF, useFBX, useProgress, Html, Environment, ContactShadows } from '@react-three/drei';\nimport { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';\nimport * as THREE from 'three';\n\nconst isMeshObject = (object: THREE.Object3D): object is THREE.Mesh => {\n return 'isMesh' in object && object.isMesh === true;\n};\n\nconst isLightObject = (object: THREE.Object3D): object is THREE.Light => {\n return 'isLight' in object && object.isLight === true;\n};\n\nexport interface ViewerProps {\n url: string;\n width?: number | string;\n height?: number | string;\n modelXOffset?: number;\n modelYOffset?: number;\n defaultRotationX?: number;\n defaultRotationY?: number;\n defaultZoom?: number;\n minZoomDistance?: number;\n maxZoomDistance?: number;\n enableMouseParallax?: boolean;\n enableManualRotation?: boolean;\n enableHoverRotation?: boolean;\n enableManualZoom?: boolean;\n ambientIntensity?: number;\n keyLightIntensity?: number;\n fillLightIntensity?: number;\n rimLightIntensity?: number;\n environmentPreset?: 'city' | 'sunset' | 'night' | 'dawn' | 'studio' | 'apartment' | 'forest' | 'park' | 'none';\n autoFrame?: boolean;\n placeholderSrc?: string;\n showScreenshotButton?: boolean;\n fadeIn?: boolean;\n autoRotate?: boolean;\n autoRotateSpeed?: number;\n onModelLoaded?: () => void;\n}\n\nconst isTouch = typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0);\nconst deg2rad = (d: number) => (d * Math.PI) / 180;\nconst DECIDE = 8; // px before we decide horizontal vs vertical\nconst ROTATE_SPEED = 0.005;\nconst INERTIA = 0.925;\nconst PARALLAX_MAG = 0.05;\nconst PARALLAX_EASE = 0.12;\nconst HOVER_MAG = deg2rad(6);\nconst HOVER_EASE = 0.15;\n\nconst Loader: FC<{ placeholderSrc?: string }> = ({ placeholderSrc }) => {\n const { progress, active } = useProgress();\n if (!active && placeholderSrc) return null;\n return (\n \n {placeholderSrc ? (\n \n ) : (\n `${Math.round(progress)} %`\n )}\n \n );\n};\n\nconst DesktopControls: FC<{\n pivot: THREE.Vector3;\n min: number;\n max: number;\n zoomEnabled: boolean;\n}> = ({ pivot, min, max, zoomEnabled }) => {\n const ref = useRef(null);\n useFrame(() => ref.current?.target.copy(pivot));\n return (\n \n );\n};\n\ninterface ModelInnerProps {\n url: string;\n xOff: number;\n yOff: number;\n pivot: THREE.Vector3;\n initYaw: number;\n initPitch: number;\n minZoom: number;\n maxZoom: number;\n enableMouseParallax: boolean;\n enableManualRotation: boolean;\n enableHoverRotation: boolean;\n enableManualZoom: boolean;\n autoFrame: boolean;\n fadeIn: boolean;\n autoRotate: boolean;\n autoRotateSpeed: number;\n onLoaded?: () => void;\n}\n\nconst ModelInner: FC = ({\n url,\n xOff,\n yOff,\n pivot,\n initYaw,\n initPitch,\n minZoom,\n maxZoom,\n enableMouseParallax,\n enableManualRotation,\n enableHoverRotation,\n enableManualZoom,\n autoFrame,\n fadeIn,\n autoRotate,\n autoRotateSpeed,\n onLoaded\n}) => {\n const outer = useRef(null!);\n const inner = useRef(null!);\n const { camera, gl } = useThree();\n\n const vel = useRef({ x: 0, y: 0 });\n const tPar = useRef({ x: 0, y: 0 });\n const cPar = useRef({ x: 0, y: 0 });\n const tHov = useRef({ x: 0, y: 0 });\n const cHov = useRef({ x: 0, y: 0 });\n\n const ext = useMemo(() => url.split('.').pop()!.toLowerCase(), [url]);\n const content = useMemo(() => {\n if (ext === 'glb' || ext === 'gltf') return useGLTF(url).scene.clone();\n if (ext === 'fbx') return useFBX(url).clone();\n if (ext === 'obj') return useLoader(OBJLoader, url).clone();\n console.error('Unsupported format:', ext);\n return null;\n }, [url, ext]);\n\n const pivotW = useRef(new THREE.Vector3());\n useLayoutEffect(() => {\n if (!content) return;\n const g = inner.current;\n g.updateWorldMatrix(true, true);\n\n const sphere = new THREE.Box3().setFromObject(g).getBoundingSphere(new THREE.Sphere());\n const s = 1 / (sphere.radius * 2);\n g.position.set(-sphere.center.x, -sphere.center.y, -sphere.center.z);\n g.scale.setScalar(s);\n\n g.traverse((o: THREE.Object3D) => {\n if (isMeshObject(o)) {\n o.castShadow = true;\n o.receiveShadow = true;\n if (fadeIn) {\n const materials = Array.isArray(o.material) ? o.material : [o.material];\n materials.forEach(material => {\n material.transparent = true;\n material.opacity = 0;\n });\n }\n }\n });\n\n g.getWorldPosition(pivotW.current);\n pivot.copy(pivotW.current);\n outer.current.rotation.set(initPitch, initYaw, 0);\n\n if (autoFrame && (camera as THREE.PerspectiveCamera).isPerspectiveCamera) {\n const persp = camera as THREE.PerspectiveCamera;\n const fitR = sphere.radius * s;\n const d = (fitR * 1.2) / Math.sin((persp.fov * Math.PI) / 180 / 2);\n persp.position.set(pivotW.current.x, pivotW.current.y, pivotW.current.z + d);\n persp.near = d / 10;\n persp.far = d * 10;\n persp.updateProjectionMatrix();\n }\n\n /* optional fade-in */\n if (fadeIn) {\n let t = 0;\n const id = setInterval(() => {\n t += 0.05;\n const v = Math.min(t, 1);\n g.traverse((o: THREE.Object3D) => {\n if (isMeshObject(o)) {\n const materials = Array.isArray(o.material) ? o.material : [o.material];\n materials.forEach(material => {\n material.opacity = v;\n });\n }\n });\n invalidate();\n if (v === 1) {\n clearInterval(id);\n onLoaded?.();\n }\n }, 16);\n return () => clearInterval(id);\n } else onLoaded?.();\n }, [content]);\n\n useEffect(() => {\n if (!enableManualRotation || isTouch) return;\n const el = gl.domElement;\n let drag = false;\n let lx = 0,\n ly = 0;\n const down = (e: PointerEvent) => {\n if (e.pointerType !== 'mouse' && e.pointerType !== 'pen') return;\n drag = true;\n lx = e.clientX;\n ly = e.clientY;\n window.addEventListener('pointerup', up);\n };\n const move = (e: PointerEvent) => {\n if (!drag) return;\n const dx = e.clientX - lx;\n const dy = e.clientY - ly;\n lx = e.clientX;\n ly = e.clientY;\n outer.current.rotation.y += dx * ROTATE_SPEED;\n outer.current.rotation.x += dy * ROTATE_SPEED;\n vel.current = { x: dx * ROTATE_SPEED, y: dy * ROTATE_SPEED };\n invalidate();\n };\n const up = () => (drag = false);\n el.addEventListener('pointerdown', down);\n el.addEventListener('pointermove', move);\n return () => {\n el.removeEventListener('pointerdown', down);\n el.removeEventListener('pointermove', move);\n window.removeEventListener('pointerup', up);\n };\n }, [gl, enableManualRotation]);\n\n useEffect(() => {\n if (!isTouch) return;\n const el = gl.domElement;\n const pts = new Map();\n type Mode = 'idle' | 'decide' | 'rotate' | 'pinch';\n let mode: Mode = 'idle';\n let sx = 0,\n sy = 0,\n lx = 0,\n ly = 0,\n startDist = 0,\n startZ = 0;\n\n const down = (e: PointerEvent) => {\n if (e.pointerType !== 'touch') return;\n pts.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (pts.size === 1) {\n mode = 'decide';\n sx = lx = e.clientX;\n sy = ly = e.clientY;\n } else if (pts.size === 2 && enableManualZoom) {\n mode = 'pinch';\n const [p1, p2] = [...pts.values()];\n startDist = Math.hypot(p1.x - p2.x, p1.y - p2.y);\n startZ = camera.position.z;\n e.preventDefault();\n }\n invalidate();\n };\n\n const move = (e: PointerEvent) => {\n const p = pts.get(e.pointerId);\n if (!p) return;\n p.x = e.clientX;\n p.y = e.clientY;\n\n if (mode === 'decide') {\n const dx = e.clientX - sx;\n const dy = e.clientY - sy;\n if (Math.abs(dx) > DECIDE || Math.abs(dy) > DECIDE) {\n if (enableManualRotation && Math.abs(dx) > Math.abs(dy)) {\n mode = 'rotate';\n el.setPointerCapture(e.pointerId);\n } else {\n mode = 'idle';\n pts.clear();\n }\n }\n }\n\n if (mode === 'rotate') {\n e.preventDefault();\n const dx = e.clientX - lx;\n const dy = e.clientY - ly;\n lx = e.clientX;\n ly = e.clientY;\n outer.current.rotation.y += dx * ROTATE_SPEED;\n outer.current.rotation.x += dy * ROTATE_SPEED;\n vel.current = { x: dx * ROTATE_SPEED, y: dy * ROTATE_SPEED };\n invalidate();\n } else if (mode === 'pinch' && pts.size === 2) {\n e.preventDefault();\n const [p1, p2] = [...pts.values()];\n const d = Math.hypot(p1.x - p2.x, p1.y - p2.y);\n const ratio = startDist / d;\n camera.position.z = THREE.MathUtils.clamp(startZ * ratio, minZoom, maxZoom);\n invalidate();\n }\n };\n\n const up = (e: PointerEvent) => {\n pts.delete(e.pointerId);\n if (mode === 'rotate' && pts.size === 0) mode = 'idle';\n if (mode === 'pinch' && pts.size < 2) mode = 'idle';\n };\n\n el.addEventListener('pointerdown', down, { passive: true });\n window.addEventListener('pointermove', move, { passive: false });\n window.addEventListener('pointerup', up, { passive: true });\n window.addEventListener('pointercancel', up, { passive: true });\n return () => {\n el.removeEventListener('pointerdown', down);\n window.removeEventListener('pointermove', move);\n window.removeEventListener('pointerup', up);\n window.removeEventListener('pointercancel', up);\n };\n }, [gl, enableManualRotation, enableManualZoom, minZoom, maxZoom]);\n\n useEffect(() => {\n if (isTouch) return;\n const mm = (e: PointerEvent) => {\n if (e.pointerType !== 'mouse') return;\n const nx = (e.clientX / window.innerWidth) * 2 - 1;\n const ny = (e.clientY / window.innerHeight) * 2 - 1;\n if (enableMouseParallax) tPar.current = { x: -nx * PARALLAX_MAG, y: -ny * PARALLAX_MAG };\n if (enableHoverRotation) tHov.current = { x: ny * HOVER_MAG, y: nx * HOVER_MAG };\n invalidate();\n };\n window.addEventListener('pointermove', mm);\n return () => window.removeEventListener('pointermove', mm);\n }, [enableMouseParallax, enableHoverRotation]);\n\n useFrame((_, dt) => {\n let need = false;\n cPar.current.x += (tPar.current.x - cPar.current.x) * PARALLAX_EASE;\n cPar.current.y += (tPar.current.y - cPar.current.y) * PARALLAX_EASE;\n const phx = cHov.current.x,\n phy = cHov.current.y;\n cHov.current.x += (tHov.current.x - cHov.current.x) * HOVER_EASE;\n cHov.current.y += (tHov.current.y - cHov.current.y) * HOVER_EASE;\n\n const ndc = pivotW.current.clone().project(camera);\n ndc.x += xOff + cPar.current.x;\n ndc.y += yOff + cPar.current.y;\n outer.current.position.copy(ndc.unproject(camera));\n\n outer.current.rotation.x += cHov.current.x - phx;\n outer.current.rotation.y += cHov.current.y - phy;\n\n if (autoRotate) {\n outer.current.rotation.y += autoRotateSpeed * dt;\n need = true;\n }\n\n outer.current.rotation.y += vel.current.x;\n outer.current.rotation.x += vel.current.y;\n vel.current.x *= INERTIA;\n vel.current.y *= INERTIA;\n if (Math.abs(vel.current.x) > 1e-4 || Math.abs(vel.current.y) > 1e-4) need = true;\n\n if (\n Math.abs(cPar.current.x - tPar.current.x) > 1e-4 ||\n Math.abs(cPar.current.y - tPar.current.y) > 1e-4 ||\n Math.abs(cHov.current.x - tHov.current.x) > 1e-4 ||\n Math.abs(cHov.current.y - tHov.current.y) > 1e-4\n )\n need = true;\n\n if (need) invalidate();\n });\n\n if (!content) return null;\n return (\n \n \n \n \n \n );\n};\n\nconst ModelViewer: FC = ({\n url,\n width = 400,\n height = 400,\n modelXOffset = 0,\n modelYOffset = 0,\n defaultRotationX = -50,\n defaultRotationY = 20,\n defaultZoom = 0.5,\n minZoomDistance = 0.5,\n maxZoomDistance = 10,\n enableMouseParallax = true,\n enableManualRotation = true,\n enableHoverRotation = true,\n enableManualZoom = true,\n ambientIntensity = 0.3,\n keyLightIntensity = 1,\n fillLightIntensity = 0.5,\n rimLightIntensity = 0.8,\n environmentPreset = 'forest',\n autoFrame = false,\n placeholderSrc,\n showScreenshotButton = true,\n fadeIn = false,\n autoRotate = false,\n autoRotateSpeed = 0.35,\n onModelLoaded\n}) => {\n useEffect(() => void useGLTF.preload(url), [url]);\n const pivot = useRef(new THREE.Vector3()).current;\n const contactRef = useRef(null);\n const rendererRef = useRef(null);\n const sceneRef = useRef(null);\n const cameraRef = useRef(null);\n\n const initYaw = deg2rad(defaultRotationX);\n const initPitch = deg2rad(defaultRotationY);\n const camZ = Math.min(Math.max(defaultZoom, minZoomDistance), maxZoomDistance);\n\n const capture = () => {\n const g = rendererRef.current,\n s = sceneRef.current,\n c = cameraRef.current;\n if (!g || !s || !c) return;\n g.shadowMap.enabled = false;\n const tmp: { l: THREE.Light; cast: boolean }[] = [];\n s.traverse((o: THREE.Object3D) => {\n if (isLightObject(o)) {\n tmp.push({ l: o, cast: o.castShadow });\n o.castShadow = false;\n }\n });\n if (contactRef.current) contactRef.current.visible = false;\n g.render(s, c);\n const urlPNG = g.domElement.toDataURL('image/png');\n const a = document.createElement('a');\n a.download = 'model.png';\n a.href = urlPNG;\n a.click();\n g.shadowMap.enabled = true;\n tmp.forEach(({ l, cast }) => (l.castShadow = cast));\n if (contactRef.current) contactRef.current.visible = true;\n invalidate();\n };\n\n return (\n \n {showScreenshotButton && (\n \n Take Screenshot\n \n )}\n\n {\n rendererRef.current = gl;\n sceneRef.current = scene;\n cameraRef.current = camera;\n gl.toneMapping = THREE.ACESFilmicToneMapping;\n gl.outputColorSpace = THREE.SRGBColorSpace;\n }}\n camera={{ fov: 50, position: [0, 0, camZ], near: 0.01, far: 100 }}\n style={{ touchAction: 'pan-y pinch-zoom' }}\n >\n {environmentPreset !== 'none' && }\n\n \n \n \n \n\n \n\n }>\n \n \n\n {!isTouch && (\n \n )}\n \n
    \n );\n};\n\nexport default ModelViewer;\n" + "content": "import { type FC, Suspense, useRef, useLayoutEffect, useEffect, useMemo } from 'react';\nimport { Canvas, useFrame, useLoader, useThree, invalidate } from '@react-three/fiber';\nimport { OrbitControls, useGLTF, useFBX, useProgress, Html, Environment, ContactShadows } from '@react-three/drei';\nimport { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';\nimport * as THREE from 'three';\n\nconst isMeshObject = (object: THREE.Object3D): object is THREE.Mesh => {\n return 'isMesh' in object && object.isMesh === true;\n};\n\nconst isLightObject = (object: THREE.Object3D): object is THREE.Light => {\n return 'isLight' in object && object.isLight === true;\n};\n\nexport interface ViewerProps {\n url: string;\n width?: number | string;\n height?: number | string;\n modelXOffset?: number;\n modelYOffset?: number;\n defaultRotationX?: number;\n defaultRotationY?: number;\n defaultZoom?: number;\n minZoomDistance?: number;\n maxZoomDistance?: number;\n enableMouseParallax?: boolean;\n enableManualRotation?: boolean;\n enableHoverRotation?: boolean;\n enableManualZoom?: boolean;\n ambientIntensity?: number;\n keyLightIntensity?: number;\n fillLightIntensity?: number;\n rimLightIntensity?: number;\n environmentPreset?: 'city' | 'sunset' | 'night' | 'dawn' | 'studio' | 'apartment' | 'forest' | 'park' | 'none';\n autoFrame?: boolean;\n placeholderSrc?: string;\n showScreenshotButton?: boolean;\n fadeIn?: boolean;\n autoRotate?: boolean;\n autoRotateSpeed?: number;\n onModelLoaded?: () => void;\n}\n\nconst isTouch = typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0);\nconst deg2rad = (d: number) => (d * Math.PI) / 180;\nconst DECIDE = 8; // px before we decide horizontal vs vertical\nconst ROTATE_SPEED = 0.005;\nconst INERTIA = 0.925;\nconst PARALLAX_MAG = 0.05;\nconst PARALLAX_EASE = 0.12;\nconst HOVER_MAG = deg2rad(6);\nconst HOVER_EASE = 0.15;\n\nconst Loader: FC<{ placeholderSrc?: string }> = ({ placeholderSrc }) => {\n const { progress, active } = useProgress();\n if (!active && placeholderSrc) return null;\n return (\n \n {placeholderSrc ? (\n \n ) : (\n `${Math.round(progress)} %`\n )}\n \n );\n};\n\nconst DesktopControls: FC<{\n pivot: THREE.Vector3;\n min: number;\n max: number;\n zoomEnabled: boolean;\n}> = ({ pivot, min, max, zoomEnabled }) => {\n const ref = useRef(null);\n useFrame(() => ref.current?.target.copy(pivot));\n return (\n \n );\n};\n\ninterface ModelInnerProps {\n url: string;\n xOff: number;\n yOff: number;\n pivot: THREE.Vector3;\n initYaw: number;\n initPitch: number;\n minZoom: number;\n maxZoom: number;\n enableMouseParallax: boolean;\n enableManualRotation: boolean;\n enableHoverRotation: boolean;\n enableManualZoom: boolean;\n autoFrame: boolean;\n fadeIn: boolean;\n autoRotate: boolean;\n autoRotateSpeed: number;\n onLoaded?: () => void;\n}\n\nconst ModelInner: FC = ({\n url,\n xOff,\n yOff,\n pivot,\n initYaw,\n initPitch,\n minZoom,\n maxZoom,\n enableMouseParallax,\n enableManualRotation,\n enableHoverRotation,\n enableManualZoom,\n autoFrame,\n fadeIn,\n autoRotate,\n autoRotateSpeed,\n onLoaded\n}) => {\n const outer = useRef(null!);\n const inner = useRef(null!);\n const { camera, gl } = useThree();\n\n const vel = useRef({ x: 0, y: 0 });\n const tPar = useRef({ x: 0, y: 0 });\n const cPar = useRef({ x: 0, y: 0 });\n const tHov = useRef({ x: 0, y: 0 });\n const cHov = useRef({ x: 0, y: 0 });\n\n const ext = useMemo(() => url.split('.').pop()!.toLowerCase(), [url]);\n const content = useMemo(() => {\n if (ext === 'glb' || ext === 'gltf') return useGLTF(url).scene.clone();\n if (ext === 'fbx') return useFBX(url).clone();\n if (ext === 'obj') return useLoader(OBJLoader, url).clone();\n console.error('Unsupported format:', ext);\n return null;\n }, [url, ext]);\n\n const pivotW = useRef(new THREE.Vector3());\n useLayoutEffect(() => {\n if (!content) return;\n const g = inner.current;\n g.updateWorldMatrix(true, true);\n\n const sphere = new THREE.Box3().setFromObject(g).getBoundingSphere(new THREE.Sphere());\n const s = 1 / (sphere.radius * 2);\n g.position.set(-sphere.center.x, -sphere.center.y, -sphere.center.z);\n g.scale.setScalar(s);\n\n g.traverse((o: THREE.Object3D) => {\n if (isMeshObject(o)) {\n o.castShadow = true;\n o.receiveShadow = true;\n if (fadeIn) {\n const materials = Array.isArray(o.material) ? o.material : [o.material];\n materials.forEach(material => {\n material.transparent = true;\n material.opacity = 0;\n });\n }\n }\n });\n\n g.getWorldPosition(pivotW.current);\n pivot.copy(pivotW.current);\n outer.current.rotation.set(initPitch, initYaw, 0);\n\n if (autoFrame && (camera as THREE.PerspectiveCamera).isPerspectiveCamera) {\n const persp = camera as THREE.PerspectiveCamera;\n const fitR = sphere.radius * s;\n const d = (fitR * 1.2) / Math.sin((persp.fov * Math.PI) / 180 / 2);\n persp.position.set(pivotW.current.x, pivotW.current.y, pivotW.current.z + d);\n persp.near = d / 10;\n persp.far = d * 10;\n persp.updateProjectionMatrix();\n }\n\n /* optional fade-in */\n if (fadeIn) {\n let t = 0;\n const id = setInterval(() => {\n t += 0.05;\n const v = Math.min(t, 1);\n g.traverse((o: THREE.Object3D) => {\n if (isMeshObject(o)) {\n const materials = Array.isArray(o.material) ? o.material : [o.material];\n materials.forEach(material => {\n material.opacity = v;\n });\n }\n });\n invalidate();\n if (v === 1) {\n clearInterval(id);\n onLoaded?.();\n }\n }, 16);\n return () => clearInterval(id);\n } else onLoaded?.();\n }, [content]);\n\n useEffect(() => {\n if (!enableManualRotation || isTouch) return;\n const el = gl.domElement;\n let drag = false;\n let lx = 0,\n ly = 0;\n const down = (e: PointerEvent) => {\n if (e.pointerType !== 'mouse' && e.pointerType !== 'pen') return;\n drag = true;\n lx = e.clientX;\n ly = e.clientY;\n window.addEventListener('pointerup', up);\n };\n const move = (e: PointerEvent) => {\n if (!drag) return;\n const dx = e.clientX - lx;\n const dy = e.clientY - ly;\n lx = e.clientX;\n ly = e.clientY;\n outer.current.rotation.y += dx * ROTATE_SPEED;\n outer.current.rotation.x += dy * ROTATE_SPEED;\n vel.current = { x: dx * ROTATE_SPEED, y: dy * ROTATE_SPEED };\n invalidate();\n };\n const up = () => (drag = false);\n el.addEventListener('pointerdown', down);\n el.addEventListener('pointermove', move);\n return () => {\n el.removeEventListener('pointerdown', down);\n el.removeEventListener('pointermove', move);\n window.removeEventListener('pointerup', up);\n };\n }, [gl, enableManualRotation]);\n\n useEffect(() => {\n if (!isTouch) return;\n const el = gl.domElement;\n const pts = new Map();\n type Mode = 'idle' | 'decide' | 'rotate' | 'pinch';\n let mode: Mode = 'idle';\n let sx = 0,\n sy = 0,\n lx = 0,\n ly = 0,\n startDist = 0,\n startZ = 0;\n\n const down = (e: PointerEvent) => {\n if (e.pointerType !== 'touch') return;\n pts.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (pts.size === 1) {\n mode = 'decide';\n sx = lx = e.clientX;\n sy = ly = e.clientY;\n } else if (pts.size === 2 && enableManualZoom) {\n mode = 'pinch';\n const [p1, p2] = [...pts.values()];\n startDist = Math.hypot(p1.x - p2.x, p1.y - p2.y);\n startZ = camera.position.z;\n e.preventDefault();\n }\n invalidate();\n };\n\n const move = (e: PointerEvent) => {\n const p = pts.get(e.pointerId);\n if (!p) return;\n p.x = e.clientX;\n p.y = e.clientY;\n\n if (mode === 'decide') {\n const dx = e.clientX - sx;\n const dy = e.clientY - sy;\n if (Math.abs(dx) > DECIDE || Math.abs(dy) > DECIDE) {\n if (enableManualRotation && Math.abs(dx) > Math.abs(dy)) {\n mode = 'rotate';\n el.setPointerCapture(e.pointerId);\n } else {\n mode = 'idle';\n pts.clear();\n }\n }\n }\n\n if (mode === 'rotate') {\n e.preventDefault();\n const dx = e.clientX - lx;\n const dy = e.clientY - ly;\n lx = e.clientX;\n ly = e.clientY;\n outer.current.rotation.y += dx * ROTATE_SPEED;\n outer.current.rotation.x += dy * ROTATE_SPEED;\n vel.current = { x: dx * ROTATE_SPEED, y: dy * ROTATE_SPEED };\n invalidate();\n } else if (mode === 'pinch' && pts.size === 2) {\n e.preventDefault();\n const [p1, p2] = [...pts.values()];\n const d = Math.hypot(p1.x - p2.x, p1.y - p2.y);\n const ratio = startDist / d;\n camera.position.z = THREE.MathUtils.clamp(startZ * ratio, minZoom, maxZoom);\n invalidate();\n }\n };\n\n const up = (e: PointerEvent) => {\n pts.delete(e.pointerId);\n if (mode === 'rotate' && pts.size === 0) mode = 'idle';\n if (mode === 'pinch' && pts.size < 2) mode = 'idle';\n };\n\n el.addEventListener('pointerdown', down, { passive: true });\n window.addEventListener('pointermove', move, { passive: false });\n window.addEventListener('pointerup', up, { passive: true });\n window.addEventListener('pointercancel', up, { passive: true });\n return () => {\n el.removeEventListener('pointerdown', down);\n window.removeEventListener('pointermove', move);\n window.removeEventListener('pointerup', up);\n window.removeEventListener('pointercancel', up);\n };\n }, [gl, enableManualRotation, enableManualZoom, minZoom, maxZoom]);\n\n useEffect(() => {\n if (isTouch) return;\n const mm = (e: PointerEvent) => {\n if (e.pointerType !== 'mouse') return;\n const nx = (e.clientX / window.innerWidth) * 2 - 1;\n const ny = (e.clientY / window.innerHeight) * 2 - 1;\n if (enableMouseParallax) tPar.current = { x: -nx * PARALLAX_MAG, y: -ny * PARALLAX_MAG };\n if (enableHoverRotation) tHov.current = { x: ny * HOVER_MAG, y: nx * HOVER_MAG };\n invalidate();\n };\n window.addEventListener('pointermove', mm);\n return () => window.removeEventListener('pointermove', mm);\n }, [enableMouseParallax, enableHoverRotation]);\n\n useFrame((_, dt) => {\n let need = false;\n cPar.current.x += (tPar.current.x - cPar.current.x) * PARALLAX_EASE;\n cPar.current.y += (tPar.current.y - cPar.current.y) * PARALLAX_EASE;\n const phx = cHov.current.x,\n phy = cHov.current.y;\n cHov.current.x += (tHov.current.x - cHov.current.x) * HOVER_EASE;\n cHov.current.y += (tHov.current.y - cHov.current.y) * HOVER_EASE;\n\n const ndc = pivotW.current.clone().project(camera);\n ndc.x += xOff + cPar.current.x;\n ndc.y += yOff + cPar.current.y;\n outer.current.position.copy(ndc.unproject(camera));\n\n outer.current.rotation.x += cHov.current.x - phx;\n outer.current.rotation.y += cHov.current.y - phy;\n\n if (autoRotate) {\n outer.current.rotation.y += autoRotateSpeed * dt;\n need = true;\n }\n\n outer.current.rotation.y += vel.current.x;\n outer.current.rotation.x += vel.current.y;\n vel.current.x *= INERTIA;\n vel.current.y *= INERTIA;\n if (Math.abs(vel.current.x) > 1e-4 || Math.abs(vel.current.y) > 1e-4) need = true;\n\n if (\n Math.abs(cPar.current.x - tPar.current.x) > 1e-4 ||\n Math.abs(cPar.current.y - tPar.current.y) > 1e-4 ||\n Math.abs(cHov.current.x - tHov.current.x) > 1e-4 ||\n Math.abs(cHov.current.y - tHov.current.y) > 1e-4\n )\n need = true;\n\n if (need) invalidate();\n });\n\n if (!content) return null;\n return (\n \n \n \n \n \n );\n};\n\nconst ModelViewer: FC = ({\n url,\n width = 400,\n height = 400,\n modelXOffset = 0,\n modelYOffset = 0,\n defaultRotationX = -50,\n defaultRotationY = 20,\n defaultZoom = 0.5,\n minZoomDistance = 0.5,\n maxZoomDistance = 10,\n enableMouseParallax = true,\n enableManualRotation = true,\n enableHoverRotation = true,\n enableManualZoom = true,\n ambientIntensity = 0.3,\n keyLightIntensity = 1,\n fillLightIntensity = 0.5,\n rimLightIntensity = 0.8,\n environmentPreset = 'forest',\n autoFrame = false,\n placeholderSrc,\n showScreenshotButton = true,\n fadeIn = false,\n autoRotate = false,\n autoRotateSpeed = 0.35,\n onModelLoaded\n}) => {\n useEffect(() => void useGLTF.preload(url), [url]);\n const pivot = useRef(new THREE.Vector3()).current;\n const contactRef = useRef(null);\n const rendererRef = useRef(null);\n const sceneRef = useRef(null);\n const cameraRef = useRef(null);\n\n const initYaw = deg2rad(defaultRotationX);\n const initPitch = deg2rad(defaultRotationY);\n const camZ = Math.min(Math.max(defaultZoom, minZoomDistance), maxZoomDistance);\n\n const capture = () => {\n const g = rendererRef.current,\n s = sceneRef.current,\n c = cameraRef.current;\n if (!g || !s || !c) return;\n g.shadowMap.enabled = false;\n const tmp: { l: THREE.Light; cast: boolean }[] = [];\n s.traverse((o: THREE.Object3D) => {\n if (isLightObject(o)) {\n tmp.push({ l: o, cast: o.castShadow });\n o.castShadow = false;\n }\n });\n if (contactRef.current) contactRef.current.visible = false;\n g.render(s, c);\n const urlPNG = g.domElement.toDataURL('image/png');\n const a = document.createElement('a');\n a.download = 'model.png';\n a.href = urlPNG;\n a.click();\n g.shadowMap.enabled = true;\n tmp.forEach(({ l, cast }) => (l.castShadow = cast));\n if (contactRef.current) contactRef.current.visible = true;\n invalidate();\n };\n\n return (\n \n {showScreenshotButton && (\n \n Take Screenshot\n \n )}\n\n {\n rendererRef.current = gl;\n sceneRef.current = scene;\n cameraRef.current = camera;\n gl.toneMapping = THREE.ACESFilmicToneMapping;\n gl.outputColorSpace = THREE.SRGBColorSpace;\n }}\n camera={{ fov: 50, position: [0, 0, camZ], near: 0.01, far: 100 }}\n style={{ touchAction: 'pan-y pinch-zoom' }}\n >\n {environmentPreset !== 'none' && }\n\n \n \n \n \n\n \n\n }>\n \n \n\n {!isTouch && (\n \n )}\n \n
    \n );\n};\n\nexport default ModelViewer;\n" } ], "registryDependencies": [], diff --git a/public/r/ModelViewer-TS-TW.json b/public/r/ModelViewer-TS-TW.json index c61e54fc..7a2be4d8 100644 --- a/public/r/ModelViewer-TS-TW.json +++ b/public/r/ModelViewer-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "ModelViewer/ModelViewer.tsx", - "content": "import { FC, Suspense, useRef, useLayoutEffect, useEffect, useMemo } from 'react';\nimport { Canvas, useFrame, useLoader, useThree, invalidate } from '@react-three/fiber';\nimport { OrbitControls, useGLTF, useFBX, useProgress, Html, Environment, ContactShadows } from '@react-three/drei';\nimport { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';\nimport * as THREE from 'three';\n\nexport interface ViewerProps {\n url: string;\n width?: number | string;\n height?: number | string;\n modelXOffset?: number;\n modelYOffset?: number;\n defaultRotationX?: number;\n defaultRotationY?: number;\n defaultZoom?: number;\n minZoomDistance?: number;\n maxZoomDistance?: number;\n enableMouseParallax?: boolean;\n enableManualRotation?: boolean;\n enableHoverRotation?: boolean;\n enableManualZoom?: boolean;\n ambientIntensity?: number;\n keyLightIntensity?: number;\n fillLightIntensity?: number;\n rimLightIntensity?: number;\n environmentPreset?: 'city' | 'sunset' | 'night' | 'dawn' | 'studio' | 'apartment' | 'forest' | 'park' | 'none';\n autoFrame?: boolean;\n placeholderSrc?: string;\n showScreenshotButton?: boolean;\n fadeIn?: boolean;\n autoRotate?: boolean;\n autoRotateSpeed?: number;\n onModelLoaded?: () => void;\n}\n\nconst isTouch = typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0);\nconst deg2rad = (d: number) => (d * Math.PI) / 180;\nconst DECIDE = 8; // px before we decide horizontal vs vertical\nconst ROTATE_SPEED = 0.005;\nconst INERTIA = 0.925;\nconst PARALLAX_MAG = 0.05;\nconst PARALLAX_EASE = 0.12;\nconst HOVER_MAG = deg2rad(6);\nconst HOVER_EASE = 0.15;\n\nconst Loader: FC<{ placeholderSrc?: string }> = ({ placeholderSrc }) => {\n const { progress, active } = useProgress();\n if (!active && placeholderSrc) return null;\n return (\n \n {placeholderSrc ? (\n \n ) : (\n `${Math.round(progress)} %`\n )}\n \n );\n};\n\nconst DesktopControls: FC<{\n pivot: THREE.Vector3;\n min: number;\n max: number;\n zoomEnabled: boolean;\n}> = ({ pivot, min, max, zoomEnabled }) => {\n const ref = useRef(null);\n useFrame(() => ref.current?.target.copy(pivot));\n return (\n \n );\n};\n\ninterface ModelInnerProps {\n url: string;\n xOff: number;\n yOff: number;\n pivot: THREE.Vector3;\n initYaw: number;\n initPitch: number;\n minZoom: number;\n maxZoom: number;\n enableMouseParallax: boolean;\n enableManualRotation: boolean;\n enableHoverRotation: boolean;\n enableManualZoom: boolean;\n autoFrame: boolean;\n fadeIn: boolean;\n autoRotate: boolean;\n autoRotateSpeed: number;\n onLoaded?: () => void;\n}\n\nconst ModelInner: FC = ({\n url,\n xOff,\n yOff,\n pivot,\n initYaw,\n initPitch,\n minZoom,\n maxZoom,\n enableMouseParallax,\n enableManualRotation,\n enableHoverRotation,\n enableManualZoom,\n autoFrame,\n fadeIn,\n autoRotate,\n autoRotateSpeed,\n onLoaded\n}) => {\n const outer = useRef(null!);\n const inner = useRef(null!);\n const { camera, gl } = useThree();\n\n const vel = useRef({ x: 0, y: 0 });\n const tPar = useRef({ x: 0, y: 0 });\n const cPar = useRef({ x: 0, y: 0 });\n const tHov = useRef({ x: 0, y: 0 });\n const cHov = useRef({ x: 0, y: 0 });\n\n const ext = useMemo(() => url.split('.').pop()!.toLowerCase(), [url]);\n const content = useMemo(() => {\n if (ext === 'glb' || ext === 'gltf') return useGLTF(url).scene.clone();\n if (ext === 'fbx') return useFBX(url).clone();\n if (ext === 'obj') return useLoader(OBJLoader, url).clone();\n console.error('Unsupported format:', ext);\n return null;\n }, [url, ext]);\n\n const pivotW = useRef(new THREE.Vector3());\n useLayoutEffect(() => {\n if (!content) return;\n const g = inner.current;\n g.updateWorldMatrix(true, true);\n\n const sphere = new THREE.Box3().setFromObject(g).getBoundingSphere(new THREE.Sphere());\n const s = 1 / (sphere.radius * 2);\n g.position.set(-sphere.center.x, -sphere.center.y, -sphere.center.z);\n g.scale.setScalar(s);\n\n g.traverse((o: any) => {\n if (o.isMesh) {\n o.castShadow = true;\n o.receiveShadow = true;\n if (fadeIn) {\n o.material.transparent = true;\n o.material.opacity = 0;\n }\n }\n });\n\n g.getWorldPosition(pivotW.current);\n pivot.copy(pivotW.current);\n outer.current.rotation.set(initPitch, initYaw, 0);\n\n if (autoFrame && (camera as THREE.PerspectiveCamera).isPerspectiveCamera) {\n const persp = camera as THREE.PerspectiveCamera;\n const fitR = sphere.radius * s;\n const d = (fitR * 1.2) / Math.sin((persp.fov * Math.PI) / 180 / 2);\n persp.position.set(pivotW.current.x, pivotW.current.y, pivotW.current.z + d);\n persp.near = d / 10;\n persp.far = d * 10;\n persp.updateProjectionMatrix();\n }\n\n /* optional fade-in */\n if (fadeIn) {\n let t = 0;\n const id = setInterval(() => {\n t += 0.05;\n const v = Math.min(t, 1);\n g.traverse((o: any) => {\n if (o.isMesh) o.material.opacity = v;\n });\n invalidate();\n if (v === 1) {\n clearInterval(id);\n onLoaded?.();\n }\n }, 16);\n return () => clearInterval(id);\n } else onLoaded?.();\n }, [content]);\n\n useEffect(() => {\n if (!enableManualRotation || isTouch) return;\n const el = gl.domElement;\n let drag = false;\n let lx = 0,\n ly = 0;\n const down = (e: PointerEvent) => {\n if (e.pointerType !== 'mouse' && e.pointerType !== 'pen') return;\n drag = true;\n lx = e.clientX;\n ly = e.clientY;\n window.addEventListener('pointerup', up);\n };\n const move = (e: PointerEvent) => {\n if (!drag) return;\n const dx = e.clientX - lx;\n const dy = e.clientY - ly;\n lx = e.clientX;\n ly = e.clientY;\n outer.current.rotation.y += dx * ROTATE_SPEED;\n outer.current.rotation.x += dy * ROTATE_SPEED;\n vel.current = { x: dx * ROTATE_SPEED, y: dy * ROTATE_SPEED };\n invalidate();\n };\n const up = () => (drag = false);\n el.addEventListener('pointerdown', down);\n el.addEventListener('pointermove', move);\n return () => {\n el.removeEventListener('pointerdown', down);\n el.removeEventListener('pointermove', move);\n window.removeEventListener('pointerup', up);\n };\n }, [gl, enableManualRotation]);\n\n useEffect(() => {\n if (!isTouch) return;\n const el = gl.domElement;\n const pts = new Map();\n type Mode = 'idle' | 'decide' | 'rotate' | 'pinch';\n let mode: Mode = 'idle';\n let sx = 0,\n sy = 0,\n lx = 0,\n ly = 0,\n startDist = 0,\n startZ = 0;\n\n const down = (e: PointerEvent) => {\n if (e.pointerType !== 'touch') return;\n pts.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (pts.size === 1) {\n mode = 'decide';\n sx = lx = e.clientX;\n sy = ly = e.clientY;\n } else if (pts.size === 2 && enableManualZoom) {\n mode = 'pinch';\n const [p1, p2] = [...pts.values()];\n startDist = Math.hypot(p1.x - p2.x, p1.y - p2.y);\n startZ = camera.position.z;\n e.preventDefault();\n }\n invalidate();\n };\n\n const move = (e: PointerEvent) => {\n const p = pts.get(e.pointerId);\n if (!p) return;\n p.x = e.clientX;\n p.y = e.clientY;\n\n if (mode === 'decide') {\n const dx = e.clientX - sx;\n const dy = e.clientY - sy;\n if (Math.abs(dx) > DECIDE || Math.abs(dy) > DECIDE) {\n if (enableManualRotation && Math.abs(dx) > Math.abs(dy)) {\n mode = 'rotate';\n el.setPointerCapture(e.pointerId);\n } else {\n mode = 'idle';\n pts.clear();\n }\n }\n }\n\n if (mode === 'rotate') {\n e.preventDefault();\n const dx = e.clientX - lx;\n const dy = e.clientY - ly;\n lx = e.clientX;\n ly = e.clientY;\n outer.current.rotation.y += dx * ROTATE_SPEED;\n outer.current.rotation.x += dy * ROTATE_SPEED;\n vel.current = { x: dx * ROTATE_SPEED, y: dy * ROTATE_SPEED };\n invalidate();\n } else if (mode === 'pinch' && pts.size === 2) {\n e.preventDefault();\n const [p1, p2] = [...pts.values()];\n const d = Math.hypot(p1.x - p2.x, p1.y - p2.y);\n const ratio = startDist / d;\n camera.position.z = THREE.MathUtils.clamp(startZ * ratio, minZoom, maxZoom);\n invalidate();\n }\n };\n\n const up = (e: PointerEvent) => {\n pts.delete(e.pointerId);\n if (mode === 'rotate' && pts.size === 0) mode = 'idle';\n if (mode === 'pinch' && pts.size < 2) mode = 'idle';\n };\n\n el.addEventListener('pointerdown', down, { passive: true });\n window.addEventListener('pointermove', move, { passive: false });\n window.addEventListener('pointerup', up, { passive: true });\n window.addEventListener('pointercancel', up, { passive: true });\n return () => {\n el.removeEventListener('pointerdown', down);\n window.removeEventListener('pointermove', move);\n window.removeEventListener('pointerup', up);\n window.removeEventListener('pointercancel', up);\n };\n }, [gl, enableManualRotation, enableManualZoom, minZoom, maxZoom]);\n\n useEffect(() => {\n if (isTouch) return;\n const mm = (e: PointerEvent) => {\n if (e.pointerType !== 'mouse') return;\n const nx = (e.clientX / window.innerWidth) * 2 - 1;\n const ny = (e.clientY / window.innerHeight) * 2 - 1;\n if (enableMouseParallax) tPar.current = { x: -nx * PARALLAX_MAG, y: -ny * PARALLAX_MAG };\n if (enableHoverRotation) tHov.current = { x: ny * HOVER_MAG, y: nx * HOVER_MAG };\n invalidate();\n };\n window.addEventListener('pointermove', mm);\n return () => window.removeEventListener('pointermove', mm);\n }, [enableMouseParallax, enableHoverRotation]);\n\n useFrame((_, dt) => {\n let need = false;\n cPar.current.x += (tPar.current.x - cPar.current.x) * PARALLAX_EASE;\n cPar.current.y += (tPar.current.y - cPar.current.y) * PARALLAX_EASE;\n const phx = cHov.current.x,\n phy = cHov.current.y;\n cHov.current.x += (tHov.current.x - cHov.current.x) * HOVER_EASE;\n cHov.current.y += (tHov.current.y - cHov.current.y) * HOVER_EASE;\n\n const ndc = pivotW.current.clone().project(camera);\n ndc.x += xOff + cPar.current.x;\n ndc.y += yOff + cPar.current.y;\n outer.current.position.copy(ndc.unproject(camera));\n\n outer.current.rotation.x += cHov.current.x - phx;\n outer.current.rotation.y += cHov.current.y - phy;\n\n if (autoRotate) {\n outer.current.rotation.y += autoRotateSpeed * dt;\n need = true;\n }\n\n outer.current.rotation.y += vel.current.x;\n outer.current.rotation.x += vel.current.y;\n vel.current.x *= INERTIA;\n vel.current.y *= INERTIA;\n if (Math.abs(vel.current.x) > 1e-4 || Math.abs(vel.current.y) > 1e-4) need = true;\n\n if (\n Math.abs(cPar.current.x - tPar.current.x) > 1e-4 ||\n Math.abs(cPar.current.y - tPar.current.y) > 1e-4 ||\n Math.abs(cHov.current.x - tHov.current.x) > 1e-4 ||\n Math.abs(cHov.current.y - tHov.current.y) > 1e-4\n )\n need = true;\n\n if (need) invalidate();\n });\n\n if (!content) return null;\n return (\n \n \n \n \n \n );\n};\n\nconst ModelViewer: FC = ({\n url,\n width = 400,\n height = 400,\n modelXOffset = 0,\n modelYOffset = 0,\n defaultRotationX = -50,\n defaultRotationY = 20,\n defaultZoom = 0.5,\n minZoomDistance = 0.5,\n maxZoomDistance = 10,\n enableMouseParallax = true,\n enableManualRotation = true,\n enableHoverRotation = true,\n enableManualZoom = true,\n ambientIntensity = 0.3,\n keyLightIntensity = 1,\n fillLightIntensity = 0.5,\n rimLightIntensity = 0.8,\n environmentPreset = 'forest',\n autoFrame = false,\n placeholderSrc,\n showScreenshotButton = true,\n fadeIn = false,\n autoRotate = false,\n autoRotateSpeed = 0.35,\n onModelLoaded\n}) => {\n useEffect(() => void useGLTF.preload(url), [url]);\n const pivot = useRef(new THREE.Vector3()).current;\n const contactRef = useRef(null);\n const rendererRef = useRef(null);\n const sceneRef = useRef(null);\n const cameraRef = useRef(null);\n\n const initYaw = deg2rad(defaultRotationX);\n const initPitch = deg2rad(defaultRotationY);\n const camZ = Math.min(Math.max(defaultZoom, minZoomDistance), maxZoomDistance);\n\n const capture = () => {\n const g = rendererRef.current,\n s = sceneRef.current,\n c = cameraRef.current;\n if (!g || !s || !c) return;\n g.shadowMap.enabled = false;\n const tmp: { l: THREE.Light; cast: boolean }[] = [];\n s.traverse((o: any) => {\n if (o.isLight && 'castShadow' in o) {\n tmp.push({ l: o, cast: o.castShadow });\n o.castShadow = false;\n }\n });\n if (contactRef.current) contactRef.current.visible = false;\n g.render(s, c);\n const urlPNG = g.domElement.toDataURL('image/png');\n const a = document.createElement('a');\n a.download = 'model.png';\n a.href = urlPNG;\n a.click();\n g.shadowMap.enabled = true;\n tmp.forEach(({ l, cast }) => (l.castShadow = cast));\n if (contactRef.current) contactRef.current.visible = true;\n invalidate();\n };\n\n return (\n \n {showScreenshotButton && (\n \n Take Screenshot\n \n )}\n\n {\n rendererRef.current = gl;\n sceneRef.current = scene;\n cameraRef.current = camera;\n gl.toneMapping = THREE.ACESFilmicToneMapping;\n gl.outputColorSpace = THREE.SRGBColorSpace;\n }}\n camera={{ fov: 50, position: [0, 0, camZ], near: 0.01, far: 100 }}\n style={{ touchAction: 'pan-y pinch-zoom' }}\n >\n {environmentPreset !== 'none' && }\n\n \n \n \n \n\n \n\n }>\n \n \n\n {!isTouch && (\n \n )}\n \n
    \n );\n};\n\nexport default ModelViewer;\n" + "content": "import { type FC, Suspense, useRef, useLayoutEffect, useEffect, useMemo } from 'react';\nimport { Canvas, useFrame, useLoader, useThree, invalidate } from '@react-three/fiber';\nimport { OrbitControls, useGLTF, useFBX, useProgress, Html, Environment, ContactShadows } from '@react-three/drei';\nimport { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';\nimport * as THREE from 'three';\n\nexport interface ViewerProps {\n url: string;\n width?: number | string;\n height?: number | string;\n modelXOffset?: number;\n modelYOffset?: number;\n defaultRotationX?: number;\n defaultRotationY?: number;\n defaultZoom?: number;\n minZoomDistance?: number;\n maxZoomDistance?: number;\n enableMouseParallax?: boolean;\n enableManualRotation?: boolean;\n enableHoverRotation?: boolean;\n enableManualZoom?: boolean;\n ambientIntensity?: number;\n keyLightIntensity?: number;\n fillLightIntensity?: number;\n rimLightIntensity?: number;\n environmentPreset?: 'city' | 'sunset' | 'night' | 'dawn' | 'studio' | 'apartment' | 'forest' | 'park' | 'none';\n autoFrame?: boolean;\n placeholderSrc?: string;\n showScreenshotButton?: boolean;\n fadeIn?: boolean;\n autoRotate?: boolean;\n autoRotateSpeed?: number;\n onModelLoaded?: () => void;\n}\n\nconst isTouch = typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0);\nconst deg2rad = (d: number) => (d * Math.PI) / 180;\nconst DECIDE = 8; // px before we decide horizontal vs vertical\nconst ROTATE_SPEED = 0.005;\nconst INERTIA = 0.925;\nconst PARALLAX_MAG = 0.05;\nconst PARALLAX_EASE = 0.12;\nconst HOVER_MAG = deg2rad(6);\nconst HOVER_EASE = 0.15;\n\nconst Loader: FC<{ placeholderSrc?: string }> = ({ placeholderSrc }) => {\n const { progress, active } = useProgress();\n if (!active && placeholderSrc) return null;\n return (\n \n {placeholderSrc ? (\n \n ) : (\n `${Math.round(progress)} %`\n )}\n \n );\n};\n\nconst DesktopControls: FC<{\n pivot: THREE.Vector3;\n min: number;\n max: number;\n zoomEnabled: boolean;\n}> = ({ pivot, min, max, zoomEnabled }) => {\n const ref = useRef(null);\n useFrame(() => ref.current?.target.copy(pivot));\n return (\n \n );\n};\n\ninterface ModelInnerProps {\n url: string;\n xOff: number;\n yOff: number;\n pivot: THREE.Vector3;\n initYaw: number;\n initPitch: number;\n minZoom: number;\n maxZoom: number;\n enableMouseParallax: boolean;\n enableManualRotation: boolean;\n enableHoverRotation: boolean;\n enableManualZoom: boolean;\n autoFrame: boolean;\n fadeIn: boolean;\n autoRotate: boolean;\n autoRotateSpeed: number;\n onLoaded?: () => void;\n}\n\nconst ModelInner: FC = ({\n url,\n xOff,\n yOff,\n pivot,\n initYaw,\n initPitch,\n minZoom,\n maxZoom,\n enableMouseParallax,\n enableManualRotation,\n enableHoverRotation,\n enableManualZoom,\n autoFrame,\n fadeIn,\n autoRotate,\n autoRotateSpeed,\n onLoaded\n}) => {\n const outer = useRef(null!);\n const inner = useRef(null!);\n const { camera, gl } = useThree();\n\n const vel = useRef({ x: 0, y: 0 });\n const tPar = useRef({ x: 0, y: 0 });\n const cPar = useRef({ x: 0, y: 0 });\n const tHov = useRef({ x: 0, y: 0 });\n const cHov = useRef({ x: 0, y: 0 });\n\n const ext = useMemo(() => url.split('.').pop()!.toLowerCase(), [url]);\n const content = useMemo(() => {\n if (ext === 'glb' || ext === 'gltf') return useGLTF(url).scene.clone();\n if (ext === 'fbx') return useFBX(url).clone();\n if (ext === 'obj') return useLoader(OBJLoader, url).clone();\n console.error('Unsupported format:', ext);\n return null;\n }, [url, ext]);\n\n const pivotW = useRef(new THREE.Vector3());\n useLayoutEffect(() => {\n if (!content) return;\n const g = inner.current;\n g.updateWorldMatrix(true, true);\n\n const sphere = new THREE.Box3().setFromObject(g).getBoundingSphere(new THREE.Sphere());\n const s = 1 / (sphere.radius * 2);\n g.position.set(-sphere.center.x, -sphere.center.y, -sphere.center.z);\n g.scale.setScalar(s);\n\n g.traverse((o: any) => {\n if (o.isMesh) {\n o.castShadow = true;\n o.receiveShadow = true;\n if (fadeIn) {\n o.material.transparent = true;\n o.material.opacity = 0;\n }\n }\n });\n\n g.getWorldPosition(pivotW.current);\n pivot.copy(pivotW.current);\n outer.current.rotation.set(initPitch, initYaw, 0);\n\n if (autoFrame && (camera as THREE.PerspectiveCamera).isPerspectiveCamera) {\n const persp = camera as THREE.PerspectiveCamera;\n const fitR = sphere.radius * s;\n const d = (fitR * 1.2) / Math.sin((persp.fov * Math.PI) / 180 / 2);\n persp.position.set(pivotW.current.x, pivotW.current.y, pivotW.current.z + d);\n persp.near = d / 10;\n persp.far = d * 10;\n persp.updateProjectionMatrix();\n }\n\n /* optional fade-in */\n if (fadeIn) {\n let t = 0;\n const id = setInterval(() => {\n t += 0.05;\n const v = Math.min(t, 1);\n g.traverse((o: any) => {\n if (o.isMesh) o.material.opacity = v;\n });\n invalidate();\n if (v === 1) {\n clearInterval(id);\n onLoaded?.();\n }\n }, 16);\n return () => clearInterval(id);\n } else onLoaded?.();\n }, [content]);\n\n useEffect(() => {\n if (!enableManualRotation || isTouch) return;\n const el = gl.domElement;\n let drag = false;\n let lx = 0,\n ly = 0;\n const down = (e: PointerEvent) => {\n if (e.pointerType !== 'mouse' && e.pointerType !== 'pen') return;\n drag = true;\n lx = e.clientX;\n ly = e.clientY;\n window.addEventListener('pointerup', up);\n };\n const move = (e: PointerEvent) => {\n if (!drag) return;\n const dx = e.clientX - lx;\n const dy = e.clientY - ly;\n lx = e.clientX;\n ly = e.clientY;\n outer.current.rotation.y += dx * ROTATE_SPEED;\n outer.current.rotation.x += dy * ROTATE_SPEED;\n vel.current = { x: dx * ROTATE_SPEED, y: dy * ROTATE_SPEED };\n invalidate();\n };\n const up = () => (drag = false);\n el.addEventListener('pointerdown', down);\n el.addEventListener('pointermove', move);\n return () => {\n el.removeEventListener('pointerdown', down);\n el.removeEventListener('pointermove', move);\n window.removeEventListener('pointerup', up);\n };\n }, [gl, enableManualRotation]);\n\n useEffect(() => {\n if (!isTouch) return;\n const el = gl.domElement;\n const pts = new Map();\n type Mode = 'idle' | 'decide' | 'rotate' | 'pinch';\n let mode: Mode = 'idle';\n let sx = 0,\n sy = 0,\n lx = 0,\n ly = 0,\n startDist = 0,\n startZ = 0;\n\n const down = (e: PointerEvent) => {\n if (e.pointerType !== 'touch') return;\n pts.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (pts.size === 1) {\n mode = 'decide';\n sx = lx = e.clientX;\n sy = ly = e.clientY;\n } else if (pts.size === 2 && enableManualZoom) {\n mode = 'pinch';\n const [p1, p2] = [...pts.values()];\n startDist = Math.hypot(p1.x - p2.x, p1.y - p2.y);\n startZ = camera.position.z;\n e.preventDefault();\n }\n invalidate();\n };\n\n const move = (e: PointerEvent) => {\n const p = pts.get(e.pointerId);\n if (!p) return;\n p.x = e.clientX;\n p.y = e.clientY;\n\n if (mode === 'decide') {\n const dx = e.clientX - sx;\n const dy = e.clientY - sy;\n if (Math.abs(dx) > DECIDE || Math.abs(dy) > DECIDE) {\n if (enableManualRotation && Math.abs(dx) > Math.abs(dy)) {\n mode = 'rotate';\n el.setPointerCapture(e.pointerId);\n } else {\n mode = 'idle';\n pts.clear();\n }\n }\n }\n\n if (mode === 'rotate') {\n e.preventDefault();\n const dx = e.clientX - lx;\n const dy = e.clientY - ly;\n lx = e.clientX;\n ly = e.clientY;\n outer.current.rotation.y += dx * ROTATE_SPEED;\n outer.current.rotation.x += dy * ROTATE_SPEED;\n vel.current = { x: dx * ROTATE_SPEED, y: dy * ROTATE_SPEED };\n invalidate();\n } else if (mode === 'pinch' && pts.size === 2) {\n e.preventDefault();\n const [p1, p2] = [...pts.values()];\n const d = Math.hypot(p1.x - p2.x, p1.y - p2.y);\n const ratio = startDist / d;\n camera.position.z = THREE.MathUtils.clamp(startZ * ratio, minZoom, maxZoom);\n invalidate();\n }\n };\n\n const up = (e: PointerEvent) => {\n pts.delete(e.pointerId);\n if (mode === 'rotate' && pts.size === 0) mode = 'idle';\n if (mode === 'pinch' && pts.size < 2) mode = 'idle';\n };\n\n el.addEventListener('pointerdown', down, { passive: true });\n window.addEventListener('pointermove', move, { passive: false });\n window.addEventListener('pointerup', up, { passive: true });\n window.addEventListener('pointercancel', up, { passive: true });\n return () => {\n el.removeEventListener('pointerdown', down);\n window.removeEventListener('pointermove', move);\n window.removeEventListener('pointerup', up);\n window.removeEventListener('pointercancel', up);\n };\n }, [gl, enableManualRotation, enableManualZoom, minZoom, maxZoom]);\n\n useEffect(() => {\n if (isTouch) return;\n const mm = (e: PointerEvent) => {\n if (e.pointerType !== 'mouse') return;\n const nx = (e.clientX / window.innerWidth) * 2 - 1;\n const ny = (e.clientY / window.innerHeight) * 2 - 1;\n if (enableMouseParallax) tPar.current = { x: -nx * PARALLAX_MAG, y: -ny * PARALLAX_MAG };\n if (enableHoverRotation) tHov.current = { x: ny * HOVER_MAG, y: nx * HOVER_MAG };\n invalidate();\n };\n window.addEventListener('pointermove', mm);\n return () => window.removeEventListener('pointermove', mm);\n }, [enableMouseParallax, enableHoverRotation]);\n\n useFrame((_, dt) => {\n let need = false;\n cPar.current.x += (tPar.current.x - cPar.current.x) * PARALLAX_EASE;\n cPar.current.y += (tPar.current.y - cPar.current.y) * PARALLAX_EASE;\n const phx = cHov.current.x,\n phy = cHov.current.y;\n cHov.current.x += (tHov.current.x - cHov.current.x) * HOVER_EASE;\n cHov.current.y += (tHov.current.y - cHov.current.y) * HOVER_EASE;\n\n const ndc = pivotW.current.clone().project(camera);\n ndc.x += xOff + cPar.current.x;\n ndc.y += yOff + cPar.current.y;\n outer.current.position.copy(ndc.unproject(camera));\n\n outer.current.rotation.x += cHov.current.x - phx;\n outer.current.rotation.y += cHov.current.y - phy;\n\n if (autoRotate) {\n outer.current.rotation.y += autoRotateSpeed * dt;\n need = true;\n }\n\n outer.current.rotation.y += vel.current.x;\n outer.current.rotation.x += vel.current.y;\n vel.current.x *= INERTIA;\n vel.current.y *= INERTIA;\n if (Math.abs(vel.current.x) > 1e-4 || Math.abs(vel.current.y) > 1e-4) need = true;\n\n if (\n Math.abs(cPar.current.x - tPar.current.x) > 1e-4 ||\n Math.abs(cPar.current.y - tPar.current.y) > 1e-4 ||\n Math.abs(cHov.current.x - tHov.current.x) > 1e-4 ||\n Math.abs(cHov.current.y - tHov.current.y) > 1e-4\n )\n need = true;\n\n if (need) invalidate();\n });\n\n if (!content) return null;\n return (\n \n \n \n \n \n );\n};\n\nconst ModelViewer: FC = ({\n url,\n width = 400,\n height = 400,\n modelXOffset = 0,\n modelYOffset = 0,\n defaultRotationX = -50,\n defaultRotationY = 20,\n defaultZoom = 0.5,\n minZoomDistance = 0.5,\n maxZoomDistance = 10,\n enableMouseParallax = true,\n enableManualRotation = true,\n enableHoverRotation = true,\n enableManualZoom = true,\n ambientIntensity = 0.3,\n keyLightIntensity = 1,\n fillLightIntensity = 0.5,\n rimLightIntensity = 0.8,\n environmentPreset = 'forest',\n autoFrame = false,\n placeholderSrc,\n showScreenshotButton = true,\n fadeIn = false,\n autoRotate = false,\n autoRotateSpeed = 0.35,\n onModelLoaded\n}) => {\n useEffect(() => void useGLTF.preload(url), [url]);\n const pivot = useRef(new THREE.Vector3()).current;\n const contactRef = useRef(null);\n const rendererRef = useRef(null);\n const sceneRef = useRef(null);\n const cameraRef = useRef(null);\n\n const initYaw = deg2rad(defaultRotationX);\n const initPitch = deg2rad(defaultRotationY);\n const camZ = Math.min(Math.max(defaultZoom, minZoomDistance), maxZoomDistance);\n\n const capture = () => {\n const g = rendererRef.current,\n s = sceneRef.current,\n c = cameraRef.current;\n if (!g || !s || !c) return;\n g.shadowMap.enabled = false;\n const tmp: { l: THREE.Light; cast: boolean }[] = [];\n s.traverse((o: any) => {\n if (o.isLight && 'castShadow' in o) {\n tmp.push({ l: o, cast: o.castShadow });\n o.castShadow = false;\n }\n });\n if (contactRef.current) contactRef.current.visible = false;\n g.render(s, c);\n const urlPNG = g.domElement.toDataURL('image/png');\n const a = document.createElement('a');\n a.download = 'model.png';\n a.href = urlPNG;\n a.click();\n g.shadowMap.enabled = true;\n tmp.forEach(({ l, cast }) => (l.castShadow = cast));\n if (contactRef.current) contactRef.current.visible = true;\n invalidate();\n };\n\n return (\n \n {showScreenshotButton && (\n \n Take Screenshot\n \n )}\n\n {\n rendererRef.current = gl;\n sceneRef.current = scene;\n cameraRef.current = camera;\n gl.toneMapping = THREE.ACESFilmicToneMapping;\n gl.outputColorSpace = THREE.SRGBColorSpace;\n }}\n camera={{ fov: 50, position: [0, 0, camZ], near: 0.01, far: 100 }}\n style={{ touchAction: 'pan-y pinch-zoom' }}\n >\n {environmentPreset !== 'none' && }\n\n \n \n \n \n\n \n\n }>\n \n \n\n {!isTouch && (\n \n )}\n \n
    \n );\n};\n\nexport default ModelViewer;\n" } ], "registryDependencies": [], diff --git a/public/r/MoltenMetal-JS-CSS.json b/public/r/MoltenMetal-JS-CSS.json index e65c13fb..b1b2d98a 100644 --- a/public/r/MoltenMetal-JS-CSS.json +++ b/public/r/MoltenMetal-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "MoltenMetal/MoltenMetal.css", + "type": "registry:file", + "path": "MoltenMetal.css", + "target": "@components/MoltenMetal.css", "content": ".molten-metal-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "MoltenMetal/MoltenMetal.jsx", + "path": "MoltenMetal.jsx", "content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './MoltenMetal.css';\n\nconst hexToRgb = hex => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 1, 1];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst colorModeToFloat = mode => (mode === 'ember' ? 1 : mode === 'frost' ? 2 : 0);\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uSpeed;\nuniform float uScale;\nuniform float uDetail;\nuniform float uGlow;\nuniform float uCoreSize;\nuniform float uSwirl;\nuniform float uFold;\nuniform float uBlackPoint;\nuniform float uBrightness;\nuniform float uColorMode;\nuniform float uGrain;\nuniform float uGrainIntensity;\nuniform float uOpacity;\nuniform vec2 uMouse;\nuniform float uMouseStrength;\nuniform bool uEnableMouse;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nout vec4 fragColor;\n\nfloat hash(vec2 p) {\n return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);\n}\n\nvoid main() {\n float time = iTime * uSpeed;\n vec2 p = uScale * ((gl_FragCoord.xy - 0.5 * iResolution.xy) / iResolution.y) - 0.5;\n\n vec2 drift = vec2(0.0);\n if (uEnableMouse) {\n drift = (uMouse - 0.5) * uMouseStrength * 2.0;\n }\n p += drift;\n\n vec2 i = p;\n float c = 0.0;\n float r = length(p + vec2(sin(time), sin(time * 0.3 + 5.0)) * 0.5);\n float d = length(p);\n float rot = d + time + p.x * uSwirl;\n\n float cosRot = cos(rot);\n mat2 warp = mat2(cos(rot - sin(time / 5.0)), sin(rot), -sin(cosRot - time), cosRot) * uFold;\n float glowCore = uGlow * uCoreSize;\n\n for (float n = 0.0; n < 8.0; n++) {\n if (n >= uDetail) break;\n p *= warp;\n float t = r - time / (n + 3.0);\n i -= p + vec2(cos(t - i.x - r) + sin(t + i.y), sin(t - i.y) + cos(t + i.x) + r);\n c += glowCore / length(vec2(sin(i.x + t), cos(i.y + t)));\n }\n\n c /= 6.0;\n\n float intensity = max(c - uBlackPoint, 0.0) * uBrightness;\n\n float g = clamp(intensity, 0.0, 1.0);\n\n float mid = 0.5;\n if (uColorMode > 1.5) {\n mid = 0.65;\n } else if (uColorMode > 0.5) {\n mid = 0.35;\n }\n\n vec3 col = mix(uColor1, uColor2, smoothstep(0.0, mid, g));\n col = mix(col, uColor3, smoothstep(mid, 1.0, g));\n\n float a = g;\n if (uGrain > 0.5) {\n float gr = hash(gl_FragCoord.xy + iTime);\n a += (gr - 0.5) * uGrainIntensity;\n }\n a = clamp(a, 0.0, 1.0) * uOpacity;\n fragColor = vec4(col * a, a);\n}\n`;\n\nconst ctxMap = new WeakMap();\n\nconst MoltenMetal = ({\n color1 = '#5227FF',\n color2 = '#FF9FFC',\n color3 = '#FFFFFF',\n speed = 0.35,\n scale = 4,\n detail = 3,\n glow = 1.6,\n coreSize = 0.1,\n swirl = 1,\n fold = -0.2,\n blackPoint = 0.05,\n brightness = 1.3,\n colorMode = 'molten',\n grain = true,\n grainIntensity = 0.05,\n mouseInteraction = true,\n mouseStrength = 0.3,\n opacity = 1.0,\n className = ''\n}) => {\n const containerRef = useRef(null);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n webgl: 2,\n alpha: true,\n premultipliedAlpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n const canvas = gl.canvas;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uSpeed: { value: 0.35 },\n uScale: { value: 4 },\n uDetail: { value: 3 },\n uGlow: { value: 1.6 },\n uCoreSize: { value: 0.1 },\n uSwirl: { value: 1 },\n uFold: { value: -0.2 },\n uBlackPoint: { value: 0.05 },\n uBrightness: { value: 1.3 },\n uColorMode: { value: 0 },\n uGrain: { value: 1 },\n uGrainIntensity: { value: 0.05 },\n uOpacity: { value: 1.0 },\n uMouse: { value: new Float32Array([0.5, 0.5]) },\n uMouseStrength: { value: 0.3 },\n uEnableMouse: { value: true },\n uColor1: { value: new Float32Array([1, 1, 1]) },\n uColor2: { value: new Float32Array([1, 1, 1]) },\n uColor3: { value: new Float32Array([1, 1, 1]) }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n ctxMap.set(container, { renderer, program, mesh });\n\n const setSize = () => {\n const rect = container.getBoundingClientRect();\n const w = Math.max(1, Math.floor(rect.width));\n const h = Math.max(1, Math.floor(rect.height));\n renderer.setSize(w, h);\n const res = program.uniforms.iResolution.value;\n res[0] = gl.drawingBufferWidth;\n res[1] = gl.drawingBufferHeight;\n renderer.render({ scene: mesh });\n };\n\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n setSize();\n\n const targetMouse = [0.5, 0.5];\n const currentMouse = [0.5, 0.5];\n\n const handleMouseMove = e => {\n const rect = canvas.getBoundingClientRect();\n targetMouse[0] = (e.clientX - rect.left) / rect.width;\n targetMouse[1] = 1.0 - (e.clientY - rect.top) / rect.height;\n };\n const handleMouseLeave = () => {\n targetMouse[0] = 0.5;\n targetMouse[1] = 0.5;\n };\n canvas.addEventListener('mousemove', handleMouseMove);\n canvas.addEventListener('mouseleave', handleMouseLeave);\n\n let raf = 0;\n let isVisible = true;\n let isPageVisible = !document.hidden;\n const t0 = performance.now();\n\n const loop = t => {\n program.uniforms.iTime.value = (t - t0) * 0.001;\n currentMouse[0] += 0.05 * (targetMouse[0] - currentMouse[0]);\n currentMouse[1] += 0.05 * (targetMouse[1] - currentMouse[1]);\n program.uniforms.uMouse.value[0] = currentMouse[0];\n program.uniforms.uMouse.value[1] = currentMouse[1];\n renderer.render({ scene: mesh });\n raf = requestAnimationFrame(loop);\n };\n\n const tryStart = () => {\n if (isVisible && isPageVisible && raf === 0) raf = requestAnimationFrame(loop);\n };\n const tryStop = () => {\n if (raf !== 0) {\n cancelAnimationFrame(raf);\n raf = 0;\n }\n };\n\n const io = new IntersectionObserver(\n ([entry]) => {\n isVisible = entry.isIntersecting;\n isVisible ? tryStart() : tryStop();\n },\n { threshold: 0 }\n );\n io.observe(container);\n\n const onVisibility = () => {\n isPageVisible = !document.hidden;\n isPageVisible ? tryStart() : tryStop();\n };\n document.addEventListener('visibilitychange', onVisibility);\n\n tryStart();\n\n return () => {\n tryStop();\n ro.disconnect();\n io.disconnect();\n document.removeEventListener('visibilitychange', onVisibility);\n canvas.removeEventListener('mousemove', handleMouseMove);\n canvas.removeEventListener('mouseleave', handleMouseLeave);\n ctxMap.delete(container);\n try {\n container.removeChild(canvas);\n } catch {}\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, []);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n const ctx = ctxMap.get(container);\n if (!ctx) return;\n const u = ctx.program.uniforms;\n\n u.uSpeed.value = speed;\n u.uScale.value = scale;\n u.uDetail.value = detail;\n u.uGlow.value = glow;\n u.uCoreSize.value = Math.max(coreSize, 0.001);\n u.uSwirl.value = swirl;\n u.uFold.value = fold;\n u.uBlackPoint.value = blackPoint;\n u.uBrightness.value = brightness;\n u.uColorMode.value = colorModeToFloat(colorMode);\n u.uGrain.value = grain ? 1 : 0;\n u.uGrainIntensity.value = grainIntensity;\n u.uOpacity.value = opacity;\n u.uMouseStrength.value = mouseStrength;\n u.uEnableMouse.value = mouseInteraction;\n const c1 = hexToRgb(color1);\n const c2 = hexToRgb(color2);\n const c3 = hexToRgb(color3);\n const uc1 = u.uColor1.value;\n const uc2 = u.uColor2.value;\n const uc3 = u.uColor3.value;\n uc1[0] = c1[0];\n uc1[1] = c1[1];\n uc1[2] = c1[2];\n uc2[0] = c2[0];\n uc2[1] = c2[1];\n uc2[2] = c2[2];\n uc3[0] = c3[0];\n uc3[1] = c3[1];\n uc3[2] = c3[2];\n }, [\n color1,\n color2,\n color3,\n speed,\n scale,\n detail,\n glow,\n coreSize,\n swirl,\n fold,\n blackPoint,\n brightness,\n colorMode,\n grain,\n grainIntensity,\n mouseInteraction,\n mouseStrength,\n opacity\n ]);\n\n return
    ;\n};\n\nexport default MoltenMetal;\n" } ], diff --git a/public/r/MoltenMetal-TS-CSS.json b/public/r/MoltenMetal-TS-CSS.json index c1f2d148..f6babef4 100644 --- a/public/r/MoltenMetal-TS-CSS.json +++ b/public/r/MoltenMetal-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "MoltenMetal/MoltenMetal.css", + "type": "registry:file", + "path": "MoltenMetal.css", + "target": "@components/MoltenMetal.css", "content": ".molten-metal-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "MoltenMetal/MoltenMetal.tsx", + "path": "MoltenMetal.tsx", "content": "import React, { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './MoltenMetal.css';\n\nexport type MoltenMetalColorMode = 'molten' | 'ember' | 'frost';\n\nexport interface MoltenMetalProps {\n color1?: string;\n color2?: string;\n color3?: string;\n speed?: number;\n scale?: number;\n detail?: number;\n glow?: number;\n coreSize?: number;\n swirl?: number;\n fold?: number;\n blackPoint?: number;\n brightness?: number;\n colorMode?: MoltenMetalColorMode;\n grain?: boolean;\n grainIntensity?: number;\n mouseInteraction?: boolean;\n mouseStrength?: number;\n opacity?: number;\n className?: string;\n}\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 1, 1];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst colorModeToFloat = (mode: MoltenMetalColorMode): number => (mode === 'ember' ? 1 : mode === 'frost' ? 2 : 0);\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uSpeed;\nuniform float uScale;\nuniform float uDetail;\nuniform float uGlow;\nuniform float uCoreSize;\nuniform float uSwirl;\nuniform float uFold;\nuniform float uBlackPoint;\nuniform float uBrightness;\nuniform float uColorMode;\nuniform float uGrain;\nuniform float uGrainIntensity;\nuniform float uOpacity;\nuniform vec2 uMouse;\nuniform float uMouseStrength;\nuniform bool uEnableMouse;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nout vec4 fragColor;\n\nfloat hash(vec2 p) {\n return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);\n}\n\nvoid main() {\n float time = iTime * uSpeed;\n vec2 p = uScale * ((gl_FragCoord.xy - 0.5 * iResolution.xy) / iResolution.y) - 0.5;\n\n vec2 drift = vec2(0.0);\n if (uEnableMouse) {\n drift = (uMouse - 0.5) * uMouseStrength * 2.0;\n }\n p += drift;\n\n vec2 i = p;\n float c = 0.0;\n float r = length(p + vec2(sin(time), sin(time * 0.3 + 5.0)) * 0.5);\n float d = length(p);\n float rot = d + time + p.x * uSwirl;\n\n float cosRot = cos(rot);\n mat2 warp = mat2(cos(rot - sin(time / 5.0)), sin(rot), -sin(cosRot - time), cosRot) * uFold;\n float glowCore = uGlow * uCoreSize;\n\n for (float n = 0.0; n < 8.0; n++) {\n if (n >= uDetail) break;\n p *= warp;\n float t = r - time / (n + 3.0);\n i -= p + vec2(cos(t - i.x - r) + sin(t + i.y), sin(t - i.y) + cos(t + i.x) + r);\n c += glowCore / length(vec2(sin(i.x + t), cos(i.y + t)));\n }\n\n c /= 6.0;\n\n float intensity = max(c - uBlackPoint, 0.0) * uBrightness;\n\n float g = clamp(intensity, 0.0, 1.0);\n\n float mid = 0.5;\n if (uColorMode > 1.5) {\n mid = 0.65;\n } else if (uColorMode > 0.5) {\n mid = 0.35;\n }\n\n vec3 col = mix(uColor1, uColor2, smoothstep(0.0, mid, g));\n col = mix(col, uColor3, smoothstep(mid, 1.0, g));\n\n float a = g;\n if (uGrain > 0.5) {\n float gr = hash(gl_FragCoord.xy + iTime);\n a += (gr - 0.5) * uGrainIntensity;\n }\n a = clamp(a, 0.0, 1.0) * uOpacity;\n fragColor = vec4(col * a, a);\n}\n`;\n\ntype MoltenMetalCtx = {\n renderer: InstanceType;\n program: InstanceType;\n mesh: InstanceType;\n};\nconst ctxMap = new WeakMap();\n\nconst MoltenMetal: React.FC = ({\n color1 = '#5227FF',\n color2 = '#FF9FFC',\n color3 = '#FFFFFF',\n speed = 0.35,\n scale = 4,\n detail = 3,\n glow = 1.6,\n coreSize = 0.1,\n swirl = 1,\n fold = -0.2,\n blackPoint = 0.05,\n brightness = 1.3,\n colorMode = 'molten',\n grain = true,\n grainIntensity = 0.05,\n mouseInteraction = true,\n mouseStrength = 0.3,\n opacity = 1.0,\n className = ''\n}) => {\n const containerRef = useRef(null);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n webgl: 2,\n alpha: true,\n premultipliedAlpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n const canvas = gl.canvas;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uSpeed: { value: 0.35 },\n uScale: { value: 4 },\n uDetail: { value: 3 },\n uGlow: { value: 1.6 },\n uCoreSize: { value: 0.1 },\n uSwirl: { value: 1 },\n uFold: { value: -0.2 },\n uBlackPoint: { value: 0.05 },\n uBrightness: { value: 1.3 },\n uColorMode: { value: 0 },\n uGrain: { value: 1 },\n uGrainIntensity: { value: 0.05 },\n uOpacity: { value: 1.0 },\n uMouse: { value: new Float32Array([0.5, 0.5]) },\n uMouseStrength: { value: 0.3 },\n uEnableMouse: { value: true },\n uColor1: { value: new Float32Array([1, 1, 1]) },\n uColor2: { value: new Float32Array([1, 1, 1]) },\n uColor3: { value: new Float32Array([1, 1, 1]) }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n ctxMap.set(container, { renderer, program, mesh });\n\n const setSize = () => {\n const rect = container.getBoundingClientRect();\n const w = Math.max(1, Math.floor(rect.width));\n const h = Math.max(1, Math.floor(rect.height));\n renderer.setSize(w, h);\n const res = program.uniforms.iResolution.value as Float32Array;\n res[0] = gl.drawingBufferWidth;\n res[1] = gl.drawingBufferHeight;\n renderer.render({ scene: mesh });\n };\n\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n setSize();\n\n const targetMouse: [number, number] = [0.5, 0.5];\n const currentMouse: [number, number] = [0.5, 0.5];\n\n const handleMouseMove = (e: MouseEvent) => {\n const rect = canvas.getBoundingClientRect();\n targetMouse[0] = (e.clientX - rect.left) / rect.width;\n targetMouse[1] = 1.0 - (e.clientY - rect.top) / rect.height;\n };\n const handleMouseLeave = () => {\n targetMouse[0] = 0.5;\n targetMouse[1] = 0.5;\n };\n canvas.addEventListener('mousemove', handleMouseMove);\n canvas.addEventListener('mouseleave', handleMouseLeave);\n\n let raf = 0;\n let isVisible = true;\n let isPageVisible = !document.hidden;\n const t0 = performance.now();\n\n const loop = (t: number) => {\n program.uniforms.iTime.value = (t - t0) * 0.001;\n currentMouse[0] += 0.05 * (targetMouse[0] - currentMouse[0]);\n currentMouse[1] += 0.05 * (targetMouse[1] - currentMouse[1]);\n const m = program.uniforms.uMouse.value as Float32Array;\n m[0] = currentMouse[0];\n m[1] = currentMouse[1];\n renderer.render({ scene: mesh });\n raf = requestAnimationFrame(loop);\n };\n\n const tryStart = () => {\n if (isVisible && isPageVisible && raf === 0) raf = requestAnimationFrame(loop);\n };\n const tryStop = () => {\n if (raf !== 0) {\n cancelAnimationFrame(raf);\n raf = 0;\n }\n };\n\n const io = new IntersectionObserver(\n ([entry]) => {\n isVisible = entry.isIntersecting;\n isVisible ? tryStart() : tryStop();\n },\n { threshold: 0 }\n );\n io.observe(container);\n\n const onVisibility = () => {\n isPageVisible = !document.hidden;\n isPageVisible ? tryStart() : tryStop();\n };\n document.addEventListener('visibilitychange', onVisibility);\n\n tryStart();\n\n return () => {\n tryStop();\n ro.disconnect();\n io.disconnect();\n document.removeEventListener('visibilitychange', onVisibility);\n canvas.removeEventListener('mousemove', handleMouseMove);\n canvas.removeEventListener('mouseleave', handleMouseLeave);\n ctxMap.delete(container);\n try {\n container.removeChild(canvas);\n } catch {}\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, []);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n const ctx = ctxMap.get(container);\n if (!ctx) return;\n const u = ctx.program.uniforms;\n\n u.uSpeed.value = speed;\n u.uScale.value = scale;\n u.uDetail.value = detail;\n u.uGlow.value = glow;\n u.uCoreSize.value = Math.max(coreSize, 0.001);\n u.uSwirl.value = swirl;\n u.uFold.value = fold;\n u.uBlackPoint.value = blackPoint;\n u.uBrightness.value = brightness;\n u.uColorMode.value = colorModeToFloat(colorMode);\n u.uGrain.value = grain ? 1 : 0;\n u.uGrainIntensity.value = grainIntensity;\n u.uOpacity.value = opacity;\n u.uMouseStrength.value = mouseStrength;\n u.uEnableMouse.value = mouseInteraction;\n const c1 = hexToRgb(color1);\n const c2 = hexToRgb(color2);\n const c3 = hexToRgb(color3);\n const uc1 = u.uColor1.value as Float32Array;\n const uc2 = u.uColor2.value as Float32Array;\n const uc3 = u.uColor3.value as Float32Array;\n uc1[0] = c1[0];\n uc1[1] = c1[1];\n uc1[2] = c1[2];\n uc2[0] = c2[0];\n uc2[1] = c2[1];\n uc2[2] = c2[2];\n uc3[0] = c3[0];\n uc3[1] = c3[1];\n uc3[2] = c3[2];\n }, [\n color1,\n color2,\n color3,\n speed,\n scale,\n detail,\n glow,\n coreSize,\n swirl,\n fold,\n blackPoint,\n brightness,\n colorMode,\n grain,\n grainIntensity,\n mouseInteraction,\n mouseStrength,\n opacity\n ]);\n\n return
    ;\n};\n\nexport default MoltenMetal;\n" } ], diff --git a/public/r/MorphSlider-JS-CSS.json b/public/r/MorphSlider-JS-CSS.json index 6ad20079..1cb8a817 100644 --- a/public/r/MorphSlider-JS-CSS.json +++ b/public/r/MorphSlider-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "MorphSlider/MorphSlider.css", + "type": "registry:file", + "path": "MorphSlider.css", + "target": "@components/MorphSlider.css", "content": ".morph-slider {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n background: #0c0c0e;\n user-select: none;\n touch-action: pan-y;\n}\n\n.morph-slider-stage {\n position: absolute;\n inset: 0;\n cursor: grab;\n outline: none;\n}\n\n.morph-slider-stage:active {\n cursor: grabbing;\n}\n\n.morph-slider-stage:focus-visible {\n box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.7);\n}\n\n.morph-slider-canvas {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.morph-slider-caption {\n position: absolute;\n left: 22px;\n bottom: 22px;\n z-index: 2;\n max-width: 70%;\n pointer-events: none;\n display: grid;\n}\n\n.morph-slider-caption-text {\n grid-area: 1 / 1;\n justify-self: start;\n display: inline-block;\n padding: 8px 14px;\n border-radius: 10px;\n font-size: 15px;\n font-weight: 600;\n letter-spacing: 0.01em;\n color: #fff;\n background: rgba(10, 10, 12, 0.42);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n opacity: 0;\n transform: translateY(12px);\n filter: blur(6px);\n transition:\n opacity var(--ms-swap) cubic-bezier(0.16, 1, 0.3, 1),\n transform var(--ms-swap) cubic-bezier(0.16, 1, 0.3, 1),\n filter var(--ms-swap) cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.morph-slider-caption-text.is-active {\n opacity: 1;\n transform: translateY(0);\n filter: blur(0);\n}\n\n.morph-slider-controls {\n position: absolute;\n top: 50%;\n left: 0;\n right: 0;\n z-index: 3;\n display: flex;\n justify-content: space-between;\n padding: 0 16px;\n transform: translateY(-50%);\n pointer-events: none;\n}\n\n.morph-slider-btn {\n pointer-events: auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n border: 1px solid rgba(255, 255, 255, 0.22);\n border-radius: 50%;\n color: #fff;\n background: rgba(12, 12, 14, 0.4);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n cursor: pointer;\n transition:\n transform 0.25s cubic-bezier(0.16, 1, 0.3, 1),\n background 0.25s ease,\n border-color 0.25s ease;\n}\n\n.morph-slider-btn:hover {\n background: rgba(24, 24, 28, 0.6);\n border-color: rgba(255, 255, 255, 0.5);\n transform: scale(1.06);\n}\n\n.morph-slider-btn:active {\n transform: scale(0.96);\n}\n\n.morph-slider-btn:focus-visible {\n outline: 2px solid rgba(255, 255, 255, 0.8);\n outline-offset: 2px;\n}\n\n.morph-slider-indicators {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 18px;\n z-index: 3;\n display: flex;\n gap: 8px;\n justify-content: center;\n align-items: center;\n}\n\n.morph-slider-dot {\n width: 8px;\n height: 8px;\n padding: 0;\n border: none;\n border-radius: 999px;\n background: rgba(255, 255, 255, 0.35);\n cursor: pointer;\n transition:\n width var(--ms-dot) cubic-bezier(0.16, 1, 0.3, 1),\n background var(--ms-dot) ease;\n}\n\n.morph-slider-dot.is-active {\n width: 22px;\n background: rgba(255, 255, 255, 0.95);\n}\n\n.morph-slider-dot:focus-visible {\n outline: 2px solid rgba(255, 255, 255, 0.8);\n outline-offset: 2px;\n}\n\n@media (max-width: 420px) {\n .morph-slider-btn {\n width: 34px;\n height: 34px;\n }\n\n .morph-slider-caption-text {\n font-size: 13px;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .morph-slider-caption-text,\n .morph-slider-btn,\n .morph-slider-dot {\n animation: none;\n transition: none;\n }\n}\n" }, { "type": "registry:component", - "path": "MorphSlider/MorphSlider.jsx", + "path": "MorphSlider.jsx", "content": "import { useEffect, useRef, useState, useCallback } from 'react';\nimport { Renderer, Triangle, Program, Mesh, Texture } from 'ogl';\nimport { gsap } from 'gsap';\n\nimport './MorphSlider.css';\n\nconst TRANSITIONS = { melt: 0, ripple: 1, shear: 2, swirl: 3 };\n\nconst DEFAULT_ITEMS = [\n {\n image: 'https://images.unsplash.com/photo-1782977389500-dd7adad33ebe?q=80&w=1600&auto=format&fit=crop',\n caption: 'One'\n },\n {\n image: 'https://images.unsplash.com/photo-1781499455083-6ccc3beb20cd?q=80&w=1600&auto=format&fit=crop',\n caption: 'Two'\n },\n {\n image: 'https://images.unsplash.com/photo-1776394254711-4a0d7345269a?q=80&w=1600&auto=format&fit=crop',\n caption: 'Three'\n },\n {\n image: 'https://images.unsplash.com/photo-1781242629922-6f39cc3671cd?q=80&w=1600&auto=format&fit=crop',\n caption: 'Four'\n }\n];\n\nconst vertexShader = `\nattribute vec2 position;\nattribute vec2 uv;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform sampler2D tCurrent;\nuniform sampler2D tNext;\nuniform vec2 uResolution;\nuniform vec2 uCurrentSize;\nuniform vec2 uNextSize;\nuniform float uProgress;\nuniform float uDir;\nuniform int uMode;\nuniform float uIntensity;\nuniform float uScale;\nuniform float uAberration;\nuniform float uDrift;\nuniform float uTime;\nuniform float uReduce;\nuniform vec2 uPointer;\nuniform vec3 uOverlay;\n\nvarying vec2 vUv;\n\nconst float PI = 3.14159265359;\n\nfloat hash11(float p) {\n p = fract(p * 0.1031);\n p *= p + 33.33;\n p *= p + p;\n return fract(p);\n}\n\nfloat hash21(vec2 p) {\n vec3 p3 = fract(vec3(p.xyx) * 0.1031);\n p3 += dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfloat noise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n float a = hash21(i);\n float b = hash21(i + vec2(1.0, 0.0));\n float c = hash21(i + vec2(0.0, 1.0));\n float d = hash21(i + vec2(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfloat fbm(vec2 p) {\n float v = 0.0;\n float a = 0.5;\n for (int i = 0; i < 5; i++) {\n v += a * noise(p);\n p *= 2.0;\n a *= 0.5;\n }\n return v;\n}\n\nmat2 rot(float a) {\n float s = sin(a);\n float c = cos(a);\n return mat2(c, -s, s, c);\n}\n\nvec2 coverUV(vec2 uv, vec2 res, vec2 img) {\n float rA = res.x / max(res.y, 1.0);\n float iA = img.x / max(img.y, 1.0);\n vec2 s = vec2(1.0);\n float ratio = rA / max(iA, 0.0001);\n if (ratio > 1.0) {\n s.y = 1.0 / ratio;\n } else {\n s.x = ratio;\n }\n return (uv - 0.5) * s + 0.5;\n}\n\nvoid main() {\n float p = clamp(uProgress, 0.0, 1.0);\n float env = sin(p * PI);\n\n vec2 uv = vUv;\n\n uv += vec2(sin(uTime * 0.25 + uv.y * 4.0), cos(uTime * 0.22 + uv.x * 4.0)) * uDrift * 0.008;\n uv = (uv - 0.5) * (1.0 - uDrift * 0.02 * sin(uTime * 0.4)) + 0.5;\n\n vec2 uvC = uv;\n vec2 uvN = uv;\n float m = smoothstep(0.0, 1.0, p);\n\n if (uReduce < 0.5) {\n if (uMode == 3) {\n vec2 c = uv - 0.5;\n float r = length(c);\n float ang = env * uIntensity * 3.5 * (1.0 - r);\n uvC = rot(ang) * c + 0.5;\n uvN = rot(-ang) * c + 0.5;\n m = smoothstep(0.0, 1.0, p);\n } else if (uMode == 1) {\n float d = distance(uv, uPointer);\n float ring = p * 1.6;\n float wave = sin((d - ring) * 30.0) * env;\n vec2 dir = normalize(uv - uPointer + 1e-4);\n vec2 disp = dir * wave * uIntensity * 0.25;\n uvC = uv + disp;\n uvN = uv + disp * 0.6;\n m = 1.0 - smoothstep(ring - 0.03, ring + 0.03, d);\n } else if (uMode == 2) {\n float slices = 14.0;\n float row = floor(uv.y * slices);\n float rnd = hash11(row);\n vec2 disp = vec2((rnd - 0.5) * env * uIntensity * 0.6, 0.0);\n uvC = uv + disp;\n uvN = uv + disp;\n float localX = uDir > 0.0 ? uv.x : 1.0 - uv.x;\n float th = p * 1.5 - 0.25 + (rnd - 0.5) * 0.25;\n m = 1.0 - smoothstep(th - 0.06, th + 0.06, localX);\n } else {\n float nn = fbm(uv * uScale + uTime * 0.03);\n float warp = fbm(uv * uScale * 1.7 - uTime * 0.02);\n vec2 g = vec2(nn, warp) - 0.5;\n uvC = uv + g * uIntensity * 0.5 * p;\n uvN = uv - g * uIntensity * 0.5 * (1.0 - p);\n m = smoothstep(nn - 0.15, nn + 0.15, p);\n }\n }\n\n vec2 sC = coverUV(uvC, uResolution, uCurrentSize);\n vec2 sN = coverUV(uvN, uResolution, uNextSize);\n\n float ca = uReduce < 0.5 ? uAberration * env * 0.03 : 0.0;\n\n vec3 colC = vec3(\n texture2D(tCurrent, sC + vec2(ca, 0.0)).r,\n texture2D(tCurrent, sC).g,\n texture2D(tCurrent, sC - vec2(ca, 0.0)).b\n );\n vec3 colN = vec3(\n texture2D(tNext, sN + vec2(ca, 0.0)).r,\n texture2D(tNext, sN).g,\n texture2D(tNext, sN - vec2(ca, 0.0)).b\n );\n\n vec3 col = mix(colC, colN, m);\n\n float vig = smoothstep(1.25, 0.25, length(uv - 0.5));\n col = mix(col, uOverlay, (1.0 - vig) * 0.28);\n\n gl_FragColor = vec4(col, 1.0);\n}\n`;\n\nfunction makeFallbackTexture(gl) {\n const size = 4;\n const data = new Uint8Array(size * size * 4);\n for (let i = 0; i < size * size; i++) {\n data[i * 4] = 24;\n data[i * 4 + 1] = 24;\n data[i * 4 + 2] = 28;\n data[i * 4 + 3] = 255;\n }\n return new Texture(gl, { image: data, width: size, height: size, generateMipmaps: false });\n}\n\nfunction hexToRgb(hex) {\n let h = (hex || '#000000').replace('#', '');\n if (h.length === 3) {\n h = h\n .split('')\n .map(c => c + c)\n .join('');\n }\n const n = parseInt(h, 16);\n return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\nclass MorphEngine {\n constructor(container, { items, startIndex, reducedMotion, getOptions, onIndexChange, dprCap }) {\n this.container = container;\n this.items = items;\n this.getOptions = getOptions;\n this.onIndexChange = onIndexChange;\n this.reducedMotion = reducedMotion;\n\n this.current = startIndex;\n this.animating = false;\n this.dragging = false;\n this.dragDir = 0;\n this.shownIndex = startIndex;\n this.tween = null;\n\n this.renderer = new Renderer({\n alpha: false,\n antialias: true,\n dpr: Math.min(window.devicePixelRatio || 1, dprCap)\n });\n this.gl = this.renderer.gl;\n this.gl.clearColor(0.05, 0.05, 0.06, 1);\n\n this.canvas = this.gl.canvas;\n this.canvas.className = 'morph-slider-canvas';\n container.appendChild(this.canvas);\n\n this.geometry = new Triangle(this.gl);\n\n this.textures = this.items.map(() => makeFallbackTexture(this.gl));\n this.sizes = this.items.map(() => [1, 1]);\n\n const opts = this.getOptions();\n this.program = new Program(this.gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n tCurrent: { value: this.textures[this.current] },\n tNext: { value: this.textures[this.current] },\n uResolution: { value: [1, 1] },\n uCurrentSize: { value: this.sizes[this.current] },\n uNextSize: { value: this.sizes[this.current] },\n uProgress: { value: 0 },\n uDir: { value: 1 },\n uMode: { value: TRANSITIONS[opts.transition] ?? 0 },\n uIntensity: { value: opts.intensity },\n uScale: { value: opts.scale },\n uAberration: { value: opts.aberration },\n uDrift: { value: opts.drift },\n uTime: { value: 0 },\n uReduce: { value: reducedMotion ? 1 : 0 },\n uPointer: { value: [0.5, 0.5] },\n uOverlay: { value: hexToRgb(opts.overlayColor) }\n }\n });\n\n this.mesh = new Mesh(this.gl, { geometry: this.geometry, program: this.program });\n\n this.boundContextLost = this.onContextLost.bind(this);\n this.canvas.addEventListener('webglcontextlost', this.boundContextLost, false);\n\n this.resizeObserver = new ResizeObserver(() => this.resize());\n this.resizeObserver.observe(container);\n this.resize();\n\n this.loadTextures();\n\n this.boundLoop = this.loop.bind(this);\n this.raf = requestAnimationFrame(this.boundLoop);\n }\n\n loadTextures() {\n this.items.forEach((item, index) => {\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.src = item.image;\n img.onload = () => {\n const texture = new Texture(this.gl, { generateMipmaps: false });\n texture.image = img;\n this.textures[index] = texture;\n this.sizes[index] = [img.naturalWidth || 1, img.naturalHeight || 1];\n if (index === this.current) {\n this.program.uniforms.tCurrent.value = texture;\n this.program.uniforms.uCurrentSize.value = this.sizes[index];\n }\n };\n img.onerror = () => {};\n });\n }\n\n resize() {\n const rect = this.container.getBoundingClientRect();\n const w = Math.max(rect.width, 1);\n const h = Math.max(rect.height, 1);\n this.renderer.setSize(w, h);\n this.program.uniforms.uResolution.value = [this.gl.canvas.width, this.gl.canvas.height];\n }\n\n syncOptions() {\n const opts = this.getOptions();\n this.program.uniforms.uMode.value = TRANSITIONS[opts.transition] ?? 0;\n this.program.uniforms.uIntensity.value = opts.intensity;\n this.program.uniforms.uScale.value = opts.scale;\n this.program.uniforms.uAberration.value = opts.aberration;\n this.program.uniforms.uDrift.value = opts.drift;\n this.program.uniforms.uOverlay.value = hexToRgb(opts.overlayColor);\n }\n\n loop(t) {\n this.program.uniforms.uTime.value = t * 0.001;\n if (!this.dragging && !this.animating) this.syncOptions();\n this.renderer.render({ scene: this.mesh });\n this.raf = requestAnimationFrame(this.boundLoop);\n }\n\n wrap(i) {\n const n = this.items.length;\n return ((i % n) + n) % n;\n }\n\n prepareNext(dir) {\n const target = this.wrap(this.current + dir);\n this.program.uniforms.tCurrent.value = this.textures[this.current];\n this.program.uniforms.uCurrentSize.value = this.sizes[this.current];\n this.program.uniforms.tNext.value = this.textures[target];\n this.program.uniforms.uNextSize.value = this.sizes[target];\n this.program.uniforms.uDir.value = dir;\n return target;\n }\n\n goTo(dir) {\n if (this.animating || this.dragging || this.items.length < 2) return;\n const opts = this.getOptions();\n if (!opts.loop) {\n const raw = this.current + dir;\n if (raw < 0 || raw > this.items.length - 1) return;\n }\n this.syncOptions();\n const target = this.prepareNext(dir);\n this.animating = true;\n this.announce(target);\n const duration = this.reducedMotion ? Math.min(opts.duration, 0.4) : opts.duration;\n this.tween = gsap.fromTo(\n this.program.uniforms.uProgress,\n { value: 0 },\n {\n value: 1,\n duration,\n ease: opts.ease,\n onComplete: () => this.commit(target)\n }\n );\n }\n\n announce(index) {\n if (index === this.shownIndex) return;\n this.shownIndex = index;\n if (this.onIndexChange) this.onIndexChange(index);\n }\n\n commit(target) {\n this.current = target;\n this.program.uniforms.tCurrent.value = this.textures[target];\n this.program.uniforms.uCurrentSize.value = this.sizes[target];\n this.program.uniforms.uProgress.value = 0;\n this.animating = false;\n this.tween = null;\n this.announce(target);\n }\n\n next() {\n this.goTo(1);\n }\n\n prev() {\n this.goTo(-1);\n }\n\n setPointer(x, y) {\n this.program.uniforms.uPointer.value = [x, y];\n }\n\n beginDrag() {\n if (this.animating || this.items.length < 2) return false;\n this.dragging = true;\n this.dragDir = 0;\n this.syncOptions();\n return true;\n }\n\n drag(ndx) {\n if (!this.dragging) return;\n const opts = this.getOptions();\n const dir = ndx < 0 ? 1 : -1;\n if (!opts.loop) {\n const raw = this.current + dir;\n if (raw < 0 || raw > this.items.length - 1) {\n this.program.uniforms.uProgress.value = 0;\n return;\n }\n }\n if (dir !== this.dragDir) {\n this.dragDir = dir;\n this.prepareNext(dir);\n }\n const progress = Math.min(Math.abs(ndx), 1);\n this.program.uniforms.uProgress.value = progress;\n this.announce(progress > 0.5 ? this.wrap(this.current + dir) : this.current);\n }\n\n endDrag() {\n if (!this.dragging) return;\n this.dragging = false;\n const p = this.program.uniforms.uProgress.value;\n if (this.dragDir === 0) return;\n const target = this.wrap(this.current + this.dragDir);\n const duration = this.reducedMotion ? 0.3 : 0.5;\n this.animating = true;\n if (p > 0.4) {\n this.announce(target);\n this.tween = gsap.to(this.program.uniforms.uProgress, {\n value: 1,\n duration,\n ease: 'power2.out',\n onComplete: () => this.commit(target)\n });\n } else {\n this.announce(this.current);\n this.tween = gsap.to(this.program.uniforms.uProgress, {\n value: 0,\n duration,\n ease: 'power2.out',\n onComplete: () => {\n this.animating = false;\n this.tween = null;\n }\n });\n }\n }\n\n onContextLost(e) {\n e.preventDefault();\n cancelAnimationFrame(this.raf);\n }\n\n destroy() {\n cancelAnimationFrame(this.raf);\n if (this.tween) this.tween.kill();\n this.resizeObserver.disconnect();\n this.canvas.removeEventListener('webglcontextlost', this.boundContextLost);\n this.textures.forEach(tex => {\n if (tex && tex.texture) this.gl.deleteTexture(tex.texture);\n });\n if (this.program && this.program.program) this.gl.deleteProgram(this.program.program);\n const ext = this.gl.getExtension('WEBGL_lose_context');\n if (ext) ext.loseContext();\n if (this.canvas.parentNode) this.canvas.parentNode.removeChild(this.canvas);\n }\n}\n\nexport default function MorphSlider({\n items = DEFAULT_ITEMS,\n startIndex = 0,\n transition = 'melt',\n duration = 1.1,\n ease = 'power2.inOut',\n intensity = 0.55,\n scale = 2.4,\n aberration = 0.35,\n drift = 0.4,\n autoplay = false,\n autoplayDelay = 4,\n loop = true,\n radius = 16,\n overlayColor = '#000000',\n showCaptions = true,\n showControls = true,\n showIndicators = true,\n className = '',\n ...props\n}) {\n const containerRef = useRef(null);\n const engineRef = useRef(null);\n const [index, setIndex] = useState(startIndex);\n const [hovering, setHovering] = useState(false);\n\n const optsRef = useRef();\n optsRef.current = { transition, duration, ease, intensity, scale, aberration, drift, overlayColor, loop };\n\n useEffect(() => {\n if (!containerRef.current) return undefined;\n const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n const engine = new MorphEngine(containerRef.current, {\n items,\n startIndex,\n reducedMotion,\n dprCap: 2,\n getOptions: () => optsRef.current,\n onIndexChange: setIndex\n });\n engineRef.current = engine;\n setIndex(startIndex);\n\n return () => {\n engine.destroy();\n engineRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [items, startIndex]);\n\n const handleNext = useCallback(() => engineRef.current?.next(), []);\n const handlePrev = useCallback(() => engineRef.current?.prev(), []);\n\n useEffect(() => {\n if (!autoplay || hovering) return undefined;\n const id = setTimeout(() => engineRef.current?.next(), Math.max(autoplayDelay, 1) * 1000);\n return () => clearTimeout(id);\n }, [autoplay, autoplayDelay, hovering, index]);\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return undefined;\n let startX = 0;\n let width = 1;\n let active = false;\n\n const onDown = e => {\n const rect = el.getBoundingClientRect();\n width = rect.width || 1;\n startX = e.clientX;\n const px = (e.clientX - rect.left) / rect.width;\n const py = (e.clientY - rect.top) / rect.height;\n engineRef.current?.setPointer(px, 1 - py);\n active = engineRef.current?.beginDrag() ?? false;\n if (active && el.setPointerCapture) {\n try {\n el.setPointerCapture(e.pointerId);\n } catch {}\n }\n };\n const onMove = e => {\n if (!active) return;\n const ndx = (e.clientX - startX) / width;\n engineRef.current?.drag(ndx);\n };\n const onUp = () => {\n if (!active) return;\n active = false;\n engineRef.current?.endDrag();\n };\n\n el.addEventListener('pointerdown', onDown);\n el.addEventListener('pointermove', onMove);\n el.addEventListener('pointerup', onUp);\n el.addEventListener('pointercancel', onUp);\n\n return () => {\n el.removeEventListener('pointerdown', onDown);\n el.removeEventListener('pointermove', onMove);\n el.removeEventListener('pointerup', onUp);\n el.removeEventListener('pointercancel', onUp);\n };\n }, []);\n\n const onKeyDown = useCallback(\n e => {\n if (e.key === 'ArrowRight') {\n e.preventDefault();\n handleNext();\n } else if (e.key === 'ArrowLeft') {\n e.preventDefault();\n handlePrev();\n }\n },\n [handleNext, handlePrev]\n );\n\n const hasCaptions = items.some(item => item.caption);\n\n return (\n setHovering(true)}\n onMouseLeave={() => setHovering(false)}\n {...props}\n >\n \n\n {showCaptions && hasCaptions && (\n
    \n {items.map((item, i) =>\n item.caption ? (\n \n {item.caption}\n \n ) : null\n )}\n
    \n )}\n\n {showControls && (\n
    \n \n \n
    \n )}\n\n {showIndicators && (\n
    \n {items.map((item, i) => (\n {\n const engine = engineRef.current;\n if (!engine || i === index) return;\n engine.goTo(i > index ? 1 : -1);\n }}\n />\n ))}\n
    \n )}\n
    \n );\n}\n" } ], diff --git a/public/r/MorphSlider-TS-CSS.json b/public/r/MorphSlider-TS-CSS.json index 5d0d4c04..806c4941 100644 --- a/public/r/MorphSlider-TS-CSS.json +++ b/public/r/MorphSlider-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "MorphSlider/MorphSlider.css", + "type": "registry:file", + "path": "MorphSlider.css", + "target": "@components/MorphSlider.css", "content": ".morph-slider {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n background: #0c0c0e;\n user-select: none;\n touch-action: pan-y;\n}\n\n.morph-slider-stage {\n position: absolute;\n inset: 0;\n cursor: grab;\n outline: none;\n}\n\n.morph-slider-stage:active {\n cursor: grabbing;\n}\n\n.morph-slider-stage:focus-visible {\n box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.7);\n}\n\n.morph-slider-canvas {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.morph-slider-caption {\n position: absolute;\n left: 22px;\n bottom: 22px;\n z-index: 2;\n max-width: 70%;\n pointer-events: none;\n display: grid;\n}\n\n.morph-slider-caption-text {\n grid-area: 1 / 1;\n justify-self: start;\n display: inline-block;\n padding: 8px 14px;\n border-radius: 10px;\n font-size: 15px;\n font-weight: 600;\n letter-spacing: 0.01em;\n color: #fff;\n background: rgba(10, 10, 12, 0.42);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n opacity: 0;\n transform: translateY(12px);\n filter: blur(6px);\n transition:\n opacity var(--ms-swap) cubic-bezier(0.16, 1, 0.3, 1),\n transform var(--ms-swap) cubic-bezier(0.16, 1, 0.3, 1),\n filter var(--ms-swap) cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.morph-slider-caption-text.is-active {\n opacity: 1;\n transform: translateY(0);\n filter: blur(0);\n}\n\n.morph-slider-controls {\n position: absolute;\n top: 50%;\n left: 0;\n right: 0;\n z-index: 3;\n display: flex;\n justify-content: space-between;\n padding: 0 16px;\n transform: translateY(-50%);\n pointer-events: none;\n}\n\n.morph-slider-btn {\n pointer-events: auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n border: 1px solid rgba(255, 255, 255, 0.22);\n border-radius: 50%;\n color: #fff;\n background: rgba(12, 12, 14, 0.4);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n cursor: pointer;\n transition:\n transform 0.25s cubic-bezier(0.16, 1, 0.3, 1),\n background 0.25s ease,\n border-color 0.25s ease;\n}\n\n.morph-slider-btn:hover {\n background: rgba(24, 24, 28, 0.6);\n border-color: rgba(255, 255, 255, 0.5);\n transform: scale(1.06);\n}\n\n.morph-slider-btn:active {\n transform: scale(0.96);\n}\n\n.morph-slider-btn:focus-visible {\n outline: 2px solid rgba(255, 255, 255, 0.8);\n outline-offset: 2px;\n}\n\n.morph-slider-indicators {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 18px;\n z-index: 3;\n display: flex;\n gap: 8px;\n justify-content: center;\n align-items: center;\n}\n\n.morph-slider-dot {\n width: 8px;\n height: 8px;\n padding: 0;\n border: none;\n border-radius: 999px;\n background: rgba(255, 255, 255, 0.35);\n cursor: pointer;\n transition:\n width var(--ms-dot) cubic-bezier(0.16, 1, 0.3, 1),\n background var(--ms-dot) ease;\n}\n\n.morph-slider-dot.is-active {\n width: 22px;\n background: rgba(255, 255, 255, 0.95);\n}\n\n.morph-slider-dot:focus-visible {\n outline: 2px solid rgba(255, 255, 255, 0.8);\n outline-offset: 2px;\n}\n\n@media (max-width: 420px) {\n .morph-slider-btn {\n width: 34px;\n height: 34px;\n }\n\n .morph-slider-caption-text {\n font-size: 13px;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .morph-slider-caption-text,\n .morph-slider-btn,\n .morph-slider-dot {\n animation: none;\n transition: none;\n }\n}\n" }, { "type": "registry:component", - "path": "MorphSlider/MorphSlider.tsx", + "path": "MorphSlider.tsx", "content": "import { useEffect, useRef, useState, useCallback } from 'react';\nimport type { CSSProperties } from 'react';\nimport { Renderer, Triangle, Program, Mesh, Texture } from 'ogl';\nimport { gsap } from 'gsap';\n\nimport './MorphSlider.css';\n\nexport type MorphTransition = 'melt' | 'ripple' | 'shear' | 'swirl';\n\nexport interface MorphItem {\n image: string;\n caption?: string;\n}\n\nexport interface MorphSliderProps {\n items?: MorphItem[];\n startIndex?: number;\n transition?: MorphTransition;\n duration?: number;\n ease?: string;\n intensity?: number;\n scale?: number;\n aberration?: number;\n drift?: number;\n autoplay?: boolean;\n autoplayDelay?: number;\n loop?: boolean;\n radius?: number;\n overlayColor?: string;\n showCaptions?: boolean;\n showControls?: boolean;\n showIndicators?: boolean;\n className?: string;\n [key: string]: unknown;\n}\n\ninterface EngineOptions {\n transition: MorphTransition;\n duration: number;\n ease: string;\n intensity: number;\n scale: number;\n aberration: number;\n drift: number;\n overlayColor: string;\n loop: boolean;\n}\n\ntype GL = Renderer['gl'];\n\nconst TRANSITIONS: Record = { melt: 0, ripple: 1, shear: 2, swirl: 3 };\n\nconst DEFAULT_ITEMS: MorphItem[] = [\n {\n image: 'https://images.unsplash.com/photo-1782977389500-dd7adad33ebe?q=80&w=1600&auto=format&fit=crop',\n caption: 'One'\n },\n {\n image: 'https://images.unsplash.com/photo-1781499455083-6ccc3beb20cd?q=80&w=1600&auto=format&fit=crop',\n caption: 'Two'\n },\n {\n image: 'https://images.unsplash.com/photo-1776394254711-4a0d7345269a?q=80&w=1600&auto=format&fit=crop',\n caption: 'Three'\n },\n {\n image: 'https://images.unsplash.com/photo-1781242629922-6f39cc3671cd?q=80&w=1600&auto=format&fit=crop',\n caption: 'Four'\n }\n];\n\nconst vertexShader = `\nattribute vec2 position;\nattribute vec2 uv;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform sampler2D tCurrent;\nuniform sampler2D tNext;\nuniform vec2 uResolution;\nuniform vec2 uCurrentSize;\nuniform vec2 uNextSize;\nuniform float uProgress;\nuniform float uDir;\nuniform int uMode;\nuniform float uIntensity;\nuniform float uScale;\nuniform float uAberration;\nuniform float uDrift;\nuniform float uTime;\nuniform float uReduce;\nuniform vec2 uPointer;\nuniform vec3 uOverlay;\n\nvarying vec2 vUv;\n\nconst float PI = 3.14159265359;\n\nfloat hash11(float p) {\n p = fract(p * 0.1031);\n p *= p + 33.33;\n p *= p + p;\n return fract(p);\n}\n\nfloat hash21(vec2 p) {\n vec3 p3 = fract(vec3(p.xyx) * 0.1031);\n p3 += dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfloat noise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n float a = hash21(i);\n float b = hash21(i + vec2(1.0, 0.0));\n float c = hash21(i + vec2(0.0, 1.0));\n float d = hash21(i + vec2(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfloat fbm(vec2 p) {\n float v = 0.0;\n float a = 0.5;\n for (int i = 0; i < 5; i++) {\n v += a * noise(p);\n p *= 2.0;\n a *= 0.5;\n }\n return v;\n}\n\nmat2 rot(float a) {\n float s = sin(a);\n float c = cos(a);\n return mat2(c, -s, s, c);\n}\n\nvec2 coverUV(vec2 uv, vec2 res, vec2 img) {\n float rA = res.x / max(res.y, 1.0);\n float iA = img.x / max(img.y, 1.0);\n vec2 s = vec2(1.0);\n float ratio = rA / max(iA, 0.0001);\n if (ratio > 1.0) {\n s.y = 1.0 / ratio;\n } else {\n s.x = ratio;\n }\n return (uv - 0.5) * s + 0.5;\n}\n\nvoid main() {\n float p = clamp(uProgress, 0.0, 1.0);\n float env = sin(p * PI);\n\n vec2 uv = vUv;\n\n uv += vec2(sin(uTime * 0.25 + uv.y * 4.0), cos(uTime * 0.22 + uv.x * 4.0)) * uDrift * 0.008;\n uv = (uv - 0.5) * (1.0 - uDrift * 0.02 * sin(uTime * 0.4)) + 0.5;\n\n vec2 uvC = uv;\n vec2 uvN = uv;\n float m = smoothstep(0.0, 1.0, p);\n\n if (uReduce < 0.5) {\n if (uMode == 3) {\n vec2 c = uv - 0.5;\n float r = length(c);\n float ang = env * uIntensity * 3.5 * (1.0 - r);\n uvC = rot(ang) * c + 0.5;\n uvN = rot(-ang) * c + 0.5;\n m = smoothstep(0.0, 1.0, p);\n } else if (uMode == 1) {\n float d = distance(uv, uPointer);\n float ring = p * 1.6;\n float wave = sin((d - ring) * 30.0) * env;\n vec2 dir = normalize(uv - uPointer + 1e-4);\n vec2 disp = dir * wave * uIntensity * 0.25;\n uvC = uv + disp;\n uvN = uv + disp * 0.6;\n m = 1.0 - smoothstep(ring - 0.03, ring + 0.03, d);\n } else if (uMode == 2) {\n float slices = 14.0;\n float row = floor(uv.y * slices);\n float rnd = hash11(row);\n vec2 disp = vec2((rnd - 0.5) * env * uIntensity * 0.6, 0.0);\n uvC = uv + disp;\n uvN = uv + disp;\n float localX = uDir > 0.0 ? uv.x : 1.0 - uv.x;\n float th = p * 1.5 - 0.25 + (rnd - 0.5) * 0.25;\n m = 1.0 - smoothstep(th - 0.06, th + 0.06, localX);\n } else {\n float nn = fbm(uv * uScale + uTime * 0.03);\n float warp = fbm(uv * uScale * 1.7 - uTime * 0.02);\n vec2 g = vec2(nn, warp) - 0.5;\n uvC = uv + g * uIntensity * 0.5 * p;\n uvN = uv - g * uIntensity * 0.5 * (1.0 - p);\n m = smoothstep(nn - 0.15, nn + 0.15, p);\n }\n }\n\n vec2 sC = coverUV(uvC, uResolution, uCurrentSize);\n vec2 sN = coverUV(uvN, uResolution, uNextSize);\n\n float ca = uReduce < 0.5 ? uAberration * env * 0.03 : 0.0;\n\n vec3 colC = vec3(\n texture2D(tCurrent, sC + vec2(ca, 0.0)).r,\n texture2D(tCurrent, sC).g,\n texture2D(tCurrent, sC - vec2(ca, 0.0)).b\n );\n vec3 colN = vec3(\n texture2D(tNext, sN + vec2(ca, 0.0)).r,\n texture2D(tNext, sN).g,\n texture2D(tNext, sN - vec2(ca, 0.0)).b\n );\n\n vec3 col = mix(colC, colN, m);\n\n float vig = smoothstep(1.25, 0.25, length(uv - 0.5));\n col = mix(col, uOverlay, (1.0 - vig) * 0.28);\n\n gl_FragColor = vec4(col, 1.0);\n}\n`;\n\nfunction makeFallbackTexture(gl: GL): Texture {\n const size = 4;\n const data = new Uint8Array(size * size * 4);\n for (let i = 0; i < size * size; i++) {\n data[i * 4] = 24;\n data[i * 4 + 1] = 24;\n data[i * 4 + 2] = 28;\n data[i * 4 + 3] = 255;\n }\n return new Texture(gl, { image: data, width: size, height: size, generateMipmaps: false });\n}\n\nfunction hexToRgb(hex: string): [number, number, number] {\n let h = (hex || '#000000').replace('#', '');\n if (h.length === 3) {\n h = h\n .split('')\n .map(c => c + c)\n .join('');\n }\n const n = parseInt(h, 16);\n return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\ninterface EngineConfig {\n items: MorphItem[];\n startIndex: number;\n reducedMotion: boolean;\n getOptions: () => EngineOptions;\n onIndexChange: (index: number) => void;\n dprCap: number;\n}\n\nclass MorphEngine {\n private container: HTMLElement;\n private items: MorphItem[];\n private getOptions: () => EngineOptions;\n private onIndexChange: (index: number) => void;\n private reducedMotion: boolean;\n\n private current: number;\n private animating = false;\n private dragging = false;\n private dragDir = 0;\n private shownIndex: number;\n private tween: gsap.core.Tween | null = null;\n\n private renderer: Renderer;\n private gl: GL;\n private canvas: HTMLCanvasElement;\n private geometry: Triangle;\n private program: Program;\n private mesh: Mesh;\n private textures: Texture[];\n private sizes: [number, number][];\n private resizeObserver: ResizeObserver;\n private raf = 0;\n private boundLoop: (t: number) => void;\n private boundContextLost: (e: Event) => void;\n\n constructor(container: HTMLElement, config: EngineConfig) {\n this.container = container;\n this.items = config.items;\n this.getOptions = config.getOptions;\n this.onIndexChange = config.onIndexChange;\n this.reducedMotion = config.reducedMotion;\n this.current = config.startIndex;\n this.shownIndex = config.startIndex;\n\n this.renderer = new Renderer({\n alpha: false,\n antialias: true,\n dpr: Math.min(window.devicePixelRatio || 1, config.dprCap)\n });\n this.gl = this.renderer.gl;\n this.gl.clearColor(0.05, 0.05, 0.06, 1);\n\n this.canvas = this.gl.canvas as HTMLCanvasElement;\n this.canvas.className = 'morph-slider-canvas';\n container.appendChild(this.canvas);\n\n this.geometry = new Triangle(this.gl);\n\n this.textures = this.items.map(() => makeFallbackTexture(this.gl));\n this.sizes = this.items.map(() => [1, 1] as [number, number]);\n\n const opts = this.getOptions();\n this.program = new Program(this.gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n tCurrent: { value: this.textures[this.current] },\n tNext: { value: this.textures[this.current] },\n uResolution: { value: [1, 1] },\n uCurrentSize: { value: this.sizes[this.current] },\n uNextSize: { value: this.sizes[this.current] },\n uProgress: { value: 0 },\n uDir: { value: 1 },\n uMode: { value: TRANSITIONS[opts.transition] ?? 0 },\n uIntensity: { value: opts.intensity },\n uScale: { value: opts.scale },\n uAberration: { value: opts.aberration },\n uDrift: { value: opts.drift },\n uTime: { value: 0 },\n uReduce: { value: this.reducedMotion ? 1 : 0 },\n uPointer: { value: [0.5, 0.5] },\n uOverlay: { value: hexToRgb(opts.overlayColor) }\n }\n });\n\n this.mesh = new Mesh(this.gl, { geometry: this.geometry, program: this.program });\n\n this.boundContextLost = this.onContextLost.bind(this);\n this.canvas.addEventListener('webglcontextlost', this.boundContextLost, false);\n\n this.resizeObserver = new ResizeObserver(() => this.resize());\n this.resizeObserver.observe(container);\n this.resize();\n\n this.loadTextures();\n\n this.boundLoop = this.loop.bind(this);\n this.raf = requestAnimationFrame(this.boundLoop);\n }\n\n private loadTextures(): void {\n this.items.forEach((item, index) => {\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.src = item.image;\n img.onload = () => {\n const texture = new Texture(this.gl, { generateMipmaps: false });\n texture.image = img;\n this.textures[index] = texture;\n this.sizes[index] = [img.naturalWidth || 1, img.naturalHeight || 1];\n if (index === this.current) {\n this.program.uniforms.tCurrent.value = texture;\n this.program.uniforms.uCurrentSize.value = this.sizes[index];\n }\n };\n img.onerror = () => {};\n });\n }\n\n private resize(): void {\n const rect = this.container.getBoundingClientRect();\n const w = Math.max(rect.width, 1);\n const h = Math.max(rect.height, 1);\n this.renderer.setSize(w, h);\n this.program.uniforms.uResolution.value = [this.gl.canvas.width, this.gl.canvas.height];\n }\n\n private syncOptions(): void {\n const opts = this.getOptions();\n this.program.uniforms.uMode.value = TRANSITIONS[opts.transition] ?? 0;\n this.program.uniforms.uIntensity.value = opts.intensity;\n this.program.uniforms.uScale.value = opts.scale;\n this.program.uniforms.uAberration.value = opts.aberration;\n this.program.uniforms.uDrift.value = opts.drift;\n this.program.uniforms.uOverlay.value = hexToRgb(opts.overlayColor);\n }\n\n private loop(t: number): void {\n this.program.uniforms.uTime.value = t * 0.001;\n if (!this.dragging && !this.animating) this.syncOptions();\n this.renderer.render({ scene: this.mesh });\n this.raf = requestAnimationFrame(this.boundLoop);\n }\n\n private wrap(i: number): number {\n const n = this.items.length;\n return ((i % n) + n) % n;\n }\n\n private prepareNext(dir: number): number {\n const target = this.wrap(this.current + dir);\n this.program.uniforms.tCurrent.value = this.textures[this.current];\n this.program.uniforms.uCurrentSize.value = this.sizes[this.current];\n this.program.uniforms.tNext.value = this.textures[target];\n this.program.uniforms.uNextSize.value = this.sizes[target];\n this.program.uniforms.uDir.value = dir;\n return target;\n }\n\n goTo(dir: number): void {\n if (this.animating || this.dragging || this.items.length < 2) return;\n const opts = this.getOptions();\n if (!opts.loop) {\n const raw = this.current + dir;\n if (raw < 0 || raw > this.items.length - 1) return;\n }\n this.syncOptions();\n const target = this.prepareNext(dir);\n this.animating = true;\n this.announce(target);\n const duration = this.reducedMotion ? Math.min(opts.duration, 0.4) : opts.duration;\n this.tween = gsap.fromTo(\n this.program.uniforms.uProgress,\n { value: 0 },\n {\n value: 1,\n duration,\n ease: opts.ease,\n onComplete: () => this.commit(target)\n }\n );\n }\n\n private announce(index: number): void {\n if (index === this.shownIndex) return;\n this.shownIndex = index;\n this.onIndexChange(index);\n }\n\n private commit(target: number): void {\n this.current = target;\n this.program.uniforms.tCurrent.value = this.textures[target];\n this.program.uniforms.uCurrentSize.value = this.sizes[target];\n this.program.uniforms.uProgress.value = 0;\n this.animating = false;\n this.tween = null;\n this.announce(target);\n }\n\n next(): void {\n this.goTo(1);\n }\n\n prev(): void {\n this.goTo(-1);\n }\n\n setPointer(x: number, y: number): void {\n this.program.uniforms.uPointer.value = [x, y];\n }\n\n beginDrag(): boolean {\n if (this.animating || this.items.length < 2) return false;\n this.dragging = true;\n this.dragDir = 0;\n this.syncOptions();\n return true;\n }\n\n drag(ndx: number): void {\n if (!this.dragging) return;\n const opts = this.getOptions();\n const dir = ndx < 0 ? 1 : -1;\n if (!opts.loop) {\n const raw = this.current + dir;\n if (raw < 0 || raw > this.items.length - 1) {\n this.program.uniforms.uProgress.value = 0;\n return;\n }\n }\n if (dir !== this.dragDir) {\n this.dragDir = dir;\n this.prepareNext(dir);\n }\n const progress = Math.min(Math.abs(ndx), 1);\n this.program.uniforms.uProgress.value = progress;\n this.announce(progress > 0.5 ? this.wrap(this.current + dir) : this.current);\n }\n\n endDrag(): void {\n if (!this.dragging) return;\n this.dragging = false;\n const p = this.program.uniforms.uProgress.value as number;\n if (this.dragDir === 0) return;\n const target = this.wrap(this.current + this.dragDir);\n const duration = this.reducedMotion ? 0.3 : 0.5;\n this.animating = true;\n if (p > 0.4) {\n this.announce(target);\n this.tween = gsap.to(this.program.uniforms.uProgress, {\n value: 1,\n duration,\n ease: 'power2.out',\n onComplete: () => this.commit(target)\n });\n } else {\n this.announce(this.current);\n this.tween = gsap.to(this.program.uniforms.uProgress, {\n value: 0,\n duration,\n ease: 'power2.out',\n onComplete: () => {\n this.animating = false;\n this.tween = null;\n }\n });\n }\n }\n\n private onContextLost(e: Event): void {\n e.preventDefault();\n cancelAnimationFrame(this.raf);\n }\n\n destroy(): void {\n cancelAnimationFrame(this.raf);\n if (this.tween) this.tween.kill();\n this.resizeObserver.disconnect();\n this.canvas.removeEventListener('webglcontextlost', this.boundContextLost);\n this.textures.forEach(tex => {\n if (tex && tex.texture) this.gl.deleteTexture(tex.texture);\n });\n if (this.program && this.program.program) this.gl.deleteProgram(this.program.program);\n const ext = this.gl.getExtension('WEBGL_lose_context');\n if (ext) ext.loseContext();\n if (this.canvas.parentNode) this.canvas.parentNode.removeChild(this.canvas);\n }\n}\n\nexport default function MorphSlider({\n items = DEFAULT_ITEMS,\n startIndex = 0,\n transition = 'melt',\n duration = 1.1,\n ease = 'power2.inOut',\n intensity = 0.55,\n scale = 2.4,\n aberration = 0.35,\n drift = 0.4,\n autoplay = false,\n autoplayDelay = 4,\n loop = true,\n radius = 16,\n overlayColor = '#000000',\n showCaptions = true,\n showControls = true,\n showIndicators = true,\n className = '',\n ...props\n}: MorphSliderProps) {\n const containerRef = useRef(null);\n const engineRef = useRef(null);\n const [index, setIndex] = useState(startIndex);\n const [hovering, setHovering] = useState(false);\n\n const optsRef = useRef({\n transition,\n duration,\n ease,\n intensity,\n scale,\n aberration,\n drift,\n overlayColor,\n loop\n });\n optsRef.current = { transition, duration, ease, intensity, scale, aberration, drift, overlayColor, loop };\n\n useEffect(() => {\n if (!containerRef.current) return undefined;\n const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n const engine = new MorphEngine(containerRef.current, {\n items,\n startIndex,\n reducedMotion,\n dprCap: 2,\n getOptions: () => optsRef.current,\n onIndexChange: setIndex\n });\n engineRef.current = engine;\n setIndex(startIndex);\n\n return () => {\n engine.destroy();\n engineRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [items, startIndex]);\n\n const handleNext = useCallback(() => engineRef.current?.next(), []);\n const handlePrev = useCallback(() => engineRef.current?.prev(), []);\n\n useEffect(() => {\n if (!autoplay || hovering) return undefined;\n const id = window.setTimeout(() => engineRef.current?.next(), Math.max(autoplayDelay, 1) * 1000);\n return () => window.clearTimeout(id);\n }, [autoplay, autoplayDelay, hovering, index]);\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return undefined;\n let startX = 0;\n let width = 1;\n let active = false;\n\n const onDown = (e: PointerEvent) => {\n const rect = el.getBoundingClientRect();\n width = rect.width || 1;\n startX = e.clientX;\n const px = (e.clientX - rect.left) / rect.width;\n const py = (e.clientY - rect.top) / rect.height;\n engineRef.current?.setPointer(px, 1 - py);\n active = engineRef.current?.beginDrag() ?? false;\n if (active && el.setPointerCapture) {\n try {\n el.setPointerCapture(e.pointerId);\n } catch {}\n }\n };\n const onMove = (e: PointerEvent) => {\n if (!active) return;\n const ndx = (e.clientX - startX) / width;\n engineRef.current?.drag(ndx);\n };\n const onUp = () => {\n if (!active) return;\n active = false;\n engineRef.current?.endDrag();\n };\n\n el.addEventListener('pointerdown', onDown);\n el.addEventListener('pointermove', onMove);\n el.addEventListener('pointerup', onUp);\n el.addEventListener('pointercancel', onUp);\n\n return () => {\n el.removeEventListener('pointerdown', onDown);\n el.removeEventListener('pointermove', onMove);\n el.removeEventListener('pointerup', onUp);\n el.removeEventListener('pointercancel', onUp);\n };\n }, []);\n\n const onKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === 'ArrowRight') {\n e.preventDefault();\n handleNext();\n } else if (e.key === 'ArrowLeft') {\n e.preventDefault();\n handlePrev();\n }\n },\n [handleNext, handlePrev]\n );\n\n const hasCaptions = items.some(item => item.caption);\n\n return (\n setHovering(true)}\n onMouseLeave={() => setHovering(false)}\n {...props}\n >\n \n\n {showCaptions && hasCaptions && (\n
    \n {items.map((item, i) =>\n item.caption ? (\n \n {item.caption}\n \n ) : null\n )}\n
    \n )}\n\n {showControls && (\n
    \n \n \n
    \n )}\n\n {showIndicators && (\n
    \n {items.map((item, i) => (\n {\n const engine = engineRef.current;\n if (!engine || i === index) return;\n engine.goTo(i > index ? 1 : -1);\n }}\n />\n ))}\n
    \n )}\n
    \n );\n}\n" } ], diff --git a/public/r/Noise-JS-CSS.json b/public/r/Noise-JS-CSS.json index e5730a79..e5afeffa 100644 --- a/public/r/Noise-JS-CSS.json +++ b/public/r/Noise-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Noise/Noise.css", + "type": "registry:file", + "path": "Noise.css", + "target": "@components/Noise.css", "content": ".noise-overlay {\n position: absolute;\n left: 0;\n top: 0;\n width: 100vw;\n height: 100vh;\n pointer-events: none;\n}\n" }, { "type": "registry:component", - "path": "Noise/Noise.jsx", + "path": "Noise.jsx", "content": "import { useRef, useEffect } from 'react';\nimport './Noise.css';\n\nconst Noise = ({\n patternSize = 250,\n patternScaleX = 1,\n patternScaleY = 1,\n patternRefreshInterval = 2,\n patternAlpha = 15\n}) => {\n const grainRef = useRef(null);\n\n useEffect(() => {\n const canvas = grainRef.current;\n if (!canvas) return;\n\n const ctx = canvas.getContext('2d', { alpha: true });\n if (!ctx) return;\n\n let frame = 0;\n let animationId;\n const canvasSize = 1024;\n\n const resize = () => {\n if (!canvas) return;\n canvas.width = canvasSize;\n canvas.height = canvasSize;\n\n canvas.style.width = '100vw';\n canvas.style.height = '100vh';\n };\n\n const drawGrain = () => {\n const imageData = ctx.createImageData(canvasSize, canvasSize);\n const data = imageData.data;\n\n for (let i = 0; i < data.length; i += 4) {\n const value = Math.random() * 255;\n data[i] = value;\n data[i + 1] = value;\n data[i + 2] = value;\n data[i + 3] = patternAlpha;\n }\n\n ctx.putImageData(imageData, 0, 0);\n };\n\n const loop = () => {\n if (frame % patternRefreshInterval === 0) {\n drawGrain();\n }\n frame++;\n animationId = window.requestAnimationFrame(loop);\n };\n\n window.addEventListener('resize', resize);\n resize();\n loop();\n\n return () => {\n window.removeEventListener('resize', resize);\n window.cancelAnimationFrame(animationId);\n };\n }, [patternSize, patternScaleX, patternScaleY, patternRefreshInterval, patternAlpha]);\n\n return ;\n};\n\nexport default Noise;\n" } ], diff --git a/public/r/Noise-TS-CSS.json b/public/r/Noise-TS-CSS.json index 8bdbfec0..95136cb6 100644 --- a/public/r/Noise-TS-CSS.json +++ b/public/r/Noise-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Noise/Noise.css", + "type": "registry:file", + "path": "Noise.css", + "target": "@components/Noise.css", "content": ".noise-overlay {\n position: absolute;\n left: 0;\n top: 0;\n width: 100vw;\n height: 100vh;\n pointer-events: none;\n}\n" }, { "type": "registry:component", - "path": "Noise/Noise.tsx", + "path": "Noise.tsx", "content": "import type React from 'react';\nimport { useRef, useEffect } from 'react';\nimport './Noise.css';\n\ninterface NoiseProps {\n patternSize?: number;\n patternScaleX?: number;\n patternScaleY?: number;\n patternRefreshInterval?: number;\n patternAlpha?: number;\n}\n\nconst Noise: React.FC = ({\n patternSize = 250,\n patternScaleX = 1,\n patternScaleY = 1,\n patternRefreshInterval = 2,\n patternAlpha = 15\n}) => {\n const grainRef = useRef(null);\n\n useEffect(() => {\n const canvas = grainRef.current;\n if (!canvas) return;\n\n const ctx = canvas.getContext('2d', { alpha: true });\n if (!ctx) return;\n\n let frame = 0;\n let animationId: number;\n const canvasSize = 1024;\n\n const resize = () => {\n if (!canvas) return;\n canvas.width = canvasSize;\n canvas.height = canvasSize;\n\n canvas.style.width = '100vw';\n canvas.style.height = '100vh';\n };\n\n const drawGrain = () => {\n const imageData = ctx.createImageData(canvasSize, canvasSize);\n const data = imageData.data;\n\n for (let i = 0; i < data.length; i += 4) {\n const value = Math.random() * 255;\n data[i] = value;\n data[i + 1] = value;\n data[i + 2] = value;\n data[i + 3] = patternAlpha;\n }\n\n ctx.putImageData(imageData, 0, 0);\n };\n\n const loop = () => {\n if (frame % patternRefreshInterval === 0) {\n drawGrain();\n }\n frame++;\n animationId = window.requestAnimationFrame(loop);\n };\n\n window.addEventListener('resize', resize);\n resize();\n loop();\n\n return () => {\n window.removeEventListener('resize', resize);\n window.cancelAnimationFrame(animationId);\n };\n }, [patternSize, patternScaleX, patternScaleY, patternRefreshInterval, patternAlpha]);\n\n return ;\n};\n\nexport default Noise;\n" } ], diff --git a/public/r/OptionWheel-JS-CSS.json b/public/r/OptionWheel-JS-CSS.json index b5ab029e..1cfa3e0e 100644 --- a/public/r/OptionWheel-JS-CSS.json +++ b/public/r/OptionWheel-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "OptionWheel/OptionWheel.css", + "type": "registry:file", + "path": "OptionWheel.css", + "target": "@components/OptionWheel.css", "content": ".option-wheel {\n --ow-text-color: #a6a6a6;\n --ow-active-color: #ffffff;\n --ow-font-size: 3rem;\n --ow-inset: 80px;\n\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n cursor: grab;\n user-select: none;\n touch-action: none;\n outline: none;\n}\n\n.option-wheel--dragging {\n cursor: grabbing;\n}\n\n/* Each option is absolutely centered, then offset along the curve by the\n rAF loop through transform/opacity/filter, so everything stays in step. */\n.option-wheel__item {\n position: absolute;\n top: 50%;\n left: var(--ow-inset);\n white-space: nowrap;\n font-size: var(--ow-font-size);\n line-height: 1;\n font-weight: 200;\n transform-origin: left center;\n cursor: pointer;\n will-change: transform, opacity, filter;\n /* --ow-p goes 0 -> 1 as an option approaches the middle of the wheel */\n color: color-mix(in srgb, var(--ow-active-color) calc(var(--ow-p, 0) * 100%), var(--ow-text-color));\n}\n\n.option-wheel--right .option-wheel__item {\n left: auto;\n right: var(--ow-inset);\n transform-origin: right center;\n}\n\n.option-wheel__item--selected {\n font-weight: 500;\n}\n" }, { "type": "registry:component", - "path": "OptionWheel/OptionWheel.jsx", + "path": "OptionWheel.jsx", "content": "import { useRef, useState, useCallback, useEffect } from 'react';\nimport './OptionWheel.css';\n\nconst DEFAULT_ITEMS = [\n 'Ambient',\n 'House',\n 'Techno',\n 'Jazz',\n 'Lo-Fi',\n 'Synthwave',\n 'Trance',\n 'Funk',\n 'Disco',\n 'Hip-Hop',\n 'Chillwave',\n 'Drum & Bass'\n];\n\nconst OptionWheel = ({\n items = DEFAULT_ITEMS,\n defaultSelected = 3,\n onChange,\n textColor = '#a6a6a6',\n activeColor = '#ffffff',\n side = 'left',\n fontSize = 3,\n spacing = 1.4,\n curve = 1,\n tilt = 6,\n blur = 2,\n fade = 0.25,\n minOpacity = 0.05,\n smoothing = 200,\n inset = 80,\n loop = false,\n draggable = true,\n soundUrl = '',\n soundVolume = 0.5,\n className = ''\n}) => {\n const rootRef = useRef(null);\n const itemRefs = useRef([]);\n const posRef = useRef(defaultSelected);\n const targetRef = useRef(defaultSelected);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const cfgRef = useRef({});\n const onChangeRef = useRef(onChange);\n const selectedRef = useRef(defaultSelected);\n const wheelTimerRef = useRef(null);\n const dragRef = useRef(null);\n const dragMovedRef = useRef(false);\n const audioRef = useRef(null);\n const audioUrlRef = useRef('');\n const lastTickRef = useRef(0);\n const [selectedIndex, setSelectedIndex] = useState(defaultSelected);\n const [isDragging, setIsDragging] = useState(false);\n\n const remPx = typeof window !== 'undefined' ? parseFloat(getComputedStyle(document.documentElement).fontSize) || 16 : 16;\n\n onChangeRef.current = onChange;\n cfgRef.current = {\n count: items.length,\n items,\n rowH: Math.max(fontSize * spacing * remPx, 1),\n curve,\n tilt,\n blur,\n fade,\n minOpacity,\n side,\n loop,\n smoothing,\n draggable,\n soundUrl,\n soundVolume\n };\n\n // Single rAF loop that eases the wheel position toward its target with\n // frame-rate independent exponential smoothing, then lays every option out\n // along the curve based on its distance from the current position.\n const runFrame = useCallback(now => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const cfg = cfgRef.current;\n const tau = Math.max(cfg.smoothing, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n const target = targetRef.current;\n const cur = posRef.current;\n let next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.001;\n if (settled) next = target;\n posRef.current = next;\n\n const els = itemRefs.current;\n const n = cfg.count;\n const mirror = cfg.side === 'right' ? -1 : 1;\n // Options sit on a circle whose radius keeps the arc length between two\n // neighbors equal to one row height, so tilt controls how tightly it curls.\n const tiltRad = (cfg.tilt * Math.PI) / 180;\n const R = tiltRad > 0.0005 ? cfg.rowH / tiltRad : 0;\n for (let i = 0; i < n; i++) {\n const el = els[i];\n if (!el) continue;\n let d = i - next;\n if (cfg.loop && n > 1) {\n d = ((d % n) + n) % n;\n if (d > n / 2) d -= n;\n }\n const dist = Math.abs(d);\n let x = 0;\n let y = d * cfg.rowH;\n let rot = 0;\n if (R > 0) {\n const ang = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, d * tiltRad));\n y = R * Math.sin(ang);\n x = -mirror * R * (1 - Math.cos(ang)) * cfg.curve;\n rot = (mirror * ang * 180) / Math.PI;\n }\n el.style.transform = `translate(${x.toFixed(2)}px, calc(${y.toFixed(2)}px - 50%)) rotate(${rot.toFixed(3)}deg)`;\n el.style.opacity = String(Math.max(cfg.minOpacity, 1 - dist * cfg.fade));\n el.style.filter = cfg.blur > 0 ? `blur(${(dist * cfg.blur).toFixed(2)}px)` : 'none';\n el.style.setProperty('--ow-p', Math.max(0, 1 - Math.min(dist, 1)).toFixed(4));\n }\n\n rafRef.current = settled ? null : requestAnimationFrame(runFrame);\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) {\n cancelAnimationFrame(rafRef.current);\n }\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n // Optional tick on selection change, throttled so fast scrolling can't spam\n // it, and with playback failures (e.g. autoplay policies) silently ignored.\n const playTick = useCallback(() => {\n const { soundUrl, soundVolume } = cfgRef.current;\n if (!soundUrl) return;\n const now = performance.now();\n if (now - lastTickRef.current < 70) return;\n lastTickRef.current = now;\n if (!audioRef.current || audioUrlRef.current !== soundUrl) {\n audioRef.current = new Audio(soundUrl);\n audioRef.current.preload = 'auto';\n audioUrlRef.current = soundUrl;\n }\n const audio = audioRef.current;\n audio.volume = Math.min(Math.max(soundVolume, 0), 1);\n audio.currentTime = 0;\n audio.play()?.catch(() => {});\n }, []);\n\n const applyTarget = useCallback(\n (value, snap) => {\n const cfg = cfgRef.current;\n let v = value;\n if (!cfg.loop) v = Math.min(Math.max(v, 0), Math.max(cfg.count - 1, 0));\n if (snap) v = Math.round(v);\n targetRef.current = v;\n const idx = ((Math.round(v) % cfg.count) + cfg.count) % cfg.count;\n if (idx !== selectedRef.current) {\n selectedRef.current = idx;\n setSelectedIndex(idx);\n onChangeRef.current?.(idx, cfg.items[idx]);\n playTick();\n }\n startLoop();\n },\n [startLoop, playTick]\n );\n\n // Wheel / touchpad scrolling, registered manually so it can be non-passive.\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const onWheel = e => {\n e.preventDefault();\n const cfg = cfgRef.current;\n const delta = e.deltaMode === 1 ? e.deltaY * 24 : e.deltaY;\n // Cap each event at one step so notchy mouse wheels move exactly one\n // option per click, while touchpads still scroll continuously.\n const step = Math.max(-1, Math.min(1, delta / cfg.rowH));\n applyTarget(targetRef.current + step, false);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n wheelTimerRef.current = setTimeout(() => applyTarget(targetRef.current, true), 140);\n };\n el.addEventListener('wheel', onWheel, { passive: false });\n return () => {\n el.removeEventListener('wheel', onWheel);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n };\n }, [applyTarget]);\n\n const handlePointerDown = useCallback(e => {\n if (!cfgRef.current.draggable) return;\n dragRef.current = { y: e.clientY, start: targetRef.current, id: e.pointerId };\n dragMovedRef.current = false;\n setIsDragging(true);\n }, []);\n\n const handlePointerMove = useCallback(\n e => {\n const drag = dragRef.current;\n if (!drag) return;\n const dy = e.clientY - drag.y;\n if (!dragMovedRef.current && Math.abs(dy) > 4) {\n dragMovedRef.current = true;\n // Capture only once a real drag starts, so plain clicks still reach\n // the items and navigate to them.\n rootRef.current?.setPointerCapture(drag.id);\n }\n if (dragMovedRef.current) applyTarget(drag.start - dy / cfgRef.current.rowH, false);\n },\n [applyTarget]\n );\n\n const handlePointerEnd = useCallback(() => {\n if (!dragRef.current) return;\n dragRef.current = null;\n setIsDragging(false);\n if (dragMovedRef.current) applyTarget(targetRef.current, true);\n }, [applyTarget]);\n\n const handleItemClick = useCallback(\n index => {\n if (dragMovedRef.current) return;\n const cfg = cfgRef.current;\n const cur = targetRef.current;\n let d = index - (((cur % cfg.count) + cfg.count) % cfg.count);\n if (cfg.loop && cfg.count > 1) {\n if (d > cfg.count / 2) d -= cfg.count;\n else if (d < -cfg.count / 2) d += cfg.count;\n }\n applyTarget(cur + d, true);\n },\n [applyTarget]\n );\n\n const handleKeyDown = useCallback(\n e => {\n let delta = null;\n if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') delta = -1;\n else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') delta = 1;\n if (delta == null) return;\n e.preventDefault();\n applyTarget(Math.round(targetRef.current) + delta, true);\n },\n [applyTarget]\n );\n\n useEffect(() => {\n applyTarget(targetRef.current, false);\n }, [items, fontSize, spacing, curve, tilt, blur, fade, minOpacity, side, loop, smoothing, applyTarget]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n audioRef.current?.pause();\n },\n []\n );\n\n return (\n \n {items.map((label, index) => (\n {\n itemRefs.current[index] = el;\n }}\n role=\"option\"\n aria-selected={selectedIndex === index}\n className={`option-wheel__item${selectedIndex === index ? ' option-wheel__item--selected' : ''}`}\n onClick={() => handleItemClick(index)}\n >\n {label}\n \n ))}\n \n );\n};\n\nexport default OptionWheel;\n" } ], diff --git a/public/r/OptionWheel-TS-CSS.json b/public/r/OptionWheel-TS-CSS.json index 56904bd3..189b6c83 100644 --- a/public/r/OptionWheel-TS-CSS.json +++ b/public/r/OptionWheel-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "OptionWheel/OptionWheel.css", + "type": "registry:file", + "path": "OptionWheel.css", + "target": "@components/OptionWheel.css", "content": ".option-wheel {\n --ow-text-color: #a6a6a6;\n --ow-active-color: #ffffff;\n --ow-font-size: 3rem;\n --ow-inset: 80px;\n\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n cursor: grab;\n user-select: none;\n touch-action: none;\n outline: none;\n}\n\n.option-wheel--dragging {\n cursor: grabbing;\n}\n\n/* Each option is absolutely centered, then offset along the curve by the\n rAF loop through transform/opacity/filter, so everything stays in step. */\n.option-wheel__item {\n position: absolute;\n top: 50%;\n left: var(--ow-inset);\n white-space: nowrap;\n font-size: var(--ow-font-size);\n line-height: 1;\n font-weight: 200;\n transform-origin: left center;\n cursor: pointer;\n will-change: transform, opacity, filter;\n /* --ow-p goes 0 -> 1 as an option approaches the middle of the wheel */\n color: color-mix(in srgb, var(--ow-active-color) calc(var(--ow-p, 0) * 100%), var(--ow-text-color));\n}\n\n.option-wheel--right .option-wheel__item {\n left: auto;\n right: var(--ow-inset);\n transform-origin: right center;\n}\n\n.option-wheel__item--selected {\n font-weight: 500;\n}\n" }, { "type": "registry:component", - "path": "OptionWheel/OptionWheel.tsx", - "content": "import { useRef, useState, useCallback, useEffect, CSSProperties } from 'react';\nimport './OptionWheel.css';\n\ntype Side = 'left' | 'right';\n\nexport interface OptionWheelProps {\n items?: string[];\n defaultSelected?: number;\n onChange?: (index: number, item: string) => void;\n textColor?: string;\n activeColor?: string;\n side?: Side;\n fontSize?: number;\n spacing?: number;\n curve?: number;\n tilt?: number;\n blur?: number;\n fade?: number;\n minOpacity?: number;\n smoothing?: number;\n inset?: number;\n loop?: boolean;\n draggable?: boolean;\n soundUrl?: string;\n soundVolume?: number;\n className?: string;\n}\n\ninterface WheelConfig {\n count: number;\n items: string[];\n rowH: number;\n curve: number;\n tilt: number;\n blur: number;\n fade: number;\n minOpacity: number;\n side: Side;\n loop: boolean;\n smoothing: number;\n draggable: boolean;\n soundUrl: string;\n soundVolume: number;\n}\n\nconst DEFAULT_ITEMS = [\n 'Ambient',\n 'House',\n 'Techno',\n 'Jazz',\n 'Lo-Fi',\n 'Synthwave',\n 'Trance',\n 'Funk',\n 'Disco',\n 'Hip-Hop',\n 'Chillwave',\n 'Drum & Bass'\n];\n\nconst OptionWheel = ({\n items = DEFAULT_ITEMS,\n defaultSelected = 3,\n onChange,\n textColor = '#a6a6a6',\n activeColor = '#ffffff',\n side = 'left',\n fontSize = 3,\n spacing = 1.4,\n curve = 1,\n tilt = 6,\n blur = 2,\n fade = 0.25,\n minOpacity = 0.05,\n smoothing = 200,\n inset = 80,\n loop = false,\n draggable = true,\n soundUrl = '',\n soundVolume = 0.5,\n className = ''\n}: OptionWheelProps) => {\n const rootRef = useRef(null);\n const itemRefs = useRef<(HTMLDivElement | null)[]>([]);\n const posRef = useRef(defaultSelected);\n const targetRef = useRef(defaultSelected);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const cfgRef = useRef({} as WheelConfig);\n const onChangeRef = useRef(onChange);\n const selectedRef = useRef(defaultSelected);\n const wheelTimerRef = useRef | null>(null);\n const dragRef = useRef<{ y: number; start: number; id: number } | null>(null);\n const dragMovedRef = useRef(false);\n const audioRef = useRef(null);\n const audioUrlRef = useRef('');\n const lastTickRef = useRef(0);\n const [selectedIndex, setSelectedIndex] = useState(defaultSelected);\n const [isDragging, setIsDragging] = useState(false);\n\n const remPx = typeof window !== 'undefined' ? parseFloat(getComputedStyle(document.documentElement).fontSize) || 16 : 16;\n\n onChangeRef.current = onChange;\n cfgRef.current = {\n count: items.length,\n items,\n rowH: Math.max(fontSize * spacing * remPx, 1),\n curve,\n tilt,\n blur,\n fade,\n minOpacity,\n side,\n loop,\n smoothing,\n draggable,\n soundUrl,\n soundVolume\n };\n\n // Single rAF loop that eases the wheel position toward its target with\n // frame-rate independent exponential smoothing, then lays every option out\n // along the curve based on its distance from the current position.\n const runFrame = useCallback((now: number) => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const cfg = cfgRef.current;\n const tau = Math.max(cfg.smoothing, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n const target = targetRef.current;\n const cur = posRef.current;\n let next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.001;\n if (settled) next = target;\n posRef.current = next;\n\n const els = itemRefs.current;\n const n = cfg.count;\n const mirror = cfg.side === 'right' ? -1 : 1;\n // Options sit on a circle whose radius keeps the arc length between two\n // neighbors equal to one row height, so tilt controls how tightly it curls.\n const tiltRad = (cfg.tilt * Math.PI) / 180;\n const R = tiltRad > 0.0005 ? cfg.rowH / tiltRad : 0;\n for (let i = 0; i < n; i++) {\n const el = els[i];\n if (!el) continue;\n let d = i - next;\n if (cfg.loop && n > 1) {\n d = ((d % n) + n) % n;\n if (d > n / 2) d -= n;\n }\n const dist = Math.abs(d);\n let x = 0;\n let y = d * cfg.rowH;\n let rot = 0;\n if (R > 0) {\n const ang = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, d * tiltRad));\n y = R * Math.sin(ang);\n x = -mirror * R * (1 - Math.cos(ang)) * cfg.curve;\n rot = (mirror * ang * 180) / Math.PI;\n }\n el.style.transform = `translate(${x.toFixed(2)}px, calc(${y.toFixed(2)}px - 50%)) rotate(${rot.toFixed(3)}deg)`;\n el.style.opacity = String(Math.max(cfg.minOpacity, 1 - dist * cfg.fade));\n el.style.filter = cfg.blur > 0 ? `blur(${(dist * cfg.blur).toFixed(2)}px)` : 'none';\n el.style.setProperty('--ow-p', Math.max(0, 1 - Math.min(dist, 1)).toFixed(4));\n }\n\n rafRef.current = settled ? null : requestAnimationFrame(runFrame);\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) {\n cancelAnimationFrame(rafRef.current);\n }\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n // Optional tick on selection change, throttled so fast scrolling can't spam\n // it, and with playback failures (e.g. autoplay policies) silently ignored.\n const playTick = useCallback(() => {\n const { soundUrl, soundVolume } = cfgRef.current;\n if (!soundUrl) return;\n const now = performance.now();\n if (now - lastTickRef.current < 70) return;\n lastTickRef.current = now;\n if (!audioRef.current || audioUrlRef.current !== soundUrl) {\n audioRef.current = new Audio(soundUrl);\n audioRef.current.preload = 'auto';\n audioUrlRef.current = soundUrl;\n }\n const audio = audioRef.current;\n audio.volume = Math.min(Math.max(soundVolume, 0), 1);\n audio.currentTime = 0;\n audio.play()?.catch(() => {});\n }, []);\n\n const applyTarget = useCallback(\n (value: number, snap: boolean) => {\n const cfg = cfgRef.current;\n let v = value;\n if (!cfg.loop) v = Math.min(Math.max(v, 0), Math.max(cfg.count - 1, 0));\n if (snap) v = Math.round(v);\n targetRef.current = v;\n const idx = ((Math.round(v) % cfg.count) + cfg.count) % cfg.count;\n if (idx !== selectedRef.current) {\n selectedRef.current = idx;\n setSelectedIndex(idx);\n onChangeRef.current?.(idx, cfg.items[idx]);\n playTick();\n }\n startLoop();\n },\n [startLoop, playTick]\n );\n\n // Wheel / touchpad scrolling, registered manually so it can be non-passive.\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const onWheel = (e: WheelEvent) => {\n e.preventDefault();\n const cfg = cfgRef.current;\n const delta = e.deltaMode === 1 ? e.deltaY * 24 : e.deltaY;\n // Cap each event at one step so notchy mouse wheels move exactly one\n // option per click, while touchpads still scroll continuously.\n const step = Math.max(-1, Math.min(1, delta / cfg.rowH));\n applyTarget(targetRef.current + step, false);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n wheelTimerRef.current = setTimeout(() => applyTarget(targetRef.current, true), 140);\n };\n el.addEventListener('wheel', onWheel, { passive: false });\n return () => {\n el.removeEventListener('wheel', onWheel);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n };\n }, [applyTarget]);\n\n const handlePointerDown = useCallback((e: React.PointerEvent) => {\n if (!cfgRef.current.draggable) return;\n dragRef.current = { y: e.clientY, start: targetRef.current, id: e.pointerId };\n dragMovedRef.current = false;\n setIsDragging(true);\n }, []);\n\n const handlePointerMove = useCallback(\n (e: React.PointerEvent) => {\n const drag = dragRef.current;\n if (!drag) return;\n const dy = e.clientY - drag.y;\n if (!dragMovedRef.current && Math.abs(dy) > 4) {\n dragMovedRef.current = true;\n // Capture only once a real drag starts, so plain clicks still reach\n // the items and navigate to them.\n rootRef.current?.setPointerCapture(drag.id);\n }\n if (dragMovedRef.current) applyTarget(drag.start - dy / cfgRef.current.rowH, false);\n },\n [applyTarget]\n );\n\n const handlePointerEnd = useCallback(() => {\n if (!dragRef.current) return;\n dragRef.current = null;\n setIsDragging(false);\n if (dragMovedRef.current) applyTarget(targetRef.current, true);\n }, [applyTarget]);\n\n const handleItemClick = useCallback(\n (index: number) => {\n if (dragMovedRef.current) return;\n const cfg = cfgRef.current;\n const cur = targetRef.current;\n let d = index - (((cur % cfg.count) + cfg.count) % cfg.count);\n if (cfg.loop && cfg.count > 1) {\n if (d > cfg.count / 2) d -= cfg.count;\n else if (d < -cfg.count / 2) d += cfg.count;\n }\n applyTarget(cur + d, true);\n },\n [applyTarget]\n );\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n let delta: number | null = null;\n if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') delta = -1;\n else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') delta = 1;\n if (delta == null) return;\n e.preventDefault();\n applyTarget(Math.round(targetRef.current) + delta, true);\n },\n [applyTarget]\n );\n\n useEffect(() => {\n applyTarget(targetRef.current, false);\n }, [items, fontSize, spacing, curve, tilt, blur, fade, minOpacity, side, loop, smoothing, applyTarget]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n audioRef.current?.pause();\n },\n []\n );\n\n return (\n \n {items.map((label, index) => (\n {\n itemRefs.current[index] = el;\n }}\n role=\"option\"\n aria-selected={selectedIndex === index}\n className={`option-wheel__item${selectedIndex === index ? ' option-wheel__item--selected' : ''}`}\n onClick={() => handleItemClick(index)}\n >\n {label}\n \n ))}\n \n );\n};\n\nexport default OptionWheel;\n" + "path": "OptionWheel.tsx", + "content": "import { useRef, useState, useCallback, useEffect, type CSSProperties } from 'react';\nimport './OptionWheel.css';\n\ntype Side = 'left' | 'right';\n\nexport interface OptionWheelProps {\n items?: string[];\n defaultSelected?: number;\n onChange?: (index: number, item: string) => void;\n textColor?: string;\n activeColor?: string;\n side?: Side;\n fontSize?: number;\n spacing?: number;\n curve?: number;\n tilt?: number;\n blur?: number;\n fade?: number;\n minOpacity?: number;\n smoothing?: number;\n inset?: number;\n loop?: boolean;\n draggable?: boolean;\n soundUrl?: string;\n soundVolume?: number;\n className?: string;\n}\n\ninterface WheelConfig {\n count: number;\n items: string[];\n rowH: number;\n curve: number;\n tilt: number;\n blur: number;\n fade: number;\n minOpacity: number;\n side: Side;\n loop: boolean;\n smoothing: number;\n draggable: boolean;\n soundUrl: string;\n soundVolume: number;\n}\n\nconst DEFAULT_ITEMS = [\n 'Ambient',\n 'House',\n 'Techno',\n 'Jazz',\n 'Lo-Fi',\n 'Synthwave',\n 'Trance',\n 'Funk',\n 'Disco',\n 'Hip-Hop',\n 'Chillwave',\n 'Drum & Bass'\n];\n\nconst OptionWheel = ({\n items = DEFAULT_ITEMS,\n defaultSelected = 3,\n onChange,\n textColor = '#a6a6a6',\n activeColor = '#ffffff',\n side = 'left',\n fontSize = 3,\n spacing = 1.4,\n curve = 1,\n tilt = 6,\n blur = 2,\n fade = 0.25,\n minOpacity = 0.05,\n smoothing = 200,\n inset = 80,\n loop = false,\n draggable = true,\n soundUrl = '',\n soundVolume = 0.5,\n className = ''\n}: OptionWheelProps) => {\n const rootRef = useRef(null);\n const itemRefs = useRef<(HTMLDivElement | null)[]>([]);\n const posRef = useRef(defaultSelected);\n const targetRef = useRef(defaultSelected);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const cfgRef = useRef({} as WheelConfig);\n const onChangeRef = useRef(onChange);\n const selectedRef = useRef(defaultSelected);\n const wheelTimerRef = useRef | null>(null);\n const dragRef = useRef<{ y: number; start: number; id: number } | null>(null);\n const dragMovedRef = useRef(false);\n const audioRef = useRef(null);\n const audioUrlRef = useRef('');\n const lastTickRef = useRef(0);\n const [selectedIndex, setSelectedIndex] = useState(defaultSelected);\n const [isDragging, setIsDragging] = useState(false);\n\n const remPx = typeof window !== 'undefined' ? parseFloat(getComputedStyle(document.documentElement).fontSize) || 16 : 16;\n\n onChangeRef.current = onChange;\n cfgRef.current = {\n count: items.length,\n items,\n rowH: Math.max(fontSize * spacing * remPx, 1),\n curve,\n tilt,\n blur,\n fade,\n minOpacity,\n side,\n loop,\n smoothing,\n draggable,\n soundUrl,\n soundVolume\n };\n\n // Single rAF loop that eases the wheel position toward its target with\n // frame-rate independent exponential smoothing, then lays every option out\n // along the curve based on its distance from the current position.\n const runFrame = useCallback((now: number) => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const cfg = cfgRef.current;\n const tau = Math.max(cfg.smoothing, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n const target = targetRef.current;\n const cur = posRef.current;\n let next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.001;\n if (settled) next = target;\n posRef.current = next;\n\n const els = itemRefs.current;\n const n = cfg.count;\n const mirror = cfg.side === 'right' ? -1 : 1;\n // Options sit on a circle whose radius keeps the arc length between two\n // neighbors equal to one row height, so tilt controls how tightly it curls.\n const tiltRad = (cfg.tilt * Math.PI) / 180;\n const R = tiltRad > 0.0005 ? cfg.rowH / tiltRad : 0;\n for (let i = 0; i < n; i++) {\n const el = els[i];\n if (!el) continue;\n let d = i - next;\n if (cfg.loop && n > 1) {\n d = ((d % n) + n) % n;\n if (d > n / 2) d -= n;\n }\n const dist = Math.abs(d);\n let x = 0;\n let y = d * cfg.rowH;\n let rot = 0;\n if (R > 0) {\n const ang = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, d * tiltRad));\n y = R * Math.sin(ang);\n x = -mirror * R * (1 - Math.cos(ang)) * cfg.curve;\n rot = (mirror * ang * 180) / Math.PI;\n }\n el.style.transform = `translate(${x.toFixed(2)}px, calc(${y.toFixed(2)}px - 50%)) rotate(${rot.toFixed(3)}deg)`;\n el.style.opacity = String(Math.max(cfg.minOpacity, 1 - dist * cfg.fade));\n el.style.filter = cfg.blur > 0 ? `blur(${(dist * cfg.blur).toFixed(2)}px)` : 'none';\n el.style.setProperty('--ow-p', Math.max(0, 1 - Math.min(dist, 1)).toFixed(4));\n }\n\n rafRef.current = settled ? null : requestAnimationFrame(runFrame);\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) {\n cancelAnimationFrame(rafRef.current);\n }\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n // Optional tick on selection change, throttled so fast scrolling can't spam\n // it, and with playback failures (e.g. autoplay policies) silently ignored.\n const playTick = useCallback(() => {\n const { soundUrl, soundVolume } = cfgRef.current;\n if (!soundUrl) return;\n const now = performance.now();\n if (now - lastTickRef.current < 70) return;\n lastTickRef.current = now;\n if (!audioRef.current || audioUrlRef.current !== soundUrl) {\n audioRef.current = new Audio(soundUrl);\n audioRef.current.preload = 'auto';\n audioUrlRef.current = soundUrl;\n }\n const audio = audioRef.current;\n audio.volume = Math.min(Math.max(soundVolume, 0), 1);\n audio.currentTime = 0;\n audio.play()?.catch(() => {});\n }, []);\n\n const applyTarget = useCallback(\n (value: number, snap: boolean) => {\n const cfg = cfgRef.current;\n let v = value;\n if (!cfg.loop) v = Math.min(Math.max(v, 0), Math.max(cfg.count - 1, 0));\n if (snap) v = Math.round(v);\n targetRef.current = v;\n const idx = ((Math.round(v) % cfg.count) + cfg.count) % cfg.count;\n if (idx !== selectedRef.current) {\n selectedRef.current = idx;\n setSelectedIndex(idx);\n onChangeRef.current?.(idx, cfg.items[idx]);\n playTick();\n }\n startLoop();\n },\n [startLoop, playTick]\n );\n\n // Wheel / touchpad scrolling, registered manually so it can be non-passive.\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const onWheel = (e: WheelEvent) => {\n e.preventDefault();\n const cfg = cfgRef.current;\n const delta = e.deltaMode === 1 ? e.deltaY * 24 : e.deltaY;\n // Cap each event at one step so notchy mouse wheels move exactly one\n // option per click, while touchpads still scroll continuously.\n const step = Math.max(-1, Math.min(1, delta / cfg.rowH));\n applyTarget(targetRef.current + step, false);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n wheelTimerRef.current = setTimeout(() => applyTarget(targetRef.current, true), 140);\n };\n el.addEventListener('wheel', onWheel, { passive: false });\n return () => {\n el.removeEventListener('wheel', onWheel);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n };\n }, [applyTarget]);\n\n const handlePointerDown = useCallback((e: React.PointerEvent) => {\n if (!cfgRef.current.draggable) return;\n dragRef.current = { y: e.clientY, start: targetRef.current, id: e.pointerId };\n dragMovedRef.current = false;\n setIsDragging(true);\n }, []);\n\n const handlePointerMove = useCallback(\n (e: React.PointerEvent) => {\n const drag = dragRef.current;\n if (!drag) return;\n const dy = e.clientY - drag.y;\n if (!dragMovedRef.current && Math.abs(dy) > 4) {\n dragMovedRef.current = true;\n // Capture only once a real drag starts, so plain clicks still reach\n // the items and navigate to them.\n rootRef.current?.setPointerCapture(drag.id);\n }\n if (dragMovedRef.current) applyTarget(drag.start - dy / cfgRef.current.rowH, false);\n },\n [applyTarget]\n );\n\n const handlePointerEnd = useCallback(() => {\n if (!dragRef.current) return;\n dragRef.current = null;\n setIsDragging(false);\n if (dragMovedRef.current) applyTarget(targetRef.current, true);\n }, [applyTarget]);\n\n const handleItemClick = useCallback(\n (index: number) => {\n if (dragMovedRef.current) return;\n const cfg = cfgRef.current;\n const cur = targetRef.current;\n let d = index - (((cur % cfg.count) + cfg.count) % cfg.count);\n if (cfg.loop && cfg.count > 1) {\n if (d > cfg.count / 2) d -= cfg.count;\n else if (d < -cfg.count / 2) d += cfg.count;\n }\n applyTarget(cur + d, true);\n },\n [applyTarget]\n );\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n let delta: number | null = null;\n if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') delta = -1;\n else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') delta = 1;\n if (delta == null) return;\n e.preventDefault();\n applyTarget(Math.round(targetRef.current) + delta, true);\n },\n [applyTarget]\n );\n\n useEffect(() => {\n applyTarget(targetRef.current, false);\n }, [items, fontSize, spacing, curve, tilt, blur, fade, minOpacity, side, loop, smoothing, applyTarget]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n audioRef.current?.pause();\n },\n []\n );\n\n return (\n \n {items.map((label, index) => (\n {\n itemRefs.current[index] = el;\n }}\n role=\"option\"\n aria-selected={selectedIndex === index}\n className={`option-wheel__item${selectedIndex === index ? ' option-wheel__item--selected' : ''}`}\n onClick={() => handleItemClick(index)}\n >\n {label}\n \n ))}\n \n );\n};\n\nexport default OptionWheel;\n" } ], "registryDependencies": [], diff --git a/public/r/OptionWheel-TS-TW.json b/public/r/OptionWheel-TS-TW.json index ee412653..797fb576 100644 --- a/public/r/OptionWheel-TS-TW.json +++ b/public/r/OptionWheel-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "OptionWheel/OptionWheel.tsx", - "content": "import { useRef, useState, useCallback, useEffect, CSSProperties } from 'react';\n\ntype Side = 'left' | 'right';\n\nexport interface OptionWheelProps {\n items?: string[];\n defaultSelected?: number;\n onChange?: (index: number, item: string) => void;\n textColor?: string;\n activeColor?: string;\n side?: Side;\n fontSize?: number;\n spacing?: number;\n curve?: number;\n tilt?: number;\n blur?: number;\n fade?: number;\n minOpacity?: number;\n smoothing?: number;\n inset?: number;\n loop?: boolean;\n draggable?: boolean;\n soundUrl?: string;\n soundVolume?: number;\n className?: string;\n}\n\ninterface WheelConfig {\n count: number;\n items: string[];\n rowH: number;\n curve: number;\n tilt: number;\n blur: number;\n fade: number;\n minOpacity: number;\n side: Side;\n loop: boolean;\n smoothing: number;\n draggable: boolean;\n soundUrl: string;\n soundVolume: number;\n}\n\nconst DEFAULT_ITEMS = [\n 'Ambient',\n 'House',\n 'Techno',\n 'Jazz',\n 'Lo-Fi',\n 'Synthwave',\n 'Trance',\n 'Funk',\n 'Disco',\n 'Hip-Hop',\n 'Chillwave',\n 'Drum & Bass'\n];\n\nconst OptionWheel = ({\n items = DEFAULT_ITEMS,\n defaultSelected = 3,\n onChange,\n textColor = '#a6a6a6',\n activeColor = '#ffffff',\n side = 'left',\n fontSize = 3,\n spacing = 1.4,\n curve = 1,\n tilt = 6,\n blur = 2,\n fade = 0.25,\n minOpacity = 0.05,\n smoothing = 200,\n inset = 80,\n loop = false,\n draggable = true,\n soundUrl = '',\n soundVolume = 0.5,\n className = ''\n}: OptionWheelProps) => {\n const rootRef = useRef(null);\n const itemRefs = useRef<(HTMLDivElement | null)[]>([]);\n const posRef = useRef(defaultSelected);\n const targetRef = useRef(defaultSelected);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const cfgRef = useRef({} as WheelConfig);\n const onChangeRef = useRef(onChange);\n const selectedRef = useRef(defaultSelected);\n const wheelTimerRef = useRef | null>(null);\n const dragRef = useRef<{ y: number; start: number; id: number } | null>(null);\n const dragMovedRef = useRef(false);\n const audioRef = useRef(null);\n const audioUrlRef = useRef('');\n const lastTickRef = useRef(0);\n const [selectedIndex, setSelectedIndex] = useState(defaultSelected);\n const [isDragging, setIsDragging] = useState(false);\n\n const remPx = typeof window !== 'undefined' ? parseFloat(getComputedStyle(document.documentElement).fontSize) || 16 : 16;\n\n onChangeRef.current = onChange;\n cfgRef.current = {\n count: items.length,\n items,\n rowH: Math.max(fontSize * spacing * remPx, 1),\n curve,\n tilt,\n blur,\n fade,\n minOpacity,\n side,\n loop,\n smoothing,\n draggable,\n soundUrl,\n soundVolume\n };\n\n // Single rAF loop that eases the wheel position toward its target with\n // frame-rate independent exponential smoothing, then lays every option out\n // along the curve based on its distance from the current position.\n const runFrame = useCallback((now: number) => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const cfg = cfgRef.current;\n const tau = Math.max(cfg.smoothing, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n const target = targetRef.current;\n const cur = posRef.current;\n let next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.001;\n if (settled) next = target;\n posRef.current = next;\n\n const els = itemRefs.current;\n const n = cfg.count;\n const mirror = cfg.side === 'right' ? -1 : 1;\n // Options sit on a circle whose radius keeps the arc length between two\n // neighbors equal to one row height, so tilt controls how tightly it curls.\n const tiltRad = (cfg.tilt * Math.PI) / 180;\n const R = tiltRad > 0.0005 ? cfg.rowH / tiltRad : 0;\n for (let i = 0; i < n; i++) {\n const el = els[i];\n if (!el) continue;\n let d = i - next;\n if (cfg.loop && n > 1) {\n d = ((d % n) + n) % n;\n if (d > n / 2) d -= n;\n }\n const dist = Math.abs(d);\n let x = 0;\n let y = d * cfg.rowH;\n let rot = 0;\n if (R > 0) {\n const ang = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, d * tiltRad));\n y = R * Math.sin(ang);\n x = -mirror * R * (1 - Math.cos(ang)) * cfg.curve;\n rot = (mirror * ang * 180) / Math.PI;\n }\n el.style.transform = `translate(${x.toFixed(2)}px, calc(${y.toFixed(2)}px - 50%)) rotate(${rot.toFixed(3)}deg)`;\n el.style.opacity = String(Math.max(cfg.minOpacity, 1 - dist * cfg.fade));\n el.style.filter = cfg.blur > 0 ? `blur(${(dist * cfg.blur).toFixed(2)}px)` : 'none';\n el.style.setProperty('--ow-p', Math.max(0, 1 - Math.min(dist, 1)).toFixed(4));\n }\n\n rafRef.current = settled ? null : requestAnimationFrame(runFrame);\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) {\n cancelAnimationFrame(rafRef.current);\n }\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n // Optional tick on selection change, throttled so fast scrolling can't spam\n // it, and with playback failures (e.g. autoplay policies) silently ignored.\n const playTick = useCallback(() => {\n const { soundUrl, soundVolume } = cfgRef.current;\n if (!soundUrl) return;\n const now = performance.now();\n if (now - lastTickRef.current < 70) return;\n lastTickRef.current = now;\n if (!audioRef.current || audioUrlRef.current !== soundUrl) {\n audioRef.current = new Audio(soundUrl);\n audioRef.current.preload = 'auto';\n audioUrlRef.current = soundUrl;\n }\n const audio = audioRef.current;\n audio.volume = Math.min(Math.max(soundVolume, 0), 1);\n audio.currentTime = 0;\n audio.play()?.catch(() => {});\n }, []);\n\n const applyTarget = useCallback(\n (value: number, snap: boolean) => {\n const cfg = cfgRef.current;\n let v = value;\n if (!cfg.loop) v = Math.min(Math.max(v, 0), Math.max(cfg.count - 1, 0));\n if (snap) v = Math.round(v);\n targetRef.current = v;\n const idx = ((Math.round(v) % cfg.count) + cfg.count) % cfg.count;\n if (idx !== selectedRef.current) {\n selectedRef.current = idx;\n setSelectedIndex(idx);\n onChangeRef.current?.(idx, cfg.items[idx]);\n playTick();\n }\n startLoop();\n },\n [startLoop, playTick]\n );\n\n // Wheel / touchpad scrolling, registered manually so it can be non-passive.\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const onWheel = (e: WheelEvent) => {\n e.preventDefault();\n const cfg = cfgRef.current;\n const delta = e.deltaMode === 1 ? e.deltaY * 24 : e.deltaY;\n // Cap each event at one step so notchy mouse wheels move exactly one\n // option per click, while touchpads still scroll continuously.\n const step = Math.max(-1, Math.min(1, delta / cfg.rowH));\n applyTarget(targetRef.current + step, false);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n wheelTimerRef.current = setTimeout(() => applyTarget(targetRef.current, true), 140);\n };\n el.addEventListener('wheel', onWheel, { passive: false });\n return () => {\n el.removeEventListener('wheel', onWheel);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n };\n }, [applyTarget]);\n\n const handlePointerDown = useCallback((e: React.PointerEvent) => {\n if (!cfgRef.current.draggable) return;\n dragRef.current = { y: e.clientY, start: targetRef.current, id: e.pointerId };\n dragMovedRef.current = false;\n setIsDragging(true);\n }, []);\n\n const handlePointerMove = useCallback(\n (e: React.PointerEvent) => {\n const drag = dragRef.current;\n if (!drag) return;\n const dy = e.clientY - drag.y;\n if (!dragMovedRef.current && Math.abs(dy) > 4) {\n dragMovedRef.current = true;\n // Capture only once a real drag starts, so plain clicks still reach\n // the items and navigate to them.\n rootRef.current?.setPointerCapture(drag.id);\n }\n if (dragMovedRef.current) applyTarget(drag.start - dy / cfgRef.current.rowH, false);\n },\n [applyTarget]\n );\n\n const handlePointerEnd = useCallback(() => {\n if (!dragRef.current) return;\n dragRef.current = null;\n setIsDragging(false);\n if (dragMovedRef.current) applyTarget(targetRef.current, true);\n }, [applyTarget]);\n\n const handleItemClick = useCallback(\n (index: number) => {\n if (dragMovedRef.current) return;\n const cfg = cfgRef.current;\n const cur = targetRef.current;\n let d = index - (((cur % cfg.count) + cfg.count) % cfg.count);\n if (cfg.loop && cfg.count > 1) {\n if (d > cfg.count / 2) d -= cfg.count;\n else if (d < -cfg.count / 2) d += cfg.count;\n }\n applyTarget(cur + d, true);\n },\n [applyTarget]\n );\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n let delta: number | null = null;\n if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') delta = -1;\n else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') delta = 1;\n if (delta == null) return;\n e.preventDefault();\n applyTarget(Math.round(targetRef.current) + delta, true);\n },\n [applyTarget]\n );\n\n useEffect(() => {\n applyTarget(targetRef.current, false);\n }, [items, fontSize, spacing, curve, tilt, blur, fade, minOpacity, side, loop, smoothing, applyTarget]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n audioRef.current?.pause();\n },\n []\n );\n\n return (\n \n {items.map((label, index) => (\n {\n itemRefs.current[index] = el;\n }}\n role=\"option\"\n aria-selected={selectedIndex === index}\n className={`absolute top-1/2 cursor-pointer whitespace-nowrap leading-none will-change-[transform,opacity,filter] [font-size:var(--ow-font-size)] [color:color-mix(in_srgb,var(--ow-active-color)_calc(var(--ow-p,0)*100%),var(--ow-text-color))] ${\n side === 'right' ? 'right-[var(--ow-inset)] origin-right' : 'left-[var(--ow-inset)] origin-left'\n } ${selectedIndex === index ? 'font-medium' : 'font-extralight'}`}\n onClick={() => handleItemClick(index)}\n >\n {label}\n \n ))}\n \n );\n};\n\nexport default OptionWheel;\n" + "content": "import { useRef, useState, useCallback, useEffect, type CSSProperties } from 'react';\n\ntype Side = 'left' | 'right';\n\nexport interface OptionWheelProps {\n items?: string[];\n defaultSelected?: number;\n onChange?: (index: number, item: string) => void;\n textColor?: string;\n activeColor?: string;\n side?: Side;\n fontSize?: number;\n spacing?: number;\n curve?: number;\n tilt?: number;\n blur?: number;\n fade?: number;\n minOpacity?: number;\n smoothing?: number;\n inset?: number;\n loop?: boolean;\n draggable?: boolean;\n soundUrl?: string;\n soundVolume?: number;\n className?: string;\n}\n\ninterface WheelConfig {\n count: number;\n items: string[];\n rowH: number;\n curve: number;\n tilt: number;\n blur: number;\n fade: number;\n minOpacity: number;\n side: Side;\n loop: boolean;\n smoothing: number;\n draggable: boolean;\n soundUrl: string;\n soundVolume: number;\n}\n\nconst DEFAULT_ITEMS = [\n 'Ambient',\n 'House',\n 'Techno',\n 'Jazz',\n 'Lo-Fi',\n 'Synthwave',\n 'Trance',\n 'Funk',\n 'Disco',\n 'Hip-Hop',\n 'Chillwave',\n 'Drum & Bass'\n];\n\nconst OptionWheel = ({\n items = DEFAULT_ITEMS,\n defaultSelected = 3,\n onChange,\n textColor = '#a6a6a6',\n activeColor = '#ffffff',\n side = 'left',\n fontSize = 3,\n spacing = 1.4,\n curve = 1,\n tilt = 6,\n blur = 2,\n fade = 0.25,\n minOpacity = 0.05,\n smoothing = 200,\n inset = 80,\n loop = false,\n draggable = true,\n soundUrl = '',\n soundVolume = 0.5,\n className = ''\n}: OptionWheelProps) => {\n const rootRef = useRef(null);\n const itemRefs = useRef<(HTMLDivElement | null)[]>([]);\n const posRef = useRef(defaultSelected);\n const targetRef = useRef(defaultSelected);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const cfgRef = useRef({} as WheelConfig);\n const onChangeRef = useRef(onChange);\n const selectedRef = useRef(defaultSelected);\n const wheelTimerRef = useRef | null>(null);\n const dragRef = useRef<{ y: number; start: number; id: number } | null>(null);\n const dragMovedRef = useRef(false);\n const audioRef = useRef(null);\n const audioUrlRef = useRef('');\n const lastTickRef = useRef(0);\n const [selectedIndex, setSelectedIndex] = useState(defaultSelected);\n const [isDragging, setIsDragging] = useState(false);\n\n const remPx = typeof window !== 'undefined' ? parseFloat(getComputedStyle(document.documentElement).fontSize) || 16 : 16;\n\n onChangeRef.current = onChange;\n cfgRef.current = {\n count: items.length,\n items,\n rowH: Math.max(fontSize * spacing * remPx, 1),\n curve,\n tilt,\n blur,\n fade,\n minOpacity,\n side,\n loop,\n smoothing,\n draggable,\n soundUrl,\n soundVolume\n };\n\n // Single rAF loop that eases the wheel position toward its target with\n // frame-rate independent exponential smoothing, then lays every option out\n // along the curve based on its distance from the current position.\n const runFrame = useCallback((now: number) => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const cfg = cfgRef.current;\n const tau = Math.max(cfg.smoothing, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n const target = targetRef.current;\n const cur = posRef.current;\n let next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.001;\n if (settled) next = target;\n posRef.current = next;\n\n const els = itemRefs.current;\n const n = cfg.count;\n const mirror = cfg.side === 'right' ? -1 : 1;\n // Options sit on a circle whose radius keeps the arc length between two\n // neighbors equal to one row height, so tilt controls how tightly it curls.\n const tiltRad = (cfg.tilt * Math.PI) / 180;\n const R = tiltRad > 0.0005 ? cfg.rowH / tiltRad : 0;\n for (let i = 0; i < n; i++) {\n const el = els[i];\n if (!el) continue;\n let d = i - next;\n if (cfg.loop && n > 1) {\n d = ((d % n) + n) % n;\n if (d > n / 2) d -= n;\n }\n const dist = Math.abs(d);\n let x = 0;\n let y = d * cfg.rowH;\n let rot = 0;\n if (R > 0) {\n const ang = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, d * tiltRad));\n y = R * Math.sin(ang);\n x = -mirror * R * (1 - Math.cos(ang)) * cfg.curve;\n rot = (mirror * ang * 180) / Math.PI;\n }\n el.style.transform = `translate(${x.toFixed(2)}px, calc(${y.toFixed(2)}px - 50%)) rotate(${rot.toFixed(3)}deg)`;\n el.style.opacity = String(Math.max(cfg.minOpacity, 1 - dist * cfg.fade));\n el.style.filter = cfg.blur > 0 ? `blur(${(dist * cfg.blur).toFixed(2)}px)` : 'none';\n el.style.setProperty('--ow-p', Math.max(0, 1 - Math.min(dist, 1)).toFixed(4));\n }\n\n rafRef.current = settled ? null : requestAnimationFrame(runFrame);\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) {\n cancelAnimationFrame(rafRef.current);\n }\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n // Optional tick on selection change, throttled so fast scrolling can't spam\n // it, and with playback failures (e.g. autoplay policies) silently ignored.\n const playTick = useCallback(() => {\n const { soundUrl, soundVolume } = cfgRef.current;\n if (!soundUrl) return;\n const now = performance.now();\n if (now - lastTickRef.current < 70) return;\n lastTickRef.current = now;\n if (!audioRef.current || audioUrlRef.current !== soundUrl) {\n audioRef.current = new Audio(soundUrl);\n audioRef.current.preload = 'auto';\n audioUrlRef.current = soundUrl;\n }\n const audio = audioRef.current;\n audio.volume = Math.min(Math.max(soundVolume, 0), 1);\n audio.currentTime = 0;\n audio.play()?.catch(() => {});\n }, []);\n\n const applyTarget = useCallback(\n (value: number, snap: boolean) => {\n const cfg = cfgRef.current;\n let v = value;\n if (!cfg.loop) v = Math.min(Math.max(v, 0), Math.max(cfg.count - 1, 0));\n if (snap) v = Math.round(v);\n targetRef.current = v;\n const idx = ((Math.round(v) % cfg.count) + cfg.count) % cfg.count;\n if (idx !== selectedRef.current) {\n selectedRef.current = idx;\n setSelectedIndex(idx);\n onChangeRef.current?.(idx, cfg.items[idx]);\n playTick();\n }\n startLoop();\n },\n [startLoop, playTick]\n );\n\n // Wheel / touchpad scrolling, registered manually so it can be non-passive.\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const onWheel = (e: WheelEvent) => {\n e.preventDefault();\n const cfg = cfgRef.current;\n const delta = e.deltaMode === 1 ? e.deltaY * 24 : e.deltaY;\n // Cap each event at one step so notchy mouse wheels move exactly one\n // option per click, while touchpads still scroll continuously.\n const step = Math.max(-1, Math.min(1, delta / cfg.rowH));\n applyTarget(targetRef.current + step, false);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n wheelTimerRef.current = setTimeout(() => applyTarget(targetRef.current, true), 140);\n };\n el.addEventListener('wheel', onWheel, { passive: false });\n return () => {\n el.removeEventListener('wheel', onWheel);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n };\n }, [applyTarget]);\n\n const handlePointerDown = useCallback((e: React.PointerEvent) => {\n if (!cfgRef.current.draggable) return;\n dragRef.current = { y: e.clientY, start: targetRef.current, id: e.pointerId };\n dragMovedRef.current = false;\n setIsDragging(true);\n }, []);\n\n const handlePointerMove = useCallback(\n (e: React.PointerEvent) => {\n const drag = dragRef.current;\n if (!drag) return;\n const dy = e.clientY - drag.y;\n if (!dragMovedRef.current && Math.abs(dy) > 4) {\n dragMovedRef.current = true;\n // Capture only once a real drag starts, so plain clicks still reach\n // the items and navigate to them.\n rootRef.current?.setPointerCapture(drag.id);\n }\n if (dragMovedRef.current) applyTarget(drag.start - dy / cfgRef.current.rowH, false);\n },\n [applyTarget]\n );\n\n const handlePointerEnd = useCallback(() => {\n if (!dragRef.current) return;\n dragRef.current = null;\n setIsDragging(false);\n if (dragMovedRef.current) applyTarget(targetRef.current, true);\n }, [applyTarget]);\n\n const handleItemClick = useCallback(\n (index: number) => {\n if (dragMovedRef.current) return;\n const cfg = cfgRef.current;\n const cur = targetRef.current;\n let d = index - (((cur % cfg.count) + cfg.count) % cfg.count);\n if (cfg.loop && cfg.count > 1) {\n if (d > cfg.count / 2) d -= cfg.count;\n else if (d < -cfg.count / 2) d += cfg.count;\n }\n applyTarget(cur + d, true);\n },\n [applyTarget]\n );\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n let delta: number | null = null;\n if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') delta = -1;\n else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') delta = 1;\n if (delta == null) return;\n e.preventDefault();\n applyTarget(Math.round(targetRef.current) + delta, true);\n },\n [applyTarget]\n );\n\n useEffect(() => {\n applyTarget(targetRef.current, false);\n }, [items, fontSize, spacing, curve, tilt, blur, fade, minOpacity, side, loop, smoothing, applyTarget]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n audioRef.current?.pause();\n },\n []\n );\n\n return (\n \n {items.map((label, index) => (\n {\n itemRefs.current[index] = el;\n }}\n role=\"option\"\n aria-selected={selectedIndex === index}\n className={`absolute top-1/2 cursor-pointer whitespace-nowrap leading-none will-change-[transform,opacity,filter] [font-size:var(--ow-font-size)] [color:color-mix(in_srgb,var(--ow-active-color)_calc(var(--ow-p,0)*100%),var(--ow-text-color))] ${\n side === 'right' ? 'right-[var(--ow-inset)] origin-right' : 'left-[var(--ow-inset)] origin-left'\n } ${selectedIndex === index ? 'font-medium' : 'font-extralight'}`}\n onClick={() => handleItemClick(index)}\n >\n {label}\n \n ))}\n \n );\n};\n\nexport default OptionWheel;\n" } ], "registryDependencies": [], diff --git a/public/r/Orb-JS-CSS.json b/public/r/Orb-JS-CSS.json index b9ddd77a..8611297a 100644 --- a/public/r/Orb-JS-CSS.json +++ b/public/r/Orb-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Orb/Orb.css", + "type": "registry:file", + "path": "Orb.css", + "target": "@components/Orb.css", "content": ".orb-container {\n position: relative;\n z-index: 0;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "Orb/Orb.jsx", + "path": "Orb.jsx", "content": "import { Mesh, Program, Renderer, Triangle, Vec3 } from 'ogl';\nimport { useEffect, useRef } from 'react';\nimport './Orb.css';\n\nexport default function Orb({\n hue = 0,\n hoverIntensity = 0.2,\n rotateOnHover = true,\n forceHoverState = false,\n backgroundColor = '#000000'\n}) {\n const ctnDom = useRef(null);\n\n const vert = /* glsl */ `\n precision highp float;\n attribute vec2 position;\n attribute vec2 uv;\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = vec4(position, 0.0, 1.0);\n }\n `;\n\n const frag = /* glsl */ `\n precision highp float;\n\n uniform float iTime;\n uniform vec3 iResolution;\n uniform float hue;\n uniform float hover;\n uniform float rot;\n uniform float hoverIntensity;\n uniform vec3 backgroundColor;\n varying vec2 vUv;\n\n vec3 rgb2yiq(vec3 c) {\n float y = dot(c, vec3(0.299, 0.587, 0.114));\n float i = dot(c, vec3(0.596, -0.274, -0.322));\n float q = dot(c, vec3(0.211, -0.523, 0.312));\n return vec3(y, i, q);\n }\n \n vec3 yiq2rgb(vec3 c) {\n float r = c.x + 0.956 * c.y + 0.621 * c.z;\n float g = c.x - 0.272 * c.y - 0.647 * c.z;\n float b = c.x - 1.106 * c.y + 1.703 * c.z;\n return vec3(r, g, b);\n }\n \n vec3 adjustHue(vec3 color, float hueDeg) {\n float hueRad = hueDeg * 3.14159265 / 180.0;\n vec3 yiq = rgb2yiq(color);\n float cosA = cos(hueRad);\n float sinA = sin(hueRad);\n float i = yiq.y * cosA - yiq.z * sinA;\n float q = yiq.y * sinA + yiq.z * cosA;\n yiq.y = i;\n yiq.z = q;\n return yiq2rgb(yiq);\n }\n\n vec3 hash33(vec3 p3) {\n p3 = fract(p3 * vec3(0.1031, 0.11369, 0.13787));\n p3 += dot(p3, p3.yxz + 19.19);\n return -1.0 + 2.0 * fract(vec3(\n p3.x + p3.y,\n p3.x + p3.z,\n p3.y + p3.z\n ) * p3.zyx);\n }\n\n float snoise3(vec3 p) {\n const float K1 = 0.333333333;\n const float K2 = 0.166666667;\n vec3 i = floor(p + (p.x + p.y + p.z) * K1);\n vec3 d0 = p - (i - (i.x + i.y + i.z) * K2);\n vec3 e = step(vec3(0.0), d0 - d0.yzx);\n vec3 i1 = e * (1.0 - e.zxy);\n vec3 i2 = 1.0 - e.zxy * (1.0 - e);\n vec3 d1 = d0 - (i1 - K2);\n vec3 d2 = d0 - (i2 - K1);\n vec3 d3 = d0 - 0.5;\n vec4 h = max(0.6 - vec4(\n dot(d0, d0),\n dot(d1, d1),\n dot(d2, d2),\n dot(d3, d3)\n ), 0.0);\n vec4 n = h * h * h * h * vec4(\n dot(d0, hash33(i)),\n dot(d1, hash33(i + i1)),\n dot(d2, hash33(i + i2)),\n dot(d3, hash33(i + 1.0))\n );\n return dot(vec4(31.316), n);\n }\n\n vec4 extractAlpha(vec3 colorIn) {\n float a = max(max(colorIn.r, colorIn.g), colorIn.b);\n return vec4(colorIn.rgb / (a + 1e-5), a);\n }\n\n const vec3 baseColor1 = vec3(0.611765, 0.262745, 0.996078);\n const vec3 baseColor2 = vec3(0.298039, 0.760784, 0.913725);\n const vec3 baseColor3 = vec3(0.062745, 0.078431, 0.600000);\n const float innerRadius = 0.6;\n const float noiseScale = 0.65;\n\n float light1(float intensity, float attenuation, float dist) {\n return intensity / (1.0 + dist * attenuation);\n }\n float light2(float intensity, float attenuation, float dist) {\n return intensity / (1.0 + dist * dist * attenuation);\n }\n\n vec4 draw(vec2 uv) {\n vec3 color1 = adjustHue(baseColor1, hue);\n vec3 color2 = adjustHue(baseColor2, hue);\n vec3 color3 = adjustHue(baseColor3, hue);\n \n float ang = atan(uv.y, uv.x);\n float len = length(uv);\n float invLen = len > 0.0 ? 1.0 / len : 0.0;\n\n float bgLuminance = dot(backgroundColor, vec3(0.299, 0.587, 0.114));\n \n float n0 = snoise3(vec3(uv * noiseScale, iTime * 0.5)) * 0.5 + 0.5;\n float r0 = mix(mix(innerRadius, 1.0, 0.4), mix(innerRadius, 1.0, 0.6), n0);\n float d0 = distance(uv, (r0 * invLen) * uv);\n float v0 = light1(1.0, 10.0, d0);\n\n v0 *= smoothstep(r0 * 1.05, r0, len);\n float innerFade = smoothstep(r0 * 0.8, r0 * 0.95, len);\n v0 *= mix(innerFade, 1.0, bgLuminance * 0.7);\n float cl = cos(ang + iTime * 2.0) * 0.5 + 0.5;\n \n float a = iTime * -1.0;\n vec2 pos = vec2(cos(a), sin(a)) * r0;\n float d = distance(uv, pos);\n float v1 = light2(1.5, 5.0, d);\n v1 *= light1(1.0, 50.0, d0);\n \n float v2 = smoothstep(1.0, mix(innerRadius, 1.0, n0 * 0.5), len);\n float v3 = smoothstep(innerRadius, mix(innerRadius, 1.0, 0.5), len);\n \n vec3 colBase = mix(color1, color2, cl);\n float fadeAmount = mix(1.0, 0.1, bgLuminance);\n \n vec3 darkCol = mix(color3, colBase, v0);\n darkCol = (darkCol + v1) * v2 * v3;\n darkCol = clamp(darkCol, 0.0, 1.0);\n \n vec3 lightCol = (colBase + v1) * mix(1.0, v2 * v3, fadeAmount);\n lightCol = mix(backgroundColor, lightCol, v0);\n lightCol = clamp(lightCol, 0.0, 1.0);\n \n vec3 finalCol = mix(darkCol, lightCol, bgLuminance);\n \n return extractAlpha(finalCol);\n }\n\n vec4 mainImage(vec2 fragCoord) {\n vec2 center = iResolution.xy * 0.5;\n float size = min(iResolution.x, iResolution.y);\n vec2 uv = (fragCoord - center) / size * 2.0;\n \n float angle = rot;\n float s = sin(angle);\n float c = cos(angle);\n uv = vec2(c * uv.x - s * uv.y, s * uv.x + c * uv.y);\n \n uv.x += hover * hoverIntensity * 0.1 * sin(uv.y * 10.0 + iTime);\n uv.y += hover * hoverIntensity * 0.1 * sin(uv.x * 10.0 + iTime);\n \n return draw(uv);\n }\n\n void main() {\n vec2 fragCoord = vUv * iResolution.xy;\n vec4 col = mainImage(fragCoord);\n gl_FragColor = vec4(col.rgb * col.a, col.a);\n }\n `;\n\n useEffect(() => {\n const container = ctnDom.current;\n if (!container) return;\n\n const renderer = new Renderer({ alpha: true, premultipliedAlpha: false });\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n container.appendChild(gl.canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex: vert,\n fragment: frag,\n uniforms: {\n iTime: { value: 0 },\n iResolution: {\n value: new Vec3(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height)\n },\n hue: { value: hue },\n hover: { value: 0 },\n rot: { value: 0 },\n hoverIntensity: { value: hoverIntensity },\n backgroundColor: { value: hexToVec3(backgroundColor) }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n\n function resize() {\n if (!container) return;\n const dpr = window.devicePixelRatio || 1;\n const width = container.clientWidth;\n const height = container.clientHeight;\n renderer.setSize(width * dpr, height * dpr);\n gl.canvas.style.width = width + 'px';\n gl.canvas.style.height = height + 'px';\n program.uniforms.iResolution.value.set(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height);\n }\n window.addEventListener('resize', resize);\n resize();\n\n let targetHover = 0;\n let lastTime = 0;\n let currentRot = 0;\n const rotationSpeed = 0.3;\n\n const handleMouseMove = e => {\n const rect = container.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n const width = rect.width;\n const height = rect.height;\n const size = Math.min(width, height);\n const centerX = width / 2;\n const centerY = height / 2;\n const uvX = ((x - centerX) / size) * 2.0;\n const uvY = ((y - centerY) / size) * 2.0;\n\n if (Math.sqrt(uvX * uvX + uvY * uvY) < 0.8) {\n targetHover = 1;\n } else {\n targetHover = 0;\n }\n };\n\n const handleMouseLeave = () => {\n targetHover = 0;\n };\n\n container.addEventListener('mousemove', handleMouseMove);\n container.addEventListener('mouseleave', handleMouseLeave);\n\n let rafId;\n const update = t => {\n rafId = requestAnimationFrame(update);\n const dt = (t - lastTime) * 0.001;\n lastTime = t;\n program.uniforms.iTime.value = t * 0.001;\n program.uniforms.hue.value = hue;\n program.uniforms.hoverIntensity.value = hoverIntensity;\n program.uniforms.backgroundColor.value = hexToVec3(backgroundColor);\n\n const effectiveHover = forceHoverState ? 1 : targetHover;\n program.uniforms.hover.value += (effectiveHover - program.uniforms.hover.value) * 0.1;\n\n if (rotateOnHover && effectiveHover > 0.5) {\n currentRot += dt * rotationSpeed;\n }\n program.uniforms.rot.value = currentRot;\n\n renderer.render({ scene: mesh });\n };\n rafId = requestAnimationFrame(update);\n\n return () => {\n cancelAnimationFrame(rafId);\n window.removeEventListener('resize', resize);\n container.removeEventListener('mousemove', handleMouseMove);\n container.removeEventListener('mouseleave', handleMouseLeave);\n container.removeChild(gl.canvas);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hue, hoverIntensity, rotateOnHover, forceHoverState, backgroundColor]);\n\n return
    ;\n}\n\nfunction hslToRgb(h, s, l) {\n let r, g, b;\n\n if (s === 0) {\n r = g = b = l;\n } else {\n const hue2rgb = (p, q, t) => {\n if (t < 0) t += 1;\n if (t > 1) t -= 1;\n if (t < 1 / 6) return p + (q - p) * 6 * t;\n if (t < 1 / 2) return q;\n if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;\n return p;\n };\n\n const q = l < 0.5 ? l * (1 + s) : l + s - l * s;\n const p = 2 * l - q;\n r = hue2rgb(p, q, h + 1 / 3);\n g = hue2rgb(p, q, h);\n b = hue2rgb(p, q, h - 1 / 3);\n }\n\n return new Vec3(r, g, b);\n}\n\nfunction hexToVec3(color) {\n if (color.startsWith('#')) {\n const r = parseInt(color.slice(1, 3), 16) / 255;\n const g = parseInt(color.slice(3, 5), 16) / 255;\n const b = parseInt(color.slice(5, 7), 16) / 255;\n return new Vec3(r, g, b);\n }\n\n const rgbMatch = color.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/);\n if (rgbMatch) {\n return new Vec3(parseInt(rgbMatch[1]) / 255, parseInt(rgbMatch[2]) / 255, parseInt(rgbMatch[3]) / 255);\n }\n\n const hslMatch = color.match(/hsla?\\((\\d+),\\s*(\\d+)%,\\s*(\\d+)%/);\n if (hslMatch) {\n const h = parseInt(hslMatch[1]) / 360;\n const s = parseInt(hslMatch[2]) / 100;\n const l = parseInt(hslMatch[3]) / 100;\n return hslToRgb(h, s, l);\n }\n\n return new Vec3(0, 0, 0);\n}\n" } ], diff --git a/public/r/Orb-TS-CSS.json b/public/r/Orb-TS-CSS.json index 4a059a17..d100a806 100644 --- a/public/r/Orb-TS-CSS.json +++ b/public/r/Orb-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Orb/Orb.css", + "type": "registry:file", + "path": "Orb.css", + "target": "@components/Orb.css", "content": ".orb-container {\n position: relative;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "Orb/Orb.tsx", + "path": "Orb.tsx", "content": "import { Mesh, Program, Renderer, Triangle, Vec3 } from 'ogl';\nimport { useEffect, useRef } from 'react';\n\nimport './Orb.css';\n\ninterface OrbProps {\n hue?: number;\n hoverIntensity?: number;\n rotateOnHover?: boolean;\n forceHoverState?: boolean;\n backgroundColor?: string;\n}\n\nexport default function Orb({\n hue = 0,\n hoverIntensity = 0.2,\n rotateOnHover = true,\n forceHoverState = false,\n backgroundColor = '#000000'\n}: OrbProps) {\n const ctnDom = useRef(null);\n\n const vert = /* glsl */ `\n precision highp float;\n attribute vec2 position;\n attribute vec2 uv;\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = vec4(position, 0.0, 1.0);\n }\n `;\n\n const frag = /* glsl */ `\n precision highp float;\n\n uniform float iTime;\n uniform vec3 iResolution;\n uniform float hue;\n uniform float hover;\n uniform float rot;\n uniform float hoverIntensity;\n uniform vec3 backgroundColor;\n varying vec2 vUv;\n\n vec3 rgb2yiq(vec3 c) {\n float y = dot(c, vec3(0.299, 0.587, 0.114));\n float i = dot(c, vec3(0.596, -0.274, -0.322));\n float q = dot(c, vec3(0.211, -0.523, 0.312));\n return vec3(y, i, q);\n }\n \n vec3 yiq2rgb(vec3 c) {\n float r = c.x + 0.956 * c.y + 0.621 * c.z;\n float g = c.x - 0.272 * c.y - 0.647 * c.z;\n float b = c.x - 1.106 * c.y + 1.703 * c.z;\n return vec3(r, g, b);\n }\n \n vec3 adjustHue(vec3 color, float hueDeg) {\n float hueRad = hueDeg * 3.14159265 / 180.0;\n vec3 yiq = rgb2yiq(color);\n float cosA = cos(hueRad);\n float sinA = sin(hueRad);\n float i = yiq.y * cosA - yiq.z * sinA;\n float q = yiq.y * sinA + yiq.z * cosA;\n yiq.y = i;\n yiq.z = q;\n return yiq2rgb(yiq);\n }\n \n vec3 hash33(vec3 p3) {\n p3 = fract(p3 * vec3(0.1031, 0.11369, 0.13787));\n p3 += dot(p3, p3.yxz + 19.19);\n return -1.0 + 2.0 * fract(vec3(\n p3.x + p3.y,\n p3.x + p3.z,\n p3.y + p3.z\n ) * p3.zyx);\n }\n \n float snoise3(vec3 p) {\n const float K1 = 0.333333333;\n const float K2 = 0.166666667;\n vec3 i = floor(p + (p.x + p.y + p.z) * K1);\n vec3 d0 = p - (i - (i.x + i.y + i.z) * K2);\n vec3 e = step(vec3(0.0), d0 - d0.yzx);\n vec3 i1 = e * (1.0 - e.zxy);\n vec3 i2 = 1.0 - e.zxy * (1.0 - e);\n vec3 d1 = d0 - (i1 - K2);\n vec3 d2 = d0 - (i2 - K1);\n vec3 d3 = d0 - 0.5;\n vec4 h = max(0.6 - vec4(\n dot(d0, d0),\n dot(d1, d1),\n dot(d2, d2),\n dot(d3, d3)\n ), 0.0);\n vec4 n = h * h * h * h * vec4(\n dot(d0, hash33(i)),\n dot(d1, hash33(i + i1)),\n dot(d2, hash33(i + i2)),\n dot(d3, hash33(i + 1.0))\n );\n return dot(vec4(31.316), n);\n }\n \n vec4 extractAlpha(vec3 colorIn) {\n float a = max(max(colorIn.r, colorIn.g), colorIn.b);\n return vec4(colorIn.rgb / (a + 1e-5), a);\n }\n \n const vec3 baseColor1 = vec3(0.611765, 0.262745, 0.996078);\n const vec3 baseColor2 = vec3(0.298039, 0.760784, 0.913725);\n const vec3 baseColor3 = vec3(0.062745, 0.078431, 0.600000);\n const float innerRadius = 0.6;\n const float noiseScale = 0.65;\n \n float light1(float intensity, float attenuation, float dist) {\n return intensity / (1.0 + dist * attenuation);\n }\n \n float light2(float intensity, float attenuation, float dist) {\n return intensity / (1.0 + dist * dist * attenuation);\n }\n \n vec4 draw(vec2 uv) {\n vec3 color1 = adjustHue(baseColor1, hue);\n vec3 color2 = adjustHue(baseColor2, hue);\n vec3 color3 = adjustHue(baseColor3, hue);\n \n float ang = atan(uv.y, uv.x);\n float len = length(uv);\n float invLen = len > 0.0 ? 1.0 / len : 0.0;\n \n float bgLuminance = dot(backgroundColor, vec3(0.299, 0.587, 0.114));\n \n float n0 = snoise3(vec3(uv * noiseScale, iTime * 0.5)) * 0.5 + 0.5;\n float r0 = mix(mix(innerRadius, 1.0, 0.4), mix(innerRadius, 1.0, 0.6), n0);\n float d0 = distance(uv, (r0 * invLen) * uv);\n float v0 = light1(1.0, 10.0, d0);\n\n v0 *= smoothstep(r0 * 1.05, r0, len);\n float innerFade = smoothstep(r0 * 0.8, r0 * 0.95, len);\n v0 *= mix(innerFade, 1.0, bgLuminance * 0.7);\n float cl = cos(ang + iTime * 2.0) * 0.5 + 0.5;\n \n float a = iTime * -1.0;\n vec2 pos = vec2(cos(a), sin(a)) * r0;\n float d = distance(uv, pos);\n float v1 = light2(1.5, 5.0, d);\n v1 *= light1(1.0, 50.0, d0);\n \n float v2 = smoothstep(1.0, mix(innerRadius, 1.0, n0 * 0.5), len);\n float v3 = smoothstep(innerRadius, mix(innerRadius, 1.0, 0.5), len);\n \n vec3 colBase = mix(color1, color2, cl);\n float fadeAmount = mix(1.0, 0.1, bgLuminance);\n \n vec3 darkCol = mix(color3, colBase, v0);\n darkCol = (darkCol + v1) * v2 * v3;\n darkCol = clamp(darkCol, 0.0, 1.0);\n \n vec3 lightCol = (colBase + v1) * mix(1.0, v2 * v3, fadeAmount);\n lightCol = mix(backgroundColor, lightCol, v0);\n lightCol = clamp(lightCol, 0.0, 1.0);\n \n vec3 finalCol = mix(darkCol, lightCol, bgLuminance);\n \n return extractAlpha(finalCol);\n }\n \n vec4 mainImage(vec2 fragCoord) {\n vec2 center = iResolution.xy * 0.5;\n float size = min(iResolution.x, iResolution.y);\n vec2 uv = (fragCoord - center) / size * 2.0;\n \n float angle = rot;\n float s = sin(angle);\n float c = cos(angle);\n uv = vec2(c * uv.x - s * uv.y, s * uv.x + c * uv.y);\n \n uv.x += hover * hoverIntensity * 0.1 * sin(uv.y * 10.0 + iTime);\n uv.y += hover * hoverIntensity * 0.1 * sin(uv.x * 10.0 + iTime);\n \n return draw(uv);\n }\n \n void main() {\n vec2 fragCoord = vUv * iResolution.xy;\n vec4 col = mainImage(fragCoord);\n gl_FragColor = vec4(col.rgb * col.a, col.a);\n }\n `;\n\n useEffect(() => {\n const container = ctnDom.current;\n if (!container) return;\n\n const renderer = new Renderer({ alpha: true, premultipliedAlpha: false });\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n container.appendChild(gl.canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex: vert,\n fragment: frag,\n uniforms: {\n iTime: { value: 0 },\n iResolution: {\n value: new Vec3(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height)\n },\n hue: { value: hue },\n hover: { value: 0 },\n rot: { value: 0 },\n hoverIntensity: { value: hoverIntensity },\n backgroundColor: { value: hexToVec3(backgroundColor) }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n\n function resize() {\n if (!container) return;\n const dpr = window.devicePixelRatio || 1;\n const width = container.clientWidth;\n const height = container.clientHeight;\n renderer.setSize(width * dpr, height * dpr);\n gl.canvas.style.width = width + 'px';\n gl.canvas.style.height = height + 'px';\n program.uniforms.iResolution.value.set(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height);\n }\n window.addEventListener('resize', resize);\n resize();\n\n let targetHover = 0;\n let lastTime = 0;\n let currentRot = 0;\n const rotationSpeed = 0.3;\n\n const handleMouseMove = (e: MouseEvent) => {\n const rect = container.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n const width = rect.width;\n const height = rect.height;\n const size = Math.min(width, height);\n const centerX = width / 2;\n const centerY = height / 2;\n const uvX = ((x - centerX) / size) * 2.0;\n const uvY = ((y - centerY) / size) * 2.0;\n\n if (Math.sqrt(uvX * uvX + uvY * uvY) < 0.8) {\n targetHover = 1;\n } else {\n targetHover = 0;\n }\n };\n\n const handleMouseLeave = () => {\n targetHover = 0;\n };\n\n container.addEventListener('mousemove', handleMouseMove);\n container.addEventListener('mouseleave', handleMouseLeave);\n\n let rafId: number;\n const update = (t: number) => {\n rafId = requestAnimationFrame(update);\n const dt = (t - lastTime) * 0.001;\n lastTime = t;\n program.uniforms.iTime.value = t * 0.001;\n program.uniforms.hue.value = hue;\n program.uniforms.hoverIntensity.value = hoverIntensity;\n\n const effectiveHover = forceHoverState ? 1 : targetHover;\n program.uniforms.hover.value += (effectiveHover - program.uniforms.hover.value) * 0.1;\n\n if (rotateOnHover && effectiveHover > 0.5) {\n currentRot += dt * rotationSpeed;\n }\n program.uniforms.rot.value = currentRot;\n program.uniforms.backgroundColor.value = hexToVec3(backgroundColor);\n\n renderer.render({ scene: mesh });\n };\n rafId = requestAnimationFrame(update);\n\n return () => {\n cancelAnimationFrame(rafId);\n window.removeEventListener('resize', resize);\n container.removeEventListener('mousemove', handleMouseMove);\n container.removeEventListener('mouseleave', handleMouseLeave);\n container.removeChild(gl.canvas);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, [hue, hoverIntensity, rotateOnHover, forceHoverState, backgroundColor]);\n\n return
    ;\n}\n\nfunction hslToRgb(h: number, s: number, l: number) {\n let r, g, b;\n\n if (s === 0) {\n r = g = b = l;\n } else {\n const hue2rgb = (p: number, q: number, t: number) => {\n if (t < 0) t += 1;\n if (t > 1) t -= 1;\n if (t < 1 / 6) return p + (q - p) * 6 * t;\n if (t < 1 / 2) return q;\n if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;\n return p;\n };\n\n const q = l < 0.5 ? l * (1 + s) : l + s - l * s;\n const p = 2 * l - q;\n r = hue2rgb(p, q, h + 1 / 3);\n g = hue2rgb(p, q, h);\n b = hue2rgb(p, q, h - 1 / 3);\n }\n\n return new Vec3(r, g, b);\n}\n\nfunction hexToVec3(color: string) {\n if (color.startsWith('#')) {\n const r = parseInt(color.slice(1, 3), 16) / 255;\n const g = parseInt(color.slice(3, 5), 16) / 255;\n const b = parseInt(color.slice(5, 7), 16) / 255;\n return new Vec3(r, g, b);\n }\n\n const rgbMatch = color.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/);\n if (rgbMatch) {\n return new Vec3(parseInt(rgbMatch[1]) / 255, parseInt(rgbMatch[2]) / 255, parseInt(rgbMatch[3]) / 255);\n }\n\n const hslMatch = color.match(/hsla?\\((\\d+),\\s*(\\d+)%,\\s*(\\d+)%/);\n if (hslMatch) {\n const h = parseInt(hslMatch[1]) / 360;\n const s = parseInt(hslMatch[2]) / 100;\n const l = parseInt(hslMatch[3]) / 100;\n return hslToRgb(h, s, l);\n }\n\n return new Vec3(0, 0, 0);\n}\n" } ], diff --git a/public/r/OrbitImages-JS-CSS.json b/public/r/OrbitImages-JS-CSS.json index 4aff6951..ffd579f7 100644 --- a/public/r/OrbitImages-JS-CSS.json +++ b/public/r/OrbitImages-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "OrbitImages/OrbitImages.css", + "type": "registry:file", + "path": "OrbitImages.css", + "target": "@components/OrbitImages.css", "content": ".orbit-container {\n position: relative;\n margin-left: auto;\n margin-right: auto;\n}\n\n.orbit-scaling-container {\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n.orbit-scaling-container--responsive {\n position: absolute;\n left: 50%;\n top: 50%;\n transform-origin: center center;\n}\n\n.orbit-rotation-wrapper {\n width: 100%;\n height: 100%;\n transform-origin: center center;\n position: relative;\n}\n\n.orbit-path-svg {\n position: absolute;\n inset: 0;\n pointer-events: none;\n}\n\n.orbit-item {\n position: absolute;\n will-change: transform;\n user-select: none;\n}\n\n.orbit-center-content {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 10;\n}\n\n.orbit-image {\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n" }, { "type": "registry:component", - "path": "OrbitImages/OrbitImages.jsx", + "path": "OrbitImages.jsx", "content": "// Component created by Dominik Koch\n// https://x.com/dominikkoch\n\nimport { useMemo, useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport { motion, useMotionValue, useTransform, animate } from 'motion/react';\nimport './OrbitImages.css';\n\nfunction generateEllipsePath(cx, cy, rx, ry) {\n return `M ${cx - rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx + rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy}`;\n}\n\nfunction generateCirclePath(cx, cy, r) {\n return generateEllipsePath(cx, cy, r, r);\n}\n\nfunction generateSquarePath(cx, cy, size) {\n const h = size / 2;\n return `M ${cx - h} ${cy - h} L ${cx + h} ${cy - h} L ${cx + h} ${cy + h} L ${cx - h} ${cy + h} Z`;\n}\n\nfunction generateRectanglePath(cx, cy, w, h) {\n const hw = w / 2;\n const hh = h / 2;\n return `M ${cx - hw} ${cy - hh} L ${cx + hw} ${cy - hh} L ${cx + hw} ${cy + hh} L ${cx - hw} ${cy + hh} Z`;\n}\n\nfunction generateTrianglePath(cx, cy, size) {\n const height = (size * Math.sqrt(3)) / 2;\n const hs = size / 2;\n return `M ${cx} ${cy - height / 1.5} L ${cx + hs} ${cy + height / 3} L ${cx - hs} ${cy + height / 3} Z`;\n}\n\nfunction generateStarPath(cx, cy, outerR, innerR, points) {\n const step = Math.PI / points;\n let path = '';\n for (let i = 0; i < 2 * points; i++) {\n const r = i % 2 === 0 ? outerR : innerR;\n const angle = i * step - Math.PI / 2;\n const x = cx + r * Math.cos(angle);\n const y = cy + r * Math.sin(angle);\n path += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;\n }\n return path + ' Z';\n}\n\nfunction generateHeartPath(cx, cy, size) {\n const s = size / 30;\n return `M ${cx} ${cy + 12 * s} C ${cx - 20 * s} ${cy - 5 * s}, ${cx - 12 * s} ${cy - 18 * s}, ${cx} ${cy - 8 * s} C ${cx + 12 * s} ${cy - 18 * s}, ${cx + 20 * s} ${cy - 5 * s}, ${cx} ${cy + 12 * s}`;\n}\n\nfunction generateInfinityPath(cx, cy, w, h) {\n const hw = w / 2;\n const hh = h / 2;\n return `M ${cx} ${cy} C ${cx + hw * 0.5} ${cy - hh}, ${cx + hw} ${cy - hh}, ${cx + hw} ${cy} C ${cx + hw} ${cy + hh}, ${cx + hw * 0.5} ${cy + hh}, ${cx} ${cy} C ${cx - hw * 0.5} ${cy + hh}, ${cx - hw} ${cy + hh}, ${cx - hw} ${cy} C ${cx - hw} ${cy - hh}, ${cx - hw * 0.5} ${cy - hh}, ${cx} ${cy}`;\n}\n\nfunction generateWavePath(cx, cy, w, amplitude, waves) {\n const pts = [];\n const segs = waves * 20;\n const hw = w / 2;\n for (let i = 0; i <= segs; i++) {\n const x = cx - hw + (w * i) / segs;\n const y = cy + Math.sin((i / segs) * waves * 2 * Math.PI) * amplitude;\n pts.push(i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`);\n }\n for (let i = segs; i >= 0; i--) {\n const x = cx - hw + (w * i) / segs;\n const y = cy - Math.sin((i / segs) * waves * 2 * Math.PI) * amplitude;\n pts.push(`L ${x} ${y}`);\n }\n return pts.join(' ') + ' Z';\n}\n\nfunction OrbitItem({ item, index, totalItems, path, itemSize, rotation, progress, fill }) {\n const itemOffset = fill ? (index / totalItems) * 100 : 0;\n\n const offsetDistance = useTransform(progress, (p) => {\n const offset = (((p + itemOffset) % 100) + 100) % 100;\n return `${offset}%`;\n });\n\n return (\n \n
    {item}
    \n \n );\n}\n\nexport default function OrbitImages({\n images = [],\n altPrefix = 'Orbiting image',\n shape = 'ellipse',\n customPath,\n baseWidth = 1400,\n radiusX = 700,\n radiusY = 170,\n radius = 300,\n starPoints = 5,\n starInnerRatio = 0.5,\n rotation = -8,\n duration = 40,\n itemSize = 64,\n direction = 'normal',\n fill = true,\n width = 100,\n height = 100,\n className = '',\n showPath = false,\n pathColor = 'rgba(0,0,0,0.1)',\n pathWidth = 2,\n easing = 'linear',\n paused = false,\n centerContent,\n responsive = false,\n}) {\n const containerRef = useRef(null);\n const [scale, setScale] = useState(null);\n\n const designCenterX = baseWidth / 2;\n const designCenterY = baseWidth / 2;\n\n const path = useMemo(() => {\n switch (shape) {\n case 'circle':\n return generateCirclePath(designCenterX, designCenterY, radius);\n case 'ellipse':\n return generateEllipsePath(designCenterX, designCenterY, radiusX, radiusY);\n case 'square':\n return generateSquarePath(designCenterX, designCenterY, radius * 2);\n case 'rectangle':\n return generateRectanglePath(designCenterX, designCenterY, radiusX * 2, radiusY * 2);\n case 'triangle':\n return generateTrianglePath(designCenterX, designCenterY, radius * 2);\n case 'star':\n return generateStarPath(designCenterX, designCenterY, radius, radius * starInnerRatio, starPoints);\n case 'heart':\n return generateHeartPath(designCenterX, designCenterY, radius * 2);\n case 'infinity':\n return generateInfinityPath(designCenterX, designCenterY, radiusX * 2, radiusY * 2);\n case 'wave':\n return generateWavePath(designCenterX, designCenterY, radiusX * 2, radiusY, 3);\n case 'custom':\n return customPath || generateCirclePath(designCenterX, designCenterY, radius);\n default:\n return generateEllipsePath(designCenterX, designCenterY, radiusX, radiusY);\n }\n }, [shape, customPath, designCenterX, designCenterY, radiusX, radiusY, radius, starPoints, starInnerRatio]);\n\n useLayoutEffect(() => {\n if (!responsive || !containerRef.current) return;\n const updateScale = () => {\n if (!containerRef.current) return;\n setScale(containerRef.current.clientWidth / baseWidth);\n };\n updateScale();\n const observer = new ResizeObserver(updateScale);\n observer.observe(containerRef.current);\n return () => observer.disconnect();\n }, [responsive, baseWidth]);\n\n const progress = useMotionValue(0);\n\n useEffect(() => {\n if (paused) return;\n const controls = animate(progress, direction === 'reverse' ? -100 : 100, {\n duration,\n ease: easing,\n repeat: Infinity,\n repeatType: 'loop',\n });\n return () => controls.stop();\n }, [progress, duration, easing, direction, paused]);\n\n const containerWidth = responsive ? '100%' : (typeof width === 'number' ? width : '100%');\n const containerHeight = responsive ? 'auto' : (typeof height === 'number' ? height : (typeof width === 'number' ? width : 'auto'));\n\n const items = images.map((src, index) => (\n \n ));\n\n return (\n \n \n \n {showPath && (\n \n \n \n )}\n\n {items.map((item, index) => (\n \n ))}\n
    \n
    \n\n {centerContent && (\n
    \n {centerContent}\n
    \n )}\n \n );\n}\n" } ], diff --git a/public/r/OrbitImages-TS-CSS.json b/public/r/OrbitImages-TS-CSS.json index 7787037a..2920cbca 100644 --- a/public/r/OrbitImages-TS-CSS.json +++ b/public/r/OrbitImages-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "OrbitImages/OrbitImages.css", + "type": "registry:file", + "path": "OrbitImages.css", + "target": "@components/OrbitImages.css", "content": ".orbit-container {\n position: relative;\n margin-left: auto;\n margin-right: auto;\n}\n\n.orbit-scaling-container {\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n.orbit-scaling-container--responsive {\n position: absolute;\n left: 50%;\n top: 50%;\n transform-origin: center center;\n}\n\n.orbit-rotation-wrapper {\n width: 100%;\n height: 100%;\n transform-origin: center center;\n position: relative;\n}\n\n.orbit-path-svg {\n position: absolute;\n inset: 0;\n pointer-events: none;\n}\n\n.orbit-item {\n position: absolute;\n will-change: transform;\n user-select: none;\n}\n\n.orbit-center-content {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 10;\n}\n\n.orbit-image {\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n" }, { "type": "registry:component", - "path": "OrbitImages/OrbitImages.tsx", - "content": "// Component created by Dominik Koch\n// https://x.com/dominikkoch\n\nimport { useMemo, useEffect, useLayoutEffect, useRef, useState, ReactNode } from 'react';\nimport { motion, useMotionValue, useTransform, animate, MotionValue } from 'motion/react';\nimport './OrbitImages.css';\n\ntype OrbitShape =\n | 'ellipse'\n | 'circle'\n | 'square'\n | 'rectangle'\n | 'triangle'\n | 'star'\n | 'heart'\n | 'infinity'\n | 'wave'\n | 'custom';\n\ninterface OrbitImagesProps {\n images?: string[];\n altPrefix?: string;\n shape?: OrbitShape;\n customPath?: string;\n baseWidth?: number;\n radiusX?: number;\n radiusY?: number;\n radius?: number;\n starPoints?: number;\n starInnerRatio?: number;\n rotation?: number;\n duration?: number;\n itemSize?: number;\n direction?: 'normal' | 'reverse';\n fill?: boolean;\n width?: number | '100%';\n height?: number | 'auto';\n className?: string;\n showPath?: boolean;\n pathColor?: string;\n pathWidth?: number;\n easing?: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';\n paused?: boolean;\n centerContent?: ReactNode;\n responsive?: boolean;\n}\n\ninterface OrbitItemProps {\n item: ReactNode;\n index: number;\n totalItems: number;\n path: string;\n itemSize: number;\n rotation: number;\n progress: MotionValue;\n fill: boolean;\n}\n\nfunction generateEllipsePath(cx: number, cy: number, rx: number, ry: number): string {\n return `M ${cx - rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx + rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy}`;\n}\n\nfunction generateCirclePath(cx: number, cy: number, r: number): string {\n return generateEllipsePath(cx, cy, r, r);\n}\n\nfunction generateSquarePath(cx: number, cy: number, size: number): string {\n const h = size / 2;\n return `M ${cx - h} ${cy - h} L ${cx + h} ${cy - h} L ${cx + h} ${cy + h} L ${cx - h} ${cy + h} Z`;\n}\n\nfunction generateRectanglePath(cx: number, cy: number, w: number, h: number): string {\n const hw = w / 2;\n const hh = h / 2;\n return `M ${cx - hw} ${cy - hh} L ${cx + hw} ${cy - hh} L ${cx + hw} ${cy + hh} L ${cx - hw} ${cy + hh} Z`;\n}\n\nfunction generateTrianglePath(cx: number, cy: number, size: number): string {\n const height = (size * Math.sqrt(3)) / 2;\n const hs = size / 2;\n return `M ${cx} ${cy - height / 1.5} L ${cx + hs} ${cy + height / 3} L ${cx - hs} ${cy + height / 3} Z`;\n}\n\nfunction generateStarPath(cx: number, cy: number, outerR: number, innerR: number, points: number): string {\n const step = Math.PI / points;\n let path = '';\n for (let i = 0; i < 2 * points; i++) {\n const r = i % 2 === 0 ? outerR : innerR;\n const angle = i * step - Math.PI / 2;\n const x = cx + r * Math.cos(angle);\n const y = cy + r * Math.sin(angle);\n path += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;\n }\n return path + ' Z';\n}\n\nfunction generateHeartPath(cx: number, cy: number, size: number): string {\n const s = size / 30;\n return `M ${cx} ${cy + 12 * s} C ${cx - 20 * s} ${cy - 5 * s}, ${cx - 12 * s} ${cy - 18 * s}, ${cx} ${cy - 8 * s} C ${cx + 12 * s} ${cy - 18 * s}, ${cx + 20 * s} ${cy - 5 * s}, ${cx} ${cy + 12 * s}`;\n}\n\nfunction generateInfinityPath(cx: number, cy: number, w: number, h: number): string {\n const hw = w / 2;\n const hh = h / 2;\n return `M ${cx} ${cy} C ${cx + hw * 0.5} ${cy - hh}, ${cx + hw} ${cy - hh}, ${cx + hw} ${cy} C ${cx + hw} ${cy + hh}, ${cx + hw * 0.5} ${cy + hh}, ${cx} ${cy} C ${cx - hw * 0.5} ${cy + hh}, ${cx - hw} ${cy + hh}, ${cx - hw} ${cy} C ${cx - hw} ${cy - hh}, ${cx - hw * 0.5} ${cy - hh}, ${cx} ${cy}`;\n}\n\nfunction generateWavePath(cx: number, cy: number, w: number, amplitude: number, waves: number): string {\n const pts: string[] = [];\n const segs = waves * 20;\n const hw = w / 2;\n for (let i = 0; i <= segs; i++) {\n const x = cx - hw + (w * i) / segs;\n const y = cy + Math.sin((i / segs) * waves * 2 * Math.PI) * amplitude;\n pts.push(i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`);\n }\n for (let i = segs; i >= 0; i--) {\n const x = cx - hw + (w * i) / segs;\n const y = cy - Math.sin((i / segs) * waves * 2 * Math.PI) * amplitude;\n pts.push(`L ${x} ${y}`);\n }\n return pts.join(' ') + ' Z';\n}\n\nfunction OrbitItem({ item, index, totalItems, path, itemSize, rotation, progress, fill }: OrbitItemProps) {\n const itemOffset = fill ? (index / totalItems) * 100 : 0;\n\n const offsetDistance = useTransform(progress, (p: number) => {\n const offset = (((p + itemOffset) % 100) + 100) % 100;\n return `${offset}%`;\n });\n\n return (\n \n
    {item}
    \n \n );\n}\n\nexport default function OrbitImages({\n images = [],\n altPrefix = 'Orbiting image',\n shape = 'ellipse',\n customPath,\n baseWidth = 1400,\n radiusX = 700,\n radiusY = 170,\n radius = 300,\n starPoints = 5,\n starInnerRatio = 0.5,\n rotation = -8,\n duration = 40,\n itemSize = 64,\n direction = 'normal',\n fill = true,\n width = 100,\n height = 100,\n className = '',\n showPath = false,\n pathColor = 'rgba(0,0,0,0.1)',\n pathWidth = 2,\n easing = 'linear',\n paused = false,\n centerContent,\n responsive = false,\n}: OrbitImagesProps) {\n const containerRef = useRef(null);\n const [scale, setScale] = useState(null);\n\n const designCenterX = baseWidth / 2;\n const designCenterY = baseWidth / 2;\n\n const path = useMemo(() => {\n switch (shape) {\n case 'circle':\n return generateCirclePath(designCenterX, designCenterY, radius);\n case 'ellipse':\n return generateEllipsePath(designCenterX, designCenterY, radiusX, radiusY);\n case 'square':\n return generateSquarePath(designCenterX, designCenterY, radius * 2);\n case 'rectangle':\n return generateRectanglePath(designCenterX, designCenterY, radiusX * 2, radiusY * 2);\n case 'triangle':\n return generateTrianglePath(designCenterX, designCenterY, radius * 2);\n case 'star':\n return generateStarPath(designCenterX, designCenterY, radius, radius * starInnerRatio, starPoints);\n case 'heart':\n return generateHeartPath(designCenterX, designCenterY, radius * 2);\n case 'infinity':\n return generateInfinityPath(designCenterX, designCenterY, radiusX * 2, radiusY * 2);\n case 'wave':\n return generateWavePath(designCenterX, designCenterY, radiusX * 2, radiusY, 3);\n case 'custom':\n return customPath || generateCirclePath(designCenterX, designCenterY, radius);\n default:\n return generateEllipsePath(designCenterX, designCenterY, radiusX, radiusY);\n }\n }, [shape, customPath, designCenterX, designCenterY, radiusX, radiusY, radius, starPoints, starInnerRatio]);\n\n useLayoutEffect(() => {\n if (!responsive || !containerRef.current) return;\n const updateScale = () => {\n if (!containerRef.current) return;\n setScale(containerRef.current.clientWidth / baseWidth);\n };\n updateScale();\n const observer = new ResizeObserver(updateScale);\n observer.observe(containerRef.current);\n return () => observer.disconnect();\n }, [responsive, baseWidth]);\n\n const progress = useMotionValue(0);\n\n useEffect(() => {\n if (paused) return;\n const controls = animate(progress, direction === 'reverse' ? -100 : 100, {\n duration,\n ease: easing,\n repeat: Infinity,\n repeatType: 'loop',\n });\n return () => controls.stop();\n }, [progress, duration, easing, direction, paused]);\n\n const containerWidth = responsive ? '100%' : (typeof width === 'number' ? width : '100%');\n const containerHeight = responsive ? 'auto' : (typeof height === 'number' ? height : (typeof width === 'number' ? width : 'auto'));\n\n const items = images.map((src, index) => (\n \n ));\n\n return (\n \n \n \n {showPath && (\n \n \n \n )}\n\n {items.map((item, index) => (\n \n ))}\n \n \n\n {centerContent && (\n
    \n {centerContent}\n
    \n )}\n \n );\n}\n" + "path": "OrbitImages.tsx", + "content": "// Component created by Dominik Koch\n// https://x.com/dominikkoch\n\nimport { useMemo, useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react';\nimport { motion, useMotionValue, useTransform, animate, MotionValue } from 'motion/react';\nimport './OrbitImages.css';\n\ntype OrbitShape =\n | 'ellipse'\n | 'circle'\n | 'square'\n | 'rectangle'\n | 'triangle'\n | 'star'\n | 'heart'\n | 'infinity'\n | 'wave'\n | 'custom';\n\ninterface OrbitImagesProps {\n images?: string[];\n altPrefix?: string;\n shape?: OrbitShape;\n customPath?: string;\n baseWidth?: number;\n radiusX?: number;\n radiusY?: number;\n radius?: number;\n starPoints?: number;\n starInnerRatio?: number;\n rotation?: number;\n duration?: number;\n itemSize?: number;\n direction?: 'normal' | 'reverse';\n fill?: boolean;\n width?: number | '100%';\n height?: number | 'auto';\n className?: string;\n showPath?: boolean;\n pathColor?: string;\n pathWidth?: number;\n easing?: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';\n paused?: boolean;\n centerContent?: ReactNode;\n responsive?: boolean;\n}\n\ninterface OrbitItemProps {\n item: ReactNode;\n index: number;\n totalItems: number;\n path: string;\n itemSize: number;\n rotation: number;\n progress: MotionValue;\n fill: boolean;\n}\n\nfunction generateEllipsePath(cx: number, cy: number, rx: number, ry: number): string {\n return `M ${cx - rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx + rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy}`;\n}\n\nfunction generateCirclePath(cx: number, cy: number, r: number): string {\n return generateEllipsePath(cx, cy, r, r);\n}\n\nfunction generateSquarePath(cx: number, cy: number, size: number): string {\n const h = size / 2;\n return `M ${cx - h} ${cy - h} L ${cx + h} ${cy - h} L ${cx + h} ${cy + h} L ${cx - h} ${cy + h} Z`;\n}\n\nfunction generateRectanglePath(cx: number, cy: number, w: number, h: number): string {\n const hw = w / 2;\n const hh = h / 2;\n return `M ${cx - hw} ${cy - hh} L ${cx + hw} ${cy - hh} L ${cx + hw} ${cy + hh} L ${cx - hw} ${cy + hh} Z`;\n}\n\nfunction generateTrianglePath(cx: number, cy: number, size: number): string {\n const height = (size * Math.sqrt(3)) / 2;\n const hs = size / 2;\n return `M ${cx} ${cy - height / 1.5} L ${cx + hs} ${cy + height / 3} L ${cx - hs} ${cy + height / 3} Z`;\n}\n\nfunction generateStarPath(cx: number, cy: number, outerR: number, innerR: number, points: number): string {\n const step = Math.PI / points;\n let path = '';\n for (let i = 0; i < 2 * points; i++) {\n const r = i % 2 === 0 ? outerR : innerR;\n const angle = i * step - Math.PI / 2;\n const x = cx + r * Math.cos(angle);\n const y = cy + r * Math.sin(angle);\n path += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;\n }\n return path + ' Z';\n}\n\nfunction generateHeartPath(cx: number, cy: number, size: number): string {\n const s = size / 30;\n return `M ${cx} ${cy + 12 * s} C ${cx - 20 * s} ${cy - 5 * s}, ${cx - 12 * s} ${cy - 18 * s}, ${cx} ${cy - 8 * s} C ${cx + 12 * s} ${cy - 18 * s}, ${cx + 20 * s} ${cy - 5 * s}, ${cx} ${cy + 12 * s}`;\n}\n\nfunction generateInfinityPath(cx: number, cy: number, w: number, h: number): string {\n const hw = w / 2;\n const hh = h / 2;\n return `M ${cx} ${cy} C ${cx + hw * 0.5} ${cy - hh}, ${cx + hw} ${cy - hh}, ${cx + hw} ${cy} C ${cx + hw} ${cy + hh}, ${cx + hw * 0.5} ${cy + hh}, ${cx} ${cy} C ${cx - hw * 0.5} ${cy + hh}, ${cx - hw} ${cy + hh}, ${cx - hw} ${cy} C ${cx - hw} ${cy - hh}, ${cx - hw * 0.5} ${cy - hh}, ${cx} ${cy}`;\n}\n\nfunction generateWavePath(cx: number, cy: number, w: number, amplitude: number, waves: number): string {\n const pts: string[] = [];\n const segs = waves * 20;\n const hw = w / 2;\n for (let i = 0; i <= segs; i++) {\n const x = cx - hw + (w * i) / segs;\n const y = cy + Math.sin((i / segs) * waves * 2 * Math.PI) * amplitude;\n pts.push(i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`);\n }\n for (let i = segs; i >= 0; i--) {\n const x = cx - hw + (w * i) / segs;\n const y = cy - Math.sin((i / segs) * waves * 2 * Math.PI) * amplitude;\n pts.push(`L ${x} ${y}`);\n }\n return pts.join(' ') + ' Z';\n}\n\nfunction OrbitItem({ item, index, totalItems, path, itemSize, rotation, progress, fill }: OrbitItemProps) {\n const itemOffset = fill ? (index / totalItems) * 100 : 0;\n\n const offsetDistance = useTransform(progress, (p: number) => {\n const offset = (((p + itemOffset) % 100) + 100) % 100;\n return `${offset}%`;\n });\n\n return (\n \n
    {item}
    \n \n );\n}\n\nexport default function OrbitImages({\n images = [],\n altPrefix = 'Orbiting image',\n shape = 'ellipse',\n customPath,\n baseWidth = 1400,\n radiusX = 700,\n radiusY = 170,\n radius = 300,\n starPoints = 5,\n starInnerRatio = 0.5,\n rotation = -8,\n duration = 40,\n itemSize = 64,\n direction = 'normal',\n fill = true,\n width = 100,\n height = 100,\n className = '',\n showPath = false,\n pathColor = 'rgba(0,0,0,0.1)',\n pathWidth = 2,\n easing = 'linear',\n paused = false,\n centerContent,\n responsive = false,\n}: OrbitImagesProps) {\n const containerRef = useRef(null);\n const [scale, setScale] = useState(null);\n\n const designCenterX = baseWidth / 2;\n const designCenterY = baseWidth / 2;\n\n const path = useMemo(() => {\n switch (shape) {\n case 'circle':\n return generateCirclePath(designCenterX, designCenterY, radius);\n case 'ellipse':\n return generateEllipsePath(designCenterX, designCenterY, radiusX, radiusY);\n case 'square':\n return generateSquarePath(designCenterX, designCenterY, radius * 2);\n case 'rectangle':\n return generateRectanglePath(designCenterX, designCenterY, radiusX * 2, radiusY * 2);\n case 'triangle':\n return generateTrianglePath(designCenterX, designCenterY, radius * 2);\n case 'star':\n return generateStarPath(designCenterX, designCenterY, radius, radius * starInnerRatio, starPoints);\n case 'heart':\n return generateHeartPath(designCenterX, designCenterY, radius * 2);\n case 'infinity':\n return generateInfinityPath(designCenterX, designCenterY, radiusX * 2, radiusY * 2);\n case 'wave':\n return generateWavePath(designCenterX, designCenterY, radiusX * 2, radiusY, 3);\n case 'custom':\n return customPath || generateCirclePath(designCenterX, designCenterY, radius);\n default:\n return generateEllipsePath(designCenterX, designCenterY, radiusX, radiusY);\n }\n }, [shape, customPath, designCenterX, designCenterY, radiusX, radiusY, radius, starPoints, starInnerRatio]);\n\n useLayoutEffect(() => {\n if (!responsive || !containerRef.current) return;\n const updateScale = () => {\n if (!containerRef.current) return;\n setScale(containerRef.current.clientWidth / baseWidth);\n };\n updateScale();\n const observer = new ResizeObserver(updateScale);\n observer.observe(containerRef.current);\n return () => observer.disconnect();\n }, [responsive, baseWidth]);\n\n const progress = useMotionValue(0);\n\n useEffect(() => {\n if (paused) return;\n const controls = animate(progress, direction === 'reverse' ? -100 : 100, {\n duration,\n ease: easing,\n repeat: Infinity,\n repeatType: 'loop',\n });\n return () => controls.stop();\n }, [progress, duration, easing, direction, paused]);\n\n const containerWidth = responsive ? '100%' : (typeof width === 'number' ? width : '100%');\n const containerHeight = responsive ? 'auto' : (typeof height === 'number' ? height : (typeof width === 'number' ? width : 'auto'));\n\n const items = images.map((src, index) => (\n \n ));\n\n return (\n \n \n \n {showPath && (\n \n \n \n )}\n\n {items.map((item, index) => (\n \n ))}\n \n \n\n {centerContent && (\n
    \n {centerContent}\n
    \n )}\n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/OrbitImages-TS-TW.json b/public/r/OrbitImages-TS-TW.json index 1f267af1..9ceb84c4 100644 --- a/public/r/OrbitImages-TS-TW.json +++ b/public/r/OrbitImages-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "OrbitImages/OrbitImages.tsx", - "content": "// Component created by Dominik Koch\n// https://x.com/dominikkoch\n\nimport { useMemo, useEffect, useLayoutEffect, useRef, useState, ReactNode } from 'react';\nimport { motion, useMotionValue, useTransform, animate, MotionValue } from 'motion/react';\n\ntype OrbitShape =\n | 'ellipse'\n | 'circle'\n | 'square'\n | 'rectangle'\n | 'triangle'\n | 'star'\n | 'heart'\n | 'infinity'\n | 'wave'\n | 'custom';\n\ninterface OrbitImagesProps {\n images?: string[];\n altPrefix?: string;\n shape?: OrbitShape;\n customPath?: string;\n baseWidth?: number;\n radiusX?: number;\n radiusY?: number;\n radius?: number;\n starPoints?: number;\n starInnerRatio?: number;\n rotation?: number;\n duration?: number;\n itemSize?: number;\n direction?: 'normal' | 'reverse';\n fill?: boolean;\n width?: number | '100%';\n height?: number | 'auto';\n className?: string;\n showPath?: boolean;\n pathColor?: string;\n pathWidth?: number;\n easing?: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';\n paused?: boolean;\n centerContent?: ReactNode;\n responsive?: boolean;\n}\n\ninterface OrbitItemProps {\n item: ReactNode;\n index: number;\n totalItems: number;\n path: string;\n itemSize: number;\n rotation: number;\n progress: MotionValue;\n fill: boolean;\n}\n\nfunction generateEllipsePath(cx: number, cy: number, rx: number, ry: number): string {\n return `M ${cx - rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx + rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy}`;\n}\n\nfunction generateCirclePath(cx: number, cy: number, r: number): string {\n return generateEllipsePath(cx, cy, r, r);\n}\n\nfunction generateSquarePath(cx: number, cy: number, size: number): string {\n const h = size / 2;\n return `M ${cx - h} ${cy - h} L ${cx + h} ${cy - h} L ${cx + h} ${cy + h} L ${cx - h} ${cy + h} Z`;\n}\n\nfunction generateRectanglePath(cx: number, cy: number, w: number, h: number): string {\n const hw = w / 2;\n const hh = h / 2;\n return `M ${cx - hw} ${cy - hh} L ${cx + hw} ${cy - hh} L ${cx + hw} ${cy + hh} L ${cx - hw} ${cy + hh} Z`;\n}\n\nfunction generateTrianglePath(cx: number, cy: number, size: number): string {\n const height = (size * Math.sqrt(3)) / 2;\n const hs = size / 2;\n return `M ${cx} ${cy - height / 1.5} L ${cx + hs} ${cy + height / 3} L ${cx - hs} ${cy + height / 3} Z`;\n}\n\nfunction generateStarPath(cx: number, cy: number, outerR: number, innerR: number, points: number): string {\n const step = Math.PI / points;\n let path = '';\n for (let i = 0; i < 2 * points; i++) {\n const r = i % 2 === 0 ? outerR : innerR;\n const angle = i * step - Math.PI / 2;\n const x = cx + r * Math.cos(angle);\n const y = cy + r * Math.sin(angle);\n path += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;\n }\n return path + ' Z';\n}\n\nfunction generateHeartPath(cx: number, cy: number, size: number): string {\n const s = size / 30;\n return `M ${cx} ${cy + 12 * s} C ${cx - 20 * s} ${cy - 5 * s}, ${cx - 12 * s} ${cy - 18 * s}, ${cx} ${cy - 8 * s} C ${cx + 12 * s} ${cy - 18 * s}, ${cx + 20 * s} ${cy - 5 * s}, ${cx} ${cy + 12 * s}`;\n}\n\nfunction generateInfinityPath(cx: number, cy: number, w: number, h: number): string {\n const hw = w / 2;\n const hh = h / 2;\n return `M ${cx} ${cy} C ${cx + hw * 0.5} ${cy - hh}, ${cx + hw} ${cy - hh}, ${cx + hw} ${cy} C ${cx + hw} ${cy + hh}, ${cx + hw * 0.5} ${cy + hh}, ${cx} ${cy} C ${cx - hw * 0.5} ${cy + hh}, ${cx - hw} ${cy + hh}, ${cx - hw} ${cy} C ${cx - hw} ${cy - hh}, ${cx - hw * 0.5} ${cy - hh}, ${cx} ${cy}`;\n}\n\nfunction generateWavePath(cx: number, cy: number, w: number, amplitude: number, waves: number): string {\n const pts: string[] = [];\n const segs = waves * 20;\n const hw = w / 2;\n for (let i = 0; i <= segs; i++) {\n const x = cx - hw + (w * i) / segs;\n const y = cy + Math.sin((i / segs) * waves * 2 * Math.PI) * amplitude;\n pts.push(i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`);\n }\n for (let i = segs; i >= 0; i--) {\n const x = cx - hw + (w * i) / segs;\n const y = cy - Math.sin((i / segs) * waves * 2 * Math.PI) * amplitude;\n pts.push(`L ${x} ${y}`);\n }\n return pts.join(' ') + ' Z';\n}\n\nfunction OrbitItem({ item, index, totalItems, path, itemSize, rotation, progress, fill }: OrbitItemProps) {\n const itemOffset = fill ? (index / totalItems) * 100 : 0;\n\n const offsetDistance = useTransform(progress, (p: number) => {\n const offset = (((p + itemOffset) % 100) + 100) % 100;\n return `${offset}%`;\n });\n\n return (\n \n
    {item}
    \n \n );\n}\n\nexport default function OrbitImages({\n images = [],\n altPrefix = 'Orbiting image',\n shape = 'ellipse',\n customPath,\n baseWidth = 1400,\n radiusX = 700,\n radiusY = 170,\n radius = 300,\n starPoints = 5,\n starInnerRatio = 0.5,\n rotation = -8,\n duration = 40,\n itemSize = 64,\n direction = 'normal',\n fill = true,\n width = 100,\n height = 100,\n className = '',\n showPath = false,\n pathColor = 'rgba(0,0,0,0.1)',\n pathWidth = 2,\n easing = 'linear',\n paused = false,\n centerContent,\n responsive = false,\n}: OrbitImagesProps) {\n const containerRef = useRef(null);\n const [scale, setScale] = useState(null);\n\n const designCenterX = baseWidth / 2;\n const designCenterY = baseWidth / 2;\n\n const path = useMemo(() => {\n switch (shape) {\n case 'circle':\n return generateCirclePath(designCenterX, designCenterY, radius);\n case 'ellipse':\n return generateEllipsePath(designCenterX, designCenterY, radiusX, radiusY);\n case 'square':\n return generateSquarePath(designCenterX, designCenterY, radius * 2);\n case 'rectangle':\n return generateRectanglePath(designCenterX, designCenterY, radiusX * 2, radiusY * 2);\n case 'triangle':\n return generateTrianglePath(designCenterX, designCenterY, radius * 2);\n case 'star':\n return generateStarPath(designCenterX, designCenterY, radius, radius * starInnerRatio, starPoints);\n case 'heart':\n return generateHeartPath(designCenterX, designCenterY, radius * 2);\n case 'infinity':\n return generateInfinityPath(designCenterX, designCenterY, radiusX * 2, radiusY * 2);\n case 'wave':\n return generateWavePath(designCenterX, designCenterY, radiusX * 2, radiusY, 3);\n case 'custom':\n return customPath || generateCirclePath(designCenterX, designCenterY, radius);\n default:\n return generateEllipsePath(designCenterX, designCenterY, radiusX, radiusY);\n }\n }, [shape, customPath, designCenterX, designCenterY, radiusX, radiusY, radius, starPoints, starInnerRatio]);\n\n useLayoutEffect(() => {\n if (!responsive || !containerRef.current) return;\n const updateScale = () => {\n if (!containerRef.current) return;\n setScale(containerRef.current.clientWidth / baseWidth);\n };\n updateScale();\n const observer = new ResizeObserver(updateScale);\n observer.observe(containerRef.current);\n return () => observer.disconnect();\n }, [responsive, baseWidth]);\n\n const progress = useMotionValue(0);\n\n useEffect(() => {\n if (paused) return;\n const controls = animate(progress, direction === 'reverse' ? -100 : 100, {\n duration,\n ease: easing,\n repeat: Infinity,\n repeatType: 'loop',\n });\n return () => controls.stop();\n }, [progress, duration, easing, direction, paused]);\n\n const containerWidth = responsive ? '100%' : (typeof width === 'number' ? width : '100%');\n const containerHeight = responsive ? 'auto' : (typeof height === 'number' ? height : (typeof width === 'number' ? width : 'auto'));\n\n const items = images.map((src, index) => (\n \n ));\n\n return (\n \n \n \n {showPath && (\n \n \n \n )}\n\n {items.map((item, index) => (\n \n ))}\n \n \n\n {centerContent && (\n
    \n {centerContent}\n
    \n )}\n \n );\n}\n" + "content": "// Component created by Dominik Koch\n// https://x.com/dominikkoch\n\nimport { useMemo, useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react';\nimport { motion, useMotionValue, useTransform, animate, MotionValue } from 'motion/react';\n\ntype OrbitShape =\n | 'ellipse'\n | 'circle'\n | 'square'\n | 'rectangle'\n | 'triangle'\n | 'star'\n | 'heart'\n | 'infinity'\n | 'wave'\n | 'custom';\n\ninterface OrbitImagesProps {\n images?: string[];\n altPrefix?: string;\n shape?: OrbitShape;\n customPath?: string;\n baseWidth?: number;\n radiusX?: number;\n radiusY?: number;\n radius?: number;\n starPoints?: number;\n starInnerRatio?: number;\n rotation?: number;\n duration?: number;\n itemSize?: number;\n direction?: 'normal' | 'reverse';\n fill?: boolean;\n width?: number | '100%';\n height?: number | 'auto';\n className?: string;\n showPath?: boolean;\n pathColor?: string;\n pathWidth?: number;\n easing?: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';\n paused?: boolean;\n centerContent?: ReactNode;\n responsive?: boolean;\n}\n\ninterface OrbitItemProps {\n item: ReactNode;\n index: number;\n totalItems: number;\n path: string;\n itemSize: number;\n rotation: number;\n progress: MotionValue;\n fill: boolean;\n}\n\nfunction generateEllipsePath(cx: number, cy: number, rx: number, ry: number): string {\n return `M ${cx - rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx + rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy}`;\n}\n\nfunction generateCirclePath(cx: number, cy: number, r: number): string {\n return generateEllipsePath(cx, cy, r, r);\n}\n\nfunction generateSquarePath(cx: number, cy: number, size: number): string {\n const h = size / 2;\n return `M ${cx - h} ${cy - h} L ${cx + h} ${cy - h} L ${cx + h} ${cy + h} L ${cx - h} ${cy + h} Z`;\n}\n\nfunction generateRectanglePath(cx: number, cy: number, w: number, h: number): string {\n const hw = w / 2;\n const hh = h / 2;\n return `M ${cx - hw} ${cy - hh} L ${cx + hw} ${cy - hh} L ${cx + hw} ${cy + hh} L ${cx - hw} ${cy + hh} Z`;\n}\n\nfunction generateTrianglePath(cx: number, cy: number, size: number): string {\n const height = (size * Math.sqrt(3)) / 2;\n const hs = size / 2;\n return `M ${cx} ${cy - height / 1.5} L ${cx + hs} ${cy + height / 3} L ${cx - hs} ${cy + height / 3} Z`;\n}\n\nfunction generateStarPath(cx: number, cy: number, outerR: number, innerR: number, points: number): string {\n const step = Math.PI / points;\n let path = '';\n for (let i = 0; i < 2 * points; i++) {\n const r = i % 2 === 0 ? outerR : innerR;\n const angle = i * step - Math.PI / 2;\n const x = cx + r * Math.cos(angle);\n const y = cy + r * Math.sin(angle);\n path += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;\n }\n return path + ' Z';\n}\n\nfunction generateHeartPath(cx: number, cy: number, size: number): string {\n const s = size / 30;\n return `M ${cx} ${cy + 12 * s} C ${cx - 20 * s} ${cy - 5 * s}, ${cx - 12 * s} ${cy - 18 * s}, ${cx} ${cy - 8 * s} C ${cx + 12 * s} ${cy - 18 * s}, ${cx + 20 * s} ${cy - 5 * s}, ${cx} ${cy + 12 * s}`;\n}\n\nfunction generateInfinityPath(cx: number, cy: number, w: number, h: number): string {\n const hw = w / 2;\n const hh = h / 2;\n return `M ${cx} ${cy} C ${cx + hw * 0.5} ${cy - hh}, ${cx + hw} ${cy - hh}, ${cx + hw} ${cy} C ${cx + hw} ${cy + hh}, ${cx + hw * 0.5} ${cy + hh}, ${cx} ${cy} C ${cx - hw * 0.5} ${cy + hh}, ${cx - hw} ${cy + hh}, ${cx - hw} ${cy} C ${cx - hw} ${cy - hh}, ${cx - hw * 0.5} ${cy - hh}, ${cx} ${cy}`;\n}\n\nfunction generateWavePath(cx: number, cy: number, w: number, amplitude: number, waves: number): string {\n const pts: string[] = [];\n const segs = waves * 20;\n const hw = w / 2;\n for (let i = 0; i <= segs; i++) {\n const x = cx - hw + (w * i) / segs;\n const y = cy + Math.sin((i / segs) * waves * 2 * Math.PI) * amplitude;\n pts.push(i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`);\n }\n for (let i = segs; i >= 0; i--) {\n const x = cx - hw + (w * i) / segs;\n const y = cy - Math.sin((i / segs) * waves * 2 * Math.PI) * amplitude;\n pts.push(`L ${x} ${y}`);\n }\n return pts.join(' ') + ' Z';\n}\n\nfunction OrbitItem({ item, index, totalItems, path, itemSize, rotation, progress, fill }: OrbitItemProps) {\n const itemOffset = fill ? (index / totalItems) * 100 : 0;\n\n const offsetDistance = useTransform(progress, (p: number) => {\n const offset = (((p + itemOffset) % 100) + 100) % 100;\n return `${offset}%`;\n });\n\n return (\n \n
    {item}
    \n \n );\n}\n\nexport default function OrbitImages({\n images = [],\n altPrefix = 'Orbiting image',\n shape = 'ellipse',\n customPath,\n baseWidth = 1400,\n radiusX = 700,\n radiusY = 170,\n radius = 300,\n starPoints = 5,\n starInnerRatio = 0.5,\n rotation = -8,\n duration = 40,\n itemSize = 64,\n direction = 'normal',\n fill = true,\n width = 100,\n height = 100,\n className = '',\n showPath = false,\n pathColor = 'rgba(0,0,0,0.1)',\n pathWidth = 2,\n easing = 'linear',\n paused = false,\n centerContent,\n responsive = false,\n}: OrbitImagesProps) {\n const containerRef = useRef(null);\n const [scale, setScale] = useState(null);\n\n const designCenterX = baseWidth / 2;\n const designCenterY = baseWidth / 2;\n\n const path = useMemo(() => {\n switch (shape) {\n case 'circle':\n return generateCirclePath(designCenterX, designCenterY, radius);\n case 'ellipse':\n return generateEllipsePath(designCenterX, designCenterY, radiusX, radiusY);\n case 'square':\n return generateSquarePath(designCenterX, designCenterY, radius * 2);\n case 'rectangle':\n return generateRectanglePath(designCenterX, designCenterY, radiusX * 2, radiusY * 2);\n case 'triangle':\n return generateTrianglePath(designCenterX, designCenterY, radius * 2);\n case 'star':\n return generateStarPath(designCenterX, designCenterY, radius, radius * starInnerRatio, starPoints);\n case 'heart':\n return generateHeartPath(designCenterX, designCenterY, radius * 2);\n case 'infinity':\n return generateInfinityPath(designCenterX, designCenterY, radiusX * 2, radiusY * 2);\n case 'wave':\n return generateWavePath(designCenterX, designCenterY, radiusX * 2, radiusY, 3);\n case 'custom':\n return customPath || generateCirclePath(designCenterX, designCenterY, radius);\n default:\n return generateEllipsePath(designCenterX, designCenterY, radiusX, radiusY);\n }\n }, [shape, customPath, designCenterX, designCenterY, radiusX, radiusY, radius, starPoints, starInnerRatio]);\n\n useLayoutEffect(() => {\n if (!responsive || !containerRef.current) return;\n const updateScale = () => {\n if (!containerRef.current) return;\n setScale(containerRef.current.clientWidth / baseWidth);\n };\n updateScale();\n const observer = new ResizeObserver(updateScale);\n observer.observe(containerRef.current);\n return () => observer.disconnect();\n }, [responsive, baseWidth]);\n\n const progress = useMotionValue(0);\n\n useEffect(() => {\n if (paused) return;\n const controls = animate(progress, direction === 'reverse' ? -100 : 100, {\n duration,\n ease: easing,\n repeat: Infinity,\n repeatType: 'loop',\n });\n return () => controls.stop();\n }, [progress, duration, easing, direction, paused]);\n\n const containerWidth = responsive ? '100%' : (typeof width === 'number' ? width : '100%');\n const containerHeight = responsive ? 'auto' : (typeof height === 'number' ? height : (typeof width === 'number' ? width : 'auto'));\n\n const items = images.map((src, index) => (\n \n ));\n\n return (\n \n \n \n {showPath && (\n \n \n \n )}\n\n {items.map((item, index) => (\n \n ))}\n \n \n\n {centerContent && (\n
    \n {centerContent}\n
    \n )}\n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/ParticleText-JS-CSS.json b/public/r/ParticleText-JS-CSS.json index 3a6acb98..7bb3b3cc 100644 --- a/public/r/ParticleText-JS-CSS.json +++ b/public/r/ParticleText-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "ParticleText/ParticleText.css", + "type": "registry:file", + "path": "ParticleText.css", + "target": "@components/ParticleText.css", "content": ".particle-text {\n position: relative;\n display: block;\n width: 100%;\n height: 100%;\n min-height: 240px;\n overflow: hidden;\n touch-action: none;\n isolation: isolate;\n}\n\n.particle-text__canvas {\n position: absolute;\n inset: 0;\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.particle-text__sr {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n" }, { "type": "registry:component", - "path": "ParticleText/ParticleText.jsx", + "path": "ParticleText.jsx", "content": "import { useEffect, useRef } from 'react';\nimport './ParticleText.css';\n\nconst hexToRgb = hex => {\n const clean = hex.replace('#', '').trim();\n if (!/^[0-9a-fA-F]{6}$/.test(clean)) return null;\n return {\n r: parseInt(clean.slice(0, 2), 16),\n g: parseInt(clean.slice(2, 4), 16),\n b: parseInt(clean.slice(4, 6), 16)\n };\n};\n\nconst mixRgb = (from, to, amount) => ({\n r: Math.round(from.r + (to.r - from.r) * amount),\n g: Math.round(from.g + (to.g - from.g) * amount),\n b: Math.round(from.b + (to.b - from.b) * amount)\n});\n\nconst rgbToCss = rgb => `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;\n\nconst clamp = (value, min, max) => Math.min(Math.max(value, min), max);\nconst easeOutCubic = t => 1 - Math.pow(1 - t, 3);\n\nconst resolveFontSize = (value, container, fontWeight, fontFamily) => {\n if (typeof value === 'number') return value;\n\n const probe = document.createElement('span');\n probe.textContent = 'M';\n probe.style.position = 'absolute';\n probe.style.visibility = 'hidden';\n probe.style.pointerEvents = 'none';\n probe.style.fontSize = value;\n probe.style.fontWeight = String(fontWeight);\n probe.style.fontFamily = fontFamily;\n container.appendChild(probe);\n const size = parseFloat(window.getComputedStyle(probe).fontSize) || 96;\n probe.remove();\n return size;\n};\n\nconst waitForFonts = async font => {\n if (!('fonts' in document)) return;\n\n try {\n await document.fonts.load(font);\n } catch {}\n\n await document.fonts.ready;\n};\n\nconst ParticleText = ({\n text = 'React Bits',\n particleSize = 2,\n density = 4,\n color = '#ffffff',\n highlightColor = '#8b5cf6',\n scatter = 180,\n gatherDuration = 1600,\n stagger = 420,\n pointerRepel = 40,\n repelRadius = 120,\n idleDrift = 0.7,\n trigger = 'mount',\n fontSize = 'clamp(3rem, 12vw, 8rem)',\n fontWeight = 800,\n fontFamily = 'inherit',\n glow = true,\n className = '',\n style\n}) => {\n const containerRef = useRef(null);\n const canvasRef = useRef(null);\n\n useEffect(() => {\n if (typeof window === 'undefined') return undefined;\n\n const container = containerRef.current;\n const canvas = canvasRef.current;\n if (!container || !canvas) return undefined;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return undefined;\n\n let particles = [];\n let animationFrame = null;\n let resizeFrame = null;\n let buildId = 0;\n let gathering = false;\n let gatherStart = 0;\n let reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;\n let width = 0;\n let height = 0;\n let dpr = 1;\n\n const pointer = {\n active: false,\n x: 0,\n y: 0,\n smoothX: 0,\n smoothY: 0\n };\n\n const startGather = (fromScatter = true) => {\n if (!particles.length) return;\n\n const now = performance.now();\n const spread = reducedMotion ? 0 : scatter;\n\n particles.forEach(particle => {\n if (fromScatter) {\n const angle = particle.seed * Math.PI * 2;\n const distance = spread * (0.35 + particle.depth * 0.75);\n particle.x = particle.targetX + Math.cos(angle) * distance + (particle.depth - 0.5) * spread * 0.55;\n particle.y = particle.targetY + Math.sin(angle) * distance + (particle.seed - 0.5) * spread * 0.55;\n }\n\n particle.startX = particle.x;\n particle.startY = particle.y;\n particle.delay = reducedMotion ? 0 : particle.seed * stagger;\n });\n\n gatherStart = now;\n gathering = true;\n };\n\n const drawParticle = particle => {\n const size = particle.size;\n ctx.fillStyle = particle.color;\n\n if (size <= 2.1) {\n ctx.fillRect(particle.x - size / 2, particle.y - size / 2, size, size);\n return;\n }\n\n ctx.beginPath();\n ctx.arc(particle.x, particle.y, size / 2, 0, Math.PI * 2);\n ctx.fill();\n };\n\n const render = now => {\n ctx.clearRect(0, 0, width, height);\n\n if (glow && !reducedMotion) {\n ctx.shadowBlur = particleSize * 3;\n ctx.shadowColor = highlightColor;\n } else {\n ctx.shadowBlur = 0;\n }\n\n pointer.smoothX += (pointer.x - pointer.smoothX) * 0.18;\n pointer.smoothY += (pointer.y - pointer.smoothY) * 0.18;\n\n let complete = true;\n\n particles.forEach(particle => {\n let baseX = particle.targetX;\n let baseY = particle.targetY;\n let progress = 1;\n\n if (gathering) {\n const local = (now - gatherStart - particle.delay) / Math.max(1, reducedMotion ? 1 : gatherDuration);\n progress = clamp(local, 0, 1);\n const eased = easeOutCubic(progress);\n baseX = particle.startX + (particle.targetX - particle.startX) * eased;\n baseY = particle.startY + (particle.targetY - particle.startY) * eased;\n if (progress < 1) complete = false;\n } else if (!reducedMotion && idleDrift > 0) {\n const driftTime = now * 0.001;\n baseX += Math.sin(driftTime * 0.9 + particle.seed * 10) * idleDrift * particle.depth;\n baseY += Math.cos(driftTime * 0.75 + particle.depth * 10) * idleDrift * particle.depth;\n }\n\n if (pointer.active && !reducedMotion && pointerRepel > 0 && repelRadius > 0) {\n const dx = baseX - pointer.smoothX;\n const dy = baseY - pointer.smoothY;\n const distance = Math.hypot(dx, dy);\n if (distance > 0 && distance < repelRadius) {\n const force = Math.pow(1 - distance / repelRadius, 2) * pointerRepel;\n baseX += (dx / distance) * force;\n baseY += (dy / distance) * force;\n }\n }\n\n const follow = reducedMotion ? 1 : 0.22;\n particle.x += (baseX - particle.x) * follow;\n particle.y += (baseY - particle.y) * follow;\n\n ctx.globalAlpha = clamp(0.35 + progress * 0.65, 0, 1);\n drawParticle(particle);\n });\n\n ctx.globalAlpha = 1;\n ctx.shadowBlur = 0;\n\n if (gathering && complete) {\n gathering = false;\n }\n\n animationFrame = window.requestAnimationFrame(render);\n };\n\n const ensureRenderLoop = () => {\n if (animationFrame === null) {\n animationFrame = window.requestAnimationFrame(render);\n }\n };\n\n const sampleText = async () => {\n const currentBuild = ++buildId;\n const rect = container.getBoundingClientRect();\n width = Math.floor(rect.width);\n height = Math.floor(rect.height);\n\n if (width <= 0 || height <= 0) return;\n\n dpr = Math.min(window.devicePixelRatio || 1, 2);\n canvas.width = Math.max(1, Math.floor(width * dpr));\n canvas.height = Math.max(1, Math.floor(height * dpr));\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n const computed = window.getComputedStyle(container);\n const resolvedFamily = fontFamily === 'inherit' ? computed.fontFamily || 'sans-serif' : fontFamily;\n let resolvedSize = resolveFontSize(fontSize, container, fontWeight, resolvedFamily);\n let font = `${fontWeight} ${resolvedSize}px ${resolvedFamily}`;\n\n await waitForFonts(font);\n if (currentBuild !== buildId) return;\n\n const offscreen = document.createElement('canvas');\n const offCtx = offscreen.getContext('2d', { willReadFrequently: true });\n if (!offCtx) return;\n\n const content = String(text || ' ');\n const maxTextWidth = width * 0.92;\n offCtx.font = font;\n let metrics = offCtx.measureText(content);\n const measuredWidth = Math.max(1, metrics.width);\n if (measuredWidth > maxTextWidth) {\n resolvedSize = Math.max(18, resolvedSize * (maxTextWidth / measuredWidth));\n font = `${fontWeight} ${resolvedSize}px ${resolvedFamily}`;\n await waitForFonts(font);\n if (currentBuild !== buildId) return;\n offCtx.font = font;\n metrics = offCtx.measureText(content);\n }\n\n const left = Math.ceil(metrics.actualBoundingBoxLeft || 0);\n const right = Math.ceil(metrics.actualBoundingBoxRight || metrics.width);\n const ascent = Math.ceil(metrics.actualBoundingBoxAscent || resolvedSize * 0.78);\n const descent = Math.ceil(metrics.actualBoundingBoxDescent || resolvedSize * 0.22);\n const padding = Math.max(12, Math.ceil(resolvedSize * 0.08));\n const textWidth = Math.max(1, left + right);\n const textHeight = Math.max(1, ascent + descent);\n\n offscreen.width = textWidth + padding * 2;\n offscreen.height = textHeight + padding * 2;\n offCtx.clearRect(0, 0, offscreen.width, offscreen.height);\n offCtx.font = font;\n offCtx.textAlign = 'left';\n offCtx.textBaseline = 'alphabetic';\n offCtx.fillStyle = '#ffffff';\n offCtx.fillText(content, padding - left, padding + ascent);\n\n const imageData = offCtx.getImageData(0, 0, offscreen.width, offscreen.height);\n const targets = [];\n const step = Math.max(2, Math.floor(density));\n\n for (let y = 0; y < offscreen.height; y += step) {\n for (let x = 0; x < offscreen.width; x += step) {\n const alpha = imageData.data[(y * offscreen.width + x) * 4 + 3];\n if (alpha > 40) {\n targets.push({\n x: width / 2 - offscreen.width / 2 + x,\n y: height / 2 - offscreen.height / 2 + y,\n alpha: alpha / 255\n });\n }\n }\n }\n\n const maxParticles = Math.max(900, Math.min(5200, Math.floor((width * height) / 90)));\n const stride = Math.max(1, Math.ceil(targets.length / maxParticles));\n const baseRgb = hexToRgb(color);\n const highlightRgb = hexToRgb(highlightColor);\n const selected = targets.filter((_, index) => index % stride === 0);\n\n particles = selected.map((target, index) => {\n const seed = ((index * 9301 + 49297) % 233280) / 233280;\n const depth = 0.45 + (((index * 233 + 97) % 1000) / 1000) * 0.9;\n const blend = baseRgb && highlightRgb ? clamp(target.x / Math.max(1, width) + (seed - 0.5) * 0.35, 0, 1) : 0;\n const particleColor = baseRgb && highlightRgb ? rgbToCss(mixRgb(baseRgb, highlightRgb, blend)) : color;\n const angle = seed * Math.PI * 2;\n const distance = (reducedMotion ? 0 : scatter) * (0.35 + depth * 0.75);\n const startX = target.x + Math.cos(angle) * distance + (seed - 0.5) * scatter * 0.45;\n const startY = target.y + Math.sin(angle) * distance + (depth - 0.9) * scatter * 0.45;\n\n return {\n x: reducedMotion ? target.x : startX,\n y: reducedMotion ? target.y : startY,\n startX,\n startY,\n targetX: target.x,\n targetY: target.y,\n size: Math.max(0.6, particleSize * (0.75 + target.alpha * 0.45)),\n color: particleColor,\n seed,\n depth,\n delay: seed * stagger\n };\n });\n\n pointer.x = width / 2;\n pointer.y = height / 2;\n pointer.smoothX = pointer.x;\n pointer.smoothY = pointer.y;\n\n if (reducedMotion) {\n particles.forEach(particle => {\n particle.x = particle.targetX;\n particle.y = particle.targetY;\n particle.startX = particle.targetX;\n particle.startY = particle.targetY;\n particle.delay = 0;\n });\n gathering = false;\n } else {\n startGather(false);\n }\n\n ensureRenderLoop();\n };\n\n const queueSample = () => {\n if (resizeFrame) window.cancelAnimationFrame(resizeFrame);\n resizeFrame = window.requestAnimationFrame(sampleText);\n };\n\n const handlePointerMove = event => {\n const rect = canvas.getBoundingClientRect();\n pointer.x = event.clientX - rect.left;\n pointer.y = event.clientY - rect.top;\n pointer.active = true;\n };\n\n const handlePointerLeave = () => {\n pointer.active = false;\n };\n\n const handlePointerEnter = event => {\n handlePointerMove(event);\n if (trigger === 'hover') startGather(true);\n };\n\n const handleClick = () => {\n if (trigger === 'click') startGather(true);\n };\n\n const reduceMotionQuery = window.matchMedia?.('(prefers-reduced-motion: reduce)');\n const handleReduceMotionChange = event => {\n reducedMotion = event.matches;\n sampleText();\n };\n\n reduceMotionQuery?.addEventListener('change', handleReduceMotionChange);\n canvas.addEventListener('pointerenter', handlePointerEnter);\n canvas.addEventListener('pointermove', handlePointerMove);\n canvas.addEventListener('pointerleave', handlePointerLeave);\n canvas.addEventListener('click', handleClick);\n\n const resizeObserver = new ResizeObserver(queueSample);\n resizeObserver.observe(container);\n sampleText();\n\n return () => {\n buildId += 1;\n resizeObserver.disconnect();\n reduceMotionQuery?.removeEventListener('change', handleReduceMotionChange);\n canvas.removeEventListener('pointerenter', handlePointerEnter);\n canvas.removeEventListener('pointermove', handlePointerMove);\n canvas.removeEventListener('pointerleave', handlePointerLeave);\n canvas.removeEventListener('click', handleClick);\n\n if (animationFrame !== null) window.cancelAnimationFrame(animationFrame);\n if (resizeFrame !== null) window.cancelAnimationFrame(resizeFrame);\n };\n }, [\n text,\n particleSize,\n density,\n color,\n highlightColor,\n scatter,\n gatherDuration,\n stagger,\n pointerRepel,\n repelRadius,\n idleDrift,\n trigger,\n fontSize,\n fontWeight,\n fontFamily,\n glow\n ]);\n\n return (\n
    \n \n {text}\n
    \n );\n};\n\nexport default ParticleText;\n" } ], diff --git a/public/r/ParticleText-TS-CSS.json b/public/r/ParticleText-TS-CSS.json index 1561099b..9aee4939 100644 --- a/public/r/ParticleText-TS-CSS.json +++ b/public/r/ParticleText-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "ParticleText/ParticleText.css", + "type": "registry:file", + "path": "ParticleText.css", + "target": "@components/ParticleText.css", "content": ".particle-text {\n position: relative;\n display: block;\n width: 100%;\n height: 100%;\n min-height: 240px;\n overflow: hidden;\n touch-action: none;\n isolation: isolate;\n}\n\n.particle-text__canvas {\n position: absolute;\n inset: 0;\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.particle-text__sr {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n" }, { "type": "registry:component", - "path": "ParticleText/ParticleText.tsx", + "path": "ParticleText.tsx", "content": "import { useEffect, useRef, type CSSProperties } from 'react';\nimport './ParticleText.css';\n\nexport interface ParticleTextProps {\n text?: string;\n particleSize?: number;\n density?: number;\n color?: string;\n highlightColor?: string;\n scatter?: number;\n gatherDuration?: number;\n stagger?: number;\n pointerRepel?: number;\n repelRadius?: number;\n idleDrift?: number;\n trigger?: 'mount' | 'hover' | 'click';\n fontSize?: number | string;\n fontWeight?: number | string;\n fontFamily?: string;\n glow?: boolean;\n className?: string;\n style?: CSSProperties;\n}\n\ntype Rgb = { r: number; g: number; b: number };\ntype Target = { x: number; y: number; alpha: number };\ntype Particle = {\n x: number;\n y: number;\n startX: number;\n startY: number;\n targetX: number;\n targetY: number;\n size: number;\n color: string;\n seed: number;\n depth: number;\n delay: number;\n};\n\nconst hexToRgb = (hex: string): Rgb | null => {\n const clean = hex.replace('#', '').trim();\n if (!/^[0-9a-fA-F]{6}$/.test(clean)) return null;\n return {\n r: parseInt(clean.slice(0, 2), 16),\n g: parseInt(clean.slice(2, 4), 16),\n b: parseInt(clean.slice(4, 6), 16)\n };\n};\n\nconst mixRgb = (from: Rgb, to: Rgb, amount: number): Rgb => ({\n r: Math.round(from.r + (to.r - from.r) * amount),\n g: Math.round(from.g + (to.g - from.g) * amount),\n b: Math.round(from.b + (to.b - from.b) * amount)\n});\n\nconst rgbToCss = (rgb: Rgb): string => `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;\n\nconst clamp = (value: number, min: number, max: number): number => Math.min(Math.max(value, min), max);\nconst easeOutCubic = (t: number): number => 1 - Math.pow(1 - t, 3);\n\nconst resolveFontSize = (\n value: number | string,\n container: HTMLDivElement,\n fontWeight: number | string,\n fontFamily: string\n): number => {\n if (typeof value === 'number') return value;\n\n const probe = document.createElement('span');\n probe.textContent = 'M';\n probe.style.position = 'absolute';\n probe.style.visibility = 'hidden';\n probe.style.pointerEvents = 'none';\n probe.style.fontSize = value;\n probe.style.fontWeight = String(fontWeight);\n probe.style.fontFamily = fontFamily;\n container.appendChild(probe);\n const size = parseFloat(window.getComputedStyle(probe).fontSize) || 96;\n probe.remove();\n return size;\n};\n\nconst waitForFonts = async (font: string): Promise => {\n if (!('fonts' in document)) return;\n\n try {\n await document.fonts.load(font);\n } catch {}\n\n await document.fonts.ready;\n};\n\nconst ParticleText = ({\n text = 'React Bits',\n particleSize = 2,\n density = 4,\n color = '#ffffff',\n highlightColor = '#8b5cf6',\n scatter = 180,\n gatherDuration = 1600,\n stagger = 420,\n pointerRepel = 40,\n repelRadius = 120,\n idleDrift = 0.7,\n trigger = 'mount',\n fontSize = 'clamp(3rem, 12vw, 8rem)',\n fontWeight = 800,\n fontFamily = 'inherit',\n glow = true,\n className = '',\n style\n}: ParticleTextProps) => {\n const containerRef = useRef(null);\n const canvasRef = useRef(null);\n\n useEffect(() => {\n if (typeof window === 'undefined') return undefined;\n\n const container = containerRef.current;\n const canvas = canvasRef.current;\n if (!container || !canvas) return undefined;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return undefined;\n\n let particles: Particle[] = [];\n let animationFrame: number | null = null;\n let resizeFrame: number | null = null;\n let buildId = 0;\n let gathering = false;\n let gatherStart = 0;\n let reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;\n let width = 0;\n let height = 0;\n let dpr = 1;\n\n const pointer = {\n active: false,\n x: 0,\n y: 0,\n smoothX: 0,\n smoothY: 0\n };\n\n const startGather = (fromScatter = true): void => {\n if (!particles.length) return;\n\n const now = performance.now();\n const spread = reducedMotion ? 0 : scatter;\n\n particles.forEach(particle => {\n if (fromScatter) {\n const angle = particle.seed * Math.PI * 2;\n const distance = spread * (0.35 + particle.depth * 0.75);\n particle.x = particle.targetX + Math.cos(angle) * distance + (particle.depth - 0.5) * spread * 0.55;\n particle.y = particle.targetY + Math.sin(angle) * distance + (particle.seed - 0.5) * spread * 0.55;\n }\n\n particle.startX = particle.x;\n particle.startY = particle.y;\n particle.delay = reducedMotion ? 0 : particle.seed * stagger;\n });\n\n gatherStart = now;\n gathering = true;\n };\n\n const drawParticle = (particle: Particle): void => {\n const size = particle.size;\n ctx.fillStyle = particle.color;\n\n if (size <= 2.1) {\n ctx.fillRect(particle.x - size / 2, particle.y - size / 2, size, size);\n return;\n }\n\n ctx.beginPath();\n ctx.arc(particle.x, particle.y, size / 2, 0, Math.PI * 2);\n ctx.fill();\n };\n\n const render = (now: number): void => {\n ctx.clearRect(0, 0, width, height);\n\n if (glow && !reducedMotion) {\n ctx.shadowBlur = particleSize * 3;\n ctx.shadowColor = highlightColor;\n } else {\n ctx.shadowBlur = 0;\n }\n\n pointer.smoothX += (pointer.x - pointer.smoothX) * 0.18;\n pointer.smoothY += (pointer.y - pointer.smoothY) * 0.18;\n\n let complete = true;\n\n particles.forEach(particle => {\n let baseX = particle.targetX;\n let baseY = particle.targetY;\n let progress = 1;\n\n if (gathering) {\n const local = (now - gatherStart - particle.delay) / Math.max(1, reducedMotion ? 1 : gatherDuration);\n progress = clamp(local, 0, 1);\n const eased = easeOutCubic(progress);\n baseX = particle.startX + (particle.targetX - particle.startX) * eased;\n baseY = particle.startY + (particle.targetY - particle.startY) * eased;\n if (progress < 1) complete = false;\n } else if (!reducedMotion && idleDrift > 0) {\n const driftTime = now * 0.001;\n baseX += Math.sin(driftTime * 0.9 + particle.seed * 10) * idleDrift * particle.depth;\n baseY += Math.cos(driftTime * 0.75 + particle.depth * 10) * idleDrift * particle.depth;\n }\n\n if (pointer.active && !reducedMotion && pointerRepel > 0 && repelRadius > 0) {\n const dx = baseX - pointer.smoothX;\n const dy = baseY - pointer.smoothY;\n const distance = Math.hypot(dx, dy);\n if (distance > 0 && distance < repelRadius) {\n const force = Math.pow(1 - distance / repelRadius, 2) * pointerRepel;\n baseX += (dx / distance) * force;\n baseY += (dy / distance) * force;\n }\n }\n\n const follow = reducedMotion ? 1 : 0.22;\n particle.x += (baseX - particle.x) * follow;\n particle.y += (baseY - particle.y) * follow;\n\n ctx.globalAlpha = clamp(0.35 + progress * 0.65, 0, 1);\n drawParticle(particle);\n });\n\n ctx.globalAlpha = 1;\n ctx.shadowBlur = 0;\n\n if (gathering && complete) {\n gathering = false;\n }\n\n animationFrame = window.requestAnimationFrame(render);\n };\n\n const ensureRenderLoop = (): void => {\n if (animationFrame === null) {\n animationFrame = window.requestAnimationFrame(render);\n }\n };\n\n const sampleText = async (): Promise => {\n const currentBuild = ++buildId;\n const rect = container.getBoundingClientRect();\n width = Math.floor(rect.width);\n height = Math.floor(rect.height);\n\n if (width <= 0 || height <= 0) return;\n\n dpr = Math.min(window.devicePixelRatio || 1, 2);\n canvas.width = Math.max(1, Math.floor(width * dpr));\n canvas.height = Math.max(1, Math.floor(height * dpr));\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n const computed = window.getComputedStyle(container);\n const resolvedFamily = fontFamily === 'inherit' ? computed.fontFamily || 'sans-serif' : fontFamily;\n let resolvedSize = resolveFontSize(fontSize, container, fontWeight, resolvedFamily);\n let font = `${fontWeight} ${resolvedSize}px ${resolvedFamily}`;\n\n await waitForFonts(font);\n if (currentBuild !== buildId) return;\n\n const offscreen = document.createElement('canvas');\n const offCtx = offscreen.getContext('2d', { willReadFrequently: true });\n if (!offCtx) return;\n\n const content = String(text || ' ');\n const maxTextWidth = width * 0.92;\n offCtx.font = font;\n let metrics = offCtx.measureText(content);\n const measuredWidth = Math.max(1, metrics.width);\n if (measuredWidth > maxTextWidth) {\n resolvedSize = Math.max(18, resolvedSize * (maxTextWidth / measuredWidth));\n font = `${fontWeight} ${resolvedSize}px ${resolvedFamily}`;\n await waitForFonts(font);\n if (currentBuild !== buildId) return;\n offCtx.font = font;\n metrics = offCtx.measureText(content);\n }\n\n const left = Math.ceil(metrics.actualBoundingBoxLeft || 0);\n const right = Math.ceil(metrics.actualBoundingBoxRight || metrics.width);\n const ascent = Math.ceil(metrics.actualBoundingBoxAscent || resolvedSize * 0.78);\n const descent = Math.ceil(metrics.actualBoundingBoxDescent || resolvedSize * 0.22);\n const padding = Math.max(12, Math.ceil(resolvedSize * 0.08));\n const textWidth = Math.max(1, left + right);\n const textHeight = Math.max(1, ascent + descent);\n\n offscreen.width = textWidth + padding * 2;\n offscreen.height = textHeight + padding * 2;\n offCtx.clearRect(0, 0, offscreen.width, offscreen.height);\n offCtx.font = font;\n offCtx.textAlign = 'left';\n offCtx.textBaseline = 'alphabetic';\n offCtx.fillStyle = '#ffffff';\n offCtx.fillText(content, padding - left, padding + ascent);\n\n const imageData = offCtx.getImageData(0, 0, offscreen.width, offscreen.height);\n const targets: Target[] = [];\n const step = Math.max(2, Math.floor(density));\n\n for (let y = 0; y < offscreen.height; y += step) {\n for (let x = 0; x < offscreen.width; x += step) {\n const alpha = imageData.data[(y * offscreen.width + x) * 4 + 3];\n if (alpha > 40) {\n targets.push({\n x: width / 2 - offscreen.width / 2 + x,\n y: height / 2 - offscreen.height / 2 + y,\n alpha: alpha / 255\n });\n }\n }\n }\n\n const maxParticles = Math.max(900, Math.min(5200, Math.floor((width * height) / 90)));\n const stride = Math.max(1, Math.ceil(targets.length / maxParticles));\n const baseRgb = hexToRgb(color);\n const highlightRgb = hexToRgb(highlightColor);\n const selected = targets.filter((_, index) => index % stride === 0);\n\n particles = selected.map((target, index) => {\n const seed = ((index * 9301 + 49297) % 233280) / 233280;\n const depth = 0.45 + (((index * 233 + 97) % 1000) / 1000) * 0.9;\n const blend = baseRgb && highlightRgb ? clamp(target.x / Math.max(1, width) + (seed - 0.5) * 0.35, 0, 1) : 0;\n const particleColor = baseRgb && highlightRgb ? rgbToCss(mixRgb(baseRgb, highlightRgb, blend)) : color;\n const angle = seed * Math.PI * 2;\n const distance = (reducedMotion ? 0 : scatter) * (0.35 + depth * 0.75);\n const startX = target.x + Math.cos(angle) * distance + (seed - 0.5) * scatter * 0.45;\n const startY = target.y + Math.sin(angle) * distance + (depth - 0.9) * scatter * 0.45;\n\n return {\n x: reducedMotion ? target.x : startX,\n y: reducedMotion ? target.y : startY,\n startX,\n startY,\n targetX: target.x,\n targetY: target.y,\n size: Math.max(0.6, particleSize * (0.75 + target.alpha * 0.45)),\n color: particleColor,\n seed,\n depth,\n delay: seed * stagger\n };\n });\n\n pointer.x = width / 2;\n pointer.y = height / 2;\n pointer.smoothX = pointer.x;\n pointer.smoothY = pointer.y;\n\n if (reducedMotion) {\n particles.forEach(particle => {\n particle.x = particle.targetX;\n particle.y = particle.targetY;\n particle.startX = particle.targetX;\n particle.startY = particle.targetY;\n particle.delay = 0;\n });\n gathering = false;\n } else {\n startGather(false);\n }\n\n ensureRenderLoop();\n };\n\n const queueSample = (): void => {\n if (resizeFrame) window.cancelAnimationFrame(resizeFrame);\n resizeFrame = window.requestAnimationFrame(sampleText);\n };\n\n const handlePointerMove = (event: PointerEvent): void => {\n const rect = canvas.getBoundingClientRect();\n pointer.x = event.clientX - rect.left;\n pointer.y = event.clientY - rect.top;\n pointer.active = true;\n };\n\n const handlePointerLeave = (): void => {\n pointer.active = false;\n };\n\n const handlePointerEnter = (event: PointerEvent): void => {\n handlePointerMove(event);\n if (trigger === 'hover') startGather(true);\n };\n\n const handleClick = (): void => {\n if (trigger === 'click') startGather(true);\n };\n\n const reduceMotionQuery = window.matchMedia?.('(prefers-reduced-motion: reduce)');\n const handleReduceMotionChange = (event: MediaQueryListEvent): void => {\n reducedMotion = event.matches;\n void sampleText();\n };\n\n reduceMotionQuery?.addEventListener('change', handleReduceMotionChange);\n canvas.addEventListener('pointerenter', handlePointerEnter);\n canvas.addEventListener('pointermove', handlePointerMove);\n canvas.addEventListener('pointerleave', handlePointerLeave);\n canvas.addEventListener('click', handleClick);\n\n const resizeObserver = new ResizeObserver(queueSample);\n resizeObserver.observe(container);\n void sampleText();\n\n return () => {\n buildId += 1;\n resizeObserver.disconnect();\n reduceMotionQuery?.removeEventListener('change', handleReduceMotionChange);\n canvas.removeEventListener('pointerenter', handlePointerEnter);\n canvas.removeEventListener('pointermove', handlePointerMove);\n canvas.removeEventListener('pointerleave', handlePointerLeave);\n canvas.removeEventListener('click', handleClick);\n\n if (animationFrame !== null) window.cancelAnimationFrame(animationFrame);\n if (resizeFrame !== null) window.cancelAnimationFrame(resizeFrame);\n };\n }, [\n text,\n particleSize,\n density,\n color,\n highlightColor,\n scatter,\n gatherDuration,\n stagger,\n pointerRepel,\n repelRadius,\n idleDrift,\n trigger,\n fontSize,\n fontWeight,\n fontFamily,\n glow\n ]);\n\n return (\n
    \n \n {text}\n
    \n );\n};\n\nexport default ParticleText;\n" } ], diff --git a/public/r/Particles-JS-CSS.json b/public/r/Particles-JS-CSS.json index 725aac9d..bb9e8498 100644 --- a/public/r/Particles-JS-CSS.json +++ b/public/r/Particles-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Particles/Particles.css", + "type": "registry:file", + "path": "Particles.css", + "target": "@components/Particles.css", "content": ".particles-container {\n position: relative;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "Particles/Particles.jsx", + "path": "Particles.jsx", "content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Camera, Geometry, Program, Mesh } from 'ogl';\n\nimport './Particles.css';\n\nconst defaultColors = ['#ffffff', '#ffffff', '#ffffff'];\n\nconst hexToRgb = hex => {\n hex = hex.replace(/^#/, '');\n if (hex.length === 3) {\n hex = hex\n .split('')\n .map(c => c + c)\n .join('');\n }\n const int = parseInt(hex.slice(0, 6), 16);\n const r = ((int >> 16) & 255) / 255;\n const g = ((int >> 8) & 255) / 255;\n const b = (int & 255) / 255;\n return [r, g, b];\n};\n\nconst vertex = /* glsl */ `\n attribute vec3 position;\n attribute vec4 random;\n attribute vec3 color;\n \n uniform mat4 modelMatrix;\n uniform mat4 viewMatrix;\n uniform mat4 projectionMatrix;\n uniform float uTime;\n uniform float uSpread;\n uniform float uBaseSize;\n uniform float uSizeRandomness;\n \n varying vec4 vRandom;\n varying vec3 vColor;\n \n void main() {\n vRandom = random;\n vColor = color;\n \n vec3 pos = position * uSpread;\n pos.z *= 10.0;\n \n vec4 mPos = modelMatrix * vec4(pos, 1.0);\n float t = uTime;\n mPos.x += sin(t * random.z + 6.28 * random.w) * mix(0.1, 1.5, random.x);\n mPos.y += sin(t * random.y + 6.28 * random.x) * mix(0.1, 1.5, random.w);\n mPos.z += sin(t * random.w + 6.28 * random.y) * mix(0.1, 1.5, random.z);\n \n vec4 mvPos = viewMatrix * mPos;\n\n if (uSizeRandomness == 0.0) {\n gl_PointSize = uBaseSize;\n } else {\n gl_PointSize = (uBaseSize * (1.0 + uSizeRandomness * (random.x - 0.5))) / length(mvPos.xyz);\n }\n\n gl_Position = projectionMatrix * mvPos;\n }\n`;\n\nconst fragment = /* glsl */ `\n precision highp float;\n \n uniform float uTime;\n uniform float uAlphaParticles;\n varying vec4 vRandom;\n varying vec3 vColor;\n \n void main() {\n vec2 uv = gl_PointCoord.xy;\n float d = length(uv - vec2(0.5));\n \n if(uAlphaParticles < 0.5) {\n if(d > 0.5) {\n discard;\n }\n gl_FragColor = vec4(vColor + 0.2 * sin(uv.yxx + uTime + vRandom.y * 6.28), 1.0);\n } else {\n float circle = smoothstep(0.5, 0.4, d) * 0.8;\n gl_FragColor = vec4(vColor + 0.2 * sin(uv.yxx + uTime + vRandom.y * 6.28), circle);\n }\n }\n`;\n\nconst Particles = ({\n particleCount = 200,\n particleSpread = 10,\n speed = 0.1,\n particleColors,\n moveParticlesOnHover = false,\n particleHoverFactor = 1,\n alphaParticles = false,\n particleBaseSize = 100,\n sizeRandomness = 1,\n cameraDistance = 20,\n disableRotation = false,\n pixelRatio = 1,\n className\n}) => {\n const containerRef = useRef(null);\n const mouseRef = useRef({ x: 0, y: 0 });\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n dpr: pixelRatio,\n depth: false,\n alpha: true\n });\n const gl = renderer.gl;\n container.appendChild(gl.canvas);\n gl.clearColor(0, 0, 0, 0);\n\n const camera = new Camera(gl, { fov: 15 });\n camera.position.set(0, 0, cameraDistance);\n\n const resize = () => {\n const width = container.clientWidth;\n const height = container.clientHeight;\n renderer.setSize(width, height);\n camera.perspective({ aspect: gl.canvas.width / gl.canvas.height });\n };\n window.addEventListener('resize', resize, false);\n resize();\n\n const handleMouseMove = e => {\n const rect = container.getBoundingClientRect();\n const x = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n const y = -(((e.clientY - rect.top) / rect.height) * 2 - 1);\n mouseRef.current = { x, y };\n };\n\n if (moveParticlesOnHover) {\n container.addEventListener('mousemove', handleMouseMove);\n }\n\n const count = particleCount;\n const positions = new Float32Array(count * 3);\n const randoms = new Float32Array(count * 4);\n const colors = new Float32Array(count * 3);\n const palette = particleColors && particleColors.length > 0 ? particleColors : defaultColors;\n\n for (let i = 0; i < count; i++) {\n let x, y, z, len;\n do {\n x = Math.random() * 2 - 1;\n y = Math.random() * 2 - 1;\n z = Math.random() * 2 - 1;\n len = x * x + y * y + z * z;\n } while (len > 1 || len === 0);\n const r = Math.cbrt(Math.random());\n positions.set([x * r, y * r, z * r], i * 3);\n randoms.set([Math.random(), Math.random(), Math.random(), Math.random()], i * 4);\n const col = hexToRgb(palette[Math.floor(Math.random() * palette.length)]);\n colors.set(col, i * 3);\n }\n\n const geometry = new Geometry(gl, {\n position: { size: 3, data: positions },\n random: { size: 4, data: randoms },\n color: { size: 3, data: colors }\n });\n\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n uTime: { value: 0 },\n uSpread: { value: particleSpread },\n uBaseSize: { value: particleBaseSize * pixelRatio },\n uSizeRandomness: { value: sizeRandomness },\n uAlphaParticles: { value: alphaParticles ? 1 : 0 }\n },\n transparent: true,\n depthTest: false\n });\n\n const particles = new Mesh(gl, { mode: gl.POINTS, geometry, program });\n\n let animationFrameId;\n let lastTime = performance.now();\n let elapsed = 0;\n\n const update = t => {\n animationFrameId = requestAnimationFrame(update);\n const delta = t - lastTime;\n lastTime = t;\n elapsed += delta * speed;\n\n program.uniforms.uTime.value = elapsed * 0.001;\n\n if (moveParticlesOnHover) {\n particles.position.x = -mouseRef.current.x * particleHoverFactor;\n particles.position.y = -mouseRef.current.y * particleHoverFactor;\n } else {\n particles.position.x = 0;\n particles.position.y = 0;\n }\n\n if (!disableRotation) {\n particles.rotation.x = Math.sin(elapsed * 0.0002) * 0.1;\n particles.rotation.y = Math.cos(elapsed * 0.0005) * 0.15;\n particles.rotation.z += 0.01 * speed;\n }\n\n renderer.render({ scene: particles, camera });\n };\n\n animationFrameId = requestAnimationFrame(update);\n\n return () => {\n window.removeEventListener('resize', resize);\n if (moveParticlesOnHover) {\n container.removeEventListener('mousemove', handleMouseMove);\n }\n cancelAnimationFrame(animationFrameId);\n if (container.contains(gl.canvas)) {\n container.removeChild(gl.canvas);\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n particleCount,\n particleSpread,\n speed,\n moveParticlesOnHover,\n particleHoverFactor,\n alphaParticles,\n particleBaseSize,\n sizeRandomness,\n cameraDistance,\n disableRotation,\n pixelRatio\n ]);\n\n return
    ;\n};\n\nexport default Particles;\n" } ], diff --git a/public/r/Particles-TS-CSS.json b/public/r/Particles-TS-CSS.json index 6e639a89..ad2fa7b1 100644 --- a/public/r/Particles-TS-CSS.json +++ b/public/r/Particles-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Particles/Particles.css", + "type": "registry:file", + "path": "Particles.css", + "target": "@components/Particles.css", "content": ".particles-container {\n position: relative;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "Particles/Particles.tsx", + "path": "Particles.tsx", "content": "import React, { useEffect, useRef } from 'react';\nimport { Renderer, Camera, Geometry, Program, Mesh } from 'ogl';\n\nimport './Particles.css';\n\ninterface ParticlesProps {\n particleCount?: number;\n particleSpread?: number;\n speed?: number;\n particleColors?: string[];\n moveParticlesOnHover?: boolean;\n particleHoverFactor?: number;\n alphaParticles?: boolean;\n particleBaseSize?: number;\n sizeRandomness?: number;\n cameraDistance?: number;\n disableRotation?: boolean;\n pixelRatio?: number;\n className?: string;\n}\n\nconst defaultColors: string[] = ['#ffffff', '#ffffff', '#ffffff'];\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n hex = hex.replace(/^#/, '');\n if (hex.length === 3) {\n hex = hex\n .split('')\n .map(c => c + c)\n .join('');\n }\n const int = parseInt(hex.slice(0, 6), 16);\n const r = ((int >> 16) & 255) / 255;\n const g = ((int >> 8) & 255) / 255;\n const b = (int & 255) / 255;\n return [r, g, b];\n};\n\nconst vertex = /* glsl */ `\n attribute vec3 position;\n attribute vec4 random;\n attribute vec3 color;\n \n uniform mat4 modelMatrix;\n uniform mat4 viewMatrix;\n uniform mat4 projectionMatrix;\n uniform float uTime;\n uniform float uSpread;\n uniform float uBaseSize;\n uniform float uSizeRandomness;\n \n varying vec4 vRandom;\n varying vec3 vColor;\n \n void main() {\n vRandom = random;\n vColor = color;\n \n vec3 pos = position * uSpread;\n pos.z *= 10.0;\n \n vec4 mPos = modelMatrix * vec4(pos, 1.0);\n float t = uTime;\n mPos.x += sin(t * random.z + 6.28 * random.w) * mix(0.1, 1.5, random.x);\n mPos.y += sin(t * random.y + 6.28 * random.x) * mix(0.1, 1.5, random.w);\n mPos.z += sin(t * random.w + 6.28 * random.y) * mix(0.1, 1.5, random.z);\n \n vec4 mvPos = viewMatrix * mPos;\n if (uSizeRandomness == 0.0) {\n gl_PointSize = uBaseSize;\n } else {\n gl_PointSize = (uBaseSize * (1.0 + uSizeRandomness * (random.x - 0.5))) / length(mvPos.xyz);\n }\n \n gl_Position = projectionMatrix * mvPos;\n }\n`;\n\nconst fragment = /* glsl */ `\n precision highp float;\n \n uniform float uTime;\n uniform float uAlphaParticles;\n varying vec4 vRandom;\n varying vec3 vColor;\n \n void main() {\n vec2 uv = gl_PointCoord.xy;\n float d = length(uv - vec2(0.5));\n \n if(uAlphaParticles < 0.5) {\n if(d > 0.5) {\n discard;\n }\n gl_FragColor = vec4(vColor + 0.2 * sin(uv.yxx + uTime + vRandom.y * 6.28), 1.0);\n } else {\n float circle = smoothstep(0.5, 0.4, d) * 0.8;\n gl_FragColor = vec4(vColor + 0.2 * sin(uv.yxx + uTime + vRandom.y * 6.28), circle);\n }\n }\n`;\n\nconst Particles: React.FC = ({\n particleCount = 200,\n particleSpread = 10,\n speed = 0.1,\n particleColors,\n moveParticlesOnHover = false,\n particleHoverFactor = 1,\n alphaParticles = false,\n particleBaseSize = 100,\n sizeRandomness = 1,\n cameraDistance = 20,\n disableRotation = false,\n pixelRatio = 1,\n className\n}) => {\n const containerRef = useRef(null);\n const mouseRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n dpr: pixelRatio,\n depth: false,\n alpha: true\n });\n const gl = renderer.gl;\n container.appendChild(gl.canvas);\n gl.clearColor(0, 0, 0, 0);\n\n const camera = new Camera(gl, { fov: 15 });\n camera.position.set(0, 0, cameraDistance);\n\n const resize = () => {\n const width = container.clientWidth;\n const height = container.clientHeight;\n renderer.setSize(width, height);\n camera.perspective({ aspect: gl.canvas.width / gl.canvas.height });\n };\n window.addEventListener('resize', resize, false);\n resize();\n\n const handleMouseMove = (e: MouseEvent) => {\n const rect = container.getBoundingClientRect();\n const x = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n const y = -(((e.clientY - rect.top) / rect.height) * 2 - 1);\n mouseRef.current = { x, y };\n };\n\n if (moveParticlesOnHover) {\n container.addEventListener('mousemove', handleMouseMove);\n }\n\n const count = particleCount;\n const positions = new Float32Array(count * 3);\n const randoms = new Float32Array(count * 4);\n const colors = new Float32Array(count * 3);\n const palette = particleColors && particleColors.length > 0 ? particleColors : defaultColors;\n\n for (let i = 0; i < count; i++) {\n let x: number, y: number, z: number, len: number;\n do {\n x = Math.random() * 2 - 1;\n y = Math.random() * 2 - 1;\n z = Math.random() * 2 - 1;\n len = x * x + y * y + z * z;\n } while (len > 1 || len === 0);\n const r = Math.cbrt(Math.random());\n positions.set([x * r, y * r, z * r], i * 3);\n randoms.set([Math.random(), Math.random(), Math.random(), Math.random()], i * 4);\n const col = hexToRgb(palette[Math.floor(Math.random() * palette.length)]);\n colors.set(col, i * 3);\n }\n\n const geometry = new Geometry(gl, {\n position: { size: 3, data: positions },\n random: { size: 4, data: randoms },\n color: { size: 3, data: colors }\n });\n\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n uTime: { value: 0 },\n uSpread: { value: particleSpread },\n uBaseSize: { value: particleBaseSize * pixelRatio },\n uSizeRandomness: { value: sizeRandomness },\n uAlphaParticles: { value: alphaParticles ? 1 : 0 }\n },\n transparent: true,\n depthTest: false\n });\n\n const particles = new Mesh(gl, { mode: gl.POINTS, geometry, program });\n\n let animationFrameId: number;\n let lastTime = performance.now();\n let elapsed = 0;\n\n const update = (t: number) => {\n animationFrameId = requestAnimationFrame(update);\n const delta = t - lastTime;\n lastTime = t;\n elapsed += delta * speed;\n\n program.uniforms.uTime.value = elapsed * 0.001;\n\n if (moveParticlesOnHover) {\n particles.position.x = -mouseRef.current.x * particleHoverFactor;\n particles.position.y = -mouseRef.current.y * particleHoverFactor;\n } else {\n particles.position.x = 0;\n particles.position.y = 0;\n }\n\n if (!disableRotation) {\n particles.rotation.x = Math.sin(elapsed * 0.0002) * 0.1;\n particles.rotation.y = Math.cos(elapsed * 0.0005) * 0.15;\n particles.rotation.z += 0.01 * speed;\n }\n\n renderer.render({ scene: particles, camera });\n };\n\n animationFrameId = requestAnimationFrame(update);\n\n return () => {\n window.removeEventListener('resize', resize);\n if (moveParticlesOnHover) {\n container.removeEventListener('mousemove', handleMouseMove);\n }\n cancelAnimationFrame(animationFrameId);\n if (container.contains(gl.canvas)) {\n container.removeChild(gl.canvas);\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n particleCount,\n particleSpread,\n speed,\n moveParticlesOnHover,\n particleHoverFactor,\n alphaParticles,\n particleBaseSize,\n sizeRandomness,\n cameraDistance,\n disableRotation,\n pixelRatio\n ]);\n\n return
    ;\n};\n\nexport default Particles;\n" } ], diff --git a/public/r/PillNav-JS-CSS.json b/public/r/PillNav-JS-CSS.json index 4e4c7597..20766bea 100644 --- a/public/r/PillNav-JS-CSS.json +++ b/public/r/PillNav-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "PillNav/PillNav.css", + "type": "registry:file", + "path": "PillNav.css", + "target": "@components/PillNav.css", "content": ".pill-nav-container {\n position: absolute;\n top: 1em;\n z-index: 99;\n}\n\n@media (max-width: 768px) {\n .pill-nav-container {\n width: 100%;\n left: 0;\n }\n}\n\n.pill-nav {\n --nav-h: 42px;\n --logo: 36px;\n --pill-pad-x: 18px;\n --pill-gap: 3px;\n width: max-content;\n display: flex;\n align-items: center;\n box-sizing: border-box;\n}\n\n@media (max-width: 768px) {\n .pill-nav {\n width: 100%;\n justify-content: space-between;\n padding: 0 1rem;\n background: transparent;\n }\n}\n\n.pill-nav-items {\n position: relative;\n display: flex;\n align-items: center;\n height: var(--nav-h);\n background: var(--base, #000);\n border-radius: 9999px;\n}\n\n.pill-logo {\n width: var(--nav-h);\n height: var(--nav-h);\n border-radius: 50%;\n background: var(--base, #000);\n padding: 8px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n overflow: hidden;\n}\n\n.pill-logo img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n\n.pill-list {\n list-style: none;\n display: flex;\n align-items: stretch;\n gap: var(--pill-gap);\n margin: 0;\n padding: 3px;\n height: 100%;\n}\n\n.pill-list > li {\n display: flex;\n height: 100%;\n}\n\n.pill {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 0 var(--pill-pad-x);\n background: var(--pill-bg, #fff);\n color: var(--pill-text, var(--base, #000));\n text-decoration: none;\n border-radius: 9999px;\n box-sizing: border-box;\n font-weight: 600;\n font-size: 16px;\n line-height: 0;\n text-transform: uppercase;\n letter-spacing: 0.2px;\n white-space: nowrap;\n cursor: pointer;\n position: relative;\n overflow: hidden;\n}\n\n.pill .hover-circle {\n position: absolute;\n left: 50%;\n bottom: 0;\n border-radius: 50%;\n background: var(--base, #000);\n z-index: 1;\n display: block;\n pointer-events: none;\n will-change: transform;\n}\n\n.pill .label-stack {\n position: relative;\n display: inline-block;\n line-height: 1;\n z-index: 2;\n}\n\n.pill .pill-label {\n position: relative;\n z-index: 2;\n display: inline-block;\n line-height: 1;\n will-change: transform;\n}\n\n.pill .pill-label-hover {\n position: absolute;\n left: 0;\n top: 0;\n color: var(--hover-text, #fff);\n z-index: 3;\n display: inline-block;\n will-change: transform, opacity;\n}\n\n.pill.is-active::after {\n content: '';\n position: absolute;\n bottom: -6px;\n left: 50%;\n transform: translateX(-50%);\n width: 12px;\n height: 12px;\n background: var(--base, #000);\n border-radius: 50px;\n z-index: 4;\n}\n\n.desktop-only {\n display: block;\n}\n\n.mobile-only {\n display: none;\n}\n\n@media (max-width: 768px) {\n .desktop-only {\n display: none;\n }\n\n .mobile-only {\n display: block;\n }\n}\n\n.mobile-menu-button {\n width: var(--nav-h);\n height: var(--nav-h);\n border-radius: 50%;\n background: var(--base, #000);\n border: none;\n display: none;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 4px;\n cursor: pointer;\n padding: 0;\n position: relative;\n}\n\n@media (max-width: 768px) {\n .mobile-menu-button {\n display: flex;\n }\n}\n\n.hamburger-line {\n width: 16px;\n height: 2px;\n background: var(--pill-bg, #fff);\n border-radius: 1px;\n transition: all 0.01s ease;\n transform-origin: center;\n}\n\n.mobile-menu-popover {\n position: absolute;\n top: 3em;\n left: 1rem;\n right: 1rem;\n background: var(--base, #f0f0f0);\n border-radius: 27px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);\n z-index: 998;\n opacity: 0;\n transform-origin: top center;\n visibility: hidden;\n}\n\n.mobile-menu-list {\n list-style: none;\n margin: 0;\n padding: 3px;\n display: flex;\n flex-direction: column;\n gap: 3px;\n}\n\n.mobile-menu-popover .mobile-menu-link {\n display: block;\n padding: 12px 16px;\n color: var(--pill-text, #fff);\n background-color: var(--pill-bg, #fff);\n text-decoration: none;\n font-size: 16px;\n font-weight: 500;\n border-radius: 50px;\n transition: all 0.2s ease;\n}\n\n.mobile-menu-popover .mobile-menu-link:hover {\n cursor: pointer;\n background-color: var(--base);\n color: var(--hover-text, #fff);\n}\n" }, { "type": "registry:component", - "path": "PillNav/PillNav.jsx", + "path": "PillNav.jsx", "content": "import { useEffect, useRef, useState } from 'react';\nimport { Link } from 'react-router-dom';\nimport { gsap } from 'gsap';\nimport './PillNav.css';\n\nconst PillNav = ({\n logo,\n logoAlt = 'Logo',\n items,\n activeHref,\n className = '',\n ease = 'power3.easeOut',\n baseColor = '#fff',\n pillColor = '#120F17',\n hoveredPillTextColor = '#120F17',\n pillTextColor,\n onMobileMenuClick,\n initialLoadAnimation = true\n}) => {\n const resolvedPillTextColor = pillTextColor ?? baseColor;\n const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);\n const circleRefs = useRef([]);\n const tlRefs = useRef([]);\n const activeTweenRefs = useRef([]);\n const logoImgRef = useRef(null);\n const logoTweenRef = useRef(null);\n const hamburgerRef = useRef(null);\n const mobileMenuRef = useRef(null);\n const navItemsRef = useRef(null);\n const logoRef = useRef(null);\n\n useEffect(() => {\n const layout = () => {\n circleRefs.current.forEach(circle => {\n if (!circle?.parentElement) return;\n\n const pill = circle.parentElement;\n const rect = pill.getBoundingClientRect();\n const { width: w, height: h } = rect;\n const R = ((w * w) / 4 + h * h) / (2 * h);\n const D = Math.ceil(2 * R) + 2;\n const delta = Math.ceil(R - Math.sqrt(Math.max(0, R * R - (w * w) / 4))) + 1;\n const originY = D - delta;\n\n circle.style.width = `${D}px`;\n circle.style.height = `${D}px`;\n circle.style.bottom = `-${delta}px`;\n\n gsap.set(circle, {\n xPercent: -50,\n scale: 0,\n transformOrigin: `50% ${originY}px`\n });\n\n const label = pill.querySelector('.pill-label');\n const white = pill.querySelector('.pill-label-hover');\n\n if (label) gsap.set(label, { y: 0 });\n if (white) gsap.set(white, { y: h + 12, opacity: 0 });\n\n const index = circleRefs.current.indexOf(circle);\n if (index === -1) return;\n\n tlRefs.current[index]?.kill();\n const tl = gsap.timeline({ paused: true });\n\n tl.to(circle, { scale: 1.2, xPercent: -50, duration: 2, ease, overwrite: 'auto' }, 0);\n\n if (label) {\n tl.to(label, { y: -(h + 8), duration: 2, ease, overwrite: 'auto' }, 0);\n }\n\n if (white) {\n gsap.set(white, { y: Math.ceil(h + 100), opacity: 0 });\n tl.to(white, { y: 0, opacity: 1, duration: 2, ease, overwrite: 'auto' }, 0);\n }\n\n tlRefs.current[index] = tl;\n });\n };\n\n layout();\n\n const onResize = () => layout();\n window.addEventListener('resize', onResize);\n\n if (document.fonts?.ready) {\n document.fonts.ready.then(layout).catch(() => {});\n }\n\n const menu = mobileMenuRef.current;\n if (menu) {\n gsap.set(menu, { visibility: 'hidden', opacity: 0, scaleY: 1 });\n }\n\n if (initialLoadAnimation) {\n const logo = logoRef.current;\n const navItems = navItemsRef.current;\n\n if (logo) {\n gsap.set(logo, { scale: 0 });\n gsap.to(logo, {\n scale: 1,\n duration: 0.6,\n ease\n });\n }\n\n if (navItems) {\n gsap.set(navItems, { width: 0, overflow: 'hidden' });\n gsap.to(navItems, {\n width: 'auto',\n duration: 0.6,\n ease\n });\n }\n }\n\n return () => window.removeEventListener('resize', onResize);\n }, [items, ease, initialLoadAnimation]);\n\n const handleEnter = i => {\n const tl = tlRefs.current[i];\n if (!tl) return;\n activeTweenRefs.current[i]?.kill();\n activeTweenRefs.current[i] = tl.tweenTo(tl.duration(), {\n duration: 0.3,\n ease,\n overwrite: 'auto'\n });\n };\n\n const handleLeave = i => {\n const tl = tlRefs.current[i];\n if (!tl) return;\n activeTweenRefs.current[i]?.kill();\n activeTweenRefs.current[i] = tl.tweenTo(0, {\n duration: 0.2,\n ease,\n overwrite: 'auto'\n });\n };\n\n const handleLogoEnter = () => {\n const img = logoImgRef.current;\n if (!img) return;\n logoTweenRef.current?.kill();\n gsap.set(img, { rotate: 0 });\n logoTweenRef.current = gsap.to(img, {\n rotate: 360,\n duration: 0.2,\n ease,\n overwrite: 'auto'\n });\n };\n\n const toggleMobileMenu = () => {\n const newState = !isMobileMenuOpen;\n setIsMobileMenuOpen(newState);\n\n const hamburger = hamburgerRef.current;\n const menu = mobileMenuRef.current;\n\n if (hamburger) {\n const lines = hamburger.querySelectorAll('.hamburger-line');\n if (newState) {\n gsap.to(lines[0], { rotation: 45, y: 3, duration: 0.3, ease });\n gsap.to(lines[1], { rotation: -45, y: -3, duration: 0.3, ease });\n } else {\n gsap.to(lines[0], { rotation: 0, y: 0, duration: 0.3, ease });\n gsap.to(lines[1], { rotation: 0, y: 0, duration: 0.3, ease });\n }\n }\n\n if (menu) {\n if (newState) {\n gsap.set(menu, { visibility: 'visible' });\n gsap.fromTo(\n menu,\n { opacity: 0, y: 10, scaleY: 1 },\n {\n opacity: 1,\n y: 0,\n scaleY: 1,\n duration: 0.3,\n ease,\n transformOrigin: 'top center'\n }\n );\n } else {\n gsap.to(menu, {\n opacity: 0,\n y: 10,\n scaleY: 1,\n duration: 0.2,\n ease,\n transformOrigin: 'top center',\n onComplete: () => {\n gsap.set(menu, { visibility: 'hidden' });\n }\n });\n }\n }\n\n onMobileMenuClick?.();\n };\n\n const isExternalLink = href =>\n href.startsWith('http://') ||\n href.startsWith('https://') ||\n href.startsWith('//') ||\n href.startsWith('mailto:') ||\n href.startsWith('tel:') ||\n href.startsWith('#');\n\n const isRouterLink = href => href && !isExternalLink(href);\n\n const cssVars = {\n ['--base']: baseColor,\n ['--pill-bg']: pillColor,\n ['--hover-text']: hoveredPillTextColor,\n ['--pill-text']: resolvedPillTextColor\n };\n\n return (\n
    \n \n\n
    \n
      \n {items.map((item, i) => (\n
    • \n {isRouterLink(item.href) ? (\n setIsMobileMenuOpen(false)}\n >\n {item.label}\n \n ) : (\n setIsMobileMenuOpen(false)}\n >\n {item.label}\n \n )}\n
    • \n ))}\n
    \n
    \n
    \n );\n};\n\nexport default PillNav;\n" } ], diff --git a/public/r/PillNav-TS-CSS.json b/public/r/PillNav-TS-CSS.json index 34ea6123..cba0db61 100644 --- a/public/r/PillNav-TS-CSS.json +++ b/public/r/PillNav-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "PillNav/PillNav.css", + "type": "registry:file", + "path": "PillNav.css", + "target": "@components/PillNav.css", "content": ".pill-nav-container {\n position: absolute;\n top: 1em;\n z-index: 99;\n}\n\n@media (max-width: 768px) {\n .pill-nav-container {\n width: 100%;\n left: 0;\n }\n}\n\n.pill-nav {\n --nav-h: 42px;\n --logo: 36px;\n --pill-pad-x: 18px;\n --pill-gap: 3px;\n width: max-content;\n display: flex;\n align-items: center;\n box-sizing: border-box;\n}\n\n@media (max-width: 768px) {\n .pill-nav {\n width: 100%;\n justify-content: space-between;\n padding: 0 1rem;\n background: transparent;\n }\n}\n\n.pill-nav-items {\n position: relative;\n display: flex;\n align-items: center;\n height: var(--nav-h);\n background: var(--base, #000);\n border-radius: 9999px;\n}\n\n.pill-logo {\n width: var(--nav-h);\n height: var(--nav-h);\n border-radius: 50%;\n background: var(--base, #000);\n padding: 8px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n overflow: hidden;\n}\n\n.pill-logo img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n\n.pill-list {\n list-style: none;\n display: flex;\n align-items: stretch;\n gap: var(--pill-gap);\n margin: 0;\n padding: 3px;\n height: 100%;\n}\n\n.pill-list > li {\n display: flex;\n height: 100%;\n}\n\n.pill {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 0 var(--pill-pad-x);\n background: var(--pill-bg, #fff);\n color: var(--pill-text, var(--base, #000));\n text-decoration: none;\n border-radius: 9999px;\n box-sizing: border-box;\n font-weight: 600;\n font-size: 16px;\n line-height: 0;\n text-transform: uppercase;\n letter-spacing: 0.2px;\n white-space: nowrap;\n cursor: pointer;\n position: relative;\n overflow: hidden;\n}\n\n.pill .hover-circle {\n position: absolute;\n left: 50%;\n bottom: 0;\n border-radius: 50%;\n background: var(--base, #000);\n z-index: 1;\n display: block;\n pointer-events: none;\n will-change: transform;\n}\n\n.pill .label-stack {\n position: relative;\n display: inline-block;\n line-height: 1;\n z-index: 2;\n}\n\n.pill .pill-label {\n position: relative;\n z-index: 2;\n display: inline-block;\n line-height: 1;\n will-change: transform;\n}\n\n.pill .pill-label-hover {\n position: absolute;\n left: 0;\n top: 0;\n color: var(--hover-text, #fff);\n z-index: 3;\n display: inline-block;\n will-change: transform, opacity;\n}\n\n.pill.is-active::after {\n content: '';\n position: absolute;\n bottom: -6px;\n left: 50%;\n transform: translateX(-50%);\n width: 12px;\n height: 12px;\n background: var(--base, #000);\n border-radius: 50px;\n z-index: 4;\n}\n\n.desktop-only {\n display: block;\n}\n\n.mobile-only {\n display: none;\n}\n\n@media (max-width: 768px) {\n .desktop-only {\n display: none;\n }\n\n .mobile-only {\n display: block;\n }\n}\n\n.mobile-menu-button {\n width: var(--nav-h);\n height: var(--nav-h);\n border-radius: 50%;\n background: var(--base, #000);\n border: none;\n display: none;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 4px;\n cursor: pointer;\n padding: 0;\n position: relative;\n}\n\n@media (max-width: 768px) {\n .mobile-menu-button {\n display: flex;\n }\n}\n\n.hamburger-line {\n width: 16px;\n height: 2px;\n background: var(--pill-bg, #fff);\n border-radius: 1px;\n transition: all 0.01s ease;\n transform-origin: center;\n}\n\n.mobile-menu-popover {\n position: absolute;\n top: 3em;\n left: 1rem;\n right: 1rem;\n background: var(--base, #f0f0f0);\n border-radius: 27px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);\n z-index: 998;\n opacity: 0;\n transform-origin: top center;\n visibility: hidden;\n}\n\n.mobile-menu-list {\n list-style: none;\n margin: 0;\n padding: 3px;\n display: flex;\n flex-direction: column;\n gap: 3px;\n}\n\n.mobile-menu-popover .mobile-menu-link {\n display: block;\n padding: 12px 16px;\n color: var(--pill-text, #fff);\n background-color: var(--pill-bg, #fff);\n text-decoration: none;\n font-size: 16px;\n font-weight: 500;\n border-radius: 50px;\n transition: all 0.2s ease;\n}\n\n.mobile-menu-popover .mobile-menu-link:hover {\n cursor: pointer;\n background-color: var(--base);\n color: var(--hover-text, #fff);\n}\n" }, { "type": "registry:component", - "path": "PillNav/PillNav.tsx", + "path": "PillNav.tsx", "content": "import React, { useEffect, useRef, useState } from 'react';\nimport { Link } from 'react-router-dom';\nimport { gsap } from 'gsap';\nimport './PillNav.css';\n\nexport type PillNavItem = {\n label: string;\n href: string;\n ariaLabel?: string;\n};\n\nexport interface PillNavProps {\n logo: string;\n logoAlt?: string;\n items: PillNavItem[];\n activeHref?: string;\n className?: string;\n ease?: string;\n baseColor?: string;\n pillColor?: string;\n hoveredPillTextColor?: string;\n pillTextColor?: string;\n onMobileMenuClick?: () => void;\n initialLoadAnimation?: boolean;\n}\n\nconst PillNav: React.FC = ({\n logo,\n logoAlt = 'Logo',\n items,\n activeHref,\n className = '',\n ease = 'power3.easeOut',\n baseColor = '#fff',\n pillColor = '#120F17',\n hoveredPillTextColor = '#120F17',\n pillTextColor,\n onMobileMenuClick,\n initialLoadAnimation = true\n}) => {\n const resolvedPillTextColor = pillTextColor ?? baseColor;\n const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);\n const circleRefs = useRef>([]);\n const tlRefs = useRef>([]);\n const activeTweenRefs = useRef>([]);\n const logoImgRef = useRef(null);\n const logoTweenRef = useRef(null);\n const hamburgerRef = useRef(null);\n const mobileMenuRef = useRef(null);\n const navItemsRef = useRef(null);\n const logoRef = useRef(null);\n\n useEffect(() => {\n const layout = () => {\n circleRefs.current.forEach(circle => {\n if (!circle?.parentElement) return;\n\n const pill = circle.parentElement as HTMLElement;\n const rect = pill.getBoundingClientRect();\n const { width: w, height: h } = rect;\n const R = ((w * w) / 4 + h * h) / (2 * h);\n const D = Math.ceil(2 * R) + 2;\n const delta = Math.ceil(R - Math.sqrt(Math.max(0, R * R - (w * w) / 4))) + 1;\n const originY = D - delta;\n\n circle.style.width = `${D}px`;\n circle.style.height = `${D}px`;\n circle.style.bottom = `-${delta}px`;\n\n gsap.set(circle, {\n xPercent: -50,\n scale: 0,\n transformOrigin: `50% ${originY}px`\n });\n\n const label = pill.querySelector('.pill-label');\n const white = pill.querySelector('.pill-label-hover');\n\n if (label) gsap.set(label, { y: 0 });\n if (white) gsap.set(white, { y: h + 12, opacity: 0 });\n\n const index = circleRefs.current.indexOf(circle);\n if (index === -1) return;\n\n tlRefs.current[index]?.kill();\n const tl = gsap.timeline({ paused: true });\n\n tl.to(circle, { scale: 1.2, xPercent: -50, duration: 2, ease, overwrite: 'auto' }, 0);\n\n if (label) {\n tl.to(label, { y: -(h + 8), duration: 2, ease, overwrite: 'auto' }, 0);\n }\n\n if (white) {\n gsap.set(white, { y: Math.ceil(h + 100), opacity: 0 });\n tl.to(white, { y: 0, opacity: 1, duration: 2, ease, overwrite: 'auto' }, 0);\n }\n\n tlRefs.current[index] = tl;\n });\n };\n\n layout();\n\n const onResize = () => layout();\n window.addEventListener('resize', onResize);\n\n if (document.fonts?.ready) {\n document.fonts.ready.then(layout).catch(() => {});\n }\n\n const menu = mobileMenuRef.current;\n if (menu) {\n gsap.set(menu, { visibility: 'hidden', opacity: 0, scaleY: 1 });\n }\n\n if (initialLoadAnimation) {\n const logo = logoRef.current;\n const navItems = navItemsRef.current;\n\n if (logo) {\n gsap.set(logo, { scale: 0 });\n gsap.to(logo, {\n scale: 1,\n duration: 0.6,\n ease\n });\n }\n\n if (navItems) {\n gsap.set(navItems, { width: 0, overflow: 'hidden' });\n gsap.to(navItems, {\n width: 'auto',\n duration: 0.6,\n ease\n });\n }\n }\n\n return () => window.removeEventListener('resize', onResize);\n }, [items, ease, initialLoadAnimation]);\n\n const handleEnter = (i: number) => {\n const tl = tlRefs.current[i];\n if (!tl) return;\n activeTweenRefs.current[i]?.kill();\n activeTweenRefs.current[i] = tl.tweenTo(tl.duration(), {\n duration: 0.3,\n ease,\n overwrite: 'auto'\n });\n };\n\n const handleLeave = (i: number) => {\n const tl = tlRefs.current[i];\n if (!tl) return;\n activeTweenRefs.current[i]?.kill();\n activeTweenRefs.current[i] = tl.tweenTo(0, {\n duration: 0.2,\n ease,\n overwrite: 'auto'\n });\n };\n\n const handleLogoEnter = () => {\n const img = logoImgRef.current;\n if (!img) return;\n logoTweenRef.current?.kill();\n gsap.set(img, { rotate: 0 });\n logoTweenRef.current = gsap.to(img, {\n rotate: 360,\n duration: 0.2,\n ease,\n overwrite: 'auto'\n });\n };\n\n const toggleMobileMenu = () => {\n const newState = !isMobileMenuOpen;\n setIsMobileMenuOpen(newState);\n\n const hamburger = hamburgerRef.current;\n const menu = mobileMenuRef.current;\n\n if (hamburger) {\n const lines = hamburger.querySelectorAll('.hamburger-line');\n if (newState) {\n gsap.to(lines[0], { rotation: 45, y: 3, duration: 0.3, ease });\n gsap.to(lines[1], { rotation: -45, y: -3, duration: 0.3, ease });\n } else {\n gsap.to(lines[0], { rotation: 0, y: 0, duration: 0.3, ease });\n gsap.to(lines[1], { rotation: 0, y: 0, duration: 0.3, ease });\n }\n }\n\n if (menu) {\n if (newState) {\n gsap.set(menu, { visibility: 'visible' });\n gsap.fromTo(\n menu,\n { opacity: 0, y: 10, scaleY: 1 },\n {\n opacity: 1,\n y: 0,\n scaleY: 1,\n duration: 0.3,\n ease,\n transformOrigin: 'top center'\n }\n );\n } else {\n gsap.to(menu, {\n opacity: 0,\n y: 10,\n scaleY: 1,\n duration: 0.2,\n ease,\n transformOrigin: 'top center',\n onComplete: () => {\n gsap.set(menu, { visibility: 'hidden' });\n }\n });\n }\n }\n\n onMobileMenuClick?.();\n };\n\n const isExternalLink = (href: string) =>\n href.startsWith('http://') ||\n href.startsWith('https://') ||\n href.startsWith('//') ||\n href.startsWith('mailto:') ||\n href.startsWith('tel:') ||\n href.startsWith('#');\n\n const isRouterLink = (href?: string) => href && !isExternalLink(href);\n\n const cssVars = {\n ['--base']: baseColor,\n ['--pill-bg']: pillColor,\n ['--hover-text']: hoveredPillTextColor,\n ['--pill-text']: resolvedPillTextColor\n } as React.CSSProperties;\n\n return (\n
    \n \n\n
    \n
      \n {items.map(item => (\n
    • \n {isRouterLink(item.href) ? (\n setIsMobileMenuOpen(false)}\n >\n {item.label}\n \n ) : (\n setIsMobileMenuOpen(false)}\n >\n {item.label}\n \n )}\n
    • \n ))}\n
    \n
    \n
    \n );\n};\n\nexport default PillNav;\n" } ], diff --git a/public/r/PixelBlast-JS-CSS.json b/public/r/PixelBlast-JS-CSS.json index dd71b151..9b5953cc 100644 --- a/public/r/PixelBlast-JS-CSS.json +++ b/public/r/PixelBlast-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "PixelBlast/PixelBlast.css", + "type": "registry:file", + "path": "PixelBlast.css", + "target": "@components/PixelBlast.css", "content": ".pixel-blast-container {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "PixelBlast/PixelBlast.jsx", + "path": "PixelBlast.jsx", "content": "import { Effect, EffectComposer, EffectPass, RenderPass } from 'postprocessing';\nimport { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\nimport './PixelBlast.css';\n\nconst createTouchTexture = () => {\n const size = 64;\n const canvas = document.createElement('canvas');\n canvas.width = size;\n canvas.height = size;\n const ctx = canvas.getContext('2d');\n if (!ctx) throw new Error('2D context not available');\n ctx.fillStyle = 'black';\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n const texture = new THREE.Texture(canvas);\n texture.minFilter = THREE.LinearFilter;\n texture.magFilter = THREE.LinearFilter;\n texture.generateMipmaps = false;\n const trail = [];\n let last = null;\n const maxAge = 64;\n let radius = 0.1 * size;\n const speed = 1 / maxAge;\n const clear = () => {\n ctx.fillStyle = 'black';\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n };\n const drawPoint = p => {\n const pos = { x: p.x * size, y: (1 - p.y) * size };\n let intensity = 1;\n const easeOutSine = t => Math.sin((t * Math.PI) / 2);\n const easeOutQuad = t => -t * (t - 2);\n if (p.age < maxAge * 0.3) intensity = easeOutSine(p.age / (maxAge * 0.3));\n else intensity = easeOutQuad(1 - (p.age - maxAge * 0.3) / (maxAge * 0.7)) || 0;\n intensity *= p.force;\n const color = `${((p.vx + 1) / 2) * 255}, ${((p.vy + 1) / 2) * 255}, ${intensity * 255}`;\n const offset = size * 5;\n ctx.shadowOffsetX = offset;\n ctx.shadowOffsetY = offset;\n ctx.shadowBlur = radius;\n ctx.shadowColor = `rgba(${color},${0.22 * intensity})`;\n ctx.beginPath();\n ctx.fillStyle = 'rgba(255,0,0,1)';\n ctx.arc(pos.x - offset, pos.y - offset, radius, 0, Math.PI * 2);\n ctx.fill();\n };\n const addTouch = norm => {\n let force = 0;\n let vx = 0;\n let vy = 0;\n if (last) {\n const dx = norm.x - last.x;\n const dy = norm.y - last.y;\n if (dx === 0 && dy === 0) return;\n const dd = dx * dx + dy * dy;\n const d = Math.sqrt(dd);\n vx = dx / (d || 1);\n vy = dy / (d || 1);\n force = Math.min(dd * 10000, 1);\n }\n last = { x: norm.x, y: norm.y };\n trail.push({ x: norm.x, y: norm.y, age: 0, force, vx, vy });\n };\n const update = () => {\n clear();\n for (let i = trail.length - 1; i >= 0; i--) {\n const point = trail[i];\n const f = point.force * speed * (1 - point.age / maxAge);\n point.x += point.vx * f;\n point.y += point.vy * f;\n point.age++;\n if (point.age > maxAge) trail.splice(i, 1);\n }\n for (let i = 0; i < trail.length; i++) drawPoint(trail[i]);\n texture.needsUpdate = true;\n };\n return {\n canvas,\n texture,\n addTouch,\n update,\n set radiusScale(v) {\n radius = 0.1 * size * v;\n },\n get radiusScale() {\n return radius / (0.1 * size);\n },\n size\n };\n};\n\nconst createLiquidEffect = (texture, opts) => {\n const fragment = `\n uniform sampler2D uTexture;\n uniform float uStrength;\n uniform float uTime;\n uniform float uFreq;\n\n void mainUv(inout vec2 uv) {\n vec4 tex = texture2D(uTexture, uv);\n float vx = tex.r * 2.0 - 1.0;\n float vy = tex.g * 2.0 - 1.0;\n float intensity = tex.b;\n\n float wave = 0.5 + 0.5 * sin(uTime * uFreq + intensity * 6.2831853);\n\n float amt = uStrength * intensity * wave;\n\n uv += vec2(vx, vy) * amt;\n }\n `;\n return new Effect('LiquidEffect', fragment, {\n uniforms: new Map([\n ['uTexture', new THREE.Uniform(texture)],\n ['uStrength', new THREE.Uniform(opts?.strength ?? 0.025)],\n ['uTime', new THREE.Uniform(0)],\n ['uFreq', new THREE.Uniform(opts?.freq ?? 4.5)]\n ])\n });\n};\n\nconst SHAPE_MAP = {\n square: 0,\n circle: 1,\n triangle: 2,\n diamond: 3\n};\n\nconst VERTEX_SRC = `\nvoid main() {\n gl_Position = vec4(position, 1.0);\n}\n`;\n\nconst FRAGMENT_SRC = `\nprecision highp float;\n\nuniform vec3 uColor;\nuniform vec2 uResolution;\nuniform float uTime;\nuniform float uPixelSize;\nuniform float uScale;\nuniform float uDensity;\nuniform float uPixelJitter;\nuniform int uEnableRipples;\nuniform float uRippleSpeed;\nuniform float uRippleThickness;\nuniform float uRippleIntensity;\nuniform float uEdgeFade;\n\nuniform int uShapeType;\nconst int SHAPE_SQUARE = 0;\nconst int SHAPE_CIRCLE = 1;\nconst int SHAPE_TRIANGLE = 2;\nconst int SHAPE_DIAMOND = 3;\n\nconst int MAX_CLICKS = 10;\n\nuniform vec2 uClickPos [MAX_CLICKS];\nuniform float uClickTimes[MAX_CLICKS];\n\nout vec4 fragColor;\n\nfloat Bayer2(vec2 a) {\n a = floor(a);\n return fract(a.x / 2. + a.y * a.y * .75);\n}\n#define Bayer4(a) (Bayer2(.5*(a))*0.25 + Bayer2(a))\n#define Bayer8(a) (Bayer4(.5*(a))*0.25 + Bayer2(a))\n\n#define FBM_OCTAVES 5\n#define FBM_LACUNARITY 1.25\n#define FBM_GAIN 1.0\n\nfloat hash11(float n){ return fract(sin(n)*43758.5453); }\n\nfloat vnoise(vec3 p){\n vec3 ip = floor(p);\n vec3 fp = fract(p);\n float n000 = hash11(dot(ip + vec3(0.0,0.0,0.0), vec3(1.0,57.0,113.0)));\n float n100 = hash11(dot(ip + vec3(1.0,0.0,0.0), vec3(1.0,57.0,113.0)));\n float n010 = hash11(dot(ip + vec3(0.0,1.0,0.0), vec3(1.0,57.0,113.0)));\n float n110 = hash11(dot(ip + vec3(1.0,1.0,0.0), vec3(1.0,57.0,113.0)));\n float n001 = hash11(dot(ip + vec3(0.0,0.0,1.0), vec3(1.0,57.0,113.0)));\n float n101 = hash11(dot(ip + vec3(1.0,0.0,1.0), vec3(1.0,57.0,113.0)));\n float n011 = hash11(dot(ip + vec3(0.0,1.0,1.0), vec3(1.0,57.0,113.0)));\n float n111 = hash11(dot(ip + vec3(1.0,1.0,1.0), vec3(1.0,57.0,113.0)));\n vec3 w = fp*fp*fp*(fp*(fp*6.0-15.0)+10.0);\n float x00 = mix(n000, n100, w.x);\n float x10 = mix(n010, n110, w.x);\n float x01 = mix(n001, n101, w.x);\n float x11 = mix(n011, n111, w.x);\n float y0 = mix(x00, x10, w.y);\n float y1 = mix(x01, x11, w.y);\n return mix(y0, y1, w.z) * 2.0 - 1.0;\n}\n\nfloat fbm2(vec2 uv, float t){\n vec3 p = vec3(uv * uScale, t);\n float amp = 1.0;\n float freq = 1.0;\n float sum = 1.0;\n for (int i = 0; i < FBM_OCTAVES; ++i){\n sum += amp * vnoise(p * freq);\n freq *= FBM_LACUNARITY;\n amp *= FBM_GAIN;\n }\n return sum * 0.5 + 0.5;\n}\n\nfloat maskCircle(vec2 p, float cov){\n float r = sqrt(cov) * .25;\n float d = length(p - 0.5) - r;\n float aa = 0.5 * fwidth(d);\n return cov * (1.0 - smoothstep(-aa, aa, d * 2.0));\n}\n\nfloat maskTriangle(vec2 p, vec2 id, float cov){\n bool flip = mod(id.x + id.y, 2.0) > 0.5;\n if (flip) p.x = 1.0 - p.x;\n float r = sqrt(cov);\n float d = p.y - r*(1.0 - p.x);\n float aa = fwidth(d);\n return cov * clamp(0.5 - d/aa, 0.0, 1.0);\n}\n\nfloat maskDiamond(vec2 p, float cov){\n float r = sqrt(cov) * 0.564;\n return step(abs(p.x - 0.49) + abs(p.y - 0.49), r);\n}\n\nvoid main(){\n float pixelSize = uPixelSize;\n vec2 fragCoord = gl_FragCoord.xy - uResolution * .5;\n float aspectRatio = uResolution.x / uResolution.y;\n\n vec2 pixelId = floor(fragCoord / pixelSize);\n vec2 pixelUV = fract(fragCoord / pixelSize);\n\n float cellPixelSize = 8.0 * pixelSize;\n vec2 cellId = floor(fragCoord / cellPixelSize);\n vec2 cellCoord = cellId * cellPixelSize;\n vec2 uv = cellCoord / uResolution * vec2(aspectRatio, 1.0);\n\n float base = fbm2(uv, uTime * 0.05);\n base = base * 0.5 - 0.65;\n\n float feed = base + (uDensity - 0.5) * 0.3;\n\n float speed = uRippleSpeed;\n float thickness = uRippleThickness;\n const float dampT = 1.0;\n const float dampR = 10.0;\n\n if (uEnableRipples == 1) {\n for (int i = 0; i < MAX_CLICKS; ++i){\n vec2 pos = uClickPos[i];\n if (pos.x < 0.0) continue;\n float cellPixelSize = 8.0 * pixelSize;\n vec2 cuv = (((pos - uResolution * .5 - cellPixelSize * .5) / (uResolution))) * vec2(aspectRatio, 1.0);\n float t = max(uTime - uClickTimes[i], 0.0);\n float r = distance(uv, cuv);\n float waveR = speed * t;\n float ring = exp(-pow((r - waveR) / thickness, 2.0));\n float atten = exp(-dampT * t) * exp(-dampR * r);\n feed = max(feed, ring * atten * uRippleIntensity);\n }\n }\n\n float bayer = Bayer8(fragCoord / uPixelSize) - 0.5;\n float bw = step(0.5, feed + bayer);\n\n float h = fract(sin(dot(floor(fragCoord / uPixelSize), vec2(127.1, 311.7))) * 43758.5453);\n float jitterScale = 1.0 + (h - 0.5) * uPixelJitter;\n float coverage = bw * jitterScale;\n float M;\n if (uShapeType == SHAPE_CIRCLE) M = maskCircle (pixelUV, coverage);\n else if (uShapeType == SHAPE_TRIANGLE) M = maskTriangle(pixelUV, pixelId, coverage);\n else if (uShapeType == SHAPE_DIAMOND) M = maskDiamond(pixelUV, coverage);\n else M = coverage;\n\n if (uEdgeFade > 0.0) {\n vec2 norm = gl_FragCoord.xy / uResolution;\n float edge = min(min(norm.x, norm.y), min(1.0 - norm.x, 1.0 - norm.y));\n float fade = smoothstep(0.0, uEdgeFade, edge);\n M *= fade;\n }\n\n vec3 color = uColor;\n\n // sRGB gamma correction - convert linear to sRGB for accurate color output\n vec3 srgbColor = mix(\n color * 12.92,\n 1.055 * pow(color, vec3(1.0 / 2.4)) - 0.055,\n step(0.0031308, color)\n );\n\n fragColor = vec4(srgbColor, M);\n}\n`;\n\nconst MAX_CLICKS = 10;\n\nconst PixelBlast = ({\n variant = 'square',\n pixelSize = 3,\n color = '#B497CF',\n className,\n style,\n antialias = true,\n patternScale = 2,\n patternDensity = 1,\n liquid = false,\n liquidStrength = 0.1,\n liquidRadius = 1,\n pixelSizeJitter = 0,\n enableRipples = true,\n rippleIntensityScale = 1,\n rippleThickness = 0.1,\n rippleSpeed = 0.3,\n liquidWobbleSpeed = 4.5,\n autoPauseOffscreen = true,\n speed = 0.5,\n transparent = true,\n edgeFade = 0.5,\n noiseAmount = 0\n}) => {\n const containerRef = useRef(null);\n const visibilityRef = useRef({ visible: true });\n const speedRef = useRef(speed);\n\n const threeRef = useRef(null);\n const prevConfigRef = useRef(null);\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n speedRef.current = speed;\n const needsReinitKeys = ['antialias', 'liquid', 'noiseAmount'];\n const cfg = { antialias, liquid, noiseAmount };\n let mustReinit = false;\n if (!threeRef.current) mustReinit = true;\n else if (prevConfigRef.current) {\n for (const k of needsReinitKeys)\n if (prevConfigRef.current[k] !== cfg[k]) {\n mustReinit = true;\n break;\n }\n }\n if (mustReinit) {\n if (threeRef.current) {\n const t = threeRef.current;\n t.resizeObserver?.disconnect();\n cancelAnimationFrame(t.raf);\n t.quad?.geometry.dispose();\n t.material.dispose();\n t.composer?.dispose();\n t.renderer.dispose();\n t.renderer.forceContextLoss();\n if (t.renderer.domElement.parentElement === container) container.removeChild(t.renderer.domElement);\n threeRef.current = null;\n }\n const canvas = document.createElement('canvas');\n const renderer = new THREE.WebGLRenderer({\n canvas,\n antialias,\n alpha: true,\n powerPreference: 'high-performance'\n });\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n container.appendChild(renderer.domElement);\n if (transparent) renderer.setClearAlpha(0);\n else renderer.setClearColor(0x000000, 1);\n const uniforms = {\n uResolution: { value: new THREE.Vector2(0, 0) },\n uTime: { value: 0 },\n uColor: { value: new THREE.Color(color) },\n uClickPos: {\n value: Array.from({ length: MAX_CLICKS }, () => new THREE.Vector2(-1, -1))\n },\n uClickTimes: { value: new Float32Array(MAX_CLICKS) },\n uShapeType: { value: SHAPE_MAP[variant] ?? 0 },\n uPixelSize: { value: pixelSize * renderer.getPixelRatio() },\n uScale: { value: patternScale },\n uDensity: { value: patternDensity },\n uPixelJitter: { value: pixelSizeJitter },\n uEnableRipples: { value: enableRipples ? 1 : 0 },\n uRippleSpeed: { value: rippleSpeed },\n uRippleThickness: { value: rippleThickness },\n uRippleIntensity: { value: rippleIntensityScale },\n uEdgeFade: { value: edgeFade }\n };\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n const material = new THREE.ShaderMaterial({\n vertexShader: VERTEX_SRC,\n fragmentShader: FRAGMENT_SRC,\n uniforms,\n transparent: true,\n depthTest: false,\n depthWrite: false,\n glslVersion: THREE.GLSL3\n });\n const quadGeom = new THREE.PlaneGeometry(2, 2);\n const quad = new THREE.Mesh(quadGeom, material);\n scene.add(quad);\n const clock = new THREE.Clock();\n const setSize = () => {\n const w = container.clientWidth || 1;\n const h = container.clientHeight || 1;\n renderer.setSize(w, h, false);\n uniforms.uResolution.value.set(renderer.domElement.width, renderer.domElement.height);\n if (threeRef.current?.composer)\n threeRef.current.composer.setSize(renderer.domElement.width, renderer.domElement.height);\n uniforms.uPixelSize.value = pixelSize * renderer.getPixelRatio();\n };\n setSize();\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n const randomFloat = () => {\n if (typeof window !== 'undefined' && window.crypto?.getRandomValues) {\n const u32 = new Uint32Array(1);\n window.crypto.getRandomValues(u32);\n return u32[0] / 0xffffffff;\n }\n return Math.random();\n };\n const timeOffset = randomFloat() * 1000;\n let composer;\n let touch;\n let liquidEffect;\n if (liquid) {\n touch = createTouchTexture();\n touch.radiusScale = liquidRadius;\n composer = new EffectComposer(renderer);\n const renderPass = new RenderPass(scene, camera);\n liquidEffect = createLiquidEffect(touch.texture, {\n strength: liquidStrength,\n freq: liquidWobbleSpeed\n });\n const effectPass = new EffectPass(camera, liquidEffect);\n effectPass.renderToScreen = true;\n composer.addPass(renderPass);\n composer.addPass(effectPass);\n }\n if (noiseAmount > 0) {\n if (!composer) {\n composer = new EffectComposer(renderer);\n composer.addPass(new RenderPass(scene, camera));\n }\n const noiseEffect = new Effect(\n 'NoiseEffect',\n `uniform float uTime; uniform float uAmount; float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7))) * 43758.5453);} void mainUv(inout vec2 uv){} void mainImage(const in vec4 inputColor,const in vec2 uv,out vec4 outputColor){ float n=hash(floor(uv*vec2(1920.0,1080.0))+floor(uTime*60.0)); float g=(n-0.5)*uAmount; outputColor=inputColor+vec4(vec3(g),0.0);} `,\n {\n uniforms: new Map([\n ['uTime', new THREE.Uniform(0)],\n ['uAmount', new THREE.Uniform(noiseAmount)]\n ])\n }\n );\n const noisePass = new EffectPass(camera, noiseEffect);\n noisePass.renderToScreen = true;\n if (composer && composer.passes.length > 0) composer.passes.forEach(p => (p.renderToScreen = false));\n composer.addPass(noisePass);\n }\n if (composer) composer.setSize(renderer.domElement.width, renderer.domElement.height);\n const mapToPixels = e => {\n const rect = renderer.domElement.getBoundingClientRect();\n const scaleX = renderer.domElement.width / rect.width;\n const scaleY = renderer.domElement.height / rect.height;\n const fx = (e.clientX - rect.left) * scaleX;\n const fy = (rect.height - (e.clientY - rect.top)) * scaleY;\n return {\n fx,\n fy,\n w: renderer.domElement.width,\n h: renderer.domElement.height\n };\n };\n const onPointerDown = e => {\n const { fx, fy } = mapToPixels(e);\n const ix = threeRef.current?.clickIx ?? 0;\n uniforms.uClickPos.value[ix].set(fx, fy);\n uniforms.uClickTimes.value[ix] = uniforms.uTime.value;\n if (threeRef.current) threeRef.current.clickIx = (ix + 1) % MAX_CLICKS;\n };\n const onPointerMove = e => {\n if (!touch) return;\n const { fx, fy, w, h } = mapToPixels(e);\n touch.addTouch({ x: fx / w, y: fy / h });\n };\n renderer.domElement.addEventListener('pointerdown', onPointerDown, {\n passive: true\n });\n renderer.domElement.addEventListener('pointermove', onPointerMove, {\n passive: true\n });\n let raf = 0;\n const animate = () => {\n if (autoPauseOffscreen && !visibilityRef.current.visible) {\n raf = requestAnimationFrame(animate);\n return;\n }\n uniforms.uTime.value = timeOffset + clock.getElapsedTime() * speedRef.current;\n if (liquidEffect) liquidEffect.uniforms.get('uTime').value = uniforms.uTime.value;\n if (composer) {\n if (touch) touch.update();\n composer.passes.forEach(p => {\n const effs = p.effects;\n if (effs)\n effs.forEach(eff => {\n const u = eff.uniforms?.get('uTime');\n if (u) u.value = uniforms.uTime.value;\n });\n });\n composer.render();\n } else renderer.render(scene, camera);\n raf = requestAnimationFrame(animate);\n };\n raf = requestAnimationFrame(animate);\n threeRef.current = {\n renderer,\n scene,\n camera,\n material,\n clock,\n clickIx: 0,\n uniforms,\n resizeObserver: ro,\n raf,\n quad,\n timeOffset,\n composer,\n touch,\n liquidEffect\n };\n } else {\n const t = threeRef.current;\n t.uniforms.uShapeType.value = SHAPE_MAP[variant] ?? 0;\n t.uniforms.uPixelSize.value = pixelSize * t.renderer.getPixelRatio();\n t.uniforms.uColor.value.set(color);\n t.uniforms.uScale.value = patternScale;\n t.uniforms.uDensity.value = patternDensity;\n t.uniforms.uPixelJitter.value = pixelSizeJitter;\n t.uniforms.uEnableRipples.value = enableRipples ? 1 : 0;\n t.uniforms.uRippleIntensity.value = rippleIntensityScale;\n t.uniforms.uRippleThickness.value = rippleThickness;\n t.uniforms.uRippleSpeed.value = rippleSpeed;\n t.uniforms.uEdgeFade.value = edgeFade;\n if (transparent) t.renderer.setClearAlpha(0);\n else t.renderer.setClearColor(0x000000, 1);\n if (t.liquidEffect) {\n const uStrength = t.liquidEffect;\n if (uStrength) uStrength.value = liquidStrength;\n const uFreq = t.liquidEffect.uniforms.get('uFreq');\n if (uFreq) uFreq.value = liquidWobbleSpeed;\n }\n if (t.touch) t.touch.radiusScale = liquidRadius;\n }\n prevConfigRef.current = cfg;\n return () => {\n if (threeRef.current && mustReinit) return;\n if (!threeRef.current) return;\n const t = threeRef.current;\n t.resizeObserver?.disconnect();\n cancelAnimationFrame(t.raf);\n t.quad?.geometry.dispose();\n t.material.dispose();\n t.composer?.dispose();\n t.renderer.dispose();\n t.renderer.forceContextLoss();\n if (t.renderer.domElement.parentElement === container) container.removeChild(t.renderer.domElement);\n threeRef.current = null;\n };\n }, [\n antialias,\n liquid,\n noiseAmount,\n pixelSize,\n patternScale,\n patternDensity,\n enableRipples,\n rippleIntensityScale,\n rippleThickness,\n rippleSpeed,\n pixelSizeJitter,\n edgeFade,\n transparent,\n liquidStrength,\n liquidRadius,\n liquidWobbleSpeed,\n autoPauseOffscreen,\n variant,\n color,\n speed\n ]);\n\n return (\n \n );\n};\n\nexport default PixelBlast;\n" } ], diff --git a/public/r/PixelBlast-TS-CSS.json b/public/r/PixelBlast-TS-CSS.json index c9b9f6b5..c13da9e7 100644 --- a/public/r/PixelBlast-TS-CSS.json +++ b/public/r/PixelBlast-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "PixelBlast/PixelBlast.css", + "type": "registry:file", + "path": "PixelBlast.css", + "target": "@components/PixelBlast.css", "content": ".pixel-blast-container {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "PixelBlast/PixelBlast.tsx", + "path": "PixelBlast.tsx", "content": "import { Effect, EffectComposer, EffectPass, RenderPass } from 'postprocessing';\nimport React, { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\nimport './PixelBlast.css';\n\ntype PixelBlastVariant = 'square' | 'circle' | 'triangle' | 'diamond';\n\ninterface TouchPoint {\n x: number;\n y: number;\n vx: number;\n vy: number;\n force: number;\n age: number;\n}\n\ninterface TouchTexture {\n canvas: HTMLCanvasElement;\n texture: THREE.Texture;\n addTouch: (norm: { x: number; y: number }) => void;\n update: () => void;\n radiusScale: number;\n size: number;\n}\n\ninterface ReinitConfig {\n antialias: boolean;\n liquid: boolean;\n noiseAmount: number;\n}\n\ntype PixelBlastProps = {\n variant?: PixelBlastVariant;\n pixelSize?: number;\n color?: string;\n className?: string;\n style?: React.CSSProperties;\n antialias?: boolean;\n patternScale?: number;\n patternDensity?: number;\n liquid?: boolean;\n liquidStrength?: number;\n liquidRadius?: number;\n pixelSizeJitter?: number;\n enableRipples?: boolean;\n rippleIntensityScale?: number;\n rippleThickness?: number;\n rippleSpeed?: number;\n liquidWobbleSpeed?: number;\n autoPauseOffscreen?: boolean;\n speed?: number;\n transparent?: boolean;\n edgeFade?: number;\n noiseAmount?: number;\n};\n\nconst createTouchTexture = (): TouchTexture => {\n const size = 64;\n const canvas = document.createElement('canvas');\n canvas.width = size;\n canvas.height = size;\n const ctx = canvas.getContext('2d');\n if (!ctx) throw new Error('2D context not available');\n ctx.fillStyle = 'black';\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n const texture = new THREE.Texture(canvas);\n texture.minFilter = THREE.LinearFilter;\n texture.magFilter = THREE.LinearFilter;\n texture.generateMipmaps = false;\n const trail: TouchPoint[] = [];\n let last: { x: number; y: number } | null = null;\n const maxAge = 64;\n let radius = 0.1 * size;\n const speed = 1 / maxAge;\n const clear = () => {\n ctx.fillStyle = 'black';\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n };\n const drawPoint = (p: TouchPoint) => {\n const pos = { x: p.x * size, y: (1 - p.y) * size };\n let intensity = 1;\n const easeOutSine = (t: number) => Math.sin((t * Math.PI) / 2);\n const easeOutQuad = (t: number) => -t * (t - 2);\n if (p.age < maxAge * 0.3) intensity = easeOutSine(p.age / (maxAge * 0.3));\n else intensity = easeOutQuad(1 - (p.age - maxAge * 0.3) / (maxAge * 0.7)) || 0;\n intensity *= p.force;\n const color = `${((p.vx + 1) / 2) * 255}, ${((p.vy + 1) / 2) * 255}, ${intensity * 255}`;\n const offset = size * 5;\n ctx.shadowOffsetX = offset;\n ctx.shadowOffsetY = offset;\n ctx.shadowBlur = radius;\n ctx.shadowColor = `rgba(${color},${0.22 * intensity})`;\n ctx.beginPath();\n ctx.fillStyle = 'rgba(255,0,0,1)';\n ctx.arc(pos.x - offset, pos.y - offset, radius, 0, Math.PI * 2);\n ctx.fill();\n };\n const addTouch = (norm: { x: number; y: number }) => {\n let force = 0;\n let vx = 0;\n let vy = 0;\n if (last) {\n const dx = norm.x - last.x;\n const dy = norm.y - last.y;\n if (dx === 0 && dy === 0) return;\n const dd = dx * dx + dy * dy;\n const d = Math.sqrt(dd);\n vx = dx / (d || 1);\n vy = dy / (d || 1);\n force = Math.min(dd * 10000, 1);\n }\n last = { x: norm.x, y: norm.y };\n trail.push({ x: norm.x, y: norm.y, age: 0, force, vx, vy });\n };\n const update = () => {\n clear();\n for (let i = trail.length - 1; i >= 0; i--) {\n const point = trail[i];\n const f = point.force * speed * (1 - point.age / maxAge);\n point.x += point.vx * f;\n point.y += point.vy * f;\n point.age++;\n if (point.age > maxAge) trail.splice(i, 1);\n }\n for (let i = 0; i < trail.length; i++) drawPoint(trail[i]);\n texture.needsUpdate = true;\n };\n return {\n canvas,\n texture,\n addTouch,\n update,\n set radiusScale(v: number) {\n radius = 0.1 * size * v;\n },\n get radiusScale() {\n return radius / (0.1 * size);\n },\n size\n };\n};\n\nconst createLiquidEffect = (texture: THREE.Texture, opts?: { strength?: number; freq?: number }) => {\n const fragment = `\n uniform sampler2D uTexture;\n uniform float uStrength;\n uniform float uTime;\n uniform float uFreq;\n\n void mainUv(inout vec2 uv) {\n vec4 tex = texture2D(uTexture, uv);\n float vx = tex.r * 2.0 - 1.0;\n float vy = tex.g * 2.0 - 1.0;\n float intensity = tex.b;\n\n float wave = 0.5 + 0.5 * sin(uTime * uFreq + intensity * 6.2831853);\n\n float amt = uStrength * intensity * wave;\n\n uv += vec2(vx, vy) * amt;\n }\n `;\n return new Effect('LiquidEffect', fragment, {\n uniforms: new Map([\n ['uTexture', new THREE.Uniform(texture)],\n ['uStrength', new THREE.Uniform(opts?.strength ?? 0.025)],\n ['uTime', new THREE.Uniform(0)],\n ['uFreq', new THREE.Uniform(opts?.freq ?? 4.5)]\n ])\n });\n};\n\nconst SHAPE_MAP: Record = {\n square: 0,\n circle: 1,\n triangle: 2,\n diamond: 3\n};\n\nconst VERTEX_SRC = `\nvoid main() {\n gl_Position = vec4(position, 1.0);\n}\n`;\n\nconst FRAGMENT_SRC = `\nprecision highp float;\n\nuniform vec3 uColor;\nuniform vec2 uResolution;\nuniform float uTime;\nuniform float uPixelSize;\nuniform float uScale;\nuniform float uDensity;\nuniform float uPixelJitter;\nuniform int uEnableRipples;\nuniform float uRippleSpeed;\nuniform float uRippleThickness;\nuniform float uRippleIntensity;\nuniform float uEdgeFade;\n\nuniform int uShapeType;\nconst int SHAPE_SQUARE = 0;\nconst int SHAPE_CIRCLE = 1;\nconst int SHAPE_TRIANGLE = 2;\nconst int SHAPE_DIAMOND = 3;\n\nconst int MAX_CLICKS = 10;\n\nuniform vec2 uClickPos [MAX_CLICKS];\nuniform float uClickTimes[MAX_CLICKS];\n\nout vec4 fragColor;\n\nfloat Bayer2(vec2 a) {\n a = floor(a);\n return fract(a.x / 2. + a.y * a.y * .75);\n}\n#define Bayer4(a) (Bayer2(.5*(a))*0.25 + Bayer2(a))\n#define Bayer8(a) (Bayer4(.5*(a))*0.25 + Bayer2(a))\n\n#define FBM_OCTAVES 5\n#define FBM_LACUNARITY 1.25\n#define FBM_GAIN 1.0\n\nfloat hash11(float n){ return fract(sin(n)*43758.5453); }\n\nfloat vnoise(vec3 p){\n vec3 ip = floor(p);\n vec3 fp = fract(p);\n float n000 = hash11(dot(ip + vec3(0.0,0.0,0.0), vec3(1.0,57.0,113.0)));\n float n100 = hash11(dot(ip + vec3(1.0,0.0,0.0), vec3(1.0,57.0,113.0)));\n float n010 = hash11(dot(ip + vec3(0.0,1.0,0.0), vec3(1.0,57.0,113.0)));\n float n110 = hash11(dot(ip + vec3(1.0,1.0,0.0), vec3(1.0,57.0,113.0)));\n float n001 = hash11(dot(ip + vec3(0.0,0.0,1.0), vec3(1.0,57.0,113.0)));\n float n101 = hash11(dot(ip + vec3(1.0,0.0,1.0), vec3(1.0,57.0,113.0)));\n float n011 = hash11(dot(ip + vec3(0.0,1.0,1.0), vec3(1.0,57.0,113.0)));\n float n111 = hash11(dot(ip + vec3(1.0,1.0,1.0), vec3(1.0,57.0,113.0)));\n vec3 w = fp*fp*fp*(fp*(fp*6.0-15.0)+10.0);\n float x00 = mix(n000, n100, w.x);\n float x10 = mix(n010, n110, w.x);\n float x01 = mix(n001, n101, w.x);\n float x11 = mix(n011, n111, w.x);\n float y0 = mix(x00, x10, w.y);\n float y1 = mix(x01, x11, w.y);\n return mix(y0, y1, w.z) * 2.0 - 1.0;\n}\n\nfloat fbm2(vec2 uv, float t){\n vec3 p = vec3(uv * uScale, t);\n float amp = 1.0;\n float freq = 1.0;\n float sum = 1.0;\n for (int i = 0; i < FBM_OCTAVES; ++i){\n sum += amp * vnoise(p * freq);\n freq *= FBM_LACUNARITY;\n amp *= FBM_GAIN;\n }\n return sum * 0.5 + 0.5;\n}\n\nfloat maskCircle(vec2 p, float cov){\n float r = sqrt(cov) * .25;\n float d = length(p - 0.5) - r;\n float aa = 0.5 * fwidth(d);\n return cov * (1.0 - smoothstep(-aa, aa, d * 2.0));\n}\n\nfloat maskTriangle(vec2 p, vec2 id, float cov){\n bool flip = mod(id.x + id.y, 2.0) > 0.5;\n if (flip) p.x = 1.0 - p.x;\n float r = sqrt(cov);\n float d = p.y - r*(1.0 - p.x);\n float aa = fwidth(d);\n return cov * clamp(0.5 - d/aa, 0.0, 1.0);\n}\n\nfloat maskDiamond(vec2 p, float cov){\n float r = sqrt(cov) * 0.564;\n return step(abs(p.x - 0.49) + abs(p.y - 0.49), r);\n}\n\nvoid main(){\n float pixelSize = uPixelSize;\n vec2 fragCoord = gl_FragCoord.xy - uResolution * .5;\n float aspectRatio = uResolution.x / uResolution.y;\n\n vec2 pixelId = floor(fragCoord / pixelSize);\n vec2 pixelUV = fract(fragCoord / pixelSize);\n\n float cellPixelSize = 8.0 * pixelSize;\n vec2 cellId = floor(fragCoord / cellPixelSize);\n vec2 cellCoord = cellId * cellPixelSize;\n vec2 uv = cellCoord / uResolution * vec2(aspectRatio, 1.0);\n\n float base = fbm2(uv, uTime * 0.05);\n base = base * 0.5 - 0.65;\n\n float feed = base + (uDensity - 0.5) * 0.3;\n\n float speed = uRippleSpeed;\n float thickness = uRippleThickness;\n const float dampT = 1.0;\n const float dampR = 10.0;\n\n if (uEnableRipples == 1) {\n for (int i = 0; i < MAX_CLICKS; ++i){\n vec2 pos = uClickPos[i];\n if (pos.x < 0.0) continue;\n float cellPixelSize = 8.0 * pixelSize;\n vec2 cuv = (((pos - uResolution * .5 - cellPixelSize * .5) / (uResolution))) * vec2(aspectRatio, 1.0);\n float t = max(uTime - uClickTimes[i], 0.0);\n float r = distance(uv, cuv);\n float waveR = speed * t;\n float ring = exp(-pow((r - waveR) / thickness, 2.0));\n float atten = exp(-dampT * t) * exp(-dampR * r);\n feed = max(feed, ring * atten * uRippleIntensity);\n }\n }\n\n float bayer = Bayer8(fragCoord / uPixelSize) - 0.5;\n float bw = step(0.5, feed + bayer);\n\n float h = fract(sin(dot(floor(fragCoord / uPixelSize), vec2(127.1, 311.7))) * 43758.5453);\n float jitterScale = 1.0 + (h - 0.5) * uPixelJitter;\n float coverage = bw * jitterScale;\n float M;\n if (uShapeType == SHAPE_CIRCLE) M = maskCircle (pixelUV, coverage);\n else if (uShapeType == SHAPE_TRIANGLE) M = maskTriangle(pixelUV, pixelId, coverage);\n else if (uShapeType == SHAPE_DIAMOND) M = maskDiamond(pixelUV, coverage);\n else M = coverage;\n\n if (uEdgeFade > 0.0) {\n vec2 norm = gl_FragCoord.xy / uResolution;\n float edge = min(min(norm.x, norm.y), min(1.0 - norm.x, 1.0 - norm.y));\n float fade = smoothstep(0.0, uEdgeFade, edge);\n M *= fade;\n }\n\n vec3 color = uColor;\n\n // sRGB gamma correction - convert linear to sRGB for accurate color output\n vec3 srgbColor = mix(\n color * 12.92,\n 1.055 * pow(color, vec3(1.0 / 2.4)) - 0.055,\n step(0.0031308, color)\n );\n\n fragColor = vec4(srgbColor, M);\n}\n`;\n\nconst MAX_CLICKS = 10;\n\nconst PixelBlast: React.FC = ({\n variant = 'square',\n pixelSize = 3,\n color = '#B497CF',\n className,\n style,\n antialias = true,\n patternScale = 2,\n patternDensity = 1,\n liquid = false,\n liquidStrength = 0.1,\n liquidRadius = 1,\n pixelSizeJitter = 0,\n enableRipples = true,\n rippleIntensityScale = 1,\n rippleThickness = 0.1,\n rippleSpeed = 0.3,\n liquidWobbleSpeed = 4.5,\n autoPauseOffscreen = true,\n speed = 0.5,\n transparent = true,\n edgeFade = 0.5,\n noiseAmount = 0\n}) => {\n const containerRef = useRef(null);\n const visibilityRef = useRef({ visible: true });\n const speedRef = useRef(speed);\n\n const threeRef = useRef<{\n renderer: THREE.WebGLRenderer;\n scene: THREE.Scene;\n camera: THREE.OrthographicCamera;\n material: THREE.ShaderMaterial;\n clock: THREE.Clock;\n clickIx: number;\n uniforms: {\n uResolution: { value: THREE.Vector2 };\n uTime: { value: number };\n uColor: { value: THREE.Color };\n uClickPos: { value: THREE.Vector2[] };\n uClickTimes: { value: Float32Array };\n uShapeType: { value: number };\n uPixelSize: { value: number };\n uScale: { value: number };\n uDensity: { value: number };\n uPixelJitter: { value: number };\n uEnableRipples: { value: number };\n uRippleSpeed: { value: number };\n uRippleThickness: { value: number };\n uRippleIntensity: { value: number };\n uEdgeFade: { value: number };\n };\n resizeObserver?: ResizeObserver;\n raf?: number;\n quad?: THREE.Mesh;\n timeOffset?: number;\n composer?: EffectComposer;\n touch?: ReturnType;\n liquidEffect?: Effect;\n } | null>(null);\n const prevConfigRef = useRef(null);\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n speedRef.current = speed;\n const needsReinitKeys: (keyof ReinitConfig)[] = ['antialias', 'liquid', 'noiseAmount'];\n const cfg: ReinitConfig = { antialias, liquid, noiseAmount };\n let mustReinit = false;\n if (!threeRef.current) mustReinit = true;\n else if (prevConfigRef.current) {\n for (const k of needsReinitKeys)\n if (prevConfigRef.current[k] !== cfg[k]) {\n mustReinit = true;\n break;\n }\n }\n if (mustReinit) {\n if (threeRef.current) {\n const t = threeRef.current;\n t.resizeObserver?.disconnect();\n cancelAnimationFrame(t.raf!);\n t.quad?.geometry.dispose();\n t.material.dispose();\n t.composer?.dispose();\n t.renderer.dispose();\n t.renderer.forceContextLoss();\n if (t.renderer.domElement.parentElement === container) container.removeChild(t.renderer.domElement);\n threeRef.current = null;\n }\n const canvas = document.createElement('canvas');\n const renderer = new THREE.WebGLRenderer({\n canvas,\n antialias,\n alpha: true,\n powerPreference: 'high-performance'\n });\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n container.appendChild(renderer.domElement);\n if (transparent) renderer.setClearAlpha(0);\n else renderer.setClearColor(0x000000, 1);\n const uniforms = {\n uResolution: { value: new THREE.Vector2(0, 0) },\n uTime: { value: 0 },\n uColor: { value: new THREE.Color(color) },\n uClickPos: {\n value: Array.from({ length: MAX_CLICKS }, () => new THREE.Vector2(-1, -1))\n },\n uClickTimes: { value: new Float32Array(MAX_CLICKS) },\n uShapeType: { value: SHAPE_MAP[variant] ?? 0 },\n uPixelSize: { value: pixelSize * renderer.getPixelRatio() },\n uScale: { value: patternScale },\n uDensity: { value: patternDensity },\n uPixelJitter: { value: pixelSizeJitter },\n uEnableRipples: { value: enableRipples ? 1 : 0 },\n uRippleSpeed: { value: rippleSpeed },\n uRippleThickness: { value: rippleThickness },\n uRippleIntensity: { value: rippleIntensityScale },\n uEdgeFade: { value: edgeFade }\n };\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n const material = new THREE.ShaderMaterial({\n vertexShader: VERTEX_SRC,\n fragmentShader: FRAGMENT_SRC,\n uniforms,\n transparent: true,\n depthTest: false,\n depthWrite: false,\n glslVersion: THREE.GLSL3\n });\n\n const quadGeom = new THREE.PlaneGeometry(2, 2);\n const quad = new THREE.Mesh(quadGeom, material);\n scene.add(quad);\n const clock = new THREE.Clock();\n const setSize = () => {\n const w = container.clientWidth || 1;\n const h = container.clientHeight || 1;\n renderer.setSize(w, h, false);\n uniforms.uResolution.value.set(renderer.domElement.width, renderer.domElement.height);\n if (threeRef.current?.composer)\n threeRef.current.composer.setSize(renderer.domElement.width, renderer.domElement.height);\n uniforms.uPixelSize.value = pixelSize * renderer.getPixelRatio();\n };\n setSize();\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n const randomFloat = (): number => {\n if (typeof window !== 'undefined' && window.crypto?.getRandomValues) {\n const u32 = new Uint32Array(1);\n window.crypto.getRandomValues(u32);\n return u32[0] / 0xffffffff;\n }\n return Math.random();\n };\n const timeOffset = randomFloat() * 1000;\n let composer: EffectComposer | undefined;\n let touch: ReturnType | undefined;\n let liquidEffect: Effect | undefined;\n if (liquid) {\n touch = createTouchTexture();\n touch.radiusScale = liquidRadius;\n composer = new EffectComposer(renderer);\n const renderPass = new RenderPass(scene, camera);\n liquidEffect = createLiquidEffect(touch.texture, {\n strength: liquidStrength,\n freq: liquidWobbleSpeed\n });\n const effectPass = new EffectPass(camera, liquidEffect);\n effectPass.renderToScreen = true;\n composer.addPass(renderPass);\n composer.addPass(effectPass);\n }\n if (noiseAmount > 0) {\n if (!composer) {\n composer = new EffectComposer(renderer);\n composer.addPass(new RenderPass(scene, camera));\n }\n const noiseEffect = new Effect(\n 'NoiseEffect',\n `uniform float uTime; uniform float uAmount; float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7))) * 43758.5453);} void mainUv(inout vec2 uv){} void mainImage(const in vec4 inputColor,const in vec2 uv,out vec4 outputColor){ float n=hash(floor(uv*vec2(1920.0,1080.0))+floor(uTime*60.0)); float g=(n-0.5)*uAmount; outputColor=inputColor+vec4(vec3(g),0.0);} `,\n {\n uniforms: new Map([\n ['uTime', new THREE.Uniform(0)],\n ['uAmount', new THREE.Uniform(noiseAmount)]\n ])\n }\n );\n const noisePass = new EffectPass(camera, noiseEffect);\n noisePass.renderToScreen = true;\n if (composer && composer.passes.length > 0) {\n composer.passes.forEach(p => {\n const pass = p as { renderToScreen?: boolean };\n pass.renderToScreen = false;\n });\n }\n composer.addPass(noisePass);\n }\n if (composer) composer.setSize(renderer.domElement.width, renderer.domElement.height);\n const mapToPixels = (e: PointerEvent) => {\n const rect = renderer.domElement.getBoundingClientRect();\n const scaleX = renderer.domElement.width / rect.width;\n const scaleY = renderer.domElement.height / rect.height;\n const fx = (e.clientX - rect.left) * scaleX;\n const fy = (rect.height - (e.clientY - rect.top)) * scaleY;\n return {\n fx,\n fy,\n w: renderer.domElement.width,\n h: renderer.domElement.height\n };\n };\n const onPointerDown = (e: PointerEvent) => {\n const { fx, fy } = mapToPixels(e);\n const ix = threeRef.current?.clickIx ?? 0;\n uniforms.uClickPos.value[ix].set(fx, fy);\n uniforms.uClickTimes.value[ix] = uniforms.uTime.value;\n if (threeRef.current) threeRef.current.clickIx = (ix + 1) % MAX_CLICKS;\n };\n const onPointerMove = (e: PointerEvent) => {\n if (!touch) return;\n const { fx, fy, w, h } = mapToPixels(e);\n touch.addTouch({ x: fx / w, y: fy / h });\n };\n renderer.domElement.addEventListener('pointerdown', onPointerDown, {\n passive: true\n });\n renderer.domElement.addEventListener('pointermove', onPointerMove, {\n passive: true\n });\n let raf = 0;\n const animate = () => {\n if (autoPauseOffscreen && !visibilityRef.current.visible) {\n raf = requestAnimationFrame(animate);\n return;\n }\n uniforms.uTime.value = timeOffset + clock.getElapsedTime() * speedRef.current;\n if (liquidEffect) {\n const liqEffect = liquidEffect as Effect & { uniforms: Map };\n const timeUniform = liqEffect.uniforms.get('uTime');\n if (timeUniform) timeUniform.value = uniforms.uTime.value;\n }\n if (composer) {\n if (touch) touch.update();\n composer.passes.forEach(p => {\n const pass = p as { effects?: Array }> };\n if (pass.effects) {\n pass.effects.forEach(eff => {\n const timeUniform = eff.uniforms?.get('uTime');\n if (timeUniform) timeUniform.value = uniforms.uTime.value;\n });\n }\n });\n composer.render();\n } else renderer.render(scene, camera);\n raf = requestAnimationFrame(animate);\n };\n raf = requestAnimationFrame(animate);\n threeRef.current = {\n renderer,\n scene,\n camera,\n material,\n clock,\n clickIx: 0,\n uniforms,\n resizeObserver: ro,\n raf,\n quad,\n timeOffset,\n composer,\n touch,\n liquidEffect\n };\n } else {\n const t = threeRef.current!;\n t.uniforms.uShapeType.value = SHAPE_MAP[variant] ?? 0;\n t.uniforms.uPixelSize.value = pixelSize * t.renderer.getPixelRatio();\n t.uniforms.uColor.value.set(color);\n t.uniforms.uScale.value = patternScale;\n t.uniforms.uDensity.value = patternDensity;\n t.uniforms.uPixelJitter.value = pixelSizeJitter;\n t.uniforms.uEnableRipples.value = enableRipples ? 1 : 0;\n t.uniforms.uRippleIntensity.value = rippleIntensityScale;\n t.uniforms.uRippleThickness.value = rippleThickness;\n t.uniforms.uRippleSpeed.value = rippleSpeed;\n t.uniforms.uEdgeFade.value = edgeFade;\n if (transparent) t.renderer.setClearAlpha(0);\n else t.renderer.setClearColor(0x000000, 1);\n if (t.liquidEffect) {\n const liqEffect = t.liquidEffect as Effect & { uniforms: Map };\n const uStrength = liqEffect.uniforms.get('uStrength');\n if (uStrength) uStrength.value = liquidStrength;\n const uFreq = liqEffect.uniforms.get('uFreq');\n if (uFreq) uFreq.value = liquidWobbleSpeed;\n }\n if (t.touch) t.touch.radiusScale = liquidRadius;\n }\n prevConfigRef.current = cfg;\n return () => {\n if (threeRef.current && mustReinit) return;\n if (!threeRef.current) return;\n const t = threeRef.current;\n t.resizeObserver?.disconnect();\n cancelAnimationFrame(t.raf!);\n t.quad?.geometry.dispose();\n t.material.dispose();\n t.composer?.dispose();\n t.renderer.dispose();\n t.renderer.forceContextLoss();\n if (t.renderer.domElement.parentElement === container) container.removeChild(t.renderer.domElement);\n threeRef.current = null;\n };\n }, [\n antialias,\n liquid,\n noiseAmount,\n pixelSize,\n patternScale,\n patternDensity,\n enableRipples,\n rippleIntensityScale,\n rippleThickness,\n rippleSpeed,\n pixelSizeJitter,\n edgeFade,\n transparent,\n liquidStrength,\n liquidRadius,\n liquidWobbleSpeed,\n autoPauseOffscreen,\n variant,\n color,\n speed\n ]);\n\n return (\n \n );\n};\n\nexport default PixelBlast;\n" } ], diff --git a/public/r/PixelCard-JS-CSS.json b/public/r/PixelCard-JS-CSS.json index ec6a8880..077650c5 100644 --- a/public/r/PixelCard-JS-CSS.json +++ b/public/r/PixelCard-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "PixelCard/PixelCard.css", + "type": "registry:file", + "path": "PixelCard.css", + "target": "@components/PixelCard.css", "content": ".pixel-canvas {\n width: 100%;\n height: 100%;\n display: block;\n}\n\n.pixel-card {\n height: 400px;\n width: 300px;\n position: relative;\n overflow: hidden;\n display: grid;\n place-items: center;\n aspect-ratio: 4 / 5;\n border: 1px solid #27272a;\n border-radius: 25px;\n isolation: isolate;\n transition: border-color 200ms cubic-bezier(0.5, 1, 0.89, 1);\n user-select: none;\n}\n\n.pixel-card::before {\n content: '';\n position: absolute;\n inset: 0;\n margin: auto;\n aspect-ratio: 1;\n background: radial-gradient(circle, #09090b, transparent 85%);\n opacity: 0;\n transition: opacity 800ms cubic-bezier(0.5, 1, 0.89, 1);\n}\n\n.pixel-card:hover::before,\n.pixel-card:focus-within::before {\n opacity: 1;\n}\n" }, { "type": "registry:component", - "path": "PixelCard/PixelCard.jsx", + "path": "PixelCard.jsx", "content": "import { useEffect, useRef } from 'react';\nimport './PixelCard.css';\n\nclass Pixel {\n constructor(canvas, context, x, y, color, speed, delay) {\n this.width = canvas.width;\n this.height = canvas.height;\n this.ctx = context;\n this.x = x;\n this.y = y;\n this.color = color;\n this.speed = this.getRandomValue(0.1, 0.9) * speed;\n this.size = 0;\n this.sizeStep = Math.random() * 0.4;\n this.minSize = 0.5;\n this.maxSizeInteger = 2;\n this.maxSize = this.getRandomValue(this.minSize, this.maxSizeInteger);\n this.delay = delay;\n this.counter = 0;\n this.counterStep = Math.random() * 4 + (this.width + this.height) * 0.01;\n this.isIdle = false;\n this.isReverse = false;\n this.isShimmer = false;\n }\n\n getRandomValue(min, max) {\n return Math.random() * (max - min) + min;\n }\n\n draw() {\n const centerOffset = this.maxSizeInteger * 0.5 - this.size * 0.5;\n this.ctx.fillStyle = this.color;\n this.ctx.fillRect(this.x + centerOffset, this.y + centerOffset, this.size, this.size);\n }\n\n appear() {\n this.isIdle = false;\n if (this.counter <= this.delay) {\n this.counter += this.counterStep;\n return;\n }\n if (this.size >= this.maxSize) {\n this.isShimmer = true;\n }\n if (this.isShimmer) {\n this.shimmer();\n } else {\n this.size += this.sizeStep;\n }\n this.draw();\n }\n\n disappear() {\n this.isShimmer = false;\n this.counter = 0;\n if (this.size <= 0) {\n this.isIdle = true;\n return;\n } else {\n this.size -= 0.1;\n }\n this.draw();\n }\n\n shimmer() {\n if (this.size >= this.maxSize) {\n this.isReverse = true;\n } else if (this.size <= this.minSize) {\n this.isReverse = false;\n }\n if (this.isReverse) {\n this.size -= this.speed;\n } else {\n this.size += this.speed;\n }\n }\n}\n\nfunction getEffectiveSpeed(value, reducedMotion) {\n const min = 0;\n const max = 100;\n const throttle = 0.001;\n const parsed = parseInt(value, 10);\n\n if (parsed <= min || reducedMotion) {\n return min;\n } else if (parsed >= max) {\n return max * throttle;\n } else {\n return parsed * throttle;\n }\n}\n\nconst VARIANTS = {\n default: {\n activeColor: null,\n gap: 5,\n speed: 35,\n colors: '#f8fafc,#f1f5f9,#cbd5e1',\n noFocus: false\n },\n blue: {\n activeColor: '#e0f2fe',\n gap: 10,\n speed: 25,\n colors: '#e0f2fe,#7dd3fc,#0ea5e9',\n noFocus: false\n },\n yellow: {\n activeColor: '#fef08a',\n gap: 3,\n speed: 20,\n colors: '#fef08a,#fde047,#eab308',\n noFocus: false\n },\n pink: {\n activeColor: '#fecdd3',\n gap: 6,\n speed: 80,\n colors: '#fecdd3,#fda4af,#e11d48',\n noFocus: true\n }\n};\n\nexport default function PixelCard({ variant = 'default', gap, speed, colors, noFocus, className = '', children }) {\n const containerRef = useRef(null);\n const canvasRef = useRef(null);\n const pixelsRef = useRef([]);\n const animationRef = useRef(null);\n const timePreviousRef = useRef(performance.now());\n const reducedMotion = useRef(\n typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\n ).current;\n\n const variantCfg = VARIANTS[variant] || VARIANTS.default;\n const finalGap = gap ?? variantCfg.gap;\n const finalSpeed = speed ?? variantCfg.speed;\n const finalColors = colors ?? variantCfg.colors;\n const finalNoFocus = noFocus ?? variantCfg.noFocus;\n\n const initPixels = () => {\n if (!containerRef.current || !canvasRef.current) return;\n\n const rect = containerRef.current.getBoundingClientRect();\n const width = Math.floor(rect.width);\n const height = Math.floor(rect.height);\n const ctx = canvasRef.current.getContext('2d');\n\n canvasRef.current.width = width;\n canvasRef.current.height = height;\n canvasRef.current.style.width = `${width}px`;\n canvasRef.current.style.height = `${height}px`;\n\n const colorsArray = finalColors.split(',');\n const pxs = [];\n for (let x = 0; x < width; x += parseInt(finalGap, 10)) {\n for (let y = 0; y < height; y += parseInt(finalGap, 10)) {\n const color = colorsArray[Math.floor(Math.random() * colorsArray.length)];\n\n const dx = x - width / 2;\n const dy = y - height / 2;\n const distance = Math.sqrt(dx * dx + dy * dy);\n const delay = reducedMotion ? 0 : distance;\n\n pxs.push(new Pixel(canvasRef.current, ctx, x, y, color, getEffectiveSpeed(finalSpeed, reducedMotion), delay));\n }\n }\n pixelsRef.current = pxs;\n };\n\n const doAnimate = fnName => {\n animationRef.current = requestAnimationFrame(() => doAnimate(fnName));\n const timeNow = performance.now();\n const timePassed = timeNow - timePreviousRef.current;\n const timeInterval = 1000 / 60;\n\n if (timePassed < timeInterval) return;\n timePreviousRef.current = timeNow - (timePassed % timeInterval);\n\n const ctx = canvasRef.current?.getContext('2d');\n if (!ctx || !canvasRef.current) return;\n\n ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);\n\n let allIdle = true;\n for (let i = 0; i < pixelsRef.current.length; i++) {\n const pixel = pixelsRef.current[i];\n pixel[fnName]();\n if (!pixel.isIdle) {\n allIdle = false;\n }\n }\n if (allIdle) {\n cancelAnimationFrame(animationRef.current);\n }\n };\n\n const handleAnimation = name => {\n cancelAnimationFrame(animationRef.current);\n animationRef.current = requestAnimationFrame(() => doAnimate(name));\n };\n\n const onMouseEnter = () => handleAnimation('appear');\n const onMouseLeave = () => handleAnimation('disappear');\n const onFocus = e => {\n if (e.currentTarget.contains(e.relatedTarget)) return;\n handleAnimation('appear');\n };\n const onBlur = e => {\n if (e.currentTarget.contains(e.relatedTarget)) return;\n handleAnimation('disappear');\n };\n\n useEffect(() => {\n initPixels();\n const observer = new ResizeObserver(() => {\n initPixels();\n });\n if (containerRef.current) {\n observer.observe(containerRef.current);\n }\n return () => {\n observer.disconnect();\n cancelAnimationFrame(animationRef.current);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [finalGap, finalSpeed, finalColors, finalNoFocus]);\n\n return (\n \n \n {children}\n
    \n );\n}\n" } ], diff --git a/public/r/PixelCard-TS-CSS.json b/public/r/PixelCard-TS-CSS.json index 90eb7305..72f78a4f 100644 --- a/public/r/PixelCard-TS-CSS.json +++ b/public/r/PixelCard-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "PixelCard/PixelCard.css", + "type": "registry:file", + "path": "PixelCard.css", + "target": "@components/PixelCard.css", "content": ".pixel-canvas {\n width: 100%;\n height: 100%;\n display: block;\n}\n\n.pixel-card {\n height: 400px;\n width: 300px;\n position: relative;\n overflow: hidden;\n display: grid;\n place-items: center;\n aspect-ratio: 4 / 5;\n border: 1px solid #27272a;\n border-radius: 25px;\n isolation: isolate;\n transition: border-color 200ms cubic-bezier(0.5, 1, 0.89, 1);\n user-select: none;\n}\n\n.pixel-card::before {\n content: '';\n position: absolute;\n inset: 0;\n margin: auto;\n aspect-ratio: 1;\n background: radial-gradient(circle, #09090b, transparent 85%);\n opacity: 0;\n transition: opacity 800ms cubic-bezier(0.5, 1, 0.89, 1);\n}\n\n.pixel-card:hover::before,\n.pixel-card:focus-within::before {\n opacity: 1;\n}\n" }, { "type": "registry:component", - "path": "PixelCard/PixelCard.tsx", - "content": "import { useEffect, useRef } from 'react';\nimport { JSX } from 'react';\nimport './PixelCard.css';\n\nclass Pixel {\n width: number;\n height: number;\n ctx: CanvasRenderingContext2D;\n x: number;\n y: number;\n color: string;\n speed: number;\n size: number;\n sizeStep: number;\n minSize: number;\n maxSizeInteger: number;\n maxSize: number;\n delay: number;\n counter: number;\n counterStep: number;\n isIdle: boolean;\n isReverse: boolean;\n isShimmer: boolean;\n\n constructor(\n canvas: HTMLCanvasElement,\n context: CanvasRenderingContext2D,\n x: number,\n y: number,\n color: string,\n speed: number,\n delay: number\n ) {\n this.width = canvas.width;\n this.height = canvas.height;\n this.ctx = context;\n this.x = x;\n this.y = y;\n this.color = color;\n this.speed = this.getRandomValue(0.1, 0.9) * speed;\n this.size = 0;\n this.sizeStep = Math.random() * 0.4;\n this.minSize = 0.5;\n this.maxSizeInteger = 2;\n this.maxSize = this.getRandomValue(this.minSize, this.maxSizeInteger);\n this.delay = delay;\n this.counter = 0;\n this.counterStep = Math.random() * 4 + (this.width + this.height) * 0.01;\n this.isIdle = false;\n this.isReverse = false;\n this.isShimmer = false;\n }\n\n getRandomValue(min: number, max: number) {\n return Math.random() * (max - min) + min;\n }\n\n draw() {\n const centerOffset = this.maxSizeInteger * 0.5 - this.size * 0.5;\n this.ctx.fillStyle = this.color;\n this.ctx.fillRect(this.x + centerOffset, this.y + centerOffset, this.size, this.size);\n }\n\n appear() {\n this.isIdle = false;\n if (this.counter <= this.delay) {\n this.counter += this.counterStep;\n return;\n }\n if (this.size >= this.maxSize) {\n this.isShimmer = true;\n }\n if (this.isShimmer) {\n this.shimmer();\n } else {\n this.size += this.sizeStep;\n }\n this.draw();\n }\n\n disappear() {\n this.isShimmer = false;\n this.counter = 0;\n if (this.size <= 0) {\n this.isIdle = true;\n return;\n } else {\n this.size -= 0.1;\n }\n this.draw();\n }\n\n shimmer() {\n if (this.size >= this.maxSize) {\n this.isReverse = true;\n } else if (this.size <= this.minSize) {\n this.isReverse = false;\n }\n if (this.isReverse) {\n this.size -= this.speed;\n } else {\n this.size += this.speed;\n }\n }\n}\n\nfunction getEffectiveSpeed(value: number, reducedMotion: boolean) {\n const min = 0;\n const max = 100;\n const throttle = 0.001;\n\n if (value <= min || reducedMotion) {\n return min;\n } else if (value >= max) {\n return max * throttle;\n } else {\n return value * throttle;\n }\n}\n\nconst VARIANTS = {\n default: {\n activeColor: null,\n gap: 5,\n speed: 35,\n colors: '#f8fafc,#f1f5f9,#cbd5e1',\n noFocus: false\n },\n blue: {\n activeColor: '#e0f2fe',\n gap: 10,\n speed: 25,\n colors: '#e0f2fe,#7dd3fc,#0ea5e9',\n noFocus: false\n },\n yellow: {\n activeColor: '#fef08a',\n gap: 3,\n speed: 20,\n colors: '#fef08a,#fde047,#eab308',\n noFocus: false\n },\n pink: {\n activeColor: '#fecdd3',\n gap: 6,\n speed: 80,\n colors: '#fecdd3,#fda4af,#e11d48',\n noFocus: true\n }\n};\n\ninterface PixelCardProps {\n variant?: 'default' | 'blue' | 'yellow' | 'pink';\n gap?: number;\n speed?: number;\n colors?: string;\n noFocus?: boolean;\n className?: string;\n children: React.ReactNode;\n}\n\ninterface VariantConfig {\n activeColor: string | null;\n gap: number;\n speed: number;\n colors: string;\n noFocus: boolean;\n}\n\nexport default function PixelCard({\n variant = 'default',\n gap,\n speed,\n colors,\n noFocus,\n className = '',\n children\n}: PixelCardProps): JSX.Element {\n const containerRef = useRef(null);\n const canvasRef = useRef(null);\n const pixelsRef = useRef([]);\n const animationRef = useRef | null>(null);\n const timePreviousRef = useRef(performance.now());\n const reducedMotion = useRef(\n typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\n ).current;\n\n const variantCfg: VariantConfig = VARIANTS[variant] || VARIANTS.default;\n const finalGap = gap ?? variantCfg.gap;\n const finalSpeed = speed ?? variantCfg.speed;\n const finalColors = colors ?? variantCfg.colors;\n const finalNoFocus = noFocus ?? variantCfg.noFocus;\n\n const initPixels = () => {\n if (!containerRef.current || !canvasRef.current) return;\n\n const rect = containerRef.current.getBoundingClientRect();\n const width = Math.floor(rect.width);\n const height = Math.floor(rect.height);\n const ctx = canvasRef.current.getContext('2d');\n\n canvasRef.current.width = width;\n canvasRef.current.height = height;\n canvasRef.current.style.width = `${width}px`;\n canvasRef.current.style.height = `${height}px`;\n\n const colorsArray = finalColors.split(',');\n const pxs = [];\n for (let x = 0; x < width; x += parseInt(finalGap.toString(), 10)) {\n for (let y = 0; y < height; y += parseInt(finalGap.toString(), 10)) {\n const color = colorsArray[Math.floor(Math.random() * colorsArray.length)];\n\n const dx = x - width / 2;\n const dy = y - height / 2;\n const distance = Math.sqrt(dx * dx + dy * dy);\n const delay = reducedMotion ? 0 : distance;\n if (!ctx) return;\n pxs.push(new Pixel(canvasRef.current, ctx, x, y, color, getEffectiveSpeed(finalSpeed, reducedMotion), delay));\n }\n }\n pixelsRef.current = pxs;\n };\n\n const doAnimate = (fnName: keyof Pixel) => {\n animationRef.current = requestAnimationFrame(() => doAnimate(fnName));\n const timeNow = performance.now();\n const timePassed = timeNow - timePreviousRef.current;\n const timeInterval = 1000 / 60;\n\n if (timePassed < timeInterval) return;\n timePreviousRef.current = timeNow - (timePassed % timeInterval);\n\n const ctx = canvasRef.current?.getContext('2d');\n if (!ctx || !canvasRef.current) return;\n\n ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);\n\n let allIdle = true;\n for (let i = 0; i < pixelsRef.current.length; i++) {\n const pixel = pixelsRef.current[i];\n // @ts-ignore\n pixel[fnName]();\n if (!pixel.isIdle) {\n allIdle = false;\n }\n }\n if (allIdle) {\n cancelAnimationFrame(animationRef.current);\n }\n };\n\n const handleAnimation = (name: keyof Pixel) => {\n if (animationRef.current !== null) {\n cancelAnimationFrame(animationRef.current);\n }\n animationRef.current = requestAnimationFrame(() => doAnimate(name));\n };\n\n const onMouseEnter = () => handleAnimation('appear');\n const onMouseLeave = () => handleAnimation('disappear');\n const onFocus: React.FocusEventHandler = e => {\n if (e.currentTarget.contains(e.relatedTarget)) return;\n handleAnimation('appear');\n };\n const onBlur: React.FocusEventHandler = e => {\n if (e.currentTarget.contains(e.relatedTarget)) return;\n handleAnimation('disappear');\n };\n\n useEffect(() => {\n initPixels();\n const observer = new ResizeObserver(() => {\n initPixels();\n });\n if (containerRef.current) {\n observer.observe(containerRef.current);\n }\n return () => {\n observer.disconnect();\n if (animationRef.current !== null) {\n cancelAnimationFrame(animationRef.current);\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [finalGap, finalSpeed, finalColors, finalNoFocus]);\n\n return (\n \n \n {children}\n
    \n );\n}\n" + "path": "PixelCard.tsx", + "content": "import { useEffect, useRef } from 'react';\nimport { type JSX } from 'react';\nimport './PixelCard.css';\n\nclass Pixel {\n width: number;\n height: number;\n ctx: CanvasRenderingContext2D;\n x: number;\n y: number;\n color: string;\n speed: number;\n size: number;\n sizeStep: number;\n minSize: number;\n maxSizeInteger: number;\n maxSize: number;\n delay: number;\n counter: number;\n counterStep: number;\n isIdle: boolean;\n isReverse: boolean;\n isShimmer: boolean;\n\n constructor(\n canvas: HTMLCanvasElement,\n context: CanvasRenderingContext2D,\n x: number,\n y: number,\n color: string,\n speed: number,\n delay: number\n ) {\n this.width = canvas.width;\n this.height = canvas.height;\n this.ctx = context;\n this.x = x;\n this.y = y;\n this.color = color;\n this.speed = this.getRandomValue(0.1, 0.9) * speed;\n this.size = 0;\n this.sizeStep = Math.random() * 0.4;\n this.minSize = 0.5;\n this.maxSizeInteger = 2;\n this.maxSize = this.getRandomValue(this.minSize, this.maxSizeInteger);\n this.delay = delay;\n this.counter = 0;\n this.counterStep = Math.random() * 4 + (this.width + this.height) * 0.01;\n this.isIdle = false;\n this.isReverse = false;\n this.isShimmer = false;\n }\n\n getRandomValue(min: number, max: number) {\n return Math.random() * (max - min) + min;\n }\n\n draw() {\n const centerOffset = this.maxSizeInteger * 0.5 - this.size * 0.5;\n this.ctx.fillStyle = this.color;\n this.ctx.fillRect(this.x + centerOffset, this.y + centerOffset, this.size, this.size);\n }\n\n appear() {\n this.isIdle = false;\n if (this.counter <= this.delay) {\n this.counter += this.counterStep;\n return;\n }\n if (this.size >= this.maxSize) {\n this.isShimmer = true;\n }\n if (this.isShimmer) {\n this.shimmer();\n } else {\n this.size += this.sizeStep;\n }\n this.draw();\n }\n\n disappear() {\n this.isShimmer = false;\n this.counter = 0;\n if (this.size <= 0) {\n this.isIdle = true;\n return;\n } else {\n this.size -= 0.1;\n }\n this.draw();\n }\n\n shimmer() {\n if (this.size >= this.maxSize) {\n this.isReverse = true;\n } else if (this.size <= this.minSize) {\n this.isReverse = false;\n }\n if (this.isReverse) {\n this.size -= this.speed;\n } else {\n this.size += this.speed;\n }\n }\n}\n\nfunction getEffectiveSpeed(value: number, reducedMotion: boolean) {\n const min = 0;\n const max = 100;\n const throttle = 0.001;\n\n if (value <= min || reducedMotion) {\n return min;\n } else if (value >= max) {\n return max * throttle;\n } else {\n return value * throttle;\n }\n}\n\nconst VARIANTS = {\n default: {\n activeColor: null,\n gap: 5,\n speed: 35,\n colors: '#f8fafc,#f1f5f9,#cbd5e1',\n noFocus: false\n },\n blue: {\n activeColor: '#e0f2fe',\n gap: 10,\n speed: 25,\n colors: '#e0f2fe,#7dd3fc,#0ea5e9',\n noFocus: false\n },\n yellow: {\n activeColor: '#fef08a',\n gap: 3,\n speed: 20,\n colors: '#fef08a,#fde047,#eab308',\n noFocus: false\n },\n pink: {\n activeColor: '#fecdd3',\n gap: 6,\n speed: 80,\n colors: '#fecdd3,#fda4af,#e11d48',\n noFocus: true\n }\n};\n\ninterface PixelCardProps {\n variant?: 'default' | 'blue' | 'yellow' | 'pink';\n gap?: number;\n speed?: number;\n colors?: string;\n noFocus?: boolean;\n className?: string;\n children: React.ReactNode;\n}\n\ninterface VariantConfig {\n activeColor: string | null;\n gap: number;\n speed: number;\n colors: string;\n noFocus: boolean;\n}\n\nexport default function PixelCard({\n variant = 'default',\n gap,\n speed,\n colors,\n noFocus,\n className = '',\n children\n}: PixelCardProps): JSX.Element {\n const containerRef = useRef(null);\n const canvasRef = useRef(null);\n const pixelsRef = useRef([]);\n const animationRef = useRef | null>(null);\n const timePreviousRef = useRef(performance.now());\n const reducedMotion = useRef(\n typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\n ).current;\n\n const variantCfg: VariantConfig = VARIANTS[variant] || VARIANTS.default;\n const finalGap = gap ?? variantCfg.gap;\n const finalSpeed = speed ?? variantCfg.speed;\n const finalColors = colors ?? variantCfg.colors;\n const finalNoFocus = noFocus ?? variantCfg.noFocus;\n\n const initPixels = () => {\n if (!containerRef.current || !canvasRef.current) return;\n\n const rect = containerRef.current.getBoundingClientRect();\n const width = Math.floor(rect.width);\n const height = Math.floor(rect.height);\n const ctx = canvasRef.current.getContext('2d');\n\n canvasRef.current.width = width;\n canvasRef.current.height = height;\n canvasRef.current.style.width = `${width}px`;\n canvasRef.current.style.height = `${height}px`;\n\n const colorsArray = finalColors.split(',');\n const pxs = [];\n for (let x = 0; x < width; x += parseInt(finalGap.toString(), 10)) {\n for (let y = 0; y < height; y += parseInt(finalGap.toString(), 10)) {\n const color = colorsArray[Math.floor(Math.random() * colorsArray.length)];\n\n const dx = x - width / 2;\n const dy = y - height / 2;\n const distance = Math.sqrt(dx * dx + dy * dy);\n const delay = reducedMotion ? 0 : distance;\n if (!ctx) return;\n pxs.push(new Pixel(canvasRef.current, ctx, x, y, color, getEffectiveSpeed(finalSpeed, reducedMotion), delay));\n }\n }\n pixelsRef.current = pxs;\n };\n\n const doAnimate = (fnName: keyof Pixel) => {\n animationRef.current = requestAnimationFrame(() => doAnimate(fnName));\n const timeNow = performance.now();\n const timePassed = timeNow - timePreviousRef.current;\n const timeInterval = 1000 / 60;\n\n if (timePassed < timeInterval) return;\n timePreviousRef.current = timeNow - (timePassed % timeInterval);\n\n const ctx = canvasRef.current?.getContext('2d');\n if (!ctx || !canvasRef.current) return;\n\n ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);\n\n let allIdle = true;\n for (let i = 0; i < pixelsRef.current.length; i++) {\n const pixel = pixelsRef.current[i];\n // @ts-ignore\n pixel[fnName]();\n if (!pixel.isIdle) {\n allIdle = false;\n }\n }\n if (allIdle) {\n cancelAnimationFrame(animationRef.current);\n }\n };\n\n const handleAnimation = (name: keyof Pixel) => {\n if (animationRef.current !== null) {\n cancelAnimationFrame(animationRef.current);\n }\n animationRef.current = requestAnimationFrame(() => doAnimate(name));\n };\n\n const onMouseEnter = () => handleAnimation('appear');\n const onMouseLeave = () => handleAnimation('disappear');\n const onFocus: React.FocusEventHandler = e => {\n if (e.currentTarget.contains(e.relatedTarget)) return;\n handleAnimation('appear');\n };\n const onBlur: React.FocusEventHandler = e => {\n if (e.currentTarget.contains(e.relatedTarget)) return;\n handleAnimation('disappear');\n };\n\n useEffect(() => {\n initPixels();\n const observer = new ResizeObserver(() => {\n initPixels();\n });\n if (containerRef.current) {\n observer.observe(containerRef.current);\n }\n return () => {\n observer.disconnect();\n if (animationRef.current !== null) {\n cancelAnimationFrame(animationRef.current);\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [finalGap, finalSpeed, finalColors, finalNoFocus]);\n\n return (\n \n \n {children}\n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/PixelCard-TS-TW.json b/public/r/PixelCard-TS-TW.json index af49d262..9899915a 100644 --- a/public/r/PixelCard-TS-TW.json +++ b/public/r/PixelCard-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "PixelCard/PixelCard.tsx", - "content": "import { useEffect, useRef } from 'react';\nimport { JSX } from 'react';\n\nclass Pixel {\n width: number;\n height: number;\n ctx: CanvasRenderingContext2D;\n x: number;\n y: number;\n color: string;\n speed: number;\n size: number;\n sizeStep: number;\n minSize: number;\n maxSizeInteger: number;\n maxSize: number;\n delay: number;\n counter: number;\n counterStep: number;\n isIdle: boolean;\n isReverse: boolean;\n isShimmer: boolean;\n\n constructor(\n canvas: HTMLCanvasElement,\n context: CanvasRenderingContext2D,\n x: number,\n y: number,\n color: string,\n speed: number,\n delay: number\n ) {\n this.width = canvas.width;\n this.height = canvas.height;\n this.ctx = context;\n this.x = x;\n this.y = y;\n this.color = color;\n this.speed = this.getRandomValue(0.1, 0.9) * speed;\n this.size = 0;\n this.sizeStep = Math.random() * 0.4;\n this.minSize = 0.5;\n this.maxSizeInteger = 2;\n this.maxSize = this.getRandomValue(this.minSize, this.maxSizeInteger);\n this.delay = delay;\n this.counter = 0;\n this.counterStep = Math.random() * 4 + (this.width + this.height) * 0.01;\n this.isIdle = false;\n this.isReverse = false;\n this.isShimmer = false;\n }\n\n getRandomValue(min: number, max: number) {\n return Math.random() * (max - min) + min;\n }\n\n draw() {\n const centerOffset = this.maxSizeInteger * 0.5 - this.size * 0.5;\n this.ctx.fillStyle = this.color;\n this.ctx.fillRect(this.x + centerOffset, this.y + centerOffset, this.size, this.size);\n }\n\n appear() {\n this.isIdle = false;\n if (this.counter <= this.delay) {\n this.counter += this.counterStep;\n return;\n }\n if (this.size >= this.maxSize) {\n this.isShimmer = true;\n }\n if (this.isShimmer) {\n this.shimmer();\n } else {\n this.size += this.sizeStep;\n }\n this.draw();\n }\n\n disappear() {\n this.isShimmer = false;\n this.counter = 0;\n if (this.size <= 0) {\n this.isIdle = true;\n return;\n } else {\n this.size -= 0.1;\n }\n this.draw();\n }\n\n shimmer() {\n if (this.size >= this.maxSize) {\n this.isReverse = true;\n } else if (this.size <= this.minSize) {\n this.isReverse = false;\n }\n if (this.isReverse) {\n this.size -= this.speed;\n } else {\n this.size += this.speed;\n }\n }\n}\n\nfunction getEffectiveSpeed(value: number, reducedMotion: boolean) {\n const min = 0;\n const max = 100;\n const throttle = 0.001;\n\n if (value <= min || reducedMotion) {\n return min;\n } else if (value >= max) {\n return max * throttle;\n } else {\n return value * throttle;\n }\n}\n\nconst VARIANTS = {\n default: {\n activeColor: null,\n gap: 5,\n speed: 35,\n colors: '#f8fafc,#f1f5f9,#cbd5e1',\n noFocus: false\n },\n blue: {\n activeColor: '#e0f2fe',\n gap: 10,\n speed: 25,\n colors: '#e0f2fe,#7dd3fc,#0ea5e9',\n noFocus: false\n },\n yellow: {\n activeColor: '#fef08a',\n gap: 3,\n speed: 20,\n colors: '#fef08a,#fde047,#eab308',\n noFocus: false\n },\n pink: {\n activeColor: '#fecdd3',\n gap: 6,\n speed: 80,\n colors: '#fecdd3,#fda4af,#e11d48',\n noFocus: true\n }\n};\n\ninterface PixelCardProps {\n variant?: 'default' | 'blue' | 'yellow' | 'pink';\n gap?: number;\n speed?: number;\n colors?: string;\n noFocus?: boolean;\n className?: string;\n children: React.ReactNode;\n}\n\ninterface VariantConfig {\n activeColor: string | null;\n gap: number;\n speed: number;\n colors: string;\n noFocus: boolean;\n}\n\nexport default function PixelCard({\n variant = 'default',\n gap,\n speed,\n colors,\n noFocus,\n className = '',\n children\n}: PixelCardProps): JSX.Element {\n const containerRef = useRef(null);\n const canvasRef = useRef(null);\n const pixelsRef = useRef([]);\n const animationRef = useRef | null>(null);\n const timePreviousRef = useRef(performance.now());\n const reducedMotion = useRef(\n typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\n ).current;\n\n const variantCfg: VariantConfig = VARIANTS[variant] || VARIANTS.default;\n const finalGap = gap ?? variantCfg.gap;\n const finalSpeed = speed ?? variantCfg.speed;\n const finalColors = colors ?? variantCfg.colors;\n const finalNoFocus = noFocus ?? variantCfg.noFocus;\n\n const initPixels = () => {\n if (!containerRef.current || !canvasRef.current) return;\n\n const rect = containerRef.current.getBoundingClientRect();\n const width = Math.floor(rect.width);\n const height = Math.floor(rect.height);\n const ctx = canvasRef.current.getContext('2d');\n\n canvasRef.current.width = width;\n canvasRef.current.height = height;\n canvasRef.current.style.width = `${width}px`;\n canvasRef.current.style.height = `${height}px`;\n\n const colorsArray = finalColors.split(',');\n const pxs = [];\n for (let x = 0; x < width; x += parseInt(finalGap.toString(), 10)) {\n for (let y = 0; y < height; y += parseInt(finalGap.toString(), 10)) {\n const color = colorsArray[Math.floor(Math.random() * colorsArray.length)];\n\n const dx = x - width / 2;\n const dy = y - height / 2;\n const distance = Math.sqrt(dx * dx + dy * dy);\n const delay = reducedMotion ? 0 : distance;\n if (!ctx) return;\n pxs.push(new Pixel(canvasRef.current, ctx, x, y, color, getEffectiveSpeed(finalSpeed, reducedMotion), delay));\n }\n }\n pixelsRef.current = pxs;\n };\n\n const doAnimate = (fnName: keyof Pixel) => {\n animationRef.current = requestAnimationFrame(() => doAnimate(fnName));\n const timeNow = performance.now();\n const timePassed = timeNow - timePreviousRef.current;\n const timeInterval = 1000 / 60;\n\n if (timePassed < timeInterval) return;\n timePreviousRef.current = timeNow - (timePassed % timeInterval);\n\n const ctx = canvasRef.current?.getContext('2d');\n if (!ctx || !canvasRef.current) return;\n\n ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);\n\n let allIdle = true;\n for (let i = 0; i < pixelsRef.current.length; i++) {\n const pixel = pixelsRef.current[i];\n // @ts-ignore\n pixel[fnName]();\n if (!pixel.isIdle) {\n allIdle = false;\n }\n }\n if (allIdle) {\n cancelAnimationFrame(animationRef.current);\n }\n };\n\n const handleAnimation = (name: keyof Pixel) => {\n if (animationRef.current !== null) {\n cancelAnimationFrame(animationRef.current);\n }\n animationRef.current = requestAnimationFrame(() => doAnimate(name));\n };\n\n const onMouseEnter = () => handleAnimation('appear');\n const onMouseLeave = () => handleAnimation('disappear');\n const onFocus: React.FocusEventHandler = e => {\n if (e.currentTarget.contains(e.relatedTarget)) return;\n handleAnimation('appear');\n };\n const onBlur: React.FocusEventHandler = e => {\n if (e.currentTarget.contains(e.relatedTarget)) return;\n handleAnimation('disappear');\n };\n\n useEffect(() => {\n initPixels();\n const observer = new ResizeObserver(() => {\n initPixels();\n });\n if (containerRef.current) {\n observer.observe(containerRef.current);\n }\n return () => {\n observer.disconnect();\n if (animationRef.current !== null) {\n cancelAnimationFrame(animationRef.current);\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [finalGap, finalSpeed, finalColors, finalNoFocus]);\n\n return (\n \n \n {children}\n \n );\n}\n" + "content": "import { useEffect, useRef } from 'react';\nimport { type JSX } from 'react';\n\nclass Pixel {\n width: number;\n height: number;\n ctx: CanvasRenderingContext2D;\n x: number;\n y: number;\n color: string;\n speed: number;\n size: number;\n sizeStep: number;\n minSize: number;\n maxSizeInteger: number;\n maxSize: number;\n delay: number;\n counter: number;\n counterStep: number;\n isIdle: boolean;\n isReverse: boolean;\n isShimmer: boolean;\n\n constructor(\n canvas: HTMLCanvasElement,\n context: CanvasRenderingContext2D,\n x: number,\n y: number,\n color: string,\n speed: number,\n delay: number\n ) {\n this.width = canvas.width;\n this.height = canvas.height;\n this.ctx = context;\n this.x = x;\n this.y = y;\n this.color = color;\n this.speed = this.getRandomValue(0.1, 0.9) * speed;\n this.size = 0;\n this.sizeStep = Math.random() * 0.4;\n this.minSize = 0.5;\n this.maxSizeInteger = 2;\n this.maxSize = this.getRandomValue(this.minSize, this.maxSizeInteger);\n this.delay = delay;\n this.counter = 0;\n this.counterStep = Math.random() * 4 + (this.width + this.height) * 0.01;\n this.isIdle = false;\n this.isReverse = false;\n this.isShimmer = false;\n }\n\n getRandomValue(min: number, max: number) {\n return Math.random() * (max - min) + min;\n }\n\n draw() {\n const centerOffset = this.maxSizeInteger * 0.5 - this.size * 0.5;\n this.ctx.fillStyle = this.color;\n this.ctx.fillRect(this.x + centerOffset, this.y + centerOffset, this.size, this.size);\n }\n\n appear() {\n this.isIdle = false;\n if (this.counter <= this.delay) {\n this.counter += this.counterStep;\n return;\n }\n if (this.size >= this.maxSize) {\n this.isShimmer = true;\n }\n if (this.isShimmer) {\n this.shimmer();\n } else {\n this.size += this.sizeStep;\n }\n this.draw();\n }\n\n disappear() {\n this.isShimmer = false;\n this.counter = 0;\n if (this.size <= 0) {\n this.isIdle = true;\n return;\n } else {\n this.size -= 0.1;\n }\n this.draw();\n }\n\n shimmer() {\n if (this.size >= this.maxSize) {\n this.isReverse = true;\n } else if (this.size <= this.minSize) {\n this.isReverse = false;\n }\n if (this.isReverse) {\n this.size -= this.speed;\n } else {\n this.size += this.speed;\n }\n }\n}\n\nfunction getEffectiveSpeed(value: number, reducedMotion: boolean) {\n const min = 0;\n const max = 100;\n const throttle = 0.001;\n\n if (value <= min || reducedMotion) {\n return min;\n } else if (value >= max) {\n return max * throttle;\n } else {\n return value * throttle;\n }\n}\n\nconst VARIANTS = {\n default: {\n activeColor: null,\n gap: 5,\n speed: 35,\n colors: '#f8fafc,#f1f5f9,#cbd5e1',\n noFocus: false\n },\n blue: {\n activeColor: '#e0f2fe',\n gap: 10,\n speed: 25,\n colors: '#e0f2fe,#7dd3fc,#0ea5e9',\n noFocus: false\n },\n yellow: {\n activeColor: '#fef08a',\n gap: 3,\n speed: 20,\n colors: '#fef08a,#fde047,#eab308',\n noFocus: false\n },\n pink: {\n activeColor: '#fecdd3',\n gap: 6,\n speed: 80,\n colors: '#fecdd3,#fda4af,#e11d48',\n noFocus: true\n }\n};\n\ninterface PixelCardProps {\n variant?: 'default' | 'blue' | 'yellow' | 'pink';\n gap?: number;\n speed?: number;\n colors?: string;\n noFocus?: boolean;\n className?: string;\n children: React.ReactNode;\n}\n\ninterface VariantConfig {\n activeColor: string | null;\n gap: number;\n speed: number;\n colors: string;\n noFocus: boolean;\n}\n\nexport default function PixelCard({\n variant = 'default',\n gap,\n speed,\n colors,\n noFocus,\n className = '',\n children\n}: PixelCardProps): JSX.Element {\n const containerRef = useRef(null);\n const canvasRef = useRef(null);\n const pixelsRef = useRef([]);\n const animationRef = useRef | null>(null);\n const timePreviousRef = useRef(performance.now());\n const reducedMotion = useRef(\n typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\n ).current;\n\n const variantCfg: VariantConfig = VARIANTS[variant] || VARIANTS.default;\n const finalGap = gap ?? variantCfg.gap;\n const finalSpeed = speed ?? variantCfg.speed;\n const finalColors = colors ?? variantCfg.colors;\n const finalNoFocus = noFocus ?? variantCfg.noFocus;\n\n const initPixels = () => {\n if (!containerRef.current || !canvasRef.current) return;\n\n const rect = containerRef.current.getBoundingClientRect();\n const width = Math.floor(rect.width);\n const height = Math.floor(rect.height);\n const ctx = canvasRef.current.getContext('2d');\n\n canvasRef.current.width = width;\n canvasRef.current.height = height;\n canvasRef.current.style.width = `${width}px`;\n canvasRef.current.style.height = `${height}px`;\n\n const colorsArray = finalColors.split(',');\n const pxs = [];\n for (let x = 0; x < width; x += parseInt(finalGap.toString(), 10)) {\n for (let y = 0; y < height; y += parseInt(finalGap.toString(), 10)) {\n const color = colorsArray[Math.floor(Math.random() * colorsArray.length)];\n\n const dx = x - width / 2;\n const dy = y - height / 2;\n const distance = Math.sqrt(dx * dx + dy * dy);\n const delay = reducedMotion ? 0 : distance;\n if (!ctx) return;\n pxs.push(new Pixel(canvasRef.current, ctx, x, y, color, getEffectiveSpeed(finalSpeed, reducedMotion), delay));\n }\n }\n pixelsRef.current = pxs;\n };\n\n const doAnimate = (fnName: keyof Pixel) => {\n animationRef.current = requestAnimationFrame(() => doAnimate(fnName));\n const timeNow = performance.now();\n const timePassed = timeNow - timePreviousRef.current;\n const timeInterval = 1000 / 60;\n\n if (timePassed < timeInterval) return;\n timePreviousRef.current = timeNow - (timePassed % timeInterval);\n\n const ctx = canvasRef.current?.getContext('2d');\n if (!ctx || !canvasRef.current) return;\n\n ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);\n\n let allIdle = true;\n for (let i = 0; i < pixelsRef.current.length; i++) {\n const pixel = pixelsRef.current[i];\n // @ts-ignore\n pixel[fnName]();\n if (!pixel.isIdle) {\n allIdle = false;\n }\n }\n if (allIdle) {\n cancelAnimationFrame(animationRef.current);\n }\n };\n\n const handleAnimation = (name: keyof Pixel) => {\n if (animationRef.current !== null) {\n cancelAnimationFrame(animationRef.current);\n }\n animationRef.current = requestAnimationFrame(() => doAnimate(name));\n };\n\n const onMouseEnter = () => handleAnimation('appear');\n const onMouseLeave = () => handleAnimation('disappear');\n const onFocus: React.FocusEventHandler = e => {\n if (e.currentTarget.contains(e.relatedTarget)) return;\n handleAnimation('appear');\n };\n const onBlur: React.FocusEventHandler = e => {\n if (e.currentTarget.contains(e.relatedTarget)) return;\n handleAnimation('disappear');\n };\n\n useEffect(() => {\n initPixels();\n const observer = new ResizeObserver(() => {\n initPixels();\n });\n if (containerRef.current) {\n observer.observe(containerRef.current);\n }\n return () => {\n observer.disconnect();\n if (animationRef.current !== null) {\n cancelAnimationFrame(animationRef.current);\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [finalGap, finalSpeed, finalColors, finalNoFocus]);\n\n return (\n \n \n {children}\n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/PixelSnow-JS-CSS.json b/public/r/PixelSnow-JS-CSS.json index ac0f5bcc..2d732d1f 100644 --- a/public/r/PixelSnow-JS-CSS.json +++ b/public/r/PixelSnow-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "PixelSnow/PixelSnow.css", + "type": "registry:file", + "path": "PixelSnow.css", + "target": "@components/PixelSnow.css", "content": ".pixel-snow-container {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n contain: layout style paint;\n}\n\n.pixel-snow-container canvas {\n display: block;\n width: 100%;\n height: 100%;\n transform: translateZ(0);\n will-change: transform;\n backface-visibility: hidden;\n}\n" }, { "type": "registry:component", - "path": "PixelSnow/PixelSnow.jsx", + "path": "PixelSnow.jsx", "content": "import { useCallback, useEffect, useMemo, useRef } from 'react';\nimport {\n Color,\n Mesh,\n OrthographicCamera,\n PlaneGeometry,\n Scene,\n ShaderMaterial,\n Vector2,\n Vector3,\n WebGLRenderer\n} from 'three';\n\nimport './PixelSnow.css';\n\nconst vertexShader = `\nvoid main() {\n gl_Position = vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision mediump float;\n\nuniform float uTime;\nuniform vec2 uResolution;\nuniform float uFlakeSize;\nuniform float uMinFlakeSize;\nuniform float uPixelResolution;\nuniform float uSpeed;\nuniform float uDepthFade;\nuniform float uFarPlane;\nuniform vec3 uColor;\nuniform float uBrightness;\nuniform float uGamma;\nuniform float uDensity;\nuniform float uVariant;\nuniform float uDirection;\n\n// Precomputed constants\n#define PI 3.14159265\n#define PI_OVER_6 0.5235988\n#define PI_OVER_3 1.0471976\n#define INV_SQRT3 0.57735027\n#define M1 1597334677U\n#define M2 3812015801U\n#define M3 3299493293U\n#define F0 2.3283064e-10\n\n// Optimized hash - inline multiplication\n#define hash(n) (n * (n ^ (n >> 15)))\n#define coord3(p) (uvec3(p).x * M1 ^ uvec3(p).y * M2 ^ uvec3(p).z * M3)\n\n// Precomputed camera basis vectors (normalized vec3(1,1,1), vec3(1,0,-1))\nconst vec3 camK = vec3(0.57735027, 0.57735027, 0.57735027);\nconst vec3 camI = vec3(0.70710678, 0.0, -0.70710678);\nconst vec3 camJ = vec3(-0.40824829, 0.81649658, -0.40824829);\n\n// Precomputed branch direction\nconst vec2 b1d = vec2(0.574, 0.819);\n\nvec3 hash3(uint n) {\n uvec3 hashed = hash(n) * uvec3(1U, 511U, 262143U);\n return vec3(hashed) * F0;\n}\n\nfloat snowflakeDist(vec2 p) {\n float r = length(p);\n float a = atan(p.y, p.x);\n a = abs(mod(a + PI_OVER_6, PI_OVER_3) - PI_OVER_6);\n vec2 q = r * vec2(cos(a), sin(a));\n float dMain = max(abs(q.y), max(-q.x, q.x - 1.0));\n float b1t = clamp(dot(q - vec2(0.4, 0.0), b1d), 0.0, 0.4);\n float dB1 = length(q - vec2(0.4, 0.0) - b1t * b1d);\n float b2t = clamp(dot(q - vec2(0.7, 0.0), b1d), 0.0, 0.25);\n float dB2 = length(q - vec2(0.7, 0.0) - b2t * b1d);\n return min(dMain, min(dB1, dB2)) * 10.0;\n}\n\nvoid main() {\n // Precompute reciprocals to avoid division\n float invPixelRes = 1.0 / uPixelResolution;\n float pixelSize = max(1.0, floor(0.5 + uResolution.x * invPixelRes));\n float invPixelSize = 1.0 / pixelSize;\n \n vec2 fragCoord = floor(gl_FragCoord.xy * invPixelSize);\n vec2 res = uResolution * invPixelSize;\n float invResX = 1.0 / res.x;\n\n vec3 ray = normalize(vec3((fragCoord - res * 0.5) * invResX, 1.0));\n ray = ray.x * camI + ray.y * camJ + ray.z * camK;\n\n // Precompute time-based values\n float timeSpeed = uTime * uSpeed;\n float windX = cos(uDirection) * 0.4;\n float windY = sin(uDirection) * 0.4;\n vec3 camPos = (windX * camI + windY * camJ + 0.1 * camK) * timeSpeed;\n vec3 pos = camPos;\n\n // Precompute ray reciprocal for strides\n vec3 absRay = max(abs(ray), vec3(0.001));\n vec3 strides = 1.0 / absRay;\n vec3 raySign = step(ray, vec3(0.0));\n vec3 phase = fract(pos) * strides;\n phase = mix(strides - phase, phase, raySign);\n\n // Precompute for intersection test\n float rayDotCamK = dot(ray, camK);\n float invRayDotCamK = 1.0 / rayDotCamK;\n float invDepthFade = 1.0 / uDepthFade;\n float halfInvResX = 0.5 * invResX;\n vec3 timeAnim = timeSpeed * 0.1 * vec3(7.0, 8.0, 5.0);\n\n float t = 0.0;\n for (int i = 0; i < 128; i++) {\n if (t >= uFarPlane) break;\n \n vec3 fpos = floor(pos);\n uint cellCoord = coord3(fpos);\n float cellHash = hash3(cellCoord).x;\n\n if (cellHash < uDensity) {\n vec3 h = hash3(cellCoord);\n \n // Optimized flake position calculation\n vec3 sinArg1 = fpos.yzx * 0.073;\n vec3 sinArg2 = fpos.zxy * 0.27;\n vec3 flakePos = 0.5 - 0.5 * cos(4.0 * sin(sinArg1) + 4.0 * sin(sinArg2) + 2.0 * h + timeAnim);\n flakePos = flakePos * 0.8 + 0.1 + fpos;\n\n float toIntersection = dot(flakePos - pos, camK) * invRayDotCamK;\n \n if (toIntersection > 0.0) {\n vec3 testPos = pos + ray * toIntersection - flakePos;\n float testX = dot(testPos, camI);\n float testY = dot(testPos, camJ);\n vec2 testUV = abs(vec2(testX, testY));\n \n float depth = dot(flakePos - camPos, camK);\n float flakeSize = max(uFlakeSize, uMinFlakeSize * depth * halfInvResX);\n \n // Avoid branching with step functions where possible\n float dist;\n if (uVariant < 0.5) {\n dist = max(testUV.x, testUV.y);\n } else if (uVariant < 1.5) {\n dist = length(testUV);\n } else {\n float invFlakeSize = 1.0 / flakeSize;\n dist = snowflakeDist(vec2(testX, testY) * invFlakeSize) * flakeSize;\n }\n\n if (dist < flakeSize) {\n float flakeSizeRatio = uFlakeSize / flakeSize;\n float intensity = exp2(-(t + toIntersection) * invDepthFade) *\n min(1.0, flakeSizeRatio * flakeSizeRatio) * uBrightness;\n gl_FragColor = vec4(uColor * pow(vec3(intensity), vec3(uGamma)), 1.0);\n return;\n }\n }\n }\n\n float nextStep = min(min(phase.x, phase.y), phase.z);\n vec3 sel = step(phase, vec3(nextStep));\n phase = phase - nextStep + strides * sel;\n t += nextStep;\n pos = mix(pos + ray * nextStep, floor(pos + ray * nextStep + 0.5), sel);\n }\n\n gl_FragColor = vec4(0.0);\n}\n`;\n\nexport default function PixelSnow({\n color = '#ffffff',\n flakeSize = 0.01,\n minFlakeSize = 1.25,\n pixelResolution = 200,\n speed = 1.25,\n depthFade = 8,\n farPlane = 20,\n brightness = 1,\n gamma = 0.4545,\n density = 0.3,\n variant = 'square',\n direction = 125,\n className = '',\n style = {}\n}) {\n const containerRef = useRef(null);\n const animationRef = useRef(0);\n const isVisibleRef = useRef(true);\n const rendererRef = useRef(null);\n const materialRef = useRef(null);\n const resizeTimeoutRef = useRef(null);\n\n // Memoize shader variant value\n const variantValue = useMemo(() => {\n return variant === 'round' ? 1.0 : variant === 'snowflake' ? 2.0 : 0.0;\n }, [variant]);\n\n // Memoize color conversion\n const colorVector = useMemo(() => {\n const threeColor = new Color(color);\n return new Vector3(threeColor.r, threeColor.g, threeColor.b);\n }, [color]);\n\n // Debounced resize handler\n const handleResize = useCallback(() => {\n if (resizeTimeoutRef.current) {\n clearTimeout(resizeTimeoutRef.current);\n }\n resizeTimeoutRef.current = window.setTimeout(() => {\n const container = containerRef.current;\n const renderer = rendererRef.current;\n const material = materialRef.current;\n if (!container || !renderer || !material) return;\n\n const w = container.offsetWidth;\n const h = container.offsetHeight;\n renderer.setSize(w, h);\n material.uniforms.uResolution.value.set(w, h);\n }, 100);\n }, []);\n\n // Visibility observer\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const observer = new IntersectionObserver(\n ([entry]) => {\n isVisibleRef.current = entry.isIntersecting;\n },\n { threshold: 0 }\n );\n\n observer.observe(container);\n return () => observer.disconnect();\n }, []);\n\n // Main Three.js setup - only runs once\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const scene = new Scene();\n const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);\n const renderer = new WebGLRenderer({\n antialias: false,\n alpha: true,\n premultipliedAlpha: false,\n powerPreference: 'high-performance',\n stencil: false,\n depth: false\n });\n\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setSize(container.offsetWidth, container.offsetHeight);\n renderer.setClearColor(0x000000, 0);\n container.appendChild(renderer.domElement);\n rendererRef.current = renderer;\n\n const material = new ShaderMaterial({\n vertexShader,\n fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uResolution: { value: new Vector2(container.offsetWidth, container.offsetHeight) },\n uFlakeSize: { value: flakeSize },\n uMinFlakeSize: { value: minFlakeSize },\n uPixelResolution: { value: pixelResolution },\n uSpeed: { value: speed },\n uDepthFade: { value: depthFade },\n uFarPlane: { value: farPlane },\n uColor: { value: colorVector.clone() },\n uBrightness: { value: brightness },\n uGamma: { value: gamma },\n uDensity: { value: density },\n uVariant: { value: variantValue },\n uDirection: { value: (direction * Math.PI) / 180 }\n },\n transparent: true\n });\n materialRef.current = material;\n\n const geometry = new PlaneGeometry(2, 2);\n scene.add(new Mesh(geometry, material));\n\n window.addEventListener('resize', handleResize);\n\n const startTime = performance.now();\n const animate = () => {\n animationRef.current = requestAnimationFrame(animate);\n\n // Only render if visible\n if (isVisibleRef.current) {\n material.uniforms.uTime.value = (performance.now() - startTime) * 0.001;\n renderer.render(scene, camera);\n }\n };\n animate();\n\n return () => {\n cancelAnimationFrame(animationRef.current);\n window.removeEventListener('resize', handleResize);\n if (resizeTimeoutRef.current) {\n clearTimeout(resizeTimeoutRef.current);\n }\n if (container.contains(renderer.domElement)) {\n container.removeChild(renderer.domElement);\n }\n renderer.dispose();\n renderer.forceContextLoss();\n geometry.dispose();\n material.dispose();\n rendererRef.current = null;\n materialRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [handleResize]); // Only recreate scene when handleResize changes\n\n // Update material uniforms when props change\n useEffect(() => {\n const material = materialRef.current;\n if (!material) return;\n\n material.uniforms.uFlakeSize.value = flakeSize;\n material.uniforms.uMinFlakeSize.value = minFlakeSize;\n material.uniforms.uPixelResolution.value = pixelResolution;\n material.uniforms.uSpeed.value = speed;\n material.uniforms.uDepthFade.value = depthFade;\n material.uniforms.uFarPlane.value = farPlane;\n material.uniforms.uBrightness.value = brightness;\n material.uniforms.uGamma.value = gamma;\n material.uniforms.uDensity.value = density;\n material.uniforms.uVariant.value = variantValue;\n material.uniforms.uDirection.value = (direction * Math.PI) / 180;\n material.uniforms.uColor.value.copy(colorVector);\n }, [\n flakeSize,\n minFlakeSize,\n pixelResolution,\n speed,\n depthFade,\n farPlane,\n brightness,\n gamma,\n density,\n variantValue,\n direction,\n colorVector\n ]);\n\n return
    ;\n}\n" } ], diff --git a/public/r/PixelSnow-TS-CSS.json b/public/r/PixelSnow-TS-CSS.json index b644c64b..f31385a1 100644 --- a/public/r/PixelSnow-TS-CSS.json +++ b/public/r/PixelSnow-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "PixelSnow/PixelSnow.css", + "type": "registry:file", + "path": "PixelSnow.css", + "target": "@components/PixelSnow.css", "content": ".pixel-snow-container {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n contain: layout style paint;\n}\n\n.pixel-snow-container canvas {\n display: block;\n width: 100%;\n height: 100%;\n transform: translateZ(0);\n will-change: transform;\n backface-visibility: hidden;\n}\n" }, { "type": "registry:component", - "path": "PixelSnow/PixelSnow.tsx", + "path": "PixelSnow.tsx", "content": "import { useCallback, useEffect, useMemo, useRef } from 'react';\nimport {\n Color,\n Mesh,\n OrthographicCamera,\n PlaneGeometry,\n Scene,\n ShaderMaterial,\n Vector2,\n Vector3,\n WebGLRenderer\n} from 'three';\n\nimport './PixelSnow.css';\n\nconst vertexShader = `\nvoid main() {\n gl_Position = vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision mediump float;\n\nuniform float uTime;\nuniform vec2 uResolution;\nuniform float uFlakeSize;\nuniform float uMinFlakeSize;\nuniform float uPixelResolution;\nuniform float uSpeed;\nuniform float uDepthFade;\nuniform float uFarPlane;\nuniform vec3 uColor;\nuniform float uBrightness;\nuniform float uGamma;\nuniform float uDensity;\nuniform float uVariant;\nuniform float uDirection;\n\n// Precomputed constants\n#define PI 3.14159265\n#define PI_OVER_6 0.5235988\n#define PI_OVER_3 1.0471976\n#define INV_SQRT3 0.57735027\n#define M1 1597334677U\n#define M2 3812015801U\n#define M3 3299493293U\n#define F0 2.3283064e-10\n\n// Optimized hash - inline multiplication\n#define hash(n) (n * (n ^ (n >> 15)))\n#define coord3(p) (uvec3(p).x * M1 ^ uvec3(p).y * M2 ^ uvec3(p).z * M3)\n\n// Precomputed camera basis vectors (normalized vec3(1,1,1), vec3(1,0,-1))\nconst vec3 camK = vec3(0.57735027, 0.57735027, 0.57735027);\nconst vec3 camI = vec3(0.70710678, 0.0, -0.70710678);\nconst vec3 camJ = vec3(-0.40824829, 0.81649658, -0.40824829);\n\n// Precomputed branch direction\nconst vec2 b1d = vec2(0.574, 0.819);\n\nvec3 hash3(uint n) {\n uvec3 hashed = hash(n) * uvec3(1U, 511U, 262143U);\n return vec3(hashed) * F0;\n}\n\nfloat snowflakeDist(vec2 p) {\n float r = length(p);\n float a = atan(p.y, p.x);\n a = abs(mod(a + PI_OVER_6, PI_OVER_3) - PI_OVER_6);\n vec2 q = r * vec2(cos(a), sin(a));\n float dMain = max(abs(q.y), max(-q.x, q.x - 1.0));\n float b1t = clamp(dot(q - vec2(0.4, 0.0), b1d), 0.0, 0.4);\n float dB1 = length(q - vec2(0.4, 0.0) - b1t * b1d);\n float b2t = clamp(dot(q - vec2(0.7, 0.0), b1d), 0.0, 0.25);\n float dB2 = length(q - vec2(0.7, 0.0) - b2t * b1d);\n return min(dMain, min(dB1, dB2)) * 10.0;\n}\n\nvoid main() {\n // Precompute reciprocals to avoid division\n float invPixelRes = 1.0 / uPixelResolution;\n float pixelSize = max(1.0, floor(0.5 + uResolution.x * invPixelRes));\n float invPixelSize = 1.0 / pixelSize;\n \n vec2 fragCoord = floor(gl_FragCoord.xy * invPixelSize);\n vec2 res = uResolution * invPixelSize;\n float invResX = 1.0 / res.x;\n\n vec3 ray = normalize(vec3((fragCoord - res * 0.5) * invResX, 1.0));\n ray = ray.x * camI + ray.y * camJ + ray.z * camK;\n\n // Precompute time-based values\n float timeSpeed = uTime * uSpeed;\n float windX = cos(uDirection) * 0.4;\n float windY = sin(uDirection) * 0.4;\n vec3 camPos = (windX * camI + windY * camJ + 0.1 * camK) * timeSpeed;\n vec3 pos = camPos;\n\n // Precompute ray reciprocal for strides\n vec3 absRay = max(abs(ray), vec3(0.001));\n vec3 strides = 1.0 / absRay;\n vec3 raySign = step(ray, vec3(0.0));\n vec3 phase = fract(pos) * strides;\n phase = mix(strides - phase, phase, raySign);\n\n // Precompute for intersection test\n float rayDotCamK = dot(ray, camK);\n float invRayDotCamK = 1.0 / rayDotCamK;\n float invDepthFade = 1.0 / uDepthFade;\n float halfInvResX = 0.5 * invResX;\n vec3 timeAnim = timeSpeed * 0.1 * vec3(7.0, 8.0, 5.0);\n\n float t = 0.0;\n for (int i = 0; i < 128; i++) {\n if (t >= uFarPlane) break;\n \n vec3 fpos = floor(pos);\n uint cellCoord = coord3(fpos);\n float cellHash = hash3(cellCoord).x;\n\n if (cellHash < uDensity) {\n vec3 h = hash3(cellCoord);\n \n // Optimized flake position calculation\n vec3 sinArg1 = fpos.yzx * 0.073;\n vec3 sinArg2 = fpos.zxy * 0.27;\n vec3 flakePos = 0.5 - 0.5 * cos(4.0 * sin(sinArg1) + 4.0 * sin(sinArg2) + 2.0 * h + timeAnim);\n flakePos = flakePos * 0.8 + 0.1 + fpos;\n\n float toIntersection = dot(flakePos - pos, camK) * invRayDotCamK;\n \n if (toIntersection > 0.0) {\n vec3 testPos = pos + ray * toIntersection - flakePos;\n float testX = dot(testPos, camI);\n float testY = dot(testPos, camJ);\n vec2 testUV = abs(vec2(testX, testY));\n \n float depth = dot(flakePos - camPos, camK);\n float flakeSize = max(uFlakeSize, uMinFlakeSize * depth * halfInvResX);\n \n // Avoid branching with step functions where possible\n float dist;\n if (uVariant < 0.5) {\n dist = max(testUV.x, testUV.y);\n } else if (uVariant < 1.5) {\n dist = length(testUV);\n } else {\n float invFlakeSize = 1.0 / flakeSize;\n dist = snowflakeDist(vec2(testX, testY) * invFlakeSize) * flakeSize;\n }\n\n if (dist < flakeSize) {\n float flakeSizeRatio = uFlakeSize / flakeSize;\n float intensity = exp2(-(t + toIntersection) * invDepthFade) *\n min(1.0, flakeSizeRatio * flakeSizeRatio) * uBrightness;\n gl_FragColor = vec4(uColor * pow(vec3(intensity), vec3(uGamma)), 1.0);\n return;\n }\n }\n }\n\n float nextStep = min(min(phase.x, phase.y), phase.z);\n vec3 sel = step(phase, vec3(nextStep));\n phase = phase - nextStep + strides * sel;\n t += nextStep;\n pos = mix(pos + ray * nextStep, floor(pos + ray * nextStep + 0.5), sel);\n }\n\n gl_FragColor = vec4(0.0);\n}\n`;\n\ninterface PixelSnowProps {\n color?: string;\n flakeSize?: number;\n minFlakeSize?: number;\n pixelResolution?: number;\n speed?: number;\n depthFade?: number;\n farPlane?: number;\n brightness?: number;\n gamma?: number;\n density?: number;\n variant?: 'square' | 'round' | 'snowflake';\n direction?: number;\n className?: string;\n style?: React.CSSProperties;\n}\n\nexport default function PixelSnow({\n color = '#ffffff',\n flakeSize = 0.01,\n minFlakeSize = 1.25,\n pixelResolution = 200,\n speed = 1.25,\n depthFade = 8,\n farPlane = 20,\n brightness = 1,\n gamma = 0.4545,\n density = 0.3,\n variant = 'square',\n direction = 125,\n className = '',\n style = {}\n}: PixelSnowProps) {\n const containerRef = useRef(null);\n const animationRef = useRef(0);\n const isVisibleRef = useRef(true);\n const rendererRef = useRef(null);\n const materialRef = useRef(null);\n const resizeTimeoutRef = useRef(null);\n\n // Memoize shader variant value\n const variantValue = useMemo(() => {\n return variant === 'round' ? 1.0 : variant === 'snowflake' ? 2.0 : 0.0;\n }, [variant]);\n\n // Memoize color conversion\n const colorVector = useMemo(() => {\n const threeColor = new Color(color);\n return new Vector3(threeColor.r, threeColor.g, threeColor.b);\n }, [color]);\n\n // Debounced resize handler\n const handleResize = useCallback(() => {\n if (resizeTimeoutRef.current) {\n clearTimeout(resizeTimeoutRef.current);\n }\n resizeTimeoutRef.current = window.setTimeout(() => {\n const container = containerRef.current;\n const renderer = rendererRef.current;\n const material = materialRef.current;\n if (!container || !renderer || !material) return;\n\n const w = container.offsetWidth;\n const h = container.offsetHeight;\n renderer.setSize(w, h);\n material.uniforms.uResolution.value.set(w, h);\n }, 100);\n }, []);\n\n // Visibility observer\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const observer = new IntersectionObserver(\n ([entry]) => {\n isVisibleRef.current = entry.isIntersecting;\n },\n { threshold: 0 }\n );\n\n observer.observe(container);\n return () => observer.disconnect();\n }, []);\n\n // Main Three.js setup - only runs once\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const scene = new Scene();\n const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);\n const renderer = new WebGLRenderer({\n antialias: false,\n alpha: true,\n premultipliedAlpha: false,\n powerPreference: 'high-performance',\n stencil: false,\n depth: false\n });\n\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setSize(container.offsetWidth, container.offsetHeight);\n renderer.setClearColor(0x000000, 0);\n container.appendChild(renderer.domElement);\n rendererRef.current = renderer;\n\n const material = new ShaderMaterial({\n vertexShader,\n fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uResolution: { value: new Vector2(container.offsetWidth, container.offsetHeight) },\n uFlakeSize: { value: flakeSize },\n uMinFlakeSize: { value: minFlakeSize },\n uPixelResolution: { value: pixelResolution },\n uSpeed: { value: speed },\n uDepthFade: { value: depthFade },\n uFarPlane: { value: farPlane },\n uColor: { value: colorVector.clone() },\n uBrightness: { value: brightness },\n uGamma: { value: gamma },\n uDensity: { value: density },\n uVariant: { value: variantValue },\n uDirection: { value: (direction * Math.PI) / 180 }\n },\n transparent: true\n });\n materialRef.current = material;\n\n const geometry = new PlaneGeometry(2, 2);\n scene.add(new Mesh(geometry, material));\n\n window.addEventListener('resize', handleResize);\n\n const startTime = performance.now();\n const animate = () => {\n animationRef.current = requestAnimationFrame(animate);\n\n // Only render if visible\n if (isVisibleRef.current) {\n material.uniforms.uTime.value = (performance.now() - startTime) * 0.001;\n renderer.render(scene, camera);\n }\n };\n animate();\n\n return () => {\n cancelAnimationFrame(animationRef.current);\n window.removeEventListener('resize', handleResize);\n if (resizeTimeoutRef.current) {\n clearTimeout(resizeTimeoutRef.current);\n }\n if (container.contains(renderer.domElement)) {\n container.removeChild(renderer.domElement);\n }\n renderer.dispose();\n renderer.forceContextLoss();\n geometry.dispose();\n material.dispose();\n rendererRef.current = null;\n materialRef.current = null;\n };\n }, [handleResize]); // Only recreate scene when handleResize changes\n\n // Update material uniforms when props change\n useEffect(() => {\n const material = materialRef.current;\n if (!material) return;\n\n material.uniforms.uFlakeSize.value = flakeSize;\n material.uniforms.uMinFlakeSize.value = minFlakeSize;\n material.uniforms.uPixelResolution.value = pixelResolution;\n material.uniforms.uSpeed.value = speed;\n material.uniforms.uDepthFade.value = depthFade;\n material.uniforms.uFarPlane.value = farPlane;\n material.uniforms.uBrightness.value = brightness;\n material.uniforms.uGamma.value = gamma;\n material.uniforms.uDensity.value = density;\n material.uniforms.uVariant.value = variantValue;\n material.uniforms.uDirection.value = (direction * Math.PI) / 180;\n material.uniforms.uColor.value.copy(colorVector);\n }, [\n flakeSize,\n minFlakeSize,\n pixelResolution,\n speed,\n depthFade,\n farPlane,\n brightness,\n gamma,\n density,\n variantValue,\n direction,\n colorVector\n ]);\n\n return
    ;\n}\n" } ], diff --git a/public/r/PixelSwap-JS-CSS.json b/public/r/PixelSwap-JS-CSS.json index 2dc1a9da..dfb85aee 100644 --- a/public/r/PixelSwap-JS-CSS.json +++ b/public/r/PixelSwap-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "PixelSwap/PixelSwap.css", + "type": "registry:file", + "path": "PixelSwap.css", + "target": "@components/PixelSwap.css", "content": ".pixel-swap {\n position: relative;\n width: 100%;\n overflow: hidden;\n isolation: isolate;\n outline: none;\n}\n\n.pixel-swap__layer {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n}\n\n.pixel-swap__layer[data-visible='false'] {\n visibility: hidden;\n}\n\n.pixel-swap__grid {\n position: absolute;\n inset: 0;\n z-index: 3;\n pointer-events: none;\n}\n\n.pixel-swap__pixel {\n position: absolute;\n overflow: hidden;\n opacity: 0;\n contain: paint;\n}\n\n.pixel-swap__pixel-content {\n position: absolute;\n}\n" }, { "type": "registry:component", - "path": "PixelSwap/PixelSwap.jsx", + "path": "PixelSwap.jsx", "content": "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport './PixelSwap.css';\n\n// Every pixel is a window onto its own copy of the incoming content, so the\n// grid stays bounded no matter how small the requested pixel size is.\nconst MAX_PIXELS = 220;\nconst KEYFRAME_STEPS = 14;\n\nconst PATTERNS = {\n random: () => null,\n center: (x, y) => Math.hypot(x - 0.5, y - 0.5) / Math.SQRT1_2,\n edges: (x, y) => Math.min(x, 1 - x, y, 1 - y) * 2,\n 'left-to-right': x => x,\n 'right-to-left': x => 1 - x,\n 'top-to-bottom': (_x, y) => y,\n 'bottom-to-top': (_x, y) => 1 - y,\n diagonal: (x, y) => (x + y) / 2,\n spiral: (x, y) => {\n const angle = (Math.atan2(y - 0.5, x - 0.5) + Math.PI) / (Math.PI * 2);\n const radius = Math.hypot(x - 0.5, y - 0.5) / Math.SQRT1_2;\n return (angle + radius) % 1;\n }\n};\n\nconst EASINGS = {\n linear: [0, 0, 1, 1],\n ease: [0.25, 0.1, 0.25, 1],\n 'ease-in': [0.42, 0, 1, 1],\n 'ease-out': [0, 0, 0.58, 1],\n 'ease-in-out': [0.42, 0, 0.58, 1]\n};\n\nconst clamp = (value, min, max) => Math.min(Math.max(value, min), max);\n\nconst noise = seed => {\n const value = Math.sin(seed * 127.1 + 311.7) * 43758.5453;\n return value - Math.floor(value);\n};\n\nconst makeEasing = value => {\n const match = /cubic-bezier\\(([^)]+)\\)/.exec(value);\n const points = match ? match[1].split(',').map(Number) : EASINGS[value];\n if (!points || points.length !== 4 || points.some(Number.isNaN)) return makeEasing('ease');\n\n const [x1, y1, x2, y2] = points;\n if (x1 === y1 && x2 === y2) return progress => progress;\n\n const cx = 3 * x1;\n const bx = 3 * (x2 - x1) - cx;\n const ax = 1 - cx - bx;\n const cy = 3 * y1;\n const by = 3 * (y2 - y1) - cy;\n const ay = 1 - cy - by;\n\n return progress => {\n let t = progress;\n for (let i = 0; i < 5; i += 1) {\n const slope = (3 * ax * t + 2 * bx) * t + cx;\n if (!slope) break;\n t -= (((ax * t + bx) * t + cx) * t - progress) / slope;\n }\n t = clamp(t, 0, 1);\n return ((ay * t + by) * t + cy) * t;\n };\n};\n\n// Pixels grow slightly past their own box so gaps and rounded corners close\n// completely by the end. Overlap is invisible because every pixel shows the\n// same content locked to the same origin.\nconst coverScale = (size, gap, radius) => {\n const p = clamp(radius, 0, 50) / 100;\n const corner = Math.SQRT1_2 / (Math.SQRT2 * (0.5 - p) + p);\n return ((size + gap) / size) * Math.max(1, corner);\n};\n\nconst buildGrid = ({ width, height, pixelSize, gap, pattern, randomness }) => {\n let size = pixelSize;\n let columns = Math.max(1, Math.ceil((width + gap) / (size + gap)));\n let rows = Math.max(1, Math.ceil((height + gap) / (size + gap)));\n\n if (columns * rows > MAX_PIXELS) {\n size = Math.ceil(size * Math.sqrt((columns * rows) / MAX_PIXELS));\n columns = Math.max(1, Math.ceil((width + gap) / (size + gap)));\n rows = Math.max(1, Math.ceil((height + gap) / (size + gap)));\n }\n\n // Overhang the box so edge pixels stay square instead of being cut short.\n const stride = size + gap;\n const originX = (width - (columns * stride - gap)) / 2;\n const originY = (height - (rows * stride - gap)) / 2;\n const order = PATTERNS[pattern] ?? PATTERNS.random;\n const mix = clamp(randomness, 0, 1);\n const pixels = [];\n\n for (let row = 0; row < rows; row += 1) {\n for (let column = 0; column < columns; column += 1) {\n const index = row * columns + column;\n const x = columns <= 1 ? 0.5 : column / (columns - 1);\n const y = rows <= 1 ? 0.5 : row / (rows - 1);\n const base = order(x, y);\n const random = noise(index + 1);\n\n pixels.push({\n id: index,\n left: originX + column * stride,\n top: originY + row * stride,\n offset: base === null ? random : base * (1 - mix) + random * mix\n });\n }\n }\n\n return { pixels, size, gap, width, height };\n};\n\n// One shared pair of keyframe lists for the whole grid: the window transform\n// and its exact inverse, so revealed content never drifts or scales.\nconst buildKeyframes = ({ ease, startScale, endScale, spin, fade }) => {\n const window = [];\n const content = [];\n\n for (let step = 0; step <= KEYFRAME_STEPS; step += 1) {\n const progress = step / KEYFRAME_STEPS;\n const eased = ease(progress);\n const scale = startScale + (endScale - startScale) * eased;\n const angle = spin * (1 - eased);\n\n window.push({\n offset: progress,\n opacity: fade ? Math.min(1, eased * 1.6) : 1,\n transform: `rotate(${angle}deg) scale(${scale})`\n });\n content.push({\n offset: progress,\n transform: `scale(${1 / scale}) rotate(${-angle}deg)`\n });\n }\n\n return { window, content };\n};\n\nfunction PixelSwap({\n firstContent,\n secondContent,\n pixelSize = 64,\n gap = 0,\n pixelRadius = 0,\n pixelSpin = 0,\n pixelScale = 0.35,\n fade = true,\n duration = 1400,\n pixelDuration = 450,\n pattern = 'random',\n randomness = 0,\n easing = 'cubic-bezier(0.22, 1, 0.36, 1)',\n trigger = 'hover',\n initialActive = false,\n active,\n onActiveChange,\n onComplete,\n aspectRatio = '16 / 10',\n className = '',\n style\n}) {\n const [internalActive, setInternalActive] = useState(initialActive);\n const [shownActive, setShownActive] = useState(active ?? initialActive);\n const [transition, setTransition] = useState(null);\n const [box, setBox] = useState({ width: 0, height: 0 });\n\n const containerRef = useRef(null);\n const layerRefs = useRef([]);\n const pixelRefs = useRef([]);\n const animationsRef = useRef([]);\n const timerRef = useRef(0);\n\n const desiredActive = active ?? internalActive;\n const incomingIndex = transition?.to ? 1 : 0;\n\n const grid = useMemo(\n () =>\n buildGrid({\n width: box.width,\n height: box.height,\n pixelSize: Math.max(8, Math.round(pixelSize)),\n gap: Math.max(0, Math.round(gap)),\n pattern,\n randomness\n }),\n [box.width, box.height, pixelSize, gap, pattern, randomness]\n );\n\n // Snapshot the animation inputs so a transition already in flight is never\n // rebuilt halfway through by an unrelated prop change.\n const config = { duration, pixelDuration, pixelSpin, pixelScale, pixelRadius, fade, easing, onComplete };\n const configRef = useRef(config);\n const gridRef = useRef(grid);\n configRef.current = config;\n gridRef.current = grid;\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n // Measure the padding box, which is the coordinate space the absolutely\n // positioned layers and pixel grid actually live in.\n const measure = () => {\n const width = container.clientWidth;\n const height = container.clientHeight;\n if (!width || !height) return;\n setBox(current => (current.width === width && current.height === height ? current : { width, height }));\n };\n\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(container);\n return () => observer.disconnect();\n }, []);\n\n const stopAnimations = useCallback(() => {\n animationsRef.current.forEach(animation => animation.cancel());\n animationsRef.current = [];\n pixelRefs.current.forEach(pixel => pixel?.replaceChildren());\n if (timerRef.current) window.clearTimeout(timerRef.current);\n timerRef.current = 0;\n }, []);\n\n useEffect(() => stopAnimations, [stopAnimations]);\n\n useEffect(() => {\n if (transition || desiredActive === shownActive) return;\n setTransition({ to: desiredActive, grid: gridRef.current });\n }, [desiredActive, shownActive, transition]);\n\n useEffect(() => {\n if (!transition) return;\n const settings = configRef.current;\n const { grid: frozenGrid, to } = transition;\n\n const finish = () => {\n stopAnimations();\n setShownActive(to);\n setTransition(null);\n settings.onComplete?.(to);\n };\n\n const source = layerRefs.current[to ? 1 : 0];\n if (!source || !frozenGrid.pixels.length || window.matchMedia('(prefers-reduced-motion: reduce)').matches) {\n finish();\n return;\n }\n\n const total = Math.max(200, settings.duration);\n const pixelMs = clamp(settings.pixelDuration, 60, total);\n const spread = Math.max(0, total - pixelMs);\n const endScale = coverScale(frozenGrid.size, frozenGrid.gap, settings.pixelRadius);\n const keyframes = buildKeyframes({\n ease: makeEasing(settings.easing),\n startScale: clamp(settings.pixelScale, 0.05, 1) * endScale,\n endScale,\n spin: settings.pixelSpin,\n fade: settings.fade\n });\n\n frozenGrid.pixels.forEach((pixel, index) => {\n const pixelElement = pixelRefs.current[index];\n if (!pixelElement) return;\n\n // Clone the rendered layer instead of re-rendering the content through\n // React once per pixel: same visual result, a fraction of the cost.\n const content = document.createElement('div');\n content.className = 'pixel-swap__pixel-content';\n content.style.left = `${-pixel.left}px`;\n content.style.top = `${-pixel.top}px`;\n content.style.width = `${frozenGrid.width}px`;\n content.style.height = `${frozenGrid.height}px`;\n // Counter-transform about the pixel's centre, not the content's, so the\n // two transforms cancel to an exact identity at every frame.\n const originX = pixel.left + frozenGrid.size / 2;\n const originY = pixel.top + frozenGrid.size / 2;\n content.style.transformOrigin = `${originX}px ${originY}px`;\n\n const clone = source.cloneNode(true);\n clone.dataset.visible = 'true';\n clone.removeAttribute('aria-hidden');\n content.appendChild(clone);\n pixelElement.replaceChildren(content);\n\n const timing = { duration: pixelMs, delay: pixel.offset * spread, easing: 'linear', fill: 'both' };\n animationsRef.current.push(\n pixelElement.animate(keyframes.window, timing),\n content.animate(keyframes.content, timing)\n );\n });\n\n timerRef.current = window.setTimeout(finish, total);\n return stopAnimations;\n }, [stopAnimations, transition]);\n\n const requestActive = useCallback(\n next => {\n if (active === undefined) setInternalActive(next);\n onActiveChange?.(next);\n },\n [active, onActiveChange]\n );\n\n const interactionProps = useMemo(() => {\n if (trigger === 'hover') {\n return {\n onMouseEnter: () => requestActive(true),\n onMouseLeave: () => requestActive(false),\n onFocus: () => requestActive(true),\n onBlur: () => requestActive(false),\n tabIndex: 0\n };\n }\n\n if (trigger === 'click') {\n return {\n onClick: () => requestActive(!desiredActive),\n onKeyDown: event => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n requestActive(!desiredActive);\n }\n },\n role: 'button',\n tabIndex: 0\n };\n }\n\n return {};\n }, [desiredActive, requestActive, trigger]);\n\n const renderLayer = (content, index) => {\n const isShown = index === (shownActive ? 1 : 0);\n return (\n {\n layerRefs.current[index] = element;\n }}\n className=\"pixel-swap__layer\"\n data-visible={isShown && !(transition && index === incomingIndex)}\n style={{ zIndex: isShown ? 2 : 1 }}\n aria-hidden={!isShown}\n >\n {content}\n
    \n );\n };\n\n return (\n \n {renderLayer(firstContent, 0)}\n {renderLayer(secondContent, 1)}\n\n {transition && (\n
    \n {transition.grid.pixels.map((pixel, index) => (\n {\n pixelRefs.current[index] = element;\n }}\n className=\"pixel-swap__pixel\"\n style={{\n left: pixel.left,\n top: pixel.top,\n width: transition.grid.size,\n height: transition.grid.size,\n borderRadius: `${clamp(pixelRadius, 0, 50)}%`\n }}\n />\n ))}\n
    \n )}\n
    \n );\n}\n\nexport default PixelSwap;\n" } ], diff --git a/public/r/PixelSwap-TS-CSS.json b/public/r/PixelSwap-TS-CSS.json index 2c080670..a550c977 100644 --- a/public/r/PixelSwap-TS-CSS.json +++ b/public/r/PixelSwap-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "PixelSwap/PixelSwap.css", + "type": "registry:file", + "path": "PixelSwap.css", + "target": "@components/PixelSwap.css", "content": ".pixel-swap {\n position: relative;\n width: 100%;\n overflow: hidden;\n isolation: isolate;\n outline: none;\n}\n\n.pixel-swap__layer {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n}\n\n.pixel-swap__layer[data-visible='false'] {\n visibility: hidden;\n}\n\n.pixel-swap__grid {\n position: absolute;\n inset: 0;\n z-index: 3;\n pointer-events: none;\n}\n\n.pixel-swap__pixel {\n position: absolute;\n overflow: hidden;\n opacity: 0;\n contain: paint;\n}\n\n.pixel-swap__pixel-content {\n position: absolute;\n}\n" }, { "type": "registry:component", - "path": "PixelSwap/PixelSwap.tsx", + "path": "PixelSwap.tsx", "content": "import { CSSProperties, KeyboardEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nexport type PixelSwapPattern =\n | 'random'\n | 'center'\n | 'edges'\n | 'left-to-right'\n | 'right-to-left'\n | 'top-to-bottom'\n | 'bottom-to-top'\n | 'diagonal'\n | 'spiral';\n\nexport type PixelSwapTrigger = 'hover' | 'click' | 'manual';\n\nexport interface PixelSwapProps {\n firstContent: ReactNode;\n secondContent: ReactNode;\n pixelSize?: number;\n gap?: number;\n pixelRadius?: number;\n pixelSpin?: number;\n pixelScale?: number;\n fade?: boolean;\n duration?: number;\n pixelDuration?: number;\n pattern?: PixelSwapPattern;\n randomness?: number;\n easing?: string;\n trigger?: PixelSwapTrigger;\n initialActive?: boolean;\n active?: boolean;\n onActiveChange?: (active: boolean) => void;\n onComplete?: (active: boolean) => void;\n aspectRatio?: string;\n className?: string;\n style?: CSSProperties;\n}\n\ninterface Pixel {\n id: number;\n left: number;\n top: number;\n offset: number;\n}\n\ninterface Grid {\n pixels: Pixel[];\n size: number;\n gap: number;\n width: number;\n height: number;\n}\n\ninterface Transition {\n to: boolean;\n grid: Grid;\n}\nimport './PixelSwap.css';\n\n// Every pixel is a window onto its own copy of the incoming content, so the\n// grid stays bounded no matter how small the requested pixel size is.\nconst MAX_PIXELS = 220;\nconst KEYFRAME_STEPS = 14;\n\nconst PATTERNS: Record number | null> = {\n random: () => null,\n center: (x, y) => Math.hypot(x - 0.5, y - 0.5) / Math.SQRT1_2,\n edges: (x, y) => Math.min(x, 1 - x, y, 1 - y) * 2,\n 'left-to-right': x => x,\n 'right-to-left': x => 1 - x,\n 'top-to-bottom': (_x, y) => y,\n 'bottom-to-top': (_x, y) => 1 - y,\n diagonal: (x, y) => (x + y) / 2,\n spiral: (x, y) => {\n const angle = (Math.atan2(y - 0.5, x - 0.5) + Math.PI) / (Math.PI * 2);\n const radius = Math.hypot(x - 0.5, y - 0.5) / Math.SQRT1_2;\n return (angle + radius) % 1;\n }\n};\n\nconst EASINGS: Record = {\n linear: [0, 0, 1, 1],\n ease: [0.25, 0.1, 0.25, 1],\n 'ease-in': [0.42, 0, 1, 1],\n 'ease-out': [0, 0, 0.58, 1],\n 'ease-in-out': [0.42, 0, 0.58, 1]\n};\n\nconst clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);\n\nconst noise = (seed: number): number => {\n const value = Math.sin(seed * 127.1 + 311.7) * 43758.5453;\n return value - Math.floor(value);\n};\n\nconst makeEasing = (value: string): ((progress: number) => number) => {\n const match = /cubic-bezier\\(([^)]+)\\)/.exec(value);\n const points = match ? match[1].split(',').map(Number) : EASINGS[value];\n if (!points || points.length !== 4 || points.some(Number.isNaN)) return makeEasing('ease');\n\n const [x1, y1, x2, y2] = points;\n if (x1 === y1 && x2 === y2) return (progress: number) => progress;\n\n const cx = 3 * x1;\n const bx = 3 * (x2 - x1) - cx;\n const ax = 1 - cx - bx;\n const cy = 3 * y1;\n const by = 3 * (y2 - y1) - cy;\n const ay = 1 - cy - by;\n\n return (progress: number) => {\n let t = progress;\n for (let i = 0; i < 5; i += 1) {\n const slope = (3 * ax * t + 2 * bx) * t + cx;\n if (!slope) break;\n t -= (((ax * t + bx) * t + cx) * t - progress) / slope;\n }\n t = clamp(t, 0, 1);\n return ((ay * t + by) * t + cy) * t;\n };\n};\n\n// Pixels grow slightly past their own box so gaps and rounded corners close\n// completely by the end. Overlap is invisible because every pixel shows the\n// same content locked to the same origin.\nconst coverScale = (size: number, gap: number, radius: number): number => {\n const p = clamp(radius, 0, 50) / 100;\n const corner = Math.SQRT1_2 / (Math.SQRT2 * (0.5 - p) + p);\n return ((size + gap) / size) * Math.max(1, corner);\n};\n\nconst buildGrid = ({\n width,\n height,\n pixelSize,\n gap,\n pattern,\n randomness\n}: {\n width: number;\n height: number;\n pixelSize: number;\n gap: number;\n pattern: PixelSwapPattern;\n randomness: number;\n}): Grid => {\n let size = pixelSize;\n let columns = Math.max(1, Math.ceil((width + gap) / (size + gap)));\n let rows = Math.max(1, Math.ceil((height + gap) / (size + gap)));\n\n if (columns * rows > MAX_PIXELS) {\n size = Math.ceil(size * Math.sqrt((columns * rows) / MAX_PIXELS));\n columns = Math.max(1, Math.ceil((width + gap) / (size + gap)));\n rows = Math.max(1, Math.ceil((height + gap) / (size + gap)));\n }\n\n // Overhang the box so edge pixels stay square instead of being cut short.\n const stride = size + gap;\n const originX = (width - (columns * stride - gap)) / 2;\n const originY = (height - (rows * stride - gap)) / 2;\n const order = PATTERNS[pattern] ?? PATTERNS.random;\n const mix = clamp(randomness, 0, 1);\n const pixels: Pixel[] = [];\n\n for (let row = 0; row < rows; row += 1) {\n for (let column = 0; column < columns; column += 1) {\n const index = row * columns + column;\n const x = columns <= 1 ? 0.5 : column / (columns - 1);\n const y = rows <= 1 ? 0.5 : row / (rows - 1);\n const base = order(x, y);\n const random = noise(index + 1);\n\n pixels.push({\n id: index,\n left: originX + column * stride,\n top: originY + row * stride,\n offset: base === null ? random : base * (1 - mix) + random * mix\n });\n }\n }\n\n return { pixels, size, gap, width, height };\n};\n\n// One shared pair of keyframe lists for the whole grid: the window transform\n// and its exact inverse, so revealed content never drifts or scales.\nconst buildKeyframes = ({\n ease,\n startScale,\n endScale,\n spin,\n fade\n}: {\n ease: (progress: number) => number;\n startScale: number;\n endScale: number;\n spin: number;\n fade: boolean;\n}) => {\n const window: Keyframe[] = [];\n const content: Keyframe[] = [];\n\n for (let step = 0; step <= KEYFRAME_STEPS; step += 1) {\n const progress = step / KEYFRAME_STEPS;\n const eased = ease(progress);\n const scale = startScale + (endScale - startScale) * eased;\n const angle = spin * (1 - eased);\n\n window.push({\n offset: progress,\n opacity: fade ? Math.min(1, eased * 1.6) : 1,\n transform: `rotate(${angle}deg) scale(${scale})`\n });\n content.push({\n offset: progress,\n transform: `scale(${1 / scale}) rotate(${-angle}deg)`\n });\n }\n\n return { window, content };\n};\n\nfunction PixelSwap({\n firstContent,\n secondContent,\n pixelSize = 64,\n gap = 0,\n pixelRadius = 0,\n pixelSpin = 0,\n pixelScale = 0.35,\n fade = true,\n duration = 1400,\n pixelDuration = 450,\n pattern = 'random',\n randomness = 0,\n easing = 'cubic-bezier(0.22, 1, 0.36, 1)',\n trigger = 'hover',\n initialActive = false,\n active,\n onActiveChange,\n onComplete,\n aspectRatio = '16 / 10',\n className = '',\n style\n}: PixelSwapProps) {\n const [internalActive, setInternalActive] = useState(initialActive);\n const [shownActive, setShownActive] = useState(active ?? initialActive);\n const [transition, setTransition] = useState(null);\n const [box, setBox] = useState({ width: 0, height: 0 });\n\n const containerRef = useRef(null);\n const layerRefs = useRef<(HTMLDivElement | null)[]>([]);\n const pixelRefs = useRef<(HTMLDivElement | null)[]>([]);\n const animationsRef = useRef([]);\n const timerRef = useRef(0);\n\n const desiredActive = active ?? internalActive;\n const incomingIndex = transition?.to ? 1 : 0;\n\n const grid = useMemo(\n () =>\n buildGrid({\n width: box.width,\n height: box.height,\n pixelSize: Math.max(8, Math.round(pixelSize)),\n gap: Math.max(0, Math.round(gap)),\n pattern,\n randomness\n }),\n [box.width, box.height, pixelSize, gap, pattern, randomness]\n );\n\n // Snapshot the animation inputs so a transition already in flight is never\n // rebuilt halfway through by an unrelated prop change.\n const config = { duration, pixelDuration, pixelSpin, pixelScale, pixelRadius, fade, easing, onComplete };\n const configRef = useRef(config);\n const gridRef = useRef(grid);\n configRef.current = config;\n gridRef.current = grid;\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n // Measure the padding box, which is the coordinate space the absolutely\n // positioned layers and pixel grid actually live in.\n const measure = () => {\n const width = container.clientWidth;\n const height = container.clientHeight;\n if (!width || !height) return;\n setBox(current => (current.width === width && current.height === height ? current : { width, height }));\n };\n\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(container);\n return () => observer.disconnect();\n }, []);\n\n const stopAnimations = useCallback(() => {\n animationsRef.current.forEach(animation => animation.cancel());\n animationsRef.current = [];\n pixelRefs.current.forEach(pixel => pixel?.replaceChildren());\n if (timerRef.current) window.clearTimeout(timerRef.current);\n timerRef.current = 0;\n }, []);\n\n useEffect(() => stopAnimations, [stopAnimations]);\n\n useEffect(() => {\n if (transition || desiredActive === shownActive) return;\n setTransition({ to: desiredActive, grid: gridRef.current });\n }, [desiredActive, shownActive, transition]);\n\n useEffect(() => {\n if (!transition) return;\n const settings = configRef.current;\n const { grid: frozenGrid, to } = transition;\n\n const finish = () => {\n stopAnimations();\n setShownActive(to);\n setTransition(null);\n settings.onComplete?.(to);\n };\n\n const source = layerRefs.current[to ? 1 : 0];\n if (!source || !frozenGrid.pixels.length || window.matchMedia('(prefers-reduced-motion: reduce)').matches) {\n finish();\n return;\n }\n\n const total = Math.max(200, settings.duration);\n const pixelMs = clamp(settings.pixelDuration, 60, total);\n const spread = Math.max(0, total - pixelMs);\n const endScale = coverScale(frozenGrid.size, frozenGrid.gap, settings.pixelRadius);\n const keyframes = buildKeyframes({\n ease: makeEasing(settings.easing),\n startScale: clamp(settings.pixelScale, 0.05, 1) * endScale,\n endScale,\n spin: settings.pixelSpin,\n fade: settings.fade\n });\n\n frozenGrid.pixels.forEach((pixel, index) => {\n const pixelElement = pixelRefs.current[index];\n if (!pixelElement) return;\n\n // Clone the rendered layer instead of re-rendering the content through\n // React once per pixel: same visual result, a fraction of the cost.\n const content = document.createElement('div');\n content.className = 'pixel-swap__pixel-content';\n content.style.left = `${-pixel.left}px`;\n content.style.top = `${-pixel.top}px`;\n content.style.width = `${frozenGrid.width}px`;\n content.style.height = `${frozenGrid.height}px`;\n // Counter-transform about the pixel's centre, not the content's, so the\n // two transforms cancel to an exact identity at every frame.\n const originX = pixel.left + frozenGrid.size / 2;\n const originY = pixel.top + frozenGrid.size / 2;\n content.style.transformOrigin = `${originX}px ${originY}px`;\n\n const clone = source.cloneNode(true) as HTMLElement;\n clone.dataset.visible = 'true';\n clone.removeAttribute('aria-hidden');\n content.appendChild(clone);\n pixelElement.replaceChildren(content);\n\n const timing: KeyframeAnimationOptions = {\n duration: pixelMs,\n delay: pixel.offset * spread,\n easing: 'linear',\n fill: 'both'\n };\n animationsRef.current.push(\n pixelElement.animate(keyframes.window, timing),\n content.animate(keyframes.content, timing)\n );\n });\n\n timerRef.current = window.setTimeout(finish, total);\n return stopAnimations;\n }, [stopAnimations, transition]);\n\n const requestActive = useCallback(\n (next: boolean) => {\n if (active === undefined) setInternalActive(next);\n onActiveChange?.(next);\n },\n [active, onActiveChange]\n );\n\n const interactionProps = useMemo(() => {\n if (trigger === 'hover') {\n return {\n onMouseEnter: () => requestActive(true),\n onMouseLeave: () => requestActive(false),\n onFocus: () => requestActive(true),\n onBlur: () => requestActive(false),\n tabIndex: 0\n };\n }\n\n if (trigger === 'click') {\n return {\n onClick: () => requestActive(!desiredActive),\n onKeyDown: (event: KeyboardEvent) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n requestActive(!desiredActive);\n }\n },\n role: 'button',\n tabIndex: 0\n };\n }\n\n return {};\n }, [desiredActive, requestActive, trigger]);\n\n const renderLayer = (content: ReactNode, index: number) => {\n const isShown = index === (shownActive ? 1 : 0);\n return (\n {\n layerRefs.current[index] = element;\n }}\n className=\"pixel-swap__layer\"\n data-visible={isShown && !(transition && index === incomingIndex)}\n style={{ zIndex: isShown ? 2 : 1 }}\n aria-hidden={!isShown}\n >\n {content}\n \n );\n };\n\n return (\n \n {renderLayer(firstContent, 0)}\n {renderLayer(secondContent, 1)}\n\n {transition && (\n
    \n {transition.grid.pixels.map((pixel, index) => (\n {\n pixelRefs.current[index] = element;\n }}\n className=\"pixel-swap__pixel\"\n style={{\n left: pixel.left,\n top: pixel.top,\n width: transition.grid.size,\n height: transition.grid.size,\n borderRadius: `${clamp(pixelRadius, 0, 50)}%`\n }}\n />\n ))}\n
    \n )}\n \n );\n}\n\nexport default PixelSwap;\n" } ], diff --git a/public/r/PixelTrail-JS-CSS.json b/public/r/PixelTrail-JS-CSS.json index fb2cf90b..6049e567 100644 --- a/public/r/PixelTrail-JS-CSS.json +++ b/public/r/PixelTrail-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "PixelTrail/PixelTrail.css", + "type": "registry:file", + "path": "PixelTrail.css", + "target": "@components/PixelTrail.css", "content": ".goo-filter-container {\n position: absolute;\n overflow: hidden;\n z-index: 1;\n}\n\n.pixel-canvas {\n position: absolute;\n z-index: 1;\n}\n" }, { "type": "registry:component", - "path": "PixelTrail/PixelTrail.jsx", + "path": "PixelTrail.jsx", "content": "/* eslint-disable react/no-unknown-property */\nimport { useMemo } from 'react';\nimport { Canvas, useThree } from '@react-three/fiber';\nimport { shaderMaterial, useTrailTexture } from '@react-three/drei';\nimport * as THREE from 'three';\n\nimport './PixelTrail.css';\n\nconst GooeyFilter = ({ id = 'goo-filter', strength = 10 }) => {\n return (\n \n \n \n \n \n \n \n \n \n );\n};\n\nconst DotMaterial = shaderMaterial(\n {\n resolution: new THREE.Vector2(),\n mouseTrail: null,\n gridSize: 100,\n pixelColor: new THREE.Color('#ffffff')\n },\n `\n varying vec2 vUv;\n void main() {\n gl_Position = vec4(position.xy, 0.0, 1.0);\n }\n `,\n `\n uniform vec2 resolution;\n uniform sampler2D mouseTrail;\n uniform float gridSize;\n uniform vec3 pixelColor;\n\n vec2 coverUv(vec2 uv) {\n vec2 s = resolution.xy / max(resolution.x, resolution.y);\n vec2 newUv = (uv - 0.5) * s + 0.5;\n return clamp(newUv, 0.0, 1.0);\n }\n\n float sdfCircle(vec2 p, float r) {\n return length(p - 0.5) - r;\n }\n\n void main() {\n vec2 screenUv = gl_FragCoord.xy / resolution;\n vec2 uv = coverUv(screenUv);\n\n vec2 gridUv = fract(uv * gridSize);\n vec2 gridUvCenter = (floor(uv * gridSize) + 0.5) / gridSize;\n\n float trail = texture2D(mouseTrail, gridUvCenter).r;\n\n gl_FragColor = vec4(pixelColor, trail);\n }\n `\n);\n\nfunction Scene({ gridSize, trailSize, maxAge, interpolate, easingFunction, pixelColor }) {\n const size = useThree(s => s.size);\n const viewport = useThree(s => s.viewport);\n\n const dotMaterial = useMemo(() => new DotMaterial(), []);\n dotMaterial.uniforms.pixelColor.value = new THREE.Color(pixelColor);\n\n const [trail, onMove] = useTrailTexture({\n size: 512,\n radius: trailSize,\n maxAge: maxAge,\n interpolate: interpolate || 0.1,\n ease: easingFunction || (x => x)\n });\n\n if (trail) {\n trail.minFilter = THREE.NearestFilter;\n trail.magFilter = THREE.NearestFilter;\n trail.wrapS = THREE.ClampToEdgeWrapping;\n trail.wrapT = THREE.ClampToEdgeWrapping;\n }\n\n const scale = Math.max(viewport.width, viewport.height) / 2;\n\n return (\n \n \n \n \n );\n}\n\nexport default function PixelTrail({\n gridSize = 40,\n trailSize = 0.1,\n maxAge = 250,\n interpolate = 5,\n easingFunction = x => x,\n canvasProps = {},\n glProps = {\n antialias: false,\n powerPreference: 'high-performance',\n alpha: true\n },\n gooeyFilter,\n color = '#ffffff',\n className = ''\n}) {\n return (\n <>\n {gooeyFilter && }\n \n \n
    \n \n );\n}\n" } ], diff --git a/public/r/PixelTrail-TS-CSS.json b/public/r/PixelTrail-TS-CSS.json index b7f428f8..9ea67ef9 100644 --- a/public/r/PixelTrail-TS-CSS.json +++ b/public/r/PixelTrail-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "PixelTrail/PixelTrail.css", + "type": "registry:file", + "path": "PixelTrail.css", + "target": "@components/PixelTrail.css", "content": ".goo-filter-container {\n position: absolute;\n overflow: hidden;\n z-index: 1;\n}\n\n.pixel-canvas {\n position: absolute;\n z-index: 1;\n}\n" }, { "type": "registry:component", - "path": "PixelTrail/PixelTrail.tsx", - "content": "/* eslint-disable react/no-unknown-property */\nimport React, { useMemo } from 'react';\nimport { Canvas, useThree, CanvasProps, ThreeEvent } from '@react-three/fiber';\nimport { shaderMaterial, useTrailTexture } from '@react-three/drei';\nimport * as THREE from 'three';\n\nimport './PixelTrail.css';\n\ninterface GooeyFilterProps {\n id?: string;\n strength?: number;\n}\n\ninterface DotMaterialUniforms {\n resolution: THREE.Vector2;\n mouseTrail: THREE.Texture | null;\n gridSize: number;\n pixelColor: THREE.Color;\n}\n\ninterface SceneProps {\n gridSize: number;\n trailSize: number;\n maxAge: number;\n interpolate: number;\n easingFunction: (x: number) => number;\n pixelColor: string;\n}\n\ninterface PixelTrailProps {\n gridSize?: number;\n trailSize?: number;\n maxAge?: number;\n interpolate?: number;\n easingFunction?: (x: number) => number;\n canvasProps?: Partial;\n glProps?: WebGLContextAttributes & { powerPreference?: string };\n gooeyFilter?: { id: string; strength: number };\n color?: string;\n className?: string;\n}\n\nconst GooeyFilter: React.FC = ({ id = 'goo-filter', strength = 10 }) => {\n return (\n \n \n \n \n \n \n \n \n \n );\n};\n\nconst DotMaterial = shaderMaterial(\n {\n resolution: new THREE.Vector2(),\n mouseTrail: null,\n gridSize: 100,\n pixelColor: new THREE.Color('#ffffff')\n },\n /* glsl vertex shader */ `\n varying vec2 vUv;\n void main() {\n gl_Position = vec4(position.xy, 0.0, 1.0);\n }\n `,\n /* glsl fragment shader */ `\n uniform vec2 resolution;\n uniform sampler2D mouseTrail;\n uniform float gridSize;\n uniform vec3 pixelColor;\n\n vec2 coverUv(vec2 uv) {\n vec2 s = resolution.xy / max(resolution.x, resolution.y);\n vec2 newUv = (uv - 0.5) * s + 0.5;\n return clamp(newUv, 0.0, 1.0);\n }\n\n float sdfCircle(vec2 p, float r) {\n return length(p - 0.5) - r;\n }\n\n void main() {\n vec2 screenUv = gl_FragCoord.xy / resolution;\n vec2 uv = coverUv(screenUv);\n\n vec2 gridUv = fract(uv * gridSize);\n vec2 gridUvCenter = (floor(uv * gridSize) + 0.5) / gridSize;\n\n float trail = texture2D(mouseTrail, gridUvCenter).r;\n\n gl_FragColor = vec4(pixelColor, trail);\n }\n `\n);\n\nfunction Scene({ gridSize, trailSize, maxAge, interpolate, easingFunction, pixelColor }: SceneProps) {\n const size = useThree(s => s.size);\n const viewport = useThree(s => s.viewport);\n\n const dotMaterial = useMemo(() => new DotMaterial(), []);\n dotMaterial.uniforms.pixelColor.value = new THREE.Color(pixelColor);\n\n const [trail, onMove] = useTrailTexture({\n size: 512,\n radius: trailSize,\n maxAge: maxAge,\n interpolate: interpolate || 0.1,\n ease: easingFunction || ((x: number) => x)\n }) as [THREE.Texture | null, (e: ThreeEvent) => void];\n\n if (trail) {\n trail.minFilter = THREE.NearestFilter;\n trail.magFilter = THREE.NearestFilter;\n trail.wrapS = THREE.ClampToEdgeWrapping;\n trail.wrapT = THREE.ClampToEdgeWrapping;\n }\n\n const scale = Math.max(viewport.width, viewport.height) / 2;\n\n return (\n \n \n \n \n );\n}\n\nexport default function PixelTrail({\n gridSize = 40,\n trailSize = 0.1,\n maxAge = 250,\n interpolate = 5,\n easingFunction = (x: number) => x,\n canvasProps = {},\n glProps = {\n antialias: false,\n powerPreference: 'high-performance',\n alpha: true\n },\n gooeyFilter,\n color = '#ffffff',\n className = ''\n}: PixelTrailProps) {\n return (\n <>\n {gooeyFilter && }\n \n \n
    \n \n );\n}\n" + "path": "PixelTrail.tsx", + "content": "/* eslint-disable react/no-unknown-property */\nimport React, { useMemo } from 'react';\nimport { Canvas, useThree, type CanvasProps, type ThreeEvent } from '@react-three/fiber';\nimport { shaderMaterial, useTrailTexture } from '@react-three/drei';\nimport * as THREE from 'three';\n\nimport './PixelTrail.css';\n\ninterface GooeyFilterProps {\n id?: string;\n strength?: number;\n}\n\ninterface DotMaterialUniforms {\n resolution: THREE.Vector2;\n mouseTrail: THREE.Texture | null;\n gridSize: number;\n pixelColor: THREE.Color;\n}\n\ninterface SceneProps {\n gridSize: number;\n trailSize: number;\n maxAge: number;\n interpolate: number;\n easingFunction: (x: number) => number;\n pixelColor: string;\n}\n\ninterface PixelTrailProps {\n gridSize?: number;\n trailSize?: number;\n maxAge?: number;\n interpolate?: number;\n easingFunction?: (x: number) => number;\n canvasProps?: Partial;\n glProps?: WebGLContextAttributes & { powerPreference?: string };\n gooeyFilter?: { id: string; strength: number };\n color?: string;\n className?: string;\n}\n\nconst GooeyFilter: React.FC = ({ id = 'goo-filter', strength = 10 }) => {\n return (\n \n \n \n \n \n \n \n \n \n );\n};\n\nconst DotMaterial = shaderMaterial(\n {\n resolution: new THREE.Vector2(),\n mouseTrail: null,\n gridSize: 100,\n pixelColor: new THREE.Color('#ffffff')\n },\n /* glsl vertex shader */ `\n varying vec2 vUv;\n void main() {\n gl_Position = vec4(position.xy, 0.0, 1.0);\n }\n `,\n /* glsl fragment shader */ `\n uniform vec2 resolution;\n uniform sampler2D mouseTrail;\n uniform float gridSize;\n uniform vec3 pixelColor;\n\n vec2 coverUv(vec2 uv) {\n vec2 s = resolution.xy / max(resolution.x, resolution.y);\n vec2 newUv = (uv - 0.5) * s + 0.5;\n return clamp(newUv, 0.0, 1.0);\n }\n\n float sdfCircle(vec2 p, float r) {\n return length(p - 0.5) - r;\n }\n\n void main() {\n vec2 screenUv = gl_FragCoord.xy / resolution;\n vec2 uv = coverUv(screenUv);\n\n vec2 gridUv = fract(uv * gridSize);\n vec2 gridUvCenter = (floor(uv * gridSize) + 0.5) / gridSize;\n\n float trail = texture2D(mouseTrail, gridUvCenter).r;\n\n gl_FragColor = vec4(pixelColor, trail);\n }\n `\n);\n\nfunction Scene({ gridSize, trailSize, maxAge, interpolate, easingFunction, pixelColor }: SceneProps) {\n const size = useThree(s => s.size);\n const viewport = useThree(s => s.viewport);\n\n const dotMaterial = useMemo(() => new DotMaterial(), []);\n dotMaterial.uniforms.pixelColor.value = new THREE.Color(pixelColor);\n\n const [trail, onMove] = useTrailTexture({\n size: 512,\n radius: trailSize,\n maxAge: maxAge,\n interpolate: interpolate || 0.1,\n ease: easingFunction || ((x: number) => x)\n }) as [THREE.Texture | null, (e: ThreeEvent) => void];\n\n if (trail) {\n trail.minFilter = THREE.NearestFilter;\n trail.magFilter = THREE.NearestFilter;\n trail.wrapS = THREE.ClampToEdgeWrapping;\n trail.wrapT = THREE.ClampToEdgeWrapping;\n }\n\n const scale = Math.max(viewport.width, viewport.height) / 2;\n\n return (\n \n \n \n \n );\n}\n\nexport default function PixelTrail({\n gridSize = 40,\n trailSize = 0.1,\n maxAge = 250,\n interpolate = 5,\n easingFunction = (x: number) => x,\n canvasProps = {},\n glProps = {\n antialias: false,\n powerPreference: 'high-performance',\n alpha: true\n },\n gooeyFilter,\n color = '#ffffff',\n className = ''\n}: PixelTrailProps) {\n return (\n <>\n {gooeyFilter && }\n \n \n
    \n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/PixelTrail-TS-TW.json b/public/r/PixelTrail-TS-TW.json index 3110313b..2df874f5 100644 --- a/public/r/PixelTrail-TS-TW.json +++ b/public/r/PixelTrail-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "PixelTrail/PixelTrail.tsx", - "content": "import { shaderMaterial, useTrailTexture } from '@react-three/drei';\nimport { Canvas, CanvasProps, ThreeEvent, useThree } from '@react-three/fiber';\nimport React, { useEffect, useMemo } from 'react';\nimport * as THREE from 'three';\n\ninterface GooeyFilterProps {\n id?: string;\n strength?: number;\n}\n\ninterface DotMaterialUniforms {\n resolution: THREE.Vector2;\n mouseTrail: THREE.Texture | null;\n gridSize: number;\n pixelColor: THREE.Color;\n}\n\ninterface SceneProps {\n gridSize: number;\n trailSize: number;\n maxAge: number;\n interpolate: number;\n easingFunction: (x: number) => number;\n pixelColor: string;\n}\n\ninterface PixelTrailProps {\n gridSize?: number;\n trailSize?: number;\n maxAge?: number;\n interpolate?: number;\n easingFunction?: (x: number) => number;\n canvasProps?: Partial;\n glProps?: WebGLContextAttributes & { powerPreference?: string };\n gooeyFilter?: { id: string; strength: number };\n color?: string;\n className?: string;\n}\n\nconst GooeyFilter: React.FC = ({ id = 'goo-filter', strength = 10 }) => {\n return (\n \n \n \n \n \n \n \n \n \n );\n};\n\nconst DotMaterial = shaderMaterial(\n {\n resolution: new THREE.Vector2(),\n mouseTrail: null,\n gridSize: 100,\n pixelColor: new THREE.Color('#ffffff')\n },\n /* glsl vertex shader */ `\n varying vec2 vUv;\n void main() {\n gl_Position = vec4(position.xy, 0.0, 1.0);\n }\n `,\n /* glsl fragment shader */ `\n uniform vec2 resolution;\n uniform sampler2D mouseTrail;\n uniform float gridSize;\n uniform vec3 pixelColor;\n\n vec2 coverUv(vec2 uv) {\n vec2 s = resolution.xy / max(resolution.x, resolution.y);\n vec2 newUv = (uv - 0.5) * s + 0.5;\n return clamp(newUv, 0.0, 1.0);\n }\n\n float sdfCircle(vec2 p, float r) {\n return length(p - 0.5) - r;\n }\n\n void main() {\n vec2 screenUv = gl_FragCoord.xy / resolution;\n vec2 uv = coverUv(screenUv);\n\n vec2 gridUv = fract(uv * gridSize);\n vec2 gridUvCenter = (floor(uv * gridSize) + 0.5) / gridSize;\n\n float trail = texture2D(mouseTrail, gridUvCenter).r;\n\n gl_FragColor = vec4(pixelColor, trail);\n }\n `\n);\n\nconst identityEase = (x: number) => x;\n\nfunction Scene({ gridSize, trailSize, maxAge, interpolate, easingFunction, pixelColor }: SceneProps) {\n const size = useThree(s => s.size);\n const viewport = useThree(s => s.viewport);\n\n const dotMaterial = useMemo(() => new DotMaterial(), []);\n useEffect(() => {\n return () => {\n dotMaterial.dispose();\n };\n }, [dotMaterial]);\n\n useEffect(() => {\n (dotMaterial.uniforms.pixelColor.value as THREE.Color).set(pixelColor);\n }, [dotMaterial, pixelColor]);\n\n const [trail, onMove] = useTrailTexture({\n size: 512,\n radius: trailSize,\n maxAge: maxAge,\n interpolate: interpolate || 0.1,\n ease: easingFunction || identityEase\n }) as [THREE.Texture | null, (e: ThreeEvent) => void];\n\n useEffect(() => {\n if (!trail) return;\n trail.minFilter = THREE.NearestFilter;\n trail.magFilter = THREE.NearestFilter;\n trail.wrapS = THREE.ClampToEdgeWrapping;\n trail.wrapT = THREE.ClampToEdgeWrapping;\n }, [trail]);\n\n const scale = Math.max(viewport.width, viewport.height) / 2;\n\n return (\n \n \n \n \n );\n}\n\nexport default function PixelTrail({\n gridSize = 40,\n trailSize = 0.1,\n maxAge = 250,\n interpolate = 5,\n easingFunction = identityEase,\n canvasProps = {},\n glProps = {\n antialias: false,\n powerPreference: 'high-performance',\n alpha: true\n },\n gooeyFilter,\n color = '#ffffff',\n className = ''\n}: PixelTrailProps) {\n return (\n <>\n {gooeyFilter && }\n \n \n
    \n \n );\n}\n" + "content": "import { shaderMaterial, useTrailTexture } from '@react-three/drei';\nimport { Canvas, type CanvasProps, type ThreeEvent, useThree } from '@react-three/fiber';\nimport React, { useEffect, useMemo } from 'react';\nimport * as THREE from 'three';\n\ninterface GooeyFilterProps {\n id?: string;\n strength?: number;\n}\n\ninterface DotMaterialUniforms {\n resolution: THREE.Vector2;\n mouseTrail: THREE.Texture | null;\n gridSize: number;\n pixelColor: THREE.Color;\n}\n\ninterface SceneProps {\n gridSize: number;\n trailSize: number;\n maxAge: number;\n interpolate: number;\n easingFunction: (x: number) => number;\n pixelColor: string;\n}\n\ninterface PixelTrailProps {\n gridSize?: number;\n trailSize?: number;\n maxAge?: number;\n interpolate?: number;\n easingFunction?: (x: number) => number;\n canvasProps?: Partial;\n glProps?: WebGLContextAttributes & { powerPreference?: string };\n gooeyFilter?: { id: string; strength: number };\n color?: string;\n className?: string;\n}\n\nconst GooeyFilter: React.FC = ({ id = 'goo-filter', strength = 10 }) => {\n return (\n \n \n \n \n \n \n \n \n \n );\n};\n\nconst DotMaterial = shaderMaterial(\n {\n resolution: new THREE.Vector2(),\n mouseTrail: null,\n gridSize: 100,\n pixelColor: new THREE.Color('#ffffff')\n },\n /* glsl vertex shader */ `\n varying vec2 vUv;\n void main() {\n gl_Position = vec4(position.xy, 0.0, 1.0);\n }\n `,\n /* glsl fragment shader */ `\n uniform vec2 resolution;\n uniform sampler2D mouseTrail;\n uniform float gridSize;\n uniform vec3 pixelColor;\n\n vec2 coverUv(vec2 uv) {\n vec2 s = resolution.xy / max(resolution.x, resolution.y);\n vec2 newUv = (uv - 0.5) * s + 0.5;\n return clamp(newUv, 0.0, 1.0);\n }\n\n float sdfCircle(vec2 p, float r) {\n return length(p - 0.5) - r;\n }\n\n void main() {\n vec2 screenUv = gl_FragCoord.xy / resolution;\n vec2 uv = coverUv(screenUv);\n\n vec2 gridUv = fract(uv * gridSize);\n vec2 gridUvCenter = (floor(uv * gridSize) + 0.5) / gridSize;\n\n float trail = texture2D(mouseTrail, gridUvCenter).r;\n\n gl_FragColor = vec4(pixelColor, trail);\n }\n `\n);\n\nconst identityEase = (x: number) => x;\n\nfunction Scene({ gridSize, trailSize, maxAge, interpolate, easingFunction, pixelColor }: SceneProps) {\n const size = useThree(s => s.size);\n const viewport = useThree(s => s.viewport);\n\n const dotMaterial = useMemo(() => new DotMaterial(), []);\n useEffect(() => {\n return () => {\n dotMaterial.dispose();\n };\n }, [dotMaterial]);\n\n useEffect(() => {\n (dotMaterial.uniforms.pixelColor.value as THREE.Color).set(pixelColor);\n }, [dotMaterial, pixelColor]);\n\n const [trail, onMove] = useTrailTexture({\n size: 512,\n radius: trailSize,\n maxAge: maxAge,\n interpolate: interpolate || 0.1,\n ease: easingFunction || identityEase\n }) as [THREE.Texture | null, (e: ThreeEvent) => void];\n\n useEffect(() => {\n if (!trail) return;\n trail.minFilter = THREE.NearestFilter;\n trail.magFilter = THREE.NearestFilter;\n trail.wrapS = THREE.ClampToEdgeWrapping;\n trail.wrapT = THREE.ClampToEdgeWrapping;\n }, [trail]);\n\n const scale = Math.max(viewport.width, viewport.height) / 2;\n\n return (\n \n \n \n \n );\n}\n\nexport default function PixelTrail({\n gridSize = 40,\n trailSize = 0.1,\n maxAge = 250,\n interpolate = 5,\n easingFunction = identityEase,\n canvasProps = {},\n glProps = {\n antialias: false,\n powerPreference: 'high-performance',\n alpha: true\n },\n gooeyFilter,\n color = '#ffffff',\n className = ''\n}: PixelTrailProps) {\n return (\n <>\n {gooeyFilter && }\n \n \n
    \n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/PixelTransition-JS-CSS.json b/public/r/PixelTransition-JS-CSS.json index 714816e6..2de269a2 100644 --- a/public/r/PixelTransition-JS-CSS.json +++ b/public/r/PixelTransition-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "PixelTransition/PixelTransition.css", + "type": "registry:file", + "path": "PixelTransition.css", + "target": "@components/PixelTransition.css", "content": ".pixelated-image-card {\n background-color: #222;\n color: var(--color-primary, #fff);\n border-radius: 15px;\n border: 2px solid #fff;\n width: 300px;\n max-width: 100%;\n position: relative;\n overflow: hidden;\n}\n\n.pixelated-image-card__default,\n.pixelated-image-card__active,\n.pixelated-image-card__pixels {\n width: 100%;\n height: 100%;\n position: absolute;\n top: 0;\n left: 0;\n}\n\n.pixelated-image-card__active {\n z-index: 2;\n}\n\n.pixelated-image-card__active {\n display: none;\n}\n\n.pixelated-image-card__pixels {\n pointer-events: none;\n position: absolute;\n z-index: 3;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n}\n\n.pixelated-image-card__pixel {\n display: none;\n position: absolute;\n}\n" }, { "type": "registry:component", - "path": "PixelTransition/PixelTransition.jsx", + "path": "PixelTransition.jsx", "content": "import { useRef, useEffect, useState } from 'react';\nimport { gsap } from 'gsap';\nimport './PixelTransition.css';\n\nfunction PixelTransition({\n firstContent,\n secondContent,\n gridSize = 7,\n pixelColor = 'currentColor',\n animationStepDuration = 0.3,\n once = false,\n aspectRatio = '100%',\n className = '',\n style = {}\n}) {\n const containerRef = useRef(null);\n const pixelGridRef = useRef(null);\n const activeRef = useRef(null);\n const delayedCallRef = useRef(null);\n\n const [isActive, setIsActive] = useState(false);\n\n const isTouchDevice =\n 'ontouchstart' in window || navigator.maxTouchPoints > 0 || window.matchMedia('(pointer: coarse)').matches;\n\n useEffect(() => {\n const pixelGridEl = pixelGridRef.current;\n if (!pixelGridEl) return;\n\n pixelGridEl.innerHTML = '';\n\n for (let row = 0; row < gridSize; row++) {\n for (let col = 0; col < gridSize; col++) {\n const pixel = document.createElement('div');\n pixel.classList.add('pixelated-image-card__pixel');\n pixel.style.backgroundColor = pixelColor;\n\n const size = 100 / gridSize;\n pixel.style.width = `${size}%`;\n pixel.style.height = `${size}%`;\n pixel.style.left = `${col * size}%`;\n pixel.style.top = `${row * size}%`;\n pixelGridEl.appendChild(pixel);\n }\n }\n }, [gridSize, pixelColor]);\n\n const animatePixels = activate => {\n setIsActive(activate);\n\n const pixelGridEl = pixelGridRef.current;\n const activeEl = activeRef.current;\n if (!pixelGridEl || !activeEl) return;\n\n const pixels = pixelGridEl.querySelectorAll('.pixelated-image-card__pixel');\n if (!pixels.length) return;\n\n gsap.killTweensOf(pixels);\n if (delayedCallRef.current) {\n delayedCallRef.current.kill();\n }\n\n gsap.set(pixels, { display: 'none' });\n\n const totalPixels = pixels.length;\n const staggerDuration = animationStepDuration / totalPixels;\n\n gsap.to(pixels, {\n display: 'block',\n duration: 0,\n stagger: {\n each: staggerDuration,\n from: 'random'\n }\n });\n\n delayedCallRef.current = gsap.delayedCall(animationStepDuration, () => {\n activeEl.style.display = activate ? 'block' : 'none';\n activeEl.style.pointerEvents = activate ? 'none' : '';\n });\n\n gsap.to(pixels, {\n display: 'none',\n duration: 0,\n delay: animationStepDuration,\n stagger: {\n each: staggerDuration,\n from: 'random'\n }\n });\n };\n\n const handleEnter = () => {\n if (!isActive) animatePixels(true);\n };\n const handleLeave = () => {\n if (isActive && !once) animatePixels(false);\n };\n const handleClick = () => {\n if (!isActive) animatePixels(true);\n else if (isActive && !once) animatePixels(false);\n };\n\n return (\n \n
    \n
    \n {firstContent}\n
    \n
    \n {secondContent}\n
    \n
    \n
    \n );\n}\n\nexport default PixelTransition;\n" } ], diff --git a/public/r/PixelTransition-TS-CSS.json b/public/r/PixelTransition-TS-CSS.json index b4102bec..e41e529b 100644 --- a/public/r/PixelTransition-TS-CSS.json +++ b/public/r/PixelTransition-TS-CSS.json @@ -6,14 +6,15 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "PixelTransition/PixelTransition.css", + "type": "registry:file", + "path": "PixelTransition.css", + "target": "@components/PixelTransition.css", "content": ".pixelated-image-card {\n background-color: #222;\n color: #fff;\n border-radius: 15px;\n border: 2px solid #fff;\n width: 300px;\n max-width: 100%;\n position: relative;\n overflow: hidden;\n}\n\n.pixelated-image-card__default,\n.pixelated-image-card__active,\n.pixelated-image-card__pixels {\n width: 100%;\n height: 100%;\n position: absolute;\n top: 0;\n left: 0;\n}\n\n.pixelated-image-card__active {\n z-index: 2;\n}\n\n.pixelated-image-card__active {\n display: none;\n}\n\n.pixelated-image-card__pixels {\n pointer-events: none;\n position: absolute;\n z-index: 3;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n}\n\n.pixelated-image-card__pixel {\n display: none;\n position: absolute;\n}\n" }, { "type": "registry:component", - "path": "PixelTransition/PixelTransition.tsx", - "content": "import React, { useRef, useEffect, useState, CSSProperties } from 'react';\nimport { gsap } from 'gsap';\nimport './PixelTransition.css';\n\ninterface PixelTransitionProps {\n firstContent: React.ReactNode | string;\n secondContent: React.ReactNode | string;\n gridSize?: number;\n pixelColor?: string;\n animationStepDuration?: number;\n once?: boolean;\n className?: string;\n style?: CSSProperties;\n aspectRatio?: string;\n}\n\nconst PixelTransition: React.FC = ({\n firstContent,\n secondContent,\n gridSize = 7,\n pixelColor = 'currentColor',\n animationStepDuration = 0.3,\n once = false,\n aspectRatio = '100%',\n className = '',\n style = {}\n}) => {\n const containerRef = useRef(null);\n const pixelGridRef = useRef(null);\n const activeRef = useRef(null);\n const delayedCallRef = useRef(null);\n\n const [isActive, setIsActive] = useState(false);\n\n const isTouchDevice =\n 'ontouchstart' in window || navigator.maxTouchPoints > 0 || window.matchMedia('(pointer: coarse)').matches;\n\n useEffect(() => {\n const pixelGridEl = pixelGridRef.current;\n if (!pixelGridEl) return;\n\n pixelGridEl.innerHTML = '';\n\n for (let row = 0; row < gridSize; row++) {\n for (let col = 0; col < gridSize; col++) {\n const pixel = document.createElement('div');\n pixel.classList.add('pixelated-image-card__pixel');\n pixel.style.backgroundColor = pixelColor;\n\n const size = 100 / gridSize;\n pixel.style.width = `${size}%`;\n pixel.style.height = `${size}%`;\n pixel.style.left = `${col * size}%`;\n pixel.style.top = `${row * size}%`;\n pixelGridEl.appendChild(pixel);\n }\n }\n }, [gridSize, pixelColor]);\n\n const animatePixels = (activate: boolean): void => {\n setIsActive(activate);\n\n const pixelGridEl = pixelGridRef.current;\n const activeEl = activeRef.current;\n if (!pixelGridEl || !activeEl) return;\n\n const pixels = pixelGridEl.querySelectorAll('.pixelated-image-card__pixel');\n if (!pixels.length) return;\n\n gsap.killTweensOf(pixels);\n if (delayedCallRef.current) {\n delayedCallRef.current.kill();\n }\n\n gsap.set(pixels, { display: 'none' });\n\n const totalPixels = pixels.length;\n const staggerDuration = animationStepDuration / totalPixels;\n\n gsap.to(pixels, {\n display: 'block',\n duration: 0,\n stagger: {\n each: staggerDuration,\n from: 'random'\n }\n });\n\n delayedCallRef.current = gsap.delayedCall(animationStepDuration, () => {\n activeEl.style.display = activate ? 'block' : 'none';\n activeEl.style.pointerEvents = activate ? 'none' : '';\n });\n\n gsap.to(pixels, {\n display: 'none',\n duration: 0,\n delay: animationStepDuration,\n stagger: {\n each: staggerDuration,\n from: 'random'\n }\n });\n };\n\n const handleEnter = (): void => {\n if (!isActive) animatePixels(true);\n };\n const handleLeave = (): void => {\n if (isActive && !once) animatePixels(false);\n };\n const handleClick = (): void => {\n if (!isActive) animatePixels(true);\n else if (isActive && !once) animatePixels(false);\n };\n return (\n \n
    \n
    \n {firstContent}\n
    \n
    \n {secondContent}\n
    \n
    \n
    \n );\n};\n\nexport default PixelTransition;\n" + "path": "PixelTransition.tsx", + "content": "import React, { useRef, useEffect, useState, type CSSProperties } from 'react';\nimport { gsap } from 'gsap';\nimport './PixelTransition.css';\n\ninterface PixelTransitionProps {\n firstContent: React.ReactNode | string;\n secondContent: React.ReactNode | string;\n gridSize?: number;\n pixelColor?: string;\n animationStepDuration?: number;\n once?: boolean;\n className?: string;\n style?: CSSProperties;\n aspectRatio?: string;\n}\n\nconst PixelTransition: React.FC = ({\n firstContent,\n secondContent,\n gridSize = 7,\n pixelColor = 'currentColor',\n animationStepDuration = 0.3,\n once = false,\n aspectRatio = '100%',\n className = '',\n style = {}\n}) => {\n const containerRef = useRef(null);\n const pixelGridRef = useRef(null);\n const activeRef = useRef(null);\n const delayedCallRef = useRef(null);\n\n const [isActive, setIsActive] = useState(false);\n\n const isTouchDevice =\n 'ontouchstart' in window || navigator.maxTouchPoints > 0 || window.matchMedia('(pointer: coarse)').matches;\n\n useEffect(() => {\n const pixelGridEl = pixelGridRef.current;\n if (!pixelGridEl) return;\n\n pixelGridEl.innerHTML = '';\n\n for (let row = 0; row < gridSize; row++) {\n for (let col = 0; col < gridSize; col++) {\n const pixel = document.createElement('div');\n pixel.classList.add('pixelated-image-card__pixel');\n pixel.style.backgroundColor = pixelColor;\n\n const size = 100 / gridSize;\n pixel.style.width = `${size}%`;\n pixel.style.height = `${size}%`;\n pixel.style.left = `${col * size}%`;\n pixel.style.top = `${row * size}%`;\n pixelGridEl.appendChild(pixel);\n }\n }\n }, [gridSize, pixelColor]);\n\n const animatePixels = (activate: boolean): void => {\n setIsActive(activate);\n\n const pixelGridEl = pixelGridRef.current;\n const activeEl = activeRef.current;\n if (!pixelGridEl || !activeEl) return;\n\n const pixels = pixelGridEl.querySelectorAll('.pixelated-image-card__pixel');\n if (!pixels.length) return;\n\n gsap.killTweensOf(pixels);\n if (delayedCallRef.current) {\n delayedCallRef.current.kill();\n }\n\n gsap.set(pixels, { display: 'none' });\n\n const totalPixels = pixels.length;\n const staggerDuration = animationStepDuration / totalPixels;\n\n gsap.to(pixels, {\n display: 'block',\n duration: 0,\n stagger: {\n each: staggerDuration,\n from: 'random'\n }\n });\n\n delayedCallRef.current = gsap.delayedCall(animationStepDuration, () => {\n activeEl.style.display = activate ? 'block' : 'none';\n activeEl.style.pointerEvents = activate ? 'none' : '';\n });\n\n gsap.to(pixels, {\n display: 'none',\n duration: 0,\n delay: animationStepDuration,\n stagger: {\n each: staggerDuration,\n from: 'random'\n }\n });\n };\n\n const handleEnter = (): void => {\n if (!isActive) animatePixels(true);\n };\n const handleLeave = (): void => {\n if (isActive && !once) animatePixels(false);\n };\n const handleClick = (): void => {\n if (!isActive) animatePixels(true);\n else if (isActive && !once) animatePixels(false);\n };\n return (\n \n
    \n
    \n {firstContent}\n
    \n
    \n {secondContent}\n
    \n
    \n
    \n );\n};\n\nexport default PixelTransition;\n" } ], "registryDependencies": [], diff --git a/public/r/PixelTransition-TS-TW.json b/public/r/PixelTransition-TS-TW.json index 817c3540..f213c1b5 100644 --- a/public/r/PixelTransition-TS-TW.json +++ b/public/r/PixelTransition-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "PixelTransition/PixelTransition.tsx", - "content": "import React, { useRef, useEffect, useState, CSSProperties } from 'react';\nimport { gsap } from 'gsap';\n\ninterface PixelTransitionProps {\n firstContent: React.ReactNode | string;\n secondContent: React.ReactNode | string;\n gridSize?: number;\n pixelColor?: string;\n animationStepDuration?: number;\n once?: boolean;\n className?: string;\n style?: CSSProperties;\n aspectRatio?: string;\n}\n\nconst PixelTransition: React.FC = ({\n firstContent,\n secondContent,\n gridSize = 7,\n pixelColor = 'currentColor',\n animationStepDuration = 0.3,\n once = false,\n aspectRatio = '100%',\n className = '',\n style = {}\n}) => {\n const containerRef = useRef(null);\n const pixelGridRef = useRef(null);\n const activeRef = useRef(null);\n const delayedCallRef = useRef(null);\n\n const [isActive, setIsActive] = useState(false);\n\n const isTouchDevice =\n 'ontouchstart' in window || navigator.maxTouchPoints > 0 || window.matchMedia('(pointer: coarse)').matches;\n\n useEffect(() => {\n const pixelGridEl = pixelGridRef.current;\n if (!pixelGridEl) return;\n\n pixelGridEl.innerHTML = '';\n\n for (let row = 0; row < gridSize; row++) {\n for (let col = 0; col < gridSize; col++) {\n const pixel = document.createElement('div');\n pixel.classList.add('pixelated-image-card__pixel');\n pixel.classList.add('absolute', 'hidden');\n pixel.style.backgroundColor = pixelColor;\n\n const size = 100 / gridSize;\n pixel.style.width = `${size}%`;\n pixel.style.height = `${size}%`;\n pixel.style.left = `${col * size}%`;\n pixel.style.top = `${row * size}%`;\n\n pixelGridEl.appendChild(pixel);\n }\n }\n }, [gridSize, pixelColor]);\n\n const animatePixels = (activate: boolean): void => {\n setIsActive(activate);\n\n const pixelGridEl = pixelGridRef.current;\n const activeEl = activeRef.current;\n if (!pixelGridEl || !activeEl) return;\n\n const pixels = pixelGridEl.querySelectorAll('.pixelated-image-card__pixel');\n if (!pixels.length) return;\n\n gsap.killTweensOf(pixels);\n if (delayedCallRef.current) {\n delayedCallRef.current.kill();\n }\n\n gsap.set(pixels, { display: 'none' });\n\n const totalPixels = pixels.length;\n const staggerDuration = animationStepDuration / totalPixels;\n\n gsap.to(pixels, {\n display: 'block',\n duration: 0,\n stagger: {\n each: staggerDuration,\n from: 'random'\n }\n });\n\n delayedCallRef.current = gsap.delayedCall(animationStepDuration, () => {\n activeEl.style.display = activate ? 'block' : 'none';\n activeEl.style.pointerEvents = activate ? 'none' : '';\n });\n\n gsap.to(pixels, {\n display: 'none',\n duration: 0,\n delay: animationStepDuration,\n stagger: {\n each: staggerDuration,\n from: 'random'\n }\n });\n };\n\n const handleEnter = (): void => {\n if (!isActive) animatePixels(true);\n };\n const handleLeave = (): void => {\n if (isActive && !once) animatePixels(false);\n };\n const handleClick = (): void => {\n if (!isActive) animatePixels(true);\n else if (isActive && !once) animatePixels(false);\n };\n return (\n \n
    \n\n
    \n {firstContent}\n
    \n\n \n {secondContent}\n
    \n\n
    \n
    \n );\n};\n\nexport default PixelTransition;\n" + "content": "import React, { useRef, useEffect, useState, type CSSProperties } from 'react';\nimport { gsap } from 'gsap';\n\ninterface PixelTransitionProps {\n firstContent: React.ReactNode | string;\n secondContent: React.ReactNode | string;\n gridSize?: number;\n pixelColor?: string;\n animationStepDuration?: number;\n once?: boolean;\n className?: string;\n style?: CSSProperties;\n aspectRatio?: string;\n}\n\nconst PixelTransition: React.FC = ({\n firstContent,\n secondContent,\n gridSize = 7,\n pixelColor = 'currentColor',\n animationStepDuration = 0.3,\n once = false,\n aspectRatio = '100%',\n className = '',\n style = {}\n}) => {\n const containerRef = useRef(null);\n const pixelGridRef = useRef(null);\n const activeRef = useRef(null);\n const delayedCallRef = useRef(null);\n\n const [isActive, setIsActive] = useState(false);\n\n const isTouchDevice =\n 'ontouchstart' in window || navigator.maxTouchPoints > 0 || window.matchMedia('(pointer: coarse)').matches;\n\n useEffect(() => {\n const pixelGridEl = pixelGridRef.current;\n if (!pixelGridEl) return;\n\n pixelGridEl.innerHTML = '';\n\n for (let row = 0; row < gridSize; row++) {\n for (let col = 0; col < gridSize; col++) {\n const pixel = document.createElement('div');\n pixel.classList.add('pixelated-image-card__pixel');\n pixel.classList.add('absolute', 'hidden');\n pixel.style.backgroundColor = pixelColor;\n\n const size = 100 / gridSize;\n pixel.style.width = `${size}%`;\n pixel.style.height = `${size}%`;\n pixel.style.left = `${col * size}%`;\n pixel.style.top = `${row * size}%`;\n\n pixelGridEl.appendChild(pixel);\n }\n }\n }, [gridSize, pixelColor]);\n\n const animatePixels = (activate: boolean): void => {\n setIsActive(activate);\n\n const pixelGridEl = pixelGridRef.current;\n const activeEl = activeRef.current;\n if (!pixelGridEl || !activeEl) return;\n\n const pixels = pixelGridEl.querySelectorAll('.pixelated-image-card__pixel');\n if (!pixels.length) return;\n\n gsap.killTweensOf(pixels);\n if (delayedCallRef.current) {\n delayedCallRef.current.kill();\n }\n\n gsap.set(pixels, { display: 'none' });\n\n const totalPixels = pixels.length;\n const staggerDuration = animationStepDuration / totalPixels;\n\n gsap.to(pixels, {\n display: 'block',\n duration: 0,\n stagger: {\n each: staggerDuration,\n from: 'random'\n }\n });\n\n delayedCallRef.current = gsap.delayedCall(animationStepDuration, () => {\n activeEl.style.display = activate ? 'block' : 'none';\n activeEl.style.pointerEvents = activate ? 'none' : '';\n });\n\n gsap.to(pixels, {\n display: 'none',\n duration: 0,\n delay: animationStepDuration,\n stagger: {\n each: staggerDuration,\n from: 'random'\n }\n });\n };\n\n const handleEnter = (): void => {\n if (!isActive) animatePixels(true);\n };\n const handleLeave = (): void => {\n if (isActive && !once) animatePixels(false);\n };\n const handleClick = (): void => {\n if (!isActive) animatePixels(true);\n else if (isActive && !once) animatePixels(false);\n };\n return (\n \n
    \n\n
    \n {firstContent}\n
    \n\n \n {secondContent}\n
    \n\n
    \n
    \n );\n};\n\nexport default PixelTransition;\n" } ], "registryDependencies": [], diff --git a/public/r/Plasma-JS-CSS.json b/public/r/Plasma-JS-CSS.json index eab0a5c8..79b095b9 100644 --- a/public/r/Plasma-JS-CSS.json +++ b/public/r/Plasma-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Plasma/Plasma.css", + "type": "registry:file", + "path": "Plasma.css", + "target": "@components/Plasma.css", "content": ".plasma-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "Plasma/Plasma.jsx", + "path": "Plasma.jsx", "content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './Plasma.css';\n\nconst hexToRgb = hex => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 0.5, 0.2];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst vertex = `#version 300 es\nprecision highp float;\nin vec2 position;\nin vec2 uv;\nout vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst ORIGINAL_QUALITY = 60;\n\nconst buildFragment = (iterations) => {\n return `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform vec3 uCustomColor;\nuniform float uUseCustomColor;\nuniform float uSpeed;\nuniform float uDirection;\nuniform float uScale; \nuniform float uOpacity;\nuniform vec2 uMouse;\nuniform float uMouseInteractive;\nuniform float uQuality;\nuniform float uStepScale;\nout vec4 fragColor;\n\nvoid mainImage(out vec4 o, vec2 C) {\n vec2 center = iResolution.xy * 0.5;\n C = (C - center) / uScale + center;\n \n vec2 mouseOffset = (uMouse - center) * 0.0002;\n C += mouseOffset * length(C - center) * step(0.5, uMouseInteractive);\n \n float i, d, z, T = iTime * uSpeed * uDirection;\n vec3 O, p, S;\n\n for (vec2 r = iResolution.xy, Q; ++i < 60.0; O += o.w/d*o.xyz) {\n p = z*normalize(vec3(C-.5*r,r.y)); \n p.z -= 4.; \n S = p;\n d = p.y-T;\n \n p.x += .4*(1.+p.y)*sin(d + p.x*0.1)*cos(.34*d + p.x*0.05); \n Q = p.xz *= mat2(cos(p.y+vec4(0,11,33,0)-T)); \n z += d = (abs(sqrt(length(Q*Q)) - .25*(5.+S.y))/3.+8e-4) * uStepScale;\n o = 1.+sin(S.y+p.z*.5+S.z-length(S-p)+vec4(2,1,0,8));\n if (i >= uQuality) break;\n }\n \n o.xyz = tanh(O/1e4);\n}\n\nbool finite1(float x){ return !(isnan(x) || isinf(x)); }\nvec3 sanitize(vec3 c){\n return vec3(\n finite1(c.r) ? c.r : 0.0,\n finite1(c.g) ? c.g : 0.0,\n finite1(c.b) ? c.b : 0.0\n );\n}\n\nvoid main() {\n vec4 o = vec4(0.0);\n mainImage(o, gl_FragCoord.xy);\n vec3 rgb = sanitize(o.rgb);\n \n float intensity = (rgb.r + rgb.g + rgb.b) / 3.0;\n vec3 customColor = intensity * uCustomColor;\n vec3 finalColor = mix(rgb, customColor, step(0.5, uUseCustomColor));\n \n float alpha = length(rgb) * uOpacity;\n fragColor = vec4(finalColor, alpha);\n}`;\n};\n\nexport const Plasma = ({\n color = '#ffffff',\n speed = 1,\n direction = 'forward',\n scale = 1,\n opacity = 1,\n mouseInteractive = true,\n renderScale = 0.55,\n maxDpr = 1.5,\n targetFps = 60,\n iterations = 60,\n}) => {\n const containerRef = useRef(null);\n const mousePos = useRef({ x: 0, y: 0 });\n const pendingMouse = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n const containerEl = containerRef.current;\n\n const prefersReducedMotion =\n typeof window !== 'undefined' &&\n window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;\n\n const useCustomColor = color ? 1.0 : 0.0;\n const customColorRgb = color ? hexToRgb(color) : [1, 1, 1];\n\n const directionMultiplier = direction === 'reverse' ? -1.0 : 1.0;\n\n let renderer;\n try {\n renderer = new Renderer({\n webgl: 2,\n alpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, maxDpr)\n });\n } catch {\n return;\n }\n const gl = renderer.gl;\n if (!gl) return;\n const canvas = gl.canvas;\n canvas.style.display = 'block';\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n // Rendering at renderScale internally, CSS stretches it back up.\n containerEl.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n\n const program = new Program(gl, {\n vertex: vertex,\n fragment: buildFragment(iterations),\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uCustomColor: { value: new Float32Array(customColorRgb) },\n uUseCustomColor: { value: useCustomColor },\n uSpeed: { value: speed * 0.4 },\n uDirection: { value: directionMultiplier },\n uScale: { value: scale },\n uOpacity: { value: opacity },\n uMouse: { value: new Float32Array([0, 0]) },\n uMouseInteractive: { value: mouseInteractive ? 1.0 : 0.0 },\n uQuality: { value: iterations },\n uStepScale: { value: ORIGINAL_QUALITY / iterations },\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n\n const handleMouseMove = e => {\n if (!mouseInteractive) return;\n const rect = containerEl.getBoundingClientRect();\n // Store the latest position but don't touch GL state here, the rAF loop picks it up once per rendered frame instead of once per mouse event.\n pendingMouse.current = {\n x: e.clientX - rect.left,\n y: e.clientY - rect.top,\n };\n };\n\n if (mouseInteractive) {\n containerEl.addEventListener('mousemove', handleMouseMove, { passive: true });\n }\n\n let resizePending = false;\n const setSize = () => {\n const rect = containerEl.getBoundingClientRect();\n const width = Math.max(1, Math.floor(rect.width * renderScale));\n const height = Math.max(1, Math.floor(rect.height * renderScale));\n renderer.setSize(width, height);\n\n // renderer.setSize also sets canvas.style.width/height to match the (scaled-down) drawing buffer - override that so the canvas still stretches to fill its container via CSS while the buffer stays small.\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n\n const res = program.uniforms.iResolution.value;\n res[0] = gl.drawingBufferWidth;\n res[1] = gl.drawingBufferHeight;\n };\n\n const ro = new ResizeObserver(() => {\n // Batch rapid resize events (ex. during a window drag) into one setSize per frame.\n if (resizePending) return;\n resizePending = true;\n requestAnimationFrame(() => {\n resizePending = false;\n setSize();\n });\n });\n ro.observe(containerEl);\n setSize();\n\n let raf = 0;\n let contextLost = false;\n let isVisible = true;\n let tabVisible = document.visibilityState !== 'hidden';\n const t0 = performance.now();\n const frameInterval = 1000 / targetFps;\n let lastFrameTime = 0;\n\n const renderStaticFrame = () => {\n program.uniforms.iTime.value = 0;\n renderer.render({ scene: mesh });\n };\n\n const loop = t => {\n if (contextLost || !isVisible || !tabVisible) return;\n\n if (t - lastFrameTime < frameInterval) {\n raf = requestAnimationFrame(loop);\n return;\n }\n lastFrameTime = t;\n\n if (pendingMouse.current) {\n mousePos.current = pendingMouse.current;\n pendingMouse.current = null;\n const mouseUniform = program.uniforms.uMouse.value;\n mouseUniform[0] = mousePos.current.x;\n mouseUniform[1] = mousePos.current.y;\n }\n\n let timeValue = (t - t0) * 0.001;\n if (direction === 'pingpong') {\n const pingpongDuration = 10;\n const segmentTime = timeValue % pingpongDuration;\n const isForward = Math.floor(timeValue / pingpongDuration) % 2 === 0;\n const u = segmentTime / pingpongDuration;\n const smooth = u * u * (3 - 2 * u);\n const pingpongTime = isForward ? smooth * pingpongDuration : (1 - smooth) * pingpongDuration;\n program.uniforms.uDirection.value = 1.0;\n program.uniforms.iTime.value = pingpongTime;\n } else {\n program.uniforms.iTime.value = timeValue;\n }\n renderer.render({ scene: mesh });\n raf = requestAnimationFrame(loop);\n };\n\n const handleContextLost = (e) => {\n e.preventDefault();\n contextLost = true;\n cancelAnimationFrame(raf);\n };\n const handleContextRestored = () => {\n contextLost = false;\n if (isVisible && tabVisible && !prefersReducedMotion) {\n cancelAnimationFrame(raf);\n raf = requestAnimationFrame(loop);\n }\n };\n canvas.addEventListener('webglcontextlost', handleContextLost);\n canvas.addEventListener('webglcontextrestored', handleContextRestored);\n\n const io = new IntersectionObserver(([entry]) => {\n const wasVisible = isVisible;\n isVisible = entry.isIntersecting;\n if (isVisible && !wasVisible && !contextLost && tabVisible && !prefersReducedMotion) {\n cancelAnimationFrame(raf);\n raf = requestAnimationFrame(loop);\n }\n }, { threshold: 0 });\n io.observe(containerEl);\n\n const handleVisibilityChange = () => {\n tabVisible = document.visibilityState !== 'hidden';\n if (tabVisible && isVisible && !contextLost && !prefersReducedMotion) {\n cancelAnimationFrame(raf);\n lastFrameTime = 0;\n raf = requestAnimationFrame(loop);\n } else {\n cancelAnimationFrame(raf);\n }\n };\n document.addEventListener('visibilitychange', handleVisibilityChange);\n\n // Respect prefers-reduced-motion: paint one frame and stop, rather than running a perpetual animation loop for users who've asked not to see motion.\n if (prefersReducedMotion) {\n renderStaticFrame();\n } else {\n raf = requestAnimationFrame(loop);\n }\n\n return () => {\n cancelAnimationFrame(raf);\n ro.disconnect();\n io.disconnect();\n document.removeEventListener('visibilitychange', handleVisibilityChange);\n canvas.removeEventListener('webglcontextlost', handleContextLost);\n canvas.removeEventListener('webglcontextrestored', handleContextRestored);\n if (mouseInteractive && containerEl) {\n containerEl.removeEventListener('mousemove', handleMouseMove);\n }\n try {\n containerEl?.removeChild(canvas);\n } catch {}\n };\n }, [color, speed, direction, scale, opacity, mouseInteractive, renderScale, maxDpr, targetFps, iterations]);\n\n return
    ;\n};\n\nexport default Plasma;\n" } ], diff --git a/public/r/Plasma-TS-CSS.json b/public/r/Plasma-TS-CSS.json index 79c72f20..9a20bcc3 100644 --- a/public/r/Plasma-TS-CSS.json +++ b/public/r/Plasma-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Plasma/Plasma.css", + "type": "registry:file", + "path": "Plasma.css", + "target": "@components/Plasma.css", "content": ".plasma-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "Plasma/Plasma.tsx", + "path": "Plasma.tsx", "content": "import React, { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './Plasma.css';\n\ninterface PlasmaProps {\n color?: string;\n speed?: number;\n direction?: 'forward' | 'reverse' | 'pingpong';\n scale?: number;\n opacity?: number;\n mouseInteractive?: boolean;\n /** Internal render resolution multiplier. 1 = full res, 0.5 = quarter the pixels. Default 0.55. */\n renderScale?: number;\n /** Hard cap on devicePixelRatio used for rendering. Default 1.5. */\n maxDpr?: number;\n /** Target frame rate for the animation loop. Default 30. */\n targetFps?: number;\n /** Raymarch step count — lower is cheaper, less detailed. Default 60. */\n iterations?: number;\n}\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 0.5, 0.2];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst vertex = `#version 300 es\nprecision highp float;\nin vec2 position;\nin vec2 uv;\nout vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst ORIGINAL_QUALITY = 60;\n\nconst buildFragment = (iterations: number) => {\n return `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform vec3 uCustomColor;\nuniform float uUseCustomColor;\nuniform float uSpeed;\nuniform float uDirection;\nuniform float uScale;\nuniform float uOpacity;\nuniform vec2 uMouse;\nuniform float uMouseInteractive;\nuniform float uQuality;\nuniform float uStepScale;\nout vec4 fragColor;\n\nvoid mainImage(out vec4 o, vec2 C) {\n vec2 center = iResolution.xy * 0.5;\n C = (C - center) / uScale + center;\n \n vec2 mouseOffset = (uMouse - center) * 0.0002;\n C += mouseOffset * length(C - center) * step(0.5, uMouseInteractive);\n \n float i, d, z, T = iTime * uSpeed * uDirection;\n vec3 O, p, S;\n\n for (vec2 r = iResolution.xy, Q; ++i < 60.0; O += o.w/d*o.xyz) {\n p = z*normalize(vec3(C-.5*r,r.y)); \n p.z -= 4.; \n S = p;\n d = p.y-T;\n \n p.x += .4*(1.+p.y)*sin(d + p.x*0.1)*cos(.34*d + p.x*0.05); \n Q = p.xz *= mat2(cos(p.y+vec4(0,11,33,0)-T)); \n z += d = (abs(sqrt(length(Q*Q)) - .25*(5.+S.y))/3.+8e-4) * uStepScale;\n o = 1.+sin(S.y+p.z*.5+S.z-length(S-p)+vec4(2,1,0,8));\n if (i >= uQuality) break;\n }\n \n o.xyz = tanh(O/1e4);\n}\n\nbool finite1(float x){ return !(isnan(x) || isinf(x)); }\nvec3 sanitize(vec3 c){\n return vec3(\n finite1(c.r) ? c.r : 0.0,\n finite1(c.g) ? c.g : 0.0,\n finite1(c.b) ? c.b : 0.0\n );\n}\n\nvoid main() {\n vec4 o = vec4(0.0);\n mainImage(o, gl_FragCoord.xy);\n vec3 rgb = sanitize(o.rgb);\n \n float intensity = (rgb.r + rgb.g + rgb.b) / 3.0;\n vec3 customColor = intensity * uCustomColor;\n vec3 finalColor = mix(rgb, customColor, step(0.5, uUseCustomColor));\n \n float alpha = length(rgb) * uOpacity;\n fragColor = vec4(finalColor, alpha);\n}`;\n};\n\nexport const Plasma: React.FC = ({\n color = '#ffffff',\n speed = 1,\n direction = 'forward',\n scale = 1,\n opacity = 1,\n mouseInteractive = true,\n renderScale = 0.55,\n maxDpr = 1.5,\n targetFps = 60,\n iterations = 60,\n}) => {\n const containerRef = useRef(null);\n const mousePos = useRef({ x: 0, y: 0 });\n const pendingMouse = useRef<{ x: number; y: number } | null>(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n const containerEl = containerRef.current;\n\n const prefersReducedMotion =\n typeof window !== 'undefined' &&\n window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;\n\n const useCustomColor = color ? 1.0 : 0.0;\n const customColorRgb = color ? hexToRgb(color) : [1, 1, 1];\n\n const directionMultiplier = direction === 'reverse' ? -1.0 : 1.0;\n\n let renderer: Renderer;\n try {\n renderer = new Renderer({\n webgl: 2,\n alpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, maxDpr)\n });\n } catch {\n return;\n }\n const gl = renderer.gl;\n if (!gl) return;\n const canvas = gl.canvas as HTMLCanvasElement;\n canvas.style.display = 'block';\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n // Rendering at renderScale internally, CSS stretches it back up.\n containerEl.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n\n const program = new Program(gl, {\n vertex: vertex,\n fragment: buildFragment(iterations),\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uCustomColor: { value: new Float32Array(customColorRgb) },\n uUseCustomColor: { value: useCustomColor },\n uSpeed: { value: speed * 0.4 },\n uDirection: { value: directionMultiplier },\n uScale: { value: scale },\n uOpacity: { value: opacity },\n uMouse: { value: new Float32Array([0, 0]) },\n uMouseInteractive: { value: mouseInteractive ? 1.0 : 0.0 },\n uQuality: { value: iterations },\n uStepScale: { value: ORIGINAL_QUALITY / iterations },\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n\n const handleMouseMove = (e: MouseEvent) => {\n if (!mouseInteractive) return;\n const rect = containerEl.getBoundingClientRect();\n // Store the latest position but don't touch GL state here, the rAF loop picks it up once per rendered frame instead of once per mouse event.\n pendingMouse.current = {\n x: e.clientX - rect.left,\n y: e.clientY - rect.top,\n };\n };\n\n if (mouseInteractive) {\n containerEl.addEventListener('mousemove', handleMouseMove, { passive: true });\n }\n\n let resizePending = false;\n const setSize = () => {\n const rect = containerEl.getBoundingClientRect();\n const width = Math.max(1, Math.floor(rect.width * renderScale));\n const height = Math.max(1, Math.floor(rect.height * renderScale));\n renderer.setSize(width, height);\n\n // renderer.setSize also sets canvas.style.width/height to match the (scaled-down) drawing buffer - override that so the canvas still stretches to fill its container via CSS while the buffer stays small.\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n\n const res = program.uniforms.iResolution.value as Float32Array;\n res[0] = gl.drawingBufferWidth;\n res[1] = gl.drawingBufferHeight;\n };\n\n const ro = new ResizeObserver(() => {\n // Batch rapid resize events (ex. during a window drag) into one setSize per frame.\n if (resizePending) return;\n resizePending = true;\n requestAnimationFrame(() => {\n resizePending = false;\n setSize();\n });\n });\n ro.observe(containerEl);\n setSize();\n\n let raf = 0;\n let contextLost = false;\n let isVisible = true;\n let tabVisible = document.visibilityState !== 'hidden';\n const t0 = performance.now();\n const frameInterval = 1000 / targetFps;\n let lastFrameTime = 0;\n\n const renderStaticFrame = () => {\n (program.uniforms.iTime as any).value = 0;\n renderer.render({ scene: mesh });\n };\n\n const loop = (t: number) => {\n if (contextLost || !isVisible || !tabVisible) return;\n\n if (t - lastFrameTime < frameInterval) {\n raf = requestAnimationFrame(loop);\n return;\n }\n lastFrameTime = t;\n\n if (pendingMouse.current) {\n mousePos.current = pendingMouse.current;\n pendingMouse.current = null;\n const mouseUniform = program.uniforms.uMouse.value as Float32Array;\n mouseUniform[0] = mousePos.current.x;\n mouseUniform[1] = mousePos.current.y;\n }\n\n let timeValue = (t - t0) * 0.001;\n if (direction === 'pingpong') {\n const pingpongDuration = 10;\n const segmentTime = timeValue % pingpongDuration;\n const isForward = Math.floor(timeValue / pingpongDuration) % 2 === 0;\n const u = segmentTime / pingpongDuration;\n const smooth = u * u * (3 - 2 * u);\n const pingpongTime = isForward ? smooth * pingpongDuration : (1 - smooth) * pingpongDuration;\n (program.uniforms.uDirection as any).value = 1.0;\n (program.uniforms.iTime as any).value = pingpongTime;\n } else {\n (program.uniforms.iTime as any).value = timeValue;\n }\n renderer.render({ scene: mesh });\n raf = requestAnimationFrame(loop);\n };\n\n const handleContextLost = (e: Event) => {\n e.preventDefault();\n contextLost = true;\n cancelAnimationFrame(raf);\n };\n const handleContextRestored = () => {\n contextLost = false;\n if (isVisible && tabVisible && !prefersReducedMotion) {\n cancelAnimationFrame(raf);\n raf = requestAnimationFrame(loop);\n }\n };\n canvas.addEventListener('webglcontextlost', handleContextLost);\n canvas.addEventListener('webglcontextrestored', handleContextRestored);\n\n const io = new IntersectionObserver(([entry]) => {\n const wasVisible = isVisible;\n isVisible = entry.isIntersecting;\n if (isVisible && !wasVisible && !contextLost && tabVisible && !prefersReducedMotion) {\n cancelAnimationFrame(raf);\n raf = requestAnimationFrame(loop);\n }\n }, { threshold: 0 });\n io.observe(containerEl);\n\n const handleVisibilityChange = () => {\n tabVisible = document.visibilityState !== 'hidden';\n if (tabVisible && isVisible && !contextLost && !prefersReducedMotion) {\n cancelAnimationFrame(raf);\n lastFrameTime = 0;\n raf = requestAnimationFrame(loop);\n } else {\n cancelAnimationFrame(raf);\n }\n };\n document.addEventListener('visibilitychange', handleVisibilityChange);\n\n // Respect prefers-reduced-motion: paint one frame and stop, rather than running a perpetual animation loop for users who've asked not to see motion.\n if (prefersReducedMotion) {\n renderStaticFrame();\n } else {\n raf = requestAnimationFrame(loop);\n }\n\n return () => {\n cancelAnimationFrame(raf);\n ro.disconnect();\n io.disconnect();\n document.removeEventListener('visibilitychange', handleVisibilityChange);\n canvas.removeEventListener('webglcontextlost', handleContextLost);\n canvas.removeEventListener('webglcontextrestored', handleContextRestored);\n if (mouseInteractive && containerEl) {\n containerEl.removeEventListener('mousemove', handleMouseMove);\n }\n try {\n containerEl?.removeChild(canvas);\n } catch {}\n };\n }, [color, speed, direction, scale, opacity, mouseInteractive, renderScale, maxDpr, targetFps, iterations]);\n\n return
    ;\n};\n\nexport default Plasma;\n" } ], diff --git a/public/r/PlasmaWave-JS-CSS.json b/public/r/PlasmaWave-JS-CSS.json index 32e4073c..5b0dae5d 100644 --- a/public/r/PlasmaWave-JS-CSS.json +++ b/public/r/PlasmaWave-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "PlasmaWave/PlasmaWave.css", + "type": "registry:file", + "path": "PlasmaWave.css", + "target": "@components/PlasmaWave.css", "content": ".plasma-wave-container {\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "PlasmaWave/PlasmaWave.jsx", + "path": "PlasmaWave.jsx", "content": "import { useRef, useEffect } from 'react';\nimport { Renderer, Camera, Transform, Program, Mesh, Geometry } from 'ogl';\n\nimport './PlasmaWave.css';\n\nfunction hexToRgb(hex) {\n const r = parseInt(hex.slice(1, 3), 16) / 255;\n const g = parseInt(hex.slice(3, 5), 16) / 255;\n const b = parseInt(hex.slice(5, 7), 16) / 255;\n return [r, g, b];\n}\n\nconst VERT = /* glsl */ `\nattribute vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst FRAG = /* glsl */ `\nprecision mediump float;\nuniform float iTime;\nuniform vec2 iResolution;\nuniform vec2 uOffset;\nuniform float uRotation;\nuniform float uFocalLength;\nuniform float uSpeed1;\nuniform float uSpeed2;\nuniform float uDir2;\nuniform float uBend1;\nuniform float uBend2;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\n\nconst float lt = 0.3;\nconst float pi = 3.14159;\nconst float pi2 = 6.28318;\nconst float pi_2 = 1.5708;\n#define MAX_STEPS 14\n\nvoid mainImage(out vec4 C, in vec2 U) {\n float t = iTime * pi;\n float s = 1.0;\n float d = 0.0;\n vec2 R = iResolution;\n\n vec3 o = vec3(0.0, 0.0, -7.0);\n vec3 u = normalize(vec3((U - 0.5 * R) / R.y, uFocalLength));\n vec2 k = vec2(0.0);\n vec3 p;\n\n float t1 = t * 0.7;\n float t2 = t * 0.9;\n float tSpeed1 = t * uSpeed1;\n float tSpeed2 = t * uSpeed2 * uDir2;\n\n for (int i = 0; i < MAX_STEPS; ++i) {\n p = o + u * d;\n p.x -= 15.0;\n\n float px = p.x;\n float wob1 = uBend1 + sin(t1 + px * 0.8) * 0.1;\n float wob2 = uBend2 + cos(t2 + px * 1.1) * 0.1;\n\n float px2 = px + pi_2;\n vec2 sinOffset = sin(vec2(px, px2) + tSpeed1) * wob1;\n vec2 cosOffset = cos(vec2(px, px2) + tSpeed2) * wob2;\n\n vec2 yz = p.yz;\n float pxLt = px + lt;\n k.x = max(pxLt, length(yz - sinOffset) - lt);\n k.y = max(pxLt, length(yz - cosOffset) - lt);\n\n float current = min(k.x, k.y);\n s = min(s, current);\n if (s < 0.001 || d > 300.0) break;\n d += s * 0.7;\n }\n\n float sqrtD = sqrt(d);\n vec3 raw = max(cos(d * pi2) - s * sqrtD - vec3(k, 0.0), 0.0);\n raw.gb += 0.1;\n float maxC = max(raw.r, max(raw.g, raw.b));\n if (maxC < 0.15) discard;\n raw = raw * 0.4 + raw.brg * 0.6 + raw * raw;\n float lum = dot(raw, vec3(0.299, 0.587, 0.114));\n float w1 = max(0.0, 1.0 - k.x * 2.0);\n float w2 = max(0.0, 1.0 - k.y * 2.0);\n float wt = w1 + w2 + 0.001;\n vec3 c = (uColor1 * w1 + uColor2 * w2) / wt * lum * 3.5;\n C = vec4(c, 1.0);\n}\n\nvoid main() {\n vec2 coord = gl_FragCoord.xy + uOffset;\n coord -= 0.5 * iResolution;\n float c = cos(uRotation), s = sin(uRotation);\n coord = mat2(c, -s, s, c) * coord;\n coord += 0.5 * iResolution;\n\n vec4 color;\n mainImage(color, coord);\n gl_FragColor = color;\n}\n`;\n\nexport default function PlasmaWave(props) {\n const {\n xOffset = 0,\n yOffset = 0,\n rotationDeg = 0,\n focalLength = 0.8,\n speed1 = 0.05,\n speed2 = 0.05,\n dir2 = 1.0,\n bend1 = 1,\n bend2 = 0.5,\n colors = ['#A855F7', '#06B6D4']\n } = props;\n\n const propsRef = useRef(props);\n propsRef.current = props;\n\n const containerRef = useRef(null);\n\n useEffect(() => {\n const ctn = containerRef.current;\n if (!ctn) return;\n\n const renderer = new Renderer({\n alpha: true,\n dpr: Math.min(window.devicePixelRatio, 1.5),\n antialias: false,\n depth: false,\n stencil: false,\n premultipliedAlpha: false,\n preserveDrawingBuffer: false,\n powerPreference: 'high-performance'\n });\n\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n ctn.appendChild(gl.canvas);\n\n const camera = new Camera(gl);\n const scene = new Transform();\n\n const geometry = new Geometry(gl, {\n position: { size: 2, data: new Float32Array([-1, -1, 3, -1, -1, 3]) }\n });\n\n const uniformOffset = new Float32Array([xOffset, yOffset]);\n const uniformResolution = new Float32Array([1, 1]);\n const c1 = hexToRgb(colors[0]);\n const c2 = hexToRgb(colors[1]);\n\n const program = new Program(gl, {\n vertex: VERT,\n fragment: FRAG,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: uniformResolution },\n uOffset: { value: uniformOffset },\n uRotation: { value: (rotationDeg * Math.PI) / 180 },\n uFocalLength: { value: focalLength },\n uSpeed1: { value: speed1 },\n uSpeed2: { value: speed2 },\n uDir2: { value: dir2 },\n uBend1: { value: bend1 },\n uBend2: { value: bend2 },\n uColor1: { value: c1 },\n uColor2: { value: c2 }\n }\n });\n\n new Mesh(gl, { geometry, program }).setParent(scene);\n\n function resize() {\n if (!ctn) return;\n const { width, height } = ctn.getBoundingClientRect();\n renderer.setSize(width, height);\n uniformResolution[0] = width * renderer.dpr;\n uniformResolution[1] = height * renderer.dpr;\n gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);\n }\n\n const ro = new ResizeObserver(resize);\n ro.observe(ctn);\n resize();\n\n const startTime = performance.now();\n let animateId;\n\n const update = (now) => {\n const {\n xOffset: xOff = 0,\n yOffset: yOff = 0,\n rotationDeg: rot = 0,\n focalLength: fLen = 0.8,\n speed1: s1 = 0.05,\n speed2: s2 = 0.05,\n dir2: d2 = 1.0,\n bend1: b1 = 1,\n bend2: b2 = 0.5,\n colors: cols = ['#A855F7', '#06B6D4']\n } = propsRef.current;\n\n uniformOffset[0] = xOff;\n uniformOffset[1] = yOff;\n program.uniforms.iTime.value = (now - startTime) * 0.001;\n program.uniforms.uRotation.value = (rot * Math.PI) / 180;\n program.uniforms.uFocalLength.value = fLen;\n program.uniforms.uSpeed1.value = s1;\n program.uniforms.uSpeed2.value = s2;\n program.uniforms.uDir2.value = d2;\n program.uniforms.uBend1.value = b1;\n program.uniforms.uBend2.value = b2;\n program.uniforms.uColor1.value = hexToRgb(cols[0]);\n program.uniforms.uColor2.value = hexToRgb(cols[1]);\n\n renderer.render({ scene, camera });\n animateId = requestAnimationFrame(update);\n };\n\n animateId = requestAnimationFrame(update);\n\n return () => {\n cancelAnimationFrame(animateId);\n ro.disconnect();\n if (ctn && gl.canvas.parentNode === ctn) {\n ctn.removeChild(gl.canvas);\n }\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return
    ;\n}\n" } ], diff --git a/public/r/PlasmaWave-TS-CSS.json b/public/r/PlasmaWave-TS-CSS.json index bccb8478..694add7f 100644 --- a/public/r/PlasmaWave-TS-CSS.json +++ b/public/r/PlasmaWave-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "PlasmaWave/PlasmaWave.css", + "type": "registry:file", + "path": "PlasmaWave.css", + "target": "@components/PlasmaWave.css", "content": ".plasma-wave-container {\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "PlasmaWave/PlasmaWave.tsx", + "path": "PlasmaWave.tsx", "content": "import { useRef, useEffect } from 'react';\nimport { Renderer, Camera, Transform, Program, Mesh, Geometry } from 'ogl';\n\nimport './PlasmaWave.css';\n\nfunction hexToRgb(hex: string): [number, number, number] {\n const r = parseInt(hex.slice(1, 3), 16) / 255;\n const g = parseInt(hex.slice(3, 5), 16) / 255;\n const b = parseInt(hex.slice(5, 7), 16) / 255;\n return [r, g, b];\n}\n\nconst VERT = /* glsl */ `\nattribute vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst FRAG = /* glsl */ `\nprecision mediump float;\nuniform float iTime;\nuniform vec2 iResolution;\nuniform vec2 uOffset;\nuniform float uRotation;\nuniform float uFocalLength;\nuniform float uSpeed1;\nuniform float uSpeed2;\nuniform float uDir2;\nuniform float uBend1;\nuniform float uBend2;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\n\nconst float lt = 0.3;\nconst float pi = 3.14159;\nconst float pi2 = 6.28318;\nconst float pi_2 = 1.5708;\n#define MAX_STEPS 14\n\nvoid mainImage(out vec4 C, in vec2 U) {\n float t = iTime * pi;\n float s = 1.0;\n float d = 0.0;\n vec2 R = iResolution;\n\n vec3 o = vec3(0.0, 0.0, -7.0);\n vec3 u = normalize(vec3((U - 0.5 * R) / R.y, uFocalLength));\n vec2 k = vec2(0.0);\n vec3 p;\n\n float t1 = t * 0.7;\n float t2 = t * 0.9;\n float tSpeed1 = t * uSpeed1;\n float tSpeed2 = t * uSpeed2 * uDir2;\n\n for (int i = 0; i < MAX_STEPS; ++i) {\n p = o + u * d;\n p.x -= 15.0;\n\n float px = p.x;\n float wob1 = uBend1 + sin(t1 + px * 0.8) * 0.1;\n float wob2 = uBend2 + cos(t2 + px * 1.1) * 0.1;\n\n float px2 = px + pi_2;\n vec2 sinOffset = sin(vec2(px, px2) + tSpeed1) * wob1;\n vec2 cosOffset = cos(vec2(px, px2) + tSpeed2) * wob2;\n\n vec2 yz = p.yz;\n float pxLt = px + lt;\n k.x = max(pxLt, length(yz - sinOffset) - lt);\n k.y = max(pxLt, length(yz - cosOffset) - lt);\n\n float current = min(k.x, k.y);\n s = min(s, current);\n if (s < 0.001 || d > 300.0) break;\n d += s * 0.7;\n }\n\n float sqrtD = sqrt(d);\n vec3 raw = max(cos(d * pi2) - s * sqrtD - vec3(k, 0.0), 0.0);\n raw.gb += 0.1;\n float maxC = max(raw.r, max(raw.g, raw.b));\n if (maxC < 0.15) discard;\n raw = raw * 0.4 + raw.brg * 0.6 + raw * raw;\n float lum = dot(raw, vec3(0.299, 0.587, 0.114));\n float w1 = max(0.0, 1.0 - k.x * 2.0);\n float w2 = max(0.0, 1.0 - k.y * 2.0);\n float wt = w1 + w2 + 0.001;\n vec3 c = (uColor1 * w1 + uColor2 * w2) / wt * lum * 3.5;\n C = vec4(c, 1.0);\n}\n\nvoid main() {\n vec2 coord = gl_FragCoord.xy + uOffset;\n coord -= 0.5 * iResolution;\n float c = cos(uRotation), s = sin(uRotation);\n coord = mat2(c, -s, s, c) * coord;\n coord += 0.5 * iResolution;\n\n vec4 color;\n mainImage(color, coord);\n gl_FragColor = color;\n}\n`;\n\ninterface PlasmaWaveProps {\n xOffset?: number;\n yOffset?: number;\n rotationDeg?: number;\n focalLength?: number;\n speed1?: number;\n speed2?: number;\n dir2?: number;\n bend1?: number;\n bend2?: number;\n colors?: [string, string];\n}\n\nexport default function PlasmaWave(props: PlasmaWaveProps) {\n const {\n xOffset = 0,\n yOffset = 0,\n rotationDeg = 0,\n focalLength = 0.8,\n speed1 = 0.05,\n speed2 = 0.05,\n dir2 = 1.0,\n bend1 = 1,\n bend2 = 0.5,\n colors = ['#A855F7', '#06B6D4']\n } = props;\n\n const propsRef = useRef(props);\n propsRef.current = props;\n\n const containerRef = useRef(null);\n\n useEffect(() => {\n const ctn = containerRef.current;\n if (!ctn) return;\n\n const renderer = new Renderer({\n alpha: true,\n dpr: Math.min(window.devicePixelRatio, 1.5),\n antialias: false,\n depth: false,\n stencil: false,\n premultipliedAlpha: false,\n preserveDrawingBuffer: false,\n powerPreference: 'high-performance'\n });\n\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n ctn.appendChild(gl.canvas);\n\n const camera = new Camera(gl);\n const scene = new Transform();\n\n const geometry = new Geometry(gl, {\n position: { size: 2, data: new Float32Array([-1, -1, 3, -1, -1, 3]) }\n });\n\n const uniformOffset = new Float32Array([xOffset, yOffset]);\n const uniformResolution = new Float32Array([1, 1]);\n const c1 = hexToRgb(colors[0]);\n const c2 = hexToRgb(colors[1]);\n\n const program = new Program(gl, {\n vertex: VERT,\n fragment: FRAG,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: uniformResolution },\n uOffset: { value: uniformOffset },\n uRotation: { value: (rotationDeg * Math.PI) / 180 },\n uFocalLength: { value: focalLength },\n uSpeed1: { value: speed1 },\n uSpeed2: { value: speed2 },\n uDir2: { value: dir2 },\n uBend1: { value: bend1 },\n uBend2: { value: bend2 },\n uColor1: { value: c1 },\n uColor2: { value: c2 }\n }\n });\n\n new Mesh(gl, { geometry, program }).setParent(scene);\n\n function resize() {\n if (!ctn) return;\n const { width, height } = ctn.getBoundingClientRect();\n renderer.setSize(width, height);\n uniformResolution[0] = width * renderer.dpr;\n uniformResolution[1] = height * renderer.dpr;\n gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);\n }\n\n const ro = new ResizeObserver(resize);\n ro.observe(ctn);\n resize();\n\n const startTime = performance.now();\n let animateId: number;\n\n const update = (now: number) => {\n const {\n xOffset: xOff = 0,\n yOffset: yOff = 0,\n rotationDeg: rot = 0,\n focalLength: fLen = 0.8,\n speed1: s1 = 0.05,\n speed2: s2 = 0.05,\n dir2: d2 = 1.0,\n bend1: b1 = 1,\n bend2: b2 = 0.5,\n colors: cols = ['#A855F7', '#06B6D4']\n } = propsRef.current;\n\n uniformOffset[0] = xOff;\n uniformOffset[1] = yOff;\n program.uniforms.iTime.value = (now - startTime) * 0.001;\n program.uniforms.uRotation.value = (rot * Math.PI) / 180;\n program.uniforms.uFocalLength.value = fLen;\n program.uniforms.uSpeed1.value = s1;\n program.uniforms.uSpeed2.value = s2;\n program.uniforms.uDir2.value = d2;\n program.uniforms.uBend1.value = b1;\n program.uniforms.uBend2.value = b2;\n program.uniforms.uColor1.value = hexToRgb(cols[0]);\n program.uniforms.uColor2.value = hexToRgb(cols[1]);\n\n renderer.render({ scene, camera });\n animateId = requestAnimationFrame(update);\n };\n\n animateId = requestAnimationFrame(update);\n\n return () => {\n cancelAnimationFrame(animateId);\n ro.disconnect();\n if (ctn && gl.canvas.parentNode === ctn) {\n ctn.removeChild(gl.canvas);\n }\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, []);\n\n return
    ;\n}\n" } ], diff --git a/public/r/Prism-JS-CSS.json b/public/r/Prism-JS-CSS.json index a53c5cf1..cf2cb3b6 100644 --- a/public/r/Prism-JS-CSS.json +++ b/public/r/Prism-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Prism/Prism.css", + "type": "registry:file", + "path": "Prism.css", + "target": "@components/Prism.css", "content": ".prism-container {\n position: relative;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "Prism/Prism.jsx", + "path": "Prism.jsx", "content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Triangle, Program, Mesh } from 'ogl';\nimport './Prism.css';\n\nconst Prism = ({\n height = 3.5,\n baseWidth = 5.5,\n animationType = 'rotate',\n glow = 1,\n offset = { x: 0, y: 0 },\n noise = 0.5,\n transparent = true,\n scale = 3.6,\n hueShift = 0,\n colorFrequency = 1,\n hoverStrength = 2,\n inertia = 0.05,\n bloom = 1,\n suspendWhenOffscreen = false,\n timeScale = 0.5\n}) => {\n const containerRef = useRef(null);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const H = Math.max(0.001, height);\n const BW = Math.max(0.001, baseWidth);\n const BASE_HALF = BW * 0.5;\n const GLOW = Math.max(0.0, glow);\n const NOISE = Math.max(0.0, noise);\n const offX = offset?.x ?? 0;\n const offY = offset?.y ?? 0;\n const SAT = transparent ? 1.5 : 1;\n const SCALE = Math.max(0.001, scale);\n const HUE = hueShift || 0;\n const CFREQ = Math.max(0.0, colorFrequency || 1);\n const BLOOM = Math.max(0.0, bloom || 1);\n const RSX = 1;\n const RSY = 1;\n const RSZ = 1;\n const TS = Math.max(0, timeScale || 1);\n const HOVSTR = Math.max(0, hoverStrength || 1);\n const INERT = Math.max(0, Math.min(1, inertia || 0.12));\n\n const dpr = Math.min(2, window.devicePixelRatio || 1);\n const renderer = new Renderer({\n dpr,\n alpha: transparent,\n antialias: false\n });\n const gl = renderer.gl;\n gl.disable(gl.DEPTH_TEST);\n gl.disable(gl.CULL_FACE);\n gl.disable(gl.BLEND);\n\n Object.assign(gl.canvas.style, {\n position: 'absolute',\n inset: '0',\n width: '100%',\n height: '100%',\n display: 'block'\n });\n container.appendChild(gl.canvas);\n\n const vertex = /* glsl */ `\n attribute vec2 position;\n void main() {\n gl_Position = vec4(position, 0.0, 1.0);\n }\n `;\n\n const fragment = /* glsl */ `\n precision highp float;\n\n uniform vec2 iResolution;\n uniform float iTime;\n\n uniform float uHeight;\n uniform float uBaseHalf;\n uniform mat3 uRot;\n uniform int uUseBaseWobble;\n uniform float uGlow;\n uniform vec2 uOffsetPx;\n uniform float uNoise;\n uniform float uSaturation;\n uniform float uScale;\n uniform float uHueShift;\n uniform float uColorFreq;\n uniform float uBloom;\n uniform float uCenterShift;\n uniform float uInvBaseHalf;\n uniform float uInvHeight;\n uniform float uMinAxis;\n uniform float uPxScale;\n uniform float uTimeScale;\n\n vec4 tanh4(vec4 x){\n vec4 e2x = exp(2.0*x);\n return (e2x - 1.0) / (e2x + 1.0);\n }\n\n float rand(vec2 co){\n return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453123);\n }\n\n float sdOctaAnisoInv(vec3 p){\n vec3 q = vec3(abs(p.x) * uInvBaseHalf, abs(p.y) * uInvHeight, abs(p.z) * uInvBaseHalf);\n float m = q.x + q.y + q.z - 1.0;\n return m * uMinAxis * 0.5773502691896258;\n }\n\n float sdPyramidUpInv(vec3 p){\n float oct = sdOctaAnisoInv(p);\n float halfSpace = -p.y;\n return max(oct, halfSpace);\n }\n\n mat3 hueRotation(float a){\n float c = cos(a), s = sin(a);\n mat3 W = mat3(\n 0.299, 0.587, 0.114,\n 0.299, 0.587, 0.114,\n 0.299, 0.587, 0.114\n );\n mat3 U = mat3(\n 0.701, -0.587, -0.114,\n -0.299, 0.413, -0.114,\n -0.300, -0.588, 0.886\n );\n mat3 V = mat3(\n 0.168, -0.331, 0.500,\n 0.328, 0.035, -0.500,\n -0.497, 0.296, 0.201\n );\n return W + U * c + V * s;\n }\n\n void main(){\n vec2 f = (gl_FragCoord.xy - 0.5 * iResolution.xy - uOffsetPx) * uPxScale;\n\n float z = 5.0;\n float d = 0.0;\n\n vec3 p;\n vec4 o = vec4(0.0);\n\n float centerShift = uCenterShift;\n float cf = uColorFreq;\n\n mat2 wob = mat2(1.0);\n if (uUseBaseWobble == 1) {\n float t = iTime * uTimeScale;\n float c0 = cos(t + 0.0);\n float c1 = cos(t + 33.0);\n float c2 = cos(t + 11.0);\n wob = mat2(c0, c1, c2, c0);\n }\n\n const int STEPS = 100;\n for (int i = 0; i < STEPS; i++) {\n p = vec3(f, z);\n p.xz = p.xz * wob;\n p = uRot * p;\n vec3 q = p;\n q.y += centerShift;\n d = 0.1 + 0.2 * abs(sdPyramidUpInv(q));\n z -= d;\n o += (sin((p.y + z) * cf + vec4(0.0, 1.0, 2.0, 3.0)) + 1.0) / d;\n }\n\n o = tanh4(o * o * (uGlow * uBloom) / 1e5);\n\n vec3 col = o.rgb;\n float n = rand(gl_FragCoord.xy + vec2(iTime));\n col += (n - 0.5) * uNoise;\n col = clamp(col, 0.0, 1.0);\n\n float L = dot(col, vec3(0.2126, 0.7152, 0.0722));\n col = clamp(mix(vec3(L), col, uSaturation), 0.0, 1.0);\n\n if(abs(uHueShift) > 0.0001){\n col = clamp(hueRotation(uHueShift) * col, 0.0, 1.0);\n }\n\n gl_FragColor = vec4(col, o.a);\n }\n `;\n\n const geometry = new Triangle(gl);\n const iResBuf = new Float32Array(2);\n const offsetPxBuf = new Float32Array(2);\n\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iResolution: { value: iResBuf },\n iTime: { value: 0 },\n uHeight: { value: H },\n uBaseHalf: { value: BASE_HALF },\n uUseBaseWobble: { value: 1 },\n uRot: { value: new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]) },\n uGlow: { value: GLOW },\n uOffsetPx: { value: offsetPxBuf },\n uNoise: { value: NOISE },\n uSaturation: { value: SAT },\n uScale: { value: SCALE },\n uHueShift: { value: HUE },\n uColorFreq: { value: CFREQ },\n uBloom: { value: BLOOM },\n uCenterShift: { value: H * 0.25 },\n uInvBaseHalf: { value: 1 / BASE_HALF },\n uInvHeight: { value: 1 / H },\n uMinAxis: { value: Math.min(BASE_HALF, H) },\n uPxScale: {\n value: 1 / ((gl.drawingBufferHeight || 1) * 0.1 * SCALE)\n },\n uTimeScale: { value: TS }\n }\n });\n const mesh = new Mesh(gl, { geometry, program });\n\n const resize = () => {\n const w = container.clientWidth || 1;\n const h = container.clientHeight || 1;\n renderer.setSize(w, h);\n iResBuf[0] = gl.drawingBufferWidth;\n iResBuf[1] = gl.drawingBufferHeight;\n offsetPxBuf[0] = offX * dpr;\n offsetPxBuf[1] = offY * dpr;\n program.uniforms.uPxScale.value = 1 / ((gl.drawingBufferHeight || 1) * 0.1 * SCALE);\n };\n const ro = new ResizeObserver(resize);\n ro.observe(container);\n resize();\n\n const rotBuf = new Float32Array(9);\n const setMat3FromEuler = (yawY, pitchX, rollZ, out) => {\n const cy = Math.cos(yawY),\n sy = Math.sin(yawY);\n const cx = Math.cos(pitchX),\n sx = Math.sin(pitchX);\n const cz = Math.cos(rollZ),\n sz = Math.sin(rollZ);\n const r00 = cy * cz + sy * sx * sz;\n const r01 = -cy * sz + sy * sx * cz;\n const r02 = sy * cx;\n\n const r10 = cx * sz;\n const r11 = cx * cz;\n const r12 = -sx;\n\n const r20 = -sy * cz + cy * sx * sz;\n const r21 = sy * sz + cy * sx * cz;\n const r22 = cy * cx;\n\n out[0] = r00;\n out[1] = r10;\n out[2] = r20;\n out[3] = r01;\n out[4] = r11;\n out[5] = r21;\n out[6] = r02;\n out[7] = r12;\n out[8] = r22;\n return out;\n };\n\n const NOISE_IS_ZERO = NOISE < 1e-6;\n let raf = 0;\n const t0 = performance.now();\n const startRAF = () => {\n if (raf) return;\n raf = requestAnimationFrame(render);\n };\n const stopRAF = () => {\n if (!raf) return;\n cancelAnimationFrame(raf);\n raf = 0;\n };\n\n const rnd = () => Math.random();\n const wX = (0.3 + rnd() * 0.6) * RSX;\n const wY = (0.2 + rnd() * 0.7) * RSY;\n const wZ = (0.1 + rnd() * 0.5) * RSZ;\n const phX = rnd() * Math.PI * 2;\n const phZ = rnd() * Math.PI * 2;\n\n let yaw = 0,\n pitch = 0,\n roll = 0;\n let targetYaw = 0,\n targetPitch = 0;\n const lerp = (a, b, t) => a + (b - a) * t;\n\n const pointer = { x: 0, y: 0, inside: true };\n const onMove = e => {\n const ww = Math.max(1, window.innerWidth);\n const wh = Math.max(1, window.innerHeight);\n const cx = ww * 0.5;\n const cy = wh * 0.5;\n const nx = (e.clientX - cx) / (ww * 0.5);\n const ny = (e.clientY - cy) / (wh * 0.5);\n pointer.x = Math.max(-1, Math.min(1, nx));\n pointer.y = Math.max(-1, Math.min(1, ny));\n pointer.inside = true;\n };\n const onLeave = () => {\n pointer.inside = false;\n };\n const onBlur = () => {\n pointer.inside = false;\n };\n\n let onPointerMove = null;\n if (animationType === 'hover') {\n onPointerMove = e => {\n onMove(e);\n startRAF();\n };\n window.addEventListener('pointermove', onPointerMove, { passive: true });\n window.addEventListener('mouseleave', onLeave);\n window.addEventListener('blur', onBlur);\n program.uniforms.uUseBaseWobble.value = 0;\n } else if (animationType === '3drotate') {\n program.uniforms.uUseBaseWobble.value = 0;\n } else {\n program.uniforms.uUseBaseWobble.value = 1;\n }\n\n const render = t => {\n const time = (t - t0) * 0.001;\n program.uniforms.iTime.value = time;\n\n let continueRAF = true;\n\n if (animationType === 'hover') {\n const maxPitch = 0.6 * HOVSTR;\n const maxYaw = 0.6 * HOVSTR;\n targetYaw = (pointer.inside ? -pointer.x : 0) * maxYaw;\n targetPitch = (pointer.inside ? pointer.y : 0) * maxPitch;\n const prevYaw = yaw;\n const prevPitch = pitch;\n const prevRoll = roll;\n yaw = lerp(prevYaw, targetYaw, INERT);\n pitch = lerp(prevPitch, targetPitch, INERT);\n roll = lerp(prevRoll, 0, 0.1);\n program.uniforms.uRot.value = setMat3FromEuler(yaw, pitch, roll, rotBuf);\n\n if (NOISE_IS_ZERO) {\n const settled =\n Math.abs(yaw - targetYaw) < 1e-4 && Math.abs(pitch - targetPitch) < 1e-4 && Math.abs(roll) < 1e-4;\n if (settled) continueRAF = false;\n }\n } else if (animationType === '3drotate') {\n const tScaled = time * TS;\n yaw = tScaled * wY;\n pitch = Math.sin(tScaled * wX + phX) * 0.6;\n roll = Math.sin(tScaled * wZ + phZ) * 0.5;\n program.uniforms.uRot.value = setMat3FromEuler(yaw, pitch, roll, rotBuf);\n if (TS < 1e-6) continueRAF = false;\n } else {\n rotBuf[0] = 1;\n rotBuf[1] = 0;\n rotBuf[2] = 0;\n rotBuf[3] = 0;\n rotBuf[4] = 1;\n rotBuf[5] = 0;\n rotBuf[6] = 0;\n rotBuf[7] = 0;\n rotBuf[8] = 1;\n program.uniforms.uRot.value = rotBuf;\n if (TS < 1e-6) continueRAF = false;\n }\n\n renderer.render({ scene: mesh });\n if (continueRAF) {\n raf = requestAnimationFrame(render);\n } else {\n raf = 0;\n }\n };\n\n if (suspendWhenOffscreen) {\n const io = new IntersectionObserver(entries => {\n const vis = entries.some(e => e.isIntersecting);\n if (vis) startRAF();\n else stopRAF();\n });\n io.observe(container);\n startRAF();\n container.__prismIO = io;\n } else {\n startRAF();\n }\n\n return () => {\n stopRAF();\n ro.disconnect();\n if (animationType === 'hover') {\n if (onPointerMove) window.removeEventListener('pointermove', onPointerMove);\n window.removeEventListener('mouseleave', onLeave);\n window.removeEventListener('blur', onBlur);\n }\n if (suspendWhenOffscreen) {\n const io = container.__prismIO;\n if (io) io.disconnect();\n delete container.__prismIO;\n }\n if (gl.canvas.parentElement === container) container.removeChild(gl.canvas);\n };\n }, [\n height,\n baseWidth,\n animationType,\n glow,\n noise,\n offset?.x,\n offset?.y,\n scale,\n transparent,\n hueShift,\n colorFrequency,\n timeScale,\n hoverStrength,\n inertia,\n bloom,\n suspendWhenOffscreen\n ]);\n\n return
    ;\n};\n\nexport default Prism;\n" } ], diff --git a/public/r/Prism-TS-CSS.json b/public/r/Prism-TS-CSS.json index 06a9c9d1..2e02dd37 100644 --- a/public/r/Prism-TS-CSS.json +++ b/public/r/Prism-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Prism/Prism.css", + "type": "registry:file", + "path": "Prism.css", + "target": "@components/Prism.css", "content": ".prism-container {\n position: relative;\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "Prism/Prism.tsx", + "path": "Prism.tsx", "content": "import React, { useEffect, useRef } from 'react';\nimport { Renderer, Triangle, Program, Mesh } from 'ogl';\nimport './Prism.css';\n\ntype PrismProps = {\n height?: number;\n baseWidth?: number;\n animationType?: 'rotate' | 'hover' | '3drotate';\n glow?: number;\n offset?: { x?: number; y?: number };\n noise?: number;\n transparent?: boolean;\n scale?: number;\n hueShift?: number;\n colorFrequency?: number;\n hoverStrength?: number;\n inertia?: number;\n bloom?: number;\n suspendWhenOffscreen?: boolean;\n timeScale?: number;\n};\n\nconst Prism: React.FC = ({\n height = 3.5,\n baseWidth = 5.5,\n animationType = 'rotate',\n glow = 1,\n offset = { x: 0, y: 0 },\n noise = 0.5,\n transparent = true,\n scale = 3.6,\n hueShift = 0,\n colorFrequency = 1,\n hoverStrength = 2,\n inertia = 0.05,\n bloom = 1,\n suspendWhenOffscreen = false,\n timeScale = 0.5\n}) => {\n const containerRef = useRef(null);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const H = Math.max(0.001, height);\n const BW = Math.max(0.001, baseWidth);\n const BASE_HALF = BW * 0.5;\n const GLOW = Math.max(0.0, glow);\n const NOISE = Math.max(0.0, noise);\n const offX = offset?.x ?? 0;\n const offY = offset?.y ?? 0;\n const SAT = transparent ? 1.5 : 1;\n const SCALE = Math.max(0.001, scale);\n const HUE = hueShift || 0;\n const CFREQ = Math.max(0.0, colorFrequency || 1);\n const BLOOM = Math.max(0.0, bloom || 1);\n const RSX = 1;\n const RSY = 1;\n const RSZ = 1;\n const TS = Math.max(0, timeScale || 1);\n const HOVSTR = Math.max(0, hoverStrength || 1);\n const INERT = Math.max(0, Math.min(1, inertia || 0.12));\n\n const dpr = Math.min(2, window.devicePixelRatio || 1);\n const renderer = new Renderer({\n dpr,\n alpha: transparent,\n antialias: false\n });\n const gl = renderer.gl;\n gl.disable(gl.DEPTH_TEST);\n gl.disable(gl.CULL_FACE);\n gl.disable(gl.BLEND);\n\n Object.assign(gl.canvas.style, {\n position: 'absolute',\n inset: '0',\n width: '100%',\n height: '100%',\n display: 'block'\n } as Partial);\n container.appendChild(gl.canvas);\n\n const vertex = /* glsl */ `\n attribute vec2 position;\n void main() {\n gl_Position = vec4(position, 0.0, 1.0);\n }\n `;\n\n const fragment = /* glsl */ `\n precision highp float;\n\n uniform vec2 iResolution;\n uniform float iTime;\n\n uniform float uHeight;\n uniform float uBaseHalf;\n uniform mat3 uRot;\n uniform int uUseBaseWobble;\n uniform float uGlow;\n uniform vec2 uOffsetPx;\n uniform float uNoise;\n uniform float uSaturation;\n uniform float uScale;\n uniform float uHueShift;\n uniform float uColorFreq;\n uniform float uBloom;\n uniform float uCenterShift;\n uniform float uInvBaseHalf;\n uniform float uInvHeight;\n uniform float uMinAxis;\n uniform float uPxScale;\n uniform float uTimeScale;\n\n vec4 tanh4(vec4 x){\n vec4 e2x = exp(2.0*x);\n return (e2x - 1.0) / (e2x + 1.0);\n }\n\n float rand(vec2 co){\n return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453123);\n }\n\n float sdOctaAnisoInv(vec3 p){\n vec3 q = vec3(abs(p.x) * uInvBaseHalf, abs(p.y) * uInvHeight, abs(p.z) * uInvBaseHalf);\n float m = q.x + q.y + q.z - 1.0;\n return m * uMinAxis * 0.5773502691896258;\n }\n\n float sdPyramidUpInv(vec3 p){\n float oct = sdOctaAnisoInv(p);\n float halfSpace = -p.y;\n return max(oct, halfSpace);\n }\n\n mat3 hueRotation(float a){\n float c = cos(a), s = sin(a);\n mat3 W = mat3(\n 0.299, 0.587, 0.114,\n 0.299, 0.587, 0.114,\n 0.299, 0.587, 0.114\n );\n mat3 U = mat3(\n 0.701, -0.587, -0.114,\n -0.299, 0.413, -0.114,\n -0.300, -0.588, 0.886\n );\n mat3 V = mat3(\n 0.168, -0.331, 0.500,\n 0.328, 0.035, -0.500,\n -0.497, 0.296, 0.201\n );\n return W + U * c + V * s;\n }\n\n void main(){\n vec2 f = (gl_FragCoord.xy - 0.5 * iResolution.xy - uOffsetPx) * uPxScale;\n\n float z = 5.0;\n float d = 0.0;\n\n vec3 p;\n vec4 o = vec4(0.0);\n\n float centerShift = uCenterShift;\n float cf = uColorFreq;\n\n mat2 wob = mat2(1.0);\n if (uUseBaseWobble == 1) {\n float t = iTime * uTimeScale;\n float c0 = cos(t + 0.0);\n float c1 = cos(t + 33.0);\n float c2 = cos(t + 11.0);\n wob = mat2(c0, c1, c2, c0);\n }\n\n const int STEPS = 100;\n for (int i = 0; i < STEPS; i++) {\n p = vec3(f, z);\n p.xz = p.xz * wob;\n p = uRot * p;\n vec3 q = p;\n q.y += centerShift;\n d = 0.1 + 0.2 * abs(sdPyramidUpInv(q));\n z -= d;\n o += (sin((p.y + z) * cf + vec4(0.0, 1.0, 2.0, 3.0)) + 1.0) / d;\n }\n\n o = tanh4(o * o * (uGlow * uBloom) / 1e5);\n\n vec3 col = o.rgb;\n float n = rand(gl_FragCoord.xy + vec2(iTime));\n col += (n - 0.5) * uNoise;\n col = clamp(col, 0.0, 1.0);\n\n float L = dot(col, vec3(0.2126, 0.7152, 0.0722));\n col = clamp(mix(vec3(L), col, uSaturation), 0.0, 1.0);\n\n if(abs(uHueShift) > 0.0001){\n col = clamp(hueRotation(uHueShift) * col, 0.0, 1.0);\n }\n\n gl_FragColor = vec4(col, o.a);\n }\n `;\n\n const geometry = new Triangle(gl);\n const iResBuf = new Float32Array(2);\n const offsetPxBuf = new Float32Array(2);\n\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iResolution: { value: iResBuf },\n iTime: { value: 0 },\n uHeight: { value: H },\n uBaseHalf: { value: BASE_HALF },\n uUseBaseWobble: { value: 1 },\n uRot: { value: new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]) },\n uGlow: { value: GLOW },\n uOffsetPx: { value: offsetPxBuf },\n uNoise: { value: NOISE },\n uSaturation: { value: SAT },\n uScale: { value: SCALE },\n uHueShift: { value: HUE },\n uColorFreq: { value: CFREQ },\n uBloom: { value: BLOOM },\n uCenterShift: { value: H * 0.25 },\n uInvBaseHalf: { value: 1 / BASE_HALF },\n uInvHeight: { value: 1 / H },\n uMinAxis: { value: Math.min(BASE_HALF, H) },\n uPxScale: {\n value: 1 / ((gl.drawingBufferHeight || 1) * 0.1 * SCALE)\n },\n uTimeScale: { value: TS }\n }\n });\n const mesh = new Mesh(gl, { geometry, program });\n\n const resize = () => {\n const w = container.clientWidth || 1;\n const h = container.clientHeight || 1;\n renderer.setSize(w, h);\n iResBuf[0] = gl.drawingBufferWidth;\n iResBuf[1] = gl.drawingBufferHeight;\n offsetPxBuf[0] = offX * dpr;\n offsetPxBuf[1] = offY * dpr;\n program.uniforms.uPxScale.value = 1 / ((gl.drawingBufferHeight || 1) * 0.1 * SCALE);\n };\n const ro = new ResizeObserver(resize);\n ro.observe(container);\n resize();\n\n const rotBuf = new Float32Array(9);\n const setMat3FromEuler = (yawY: number, pitchX: number, rollZ: number, out: Float32Array) => {\n const cy = Math.cos(yawY),\n sy = Math.sin(yawY);\n const cx = Math.cos(pitchX),\n sx = Math.sin(pitchX);\n const cz = Math.cos(rollZ),\n sz = Math.sin(rollZ);\n const r00 = cy * cz + sy * sx * sz;\n const r01 = -cy * sz + sy * sx * cz;\n const r02 = sy * cx;\n\n const r10 = cx * sz;\n const r11 = cx * cz;\n const r12 = -sx;\n\n const r20 = -sy * cz + cy * sx * sz;\n const r21 = sy * sz + cy * sx * cz;\n const r22 = cy * cx;\n\n out[0] = r00;\n out[1] = r10;\n out[2] = r20;\n out[3] = r01;\n out[4] = r11;\n out[5] = r21;\n out[6] = r02;\n out[7] = r12;\n out[8] = r22;\n return out;\n };\n\n const NOISE_IS_ZERO = NOISE < 1e-6;\n let raf = 0;\n const t0 = performance.now();\n const startRAF = () => {\n if (raf) return;\n raf = requestAnimationFrame(render);\n };\n const stopRAF = () => {\n if (!raf) return;\n cancelAnimationFrame(raf);\n raf = 0;\n };\n\n const rnd = () => Math.random();\n const wX = (0.3 + rnd() * 0.6) * RSX;\n const wY = (0.2 + rnd() * 0.7) * RSY;\n const wZ = (0.1 + rnd() * 0.5) * RSZ;\n const phX = rnd() * Math.PI * 2;\n const phZ = rnd() * Math.PI * 2;\n\n let yaw = 0,\n pitch = 0,\n roll = 0;\n let targetYaw = 0,\n targetPitch = 0;\n const lerp = (a: number, b: number, t: number) => a + (b - a) * t;\n\n const pointer = { x: 0, y: 0, inside: true };\n const onMove = (e: PointerEvent) => {\n const ww = Math.max(1, window.innerWidth);\n const wh = Math.max(1, window.innerHeight);\n const cx = ww * 0.5;\n const cy = wh * 0.5;\n const nx = (e.clientX - cx) / (ww * 0.5);\n const ny = (e.clientY - cy) / (wh * 0.5);\n pointer.x = Math.max(-1, Math.min(1, nx));\n pointer.y = Math.max(-1, Math.min(1, ny));\n pointer.inside = true;\n };\n const onLeave = () => {\n pointer.inside = false;\n };\n const onBlur = () => {\n pointer.inside = false;\n };\n\n let onPointerMove: ((e: PointerEvent) => void) | null = null;\n if (animationType === 'hover') {\n onPointerMove = (e: PointerEvent) => {\n onMove(e);\n startRAF();\n };\n window.addEventListener('pointermove', onPointerMove, { passive: true });\n window.addEventListener('mouseleave', onLeave);\n window.addEventListener('blur', onBlur);\n program.uniforms.uUseBaseWobble.value = 0;\n } else if (animationType === '3drotate') {\n program.uniforms.uUseBaseWobble.value = 0;\n } else {\n program.uniforms.uUseBaseWobble.value = 1;\n }\n\n const render = (t: number) => {\n const time = (t - t0) * 0.001;\n program.uniforms.iTime.value = time;\n\n let continueRAF = true;\n\n if (animationType === 'hover') {\n const maxPitch = 0.6 * HOVSTR;\n const maxYaw = 0.6 * HOVSTR;\n targetYaw = (pointer.inside ? -pointer.x : 0) * maxYaw;\n targetPitch = (pointer.inside ? pointer.y : 0) * maxPitch;\n const prevYaw = yaw;\n const prevPitch = pitch;\n const prevRoll = roll;\n yaw = lerp(prevYaw, targetYaw, INERT);\n pitch = lerp(prevPitch, targetPitch, INERT);\n roll = lerp(prevRoll, 0, 0.1);\n program.uniforms.uRot.value = setMat3FromEuler(yaw, pitch, roll, rotBuf);\n\n if (NOISE_IS_ZERO) {\n const settled =\n Math.abs(yaw - targetYaw) < 1e-4 && Math.abs(pitch - targetPitch) < 1e-4 && Math.abs(roll) < 1e-4;\n if (settled) continueRAF = false;\n }\n } else if (animationType === '3drotate') {\n const tScaled = time * TS;\n yaw = tScaled * wY;\n pitch = Math.sin(tScaled * wX + phX) * 0.6;\n roll = Math.sin(tScaled * wZ + phZ) * 0.5;\n program.uniforms.uRot.value = setMat3FromEuler(yaw, pitch, roll, rotBuf);\n if (TS < 1e-6) continueRAF = false;\n } else {\n rotBuf[0] = 1;\n rotBuf[1] = 0;\n rotBuf[2] = 0;\n rotBuf[3] = 0;\n rotBuf[4] = 1;\n rotBuf[5] = 0;\n rotBuf[6] = 0;\n rotBuf[7] = 0;\n rotBuf[8] = 1;\n program.uniforms.uRot.value = rotBuf;\n if (TS < 1e-6) continueRAF = false;\n }\n\n renderer.render({ scene: mesh });\n if (continueRAF) {\n raf = requestAnimationFrame(render);\n } else {\n raf = 0;\n }\n };\n\n interface PrismContainer extends HTMLElement {\n __prismIO?: IntersectionObserver;\n }\n\n if (suspendWhenOffscreen) {\n const io = new IntersectionObserver(entries => {\n const vis = entries.some(e => e.isIntersecting);\n if (vis) startRAF();\n else stopRAF();\n });\n io.observe(container);\n startRAF();\n (container as PrismContainer).__prismIO = io;\n } else {\n startRAF();\n }\n\n return () => {\n stopRAF();\n ro.disconnect();\n if (animationType === 'hover') {\n if (onPointerMove) window.removeEventListener('pointermove', onPointerMove as EventListener);\n window.removeEventListener('mouseleave', onLeave);\n window.removeEventListener('blur', onBlur);\n }\n if (suspendWhenOffscreen) {\n const io = (container as PrismContainer).__prismIO as IntersectionObserver | undefined;\n if (io) io.disconnect();\n delete (container as PrismContainer).__prismIO;\n }\n if (gl.canvas.parentElement === container) container.removeChild(gl.canvas);\n };\n }, [\n height,\n baseWidth,\n animationType,\n glow,\n noise,\n offset?.x,\n offset?.y,\n scale,\n transparent,\n hueShift,\n colorFrequency,\n timeScale,\n hoverStrength,\n inertia,\n bloom,\n suspendWhenOffscreen\n ]);\n\n return
    ;\n};\n\nexport default Prism;\n" } ], diff --git a/public/r/PrismaticBurst-JS-CSS.json b/public/r/PrismaticBurst-JS-CSS.json index e577d410..dade6c36 100644 --- a/public/r/PrismaticBurst-JS-CSS.json +++ b/public/r/PrismaticBurst-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "PrismaticBurst/PrismaticBurst.css", + "type": "registry:file", + "path": "PrismaticBurst.css", + "target": "@components/PrismaticBurst.css", "content": ".prismatic-burst-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "PrismaticBurst/PrismaticBurst.jsx", + "path": "PrismaticBurst.jsx", "content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle, Texture } from 'ogl';\nimport './PrismaticBurst.css';\n\nconst vertexShader = `#version 300 es\nin vec2 position;\nin vec2 uv;\nout vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragmentShader = `#version 300 es\nprecision highp float;\nprecision highp int;\n\nout vec4 fragColor;\n\nuniform vec2 uResolution;\nuniform float uTime;\n\nuniform float uIntensity;\nuniform float uSpeed;\nuniform int uAnimType;\nuniform vec2 uMouse;\nuniform int uColorCount;\nuniform float uDistort;\nuniform vec2 uOffset;\nuniform sampler2D uGradient;\nuniform float uNoiseAmount;\nuniform int uRayCount;\n\nfloat hash21(vec2 p){\n p = floor(p);\n float f = 52.9829189 * fract(dot(p, vec2(0.065, 0.005)));\n return fract(f);\n}\n\nmat2 rot30(){ return mat2(0.8, -0.5, 0.5, 0.8); }\n\nfloat layeredNoise(vec2 fragPx){\n vec2 p = mod(fragPx + vec2(uTime * 30.0, -uTime * 21.0), 1024.0);\n vec2 q = rot30() * p;\n float n = 0.0;\n n += 0.40 * hash21(q);\n n += 0.25 * hash21(q * 2.0 + 17.0);\n n += 0.20 * hash21(q * 4.0 + 47.0);\n n += 0.10 * hash21(q * 8.0 + 113.0);\n n += 0.05 * hash21(q * 16.0 + 191.0);\n return n;\n}\n\nvec3 rayDir(vec2 frag, vec2 res, vec2 offset, float dist){\n float focal = res.y * max(dist, 1e-3);\n return normalize(vec3(2.0 * (frag - offset) - res, focal));\n}\n\nfloat edgeFade(vec2 frag, vec2 res, vec2 offset){\n vec2 toC = frag - 0.5 * res - offset;\n float r = length(toC) / (0.5 * min(res.x, res.y));\n float x = clamp(r, 0.0, 1.0);\n float q = x * x * x * (x * (x * 6.0 - 15.0) + 10.0);\n float s = q * 0.5;\n s = pow(s, 1.5);\n float tail = 1.0 - pow(1.0 - s, 2.0);\n s = mix(s, tail, 0.2);\n float dn = (layeredNoise(frag * 0.15) - 0.5) * 0.0015 * s;\n return clamp(s + dn, 0.0, 1.0);\n}\n\nmat3 rotX(float a){ float c = cos(a), s = sin(a); return mat3(1.0,0.0,0.0, 0.0,c,-s, 0.0,s,c); }\nmat3 rotY(float a){ float c = cos(a), s = sin(a); return mat3(c,0.0,s, 0.0,1.0,0.0, -s,0.0,c); }\nmat3 rotZ(float a){ float c = cos(a), s = sin(a); return mat3(c,-s,0.0, s,c,0.0, 0.0,0.0,1.0); }\n\nvec3 sampleGradient(float t){\n t = clamp(t, 0.0, 1.0);\n return texture(uGradient, vec2(t, 0.5)).rgb;\n}\n\nvec2 rot2(vec2 v, float a){\n float s = sin(a), c = cos(a);\n return mat2(c, -s, s, c) * v;\n}\n\nfloat bendAngle(vec3 q, float t){\n float a = 0.8 * sin(q.x * 0.55 + t * 0.6)\n + 0.7 * sin(q.y * 0.50 - t * 0.5)\n + 0.6 * sin(q.z * 0.60 + t * 0.7);\n return a;\n}\n\nvoid main(){\n vec2 frag = gl_FragCoord.xy;\n float t = uTime * uSpeed;\n float jitterAmp = 0.1 * clamp(uNoiseAmount, 0.0, 1.0);\n vec3 dir = rayDir(frag, uResolution, uOffset, 1.0);\n float marchT = 0.0;\n vec3 col = vec3(0.0);\n float n = layeredNoise(frag);\n vec4 c = cos(t * 0.2 + vec4(0.0, 33.0, 11.0, 0.0));\n mat2 M2 = mat2(c.x, c.y, c.z, c.w);\n float amp = clamp(uDistort, 0.0, 50.0) * 0.15;\n\n mat3 rot3dMat = mat3(1.0);\n if(uAnimType == 1){\n vec3 ang = vec3(t * 0.31, t * 0.21, t * 0.17);\n rot3dMat = rotZ(ang.z) * rotY(ang.y) * rotX(ang.x);\n }\n mat3 hoverMat = mat3(1.0);\n if(uAnimType == 2){\n vec2 m = uMouse * 2.0 - 1.0;\n vec3 ang = vec3(m.y * 0.6, m.x * 0.6, 0.0);\n hoverMat = rotY(ang.y) * rotX(ang.x);\n }\n\n for (int i = 0; i < 44; ++i) {\n vec3 P = marchT * dir;\n P.z -= 2.0;\n float rad = length(P);\n vec3 Pl = P * (10.0 / max(rad, 1e-6));\n\n if(uAnimType == 0){\n Pl.xz *= M2;\n } else if(uAnimType == 1){\n Pl = rot3dMat * Pl;\n } else {\n Pl = hoverMat * Pl;\n }\n\n float stepLen = min(rad - 0.3, n * jitterAmp) + 0.1;\n\n float grow = smoothstep(0.35, 3.0, marchT);\n float a1 = amp * grow * bendAngle(Pl * 0.6, t);\n float a2 = 0.5 * amp * grow * bendAngle(Pl.zyx * 0.5 + 3.1, t * 0.9);\n vec3 Pb = Pl;\n Pb.xz = rot2(Pb.xz, a1);\n Pb.xy = rot2(Pb.xy, a2);\n\n float rayPattern = smoothstep(\n 0.5, 0.7,\n sin(Pb.x + cos(Pb.y) * cos(Pb.z)) *\n sin(Pb.z + sin(Pb.y) * cos(Pb.x + t))\n );\n\n if (uRayCount > 0) {\n float ang = atan(Pb.y, Pb.x);\n float comb = 0.5 + 0.5 * cos(float(uRayCount) * ang);\n comb = pow(comb, 3.0);\n rayPattern *= smoothstep(0.15, 0.95, comb);\n }\n\n vec3 spectralDefault = 1.0 + vec3(\n cos(marchT * 3.0 + 0.0),\n cos(marchT * 3.0 + 1.0),\n cos(marchT * 3.0 + 2.0)\n );\n\n float saw = fract(marchT * 0.25);\n float tRay = saw * saw * (3.0 - 2.0 * saw);\n vec3 userGradient = 2.0 * sampleGradient(tRay);\n vec3 spectral = (uColorCount > 0) ? userGradient : spectralDefault;\n vec3 base = (0.05 / (0.4 + stepLen))\n * smoothstep(5.0, 0.0, rad)\n * spectral;\n\n col += base * rayPattern;\n marchT += stepLen;\n }\n\n col *= edgeFade(frag, uResolution, uOffset);\n col *= uIntensity;\n\n fragColor = vec4(clamp(col, 0.0, 1.0), 1.0);\n}`;\n\nconst hexToRgb01 = hex => {\n let h = hex.trim();\n if (h.startsWith('#')) h = h.slice(1);\n if (h.length === 3) {\n const r = h[0],\n g = h[1],\n b = h[2];\n h = r + r + g + g + b + b;\n }\n const intVal = parseInt(h.slice(0, 6), 16);\n if (isNaN(intVal) || (h.length !== 6 && h.length !== 8)) return [1, 1, 1];\n const r = ((intVal >> 16) & 255) / 255;\n const g = ((intVal >> 8) & 255) / 255;\n const b = (intVal & 255) / 255;\n return [r, g, b];\n};\n\nconst toPx = v => {\n if (v == null) return 0;\n if (typeof v === 'number') return v;\n const s = String(v).trim();\n const num = parseFloat(s.replace('px', ''));\n return isNaN(num) ? 0 : num;\n};\n\nconst PrismaticBurst = ({\n intensity = 2,\n speed = 0.5,\n animationType = 'rotate3d',\n colors,\n distort = 0,\n paused = false,\n offset = { x: 0, y: 0 },\n hoverDampness = 0,\n rayCount,\n mixBlendMode = 'lighten'\n}) => {\n const containerRef = useRef(null);\n const programRef = useRef(null);\n const rendererRef = useRef(null);\n const mouseTargetRef = useRef([0.5, 0.5]);\n const mouseSmoothRef = useRef([0.5, 0.5]);\n const pausedRef = useRef(paused);\n const gradTexRef = useRef(null);\n const hoverDampRef = useRef(hoverDampness);\n const isVisibleRef = useRef(true);\n const meshRef = useRef(null);\n const triRef = useRef(null);\n\n useEffect(() => {\n pausedRef.current = paused;\n }, [paused]);\n useEffect(() => {\n hoverDampRef.current = hoverDampness;\n }, [hoverDampness]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n const renderer = new Renderer({\n dpr,\n alpha: false,\n antialias: false\n });\n rendererRef.current = renderer;\n\n const gl = renderer.gl;\n gl.canvas.style.position = 'absolute';\n gl.canvas.style.inset = '0';\n gl.canvas.style.width = '100%';\n gl.canvas.style.height = '100%';\n gl.canvas.style.mixBlendMode = mixBlendMode && mixBlendMode !== 'none' ? mixBlendMode : '';\n container.appendChild(gl.canvas);\n\n const white = new Uint8Array([255, 255, 255, 255]);\n const gradientTex = new Texture(gl, {\n image: white,\n width: 1,\n height: 1,\n generateMipmaps: false,\n flipY: false\n });\n\n gradientTex.minFilter = gl.LINEAR;\n gradientTex.magFilter = gl.LINEAR;\n gradientTex.wrapS = gl.CLAMP_TO_EDGE;\n gradientTex.wrapT = gl.CLAMP_TO_EDGE;\n gradTexRef.current = gradientTex;\n\n const program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n uResolution: { value: [1, 1] },\n uTime: { value: 0 },\n\n uIntensity: { value: 1 },\n uSpeed: { value: 1 },\n uAnimType: { value: 0 },\n uMouse: { value: [0.5, 0.5] },\n uColorCount: { value: 0 },\n uDistort: { value: 0 },\n uOffset: { value: [0, 0] },\n uGradient: { value: gradientTex },\n uNoiseAmount: { value: 0.8 },\n uRayCount: { value: 0 }\n }\n });\n\n programRef.current = program;\n\n const triangle = new Triangle(gl);\n const mesh = new Mesh(gl, { geometry: triangle, program });\n triRef.current = triangle;\n meshRef.current = mesh;\n\n const resize = () => {\n const w = container.clientWidth || 1;\n const h = container.clientHeight || 1;\n renderer.setSize(w, h);\n program.uniforms.uResolution.value = [gl.drawingBufferWidth, gl.drawingBufferHeight];\n };\n\n let ro = null;\n if ('ResizeObserver' in window) {\n ro = new ResizeObserver(resize);\n ro.observe(container);\n } else {\n window.addEventListener('resize', resize);\n }\n resize();\n\n const onPointer = e => {\n const rect = container.getBoundingClientRect();\n const x = (e.clientX - rect.left) / Math.max(rect.width, 1);\n const y = (e.clientY - rect.top) / Math.max(rect.height, 1);\n mouseTargetRef.current = [Math.min(Math.max(x, 0), 1), Math.min(Math.max(y, 0), 1)];\n };\n container.addEventListener('pointermove', onPointer, { passive: true });\n\n let io = null;\n if ('IntersectionObserver' in window) {\n io = new IntersectionObserver(\n entries => {\n if (entries[0]) {\n isVisibleRef.current = entries[0].isIntersecting;\n }\n },\n { root: null, threshold: 0.01 }\n );\n io.observe(container);\n }\n\n const onVis = () => {};\n document.addEventListener('visibilitychange', onVis);\n\n let raf = 0;\n let last = performance.now();\n let accumTime = 0;\n\n const update = now => {\n const dt = Math.max(0, now - last) * 0.001;\n last = now;\n const visible = isVisibleRef.current && !document.hidden;\n if (!pausedRef.current) accumTime += dt;\n\n if (!visible) {\n raf = requestAnimationFrame(update);\n return;\n }\n\n const tau = 0.02 + Math.max(0, Math.min(1, hoverDampRef.current)) * 0.5;\n const alpha = 1 - Math.exp(-dt / tau);\n const tgt = mouseTargetRef.current;\n const sm = mouseSmoothRef.current;\n sm[0] += (tgt[0] - sm[0]) * alpha;\n sm[1] += (tgt[1] - sm[1]) * alpha;\n\n program.uniforms.uMouse.value = sm;\n program.uniforms.uTime.value = accumTime;\n\n renderer.render({ scene: meshRef.current });\n raf = requestAnimationFrame(update);\n };\n raf = requestAnimationFrame(update);\n\n return () => {\n cancelAnimationFrame(raf);\n container.removeEventListener('pointermove', onPointer);\n ro?.disconnect();\n if (!ro) window.removeEventListener('resize', resize);\n io?.disconnect();\n document.removeEventListener('visibilitychange', onVis);\n try {\n container.removeChild(gl.canvas);\n } catch {\n console.warn('Canvas already removed');\n }\n try {\n meshRef.current?.remove?.();\n } catch (e) {\n /* ignore dispose errors */\n }\n try {\n triRef.current?.remove?.();\n } catch (e) {\n /* ignore dispose errors */\n }\n try {\n programRef.current?.remove?.();\n } catch (e) {\n /* ignore dispose errors */\n }\n try {\n const glCtx = rendererRef.current?.gl;\n if (glCtx && gradTexRef.current?.texture) {\n glCtx.deleteTexture(gradTexRef.current.texture);\n }\n } catch (e) {\n /* ignore texture delete errors */\n }\n programRef.current = null;\n rendererRef.current = null;\n gradTexRef.current = null;\n meshRef.current = null;\n triRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n const canvas = rendererRef.current?.gl?.canvas;\n\n if (canvas) {\n canvas.style.mixBlendMode = mixBlendMode && mixBlendMode !== 'none' ? mixBlendMode : '';\n }\n }, [mixBlendMode]);\n\n useEffect(() => {\n const program = programRef.current;\n const renderer = rendererRef.current;\n const gradTex = gradTexRef.current;\n if (!program || !renderer || !gradTex) return;\n\n program.uniforms.uIntensity.value = intensity ?? 1;\n program.uniforms.uSpeed.value = speed ?? 1;\n\n const animTypeMap = {\n rotate: 0,\n rotate3d: 1,\n hover: 2\n };\n program.uniforms.uAnimType.value = animTypeMap[animationType ?? 'rotate'];\n\n program.uniforms.uDistort.value = typeof distort === 'number' ? distort : 0;\n\n const ox = toPx(offset?.x);\n const oy = toPx(offset?.y);\n program.uniforms.uOffset.value = [ox, oy];\n program.uniforms.uRayCount.value = Math.max(0, Math.floor(rayCount ?? 0));\n\n let count = 0;\n if (Array.isArray(colors) && colors.length > 0) {\n const gl = renderer.gl;\n const capped = colors.slice(0, 64);\n count = capped.length;\n const data = new Uint8Array(count * 4);\n for (let i = 0; i < count; i++) {\n const [r, g, b] = hexToRgb01(capped[i]);\n data[i * 4 + 0] = Math.round(r * 255);\n data[i * 4 + 1] = Math.round(g * 255);\n data[i * 4 + 2] = Math.round(b * 255);\n data[i * 4 + 3] = 255;\n }\n gradTex.image = data;\n gradTex.width = count;\n gradTex.height = 1;\n gradTex.minFilter = gl.LINEAR;\n gradTex.magFilter = gl.LINEAR;\n gradTex.wrapS = gl.CLAMP_TO_EDGE;\n gradTex.wrapT = gl.CLAMP_TO_EDGE;\n gradTex.flipY = false;\n gradTex.generateMipmaps = false;\n gradTex.format = gl.RGBA;\n gradTex.type = gl.UNSIGNED_BYTE;\n gradTex.needsUpdate = true;\n } else {\n count = 0;\n }\n program.uniforms.uColorCount.value = count;\n }, [intensity, speed, animationType, colors, distort, offset, rayCount]);\n\n return
    ;\n};\n\nexport default PrismaticBurst;\n" } ], diff --git a/public/r/PrismaticBurst-TS-CSS.json b/public/r/PrismaticBurst-TS-CSS.json index 75125eee..cd52d828 100644 --- a/public/r/PrismaticBurst-TS-CSS.json +++ b/public/r/PrismaticBurst-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "PrismaticBurst/PrismaticBurst.css", + "type": "registry:file", + "path": "PrismaticBurst.css", + "target": "@components/PrismaticBurst.css", "content": ".prismatic-burst-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n" }, { "type": "registry:component", - "path": "PrismaticBurst/PrismaticBurst.tsx", + "path": "PrismaticBurst.tsx", "content": "import React, { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle, Texture } from 'ogl';\nimport './PrismaticBurst.css';\n\ntype Offset = { x?: number | string; y?: number | string };\ntype AnimationType = 'rotate' | 'rotate3d' | 'hover';\n\nexport type PrismaticBurstProps = {\n intensity?: number;\n speed?: number;\n animationType?: AnimationType;\n colors?: string[];\n distort?: number;\n paused?: boolean;\n offset?: Offset;\n hoverDampness?: number;\n rayCount?: number;\n mixBlendMode?: React.CSSProperties['mixBlendMode'] | 'none';\n};\n\nconst vertexShader = `#version 300 es\nin vec2 position;\nin vec2 uv;\nout vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragmentShader = `#version 300 es\nprecision highp float;\nprecision highp int;\n\nout vec4 fragColor;\n\nuniform vec2 uResolution;\nuniform float uTime;\n\nuniform float uIntensity;\nuniform float uSpeed;\nuniform int uAnimType;\nuniform vec2 uMouse;\nuniform int uColorCount;\nuniform float uDistort;\nuniform vec2 uOffset;\nuniform sampler2D uGradient;\nuniform float uNoiseAmount;\nuniform int uRayCount;\n\nfloat hash21(vec2 p){\n p = floor(p);\n float f = 52.9829189 * fract(dot(p, vec2(0.065, 0.005)));\n return fract(f);\n}\n\nmat2 rot30(){ return mat2(0.8, -0.5, 0.5, 0.8); }\n\nfloat layeredNoise(vec2 fragPx){\n vec2 p = mod(fragPx + vec2(uTime * 30.0, -uTime * 21.0), 1024.0);\n vec2 q = rot30() * p;\n float n = 0.0;\n n += 0.40 * hash21(q);\n n += 0.25 * hash21(q * 2.0 + 17.0);\n n += 0.20 * hash21(q * 4.0 + 47.0);\n n += 0.10 * hash21(q * 8.0 + 113.0);\n n += 0.05 * hash21(q * 16.0 + 191.0);\n return n;\n}\n\nvec3 rayDir(vec2 frag, vec2 res, vec2 offset, float dist){\n float focal = res.y * max(dist, 1e-3);\n return normalize(vec3(2.0 * (frag - offset) - res, focal));\n}\n\nfloat edgeFade(vec2 frag, vec2 res, vec2 offset){\n vec2 toC = frag - 0.5 * res - offset;\n float r = length(toC) / (0.5 * min(res.x, res.y));\n float x = clamp(r, 0.0, 1.0);\n float q = x * x * x * (x * (x * 6.0 - 15.0) + 10.0);\n float s = q * 0.5;\n s = pow(s, 1.5);\n float tail = 1.0 - pow(1.0 - s, 2.0);\n s = mix(s, tail, 0.2);\n float dn = (layeredNoise(frag * 0.15) - 0.5) * 0.0015 * s;\n return clamp(s + dn, 0.0, 1.0);\n}\n\nmat3 rotX(float a){ float c = cos(a), s = sin(a); return mat3(1.0,0.0,0.0, 0.0,c,-s, 0.0,s,c); }\nmat3 rotY(float a){ float c = cos(a), s = sin(a); return mat3(c,0.0,s, 0.0,1.0,0.0, -s,0.0,c); }\nmat3 rotZ(float a){ float c = cos(a), s = sin(a); return mat3(c,-s,0.0, s,c,0.0, 0.0,0.0,1.0); }\n\nvec3 sampleGradient(float t){\n t = clamp(t, 0.0, 1.0);\n return texture(uGradient, vec2(t, 0.5)).rgb;\n}\n\nvec2 rot2(vec2 v, float a){\n float s = sin(a), c = cos(a);\n return mat2(c, -s, s, c) * v;\n}\n\nfloat bendAngle(vec3 q, float t){\n float a = 0.8 * sin(q.x * 0.55 + t * 0.6)\n + 0.7 * sin(q.y * 0.50 - t * 0.5)\n + 0.6 * sin(q.z * 0.60 + t * 0.7);\n return a;\n}\n\nvoid main(){\n vec2 frag = gl_FragCoord.xy;\n float t = uTime * uSpeed;\n float jitterAmp = 0.1 * clamp(uNoiseAmount, 0.0, 1.0);\n vec3 dir = rayDir(frag, uResolution, uOffset, 1.0);\n float marchT = 0.0;\n vec3 col = vec3(0.0);\n float n = layeredNoise(frag);\n vec4 c = cos(t * 0.2 + vec4(0.0, 33.0, 11.0, 0.0));\n mat2 M2 = mat2(c.x, c.y, c.z, c.w);\n float amp = clamp(uDistort, 0.0, 50.0) * 0.15;\n\n mat3 rot3dMat = mat3(1.0);\n if(uAnimType == 1){\n vec3 ang = vec3(t * 0.31, t * 0.21, t * 0.17);\n rot3dMat = rotZ(ang.z) * rotY(ang.y) * rotX(ang.x);\n }\n mat3 hoverMat = mat3(1.0);\n if(uAnimType == 2){\n vec2 m = uMouse * 2.0 - 1.0;\n vec3 ang = vec3(m.y * 0.6, m.x * 0.6, 0.0);\n hoverMat = rotY(ang.y) * rotX(ang.x);\n }\n\n for (int i = 0; i < 44; ++i) {\n vec3 P = marchT * dir;\n P.z -= 2.0;\n float rad = length(P);\n vec3 Pl = P * (10.0 / max(rad, 1e-6));\n\n if(uAnimType == 0){\n Pl.xz *= M2;\n } else if(uAnimType == 1){\n Pl = rot3dMat * Pl;\n } else {\n Pl = hoverMat * Pl;\n }\n\n float stepLen = min(rad - 0.3, n * jitterAmp) + 0.1;\n\n float grow = smoothstep(0.35, 3.0, marchT);\n float a1 = amp * grow * bendAngle(Pl * 0.6, t);\n float a2 = 0.5 * amp * grow * bendAngle(Pl.zyx * 0.5 + 3.1, t * 0.9);\n vec3 Pb = Pl;\n Pb.xz = rot2(Pb.xz, a1);\n Pb.xy = rot2(Pb.xy, a2);\n\n float rayPattern = smoothstep(\n 0.5, 0.7,\n sin(Pb.x + cos(Pb.y) * cos(Pb.z)) *\n sin(Pb.z + sin(Pb.y) * cos(Pb.x + t))\n );\n\n if (uRayCount > 0) {\n float ang = atan(Pb.y, Pb.x);\n float comb = 0.5 + 0.5 * cos(float(uRayCount) * ang);\n comb = pow(comb, 3.0);\n rayPattern *= smoothstep(0.15, 0.95, comb);\n }\n\n vec3 spectralDefault = 1.0 + vec3(\n cos(marchT * 3.0 + 0.0),\n cos(marchT * 3.0 + 1.0),\n cos(marchT * 3.0 + 2.0)\n );\n\n float saw = fract(marchT * 0.25);\n float tRay = saw * saw * (3.0 - 2.0 * saw);\n vec3 userGradient = 2.0 * sampleGradient(tRay);\n vec3 spectral = (uColorCount > 0) ? userGradient : spectralDefault;\n vec3 base = (0.05 / (0.4 + stepLen))\n * smoothstep(5.0, 0.0, rad)\n * spectral;\n\n col += base * rayPattern;\n marchT += stepLen;\n }\n\n col *= edgeFade(frag, uResolution, uOffset);\n col *= uIntensity;\n\n fragColor = vec4(clamp(col, 0.0, 1.0), 1.0);\n}`;\n\nconst hexToRgb01 = (hex: string): [number, number, number] => {\n let h = hex.trim();\n if (h.startsWith('#')) h = h.slice(1);\n if (h.length === 3) {\n const r = h[0],\n g = h[1],\n b = h[2];\n h = r + r + g + g + b + b;\n }\n const intVal = parseInt(h.slice(0, 6), 16);\n if (isNaN(intVal) || (h.length !== 6 && h.length !== 8)) return [1, 1, 1];\n const r = ((intVal >> 16) & 255) / 255;\n const g = ((intVal >> 8) & 255) / 255;\n const b = (intVal & 255) / 255;\n return [r, g, b];\n};\n\nconst toPx = (v: number | string | undefined): number => {\n if (v == null) return 0;\n if (typeof v === 'number') return v;\n const s = String(v).trim();\n const num = parseFloat(s.replace('px', ''));\n return isNaN(num) ? 0 : num;\n};\n\nconst PrismaticBurst = ({\n intensity = 2,\n speed = 0.5,\n animationType = 'rotate3d',\n colors,\n distort = 0,\n paused = false,\n offset = { x: 0, y: 0 },\n hoverDampness = 0,\n rayCount,\n mixBlendMode = 'lighten'\n}: PrismaticBurstProps) => {\n const containerRef = useRef(null);\n const programRef = useRef(null);\n const rendererRef = useRef(null);\n const mouseTargetRef = useRef<[number, number]>([0.5, 0.5]);\n const mouseSmoothRef = useRef<[number, number]>([0.5, 0.5]);\n const pausedRef = useRef(paused);\n const gradTexRef = useRef(null);\n const hoverDampRef = useRef(hoverDampness);\n const isVisibleRef = useRef(true);\n const meshRef = useRef(null);\n const triRef = useRef(null);\n\n useEffect(() => {\n pausedRef.current = paused;\n }, [paused]);\n useEffect(() => {\n hoverDampRef.current = hoverDampness;\n }, [hoverDampness]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n const renderer = new Renderer({ dpr, alpha: false, antialias: false });\n rendererRef.current = renderer;\n\n const gl = renderer.gl;\n gl.canvas.style.position = 'absolute';\n gl.canvas.style.inset = '0';\n gl.canvas.style.width = '100%';\n gl.canvas.style.height = '100%';\n gl.canvas.style.mixBlendMode = mixBlendMode && mixBlendMode !== 'none' ? mixBlendMode : '';\n container.appendChild(gl.canvas);\n\n const white = new Uint8Array([255, 255, 255, 255]);\n const gradientTex = new Texture(gl, {\n image: white,\n width: 1,\n height: 1,\n generateMipmaps: false,\n flipY: false\n });\n\n gradientTex.minFilter = gl.LINEAR;\n gradientTex.magFilter = gl.LINEAR;\n gradientTex.wrapS = gl.CLAMP_TO_EDGE;\n gradientTex.wrapT = gl.CLAMP_TO_EDGE;\n gradTexRef.current = gradientTex;\n\n const program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n uResolution: { value: [1, 1] as [number, number] },\n uTime: { value: 0 },\n\n uIntensity: { value: 1 },\n uSpeed: { value: 1 },\n uAnimType: { value: 0 },\n uMouse: { value: [0.5, 0.5] as [number, number] },\n uColorCount: { value: 0 },\n uDistort: { value: 0 },\n uOffset: { value: [0, 0] as [number, number] },\n uGradient: { value: gradientTex },\n uNoiseAmount: { value: 0.8 },\n uRayCount: { value: 0 }\n }\n });\n\n programRef.current = program;\n\n const triangle = new Triangle(gl);\n const mesh = new Mesh(gl, { geometry: triangle, program });\n triRef.current = triangle;\n meshRef.current = mesh;\n\n const resize = () => {\n const w = container.clientWidth || 1;\n const h = container.clientHeight || 1;\n renderer.setSize(w, h);\n program.uniforms.uResolution.value = [gl.drawingBufferWidth, gl.drawingBufferHeight];\n };\n\n let ro: ResizeObserver | null = null;\n if ('ResizeObserver' in window) {\n ro = new ResizeObserver(resize);\n ro.observe(container);\n } else {\n (window as Window).addEventListener('resize', resize);\n }\n resize();\n\n const onPointer = (e: PointerEvent) => {\n const rect = container.getBoundingClientRect();\n const x = (e.clientX - rect.left) / Math.max(rect.width, 1);\n const y = (e.clientY - rect.top) / Math.max(rect.height, 1);\n mouseTargetRef.current = [Math.min(Math.max(x, 0), 1), Math.min(Math.max(y, 0), 1)];\n };\n container.addEventListener('pointermove', onPointer, { passive: true });\n\n let io: IntersectionObserver | null = null;\n if ('IntersectionObserver' in window) {\n io = new IntersectionObserver(\n entries => {\n if (entries[0]) isVisibleRef.current = entries[0].isIntersecting;\n },\n { root: null, threshold: 0.01 }\n );\n io.observe(container);\n }\n const onVis = () => {};\n document.addEventListener('visibilitychange', onVis);\n\n let raf = 0;\n let last = performance.now();\n let accumTime = 0;\n\n const update = (now: number) => {\n const dt = Math.max(0, now - last) * 0.001;\n last = now;\n const visible = isVisibleRef.current && !document.hidden;\n if (!pausedRef.current) accumTime += dt;\n if (!visible) {\n raf = requestAnimationFrame(update);\n return;\n }\n const tau = 0.02 + Math.max(0, Math.min(1, hoverDampRef.current)) * 0.5;\n const alpha = 1 - Math.exp(-dt / tau);\n const tgt = mouseTargetRef.current;\n const sm = mouseSmoothRef.current;\n sm[0] += (tgt[0] - sm[0]) * alpha;\n sm[1] += (tgt[1] - sm[1]) * alpha;\n program.uniforms.uMouse.value = sm as any;\n program.uniforms.uTime.value = accumTime;\n renderer.render({ scene: meshRef.current! });\n raf = requestAnimationFrame(update);\n };\n raf = requestAnimationFrame(update);\n\n return () => {\n cancelAnimationFrame(raf);\n container.removeEventListener('pointermove', onPointer);\n ro?.disconnect();\n if (!ro) window.removeEventListener('resize', resize);\n io?.disconnect();\n document.removeEventListener('visibilitychange', onVis);\n try {\n container.removeChild(gl.canvas);\n } catch (e) {\n void e;\n }\n meshRef.current = null;\n triRef.current = null;\n programRef.current = null;\n try {\n const glCtx = rendererRef.current?.gl;\n if (glCtx && gradTexRef.current?.texture) glCtx.deleteTexture(gradTexRef.current.texture);\n } catch (e) {\n void e;\n }\n programRef.current = null;\n rendererRef.current = null;\n gradTexRef.current = null;\n meshRef.current = null;\n triRef.current = null;\n };\n }, []);\n\n useEffect(() => {\n const canvas = rendererRef.current?.gl?.canvas as HTMLCanvasElement | undefined;\n if (canvas) {\n canvas.style.mixBlendMode = mixBlendMode && mixBlendMode !== 'none' ? mixBlendMode : '';\n }\n }, [mixBlendMode]);\n\n useEffect(() => {\n const program = programRef.current;\n const renderer = rendererRef.current;\n const gradTex = gradTexRef.current;\n if (!program || !renderer || !gradTex) return;\n\n program.uniforms.uIntensity.value = intensity ?? 1;\n program.uniforms.uSpeed.value = speed ?? 1;\n\n const animTypeMap: Record = {\n rotate: 0,\n rotate3d: 1,\n hover: 2\n };\n program.uniforms.uAnimType.value = animTypeMap[animationType ?? 'rotate'];\n\n program.uniforms.uDistort.value = typeof distort === 'number' ? distort : 0;\n\n const ox = toPx(offset?.x);\n const oy = toPx(offset?.y);\n program.uniforms.uOffset.value = [ox, oy];\n program.uniforms.uRayCount.value = Math.max(0, Math.floor(rayCount ?? 0));\n\n let count = 0;\n if (Array.isArray(colors) && colors.length > 0) {\n const gl = renderer.gl;\n const capped = colors.slice(0, 64);\n count = capped.length;\n const data = new Uint8Array(count * 4);\n for (let i = 0; i < count; i++) {\n const [r, g, b] = hexToRgb01(capped[i]);\n data[i * 4 + 0] = Math.round(r * 255);\n data[i * 4 + 1] = Math.round(g * 255);\n data[i * 4 + 2] = Math.round(b * 255);\n data[i * 4 + 3] = 255;\n }\n gradTex.image = data;\n gradTex.width = count;\n gradTex.height = 1;\n gradTex.minFilter = gl.LINEAR;\n gradTex.magFilter = gl.LINEAR;\n gradTex.wrapS = gl.CLAMP_TO_EDGE;\n gradTex.wrapT = gl.CLAMP_TO_EDGE;\n gradTex.flipY = false;\n gradTex.generateMipmaps = false;\n gradTex.format = gl.RGBA;\n gradTex.type = gl.UNSIGNED_BYTE;\n gradTex.needsUpdate = true;\n } else {\n count = 0;\n }\n program.uniforms.uColorCount.value = count;\n }, [intensity, speed, animationType, colors, distort, offset, rayCount]);\n\n return
    ;\n};\n\nexport default PrismaticBurst;\n" } ], diff --git a/public/r/ProfileCard-JS-CSS.json b/public/r/ProfileCard-JS-CSS.json index 4d101919..9d8a0b1f 100644 --- a/public/r/ProfileCard-JS-CSS.json +++ b/public/r/ProfileCard-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "ProfileCard/ProfileCard.css", + "type": "registry:file", + "path": "ProfileCard.css", + "target": "@components/ProfileCard.css", "content": ":root {\n --pointer-x: 50%;\n --pointer-y: 50%;\n --pointer-from-center: 0;\n --pointer-from-top: 0.5;\n --pointer-from-left: 0.5;\n --card-opacity: 0;\n --rotate-x: 0deg;\n --rotate-y: 0deg;\n --background-x: 50%;\n --background-y: 50%;\n --grain: none;\n --icon: none;\n --behind-gradient: none;\n --behind-glow-color: rgba(125, 190, 255, 0.67);\n --behind-glow-size: 25%;\n --inner-gradient: none;\n --sunpillar-1: hsl(2, 100%, 73%);\n --sunpillar-2: hsl(53, 100%, 69%);\n --sunpillar-3: hsl(93, 100%, 69%);\n --sunpillar-4: hsl(176, 100%, 76%);\n --sunpillar-5: hsl(228, 100%, 74%);\n --sunpillar-6: hsl(283, 100%, 73%);\n --sunpillar-clr-1: var(--sunpillar-1);\n --sunpillar-clr-2: var(--sunpillar-2);\n --sunpillar-clr-3: var(--sunpillar-3);\n --sunpillar-clr-4: var(--sunpillar-4);\n --sunpillar-clr-5: var(--sunpillar-5);\n --sunpillar-clr-6: var(--sunpillar-6);\n --card-radius: 30px;\n}\n\n.pc-card-wrapper {\n perspective: 500px;\n transform: translate3d(0, 0, 0.1px);\n position: relative;\n touch-action: none;\n}\n\n.pc-behind {\n position: absolute;\n inset: 0;\n z-index: 0;\n pointer-events: none;\n background: radial-gradient(\n circle at var(--pointer-x) var(--pointer-y),\n var(--behind-glow-color) 0%,\n transparent var(--behind-glow-size)\n );\n filter: blur(50px) saturate(1.1);\n opacity: calc(0.8 * var(--card-opacity));\n transition: opacity 200ms ease;\n}\n\n.pc-card-wrapper:hover,\n.pc-card-wrapper.active {\n --card-opacity: 1;\n}\n\n.pc-card {\n height: 80svh;\n max-height: 540px;\n display: grid;\n aspect-ratio: 0.718;\n border-radius: var(--card-radius);\n position: relative;\n background-blend-mode: color-dodge, normal, normal, normal;\n animation: glow-bg 12s linear infinite;\n box-shadow: rgba(0, 0, 0, 0.8) calc((var(--pointer-from-left) * 10px) - 3px)\n calc((var(--pointer-from-top) * 20px) - 6px) 20px -5px;\n transition: transform 1s ease;\n transform: translateZ(0) rotateX(0deg) rotateY(0deg);\n background: rgba(0, 0, 0, 0.9);\n backface-visibility: hidden;\n overflow: hidden;\n}\n\n.pc-card:hover,\n.pc-card.active {\n transition: none;\n transform: translateZ(0) rotateX(var(--rotate-y)) rotateY(var(--rotate-x));\n}\n\n.pc-card-shell.entering .pc-card {\n transition: transform 180ms ease-out;\n}\n\n.pc-card-shell {\n position: relative;\n z-index: 1;\n}\n\n.pc-card * {\n display: grid;\n grid-area: 1/-1;\n border-radius: var(--card-radius);\n pointer-events: none;\n}\n\n.pc-inside {\n inset: 0;\n position: absolute;\n background-image: var(--inner-gradient);\n background-color: rgba(0, 0, 0, 0.9);\n transform: none;\n}\n\n.pc-shine {\n mask-image: var(--icon);\n mask-mode: luminance;\n mask-repeat: repeat;\n mask-size: 150%;\n mask-position: top calc(200% - (var(--background-y) * 5)) left calc(100% - var(--background-x));\n transition: filter 0.8s ease;\n filter: brightness(0.66) contrast(1.33) saturate(0.33) opacity(0.5);\n animation: holo-bg 18s linear infinite;\n animation-play-state: running;\n mix-blend-mode: color-dodge;\n}\n\n.pc-shine,\n.pc-shine::after {\n --space: 5%;\n --angle: -45deg;\n transform: translate3d(0, 0, 1px);\n overflow: hidden;\n z-index: 3;\n background: transparent;\n background-size: cover;\n background-position: center;\n background-image:\n repeating-linear-gradient(\n 0deg,\n var(--sunpillar-clr-1) calc(var(--space) * 1),\n var(--sunpillar-clr-2) calc(var(--space) * 2),\n var(--sunpillar-clr-3) calc(var(--space) * 3),\n var(--sunpillar-clr-4) calc(var(--space) * 4),\n var(--sunpillar-clr-5) calc(var(--space) * 5),\n var(--sunpillar-clr-6) calc(var(--space) * 6),\n var(--sunpillar-clr-1) calc(var(--space) * 7)\n ),\n repeating-linear-gradient(\n var(--angle),\n #0e152e 0%,\n hsl(180, 10%, 60%) 3.8%,\n hsl(180, 29%, 66%) 4.5%,\n hsl(180, 10%, 60%) 5.2%,\n #0e152e 10%,\n #0e152e 12%\n ),\n radial-gradient(\n farthest-corner circle at var(--pointer-x) var(--pointer-y),\n hsla(0, 0%, 0%, 0.1) 12%,\n hsla(0, 0%, 0%, 0.15) 20%,\n hsla(0, 0%, 0%, 0.25) 120%\n );\n background-position:\n 0 var(--background-y),\n var(--background-x) var(--background-y),\n center;\n background-blend-mode: color, hard-light;\n background-size:\n 500% 500%,\n 300% 300%,\n 200% 200%;\n background-repeat: repeat;\n}\n\n.pc-shine::before,\n.pc-shine::after {\n content: '';\n background-position: center;\n background-size: cover;\n grid-area: 1/1;\n opacity: 0;\n transition: opacity 0.8s ease;\n}\n\n.pc-card:hover .pc-shine,\n.pc-card.active .pc-shine {\n filter: brightness(0.85) contrast(1.5) saturate(0.5);\n animation-play-state: paused;\n}\n\n.pc-card:hover .pc-shine::before,\n.pc-card.active .pc-shine::before,\n.pc-card:hover .pc-shine::after,\n.pc-card.active .pc-shine::after {\n opacity: 1;\n}\n\n.pc-shine::before {\n background-image:\n linear-gradient(\n 45deg,\n var(--sunpillar-4),\n var(--sunpillar-5),\n var(--sunpillar-6),\n var(--sunpillar-1),\n var(--sunpillar-2),\n var(--sunpillar-3)\n ),\n radial-gradient(circle at var(--pointer-x) var(--pointer-y), hsl(0, 0%, 70%) 0%, hsla(0, 0%, 30%, 0.2) 90%),\n var(--grain);\n background-size:\n 250% 250%,\n 100% 100%,\n 220px 220px;\n background-position:\n var(--pointer-x) var(--pointer-y),\n center,\n calc(var(--pointer-x) * 0.01) calc(var(--pointer-y) * 0.01);\n background-blend-mode: color-dodge;\n filter: brightness(calc(2 - var(--pointer-from-center))) contrast(calc(var(--pointer-from-center) + 2))\n saturate(calc(0.5 + var(--pointer-from-center)));\n mix-blend-mode: luminosity;\n}\n\n.pc-shine::after {\n background-position:\n 0 var(--background-y),\n calc(var(--background-x) * 0.4) calc(var(--background-y) * 0.5),\n center;\n background-size:\n 200% 300%,\n 700% 700%,\n 100% 100%;\n mix-blend-mode: difference;\n filter: brightness(0.8) contrast(1.5);\n}\n\n.pc-glare {\n transform: translate3d(0, 0, 1.1px);\n overflow: hidden;\n background-image: radial-gradient(\n farthest-corner circle at var(--pointer-x) var(--pointer-y),\n hsl(248, 25%, 80%) 12%,\n hsla(207, 40%, 30%, 0.8) 90%\n );\n mix-blend-mode: overlay;\n filter: brightness(0.8) contrast(1.2);\n z-index: 4;\n}\n\n.pc-avatar-content {\n mix-blend-mode: luminosity;\n overflow: visible;\n transform: translateZ(2);\n backface-visibility: hidden;\n}\n\n.pc-avatar-content .avatar {\n width: 100%;\n position: absolute;\n left: 50%;\n transform-origin: 50% 100%;\n transform: translateX(calc(-50% + (var(--pointer-from-left) - 0.5) * 6px)) translateZ(0)\n scaleY(calc(1 + (var(--pointer-from-top) - 0.5) * 0.02)) scaleX(calc(1 + (var(--pointer-from-left) - 0.5) * 0.01));\n bottom: -1px;\n backface-visibility: hidden;\n will-change: transform;\n transition: transform 120ms ease-out;\n}\n\n.pc-avatar-content::before {\n content: '';\n position: absolute;\n inset: 0;\n z-index: 1;\n backdrop-filter: none;\n pointer-events: none;\n}\n\n.pc-user-info {\n position: absolute;\n --ui-inset: 20px;\n --ui-radius-bias: 6px;\n bottom: var(--ui-inset);\n left: var(--ui-inset);\n right: var(--ui-inset);\n z-index: 2;\n display: flex;\n align-items: center;\n justify-content: space-between;\n background: rgba(255, 255, 255, 0.1);\n backdrop-filter: blur(30px);\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: calc(max(0px, var(--card-radius) - var(--ui-inset) + var(--ui-radius-bias)));\n padding: 12px 14px;\n pointer-events: auto;\n}\n\n.pc-user-details {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.pc-mini-avatar {\n width: 48px;\n height: 48px;\n border-radius: 50%;\n overflow: hidden;\n border: 1px solid rgba(255, 255, 255, 0.1);\n flex-shrink: 0;\n}\n\n.pc-mini-avatar img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n border-radius: 50%;\n}\n\n.pc-user-text {\n display: flex;\n align-items: flex-start;\n flex-direction: column;\n gap: 6px;\n}\n\n.pc-handle {\n font-size: 14px;\n font-weight: 500;\n color: rgba(255, 255, 255, 0.9);\n line-height: 1;\n}\n\n.pc-status {\n font-size: 14px;\n color: rgba(255, 255, 255, 0.7);\n line-height: 1;\n}\n\n.pc-contact-btn {\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 8px;\n padding: 12px 16px;\n font-size: 12px;\n font-weight: 600;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n transition: all 0.2s ease;\n backdrop-filter: blur(10px);\n}\n\n.pc-contact-btn:hover {\n border-color: rgba(255, 255, 255, 0.4);\n transform: translateY(-1px);\n transition: all 0.2s ease;\n}\n\n.pc-content:not(.pc-avatar-content) {\n max-height: 100%;\n overflow: hidden;\n text-align: center;\n position: relative;\n transform: translate3d(\n calc(var(--pointer-from-left) * -6px + 3px),\n calc(var(--pointer-from-top) * -6px + 3px),\n 0.1px\n );\n z-index: 5;\n mix-blend-mode: luminosity;\n}\n\n.pc-details {\n width: 100%;\n position: absolute;\n top: 3em;\n display: flex;\n flex-direction: column;\n}\n\n.pc-details h3 {\n font-weight: 600;\n margin: 0;\n font-size: min(5svh, 3em);\n margin: 0;\n background-image: linear-gradient(to bottom, #fff, #6f6fbe);\n background-size: 1em 1.5em;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n -webkit-background-clip: text;\n}\n\n.pc-details p {\n font-weight: 600;\n position: relative;\n top: -12px;\n white-space: nowrap;\n font-size: 16px;\n margin: 0 auto;\n width: min-content;\n background-image: linear-gradient(to bottom, #fff, #4a4ac0);\n background-size: 1em 1.5em;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n -webkit-background-clip: text;\n}\n\n@keyframes glow-bg {\n 0% {\n --bgrotate: 0deg;\n }\n\n 100% {\n --bgrotate: 360deg;\n }\n}\n\n@keyframes holo-bg {\n 0% {\n background-position:\n 0 var(--background-y),\n 0 0,\n center;\n }\n\n 100% {\n background-position:\n 0 var(--background-y),\n 90% 90%,\n center;\n }\n}\n\n@media (max-width: 768px) {\n .pc-card {\n height: 70svh;\n max-height: 450px;\n }\n\n .pc-details {\n top: 2em;\n }\n\n .pc-details h3 {\n font-size: min(4svh, 2.5em);\n }\n\n .pc-details p {\n font-size: 14px;\n }\n\n .pc-user-info {\n --ui-inset: 15px;\n padding: 10px 12px;\n }\n\n .pc-mini-avatar {\n width: 28px;\n height: 28px;\n }\n\n .pc-user-details {\n gap: 10px;\n }\n\n .pc-handle {\n font-size: 13px;\n }\n\n .pc-status {\n font-size: 10px;\n }\n\n .pc-contact-btn {\n padding: 6px 12px;\n font-size: 11px;\n }\n}\n\n@media (max-width: 480px) {\n .pc-card {\n height: 60svh;\n max-height: 380px;\n }\n\n .pc-details {\n top: 1.5em;\n }\n\n .pc-details h3 {\n font-size: min(3.5svh, 2em);\n }\n\n .pc-details p {\n font-size: 12px;\n top: -8px;\n }\n\n .pc-user-info {\n --ui-inset: 12px;\n padding: 8px 10px;\n }\n\n .pc-mini-avatar {\n width: 24px;\n height: 24px;\n }\n\n .pc-user-details {\n gap: 8px;\n }\n\n .pc-handle {\n font-size: 12px;\n }\n\n .pc-status {\n font-size: 9px;\n }\n\n .pc-contact-btn {\n padding: 5px 10px;\n font-size: 10px;\n border-radius: 50px;\n }\n}\n\n@media (max-width: 320px) {\n .pc-card {\n height: 55svh;\n max-height: 320px;\n }\n\n .pc-details h3 {\n font-size: min(3svh, 1.5em);\n }\n\n .pc-details p {\n font-size: 11px;\n }\n\n .pc-user-info {\n padding: 6px 8px;\n }\n\n .pc-mini-avatar {\n width: 20px;\n height: 20px;\n }\n\n .pc-user-details {\n gap: 6px;\n }\n\n .pc-handle {\n font-size: 11px;\n }\n\n .pc-status {\n font-size: 8px;\n }\n\n .pc-contact-btn {\n padding: 4px 8px;\n font-size: 9px;\n border-radius: 50px;\n }\n}\n" }, { "type": "registry:component", - "path": "ProfileCard/ProfileCard.jsx", + "path": "ProfileCard.jsx", "content": "import React, { useEffect, useRef, useCallback, useMemo } from 'react';\nimport './ProfileCard.css';\n\nconst DEFAULT_INNER_GRADIENT = 'linear-gradient(145deg,#60496e8c 0%,#71C4FF44 100%)';\n\nconst ANIMATION_CONFIG = {\n INITIAL_DURATION: 1200,\n INITIAL_X_OFFSET: 70,\n INITIAL_Y_OFFSET: 60,\n DEVICE_BETA_OFFSET: 20,\n ENTER_TRANSITION_MS: 180\n};\n\nconst clamp = (v, min = 0, max = 100) => Math.min(Math.max(v, min), max);\nconst round = (v, precision = 3) => parseFloat(v.toFixed(precision));\nconst adjust = (v, fMin, fMax, tMin, tMax) => round(tMin + ((tMax - tMin) * (v - fMin)) / (fMax - fMin));\n\nconst ProfileCardComponent = ({\n avatarUrl = '',\n iconUrl = '',\n grainUrl = '',\n innerGradient,\n behindGlowEnabled = true,\n behindGlowColor,\n behindGlowSize,\n className = '',\n enableTilt = true,\n enableMobileTilt = false,\n mobileTiltSensitivity = 5,\n miniAvatarUrl,\n name = 'Javi A. Torres',\n title = 'Software Engineer',\n handle = 'javicodes',\n status = 'Online',\n contactText = 'Contact',\n showUserInfo = true,\n onContactClick\n}) => {\n const wrapRef = useRef(null);\n const shellRef = useRef(null);\n\n const enterTimerRef = useRef(null);\n const leaveRafRef = useRef(null);\n\n const tiltEngine = useMemo(() => {\n if (!enableTilt) return null;\n\n let rafId = null;\n let running = false;\n let lastTs = 0;\n\n let currentX = 0;\n let currentY = 0;\n let targetX = 0;\n let targetY = 0;\n\n const DEFAULT_TAU = 0.14;\n const INITIAL_TAU = 0.6;\n let initialUntil = 0;\n\n const setVarsFromXY = (x, y) => {\n const shell = shellRef.current;\n const wrap = wrapRef.current;\n if (!shell || !wrap) return;\n\n const width = shell.clientWidth || 1;\n const height = shell.clientHeight || 1;\n\n const percentX = clamp((100 / width) * x);\n const percentY = clamp((100 / height) * y);\n\n const centerX = percentX - 50;\n const centerY = percentY - 50;\n\n const properties = {\n '--pointer-x': `${percentX}%`,\n '--pointer-y': `${percentY}%`,\n '--background-x': `${adjust(percentX, 0, 100, 35, 65)}%`,\n '--background-y': `${adjust(percentY, 0, 100, 35, 65)}%`,\n '--pointer-from-center': `${clamp(Math.hypot(percentY - 50, percentX - 50) / 50, 0, 1)}`,\n '--pointer-from-top': `${percentY / 100}`,\n '--pointer-from-left': `${percentX / 100}`,\n '--rotate-x': `${round(-(centerX / 5))}deg`,\n '--rotate-y': `${round(centerY / 4)}deg`\n };\n\n for (const [k, v] of Object.entries(properties)) wrap.style.setProperty(k, v);\n };\n\n const step = ts => {\n if (!running) return;\n if (lastTs === 0) lastTs = ts;\n const dt = (ts - lastTs) / 1000;\n lastTs = ts;\n\n const tau = ts < initialUntil ? INITIAL_TAU : DEFAULT_TAU;\n const k = 1 - Math.exp(-dt / tau);\n\n currentX += (targetX - currentX) * k;\n currentY += (targetY - currentY) * k;\n\n setVarsFromXY(currentX, currentY);\n\n const stillFar = Math.abs(targetX - currentX) > 0.05 || Math.abs(targetY - currentY) > 0.05;\n\n if (stillFar || document.hasFocus()) {\n rafId = requestAnimationFrame(step);\n } else {\n running = false;\n lastTs = 0;\n if (rafId) {\n cancelAnimationFrame(rafId);\n rafId = null;\n }\n }\n };\n\n const start = () => {\n if (running) return;\n running = true;\n lastTs = 0;\n rafId = requestAnimationFrame(step);\n };\n\n return {\n setImmediate(x, y) {\n currentX = x;\n currentY = y;\n setVarsFromXY(currentX, currentY);\n },\n setTarget(x, y) {\n targetX = x;\n targetY = y;\n start();\n },\n toCenter() {\n const shell = shellRef.current;\n if (!shell) return;\n this.setTarget(shell.clientWidth / 2, shell.clientHeight / 2);\n },\n beginInitial(durationMs) {\n initialUntil = performance.now() + durationMs;\n start();\n },\n getCurrent() {\n return { x: currentX, y: currentY, tx: targetX, ty: targetY };\n },\n cancel() {\n if (rafId) cancelAnimationFrame(rafId);\n rafId = null;\n running = false;\n lastTs = 0;\n }\n };\n }, [enableTilt]);\n\n const getOffsets = (evt, el) => {\n const rect = el.getBoundingClientRect();\n return { x: evt.clientX - rect.left, y: evt.clientY - rect.top };\n };\n\n const handlePointerMove = useCallback(\n event => {\n const shell = shellRef.current;\n if (!shell || !tiltEngine) return;\n const { x, y } = getOffsets(event, shell);\n tiltEngine.setTarget(x, y);\n },\n [tiltEngine]\n );\n\n const handlePointerEnter = useCallback(\n event => {\n const shell = shellRef.current;\n if (!shell || !tiltEngine) return;\n\n shell.classList.add('active');\n shell.classList.add('entering');\n if (enterTimerRef.current) window.clearTimeout(enterTimerRef.current);\n enterTimerRef.current = window.setTimeout(() => {\n shell.classList.remove('entering');\n }, ANIMATION_CONFIG.ENTER_TRANSITION_MS);\n\n const { x, y } = getOffsets(event, shell);\n tiltEngine.setTarget(x, y);\n },\n [tiltEngine]\n );\n\n const handlePointerLeave = useCallback(() => {\n const shell = shellRef.current;\n if (!shell || !tiltEngine) return;\n\n tiltEngine.toCenter();\n\n const checkSettle = () => {\n const { x, y, tx, ty } = tiltEngine.getCurrent();\n const settled = Math.hypot(tx - x, ty - y) < 0.6;\n if (settled) {\n shell.classList.remove('active');\n leaveRafRef.current = null;\n } else {\n leaveRafRef.current = requestAnimationFrame(checkSettle);\n }\n };\n if (leaveRafRef.current) cancelAnimationFrame(leaveRafRef.current);\n leaveRafRef.current = requestAnimationFrame(checkSettle);\n }, [tiltEngine]);\n\n const handleDeviceOrientation = useCallback(\n event => {\n const shell = shellRef.current;\n if (!shell || !tiltEngine) return;\n\n const { beta, gamma } = event;\n if (beta == null || gamma == null) return;\n\n const centerX = shell.clientWidth / 2;\n const centerY = shell.clientHeight / 2;\n const x = clamp(centerX + gamma * mobileTiltSensitivity, 0, shell.clientWidth);\n const y = clamp(\n centerY + (beta - ANIMATION_CONFIG.DEVICE_BETA_OFFSET) * mobileTiltSensitivity,\n 0,\n shell.clientHeight\n );\n\n tiltEngine.setTarget(x, y);\n },\n [tiltEngine, mobileTiltSensitivity]\n );\n\n useEffect(() => {\n if (!enableTilt || !tiltEngine) return;\n\n const shell = shellRef.current;\n if (!shell) return;\n\n const pointerMoveHandler = handlePointerMove;\n const pointerEnterHandler = handlePointerEnter;\n const pointerLeaveHandler = handlePointerLeave;\n const deviceOrientationHandler = handleDeviceOrientation;\n\n shell.addEventListener('pointerenter', pointerEnterHandler);\n shell.addEventListener('pointermove', pointerMoveHandler);\n shell.addEventListener('pointerleave', pointerLeaveHandler);\n\n const handleClick = () => {\n if (!enableMobileTilt || location.protocol !== 'https:') return;\n const anyMotion = window.DeviceMotionEvent;\n if (anyMotion && typeof anyMotion.requestPermission === 'function') {\n anyMotion\n .requestPermission()\n .then(state => {\n if (state === 'granted') {\n window.addEventListener('deviceorientation', deviceOrientationHandler);\n }\n })\n .catch(console.error);\n } else {\n window.addEventListener('deviceorientation', deviceOrientationHandler);\n }\n };\n shell.addEventListener('click', handleClick);\n\n const initialX = (shell.clientWidth || 0) - ANIMATION_CONFIG.INITIAL_X_OFFSET;\n const initialY = ANIMATION_CONFIG.INITIAL_Y_OFFSET;\n tiltEngine.setImmediate(initialX, initialY);\n tiltEngine.toCenter();\n tiltEngine.beginInitial(ANIMATION_CONFIG.INITIAL_DURATION);\n\n return () => {\n shell.removeEventListener('pointerenter', pointerEnterHandler);\n shell.removeEventListener('pointermove', pointerMoveHandler);\n shell.removeEventListener('pointerleave', pointerLeaveHandler);\n shell.removeEventListener('click', handleClick);\n window.removeEventListener('deviceorientation', deviceOrientationHandler);\n if (enterTimerRef.current) window.clearTimeout(enterTimerRef.current);\n if (leaveRafRef.current) cancelAnimationFrame(leaveRafRef.current);\n tiltEngine.cancel();\n shell.classList.remove('entering');\n };\n }, [\n enableTilt,\n enableMobileTilt,\n tiltEngine,\n handlePointerMove,\n handlePointerEnter,\n handlePointerLeave,\n handleDeviceOrientation\n ]);\n\n const cardStyle = useMemo(\n () => ({\n '--icon': iconUrl ? `url(${iconUrl})` : 'none',\n '--grain': grainUrl ? `url(${grainUrl})` : 'none',\n '--inner-gradient': innerGradient ?? DEFAULT_INNER_GRADIENT,\n '--behind-glow-color': behindGlowColor ?? 'rgba(125, 190, 255, 0.67)',\n '--behind-glow-size': behindGlowSize ?? '50%'\n }),\n [iconUrl, grainUrl, innerGradient, behindGlowColor, behindGlowSize]\n );\n\n const handleContactClick = useCallback(() => {\n onContactClick?.();\n }, [onContactClick]);\n\n return (\n
    \n {behindGlowEnabled &&
    }\n
    \n
    \n
    \n
    \n
    \n
    \n {\n const t = e.target;\n t.style.display = 'none';\n }}\n />\n {showUserInfo && (\n
    \n
    \n
    \n {\n const t = e.target;\n t.style.opacity = '0.5';\n t.src = avatarUrl;\n }}\n />\n
    \n
    \n
    @{handle}
    \n
    {status}
    \n
    \n
    \n \n {contactText}\n \n
    \n )}\n
    \n
    \n
    \n

    {name}

    \n

    {title}

    \n
    \n
    \n
    \n
    \n
    \n
    \n );\n};\n\nconst ProfileCard = React.memo(ProfileCardComponent);\nexport default ProfileCard;\n" } ], diff --git a/public/r/ProfileCard-TS-CSS.json b/public/r/ProfileCard-TS-CSS.json index 1074f444..bfbae9c9 100644 --- a/public/r/ProfileCard-TS-CSS.json +++ b/public/r/ProfileCard-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "ProfileCard/ProfileCard.css", + "type": "registry:file", + "path": "ProfileCard.css", + "target": "@components/ProfileCard.css", "content": ":root {\n --pointer-x: 50%;\n --pointer-y: 50%;\n --pointer-from-center: 0;\n --pointer-from-top: 0.5;\n --pointer-from-left: 0.5;\n --card-opacity: 0;\n --rotate-x: 0deg;\n --rotate-y: 0deg;\n --background-x: 50%;\n --background-y: 50%;\n --grain: none;\n --icon: none;\n --behind-gradient: none;\n --behind-glow-color: rgba(125, 190, 255, 0.67);\n --behind-glow-size: 25%;\n --inner-gradient: none;\n --sunpillar-1: hsl(2, 100%, 73%);\n --sunpillar-2: hsl(53, 100%, 69%);\n --sunpillar-3: hsl(93, 100%, 69%);\n --sunpillar-4: hsl(176, 100%, 76%);\n --sunpillar-5: hsl(228, 100%, 74%);\n --sunpillar-6: hsl(283, 100%, 73%);\n --sunpillar-clr-1: var(--sunpillar-1);\n --sunpillar-clr-2: var(--sunpillar-2);\n --sunpillar-clr-3: var(--sunpillar-3);\n --sunpillar-clr-4: var(--sunpillar-4);\n --sunpillar-clr-5: var(--sunpillar-5);\n --sunpillar-clr-6: var(--sunpillar-6);\n --card-radius: 30px;\n}\n\n.pc-card-wrapper {\n perspective: 500px;\n transform: translate3d(0, 0, 0.1px);\n position: relative;\n touch-action: none;\n}\n\n.pc-behind {\n position: absolute;\n inset: 0;\n z-index: 0;\n pointer-events: none;\n background: radial-gradient(\n circle at var(--pointer-x) var(--pointer-y),\n var(--behind-glow-color) 0%,\n transparent var(--behind-glow-size)\n );\n filter: blur(50px) saturate(1.1);\n opacity: calc(0.8 * var(--card-opacity));\n transition: opacity 200ms ease;\n}\n\n.pc-card-wrapper:hover,\n.pc-card-wrapper.active {\n --card-opacity: 1;\n}\n\n.pc-card {\n height: 80svh;\n max-height: 540px;\n display: grid;\n aspect-ratio: 0.718;\n border-radius: var(--card-radius);\n position: relative;\n background-blend-mode: color-dodge, normal, normal, normal;\n animation: glow-bg 12s linear infinite;\n box-shadow: rgba(0, 0, 0, 0.8) calc((var(--pointer-from-left) * 10px) - 3px)\n calc((var(--pointer-from-top) * 20px) - 6px) 20px -5px;\n transition: transform 1s ease;\n transform: translateZ(0) rotateX(0deg) rotateY(0deg);\n background: rgba(0, 0, 0, 0.9);\n backface-visibility: hidden;\n overflow: hidden;\n}\n\n.pc-card:hover,\n.pc-card.active {\n transition: none;\n transform: translateZ(0) rotateX(var(--rotate-y)) rotateY(var(--rotate-x));\n}\n\n.pc-card-shell.entering .pc-card {\n transition: transform 180ms ease-out;\n}\n\n.pc-card-shell {\n position: relative;\n z-index: 1;\n}\n\n.pc-card * {\n display: grid;\n grid-area: 1/-1;\n border-radius: var(--card-radius);\n pointer-events: none;\n}\n\n.pc-inside {\n inset: 0;\n position: absolute;\n background-image: var(--inner-gradient);\n background-color: rgba(0, 0, 0, 0.9);\n transform: none;\n}\n\n.pc-shine {\n mask-image: var(--icon);\n mask-mode: luminance;\n mask-repeat: repeat;\n mask-size: 150%;\n mask-position: top calc(200% - (var(--background-y) * 5)) left calc(100% - var(--background-x));\n transition: filter 0.8s ease;\n filter: brightness(0.66) contrast(1.33) saturate(0.33) opacity(0.5);\n animation: holo-bg 18s linear infinite;\n animation-play-state: running;\n mix-blend-mode: color-dodge;\n}\n\n.pc-shine,\n.pc-shine::after {\n --space: 5%;\n --angle: -45deg;\n transform: translate3d(0, 0, 1px);\n overflow: hidden;\n z-index: 3;\n background: transparent;\n background-size: cover;\n background-position: center;\n background-image:\n repeating-linear-gradient(\n 0deg,\n var(--sunpillar-clr-1) calc(var(--space) * 1),\n var(--sunpillar-clr-2) calc(var(--space) * 2),\n var(--sunpillar-clr-3) calc(var(--space) * 3),\n var(--sunpillar-clr-4) calc(var(--space) * 4),\n var(--sunpillar-clr-5) calc(var(--space) * 5),\n var(--sunpillar-clr-6) calc(var(--space) * 6),\n var(--sunpillar-clr-1) calc(var(--space) * 7)\n ),\n repeating-linear-gradient(\n var(--angle),\n #0e152e 0%,\n hsl(180, 10%, 60%) 3.8%,\n hsl(180, 29%, 66%) 4.5%,\n hsl(180, 10%, 60%) 5.2%,\n #0e152e 10%,\n #0e152e 12%\n ),\n radial-gradient(\n farthest-corner circle at var(--pointer-x) var(--pointer-y),\n hsla(0, 0%, 0%, 0.1) 12%,\n hsla(0, 0%, 0%, 0.15) 20%,\n hsla(0, 0%, 0%, 0.25) 120%\n );\n background-position:\n 0 var(--background-y),\n var(--background-x) var(--background-y),\n center;\n background-blend-mode: color, hard-light;\n background-size:\n 500% 500%,\n 300% 300%,\n 200% 200%;\n background-repeat: repeat;\n}\n\n.pc-shine::before,\n.pc-shine::after {\n content: '';\n background-position: center;\n background-size: cover;\n grid-area: 1/1;\n opacity: 0;\n transition: opacity 0.8s ease;\n}\n\n.pc-card:hover .pc-shine,\n.pc-card.active .pc-shine {\n filter: brightness(0.85) contrast(1.5) saturate(0.5);\n animation-play-state: paused;\n}\n\n.pc-card:hover .pc-shine::before,\n.pc-card.active .pc-shine::before,\n.pc-card:hover .pc-shine::after,\n.pc-card.active .pc-shine::after {\n opacity: 1;\n}\n\n.pc-shine::before {\n background-image:\n linear-gradient(\n 45deg,\n var(--sunpillar-4),\n var(--sunpillar-5),\n var(--sunpillar-6),\n var(--sunpillar-1),\n var(--sunpillar-2),\n var(--sunpillar-3)\n ),\n radial-gradient(circle at var(--pointer-x) var(--pointer-y), hsl(0, 0%, 70%) 0%, hsla(0, 0%, 30%, 0.2) 90%),\n var(--grain);\n background-size:\n 250% 250%,\n 100% 100%,\n 220px 220px;\n background-position:\n var(--pointer-x) var(--pointer-y),\n center,\n calc(var(--pointer-x) * 0.01) calc(var(--pointer-y) * 0.01);\n background-blend-mode: color-dodge;\n filter: brightness(calc(2 - var(--pointer-from-center))) contrast(calc(var(--pointer-from-center) + 2))\n saturate(calc(0.5 + var(--pointer-from-center)));\n mix-blend-mode: luminosity;\n}\n\n.pc-shine::after {\n background-position:\n 0 var(--background-y),\n calc(var(--background-x) * 0.4) calc(var(--background-y) * 0.5),\n center;\n background-size:\n 200% 300%,\n 700% 700%,\n 100% 100%;\n mix-blend-mode: difference;\n filter: brightness(0.8) contrast(1.5);\n}\n\n.pc-glare {\n transform: translate3d(0, 0, 1.1px);\n overflow: hidden;\n background-image: radial-gradient(\n farthest-corner circle at var(--pointer-x) var(--pointer-y),\n hsl(248, 25%, 80%) 12%,\n hsla(207, 40%, 30%, 0.8) 90%\n );\n mix-blend-mode: overlay;\n filter: brightness(0.8) contrast(1.2);\n z-index: 4;\n}\n\n.pc-avatar-content {\n mix-blend-mode: luminosity;\n overflow: visible;\n transform: translateZ(2);\n backface-visibility: hidden;\n}\n\n.pc-avatar-content .avatar {\n width: 100%;\n position: absolute;\n left: 50%;\n transform-origin: 50% 100%;\n transform: translateX(calc(-50% + (var(--pointer-from-left) - 0.5) * 6px)) translateZ(0)\n scaleY(calc(1 + (var(--pointer-from-top) - 0.5) * 0.02)) scaleX(calc(1 + (var(--pointer-from-left) - 0.5) * 0.01));\n bottom: -1px;\n backface-visibility: hidden;\n will-change: transform;\n transition: transform 120ms ease-out;\n}\n\n.pc-avatar-content::before {\n content: '';\n position: absolute;\n inset: 0;\n z-index: 1;\n backdrop-filter: none;\n pointer-events: none;\n}\n\n.pc-user-info {\n position: absolute;\n --ui-inset: 20px;\n --ui-radius-bias: 6px;\n bottom: var(--ui-inset);\n left: var(--ui-inset);\n right: var(--ui-inset);\n z-index: 2;\n display: flex;\n align-items: center;\n justify-content: space-between;\n background: rgba(255, 255, 255, 0.1);\n backdrop-filter: blur(30px);\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: calc(max(0px, var(--card-radius) - var(--ui-inset) + var(--ui-radius-bias)));\n padding: 12px 14px;\n pointer-events: auto;\n}\n\n.pc-user-details {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.pc-mini-avatar {\n width: 48px;\n height: 48px;\n border-radius: 50%;\n overflow: hidden;\n border: 1px solid rgba(255, 255, 255, 0.1);\n flex-shrink: 0;\n}\n\n.pc-mini-avatar img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n border-radius: 50%;\n}\n\n.pc-user-text {\n display: flex;\n align-items: flex-start;\n flex-direction: column;\n gap: 6px;\n}\n\n.pc-handle {\n font-size: 14px;\n font-weight: 500;\n color: rgba(255, 255, 255, 0.9);\n line-height: 1;\n}\n\n.pc-status {\n font-size: 14px;\n color: rgba(255, 255, 255, 0.7);\n line-height: 1;\n}\n\n.pc-contact-btn {\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 8px;\n padding: 12px 16px;\n font-size: 12px;\n font-weight: 600;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n transition: all 0.2s ease;\n backdrop-filter: blur(10px);\n}\n\n.pc-contact-btn:hover {\n border-color: rgba(255, 255, 255, 0.4);\n transform: translateY(-1px);\n transition: all 0.2s ease;\n}\n\n.pc-content:not(.pc-avatar-content) {\n max-height: 100%;\n overflow: hidden;\n text-align: center;\n position: relative;\n transform: translate3d(\n calc(var(--pointer-from-left) * -6px + 3px),\n calc(var(--pointer-from-top) * -6px + 3px),\n 0.1px\n );\n z-index: 5;\n mix-blend-mode: luminosity;\n}\n\n.pc-details {\n width: 100%;\n position: absolute;\n top: 3em;\n display: flex;\n flex-direction: column;\n}\n\n.pc-details h3 {\n font-weight: 600;\n margin: 0;\n font-size: min(5svh, 3em);\n margin: 0;\n background-image: linear-gradient(to bottom, #fff, #6f6fbe);\n background-size: 1em 1.5em;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n -webkit-background-clip: text;\n}\n\n.pc-details p {\n font-weight: 600;\n position: relative;\n top: -12px;\n white-space: nowrap;\n font-size: 16px;\n margin: 0 auto;\n width: min-content;\n background-image: linear-gradient(to bottom, #fff, #4a4ac0);\n background-size: 1em 1.5em;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n -webkit-background-clip: text;\n}\n\n@keyframes glow-bg {\n 0% {\n --bgrotate: 0deg;\n }\n\n 100% {\n --bgrotate: 360deg;\n }\n}\n\n@keyframes holo-bg {\n 0% {\n background-position:\n 0 var(--background-y),\n 0 0,\n center;\n }\n\n 100% {\n background-position:\n 0 var(--background-y),\n 90% 90%,\n center;\n }\n}\n\n@media (max-width: 768px) {\n .pc-card {\n height: 70svh;\n max-height: 450px;\n }\n\n .pc-details {\n top: 2em;\n }\n\n .pc-details h3 {\n font-size: min(4svh, 2.5em);\n }\n\n .pc-details p {\n font-size: 14px;\n }\n\n .pc-user-info {\n --ui-inset: 15px;\n padding: 10px 12px;\n }\n\n .pc-mini-avatar {\n width: 28px;\n height: 28px;\n }\n\n .pc-user-details {\n gap: 10px;\n }\n\n .pc-handle {\n font-size: 13px;\n }\n\n .pc-status {\n font-size: 10px;\n }\n\n .pc-contact-btn {\n padding: 6px 12px;\n font-size: 11px;\n }\n}\n\n@media (max-width: 480px) {\n .pc-card {\n height: 60svh;\n max-height: 380px;\n }\n\n .pc-details {\n top: 1.5em;\n }\n\n .pc-details h3 {\n font-size: min(3.5svh, 2em);\n }\n\n .pc-details p {\n font-size: 12px;\n top: -8px;\n }\n\n .pc-user-info {\n --ui-inset: 12px;\n padding: 8px 10px;\n }\n\n .pc-mini-avatar {\n width: 24px;\n height: 24px;\n }\n\n .pc-user-details {\n gap: 8px;\n }\n\n .pc-handle {\n font-size: 12px;\n }\n\n .pc-status {\n font-size: 9px;\n }\n\n .pc-contact-btn {\n padding: 5px 10px;\n font-size: 10px;\n border-radius: 50px;\n }\n}\n\n@media (max-width: 320px) {\n .pc-card {\n height: 55svh;\n max-height: 320px;\n }\n\n .pc-details h3 {\n font-size: min(3svh, 1.5em);\n }\n\n .pc-details p {\n font-size: 11px;\n }\n\n .pc-user-info {\n padding: 6px 8px;\n }\n\n .pc-mini-avatar {\n width: 20px;\n height: 20px;\n }\n\n .pc-user-details {\n gap: 6px;\n }\n\n .pc-handle {\n font-size: 11px;\n }\n\n .pc-status {\n font-size: 8px;\n }\n\n .pc-contact-btn {\n padding: 4px 8px;\n font-size: 9px;\n border-radius: 50px;\n }\n}\n" }, { "type": "registry:component", - "path": "ProfileCard/ProfileCard.tsx", + "path": "ProfileCard.tsx", "content": "import React, { useEffect, useRef, useCallback, useMemo } from 'react';\nimport './ProfileCard.css';\n\ninterface ProfileCardProps {\n avatarUrl: string;\n iconUrl?: string;\n grainUrl?: string;\n innerGradient?: string;\n behindGlowEnabled?: boolean;\n behindGlowColor?: string;\n behindGlowSize?: string;\n className?: string;\n enableTilt?: boolean;\n enableMobileTilt?: boolean;\n mobileTiltSensitivity?: number;\n miniAvatarUrl?: string;\n name?: string;\n title?: string;\n handle?: string;\n status?: string;\n contactText?: string;\n showUserInfo?: boolean;\n onContactClick?: () => void;\n}\n\nconst DEFAULT_INNER_GRADIENT = 'linear-gradient(145deg,#60496e8c 0%,#71C4FF44 100%)';\n\nconst ANIMATION_CONFIG = {\n INITIAL_DURATION: 1200,\n INITIAL_X_OFFSET: 70,\n INITIAL_Y_OFFSET: 60,\n DEVICE_BETA_OFFSET: 20,\n ENTER_TRANSITION_MS: 180\n} as const;\n\nconst clamp = (v: number, min = 0, max = 100): number => Math.min(Math.max(v, min), max);\nconst round = (v: number, precision = 3): number => parseFloat(v.toFixed(precision));\nconst adjust = (v: number, fMin: number, fMax: number, tMin: number, tMax: number): number =>\n round(tMin + ((tMax - tMin) * (v - fMin)) / (fMax - fMin));\n\nconst ProfileCardComponent: React.FC = ({\n avatarUrl = '',\n iconUrl = '',\n grainUrl = '',\n innerGradient,\n behindGlowEnabled = true,\n behindGlowColor,\n behindGlowSize,\n className = '',\n enableTilt = true,\n enableMobileTilt = false,\n mobileTiltSensitivity = 5,\n miniAvatarUrl,\n name = 'Javi A. Torres',\n title = 'Software Engineer',\n handle = 'javicodes',\n status = 'Online',\n contactText = 'Contact',\n showUserInfo = true,\n onContactClick\n}) => {\n const wrapRef = useRef(null);\n const shellRef = useRef(null);\n\n const enterTimerRef = useRef(null);\n const leaveRafRef = useRef(null);\n\n const tiltEngine = useMemo(() => {\n if (!enableTilt) return null;\n\n let rafId: number | null = null;\n let running = false;\n let lastTs = 0;\n\n let currentX = 0;\n let currentY = 0;\n let targetX = 0;\n let targetY = 0;\n\n const DEFAULT_TAU = 0.14;\n const INITIAL_TAU = 0.6;\n let initialUntil = 0;\n\n const setVarsFromXY = (x: number, y: number) => {\n const shell = shellRef.current;\n const wrap = wrapRef.current;\n if (!shell || !wrap) return;\n\n const width = shell.clientWidth || 1;\n const height = shell.clientHeight || 1;\n\n const percentX = clamp((100 / width) * x);\n const percentY = clamp((100 / height) * y);\n\n const centerX = percentX - 50;\n const centerY = percentY - 50;\n\n const properties = {\n '--pointer-x': `${percentX}%`,\n '--pointer-y': `${percentY}%`,\n '--background-x': `${adjust(percentX, 0, 100, 35, 65)}%`,\n '--background-y': `${adjust(percentY, 0, 100, 35, 65)}%`,\n '--pointer-from-center': `${clamp(Math.hypot(percentY - 50, percentX - 50) / 50, 0, 1)}`,\n '--pointer-from-top': `${percentY / 100}`,\n '--pointer-from-left': `${percentX / 100}`,\n '--rotate-x': `${round(-(centerX / 5))}deg`,\n '--rotate-y': `${round(centerY / 4)}deg`\n } as Record;\n\n for (const [k, v] of Object.entries(properties)) wrap.style.setProperty(k, v);\n };\n\n const step = (ts: number) => {\n if (!running) return;\n if (lastTs === 0) lastTs = ts;\n const dt = (ts - lastTs) / 1000;\n lastTs = ts;\n\n const tau = ts < initialUntil ? INITIAL_TAU : DEFAULT_TAU;\n const k = 1 - Math.exp(-dt / tau);\n\n currentX += (targetX - currentX) * k;\n currentY += (targetY - currentY) * k;\n\n setVarsFromXY(currentX, currentY);\n\n const stillFar = Math.abs(targetX - currentX) > 0.05 || Math.abs(targetY - currentY) > 0.05;\n\n if (stillFar || document.hasFocus()) {\n rafId = requestAnimationFrame(step);\n } else {\n running = false;\n lastTs = 0;\n if (rafId) {\n cancelAnimationFrame(rafId);\n rafId = null;\n }\n }\n };\n\n const start = () => {\n if (running) return;\n running = true;\n lastTs = 0;\n rafId = requestAnimationFrame(step);\n };\n\n return {\n setImmediate(x: number, y: number) {\n currentX = x;\n currentY = y;\n setVarsFromXY(currentX, currentY);\n },\n setTarget(x: number, y: number) {\n targetX = x;\n targetY = y;\n start();\n },\n toCenter() {\n const shell = shellRef.current;\n if (!shell) return;\n this.setTarget(shell.clientWidth / 2, shell.clientHeight / 2);\n },\n beginInitial(durationMs: number) {\n initialUntil = performance.now() + durationMs;\n start();\n },\n getCurrent() {\n return { x: currentX, y: currentY, tx: targetX, ty: targetY };\n },\n cancel() {\n if (rafId) cancelAnimationFrame(rafId);\n rafId = null;\n running = false;\n lastTs = 0;\n }\n };\n }, [enableTilt]);\n\n const getOffsets = (evt: PointerEvent, el: HTMLElement) => {\n const rect = el.getBoundingClientRect();\n return { x: evt.clientX - rect.left, y: evt.clientY - rect.top };\n };\n\n const handlePointerMove = useCallback(\n (event: PointerEvent) => {\n const shell = shellRef.current;\n if (!shell || !tiltEngine) return;\n const { x, y } = getOffsets(event, shell);\n tiltEngine.setTarget(x, y);\n },\n [tiltEngine]\n );\n\n const handlePointerEnter = useCallback(\n (event: PointerEvent) => {\n const shell = shellRef.current;\n if (!shell || !tiltEngine) return;\n\n shell.classList.add('active');\n shell.classList.add('entering');\n if (enterTimerRef.current) window.clearTimeout(enterTimerRef.current);\n enterTimerRef.current = window.setTimeout(() => {\n shell.classList.remove('entering');\n }, ANIMATION_CONFIG.ENTER_TRANSITION_MS);\n\n const { x, y } = getOffsets(event, shell);\n tiltEngine.setTarget(x, y);\n },\n [tiltEngine]\n );\n\n const handlePointerLeave = useCallback(() => {\n const shell = shellRef.current;\n if (!shell || !tiltEngine) return;\n\n tiltEngine.toCenter();\n\n const checkSettle = () => {\n const { x, y, tx, ty } = tiltEngine.getCurrent();\n const settled = Math.hypot(tx - x, ty - y) < 0.6;\n if (settled) {\n shell.classList.remove('active');\n leaveRafRef.current = null;\n } else {\n leaveRafRef.current = requestAnimationFrame(checkSettle);\n }\n };\n if (leaveRafRef.current) cancelAnimationFrame(leaveRafRef.current);\n leaveRafRef.current = requestAnimationFrame(checkSettle);\n }, [tiltEngine]);\n\n const handleDeviceOrientation = useCallback(\n (event: DeviceOrientationEvent) => {\n const shell = shellRef.current;\n if (!shell || !tiltEngine) return;\n\n const { beta, gamma } = event;\n if (beta == null || gamma == null) return;\n\n const centerX = shell.clientWidth / 2;\n const centerY = shell.clientHeight / 2;\n const x = clamp(centerX + gamma * mobileTiltSensitivity, 0, shell.clientWidth);\n const y = clamp(\n centerY + (beta - ANIMATION_CONFIG.DEVICE_BETA_OFFSET) * mobileTiltSensitivity,\n 0,\n shell.clientHeight\n );\n\n tiltEngine.setTarget(x, y);\n },\n [tiltEngine, mobileTiltSensitivity]\n );\n\n useEffect(() => {\n if (!enableTilt || !tiltEngine) return;\n\n const shell = shellRef.current;\n if (!shell) return;\n\n const pointerMoveHandler = handlePointerMove as EventListener;\n const pointerEnterHandler = handlePointerEnter as EventListener;\n const pointerLeaveHandler = handlePointerLeave as EventListener;\n const deviceOrientationHandler = handleDeviceOrientation as EventListener;\n\n shell.addEventListener('pointerenter', pointerEnterHandler);\n shell.addEventListener('pointermove', pointerMoveHandler);\n shell.addEventListener('pointerleave', pointerLeaveHandler);\n\n const handleClick = () => {\n if (!enableMobileTilt || location.protocol !== 'https:') return;\n const anyMotion = window.DeviceMotionEvent as any;\n if (anyMotion && typeof anyMotion.requestPermission === 'function') {\n anyMotion\n .requestPermission()\n .then((state: string) => {\n if (state === 'granted') {\n window.addEventListener('deviceorientation', deviceOrientationHandler);\n }\n })\n .catch(console.error);\n } else {\n window.addEventListener('deviceorientation', deviceOrientationHandler);\n }\n };\n shell.addEventListener('click', handleClick);\n\n const initialX = (shell.clientWidth || 0) - ANIMATION_CONFIG.INITIAL_X_OFFSET;\n const initialY = ANIMATION_CONFIG.INITIAL_Y_OFFSET;\n tiltEngine.setImmediate(initialX, initialY);\n tiltEngine.toCenter();\n tiltEngine.beginInitial(ANIMATION_CONFIG.INITIAL_DURATION);\n\n return () => {\n shell.removeEventListener('pointerenter', pointerEnterHandler);\n shell.removeEventListener('pointermove', pointerMoveHandler);\n shell.removeEventListener('pointerleave', pointerLeaveHandler);\n shell.removeEventListener('click', handleClick);\n window.removeEventListener('deviceorientation', deviceOrientationHandler);\n if (enterTimerRef.current) window.clearTimeout(enterTimerRef.current);\n if (leaveRafRef.current) cancelAnimationFrame(leaveRafRef.current);\n tiltEngine.cancel();\n shell.classList.remove('entering');\n };\n }, [\n enableTilt,\n enableMobileTilt,\n tiltEngine,\n handlePointerMove,\n handlePointerEnter,\n handlePointerLeave,\n handleDeviceOrientation\n ]);\n\n const cardStyle = useMemo(\n () =>\n ({\n '--icon': iconUrl ? `url(${iconUrl})` : 'none',\n '--grain': grainUrl ? `url(${grainUrl})` : 'none',\n '--inner-gradient': innerGradient ?? DEFAULT_INNER_GRADIENT,\n '--behind-glow-color': behindGlowColor ?? 'rgba(125, 190, 255, 0.67)',\n '--behind-glow-size': behindGlowSize ?? '50%'\n }) as React.CSSProperties,\n [iconUrl, grainUrl, innerGradient, behindGlowColor, behindGlowSize]\n );\n\n const handleContactClick = useCallback(() => {\n onContactClick?.();\n }, [onContactClick]);\n\n return (\n
    \n {behindGlowEnabled &&
    }\n
    \n
    \n
    \n
    \n
    \n
    \n {\n const t = e.target as HTMLImageElement;\n t.style.display = 'none';\n }}\n />\n {showUserInfo && (\n
    \n
    \n
    \n {\n const t = e.target as HTMLImageElement;\n t.style.opacity = '0.5';\n t.src = avatarUrl;\n }}\n />\n
    \n
    \n
    @{handle}
    \n
    {status}
    \n
    \n
    \n \n {contactText}\n \n
    \n )}\n
    \n
    \n
    \n

    {name}

    \n

    {title}

    \n
    \n
    \n
    \n
    \n
    \n
    \n );\n};\n\nconst ProfileCard = React.memo(ProfileCardComponent);\nexport default ProfileCard;\n" } ], diff --git a/public/r/Radar-JS-CSS.json b/public/r/Radar-JS-CSS.json index 4c87e36c..baf3305e 100644 --- a/public/r/Radar-JS-CSS.json +++ b/public/r/Radar-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Radar/Radar.css", + "type": "registry:file", + "path": "Radar.css", + "target": "@components/Radar.css", "content": ".radar-container {\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "Radar/Radar.jsx", + "path": "Radar.jsx", "content": "import { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport { useEffect, useRef } from 'react';\n\nimport './Radar.css';\n\nfunction hexToVec3(hex) {\n const h = hex.replace('#', '');\n return [\n parseInt(h.slice(0, 2), 16) / 255,\n parseInt(h.slice(2, 4), 16) / 255,\n parseInt(h.slice(4, 6), 16) / 255\n ];\n}\n\nconst vertexShader = `\nattribute vec2 uv;\nattribute vec2 position;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0, 1);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float uTime;\nuniform vec3 uResolution;\nuniform float uSpeed;\nuniform float uScale;\nuniform float uRingCount;\nuniform float uSpokeCount;\nuniform float uRingThickness;\nuniform float uSpokeThickness;\nuniform float uSweepSpeed;\nuniform float uSweepWidth;\nuniform float uSweepLobes;\nuniform vec3 uColor;\nuniform vec3 uBgColor;\nuniform float uFalloff;\nuniform float uBrightness;\nuniform vec2 uMouse;\nuniform float uMouseInfluence;\nuniform bool uEnableMouse;\n\n#define TAU 6.28318530718\n#define PI 3.14159265359\n\nvoid main() {\n vec2 st = gl_FragCoord.xy / uResolution.xy;\n st = st * 2.0 - 1.0;\n st.x *= uResolution.x / uResolution.y;\n\n if (uEnableMouse) {\n vec2 mShift = (uMouse * 2.0 - 1.0);\n mShift.x *= uResolution.x / uResolution.y;\n st -= mShift * uMouseInfluence;\n }\n\n st *= uScale;\n\n float dist = length(st);\n float theta = atan(st.y, st.x);\n float t = uTime * uSpeed;\n\n float ringPhase = dist * uRingCount - t;\n float ringDist = abs(fract(ringPhase) - 0.5);\n float ringGlow = 1.0 - smoothstep(0.0, uRingThickness, ringDist);\n\n float spokeAngle = abs(fract(theta * uSpokeCount / TAU + 0.5) - 0.5) * TAU / uSpokeCount;\n float arcDist = spokeAngle * dist;\n float spokeGlow = (1.0 - smoothstep(0.0, uSpokeThickness, arcDist)) * smoothstep(0.0, 0.1, dist);\n\n float sweepPhase = t * uSweepSpeed;\n float sweepBeam = pow(max(0.5 * sin(uSweepLobes * theta + sweepPhase) + 0.5, 0.0), uSweepWidth);\n\n float fade = smoothstep(1.05, 0.85, dist) * pow(max(1.0 - dist, 0.0), uFalloff);\n\n float intensity = max((ringGlow + spokeGlow + sweepBeam) * fade * uBrightness, 0.0);\n vec3 col = uColor * intensity + uBgColor;\n\n float alpha = clamp(length(col), 0.0, 1.0);\n gl_FragColor = vec4(col, alpha);\n}\n`;\n\nexport default function Radar({\n speed = 1.0,\n scale = 0.5,\n ringCount = 10.0,\n spokeCount = 10.0,\n ringThickness = 0.05,\n spokeThickness = 0.01,\n sweepSpeed = 1.0,\n sweepWidth = 2.0,\n sweepLobes = 1.0,\n color = '#9f29ff',\n backgroundColor = '#000000',\n falloff = 2.0,\n brightness = 1.0,\n enableMouseInteraction = true,\n mouseInfluence = 0.1\n}) {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n const container = containerRef.current;\n const renderer = new Renderer({ alpha: true, premultipliedAlpha: false });\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n\n let program;\n let currentMouse = [0.5, 0.5];\n let targetMouse = [0.5, 0.5];\n\n function handleMouseMove(e) {\n const rect = gl.canvas.getBoundingClientRect();\n targetMouse = [\n (e.clientX - rect.left) / rect.width,\n 1.0 - (e.clientY - rect.top) / rect.height\n ];\n }\n\n function handleMouseLeave() {\n targetMouse = [0.5, 0.5];\n }\n\n function resize() {\n renderer.setSize(container.offsetWidth, container.offsetHeight);\n if (program) {\n program.uniforms.uResolution.value = [gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height];\n }\n }\n window.addEventListener('resize', resize);\n resize();\n\n const geometry = new Triangle(gl);\n program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uResolution: { value: [gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height] },\n uSpeed: { value: speed },\n uScale: { value: scale },\n uRingCount: { value: ringCount },\n uSpokeCount: { value: spokeCount },\n uRingThickness: { value: ringThickness },\n uSpokeThickness: { value: spokeThickness },\n uSweepSpeed: { value: sweepSpeed },\n uSweepWidth: { value: sweepWidth },\n uSweepLobes: { value: sweepLobes },\n uColor: { value: hexToVec3(color) },\n uBgColor: { value: hexToVec3(backgroundColor) },\n uFalloff: { value: falloff },\n uBrightness: { value: brightness },\n uMouse: { value: new Float32Array([0.5, 0.5]) },\n uMouseInfluence: { value: mouseInfluence },\n uEnableMouse: { value: enableMouseInteraction }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n container.appendChild(gl.canvas);\n\n if (enableMouseInteraction) {\n gl.canvas.addEventListener('mousemove', handleMouseMove);\n gl.canvas.addEventListener('mouseleave', handleMouseLeave);\n }\n\n let animationFrameId;\n\n function update(time) {\n animationFrameId = requestAnimationFrame(update);\n program.uniforms.uTime.value = time * 0.001;\n\n if (enableMouseInteraction) {\n currentMouse[0] += 0.05 * (targetMouse[0] - currentMouse[0]);\n currentMouse[1] += 0.05 * (targetMouse[1] - currentMouse[1]);\n program.uniforms.uMouse.value[0] = currentMouse[0];\n program.uniforms.uMouse.value[1] = currentMouse[1];\n } else {\n program.uniforms.uMouse.value[0] = 0.5;\n program.uniforms.uMouse.value[1] = 0.5;\n }\n\n renderer.render({ scene: mesh });\n }\n animationFrameId = requestAnimationFrame(update);\n\n return () => {\n cancelAnimationFrame(animationFrameId);\n window.removeEventListener('resize', resize);\n if (enableMouseInteraction) {\n gl.canvas.removeEventListener('mousemove', handleMouseMove);\n gl.canvas.removeEventListener('mouseleave', handleMouseLeave);\n }\n container.removeChild(gl.canvas);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, [speed, scale, ringCount, spokeCount, ringThickness, spokeThickness, sweepSpeed, sweepWidth, sweepLobes, color, backgroundColor, falloff, brightness, enableMouseInteraction, mouseInfluence]);\n\n return
    ;\n}\n" } ], diff --git a/public/r/Radar-TS-CSS.json b/public/r/Radar-TS-CSS.json index b611b098..40120b1a 100644 --- a/public/r/Radar-TS-CSS.json +++ b/public/r/Radar-TS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "Radar/Radar.css", + "type": "registry:file", + "path": "Radar.css", + "target": "@components/Radar.css", "content": ".radar-container {\n width: 100%;\n height: 100%;\n}\n" }, { "type": "registry:component", - "path": "Radar/Radar.tsx", + "path": "Radar.tsx", "content": "import { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport { useEffect, useRef } from 'react';\n\nimport './Radar.css';\n\ninterface RadarProps {\n speed?: number;\n scale?: number;\n ringCount?: number;\n spokeCount?: number;\n ringThickness?: number;\n spokeThickness?: number;\n sweepSpeed?: number;\n sweepWidth?: number;\n sweepLobes?: number;\n color?: string;\n backgroundColor?: string;\n falloff?: number;\n brightness?: number;\n enableMouseInteraction?: boolean;\n mouseInfluence?: number;\n}\n\nfunction hexToVec3(hex: string): [number, number, number] {\n const h = hex.replace('#', '');\n return [\n parseInt(h.slice(0, 2), 16) / 255,\n parseInt(h.slice(2, 4), 16) / 255,\n parseInt(h.slice(4, 6), 16) / 255\n ];\n}\n\nconst vertexShader = `\nattribute vec2 uv;\nattribute vec2 position;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0, 1);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float uTime;\nuniform vec3 uResolution;\nuniform float uSpeed;\nuniform float uScale;\nuniform float uRingCount;\nuniform float uSpokeCount;\nuniform float uRingThickness;\nuniform float uSpokeThickness;\nuniform float uSweepSpeed;\nuniform float uSweepWidth;\nuniform float uSweepLobes;\nuniform vec3 uColor;\nuniform vec3 uBgColor;\nuniform float uFalloff;\nuniform float uBrightness;\nuniform vec2 uMouse;\nuniform float uMouseInfluence;\nuniform bool uEnableMouse;\n\n#define TAU 6.28318530718\n#define PI 3.14159265359\n\nvoid main() {\n vec2 st = gl_FragCoord.xy / uResolution.xy;\n st = st * 2.0 - 1.0;\n st.x *= uResolution.x / uResolution.y;\n\n if (uEnableMouse) {\n vec2 mShift = (uMouse * 2.0 - 1.0);\n mShift.x *= uResolution.x / uResolution.y;\n st -= mShift * uMouseInfluence;\n }\n\n st *= uScale;\n\n float dist = length(st);\n float theta = atan(st.y, st.x);\n float t = uTime * uSpeed;\n\n float ringPhase = dist * uRingCount - t;\n float ringDist = abs(fract(ringPhase) - 0.5);\n float ringGlow = 1.0 - smoothstep(0.0, uRingThickness, ringDist);\n\n float spokeAngle = abs(fract(theta * uSpokeCount / TAU + 0.5) - 0.5) * TAU / uSpokeCount;\n float arcDist = spokeAngle * dist;\n float spokeGlow = (1.0 - smoothstep(0.0, uSpokeThickness, arcDist)) * smoothstep(0.0, 0.1, dist);\n\n float sweepPhase = t * uSweepSpeed;\n float sweepBeam = pow(max(0.5 * sin(uSweepLobes * theta + sweepPhase) + 0.5, 0.0), uSweepWidth);\n\n float fade = smoothstep(1.05, 0.85, dist) * pow(max(1.0 - dist, 0.0), uFalloff);\n\n float intensity = max((ringGlow + spokeGlow + sweepBeam) * fade * uBrightness, 0.0);\n vec3 col = uColor * intensity + uBgColor;\n\n float alpha = clamp(length(col), 0.0, 1.0);\n gl_FragColor = vec4(col, alpha);\n}\n`;\n\nexport default function Radar({\n speed = 1.0,\n scale = 0.5,\n ringCount = 10.0,\n spokeCount = 10.0,\n ringThickness = 0.05,\n spokeThickness = 0.01,\n sweepSpeed = 1.0,\n sweepWidth = 2.0,\n sweepLobes = 1.0,\n color = '#9f29ff',\n backgroundColor = '#000000',\n falloff = 2.0,\n brightness = 1.0,\n enableMouseInteraction = true,\n mouseInfluence = 0.1\n}: RadarProps) {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n const container = containerRef.current;\n const renderer = new Renderer({ alpha: true, premultipliedAlpha: false });\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n\n let program: Program;\n let currentMouse = [0.5, 0.5];\n let targetMouse = [0.5, 0.5];\n\n function handleMouseMove(e: MouseEvent) {\n const rect = gl.canvas.getBoundingClientRect();\n targetMouse = [\n (e.clientX - rect.left) / rect.width,\n 1.0 - (e.clientY - rect.top) / rect.height\n ];\n }\n\n function handleMouseLeave() {\n targetMouse = [0.5, 0.5];\n }\n\n function resize() {\n renderer.setSize(container.offsetWidth, container.offsetHeight);\n if (program) {\n program.uniforms.uResolution.value = [gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height];\n }\n }\n window.addEventListener('resize', resize);\n resize();\n\n const geometry = new Triangle(gl);\n program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uResolution: { value: [gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height] },\n uSpeed: { value: speed },\n uScale: { value: scale },\n uRingCount: { value: ringCount },\n uSpokeCount: { value: spokeCount },\n uRingThickness: { value: ringThickness },\n uSpokeThickness: { value: spokeThickness },\n uSweepSpeed: { value: sweepSpeed },\n uSweepWidth: { value: sweepWidth },\n uSweepLobes: { value: sweepLobes },\n uColor: { value: hexToVec3(color) },\n uBgColor: { value: hexToVec3(backgroundColor) },\n uFalloff: { value: falloff },\n uBrightness: { value: brightness },\n uMouse: { value: new Float32Array([0.5, 0.5]) },\n uMouseInfluence: { value: mouseInfluence },\n uEnableMouse: { value: enableMouseInteraction }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n container.appendChild(gl.canvas);\n\n if (enableMouseInteraction) {\n gl.canvas.addEventListener('mousemove', handleMouseMove);\n gl.canvas.addEventListener('mouseleave', handleMouseLeave);\n }\n\n let animationFrameId: number;\n\n function update(time: number) {\n animationFrameId = requestAnimationFrame(update);\n program.uniforms.uTime.value = time * 0.001;\n\n if (enableMouseInteraction) {\n currentMouse[0] += 0.05 * (targetMouse[0] - currentMouse[0]);\n currentMouse[1] += 0.05 * (targetMouse[1] - currentMouse[1]);\n program.uniforms.uMouse.value[0] = currentMouse[0];\n program.uniforms.uMouse.value[1] = currentMouse[1];\n } else {\n program.uniforms.uMouse.value[0] = 0.5;\n program.uniforms.uMouse.value[1] = 0.5;\n }\n\n renderer.render({ scene: mesh });\n }\n animationFrameId = requestAnimationFrame(update);\n\n return () => {\n cancelAnimationFrame(animationFrameId);\n window.removeEventListener('resize', resize);\n if (enableMouseInteraction) {\n gl.canvas.removeEventListener('mousemove', handleMouseMove);\n gl.canvas.removeEventListener('mouseleave', handleMouseLeave);\n }\n container.removeChild(gl.canvas);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n }, [speed, scale, ringCount, spokeCount, ringThickness, spokeThickness, sweepSpeed, sweepWidth, sweepLobes, color, backgroundColor, falloff, brightness, enableMouseInteraction, mouseInfluence]);\n\n return
    ;\n}\n" } ], diff --git a/public/r/ReflectiveCard-JS-CSS.json b/public/r/ReflectiveCard-JS-CSS.json index 3975e063..f5d070c1 100644 --- a/public/r/ReflectiveCard-JS-CSS.json +++ b/public/r/ReflectiveCard-JS-CSS.json @@ -6,13 +6,14 @@ "type": "registry:component", "files": [ { - "type": "registry:component", - "path": "ReflectiveCard/ReflectiveCard.css", + "type": "registry:file", + "path": "ReflectiveCard.css", + "target": "@components/ReflectiveCard.css", "content": ".reflective-card-container {\n position: relative;\n width: 320px;\n height: 500px;\n border-radius: 20px;\n overflow: hidden;\n background: #1a1a1a;\n box-shadow:\n 0 20px 50px rgba(0, 0, 0, 0.5),\n 0 0 0 1px rgba(255, 255, 255, 0.1) inset;\n isolation: isolate;\n font-family: 'Inter', sans-serif;\n}\n\n.reflective-svg-filters {\n position: absolute;\n width: 0;\n height: 0;\n pointer-events: none;\n opacity: 0;\n}\n\n.reflective-video {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n transform: scale(1.2) scaleX(-1);\n\n filter: saturate(var(--saturation, 0)) contrast(120%) brightness(110%) blur(var(--blur-strength, 12px))\n url(#metallic-displacement);\n\n z-index: 0;\n opacity: 0.9;\n transition: filter 0.3s ease;\n}\n\n.reflective-noise {\n position: absolute;\n inset: 0;\n z-index: 1;\n opacity: var(--roughness, 0.4);\n pointer-events: none;\n background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E\");\n mix-blend-mode: overlay;\n}\n\n.reflective-sheen {\n position: absolute;\n inset: 0;\n z-index: 2;\n background: linear-gradient(\n 135deg,\n rgba(255, 255, 255, 0.4) 0%,\n rgba(255, 255, 255, 0.1) 40%,\n rgba(255, 255, 255, 0) 50%,\n rgba(255, 255, 255, 0.1) 60%,\n rgba(255, 255, 255, 0.3) 100%\n );\n pointer-events: none;\n mix-blend-mode: overlay;\n opacity: var(--metalness, 1);\n}\n\n.reflective-border {\n position: absolute;\n inset: 0;\n border-radius: 20px;\n padding: 1px;\n background: linear-gradient(\n 135deg,\n rgba(255, 255, 255, 0.8) 0%,\n rgba(255, 255, 255, 0.2) 50%,\n rgba(255, 255, 255, 0.6) 100%\n );\n -webkit-mask:\n linear-gradient(#fff 0 0) content-box,\n linear-gradient(#fff 0 0);\n -webkit-mask-composite: xor;\n mask:\n linear-gradient(#fff 0 0) content-box,\n linear-gradient(#fff 0 0);\n mask-composite: exclude;\n z-index: 20;\n pointer-events: none;\n}\n\n.reflective-content {\n position: relative;\n z-index: 10;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n padding: 32px;\n color: var(--text-color, white);\n background: var(--overlay-color, rgba(255, 255, 255, 0.05));\n}\n\n.card-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n border-bottom: 1px solid rgba(255, 255, 255, 0.2);\n padding-bottom: 16px;\n}\n\n.security-badge {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.1em;\n padding: 4px 8px;\n background: rgba(255, 255, 255, 0.1);\n border-radius: 4px;\n border: 1px solid rgba(255, 255, 255, 0.2);\n}\n\n.status-icon {\n opacity: 0.8;\n}\n\n.card-body {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: end;\n align-items: center;\n text-align: center;\n gap: 24px;\n margin-bottom: 2em;\n}\n\n.user-name {\n font-size: 24px;\n font-weight: 700;\n letter-spacing: 0.05em;\n margin: 0 0 8px 0;\n text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n}\n\n.user-role {\n font-size: 12px;\n letter-spacing: 0.2em;\n opacity: 0.7;\n margin: 0;\n text-transform: uppercase;\n}\n\n.card-footer {\n display: flex;\n justify-content: space-between;\n align-items: flex-end;\n border-top: 1px solid rgba(255, 255, 255, 0.2);\n padding-top: 24px;\n}\n\n.id-section {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.label {\n font-size: 9px;\n letter-spacing: 0.1em;\n opacity: 0.6;\n}\n\n.value {\n font-family: monospace;\n font-size: 14px;\n letter-spacing: 0.05em;\n}\n\n.fingerprint-icon {\n opacity: 0.4;\n}\n" }, { "type": "registry:component", - "path": "ReflectiveCard/ReflectiveCard.jsx", + "path": "ReflectiveCard.jsx", "content": "import { useEffect, useRef } from 'react';\nimport './ReflectiveCard.css';\nimport { Fingerprint, Activity, Lock } from 'lucide-react';\n\nconst ReflectiveCard = ({\n blurStrength = 12,\n color = 'white',\n metalness = 1,\n roughness = 0.4,\n overlayColor = 'rgba(255, 255, 255, 0.1)',\n displacementStrength = 20,\n noiseScale = 1,\n specularConstant = 1.2,\n grayscale = 1,\n glassDistortion = 0,\n className = '',\n style = {}\n}) => {\n const videoRef = useRef(null);\n\n useEffect(() => {\n let stream = null;\n\n const startWebcam = async () => {\n try {\n stream = await navigator.mediaDevices.getUserMedia({\n video: {\n width: { ideal: 640 },\n height: { ideal: 480 },\n facingMode: 'user'\n }\n });\n\n if (videoRef.current) {\n videoRef.current.srcObject = stream;\n }\n } catch (err) {\n console.error('Error accessing webcam:', err);\n }\n };\n\n startWebcam();\n\n return () => {\n if (stream) {\n stream.getTracks().forEach(track => track.stop());\n }\n };\n }, []);\n\n const baseFrequency = 0.03 / Math.max(0.1, noiseScale);\n const saturation = 1 - Math.max(0, Math.min(1, grayscale));\n\n const cssVariables = {\n '--blur-strength': `${blurStrength}px`,\n '--metalness': metalness,\n '--roughness': roughness,\n '--overlay-color': overlayColor,\n '--text-color': color,\n '--saturation': saturation\n };\n\n return (\n
    \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n