ZAKAI LAB
ANIMATION LIBRARY

IDEAS
DESIGN
TECHNOLOGY
PEOPLE

MOTION
FOR A
BRIGHTER WEB

03

LAB Neural Wander

AI flows. Ideas connect.
Scroll
GSAP
Backgrounds

01 / הרעיון

כל סיפור
מתחיל בנקודה.

גללו מטה. האור במסילה מתקדם עם הסיפור, והתחנה הפעילה מסמנת באיזה פרק אנחנו נמצאים.

אל הפרק הבא ↓

02 / הכיוון

הנקודה
מוצאת כיוון.

כל תחנה במסילה מחוברת לפרק בעמוד.
האור נע בין התחנות בהתאם להתקדמות הגלילה,
ומחבר את חלקי הסיפור.


ממשיכים בתנועה ↓

03 / התנועה

הכיוון
הופך לתנועה.

אפשר לגלול קדימה ואחורה ולראות את האור נע במסילה, או ללחוץ על אחת התחנות כדי לעבור ישירות לפרק הרצוי.

אל החיבור ↓

04 / החיבור

והתנועה
מחברת הכול.

האור מגיע לתחנה האחרונה והסיפור נשלם. אפשר לחזור לכל אחד מהפרקים דרך המסילה, או להתחיל שוב מהנקודה הראשונה.

בחזרה לרעיון ↑

				
					<script>
(() => {
  'use strict';
  window.ZakaiChapterRail?.destroy?.();
  window.ZakaiStoryRail?.destroy?.();
  let disposed = false;
  const cleanup = [];

  function init() {
    if (disposed) return;
    const ids = ['lab-story-01', 'lab-story-02', 'lab-story-03', 'lab-story-04'];
    const names = ['הרעיון', 'הכיוון', 'התנועה', 'החיבור'];
    const items = ids.map((id, i) => ({
      id, name: names[i], top: 0,
      section: document.querySelector('.zkl-story-chapter#' + id)
        || document.getElementById(id)
    })).filter(item => item.section);

    if (!items.length) {
      console.warn('Zakai: לא נמצאו קונטיינרים עם מזהי הפרקים.');
      return;
    }

    // החלפת המסילות הישנות בעמוד זה.
    document.querySelectorAll('.zkl-chapter-rail, #zks-story-rail')
      .forEach(el => el.remove());
    document.getElementById('zks-story-rail-css')?.remove();

    const style = document.createElement('style');
    style.id = 'zks-story-rail-css';
    style.textContent = `
      #zks-story-rail {
        --zks-offset: 182px;
        position: fixed !important;
        top: max(140px, 23vh) !important;
        bottom: max(40px, 10vh) !important;
        left: 20px !important;
        width: 72px !important;
        height: auto !important;
        min-height: 0 !important;
        margin: 0 !important;
        padding: 0 !important;
        box-sizing: border-box !important;
        z-index: 90;
        direction: ltr;
        background: linear-gradient(180deg,#483a29,#2d241c 52%,#3b2f21);
        border: 1px solid #b28b4c;
        border-radius: 36px;
        box-shadow: 0 8px 24px #00000035;
        pointer-events: none;
      }
      #zks-story-rail .zks-caption {
        position: absolute; top: 20px; left: 0; right: 0;
        color: #ffe6a1 !important; text-align: center;
        font: 12px/1.5 Arial,sans-serif;
      }
      #zks-story-rail .zks-track {
        position: absolute !important;
        top: 64px !important; bottom: 36px !important;
        left: 18px !important; width: 36px !important;
        height: auto !important; margin: 0 !important;
      }
      #zks-story-rail svg {
        position: absolute !important; inset: 0 !important;
        display: block !important;
        width: 100% !important; height: 100% !important;
        max-width: none !important; max-height: none !important;
        overflow: visible !important;
      }
      #zks-story-rail path { fill: none !important; }
      #zks-story-rail .zks-base { stroke: #bd9655 !important; }
      #zks-story-rail .zks-fill { stroke: #dda532 !important; }
      #zks-story-rail .zks-tail { stroke: #ffe6a1 !important; }
      #zks-story-rail .zks-head {
        fill: #ffe6a1 !important;
        filter: drop-shadow(0 0 5px #dda532);
      }
      #zks-story-rail .zks-stop {
        position: absolute; width: 44px; height: 44px;
        display: flex; align-items: center; justify-content: center;
        transform: translate(-50%,-50%);
        pointer-events: auto; text-decoration: none !important;
        border: 0; background: transparent;
        font: 12px/1 Arial,sans-serif;
      }
      #zks-story-rail .zks-dot {
        box-sizing: border-box; width: 34px; height: 34px;
        display: flex; align-items: center; justify-content: center;
        border: 2px solid #c1a16c; border-radius: 50%;
        background: #28221e; color: #fff2d8 !important;
      }
      #zks-story-rail .zks-stop[aria-current] .zks-dot {
        background: #ffe6a1; color: #2e2417 !important;
        border-color: #ffe6a1;
        box-shadow: 0 0 0 5px #dda53226;
      }
      #zks-story-rail .zks-label {
        position: absolute; left: 48px;
        direction: rtl; white-space: nowrap;
        padding: 7px 10px; border: 1px solid #826642;
        border-radius: 999px; background: #28221e;
        color: #fff2d8 !important; font: 12px/1.3 Arial,sans-serif;
        pointer-events: none;
      }
      #zks-story-rail .zks-stop:focus-visible {
        outline: 2px solid #ffe6a1; outline-offset: 2px;
        border-radius: 50%;
      }
      @media(max-width:767px) {
        #zks-story-rail {
          --zks-offset: 110px;
          left: 10px !important; width: 48px !important;
          top: 90px !important; bottom: auto !important;
          height: 420px !important;
          max-height: calc(100vh - 150px) !important;
          max-height: calc(100dvh - 150px) !important;
        }
        #zks-story-rail .zks-track {
          left: 8px !important; width: 30px !important;
          top: 55px !important; bottom: 30px !important;
        }
        #zks-story-rail .zks-caption { top: 16px; font-size: 11px; }
        #zks-story-rail .zks-dot { width: 30px; height: 30px; }
        #zks-story-rail .zks-label { opacity: 0; }
        #zks-story-rail .zks-stop:hover .zks-label,
        #zks-story-rail .zks-stop:focus-visible .zks-label { opacity: 1; }
      }
      @media(min-width:768px) and (max-height:500px) {
        #zks-story-rail { top: 80px !important; bottom: 20px !important; }
        #zks-story-rail .zks-track {
          top: 45px !important; bottom: 25px !important;
        }
      }
      @media(prefers-reduced-motion:reduce) {
        #zks-story-rail .zks-tail { display: none; }
        #zks-story-rail .zks-head { filter: none; }
      }
    `;
    document.head.appendChild(style);

    const rail = document.createElement('nav');
    rail.id = 'zks-story-rail';
    rail.setAttribute('aria-label', 'ניווט בין פרקי העמוד');
    const d = 'M14 18 C4 125 24 220 14 325 S4 530 14 610 S24 815 14 982';
    rail.innerHTML = `
      <span class="zks-caption" aria-hidden="true">פרקים</span>
      <div class="zks-track">
        <svg viewBox="0 0 28 1000" preserveAspectRatio="none" aria-hidden="true">
          <path class="zks-base" d="${d}" fill="none" stroke="#bd9655" stroke-width="1.25" />
          <path class="zks-fill" d="${d}" fill="none" stroke="#dda532" stroke-width="1.8" />
          <path class="zks-tail" d="${d}" fill="none" stroke="#ffe6a1" stroke-width="2.6" stroke-linecap="round" />
          <circle class="zks-head" cx="14" cy="18" r="3.5" fill="#ffe6a1" />
        </svg>
        ${items.map((item,i) => `<a class="zks-stop" href="#${item.id}">
          <span class="zks-dot">${String(i+1).padStart(2,'0')}</span>
          <span class="zks-label">${item.name}</span></a>`).join('')}
      </div>`;
    document.body.appendChild(rail);

    const path = rail.querySelector('.zks-base');
    const fill = rail.querySelector('.zks-fill');
    const tail = rail.querySelector('.zks-tail');
    const head = rail.querySelector('.zks-head');
    const links = [...rail.querySelectorAll('.zks-stop')];
    items.forEach((item,i) => item.link = links[i]);
    const length = path.getTotalLength();
    const motion = matchMedia('(prefers-reduced-motion: reduce)');
    let raf = 0, dirty = true, shown = null, last = null, active = -1;
    let pending = null;
    const softness = 160;
    const scrollY = () => (document.scrollingElement || document.documentElement).scrollTop;

    function measure() {
      const y = scrollY();
      const offset = parseFloat(getComputedStyle(rail).getPropertyValue('--zks-offset')) || 0;
      const max = Math.max(0, document.documentElement.scrollHeight - innerHeight);
      items.forEach(item => item.top = Math.min(max, Math.max(0,
        item.section.getBoundingClientRect().top + y - offset)));
      items.sort((a,b) => a.top - b.top);
      items.forEach((item,i) => {
        const point = path.getPointAtLength(items.length > 1 ? length*i/(items.length-1) : 0);
        item.link.style.left = (point.x/28*100) + '%';
        item.link.style.top = (point.y/1000*100) + '%';
      });
      dirty = false;
    }
    function render(now) {
      raf = 0;
      if (disposed) return;
      if (dirty) measure();
      const y = scrollY();
      let index = 0;
      for (let i=1; i<items.length; i++) if (y >= items[i].top) index = i;
      const next = items[index+1];
      const span = next ? next.top-items[index].top : 0;
      const t = span > 0 ? Math.max(0,Math.min(1,(y-items[index].top)/span)) : 0;
      const target = items.length > 1 ? (index+t)/(items.length-1) : 0;
      const dt = last === null ? 16.7 : Math.max(0,Math.min(64,now-last));
      last = now;
      if (shown === null || motion.matches) shown = target;
      else shown += (target-shown)*(1-Math.exp(-dt/softness));
      const moving = Math.abs(target-shown) > 0.00005;
      if (!moving) shown = target;
      const point = path.getPointAtLength(length*shown);
      head.setAttribute('cx',point.x);
      head.setAttribute('cy',point.y);
      fill.style.strokeDasharray = `${length} ${length}`;
      fill.style.strokeDashoffset = String(length*(1-shown));
      const size = Math.min(75,length*shown);
      tail.style.strokeDasharray = `${size} ${length}`;
      tail.style.strokeDashoffset = String(-Math.max(0,length*shown-size));
      if (index !== active) {
        active = index;
        items.forEach((item,i) => {
          if(i === index) item.link.setAttribute('aria-current','location');
          else item.link.removeAttribute('aria-current');
        });
        document.dispatchEvent(new CustomEvent('zkl:chapterchange', {
          detail: {index,total:items.length,name:items[index].name,id:items[index].id}
        }));
      }
      if (pending && Math.abs(y-pending.top)<3) {
        const heading = pending.section.querySelector('h1,h2,h3') || pending.section;
        if (!heading.hasAttribute('tabindex')) heading.setAttribute('tabindex','-1');
        heading.focus({preventScroll:true});
        pending = null;
      }
      if (moving && !motion.matches) raf = requestAnimationFrame(render);
      else last = null;
    }
    function schedule() { if (!disposed && !raf) raf = requestAnimationFrame(render); }
    function refresh() { dirty = true; schedule(); }
    function cancelPending() { pending = null; }
    function listen(el,type,fn,options) {
      el.addEventListener(type,fn,options);
      cleanup.push(() => el.removeEventListener(type,fn,options));
    }
    items.forEach(item => listen(item.link,'click',event => {
      if(event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
      event.preventDefault(); measure(); pending = item;
      window.scrollTo({top:item.top,behavior:motion.matches?'instant':'smooth'});
      schedule();
    }));
    listen(window,'scroll',schedule,{passive:true});
    listen(window,'resize',refresh);
    listen(window,'pageshow',refresh);
    listen(window,'wheel',cancelPending,{passive:true});
    listen(window,'touchstart',cancelPending,{passive:true});
    listen(motion,'change',refresh);
    const observer = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(refresh) : null;
    observer?.observe(document.body);
    items.forEach(item => observer?.observe(item.section));
    document.fonts?.ready.then(() => { if (!disposed) refresh(); });
    cleanup.push(() => {
      cancelAnimationFrame(raf); observer?.disconnect(); rail.remove(); style.remove();
    });
    render(performance.now());
  }
  window.ZakaiStoryRail = {
    destroy() {
      disposed = true;
      document.removeEventListener('DOMContentLoaded',init);
      cleanup.splice(0).forEach(fn => fn());
    }
  };
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded',init,{once:true});
  else init();
})();
</script>

				
			
				
					/* קונטיינרים של הפרקים */
.zkl-story-chapter {
  position: relative; isolation: isolate; box-sizing: border-box;
  overflow: hidden; min-height: 420px;
  padding: clamp(36px, 5vw, 64px);
  display: flex; flex-direction: row; align-items: center;
  justify-content: center; gap: 32px; direction: rtl;
  background: #181511 !important;
  border: 1px solid rgba(218,182,109,.18); border-radius: 20px;
}
#lab-story-02 { background: #211b15 !important; }
#lab-story-03 { background: #171410 !important; }
#lab-story-04 { background: #241d16 !important; }
.zkl-story-chapter::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  pointer-events: none;
  background: radial-gradient(ellipse at 18% 50%, rgba(221,165,50,.14), transparent 65%);
}
#lab-story-02::before {
  background: radial-gradient(ellipse at 75% 20%, rgba(221,165,50,.1), transparent 65%);
}
#lab-story-04::before {
  background: radial-gradient(ellipse at 25% 75%, rgba(229,111,45,.12), transparent 65%);
}
.zkl-story-chapter::after {
  position: absolute; left: 7%; bottom: 20px; z-index: -1;
  pointer-events: none; font-family: Arial, sans-serif;
  font-size: clamp(100px,14vw,180px); font-weight: 400;
  line-height: 1; color: rgba(218,182,109,.07);
}
#lab-story-01::after { content: "01"; }
#lab-story-02::after { content: "02"; }
#lab-story-03::after { content: "03"; }
#lab-story-04::after { content: "04"; }
.zkl-story-chapter .zkl-story-art::after,
.zkl-story-chapter .zkl-story-copy::after {
  content: none !important; display: none !important;
}
.zkl-story-chapter .zkl-story-copy {
  position: relative; z-index: 1; flex: 1 1 0;
  min-width: 0; width: 100%; max-width: 620px;
  direction: rtl; text-align: right;
}
.zkl-story-chapter .zkl-story-art {
  position: relative; flex: 0 1 35%; min-width: 0;
  min-height: 220px; display: flex;
  align-items: center; justify-content: center;
}
.zkl-story-art .elementor-widget-html { width: 100%; }
.zkl-story-copy .zkl-story-kicker {
  margin: 0 0 20px; color: #f0ca83 !important;
  font-size: 13px !important; font-weight: 400; line-height: 1.5;
}
.zkl-story-copy .zkl-story-title {
  margin: 0 0 24px; color: #fff6e8 !important;
  font-size: clamp(32px,3.8vw,52px) !important;
  font-weight: 400 !important; line-height: 1.16 !important;
  letter-spacing: -.6px;
}
.zkl-story-copy .zkl-story-text {
  max-width: 460px; margin: 0 0 28px;
  color: #e0d3c1 !important; font-size: 17px !important;
  line-height: 1.8 !important;
}
.zkl-story-copy .zkl-story-next {
  display: inline-block; padding: 10px 0;
  color: #f0ca83 !important; font-size: 14px !important;
  line-height: 1.5; text-decoration: none;
  border-bottom: 1px solid #a17b40;
  transition: color .2s, border-color .2s;
}
.zkl-story-copy .zkl-story-next:hover {
  color: #fff6e8 !important; border-bottom-color: #fff6e8;
}
.zkl-story-copy .zkl-story-next:focus-visible {
  outline: 2px solid #f0ca83; outline-offset: 5px;
}
/* תגובה עדינה למעבר עכבר על הפרק */
.zkl-story-chapter .zkl-story-art svg {
  transform: scale(1) rotate(0deg); transform-origin: center;
  filter: drop-shadow(0 0 0 transparent);
  transition: transform .7s cubic-bezier(.22,1,.36,1), filter .7s ease;
}
.zkl-story-chapter .zkl-story-art svg > circle:first-of-type {
  opacity: .55; transition: opacity .7s ease;
}
@media (hover: hover) and (pointer: fine) {
  .zkl-story-chapter:hover .zkl-story-art svg {
    transform: scale(1.07) rotate(-5deg);
    filter: drop-shadow(0 0 12px rgba(240,202,131,.28));
  }
  #lab-story-02:hover .zkl-story-art svg { transform: scale(1.07) rotate(7deg); }
  #lab-story-03:hover .zkl-story-art svg { transform: scale(1.07) rotate(-9deg); }
  #lab-story-04:hover .zkl-story-art svg { transform: scale(1.07) rotate(5deg); }
  .zkl-story-chapter:hover .zkl-story-art svg > circle:first-of-type { opacity: 1; }
}
.zkl-story-chapter:focus-within .zkl-story-art svg {
  transform: scale(1.07) rotate(-5deg);
  filter: drop-shadow(0 0 12px rgba(240,202,131,.28));
}
.zkl-story-chapter:focus-within .zkl-story-art svg > circle:first-of-type { opacity: 1; }
@media (max-width: 767px) {
  .zkl-story-chapter {
    min-height: 0; padding: 36px 24px;
    border-radius: 16px; flex-direction: column; gap: 24px;
  }
  .zkl-story-chapter .zkl-story-copy,
  .zkl-story-chapter .zkl-story-art { flex: 0 1 auto; width: 100%; }
  .zkl-story-copy .zkl-story-title { font-size: clamp(28px,7vw,36px) !important; }
  .zkl-story-copy .zkl-story-text { font-size: 16px !important; line-height: 1.7 !important; }
  .zkl-story-chapter::after { font-size: 100px; left: 18px; bottom: 16px; }
}
@media (prefers-reduced-motion: reduce) {
  .zkl-story-chapter .zkl-story-art svg { transform: none !important; transition: none; }
  .zkl-story-chapter .zkl-story-art svg > circle:first-of-type { transition: none; }
}

				
			
				
					<!-- פרק 01 -->
<div class="zkl-story-chapter" id="lab-story-01">
  <div class="zkl-story-copy">
    <p class="zkl-story-kicker">01 / שם הפרק</p>
    <h2 class="zkl-story-title">כותרת הפרק</h2>
    <p class="zkl-story-text">תוכן הפרק</p>
    <a class="zkl-story-next" href="#lab-story-02">
      לפרק הבא ↓
    </a>
  </div>

  <div class="zkl-story-art">
    <!-- האיור או התמונה שלכם -->
  </div>
</div>

<!-- פרק 02 -->
<div class="zkl-story-chapter" id="lab-story-02">
  <div class="zkl-story-copy">
    <p class="zkl-story-kicker">02 / שם הפרק</p>
    <h2 class="zkl-story-title">כותרת הפרק</h2>
    <p class="zkl-story-text">תוכן הפרק</p>
    <a class="zkl-story-next" href="#lab-story-03">
      לפרק הבא ↓
    </a>
  </div>

  <div class="zkl-story-art">
    <!-- האיור או התמונה שלכם -->
  </div>
</div>

<!-- פרק 03 -->
<div class="zkl-story-chapter" id="lab-story-03">
  <div class="zkl-story-copy">
    <p class="zkl-story-kicker">03 / שם הפרק</p>
    <h2 class="zkl-story-title">כותרת הפרק</h2>
    <p class="zkl-story-text">תוכן הפרק</p>
    <a class="zkl-story-next" href="#lab-story-04">
      לפרק הבא ↓
    </a>
  </div>

  <div class="zkl-story-art">
    <!-- האיור או התמונה שלכם -->
  </div>
</div>

<!-- פרק 04 -->
<div class="zkl-story-chapter" id="lab-story-04">
  <div class="zkl-story-copy">
    <p class="zkl-story-kicker">04 / שם הפרק</p>
    <h2 class="zkl-story-title">כותרת הפרק</h2>
    <p class="zkl-story-text">תוכן הפרק</p>
    <a class="zkl-story-next" href="#lab-story-01">
      בחזרה להתחלה ↑
    </a>
  </div>

  <div class="zkl-story-art">
    <!-- האיור או התמונה שלכם -->
  </div>
</div>
				
			

מה אפשר לשנות.. ואיך ⚙️

EXPLORE / LEARN / REUSE / CREATE

.Costomize to fit your vision

המסילה נוצרת אוטומטית באמצעות JavaScript. אין צורך להוסיף HTML נפרד למסילה.
באלמנטור אפשר לבנות את ארבעת הפרקים בקונטיינרים רגילים: לכל קונטיינר חיצוני הקלאס zkl-story-chapter ומזהה ייחודי lab-story-01 עד lab-story-04. בתוך כל פרק עורך טקסט עם הקלאס zkl-story-copy וקונטיינר איור עם הקלאס zkl-story-art. האיור מוכנס לווידג׳ט HTML בתוך קונטיינר האיור.
המזהה שייך לקונטיינר החיצוני בלבד. לא לעורך הטקסט ולא לקונטיינר האיור.
קוד ה-HTML מתאר את המבנה המלא. משתמשים בו בשלמותו בווידג׳ט HTML, או מפרקים אותו לרכיבי אלמנטור לפי המבנה המתואר. אין להדביק את המבנה המלא בעורך טקסט.
את ה-CSS מוסיפים פעם אחת. אם מדביקים בווידג׳ט HTML, עוטפים אותו בתגי style. בשדה CSS של אלמנטור מדביקים בלי תגיות.
את ה-JS מוסיפים פעם אחת בווידג׳ט HTML, עם תגי script כפי שסופק. הוא כולל גם את העיצוב של המסילה. מחליפים את הסקריפט הקודם של רכיב זה ולא מריצים שני עותקים.
בנייד המסילה מוגבלת ל-420 פיקסלים או לגובה הזמין במסך, הקטן מביניהם. הפרקים עוברים לפריסה אנכית ללא גובה מינימלי גדול.
התנועה של האור מחוברת לגלילה עם ריכוך. לחיצה על תחנה מעבירה לפרק. משתמשים שמעדיפים הפחתת תנועה מקבלים תנועה ישירה ללא הריכוך.

אנימציית האיורים במעבר עכבר עדינה. בנייד היא אינה מופעלת במגע. זו נקודה לשיפור עיצובי בהמשך.
העכבר הכתום וההילה הם רכיב גלובלי נפרד, ואינם כלולים בדוגמה זו.

 ב-HTML; צבעים, ריווח וגדלים ב-CSS;

שמות הפרקים במערך names; מזהי הפרקים במערך ids ובקונטיינרים ובקישורים יחד; ריכוך האור במשתנה softness (160 מילישניות); היסט הגלילה ב–zks-offset לפי גובה ההדר.
המסילה מקובעת לצד המסך לאורך העמוד. בעמוד אמיתי יש להשאיר עבורה שוליים פנויים כדי שלא תכסה תוכן או כפתורים צפים.

הערה חשובה  🚩  

הרכיב פועל ללא ספריות חיצוניות. יש להוסיף את ה־CSS ואת הסקריפט פעם אחת בלבד, ולוודא שמזהי הקונטיינרים תואמים למזהי הפרקים בסקריפט.

מתלבט? בוא/י לדבר איתי בווטסאפ?