→ לכל המאגר

G105 · מרקי שמתקבע לכותרת

GSAP GSAP · ScrollTrigger טקסט וכותרותהירו ופתיחה ממתין

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

מתי משתמשים: פתיחת סקשן ערכים או שירותים: קודם השטף, אחר כך המסר. פעם אחת בעמוד.

הדמו כאן עיצובי-ניטרלי בכוונה. כשהמהלך נכנס לפרויקט, מיובאת רק ההתנהגות: הצבעים, הרדיוסים, הפונטים והצללים יורשים את העיצוב של אותו פרויקט.

הדמו

גלול למטה, הדמו מגיע ↓
אסטרטגיהקופיעיצובאתר שמביא לקוחותפיתוחמדידהליווי

זה כל מה שאנחנו עושים.

עוד מסלול גלילה. נסה גם חזרה למעלה ↑
הרצועה ממוקמת מהקצה ולא ממורכזת, והעותקים שלה נבנים ב-JS עד שהם מכסים מסך שלם ועוד מחזור, אחרת בקצה הלולאה נפתח חלל ריק. הכיוון של הפריסה ltr כדי שהשכפול ייצמד בצד הנכון, וכל מילה עדיין נקראת בעברית. הנעילה נמדדת ב-getBoundingClientRect ולא ב-offsetLeft, כי ב-RTL עם flip ההיסט הזה משקר. במובייל אותו דבר עם פונט קטן.

קוד להדבקה

הקוד כאן עומד בפני עצמו. כל טוקן עיצוב נושא ברירת מחדל, מחלקות העזר של המאגר מוטמעות בו, ותגי הסקריפט מסודרים לפי הסדר הנכון. פרויקט שמגדיר --accent או --line משלו יורש אותם אוטומטית. הרכיב הזה מסתמך על dir="rtl".

פתח קובץ עצמאי ↗
הצג את הקוד כאן

CSS

/* G105 · מרקי שמתקבע לכותרת */
.mq{position:relative;height:220vh}
.mq-pin{position:sticky;top:0;height:100vh;overflow:hidden}
.mq-track{position:absolute;top:50%;inset-inline-start:0;translate:0 -50%;display:flex;direction:ltr;white-space:nowrap;font-size:clamp(30px,6vw,84px);font-weight:800;will-change:transform}
.mq-set{display:flex;gap:.5em;padding-inline-end:.5em}
.mq-set span{color:var(--muted, #6a6d85);opacity:.38}
.mq-set span::after{content:"·";margin-inline-start:.5em;opacity:.5}
.mq-set span.key{opacity:1;color:var(--ink, #16182b)}
.mq-set span.key.on{color:var(--accent, #4a3aff)}
.mq-sub{position:absolute;bottom:18vh;inset-inline:0;text-align:center;color:var(--muted, #6a6d85);opacity:0}

HTML

<div class="mq">
  <div class="mq-pin">
    <div class="mq-track"><div class="mq-set"><span>אסטרטגיה</span><span>קופי</span><span>עיצוב</span><span class="key">אתר שמביא לקוחות</span><span>פיתוח</span><span>מדידה</span><span>ליווי</span></div></div>
    <p class="mq-sub">זה כל מה שאנחנו עושים.</p>
  </div>
</div>

JS

gsap.registerPlugin(ScrollTrigger);

(function(){
  const track=document.querySelector(".mq-track"),pin=document.querySelector(".mq-pin"),sub=document.querySelector(".mq-sub");
  const base=track.querySelector(".mq-set");
  const mid=()=>{const r=pin.getBoundingClientRect();return r.left+r.width/2;};
  const nearest=()=>{let d=null;track.querySelectorAll(".key").forEach(k=>{const r=k.getBoundingClientRect(),v=mid()-(r.left+r.width/2);if(d===null||Math.abs(v)<Math.abs(d))d=v;});return d;};
  let W=0,loop=null,lock=null;
  function fill(){
    track.querySelectorAll(".mq-set").forEach((n,i)=>{if(i)n.remove();});
    W=base.getBoundingClientRect().width;
    // כמה עותקים צריך כדי שהחלון תמיד יראה תוכן: מסך שלם ועוד מחזור אחד
    const need=Math.ceil((pin.clientWidth+W)/W)+1;
    for(let i=1;i<need;i++){const c=base.cloneNode(true);c.setAttribute("aria-hidden","true");track.appendChild(c);}
  }
  fill();
  if(matchMedia("(prefers-reduced-motion: reduce)").matches){
    gsap.set(track,{x:nearest()});track.querySelectorAll(".key").forEach(k=>k.classList.add("on"));gsap.set(sub,{opacity:1});return;
  }
  function build(){
    loop&&loop.kill();lock=null;fill();gsap.set(track,{x:0});
    loop=gsap.to(track,{x:"-="+W,duration:W/90,ease:"none",repeat:-1,
      modifiers:{x:gsap.utils.unitize(v=>gsap.utils.wrap(-W,0,parseFloat(v)))}});
  }
  build();
  ScrollTrigger.addEventListener("refresh",build);
  ScrollTrigger.create({trigger:".mq",start:"top top",end:"bottom bottom",scrub:.6,onUpdate(s){
    const p=s.progress;
    if(p<.5){
      if(lock){lock=null;loop.play();}
      loop.timeScale(gsap.utils.clamp(0,1,(.5-p)*10));            // מאט עד עצירה מלאה בדיוק בחצי
      track.querySelectorAll(".key").forEach(k=>k.classList.remove("on"));
      gsap.set(sub,{opacity:0});return;
    }
    if(!lock){loop.pause();lock={from:gsap.getProperty(track,"x"),delta:nearest()};}
    const t=gsap.utils.clamp(0,1,(p-.5)*2.5),e=gsap.parseEase("power3.out")(t);
    gsap.set(track,{x:lock.from+lock.delta*e});
    track.querySelectorAll(".key").forEach(k=>k.classList.toggle("on",t>.8));
    gsap.set(sub,{opacity:gsap.utils.clamp(0,1,(t-.7)*3.4)});
  }});
})();