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