(() => { const canvas = document.getElementById('joinProgressCanvas'); if (!canvas) { return; } const context = canvas.getContext('2d'); if (!context) { return; } const animeLib = window.anime; const hasAnime = typeof animeLib === 'function'; const waveTrack = document.getElementById('joinWaveTrack'); const waveFill = document.getElementById('joinWaveFill'); const waveBoat = document.getElementById('joinWaveBoat'); const waveMarks = document.getElementById('joinWaveMarks'); const joinData = window.itstudioJoinData && typeof window.itstudioJoinData === 'object' ? window.itstudioJoinData : {}; const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); let width = 0; let height = 0; let dpr = 1; let rafId = 0; let targetProgress = 0; let displayedProgress = 0; let trackLeft = 0; let trackWidth = 0; let stageMarkers = []; const waveShape1 = { base: 0.16, amp: 7.5, len: 280, speed: 0.9 }; const waveShape2 = { base: 0.44, amp: 6, len: 340, speed: 0.75 }; const waveShape3 = { base: 0.68, amp: 6.5, len: 400, speed: 0.62 }; const palettes = { light: { wave1: 'rgba(198, 227, 255, 0.92)', wave2: 'rgba(142, 193, 236, 0.4)', wave3: 'rgba(84, 151, 214, 0.48)', }, dark: { wave1: 'rgba(132, 190, 242, 0.88)', wave2: 'rgba(88, 151, 214, 0.4)', wave3: 'rgba(56, 118, 183, 0.48)', }, }; function clamp(value, min, max) { return Math.min(max, Math.max(min, value)); } function getTheme() { return document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'; } function getNumber(value) { const numeric = Number(value); return Number.isFinite(numeric) ? numeric : null; } function getNavigationType() { if (window.performance && typeof window.performance.getEntriesByType === 'function') { const entries = window.performance.getEntriesByType('navigation'); if (entries && entries.length > 0 && entries[0].type) { return entries[0].type; } } if (window.performance && window.performance.navigation) { if (window.performance.navigation.type === 1) { return 'reload'; } if (window.performance.navigation.type === 2) { return 'back_forward'; } } return 'navigate'; } function shouldAnimateEntry() { if (prefersReducedMotion.matches) { return false; } return getNavigationType() !== 'reload'; } function resize() { dpr = window.devicePixelRatio || 1; width = canvas.clientWidth; height = canvas.clientHeight; canvas.width = Math.max(1, Math.floor(width * dpr)); canvas.height = Math.max(1, Math.floor(height * dpr)); context.setTransform(dpr, 0, 0, dpr, 0, 0); updateTrackMetrics(); } function updateTrackMetrics() { if (!waveTrack) { trackLeft = 0; trackWidth = width; return; } const canvasRect = canvas.getBoundingClientRect(); const trackRect = waveTrack.getBoundingClientRect(); trackLeft = clamp(trackRect.left - canvasRect.left, 0, width); trackWidth = Number.isFinite(trackRect.width) && trackRect.width > 0 ? trackRect.width : width; } function drawWave(color, yBase, amplitude, wavelength, speed, time) { const overscan = 24; const step = 6; context.beginPath(); context.moveTo(-overscan, height + 2); for (let x = -overscan; x <= width + overscan; x += step) { const theta = (x / wavelength) * Math.PI * 2 + time * speed; const y = yBase + Math.sin(theta) * amplitude; context.lineTo(x, y); } context.lineTo(width + overscan, height + 2); context.lineTo(-overscan, height + 2); context.closePath(); context.fillStyle = color; context.fill(); } function sampleWaveAtX(shape, x, time) { const theta = (x / shape.len) * Math.PI * 2 + time * shape.speed; const y = Math.sin(theta) * shape.amp; const slope = Math.cos(theta) * (Math.PI * 2 / shape.len) * shape.amp; return { y, slope }; } function getWaveXFromProgress(progress) { const safeProgress = clamp(progress, 0, 1); const currentTrackWidth = trackWidth > 0 ? trackWidth : width; return trackLeft + (safeProgress * currentTrackWidth); } function applyFloatingMotion(time) { if (width <= 0) { return; } const boatWave = sampleWaveAtX(waveShape3, getWaveXFromProgress(displayedProgress), time); if (waveBoat) { const boatTilt = clamp(boatWave.slope * 34, -7.2, 7.2); waveBoat.style.setProperty('--boat-wave-offset', `${boatWave.y.toFixed(2)}px`); waveBoat.style.setProperty('--boat-tilt', `${boatTilt.toFixed(2)}deg`); } stageMarkers.forEach((marker) => { if (marker.type === 'lighthouse') { marker.element.style.setProperty('--mark-wave-offset', '0px'); marker.element.style.setProperty('--mark-tilt', '0deg'); return; } const markerWave = sampleWaveAtX(waveShape3, getWaveXFromProgress(marker.progress), time); const markerTilt = clamp(markerWave.slope * 34, -7.2, 7.2); marker.element.style.setProperty('--mark-wave-offset', `${markerWave.y.toFixed(2)}px`); marker.element.style.setProperty('--mark-tilt', `${markerTilt.toFixed(2)}deg`); }); } function renderWaves(timestamp) { const theme = getTheme(); const palette = palettes[theme]; const time = timestamp * 0.001; context.clearRect(0, 0, width, height); drawWave(palette.wave1, height * waveShape1.base, waveShape1.amp, waveShape1.len, waveShape1.speed, time); drawWave(palette.wave2, height * waveShape2.base, waveShape2.amp, waveShape2.len, waveShape2.speed, time); drawWave(palette.wave3, height * waveShape3.base, waveShape3.amp, waveShape3.len, waveShape3.speed, time); applyFloatingMotion(time); rafId = requestAnimationFrame(renderWaves); } function stopWaves() { if (rafId) { cancelAnimationFrame(rafId); rafId = 0; } } function drawStaticWaves() { const theme = getTheme(); const palette = palettes[theme]; context.clearRect(0, 0, width, height); drawWave(palette.wave1, height * waveShape1.base, waveShape1.amp, waveShape1.len, waveShape1.speed, 0); drawWave(palette.wave2, height * waveShape2.base, waveShape2.amp, waveShape2.len, waveShape2.speed, 0); drawWave(palette.wave3, height * waveShape3.base, waveShape3.amp, waveShape3.len, waveShape3.speed, 0); applyFloatingMotion(0); } function startWaves() { stopWaves(); resize(); if (prefersReducedMotion.matches) { drawStaticWaves(); return; } rafId = requestAnimationFrame(renderWaves); } function computeTargetProgress() { const stages = Array.isArray(joinData.stages) ? joinData.stages : []; if (!stages.length) { return 0; } const denominator = stages.length > 1 ? (stages.length - 1) : 1; const currentIndex = getNumber(joinData.currentStageIndex); // 有进行中阶段时,船严格对齐对应浮标节点。 if (currentIndex !== null && currentIndex >= 0) { return clamp(currentIndex / denominator, 0, 1); } // 无进行中阶段时,停在最近已完成阶段节点。 let lastCompletedIndex = -1; for (let i = 0; i < stages.length; i += 1) { if (stages[i] && stages[i].status === 'completed') { lastCompletedIndex = i; } } if (lastCompletedIndex >= 0) { return clamp(lastCompletedIndex / denominator, 0, 1); } return 0; } function getBuoyMarkerSvg() { return ` `; } function getLighthouseMarkerSvg() { return ` `; } function renderStageMarks() { if (!waveMarks) { return; } stageMarkers = []; waveMarks.innerHTML = ''; const stages = Array.isArray(joinData.stages) ? joinData.stages : []; if (!stages.length) { return; } const currentIndex = getNumber(joinData.currentStageIndex); const safeCurrentIndex = currentIndex === null ? -1 : Math.round(currentIndex); const denominator = stages.length > 1 ? (stages.length - 1) : 1; stages.forEach((stage, index) => { const marker = document.createElement('span'); marker.className = 'join-wave-mark'; const progress = denominator > 0 ? (index / denominator) : 0; marker.style.left = `${(progress * 100).toFixed(3)}%`; const isLighthouse = index === stages.length - 1 || (stage && stage.key === 'public_notice'); marker.classList.add(isLighthouse ? 'is-lighthouse' : 'is-buoy'); if (index === safeCurrentIndex) { marker.classList.add('is-active'); } const icon = document.createElement('span'); icon.className = 'join-wave-mark-icon'; icon.innerHTML = isLighthouse ? getLighthouseMarkerSvg() : getBuoyMarkerSvg(); marker.appendChild(icon); const beams = isLighthouse ? Array.from(icon.querySelectorAll('.marker-lh-beam')) : []; waveMarks.appendChild(marker); stageMarkers.push({ element: marker, icon, beams, progress, type: isLighthouse ? 'lighthouse' : 'buoy', }); }); } function stopBoatAnimations() { if (!hasAnime) { return; } const targets = [waveFill, waveBoat]; stageMarkers.forEach((marker) => { if (marker.icon) { targets.push(marker.icon); } if (Array.isArray(marker.beams) && marker.beams.length) { marker.beams.forEach((beam) => targets.push(beam)); } }); animeLib.remove(targets); } function startLighthouseBeacon() { const allBeams = []; const leftBeams = []; const rightBeams = []; stageMarkers.forEach((marker) => { if (marker.type !== 'lighthouse' || !Array.isArray(marker.beams)) { return; } marker.beams.forEach((beam) => { allBeams.push(beam); beam.style.opacity = '0.28'; if (beam.classList.contains('marker-lh-beam-left')) { leftBeams.push(beam); } else if (beam.classList.contains('marker-lh-beam-right')) { rightBeams.push(beam); } }); }); if (!allBeams.length || !hasAnime || prefersReducedMotion.matches) { return; } const duration = 3600; const easing = 'easeInOutSine'; if (rightBeams.length) { animeLib({ targets: rightBeams, opacity: [0.2, 0.86, 0.2], duration, easing, loop: true, }); } if (leftBeams.length) { animeLib({ targets: leftBeams, opacity: [0.2, 0.82, 0.2], duration, delay: duration / 2, easing, loop: true, }); } else if (!rightBeams.length) { animeLib({ targets: allBeams, opacity: [0.2, 0.84, 0.2], duration, easing, loop: true, }); } } function setProgress(value) { const safe = clamp(value, 0, 1); displayedProgress = safe; const percent = `${(safe * 100).toFixed(3)}%`; if (waveFill) { waveFill.style.width = percent; } if (waveBoat) { waveBoat.style.left = percent; } } function animateBoatToTarget() { if (!waveBoat || !waveFill) { return; } stopBoatAnimations(); if (!hasAnime || !shouldAnimateEntry()) { setProgress(targetProgress); startLighthouseBeacon(); return; } const state = { value: 0 }; setProgress(0); animeLib({ targets: state, value: targetProgress, duration: 1750, easing: 'easeOutCubic', update: () => { setProgress(state.value); }, complete: () => { setProgress(targetProgress); startLighthouseBeacon(); }, }); animeLib({ targets: waveFill, opacity: [0.55, 1], duration: 1100, easing: 'easeOutSine', }); const activeIcon = waveMarks ? waveMarks.querySelector('.join-wave-mark.is-active .join-wave-mark-icon') : null; if (activeIcon) { animeLib({ targets: activeIcon, scale: [1, 1.24, 1], opacity: [0.82, 1, 0.92], duration: 850, delay: 980, easing: 'easeOutSine', }); } } function handleMotionPreferenceChange() { targetProgress = computeTargetProgress(); setProgress(targetProgress); stopBoatAnimations(); startWaves(); startLighthouseBeacon(); } const observer = new MutationObserver(() => { if (prefersReducedMotion.matches) { drawStaticWaves(); } }); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); if (typeof prefersReducedMotion.addEventListener === 'function') { prefersReducedMotion.addEventListener('change', handleMotionPreferenceChange); } else if (typeof prefersReducedMotion.addListener === 'function') { prefersReducedMotion.addListener(handleMotionPreferenceChange); } window.addEventListener('resize', () => { startWaves(); setProgress(targetProgress); }); window.addEventListener('orientationchange', () => { startWaves(); setProgress(targetProgress); }); window.addEventListener('beforeunload', () => { stopWaves(); stopBoatAnimations(); }); targetProgress = computeTargetProgress(); renderStageMarks(); startWaves(); animateBoatToTarget(); })();