1// 1. Get references 2const page = document.getElementById('page'); 3const svg = document.getElementById('sine-svg'); 4const wavePath = document.getElementById('sine-path'); 5 6const emilyAnchor = document.getElementById('a-emily'); 7const emilyPhoto = document.getElementById('photo-emily'); 8const lauraAnchor = document.getElementById('a-laura'); 9const lauraPhoto = document.getElementById('photo-laura'); 10const photoYOffset = 100; 11 12// Sine wave params 13const phase = Math.random() * 2 * Math.PI; // random start 14const offsetX = 180; 15const amplitude = 75; 16const wavelength = 1800; 17const maxDotSize = 70; 18const spacing = 400; 19// Extend the path above and below the visible area 20let yMin = 0; 21let yMax = 0; 22 23// We'll track all our dot sets here 24let dotSets = []; 25let canvW = 0; 26let canvH = 0; 27 28// Keep track of path length 29let totalLength = 0; 30 31// 1) Build the vertical sine wave path to fill the screen 32function buildSineWave() { 33 canvW = page.offsetWidth; 34 canvH = page.offsetHeight; 35 // console.log('canvH', canvH); 36 // Adjust the viewBox so the coordinates match window size 37 // svg.setAttribute('viewBox', `0 -${maxDotSize * 0.5} ${width} ${canvH + maxDotSize}`); 38 svg.setAttribute('viewBox', `0 0 ${canvW} ${canvH}`); 39 svg.style.width = `${canvW}px`; 40 svg.style.height = `${canvH}px`; 41 42 yMin = -maxDotSize; // e.g. -25 for a 50px diameter dot 43 yMax = canvH + maxDotSize; // e.g. height + 25 44 45 // Build the path from yMin to yMax 46 let d = `M ${offsetX + amplitude * Math.sin((yMin / wavelength) * 2 * Math.PI + phase)} ${yMin}`; 47 for (let y = yMin + 1; y <= yMax; y++) { 48 const x = offsetX + amplitude * Math.sin((y / wavelength) * 2 * Math.PI + phase); 49 d += ` L ${x} ${y}`; 50 } 51 52 wavePath.setAttribute('d', d); 53 54 let xAbsolute, yAbsolute, yOffset, left = 0; 55 56 // Position the portrait photos 57 if(emilyAnchor && emilyPhoto) { 58 const emilyRect = emilyAnchor.getBoundingClientRect(); 59 const emilyPhotoRect = emilyPhoto.getBoundingClientRect(); 60 xAbsolute = emilyRect.left + window.scrollX; 61 yAbsolute = emilyRect.top + window.scrollY; 62 yOffset = emilyPhotoRect.height * 0.5; 63 left = getXOfSinewave(yAbsolute + yOffset + photoYOffset); 64 emilyPhoto.style.top = `${yAbsolute + yOffset - (emilyPhotoRect.height * 0.5) + photoYOffset}px`; 65 emilyPhoto.style.left = `${left - (emilyPhotoRect.width * 0.5)}px`; 66 emilyPhoto.style.opacity = 1; 67 } 68 69 if(lauraAnchor && lauraPhoto) { 70 const lauraRect = lauraAnchor.getBoundingClientRect(); 71 const lauraPhotoRect = lauraPhoto.getBoundingClientRect(); 72 xAbsolute = lauraRect.left + window.scrollX; 73 yAbsolute = lauraRect.top + window.scrollY; 74 yOffset = lauraPhotoRect.height * 0.5; 75 left = getXOfSinewave(yAbsolute + yOffset + photoYOffset); 76 lauraPhoto.style.top = `${yAbsolute + yOffset - (lauraPhotoRect.height * 0.5) + photoYOffset}px`; 77 lauraPhoto.style.left = `${left - (lauraPhotoRect.width * 0.5)}px`; 78 lauraPhoto.style.opacity = 1; 79 } 80 81 // Measure total length of the path 82 totalLength = wavePath.getTotalLength(); 83} 84 85function getXOfSinewave(y) { 86 return offsetX + amplitude * Math.sin((y / wavelength) * 2 * Math.PI + phase); 87} 88 89 90// 2) Helper to create a single dot (circle) in the SVG 91function createDot(color, diameter) { 92 // create a new circle in the SVG namespace 93 const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); 94 circle.setAttribute('fill', color); 95 circle.setAttribute('r', diameter / 2); // radius = diameter/2 96 circle.setAttribute('class', 'color-dot'); 97 svg.appendChild(circle); 98 return circle; 99} 100 101// 3) Helper to create a triple-dot set 102function createDotSet(options) { 103 // options might look like: 104 // { 105 // dot1Color: 'red', dot1Diameter: 50, 106 // dot2Color: 'blue', dot2Diameter: 33, 107 // dot3Color: 'green', dot3Diameter: 27, 108 // speed: 2, gap12: 30, gap23: 50 109 // } 110 111 const dot1 = createDot(options.dot1Color, options.dot1Diameter); 112 const dot2 = createDot(options.dot2Color, options.dot2Diameter); 113 const dot3 = createDot(options.dot3Color, options.dot3Diameter); 114 115 // We'll store offsets, which we can update each frame 116 // The baseOffset controls the leading dot (dot1) 117 return { 118 dot1, 119 dot2, 120 dot3, 121 speed: options.speed, 122 gap12: options.gap12, 123 gap23: options.gap23, 124 baseOffset: options.baseOffset // initial offset for the first dot 125 }; 126} 127 128// 4) Animation loop 129function animate() { 130 // Reassign dotSets by filtering out those that are âdoneâ 131 dotSets = dotSets.filter(set => { 132 // 1) Increment the leading dotâs offset (no modulo!) 133 set.baseOffset += set.speed; 134 135 // 2) Calculate each dotâs offset 136 const offset1 = set.baseOffset; 137 const offset2 = offset1 - set.gap12; 138 const offset3 = offset2 - set.gap23; 139 140 // 3) Check if the trailing dot is beyond the path length 141 if (offset3 > totalLength) { 142 // This means the last dot is off the path (below the bottom). 143 // Remove the actual <circle> elements from the DOM so they disappear. 144 svg.removeChild(set.dot1); 145 svg.removeChild(set.dot2); 146 svg.removeChild(set.dot3); 147 return false; // Exclude from dotSets 148 } 149 150 // 4) If still on screen, position them as usual 151 if (offset1 >= 0 && offset1 <= totalLength) { 152 const p1 = wavePath.getPointAtLength(offset1); 153 set.dot1.setAttribute('cx', p1.x); 154 set.dot1.setAttribute('cy', p1.y); 155 } else { 156 // If offset is < 0 or > totalLength, the dot is âoff-path,â 157 // so you could optionally hide or not update it. 158 set.dot1.setAttribute('cx', -9999); // move offscreen 159 set.dot1.setAttribute('cy', -9999); 160 } 161 162 if (offset2 >= 0 && offset2 <= totalLength) { 163 const p2 = wavePath.getPointAtLength(offset2); 164 set.dot2.setAttribute('cx', p2.x); 165 set.dot2.setAttribute('cy', p2.y); 166 } else { 167 set.dot2.setAttribute('cx', -9999); 168 set.dot2.setAttribute('cy', -9999); 169 } 170 171 if (offset3 >= 0 && offset3 <= totalLength) {
172 const p3 = wavePath.getPointAtLength(offset3); 173 set.dot3.setAttribute('cx', p3.x); 174 set.dot3.setAttribute('cy', p3.y); 175 } else { 176 set.dot3.setAttribute('cx', -9999); 177 set.dot3.setAttribute('cy', -9999); 178 } 179 180 // 5) Keep this set in the array 181 return true; 182 }); 183 184 // Continue animation 185 // requestAnimationFrame(animate); 186} 187 188const blueSet = { 189 dot1Color: 'rgb(16, 95, 168)', 190 dot1Diameter: maxDotSize, 191 dot2Color: 'rgb(37, 144, 205)', 192 dot2Diameter: 40, 193 dot3Color: 'rgb(114, 186, 231)', 194 dot3Diameter: 25, 195 speed: 2, 196 gap12: 35, 197 gap23: 60, 198 baseOffset: 0 199}; 200 201const yellowSet = { 202 dot1Color: 'rgb(253, 211, 0)', 203 dot1Diameter: maxDotSize, 204 dot2Color: 'rgb(255, 224, 95)', 205 dot2Diameter: 40, 206 dot3Color: 'rgb(255, 234, 146)', 207 dot3Diameter: 25, 208 speed: 2, 209 gap12: 35, 210 gap23: 60, 211 baseOffset: 0 212}; 213 214let whichSet = 0; 215 216// 5) Initialize 217window.addEventListener('load', () => { 218 // Build the sine wave path 219 setTimeout(() => { 220 buildSineWave(); 221 222 const c = canvH / spacing; 223 let baseOffset = 0; 224 225 for(let i = 0; i <= c; i++) { 226 let baseSet = (i % 2 === 0) ? blueSet : yellowSet; 227 228 const dot1Diameter = baseSet.dot1Diameter - (baseSet.dot1Diameter * Math.random() * 0.2); 229 const dot2Diameter = baseSet.dot2Diameter - (baseSet.dot2Diameter * Math.random() * 0.2); 230 const dot3Diameter = baseSet.dot3Diameter - (baseSet.dot3Diameter * Math.random() * 0.2); 231 const gap12 = baseSet.gap12 - (baseSet.gap12 * Math.random() * 0.2); 232 const gap23 = baseSet.gap23 - (baseSet.gap23 * Math.random() * 0.2); 233 baseOffset += spacing + (spacing * (Math.random() + 0.5) * 0.2); 234 235 const nextSet = { 236 ...baseSet, 237 dot1Diameter, 238 dot2Diameter, 239 dot3Diameter, 240 gap12, 241 gap23, 242 baseOffset 243 }; 244 245 dotSets.push(createDotSet(nextSet)); 246 } 247 // Start animation 248 animate(); 249 }, 100); 250 251 // add a new triple-dot set every 3 seconds 252 // setInterval(() => { 253 // if(whichSet == 0) { 254 // dotSets.push(createDotSet(blueSet)); 255 // whichSet = 1; 256 // } else { 257 // dotSets.push(createDotSet(yellowSet)); 258 // whichSet = 0; 259 // } 260 // }, 6000); 261 262}); 263 264// Handle window resizing to rebuild the path 265window.addEventListener('resize', () => { 266 buildSineWave(); 267 // The offsets might shift, but the animation will keep going 268});
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.