Vytváření Squid Games Dalgona pomocí plátna

Chvíli jsem front-end webový vývojář, a přesto jsem prvek plátna HTML5 použil pouze 1–2krát, a to pouze ve svém volném čase. Wes Bos má ve svém kurzu JavaScript pro začátečníky hru Etch-a-Sketch využívající plátno, což, pokud jste plátno nikdy předtím nepoužívali jako já, je pěkným úvodem do skvělých věcí, které s ním můžete dělat.

Po zhlédnutí Squid Game na Netflixu jsem začal přemýšlet o tom, jestli bych mohl některou z těch her znovu vytvořit v prohlížeči.

Zobrazit na Github

Jasnou volbou se stala Dalgona na základě toho, co jsem si pamatoval o plátně a schopnosti kreslit volnou rukou, což by uživateli umožnilo nakreslit tvar - podobně jako v show, kde hráči musí pečlivě a dokonale vystřihnout tvar cukroví. Ale nejen, že by uživatel potřeboval nakreslit tvar, tvar by musel být předem načten, uživatel by ho musel obkreslit, aby se pokusil najít shodu, a na samém konci musel existovat způsob, jak tyto dva porovnat a určit zda byli blízko.

V tuto chvíli jsem netušil, kde začít, ale rychlé hledání „sledovacích her na plátně“ vedlo k tomuto příkladu na nose s názvem Letterpaint, což je hra, kde uživatel musí vyplnit písmeno co nejblíže možné.

Tento projekt nebyl nejlepší nápad pro začátečníka s plátnem. Dal jsem si za cíl jednou týdně vytvořit blogový příspěvek Codepen nebo Dev.to, ale jakmile jsem s tímto projektem začal, všechno se zastavilo. Celé dva víkendy jsem se snažil přijít na to, jak nakreslit deštník - ne ledajaký - měl být ten z pořadu kvůli přesnosti.

To, co začalo jako zábavný nápad, se stalo frustrujícím a několikrát jsem přemýšlel o tom, že to vzdám. Zajímalo by mě, jestli je to nejlepší způsob, jak využít čas na kódování o víkendech? Ale zvědavost nakonec zvítězila a kód jsem zprovoznil - není nejhezčí a je třeba ho předělat - ale cítil jsem velký pocit úspěchu, když jsem ho uvedl do provozu. A svým způsobem to bylo upřímné. Kódování je těžké a ne vždy se můžete „naučit HTML za den“. Takže si projdu nejen to, jak tato hra funguje, ale i mé problémy a řešení problémů, kterými jsem musel projít, abych to dokončil.

  • Nastavte plátno
  • Nakreslete tvary
    • Trojúhelník
    • Kruh
    • Hvězda
    • Deštník
  • Nastavte funkci uživatelského malování
  • Porovnejte vstup uživatele s tvarem
  • Určení stavu vítězství
  • Resetovat vše
  • Změnit velikost všeho
  • Testování na mobilu
  • Závěr

Nastavení plátna

Toto je standardní kód, kdykoli používáte plátno. Budete chtít nastavit kontext kreslení, šířku a výšku a také styl čáry.

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

/* Set up the size and line styles of the canvas */
function setupCanvas() {
   canvas.height = 370;
   canvas.width = 370;
   canvas.style.width = `${canvas.width}px`;
   canvas.style.height = `${canvas.height}px`;
   ctx.lineWidth = 12;
   ctx.lineCap = 'round';
}

Nakreslete tvary

To je místo, kde se nováček na plátně stal obrovskou překážkou. Nikdy jsem nezkoušel kreslit žádné tvary ani pomocí SVG, ani pomocí plátna, takže pokusit se prokousat se surovou silou tím vším byla docela výzva.

Trojúhelník

Toto byl první tvar, o který jsem se pokusil, a hlavní boj, který jsem zde měl, byl ve skutečnosti způsoben spíše geometrií než kódováním. Pokud se pokoušíte nakreslit mnohoúhelník, je to velmi jednoduché. Nastavíte počáteční bod skládající se ze souřadnic x a y, poté řeknete plátnu, aby nakreslilo čáru k jiné sadě souřadnic atd., takže dohromady tvoří 3 samostatné souřadnice a vytvoří trojúhelník.

Zpočátku jsem se snažil udělat z toho přesný rovnostranný trojúhelník, ale než abych se snažil hledat geometrické vzorce, rozhodl jsem se jen ručně otestovat souřadnice a rozhodl jsem se pro to, co vypadalo „správně“, aniž bych se staral o to, aby to bylo dokonalé.

/* Triangle shape */
function drawTriangle() {
   ctx.strokeStyle = 'rgb(66, 10, 0)';
   ctx.beginPath();
   ctx.moveTo(185, 85);
   ctx.lineTo(285, 260);
   ctx.lineTo(85, 260);
   ctx.closePath();
   ctx.stroke();
}

Kruh

Kruhy se ve skutečnosti kreslí docela snadno. Pomocí vestavěného arc() stačí zadat střed kruhu a poté přidat další parametr pro poloměr. Poslední dva parametry budou vždy stejné, pokud tvoříte úplný kruh.

function drawCircle() {
   ctx.strokeStyle = 'rgb(66, 10, 0)';
   ctx.beginPath();
   ctx.arc(185, 185, 100, 0 * Math.PI, 2 * Math.PI);
   ctx.closePath();
   ctx.stroke();
}

Hvězda

Krátce jsem to zkusil nakreslit jako trojúhelník nastavením ručních souřadnic, ale pak jsem to vzdal a zjistil jsem, že někdo zakódoval dynamickou funkci speciálně pro kreslení hvězd, kde lze určit počet bodů. (Miluji open source).

function drawStar() {
   ctx.strokeStyle = 'rgb(66, 10, 0)';

   let rot = Math.PI / 2 * 3;
   let x = 185;
   let y = 185;
   let cx = 185;
   let cy = 185;
   const spikes = 5;
   const outerRadius = 120;
   const innerRadius = 60;
   const step = Math.PI / 5;

   ctx.strokeSyle = "#000";
   ctx.beginPath();
   ctx.moveTo(cx, cy - outerRadius)
   for (i = 0; i < spikes; i++) {
       x = cx + Math.cos(rot) * outerRadius;
       y = cy + Math.sin(rot) * outerRadius;
       ctx.lineTo(x, y)
       rot += step

       x = cx + Math.cos(rot) * innerRadius;
       y = cy + Math.sin(rot) * innerRadius;
       ctx.lineTo(x, y)
       rot += step
   }
   ctx.lineTo(cx, cy - outerRadius)
   ctx.closePath();
   ctx.stroke();
}

Deštník

Oh Gi-Hun, cítím tvou bolest. Šel jsem na to mnoha různými způsoby. Stáhl jsem si vektorový software s otevřeným zdrojovým kódem, abych se pokusil ručně nakreslit deštník a poté jej importovat jako obrázek SVG na plátno, ale nemohl jsem přijít na to, jak správně kreslit křivky, a naučit se program nakreslit jeden tvar v této hře mi připadalo přehnané. .

Prošel jsem mnoha pokusy nakreslit to ručně jako trojúhelník, ale lineTo() funguje pro polygony a ne pro křivky. Pak jsem měl zjevení, že již existuje metoda kreslení křivek - arc() metoda. Nebyl deštník pouze souborem několika různě velkých křivek a rovných čar – obojí jsem už udělal? Poplácal jsem se po zádech, že jsem na to přišel.

...Bohužel v praxi to nebylo tak jednoduché. První oblouk - hlavní celkový slunečník byl dostatečně snadný, musel jsem mírně upravit arc() tak, aby to byl půlkruh namísto úplného kruhu, a poté změňte výchozí směr. Ale jakmile jsem začal přidávat další oblouky, všechny následující začaly uzavírat cestu pod obloukem napůl rovnou vodorovnou čarou:

ctx.beginPath();
// Umbrella parasol
ctx.arc(200, 180, 120, 0*Math.PI, 1 * Math.PI, true); 
// Umbrella curves
ctx.moveTo(105, 180);
ctx.arc(105, 180, 25, 0*Math.PI, 1 * Math.PI, true);

Nemohl jsem na to přijít. Pokud jsem odstranil první oblouk slunečníku, tato vodorovná čára zmizela na druhém oblouku, ale pokud bych přidal další, problém by se opakoval. Prošel jsem procesem pokus-omyl s beginPath() a stroke() a konečně to KONEČNĚ zprovoznilo vytvořením samostatné podfunkce pro všechny jednotlivé oblouky:

/* Draw individual arcs */
function drawArc(x, y, radius, start, end, counterClockwise = true) {
   ctx.beginPath();
   ctx.arc(x, y, radius, start * Math.PI, end * Math.PI, counterClockwise);
   ctx.stroke();
}

Proč to fungovalo oproti původní funkci? Upřímně, nemám tušení. Možná moveTo() způsobil, že kreslil čáry. V tuto chvíli jsem to nechal tak, jak je, a řekl jsem si, že to nebudu upravovat, jinak riskovat, že to celé znovu rozbiju. Okamžitě jsem provedl změny na Github a cítil jsem neuvěřitelnou radost, že se mi to podařilo. Neuvěřitelná radost z toho, jak nakreslit deštník. Někdy jsou to maličkosti.

/* Umbrella Shape */
function drawUmbrella() {
   ctx.strokeStyle = 'rgb(66, 10, 0)';

   /* Draw individual arcs */
   drawArc(185, 165, 120, 0, 1); // large parasol
   drawArc(93, 165, 26, 0, 1);
   drawArc(146, 165, 26, 0, 1);
   drawArc(228, 165, 26, 0, 1);
   drawArc(279, 165, 26, 0, 1);

   /* Draw handle */
   ctx.moveTo(172, 165);
   ctx.lineTo(172, 285);
   ctx.stroke();
   drawArc(222, 285, 50, 0, 1, false);
   drawArc(256, 285, 16, 0, 1);
   drawArc(221, 286, 19, 0, 1, false);
   ctx.moveTo(202, 285);
   ctx.lineTo(202, 169);
   ctx.stroke();
}

Nastavení funkce uživatelského malování

Je zde několik věcí, které to komplikují, než kdybyste chtěli nechat uživatele, aby maloval na plátno cokoliv. Aby malba byla souvislá čára a nebyla flekatá jako výchozí chování plátna, musíme se napojit na předchozí souřadnice x a y uživatele.

function paint(x, y) {
  ctx.strokeStyle = 'rgb(247, 226, 135)';
  ctx.beginPath();
  /* Draw a continuous line */
  if (prevX > 0 && prevY > 0) {
    ctx.moveTo(prevX, prevY);
  }
  ctx.lineTo(x, y);
  ctx.stroke();
  ctx.closePath();
  prevX = x;
  prevY = y;
}

Některé další funkce, které zde nejsou podrobně popsány:uživatel by měl kreslit pouze se stisknutou myší, aby měl větší kontrolu nad řezáním tvaru, a neměl by automaticky malovat při přesunu kurzoru na výkres. Aby to bylo ještě obtížnější, uživatel se smí pokusit pouze o jeden souvislý pohyb – jakmile uživatel pustí myš, spustí se konec hry. Musí tedy dokončit trasování jedním souvislým pohybem.

Porovnejte uživatelský vstup s tvarem podle barvy

Nyní máme tvary pro bonbóny a uživatel může kreslit přes tvar, ale jak zjistíme, zda uživatel přesně obkreslil tvar? První věc, na kterou jsem myslel, bylo nějak zjistit souřadnice každého pixelu ve výkresu a poté porovnat se souřadnicemi tvaru, který uživatel obkreslil. Zde se znovu objevila logika hry Letterpaint, aby bylo vše mnohem jednodušší.

Všechny tvary používají stejnou barvu a uživatel maluje jinou barvu. Takže co místo toho, abychom se pokusili porovnat souřadnice, porovnali jsme pouze počet pixelů každé z barev? Pokud se uživateli podařilo dokonale obkreslit tvar, pak se počet namalovaných pixelů bude rovnat počtu pixelů tvaru, a tedy roven 1. Pokud uživatel dokonale vykreslí pouze polovinu tvaru, pak bude poměr 50 %. K tomu máme funkci, která získává data pixelů pomocí metody getImageData) který vrací objekt obsahující data pixelů.

function getPixelColor(x, y) {
   const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
   let index = ((y * (pixels.width * 4)) + (x * 4));
   return {
      r:pixels.data[index],
      g:pixels.data[index + 1],
      b:pixels.data[index + 2],
      a:pixels.data[index + 3]
   };
}

Takže pro každou funkci, která kreslí tvar, bude muset zavolat funkci, aby získala počet pixelů:

function drawCircle() {
   /* Draw circle code... */

   /* Get pixels of shape */
   pixelsShape = getPixelAmount(66, 10, 0);
}

Ale počkejte chvíli, znamená to, že uživatel může nakreslit přesně stejný tvar, aniž by se skutečně snažil vysledovat? Nebo by mohl uživatel jen klikatit shlukem pixelů, které je stejné jako na výkresu? Ano, abychom zabránili tomu, že ve skutečnosti musíme přidat kontrolu funkce malování, abychom se ujistili, že uživatel příliš nevybočuje z tvaru:

let color = getPixelColor(x, y);
if (color.r === 0 && color.g === 0 && color.b === 0) {
  score.textContent = `FAILURE - You broke the shape`;
  brokeShape = true;
} 

Znovu kontrolujeme pixely a pokud jsou r, g a b 0 (uživatel maluje na část plátna, na které není nic), pak hru automaticky zklamali. Konec okamžité hry stejně jako show.

Je v tom nějaká drobná chyba, na kterou jsem nebyl schopen přijít. Při kreslení jsem odhlásil hodnoty r, g a b do konzoly a ve vzácných případech místo r rovného 66 (barva tvaru) vrátilo 65 nebo jiné velmi malé odchylky. Skutečné množství pixelů každé z barev tedy pravděpodobně nebude 100% přesné.

Určení stavu vítězství

Porovnáváme pixely mezi kresbami a uživatelskou malbou a pouze kontrolujeme, zda uživatel již tvar neporušil, a pokud dosáhne určitého procenta, pak vyhrává.

function evaluatePixels() {
   if (!brokeShape) {
      const pixelsTrace = getPixelAmount(247, 226, 135);
      let pixelDifference = pixelsTrace / pixelsShape;
      /* User has scored at last 50% */
      if (pixelDifference >= 0.75 && pixelDifference <= 1) {
         score.textContent = `SUCCESS - You scored ${Math.round(pixelDifference * 100)}%`;
      } else {
         score.textContent = `FAILURE - You cut ${Math.round(pixelDifference * 100)}%`;
      }
   }
}

Resetovat vše

Je zde spousta malých funkcí. V podstatě chceme vyčistit vše při restartování her:vymažte tvar, vymažte všechny předchozí souřadnice x a y, vymažte výsledky, vymažte všechna uložená data pixelů a resetujte všechny stavy hry.

function clearCanvas() {
   ctx.clearRect(0, 0, canvas.width, canvas.height);
   gameStart.classList.remove('hidden');
   mouseDown = false;
   startedTurn = false;
   brokeShape = false;
   score.textContent = '';
   prevX = '';
   prevY = '';
   pixelsShape = 0;
}

Změnit velikost všeho

Zde je základní pravidlo vývoje webu. Před kódováním se ujistěte, že víte, na jakých velikostech obrazovky váš web běží. Původně jsem nastavil velikost plátna pro testování, abych se ujistil, že dokážu nakreslit trojúhelník. Pak jsem si uvědomil, že tato hra má přinejmenším stejný smysl jak na smartphonu, tak na stolním počítači, a její velikost byla změněna na 400 pixelů, takže byla viditelná na mém Pixelu. Co si tedy myslíte, že se stalo se všemi mými funkcemi kreslení? Měly úplně špatnou velikost a/nebo už nebyly vycentrované, takže jsem se musel vrátit a upravit souřadnice pro všechny. Naštěstí jsem ještě nepřišel na funkci tažení deštníku.

...Dokud jsem si neuvědomil, že bych měl ještě podruhé změnit velikost plátna, protože některé z předchozích iPhonů mají rozlišení menší než 400 pixelů, takže konečná velikost plátna byla 370 pixelů. Naštěstí pro deštník to byla jednoduchá záležitost úpravy pixelů a souřadnic a zohlednění upravených průměrů.

Testování na mobilu

Jedna poslední drobná vráska, kterou jsem se právě chystal publikovat:NA MOBILU TO NEFUNGOVALO . Testoval jsem v prohlížeči pomocí mobilního emulátoru a musel jsem vypnout "přetažením posouvat" a myslel jsem... počkejte chvilku. Pak jsem to po publikování na Github skutečně otestoval a ano, na dotykových zařízeních to po vybalení nefunguje, protože dotykem obrazovky se posouvá prohlížeč namísto kreslení na skutečné plátno.

Na pomoc opět přišel něčí tutoriál. V podstatě musíme namapovat každou obsluhu události myši na její dotykový ekvivalent, A zabránit posouvání obrazovky, když se jedná o dotykovou obrazovku. To znamenalo, že jsem musel přesunout pokyny zespodu plátna do vyskakovacího okna pro výběr počátečního tvaru (aby bylo posouvání na mobilu zbytečné) a musel jsem zvětšit šířku čáry plátna z 12 na 15, protože mi to na mobilu připadalo trochu PŘÍLIŠ tenké. . Také „rozbití tvaru“ je na mobilu nějak neúmyslně mnohem velkorysejší, což znamená, že uživatel může mnohem více malovat mimo tvar, takže to znamenalo přidat kontrolu ověření, aby uživatel selhal, pokud dosáhne také více než 100 % . V tuto chvíli jsem cítil, že je čas nechat ostatní lidi, aby si s tím začali hrát.

Závěr

I když tato zkušenost byla občas frustrující, tento projekt je příkladem toho, proč miluji vývoj webu. Můžete vzít reprezentaci designu, nápadu, konceptu a udělat z něj něco interaktivního v prohlížeči, s nímž si může hrát každý. Důležitou částí je vymyslet, jak něco uvést do provozu; kód lze poté vždy vyčistit. Až budu mít více zkušeností s plátnem, bude zábavné se vrátit a věci v tomto projektu vylepšit.