מאחורי הקלעים · קו לילה 47

כיצד נבנתה
״התחנה האחרונה״

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

→ חזרה לרכבת

01 · הקונספט

קולנוע אילם בדפדפן

נקודת המוצא: נסיעת רכבת לילית כחוויית גלילה נרטיבית. הגלילה איננה "מעבר בין סקשנים" — היא הדוושה של הרכבת. כשגוללים, העולם נוסע; כשעוצרים, הרכבת עוצרת בתחנה, וכרטיס ביניים (אינטרטייטל, כמו בסרט אילם) מגיש מיקרו־סיפור עברי מקורי.

שלוש החלטות עיצוב מגדירות את היצירה:

02 · הפלטה

שישה צבעים, מצב רוח אחד

פלטה לילית ממושמעת: שלושה כחולים לעומק, קרם ירח לטקסט, ענבר לחלונות ולפנסים, ואדום־חלודה אחד ששמור אך ורק לשלט "התחנה האחרונה".

לילה#0A0E1F
חצות#16203E
ערפל#2C3A5C
קרן ירח#F2E8CF
ענבר#E8A54B
אות עצירה#B4432E

העומק בלילה לא מגיע משחור שטוח אלא משכבות אור: הילת ירח תלת־שכבתית (box-shadow כפול), אורות כפר ענבריים על הגבעות, פס אור שנשפך מחלונות הרכבת על המסילה, ווינייטה רדיאלית שסוגרת את הפריים.

03 · הארכיטקטורה

עולם חד־ממדי: מספר אחד מניע הכול

כל הסצנה נשלטת על ידי משתנה יחיד — journey.dist — המרחק שהרכבת עברה. כל שכבת פרלקסה מקבלת מקדם מהירות f, וכל פריים מציב אותה לפי אותו מרחק. שלטי התחנות הם "עצמים בעולם" עם קואורדינטת מרחק משלהם, ולכן הם מגיעים למרכז המסך בדיוק כשהרכבת עוצרת — הסנכרון נובע מהמבנה, לא מכוונון ידני.

שמיים + כוכבים
f = 0 (סטטי, מנצנץ)
ירח
f = 0.006
רכס הרים
f = 0.06
גבעות + אורות כפר
f = 0.16
ברושים
f = 0.38
עמודי חשמל
f = 0.66
שלטי תחנה
f = 0.85
מסילה
f = 1.0

כל שכבה היא div עם רקע SVG שחוזר על עצמו (repeat-x). במקום להזיז div ברוחב קילומטר, ה־transform מתעטף במודולו של רוחב האריח — כך השכבה אינסופית אבל הזיכרון קבוע:

// הלב של מנוע הפרלקסה — render() רץ על כל עדכון של ציר הזמן
for (var i = 0; i < layers.length; i++) {
  var L = layers[i];
  // עטיפת מודולו: תזוזה אינסופית עם אריח סופי
  var x = -((d * L.f) % L.tile);
  L.el.style.transform = "translate3d(" + x + "px,0,0)";
}

// שלט תחנה = עצם בעולם: worldX קבוע, המסך זז סביבו
var wx = SEG * (s + 1);            // המרחק שבו התחנה "חיה"
var sx = (wx - d) * STF;           // היטל למסך לפי מקדם הפרלקסה
el.style.transform = "translate3d(" + sx + "px,0,0)";
// כאשר d == wx ⇒ sx == 0 ⇒ השלט בדיוק במרכז — בלי כוונון ידני

04 · הגלילה

GSAP ScrollTrigger: נסיעה, עצירה, סיפור

ציר זמן ראשי אחד ממופה למרווח גלילה של 1150vh עם scrub: 1 (החלקת אינרציה קלה — תחושת מסה של רכבת). המבנה הוא לולאה של חמישה מקטעים: נסיעה → עצירה → כרטיס ביניים → יציאה. תאוצת היציאה והבלימה מגיעות בחינם מ־ease: "power2.inOut" על כל מקטע נסיעה:

var tl = gsap.timeline({
  onUpdate: render,                      // כל טיק מצייר את העולם מחדש
  scrollTrigger: {
    trigger: "#spacer",                // div בגובה 1150vh — "אורך הפסים"
    start: "top top", end: "bottom bottom",
    scrub: 1                             // אינרציה: הרכבת לא נעצרת מיד
  }
});

for (var n = 1; n <= 5; n++) {
  // נסיעה: האטה ותאוצה מובנות ב-ease
  tl.to(journey, { dist: SEG * n, duration: 3, ease: "power2.inOut" });
  // עצירה: הכרטיס עולה, העולם קופא
  tl.to(cards[n], { autoAlpha: 1, y: 0, duration: 0.85, ease: "power3.out" });
  tl.to({}, { duration: 1.35 });         // שהות לקריאה
  if (n < 5) tl.to(cards[n], { autoAlpha: 0, y: -36, duration: 0.6 });
}

בזמן נסיעה, שכבת "פסי מהירות" (repeating-linear-gradient) מקבלת אטימות פרופורציונלית למהירות הרגעית — הנגזרת של dist בין פריימים — כך שהתחושה של תאוצה מתקבלת גם בלי מוֹשן בלר יקר.

05 · גרפיקה פרוצדורלית

אף פיקסל לא צויר מראש

כל הסילואטות — הרים, גבעות, ברושים, עמודים, מסילה ואפילו הרכבת עצמה — נבנות בזמן טעינה כמחרוזות SVG ומוזרקות כ־background-image בקידוד data-URI. מחולל אקראיות זרוע (mulberry32) מבטיח שהנוף זהה בכל טעינה — גנרטיבי אבל יציב:

// רכס הרים: הליכת מקרים שקצותיה מעוגנים לאותו גובה — ריצוף חלק
function ridgePath(rnd, W, H, base, amp, step) {
  var pts = [], y0 = base + (rnd() - 0.5) * amp;
  for (var x = 0; x <= W; x += step)
    pts.push([x, (x === 0 || x === W) ? y0 : base + (rnd() - 0.5) * amp]);
  // y(0) === y(W) ⇒ האריח מתחבר לעצמו בלי תפר
  var d = "M0," + H + " L0," + pts[0][1];
  for (var i = 1; i < pts.length; i++) {
    var mx = (pts[i-1][0] + pts[i][0]) / 2;      // עיקול Q דרך נקודת אמצע
    d += " Q" + mx + "," + pts[i-1][1] + " " + pts[i][0] + "," + pts[i][1];
  }
  return d + " L" + W + "," + H + " Z";
}

אותו עיקרון עיגון־קצוות משמש גם את חוטי החשמל: כבל התלוי בין עמודים משורטט כרצף עקומות Q שחורג מגבולות האריח משני הצדדים, כך ש־y בנקודה 0 שווה ל־y בנקודה 1400 והריצוף חלק. הרכבת היא אריח של קרון בודד: חלונות ענבריים עם צלליות נוסעים (ראש + כתפיים), חלונות חשוכים, גלגלים ופס אור תחתון שנשפך על המסילה.

לילה חי גם כשלא גוללים: קנבס כוכבים (עד ~160, DPR מוגבל ל־2) מנצנץ בסינוס עם פאזה אקראית, מטאור חוצה את השמיים כל כמה שניות, עננים נסחפים, הרכבת מתנדנדת קלות ופנס התחנה מהבהב.

06 · שפת הקולנוע

לטרבוקס, גרעין והבהוב מקרן

/* גרעין פילם — אריח רעש אחד, שלושה צעדים, אפס JavaScript */
#grain {
  position: fixed; inset: -60px; opacity: .055; pointer-events: none;
  background-image: url("data:image/svg+xml,…feTurbulence
    type='fractalNoise' baseFrequency='0.85' stitchTiles='stitch'…");
  animation: grainShift .45s steps(3) infinite;
}

07 · האפילוג

אחרי הטיטרים: אתר שלם מעבר לסרט

הסרט נגמר ב"סוֹף" — אבל האתר ממשיך. אחרי מסך הסיום נפתח אפילוג בן ארבעה פרקים, שמרחיב את העולם בלי לשבור את השפה: אותה פלטה, אותם מסגרות כפולות של אינטרטייטל, אותו קצב.

יומן הנוסעים

ארבעה כרטיסי מיקרו־סיפורת נוספים ("הצעיף", "המכתב", "השעון", "האספן") — כל אחד עם מספר מושב וקרון, בסגנון כרטיסי הביניים של הסרט. הכרטיסים נחשפים בגלילה עם stagger עדין, ובריחוף הם מתרוממים והאורנמנט (✦) מתרחב ומתחמם.

לוח היציאות: split-flap עברי

לוח יציאות של תחנת רכבת משנות ה־20: שעון ספרות מפוצלות שעומד לנצח על 23:47 (שעת היציאה), שש שורות יעד — חמש התחנות ועוד "רכבת הבוקר" שבוטלה כי ביום הקו ישן. השורות נכנסות בגלילה עם היפוך rotationX מדורג, כמו לוח מכני שמתעדכן. ובתור חיים סמויים: אחת לכמה שניות הערה אחת בלוח "מתהפכת" — האותיות מתערבבות באקראי מתוך הא״ב העברי ונרגעות בהדרגה חזרה לטקסט, מימין לשמאל:

// רחש הלוח: אות אחר אות נרגעת חזרה למקומה (RTL-friendly)
var settled = Math.floor((frame / total) * target.length);
for (var i = 0; i < target.length; i++) {
  out += (i < settled) ? target[i]
       : CHARS[Math.floor(Math.random() * CHARS.length)];
}
// רץ רק כשהלוח על המסך, ולא תחת prefers-reduced-motion

מפת הקו: הקו משרטט את עצמו

מפת SVG של שש נקודות (מוצא + חמש תחנות) עם טכניקת ה־stroke הקלאסית: אורך הנתיב נמדד עם getTotalLength(), מוגדר כ־stroke-dasharray, וה־offset נגלל מ־L ל־0 עם scrub. קצה הקו נושא נקודת אור זוהרת — "הרכבת" על המפה — ותחנה נדלקת ברגע שהדיו מגיע אליה:

// הקו נמדד פעם אחת, ומצטייר לפי התקדמות הגלילה
var L = main.getTotalLength();
main.setAttribute("stroke-dasharray", L);

function updateMap(p) {
  var drawn = p * L;
  main.setAttribute("stroke-dashoffset", L - drawn);
  var pt = main.getPointAtLength(drawn);   // קצה הדיו = מיקום הרכבת
  tip.setAttribute("cx", pt.x); tip.setAttribute("cy", pt.y);
  // תחנה נדלקת כשהקו עובר את הקואורדינטה שלה על הנתיב
  stns.forEach(s => s.g.classList.toggle("lit", drawn >= s.at));
}

גם מיקומי התחנות עצמם נגזרים מהנתיב — getPointAtLength(fr[i] * L) — כך שאפשר לעקם את הקו כרצוננו והתחנות תמיד יישבו עליו בדיוק.

חדר המכונות: דגימות חיות

סקשן "כך נבנה הלילה" לא מספר על הגרפיקה הפרוצדורלית — הוא מריץ אותה: אריח הרכבת האמיתי, קנבס כוכבים קטן שחי רק כשהוא על המסך, דגימת גרעין־פילם, וגנרטור רכסים אינטראקטיבי. כפתור "לגלגל רכס חדש" מזין זרע אקראי לאותה פונקציית ridgePath שמציירת את ההרים בסרט — הדגמה חיה של ההבדל בין גנרטיבי לאקראי.

08 · טיפוגרפיה ונגישות

סריף עברי במשקל קולנועי

תפקידגופןהערות
כותרות וסיפורתFrank Ruhl Libre 300–900סריף עברי קלאסי; 900 לכותרות, 400 עם line-height 1.95 לפרוזה
ממשק ותוויותHeebo 200–600גרוטסק נקי; תוויות ב־letter-spacing רחב (0.42em) לתחושת כרטיס קולנוע

09 · שחזור

איך בונים כזה בעצמכם

ללא נכסי AI: באתר הזה לא נעשה שימוש בתמונות מגונרטות או בקבצי מדיה כלל — כל הוויזואליה פרוצדורלית (SVG + Canvas + CSS). התלות החיצונית היחידה: GSAP 3.12.5 מ־CDN ושני גופנים מ־Google Fonts.

→ לעלות שוב על הרכבת