PageSourceSearch

https://sweethive.com/assets/sh-story.js?v=20260827b

js sweethive.com collected 2026-10-01 23:56:11 UTC 16,897 bytes, 347 lines download raw bytes

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.