מאחורי הקלעים · קו לילה 47
סרט גלילה אילם שנבנה כולו בקוד: אף תמונה לא צוירה מראש. הרים, רכבת, כוכבים ותחנות — הכול נוצר בזמן טעינה מ־SVG פרוצדורלי, ומונע על ידי מונה מרחק אחד שממופה לגלילה.
→ חזרה לרכבת01 · הקונספט
נקודת המוצא: נסיעת רכבת לילית כחוויית גלילה נרטיבית. הגלילה איננה "מעבר בין סקשנים" — היא הדוושה של הרכבת. כשגוללים, העולם נוסע; כשעוצרים, הרכבת עוצרת בתחנה, וכרטיס ביניים (אינטרטייטל, כמו בסרט אילם) מגיש מיקרו־סיפור עברי מקורי.
שלוש החלטות עיצוב מגדירות את היצירה:
02 · הפלטה
פלטה לילית ממושמעת: שלושה כחולים לעומק, קרם ירח לטקסט, ענבר לחלונות ולפנסים, ואדום־חלודה אחד ששמור אך ורק לשלט "התחנה האחרונה".
העומק בלילה לא מגיע משחור שטוח אלא משכבות אור: הילת ירח תלת־שכבתית (box-shadow כפול), אורות כפר ענבריים על הגבעות, פס אור שנשפך מחלונות הרכבת על המסילה, ווינייטה רדיאלית שסוגרת את הפריים.
03 · הארכיטקטורה
כל הסצנה נשלטת על ידי משתנה יחיד — journey.dist — המרחק שהרכבת עברה. כל שכבת פרלקסה מקבלת מקדם מהירות f, וכל פריים מציב אותה לפי אותו מרחק. שלטי התחנות הם "עצמים בעולם" עם קואורדינטת מרחק משלהם, ולכן הם מגיעים למרכז המסך בדיוק כשהרכבת עוצרת — הסנכרון נובע מהמבנה, לא מכוונון ידני.
כל שכבה היא 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 · הגלילה
ציר זמן ראשי אחד ממופה למרווח גלילה של 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 עדין, ובריחוף הם מתרוממים והאורנמנט (✦) מתרחב ומתחמם.
לוח יציאות של תחנת רכבת משנות ה־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) לתחושת כרטיס קולנוע |
prefers-reduced-motion: הנצנוץ, הגרעין, ההבהוב, המטאורים והנדנוד כבים; ה־scrub הופך ישיר (ללא אינרציה) והגלילה נשארת פונקציונלית לחלוטין.09 · שחזור
position: fixed; inset: 0, ומאחוריה div#spacer גבוה (כאן 1150vh) שכל תפקידו לייצר מסלול גלילה.{ dist: 0 }, מערך שכבות עם מקדם מהירות ורוחב אריח, ופונקציית render() אחת שממקמת הכול.power2.inOut, עצירות עם כרטיסי טקסט, ו־scrub: 1 בשביל תחושת המסה.prefers-reduced-motion.ללא נכסי AI: באתר הזה לא נעשה שימוש בתמונות מגונרטות או בקבצי מדיה כלל — כל הוויזואליה פרוצדורלית (SVG + Canvas + CSS). התלות החיצונית היחידה: GSAP 3.12.5 מ־CDN ושני גופנים מ־Google Fonts.