Files
WeChatDataAnalysis/frontend/components/wrapped/shared/WrappedGameboyDither.vue
2977094657 e9c81caa12 improvement(wrapped-ui): 下线 DOS 主题并优化 Wrapped 多主题体验
- 移除 DOS 主题入口、切换器组件与相关样式逻辑,统一主题为 Modern / GameBoy / Win98。

- 新增 WrappedGameboyDither 组件,并在背景与 CRT 叠加层中引入 GameBoy 噪点效果。

- 优化 wrapped 页面视口高度与背景同步逻辑(含 ResizeObserver 与 100dvh 适配),提升桌面容器显示稳定性。

- 调整封面标题与预览位移、回复速度卡片滚动行为等细节,提升主题下视觉与交互一致性。
2026-02-07 20:59:03 +08:00

99 lines
2.1 KiB
Vue

<template>
<canvas
ref="grainRef"
class="pointer-events-none absolute inset-0 h-full w-full"
:style="canvasStyle"
aria-hidden="true"
></canvas>
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
const props = defineProps({
patternRefreshInterval: {
type: Number,
default: 2
},
patternAlpha: {
type: Number,
default: 20
},
mixBlendMode: {
type: String,
default: 'multiply'
},
patternSize: {
type: Number,
default: 512
}
})
const grainRef = ref(null)
const canvasStyle = computed(() => `image-rendering: pixelated; mix-blend-mode: ${props.mixBlendMode};`)
let animationId = 0
let frame = 0
let noiseData
let noise32
const clamp = (value, min, max) => Math.min(max, Math.max(min, value))
const resize = () => {
const canvas = grainRef.value
if (!canvas) return
const size = Math.max(64, Math.round(props.patternSize))
canvas.width = size
canvas.height = size
}
const initImageData = (ctx) => {
const canvas = grainRef.value
if (!canvas) return
noiseData = ctx.createImageData(canvas.width, canvas.height)
noise32 = new Uint32Array(noiseData.data.buffer)
}
const drawGrain = () => {
if (!noise32) return
const alpha = clamp(Math.round(props.patternAlpha), 0, 255) << 24
for (let i = 0; i < noise32.length; i++) {
const value = (Math.random() * 255) | 0
noise32[i] = alpha | (value << 16) | (value << 8) | value
}
}
const loop = (ctx) => {
const refreshEvery = Math.max(1, Math.round(props.patternRefreshInterval))
if (frame % refreshEvery === 0) {
drawGrain()
ctx.putImageData(noiseData, 0, 0)
}
frame++
animationId = window.requestAnimationFrame(() => loop(ctx))
}
onMounted(() => {
const canvas = grainRef.value
if (!canvas) return
const ctx = canvas.getContext('2d', { alpha: true })
if (!ctx) return
resize()
initImageData(ctx)
drawGrain()
ctx.putImageData(noiseData, 0, 0)
loop(ctx)
window.addEventListener('resize', resize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', resize)
window.cancelAnimationFrame(animationId)
})
</script>