5 changed files with 349 additions and 1 deletions
-
2src/main.js
-
6src/store/modules/common.js
-
334src/utils/clickBloom.js
-
1src/views/common/login.vue
-
7src/views/common/theme.vue
@ -0,0 +1,334 @@ |
|||
const EFFECT_MS = 2500 |
|||
const MAX_BURSTS = 4 |
|||
|
|||
let canvas = null |
|||
let ctx = null |
|||
let bursts = [] |
|||
let frameId = 0 |
|||
|
|||
function ensureCanvas () { |
|||
if (canvas) { |
|||
return |
|||
} |
|||
canvas = document.createElement('canvas') |
|||
canvas.style.position = 'fixed' |
|||
canvas.style.left = '0' |
|||
canvas.style.top = '0' |
|||
canvas.style.width = '100%' |
|||
canvas.style.height = '100%' |
|||
canvas.style.pointerEvents = 'none' |
|||
canvas.style.zIndex = '10000' |
|||
document.body.appendChild(canvas) |
|||
ctx = canvas.getContext('2d') |
|||
resizeCanvas() |
|||
window.addEventListener('resize', resizeCanvas) |
|||
} |
|||
|
|||
function resizeCanvas () { |
|||
if (!canvas) { |
|||
return |
|||
} |
|||
const ratio = window.devicePixelRatio || 1 |
|||
canvas.width = Math.floor(window.innerWidth * ratio) |
|||
canvas.height = Math.floor(window.innerHeight * ratio) |
|||
if (ctx) { |
|||
ctx.setTransform(ratio, 0, 0, ratio, 0, 0) |
|||
} |
|||
} |
|||
|
|||
function removeCanvas () { |
|||
if (frameId) { |
|||
cancelAnimationFrame(frameId) |
|||
frameId = 0 |
|||
} |
|||
window.removeEventListener('resize', resizeCanvas) |
|||
if (canvas && canvas.parentNode) { |
|||
canvas.parentNode.removeChild(canvas) |
|||
} |
|||
canvas = null |
|||
ctx = null |
|||
bursts = [] |
|||
} |
|||
|
|||
function farthestScreenDistance (x, y) { |
|||
const width = window.innerWidth |
|||
const height = window.innerHeight |
|||
const corners = [[0, 0], [width, 0], [0, height], [width, height]] |
|||
let maxDistance = 0 |
|||
corners.forEach(function (corner) { |
|||
const dx = corner[0] - x |
|||
const dy = corner[1] - y |
|||
const distance = Math.sqrt(dx * dx + dy * dy) |
|||
if (distance > maxDistance) { |
|||
maxDistance = distance |
|||
} |
|||
}) |
|||
return maxDistance + 48 |
|||
} |
|||
|
|||
function createBurst (x, y) { |
|||
const reach = farthestScreenDistance(x, y) |
|||
const particles = [] |
|||
const particleCount = 46 |
|||
for (let i = 0; i < particleCount; i++) { |
|||
const angle = Math.random() * Math.PI * 2 |
|||
particles.push({ |
|||
angle: angle, |
|||
speed: reach * (0.72 + Math.random() * 0.38), |
|||
size: 2.2 + Math.random() * 3.6 |
|||
}) |
|||
} |
|||
const bolts = [] |
|||
const boltCount = 18 |
|||
for (let i = 0; i < boltCount; i++) { |
|||
const angle = (Math.PI * 2 * i) / boltCount + (Math.random() - 0.5) * 0.28 |
|||
const length = reach * (0.92 + Math.random() * 0.16) |
|||
const segments = 16 |
|||
const points = [{ x: x, y: y }] |
|||
for (let s = 1; s <= segments; s++) { |
|||
const ratio = s / segments |
|||
const dist = length * ratio |
|||
const sway = (Math.random() - 0.5) * 54 * (1 - ratio * 0.25) |
|||
points.push({ |
|||
x: x + Math.cos(angle) * dist + Math.cos(angle + Math.PI / 2) * sway, |
|||
y: y + Math.sin(angle) * dist + Math.sin(angle + Math.PI / 2) * sway |
|||
}) |
|||
} |
|||
bolts.push({ points: points }) |
|||
} |
|||
const stars = [] |
|||
const starCount = 200 |
|||
for (let i = 0; i < starCount; i++) { |
|||
stars.push({ |
|||
x: Math.random() * window.innerWidth, |
|||
y: Math.random() * window.innerHeight, |
|||
size: 1.4 + Math.random() * 6.2, |
|||
phase: Math.random() * Math.PI * 2, |
|||
blinks: 3 + Math.floor(Math.random() * 4), |
|||
gold: i % 2 === 0 |
|||
}) |
|||
} |
|||
return { |
|||
x: x, |
|||
y: y, |
|||
reach: reach, |
|||
start: performance.now(), |
|||
particles: particles, |
|||
bolts: bolts, |
|||
stars: stars |
|||
} |
|||
} |
|||
|
|||
function pointOnBolt (points, t) { |
|||
const clamped = Math.max(0, Math.min(1, t)) |
|||
const scaled = clamped * (points.length - 1) |
|||
const index = Math.floor(scaled) |
|||
const next = Math.min(index + 1, points.length - 1) |
|||
const mix = scaled - index |
|||
return { |
|||
x: points[index].x + (points[next].x - points[index].x) * mix, |
|||
y: points[index].y + (points[next].y - points[index].y) * mix |
|||
} |
|||
} |
|||
|
|||
function drawBolt (bolt, progress) { |
|||
const travel = Math.min(1, progress / 0.42) |
|||
const head = travel |
|||
const tail = Math.max(0, head - 0.16) |
|||
if (head <= tail) { |
|||
return |
|||
} |
|||
const steps = 10 |
|||
ctx.beginPath() |
|||
const start = pointOnBolt(bolt.points, tail) |
|||
ctx.moveTo(start.x, start.y) |
|||
for (let step = 1; step <= steps; step++) { |
|||
const point = pointOnBolt(bolt.points, tail + (head - tail) * (step / steps)) |
|||
ctx.lineTo(point.x, point.y) |
|||
} |
|||
const tipFade = travel < 1 ? 1 : Math.max(0, 1 - (progress - 0.42) / 0.58) |
|||
ctx.save() |
|||
ctx.globalAlpha = tipFade |
|||
ctx.lineCap = 'round' |
|||
ctx.lineJoin = 'round' |
|||
ctx.strokeStyle = 'rgba(70, 225, 255, 0.95)' |
|||
ctx.lineWidth = 5.5 |
|||
ctx.shadowColor = 'rgba(0, 200, 255, 1)' |
|||
ctx.shadowBlur = 18 |
|||
ctx.stroke() |
|||
ctx.shadowBlur = 0 |
|||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.98)' |
|||
ctx.lineWidth = 1.7 |
|||
ctx.stroke() |
|||
ctx.restore() |
|||
} |
|||
|
|||
function drawSeismicWave (burst, progress) { |
|||
const delays = [0, 0.1, 0.2] |
|||
delays.forEach(function (delay, index) { |
|||
const span = 1 - delay |
|||
const local = (progress - delay) / span |
|||
if (local <= 0 || local > 1) { |
|||
return |
|||
} |
|||
const eased = 1 - Math.pow(1 - local, 2) |
|||
const radius = 28 + eased * (burst.reach + index * 36) |
|||
const alpha = (1 - local) * 0.42 |
|||
const steps = 80 |
|||
ctx.save() |
|||
ctx.beginPath() |
|||
for (let i = 0; i <= steps; i++) { |
|||
const angle = (Math.PI * 2 * i) / steps |
|||
const wobble = 1 + Math.sin(angle * 7 + index * 1.4) * 0.04 |
|||
const pointX = burst.x + Math.cos(angle) * radius * wobble |
|||
const pointY = burst.y + Math.sin(angle) * radius * wobble |
|||
if (i === 0) { |
|||
ctx.moveTo(pointX, pointY) |
|||
} else { |
|||
ctx.lineTo(pointX, pointY) |
|||
} |
|||
} |
|||
ctx.closePath() |
|||
ctx.lineWidth = 10 * (1 - local * 0.55) |
|||
ctx.strokeStyle = 'rgba(190, 245, 255, ' + alpha + ')' |
|||
ctx.shadowColor = 'rgba(140, 230, 255, ' + alpha + ')' |
|||
ctx.shadowBlur = 14 |
|||
ctx.stroke() |
|||
ctx.restore() |
|||
}) |
|||
} |
|||
|
|||
function drawDiamond (x, y, size, alpha) { |
|||
if (alpha <= 0.02) { |
|||
return |
|||
} |
|||
ctx.save() |
|||
ctx.translate(x, y) |
|||
ctx.rotate(Math.PI / 4) |
|||
ctx.fillStyle = 'rgba(176, 232, 255, ' + alpha + ')' |
|||
ctx.fillRect(-size, -size, size * 2, size * 2) |
|||
ctx.fillStyle = 'rgba(255, 255, 255, ' + alpha + ')' |
|||
ctx.fillRect(-size * 0.38, -size * 0.38, size * 0.76, size * 0.76) |
|||
ctx.restore() |
|||
} |
|||
|
|||
function drawScreenStars (burst, progress) { |
|||
const fade = 0.45 + (1 - progress) * 0.55 |
|||
burst.stars.forEach(function (star) { |
|||
const twinkle = Math.abs(Math.sin(progress * Math.PI * star.blinks + star.phase)) |
|||
if (twinkle < 0.12) { |
|||
return |
|||
} |
|||
const alpha = fade * twinkle |
|||
ctx.save() |
|||
ctx.translate(star.x, star.y) |
|||
ctx.rotate(Math.PI / 4) |
|||
ctx.fillStyle = star.gold ? 'rgba(255, 230, 0, ' + alpha + ')' : 'rgba(0, 245, 255, ' + alpha + ')' |
|||
ctx.fillRect(-star.size, -star.size, star.size * 2, star.size * 2) |
|||
ctx.restore() |
|||
}) |
|||
} |
|||
|
|||
function drawWaveDiamonds (burst, progress) { |
|||
const delays = [0, 0.1, 0.2] |
|||
delays.forEach(function (delay, index) { |
|||
const span = 1 - delay |
|||
const local = (progress - delay) / span |
|||
if (local <= 0 || local > 1) { |
|||
return |
|||
} |
|||
const eased = 1 - Math.pow(1 - local, 2) |
|||
const radius = 28 + eased * (burst.reach + index * 36) |
|||
const alpha = (1 - local) * 0.9 |
|||
const count = 26 |
|||
for (let i = 0; i < count; i++) { |
|||
const angle = (Math.PI * 2 * i) / count + progress * 0.6 + index |
|||
const wobble = 1 + Math.sin(angle * 7 + index * 1.4) * 0.04 |
|||
drawDiamond( |
|||
burst.x + Math.cos(angle) * radius * wobble, |
|||
burst.y + Math.sin(angle) * radius * wobble, |
|||
2.2 + (i % 3) * 0.7, |
|||
alpha |
|||
) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
function drawBurst (burst, now) { |
|||
const progress = Math.min(1, (now - burst.start) / EFFECT_MS) |
|||
const fade = 1 - progress |
|||
drawSeismicWave(burst, progress) |
|||
drawWaveDiamonds(burst, progress) |
|||
drawScreenStars(burst, progress) |
|||
burst.particles.forEach(function (particle) { |
|||
const dist = particle.speed * progress |
|||
const px = burst.x + Math.cos(particle.angle) * dist |
|||
const py = burst.y + Math.sin(particle.angle) * dist |
|||
const radius = particle.size * (1 - progress * 0.25) |
|||
ctx.save() |
|||
ctx.beginPath() |
|||
ctx.fillStyle = 'rgba(214, 224, 236, ' + fade + ')' |
|||
ctx.shadowColor = 'rgba(236, 244, 255, ' + fade + ')' |
|||
ctx.shadowBlur = 16 |
|||
ctx.arc(px, py, radius * 2.1, 0, Math.PI * 2) |
|||
ctx.fill() |
|||
ctx.shadowBlur = 0 |
|||
ctx.beginPath() |
|||
ctx.fillStyle = 'rgba(120, 230, 255, ' + fade + ')' |
|||
ctx.arc(px, py, radius * 0.72, 0, Math.PI * 2) |
|||
ctx.fill() |
|||
ctx.restore() |
|||
}) |
|||
burst.bolts.forEach(function (bolt) { |
|||
drawBolt(bolt, progress) |
|||
}) |
|||
return progress < 1 |
|||
} |
|||
|
|||
function render (now) { |
|||
if (!ctx || !canvas) { |
|||
frameId = 0 |
|||
return |
|||
} |
|||
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight) |
|||
const alive = [] |
|||
bursts.forEach(function (burst) { |
|||
if (drawBurst(burst, now)) { |
|||
alive.push(burst) |
|||
} |
|||
}) |
|||
bursts = alive |
|||
if (bursts.length === 0) { |
|||
removeCanvas() |
|||
return |
|||
} |
|||
frameId = requestAnimationFrame(render) |
|||
} |
|||
|
|||
function spawn (x, y) { |
|||
ensureCanvas() |
|||
bursts.push(createBurst(x, y)) |
|||
if (bursts.length > MAX_BURSTS) { |
|||
bursts.splice(0, bursts.length - MAX_BURSTS) |
|||
} |
|||
if (!frameId) { |
|||
frameId = requestAnimationFrame(render) |
|||
} |
|||
} |
|||
|
|||
export function installClickBloom (store) { |
|||
if (!store || window.__wmsClickBloomInstalled) { |
|||
return |
|||
} |
|||
window.__wmsClickBloomInstalled = true |
|||
document.addEventListener('mousedown', function (event) { |
|||
const enabled = store.state && store.state.common && store.state.common.clickEffectEnabled |
|||
if (!enabled || event.button !== 0) { |
|||
if (!enabled) { |
|||
removeCanvas() |
|||
} |
|||
return |
|||
} |
|||
spawn(event.clientX, event.clientY) |
|||
}, true) |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue