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

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

इंग्वार स्टेपन्यान
इन्ग्वार स्टेपन्यान

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

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

Embind से कैनवस

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

Embind का इस्तेमाल करने का तरीका समझने के लिए, सबसे पहले MDN से दिए गए इस उदाहरण पर एक नज़र डालें, जिसमें <कैनवस> एलिमेंट मिलता है और इस पर कुछ आकार बनाए होते हैं

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 से जनरेट किए गए 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 बैकएंड के साथ काम करती है.

आयत बनाना

आधिकारिक वेबसाइट के "एसडीएल के बारे में जानकारी" सेक्शन में लिखा है:

Simple DirectMedia लेयर एक क्रॉस-प्लैटफ़ॉर्म डेवलपमेंट लाइब्रेरी है. इसे 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 को उस SDL2 लाइब्रेरी को फ़ेच करने के लिए कहा जाएगा जो पहले से WebAssembly में पहले से कंपाइल की गई है. साथ ही, उसे आपके मुख्य ऐप्लिकेशन से लिंक भी करेगी.

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

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

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

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

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

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

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

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

"मुख्य लूप" एपीआई के साथ इवेंट लूप को अनब्लॉक करना

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

सबसे पहले, इवेंट लूप के मुख्य हिस्से को किसी अलग फ़ंक्शन में निकालना ज़रूरी है. इसके बाद, पहले आर्ग्युमेंट में उस फ़ंक्शन को कॉलबैक के तौर पर, emscripten_set_main_loop को कॉल किया जाना चाहिए. साथ ही, दूसरे आर्ग्युमेंट में FPS (फ़्रेम प्रति सेकंड) (नेटिव रीफ़्रेश इंटरवल के लिए 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 से बनाया गया एचटीएमएल पेज, जो काले रंग के स्क्वेयर कैनवस पर हरे रंग का गोला दिखाता है.

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