Emscripten में कैनवस की तरफ़ ड्रॉइंग

Emscripten की मदद से, WebAssembly से वेब पर 2D ग्राफ़िक रेंडर करने का तरीका जानें.

अलग-अलग ऑपरेटिंग सिस्टम में, ग्राफ़िक ड्रॉ करने के लिए अलग-अलग एपीआई होते हैं. क्रॉस-प्लैटफ़ॉर्म कोड लिखते समय या ग्राफ़िक को एक सिस्टम से दूसरे सिस्टम में पोर्ट करते समय, इनमें अंतर और भी भ्रमित करने वाला हो जाता है. इसमें, नेटिव कोड को WebAssembly में पोर्ट करने का मामला भी शामिल है.

इस पोस्ट में, Emscripten के साथ कॉम्पाइल किए गए C या C++ कोड से, वेब पर कैनवस एलिमेंट में 2D ग्राफ़िक्स बनाने के कुछ तरीके जानेंगे.

अगर आपको किसी मौजूदा प्रोजेक्ट को पोर्ट करने के बजाय नया प्रोजेक्ट शुरू करना है, तो Emscripten के बाइंडिंग सिस्टम Embind की मदद से, एचटीएमएल Canvas API का इस्तेमाल करना आसान हो सकता है. Embind की मदद से, सीधे तौर पर किसी भी JavaScript वैल्यू पर काम किया जा सकता है.

Embind का इस्तेमाल करने का तरीका जानने के लिए, पहले MDN का यह उदाहरण देखें. इसमें <canvas> एलिमेंट ढूंढा गया है और उस पर कुछ आकार बनाए गए हैं

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

ctx.fillStyle = 'green';
ctx.fillRect(10, 10, 150, 100);

Embind की मदद से, इसे C++ में ट्रांसलिटरेट करने का तरीका यहां बताया गया है:

#include <emscripten/val.h>

using emscripten::val;

// Use thread_local when you want to retrieve & cache a global JS variable once per thread.
thread_local const val document = val::global("document");

// …

int main() {
  val canvas = document.call<val>("getElementById", "canvas");
  val ctx = canvas.call<val>("getContext", "2d");
  ctx.set("fillStyle", "green");
  ctx.call<void>("fillRect", 10, 10, 150, 100);
}

इस कोड को लिंक करते समय, Embind को चालू करने के लिए --bind को पास करना न भूलें:

emcc --bind example.cpp -o example.html

इसके बाद, स्टैटिक सर्वर की मदद से कंपाइल की गई ऐसेट को दिखाया जा सकता है और उदाहरण को ब्राउज़र में लोड किया जा सकता है:

Emscripten से जनरेट किया गया एचटीएमएल पेज, जिसमें काले कैनवस पर हरा रेक्टैंगल दिख रहा है.

कैनवस एलिमेंट चुनना

पिछले शेल कमांड के साथ Emscripten से जनरेट किए गए एचटीएमएल शेल का इस्तेमाल करने पर, कैनवस शामिल किया जाता है और उसे आपके लिए सेट अप किया जाता है. इससे आसानी से डेमो और उदाहरण बनाए जा सकते हैं. हालांकि, बड़े ऐप्लिकेशन में, आपको अपने डिज़ाइन के एचटीएमएल पेज में, Emscripten से जनरेट किए गए JavaScript और WebAssembly को शामिल करना होगा.

जनरेट किए गए JavaScript कोड को Module.canvas प्रॉपर्टी में सेव किया गया कैनवस एलिमेंट मिलना चाहिए. अन्य मॉड्यूल प्रॉपर्टी की तरह ही, इसे शुरू करने के दौरान सेट किया जा सकता है.

अगर ES6 मोड का इस्तेमाल किया जा रहा है (.mjs एक्सटेंशन वाले पाथ पर आउटपुट सेट करना या -s EXPORT_ES6 सेटिंग का इस्तेमाल करना), तो कैनवस को इस तरह पास किया जा सकता है:

import initModule from './emscripten-generated.mjs';

const Module = await initModule({
  canvas: document.getElementById('my-canvas')
});

अगर सामान्य स्क्रिप्ट आउटपुट का इस्तेमाल किया जा रहा है, तो Emscripten से जनरेट की गई JavaScript फ़ाइल लोड करने से पहले, आपको Module ऑब्जेक्ट का एलान करना होगा:

<script>
var Module = {
  canvas: document.getElementById('my-canvas')
};
</script>
<script src="emscripten-generated.js"></script>

OpenGL और SDL2

OpenGL, कंप्यूटर ग्राफ़िक्स के लिए एक लोकप्रिय क्रॉस-प्लैटफ़ॉर्म एपीआई है. Emscripten में इस्तेमाल करने पर, यह OpenGL ऑपरेशन के काम करने वाले सबसेट को WebGL में बदल देगा. अगर आपका ऐप्लिकेशन, OpenGL ES 2.0 या 3.0 में काम करने वाली सुविधाओं पर निर्भर है, लेकिन WebGL में नहीं, तो Emscripten उन सुविधाओं को भी एमुलेट कर सकता है. हालांकि, इसके लिए आपको इससे जुड़ी सेटिंग के ज़रिए ऑप्ट-इन करना होगा.

OpenGL का इस्तेमाल सीधे तौर पर या उच्च लेवल की 2D और 3D ग्राफ़िक लाइब्रेरी के ज़रिए किया जा सकता है. इनमें से कुछ को Emscripten की मदद से वेब पर पोर्ट किया गया है. इस पोस्ट में, हम 2D ग्राफ़िक्स पर फ़ोकस कर रहे हैं. इसके लिए, फ़िलहाल SDL2 सबसे पसंदीदा लाइब्रेरी है, क्योंकि इसकी अच्छी तरह से जांच की गई है और यह Emscripten बैकएंड के साथ आधिकारिक तौर पर काम करती है.

आयत बनाना

आधिकारिक वेबसाइट पर "SDL के बारे में जानकारी" सेक्शन में यह जानकारी दी गई है:

Simple DirectMedia Layer, एक क्रॉस-प्लैटफ़ॉर्म डेवलपमेंट लाइब्रेरी है. इसे OpenGL और Direct3D की मदद से, ऑडियो, कीबोर्ड, माउस, जॉयस्टिक, और ग्राफ़िक हार्डवेयर का लो लेवल ऐक्सेस देने के लिए डिज़ाइन किया गया है.

ऑडियो, कीबोर्ड, माउस, और ग्राफ़िक को कंट्रोल करने जैसी सभी सुविधाओं को वेब पर Emscripten के साथ काम करने के लिए पोर्ट किया गया है. इससे, SDL2 के साथ बनाए गए पूरे गेम को आसानी से पोर्ट किया जा सकता है. अगर किसी मौजूदा प्रोजेक्ट को पोर्ट किया जा रहा है, तो Emscripten के दस्तावेज़ों में "बिल्ड सिस्टम के साथ इंटिग्रेट करना" सेक्शन देखें.

आसानी के लिए, इस पोस्ट में हम एक फ़ाइल वाले मामले पर फ़ोकस करेंगे. साथ ही, पहले दिए गए रेक्टैंगल के उदाहरण को SDL2 में बदलेंगे:

#include <SDL2/SDL.h>

int main() {
  // Initialize SDL graphics subsystem.
  SDL_Init(SDL_INIT_VIDEO);

  // Initialize a 300x300 window and a renderer.
  SDL_Window *window;
  SDL_Renderer *renderer;
  SDL_CreateWindowAndRenderer(300, 300, 0, &window, &renderer);

  // Set a color for drawing matching the earlier `ctx.fillStyle = "green"`.
  SDL_SetRenderDrawColor(renderer, /* RGBA: green */ 0x00, 0x80, 0x00, 0xFF);
  // Create and draw a rectangle like in the earlier `ctx.fillRect()`.
  SDL_Rect rect = {.x = 10, .y = 10, .w = 150, .h = 100};
  SDL_RenderFillRect(renderer, &rect);

  // Render everything from a buffer to the actual screen.
  SDL_RenderPresent(renderer);

  // TODO: cleanup
}

Emscripten के साथ लिंक करते समय, आपको -s USE_SDL=2 का इस्तेमाल करना होगा. इससे Emscripten को, पहले से ही WebAssembly में प्री-कंपाइल की गई SDL2 लाइब्रेरी को फ़ेच करने और उसे आपके मुख्य ऐप्लिकेशन से लिंक करने का निर्देश मिलेगा.

emcc example.cpp -o example.html -s USE_SDL=2

उदाहरण ब्राउज़र में लोड होने के बाद, आपको हरे रंग का रेक्टैंगल दिखेगा:

Emscripten से जनरेट किया गया एचटीएमएल पेज, जिसमें काले रंग के स्क्वेयर कैनवस पर हरा रेक्टैंगल दिख रहा है.

हालांकि, इस कोड में कुछ समस्याएं हैं. सबसे पहले, इसमें आवंटित किए गए रिसोर्स को ठीक से क्लीनअप नहीं किया जाता. दूसरा, वेब पर, किसी ऐप्लिकेशन के बंद होने पर पेज अपने-आप बंद नहीं होते. इसलिए, कैनवस पर मौजूद इमेज सेव रहती है. हालांकि, जब उसी कोड को नेटिव तौर पर

clang example.cpp -o example -lSDL2

और उसे लागू किया जाता है, तो बनाई गई विंडो सिर्फ़ कुछ समय के लिए झपकेगी और बाहर निकलने पर तुरंत बंद हो जाएगी. इससे, उपयोगकर्ता को इमेज देखने का मौका नहीं मिलेगा.

इवेंट लूप को इंटिग्रेट करना

एक ज़्यादा बेहतर और सामान्य उदाहरण के तौर पर, इवेंट लूप में तब तक इंतज़ार करना होगा, जब तक उपयोगकर्ता ऐप्लिकेशन को बंद नहीं कर देता:

#include <SDL2/SDL.h>

int main() {
  SDL_Init(SDL_INIT_VIDEO);

  SDL_Window *window;
  SDL_Renderer *renderer;
  SDL_CreateWindowAndRenderer(300, 300, 0, &window, &renderer);

  SDL_SetRenderDrawColor(renderer, /* RGBA: green */ 0x00, 0x80, 0x00, 0xFF);
  SDL_Rect rect = {.x = 10, .y = 10, .w = 150, .h = 100};
  SDL_RenderFillRect(renderer, &rect);
  SDL_RenderPresent(renderer);

  while (1) {
    SDL_Event event;
    SDL_PollEvent(&event);
    if (event.type == SDL_QUIT) {
      break;
    }
  }

  SDL_DestroyRenderer(renderer);
  SDL_DestroyWindow(window);

  SDL_Quit();
}

इमेज को विंडो में ड्रॉ करने के बाद, ऐप्लिकेशन एक लूप में इंतज़ार करता है. इस दौरान, वह कीबोर्ड, माउस, और उपयोगकर्ता के अन्य इवेंट को प्रोसेस कर सकता है. जब उपयोगकर्ता विंडो बंद करेगा, तो वह SDL_QUIT इवेंट ट्रिगर करेगा. इस इवेंट को लूप से बाहर निकलने के लिए इंटरसेप्ट किया जाएगा. लूप से बाहर निकलने के बाद, ऐप्लिकेशन क्लीनअप करेगा और फिर अपने-आप बंद हो जाएगा.

अब Linux पर इस उदाहरण को कंपाइल करने पर, उम्मीद के मुताबिक काम होता है और हरे रंग के रेक्टैंगल के साथ 300 x 300 विंडो दिखती है:

काले रंग के बैकग्राउंड और हरे रंग के रेक्टैंगल वाली स्क्वेयर Linux विंडो.

हालांकि, यह उदाहरण अब वेब पर काम नहीं करता. Emscripten से जनरेट किया गया पेज, लोड होने के दौरान तुरंत फ़्रीज़ हो जाता है और रेंडर की गई इमेज कभी नहीं दिखाता:

Emscripten से जनरेट किया गया एचटीएमएल पेज, जिस पर &#39;पेज काम नहीं कर रहा है&#39; गड़बड़ी का डायलॉग ओवरले किया गया है. इसमें, पेज के काम करने का इंतज़ार करने या पेज से बाहर निकलने का सुझाव दिया गया है

क्या हुआ? हम "WebAssembly से एसिंक्रोनस वेब एपीआई का इस्तेमाल करना" लेख में दिए गए जवाब को कोट करेंगे:

आसान शब्दों में, ब्राउज़र कोड के सभी हिस्सों को एक अनलिमिटेड लूप में चलाता है. इसके लिए, वह उन्हें लाइन में एक-एक करके लेता है. जब कोई इवेंट ट्रिगर होता है, तो ब्राउज़र उससे जुड़े हैंडलर को सूची में जोड़ देता है. इसके बाद, अगले लूप में उसे सूची से हटाकर चलाया जाता है. इस तरीके से, एक ही थ्रेड का इस्तेमाल करके, एक साथ कई काम किए जा सकते हैं.

इस तरीके के बारे में यह याद रखना ज़रूरी है कि जब आपका कस्टम JavaScript (या WebAssembly) कोड चलता है, तब इवेंट लूप ब्लॉक हो जाता है […]

ऊपर दिया गया उदाहरण, अनलिमिटेड इवेंट लूप को लागू करता है. हालांकि, कोड खुद ही एक दूसरे अनलिमिटेड इवेंट लूप में चलता है, जिसे ब्राउज़र ने इंप्लिसिट तौर पर उपलब्ध कराया है. इनर लूप, आउटर लूप को कभी कंट्रोल नहीं देता. इसलिए, ब्राउज़र को बाहरी इवेंट को प्रोसेस करने या पेज पर चीज़ें ड्रॉ करने का मौका नहीं मिलता.

इस समस्या को ठीक करने के दो तरीके हैं.

Asyncify की मदद से इवेंट लूप को अनब्लॉक करना

सबसे पहले, लिंक किए गए लेख में बताए गए तरीके से, Asyncify का इस्तेमाल किया जा सकता है. यह Emscripten की एक सुविधा है, जिसकी मदद से C या C++ प्रोग्राम को "रोका" जा सकता है, इवेंट लूप को फिर से कंट्रोल दिया जा सकता है, और असाइनिमेंट के पूरा होने पर प्रोग्राम को फिर से चालू किया जा सकता है.

इस तरह की एसिंक्रोनस कार्रवाई को "कम से कम समय के लिए रोका जा सकता है". इसे emscripten_sleep(0) एपीआई के ज़रिए दिखाया जाता है. इसे लूप के बीच में जोड़कर, यह पक्का किया जा सकता है कि हर बार दोहराए जाने पर, कंट्रोल ब्राउज़र के इवेंट लूप पर वापस आ जाए. साथ ही, पेज रिस्पॉन्सिव बना रहे और वह किसी भी इवेंट को हैंडल कर सके:

#include <SDL2/SDL.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

int main() {
  SDL_Init(SDL_INIT_VIDEO);

  SDL_Window *window;
  SDL_Renderer *renderer;
  SDL_CreateWindowAndRenderer(300, 300, 0, &window, &renderer);

  SDL_SetRenderDrawColor(renderer, /* RGBA: green */ 0x00, 0x80, 0x00, 0xFF);
  SDL_Rect rect = {.x = 10, .y = 10, .w = 150, .h = 100};
  SDL_RenderFillRect(renderer, &rect);
  SDL_RenderPresent(renderer);

  while (1) {
    SDL_Event event;
    SDL_PollEvent(&event);
    if (event.type == SDL_QUIT) {
      break;
    }
#ifdef __EMSCRIPTEN__
    emscripten_sleep(0);
#endif
  }

  SDL_DestroyRenderer(renderer);
  SDL_DestroyWindow(window);

  SDL_Quit();
}

अब इस कोड को Asyncify की सुविधा चालू करके कंपाइल करना होगा:

emcc example.cpp -o example.html -s USE_SDL=2 -s ASYNCIFY

इसके बाद, ऐप्लिकेशन वेब पर फिर से उम्मीद के मुताबिक काम करता है:

Emscripten से जनरेट किया गया एचटीएमएल पेज, जिसमें काले रंग के स्क्वेयर कैनवस पर हरा रेक्टैंगल दिख रहा है.

हालांकि, Asyncify का इस्तेमाल करने पर, कोड का साइज़ ज़्यादा हो सकता है. अगर इसका इस्तेमाल सिर्फ़ ऐप्लिकेशन में टॉप-लेवल इवेंट लूप के लिए किया जाता है, तो emscripten_set_main_loop फ़ंक्शन का इस्तेमाल करना बेहतर विकल्प हो सकता है.

"main loop" एपीआई की मदद से, इवेंट लूप को अनब्लॉक करना

emscripten_set_main_loop को कॉल स्टैक को अनवाइंड और रीवाइंड करने के लिए, कंपाइलर ट्रांसफ़ॉर्मेशन की ज़रूरत नहीं होती. इससे कोड के साइज़ में होने वाले ओवरहेड से बचा जा सकता है. हालांकि, इसके लिए कोड में मैन्युअल तरीके से ज़्यादा बदलाव करने पड़ते हैं.

सबसे पहले, इवेंट लूप के बॉडी को अलग फ़ंक्शन में निकालना होगा. इसके बाद, emscripten_set_main_loop को पहले आर्ग्युमेंट में कॉलबैक के तौर पर, दूसरे आर्ग्युमेंट में एफ़पीएस (नेटिव रीफ़्रेश इंटरवल के लिए 0) के साथ, और तीसरे आर्ग्युमेंट में एक बूलियन के साथ कॉल किया जाना चाहिए. बूलियन से यह पता चलता है कि अनलिमिटेड लूप (true) को सिम्युलेट करना है या नहीं:

emscripten_set_main_loop(callback, 0, true);

नए बनाए गए कॉलबैक के पास, main फ़ंक्शन में स्टैक वैरिएबल का ऐक्सेस नहीं होगा. इसलिए, window और renderer जैसे वैरिएबल को या तो ढेर में सेट किए गए स्ट्रक्चर में निकाला जाना चाहिए और उसके पॉइंटर को एपीआई के emscripten_set_main_loop_arg वैरिएंट के ज़रिए पास किया जाना चाहिए या ग्लोबल static वैरिएबल में निकाला जाना चाहिए. मैंने आसानी के लिए, बाद वाले विकल्प को चुना है. इस नतीजे को समझना थोड़ा मुश्किल है, लेकिन यह पिछले उदाहरण जैसा ही रेक्टैंगल बनाता है:

#include <SDL2/SDL.h>
#include <stdio.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

SDL_Window *window;
SDL_Renderer *renderer;

bool handle_events() {
  SDL_Event event;
  SDL_PollEvent(&event);
  if (event.type == SDL_QUIT) {
    return false;
  }
  return true;
}

void run_main_loop() {
#ifdef __EMSCRIPTEN__
  emscripten_set_main_loop([]() { handle_events(); }, 0, true);
#else
  while (handle_events())
    ;
#endif
}

int main() {
  SDL_Init(SDL_INIT_VIDEO);

  SDL_CreateWindowAndRenderer(300, 300, 0, &window, &renderer);

  SDL_SetRenderDrawColor(renderer, /* RGBA: green */ 0x00, 0x80, 0x00, 0xFF);
  SDL_Rect rect = {.x = 10, .y = 10, .w = 150, .h = 100};
  SDL_RenderFillRect(renderer, &rect);
  SDL_RenderPresent(renderer);

  run_main_loop();

  SDL_DestroyRenderer(renderer);
  SDL_DestroyWindow(window);

  SDL_Quit();
}

कंट्रोल फ़्लो में किए गए सभी बदलाव मैन्युअल तरीके से किए जाते हैं और सोर्स कोड में दिखते हैं. इसलिए, इसे फिर से Asyncify सुविधा के बिना कंपाइल किया जा सकता है:

emcc example.cpp -o example.html -s USE_SDL=2

यह उदाहरण बेकार लग सकता है, क्योंकि यह पहले वर्शन से अलग नहीं है. पहले वर्शन में, कोड काफ़ी आसान होने के बावजूद, कैनवस पर रेक्टैंगल को सफलतापूर्वक ड्रॉ किया गया था. साथ ही, handle_events फ़ंक्शन में सिर्फ़ SDL_QUIT इवेंट को हैंडल किया गया था. हालांकि, वेब पर इस इवेंट को अनदेखा कर दिया जाता है.

हालांकि, अगर आपको किसी तरह का ऐनिमेशन या इंटरैक्टिविटी जोड़नी है, तो Asyncify या emscripten_set_main_loop के ज़रिए सही इवेंट लूप इंटिग्रेशन का इस्तेमाल करें.

उपयोगकर्ता के इंटरैक्शन मैनेज करना

उदाहरण के लिए, पिछले उदाहरण में कुछ बदलाव करके, कीबोर्ड इवेंट के जवाब में रेक्टैंगल को मूव किया जा सकता है:

#include <SDL2/SDL.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

SDL_Window *window;
SDL_Renderer *renderer;

SDL_Rect rect = {.x = 10, .y = 10, .w = 150, .h = 100};

void redraw() {
  SDL_SetRenderDrawColor(renderer, /* RGBA: black */ 0x00, 0x00, 0x00, 0xFF);
  SDL_RenderClear(renderer);
  SDL_SetRenderDrawColor(renderer, /* RGBA: green */ 0x00, 0x80, 0x00, 0xFF);
  SDL_RenderFillRect(renderer, &rect);
  SDL_RenderPresent(renderer);
}

uint32_t ticksForNextKeyDown = 0;

bool handle_events() {
  SDL_Event event;
  SDL_PollEvent(&event);
  if (event.type == SDL_QUIT) {
    return false;
  }
  if (event.type == SDL_KEYDOWN) {
    uint32_t ticksNow = SDL_GetTicks();
    if (SDL_TICKS_PASSED(ticksNow, ticksForNextKeyDown)) {
      // Throttle keydown events for 10ms.
      ticksForNextKeyDown = ticksNow + 10;
      switch (event.key.keysym.sym) {
        case SDLK_UP:
          rect.y -= 1;
          break;
        case SDLK_DOWN:
          rect.y += 1;
          break;
        case SDLK_RIGHT:
          rect.x += 1;
          break;
        case SDLK_LEFT:
          rect.x -= 1;
          break;
      }
      redraw();
    }
  }
  return true;
}

void run_main_loop() {
#ifdef __EMSCRIPTEN__
  emscripten_set_main_loop([]() { handle_events(); }, 0, true);
#else
  while (handle_events())
    ;
#endif
}

int main() {
  SDL_Init(SDL_INIT_VIDEO);

  SDL_CreateWindowAndRenderer(300, 300, 0, &window, &renderer);

  redraw();
  run_main_loop();

  SDL_DestroyRenderer(renderer);
  SDL_DestroyWindow(window);

  SDL_Quit();
}

SDL2_gfx की मदद से अन्य आकार बनाना

SDL2, अलग-अलग प्लैटफ़ॉर्म के बीच के अंतर और अलग-अलग तरह के मीडिया डिवाइसों को एक ही एपीआई में अलग रखता है. हालांकि, यह अब भी एक बहुत ही लो-लेवल लाइब्रेरी है. खास तौर पर, ग्राफ़िक के लिए यह पॉइंट, लाइनें, और रेक्टैंगल बनाने के लिए एपीआई उपलब्ध कराता है. हालांकि, ज़्यादा जटिल आकार और ट्रांसफ़ॉर्मेशन लागू करने का काम उपयोगकर्ता को करना होता है.

SDL2_gfx एक अलग लाइब्रेरी है, जो इस अंतर को पूरा करती है. उदाहरण के लिए, ऊपर दिए गए उदाहरण में रेक्टैंगल को सर्कल से बदलने के लिए, इसका इस्तेमाल किया जा सकता है:

#include <SDL2/SDL.h>
#include <SDL2/SDL2_gfxPrimitives.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

SDL_Window *window;
SDL_Renderer *renderer;

SDL_Point center = {.x = 100, .y = 100};
const int radius = 100;

void redraw() {
  SDL_SetRenderDrawColor(renderer, /* RGBA: black */ 0x00, 0x00, 0x00, 0xFF);
  SDL_RenderClear(renderer);
  filledCircleRGBA(renderer, center.x, center.y, radius,
                   /* RGBA: green */ 0x00, 0x80, 0x00, 0xFF);
  SDL_RenderPresent(renderer);
}

uint32_t ticksForNextKeyDown = 0;

bool handle_events() {
  SDL_Event event;
  SDL_PollEvent(&event);
  if (event.type == SDL_QUIT) {
    return false;
  }
  if (event.type == SDL_KEYDOWN) {
    uint32_t ticksNow = SDL_GetTicks();
    if (SDL_TICKS_PASSED(ticksNow, ticksForNextKeyDown)) {
      // Throttle keydown events for 10ms.
      ticksForNextKeyDown = ticksNow + 10;
      switch (event.key.keysym.sym) {
        case SDLK_UP:
          center.y -= 1;
          break;
        case SDLK_DOWN:
          center.y += 1;
          break;
        case SDLK_RIGHT:
          center.x += 1;
          break;
        case SDLK_LEFT:
          center.x -= 1;
          break;
      }
      redraw();
    }
  }
  return true;
}

void run_main_loop() {
#ifdef __EMSCRIPTEN__
  emscripten_set_main_loop([]() { handle_events(); }, 0, true);
#else
  while (handle_events())
    ;
#endif
}

int main() {
  SDL_Init(SDL_INIT_VIDEO);

  SDL_CreateWindowAndRenderer(300, 300, 0, &window, &renderer);

  redraw();
  run_main_loop();

  SDL_DestroyRenderer(renderer);
  SDL_DestroyWindow(window);

  SDL_Quit();
}

अब SDL2_gfx लाइब्रेरी को भी ऐप्लिकेशन में लिंक करना होगा. यह SDL2 की तरह ही किया जाता है:

# Native version
$ clang example.cpp -o example -lSDL2 -lSDL2_gfx
# Web version
$ emcc --bind foo.cpp -o foo.html -s USE_SDL=2 -s USE_SDL_GFX=2

Linux पर चलने वाले नतीजे यहां दिए गए हैं:

काले रंग के बैकग्राउंड और हरे रंग के सर्कल वाली स्क्वेयर Linux विंडो.

वेब पर:

Emscripten से जनरेट किया गया एचटीएमएल पेज, जिसमें काले रंग के स्क्वेयर कैनवस पर हरा सर्कल दिख रहा है.

ज़्यादा ग्राफ़िक प्राइमिटिव के लिए, अपने-आप जनरेट हुए दस्तावेज़ देखें.