→ חזרה לשמיים
✦ מאחורי הקלעים

איך נבנה
"דגי כוכבים"

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

01 — הרעיון

פיוט אינטראקטיבי, לא "דמו טכני"

נקודת המוצא: מה אם בְּרֵכַת קוי יפנית הייתה שמי לילה? הדגים שוחים בין המזלות, והאינטראקציה היחידה היא האכלה — לחיצה מפזרת כוכב־מאכל, הלהקה נאספת, ומי שאוכל "מחזיר" כוכב חדש לשמיים. יש לולאת משוב רגשית שלמה: נתינה ← תגובה ← לידה. המונה בתחתית סופר כמה כוכבים נולדו.

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

02 — האלגוריתם

Boids: שלושה חוקים, להקה חיה

כל דג הוא "בואיד" (Craig Reynolds, 1986) שמציית לשלושה כוחות מקומיים בלבד — ומהם צומחת התנהגות להקתית שלא תוכנתה בשום מקום:

// steer() — the heart of the flock
for (const o of flock) {
  const dx = o.x - this.x, dy = o.y - this.y;
  const d2 = dx*dx + dy*dy;              // squared distance — no sqrt
  if (d2 < 10000) {                       // alignment + cohesion (100px)
    cx += o.x; cy += o.y; alx += o.vx; aly += o.vy; cn++;
  }
  if (d2 < 2500) {                        // separation (50px), inverse falloff
    const d = Math.sqrt(d2);
    sx -= dx/d * (50-d)/50;  sy -= dy/d * (50-d)/50;
  }
}
ax += (cx/cn - this.x) * .0022   // cohesion   — weakest
    + (alx/cn)          * .05     // alignment  — medium
    +  sx               * .22;    // separation — strongest

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

03 — האנטומיה

עמוד שדרה: שרשרת עקיבה (Follow Chain)

דג אינו נקודה — הוא שרשרת של 14 חוליות. הראש נע לפי הפיזיקה של הבואיד, וכל חוליה פשוט "נגררת" אחרי קודמתה במרחק קבוע. שתי שורות מתמטיקה מייצרות תנועת שחייה משכנעת להפליא:

// each vertebra keeps a fixed gap from the previous one
for (let i = 1; i < SEG; i++) {
  const p = spine[i], q = spine[i-1];
  const d = Math.hypot(p.x-q.x, p.y-q.y);
  p.x = q.x + (p.x-q.x)/d * gap;          // pull toward parent
  p.y = q.y + (p.y-q.y)/d * gap;
}
// swim wave: sideways sine, growing toward the tail
const wave = Math.sin(phase - i*.55) * (i/SEG) * 3.1*size;

הגוף עצמו הוא פוליגון אחד: לכל חוליה מחושב וקטור ניצב, ומוזזים ממנו שני קדקודים ימינה ושמאלה לפי פרופיל רוחב קבוע (צר בראש, רחב בכתפיים, מתחדד לזנב). כתמי הצבע של הקוי מצוירים בתוך ctx.clip() של אותו פוליגון — כך הם נעים ומתעוותים יחד עם הגוף. קצב הנפנוף (phase) מואץ לפי המהירות, so הדג "מתאמץ" כשהוא רודף אחרי אוכל.

04 — האור

שלוש שכבות קנבס, זוהר אחד

הסצנה בנויה משלושה אלמנטים של <canvas> בערימה, כל אחד עם תפקיד ותדירות ציור משלו:

// motion trails: fade the layer toward TRANSPARENT (not black),
// so the nebula behind stays visible through the trail
sc.globalCompositeOperation = 'destination-out';
sc.fillStyle = 'rgba(0,0,0,0.16)';
sc.fillRect(0, 0, W, H);
sc.globalCompositeOperation = 'source-over';

הזוהר של כל דג, של כוכבי המאכל ושל החלקיקים מצויר ב-globalCompositeOperation='lighter' — חיבור אדיטיבי של אור, כמו חשיפה כפולה בצילום. זה מה שהופך משטח שטוח ל"ליל". אין אף shadowBlur בכל הפרויקט — הוא יקר; כל הזוהר הוא גרדיאנטים רדיאליים זולים.

ביצועים: עד 26 דגים (לפי שטח המסך), חישוב שכנים בריבוע מרחק בלי שורש, DPR חסום ב-2, ורק transforms — אין layout בלולאה. התוצאה: 60fps גם על מחשב צנוע.
05 — האינטראקציה

האכלה, אכילה, ולידת כוכב

לולאת המשוב המלאה, צעד אחר צעד:

  1. pointerdown מפיל כוכב־מאכל: כוכב 8 קודקודים מסתובב, פועם, ונודד לאט. טבעת אדווה מתפשטת מהנקודה.
  2. כל דג בטווח מקבל כוח משיכה שמתחזק ככל שמתקרבים, ומכפיל מהירות ×1.5 — הלהקה מתכנסת.
  3. כשראש דג נוגע בכוכב — הכוכב "נאכל": 12 ניצוצות מתפזרים, הדג עצמו זוהר לרגע (glowPulse שדועך אקספוננציאלית).
  4. כוכב קטן עף בקשת (easeOutCubic + קפיצת סינוס) לנקודה אקראית ברקיע — ושם הוא נטמע במערך הכוכבים המהבהבים, בגוון זהב, לתמיד. המונה מתעדכן.
// the eaten star ascends and becomes a permanent sky star
const u = Math.min(fl.t / fl.dur, 1);
const e = 1 - Math.pow(1 - u, 3);            // easeOutCubic
const x = fl.sx + (fl.tx - fl.sx) * e;
const y = fl.sy + (fl.ty - fl.sy) * e - Math.sin(u * Math.PI) * 60;
if (u >= 1) skyStars.push({ x: fl.tx, y: fl.ty, born: true, ... });

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

06 — הצליל

WebAudio: פנטטוני, אופציונלי, מנומס

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

note(f, dur, vol) {
  const o = ctx.createOscillator(), g = ctx.createGain();
  o.frequency.value = f;                          // pentatonic: C D E G A
  g.gain.linearRampToValueAtTime(vol, t + .015); // fast attack
  g.gain.exponentialRampToValueAtTime(.0001, t + dur); // long decay
}

פיזור כוכב = תו נמוך אקראי מהסולם; אכילה = תו גבוה + אוקטבה חלשה מעליו (כמו פעמון). סולם פנטטוני נבחר כי כל צירוף אקראי שלו נשמע נכון — המבקר מנגן מוזיקה בלי לדעת.

07 — העיצוב

פלטה, טיפוגרפיה, ריטואל כניסה

פלטת "ליל זהב" — שישה ערכים, מצב רוח אחד:

#04060F לילה
#0A1026 עומק
#AAB6D8 דיו
#FFD98A זהב
#FF8C42 קוי
#F3EFE6 ירח

Frank Ruhl Libre 900 לכותרת — סריף עברי כבד עם נוכחות של שער ספר, על גרדיאנט ירח→זהב. Heebo 300 לטקסט רץ — קל, אוורירי, לא מתחרה בשמיים. הכותרת נכנסת ב-cubic-bezier(.16,1,.3,1) (expo-out), הקנבס עולה ב-fade של 1.6 שניות רק אחרי הפריים הראשון — אין הבזק של דף ריק.

prefers-reduced-motion מכובד: מהירות הדגים יורדת ל-40%, הדעיכה מתקצרת, ואנימציות הכניסה מדלגות לסופן.

08 — העמוד

אתר שלם שצף מעל השמיים

ההרחבה לאתר מלא נבנתה על עיקרון אחד: הקנבס לא זז — הדף זז מעליו. שלושת הקנבסים נשארים fixed, ו-<main> מקבל pointer-events:none בעוד שהפאנלים עצמם מחזירים pointer-events:auto. כך לחיצה ברווח שבין פאנלים עוברת דרך הדף אל קנבס הסצנה — והשמיים נשארים אינטראקטיביים לכל אורך הגלילה.

כלל הביצועים נשמר גם בדף הארוך: transforms ו-opacity בלבד באנימציות, מאזין גלילה אחד עם rAF-throttle, ואפס layout בלולאת הציור.
09 — הגלריה

שלושה חלומות שצוירו ב-AI

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

  1. שניים שפגשו את שביל החלב"Dreamy painterly illustration, two koi fish swimming through a deep indigo night sky among glowing golden stars and faint violet nebula clouds, orange-and-white koi with long flowing translucent fins, soft warm gold rim light, very dark navy background, tiny specks of starlight, elegant minimal composition, ethereal digital painting, no text"
  2. הזינוק"A single white and orange koi fish leaping in a graceful arc across a starry midnight sky, trailing a ribbon of golden stardust particles behind it, faint crescent moon glow in corner, deep midnight blue palette with warm amber-gold accents, painterly cinematic digital art, soft volumetric light, dark navy background, no text"
  3. המעגל"A school of seven koi fish swirling in a circle around one bright glowing golden star, seen from directly above, deep dark night sky background with wisps of violet and teal nebula, scattered tiny stars, deep navy and amber gold palette, ethereal dreamlike painterly illustration, symmetrical mandala-like composition, no text"

העיקרון בניסוח: לחזור על מילות הפלטה בכל פרומפט (deep navy / indigo, warm gold, orange-and-white koi), לבקש "no text", ולתאר קומפוזיציה — לא רק נושא. התמונות נטענות ב-loading="lazy" ומוגבלות ל-400KB.

10 — שחזור

רוצים לבנות כזה בעצמכם?

  1. שלושה קנבסים בערימה: רקע סטטי, כוכבים מהבהבים, סצנה עם דעיכת destination-out.
  2. מחלקת Boid עם הפרדה/יישור/לכידות + שיטוט + קירות רכים. כווננו משקלים עד שהלהקה "נושמת".
  3. הוסיפו לכל בואיד שרשרת חוליות עם מרווח קבוע + גל סינוס ניצב — פתאום יש לכם דג.
  4. פוליגון גוף מפרופיל רוחב, כתמים בתוך clip, סנפירים כאליפסות מסובבות.
  5. כל זוהר = גרדיאנט רדיאלי ב-lighter. לעולם לא shadowBlur בלולאה.
  6. אינטראקציה אחת טובה (האכלה) עם משוב בשלוש שכבות: חלקיקים, צליל, ושינוי קבוע בעולם.
  7. מסביב — עמוד גליל: קנבסים fixed, פאנלים צפים עם pointer-events חכם, פרלקסה לפי עומק, ו-ScrollTrigger לכניסות.
הלהקה, הכוכבים, הערפילית והצליל — קוד בלבד, מחושבים בזמן אמת. נכסי ה-AI היחידים באתר הם שלוש תמונות הגלריה שבסעיף 09, והפרומפטים שלהן מתועדים שם במלואם.