Skip to content

Commit

Permalink
animation details (#634)
Browse files Browse the repository at this point in the history
  • Loading branch information
nighca authored Jul 4, 2024
1 parent 13d851c commit 6003a25
Show file tree
Hide file tree
Showing 14 changed files with 308 additions and 160 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
<!--
Placeholder when no target (sprite/sound/stage) selected
TODO: extract as UI component when there is another similar case
-->

<template>
<div class="editor-placeholder">
<img :src="placeholderImg" />
<p class="text">{{ $t({ en: 'Add a sprite to start', zh: '添加一个精灵' }) }}</p>
<div class="op">
<UIEmpty size="extra-large">
{{ $t({ en: 'Add a sprite to start', zh: '添加一个精灵' }) }}
<template #op>
<UIButton
type="boring"
size="large"
Expand All @@ -30,17 +28,16 @@
</template>
{{ $t({ en: 'Choose from asset library', zh: '从素材库选择' }) }}
</UIButton>
</div>
</div>
</template>
</UIEmpty>
</template>

<script setup lang="ts">
import { UIButton } from '@/components/ui'
import { UIEmpty, UIButton } from '@/components/ui'
import { useAddAssetFromLibrary, useAddSpriteFromLocalFile } from '@/components/asset'
import { useMessageHandle } from '@/utils/exception'
import { AssetType } from '@/apis/asset'
import { useEditorCtx } from '../../EditorContextProvider.vue'
import placeholderImg from './placeholder.svg'
import localFileImg from './local-file.svg'
import assetLibraryImg from './asset-library.svg'

Expand All @@ -60,28 +57,6 @@ const handleAddFromAssetLibrary = useMessageHandle(
</script>

<style scoped lang="scss">
.editor-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

.text {
margin-top: 4px;
color: var(--ui-color-grey-700);
font-size: 16px;
line-height: 26px;
}

.op {
margin-top: 32px;
display: flex;
gap: var(--ui-gap-large);
}

.icon {
width: 24px;
height: 24px;
Expand Down
52 changes: 34 additions & 18 deletions spx-gui/src/components/editor/sprite/AnimationEditor.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
<template>
<EditorList color="sprite" :add-text="$t({ en: 'Add costume', zh: '添加造型' })">
<UIEmpty v-if="sprite.animations.length === 0" size="extra-large">
{{ $t({ en: 'No animations', zh: '没有动画' }) }}
<template #op>
<UIButton type="boring" size="large" @click="handleGroupCostumes">
<template #icon>
<img class="icon" :src="galleryIcon" />
</template>
{{ $t({ en: 'Group costumes as animation', zh: '将造型合并为动画' }) }}
</UIButton>
</template>
</UIEmpty>
<EditorList v-else color="sprite" :add-text="$t({ en: 'Add costume', zh: '添加造型' })">
<AnimationItem
v-for="animation in sprite.animations"
:key="animation.name"
Expand All @@ -22,35 +33,34 @@
</template>

<script setup lang="ts">
import { shallowRef, watchEffect } from 'vue'
import type { Sprite } from '@/models/sprite'
import EditorList from '../common/EditorList.vue'
import { UIMenu, UIMenuItem, useModal } from '@/components/ui'
import { shallowRef, watch } from 'vue'
import { UIMenu, UIMenuItem, useModal, UIEmpty, UIButton } from '@/components/ui'
import { useMessageHandle } from '@/utils/exception'
import AnimationDetail from './AnimationDetail.vue'
import GroupCostumesModal from '@/components/asset/animation/GroupCostumesModal.vue'
import { useEditorCtx } from '../EditorContextProvider.vue'
import { Animation } from '@/models/animation'
import AnimationItem from './AnimationItem.vue'
import { useMessageHandle } from '@/utils/exception'
import galleryIcon from './gallery.svg'
const props = defineProps<{
sprite: Sprite
}>()
const selectedAnimation = shallowRef<Animation | null>(props.sprite.animations[0] || null)
const editorCtx = useEditorCtx()
const groupCostumes = useModal(GroupCostumesModal)
const selectedAnimation = shallowRef<Animation | null>(props.sprite.animations[0] || null)
watch(
() => props.sprite,
(sprite) => {
if (!(selectedAnimation.value && sprite.animations.includes(selectedAnimation.value))) {
selectedAnimation.value = sprite.animations[0] || null
}
watchEffect(() => {
if (selectedAnimation.value == null) return
if (!props.sprite.animations.includes(selectedAnimation.value)) {
selectedAnimation.value = props.sprite.animations[0] ?? null
}
)
})
const groupCostumes = useModal(GroupCostumesModal)
const handleGroupCostumes = useMessageHandle(
async () => {
Expand All @@ -60,20 +70,22 @@ const handleGroupCostumes = useMessageHandle(
editorCtx.project.history.doAction(
{
name: { en: `Group costumes as animation`, zh: `将造型合并为动画` }
name: { en: 'Group costumes as animation', zh: '将造型合并为动画' }
},
() => {
const animation = Animation.create(
'',
props.sprite,
selectedCostumes.map((costume) => costume.clone())
)
props.sprite.addAnimation(animation)
if (removeCostumes) {
for (const costume of selectedCostumes) {
props.sprite.removeCostume(costume.name)
for (let i = selectedCostumes.length - 1; i >= 0; i--) {
// Do not remove the last costume
if (props.sprite.costumes.length <= 1) break
props.sprite.removeCostume(selectedCostumes[i].name)
}
}
selectedAnimation.value = animation
}
)
},
Expand All @@ -84,6 +96,10 @@ const handleGroupCostumes = useMessageHandle(
).fn
</script>
<style scoped lang="scss">
.icon {
width: 24px;
height: 24px;
}
.background {
width: 100%;
height: 100%;
Expand Down
10 changes: 1 addition & 9 deletions spx-gui/src/components/editor/sprite/AnimationItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,11 @@
<EditorItem :selected="selected" color="sprite">
<UIImg class="img" :src="imgSrc" :loading="imgLoading" />
<EditorItemName class="name">{{ animation.name }}</EditorItemName>
<UICornerIcon
v-show="selected && removable"
color="sprite"
type="trash"
@click.stop="handelRemove"
/>
<UICornerIcon v-show="selected" color="sprite" type="trash" @click.stop="handelRemove" />
</EditorItem>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { UIImg, UICornerIcon, useModal } from '@/components/ui'
import { useFileUrl } from '@/utils/file'
import type { Sprite } from '@/models/sprite'
Expand All @@ -32,8 +26,6 @@ const props = defineProps<{
const editorCtx = useEditorCtx()
const [imgSrc, imgLoading] = useFileUrl(() => props.animation.costumes[0].img)
const removable = computed(() => props.sprite.costumes.length > 1)
const removeAnimation = useModal(AnimationRemoveModal)
const handelRemove = useMessageHandle(
Expand Down
13 changes: 8 additions & 5 deletions spx-gui/src/components/editor/sprite/AnimationRemoveModal.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<UIFormModal
style="width: 512px"
:title="$t({ en: 'Remove animation', zh: '移除动画' })"
style="width: 560px"
:title="$t({ en: 'Remove animation', zh: '删除动画' })"
:visible="visible"
@update:visible="emit('cancelled')"
>
Expand All @@ -10,11 +10,11 @@
{{
$t({
en: `Animation ${animation.name} will be removed. Do you want to preserve the costumes?`,
zh: `动画 ${animation.name} 将被移除。是否保留其中的造型?`
zh: `动画 ${animation.name} 将被删除。是否保留其中的造型?`
})
}}
</div>
<UICheckbox v-model:checked="preserveCostumes">
<UICheckbox v-model:checked="preserveCostumes" class="checkbox">
{{
$t({
en: "Preserve (the costumes will be moved to the sprite's costume list)",
Expand Down Expand Up @@ -59,7 +59,7 @@ const handleConfirm = async () => {
{
name: {
en: `Remove animation ${props.animation.name}`,
zh: `移除动画 ${props.animation.name}`
zh: `删除动画 ${props.animation.name}`
}
},
() => {
Expand All @@ -76,6 +76,9 @@ const handleConfirm = async () => {
}
</script>
<style scoped lang="scss">
.checkbox {
margin-top: 20px;
}
.action {
margin-top: 40px;
display: flex;
Expand Down
8 changes: 4 additions & 4 deletions spx-gui/src/components/editor/sprite/SpriteEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<UITabs v-model:value="selectedTab" color="sprite">
<UITab value="code">{{ $t({ en: 'Code', zh: '代码' }) }}</UITab>
<UITab value="costumes">{{ $t({ en: 'Costumes', zh: '造型' }) }}</UITab>
<UITab value="animation">{{ $t({ en: 'Animation', zh: '动画' }) }}</UITab>
<UITab value="animations">{{ $t({ en: 'Animations', zh: '动画' }) }}</UITab>
</UITabs>
<template #extra>
<FormatButton
Expand All @@ -20,8 +20,8 @@
@update:value="handleCodeUpdate"
/>
<CostumesEditor v-show="selectedTab === 'costumes'" :sprite="sprite" />
<!-- We use v-if to prevent the animation from running in the background -->
<AnimationEditor v-if="selectedTab === 'animation'" :sprite="sprite" />
<!-- We use v-if to prevent AnimationEditor from running in the background -->
<AnimationEditor v-if="selectedTab === 'animations'" :sprite="sprite" />
</template>

<script setup lang="ts">
Expand All @@ -40,7 +40,7 @@ const props = defineProps<{
}>()
const editorCtx = useEditorCtx()
const selectedTab = ref<'code' | 'costumes' | 'animation'>('code')
const selectedTab = ref<'code' | 'costumes' | 'animations'>('code')
const codeEditor = ref<InstanceType<typeof CodeEditor>>()
const code = computed(() => props.sprite.code)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<UIDropdownModal
:title="$t({ en: 'Adjust duration', zh: '调整时长' })"
:title="$t(actionName)"
style="width: 280px"
@cancel="emit('close')"
@confirm="handleConfirm"
Expand All @@ -16,6 +16,7 @@
import { ref } from 'vue'
import type { Animation } from '@/models/animation'
import { UIDropdownModal, UINumberInput } from '@/components/ui'
import { useEditorCtx } from '../../EditorContextProvider.vue'
const props = defineProps<{
animation: Animation
Expand All @@ -25,10 +26,14 @@ const emit = defineEmits<{
close: []
}>()
const editorCtx = useEditorCtx()
const actionName = { en: 'Adjust animation duration', zh: '调整动画时长' }
const duration = ref(props.animation.duration)
function handleConfirm() {
props.animation.setDuration(duration.value)
async function handleConfirm() {
await editorCtx.project.history.doAction({ name: actionName }, () => {
props.animation.setDuration(duration.value)
})
emit('close')
}
</script>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<UIDropdownModal
:title="$t({ en: 'Select sound', zh: '选择声音' })"
:title="$t(actionName)"
style="width: 320px; max-height: 400px"
@cancel="emit('close')"
@confirm="handleConfirm"
Expand Down Expand Up @@ -63,8 +63,9 @@ const emit = defineEmits<{
const editorCtx = useEditorCtx()
const actionName = { en: 'Select sound', zh: '选择声音' }
const selected = ref(props.animation.sound)
function handleSoundClick(sound: string) {
async function handleSoundClick(sound: string) {
selected.value = selected.value === sound ? null : sound
}
Expand Down Expand Up @@ -97,8 +98,10 @@ function handleRecorded(sound: Sound) {
selected.value = sound.name
}
function handleConfirm() {
props.animation.setSound(selected.value)
async function handleConfirm() {
await editorCtx.project.history.doAction({ name: actionName }, () =>
props.animation.setSound(selected.value)
)
emit('close')
}
</script>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<template>
<UIDropdownModal
:title="$t({ en: 'Bind state', zh: '绑定状态' })"
:title="$t(actionName)"
style="width: 320px"
@cancel="emit('close')"
@confirm="handleConfirm"
Expand Down Expand Up @@ -44,6 +44,7 @@ import { ref } from 'vue'
import type { Animation } from '@/models/animation'
import { type Sprite, State } from '@/models/sprite'
import { UIDropdownModal, UICornerIcon, UIBlockItem } from '@/components/ui'
import { useEditorCtx } from '@/components/editor/EditorContextProvider.vue'
import iconStateDefault from './default.svg?raw'
import iconStateStep from './step.svg?raw'
import iconStateDie from './die.svg?raw'
Expand All @@ -57,6 +58,8 @@ const emit = defineEmits<{
close: []
}>()

const editorCtx = useEditorCtx()
const actionName = { en: 'Bind state', zh: '绑定状态' }
const boundStates = ref(props.sprite.getAnimationBoundStates(props.animation.name))

function isBound(state: State) {
Expand All @@ -69,8 +72,10 @@ function handleStateItemClick(state: State) {
: [...boundStates.value, state]
}

function handleConfirm() {
props.sprite.setAnimationBoundStates(props.animation.name, boundStates.value)
async function handleConfirm() {
await editorCtx.project.history.doAction({ name: actionName }, () => {
props.sprite.setAnimationBoundStates(props.animation.name, boundStates.value)
})
emit('close')
}
</script>
Expand Down
3 changes: 3 additions & 0 deletions spx-gui/src/components/editor/sprite/gallery.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 6003a25

Please sign in to comment.