reborn-ui 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/dist/index.js +871 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
- package/registry/.gitkeep +2 -0
- package/registry/components/animate-grid.json +18 -0
- package/registry/components/animated-beam.json +16 -0
- package/registry/components/animated-circular-progressbar.json +16 -0
- package/registry/components/animated-list.json +22 -0
- package/registry/components/animated-testimonials.json +18 -0
- package/registry/components/animated-tooltip.json +18 -0
- package/registry/components/apple-card-carousel.json +35 -0
- package/registry/components/aurora-background.json +16 -0
- package/registry/components/balance-slider.json +16 -0
- package/registry/components/bending-gallery.json +18 -0
- package/registry/components/bento-grid.json +24 -0
- package/registry/components/bg-black-hole.json +14 -0
- package/registry/components/bg-bubbles.json +18 -0
- package/registry/components/bg-falling-stars.json +16 -0
- package/registry/components/bg-neural.json +14 -0
- package/registry/components/bg-particle-whirlpool.json +18 -0
- package/registry/components/bg-silk.json +16 -0
- package/registry/components/bg-stars.json +18 -0
- package/registry/components/bg-stractium.json +16 -0
- package/registry/components/blur-reveal.json +18 -0
- package/registry/components/book.json +28 -0
- package/registry/components/border-beam.json +16 -0
- package/registry/components/box-reveal.json +18 -0
- package/registry/components/card-3d.json +24 -0
- package/registry/components/card-spotlight.json +16 -0
- package/registry/components/carousel-3d.json +15 -0
- package/registry/components/color-picker.json +26 -0
- package/registry/components/colourful-text.json +18 -0
- package/registry/components/compare.json +22 -0
- package/registry/components/confetti.json +22 -0
- package/registry/components/container-scroll.json +26 -0
- package/registry/components/container-text-flip.json +19 -0
- package/registry/components/cosmic-portal.json +18 -0
- package/registry/components/direction-aware-hover.json +16 -0
- package/registry/components/dock.json +32 -0
- package/registry/components/expandable-gallery.json +16 -0
- package/registry/components/file-tree.json +28 -0
- package/registry/components/file-upload.json +22 -0
- package/registry/components/flickering-grid.json +16 -0
- package/registry/components/flip-card.json +16 -0
- package/registry/components/flip-words.json +16 -0
- package/registry/components/fluid-cursor.json +16 -0
- package/registry/components/focus.json +16 -0
- package/registry/components/github-globe.json +23 -0
- package/registry/components/glare-card.json +18 -0
- package/registry/components/globe.json +19 -0
- package/registry/components/glow-border.json +16 -0
- package/registry/components/glowing-effect.json +18 -0
- package/registry/components/gradient-button.json +16 -0
- package/registry/components/halo-search.json +16 -0
- package/registry/components/hyper-text.json +19 -0
- package/registry/components/icon-cloud.json +16 -0
- package/registry/components/image-trail-cursor.json +22 -0
- package/registry/components/images-slider.json +18 -0
- package/registry/components/infinite-grid.json +47 -0
- package/registry/components/input.json +18 -0
- package/registry/components/interactive-grid-pattern.json +16 -0
- package/registry/components/interactive-hover-button.json +16 -0
- package/registry/components/iphone-mockup.json +16 -0
- package/registry/components/lamp-effect.json +16 -0
- package/registry/components/lens.json +18 -0
- package/registry/components/letter-pullup.json +18 -0
- package/registry/components/light-speed.json +31 -0
- package/registry/components/line-shadow-text.json +16 -0
- package/registry/components/link-preview.json +16 -0
- package/registry/components/liquid-background.json +18 -0
- package/registry/components/liquid-glass.json +16 -0
- package/registry/components/liquid-logo.json +24 -0
- package/registry/components/logo-cloud.json +24 -0
- package/registry/components/logo-origami.json +22 -0
- package/registry/components/marquee.json +20 -0
- package/registry/components/meteors.json +16 -0
- package/registry/components/morphing-tabs.json +16 -0
- package/registry/components/morphing-text.json +16 -0
- package/registry/components/multi-step-loader.json +16 -0
- package/registry/components/neon-border.json +16 -0
- package/registry/components/number-ticker.json +18 -0
- package/registry/components/orbit.json +16 -0
- package/registry/components/particle-image.json +24 -0
- package/registry/components/particles-bg.json +18 -0
- package/registry/components/pattern-background.json +18 -0
- package/registry/components/photo-gallery.json +16 -0
- package/registry/components/radiant-text.json +16 -0
- package/registry/components/rainbow-button.json +16 -0
- package/registry/components/ripple-button.json +16 -0
- package/registry/components/ripple.json +24 -0
- package/registry/components/safari-mockup.json +16 -0
- package/registry/components/scratch-to-reveal.json +18 -0
- package/registry/components/scroll-island.json +20 -0
- package/registry/components/shader-toy.json +22 -0
- package/registry/components/shimmer-button.json +16 -0
- package/registry/components/sleek-line-cursor.json +12 -0
- package/registry/components/smooth-cursor.json +23 -0
- package/registry/components/snowfall-bg.json +18 -0
- package/registry/components/sparkles-text.json +18 -0
- package/registry/components/sparkles.json +18 -0
- package/registry/components/spinning-text.json +18 -0
- package/registry/components/spline.json +23 -0
- package/registry/components/spring-calendar.json +22 -0
- package/registry/components/svg-mask.json +16 -0
- package/registry/components/tailed-cursor.json +14 -0
- package/registry/components/testimonial-slider.json +16 -0
- package/registry/components/tetris.json +19 -0
- package/registry/components/text-3d.json +16 -0
- package/registry/components/text-generate-effect.json +16 -0
- package/registry/components/text-glitch.json +12 -0
- package/registry/components/text-highlight.json +16 -0
- package/registry/components/text-hover-effect.json +16 -0
- package/registry/components/text-reveal-card.json +20 -0
- package/registry/components/text-reveal.json +18 -0
- package/registry/components/text-scroll-reveal.json +20 -0
- package/registry/components/timeline.json +18 -0
- package/registry/components/tracing-beam.json +19 -0
- package/registry/components/vanishing-input.json +18 -0
- package/registry/components/video-text.json +16 -0
- package/registry/components/vortex.json +19 -0
- package/registry/components/warp-background.json +22 -0
- package/registry/components/wavy-background.json +19 -0
- package/registry/components/world-map.json +19 -0
- package/registry/registry.json +2007 -0
- package/templates/composables/useMouseState.ts +21 -0
- package/templates/lib/utils.ts +13 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "infinite-grid",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"gsap",
|
|
5
|
+
"ogl"
|
|
6
|
+
],
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"path": "createTexture.ts",
|
|
10
|
+
"content": "/**\r\n * @fileoverview Card Texture Generation Utilities for OGL\r\n *\r\n * This module provides utilities for generating Canvas-based textures used\r\n * in the InfiniteGridClass system. Each card requires two textures:\r\n *\r\n * 1. Foreground Texture: Contains the main card content (title, image, tags, date)\r\n * 2. Background Texture: Contains a blurred, darkened version of the card image\r\n *\r\n * The textures are generated using HTML5 Canvas 2D API and converted to\r\n * OGL Textures with proper configuration.\r\n *\r\n * Key Features:\r\n * - Automatic text truncation with ellipsis\r\n * - Image loading with fallback handling\r\n * - Styled tag pills with rounded corners\r\n * - Responsive layout within fixed canvas dimensions\r\n * - Blur effects for background textures\r\n * - Proper error handling for failed image loads\r\n *\r\n * Usage:\r\n * ```typescript\r\n * import { generateForegroundTexture, generateBackgroundTexture } from './createTexture';\r\n *\r\n * const cardData = {\r\n * title: \"Project Title\",\r\n * image: \"/path/to/image.jpg\",\r\n * tags: [\"web\", \"ogl\"],\r\n * date: \"2024\"\r\n * };\r\n *\r\n * const foregroundTexture = await generateForegroundTexture(cardData, gl);\r\n * const backgroundTexture = await generateBackgroundTexture(cardData, gl);\r\n * ```\r\n */\r\n\r\n// Card Texture Generation Utilities for OGL\r\n\r\nimport { Texture, Renderer } from \"ogl\"; // Required for OGL Texture\r\n\r\n/**\r\n * Represents the data structure for a single card/tile\r\n * This interface must match the CardData interface in InfiniteGridClass.ts\r\n */\r\ninterface CardData {\r\n /** The main title text displayed prominently on the card */\r\n title: string;\r\n /** Badge text (currently not implemented in the rendering pipeline) */\r\n badge: string;\r\n /** Detailed description text for the card content (optional) */\r\n description?: string;\r\n /** Array of tag strings that will be displayed as styled pills */\r\n tags: string[];\r\n /** Date string displayed in the bottom-right corner */\r\n date: string;\r\n /** Optional image URL - falls back to '/photo.png' if not provided */\r\n image?: string;\r\n}\r\n\r\n/**\r\n * Canvas dimensions for all generated textures\r\n * These dimensions affect the resolution and memory usage of the textures\r\n */\r\nconst cardWidth = 512;\r\nconst cardHeight = 512;\r\nconst padding = 30;\r\n\r\n/**\r\n * Creates a canvas element with 2D rendering context\r\n *\r\n * This helper function ensures consistent canvas setup across all texture\r\n * generation functions and provides proper error handling for context creation.\r\n *\r\n * @returns Object containing the canvas element and its 2D context\r\n * @throws {Error} If 2D context creation fails\r\n */\r\nfunction createCanvasContext(): { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } {\r\n const canvas = document.createElement(\"canvas\");\r\n canvas.width = cardWidth;\r\n canvas.height = cardHeight;\r\n const ctx = canvas.getContext(\"2d\");\r\n if (!ctx) {\r\n throw new Error(\"Failed to get 2D context for canvas\");\r\n }\r\n return { canvas, ctx };\r\n}\r\n\r\n// Option 1: Pre-generate textures once, reuse them\r\nconst textureCache = new Map<string, Texture>();\r\n\r\n/**\r\n * Generates the foreground texture for a card using Canvas 2D API\r\n *\r\n * This function creates the main visible content of each card including:\r\n * - Title text with automatic truncation and ellipsis\r\n * - Main image with aspect ratio preservation and centering\r\n * - Styled tag pills at the bottom\r\n * - Date text in the bottom-right corner\r\n * - Border outline around the entire card\r\n *\r\n * The generated texture is used for the front-facing mesh that users\r\n * can interact with (hover and click).\r\n *\r\n * @param data - Card data containing title, image, tags, date, etc.\r\n * @param renderer - OGL Renderer for texture creation\r\n * @returns Promise resolving to an OGL Texture\r\n *\r\n * @example\r\n * ```typescript\r\n * const cardData = {\r\n * title: \"Amazing Project\",\r\n * image: \"/images/project.jpg\",\r\n * tags: [\"web\", \"ogl\"],\r\n * date: \"2024\",\r\n * badge: \"NEW\",\r\n * description: \"A cool project\"\r\n * };\r\n * const texture = await generateForegroundTexture(cardData, renderer);\r\n * ```\r\n */\r\nexport async function generateForegroundTexture(\r\n data: CardData,\r\n renderer: Renderer,\r\n): Promise<Texture> {\r\n const cacheKey = `${data.title}-${data.tags?.join(\"-\")}`;\r\n if (textureCache.has(cacheKey)) {\r\n return textureCache.get(cacheKey)!;\r\n }\r\n\r\n const { canvas, ctx } = createCanvasContext();\r\n\r\n // Set default styles\r\n ctx.fillStyle = \"white\";\r\n ctx.strokeStyle = \"rgba(60, 60, 60, 1)\";\r\n ctx.lineWidth = 1;\r\n\r\n // Card background and border (transparent for foreground to show background)\r\n ctx.beginPath();\r\n ctx.rect(0, 0, cardWidth, cardHeight);\r\n ctx.stroke(); // Draw border\r\n // ctx.fill() is not needed as background is transparent\r\n\r\n let currentY = padding;\r\n\r\n // Title Text\r\n ctx.font = \"24px Arial, sans-serif\";\r\n ctx.fillStyle = \"white\";\r\n ctx.textBaseline = \"top\";\r\n\r\n // Measure text to determine actual width\r\n const titleText = data.title;\r\n const titleMaxWidth = cardWidth - padding * 2;\r\n\r\n // For `ellipsis` and `wrap: 'none'`, we need to manually truncate\r\n let truncatedTitle = titleText;\r\n let textMetrics = ctx.measureText(truncatedTitle);\r\n\r\n // Simple truncation if text exceeds maxWidth\r\n while (textMetrics.width > titleMaxWidth && truncatedTitle.length > 3) {\r\n truncatedTitle = truncatedTitle.substring(0, truncatedTitle.length - 4) + \"...\";\r\n textMetrics = ctx.measureText(truncatedTitle);\r\n }\r\n ctx.fillText(truncatedTitle, padding, currentY);\r\n\r\n const headerHeight = 24; // Assuming 24px font height is a good approximation for header height\r\n\r\n currentY += headerHeight + 30; // Move Y cursor down\r\n\r\n const topElementsMaxY = currentY;\r\n const bottomReservedSpace = 100;\r\n const availableImageHeight = cardHeight - topElementsMaxY - bottomReservedSpace;\r\n const availableImageWidth = cardWidth - padding * 2;\r\n\r\n // Image Loading and Placement\r\n const imageObj = new Image();\r\n imageObj.crossOrigin = \"anonymous\";\r\n imageObj.src = data.image || \"/photo.png\"; // Fallback image\r\n\r\n const loadImagePromise = new Promise<void>((resolve) => {\r\n imageObj.onload = () => {\r\n let imgWidth = imageObj.naturalWidth;\r\n let imgHeight = imageObj.naturalHeight;\r\n const naturalAspectRatio = imgWidth / imgHeight;\r\n\r\n // Scale image to fit within available space, maintaining aspect ratio\r\n if (imgWidth > availableImageWidth || imgHeight > availableImageHeight) {\r\n if (availableImageWidth / naturalAspectRatio <= availableImageHeight) {\r\n imgWidth = availableImageWidth;\r\n imgHeight = availableImageWidth / naturalAspectRatio;\r\n } else {\r\n imgHeight = availableImageHeight;\r\n imgWidth = availableImageHeight * naturalAspectRatio;\r\n }\r\n }\r\n\r\n const imageX = padding + (availableImageWidth - imgWidth) / 2;\r\n const imageY = topElementsMaxY + (availableImageHeight - imgHeight) / 2;\r\n\r\n // Draw image (no direct cornerRadius for images in vanilla canvas,\r\n // you'd need to clip the path if truly desired. For simplicity, we draw directly).\r\n ctx.drawImage(imageObj, imageX, imageY, imgWidth, imgHeight);\r\n resolve();\r\n };\r\n\r\n imageObj.onerror = () => {\r\n console.error(\"Failed to load foreground image:\", imageObj.src);\r\n // Placeholder text on image load error\r\n ctx.fillStyle = \"gray\";\r\n ctx.font = \"30px Arial\";\r\n ctx.textAlign = \"center\";\r\n ctx.textBaseline = \"middle\";\r\n ctx.fillText(\"Image Error\", cardWidth / 2, cardHeight / 2 - 50);\r\n resolve(); // Resolve to allow card generation to continue\r\n };\r\n });\r\n\r\n await loadImagePromise; // Wait for the image to load or fail\r\n\r\n // Tags\r\n let currentXForTags = padding;\r\n const tagFontSize = 16;\r\n const tagPaddingX = 15;\r\n const tagPaddingY = 8;\r\n const tagGap = 10;\r\n\r\n const tagsY = cardHeight - padding - tagFontSize - tagPaddingY;\r\n data.tags.forEach((tagText) => {\r\n ctx.font = `${tagFontSize}px Helvetica, Arial, sans-serif`;\r\n ctx.textBaseline = \"middle\"; // Align text vertically in the middle of the shape\r\n\r\n const textToDraw = `#${tagText.toUpperCase()}`;\r\n const textMetrics = ctx.measureText(textToDraw);\r\n const tagLabelWidth = textMetrics.width;\r\n\r\n const tagShapeWidth = tagLabelWidth + tagPaddingX;\r\n const tagShapeHeight = tagFontSize + tagPaddingY;\r\n\r\n // Draw rounded rectangle for tag shape\r\n ctx.fillStyle = \"rgba(248,250, 252, 0.15)\";\r\n drawRoundedRect(ctx, currentXForTags, tagsY, tagShapeWidth, tagShapeHeight, tagShapeHeight / 2); // Use half height for perfect pill shape\r\n ctx.fill();\r\n\r\n // Draw tag text\r\n ctx.fillStyle = \"white\";\r\n ctx.textAlign = \"center\";\r\n ctx.fillText(textToDraw, currentXForTags + tagShapeWidth / 2, tagsY + tagShapeHeight / 2); // Center text in shape\r\n\r\n currentXForTags += tagShapeWidth + tagGap;\r\n });\r\n\r\n // Date\r\n ctx.font = \"20px Arial, sans-serif\";\r\n ctx.fillStyle = \"rgba(255, 255, 255, 1)\";\r\n ctx.textAlign = \"right\"; // Align text to the right\r\n ctx.textBaseline = \"bottom\"; // Align text to the bottom of its bounding box\r\n ctx.fillText(data.date, cardWidth - padding, cardHeight - padding);\r\n\r\n const texture = new Texture(renderer.gl, {\r\n image: canvas,\r\n generateMipmaps: false,\r\n flipY: false,\r\n });\r\n\r\n textureCache.set(cacheKey, texture);\r\n return texture;\r\n}\r\n\r\n/**\r\n * Generates the background texture for a card using Canvas 2D API\r\n *\r\n * This function creates a blurred, darkened version of the card's image\r\n * that serves as the background layer visible during hover effects.\r\n * The background provides visual depth and context while maintaining\r\n * readability of the foreground content.\r\n *\r\n * Processing steps:\r\n * 1. Loads the same image used in the foreground\r\n * 2. Scales it up for better blur coverage\r\n * 3. Applies canvas blur filter\r\n * 4. Adds a semi-transparent dark overlay\r\n * 5. Falls back to solid color if image loading fails\r\n *\r\n * @param data - Card data containing the image URL\r\n * @param renderer - OGL Renderer for texture creation\r\n * @returns Promise resolving to an OGL Texture for background layer\r\n *\r\n * @example\r\n * ```typescript\r\n * const backgroundTexture = await generateBackgroundTexture(cardData, renderer);\r\n * // Use this texture for the background mesh with shader material\r\n * ```\r\n */\r\nexport async function generateBackgroundTexture(\r\n data: CardData,\r\n renderer: Renderer,\r\n): Promise<Texture> {\r\n const { canvas, ctx } = createCanvasContext();\r\n\r\n // Start with transparent background - image will fill the canvas\r\n // Alternative: ctx.fillStyle = 'rgba(0,0,0,0.5)' for solid fallback\r\n\r\n const backgroundImageObj = new Image();\r\n backgroundImageObj.crossOrigin = \"Anonymous\"; // Enable CORS for external images\r\n backgroundImageObj.src = data.image || \"/photo.png\"; // Use same image as foreground\r\n\r\n const loadBackgroundImagePromise = new Promise<void>((resolve) => {\r\n backgroundImageObj.onload = () => {\r\n const backgroundScale = 2.0; // Make background image larger for blur effect\r\n const bgImgWidth = backgroundImageObj.naturalWidth * backgroundScale;\r\n const bgImgHeight = backgroundImageObj.naturalHeight * backgroundScale;\r\n\r\n // Draw the image first\r\n ctx.drawImage(\r\n backgroundImageObj,\r\n (cardWidth - bgImgWidth) / 2,\r\n (cardHeight - bgImgHeight) / 2,\r\n bgImgWidth,\r\n bgImgHeight,\r\n );\r\n\r\n // Apply blur directly on the canvas content\r\n // Note: blur performance and quality can vary between browsers.\r\n // For more control/consistency, you might apply blur in a shader or pre-process images.\r\n ctx.filter = \"blur(10px)\"; // Adjust blur radius as needed\r\n ctx.drawImage(canvas, 0, 0); // Redraw the canvas content with blur\r\n ctx.filter = \"none\"; // Reset filter for subsequent draws\r\n\r\n // Add a semi-transparent overlay to darken/blend the background\r\n ctx.fillStyle = \"rgba(0,0,0,0.4)\"; // Dark overlay\r\n ctx.fillRect(0, 0, cardWidth, cardHeight);\r\n\r\n resolve();\r\n };\r\n\r\n backgroundImageObj.onerror = () => {\r\n console.warn(\"Failed to load background image:\", backgroundImageObj.src);\r\n // Fallback to a solid color background if image fails to load\r\n // ctx.fillStyle = data.color1 || 'rgba(50,50,50,0.5)'; // Use a default dark gray if data.color1 is not present\r\n ctx.fillStyle = \"rgba(0,0,0,0.5)\"; // Fallback to semi-transparent black\r\n ctx.fillRect(0, 0, cardWidth, cardHeight);\r\n resolve();\r\n };\r\n });\r\n\r\n await loadBackgroundImagePromise; // Wait for background image to load or fail\r\n\r\n const backgroundTexture = new Texture(renderer.gl, {\r\n image: canvas,\r\n generateMipmaps: false,\r\n flipY: false,\r\n });\r\n\r\n return backgroundTexture;\r\n}\r\n\r\n/**\r\n * Helper function to draw a rounded rectangle using Canvas 2D API\r\n *\r\n * Canvas doesn't have a built-in rounded rectangle method, so this\r\n * function uses quadratic curves to create smooth corners. This is\r\n * used for drawing the tag pill backgrounds.\r\n *\r\n * @param ctx - The 2D rendering context to draw on\r\n * @param x - X coordinate of the top-left corner\r\n * @param y - Y coordinate of the top-left corner\r\n * @param width - Width of the rectangle\r\n * @param height - Height of the rectangle\r\n * @param radius - Corner radius in pixels\r\n */\r\nfunction drawRoundedRect(\r\n ctx: CanvasRenderingContext2D,\r\n x: number,\r\n y: number,\r\n width: number,\r\n height: number,\r\n radius: number,\r\n): void {\r\n ctx.beginPath();\r\n ctx.moveTo(x + radius, y);\r\n ctx.lineTo(x + width - radius, y);\r\n ctx.quadraticCurveTo(x + width, y, x + width, y + radius);\r\n ctx.lineTo(x + width, y + height - radius);\r\n ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);\r\n ctx.lineTo(x + radius, y + height);\r\n ctx.quadraticCurveTo(x, y + height, x, y + height - radius);\r\n ctx.lineTo(x, y + radius);\r\n ctx.quadraticCurveTo(x, y, x + radius, y);\r\n ctx.closePath();\r\n}\r\n\r\n/**\r\n * Convenience function to generate both foreground and background textures\r\n *\r\n * This function generates both texture types in parallel for efficiency.\r\n * While the individual generation functions are typically used separately\r\n * in the main grid system, this function can be useful for testing or\r\n * simpler use cases.\r\n *\r\n * @param data - Card data for texture generation\r\n * @param renderer - OGL Renderer for texture creation\r\n * @returns Promise resolving to an object with both texture types\r\n *\r\n * @example\r\n * ```typescript\r\n * const { foreground, background } = await generateCardTextures(cardData, renderer);\r\n * // Use foreground for main mesh, background for hover effect\r\n * ```\r\n */\r\nexport async function generateCardTextures(\r\n data: CardData,\r\n renderer: Renderer,\r\n): Promise<{\r\n foreground: Texture;\r\n background: Texture;\r\n}> {\r\n const [foreground, background] = await Promise.all([\r\n generateForegroundTexture(data, renderer),\r\n generateBackgroundTexture(data, renderer),\r\n ]);\r\n return { foreground, background };\r\n}\r\n"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"path": "DisposalManager.ts",
|
|
14
|
+
"content": "/* eslint-disable @typescript-eslint/no-explicit-any */\r\n/**\r\n * @fileoverview DisposalManager - Resource Cleanup System for InfiniteGrid\r\n *\r\n * This module handles the complete disposal and cleanup of all resources\r\n * used by the infinite grid system. It ensures proper memory management\r\n * by disposing of WebGL resources, clearing references, and removing\r\n * event listeners to prevent memory leaks.\r\n *\r\n * Key Responsibilities:\r\n * - Animation frame cleanup\r\n * - Event listener removal\r\n * - OGL resource disposal (meshes, programs, textures)\r\n * - Post-processing resource cleanup\r\n * - Renderer and canvas cleanup\r\n * - Data structure clearing\r\n * - GSAP animation cleanup\r\n */\r\n\r\nimport { Mesh, Transform, Renderer, Camera, RenderTarget } from \"ogl\";\r\nimport type { CardTexturePair, TileGroupData } from \"./types.ts\";\r\nimport { CustomPostProcessShader } from \"./PostProcessShader\";\r\nimport { EventHandler } from \"./EventHandler\";\r\nimport { GridManager } from \"./GridManager\";\r\n\r\n/**\r\n * Interface defining the required properties that the DisposalManager\r\n * needs to access from the main InfiniteGrid class for cleanup\r\n */\r\nexport interface DisposableHost {\r\n // Animation and timing\r\n animationFrameId: number | null;\r\n\r\n // Event handling\r\n eventHandler?: EventHandler;\r\n\r\n // Grid management\r\n gridManager: GridManager;\r\n\r\n // OGL core objects\r\n scene: Transform;\r\n camera: Camera | null;\r\n renderer: Renderer | null;\r\n\r\n // Post-processing\r\n postProcessShader: CustomPostProcessShader | null;\r\n sceneRenderTarget: RenderTarget | null;\r\n\r\n // Scene objects and data\r\n groupObjects: Transform[];\r\n foregroundMeshMap: Map<string, Mesh>;\r\n backgroundMeshMap: Map<string, Mesh>;\r\n cardTextures: CardTexturePair[];\r\n staticUniforms: Map<string, any>;\r\n tileGroupsData: TileGroupData[];\r\n\r\n // Interaction objects\r\n raycast: any;\r\n pointer: any;\r\n scrollTracker: any;\r\n\r\n // Container reference\r\n container: HTMLElement;\r\n} /**\r\n * DisposalManager handles the complete cleanup of InfiniteGrid resources\r\n *\r\n * This class provides a systematic approach to disposing of all resources\r\n * used by the infinite grid system, ensuring no memory leaks occur when\r\n * the grid is no longer needed.\r\n *\r\n * The disposal process follows this order:\r\n * 1. Stop animation loops\r\n * 2. Remove event listeners\r\n * 3. Dispose 3D scene objects and materials\r\n * 4. Clean up post-processing resources\r\n * 5. Remove renderer and canvas\r\n * 6. Clear data structures and references\r\n * 7. Clean up animation systems\r\n */\r\nexport class DisposalManager {\r\n private host: DisposableHost;\r\n\r\n /**\r\n * Creates a new DisposalManager instance\r\n * @param host - The main grid class that provides resources to dispose\r\n */\r\n constructor(host: DisposableHost) {\r\n this.host = host;\r\n }\r\n\r\n /**\r\n * Performs complete disposal of all grid resources\r\n *\r\n * This method should be called when the grid is no longer needed,\r\n * such as when navigating away from the page or unmounting a component.\r\n *\r\n * @example\r\n * ```typescript\r\n * // In a Vue component's onBeforeUnmount\r\n * onBeforeUnmount(() => {\r\n * if (gridInstance) {\r\n * disposalManager.dispose();\r\n * }\r\n * });\r\n * ```\r\n */\r\n public dispose(): void {\r\n // Step 1: Cancel animation loops\r\n this.stopAnimationLoop();\r\n\r\n // Step 2: Remove event listeners\r\n this.cleanupEventListeners();\r\n\r\n // Step 3: Clear grid manager\r\n this.cleanupGridManager();\r\n\r\n // Step 4: Dispose 3D scene objects\r\n this.disposeSceneObjects();\r\n\r\n // Step 5: Clean up post-processing\r\n this.disposePostProcessing();\r\n\r\n // Step 6: Dispose renderer and canvas\r\n this.disposeRenderer();\r\n\r\n // Step 7: Clear data structures\r\n this.clearDataStructures();\r\n\r\n // Step 8: Clean up animation systems\r\n this.cleanupAnimationSystems();\r\n }\r\n\r\n /**\r\n * Cancels the main animation frame loop\r\n */\r\n private stopAnimationLoop(): void {\r\n if (this.host.animationFrameId) {\r\n cancelAnimationFrame(this.host.animationFrameId);\r\n this.host.animationFrameId = null;\r\n }\r\n }\r\n\r\n /**\r\n * Removes all event listeners through the EventHandler\r\n */\r\n private cleanupEventListeners(): void {\r\n if (this.host.eventHandler) {\r\n this.host.eventHandler.removeEventListeners();\r\n }\r\n }\r\n\r\n /**\r\n * Clears the grid manager and all its managed resources\r\n */\r\n private cleanupGridManager(): void {\r\n if (this.host.gridManager) {\r\n this.host.gridManager.clear();\r\n }\r\n }\r\n\r\n /**\r\n * Disposes all 3D scene objects including meshes, geometries, and programs\r\n */\r\n private disposeSceneObjects(): void {\r\n // Clean up group objects and their children\r\n this.host.groupObjects.forEach((group) => {\r\n this.disposeTransformAndChildren(group);\r\n });\r\n\r\n // Clear the main scene\r\n this.disposeTransformAndChildren(this.host.scene);\r\n\r\n // Clear mesh maps\r\n this.host.foregroundMeshMap.forEach((mesh) => {\r\n this.disposeMesh(mesh);\r\n });\r\n this.host.backgroundMeshMap.forEach((mesh) => {\r\n this.disposeMesh(mesh);\r\n });\r\n }\r\n\r\n /**\r\n * Recursively disposes a Transform and all its children\r\n * @param transform - The transform to dispose\r\n */\r\n private disposeTransformAndChildren(transform: Transform): void {\r\n if (!transform) return;\r\n\r\n // Dispose all children first\r\n transform.traverse((child) => {\r\n if (child instanceof Mesh) {\r\n this.disposeMesh(child);\r\n }\r\n });\r\n\r\n // Clear parent-child relationships\r\n (transform as any).parent = null;\r\n (transform as any).children = [];\r\n }\r\n\r\n /**\r\n * Disposes a single mesh and its resources\r\n * @param mesh - The mesh to dispose\r\n */\r\n private disposeMesh(mesh: Mesh): void {\r\n if (!mesh) return;\r\n\r\n // Clear geometry reference (OGL manages WebGL resources automatically)\r\n (mesh as any).geometry = null;\r\n\r\n // Clear program reference\r\n if ((mesh as any).program) {\r\n (mesh as any).program = null;\r\n }\r\n\r\n // Clear user data\r\n (mesh as any).userData = null;\r\n\r\n // Clear parent reference\r\n (mesh as any).parent = null;\r\n }\r\n\r\n /**\r\n * Disposes post-processing resources\r\n */\r\n private disposePostProcessing(): void {\r\n if (this.host.postProcessShader) {\r\n this.host.postProcessShader.dispose();\r\n this.host.postProcessShader = null;\r\n }\r\n\r\n if (this.host.sceneRenderTarget) {\r\n // OGL RenderTarget disposal is handled automatically\r\n this.host.sceneRenderTarget = null;\r\n }\r\n }\r\n\r\n /**\r\n * Disposes the renderer and removes canvas from DOM\r\n */\r\n private disposeRenderer(): void {\r\n if (this.host.renderer) {\r\n // Remove canvas from DOM if it's still attached\r\n const canvas = this.host.renderer.gl.canvas;\r\n if (canvas.parentNode === this.host.container) {\r\n this.host.container.removeChild(canvas);\r\n }\r\n\r\n // Clear renderer reference (WebGL context cleanup is automatic)\r\n this.host.renderer = null;\r\n }\r\n\r\n // Clear camera reference\r\n this.host.camera = null;\r\n }\r\n\r\n /**\r\n * Clears all data structures and maps\r\n */\r\n private clearDataStructures(): void {\r\n // Clear texture references (OGL handles WebGL texture cleanup)\r\n this.host.cardTextures.forEach((texturePair) => {\r\n texturePair.foreground = null;\r\n texturePair.background = null;\r\n });\r\n\r\n // Clear all arrays and maps\r\n this.host.groupObjects = [];\r\n this.host.foregroundMeshMap.clear();\r\n this.host.backgroundMeshMap.clear();\r\n this.host.staticUniforms.clear();\r\n this.host.cardTextures = [];\r\n this.host.tileGroupsData = [];\r\n\r\n // Clear interaction objects\r\n this.host.raycast = null;\r\n this.host.pointer = null;\r\n }\r\n\r\n /**\r\n * Cleans up GSAP animations and trackers\r\n */\r\n private cleanupAnimationSystems(): void {\r\n if (this.host.scrollTracker) {\r\n this.host.scrollTracker.kill();\r\n this.host.scrollTracker = null;\r\n }\r\n\r\n // Kill any remaining GSAP animations to prevent callbacks\r\n // This is a safety measure in case any animations are still running\r\n try {\r\n // Note: This would require importing gsap, but we'll let the main class handle it\r\n // gsap.killTweensOf(this.host);\r\n } catch (error) {\r\n console.warn(\"Error killing GSAP tweens during disposal:\", error);\r\n }\r\n }\r\n\r\n /**\r\n * Performs a partial cleanup that preserves the core structure\r\n * but clears dynamic content. Useful for reinitialization scenarios.\r\n */\r\n public partialCleanup(): void {\r\n // Stop animations but don't destroy everything\r\n this.stopAnimationLoop();\r\n\r\n // Clear dynamic content\r\n this.host.cardTextures.forEach((texturePair) => {\r\n texturePair.foreground = null;\r\n texturePair.background = null;\r\n });\r\n this.host.cardTextures = [];\r\n\r\n // Clear mesh content but keep structure\r\n this.host.foregroundMeshMap.clear();\r\n this.host.backgroundMeshMap.clear();\r\n this.host.staticUniforms.clear();\r\n }\r\n\r\n /**\r\n * Validates that all resources have been properly disposed\r\n * Useful for debugging memory leaks\r\n */\r\n public validateDisposal(): boolean {\r\n const issues: string[] = [];\r\n\r\n if (this.host.animationFrameId !== null) {\r\n issues.push(\"Animation frame still active\");\r\n }\r\n\r\n if (this.host.renderer !== null) {\r\n issues.push(\"Renderer not disposed\");\r\n }\r\n\r\n if (this.host.camera !== null) {\r\n issues.push(\"Camera not disposed\");\r\n }\r\n\r\n if (this.host.postProcessShader !== null) {\r\n issues.push(\"Post-process shader not disposed\");\r\n }\r\n\r\n if (this.host.groupObjects.length > 0) {\r\n issues.push(\"Group objects not cleared\");\r\n }\r\n\r\n if (this.host.foregroundMeshMap.size > 0) {\r\n issues.push(\"Foreground mesh map not cleared\");\r\n }\r\n\r\n if (this.host.backgroundMeshMap.size > 0) {\r\n issues.push(\"Background mesh map not cleared\");\r\n }\r\n\r\n if (this.host.cardTextures.length > 0) {\r\n issues.push(\"Card textures not cleared\");\r\n }\r\n\r\n if (this.host.scrollTracker !== null) {\r\n issues.push(\"Scroll tracker not disposed\");\r\n }\r\n\r\n if (issues.length > 0) {\r\n console.warn(\"Disposal validation failed:\", issues);\r\n return false;\r\n }\r\n return true;\r\n }\r\n}\r\n"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"path": "EventHandler.ts",
|
|
18
|
+
"content": "/* eslint-disable @typescript-eslint/no-explicit-any */\r\n/**\r\n * @fileoverview EventHandler - Event Management System for InfiniteGrid\r\n *\r\n * This module handles all user interactions for the infinite grid system,\r\n * including pointer events (mouse/touch), hover effects, click detection,\r\n * and window resize handling.\r\n *\r\n * Key Responsibilities:\r\n * - Pointer down/move/up event handling for scrolling\r\n * - Touch event support with proper coordinate handling\r\n * - Hover state management with background blur effects\r\n * - Click detection with movement threshold\r\n * - Window resize handling for responsive behavior\r\n * - Event listener lifecycle management\r\n */\r\n\r\nimport { gsap } from \"gsap\";\r\nimport { Mesh, Vec2, Raycast } from \"ogl\";\r\nimport type {\r\n Position2D,\r\n ScrollState,\r\n TileUserData,\r\n TileClickEventDetail,\r\n CardData,\r\n} from \"./types.ts\";\r\n\r\n/**\r\n * Interface defining the required properties and methods that the EventHandler\r\n * needs to access from the main InfiniteGrid class\r\n */\r\nexport interface EventHandlerHost {\r\n // Container and interaction state\r\n container: HTMLElement;\r\n pointer: Vec2;\r\n raycast: Raycast;\r\n camera: any;\r\n renderer: any;\r\n\r\n // Post processing\r\n sceneRenderTarget: any;\r\n postProcessShader: any;\r\n\r\n // Scroll and movement state\r\n scroll: ScrollState;\r\n isDown: boolean;\r\n isHoveringCanvas: boolean;\r\n hasMovedSignificantly: boolean;\r\n startPosition: Position2D;\r\n scrollPosition: Position2D;\r\n scrollTracker: any;\r\n\r\n // Hover state\r\n currentHoveredTileKey: string;\r\n backgroundMeshMap: Map<string, Mesh>;\r\n foregroundMeshMap: Map<string, Mesh>;\r\n\r\n // Configuration\r\n options: {\r\n baseCameraZ: number;\r\n };\r\n\r\n // Animation constants\r\n maxClickMovement: number;\r\n hoverTransitionDuration: number;\r\n hoverEase: string;\r\n initialBackgroundOpacity: number;\r\n hoveredBackgroundOpacity: number;\r\n\r\n // Card data\r\n cardData: CardData[];\r\n\r\n // Methods that need to be called\r\n updatePositions(): void;\r\n animateInertiaScroll(vx?: number | string, vy?: number | string): void;\r\n getTileKeyFromMesh(mesh: Mesh): string;\r\n getCardDataForTile(groupIndex: number, tileIndex: number): CardData;\r\n getInteractiveMeshes(): Mesh[];\r\n updatePointerCoordinates(clientX: number, clientY: number): void;\r\n performRaycast(): Mesh[];\r\n fadeInBackground(mesh: Mesh): void;\r\n fadeOutBackground(mesh: Mesh): void;\r\n}\r\n\r\n/**\r\n * EventHandler class manages all user interactions for the infinite grid\r\n *\r\n * This class encapsulates event handling logic including:\r\n * - Mouse and touch input processing\r\n * - Hover state management with visual feedback\r\n * - Click detection with drag threshold\r\n * - Responsive window resize handling\r\n * - Event listener lifecycle management\r\n */\r\nexport class EventHandler {\r\n private host: EventHandlerHost;\r\n private isInitialized: boolean = false;\r\n\r\n /**\r\n * Creates a new EventHandler instance\r\n * @param host - The main grid class that provides required properties and methods\r\n */\r\n constructor(host: EventHandlerHost) {\r\n this.host = host;\r\n\r\n // Bind all event handler methods to maintain proper 'this' context\r\n this.onPointerDown = this.onPointerDown.bind(this);\r\n this.onPointerMove = this.onPointerMove.bind(this);\r\n this.onPointerUp = this.onPointerUp.bind(this);\r\n this.onPointerOut = this.onPointerOut.bind(this);\r\n this.onWindowResize = this.onWindowResize.bind(this);\r\n this.handleMouseClick = this.handleMouseClick.bind(this);\r\n this.handleTouchEnd = this.handleTouchEnd.bind(this);\r\n }\r\n\r\n /**\r\n * Initializes event listeners\r\n * Should be called once after the grid is set up\r\n */\r\n public initialize(): void {\r\n if (this.isInitialized) {\r\n console.warn(\"EventHandler already initialized\");\r\n return;\r\n }\r\n\r\n this.addEventListeners();\r\n this.isInitialized = true;\r\n }\r\n\r\n /**\r\n * Handles pointer down events (mouse button press or touch start)\r\n * Initiates drag interaction and camera zoom\r\n */\r\n private onPointerDown(e: MouseEvent | TouchEvent): void {\r\n e.preventDefault();\r\n\r\n this.host.currentHoveredTileKey = \"\";\r\n this.host.isDown = true;\r\n this.host.hasMovedSignificantly = false;\r\n this.host.scrollPosition.x = this.host.scroll.current.x;\r\n this.host.scrollPosition.y = this.host.scroll.current.y;\r\n\r\n const clientX = \"touches\" in e ? e.touches[0].clientX : e.clientX;\r\n const clientY = \"touches\" in e ? e.touches[0].clientY : e.clientY;\r\n\r\n this.host.startPosition.x = clientX;\r\n this.host.startPosition.y = clientY;\r\n\r\n // Update pointer coordinates for raycasting\r\n this.host.updatePointerCoordinates(clientX, clientY);\r\n\r\n if (this.host.camera) {\r\n gsap.to(this.host.camera.position, {\r\n z: this.host.options.baseCameraZ * 1.3,\r\n duration: 0.3,\r\n ease: \"power2.out\",\r\n overwrite: true,\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * Handles pointer move events (mouse move or touch move)\r\n * Processes scrolling or hover states depending on interaction state\r\n */\r\n private onPointerMove(e: MouseEvent | TouchEvent): void {\r\n if (!this.host.isDown) {\r\n this.handleHover(e);\r\n return;\r\n }\r\n\r\n const clientX = \"touches\" in e ? e.touches[0].clientX : e.clientX;\r\n const clientY = \"touches\" in e ? e.touches[0].clientY : e.clientY;\r\n\r\n // Check if movement is significant enough to disable click\r\n const movementDistance = Math.sqrt(\r\n Math.pow(clientX - this.host.startPosition.x, 2) +\r\n Math.pow(clientY - this.host.startPosition.y, 2),\r\n );\r\n\r\n if (movementDistance > this.host.maxClickMovement) {\r\n this.host.hasMovedSignificantly = true;\r\n }\r\n\r\n const distanceX = (this.host.startPosition.x - clientX) * this.host.scroll.scale;\r\n const distanceY = (this.host.startPosition.y - clientY) * this.host.scroll.scale;\r\n\r\n gsap.to(this.host.scroll.current, {\r\n x: this.host.scrollPosition.x - distanceX,\r\n y: this.host.scrollPosition.y + distanceY,\r\n duration: 0.1,\r\n ease: \"power1.out\",\r\n overwrite: true,\r\n onUpdate: () => this.host.updatePositions(),\r\n });\r\n\r\n this.host.scroll.last.x = this.host.scroll.current.x;\r\n this.host.scroll.last.y = this.host.scroll.current.y;\r\n }\r\n\r\n /**\r\n * Handles pointer up events (mouse button release or touch end)\r\n * Ends drag interaction and applies inertia scrolling\r\n */\r\n private onPointerUp(e?: MouseEvent | TouchEvent): void {\r\n this.host.isDown = false;\r\n\r\n // Clear hover state when pointer is lifted\r\n if (this.host.currentHoveredTileKey) {\r\n const mesh = this.host.backgroundMeshMap.get(this.host.currentHoveredTileKey);\r\n if (mesh) {\r\n this.host.fadeOutBackground(mesh);\r\n }\r\n this.host.currentHoveredTileKey = \"\";\r\n }\r\n\r\n if (this.host.camera) {\r\n gsap.to(this.host.camera.position, {\r\n z: this.host.options.baseCameraZ,\r\n duration: 0.3,\r\n ease: \"power2.out\",\r\n overwrite: true,\r\n });\r\n }\r\n\r\n const vx = this.host.scrollTracker.get(\"x\");\r\n const vy = this.host.scrollTracker.get(\"y\");\r\n\r\n this.host.animateInertiaScroll(vx, vy);\r\n }\r\n\r\n /**\r\n * Handles pointer leaving the canvas area\r\n * Clears hover states when mouse exits\r\n */\r\n private onPointerOut(e: MouseEvent): void {\r\n this.host.isHoveringCanvas = false;\r\n\r\n // Clear hover state when pointer leaves canvas\r\n if (this.host.currentHoveredTileKey) {\r\n const mesh = this.host.backgroundMeshMap.get(this.host.currentHoveredTileKey);\r\n if (mesh) {\r\n this.host.fadeOutBackground(mesh);\r\n }\r\n this.host.currentHoveredTileKey = \"\";\r\n }\r\n }\r\n\r\n /**\r\n * Handles window resize events\r\n * Updates camera aspect ratio and renderer size\r\n */\r\n private onWindowResize(): void {\r\n const newWidth = this.host.container.clientWidth;\r\n const newHeight = this.host.container.clientHeight;\r\n\r\n if (this.host.camera) {\r\n this.host.camera.aspect = newWidth / newHeight;\r\n this.host.camera.perspective({ aspect: newWidth / newHeight });\r\n }\r\n\r\n if (this.host.renderer) {\r\n this.host.renderer.setSize(newWidth, newHeight);\r\n }\r\n\r\n // Update post-processing render targets\r\n if (this.host.sceneRenderTarget) {\r\n this.host.sceneRenderTarget.setSize(newWidth, newHeight);\r\n }\r\n if (this.host.postProcessShader) {\r\n this.host.postProcessShader.resize(newWidth, newHeight);\r\n }\r\n }\r\n\r\n /**\r\n * Handles hover effects when not dragging\r\n * Manages background blur fade in/out based on tile intersection\r\n */\r\n private handleHover(e: MouseEvent | TouchEvent): void {\r\n const clientX = \"touches\" in e ? e.touches[0].clientX : e.clientX;\r\n const clientY = \"touches\" in e ? e.touches[0].clientY : e.clientY;\r\n\r\n // Update pointer coordinates for raycasting\r\n this.host.updatePointerCoordinates(clientX, clientY);\r\n\r\n // Perform raycasting to find hovered tiles\r\n const hits = this.host.performRaycast();\r\n\r\n // Handle hover state changes\r\n const newHoveredTileKey = hits.length > 0 ? this.host.getTileKeyFromMesh(hits[0]) : \"\";\r\n\r\n // If hovering over a different tile\r\n if (newHoveredTileKey !== this.host.currentHoveredTileKey) {\r\n // Fade out previous hovered tile\r\n if (this.host.currentHoveredTileKey) {\r\n const prevMesh = this.host.backgroundMeshMap.get(this.host.currentHoveredTileKey);\r\n if (prevMesh) {\r\n this.host.fadeOutBackground(prevMesh);\r\n }\r\n }\r\n\r\n // Fade in new hovered tile\r\n if (newHoveredTileKey) {\r\n const newMesh = this.host.backgroundMeshMap.get(newHoveredTileKey);\r\n if (newMesh) {\r\n this.host.fadeInBackground(newMesh);\r\n }\r\n }\r\n\r\n this.host.currentHoveredTileKey = newHoveredTileKey;\r\n }\r\n }\r\n\r\n /**\r\n * Handles mouse click events\r\n * Processes clicks only if no significant movement occurred\r\n */\r\n private handleMouseClick(e: MouseEvent): void {\r\n // Don't handle clicks if the user has moved significantly (drag vs click)\r\n if (this.host.hasMovedSignificantly) {\r\n return;\r\n }\r\n\r\n // Update pointer coordinates for raycasting\r\n this.host.updatePointerCoordinates(e.clientX, e.clientY);\r\n\r\n // Perform click logic\r\n this.performTileClick();\r\n }\r\n\r\n /**\r\n * Handles touch end events\r\n * Combines pointer up logic with click detection for touch devices\r\n */\r\n private handleTouchEnd(e: TouchEvent): void {\r\n // Call the original onPointerUp logic first\r\n this.onPointerUp(e);\r\n\r\n // Don't handle clicks if the user has moved significantly (drag vs click)\r\n if (this.host.hasMovedSignificantly) {\r\n return;\r\n }\r\n\r\n // For touch end, use changedTouches to get the final touch position\r\n if (e.changedTouches && e.changedTouches.length > 0) {\r\n const touch = e.changedTouches[0];\r\n this.host.updatePointerCoordinates(touch.clientX, touch.clientY);\r\n\r\n // Perform click logic\r\n this.performTileClick();\r\n }\r\n }\r\n\r\n /**\r\n * Performs tile click detection and event dispatching\r\n * Uses raycasting to determine which tile was clicked\r\n */\r\n private performTileClick(): void {\r\n // Perform raycasting to find clicked tiles\r\n const hits = this.host.performRaycast();\r\n\r\n if (hits.length > 0) {\r\n const clickedMesh = hits[0]; // Get the closest hit\r\n const userData = (clickedMesh as any).userData as TileUserData;\r\n\r\n if (userData) {\r\n // Get the card data for the clicked tile\r\n const cardData = this.host.getCardDataForTile(userData.groupIndex, userData.tileIndex);\r\n\r\n // Create and dispatch custom event\r\n const eventDetail: TileClickEventDetail = {\r\n groupIndex: userData.groupIndex,\r\n tileIndex: userData.tileIndex,\r\n cardData: cardData,\r\n };\r\n\r\n const customEvent = new CustomEvent<TileClickEventDetail>(\"tileClicked\", {\r\n detail: eventDetail,\r\n bubbles: true,\r\n cancelable: true,\r\n });\r\n\r\n this.host.container.dispatchEvent(customEvent);\r\n\r\n // Log for debugging\r\n window.alert(\r\n \"Tile clicked: \" +\r\n JSON.stringify({\r\n title: cardData.title,\r\n groupIndex: userData.groupIndex,\r\n tileIndex: userData.tileIndex,\r\n }),\r\n );\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Adds all event listeners to the container and window\r\n * Should be called once during initialization\r\n */\r\n private addEventListeners(): void {\r\n this.host.container.addEventListener(\"mousedown\", this.onPointerDown);\r\n this.host.container.addEventListener(\"mousemove\", this.onPointerMove);\r\n this.host.container.addEventListener(\"mouseup\", this.onPointerUp);\r\n this.host.container.addEventListener(\"mouseleave\", this.onPointerOut);\r\n this.host.container.addEventListener(\"touchstart\", this.onPointerDown, { passive: false });\r\n this.host.container.addEventListener(\"touchmove\", this.onPointerMove, { passive: false });\r\n this.host.container.addEventListener(\"touchend\", this.handleTouchEnd, { passive: true });\r\n\r\n // Only add click event for mouse interactions\r\n this.host.container.addEventListener(\"click\", this.handleMouseClick);\r\n\r\n window.addEventListener(\"resize\", this.onWindowResize);\r\n }\r\n\r\n /**\r\n * Removes all event listeners\r\n * Should be called during cleanup/disposal\r\n */\r\n public removeEventListeners(): void {\r\n if (!this.isInitialized) {\r\n return;\r\n }\r\n\r\n this.host.container.removeEventListener(\"mousedown\", this.onPointerDown);\r\n this.host.container.removeEventListener(\"mousemove\", this.onPointerMove);\r\n this.host.container.removeEventListener(\"mouseup\", this.onPointerUp);\r\n this.host.container.removeEventListener(\"mouseleave\", this.onPointerOut);\r\n this.host.container.removeEventListener(\"touchstart\", this.onPointerDown);\r\n this.host.container.removeEventListener(\"touchmove\", this.onPointerMove);\r\n this.host.container.removeEventListener(\"touchend\", this.handleTouchEnd);\r\n this.host.container.removeEventListener(\"click\", this.handleMouseClick);\r\n\r\n window.removeEventListener(\"resize\", this.onWindowResize);\r\n\r\n this.isInitialized = false;\r\n }\r\n\r\n /**\r\n * Gets the initialization status\r\n */\r\n public get initialized(): boolean {\r\n return this.isInitialized;\r\n }\r\n}\r\n"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"path": "GridManager.ts",
|
|
22
|
+
"content": "/* eslint-disable @typescript-eslint/no-explicit-any */\r\n/**\r\n * @fileoverview GridManager - Grid Creation and Management System for InfiniteGrid\r\n *\r\n * This module handles all aspects of grid creation, tile management, and texture\r\n * generation for the infinite grid system. It's responsible for:\r\n *\r\n * Key Features:\r\n * - 3x3 tile group initialization for infinite scrolling\r\n * - Dynamic tile creation with foreground and background meshes\r\n * - Texture generation and management for card data\r\n * - Shader program creation for tile rendering\r\n * - Tile positioning and indexing systems\r\n * - Card data to texture mapping\r\n *\r\n * Architecture:\r\n * - Creates 9 tile groups arranged in a 3x3 pattern\r\n * - Each group contains a configurable grid of individual tiles\r\n * - Each tile has both foreground (content) and background (blur) layers\r\n * - Textures are generated dynamically from card data using Canvas 2D API\r\n * - Programs are created with appropriate shaders for each tile type\r\n */\r\n\r\nimport { Renderer, Transform, Texture, Program, Mesh, Vec3, Plane } from \"ogl\";\r\nimport { generateForegroundTexture, generateBackgroundTexture } from \"./createTexture\";\r\nimport { gaussianBlurVertexShader, gaussianBlurFragmentShader } from \"./shaders\";\r\n\r\nimport type { CardData, TileGroupData, TileUserData, CardTexturePair } from \"./types.ts\";\r\n\r\n/**\r\n * Interface defining the required properties and methods that the GridManager\r\n * needs to access from the main InfiniteGrid class\r\n */\r\nexport interface GridManagerHost {\r\n // Core properties\r\n renderer: Renderer | null;\r\n scene: Transform;\r\n cardData: CardData[];\r\n\r\n // Grid configuration\r\n GRID_COLS: number;\r\n GRID_ROWS: number;\r\n GRID_WIDTH: number;\r\n GRID_HEIGHT: number;\r\n TILE_SIZE: number;\r\n TILE_SPACE: number;\r\n\r\n // Animation constants\r\n initialBackgroundOpacity: number;\r\n\r\n // Data structures that will be populated\r\n tileGroupsData: TileGroupData[];\r\n groupObjects: Transform[];\r\n foregroundMeshMap: Map<string, Mesh>;\r\n backgroundMeshMap: Map<string, Mesh>;\r\n cardTextures: CardTexturePair[];\r\n staticUniforms: Map<string, any>;\r\n}\r\n\r\n/**\r\n * GridManager handles all grid creation and management functionality\r\n *\r\n * This class encapsulates the complex logic of creating and managing\r\n * the 3x3 infinite grid system including:\r\n * - Tile group initialization and positioning\r\n * - Dynamic tile creation with proper materials\r\n * - Texture generation from card data\r\n * - Shader program creation and management\r\n * - Tile indexing and key management\r\n *\r\n * The grid system uses a 3x3 pattern of tile groups where each group\r\n * contains a configurable number of individual tiles. This creates\r\n * the illusion of infinite content while maintaining performance.\r\n */\r\nexport class GridManager {\r\n private host: GridManagerHost;\r\n private isInitialized: boolean = false;\r\n\r\n /**\r\n * Creates a new GridManager instance\r\n * @param host - The main grid class that provides required properties and methods\r\n */\r\n constructor(host: GridManagerHost) {\r\n this.host = host;\r\n }\r\n\r\n /**\r\n * Initializes the complete grid system\r\n *\r\n * This method sets up the entire grid in the correct order:\r\n * 1. Initialize tile group positions\r\n * 2. Generate textures for all card data\r\n * 3. Create all tile meshes with proper materials\r\n *\r\n * @returns Promise that resolves when all grid setup is complete\r\n */\r\n public async initialize(): Promise<void> {\r\n if (this.isInitialized) {\r\n console.warn(\"GridManager already initialized\");\r\n return;\r\n }\r\n\r\n this.initializeTileGroups();\r\n await this.generateTexturesForCardData();\r\n this.createTiles();\r\n\r\n this.isInitialized = true;\r\n }\r\n\r\n /**\r\n * Initializes the 3x3 grid of tile groups for infinite scrolling\r\n *\r\n * Creates 9 tile groups arranged in a 3x3 pattern. Each group contains\r\n * a grid of tiles. As the user scrolls, groups are repositioned to\r\n * create the illusion of infinite content.\r\n */\r\n private initializeTileGroups(): void {\r\n this.host.tileGroupsData = [];\r\n for (let r = -1; r <= 1; r++) {\r\n for (let c = -1; c <= 1; c++) {\r\n this.host.tileGroupsData.push({\r\n basePos: new Vec3(this.host.GRID_WIDTH * c, this.host.GRID_HEIGHT * r, 0),\r\n offset: { x: 0, y: 0 },\r\n });\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Creates all tiles for the grid system\r\n *\r\n * For each tile group, creates individual tiles with:\r\n * - Background mesh (for blur effects)\r\n * - Foreground mesh (for content display)\r\n * - Proper positioning and parenting\r\n * - User data for interaction\r\n */\r\n private createTiles(): void {\r\n if (!this.host.renderer) {\r\n throw new Error(\"Renderer not available for tile creation\");\r\n }\r\n\r\n const gl = this.host.renderer.gl;\r\n\r\n this.host.tileGroupsData.forEach((groupData, groupIndex) => {\r\n const groupObject = new Transform();\r\n groupObject.position.set(groupData.basePos.x, groupData.basePos.y, groupData.basePos.z);\r\n groupObject.setParent(this.host.scene);\r\n this.host.groupObjects[groupIndex] = groupObject;\r\n\r\n const startX = -((this.host.GRID_COLS - 1) / 2) * this.host.TILE_SPACE;\r\n const startY = ((this.host.GRID_ROWS - 1) / 2) * this.host.TILE_SPACE;\r\n\r\n for (let row = 0; row < this.host.GRID_ROWS; row++) {\r\n for (let col = 0; col < this.host.GRID_COLS; col++) {\r\n const x = startX + col * this.host.TILE_SPACE;\r\n const y = startY - row * this.host.TILE_SPACE;\r\n\r\n const tileIndex = row * this.host.GRID_COLS + col;\r\n const tileKey = this.getTileKey(groupIndex, tileIndex);\r\n\r\n // Create background mesh (for blur effects)\r\n this.createBackgroundMesh(gl, groupObject, groupIndex, tileIndex, tileKey, x, y);\r\n\r\n // Create foreground mesh (for content display)\r\n this.createForegroundMesh(gl, groupObject, groupIndex, tileIndex, tileKey, x, y);\r\n }\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Creates a background mesh for blur effects\r\n */\r\n private createBackgroundMesh(\r\n gl: any, // OGL context\r\n groupObject: Transform,\r\n groupIndex: number,\r\n tileIndex: number,\r\n tileKey: string,\r\n x: number,\r\n y: number,\r\n ): void {\r\n const backgroundProgram = this.createBackgroundProgram(groupIndex, tileIndex);\r\n const backgroundGeometry = new Plane(gl, {\r\n width: this.host.TILE_SIZE,\r\n height: this.host.TILE_SIZE,\r\n });\r\n const backgroundMesh = new Mesh(gl, {\r\n geometry: backgroundGeometry,\r\n program: backgroundProgram,\r\n });\r\n backgroundMesh.position.set(x, y, -0.01);\r\n backgroundMesh.setParent(groupObject);\r\n this.host.backgroundMeshMap.set(tileKey, backgroundMesh);\r\n }\r\n\r\n /**\r\n * Creates a foreground mesh for content display\r\n */\r\n private createForegroundMesh(\r\n gl: any, // OGL context\r\n groupObject: Transform,\r\n groupIndex: number,\r\n tileIndex: number,\r\n tileKey: string,\r\n x: number,\r\n y: number,\r\n ): void {\r\n const foregroundProgram = this.createForegroundProgram(groupIndex, tileIndex);\r\n const foregroundGeometry = new Plane(gl, {\r\n width: this.host.TILE_SIZE,\r\n height: this.host.TILE_SIZE,\r\n });\r\n const foregroundMesh = new Mesh(gl, {\r\n geometry: foregroundGeometry,\r\n program: foregroundProgram,\r\n });\r\n foregroundMesh.position.set(x, y, 0);\r\n foregroundMesh.setParent(groupObject);\r\n\r\n // Store user data for interaction\r\n (foregroundMesh as any).userData = {\r\n groupIndex,\r\n tileIndex,\r\n tileKey,\r\n } as TileUserData;\r\n\r\n this.host.foregroundMeshMap.set(tileKey, foregroundMesh);\r\n }\r\n\r\n /**\r\n * Generates a unique tile key for indexing\r\n * @param groupIndex - The index of the tile group (0-8)\r\n * @param tileIndex - The index of the tile within the group\r\n * @returns A unique string key for the tile\r\n */\r\n public getTileKey(groupIndex: number, tileIndex: number): string {\r\n return `${groupIndex}-${tileIndex}`;\r\n }\r\n\r\n /**\r\n * Calculates the card texture index for a given tile\r\n * @param groupIndex - The index of the tile group\r\n * @param tileIndex - The index of the tile within the group\r\n * @returns The index of the card data to use for this tile\r\n */\r\n public getCardTextureIndex(groupIndex: number, tileIndex: number): number {\r\n const tilesPerGroup = this.host.GRID_COLS * this.host.GRID_ROWS;\r\n return (groupIndex * tilesPerGroup + tileIndex) % this.host.cardData.length;\r\n }\r\n\r\n /**\r\n * Gets the foreground texture for a specific tile\r\n * @param groupIndex - The index of the tile group\r\n * @param tileIndex - The index of the tile within the group\r\n * @returns The foreground texture or null if not available\r\n */\r\n public getCardForegroundTexture(groupIndex: number, tileIndex: number): Texture | null {\r\n if (this.host.cardTextures.length === 0) return null;\r\n const textureIndex = this.getCardTextureIndex(groupIndex, tileIndex);\r\n return this.host.cardTextures[textureIndex]?.foreground || null;\r\n }\r\n\r\n /**\r\n * Gets the background texture for a specific tile\r\n * @param groupIndex - The index of the tile group\r\n * @param tileIndex - The index of the tile within the group\r\n * @returns The background texture or null if not available\r\n */\r\n public getCardBackgroundTexture(groupIndex: number, tileIndex: number): Texture | null {\r\n if (this.host.cardTextures.length === 0) return null;\r\n const textureIndex = this.getCardTextureIndex(groupIndex, tileIndex);\r\n return this.host.cardTextures[textureIndex]?.background || null;\r\n }\r\n\r\n /**\r\n * Creates a shader program for background tiles (with blur effects)\r\n * @param groupIndex - The index of the tile group\r\n * @param tileIndex - The index of the tile within the group\r\n * @returns A configured Program for background rendering\r\n */\r\n private createBackgroundProgram(groupIndex: number, tileIndex: number): Program {\r\n if (!this.host.renderer) throw new Error(\"Renderer not initialized\");\r\n\r\n const gl = this.host.renderer.gl;\r\n const texture = this.getCardBackgroundTexture(groupIndex, tileIndex);\r\n const texWidth = 512;\r\n const texHeight = 512;\r\n\r\n const uniforms = {\r\n map: { value: texture },\r\n resolution: { value: [texWidth, texHeight] },\r\n uOpacity: { value: this.host.initialBackgroundOpacity },\r\n };\r\n\r\n this.host.staticUniforms.set(this.getTileKey(groupIndex, tileIndex), uniforms);\r\n\r\n return new Program(gl, {\r\n vertex: gaussianBlurVertexShader,\r\n fragment: gaussianBlurFragmentShader,\r\n uniforms: uniforms,\r\n transparent: true,\r\n cullFace: false,\r\n });\r\n }\r\n\r\n /**\r\n * Creates a shader program for foreground tiles (content display)\r\n * @param groupIndex - The index of the tile group\r\n * @param tileIndex - The index of the tile within the group\r\n * @returns A configured Program for foreground rendering\r\n */\r\n private createForegroundProgram(groupIndex: number, tileIndex: number): Program {\r\n if (!this.host.renderer) throw new Error(\"Renderer not initialized\");\r\n\r\n const gl = this.host.renderer.gl;\r\n const texture = this.getCardForegroundTexture(groupIndex, tileIndex);\r\n\r\n return new Program(gl, {\r\n vertex: `\r\n attribute vec2 uv;\r\n attribute vec3 position;\r\n \r\n uniform mat4 modelViewMatrix;\r\n uniform mat4 projectionMatrix;\r\n \r\n varying vec2 vUv;\r\n \r\n void main() {\r\n // Flip UV coordinates 180 degrees (both X and Y)\r\n vUv = vec2(uv.x, 1.0 - uv.y);\r\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\r\n }\r\n `,\r\n fragment: `\r\n precision highp float;\r\n \r\n uniform sampler2D map;\r\n \r\n varying vec2 vUv;\r\n \r\n void main() {\r\n gl_FragColor = texture2D(map, vUv);\r\n }\r\n `,\r\n uniforms: {\r\n map: { value: texture },\r\n },\r\n transparent: true,\r\n cullFace: false,\r\n });\r\n }\r\n\r\n /**\r\n * Generates textures for all card data\r\n *\r\n * Creates both foreground and background textures for each card\r\n * using the Canvas 2D API. Textures are cached for efficient reuse.\r\n *\r\n * @returns Promise that resolves when all textures are generated\r\n */\r\n private async generateTexturesForCardData(): Promise<void> {\r\n if (!this.host.renderer) throw new Error(\"Renderer not initialized\");\r\n\r\n if (this.host.cardData.length === 0) {\r\n this.host.cardTextures = [];\r\n return;\r\n }\r\n\r\n const texturePromises = this.host.cardData.map(async (card): Promise<CardTexturePair> => {\r\n const foreground = await generateForegroundTexture(card, this.host.renderer!);\r\n const background = await generateBackgroundTexture(card, this.host.renderer!);\r\n return { foreground, background };\r\n });\r\n\r\n this.host.cardTextures = await Promise.all(texturePromises);\r\n }\r\n\r\n /**\r\n * Extracts tile key from a mesh using its userData\r\n * @param mesh - The mesh to get the tile key from\r\n * @returns The tile key or empty string if not found\r\n */\r\n public getTileKeyFromMesh(mesh: Mesh): string {\r\n const userData = (mesh as any).userData as TileUserData;\r\n return userData?.tileKey || \"\";\r\n }\r\n\r\n /**\r\n * Gets the card data for a specific tile\r\n * @param groupIndex - The group index of the tile\r\n * @param tileIndex - The tile index within the group\r\n * @returns The card data for the tile\r\n */\r\n public getCardDataForTile(groupIndex: number, tileIndex: number): CardData {\r\n const cardIndex = this.getCardTextureIndex(groupIndex, tileIndex);\r\n return (\r\n this.host.cardData[cardIndex] || {\r\n title: \"Default Card\",\r\n badge: \"\",\r\n description: \"No data available\",\r\n tags: [],\r\n date: new Date().getFullYear().toString(),\r\n }\r\n );\r\n }\r\n\r\n /**\r\n * Updates card data and regenerates textures\r\n *\r\n * This method allows for dynamic content updates by:\r\n * 1. Updating the card data\r\n * 2. Regenerating all textures\r\n * 3. Updating existing tile programs with new textures\r\n *\r\n * @param newCardData - The new card data to use\r\n * @returns Promise that resolves when update is complete\r\n */\r\n public async updateCardData(newCardData: CardData[]): Promise<void> {\r\n this.host.cardData = newCardData;\r\n await this.generateTexturesForCardData();\r\n this.updateTileTextures();\r\n }\r\n\r\n /**\r\n * Updates all tile textures with newly generated textures\r\n * This is called after card data changes to refresh the display\r\n */\r\n private updateTileTextures(): void {\r\n this.host.tileGroupsData.forEach((_, groupIndex) => {\r\n for (let row = 0; row < this.host.GRID_ROWS; row++) {\r\n for (let col = 0; col < this.host.GRID_COLS; col++) {\r\n const tileIndex = row * this.host.GRID_COLS + col;\r\n const tileKey = this.getTileKey(groupIndex, tileIndex);\r\n\r\n // Update foreground mesh texture\r\n const foregroundMesh = this.host.foregroundMeshMap.get(tileKey);\r\n if (foregroundMesh && foregroundMesh.program) {\r\n const newForegroundTexture = this.getCardForegroundTexture(groupIndex, tileIndex);\r\n if (newForegroundTexture) {\r\n foregroundMesh.program.uniforms.map.value = newForegroundTexture;\r\n }\r\n }\r\n\r\n // Update background mesh texture\r\n const backgroundMesh = this.host.backgroundMeshMap.get(tileKey);\r\n if (backgroundMesh && backgroundMesh.program) {\r\n const newBackgroundTexture = this.getCardBackgroundTexture(groupIndex, tileIndex);\r\n if (newBackgroundTexture) {\r\n backgroundMesh.program.uniforms.map.value = newBackgroundTexture;\r\n }\r\n }\r\n }\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Gets all interactive meshes (foreground meshes that can be clicked)\r\n * @returns Array of foreground meshes\r\n */\r\n public getInteractiveMeshes(): Mesh[] {\r\n return Array.from(this.host.foregroundMeshMap.values());\r\n }\r\n\r\n /**\r\n * Clears all grid data and meshes\r\n * This is useful for cleanup or reinitialization\r\n */\r\n public clear(): void {\r\n // Clear tile group data\r\n this.host.tileGroupsData = [];\r\n\r\n // Clear group objects\r\n this.host.groupObjects.forEach((group) => {\r\n if (group && group.parent) {\r\n group.parent.removeChild(group);\r\n }\r\n });\r\n this.host.groupObjects = [];\r\n\r\n // Clear mesh maps\r\n this.host.foregroundMeshMap.clear();\r\n this.host.backgroundMeshMap.clear();\r\n\r\n // Clear uniforms and textures\r\n this.host.staticUniforms.clear();\r\n this.host.cardTextures = [];\r\n\r\n this.isInitialized = false;\r\n }\r\n\r\n /**\r\n * Gets the initialization status\r\n */\r\n public get initialized(): boolean {\r\n return this.isInitialized;\r\n }\r\n\r\n /**\r\n * Gets statistics about the current grid\r\n * @returns Object containing grid statistics\r\n */\r\n public getGridStats(): {\r\n totalGroups: number;\r\n tilesPerGroup: number;\r\n totalTiles: number;\r\n totalTextures: number;\r\n memoryEstimate: string;\r\n } {\r\n const tilesPerGroup = this.host.GRID_COLS * this.host.GRID_ROWS;\r\n const totalTiles = this.host.tileGroupsData.length * tilesPerGroup;\r\n const totalTextures = this.host.cardTextures.length * 2; // foreground + background\r\n\r\n // Rough memory estimate (512x512 RGBA textures)\r\n const bytesPerTexture = 512 * 512 * 4; // RGBA\r\n const totalMemoryBytes = totalTextures * bytesPerTexture;\r\n const memoryMB = (totalMemoryBytes / (1024 * 1024)).toFixed(2);\r\n\r\n return {\r\n totalGroups: this.host.tileGroupsData.length,\r\n tilesPerGroup,\r\n totalTiles,\r\n totalTextures,\r\n memoryEstimate: `${memoryMB} MB`,\r\n };\r\n }\r\n}\r\n"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "InfiniteGrid.vue",
|
|
26
|
+
"content": "<template>\r\n <div\r\n ref=\"infiniteGridContainer\"\r\n class=\"infinite-grid-container\"\r\n ></div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, onMounted, onBeforeUnmount, watch, computed } from \"vue\";\r\nimport { InfiniteGridClass } from \"./InfiniteGridClass\";\r\nimport type { InfiniteGridOptions, CardData } from \"./types\";\r\n\r\ninterface Props {\r\n cardData: CardData[];\r\n options?: Partial<InfiniteGridOptions>;\r\n onTilesLoaded?: () => void;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n options: () => ({}),\r\n});\r\n\r\nconst emit = defineEmits([\"tileClicked\"]);\r\n\r\n// Define default options for the infinite grid\r\nconst defaultOptions: InfiniteGridOptions = {\r\n gridCols: 4,\r\n gridRows: 4,\r\n gridGap: 0,\r\n tileSize: 3,\r\n baseCameraZ: 10,\r\n enablePostProcessing: true,\r\n postProcessParams: {\r\n distortionIntensity: -0.2,\r\n vignetteOffset: 0.0,\r\n vignetteDarkness: 0.0,\r\n },\r\n};\r\n\r\n// Merge default options with passed options\r\nconst mergedOptions = computed(() => ({\r\n ...defaultOptions,\r\n ...props.options,\r\n postProcessParams: {\r\n ...defaultOptions.postProcessParams,\r\n ...props.options?.postProcessParams,\r\n },\r\n}));\r\n\r\nconst infiniteGridContainer = ref<HTMLElement | null>(null);\r\nlet infiniteGridInstance: InfiniteGridClass | null = null;\r\n\r\nfunction handleTileClicked(event: Event) {\r\n const customEvent = event as CustomEvent;\r\n emit(\"tileClicked\", customEvent.detail);\r\n}\r\n\r\nonMounted(async () => {\r\n if (infiniteGridContainer.value) {\r\n infiniteGridInstance = new InfiniteGridClass(\r\n infiniteGridContainer.value,\r\n props.cardData,\r\n mergedOptions.value,\r\n );\r\n await infiniteGridInstance.init();\r\n\r\n props.onTilesLoaded?.();\r\n\r\n infiniteGridContainer.value.addEventListener(\"tileClicked\", handleTileClicked);\r\n }\r\n});\r\n\r\nonBeforeUnmount(() => {\r\n if (infiniteGridInstance) {\r\n if (infiniteGridContainer.value) {\r\n infiniteGridContainer.value.removeEventListener(\"tileClicked\", handleTileClicked);\r\n }\r\n infiniteGridInstance.dispose();\r\n infiniteGridInstance = null;\r\n }\r\n});\r\n\r\nwatch(\r\n () => [props.cardData, mergedOptions.value],\r\n async ([newCardData, newOptions]) => {\r\n if (infiniteGridInstance) {\r\n infiniteGridInstance.dispose();\r\n infiniteGridInstance = null;\r\n }\r\n\r\n if (infiniteGridContainer.value) {\r\n infiniteGridInstance = new InfiniteGridClass(\r\n infiniteGridContainer.value,\r\n newCardData as CardData[],\r\n newOptions as InfiniteGridOptions,\r\n );\r\n await infiniteGridInstance.init();\r\n infiniteGridContainer.value.addEventListener(\"tileClicked\", handleTileClicked);\r\n }\r\n },\r\n { deep: true },\r\n);\r\n</script>\r\n\r\n<style scoped>\r\n.infinite-grid-container {\r\n position: relative;\r\n width: 100%;\r\n height: 100%;\r\n margin: 0;\r\n padding: 0;\r\n overflow: hidden;\r\n background: #000;\r\n}\r\n\r\n.infinite-grid-container > canvas {\r\n display: block;\r\n width: 100% !important;\r\n height: 100% !important;\r\n}\r\n\r\n.vignette-overlay {\r\n position: absolute;\r\n top: 0;\r\n left: 0;\r\n width: 100%;\r\n height: 100%;\r\n z-index: 10;\r\n pointer-events: none;\r\n background: radial-gradient(\r\n ellipse at center,\r\n transparent 0%,\r\n rgba(0, 0, 0, 0.1) 60%,\r\n rgba(0, 0, 0, 0.8) 90%,\r\n rgba(0, 0, 0, 1) 100%\r\n );\r\n}\r\n\r\n.blur-overlay {\r\n position: absolute;\r\n top: 0;\r\n left: 0;\r\n width: 100%;\r\n height: 100%;\r\n z-index: 15;\r\n backdrop-filter: blur(8px);\r\n -webkit-backdrop-filter: blur(8px);\r\n pointer-events: none;\r\n mask-image: radial-gradient(\r\n ellipse at center,\r\n transparent 50%,\r\n rgba(0, 0, 0, 0.1) 70%,\r\n rgba(0, 0, 0, 0.8) 90%,\r\n rgba(0, 0, 0, 1) 100%\r\n );\r\n -webkit-mask-image: radial-gradient(\r\n ellipse at center,\r\n transparent 60%,\r\n rgba(0, 0, 0, 0.5) 70%,\r\n rgba(0, 0, 0, 0.8) 90%,\r\n rgba(0, 0, 0, 1) 100%\r\n );\r\n}\r\n</style>\r\n"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"path": "InfiniteGridClass.ts",
|
|
30
|
+
"content": "/* eslint-disable @typescript-eslint/no-explicit-any */\r\n/**\r\n * @fileoverview InfiniteGridClass - OGL Infinite Scrolling Grid System\r\n *\r\n * A comprehensive OGL-based infinite grid system that creates a seamless,\r\n * scrollable interface for displaying card-based content. The system uses a\r\n * 3x3 tile group architecture to create the illusion of infinite content while\r\n * maintaining optimal performance.\r\n *\r\n * Key Features:\r\n * - Infinite scrolling in all directions\r\n * - Interactive hover effects with background blur\r\n * - Click events with custom data dispatching\r\n * - Post-processing visual effects (distortion, vignette)\r\n * - GSAP-powered smooth animations and inertia\r\n * - Responsive viewport calculations\r\n * - Memory-efficient tile repositioning system\r\n *\r\n * Architecture Overview:\r\n * - Uses 9 tile groups arranged in a 3x3 pattern\r\n * - Each group contains a configurable grid of individual tiles\r\n * - Groups are repositioned as the user scrolls to maintain infinite effect\r\n * - Each tile has foreground (content) and background (blur) layers\r\n * - Textures are generated dynamically from card data using Canvas 2D API\r\n */\r\n\r\nimport { Renderer, Camera, Transform, Mesh, Vec2, RenderTarget, Raycast } from \"ogl\";\r\nimport { gsap } from \"gsap\";\r\nimport { InertiaPlugin } from \"gsap/InertiaPlugin\";\r\nimport { CustomPostProcessShader } from \"./PostProcessShader\";\r\nimport { EventHandler, type EventHandlerHost } from \"./EventHandler\";\r\nimport { DisposalManager, type DisposableHost } from \"./DisposalManager\";\r\nimport { GridManager, type GridManagerHost } from \"./GridManager\";\r\n\r\nimport type {\r\n CardData,\r\n InfiniteGridOptions,\r\n Position2D,\r\n ScrollState,\r\n TileGroupData,\r\n CardTexturePair,\r\n Viewport,\r\n} from \"./types.ts\";\r\n\r\ngsap.registerPlugin(InertiaPlugin);\r\n\r\n/**\r\n * InfiniteGridClass - An OGL-based infinite scrolling grid system\r\n *\r\n * This class creates an infinite, scrollable grid of tiles that displays card data.\r\n * It uses a 3x3 grid system where tiles are repositioned as the user scrolls to\r\n * create an infinite scrolling effect. Each tile can display custom content\r\n * with foreground and background textures.\r\n *\r\n * Key Features:\r\n * - Infinite scrolling in all directions using tile repositioning\r\n * - Interactive hover effects with background blur transitions\r\n * - Click events with custom event dispatching\r\n * - Optional post-processing effects (distortion, vignette)\r\n * - GSAP-powered smooth animations and inertia scrolling\r\n * - Responsive design with automatic viewport calculations\r\n *\r\n * @example\r\n * ```typescript\r\n * const grid = new InfiniteGridClass(\r\n * containerElement,\r\n * cardDataArray,\r\n * {\r\n * gridCols: 3,\r\n * gridRows: 3,\r\n * enablePostProcessing: true\r\n * }\r\n * );\r\n * await grid.init();\r\n * ```\r\n */\r\nexport class InfiniteGridClass implements EventHandlerHost, DisposableHost, GridManagerHost {\r\n /**\r\n * Core Container and Data Properties\r\n */\r\n\r\n /** The HTML element that contains the OGL canvas */\r\n public container: HTMLElement;\r\n /** Array of card data to be displayed in the grid */\r\n public cardData: CardData[];\r\n /** Merged configuration options with defaults applied */\r\n public options: Required<InfiniteGridOptions>;\r\n\r\n /**\r\n * Grid Layout Properties (calculated once, read-only)\r\n */\r\n\r\n /** Gap between tiles in OGL world units */\r\n public readonly GRID_GAP: number;\r\n /** Size of each individual tile in OGL world units */\r\n public readonly TILE_SIZE: number;\r\n /** Total space occupied by one tile including gap */\r\n public readonly TILE_SPACE: number;\r\n /** Number of columns in each grid section */\r\n public readonly GRID_COLS: number;\r\n /** Number of rows in each grid section */\r\n public readonly GRID_ROWS: number;\r\n /** Total width of one grid section */\r\n public readonly GRID_WIDTH: number;\r\n /** Total height of one grid section */\r\n public readonly GRID_HEIGHT: number;\r\n /** Total width of all 3 grid sections (for infinite wrapping) */\r\n private readonly TOTAL_GRID_WIDTH: number;\r\n /** Total height of all 3 grid sections (for infinite wrapping) */\r\n private readonly TOTAL_GRID_HEIGHT: number;\r\n\r\n /**\r\n * OGL Core Rendering Objects\r\n */\r\n\r\n /** Main OGL scene containing all 3D objects */\r\n public scene: Transform;\r\n /** Perspective camera for viewing the scene */\r\n public camera: Camera | null;\r\n /** WebGL renderer for drawing to the canvas */\r\n public renderer: Renderer | null;\r\n /** 2D pointer coordinates for interaction */\r\n public pointer: Vec2;\r\n /** Raycast utility for mouse/touch interaction */\r\n public raycast: Raycast;\r\n\r\n /**\r\n * Post-Processing Objects\r\n */\r\n\r\n /** Post-processing shader for visual effects (distortion, vignette) */\r\n public postProcessShader: CustomPostProcessShader | null;\r\n /** Render target for capturing the scene before post-processing */\r\n public sceneRenderTarget: RenderTarget | null;\r\n\r\n /**\r\n * Scene Objects and Data Structures\r\n */\r\n\r\n /** Array of OGL transforms, each containing one 3x3 section of tiles */\r\n public groupObjects: Transform[];\r\n /** Map of tile keys to foreground mesh objects (clickable content) */\r\n public foregroundMeshMap: Map<string, Mesh>;\r\n /** Map of tile keys to background mesh objects (blur effect on hover) */\r\n public backgroundMeshMap: Map<string, Mesh>;\r\n /** Generated textures for all cards (foreground + background pairs) */\r\n public cardTextures: CardTexturePair[];\r\n /** Static shader uniforms for background materials */\r\n public staticUniforms: Map<string, any>;\r\n\r\n /**\r\n * User Interaction State\r\n */\r\n\r\n /** Key of the currently hovered tile (empty string if none) */\r\n public currentHoveredTileKey: string;\r\n /** Whether the user is currently dragging/scrolling */\r\n public isDown: boolean;\r\n public isHoveringCanvas: boolean;\r\n /** Whether the scene has moved significantly during this interaction */\r\n public hasMovedSignificantly: boolean;\r\n /** Position where the current drag started */\r\n public startPosition: Position2D;\r\n /** Scroll position when the current drag started */\r\n public scrollPosition: Position2D;\r\n /** Current scroll state and behavior settings */\r\n public scroll: ScrollState;\r\n /** Direction of scroll movement for infinite wrapping logic */\r\n private direction: Position2D;\r\n /** GSAP InertiaPlugin tracker for smooth scroll transitions */\r\n public scrollTracker: any;\r\n\r\n /**\r\n * Animation Configuration Constants\r\n */\r\n\r\n /** Duration of hover transition animations in seconds */\r\n public readonly hoverTransitionDuration: number;\r\n /** GSAP easing function for hover animations */\r\n public readonly hoverEase: string;\r\n /** Initial opacity of background blur effect (0 = transparent) */\r\n public readonly initialBackgroundOpacity: number;\r\n /** Target opacity when background is hovered (1 = fully visible) */\r\n public readonly hoveredBackgroundOpacity: number;\r\n /** Maximum movement distance in pixels before click is disabled */\r\n public readonly maxClickMovement: number;\r\n\r\n /**\r\n * Animation Frame Management\r\n */\r\n\r\n /** RequestAnimationFrame ID for the main render loop */\r\n public animationFrameId: number | null;\r\n\r\n /**\r\n * Tile Group Data Structure\r\n */\r\n\r\n /** Array containing position data for all 9 tile groups (3x3 infinite grid) */\r\n public tileGroupsData: TileGroupData[];\r\n\r\n /**\r\n * Modular System Components\r\n */\r\n\r\n /** Event handler for managing user interactions */\r\n public eventHandler?: EventHandler;\r\n /** Grid manager for handling grid creation and management */\r\n public gridManager: GridManager;\r\n /** Disposal manager for resource cleanup */\r\n private disposalManager: DisposalManager;\r\n\r\n /**\r\n * Creates a new InfiniteGridClass instance\r\n *\r\n * @param containerElement - HTML element that will contain the OGL canvas\r\n * @param cardData - Array of card data to display in the grid (defaults to empty array)\r\n * @param options - Configuration options (merged with defaults)\r\n *\r\n * @throws {Error} Throws if containerElement is null or undefined\r\n *\r\n * @example\r\n * ```typescript\r\n * const container = document.getElementById('grid-container');\r\n * const cards = [\r\n * { title: 'Card 1', badge: 'NEW', description: '...', tags: ['tag1'], date: '2024' },\r\n * // ... more cards\r\n * ];\r\n * const grid = new InfiniteGridClass(container, cards, {\r\n * gridCols: 4,\r\n * tileSize: 2.5,\r\n * enablePostProcessing: true\r\n * });\r\n * ```\r\n */\r\n constructor(\r\n containerElement: HTMLElement,\r\n cardData: CardData[] = [],\r\n options: Partial<InfiniteGridOptions> = {},\r\n ) {\r\n if (!containerElement) {\r\n console.error(\"InfiniteGridClass: Container element is required.\");\r\n throw new Error(\"Container element is required\");\r\n }\r\n\r\n this.container = containerElement;\r\n this.cardData = cardData;\r\n\r\n // Merge options with defaults to ensure all properties are defined\r\n this.options = {\r\n gridCols: 3,\r\n gridRows: 3,\r\n gridGap: 0,\r\n tileSize: 3,\r\n baseCameraZ: 10,\r\n enablePostProcessing: true,\r\n postProcessParams: {\r\n distortionIntensity: 0.0,\r\n vignetteOffset: 1.2, // Set outside screen bounds to disable vignette initially\r\n vignetteDarkness: 1.5,\r\n ...options.postProcessParams,\r\n },\r\n ...options,\r\n };\r\n\r\n // Initialize grid properties\r\n this.GRID_GAP = this.options.gridGap;\r\n this.TILE_SIZE = this.options.tileSize;\r\n this.TILE_SPACE = this.TILE_SIZE + this.GRID_GAP;\r\n this.GRID_COLS = this.options.gridCols;\r\n this.GRID_ROWS = this.options.gridRows;\r\n this.GRID_WIDTH = this.TILE_SPACE * this.GRID_COLS;\r\n this.GRID_HEIGHT = this.TILE_SPACE * this.GRID_ROWS;\r\n this.TOTAL_GRID_WIDTH = this.GRID_WIDTH * 3;\r\n this.TOTAL_GRID_HEIGHT = this.GRID_HEIGHT * 3;\r\n\r\n // Initialize OGL objects\r\n this.scene = new Transform();\r\n this.camera = null;\r\n this.renderer = null;\r\n this.pointer = new Vec2();\r\n this.raycast = new Raycast();\r\n\r\n // Initialize post-processing\r\n this.postProcessShader = null;\r\n this.sceneRenderTarget = null;\r\n\r\n // Initialize maps and arrays\r\n this.groupObjects = [];\r\n this.foregroundMeshMap = new Map();\r\n this.backgroundMeshMap = new Map();\r\n this.cardTextures = [];\r\n this.staticUniforms = new Map();\r\n\r\n // Initialize interaction state\r\n this.currentHoveredTileKey = \"\";\r\n this.isDown = false;\r\n this.isHoveringCanvas = false;\r\n this.hasMovedSignificantly = false;\r\n this.startPosition = { x: 0, y: 0 };\r\n this.scrollPosition = { x: 0, y: 0 };\r\n this.scroll = {\r\n scale: 0.012,\r\n current: { x: 0, y: 0 },\r\n last: { x: 0, y: 0 },\r\n };\r\n this.direction = { x: 0, y: 0 };\r\n this.scrollTracker = InertiaPlugin.track(this.scroll.current, \"x,y\")[0];\r\n\r\n // Initialize animation properties\r\n this.hoverTransitionDuration = 0.6;\r\n this.hoverEase = \"power2.out\";\r\n this.initialBackgroundOpacity = 0.0;\r\n this.hoveredBackgroundOpacity = 1.0;\r\n this.maxClickMovement = 5; // pixels\r\n\r\n this.animationFrameId = null;\r\n this.tileGroupsData = [];\r\n\r\n // Initialize modular components\r\n this.eventHandler = new EventHandler(this);\r\n this.gridManager = new GridManager(this);\r\n this.disposalManager = new DisposalManager(this);\r\n\r\n // Bind remaining methods to maintain proper 'this' context\r\n this.render = this.render.bind(this);\r\n }\r\n\r\n /**\r\n * Initializes the infinite grid system asynchronously\r\n *\r\n * This method sets up all necessary components in the correct order:\r\n * 1. WebGL renderer and camera\r\n * 2. Tile group positioning structure\r\n * 3. Texture generation for all card data\r\n * 4. 3D mesh creation and scene setup\r\n * 5. Event listeners for interaction\r\n * 6. Animation systems and render loop\r\n *\r\n * @returns Promise that resolves when initialization is complete\r\n *\r\n * @example\r\n * ```typescript\r\n * const grid = new InfiniteGridClass(container, cardData);\r\n * await grid.init(); // Wait for all textures to load and scene to be ready\r\n * // Grid is now interactive and rendering\r\n * ```\r\n */\r\n public async init(): Promise<void> {\r\n this.setupRenderer();\r\n this.setupCamera();\r\n this.setupPostProcessing();\r\n\r\n // Initialize grid using the modular system\r\n await this.gridManager.initialize();\r\n\r\n // Initialize event handling using the modular system\r\n if (this.eventHandler) {\r\n this.eventHandler.initialize();\r\n }\r\n\r\n this.animateInertiaScroll();\r\n this.animatePostProcessing(\r\n -0.1, // Target distortionIntensity\r\n 0.3, // Target vignetteOffset (where vignette starts - should be < 1.0)\r\n 1.25, // Target vignetteDarkness (where vignette reaches max - should be > vignetteOffset)\r\n 1.5, // Duration\r\n 1.5, // Delay\r\n \"power3.out\", // Ease\r\n );\r\n\r\n this.updatePositions();\r\n this.render();\r\n }\r\n\r\n /**\r\n * Sets up the WebGL renderer with optimal settings\r\n *\r\n * Creates an OGL renderer with antialiasing and transparency,\r\n * configures it for the container size, and appends the canvas\r\n * to the DOM.\r\n */\r\n private setupRenderer(): void {\r\n const gl =\r\n this.container.ownerDocument.createElement(\"canvas\").getContext(\"webgl2\") ||\r\n this.container.ownerDocument.createElement(\"canvas\").getContext(\"webgl\");\r\n if (!gl) {\r\n throw new Error(\"WebGL not supported\");\r\n }\r\n\r\n this.renderer = new Renderer({\r\n canvas: gl.canvas as HTMLCanvasElement,\r\n width: this.container.clientWidth,\r\n height: this.container.clientHeight,\r\n dpr: window.devicePixelRatio,\r\n alpha: true,\r\n antialias: true,\r\n });\r\n\r\n // Set canvas styles\r\n this.renderer.gl.canvas.style.width = \"100%\";\r\n this.renderer.gl.canvas.style.height = \"100%\";\r\n\r\n this.container.appendChild(this.renderer.gl.canvas);\r\n }\r\n\r\n /**\r\n * Sets up the perspective camera with proper positioning\r\n *\r\n * Creates a perspective camera with a 45-degree field of view,\r\n * positions it at the configured Z distance.\r\n */\r\n private setupCamera(): void {\r\n const aspectRatio = this.container.clientWidth / this.container.clientHeight;\r\n this.camera = new Camera(this.renderer!.gl, {\r\n fov: 45,\r\n aspect: aspectRatio,\r\n near: 1,\r\n far: 1000,\r\n });\r\n this.camera.position.set(0, 0, this.options.baseCameraZ);\r\n }\r\n\r\n /**\r\n * Sets up post-processing effects if enabled\r\n *\r\n * Creates a render target for capturing the scene before post-processing\r\n * and initializes the post-processing shader with configured parameters.\r\n */\r\n private setupPostProcessing(): void {\r\n if (!this.options.enablePostProcessing || !this.renderer) {\r\n return;\r\n }\r\n\r\n // Create render target for capturing the scene\r\n this.sceneRenderTarget = new RenderTarget(this.renderer.gl, {\r\n width: this.container.clientWidth,\r\n height: this.container.clientHeight,\r\n });\r\n\r\n // Create post-processing shader\r\n this.postProcessShader = new CustomPostProcessShader(\r\n this.renderer.gl as any, // Type assertion needed for OGL context\r\n this.options.postProcessParams,\r\n );\r\n }\r\n\r\n /**\r\n * Calculates the viewport dimensions in world space\r\n *\r\n * Uses the camera's field of view and position to determine how much\r\n * world space is visible. This is essential for infinite scrolling\r\n * calculations to know when tile groups need to be repositioned.\r\n *\r\n * @returns Viewport object with width and height in world units\r\n */\r\n private get viewport(): Viewport {\r\n if (!this.camera) {\r\n return { width: 0, height: 0 };\r\n }\r\n const fov = this.camera.fov * (Math.PI / 180); // Convert FOV to radians\r\n const viewHeight = 2 * Math.tan(fov / 2) * this.camera.position.z;\r\n return { width: viewHeight * this.camera.aspect, height: viewHeight };\r\n }\r\n\r\n public updatePositions(): void {\r\n const scrollX = this.scroll.current.x;\r\n const scrollY = this.scroll.current.y;\r\n\r\n // Update direction based on scroll movement\r\n if (this.scroll.current.y > this.scroll.last.y) {\r\n this.direction.y = -1;\r\n } else if (this.scroll.current.y < this.scroll.last.y) {\r\n this.direction.y = 1;\r\n } else {\r\n this.direction.y = 0;\r\n }\r\n\r\n if (this.scroll.current.x > this.scroll.last.x) {\r\n this.direction.x = -1;\r\n } else if (this.scroll.current.x < this.scroll.last.x) {\r\n this.direction.x = 1;\r\n } else {\r\n this.direction.x = 0;\r\n }\r\n\r\n this.tileGroupsData.forEach((groupData, i) => {\r\n const groupObject = this.groupObjects[i];\r\n\r\n if (groupObject) {\r\n const posX = groupData.basePos.x + scrollX + groupData.offset.x;\r\n const posY = groupData.basePos.y + scrollY + groupData.offset.y;\r\n\r\n const groupOffX = this.GRID_WIDTH / 2;\r\n const groupOffY = this.GRID_HEIGHT / 2;\r\n const viewportOff = {\r\n x: this.viewport.width / 2,\r\n y: this.viewport.height / 2,\r\n };\r\n\r\n // Handle infinite scrolling wrapping\r\n if (this.direction.x < 0 && posX - groupOffX > viewportOff.x) {\r\n groupData.offset.x -= this.TOTAL_GRID_WIDTH;\r\n } else if (this.direction.x > 0 && posX + groupOffX < -viewportOff.x) {\r\n groupData.offset.x += this.TOTAL_GRID_WIDTH;\r\n }\r\n\r\n if (this.direction.y < 0 && posY - groupOffY > viewportOff.y) {\r\n groupData.offset.y -= this.TOTAL_GRID_HEIGHT;\r\n } else if (this.direction.y > 0 && posY + groupOffY < -viewportOff.y) {\r\n groupData.offset.y += this.TOTAL_GRID_HEIGHT;\r\n }\r\n\r\n groupObject.position.x = groupData.basePos.x + scrollX + groupData.offset.x;\r\n groupObject.position.y = groupData.basePos.y + scrollY + groupData.offset.y;\r\n groupObject.position.z = groupData.basePos.z;\r\n }\r\n });\r\n }\r\n\r\n public animateInertiaScroll(vx: number | string = \"auto\", vy: number | string = \"auto\"): void {\r\n gsap.to(this.scroll.current, {\r\n inertia: {\r\n x: vx,\r\n y: vy,\r\n min: 60,\r\n resistance: 100,\r\n },\r\n ease: \"power2.out\",\r\n onUpdate: () => this.updatePositions(),\r\n onComplete: () => {\r\n this.direction.x = 0;\r\n this.direction.y = 0;\r\n },\r\n });\r\n }\r\n\r\n /**\r\n * Gets all interactive meshes for raycasting\r\n * @returns Array of foreground meshes that can be interacted with\r\n */\r\n public getInteractiveMeshes(): Mesh[] {\r\n return this.gridManager.getInteractiveMeshes();\r\n }\r\n\r\n /**\r\n * Updates mouse/touch coordinates for raycasting\r\n * @param clientX - X coordinate in client space\r\n * @param clientY - Y coordinate in client space\r\n */\r\n public updatePointerCoordinates(clientX: number, clientY: number): void {\r\n if (!this.renderer) return;\r\n\r\n // Convert to normalized device coordinates (-1 to 1)\r\n const rect = this.renderer.gl.canvas.getBoundingClientRect();\r\n const x = ((clientX - rect.left) / rect.width) * 2 - 1;\r\n const y = -(((clientY - rect.top) / rect.height) * 2 - 1); // Flip Y coordinate\r\n\r\n this.pointer.set(x, y);\r\n }\r\n\r\n /**\r\n * Performs raycasting and returns hit results\r\n * @returns Array of hit meshes ordered by distance\r\n */\r\n public performRaycast(): Mesh[] {\r\n if (!this.camera || !this.renderer) return [];\r\n\r\n // Update raycast with current camera and mouse position\r\n this.raycast.castMouse(this.camera, this.pointer);\r\n\r\n // Get all interactive meshes\r\n const meshes = this.getInteractiveMeshes();\r\n\r\n // Perform intersection test\r\n const hits = this.raycast.intersectBounds(meshes);\r\n\r\n return hits;\r\n }\r\n\r\n /**\r\n * Extracts tile key from a mesh using its userData\r\n * @param mesh - The mesh to get the tile key from\r\n * @returns The tile key or empty string if not found\r\n */\r\n public getTileKeyFromMesh(mesh: Mesh): string {\r\n return this.gridManager.getTileKeyFromMesh(mesh);\r\n }\r\n\r\n public fadeInBackground(mesh: Mesh): void {\r\n if (mesh.program && mesh.program.uniforms.uOpacity) {\r\n gsap.to(mesh.program.uniforms.uOpacity, {\r\n value: this.hoveredBackgroundOpacity,\r\n duration: this.hoverTransitionDuration,\r\n ease: this.hoverEase,\r\n overwrite: true,\r\n });\r\n }\r\n }\r\n\r\n public fadeOutBackground(mesh: Mesh): void {\r\n if (mesh.program && mesh.program.uniforms.uOpacity) {\r\n gsap.to(mesh.program.uniforms.uOpacity, {\r\n value: this.initialBackgroundOpacity,\r\n duration: this.hoverTransitionDuration,\r\n ease: this.hoverEase,\r\n overwrite: true,\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * Gets the card data for a specific tile\r\n * @param groupIndex - The group index of the tile\r\n * @param tileIndex - The tile index within the group\r\n * @returns The card data for the tile\r\n */\r\n public getCardDataForTile(groupIndex: number, tileIndex: number): CardData {\r\n return this.gridManager.getCardDataForTile(groupIndex, tileIndex);\r\n }\r\n\r\n private render(): void {\r\n this.scroll.last.x = this.scroll.current.x;\r\n this.scroll.last.y = this.scroll.current.y;\r\n this.updatePositions();\r\n\r\n if (this.renderer && this.camera) {\r\n if (this.options.enablePostProcessing && this.postProcessShader && this.sceneRenderTarget) {\r\n // Render scene to render target first\r\n this.renderer.render({\r\n scene: this.scene,\r\n camera: this.camera,\r\n target: this.sceneRenderTarget,\r\n });\r\n\r\n // Debug: Check if render target has a texture\r\n if (!this.sceneRenderTarget.texture) {\r\n console.error(\"PostProcessing: Render target has no texture\");\r\n }\r\n\r\n // Set the rendered scene as input to post-processing shader\r\n this.postProcessShader.setInputTexture(this.sceneRenderTarget.texture);\r\n\r\n // Render post-processing effect to screen\r\n this.postProcessShader.render(null); // null = render to screen\r\n } else {\r\n // Direct render without post-processing\r\n this.renderer.render({ scene: this.scene, camera: this.camera });\r\n }\r\n }\r\n\r\n this.animationFrameId = requestAnimationFrame(this.render);\r\n }\r\n\r\n /**\r\n * Animates the post-processing effects\r\n *\r\n * @param targetDistortion - Target distortion intensity (0 = no distortion)\r\n * @param targetVignetteOffset - Target vignette offset (0.0-1.0)\r\n * @param targetVignetteDarkness - Target vignette darkness (should be > offset)\r\n * @param duration - Animation duration in seconds\r\n * @param delay - Animation delay in seconds\r\n * @param ease - GSAP ease function\r\n *\r\n * @example\r\n * ```typescript\r\n * // Animate to strong distortion and vignette\r\n * grid.animatePostProcessing(0.5, 0.6, 0.9, 2.0);\r\n *\r\n * // Reset to no effects\r\n * grid.animatePostProcessing(0, 0.8, 1.0, 1.0);\r\n * ```\r\n */\r\n public animatePostProcessing(\r\n targetDistortion: number,\r\n targetVignetteOffset: number,\r\n targetVignetteDarkness: number,\r\n duration: number = 1,\r\n delay: number = 0,\r\n ease: string = \"power2.out\",\r\n ): void {\r\n if (this.postProcessShader) {\r\n this.postProcessShader.animate(\r\n targetDistortion,\r\n targetVignetteOffset,\r\n targetVignetteDarkness,\r\n duration,\r\n delay,\r\n ease,\r\n );\r\n }\r\n }\r\n\r\n /**\r\n * Toggles post-processing on/off for debugging\r\n * @param enabled - Whether to enable post-processing\r\n */\r\n public setPostProcessingEnabled(enabled: boolean): void {\r\n this.options.enablePostProcessing = enabled;\r\n }\r\n\r\n /**\r\n * Gets the current distortion intensity\r\n */\r\n public get distortionIntensity(): number {\r\n return this.postProcessShader?.distortionIntensity ?? 0;\r\n }\r\n\r\n /**\r\n * Sets the distortion intensity (0 = no distortion)\r\n */\r\n public set distortionIntensity(value: number) {\r\n if (this.postProcessShader) {\r\n this.postProcessShader.distortionIntensity = value;\r\n }\r\n }\r\n\r\n /**\r\n * Gets the current vignette offset\r\n */\r\n public get vignetteOffset(): number {\r\n return this.postProcessShader?.vignetteOffset ?? 0.8;\r\n }\r\n\r\n /**\r\n * Sets the vignette offset (0.0 = center, 1.0 = edges)\r\n */\r\n public set vignetteOffset(value: number) {\r\n if (this.postProcessShader) {\r\n this.postProcessShader.vignetteOffset = value;\r\n }\r\n }\r\n\r\n /**\r\n * Gets the current vignette darkness\r\n */\r\n public get vignetteDarkness(): number {\r\n return this.postProcessShader?.vignetteDarkness ?? 1.0;\r\n }\r\n\r\n /**\r\n * Sets the vignette darkness (should be > vignetteOffset)\r\n */\r\n public set vignetteDarkness(value: number) {\r\n if (this.postProcessShader) {\r\n this.postProcessShader.vignetteDarkness = value;\r\n }\r\n }\r\n\r\n /**\r\n * Completely disposes of the infinite grid and cleans up all resources\r\n *\r\n * This method now uses the DisposalManager for systematic cleanup to prevent memory leaks.\r\n * The disposal manager handles:\r\n * - Cancels the animation frame loop\r\n * - Removes all event listeners\r\n * - Disposes all OGL geometries, materials, and textures\r\n * - Clears all data structures and maps\r\n * - Removes the canvas from the DOM\r\n *\r\n * Call this method when the grid is no longer needed, such as when\r\n * navigating away from the page or unmounting the component.\r\n *\r\n * @example\r\n * ```typescript\r\n * // In a Vue component's onBeforeUnmount or React's useEffect cleanup\r\n * onBeforeUnmount(() => {\r\n * if (gridInstance) {\r\n * gridInstance.dispose();\r\n * gridInstance = null;\r\n * }\r\n * });\r\n * ```\r\n */\r\n public dispose(): void {\r\n this.disposalManager.dispose();\r\n }\r\n\r\n /**\r\n * Gets the event handler instance for advanced event management\r\n * @returns The EventHandler instance or undefined if not initialized\r\n */\r\n public getEventHandler(): EventHandler | undefined {\r\n return this.eventHandler;\r\n }\r\n\r\n /**\r\n * Gets the grid manager instance for advanced grid management\r\n * @returns The GridManager instance\r\n */\r\n public getGridManager(): GridManager {\r\n return this.gridManager;\r\n }\r\n\r\n /**\r\n * Gets the disposal manager instance for advanced cleanup control\r\n * @returns The DisposalManager instance\r\n */\r\n public getDisposalManager(): DisposalManager {\r\n return this.disposalManager;\r\n }\r\n\r\n /**\r\n * Updates card data and regenerates the grid\r\n * @param newCardData - The new card data to display\r\n * @returns Promise that resolves when update is complete\r\n */\r\n public async updateCardData(newCardData: CardData[]): Promise<void> {\r\n this.cardData = newCardData;\r\n await this.gridManager.updateCardData(newCardData);\r\n }\r\n\r\n /**\r\n * Gets statistics about the current grid\r\n * @returns Object containing grid statistics\r\n */\r\n public getGridStats() {\r\n return this.gridManager.getGridStats();\r\n }\r\n\r\n /**\r\n * Validates that all resources have been properly disposed\r\n * Useful for debugging memory leaks\r\n * @returns True if disposal was successful, false if issues were found\r\n */\r\n public validateDisposal(): boolean {\r\n return this.disposalManager.validateDisposal();\r\n }\r\n\r\n /**\r\n * Performs a partial cleanup that preserves the core structure\r\n * but clears dynamic content. Useful for reinitialization scenarios.\r\n */\r\n public partialCleanup(): void {\r\n this.disposalManager.partialCleanup();\r\n }\r\n}\r\n"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"path": "PostProcessShader.ts",
|
|
34
|
+
"content": "import {\r\n Program,\r\n RenderTarget,\r\n Mesh,\r\n Plane,\r\n Vec2,\r\n Renderer,\r\n Transform,\r\n Camera,\r\n Texture,\r\n} from \"ogl\";\r\nimport { gsap } from \"gsap\";\r\n\r\n// Import shaders as raw strings\r\nimport { postProcessVertexShader, postProcessFragmentShader } from \"./shaders\";\r\n\r\n// OGL context type\r\ntype OGLContext = WebGL2RenderingContext & { renderer: Renderer; canvas: HTMLCanvasElement };\r\n\r\n/**\r\n * @interface CustomPostProcessShaderParameters\r\n * @description Defines the initial parameters for the CustomPostProcessShader.\r\n * @property {number} [distortionIntensity] - Initial intensity of the distortion effect. Defaults to 0.\r\n * @property {number} [vignetteOffset] - Initial offset for the vignette effect (0.0 = center, 1.0 = edges). Defaults to 0.8.\r\n * @property {number} [vignetteDarkness] - Controls vignette transition smoothness (should be > vignetteOffset). Defaults to 1.0.\r\n */\r\ninterface CustomPostProcessShaderParameters {\r\n distortionIntensity?: number;\r\n vignetteOffset?: number;\r\n vignetteDarkness?: number;\r\n}\r\n\r\n/**\r\n * @class CustomPostProcessShader\r\n * @description A custom OGL-based post-processing shader for distortion and vignette effects.\r\n * It provides animatable properties for these effects via GSAP.\r\n */\r\nexport class CustomPostProcessShader {\r\n private gl: OGLContext;\r\n private program: Program | null;\r\n private renderTarget: RenderTarget | null;\r\n private mesh: Mesh | null;\r\n private geometry: Plane | null;\r\n private scene: Transform | null;\r\n private camera: Camera | null;\r\n\r\n /**\r\n * @private\r\n * @property {number} _distortionIntensity - Internal property for distortion intensity, animated by GSAP.\r\n */\r\n private _distortionIntensity: number = 0;\r\n\r\n /**\r\n * @private\r\n * @property {number} _vignetteOffset - Internal property for vignette offset (0.0-1.0 range), animated by GSAP.\r\n */\r\n private _vignetteOffset: number = 0.8;\r\n\r\n /**\r\n * @private\r\n * @property {number} _vignetteDarkness - Internal property for vignette transition end (should be > _vignetteOffset), animated by GSAP.\r\n */\r\n private _vignetteDarkness: number = 1.0;\r\n\r\n /**\r\n * Creates an instance of CustomPostProcessShader.\r\n * @param {OGLContext} gl - The OGL WebGL context\r\n * @param {CustomPostProcessShaderParameters} [initialParams={}] - Optional initial parameters for the shader effects.\r\n */\r\n constructor(gl: OGLContext, initialParams: CustomPostProcessShaderParameters = {}) {\r\n this.gl = gl;\r\n\r\n // Initialize internal properties from constructor parameters\r\n this._distortionIntensity = initialParams.distortionIntensity ?? 0;\r\n this._vignetteOffset = initialParams.vignetteOffset ?? 1.2;\r\n this._vignetteDarkness = initialParams.vignetteDarkness ?? 1.5;\r\n\r\n // Create render target\r\n this.renderTarget = new RenderTarget(gl, {\r\n width: gl.canvas.width,\r\n height: gl.canvas.height,\r\n });\r\n\r\n // Create geometry for full-screen quad\r\n this.geometry = new Plane(gl, {\r\n width: 2,\r\n height: 2,\r\n });\r\n\r\n // Create shader program\r\n this.program = new Program(gl, {\r\n vertex: postProcessVertexShader,\r\n fragment: postProcessFragmentShader,\r\n uniforms: {\r\n tDiffuse: { value: null }, // The input texture from the previous pass\r\n distortion: { value: new Vec2(0, 0) }, // This will be calculated from _distortionIntensity\r\n vignetteOffset: { value: this._vignetteOffset },\r\n vignetteDarkness: { value: this._vignetteDarkness },\r\n },\r\n transparent: false,\r\n cullFace: false,\r\n });\r\n\r\n // Create mesh\r\n this.mesh = new Mesh(gl, {\r\n geometry: this.geometry,\r\n program: this.program,\r\n });\r\n\r\n // Create a scene for the post-processing mesh\r\n this.scene = new Transform();\r\n this.mesh.setParent(this.scene);\r\n\r\n // Create an orthographic camera for post-processing\r\n this.camera = new Camera(gl, {\r\n left: -1,\r\n right: 1,\r\n bottom: -1,\r\n top: 1,\r\n near: 0,\r\n far: 2,\r\n });\r\n this.camera.position.set(0, 0, 1);\r\n\r\n // Immediately update uniforms based on initial values\r\n this.updateUniforms();\r\n }\r\n\r\n /**\r\n * @property {number} distortionIntensity - Getter/Setter for the distortion intensity.\r\n * When set, it updates the shader uniforms.\r\n */\r\n get distortionIntensity(): number {\r\n return this._distortionIntensity;\r\n }\r\n\r\n set distortionIntensity(value: number) {\r\n this._distortionIntensity = value;\r\n this.updateUniforms();\r\n }\r\n\r\n /**\r\n * @property {number} vignetteOffset - Getter/Setter for the vignette offset.\r\n * Controls where the vignette effect starts (0.0 = center, 1.0 = screen edges).\r\n * When set, it updates the shader uniforms.\r\n */\r\n get vignetteOffset(): number {\r\n return this._vignetteOffset;\r\n }\r\n\r\n set vignetteOffset(value: number) {\r\n this._vignetteOffset = value;\r\n this.updateUniforms();\r\n }\r\n\r\n /**\r\n * @property {number} vignetteDarkness - Getter/Setter for the vignette transition end.\r\n * Controls where the vignette reaches maximum darkness (should be > vignetteOffset for smooth transition).\r\n * When set, it updates the shader uniforms.\r\n */\r\n get vignetteDarkness(): number {\r\n return this._vignetteDarkness;\r\n }\r\n\r\n set vignetteDarkness(value: number) {\r\n this._vignetteDarkness = value;\r\n this.updateUniforms();\r\n }\r\n\r\n /**\r\n * @method updateUniforms\r\n * @description Updates all relevant uniforms of the shader material based on the current\r\n * internal `_distortionIntensity`, `_vignetteOffset`, and `_vignetteDarkness` properties.\r\n * This method should be called whenever the internal properties change to reflect\r\n * the changes in the shader.\r\n */\r\n updateUniforms(): void {\r\n if (!this.program) return;\r\n\r\n // Calculate distortion uniform based on current distortionIntensity and aspect ratio.\r\n // We'll use the current window aspect ratio, which is common for full-screen effects.\r\n const aspectRatio = window.innerWidth / window.innerHeight;\r\n // The distortion uniform is a vec2. We'll apply the intensity scaled by aspect ratio\r\n // to the x component and directly to the y component, allowing for non-uniform scaling\r\n // if the shader utilizes both.\r\n // Assuming the shader's `distortion.x` is the primary scalar for the effect,\r\n // and `distortion.y` can be used for secondary axis scaling or ignored.\r\n this.program.uniforms.distortion.value.set(\r\n this._distortionIntensity * aspectRatio,\r\n this._distortionIntensity,\r\n );\r\n\r\n this.program.uniforms.vignetteOffset.value = this._vignetteOffset;\r\n this.program.uniforms.vignetteDarkness.value = this._vignetteDarkness;\r\n }\r\n\r\n /**\r\n * @method animate\r\n * @description Animates the distortion and vignette parameters using GSAP.\r\n * @param {number} targetDistortion - The target distortion intensity value.\r\n * @param {number} targetVignetteOffset - The target vignette offset value.\r\n * @param {number} targetVignetteDarkness - The target vignette darkness value.\r\n * @param {number} [duration=1] - The duration of the animation in seconds.\r\n * @param {number} [delay=0] - The delay before the animation starts in seconds.\r\n * @param {string} [ease='power2.out'] - The GSAP ease function to use for the animation.\r\n */\r\n animate(\r\n targetDistortion: number,\r\n targetVignetteOffset: number,\r\n targetVignetteDarkness: number,\r\n duration: number = 1,\r\n delay: number = 0,\r\n ease: string = \"power2.out\",\r\n ): void {\r\n gsap.to(this, {\r\n distortionIntensity: targetDistortion,\r\n vignetteOffset: targetVignetteOffset,\r\n vignetteDarkness: targetVignetteDarkness,\r\n duration: duration,\r\n delay: delay,\r\n ease: ease,\r\n onUpdate: () => this.updateUniforms(), // Ensure uniforms are updated during animation\r\n });\r\n }\r\n\r\n /**\r\n * @method setInputTexture\r\n * @description Sets the input texture for post-processing\r\n * @param {any} texture - The input texture (usually from a RenderTarget)\r\n */\r\n setInputTexture(texture: Texture): void {\r\n if (!this.program) return;\r\n this.program.uniforms.tDiffuse.value = texture;\r\n }\r\n\r\n /**\r\n * @method render\r\n * @description Renders the post-processing effect to the specified target or screen\r\n * @param {RenderTarget | null} target - The render target (null for screen)\r\n */\r\n render(target: RenderTarget | null = null): void {\r\n if (!this.scene || !this.camera) return;\r\n\r\n // Use OGL's renderer to render the post-processing scene\r\n const renderer = this.gl.renderer;\r\n if (target) {\r\n renderer.render({ scene: this.scene, camera: this.camera, target });\r\n } else {\r\n renderer.render({ scene: this.scene, camera: this.camera });\r\n }\r\n }\r\n\r\n /**\r\n * @method resize\r\n * @description Resizes the render target when the canvas size changes\r\n * @param {number} width - New width\r\n * @param {number} height - New height\r\n */\r\n resize(width: number, height: number): void {\r\n if (!this.renderTarget) return;\r\n this.renderTarget.setSize(width, height);\r\n this.updateUniforms();\r\n }\r\n\r\n /**\r\n * @method dispose\r\n * @description Cleans up WebGL resources\r\n */\r\n dispose(): void {\r\n // OGL resources are automatically cleaned up by the WebGL context\r\n // We just need to release references\r\n this.renderTarget = null;\r\n this.geometry = null;\r\n this.program = null;\r\n this.mesh = null;\r\n this.scene = null;\r\n this.camera = null;\r\n }\r\n}\r\n"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"path": "shaders.ts",
|
|
38
|
+
"content": "export const gaussianBlurFragmentShader = /* glsl */ `precision highp float;\r\n\r\nuniform sampler2D map;\r\n// uniform float blurAmount; // Removed: Blur amount is now fixed\r\nuniform vec2 resolution;\r\nuniform float uOpacity; // New uniform for controlling opacity from JS\r\n\r\nvarying vec2 vUv;\r\n\r\nvoid main() {\r\n vec2 onePixel = vec2(1.0, 1.0) / resolution;\r\n\r\n // Fixed blur amount directly in the shader\r\n float fixedBlurAmount = 10.0; // Changed to a fixed value as requested\r\n\r\n vec4 sum = vec4(0.0);\r\n\r\n sum += texture2D(map, vUv + onePixel * vec2(-1.0, -1.0) * fixedBlurAmount) * 0.0625;\r\n sum += texture2D(map, vUv + onePixel * vec2( 0.0, -1.0) * fixedBlurAmount) * 0.125;\r\n sum += texture2D(map, vUv + onePixel * vec2( 1.0, -1.0) * fixedBlurAmount) * 0.0625;\r\n sum += texture2D(map, vUv + onePixel * vec2(-1.0, 0.0) * fixedBlurAmount) * 0.125;\r\n sum += texture2D(map, vUv + onePixel * vec2( 0.0, 0.0) * fixedBlurAmount) * 0.25;\r\n sum += texture2D(map, vUv + onePixel * vec2( 1.0, 0.0) * fixedBlurAmount) * 0.125;\r\n sum += texture2D(map, vUv + onePixel * vec2(-1.0, 1.0) * fixedBlurAmount) * 0.0625;\r\n sum += texture2D(map, vUv + onePixel * vec2( 0.0, 1.0) * fixedBlurAmount) * 0.125;\r\n sum += texture2D(map, vUv + onePixel * vec2( 1.0, 1.0) * fixedBlurAmount) * 0.0625;\r\n\r\n // Apply the opacity uniform to the alpha channel of the final color\r\n gl_FragColor = vec4(sum.rgb, sum.a * uOpacity); // sum.a is usually 1.0 from texture, so multiply by uOpacity\r\n}`;\r\n\r\nexport const gaussianBlurVertexShader = `\r\n attribute vec2 uv;\r\n attribute vec3 position;\r\n \r\n uniform mat4 modelViewMatrix;\r\n uniform mat4 projectionMatrix;\r\n \r\n varying vec2 vUv;\r\n \r\n void main() {\r\n // Flip UV coordinates 180 degrees (both X and Y)\r\n vUv = vec2(uv.x, 1.0 - uv.y);\r\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\r\n }\r\n`;\r\n\r\nexport const postProcessFragmentShader = /* glsl */ `precision highp float;\r\n\r\nuniform sampler2D tDiffuse;\r\nuniform vec2 distortion;\r\nuniform float vignetteOffset;\r\nuniform float vignetteDarkness;\r\n\r\nvarying vec2 vUv;\r\n\r\nvoid main() {\r\n vec2 shiftedUv = 2.0 * (vUv - 0.5);\r\n float distanceToCenter = length(shiftedUv);\r\n\r\n // Lens distortion effect\r\n // NOTE: The original shader had 'distortion * dot(shiftedUv)'.\r\n // If distortion is a vec2, dot product will result in a scalar.\r\n // If you intend distortion to scale both X and Y independently based on distance,\r\n // you might need something like: shiftedUv *= (0.88 + distortion.x * abs(shiftedUv.x) + distortion.y * abs(shiftedUv.y));\r\n // For now, I'll keep the dot product as it was in your provided shader,\r\n // which applies uniform distortion based on radial distance.\r\n shiftedUv *= (0.88 + distortion.x * dot(shiftedUv, shiftedUv)); // Assuming distortion.x controls the scalar distortion factor\r\n vec2 transformedUv = shiftedUv * 0.5 + 0.5;\r\n\r\n // Vignette effect\r\n // Corrected 'vignetteOffset * 0.799' and '(vignetteDarkness + vignetteOffset)' if that was the intent.\r\n // The second parameter to smoothstep is the \"edge\" where it starts.\r\n // I'll interpret your intent as scaling the effect based on a combined factor.\r\n float vignetteIntensity = smoothstep(vignetteOffset, vignetteDarkness, distanceToCenter); // Simplified as common vignette\r\n // If your original intention for vignette was 'smoothstep(0.8, vignetteOffset * 0.799, (vignetteDarkness + vignetteOffset) * distanceToCenter);'\r\n // this would be a more complex interaction. Let's start with a standard vignette.\r\n\r\n // Sample render texture and output fragment\r\n vec3 color = texture2D(tDiffuse, transformedUv).rgb * (1.0 - vignetteIntensity); // Apply darkening based on intensity\r\n // The original '* vignetteIntensity' would brighten. Vignettes usually darken.\r\n // If you want a more subtle darkening, adjust the '(1.0 - vignetteIntensity)''.\r\n gl_FragColor = vec4(color, 1.);\r\n}`;\r\n\r\nexport const postProcessVertexShader = /* glsl */ `\r\nattribute vec2 uv;\r\nattribute vec3 position;\r\n\r\nuniform mat4 modelViewMatrix;\r\nuniform mat4 projectionMatrix;\r\n\r\nvarying vec2 vUv;\r\n\r\nvoid main() {\r\n vUv = uv;\r\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\r\n}`;\r\n"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"path": "types.ts",
|
|
42
|
+
"content": "/**\r\n * Type definitions for the Infinite Grid system\r\n */\r\n\r\nimport { Texture, Vec3 } from \"ogl\";\r\n\r\n/**\r\n * Represents the data structure for a single card/tile in the grid\r\n */\r\nexport interface CardData {\r\n /** The main title text displayed on the card */\r\n title: string;\r\n /** Badge text (currently not implemented in rendering) */\r\n badge: string;\r\n /** Detailed description text (optional) */\r\n description?: string;\r\n /** Array of tag strings displayed as pills at the bottom */\r\n tags: string[];\r\n /** Date string displayed in the bottom-right corner */\r\n date: string;\r\n /** Optional image URL for the card's main image */\r\n image?: string;\r\n}\r\n\r\n/**\r\n * Configuration parameters for post-processing visual effects\r\n */\r\nexport interface PostProcessParams {\r\n /** Intensity of the barrel/pincushion distortion effect (0 = no distortion) */\r\n distortionIntensity?: number;\r\n /** Offset value for the vignette effect (higher = smaller dark area) */\r\n vignetteOffset?: number;\r\n /** Darkness intensity of the vignette effect (higher = darker edges) */\r\n vignetteDarkness?: number;\r\n}\r\n\r\n/**\r\n * Configuration options for initializing the infinite grid\r\n */\r\nexport interface InfiniteGridOptions {\r\n /** Number of columns in each grid section (default: 3) */\r\n gridCols?: number;\r\n /** Number of rows in each grid section (default: 3) */\r\n gridRows?: number;\r\n /** Gap between tiles in Three.js units (default: 0) */\r\n gridGap?: number;\r\n /** Size of each tile in Three.js units (default: 3) */\r\n tileSize?: number;\r\n /** Base Z position of the camera (default: 10) */\r\n baseCameraZ?: number;\r\n /** Whether to enable post-processing effects (default: true) */\r\n enablePostProcessing: boolean;\r\n /** Parameters for post-processing effects */\r\n postProcessParams: PostProcessParams;\r\n}\r\n\r\n/**\r\n * Simple 2D coordinate structure\r\n */\r\nexport interface Position2D {\r\n /** X coordinate */\r\n x: number;\r\n /** Y coordinate */\r\n y: number;\r\n}\r\n\r\n/**\r\n * Tracks the current scroll state and behavior\r\n */\r\nexport interface ScrollState {\r\n /** Scaling factor for scroll sensitivity */\r\n scale: number;\r\n /** Current scroll position */\r\n current: Position2D;\r\n /** Previous scroll position for delta calculations */\r\n last: Position2D;\r\n}\r\n\r\n/**\r\n * Data structure for each group of tiles (3x3 groups create infinite effect)\r\n */\r\nexport interface TileGroupData {\r\n /** Base 3D position of the group in world space */\r\n basePos: Vec3;\r\n /** Additional offset for infinite scrolling wrapping */\r\n offset: Position2D;\r\n}\r\n\r\n/**\r\n * User data attached to each tile mesh for identification\r\n */\r\nexport interface TileUserData {\r\n /** Index of the tile group this tile belongs to */\r\n groupIndex: number;\r\n /** Index of the tile within its group */\r\n tileIndex: number;\r\n /** Unique string identifier for the tile */\r\n tileKey: string;\r\n}\r\n\r\n/**\r\n * Event detail passed when a tile is clicked\r\n */\r\nexport interface TileClickEventDetail {\r\n /** Index of the clicked tile's group */\r\n groupIndex: number;\r\n /** Index of the clicked tile within its group */\r\n tileIndex: number;\r\n /** The card data associated with the clicked tile */\r\n cardData: CardData;\r\n}\r\n\r\n/**\r\n * Pair of textures for each card (foreground content + background blur)\r\n */\r\nexport interface CardTexturePair {\r\n /** Canvas texture containing the card's main content */\r\n foreground: Texture | null;\r\n /** Canvas texture containing the blurred background image */\r\n background: Texture | null;\r\n}\r\n\r\n/**\r\n * Camera viewport dimensions in world space\r\n */\r\nexport interface Viewport {\r\n /** Width of the viewport in world units */\r\n width: number;\r\n /** Height of the viewport in world units */\r\n height: number;\r\n}\r\n\r\n// Custom event types\r\ndeclare global {\r\n interface HTMLElementEventMap {\r\n tileClicked: CustomEvent<TileClickEventDetail>;\r\n }\r\n}\r\n"
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"fileCount": 9,
|
|
46
|
+
"contentHash": "00d9b632f4bb16fe9bd515dca9223fc2eb6a0932"
|
|
47
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "input",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"@vueuse/core"
|
|
5
|
+
],
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "IInput.vue",
|
|
9
|
+
"content": "<!-- Uses base code from shadcn-vue Input component and extends it's functionality-->\r\n<template>\r\n <div\r\n ref=\"inputContainerRef\"\r\n :class=\"cn('group/input rounded-lg p-[2px] transition duration-300', props.containerClass)\"\r\n :style=\"{\r\n background: containerBg,\r\n }\"\r\n @mouseenter=\"() => (visible = true)\"\r\n @mouseleave=\"() => (visible = false)\"\r\n @mousemove=\"handleMouseMove\"\r\n >\r\n <input\r\n v-bind=\"$attrs\"\r\n v-model=\"modelValue\"\r\n :class=\"\r\n cn(\r\n `flex h-10 w-full border-none bg-gray-50 dark:bg-zinc-800 text-black dark:text-white shadow-input rounded-md px-3 py-2 text-sm file:border-0 file:bg-transparent \r\n file:text-sm file:font-medium placeholder:text-neutral-400 dark:placeholder-text-neutral-600 \r\n focus-visible:outline-none focus-visible:ring-[2px] focus-visible:ring-neutral-400 dark:focus-visible:ring-neutral-600\r\n disabled:cursor-not-allowed disabled:opacity-50\r\n dark:shadow-[0px_0px_1px_1px_var(--neutral-700)]\r\n group-hover/input:shadow-none transition duration-400`,\r\n props.class,\r\n )\r\n \"\r\n />\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport type { HTMLAttributes } from \"vue\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { useVModel } from \"@vueuse/core\";\r\nimport { ref, computed } from \"vue\";\r\n\r\ndefineOptions({\r\n inheritAttrs: false,\r\n});\r\n\r\nconst props = defineProps<{\r\n defaultValue?: string | number;\r\n modelValue?: string | number;\r\n class?: HTMLAttributes[\"class\"];\r\n containerClass?: HTMLAttributes[\"class\"];\r\n}>();\r\n\r\nconst emits = defineEmits<{\r\n (e: \"update:modelValue\", payload: string | number): void;\r\n}>();\r\n\r\nconst modelValue = useVModel(props, \"modelValue\", emits, {\r\n passive: true,\r\n defaultValue: props.defaultValue,\r\n});\r\n\r\nconst inputContainerRef = ref<HTMLDivElement | null>(null);\r\nconst mouse = ref<{ x: number; y: number }>({ x: 0, y: 0 });\r\nconst radius = 100;\r\nconst visible = ref(false);\r\n\r\nconst containerBg = computed(() => {\r\n return `\r\n radial-gradient(\r\n ${visible.value ? radius + \"px\" : \"0px\"} circle at ${mouse.value.x}px ${mouse.value.y}px,\r\n var(--blue-500),\r\n transparent 80%\r\n )\r\n `;\r\n});\r\n\r\nfunction handleMouseMove({ clientX, clientY }: MouseEvent) {\r\n if (!inputContainerRef.value) return;\r\n\r\n const { left, top } = inputContainerRef.value.getBoundingClientRect();\r\n mouse.value = { x: clientX - left, y: clientY - top };\r\n}\r\n</script>\r\n\r\n<style scoped>\r\ninput {\r\n box-shadow:\r\n 0px 2px 3px -1px rgba(0, 0, 0, 0.1),\r\n 0px 1px 0px 0px rgba(25, 28, 33, 0.02),\r\n 0px 0px 0px 1px rgba(25, 28, 33, 0.08);\r\n}\r\n</style>\r\n"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"path": "index.ts",
|
|
13
|
+
"content": "export { default as IInput } from \"./IInput.vue\";\r\n"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"fileCount": 2,
|
|
17
|
+
"contentHash": "1b1c8c430b978c59c8acf405ee63bc903b2aae09"
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "interactive-grid-pattern",
|
|
3
|
+
"dependencies": [],
|
|
4
|
+
"files": [
|
|
5
|
+
{
|
|
6
|
+
"path": "index.ts",
|
|
7
|
+
"content": "export { default as InteractiveGridPattern } from \"./InteractiveGridPattern.vue\";\r\n"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"path": "InteractiveGridPattern.vue",
|
|
11
|
+
"content": "<template>\r\n <svg\r\n :width=\"gridWidth\"\r\n :height=\"gridHeight\"\r\n :class=\"svgClass\"\r\n >\r\n <rect\r\n v-for=\"(_, index) in totalSquares\"\r\n :key=\"index\"\r\n :x=\"getX(index)\"\r\n :y=\"getY(index)\"\r\n :width=\"width\"\r\n :height=\"height\"\r\n :class=\"getRectClass(index)\"\r\n @mouseenter=\"handleMouseEnter(index)\"\r\n @mouseleave=\"handleMouseLeave\"\r\n />\r\n </svg>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { cn } from \"@/lib/utils\";\r\nimport { ref, computed, type HTMLAttributes } from \"vue\";\r\n\r\ninterface InteractiveGridPatternProps {\r\n className?: HTMLAttributes[\"class\"];\r\n squaresClassName?: HTMLAttributes[\"class\"];\r\n width?: number;\r\n height?: number;\r\n squares?: [number, number];\r\n}\r\n\r\nconst props = withDefaults(defineProps<InteractiveGridPatternProps>(), {\r\n width: 40,\r\n height: 40,\r\n squares: () => [24, 24],\r\n});\r\n\r\nconst horizontal = computed(() => props.squares[0]);\r\nconst vertical = computed(() => props.squares[1]);\r\n\r\nconst totalSquares = computed(() => horizontal.value * vertical.value);\r\n\r\nconst hoveredSquare = ref<number | null>(null);\r\n\r\nconst gridWidth = computed(() => props.width * horizontal.value);\r\nconst gridHeight = computed(() => props.height * vertical.value);\r\n\r\nfunction getX(index: number) {\r\n return (index % horizontal.value) * props.width;\r\n}\r\n\r\nfunction getY(index: number) {\r\n return Math.floor(index / horizontal.value) * props.height;\r\n}\r\n\r\nconst svgClass = computed(() =>\r\n cn(\"absolute inset-0 h-full w-full border border-gray-400/30\", props.className),\r\n);\r\n\r\nfunction getRectClass(index: number) {\r\n return cn(\r\n \"stroke-gray-400/30 transition-all duration-100 ease-in-out [&:not(:hover)]:duration-1000\",\r\n hoveredSquare.value === index ? \"fill-gray-300/30\" : \"fill-transparent\",\r\n props.squaresClassName,\r\n );\r\n}\r\n\r\nfunction handleMouseEnter(index: number) {\r\n hoveredSquare.value = index;\r\n}\r\n\r\nfunction handleMouseLeave() {\r\n hoveredSquare.value = null;\r\n}\r\n</script>\r\n"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"fileCount": 2,
|
|
15
|
+
"contentHash": "dd52fd46d8f7fcde503f7a7d1df7d46f12532fa3"
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "interactive-hover-button",
|
|
3
|
+
"dependencies": [],
|
|
4
|
+
"files": [
|
|
5
|
+
{
|
|
6
|
+
"path": "index.ts",
|
|
7
|
+
"content": "export { default as InteractiveHoverButton } from \"./InteractiveHoverButton.vue\";\r\n"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"path": "InteractiveHoverButton.vue",
|
|
11
|
+
"content": "<template>\r\n <button\r\n ref=\"buttonRef\"\r\n :class=\"\r\n cn(\r\n 'group relative w-auto cursor-pointer overflow-hidden rounded-full border bg-background p-2 px-6 text-center font-semibold',\r\n props.class,\r\n )\r\n \"\r\n >\r\n <div class=\"flex items-center gap-2\">\r\n <div\r\n class=\"size-2 scale-100 rounded-lg bg-primary transition-all duration-300 group-hover:scale-[100.8]\"\r\n ></div>\r\n <span\r\n class=\"inline-block whitespace-nowrap transition-all duration-300 group-hover:translate-x-12 group-hover:opacity-0\"\r\n >\r\n {{ text }}\r\n </span>\r\n </div>\r\n\r\n <div\r\n class=\"absolute top-0 z-10 flex size-full translate-x-12 items-center justify-center gap-2 text-primary-foreground opacity-0 transition-all duration-300 group-hover:-translate-x-5 group-hover:opacity-100\"\r\n >\r\n <span class=\"whitespace-nowrap\">{{ text }}</span>\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"24\"\r\n height=\"24\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n class=\"lucide lucide-arrow-right\"\r\n >\r\n <path d=\"M5 12h14\" />\r\n <path d=\"m12 5 7 7-7 7\" />\r\n </svg>\r\n </div>\r\n </button>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { cn } from \"@/lib/utils\";\r\nimport { ref } from \"vue\";\r\n\r\ninterface Props {\r\n text?: string;\r\n class?: string;\r\n}\r\nconst props = withDefaults(defineProps<Props>(), {\r\n text: \"Button\",\r\n});\r\n\r\nconst buttonRef = ref<HTMLButtonElement>();\r\n</script>\r\n\r\n<style></style>\r\n"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"fileCount": 2,
|
|
15
|
+
"contentHash": "a9ea908d3a459e8989019e7dac0b3d2d6a5c988f"
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "iphone-mockup",
|
|
3
|
+
"dependencies": [],
|
|
4
|
+
"files": [
|
|
5
|
+
{
|
|
6
|
+
"path": "index.ts",
|
|
7
|
+
"content": "export { default as IPhone15ProMockup } from \"./iPhone15ProMockup.vue\";\r\n"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"path": "iPhone15ProMockup.vue",
|
|
11
|
+
"content": "<!-- eslint-disable check-file/filename-naming-convention -->\r\n<template>\r\n <svg\r\n fill=\"none\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n :width=\"width\"\r\n :height=\"height\"\r\n :viewBox=\"`0 0 ${width} ${height}`\"\r\n >\r\n <path\r\n d=\"M2 73C2 32.6832 34.6832 0 75 0H357C397.317 0 430 32.6832 430 73V809C430 849.317 397.317 882 357 882H75C34.6832 882 2 849.317 2 809V73Z\"\r\n class=\"fill-[#E5E5E5] dark:fill-[#404040]\"\r\n ></path>\r\n <path\r\n d=\"M0 171C0 170.448 0.447715 170 1 170H3V204H1C0.447715 204 0 203.552 0 203V171Z\"\r\n class=\"fill-[#E5E5E5] dark:fill-[#404040]\"\r\n ></path>\r\n <path\r\n d=\"M1 234C1 233.448 1.44772 233 2 233H3.5V300H2C1.44772 300 1 299.552 1 299V234Z\"\r\n class=\"fill-[#E5E5E5] dark:fill-[#404040]\"\r\n ></path>\r\n <path\r\n d=\"M1 319C1 318.448 1.44772 318 2 318H3.5V385H2C1.44772 385 1 384.552 1 384V319Z\"\r\n class=\"fill-[#E5E5E5] dark:fill-[#404040]\"\r\n ></path>\r\n <path\r\n d=\"M430 279H432C432.552 279 433 279.448 433 280V384C433 384.552 432.552 385 432 385H430V279Z\"\r\n class=\"fill-[#E5E5E5] dark:fill-[#404040]\"\r\n ></path>\r\n <path\r\n d=\"M6 74C6 35.3401 37.3401 4 76 4H356C394.66 4 426 35.3401 426 74V808C426 846.66 394.66 878 356 878H76C37.3401 878 6 846.66 6 808V74Z\"\r\n class=\"fill-white dark:fill-[#262626]\"\r\n ></path>\r\n <path\r\n opacity=\"0.5\"\r\n d=\"M174 5H258V5.5C258 6.60457 257.105 7.5 256 7.5H176C174.895 7.5 174 6.60457 174 5.5V5Z\"\r\n class=\"fill-[#E5E5E5] dark:fill-[#404040]\"\r\n ></path>\r\n <path\r\n d=\"M21.25 75C21.25 44.2101 46.2101 19.25 77 19.25H355C385.79 19.25 410.75 44.2101 410.75 75V807C410.75 837.79 385.79 862.75 355 862.75H77C46.2101 862.75 21.25 837.79 21.25 807V75Z\"\r\n class=\"fill-[#E5E5E5] stroke-[#E5E5E5] stroke-[0.5] dark:fill-[#404040] dark:stroke-[#404040]\"\r\n ></path>\r\n <image\r\n v-if=\"src\"\r\n x=\"21.25\"\r\n y=\"19.25\"\r\n width=\"389.5\"\r\n height=\"843.5\"\r\n preserveAspectRatio=\"xMidYMid slice\"\r\n style=\"clip-path: url(#roundedCorners)\"\r\n :href=\"src\"\r\n ></image>\r\n\r\n <path\r\n d=\"M154 48.5C154 38.2827 162.283 30 172.5 30H259.5C269.717 30 278 38.2827 278 48.5C278 58.7173 269.717 67 259.5 67H172.5C162.283 67 154 58.7173 154 48.5Z\"\r\n class=\"fill-[#F5F5F5] dark:fill-[#262626]\"\r\n ></path>\r\n <path\r\n d=\"M249 48.5C249 42.701 253.701 38 259.5 38C265.299 38 270 42.701 270 48.5C270 54.299 265.299 59 259.5 59C253.701 59 249 54.299 249 48.5Z\"\r\n class=\"fill-[#F5F5F5] dark:fill-[#262626]\"\r\n ></path>\r\n <path\r\n d=\"M254 48.5C254 45.4624 256.462 43 259.5 43C262.538 43 265 45.4624 265 48.5C265 51.5376 262.538 54 259.5 54C256.462 54 254 51.5376 254 48.5Z\"\r\n class=\"fill-[#E5E5E5] dark:fill-[#404040]\"\r\n ></path>\r\n <defs>\r\n <clipPath id=\"roundedCorners\">\r\n <rect\r\n x=\"21.25\"\r\n y=\"19.25\"\r\n width=\"389.5\"\r\n height=\"843.5\"\r\n rx=\"55.75\"\r\n ry=\"55.75\"\r\n ></rect>\r\n </clipPath>\r\n </defs>\r\n </svg>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\n// eslint-disable-next-line check-file/filename-naming-convention\r\ninterface Props {\r\n width?: number;\r\n height?: number;\r\n src?: string;\r\n}\r\n\r\nwithDefaults(defineProps<Props>(), {\r\n width: 433,\r\n height: 882,\r\n});\r\n</script>\r\n"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"fileCount": 2,
|
|
15
|
+
"contentHash": "39c553d96aabdf77010b9c91273d4ca3327423bb"
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lamp-effect",
|
|
3
|
+
"dependencies": [],
|
|
4
|
+
"files": [
|
|
5
|
+
{
|
|
6
|
+
"path": "index.ts",
|
|
7
|
+
"content": "export { default as LampEffect } from \"./LampEffect.vue\";\r\n"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"path": "LampEffect.vue",
|
|
11
|
+
"content": "<template>\r\n <div\r\n :class=\"\r\n cn(\r\n 'relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-slate-950 w-full rounded-md z-0',\r\n $props.class,\r\n )\r\n \"\r\n >\r\n <div class=\"relative isolate z-0 flex w-full flex-1 scale-y-125 items-center justify-center\">\r\n <!-- Conic Gradient -->\r\n <div\r\n :style=\"{\r\n backgroundImage: `conic-gradient(var(--conic-position), var(--tw-gradient-stops))`,\r\n }\"\r\n class=\"animate-conic-gradient bg-gradient-conic absolute inset-auto right-1/2 h-56 w-60 overflow-visible from-cyan-500 via-transparent to-transparent text-white opacity-50 [--conic-position:from_70deg_at_center_top]\"\r\n >\r\n <div\r\n class=\"absolute bottom-0 left-0 z-20 h-40 w-full bg-slate-950 [mask-image:linear-gradient(to_top,white,transparent)]\"\r\n />\r\n <div\r\n class=\"absolute bottom-0 left-0 z-20 h-full w-40 bg-slate-950 [mask-image:linear-gradient(to_right,white,transparent)]\"\r\n />\r\n </div>\r\n\r\n <div\r\n :style=\"{\r\n backgroundImage: `conic-gradient(var(--conic-position), var(--tw-gradient-stops))`,\r\n }\"\r\n class=\"animate-conic-gradient bg-gradient-conic absolute inset-auto left-1/2 h-56 w-60 from-transparent via-transparent to-cyan-500 text-white opacity-50 [--conic-position:from_290deg_at_center_top]\"\r\n >\r\n <div\r\n class=\"absolute bottom-0 right-0 z-20 h-full w-40 bg-slate-950 [mask-image:linear-gradient(to_left,white,transparent)]\"\r\n />\r\n <div\r\n class=\"absolute bottom-0 right-0 z-20 h-40 w-full bg-slate-950 [mask-image:linear-gradient(to_top,white,transparent)]\"\r\n />\r\n </div>\r\n\r\n <div\r\n class=\"absolute top-1/2 h-48 w-full translate-y-12 scale-x-150 bg-slate-950 blur-2xl\"\r\n ></div>\r\n\r\n <div\r\n class=\"absolute top-1/2 z-50 h-48 w-full bg-transparent opacity-10 backdrop-blur-md\"\r\n ></div>\r\n\r\n <div\r\n class=\"absolute inset-auto z-50 h-36 w-[28rem] -translate-y-1/2 rounded-full bg-cyan-500 opacity-50 blur-3xl\"\r\n ></div>\r\n\r\n <!-- Spotlight -->\r\n <div\r\n class=\"animate-spotlight absolute inset-auto z-30 h-36 w-32 -translate-y-24 rounded-full bg-cyan-400 blur-2xl\"\r\n ></div>\r\n\r\n <!-- Glowing Line -->\r\n <div\r\n class=\"animate-glowing-line absolute inset-auto z-50 h-0.5 w-60 -translate-y-28 bg-cyan-400\"\r\n ></div>\r\n\r\n <div class=\"absolute inset-auto z-40 h-44 w-full translate-y-[-12.5rem] bg-slate-950\"></div>\r\n </div>\r\n\r\n <div class=\"relative z-50 flex -translate-y-80 flex-col items-center px-5\">\r\n <slot />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed, type HTMLAttributes } from \"vue\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface LampEffectProps {\r\n delay?: number;\r\n duration?: number;\r\n class?: HTMLAttributes[\"class\"];\r\n}\r\n\r\nconst props = withDefaults(defineProps<LampEffectProps>(), {\r\n delay: 0.5,\r\n duration: 0.8,\r\n});\r\n\r\nconst durationInSeconds = computed(() => `${props.duration}s`);\r\nconst delayInSeconds = computed(() => `${props.delay}s`);\r\n</script>\r\n\r\n<style scoped>\r\n/* Spotlight Animation */\r\n.animate-spotlight {\r\n animation: spotlight-anim ease-in-out v-bind(durationInSeconds) forwards;\r\n animation-delay: v-bind(delayInSeconds);\r\n}\r\n\r\n/* Glowing Line Animation */\r\n.animate-glowing-line {\r\n animation: glowing-line-anim ease-in-out v-bind(durationInSeconds) forwards;\r\n animation-delay: v-bind(delayInSeconds);\r\n}\r\n\r\n/* Conic Gradient Animation */\r\n.animate-conic-gradient {\r\n animation: conic-gradient-anim ease-in-out v-bind(durationInSeconds) forwards;\r\n animation-delay: v-bind(delayInSeconds);\r\n}\r\n\r\n/* Keyframes for Spotlight */\r\n@keyframes spotlight-anim {\r\n from {\r\n width: 8rem;\r\n }\r\n to {\r\n width: 16rem;\r\n }\r\n}\r\n\r\n/* Keyframes for Glowing Line */\r\n@keyframes glowing-line-anim {\r\n from {\r\n width: 15rem;\r\n }\r\n to {\r\n width: 30rem;\r\n }\r\n}\r\n\r\n/* Keyframes for Conic Gradient */\r\n@keyframes conic-gradient-anim {\r\n from {\r\n opacity: 0.5;\r\n width: 15rem;\r\n }\r\n to {\r\n opacity: 1;\r\n width: 30rem;\r\n }\r\n}\r\n</style>\r\n"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"fileCount": 2,
|
|
15
|
+
"contentHash": "8633901bef5b912d8a2998d0480790871cb8c97c"
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lens",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"motion-v"
|
|
5
|
+
],
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "index.ts",
|
|
9
|
+
"content": "export { default as Lens } from \"./Lens.vue\";\r\n"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"path": "Lens.vue",
|
|
13
|
+
"content": "<template>\r\n <div\r\n ref=\"containerRef\"\r\n class=\"relative z-20 overflow-hidden rounded-lg\"\r\n @mouseenter=\"setIsHovering(true)\"\r\n @mouseleave=\"setIsHovering(false)\"\r\n @mousemove=\"handleMouseMove\"\r\n >\r\n <slot />\r\n\r\n <div v-if=\"props.isStatic || isHovering\">\r\n <Motion\r\n :initial=\"{ opacity: 0, scale: 0.58 }\"\r\n :animate=\"{\r\n opacity: 1,\r\n scale: 1,\r\n }\"\r\n :transition=\"{ duration: 0.3, ease: 'easeOut' }\"\r\n :leave=\"{ opacity: 0, scale: 0.8 }\"\r\n class=\"absolute inset-0 overflow-hidden\"\r\n :style=\"{\r\n maskImage: `radial-gradient(${maskPosition}, black 100%, transparent 100%)`,\r\n WebkitMaskImage: `radial-gradient(${maskPosition}, black 100%, transparent 100%)`,\r\n transformOrigin,\r\n }\"\r\n >\r\n <div\r\n class=\"absolute inset-0\"\r\n :style=\"{ transform: `scale(${props.zoomFactor})`, transformOrigin }\"\r\n >\r\n <slot />\r\n </div>\r\n </Motion>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { Motion } from \"motion-v\";\r\nimport { ref, computed, watchEffect } from \"vue\";\r\n\r\ninterface LensProps {\r\n zoomFactor?: number;\r\n lensSize?: number;\r\n position?: {\r\n x: number;\r\n y: number;\r\n };\r\n isStatic?: boolean;\r\n hovering?: boolean;\r\n}\r\n\r\nconst props = withDefaults(defineProps<LensProps>(), {\r\n zoomFactor: 1.5,\r\n lensSize: 170,\r\n isStatic: false,\r\n hovering: undefined,\r\n position: () => ({ x: 200, y: 150 }),\r\n});\r\n\r\nconst emit = defineEmits<{\r\n (e: \"hover-update\", value: boolean): void;\r\n}>();\r\n\r\nconst containerRef = ref<HTMLElement | null>(null);\r\nconst localIsHovering = ref(false);\r\nconst mousePosition = ref({ x: 100, y: 100 });\r\n\r\nconst isHovering = computed(() => props.hovering ?? localIsHovering.value);\r\n\r\nfunction setIsHovering(hover: boolean) {\r\n localIsHovering.value = hover;\r\n emit(\"hover-update\", hover);\r\n}\r\n\r\nfunction handleMouseMove(e: MouseEvent) {\r\n const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();\r\n mousePosition.value = {\r\n x: e.clientX - rect.left,\r\n y: e.clientY - rect.top,\r\n };\r\n}\r\n\r\nconst maskPosition = computed(() => {\r\n const pos = props.isStatic ? props.position : mousePosition.value;\r\n return `circle ${props.lensSize! / 2}px at ${pos.x}px ${pos.y}px`;\r\n});\r\n\r\nconst transformOrigin = computed(() => {\r\n const pos = props.isStatic ? props.position : mousePosition.value;\r\n return `${pos.x}px ${pos.y}px`;\r\n});\r\n\r\nwatchEffect(() => {\r\n setIsHovering(false);\r\n});\r\n</script>\r\n"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"fileCount": 2,
|
|
17
|
+
"contentHash": "4e808dc7e731a6c23bdb5d3357b3be67ddd78984"
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "letter-pullup",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"motion-v"
|
|
5
|
+
],
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "index.ts",
|
|
9
|
+
"content": "export { default as LetterPullup } from \"./LetterPullup.vue\";\r\n"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"path": "LetterPullup.vue",
|
|
13
|
+
"content": "<template>\r\n <div class=\"flex justify-center\">\r\n <div\r\n v-for=\"(letter, index) in letters\"\r\n :key=\"letter\"\r\n >\r\n <Motion\r\n as=\"h1\"\r\n :initial=\"pullupVariant.initial\"\r\n :animate=\"pullupVariant.animate\"\r\n :transition=\"{\r\n delay: index * (props.delay ? props.delay : 0.05),\r\n }\"\r\n :class=\"\r\n cn(\r\n 'font-display text-center text-4xl font-bold tracking-[-0.02em] text-black drop-shadow-sm md:text-4xl md:leading-[5rem]',\r\n props.class,\r\n )\r\n \"\r\n >\r\n <span v-if=\"letter === ' '\"> </span>\r\n <span v-else>{{ letter }}</span>\r\n </Motion>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { Motion } from \"motion-v\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface LetterPullupProps {\r\n class?: string;\r\n words: string;\r\n delay?: number;\r\n}\r\n\r\nconst props = defineProps<LetterPullupProps>();\r\n\r\nconst letters = props.words.split(\"\");\r\n\r\nconst pullupVariant = {\r\n initial: { y: 100, opacity: 0 },\r\n animate: {\r\n y: 0,\r\n opacity: 1,\r\n },\r\n};\r\n</script>\r\n"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"fileCount": 2,
|
|
17
|
+
"contentHash": "1c5ee2943a81139dbbd8b8220b24c807cddb91eb"
|
|
18
|
+
}
|