1/* SweetHive scroll stories â scrubbed GSAP timelines over CSS-sticky stages. 2 Story 1 (#story, "Scoped by design"): dashboard â hive contexts â 3 Administration context â scoped composer â group-permissions proof â 4 context agent â apps. Screenshots 00â03 are pixel-aligned states of the 5 same app viewport, so the "camera" (scale + translate on .story-cam) plus 6 aligned crossfades read as navigating deeper into the product. 7 Story 2 (#story-local, "Local AI"): the personal-agent banner â install 8 dialog â the dark SweetHive Agents Node window (local models â local chat, 9 also pixel-aligned). Cloud recedes, the desktop app becomes the hero. */ 10(function () { 11 'use strict'; 12 if (!window.gsap || !window.ScrollTrigger) return; 13 var story1 = document.getElementById('story'); 14 var story2 = document.getElementById('story-local'); 15 if (!story1 && !story2) return; 16 gsap.registerPlugin(ScrollTrigger); 17 ScrollTrigger.config({ ignoreMobileResize: true }); 18 19 /* Web fonts swap in after the triggers are measured and shift every 20 position below the hero; re-measure once they are ready - but never 21 during an in-flight scroll (a mid-scroll refresh caches wrong values). */ 22 if (document.fonts && document.fonts.ready) { 23 document.fonts.ready.then(function () { 24 var doRefresh = function () { requestAnimationFrame(function () { ScrollTrigger.refresh(); }); }; 25 if (ScrollTrigger.isScrolling && ScrollTrigger.isScrolling()) { 26 var once = function () { ScrollTrigger.removeEventListener('scrollEnd', once); doRefresh(); }; 27 ScrollTrigger.addEventListener('scrollEnd', once); 28 } else { doRefresh(); } 29 }); 30 } 31 32 /* Self-heal: anything that loads late (widgets, embeds) shifts the tracks 33 and leaves the triggers measuring a page that no longer exists - the pin 34 then sticks on upward scroll. At scroll end, compare each trigger's 35 cached start with the track's real position and re-measure on drift. 36 Guarded hard: at most one refresh per 10s, and if a refresh does not 37 resolve the same drift (e.g. sub-pixel offsets under browser zoom) the 38 check disables itself instead of refreshing in a loop. */ 39 var healLast = 0, healPrevDrift = 0, healDisabled = false; 40 ScrollTrigger.addEventListener('scrollEnd', function () { 41 if (healDisabled) return; 42 var now = Date.now(); 43 if (now - healLast < 10000) return; 44 var sts = ScrollTrigger.getAll(); 45 for (var i = 0; i < sts.length; i++) { 46 var el = sts[i].trigger; 47 if (!el) continue; 48 var drift = Math.abs(el.getBoundingClientRect().top + (window.scrollY || window.pageYOffset || 0) - sts[i].start); 49 if (drift > 4) { 50 if (Math.abs(drift - healPrevDrift) < 2) { healDisabled = true; return; } 51 healPrevDrift = drift; 52 healLast = now; 53 ScrollTrigger.refresh(); 54 return; 55 } 56 } 57 }); 58 59 /* ---------------- shared helpers ---------------- */ 60 /* Camera state: s = zoom, fx/fy = focal point as a fraction of the shot. 61 Focal points are clamped so the frame never shows past the image edge. */ 62 function resolveCam(c) { 63 var half = 1 / (2 * c.s); 64 return { 65 s: c.s, 66 fx: gsap.utils.clamp(half, 1 - half, c.fx), 67 fy: gsap.utils.clamp(half, 1 - half, c.fy) 68 }; 69 } 70 /* transform list is translate() scale(): translation is post-scale, so the 71 offset that centers focal point f is -(f - .5) * s in element percent. */ 72 function camVars(c) { 73 c = resolveCam(c); 74 return { scale: c.s, xPercent: -(c.fx - .5) * 100 * c.s, yPercent: -(c.fy - .5) * 100 * c.s }; 75 } 76 /* Where an image-fraction point lands inside the frame (in %) at a camera. */ 77 function framePct(c, px, py) { 78 c = resolveCam(c); 79 return { 80 x: (px - (c.fx - 1 / (2 * c.s))) * c.s * 100, 81 y: (py - (c.fy - 1 / (2 * c.s))) * c.s * 100 82 }; 83 } 84 /* Place ring/tag annotations for the exact camera state they appear under. */ 85 function placeAnnos(q, table) { 86 Object.keys(table).forEach(function (key) { 87 var el = q('[data-anno="' + key + '"]')[0]; 88 if (!el) return; 89 var a = table[key], tl0 = framePct(a.cam, a.r[0], a.r[1]), br = framePct(a.cam, a.r[2], a.r[3]); 90 el.style.left = tl0.x + '%'; 91 el.style.top = tl0.y + '%'; 92 if (el.classList.contains('anno-ring')) { 93 el.style.width = (br.x - tl0.x) + '%'; 94 el.style.height = (br.y - tl0.y) + '%'; 95 } 96 }); 97 } 98 /* Warm the pinned-track images just before a section approaches. Only when 99 the pinned variant is active â the static fallback's images stay 100 native-lazy, and the hidden track's images are then never fetched at all. 101 No ScrollTrigger.refresh() here: every image slot reserves its size via 102 aspect-ratio/width/height, so decoding never changes layout â and a 103 refresh during an in-flight smooth scroll would cache wrong positions. */ 104 function setupWarm(section) { 105 var warmed = false;
106 function warm() { 107 if (warmed || !section.classList.contains('story-pinned')) return; 108 warmed = true; 109 section.querySelectorAll('.story-track img[loading="lazy"]').forEach(function (img) { 110 img.loading = 'eager'; 111 if (img.decode) img.decode().catch(function () {}); 112 }); 113 } 114 if ('IntersectionObserver' in window) { 115 var io = new IntersectionObserver(function (entries) { 116 if (entries.some(function (e) { return e.isIntersecting; })) { warm(); io.disconnect(); } 117 }, { rootMargin: '150% 0%' }); 118 io.observe(section); 119 } else { warm(); } 120 } 121 /* Progress rail: light the labels up to the last passed threshold. */ 122 function railUpdater(items, thresholds) { 123 var state = -1; 124 var fn = function (self) { 125 var t = self.progress * 100, active = -1; 126 for (var i = 0; i < thresholds.length; i++) if (t >= thresholds[i]) active = i; 127 if (active === state) return; 128 state = active; 129 items.forEach(function (li, i) { li.classList.toggle('on', i <= active); }); 130 }; 131 fn.reset = function () { state = -1; items.forEach(function (li) { li.classList.remove('on'); }); }; 132 return fn; 133 } 134 function makeTimeline(track, onUpdate) { 135 return gsap.timeline({ 136 defaults: { ease: 'none' }, 137 scrollTrigger: { 138 trigger: track, 139 start: 'top top', 140 end: 'bottom bottom', 141 scrub: 0.7, 142 invalidateOnRefresh: true, 143 onUpdate: onUpdate 144 } 145 }); 146 } 147 148 if (story1) setupWarm(story1); 149 if (story2) setupWarm(story2); 150 151 /* ================ story 1: "Scoped by design" ================ */ 152 function buildScopedStory(section) { 153 section.classList.add('story-pinned'); 154 155 var CAM = { 156 open: { s: 1.00, fx: .50, fy: .50 }, 157 settle: { s: 1.04, fx: .50, fy: .47 }, 158 hiveTile: { s: 1.35, fx: .41, fy: .31 }, // MyShop tile in the Hives card 159 contexts: { s: 1.50, fx: .47, fy: .50 }, // "Contexts in MyShop" panel 160 dive: { s: 2.10, fx: .385, fy: .575 }, // Administration tile (the portal) 161 adminFull: { s: 1.06, fx: .50, fy: .50 }, // whole Administration context 162 composer: { s: 1.32, fx: .50, fy: .47 }, // Write-a-message modal 163 composerL: { s: 1.32, fx: .46, fy: .47 }, // modal eased left, room for 04 164 agent: { s: 1.22, fx: .40, fy: .34 }, // personal-agent banner region 165 apps: { s: 1.18, fx: .47, fy: .60 } // Apps card at the bottom 166 }; 167 /* Annotation regions as fractions of the underlying screenshot 168 [x0, y0, x1, y1], each tied to the camera state it is shown at. */ 169 var ANNO = { 170 groups: { cam: CAM.adminFull, r: [.315, .425, .665, .570] }, 171 people: { cam: CAM.adminFull, r: [.080, .720, .300, .960] }, 172 ctxin: { cam: CAM.adminFull, r: [.315, .590, .665, .830] }, 173 apps: { cam: CAM.adminFull, r: [.315, .875, .665, .995] }, 174 writing: { cam: CAM.composerL, r: [.235, .250, .395, .307] }, 175 incontext: { cam: CAM.composerL, r: [.500, .250, .705, .307] }, 176 togroups: { cam: CAM.composerL, r: [.235, .315, .490, .398] }, 177 peoplein: { cam: CAM.composerL, r: [.500, .322, .660, .380] }, 178 agentsrc: { cam: CAM.agent, r: [.318, .205, .665, .385] } 179 }; 180 var RAIL_AT = [12, 26, 42, 58, 67, 82]; 181 182 var q = gsap.utils.selector(section); 183 var cam = q('.story-cam')[0]; 184 var frame = q('.story-frame')[0]; 185 var veil = q('.story-veil')[0]; 186 var steps = q('.story-step'); 187 var shot = function (n) { return q('.story-shot[data-shot="' + n + '"]')[0]; }; 188 var f04 = q('.story-float-04')[0], f05 = q('.story-float-05')[0], f06 = q('.story-float-06')[0]; 189 var fw = function (k) { return function () { return frame.offsetWidth * k; }; }; 190 var fh = function (k) { return function () { return frame.offsetHeight * k; }; }; 191 192 placeAnnos(q, ANNO); 193 var anno = function (key) { return q('[data-anno="' + key + '"]')[0]; }; 194 var railItems = q('[data-tree]'); 195 if (!railItems.length) railItems = q('.story-rail li'); 196 var rail = railUpdater(railItems, RAIL_AT);
197 var tl = makeTimeline(q('.story-track')[0], rail); 198 199 var stepIn = function (i, t) { tl.fromTo(steps[i], { autoAlpha: 0, y: 16 }, { autoAlpha: 1, y: 0, duration: 2.2, ease: 'power1.out' }, t); }; 200 var stepOut = function (i, t) { tl.to(steps[i], { autoAlpha: 0, y: -12, duration: 1.8, ease: 'power1.in' }, t); }; 201 var annoIn = function (k, t, d) { tl.to(anno(k), { autoAlpha: 1, duration: d || 1.6 }, t); }; 202 var annoOut = function (k, t) { tl.to(anno(k), { autoAlpha: 0, duration: 1.4 }, t); }; 203 204 /* ---- 0â12: SweetHive. A composed product shot, barely breathing. ---- */ 205 tl.set(cam, camVars(CAM.open), 0) 206 .to(cam, Object.assign({ duration: 12 }, camVars(CAM.settle)), 0); 207 stepIn(0, 0.5); stepOut(0, 10.5); 208 209 /* ---- 12â26: a hive contains contexts (panel expands in place). ---- */ 210 tl.to(shot('01'), { autoAlpha: 1, duration: 3.5 }, 12) 211 .to(cam, Object.assign({ duration: 5, ease: 'power1.inOut' }, camVars(CAM.hiveTile)), 12) 212 .to(cam, Object.assign({ duration: 7, ease: 'power1.inOut' }, camVars(CAM.contexts)), 18); 213 stepIn(1, 13.5); stepOut(1, 24.5); 214 215 /* ---- 26â41: dive through the Administration tile into the context. ---- */ 216 tl.to(cam, Object.assign({ duration: 4.5, ease: 'power1.in' }, camVars(CAM.dive)), 26) 217 /* soft paper wash over the swap peak, like a rack focus between spaces */ 218 .to(veil, { opacity: .55, duration: 1.7, ease: 'power1.in' }, 28.6) 219 .to(shot('02'), { autoAlpha: 1, duration: 2 }, 29.5) 220 .to(veil, { opacity: 0, duration: 2.2, ease: 'power1.out' }, 30.4) 221 .to(cam, Object.assign({ duration: 6.5, ease: 'power2.out' }, camVars(CAM.adminFull)), 30.5); 222 stepIn(2, 32.5); stepOut(2, 40.5); 223 annoIn('groups', 34); annoIn('people', 35); annoIn('ctxin', 36); annoIn('apps', 37); 224 annoOut('groups', 40); annoOut('people', 40); annoOut('ctxin', 40); annoOut('apps', 40); 225 226 /* ---- 42â57: the composer â scope is explicit before content exists. ---- */ 227 tl.to(cam, Object.assign({ duration: 4, ease: 'power1.inOut' }, camVars(CAM.composer)), 41.5) 228 .to(shot('03'), { autoAlpha: 1, duration: 3 }, 42); 229 stepIn(3, 43.5); stepOut(3, 56); 230 annoIn('writing', 46); tl.to(anno('writing'), { autoAlpha: .45, duration: 1.2 }, 49); 231 annoIn('incontext', 49); tl.to(anno('incontext'), { autoAlpha: .45, duration: 1.2 }, 52); 232 annoIn('togroups', 52); tl.to(anno('togroups'), { autoAlpha: .45, duration: 1.2 }, 55); 233 annoIn('peoplein', 55); 234 ['writing', 'incontext', 'togroups', 'peoplein'].forEach(function (k) { annoOut(k, 57); }); 235 236 /* ---- 57â67: group permissions rise from the To-groups region. ---- */ 237 tl.to(cam, Object.assign({ duration: 3, ease: 'power1.inOut' }, camVars(CAM.composerL)), 57) 238 .to(veil, { opacity: .32, duration: 2.5 }, 58) 239 .fromTo(f04, 240 { x: fw(-.42), y: fh(.12), scale: .55, autoAlpha: 0 }, 241 { x: 0, y: 0, scale: 1, autoAlpha: 1, duration: 3, ease: 'power2.out' }, 58.5); 242 stepIn(4, 59); stepOut(4, 66); 243 tl.to(f04, { autoAlpha: 0, scale: .96, duration: 1.5, ease: 'power1.in' }, 66.3); 244 245 /* ---- 67â81: the modal closes; the agent belongs to this context. ---- */ 246 tl.to(shot('03'), { autoAlpha: 0, duration: 2.5 }, 67) 247 .to(veil, { opacity: .15, duration: 2 }, 67) 248 .to(cam, Object.assign({ duration: 4, ease: 'power1.inOut' }, camVars(CAM.agent)), 67) 249 .fromTo(f05, 250 { x: fw(-.18), y: fh(.16), scale: .5, autoAlpha: 0 }, 251 { x: 0, y: 0, scale: 1, autoAlpha: 1, duration: 3.5, ease: 'power2.out' }, 69.5); 252 annoIn('agentsrc', 70.5); annoOut('agentsrc', 79.5); 253 stepIn(5, 71); stepOut(5, 80.5); 254 255 /* ---- 81â100: apps live here too; end on a stable composition. ---- */ 256 tl.to(f05, { autoAlpha: 0, scale: .95, x: fw(-.04), duration: 2.5, ease: 'power1.in' }, 81.5) 257 .to(cam, Object.assign({ duration: 5, ease: 'power1.inOut' }, camVars(CAM.apps)), 82) 258 .to(veil, { opacity: .2, duration: 2.5 }, 83) 259 .fromTo(f06, 260 { x: fw(-.10), y: fh(.34), scale: .6, autoAlpha: 0 }, 261 { x: 0, y: 0, scale: 1, autoAlpha: 1, duration: 4, ease: 'power2.out' }, 84); 262 stepIn(6, 86.5); 263 tl.to(q('.fanno-crm')[0], { autoAlpha: 1, duration: 1.5 }, 89.5) 264 .to(q('.fanno-reach')[0], { autoAlpha: 1, duration: 1.5 }, 91.5) 265 /* breathing room: nothing moves from 93 to 100 */ 266 .to({}, { duration: 7 }, 93); 267 268 return function () { 269 section.classList.remove('story-pinned'); 270 rail.reset(); 271 }; 272 } 273 274 /* ================ story 2: "Local AI" ================ */ 275 function buildLocalStory(section) { 276 section.classList.add('story-pinned'); 277 278 var CAM2 = { 279 wide: { s: 1.24, fx: .49, fy: .33 }, // Administration with the agent banner 280 banner: { s: 1.45, fx: .49, fy: .30 } // pushed onto the banner itself 281 }; 282 var ANNO2 = { 283 banner2: { cam: CAM2.banner, r: [.318, .205, .665, .385] } 284 }; 285 var RAIL2_AT = [0, 14, 38, 58]; 286 287 var q = gsap.utils.selector(section); 288 var cam = q('.story-cam')[0]; 289 var frame = q('.story-frame')[0]; 290 var veil = q('.story-veil')[0]; 291 var steps = q('.story-step'); 292 var f07 = q('.story-float-07')[0]; 293 var node = q('.story-nodewin')[0]; 294 var shot09 = q('.shot09')[0]; 295 var fw = function (k) { return function () { return frame.offsetWidth * k; }; }; 296 var fh = function (k) { return function () { return frame.offsetHeight * k; }; }; 297 298 placeAnnos(q, ANNO2); 299 var rail = railUpdater(q('.story-rail li'), RAIL2_AT);
300 var tl = makeTimeline(q('.story-track')[0], rail); 301 302 var stepIn = function (i, t) { tl.fromTo(steps[i], { autoAlpha: 0, y: 16 }, { autoAlpha: 1, y: 0, duration: 2.2, ease: 'power1.out' }, t); }; 303 var stepOut = function (i, t) { tl.to(steps[i], { autoAlpha: 0, y: -12, duration: 1.8, ease: 'power1.in' }, t); }; 304 305 /* ---- 0â14: the banner every context shows. ---- */ 306 tl.set(cam, camVars(CAM2.wide), 0) 307 .to(cam, Object.assign({ duration: 12, ease: 'power1.inOut' }, camVars(CAM2.banner)), 2) 308 .to(q('[data-anno="banner2"]')[0], { autoAlpha: 1, duration: 1.8 }, 4) 309 .to(q('[data-anno="banner2"]')[0], { autoAlpha: 0, duration: 1.4 }, 15); 310 stepIn(0, 1); stepOut(0, 12.5); 311 312 /* ---- 14â36: the install dialog rises from the banner's button. ---- */ 313 tl.fromTo(f07, 314 { x: fw(-.13), y: fh(.02), scale: .45, autoAlpha: 0 }, 315 { x: 0, y: 0, scale: 1, autoAlpha: 1, duration: 4.5, ease: 'power2.out' }, 14.5); 316 stepIn(1, 16.5); stepOut(1, 34); 317 318 /* ---- 36â56: the cloud recedes; the Agents Node window is the hero. ---- */ 319 tl.to(veil, { opacity: .55, duration: 4 }, 36) 320 .to(f07, { autoAlpha: 0, scale: .96, y: fh(-.04), duration: 3, ease: 'power1.in' }, 38) 321 .fromTo(node, 322 { y: fh(.35), scale: .72, autoAlpha: 0 }, 323 { y: 0, scale: 1, autoAlpha: 1, duration: 6.5, ease: 'power2.out' }, 39.5); 324 stepIn(2, 42.5); stepOut(2, 55); 325 326 /* ---- 56â100: local models â local chat (aligned crossfade), then hold. ---- */ 327 tl.to(shot09, { autoAlpha: 1, duration: 4.5 }, 58); 328 stepIn(3, 60.5); 329 tl.to(q('.ring-agent')[0], { autoAlpha: 1, duration: 1.8 }, 64) 330 .to(q('.ring-agent')[0], { autoAlpha: 0, duration: 1.6 }, 84) 331 /* breathing room: nothing moves from 86 to 100 */ 332 .to({}, { duration: 14 }, 86); 333 334 return function () { 335 section.classList.remove('story-pinned'); 336 rail.reset(); 337 }; 338 } 339 340 var mm = gsap.matchMedia(); 341 mm.add('(min-width: 920px) and (prefers-reduced-motion: no-preference)', function () { 342 var cleanups = []; 343 if (story1) cleanups.push(buildScopedStory(story1)); 344 if (story2) cleanups.push(buildLocalStory(story2)); 345 return function () { cleanups.forEach(function (fn) { fn(); }); }; 346 }); 347})();
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.