1import { Controller } from "@hotwired/stimulus"; 2 3export default class extends Controller { 4 static targets = ["canvas"]; 5 static values = { 6 strokeColor: { type: String, default: "#737373" }, 7 numberOfLines: { type: Number, default: 50 }, 8 numberOfDiscs: { type: Number, default: 50 }, 9 particleRgbColor: { type: Array, default: [255, 255, 255] }, 10 }; 11 12 connect() { 13 this.canvas = this.canvasTarget; 14 this.ctx = this.canvas.getContext("2d"); 15 this.animationId = null; 16 17 // Initialize state 18 this.state = { 19 discs: [], 20 lines: [], 21 particles: [], 22 clip: {}, 23 startDisc: { p: 0, x: 0, y: 0, w: 0, h: 0 }, 24 endDisc: { p: 0, x: 0, y: 0, w: 0, h: 0 }, 25 rect: { width: 0, height: 0 }, 26 render: { width: 0, height: 0, dpi: 1 }, 27 particleArea: {}, 28 linesCanvas: null, 29 }; 30 31 this.init(); 32 this.startAnimation(); 33 this.setupResize(); 34 } 35 36 disconnect() { 37 if (this.animationId) { 38 cancelAnimationFrame(this.animationId); 39 } 40 window.removeEventListener("resize", this.handleResize); 41 } 42 43 init() { 44 this.setSize(); 45 this.setDiscs(); 46 this.setLines(); 47 this.setParticles(); 48 } 49 50 setSize() { 51 const rect = this.canvas.getBoundingClientRect(); 52 this.state.rect = { width: rect.width, height: rect.height }; 53 this.state.render = { 54 width: rect.width, 55 height: rect.height, 56 dpi: window.devicePixelRatio || 1, 57 }; 58 this.canvas.width = this.state.render.width * this.state.render.dpi; 59 this.canvas.height = this.state.render.height * this.state.render.dpi; 60 } 61 62 tweenValue(start, end, p, ease = null) { 63 const delta = end - start; 64 const easeFn = ease === "inExpo" ? (p) => (p === 0 ? 0 : Math.pow(2, 10 * (p - 1))) : (p) => p; 65 return start + delta * easeFn(p); 66 } 67 68 tweenDisc(disc) { 69 const { startDisc, endDisc } = this.state; 70 disc.x = this.tweenValue(startDisc.x, endDisc.x, disc.p); 71 disc.y = this.tweenValue(startDisc.y, endDisc.y, disc.p, "inExpo"); 72 disc.w = this.tweenValue(startDisc.w, endDisc.w, disc.p); 73 disc.h = this.tweenValue(startDisc.h, endDisc.h, disc.p); 74 } 75 76 setDiscs() { 77 const { width, height } = this.state.rect; 78 if (width <= 0 || height <= 0) return; 79 80 this.state.discs = []; 81 this.state.startDisc = { 82 p: 0, 83 x: width * 0.5, 84 y: height * 0.45, 85 w: width * 0.75, 86 h: height * 0.7, 87 }; 88 this.state.endDisc = { 89 p: 0, 90 x: width * 0.5, 91 y: height * 0.95, 92 w: 0, 93 h: 0, 94 }; 95 96 let prevBottom = height; 97 this.state.clip = {}; 98 99 for (let i = 0; i < this.numberOfDiscsValue; i++) { 100 const p = i / this.numberOfDiscsValue; 101 const disc = { p, x: 0, y: 0, w: 0, h: 0 }; 102 this.tweenDisc(disc); 103 const bottom = disc.y + disc.h; 104 if (bottom <= prevBottom) { 105 this.state.clip = { disc: { ...disc }, i }; 106 } 107 prevBottom = bottom; 108 this.state.discs.push(disc); 109 } 110 111 if (this.state.clip.disc) { 112 const clipPath = new Path2D(); 113 const disc = this.state.clip.disc; 114 clipPath.ellipse(disc.x, disc.y, disc.w, disc.h, 0, 0, Math.PI * 2); 115 clipPath.rect(disc.x - disc.w, 0, disc.w * 2, disc.y); 116 this.state.clip.path = clipPath; 117 } 118 } 119 120 setLines() { 121 const { width, height } = this.state.rect; 122 if (width <= 0 || height <= 0) return; 123 124 this.state.lines = []; 125 const linesAngle = (Math.PI * 2) / this.numberOfLinesValue; 126 127 for (let i = 0; i < this.numberOfLinesValue; i++) { 128 this.state.lines.push([]); 129 } 130 131 this.state.discs.forEach((disc) => { 132 for (let i = 0; i < this.numberOfLinesValue; i++) { 133 const angle = i * linesAngle; 134 const p = { 135 x: disc.x + Math.cos(angle) * disc.w, 136 y: disc.y + Math.sin(angle) * disc.h, 137 }; 138 this.state.lines[i].push(p); 139 } 140 }); 141 142 const offCanvas = document.createElement("canvas"); 143 offCanvas.width = Math.max(1, width); 144 offCanvas.height = Math.max(1, height); 145 146 const ctx = offCanvas.getContext("2d"); 147 if (!ctx || !this.state.clip.path) { 148 this.state.linesCanvas = null; 149 return; 150 } 151 152 ctx.clearRect(0, 0, offCanvas.width, offCanvas.height); 153
154 this.state.lines.forEach((line) => { 155 ctx.save(); 156 let lineIsIn = false; 157 line.forEach((p1, j) => { 158 if (j === 0) return; 159 const p0 = line[j - 1]; 160 if ( 161 !lineIsIn && 162 (ctx.isPointInPath(this.state.clip.path, p1.x, p1.y) || ctx.isPointInStroke(this.state.clip.path, p1.x, p1.y)) 163 ) { 164 lineIsIn = true; 165 } else if (lineIsIn) { 166 ctx.clip(this.state.clip.path); 167 } 168 ctx.beginPath(); 169 ctx.moveTo(p0.x, p0.y); 170 ctx.lineTo(p1.x, p1.y); 171 ctx.strokeStyle = this.strokeColorValue; 172 ctx.lineWidth = 2; 173 ctx.stroke(); 174 ctx.closePath(); 175 }); 176 ctx.restore(); 177 }); 178 this.state.linesCanvas = offCanvas; 179 } 180 181 initParticle(start = false) { 182 const sx = (this.state.particleArea.sx || 0) + (this.state.particleArea.sw || 0) * Math.random(); 183 const ex = (this.state.particleArea.ex || 0) + (this.state.particleArea.ew || 0) * Math.random(); 184 const dx = ex - sx; 185 const y = start ? (this.state.particleArea.h || 0) * Math.random() : this.state.particleArea.h || 0; 186 const r = 0.5 + Math.random() * 4; 187 const vy = 0.5 + Math.random(); 188 const [red, green, blue] = this.particleRgbColorValue; 189 190 return { 191 x: sx, 192 sx, 193 dx, 194 y, 195 vy, 196 p: 0, 197 r, 198 c: `rgba(${red}, ${green}, ${blue}, ${Math.random()})`, 199 }; 200 } 201 202 setParticles() { 203 const { width, height } = this.state.rect; 204 this.state.particles = []; 205 const disc = this.state.clip.disc; 206 if (!disc) return; 207 208 this.state.particleArea = { 209 sw: disc.w * 0.5, 210 ew: disc.w * 2, 211 h: height * 0.85, 212 }; 213 this.state.particleArea.sx = (width - (this.state.particleArea.sw || 0)) / 2; 214 this.state.particleArea.ex = (width - (this.state.particleArea.ew || 0)) / 2; 215 216 const totalParticles = 100; 217 for (let i = 0; i < totalParticles; i++) { 218 this.state.particles.push(this.initParticle(true)); 219 } 220 } 221 222 drawDiscs(ctx) { 223 ctx.strokeStyle = this.strokeColorValue; 224 ctx.lineWidth = 2; 225 const outerDisc = this.state.startDisc; 226 ctx.beginPath(); 227 ctx.ellipse(outerDisc.x, outerDisc.y, outerDisc.w, outerDisc.h, 0, 0, Math.PI * 2); 228 ctx.stroke(); 229 ctx.closePath(); 230 231 this.state.discs.forEach((disc, i) => { 232 if (i % 5 !== 0) return; 233 if (disc.w < (this.state.clip.disc?.w || 0) - 5) { 234 ctx.save(); 235 ctx.clip(this.state.clip.path); 236 } 237 ctx.beginPath(); 238 ctx.ellipse(disc.x, disc.y, disc.w, disc.h, 0, 0, Math.PI * 2); 239 ctx.stroke(); 240 ctx.closePath(); 241 if (disc.w < (this.state.clip.disc?.w || 0) - 5) { 242 ctx.restore(); 243 } 244 }); 245 } 246 247 drawLines(ctx) { 248 if (this.state.linesCanvas && this.state.linesCanvas.width > 0 && this.state.linesCanvas.height > 0) { 249 ctx.drawImage(this.state.linesCanvas, 0, 0); 250 } 251 } 252 253 drawParticles(ctx) { 254 if (!this.state.clip.path) return; 255 ctx.save(); 256 ctx.clip(this.state.clip.path); 257 this.state.particles.forEach((particle) => { 258 ctx.fillStyle = particle.c; 259 ctx.beginPath(); 260 ctx.rect(particle.x, particle.y, particle.r, particle.r); 261 ctx.closePath(); 262 ctx.fill(); 263 }); 264 ctx.restore(); 265 } 266 267 moveDiscs() { 268 this.state.discs.forEach((disc) => { 269 disc.p = (disc.p + 0.001) % 1; 270 this.tweenDisc(disc); 271 }); 272 } 273 274 moveParticles() { 275 this.state.particles.forEach((particle, idx) => { 276 particle.p = 1 - particle.y / (this.state.particleArea.h || 1); 277 particle.x = particle.sx + particle.dx * particle.p; 278 particle.y -= particle.vy; 279 if (particle.y < 0) { 280 this.state.particles[idx] = this.initParticle(); 281 } 282 }); 283 } 284 285 startAnimation() { 286 const tick = () => { 287 const ctx = this.ctx; 288 if (!ctx) return; 289 290 ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 291 ctx.save(); 292 ctx.scale(this.state.render.dpi, this.state.render.dpi); 293 294 this.moveDiscs(); 295 this.moveParticles(); 296 this.drawDiscs(ctx); 297 this.drawLines(ctx); 298 this.drawParticles(ctx); 299
300 ctx.restore(); 301 this.animationId = requestAnimationFrame(tick); 302 }; 303 tick(); 304 } 305 306 setupResize() { 307 this.handleResize = this.handleResize.bind(this); 308 window.addEventListener("resize", this.handleResize); 309 } 310 311 handleResize() { 312 this.init(); 313 } 314};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.