/* * DBC / BIOTEN V7.8.0 immersive system * Semantic particle fields, restrained glass spotlights and viewport reveals. * Canvas scenes are visual metaphors, not scientific simulations or test data. */ (function () { 'use strict'; var body = document.body; if (!body || !body.dataset.v780Experience) return; var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); var finePointer = window.matchMedia('(hover: hover) and (pointer: fine)'); var fields = []; function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } function mix(from, to, amount) { return from + (to - from) * amount; } function smooth(value) { var t = clamp(value, 0, 1); return t * t * (3 - 2 * t); } function randomBetween(min, max) { return min + Math.random() * (max - min); } function phaseMix(progress, start, end) { return smooth((progress - start) / Math.max(.001, end - start)); } function resolveBudget(mode) { var mobile = window.innerWidth <= 560; var tablet = window.innerWidth <= 980; if (mode === 'emulsion') return mobile ? 46 : tablet ? 72 : 112; if (mode === 'barrier') return mobile ? 34 : tablet ? 54 : 82; return mobile ? 38 : tablet ? 60 : 90; } function ParticleField(canvas) { this.canvas = canvas; this.stage = canvas.parentElement; this.mode = canvas.dataset.v780Field || 'workbench'; this.context = canvas.getContext && canvas.getContext('2d', { alpha: true }); this.width = 0; this.height = 0; this.ratio = 1; this.particles = []; this.visible = false; this.frame = 0; this.startedAt = performance.now(); this.pointer = { x: .5, y: .5, active: false }; this.onFrame = this.onFrame.bind(this); this.resize = this.resize.bind(this); if (!this.context || !this.stage) return; this.connect(); } ParticleField.prototype.connect = function () { var self = this; this.canvas.setAttribute('aria-hidden', 'true'); if ('ResizeObserver' in window) { this.resizeObserver = new ResizeObserver(this.resize); this.resizeObserver.observe(this.stage); } else { window.addEventListener('resize', this.resize, { passive: true }); } if ('IntersectionObserver' in window) { this.visibilityObserver = new IntersectionObserver(function (entries) { self.visible = Boolean(entries[0] && entries[0].isIntersecting); if (self.visible) self.start(); else self.stop(); }, { rootMargin: '160px 0px', threshold: 0 }); this.visibilityObserver.observe(this.stage); } else { this.visible = true; } if (finePointer.matches) { this.stage.addEventListener('pointermove', function (event) { var rect = self.stage.getBoundingClientRect(); self.pointer.x = clamp((event.clientX - rect.left) / Math.max(1, rect.width), 0, 1); self.pointer.y = clamp((event.clientY - rect.top) / Math.max(1, rect.height), 0, 1); self.pointer.active = true; }, { passive: true }); this.stage.addEventListener('pointerleave', function () { self.pointer.active = false; }); } document.addEventListener('visibilitychange', function () { if (document.hidden) self.stop(); else if (self.visible) self.start(); }); this.resize(); if (this.visible || !this.visibilityObserver) this.start(); }; ParticleField.prototype.resize = function () { var rect = this.stage.getBoundingClientRect(); this.width = Math.max(1, Math.round(rect.width)); this.height = Math.max(1, Math.round(rect.height)); this.ratio = Math.min(window.devicePixelRatio || 1, window.innerWidth <= 560 ? 1.25 : 1.6); this.canvas.width = Math.round(this.width * this.ratio); this.canvas.height = Math.round(this.height * this.ratio); this.canvas.style.width = this.width + 'px'; this.canvas.style.height = this.height + 'px'; this.context.setTransform(this.ratio, 0, 0, this.ratio, 0, 0); this.build(resolveBudget(this.mode)); this.canvas.dataset.v780ParticleCount = String(this.particles.length); this.canvas.dataset.v780Motion = reducedMotion.matches ? 'static-semantic-state' : 'semantic-cycle'; this.draw(reducedMotion.matches ? .69 : .08); }; ParticleField.prototype.build = function (count) { var mode = this.mode; this.particles = Array.from({ length: count }, function (_, index) { var group = index % 3; var seed; var cluster; var resolved; if (mode === 'emulsion') { seed = { x: randomBetween(.08, .95), y: randomBetween(.08, .92) }; var centers = [{ x: .52, y: .3 }, { x: .68, y: .49 }, { x: .53, y: .7 }]; var angle = Math.random() * Math.PI * 2; var radius = randomBetween(.018, .095); cluster = { x: centers[group].x + Math.cos(angle) * radius, y: centers[group].y + Math.sin(angle) * radius }; var lane = index / Math.max(1, count - 1); resolved = { x: .42 + lane * .48, y: .63 - lane * .27 + Math.sin(index * 1.43) * .016 }; } else if (mode === 'barrier') { seed = { x: randomBetween(.7, 1.04), y: randomBetween(.08, .94) }; cluster = { x: .655 + randomBetween(-.025, .022), y: seed.y + randomBetween(-.06, .06) }; resolved = { x: .71 + randomBetween(.015, .2), y: clamp(seed.y + (group === 0 ? -.19 : group === 1 ? .18 : .08), .06, .94) }; } else { seed = { x: randomBetween(.04, .25), y: randomBetween(.14, .86) }; var route = index / Math.max(1, count - 1); cluster = { x: .31 + randomBetween(-.04, .04), y: .26 + group * .24 + randomBetween(-.055, .055) }; resolved = { x: .54 + route * .4, y: .22 + route * .58 + Math.sin(index * 1.2) * .018 }; } return { group: group, seed: seed, cluster: cluster, resolved: resolved, size: randomBetween(.75, 2.35), alpha: randomBetween(.28, .88), drift: Math.random() * Math.PI * 2 }; }); }; ParticleField.prototype.position = function (particle, progress) { var from = particle.seed; var to = particle.cluster; var amount = 0; if (progress < .2) { amount = phaseMix(progress, 0, .2) * .1; } else if (progress < .47) { amount = phaseMix(progress, .2, .47); } else if (progress < .78) { from = particle.cluster; to = particle.resolved; amount = phaseMix(progress, .47, .78); } else { from = particle.resolved; to = particle.seed; amount = phaseMix(progress, .78, 1); } var driftStrength = progress < .47 ? .0065 : .0022; var x = mix(from.x, to.x, amount) + Math.cos(particle.drift + progress * Math.PI * 2) * driftStrength; var y = mix(from.y, to.y, amount) + Math.sin(particle.drift + progress * Math.PI * 2) * driftStrength; if (this.pointer.active && !reducedMotion.matches) { var dx = x - this.pointer.x; var dy = y - this.pointer.y; var distance = Math.max(.035, Math.hypot(dx, dy)); if (distance < .2) { var force = (1 - distance / .2) * .012; x += dx / distance * force; y += dy / distance * force; } } return { x: x, y: y }; }; ParticleField.prototype.palette = function (group) { if (this.mode === 'barrier') { return group === 0 ? '#6fbdec' : group === 1 ? '#57d7ae' : '#c9a56a'; } return group === 2 ? '#65aee4' : '#50d6aa'; }; ParticleField.prototype.drawEmulsionGuide = function (progress) { if (progress < .4 || progress > .88) return; var ctx = this.context; var strength = progress < .7 ? phaseMix(progress, .4, .7) : 1 - phaseMix(progress, .78, .92); ctx.save(); ctx.globalAlpha = .42 * strength; ctx.strokeStyle = '#23ae91'; ctx.lineWidth = 1.2; ctx.shadowColor = '#50e0b5'; ctx.shadowBlur = 13; ctx.beginPath(); ctx.moveTo(this.width * .39, this.height * .66); ctx.bezierCurveTo(this.width * .54, this.height * .55, this.width * .71, this.height * .46, this.width * .92, this.height * .34); ctx.stroke(); ctx.restore(); }; ParticleField.prototype.drawBarrierGuide = function (progress) { var ctx = this.context; var formation = progress < .32 ? phaseMix(progress, .04, .32) : progress > .82 ? 1 - phaseMix(progress, .82, 1) : 1; var x = this.width * .665; var top = this.height * (.11 + (1 - formation) * .37); var bottom = this.height * (.91 - (1 - formation) * .37); ctx.save(); ctx.globalAlpha = .72 * formation; ctx.strokeStyle = '#35bd8f'; ctx.lineWidth = 1.5; ctx.shadowColor = '#48ddb0'; ctx.shadowBlur = 16; ctx.beginPath(); ctx.moveTo(x, top); ctx.lineTo(x, bottom); ctx.stroke(); ctx.globalAlpha = .2 * formation; ctx.lineWidth = 7; ctx.stroke(); ctx.restore(); }; ParticleField.prototype.drawWorkbenchGuide = function (progress) { if (progress < .34) return; var ctx = this.context; var strength = progress < .76 ? phaseMix(progress, .34, .62) : 1 - phaseMix(progress, .82, 1); var nodes = [ [.47, .31], [.56, .4], [.65, .5], [.74, .6], [.84, .7], [.93, .79] ]; ctx.save(); ctx.globalAlpha = .34 * strength; ctx.strokeStyle = '#55dfb4'; ctx.lineWidth = 1; ctx.beginPath(); nodes.forEach(function (node, index) { var x = node[0] * this.width; var y = node[1] * this.height; if (index === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }, this); ctx.stroke(); nodes.forEach(function (node, index) { ctx.globalAlpha = (.23 + index * .035) * strength; ctx.beginPath(); ctx.arc(node[0] * this.width, node[1] * this.height, 3 + index * .25, 0, Math.PI * 2); ctx.stroke(); }, this); ctx.restore(); }; ParticleField.prototype.drawConnections = function (points, progress) { if (progress < .22 || progress > .84) return; var ctx = this.context; var maxDistance = Math.min(this.width, this.height) * (this.mode === 'barrier' ? .085 : .105); ctx.save(); ctx.lineWidth = .55; for (var index = 0; index < points.length; index += 1) { for (var other = index + 1; other < Math.min(points.length, index + 8); other += 1) { var dx = points[index].x - points[other].x; var dy = points[index].y - points[other].y; var distance = Math.hypot(dx, dy); if (distance > maxDistance) continue; ctx.globalAlpha = (1 - distance / maxDistance) * .14; ctx.strokeStyle = this.palette(points[index].particle.group); ctx.beginPath(); ctx.moveTo(points[index].x, points[index].y); ctx.lineTo(points[other].x, points[other].y); ctx.stroke(); } } ctx.restore(); }; ParticleField.prototype.draw = function (progress) { var ctx = this.context; if (!ctx || !this.width || !this.height) return; ctx.clearRect(0, 0, this.width, this.height); if (this.mode === 'emulsion') this.drawEmulsionGuide(progress); else if (this.mode === 'barrier') this.drawBarrierGuide(progress); else this.drawWorkbenchGuide(progress); var points = this.particles.map(function (particle) { var position = this.position(particle, progress); return { x: position.x * this.width, y: position.y * this.height, particle: particle }; }, this); this.drawConnections(points, progress); points.forEach(function (point) { var particle = point.particle; var resolvedPhase = progress > .48 && progress < .8; ctx.save(); ctx.globalAlpha = particle.alpha * (resolvedPhase ? .92 : .68); ctx.fillStyle = this.palette(particle.group); ctx.shadowColor = this.palette(particle.group); ctx.shadowBlur = particle.size > 1.5 ? 6 : 2; ctx.beginPath(); ctx.arc(point.x, point.y, particle.size * (resolvedPhase ? 1.08 : .88), 0, Math.PI * 2); ctx.fill(); ctx.restore(); }, this); }; ParticleField.prototype.onFrame = function (now) { if (!this.visible || reducedMotion.matches || document.hidden) { this.frame = 0; return; } var duration = this.mode === 'barrier' ? 11800 : this.mode === 'emulsion' ? 15600 : 13200; var progress = ((now - this.startedAt) % duration) / duration; this.draw(progress); this.frame = window.requestAnimationFrame(this.onFrame); }; ParticleField.prototype.start = function () { if (reducedMotion.matches) { this.draw(.69); return; } if (!this.frame) this.frame = window.requestAnimationFrame(this.onFrame); }; ParticleField.prototype.stop = function () { if (this.frame) window.cancelAnimationFrame(this.frame); this.frame = 0; }; function initFields() { document.querySelectorAll('canvas[data-v780-field]').forEach(function (canvas) { fields.push(new ParticleField(canvas)); }); } function initGlass() { if (!finePointer.matches) return; document.querySelectorAll('[data-v780-glass]').forEach(function (surface) { var rect = null; var frame = 0; var point = { x: 50, y: 50 }; surface.addEventListener('pointerenter', function () { rect = surface.getBoundingClientRect(); surface.dataset.v780Pointer = 'active'; }); surface.addEventListener('pointermove', function (event) { if (!rect) rect = surface.getBoundingClientRect(); point.x = clamp((event.clientX - rect.left) / Math.max(1, rect.width) * 100, 0, 100); point.y = clamp((event.clientY - rect.top) / Math.max(1, rect.height) * 100, 0, 100); if (frame) return; frame = window.requestAnimationFrame(function () { surface.style.setProperty('--v780-pointer-x', point.x.toFixed(2) + '%'); surface.style.setProperty('--v780-pointer-y', point.y.toFixed(2) + '%'); frame = 0; }); }, { passive: true }); surface.addEventListener('pointerleave', function () { rect = null; surface.dataset.v780Pointer = 'idle'; surface.style.setProperty('--v780-pointer-x', '50%'); surface.style.setProperty('--v780-pointer-y', '0%'); }); }); } function initReveals() { var elements = Array.from(document.querySelectorAll('[data-v780-reveal]')); if (!elements.length) return; if (reducedMotion.matches || !('IntersectionObserver' in window)) { elements.forEach(function (element) { element.classList.add('v780-is-visible'); }); return; } var observer = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (!entry.isIntersecting && entry.boundingClientRect.bottom >= 0) return; entry.target.classList.add('v780-is-visible'); observer.unobserve(entry.target); }); }, { rootMargin: '0px 0px -7% 0px', threshold: .08 }); elements.forEach(function (element, index) { element.style.setProperty('--v780-delay', Math.min(index % 5, 4) * 70 + 'ms'); observer.observe(element); }); } function handleReducedMotionChange() { fields.forEach(function (field) { field.stop(); field.canvas.dataset.v780Motion = reducedMotion.matches ? 'static-semantic-state' : 'semantic-cycle'; field.draw(.69); if (!reducedMotion.matches && field.visible) field.start(); }); } function init() { initFields(); initGlass(); initReveals(); reducedMotion.addEventListener && reducedMotion.addEventListener('change', handleReducedMotionChange); body.dataset.v780Ready = 'true'; } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init, { once: true }); else init(); })();