feat(stage-*): dramatically improved mobile
This commit is contained in:
@@ -331,6 +331,7 @@ watch(() => messages.value.length, () => {
|
||||
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
line-clamp: 2;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import WhisperWorker from '@proj-airi/stage-ui/libs/workers/worker?worker&url'
|
||||
import { toWAVBase64 } from '@proj-airi/audio'
|
||||
import { useMicVAD, useWhisper } from '@proj-airi/stage-ui/composables'
|
||||
import { useAudioContext, useChatStore, useConsciousnessStore, useProvidersStore, useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { BasicTextarea, TransitionVertical } from '@proj-airi/ui'
|
||||
import { BasicTextarea } from '@proj-airi/ui'
|
||||
import { useDevicesList } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
@@ -24,8 +24,8 @@ const providersStore = useProvidersStore()
|
||||
const { activeProvider, activeModel } = storeToRefs(useConsciousnessStore())
|
||||
const { themeColorsHueDynamic } = storeToRefs(useSettings())
|
||||
|
||||
const { audioInputs, ensurePermissions } = useDevicesList({ constraints: { audio: true } })
|
||||
const { selectedAudioDevice, isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
|
||||
const { ensurePermissions } = useDevicesList({ constraints: { audio: true } })
|
||||
const { isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
|
||||
const { send, onAfterSend, discoverToolsCompatibility } = useChatStore()
|
||||
const { messages } = storeToRefs(useChatStore())
|
||||
const { audioContext } = useAudioContext()
|
||||
@@ -104,17 +104,6 @@ async function handleTranscription(buffer: ArrayBufferLike) {
|
||||
generate({ type: 'generate', data: { audio: audioBase64, language: 'en' } })
|
||||
}
|
||||
|
||||
async function handleAudioInputChange(event: Event) {
|
||||
const target = event.target as HTMLSelectElement
|
||||
const found = audioInputs.value.find(d => d.deviceId === target.value)
|
||||
if (!found) {
|
||||
selectedAudioDevice.value = undefined
|
||||
return
|
||||
}
|
||||
|
||||
selectedAudioDevice.value = found
|
||||
}
|
||||
|
||||
watch(isAudioInputOn, async (value) => {
|
||||
if (value === 'false') {
|
||||
destroy()
|
||||
@@ -195,46 +184,5 @@ onAfterSend(async () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div flex="~ row" gap-2 hidden>
|
||||
<div flex="~ row" relative text-white font-normal>
|
||||
<TransitionVertical>
|
||||
<fieldset
|
||||
v-if="showMicrophoneSelect"
|
||||
right="0" bottom="[calc(100%+8px)]" text="cyan-400 dark:white" bg="white dark:cyan-900"
|
||||
absolute z-30 h-fit rounded-2xl py-3 pl-3 pr-4 text-right text-nowrap text-sm font-sans
|
||||
>
|
||||
<label v-for="(input, index) in audioInputs" :key="index" class="[&_div_span]:dark:hover:bg-cyan-300 [&_div_span]:dark:hover:bg-cyan-900">
|
||||
<input type="radio" name="audioInput" :value="input.deviceId" hidden @change="handleAudioInputChange">
|
||||
<div flex="~ row" cursor-pointer items-center grid="cols-2">
|
||||
<div min-w="6">
|
||||
<div v-if="input.deviceId === selectedAudioDeviceId" i-solar:check-circle-line-duotone />
|
||||
</div>
|
||||
<span
|
||||
inline-block
|
||||
:class="[input.deviceId === selectedAudioDeviceId ? 'cyan-400 dark:text-white' : 'cyan-400/50 dark:text-white/50']"
|
||||
transition="all duration-250 ease-in-out"
|
||||
>
|
||||
{{ input.label }}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
</TransitionVertical>
|
||||
<label
|
||||
bg="complementary-100 hover:complementary-200 dark:complementary-800 dark:hover:complementary-700"
|
||||
transition="all duration-250 ease-in-out"
|
||||
:class="{ 'transition-colors-none': themeColorsHueDynamic }"
|
||||
text="complementary-400"
|
||||
h-fit flex cursor-pointer items-center justify-center gap-2 rounded-full px-4 py-2
|
||||
>
|
||||
<input v-model="showMicrophoneSelect" type="checkbox" hidden>
|
||||
<div i-solar:microphone-2-bold-duotone />
|
||||
<div>
|
||||
<span v-if="!listening">{{ t('settings.microphone') }}</span>
|
||||
<span v-else>Listening...</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<script lang="ts" setup>
|
||||
import { Button } from '@proj-airi/stage-ui/components'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'reset'): void
|
||||
}>()
|
||||
|
||||
const { stageView, stageViewControlsEnabled } = storeToRefs(useSettings())
|
||||
|
||||
const mode = defineModel<'x' | 'y' | 'z' | 'scale'>({ required: true })
|
||||
|
||||
function handleViewControlsToggle(targetMode: 'x' | 'y' | 'z' | 'scale') {
|
||||
if (mode.value === targetMode) {
|
||||
emits('reset')
|
||||
return
|
||||
}
|
||||
|
||||
mode.value = targetMode
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div w-full flex flex-1 items-center self-end justify-end gap-2>
|
||||
<Transition name="fade">
|
||||
<div v-if="stageViewControlsEnabled" w-full flex justify-between gap-2>
|
||||
<Button variant="secondary-muted" :toggled="mode === 'x'" w-full @click="handleViewControlsToggle('x')">
|
||||
X
|
||||
</Button>
|
||||
<Button variant="secondary-muted" :toggled="mode === 'y'" w-full @click="handleViewControlsToggle('y')">
|
||||
Y
|
||||
</Button>
|
||||
<Button v-if="stageView === '3d'" variant="secondary-muted" :toggled="mode === 'z'" w-full @click="handleViewControlsToggle('z')">
|
||||
Z
|
||||
</Button>
|
||||
<Button variant="secondary-muted" :toggled="mode === 'scale'" w-full @click="handleViewControlsToggle('scale')">
|
||||
Scale
|
||||
</Button>
|
||||
</div>
|
||||
</Transition>
|
||||
<button
|
||||
w-fit flex items-center self-end justify-center justify-self-end rounded-xl p-2 backdrop-blur-md
|
||||
border="2 solid neutral-100/60 dark:neutral-800/30"
|
||||
bg="neutral-50/70 dark:neutral-800/70"
|
||||
title="View"
|
||||
text="neutral-500 dark:neutral-400"
|
||||
@click="stageViewControlsEnabled = !stageViewControlsEnabled"
|
||||
>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="!stageViewControlsEnabled" i-solar:tuning-outline size-5 />
|
||||
<div v-else i-solar:alt-arrow-right-outline size-5 />
|
||||
</Transition>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-enter-to,
|
||||
.fade-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import HeaderLink from './HeaderLink.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header mb-1 w-full gap-2>
|
||||
<div w-full flex translate-x--2 justify-center>
|
||||
<HeaderLink />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
@@ -4,11 +4,20 @@ import type { ChatProvider } from '@xsai-ext/shared-providers'
|
||||
import { useMicVAD } from '@proj-airi/stage-ui/composables'
|
||||
import { useChatStore, useConsciousnessStore, useProvidersStore, useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { BasicTextarea } from '@proj-airi/ui'
|
||||
import { useDark } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import MobileChatHistory from '../Widgets/MobileChatHistory.vue'
|
||||
import ActionViewControls from './InteractiveArea/Actions/ViewControls.vue'
|
||||
import ViewControlInputs from './ViewControls/Inputs.vue'
|
||||
|
||||
const isDark = useDark({ disableTransition: false })
|
||||
|
||||
const viewControlsActiveMode = ref<'x' | 'y' | 'z' | 'scale'>('scale')
|
||||
const viewControlsInputsRef = useTemplateRef<InstanceType<typeof ViewControlInputs>>('viewControlsInputs')
|
||||
|
||||
const messageInput = ref('')
|
||||
const listening = ref(false)
|
||||
@@ -19,7 +28,7 @@ const { activeProvider, activeModel } = storeToRefs(useConsciousnessStore())
|
||||
|
||||
// const { audioInputs } = useDevicesList({ constraints: { audio: true }, requestPermissions: true })
|
||||
// const { selectedAudioDevice, isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
|
||||
const { isAudioInputOn, selectedAudioDeviceId, themeColorsHueDynamic } = storeToRefs(useSettings())
|
||||
const { isAudioInputOn, selectedAudioDeviceId, themeColorsHueDynamic, stageView, stageViewControlsEnabled } = storeToRefs(useSettings())
|
||||
const { send, onAfterSend, discoverToolsCompatibility } = useChatStore()
|
||||
const { messages } = storeToRefs(useChatStore())
|
||||
const { t } = useI18n()
|
||||
@@ -100,34 +109,75 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div fixed bottom-0 w-full flex gap-1 max-w="[calc(100dvw-1rem)]">
|
||||
<MobileChatHistory absolute left-0 top-0 transform="translate-y-[-100%]" h="80dvh" w-full />
|
||||
<div flex flex-1 gap-1>
|
||||
<BasicTextarea
|
||||
v-model="messageInput"
|
||||
:placeholder="t('stage.message')"
|
||||
border="solid 2 primary-50 dark:primary-950/10"
|
||||
text="primary-500 hover:primary-600 dark:primary-100 dark:hover:primary-200 placeholder:primary-400 placeholder:hover:primary-500 placeholder:dark:primary-300 placeholder:dark:hover:primary-400"
|
||||
bg="primary-50/80 dark:primary-950/80"
|
||||
max-h="[10lh]" min-h="[calc(1lh+4px+4px)]"
|
||||
w-full resize-none overflow-y-scroll rounded="[1lh]" px-4 py-0.5 outline-none backdrop-blur-md scrollbar-none
|
||||
transition="all duration-250 ease-in-out placeholder:all placeholder:duration-250 placeholder:ease-in-out"
|
||||
:class="[themeColorsHueDynamic ? 'transition-colors-none placeholder:transition-colors-none' : '']"
|
||||
default-height="1lh"
|
||||
@submit="handleSend"
|
||||
@compositionstart="isComposing = true"
|
||||
@compositionend="isComposing = false"
|
||||
/>
|
||||
<button
|
||||
<div fixed bottom-0 w-full flex flex-col>
|
||||
<KeepAlive>
|
||||
<Transition name="fade">
|
||||
<MobileChatHistory v-if="!stageViewControlsEnabled" max-w="[calc(100%-3.5rem)]" w-full self-start pl-2 />
|
||||
</Transition>
|
||||
</KeepAlive>
|
||||
<div relative w-full self-end>
|
||||
<div fixed top="50%" translate-y="[-50%]" px-2>
|
||||
<ViewControlInputs ref="viewControlsInputs" :mode="viewControlsActiveMode" />
|
||||
</div>
|
||||
<div translate-y="[-100%]" absolute right-0 w-full px-2 pb-2 font-sans>
|
||||
<div flex="~ col" w-full gap-1>
|
||||
<button border="2 solid neutral-100/60 dark:neutral-800/30" bg="neutral-50/70 dark:neutral-800/70" w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md title="About">
|
||||
<div i-solar:info-circle-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
</button>
|
||||
<button border="2 solid neutral-100/60 dark:neutral-800/30" bg="neutral-50/70 dark:neutral-800/70" w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md title="Theme" @click="isDark = !isDark">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="isDark" i-solar:moon-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
<div v-else i-solar:sun-2-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
</Transition>
|
||||
</button>
|
||||
<button border="2 solid neutral-100/60 dark:neutral-800/30" bg="neutral-50/70 dark:neutral-800/70" w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md title="Language">
|
||||
<div i-solar:earth-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
</button>
|
||||
<RouterLink to="/settings" border="2 solid neutral-100/60 dark:neutral-800/30" bg="neutral-50/70 dark:neutral-800/70" w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md title="Settings">
|
||||
<div i-solar:settings-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
</RouterLink>
|
||||
<button border="2 solid neutral-100/60 dark:neutral-800/30" bg="neutral-50/70 dark:neutral-800/70" w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md title="Model">
|
||||
<div i-solar:face-scan-circle-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
</button>
|
||||
<button border="2 solid neutral-100/60 dark:neutral-800/30" bg="neutral-50/70 dark:neutral-800/70" w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md title="Stage View" @click="stageView = stageView === '2d' ? '3d' : '2d'">
|
||||
<div v-if="stageView === '2d'" size-5 text="neutral-500 dark:neutral-400" inline-flex items-center justify-center>
|
||||
<span>2D</span>
|
||||
</div>
|
||||
<div v-if="stageView === '3d'" size-5 text="neutral-500 dark:neutral-400" inline-flex items-center justify-center>
|
||||
<span>3D</span>
|
||||
</div>
|
||||
</button>
|
||||
<ActionViewControls v-model="viewControlsActiveMode" @reset="() => viewControlsInputsRef?.resetOnMode()" />
|
||||
</div>
|
||||
</div>
|
||||
<div bg="white dark:neutral-800" w-full flex gap-1 px-2 py-2>
|
||||
<BasicTextarea
|
||||
v-model="messageInput"
|
||||
:placeholder="t('stage.message')"
|
||||
border="solid 2 primary-50 dark:primary-950/10"
|
||||
text="neutral-500 hover:neutral-600 dark:neutral-100 dark:hover:neutral-200 placeholder:neutral-400 placeholder:hover:neutral-500 placeholder:dark:neutral-300 placeholder:dark:hover:neutral-400"
|
||||
bg="neutral-100/80 dark:neutral-950/80"
|
||||
max-h="[10lh]" min-h="[calc(1lh+4px+4px)]"
|
||||
w-full resize-none overflow-y-scroll rounded="[1lh]" px-4 py-0.5 outline-none backdrop-blur-md scrollbar-none
|
||||
transition="all duration-250 ease-in-out placeholder:all placeholder:duration-250 placeholder:ease-in-out"
|
||||
:class="[themeColorsHueDynamic ? 'transition-colors-none placeholder:transition-colors-none' : '']"
|
||||
default-height="1lh"
|
||||
@submit="handleSend"
|
||||
@compositionstart="isComposing = true"
|
||||
@compositionend="isComposing = false"
|
||||
/>
|
||||
<button
|
||||
|
||||
v-if="messageInput.trim() || isComposing"
|
||||
w="[calc(1lh+4px+4px)]" h="[calc(1lh+4px+4px)]" aspect-square flex items-center self-end justify-center rounded-full outline-none backdrop-blur-md
|
||||
text="primary-500 hover:primary-600 dark:primary-100 dark:hover:primary-200 placeholder:primary-400 placeholder:hover:primary-500 placeholder:dark:primary-300 placeholder:dark:hover:primary-400"
|
||||
bg="primary-50/80 dark:primary-950/80"
|
||||
@click="handleSend"
|
||||
>
|
||||
<div i-solar:arrow-up-outline />
|
||||
</button>
|
||||
v-if="messageInput.trim() || isComposing"
|
||||
w="[calc(1lh+4px+4px)]" h="[calc(1lh+4px+4px)]" aspect-square flex items-center self-end justify-center rounded-full outline-none backdrop-blur-md
|
||||
text="neutral-500 hover:neutral-600 dark:neutral-900 dark:hover:neutral-800"
|
||||
bg="primary-50/80 dark:neutral-100/80 hover:neutral-50"
|
||||
transition="all duration-250 ease-in-out"
|
||||
@click="handleSend"
|
||||
>
|
||||
<div i-solar:arrow-up-outline />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
<script setup lang="ts">
|
||||
import { useLive2d, useSettings, useVRM } from '@proj-airi/stage-ui/stores'
|
||||
import { RoundRange } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
mode: 'x' | 'y' | 'z' | 'scale'
|
||||
}>()
|
||||
|
||||
const { stageView, stageViewControlsEnabled } = storeToRefs(useSettings())
|
||||
const { scale: vrmScale, position: vrmPosition, modelSize: vrmModelSize } = storeToRefs(useVRM())
|
||||
const { scale: live2dScale, position: live2dPosition } = storeToRefs(useLive2d())
|
||||
|
||||
const viewControlsValueX = computed({
|
||||
get: () => {
|
||||
switch (stageView.value) {
|
||||
case '2d':
|
||||
return live2dPosition.value.x
|
||||
case '3d':
|
||||
return vrmPosition.value.x
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
},
|
||||
set: (value) => {
|
||||
switch (stageView.value) {
|
||||
case '2d':
|
||||
live2dPosition.value.x = value
|
||||
break
|
||||
case '3d':
|
||||
vrmPosition.value.x = value
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const viewControlsValueXMin = computed(() => {
|
||||
return stageView.value === '2d' ? -500 : -vrmModelSize.value.x - 10
|
||||
})
|
||||
|
||||
const viewControlsValueXMax = computed(() => {
|
||||
return stageView.value === '2d' ? 500 : vrmModelSize.value.x + 10
|
||||
})
|
||||
|
||||
const viewControlsValueY = computed({
|
||||
get: () => {
|
||||
switch (stageView.value) {
|
||||
case '2d':
|
||||
return live2dPosition.value.y
|
||||
case '3d':
|
||||
return vrmPosition.value.y
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
},
|
||||
set: (value) => {
|
||||
switch (stageView.value) {
|
||||
case '2d':
|
||||
live2dPosition.value.y = value
|
||||
break
|
||||
case '3d':
|
||||
vrmPosition.value.y = value
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const viewControlsValueYMin = computed(() => {
|
||||
return stageView.value === '2d' ? -500 : -vrmModelSize.value.y - 10
|
||||
})
|
||||
|
||||
const viewControlsValueYMax = computed(() => {
|
||||
return stageView.value === '2d' ? 500 : vrmModelSize.value.y + 10
|
||||
})
|
||||
|
||||
const viewControlsValueZ = computed({
|
||||
get: () => {
|
||||
switch (stageView.value) {
|
||||
case '2d':
|
||||
return 0
|
||||
case '3d':
|
||||
return vrmPosition.value.z
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
},
|
||||
set: (value) => {
|
||||
switch (stageView.value) {
|
||||
case '2d':
|
||||
break
|
||||
case '3d':
|
||||
vrmPosition.value.z = value
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const viewControlsValueZMin = computed(() => {
|
||||
return stageView.value === '2d' ? -500 : -vrmModelSize.value.z - 10
|
||||
})
|
||||
|
||||
const viewControlsValueZMax = computed(() => {
|
||||
return stageView.value === '2d' ? 500 : vrmModelSize.value.z + 10
|
||||
})
|
||||
|
||||
const viewControlsValueScale = computed({
|
||||
get: () => {
|
||||
if (stageView.value === '2d') {
|
||||
return live2dScale.value
|
||||
}
|
||||
|
||||
return vrmScale.value
|
||||
},
|
||||
set: (value) => {
|
||||
if (stageView.value === '2d') {
|
||||
live2dScale.value = value
|
||||
}
|
||||
else {
|
||||
vrmScale.value = value
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function resetOnMode() {
|
||||
switch (props.mode) {
|
||||
case 'x':
|
||||
viewControlsValueX.value = 0
|
||||
break
|
||||
case 'y':
|
||||
viewControlsValueY.value = 0
|
||||
break
|
||||
case 'z':
|
||||
viewControlsValueZ.value = 0
|
||||
break
|
||||
case 'scale':
|
||||
viewControlsValueScale.value = 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
resetOnMode,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="fade-side-pops-in">
|
||||
<div v-if="stageViewControlsEnabled">
|
||||
<Transition name="fade-side-pops-in" mode="out-in">
|
||||
<div v-if="props.mode === 'x'" relative class="[&_.round-range-tooltip]:hover:opacity-100">
|
||||
<RoundRange v-model="viewControlsValueX" :min="viewControlsValueXMin" :max="viewControlsValueXMax" :step="0.01" data-direction="vertical" h="50%" write-vertical-left />
|
||||
<div class="round-range-tooltip" top="50%" translate-y="[-50%]" absolute left-10 font-mono op-0 transition="all duration-200 ease-in-out">
|
||||
{{ viewControlsValueX.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="props.mode === 'y'" relative class="[&_.round-range-tooltip]:hover:opacity-100">
|
||||
<RoundRange v-model="viewControlsValueY" :min="viewControlsValueYMin" :max="viewControlsValueYMax" :step="0.01" write-vertical-left h="50%" data-direction="vertical" />
|
||||
<div class="round-range-tooltip" top="50%" translate-y="[-50%]" absolute left-10 font-mono op-0 transition="all duration-200 ease-in-out">
|
||||
{{ viewControlsValueY.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="stageView === '3d' && props.mode === 'z'" relative class="[&_.round-range-tooltip]:hover:opacity-100">
|
||||
<RoundRange v-model="viewControlsValueZ" :min="viewControlsValueZMin" :max="viewControlsValueZMax" :step="0.01" write-vertical-left h="50%" data-direction="vertical" />
|
||||
<div class="round-range-tooltip" top="50%" translate-y="[-50%]" absolute left-10 font-mono op-0 transition="all duration-200 ease-in-out">
|
||||
{{ viewControlsValueZ.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="props.mode === 'scale'" relative class="[&_.round-range-tooltip]:hover:opacity-100">
|
||||
<RoundRange v-model="viewControlsValueScale" :min="0" :max="3" :step="0.0001" write-vertical-left h="50%" data-direction="vertical" />
|
||||
<div class="round-range-tooltip" top="50%" translate-y="[-50%]" absolute left-10 font-mono op-0 transition="all duration-200 ease-in-out">
|
||||
{{ viewControlsValueScale.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-side-pops-in-enter-active,
|
||||
.fade-side-pops-in-leave-active {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-side-pops-in-enter-from,
|
||||
.fade-side-pops-in-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%) scale(0.8);
|
||||
}
|
||||
|
||||
.fade-side-pops-in-enter-to,
|
||||
.fade-side-pops-in-leave-from {
|
||||
opacity: 1;
|
||||
transform: translateX(0) scale(1);
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useMarkdown } from '@proj-airi/stage-ui/composables'
|
||||
import { useChatStore } from '@proj-airi/stage-ui/stores'
|
||||
import { useElementBounding, useScroll } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -10,8 +9,6 @@ const chatHistoryRef = ref<HTMLDivElement>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { messages, sending } = storeToRefs(useChatStore())
|
||||
const bounding = useElementBounding(chatHistoryRef, { immediate: true, windowScroll: true, windowResize: true })
|
||||
const { y: chatHistoryContainerY } = useScroll(chatHistoryRef)
|
||||
|
||||
const { process } = useMarkdown()
|
||||
const { onBeforeMessageComposed, onTokenLiteral } = useChatStore()
|
||||
@@ -19,16 +16,20 @@ const { onBeforeMessageComposed, onTokenLiteral } = useChatStore()
|
||||
onBeforeMessageComposed(async () => {
|
||||
// Scroll down to the new sent message
|
||||
nextTick().then(() => {
|
||||
bounding.update()
|
||||
chatHistoryContainerY.value = bounding.height.value
|
||||
if (!chatHistoryRef.value)
|
||||
return
|
||||
|
||||
chatHistoryRef.value.scrollTop = chatHistoryRef.value.scrollHeight
|
||||
})
|
||||
})
|
||||
|
||||
onTokenLiteral(async () => {
|
||||
// Scroll down to the new responding message
|
||||
nextTick().then(() => {
|
||||
bounding.update()
|
||||
chatHistoryContainerY.value = bounding.height.value
|
||||
if (!chatHistoryRef.value)
|
||||
return
|
||||
|
||||
chatHistoryRef.value.scrollTop = chatHistoryRef.value.scrollHeight
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useMarkdown } from '@proj-airi/stage-ui/composables'
|
||||
import { useChatStore } from '@proj-airi/stage-ui/stores'
|
||||
import { useElementBounding, useScroll } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -10,8 +9,6 @@ const chatHistoryRef = ref<HTMLDivElement>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { messages } = storeToRefs(useChatStore())
|
||||
const bounding = useElementBounding(chatHistoryRef, { immediate: true, windowScroll: true, windowResize: true })
|
||||
const { y: chatHistoryContainerY } = useScroll(chatHistoryRef)
|
||||
|
||||
const { process } = useMarkdown()
|
||||
const { onBeforeMessageComposed, onTokenLiteral } = useChatStore()
|
||||
@@ -19,25 +16,27 @@ const { onBeforeMessageComposed, onTokenLiteral } = useChatStore()
|
||||
onBeforeMessageComposed(async () => {
|
||||
// Scroll down to the new sent message
|
||||
nextTick().then(() => {
|
||||
bounding.update()
|
||||
chatHistoryContainerY.value = bounding.height.value
|
||||
if (!chatHistoryRef.value)
|
||||
return
|
||||
|
||||
chatHistoryRef.value.scrollTop = chatHistoryRef.value.scrollHeight
|
||||
})
|
||||
})
|
||||
|
||||
onTokenLiteral(async () => {
|
||||
// Scroll down to the new responding message
|
||||
nextTick().then(() => {
|
||||
bounding.update()
|
||||
chatHistoryContainerY.value = bounding.height.value
|
||||
if (!chatHistoryRef.value)
|
||||
return
|
||||
|
||||
chatHistoryRef.value.scrollTop = chatHistoryRef.value.scrollHeight
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div py="1" flex="~ col" rounded="lg" overflow-hidden>
|
||||
<div flex-1 /> <!-- spacer -->
|
||||
<div ref="chatHistoryRef" v-auto-animate h-full w-full max-h="40%" flex="~ col" overflow-scroll class="chat-history">
|
||||
<div flex-1 /> <!-- spacer -->
|
||||
<div flex="~ col" rounded="lg" overflow-hidden>
|
||||
<div ref="chatHistoryRef" v-auto-animate max-h="35dvh" z-5 flex="~ col" h-full w-full overflow-scroll class="chat-history">
|
||||
<div v-for="(message, index) in messages" :key="index" mb-2>
|
||||
<div v-if="message.role === 'error'" flex mr="12">
|
||||
<div
|
||||
|
||||
@@ -138,7 +138,7 @@ onMounted(() => updateThemeColor())
|
||||
<HeaderLink />
|
||||
</div>
|
||||
<!-- Content -->
|
||||
<div class="h-full px-3 py-0 sm:h-[calc(100%-4rem)] md:py-0 xl:px-0" flex="~ col" mx-auto max-w-screen-xl>
|
||||
<div class="h-[calc(100%-40px)] px-3 py-0 2xl:max-w-[2160px] sm:h-[calc(100%-56px)] md:py-0 xl:px-4" flex="~ col" mx-auto>
|
||||
<PageHeader
|
||||
:title="routeHeaderMetadata?.title"
|
||||
:subtitle="routeHeaderMetadata?.subtitle"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { onMounted, ref, watch } from 'vue'
|
||||
import Cross from '../components/Backgrounds/Cross.vue'
|
||||
import Header from '../components/Layouts/Header.vue'
|
||||
import InteractiveArea from '../components/Layouts/InteractiveArea.vue'
|
||||
import MobileHeader from '../components/Layouts/MobileHeader.vue'
|
||||
import MobileInteractiveArea from '../components/Layouts/MobileInteractiveArea.vue'
|
||||
import AnimatedWave from '../components/Widgets/AnimatedWave.vue'
|
||||
|
||||
@@ -41,7 +42,8 @@ onMounted(() => updateThemeColor())
|
||||
<div relative flex="~ col" z-2 h-100dvh w-100vw of-hidden>
|
||||
<!-- header -->
|
||||
<div class="px-0 py-1 md:px-3 md:py-3" w-full gap-2>
|
||||
<Header class="flex" />
|
||||
<Header class="hidden md:flex" />
|
||||
<MobileHeader class="flex md:hidden" />
|
||||
</div>
|
||||
<!-- page -->
|
||||
<div relative flex="~ 1 row gap-y-0 gap-x-2 <md:col">
|
||||
@@ -56,8 +58,8 @@ onMounted(() => updateThemeColor())
|
||||
:y-offset="positionInPercentageString.y"
|
||||
:scale="scale"
|
||||
/>
|
||||
<InteractiveArea class="flex <md:hidden" absolute h="85dvh" right-4 flex-1 max-w="500px" min-w="30%" />
|
||||
<MobileInteractiveArea class="hidden <md:block" mx2 mb2 @settings-open="handleSettingsOpen" />
|
||||
<InteractiveArea v-if="!isMobile" h="85dvh" absolute right-4 flex flex-1 flex-col max-w="500px" min-w="30%" />
|
||||
<MobileInteractiveArea v-if="isMobile" @settings-open="handleSettingsOpen" />
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedWave>
|
||||
|
||||
@@ -43,8 +43,14 @@ const {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex class="relative h-[calc(100dvh-8rem)] flex-col-reverse md:flex-row">
|
||||
<ModelSettings live-2d-settings-class="w-100% md:w-40% lg:w-30% xl:w-30% 2xl:w-30% h-fit overflow-y-scroll absolute bottom-0 right-0 top-0" :palette="palette" @extract-colors-from-model="extractColorsFromModel" />
|
||||
<div flex class="relative h-full flex-col-reverse md:flex-row">
|
||||
<ModelSettings
|
||||
live-2d-scene-class="absolute max-h-[calc(100dvh-100px-56px)] w-full h-full"
|
||||
live-2d-settings-class="w-100% md:w-40% lg:w-30% xl:w-30% 2xl:w-30% h-fit sm:max-h-80dvh overflow-y-scroll relative"
|
||||
vrm-scene-class="absolute max-h-[calc(100dvh-100px-56px)] w-full h-full"
|
||||
vrm-settings-class="w-100% md:w-40% lg:w-30% xl:w-30% 2xl:w-30% h-fit sm:max-h-80dvh overflow-y-scroll relative"
|
||||
:palette="palette" @extract-colors-from-model="extractColorsFromModel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<IconAnimation
|
||||
@@ -58,6 +64,7 @@ const {
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-else
|
||||
v-motion
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { WidgetStage } from '@proj-airi/stage-ui/components/scenes'
|
||||
import { useDark, useMouse } from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
import { useLive2d } from '@proj-airi/stage-ui/stores'
|
||||
import { breakpointsTailwind, useBreakpoints, useDark, useMouse } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
|
||||
import Cross from '../../components/Backgrounds/Cross.vue'
|
||||
import Header from '../../components/Layouts/Header.vue'
|
||||
import InteractiveArea from '../../components/Layouts/InteractiveArea.vue'
|
||||
import MobileHeader from '../../components/Layouts/MobileHeader.vue'
|
||||
import MobileInteractiveArea from '../../components/Layouts/MobileInteractiveArea.vue'
|
||||
import AnimatedWave from '../../components/Widgets/AnimatedWave.vue'
|
||||
|
||||
import { themeColorFromPropertyOf, useThemeColor } from '../../composables/theme-color'
|
||||
|
||||
const dark = useDark()
|
||||
const paused = ref(false)
|
||||
|
||||
@@ -17,19 +22,28 @@ function handleSettingsOpen(open: boolean) {
|
||||
}
|
||||
|
||||
const positionCursor = useMouse()
|
||||
const { scale, position, positionInPercentageString } = storeToRefs(useLive2d())
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
const isMobile = breakpoints.smaller('md')
|
||||
|
||||
const { updateThemeColor } = useThemeColor(themeColorFromPropertyOf('.widgets.top-widgets .colored-area', 'background-color'))
|
||||
watch(dark, () => updateThemeColor(), { immediate: true })
|
||||
onMounted(() => updateThemeColor())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Cross>
|
||||
<AnimatedWave
|
||||
class="widgets top-widgets"
|
||||
:fill-color="dark
|
||||
? 'oklch(35% calc(var(--chromatic-chroma) * 0.6) var(--chromatic-hue))'
|
||||
: 'color-mix(in srgb, oklch(95% calc(var(--chromatic-chroma-50) * 0.5) var(--chromatic-hue)) 80%, oklch(100% 0 360))'"
|
||||
>
|
||||
<div relative flex="~ col" z-2 h-100dvh w-100vw of-hidden>
|
||||
<!-- header -->
|
||||
<div>
|
||||
<Header class="flex" p2 />
|
||||
<div class="px-0 py-1 md:px-3 md:py-3" w-full gap-2>
|
||||
<Header class="hidden md:flex" />
|
||||
<MobileHeader class="flex md:hidden" />
|
||||
</div>
|
||||
<!-- page -->
|
||||
<div relative flex="~ 1 row gap-y-0 gap-x-2 <md:col">
|
||||
@@ -40,9 +54,12 @@ const positionCursor = useMouse()
|
||||
x: positionCursor.x.value,
|
||||
y: positionCursor.y.value,
|
||||
}"
|
||||
:x-offset="`${isMobile ? position.x : position.x - 10}%`"
|
||||
:y-offset="positionInPercentageString.y"
|
||||
:scale="scale"
|
||||
/>
|
||||
<InteractiveArea class="flex <md:hidden" flex-1 max-w="500px" min-w="30%" />
|
||||
<MobileInteractiveArea class="hidden <md:block" mx2 mb2 @settings-open="handleSettingsOpen" />
|
||||
<InteractiveArea v-if="!isMobile" h="85dvh" absolute right-4 flex flex-1 flex-col max-w="500px" min-w="30%" />
|
||||
<MobileInteractiveArea v-if="isMobile" @settings-open="handleSettingsOpen" />
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedWave>
|
||||
@@ -50,7 +67,7 @@ const positionCursor = useMouse()
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
name: StageScenePage
|
||||
name: IndexScenePage
|
||||
meta:
|
||||
layout: stage
|
||||
stageTransition:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@import './transitions.css';
|
||||
@import './vue-transitions.css';
|
||||
|
||||
:root {
|
||||
--bg-color-light: rgb(255 255 255);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
.fade-slide-out-r-to-l-enter-active,
|
||||
.fade-slide-out-r-to-l-leave-active {
|
||||
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-slide-out-r-to-l-enter-from,
|
||||
.fade-slide-out-r-to-l-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(10px);
|
||||
}
|
||||
|
||||
.fade-slide-out-r-to-l-enter-to,
|
||||
.fade-slide-out-r-to-l-leave-from {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.fade-slide-out-l-to-r-enter-active,
|
||||
.fade-slide-out-l-to-r-leave-active {
|
||||
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-slide-out-l-to-r-enter-from,
|
||||
.fade-slide-out-l-to-r-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
|
||||
.fade-slide-out-l-to-r-enter-to,
|
||||
.fade-slide-out-l-to-r-leave-from {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-enter-to,
|
||||
.fade-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { RoundRange } from '@proj-airi/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const minValue = ref(0)
|
||||
const midValue = ref(50)
|
||||
const maxValue = ref(100)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Round Range" group="form" :layout="{ type: 'grid', width: '100%' }">
|
||||
<template #controls>
|
||||
<ThemeColorsHueControl />
|
||||
</template>
|
||||
|
||||
<Variant id="default-min" title="Default (min)">
|
||||
<RoundRange v-model="minValue" w-full />
|
||||
</Variant>
|
||||
|
||||
<Variant id="default-mid" title="Default (mid)">
|
||||
<RoundRange v-model="midValue" w-full />
|
||||
</Variant>
|
||||
|
||||
<Variant id="default-max" title="Default (max)">
|
||||
<RoundRange v-model="maxValue" w-full />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -14,7 +14,7 @@ const themeClasses: Record<ThemeVariant, {
|
||||
}> = {
|
||||
primary: {
|
||||
container: [
|
||||
'bg-primary-400/10 dark:bg-primary-50/10 text-neutral-900/80 backdrop-blur-md',
|
||||
'bg-primary-100/60 dark:bg-primary-50/10 text-neutral-900/80 backdrop-blur-md',
|
||||
`before:bg-primary-400 dark:bg-primary-900/10 dark:text-neutral-100/80 before:content-[''] before:dark:bg-primary-900`,
|
||||
],
|
||||
label: [
|
||||
@@ -23,7 +23,7 @@ const themeClasses: Record<ThemeVariant, {
|
||||
},
|
||||
lime: {
|
||||
container: [
|
||||
'bg-lime-400/10 dark:bg-lime-50/10 text-neutral-900/80 backdrop-blur-md',
|
||||
'bg-lime-100/60 dark:bg-lime-50/10 text-neutral-900/80 backdrop-blur-md',
|
||||
`before:bg-lime-400 dark:bg-lime-900/10 dark:text-neutral-100/80 before:content-[''] before:dark:bg-lime-900`,
|
||||
],
|
||||
label: [
|
||||
@@ -32,7 +32,7 @@ const themeClasses: Record<ThemeVariant, {
|
||||
},
|
||||
violet: {
|
||||
container: [
|
||||
'bg-violet-400/10 dark:bg-violet-50/10 text-neutral-900/80 backdrop-blur-md',
|
||||
'bg-violet-100/60 dark:bg-violet-50/10 text-neutral-900/80 backdrop-blur-md',
|
||||
`before:bg-violet-400 dark:bg-violet-900/10 dark:text-neutral-100/80 before:content-[''] before:dark:bg-violet-900`,
|
||||
],
|
||||
label: [
|
||||
@@ -41,7 +41,7 @@ const themeClasses: Record<ThemeVariant, {
|
||||
},
|
||||
orange: {
|
||||
container: [
|
||||
'bg-orange-400/10 dark:bg-orange-50/10 text-neutral-900/80 backdrop-blur-md',
|
||||
'bg-orange-100/60 dark:bg-orange-50/10 text-neutral-900/80 backdrop-blur-md',
|
||||
`before:bg-orange-400 dark:bg-orange-900/10 dark:text-neutral-100/80 before:content-[''] before:dark:bg-orange-900`,
|
||||
],
|
||||
label: [
|
||||
|
||||
@@ -4,7 +4,7 @@ import { computed } from 'vue'
|
||||
import { BidirectionalTransition } from '../Misc'
|
||||
|
||||
// Define button variants for better type safety and maintainability
|
||||
type ButtonVariant = 'primary' | 'secondary' | 'danger'
|
||||
type ButtonVariant = 'primary' | 'secondary' | 'secondary-muted' | 'danger'
|
||||
|
||||
type ButtonTheme
|
||||
= | 'default'
|
||||
@@ -15,6 +15,7 @@ type ButtonTheme
|
||||
type ButtonSize = 'sm' | 'md' | 'lg'
|
||||
|
||||
interface ButtonProps {
|
||||
toggled?: boolean // Optional toggled state for toggle buttons
|
||||
icon?: string // Icon class name
|
||||
label?: string // Button text label
|
||||
disabled?: boolean // Disabled state
|
||||
@@ -26,6 +27,7 @@ interface ButtonProps {
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ButtonProps>(), {
|
||||
toggled: false,
|
||||
variant: 'primary',
|
||||
disabled: false,
|
||||
loading: false,
|
||||
@@ -38,18 +40,33 @@ const isDisabled = computed(() => props.disabled || props.loading)
|
||||
|
||||
// Extract variant styles for better organization
|
||||
const variantClasses: Record<ButtonVariant, {
|
||||
default: string
|
||||
// dimmed: string
|
||||
// lightened: string
|
||||
default: {
|
||||
default: string
|
||||
nonToggled?: string
|
||||
toggled?: string
|
||||
}
|
||||
}> = {
|
||||
primary: {
|
||||
default: 'bg-primary-500/15 hover:bg-primary-500/20 active:bg-primary-500/30 dark:bg-primary-700/30 dark:hover:bg-primary-700/40 dark:active:bg-primary-700/30 focus:ring-primary-300/60 dark:focus:ring-primary-600/30 border-2 border-solid border-primary-500/5 dark:border-primary-900/40 text-primary-950 dark:text-primary-100',
|
||||
'primary': {
|
||||
default: {
|
||||
default: 'bg-primary-500/15 hover:bg-primary-500/20 active:bg-primary-500/30 dark:bg-primary-700/30 dark:hover:bg-primary-700/40 dark:active:bg-primary-700/30 focus:ring-primary-300/60 dark:focus:ring-primary-600/30 border-2 border-solid border-primary-500/5 dark:border-primary-900/40 text-primary-950 dark:text-primary-100',
|
||||
},
|
||||
},
|
||||
secondary: {
|
||||
default: 'bg-neutral-400/15 hover:bg-neutral-400/20 active:bg-neutral-400/30 dark:bg-neutral-700/60 dark:hover:bg-neutral-700/80 dark:active:bg-neutral-700/60 focus:ring-neutral-300/30 dark:focus:ring-neutral-600/60 dark:focus:ring-neutral-600/30 border-2 border-solid border-neutral-300/30 dark:border-neutral-700/30 text-neutral-950 dark:text-neutral-100',
|
||||
'secondary': {
|
||||
default: {
|
||||
default: 'bg-neutral-100/55 hover:bg-neutral-400/20 active:bg-neutral-400/30 dark:bg-neutral-700/60 dark:hover:bg-neutral-700/80 dark:active:bg-neutral-700/60 focus:ring-neutral-300/30 dark:focus:ring-neutral-600/60 dark:focus:ring-neutral-600/30 border-2 border-solid border-neutral-300/30 dark:border-neutral-700/30 text-neutral-950 dark:text-neutral-100',
|
||||
},
|
||||
},
|
||||
danger: {
|
||||
default: 'bg-red-500/15 hover:bg-red-500/20 active:bg-red-500/30 dark:bg-red-700/30 dark:hover:bg-red-700/40 dark:active:bg-red-700/30 focus:ring-red-300/30 dark:focus:ring-red-600/60 dark:focus:ring-red-600/30 border-2 border-solid border-red-200/30 dark:border-red-900/30 text-red-950 dark:text-red-100',
|
||||
'secondary-muted': {
|
||||
default: {
|
||||
default: 'hover:bg-neutral-50/50 active:bg-neutral-50/90 hover:dark:bg-neutral-800/50 active:dark:bg-neutral-800/90 border-2 border-solid border-neutral-100/60 dark:border-neutral-800/30 focus:ring-neutral-300/30 dark:focus:ring-neutral-600/60 dark:focus:ring-neutral-600/30',
|
||||
nonToggled: 'bg-neutral-50/70 dark:bg-neutral-800/70 text-neutral-500 dark:text-neutral-400',
|
||||
toggled: 'bg-white/90 dark:bg-neutral-500/70 ring-neutral-300/30 dark:ring-neutral-600/60 ring-2 dark:ring-neutral-600/30 text-primary-500 dark:text-primary-100',
|
||||
},
|
||||
},
|
||||
'danger': {
|
||||
default: {
|
||||
default: 'bg-red-500/15 hover:bg-red-500/20 active:bg-red-500/30 dark:bg-red-700/30 dark:hover:bg-red-700/40 dark:active:bg-red-700/30 focus:ring-red-300/30 dark:focus:ring-red-600/60 dark:focus:ring-red-600/30 border-2 border-solid border-red-200/30 dark:border-red-900/30 text-red-950 dark:text-red-100',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -62,14 +79,16 @@ const sizeClasses: Record<ButtonSize, string> = {
|
||||
|
||||
// Base classes that are always applied
|
||||
const baseClasses = computed(() => [
|
||||
'rounded-lg font-medium outline-none',
|
||||
'rounded-xl font-medium outline-none',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'backdrop-blur-md',
|
||||
props.block ? 'w-full' : '',
|
||||
sizeClasses[props.size],
|
||||
variantClasses[props.variant][props.theme],
|
||||
variantClasses[props.variant][props.theme].default,
|
||||
props.toggled ? variantClasses[props.variant][props.theme].toggled || '' : variantClasses[props.variant][props.theme].nonToggled || '',
|
||||
{ 'opacity-50 cursor-not-allowed': isDisabled.value },
|
||||
'focus:ring-2 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-neutral-900',
|
||||
'focus:ring-2',
|
||||
])
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useMouse } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useLive2d } from '../../../../stores'
|
||||
import { Screen } from '../../../Misc'
|
||||
import { Live2DCanvas, Live2DModel } from '../../../Scenes'
|
||||
|
||||
const live2dCanvasRef = ref<InstanceType<typeof Live2DCanvas>>()
|
||||
const { positionInPercentageString, scale } = storeToRefs(useLive2d())
|
||||
|
||||
const positionCursor = useMouse()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Screen v-slot="{ width, height }" relative min-h-70dvh>
|
||||
<Live2DCanvas
|
||||
ref="live2dCanvasRef"
|
||||
v-slot="{ app }"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:resolution="3"
|
||||
rounded-full
|
||||
>
|
||||
<Live2DModel
|
||||
:app="app"
|
||||
:mouth-open-size="0"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:paused="false"
|
||||
:focus-at="{
|
||||
x: positionCursor.x.value,
|
||||
y: positionCursor.y.value,
|
||||
}"
|
||||
:x-offset="positionInPercentageString.x"
|
||||
:y-offset="positionInPercentageString.y"
|
||||
:scale="scale"
|
||||
/>
|
||||
</Live2DCanvas>
|
||||
</Screen>
|
||||
</template>
|
||||
@@ -31,6 +31,7 @@ const {
|
||||
loadSource,
|
||||
loadingModel,
|
||||
modelUrl,
|
||||
scale,
|
||||
modelSize,
|
||||
modelOffset,
|
||||
} = storeToRefs(vrm)
|
||||
@@ -59,6 +60,122 @@ modelFileDialog.onChange((files) => {
|
||||
{{ t('settings.vrm.switch-to-vrm.change-to-vrm') }}
|
||||
</Button>
|
||||
</Section>
|
||||
<Section
|
||||
:title="t('settings.vrm.scale-and-position.title')"
|
||||
icon="i-solar:scale-bold-duotone"
|
||||
:class="[
|
||||
'rounded-xl',
|
||||
'bg-white/80 dark:bg-black/75',
|
||||
'backdrop-blur-lg',
|
||||
]"
|
||||
>
|
||||
<Callout
|
||||
:label="t('settings.vrm.scale-and-position.model-info-title')"
|
||||
w-full
|
||||
>
|
||||
<div>
|
||||
<div class="text-sm text-neutral-600 space-y-1">
|
||||
<div class="flex justify-between">
|
||||
<span>{{ t('settings.vrm.scale-and-position.model-info-x') }}</span>
|
||||
<span>{{ modelSize.x.toFixed(4) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{ t('settings.vrm.scale-and-position.model-info-y') }}</span>
|
||||
<span>{{ modelSize.y.toFixed(4) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{ t('settings.vrm.scale-and-position.model-info-z') }}</span>
|
||||
<span>{{ modelSize.z.toFixed(4) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Callout>
|
||||
<Callout
|
||||
theme="lime"
|
||||
label="Tips!"
|
||||
w-full
|
||||
>
|
||||
<div class="text-sm text-neutral-600 space-y-1">
|
||||
{{ t('settings.vrm.scale-and-position.tips') }}
|
||||
</div>
|
||||
</Callout>
|
||||
<FieldRange
|
||||
v-model="scale"
|
||||
as="div"
|
||||
:min="0"
|
||||
:max="5"
|
||||
:step="scale / 100"
|
||||
:label="t('settings.vrm.scale-and-position.x')"
|
||||
:format-value="val => val.toFixed(4)"
|
||||
>
|
||||
<template #label>
|
||||
<div flex items-center>
|
||||
<div>
|
||||
{{ t('settings.vrm.scale-and-position.scale') }}
|
||||
</div>
|
||||
<button px-2 text-xs outline-none title="Reset value to default" @click="() => scale = 1">
|
||||
<div i-solar:forward-linear transform-scale-x--100 text="neutral-500 dark:neutral-400" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</FieldRange>
|
||||
<FieldRange
|
||||
v-model="modelOffset.x"
|
||||
as="div"
|
||||
:min="-modelSize.x - 10"
|
||||
:max="modelSize.x + 10"
|
||||
:step="modelSize.x / 100"
|
||||
:label="t('settings.vrm.scale-and-position.x')"
|
||||
:format-value="val => val.toFixed(4)"
|
||||
>
|
||||
<template #label>
|
||||
<div flex items-center>
|
||||
<div>
|
||||
{{ t('settings.vrm.scale-and-position.x') }}
|
||||
</div>
|
||||
<button px-2 text-xs outline-none title="Reset value to default" @click="() => modelOffset.x = 0">
|
||||
<div i-solar:forward-linear transform-scale-x--100 text="neutral-500 dark:neutral-400" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</FieldRange>
|
||||
<FieldRange
|
||||
v-model="modelOffset.y"
|
||||
as="div"
|
||||
:min="-modelSize.y - 10"
|
||||
:max="modelSize.y + 10"
|
||||
:step="modelSize.y / 100"
|
||||
:label="t('settings.vrm.scale-and-position.y')"
|
||||
:format-value="val => val.toFixed(4)"
|
||||
>
|
||||
<template #label>
|
||||
<div flex items-center>
|
||||
<div>{{ t('settings.vrm.scale-and-position.y') }}</div>
|
||||
<button px-2 text-xs outline-none title="Reset value to default" @click="() => modelOffset.y = 0">
|
||||
<div i-solar:forward-linear transform-scale-x--100 text="neutral-500 dark:neutral-400" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</FieldRange>
|
||||
<FieldRange
|
||||
v-model="modelOffset.z"
|
||||
as="div"
|
||||
:min="-modelSize.z - 10"
|
||||
:max="modelSize.z + 10"
|
||||
:step="modelSize.z / 100"
|
||||
:label="t('settings.vrm.scale-and-position.z')"
|
||||
:format-value="val => val.toFixed(4)"
|
||||
>
|
||||
<template #label>
|
||||
<div flex items-center>
|
||||
<div>{{ t('settings.vrm.scale-and-position.z') }}</div>
|
||||
<button px-2 text-xs outline-none title="Reset value to default" @click="() => modelOffset.z = 0">
|
||||
<div i-solar:forward-linear transform-scale-x--100 text="neutral-500 dark:neutral-400" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</FieldRange>
|
||||
</Section>
|
||||
<Section
|
||||
:title="t('settings.vrm.change-model.title')"
|
||||
icon="i-solar:magic-stick-3-bold-duotone"
|
||||
@@ -99,96 +216,4 @@ modelFileDialog.onChange((files) => {
|
||||
{{ t('settings.vrm.theme-color-from-model.button-extract.title') }}
|
||||
</Button>
|
||||
</Section>
|
||||
<Section
|
||||
:title="t('settings.vrm.scale-and-position.title')"
|
||||
icon="i-solar:scale-bold-duotone"
|
||||
:class="[
|
||||
'rounded-xl',
|
||||
'bg-white/80 dark:bg-black/75',
|
||||
'backdrop-blur-lg',
|
||||
]"
|
||||
>
|
||||
<Callout :label="t('settings.vrm.scale-and-position.model-info-title')">
|
||||
<div>
|
||||
<div class="text-sm text-neutral-600 space-y-1">
|
||||
<div class="flex justify-between">
|
||||
<span>{{ t('settings.vrm.scale-and-position.model-info-x') }}</span>
|
||||
<span>{{ modelSize.x.toFixed(4) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{ t('settings.vrm.scale-and-position.model-info-y') }}</span>
|
||||
<span>{{ modelSize.y.toFixed(4) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{ t('settings.vrm.scale-and-position.model-info-z') }}</span>
|
||||
<span>{{ modelSize.z.toFixed(4) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Callout>
|
||||
<Callout
|
||||
theme="lime"
|
||||
label="Tips!"
|
||||
>
|
||||
<div class="text-sm text-neutral-600 space-y-1">
|
||||
{{ t('settings.vrm.scale-and-position.tips') }}
|
||||
</div>
|
||||
</Callout>
|
||||
<FieldRange
|
||||
:model-value="Number(modelOffset.x.toFixed(4))"
|
||||
as="div"
|
||||
:min="-modelSize.x"
|
||||
:max="modelSize.x"
|
||||
:step="modelSize.x / 100"
|
||||
:label="t('settings.vrm.scale-and-position.x')"
|
||||
@update:model-value="val => modelOffset.x = val"
|
||||
>
|
||||
<template #label>
|
||||
<div flex items-center>
|
||||
<div>
|
||||
{{ t('settings.vrm.scale-and-position.x') }}
|
||||
</div>
|
||||
<button px-2 text-xs outline-none title="Reset value to default" @click="() => modelOffset.x = 0">
|
||||
<div i-solar:forward-linear transform-scale-x--100 text="neutral-500 dark:neutral-400" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</FieldRange>
|
||||
<FieldRange
|
||||
:model-value="Number(modelOffset.y.toFixed(4))"
|
||||
as="div"
|
||||
:min="-modelSize.y"
|
||||
:max="modelSize.y"
|
||||
:step="modelSize.y / 100"
|
||||
:label="t('settings.vrm.scale-and-position.y')"
|
||||
@update:model-value="val => modelOffset.y = val"
|
||||
>
|
||||
<template #label>
|
||||
<div flex items-center>
|
||||
<div>{{ t('settings.vrm.scale-and-position.y') }}</div>
|
||||
<button px-2 text-xs outline-none title="Reset value to default" @click="() => modelOffset.y = 0">
|
||||
<div i-solar:forward-linear transform-scale-x--100 text="neutral-500 dark:neutral-400" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</FieldRange>
|
||||
<FieldRange
|
||||
:model-value="Number(modelOffset.z.toFixed(4))"
|
||||
as="div"
|
||||
:min="-modelSize.z"
|
||||
:max="modelSize.z"
|
||||
:step="modelSize.z / 100"
|
||||
:label="t('settings.vrm.scale-and-position.z')"
|
||||
@update:model-value="val => modelOffset.z = val"
|
||||
>
|
||||
<template #label>
|
||||
<div flex items-center>
|
||||
<div>{{ t('settings.vrm.scale-and-position.z') }}</div>
|
||||
<button px-2 text-xs outline-none title="Reset value to default" @click="() => modelOffset.z = 0">
|
||||
<div i-solar:forward-linear transform-scale-x--100 text="neutral-500 dark:neutral-400" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</FieldRange>
|
||||
</Section>
|
||||
</template>
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { OrbitControls } from '@tresjs/cientos'
|
||||
import { TresCanvas } from '@tresjs/core'
|
||||
import { useElementBounding } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useVRM } from '../../../../stores'
|
||||
import { VRMModel } from '../../../Scenes'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'loadModelProgress', value: number): void
|
||||
(e: 'error', value: unknown): void
|
||||
}>()
|
||||
const VRMContainerRef = ref<HTMLDivElement>()
|
||||
const { width, height } = useElementBounding(VRMContainerRef)
|
||||
const { selectedModel } = storeToRefs(useVRM())
|
||||
|
||||
const cameraPositionX = ref(0.17)
|
||||
const cameraPositionY = ref(0.5)
|
||||
const cameraPositionZ = ref(-1)
|
||||
|
||||
const modelRef = ref<{
|
||||
setExpression: (expression: string) => void
|
||||
}>()
|
||||
|
||||
defineExpose({
|
||||
setExpression: (expression: string) => {
|
||||
modelRef.value?.setExpression(expression)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="VRMContainerRef" w="100%" h="100%">
|
||||
<TresCanvas :alpha="true" :antialias="true" :width="width" :height="height">
|
||||
<OrbitControls />
|
||||
<TresAxesHelper :size="1" />
|
||||
<TresPerspectiveCamera :position="[cameraPositionX, cameraPositionY, cameraPositionZ]" />
|
||||
<TresDirectionalLight :color="0xFFFFFF" :intensity="1.2" :position="[1, 1, 1]" />
|
||||
<TresAmbientLight :color="0xFFFFFF" :intensity="1.5" />
|
||||
<VRMModel
|
||||
ref="modelRef"
|
||||
:key="selectedModel"
|
||||
:model="selectedModel"
|
||||
idle-animation="/assets/vrm/animations/idle_loop.vrma"
|
||||
:paused="false"
|
||||
@load-model-progress="(val) => emit('loadModelProgress', val)"
|
||||
@error="(val) => emit('error', val)"
|
||||
/>
|
||||
</TresCanvas>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +1 @@
|
||||
export { default as ModelSettings } from './index.vue'
|
||||
export { default as ModelSettingsLive2D } from './Live2D.vue'
|
||||
export { default as ModelSettingsLive2DScene } from './Live2DScene.vue'
|
||||
export { default as ModelSettingsVRM } from './VRM.vue'
|
||||
export { default as ModelSettingsVRMScene } from './VRMScene.vue'
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import Live2DScene from '../../../Scenes/Live2D.vue'
|
||||
import VRMScene from '../../../Scenes/VRM.vue'
|
||||
import Live2D from './Live2D.vue'
|
||||
import Live2DScene from './Live2DScene.vue'
|
||||
import VRM from './VRM.vue'
|
||||
import VRMScene from './VRMScene.vue'
|
||||
|
||||
import { useSettings } from '../../../../stores/settings'
|
||||
|
||||
@@ -12,6 +12,8 @@ const props = defineProps<{
|
||||
palette: string[]
|
||||
live2dSceneClass?: string | string[]
|
||||
live2dSettingsClass?: string | string[]
|
||||
vrmSceneClass?: string | string[]
|
||||
vrmSettingsClass?: string | string[]
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
@@ -24,13 +26,15 @@ const { stageView } = storeToRefs(useSettings())
|
||||
<template>
|
||||
<!-- Live2D component for 2D stage view -->
|
||||
<template v-if="stageView === '2d'">
|
||||
<Live2DScene
|
||||
<div
|
||||
:class="[
|
||||
...(props.live2dSceneClass
|
||||
? (typeof props.live2dSceneClass === 'string' ? [props.live2dSceneClass] : props.live2dSceneClass)
|
||||
: []),
|
||||
]"
|
||||
/>
|
||||
>
|
||||
<Live2DScene />
|
||||
</div>
|
||||
<div
|
||||
flex="~ col gap-2" :class="[
|
||||
...(props.live2dSettingsClass
|
||||
@@ -43,9 +47,26 @@ const { stageView } = storeToRefs(useSettings())
|
||||
</template>
|
||||
<!-- VRM component for 3D stage view -->
|
||||
<template v-if="stageView === '3d'">
|
||||
<VRMScene />
|
||||
<div flex="~ col gap-2">
|
||||
<VRM :palette="palette" @extract-colors-from-model="$emit('extractColorsFromModel')" @switch-to-live-2-d="stageView = '2d'" />
|
||||
<div
|
||||
:class="[
|
||||
...(props.vrmSceneClass
|
||||
? (typeof props.vrmSceneClass === 'string' ? [props.vrmSceneClass] : props.vrmSceneClass)
|
||||
: []),
|
||||
]"
|
||||
>
|
||||
<VRMScene />
|
||||
</div>
|
||||
<div h-full w-full p-2>
|
||||
<div
|
||||
flex="~ col gap-2"
|
||||
:class="[
|
||||
...(props.vrmSettingsClass
|
||||
? (typeof props.vrmSettingsClass === 'string' ? [props.vrmSettingsClass] : props.vrmSettingsClass)
|
||||
: []),
|
||||
]"
|
||||
>
|
||||
<VRM :palette="palette" @extract-colors-from-model="$emit('extractColorsFromModel')" @switch-to-live-2-d="stageView = '2d'" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -1,32 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { TransitionVertical } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Screen from '../Misc/Screen.vue'
|
||||
import Live2DCanvas from './Live2D/Canvas.vue'
|
||||
import Live2DModel from './Live2D/Model.vue'
|
||||
|
||||
import { useLive2d } from '../../stores'
|
||||
|
||||
import '../../utils/live2d-zip-loader'
|
||||
|
||||
withDefaults(defineProps<{
|
||||
paused: boolean
|
||||
paused?: boolean
|
||||
mouthOpenSize?: number
|
||||
focusAt: { x: number, y: number }
|
||||
focusAt?: { x: number, y: number }
|
||||
xOffset?: number | string
|
||||
yOffset?: number | string
|
||||
scale?: number
|
||||
}>(), {
|
||||
paused: false,
|
||||
focusAt: () => ({ x: 0, y: 0 }),
|
||||
mouthOpenSize: 0,
|
||||
scale: 1,
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const show = ref(false)
|
||||
const { currentMotion } = storeToRefs(useLive2d())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -50,80 +41,5 @@ const { currentMotion } = storeToRefs(useLive2d())
|
||||
:scale="scale"
|
||||
/>
|
||||
</Live2DCanvas>
|
||||
<div absolute bottom="3" right="3">
|
||||
<div flex="~ row" cursor-pointer>
|
||||
<label
|
||||
:class="[show ? 'bg-neutral-300 dark:bg-neutral-200' : 'bg-neutral-100 dark:bg-neutral-700']"
|
||||
transition="all ease-in-out duration-500"
|
||||
text="lg neutral-500 dark:neutral-400"
|
||||
m-1 h-fit w-fit cursor-pointer appearance-none gap-1 rounded-lg rounded-md border-none p-2 outline-none
|
||||
>
|
||||
<input
|
||||
v-model="show"
|
||||
:checked="show"
|
||||
:aria-checked="show"
|
||||
name="showLive2DViewerInspector"
|
||||
type="checkbox"
|
||||
appearance-none outline-none hidden
|
||||
>
|
||||
<div select-none>
|
||||
<div i-solar:bug-bold-duotone />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<TransitionVertical>
|
||||
<div v-if="show" min-w="50vw" z="<md:20" class="bottom-11 right-0" absolute m-1 w-full>
|
||||
<div bg="neutral-200/20 dark:black/20" rounded-lg p-2 backdrop-blur-sm>
|
||||
<div font-mono>
|
||||
<span>{{ t('stage.viewers.debug-menu.emotions') }}</span>
|
||||
</div>
|
||||
<div flex="~ row" flex-wrap gap-2>
|
||||
<button
|
||||
rounded-lg bg="neutral-100/70 dark:neutral-800/50" px-2 py-1 backdrop-blur-sm
|
||||
@click="currentMotion = { group: 'Surprise', index: 0 }"
|
||||
>
|
||||
{{ t('stage.viewers.debug-menu.emotions-btn.surprised') }}
|
||||
</button>
|
||||
<button
|
||||
rounded-lg bg="neutral-100/70 dark:neutral-800/50" px-2 py-1 backdrop-blur-sm
|
||||
@click="currentMotion = { group: 'Sad', index: 0 }"
|
||||
>
|
||||
{{ t('stage.viewers.debug-menu.emotions-btn.sad') }}
|
||||
</button>
|
||||
<button
|
||||
rounded-lg bg="neutral-100/70 dark:neutral-800/50" px-2 py-1 backdrop-blur-sm
|
||||
@click="currentMotion = { group: 'Angry', index: 0 }"
|
||||
>
|
||||
{{ t('stage.viewers.debug-menu.emotions-btn.angry') }}
|
||||
</button>
|
||||
<button
|
||||
rounded-lg bg="neutral-100/70 dark:neutral-800/50" px-2 py-1 backdrop-blur-sm
|
||||
@click="currentMotion = { group: 'Happy', index: 0 }"
|
||||
>
|
||||
{{ t('stage.viewers.debug-menu.emotions-btn.happy') }}
|
||||
</button>
|
||||
<button
|
||||
rounded-lg bg="neutral-100/70 dark:neutral-800/50" px-2 py-1 backdrop-blur-sm
|
||||
@click="currentMotion = { group: 'Awkward', index: 0 }"
|
||||
>
|
||||
{{ t('stage.viewers.debug-menu.emotions-btn.awkward') }}
|
||||
</button>
|
||||
<button
|
||||
rounded-lg bg="neutral-100/70 dark:neutral-800/50" px-2 py-1 backdrop-blur-sm
|
||||
@click="currentMotion = { group: 'Question', index: 0 }"
|
||||
>
|
||||
{{ t('stage.viewers.debug-menu.emotions-btn.question') }}
|
||||
</button>
|
||||
<button
|
||||
rounded-lg bg="neutral-100/70 dark:neutral-800/50" px-2 py-1 backdrop-blur-sm
|
||||
@click="currentMotion = { group: 'Think', index: 0 }"
|
||||
>
|
||||
{{ t('stage.viewers.debug-menu.emotions-btn.think') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionVertical>
|
||||
</div>
|
||||
</Screen>
|
||||
</template>
|
||||
|
||||
@@ -41,7 +41,7 @@ const db = ref<DuckDBWasmDrizzleDatabase>()
|
||||
|
||||
const vrmViewerRef = ref<{ setExpression: (expression: string) => void }>()
|
||||
|
||||
const { stageView } = storeToRefs(useSettings())
|
||||
const { stageView, stageViewControlsEnabled } = storeToRefs(useSettings())
|
||||
const { mouthOpenSize } = storeToRefs(useSpeakingStore())
|
||||
const { audioContext, calculateVolume } = useAudioContext()
|
||||
const { onBeforeMessageComposed, onBeforeSend, onTokenLiteral, onTokenSpecial, onStreamEnd, onAssistantResponseEnd } = useChatStore()
|
||||
@@ -245,6 +245,7 @@ onMounted(async () => {
|
||||
idle-animation="/assets/vrm/animations/idle_loop.vrma"
|
||||
min-w="50% <lg:full" min-h="100 sm:100" h-full w-full flex-1
|
||||
:paused="paused"
|
||||
:show-axes="stageViewControlsEnabled"
|
||||
@error="console.error"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,44 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { TransitionVertical } from '@proj-airi/ui'
|
||||
import { OrbitControls } from '@tresjs/cientos'
|
||||
import { TresCanvas } from '@tresjs/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import DataGuiRange from '../DataGui/Range.vue'
|
||||
import Collapsable from '../Misc/Collapsable.vue'
|
||||
import Screen from '../Misc/Screen.vue'
|
||||
import VRMModel from './VRM/Model.vue'
|
||||
|
||||
import { useVRM } from '../../stores'
|
||||
import { Screen } from '../Misc'
|
||||
import { VRMModel } from '../Scenes'
|
||||
|
||||
const props = defineProps<{
|
||||
paused: boolean
|
||||
}>()
|
||||
const props = withDefaults(defineProps<{
|
||||
paused?: boolean
|
||||
showAxes?: boolean
|
||||
}>(), {
|
||||
paused: false,
|
||||
showAxes: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'loadModelProgress', value: number): void
|
||||
(e: 'error', value: unknown): void
|
||||
}>()
|
||||
const { selectedModel, scale } = storeToRefs(useVRM())
|
||||
|
||||
const show = ref(false)
|
||||
|
||||
const { selectedModel } = storeToRefs(useVRM())
|
||||
|
||||
const cameraPositionX = ref(-0.17)
|
||||
const cameraPositionY = ref(0)
|
||||
const cameraPositionX = ref(0.17)
|
||||
const cameraPositionY = ref(0.5)
|
||||
const cameraPositionZ = ref(-1)
|
||||
const vrmModelPositionX = ref(-0.18)
|
||||
const vrmModelPositionY = ref(-1.32)
|
||||
const vrmModelPositionZ = ref(-0.24)
|
||||
|
||||
const modelRef = ref<{
|
||||
setExpression: (expression: string) => void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineExpose({
|
||||
setExpression: (expression: string) => {
|
||||
modelRef.value?.setExpression(expression)
|
||||
@@ -47,14 +38,16 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Screen v-slot="{ height, width }" relative>
|
||||
<Screen v-slot="{ width, height }" relative>
|
||||
<TresCanvas :alpha="true" :antialias="true" :width="width" :height="height">
|
||||
<OrbitControls />
|
||||
<TresPerspectiveCamera :position="[cameraPositionX, cameraPositionY, cameraPositionZ]" />
|
||||
<OrbitControls :enable-zoom="false" />
|
||||
<TresAxesHelper v-if="props.showAxes" :size="1" />
|
||||
<TresPerspectiveCamera :position="[cameraPositionX, cameraPositionY, cameraPositionZ]" :zoom="scale" />
|
||||
<TresDirectionalLight :color="0xFFFFFF" :intensity="1.2" :position="[1, 1, 1]" />
|
||||
<TresAmbientLight :color="0xFFFFFF" :intensity="1.5" />
|
||||
<VRMModel
|
||||
ref="modelRef"
|
||||
:key="selectedModel"
|
||||
:model="selectedModel"
|
||||
idle-animation="/assets/vrm/animations/idle_loop.vrma"
|
||||
:paused="props.paused"
|
||||
@@ -62,144 +55,5 @@ defineExpose({
|
||||
@error="(val) => emit('error', val)"
|
||||
/>
|
||||
</TresCanvas>
|
||||
<div absolute bottom="3" right="3">
|
||||
<div flex="~ row" cursor-pointer>
|
||||
<label
|
||||
:class="[show ? 'bg-neutral-300 dark:bg-neutral-200' : 'bg-neutral-100 dark:bg-neutral-700']"
|
||||
transition="all ease-in-out duration-500"
|
||||
text="lg neutral-500 dark:neutral-400"
|
||||
m-1 h-fit w-fit cursor-pointer appearance-none gap-1 rounded-lg rounded-md border-none p-2 outline-none
|
||||
>
|
||||
<input
|
||||
v-model="show"
|
||||
:checked="show"
|
||||
:aria-checked="show"
|
||||
name="showLive2DViewerInspector"
|
||||
type="checkbox"
|
||||
appearance-none outline-none hidden
|
||||
>
|
||||
<div select-none>
|
||||
<div i-solar:bug-bold-duotone />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<TransitionVertical>
|
||||
<div v-if="show" min-w="50vw" z="<md:20" class="bottom-11 right-0" absolute m-1 w-full>
|
||||
<div bg="neutral-200/20 dark:black/20" flex="~ col" gap-2 rounded-lg p-2 backdrop-blur-sm>
|
||||
<div font-mono>
|
||||
<span>{{ t('stage.viewers.debug-menu.vrm.model.title') }}</span>
|
||||
</div>
|
||||
<Collapsable h-fit w-full flex="~ col" border="~ gray/25 rounded-lg" divide="y dashed gray/25" of-clip shadow-sm>
|
||||
<template #label>
|
||||
<span font-mono>{{ t('stage.viewers.debug-menu.vrm.camera') }}</span>
|
||||
</template>
|
||||
<div grid="~ cols-[20px_1fr_60px]" w-full gap-1 p-2 text-sm font-mono>
|
||||
<div text="neutral-400 dark:neutral-500">
|
||||
<span>X</span>
|
||||
</div>
|
||||
<label w-full flex items-center gap-2>
|
||||
<DataGuiRange v-model="cameraPositionX" :min="-10" :max="10" :step="0.01" />
|
||||
</label>
|
||||
<div text-right>
|
||||
<span>{{ cameraPositionX }}</span>
|
||||
</div>
|
||||
|
||||
<div text="neutral-400 dark:neutral-500">
|
||||
<span>Y</span>
|
||||
</div>
|
||||
<label w-full flex items-center gap-2>
|
||||
<DataGuiRange v-model="cameraPositionY" :min="-10" :max="10" :step="0.01" />
|
||||
</label>
|
||||
<div text-right>
|
||||
<span>{{ cameraPositionY }}</span>
|
||||
</div>
|
||||
|
||||
<div text="neutral-400 dark:neutral-500">
|
||||
<span>Z</span>
|
||||
</div>
|
||||
<label w-full flex items-center gap-2>
|
||||
<DataGuiRange v-model="cameraPositionZ" :min="-10" :max="10" :step="0.01" />
|
||||
</label>
|
||||
<div text-right>
|
||||
<span>{{ cameraPositionZ }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsable>
|
||||
<Collapsable h-fit w-full flex="~ col" border="~ gray/25 rounded-lg" divide="y dashed gray/25" of-clip shadow-sm>
|
||||
<template #label>
|
||||
<span font-mono>{{ t('stage.viewers.debug-menu.vrm.model.title') }}</span>
|
||||
</template>
|
||||
<div grid="~ cols-[20px_1fr_60px]" w-full gap-1 p-2 text-sm font-mono>
|
||||
<div text="neutral-400 dark:neutral-500">
|
||||
<span>X</span>
|
||||
</div>
|
||||
|
||||
<label w-full flex items-center gap-2>
|
||||
<DataGuiRange v-model="vrmModelPositionX" :min="-10" :max="10" :step="0.01" />
|
||||
</label>
|
||||
<div text-right>
|
||||
<span>{{ vrmModelPositionX }}</span>
|
||||
</div>
|
||||
|
||||
<div text="neutral-400 dark:neutral-500">
|
||||
<span>Y</span>
|
||||
</div>
|
||||
<label w-full flex items-center gap-2>
|
||||
<DataGuiRange v-model="vrmModelPositionY" :min="-10" :max="10" :step="0.01" />
|
||||
</label>
|
||||
<div text-right>
|
||||
<span>{{ vrmModelPositionY }}</span>
|
||||
</div>
|
||||
|
||||
<div text="neutral-400 dark:neutral-500">
|
||||
<span>Z</span>
|
||||
</div>
|
||||
<label w-full flex items-center gap-2>
|
||||
<DataGuiRange v-model="vrmModelPositionZ" :min="-10" :max="10" :step="0.01" />
|
||||
</label>
|
||||
<div text-right>
|
||||
<span>{{ vrmModelPositionZ }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsable>
|
||||
<div font-mono>
|
||||
<span>{{ t('stage.viewers.debug-menu.emotions') }}</span>
|
||||
</div>
|
||||
<div flex="~ row" w-full flex-wrap gap-2>
|
||||
<button
|
||||
rounded-lg bg="neutral-100/70 dark:neutral-800/50" px-2 py-1 backdrop-blur-sm
|
||||
@click="modelRef?.setExpression('neutral')"
|
||||
>
|
||||
{{ t('stage.viewers.debug-menu.emotions-btn.neutral') }}
|
||||
</button>
|
||||
<button
|
||||
rounded-lg bg="neutral-100/70 dark:neutral-800/50" px-2 py-1 backdrop-blur-sm
|
||||
@click="modelRef?.setExpression('surprised')"
|
||||
>
|
||||
{{ t('stage.viewers.debug-menu.emotions-btn.surprised') }}
|
||||
</button>
|
||||
<button
|
||||
rounded-lg bg="neutral-100/70 dark:neutral-800/50" px-2 py-1 backdrop-blur-sm
|
||||
@click="modelRef?.setExpression('sad')"
|
||||
>
|
||||
{{ t('stage.viewers.debug-menu.emotions-btn.sad') }}
|
||||
</button>
|
||||
<button
|
||||
rounded-lg bg="neutral-100/70 dark:neutral-800/50" px-2 py-1 backdrop-blur-sm
|
||||
@click="modelRef?.setExpression('angry')"
|
||||
>
|
||||
{{ t('stage.viewers.debug-menu.emotions-btn.angry') }}
|
||||
</button>
|
||||
<button
|
||||
rounded-lg bg="neutral-100/70 dark:neutral-800/50" px-2 py-1 backdrop-blur-sm
|
||||
@click="modelRef?.setExpression('happy')"
|
||||
>
|
||||
{{ t('stage.viewers.debug-menu.emotions-btn.happy') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionVertical>
|
||||
</div>
|
||||
</Screen>
|
||||
</template>
|
||||
|
||||
@@ -40,15 +40,16 @@ const {
|
||||
modelOffset,
|
||||
modelOrigin,
|
||||
modelSize,
|
||||
modelPosition,
|
||||
position,
|
||||
scale,
|
||||
} = storeToRefs(vrmStore)
|
||||
|
||||
watch(modelOffset, () => {
|
||||
if (vrm.value) {
|
||||
vrm.value.scene.position.set(
|
||||
modelPosition.value.x,
|
||||
modelPosition.value.y,
|
||||
modelPosition.value.z,
|
||||
position.value.x,
|
||||
position.value.y,
|
||||
position.value.z,
|
||||
)
|
||||
}
|
||||
}, { deep: true })
|
||||
@@ -69,9 +70,10 @@ onMounted(async () => {
|
||||
console.warn('No VRM model loaded')
|
||||
return
|
||||
}
|
||||
|
||||
const { _vrm, modelCenter: vrmModelCenter, modelSize: vrmModelSize } = _vrmInfo
|
||||
|
||||
// Set initial postions for model
|
||||
// Set initial positions for model
|
||||
modelOrigin.value = {
|
||||
x: vrmModelCenter.x,
|
||||
y: vrmModelCenter.y,
|
||||
@@ -83,7 +85,7 @@ onMounted(async () => {
|
||||
z: vrmModelSize.z,
|
||||
}
|
||||
|
||||
// Set initial positons for animation
|
||||
// Set initial positions for animation
|
||||
function removeRootPositionTrack(clip: AnimationClip) {
|
||||
clip.tracks = clip.tracks.filter((track) => {
|
||||
return !track.name.endsWith('.position')
|
||||
@@ -96,6 +98,7 @@ onMounted(async () => {
|
||||
console.warn('No VRM animation loaded')
|
||||
return
|
||||
}
|
||||
|
||||
removeRootPositionTrack(clip)
|
||||
|
||||
// play animation
|
||||
@@ -105,6 +108,7 @@ onMounted(async () => {
|
||||
vrmEmote.value = useVRMEmote(_vrm)
|
||||
|
||||
vrm.value = _vrm
|
||||
vrm.value.scene.scale.set(scale.value, scale.value, scale.value)
|
||||
|
||||
disposeBeforeRenderLoop = onBeforeRender(({ delta }) => {
|
||||
vrmAnimationMixer.value?.update(delta)
|
||||
@@ -138,6 +142,12 @@ const { pause, resume } = useLoop()
|
||||
watch(() => props.paused, (value) => {
|
||||
value ? pause() : resume()
|
||||
})
|
||||
|
||||
watch(scale, () => {
|
||||
if (vrm.value) {
|
||||
vrm.value.scene.scale.set(scale.value, scale.value, scale.value)
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -13,6 +13,7 @@ export const useSettings = defineStore('settings', () => {
|
||||
|
||||
const language = useLocalStorage('settings/language', 'en')
|
||||
const stageView = useLocalStorage('settings/stage/view/model-renderer', '2d')
|
||||
const stageViewControlsEnabled = ref(false)
|
||||
|
||||
const isAudioInputOn = useLocalStorage('settings/audio/input', 'false')
|
||||
const selectedAudioDeviceId = computed(() => selectedAudioDevice.value?.deviceId)
|
||||
@@ -77,6 +78,7 @@ export const useSettings = defineStore('settings', () => {
|
||||
usePageSpecificTransitions,
|
||||
language,
|
||||
stageView,
|
||||
stageViewControlsEnabled,
|
||||
themeColorsHue,
|
||||
themeColorsHueDynamic,
|
||||
isAudioInputOn,
|
||||
|
||||
@@ -7,18 +7,21 @@ export const useVRM = defineStore('vrm', () => {
|
||||
const modelUrl = ref<string>('/assets/vrm/models/AvatarSample-B/AvatarSample_B.vrm')
|
||||
const loadSource = ref<'file' | 'url'>('url')
|
||||
const loadingModel = ref(false)
|
||||
|
||||
const scale = useLocalStorage('settings/live2d/cameraScale', 1)
|
||||
const modelSize = useLocalStorage('settings/vrm/modelSize', { x: 0, y: 0, z: 0 })
|
||||
const modelOrigin = useLocalStorage('settings/vrm/modelOrigin', { x: 0, y: 0, z: 0 })
|
||||
const modelOffset = useLocalStorage('settings/vrm/modelOffset', { x: 0, y: 0, z: 0 })
|
||||
const modelPosition = computed(() => ({
|
||||
|
||||
const position = computed(() => ({
|
||||
x: modelOrigin.value.x + modelOffset.value.x,
|
||||
y: modelOrigin.value.y + modelOffset.value.y,
|
||||
z: modelOrigin.value.z + modelOffset.value.z,
|
||||
}))
|
||||
const positionInPercentageString = computed(() => ({
|
||||
x: `${modelPosition.value.x}%`,
|
||||
y: `${modelPosition.value.y}%`,
|
||||
z: `${modelPosition.value.z}%`,
|
||||
x: `${position.value.x}%`,
|
||||
y: `${position.value.y}%`,
|
||||
z: `${position.value.z}%`,
|
||||
}))
|
||||
|
||||
const modelObjectUrl = ref<string>()
|
||||
@@ -52,9 +55,10 @@ export const useVRM = defineStore('vrm', () => {
|
||||
loadSource,
|
||||
loadingModel,
|
||||
modelSize,
|
||||
scale,
|
||||
modelOrigin,
|
||||
modelOffset,
|
||||
modelPosition,
|
||||
position,
|
||||
positionInPercentageString,
|
||||
selectedModel, // Expose the new computed property
|
||||
}
|
||||
|
||||
@@ -153,6 +153,7 @@ https://toughengineer.github.io/demo/slider-styler*/
|
||||
border: var(--track-border);
|
||||
border-radius: var(--track-border-radius);
|
||||
background: var(--track-background);
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow: var(--track-box-shadow);
|
||||
position: relative;
|
||||
cursor: col-resize;
|
||||
@@ -225,6 +226,7 @@ https://toughengineer.github.io/demo/slider-styler*/
|
||||
border: var(--track-border);
|
||||
border-radius: var(--track-border-radius);
|
||||
background: var(--track-background);
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow: var(--track-box-shadow);
|
||||
cursor: col-resize;
|
||||
/* Trim left and right paddings of track */
|
||||
@@ -307,6 +309,7 @@ https://toughengineer.github.io/demo/slider-styler*/
|
||||
height: var(--track-height);
|
||||
border-radius: var(--track-border-radius);
|
||||
background: var(--track-background);
|
||||
backdrop-filter: blur(8px);
|
||||
border: var(--track-border);
|
||||
box-shadow: var(--track-box-shadow);
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
disabled?: boolean
|
||||
}>(), {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const modelValue = defineModel<number>({ required: true })
|
||||
|
||||
const scaledMin = computed(() => props.min * 10000)
|
||||
const scaledMax = computed(() => props.max * 10000)
|
||||
const scaledStep = computed(() => props.step * 10000)
|
||||
|
||||
const sliderRef = ref<HTMLInputElement>()
|
||||
const sliderValue = computed({
|
||||
get: () => modelValue.value * 10000,
|
||||
set: (value: number) => {
|
||||
modelValue.value = value / 10000
|
||||
updateTrackColor()
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => updateTrackColor())
|
||||
watch(sliderValue, () => updateTrackColor(), { immediate: true })
|
||||
watch([scaledMin, scaledMax, scaledStep], () => updateTrackColor(), { immediate: true })
|
||||
|
||||
function updateTrackColor() {
|
||||
if (!sliderRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
sliderRef.value.style.setProperty('--value', sliderValue.value.toString())
|
||||
sliderRef.value.style.setProperty('--min', !sliderRef.value.min ? props.min.toString() : sliderRef.value.min)
|
||||
sliderRef.value.style.setProperty('--max', !sliderRef.value.max ? props.max.toString() : sliderRef.value.max)
|
||||
}
|
||||
|
||||
function handleInput(e: Event) {
|
||||
const target = e.target as HTMLInputElement
|
||||
target.style.setProperty('--value', target.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
ref="sliderRef"
|
||||
v-model.number="sliderValue"
|
||||
type="range"
|
||||
:min="scaledMin"
|
||||
:max="scaledMax"
|
||||
:step="scaledStep"
|
||||
class="slider-progress form_input-round-range"
|
||||
@input="handleInput"
|
||||
>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/*generated with Input range slider CSS style generator (version 20211225)
|
||||
https://toughengineer.github.io/demo/slider-styler*/
|
||||
.form_input-round-range {
|
||||
--height: 2em;
|
||||
|
||||
min-height: var(--height);
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
--thumb-width: var(--height);
|
||||
--thumb-height: 0px;
|
||||
--thumb-box-shadow: none;
|
||||
--thumb-border: none;
|
||||
--thumb-border-radius: 0px;
|
||||
--thumb-background: rgb(255, 255, 255);
|
||||
|
||||
--track-height: calc(var(--height) - var(--track-value-padding) * 2);
|
||||
--track-box-shadow: 0 0 12px -2px rgb(0 0 0 / 22%);
|
||||
--track-border: none;
|
||||
--track-border-radius: 10px;
|
||||
--track-background: rgba(0, 0, 0, 0.4);
|
||||
|
||||
--track-value-background: rgb(255, 255, 255);
|
||||
--track-value-padding: 0px;
|
||||
}
|
||||
|
||||
[data-direction="vertical"].form_input-round-range {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.dark .form_input-round-range {
|
||||
--thumb-background: rgb(238, 238, 238);
|
||||
|
||||
--track-border: none;
|
||||
--track-background: rgba(99, 99, 99, 0.7);
|
||||
--track-box-shadow: 0 0 12px -2px rgb(0 0 0 / 22%);
|
||||
|
||||
--track-value-background: rgb(238, 238, 238);
|
||||
}
|
||||
|
||||
/*progress support*/
|
||||
.form_input-round-range.slider-progress {
|
||||
--range: calc(var(--max) - var(--min));
|
||||
--ratio: calc((var(--value) - var(--min)) / var(--range));
|
||||
--sx: calc(0.5 * 0em + var(--ratio) * (100% - 0em));
|
||||
}
|
||||
|
||||
.form_input-round-range:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/*webkit*/
|
||||
.form_input-round-range::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: var(--thumb-width);
|
||||
height: var(--thumb-height);
|
||||
border-radius: var(--thumb-border-radius);
|
||||
background: var(--thumb-background);
|
||||
border: var(--thumb-border);
|
||||
box-shadow: var(--thumb-box-shadow);
|
||||
margin-top: 0px;
|
||||
margin-left: calc(0 - var(--track-value-padding));
|
||||
cursor: col-resize;
|
||||
transition:
|
||||
background 0.2s ease-in-out,
|
||||
box-shadow 0.2s ease-in-out,
|
||||
border-color 0.2s ease-in-out,
|
||||
transform 0.2s cubic-bezier(0.165, 0.84, 0.44, 1),
|
||||
width 0.2s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
}
|
||||
|
||||
[data-direction="vertical"]::-webkit-slider-thumb {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.form_input-round-range::-webkit-slider-runnable-track {
|
||||
height: var(--track-height);
|
||||
border: var(--track-border);
|
||||
border-radius: var(--track-border-radius);
|
||||
background: var(--track-background);
|
||||
backdrop-filter: blur(4px);
|
||||
box-shadow: var(--track-box-shadow);
|
||||
position: relative;
|
||||
cursor: col-resize;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
box-shadow 0.2s ease-in-out,
|
||||
border-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
[data-direction="vertical"]::-webkit-slider-runnable-track {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.form_input-round-range.slider-progress::-webkit-slider-runnable-track {
|
||||
background:
|
||||
linear-gradient(var(--track-value-background), var(--track-value-background)) 0 / var(--sx) 100% no-repeat,
|
||||
var(--track-background);
|
||||
}
|
||||
|
||||
[data-direction="vertical"].form_input-round-range.slider-progress::-webkit-slider-runnable-track {
|
||||
background: linear-gradient(var(--track-value-background) var(--sx), var(--track-background) var(--sx)) no-repeat;
|
||||
}
|
||||
|
||||
/*mozilla*/
|
||||
.form_input-round-range::-moz-range-thumb {
|
||||
width: var(--thumb-width);
|
||||
height: var(--thumb-height);
|
||||
border-radius: var(--thumb-border-radius);
|
||||
background: var(--thumb-background);
|
||||
border: none;
|
||||
box-shadow: var(--thumb-box-shadow);
|
||||
cursor: col-resize;
|
||||
margin-left: calc(0 - var(--track-value-padding));
|
||||
transition:
|
||||
background 0.2s ease-in-out,
|
||||
box-shadow 0.2s ease-in-out,
|
||||
border-color 0.2s ease-in-out,
|
||||
transform 0.2s cubic-bezier(0.165, 0.84, 0.44, 1),
|
||||
width 0.2s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
}
|
||||
|
||||
[data-direction="vertical"]::-moz-range-thumb {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.form_input-round-range::-moz-range-track {
|
||||
height: var(--track-height);
|
||||
border: var(--track-border);
|
||||
border-radius: var(--track-border-radius);
|
||||
background: var(--track-background);
|
||||
backdrop-filter: blur(4px);
|
||||
box-shadow: var(--track-box-shadow);
|
||||
cursor: col-resize;
|
||||
overflow: hidden;
|
||||
/* Trim left and right paddings of track */
|
||||
width: calc(100% - var(--track-value-padding) * 2);
|
||||
}
|
||||
|
||||
[data-direction="vertical"]::-moz-range-track {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.form_input-round-range.slider-progress::-moz-range-track {
|
||||
background:
|
||||
linear-gradient(var(--track-value-background), var(--track-value-background)) 0 / var(--sx) 100% no-repeat,
|
||||
var(--track-background);
|
||||
}
|
||||
|
||||
[data-direction="vertical"].form_input-round-range.slider-progress::-moz-range-track {
|
||||
background: linear-gradient(var(--track-value-background) var(--sx), var(--track-background) var(--sx)) no-repeat;
|
||||
}
|
||||
|
||||
/*ms*/
|
||||
.form_input-round-range::-ms-fill-upper {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.form_input-round-range::-ms-fill-lower {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.form_input-round-range::-ms-thumb {
|
||||
width: var(--thumb-width);
|
||||
height: var(--thumb-height);
|
||||
border-radius: var(--thumb-border-radius);
|
||||
background: var(--thumb-background);
|
||||
border: var(--thumb-border);
|
||||
box-shadow: var(--thumb-box-shadow);
|
||||
box-sizing: border-box;
|
||||
cursor: col-resize;
|
||||
|
||||
transition:
|
||||
background 0.2s ease-in-out,
|
||||
box-shadow 0.2s ease-in-out,
|
||||
border-color 0.2s ease-in-out,
|
||||
transform 0.2s cubic-bezier(0.165, 0.84, 0.44, 1),
|
||||
width 0.2s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
}
|
||||
|
||||
[data-direction="vertical"]::-ms-thumb {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.form_input-round-range::-ms-track {
|
||||
height: var(--track-height);
|
||||
border-radius: var(--track-border-radius);
|
||||
background: var(--track-background);
|
||||
backdrop-filter: blur(4px);
|
||||
border: var(--track-border);
|
||||
box-shadow: var(--track-box-shadow);
|
||||
box-sizing: border-box;
|
||||
cursor: col-resize;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-direction="vertical"]::-ms-track {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.form_input-round-range.slider-progress::-ms-fill-lower {
|
||||
height: var(--track-height);
|
||||
border-radius: var(--track-border-radius) 0 0 var(--track-border-radius);
|
||||
margin: 0;
|
||||
background: var(--track-value-background);
|
||||
border: none;
|
||||
border-right-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as ColorHueRange } from './ColorHueRange.vue'
|
||||
export { default as Range } from './Range.vue'
|
||||
export { default as RoundRange } from './RoundRange.vue'
|
||||
|
||||
Reference in New Issue
Block a user