הקדמה ל-Morphic: ישירות (Directness), חיוּת (Liveness) והיררכיית המחלקה Morph
איך סביבת המשתמש הגרפית של Squeak הופכת כל פיקסל וצורה על המסך לעצם חי בזיכרון, איך מפרקים את תפריט העולם באמצעות הילת ה-Halo וצלמית ה-pickup, ואיך כל עצם או צבע הופך לרכיב גרפי בלחיצת הודעה (`asMorph`).
מודל מנטלי ועיקרון מנחה: אין "ציור מת" על המסך — כל צורה היא עצם חי שאפשר לגעת בו!
ברוב מערכות ההפעלה, מה שאתה רואה על המסך הוא כמו תמונה מצולמת של חפץ — אי אפשר לתפוס כפתור מתוך תפריט ולשלוף אותו החוצה. ב-Morphic, המסך הוא שולחן עבודה פיזי של לגו: כל תפריט, טקסט או מלבן הוא קוביית לגו חיה שאתה יכול לתפוס, לפרק ולשנות בזמן אמת!
הסביבה הגרפית Morphic נשענת על שני עקרונות יסוד הנדסיים בלתי ניתנים להפרדה: ישירוּת (Directness) — הצורות על המסך הן העצמים עצמם, וחיוּת (Liveness) — הסביבה מגיבה למשתמש באופן רציף והמידע מתעדכן חי מול עיניך.
המחלקה Morph (היורשת מ-Object) ניצבת בראש היררכיית הממשק הגרפי, וכל עצם בסמולטוק (מחרוזת, צבע וכו') יודע להפוך את עצמו לרכיב גרפי חי באמצעות שליחת ההודעה asMorph והצגתו בעולם עם openInWorld!
1. שני עקרונות היסוד של Morphic: ישירוּת (Directness) וחיוּת (Liveness)
למה ממשק המשתמש הגרפי של Squeak שונה מכל ממשק חלונות שהכרת?
בוא נפתח יחד דף חדש ומרתק במסע שלנו! עד עכשיו דיברנו על עצמים, מחלקות והודעות שחיים בתוך הזיכרון של Smalltalk. עכשיו אנחנו מגיעים אל הסביבה הגרפית Morphic — ממשק המשתמש הגרפי של סביבת Squeak, שבו כל אלמנט חזותי שאתה רואה על המסך הוא עצם חי בזיכרון, הניתן לתמרון, עריכה ותכנות בזמן אמת.
כדי להבין את הפילוסופיה ההנדסית של Morphic, אני רוצה שתכיר את שני עמודי התווך שמנחים אותה בכל רגע:
👆 1. ישירוּת (Directness)
כל הצורות והאלמנטים המוצגים על גבי המסך הם העצמים עצמם! אין כאן הפרדה מלאכותית בין "ציור על המסך" לבין "אובייקט בקוד". ניתן לבחון כל צורה על המסך, לתפעל אותה ולשנות את מצבה בצורה ישירה ומוחשית לחלוטין.
⚡ 2. חיוּת (Liveness)
הסביבה מגיבה אל המשתמש באופן רציף ודינמי, והמידע על המסך מתעדכן באופן חי (Live) בעקבות כל פעולה או שינוי. אינך צריך "לקמפל מחדש ולהריץ" כדי לראות שינוי בצורה או בקואורדינטות שלה!
2. עקרון ה-Morphic Halo ותרגיל פירוק תפריט העולם (World Menu)
איך לוקחים פריט מתוך תפריט המערכת והופכים אותו לכפתור עצמאי על שולחן העבודה?
איך אנחנו נוגעים בעצם גרפי באופן ישיר? לשם כך קיימת הילת מורפיק (Morphic Halo): כאשר אנחנו לוחצים על הכפתור הכחול (הכפתור האמצעי / גלגלת) של העכבר מעל כל אלמנט חזותי ב-Squeak, הסביבה מקיפה את אותו אלמנט במסגרת של צלמיות שליטה צבעוניות (Halo Icons) המאפשרות הזזה, סיבוב, שינוי גודל, שכפול, פתיחת תפריט חקירה ומחיקה.
כדי שתרגיש בעצמך עד כמה עקרון הישירוּת מרחיק לכת, בוא נעשה יחד את תרגיל ניתוק פריט מתפריט העולם (`World Menu`) בחמישה צעדים פשוטים:
לוחצים לחיצה שמאלית על רקע הסביבה (העולם) כדי להציג את תפריט העולם (World Menu).
לוחצים על הכפתור האמצעי (הכחול) מעל אזור התפריט כדי להקיף את התפריט כולו במסגרת צלמיות (Halo).
לחיצה נוספת בכפתור האמצעי (הכחול) מעל אחד מפריטי התפריט (למשל help…) תקיף אך ורק את אותו פריט בודד ב-Halo ייעודי משלו!
תופסים את צלמית ה-pickup (הצלמית השחורה הממוקמת במרכז השורה העליונה של ה-Halo) וגוררים את הפריט החוצה מתוך התפריט.
הפריט help… מנותק כעת מתפריט העולם, ומתפקד על שולחן העבודה ככפתור פעולה עצמאי לחלוטין!
3. היררכיית המחלקות של Morphic: הכל מתחיל במחלקה Morph
הכרת עץ ההורשה מ-Object דרך Morph ועד ל-BorderedMorph, StringMorph ו-HaloMorph
מאחורי הקלעים של הקסם הזה ניצבת ארכיטקטורת מחלקות מופתית. המחלקה Morph (היורשת ישירות מ-Object) עומדת בראש היררכיית רכיבי הממשק הגרפי.
ממנה מתפצלות תת-מחלקות ייעודיות לכל סוג של רכיב חזותי — החל מרכיבים בעלי מסגרת ושוליים (BorderedMorph וממנו RectangleMorph, EllipseMorph, PolygonMorph, WireMorph, StarMorph, AlignmentMorph, MenuMorph) ועד לרכיבי תמונה, טקסט, מדי התקדמות ואפילו ההילות עצמן (HaloMorph)!
היררכיית המחלקות תחת Object → Morph
4. הפיכת כל עצם וצבע לרכיב גרפי: asMorph, openInWorld ו-openViewerForArgument
איך מחרוזת טקסט או עצם מסוג Color הופכים לצורה חיה על שולחן העבודה?
בזכות הכוח של Smalltalk, כל עצם ניתן להמרה לרכיב גרפי באמצעות שליחת ההודעה הפשוטה asMorph! בוא נראה מה קורה כשאנחנו לוקחים מחרוזת טקסט רגילה ומזמינים אותה אל העולם הגרפי:
s openViewerForArgument.
- הביטוי
'Hello' asMorphממיר את המחרוזת ומחזיר מופע של המחלקהStringMorph. - שליחת ההודעה
openInWorldמציגה את הרכיב ישירות על גבי שולחן העבודה (העולם הגרפי של Squeak). - שליחת ההודעה
openViewerForArgumentפותחת חלון אינטראקציה חכם (Viewer) המציג את תכונות הרכיב (כגון קואורדינטות וזווית כיוון המתעדכנות בזמן אמת כשמזיזים את הצורה!), כולל משולשים קטנים לקביעת ערכי פרמטרים ולחיצים לשליחת הודעות תנועה וצליל ישירות אל העצם!
תרגיל למחשבה: כיצד ניתן לגרום לעצם גרפי "לצחוק"?
בתוך חלון ה-Viewer שנפתח באמצעות s openViewerForArgument (או מתוך ה-Halo של הרכיב), קיימות קטגוריות של פקודות מוכנות עבור הרכיב — וביניהן פקודות תנועה וצליל (כגון make sound עם צליל צחוק או שליחת הודעת השמעת צליל ישירות לעצם)! לחיצה על סימן הקריאה שלצד הפקודה ב-Viewer גורמת לעצם הגרפי להשמיע צליל צחוק מיד!
ומה קורה כשאנחנו שולחים את ההודעה asMorph אל עצם המייצג צבע, כמו בביטוי Color red asMorph openInWorld?
מיד מופיע מלבן אדום בפינה השמאלית-העליונה של המסך! בוא נסתכל על מימוש המתודה Color>>asMorph ונגלה איך זה עובד:
^ (RectangleMorph new)
fillStyle: self;
borderWidth: 0;
yourself
שים לב לאלגנטיות: עצם הצבע (self) יוצר מופע חדש של RectangleMorph, קובע את צבע המילוי שלו (fillStyle: self) להיות הצבע עצמו, מאפס את עובי המסגרת (borderWidth: 0), ומחזיר את המלבן הנוצר בעזרת yourself!
מיני-סימולציה 17.1: סייר מחלקות עץ ה-Morphic
מעבדת הישירוּת והחיוּת: פירוק תפריט העולם עם ה-Halo, הרצת asMorph וחלון ה-Viewer
הגשר הנרטיבי: הצגנו צורות חיות על המסך – אבל איך שולטים במיקום, בגודל ובצבע שלהן מתוך הקוד?
ראינו שפקודה כמו Color red asMorph openInWorld מציירת מלבן אדום בפינה השמאלית-העליונה של המסך. למה דווקא בפינה השמאלית-העליונה? איך בנויה מערכת הצירים של המסך ב-Squeak, ומדוע ציר ה-y שלה מתקדם כלפי מטה? בוא נעבור לסעיף 17.2 ונשלוט במתודות המיקום, הממדים והצבע!
מתודות Morph בסיסיות ומערכת הצירים של המסך (position, extent, color)
הכרת מערכת הצירים שבה ראשית הצירים (0,0) בפינה השמאלית-העליונה וציר ה-y חיובי כלפי מטה, המלבן החוסם (Bounding Box), ומתודות הגישה והעדכון למיקום, ממדים וצבע.
מודל מנטלי ועיקרון מנחה: במסך המחשב ראשית הצירים (0,0) נמצאת למעלה משמאל, וציר ה-y יורד למטה!
תחשוב על דף באנגלית או על קרן שסורקת מסך טלוויזיה מלמעלה למטה: אנחנו מתחילים בפינה השמאלית-העליונה (0,0), מתקדמים ימינה ככל ש-x גדל, ויורדים שורה-שורה כלפי מטה ככל ש-y גדל!
המיקום (position) והממדים (extent) של כל צורה ב-Morphic מוגדרים תמיד כעצמים מסוג נקודה (Point בצורה x@y או width@height) ביחס לפינה השמאלית-עליונה של המלבן החוסם (Bounding Box) שלה.
מכיוון שנקודות תומכות בחיבור וקטורי טבעי, הזזת צורה ב-10 פיקסלים ימינה ו-10 פיקסלים מטה מתבצעת בביטוי האלגנטי:
aMorph position: (aMorph position + (10@10)).
1. מערכת הצירים של המסך והמלבן החוסם (Bounding Box)
למה (0,0) נמצאת בפינה השמאלית-העליונה ואיך נקבע מיקומה של כל צורה?
לפני שנזיז צורות על המסך, אנחנו חייבים להכיר את "מפת הדרכים" של המסך ב-Squeak. בניגוד למערכת הצירים הקרטזית שאתה מכיר משיעורי מתמטיקה (שבה ציר ה-y עולה כלפי מעלה), במערכות גרפיקה ממוחשבות מערכת הצירים של המסך פועלת כך:
- ראשית הצירים
(0,0)ממוקמת בפינה השמאלית-העליונה של המסך. - ערכים חיוביים של ציר
xמתקדמים ימינה. - ערכים חיוביים של ציר
yמתקדמים כלפי מטה. - המלבן החוסם (Bounding Box): גם אם הצורה שלנו היא כוכב או עיגול, מיקומה (
position) מוגדר תמיד ביחס לפינה השמאלית-עליונה של המלבן החוסם שלה!
תיבת מחשבה: מדוע ציר ה-y במחשבים הפוך ממתמטיקה קלאסית (גדל כלפי מטה)?
היסטורית, מסכי המחשב הראשונים (מסכי קתודה CRT) סרקו את התמונה באמצעות קרן אלקטרונים שהתחילה בפינה השמאלית-העליונה של המסך (שורה 0, עמודה 0), רצה משמאל לימין לאורך השורה, וירדה שורה אחר שורה כלפי מטה — ממש כמו שאנחנו כותבים וקוראים טקסט מדף מלמעלה למטה! לכן קואורדינטת y מייצגת כמה שורות פיקסלים ירדנו מראש המסך.
2. מתודות הגישה והעדכון המרכזיות של Morph
שליטה מלאה במיקום (position), בממדים (extent) ובצבע (color) באמצעות עצמי Point ו-Color
מיקום הצורה על המסך
• position: מחזירה מופע של Point (למשל 50@100) המציין את מיקום הפינה השמאלית-עליונה של הצורה.
• position: aPoint: מעבירה ומציבה את הצורה במיקום המבוקש.
ממדי המלבן החוסם
• extent: מחזירה נקודה (width @ height) המייצגת את רוחב וגובה המלבן החוסם של הצורה.
• extent: aPoint: משנה וקובעת את ממדי הצורה.
צבע הצורה
• color: aColor: מקבלת עצם מסוג Color ומשנה מיד את צבע הצורה על המסך (למשל Color yellow, Color green או Color blue).
לדוגמה, כדי להזיז צורה קיימת aMorph ב-10 פיקסלים ימינה וב-10 פיקסלים מטה (תנועה באלכסון), אנחנו שואלים אותה מה מיקומה הנוכחי, מוסיפים לו את הנקודה 10@10 ומציבים בחזרה:
3. תרגיל מעשי מסכם: הצורה הצהובה והצורה הירוקה
יישום כל מתודות המיקום, הממדים והצבע בתרגיל מודרך שלב אחר שלב
בוא נפתור יחד את התרגיל המעשי המלא: ניצור שתי צורות מסוג Morph — אחת צהובה ואחת ירוקה — ונבצע עליהן סדרה של פעולות גיאומטריות:
yellowMorph := Morph new color: Color yellow; openInWorld.
greenMorph := Morph new color: Color green; openInWorld.
"2. הזזת הצורה הצהובה 10 פיקסלים ימינה, 10 פעמים"
10 timesRepeat: [ yellowMorph position: (yellowMorph position + (10@0)) ].
"3. הזזת הצורה הירוקה באלכסון (10 ימינה ו-10 למטה), 10 פעמים"
10 timesRepeat: [ greenMorph position: (greenMorph position + (10@10)) ].
"4. לאחר הגדלת הצורה הצהובה בעכבר, השוואת גודל הירוקה לזה של הצהובה"
greenMorph extent: yellowMorph extent.
"5. הצגת המיקום והגודל ושינוי צבע הצורה הצהובה לכחול"
Transcript show: yellowMorph position; cr; show: yellowMorph extent; cr.
yellowMorph color: Color blue.
ראשית הצירים (0,0) בפינה השמאלית-העליונה, position ו-extent
מיני-סימולציה 17.2: בוחן מתודות הגיאומטריה של Morph
מעבדת מערכת הצירים (XY Grid Sandbox): תרגיל הצורה הצהובה והירוקה
הגשר הנרטיבי: שלטנו בצורות נפרדות – אבל איך מרכיבים צורה אחת בתוך צורה אחרת כך שינועו יחד?
בממשק גרפי אמיתי, חלון מכיל כפתורים, וכפתור מכיל תווית טקסט או אייקון. כשאנחנו מזיזים את החלון, אנחנו מצפים שכל הרכיבים שבתוכו יזוזו יחד איתו אוטומטית! איך בונים עץ הכלה היררכי כזה ב-Morphic בעזרת addMorph: ו-removeMorph:? בוא נעבור לסעיף 17.3!
יחס הכלה והרכבת צורות (Composition): addMorph:, removeMorph: ומרכוז
איך בונים עץ היררכי שבו רכיב אב (Owner) מכיל תת-רכיבים (Submorphs), כיצד הזזת המלבן המכיל גוררת איתה את הכוכב המוכל, מרכוז בעזרת center:, והרכבה או הפרדה דרך הקוד או דרך הממשק הגרפי.
מודל מנטלי ועיקרון מנחה: עץ הכלה היררכי — כשהאב זז, כל ילדיו (Submorphs) זזים יחד איתו!
תחשוב על מדבקה של כוכב צהוב שהדבקת על קופסת קרטון אדומה: אם תזיז את הקופסה האדומה על השולחן, הכוכב יזוז יחד איתה אוטומטית. אם תזיז את הכוכב עצמו קצת הצידה על גבי הקופסה, ואז תזיז שוב את הקופסה — הכוכב עדיין ייסע יחד עם הקופסה!
המתודה rect addMorph: star מוסיפה את הכוכב לרשימת ה-submorphs של המלבן. המתודה rect removeMorph: star מנתקת אותו ומסירה אותו מהמסך, אך הוא ממשיך לחיות בזיכרון וניתן להציגו כעצמאי עם star openInWorld.
ב-Morphic כל פעולת קוד קיימת גם באופן חזותי ישיר: אפשר לשלב צורות דרך תפריט ההקשר (קליק ימני/צהוב → embed → into) ולנתק אותן בלחיצה כפולה על הכפתור האמצעי (הכחול) וגרירת ההילה הפנימית החוצה!
1. הרכבת צורות בקוד באמצעות addMorph:
בניית יחס אב-בן (Parent/Owner מול Child/Submorphs) בין מלבן אדום לכוכב צהוב
רכיבי Morph אינם חיים רק כצורות בודדות על המסך — הם בנויים במבנה של עץ הכלה היררכי (Containment Hierarchy), שבו רכיב אחד משמש כמכיל (Parent / Owner) של רכיבים אחרים המכונים תת-רכיבים (Child / Submorphs).
בוא נבחן את קטע הקוד היוצר מלבן אדום ומכניס לתוכו כוכב צהוב (StarMorph):
rect openInWorld.
star := StarMorph new color: Color yellow.
rect addMorph: star.
star position: rect position.
- ההודעה
rect addMorph: starמוסיפה את הרכיבstarלתוך רשימת ה-submorphsשלrect. - ההודעה
star position: rect positionמצמידה את הפינה השמאלית-עליונה של המלבן החוסם של הכוכב לפינה השמאלית-עליונה של המלבן האדום. - מרכוז מדויק (Geometry protocol): אם נרצה למרכז את הכוכב בדיוק במרכז המלבן, נוכל להשתמש במתודה
center:ולכתוב:star center: rect center!
2. התנהגות בעת תנועה וניתוק תלות באמצעות removeMorph:
מה קורה כשמזיזים את האב, מה קורה כשמזיזים את הבן, ואיך מפרידים ביניהם?
🚀 א. תנועה של המלבן המכיל מול הכוכב המוכל
• הזזת המלבן המכיל: אם נריץ rect position: (rect position + (10@10)), שתי הצורות נעות יחדיו! הכוכב זז אוטומטית יחד עם המלבן שמכיל אותו.
• הזזת הכוכב המוכל ישירות: אם נריץ star position: (star position + (10@10)), הכוכב נע ביחס למקומו, אך עדיין נשאר מוכל במלבן — כך שאם נזיז שוב את המלבן, הכוכב עדיין ינוע יחד איתו!
✂️ ב. הפרדת רכיבים בקוד (removeMorph:)
• הפקודה rect removeMorph: star מנתקת את הכוכב מרשימת ה-submorphs של המלבן.
• ברגע הניתוק הכוכב מוסר מהמסך, אך הוא ממשיך להתקיים כעצם חי בזיכרון!
• ניתן להציגו מחדש כרכיב עצמאי לחלוטין בעולם באמצעות הפקודה star openInWorld.
3. הרכבה והפרדה חזותית באמצעות הממשק הגרפי וה-Halo
איך מבצעים embed into וניתוק צורות בעכבר מבלי לכתוב שורת קוד אחת?
ברוח עקרון הישירוּת (Directness) של Morphic, כל פעולת הרכבה והפרדה שעשינו בקוד ניתנת לביצוע גם באופן חזותי ישיר עם העכבר:
- הרכבה חזותית (
embed into): פותחים את תפריט העולם → בוחרים בפריטobjects (o)→ לוחצים על כפתורGraphicsלפתיחת חלון בחירת צורות → גוררים שתי צורות ומניחים אותן זו על גבי זו → לוחצים לחיצה ימנית (צהובה) על הצורה העליונה לפתיחת תפריט ההקשר → בוחרים בתפריט-הבתembed→ ובוחריםintoכדי לשלב את הצורה העליונה בתוך הצורה שמתחתיה! - הפרדה חזותית באמצעות ה-Halo: לחיצה אחת על הכפתור האמצעי (הכחול) מעלה את ה-Halo של הצורה המכילה (האב). לחיצה שנייה (כפולה) על הכפתור האמצעי מעלה את ה-Halo של הצורה הפנימית המוכלת (הכוכב); גרירתה החוצה באמצעות סמן העכבר מנתקת אותה אוטומטית מצורת האב!
השוואה בין צורות נפרדות לבין צורה מוכלת (rect addMorph: star)
מיני-סימולציה 17.3: חוקר פעולות ההרכבה וההפרדה (קוד מול GUI)
מעבדת ההכלה החיה: המלבן האדום (rect) והכוכב הצהוב (star)
הגשר הנרטיבי: הרכבנו צורות קיימות – אבל איך יוצרים צורה גרפית חדשה לגמרי בעלת מראה ייחודי?
עד עכשיו השתמשנו במחלקות מוכנות כמו RectangleMorph ו-StarMorph. אבל מה אם אנחנו רוצים להגדיר רכיב בעל מראה ייחודי משלנו — למשל צורת צלב (CrossMorph)? איך מערכת Squeak מציירת רכיבים באמצעות drawOn: aCanvas, ואיך מבטיחים שהצלב יגיב ללחיצות עכבר רק על שטח הצלב עצמו ולא על הפינות הריקות של המלבן החוסם? בוא נעבור לסעיף 17.4!
יצירת צורות גרפיות: drawOn: aCanvas, המחלקה CrossMorph ודריסת containsPoint:
איך יוצרים תת-מחלקה של Morph ודורסים את מתודת הציור drawOn: aCanvas, חישוב הזרוע האופקית והאנכית של הצלב באמצעות bounds insetBy:, ודיוק משטח הלחיצה באמצעות דריסת containsPoint: aPoint.
מודל מנטלי ועיקרון מנחה: אתה לא מצייר ישירות למסך — הסביבה נותנת לך בד ציור (Canvas) ומבקשת ממך: drawOn:!
תחשוב על צייר שמקבל כן ציור (aCanvas): בכל פעם שצריך לרענן את המסך, מערכת Squeak ניגשת אל הצורה שלך ושולחת לה אוטומטית את ההודעה drawOn: aCanvas — והצורה מציירת את עצמה על הקנבס!
כברירת מחדל, כל Morph מגיב ללחיצות עכבר בכל שטח המלבן החוסם שלו (bounds). אם הצורה שלך אינה מלבנית מלאה (כמו CrossMorph), חובה לדרוס גם את containsPoint: aPoint כדי שהפינות הריקות לא יגיבו ללחיצות!
המתודה bounds insetBy: aPoint של מחלקת Rectangle מכווצת את גבולות המלבן החוסם כלפי פנים לפי ערכי הנקודה — וכך ברשת של $3 \times 3$ אנו גוזרים בקלות את הזרוע האופקית והזרוע האנכית של הצלב!
1. מנגנון הציור באמצעות drawOn: aCanvas ומקרה הבוחן CrossMorph
איך מציירים צלב המורכב משני מלבנים מוצלבים בעזרת חלוקה לרשת 3x3 ו-insetBy:?
כאשר אנחנו מעוניינים להגדיר רכיב בעל מראה חזותי ייחודי משלנו, אנחנו לא מרכיבים צורות קיימות אלא יוצרים תת-מחלקה של Morph ודורסים את מתודת הציור הבסיסית drawOn: aCanvas.
בכל פעם שנדרש לצייר את הרכיב מחדש על המסך, סביבת Squeak שולחת אליו אוטומטית את ההודעה drawOn: aCanvas, כאשר הארגומנט aCanvas הוא עצם מסוג Canvas המספק שירותי ציור פרימיטיביים (ציור קווים, מלבנים, שקיפות וטקסט).
בוא נגדיר יחד מחלקה חדשה בשם CrossMorph (תת-מחלקה של Morph) המציירת צלב המורכב משני מלבנים מוצלבים — זרוע אופקית וזרוע אנכית:
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Morphic-Demo'
איך נחשב את שני המלבנים המרכיבים את הצלב בתוך המתודה drawOn: aCanvas?
נחלק את המלבן החוסם של הצורה (self bounds) לרשת דמיונית של $3 \times 3$ על ידי חישוב שליש הגובה (self height / 3.0) ושליש הרוחב (self width / 3.0).
כעת נשתמש במתודה הנפלאה insetBy: של מחלקת Rectangle, שמקבלת נקודה ומחזירה מלבן חדש שהקואורדינטות של פינתו השמאלית-עליונה והימנית-תחתונה מוסטות כלפי פנים לפי ערכי הנקודה:
| crossHeight crossWidth horizontalBar verticalBar |
crossHeight := self height / 3.0.
crossWidth := self width / 3.0.
horizontalBar := self bounds insetBy: 0 @ crossHeight.
verticalBar := self bounds insetBy: crossWidth @ 0.
aCanvas fillRectangle: horizontalBar color: self color.
aCanvas fillRectangle: verticalBar color: self color
self bounds insetBy: 0 @ crossHeight: אינו מכווץ כלל את הרוחב (0בציר ה-x), אך מכווץ שליש מלמעלה ושליש מלמטה בציר ה-y — וכך מתקבלת הזרוע האופקית (horizontalBar)!self bounds insetBy: crossWidth @ 0: מכווץ שליש משמאל ושליש מימין בציר ה-x, ואינו מכווץ כלל את הגובה (0בציר ה-y) — וכך מתקבלת הזרוע האנכית (verticalBar)!- לבסוף, אנו מציירים את שני המלבנים על גבי ה-
Canvasבאמצעות המתודהfillRectangle:color:.
2. זיהוי פגיעת עכבר מדויקת: דריסת המתודה containsPoint: aPoint
למה הצלב מגיב ללחיצות גם בפינות הריקות שלו, ואיך פותרים זאת בעזרת or:?
אם ניצור מופע של CrossMorph וננסה לתפוס אותו עם העכבר באחת מארבע הפינות הריקות שמסביב לזרועות הצלב, נגלה הפתעה: הצלב נתפס ומגיב ללחיצה גם באוויר הריק!
מדוע זה קורה? משום שכברירת מחדל, מתודת containsPoint: במחלקה Morph בודקת רק אם נקודת העכבר נמצאת בתוך המלבן החוסם (self bounds).
כדי להבטיח שצורת הצלב תגיב אך ורק כאשר לוחצים על שטח הצלב עצמו (ולא על הפינות הריקות), אנחנו דורסים את המתודה containsPoint: aPoint ומחזירים חיבור לוגי (or:) הבודק האם הנקודה נמצאת בתוך הזרוע האופקית או בתוך הזרוע האנכית:
| crossHeight crossWidth horizontalBar verticalBar |
crossHeight := self height / 3.0.
crossWidth := self width / 3.0.
horizontalBar := self bounds insetBy: 0 @ crossHeight.
verticalBar := self bounds insetBy: crossWidth @ 0.
^ (horizontalBar containsPoint: aPoint) or: [verticalBar containsPoint: aPoint]
🛠️ שכתוב 1: מניעת כפילות קוד באמצעות מתודות עזר
שים לב שחישובי horizontalBar ו-verticalBar מופיעים פעמיים — גם ב-drawOn: וגם ב-containsPoint:. בהנדסת תוכנה נכונה אנו מרכזים את חישובי המלבנים במתודות עזר פרטיות (horizontalBar ו-verticalBar) כדי למנוע כפילות קוד!
🎨 שכתוב 2: מניעת ציור כפול של מרכז הצלב
כאשר מציירים את horizontalBar ועליו את verticalBar, הריבוע המרכזי המשותף לשני המלבנים מצויר פעמיים! (בעיה בולטת אם הצבע שקוף למחצה). ניתן לפצל את הזרוע האנכית לחלק עליון וחלק תחתון כך שהשטח המשותף יצויר פעם אחת בלבד.
חלוקת המלבן החוסם לרשת 3x3, כיווץ עם insetBy: וסינון הפינות הריקות ב-containsPoint:
מיני-סימולציה 17.4: מפענח המתודה bounds insetBy: ב-CrossMorph
מעבדת בדיקת הפגיעה (Hit-Test Lab): לחץ על ה-CrossMorph ועל הפינות הריקות!
containsPoint::
containsPoint: aPoint!
הגשר הנרטיבי: הצלב שלנו יודע לצייר את עצמו ולזהות פגיעת עכבר – עכשיו בוא נלמד אותו להגיב ללחיצות, למקלדת ולאנימציה!
זיהוי שהעכבר לחץ על הצלב (containsPoint:) הוא רק הצעד הראשון. איך גורמים לצלב לגדול בלחיצה שמאלית, לקטון בלחיצה ימנית, להחליף צבע כשהעכבר מרחף מעליו, לזוז עם חצי המקלדת ולהתנפנף באנימציה מחזורית? בוא נעבור לסעיף 17.5!
אירועי עכבר, אירועי מקלדת ואנימציה באמצעות מנגנון ה-Stepping
הטיפול הדו-שלבי באירועי עכבר (handlesMouse... ו-mouse...), חשיבות הפקודה הקריטית self changed, הטיפול התלת-שלבי באירועי מקלדת (פוקוס newKeyboardFocus: וקודי חצים 28–31), ופרוטוקול האנימציה (stepTime ו-step).
מודל מנטלי ועיקרון מנחה: קודם מרימים יד ("אני מאזין!"), אחר כך מגיבים — ובסוף קוראים ל-self changed!
מערכת Morphic לא שולחת אירועי עכבר לכל רכיב סתם כך. בשלב 1 הרכיב מצהיר שהוא מעוניין להאזין (למשל handlesMouseDown: ^ true), ובשלב 2 הוא מממש את מתודת התגובה (mouseDown: anEvent) וקורא ל-self changed!
כדי לקלוט מקשים: (1) בכניסת עכבר לוקחים פוקוס מקלדת (anEvent hand newKeyboardFocus: self); (2) מטפלים בהקשה ב-handleKeystroke: לפי keyValue; (3) ביציאת עכבר משחררים פוקוס עם newKeyboardFocus: nil.
בפרוטוקול stepping and presenter, המתודה stepTime קובעת את מרווח הזמן במילישניות בין פעימות, והמתודה step נקראת אוטומטית בכל פעימה כדי לבצע את צעד האנימציה!
1. אירועי עכבר (Mouse Events): מנגנון דו-שלבי ופקודת self changed
שלב ההכרזה (handlesMouse...) ושלב התגובה (mouseDown:, mouseEnter:, mouseLeave:)
הטיפול באירועי עכבר בסמולטוק מתבצע תמיד בשני שלבים מובנים:
- שלב 1 – הכרזה: הרכיב מודיע למערכת שהוא מעוניין להאזין לקבוצת אירועים באמצעות דריסת מתודות המתחילות ב-
handles...והחזרת^ true:handlesMouseDown: anEvent– מאזינה לאירועי לחיצה, שחרור ותזוזה של העכבר.handlesMouseOver: anEvent– מאזינה לאירועי כניסה ויציאה של סמן העכבר משטח הצורה.handlesMouseOverDragging: anEvent– מאזינה לאירועי גרירה פנימה והחוצה משטח הצורה.
- שלב 2 – תגובה: מימוש מתודת הטיפול הייעודית לאירוע שהתרחש:
mouseDown: anEvent(לחיצה),mouseUp: anEvent(שחרור),mouseMove: anEvent(תזוזה),mouseEnter: anEvent(כניסה לשטח הצורה) ו-mouseLeave: anEvent(יציאה משטח הצורה).
בוא נראה דוגמה מלאה ב-CrossMorph (כולל פתרון תרגילי ההרחבה): הגדלת הצלב בלחיצה שמאלית (הכפתור האדום – redButtonPressed), הקטנתו בלחיצה ימנית (הכפתור הצהוב – yellowButtonPressed), והחלפת צבעו לאדום בכניסת עכבר ולירוק ביציאת עכבר:
^ true
CrossMorph >> mouseDown: anEvent
anEvent redButtonPressed
ifTrue: [self extent: (self extent * 1.1)].
anEvent yellowButtonPressed
ifTrue: [self extent: (self extent * 0.9)].
self changed
self changed היא קריטית?
כאשר אנחנו משנים את המצב הפנימי של הרכיב, שליחת ההודעה self changed מודיעה לסביבת Morphic שהרכיב השתנה ויש לתזמן שליחת drawOn: לצורך ציורו מחדש על המסך!
2. אירועי מקלדת (Keyboard Events): מנגנון תלת-שלבי ופוקוס המקלדת
איך מעבירים פוקוס מקלדת דרך anEvent hand, קולטים את מקשי החצים (28–31) ומשחררים פוקוס?
בניגוד לעכבר שתמיד מצביע על נקודה ספציפית במסך, הקשת מקלדת צריכה לדעת לאיזה רכיב על המסך יש כרגע "פוקוס מקלדת" (Keyboard Focus)! לכן טיפול במקלדת מתבצע בשלושה שלבים מובנים:
CrossMorph >> handlesMouseOver: anEvent
^ true
CrossMorph >> mouseEnter: anEvent
self color: Color red.
anEvent hand newKeyboardFocus: self
"שלב 2: טיפול בהקשת המקש (handleKeystroke:) לפי קוד המקש keyValue"
CrossMorph >> handleKeystroke: anEvent
| keyValue |
keyValue := anEvent keyValue.
keyValue = 30 ifTrue: [self position: self position - (0 @ 1)]. "חץ למעלה"
keyValue = 31 ifTrue: [self position: self position + (0 @ 1)]. "חץ למטה"
keyValue = 29 ifTrue: [self position: self position + (1 @ 0)]. "חץ ימינה"
keyValue = 28 ifTrue: [self position: self position - (1 @ 0)] "חץ שמאלה"
"שלב 3: שחרור פוקוס המקלדת כאשר העכבר יוצא משטח הצורה (וצביעה בירוק)"
CrossMorph >> mouseLeave: anEvent
self color: Color green.
anEvent hand newKeyboardFocus: nil
שים לב: ההודעה anEvent hand שולפת מתוך האירוע את העצם המייצג את מצביע העכבר הפעיל (HandMorph), וההודעה newKeyboardFocus: self מורה לו להעביר את קליטת המקשים אל הרכיב שלנו (או לבטל את הפוקוס באמצעות העברת nil ביציאה).
3. אנימציה באמצעות מנגנון ה-Stepping (פרוטוקול stepping and presenter)
איך יוצרים תנועה רציפה ומחזורית בעזרת stepTime, step ומתודות הבקרה?
סביבת Morphic כוללת תשתית מובנית לביצוע אנימציות מחזוריות הרצות תחת הפרוטוקול stepping and presenter (ציור מחדש בסדרה של "צעדים"; קצב של 15–30 מילישניות יוצר תחושת רצף חלקה לעין, או למשל 100 מילישניות עבור 10 צעדים בשנייה):
מחזירה מספר שלם המציין את מרווח הזמן המינימלי במילישניות בין צעד אנימציה אחד למשנהו (למשל ^ 100).
המתודה הנקראת אוטומטית על ידי המערכת בכל פעימת stepTime, ובה מבוצע שינוי המיקום, הזווית או הצבע של הצורה.
מתודות בקרה המפעילות את מנגנון הצעדים בעולם, עוצרות את האנימציה, או מחזירות בוליאני האם האנימציה פעילה כעת.
אירועי עכבר (2 שלבים), אירועי מקלדת (3 שלבים) ומנגנון ה-Stepping
מיני-סימולציה 17.5: מפענח קודי מקשים (keyValue) וכפתורי העכבר ב-Squeak
מעבדת האירועים החיה של CrossMorph: עכבר, מקלדת (28–31) ואנימציית step
הגשר הנרטיבי: שלטנו באירועי עכבר ומקלדת על צורות – אבל איך מבקשים מהמשתמש להקליד טקסט או לבחור פריט מתפריט קופץ?
לפעמים אנחנו לא רוצים לקלוט מקש-מקש בעצמנו, אלא להקפיץ למשתמש תיבת דו-שיח מוכנה להקלדת שם (FillInTheBlank) או תפריט קופץ תלוי-הקשר עם קווי הפרדה (PopUpMenu). איך מפעילים אותם בשורת קוד אחת ב-Squeak ומה הם מחזירים? בוא נעבור לסעיף 17.6!
תיבת דו-שיח (FillInTheBlank) ותפריט קופץ תלוי-הקשר (PopUpMenu)
שימוש במחלקות המוכנות של Squeak לקבלת קלט טקסטואלי מהמשתמש (request:initialAnswer:) ולהצגת תפריט בחירה צף עם קווי הפרדה (labelArray:lines: ו-startUpWithCaption:).
מודל מנטלי ועיקרון מנחה: דיאלוג מיידי עם המשתמש — שולחים הודעה למחלקת השירות ומקבלים תשובה מוכנה!
במקום לבנות מאפס טופס קלט עם שדה טקסט וכפתורי אישור/ביטול בכל פעם שאנחנו צריכים לשאול את המשתמש שאלה קצרה, סביבת Squeak מעמידה לרשותנו "שליחים מוכנים" — אחד לשאלה פתוחה (FillInTheBlank) ואחד לבחירה מרשימה (PopUpMenu)!
הכר היטב את הערכים המוחזרים: FillInTheBlank מחזירה את מחרוזת הטקסט שהוקלדה (או מחרוזת ריקה '' בביטול), ואילו PopUpMenu מחזירה את האינדקס המספרי של הפריט שנבחר (1, 2, 3... או 0 בביטול)!
ב-PopUpMenu labelArray:lines: המערך הראשון קובע את שמות הפריטים בתפריט, והמערך השני מציין את מספרי האינדקסים של הפריטים שאחריהם יימתח קו הפרדה חזותי!
1. תיבת דו-שיח לקלט טקסטואלי: המחלקה FillInTheBlank
איך מציגים חלונית דו-שיח מודאלית ומקבלים מהמשתמש מחרוזת טקסט?
כאשר התוכנית שלנו זקוקה לקלט טקסטואלי מהיר מהמשתמש (למשל שם שחקן, שם קובץ או פרמטר טקסטואלי), איננו צריכים לבנות חלון מורכב בעצמנו. סביבת Squeak מעמידה לרשותנו את המחלקה FillInTheBlank, המציגה חלונית דו-שיח מודאלית שבה המשתמש יכול להקליד טקסט.
ההפעלה מתבצעת באמצעות מתודת המחלקה המרכזית request:initialAnswer:, המקבלת שתי מחרוזות:
request:– המחרוזת הראשונה, המוצגת בכותרת/גוף התיבה כהנחיה למשתמש (בדוגמה:'enter your name:').initialAnswer:– המחרוזת השנייה, המוצגת כערך ברירת המחדל בתוך שדה הטקסט (בדוגמה:'??????'), והיא גם הערך שיוחזר אם המשתמש ילחץ אישור מבלי לשנות דבר.- הערך המוחזר: מחרוזת הטקסט שהקליד המשתמש, או מחרוזת ריקה (
'') אם המשתמש ביטל את הפעולה (Cancel).
2. תפריט קופץ תלוי-הקשר: המחלקה PopUpMenu
אתחול תפריט עם קווי הפרדה (labelArray:lines:) והצגתו במיקום העכבר (startUpWithCaption:)
כאשר אנחנו רוצים להציג למשתמש תפריט בחירה צף על גבי המסך (תפריט תלוי-הקשר), אנחנו משתמשים במחלקה PopUpMenu בשני צעדים ברורים:
choice := menu startUpWithCaption: 'Choose an item'.
📋 צעד 1: יצירת התפריט (labelArray:lines:)
מתודת המחלקה מקבלת שני מערכים:
1. המערך הראשון (labelArray:): שמות הפריטים שיופיעו בתפריט, למשל #('x' 'y' 'z').
2. המערך השני (lines:): מספרי האינדקסים של הפריטים שאחריהם יימתח קו הפרדה חזותי, למשל #(1 2 3).
🖱️ צעד 2: הצגה וקבלת הבחירה (startUpWithCaption:)
ההודעה startUpWithCaption: 'Choose an item' פותחת את התפריט במיקום סמן העכבר עם הכותרת המבוקשת וממתינה לבחירה.
הערך המוחזר: מספר שלם המציין את האינדקס של הפריט שנבחר (1 עבור 'x', 2 עבור 'y', 3 עבור 'z'), או 0 אם המשתמש לחץ מחוץ לתפריט וביטל!
השוואה ויזואלית בין FillInTheBlank לבין PopUpMenu
מיני-סימולציה 17.6: מפענח הערכים המוחזרים מ-FillInTheBlank ומ-PopUpMenu
מעבדת הממשקים של Squeak: הרץ בלייב את FillInTheBlank ואת PopUpMenu!
הגשר הנרטיבי: למדנו לתקשר עם המשתמש בתיבות דו-שיח ובתפריטים – עכשיו איך גורמים לצורות לתקשר זו עם זו בגרירה והשמטה (Drag & Drop)?
אחת הפעולות הטבעיות ביותר בממשק גרפי היא לתפוס צורה אחת עם העכבר, לגרור אותה ולהשמיט אותה לתוך צורה אחרת (Drag and Drop). איך העצם המקבל מחליט אם להסכים לקבל את העצם הנגרר (למשל רק אם הוא כחול!), ואיך גורמים לעצם שנדחה "לזנק" בחזרה למקומו המקורי? בוא נעבור לסעיף 17.7!
מנגנון גרירה והשמטה (Drag and Drop): העצם המקבל והעצם הנגרר
שיתוף הפעולה בין העצם הנגרר (DroppedMorph) לעצם המקבל (ReceiverMorph): המתודות wantsDroppedMorph:event:, repelsMorph:event:, wantsToBeDroppedInto: ו-rejectDropMorphEvent:.
מודל מנטלי ועיקרון מנחה: לטנגו של גרירה והשמטה דרושים שניים — גם המקבל וגם הנגרר צריכים להסכים!
העצם המקבל נשאל ב-wantsDroppedMorph:event: האם הוא מסכים לקלוט את העצם הנגרר. אם הוא מסרב ו-repelsMorph:event: מחזירה true, העצם שנדחה "מועף" בזינוק חזרה למקומו המקורי!
גם לעצם הנגרר יש זכות החלטה! המתודה wantsToBeDroppedInto: aMorph מאפשרת לו לקבוע לתוך אילו מכולות הוא מוכן להישמט, ו-rejectDropMorphEvent: קובעת מה יעשה אם נדחה!
כברירת מחדל ב-Morph, עצם שנדחה פשוט נשאר מונח בנקודת העזיבה מעל המקבל. כדי לגרום לו לחזור למקומו המקורי בעת דחייה, אנו מממשים ב-repelsMorph:event: את שלילת הקבלה: ^ (self wantsDroppedMorph: aMorph event: anEvent) not!
1. שני השחקנים בגרירה והשמטה ומתודות העצם המקבל (ReceiverMorph)
איך בודקים תנאי קבלה (wantsDroppedMorph:event:) ואיך מעיפים עצם שנדחה חזרה למקומו (repelsMorph:event:)?
בכל פעולת גרירה והשמטה (Drag and Drop) ב-Morphic מעורבים תמיד שני עצמים:
(1) העצם הנגרר (DroppedMorph), ו-(2) העצם המקבל (ReceiverMorph) שמעליו המשתמש מרפה מהעצם הנגרר.
בוא נבחן תחילה את שתי המתודות המרכזיות בצד של העצם המקבל (The Receiver):
wantsDroppedMorph: aMorph event: anEvent: נקראת אוטומטית כאשר המשתמש מרפה מהעצם הנגרר מעל שטח המקבל, ומחזירה ערך בוליאני הקובע האם המקבל מסכים לקלוט את העצם ולהכילו. ברירת המחדל במחלקהMorphמחזירה תמידtrue. לדוגמה, אם נרצה שהמקבל יקלוט אך ורק עצמים שצבעם כחול, נדרוס את המתודה כך:
^ aMorph color = Color blue
repelsMorph: aMorph event: anEvent: קובעת מה קורה אם העצם הנגרר נדחה. כברירת מחדל, עצם שנדחה פשוט נשאר מונח במקום העזיבה ללא הכלה במקבל. אם נרצה שהמקבל ידחה את העצם באופן אקטיבי כך שהעצם הנגרר שנדחה "יועף" חזרה בזינוק למקומו המקורי על המסך, עלינו להחזירtrue. המימוש התקני מבוסס על שלילת מתודת הקבלה:
^ (self wantsDroppedMorph: aMorph event: anEvent) not
2. מתודות העצם הנגרר (DroppedMorph) והתרגיל ההדדי המסכם
wantsToBeDroppedInto:, rejectDropMorphEvent: ותרגיל החלפת הצבע בעת דחייה
גם לעצם הנגרר עצמו יש שתי מתודות ייעודיות המאפשרות לו לשלוט בהתנהגותו:
wantsToBeDroppedInto: aMorph: מקבלת את העצם המקבל ומאפשרת לעצם הנגרר לקבוע בעצמו לתוך אילו מכולות או עצמים מקבלים הוא מוכן להישמט (ברירת מחדל:^ true).rejectDropMorphEvent: anEvent: מגדירה את התנהגות העצם הנגרר במקרה שבו המקבל דחה אותו (למשל השמעת צליל, שינוי צבע או אנימציית סירוב, לצד חזרתו למקומו המקורי).
🧩 תרגיל מסכם (תרגיל 17.7): שיתוף פעולה הדדי בין ReceiverMorph ל-DroppedMorph
נגדיר מחלקת ReceiverMorph שמקבלת רק עצמים שצבעם שונה מצבעה שלה, ולצידה מחלקת DroppedMorph שכאשר היא נדחית — היא חוזרת למקומה המקורי ומשנה את צבעה כך שבגרירה הבאה היא תתקבל בהצלחה!
ReceiverMorph >> wantsDroppedMorph: aMorph event: anEvent
^ (aMorph color = self color) not
ReceiverMorph >> repelsMorph: aMorph event: anEvent
^ (self wantsDroppedMorph: aMorph event: anEvent) not
"במחלקת העצם הנגרר - בעת דחייה משנה צבע וחוזר למקומו:"
DroppedMorph >> rejectDropMorphEvent: anEvent
super rejectDropMorphEvent: anEvent.
self color: Color random
מהרגע שבו מרפים מהעצם הנגרר מעל העצם המקבל ועד לקליטה או לזינוק חזרה
מיני-סימולציה 17.7: סייר 4 מתודות ה-Drag and Drop
מכונת ה-Drag & Drop: בדוק קבלה, דחייה וזינוק חזרה (repelsMorph) ותרגיל שינוי הצבע!
הגשר הנרטיבי: עכשיו יש לנו את כל הכלים לבנות משחק שלם – בוא נבנה יחד את קוביית המשחק החיה DieMorph!
למדנו לצייר על Canvas, להגיב ללחיצות עכבר, להריץ אנימציית step ולשלוח הודעות. בסעיף 17.8 נחבר את כל היכולות האלו לפרויקט מופתי אחד: פיתוח המחלקה DieMorph — קוביית משחק אינטראקטיבית (עם 1 עד 9 פאות!) שמתגלגלת באנימציה חיה, מציירת את הנקודות שלה באופן דינמי בעזרת perform:, ונעצרת או ממשיכה בלחיצת עכבר!
דוגמה מלאה: קוביית משחק אינטראקטיבית (DieMorph) והפעלה דינמית עם perform:
פיתוח רכיב Morphic שלם מ-א' ועד ת': ירושה מ-BorderedMorph, משתני המופע faces dieValue isStopped, קואורדינטות יחסיות ב-face1..face9, קריאה דינמית למתודה עם perform:, ואנימציית גלגול הנשלטת בלחיצת עכבר.
מודל מנטלי ועיקרון מנחה: קואורדינטות יחסיות (0..1) + שליחת הודעה דינמית (perform:) = קוד נקי וללא תנאי if!
במקום לקבע מיקומי נקודות בפיקסלים קשיחים (שיישברו ברגע שנגדיל את הקובייה), מתודות face1 עד face9 מחזירות נקודות יחסיות בין 0@0 ל-1@1 (למשל 0.5@0.5 למרכז הקובייה)!
במקום לכתוב סדרת תנאי if ארוכה עבור כל ערך של dieValue, המתודה drawOn: מרכיבה סימבול בזמן ריצה (('face', dieValue asString) asSymbol → #face3) ומפעילה אותו דינמית עם self perform:!
בכל 100 מילישניות (stepTime ^ 100) מתודת step מגרילה ערך אקראי כל עוד isStopped הוא false. לחיצה שמאלית (redButtonPressed) הופכת את isStopped := isStopped not ועוצרת או מפעילה את הקובייה!
1. הגדרת המחלקה DieMorph, אתחול ומתודות הגישה
ירושה מ-BorderedMorph, משתני המופע faces, dieValue, isStopped ואימות קלט
עכשיו אנחנו מחברים את כל מה שלמדנו בפרק הזה ליצירת רכיב Morphic שלם ומרשים: קוביית משחק אינטראקטיבית (DieMorph) התומכת ב-1 עד 9 פאות, מציירת את ערכה באמצעות עיגולים שחורים על רקע ירוק, ומבצעת אנימציית גלגול שנעצרת ומתחדשת בלחיצת עכבר.
נגדיר את DieMorph כתת-מחלקה של BorderedMorph (כדי שהקובייה תהנה ממסגרת מובנית), עם שלושה משתני מופע:
faces (מספר הפאות הכולל, 1 עד 9, ברירת מחדל 6),
dieValue (הערך הנוכחי הפונה מעלה),
ו-isStopped (משתנה בוליאני המציין האם אנימציית הגלגול נעצרה או פועלת):
instanceVariableNames: 'faces dieValue isStopped'
classVariableNames: ''
poolDictionaries: ''
category: 'SBE-Morphic'
"מתודת מחלקה ליצירת קובייה עם מספר פאות מבוקש:"
DieMorph class >> faces: aNumber
^ self new faces: aNumber
"מתודת האתחול ומתודות הגישה והעדכון:"
DieMorph >> initialize
super initialize.
self extent: 50 @ 50.
self color: Color green.
dieValue := 1.
faces := 6.
isStopped := false
DieMorph >> faces: aNumber
(aNumber isInteger and: [ aNumber > 0 ] and: [ aNumber <= 9 ])
ifTrue: [ faces := aNumber ]
DieMorph >> dieValue: aNumber
(aNumber isInteger and: [ aNumber > 0 ] and: [ aNumber <= faces ])
ifTrue: [
dieValue := aNumber.
self changed ]
שים לב שבמתודה dieValue: aNumber, מיד לאחר עדכון המשתנה dieValue, אנו קוראים לפקודה הקריטית self changed כדי להודיע למערכת לצייר את הקובייה מחדש עם הערך המעודכן!
2. מיפוי הנקודות (face1 עד face9), הפעלה דינמית עם perform: וציור הנקודות
איך מציירים כל פאה בקובייה ללא משפטי if ובכל גודל קובייה שנבחר?
לכל פאה (face1 עד face9) מוגדרת מתודה המחזירה מערך דינמי ({ ... }) של נקודות יחסיות (בין 0@0 ל-1@1) המציינות את מיקומי העיגולים על פני הקובייה. לדוגמה:
^ { 0.5 @ 0.5 }
DieMorph >> face2
^ { 0.25 @ 0.25 . 0.75 @ 0.75 }
DieMorph >> face3
^ { 0.25 @ 0.25 . 0.75 @ 0.75 . 0.5 @ 0.5 }
ועכשיו תסתכל על היופי של המתודה drawOn: aCanvas ומתודת העזר drawDotOn: aCanvas at: aPoint:
super drawOn: aCanvas.
(self perform: ('face' , dieValue asString) asSymbol)
do: [ :aPoint | self drawDotOn: aCanvas at: aPoint ]
DieMorph >> drawDotOn: aCanvas at: aPoint
aCanvas
fillOval: (Rectangle
center: self position + (self extent * aPoint)
extent: self extent / 6)
color: Color black
super drawOn: aCanvas: מבקשת ממחלקת האב (BorderedMorph) לצייר תחילה את המלבן הירוק ואת המסגרת של הקובייה!- הפעלת מתודה דינמית באמצעות
perform:: במקום סדרת תנאיifארוכה, המתודה משרשרת את המחרוזת'face'עםdieValue asString(למשל'face3'), ממירה אותה לסימבול (#face3), ושולחת לעצמה את ההודעה באמצעותself perform: #face3! drawDotOn: aCanvas at: aPoint: מתרגמת כל נקודה יחסיתaPointלקואורדינטת מסך אמיתית לפיself position + (self extent * aPoint), קובעת את גודל העיגול כ-self extent / 6, ומציירת עיגול שחור באמצעותfillOval:color:!
3. אנימציית הגלגול ושליטה בעכבר (step ו-mouseDown:)
הגרלת פאה אקראית בכל 100 מילישניות ועצירה/הפעלה בלחיצה שמאלית
^ 100
DieMorph >> step
isStopped ifFalse: [ self dieValue: (1 to: faces) atRandom ]
DieMorph >> handlesMouseDown: anEvent
^ true
DieMorph >> mouseDown: anEvent
anEvent redButtonPressed
ifTrue: [ isStopped := isStopped not ]
איך פעימת step, שליחת perform: ומיפוי נקודות יחסיות בונים את הקובייה החיה
מיני-סימולציה 17.8: מפענח הקריאה הדינמית self perform: עבור פאות 1 עד 9
הקובייה החיה (Live DieMorph Viewer): לחץ על הקובייה לעצירה/הפעלה ושנה את faces:!
isStopped:aDie faces: aNumber בין 1 ל-9):הגשר הנרטיבי: ראינו את Canvas מצייר מלבנים ועיגולים ב-CrossMorph וב-DieMorph – אילו עוד יכולות מתקדמות מסתתרות במחלקת Canvas?
בשני מקרי הבוחן שלנו נעזרנו במתודות fillRectangle:color: ו-fillOval:color: של העצם aCanvas. בסעיף 17.9 נכיר לעומק את מלוא היכולות של מחלקת Canvas (קווים, מצולעים, גופנים, תמונות ביטמפ Form, טרנספורמציות Scaling ורוטציה, שקיפות והצללות), יחד עם טעינת קובצי תמונה חיצוניים ואנימציה מבוססת תהליך נפרד (fork)!
מחלקת Canvas, טעינת תמונות חיצוניות (Form) ואנימציה מבוססת תהליך (fork)
הכרת מנוע הציור הגרפי של Morphic (קווים, מלבנים, מצולעים, גופנים, תמונות ביטמפ Form, Scaling, רוטציה, הצללות ושקיפות), טעינת תמונות עם ImageReadWriter והקטנתן עם magnifyBy:, והרצת אנימציה כ-Thread נפרד באמצעות fork.
מודל מנטלי ועיקרון מנחה: Canvas הוא מנוע הציור האוניברסלי — מצורות וקטוריות ועד תמונות וטרנספורמציות!
המחלקה Canvas מספקת מתודות לציור צורות גיאומטריות (קווים, מלבנים, עיגולים, מצולעים), טקסט וגופנים, הצגת תמונות ביטמפ (Form) וביצוע טרנספורמציות של שינוי קנה-מידה (Scaling) וסיבוב (Rotation).
ניתן לטעון קובץ תמונה חיצוני אל עצם תמונה (Form) באמצעות ImageReadWriter formFromFileNamed: 'image.png' ולשנות את קנה המידה שלו בקלות באמצעות form magnifyBy: 0.5!
מלבד מנגנון ה-step המובנה, ניתן להריץ אנימציות ברקע באמצעות בלוק המופעל כ-Thread עצמאי בעזרת ההודעה fork והשהיה מחזורית עם (Delay forMilliseconds: 100) wait!
1. סעיף 17.9: מחלקת Canvas ויכולותיה הגרפיות המתקדמות
מנוע הציור הגרפי הבסיסי ותת-המחלקות המתקדמות שלו בסביבת Morphic
בכל פעם שמימשנו את המתודה drawOn: aCanvas, קיבלנו לידינו מופע של המחלקה Canvas. מהו בדיוק תפקידה של מחלקה זו?
המחלקה Canvas מספקת את מנוע הציור הגרפי הבסיסי עבור כל סביבת Morphic, והיא מעמידה לרשותנו ארגז כלים עשיר במיוחד:
📐 1. צורות גיאומטריות, טקסט ותמונות ביטמפ
• צורות גיאומטריות: ציור קווים (line:to:width:color:), מלבנים (fillRectangle:color:), אליפסות/עיגולים (fillOval:color:) ומצולעים.
• טקסט וגופנים: ציור מחרוזות וטקסט מעוצב בעזרת גופנים (Fonts) שונים.
• תמונות ביטמפ: הצגת תמונות מבוססות פיקסלים המיוצגות על ידי המחלקה Form.
✨ 2. טרנספורמציות ותת-מחלקות Canvas מתקדמות
• טרנספורמציות גיאומטריות: תמיכה בשינוי קנה-מידה (Scaling) וברוטציה/סיבוב (Rotation).
• סוגי Canvas מתקדמים: קיימות תת-מחלקות ייעודיות של Canvas התומכות בהצללות (Shadows), שקיפות (Alpha blending), צבעים מדורגים (Gradients) וציור מואץ.
2. טעינת תמונות חיצוניות ואנימציה מבוססת תהליך נפרד (Thread / fork)
שימוש ב-ImageReadWriter ו-magnifyBy:, והרצת לולאת אנימציה ברקע עם Delay ו-fork
לצד כלי הציור של Canvas, בוא נכיר שתי טכניקות שימושיות מאוד לעבודה מעשית בסביבת Squeak:
טעינת קובץ תמונה ל-Form והקטנתו ב-50%
באמצעות המחלקה ImageReadWriter אנו טוענים קובץ תמונה חיצוני (כגון 'image.png') אל עצם מסוג Form, ובעזרת ההודעה magnifyBy: 0.5 אנו מקטינים את ממדיו ב-50%:
form := form magnifyBy: 0.5.
אנימציה ברקע באמצעות בלוק ו-fork
מלבד מנגנון ה-step של Morphic, ניתן להריץ אנימציות ברקע באמצעות בלוק המופעל כתהליך נפרד (Thread) ע"י שליחת ההודעה fork לבלוק ושימוש ב-Delay להשהיה בין צעדים:
(Delay forMilliseconds: 100) wait.
self doSomething ] ] fork
שירותי הציור של Canvas והשוואת מנגנון ה-Stepping מול תהליך רקע (fork)
מיני-סימולציה 17.9: סייר יכולות Canvas, תמונות Form ותהליכי fork
סטודיו ה-Canvas החי: Scaling, רוטציה, שקיפות, הצללות, magnifyBy: ואנימציית fork
Canvas, Form ו-fork:הגשר הנרטיבי: השלמנו את כל פרקי התוכן של פרק 17 – בוא נעבור לסיכום האינטגרטיבי ולכרטיסיות הזיכרון התלת-ממדיות!
בלשונית האחרונה והמסכמת של פרק 17 (סעיף 17.10) נאגד את כל עקרונות הסביבה הגרפית Morphic לתמונה הנדסית שלמה, נתרגל במילון המושגים האינטראקטיבי בכרטיסיות תלת-ממד (3D Flashcards), ונפתח צוהר אל פרק 18 (המושג "מטה-מחלקה")!
סיכום אינטגרטיבי של פרק 17 ומילון מושגים בכרטיסיות תלת-ממד (3D Flashcards)
ריכוז כל היכולות הגרפיות של Morphic שלמדנו בפרק: מ-asMorph ו-openInWorld, דרך הרכבת צורות, דריסת drawOn: ו-containsPoint:, אירועי עכבר ומקלדת, אנימציה, FillInTheBlank ו-PopUpMenu, גרירה והשמטה ועד ל-DieMorph.
מודל מנטלי ועיקרון מנחה: כל רכיב גרפי ב-Morphic הוא עצם שלם המשלב מראה, מבנה היררכי, תגובה לאירועים וחיוּת!
ישירות וחיות, הילת ה-Halo וניתוק פריטים עם ה-pickup, המרת עצמים עם asMorph והצגה ב-openInWorld, מערכת הצירים (0@0 למעלה-משמאל, y כלפי מטה) והרכבת עץ תת-רכיבים עם addMorph: ו-removeMorph:.
יצירת צורות בדריסת drawOn: aCanvas ו-containsPoint:, אירועי עכבר ב-2 שלבים (+ self changed), מקלדת ב-3 שלבים (newKeyboardFocus:), תיבות דו-שיח ו-PopUp, ומנגנון Drag & Drop דו-צדדי.
אנימציה באמצעות מנגנון ה-Stepping (stepTime ו-step) או תהליך נפרד (fork), טעינת תמונות חיצוניות (ImageReadWriter ו-magnifyBy:), ושימוש ב-perform: ב-DieMorph!
1. ששת עמודי התווך המסכמים של פרק 17
ריכוז מסודר של כל העקרונות והיכולות שלמדנו בסביבה הגרפית Morphic
שימוש בהודעה asMorph להמרת מחרוזות וצבעים לרכיבי Morph, הצגתם בעולם באמצעות openInWorld, וחקירתם החיה דרך openViewerForArgument וה-Morphic Halo.
שליטה במיקום (position:), ממדים (extent:), מרכוז (center:) וצבע (color:) במערכת שבה (0,0) בפינה השמאלית-העליונה, והרכבת עץ אב-בן באמצעות addMorph: ו-removeMorph:.
דריסת מתודת הציור drawOn: aCanvas (תוך שימוש ב-bounds insetBy:) ודריסת מתודת זיהוי הפגיעה containsPoint: aPoint לסינון הפינות הריקות של המלבן החוסם.
הכרזה ב-handlesMouse... ותגובה ב-mouseDown: / mouseEnter: / mouseLeave: עם קריאה ל-self changed; העברת פוקוס מקלדת עם anEvent hand newKeyboardFocus: וטיפול ב-handleKeystroke:.
שימוש ב-FillInTheBlank ו-PopUpMenu, בקרת Drag & Drop ב-wantsDroppedMorph:event: ו-repelsMorph:event:, ופיתוח הקובייה DieMorph המשתמשת ב-perform:.
ציור וטרנספורמציות ב-Canvas, טעינת תמונות חיצוניות עם ImageReadWriter formFromFileNamed: והקטנתן עם magnifyBy: 0.5, ואנימציה באמצעות step או תהליך נפרד [ ... ] fork.
כל המנגנונים של פרק 17 מחוברים סביב המחלקה Morph
מיני-סימולציה 17.10: בוחן התאמה מהיר לקראת המבחן – באיזו מתודה נשתמש?
כרטיסיות זיכרון תלת-ממדיות (3D Flashcards) לכל מושגי פרק 17
לחץ על כל כרטיסייה כדי להפוך אותה ולבחון את עצמך על כל אחד מ-12 מושגי המפתח של פרק 17:
נחשפו: 0 / 12סיימנו במלואו את פרק 17 (הסביבה הגרפית Morphic)! מה מחכה לנו בחלק VII?
ראינו בפרק 17 איך העיקרון "הכל הוא עצם וכל חישוב מתרחש על ידי שליחת הודעה" קם לתחייה מול עינינו על המסך הגרפי של Squeak.
Metaclass ל-Metaclass class!