mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
Merge branch 'main' into feat/scroll-stack
This commit is contained in:
@@ -12,7 +12,7 @@ copies or substantial portions of the Software.
|
||||
|
||||
## Commons Clause Restriction
|
||||
|
||||
You may use this Software, including for any commercial purpose, **so long as you do not sell, sublicense, or redistribute the components themselves-whether alone, in a bundle, or as a ported version.**
|
||||
You may use this Software, including for any commercial purpose, **so long as you do not sell, sublicense, or redistribute the components themselves-whether alone, in a bundle, template, or as a ported version.**
|
||||
|
||||
## No Warranty
|
||||
|
||||
|
||||
27
package-lock.json
generated
27
package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"@vueuse/motion": "^3.0.3",
|
||||
"@wdns/vue-code-block": "^2.3.5",
|
||||
"gsap": "^3.13.0",
|
||||
"lenis": "^1.3.8",
|
||||
"matter-js": "^0.20.0",
|
||||
"motion-v": "^1.5.0",
|
||||
"ogl": "^1.0.11",
|
||||
@@ -6433,6 +6434,32 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lenis": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.8.tgz",
|
||||
"integrity": "sha512-LVeoMs6jZE1eu3gPsexndm+vk01pLFeq7P00vjIpI17saD52IYu8nPA4gX43elz8tp/TTCXcX6Em1MEjDl9NTw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/darkroomengineering"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nuxt/kit": ">=3.0.0",
|
||||
"react": ">=17.0.0",
|
||||
"vue": ">=3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@nuxt/kit": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"vue": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
|
||||
BIN
src/assets/logos/vue-bits-sticker.png
Normal file
BIN
src/assets/logos/vue-bits-sticker.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@@ -1,5 +1,5 @@
|
||||
// Highlighted sidebar items
|
||||
export const NEW = ['Target Cursor', 'Ripple Grid', 'Magic Bento', 'Galaxy', 'Text Type', 'Glass Surface', 'Scroll Stack'];
|
||||
export const NEW = ['Target Cursor', 'Ripple Grid', 'Magic Bento', 'Galaxy', 'Text Type', 'Glass Surface', 'Sticker Peel', 'Scroll Stack'];
|
||||
export const UPDATED = [];
|
||||
|
||||
// Used for main sidebar navigation
|
||||
@@ -39,6 +39,7 @@ export const CATEGORIES = [
|
||||
'Splash Cursor',
|
||||
'Pixel Transition',
|
||||
'Target Cursor',
|
||||
'Sticker Peel',
|
||||
'Ribbons',
|
||||
'Glare Hover',
|
||||
'Magnet Lines',
|
||||
|
||||
@@ -19,6 +19,7 @@ const animations = {
|
||||
'shape-blur': () => import('../demo/Animations/ShapeBlurDemo.vue'),
|
||||
'target-cursor': () => import('../demo/Animations/TargetCursorDemo.vue'),
|
||||
'crosshair': () => import('../demo/Animations/CrosshairDemo.vue'),
|
||||
'sticker-peel': () => import('../demo/Animations/StickerPeelDemo.vue'),
|
||||
};
|
||||
|
||||
const textAnimations = {
|
||||
|
||||
23
src/constants/code/Animations/stickerPeelCode.ts
Normal file
23
src/constants/code/Animations/stickerPeelCode.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import code from '@/content/Animations/StickerPeel/StickerPeel.vue?raw';
|
||||
import { createCodeObject } from '@/types/code';
|
||||
|
||||
export const stickerPeel = createCodeObject(code, 'Animations/StickerPeel', {
|
||||
installation: `npm install gsap`,
|
||||
usage: `<template>
|
||||
<StickerPeel
|
||||
:image-src="logo"
|
||||
:width="200"
|
||||
:rotate="30"
|
||||
:peelBackHoverPct="20"
|
||||
:peelBackActivePct="40"
|
||||
:shadow-intensity="0.6"
|
||||
:lighting-intensity="0.1"
|
||||
:initial-position="{ x: -100, y: 100 }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import StickerPeel from './StickerPeel.vue';
|
||||
import logo from './assets/sticker.png';
|
||||
</script>`
|
||||
});
|
||||
401
src/content/Animations/StickerPeel/StickerPeel.vue
Normal file
401
src/content/Animations/StickerPeel/StickerPeel.vue
Normal file
@@ -0,0 +1,401 @@
|
||||
<script setup lang="ts">
|
||||
import { gsap } from 'gsap';
|
||||
import { Draggable } from 'gsap/Draggable';
|
||||
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||
|
||||
gsap.registerPlugin(Draggable);
|
||||
|
||||
interface StickerPeelProps {
|
||||
imageSrc: string;
|
||||
rotate?: number;
|
||||
peelBackHoverPct?: number;
|
||||
peelBackActivePct?: number;
|
||||
peelEasing?: string;
|
||||
peelHoverEasing?: string;
|
||||
width?: number;
|
||||
shadowIntensity?: number;
|
||||
lightingIntensity?: number;
|
||||
initialPosition?: 'center' | 'random' | { x: number; y: number };
|
||||
peelDirection?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<StickerPeelProps>(), {
|
||||
rotate: 30,
|
||||
peelBackHoverPct: 30,
|
||||
peelBackActivePct: 40,
|
||||
peelEasing: 'power3.out',
|
||||
peelHoverEasing: 'power2.out',
|
||||
width: 200,
|
||||
shadowIntensity: 0.6,
|
||||
lightingIntensity: 0.1,
|
||||
initialPosition: 'center',
|
||||
peelDirection: 0,
|
||||
className: ''
|
||||
});
|
||||
|
||||
const containerRef = useTemplateRef('containerRef');
|
||||
const dragTargetRef = useTemplateRef('dragTargetRef');
|
||||
const pointLightRef = useTemplateRef('pointLightRef');
|
||||
const pointLightFlippedRef = useTemplateRef('pointLightFlippedRef');
|
||||
const draggableInstanceRef = ref<Draggable | null>(null);
|
||||
|
||||
const defaultPadding = 12;
|
||||
|
||||
let cleanup: (() => void) | null = null;
|
||||
|
||||
const setup = () => {
|
||||
const target = dragTargetRef.value;
|
||||
if (!target) return;
|
||||
|
||||
const boundsEl = target.parentNode as HTMLElement;
|
||||
|
||||
const draggable = Draggable.create(target, {
|
||||
type: 'x,y',
|
||||
bounds: boundsEl,
|
||||
inertia: true,
|
||||
onDrag(this: Draggable) {
|
||||
const rot = gsap.utils.clamp(-24, 24, this.deltaX * 0.4);
|
||||
gsap.to(target, { rotation: rot, duration: 0.15, ease: 'power1.out' });
|
||||
},
|
||||
onDragEnd() {
|
||||
const rotationEase = 'power2.out';
|
||||
const duration = 0.8;
|
||||
gsap.to(target, { rotation: 0, duration, ease: rotationEase });
|
||||
}
|
||||
});
|
||||
|
||||
draggableInstanceRef.value = draggable[0];
|
||||
|
||||
const handleResize = () => {
|
||||
if (draggableInstanceRef.value) {
|
||||
draggableInstanceRef.value.update();
|
||||
|
||||
const currentX = gsap.getProperty(target, 'x') as number;
|
||||
const currentY = gsap.getProperty(target, 'y') as number;
|
||||
|
||||
const boundsRect = boundsEl.getBoundingClientRect();
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
|
||||
const maxX = boundsRect.width - targetRect.width;
|
||||
const maxY = boundsRect.height - targetRect.height;
|
||||
|
||||
const newX = Math.max(0, Math.min(currentX, maxX));
|
||||
const newY = Math.max(0, Math.min(currentY, maxY));
|
||||
|
||||
if (newX !== currentX || newY !== currentY) {
|
||||
gsap.to(target, {
|
||||
x: newX,
|
||||
y: newY,
|
||||
duration: 0.3,
|
||||
ease: 'power2.out'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('orientationchange', handleResize);
|
||||
|
||||
const container = containerRef.value;
|
||||
if (!container) return;
|
||||
|
||||
const handleTouchStart = () => {
|
||||
container.classList.add('touch-active');
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
container.classList.remove('touch-active');
|
||||
};
|
||||
|
||||
container.addEventListener('touchstart', handleTouchStart);
|
||||
container.addEventListener('touchend', handleTouchEnd);
|
||||
container.addEventListener('touchcancel', handleTouchEnd);
|
||||
|
||||
cleanup = () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('orientationchange', handleResize);
|
||||
if (draggableInstanceRef.value) {
|
||||
draggableInstanceRef.value.kill();
|
||||
}
|
||||
|
||||
container.removeEventListener('touchstart', handleTouchStart);
|
||||
container.removeEventListener('touchend', handleTouchEnd);
|
||||
container.removeEventListener('touchcancel', handleTouchEnd);
|
||||
};
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setup();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.initialPosition,
|
||||
() => {
|
||||
const target = dragTargetRef.value;
|
||||
if (!target) return;
|
||||
|
||||
let startX = 0,
|
||||
startY = 0;
|
||||
|
||||
if (props.initialPosition === 'center') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof props.initialPosition === 'object' &&
|
||||
props.initialPosition.x !== undefined &&
|
||||
props.initialPosition.y !== undefined
|
||||
) {
|
||||
startX = props.initialPosition.x;
|
||||
startY = props.initialPosition.y;
|
||||
}
|
||||
|
||||
gsap.set(target, { x: startX, y: startY });
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
let lightHandler: (() => void) | null = null;
|
||||
|
||||
watch(
|
||||
() => props.peelDirection,
|
||||
() => {
|
||||
const updateLight = (e: Event) => {
|
||||
const mouseEvent = e as MouseEvent;
|
||||
const rect = containerRef.value?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
|
||||
const x = mouseEvent.clientX - rect.left;
|
||||
const y = mouseEvent.clientY - rect.top;
|
||||
|
||||
if (pointLightRef.value) {
|
||||
gsap.set(pointLightRef.value, { attr: { x, y } });
|
||||
}
|
||||
|
||||
const normalizedAngle = Math.abs(props.peelDirection % 360);
|
||||
if (pointLightFlippedRef.value) {
|
||||
if (normalizedAngle !== 180) {
|
||||
gsap.set(pointLightFlippedRef.value, {
|
||||
attr: { x, y: rect.height - y }
|
||||
});
|
||||
} else {
|
||||
gsap.set(pointLightFlippedRef.value, {
|
||||
attr: { x: -1000, y: -1000 }
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const container = containerRef.value;
|
||||
const eventType = 'mousemove';
|
||||
|
||||
if (container) {
|
||||
container.addEventListener(eventType, updateLight);
|
||||
lightHandler = () => container.removeEventListener(eventType, updateLight);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
const container = containerRef.value;
|
||||
if (container && lightHandler) {
|
||||
lightHandler();
|
||||
lightHandler = null;
|
||||
}
|
||||
|
||||
if (cleanup) {
|
||||
cleanup();
|
||||
cleanup = null;
|
||||
}
|
||||
});
|
||||
|
||||
const cssVars = computed(() => ({
|
||||
'--sticker-rotate': `${props.rotate}deg`,
|
||||
'--sticker-p': `${defaultPadding}px`,
|
||||
'--sticker-peelback-hover': `${props.peelBackHoverPct}%`,
|
||||
'--sticker-peelback-active': `${props.peelBackActivePct}%`,
|
||||
'--sticker-peel-easing': props.peelEasing,
|
||||
'--sticker-peel-hover-easing': props.peelHoverEasing,
|
||||
'--sticker-width': `${props.width}px`,
|
||||
'--sticker-shadow-opacity': props.shadowIntensity,
|
||||
'--sticker-lighting-constant': props.lightingIntensity,
|
||||
'--peel-direction': `${props.peelDirection}deg`,
|
||||
'--sticker-start': `calc(-1 * ${defaultPadding}px)`,
|
||||
'--sticker-end': `calc(100% + ${defaultPadding}px)`
|
||||
}));
|
||||
|
||||
const stickerMainStyle = computed(() => ({
|
||||
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))`,
|
||||
transition: 'clip-path 0.6s ease-out',
|
||||
filter: 'url(#dropShadow)',
|
||||
willChange: 'clip-path, transform'
|
||||
}));
|
||||
|
||||
const flapStyle = computed(() => ({
|
||||
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))`,
|
||||
top: `calc(-100% - var(--sticker-p) - var(--sticker-p))`,
|
||||
transform: 'scaleY(-1)',
|
||||
transition: 'all 0.6s ease-out',
|
||||
willChange: 'clip-path, transform'
|
||||
}));
|
||||
|
||||
const imageStyle = computed(() => ({
|
||||
transform: `rotate(calc(${props.rotate}deg - ${props.peelDirection}deg))`,
|
||||
width: `${props.width}px`
|
||||
}));
|
||||
|
||||
const shadowImageStyle = computed(() => ({
|
||||
...imageStyle.value,
|
||||
filter: 'url(#expandAndFill)'
|
||||
}));
|
||||
|
||||
const dropShadowStdDeviation = computed(() => 3 * props.shadowIntensity);
|
||||
const flippedLightingConstant = computed(() => props.lightingIntensity * 7);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="dragTargetRef"
|
||||
:class="[`absolute cursor-grab active:cursor-grabbing transform-gpu ${className}`]"
|
||||
:style="cssVars"
|
||||
>
|
||||
<svg width="0" height="0">
|
||||
<defs>
|
||||
<filter id="pointLight">
|
||||
<feGaussianBlur stdDeviation="1" result="blur" />
|
||||
<feSpecularLighting
|
||||
result="spec"
|
||||
in="blur"
|
||||
:specularExponent="100"
|
||||
:specularConstant="props.lightingIntensity"
|
||||
lighting-color="white"
|
||||
>
|
||||
<fePointLight ref="pointLightRef" :x="100" :y="100" :z="300" />
|
||||
</feSpecularLighting>
|
||||
<feComposite in="spec" in2="SourceGraphic" result="lit" />
|
||||
<feComposite in="lit" in2="SourceAlpha" operator="in" />
|
||||
</filter>
|
||||
|
||||
<filter id="pointLightFlipped">
|
||||
<feGaussianBlur stdDeviation="10" result="blur" />
|
||||
<feSpecularLighting
|
||||
result="spec"
|
||||
in="blur"
|
||||
:specularExponent="100"
|
||||
:specularConstant="flippedLightingConstant"
|
||||
lighting-color="white"
|
||||
>
|
||||
<fePointLight ref="pointLightFlippedRef" :x="100" :y="100" :z="300" />
|
||||
</feSpecularLighting>
|
||||
<feComposite in="spec" in2="SourceGraphic" result="lit" />
|
||||
<feComposite in="lit" in2="SourceAlpha" operator="in" />
|
||||
</filter>
|
||||
|
||||
<filter id="dropShadow">
|
||||
<feDropShadow
|
||||
dx="2"
|
||||
dy="4"
|
||||
:stdDeviation="dropShadowStdDeviation"
|
||||
flood-color="black"
|
||||
:flood-opacity="props.shadowIntensity"
|
||||
/>
|
||||
</filter>
|
||||
|
||||
<filter id="expandAndFill">
|
||||
<feOffset dx="0" dy="0" in="SourceAlpha" result="shape" />
|
||||
<feFlood flood-color="rgb(179,179,179)" result="flood" />
|
||||
<feComposite operator="in" in="flood" in2="shape" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div
|
||||
class="relative touch-none sm:touch-auto select-none sticker-container"
|
||||
ref="containerRef"
|
||||
:style="{
|
||||
WebkitUserSelect: 'none',
|
||||
userSelect: 'none',
|
||||
WebkitTouchCallout: 'none',
|
||||
WebkitTapHighlightColor: 'transparent',
|
||||
transform: `rotate(${peelDirection}deg)`,
|
||||
transformOrigin: 'center'
|
||||
}"
|
||||
>
|
||||
<div class="sticker-main" :style="stickerMainStyle">
|
||||
<div :style="{ filter: 'url(#pointLight)' }">
|
||||
<img :src="props.imageSrc" alt="" class="block" :style="imageStyle" draggable="false" @contextmenu.prevent />
|
||||
</div>
|
||||
|
||||
<div class="top-4 left-2 absolute opacity-40 w-full h-full" :style="{ filter: 'brightness(0) blur(8px)' }">
|
||||
<div class="sticker-flap" :style="flapStyle">
|
||||
<img
|
||||
:src="props.imageSrc"
|
||||
alt=""
|
||||
class="block"
|
||||
:style="shadowImageStyle"
|
||||
draggable="false"
|
||||
@contextmenu.prevent
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="left-0 absolute w-full h-full sticker-flap" :style="flapStyle">
|
||||
<div :style="{ filter: 'url(#pointLightFlipped)' }">
|
||||
<img
|
||||
:src="props.imageSrc"
|
||||
alt=""
|
||||
class="block"
|
||||
:style="shadowImageStyle"
|
||||
draggable="false"
|
||||
@contextmenu.prevent
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sticker-container:hover .sticker-main,
|
||||
.sticker-container.touch-active .sticker-main {
|
||||
clip-path: polygon(
|
||||
var(--sticker-start) var(--sticker-peelback-hover),
|
||||
var(--sticker-end) var(--sticker-peelback-hover),
|
||||
var(--sticker-end) var(--sticker-end),
|
||||
var(--sticker-start) var(--sticker-end)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.sticker-container:hover .sticker-flap,
|
||||
.sticker-container.touch-active .sticker-flap {
|
||||
clip-path: polygon(
|
||||
var(--sticker-start) var(--sticker-start),
|
||||
var(--sticker-end) var(--sticker-start),
|
||||
var(--sticker-end) var(--sticker-peelback-hover),
|
||||
var(--sticker-start) var(--sticker-peelback-hover)
|
||||
) !important;
|
||||
top: calc(-100% + 2 * var(--sticker-peelback-hover) - 1px) !important;
|
||||
}
|
||||
|
||||
.sticker-container:active .sticker-main {
|
||||
clip-path: polygon(
|
||||
var(--sticker-start) var(--sticker-peelback-active),
|
||||
var(--sticker-end) var(--sticker-peelback-active),
|
||||
var(--sticker-end) var(--sticker-end),
|
||||
var(--sticker-start) var(--sticker-end)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.sticker-container:active .sticker-flap {
|
||||
clip-path: polygon(
|
||||
var(--sticker-start) var(--sticker-start),
|
||||
var(--sticker-end) var(--sticker-start),
|
||||
var(--sticker-end) var(--sticker-peelback-active),
|
||||
var(--sticker-start) var(--sticker-peelback-active)
|
||||
) !important;
|
||||
top: calc(-100% + 2 * var(--sticker-peelback-active) - 1px) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -70,7 +70,7 @@ import Ribbons from '../../content/Animations/Ribbons/Ribbons.vue';
|
||||
import { ribbons } from '@/constants/code/Animations/ribbonsCode';
|
||||
|
||||
const baseThickness = ref(30);
|
||||
const colors = ref(['#5227FF']);
|
||||
const colors = ref(['#27FF64']);
|
||||
const speedMultiplier = ref(0.5);
|
||||
const maxAge = ref(500);
|
||||
const enableFade = ref(false);
|
||||
|
||||
151
src/demo/Animations/StickerPeelDemo.vue
Normal file
151
src/demo/Animations/StickerPeelDemo.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div
|
||||
class="flex-col bg-[linear-gradient(to_bottom,_#060010,_#0D0716,_#0D0716,_#060010)] h-[500px] overflow-hidden demo-container"
|
||||
>
|
||||
<StickerPeel
|
||||
:image-src="logo"
|
||||
:rotate="rotate"
|
||||
:width="width"
|
||||
:peel-back-hover-pct="peelBackHoverPct"
|
||||
:peel-back-active-pct="peelBackActivePct"
|
||||
:lighting-intensity="lightingIntensity"
|
||||
:shadow-intensity="shadowIntensity"
|
||||
:peel-direction="peelDirection"
|
||||
class-name="z-2 absolute"
|
||||
/>
|
||||
|
||||
<p
|
||||
class="top-[1em] left-1/2 z-0 absolute font-black text-[#222] text-[clamp(1.5rem,4vw,3rem)] -translate-x-1/2 transform"
|
||||
>
|
||||
Try dragging it!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Customize>
|
||||
<PreviewSlider title="Peel Direction" :min="0" :max="360" :step="1" value-unit="°" v-model="peelDirection" />
|
||||
<PreviewSlider title="Rotate" :min="0" :max="60" :step="1" value-unit="°" v-model="rotate" />
|
||||
<PreviewSlider title="Width" :min="100" :max="300" :step="10" value-unit="px" v-model="width" />
|
||||
<PreviewSlider title="Peel Hover %" :min="0" :max="50" :step="1" value-unit="%" v-model="peelBackHoverPct" />
|
||||
<PreviewSlider title="Peel Active %" :min="0" :max="70" :step="1" value-unit="%" v-model="peelBackActivePct" />
|
||||
<PreviewSlider title="Lighting Intensity" :min="0" :max="0.5" :step="0.01" v-model="lightingIntensity" />
|
||||
<PreviewSlider title="Shadow Intensity" :min="0" :max="1" :step="0.01" v-model="shadowIntensity" />
|
||||
</Customize>
|
||||
|
||||
<PropTable :data="propData" />
|
||||
<Dependencies :dependency-list="['gsap']" />
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<CodeExample :code-object="stickerPeel" />
|
||||
</template>
|
||||
|
||||
<template #cli>
|
||||
<CliInstallation :command="stickerPeel.cli" />
|
||||
</template>
|
||||
</TabbedLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import CliInstallation from '../../components/code/CliInstallation.vue';
|
||||
import CodeExample from '../../components/code/CodeExample.vue';
|
||||
import Dependencies from '../../components/code/Dependencies.vue';
|
||||
import Customize from '../../components/common/Customize.vue';
|
||||
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
||||
import PropTable from '../../components/common/PropTable.vue';
|
||||
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||
import { stickerPeel } from '../../constants/code/Animations/stickerPeelCode';
|
||||
import StickerPeel from '../../content/Animations/StickerPeel/StickerPeel.vue';
|
||||
import logo from '../../assets/logos/vue-bits-sticker.png';
|
||||
|
||||
const rotate = ref(0);
|
||||
const width = ref(200);
|
||||
const peelBackHoverPct = ref(30);
|
||||
const peelBackActivePct = ref(40);
|
||||
const lightingIntensity = ref(0.1);
|
||||
const shadowIntensity = ref(0.5);
|
||||
const peelDirection = ref(0);
|
||||
|
||||
const propData = [
|
||||
{
|
||||
name: 'imageSrc',
|
||||
type: 'string',
|
||||
default: 'required',
|
||||
description: 'The source URL for the sticker image'
|
||||
},
|
||||
{
|
||||
name: 'rotate',
|
||||
type: 'number',
|
||||
default: '30',
|
||||
description: 'The rotation angle in degrees when dragging'
|
||||
},
|
||||
{
|
||||
name: 'peelBackHoverPct',
|
||||
type: 'number',
|
||||
default: '30',
|
||||
description: 'Percentage of peel effect on hover (0-100)'
|
||||
},
|
||||
{
|
||||
name: 'peelBackActivePct',
|
||||
type: 'number',
|
||||
default: '40',
|
||||
description: 'Percentage of peel effect when active/clicked (0-100)'
|
||||
},
|
||||
{
|
||||
name: 'peelDirection',
|
||||
type: 'number',
|
||||
default: '0',
|
||||
description: 'Direction of the peel effect in degrees (0-360)'
|
||||
},
|
||||
{
|
||||
name: 'peelEasing',
|
||||
type: 'string',
|
||||
default: 'power3.out',
|
||||
description: 'GSAP easing function for peel animations'
|
||||
},
|
||||
{
|
||||
name: 'peelHoverEasing',
|
||||
type: 'string',
|
||||
default: 'power2.out',
|
||||
description: 'GSAP easing function for hover transitions'
|
||||
},
|
||||
{
|
||||
name: 'width',
|
||||
type: 'number',
|
||||
default: '200',
|
||||
description: 'Width of the sticker in pixels'
|
||||
},
|
||||
{
|
||||
name: 'shadowIntensity',
|
||||
type: 'number',
|
||||
default: '0.6',
|
||||
description: 'Intensity of the shadow effect (0-1)'
|
||||
},
|
||||
{
|
||||
name: 'lightingIntensity',
|
||||
type: 'number',
|
||||
default: '0.1',
|
||||
description: 'Intensity of the lighting effect (0-1)'
|
||||
},
|
||||
{
|
||||
name: 'initialPosition',
|
||||
type: 'string',
|
||||
default: 'center',
|
||||
description: "Initial position of the sticker ('center', 'top-left', 'top-right', 'bottom-left', 'bottom-right')"
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Custom class name for additional styling'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div class="relative overflow-y-auto no-scrollbar demo-container">
|
||||
<div class="relative overflow-y-auto no-scrollbar demo-container" ref="scrollContainerRef">
|
||||
<GlassSurface
|
||||
:key="key"
|
||||
:width="360"
|
||||
@@ -22,26 +22,21 @@
|
||||
/>
|
||||
|
||||
<div class="absolute flex flex-col items-center gap-6 top-0 left-0 right-0">
|
||||
<div
|
||||
class="absolute translate-y-1/2 top-12 text-4xl font-bold text-[#27FF64] z-0 whitespace-nowrap text-center"
|
||||
>
|
||||
<div class="absolute translate-y-1/2 top-12 text-4xl font-bold text-[#333] z-0 whitespace-nowrap text-center">
|
||||
Try scrolling.
|
||||
</div>
|
||||
|
||||
<!-- Top Spacer -->
|
||||
<div class="h-60 w-full" />
|
||||
|
||||
<!-- Image Blocks -->
|
||||
<div v-for="(item, index) in imageBlocks" :key="index" class="relative">
|
||||
<div v-for="(item, index) in imageBlocks" :key="index" class="relative py-4">
|
||||
<img :src="item.src" class="w-128 rounded-2xl object-cover grayscale-100" />
|
||||
<div
|
||||
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 font-extrabold text-center leading-[100%] text-[3rem] min-w-72"
|
||||
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 font-extrabold text-center leading-[100%] text-[3rem] min-w-72 mix-blend-overlay text-white"
|
||||
>
|
||||
{{ item.text }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Spacer -->
|
||||
<div class="h-60 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,7 +70,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { ref, watch, onMounted, onUnmounted, useTemplateRef } from 'vue';
|
||||
import Lenis from 'lenis';
|
||||
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||
import PropTable from '../../components/common/PropTable.vue';
|
||||
import CliInstallation from '../../components/code/CliInstallation.vue';
|
||||
@@ -88,6 +84,10 @@ import { useForceRerender } from '@/composables/useForceRerender';
|
||||
|
||||
const { rerenderKey: key, forceRerender } = useForceRerender();
|
||||
|
||||
const scrollContainerRef = useTemplateRef<HTMLElement>('scrollContainerRef');
|
||||
let lenis: Lenis | null = null;
|
||||
let rafId: number | null = null;
|
||||
|
||||
const borderRadius = ref(50);
|
||||
const backgroundOpacity = ref(0.1);
|
||||
const saturation = ref(1);
|
||||
@@ -118,6 +118,49 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
const initLenis = () => {
|
||||
if (!scrollContainerRef.value) return;
|
||||
|
||||
lenis = new Lenis({
|
||||
wrapper: scrollContainerRef.value,
|
||||
content: scrollContainerRef.value.firstElementChild as HTMLElement,
|
||||
duration: 1.2,
|
||||
easing: (t: number) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
||||
orientation: 'vertical',
|
||||
gestureOrientation: 'vertical',
|
||||
smoothWheel: true,
|
||||
wheelMultiplier: 1,
|
||||
touchMultiplier: 2,
|
||||
infinite: false
|
||||
});
|
||||
|
||||
const raf = (time: number) => {
|
||||
lenis?.raf(time);
|
||||
rafId = requestAnimationFrame(raf);
|
||||
};
|
||||
|
||||
rafId = requestAnimationFrame(raf);
|
||||
};
|
||||
|
||||
const destroyLenis = () => {
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId);
|
||||
rafId = null;
|
||||
}
|
||||
if (lenis) {
|
||||
lenis.destroy();
|
||||
lenis = null;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initLenis();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
destroyLenis();
|
||||
});
|
||||
|
||||
const imageBlocks = [
|
||||
{
|
||||
src: 'https://images.unsplash.com/photo-1500673587002-1d2548cfba1b?q=80&w=1740&auto=format&fit=crop',
|
||||
|
||||
Reference in New Issue
Block a user