PageSourceSearch

https://onemusic-website.vercel.app/script.js

js onemusic-website.vercel.app collected 2026-10-03 07:26:13 UTC 7,304 bytes, 223 lines download raw bytes

1// script.js
2document.addEventListener("DOMContentLoaded", () => {
3  /** @type {HTMLElement | null} */
4  const header = document.querySelector(".header");
5  /** @type {HTMLElement | null} */
6  const typingText = document.getElementById("typing-text");
7
8  // --- Typing effect ---
9  /**
10   * Immutable list of phrases cycled by the typewriter effect.
11   * @constant {readonly string[]}
12   */
13  const phrases = Object.freeze([
14    "Free. Open Source. Built for music lovers.",
15    "No ads. No subscription. No limits.",
16    "Smart autoplay. Clean UI. Instant play."
17  ]);
18
19  /** @type {number} Current phrase index (0..phrases.length-1) */
20  let phraseIndex = 0;
21  /** @type {number} Current character index within the active phrase */
22  let charIndex = 0;
23  /** @type {boolean} Whether the loop is currently in the deleting state */
24  let deleting = false;
25
26  /**
27   * Configuration object for {@link typeLoop}.
28   * All properties are optional; sensible defaults are applied when omitted.
29   *
30   * @typedef {Object} TypeLoopOptions
31   * @property {number} [typingDelay=55]   Delay between typing characters, in milliseconds.
32   * @property {number} [deletingDelay=35] Delay between deleting characters, in milliseconds.
33   * @property {number} [pauseDelay=1200]  Pause after a phrase is fully typed, before deletion begins, in milliseconds.
34   */
35
36  /**
37   * Drives the typing/deleting animation loop for the hero text.
38   *
39   * Cycles through {@link phrases} with a typewriter effect, alternating between
40   * typing and deleting states with configurable delays. The function schedules
41   * itself recursively via `setTimeout`, so it must be invoked once to start
42   * and will continue indefinitely until the page unloads.
43   *
44   * @param {TypeLoopOptions} [options={}] - Animation timing options.
45   * @returns {void}
46   * @throws {Error} If the `#typing-text` element is not found in the DOM or if phrases array is empty.
47   * @example
48   * // Default usage
49   * typeLoop();
50   *
51   * // Custom timing
52   * typeLoop({ typingDelay: 60, deletingDelay: 40, pauseDelay: 1500 });
53   */
54  function typeLoop(options = /** @type {TypeLoopOptions} */ ({})) {
55    const {
56      typingDelay = 55,
57      deletingDelay = 35,
58      pauseDelay = 1200
59    } = /** @type {TypeLoopOptions} */ (options);
60
61    if (!typingText) {
62      throw new Error("typingText element not found");
63    }
64
65    if (phrases.length === 0) {
66      throw new Error("phrases array cannot be empty");
67    }
68
69    const current = phrases[phraseIndex];
70    if (phraseIndex < 0 || phraseIndex >= phrases.length) {
71      phraseIndex = 0;
72    }
73
74    if (charIndex < 0 || charIndex > current.length) {
75      charIndex = deleting ? current.length : 0;
76    }
77
78    if (!deleting) {
79      charIndex++;
80      typingText.textContent = current.slice(0, charIndex);
81
82      if (charIndex === current.length) {
83        deleting = true;
84        setTimeout(typeLoop, pauseDelay, options);
85        return;
86      }
87    } else {
88      charIndex--;
89      typingText.textContent = current.slice(0, charIndex);
90
91      if (charIndex === 0) {
92        deleting = false;
93        phraseIndex = (phraseIndex + 1) % phrases.length;
94      }
95    }
96
97    setTimeout(typeLoop, deleting ? deletingDelay : typingDelay, options);
98  }
99
100  // Start the typing animation with default options.
101  typeLoop();
102
103  // --- Smooth anchor scroll ---
104  document.querySelectorAll('a[href^="#"]').forEach(anchor => {
105    anchor.addEventListener("click", function (e) {
106      e.preventDefault();
107      /** @type {HTMLElement | null} */
108      const target = document.querySelector(this.getAttribute("href"));
109      if (target) target.scrollIntoView({ behavior: "smooth", block: "start" });
110    });
111  });
112
113  // --- Scroll reveal ---
114  /** @type {NodeListOf<Element>} */
115  const revealItems = document.querySelectorAll("section, .card, .contact-card, .screenshots img, .video-box, .hero-logo");
116
117  revealItems.forEach((el) => el.classList.add("reveal"));
118
119  /** @type {IntersectionObserver} */
120  const observer = new IntersectionObserver((entries) => {
121    entries.forEach((entry) => {
122      if (entry.isIntersecting) {
123        entry.target.classList.add("show");
124      }
125    });
126  }, { threshold: 0.15 });
127
128  revealItems.forEach((el) => observer.observe(el));
129
130  // --- Sticky glass nav on scroll ---
131  window.addEventListener("scroll", () => {
132    if (window.scrollY > 20) {
133      header?.classList.add("glass-nav");
134    } else {
135      header?.classList.remove("glass-nav");
136    }
137  });
138
139  /**
140   * Creates and animates a single floating particle element with randomized
141   * visual properties, then removes it from the DOM once its animation
142   * completes.
143   *
144   * Particle characteristics:
145   * - Size: random between 3–8 px (square)
146   * - Horizontal position: random across the viewport width
147   * - Vertical start: 20 px below the viewport bottom
148   * - Animation duration: random between 6–11 s
149   * - Cleanup: removed from the DOM after `duration * 1000` ms
150   *
151   * @returns {void}
152   * @example
153   * // Spawn a single particle
154   * spawnParticle();
155   *
156   * // Spawn particles continuously
157   * setInterval(spawnParticle, 300);
158   */
159  function spawnParticle() {
160    /** @type {HTMLDivElement} */
161    const particle = document.createElement("div");
162    particle.className = "particle";
163
164    /** @type {number} Random size between 3–8 px */
165    const size = Math.random() * 5 + 3;
166    /** @type {number} Random horizontal position in px */
167    const left = Math.random() * window.innerWidth;
168    /** @type {number} Random animation duration between 6–11 s */
169    const duration = Math.random() * 5 + 6;
170
171    particle.style.width = `${size}px`;
172    particle.style.height = `${size}px`;
173    particle.style.left = `${left}px`;
174    particle.style.bottom = `-20px`;
175    particle.style.animationDuration = `${duration}s`;
176
177    document.body.appendChild(particle);
178
179    setTimeout(() => {
180      particle.remove();
181    }, duration * 1000);
182  }
183
184  // Continuously spawn particles.
185  setInterval(spawnParticle, 300);
186
187  // --- Magnetic hover for buttons and cards ---
188  /** @type {NodeListOf<Element>} */
189  const hoverTargets = document.querySelectorAll(".btn, .card, .contact-card");
190
191  hoverTargets.forEach((el) => {
192    el.addEventListener("mousemove", (e) => {
193      const rect = el.getBoundingClientRect();
194      const x = e.clientX - rect.left - rect.width / 2;
195      const y = e.clientY - rect.top - rect.height / 2;
196
197      el.style.transform = `translate(${x * 0.04}px, ${y * 0.04}px) scale(1.02)`;
198    });
199
200    el.addEventListener("mouseleave", () => {
201      el.style.transform = "";
202    });
203  });
204
205  // --- Mouse glow ---
206  /** @type {HTMLDivElement} */
207  const glow = document.createElement("div");
208  glow.style.position = "fixed";
209  glow.style.width = "320px";
210  glow.style.height = "320px";
211  glow.style.borderRadius = "50%";
212  glow.style.pointerEvents = "none";
213  glow.style.zIndex = "0";
214  glow.style.background = "radial-gradient(circle, rgba(255,122,24,0.12) 0%, rgba(255,122,24,0.04) 35%, transparent 70%)";
215  glow.style.transform = "translate(-50%, -50%)";
216  glow.style.filter = "blur(10px)";
217  document.body.appendChild(glow);
218
219  window.addEventListener("mousemove", (e) => {
220    glow.style.left = `${e.clientX}px`;
221    glow.style.top = `${e.clientY}px`;
222  });
223});

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.