חזרה לעיר

איך בונים עיר בענן

פירוק טכני מלא של הצעצוע הדיגיטלי: עיר צפה איזומטרית שנבנית פרוצדורלית ב-Canvas 2D אחד — בלי ספריות גרפיקה, בלי תמונות, בלי בילד. רק מתמטיקה, צבע וזמן.

Canvas 2Dהטלה איזומטריתייצור פרוצדורלימחזור יום-לילהללא תלות חיצונית

01הרעיון

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

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

02הפלטה והטיפוגרפיה

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

קרם#F2E5D0
טרקוטה#E2907B
טורקיז מאובק#7FB5A8
לילך#C5A0C9
ענבר (הדגשה)#FFB347
אינדיגו לילה#141A3D

הטיפוגרפיה: Suez One לכותרות — סריף עברי כבד עם אופי של שלט רחוב ישן, מול Heebo קליל לגוף. שניהם מ-Google Fonts.

03הטלה איזומטרית ב-12 שורות

כל העולם בנוי על ריבועי רשת (אריחים) שממופים למרחב מסך ביחס 2:1 — הקלאסיקה של משחקי אסטרטגיה ישנים. אריח לוגי בקואורדינטות (ix, iy) הופך לנקודת מסך כך:

const TW = 54, TH = 27;  // רוחב וגובה אריח — יחס 2:1

function isoToScreen(ix, iy){
  return [ (ix - iy) * TW / 2, (ix + iy) * TH / 2 ];
}

קופסה איזומטרית (בניין) היא שלושה מצולעים בלבד: מעוין עליון ושתי פאות צד. עומק נוצר רק מהצללה — הפאה השמאלית בהירה מהימנית:

ctx.fillStyle = shade(b.body, 0.74);  // פאה שמאלית
ctx.beginPath();
ctx.moveTo(-hw,-h); ctx.lineTo(0,hh-h);
ctx.lineTo(0,hh);  ctx.lineTo(-hw,0);
ctx.closePath(); ctx.fill();

ctx.fillStyle = shade(b.body, 0.52);  // פאה ימנית — כהה יותר
/* ...אותו דבר בסימן הפוך... */

ctx.fillStyle = shade(b.body, 1.06);  // גג — הכי מואר
diamondPath(0, -h, hw, hh); ctx.fill();

סדר הציור הוא הכול באיזומטריה: מבנים ממוינים לפי ix + iy (אחורי לפני קדמי), ואיים ממוינים לפי גובהם על המסך. אין z-buffer — יש רק משמעת.

04ייצור פרוצדורלי דטרמיניסטי

כל אי נולד עם זרע (seed) ומחולל מספרים אקראיים משלו — mulberry32, מחולל זעיר ומהיר. כך אותו אי תמיד ייראה אותו דבר בכל פריים, גם כשהחלונות, הארובות והצבעים "אקראיים":

function mulberry32(seed){
  return function(){
    seed |= 0; seed = seed + 0x6D2B79F5 | 0;
    let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  };
}

מהזרע נגזר הכול: סוג המבנה (בית / מגדל / כיפה / עץ), גובה, צבע גוף מתוך הפלטה, צבע גג, כמה אנשים גרים בו, ואילו חלונות דולקים בלילה. ארבעה סוגי מבנים בלבד — אבל השילוב של גובה × צבע × גג × חלונות מייצר עיר שלא חוזרת על עצמה.

אנימציית הצמיחה

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

const easeOutBack = t => {
  const c = 1.70158;
  return 1 + (c+1) * Math.pow(t-1, 3) + c * Math.pow(t-1, 2);
};
const pop = easeOutBack(clamp((now - b.born) / 0.7, 0, 1));
ctx.scale(pop, pop);

05מחזור היום והלילה

הלב של האווירה: זמן היום הוא מספר יחיד dayT בין 0 ל-1 שמתקדם לאט (מחזור מלא = 150 שניות). שמונה "פריימי מפתח" של שמיים — לילה עמוק, שחר, בוקר, צהריים, שקיעה — ומערכת אינטרפולציה ליניארית ביניהם:

const SKY = [
  {t:0.00, top:[10,15,44],    bot:[46,38,92],    amb:0.04},  // לילה
  {t:0.27, top:[96,160,210],  bot:[255,203,160], amb:0.72},  // שחר
  {t:0.42, top:[116,190,229], bot:[253,232,201], amb:1.00},  // צהריים
  {t:0.72, top:[70,70,138],   bot:[255,140,100], amb:0.42},  // שקיעה
  /* ... */
];

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

const NIGHT_TINT = [46,54,110];

function shade(rgb, f){
  const n = 1 - light.amb;              // כמה לילה עכשיו
  const r = lerp(rgb[0]*f, NIGHT_TINT[0]*0.9, n*0.72);
  /* ...וכך ל-g ול-b... */
  return `rgb(${r|0},${g|0},${b|0})`;
}

מעל זה: כוכבים שמופיעים כש-amb יורד מתחת ל-0.4, חלונות שנדלקים בענבר, אנטנות מגדלים שמהבהבות באדום, וכוכבים נופלים אקראיים. כפתור "יום/לילה" לא קופץ — הוא מגדיר dayTarget והזמן מאיץ בעדינות עד שהוא מגיע לשם.

06גשרי החבל

כשאי חדש נולד, הוא מתחבר לשכן הקרוב ביותר. הגשר הוא קשת שקיעה (catenary מקורב): אינטרפולציה ליניארית בין שתי נקודות עיגון, ועליה תוספת sin(π·t) שמושכת את האמצע כלפי מטה. הקרשים מצוירים לאורכה בניצב לכיוון הגשר:

const sag = Math.min(46, dist * 0.14);
for (let i = 0; i < planks; i++){
  const t = (i + 0.5) / planks;
  const x = lerp(ax, bx, t);
  const y = lerp(ay, by, t) + Math.sin(Math.PI * t) * sag;
  // קרש קטן בניצב לכיוון הגשר
}

נקודות העיגון מחושבות מחדש בכל פריים מתוך מיקום האיים כולל תנודת הריחוף שלהם — כך הגשר באמת "תלוי" בין שני גופים שנעים, ובנייתו נחשפת קרש-אחרי-קרש עם easeOutCubic.

07אטמוספרה: הדברים הקטנים

08ביצועים ונגישות

09לשחזר את זה בעצמכם

  1. קנבס אחד על כל המסך, לולאת requestAnimationFrame, ו-dt מחושב בין פריימים.
  2. כתבו את isoToScreen ואת קופסת שלושת-המצולעים. ציירו קופסה אחת. זה הרגע הקשה היחיד.
  3. הוסיפו shade(rgb, f) וצבעו את שלוש הפאות בעוצמות שונות — פתאום יש תלת-ממד.
  4. בנו את מבנה הנתונים: אי = רשת אריחים + זרע; מבנה = אריח + סוג + גיל. מיינו לפי עומק.
  5. חברו קליק → אריח פנוי → מבנה חדש עם easeOutBack על הסקייל.
  6. הוסיפו את מערך ה-SKY והעבירו כל צבע דרך shade. זה הצעד שהופך סצנה לעולם.
  7. עכשיו הקצפת: עננים בפרלקסה, ריחוף סינוסי, גשרים, ציפורים. כל אחד מהם 20–40 שורות.

שימו לב שלא נעשה שימוש בשום נכס שנוצר על-ידי AI — אין תמונות בכלל. כל פיקסל באתר מגיע מקוד הציור שראיתם כאן.

10שעון היממה ומעבדת הזרעים

שני רכיבים באתר הופכים את שני הרעיונות המרכזיים — הזמן והזרע — למשהו שאפשר למשש:

שעון היממה הוא סליידר שנמתח על פס שמיים שמצויר בזמן ריצה: כל עמודה בקנבס היא קריאה ל-skyAt(t) עם t אחר, כך שהפס הוא ליטרלית מפת החום של מערך ה-SKY מסעיף 05. גרירת הידית מרנדרת עיירת תצוגה קבועה ב-dayT הנבחר, ושחרור מציב dayTarget — והמגרש הראשי מאיץ בעדינות אל השעה שבחרתם. חצות מימין, בכיוון הקריאה של הדף.

for (let x = 0; x < w; x += 3){
  const t = 1 - x / w;          // RTL: חצות מימין
  const s = skyAt(t);           // אותה פונקציה של המגרש
  /* גרדיאנט אנכי top→bot לעמודה */
}

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