feat(stage-web): I Want It Dynamic! (#64)

* feat(stage-web): I Want It Dynamic!
* a hacky way to make the live2d shadow dynamic
* fix types, just use 0 for rafId as a valid raf return will never be 0
* fix type
This commit is contained in:
junkwarrior87
2025-03-10 10:50:26 +08:00
committed by GitHub
parent 7536352699
commit 966a71b282
4 changed files with 95 additions and 11 deletions
+26
View File
@@ -18,6 +18,10 @@ watch(settings.language, () => {
watch(settings.themeColorsHue, () => {
document.documentElement.style.setProperty('--theme-colors-hue', settings.themeColorsHue.value.toString())
}, { immediate: true })
watch(settings.themeColorsHueDynamic, () => {
document.documentElement.classList.toggle('dynamic-hue', settings.themeColorsHueDynamic.value)
}, { immediate: true })
</script>
<template>
@@ -32,3 +36,25 @@ watch(settings.themeColorsHue, () => {
<RouterView />
</StageTransitionGroup>
</template>
<style>
/* We need this to properly animate the CSS variable */
@property --theme-colors-hue {
syntax: '<number>';
initial-value: 0;
inherits: true;
}
@keyframes hue-anim {
from {
--theme-colors-hue: 0;
}
to {
--theme-colors-hue: 360;
}
}
.dynamic-hue {
animation: hue-anim 10s linear infinite;
}
</style>
@@ -1,10 +1,15 @@
<script setup lang="ts">
import { Collapsable } from '@proj-airi/stage-ui/components'
import { useSettings } from '@proj-airi/stage-ui/stores'
import { DEFAULT_THEME_COLORS_HUE, useSettings } from '@proj-airi/stage-ui/stores'
import { useRouter } from 'vue-router'
const router = useRouter()
const settings = useSettings()
function resetToDefault() {
settings.themeColorsHue = DEFAULT_THEME_COLORS_HUE
settings.themeColorsHueDynamic = false
}
</script>
<template>
@@ -51,7 +56,16 @@ const settings = useSettings()
Primary color
</div>
<input v-model="settings.themeColorsHue" type="range" min="0" max="360" step="1" class="theme-hue-slider">
<input
v-model="settings.themeColorsHue"
type="range"
min="0"
max="360"
step="0.01"
class="theme-hue-slider"
:disabled="settings.themeColorsHueDynamic"
:class="{ 'opacity-25 cursor-not-allowed': settings.themeColorsHueDynamic }"
>
</div>
<div mt-4 h-10 w-full flex overflow-hidden rounded-lg>
<div bg="primary-50" class="primary-color-bar" text-black>
@@ -124,6 +138,26 @@ const settings = useSettings()
500
</div>
</div>
<div mt-4 class="flex items-center justify-end gap-4">
<label class="relative inline-flex cursor-pointer items-center">
<input
v-model="settings.themeColorsHueDynamic"
type="checkbox"
class="peer sr-only"
>
<div
class="peer-checked:bg-primary-500 h-6 w-11 rounded-full bg-neutral-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:bg-white dark:bg-neutral-600 after:transition-all after:content-[''] peer-checked:after:translate-x-full peer-checked:after:border-white"
/>
<span class="ml-2 text-sm font-medium">I Want It Dynamic!</span>
</label>
<button
class="rounded-md bg-neutral-100 px-3 py-1.5 text-sm transition-colors dark:bg-neutral-800 hover:bg-neutral-200 dark:hover:bg-neutral-700"
@click="resetToDefault"
>
Reset to Default
</button>
</div>
</div>
</Collapsable>
<div fixed bottom-0 right-0 z--1 text="neutral-100/80 dark:neutral-500/20">
@@ -38,6 +38,12 @@ const dark = useDark()
const breakpoints = useBreakpoints(breakpointsTailwind)
const isMobile = computed(() => breakpoints.between('sm', 'md').value || breakpoints.smaller('sm').value)
const idleEyeFocus = useLive2DIdleEyeFocus()
const dropShadowFilter = ref<DropShadowFilter>(new DropShadowFilter({
alpha: 0.2,
blur: 0,
distance: 20,
rotation: 45,
}))
function getCoreModel() {
return model.value!.internalModel.coreModel as any
@@ -67,6 +73,7 @@ const {
live2dLoadSource,
live2dModelUrl,
themeColorsHue,
themeColorsHueDynamic,
} = storeToRefs(useSettings())
const currentMotion = ref<{ group: string, index: number }>({ group: 'Idle', index: 0 })
@@ -199,24 +206,36 @@ const handleResize = useDebounceFn(() => {
}, 100)
const dropShadowColorComputer = ref<HTMLDivElement>()
const dropShadowAnimationId = ref(0)
function updateDropShadowFilter() {
if (model.value) {
const color = getComputedStyle(dropShadowColorComputer.value!).backgroundColor
const colorNumber = Number(formatHex(color)!.replace('#', '0x'))
model.value.filters = [new DropShadowFilter({
color: colorNumber,
alpha: 0.2,
blur: 0,
distance: 20,
rotation: 45,
})]
dropShadowFilter.value.color = Number(formatHex(color)!.replace('#', '0x'))
model.value.filters = [dropShadowFilter.value]
}
}
watch([() => props.width, () => props.height], () => handleResize())
watch(dark, updateDropShadowFilter, { immediate: true })
watch([model, themeColorsHue], updateDropShadowFilter)
// TODO: This is hacky!
function updateDropShadowFilterLoop() {
updateDropShadowFilter()
dropShadowAnimationId.value = requestAnimationFrame(updateDropShadowFilterLoop)
}
watch(themeColorsHueDynamic, () => {
if (themeColorsHueDynamic.value) {
dropShadowAnimationId.value = requestAnimationFrame(updateDropShadowFilterLoop)
}
else {
cancelAnimationFrame(dropShadowAnimationId.value)
dropShadowAnimationId.value = 0
}
}, { immediate: true })
watch(mouthOpenSize, value => getCoreModel().setParameterValueById('ParamMouthOpenY', value))
watch(pixiApp, initLive2DPixiStage)
watch(live2dCurrentMotion, value => setMotion(value.group, value.index))
@@ -233,6 +252,7 @@ watchDebounced(loadingLive2dModel, (value) => {
onMounted(updateDropShadowFilter)
onUnmounted(() => {
cancelAnimationFrame(dropShadowAnimationId.value)
model.value && pixiApp.value?.stage.removeChild(model.value)
})
</script>
+5 -1
View File
@@ -4,6 +4,8 @@ import { computed, onMounted, ref, watch } from 'vue'
import { Voice } from '../constants/elevenlabs'
export const DEFAULT_THEME_COLORS_HUE = 354.31
export const useSettings = defineStore('settings', () => {
const selectedAudioDevice = ref<MediaDeviceInfo>()
@@ -35,7 +37,8 @@ export const useSettings = defineStore('settings', () => {
const disableTransitions = useLocalStorage('settings/disable-transitions', false)
const themeColorsHue = useLocalStorage('settings/theme/colors/hue', 354.31)
const themeColorsHue = useLocalStorage('settings/theme/colors/hue', DEFAULT_THEME_COLORS_HUE)
const themeColorsHueDynamic = useLocalStorage('settings/theme/colors/hue-dynamic', false)
watch(isAudioInputOn, (value) => {
if (value === 'false') {
@@ -75,6 +78,7 @@ export const useSettings = defineStore('settings', () => {
language,
stageView,
themeColorsHue,
themeColorsHueDynamic,
isAudioInputOn,
selectedAudioDevice,
selectedAudioDeviceId,