// 登录页流星轨迹验证 // 目的:确认「流星进入视口期间始终不透明」,且「透明度开始变化时整条流星已在视口外」 // 用法: 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('') }