01הרעיון
עיר בענן היא צעצוע, לא משחק: אין ניקוד, אין הפסד, אין מטרה. לחיצה על השמיים מצמיחה אי מרחף; לחיצה על אי מוסיפה מבנה — בית, מגדל, כיפה או עץ. כל אי חדש נקשר אוטומטית בגשר חבל לשכן הקרוב ביותר, והשמיים עצמם חיים: שמש, ירח, כוכבים, ציפורים, כדור פורח וכוכבים נופלים.
ההחלטה העיצובית המרכזית: הכול פרוצדורלי. אין אף קובץ תמונה באתר. כל בניין, ענן וגשר מצוירים בכל פריים מחדש מפונקציות ציור בסיסיות. כך הקובץ נשאר קטן, הכול ניתן לגיוון אינסופי, והאנימציה חיה באמת.
02הפלטה והטיפוגרפיה
מצב רוח אחד דומיננטי: חלום פסטלי שנודד בין יום שטוף שמש ללילה אינדיגו. חמישה צבעי גוף למבנים, ארבעה לגגות, וגוון לילה אחד שאליו הכול נמשך.
הטיפוגרפיה: 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אטמוספרה: הדברים הקטנים
- שלוש שכבות עננים בפרלקסה — רחוקים איטיים ושקופים, קרובים מהירים ואטומים — ועוד "ים עננים" מתגלגל בתחתית המסך.
- ריחוף — כל אי מקבל פאזה, אמפליטודה ומהירות סינוס משלו, כך שהשכונות לעולם לא נעות יחד.
- ציפורים — להקות של קווי V מתעופפים שנכנסות רק ביום; כדור פורח מפוספס חוצה לאט את השמיים כל הזמן.
- פרטיקלים — כל בנייה משחררת פליטת פוך עננים קטנה שמתרחבת ונמוגה.
- ריקוד עצירה — לחיצה על אי מלא לא עושה "כלום": האי מקפיץ את אנימציית הלידה שלו מחדש, פידבק עדין שאומר "אין מקום".
08ביצועים ונגישות
devicePixelRatioחסום ב-2 — מסכי רטינה חדים, בלי לשרוף GPU.- הכול
fillRect / arc / pathפשוטים; איןshadowBlurבלולאה החמה. ~9 איים ו-80 מבנים רצים ב-60fps בקלות. prefers-reduced-motionמכבה ריחוף, ציפורים, כוכבים נופלים ואנימציות קפיצה — העיר עדיין נבנית, בלי תנועה מיותרת.- כל ה-UI הוא DOM אמיתי (לא טקסט על קנבס): כפתורים עם
aria-label, פוקוס נראה, ו-RTL מלא.
09לשחזר את זה בעצמכם
- קנבס אחד על כל המסך, לולאת
requestAnimationFrame, ו-dtמחושב בין פריימים. - כתבו את
isoToScreenואת קופסת שלושת-המצולעים. ציירו קופסה אחת. זה הרגע הקשה היחיד. - הוסיפו
shade(rgb, f)וצבעו את שלוש הפאות בעוצמות שונות — פתאום יש תלת-ממד. - בנו את מבנה הנתונים: אי = רשת אריחים + זרע; מבנה = אריח + סוג + גיל. מיינו לפי עומק.
- חברו קליק → אריח פנוי → מבנה חדש עם
easeOutBackעל הסקייל. - הוסיפו את מערך ה-SKY והעבירו כל צבע דרך
shade. זה הצעד שהופך סצנה לעולם. - עכשיו הקצפת: עננים בפרלקסה, ריחוף סינוסי, גשרים, ציפורים. כל אחד מהם 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 ונגזרים ממנו זמן היום, מספר האיים, מיקומיהם ורשימת המבנים — ואז אותו מנוע תמונות ממוזערות מצייר את התוצאה מיד. אותו זרע יפיק את אותה עיירה בכל דפדפן, בכל מכשיר, לנצח — זו ההדגמה הכי ישירה של דטרמיניזם פרוצדורלי שיש באתר.