96 lines
4.4 KiB
JavaScript
96 lines
4.4 KiB
JavaScript
// 登录页流星轨迹验证
|
||
// 目的:确认「流星进入视口期间始终不透明」,且「透明度开始变化时整条流星已在视口外」
|
||
// 用法: node _design-test/verify-meteor.js [视口宽] [视口高] [试验次数]
|
||
const VW = Number(process.argv[2] || 1280)
|
||
const VH = Number(process.argv[3] || 720)
|
||
const N = Number(process.argv[4] || 20000)
|
||
|
||
// 动画时间轴(与 login.vue 的 @keyframes meteor-shower 一致)
|
||
// 0% 透明度 0 → 0.5% 变 1 → 60% 仍为 1 → 66.7% 变 0 → 100% 保持 0
|
||
// 位移在 0%~60% 内从起点线性走到终点,60% 之后停在终点
|
||
const FLY_END = 0.60 // 飞行结束(到达终点)的进度
|
||
const FADE_IN_END = 0.005 // 淡入完成的进度(0.5%)
|
||
const FADE_OUT_START = 0.60 // 淡出开始的进度
|
||
|
||
const inside = (x, y) => x >= 0 && x <= VW && y >= 0 && y <= VH
|
||
|
||
// 判断「头部 + 拖尾」这条线段是否与视口相交(沿线采样)
|
||
function streakVisible(hx, hy, ux, uy, tail) {
|
||
for (let i = 0; i <= 40; i++) {
|
||
const s = (i / 40) * tail
|
||
if (inside(hx - ux * s, hy - uy * s)) return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
function headAt(c) { return Math.min(c / FLY_END, 1) } // 进度 -> 路径参数
|
||
|
||
function build(variant) {
|
||
const tail = 90 + Math.random() * 70
|
||
const margin = tail + 60
|
||
const theta = (25 + Math.random() * 30) * Math.PI / 180
|
||
const ux = -Math.cos(theta), uy = Math.sin(theta)
|
||
|
||
if (variant === 'old') {
|
||
// 旧实现:起终点都取在视口内部
|
||
const sx = VW * (0.50 + Math.random() * 0.50)
|
||
const sy = VH * (Math.random() * 0.35)
|
||
const ex = VW * (Math.random() * 0.30)
|
||
const ey = VH * (0.85 + Math.random() * 0.20)
|
||
return { sx, sy, tx: ex - sx, ty: ey - sy, ux: (sx - ex), uy: (sy - ey), tail, margin }
|
||
}
|
||
// 新实现
|
||
let sx, sy
|
||
if (Math.random() < 0.5) { sx = VW * (0.45 + Math.random() * 0.65); sy = -margin }
|
||
else { sx = VW + margin; sy = VH * (0.10 + Math.random() * 0.40) }
|
||
const t = Math.min((VH + margin - sy) / uy, (-margin - sx) / ux)
|
||
return { sx, sy, tx: ux * t, ty: uy * t, ux, uy, tail, margin }
|
||
}
|
||
|
||
function analyze(variant) {
|
||
let worstFadeWhileVisible = 0 // 可见期间透明度掉到 1 以下的最大程度(0 = 完美)
|
||
let maxVisibleAtFlyEnd = 0 // 飞行结束时仍可见的比例
|
||
let minEnter = 1, maxEnter = 0, minExit = 1, maxExit = 0
|
||
const visAt = []
|
||
for (let n = 0; n < N; n++) {
|
||
const m = build(variant)
|
||
const ulen = Math.hypot(m.ux, m.uy) || 1
|
||
const ux = m.ux / ulen, uy = m.uy / ulen // 单位化的「拖尾指向」
|
||
let enter = -1, exit = -1
|
||
for (let i = 0; i <= 600; i++) {
|
||
const c = i / 600
|
||
const f = headAt(c)
|
||
const hx = m.sx + m.tx * f, hy = m.sy + m.ty * f
|
||
const vis = streakVisible(hx, hy, ux, uy, m.tail)
|
||
if (vis && enter < 0) enter = c
|
||
if (vis) exit = c
|
||
}
|
||
if (enter < 0) continue
|
||
visAt.push(enter)
|
||
if (enter < minEnter) minEnter = enter
|
||
if (enter > maxEnter) maxEnter = enter
|
||
if (exit < minExit) minExit = exit
|
||
if (exit > maxExit) maxExit = exit
|
||
// 可见窗口内透明度是否恒为 1
|
||
if (enter < FADE_IN_END) worstFadeWhileVisible = Math.max(worstFadeWhileVisible, FADE_IN_END - enter)
|
||
if (exit > FADE_OUT_START) worstFadeWhileVisible = Math.max(worstFadeWhileVisible, exit - FADE_OUT_START)
|
||
maxVisibleAtFlyEnd = Math.max(maxVisibleAtFlyEnd, Math.max(0, exit - FADE_OUT_START))
|
||
}
|
||
visAt.sort((a, b) => a - b)
|
||
const med = visAt[Math.floor(visAt.length / 2)] || 0
|
||
return { minEnter, maxEnter, medEnter: med, minExit, maxExit, worstFadeWhileVisible, maxVisibleAtFlyEnd, count: visAt.length }
|
||
}
|
||
|
||
for (const v of ['old', 'new']) {
|
||
const r = analyze(v)
|
||
const f1 = n => (n * 100).toFixed(1)
|
||
console.log((v === 'old' ? '【旧】' : '【新】') + ' 实现 视口 ' + VW + 'x' + VH + ' 有效样本 ' + r.count)
|
||
console.log(' 可见区间:进入 ' + f1(r.minEnter) + '%~' + f1(r.maxEnter) + '% 离开 ' +
|
||
f1(r.minExit) + '%~' + f1(r.maxExit) + '%')
|
||
console.log(' 可见期间透明度不足的最大窗口:' + f1(r.worstFadeWhileVisible) + '% of 周期' +
|
||
(r.worstFadeWhileVisible <= 0.0001 ? ' ✓ 无(可见时恒为不透明)' : ' ← 会在屏内淡入/淡出'))
|
||
console.log(' 飞行结束时仍未出屏的量:' + f1(r.maxVisibleAtFlyEnd) + '%' +
|
||
(r.maxVisibleAtFlyEnd <= 0.0001 ? ' ✓ 整条流星已出屏' : ' ← 未出屏就已淡出'))
|
||
console.log('')
|
||
}
|