فهرست
یکی از کارایی که کامپیوترها خوب انجام میدن، تکرار کردنه. فرض کنید قراره هزار تا شکل با ویژگیهای مختلف توی برنامهتون بسازید؛ برای چنین مواردی که قراره یه کاری به دفعات زیادی تکرار کنیم، توی برنامهنویسی از ساختاری به نام حلقه یا لوپ استفاده میکنیم که از مهمترین مفاهیم برنامهنویسی هستن و میتونیم باهاش یه تیکه کُد رو بارها و بارها تکرار کنیم.
ما که داریم برنامهنویسی رو با p5.js یاد میگیریم، از قبل با مفهوم حلقه آشنا هستیم: تابع draw
یه لوپه که داره یه تیکه کُد رو همین طوری پشت سر هم تکرار میکنه تا وقتی که برنامه بسته بشه؛ اما خودمون چطوری میتونیم چنین لوپی بنویسیم؟
روش اول: ساختن حلقه فور
توی جاوااسکریپت برای به ساختن یه لوپ چند تا راه وجود داره. من معمولاً از for
استفاده میکنم. برنامه ما با استفاده از for
میتونه یک عملیاتو به دفعاتی که خودمون مشخص میکنیم، تکرار کنه. یه لوپِ for
از پنج عنصر تشکیل میشه:
- کلیدواژه
for
ـ دربرگیرنده کلِ حلقه - متغیر اولیه ـ یه جور شمارنده که معمولاً از
0
شروع میشه - شرط ـ شرطی که تا وقتی
true
باشه لوپ ادامه پیدا میکنه - بهروزرسانی متغیر اولیه ـ آخر هر تکرار، متغیر اولیه بهروز میشه
- عملیات ـ اتفاقهایی که توی هر بار تکرار حلقه میافته
function setup() {
createCanvas(windowWidth, windowHeight);
}
function draw() {
background(200)
fill(220, 30, 90)
noStroke()
// a for loop
for(let i = 0; i < 10; i++) {
ellipse(i*50, 0, 50, 50)
}
}
توی مثالِ سادهی بالا میتونید پنج مرحله ساختن یه حلقه رو ببینید:
- (اول) با کلیدواژه
for
یه حلقه ساختیم - (دوم) با کلیدواژه
let
یه متغیر تعریف کردیم و مقدارشو0
قرار دادیم - (سوم) شرط ادامه حلقه رو کوچکتر از
10
بودن متغیر اولیه مشخص کردیم - (چهارم) تعیین کردیم که بعد از هر بار تکرار حلقه، یه واحد به متغیر اولیه اضافه بشه
- (پنجم) عملیاتی که حلقه قراره انجام بده رو مشخص کردیم که کشیدن یه دایرهس
وقتی برنامه رو اجرا کنید میبینید که ده تا دایره روی صفحه کشیده میشه؛ چون یازدهمین بار که حلقه اجرا میشه، متغیر اولیهی ما 11
شده و در این صورت شرط حلقه، یعنی i < 10
دیگه true
نیست، حلقه بسته میشه و تمام.
روش دوم: ساختن حلقه با استفاده از آرایه
آرایهها رو که یادتون هست؟ یه راه دیگه برای ساختن حلقهی for
استفاده کردن از یه آرایهس؛ توی این حالت، نوشتن حلقه یه خرده سادهتره و دیگه نیازی به تعریف کردن یه متغیر و این جور کارا ندارید؛ ببینید چطور نوشته میشه:
function setup() {
createCanvas(windowWidth, windowHeight);
// a for loop
const planets = ['mercury', 'venus', 'earth']
for(planet of planets) {
console.log(planet)
}
}
function draw() {
}
روش سوم: ساختن حلقه وایل
یه جور حلقه دیگه هم هست به نام while
که تا زمانی که یه شرط true
باشه تکرار میشه. توی مثال پایین این قدر حلقه تکرار میشه که متغیر myValue
به عدد صد برسه؛ توی هر بار اجرا شدن حلقه هم عدد 10
به myValue
اضافه میشه:
function setup() {
createCanvas(windowWidth, windowHeight);
// a while loop
let myValue = 0
while(myValue < 100) {
myValue += 10
console.log('The value of myValue is ' + myValue)
}
}
function draw() {
}
در عمل حلقهها به چه دردی میخورن؟
حالا که با حلقهها آشنا شدیم میتونیم ببینیم توی یه پروژه واقعی چطوری میشه از حلقهها استفاده کرد. از یه مثال ساده شروع میکنم: توی این اسکیس میخوام یه مربع بکشم و توشو هاشور بزنم.
const rectWidth = 100
function setup() {
createCanvas(windowWidth, windowHeight)
rectMode(CENTER)
background(200)
fill(235)
noLoop()
}
function draw() {
// square
rect(windowWidth/2, windowHeight/2, rectWidth)
// hatch
translate(windowWidth/2 - rectWidth/2, windowHeight/2 - rectWidth/2)
for(let i = 0; i < rectWidth; i += 10) {
line(i, 0, 0, i)
}
for(let i = 0; i < rectWidth; i += 10) {
line(rectWidth, i, i, rectWidth)
}
}
ببینیم توی مثال بالا دقیقاً چه اتفاقی افتاده:
اول وسط بوم یه مربع کشیدم؛ برای این کار از متغیری به نام rectWidth
استفاده کردم که در واقع اندازه مربعی که قراره کشیده بشه رو مشخص میکنه. میتونید تغییرش بدید و نتیجه رو ببینید. همچنین از پارامتر CENTER
توی تابع rectMode
استفاده کردم که باعث میشه مربع از مرکز خودش جانمایی بشه.
بعد با استفاده از تابع translate
نقطه صفر و صفر بوم رو به گوشه مربع منتقل میکنم.
حالا باید از حلقه for
استفاده کنم تا با فاصله ده پیکسل ده پیکسل و با استفاده از تابع line
نقاط روی اضلاع مربع رو به هم وصل کنم. همون طور که میبینید برای کشیدن خطوط نیمه بالای مربع از یه حلقه استفاده کردم و برای کشیدن خطوط نیمه پایین مربع، از یه حلقه دیگه.
توی اسکیس بعدی میخوام از یه حلقه for
استفاده کنم و یه ستاره بکشم:
function setup() {
createCanvas(windowWidth, windowHeight)
}
function draw() {
background(255)
strokeWeight(10)
// move canvas origin to center
translate(width / 2, height / 2)
// number of lines
const circleResolution = int(map(mouseY, 0, height, 1, 15))
// line length
const radius = mouseX - width
// spread lines in a circular shape
const angle = TWO_PI / circleResolution
// loop
for (let i=0; i<=circleResolution; i++) {
const x = cos(angle * i) * (radius/5);
const y = sin(angle * i) * (radius/5);
line(0, 0, x, y);
}
}
اگه این کد رو اجرا کنید میبینید که اندازه و تعداد پرهای ستاره با حرکت دادن ماوس روی صفحه تغییر میکنه؛ قبلاً یاد گرفتیم که هر کدی که توی تابع draw
نوشته بشه توی هر فریم یک بار تکرار میشه و برای همینه که ما میتونیم چیزهای مختلفو روی مانیتور به حرکت دربیاریم.
توی اسکیس بالا هم از تابع translate
استفاده کردم تا نقطه صفر و صفرِ بوم رو به وسط صفحه منتقل کنم. بعد از تابع map
استفاده کردم و تعداد پَرهای ستاره رو نسبت به موقعیت ماوس توی متغیر circleResolution
ذخیره کردم. بعد بازم بر اساس موقعیت ماوس طول پَرهای ستاره رو توی متغیر radius
ذخیره کردم. بعد بر اساس تعداد پَرهای ستاره، زاویه بین هر پَر ستاره رو مشخص کردم و در نهایت یه لوپ نوشتم تا پَرهای ستاره رو بر اساس متغیرهایی که تعریف کردم بکشه.
میتونیم به جای کشیدن این خطوط، از تابع vertex
که قبلاً یادگرفتیم استفاده کنیم و شکلهای مختلف تولید کنیم:
function setup() {
createCanvas(windowWidth, windowHeight)
noFill()
background(255)
strokeWeight(3)
stroke(0, 25)
}
function draw() {
if (mouseIsPressed && mouseButton == LEFT) {
push()
translate(windowWidth / 2, windowHeight / 2)
const circleResolution = int(map(mouseY + 100, 0, windowHeight, 3, 10))
const radius = mouseX - width / 2
const angle = TWO_PI / circleResolution
// shape
beginShape()
for (let i = 0; i <= circleResolution; i++) {
const x = cos(angle * i) * radius
const y = sin(angle * i) * radius
vertex(x, y)
}
endShape()
pop()
}
}
حلقه های تو در تو
توی حلقهها میشه دوباره حلقه نوشت؛ به این الگو میگن حلقههای تودرتو؛ این الگو خیلی رایجه و معمولاً برای درست کردن یه شبکه استفاده میشه؛ مثلاً به این اسکیس نگاه کنید؛ توی این اسکیس با شکل دایره یه شبکه روی بوم میسازیم که تعداد خونههای این شبکه توسط متغیر tileCount
مشخص میشه و شعاع هر دایره بر اساس موقعیت مکانی ماوس محاسبه میشه:
const tileCount = 15
const circleAlpha = 130
function setup() {
createCanvas(windowWidth, windowHeight);
}
function draw() {
background(255)
randomSeed(0)
strokeWeight(5)
translate(windowWidth / tileCount / 2, windowHeight / tileCount / 2)
for (let gridY = 0; gridY < tileCount; gridY++) {
for (let gridX = 0; gridX < tileCount; gridX++) {
const posX = windowWidth / tileCount * gridX
const posY = windowHeight / tileCount * gridY
ellipse(posX, posY, mouseY / tileCount)
}
}
}
بینظمی مکعبی
این یکی یه اسکیسِ معروفه به نام «بینظمی مکعبی» که با الگوی حلقههای تودرتو نوشته میشه؛ این مورد رو با همدیگه به صورت قدم به قدم مینویسیم: با یه اسکیس ساده شروع میکنیم که شامل setup
و draw
باشه و از noLoop
استفاده میکنیم چون یه اسکیس استاتیک یا ایستا داریم درست میکنیم که نیازی به انیمیشن نداره؛ یه متغیر هم نیاز داریم برای ذخیره اندازه مربعها:
const squareSize = 50
function setup() {
createCanvas(windowWidth, windowHeight)
noFill()
noLoop()
background(220)
}
function draw() {
}
مرحله بعدی اینه که یه چیزی بکشیم. میخوام همه صفحه رو با مربع پُر کنم. سایز مربعها بر اساس متغیر squareSize
مشخص میشه. از تابعهای push
و translate
و pop
داخل حلقههای تودرتو استفاده میکنم تا ردیفها و ستونهای شبکه رو تعریف کنم و با تابع rect
مربعها رو میکشم:
const squareSize = 50
function setup() {
createCanvas(windowWidth, windowHeight)
noFill()
noLoop()
strokeWeight(3)
background(220)
}
function draw() {
for(let x = squareSize*2; x <= width - squareSize*2; x += squareSize) {
for(let y = squareSize*2; y <= height - squareSize*2; y += squareSize) {
push()
translate(x, y)
rect(-squareSize/2, -squareSize/2, squareSize)
pop()
}
}
}
حالا میخوایم نظمشونو به هم بزنیم، طوری که هر چی به سمت پایین بوم بریم، بینظمی بیشتری رو ببینیم:
const squareSize = 50
function setup() {
createCanvas(windowWidth, windowHeight)
noFill()
noLoop()
strokeWeight(3)
background(220)
}
function draw() {
for(let x = squareSize*2; x <= width - squareSize*2; x += squareSize) {
for(let y = squareSize*2; y <= height - squareSize*2; y += squareSize) {
// random rotation
const rotation = y/height * QUARTER_PI/45 * random() * 25;
// create each square
push()
translate(x, y);
rotate(rotation);
rect(-squareSize/2, -squareSize/2, squareSize)
pop()
}
}
}