Stage XL demo Dart je vynikající jazyk a StageXL je vynikající knihovna. Umožní vám nad HTML canvas naprogramovat hru, kterou si pustíte i na Androidu nebo iOS. Pochopitelně v prohlížeči, bez rozšíření a pluginů. Následující článek je v podstatě přepisem mého workshopu z letošního DevFest 2014.

Výstupem tutoriálu je jednoduchá interaktivní blbůstka á la Rorschachův test, kterou si můžete online prohlédnout na we.are.hiring.cz (Chrome prosím, ale ne kvůli StageXL, ale kvůli Polymeru, o tom ovšem jindy).

(Článek původně vyšel na zdrojak.cz)

Co budete potřebovat

Postačí vám DartEditor a základní znalost Dart-u, žádné pokročilé techniky nám nehrozí. Zdrojáky tutoriálu jsou na GitHub, takže si je stáhněte, otevřete v DartEditoru a jdeme na to.

pubspec.yaml

V závislostech vidíte, že toho moc nevidíte:

name: devfest_2014_stageXL
author: +TomasZverina
description: Workshop o StageXL
dependencies:
  browser: any
  stagexl: any

StageXL je poměrně stabilní, takže “any” nám tu nijak zvlášť nevadí, ale pokud se bojíte, specifikujte si verzi přesně.

Krok 0: Inicizalizace StageXL

StageXL je technologie pro web. Obvykle svůj projekt pomocí “pub” kompilujete do JavaScriptu a nasazujete na web server. Celkem logicky je tedy základem html dokument. Kromě běžné inicializace Dart-u pak potřebujete jen:

<canvas id="stage" width="500" height="400" style="border: 1px solid black;"></canvas>

Nad tímhle canvasem bude StageXL operovat. Takhle ho nastartujeme …

var canvas = html.querySelector('#stage');
_stage = new Stage(canvas);
var renderLoop = new RenderLoop();
renderLoop.addStage(_stage);

… a takhle do něj přidáme první objekt:

var shape = new Shape();
shape.graphics.rect(0, 0, 50, 50);
shape.graphics.fillColor(Color.Red);
shape.x = 100;
shape.y = 100;
_stage.addChild(shape);

Asi je jasné co to dělá - na souřadnice 100x100 dej červený čtverec 50x50.

Shape je objekt z knihovny StageXL, který se dědí z DisplayObject. DisplayObject je bázová třída pro všechno co se ve StageXL renderuje a obsahuje všechno, co tak můžete potřebovat pro 2D grafiku:

num x = 0.0; // souřadnice objektu v rodiči
num y = 0.0; // souřadnice objektu v rodiči
num pivotX = 0.0; // referenční bod [0,0], ke kterému vztahuji svoje ...
num pivotY = 0.0; //      souřadnice. Moje "těžiště", střed rotace atd.
num scaleX = 1.0; // natažení v ose X
num scaleY = 1.0; // natažení v ose Y
num skewX = 0.0; // zkosení
num skewY = 0.0;
num rotation = 0.0; // rotace

num alpha = 1.0; // průhlednost (1=plný, 0=zcela průhledný)
bool visible = true; // je viditelný?

Jestli je můj objekt Shape, Bitmap nebo skupina více objektů nastrkaná do jednoho Sprite - to už je jedno.

A rovnou si ukážeme, jak objekty animovat. Náš Shape necháme, aby se během 100 vteřin (konstanta REPEAT) 100x otočil o 380°.

Tween t = new Tween(shape, REPEAT * 1, TransitionFunction.linear);
t.animate.rotation.to(REPEAT * math.PI);
_stage.renderLoop.juggler.add(t);

Animace se dělají tak, že vytvoříte Tween, řeknete co a jak dlouho se má animovat a přidáte Tween do “juggleru” - správce animací.

Když se teď ohlédnete na to málo co jsme zatím vytvořili, zjistíte, že už nám vlastně skoro nic nechybí. Teď už můžeme napsat v podstatě cokoliv:

  • umíme si připravit HTML a zinicializovat prostředí
  • umíme přidávat a pozicovat objekty
  • umíme je animovat

Zatím neumíme pracovat s obrázky (bitmapami) a neumíme přijímat události (myš, touch).

Krok 1: ResourceManager

Stáhnout si obrázky, zvuky apod. zdroje nějakou dobu trvá. Bylo by užitečné mít možnost si vypsat seznam věcí, které budeme potřebovat, zobrazit “laoding” screen a až se zdroje stáhnou, tak abychom mohli náš “stage” spustit. K tomu slouží ResourceManager.

void main() {
  _Rorschach r = new _Rorschach();
  r.initGame();
}

class _Rorschach {

  ResourceManager _resourceManager = new ResourceManager();

  initGame() {
    _resourceManager
      ..addBitmapData("bg", "bg.jpg")
      ..addBitmapData("stain", "stain.png");
    _resourceManager.load().then(_runGame);
  }

  _runGame(_) {
    // muzeme to spustit
  }
}

Vytvoříme instanci ResourceManager, zaregistrujeme zdroje, které budeme potřebovat, a spustíme load(). Load() vrací Future<ResourceManager>, tzn. až bude staženo (then), zavolá se naše metoda _runGame, která ResourceManager bere jako argument. V tomhle případě ale argument ignoruji, ResourceManager mám uložený jako vlastnost mojí třídy _Rorschach.

Stažené bitmapy pak můžeme použít ve stage:

var bgBitmap = new Bitmap(_resourceManager.getBitmapData("bg"));
_stage.addChild(bgBitmap);

Bitmap je opět DisplayObject, takže ji můžeme pozicovat, otáčet, animovat, …

Krok 2: Responzivita

V tomto kroce si upravíme HTML do finální podoby a ukážeme si, jak StageXL nastavit, aby dobře fungovala bez ohledu na velikost obrazovky.

<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
    <script async type="application/dart" src="step01.dart"></script>
    <script async src="packages/browser/dart.js"></script>
    <style type="text/css">
      body { margin: 0; padding: 0; overflow: hidden; }
      #stage { position: absolute; width: 100%; height: 100%; }
    </style>
  </head>
  <body>
    <canvas id="stage"></canvas>
  </body>
</html>

Canvas si nastylujeme na celou obrazovku a meta tagem viewport vysvětlíme prohlížeči, jak má s naším dokumentem zacházet. V tomto případě vytváříme spíš aplikaci než WWW stránku, takže si můžeme dovolit takové neslušnosti jako user-scalable=no.

Teď je potřeba StageXL naučit, jak s dostupným prostorem pracovat.

var bgBitmap = new Bitmap(_resourceManager.getBitmapData("bg"));
var canvas = html.querySelector('#stage');
_stage = new Stage(canvas, width: bgBitmap.bitmapData.width, height: bgBitmap.bitmapData.height);
_stage.scaleMode = StageScaleMode.NO_BORDER;
_stage.align = StageAlign.NONE;

V konstruktoru Stage předáme rozměry, na které ji optimalizujeme - v našem případě šířku a výšku Bitmapy, kterou máme na pozadí.

Režimem StageScaleMode.NO_BORDER jí říkáme: “Vyplň dostupné místo tak, aby kolem tebe nebyl žádný prázdný prostor, ale nedeformuj se”.

Režim StageAlign.NONE zařídí, aby se Stage centrovala uprostřed dostupného viewportu. Pusťte si hotový krok 5 a zahýbejte velikostí okna, myslím, že to bude jasné.

Kromě toho si v kroce připravíme pole skvrn, které budeme později zobrazovat.

for (int a = 0; a < 2 * STAINS_COUNT; a++) {
  Bitmap stain = new Bitmap(stainData);
  stain.visible = false;
  stain.pivotX = stainPx;
  stain.pivotY = stainPy;

  if (a % 2 == 1) {
    // zrcadlove prevracene skrvrny na druhe strane události myši
    stain.scaleX = -1;
  }

  STAINS.add(stain);
  _stage.addChild(stain);
}

Krok 3: Umísťování skvrn na stage

  • html - se nezměnilo
  • dart

Teď už to začne být zajímavější. Napojíme se na stream událostí myši nad stage a budeme kreslit.

int _stainPointer = 0;

...

_stage.onMouseMove.listen(_placeStain);

...

void _placeStain(MouseEvent event) {

  double rotation = _randomGenerator.nextDouble() * PI;
  double x = event.stageX;
  double y = event.stageY;

  STAINS[_stainPointer].x = x;
  STAINS[_stainPointer].y = y;
  STAINS[_stainPointer].rotation = rotation;
  STAINS[_stainPointer].visible = true;

  // a presunem ukazatel na dalsi
  _stainPointer = (_stainPointer + 1) % STAINS_COUNT;

}

Mimochodem - proč vůbec mám nějaké pole těch skvrn a řeším tu složitosti se _stainPointer? Inu tak - rád recykluju. Mohl bych pokaždé vytvořit novou Bitmap a staré Bitmapy zahazovat, ale garbage collector je taky jenom člověk, tak proč ho trápit.

Krok 4: Zrcadlení skvrn

  • html - se nezměnilo
  • dart

V dalším kroce zařídím, aby se skvrny zobrazovali nejen pod myší, ale i zrcadlově na druhé straně “papíru”. To už je jen nudná 2D geometrie, tak se jen lehce mrkněte do zdrojáků, co se změnilo.

Krok 5: Filtr událostí

No a jsme na konci! Teď už nám to dělá co chceme, snad jen že se skvrny objevují zbytečně často. K tomu, abych jejich četnost omezil, použiju API Dart streamů, konkrétně metodu “where”:

_stage.onMouseMove.where(_timeToPlaceFilter).listen(_placeStain);

...

bool _timeToPlaceFilter(Event event) {
  _nowTime = new DateTime.now().millisecondsSinceEpoch;
  return (_nowTime - _lastEventTime > MINIMUM_DELAY);
}

void _placeStain(Event event) {
  ...
  _lastEventTime = _nowTime;
}

Kudy dál

StageXL je velmi silný nástroj. Pokud se vám líbí Dart, není v podstatě o čem přemýšlet. Kdekoliv, kde funguje Dart (resp. Dartem vygenerovaný JavaScript), bude fungovat i StageXL.

Pokračujte pochopitelně na stránkách StageXL. Pravdou je, že dokumentace knihovny není úplně košatá, ale je tam alespoň spousta komentovaných a názorných příkladů, které si můžete nastudovat.

Kromě toho co jsem vám tu ukázal, umí StageXL masky, filtry, WebGL akceleraci, přehrávání zvuků a spoustu dalších libůstek.

P.S.: RIP, Flash. Chybět nám nebudeš, muahahaha!