Files
vue-bits/public/r/StickerPeel.json
2026-01-21 16:08:55 +05:30

1 line
13 KiB
JSON

{"name":"StickerPeel","title":"StickerPeel","description":"Sticker corner lift + peel interaction using 3D transform and shadow depth.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { gsap } from 'gsap';\nimport { Draggable } from 'gsap/all';\nimport { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';\n\ngsap.registerPlugin(Draggable);\n\ninterface StickerPeelProps {\n imageSrc: string;\n rotate?: number;\n peelBackHoverPct?: number;\n peelBackActivePct?: number;\n peelEasing?: string;\n peelHoverEasing?: string;\n width?: number;\n shadowIntensity?: number;\n lightingIntensity?: number;\n initialPosition?: 'center' | 'random' | { x: number; y: number };\n peelDirection?: number;\n className?: string;\n}\n\nconst props = withDefaults(defineProps<StickerPeelProps>(), {\n rotate: 30,\n peelBackHoverPct: 30,\n peelBackActivePct: 40,\n peelEasing: 'power3.out',\n peelHoverEasing: 'power2.out',\n width: 200,\n shadowIntensity: 0.6,\n lightingIntensity: 0.1,\n initialPosition: 'center',\n peelDirection: 0,\n className: ''\n});\n\nconst containerRef = useTemplateRef('containerRef');\nconst dragTargetRef = useTemplateRef('dragTargetRef');\nconst pointLightRef = useTemplateRef('pointLightRef');\nconst pointLightFlippedRef = useTemplateRef('pointLightFlippedRef');\nconst draggableInstanceRef = ref<Draggable | null>(null);\n\nconst defaultPadding = 12;\n\nlet cleanup: (() => void) | null = null;\n\nconst setup = () => {\n const target = dragTargetRef.value;\n if (!target) return;\n\n const boundsEl = target.parentNode as HTMLElement;\n\n const draggable = Draggable.create(target, {\n type: 'x,y',\n bounds: boundsEl,\n inertia: true,\n onDrag(this: Draggable) {\n const rot = gsap.utils.clamp(-24, 24, this.deltaX * 0.4);\n gsap.to(target, { rotation: rot, duration: 0.15, ease: 'power1.out' });\n },\n onDragEnd() {\n const rotationEase = 'power2.out';\n const duration = 0.8;\n gsap.to(target, { rotation: 0, duration, ease: rotationEase });\n }\n });\n\n draggableInstanceRef.value = draggable[0];\n\n const handleResize = () => {\n if (draggableInstanceRef.value) {\n draggableInstanceRef.value.update();\n\n const currentX = gsap.getProperty(target, 'x') as number;\n const currentY = gsap.getProperty(target, 'y') as number;\n\n const boundsRect = boundsEl.getBoundingClientRect();\n const targetRect = target.getBoundingClientRect();\n\n const maxX = boundsRect.width - targetRect.width;\n const maxY = boundsRect.height - targetRect.height;\n\n const newX = Math.max(0, Math.min(currentX, maxX));\n const newY = Math.max(0, Math.min(currentY, maxY));\n\n if (newX !== currentX || newY !== currentY) {\n gsap.to(target, {\n x: newX,\n y: newY,\n duration: 0.3,\n ease: 'power2.out'\n });\n }\n }\n };\n\n window.addEventListener('resize', handleResize);\n window.addEventListener('orientationchange', handleResize);\n\n const container = containerRef.value;\n if (!container) return;\n\n const handleTouchStart = () => {\n container.classList.add('touch-active');\n };\n\n const handleTouchEnd = () => {\n container.classList.remove('touch-active');\n };\n\n container.addEventListener('touchstart', handleTouchStart);\n container.addEventListener('touchend', handleTouchEnd);\n container.addEventListener('touchcancel', handleTouchEnd);\n\n cleanup = () => {\n window.removeEventListener('resize', handleResize);\n window.removeEventListener('orientationchange', handleResize);\n if (draggableInstanceRef.value) {\n draggableInstanceRef.value.kill();\n }\n\n container.removeEventListener('touchstart', handleTouchStart);\n container.removeEventListener('touchend', handleTouchEnd);\n container.removeEventListener('touchcancel', handleTouchEnd);\n };\n};\n\nonMounted(() => {\n setup();\n});\n\nwatch(\n () => props.initialPosition,\n () => {\n const target = dragTargetRef.value;\n if (!target) return;\n\n let startX = 0,\n startY = 0;\n\n if (props.initialPosition === 'center') {\n return;\n }\n\n if (\n typeof props.initialPosition === 'object' &&\n props.initialPosition.x !== undefined &&\n props.initialPosition.y !== undefined\n ) {\n startX = props.initialPosition.x;\n startY = props.initialPosition.y;\n }\n\n gsap.set(target, { x: startX, y: startY });\n },\n { immediate: true }\n);\n\nlet lightHandler: (() => void) | null = null;\n\nwatch(\n () => props.peelDirection,\n () => {\n const updateLight = (e: Event) => {\n const mouseEvent = e as MouseEvent;\n const rect = containerRef.value?.getBoundingClientRect();\n if (!rect) return;\n\n const x = mouseEvent.clientX - rect.left;\n const y = mouseEvent.clientY - rect.top;\n\n if (pointLightRef.value) {\n gsap.set(pointLightRef.value, { attr: { x, y } });\n }\n\n const normalizedAngle = Math.abs(props.peelDirection % 360);\n if (pointLightFlippedRef.value) {\n if (normalizedAngle !== 180) {\n gsap.set(pointLightFlippedRef.value, {\n attr: { x, y: rect.height - y }\n });\n } else {\n gsap.set(pointLightFlippedRef.value, {\n attr: { x: -1000, y: -1000 }\n });\n }\n }\n };\n\n const container = containerRef.value;\n const eventType = 'mousemove';\n\n if (container) {\n container.addEventListener(eventType, updateLight);\n lightHandler = () => container.removeEventListener(eventType, updateLight);\n }\n },\n { immediate: true }\n);\n\nonBeforeUnmount(() => {\n const container = containerRef.value;\n if (container && lightHandler) {\n lightHandler();\n lightHandler = null;\n }\n\n if (cleanup) {\n cleanup();\n cleanup = null;\n }\n});\n\nconst cssVars = computed(() => ({\n '--sticker-rotate': `${props.rotate}deg`,\n '--sticker-p': `${defaultPadding}px`,\n '--sticker-peelback-hover': `${props.peelBackHoverPct}%`,\n '--sticker-peelback-active': `${props.peelBackActivePct}%`,\n '--sticker-peel-easing': props.peelEasing,\n '--sticker-peel-hover-easing': props.peelHoverEasing,\n '--sticker-width': `${props.width}px`,\n '--sticker-shadow-opacity': props.shadowIntensity,\n '--sticker-lighting-constant': props.lightingIntensity,\n '--peel-direction': `${props.peelDirection}deg`,\n '--sticker-start': `calc(-1 * ${defaultPadding}px)`,\n '--sticker-end': `calc(100% + ${defaultPadding}px)`\n}));\n\nconst stickerMainStyle = computed(() => ({\n clipPath: `polygon(var(--sticker-start) var(--sticker-start), var(--sticker-end) var(--sticker-start), var(--sticker-end) var(--sticker-end), var(--sticker-start) var(--sticker-end))`,\n transition: 'clip-path 0.6s ease-out',\n filter: 'url(#dropShadow)',\n willChange: 'clip-path, transform'\n}));\n\nconst flapStyle = computed(() => ({\n clipPath: `polygon(var(--sticker-start) var(--sticker-start), var(--sticker-end) var(--sticker-start), var(--sticker-end) var(--sticker-start), var(--sticker-start) var(--sticker-start))`,\n top: `calc(-100% - var(--sticker-p) - var(--sticker-p))`,\n transform: 'scaleY(-1)',\n transition: 'all 0.6s ease-out',\n willChange: 'clip-path, transform'\n}));\n\nconst imageStyle = computed(() => ({\n transform: `rotate(calc(${props.rotate}deg - ${props.peelDirection}deg))`,\n width: `${props.width}px`\n}));\n\nconst shadowImageStyle = computed(() => ({\n ...imageStyle.value,\n filter: 'url(#expandAndFill)'\n}));\n\nconst dropShadowStdDeviation = computed(() => 3 * props.shadowIntensity);\nconst flippedLightingConstant = computed(() => props.lightingIntensity * 7);\n</script>\n\n<template>\n <div\n ref=\"dragTargetRef\"\n :class=\"[`absolute cursor-grab active:cursor-grabbing transform-gpu ${className}`]\"\n :style=\"cssVars\"\n >\n <svg width=\"0\" height=\"0\">\n <defs>\n <filter id=\"pointLight\">\n <feGaussianBlur stdDeviation=\"1\" result=\"blur\" />\n <feSpecularLighting\n result=\"spec\"\n in=\"blur\"\n :specularExponent=\"100\"\n :specularConstant=\"props.lightingIntensity\"\n lighting-color=\"white\"\n >\n <fePointLight ref=\"pointLightRef\" :x=\"100\" :y=\"100\" :z=\"300\" />\n </feSpecularLighting>\n <feComposite in=\"spec\" in2=\"SourceGraphic\" result=\"lit\" />\n <feComposite in=\"lit\" in2=\"SourceAlpha\" operator=\"in\" />\n </filter>\n\n <filter id=\"pointLightFlipped\">\n <feGaussianBlur stdDeviation=\"10\" result=\"blur\" />\n <feSpecularLighting\n result=\"spec\"\n in=\"blur\"\n :specularExponent=\"100\"\n :specularConstant=\"flippedLightingConstant\"\n lighting-color=\"white\"\n >\n <fePointLight ref=\"pointLightFlippedRef\" :x=\"100\" :y=\"100\" :z=\"300\" />\n </feSpecularLighting>\n <feComposite in=\"spec\" in2=\"SourceGraphic\" result=\"lit\" />\n <feComposite in=\"lit\" in2=\"SourceAlpha\" operator=\"in\" />\n </filter>\n\n <filter id=\"dropShadow\">\n <feDropShadow\n dx=\"2\"\n dy=\"4\"\n :stdDeviation=\"dropShadowStdDeviation\"\n flood-color=\"black\"\n :flood-opacity=\"props.shadowIntensity\"\n />\n </filter>\n\n <filter id=\"expandAndFill\">\n <feOffset dx=\"0\" dy=\"0\" in=\"SourceAlpha\" result=\"shape\" />\n <feFlood flood-color=\"rgb(179,179,179)\" result=\"flood\" />\n <feComposite operator=\"in\" in=\"flood\" in2=\"shape\" />\n </filter>\n </defs>\n </svg>\n\n <div\n class=\"relative touch-none sm:touch-auto select-none sticker-container\"\n ref=\"containerRef\"\n :style=\"{\n WebkitUserSelect: 'none',\n userSelect: 'none',\n WebkitTouchCallout: 'none',\n WebkitTapHighlightColor: 'transparent',\n transform: `rotate(${peelDirection}deg)`,\n transformOrigin: 'center'\n }\"\n >\n <div class=\"sticker-main\" :style=\"stickerMainStyle\">\n <div :style=\"{ filter: 'url(#pointLight)' }\">\n <img :src=\"props.imageSrc\" alt=\"\" class=\"block\" :style=\"imageStyle\" draggable=\"false\" @contextmenu.prevent />\n </div>\n\n <div class=\"top-4 left-2 absolute opacity-40 w-full h-full\" :style=\"{ filter: 'brightness(0) blur(8px)' }\">\n <div class=\"sticker-flap\" :style=\"flapStyle\">\n <img\n :src=\"props.imageSrc\"\n alt=\"\"\n class=\"block\"\n :style=\"shadowImageStyle\"\n draggable=\"false\"\n @contextmenu.prevent\n />\n </div>\n </div>\n\n <div class=\"left-0 absolute w-full h-full sticker-flap\" :style=\"flapStyle\">\n <div :style=\"{ filter: 'url(#pointLightFlipped)' }\">\n <img\n :src=\"props.imageSrc\"\n alt=\"\"\n class=\"block\"\n :style=\"shadowImageStyle\"\n draggable=\"false\"\n @contextmenu.prevent\n />\n </div>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.sticker-container:hover .sticker-main,\n.sticker-container.touch-active .sticker-main {\n clip-path: polygon(\n var(--sticker-start) var(--sticker-peelback-hover),\n var(--sticker-end) var(--sticker-peelback-hover),\n var(--sticker-end) var(--sticker-end),\n var(--sticker-start) var(--sticker-end)\n ) !important;\n}\n\n.sticker-container:hover .sticker-flap,\n.sticker-container.touch-active .sticker-flap {\n clip-path: polygon(\n var(--sticker-start) var(--sticker-start),\n var(--sticker-end) var(--sticker-start),\n var(--sticker-end) var(--sticker-peelback-hover),\n var(--sticker-start) var(--sticker-peelback-hover)\n ) !important;\n top: calc(-100% + 2 * var(--sticker-peelback-hover) - 1px) !important;\n}\n\n.sticker-container:active .sticker-main {\n clip-path: polygon(\n var(--sticker-start) var(--sticker-peelback-active),\n var(--sticker-end) var(--sticker-peelback-active),\n var(--sticker-end) var(--sticker-end),\n var(--sticker-start) var(--sticker-end)\n ) !important;\n}\n\n.sticker-container:active .sticker-flap {\n clip-path: polygon(\n var(--sticker-start) var(--sticker-start),\n var(--sticker-end) var(--sticker-start),\n var(--sticker-end) var(--sticker-peelback-active),\n var(--sticker-start) var(--sticker-peelback-active)\n ) !important;\n top: calc(-100% + 2 * var(--sticker-peelback-active) - 1px) !important;\n}\n</style>\n","path":"StickerPeel/StickerPeel.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"gsap","version":"^3.13.0"}],"devDependencies":[],"categories":["Animations"]}