Skip to content

Commit

Permalink
feat: 3D VRM supported as scene
Browse files Browse the repository at this point in the history
  • Loading branch information
nekomeowww committed Dec 4, 2024
1 parent 3b95add commit 5738c21
Show file tree
Hide file tree
Showing 11 changed files with 765 additions and 16 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ node_modules
coverage/
**/public/assets/js/*
**/public/assets/live2d/models/*
**/public/assets/vrm/models/*
1 change: 1 addition & 0 deletions cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ words:
- cubismviewmatrix
- demi
- elevenlabs
- gltf
- hiyori
- iconify
- intlify
Expand Down
35 changes: 35 additions & 0 deletions packages/stage/src/components/Collapsable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script lang="ts" setup>
import { watchEffect } from 'vue'
import TransitionVertical from './TransitionVertical.vue'
const props = defineProps<{
default?: boolean
label?: string
}>()
const isVisible = defineModel<boolean>({ default: false })
watchEffect(() => {
if (props.default != null) {
isVisible.value = !!props.default
}
})
</script>

<template>
<div flex="~ col" border="~ gray/25 rounded-lg" divide="y dashed gray/25" of-clip shadow-sm>
<button
sticky top-0 z-10 flex items-center justify-between px2 py1 text-sm backdrop-blur-xl
@click="isVisible = !isVisible"
>
<span>
<slot name="label">
{{ props.label ?? 'Collapsable' }}
</slot>
</span> <span op50>{{ isVisible ? '▲' : '▼' }}</span>
</button>
<TransitionVertical>
<div v-if="isVisible" w-full>
<slot />
</div>
</TransitionVertical>
</div>
</template>
204 changes: 204 additions & 0 deletions packages/stage/src/components/DataGui/DualEndRange.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
const props = withDefaults(defineProps<{
values: number[]
min?: number
max?: number
step?: number
disabled?: boolean
}>(), {
min: 0,
max: 100,
step: 1,
disabled: false,
})
const emit = defineEmits<{
(e: 'update:values', value: number[]): void
(e: 'mousedown', event: MouseEvent): void
}>()
function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max)
}
const sliderRef = ref<HTMLElement | null>(null)
const isDragging = ref(false)
const previousIndex = ref<number>(0)
// Utility functions
const asc = (a: number, b: number) => a - b
function findClosest(values: number[], currentValue: number) {
const { index: closestIndex } = values.reduce((acc: { distance: number, index: number } | null, value: number, index: number) => {
const distance = Math.abs(currentValue - value)
if (acc === null || distance < acc.distance || distance === acc.distance) {
return { distance, index }
}
return acc
}, null) || { index: 0 }
return closestIndex
}
function valueToPercent(value: number, min: number, max: number) {
return ((value - min) * 100) / (max - min)
}
function percentToValue(percent: number, min: number, max: number) {
return (max - min) * percent + min
}
function getDecimalPrecision(num: number) {
if (Math.abs(num) < 1) {
const parts = num.toExponential().split('e-')
const matissaDecimalPart = parts[0].split('.')[1]
return (matissaDecimalPart ? matissaDecimalPart.length : 0) + Number.parseInt(parts[1], 10)
}
const decimalPart = num.toString().split('.')[1]
return decimalPart ? decimalPart.length : 0
}
function roundValueToStep(value: number, step: number) {
const nearest = Math.round(value / step) * step
return Number(nearest.toFixed(getDecimalPrecision(step)))
}
// Computed values
const sortedValues = computed(() => {
return [...props.values]
.sort(asc)
.map(value => clamp(value, props.min, props.max))
})
const sliderStyle = computed(() => {
const sliderOffset = valueToPercent(sortedValues.value[0], props.min, props.max)
const sliderLeap = valueToPercent(sortedValues.value[sortedValues.value.length - 1], props.min, props.max) - sliderOffset
return {
left: `${sliderOffset}%`,
width: `${sliderLeap}%`,
backgroundSize: `${sliderLeap}% 100%`,
}
})
// Event handlers
function getNewValue(event: MouseEvent, move = false) {
if (!sliderRef.value)
return { newValue: sortedValues.value, activeIndex: 0 }
const { width, left } = sliderRef.value.getBoundingClientRect()
const percent = (event.clientX - left) / width
let currentValue = percentToValue(percent, props.min, props.max)
currentValue = roundValueToStep(currentValue, props.step)
currentValue = clamp(currentValue, props.min, props.max)
const activeIndex = move ? previousIndex.value : findClosest(sortedValues.value, currentValue)
const newValues = [...sortedValues.value]
newValues[activeIndex] = currentValue
const sortedNewValues = [...newValues].sort(asc)
const newActiveIndex = sortedNewValues.indexOf(currentValue)
previousIndex.value = newActiveIndex
return {
newValue: sortedNewValues,
activeIndex: newActiveIndex,
}
}
function handleMouseDown(event: MouseEvent) {
if (props.disabled)
return
event.preventDefault()
isDragging.value = true
emit('mousedown', event)
const { newValue } = getNewValue(event)
emit('update:values', newValue)
}
function handleMouseMove(event: MouseEvent) {
if (!isDragging.value || props.disabled)
return
const { newValue } = getNewValue(event, true)
emit('update:values', newValue)
}
function handleMouseUp(_: MouseEvent) {
if (!isDragging.value)
return
isDragging.value = false
}
function handleMouseLeave(event: MouseEvent) {
if (!isDragging.value)
return
handleMouseUp(event)
}
</script>

<template>
<span
ref="sliderRef"
class="range-slider"
:class="{ disabled }"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@mouseleave="handleMouseLeave"
>
<span
class="slider-track"
:style="sliderStyle"
/>
<span
v-for="(value, index) in sortedValues"
:key="index"
role="slider"
class="slider-thumb"
:style="{ left: `${valueToPercent(value, min, max)}%` }"
:data-index="index"
/>
</span>
</template>

<style scoped>
.range-slider {
width: 100%;
box-sizing: border-box;
display: inline-block;
cursor: ew-resize;
touch-action: none;
border: 3px solid #4bb9fd;
position: relative;
}
.range-slider.disabled {
cursor: default;
pointer-events: none;
opacity: 0.5;
}
.slider-track {
display: block;
position: relative;
background-color: #4bb9fd;
background-image: linear-gradient(90deg, var(--primary-light), var(--primary-light));
background-repeat: no-repeat;
height: 14px;
}
.slider-thumb {
position: absolute;
width: 10px;
height: 10px;
background: white;
top: 50%;
transform: translate(-50%, -50%);
}
</style>
Loading

0 comments on commit 5738c21

Please sign in to comment.