feat(stage-web): mini Polaroid for taking shots for your characters
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { Live2DCanvas, Live2DModel, Screen } from '@proj-airi/stage-ui/components'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const live2dCanvasRef = ref<InstanceType<typeof Live2DCanvas>>()
|
||||
const live2dModelRef = ref<InstanceType<typeof Live2DModel>>()
|
||||
|
||||
const motion = ref<string>('idle')
|
||||
const motionGroupsList = ref<{
|
||||
motionName: string
|
||||
motionIndex: number
|
||||
fileName: string
|
||||
}[]>([])
|
||||
|
||||
function download(href: string, name: string) {
|
||||
const link = document.createElement('a')
|
||||
link.href = href
|
||||
link.download = name
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
function handleSetMotion(motionName: string) {
|
||||
live2dModelRef.value?.setMotion(motionName)
|
||||
}
|
||||
|
||||
watch(live2dModelRef, (model) => {
|
||||
motionGroupsList.value = model?.listMotionGroups() || []
|
||||
}, { immediate: true })
|
||||
|
||||
function handleModelLoaded() {
|
||||
if (live2dModelRef.value) {
|
||||
live2dModelRef.value.setMotion(motion.value)
|
||||
motionGroupsList.value = live2dModelRef.value.listMotionGroups()
|
||||
}
|
||||
}
|
||||
|
||||
function handleShot() {
|
||||
if (!live2dCanvasRef.value || !live2dModelRef.value)
|
||||
return
|
||||
|
||||
const canvas = live2dCanvasRef.value.canvasElement()
|
||||
const dataUrl = canvas!.toDataURL('image/png')
|
||||
download(dataUrl, 'live2d-screenshot.png')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex flex-col items-center gap-4>
|
||||
<div h-full w-full>
|
||||
<Screen v-slot="{ width, height }" relative min-h-70dvh>
|
||||
<Live2DCanvas
|
||||
ref="live2dCanvasRef"
|
||||
v-slot="{ app }"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:resolution="3"
|
||||
rounded-full
|
||||
>
|
||||
<Live2DModel
|
||||
ref="live2dModelRef"
|
||||
:app="app"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:focus-at="{ x: width / 2, y: height / 2 }"
|
||||
@model-loaded="handleModelLoaded"
|
||||
/>
|
||||
</Live2DCanvas>
|
||||
</Screen>
|
||||
</div>
|
||||
<div>
|
||||
<select v-model="motion" rounded-lg px-3 py-2 @change="handleSetMotion(motion)">
|
||||
<option v-for="motionItem in motionGroupsList" :key="motionItem.motionIndex" :value="motionItem.motionName">
|
||||
{{ motionItem.fileName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div border="2px solid white" flex items-center justify-center rounded-full p-1>
|
||||
<button
|
||||
h-18 w-18 rounded-full bg="white active:gray-50" outline-none
|
||||
transition-colors duration-200 ease-in-out
|
||||
@click="handleShot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -23,11 +23,10 @@ async function initLive2DPixiStage(parent: HTMLDivElement) {
|
||||
extensions.add(TickerPlugin)
|
||||
|
||||
pixiApp.value = new Application({
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
width: props.width * props.resolution,
|
||||
height: props.height * props.resolution,
|
||||
backgroundAlpha: 0,
|
||||
preserveDrawingBuffer: true,
|
||||
resolution: props.resolution ?? 1, // Add resolution setting
|
||||
})
|
||||
|
||||
pixiAppCanvas.value = pixiApp.value.view
|
||||
@@ -73,8 +72,13 @@ async function captureFrame() {
|
||||
return frame
|
||||
}
|
||||
|
||||
function canvasElement() {
|
||||
return pixiAppCanvas.value
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
captureFrame,
|
||||
canvasElement,
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -24,12 +24,18 @@ const props = withDefaults(defineProps<{
|
||||
height: number
|
||||
paused?: boolean
|
||||
focusAt?: { x: number, y: number }
|
||||
disableFocusAt?: boolean
|
||||
}>(), {
|
||||
mouthOpenSize: 0,
|
||||
paused: false,
|
||||
focusAt: () => ({ x: 0, y: 0 }),
|
||||
disableFocusAt: false,
|
||||
})
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'modelLoaded'): void
|
||||
}>()
|
||||
|
||||
const pixiApp = toRef(() => props.app)
|
||||
const paused = toRef(() => props.paused)
|
||||
const focusAt = toRef(() => props.focusAt)
|
||||
@@ -153,6 +159,7 @@ async function loadModel() {
|
||||
if (motionManager.state.currentGroup === motionManager.groups.idle) {
|
||||
idleEyeFocus.update(internalModel, now)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -165,6 +172,7 @@ async function loadModel() {
|
||||
await localforage.setItem('live2dModel', live2dModelFile.value)
|
||||
}
|
||||
|
||||
emits('modelLoaded')
|
||||
loadingLive2dModel.value = false
|
||||
}
|
||||
|
||||
@@ -246,6 +254,8 @@ watch(paused, value => value ? pixiApp.value?.stop() : pixiApp.value?.start())
|
||||
watch(focusAt, (value) => {
|
||||
if (!model.value)
|
||||
return
|
||||
if (!props.disableFocusAt)
|
||||
return
|
||||
|
||||
model.value.focus(value.x, value.y)
|
||||
})
|
||||
@@ -262,6 +272,15 @@ onUnmounted(() => {
|
||||
cancelAnimationFrame(dropShadowAnimationId.value)
|
||||
model.value && pixiApp.value?.stage.removeChild(model.value)
|
||||
})
|
||||
|
||||
function listMotionGroups() {
|
||||
return availableLive2dMotions.value
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
setMotion,
|
||||
listMotionGroups,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
Reference in New Issue
Block a user