feat: substituir sprite SVG por animação de frames PNG do personagem
Cria pasta assets/sprites/character/ com 5 frames de idle e reescreve CharacterSprite.vue para ciclar entre eles via setInterval. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -1,80 +1,74 @@
|
||||
<template>
|
||||
<svg
|
||||
:width="cols * scale"
|
||||
:height="rows * scale"
|
||||
:viewBox="`0 0 ${cols * scale} ${rows * scale}`"
|
||||
shape-rendering="crispEdges"
|
||||
<img
|
||||
:src="currentFrame"
|
||||
:width="displaySize"
|
||||
:height="displaySize"
|
||||
class="fc-sprite"
|
||||
:class="{ 'fc-sprite--bob': bob && !celebrate, 'fc-sprite--celebrate': celebrate }"
|
||||
:class="{ 'fc-sprite--celebrate': celebrate }"
|
||||
aria-label="Personagem pixel art"
|
||||
>
|
||||
<rect
|
||||
v-for="(px, i) in pixels"
|
||||
:key="i"
|
||||
:x="px.x * scale"
|
||||
:y="px.y * scale"
|
||||
:width="scale"
|
||||
:height="scale"
|
||||
:fill="colors[px.k]"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import spriteData from '@/assets/sprite-data.json'
|
||||
import { ref, computed, watch, onUnmounted } from 'vue'
|
||||
|
||||
import frame1 from '@/assets/sprites/character/nivel_1_idle_frame1.png'
|
||||
import frame2 from '@/assets/sprites/character/nivel_1_idle_frame2.png'
|
||||
import frame3 from '@/assets/sprites/character/nivel_1_idle_frame3.png'
|
||||
import frame4 from '@/assets/sprites/character/nivel_1_idle_frame4.png'
|
||||
import frame5 from '@/assets/sprites/character/nivel_1_idle_frame5.png'
|
||||
|
||||
const IDLE_FRAMES = [frame1, frame2, frame3, frame4, frame5]
|
||||
// Base unit in px; displaySize = UNIT * scale
|
||||
const UNIT = 16
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
/** Zoom factor. 3 = widget (home), 4 = default, 6 = hero (character view). */
|
||||
scale?: number
|
||||
/** Color overrides — kept for API compatibility, unused with PNG sprites. */
|
||||
theme?: Partial<Record<string, string>>
|
||||
/** Enable idle frame animation. */
|
||||
bob?: boolean
|
||||
/** Celebration pose — faster animation + bounce CSS. */
|
||||
celebrate?: boolean
|
||||
}>(), { scale: 4, bob: true })
|
||||
|
||||
const rows = spriteData.rows
|
||||
const cols = spriteData.cols
|
||||
const frameIndex = ref(0)
|
||||
const displaySize = computed(() => UNIT * props.scale)
|
||||
const currentFrame = computed(() => IDLE_FRAMES[frameIndex.value])
|
||||
|
||||
const defaultColors: Record<string, string> = {
|
||||
L: 'var(--fc-sprite-outline, #10131c)',
|
||||
s: 'var(--fc-sprite-skin, #f3c79b)',
|
||||
S: 'var(--fc-sprite-skin-shade, #c98863)',
|
||||
e: 'var(--fc-sprite-eye, #10131c)',
|
||||
m: 'var(--fc-sprite-mouth, #80484b)',
|
||||
h: 'var(--fc-sprite-hat, #ffd84d)',
|
||||
w: 'var(--fc-sprite-band, #fde68a)',
|
||||
c: 'var(--fc-sprite-shirt, #00f0ff)',
|
||||
C: 'var(--fc-sprite-shirt-shade, #0369a1)',
|
||||
p: 'var(--fc-sprite-pants, #3a1f6e)',
|
||||
P: 'var(--fc-sprite-pants-shade, #2d1857)',
|
||||
b: 'var(--fc-sprite-boots, #0f172a)',
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
function startAnim() {
|
||||
stopAnim()
|
||||
const ms = props.celebrate ? 100 : 200
|
||||
timer = setInterval(() => {
|
||||
frameIndex.value = (frameIndex.value + 1) % IDLE_FRAMES.length
|
||||
}, ms)
|
||||
}
|
||||
|
||||
const colors = computed(() => ({ ...defaultColors, ...(props.theme ?? {}) }))
|
||||
function stopAnim() {
|
||||
if (timer) { clearInterval(timer); timer = null }
|
||||
}
|
||||
|
||||
const pixels = computed(() => {
|
||||
const out: { x: number; y: number; k: string }[] = []
|
||||
spriteData.grid.forEach((row: string, y: number) => {
|
||||
for (let x = 0; x < row.length; x++) {
|
||||
const k = row[x]
|
||||
if (k === '.' || !colors.value[k]) continue
|
||||
out.push({ x, y, k })
|
||||
}
|
||||
})
|
||||
return out
|
||||
})
|
||||
watch([() => props.bob, () => props.celebrate], () => {
|
||||
if (props.bob || props.celebrate) startAnim()
|
||||
else { stopAnim(); frameIndex.value = 0 }
|
||||
}, { immediate: true })
|
||||
|
||||
onUnmounted(stopAnim)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-sprite { image-rendering: pixelated; display: inline-block; line-height: 0; }
|
||||
@keyframes fc-sprite-bob {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-3px); }
|
||||
.fc-sprite {
|
||||
image-rendering: pixelated;
|
||||
display: inline-block;
|
||||
}
|
||||
.fc-sprite--bob { animation: fc-sprite-bob 2.2s steps(2) infinite; }
|
||||
|
||||
@keyframes fc-sprite-celebrate {
|
||||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
||||
25% { transform: translateY(-6px) rotate(-3deg); }
|
||||
75% { transform: translateY(-6px) rotate(3deg); }
|
||||
}
|
||||
.fc-sprite--celebrate { animation: fc-sprite-celebrate 1s ease-in-out infinite; }
|
||||
.fc-sprite--celebrate { animation: fc-sprite-celebrate 0.5s ease-in-out infinite; }
|
||||
</style>
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user