ساخت تیتراژ جنگ ستارگان در HTML — از صفر تا صد

۱۳۸ بازدید
آخرین به‌روزرسانی: ۱۲ شهریور ۱۴۰۲
زمان مطالعه: ۱۰ دقیقه
ساخت تیتراژ جنگ ستارگان در HTML — از صفر تا صد

در این مقاله با شیوه ایجاد تیتراژ سریال جنگ ستارگان در HTML آشنا می‌شویم. این صحنه شامل دو بخش است که یکی ستارگان متحرک در پس‌زمینه است و دیگری متنی است که به سمت بالا حرکت می‌کند. برای مشاهده کد کامل این پروژه به این صفحه (+) مراجعه کنید.

پس‌زمینه میدان ستاره‌ای

در ابتدا یک میدان ستاره‌ای می‌سازیم که ستاره‌ها در آن در حال حرکت هستند. بدین منظور صرفاً از بوم HTML استفاده می‌کنیم. هیچ کتابخانه شخص ثالث یا ریاضیات پیشرفته‌ای لازم نیست.برای این که درک این افکت آسان باشد، از تکنیک‌های شهودی استفاده خواهیم کرد.

کد

کد کامل این افکت را می‌توانید در این آدرس (+) ملاحظه کنید. مارکاپ این مثال بسیار ساده است. یک بدنه HTML و یک بوم تعریف می‌شود و طوری استایل‌بندی می‌شود که کل پنجره مرورگر را بپوشاند.

1<!DOCTYPE html>
2<meta charset="utf-8" />
3<body
4  style="position: fixed; left: 0px; right: 0px; top: 0px; bottom: 0px; overflow: hidden; margin: 0; padding: 0;"
5>
6  <canvas
7    id="canvas"
8    style="width: 100%; height: 100%; padding: 0;margin: 0;"
9  ></canvas>
10  <script>
11     ...
12   </script>
13</body>

مدیریت موارد مختلف

اگر بخواهیم همه موارد را روی بوم رسم کنیم، باید عنصر DOM آن را با فراخوانی getContext("2d") به دست آوریم. به این ترتیب به canvas API دسترسی می‌یابیم:

1const canvas = document.getElementById("canvas");
2const c = canvas.getContext("2d");

ردگیری اندازه پنجره

بوم HTML دارای یک وضوح داخلی است که از سوی انتساب مقادیر مشخصه‌های width و height برای context تعیین می‌شوند. می‌خواهیم بوم همواره با اندازه پنجره یکسان بماند. از این رو عرض و ارتفاع بوم را برابر با اندازه پنجره تنظیم می‌کنیم. اندازه بوم در محاسبات بعدی برای ما لازم خواهد بود و از این رو در متغیرهای w و h ذخیره می‌کنیم.

1let w;
2let h;
3
4const setCanvasExtents = () => {
5  w = document.body.clientWidth;
6  h = document.body.clientHeight;
7  canvas.width = w;
8  canvas.height = h;
9};
10
11setCanvasExtents();
12    
13window.onresize = () => {
14  setCanvasExtents();
15};

در کد فوق به تغییر اندازه پنجره واکنش نشان می‌دهیم و اندازه بوم را بر همین مبنا تغییر می‌دهیم تا مطمئن شویم که وضوح تصویر بوم همواره با اندازه پنجره مطابقت دارد.

ستاره‌ها

هر ستاره به وسیله یک شیء که دارای مختصات x ،y و z است نمایش می‌یابد. 10 هزار شیء ایجاد می‌کنیم و آن‌ها را در یک آرایه قرار می‌دهیم. ستاره‌ها در سیستم مختصات مجازی قرار دارند که به صورت زیر است:

نقطه مرکزی سیستم مختصات در مرکز بوم رسم می‌شود. از مقدار z برای نشان دادن فاصله ستاره از صفحه x-y استفاده می‌کنیم.

توزیع ستاره‌ای زیر را برای سیستم مختصات تعیین کرده‌ایم:

  • مقادیر x از 800- تا 800+.
  • مقادیر y از 450- تا 450+.
  • مقادیر z از 0 تا 1000.

این محدوده مقادیر کاملاً دلخواه نیستند. باید کاری کنیم که ستاره‌ها در یک مستطیل 16 در 9 ایجاد شوند تا به اندازه معمول نمایشگرها نزدیک باشد. به این ترتیب در فضای قابل مشاهده صفحه جای می‌گیرند. بیشینه مسافت را 1000 واحد انتخاب کرده‌ایم، زیرا در نسبت بین عرض و ارتفاع مقدار مناسبی به نظر می‌آید. می‌توانید این اعداد را تغییر داده و نتیجه را مورد بررسی قرار دهید.

1const makeStars = (count) => {
2  const out = [];
3    for (let i=0;i<count;i++){
4      const s = {
5        x: Math.random()*1600-800,
6        y: Math.random()*900-450,
7        z: Math.random()*1000
8      };
9    out.push(s);
10  }
11  return out;
12}
13
14let stars = makeStars(10000);

در کد فوق ()Math.random یک عدد تصادفی بین 0 و 1 ایجاد می‌کند. بنابراین Math.random() * 1600 عددی بین 0 تا 1600 ایجاد می‌کند. با تعیین آفست برای این عدد می‌توانیم محدوده را شیفت کنیم. بدین ترتیب Math.random()*1600–800 یک عدد تصادفی بین 800- تا 800+ تولید می‌کند.

رسم پس‌زمینه

به روشی برای رسم ستاره‌ها روی بوم نیاز داریم. همچنین به یک پس‌زمینه سیاه نیاز داریم تا ستاره‌ها را روی آن قرار دهیم.

1const clear = () => {
2  c.fillStyle = "black";
3  c.fillRect(0, 0, canvas.width, canvas.height);
4};

تابع clear صرفاً کل بوم را بدون رنگ پس‌زمینه پوشش می‌دهد.

رسم یک ستاره منفرد

باید روشی برای رسم یک ستاره منفرد نیز در اختیار داشته باشیم. برای رسم هر ستاره دقیقاً یک پیکسل را با روشنایی بین 0 تا 1 رنگ‌آمیزی می‌کنیم. هر چه این مقدار بالاتر باشد، پیکسل روشن‌تر خواهد بود:

1const clear = () => {
2  c.fillStyle = "black";
3  c.fillRect(0, 0, canvas.width, canvas.height);
4};

تابع putPixel تعیین می‌کند که رنگ rgb مطلوب برای روشنایی مورد نظر چه قدر است و یک مستطیل تک پیکسلی را با آن مقدار پر می‌کند. رنگ‌های RGB بری مقادیر قرمز، سبز و آبی دارای مقادیری بین 0 تا 255 هستند. زمانی که هر سه مؤلفه برابر باشند، یک رنگ خاکستری به دست می‌آید. با مقیاس‌بندی 255 با یک مقدار بین 0.0 تا 1.0 برای همه مؤلفه‌ها در عمل یک رنگ خاکستری بین سیاه و سفید تولید می‌کنیم.

حرکت دادن ستاره‌ها

هر بار که یک فریم رسم می‌کنیم، می‌خواهیم ستاره‌ها در میدان ستاره‌ای به سمت ما حرکت کنند. به این منظور باید مختصات z ستاره را کاهش دهیم. زمانی که ستاره به صفحه x-y نزدیک می‌شود، آن‌ها را به عقب می‌فرستیم تا دوباره به صفحه نزدیک شوند.

1const moveStars = (distance) => {
2  const count = stars.length;
3  for (var i = 0; i < count; i++) {
4    const s = stars[i];
5    s.z -= distance;
6    while (s.z <= 1){
7      s.z += 1000;
8    }
9  }
10}

زمان‌بندی

کنترل سرعت اجرای انیمیشن حائز اهمیت است. این کار با استفاده از setTimeout یا setInterval ممکن است، اما آسان‌ترین روش برای رسم فریم بعدی این است که از مرورگر بخواهیم در زمان رفرش بعدی صفحه این کار را انجام دهد. این کار موجب می‌شود که مقدار ساعتی با وضوح بالا نیز به دست آوریم. ساعت به ما اعلام می‌کند که چه مقدار زمان از فریم بعدی گذشته است و از این رو انیمیشن ما تا چه حد پیشروی کرده است. تابع requestAnimationFrame (+) دقیقاً این کار را برای برای ما انجام می‌دهد. برای آغاز به کار متد init را فراخوانی می‌کنیم و مقدار ساعت با وضوح بالا را به آن ارسال می‌کنیم.

1let prevTime;
2const init = time => {
3  prevTime = time;
4  requestAnimationFrame(tick);
5};
6
7...
8
9requestAnimationFrame(init);

درون init مقدار ساعت را در prevTime ذخیره کرده و درخواست می‌کنیم که تابع tick در زمان بعدی فراخوانی شود. تابع tick میزان زمان سپری‌شده از آخرین رفرش را محاسبه می‌کند، در ادامه انیمیشن را به سمت جلو جابجا می‌کند، حالت بعدی ستاره‌ها را روی بوم رسم می‌کند و سپس درخواست می‌کند که در زمان رفرش بعدی صفحه فراخوانی شود.

1const tick = time => {
2  let elapsed = time - prevTime;
3  prevTime = time;
4
5  moveStars(elapsed*0.1);
6
7  clear();
8
9  const cx = w/2;
10  const cy = h/2;
11
12  const count = stars.length;
13  for (var i = 0; i < count; i++) {
14    const star = stars[i];
15
16    const x = cx + star.x/(star.z * 0.001);
17    const y = cy + star.y/(star.z * 0.001);
18
19    if (x < 0 || x >= w || y < 0 || y >= h){
20      continue;
21    }
22
23    const d = (star.z/1000.0)
24    const b = 1-d*d
25
26    putPixel(x, y, b);
27  }
28
29  requestAnimationFrame(tick);
30};

جابجایی در زمان

تابع tick میزان زمان سپری‌شده از آخرین فراخوانی را محاسبه می‌کند. مقدار زمان سپری‌شده برای ما مشخص می‌کند که ستاره‌ها باید چه قدر حرکت کنند. در یک محیط 60 فریم بر ثانیه، افزایش‌ها باید به میزان 1000/60 ~ 16.6 فراخوانی شوند:

1moveStars(elapsed*0.1);

این عدد به این جهت انتخاب شده تا 10% مقیاس‌بندی شود و از این رو برابر با 1.6 خواهد بود. همه ستاره‌ها بر اساس این مقدار جابجا می‌شوند. اگر می‌خواهید ستاره‌ها با سرعت بالاتری حرکت کنند، می‌توانید این عدد را تغییر دهید.

رسم ستاره‌ها در Perspective

در این بخش بوم پاک می‌شود و همه ستاره‌ها در حلقه رسم می‌شوند. مختصات x و y روی بوم از مختصات x ،y و z ستاره به دست می‌آیند.

مختصات

برای ترجمه مختصات ستاره‌ها به مختصات بوم به دو کار نیاز داریم. ابتدا باید پرسپکتیو را معرفی کنیم چون ستاره‌های دوردست‌تر به نقطه محو شدن (افق دید) نزدیک‌تر هستند. دوم باید مختصات مجازی خود را به مختصات بوم ترجمه کنیم. مبدأ مختصات بوم در گوشه بالا-چپ صفحه است و نه مرکز آن. از این رو باید این نکته را در نظر داشته باشیم.

پرسپکتیو ما در نقطه‌ای پیرامون افق تمرکز یافته است. یک ستاره در نقطه‌ای تقریباً نزدیک به ما، یعنی در مختصات (‎-200, -100, 100) باید در بالا و چپ مرکز ظاهر شود. ستاره‌ای دوردست‌تر در مختصات ‎-200, -100, 900 نیز باید در سمت چپ و بالا ظاهر شود، اما باید به نقطه محو شدن نزدیک‌تر باشد. همین نکته در مورد ربع‌های دیگر نیز صدق می‌کند. اگر یک ستاره نزدیک باشد، باید به مختصات نزدیک‌تری به 0 ترجمه شود.

تیتراژ سریال جنگ ستارگان در HTML

برای ایجاد چنین جلوه‌ای می‌توانیم x و y را بر z تقسیم کنیم. هر چه z بزرگ‌تر باشد، به صفر نزدیک‌تر می‌شود. اما تقسیم مستقیم بر صفر موجب به دست آمدن مقدار غیرحقیقی می‌شود. ما باید آن را کمی تخفیف دهیم تا طبیعی به نظر بیاید. بنابراین مقداری که بر آن تقسیم می‌کنیم را مقیاس‌بندی می‌نماییم. ما از عدد 0.001 استفاده می‌کنیم. می‌توانید مقادیری مشابه آن را نیز امتحان کنید تا نتیجه را عملاً بررسی نمایید. زمانی که این کار را انجام دادید، مختصات به میزان نصف ابعاد بوم آفست می‌شود و عملاً مختصات مبتنی بر صفر به مرکز بوم منتقل می‌شود:

1const x = cx + star.x/(star.z * 0.001);
2const y = cy + star.y/(star.z * 0.001);
3
4if (x < 0 || x >= w || y < 0 || y >= h){
5  continue;
6}

اگر ستاره بیرون از ناحیه قابل دیدن بیفتد، با ستاره دیگر کار خود را ادامه می‌دهیم و عملاً از فراخوانی فرایند رسم برای ستاره‌های ناپیدا جلوگیری می‌کنیم.

ستاره‌های روشن

همچنین می‌خواهیم ستاره‌هایی که به ما نزدیک‌تر هستند، روشن‌تر به نظر برسند و ستاره‌های دورتر تاریک‌تر باشند. می‌دانیم که دورترین ستاره‌ها در z=1000 هستند، از این رو می‌توانیم یک مقیاس روشنایی خطی با تقسیم کردن z بر 1000 به دست آوریم. این مقدار را d می‌نامیم.

هر چه ستاره دورتر باشد، مقدار d به 1 نزدیک‌تر است. هر چه ستاره به ما نزدیک‌تر باشد، d به 0 نزدیک‌تر می‌شود. از آنجا که می‌خواهیم روشنایی در زمان نزدیک شدن به ما بیشتر افزایش یابد، باید این مقدار را با محاسبه 1-d معکوس کنیم. بدین ترتیب یک گردیان خطی از روشنایی ستاره به دست می‌آید.

محاسبه را کمی دستکاری می‌کنیم به طوری که افزایش نور ستاره‌ها سریع‌تر از روند خطی باشد. در واقع به جای کسر کردن از d از d^2 کسر می‌کنیم.

تیتراژ سریال جنگ ستارگان در HTML

1const d = (star.z/1000.0)
2const b = 1-d*d
3
4putPixel(x, y, b);

اینک تنها کاری که باقی مانده است، فراخوانی تابع رسم putPixel است که پیکسل ستاره‌ها را روی بوم قرار می‌دهد. بدین ترتیب یک جلوه 3 بعدی ابتدایی داریم که میدان ستاره‌ای مورد نظرمان را نمایش می‌دهد. برای این که تیتراژ ما طبیعی‌تر به نظر برسد، برخی تغییرات در این میان ستاره‌ای اعمال می‌کنیم. به این منظور تعداد ستاره‌ها روی 2000 تنظیم شده است. وضوح تصویر میدان ستاره‌ای نیز برابر با 1600 در 900 پیکسل است. حرکت ستاره‌ها نیز کمی کندتر شده است.

حرکت متن

ساختار نشانه‌گذاری این بخش به صورت زیر است:

1<div id="crawl-container" class="stretch">
2  <div id="crawl"> <!-- our plane in 3d -->
3    <div id="crawl-content">
4      <h1>Episode IV</h1>
5      <h2>A NEW HOPE</h2>
6      <p>It is a period of civil war. 
7      ...
8    </div>
9  </div>
10</div>

از یک تبدیل سه‌بعدی CSS روی div به نام crawl استفاده کرده‌ایم تا یک صفحه در فضای سه‌بعدی ایجاد کنیم. لبه تحتانی این صفحه را با لبه پایینی صفحه هم‌راستا ساخته‌ایم. ارتفاع صفحه دو برابر ارتفاع صفحه است و سپس به سمت عقب متمایل شده است بنابراین لبه بالایی درون نمایشگر قرار می‌گیرد.

در ادامه موقعیت بالایی crawl-content را به سمت بالا انیمیت می‌کنیم و از این رو در نظر ما این گونه به نظر می‌رسد که در حال حرکت است. در بخش نهایی یک ماسک شفافیت اضافه می‌کنیم به طوری که به نظر می‌رسد خطوط متن در بخش فوقانی محو می‌شوند. همه بخش‌های دیگر کدهای معمولی CSS است که تلاش شده است فونت و رنگ متن تیتراژ سریال جنگ ستارگان را شبیه‌سازی کند.

افزودن پرسپکتیو

یک پرسپکتیو تشدید یافته روی صفحه div به نام plane می‌خواهیم. شدت کج شدن پرسپکتیو از سوی مشخصه perspective (+) در CSS تنظیم می‌شود. توجه کنید فرزندان گره جاری تحت تأثیر این مشخصه قرار می‌گیرند و نه خود گره. از این رو آن را روی گره والد یعنی crawl-container اعمال می‌کنیم:

1#crawl-container {
2  perspective: calc(100vh * 0.4);
3}

از واحد ارتفاع ویوپورت استفاده می‌کنیم و آن‌ها را تا 0.4 مقیاس‌بندی می‌کنیم تا به طرز مناسبی دیده شوند. مقدار ارتفاع را به این جهت کاهش دادیم که وقتی اندازه پنجره تغییر می‌یابد، نقطه انتهایی حرکت متن ما همچنان در همان ارتفاع نسبی قرار گیرد.

ایجاد صفحه 3 بعدی

در این بخش به بررسی استایل‌بندی صفحه متنی 3 بعدی می‌پردازیم.

1#crawl {
2  color: #f5c91c;      // yellow color for all text 
3  
4  position: absolute;  // fixed in place
5  width: 110%;         // a bit broader than screen
6  left: -5%;           // symmetric offset to center it in
7  bottom: -5%;         // bottom anchored just below screen edge
8  height: 200%;        // twice as high as screen
9  overflow: hidden;    // don't show scroll bars
10  // the 3D part  
11 
12  // plane origin at center of bottom edge
13  transform-origin: 50% 100%; 
14  // rotate around x axis by 45 degrees (pushing it over and in)
15  transform: rotate3d(1, 0, 0, 45deg);
16}

هر محتوایی که درون این صفحه قرار گیرد به نظر می‌رسد که به صورت 3 بعدی حرکت می‌کند.

تیتراژ سریال جنگ ستارگان در HTML

محو کردن خطوط دورتر

در این بخش یک ماسک به صفحه 3 بعدی خود اضافه می‌کنیم تا خطوط دورتر محو شوند. به این منظور از گرادیان خطی (https://developer.mozilla.org/en-US/docs/Web/CSS/linear-gradient) با 3 نقطه به صورت top ،middle و bottom استفاده می‌کنیم. در بخش فوقانی همه محتوا محو می‌شود، یعنی میزان opacity برابر با 0% است. در میانه میزان مات بودن روی 66% تنظیم شده است و در انتها متن کاملاً مات است، چون مقدار opacity روی 100% قرار دارد.

1#crawl {
2  mask-image: linear-gradient(
3    rgba(0, 0, 0, 0),
4    rgba(0, 0, 0, 0.66),
5    rgba(0, 0, 0, 1)
6  );
7}

همچنین اگر از تعاریف پیشوند دار webkit- استفاده کنید، سازگاری بهتری با مرورگرهای مختلف خواهد یافت. برخی مرورگرها از آن‌ها پشتیبانی می‌کنند، اما هنوز از نمادگذاری رایج استفاده نمی‌کنند.

تیتراژ سریال جنگ ستارگان در HTML
سمت چپ: بدون ماسک – سمت راست: با استفاده از ماسک

انیمیت کردن متن

محتوای تیتراژ به صورت absolute موقعیت‌یابی شده است و از این رو می‌توان با دستکاری مشخصه top آن را جابجا کرد. از قبل یک حلقه انیمیشن ابتدایی برای میدان ستاره‌ای خود داریم. می‌توانیم از آن استفاده کرده و تیتراژ را روی آن انیمیت کنیم. زمانی که متن خارج می‌شود، دوباره وارد صفحه می‌کنیم. کد به صورت زیر است:

1const crawl = document.getElementById("crawl");
2const crawlContent = document.getElementById("crawl-content");
3const crawlContentStyle = crawlContent.style;
4
5// start crawl at bottom of 3d plane
6let crawlPos = crawl.clientHeight;
7
8const moveCrawl = distance => {
9  crawlPos -= distance;
10  crawlContentStyle.top = crawlPos + "px";
11  
12  // if we've scrolled all content past the top edge
13  // of the plane, reposition content at bottom of plane
14  if (crawlPos < -crawlContent.clientHeight) {
15    crawlPos = crawl.clientHeight;
16  }
17};
18
19let prevTime;
20const init = time => {
21  prevTime = time;
22  requestAnimationFrame(tick);
23};
24
25const tick = time => {
26  let elapsed = time - prevTime;
27  prevTime = time;
28  
29  // time-scale of crawl, increase factor to go faster
30  moveCrawl(elapsed * 0.04);
31  requestAnimationFrame(tick);
32};
33
34requestAnimationFrame(init);

استایل‌بندی متن

تلاش ما بر این است که تا حد امکان ظاهر تیتراژ اصلی سریال را شبیه‌سازی کنیم. بنابراین از فونت Roboto با وزن متوسط استفاده کرده‌ایم تا تقریباً شبیه فونت تیتراژ سریال بشود. برای این که حس نزدیکی بیشتری به تیتراژ اصلی ایجاد کنیم، کمی فاصله بین حروف نیز ایجاد کرده‌ایم. در نهایت اندازه فونت را به اندازه عرض صفحه کاهش می‌دهیم. تا زمانی که اندازه پنجره تغییر می‌یابد، متن همچنان به صورت طبیعی ظاهر شود:

1#crawl-content {
2  font-family: "Roboto";
3  letter-spacing: 0.09em;
4  font-size: calc(100vw * 0.074);
5}

نتیجه کار به صورت زیر است:

تیتراژ سریال جنگ ستارگان در HTML

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۰ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
better-programmingbetter-programming
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *