Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Animation details #634

Merged
merged 1 commit into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading