נקודה קופצת בין מילים

חוט מותג שהעין זוכרת: נקודה אחת בצבע של העסק קופצת בקשת בין המילים שעטופות ב-em, נמעכת קלות בנחיתה וצובעת את המילה. היא תמיד מראה את הדרך שלה ואף פעם לא קופצת בלי מסלול. המיקום נמדד בכל קפיצה, אז שבירת שורה במובייל לא מזיזה אותה, והיא נחה כשהכותרת מחוץ למסך. נקודה קופצת בין מילים הוא רכיב מתוך רכיבים של כליקס. מה שמורידים הוא רכיב שלם, גם כ-HTML עצמאי וגם כרכיב React. אין ספרייה חיצונית, אין שלב בנייה, ואם כליקס ייעלם מחר הקוד ימשיך לעבוד. מתאים לעמודים ולמסמכים שעוסקים בכותרת, מותג, טיפוגרפיה ובלי ספריות. באותה סדרה יש גם שאלות נפוצות, ציר תהליך וקרוסלת המלצות.

רכיב · כותרת · מותג · טיפוגרפיה · בלי ספריות

<!-- נקודה קופצת: עוטפים את המילים החשובות ב-<em> בתוך אלמנט עם klix-dothop.
     הצבע: style="--klix-dot:#hex" על האלמנט, והקצב: data-every באלפיות שנייה -->
<style>
.klix-dothop{position:relative}
.klix-dothop em{font-style:normal;transition:color .3s ease}
.klix-dothop em.is-on{color:var(--klix-dot,#f97316)}
.klix-dothop-dot{position:absolute;top:0;left:0;width:.32em;height:.32em;border-radius:50%;
  background:var(--klix-dot,#f97316);pointer-events:none;will-change:transform}
</style>

<p class="klix-dothop" data-every="1700" style="font-size:2rem;font-weight:900;line-height:1.5">
  אנחנו <em>מצלמים</em>, <em>עורכים</em> ומעלים <em>לאוויר</em>
</p>

<script>
(function () {
  function off() { return document.documentElement.getAttribute('data-motion') === 'off'; }

  document.querySelectorAll('.klix-dothop').forEach(function (root) {
    var words = root.querySelectorAll('em');
    if (!words.length) return;
    var d = document.createElement('span');
    d.className = 'klix-dothop-dot';
    d.setAttribute('aria-hidden', 'true');
    root.appendChild(d);
    var every = Number(root.getAttribute('data-every') || 1700);

    function spot(w) {
      var r = root.getBoundingClientRect(), b = w.getBoundingClientRect();
      return { x: b.left - r.left + b.width / 2 - d.offsetWidth / 2, y: b.top - r.top - d.offsetHeight * 0.9 };
    }
    function place(p, sx, sy) {
      d.style.transform = 'translate(' + p.x + 'px,' + p.y + 'px) scale(' + (sx || 1) + ',' + (sy || 1) + ')';
    }

    var i = 0, from = spot(words[0]), visible = true;
    words[0].classList.add('is-on');
    place(from);

    function hop() {
      if (!visible || document.hidden || off()) { setTimeout(hop, every); return; }
      words[i].classList.remove('is-on');
      i = (i + 1) % words.length;
      var to = spot(words[i]), start = performance.now(), dur = 520;
      // קשת גבוהה יותר לקפיצה ארוכה יותר
      var lift = Math.max(28, Math.abs(to.x - from.x) * 0.18);
      function step(now) {
        var t = Math.min(Math.max((now - start) / dur, 0), 1);
        var e = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
        place({ x: from.x + (to.x - from.x) * e, y: from.y + (to.y - from.y) * e - Math.sin(Math.PI * e) * lift });
        if (t < 1) { requestAnimationFrame(step); return; }
        place(to, 1.3, 0.7);
        setTimeout(function () { place(to); }, 90);
        words[i].classList.add('is-on');
        from = to;
        setTimeout(hop, every);
      }
      requestAnimationFrame(step);
    }

    if ('IntersectionObserver' in window) {
      new IntersectionObserver(function (en) { visible = en[0].isIntersecting; }).observe(root);
    }
    window.addEventListener('resize', function () { from = spot(words[i]); place(from); });
    setTimeout(hop, every);
  });
})();
</script>