Skip to content

Every little fish swimming around is a visitor on this page right now, and one of them is you ( ´ ▽ ` )ノ If they get in the way of reading, feel free to turn them off.

Motion Gallery

util

A gallery where layouts transform freely and dynamically. Groovy!

NameDescription
Babylon.js3D engine
Anime.jsLightweight JavaScript animation library
JS AnimationJavaScript-driven animation for more complex, precise control; popular libraries include GSAP and anime.js
Physics SimulationSimulates real-world physics such as gravity, collisions, and velocity

Watch the image layout dynamically change when you switch the type. Magic!

loading...
<template>
<div class="mt-4 w-full flex flex-col gap-4">
<util-motion-gallery
v-slot="{ isLoading }"
class="h-full"
:image-list
:layout
>
<transition name="opacity">
<div
v-if="isLoading"
class="absolute h-full w-full flex-center bg-black/50"
>
<div class="flex flex-col items-center gap-4">
<div class="h-10 w-10 animate-spin border-4 border-white border-t-transparent rounded-full" />
<span class="text-2xl text-white tracking-widest">
loading...
</span>
</div>
</div>
</transition>
</util-motion-gallery>
<select-stepper
v-model="type"
:options
class="w-full text-center"
/>
</div>
</template>
<script setup lang="ts">
import type { ComponentProps } from 'vue-component-type-helpers'
import { computed, ref } from 'vue'
import SelectStepper from '../../select-stepper.vue'
import UtilMotionGallery from '../util-motion-gallery.vue'
type Props = ComponentProps<typeof UtilMotionGallery>
type LayoutType = NonNullable<Props['layout']>['type']
const imageList: [string, ...string[]] = [
'/low/painting-codfish-bakery.webp',
'/low/painting-codfish-rain.webp',
'/low/photography-fireworks.webp',
'/low/photography-morning-light-of-rice.webp',
'/low/photography-big-stupid-bird.webp',
'/low/photography-ears-of-rice.webp',
'/low/photography-gaomei-windmill.webp',
'/low/photography-spider-at-night.webp',
'/low/photography-street-cat.webp',
'/low/photography-afternoon-cicada-chirping.webp',
'/low/photography-whispers-of-the-setting-sun.webp',
'/low/photography-city-of-lights-from-the-mountainside.webp',
]
const type = ref<LayoutType>('helix')
const layout = computed(() => ({
type: type.value,
}))
const options: LayoutType[] = [
'grid',
'wheel',
'tunnel',
'ball',
'helix',
'folder',
]
</script>
<style scoped lang="sass">
.opacity-enter-active, .opacity-leave-active
transition-duration: 0.4s
.opacity-enter-from, .opacity-leave-to
opacity: 0 !important
</style>

It leverages the power of babylon.js for all the 3D object transformations like rotation, scaling, and translation, spiced up with anime.js for smooth animations.

When rotating objects in 3D space, the order of rotation around each axis matters—a lot! It totally changes the final result.

So, rotating 90 degrees on each axis in XYZ order gives a different outcome than ZYX or YZX. Who knew, right?! ( ゚ ∀。)

For the nitty-gritty details, check out Sequencing Rotations

My head spins just thinking about it sometimes ~( ゚ ∀。)~

type Layout = {
type: 'grid';
gap?: number;
} | {
type: 'wheel';
radius?: number;
} | {
type: 'tunnel';
radius?: number;
depthGap?: number;
} | {
type: 'ball';
radius?: number;
} | {
type: 'helix';
/** 螺旋半徑,預設 1 */
radius?: number;
/** 螺距(每圈上升的高度),預設 2 */
pitch?: number;
} | {
type: 'folder';
/** 照片之間的重疊間距 */
overlap?: number;
/** 聚焦照片升起的高度 */
riseHeight?: number;
}
interface Props {
layout?: Layout;
/** 不足 30 張會自動重複填充至 30 張 */
imageList: [string, ...string[]];
fpsVisible?: boolean;
/** 背景色。無限遠處的顏色 */
clearColor?: Color4;
/** 霧化色。足夠遠處的照片會被此顏色籠罩 */
fogColor?: Color3;
}