SecurityParticles.vue 2.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. <template>
  2. <canvas ref="canvasRef" class="security-canvas"></canvas>
  3. </template>
  4. <script setup>
  5. import { ref, onMounted, onBeforeUnmount } from 'vue'
  6. const canvasRef = ref(null)
  7. let ctx = null
  8. let animationId = null
  9. let particles = []
  10. let width = 0
  11. let height = 0
  12. let dpr = 1
  13. function createParticles(count) {
  14. particles = []
  15. for (let i = 0; i < count; i++) {
  16. particles.push({
  17. x: Math.random() * width,
  18. y: Math.random() * height,
  19. vx: (Math.random() - 0.5) * 0.5,
  20. vy: -(0.2 + Math.random() * 1.2),
  21. size: 2,
  22. alpha: 0.4 + Math.random() * 0.6,
  23. sway: Math.random() * Math.PI * 2,
  24. swayAmp: 0.2 + Math.random() * 1.2
  25. })
  26. }
  27. }
  28. function resize() {
  29. const el = canvasRef.value
  30. if (!el) return
  31. const rect = el.getBoundingClientRect()
  32. width = Math.max(0, rect.width)
  33. height = Math.max(0, rect.height)
  34. dpr = window.devicePixelRatio || 1
  35. el.width = Math.max(1, Math.floor(width * dpr))
  36. el.height = Math.max(1, Math.floor(height * dpr))
  37. el.style.width = width + 'px'
  38. el.style.height = height + 'px'
  39. ctx = el.getContext('2d')
  40. // reset transform then apply devicePixelRatio scale so drawing coords stay in CSS px
  41. ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
  42. const area = width * height
  43. const count = Math.min(600, Math.max(60, Math.round(area / 8000)))
  44. createParticles(count)
  45. }
  46. function step() {
  47. if (!ctx) return
  48. ctx.clearRect(0, 0, width, height)
  49. for (let p of particles) {
  50. // horizontal sway + slight vx
  51. p.sway += 0.01 + Math.random() * 0.01
  52. p.x += p.vx + Math.sin(p.sway) * (p.swayAmp * 0.3)
  53. p.y += p.vy
  54. if (p.y < -10) {
  55. p.y = height + 10
  56. p.x = Math.random() * width
  57. }
  58. // draw 1px particle
  59. ctx.fillStyle = `rgba(255,255,255,${p.alpha})`
  60. // rounding to avoid subpixel blurry 1px
  61. ctx.fillRect(Math.round(p.x), Math.round(p.y), p.size, p.size)
  62. }
  63. animationId = requestAnimationFrame(step)
  64. }
  65. onMounted(() => {
  66. resize()
  67. animationId = requestAnimationFrame(step)
  68. window.addEventListener('resize', resize)
  69. })
  70. onBeforeUnmount(() => {
  71. window.removeEventListener('resize', resize)
  72. if (animationId) cancelAnimationFrame(animationId)
  73. })
  74. </script>
  75. <style scoped>
  76. .security-canvas {
  77. width: 100%;
  78. height: 100%;
  79. display: block;
  80. pointer-events: none;
  81. }
  82. </style>