פירוק מלא של אתר גלילה אופקי קולנועי בעברית: הקונספט, מנוע הגלילה, הטיפוגרפיה הקינטית, הגליפים המצוירים והאטמוספרה — כולל קוד אמיתי מתוך האתר.
תולדות האות העברית הן סיפור של תנועה — ממכרות סיני לירושלים, מבבל לקומראן, מדפוס איטלקי למסך. לכן הפורמט הוא מסע גלילה אופקי: תשעה פאנלים ברוחב מסך מלא, שכל אחד מהם פרק בזמן.
ההחלטה הקריטית: עברית נקראת מימין לשמאל, ולכן גם ציר הזמן נע מימין לשמאל. הפאנל הראשון עומד בקצה הימני, והמצלמה ״פאנה״ שמאלה ככל שגוללים. גלילה אנכית רגילה (הז׳סטה שכולם מכירים) מתורגמת לתנועה אופקית — כך מקבלים קולנועיות בלי לשבור את הרגלי המשתמש.
הכתובות הפרוטו־סינאיות (~1800 לפנה״ס), לוח גזר וכתובת השילוח, המעבר לכתב אשורי (סנהדרין כא ע״ב), מגילת ישעיהו (7.34 מ׳, 54 טורים), דפוס ריג׳ו די קלבריה 1475 של אברהם בן גרטון, והגופנים פרנק־ריהל (1908), דוד (1954), הדסה וקורן (1958). ״כתב רש״י״ אכן מעולם לא היה כתב ידו של רש״י.
הזהב הוא הצבע הדומיננטי; החרס והטורקיז מתחלפים כ־accent פר־פרק (הטורקיז שייך לפרק סיני — מחווה למכרות הטורקיז שבהם נולד האלף־בית). כל פאנל מגדיר --acc משלו, וה־CSS צובע קיקר, הדגשות וזוהר דרך המשתנה הזה בלבד.
הטריק המרכזי: ב־dir="rtl" פלקס מסדר את הפאנלים מימין לשמאל באופן טבעי — הפאנל הראשון בקצה הימני והבאים נערמים שמאלה, מחוץ למסך. כדי לחשוף אותם מזיזים את המסילה ב־X חיובי (ימינה), וכך המצלמה פאנה שמאלה:
// 1 יחידת duration == 1 פיקסל של מסע אופקי
const dist = track.scrollWidth - vw;
const tl = gsap.timeline({
scrollTrigger: {
trigger: '#journey', start: 'top top',
end: '+=' + dist,
scrub: 1, pin: true, anticipatePin: 1,
},
});
// RTL: הפאנלים יושבים משמאל לפאנל 0 → X חיובי = פאן ימין-לשמאל
tl.to(track, { x: dist, duration: dist, ease: 'none' }, 0);
כל אנימציות הפרקים חיות באותו טיימליין, ממוקמות לפי פיקסלים. לכל פאנל מחשבים את המרחק שבו הקצה המוביל (הימני!) שלו פוגש את קצה המסך:
// המרחק שנגלל כשקצהו הימני של הפאנל נכנס לפריים
const R = track.scrollWidth - panel.offsetLeft - panel.offsetWidth;
const start = Math.max(0, R - vw * 0.45);
tl.from(reveals, { y: 54, opacity: 0, stagger: vw * 0.05,
duration: vw * 0.3, ease: 'power3.out' }, start);
היתרון על containerAnimation: הכול דטרמיניסטי. מכיוון שמשך הטיימליין שווה למרחק בפיקסלים, מיקום צאצא בזמן start פירושו בדיוק ״כשנגללו start פיקסלים״. שינוי גודל חלון משמעותי הורס את המתמטיקה — ולכן בריסייז רחב המנוע נהרס ונבנה מחדש.
גליפי הרקע הענקיים מקבלים טווין x נגדי (מ־-16vw ל־+16vw) על פני כמעט שני מסכים — ביחס למסילה הם נעים לאט יותר, ונוצרת תחושת עומק. שנת ה־1475 הענקית נעה בכיוון ההפוך — מהר יותר — כשכבת חזית.
כל כותרת מפוצלת לאותיות שנכנסות בזו אחר זו מלמטה, עם רוטציה אקראית קלה. בעברית אסור לפצל עם split('') — ניקוד הוא תווים משלבים (combining marks) שחייבים להישאר צמודים לאות. הפתרון: Intl.Segmenter עם גרגור גרפמות:
const seg = new Intl.Segmenter('he', { granularity: 'grapheme' });
const graphemes = [...seg.segment(text)].map(s => s.segment);
// "הַכֹּל" → ["הַ", "כֹּ", "ל"] — האות והניקוד נשארים יחד
ה־spans מקבלים display:inline-block; אלגוריתם ה־bidi מסדר אותם נכון מימין לשמאל כי סדר ה־DOM נשאר לוגי. הסטאגר רץ from: 'start' — האות הראשונה (הימנית) עולה ראשונה, כמו כתיבה.
tl.from(chars, {
yPercent: 115, opacity: 0,
rotate: () => gsap.utils.random(-8, 8),
stagger: { each: vw * 0.018, from: 'start' },
ease: 'power3.out',
}, start + vw * 0.05);
אין גופן Google לכתב פרוטו־סינאי או לדעץ, ולכן האותיות הקדומות צוירו כ־path ידני — פרשנות אמנותית מופשטת (ראש שור עם קרניים לאלף הפרוטו־סינאי, צורת ה־K הנטויה לאלף הקדום), לא העתק אפיגרפי מדויק:
אפקט ״הציור החי״ — הקלאסיקה של stroke-dashoffset, מסונכרן לגלילה בתוך טיימליין המאסטר:
panel.querySelectorAll('.draw path').forEach((path, pi) => {
const len = path.getTotalLength();
gsap.set(path, { strokeDasharray: len, strokeDashoffset: len });
tl.to(path, { strokeDashoffset: 0, duration: vw * 0.5,
ease: 'power2.inOut' }, start + vw * (0.08 + pi * 0.05));
});
האותיות המרובעות, הרש״יות והמודרניות מוצגות דווקא בגופנים אמיתיים (Frank Ruhl Libre, Noto Rashi Hebrew) — עם -webkit-text-stroke ומילוי שקוף לאפקט קווי־מתאר ענק ברקע.
בפרק הדיגיטלי 22 אותיות ה־א״ב מגיבות לסמן: המשקל של כל אות נגזר מהמרחק שלה מהעכבר (נפילה ריבועית — חדה במרכז, רכה בשוליים). בלי מגע, גל סינוס עצל מרים ומוריד משקלים כדי שהפאנל לא יהיה מת:
const d = Math.hypot(dx, dy);
const k = Math.max(0, 1 - d / 170);
w = 300 + k * k * 600; // קרוב לעכבר = שמן
// idle: גל נושם כשאין סמן
w = 420 + (Math.sin(t + i * 0.42) * 0.5 + 0.5) * 420;
el.style.fontVariationSettings = `"wght" ${w}`;
זה עובד כי Frank Ruhl Libre נטען כגופן משתנה — wght@300..900 בכתובת ה־Google Fonts מחזירה קובץ אחד עם כל הציר.
st.getVelocity()) מוזרקת כ״רוח״ אופקית שדוחפת את האבק בכיוון המסע — ודועכת ב־*= 0.94 לפריים.feTurbulence כ־data-URI ברקע קבוע, אטימות 5.5%, מונפש ב־steps(4) לריצוד אנלוגי.#000 שטוח.sine.inOut yoyo) — הדף אף פעם לא סטטי לחלוטין.const wind = gsap.utils.clamp(-2.2, 2.2, scrollVel);
p.x += p.vx + Math.sin(t + p.ph) * 0.12 + wind * 0.55;
scrollVel *= 0.94; // הרוח שוככת כשמפסיקים לגלול
המסע האופקי ממוסמר (pin) — אבל ברגע שהוא משתחרר, הדף ממשיך כדף אנכי רגיל. זה טריק מבני חשוב: ScrollTrigger מוסיף spacer בגובה מרחק הסקראב, כך שכל מה שנכתב אחרי <main> ב־HTML פשוט זורם אחריו במסמך. ארבעה מקטעים אנכיים ופוטר הופכים את החוויה לאתר שלם: מעבדת ההשוואה, כרטיסי התחנות, אילן היוחסין, ומקורות.
התחנה האינטראקטיבית מציגה אות אחת (א/ב/מ/ע/ש/ת) בשישה גלגולים. שתי התקופות הקדומות הן path-ים מצוירים ביד (מילון JS של נתיבים לכל אות), וארבע המודרניות הן תווים חיים בגופנים שונים. הבמה והרצועה נבנות מאותה פונקציית formNode(era) אחת:
const ERAS = [
{ name: 'פרוטו־סינאי', svg: PROTO, vb: '0 0 100 110' },
{ name: 'דעץ · עברי קדום', svg: PALEO, vb: '0 0 100 100' },
{ name: 'אשורי מרובע', cls: 'cf-square' },
// … רש״י, פרנק־ריהל, דיגיטלי — תווים בגופנים אמיתיים
];
המעבר המונפש בין תקופות: הצורה הישנה מתכווצת ונעלמת (scale 0.9 → opacity 0), והחדשה נכנסת — SVG מצויר מחדש עם stroke-dashoffset, ותו טיפוגרפי קופץ פנימה עם back.out. שתי הצורות חיות באותו תא grid (grid-area: 1/1) ולכן נערמות במרכז בלי מיקום אבסולוטי שנלחם ב־transform של GSAP. כפתור ״נגנו את הגלגול״ מריץ setInterval שמדפדף בין התקופות — ולחיצה על תחנה עוצרת אותו.
ה־nav העליון מקבל רקע מטושטש אחרי 40px של גלילה, וכל מקטע אנכי מדליק את הקישור שלו דרך ScrollTrigger.create עם onEnter/onEnterBack. ציר הזמן התחתון — שרלוונטי רק למסע האופקי — נעלם ברגע שנכנסים למקטעים האנכיים (מחלקת past-journey על ה־body).
בתחתית המעבדה ארבעה מונים (3,800 שנה · 22 אותיות · 5 סופיות · ~950 מגילות) שסופרים מאפס בכניסה לפריים. שני פרטים חשובים: המספרים מפורמטים עם Intl.NumberFormat('he-IL') (פסיק אלפים אמיתי), והאלמנט מקבל direction:ltr; unicode-bidi:isolate — אחרת סימן ה־~ קופץ לצד הלא נכון בהקשר RTL.
מקטע נוסף מצייר את עץ המשפחה של האלפבית: פרוטו־סינאי ← פיניקי ← (עברי קדום, ארמי, יווני) ← (מרובע, ערבי, לטיני, קירילי…). העץ הוא SVG סטטי אחד ב־HTML — צמתים כ־<g> עם rect+text, קשתות כ־path בזייה. השושלת העברית מסומנת במחלקת hl וצבועה זהב; כל השאר עמום. האנימציה: הקשתות נמתחות ב־stroke-dashoffset והצמתים מופיעים דור אחר דור — הדירוג נשמר כ־custom property --d על כל צומת, ו־JS קורא אותו כדי למקם את הטווין בטיימליין. במסכים צרים העץ חי בקונטיינר עם גלילה אופקית פנימית (min-width על ה־SVG) — לא מקטינים את הטקסט עד שאינו קריא.
כרטיסי התחנות נחשפים ב־ScrollTrigger.batch עם סטאגר, ושורות המקורות מחליקות פנימה מהצד. הכול once: true — גילוי הוא אירוע חד־פעמי, לא אפקט שמתנגן שוב בכל גלילה.
transform/opacity בלבד — אפס layout thrash; ריחוף במקום ריפליי.aria-hidden; הטקסט המלא זמין לקוראי מסך בסדר לוגי.document.fonts.ready (עם timeout ביטחון של 3 שניות).<main> בגובה מסך עם מסילת פלקס; ב־RTL הפאנלים מסתדרים ימין→שמאל לבד.scrub, שבו יחידת זמן = פיקסל; מקמו כל אנימציית פרק לפי R = scrollWidth - offsetLeft - offsetWidth.Intl.Segmenter — לעולם לא split('') בעברית.האתר פרוצדורלי לחלוטין — אפס תמונות מיוצרות. כל הוויזואליה היא טיפוגרפיה, SVG שצויר ביד בקוד, ו־Canvas.