From ee4adac4d1bd364e4cd75e4f7cf671c41add15bf Mon Sep 17 00:00:00 2001 From: nighca Date: Mon, 25 May 2026 14:43:04 +0800 Subject: [PATCH 01/14] feat(animation): migrate sound binding back to onPlay --- spx-gui/src/models/spx/animation.test.ts | 30 ++++++++++++++++++------ spx-gui/src/models/spx/animation.ts | 4 +--- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/spx-gui/src/models/spx/animation.test.ts b/spx-gui/src/models/spx/animation.test.ts index d1f80c052..eae97017d 100644 --- a/spx-gui/src/models/spx/animation.test.ts +++ b/spx-gui/src/models/spx/animation.test.ts @@ -1,7 +1,7 @@ import { nextTick } from 'vue' import { describe, expect, it, vi } from 'vitest' import { delayFile } from '@/utils/test' -import { fromText, type Files } from '../common/file' +import { fromText, toConfig, type Files } from '../common/file' import { SpxProject } from './project' import { Sprite } from './sprite' import { Costume } from './costume' @@ -130,8 +130,7 @@ describe('Animation', () => { expect(exportedId).toBeUndefined() }) - // Temporary skip until goplus/spx#1574 is fixed and animation export switches back from onStart to onPlay. - it.skip('should export sound binding using onPlay', () => { + it('should export sound binding using onPlay', () => { const project = makeProject() const sprite = project.sprites[0] const animation = sprite.animations[0] @@ -142,8 +141,7 @@ describe('Animation', () => { expect(config.onStart).toBeUndefined() }) - // Temporary skip until goplus/spx#1574 is fixed and animation export switches back from onStart to onPlay. - it.skip('should export loop: true in onPlay when soundLoop is true', () => { + it('should export loop: true in onPlay when soundLoop is true', () => { const project = makeProject() const sprite = project.sprites[0] const animation = sprite.animations[0] @@ -154,8 +152,7 @@ describe('Animation', () => { expect(config.onPlay).toEqual({ play: project.sounds[0].name, loop: true }) }) - // Temporary skip until goplus/spx#1574 is fixed and animation export switches back from onStart to onPlay. - it.skip('should load soundLoop from onPlay.loop', () => { + it('should load soundLoop from onPlay.loop', () => { const project = makeProject() const sprite = project.sprites[0] const costumes = sprite.costumes @@ -194,6 +191,25 @@ describe('Animation', () => { expect(animation.soundLoop).toBe(false) }) + it('should preserve non-loop sound binding through project export and load', async () => { + const project = makeProject() + const animation = project.sprites[0].animations[0] + animation.setSound(project.sounds[0].id) + animation.setSoundLoop(false) + + const { metadata, files } = await project.export() + const spriteConfig = (await toConfig(files['assets/sprites/MySprite/index.json']!)) as { + fAnimations: Record + } + expect(spriteConfig.fAnimations.default.onPlay).toEqual({ play: project.sounds[0].name, loop: false }) + expect(spriteConfig.fAnimations.default.onStart).toBeUndefined() + + const newProject = new SpxProject() + await newProject.load({ metadata, files }) + expect(newProject.sprites[0].animations[0].sound).toBe(newProject.sounds[0].id) + expect(newProject.sprites[0].animations[0].soundLoop).toBe(false) + }) + it('should load sound binding from legacy onStart for backward compatibility', () => { const project = makeProject() const sprite = project.sprites[0] diff --git a/spx-gui/src/models/spx/animation.ts b/spx-gui/src/models/spx/animation.ts index a29968456..3a51da3c3 100644 --- a/spx-gui/src/models/spx/animation.ts +++ b/spx-gui/src/models/spx/animation.ts @@ -230,9 +230,7 @@ export class Animation extends Disposable { } const soundName = sounds.find((s) => s.id === this.sound)?.name if (soundName != null) { - // Revert to legacy onStart temporarily. For details, see https://github.com/goplus/builder/issues/3172 - // TODO: Use `onPlay` instead of `onStart` and pass loop after goplus/spx#1574 resolved and spx updated. - config.onStart = { play: soundName } + config.onPlay = { play: soundName, loop: this.soundLoop } } if (includeId) config.builder_id = this.id return [config, costumeConfigs, files] From 198d6c767c02b1f69faa7cf4ef9e17b1aceff189 Mon Sep 17 00:00:00 2001 From: nighca Date: Mon, 25 May 2026 15:54:58 +0800 Subject: [PATCH 02/14] docs(animation): clarify sound loop default --- spx-gui/src/models/spx/animation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spx-gui/src/models/spx/animation.ts b/spx-gui/src/models/spx/animation.ts index 3a51da3c3..2728383b6 100644 --- a/spx-gui/src/models/spx/animation.ts +++ b/spx-gui/src/models/spx/animation.ts @@ -11,7 +11,7 @@ import type { Sound } from './sound' type ActionConfig = { /** Sound name to play */ play?: string - /** Whether to loop the sound; for onStart defaults to false, for onPlay defaults to true */ + /** Whether to loop the sound; defaults to false */ loop?: boolean // not supported by builder: costumes?: unknown From b2ed272954ae7567607320cf078971324d8ba7ae Mon Sep 17 00:00:00 2001 From: nighca Date: Mon, 25 May 2026 16:01:20 +0800 Subject: [PATCH 03/14] Revert "docs(animation): clarify sound loop default" This reverts commit 6fdacde5e085e2d7b52036ee3fc7f75b98adb1fd. --- spx-gui/src/models/spx/animation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spx-gui/src/models/spx/animation.ts b/spx-gui/src/models/spx/animation.ts index 2728383b6..3a51da3c3 100644 --- a/spx-gui/src/models/spx/animation.ts +++ b/spx-gui/src/models/spx/animation.ts @@ -11,7 +11,7 @@ import type { Sound } from './sound' type ActionConfig = { /** Sound name to play */ play?: string - /** Whether to loop the sound; defaults to false */ + /** Whether to loop the sound; for onStart defaults to false, for onPlay defaults to true */ loop?: boolean // not supported by builder: costumes?: unknown From e63ed2504fb2a0a7b6b9f48958fc69c2800b9115 Mon Sep 17 00:00:00 2001 From: nighca Date: Mon, 25 May 2026 17:28:54 +0800 Subject: [PATCH 04/14] feat(animation): choose sound playback mode --- .../editor/sprite/animation/SoundEditor.vue | 40 +++++++++-- spx-gui/src/models/spx/animation.test.ts | 71 +++++++++---------- spx-gui/src/models/spx/animation.ts | 44 +++++++----- 3 files changed, 94 insertions(+), 61 deletions(-) diff --git a/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue b/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue index c140bbdcb..015e7a193 100644 --- a/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue +++ b/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue @@ -42,13 +42,30 @@ + + + {{ $t({ en: 'Play once', zh: '完整播放一次' }) }} + + + {{ $t({ en: 'Follow animation', zh: '跟随动画播放' }) }} + + diff --git a/spx-gui/src/models/spx/animation.test.ts b/spx-gui/src/models/spx/animation.test.ts index eae97017d..b26e8f360 100644 --- a/spx-gui/src/models/spx/animation.test.ts +++ b/spx-gui/src/models/spx/animation.test.ts @@ -6,7 +6,7 @@ import { SpxProject } from './project' import { Sprite } from './sprite' import { Costume } from './costume' import { Sound } from './sound' -import { Animation } from './animation' +import { Animation, AnimationSoundMode } from './animation' vi.mock('@/apis/aigc', async (importOriginal) => ({ ...(await importOriginal()), @@ -130,29 +130,30 @@ describe('Animation', () => { expect(exportedId).toBeUndefined() }) - it('should export sound binding using onPlay', () => { + it('should export complete sound binding using onStart', () => { const project = makeProject() const sprite = project.sprites[0] const animation = sprite.animations[0] animation.setSound(project.sounds[0].id) const [config] = animation.export('', { sounds: project.sounds }) - expect(config.onPlay).toEqual({ play: project.sounds[0].name, loop: false }) - expect(config.onStart).toBeUndefined() + expect(config.onStart).toEqual({ play: project.sounds[0].name }) + expect(config.onPlay).toBeUndefined() }) - it('should export loop: true in onPlay when soundLoop is true', () => { + it('should export follow-animation sound binding using onPlay', () => { const project = makeProject() const sprite = project.sprites[0] const animation = sprite.animations[0] animation.setSound(project.sounds[0].id) - animation.setSoundLoop(true) + animation.setSoundMode(AnimationSoundMode.FollowAnimation) const [config] = animation.export('', { sounds: project.sounds }) - expect(config.onPlay).toEqual({ play: project.sounds[0].name, loop: true }) + expect(config.onPlay).toEqual({ play: project.sounds[0].name }) + expect(config.onStart).toBeUndefined() }) - it('should load soundLoop from onPlay.loop', () => { + it('should load follow-animation sound mode from onPlay', () => { const project = makeProject() const sprite = project.sprites[0] const costumes = sprite.costumes @@ -163,16 +164,16 @@ describe('Animation', () => { { frameFrom: costumes[0].name, frameTo: costumes[0].name, - onPlay: { play: sounds[0].name, loop: true } + onPlay: { play: sounds[0].name } }, costumes, { sounds } ) expect(animation.sound).toBe(sounds[0].id) - expect(animation.soundLoop).toBe(true) + expect(animation.soundMode).toBe(AnimationSoundMode.FollowAnimation) }) - it('should default soundLoop to false when loop field is absent', () => { + it('should load complete sound mode from onStart', () => { const project = makeProject() const sprite = project.sprites[0] const costumes = sprite.costumes @@ -183,64 +184,62 @@ describe('Animation', () => { { frameFrom: costumes[0].name, frameTo: costumes[0].name, - onPlay: { play: sounds[0].name } + onStart: { play: sounds[0].name } }, costumes, { sounds } ) - expect(animation.soundLoop).toBe(false) + expect(animation.soundMode).toBe(AnimationSoundMode.Complete) }) - it('should preserve non-loop sound binding through project export and load', async () => { + it('should preserve complete sound binding through project export and load', async () => { const project = makeProject() const animation = project.sprites[0].animations[0] animation.setSound(project.sounds[0].id) - animation.setSoundLoop(false) const { metadata, files } = await project.export() const spriteConfig = (await toConfig(files['assets/sprites/MySprite/index.json']!)) as { fAnimations: Record } - expect(spriteConfig.fAnimations.default.onPlay).toEqual({ play: project.sounds[0].name, loop: false }) - expect(spriteConfig.fAnimations.default.onStart).toBeUndefined() + expect(spriteConfig.fAnimations.default.onStart).toEqual({ play: project.sounds[0].name }) + expect(spriteConfig.fAnimations.default.onPlay).toBeUndefined() const newProject = new SpxProject() await newProject.load({ metadata, files }) expect(newProject.sprites[0].animations[0].sound).toBe(newProject.sounds[0].id) - expect(newProject.sprites[0].animations[0].soundLoop).toBe(false) + expect(newProject.sprites[0].animations[0].soundMode).toBe(AnimationSoundMode.Complete) }) - it('should load sound binding from legacy onStart for backward compatibility', () => { + it('should preserve follow-animation sound binding through project export and load', async () => { const project = makeProject() - const sprite = project.sprites[0] - const costumes = sprite.costumes - const sounds = project.sounds + const animation = project.sprites[0].animations[0] + animation.setSound(project.sounds[0].id) + animation.setSoundMode(AnimationSoundMode.FollowAnimation) - const [animation] = Animation.load( - 'default', - { - frameFrom: costumes[0].name, - frameTo: costumes[0].name, - onStart: { play: sounds[0].name } - }, - costumes, - { sounds } - ) - expect(animation.sound).toBe(sounds[0].id) - expect(animation.soundLoop).toBe(false) + const { metadata, files } = await project.export() + const spriteConfig = (await toConfig(files['assets/sprites/MySprite/index.json']!)) as { + fAnimations: Record + } + expect(spriteConfig.fAnimations.default.onPlay).toEqual({ play: project.sounds[0].name }) + expect(spriteConfig.fAnimations.default.onStart).toBeUndefined() + + const newProject = new SpxProject() + await newProject.load({ metadata, files }) + expect(newProject.sprites[0].animations[0].sound).toBe(newProject.sounds[0].id) + expect(newProject.sprites[0].animations[0].soundMode).toBe(AnimationSoundMode.FollowAnimation) }) it('should clone well', () => { const project = makeProject() const sprite = project.sprites[0] const animation = sprite.animations[0] - animation.setSoundLoop(true) + animation.setSoundMode(AnimationSoundMode.FollowAnimation) const clonedAnimation = animation.clone() expect(clonedAnimation.id).not.toBe(animation.id) expect(clonedAnimation.name).toBe(animation.name) expect(clonedAnimation.duration).toBe(animation.duration) expect(clonedAnimation.sound).toBe(animation.sound) - expect(clonedAnimation.soundLoop).toBe(animation.soundLoop) + expect(clonedAnimation.soundMode).toBe(animation.soundMode) expect(clonedAnimation.costumes.length).toBe(animation.costumes.length) for (let i = 0; i < clonedAnimation.costumes.length; i++) { expect(clonedAnimation.costumes[i].id).not.toBe(animation.costumes[i].id) diff --git a/spx-gui/src/models/spx/animation.ts b/spx-gui/src/models/spx/animation.ts index 3a51da3c3..5bc09fdfa 100644 --- a/spx-gui/src/models/spx/animation.ts +++ b/spx-gui/src/models/spx/animation.ts @@ -1,11 +1,11 @@ import { reactive } from 'vue' +import { nanoid } from 'nanoid' import { Disposable } from '@/utils/disposable' import { ensureValidCostumeName, getAnimationName, validateAnimationName } from './common/asset-name' import type { Files } from '../common/file' import type { Costume, RawCostumeConfig, Pivot as CostumePivot } from './costume' import type { Sprite } from './sprite' -import { nanoid } from 'nanoid' import type { Sound } from './sound' type ActionConfig = { @@ -19,13 +19,17 @@ type ActionConfig = { export const defaultFps = 10 +export enum AnimationSoundMode { + Complete = 'complete', + FollowAnimation = 'followAnimation' +} + export type AnimationInits = { id?: string /** Duration (in seconds) for animation to be played once */ duration?: number sound?: string - /** Whether to loop the sound; defaults to false */ - soundLoop?: boolean + soundMode?: AnimationSoundMode } export type RawAnimationConfig = { @@ -108,10 +112,9 @@ export class Animation extends Disposable { this.sound = soundId } - /** Whether to loop the sound when played; defaults to false */ - soundLoop: boolean - setSoundLoop(loop: boolean) { - this.soundLoop = loop + soundMode: AnimationSoundMode + setSoundMode(mode: AnimationSoundMode) { + this.soundMode = mode } constructor(name: string, inits?: AnimationInits) { @@ -120,7 +123,7 @@ export class Animation extends Disposable { this.costumes = [] this.duration = inits?.duration ?? 0 this.sound = inits?.sound ?? null - this.soundLoop = inits?.soundLoop ?? false + this.soundMode = inits?.soundMode ?? AnimationSoundMode.Complete this.id = inits?.id ?? nanoid() return reactive(this) as this @@ -131,7 +134,7 @@ export class Animation extends Disposable { id: preserveId ? this.id : undefined, duration: this.duration, sound: this.sound ?? undefined, - soundLoop: this.soundLoop + soundMode: this.soundMode }) const costumes = this.costumes.map((c) => c.clone(preserveId)) animation.setCostumes(costumes) @@ -180,22 +183,26 @@ export class Animation extends Disposable { if (isLoop != null) console.warn(`unsupported field: isLoop for animation ${name}`) if (anitype != null) console.warn(`unsupported field: anitype for animation ${name}`) let soundId: string | undefined = undefined - let soundLoop = false - // onPlay is the current API; onStart is legacy for backward compatibility - const soundName = onPlay?.play ?? onStart?.play - if (soundName != null) { - const sound = sounds.find((s) => s.name === soundName) - if (sound == null) console.warn(`Sound ${soundName} not found when creating animation ${name}`) + let soundMode = AnimationSoundMode.Complete + const soundBinding = + onPlay?.play != null + ? { soundName: onPlay.play, mode: AnimationSoundMode.FollowAnimation } + : onStart?.play != null + ? { soundName: onStart.play, mode: AnimationSoundMode.Complete } + : null + if (soundBinding != null) { + const sound = sounds.find((s) => s.name === soundBinding.soundName) + if (sound == null) console.warn(`Sound ${soundBinding.soundName} not found when creating animation ${name}`) else { soundId = sound.id - soundLoop = onPlay?.loop ?? false + soundMode = soundBinding.mode } } const animation = new Animation(name, { id: includeId ? id : undefined, duration, sound: soundId, - soundLoop + soundMode }) const animationCostumeNames = animationCostumes.map((c) => c.name) for (const costume of animationCostumes) { @@ -230,7 +237,8 @@ export class Animation extends Disposable { } const soundName = sounds.find((s) => s.id === this.sound)?.name if (soundName != null) { - config.onPlay = { play: soundName, loop: this.soundLoop } + if (this.soundMode === AnimationSoundMode.FollowAnimation) config.onPlay = { play: soundName } + else config.onStart = { play: soundName } } if (includeId) config.builder_id = this.id return [config, costumeConfigs, files] From 08f1e4c397fddfe1cc6b1a893ed2dabcbfd0ffe7 Mon Sep 17 00:00:00 2001 From: nighca Date: Mon, 25 May 2026 17:51:55 +0800 Subject: [PATCH 05/14] refactor(animation): address sound mode review --- .../editor/sprite/animation/SoundEditor.vue | 51 +++++++++++++++---- spx-gui/src/models/spx/animation.ts | 23 ++++----- 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue b/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue index 015e7a193..71261b4ec 100644 --- a/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue +++ b/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue @@ -42,14 +42,44 @@ - - - {{ $t({ en: 'Play once', zh: '完整播放一次' }) }} - - - {{ $t({ en: 'Follow animation', zh: '跟随动画播放' }) }} - - + + + {{ + $t({ + en: 'Play the sound once and let it complete independently of the animation', + zh: '声音播放一次,并独立于动画完整播放' + }) + }} + + + + {{ + $t({ + en: 'Play the sound with the animation and stop it when the animation stops', + zh: '声音跟随动画播放,并在动画停止时停止' + }) + }} + + + @@ -63,8 +93,9 @@ import { UIMenuItem, UIBlockItem, UIIcon, - UIRadioGroup, - UIRadio + UIButtonGroup, + UIButtonGroupItem, + UITooltip } from '@/components/ui' import { useEditorCtx } from '@/components/editor/EditorContextProvider.vue' import SoundItem from '@/components/editor/stage/sound/SoundItem.vue' diff --git a/spx-gui/src/models/spx/animation.ts b/spx-gui/src/models/spx/animation.ts index 5bc09fdfa..838bae034 100644 --- a/spx-gui/src/models/spx/animation.ts +++ b/spx-gui/src/models/spx/animation.ts @@ -11,8 +11,6 @@ import type { Sound } from './sound' type ActionConfig = { /** Sound name to play */ play?: string - /** Whether to loop the sound; for onStart defaults to false, for onPlay defaults to true */ - loop?: boolean // not supported by builder: costumes?: unknown } @@ -184,18 +182,19 @@ export class Animation extends Disposable { if (anitype != null) console.warn(`unsupported field: anitype for animation ${name}`) let soundId: string | undefined = undefined let soundMode = AnimationSoundMode.Complete - const soundBinding = - onPlay?.play != null - ? { soundName: onPlay.play, mode: AnimationSoundMode.FollowAnimation } - : onStart?.play != null - ? { soundName: onStart.play, mode: AnimationSoundMode.Complete } - : null - if (soundBinding != null) { - const sound = sounds.find((s) => s.name === soundBinding.soundName) - if (sound == null) console.warn(`Sound ${soundBinding.soundName} not found when creating animation ${name}`) + let soundName: string | undefined = undefined + if (onPlay?.play != null) { + soundName = onPlay.play + soundMode = AnimationSoundMode.FollowAnimation + } else if (onStart?.play != null) { + soundName = onStart.play + soundMode = AnimationSoundMode.Complete + } + if (soundName != null) { + const sound = sounds.find((s) => s.name === soundName) + if (sound == null) console.warn(`Sound ${soundName} not found when creating animation ${name}`) else { soundId = sound.id - soundMode = soundBinding.mode } } const animation = new Animation(name, { From ff7f8942b113b277314a60d8f765041c3c3757d3 Mon Sep 17 00:00:00 2001 From: nighca Date: Mon, 25 May 2026 19:47:54 +0800 Subject: [PATCH 06/14] style(animation): clarify sound behavior control --- .../editor/sprite/animation/SoundEditor.vue | 79 ++++++++++--------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue b/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue index 71261b4ec..21eb0ec6b 100644 --- a/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue +++ b/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue @@ -42,44 +42,47 @@ - - - {{ - $t({ - en: 'Play the sound once and let it complete independently of the animation', - zh: '声音播放一次,并独立于动画完整播放' - }) - }} - - - - {{ - $t({ - en: 'Play the sound with the animation and stop it when the animation stops', - zh: '声音跟随动画播放,并在动画停止时停止' - }) - }} - - - +
+ + {{ $t({ en: 'Sound behavior', zh: '声音方式' }) }} + + + + {{ + $t({ + en: 'Play the sound once and let it complete independently of the animation', + zh: '声音播放一次,并独立于动画完整播放' + }) + }} + + + + {{ + $t({ + en: 'Play the sound with the animation and stop it when the animation stops', + zh: '声音跟随动画播放,并在动画停止时停止' + }) + }} + + + +
From de8fc2b6032faf0e1bd89a10b84790b6e2d131b6 Mon Sep 17 00:00:00 2001 From: nighca Date: Mon, 25 May 2026 19:53:45 +0800 Subject: [PATCH 07/14] style(animation): keep sound behavior control stable --- spx-gui/src/components/editor/sprite/animation/SoundEditor.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue b/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue index 21eb0ec6b..1a8320900 100644 --- a/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue +++ b/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue @@ -42,7 +42,7 @@ -
+
{{ $t({ en: 'Sound behavior', zh: '声音方式' }) }} From 80c1f4931dc453a1b397432dc14cbfce2693219f Mon Sep 17 00:00:00 2001 From: nighca Date: Tue, 26 May 2026 08:49:53 +0800 Subject: [PATCH 08/14] copy(animation): clarify looping sound behavior --- .../src/components/editor/sprite/animation/SoundEditor.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue b/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue index 1a8320900..365cc5907 100644 --- a/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue +++ b/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue @@ -71,13 +71,13 @@ {{ $t({ - en: 'Play the sound with the animation and stop it when the animation stops', - zh: '声音跟随动画播放,并在动画停止时停止' + en: 'Loop the sound during each animation playback and stop it when the animation stops', + zh: '声音在动画的单次播放周期内循环播放,并在动画停止时停止' }) }} From 3e816f3113a99f70eb50fd303e1deab0aaed0c5f Mon Sep 17 00:00:00 2001 From: nighca Date: Thu, 28 May 2026 11:25:48 +0800 Subject: [PATCH 09/14] style(animation): match sound playback selector UI --- .../editor/sprite/animation/SoundEditor.vue | 90 ++++++++++--------- spx-gui/src/components/ui/UIDropdownForm.vue | 35 ++++---- 2 files changed, 68 insertions(+), 57 deletions(-) diff --git a/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue b/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue index 365cc5907..c2ea1b0c9 100644 --- a/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue +++ b/spx-gui/src/components/editor/sprite/animation/SoundEditor.vue @@ -2,11 +2,11 @@ -
    +
    -
    - - {{ $t({ en: 'Sound behavior', zh: '声音方式' }) }} - - +