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:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user