mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
1 line
8.6 KiB
JSON
1 line
8.6 KiB
JSON
{"name":"PixelCard","title":"PixelCard","description":"Card content revealed through pixel expansion transition.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <div\n ref=\"containerRef\"\n :class=\"[\n 'h-[400px] w-[300px] relative overflow-hidden grid place-items-center aspect-[4/5] border border-[#27272a] rounded-[25px] isolate transition-colors duration-200 ease-[cubic-bezier(0.5,1,0.89,1)] select-none',\n className\n ]\"\n @mouseenter=\"onMouseEnter\"\n @mouseleave=\"onMouseLeave\"\n @focus=\"finalNoFocus ? undefined : onFocus\"\n @blur=\"finalNoFocus ? undefined : onBlur\"\n :tabindex=\"finalNoFocus ? -1 : 0\"\n >\n <canvas class=\"w-full h-full block\" ref=\"canvasRef\" />\n\n <slot />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, onMounted, onUnmounted, computed, watch, useTemplateRef } from 'vue';\n\nclass Pixel {\n width: number;\n height: number;\n ctx: CanvasRenderingContext2D;\n x: number;\n y: number;\n color: string;\n speed: number;\n size: number;\n sizeStep: number;\n minSize: number;\n maxSizeInteger: number;\n maxSize: number;\n delay: number;\n counter: number;\n counterStep: number;\n isIdle: boolean;\n isReverse: boolean;\n isShimmer: boolean;\n\n constructor(\n canvas: HTMLCanvasElement,\n context: CanvasRenderingContext2D,\n x: number,\n y: number,\n color: string,\n speed: number,\n delay: number\n ) {\n this.width = canvas.width;\n this.height = canvas.height;\n this.ctx = context;\n this.x = x;\n this.y = y;\n this.color = color;\n this.speed = this.getRandomValue(0.1, 0.9) * speed;\n this.size = 0;\n this.sizeStep = Math.random() * 0.4;\n this.minSize = 0.5;\n this.maxSizeInteger = 2;\n this.maxSize = this.getRandomValue(this.minSize, this.maxSizeInteger);\n this.delay = delay;\n this.counter = 0;\n this.counterStep = Math.random() * 4 + (this.width + this.height) * 0.01;\n this.isIdle = false;\n this.isReverse = false;\n this.isShimmer = false;\n }\n\n getRandomValue(min: number, max: number) {\n return Math.random() * (max - min) + min;\n }\n\n draw() {\n const centerOffset = this.maxSizeInteger * 0.5 - this.size * 0.5;\n this.ctx.fillStyle = this.color;\n this.ctx.fillRect(this.x + centerOffset, this.y + centerOffset, this.size, this.size);\n }\n\n appear() {\n this.isIdle = false;\n if (this.counter <= this.delay) {\n this.counter += this.counterStep;\n return;\n }\n if (this.size >= this.maxSize) {\n this.isShimmer = true;\n }\n if (this.isShimmer) {\n this.shimmer();\n } else {\n this.size += this.sizeStep;\n }\n this.draw();\n }\n\n disappear() {\n this.isShimmer = false;\n this.counter = 0;\n if (this.size <= 0) {\n this.isIdle = true;\n return;\n } else {\n this.size -= 0.1;\n }\n this.draw();\n }\n\n shimmer() {\n if (this.size >= this.maxSize) {\n this.isReverse = true;\n } else if (this.size <= this.minSize) {\n this.isReverse = false;\n }\n if (this.isReverse) {\n this.size -= this.speed;\n } else {\n this.size += this.speed;\n }\n }\n}\n\nfunction getEffectiveSpeed(value: number, reducedMotion: boolean) {\n const min = 0;\n const max = 100;\n const throttle = 0.001;\n\n if (value <= min || reducedMotion) {\n return min;\n } else if (value >= max) {\n return max * throttle;\n } else {\n return value * throttle;\n }\n}\n\nconst VARIANTS = {\n default: {\n activeColor: null,\n gap: 5,\n speed: 35,\n colors: '#f8fafc,#f1f5f9,#cbd5e1',\n noFocus: false\n },\n blue: {\n activeColor: '#e0f2fe',\n gap: 10,\n speed: 25,\n colors: '#e0f2fe,#7dd3fc,#0ea5e9',\n noFocus: false\n },\n yellow: {\n activeColor: '#fef08a',\n gap: 3,\n speed: 20,\n colors: '#fef08a,#fde047,#eab308',\n noFocus: false\n },\n pink: {\n activeColor: '#fecdd3',\n gap: 6,\n speed: 80,\n colors: '#fecdd3,#fda4af,#e11d48',\n noFocus: true\n }\n};\n\ninterface PixelCardProps {\n variant?: 'default' | 'blue' | 'yellow' | 'pink';\n gap?: number;\n speed?: number;\n colors?: string;\n noFocus?: boolean;\n className?: string;\n}\n\ninterface VariantConfig {\n activeColor: string | null;\n gap: number;\n speed: number;\n colors: string;\n noFocus: boolean;\n}\n\nconst props = withDefaults(defineProps<PixelCardProps>(), {\n variant: 'default',\n className: ''\n});\n\nconst containerRef = useTemplateRef<HTMLDivElement>('containerRef');\nconst canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef');\nconst pixelsRef = ref<Pixel[]>([]);\nconst animationRef = ref<number | null>(null);\nconst timePreviousRef = ref(performance.now());\nconst reducedMotion = ref(window.matchMedia('(prefers-reduced-motion: reduce)').matches);\n\nconst variantCfg = computed((): VariantConfig => VARIANTS[props.variant] || VARIANTS.default);\nconst finalGap = computed(() => props.gap ?? variantCfg.value.gap);\nconst finalSpeed = computed(() => props.speed ?? variantCfg.value.speed);\nconst finalColors = computed(() => props.colors ?? variantCfg.value.colors);\nconst finalNoFocus = computed(() => props.noFocus ?? variantCfg.value.noFocus);\n\nlet resizeObserver: ResizeObserver | null = null;\n\nconst initPixels = () => {\n if (!containerRef.value || !canvasRef.value) return;\n\n const rect = containerRef.value.getBoundingClientRect();\n const width = Math.floor(rect.width);\n const height = Math.floor(rect.height);\n const ctx = canvasRef.value.getContext('2d');\n\n canvasRef.value.width = width;\n canvasRef.value.height = height;\n canvasRef.value.style.width = `${width}px`;\n canvasRef.value.style.height = `${height}px`;\n\n const colorsArray = finalColors.value.split(',');\n const pxs = [];\n for (let x = 0; x < width; x += parseInt(finalGap.value.toString(), 10)) {\n for (let y = 0; y < height; y += parseInt(finalGap.value.toString(), 10)) {\n const color = colorsArray[Math.floor(Math.random() * colorsArray.length)];\n\n const dx = x - width / 2;\n const dy = y - height / 2;\n const distance = Math.sqrt(dx * dx + dy * dy);\n const delay = reducedMotion.value ? 0 : distance;\n if (!ctx) return;\n pxs.push(\n new Pixel(canvasRef.value, ctx, x, y, color, getEffectiveSpeed(finalSpeed.value, reducedMotion.value), delay)\n );\n }\n }\n pixelsRef.value = pxs;\n};\n\nconst doAnimate = (fnName: keyof Pixel) => {\n animationRef.value = requestAnimationFrame(() => doAnimate(fnName));\n const timeNow = performance.now();\n const timePassed = timeNow - timePreviousRef.value;\n const timeInterval = 1000 / 60;\n\n if (timePassed < timeInterval) return;\n timePreviousRef.value = timeNow - (timePassed % timeInterval);\n\n const ctx = canvasRef.value?.getContext('2d');\n if (!ctx || !canvasRef.value) return;\n\n ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);\n\n let allIdle = true;\n for (let i = 0; i < pixelsRef.value.length; i++) {\n const pixel = pixelsRef.value[i];\n // @ts-expect-error - Dynamic method call on Pixel class\n pixel[fnName]();\n if (!pixel.isIdle) {\n allIdle = false;\n }\n }\n if (allIdle && animationRef.value) {\n cancelAnimationFrame(animationRef.value);\n }\n};\n\nconst handleAnimation = (name: keyof Pixel) => {\n if (animationRef.value !== null) {\n cancelAnimationFrame(animationRef.value);\n }\n animationRef.value = requestAnimationFrame(() => doAnimate(name));\n};\n\nconst onMouseEnter = () => handleAnimation('appear');\nconst onMouseLeave = () => handleAnimation('disappear');\nconst onFocus = (e: FocusEvent) => {\n if ((e.currentTarget as HTMLElement).contains(e.relatedTarget as Node)) return;\n handleAnimation('appear');\n};\nconst onBlur = (e: FocusEvent) => {\n if ((e.currentTarget as HTMLElement).contains(e.relatedTarget as Node)) return;\n handleAnimation('disappear');\n};\n\nwatch([finalGap, finalSpeed, finalColors, finalNoFocus], () => {\n initPixels();\n});\n\nonMounted(() => {\n initPixels();\n resizeObserver = new ResizeObserver(() => {\n initPixels();\n });\n if (containerRef.value) {\n resizeObserver.observe(containerRef.value);\n }\n});\n\nonUnmounted(() => {\n if (resizeObserver) {\n resizeObserver.disconnect();\n }\n if (animationRef.value !== null) {\n cancelAnimationFrame(animationRef.value);\n }\n});\n</script>\n","path":"PixelCard/PixelCard.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[],"devDependencies":[],"categories":["Components"]} |