mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
Merge pull request #144 from Gazoon007/feat/orbit-images
Create <OrbitImages /> animation
This commit is contained in:
@@ -75,6 +75,7 @@ export const CATEGORIES = [
|
||||
'Star Border',
|
||||
'Sticker Peel',
|
||||
'Target Cursor',
|
||||
'Orbit Images',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -26,6 +26,7 @@ const animations = {
|
||||
'ghost-cursor': () => import('../demo/Animations/GhostCursorDemo.vue'),
|
||||
'antigravity': () => import('../demo/Animations/AntigravityDemo.vue'),
|
||||
'pixel-trail': () => import('../demo/Animations/PixelTrailDemo.vue'),
|
||||
'orbit-images': () => import('../demo/Animations/OrbitImagesDemo.vue'),
|
||||
};
|
||||
|
||||
const textAnimations = {
|
||||
|
||||
30
src/constants/code/Animations/orbitImagesCode.ts
Normal file
30
src/constants/code/Animations/orbitImagesCode.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import code from '@/content/Animations/OrbitImages/OrbitImages.vue?raw';
|
||||
import { createCodeObject } from '@/types/code';
|
||||
|
||||
export const orbitImages = createCodeObject(code, 'Animations/OrbitImages', {
|
||||
usage: `<template>
|
||||
<OrbitImages
|
||||
:images="images"
|
||||
shape="ellipse"
|
||||
:radius-x="340"
|
||||
:radius-y="80"
|
||||
:rotation="-8"
|
||||
:duration="30"
|
||||
:item-size="80"
|
||||
:responsive="true"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import OrbitImages from './OrbitImages.vue';
|
||||
|
||||
const images = [
|
||||
'https://picsum.photos/300/300?grayscale&random=1',
|
||||
'https://picsum.photos/300/300?grayscale&random=2',
|
||||
'https://picsum.photos/300/300?grayscale&random=3',
|
||||
'https://picsum.photos/300/300?grayscale&random=4',
|
||||
'https://picsum.photos/300/300?grayscale&random=5',
|
||||
'https://picsum.photos/300/300?grayscale&random=6'
|
||||
];
|
||||
</script>`
|
||||
});
|
||||
377
src/content/Animations/OrbitImages/OrbitImages.vue
Normal file
377
src/content/Animations/OrbitImages/OrbitImages.vue
Normal file
@@ -0,0 +1,377 @@
|
||||
<script setup lang="ts">
|
||||
// Component created by Dominik Koch
|
||||
// https://x.com/dominikkoch
|
||||
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
export type OrbitShape =
|
||||
| 'ellipse'
|
||||
| 'circle'
|
||||
| 'square'
|
||||
| 'rectangle'
|
||||
| 'triangle'
|
||||
| 'star'
|
||||
| 'heart'
|
||||
| 'infinity'
|
||||
| 'wave'
|
||||
| 'custom';
|
||||
|
||||
interface OrbitImagesProps {
|
||||
images?: string[];
|
||||
altPrefix?: string;
|
||||
shape?: OrbitShape;
|
||||
customPath?: string;
|
||||
baseWidth?: number;
|
||||
radiusX?: number;
|
||||
radiusY?: number;
|
||||
radius?: number;
|
||||
starPoints?: number;
|
||||
starInnerRatio?: number;
|
||||
rotation?: number;
|
||||
duration?: number;
|
||||
itemSize?: number;
|
||||
direction?: 'normal' | 'reverse';
|
||||
fill?: boolean;
|
||||
width?: number | '100%';
|
||||
height?: number | 'auto';
|
||||
className?: string;
|
||||
showPath?: boolean;
|
||||
pathColor?: string;
|
||||
pathWidth?: number;
|
||||
easing?: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';
|
||||
paused?: boolean;
|
||||
responsive?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<OrbitImagesProps>(), {
|
||||
images: () => [],
|
||||
altPrefix: 'Orbiting image',
|
||||
shape: 'ellipse',
|
||||
customPath: undefined,
|
||||
baseWidth: 1400,
|
||||
radiusX: 700,
|
||||
radiusY: 170,
|
||||
radius: 300,
|
||||
starPoints: 5,
|
||||
starInnerRatio: 0.5,
|
||||
rotation: -8,
|
||||
duration: 40,
|
||||
itemSize: 64,
|
||||
direction: 'normal',
|
||||
fill: true,
|
||||
width: 100,
|
||||
height: 100,
|
||||
className: '',
|
||||
showPath: false,
|
||||
pathColor: 'rgba(0,0,0,0.1)',
|
||||
pathWidth: 2,
|
||||
easing: 'linear',
|
||||
paused: false,
|
||||
responsive: false
|
||||
});
|
||||
|
||||
const containerRef = ref<HTMLDivElement | null>(null);
|
||||
const scale = ref(1);
|
||||
const progress = ref(0);
|
||||
let animationFrameId: number | null = null;
|
||||
let lastTime: number | null = null;
|
||||
|
||||
const designCenterX = computed(() => props.baseWidth / 2);
|
||||
const designCenterY = computed(() => props.baseWidth / 2);
|
||||
|
||||
function generateEllipsePath(cx: number, cy: number, rx: number, ry: number): string {
|
||||
return `M ${cx - rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx + rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy}`;
|
||||
}
|
||||
|
||||
function generateCirclePath(cx: number, cy: number, r: number): string {
|
||||
return generateEllipsePath(cx, cy, r, r);
|
||||
}
|
||||
|
||||
function generateSquarePath(cx: number, cy: number, size: number): string {
|
||||
const h = size / 2;
|
||||
return `M ${cx - h} ${cy - h} L ${cx + h} ${cy - h} L ${cx + h} ${cy + h} L ${cx - h} ${cy + h} Z`;
|
||||
}
|
||||
|
||||
function generateRectanglePath(cx: number, cy: number, w: number, h: number): string {
|
||||
const hw = w / 2;
|
||||
const hh = h / 2;
|
||||
return `M ${cx - hw} ${cy - hh} L ${cx + hw} ${cy - hh} L ${cx + hw} ${cy + hh} L ${cx - hw} ${cy + hh} Z`;
|
||||
}
|
||||
|
||||
function generateTrianglePath(cx: number, cy: number, size: number): string {
|
||||
const height = (size * Math.sqrt(3)) / 2;
|
||||
const hs = size / 2;
|
||||
return `M ${cx} ${cy - height / 1.5} L ${cx + hs} ${cy + height / 3} L ${cx - hs} ${cy + height / 3} Z`;
|
||||
}
|
||||
|
||||
function generateStarPath(cx: number, cy: number, outerR: number, innerR: number, points: number): string {
|
||||
const step = Math.PI / points;
|
||||
let path = '';
|
||||
for (let i = 0; i < 2 * points; i++) {
|
||||
const r = i % 2 === 0 ? outerR : innerR;
|
||||
const angle = i * step - Math.PI / 2;
|
||||
const x = cx + r * Math.cos(angle);
|
||||
const y = cy + r * Math.sin(angle);
|
||||
path += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;
|
||||
}
|
||||
return path + ' Z';
|
||||
}
|
||||
|
||||
function generateHeartPath(cx: number, cy: number, size: number): string {
|
||||
const s = size / 30;
|
||||
return `M ${cx} ${cy + 12 * s} C ${cx - 20 * s} ${cy - 5 * s}, ${cx - 12 * s} ${cy - 18 * s}, ${cx} ${cy - 8 * s} C ${cx + 12 * s} ${cy - 18 * s}, ${cx + 20 * s} ${cy - 5 * s}, ${cx} ${cy + 12 * s}`;
|
||||
}
|
||||
|
||||
function generateInfinityPath(cx: number, cy: number, w: number, h: number): string {
|
||||
const hw = w / 2;
|
||||
const hh = h / 2;
|
||||
return `M ${cx} ${cy} C ${cx + hw * 0.5} ${cy - hh}, ${cx + hw} ${cy - hh}, ${cx + hw} ${cy} C ${cx + hw} ${cy + hh}, ${cx + hw * 0.5} ${cy + hh}, ${cx} ${cy} C ${cx - hw * 0.5} ${cy + hh}, ${cx - hw} ${cy + hh}, ${cx - hw} ${cy} C ${cx - hw} ${cy - hh}, ${cx - hw * 0.5} ${cy - hh}, ${cx} ${cy}`;
|
||||
}
|
||||
|
||||
function generateWavePath(cx: number, cy: number, w: number, amplitude: number, waves: number): string {
|
||||
const pts: string[] = [];
|
||||
const segs = waves * 20;
|
||||
const hw = w / 2;
|
||||
for (let i = 0; i <= segs; i++) {
|
||||
const x = cx - hw + (w * i) / segs;
|
||||
const y = cy + Math.sin((i / segs) * waves * 2 * Math.PI) * amplitude;
|
||||
pts.push(i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`);
|
||||
}
|
||||
for (let i = segs; i >= 0; i--) {
|
||||
const x = cx - hw + (w * i) / segs;
|
||||
const y = cy - Math.sin((i / segs) * waves * 2 * Math.PI) * amplitude;
|
||||
pts.push(`L ${x} ${y}`);
|
||||
}
|
||||
return pts.join(' ') + ' Z';
|
||||
}
|
||||
|
||||
const path = computed(() => {
|
||||
const cx = designCenterX.value;
|
||||
const cy = designCenterY.value;
|
||||
|
||||
switch (props.shape) {
|
||||
case 'circle':
|
||||
return generateCirclePath(cx, cy, props.radius);
|
||||
case 'ellipse':
|
||||
return generateEllipsePath(cx, cy, props.radiusX, props.radiusY);
|
||||
case 'square':
|
||||
return generateSquarePath(cx, cy, props.radius * 2);
|
||||
case 'rectangle':
|
||||
return generateRectanglePath(cx, cy, props.radiusX * 2, props.radiusY * 2);
|
||||
case 'triangle':
|
||||
return generateTrianglePath(cx, cy, props.radius * 2);
|
||||
case 'star':
|
||||
return generateStarPath(cx, cy, props.radius, props.radius * props.starInnerRatio, props.starPoints);
|
||||
case 'heart':
|
||||
return generateHeartPath(cx, cy, props.radius * 2);
|
||||
case 'infinity':
|
||||
return generateInfinityPath(cx, cy, props.radiusX * 2, props.radiusY * 2);
|
||||
case 'wave':
|
||||
return generateWavePath(cx, cy, props.radiusX * 2, props.radiusY, 3);
|
||||
case 'custom':
|
||||
return props.customPath || generateCirclePath(cx, cy, props.radius);
|
||||
default:
|
||||
return generateEllipsePath(cx, cy, props.radiusX, props.radiusY);
|
||||
}
|
||||
});
|
||||
|
||||
const containerWidth = computed(() => {
|
||||
return props.responsive ? '100%' : (typeof props.width === 'number' ? `${props.width}px` : '100%');
|
||||
});
|
||||
|
||||
const containerHeight = computed(() => {
|
||||
return props.responsive ? 'auto' : (typeof props.height === 'number' ? `${props.height}px` : (typeof props.width === 'number' ? `${props.width}px` : 'auto'));
|
||||
});
|
||||
|
||||
function updateScale() {
|
||||
if (!props.responsive || !containerRef.value) return;
|
||||
scale.value = containerRef.value.clientWidth / props.baseWidth;
|
||||
}
|
||||
|
||||
function applyEasing(t: number): number {
|
||||
switch (props.easing) {
|
||||
case 'easeIn':
|
||||
return t * t;
|
||||
case 'easeOut':
|
||||
return 1 - (1 - t) * (1 - t);
|
||||
case 'easeInOut':
|
||||
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
||||
case 'linear':
|
||||
default:
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
function animationLoop(currentTime: number) {
|
||||
if (props.paused) {
|
||||
lastTime = null;
|
||||
animationFrameId = requestAnimationFrame(animationLoop);
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastTime === null) {
|
||||
lastTime = currentTime;
|
||||
}
|
||||
|
||||
const deltaTime = (currentTime - lastTime) / 1000;
|
||||
lastTime = currentTime;
|
||||
|
||||
const progressPerSecond = 100 / props.duration;
|
||||
const delta = props.direction === 'reverse' ? -progressPerSecond * deltaTime : progressPerSecond * deltaTime;
|
||||
|
||||
progress.value = ((progress.value + delta) % 100 + 100) % 100;
|
||||
|
||||
animationFrameId = requestAnimationFrame(animationLoop);
|
||||
}
|
||||
|
||||
function startAnimation() {
|
||||
stopAnimation();
|
||||
lastTime = null;
|
||||
animationFrameId = requestAnimationFrame(animationLoop);
|
||||
}
|
||||
|
||||
function stopAnimation() {
|
||||
if (animationFrameId !== null) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function getOffsetDistance(index: number): string {
|
||||
const itemOffset = props.fill ? (index / props.images.length) * 100 : 0;
|
||||
const offset = (((progress.value + itemOffset) % 100) + 100) % 100;
|
||||
return `${offset}%`;
|
||||
}
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
updateScale();
|
||||
startAnimation();
|
||||
|
||||
if (props.responsive && containerRef.value) {
|
||||
resizeObserver = new ResizeObserver(updateScale);
|
||||
resizeObserver.observe(containerRef.value);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAnimation();
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.duration, props.direction, props.paused],
|
||||
() => {
|
||||
if (!props.paused && animationFrameId === null) {
|
||||
startAnimation();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.responsive,
|
||||
(newVal) => {
|
||||
if (newVal && containerRef.value) {
|
||||
updateScale();
|
||||
if (!resizeObserver) {
|
||||
resizeObserver = new ResizeObserver(updateScale);
|
||||
resizeObserver.observe(containerRef.value);
|
||||
}
|
||||
} else if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
scale.value = 1;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const offsetDistances = computed(() => {
|
||||
return props.images.map((_, index) => {
|
||||
const itemOffset = props.fill ? (index / props.images.length) * 100 : 0;
|
||||
const offset = (((progress.value + itemOffset) % 100) + 100) % 100;
|
||||
return `${offset}%`;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
:class="`relative mx-auto ${props.className}`"
|
||||
:style="{
|
||||
width: containerWidth,
|
||||
height: containerHeight,
|
||||
aspectRatio: props.responsive ? '1 / 1' : undefined
|
||||
}"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
:class="props.responsive ? 'absolute left-1/2 top-1/2' : 'relative w-full h-full'"
|
||||
:style="{
|
||||
width: props.responsive ? `${props.baseWidth}px` : '100%',
|
||||
height: props.responsive ? `${props.baseWidth}px` : '100%',
|
||||
transform: props.responsive ? `translate(-50%, -50%) scale(${scale})` : undefined,
|
||||
transformOrigin: 'center center'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="relative w-full h-full"
|
||||
:style="{
|
||||
transform: `rotate(${props.rotation}deg)`,
|
||||
transformOrigin: 'center center'
|
||||
}"
|
||||
>
|
||||
<!-- SVG Path for debugging -->
|
||||
<svg
|
||||
v-if="props.showPath"
|
||||
width="100%"
|
||||
height="100%"
|
||||
:viewBox="`0 0 ${props.baseWidth} ${props.baseWidth}`"
|
||||
class="absolute inset-0 pointer-events-none"
|
||||
>
|
||||
<path
|
||||
:d="path"
|
||||
fill="none"
|
||||
:stroke="props.pathColor"
|
||||
:stroke-width="props.pathWidth / scale"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- Orbit Items -->
|
||||
<div
|
||||
v-for="(src, index) in props.images"
|
||||
:key="src"
|
||||
class="absolute will-change-transform select-none"
|
||||
:style="{
|
||||
width: `${props.itemSize}px`,
|
||||
height: `${props.itemSize}px`,
|
||||
offsetPath: `path('${path}')`,
|
||||
offsetRotate: '0deg',
|
||||
offsetAnchor: 'center center',
|
||||
offsetDistance: offsetDistances[index]
|
||||
}"
|
||||
>
|
||||
<div :style="{ transform: `rotate(${-props.rotation}deg)` }">
|
||||
<img
|
||||
:src="src"
|
||||
:alt="`${props.altPrefix} ${index + 1}`"
|
||||
:draggable="false"
|
||||
class="w-full h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center Content Slot -->
|
||||
<div
|
||||
v-if="$slots.centerContent"
|
||||
class="absolute inset-0 flex items-center justify-center z-10"
|
||||
>
|
||||
<slot name="centerContent" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
200
src/demo/Animations/OrbitImagesDemo.vue
Normal file
200
src/demo/Animations/OrbitImagesDemo.vue
Normal file
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div class="demo-container h-[400px] overflow-hidden">
|
||||
<RefreshButton @click="forceRerender" />
|
||||
<OrbitImages
|
||||
:key="key"
|
||||
:images="images"
|
||||
:shape="shape"
|
||||
:radius-x="radiusX"
|
||||
:radius-y="radiusY"
|
||||
:radius="radius"
|
||||
:rotation="rotation"
|
||||
:duration="duration"
|
||||
:item-size="itemSize"
|
||||
:direction="direction"
|
||||
:fill="fill"
|
||||
:show-path="showPath"
|
||||
:paused="paused"
|
||||
:responsive="true"
|
||||
path-color="rgba(255,255,255,0.15)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Customize>
|
||||
<PreviewSelect
|
||||
title="Shape"
|
||||
v-model="shape"
|
||||
:options="shapeOptions"
|
||||
/>
|
||||
|
||||
<PreviewSelect
|
||||
title="Direction"
|
||||
v-model="direction"
|
||||
:options="directionOptions"
|
||||
/>
|
||||
|
||||
<PreviewSlider
|
||||
title="Radius X"
|
||||
v-model="radiusX"
|
||||
:min="50"
|
||||
:max="600"
|
||||
:step="10"
|
||||
value-unit="px"
|
||||
/>
|
||||
|
||||
<PreviewSlider
|
||||
title="Radius Y"
|
||||
v-model="radiusY"
|
||||
:min="50"
|
||||
:max="600"
|
||||
:step="10"
|
||||
value-unit="px"
|
||||
/>
|
||||
|
||||
<PreviewSlider
|
||||
title="Radius"
|
||||
v-model="radius"
|
||||
:min="50"
|
||||
:max="600"
|
||||
:step="10"
|
||||
value-unit="px"
|
||||
/>
|
||||
|
||||
<PreviewSlider
|
||||
title="Rotation"
|
||||
v-model="rotation"
|
||||
:min="-180"
|
||||
:max="180"
|
||||
:step="1"
|
||||
value-unit="°"
|
||||
/>
|
||||
|
||||
<PreviewSlider
|
||||
title="Duration"
|
||||
v-model="duration"
|
||||
:min="5"
|
||||
:max="120"
|
||||
:step="5"
|
||||
value-unit="s"
|
||||
/>
|
||||
|
||||
<PreviewSlider
|
||||
title="Item Size"
|
||||
v-model="itemSize"
|
||||
:min="20"
|
||||
:max="120"
|
||||
:step="4"
|
||||
value-unit="px"
|
||||
/>
|
||||
|
||||
<PreviewSwitch title="Fill (Distribute Evenly)" v-model="fill" />
|
||||
|
||||
<PreviewSwitch title="Show Path" v-model="showPath" />
|
||||
|
||||
<PreviewSwitch title="Paused" v-model="paused" />
|
||||
</Customize>
|
||||
|
||||
<PropTable :data="propData" />
|
||||
|
||||
<Dependencies :dependency-list="[]" />
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<CodeExample :code-object="orbitImages" />
|
||||
</template>
|
||||
|
||||
<template #cli>
|
||||
<CliInstallation :command="orbitImages.cli" />
|
||||
</template>
|
||||
</TabbedLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } 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 PreviewSwitch from '../../components/common/PreviewSwitch.vue';
|
||||
import PreviewSelect from '../../components/common/PreviewSelect.vue';
|
||||
import RefreshButton from '../../components/common/RefreshButton.vue';
|
||||
import OrbitImages from '../../content/Animations/OrbitImages/OrbitImages.vue';
|
||||
import { orbitImages } from '@/constants/code/Animations/orbitImagesCode';
|
||||
import { useForceRerender } from '@/composables/useForceRerender';
|
||||
|
||||
const { rerenderKey: key, forceRerender } = useForceRerender();
|
||||
|
||||
const images = [
|
||||
'https://picsum.photos/300/300?grayscale&random=1',
|
||||
'https://picsum.photos/300/300?grayscale&random=2',
|
||||
'https://picsum.photos/300/300?grayscale&random=3',
|
||||
'https://picsum.photos/300/300?grayscale&random=4',
|
||||
'https://picsum.photos/300/300?grayscale&random=5',
|
||||
'https://picsum.photos/300/300?grayscale&random=6'
|
||||
];
|
||||
|
||||
const shape = ref<string>('ellipse');
|
||||
const radiusX = ref(340);
|
||||
const radiusY = ref(80);
|
||||
const radius = ref(160);
|
||||
const rotation = ref(-8);
|
||||
const duration = ref(30);
|
||||
const itemSize = ref(80);
|
||||
const direction = ref<'normal' | 'reverse'>('normal');
|
||||
const fill = ref(true);
|
||||
const showPath = ref(true);
|
||||
const paused = ref(false);
|
||||
|
||||
const shapeOptions = [
|
||||
{ label: 'Ellipse', value: 'ellipse' },
|
||||
{ label: 'Circle', value: 'circle' },
|
||||
{ label: 'Square', value: 'square' },
|
||||
{ label: 'Rectangle', value: 'rectangle' },
|
||||
{ label: 'Triangle', value: 'triangle' },
|
||||
{ label: 'Star', value: 'star' },
|
||||
{ label: 'Heart', value: 'heart' },
|
||||
{ label: 'Infinity', value: 'infinity' },
|
||||
{ label: 'Wave', value: 'wave' }
|
||||
];
|
||||
|
||||
const directionOptions = [
|
||||
{ label: 'Normal', value: 'normal' },
|
||||
{ label: 'Reverse', value: 'reverse' }
|
||||
];
|
||||
|
||||
const propData = [
|
||||
{ name: 'images', type: 'string[]', default: '[]', description: 'Array of image URLs to orbit along the path.' },
|
||||
{ name: 'altPrefix', type: 'string', default: '"Orbiting image"', description: 'Prefix for auto-generated alt attributes.' },
|
||||
{ name: 'shape', type: 'OrbitShape', default: '"ellipse"', description: 'Preset shape: ellipse, circle, square, rectangle, triangle, star, heart, infinity, wave, or custom.' },
|
||||
{ name: 'customPath', type: 'string', default: 'undefined', description: 'Custom SVG path string (used when shape="custom").' },
|
||||
{ name: 'baseWidth', type: 'number', default: '1400', description: 'Base width for the design coordinate space used for responsive scaling.' },
|
||||
{ name: 'radiusX', type: 'number', default: '700', description: 'Horizontal radius for ellipse/rectangle shapes.' },
|
||||
{ name: 'radiusY', type: 'number', default: '170', description: 'Vertical radius for ellipse/rectangle shapes.' },
|
||||
{ name: 'radius', type: 'number', default: '300', description: 'Radius for circle, square, triangle, star, heart shapes.' },
|
||||
{ name: 'starPoints', type: 'number', default: '5', description: 'Number of points for star shape.' },
|
||||
{ name: 'starInnerRatio', type: 'number', default: '0.5', description: 'Inner radius ratio for star (0-1).' },
|
||||
{ name: 'rotation', type: 'number', default: '-8', description: 'Rotation angle of the entire orbit path in degrees.' },
|
||||
{ name: 'duration', type: 'number', default: '40', description: 'Duration of one complete orbit in seconds.' },
|
||||
{ name: 'itemSize', type: 'number', default: '64', description: 'Width/height of each orbiting item in pixels.' },
|
||||
{ name: 'direction', type: '"normal" | "reverse"', default: '"normal"', description: 'Animation direction.' },
|
||||
{ name: 'fill', type: 'boolean', default: 'true', description: 'Whether to distribute items evenly around the orbit.' },
|
||||
{ name: 'width', type: 'number | "100%"', default: '100', description: 'Container width in pixels or "100%".' },
|
||||
{ name: 'height', type: 'number | "auto"', default: '100', description: 'Container height in pixels or "auto".' },
|
||||
{ name: 'className', type: 'string', default: '""', description: 'Additional CSS class for the container.' },
|
||||
{ name: 'showPath', type: 'boolean', default: 'false', description: 'Whether to show the orbit path for debugging.' },
|
||||
{ name: 'pathColor', type: 'string', default: '"rgba(0,0,0,0.1)"', description: 'Stroke color when showPath is true.' },
|
||||
{ name: 'pathWidth', type: 'number', default: '2', description: 'Stroke width when showPath is true.' },
|
||||
{ name: 'easing', type: 'string', default: '"linear"', description: 'Animation easing: linear, easeIn, easeOut, easeInOut.' },
|
||||
{ name: 'paused', type: 'boolean', default: 'false', description: 'Whether the animation is paused.' },
|
||||
{ name: 'responsive', type: 'boolean', default: 'false', description: 'Enable responsive scaling based on container width.' }
|
||||
];
|
||||
|
||||
watch([shape, direction], () => {
|
||||
forceRerender();
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user