feat(stage-web|stage-tamagotchi): better playground, new Volumed, CursorMomentum, and TestDummyMarker components

This commit is contained in:
Neko Ayaka
2025-03-17 18:46:58 +08:00
parent edddfeb81e
commit 097633c707
14 changed files with 458 additions and 10 deletions
@@ -13,6 +13,7 @@ import {
ProviderBasicSettings,
ProviderSettingsContainer,
ProviderSettingsLayout,
TestDummyMarker,
} from '@proj-airi/stage-ui/components'
import { voiceMap } from '@proj-airi/stage-ui/constants'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
@@ -310,9 +311,14 @@ function handleResetVoiceSettings() {
</ProviderSettingsContainer>
<div flex="~ col gap-6" class="w-full md:w-[60%]">
<div rounded-xl>
<h2 class="mb-4 text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
{{ t('settings.pages.providers.provider.elevenlabs.playground.title') }}
<div w-full rounded-xl>
<h2 class="mb-4 text-lg text-neutral-500 md:text-2xl dark:text-neutral-400" w-full>
<div class="inline-flex items-center gap-4">
<TestDummyMarker />
<div>
{{ t('settings.pages.providers.provider.elevenlabs.playground.title') }}
</div>
</div>
</h2>
<div flex="~ col gap-4">
<textarea
@@ -13,6 +13,7 @@ import {
ProviderBasicSettings,
ProviderSettingsContainer,
ProviderSettingsLayout,
TestDummyMarker,
} from '@proj-airi/stage-ui/components'
import { voiceMap } from '@proj-airi/stage-ui/constants'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
@@ -310,9 +311,14 @@ function handleResetVoiceSettings() {
</ProviderSettingsContainer>
<div flex="~ col gap-6" class="w-full md:w-[60%]">
<div rounded-xl>
<h2 class="mb-4 text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
{{ t('settings.pages.providers.provider.elevenlabs.playground.title') }}
<div w-full rounded-xl>
<h2 class="mb-4 text-lg text-neutral-500 md:text-2xl dark:text-neutral-400" w-full>
<div class="inline-flex items-center gap-4">
<TestDummyMarker />
<div>
{{ t('settings.pages.providers.provider.elevenlabs.playground.title') }}
</div>
</div>
</h2>
<div flex="~ col gap-4">
<textarea
+16 -4
View File
@@ -16,18 +16,30 @@ export default defineConfig({
id: 'design-system',
title: 'Design System',
},
{
id: 'menu',
title: 'Menu',
},
{
id: 'form',
title: 'Form',
},
{
id: 'menu',
title: 'Menu',
},
{
id: 'providers',
title: 'Providers',
},
{
id: 'physics',
title: 'Physics',
},
{
id: 'graphics',
title: 'Graphics',
},
{
id: 'gadgets',
title: 'Gadgets',
},
{
id: 'data-gui',
title: 'Data GUI',
@@ -0,0 +1,87 @@
<script setup lang="ts">
import Volumed from '../Graphics/Volumed.vue'
import CursorMomentum from '../Physics/CursorMomentum.vue'
import TestDummyMarker from './TestDummyMarker.vue'
import TestDummyMarkerFlat from './TestDummyMarkerFlat.vue'
</script>
<template>
<Story
title="Test Dummy Marker"
group="gadgets"
:layout="{ type: 'grid', width: 600 }"
>
<template #controls>
<ThemeColorsHueControl />
</template>
<Variant
id="flat"
title="Flat Version"
>
<div class="h-40 flex items-center justify-center">
<TestDummyMarkerFlat :size="40" />
</div>
</Variant>
<Variant
id="3d-basic"
title="3D Basic"
>
<div class="h-40 flex items-center justify-center">
<Volumed :perspective="800">
<TestDummyMarkerFlat :size="40" />
</Volumed>
</div>
</Variant>
<Variant
id="3d-tilted"
title="3D Tilted"
>
<div class="h-40 flex items-center justify-center">
<Volumed
:perspective="800"
transform="rotateX(45deg)"
>
<TestDummyMarkerFlat :size="40" />
</Volumed>
</div>
</Variant>
<Variant
id="with-momentum"
title="With Cursor Momentum"
>
<div class="h-40 flex items-center justify-center">
<CursorMomentum
v-slot="{ currentValue }"
:base-speed="0.1"
:friction="0.95"
:momentum-factor="0.005"
>
<Volumed
:perspective="800"
transform="rotateX(45deg)"
>
<TestDummyMarkerFlat
:size="40"
:style="{
transform: `rotate(${currentValue}deg)`,
}"
/>
</Volumed>
</CursorMomentum>
</div>
</Variant>
<Variant
id="original"
title="Original Implementation"
>
<div class="h-40 flex items-center justify-center">
<TestDummyMarker :size="40" />
</div>
</Variant>
</Story>
</template>
@@ -0,0 +1,13 @@
<script setup lang="ts">
import Volumed from '../Graphics/Volumed.vue'
import CursorMomentum from '../Physics/CursorMomentum.vue'
import TestDummyMarkerFlat from './TestDummyMarkerFlat.vue'
</script>
<template>
<CursorMomentum v-slot="{ currentValue }" :base-speed="0.1" :friction="0.95" :momentum-factor="0.0005">
<Volumed :perspective="800" transform="rotateX(45deg) translateY(3px)">
<TestDummyMarkerFlat :size="24" :style="{ transform: `rotate(${currentValue}deg)` }" />
</Volumed>
</CursorMomentum>
</template>
@@ -0,0 +1,36 @@
<script setup lang="ts">
defineProps<{
size?: number
}>()
</script>
<template>
<div
class="test-dummy-flat"
:style="{
width: `${size}px`,
height: `${size}px`,
}"
>
<div class="marker" />
</div>
</template>
<style scoped>
.test-dummy-flat {
display: inline-block;
position: relative;
}
.test-dummy-flat .marker {
position: absolute;
inset: 0;
background: conic-gradient(#ffeb3b 0deg 90deg, #000 90deg 180deg, #ffeb3b 180deg 270deg, #000 270deg 360deg);
border-radius: 50%;
box-shadow: 0 4px 12px rgba(185, 185, 185, 0.2);
}
.dark .test-dummy-flat .marker {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
</style>
@@ -0,0 +1 @@
export { default as TestDummyMarker } from './TestDummyMarker.vue'
@@ -0,0 +1,53 @@
<script setup lang="ts">
import Volumed from './Volumed.vue'
</script>
<template>
<Story
title="Volumed"
group="graphics"
:layout="{ type: 'grid', width: 600 }"
>
<template #controls>
<ThemeColorsHueControl />
</template>
<Variant
id="basic"
title="Basic 3D"
>
<div class="h-40 flex items-center justify-center">
<Volumed :perspective="800">
<div class="bg-primary-500/20 h-20 w-20" />
</Volumed>
</div>
</Variant>
<Variant
id="rotated"
title="Rotated"
>
<div class="h-40 flex items-center justify-center">
<Volumed
:perspective="800"
transform="rotateX(45deg) rotateY(45deg)"
>
<div class="bg-primary-500/20 h-20 w-20" />
</Volumed>
</div>
</Variant>
<Variant
id="nested"
title="Nested 3D"
>
<div class="h-40 flex items-center justify-center">
<Volumed :perspective="800">
<Volumed transform="rotateX(45deg)">
<div class="bg-primary-500/20 h-20 w-20" />
</Volumed>
</Volumed>
</div>
</Variant>
</Story>
</template>
@@ -0,0 +1,26 @@
<script setup lang="ts">
defineProps<{
perspective?: number
transform?: string
}>()
</script>
<template>
<div
class="volumed"
:style="{
perspective: `${perspective}px`,
transform,
}"
>
<slot />
</div>
</template>
<style scoped>
.volumed {
transform-style: preserve-3d;
width: fit-content;
height: fit-content;
}
</style>
@@ -0,0 +1 @@
export { default as Volumed } from './Volumed.vue'
@@ -0,0 +1,133 @@
<script setup lang="ts">
import { ref } from 'vue'
import CursorMomentum from './CursorMomentum.vue'
// Example components that use momentum
const rotatingBox = ref(null)
const floatingBall = ref(null)
const pulsingCircle = ref(null)
</script>
<template>
<Story
title="Cursor Momentum"
group="physics"
:layout="{ type: 'grid', width: 600 }"
>
<template #controls>
<ThemeColorsHueControl />
</template>
<Variant
id="rotating-box"
title="Rotating Box"
>
<div class="h-40 flex items-center justify-center">
<CursorMomentum
v-slot="{ currentValue }"
:base-speed="0.1"
:friction="0.95"
:momentum-factor="0.005"
>
<div
ref="rotatingBox"
class="bg-primary-500/20 h-20 w-20"
:style="{
transform: `rotate(${currentValue}deg)`,
}"
/>
</CursorMomentum>
</div>
</Variant>
<Variant
id="floating-ball"
title="Floating Ball"
>
<div class="h-40 flex items-center justify-center">
<CursorMomentum
v-slot="{ momentum }"
:base-speed="0.5"
:friction="0.98"
:momentum-factor="0.01"
>
<div
ref="floatingBall"
class="bg-primary-500/20 h-10 w-10 rounded-full"
:style="{
transform: `translateY(${Math.sin(momentum * 0.1) * 20}px)`,
}"
/>
</CursorMomentum>
</div>
</Variant>
<Variant
id="pulsing-circle"
title="Pulsing Circle"
>
<div class="h-40 flex items-center justify-center">
<CursorMomentum
v-slot="{ momentum }"
:base-speed="1"
:friction="0.9"
:momentum-factor="0.008"
>
<div
ref="pulsingCircle"
class="bg-primary-500/20 h-16 w-16 rounded-full"
:style="{
transform: `scale(${0.8 + (momentum * 0.2)})`,
}"
/>
</CursorMomentum>
</div>
</Variant>
<Variant
id="test-dummy"
title="Test Dummy Marker"
>
<div class="h-40 flex items-center justify-center">
<CursorMomentum
v-slot="{ currentValue }"
:base-speed="0.1"
:friction="0.95"
:momentum-factor="0.005"
>
<div
class="crash-test-dummy"
:style="{
width: '40px',
height: '40px',
transform: `rotateX(45deg) rotate(${currentValue}deg)`,
}"
>
<div class="marker" />
</div>
</CursorMomentum>
</div>
</Variant>
</Story>
</template>
<style scoped>
.crash-test-dummy {
display: inline-block;
position: relative;
transform-style: preserve-3d;
}
.crash-test-dummy .marker {
position: absolute;
inset: 0;
background: conic-gradient(#ffeb3b 0deg 90deg, #000 90deg 180deg, #ffeb3b 180deg 270deg, #000 270deg 360deg);
border-radius: 50%;
box-shadow: 0 4px 12px rgba(185, 185, 185, 0.2);
}
.dark .crash-test-dummy .marker {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
</style>
@@ -0,0 +1,70 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref, toRef } from 'vue'
const props = withDefaults(defineProps<{
baseSpeed?: number
friction?: number
momentumFactor?: number
}>(), {
baseSpeed: 0.1,
friction: 0.95,
momentumFactor: 0.005,
})
const momentum = ref(1) // Base momentum
const currentValue = ref(0)
let lastTimestamp = 0
// Physics parameters with defaults
const FRICTION = toRef(() => props.friction)
const BASE_SPEED = toRef(() => props.baseSpeed)
const MOMENTUM_FACTOR = toRef(() => props.momentumFactor)
function updateMomentum(timestamp: number) {
if (!lastTimestamp)
lastTimestamp = timestamp
const deltaTime = timestamp - lastTimestamp
lastTimestamp = timestamp
// Apply friction to gradually return to base speed
momentum.value = BASE_SPEED.value + (momentum.value - BASE_SPEED.value) * FRICTION.value
// Update value based on current momentum
currentValue.value += momentum.value * deltaTime
requestAnimationFrame(updateMomentum)
}
function handleMouseMove(event: MouseEvent) {
// Calculate movement speed
const speed = Math.sqrt(
event.movementX ** 2
+ event.movementY ** 2,
)
// Add to current momentum
momentum.value += speed * MOMENTUM_FACTOR.value
}
onMounted(() => {
window.addEventListener('mousemove', handleMouseMove)
requestAnimationFrame(updateMomentum)
})
onUnmounted(() => {
window.removeEventListener('mousemove', handleMouseMove)
})
// Expose values and momentum to parent
defineExpose({
momentum,
currentValue,
})
</script>
<template>
<slot
:momentum="momentum"
:current-value="currentValue"
/>
</template>
@@ -0,0 +1 @@
export { default as CursorMomentum } from './CursorMomentum.vue'
@@ -1,9 +1,12 @@
export { default as BasicTextarea } from './BasicTextarea.vue'
export { default as Collapsable } from './Collapsable.vue'
export * from './Form'
export * from './Gadgets'
export * from './Graphics'
export { default as Live2DCanvas } from './Live2D/Canvas.vue'
export { default as Live2DModel } from './Live2D/Model.vue'
export * from './Menu'
export * from './Physics'
export * from './Providers'
export { default as SceneLive2D } from './Scenes/Live2D.vue'
export { default as SceneVRM } from './Scenes/VRM.vue'