Browsed by
חודש: נובמבר 2016

Get Alive

Get Alive

lenna

זו Lenna. כל מאמר שעוסק ב- image proccessing משתמש בתמונה הזו להדגמה (למה? תקראו בלינק).

אני רוצה להראות עליה איך יוצרים מתמונה רגילה, animated gif:

fadeinlenna

תאוריה

בתור התחלה, צריך ליצור תמונת בסיס ממנה ניצור את הפריימים שיהיו ב- gif הסופי:

  • נמיר את התמונה המקורית ל- grayscale (שחור-לבן).
  • נפעיל על התמונה פילטר של ״זיהוי קצוות״ (edge detection). ישנם מספר פילטרים שעושים את זה, כמו sobel או canny. הרעיון הוא, שהם מדגישים את השינויים הבולטים בין הפיקסלים בתמונה. בצורה כזו נקבל מעין "sketch" של התמונה.
  • התוצר של edge detection יהיה לבן על רקע שחור, לכן נעשה inversion (היפוך צבעים) – ונקבל את תמונת הבסיס.

תכלס

בשביל המימוש נשתמש בספריית Pillow, שהיא fork מתוך PIL הותיקה – ספריית python לעיבוד תמונה.

נפתח טרמינל python, ונתחיל עם הפקודות הבאות:

>>> from PIL import Image
>>> im = Image.open("Lenna.png")

נשנה את התמונה ל- grayscale, כלומר שרק חלק ה- Luminance יישאר:

>>> im = im.convert("L")
>>> im.show()

וזו התוצאה:

screen-shot-2016-11-30-at-15-03-24

נפעיל את ה- edge detection המובנה של PIL:

>>> from PIL import ImageFilter
>>> im = im.filter(ImageFilter.FIND_EDGES)
>>> im.show()

ונקבל:

screen-shot-2016-11-30-at-15-06-36

מה שנשאר זה רק לעשות invert, כלומר להפוך את הצבעים:

>>> from PIL import ImageOps
>>> im = ImageOps.invert(im)
>>> im.show()

זו התמונה הסופית איתה נעבוד:

screen-shot-2016-11-30-at-15-07-24

סקריפט

נרכז את כל הקוד הנ״ל לסקריפט הזה:

import sys, os, shutil
import numpy as np
from PIL import Image, ImageFilter, ImageOps
 
if len(sys.argv) > 1:
    imageFileName = sys.argv[1]
else:
    print "arg not found."
    sys.exit()
# load image
original = Image.open(imageFileName)
# convert to grayscale -> find edges -> invert
edges = ImageOps.invert(original.convert("L").filter(ImageFilter.FIND_EDGES))

התמונה שיש לנו היא שחור לבן, כלומר ערכים בין 0 ל- 255. נכתוב פונקציה שמקבלת ערך סף (threshold) ויוצרת תמונה חדשה שבה כל מה שמעל ה- threshold יהיה צבע רקע. ככל שערך הסף נמוך יותר, נראה פחות מהציור ויותר מהרקע.

def changeColors(image, bgColor, threshold):
    # Because we want to support in colorful gifs - to allow rgb bgColor -
    # we covert the image back to rgb
    image = image.convert("RGB")
    # We use numpy to work with the image data array directly
    data = np.array(image)
    # Get the three channels of the image to compare to
    red, green, blue = data[:,:,0], data[:,:,1], data[:,:,2]
    # Create mask to check if all channels (that probably all the same)
    # are bigger than threshold
    bgMask = (red > threshold) & (green > threshold) & (blue > threshold)
    # Replace all pixels that fit for the criteria with the bg color
    data[:,:,:3][bgMask] = bgColor
    # create back image from data array
    return Image.fromarray(data)

הרעיון הוא ליצור סט של פריימים בכל פעם עם threshold יותר גבוה, כך שבכל פריים נראה יותר מהתמונה. נשמור את כל הפריימים בתיקיה זמנית, ואחרי שניצור את ה- gif נמחק אותה.

folderName = "gifTemp"
filePath = "{0}/{1}.png"
for threshold in range(0, 255, 25):
    changeColors(image.copy(), bgColor, threshold).save(filePath.format(folderName, index))
    index += 1

אחרי שהרצנו את הסקריפט יש לנו תיקיה עם פריימים (בפורמט png). עכשיו צריך ליצור מהם gif.

בעיקרון, יש דרך ליצור מתוך python קבצי animated gif. לדוגמא images2gif. אבל בכל האפשרויות שניסיתי, לא הייתי מרוצה – מבחינת איכות ונפח הקובץ של התוצאה.

לכן בחרתי להשתמש בכלים חיצוניים ffmpeg ו- gifsicle. הם open source, וקיימים בדר״כ בסביבות פיתוח (לפחות ffmpeg).

נוסיף לסקריפט שלנו שתי פקודות:

os.system("ffmpeg -i " + folderName + "/%01d.png gifResults/fadeIn.gif")

הפקודה הזו מריצה את ffmpeg שלוקח את כל קבצי png שקיימים בתיקייה ומתחילים במספר – לפי הסדר – ויוצר מהם gif בשם fadeIn.

עכשיו הבעיה היא התזמון. אנחנו רוצים שכל הפריימים יהיו אותו זמן, חוץ מהאחרון – שם אנחנו רוצים להתעכב קצת, שנוכל לשים לב לתמונה הסופית. בשביל זה נריץ את gifsicle:

os.system("gifsicle -b gifResults/fadeIn.gif -d10 '#0--2' -d75 '#-1'")

פריים "-1" – כלומר הפריים האחרון מהסוף יהיה 0.75 שנייה (שנייה שלמה זה 100), וכל שאר הפריימים יהיו עשירית שנייה.

אפשר למצוא את הסקריפט המלא כאן.

 

libGDX Fonts

libGDX Fonts

איך מכניסים טקסט לתוך משחק שנכתב ב- libGDX?

אז ככה. מכיוון שכל דבר אותו אנו יוצרים בספרייה הזו יתורגם בסופו של דבר ל- OpenGL, גם הטקסט הוא כזה. כלומר ההתייחסות בסופו של דבר היא בתור bitmap – לצייר את צורת האותיות על המסך.

נשתמש בתוכנה בשם Hiero (אפשר להוריד מכאן):

screen-shot-2016-11-14-at-13-30-03

נבחר את הפונט הרצוי, Rendering מסוג Java, ואז בתפריט File נבחר 'Save BMFont files'.

התוצר יהיה שני קבצים: png, ו- fnt.

קובץ התמונה יראה משהו כמו זה:

text

הקובץ השני מכיל את המיקומים של האותיות ברחבי התמונה.

דגשים חשובים:

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

אז אחרי שיצרנו את שני הקבצים הנ״ל, נוסיף אותם לתיקיית ה- assets לתוך תיקייה חדשה בשם fonts.

כדי להשתמש בפונט נעזר במחלקה BitmapFont בצורה הזו:

BitmapFont font = new BitmapFont(Gdx.files.internal("fonts/text.fnt"));
...
 
// on rendering
batcher.begin();
font.draw(batcher, "Hello World", x, y);
batcher.end();
 
// on destroy
font.dispose();

זה טוב ונחמד. אבל מה אם (משום מה) יש לנו אפליקציה בה אנחנו נדרשים לעשרות פונטים, או אפילו צריכים ליצור פונט בזמן אמת, מקובץ ttf?

בשביל זה יש תוסף בשם FreeType. ניצור פרוייקט חדש עם freetype מסומן:

freetype_libgdx

כשנרצה לייצר BitmapFont במהלך המשחק, נעשה כך:

public class FontsExample extends ApplicationAdapter {
	SpriteBatch batch;
	BitmapFont font;
	@Override
	public void create () {
		batch = new SpriteBatch();
		FreeTypeFontGenerator generator = new FreeTypeFontGenerator(Gdx.files.internal("fonts/Xenotron.ttf"));
		FreeTypeFontGenerator.FreeTypeFontParameter parameter = new FreeTypeFontGenerator.FreeTypeFontParameter();
		parameter.size = 20;
		font = generator.generateFont(parameter);
		font.setColor(Color.WHITE);
		generator.dispose(); // don't forget to dispose to avoid memory leaks!
	}
 
	@Override
	public void render () {
		Gdx.gl.glClearColor(1, 0, 0, 1);
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
		batch.begin();
 
		font.draw(batch, "Hello", 100, 100);
		batch.end();
	}
 
	@Override
	public void dispose () {
		batch.dispose();
		font.dispose();
	}
}

 

מה הצבע של צ׳אק נוריס

מה הצבע של צ׳אק נוריס

(הרקע לפוסט הוא השאלה הזו ב- StackOverflow)

כדי להגדיר צבע רקע לאלמנט html לא באמצעות css (דבר שכבר לא אמורים לעשות) – משתמשים ב- attribute בשם "bgcolor".

לדוגמא, הקוד הזה:

<table>
	<tr>
		<td bgcolor="#ff0000">Red TD</td>
	</tr>
</table>

יראה כך:

Red TD

הגדרנו את הצבע האדום בהקסדצימל "ff0000" – שלושה בייטים שמציינים ערכי RGB, כל ערך בטווח 0 – 255, כלומר בין ׳00׳ ל- ׳ff׳.

אבל מה יקרה כאשר במקום ערך הקסדצימלי או שם הצבע…נשים טקסט?

הנה כמה דוגמאות לערכי bgColor:

<table>
	<tr>
		<td bgcolor="chucknorris">chuck norris</td>
		<td bgcolor="mrt">Mr T</td>
		<td bgcolor="ninjaturtle">ninjaturtle</td>
	</tr>
	<tr>
		<td bgcolor="cheese">cheese</td>
		<td bgcolor="crap">crap</td>
		<td bgcolor="fish">fish</td>
	</tr>
	<tr>
		<td bgcolor="cloudy">cloudy</td>
		<td bgcolor="superman">superman</td>
		<td bgcolor="grass">grass</td>
	</tr>
</table>

וככה זה נראה:

chuck norris Mr T ninjaturtle
cheese crap fish
cloudy superman grass

(אם אתם רוצים לבדוק לאיזה צבע אתם הופכים, נסו את ה- JsBin הזה)

אז למה זה קורה?

הדפדפן מקבל מילה בתור bgColor, לדוגמא "ChuckNorris". הוא בודק האם זה אחד מהצבעים ששמורים אצלו (כמו "red"  או "blue"). זה לא. הלאה.

עכשיו הוא מניח שיש כאן ערך הקסדצימלי. כלומר רק 0-9 או A-F מעניינים אותו. כל תו אחר יוחלף מיידית ב- '0'. מה שאומר שאם נכתוב מילה שכולה אותיות אחרי F, הצבע תמיד יהיה שחור (כמו #000).

ניקח את "chucknorris". זה יוחלף להקסדצימלי – "c00c0000000". מכיוון שזה רק 11 תווים, יתווסף padding של 0 אחד מימין, כי אנחנו צריכים מספר תווים שמתלק בשלוש – c00c00000000. זה מתפצל לשלושת ערכי RGB:

c00c 0000 0000

 צריך רק שני תווים עבור כל צבע, לכן רק שני התווים השמאליים ישארו. כלומר:

c0 00 00

לסיכום, הצבע "chucknorris" הוא – 192 צבע אדום, ו- 0 ירוק וכחול.

libGDX physics with Box2D

libGDX physics with Box2D

אחד הנושאים המורכבים ביותר בפיתוח משחקים הוא השימוש במנוע הפיזיקלי. בפוסט הקרוב אנסה להסביר על איך להשתמש בעולם פיזיקלי של Box2D, בסביבת libGDX.

תאוריה

ניצור עולם – מרחב בו אנו מגדירים מהם הכוחות הפועלים. מה יהיה כוח המשיכה בציר ה- Y (אפשר 9.81, אבל לא חייבים), מה כוח המשיכה בציר ה- X (אם רוצים).
לתוך העולם הזה ניצור "גופים". לכל גוף יש הגדרות כמו לגוף בעולם האמיתי: צורה, מסה, מהירות וכו'.

ישנם שלושה סוגי גופים:

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

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

ועכשיו למעשה.

קוד

ניצור פרוייקט חדש בשם Box2dExample, ונבחר ב- extensions את הספרייה Box2D.

נלך למחלקה ב- Core שנקראת Box2dExample, ונוסיף את המשתנים הבאים:

private World world;
	private Box2DDebugRenderer b2dr;
	private OrthographicCamera b2dCam;
	private int SCREEN_WIDTH = 480;
	private int SCREEN_HEIGHT = 800;

המחלקה שאיתה יוצרים עולם ב- Box2d נקראת (באופן מפתיע) World. כדי לדבג אותה נשתמש באובייקט מסוג Box2DDebugRenderer, שיודע לרנדר את כל הגופים שקיימים כרגע בעולם.

נוסיף לפונקציית create את הקוד הבא:

public void create () {
		world = new World(new Vector2(0, 9.81f), false);
		b2dCam = new OrthographicCamera();
		b2dCam.setToOrtho(true, SCREEN_WIDTH,
				SCREEN_HEIGHT);
		b2dr = new Box2DDebugRenderer();
 
		createBall(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
		createGround(new Rectangle(SCREEN_WIDTH / 2, SCREEN_HEIGHT * 0.75f, 100, 20));
	}

כדי ליצור עולם דרושים בסך הכל שני פרמטרים: וקטור של כוח המשיכה הפועל בעולם (במקרה שלנו זה כמו בעולם האמיתי – 0 בציר ה- x ו- 9.81 מטר לשניה בציר ה- y). ומשתנה בוליאני שקובע האם לפעול גם על גופים שאינם פעילים (לשיפור ביצועים עדיף false).

זה היה קל. עכשיו נעבור לגופים.

נתחיל בגוף סטטי:

private void createGround(Rectangle rect){
		BodyDef bdef = new BodyDef();
 
		bdef.position.set(rect.getX(), rect.getY());
		bdef.type = BodyDef.BodyType.StaticBody;
		Body body = world.createBody(bdef);
 
		PolygonShape shape = new PolygonShape();
		shape.setAsBox(rect.getWidth(), rect.getHeight());
		FixtureDef fdef = new FixtureDef();
		fdef.friction = 0.5f;
		fdef.shape = shape;
 
		body.createFixture(fdef).setUserData("ground");
 
		body.setUserData(this);
 
 
		shape.dispose();
	}

כדי ליצור Body, צריך קודם כל ליצור BodyDef שם מגדירים את סוג הגוף ומיקומו.

אז מגדירים את ה- Shape בו רוצים להשתמש, והגודל שלו (חשוב לזכור לעשות dispose אחרי השימוש בו!).

ואז מגדירים FixtureDef עם כל התכונות שנרצה לגוף. וזהו.

באותה הדרך נגדיר גם גוף דינמי:

private void createBall(int x, int y){
		BodyDef bdef = new BodyDef();
 
		bdef.position.set(x, y);
		bdef.type = BodyDef.BodyType.DynamicBody;
		Body ball = world.createBody(bdef);
 
		CircleShape shape = new CircleShape();
		shape.setRadius(20f);
		FixtureDef fdef = new FixtureDef();
		fdef.restitution = 0.7f;
		fdef.density = 2f;
		fdef.friction = 0.25f;
		fdef.shape = shape;
 
		ball.createFixture(fdef).setUserData("ball");
		ball.setUserData(this);
		shape.dispose();
	}

התכונה "restitution" (פיצוי) קובעת בזמן התנגשות כמה מתוך הכוח יוחזר. כלומר אם הערך יהיה ״1״ הכדור יפול ויעלה לנצח.

נוסיף לפונקציית render את הקוד הבא:

public void render () {
		Gdx.gl.glClearColor(0, 0, 0, 1);
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
 
		world.step(Gdx.graphics.getDeltaTime(), 6, 2);
 
		b2dr.render(world, b2dCam.combined);
	}

כדי שה״עולם״ יתעדכן צריך בכל פעם לקרוא ל- step.

אם נריץ עכשיו, זה מה שנקבל:

box2d

האובייקט Box2DDebugRenderer מצייר בצבע שונה כל סוג של Body. ירוק זה גופים סטטיים, וכו׳.

נראה טוב, לא? אבל… זה זז קצת לאט. אפילו מאד לאט.

צריך לקחת בחשבון את שיטת המדידה שבה משתמשים ב- Box2d. כלומר כשקבענו שהכוחות הפועלים הם 9.81 מטר לשניה – לא הגדרנו מה נקרא מטר. ה- default זה שכל פיקסל הוא מטר אחד… כלומר מה שאנחנו רואים כאן זה בעצם כדור בקוטר 40 מטר נופל על משטח בגובה 20 מטר. זה כבר יותר מובן.

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

נוסיף משתנה בשם PPM – כלומר Pixels Per Meter:

private float PPM = 100;

ובכל מקום בו יש התייחסות למיקום/גודל על גבי המסך, נחלק ב- PPM, וגם את הגדרת גודל המצלמה בה אנחנו משתמשים b2dCam נחלק ב- PPM.

אחרי השינוי זה יראה ככה:

box2d_fixed

זה כבר יותר הגיוני.

מה עושים עם זה

במשחק אמיתי כמובן לא נשתמש ב- Box2DDebugRenderer בשביל להציג. נשמור את המיקום של כל Body בו אנחנו משתמשים, ונצייר שם צורה/תמונה באותו מיקום (לא לשכוח להכפיל בחזרה ב- PPM!). בשלבי הפיתוח נשתמש בשתי מצלמות – אחת בשביל ה- Box2DDebugRenderer והשנייה לצורות האמיתיות – ואחרי שנראה שזה חופף, ״נכבה״ את הרינדור של הדיבאגר.

הנה דוגמא פשוטה לאפליקציה שמציגה המון כדורים שנופלים:

package com.yair.physics;
 
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.BodyDef;
import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
import com.badlogic.gdx.physics.box2d.CircleShape;
import com.badlogic.gdx.physics.box2d.FixtureDef;
import com.badlogic.gdx.physics.box2d.PolygonShape;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.TimeUtils;
 
public class Box2dExample extends Game {
	private World world;
	// only for debug
//	private Box2DDebugRenderer b2dr;
//	private OrthographicCamera b2dCam
	private OrthographicCamera cam;
	private int SCREEN_WIDTH = 480;
	private int SCREEN_HEIGHT = 800;
	private float PPM = 100;
	private long lastSpawnTime;
 
	private Array<ColorBody> bodies = new Array<ColorBody>();
	private ShapeRenderer shapeRenderer;
 
	@Override
	public void create () {
		world = new World(new Vector2(0, 9.81f), false);
		// only for debug
//		b2dCam = new OrthographicCamera();
//		b2dCam.setToOrtho(true, SCREEN_WIDTH / PPM,
//				SCREEN_HEIGHT / PPM);
 
		cam = new OrthographicCamera();
		cam.setToOrtho(true, SCREEN_WIDTH,
				SCREEN_HEIGHT);
		// only for debug
//		b2dr = new Box2DDebugRenderer();
		shapeRenderer = new ShapeRenderer();
 
		createGround(new Rectangle(SCREEN_WIDTH / 2, SCREEN_HEIGHT, SCREEN_WIDTH, 10));
	}
 
	private void createBall(int x, int y){
		BodyDef bdef = new BodyDef();
		bdef.position.set(x / PPM, y / PPM);
		bdef.type = BodyDef.BodyType.DynamicBody;
		Body ball = world.createBody(bdef);
		CircleShape shape = new CircleShape();
		shape.setRadius(20f / PPM);
		FixtureDef fdef = new FixtureDef();
		fdef.restitution = 0.7f;
		fdef.density = 2f;
		fdef.friction = 0.25f;
		fdef.shape = shape;
		ball.createFixture(fdef).setUserData("ball");
		ball.setUserData(this);
		shape.dispose();
 
		bodies.add(new ColorBody(ball));
	}
 
	private void createGround(Rectangle rect){
		BodyDef bdef = new BodyDef();
		bdef.position.set(rect.getX() / PPM, rect.getY() / PPM);
		bdef.type = BodyDef.BodyType.StaticBody;
		Body ground = world.createBody(bdef);
		PolygonShape shape = new PolygonShape();
		shape.setAsBox(rect.getWidth() / PPM, rect.getHeight() / PPM);
		FixtureDef fdef = new FixtureDef();
		fdef.restitution = 0f;
		fdef.friction = 0.5f;
		fdef.shape = shape;
		ground.createFixture(fdef).setUserData("ground");
		ground.setUserData(this);
		shape.dispose();
	}
	@Override
	public void render () {
		Gdx.gl.glClearColor(0, 0, 0, 1);
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
		if ((TimeUtils.nanoTime() - lastSpawnTime)/10000 > 5000) {
			// spawn ball
			createBall( MathUtils.random(50, SCREEN_WIDTH - 50), 0);
			lastSpawnTime = TimeUtils.nanoTime();
		}
		world.step(Gdx.graphics.getDeltaTime(), 6, 2);
		// only for debug
//		b2dr.render(world, b2dCam.combined);
 
		// render ground
		shapeRenderer.setColor(Color.WHITE);
		shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
		shapeRenderer.rect(0, 0, SCREEN_WIDTH, 10);
		shapeRenderer.end();
 
		// iterate through balls array to render them
		for (ColorBody b : bodies) {
			shapeRenderer.setColor(b.color);
			shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
			shapeRenderer.circle(b.body.getPosition().x * PPM, 
SCREEN_HEIGHT - b.body.getPosition().y * PPM, 20);
			shapeRenderer.end();
		}
	}
 
	@Override
	public void dispose () {
		shapeRenderer.dispose();
	}
 
	// we want to save each ball with its specific color, so we use this struct
	private class ColorBody {
		public Body body;
		public Color color;
		public ColorBody(Body body) {
			this.body = body;
			this.color = getRandColor();
		}
		// generate random color from this list
		private Color getRandColor(){
			int rand = MathUtils.random(10);
			Color toRet;
			switch (rand) {
				case 0:
					toRet = Color.LIME;
					break;
				case 1:
					toRet = Color.RED;
					break;
				case 2:
					toRet = Color.YELLOW;
					break;
				case 3:
					toRet = Color.GREEN;
					break;
				case 4:
					toRet = Color.BLUE;
					break;
				case 5:
					toRet = Color.ORANGE;
					break;
				case 6:
					toRet = Color.DARK_GRAY;
					break;
				case 7:
					toRet = Color.MAGENTA;
					break;
				case 8:
					toRet = Color.MAROON;
					break;
				case 9:
					toRet = Color.GRAY;
					break;
				default:
					toRet = Color.WHITE;
					break;
			}
			return  toRet;
		}
	}
}

וזה נראה כך (זאת תמונה ב- Debug Mode, אפשר לראות את קווי המתאר של ה- Bodies, ואיך הם חופפים במדוייק את הציור):

balls