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

על גלי HTML5

05 בינואר 2012 מאת

ציור על קנבס

ב – HTML5 ישנן הרבה יכולות חדשות. אחת המרתקות שבהן היא תג ה <canvas>, שמאפשר להציג גרפיקה וציורים ע"י שימוש ב- Javascript. זהו לא יותר ממלבן ריק על המסך, אך האפשרויות הן עצומות. למעשה אפשר לעשות בתוך המלבן הזה הכל – ציור, תנועה, סאונד, אינטרקציה, ורק השמיים הם הגבול.

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

ציירי לי כבשה (בשמיים)

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

במהלך הלימוד נוצרה דוגמת ציור חמוד (בדומה למה שהבן שלי מצייר) שנראה ככה:

ציור על קנבס

אם מעניין אתכם לראות מה התרחש לי במוח בזמן שכתבתי את זה, אז התמונה, היא חלק קטן מזה:

קשקושים

הדוגמא המלאה כאן:
http://www.netcraft.co.il/playground/canvaspicture/index.html

ולהלן ההסברים שלב שלב, איך הגעתי לתוצאה.

הקנבס

ל – canvas יש רק שתי תכונות (attributes) הכרחיות – רוחב וגובה. אם לא מגדירים אותן אז הרוחב הדיפולטיבי יהיה 300 פיקסלים והגובה 150 פיקסל. את התכונות מגדירים בתג ולא דרך CSS, אחרת היחס של אלמנטים בתוך ה- canvas יהיה מעוות. כל הפעולות מתבצעות בקונטקסט דו מימדי (2D Rendering Context), שנמצא בכל תג <canvas>, ובעצם קונטקסט דו מימדי זו מערכת קואורדינטות מלבנית.

קונטקסט דו מימדי

השלב הראשון הוא להגדיר בקובץ ה – HTML את ה – canvas שלנו (השטח שבו הולכים לצייר), עם הרוחב והגובה שלו:

		                <canvas id="canvas" width="1100" height="650"></canvas>

ועכשיו ל Javascript.

תשתית לציור

קודם כל נדאג שהקוד שלנו ירוץ לאחר שה canvas מאותחל ב DOM. זה יכול לקרות אחרי שכל העמוד נטען (onload), אך אפשרות יותר טובה היא פשוט לשים את הסקריפט בסוף העמוד (אחרי ה <canvas>), כך הוא ירוץ יותר מהר, עוד לפני שהתמונות יטענו.

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

		            // fuction of converting grades to radians
		            Number.prototype.degree = function () {
			                return this * Math.PI / 180;
		            };

בדיקה האם הדפדפן תומך ב – canvas מתבצעת ע"י בדיקת המצאוּת getContext ואם כן – מקבלים גישה לקונטקסט ציור שהוא 2D (דו-ממדי):

		                var canvas = document.getElementById('canvas');
		                if (canvas.getContext){
			                    // use getContext to use the canvas for drawing
			                    var ctx = canvas.getContext('2d');
		                }

מגדירים משתנים בשביל הרוחב והגובה של ה – canvas, שנשתמש בהם בעתיד:

		                // get the canvas sizes
		                var	canvasWidth = canvas.getAttribute("width"),
			                      canvasHeight = canvas.getAttribute("height");

מציירים שמש

sun1

מעגל השמש

קודם נצייר מעגל של שמש בשלושה שלבים:

1. מגדירים גרדיאנט מעוגל של המעגל ע"י שיטת
createRadialGradient(x1,y1,r1,x2,y2,r2)
שבה :
x1,y1 – קואורדינאטות מרכז מעגל פנימי (קטן)
r1 – רדיוס מעגל פנימי (קטן)
x2,y2 – קואורדינאטות מרכז מעגל חיצוני( גדול)
r2 – רדיוס מעגל חיצוני (גדול)

		                // create gradient of sun circle
		                var radgrad = ctx.createRadialGradient(120,140,10,120,130,80);

2. מגדירים נקודות מפתח של גרדיאנט וצבעים ע"י שורות:

	                    radgrad.addColorStop(0, '#F4F201');
	                    radgrad.addColorStop(0.9, '#E4C700');
	                    radgrad.addColorStop(1, 'rgba(228,199,0,0)');

זה נותן 3 נקודות:

  • ראשונה, הנמצאת בנקודת התחלה של מרוח בין המעגלים (על מעגל פנימי) ומציינת צבע #F4F201
  • שנייה, הנמצאת בנקודת מרווח של 90% מכל המרווח בין המעגלים מנקודת ההתחלה שלו ומציינת צבע #E4C700
  • שלישית, הנמצאת בנקודה סופית של המרווח בין המעגלים (ז.א. על המעגל החיצוני) ומציינת צבע rgba(228,199,0,0)

פונקצית ה rgba דומה לפונקצית rgb, רק בנוסף קיים בה פרמטר שמגדיר שקיפות. במקרה שלנו אנו משתמשים בשקיפות מלאה בשביל להסתיר את הריבוע שבו יהיה מצויר המעגל (השלב הבא).

radgrad1

3. מגדירים שטח (ריבוע) שבו יהיה מצויר הגרדיאנט המעוגל וממלאים את הריבוע בגרדיאנט (כמו בצבע).

	                    // fill the defined rectangle by sun circle gradient
	                    ctx.fillStyle = radgrad;
	                    ctx.fillRect(40,50,160,160);

קרנות השמש

sun_no_circle

1. קרנות השמש נצייר בצורת משולשים המסתובבים 360 מעלות סביב נקודה אחת, והפינה הכי חדה של כל משולש פונה למרכז (נקודת מפגש). חוץ מזה אנחנו רוצים שמעגל השמש יסתיר את נקודת המפגש של הקרנות.
במקרה כזה ניתן לשנות את צורת החפיפה של האלמנטים. צורת החפיפה הדיפולטיבית היא כזאת שכל אלמנט חדש מסתיר
את הישן ואנחנו צריכים הפוך: שהקרנות "יכנסו מתחת" למעגל השמש. אז נשנה את צורת החפיפה
globalCompositeOperation מהדיפולטיבית להפוכה (ישן יסתיר חדש): destination-over.
ואחרי זה נגדיר צבע מילוי של משולשי קרנות.

		            // set composite property for insert rays behind sun circle
		            ctx.globalCompositeOperation = "destination-over";

              // set color of rays
	            ctx.fillStyle = "#edda58";

2. עכשיו נצייר 12 קרנות:

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

                        ctx.beginPath();
                        ctx.moveTo(120, 130);
                        ctx.lineTo(100, -50);
                        ctx.lineTo(130, -50);
                        ctx.lineTo(120, 130);
                        ctx.fill();

ע"י מתודה translate אנחנו מזיזים את כל הפיקסלים של הקנבס לערכים x ו-y המוגדרים בה. וע"י מתודה rotate אנחנו מסובבים את המשולש ל30 מעלות בכיוון השעון.

עושים סיבוב מלא בעזרת לולאת for:

		                // draw 12 triangles of rays: each triangle rotated to 30 grad.
		                for (var i = 0; i < 12; i++) {
			                    ctx.translate(85, -40);
			                    ctx.rotate((30).degree());
			                    ctx.beginPath();
			                    ctx.moveTo(120, 130);
			                    ctx.lineTo(100, -50);
			                    ctx.lineTo(130, -50);
			                    ctx.lineTo(120, 130);
			                    ctx.fill();
		                }

(בגלל שאנחנו עושים סיבוב מלא (30×12=360) ה-canvas חוזר לאותה צורה כמו לפני שהשתמשנו במתודה translate). אם לא היינו עושים סיבוב מלא, צריך היה לבצע שמירה של מצב ה canvas ע"י שורת ()ctx.save לפני שימוש ב-translate והחזר מצב שמור ע"י שורת ()ctx.restore אחרי סיום שימוש ב- translate.

ענני Bezier

cloudlet

  • לפני שנצייר ענן, נגדיר לו צבע מילוי:
    ctx.fillStyle = "#bed2fb";
  • הענן בנוי מ-5 עקומות המצוירות ע"י מתודה bezierCurveTo.נצייר את העקומות בעזרת bezierCurveTo בשביל שהציור יהיה דומה יותר לציור ידני (שהעקומות לא יהיו בצורה נכונה). בשביל לצייר עקומת בזיה צריך לציין קואורדינאטות של 3 נקודות: (נקודת התחלה של העקומה – היא הנקודה האחרונה שעצרנו בה או הנקודה שהגדרנו, במקרה שלנו, במתודה ctx.moveTo(410,80);). נקודת ביקורת ראשונה, נקודת ביקורת שניה ונקודת סיום של העקומה. בתמונה – נקודות אדומות הן נקודות ביקורת ונקודות כחולות – נקודות התחלה והסוף.bezier_curve1חשוב שנקודת התחלה של הציור תהיה גם נקודה סופית של העקומה אחרונה.
                        // draw one cloudlet
                        ctx.moveTo( 410, 80);
                        ctx.bezierCurveTo( 410, 20, 448, 0, 510, 30);
                        ctx.bezierCurveTo( 565, 10, 595, 8, 630, 60);
                        ctx.bezierCurveTo( 700, 65, 700, 160, 610, 160);
                        ctx.bezierCurveTo( 580, 190, 540, 200, 490, 160);
                        ctx.bezierCurveTo( 400, 190, 370, 110, 410, 80);
                        ctx.fill();
  • אנחנו רוצים לצייר את העננים עד סוף הקנבס ברוחב, אז נעטוף את הקוד הזה בלולאת while עד x פחות מרוחב הקנבס. ובקוד שלנו לכל הקואורדינאטות נוסיף משתנים x וy- בהתאם. כל ענן יצויר אחרי 400 פיקסלים מרווח ברוחב. בנוסף לכך אנחנו רוצים שהעננים יופיעו אחד למטה, אחד למעלה. בשביל זה נשנה את הקואורדינאטה y בהתאם למספר הסידורי של הענן. זוגי יצויר בגובה y – 80 פיקסלים, ואי-זוגי – בגובה y של 40 פיקסלים.

וזה הקוד הסופי של ציור העננים:

		                function drawCloudlets() {
			                    // set cloudlets color fill
			                    ctx.fillStyle = "#bed2fb";
			                    // drawing cloudlet and duplication its until canvas with
			                    var y = 0,
				                          x = 0,
				                          i = 0;
			                    while ( x < canvasWidth ) {
				                        ctx.moveTo(x + 410, y + 80);
				                        ctx.bezierCurveTo(x + 410, y + 20, x + 448, y + 0, x + 510, y + 30);
				                        ctx.bezierCurveTo(x + 565, y + 10, x + 595, y + 8, x + 630, y + 60);
				                        ctx.bezierCurveTo(x + 700, y + 65, x + 700, y + 160, x + 610, y + 160);
				                        ctx.bezierCurveTo(x + 580, y + 190, x + 540, y + 200, x + 490, y + 160);
				                        ctx.bezierCurveTo(x + 400, y + 190, x + 370, y + 110, x + 410, y + 80);
				                        ctx.fill();
				                        x = x + 400;
				                        // for even cloudlet - draw upward
				                        if (i % 2 == 0) {
					                            y = y - 40;
				                        }
				                        // for odd wave - draw downward
				                        else y = y + 40;
				                        i++;
			                    }
		                }

גלים ריבועיים

wave

לפני שנצייר גלים, נגדיר את צורת החפיפה של האלמנטים כ destination-over (בשביל השהגלים יסתירו את הסירה שנצייר אחר כך) ונגדיר צבעים של גבול ומילוי.

              // set composite property for drawing skiff behind waves
		            ctx.globalCompositeOperation = "destination-over";

		            // set color of waves line
		            ctx.strokeStyle = "#416cf8";
		            // set color of waves fill
		            ctx.fillStyle = "#416cf8";

1. בשתי השורות הבאות נגדיר שאנחנו מתחילים לצייר צורה (אזור הגלים הוא בעצם צורה שמלמעלה – גבול גלי ובצדדים ולמטה – גבולות ישרים) ונקודת התחלה:

		                // begining draw waves shape (all blue area)
		                ctx.beginPath();
		                // set start point to left side of canvas
		                ctx.moveTo(0, 500);

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

quadratic_curve1

גל אחד שלנו יצויר בקוד:

		                ctx.quadraticCurveTo(50, 480, 110,500);

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

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

	              // draw waves until canvas width
		                x = y = 0;
		                var i = 1;
		                while ( x < canvasWidth ) {
			                    // for even wave - draw upward
			                    if ( i % 2 == 0 ) {
				                        y = 40;
			                    }
			                    // for odd wave - draw downward
			                    else y = 0;
			                    // drawe wave as quadraticCurve
			                    ctx.quadraticCurveTo(x + 50, y + 480, x + 110, 500);
			                    ctx.stroke();
			                    x = x + 120;
			                    i++;
		                }

3. אחרי ציור של כל הגלים עצרנו בנקודה סופית של קנבס בגובה 500 פיקסל. עכשיו צריך למתוח קוים בשביל לסגור את הצורה כולה ולמלא את הצורה בצבע שהגדרנו לפני תחילת ציור של גלים:

		                // close shape of waves for fill
		                ctx.lineTo(canvasWidth, canvasHeight);
		                ctx.lineTo(0, canvasHeight);
		                ctx.closePath();
		                // fill shape in selected by fillStyle color
		                ctx.fill();

סירה על פני המים

skiff

ציור של סירה מתבצע ע"י ציור קוים פשוטים ע"י מתודה lineTo שמציינים בה רק קואורדינאטות של נקודה סופית של הקו.

1. לפני זה נגדיר צבעים של קוים ומילוי:

		                //set lines color
		                ctx.strokeStyle = "#000000";
		                // set color fill
		                ctx.fillStyle = "#d99d4e";

2. נצייר את צורה של גוף הסירה ונמלא אותה בצבע מילוי (לפני זה נעביר את נקודת התחלה של הצורה לנקודה הרצויה):

		              // draw skiff body
	                ctx.beginPath();
	                ctx.moveTo(380, 510);
	                ctx.lineTo(320, 450);
	                ctx.lineTo(610, 450);
	                ctx.lineTo(550, 510);
	                ctx.closePath();
	                ctx.stroke();
	                ctx.fill();

3. נגדיר צבע מילוי של מפרש , נצייר אותו בצורת משולש ונמלא אותו בצבע:

		                // set sail color fill
		                ctx.fillStyle = "#ff6662";
		                //draw skiff sail
		                ctx.beginPath();
		                ctx.moveTo(435, 450);
		                ctx.lineTo(435, 250);
		                ctx.lineTo(530, 355);
		                ctx.lineTo(435, 410);
		                ctx.closePath();
		                ctx.stroke();
		                ctx.fill();

חוף מבטחים

island

צורה של אי – היא צורה דומה לחלק מהמעגל, אז נצייר עקומה ע"י מתודה quadraticCurveTo ונסגור את הצורה עם קוים שחוזרים על גבולות הקנבס בפינה ימנית תחתונה.

לפני שנצייר אי, נחזיר את צורת החפיפה של האלמנטים למצב דיפולטיבי שהוא source-over (בשביל שהאי יעלה מעל הצורה של הגלים) ונגדיר צבע מילוי:

              // set composite property for drawing island in front of waves
		            ctx.globalCompositeOperation = "source-over";

              // set composite property for drawing island in front of waves
		            ctx.globalCompositeOperation = "source-over";

		            //set island color fill
		            ctx.fillStyle = "#925112";

לאחר מכן נצייר את הצורה עם ציור עקומה וקוים ומילוי בצבע:

		                // draw island
		                ctx.beginPath();
		                ctx.moveTo(canvasWidth, 470);
		                ctx.quadraticCurveTo((canvasWidth - canvasWidth / 6), 470, (canvasWidth - canvasWidth / 2.2), canvasHeight);
		                ctx.lineTo(canvasWidth, canvasHeight);
		                ctx.closePath();
		                ctx.fill();

הדשא של (האי) השכן

grass

ציור של דשא מורכב מהשלבים הבאים:

1. הגדרת צבע ורוחב קוים:

		                //set grass lines color
		                ctx.strokeStyle = "#61e676";
		                //set with of lines
		                ctx.lineWidth = 2;

2. ציור קבוצה של 3 קוים בעזרת bezierCurveTo, שכל קו מתחיל באותה נקודה וציור 4 קבוצות של דשא ע"י לולאת for שכל קבוצה נמצאת למטה או למעלה לפי סדר רץ:

		            //draw 4 grass groups (3 blades in each group)
		            for ( var i = 0; i < 4; i++ ) {
			                ctx.moveTo(x + 750, y + 600);
			                ctx.bezierCurveTo(x + 752, y + 550, x + 770, y + 550, x + 785, y + 545);
			                ctx.moveTo(x + 750, y + 600);
			                ctx.bezierCurveTo(x + 740, y + 555, x + 750, y + 555, x + 760, y + 540);
			                ctx.moveTo(x + 750, y + 600);
			                ctx.bezierCurveTo(x + 725, y + 550, x + 735, y + 560, x + 710, y + 555);
			                ctx.moveTo(x + 750, y + 600);
			                x = x + 100;
			                // for grass group - draw upward
			                if ( i % 2 != 0 ) {
				                    y = y + 45;
			                }
			                // for odd grass group - draw downward
			                else y = y - 45;
		            }

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

		                // closing path and drawing lines
		                ctx.closePath();
		                ctx.stroke();

ברור שזו רק דוגמה קטנה (או לא ממש קטנה), רק בשביל להכיר את היכולת המדהימה הזו של HTML5 ואם פעם נצטרך איזושהי יצירת אמנות באתר – יש לנו את הכלי וקצת ידע גם.



6 תגובות לפוסט ”על גלי HTML5“

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

    אהבתי את הרזומה שלך גם כן.
    מצפה לבאות!

  2. אני רק שאלה.

    באיזה דפדפנים יש תמיכה בcanvas ?

    לא כדאי, עד שתהיה תמיכה בכל הדפדפנים בשוק, להשתמש בRaphael או ב
    ?jquery svg

  3. הי רן,
    זה נכון שלא כל הדפדפנים תומכים בcanvas עכשיו
    ( FF, Chrome, Safari, Opera, IE9 – כן תומכים ).
    כאן ניתן לראות בצורה מגניבה את התמיכה ב HTML5+CSS3 בכל הדפדפנים:
    http://html5readiness.com/.
    אבל אם רוצים תמיכה בIE – זה גם אפשרי ע"י הוספת סקריפט שעוזר בזה:
    http://code.google.com/p/explorercanvas/ .
    ולגבי שימוש בRaphael או ב jquery svg- זה תלוי במה שאתה צריך. המטרה שלי הייתה ללמוד לצייר בcanvas והתוצאה לפניך :)

  4. פשוט מקסים!

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

    אני בעצמי עובדת כרגע על פרוייקט גראפי ב-canvas.
    אין ספק שידע מעמיק במתמטיקה הוא הכרחי כדי לצייר בחופשיות כמו שעשית. ישר כח!

  5. אחלה כתבה!
    כל הכבוד על הפוסט המושקע, תיעוד ושיתוף הדברים!
    תודה.

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

לכתוב תגובה

(חובה לפחות לרשום שם!!!)

(...אף אחד לא יראה את זה)

(תפרסם/י את עצמך! שידעו מאיפה את/ה!)