PageSourceSearch

https://basecamp.com/assets/js/modules/pricing.js?v=1.5.6

js basecamp.com collected 2026-09-24 06:36:50 UTC 4,689 bytes, 179 lines download raw bytes

1function ready() {
2
3  const unlimitedRadio = document.querySelector('#pricing-unlimited');
4  const unlimitedPackage = document.querySelector('.pricing__package--unlimited');
5
6  if(!unlimitedRadio || !unlimitedPackage || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
7
8  const random = (min, max) => min + Math.random() * (max - min);
9
10  const visibleCenter = element => {
11
12    if(!element) return null;
13
14    const rect = element.getBoundingClientRect();
15
16    const left = Math.max(rect.left, 0);
17    const right = Math.min(rect.right, innerWidth);
18    const top = Math.max(rect.top, 0);
19    const bottom = Math.min(rect.bottom, innerHeight);
20
21    if(right <= left || bottom <= top) return null;
22
23    return { x: (left + right) / 2, y: (top + bottom) / 2 };
24
25  };
26
27  const confettiBurst = 90;
28  const confettiLimit = 360;
29  const confettiColors = ['blue', 'green', 'orange', 'red', 'yellow'];
30  const confettiParticles = [];
31  const confettiVisibility = 0.5;
32
33  let confettiContainer = null;
34  let confettiObserver = null;
35  let confettiRunning = false;
36  let confettiLastTime = 0;
37
38  const watchConfetti = () => {
39
40    if(confettiObserver) confettiObserver.disconnect();
41
42    confettiObserver = new IntersectionObserver(entries => {
43
44      const entry = entries[entries.length - 1];
45
46      if(!unlimitedRadio.checked) return;
47
48      if(entry.intersectionRatio < confettiVisibility && entry.intersectionRect.height < innerHeight * confettiVisibility) return;
49
50      confettiObserver.disconnect();
51
52      confettiObserver = null;
53
54      burstConfetti(visibleCenter(unlimitedPackage) || { x: innerWidth / 2, y: innerHeight / 2 });
55
56    }, { threshold: [0, 0.25, 0.5, 0.75, 1] });
57
58    confettiObserver.observe(unlimitedPackage);
59
60  };
61
62  const burstConfetti = confettiOrigin => {
63
64    if(!confettiContainer) {
65
66      confettiContainer = document.createElement('div');
67
68      confettiContainer.className = 'pricing__confetti';
69      confettiContainer.setAttribute('aria-hidden', 'true');
70
71      document.body.appendChild(confettiContainer);
72
73    }
74
75    for(const particle of confettiParticles.splice(0, confettiParticles.length + confettiBurst - confettiLimit)) {
76
77      particle.element.remove();
78
79    }
80
81    for(let i = 0; i < confettiBurst; i++) {
82
83      const width = random(2, 10);
84      const height = random(2, 4);
85      const dx = random(-0.6, 0.6);
86      const dy = random(-1, -0.3);
87      const length = Math.hypot(dx, dy);
88      const color = confettiColors[Math.floor(Math.random() * confettiColors.length)];
89
90      const particleElement = document.createElement('i');
91
92      particleElement.style.cssText = `width:${width}px;height:${height}px;background:var(--color-${color});border-radius:${Math.random() > 0.5 ? '50%' : '1px'}`;
93
94      confettiContainer.appendChild(particleElement);
95
96      confettiParticles.push({
97        element: particleElement, width, height,
98        x: confettiOrigin.x,
99        y: confettiOrigin.y,
100        vx: dx / length * random(150, 750),
101        vy: dy / length * random(300, 800),
102        rotation: random(0, 360),
103        rotationSpeed: random(-250, 250)
104      });
105
106    }
107
108    if(!confettiRunning) {
109
110      confettiRunning = true;
111      confettiLastTime = 0;
112
113      requestAnimationFrame(confettiLoop);
114
115    }
116
117  };
118
119  unlimitedRadio.addEventListener('change', () => {
120
121    if(!unlimitedRadio.checked) return;
122
123    watchConfetti();
124
125  });
126
127  if(unlimitedRadio.checked) unlimitedRadio.dispatchEvent(new Event('change'));
128
129  function confettiLoop(time) {
130
131    const delta = confettiLastTime ? Math.min((time - confettiLastTime) / 1000, 0.064) : 0;
132    const damping = 0.98 ** (delta * 60);
133    const viewportFloor = innerHeight;
134
135    confettiLastTime = time;
136
137    for(let i = confettiParticles.length - 1; i >= 0; i--) {
138
139      const particle = confettiParticles[i];
140
141      particle.vx = (particle.vx + random(-350, 350) * delta) * damping;
142      particle.vy = (particle.vy + 180 * particle.height * delta) * damping;
143      particle.x += particle.vx * delta;
144      particle.y += particle.vy * delta;
145      particle.rotation += particle.rotationSpeed * delta;
146
147      if(particle.vy > 0 && particle.y > viewportFloor + Math.max(particle.width, particle.height) * 2) {
148
149        particle.element.remove();
150
151        confettiParticles.splice(i, 1);
152
153      } else {
154
155        particle.element.style.transform = `translate3d(${particle.x}px, ${particle.y}px, 0) rotate(${particle.rotation}deg)`;
156
157      }
158
159    }
160
161    if(confettiParticles.length) {
162
163      requestAnimationFrame(confettiLoop);
164
165    } else {
166
167      confettiRunning = false;
168
169      confettiContainer.remove();
170
171      confettiContainer = null;
172
173    }
174
175  }
176
177}
178
179export { ready };

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.