Canvas alapok — az első vonal
A Canvas egy HTML elem, amire JavaScript-tel rajzolunk. Minden játékunk erre a "rajztáblára" rajzolja a pályát, a karaktereket és az ellenségeket — képkockáról képkockára.
1A canvas létrehozása és a kontextus
A canvas maga csak egy üres téglalapot jelent a HTML-ben. Rajzolni csak a 2D kontextus (ctx) segítségével lehet — ez az a "ceruza", amivel dolgozunk.
<!-- HTML-ben: a canvas elem --> <canvas id="gc" width="540" height="580"></canvas> // JavaScript-ben: megszerezzük a rajzolási eszközt const CV = document.getElementById('gc'); const ctx = CV.getContext('2d'); // Szélességet és magasságot érdemes változóba tenni const W = CV.width; // 540 const H = CV.height; // 580
Az első rajzok — téglalapok
// Kitöltött téglalap: fillRect(x, y, szélesség, magasság) ctx.fillStyle = '#4aaa22'; // szín beállítás ctx.fillRect(0, 0, W, H); // egész canvas zöldre töltve // Keret téglalap: strokeRect ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.strokeRect(10, 10, 100, 60); // Töröl egy területet (átlátszóvá teszi) ctx.clearRect(0, 0, W, H); // minden képkocka elején ezt hívjuk
A canvas bal felső sarka a (0, 0) pont. X jobbra nő, Y lefelé nő — ez fordítva van a matematikai koordinátarendszerhez képest! Tehát a (0, 580) pont a bal alsó sarok.
3 példa a játékainkból
A pálya háttere és az út rajzolása fillRect-tel:
// Sötét háttér az egész canvasra ctx.fillStyle = '#1a1208'; ctx.fillRect(0, UIH, W, GH); // Az út (path) szürke téglalapként ctx.fillStyle = '#3a3028'; ctx.fillRect(PATH_X, UIH, PATH_W, GH); // Kastély alapterülete ctx.fillStyle = '#2a1808'; ctx.fillRect(0, UIH, 80, GH);
A pálya sorok (road, river, grass) kirajzolása:
// Minden sorra (row) egy fillRect for (let r = 0; r < ROWS; r++) { const y = rowY(r); // sor Y pozíciója ctx.fillStyle = ROW_COLORS[r]; // sor színe ctx.fillRect(0, y, W, ROW_H); } // Oldalsó füves terület ctx.fillStyle = '#2d7a10'; ctx.fillRect(0, 0, TRACK_LEFT, H);
A pályaszéli fák tövének rajzolása (trunk):
// Fa törzse — barna téglalap ctx.strokeStyle = '#8B6040'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(0, 10); ctx.lineTo(0, -18); ctx.stroke(); // Pályaszél zöld sáv (oldalsó fű) ctx.fillStyle = '#2a6a10'; ctx.fillRect(0, 0, TL, H); ctx.fillRect(TR, 0, W-TR, H);
Hozz létre egy HTML fájlt canvas-szal és rajzolj rá:
- Zöld háttér az egész canvasra (fillRect)
- Szürke út a közepén (fillRect, vízszintesen húzódó)
- Fehér keret körülötte (strokeRect)
- Töröld az út felső harmadát (clearRect) — mit látsz?
🧠 Mi a canvas koordinátarendszerének origója (0,0)?
Körök, ívek, vonalak
A teknős páncélja, a tojások, a kövek, a sirályok — mind körökből és ívekből épülnek fel. A Path (útvonal) rendszerrel bármilyen alakzatot megrajzolhatunk.
1A Path rendszer
A körök és összetett alakzatok rajzolásához először egy útvonalat (path) írunk le, majd azt töltjük ki vagy húzzuk körül.
// Kör: arc(x, y, sugár, kezdőszög, végszög) ctx.beginPath(); // mindig ezzel kezdjük ctx.arc(100, 100, 30, 0, Math.PI * 2); // teljes kör ctx.fillStyle = '#3ddc84'; ctx.fill(); // kitölti ctx.stroke(); // keretet rajzol // Ellipszis: ellipse(x, y, rx, ry, szög, start, end) ctx.beginPath(); ctx.ellipse(200, 100, 40, 25, 0, 0, Math.PI*2); ctx.fill(); // Vonal: moveTo + lineTo ctx.beginPath(); ctx.moveTo(0, 50); // toll felemelése és odavitele ctx.lineTo(200, 50); // vonal húzása ide ctx.stroke(); // Zárt alakzat: closePath ctx.beginPath(); ctx.moveTo(100, 0); ctx.lineTo(200, 100); ctx.lineTo(0, 100); ctx.closePath(); // visszaköt az elejére → háromszög ctx.fill();
3 példa a játékainkból
Lövedék és ellenség körök rajzolása:
// Ellenség: kitöltött kör + keret ctx.beginPath(); ctx.arc(e.x, e.y, e.r, 0, Math.PI*2); ctx.fillStyle = e.col; ctx.fill(); ctx.strokeStyle = 'rgba(0,0,0,.3)'; ctx.lineWidth = 2; ctx.stroke(); // Hős lövedéke: kis fényes kör ctx.beginPath(); ctx.arc(p.x, p.y, 4, 0, Math.PI*2); ctx.fillStyle = '#00d4ff'; ctx.fill();
Tojás rajzolása ellipszisként:
// Tojás = megnyújtott ellipszis const g = ctx.createRadialGradient( x-2, y-3, 1, x, y, 9 ); g.addColorStop(0, '#fffef5'); g.addColorStop(1, '#e8d080'); ctx.fillStyle = g; ctx.beginPath(); ctx.ellipse(x, y, 7, 9, 0, 0, Math.PI*2); ctx.fill();
Kő szabálytalan alakzata moveTo + lineTo-val:
// Kő = sokszög (nem kör, mert sziklás) ctx.beginPath(); ctx.moveTo(-9, 6); ctx.lineTo(-12, -2); ctx.lineTo(-6, -10); ctx.lineTo(5, -12); ctx.lineTo(13, -4); ctx.lineTo(11, 6); ctx.closePath(); // visszazár ctx.fill();
Ha nem hívod, az előző útvonal is benne marad. Pl. egy kört rajzolsz, majd egy másikat — a régi is újra kitöltődik, dupla keret jelenik meg. Mindig beginPath()-szel kezdj új alakzatot!
Rajzolj egyszerű figurákat a canvasra:
- Rajzolj egy teljes kört (arc, 0-tól Math.PI*2-ig)
- Rajzolj egy félkört (arc, 0-tól Math.PI-ig)
- Rajzolj egy tojást (ellipse, rx≠ry)
- Rajzolj egy háromszöget (moveTo + lineTo + lineTo + closePath + fill)
- Bónusz: a Froggy Rush tojás kódja alapján rajzolj radiális gradiens tojást
🧠 Mi történik ha nem hívjuk a beginPath()-t két arc() rajzolás között?
Színek, átlátszóság, gradiensek
A teknős páncéljának mélysége, a kastély árnyékai, a tojás fényes felülete — mind gradienssel készül. Ez adja a vizuális mélységet a játékainknak.
1Szín megadási módok
// Szín szöveggel (CSS színek) ctx.fillStyle = 'red'; ctx.fillStyle = '#4aaa22'; // hex ctx.fillStyle = '#4aaa22aa'; // hex + alpha // rgba — piros, zöld, kék + átlátszóság (0=átlátszó, 1=teli) ctx.fillStyle = 'rgba(255, 100, 0, 0.6)'; // 60% narancssárga // Lineáris gradiens const g = ctx.createLinearGradient(x1, y1, x2, y2); g.addColorStop(0, '#ff0000'); // piros az elején g.addColorStop(0.5, '#ffff00'); // sárga a közepén g.addColorStop(1, '#00ff00'); // zöld a végén ctx.fillStyle = g; // Radiális gradiens (belülről kifelé) const rg = ctx.createRadialGradient(cx, cy, r1, cx, cy, r2); rg.addColorStop(0, '#ffffff'); // középen fehér rg.addColorStop(1, '#1a5a10'); // szélén sötét ctx.fillStyle = rg; // Globális átlátszóság — mindent érint ctx.globalAlpha = 0.5; // 50% átlátszó minden rajzolás ctx.fillRect(0, 0, W, H); ctx.globalAlpha = 1; // visszaállítás!
3 példa a játékainkból
HP sáv gradiens — zöldtől pirosig:
// HP szín az aktuális érték alapján const pct = G.castle.hp / G.castle.maxHp; let hpCol; if (pct > 0.6) hpCol = '#3a8a3a'; else if (pct > 0.3) hpCol = '#9a8a20'; else hpCol = '#8a2020'; // Sáv háttér ctx.fillStyle = 'rgba(0,0,0,.5)'; ctx.fillRect(bx, by, bw, bh); // Kitöltött rész ctx.fillStyle = hpCol; ctx.fillRect(bx, by, bw*pct, bh);
Teknős páncél radiális gradienssel:
// Radiális gradiens: belül világos, kívül sötét const g2 = ctx.createRadialGradient( -3, -4, 2, // belső kör: eltolt (fény hatás) 0, 1, 14 // külső kör ); g2.addColorStop(0, '#3a9a28'); // belül világos zöld g2.addColorStop(1, '#1a5a10'); // kívül sötét zöld ctx.fillStyle = g2; ctx.ellipse(0, 1, 14, 11, 0, 0, Math.PI*2); ctx.fill();
Boost pad pulzáló animáció globalAlpha-val:
// frame alapján 0-1 között oszcillál const pulse = (Math.sin(frame * .1) + 1) * .5; // globalAlpha: pulzáló átlátszóság ctx.globalAlpha = 0.75 + pulse * .25; ctx.fillStyle = `rgba(255,180,0,${.35+pulse*.3})`; ctx.beginPath(); ctx.arc(0, 0, 13, 0, Math.PI*2); ctx.fill(); ctx.globalAlpha = 1; // MINDIG visszaállítani!
Ha beállítod a globalAlpha-t és elfelejtesz visszaállítani 1-re, az összes utána rajzolt elem is átlátszó lesz! Mindig add hozzá: ctx.globalAlpha = 1; a végére.
Kísérletezz a színekkel és gradiensekkel:
- Rajzolj 3 egymást fedő kört különböző rgba átlátszósággal (0.2, 0.5, 0.9)
- Készíts lineáris gradienst balról jobbra (kék → zöld) és tölts ki vele egy téglalapot
- Készíts radiális gradienst és rajzolj vele egy "fényes golyót"
- Rajzolj egy HP sávot: háttér (szürke), kitöltött rész (zöld), és csökkentsd 50%-ra
🧠 Mit jelent az rgba(255, 0, 0, 0.5)?
A game loop — requestAnimationFrame
Minden játék szíve a game loop. Ez fut 60-szor másodpercenként, frissíti a játékállapotot és újrarajzolja a képet. Nélküle a játék csak egy pillanatfelvétel lenne.
1A loop szerkezete
// A teljes játék állapota let x = 100; // kör vízszintes helyzete let vx = 2; // sebesség (pixel/frame) // 1. UPDATE — frissíti a pozíciókat, fizikát function update() { x += vx; // elmozdul if (x > W || x < 0) vx *= -1; // fal visszapattan } // 2. DRAW — kirajzolja az aktuális állapotot function draw() { ctx.clearRect(0, 0, W, H); // töröl ctx.beginPath(); ctx.arc(x, 100, 20, 0, Math.PI*2); ctx.fillStyle = '#3ddc84'; ctx.fill(); } // 3. LOOP — meghívja magát újra és újra function loop() { update(); draw(); requestAnimationFrame(loop); // ~60x/másodperc } loop(); // elindítja
setInterval fix időközönként fut, akkor is ha a tab a háttérben van — pazarolja az energiát. A requestAnimationFrame csak akkor fut, ha a böngésző éppen rajzol — automatikusan szünetet tart háttérben, és szinkronizál a monitor frissítési sebességével (általában 60fps).
3 példa a játékainkból
A Castle Siege teljes loop-ja:
function loop() { update(); // mozgatja a hőst, ellenségeket draw(); // kirajzol mindent requestAnimationFrame(loop); } function update() { // Csak ha játékban vagyunk if (!G || G.phase !== 'play') return; G.frame++; moveHero(G.hero); moveEnemies(G.enemies); moveProjectiles(G.hProj); checkCollisions(); }
Frame számláló és fázis-alapú update:
function update() { // Mindig fut (loop-ban hívják) if (!G) return; G.frame++; // 0, 1, 2, 3... folyamatosan nő if (G.phase === 'play') updatePlay(); if (G.phase === 'dead') updateDead(); if (G.phase === 'levelup') updateLevel(); } function loop() { update(); draw(); requestAnimationFrame(loop); } loop();
A frame szám szinusz-animációhoz való felhasználása:
function loop() { update(); draw(); requestAnimationFrame(loop); } function draw() { // G.frame folyamatosan nő // sin(frame * 0.2) → hullámzó animáció const walk = Math.sin(G.frame * .2) * 2; // teknős lábai walk alapján mozognak drawTurtle(G.player.x, PLAYER_Y, G.player, G.frame); }
Készítsd el az első saját animációdat:
- Egy kör mozogjon jobbra-balra és pattanjon vissza a falnál
- Adj hozzá függőleges mozgást is (fel-le pattanás)
- A kör hagyon maga után halvány nyomot (ne clearRect-eld teljesen)
- Bónusz: állítsd meg/indítsd el szóköz billentyűvel (pause/resume)
🧠 Miért jobb a requestAnimationFrame a setInterval-nál játékhoz?
Koordinátarendszer és transzformációk
A teknős irányának megfelelően dől, a fa forgásból épül fel, az árnyék eltolva jelenik meg. Mindez save/restore és translate/rotate segítségével valósul meg.
1save() és restore() — a kontextus mentése
// save/restore nélkül: az egyik elem transzformációja // befolyásolja az összeset utána! // HELYES megközelítés: ctx.save(); // elmenti: szín, transform, stb. ctx.translate(100, 200); // origó áttolása ctx.rotate(0.5); // 0.5 radián = ~28.6 fok ctx.scale(1.5, 1.5); // 1.5x nagyítás // Minden rajzolás a NEW origóhoz képest: ctx.fillStyle = '#3ddc84'; ctx.fillRect(-25, -25, 50, 50); // origó körül ctx.restore(); // visszaállítja az eredeti állapotot // Innentől minden visszatért normálisba
3 példa a játékainkból
Teknős dőlése irány szerint (vx alapján):
function drawTurtle(x, y, t) { ctx.save(); ctx.translate(x, y); // origó a teknős közepére // vx alapján dől: ha jobbra megy → jobbra dől const tilt = Math.max(-0.28, Math.min(0.28, t.vx * .075) ); ctx.rotate(tilt); // Rajzolás (0,0) körül → a teknős közepéből drawShell(); drawHead(); drawLegs(); ctx.restore(); // visszaáll az eredeti transform }
Sirály rajzolása saját origó köré translate-tel:
function drawGull(x, y, frame) { const flap = Math.sin(frame * .2) * 7; ctx.save(); ctx.translate(x, y); // sirály közepébe // Test, szárnyak, fej (0,0) körül) ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.ellipse(0, 0, 9, 5, 0, 0, Math.PI*2); ctx.fill(); // Szárnyak flap szerint mozognak ctx.ellipse(-12, flap, 10, 3, 0, 0, Math.PI*2); ctx.fill(); ctx.restore(); }
Fák rajzolása — mindig saját origó körül:
function drawTree(x, y, level) { ctx.save(); ctx.translate(x, y); // origó a fa tövéhez // Törzs — felfelé (negatív Y) ctx.strokeStyle = '#8B6040'; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.moveTo(0, 10); ctx.lineTo(0, -18); ctx.stroke(); // Lomb — a törzs tetején ctx.fillStyle = '#2a8010'; ctx.beginPath(); ctx.arc(0, -22, 12, 0, Math.PI*2); ctx.fill(); ctx.restore(); }
Minden komplex rajzolási függvényt (drawTurtle, drawGull, drawTree) ctx.save() és ctx.restore() közé kell tenni, és ctx.translate(x, y)-al odatolni. Így a rajzoló függvény mindig (0,0) körül dolgozik, és nem kell tudnia hol van a karakter a canvas-on.
Transzformációk gyakorlása:
- Rajzolj egy négyzetet, forgasd el 45 fokkal (Math.PI/4 radián)
- Írj egy
drawCar(x, y)függvényt save/translate/restore-ral, hívd meg 3 különböző pozícióban - Animáció: egy kar forgása — save → translate a csukló pozícióba → rotate(frame*0.05) → lineTo → restore
🧠 Miért rajzolunk (0,0) körül a drawTurtle-ben, ha a teknős pl. (170, 400)-on van?
Szöveg rajzolása a canvasra
A pontszám, a "GAME OVER", a lebegő "+10" szövegek, a pályaszám — mind a ctx.fillText() segítségével kerülnek a képernyőre.
1Szöveg alapok
// Betűtípus: "vastagság méret családnév" ctx.font = 'bold 20px Fredoka One, sans-serif'; // Vízszintes igazítás: 'left', 'center', 'right' ctx.textAlign = 'center'; // Függőleges igazítás: 'top', 'middle', 'bottom' ctx.textBaseline = 'middle'; // Rajzolás: fillText(szöveg, x, y) ctx.fillStyle = '#ffe44d'; ctx.fillText('Pont: 1500', W/2, 30); // Keret szöveg (ritkán használjuk) ctx.strokeStyle = '#000'; ctx.strokeText('GAME OVER', W/2, H/2); // Szöveg szélessége (pl. középre igazításhoz) const w = ctx.measureText('Helló').width;
3 példa a játékainkból
Lebegő szöveg (+arany, -HP) animáció:
// floats tömb: {x, y, txt, col, life, vy} G.floats.forEach(f => { // Átlátszóság csökken ahogy eltűnik ctx.globalAlpha = f.life / 95; ctx.fillStyle = f.col; ctx.font = 'bold 13px Nunito,sans-serif'; ctx.textAlign = 'center'; ctx.fillText(f.txt, f.x, f.y); f.y += f.vy; // felfelé úszik f.life--; // elhalványul }); ctx.globalAlpha = 1;
Game Over / győzelem overlay szöveg:
// Sötét overlay háttér ctx.fillStyle = 'rgba(0,0,0,.6)'; ctx.fillRect(0, 0, W, H); // Nagy cím középen ctx.fillStyle = '#ffe44d'; ctx.font = 'bold 38px Fredoka One'; ctx.textAlign = 'center'; ctx.fillText('🏆 GYŐZELEM!', W/2, H/2-18); // Kisebb segédszöveg alatta ctx.fillStyle = '#fff'; ctx.font = '16px Nunito'; ctx.fillText('Pont: ' + G.score, W/2, H/2+16);
Teknős neve és tojásszám felirat:
ctx.save(); ctx.translate(x, y); // Cimke a teknős felett ctx.fillStyle = 'rgba(0,80,0,.8)'; ctx.font = 'bold 9px Nunito'; ctx.textAlign = 'center'; ctx.fillText('🐢 Te', 0, -28); // Tojásszám felett ha van if (t.eggs > 0) { ctx.fillStyle = 'rgba(255,200,0,.9)'; ctx.fillText('🥚×' + t.eggs, 0, -39); } ctx.restore();
Szövegek a canvason:
- Írj középre igazított "JÁTÉK" feliratot a canvas tetejére (textAlign: center)
- Jobb alsó sarokba kis pontszám szöveg (textAlign: right, textBaseline: bottom)
- Animáció: egy szöveg csúszik felfelé és elhalványul (globalAlpha csökken)
- Bónusz: GameOver overlay — sötét háttér + nagy szöveg + kisebb leírás
🧠 Melyik textAlign érték igazítja a szöveget úgy hogy az X koordináta a szöveg közepét jelöli?
Ütközésdetektálás
Ha a béka eléri a tojást, az eltűnik. Ha az ellenség eléri a kastélyt, HP vész. Mindkettő ütközésdetektálás — két módszerrel: körös és téglalapps.
1Köröknél: Math.hypot távolság
// Két kör ütközik ha a köztük lévő távolság // kisebb mint a sugaraik összege function circleHit(ax, ay, ar, bx, by, br) { const dist = Math.hypot(bx-ax, by-ay); return dist < ar + br; } // Használat: if (circleHit(frog.x, frog.y, 13, egg.x, egg.y, 9)) { egg.collected = true; score += 10; } // Téglalap ütközés (AABB - Axis-Aligned Bounding Box) function rectHit(ax, ay, aw, ah, bx, by, bw, bh) { return ax < bx+bw && ax+aw > bx && ay < by+bh && ay+ah > by; }
3 példa a játékainkból
Torony hatótáv vizsgálat — megvan-e az ellenség?
// Torony lövedékeit csak az ellenségek // HATÓTÁVON BELÜL kapják meg G.enemies.forEach(enemy => { const d = Math.hypot( enemy.x - tower.x, enemy.y - tower.y ); if (d < tower.range) { // Eltalálta! enemy.hp -= tower.dmg; } });
Tojás és sirály ütközés a béka körével:
G.objects.forEach(o => { if (o.done) return; const sy = o.y + G.scrollY; // Math.hypot: a béka és objektum távolsága const d = Math.hypot( p.x - o.x, (PLAYER_Y - p.jumpH) - sy ); if (o.type === 'egg' && d < o.r + 13) { o.done = true; p.eggs++; } });
Kő ütközés — ugráskor nem ütközik:
// Kő ütközés: csak ha NEM ugrik if (o.type === 'rock' && !p.jumping && !p.stunned) { const pd = Math.hypot( p.x - o.x, (PLAYER_Y - p.jumpH) - sy ); if (pd < o.r + 11) { loseLife(p); // ütközés! } }
A Math.hypot(dx, dy) ugyanaz mint Math.sqrt(dx*dx + dy*dy) — de rövidebb és nem kell sqrt-t írni. Pitagorasz tétele: a² + b² = c², ahol c a két pont távolsága.
Ütközésdetektálás gyakorlása:
- Rajzolj 2 kört, és írd ki ha ütköznek (circleHit függvény)
- Mozgó kör + álló cél: jelezd vizuálisan az ütközést (szín változás)
- Írj rectHit függvényt és tesztelj 2 téglalap átfedéssel
- Bónusz: gyűjtős játék — egy irányítható kör gyűjt tojásokat (körös ütközés)
🧠 Két kör mikor ütközik?
Scroll és kamerakövetés
A Teknős Versenyben a pálya hosszabb mint a képernyő — a kamera görget. A Froggy Rush-ban a pálya "felfelé" görög. Mindkettő ugyanazon az elvon alapul.
1World-space vs Screen-space
// scrollY: mennyit görgettünk (nő ahogy haladunk) let scrollY = 0; // Objektumnak van WORLD pozíciója (fix) const egg = { x: 200, y: -800 }; // a pálya egy pontján // SCREEN pozíció = world + scroll (ahol a képernyőn látszik) const screenY = egg.y + scrollY; // Az objektum a képernyőn van ha screenY in [0, H] if (screenY > -30 && screenY < H+30) { ctx.fillRect(egg.x, screenY, 14, 18); // rajzolás } // Update-ben: scrollY nő → objektumok feljebb kerülnek scrollY += scrollSpd; // pl. 2.2 px/frame // A játékos marad a helyén (PLAYER_Y fix pozíció) // Ez adja a "haladás" érzetet!
3 példa a játékainkból
Az objektumok world-space-ben fixek, csak scrollY mozog:
// Spawn: world Y = PLAYER_Y - scrollAtPlayer // Ekkor a játékos pont rajta áll const y = PLAYER_Y - S_start; // Draw-ban: screen_y = o.y + scrollY G.objects.forEach(o => { const sy = o.y + G.scrollY; if (sy < -40 || sy > H+40) return; drawEgg(o.x, sy); }); // Update: csak scrollY nő, objektumok NEM mozognak G.scrollY += scrollSpd;
A pályasorok Y pozíciója scroll alapján:
// A sorok (row 0-12) Y pozíciója // Fix: a sor helye a képernyőn nem változik function rowY(row) { return H - ROW_H * (row + 1); } // A béka "felfelé" halad = az objektumok // "lefelé" görögnek (scrollY nő) const sy = o.y + G.scrollY; // screen pozíció // Ütközéskor is scrollY-t kell figyelembe venni const d = Math.hypot(p.x-o.x, PLAYER_Y-sy);
Egyszerű oldalnézetű kamerakövetés:
// Kamera = a játékos X-je mínusz képernyő közepe let camX = 0; function update() { player.x += player.vx; // Kamera simán követi a játékost camX += (player.x - W/2 - camX) * 0.1; } function draw() { ctx.save(); // Kamera eltolása: minden a camX-szel balra tolódik ctx.translate(-camX, 0); // Pálya (a kamera koordinátarendszerében) ground.forEach(g => ctx.fillRect(g.x, g.y, g.w, 20)); drawPlayer(player.x, player.y); ctx.restore(); }
Scroll rendszer implementálása:
- Hozz létre 20 objektumot (tojás) véletlenszerű world-space Y pozícióval (-100 és -2000 között)
- A game loopban növeld a scrollY értékét (pl. 2 px/frame)
- Rajzold ki az objektumokat
screenY = obj.y + scrollYalapján - Ha a screenY > H+30, vedd ki a tömbből (filter)
- Bónusz: ütközésdetektálás a játékossal (Math.hypot)
🧠 Mi a screen_y = obj.y + scrollY képlet?
Sprite animáció — képkockák nélkül
A játékainkban nincsenek képfájlok — a teknős lábai, a béka ugrása, a sirály szárnycsapása mind Math.sin() és a frame számláló segítségével animálódik.
1A sin() alapú animáció
// A frame folyamatosan nő: 0, 1, 2, 3, 4... // Math.sin(frame * sebesség) → -1 és +1 között oszcillál // * amplitúdó → a mozgás nagyságát adja meg // Lassú hullámzás: const slow = Math.sin(frame * 0.05) * 10; // ±10px lassan // Közepes léptetés: const walk = Math.sin(frame * 0.2) * 3; // ±3px közepesen // Gyors szárnycsapás: const flap = Math.sin(frame * 0.25) * 8; // ±8px gyorsan // Pulzáló méret (0 és 1 között, nem -1 és 1): const pulse = (Math.sin(frame * 0.1) + 1) * .5;
Ugrás parabolája sin()-nel
// jumpT: az ugrás ideje (0-tól N-ig számol) let jumpT = 0; const JUMP_FRAMES = 22; // 22 frame alatt le is ér if (jumping) { jumpT++; // sin(0) = 0, sin(π/2) = 1, sin(π) = 0 // → parabola: felmegy és visszajön jumpH = Math.sin(jumpT / JUMP_FRAMES * Math.PI) * 38; // 38px a csúcsmagasság if (jumpT >= JUMP_FRAMES) { jumping = false; jumpH = 0; } } // Rajzoláskor: Y-ból kivonjuk a jumpH-t (felfelé mozdul) ctx.translate(p.x, PLAYER_Y - p.jumpH);
3 példa a játékainkból
Teknős lábak ellentétes léptetése:
// walk: -2 és +2 között oszcillál const walk = Math.sin(frame * .2) * 2; // Bal hátsó, jobb hátsó — ELLENTÉTESEN lépnek ctx.ellipse(-10, 12 + walk, 7, 4, .3, 0, Math.PI*2); ctx.fill(); ctx.ellipse( 10, 12 - walk, 7, 4, -.3, 0, Math.PI*2); ctx.fill(); // Első lábak is ellentétesen: ctx.ellipse(-10, -8 - walk, 6, 4, .2, 0, Math.PI*2); ctx.ellipse( 10, -8 + walk, 6, 4, -.2, 0, Math.PI*2);
Béka ugrás parabolaíve sin()-nel:
// jumpT 0→22 alatt lefut (22 frame) if (p.jumping) { p.jumpT++; p.jumpH = Math.sin( p.jumpT / 22 * Math.PI ) * 38; // max 38px magasan if (p.jumpT >= 22) { p.jumping = false; p.jumpH = 0; } } // Rajzoláskor: ctx.translate(p.x, PLAYER_Y - p.jumpH);
Hős "lélegzés" animáció nyugalomban:
// Lassú fel-le mozgás = "lélegzés" const bob = Math.sin(G.frame * 0.04) * 1.5; ctx.save(); // A hős kissé feljebb-lejjebb mozog ctx.translate(h.x, h.y + bob); drawHeroBody(); drawHeroHead(); drawHeroWeapon(); ctx.restore();
Animáljunk mindent sin()-nel:
- Rajzolj egy karaktert (egyszerű pálcika ember) és animáld a karját sin()-nel
- Implementáld a parabolás ugrást (jumpT, jumpH, sin(jumpT/N * PI))
- Egy kör "lüktet" — mérete sin()-nel oszcillál 20 és 30 pixel között
- Bónusz: két karakter ellentétes léptetéssel (egyik +walk, másik -walk)
🧠 Miért adódik 1 és osztódik 2-vel a pulzáló animációnál: (sin(frame)+1)*.5?
🏆 Projektzáró — Mozgó karakter
A 3. Fázis összefoglalója: egy teljes, irányítható, animált karakter a canvason — game loop, scroll, ütközés, sin() animáció mind egyszerre.
1A projekt feladata
Készíts egy egyszerű canvas-alapú mini játékot ahol egy karakter mozog és gyűjt tárgyakat. Minden amit ebben a fázisban tanultál, benne van.
✓ Canvas + 2D kontextus, clearRect minden frame-ben
✓ Game loop: requestAnimationFrame, update + draw szétválasztva
✓ Irányítható karakter: billentyű / érintés, save/translate/restore
✓ Legalább 1 animáció: sin() alapú lábdobogás vagy ugrás
✓ Legalább 5 gyűjthető tárgy: Math.random spawning
✓ Ütközésdetektálás: Math.hypot
✓ Pontszám szöveg a canvason: ctx.fillText
// === ÁLLAPOT === const CV = ..., ctx = CV.getContext('2d'); let player = { x: 200, y: 300, vx:0, vy:0 }; let items = []; // gyűjthető tárgyak let score = 0; let frame = 0; const K = {}; // lenyomott billentyűk // === UPDATE === function update() { frame++; // Mozgás billentyűk alapján if (K['ArrowLeft']) player.vx -= 0.5; if (K['ArrowRight']) player.vx += 0.5; player.vx *= 0.85; // súrlódás player.x += player.vx; // Ütközés: gyűjt-e tárgyat? items = items.filter(item => { if (Math.hypot(player.x-item.x, player.y-item.y) < 25) { score++; return false; // eltűnik } return true; }); } // === DRAW === function draw() { ctx.clearRect(0, 0, W, H); items.forEach(item => { drawItem(item.x, item.y); }); drawPlayer(player.x, player.y, frame); // Pontszám ctx.fillStyle = '#fff'; ctx.font = 'bold 18px sans-serif'; ctx.textAlign = 'left'; ctx.fillText('Pont: ' + score, 10, 30); } // === LOOP === function loop() { update(); draw(); requestAnimationFrame(loop); } loop();
- Canvas létrehozva, ctx megszerezte, W és H változóban
- requestAnimationFrame loop: update() + draw() elkülönítve
- clearRect minden draw() elején (nem marad nyom)
- Karakter billentyűkkel irányítható (keydown/keyup, K objektum)
- Karakter save/translate/restore-ral rajzolódik
- Legalább egy sin() alapú animáció (lábdobogás, hullámzás)
- Gyűjthető tárgyak Math.random pozícióban spawolva
- Ütközésdetektálás Math.hypot-tal
- Pontszám ctx.fillText-tel a canvason
- Mobilon is működik (D-pad gombok HTML-ben)
⭐ Scroll rendszer — a pálya hosszabb mint a képernyő
⭐⭐ Ugrás implementálása (jumpT, jumpH, sin() parabolaív)
⭐⭐ Ellenség ami üldözi a játékost (AI: közeledés a játékos felé)
⭐⭐⭐ Életrendszer + Game Over képernyő