mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
Merge branch 'main' into feat/stepper
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// Highlighted sidebar items
|
||||
export const NEW = ['Target Cursor', 'Ripple Grid', 'Magic Bento', 'Galaxy', 'Text Type', 'Glass Surface', 'Sticker Peel'];
|
||||
export const NEW = ['Target Cursor', 'Ripple Grid', 'Magic Bento', 'Galaxy', 'Text Type', 'Glass Surface', 'Sticker Peel', 'Scroll Stack', 'Faulty Terminal'];
|
||||
export const UPDATED = [];
|
||||
|
||||
// Used for main sidebar navigation
|
||||
@@ -27,7 +27,8 @@ export const CATEGORIES = [
|
||||
'Rotating Text',
|
||||
'Glitch Text',
|
||||
'Scroll Velocity',
|
||||
'Text Type'
|
||||
'Text Type',
|
||||
'Variable Proximity',
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -63,6 +64,7 @@ export const CATEGORIES = [
|
||||
'Masonry',
|
||||
'Glass Surface',
|
||||
'Magic Bento',
|
||||
'Scroll Stack',
|
||||
'Profile Card',
|
||||
'Dock',
|
||||
'Gooey Nav',
|
||||
@@ -81,7 +83,10 @@ export const CATEGORIES = [
|
||||
'Elastic Slider',
|
||||
'Stack',
|
||||
'Chroma Grid',
|
||||
'Stepper'
|
||||
'Stepper',
|
||||
'Bounce Cards',
|
||||
'Counter',
|
||||
'Rolling Gallery'
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -93,6 +98,7 @@ export const CATEGORIES = [
|
||||
'Dither',
|
||||
'Dot Grid',
|
||||
'Hyperspeed',
|
||||
'Faulty Terminal',
|
||||
'Ripple Grid',
|
||||
'Silk',
|
||||
'Lightning',
|
||||
@@ -109,6 +115,7 @@ export const CATEGORIES = [
|
||||
'Liquid Chrome',
|
||||
'Grid Distortion',
|
||||
'Galaxy',
|
||||
'Light Rays',
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -44,6 +44,7 @@ const textAnimations = {
|
||||
'glitch-text': () => import("../demo/TextAnimations/GlitchTextDemo.vue"),
|
||||
'scroll-velocity': () => import("../demo/TextAnimations/ScrollVelocityDemo.vue"),
|
||||
'text-type': () => import("../demo/TextAnimations/TextTypeDemo.vue"),
|
||||
'variable-proximity': () => import("../demo/TextAnimations/VariableProximityDemo.vue"),
|
||||
};
|
||||
|
||||
const components = {
|
||||
@@ -70,6 +71,10 @@ const components = {
|
||||
'stack': () => import('../demo/Components/StackDemo.vue'),
|
||||
'chroma-grid': () => import('../demo/Components/ChromaGridDemo.vue'),
|
||||
'stepper': () => import('../demo/Components/StepperDemo.vue'),
|
||||
'bounce-cards': () => import('../demo/Components/BounceCardsDemo.vue'),
|
||||
'counter': () => import('../demo/Components/CounterDemo.vue'),
|
||||
'rolling-gallery': () => import('../demo/Components/RollingGalleryDemo.vue'),
|
||||
'scroll-stack': () => import('../demo/Components/ScrollStackDemo.vue'),
|
||||
};
|
||||
|
||||
const backgrounds = {
|
||||
@@ -95,6 +100,8 @@ const backgrounds = {
|
||||
'grid-distortion': () => import('../demo/Backgrounds/GridDistortionDemo.vue'),
|
||||
'ripple-grid': () => import('../demo/Backgrounds/RippleGridDemo.vue'),
|
||||
'galaxy': () => import('../demo/Backgrounds/GalaxyDemo.vue'),
|
||||
'faulty-terminal': () => import('../demo/Backgrounds/FaultyTerminalDemo.vue'),
|
||||
'light-rays': () => import('../demo/Backgrounds/LightRaysDemo.vue'),
|
||||
};
|
||||
|
||||
export const componentMap = {
|
||||
|
||||
33
src/constants/code/Backgrounds/faultyTerminalCode.ts
Normal file
33
src/constants/code/Backgrounds/faultyTerminalCode.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import code from '@content/Backgrounds/FaultyTerminal/FaultyTerminal.vue?raw';
|
||||
import { createCodeObject } from '../../../types/code';
|
||||
|
||||
export const faultyTerminal = createCodeObject(code, 'Backgrounds/FaultyTerminal', {
|
||||
installation: `npm install ogl`,
|
||||
usage: `<template>
|
||||
<div class="relative w-full h-[600px]">
|
||||
<FaultyTerminal
|
||||
:scale="1.5"
|
||||
:grid-mul="[2, 1]"
|
||||
:digit-size="1.2"
|
||||
:time-scale="1"
|
||||
:pause="false"
|
||||
:scanline-intensity="1"
|
||||
:glitch-amount="1"
|
||||
:flicker-amount="1"
|
||||
:noise-amp="1"
|
||||
:chromatic-aberration="0"
|
||||
:dither="0"
|
||||
:curvature="0"
|
||||
tint="#ffffff"
|
||||
:mouse-react="true"
|
||||
:mouse-strength="0.5"
|
||||
:page-load-animation="false"
|
||||
:brightness="1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FaultyTerminal from "./FaultyTerminal.vue";
|
||||
</script>`
|
||||
});
|
||||
26
src/constants/code/Backgrounds/lightRaysCode.ts
Normal file
26
src/constants/code/Backgrounds/lightRaysCode.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import code from '@content/Backgrounds/LightRays/LightRays.vue?raw';
|
||||
import { createCodeObject } from '@/types/code';
|
||||
|
||||
export const lightRays = createCodeObject(code, 'Backgrounds/LightRays', {
|
||||
installation: `npm install ogl`,
|
||||
usage: `<template>
|
||||
<div class="w-full h-[600px] relative">
|
||||
<LightRays
|
||||
rays-origin="top-center"
|
||||
rays-color="#00ffff"
|
||||
:rays-speed="1.5"
|
||||
:light-spread="0.8"
|
||||
:ray-length="1.2"
|
||||
:follow-mouse="true"
|
||||
:mouse-influence="0.1"
|
||||
:noise-amount="0.1"
|
||||
:distortion="0.05"
|
||||
class-name="custom-rays"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LightRays from "./LightRays.vue";
|
||||
</script>`
|
||||
});
|
||||
39
src/constants/code/Components/bounceCardsCode.ts
Normal file
39
src/constants/code/Components/bounceCardsCode.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import code from '@content/Components/BounceCards/BounceCards.vue?raw';
|
||||
import { createCodeObject } from '@/types/code';
|
||||
|
||||
export const bounceCards = createCodeObject(code, 'Components/BounceCards', {
|
||||
installation: `npm install gsap`,
|
||||
usage: `<template>
|
||||
<BounceCards
|
||||
:images="images"
|
||||
:container-width="500"
|
||||
:container-height="250"
|
||||
:animation-delay="0.5"
|
||||
:animation-stagger="0.06"
|
||||
ease-type="elastic.out(1, 0.8)"
|
||||
:transform-styles="transformStyles"
|
||||
:enable-hover="true"
|
||||
class="custom-bounce-cards"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BounceCards from "./BounceCards.vue";
|
||||
|
||||
const images = [
|
||||
'https://picsum.photos/400/400?grayscale',
|
||||
'https://picsum.photos/500/500?grayscale',
|
||||
'https://picsum.photos/600/600?grayscale',
|
||||
'https://picsum.photos/700/700?grayscale',
|
||||
'https://picsum.photos/300/300?grayscale'
|
||||
];
|
||||
|
||||
const transformStyles = [
|
||||
'rotate(5deg) translate(-150px)',
|
||||
'rotate(0deg) translate(-70px)',
|
||||
'rotate(-5deg)',
|
||||
'rotate(5deg) translate(70px)',
|
||||
'rotate(-5deg) translate(150px)'
|
||||
];
|
||||
</script>`
|
||||
});
|
||||
17
src/constants/code/Components/counterCode.ts
Normal file
17
src/constants/code/Components/counterCode.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import code from '@content/Components/Counter/Counter.vue?raw';
|
||||
import { createCodeObject } from '../../../types/code';
|
||||
|
||||
export const counter = createCodeObject(code, 'Components/Counter', {
|
||||
installation: `npm i motion-v`,
|
||||
usage: `import Counter from './Counter.vue'
|
||||
|
||||
<Counter
|
||||
:value="1"
|
||||
:places="[100, 10, 1]"
|
||||
:fontSize="80"
|
||||
:padding="5"
|
||||
:gap="10"
|
||||
textColor="white"
|
||||
:fontWeight="900"
|
||||
/>`
|
||||
});
|
||||
26
src/constants/code/Components/rollingGalleryCode.ts
Normal file
26
src/constants/code/Components/rollingGalleryCode.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import code from '@content/Components/RollingGallery/RollingGallery.vue?raw';
|
||||
import { createCodeObject } from '@/types/code.ts';
|
||||
|
||||
export const rollingGallery = createCodeObject(code, 'Components/RollingGallery', {
|
||||
installation: `npm install motion-v`,
|
||||
usage: `<template>
|
||||
<RollingGallery
|
||||
:autoplay="true"
|
||||
:pause-on-hover="true"
|
||||
:images="customImages"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import RollingGallery from "./RollingGallery.vue";
|
||||
|
||||
const customImages = [
|
||||
"https://images.unsplash.com/photo-1528181304800-259b08848526?q=80&w=3870&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
"https://images.unsplash.com/photo-1506665531195-3566af2b4dfa?q=80&w=3870&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
"https://images.unsplash.com/photo-1520250497591-112f2f40a3f4?q=80&w=3456&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
"https://images.unsplash.com/photo-1495103033382-fe343886b671?q=80&w=3870&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
"https://images.unsplash.com/photo-1506781961370-37a89d6b3095?q=80&w=3264&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
// Add more images as needed
|
||||
];
|
||||
</script>`
|
||||
});
|
||||
26
src/constants/code/Components/scrollStackCode.ts
Normal file
26
src/constants/code/Components/scrollStackCode.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import code from '@content/Components/ScrollStack/ScrollStack.vue?raw';
|
||||
import { createCodeObject } from '../../../types/code';
|
||||
|
||||
export const scrollStack = createCodeObject(code, 'Components/ScrollStack', {
|
||||
installation: `npm install lenis`,
|
||||
usage: `<template>
|
||||
<ScrollStack>
|
||||
<ScrollStackItem>
|
||||
<h2>Card 1</h2>
|
||||
<p>This is the first card in the stack</p>
|
||||
</ScrollStackItem>
|
||||
<ScrollStackItem>
|
||||
<h2>Card 2</h2>
|
||||
<p>This is the second card in the stack</p>
|
||||
</ScrollStackItem>
|
||||
<ScrollStackItem>
|
||||
<h2>Card 3</h2>
|
||||
<p>This is the third card in the stack</p>
|
||||
</ScrollStackItem>
|
||||
</ScrollStack>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ScrollStack, { ScrollStackItem } from "./ScrollStack.vue";
|
||||
</script>`
|
||||
});
|
||||
26
src/constants/code/TextAnimations/variableProximityCode.ts
Normal file
26
src/constants/code/TextAnimations/variableProximityCode.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import code from '@content/TextAnimations/VariableProximity/VariableProximity.vue?raw';
|
||||
import { createCodeObject } from '@/types/code';
|
||||
|
||||
export const variableProximity = createCodeObject(code, 'TextAnimations/VariableProximity', {
|
||||
installation: `npm install motion-v`,
|
||||
usage: `<template>
|
||||
<div ref="containerRef" class="relative min-h-[400px] p-4">
|
||||
<VariableProximity
|
||||
label="Hover me! Variable font magic!"
|
||||
from-font-variation-settings="'wght' 400, 'opsz' 9"
|
||||
to-font-variation-settings="'wght' 1000, 'opsz' 40"
|
||||
:container-ref="containerRef"
|
||||
:radius="100"
|
||||
falloff="linear"
|
||||
class-name="text-4xl font-bold text-center"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import VariableProximity, { type FalloffType } from './VariableProximity.vue';
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null);
|
||||
</script>`
|
||||
});
|
||||
439
src/content/Backgrounds/FaultyTerminal/FaultyTerminal.vue
Normal file
439
src/content/Backgrounds/FaultyTerminal/FaultyTerminal.vue
Normal file
@@ -0,0 +1,439 @@
|
||||
<script setup lang="ts">
|
||||
import { Color, Mesh, Program, Renderer, Triangle } from 'ogl';
|
||||
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||
|
||||
type Vec2 = [number, number];
|
||||
|
||||
interface FaultyTerminalProps {
|
||||
scale?: number;
|
||||
gridMul?: Vec2;
|
||||
digitSize?: number;
|
||||
timeScale?: number;
|
||||
pause?: boolean;
|
||||
scanlineIntensity?: number;
|
||||
glitchAmount?: number;
|
||||
flickerAmount?: number;
|
||||
noiseAmp?: number;
|
||||
chromaticAberration?: number;
|
||||
dither?: number | boolean;
|
||||
curvature?: number;
|
||||
tint?: string;
|
||||
mouseReact?: boolean;
|
||||
mouseStrength?: number;
|
||||
dpr?: number;
|
||||
pageLoadAnimation?: boolean;
|
||||
brightness?: number;
|
||||
className?: string;
|
||||
style?: Record<string, string | number>;
|
||||
}
|
||||
|
||||
const vertexShader = `
|
||||
attribute vec2 position;
|
||||
attribute vec2 uv;
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const fragmentShader = `
|
||||
precision mediump float;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
uniform float iTime;
|
||||
uniform vec3 iResolution;
|
||||
uniform float uScale;
|
||||
|
||||
uniform vec2 uGridMul;
|
||||
uniform float uDigitSize;
|
||||
uniform float uScanlineIntensity;
|
||||
uniform float uGlitchAmount;
|
||||
uniform float uFlickerAmount;
|
||||
uniform float uNoiseAmp;
|
||||
uniform float uChromaticAberration;
|
||||
uniform float uDither;
|
||||
uniform float uCurvature;
|
||||
uniform vec3 uTint;
|
||||
uniform vec2 uMouse;
|
||||
uniform float uMouseStrength;
|
||||
uniform float uUseMouse;
|
||||
uniform float uPageLoadProgress;
|
||||
uniform float uUsePageLoadAnimation;
|
||||
uniform float uBrightness;
|
||||
|
||||
float time;
|
||||
|
||||
float hash21(vec2 p){
|
||||
p = fract(p * 234.56);
|
||||
p += dot(p, p + 34.56);
|
||||
return fract(p.x * p.y);
|
||||
}
|
||||
|
||||
float noise(vec2 p)
|
||||
{
|
||||
return sin(p.x * 10.0) * sin(p.y * (3.0 + sin(time * 0.090909))) + 0.2;
|
||||
}
|
||||
|
||||
mat2 rotate(float angle)
|
||||
{
|
||||
float c = cos(angle);
|
||||
float s = sin(angle);
|
||||
return mat2(c, -s, s, c);
|
||||
}
|
||||
|
||||
float fbm(vec2 p)
|
||||
{
|
||||
p *= 1.1;
|
||||
float f = 0.0;
|
||||
float amp = 0.5 * uNoiseAmp;
|
||||
|
||||
mat2 modify0 = rotate(time * 0.02);
|
||||
f += amp * noise(p);
|
||||
p = modify0 * p * 2.0;
|
||||
amp *= 0.454545; // 1/2.2
|
||||
|
||||
mat2 modify1 = rotate(time * 0.02);
|
||||
f += amp * noise(p);
|
||||
p = modify1 * p * 2.0;
|
||||
amp *= 0.454545;
|
||||
|
||||
mat2 modify2 = rotate(time * 0.08);
|
||||
f += amp * noise(p);
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
float pattern(vec2 p, out vec2 q, out vec2 r) {
|
||||
vec2 offset1 = vec2(1.0);
|
||||
vec2 offset0 = vec2(0.0);
|
||||
mat2 rot01 = rotate(0.1 * time);
|
||||
mat2 rot1 = rotate(0.1);
|
||||
|
||||
q = vec2(fbm(p + offset1), fbm(rot01 * p + offset1));
|
||||
r = vec2(fbm(rot1 * q + offset0), fbm(q + offset0));
|
||||
return fbm(p + r);
|
||||
}
|
||||
|
||||
float digit(vec2 p){
|
||||
vec2 grid = uGridMul * 15.0;
|
||||
vec2 s = floor(p * grid) / grid;
|
||||
p = p * grid;
|
||||
vec2 q, r;
|
||||
float intensity = pattern(s * 0.1, q, r) * 1.3 - 0.03;
|
||||
|
||||
if(uUseMouse > 0.5){
|
||||
vec2 mouseWorld = uMouse * uScale;
|
||||
float distToMouse = distance(s, mouseWorld);
|
||||
float mouseInfluence = exp(-distToMouse * 8.0) * uMouseStrength * 10.0;
|
||||
intensity += mouseInfluence;
|
||||
|
||||
float ripple = sin(distToMouse * 20.0 - iTime * 5.0) * 0.1 * mouseInfluence;
|
||||
intensity += ripple;
|
||||
}
|
||||
|
||||
if(uUsePageLoadAnimation > 0.5){
|
||||
float cellRandom = fract(sin(dot(s, vec2(12.9898, 78.233))) * 43758.5453);
|
||||
float cellDelay = cellRandom * 0.8;
|
||||
float cellProgress = clamp((uPageLoadProgress - cellDelay) / 0.2, 0.0, 1.0);
|
||||
|
||||
float fadeAlpha = smoothstep(0.0, 1.0, cellProgress);
|
||||
intensity *= fadeAlpha;
|
||||
}
|
||||
|
||||
p = fract(p);
|
||||
p *= uDigitSize;
|
||||
|
||||
float px5 = p.x * 5.0;
|
||||
float py5 = (1.0 - p.y) * 5.0;
|
||||
float x = fract(px5);
|
||||
float y = fract(py5);
|
||||
|
||||
float i = floor(py5) - 2.0;
|
||||
float j = floor(px5) - 2.0;
|
||||
float n = i * i + j * j;
|
||||
float f = n * 0.0625;
|
||||
|
||||
float isOn = step(0.1, intensity - f);
|
||||
float brightness = isOn * (0.2 + y * 0.8) * (0.75 + x * 0.25);
|
||||
|
||||
return step(0.0, p.x) * step(p.x, 1.0) * step(0.0, p.y) * step(p.y, 1.0) * brightness;
|
||||
}
|
||||
|
||||
float onOff(float a, float b, float c)
|
||||
{
|
||||
return step(c, sin(iTime + a * cos(iTime * b))) * uFlickerAmount;
|
||||
}
|
||||
|
||||
float displace(vec2 look)
|
||||
{
|
||||
float y = look.y - mod(iTime * 0.25, 1.0);
|
||||
float window = 1.0 / (1.0 + 50.0 * y * y);
|
||||
return sin(look.y * 20.0 + iTime) * 0.0125 * onOff(4.0, 2.0, 0.8) * (1.0 + cos(iTime * 60.0)) * window;
|
||||
}
|
||||
|
||||
vec3 getColor(vec2 p){
|
||||
|
||||
float bar = step(mod(p.y + time * 20.0, 1.0), 0.2) * 0.4 + 1.0; // more efficient than ternary
|
||||
bar *= uScanlineIntensity;
|
||||
|
||||
float displacement = displace(p);
|
||||
p.x += displacement;
|
||||
|
||||
if (uGlitchAmount != 1.0) {
|
||||
float extra = displacement * (uGlitchAmount - 1.0);
|
||||
p.x += extra;
|
||||
}
|
||||
|
||||
float middle = digit(p);
|
||||
|
||||
const float off = 0.002;
|
||||
float sum = digit(p + vec2(-off, -off)) + digit(p + vec2(0.0, -off)) + digit(p + vec2(off, -off)) +
|
||||
digit(p + vec2(-off, 0.0)) + digit(p + vec2(0.0, 0.0)) + digit(p + vec2(off, 0.0)) +
|
||||
digit(p + vec2(-off, off)) + digit(p + vec2(0.0, off)) + digit(p + vec2(off, off));
|
||||
|
||||
vec3 baseColor = vec3(0.9) * middle + sum * 0.1 * vec3(1.0) * bar;
|
||||
return baseColor;
|
||||
}
|
||||
|
||||
vec2 barrel(vec2 uv){
|
||||
vec2 c = uv * 2.0 - 1.0;
|
||||
float r2 = dot(c, c);
|
||||
c *= 1.0 + uCurvature * r2;
|
||||
return c * 0.5 + 0.5;
|
||||
}
|
||||
|
||||
void main() {
|
||||
time = iTime * 0.333333;
|
||||
vec2 uv = vUv;
|
||||
|
||||
if(uCurvature != 0.0){
|
||||
uv = barrel(uv);
|
||||
}
|
||||
|
||||
vec2 p = uv * uScale;
|
||||
vec3 col = getColor(p);
|
||||
|
||||
if(uChromaticAberration != 0.0){
|
||||
vec2 ca = vec2(uChromaticAberration) / iResolution.xy;
|
||||
col.r = getColor(p + ca).r;
|
||||
col.b = getColor(p - ca).b;
|
||||
}
|
||||
|
||||
col *= uTint;
|
||||
col *= uBrightness;
|
||||
|
||||
if(uDither > 0.0){
|
||||
float rnd = hash21(gl_FragCoord.xy);
|
||||
col += (rnd - 0.5) * (uDither * 0.003922);
|
||||
}
|
||||
|
||||
gl_FragColor = vec4(col, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
function hexToRgb(hex: string): [number, number, number] {
|
||||
let h = hex.replace('#', '').trim();
|
||||
if (h.length === 3)
|
||||
h = h
|
||||
.split('')
|
||||
.map(c => c + c)
|
||||
.join('');
|
||||
const num = parseInt(h, 16);
|
||||
return [((num >> 16) & 255) / 255, ((num >> 8) & 255) / 255, (num & 255) / 255];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<FaultyTerminalProps>(), {
|
||||
scale: 1,
|
||||
gridMul: () => [2, 1],
|
||||
digitSize: 1.5,
|
||||
timeScale: 0.3,
|
||||
pause: false,
|
||||
scanlineIntensity: 0.3,
|
||||
glitchAmount: 1,
|
||||
flickerAmount: 1,
|
||||
noiseAmp: 1,
|
||||
chromaticAberration: 0,
|
||||
dither: 0,
|
||||
curvature: 0.2,
|
||||
tint: '#ffffff',
|
||||
mouseReact: true,
|
||||
mouseStrength: 0.2,
|
||||
dpr: Math.min(window.devicePixelRatio || 1, 2),
|
||||
pageLoadAnimation: true,
|
||||
brightness: 1,
|
||||
className: '',
|
||||
style: () => ({})
|
||||
});
|
||||
|
||||
const containerRef = useTemplateRef('containerRef');
|
||||
const programRef = ref<Program | null>(null);
|
||||
const rendererRef = ref<Renderer | null>(null);
|
||||
const mouseRef = ref({ x: 0.5, y: 0.5 });
|
||||
const smoothMouseRef = ref({ x: 0.5, y: 0.5 });
|
||||
const frozenTimeRef = ref(0);
|
||||
const rafRef = ref<number>(0);
|
||||
const loadAnimationStartRef = ref<number>(0);
|
||||
const timeOffsetRef = ref<number>(Math.random() * 100);
|
||||
|
||||
const tintVec = computed(() => hexToRgb(props.tint));
|
||||
|
||||
const ditherValue = computed(() => (typeof props.dither === 'boolean' ? (props.dither ? 1 : 0) : props.dither));
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const ctn = containerRef.value;
|
||||
if (!ctn) return;
|
||||
const rect = ctn.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) / rect.width;
|
||||
const y = 1 - (e.clientY - rect.top) / rect.height;
|
||||
mouseRef.value = { x, y };
|
||||
};
|
||||
|
||||
let cleanup: (() => void) | null = null;
|
||||
const setup = () => {
|
||||
const ctn = containerRef.value;
|
||||
if (!ctn) return;
|
||||
|
||||
const renderer = new Renderer({ dpr: props.dpr });
|
||||
rendererRef.value = renderer;
|
||||
const gl = renderer.gl;
|
||||
gl.clearColor(0, 0, 0, 1);
|
||||
|
||||
const geometry = new Triangle(gl);
|
||||
|
||||
const program = new Program(gl, {
|
||||
vertex: vertexShader,
|
||||
fragment: fragmentShader,
|
||||
uniforms: {
|
||||
iTime: { value: 0 },
|
||||
iResolution: {
|
||||
value: new Color(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height)
|
||||
},
|
||||
uScale: { value: props.scale },
|
||||
|
||||
uGridMul: { value: new Float32Array(props.gridMul) },
|
||||
uDigitSize: { value: props.digitSize },
|
||||
uScanlineIntensity: { value: props.scanlineIntensity },
|
||||
uGlitchAmount: { value: props.glitchAmount },
|
||||
uFlickerAmount: { value: props.flickerAmount },
|
||||
uNoiseAmp: { value: props.noiseAmp },
|
||||
uChromaticAberration: { value: props.chromaticAberration },
|
||||
uDither: { value: ditherValue },
|
||||
uCurvature: { value: props.curvature },
|
||||
uTint: { value: new Color(tintVec.value[0], tintVec.value[1], tintVec.value[2]) },
|
||||
uMouse: {
|
||||
value: new Float32Array([smoothMouseRef.value.x, smoothMouseRef.value.y])
|
||||
},
|
||||
uMouseStrength: { value: props.mouseStrength },
|
||||
uUseMouse: { value: props.mouseReact ? 1 : 0 },
|
||||
uPageLoadProgress: { value: props.pageLoadAnimation ? 0 : 1 },
|
||||
uUsePageLoadAnimation: { value: props.pageLoadAnimation ? 1 : 0 },
|
||||
uBrightness: { value: props.brightness }
|
||||
}
|
||||
});
|
||||
programRef.value = program;
|
||||
|
||||
const mesh = new Mesh(gl, { geometry, program });
|
||||
|
||||
function resize() {
|
||||
if (!ctn || !renderer) return;
|
||||
renderer.setSize(ctn.offsetWidth, ctn.offsetHeight);
|
||||
program.uniforms.iResolution.value = new Color(
|
||||
gl.canvas.width,
|
||||
gl.canvas.height,
|
||||
gl.canvas.width / gl.canvas.height
|
||||
);
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => resize());
|
||||
resizeObserver.observe(ctn);
|
||||
resize();
|
||||
|
||||
const update = (t: number) => {
|
||||
rafRef.value = requestAnimationFrame(update);
|
||||
|
||||
if (props.pageLoadAnimation && loadAnimationStartRef.value === 0) {
|
||||
loadAnimationStartRef.value = t;
|
||||
}
|
||||
|
||||
if (!props.pause) {
|
||||
const elapsed = (t * 0.001 + timeOffsetRef.value) * props.timeScale;
|
||||
program.uniforms.iTime.value = elapsed;
|
||||
frozenTimeRef.value = elapsed;
|
||||
} else {
|
||||
program.uniforms.iTime.value = frozenTimeRef.value;
|
||||
}
|
||||
|
||||
if (props.pageLoadAnimation && loadAnimationStartRef.value > 0) {
|
||||
const animationDuration = 2000;
|
||||
const animationElapsed = t - loadAnimationStartRef.value;
|
||||
const progress = Math.min(animationElapsed / animationDuration, 1);
|
||||
program.uniforms.uPageLoadProgress.value = progress;
|
||||
}
|
||||
|
||||
if (props.mouseReact) {
|
||||
const dampingFactor = 0.08;
|
||||
const smoothMouse = smoothMouseRef.value;
|
||||
const mouse = mouseRef.value;
|
||||
smoothMouse.x += (mouse.x - smoothMouse.x) * dampingFactor;
|
||||
smoothMouse.y += (mouse.y - smoothMouse.y) * dampingFactor;
|
||||
|
||||
const mouseUniform = program.uniforms.uMouse.value as Float32Array;
|
||||
mouseUniform[0] = smoothMouse.x;
|
||||
mouseUniform[1] = smoothMouse.y;
|
||||
}
|
||||
|
||||
renderer.render({ scene: mesh });
|
||||
};
|
||||
rafRef.value = requestAnimationFrame(update);
|
||||
ctn.appendChild(gl.canvas);
|
||||
|
||||
if (props.mouseReact) ctn.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
cleanup = () => {
|
||||
cancelAnimationFrame(rafRef.value);
|
||||
resizeObserver.disconnect();
|
||||
if (props.mouseReact) ctn.removeEventListener('mousemove', handleMouseMove);
|
||||
if (gl.canvas.parentElement === ctn) ctn.removeChild(gl.canvas);
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
||||
loadAnimationStartRef.value = 0;
|
||||
timeOffsetRef.value = Math.random() * 100;
|
||||
};
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const ctn = containerRef.value;
|
||||
if (ctn) {
|
||||
setup();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (cleanup) {
|
||||
cleanup();
|
||||
cleanup = null;
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props,
|
||||
() => {
|
||||
if (cleanup) {
|
||||
cleanup();
|
||||
cleanup = null;
|
||||
}
|
||||
setup();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
:class="['w-full h-full relative overflow-hidden', className]"
|
||||
:style="style"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</template>
|
||||
525
src/content/Backgrounds/LightRays/LightRays.vue
Normal file
525
src/content/Backgrounds/LightRays/LightRays.vue
Normal file
@@ -0,0 +1,525 @@
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
:class="[
|
||||
'w-full h-full relative pointer-events-none z-[3] overflow-hidden',
|
||||
className
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, useTemplateRef, computed, nextTick } from 'vue';
|
||||
import { Renderer, Program, Triangle, Mesh } from 'ogl';
|
||||
|
||||
export type RaysOrigin =
|
||||
| 'top-center'
|
||||
| 'top-left'
|
||||
| 'top-right'
|
||||
| 'right'
|
||||
| 'left'
|
||||
| 'bottom-center'
|
||||
| 'bottom-right'
|
||||
| 'bottom-left';
|
||||
|
||||
interface LightRaysProps {
|
||||
raysOrigin?: RaysOrigin;
|
||||
raysColor?: string;
|
||||
raysSpeed?: number;
|
||||
lightSpread?: number;
|
||||
rayLength?: number;
|
||||
pulsating?: boolean;
|
||||
fadeDistance?: number;
|
||||
saturation?: number;
|
||||
followMouse?: boolean;
|
||||
mouseInfluence?: number;
|
||||
noiseAmount?: number;
|
||||
distortion?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface MousePosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface AnchorAndDirection {
|
||||
anchor: [number, number];
|
||||
dir: [number, number];
|
||||
}
|
||||
|
||||
interface WebGLUniforms {
|
||||
iTime: { value: number };
|
||||
iResolution: { value: [number, number] };
|
||||
rayPos: { value: [number, number] };
|
||||
rayDir: { value: [number, number] };
|
||||
raysColor: { value: [number, number, number] };
|
||||
raysSpeed: { value: number };
|
||||
lightSpread: { value: number };
|
||||
rayLength: { value: number };
|
||||
pulsating: { value: number };
|
||||
fadeDistance: { value: number };
|
||||
saturation: { value: number };
|
||||
mousePos: { value: [number, number] };
|
||||
mouseInfluence: { value: number };
|
||||
noiseAmount: { value: number };
|
||||
distortion: { value: number };
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<LightRaysProps>(), {
|
||||
raysOrigin: 'top-center',
|
||||
raysColor: '#ffffff',
|
||||
raysSpeed: 1,
|
||||
lightSpread: 1,
|
||||
rayLength: 2,
|
||||
pulsating: false,
|
||||
fadeDistance: 1.0,
|
||||
saturation: 1.0,
|
||||
followMouse: true,
|
||||
mouseInfluence: 0.1,
|
||||
noiseAmount: 0.0,
|
||||
distortion: 0.0,
|
||||
className: ''
|
||||
});
|
||||
|
||||
const containerRef = useTemplateRef<HTMLDivElement>('containerRef');
|
||||
|
||||
const uniformsRef = ref<WebGLUniforms | null>(null);
|
||||
const rendererRef = ref<Renderer | null>(null);
|
||||
const mouseRef = ref<MousePosition>({ x: 0.5, y: 0.5 });
|
||||
const smoothMouseRef = ref<MousePosition>({ x: 0.5, y: 0.5 });
|
||||
const animationIdRef = ref<number | null>(null);
|
||||
const meshRef = ref<Mesh | null>(null);
|
||||
const cleanupFunctionRef = ref<(() => void) | null>(null);
|
||||
const isVisible = ref<boolean>(false);
|
||||
const observerRef = ref<IntersectionObserver | null>(null);
|
||||
const resizeTimeoutRef = ref<number | null>(null);
|
||||
|
||||
const rgbColor = computed<[number, number, number]>(() => hexToRgb(props.raysColor));
|
||||
const pulsatingValue = computed<number>(() => props.pulsating ? 1.0 : 0.0);
|
||||
const devicePixelRatio = computed<number>(() => Math.min(window.devicePixelRatio || 1, 2));
|
||||
|
||||
const hexToRgb = (hex: string): [number, number, number] => {
|
||||
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return m
|
||||
? [
|
||||
parseInt(m[1], 16) / 255,
|
||||
parseInt(m[2], 16) / 255,
|
||||
parseInt(m[3], 16) / 255,
|
||||
]
|
||||
: [1, 1, 1];
|
||||
};
|
||||
|
||||
const getAnchorAndDir = (origin: RaysOrigin, w: number, h: number): AnchorAndDirection => {
|
||||
const outside = 0.2;
|
||||
switch (origin) {
|
||||
case 'top-left':
|
||||
return { anchor: [0, -outside * h], dir: [0, 1] };
|
||||
case 'top-right':
|
||||
return { anchor: [w, -outside * h], dir: [0, 1] };
|
||||
case 'left':
|
||||
return { anchor: [-outside * w, 0.5 * h], dir: [1, 0] };
|
||||
case 'right':
|
||||
return { anchor: [(1 + outside) * w, 0.5 * h], dir: [-1, 0] };
|
||||
case 'bottom-left':
|
||||
return { anchor: [0, (1 + outside) * h], dir: [0, -1] };
|
||||
case 'bottom-center':
|
||||
return { anchor: [0.5 * w, (1 + outside) * h], dir: [0, -1] };
|
||||
case 'bottom-right':
|
||||
return { anchor: [w, (1 + outside) * h], dir: [0, -1] };
|
||||
default:
|
||||
return { anchor: [0.5 * w, -outside * h], dir: [0, 1] };
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedUpdatePlacement = (() => {
|
||||
let timeoutId: number | null = null;
|
||||
|
||||
return (updateFn: () => void): void => {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
timeoutId = window.setTimeout(() => {
|
||||
updateFn();
|
||||
timeoutId = null;
|
||||
}, 16);
|
||||
};
|
||||
})();
|
||||
|
||||
const vertexShader: string = `
|
||||
attribute vec2 position;
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = position * 0.5 + 0.5;
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}`;
|
||||
|
||||
const fragmentShader: string = `precision highp float;
|
||||
|
||||
uniform float iTime;
|
||||
uniform vec2 iResolution;
|
||||
|
||||
uniform vec2 rayPos;
|
||||
uniform vec2 rayDir;
|
||||
uniform vec3 raysColor;
|
||||
uniform float raysSpeed;
|
||||
uniform float lightSpread;
|
||||
uniform float rayLength;
|
||||
uniform float pulsating;
|
||||
uniform float fadeDistance;
|
||||
uniform float saturation;
|
||||
uniform vec2 mousePos;
|
||||
uniform float mouseInfluence;
|
||||
uniform float noiseAmount;
|
||||
uniform float distortion;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
float noise(vec2 st) {
|
||||
return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
|
||||
}
|
||||
|
||||
float rayStrength(vec2 raySource, vec2 rayRefDirection, vec2 coord,
|
||||
float seedA, float seedB, float speed) {
|
||||
vec2 sourceToCoord = coord - raySource;
|
||||
vec2 dirNorm = normalize(sourceToCoord);
|
||||
float cosAngle = dot(dirNorm, rayRefDirection);
|
||||
|
||||
float distortedAngle = cosAngle + distortion * sin(iTime * 2.0 + length(sourceToCoord) * 0.01) * 0.2;
|
||||
|
||||
float spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / max(lightSpread, 0.001));
|
||||
|
||||
float distance = length(sourceToCoord);
|
||||
float maxDistance = iResolution.x * rayLength;
|
||||
float lengthFalloff = clamp((maxDistance - distance) / maxDistance, 0.0, 1.0);
|
||||
|
||||
float fadeFalloff = clamp((iResolution.x * fadeDistance - distance) / (iResolution.x * fadeDistance), 0.5, 1.0);
|
||||
float pulse = pulsating > 0.5 ? (0.8 + 0.2 * sin(iTime * speed * 3.0)) : 1.0;
|
||||
|
||||
float baseStrength = clamp(
|
||||
(0.45 + 0.15 * sin(distortedAngle * seedA + iTime * speed)) +
|
||||
(0.3 + 0.2 * cos(-distortedAngle * seedB + iTime * speed)),
|
||||
0.0, 1.0
|
||||
);
|
||||
|
||||
return baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse;
|
||||
}
|
||||
|
||||
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
||||
vec2 coord = vec2(fragCoord.x, iResolution.y - fragCoord.y);
|
||||
|
||||
vec2 finalRayDir = rayDir;
|
||||
if (mouseInfluence > 0.0) {
|
||||
vec2 mouseScreenPos = mousePos * iResolution.xy;
|
||||
vec2 mouseDirection = normalize(mouseScreenPos - rayPos);
|
||||
finalRayDir = normalize(mix(rayDir, mouseDirection, mouseInfluence));
|
||||
}
|
||||
|
||||
vec4 rays1 = vec4(1.0) *
|
||||
rayStrength(rayPos, finalRayDir, coord, 36.2214, 21.11349,
|
||||
1.5 * raysSpeed);
|
||||
vec4 rays2 = vec4(1.0) *
|
||||
rayStrength(rayPos, finalRayDir, coord, 22.3991, 18.0234,
|
||||
1.1 * raysSpeed);
|
||||
|
||||
fragColor = rays1 * 0.5 + rays2 * 0.4;
|
||||
|
||||
if (noiseAmount > 0.0) {
|
||||
float n = noise(coord * 0.01 + iTime * 0.1);
|
||||
fragColor.rgb *= (1.0 - noiseAmount + noiseAmount * n);
|
||||
}
|
||||
|
||||
float brightness = 1.0 - (coord.y / iResolution.y);
|
||||
fragColor.x *= 0.1 + brightness * 0.8;
|
||||
fragColor.y *= 0.3 + brightness * 0.6;
|
||||
fragColor.z *= 0.5 + brightness * 0.5;
|
||||
|
||||
if (saturation != 1.0) {
|
||||
float gray = dot(fragColor.rgb, vec3(0.299, 0.587, 0.114));
|
||||
fragColor.rgb = mix(vec3(gray), fragColor.rgb, saturation);
|
||||
}
|
||||
|
||||
fragColor.rgb *= raysColor;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 color;
|
||||
mainImage(color, gl_FragCoord.xy);
|
||||
gl_FragColor = color;
|
||||
}`;
|
||||
|
||||
const initializeWebGL = async (): Promise<void> => {
|
||||
if (!containerRef.value) return;
|
||||
|
||||
await nextTick();
|
||||
|
||||
if (!containerRef.value) return;
|
||||
|
||||
try {
|
||||
const renderer = new Renderer({
|
||||
dpr: devicePixelRatio.value,
|
||||
alpha: true,
|
||||
antialias: false,
|
||||
powerPreference: 'high-performance'
|
||||
});
|
||||
rendererRef.value = renderer;
|
||||
|
||||
const gl = renderer.gl;
|
||||
gl.canvas.style.width = '100%';
|
||||
gl.canvas.style.height = '100%';
|
||||
|
||||
while (containerRef.value.firstChild) {
|
||||
containerRef.value.removeChild(containerRef.value.firstChild);
|
||||
}
|
||||
containerRef.value.appendChild(gl.canvas);
|
||||
|
||||
const uniforms: WebGLUniforms = {
|
||||
iTime: { value: 0 },
|
||||
iResolution: { value: [1, 1] },
|
||||
rayPos: { value: [0, 0] },
|
||||
rayDir: { value: [0, 1] },
|
||||
raysColor: { value: rgbColor.value },
|
||||
raysSpeed: { value: props.raysSpeed },
|
||||
lightSpread: { value: props.lightSpread },
|
||||
rayLength: { value: props.rayLength },
|
||||
pulsating: { value: pulsatingValue.value },
|
||||
fadeDistance: { value: props.fadeDistance },
|
||||
saturation: { value: props.saturation },
|
||||
mousePos: { value: [0.5, 0.5] },
|
||||
mouseInfluence: { value: props.mouseInfluence },
|
||||
noiseAmount: { value: props.noiseAmount },
|
||||
distortion: { value: props.distortion },
|
||||
};
|
||||
uniformsRef.value = uniforms;
|
||||
|
||||
const geometry = new Triangle(gl);
|
||||
const program = new Program(gl, {
|
||||
vertex: vertexShader,
|
||||
fragment: fragmentShader,
|
||||
uniforms,
|
||||
});
|
||||
const mesh = new Mesh(gl, { geometry, program });
|
||||
meshRef.value = mesh;
|
||||
|
||||
const updatePlacement = (): void => {
|
||||
if (!containerRef.value || !renderer) return;
|
||||
|
||||
renderer.dpr = devicePixelRatio.value;
|
||||
|
||||
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.value;
|
||||
renderer.setSize(wCSS, hCSS);
|
||||
|
||||
const dpr = renderer.dpr;
|
||||
const w = wCSS * dpr;
|
||||
const h = hCSS * dpr;
|
||||
|
||||
uniforms.iResolution.value = [w, h];
|
||||
|
||||
const { anchor, dir } = getAnchorAndDir(props.raysOrigin, w, h);
|
||||
uniforms.rayPos.value = anchor;
|
||||
uniforms.rayDir.value = dir;
|
||||
};
|
||||
|
||||
const loop = (t: number): void => {
|
||||
if (!rendererRef.value || !uniformsRef.value || !meshRef.value || !isVisible.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
uniforms.iTime.value = t * 0.001;
|
||||
|
||||
if (props.followMouse && props.mouseInfluence > 0.0) {
|
||||
const smoothing = 0.92;
|
||||
|
||||
smoothMouseRef.value.x =
|
||||
smoothMouseRef.value.x * smoothing +
|
||||
mouseRef.value.x * (1 - smoothing);
|
||||
smoothMouseRef.value.y =
|
||||
smoothMouseRef.value.y * smoothing +
|
||||
mouseRef.value.y * (1 - smoothing);
|
||||
|
||||
uniforms.mousePos.value = [
|
||||
smoothMouseRef.value.x,
|
||||
smoothMouseRef.value.y,
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
renderer.render({ scene: mesh });
|
||||
animationIdRef.value = requestAnimationFrame(loop);
|
||||
} catch (error) {
|
||||
console.warn('WebGL rendering error:', error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleResize = (): void => {
|
||||
debouncedUpdatePlacement(updatePlacement);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize, { passive: true });
|
||||
updatePlacement();
|
||||
animationIdRef.value = requestAnimationFrame(loop);
|
||||
|
||||
cleanupFunctionRef.value = (): void => {
|
||||
if (animationIdRef.value) {
|
||||
cancelAnimationFrame(animationIdRef.value);
|
||||
animationIdRef.value = null;
|
||||
}
|
||||
|
||||
window.removeEventListener('resize', handleResize);
|
||||
|
||||
if (resizeTimeoutRef.value) {
|
||||
clearTimeout(resizeTimeoutRef.value);
|
||||
resizeTimeoutRef.value = null;
|
||||
}
|
||||
|
||||
if (renderer) {
|
||||
try {
|
||||
const canvas = renderer.gl.canvas;
|
||||
const loseContextExt =
|
||||
renderer.gl.getExtension('WEBGL_lose_context');
|
||||
if (loseContextExt) {
|
||||
loseContextExt.loseContext();
|
||||
}
|
||||
|
||||
if (canvas && canvas.parentNode) {
|
||||
canvas.parentNode.removeChild(canvas);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error during WebGL cleanup:', error);
|
||||
}
|
||||
}
|
||||
|
||||
rendererRef.value = null;
|
||||
uniformsRef.value = null;
|
||||
meshRef.value = null;
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize WebGL:', error);
|
||||
}
|
||||
};
|
||||
|
||||
let mouseThrottleId: number | null = null;
|
||||
const handleMouseMove = (e: MouseEvent): void => {
|
||||
if (!containerRef.value || !rendererRef.value) return;
|
||||
|
||||
if (mouseThrottleId) return;
|
||||
|
||||
mouseThrottleId = requestAnimationFrame(() => {
|
||||
if (!containerRef.value) return;
|
||||
|
||||
const rect = containerRef.value.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) / rect.width;
|
||||
const y = (e.clientY - rect.top) / rect.height;
|
||||
mouseRef.value = { x, y };
|
||||
mouseThrottleId = null;
|
||||
});
|
||||
};
|
||||
|
||||
onMounted((): void => {
|
||||
if (!containerRef.value) return;
|
||||
|
||||
observerRef.value = new IntersectionObserver(
|
||||
(entries: IntersectionObserverEntry[]): void => {
|
||||
const entry = entries[0];
|
||||
isVisible.value = entry.isIntersecting;
|
||||
},
|
||||
{
|
||||
threshold: 0.1,
|
||||
rootMargin: '50px'
|
||||
}
|
||||
);
|
||||
|
||||
observerRef.value.observe(containerRef.value);
|
||||
});
|
||||
|
||||
watch(isVisible, (newVisible: boolean): void => {
|
||||
if (newVisible && containerRef.value) {
|
||||
if (cleanupFunctionRef.value) {
|
||||
cleanupFunctionRef.value();
|
||||
cleanupFunctionRef.value = null;
|
||||
}
|
||||
initializeWebGL();
|
||||
} else if (!newVisible && cleanupFunctionRef.value) {
|
||||
if (animationIdRef.value) {
|
||||
cancelAnimationFrame(animationIdRef.value);
|
||||
animationIdRef.value = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
[
|
||||
() => props.raysColor,
|
||||
() => props.raysSpeed,
|
||||
() => props.lightSpread,
|
||||
() => props.raysOrigin,
|
||||
() => props.rayLength,
|
||||
() => props.pulsating,
|
||||
() => props.fadeDistance,
|
||||
() => props.saturation,
|
||||
() => props.mouseInfluence,
|
||||
() => props.noiseAmount,
|
||||
() => props.distortion,
|
||||
],
|
||||
(): void => {
|
||||
if (!uniformsRef.value || !containerRef.value || !rendererRef.value) return;
|
||||
|
||||
const u = uniformsRef.value;
|
||||
const renderer = rendererRef.value;
|
||||
|
||||
u.raysColor.value = rgbColor.value;
|
||||
u.raysSpeed.value = props.raysSpeed;
|
||||
u.lightSpread.value = props.lightSpread;
|
||||
u.rayLength.value = props.rayLength;
|
||||
u.pulsating.value = pulsatingValue.value;
|
||||
u.fadeDistance.value = props.fadeDistance;
|
||||
u.saturation.value = props.saturation;
|
||||
u.mouseInfluence.value = props.mouseInfluence;
|
||||
u.noiseAmount.value = props.noiseAmount;
|
||||
u.distortion.value = props.distortion;
|
||||
|
||||
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.value;
|
||||
const dpr = renderer.dpr;
|
||||
const { anchor, dir } = getAnchorAndDir(props.raysOrigin, wCSS * dpr, hCSS * dpr);
|
||||
u.rayPos.value = anchor;
|
||||
u.rayDir.value = dir;
|
||||
},
|
||||
{ flush: 'post' }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.followMouse,
|
||||
(newFollowMouse: boolean): void => {
|
||||
if (newFollowMouse) {
|
||||
window.addEventListener('mousemove', handleMouseMove, { passive: true });
|
||||
} else {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
if (mouseThrottleId) {
|
||||
cancelAnimationFrame(mouseThrottleId);
|
||||
mouseThrottleId = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onUnmounted((): void => {
|
||||
if (observerRef.value) {
|
||||
observerRef.value.disconnect();
|
||||
observerRef.value = null;
|
||||
}
|
||||
|
||||
if (cleanupFunctionRef.value) {
|
||||
cleanupFunctionRef.value();
|
||||
cleanupFunctionRef.value = null;
|
||||
}
|
||||
|
||||
if (mouseThrottleId) {
|
||||
cancelAnimationFrame(mouseThrottleId);
|
||||
mouseThrottleId = null;
|
||||
}
|
||||
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
});
|
||||
</script>
|
||||
196
src/content/Components/BounceCards/BounceCards.vue
Normal file
196
src/content/Components/BounceCards/BounceCards.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<div
|
||||
:class="['relative flex items-center justify-center', className]"
|
||||
:style="{
|
||||
width: typeof containerWidth === 'number' ? `${containerWidth}px` : containerWidth,
|
||||
height: typeof containerHeight === 'number' ? `${containerHeight}px` : containerHeight
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="(src, idx) in images"
|
||||
:key="idx"
|
||||
ref="cardRefs"
|
||||
class="absolute w-[200px] aspect-square border-[5px] border-white rounded-[25px] overflow-hidden shadow-[0_4px_10px_rgba(0,0,0,0.2)] bg-[#0b0b0b] opacity-0"
|
||||
:style="{ transform: transformStyles[idx] ?? 'none' }"
|
||||
@mouseenter="() => pushSiblings(idx)"
|
||||
@mouseleave="resetSiblings"
|
||||
>
|
||||
<div v-if="!imageLoaded[idx]" class="absolute inset-0 z-[1] bg-[#0b0b0b] overflow-hidden shimmer-container"></div>
|
||||
|
||||
<img
|
||||
class="absolute inset-0 w-full h-full object-cover z-[2] transition-opacity duration-700 ease-out"
|
||||
:src="src"
|
||||
:alt="`card-${idx}`"
|
||||
:style="{ opacity: imageLoaded[idx] ? 1 : 0 }"
|
||||
@load="() => onImageLoad(idx)"
|
||||
@error="() => onImageError(idx)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref, watch, nextTick } from 'vue';
|
||||
import { gsap } from 'gsap';
|
||||
|
||||
export interface BounceCardsProps {
|
||||
className?: string;
|
||||
images?: string[];
|
||||
containerWidth?: number | string;
|
||||
containerHeight?: number | string;
|
||||
animationDelay?: number;
|
||||
animationStagger?: number;
|
||||
easeType?: string;
|
||||
transformStyles?: string[];
|
||||
enableHover?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<BounceCardsProps>(), {
|
||||
className: '',
|
||||
images: () => [],
|
||||
containerWidth: 400,
|
||||
containerHeight: 400,
|
||||
animationDelay: 0.5,
|
||||
animationStagger: 0.06,
|
||||
easeType: 'elastic.out(1, 0.8)',
|
||||
transformStyles: () => [
|
||||
'rotate(10deg) translate(-170px)',
|
||||
'rotate(5deg) translate(-85px)',
|
||||
'rotate(-3deg)',
|
||||
'rotate(-10deg) translate(85px)',
|
||||
'rotate(2deg) translate(170px)'
|
||||
],
|
||||
enableHover: true
|
||||
});
|
||||
|
||||
const imageLoaded = ref(new Array(props.images.length).fill(false));
|
||||
const cardRefs = ref<HTMLElement[]>([]);
|
||||
|
||||
const getNoRotationTransform = (transformStr: string): string => {
|
||||
const hasRotate = /rotate\([\s\S]*?\)/.test(transformStr);
|
||||
if (hasRotate) {
|
||||
return transformStr.replace(/rotate\([\s\S]*?\)/, 'rotate(0deg)');
|
||||
} else if (transformStr === 'none') {
|
||||
return 'rotate(0deg)';
|
||||
} else {
|
||||
return `${transformStr} rotate(0deg)`;
|
||||
}
|
||||
};
|
||||
|
||||
const getPushedTransform = (baseTransform: string, offsetX: number): string => {
|
||||
const translateRegex = /translate\(([-0-9.]+)px\)/;
|
||||
const match = baseTransform.match(translateRegex);
|
||||
if (match) {
|
||||
const currentX = parseFloat(match[1]);
|
||||
const newX = currentX + offsetX;
|
||||
return baseTransform.replace(translateRegex, `translate(${newX}px)`);
|
||||
} else {
|
||||
return baseTransform === 'none' ? `translate(${offsetX}px)` : `${baseTransform} translate(${offsetX}px)`;
|
||||
}
|
||||
};
|
||||
|
||||
const pushSiblings = (hoveredIdx: number) => {
|
||||
if (!props.enableHover) return;
|
||||
|
||||
props.images.forEach((_, i) => {
|
||||
gsap.killTweensOf(cardRefs.value[i]);
|
||||
|
||||
const baseTransform = props.transformStyles[i] || 'none';
|
||||
|
||||
if (i === hoveredIdx) {
|
||||
const noRotationTransform = getNoRotationTransform(baseTransform);
|
||||
gsap.to(cardRefs.value[i], {
|
||||
transform: noRotationTransform,
|
||||
duration: 0.4,
|
||||
ease: 'back.out(1.4)',
|
||||
overwrite: 'auto'
|
||||
});
|
||||
} else {
|
||||
const offsetX = i < hoveredIdx ? -160 : 160;
|
||||
const pushedTransform = getPushedTransform(baseTransform, offsetX);
|
||||
const distance = Math.abs(hoveredIdx - i);
|
||||
const delay = distance * 0.05;
|
||||
|
||||
gsap.to(cardRefs.value[i], {
|
||||
transform: pushedTransform,
|
||||
duration: 0.4,
|
||||
ease: 'back.out(1.4)',
|
||||
delay,
|
||||
overwrite: 'auto'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const resetSiblings = () => {
|
||||
if (!props.enableHover) return;
|
||||
|
||||
props.images.forEach((_, i) => {
|
||||
gsap.killTweensOf(cardRefs.value[i]);
|
||||
const baseTransform = props.transformStyles[i] || 'none';
|
||||
gsap.to(cardRefs.value[i], {
|
||||
transform: baseTransform,
|
||||
duration: 0.4,
|
||||
ease: 'back.out(1.4)',
|
||||
overwrite: 'auto'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onImageLoad = (idx: number) => {
|
||||
imageLoaded.value[idx] = true;
|
||||
};
|
||||
|
||||
const onImageError = (idx: number) => {
|
||||
imageLoaded.value[idx] = true;
|
||||
};
|
||||
|
||||
const playEntranceAnimation = () => {
|
||||
gsap.killTweensOf(cardRefs.value);
|
||||
gsap.set(cardRefs.value, { opacity: 0, scale: 0 });
|
||||
|
||||
gsap.fromTo(
|
||||
cardRefs.value,
|
||||
{ scale: 0, opacity: 0 },
|
||||
{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
stagger: props.animationStagger,
|
||||
ease: props.easeType,
|
||||
delay: props.animationDelay
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
onMounted(playEntranceAnimation);
|
||||
watch(
|
||||
() => props.images,
|
||||
async () => {
|
||||
await nextTick();
|
||||
gsap.set(cardRefs.value, { opacity: 0, scale: 0 });
|
||||
playEntranceAnimation();
|
||||
}
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
gsap.killTweensOf(cardRefs.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.shimmer-container {
|
||||
background: linear-gradient(110deg, transparent 40%, rgba(255, 255, 255, 0.1) 50%, transparent 60%);
|
||||
background-size: 600% 600%;
|
||||
background-position: -600% 0;
|
||||
animation: shimmer-sweep 6s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer-sweep {
|
||||
0% {
|
||||
background-position: -600% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
161
src/content/Components/Counter/Counter.vue
Normal file
161
src/content/Components/Counter/Counter.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<div class="relative inline-block" :style="containerStyle">
|
||||
<div class="flex overflow-hidden" :style="counterStyles">
|
||||
<div v-for="place in places" :key="place" class="relative w-[1ch] tabular-nums" :style="digitStyles">
|
||||
<Motion
|
||||
v-for="digit in 10"
|
||||
:key="digit - 1"
|
||||
tag="span"
|
||||
class="absolute top-0 left-0 w-full h-full flex items-center justify-center"
|
||||
:animate="{ y: getDigitPosition(place, digit - 1) }"
|
||||
>
|
||||
{{ digit - 1 }}
|
||||
</Motion>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pointer-events-none absolute inset-0">
|
||||
<div class="absolute top-0 w-full" :style="topGradientStyle ?? topGradientStyles" />
|
||||
<div class="absolute bottom-0 w-full" :style="bottomGradientStyle ?? bottomGradientStyles" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Motion } from 'motion-v';
|
||||
import type { CSSProperties } from 'vue';
|
||||
|
||||
interface CounterProps {
|
||||
value: number;
|
||||
fontSize?: number;
|
||||
padding?: number;
|
||||
places?: number[];
|
||||
gap?: number;
|
||||
borderRadius?: number;
|
||||
horizontalPadding?: number;
|
||||
textColor?: string;
|
||||
fontWeight?: string | number;
|
||||
containerStyle?: CSSProperties;
|
||||
counterStyle?: CSSProperties;
|
||||
digitStyle?: CSSProperties;
|
||||
gradientHeight?: number;
|
||||
gradientFrom?: string;
|
||||
gradientTo?: string;
|
||||
topGradientStyle?: CSSProperties;
|
||||
bottomGradientStyle?: CSSProperties;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<CounterProps>(), {
|
||||
fontSize: 100,
|
||||
padding: 0,
|
||||
places: () => [100, 10, 1],
|
||||
gap: 8,
|
||||
borderRadius: 4,
|
||||
horizontalPadding: 8,
|
||||
textColor: 'white',
|
||||
fontWeight: 'bold',
|
||||
containerStyle: () => ({}),
|
||||
counterStyle: () => ({}),
|
||||
digitStyle: () => ({}),
|
||||
gradientHeight: 16,
|
||||
gradientFrom: 'black',
|
||||
gradientTo: 'transparent',
|
||||
topGradientStyle: undefined,
|
||||
bottomGradientStyle: undefined
|
||||
});
|
||||
|
||||
const digitHeight = computed(() => props.fontSize + props.padding);
|
||||
|
||||
const counterStyles = computed(() => ({
|
||||
fontSize: `${props.fontSize}px`,
|
||||
gap: `${props.gap}px`,
|
||||
borderRadius: `${props.borderRadius}px`,
|
||||
paddingLeft: `${props.horizontalPadding}px`,
|
||||
paddingRight: `${props.horizontalPadding}px`,
|
||||
color: props.textColor,
|
||||
fontWeight: props.fontWeight,
|
||||
...props.counterStyle
|
||||
}));
|
||||
|
||||
const digitStyles = computed(() => ({
|
||||
height: `${digitHeight.value}px`,
|
||||
...props.digitStyle
|
||||
}));
|
||||
|
||||
const topGradientStyles = computed(
|
||||
(): CSSProperties => ({
|
||||
height: `${props.gradientHeight}px`,
|
||||
background: `linear-gradient(to bottom, ${props.gradientFrom}, ${props.gradientTo})`
|
||||
})
|
||||
);
|
||||
|
||||
const bottomGradientStyles = computed(
|
||||
(): CSSProperties => ({
|
||||
height: `${props.gradientHeight}px`,
|
||||
background: `linear-gradient(to top, ${props.gradientFrom}, ${props.gradientTo})`
|
||||
})
|
||||
);
|
||||
|
||||
const springValues = ref<Record<number, number>>({});
|
||||
|
||||
const initializeSpringValues = () => {
|
||||
props.places.forEach(place => {
|
||||
springValues.value[place] = Math.floor(props.value / place);
|
||||
});
|
||||
};
|
||||
|
||||
initializeSpringValues();
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(newValue, oldValue) => {
|
||||
if (newValue === oldValue) return;
|
||||
|
||||
props.places.forEach(place => {
|
||||
const newRoundedValue = Math.floor(newValue / place);
|
||||
const oldRoundedValue = springValues.value[place];
|
||||
|
||||
if (newRoundedValue !== oldRoundedValue) {
|
||||
springValues.value[place] = newRoundedValue;
|
||||
}
|
||||
});
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => digitHeight.value,
|
||||
() => {
|
||||
positionCache.clear();
|
||||
}
|
||||
);
|
||||
|
||||
const positionCache = new Map<string, number>();
|
||||
|
||||
const getDigitPosition = (place: number, digit: number): number => {
|
||||
const springValue = springValues.value[place] || 0;
|
||||
const cacheKey = `${place}-${digit}-${springValue}`;
|
||||
|
||||
if (positionCache.has(cacheKey)) {
|
||||
return positionCache.get(cacheKey)!;
|
||||
}
|
||||
|
||||
const placeValue = springValue % 10;
|
||||
const offset = (10 + digit - placeValue) % 10;
|
||||
let position = offset * digitHeight.value;
|
||||
|
||||
if (offset > 5) {
|
||||
position -= 10 * digitHeight.value;
|
||||
}
|
||||
|
||||
if (positionCache.size > 200) {
|
||||
const firstKey = positionCache.keys().next().value;
|
||||
if (typeof firstKey === 'string') {
|
||||
positionCache.delete(firstKey);
|
||||
}
|
||||
}
|
||||
|
||||
positionCache.set(cacheKey, position);
|
||||
return position;
|
||||
};
|
||||
</script>
|
||||
@@ -378,7 +378,7 @@ const ParticleCard = defineComponent({
|
||||
const GlobalSpotlight = defineComponent({
|
||||
name: 'GlobalSpotlight',
|
||||
props: {
|
||||
gridRef: { type: Object as PropType<HTMLDivElement | null>, required: true },
|
||||
gridRef: { type: [Object, null] as PropType<HTMLDivElement | null>, required: true },
|
||||
disableAnimations: { type: Boolean, default: false },
|
||||
enabled: { type: Boolean, default: true },
|
||||
spotlightRadius: { type: Number, default: DEFAULT_SPOTLIGHT_RADIUS },
|
||||
@@ -539,7 +539,10 @@ const GlobalSpotlight = defineComponent({
|
||||
const BentoCardGrid = defineComponent({
|
||||
name: 'BentoCardGrid',
|
||||
props: {
|
||||
gridRef: { type: Object }
|
||||
gridRef: {
|
||||
type: Function as PropType<(el: HTMLDivElement | null) => void>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div
|
||||
@@ -735,7 +738,7 @@ const setupCardRef = (el: HTMLDivElement | null, index: number) => {
|
||||
:glow-color="glowColor"
|
||||
/>
|
||||
|
||||
<BentoCardGrid :grid-ref="(el: HTMLDivElement) => (gridRef = el)">
|
||||
<BentoCardGrid :grid-ref="(el: HTMLDivElement | null) => { gridRef = el; }">
|
||||
<div class="gap-2 grid card-responsive">
|
||||
<template v-for="(card, index) in cardData" :key="index">
|
||||
<ParticleCard
|
||||
|
||||
317
src/content/Components/RollingGallery/RollingGallery.vue
Normal file
317
src/content/Components/RollingGallery/RollingGallery.vue
Normal file
@@ -0,0 +1,317 @@
|
||||
<template>
|
||||
<div class="relative h-[500px] w-full overflow-hidden">
|
||||
<div class="absolute top-0 left-0 h-full w-12 z-10 bg-gradient-to-l from-transparent to-[#0b0b0b]" />
|
||||
<div class="absolute top-0 right-0 h-full w-12 z-10 bg-gradient-to-r from-transparent to-[#0b0b0b]" />
|
||||
|
||||
<div class="flex h-full items-center justify-center [perspective:1000px] [transform-style:preserve-3d]">
|
||||
<Motion
|
||||
tag="div"
|
||||
class="flex min-h-[200px] items-center justify-center w-full cursor-grab select-none will-change-transform [transform-style:preserve-3d] active:cursor-grabbing"
|
||||
:style="trackStyle"
|
||||
:animate="animateProps"
|
||||
:transition="springTransition"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@mousedown="handleMouseDown"
|
||||
>
|
||||
<div
|
||||
v-for="(url, i) in displayImages"
|
||||
:key="`gallery-${i}`"
|
||||
:style="getItemStyle(i)"
|
||||
class="absolute flex items-center justify-center px-[8%] [backface-visibility:hidden] will-change-transform pointer-events-none"
|
||||
>
|
||||
<img
|
||||
:src="url"
|
||||
alt="gallery"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="pointer-events-auto h-[120px] w-[300px] rounded-[15px] border-[3px] border-white object-cover transition-transform duration-300 ease-in-out will-change-transform hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
</Motion>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
|
||||
import { Motion } from 'motion-v';
|
||||
|
||||
interface RollingGalleryProps {
|
||||
autoplay?: boolean;
|
||||
pauseOnHover?: boolean;
|
||||
images?: string[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<RollingGalleryProps>(), {
|
||||
autoplay: false,
|
||||
pauseOnHover: false,
|
||||
images: () => []
|
||||
});
|
||||
|
||||
const DEFAULT_IMAGES = shallowRef([
|
||||
'https://images.unsplash.com/photo-1528181304800-259b08848526?q=80&w=3870&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
||||
'https://images.unsplash.com/photo-1506665531195-3566af2b4dfa?q=80&w=3870&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
||||
'https://images.unsplash.com/photo-1520250497591-112f2f40a3f4?q=80&w=3456&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
||||
'https://images.unsplash.com/photo-1495103033382-fe343886b671?q=80&w=3870&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
||||
'https://images.unsplash.com/photo-1506781961370-37a89d6b3095?q=80&w=3264&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
||||
'https://images.unsplash.com/photo-1599576838688-8a6c11263108?q=80&w=3870&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
||||
'https://images.unsplash.com/photo-1494094892896-7f14a4433b7a?q=80&w=3870&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
||||
'https://plus.unsplash.com/premium_photo-1664910706524-e783eed89e71?q=80&w=3869&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
||||
'https://images.unsplash.com/photo-1503788311183-fa3bf9c4bc32?q=80&w=3870&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
||||
'https://images.unsplash.com/photo-1585970480901-90d6bb2a48b5?q=80&w=3774&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D'
|
||||
]);
|
||||
|
||||
const isScreenSizeSm = ref(false);
|
||||
const rotateYValue = ref(0);
|
||||
const autoplayInterval = ref<number | null>(null);
|
||||
const autoplayTimeout = ref<number | null>(null);
|
||||
const isDragging = ref(false);
|
||||
const isHovered = ref(false);
|
||||
const dragStartX = ref(0);
|
||||
const dragStartRotation = ref(0);
|
||||
|
||||
const displayImages = computed(() => {
|
||||
const sourceImages = props.images.length > 0 ? props.images : DEFAULT_IMAGES.value;
|
||||
const maxImages = REFERENCE_FACE_COUNT_SPACING;
|
||||
|
||||
if (sourceImages.length >= maxImages) {
|
||||
return sourceImages;
|
||||
}
|
||||
|
||||
const repeatedImages = [];
|
||||
const repetitions = Math.ceil(maxImages / sourceImages.length);
|
||||
|
||||
for (let i = 0; i < repetitions; i++) {
|
||||
repeatedImages.push(...sourceImages);
|
||||
}
|
||||
|
||||
return repeatedImages.slice(0, maxImages);
|
||||
});
|
||||
|
||||
const cylinderWidth = computed(() => (isScreenSizeSm.value ? 1100 : 1800));
|
||||
const faceWidth = computed(() => {
|
||||
return (cylinderWidth.value / REFERENCE_FACE_COUNT_SIZING) * 1.5;
|
||||
});
|
||||
const radius = computed(() => cylinderWidth.value / (2 * Math.PI));
|
||||
|
||||
const DRAG_FACTOR = Object.freeze(0.15);
|
||||
const MOMENTUM_FACTOR = Object.freeze(0.05);
|
||||
const AUTOPLAY_INTERVAL = Object.freeze(2000);
|
||||
const DRAG_RESTART_DELAY = Object.freeze(1500);
|
||||
const HOVER_RESTART_DELAY = Object.freeze(100);
|
||||
const HOVER_DEBOUNCE_DELAY = Object.freeze(50);
|
||||
const REFERENCE_FACE_COUNT_SPACING = Object.freeze(10);
|
||||
const REFERENCE_FACE_COUNT_SIZING = Object.freeze(10);
|
||||
|
||||
const trackStyle = computed(() => ({
|
||||
width: `${cylinderWidth.value}px`,
|
||||
transformStyle: 'preserve-3d' as const
|
||||
}));
|
||||
|
||||
const animateProps = computed(() => ({
|
||||
rotateY: rotateYValue.value
|
||||
}));
|
||||
|
||||
const springTransition = computed(() => {
|
||||
if (isDragging.value) {
|
||||
return { duration: 0 };
|
||||
} else {
|
||||
return {
|
||||
duration: 0.8,
|
||||
ease: 'easeOut' as const
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const styleCache = new Map<string, { width: string; transform: string }>();
|
||||
|
||||
const getItemStyle = (index: number) => {
|
||||
const cacheKey = `${index}-${faceWidth.value}-${radius.value}`;
|
||||
|
||||
if (styleCache.has(cacheKey)) {
|
||||
return styleCache.get(cacheKey)!;
|
||||
}
|
||||
|
||||
const style = {
|
||||
width: `${faceWidth.value}px`,
|
||||
transform: `rotateY(${index * (360 / REFERENCE_FACE_COUNT_SPACING)}deg) translateZ(${radius.value}px)`
|
||||
};
|
||||
|
||||
if (styleCache.size > 50) {
|
||||
styleCache.clear();
|
||||
}
|
||||
|
||||
styleCache.set(cacheKey, style);
|
||||
return style;
|
||||
};
|
||||
|
||||
let resizeTimeout: number | null = null;
|
||||
let hoverTimeout: number | null = null;
|
||||
|
||||
function checkScreenSize() {
|
||||
isScreenSizeSm.value = window.innerWidth <= 640;
|
||||
}
|
||||
|
||||
function throttledResize() {
|
||||
if (resizeTimeout) return;
|
||||
resizeTimeout = setTimeout(() => {
|
||||
checkScreenSize();
|
||||
resizeTimeout = null;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function handleMouseDown(event: MouseEvent) {
|
||||
isDragging.value = true;
|
||||
dragStartX.value = event.clientX;
|
||||
dragStartRotation.value = rotateYValue.value;
|
||||
|
||||
stopAutoplay();
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove, { passive: true });
|
||||
document.addEventListener('mouseup', handleMouseUp, { passive: true });
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function handleMouseMove(event: MouseEvent) {
|
||||
if (!isDragging.value) return;
|
||||
|
||||
const deltaX = event.clientX - dragStartX.value;
|
||||
const rotationDelta = deltaX * DRAG_FACTOR;
|
||||
rotateYValue.value = dragStartRotation.value + rotationDelta;
|
||||
}
|
||||
|
||||
function handleMouseUp(event: MouseEvent) {
|
||||
if (!isDragging.value) return;
|
||||
|
||||
isDragging.value = false;
|
||||
|
||||
const deltaX = event.clientX - dragStartX.value;
|
||||
const velocity = deltaX * MOMENTUM_FACTOR;
|
||||
rotateYValue.value += velocity;
|
||||
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
|
||||
stopAutoplay();
|
||||
|
||||
if (props.autoplay) {
|
||||
if (props.pauseOnHover && isHovered.value) {
|
||||
return;
|
||||
} else {
|
||||
autoplayTimeout.value = setTimeout(() => {
|
||||
if (!isDragging.value && (!props.pauseOnHover || !isHovered.value)) {
|
||||
startAutoplay();
|
||||
}
|
||||
}, DRAG_RESTART_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startAutoplay() {
|
||||
if (!props.autoplay || isDragging.value || (props.pauseOnHover && isHovered.value)) return;
|
||||
|
||||
stopAutoplay();
|
||||
|
||||
autoplayInterval.value = setInterval(() => {
|
||||
if (!isDragging.value && (!props.pauseOnHover || !isHovered.value)) {
|
||||
rotateYValue.value -= 360 / REFERENCE_FACE_COUNT_SPACING;
|
||||
}
|
||||
}, AUTOPLAY_INTERVAL);
|
||||
}
|
||||
|
||||
function stopAutoplay() {
|
||||
if (autoplayInterval.value) {
|
||||
clearInterval(autoplayInterval.value);
|
||||
autoplayInterval.value = null;
|
||||
}
|
||||
if (autoplayTimeout.value) {
|
||||
clearTimeout(autoplayTimeout.value);
|
||||
autoplayTimeout.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseEnter() {
|
||||
if (hoverTimeout) {
|
||||
clearTimeout(hoverTimeout);
|
||||
hoverTimeout = null;
|
||||
}
|
||||
|
||||
hoverTimeout = setTimeout(() => {
|
||||
isHovered.value = true;
|
||||
|
||||
if (props.autoplay && props.pauseOnHover && !isDragging.value) {
|
||||
stopAutoplay();
|
||||
}
|
||||
}, HOVER_DEBOUNCE_DELAY);
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
if (hoverTimeout) {
|
||||
clearTimeout(hoverTimeout);
|
||||
hoverTimeout = null;
|
||||
}
|
||||
|
||||
hoverTimeout = setTimeout(() => {
|
||||
isHovered.value = false;
|
||||
|
||||
if (props.autoplay && props.pauseOnHover && !isDragging.value) {
|
||||
stopAutoplay();
|
||||
autoplayTimeout.value = setTimeout(() => {
|
||||
if (props.autoplay && !isDragging.value && !isHovered.value) {
|
||||
startAutoplay();
|
||||
}
|
||||
}, HOVER_RESTART_DELAY);
|
||||
}
|
||||
}, HOVER_DEBOUNCE_DELAY);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkScreenSize();
|
||||
window.addEventListener('resize', throttledResize, { passive: true });
|
||||
|
||||
if (props.autoplay) {
|
||||
startAutoplay();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', throttledResize);
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
stopAutoplay();
|
||||
if (resizeTimeout) {
|
||||
clearTimeout(resizeTimeout);
|
||||
}
|
||||
if (hoverTimeout) {
|
||||
clearTimeout(hoverTimeout);
|
||||
hoverTimeout = null;
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.autoplay,
|
||||
newVal => {
|
||||
stopAutoplay();
|
||||
if (newVal && !isDragging.value && (!props.pauseOnHover || !isHovered.value)) {
|
||||
autoplayTimeout.value = setTimeout(() => {
|
||||
if (!isDragging.value && (!props.pauseOnHover || !isHovered.value)) {
|
||||
startAutoplay();
|
||||
}
|
||||
}, HOVER_RESTART_DELAY);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.pauseOnHover,
|
||||
() => {
|
||||
if (props.autoplay) {
|
||||
stopAutoplay();
|
||||
if (!isDragging.value && (!props.pauseOnHover || !isHovered.value)) {
|
||||
startAutoplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
292
src/content/Components/ScrollStack/ScrollStack.vue
Normal file
292
src/content/Components/ScrollStack/ScrollStack.vue
Normal file
@@ -0,0 +1,292 @@
|
||||
<script setup lang="ts">
|
||||
import Lenis from 'lenis';
|
||||
import { defineComponent, h, nextTick, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||
|
||||
interface CardTransform {
|
||||
translateY: number;
|
||||
scale: number;
|
||||
rotation: number;
|
||||
blur: number;
|
||||
}
|
||||
|
||||
interface ScrollStackProps {
|
||||
className?: string;
|
||||
itemDistance?: number;
|
||||
itemScale?: number;
|
||||
itemStackDistance?: number;
|
||||
stackPosition?: string;
|
||||
scaleEndPosition?: string;
|
||||
baseScale?: number;
|
||||
scaleDuration?: number;
|
||||
rotationAmount?: number;
|
||||
blurAmount?: number;
|
||||
onStackComplete?: () => void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ScrollStackProps>(), {
|
||||
className: '',
|
||||
itemDistance: 100,
|
||||
itemScale: 0.03,
|
||||
itemStackDistance: 30,
|
||||
stackPosition: '20%',
|
||||
scaleEndPosition: '10%',
|
||||
baseScale: 0.85,
|
||||
scaleDuration: 0.5,
|
||||
rotationAmount: 0,
|
||||
blurAmount: 0
|
||||
});
|
||||
|
||||
const scrollerRef = useTemplateRef('scrollerRef');
|
||||
const stackCompletedRef = ref(false);
|
||||
const animationFrameRef = ref<number | null>(null);
|
||||
const lenisRef = ref<Lenis | null>(null);
|
||||
const cardsRef = ref<HTMLElement[]>([]);
|
||||
const lastTransformsRef = ref(new Map<number, CardTransform>());
|
||||
const isUpdatingRef = ref(false);
|
||||
|
||||
const calculateProgress = (scrollTop: number, start: number, end: number) => {
|
||||
if (scrollTop < start) return 0;
|
||||
if (scrollTop > end) return 1;
|
||||
return (scrollTop - start) / (end - start);
|
||||
};
|
||||
|
||||
const parsePercentage = (value: string | number, containerHeight: number) => {
|
||||
if (typeof value === 'string' && value.includes('%')) {
|
||||
return (parseFloat(value) / 100) * containerHeight;
|
||||
}
|
||||
return parseFloat(value as string);
|
||||
};
|
||||
|
||||
const updateCardTransforms = () => {
|
||||
const scroller = scrollerRef.value;
|
||||
if (!scroller || !cardsRef.value.length || isUpdatingRef.value) return;
|
||||
|
||||
isUpdatingRef.value = true;
|
||||
|
||||
const scrollTop = scroller.scrollTop;
|
||||
const containerHeight = scroller.clientHeight;
|
||||
const stackPositionPx = parsePercentage(props.stackPosition, containerHeight);
|
||||
const scaleEndPositionPx = parsePercentage(props.scaleEndPosition, containerHeight);
|
||||
const endElement = scroller.querySelector('.scroll-stack-end') as HTMLElement;
|
||||
const endElementTop = endElement ? endElement.offsetTop : 0;
|
||||
|
||||
cardsRef.value.forEach((card, i) => {
|
||||
if (!card) return;
|
||||
|
||||
const cardTop = card.offsetTop;
|
||||
const triggerStart = cardTop - stackPositionPx - props.itemStackDistance * i;
|
||||
const triggerEnd = cardTop - scaleEndPositionPx;
|
||||
const pinStart = cardTop - stackPositionPx - props.itemStackDistance * i;
|
||||
const pinEnd = endElementTop - containerHeight / 2;
|
||||
|
||||
const scaleProgress = calculateProgress(scrollTop, triggerStart, triggerEnd);
|
||||
const targetScale = props.baseScale + i * props.itemScale;
|
||||
const scale = 1 - scaleProgress * (1 - targetScale);
|
||||
const rotation = props.rotationAmount ? i * props.rotationAmount * scaleProgress : 0;
|
||||
|
||||
let blur = 0;
|
||||
if (props.blurAmount) {
|
||||
let topCardIndex = 0;
|
||||
for (let j = 0; j < cardsRef.value.length; j++) {
|
||||
const jCardTop = cardsRef.value[j].offsetTop;
|
||||
const jTriggerStart = jCardTop - stackPositionPx - props.itemStackDistance * j;
|
||||
if (scrollTop >= jTriggerStart) {
|
||||
topCardIndex = j;
|
||||
}
|
||||
}
|
||||
|
||||
if (i < topCardIndex) {
|
||||
const depthInStack = topCardIndex - i;
|
||||
blur = Math.max(0, depthInStack * props.blurAmount);
|
||||
}
|
||||
}
|
||||
|
||||
let translateY = 0;
|
||||
const isPinned = scrollTop >= pinStart && scrollTop <= pinEnd;
|
||||
|
||||
if (isPinned) {
|
||||
translateY = scrollTop - cardTop + stackPositionPx + props.itemStackDistance * i;
|
||||
} else if (scrollTop > pinEnd) {
|
||||
translateY = pinEnd - cardTop + stackPositionPx + props.itemStackDistance * i;
|
||||
}
|
||||
|
||||
const newTransform = {
|
||||
translateY: Math.round(translateY * 100) / 100,
|
||||
scale: Math.round(scale * 1000) / 1000,
|
||||
rotation: Math.round(rotation * 100) / 100,
|
||||
blur: Math.round(blur * 100) / 100
|
||||
};
|
||||
|
||||
const lastTransform = lastTransformsRef.value.get(i);
|
||||
const hasChanged =
|
||||
!lastTransform ||
|
||||
Math.abs(lastTransform.translateY - newTransform.translateY) > 0.1 ||
|
||||
Math.abs(lastTransform.scale - newTransform.scale) > 0.001 ||
|
||||
Math.abs(lastTransform.rotation - newTransform.rotation) > 0.1 ||
|
||||
Math.abs(lastTransform.blur - newTransform.blur) > 0.1;
|
||||
|
||||
if (hasChanged) {
|
||||
const transform = `translate3d(0, ${newTransform.translateY}px, 0) scale(${newTransform.scale}) rotate(${newTransform.rotation}deg)`;
|
||||
const filter = newTransform.blur > 0 ? `blur(${newTransform.blur}px)` : '';
|
||||
|
||||
card.style.transform = transform;
|
||||
card.style.filter = filter;
|
||||
|
||||
lastTransformsRef.value.set(i, newTransform);
|
||||
}
|
||||
|
||||
if (i === cardsRef.value.length - 1) {
|
||||
const isInView = scrollTop >= pinStart && scrollTop <= pinEnd;
|
||||
if (isInView && !stackCompletedRef.value) {
|
||||
stackCompletedRef.value = true;
|
||||
props.onStackComplete?.();
|
||||
} else if (!isInView && stackCompletedRef.value) {
|
||||
stackCompletedRef.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
isUpdatingRef.value = false;
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
updateCardTransforms();
|
||||
};
|
||||
|
||||
const setupLenis = () => {
|
||||
const scroller = scrollerRef.value;
|
||||
if (!scroller) return;
|
||||
|
||||
const lenis = new Lenis({
|
||||
wrapper: scroller,
|
||||
content: scroller.querySelector('.scroll-stack-inner') as HTMLElement,
|
||||
duration: 1.2,
|
||||
easing: t => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
||||
smoothWheel: true,
|
||||
touchMultiplier: 2,
|
||||
infinite: false,
|
||||
gestureOrientation: 'vertical',
|
||||
wheelMultiplier: 1,
|
||||
lerp: 0.1,
|
||||
syncTouch: true,
|
||||
syncTouchLerp: 0.075
|
||||
});
|
||||
|
||||
lenis.on('scroll', handleScroll);
|
||||
|
||||
const raf = (time: number) => {
|
||||
lenis.raf(time);
|
||||
animationFrameRef.value = requestAnimationFrame(raf);
|
||||
};
|
||||
animationFrameRef.value = requestAnimationFrame(raf);
|
||||
|
||||
lenisRef.value = lenis;
|
||||
return lenis;
|
||||
};
|
||||
|
||||
let cleanup: (() => void) | null = null;
|
||||
const setup = () => {
|
||||
const scroller = scrollerRef.value;
|
||||
if (!scroller) return;
|
||||
|
||||
const cards = Array.from(scroller.querySelectorAll('.scroll-stack-card')) as HTMLElement[];
|
||||
cardsRef.value = cards;
|
||||
const transformsCache = lastTransformsRef.value;
|
||||
|
||||
cards.forEach((card, i) => {
|
||||
if (i < cards.length - 1) {
|
||||
card.style.marginBottom = `${props.itemDistance}px`;
|
||||
}
|
||||
card.style.willChange = 'transform, filter';
|
||||
card.style.transformOrigin = 'top center';
|
||||
card.style.backfaceVisibility = 'hidden';
|
||||
card.style.transform = 'translateZ(0)';
|
||||
card.style.webkitTransform = 'translateZ(0)';
|
||||
card.style.perspective = '1000px';
|
||||
card.style.webkitPerspective = '1000px';
|
||||
});
|
||||
|
||||
setupLenis();
|
||||
|
||||
updateCardTransforms();
|
||||
|
||||
cleanup = () => {
|
||||
if (animationFrameRef.value) {
|
||||
cancelAnimationFrame(animationFrameRef.value);
|
||||
}
|
||||
if (lenisRef.value) {
|
||||
lenisRef.value.destroy();
|
||||
}
|
||||
stackCompletedRef.value = false;
|
||||
cardsRef.value = [];
|
||||
transformsCache.clear();
|
||||
isUpdatingRef.value = false;
|
||||
};
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
setup();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanup?.();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props,
|
||||
() => {
|
||||
cleanup?.();
|
||||
setup();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const ScrollStackItem = defineComponent({
|
||||
name: 'ScrollStackItem',
|
||||
props: {
|
||||
itemClassName: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
return () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class:
|
||||
`scroll-stack-card relative w-full h-80 my-8 p-12 rounded-[40px] shadow-[0_0_30px_rgba(0,0,0,0.1)] box-border origin-top will-change-transform ${props.itemClassName}`.trim(),
|
||||
style: {
|
||||
backfaceVisibility: 'hidden',
|
||||
transformStyle: 'preserve-3d'
|
||||
}
|
||||
},
|
||||
slots.default?.()
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="scrollerRef"
|
||||
:class="['relative w-full h-full overflow-y-auto overflow-x-visible', className]"
|
||||
:style="{
|
||||
overscrollBehavior: 'contain',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
scrollBehavior: 'smooth',
|
||||
WebkitTransform: 'translateZ(0)',
|
||||
transform: 'translateZ(0)',
|
||||
willChange: 'scroll-position'
|
||||
}"
|
||||
>
|
||||
<div class="px-20 pt-[20vh] pb-[50rem] min-h-screen scroll-stack-inner">
|
||||
<slot />
|
||||
<!-- Spacer so the last pin can release cleanly -->
|
||||
<div class="w-full h-px scroll-stack-end" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<span
|
||||
ref="rootRef"
|
||||
:class="[props.className]"
|
||||
:style="{
|
||||
display: 'inline',
|
||||
...props.style
|
||||
}"
|
||||
@click="props.onClick"
|
||||
>
|
||||
<span
|
||||
v-for="(word, wordIndex) in words"
|
||||
:key="wordIndex"
|
||||
:style="{ display: 'inline-block', whiteSpace: 'nowrap' }"
|
||||
>
|
||||
<span
|
||||
v-for="(letter, letterIndex) in word.split('')"
|
||||
:key="getLetterKey(wordIndex, letterIndex)"
|
||||
:style="{
|
||||
display: 'inline-block',
|
||||
fontVariationSettings: props.fromFontVariationSettings
|
||||
}"
|
||||
class="letter"
|
||||
:data-index="getGlobalLetterIndex(wordIndex, letterIndex)"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{{ letter }}
|
||||
</span>
|
||||
<span v-if="wordIndex < words.length - 1" class="inline-block"> </span>
|
||||
</span>
|
||||
<span class="absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap clip-rect-0 border-0">
|
||||
{{ props.label }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick, type CSSProperties } from 'vue';
|
||||
|
||||
export type FalloffType = 'linear' | 'exponential' | 'gaussian';
|
||||
|
||||
interface VariableProximityProps {
|
||||
label: string;
|
||||
fromFontVariationSettings: string;
|
||||
toFontVariationSettings: string;
|
||||
containerRef?: HTMLElement | null | undefined;
|
||||
radius?: number;
|
||||
falloff?: FalloffType;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VariableProximityProps>(), {
|
||||
radius: 50,
|
||||
falloff: 'linear',
|
||||
className: '',
|
||||
style: () => ({}),
|
||||
onClick: undefined
|
||||
});
|
||||
|
||||
const rootRef = ref<HTMLElement | null>(null);
|
||||
const letterElements = ref<HTMLElement[]>([]);
|
||||
const mousePosition = ref({ x: 0, y: 0 });
|
||||
const lastPosition = ref<{ x: number | null; y: number | null }>({ x: null, y: null });
|
||||
const interpolatedSettings = ref<string[]>([]);
|
||||
|
||||
let animationFrameId: number | null = null;
|
||||
|
||||
const words = computed(() => props.label.split(' '));
|
||||
|
||||
const parsedSettings = computed(() => {
|
||||
const parseSettings = (settingsStr: string) => {
|
||||
const result = new Map();
|
||||
settingsStr.split(',').forEach(s => {
|
||||
const parts = s.trim().split(' ');
|
||||
if (parts.length === 2) {
|
||||
const name = parts[0].replace(/['"]/g, '');
|
||||
const value = parseFloat(parts[1]);
|
||||
result.set(name, value);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const fromSettings = parseSettings(props.fromFontVariationSettings);
|
||||
const toSettings = parseSettings(props.toFontVariationSettings);
|
||||
|
||||
return Array.from(fromSettings.entries()).map(([axis, fromValue]) => ({
|
||||
axis,
|
||||
fromValue,
|
||||
toValue: toSettings.get(axis) ?? fromValue
|
||||
}));
|
||||
});
|
||||
|
||||
const calculateDistance = (x1: number, y1: number, x2: number, y2: number) =>
|
||||
Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
||||
|
||||
const calculateFalloff = (distance: number) => {
|
||||
const norm = Math.min(Math.max(1 - distance / props.radius, 0), 1);
|
||||
switch (props.falloff) {
|
||||
case 'exponential':
|
||||
return norm ** 2;
|
||||
case 'gaussian':
|
||||
return Math.exp(-((distance / (props.radius / 2)) ** 2) / 2);
|
||||
case 'linear':
|
||||
default:
|
||||
return norm;
|
||||
}
|
||||
};
|
||||
|
||||
const getLetterKey = (wordIndex: number, letterIndex: number) => `${wordIndex}-${letterIndex}`;
|
||||
|
||||
const getGlobalLetterIndex = (wordIndex: number, letterIndex: number) => {
|
||||
let globalIndex = 0;
|
||||
for (let i = 0; i < wordIndex; i++) {
|
||||
globalIndex += words.value[i].length;
|
||||
}
|
||||
return globalIndex + letterIndex;
|
||||
};
|
||||
|
||||
const initializeLetterElements = () => {
|
||||
if (!rootRef.value) return;
|
||||
|
||||
const elements = rootRef.value.querySelectorAll('.letter');
|
||||
letterElements.value = Array.from(elements) as HTMLElement[];
|
||||
|
||||
console.log(`Found ${letterElements.value.length} letter elements`);
|
||||
};
|
||||
|
||||
const handleMouseMove = (ev: MouseEvent) => {
|
||||
const container = props.containerRef || rootRef.value;
|
||||
if (!container) return;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
mousePosition.value = {
|
||||
x: ev.clientX - rect.left,
|
||||
y: ev.clientY - rect.top
|
||||
};
|
||||
};
|
||||
|
||||
const handleTouchMove = (ev: TouchEvent) => {
|
||||
const container = props.containerRef || rootRef.value;
|
||||
if (!container) return;
|
||||
|
||||
const touch = ev.touches[0];
|
||||
const rect = container.getBoundingClientRect();
|
||||
mousePosition.value = {
|
||||
x: touch.clientX - rect.left,
|
||||
y: touch.clientY - rect.top
|
||||
};
|
||||
};
|
||||
|
||||
const animationLoop = () => {
|
||||
const container = props.containerRef || rootRef.value;
|
||||
if (!container || letterElements.value.length === 0) {
|
||||
animationFrameId = requestAnimationFrame(animationLoop);
|
||||
return;
|
||||
}
|
||||
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
|
||||
if (lastPosition.value.x === mousePosition.value.x && lastPosition.value.y === mousePosition.value.y) {
|
||||
animationFrameId = requestAnimationFrame(animationLoop);
|
||||
return;
|
||||
}
|
||||
|
||||
lastPosition.value = { x: mousePosition.value.x, y: mousePosition.value.y };
|
||||
|
||||
const newSettings = Array(letterElements.value.length).fill(props.fromFontVariationSettings);
|
||||
|
||||
letterElements.value.forEach((letterEl, index) => {
|
||||
if (!letterEl) return;
|
||||
|
||||
const rect = letterEl.getBoundingClientRect();
|
||||
const letterCenterX = rect.left + rect.width / 2 - containerRect.left;
|
||||
const letterCenterY = rect.top + rect.height / 2 - containerRect.top;
|
||||
|
||||
const distance = calculateDistance(mousePosition.value.x, mousePosition.value.y, letterCenterX, letterCenterY);
|
||||
|
||||
if (distance >= props.radius) {
|
||||
return;
|
||||
}
|
||||
|
||||
const falloffValue = calculateFalloff(distance);
|
||||
const setting = parsedSettings.value
|
||||
.map(({ axis, fromValue, toValue }) => {
|
||||
const interpolatedValue = fromValue + (toValue - fromValue) * falloffValue;
|
||||
return `'${axis}' ${interpolatedValue}`;
|
||||
})
|
||||
.join(', ');
|
||||
|
||||
newSettings[index] = setting;
|
||||
});
|
||||
|
||||
interpolatedSettings.value = newSettings;
|
||||
|
||||
letterElements.value.forEach((letterEl, index) => {
|
||||
letterEl.style.fontVariationSettings = interpolatedSettings.value[index];
|
||||
});
|
||||
|
||||
animationFrameId = requestAnimationFrame(animationLoop);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initializeLetterElements();
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('touchmove', handleTouchMove);
|
||||
|
||||
animationFrameId = requestAnimationFrame(animationLoop);
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('touchmove', handleTouchMove);
|
||||
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -566,3 +566,90 @@ div:has(> .props-table) {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-stack-card-demo {
|
||||
font-size: clamp(1.5rem, 4vw, 3rem);
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.scroll-stack-card-demo .stack-img-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 150px;
|
||||
border-radius: 1.5rem;
|
||||
border: 10px solid #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: clamp(4rem, 8vw, 8rem);
|
||||
}
|
||||
|
||||
.scroll-stack-demo-container .scroll-stack-inner {
|
||||
padding: 20vh 2rem 50rem;
|
||||
}
|
||||
|
||||
.ssc-demo-1 {
|
||||
background-color: #35724d;
|
||||
}
|
||||
|
||||
.ssc-demo-2 {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
.ssc-demo-3 {
|
||||
background-color: #35724d;
|
||||
}
|
||||
|
||||
.ssc-demo-4 {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
.ssc-demo-5 {
|
||||
background-color: #35724d;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 1240px) {
|
||||
.scroll-stack-card-demo {
|
||||
flex-direction: row;
|
||||
gap: 2rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.scroll-stack-demo-container .scroll-stack-inner {
|
||||
padding: 20vh 5rem 50rem;
|
||||
}
|
||||
|
||||
.scroll-stack-card-demo .stack-img-container {
|
||||
width: 50%;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.scroll-stack-card-demo h3 {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 480px) {
|
||||
.scroll-stack-card-demo {
|
||||
font-size: 1rem;
|
||||
padding: 0.2rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.scroll-stack-card-demo .stack-img-container {
|
||||
border-width: 5px;
|
||||
border-radius: 1rem;
|
||||
min-height: 120px;
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
202
src/demo/Backgrounds/FaultyTerminalDemo.vue
Normal file
202
src/demo/Backgrounds/FaultyTerminalDemo.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div class="relative p-0 h-[600px] overflow-hidden demo-container">
|
||||
<FaultyTerminal
|
||||
:key="key"
|
||||
:scale="scale"
|
||||
:digit-size="digitSize"
|
||||
:time-scale="timeScale"
|
||||
:scanline-intensity="scanlineIntensity"
|
||||
:curvature="curvature"
|
||||
:tint="tint"
|
||||
:mouse-react="mouseReact"
|
||||
:mouse-strength="mouseStrength"
|
||||
:page-load-animation="pageLoadAnimation"
|
||||
:noise-amp="noiseAmp"
|
||||
:brightness="brightness"
|
||||
/>
|
||||
<BackgroundContent pill-text="New Background" headline="It works on my machine, please check again" />
|
||||
</div>
|
||||
|
||||
<Customize>
|
||||
<PreviewColor title="Tint Color" v-model="tint" />
|
||||
<PreviewSlider :min="1" :max="3" :step="0.1" v-model="scale" title="Scale" />
|
||||
<PreviewSlider :min="0.5" :max="3" :step="0.1" v-model="digitSize" title="Digit Size" />
|
||||
<PreviewSlider :min="0" :max="3" :step="0.1" v-model="timeScale" title="Speed" />
|
||||
<PreviewSlider :min="0.5" :max="1" :step="0.1" v-model="noiseAmp" title="Noise Amplitude" />
|
||||
<PreviewSlider :min="0.1" :max="1" :step="0.1" v-model="brightness" title="Brightness" />
|
||||
<PreviewSlider :min="0" :max="2" :step="0.1" v-model="scanlineIntensity" title="Scanline Intensity" />
|
||||
<PreviewSlider :min="0" :max="0.5" :step="0.01" v-model="curvature" title="Curvature" />
|
||||
<PreviewSlider :min="0" :max="2" :step="0.1" v-model="mouseStrength" title="Mouse Strength" />
|
||||
<PreviewSwitch title="Mouse React" v-model="mouseReact" />
|
||||
<PreviewSwitch title="Page Load Animation" v-model="pageLoadAnimation" />
|
||||
</Customize>
|
||||
|
||||
<PropTable :data="propData" />
|
||||
<Dependencies :dependency-list="['ogl']" />
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<CodeExample :code-object="faultyTerminal" />
|
||||
</template>
|
||||
|
||||
<template #cli>
|
||||
<CliInstallation :command="faultyTerminal.cli" />
|
||||
</template>
|
||||
</TabbedLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useForceRerender } from '@/composables/useForceRerender';
|
||||
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 BackgroundContent from '../../components/common/BackgroundContent.vue';
|
||||
import Customize from '../../components/common/Customize.vue';
|
||||
import PreviewColor from '../../components/common/PreviewColor.vue';
|
||||
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
||||
import PreviewSwitch from '../../components/common/PreviewSwitch.vue';
|
||||
import PropTable from '../../components/common/PropTable.vue';
|
||||
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||
import { faultyTerminal } from '../../constants/code/Backgrounds/faultyTerminalCode';
|
||||
import FaultyTerminal from '../../content/Backgrounds/FaultyTerminal/FaultyTerminal.vue';
|
||||
|
||||
const { rerenderKey: key } = useForceRerender();
|
||||
|
||||
const scale = ref(1.5);
|
||||
const digitSize = ref(1.2);
|
||||
const timeScale = ref(0.5);
|
||||
const scanlineIntensity = ref(0.5);
|
||||
const curvature = ref(0.1);
|
||||
const tint = ref('#A7EF9E');
|
||||
const mouseReact = ref(true);
|
||||
const mouseStrength = ref(0.5);
|
||||
const pageLoadAnimation = ref(true);
|
||||
const noiseAmp = ref(1);
|
||||
const brightness = ref(0.6);
|
||||
|
||||
const propData = [
|
||||
{
|
||||
name: 'scale',
|
||||
type: 'number',
|
||||
default: '1.5',
|
||||
description: 'Controls the zoom/scale of the pattern.'
|
||||
},
|
||||
{
|
||||
name: 'gridMul',
|
||||
type: 'Vec2',
|
||||
default: '[2, 1]',
|
||||
description: 'Grid multiplier for glyph density [x, y].'
|
||||
},
|
||||
{
|
||||
name: 'digitSize',
|
||||
type: 'number',
|
||||
default: '1.2',
|
||||
description: 'Size of individual glyphs.'
|
||||
},
|
||||
{
|
||||
name: 'timeScale',
|
||||
type: 'number',
|
||||
default: '1',
|
||||
description: 'Animation speed multiplier.'
|
||||
},
|
||||
{
|
||||
name: 'pause',
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description: 'Pause/resume animation.'
|
||||
},
|
||||
{
|
||||
name: 'scanlineIntensity',
|
||||
type: 'number',
|
||||
default: '1',
|
||||
description: 'Strength of scanline effects.'
|
||||
},
|
||||
{
|
||||
name: 'glitchAmount',
|
||||
type: 'number',
|
||||
default: '1',
|
||||
description: 'Glitch displacement intensity.'
|
||||
},
|
||||
{
|
||||
name: 'flickerAmount',
|
||||
type: 'number',
|
||||
default: '1',
|
||||
description: 'Flicker effect strength.'
|
||||
},
|
||||
{
|
||||
name: 'noiseAmp',
|
||||
type: 'number',
|
||||
default: '1',
|
||||
description: 'Noise pattern amplitude.'
|
||||
},
|
||||
{
|
||||
name: 'chromaticAberration',
|
||||
type: 'number',
|
||||
default: '0',
|
||||
description: 'RGB channel separation in pixels.'
|
||||
},
|
||||
{
|
||||
name: 'dither',
|
||||
type: 'number | boolean',
|
||||
default: '0',
|
||||
description: 'Dithering effect intensity.'
|
||||
},
|
||||
{
|
||||
name: 'curvature',
|
||||
type: 'number',
|
||||
default: '0',
|
||||
description: 'Barrel distortion amount.'
|
||||
},
|
||||
{
|
||||
name: 'tint',
|
||||
type: 'string',
|
||||
default: "'#ffffff'",
|
||||
description: 'Color tint (hex).'
|
||||
},
|
||||
{
|
||||
name: 'mouseReact',
|
||||
type: 'boolean',
|
||||
default: 'true',
|
||||
description: 'Enable/disable mouse interaction.'
|
||||
},
|
||||
{
|
||||
name: 'mouseStrength',
|
||||
type: 'number',
|
||||
default: '0.5',
|
||||
description: 'Mouse interaction intensity.'
|
||||
},
|
||||
{
|
||||
name: 'pageLoadAnimation',
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description: 'Enable fade-in animation on load.'
|
||||
},
|
||||
{
|
||||
name: 'brightness',
|
||||
type: 'number',
|
||||
default: '1',
|
||||
description: 'Overall opacity/brightness control.'
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
default: "''",
|
||||
description: 'Additional CSS classes.'
|
||||
},
|
||||
{
|
||||
name: 'style',
|
||||
type: 'React.CSSProperties',
|
||||
default: '{}',
|
||||
description: 'Inline styles.'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
243
src/demo/Backgrounds/LightRaysDemo.vue
Normal file
243
src/demo/Backgrounds/LightRaysDemo.vue
Normal file
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div class="h-[600px] overflow-hidden demo-container relative">
|
||||
<LightRays
|
||||
:rays-origin="raysOrigin"
|
||||
:rays-color="raysColor"
|
||||
:rays-speed="raysSpeed"
|
||||
:light-spread="lightSpread"
|
||||
:ray-length="rayLength"
|
||||
:pulsating="pulsating"
|
||||
:fade-distance="fadeDistance"
|
||||
:saturation="saturation"
|
||||
:follow-mouse="true"
|
||||
:mouse-influence="mouseInfluence"
|
||||
:noise-amount="noiseAmount"
|
||||
:distortion="distortion"
|
||||
class="w-full h-full"
|
||||
/>
|
||||
|
||||
<BackgroundContent pill-text="New Background" headline="May these lights guide you on your path" />
|
||||
</div>
|
||||
|
||||
<Customize>
|
||||
<PreviewColor
|
||||
title="Rays Color"
|
||||
v-model="raysColor"
|
||||
/>
|
||||
|
||||
<PreviewSelect
|
||||
title="Rays Origin"
|
||||
v-model="raysOrigin"
|
||||
:options="raysOriginOptions"
|
||||
/>
|
||||
|
||||
<PreviewSlider
|
||||
title="Rays Speed"
|
||||
v-model="raysSpeed"
|
||||
:min="0.1"
|
||||
:max="3"
|
||||
:step="0.1"
|
||||
/>
|
||||
|
||||
<PreviewSlider
|
||||
title="Light Spread"
|
||||
v-model="lightSpread"
|
||||
:min="0.1"
|
||||
:max="2"
|
||||
:step="0.1"
|
||||
/>
|
||||
|
||||
<PreviewSlider
|
||||
title="Ray Length"
|
||||
v-model="rayLength"
|
||||
:min="0.5"
|
||||
:max="3"
|
||||
:step="0.1"
|
||||
/>
|
||||
|
||||
<PreviewSlider
|
||||
title="Fade Distance"
|
||||
v-model="fadeDistance"
|
||||
:min="0.5"
|
||||
:max="2"
|
||||
:step="0.1"
|
||||
/>
|
||||
|
||||
<PreviewSlider
|
||||
title="Saturation"
|
||||
v-model="saturation"
|
||||
:min="0"
|
||||
:max="2"
|
||||
:step="0.1"
|
||||
/>
|
||||
|
||||
<PreviewSlider
|
||||
title="Mouse Influence"
|
||||
v-model="mouseInfluence"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.1"
|
||||
/>
|
||||
|
||||
<PreviewSlider
|
||||
title="Noise Amount"
|
||||
v-model="noiseAmount"
|
||||
:min="0"
|
||||
:max="0.5"
|
||||
:step="0.01"
|
||||
/>
|
||||
|
||||
<PreviewSlider
|
||||
title="Distortion"
|
||||
v-model="distortion"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.1"
|
||||
/>
|
||||
|
||||
<PreviewSwitch title="Pulsating" v-model="pulsating" />
|
||||
</Customize>
|
||||
|
||||
<PropTable :data="propData" />
|
||||
<Dependencies :dependency-list="['ogl']" />
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<CodeExample :code-object="lightRays" />
|
||||
</template>
|
||||
|
||||
<template #cli>
|
||||
<CliInstallation :command="lightRays.cli" />
|
||||
</template>
|
||||
</TabbedLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import TabbedLayout from '@/components/common/TabbedLayout.vue';
|
||||
import PropTable from '@/components/common/PropTable.vue';
|
||||
import Dependencies from '@/components/code/Dependencies.vue';
|
||||
import CliInstallation from '@/components/code/CliInstallation.vue';
|
||||
import CodeExample from '@/components/code/CodeExample.vue';
|
||||
import Customize from '@/components/common/Customize.vue';
|
||||
import PreviewSwitch from '@/components/common/PreviewSwitch.vue';
|
||||
import PreviewSlider from '@/components/common/PreviewSlider.vue';
|
||||
import PreviewSelect from '@/components/common/PreviewSelect.vue';
|
||||
import PreviewColor from '@/components/common/PreviewColor.vue';
|
||||
import BackgroundContent from '@/components/common/BackgroundContent.vue';
|
||||
import LightRays, { type RaysOrigin } from '../../content/Backgrounds/LightRays/LightRays.vue';
|
||||
import { lightRays } from '@/constants/code/Backgrounds/lightRaysCode';
|
||||
|
||||
const raysOrigin = ref<RaysOrigin>('top-center');
|
||||
const raysColor = ref('#ffffff');
|
||||
const raysSpeed = ref(1);
|
||||
const lightSpread = ref(0.5);
|
||||
const rayLength = ref(1.0);
|
||||
const pulsating = ref(false);
|
||||
const fadeDistance = ref(1.0);
|
||||
const saturation = ref(1.0);
|
||||
const mouseInfluence = ref(0.5);
|
||||
const noiseAmount = ref(0.0);
|
||||
const distortion = ref(0.0);
|
||||
|
||||
const raysOriginOptions = [
|
||||
{ value: 'top-center', label: 'Top' },
|
||||
{ value: 'right', label: 'Right' },
|
||||
{ value: 'left', label: 'Left' },
|
||||
{ value: 'bottom-center', label: 'Bottom' },
|
||||
{ value: 'top-left', label: 'Top Left' },
|
||||
{ value: 'top-right', label: 'Top Right' },
|
||||
{ value: 'bottom-left', label: 'Bottom Left' },
|
||||
{ value: 'bottom-right', label: 'Bottom Right' }
|
||||
];
|
||||
|
||||
const propData = [
|
||||
{
|
||||
name: 'raysOrigin',
|
||||
type: 'RaysOrigin',
|
||||
default: '"top-center"',
|
||||
description:
|
||||
"Origin position of the light rays. Options: 'top-center', 'top-left', 'top-right', 'right', 'left', 'bottom-center', 'bottom-right', 'bottom-left'"
|
||||
},
|
||||
{
|
||||
name: 'raysColor',
|
||||
type: 'string',
|
||||
default: '"#ffffff"',
|
||||
description: 'Color of the light rays in hex format'
|
||||
},
|
||||
{
|
||||
name: 'raysSpeed',
|
||||
type: 'number',
|
||||
default: '1',
|
||||
description: 'Animation speed of the rays'
|
||||
},
|
||||
{
|
||||
name: 'lightSpread',
|
||||
type: 'number',
|
||||
default: '0.5',
|
||||
description: 'How wide the light rays spread. Lower values = tighter rays, higher values = wider spread'
|
||||
},
|
||||
{
|
||||
name: 'rayLength',
|
||||
type: 'number',
|
||||
default: '1.0',
|
||||
description: 'Maximum length/reach of the rays'
|
||||
},
|
||||
{
|
||||
name: 'pulsating',
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description: 'Enable pulsing animation effect'
|
||||
},
|
||||
{
|
||||
name: 'fadeDistance',
|
||||
type: 'number',
|
||||
default: '1.0',
|
||||
description: 'How far rays fade out from origin'
|
||||
},
|
||||
{
|
||||
name: 'saturation',
|
||||
type: 'number',
|
||||
default: '1.0',
|
||||
description: 'Color saturation level (0-1)'
|
||||
},
|
||||
{
|
||||
name: 'followMouse',
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description: 'Make rays rotate towards the mouse cursor'
|
||||
},
|
||||
{
|
||||
name: 'mouseInfluence',
|
||||
type: 'number',
|
||||
default: '0.5',
|
||||
description: 'How much mouse affects rays (0-1)'
|
||||
},
|
||||
{
|
||||
name: 'noiseAmount',
|
||||
type: 'number',
|
||||
default: '0.0',
|
||||
description: 'Add noise/grain to rays (0-1)'
|
||||
},
|
||||
{
|
||||
name: 'distortion',
|
||||
type: 'number',
|
||||
default: '0.0',
|
||||
description: 'Apply wave distortion to rays'
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
default: '""',
|
||||
description: 'Additional CSS classes to apply to the container'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
146
src/demo/Components/BounceCardsDemo.vue
Normal file
146
src/demo/Components/BounceCardsDemo.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div class="demo-container bounce-cards-demo">
|
||||
<RefreshButton @refresh="forceRerender" />
|
||||
|
||||
<BounceCards
|
||||
:key="rerenderKey"
|
||||
class="custom-bounceCards"
|
||||
:images="images"
|
||||
:animation-delay="animationDelay"
|
||||
:animation-stagger="animationStagger"
|
||||
ease-type="elastic.out(1, 0.5)"
|
||||
:transform-styles="transformStyles"
|
||||
:enable-hover="enableHover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Customize>
|
||||
<PreviewSwitch title="Enable Hover Effect" v-model="enableHover" />
|
||||
|
||||
<PreviewSlider title="Animation Delay" v-model="animationDelay" :min="0.1" :max="2" :step="0.1" />
|
||||
|
||||
<PreviewSlider title="Animation Stagger" v-model="animationStagger" :min="0" :max="0.3" :step="0.01" />
|
||||
</Customize>
|
||||
|
||||
<PropTable :data="propData" />
|
||||
|
||||
<Dependencies :dependency-list="['gsap']" />
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<CodeExample :code-object="bounceCards" />
|
||||
</template>
|
||||
|
||||
<template #cli>
|
||||
<CliInstallation :command="bounceCards.cli" />
|
||||
</template>
|
||||
</TabbedLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import TabbedLayout from '@/components/common/TabbedLayout.vue';
|
||||
import RefreshButton from '@/components/common/RefreshButton.vue';
|
||||
import PropTable from '@/components/common/PropTable.vue';
|
||||
import Dependencies from '@/components/code/Dependencies.vue';
|
||||
import CliInstallation from '@/components/code/CliInstallation.vue';
|
||||
import CodeExample from '@/components/code/CodeExample.vue';
|
||||
import Customize from '@/components/common/Customize.vue';
|
||||
import PreviewSwitch from '@/components/common/PreviewSwitch.vue';
|
||||
import PreviewSlider from '@/components/common/PreviewSlider.vue';
|
||||
import BounceCards from '@/content/Components/BounceCards/BounceCards.vue';
|
||||
import { bounceCards } from '@/constants/code/Components/bounceCardsCode';
|
||||
import { useForceRerender } from '@/composables/useForceRerender';
|
||||
|
||||
const enableHover = ref(false);
|
||||
const animationDelay = ref(1);
|
||||
const animationStagger = ref(0.08);
|
||||
const { rerenderKey, forceRerender } = useForceRerender();
|
||||
|
||||
const images = ref([
|
||||
'https://picsum.photos/id/287/300/300?grayscale',
|
||||
'https://picsum.photos/id/1001/300/300?grayscale',
|
||||
'https://picsum.photos/id/1027/300/300?grayscale',
|
||||
'https://picsum.photos/id/1025/300/300?grayscale',
|
||||
'https://picsum.photos/id/1026/300/300?grayscale'
|
||||
]);
|
||||
|
||||
const transformStyles = ref([
|
||||
'rotate(5deg) translate(-150px)',
|
||||
'rotate(0deg) translate(-70px)',
|
||||
'rotate(-5deg)',
|
||||
'rotate(5deg) translate(70px)',
|
||||
'rotate(-5deg) translate(150px)'
|
||||
]);
|
||||
|
||||
const propData = [
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
default: '-',
|
||||
description: 'Additional CSS classes for the container.'
|
||||
},
|
||||
{
|
||||
name: 'images',
|
||||
type: 'string[]',
|
||||
default: '[]',
|
||||
description: 'Array of image URLs to display.'
|
||||
},
|
||||
{
|
||||
name: 'containerWidth',
|
||||
type: 'number',
|
||||
default: '400',
|
||||
description: 'Width of the container (px).'
|
||||
},
|
||||
{
|
||||
name: 'containerHeight',
|
||||
type: 'number',
|
||||
default: '400',
|
||||
description: 'Height of the container (px).'
|
||||
},
|
||||
{
|
||||
name: 'animationDelay',
|
||||
type: 'number',
|
||||
default: '-',
|
||||
description: 'Delay (in seconds) before the animation starts.'
|
||||
},
|
||||
{
|
||||
name: 'animationStagger',
|
||||
type: 'number',
|
||||
default: '-',
|
||||
description: "Time (in seconds) between each card's animation."
|
||||
},
|
||||
{
|
||||
name: 'easeType',
|
||||
type: 'string',
|
||||
default: 'elastic.out(1, 0.8)',
|
||||
description: 'Easing function for the bounce.'
|
||||
},
|
||||
{
|
||||
name: 'transformStyles',
|
||||
type: 'string[]',
|
||||
default: 'various rotations/translations',
|
||||
description: 'Custom transforms for each card position.'
|
||||
},
|
||||
{
|
||||
name: 'enableHover',
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description: "If true, hovering pushes siblings aside and flattens the hovered card's rotation."
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bounce-cards-demo {
|
||||
min-height: 400px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
177
src/demo/Components/CounterDemo.vue
Normal file
177
src/demo/Components/CounterDemo.vue
Normal file
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div class="demo-container h-[400px] overflow-hidden relative">
|
||||
<Counter
|
||||
:value="value"
|
||||
:places="[100, 10, 1]"
|
||||
gradientFrom="#0b0b0b"
|
||||
:fontSize="fontSize"
|
||||
:padding="5"
|
||||
:gap="gap"
|
||||
:borderRadius="10"
|
||||
:horizontalPadding="15"
|
||||
textColor="white"
|
||||
:fontWeight="900"
|
||||
/>
|
||||
|
||||
<div class="flex gap-4 bottom-4 justify-center mt-4 absolute left-1/2 transform -translate-x-1/2">
|
||||
<button
|
||||
class="cursor-pointer bg-[#0b0b0b] rounded-[10px] border border-[#333] hover:bg-[#222] text-white h-10 w-10 transition-colors"
|
||||
@click="value > 0 && value--"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<button
|
||||
class="cursor-pointer bg-[#0b0b0b] rounded-[10px] border border-[#333] hover:bg-[#222] text-white h-10 w-10 transition-colors"
|
||||
@click="value < 999 && value++"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Customize>
|
||||
<PreviewSlider title="Value" v-model="value" :min="0" :max="999" :step="1" />
|
||||
|
||||
<PreviewSlider title="Gap" v-model="gap" :min="0" :max="50" :step="5" />
|
||||
|
||||
<PreviewSlider title="Font Size" v-model="fontSize" :min="40" :max="200" :step="10" />
|
||||
</Customize>
|
||||
|
||||
<PropTable :data="propData" />
|
||||
<Dependencies :dependency-list="['motion-v']" />
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<CodeExample :code-object="counter" />
|
||||
</template>
|
||||
|
||||
<template #cli>
|
||||
<CliInstallation :command="counter.cli" />
|
||||
</template>
|
||||
</TabbedLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||
import Customize from '../../components/common/Customize.vue';
|
||||
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
||||
import PropTable from '../../components/common/PropTable.vue';
|
||||
import CodeExample from '../../components/code/CodeExample.vue';
|
||||
import CliInstallation from '../../components/code/CliInstallation.vue';
|
||||
import Dependencies from '../../components/code/Dependencies.vue';
|
||||
|
||||
import Counter from '../../content/Components/Counter/Counter.vue';
|
||||
import { counter } from '../../constants/code/Components/counterCode';
|
||||
|
||||
const value = ref(123);
|
||||
const fontSize = ref(80);
|
||||
const gap = ref(10);
|
||||
|
||||
const propData = [
|
||||
{
|
||||
name: 'value',
|
||||
type: 'number',
|
||||
default: 'N/A (required)',
|
||||
description: 'The numeric value to display in the counter.'
|
||||
},
|
||||
{
|
||||
name: 'fontSize',
|
||||
type: 'number',
|
||||
default: '100',
|
||||
description: 'The base font size used for the counter digits.'
|
||||
},
|
||||
{
|
||||
name: 'padding',
|
||||
type: 'number',
|
||||
default: '0',
|
||||
description: 'Additional padding added to the digit height.'
|
||||
},
|
||||
{
|
||||
name: 'places',
|
||||
type: 'number[]',
|
||||
default: '[100, 10, 1]',
|
||||
description: 'An array of place values to determine which digits to display.'
|
||||
},
|
||||
{
|
||||
name: 'gap',
|
||||
type: 'number',
|
||||
default: '8',
|
||||
description: 'The gap (in pixels) between each digit.'
|
||||
},
|
||||
{
|
||||
name: 'borderRadius',
|
||||
type: 'number',
|
||||
default: '4',
|
||||
description: 'The border radius (in pixels) for the counter container.'
|
||||
},
|
||||
{
|
||||
name: 'horizontalPadding',
|
||||
type: 'number',
|
||||
default: '8',
|
||||
description: 'The horizontal padding (in pixels) for the counter container.'
|
||||
},
|
||||
{
|
||||
name: 'textColor',
|
||||
type: 'string',
|
||||
default: "'white'",
|
||||
description: 'The text color for the counter digits.'
|
||||
},
|
||||
{
|
||||
name: 'fontWeight',
|
||||
type: 'string | number',
|
||||
default: "'bold'",
|
||||
description: 'The font weight of the counter digits.'
|
||||
},
|
||||
{
|
||||
name: 'containerStyle',
|
||||
type: 'CSSProperties',
|
||||
default: '{}',
|
||||
description: 'Custom inline styles for the outer container.'
|
||||
},
|
||||
{
|
||||
name: 'counterStyle',
|
||||
type: 'CSSProperties',
|
||||
default: '{}',
|
||||
description: 'Custom inline styles for the counter element.'
|
||||
},
|
||||
{
|
||||
name: 'digitStyle',
|
||||
type: 'CSSProperties',
|
||||
default: '{}',
|
||||
description: 'Custom inline styles for each digit container.'
|
||||
},
|
||||
{
|
||||
name: 'gradientHeight',
|
||||
type: 'number',
|
||||
default: '16',
|
||||
description: 'The height (in pixels) of the gradient overlays.'
|
||||
},
|
||||
{
|
||||
name: 'gradientFrom',
|
||||
type: 'string',
|
||||
default: "'black'",
|
||||
description: 'The starting color for the gradient overlays.'
|
||||
},
|
||||
{
|
||||
name: 'gradientTo',
|
||||
type: 'string',
|
||||
default: "'transparent'",
|
||||
description: 'The ending color for the gradient overlays.'
|
||||
},
|
||||
{
|
||||
name: 'topGradientStyle',
|
||||
type: 'CSSProperties',
|
||||
default: 'undefined',
|
||||
description: 'Custom inline styles for the top gradient overlay.'
|
||||
},
|
||||
{
|
||||
name: 'bottomGradientStyle',
|
||||
type: 'CSSProperties',
|
||||
default: 'undefined',
|
||||
description: 'Custom inline styles for the bottom gradient overlay.'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
75
src/demo/Components/RollingGalleryDemo.vue
Normal file
75
src/demo/Components/RollingGalleryDemo.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div class="demo-container relative min-h-[500px] overflow-hidden">
|
||||
<div class="flex h-full max-w-[600px] flex-col items-center justify-center">
|
||||
<h2
|
||||
class="absolute top-4 mt-6 whitespace-nowrap text-center font-black text-2xl text-white md:top-4 md:text-5xl"
|
||||
>
|
||||
Your trip to Thailand.
|
||||
</h2>
|
||||
<RollingGallery
|
||||
:autoplay="autoplay"
|
||||
:pause-on-hover="pauseOnHover"
|
||||
:images="customImages.length > 0 ? customImages : undefined"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Customize>
|
||||
<PreviewSwitch title="Autoplay" v-model="autoplay" />
|
||||
|
||||
<PreviewSwitch title="Pause on Hover" v-model="pauseOnHover" />
|
||||
</Customize>
|
||||
|
||||
<PropTable :data="propData" />
|
||||
<Dependencies :dependency-list="['motion-v']" />
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<CodeExample :code-object="rollingGallery" />
|
||||
</template>
|
||||
|
||||
<template #cli>
|
||||
<CliInstallation :command="rollingGallery.cli" />
|
||||
</template>
|
||||
</TabbedLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import TabbedLayout from '@/components/common/TabbedLayout.vue';
|
||||
import PropTable from '@/components/common/PropTable.vue';
|
||||
import Dependencies from '@/components/code/Dependencies.vue';
|
||||
import CliInstallation from '@/components/code/CliInstallation.vue';
|
||||
import CodeExample from '@/components/code/CodeExample.vue';
|
||||
import Customize from '@/components/common/Customize.vue';
|
||||
import PreviewSwitch from '@/components/common/PreviewSwitch.vue';
|
||||
import RollingGallery from '@/content/Components/RollingGallery/RollingGallery.vue';
|
||||
import { rollingGallery } from '@/constants/code/Components/rollingGalleryCode';
|
||||
|
||||
const autoplay = ref(true);
|
||||
const pauseOnHover = ref(true);
|
||||
const customImages = ref<string[]>([]);
|
||||
|
||||
const propData = [
|
||||
{
|
||||
name: 'autoplay',
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description: 'Controls the autoplay toggle of the carousel. When turned on, it rotates and loops infinitely.'
|
||||
},
|
||||
{
|
||||
name: 'pauseOnHover',
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description: 'Allows the carousel to be paused on hover when autoplay is turned on.'
|
||||
},
|
||||
{
|
||||
name: 'images',
|
||||
type: 'string[]',
|
||||
default: '[]',
|
||||
description: 'Array of image URLs to display in the gallery.'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
216
src/demo/Components/ScrollStackDemo.vue
Normal file
216
src/demo/Components/ScrollStackDemo.vue
Normal file
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div class="relative h-[500px] overflow-hidden demo-container">
|
||||
<RefreshButton
|
||||
@refresh="
|
||||
() => {
|
||||
isCompleted = false;
|
||||
forceRerender();
|
||||
}
|
||||
"
|
||||
/>
|
||||
<p
|
||||
class="top-[25%] left-[50%] absolute font-black text-[#333] text-[clamp(2rem,4vw,3rem)] text-center transition-all -translate-x-1/2 -translate-y-1/2 duration-300 ease-in-out pointer-events-none transform"
|
||||
>
|
||||
{{ isCompleted ? 'Stack Completed!' : 'Scroll Down' }}
|
||||
</p>
|
||||
|
||||
<ScrollStack
|
||||
:key="rerenderKey"
|
||||
:item-distance="itemDistance"
|
||||
className="scroll-stack-demo-container"
|
||||
:item-stack-distance="itemStackDistance"
|
||||
:stack-position="stackPosition"
|
||||
:base-scale="baseScale"
|
||||
:rotation-amount="rotationAmount"
|
||||
:blur-amount="blurAmount"
|
||||
@stackComplete="handleStackComplete"
|
||||
>
|
||||
<ScrollStackItem itemClassName="scroll-stack-card-demo ssc-demo-1">
|
||||
<h3>Text Animations</h3>
|
||||
|
||||
<div className="stack-img-container">
|
||||
<i class="pi-align-left pi" style="font-size: 120px"></i>
|
||||
</div>
|
||||
</ScrollStackItem>
|
||||
|
||||
<ScrollStackItem itemClassName="scroll-stack-card-demo ssc-demo-2">
|
||||
<h3>Animations</h3>
|
||||
|
||||
<div className="stack-img-container">
|
||||
<i class="pi pi-play" style="font-size: 120px"></i>
|
||||
</div>
|
||||
</ScrollStackItem>
|
||||
|
||||
<ScrollStackItem itemClassName="scroll-stack-card-demo ssc-demo-3">
|
||||
<h3>Components</h3>
|
||||
|
||||
<div className="stack-img-container">
|
||||
<i class="pi pi-sliders-h" style="font-size: 120px"></i>
|
||||
</div>
|
||||
</ScrollStackItem>
|
||||
|
||||
<ScrollStackItem itemClassName="scroll-stack-card-demo ssc-demo-4">
|
||||
<h3>Backgrounds</h3>
|
||||
|
||||
<div className="stack-img-container">
|
||||
<i class="pi pi-image" style="font-size: 120px"></i>
|
||||
</div>
|
||||
</ScrollStackItem>
|
||||
|
||||
<ScrollStackItem itemClassName="scroll-stack-card-demo ssc-demo-5">
|
||||
<h3>All on Vue Bits!</h3>
|
||||
</ScrollStackItem>
|
||||
</ScrollStack>
|
||||
</div>
|
||||
|
||||
<Customize>
|
||||
<PreviewSlider title="Item Distance" v-model="itemDistance" :min="0" :max="1000" :step="10" value-unit="px" />
|
||||
<PreviewSlider
|
||||
title="Stack Distance"
|
||||
v-model="itemStackDistance"
|
||||
:min="0"
|
||||
:max="40"
|
||||
:step="5"
|
||||
value-unit="px"
|
||||
/>
|
||||
<PreviewSelect title="Stack Position" v-model="stackPosition" :options="stackPositionOptions" />
|
||||
<PreviewSlider title="Base Scale" v-model="baseScale" :min="0.5" :max="1.0" :step="0.05" />
|
||||
<PreviewSlider title="Rotation Amount" v-model="rotationAmount" :min="0" :max="1" :step="0.1" value-unit="°" />
|
||||
<PreviewSlider title="Blur Amount" v-model="blurAmount" :min="0" :max="10" :step="0.5" value-unit="px" />
|
||||
</Customize>
|
||||
|
||||
<PropTable :data="propData" />
|
||||
<Dependencies :dependency-list="['lenis']" />
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<CodeExample :code-object="scrollStack" />
|
||||
</template>
|
||||
|
||||
<template #cli>
|
||||
<CliInstallation :command="scrollStack.cli" />
|
||||
</template>
|
||||
</TabbedLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PreviewSelect from '@/components/common/PreviewSelect.vue';
|
||||
import { useForceRerender } from '@/composables/useForceRerender';
|
||||
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 RefreshButton from '../../components/common/RefreshButton.vue';
|
||||
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||
import { scrollStack } from '../../constants/code/Components/scrollStackCode';
|
||||
import ScrollStack, { ScrollStackItem } from '../../content/Components/ScrollStack/ScrollStack.vue';
|
||||
|
||||
const { rerenderKey, forceRerender } = useForceRerender();
|
||||
|
||||
const isCompleted = ref(false);
|
||||
const itemDistance = ref(200);
|
||||
const itemStackDistance = ref(30);
|
||||
const baseScale = ref(0.85);
|
||||
const rotationAmount = ref(0);
|
||||
const blurAmount = ref(0);
|
||||
const stackPosition = ref('20%');
|
||||
|
||||
const handleStackComplete = () => {
|
||||
isCompleted.value = true;
|
||||
};
|
||||
|
||||
const stackPositionOptions = [
|
||||
{ value: '10%', label: '10%' },
|
||||
{ value: '15%', label: '15%' },
|
||||
{ value: '20%', label: '20%' },
|
||||
{ value: '25%', label: '25%' },
|
||||
{ value: '30%', label: '30%' },
|
||||
{ value: '35%', label: '35%' }
|
||||
];
|
||||
|
||||
const propData = [
|
||||
{
|
||||
name: 'children',
|
||||
type: 'ReactNode',
|
||||
default: 'required',
|
||||
description: 'The content to be displayed in the scroll stack. Should contain ScrollStackItem components.'
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
default: '""',
|
||||
description: 'Additional CSS classes to apply to the scroll stack container.'
|
||||
},
|
||||
{
|
||||
name: 'itemDistance',
|
||||
type: 'number',
|
||||
default: '100',
|
||||
description: 'Distance between stacked items in pixels.'
|
||||
},
|
||||
{
|
||||
name: 'itemScale',
|
||||
type: 'number',
|
||||
default: '0.03',
|
||||
description: 'Scale increment for each stacked item.'
|
||||
},
|
||||
{
|
||||
name: 'itemStackDistance',
|
||||
type: 'number',
|
||||
default: '30',
|
||||
description: 'Distance between items when they start stacking.'
|
||||
},
|
||||
{
|
||||
name: 'stackPosition',
|
||||
type: 'string',
|
||||
default: '"20%"',
|
||||
description: 'Position where the stacking effect begins as a percentage of viewport height.'
|
||||
},
|
||||
{
|
||||
name: 'scaleEndPosition',
|
||||
type: 'string',
|
||||
default: '"10%"',
|
||||
description: 'Position where the scaling effect ends as a percentage of viewport height.'
|
||||
},
|
||||
{
|
||||
name: 'baseScale',
|
||||
type: 'number',
|
||||
default: '0.85',
|
||||
description: 'Base scale value for the first item in the stack.'
|
||||
},
|
||||
{
|
||||
name: 'scaleDuration',
|
||||
type: 'number',
|
||||
default: '0.5',
|
||||
description: 'Duration of the scaling animation in seconds.'
|
||||
},
|
||||
{
|
||||
name: 'rotationAmount',
|
||||
type: 'number',
|
||||
default: '0',
|
||||
description: 'Rotation amount for each item in degrees.'
|
||||
},
|
||||
{
|
||||
name: 'blurAmount',
|
||||
type: 'number',
|
||||
default: '0',
|
||||
description: 'Blur amount for items that are further back in the stack.'
|
||||
},
|
||||
{
|
||||
name: 'onStackComplete',
|
||||
type: 'function',
|
||||
default: 'undefined',
|
||||
description: 'Callback function called when the stack animation is complete.'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
126
src/demo/TextAnimations/VariableProximityDemo.vue
Normal file
126
src/demo/TextAnimations/VariableProximityDemo.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="demo-container relative min-h-[400px] overflow-hidden p-4 font-['Roboto_Flex',sans-serif]"
|
||||
>
|
||||
<VariableProximity
|
||||
label="Hover me! And then star Vue Bits on GitHub, or else..."
|
||||
class-name="variable-proximity-demo"
|
||||
from-font-variation-settings="'wght' 400, 'opsz' 9"
|
||||
to-font-variation-settings="'wght' 1000, 'opsz' 40"
|
||||
:container-ref="containerRef"
|
||||
:radius="radius"
|
||||
:falloff="falloff"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Customize>
|
||||
<PreviewSlider title="Radius" v-model="radius" :min="50" :max="300" :step="10" />
|
||||
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button
|
||||
v-for="type in falloffTypes"
|
||||
:key="type"
|
||||
class="text-xs cursor-pointer bg-[#0b0b0b] rounded-[10px] border border-[#333] hover:bg-[#222] text-white h-8 px-3 transition-colors"
|
||||
:class="{ 'bg-[#333]': falloff === type }"
|
||||
@click="falloff = type"
|
||||
>
|
||||
{{ type }}
|
||||
</button>
|
||||
</div>
|
||||
</Customize>
|
||||
|
||||
<PropTable :data="propData" />
|
||||
<Dependencies :dependency-list="['motion-v']" />
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<CodeExample :code-object="variableProximity" />
|
||||
</template>
|
||||
|
||||
<template #cli>
|
||||
<CliInstallation :command="variableProximity.cli" />
|
||||
</template>
|
||||
</TabbedLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import TabbedLayout from '@/components/common/TabbedLayout.vue';
|
||||
import PropTable from '@/components/common/PropTable.vue';
|
||||
import Dependencies from '@/components/code/Dependencies.vue';
|
||||
import CliInstallation from '@/components/code/CliInstallation.vue';
|
||||
import CodeExample from '@/components/code/CodeExample.vue';
|
||||
import Customize from '@/components/common/Customize.vue';
|
||||
import PreviewSlider from '@/components/common/PreviewSlider.vue';
|
||||
import VariableProximity, { type FalloffType } from '@/content/TextAnimations/VariableProximity/VariableProximity.vue';
|
||||
import { variableProximity } from '@/constants/code/TextAnimations/variableProximityCode';
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null);
|
||||
const radius = ref(100);
|
||||
const falloff = ref<FalloffType>('linear');
|
||||
|
||||
const falloffTypes: FalloffType[] = ['linear', 'exponential', 'gaussian'];
|
||||
|
||||
const propData = [
|
||||
{
|
||||
name: 'label',
|
||||
type: 'string',
|
||||
default: '""',
|
||||
description: 'The text content to display.'
|
||||
},
|
||||
{
|
||||
name: 'fromFontVariationSettings',
|
||||
type: 'string',
|
||||
default: "\"'wght' 400, 'opsz' 9\"",
|
||||
description: 'The starting variation settings.'
|
||||
},
|
||||
{
|
||||
name: 'toFontVariationSettings',
|
||||
type: 'string',
|
||||
default: "\"'wght' 800, 'opsz' 40\"",
|
||||
description: 'The variation settings to reach at cursor proximity.'
|
||||
},
|
||||
{
|
||||
name: 'containerRef',
|
||||
type: 'HTMLElement',
|
||||
default: 'undefined',
|
||||
description: 'Reference to container for relative calculations.'
|
||||
},
|
||||
{
|
||||
name: 'radius',
|
||||
type: 'number',
|
||||
default: '50',
|
||||
description: 'Proximity radius to influence the effect.'
|
||||
},
|
||||
{
|
||||
name: 'falloff',
|
||||
type: '"linear" | "exponential" | "gaussian"',
|
||||
default: '"linear"',
|
||||
description: 'Type of falloff for the effect.'
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
default: '""',
|
||||
description: 'Additional CSS classes to apply.'
|
||||
},
|
||||
{
|
||||
name: 'style',
|
||||
type: 'CSSProperties',
|
||||
default: '{}',
|
||||
description: 'Inline styles to apply.'
|
||||
},
|
||||
{
|
||||
name: 'onClick',
|
||||
type: '() => void',
|
||||
default: 'undefined',
|
||||
description: 'Click event handler.'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
<style scoped>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght@8..144,100..1000&display=swap');
|
||||
</style>
|
||||
Reference in New Issue
Block a user