Skip to content

Commit

Permalink
sv
Browse files Browse the repository at this point in the history
  • Loading branch information
nighca committed Jan 27, 2025
1 parent 7294c9a commit dee72cd
Show file tree
Hide file tree
Showing 42 changed files with 569 additions and 395 deletions.
24 changes: 0 additions & 24 deletions spx-gui/src/components/asset/animation/CostumeItem.vue

This file was deleted.

4 changes: 2 additions & 2 deletions spx-gui/src/components/asset/animation/GroupCostumesModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
v-for="costume in props.sprite.costumes"
:key="costume.id"
:costume="costume"
:selected="selectedCostumeSet.has(costume)"
:checked="selectedCostumeSet.has(costume)"
@click="handleCostumeClick(costume)"
/>
</ul>
Expand Down Expand Up @@ -54,7 +54,7 @@ import type { Costume } from '@/models/costume'
import type { Sprite } from '@/models/sprite'
import { defaultFps } from '@/models/animation'
import AnimationPlayer from '@/components/editor/sprite/animation/AnimationPlayer.vue'
import CostumeItem from './CostumeItem.vue'
import CostumeItem from '@/components/editor/sprite/CheckableCostumeItem.vue'
const props = defineProps<{
visible: boolean
Expand Down
21 changes: 19 additions & 2 deletions spx-gui/src/components/asset/library/SpriteItem.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
<template>
<UISpriteItem :selected="selected" :img-src="imgSrc" :img-loading="!imgSrc || imgLoading" :name="asset.displayName" />
<UISpriteItem ref="wrapperRef" selectable :selected="selected" :name="asset.displayName">
<template #img="{ style }">
<CostumesAutoPlayer
v-if="animation != null && hovered"
:style="style"
:costumes="animation.costumes"
:duration="animation.duration"
:placeholder-img="imgSrc"
/>
<UIImg v-else :style="style" :src="imgSrc" :loading="imgLoading" />
</template>
</UISpriteItem>
</template>

<script setup lang="ts">
import { UISpriteItem } from '@/components/ui'
import { computed, ref } from 'vue'
import { UIImg, UISpriteItem } from '@/components/ui'
import { useFileUrl } from '@/utils/file'
import type { AssetData } from '@/apis/asset'
import { asset2Sprite } from '@/models/common/asset'
import { useAsyncComputed } from '@/utils/utils'
import { useHovered } from '@/utils/dom'
import CostumesAutoPlayer from '@/components/common/CostumesAutoPlayer.vue'
const props = defineProps<{
asset: AssetData
Expand All @@ -16,4 +30,7 @@ const props = defineProps<{
const sprite = useAsyncComputed(() => asset2Sprite(props.asset))
const [imgSrc, imgLoading] = useFileUrl(() => sprite.value?.defaultCostume?.img)
const wrapperRef = ref<InstanceType<typeof UISpriteItem>>()
const hovered = useHovered(() => wrapperRef.value?.$el ?? null)
const animation = computed(() => sprite.value?.getDefaultAnimation() ?? null)
</script>
24 changes: 0 additions & 24 deletions spx-gui/src/components/asset/preprocessing/CostumeItem.vue

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
v-for="costume in costumes"
:key="costume.id"
:costume="costume"
:selected="isCostumeSelected(costume)"
:checked="isCostumeSelected(costume)"
@click="handleCostumeClick(costume)"
/>
</ul>
Expand Down Expand Up @@ -84,7 +84,7 @@ import type { MethodComponent } from './common/types'
import ProcessItem from './common/ProcessItem.vue'
import ImgPreview from './common/ImgPreview.vue'
import ProcessDetail from './common/ProcessDetail.vue'
import CostumeItem from './CostumeItem.vue'
import CostumeItem from '@/components/editor/sprite/CheckableCostumeItem.vue'
import originalThumbnail from './original-thumbnail.svg'
import SplitSpriteSheet from './split-sprite-sheet/SplitSpriteSheet.vue'
import splitSpriteSheetThumbnail from './split-sprite-sheet/thumbnail.svg'
Expand Down
43 changes: 39 additions & 4 deletions spx-gui/src/components/asset/scratch/SpriteItem.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
<template>
<UISpriteItem :selected="selected" :img-src="imgSrc" :img-loading="!imgSrc" :name="asset.name" />
<UISpriteItem ref="wrapperRef" selectable :selected="selected" :name="asset.name">
<template #img="{ style }">
<CostumesAutoPlayer
v-if="animation != null && hovered"
:style="style"
:costumes="animation.costumes"
:duration="animation.duration"
:placeholder-img="imgSrc"
/>
<UIImg v-else :style="style" :src="imgSrc" :loading="imgSrc == null" />
</template>
</UISpriteItem>
</template>

<script setup lang="ts">
import { UISpriteItem } from '@/components/ui'
import type { ExportedScratchSprite } from '@/utils/scratch'
import { ref, watchEffect } from 'vue'
import { computed, ref, watchEffect } from 'vue'
import { UIImg, UISpriteItem } from '@/components/ui'
import { useHovered } from '@/utils/dom'
import type { ExportedScratchFile, ExportedScratchSprite } from '@/utils/scratch'
import { fromBlob } from '@/models/common/file'
import { Costume } from '@/models/costume'
import { defaultFps } from '@/models/animation'
import CostumesAutoPlayer from '@/components/common/CostumesAutoPlayer.vue'
const props = defineProps<{
asset: ExportedScratchSprite
Expand All @@ -23,4 +39,23 @@ watchEffect((onCleanup) => {
onCleanup(() => URL.revokeObjectURL(url))
})
const wrapperRef = ref<InstanceType<typeof UISpriteItem>>()
const hovered = useHovered(() => wrapperRef.value?.$el ?? null)
function adaptCostume(c: ExportedScratchFile) {
const file = fromBlob(c.name, c.blob)
return new Costume(c.name, file, {
bitmapResolution: c.bitmapResolution
})
}
const animation = computed(() => {
const costumes = props.asset.costumes
if (costumes.length <= 1) return null
return {
costumes: costumes.map(adaptCostume),
duration: costumes.length / defaultFps
}
})
</script>
31 changes: 31 additions & 0 deletions spx-gui/src/components/common/CostumesAutoPlayer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
import { useMessageHandle } from '@/utils/exception'
import { getCleanupSignal } from '@/utils/disposable'
import type { Costume } from '@/models/costume'
import CostumesPlayer from './CostumesPlayer.vue'
const props = defineProps<{
costumes: Costume[]
/** Duration (in seconds) for all costumes to be played once */
duration: number
placeholderImg?: string | null
}>()
const playerRef = ref<InstanceType<typeof CostumesPlayer>>()
const loadAndPlay = useMessageHandle(async (onCleanup) => {
const player = playerRef.value
if (player == null) return
const signal = getCleanupSignal(onCleanup)
const { costumes, duration } = props
await player.load(costumes, duration, signal)
player.play(signal)
}).fn
watchEffect(loadAndPlay)
</script>

<template>
<CostumesPlayer ref="playerRef" :placeholder-img="placeholderImg" />
</template>
131 changes: 131 additions & 0 deletions spx-gui/src/components/common/CostumesPlayer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<script setup lang="ts">
import { ref } from 'vue'
import { File } from '@/models/common/file'
import type { Costume } from '@/models/costume'
import { UIImg } from '../ui'
const props = defineProps<{
placeholderImg?: string | null
}>()
type Frame = {
img: HTMLImageElement
width: number
height: number
x: number
y: number
}
const canvasRef = ref<HTMLCanvasElement>()
async function loadImg(file: File, signal: AbortSignal) {
const url = await file.url((f) => signal.addEventListener('abort', f))
const img = new Image()
img.src = url
await img.decode().catch((e) => {
// Sometimes `decode` fails, while the image is still able to be displayed
console.warn('Failed to decode image', url, e)
})
return img
}
async function loadFrame(costume: Costume, signal: AbortSignal): Promise<Frame> {
const [img, size] = await Promise.all([loadImg(costume.img, signal), costume.getSize()])
const x = costume.x / costume.bitmapResolution
const y = costume.y / costume.bitmapResolution
return { img, x, y, ...size }
}
async function loadFrames(costumes: Costume[], signal: AbortSignal) {
return Promise.all(costumes.map((costume) => loadFrame(costume, signal)))
}
const drawingOptionsRef = ref({
scale: 1,
offsetX: 0,
offsetY: 0
})
function adjustDrawingOptions(canvas: HTMLCanvasElement, firstFrame: Frame) {
const scale = Math.min(canvas.width / firstFrame.width, canvas.height / firstFrame.height)
drawingOptionsRef.value = {
scale,
offsetX: (canvas.width - firstFrame.width * scale) / 2,
offsetY: (canvas.height - firstFrame.height * scale) / 2
}
}
function drawFrame(canvas: HTMLCanvasElement, frame: Frame) {
const ctx = canvas.getContext('2d')!
const { scale, offsetX, offsetY } = drawingOptionsRef.value
const x = offsetX - frame.x * scale
const y = offsetY - frame.y * scale
const width = frame.width * scale
const height = frame.height * scale
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(frame.img, x, y, width, height)
}
function playFrames(frames: Frame[], duration: number, signal: AbortSignal) {
const canvas = canvasRef.value
if (canvas == null) return
const dpr = window.devicePixelRatio
canvas.width = Math.floor(canvas.clientWidth * dpr)
canvas.height = Math.floor(canvas.clientHeight * dpr)
if (frames.length === 0) return
adjustDrawingOptions(canvas, frames[0])
const interval = (duration * 1000) / frames.length
let currIdx = 0
drawFrame(canvas, frames[currIdx])
const timer = setInterval(() => {
currIdx = (currIdx + 1) % frames.length
drawFrame(canvas, frames[currIdx])
}, interval)
signal.addEventListener('abort', () => clearInterval(timer))
}
type Loaded = {
frames: Frame[]
duration: number
}
const loadedRef = ref<Loaded | null>(null)
async function load(costumes: Costume[], duration: number, signal: AbortSignal) {
const frames = await loadFrames(costumes, signal)
signal.throwIfAborted()
loadedRef.value = { frames, duration }
}
async function play(signal: AbortSignal) {
if (loadedRef.value == null) throw new Error('not loaded yet')
const { frames, duration } = loadedRef.value!
playFrames(frames, duration, signal)
}
defineExpose({ load, play })
</script>

<template>
<div class="frames-player">
<canvas ref="canvasRef" class="canvas"></canvas>
<UIImg
v-show="props.placeholderImg != null && loadedRef == null"
class="placeholder"
:src="props.placeholderImg ?? null"
loading
/>
</div>
</template>

<style lang="scss" scoped>
.frames-player {
position: relative;
}
.canvas,
.placeholder {
position: absolute;
width: 100%;
height: 100%;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ function handleWheel(e: WheelEvent) {
v-for="item in selector.items"
:key="item.name"
:resource="item"
selectable
:selected="item.name === selected"
@click="handleSelect(item.name)"
/>
Expand Down

This file was deleted.

This file was deleted.

Loading

0 comments on commit dee72cd

Please sign in to comment.