(() => { const LOGO_SRC = "data:image/png;base64,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"; const canvas = document.getElementById('dbcLogoParticleCanvas'); if (!canvas) return; const stage = canvas.closest('.dbc-particle-stage'); if (!stage) return; const hero = canvas.closest('.dbc-particle-hero') || stage; const ctx = canvas.getContext('2d', { alpha: true }); const prefersReducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; const isCompactViewport = window.innerWidth <= 760; const isMediumViewport = window.innerWidth <= 1080; const supportsFinePointer = window.matchMedia ? window.matchMedia('(hover: hover) and (pointer: fine)').matches : true; const config = { alphaThreshold: 80, // 只采样不透明区域 desktopGap: 5, // 数字越小粒子越密 mobileGap: 5, maxParticles: isCompactViewport ? 2600 : (isMediumViewport ? 3400 : 4000), spring: isCompactViewport ? 0.038 : 0.034, friction: isCompactViewport ? 0.835 : 0.845, scatterSpring: 0.012, scatterFriction: 0.91, repulseRadius: 92, repulseForce: 1.85, idleWave: isCompactViewport ? 0.38 : 0.75, particleSize: isCompactViewport ? 1.48 : 1.35, dprLimit: isCompactViewport ? 1.75 : 2, desktopYOffset: -155, mobileYOffset: -118, dbcHold: isCompactViewport ? 1150 : 1500, dbcBurstHold: isCompactViewport ? 220 : 250, dbcBurstShakeDuration: 170, dbcBurstDesktopDistance: 28, dbcBurstMobileDistance: 15, dbcBurstDesktopKick: 0.62, dbcBurstMobileKick: 0.4, logoColorTransitionDuration: 720, logoColorTransitionDelay: 0.58, cycleHold: isCompactViewport ? 800 : 1050, scatterHold: isCompactViewport ? 320 : 420, transitionOutwardForce: 0.001, transitionShakeDuration: 460, transitionShakeForce: 0.026, transitionShakePulse: 0.03, desktopCenterX: 0.5, mobileCenterX: 0.5, dbcText: 'DBC', dbcFontWeight: 900, dbcDesktopFontScale: 0.18, dbcMobileFontScale: 0.29, dbcPalette: ['#004A96', '#0D5B9C', '#1D6FB8'], biotenText: 'BIOTEN', biotenBioText: 'BIO', biotenFontFamily: '"Neue Helvetica", "Helvetica Neue", Helvetica, 方正黑体简体, FZHei-B01S, FZHei, PingFang SC, Microsoft YaHei, Arial, sans-serif', biotenFontWeight: 900, biotenDesktopFontScale: 0.145, biotenMobileFontScale: 0.235, biotenBioGreen: '#00793A', biotenTenPalette: ['#004A96', '#0D5B9C', '#00793A', '#0A8F67'], biotenLetterStagger: isCompactViewport ? 92 : 116, biotenLetterSweep: isCompactViewport ? 46 : 58, biotenLetterJitter: isCompactViewport ? 28 : 36, captionRevealMinDelay: isCompactViewport ? 650 : 800, captionRevealMaxDelay: isCompactViewport ? 950 : 1300, heroProductRevealDelay: isCompactViewport ? 200 : 230, appRevealMinDelay: 1200, appRevealMaxDelay: 2200, appGroupStagger: isCompactViewport ? 140 : 170, appParticleScatter: isCompactViewport ? 120 : 180, applicationDesktopGap: 4, applicationMobileGap: 3, appParticleSize: isCompactViewport ? 1.46 : 1.22, appParticleJitter: isCompactViewport ? 0.2 : 0.26, appIdleWave: isCompactViewport ? 0.18 : 0.24 }; let dpr = Math.min(window.devicePixelRatio || 1, config.dprLimit); let w = 0, h = 0; let particles = []; let targets = []; let currentMode = 'dbc'; hero.dataset.particleMode = currentMode; let sequenceStarted = false; let sequenceTimers = []; let transitionShakeStart = 0; let transitionShakeUntil = 0; let captionVisible = false; let heroProductRevealTimer = null; let captionReadyAt = 0; let captionForceAt = 0; let appParticles = []; let appTargets = []; let appVisible = false; let appReadyAt = 0; let appForceAt = 0; let initialized = false; let mouse = { x: -9999, y: -9999, active: false }; let rafId = null; let tick = 0; let backgroundGradient = null; let targetCache = { dbc: null, logo: null, bioten: null, applications: null }; let warmupQueued = false; let warmupTimerId = 0; let warmupIdleCallbackId = 0; let heroVisibilityObserver = null; let resizeObserver = null; let isHeroInView = true; let isPageActive = true; let isNavigationOpen = false; const img = new Image(); if (!prefersReducedMotion) img.src = LOGO_SRC; const biotenCaption = document.createElement('div'); biotenCaption.className = 'dbc-bioten-particle-caption'; const isEnglish = document.documentElement.dataset.lang === 'en'; const captionBrand = document.createElement('span'); captionBrand.className = 'dbc-bioten-caption-brand'; captionBrand.textContent = isEnglish ? 'DBC · We Innovate Future Applications' : '世界杯竞猜官网 · 创新未来应用'; const captionText = document.createElement('span'); captionText.className = 'dbc-bioten-caption-text'; captionText.textContent = isEnglish ? 'BIOTEN · Fiber-Based Food Packaging Barrier System' : '博碳 · 纸基食品包装阻隔工程系统'; biotenCaption.append(captionBrand, captionText); stage.appendChild(biotenCaption); if (prefersReducedMotion) { canvas.hidden = true; canvas.setAttribute('aria-hidden', 'true'); stage.classList.add('is-bioten-particle-ready'); hero.classList.add('is-bioten-product-ready', 'is-reduced-motion'); return; } function parseParticleColor(value) { if (typeof value !== 'string') return [0, 74, 150, 1]; const hex = value.trim().match(/^#([\da-f]{6})$/i); if (hex) { return [ parseInt(hex[1].slice(0, 2), 16), parseInt(hex[1].slice(2, 4), 16), parseInt(hex[1].slice(4, 6), 16), 1 ]; } const rgb = value.trim().match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i); if (!rgb) return [0, 74, 150, 1]; return [ Number(rgb[1]), Number(rgb[2]), Number(rgb[3]), rgb[4] === undefined ? 1 : Number(rgb[4]) ]; } function mixParticleColor(from, to, progress) { const eased = progress * progress * (3 - 2 * progress); const r = Math.round(from[0] + (to[0] - from[0]) * eased); const g = Math.round(from[1] + (to[1] - from[1]) * eased); const b = Math.round(from[2] + (to[2] - from[2]) * eased); const a = from[3] + (to[3] - from[3]) * eased; return `rgba(${r},${g},${b},${a.toFixed(3)})`; } class Particle { constructor(target) { this.x = Math.random() * w; this.y = Math.random() * h; this.vx = (Math.random() - 0.5) * 8; this.vy = (Math.random() - 0.5) * 8; this.tx = target.x; this.ty = target.y; this.color = target.color; this.colorFrom = parseParticleColor(target.color); this.colorTo = this.colorFrom; this.colorTransitionStart = 0; this.colorTransitionDuration = 0; this.colorTransitionDelay = 0; this.size = target.size; this.seed = Math.random() * Math.PI * 2; this.hasHalo = isCompactViewport && Math.random() < 0.26; this.targetActiveAt = 0; } setTarget(target, activeAt = 0, options = {}) { this.tx = target.x; this.ty = target.y; this.size = target.size; this.targetActiveAt = activeAt; if (options.colorTransitionDuration > 0) { this.colorFrom = parseParticleColor(this.color); this.colorTo = parseParticleColor(target.color); this.colorTransitionStart = options.colorTransitionStart || performance.now(); this.colorTransitionDuration = options.colorTransitionDuration; this.colorTransitionDelay = clamp(options.colorTransitionDelay || 0, 0, 0.9); } else { this.color = target.color; this.colorFrom = parseParticleColor(target.color); this.colorTo = this.colorFrom; this.colorTransitionStart = 0; this.colorTransitionDuration = 0; this.colorTransitionDelay = 0; } } updateColor(now) { if (!this.colorTransitionDuration) return; const rawProgress = clamp( (now - this.colorTransitionStart) / this.colorTransitionDuration, 0, 1 ); const visibleProgress = clamp( (rawProgress - this.colorTransitionDelay) / Math.max(0.001, 1 - this.colorTransitionDelay), 0, 1 ); this.color = mixParticleColor(this.colorFrom, this.colorTo, visibleProgress); if (rawProgress >= 1) this.colorTransitionDuration = 0; } update(now) { this.updateColor(now); if (now < this.targetActiveAt) { this.vx += Math.sin(tick * 0.017 + this.seed) * 0.018; this.vy += Math.cos(tick * 0.015 + this.seed) * 0.018; if (mouse.active) { const mx = this.x - mouse.x; const my = this.y - mouse.y; const dist2 = mx * mx + my * my; const radius = config.repulseRadius; if (dist2 < radius * radius) { const dist = Math.sqrt(dist2) || 0.001; const force = (1 - dist / radius) * config.repulseForce; this.vx += (mx / dist) * force; this.vy += (my / dist) * force; } } this.vx *= 0.94; this.vy *= 0.94; this.x += this.vx; this.y += this.vy; return; } const wave = Math.sin(tick * 0.018 + this.seed) * config.idleWave; const dx = (this.tx + wave) - this.x; const dy = (this.ty - wave) - this.y; const spring = currentMode === 'scatter' ? config.scatterSpring : config.spring; const friction = currentMode === 'scatter' ? config.scatterFriction : config.friction; this.vx += dx * spring; this.vy += dy * spring; if (currentMode === 'scatter') { const cx = visualCenterX(); const cy = h / 2 + particleYOffset(); const sx = this.x - cx; const sy = this.y - cy; const sd = Math.sqrt(sx * sx + sy * sy) || 0.001; this.vx += (sx / sd) * config.transitionOutwardForce; this.vy += (sy / sd) * config.transitionOutwardForce; } applyTransitionShake(this, now); if (mouse.active) { const mx = this.x - mouse.x; const my = this.y - mouse.y; const dist2 = mx * mx + my * my; const radius = config.repulseRadius; if (dist2 < radius * radius) { const dist = Math.sqrt(dist2) || 0.001; const force = (1 - dist / radius) * config.repulseForce; this.vx += (mx / dist) * force; this.vy += (my / dist) * force; } } this.vx *= friction; this.vy *= friction; this.x += this.vx; this.y += this.vy; } draw() { const previousAlpha = ctx.globalAlpha; if (this.hasHalo && currentMode !== 'scatter') { ctx.globalAlpha = previousAlpha * 0.13; ctx.beginPath(); ctx.fillStyle = this.color; ctx.arc(this.x, this.y, this.size * 2.05, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = previousAlpha; } ctx.beginPath(); ctx.fillStyle = this.color; ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fill(); } } class AppParticle { constructor(target, source) { this.sx = source.x + rand(-10, 10); this.sy = source.y + rand(-8, 8); this.x = this.sx; this.y = this.sy; this.vx = (Math.random() - 0.5) * 1.8; this.vy = (Math.random() - 0.5) * 1.8; this.tx = target.x; this.ty = target.y; this.color = target.color; this.size = target.size; this.groupIndex = target.groupIndex || 0; this.seed = Math.random() * Math.PI * 2; this.hasHalo = w <= 520 && Math.random() < 0.06; this.createdAt = performance.now(); this.launchDelay = this.groupIndex * config.appGroupStagger + Math.random() * config.appParticleScatter; this.alpha = 0; } setTarget(target) { this.tx = target.x; this.ty = target.y; this.color = target.color; this.size = target.size; this.groupIndex = target.groupIndex || 0; } update(now) { const age = now - this.createdAt; const launchAge = age - this.launchDelay; if (launchAge < 0) { const preGlow = Math.max(0, 1 - Math.abs(launchAge) / 360); this.x = this.sx + Math.sin(tick * 0.025 + this.seed) * 2.4; this.y = this.sy + Math.cos(tick * 0.024 + this.seed) * 2.4; this.alpha = Math.max(this.alpha, preGlow * 0.18); return; } const waveStrength = w <= 760 ? 0.18 : config.appIdleWave; const wave = Math.sin(tick * 0.015 + this.seed) * waveStrength; const dx = (this.tx + wave) - this.x; const dy = (this.ty - wave) - this.y; const travelEase = Math.min(1, launchAge / 820); this.vx += dx * (0.018 + travelEase * 0.014); this.vy += dy * (0.018 + travelEase * 0.014); if (mouse.active) { const mx = this.x - mouse.x; const my = this.y - mouse.y; const dist2 = mx * mx + my * my; const radius = config.repulseRadius * 0.78; if (dist2 < radius * radius) { const dist = Math.sqrt(dist2) || 0.001; const force = (1 - dist / radius) * (config.repulseForce * 0.82); this.vx += (mx / dist) * force; this.vy += (my / dist) * force; } } this.vx *= 0.89; this.vy *= 0.89; this.x += this.vx; this.y += this.vy; this.alpha = Math.min(1, this.alpha + 0.018 + travelEase * 0.008); } draw() { if (this.alpha <= 0) return; const previousAlpha = ctx.globalAlpha; if (this.hasHalo) { ctx.globalAlpha = Math.max(0, Math.min(1, this.alpha * 0.1)); ctx.beginPath(); ctx.fillStyle = this.color; ctx.arc(this.x, this.y, this.size * 1.65, 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = Math.max(0, Math.min(1, this.alpha)); ctx.beginPath(); ctx.fillStyle = this.color; ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = previousAlpha; } } function resizeCanvas() { if (!isPageActive) return; const rect = stage.getBoundingClientRect(); const nextDpr = Math.min(window.devicePixelRatio || 1, config.dprLimit); const nextW = Math.max(320, Math.floor(rect.width)); const nextH = Math.max(300, Math.floor(rect.height)); const layoutChanged = nextW !== w || nextH !== h; const pixelRatioChanged = nextDpr !== dpr; dpr = nextDpr; w = nextW; h = nextH; canvas.width = Math.floor(w * dpr); canvas.height = Math.floor(h * dpr); canvas.style.width = w + 'px'; canvas.style.height = h + 'px'; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); if (layoutChanged) clearTargetCache(); if (layoutChanged || pixelRatioChanged || !backgroundGradient) { backgroundGradient = createBackgroundGradient(); } rebuildCurrentTargets(); if (appVisible) setAppTargets(getApplicationTargets(), false); scheduleTargetWarmup(); } function createBackgroundGradient() { const centerX = visualCenterX(); const gradient = ctx.createRadialGradient(centerX, h * 0.52, 20, centerX, h * 0.52, Math.max(w, h) * 0.55); gradient.addColorStop(0, 'rgba(0, 121, 58, 0.08)'); gradient.addColorStop(1, 'rgba(255, 255, 255, 0)'); return gradient; } function cancelTargetWarmup() { if (warmupTimerId) { window.clearTimeout(warmupTimerId); warmupTimerId = 0; } if (warmupIdleCallbackId && 'cancelIdleCallback' in window) { window.cancelIdleCallback(warmupIdleCallbackId); warmupIdleCallbackId = 0; } warmupQueued = false; } function clearTargetCache() { targetCache = { dbc: null, logo: null, bioten: null, applications: null }; backgroundGradient = null; cancelTargetWarmup(); } function getDbcTargets() { if (!targetCache.dbc) targetCache.dbc = buildDbcTargets(); return targetCache.dbc; } function getLogoTargets() { if (!targetCache.logo) targetCache.logo = buildLogoTargets(); return targetCache.logo; } function getBiotenTargets() { if (!targetCache.bioten) targetCache.bioten = buildBiotenTargets(); return targetCache.bioten; } function getApplicationTargets() { if (!targetCache.applications) targetCache.applications = buildApplicationTargets(); return targetCache.applications; } function scheduleTargetWarmup() { if (!isPageActive || warmupQueued || !img.complete || !img.naturalWidth || !w || !h) return; warmupQueued = true; const warmTargets = () => { warmupTimerId = 0; warmupIdleCallbackId = 0; warmupQueued = false; if (!isPageActive || !img.complete || !img.naturalWidth || !w || !h) return; getBiotenTargets(); getApplicationTargets(); }; if ('requestIdleCallback' in window) { warmupIdleCallbackId = window.requestIdleCallback(warmTargets, { timeout: 1400 }); } else { warmupTimerId = window.setTimeout(warmTargets, 120); } } function particleYOffset() { if (isShortDesktopViewport()) { return -Math.min(96, h * 0.18); } return w <= 760 ? config.mobileYOffset : config.desktopYOffset; } function isExtremeLandscapeViewport() { return window.innerWidth > window.innerHeight && window.innerHeight <= 500; } function isShortDesktopViewport() { return w > 760 && !isExtremeLandscapeViewport() && window.innerHeight <= 720; } function visualCenterX() { return w * (w <= 760 ? config.mobileCenterX : config.desktopCenterX); } function buildDbcTargets() { const off = document.createElement('canvas'); const octx = off.getContext('2d', { willReadFrequently: true }); const isMobile = w <= 760; const fontSize = Math.min( w * (isMobile ? config.dbcMobileFontScale : config.dbcDesktopFontScale), h * (isMobile ? 0.28 : 0.34) ); const gap = w < 520 ? config.mobileGap : config.desktopGap; const centerX = visualCenterX(); const centerY = h / 2 + particleYOffset(); off.width = w; off.height = h; octx.clearRect(0, 0, w, h); octx.textAlign = 'center'; octx.textBaseline = 'middle'; octx.font = `${config.dbcFontWeight} ${fontSize}px ${config.biotenFontFamily}`; octx.fillStyle = '#111'; octx.fillText(config.dbcText, centerX, centerY); const rawTextWidth = octx.measureText(config.dbcText).width; const textWidth = Math.min(w, Math.ceil(rawTextWidth + 48)); const scanX = Math.max(0, Math.floor(centerX - textWidth / 2)); const scanY = Math.max(0, Math.floor(centerY - fontSize * 0.58)); const scanW = Math.min(w - scanX, textWidth); const scanH = Math.min(h - scanY, Math.ceil(fontSize * 1.16)); const data = octx.getImageData(scanX, scanY, scanW, scanH).data; const textLeft = centerX - rawTextWidth / 2; const letterBounds = Array.from(config.dbcText).map((letter, index) => { const prefix = config.dbcText.slice(0, index); const left = textLeft + octx.measureText(prefix).width; return { index, right: left + octx.measureText(letter).width }; }); const nextTargets = []; for (let y = 0; y < scanH; y += gap) { for (let x = 0; x < scanW; x += gap) { const a = data[(y * scanW + x) * 4 + 3]; if (a <= config.alphaThreshold) continue; const px = scanX + x; const letter = letterBounds.find((item) => px <= item.right + 1) || letterBounds[letterBounds.length - 1]; nextTargets.push({ x: px, y: scanY + y, color: config.dbcPalette[Math.min(letter.index, config.dbcPalette.length - 1)], size: config.particleSize + 0.15 + Math.random() * 0.62 }); } } return nextTargets; } function buildLogoTargets() { const off = document.createElement('canvas'); const octx = off.getContext('2d', { willReadFrequently: true }); const logoScale = Math.min(w * 0.56 / img.naturalWidth, h * 0.56 / img.naturalHeight); const logoW = Math.floor(img.naturalWidth * logoScale); const logoH = Math.floor(img.naturalHeight * logoScale); off.width = logoW; off.height = logoH; octx.clearRect(0, 0, logoW, logoH); octx.drawImage(img, 0, 0, logoW, logoH); const data = octx.getImageData(0, 0, logoW, logoH).data; const gap = w < 520 ? config.mobileGap : config.desktopGap; const nextTargets = []; const offsetX = clamp(visualCenterX() - logoW / 2, 0, Math.max(0, w - logoW)); const offsetY = (h - logoH) / 2 + particleYOffset(); for (let y = 0; y < logoH; y += gap) { for (let x = 0; x < logoW; x += gap) { const i = (y * logoW + x) * 4; const a = data[i + 3]; if (a > config.alphaThreshold) { const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; nextTargets.push({ x: offsetX + x, y: offsetY + y, color: `rgba(${r},${g},${b},${Math.min(1, a / 255)})`, size: config.particleSize + Math.random() * 0.65 }); } } } return nextTargets; } function buildBiotenTargets() { const off = document.createElement('canvas'); const octx = off.getContext('2d', { willReadFrequently: true }); const isMobile = w <= 760; const fontSize = Math.min( w * (isMobile ? config.biotenMobileFontScale : config.biotenDesktopFontScale), h * (isMobile ? 0.26 : 0.31) ); const gap = w < 520 ? config.mobileGap : config.desktopGap; off.width = w; off.height = h; octx.clearRect(0, 0, w, h); octx.textAlign = 'center'; octx.textBaseline = 'middle'; octx.font = `${config.biotenFontWeight} ${fontSize}px ${config.biotenFontFamily}`; octx.fillStyle = '#111'; const textCenterX = visualCenterX(); const textCenterY = h / 2 + particleYOffset(); octx.fillText(config.biotenText, textCenterX, textCenterY + fontSize * 0.04); const metrics = octx.measureText(config.biotenText); const bioWidth = octx.measureText(config.biotenBioText).width; const rawTextWidth = metrics.width; const textWidth = Math.min(w, Math.ceil(rawTextWidth + 48)); const scanX = Math.max(0, Math.floor(textCenterX - textWidth / 2)); const scanY = Math.max(0, Math.floor(textCenterY - fontSize * 0.58)); const scanW = Math.min(w - scanX, textWidth); const scanH = Math.min(h - scanY, Math.ceil(fontSize * 1.16)); const data = octx.getImageData(scanX, scanY, scanW, scanH).data; const nextTargets = []; const textLeft = textCenterX - rawTextWidth / 2; const bioEnd = textLeft + bioWidth; const letters = Array.from(config.biotenText); const letterBounds = letters.map((letter, index) => { const prefix = config.biotenText.slice(0, index); const left = textLeft + octx.measureText(prefix).width; const right = left + octx.measureText(letter).width; return { index, left, right }; }); function letterMetaForX(px) { const fallback = letterBounds[letterBounds.length - 1] || { index: 0, left: textLeft, right: textLeft + rawTextWidth }; const bound = letterBounds.find((item) => px <= item.right + 1) || fallback; const width = Math.max(1, bound.right - bound.left); return { letterIndex: bound.index, letterProgress: clamp((px - bound.left) / width, 0, 1) }; } for (let y = 0; y < scanH; y += gap) { for (let x = 0; x < scanW; x += gap) { const a = data[(y * scanW + x) * 4 + 3]; if (a > config.alphaThreshold) { const px = scanX + x; const py = scanY + y; const letterMeta = letterMetaForX(px); const color = px <= bioEnd ? config.biotenBioGreen : config.biotenTenPalette[Math.floor(Math.random() * config.biotenTenPalette.length)]; nextTargets.push({ x: px, y: py, letterIndex: letterMeta.letterIndex, letterProgress: letterMeta.letterProgress, color, size: config.particleSize + 0.15 + Math.random() * 0.62 }); } } } return nextTargets; } function buildScatterTargets(count) { const nextTargets = []; const cx = visualCenterX(); const cy = h / 2 + particleYOffset(); const total = Math.max(1, count || particles.length || targets.length || 1); const goldenAngle = Math.PI * (3 - Math.sqrt(5)); const palette = [ 'rgba(0,121,58,.84)', 'rgba(0,74,150,.78)', 'rgba(10,143,103,.74)', 'rgba(29,111,184,.72)' ]; for (let i = 0; i < total; i++) { const progress = (i + 0.5) / total; const angle = i * goldenAngle + rand(-0.055, 0.055); const cos = Math.cos(angle); const sin = Math.sin(angle); const edgeX = cos > 0 ? (w - cx) / Math.max(0.001, cos) : -cx / Math.min(-0.001, cos); const edgeY = sin > 0 ? (h - cy) / Math.max(0.001, sin) : -cy / Math.min(-0.001, sin); const edgeDistance = Math.min(Math.abs(edgeX), Math.abs(edgeY)); const radialSeed = (i % 89) / 88; const depth = 0.08 + Math.pow(radialSeed, 0.6) * 0.8 + rand(-0.025, 0.04); const distance = edgeDistance * depth + rand(-12, 22); const color = palette[(i + Math.floor(progress * 11)) % palette.length]; nextTargets.push({ x: cx + cos * distance, y: cy + sin * distance, color, size: config.particleSize + 0.15 + Math.random() * 0.9 }); } return nextTargets; } function buildDbcBurstTargets() { const cx = visualCenterX(); const cy = h / 2 + particleYOffset(); const isMobile = w <= 760; const baseDistance = isMobile ? config.dbcBurstMobileDistance : config.dbcBurstDesktopDistance; return particles.map((particle, index) => { const sourceX = Number.isFinite(particle.tx) ? particle.tx : particle.x; const sourceY = Number.isFinite(particle.ty) ? particle.ty : particle.y; const dx = sourceX - cx; const dy = sourceY - cy; const distance = Math.sqrt(dx * dx + dy * dy) || 0.001; const nx = dx / distance; const ny = dy / distance; const tangentX = -ny; const tangentY = nx; const expansion = baseDistance * (0.72 + ((index * 37) % 29) / 58 + rand(-0.08, 0.08)); const tangentShift = rand(-2.8, 2.8) * (isMobile ? 0.62 : 1); return { x: sourceX + nx * expansion + tangentX * tangentShift, y: sourceY + ny * expansion + tangentY * tangentShift, color: config.dbcPalette[index % config.dbcPalette.length], size: particle.size + 0.08 + Math.random() * 0.24 }; }); } function kickDbcBurstParticles() { const cx = visualCenterX(); const cy = h / 2 + particleYOffset(); const kick = w <= 760 ? config.dbcBurstMobileKick : config.dbcBurstDesktopKick; particles.forEach((particle) => { const dx = particle.x - cx; const dy = particle.y - cy; const distance = Math.sqrt(dx * dx + dy * dy) || 0.001; const nx = dx / distance; const ny = dy / distance; const tangent = rand(-0.09, 0.09); particle.vx += nx * kick - ny * tangent + rand(-0.04, 0.04); particle.vy += ny * kick + nx * tangent + rand(-0.04, 0.04); }); } function kickScatterParticles() { const cx = visualCenterX(); const cy = h / 2 + particleYOffset(); particles.forEach((p) => { const dx = p.tx - p.x; const dy = p.ty - p.y; const td = Math.sqrt(dx * dx + dy * dy) || 0.001; const sx = p.x - cx; const sy = p.y - cy; const sd = Math.sqrt(sx * sx + sy * sy) || 0.001; p.vx += dx * 0.0035 + (dx / td) * 0.44 + (sx / sd) * 0.14 + rand(-0.15, 0.15); p.vy += dy * 0.0035 + (dy / td) * 0.44 + (sy / sd) * 0.14 + rand(-0.15, 0.15); }); } function beginTransitionShake(now, duration = config.transitionShakeDuration) { transitionShakeStart = now; transitionShakeUntil = now + duration; } function applyTransitionShake(particle, now) { if (now >= transitionShakeUntil || currentMode === 'logo') return; const duration = Math.max(1, transitionShakeUntil - transitionShakeStart); const progress = clamp((now - transitionShakeStart) / duration, 0, 1); const envelope = Math.sin(progress * Math.PI); const microPulse = 0.68 + Math.sin(tick * 0.72 + particle.seed * 2.1) * config.transitionShakePulse; const force = config.transitionShakeForce * envelope * microPulse; const angleA = particle.seed + tick * 0.38; const angleB = particle.seed * 1.7 - tick * 0.52; particle.vx += Math.cos(angleA) * force + Math.cos(angleB) * force * 0.36; particle.vy += Math.sin(angleA) * force + Math.sin(angleB) * force * 0.36; } function applicationIconLayout() { // 极矮横屏优先保护品牌主动画与完整主张,应用图标作为增强信息不强行挤入。 if (isExtremeLandscapeViewport()) return []; const isMobile = w <= 760; const isPhone = w <= 520; const fontSize = Math.min( w * (isMobile ? config.biotenMobileFontScale : config.biotenDesktopFontScale), h * (isMobile ? 0.26 : 0.31) ); const textCenterY = h / 2 + particleYOffset(); const iconSize = isPhone ? clamp(w * 0.132, 46, 52) : isMobile ? clamp(w * 0.082, 56, 64) : clamp(w * 0.061, 64, 88); let iconY = textCenterY - fontSize * (isPhone ? 0.79 : 0.72); if (isShortDesktopViewport()) { const biotenTop = textCenterY - fontSize * 0.52; const desiredY = biotenTop - iconSize * 0.52 - 10; const safeTop = iconSize * 0.55 + 12; iconY = Math.max(safeTop, desiredY); } const mobileProgress = clamp((w - 520) / 240, 0, 1); const mobileSpread = 0.38 - mobileProgress * 0.08; const xs = isMobile ? [ 0.5 - mobileSpread, 0.5 - mobileSpread / 2, 0.5, 0.5 + mobileSpread / 2, 0.5 + mobileSpread ] : [0.2, 0.35, 0.5, 0.65, 0.8]; return [ { type: 'cup', x: w * xs[0], y: iconY, size: iconSize }, { type: 'carton', x: w * xs[1], y: iconY + (isMobile ? 1 : 3), size: iconSize }, { type: 'coatedPaper', x: w * xs[2], y: iconY + (isMobile ? 2 : 4), size: iconSize }, { type: 'bag', x: w * xs[3], y: iconY, size: iconSize }, { type: 'serviceware', x: w * xs[4], y: iconY, size: iconSize } ]; } function drawApplicationIcon(tctx, icon) { const { type, x, y, size } = icon; const s = size; const baseInk = '#000000'; const accentInk = '#00ff66'; tctx.save(); tctx.strokeStyle = baseInk; tctx.fillStyle = baseInk; tctx.lineWidth = Math.max(3.8, s * 0.072); tctx.lineCap = 'round'; tctx.lineJoin = 'round'; const accentStroke = (draw) => { const previousStroke = tctx.strokeStyle; tctx.strokeStyle = accentInk; draw(); tctx.strokeStyle = previousStroke; }; if (type === 'cup') { // Paper cup: a clear oval rim, tapered wall and bottom seam. tctx.beginPath(); tctx.ellipse(x, y - s * 0.35, s * 0.34, s * 0.09, 0, 0, Math.PI * 2); tctx.stroke(); tctx.beginPath(); tctx.moveTo(x - s * 0.31, y - s * 0.31); tctx.lineTo(x - s * 0.2, y + s * 0.4); tctx.quadraticCurveTo(x, y + s * 0.47, x + s * 0.2, y + s * 0.4); tctx.lineTo(x + s * 0.31, y - s * 0.31); tctx.stroke(); tctx.beginPath(); tctx.moveTo(x - s * 0.17, y + s * 0.37); tctx.quadraticCurveTo(x, y + s * 0.42, x + s * 0.17, y + s * 0.37); tctx.stroke(); accentStroke(() => { tctx.beginPath(); tctx.moveTo(x - s * 0.25, y + s * 0.05); tctx.quadraticCurveTo(x, y + s * 0.11, x + s * 0.25, y + s * 0.05); tctx.stroke(); }); } else if (type === 'carton') { // Folding paper box: a dominant rectangular front face keeps the package legible on phones. tctx.beginPath(); tctx.moveTo(x - s * 0.42, y - s * 0.18); tctx.lineTo(x + s * 0.18, y - s * 0.18); tctx.lineTo(x + s * 0.18, y + s * 0.38); tctx.lineTo(x - s * 0.42, y + s * 0.38); tctx.closePath(); tctx.moveTo(x - s * 0.42, y - s * 0.18); tctx.lineTo(x - s * 0.18, y - s * 0.38); tctx.lineTo(x + s * 0.42, y - s * 0.1); tctx.lineTo(x + s * 0.18, y - s * 0.18); tctx.closePath(); tctx.moveTo(x + s * 0.18, y - s * 0.18); tctx.lineTo(x + s * 0.42, y - s * 0.1); tctx.lineTo(x + s * 0.42, y + s * 0.28); tctx.lineTo(x + s * 0.18, y + s * 0.38); tctx.closePath(); tctx.moveTo(x - s * 0.3, y + s * 0.06); tctx.lineTo(x + s * 0.06, y + s * 0.06); tctx.stroke(); accentStroke(() => { tctx.beginPath(); tctx.moveTo(x - s * 0.18, y - s * 0.38); tctx.lineTo(x + s * 0.18, y - s * 0.18); tctx.stroke(); }); } else if (type === 'coatedPaper') { // Waterborne coated paper: coating roller, paper web, isolated droplet and interface line. tctx.beginPath(); tctx.moveTo(x - s * 0.42, y + s * 0.19); tctx.lineTo(x + s * 0.42, y + s * 0.19); tctx.lineTo(x + s * 0.33, y + s * 0.4); tctx.lineTo(x - s * 0.33, y + s * 0.4); tctx.closePath(); tctx.stroke(); tctx.beginPath(); tctx.moveTo(x - s * 0.25, y - s * 0.12); tctx.lineTo(x + s * 0.09, y - s * 0.12); tctx.quadraticCurveTo(x + s * 0.18, y - s * 0.12, x + s * 0.18, y - s * 0.03); tctx.lineTo(x + s * 0.18, y + s * 0.03); tctx.quadraticCurveTo(x + s * 0.18, y + s * 0.12, x + s * 0.09, y + s * 0.12); tctx.lineTo(x - s * 0.25, y + s * 0.12); tctx.quadraticCurveTo(x - s * 0.34, y + s * 0.12, x - s * 0.34, y + s * 0.03); tctx.lineTo(x - s * 0.34, y - s * 0.03); tctx.quadraticCurveTo(x - s * 0.34, y - s * 0.12, x - s * 0.25, y - s * 0.12); tctx.closePath(); tctx.moveTo(x + s * 0.18, y); tctx.lineTo(x + s * 0.31, y - s * 0.14); tctx.lineTo(x + s * 0.31, y - s * 0.34); tctx.stroke(); tctx.beginPath(); tctx.moveTo(x - s * 0.35, y - s * 0.5); tctx.bezierCurveTo(x - s * 0.47, y - s * 0.37, x - s * 0.46, y - s * 0.27, x - s * 0.35, y - s * 0.24); tctx.bezierCurveTo(x - s * 0.24, y - s * 0.27, x - s * 0.23, y - s * 0.37, x - s * 0.35, y - s * 0.5); tctx.stroke(); accentStroke(() => { tctx.beginPath(); tctx.moveTo(x - s * 0.34, y + s * 0.22); tctx.lineTo(x + s * 0.34, y + s * 0.22); tctx.stroke(); }); } else if (type === 'bag') { // Flat-bottom paper bag with handles, side gussets and a reinforced base. tctx.beginPath(); tctx.moveTo(x - s * 0.39, y - s * 0.05); tctx.lineTo(x + s * 0.39, y - s * 0.05); tctx.lineTo(x + s * 0.34, y + s * 0.46); tctx.lineTo(x - s * 0.34, y + s * 0.46); tctx.closePath(); tctx.moveTo(x - s * 0.22, y - s * 0.05); tctx.quadraticCurveTo(x - s * 0.2, y - s * 0.46, x, y - s * 0.46); tctx.quadraticCurveTo(x + s * 0.2, y - s * 0.46, x + s * 0.22, y - s * 0.05); tctx.moveTo(x - s * 0.39, y - s * 0.05); tctx.lineTo(x - s * 0.27, y + s * 0.12); tctx.moveTo(x + s * 0.39, y - s * 0.05); tctx.lineTo(x + s * 0.27, y + s * 0.12); tctx.stroke(); accentStroke(() => { tctx.beginPath(); tctx.moveTo(x - s * 0.27, y + s * 0.15); tctx.lineTo(x + s * 0.27, y + s * 0.15); tctx.moveTo(x - s * 0.3, y + s * 0.34); tctx.lineTo(x + s * 0.3, y + s * 0.34); tctx.stroke(); }); } else if (type === 'serviceware') { // Fork, spoon, knife and a bent straw, optically separated for small screens. tctx.lineWidth = Math.max(3.5, s * 0.065); const forkX = x - s * 0.32; tctx.beginPath(); tctx.moveTo(forkX, y - s * 0.09); tctx.lineTo(forkX, y + s * 0.43); [-0.08, 0, 0.08].forEach((offset) => { tctx.moveTo(forkX + s * offset, y - s * 0.43); tctx.lineTo(forkX + s * offset, y - s * 0.13); }); tctx.moveTo(forkX - s * 0.08, y - s * 0.13); tctx.quadraticCurveTo(forkX, y - s * 0.03, forkX + s * 0.08, y - s * 0.13); tctx.stroke(); tctx.beginPath(); tctx.ellipse(x - s * 0.1, y - s * 0.29, s * 0.075, s * 0.14, 0, 0, Math.PI * 2); tctx.moveTo(x - s * 0.1, y - s * 0.15); tctx.lineTo(x - s * 0.1, y + s * 0.43); tctx.stroke(); tctx.beginPath(); tctx.moveTo(x + s * 0.08, y - s * 0.43); tctx.quadraticCurveTo(x + s * 0.25, y - s * 0.12, x + s * 0.13, y + s * 0.08); tctx.lineTo(x + s * 0.13, y + s * 0.43); tctx.moveTo(x + s * 0.08, y - s * 0.43); tctx.lineTo(x + s * 0.08, y + s * 0.43); tctx.stroke(); accentStroke(() => { tctx.beginPath(); tctx.moveTo(x + s * 0.34, y + s * 0.43); tctx.lineTo(x + s * 0.34, y - s * 0.24); tctx.lineTo(x + s * 0.23, y - s * 0.38); tctx.stroke(); }); } tctx.restore(); } function buildApplicationTargets() { const off = document.createElement('canvas'); const octx = off.getContext('2d', { willReadFrequently: true }); const gap = w <= 760 ? config.applicationMobileGap : config.applicationDesktopGap; const nextTargets = []; const basePalette = ['#004A96', '#0D5B9C', '#1D6FB8', '#004A96', '#0D5B9C']; const accentPalette = ['#0A8F67', '#00793A']; off.width = w; off.height = h; applicationIconLayout().forEach((icon, groupIndex) => { octx.clearRect(0, 0, w, h); drawApplicationIcon(octx, icon); const padding = Math.ceil(icon.size * 0.62); const sampleX = Math.max(0, Math.floor(icon.x - padding)); const sampleY = Math.max(0, Math.floor(icon.y - padding)); const sampleWidth = Math.min(w - sampleX, padding * 2 + 1); const sampleHeight = Math.min(h - sampleY, padding * 2 + 1); const data = octx.getImageData(sampleX, sampleY, sampleWidth, sampleHeight).data; const particleMetrics = w <= 520 ? { size: 1.38, jitter: 0.18 } : w <= 760 ? { size: 1.34, jitter: 0.16 } : { size: config.appParticleSize, jitter: config.appParticleJitter }; for (let localY = 0; localY < sampleHeight; localY += gap) { for (let localX = 0; localX < sampleWidth; localX += gap) { const pixelIndex = (localY * sampleWidth + localX) * 4; const red = data[pixelIndex]; const green = data[pixelIndex + 1]; const blue = data[pixelIndex + 2]; const a = data[pixelIndex + 3]; if (a > 40) { const isAccent = green > 110 && green > red * 1.7 && green > blue * 1.25; const color = isAccent ? accentPalette[groupIndex % accentPalette.length] : basePalette[groupIndex % basePalette.length]; nextTargets.push({ x: sampleX + localX, y: sampleY + localY, groupIndex, color, size: particleMetrics.size + (isAccent ? 0.08 : 0) + Math.random() * particleMetrics.jitter }); } } } }); return nextTargets; } function limitTargets(nextTargets) { if (nextTargets.length <= config.maxParticles) return nextTargets; const limited = []; const step = nextTargets.length / config.maxParticles; for (let i = 0; i < config.maxParticles; i++) { limited.push(nextTargets[Math.floor(i * step)]); } return limited; } function setTargets(nextTargets, options = {}) { if (!nextTargets.length) return; // 控制粒子数量,避免首页性能压力过大 targets = limitTargets(nextTargets); syncParticles(options); } function rebuildCurrentTargets() { if (!img.complete || !img.naturalWidth) return; if (currentMode === 'dbc') { setTargets(getDbcTargets(), { spatialMatch: true }); return; } if (currentMode === 'bioten') { setTargets(getBiotenTargets(), { spatialMatch: true }); return; } if (currentMode === 'dbcBurst') { setTargets(buildDbcBurstTargets()); return; } if (currentMode === 'scatter') { setTargets(buildScatterTargets(particles.length || targets.length)); return; } setTargets(getLogoTargets(), { spatialMatch: true }); } function switchTo(mode) { if (!isPageActive) return; currentMode = mode; hero.dataset.particleMode = mode; captionVisible = false; if (heroProductRevealTimer) { clearTimeout(heroProductRevealTimer); heroProductRevealTimer = null; } captionReadyAt = 0; captionForceAt = 0; appVisible = false; appReadyAt = 0; appForceAt = 0; appParticles = []; appTargets = []; stage.classList.remove('is-bioten-particle-ready'); hero.classList.remove('is-bioten-product-ready'); if (mode === 'dbcBurst') { beginTransitionShake(performance.now(), config.dbcBurstShakeDuration); setTargets(buildDbcBurstTargets()); kickDbcBurstParticles(); } else if (mode === 'scatter') { beginTransitionShake(performance.now(), config.transitionShakeDuration); setTargets(buildScatterTargets(particles.length || targets.length)); kickScatterParticles(); } else if (mode === 'bioten') { const now = performance.now(); beginTransitionShake(now, config.transitionShakeDuration); setTargets(getBiotenTargets(), { spatialMatch: true, sequentialLetters: true, activeAt: now }); captionReadyAt = now + config.captionRevealMinDelay; captionForceAt = now + config.captionRevealMaxDelay; appReadyAt = now + config.appRevealMinDelay; appForceAt = now + config.appRevealMaxDelay; } else if (mode === 'dbc') { setTargets(getDbcTargets(), { spatialMatch: true }); } else { const now = performance.now(); setTargets(getLogoTargets(), { spatialMatch: true, colorTransitionStart: now, colorTransitionDuration: config.logoColorTransitionDuration, colorTransitionDelay: config.logoColorTransitionDelay }); } } function startBiotenSequence() { if (!isPageActive || sequenceStarted) return; sequenceStarted = true; sequenceTimers.forEach(clearTimeout); if (currentMode === 'bioten') { sequenceTimers = []; return; } if (currentMode === 'scatter') { sequenceTimers = [ window.setTimeout(() => switchTo('bioten'), config.scatterHold) ]; return; } if (currentMode === 'dbcBurst') { sequenceTimers = [ window.setTimeout(() => switchTo('logo'), config.dbcBurstHold), window.setTimeout(() => switchTo('scatter'), config.dbcBurstHold + config.cycleHold), window.setTimeout(() => switchTo('bioten'), config.dbcBurstHold + config.cycleHold + config.scatterHold) ]; return; } if (currentMode === 'logo') { sequenceTimers = [ window.setTimeout(() => switchTo('scatter'), config.cycleHold), window.setTimeout(() => switchTo('bioten'), config.cycleHold + config.scatterHold) ]; return; } sequenceTimers = [ window.setTimeout(() => switchTo('dbcBurst'), config.dbcHold), window.setTimeout(() => switchTo('logo'), config.dbcHold + config.dbcBurstHold), window.setTimeout(() => switchTo('scatter'), config.dbcHold + config.dbcBurstHold + config.cycleHold), window.setTimeout(() => switchTo('bioten'), config.dbcHold + config.dbcBurstHold + config.cycleHold + config.scatterHold) ]; } function syncParticles(options = {}) { const spatialMatch = Boolean(options.spatialMatch); if (!targets.length) return; while (particles.length < targets.length) { particles.push(new Particle(targets[particles.length])); } if (particles.length > targets.length) { particles.length = targets.length; } if (spatialMatch) { const sortedParticles = particles.slice().sort((a, b) => (a.x - b.x) || (a.y - b.y)); const sortedTargets = targets.slice().sort((a, b) => (a.x - b.x) || (a.y - b.y)); sortedParticles.forEach((p, i) => p.setTarget( sortedTargets[i], targetActiveAt(sortedTargets[i], options), options )); return; } particles.forEach((p, i) => p.setTarget( targets[i], targetActiveAt(targets[i], options), options )); } function targetActiveAt(target, options = {}) { if (!options.sequentialLetters) return 0; const letterIndex = target.letterIndex || 0; const letterProgress = target.letterProgress || 0; return (options.activeAt || performance.now()) + letterIndex * config.biotenLetterStagger + letterProgress * config.biotenLetterSweep + Math.random() * config.biotenLetterJitter; } function setAppTargets(nextTargets, respawn) { if (nextTargets.length > 1200) { const groupTotal = Math.max(1, applicationIconLayout().length); const appParticleLimit = 1500; const perGroupLimit = Math.floor(appParticleLimit / groupTotal); const groupedTargets = Array.from({ length: groupTotal }, () => []); nextTargets.forEach((target) => { const group = clamp(target.groupIndex || 0, 0, groupTotal - 1); groupedTargets[group].push(target); }); appTargets = groupedTargets.flatMap((groupTargets) => { if (groupTargets.length <= perGroupLimit) return groupTargets; const sampled = []; const step = groupTargets.length / perGroupLimit; for (let i = 0; i < perGroupLimit; i++) { sampled.push(groupTargets[Math.floor(i * step)]); } return sampled; }); } else { appTargets = nextTargets; } if (!appTargets.length) return; if (respawn || !appParticles.length) { const iconLayout = applicationIconLayout(); const sourceGroups = iconLayout.map(() => []); particles.forEach((particle) => { let nearestGroup = 0; let nearestDistance = Infinity; iconLayout.forEach((icon, groupIndex) => { const distance = Math.abs(particle.x - icon.x); if (distance < nearestDistance) { nearestDistance = distance; nearestGroup = groupIndex; } }); sourceGroups[nearestGroup].push(particle); }); appParticles = appTargets.map((target, index) => { const groupSources = sourceGroups[target.groupIndex] || particles; const sourcePool = groupSources.length ? groupSources : particles; const source = sourcePool[Math.floor(Math.random() * Math.max(1, sourcePool.length))] || target; return new AppParticle(target, source); }); return; } while (appParticles.length < appTargets.length) { const target = appTargets[appParticles.length]; const source = particles[Math.floor(Math.random() * Math.max(1, particles.length))] || target; appParticles.push(new AppParticle(target, source)); } if (appParticles.length > appTargets.length) { appParticles.length = appTargets.length; } appParticles.forEach((p, i) => p.setTarget(appTargets[i])); } function revealApplicationParticles(now, settled) { if (appVisible || currentMode !== 'bioten') return; if (now < appReadyAt) return; if (settled || now >= appForceAt) { appVisible = true; setAppTargets(getApplicationTargets(), true); } } function biotenParticlesSettled() { if (!particles.length) return false; const stride = Math.max(1, Math.floor(particles.length / 180)); let checked = 0; let close = 0; for (let i = 0; i < particles.length; i += stride) { const p = particles[i]; const dx = p.x - p.tx; const dy = p.y - p.ty; checked++; if ((dx * dx + dy * dy) < 100) close++; } return checked > 0 && close / checked > 0.82; } function updateBiotenCaption(now, settled) { if (captionVisible || currentMode !== 'bioten') return; if (now < captionReadyAt) return; if (settled || now >= captionForceAt) { captionVisible = true; stage.classList.add('is-bioten-particle-ready'); heroProductRevealTimer = window.setTimeout(() => { if (currentMode === 'bioten' && captionVisible) { hero.classList.add('is-bioten-product-ready'); } }, config.heroProductRevealDelay); } } function shouldRunAnimation() { return initialized && isPageActive && isHeroInView && !isNavigationOpen && !document.hidden; } function stopAnimationLoop() { if (rafId === null) return; cancelAnimationFrame(rafId); rafId = null; } function startAnimationLoop() { if (rafId !== null || !shouldRunAnimation()) return; rafId = requestAnimationFrame(animate); } function syncAnimationLoop() { if (shouldRunAnimation()) { startAnimationLoop(); } else { stopAnimationLoop(); } } function animate() { rafId = null; if (!shouldRunAnimation()) return; tick++; const now = performance.now(); ctx.clearRect(0, 0, w, h); ctx.fillStyle = backgroundGradient || createBackgroundGradient(); ctx.fillRect(0, 0, w, h); for (const p of particles) { p.update(now); p.draw(); } const needsSettleCheck = currentMode === 'bioten' && ((!captionVisible && now >= captionReadyAt) || (!appVisible && now >= appReadyAt)); const settled = needsSettleCheck ? biotenParticlesSettled() : false; revealApplicationParticles(now, settled); for (const p of appParticles) { p.update(now); p.draw(); } updateBiotenCaption(now, settled); startAnimationLoop(); } function pointerPosition(event) { const rect = canvas.getBoundingClientRect(); const point = event.touches ? event.touches[0] : event; mouse.x = point.clientX - rect.left; mouse.y = point.clientY - rect.top; } if (supportsFinePointer) { hero.addEventListener('mousemove', (e) => { mouse.active = true; pointerPosition(e); }); hero.addEventListener('mouseleave', () => { mouse.active = false; mouse.x = -9999; mouse.y = -9999; }); } const brands = ['DBC®', 'DPU®', 'BASF&DBC®', 'ANTUX®', 'CASU®', 'BIOTEN™', 'ECOTEN™', '世界杯竞猜官网®', '环涂士®', '中科联化®', '麻雀®', '道赢®', '当众®']; const bubblePalette = [ 'rgba(0,121,58,.25)', 'rgba(38,40,150,.23)', 'rgba(216,182,90,.32)', 'rgba(0,150,102,.22)', 'rgba(49,130,206,.21)', 'rgba(236,137,54,.22)', 'rgba(136,96,208,.21)', 'rgba(46,184,160,.23)', 'rgba(244,114,182,.19)', 'rgba(85,107,47,.20)', 'rgba(0,121,58,.19)', 'rgba(216,182,90,.24)', 'rgba(38,40,150,.18)' ]; const coreBusinessBrands = new Set(['DPU®', 'ANTUX®', 'CASU®', 'BIOTEN™', 'ECOTEN™']); function rand(min, max) { return min + Math.random() * (max - min); } function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } function visualLength(text) { let total = 0; for (const char of Array.from(text)) { const code = char.codePointAt(0); if (code >= 0x4e00 && code <= 0x9fff) total += 1.15; else if (char === '®' || char === '™' || char === '&') total += 0.35; else total += 0.68; } return total; } function brandDiameter(text, isMobile) { const length = visualLength(text); const base = (isMobile ? 54 : 64) + length * (isMobile ? 9.2 : 11.6); return clamp(base, isMobile ? 62 : 76, isMobile ? 142 : 176); } function buildBrandBubbles() { const layer = stage.querySelector('.dbc-brand-bubble-layer'); if (!layer) return; layer.innerHTML = ''; const isMobile = window.innerWidth <= 760; const desktopOrbit = [ [50, 12], [66, 16], [80, 25], [90, 40], [88, 58], [76, 73], [59, 80], [41, 80], [24, 73], [12, 58], [10, 40], [20, 25], [34, 16] ]; const mobileOrbit = [ [50, 10], [70, 15], [86, 28], [90, 46], [82, 64], [63, 77], [40, 78], [20, 68], [10, 50], [14, 31], [30, 16], [50, 22], [72, 72] ]; const orbit = isMobile ? mobileOrbit : desktopOrbit; const sizeBias = { 'DBC®': 0.92, 'BASF&DBC®': 0.82, 'BIOTEN™': 0.98, 'ECOTEN™': 0.9, '中科联化®': 0.78, '麻雀®': 0.72, '当众®': 0.7 }; brands.forEach((brand, index) => { const bubble = document.createElement('div'); const label = document.createElement('span'); const point = orbit[index % orbit.length]; const x = clamp(point[0] + rand(-1.8, 1.8), isMobile ? 7 : 8, isMobile ? 93 : 92); const y = clamp(point[1] + rand(-1.6, 1.6), isMobile ? 8 : 9, isMobile ? 80 : 82); const d = brandDiameter(brand, isMobile) * (isMobile ? 0.64 : 0.66) * (sizeBias[brand] || rand(0.64, 0.96)); const fs = clamp(d / (visualLength(brand) > 5.8 ? 10.8 : 8.6), isMobile ? 9.2 : 10.2, isMobile ? 13 : 15); bubble.className = 'dbc-brand-bubble'; label.textContent = brand; bubble.appendChild(label); bubble.style.setProperty('--x', x.toFixed(2) + '%'); bubble.style.setProperty('--y', y.toFixed(2) + '%'); bubble.style.setProperty('--d', d.toFixed(0) + 'px'); bubble.style.setProperty('--fs', fs.toFixed(1) + 'px'); bubble.style.setProperty('--bubble-bg', bubblePalette[index % bubblePalette.length]); bubble.style.setProperty('--opacity', coreBusinessBrands.has(brand) ? '1' : rand(0.38, 0.68).toFixed(2)); bubble.style.setProperty('--dx', rand(-17, 17).toFixed(1) + 'px'); bubble.style.setProperty('--dy', rand(-15, 15).toFixed(1) + 'px'); bubble.style.setProperty('--r', rand(-8, 8).toFixed(1) + 'deg'); bubble.style.setProperty('--rr', rand(-5, 5).toFixed(1) + 'deg'); bubble.style.setProperty('--duration', rand(5.8, 9.8).toFixed(2) + 's'); bubble.style.setProperty('--delay', (-rand(0, 5)).toFixed(2) + 's'); layer.appendChild(bubble); }); } function resetPointerState() { mouse.active = false; mouse.x = -9999; mouse.y = -9999; } function clearRuntimeTimers() { sequenceTimers.forEach((timerId) => window.clearTimeout(timerId)); sequenceTimers = []; sequenceStarted = false; if (heroProductRevealTimer) { window.clearTimeout(heroProductRevealTimer); heroProductRevealTimer = null; } cancelTargetWarmup(); } function disconnectRuntimeObservers() { if (heroVisibilityObserver) { heroVisibilityObserver.disconnect(); heroVisibilityObserver = null; } if (resizeObserver) { resizeObserver.disconnect(); resizeObserver = null; } } function updateHeroVisibilityFromLayout() { if (!('IntersectionObserver' in window)) { isHeroInView = true; return; } const rect = hero.getBoundingClientRect(); isHeroInView = rect.bottom > 0 && rect.top < window.innerHeight && rect.right > 0 && rect.left < window.innerWidth; } function handleHeroIntersection(entries) { const entry = entries.find((candidate) => candidate.target === hero); if (!entry) return; isHeroInView = entry.isIntersecting; if (!isHeroInView) resetPointerState(); syncAnimationLoop(); } function handleDocumentVisibilityChange() { if (document.hidden) resetPointerState(); syncAnimationLoop(); } function handleNavigationOpen() { isNavigationOpen = true; resetPointerState(); hero.classList.add('is-navigation-paused'); syncAnimationLoop(); } function handleNavigationClose() { isNavigationOpen = false; hero.classList.remove('is-navigation-paused'); updateHeroVisibilityFromLayout(); syncAnimationLoop(); } function setupRuntimeObservers() { disconnectRuntimeObservers(); document.removeEventListener('visibilitychange', handleDocumentVisibilityChange); window.removeEventListener('resize', resizeCanvas); updateHeroVisibilityFromLayout(); if ('IntersectionObserver' in window) { heroVisibilityObserver = new IntersectionObserver(handleHeroIntersection, { threshold: [0, 0.01] }); heroVisibilityObserver.observe(hero); } if ('ResizeObserver' in window) { resizeObserver = new ResizeObserver(resizeCanvas); resizeObserver.observe(stage); } document.addEventListener('visibilitychange', handleDocumentVisibilityChange); window.addEventListener('resize', resizeCanvas); } function handlePageHide() { if (!isPageActive) return; isPageActive = false; resetPointerState(); stopAnimationLoop(); clearRuntimeTimers(); disconnectRuntimeObservers(); document.removeEventListener('visibilitychange', handleDocumentVisibilityChange); window.removeEventListener('resize', resizeCanvas); } function handlePageShow(event) { if (!event.persisted || isPageActive) return; isPageActive = true; setupRuntimeObservers(); if (!initialized && img.complete && img.naturalWidth) { initializeParticleHero(); } else if (initialized) { resizeCanvas(); } if (initialized) { if (currentMode === 'bioten' && captionVisible) { hero.classList.add('is-bioten-product-ready'); } startBiotenSequence(); scheduleTargetWarmup(); syncAnimationLoop(); } } function initializeParticleHero() { if (!isPageActive || initialized) return; initialized = true; resizeCanvas(); startAnimationLoop(); startBiotenSequence(); } setupRuntimeObservers(); document.addEventListener('dbc:navigation-open', handleNavigationOpen); document.addEventListener('dbc:navigation-close', handleNavigationClose); window.addEventListener('pagehide', handlePageHide); window.addEventListener('pageshow', handlePageShow); img.onload = initializeParticleHero; if (img.complete && img.naturalWidth) initializeParticleHero(); })();