PageSourceSearch

https://railsblocks.com/assets/controllers/black_hole_background_c…db93e5f754606ce33dc7f3b0f38bb4ce7806e0f4.js

js railsblocks.com collected 2026-09-25 20:22:51 UTC 8,628 bytes, 314 lines download raw bytes

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.