Merge branch 'main' into feat/bounce-cards

This commit is contained in:
David
2025-07-25 16:39:31 +03:00
committed by GitHub
16 changed files with 2083 additions and 4 deletions

View File

@@ -1,5 +1,5 @@
// Highlighted sidebar items // 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 = []; export const UPDATED = [];
// Used for main sidebar navigation // Used for main sidebar navigation
@@ -63,6 +63,7 @@ export const CATEGORIES = [
'Masonry', 'Masonry',
'Glass Surface', 'Glass Surface',
'Magic Bento', 'Magic Bento',
'Scroll Stack',
'Profile Card', 'Profile Card',
'Dock', 'Dock',
'Gooey Nav', 'Gooey Nav',
@@ -81,7 +82,9 @@ export const CATEGORIES = [
'Elastic Slider', 'Elastic Slider',
'Stack', 'Stack',
'Chroma Grid', 'Chroma Grid',
'Bounce Cards' 'Bounce Cards',
'Counter',
'Rolling Gallery'
] ]
}, },
{ {
@@ -93,6 +96,7 @@ export const CATEGORIES = [
'Dither', 'Dither',
'Dot Grid', 'Dot Grid',
'Hyperspeed', 'Hyperspeed',
'Faulty Terminal',
'Ripple Grid', 'Ripple Grid',
'Silk', 'Silk',
'Lightning', 'Lightning',

View File

@@ -70,6 +70,9 @@ const components = {
'stack': () => import('../demo/Components/StackDemo.vue'), 'stack': () => import('../demo/Components/StackDemo.vue'),
'chroma-grid': () => import('../demo/Components/ChromaGridDemo.vue'), 'chroma-grid': () => import('../demo/Components/ChromaGridDemo.vue'),
'bounce-cards': () => import('../demo/Components/BounceCardsDemo.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 = { const backgrounds = {
@@ -95,6 +98,7 @@ const backgrounds = {
'grid-distortion': () => import('../demo/Backgrounds/GridDistortionDemo.vue'), 'grid-distortion': () => import('../demo/Backgrounds/GridDistortionDemo.vue'),
'ripple-grid': () => import('../demo/Backgrounds/RippleGridDemo.vue'), 'ripple-grid': () => import('../demo/Backgrounds/RippleGridDemo.vue'),
'galaxy': () => import('../demo/Backgrounds/GalaxyDemo.vue'), 'galaxy': () => import('../demo/Backgrounds/GalaxyDemo.vue'),
'faulty-terminal': () => import('../demo/Backgrounds/FaultyTerminalDemo.vue'),
}; };
export const componentMap = { export const componentMap = {

View 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>`
});

View 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"
/>`
});

View 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>`
});

View 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>`
});

View 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>

View 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>

View File

@@ -378,7 +378,7 @@ const ParticleCard = defineComponent({
const GlobalSpotlight = defineComponent({ const GlobalSpotlight = defineComponent({
name: 'GlobalSpotlight', name: 'GlobalSpotlight',
props: { 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 }, disableAnimations: { type: Boolean, default: false },
enabled: { type: Boolean, default: true }, enabled: { type: Boolean, default: true },
spotlightRadius: { type: Number, default: DEFAULT_SPOTLIGHT_RADIUS }, spotlightRadius: { type: Number, default: DEFAULT_SPOTLIGHT_RADIUS },
@@ -539,7 +539,10 @@ const GlobalSpotlight = defineComponent({
const BentoCardGrid = defineComponent({ const BentoCardGrid = defineComponent({
name: 'BentoCardGrid', name: 'BentoCardGrid',
props: { props: {
gridRef: { type: Object } gridRef: {
type: Function as PropType<(el: HTMLDivElement | null) => void>,
required: true
}
}, },
template: ` template: `
<div <div

View 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>

View 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>

View File

@@ -566,3 +566,90 @@ div:has(> .props-table) {
order: 2; 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;
}
}

View 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>

View 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>

View 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>

View 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>