feat: align with upstream 1.5.9.9

This commit is contained in:
2026-09-12 18:21:11 +08:00
commit 34f0679117
805 changed files with 376275 additions and 0 deletions
+307
View File
@@ -0,0 +1,307 @@
<template>
<div class="absolute inset-0 z-0 bg-bg-primary overflow-hidden">
<canvas
ref="canvasRef"
class="w-full h-full block touch-none pointer-events-auto"
/>
<div
class="absolute inset-0 pointer-events-none"
:style="{ background: `radial-gradient(circle_at_center,transparent_45%,${glowColors.overlayEnd} 100%)` }"
/>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { useTheme } from '~/composables/useTheme'
const props = defineProps({
settings: { type: Object, required: true },
isAccelerating: { type: Boolean, required: true },
currentProgress: { type: Number, required: true }
})
const { isDark } = useTheme()
const canvasRef = ref(null)
const settingsRef = ref(props.settings)
const isAcceleratingRef = ref(props.isAccelerating)
watch(() => props.settings, (val) => { settingsRef.value = val })
watch(() => props.isAccelerating, (val) => { isAcceleratingRef.value = val })
watch(isDark, () => {
loadBrandColors()
// 主题切换后重绘光晕颜色
glowColors.load()
// 主题切换后重新生成星星颜色
stars.length = 0
for (let i = 0; i < 250; i++) {
stars.push(initStar({ z: Math.random() * 1000 }))
}
})
const pointer = { x: 0, y: 0, targetX: 0, targetY: 0 }
// Canvas 动画所需的状态,提升为模块级以便主题切换时重绘
const stars = []
// Canvas 2D API 不识别 CSS 变量,此处从 CSS 变量动态读取实际色值
const BRAND_COLORS = {}
function loadBrandColors(root) {
root = root || document.documentElement
const cs = getComputedStyle(root)
const keys = [
'warp-canvas-indigo', 'warp-canvas-primary', 'warp-canvas-primary-light', 'brand-blue-light',
'brand-green', 'brand-green-light', 'brand-red', 'brand-pink',
'brand-teal', 'brand-orange', 'brand-yellow-light',
'text-primary', 'text-primary-lighter', 'brand-cyan',
]
for (const key of keys) {
const val = cs.getPropertyValue(`--${key}`).trim()
if (val) BRAND_COLORS[key] = toHexValue(val)
}
}
if (typeof document !== 'undefined') {
loadBrandColors()
}
/** 将任意 CSS 颜色值统一转为 hex (#RRGGBB) */
function toHexValue(color) {
if (color.startsWith('#')) {
// #RGB 展开为 #RRGGBB
if (color.length === 4) {
return `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}`
}
return color
}
// rgb(r, g, b) / rgba(r, g, b, a)
const m = color.match(/rgba?\(\s*([\d.]+)\s*[,%]\s*([\d.]+)\s*[,%]\s*([\d.]+)/)
if (m) {
const r = parseInt(m[1], 10)
const g = parseInt(m[2], 10)
const b = parseInt(m[3], 10)
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`
}
// 兜底:原样返回
return color
}
/** 将 hex 颜色转为 rgba 数组 [r, g, b, a] */
function hexToRgba(hex) {
let h = hex.replace('#', '')
if (h.length === 3) h = `${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}`
const r = parseInt(h.substring(0, 2), 16)
const g = parseInt(h.substring(2, 4), 16)
const b = parseInt(h.substring(4, 6), 16)
return [r, g, b, 1]
}
/** 将 hex 颜色 + alpha 转为 rgba() 字符串 */
function hexWithAlpha(hex, alpha) {
const [r, g, b] = hexToRgba(hex)
return `rgba(${r}, ${g}, ${b}, ${alpha})`
}
// 光晕颜色 — 统一从 CSS 变量 --app-loading-brand-primary 读取
const glowColors = {
color: '', // hex
overlayEnd: '', // 解析后的 rgba 字符串
load() {
const cs = getComputedStyle(document.documentElement)
const raw = cs.getPropertyValue('--app-loading-brand-primary').trim()
// 回退值与 SSR 默认主题(经典深色)的主色对齐
this.color = raw ? toHexValue(raw) : '#6366f1'
// 解析 --canvas-glow-overlay-end 的实际 rgba 值
const rawOverlay = cs.getPropertyValue('--canvas-glow-overlay-end').trim()
this.overlayEnd = rawOverlay || 'rgba(245, 245, 247, 0.7)'
}
}
if (typeof document !== 'undefined') {
glowColors.load()
}
function getRandomColor(base) {
const cold = ['warp-canvas-indigo', 'warp-canvas-primary', 'brand-blue-light', 'text-primary', 'warp-canvas-primary-light']
if (base === 'emerald') {
const greens = ['brand-green', 'brand-green-light', 'brand-cyan']
return toHex(greens[Math.floor(Math.random() * greens.length)])
}
if (base === 'cyberpunk') {
const pinks = ['brand-red', 'brand-pink', 'warp-canvas-primary', 'brand-teal']
return toHex(pinks[Math.floor(Math.random() * pinks.length)])
}
if (base === 'sunset') {
const warm = ['brand-orange', 'brand-red', 'text-primary-lighter', 'brand-yellow-light', 'brand-pink']
return toHex(warm[Math.floor(Math.random() * warm.length)])
}
return toHex(cold[Math.floor(Math.random() * cold.length)])
}
function toHex(color) {
// 已经是 hex 直接返回
if (color.startsWith('#')) return color
// 从映射表取(已统一为 hex)
return BRAND_COLORS[color] ?? glowColors.color
}
function initStar(partial) {
partial = partial || {}
return {
x: partial.x ?? (Math.random() - 0.5) * 1000,
y: partial.y ?? (Math.random() - 0.5) * 1000,
z: partial.z ?? Math.random() * 1000 + 10,
prevZ: partial.z ?? 1000,
color: getRandomColor('indigo'),
size: Math.random() * 1.5 + 1.2,
angle: Math.random() * Math.PI * 2,
}
}
onMounted(() => {
const canvas = canvasRef.value
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
let animationId
let width = 0
let height = 0
const handleResize = () => {
const parent = canvas.parentElement
if (parent) {
width = parent.clientWidth
height = parent.clientHeight
canvas.width = width
canvas.height = height
}
}
const resizeObserver = new ResizeObserver(() => handleResize())
if (canvas.parentElement) {
resizeObserver.observe(canvas.parentElement)
}
handleResize()
for (let i = 0; i < 250; i++) {
stars.push(initStar({ z: Math.random() * 1000 }))
}
let speedFactor = 1.0
let thicknessFactor = 1.2
let glowFactor = 0.12
let radialGlowOpacity = 0.2
const handleMouseMove = (e) => {
const halfW = window.innerWidth / 2
const halfH = window.innerHeight / 2
pointer.targetX = (e.clientX - halfW) / halfW
pointer.targetY = (e.clientY - halfH) / halfH
}
const handleTouchMove = (e) => {
if (e.touches.length > 0) {
const touch = e.touches[0]
const halfW = window.innerWidth / 2
const halfH = window.innerHeight / 2
pointer.targetX = (touch.clientX - halfW) / halfW
pointer.targetY = (touch.clientY - halfH) / halfH
}
}
const handleMouseLeave = () => {
pointer.targetX = 0
pointer.targetY = 0
}
if (props.settings.interactive) {
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('touchmove', handleTouchMove, { passive: true })
window.addEventListener('mouseleave', handleMouseLeave)
}
const tick = () => {
const currentSettings = settingsRef.value
const accelerating = isAcceleratingRef.value
const targetSpeedFactor = accelerating ? 3.2 : 1.0
const targetThicknessFactor = accelerating ? 2.2 : 1.2
const targetGlowFactor = accelerating ? 0.26 : 0.12
const targetRadialGlowOpacity = accelerating ? 0.42 : 0.2
speedFactor += (targetSpeedFactor - speedFactor) * 0.07
thicknessFactor += (targetThicknessFactor - thicknessFactor) * 0.07
glowFactor += (targetGlowFactor - glowFactor) * 0.07
radialGlowOpacity += (targetRadialGlowOpacity - radialGlowOpacity) * 0.07
pointer.x += (pointer.targetX - pointer.x) * 0.08
pointer.y += (pointer.targetY - pointer.y) * 0.08
ctx.save()
// 从 CSS 变量读取背景叠加色
const bgOverlay = getComputedStyle(document.documentElement)
.getPropertyValue('--canvas-bg-overlay').trim()
ctx.fillStyle = bgOverlay || 'rgba(7, 7, 9, 0.18)'
ctx.fillRect(0, 0, width, height)
const centerX = width / 2 + pointer.x * (width * 0.12)
const centerY = height / 2 + pointer.y * (height * 0.12)
const currentSpeed = 7 * currentSettings.speedMultiplier * speedFactor
stars.forEach((star) => {
star.prevZ = star.z
star.z -= currentSpeed
if (star.z <= 0) {
Object.assign(star, initStar({ z: 1000 }))
star.prevZ = star.z
}
const scaleFactor = 300
const screenX = (star.x / star.z) * scaleFactor + centerX
const screenY = (star.y / star.z) * scaleFactor + centerY
const prevScreenX = (star.x / star.prevZ) * scaleFactor + centerX
const prevScreenY = (star.y / star.prevZ) * scaleFactor + centerY
if (screenX < -200 || screenX > width + 200 || screenY < -200 || screenY > height + 200) return
const depthAlpha = Math.min(1, (1000 - star.z) / 800) * 0.85
ctx.strokeStyle = star.color
ctx.beginPath()
ctx.lineWidth = star.size * thicknessFactor * (1 - star.z / 1000)
ctx.globalAlpha = depthAlpha
ctx.moveTo(prevScreenX, prevScreenY)
ctx.lineTo(screenX, screenY)
ctx.stroke()
})
if (currentSettings.glowEffect) {
ctx.globalAlpha = glowFactor
const grad = ctx.createRadialGradient(centerX, centerY, 5, centerX, centerY, Math.max(width, height) * 0.75)
grad.addColorStop(0, hexWithAlpha(glowColors.color, radialGlowOpacity))
grad.addColorStop(0.5, hexWithAlpha(glowColors.color, 0.04))
grad.addColorStop(1, 'transparent')
ctx.fillStyle = grad
ctx.fillRect(0, 0, width, height)
}
ctx.restore()
animationId = requestAnimationFrame(tick)
}
animationId = requestAnimationFrame(tick)
onUnmounted(() => {
cancelAnimationFrame(animationId)
resizeObserver.disconnect()
if (props.settings.interactive) {
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('touchmove', handleTouchMove)
window.removeEventListener('mouseleave', handleMouseLeave)
}
})
})
</script>