Replikace DOOM Screen Melt pomocí JavaScriptu a plátna

Retro hry miluji skoro stejně jako vývoj a čas od času se přistihnu jako závislý na hrách, které jsem nehrál 20 a více let. Tento víkend při načítání DOOM na mém rychlém 486/SX (plná rychlost 66 MHz!) jsem si vzpomněl na úžasný efekt tání obrazovky při přechodu mezi menu a úrovněmi. Když jsem se na to podíval, opravdu jsem neměl ponětí, jak toho bylo dosaženo, takže když jsem viděl, že DOOM je open source, šel jsem přímo ke zdroji a byl jsem překvapen, jak snadné je toho dosáhnout.

Jak tedy přesně efekt funguje? Nejprve musíte logicky rozdělit obrazovku na sloupce, aby bylo možné je nezávisle přesouvat.

Další Každému sloupci je pak potřeba přiřadit hodnotu výšky menší než 0. Začneme tím, že prvnímu sloupci přiřadíme náhodnou hodnotu mezi 0 a -100 a každému sousednímu sloupci je přiřazena náhodná hodnota do 50 od souseda. Máme také stanovený limit pro hodnoty, nikdy nepovolujeme hodnotu vyšší než 0 a nikdy nepovolujeme hodnotu nižší než naše maximální odchylka -100.

Tyto hodnoty nejsou zasazeny do kamene a lze s nimi hrát, ale čím vyšší je odchylka mezi sloupci, tím náhodnější bude efekt. Důvodem udržení hodnot sloupců v určitém rozsahu od jejich sousedů je vytvoření efektu zvlněného kopce. Stejnou metodu lze také použít při vytváření jednoduchého 2D terénu.

Dalším a posledním krokem je snížit sloupce, aby se odkryl obraz za nimi. "Kouzlo" efektu taveniny je znázorněno níže. To by také mělo objasnit, proč musíme pro začátek přiřadit záporné hodnoty.

Implementace

Když jsem implementoval efekt, vyzkoušel jsem dva různé přístupy přímé manipulace s pixely pomocí getImageData a putImageData a pomocí standardního drawImage s offsety. Přístup drawImage byl mnohem rychlejší a metoda, kterou vysvětlím.

Pro efekt použijeme dva obrázky, první obrázek je pozadí a bude se kreslit jako první při každém fajfku, 2. obrázek pak vykreslíme ve sloupcích s posunutím polohy y každého sloupce o jeho hodnotu zvyšující hodnotu pokaždé, když doMelt() funkce se volá, dokud hodnoty všech sloupců nejsou větší než výška obrázku.

HTML

Potřebný html je velmi minimální vše, co potřebujeme, je prvek canvas

<canvas id="canvas"></canvas>

JavaScript

Pro efekt tání vytvoříme v paměti prvek plátna, kam nakreslíme sloupce odsazení, obrázek1 a obrázek2 budou obsahovat odkazy na objekty obrázků vytvořené v rámci js, bgImage a meltImage se používají k přepínání mezi tím, jaký obrázek je pozadí a jaký obrázek taje.

var meltCan = document.createElement("canvas"),
meltCtx = meltCan.getContext("2d"),
images = [image1, image2],
bgImage = 1,
meltImage = 0,

Následující nastavení určují, jak bude výsledný efekt vypadat. colSize řídí šířku sloupců, maxDev řídí nejvyšší, kam může sloupec jít, maxDiff řídí maximální rozdíl v hodnotě mezi sousedními sloupci a fallSpeed ​​řídí, jak rychle sloupce padají.

settings = {
colSize: 2,
maxDev: 100,
maxDiff: 50,
fallSpeed: 6,
}

Funkce init() je místo, kde nastavujeme počáteční hodnoty našich sloupců a kreslíme obrázek, který se chystáme roztavit, na naše dočasné plátno. První prvek nastavíme na náhodné číslo, které spadá mezi 0 a maxDev, a poté pro každý sousední sloupec vybereme náhodnou hodnotu, která je v rámci nastaveného maxDiff rozsahu.

function init() {
	meltCtx.drawImage(images[meltImage],0,0);

	for (var x = 0; x < columns; x++) {
		if (x === 0) {
			y[x] = -Math.floor(Math.random() * settings.maxDev);
		} else {
			y[x] = y[x - 1] + (Math.floor(Math.random() * settings.maxDiff) - settings.maxDiff / 2);
		}

		if (y[x] > 0) {
			y[x] = 0;
		} else if (y[x] < -settings.maxDev) {
			y[x] = -settings.maxDev;
		}
	}
}

doMelt() funkce je místo, kde se děje kouzlo. Nejprve nakreslíme náš obrázek, který je za roztaveným obrázkem, na plátno, dalším přístupem je umístit prvek plátna před obrázek a použít clearRect k vyčištění plátna. V tomto příkladu však nakreslíme oba obrázky na stejné plátno. Dále iterujeme přes sloupce a zvyšujeme jejich hodnotu pádovou rychlostí. Pokud hodnota není větší než 0, znamená to, že uživatel ještě nevidí účinek, takže pozice y sloupců (yPos) zůstane na 0. Pokud je hodnota sloupce větší než 0, pozice y sloupců se nastaví na hodnotu sloupců . Poté použijeme drawImage k nakreslení sloupce z dočasného plátna na primární plátno pomocí posunutí jeho y o yPos.

Příznak hotovo zůstane pravdivý, pokud jsou hodnoty sloupců větší než výška, a my zaměníme obrázky, abychom to udělali znovu.

function doMelt() {
    ctx.drawImage(images[bgImage],0,0);
    done = true;
    
    for (col = 0; col < columns; col++) {
        y[col] += settings.fallSpeed;

        if (y[col] < 0 ) {
            done = false;
            yPos = 0;
        }else if(y[col] < height){
            done = false;
            yPos = y[col];
        }   
        
        ctx.drawImage(meltCan, col * settings.colSize, 0, settings.colSize, height, col * settings.colSize, yPos, settings.colSize, height); 
    }
    
    if(done){
        var swap = meltImage;
        meltImage = bgImage;
        bgImage = swap;
        init();
    }
    requestAnimationFrame(domelt);
}

Dokončený kód a efekt lze vidět na CodePen:http://codepen.io/loktar00/details/vuiHw.

Pokud jste zvědaví, jak strůjci DOOM implementovali efekt, můžete se na to podívat na https://github.com/id-Software/DOOM/blob/master/linuxdoom-1.10/f_wipe.c