Skip to content

Commit

Permalink
Preload frames & audio / removal modal (#633)
Browse files Browse the repository at this point in the history
* feat: remove costume & preload frames

* fix: use ref for muted
  • Loading branch information
ComfyFluffy authored Jul 4, 2024
1 parent d534306 commit 13d851c
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 49 deletions.
16 changes: 10 additions & 6 deletions spx-gui/src/components/editor/sprite/AnimationItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@

<script setup lang="ts">
import { computed } from 'vue'
import { UIImg, UICornerIcon } from '@/components/ui'
import { UIImg, UICornerIcon, useModal } from '@/components/ui'
import { useFileUrl } from '@/utils/file'
import type { Sprite } from '@/models/sprite'
import EditorItem from '../common/EditorItem.vue'
import EditorItemName from '../common/EditorItemName.vue'
import { useEditorCtx } from '../EditorContextProvider.vue'
import type { Animation } from '@/models/animation'
import { useMessageHandle } from '@/utils/exception'
import AnimationRemoveModal from './AnimationRemoveModal.vue'
const props = defineProps<{
animation: Animation
Expand All @@ -33,12 +34,15 @@ const [imgSrc, imgLoading] = useFileUrl(() => props.animation.costumes[0].img)
const removable = computed(() => props.sprite.costumes.length > 1)
const removeAnimation = useModal(AnimationRemoveModal)
const handelRemove = useMessageHandle(
() => {
const name = props.animation.name
const action = { name: { en: `Remove animation ${name}`, zh: `删除动画 ${name}` } }
return editorCtx.project.history.doAction(action, () => props.sprite.removeAnimation(name))
},
() =>
removeAnimation({
animation: props.animation,
sprite: props.sprite,
project: editorCtx.project
}),
{
en: 'Failed to remove animation',
zh: '删除动画失败'
Expand Down
34 changes: 28 additions & 6 deletions spx-gui/src/components/editor/sprite/AnimationRemoveModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
style="width: 512px"
:title="$t({ en: 'Remove animation', zh: '移除动画' })"
:visible="visible"
@update:visible="emit('cancel')"
@update:visible="emit('cancelled')"
>
<div>
<div>
Expand All @@ -24,7 +24,7 @@
</UICheckbox>
</div>
<div class="action">
<UIButton type="boring" @click="emit('cancel')">
<UIButton type="boring" @click="emit('cancelled')">
{{ $t({ en: 'Cancel', zh: '取消' }) }}
</UIButton>
<UIButton type="primary" @click="handleConfirm">
Expand All @@ -36,21 +36,43 @@
<script setup lang="ts">
import { UIButton, UICheckbox, UIFormModal } from '@/components/ui'
import type { Animation } from '@/models/animation'
import type { Project } from '@/models/project'
import type { Sprite } from '@/models/sprite'
import { defineProps, defineEmits, ref } from 'vue'
defineProps<{
const props = defineProps<{
visible: boolean
animation: Animation
sprite: Sprite
project: Project
}>()
const emit = defineEmits<{
cancel: []
cancelled: []
resolved: []
}>()
const preserveCostumes = ref(false)
const handleConfirm = () => {
// TODO
const handleConfirm = async () => {
await props.project.history.doAction(
{
name: {
en: `Remove animation ${props.animation.name}`,
zh: `移除动画 ${props.animation.name}`
}
},
() => {
props.sprite.removeAnimation(props.animation.name)
if (preserveCostumes.value) {
for (const costume of props.animation.costumes) {
const clonedCostume = costume.clone()
props.sprite.addCostume(clonedCostume)
}
}
}
)
emit('resolved')
}
</script>
<style scoped lang="scss">
Expand Down
81 changes: 49 additions & 32 deletions spx-gui/src/components/editor/sprite/animation/AnimationPlayer.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
<template>
<div :style="divStyle" class="animation-player-inner">
<MuteSwitch
class="mute-switch"
:state="soundElement ? (soundElement.muted ? 'muted' : 'normal') : 'disabled'"
@click="soundElement && (soundElement.muted = !soundElement.muted)"
/>
<MuteSwitch v-if="audioElement" class="mute-switch" :muted="muted" @click="muted = !muted" />
</div>
</template>
<script setup lang="ts">
import type { Animation } from '@/models/animation'
import type { Disposer } from '@/models/common/disposable'
import { computed, ref, watchEffect } from 'vue'
import { computed, ref, watch, watchEffect } from 'vue'
import { useEditorCtx } from '../../EditorContextProvider.vue'
import { useFileUrl } from '@/utils/file'
import MuteSwitch from './MuteSwitch.vue'
Expand All @@ -19,41 +15,62 @@ const props = defineProps<{
animation: Animation
}>()
const editorCtx = useEditorCtx()
const [soundSrc, soudLoading] = useFileUrl(
() => editorCtx.project.sounds.find((sound) => sound.name === props.animation.sound)?.file
)
const frameSrcList = ref<string[]>([])
const audioElement = ref<HTMLAudioElement | null>(null)
const muted = ref(true)
watch(muted, (muted) => {
if (!audioElement.value) {
return
}
audioElement.value.muted = muted
})
watchEffect(async (onCleanup) => {
if (!soundSrc.value && soudLoading.value) {
audioElement.value?.pause()
audioElement.value = null
}
if (!props.animation.costumes.length) {
frameSrcList.value = []
}
if ((!soundSrc.value && soudLoading.value) || !props.animation.costumes.length) return
const disposers: Disposer[] = []
onCleanup(() => {
disposers.forEach((f) => f())
})
const urls = await Promise.all(
props.animation.costumes.map((costume) =>
costume.img.url((f) => {
disposers.push(f)
})
)
props.animation.costumes.map((costume) => costume.img.url((f) => disposers.push(f)))
)
frameSrcList.value = urls
})
const editorCtx = useEditorCtx()
await Promise.all([
// Preload all frames and audio
...urls.map((url) => {
const img = new Image()
img.src = url
return img.decode()
})
])
const currentSound = computed(() => {
const sound = editorCtx.project.sounds.find((sound) => sound.name === props.animation.sound)
return sound
})
const [soundSrc] = useFileUrl(() => currentSound.value?.file)
const soundElement = ref<HTMLAudioElement | null>()
watchEffect(() => {
soundElement.value?.pause()
if (!soundSrc.value) {
soundElement.value = null
return
if (soundSrc.value && !soudLoading.value) {
const nextAudioElement = new Audio()
nextAudioElement.muted = muted.value
nextAudioElement.src = soundSrc.value
nextAudioElement.load()
await new Promise((resolve, reject) => {
nextAudioElement.oncanplaythrough = resolve
nextAudioElement.onerror = reject
})
audioElement.value?.pause()
audioElement.value = nextAudioElement
}
soundElement.value = new Audio(soundSrc.value)
soundElement.value.load()
soundElement.value.muted = true
frameSrcList.value = urls
})
const currentFrameIndex = ref(0)
Expand Down Expand Up @@ -87,9 +104,9 @@ watchEffect((onCleanup) => {
watchEffect((onCleanup) => {
const resetSound = () => {
if (!soundElement.value) return
soundElement.value.currentTime = 0
soundElement.value.play()
if (!audioElement.value) return
audioElement.value.currentTime = 0
audioElement.value.play()
}
const soundIntervalId = setInterval(resetSound, props.animation.duration * 1000)
try {
Expand All @@ -101,7 +118,7 @@ watchEffect((onCleanup) => {
onCleanup(() => {
clearInterval(soundIntervalId)
soundElement.value?.pause()
audioElement.value?.pause()
})
})
</script>
Expand Down
9 changes: 4 additions & 5 deletions spx-gui/src/components/editor/sprite/animation/MuteSwitch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
<div>
<UITooltip>
<template #trigger>
<div :class="['mute-switch-button', state]">
<UIIcon :type="state === 'normal' ? 'volumeUp' : 'volumeOff'" />
<div class="mute-switch-button">
<UIIcon :type="muted ? 'volumeOff' : 'volumeUp'" />
</div>
</template>
<template #default>
Expand All @@ -18,7 +18,7 @@
import { UIIcon, UITooltip } from '@/components/ui'
defineProps<{
state: 'normal' | 'muted' | 'disabled'
muted: boolean
}>()
</script>
<style scoped lang="scss">
Expand All @@ -34,9 +34,8 @@ defineProps<{
justify-content: center;
background-color: #fff;
&.disabled {
&:hover {
background-color: var(--ui-color-grey-300);
cursor: not-allowed;
}
}
</style>

0 comments on commit 13d851c

Please sign in to comment.