diff --git a/js/main.js b/js/main.js index 916b5c1..95f4805 100644 --- a/js/main.js +++ b/js/main.js @@ -293,20 +293,21 @@ window.addEventListener('scroll', function () { document.body.appendChild(s); s.animate([ { transform: 'translate(-50%,-50%) scale(0)' }, - { transform: 'translate(-50%,-50%) scale(' + size + ')', offset: 0.3 }, - { transform: 'translate(-50%,-50%) scale(' + size + ')', offset: 0.7 }, + { transform: 'translate(-50%,-50%) scale(' + size + ')', offset: 0.08 }, // pop in almost instantly + { transform: 'translate(-50%,-50%) scale(' + size + ')', offset: 0.6 }, { transform: 'translate(-50%,-50%) scale(0)' } - ], { duration: life, easing: 'ease-in-out' }).onfinish = function () { s.remove(); }; + ], { duration: life, easing: 'ease-out' }).onfinish = function () { s.remove(); }; } - function teleport(img) { + function teleport(img, startDelay) { var r = img.getBoundingClientRect(); - var START_DELAY = 500; // pause before anything happens + var START_DELAY = (typeof startDelay === 'number') ? startDelay : 500; // pause before anything happens var CHARGE = 1000; // sparkles gather along the bottom before the figure grows var GROW = 2200; // the bottom-up materialise var baseShadow = 'drop-shadow(0 8px 24px rgba(0,0,0,0.35))'; function ease(t) { return t * t; } // ease-in: gentle at the bottom, no slow-down toward the top + img.dataset.teleBusy = '1'; // Keep it invisible (clipped) but opaque from the outset, so nothing flashes during the delay img.style.opacity = '1'; img.style.webkitClipPath = img.style.clipPath = 'inset(100% 0 0 0)'; @@ -369,16 +370,74 @@ window.addEventListener('scroll', function () { img.style.filter = ''; // CSS drop-shadow returns img.classList.remove('mascot-teleport'); img.style.opacity = '1'; + delete img.dataset.teleBusy; } } requestAnimationFrame(frame); }, START_DELAY); } + // Reverse "beam out": dissolve top-down into a puff of sparkles, then stay hidden + function dematerialize(img, done) { + var r = img.getBoundingClientRect(); + var DUR = 800; + var baseShadow = 'drop-shadow(0 8px 24px rgba(0,0,0,0.35))'; + var start = null; + function frame(ts) { + if (!start) start = ts; + var p = Math.min((ts - start) / DUR, 1); + + // Dissolve away from the top downwards + img.style.webkitClipPath = img.style.clipPath = 'inset(' + (p * 100).toFixed(2) + '% 0 0 0)'; + + // Energy flash that builds as it beams out + var bright = (1 + 1.2 * p).toFixed(2); + var glow = (0.7 * p).toFixed(2); + img.style.filter = baseShadow + ' drop-shadow(0 0 14px rgba(255,255,255,' + glow + ')) brightness(' + bright + ')'; + + // Sparkles riding the dissolving edge + var lineY = r.top + p * r.height; + if (p < 0.98) { + for (var i = 0; i < 6; i++) { + var x = r.left + Math.random() * r.width; + var y = lineY + (Math.random() - 0.5) * r.height * 0.1; + y = Math.max(r.top, Math.min(r.bottom, y)); + teleSpark(x, y, 480 + Math.random() * 360); + } + } + + if (p < 1) { + requestAnimationFrame(frame); + } else { + // A final puff of sparkles where it stood + for (var j = 0; j < 16; j++) { + teleSpark(r.left + Math.random() * r.width, r.top + Math.random() * r.height, 420 + Math.random() * 360); + } + img.style.opacity = '0'; // stay hidden during the wait + img.style.webkitClipPath = img.style.clipPath = ''; + img.style.filter = ''; + if (done) done(); + } + } + requestAnimationFrame(frame); + } + var io = new IntersectionObserver(function (entries) { entries.forEach(function (e) { if (e.isIntersecting) { io.unobserve(e.target); teleport(e.target); } }); }, { threshold: 0.25 }); - imgs.forEach(function (im) { io.observe(im); }); + + imgs.forEach(function (im) { + io.observe(im); + // Click to beam out, then re-materialise after ~3s + im.style.cursor = 'pointer'; + im.addEventListener('click', function () { + if (im.dataset.teleBusy) return; // ignore while an effect is already running + im.dataset.teleBusy = '1'; + dematerialize(im, function () { + setTimeout(function () { teleport(im, 0); }, 3000); + }); + }); + }); })();