Files
nl-game/src/components/PixelAvatar.vue

69 lines
2.1 KiB
Vue
Raw Normal View History

// 像素头像渲染组件:把 'px:NN' 编码画成像素图8×8 网格放大,锐利无插值)
// 兼容旧 emoji 数据:非像素编码时原样以文字显示,历史用户零迁移
<script setup>
import { computed } from 'vue'
import { AVATAR_SHAPES, AVATAR_PALETTES, decodePixelAvatar } from '../games/pixelAvatars'
const props = defineProps({
// 头像值:'px:NN' 像素编码 或 旧 emoji 字符
code: { type: String, default: '' },
// 渲染尺寸px
size: { type: Number, default: 32 },
})
const px = computed(() => decodePixelAvatar(props.code))
// dataURL 缓存:同 code+size 只画一次(组件实例间不共享,同一实例多次渲染直接命中)
const cache = new Map()
function dataUrl(size) {
const key = props.code + '@' + size
if (cache.has(key)) return cache.get(key)
const p = px.value
if (!p) return ''
const shape = AVATAR_SHAPES[p.shape]
const pal = AVATAR_PALETTES[p.palette]
const cell = Math.max(1, Math.round(size / 8))
const real = cell * 8
const cv = document.createElement('canvas')
cv.width = real
cv.height = real
const ctx = cv.getContext('2d')
const colors = { 1: pal.c1, 2: pal.c2, 3: pal.c3, 4: '#ffffff', 5: '#1a1a24' }
shape.forEach((row, y) => {
for (let x = 0; x < 8; x++) {
const ch = row[x]
if (ch === '.') continue
ctx.fillStyle = colors[ch] || colors[1]
ctx.fillRect(x * cell, y * cell, cell, cell)
}
})
const url = cv.toDataURL()
cache.set(key, url)
return url
}
</script>
<template>
<span class="px-avatar" :style="{ width: size + 'px', height: size + 'px', fontSize: Math.round(size * 0.6) + 'px' }">
<img v-if="px" :src="dataUrl(size)" :width="size" :height="size" alt="" class="px-img" />
<template v-else>{{ code || '🎮' }}</template>
</span>
</template>
<style scoped>
.px-avatar {
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
line-height: 1;
flex-shrink: 0;
}
.px-img {
display: block;
width: 100%;
height: 100%;
image-rendering: pixelated; /* 像素图放大不模糊 */
}
</style>