Repository: pothonprogramming/pothonprogramming.github.io Branch: master Commit: f9f535c65af0 Files: 204 Total size: 594.8 KB Directory structure: gitextract_49aaihe4/ ├── content/ │ ├── 2018/ │ │ └── minkowski-difference/ │ │ └── minkowski-difference.html │ ├── animation/ │ │ ├── animation.css │ │ ├── animation.html │ │ └── animation.js │ ├── animation-game-loop/ │ │ ├── animation.css │ │ ├── animation.html │ │ └── animation.js │ ├── better-tile/ │ │ ├── better-tile.html │ │ └── better-tile.js │ ├── better-tile-graphics/ │ │ ├── better-tile-graphics.html │ │ └── better-tile-graphics.js │ ├── blit/ │ │ ├── blit.css │ │ ├── blit.html │ │ └── blit.js │ ├── bouncing-polygons/ │ │ └── bouncing-polygons.html │ ├── calculator/ │ │ ├── calculator.css │ │ ├── calculator.html │ │ └── calculator.js │ ├── canvas/ │ │ ├── canvas.css │ │ ├── canvas.html │ │ └── canvas.js │ ├── circle-collision-detection/ │ │ └── circle-collision-detection.html │ ├── circle-collision-response/ │ │ └── circle-collision-response.html │ ├── collision/ │ │ ├── collision.css │ │ ├── collision.html │ │ └── collision.js │ ├── control/ │ │ ├── control.css │ │ ├── control.html │ │ └── control.js │ ├── cube/ │ │ └── cube.html │ ├── dino/ │ │ ├── dino.css │ │ ├── dino.html │ │ └── dino.js │ ├── dominiques-doors/ │ │ ├── area0.json │ │ ├── area1.json │ │ ├── area2.json │ │ ├── area3.json │ │ ├── dominiques-doors.css │ │ ├── dominiques-doors.html │ │ └── dominiques-doors.js │ ├── elements/ │ │ ├── elements.html │ │ └── elements.js │ ├── gjk/ │ │ └── gjk.html │ ├── hello-world/ │ │ └── hello.html │ ├── hit-the-wall/ │ │ ├── hit-the-wall.css │ │ ├── hit-the-wall.html │ │ └── hit-the-wall.js │ ├── hitbox/ │ │ └── hitbox.html │ ├── https-server/ │ │ ├── index.css │ │ ├── index.html │ │ ├── server.js │ │ └── ssl/ │ │ ├── crt.cnf │ │ ├── crt.pfx │ │ ├── csr.cnf │ │ ├── make-crt.sh │ │ ├── make-csr.sh │ │ ├── make-key.sh │ │ └── make-pfx.sh │ ├── indexed-db/ │ │ ├── index.css │ │ └── index.html │ ├── inheritance/ │ │ ├── inheritance.html │ │ └── inheritance.js │ ├── inventory/ │ │ └── inventory.html │ ├── ipo/ │ │ ├── components/ │ │ │ ├── input.js │ │ │ ├── main.js │ │ │ ├── output.js │ │ │ └── processor.js │ │ └── ipo.html │ ├── json/ │ │ ├── json.html │ │ ├── json.js │ │ └── json.json │ ├── load-image/ │ │ ├── load-image.css │ │ ├── load-image.html │ │ └── load-image.js │ ├── multiple-inheritance/ │ │ ├── multiple-inheritance.html │ │ └── multiple-inheritance.js │ ├── objects-and-vars/ │ │ ├── objects.html │ │ └── objects.js │ ├── offline-web-app/ │ │ ├── manifest.json │ │ ├── server.js │ │ ├── ssl/ │ │ │ └── crt.pfx │ │ ├── web-app-service.js │ │ ├── web-app.css │ │ └── web-app.html │ ├── pagination/ │ │ ├── article1.txt │ │ ├── article2.txt │ │ ├── article3.txt │ │ ├── article4.txt │ │ ├── article5.txt │ │ ├── pagination.css │ │ ├── pagination.html │ │ └── paginator.js │ ├── particle-pool/ │ │ └── particle-pool.html │ ├── platform/ │ │ └── platform.html │ ├── platformer-ai/ │ │ └── platformer-ai.html │ ├── polygon/ │ │ └── polygon.html │ ├── polygon-rotation/ │ │ └── polygon-rotation.html │ ├── pre-scale-performance/ │ │ ├── pre-scale-performance.css │ │ ├── pre-scale-performance.html │ │ └── pre-scale-performance.js │ ├── prototype-inheritance/ │ │ ├── prototype-inheritance.html │ │ └── prototype-inheritance.js │ ├── rabbit-trap/ │ │ ├── 01/ │ │ │ ├── controller-01.js │ │ │ ├── display-01.js │ │ │ ├── engine-01.js │ │ │ ├── game-01.js │ │ │ └── main-01.js │ │ ├── 02/ │ │ │ ├── controller-02.js │ │ │ ├── display-02.js │ │ │ ├── game-02.js │ │ │ └── main-02.js │ │ ├── 03/ │ │ │ ├── display-03.js │ │ │ ├── game-03.js │ │ │ └── main-03.js │ │ ├── 04/ │ │ │ ├── display-04.js │ │ │ └── game-04.js │ │ ├── 05/ │ │ │ ├── display-05.js │ │ │ ├── game-05.js │ │ │ └── main-05.js │ │ ├── 06/ │ │ │ ├── engine-06.js │ │ │ ├── game-06.js │ │ │ ├── main-06.js │ │ │ ├── script.txt │ │ │ ├── zone00.json │ │ │ ├── zone01.json │ │ │ ├── zone02.json │ │ │ ├── zone03.json │ │ │ └── zone04.json │ │ ├── 07/ │ │ │ ├── game-07.js │ │ │ ├── main-07.js │ │ │ └── zone00.json │ │ ├── rabbit-trap.css │ │ └── rabbit-trap.html │ ├── rectangle-collision/ │ │ └── rectangle-collision.html │ ├── shoot/ │ │ └── shoot.html │ ├── snake/ │ │ ├── snake.css │ │ ├── snake.html │ │ └── snake.js │ ├── square-collision-response/ │ │ ├── response.css │ │ ├── response.html │ │ └── response.js │ ├── starfield/ │ │ └── starfield.html │ ├── stay-down/ │ │ ├── game-states/ │ │ │ ├── pause.js │ │ │ ├── run.js │ │ │ └── title.js │ │ ├── initialize.js │ │ ├── stay-down.html │ │ ├── stay-down.js │ │ ├── tools/ │ │ │ ├── controller.js │ │ │ ├── engine.js │ │ │ ├── loader.js │ │ │ ├── state-manager.js │ │ │ └── text.js │ │ └── utilities/ │ │ ├── buffer.js │ │ ├── collider.js │ │ ├── frame.js │ │ ├── item.js │ │ ├── platform.js │ │ ├── player.js │ │ └── rectangle-2d.js │ ├── tile-animation/ │ │ └── tile-animation.html │ ├── tile-graphics/ │ │ ├── tile-graphics.css │ │ ├── tile-graphics.html │ │ └── tile-graphics.js │ ├── tile-grid/ │ │ ├── tile-grid.css │ │ ├── tile-grid.html │ │ └── tile-grid.js │ ├── tile-scroll/ │ │ └── tile-scroll.html │ ├── tile-types/ │ │ ├── tile-types.css │ │ ├── tile-types.html │ │ └── tile-types.js │ ├── tile-world/ │ │ ├── tile-world.css │ │ ├── tile-world.html │ │ └── tile-world.js │ ├── top-down-tiles/ │ │ ├── top-down-tiles.css │ │ ├── top-down-tiles.html │ │ └── top-down-tiles.js │ ├── touch-controller/ │ │ ├── touch-controller.css │ │ ├── touch-controller.html │ │ └── touch-controller.js │ ├── vector-math/ │ │ └── vector-math.html │ ├── walk-on-tiles/ │ │ ├── walk-on-tiles.css │ │ ├── walk-on-tiles.html │ │ └── walk-on-tiles.js │ ├── web-app/ │ │ ├── manifest.json │ │ ├── server.js │ │ ├── ssl/ │ │ │ └── crt.pfx │ │ ├── web-app.css │ │ └── web-app.html │ ├── wmw-basic/ │ │ └── basic.html │ └── wmw-bouncing-balls/ │ └── bouncing-balls.html ├── data/ │ ├── logs.json │ └── projects.json ├── index.css ├── index.html ├── index.js ├── library/ │ └── dom-kit.js ├── log.css ├── project.css ├── robots.txt ├── server.js ├── theme.css └── tools/ ├── log.js └── project.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: content/2018/minkowski-difference/minkowski-difference.html ================================================ Minkowski Difference
circle polygon rectangle
================================================ FILE: content/animation/animation.css ================================================ /* Frank Poth 12/23/2017 */ * { box-sizing:border-box; margin:0; padding:0; } html { height:100%; width:100%; } body { align-content:space-around; background-color:#202830; color:#ffffff; display:grid; grid-template-columns:auto; grid-template-rows:auto auto auto; height:100%; justify-items:center; padding:0 8px; width:100%; } ================================================ FILE: content/animation/animation.html ================================================

PoP Vlog - Sprite Animation

Use the keyboard to walk left and right. Also, press up to jump.

================================================ FILE: content/animation/animation.js ================================================ // Frank Poth 12/23/2017 /* This example will show you how to do custom sprite animation in JavaScript. It uses an Animation class that handles updating and changing a sprite's current frame, and a sprite_sheet object to hold the source image and the different animation frame sets. */ (function() { "use strict"; /* Each sprite sheet tile is 16x16 pixels in dimension. */ const SPRITE_SIZE = 16; /* The Animation class manages frames within an animation frame set. The frame set is an array of values that correspond to the location of sprite images in the sprite sheet. For example, a frame value of 0 would correspond to the first sprite image / tile in the sprite sheet. By arranging these values in a frame set array, you can create a sequence of frames that make an animation when played in quick succession. */ var Animation = function(frame_set, delay) { this.count = 0;// Counts the number of game cycles since the last frame change. this.delay = delay;// The number of game cycles to wait until the next frame change. this.frame = 0;// The value in the sprite sheet of the sprite image / tile to display. this.frame_index = 0;// The frame's index in the current animation frame set. this.frame_set = frame_set;// The current animation frame set that holds sprite tile values. }; Animation.prototype = { /* This changes the current animation frame set. For example, if the current set is [0, 1], and the new set is [2, 3], it changes the set to [2, 3]. It also sets the delay. */ change:function(frame_set, delay = 15) { if (this.frame_set != frame_set) {// If the frame set is different: this.count = 0;// Reset the count. this.delay = delay;// Set the delay. this.frame_index = 0;// Start at the first frame in the new frame set. this.frame_set = frame_set;// Set the new frame set. this.frame = this.frame_set[this.frame_index];// Set the new frame value. } }, /* Call this on each game cycle. */ update:function() { this.count ++;// Keep track of how many cycles have passed since the last frame change. if (this.count >= this.delay) {// If enough cycles have passed, we change the frame. this.count = 0;// Reset the count. /* If the frame index is on the last value in the frame set, reset to 0. If the frame index is not on the last value, just add 1 to it. */ this.frame_index = (this.frame_index == this.frame_set.length - 1) ? 0 : this.frame_index + 1; this.frame = this.frame_set[this.frame_index];// Change the current frame value. } } }; var buffer, controller, display, loop, player, render, resize, sprite_sheet; buffer = document.createElement("canvas").getContext("2d"); display = document.querySelector("canvas").getContext("2d"); /* I made some changes to the controller object. */ controller = { /* Now each key object knows its physical state as well as its active state. When a key is active it is used in the game logic, but its physical state is always recorded and never altered for reference. */ left: { active:false, state:false }, right: { active:false, state:false }, up: { active:false, state:false }, keyUpDown:function(event) { /* Get the physical state of the key being pressed. true = down false = up*/ var key_state = (event.type == "keydown") ? true : false; switch(event.keyCode) { case 37:// left key /* If the virtual state of the key is not equal to the physical state of the key, we know something has changed, and we must update the active state of the key. By doing this it prevents repeat firing of keydown events from altering the active state of the key. Basically, when you are jumping, holding the jump key down isn't going to work. You'll have to hit it every time, but only if you set the active key state to false when you jump. */ if (controller.left.state != key_state) controller.left.active = key_state; controller.left.state = key_state;// Always update the physical state. break; case 38:// up key if (controller.up.state != key_state) controller.up.active = key_state; controller.up.state = key_state; break; case 39:// right key if (controller.right.state != key_state) controller.right.active = key_state; controller.right.state = key_state; break; } //console.log("left: " + controller.left.state + ", " + controller.left.active + "\nright: " + controller.right.state + ", " + controller.right.active + "\nup: " + controller.up.state + ", " + controller.up.active); } }; /* The player object is just a rectangle with an animation object. */ player = { animation:new Animation(),// You don't need to setup Animation right away. jumping:true, height:16, width:16, x:0, y:40 - 18, x_velocity:0, y_velocity:0 }; /* The sprite sheet object holds the sprite sheet graphic and some animation frame sets. An animation frame set is just an array of frame values that correspond to each sprite image in the sprite sheet, just like a tile sheet and a tile map. */ sprite_sheet = { frame_sets:[[0, 1], [2, 3], [4, 5]],// standing still, walk right, walk left image:new Image() }; loop = function(time_stamp) { if (controller.up.active && !player.jumping) { controller.up.active = false; player.jumping = true; player.y_velocity -= 2.5; } if (controller.left.active) { /* To change the animation, all you have to do is call animation.change. */ player.animation.change(sprite_sheet.frame_sets[2], 15); player.x_velocity -= 0.05; } if (controller.right.active) { player.animation.change(sprite_sheet.frame_sets[1], 15); player.x_velocity += 0.05; } /* If you're just standing still, change the animation to standing still. */ if (!controller.left.active && !controller.right.active) { player.animation.change(sprite_sheet.frame_sets[0], 20); } player.y_velocity += 0.25; player.x += player.x_velocity; player.y += player.y_velocity; player.x_velocity *= 0.9; player.y_velocity *= 0.9; if (player.y + player.height > buffer.canvas.height - 2) { player.jumping = false; player.y = buffer.canvas.height - 2 - player.height; player.y_velocity = 0; } if (player.x + player.width < 0) { player.x = buffer.canvas.width; } else if (player.x > buffer.canvas.width) { player.x = - player.width; } player.animation.update(); render(); window.requestAnimationFrame(loop); }; render = function() { /* Draw the background. */ buffer.fillStyle = "#7ec0ff"; buffer.fillRect(0, 0, buffer.canvas.width, buffer.canvas.height); buffer.strokeStyle = "#8ed0ff"; buffer.lineWidth = 10; buffer.beginPath(); buffer.moveTo(0, 0); buffer.bezierCurveTo(40, 20, 40, 0, 80, 0); buffer.moveTo(0, 0); buffer.bezierCurveTo(40, 20, 40, 20, 80, 0); buffer.stroke(); buffer.fillStyle = "#009900"; buffer.fillRect(0, 36, buffer.canvas.width, 4); /* When you draw your sprite, just use the animation frame value to determine where to cut your image from the sprite sheet. It's the same technique used for cutting tiles out of a tile sheet. Here I have a very easy implementation set up because my sprite sheet is only a single row. */ /* 02/07/2018 I added Math.floor to the player's x and y positions to eliminate antialiasing issues. Take out the Math.floor to see what I mean. */ buffer.drawImage(sprite_sheet.image, player.animation.frame * SPRITE_SIZE, 0, SPRITE_SIZE, SPRITE_SIZE, Math.floor(player.x), Math.floor(player.y), SPRITE_SIZE, SPRITE_SIZE); display.drawImage(buffer.canvas, 0, 0, buffer.canvas.width, buffer.canvas.height, 0, 0, display.canvas.width, display.canvas.height); }; resize = function() { display.canvas.width = document.documentElement.clientWidth - 32; if (display.canvas.width > document.documentElement.clientHeight) { display.canvas.width = document.documentElement.clientHeight; } display.canvas.height = display.canvas.width * 0.5; display.imageSmoothingEnabled = false; }; //////////////////// //// INITIALIZE //// //////////////////// buffer.canvas.width = 80; buffer.canvas.height = 40; window.addEventListener("resize", resize); window.addEventListener("keydown", controller.keyUpDown); window.addEventListener("keyup", controller.keyUpDown); resize(); sprite_sheet.image.addEventListener("load", function(event) {// When the load event fires, do this: window.requestAnimationFrame(loop);// Start the game loop. }); sprite_sheet.image.src = "animation.png";// Start loading the image. })(); ================================================ FILE: content/animation-game-loop/animation.css ================================================ /* Frank Poth 08/12/2017 */ * { box-sizing:border-box; margin:0; padding:0; } html, body { height:100%; width:100%; } body { align-content:space-around; background-color:#202830; color:#ffffff; display:grid; justify-content:center; } canvas { background-color:#ffffff; } ================================================ FILE: content/animation-game-loop/animation.html ================================================ Animation

PoP Vlog - Animation

================================================ FILE: content/animation-game-loop/animation.js ================================================ // Frank Poth 08/12/2017 var context, rectangle, loop; context = document.querySelector("canvas").getContext("2d"); context.canvas.height = 180; context.canvas.width = 320; rectangle = { height:32, width:32, x:0, y:72, // Center of the canvas }; loop = function() { rectangle.x += 1; context.fillStyle = "#202020"; context.fillRect(0, 0, 320, 180);// x, y, width, height context.fillStyle = "#ff0000";// hex for red context.beginPath(); context.rect(rectangle.x, rectangle.y, rectangle.width, rectangle.height); context.fill(); if (rectangle.x > 320) {// if rectangle goes past right boundary rectangle.x = -32; } // call update when the browser is ready to draw again window.requestAnimationFrame(loop); }; window.requestAnimationFrame(loop); ================================================ FILE: content/better-tile/better-tile.html ================================================ Better Tile ================================================ FILE: content/better-tile/better-tile.js ================================================ (() => { // The display canvas' context. Draw the tile buffer here. const DISPLAY = document.querySelector('canvas').getContext('2d', { alpha:false, desynchronized:false }); // The tile buffer canvas' context. Draw individual tiles here. const BUFFER = document.createElement('canvas').getContext('2d', { alpha:false, desynchronized:true }); // This is the width and height for every tile. const TILE_SIZE = 16; // The TILES object contains "tile" objects with keys that correspond to the map values. // Each tile object has a color. const TILES = { 0: { color:'#d8f4f4' }, // sky 1: { color:'#ffffff' }, // cloud 2: { color:'#3e611e' }, // grass 3: { color:'#412823' } // dirt } // The map holds all the info about the map we will be drawing, including the tile indices array. const MAP = { columns: 16, rows: 14, height: 14 * TILE_SIZE, width: 16 * TILE_SIZE, // This is used during image scaling to ensure the rendered image is not skewed. width_height_ratio: 16 / 14, // The values in this array correspond to the keys in the TILES object. tiles:[1,1,1,1,0,0,0,0,0,0,0,0,0,0,1,1, 0,1,1,0,0,0,0,0,0,0,0,0,1,1,1,1, 0,0,0,0,1,1,1,0,0,0,0,1,1,1,1,0, 0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0, 0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,2,2,0,0,0, 2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2, 3,2,2,2,2,2,2,0,0,0,2,2,2,2,2,2, 3,3,3,3,3,3,3,0,0,0,3,3,3,3,3,3] }; // This will render tiles to the buffer. function renderTiles() { var map_index = 0; // This represents the position in the MAP.tiles array we're getting our tile value from. // Increment by the actual TILE_SIZE to avoid having to multiply on every iteration. for (var top = 0; top < MAP.height; top += TILE_SIZE) { for (var left = 0; left < MAP.width; left += TILE_SIZE) { var tile_value = MAP.tiles[map_index]; // Get the tile value from the map. var tile = TILES[tile_value]; // Get the specific tile object from the TILES object. BUFFER.fillStyle = tile.color; // Now that we have the tile we can access its properties. BUFFER.fillRect(left, top, TILE_SIZE, TILE_SIZE); // Draw the tile at the left, top position and TILE_SIZE. map_index ++; // Make sure to increment the map_index so we can get the next tile from the map. } } } // Render the buffer to the display. // If this example required a game loop or repeated draws, this would be your main rendering function. // The benefit of this approach is that you only make 1 drawImage call here instead of 1 call for every tile. function renderDisplay() { DISPLAY.drawImage(BUFFER.canvas, 0, 0); } // This function resizes the CSS width and height of the DISPLAY canvas to force it to scale to fit the window. function resize(event) { // Get the height and width of the window var height = document.documentElement.clientHeight; var width = document.documentElement.clientWidth; // This makes sure the DISPLAY canvas is resized in a way that maintains the MAP's width / height ratio. if (width / height < MAP.width_height_ratio) height = Math.floor(width / MAP.width_height_ratio); else width = Math.floor(height * MAP.width_height_ratio); // This sets the CSS of the DISPLAY canvas to resize it to the scaled height and width. DISPLAY.canvas.style.height = height + 'px'; DISPLAY.canvas.style.width = width + 'px'; } // Set the initial width and height of the BUFFER and the DISPLAY canvases. BUFFER.canvas.width = DISPLAY.canvas.width = MAP.width; BUFFER.canvas.height = DISPLAY.canvas.height = MAP.height; // To ensure there is no anti-aliasing when drawing to the canvas, set image smoothing to false on both canvases. BUFFER.imageSmoothingEnabled = DISPLAY.imageSmoothingEnabled = false; // Draw the individual tiles to the buffer. renderTiles(); // Draw the BUFFER to the DISPLAY canvas. renderDisplay(); // Add the resize event listener. window.addEventListener('resize', resize); // Calling resize forces the DISPLAY canvas to be scaled by the CSS. resize(); })(); ================================================ FILE: content/better-tile-graphics/better-tile-graphics.html ================================================ Better Tile Graphics ================================================ FILE: content/better-tile-graphics/better-tile-graphics.js ================================================ (() => { // The display canvas' context. Draw the tile buffer here. It's important not to desynchronize when using CSS to scale. const DISPLAY = document.querySelector('canvas').getContext('2d', { alpha:false, desynchronized:false }); // The tile buffer canvas' context. Draw individual tiles here. const BUFFER = document.createElement('canvas').getContext('2d', { alpha:false, desynchronized:true }); // This is the width and height for every tile. const TILE_SIZE = 16; // This image will hold the tile sheet once it is loaded. const TILE_SHEET_IMAGE = new Image(); // The map holds all the info about the map we will be drawing, including the tile indices array. const MAP = { columns: 16, rows: 14, height: 14 * TILE_SIZE, width: 16 * TILE_SIZE, // This is used during image scaling to ensure the rendered image is not skewed. width_height_ratio: 16 / 14, // The values in this array correspond to the keys in the TILES object. tiles:[10,22,22,22,22,22,22,22,22,22,22,22,23,-1, 8,15, 20, 1, 2, 3, 2, 1, 1, 3, 3, 2, 1, 1, 3,-1, 9,19, 20,-1, 7, 5, 0, 6, 6,-1,-1,-1,-1,-1, 4, 5, 6,19, 20,-1, 8,15,16,17,18,-1,-1,-1, 6, 5,15,16,17,10, 20,-1, 8,19,11,12,23,-1,-1,-1,15,17,10,11,10,10, 20,-1, 8,19,10,23, 3,-1,-1,-1,21,22,22,22,22,13, 20,-1, 8,19,20, 2,-1,-1,-1,-1, 3, 2, 1, 1, 2,19, 20,-1, 8,19,20,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,19, 20,-1, 8,19,20, 5, 5, 6, 4, 6, 5, 4, 4, 0, 6,19, 20,-1, 8,19,10,17,16,17,17,16,17,17,17,17,16,10, 20,-1, 8,21,22,22,22,22,22,22,22,22,22,22,22,10, 20,-1, 9, 3, 1, 1, 2, 3, 3, 2, 1, 2, 3, 1, 1,19, 20, 4, 5, 6, 6, 5, 4, 4, 5, 4, 6, 5, 6,-1, 7,19, 10,16,17,17,16,17,17,17,16,17,17,16,18,-1, 8,19] }; // This will calculate the tile's source position in the tile sheet given the number of columns in the tile sheet and the index of the tile in the tile sheet. function calculateTileSourcePosition(tile_index, tile_sheet_columns) { return { x: tile_index % tile_sheet_columns * TILE_SIZE, y:Math.floor(tile_index / tile_sheet_columns) * TILE_SIZE }; } // This will render tiles to the buffer. function renderTiles() { var map_index = 0; // This represents the position in the MAP.tiles array we're getting our tile value from. // Increment by the actual TILE_SIZE to avoid having to multiply on every iteration. for (var top = 0; top < MAP.height; top += TILE_SIZE) { for (var left = 0; left < MAP.width; left += TILE_SIZE) { var tile_value = MAP.tiles[map_index]; // Get the tile value from the map. map_index ++; // Make sure to increment the map_index so we can get the next tile from the map. if (tile_value == -1) continue; // If the tile space is meant to be empty, skip this iteration. var tile_source_position = calculateTileSourcePosition(tile_value, 6); // Get the specific tile object from the TILES object. BUFFER.drawImage(TILE_SHEET_IMAGE, tile_source_position.x, tile_source_position.y, TILE_SIZE, TILE_SIZE, left, top, TILE_SIZE, TILE_SIZE); } } } // Render the buffer to the display. // If this example required a game loop or repeated draws, this would be your main rendering function. // The benefit of this approach is that you only make 1 drawImage call here instead of 1 call for every tile. function renderDisplay() { DISPLAY.drawImage(BUFFER.canvas, 0, 0); } // This function resizes the CSS width and height of the DISPLAY canvas to force it to scale to fit the window. function resize(event) { // Get the height and width of the window var height = document.documentElement.clientHeight; var width = document.documentElement.clientWidth; // This makes sure the DISPLAY canvas is resized in a way that maintains the MAP's width / height ratio. if (width / height < MAP.width_height_ratio) height = Math.floor(width / MAP.width_height_ratio); else width = Math.floor(height * MAP.width_height_ratio); // This sets the CSS of the DISPLAY canvas to resize it to the scaled height and width. DISPLAY.canvas.style.height = height + 'px'; DISPLAY.canvas.style.width = width + 'px'; } // Set the initial width and height of the BUFFER and the DISPLAY canvases. BUFFER.canvas.width = DISPLAY.canvas.width = MAP.width; BUFFER.canvas.height = DISPLAY.canvas.height = MAP.height; // To ensure there is no anti-aliasing when drawing to the canvas, set image smoothing to false on both canvases. BUFFER.imageSmoothingEnabled = DISPLAY.imageSmoothingEnabled = false; // Add the resize event listener. window.addEventListener('resize', resize); TILE_SHEET_IMAGE.addEventListener('load', function(event) { // Draw the individual tiles to the buffer. renderTiles(); // Draw the BUFFER to the DISPLAY canvas. renderDisplay(); // Calling resize forces the DISPLAY canvas to be scaled by the CSS. resize(); }, { once:true }); TILE_SHEET_IMAGE.src = 'better-tile-graphics.png'; })(); ================================================ FILE: content/blit/blit.css ================================================ /* Frank Poth 12/26/2017 */ * { box-sizing:border-box; margin:0; padding:0; user-select:none; } html { height:100%; width:100%; } body { align-content:space-around; background-color:#202830; color:#ffffff; display:grid; grid-template-columns:auto; grid-template-rows:auto auto auto auto; height:100%; justify-items:center; padding:0 8px; width:100%; } div { align-content:space-around; display:grid; grid-column-gap:8px; grid-row-gap:8px; grid-template-areas:"input input" "button1 button2" "average1 average2" "output1 output2"; grid-template-columns:auto auto; grid-template-rows:auto auto auto auto; min-width:50%; } #number-of-tests-input { background-color:rgba(0, 0, 0, 0); border:none; color:#ffffff; font-size:1.0em; grid-area:input; text-align:center; } #draw-image-button { grid-area:button1; } #draw-image-average { grid-area:average1; } #draw-image-output { grid-area:output1; } #image-data-button { grid-area:button2; } #image-data-average { grid-area:average2; } #image-data-output { grid-area:output2; } .button { border-color:#ffffff; border-radius:16px; border-style:solid; border-width:1px; cursor:pointer; padding:4px; text-align:center; } .output { font-size:1.0em; height:6.0em; overflow-y:auto; } ================================================ FILE: content/blit/blit.html ================================================

PoP Vlog - Blit Test

Which method is faster: CanvasRenderingContext2D.drawImage or CanvasRenderingContext2D.putImageData?
Type clear to clear tests.

Test drawImage Test imageData

Average (0 sets) 0.0 ms

Average (0 sets) 0.0 ms

Number of tests (0): 0.0 ms

Number of tests (0): 0.0 ms

================================================ FILE: content/blit/blit.js ================================================ // Frank Poth 12/27/2017 /* This program tests drawImage and putImageData efficiency. Basically, I run these functions thousands of times and time them. After some tests it became pretty obvious that drawImage is way faster, at least on my version of Chrome, anyway. The speed difference increases with the number of tests performed, but drawImage still comes out ahead. "Comes out ahead..." What is this, a horse race? */ (function() { var averages, buffer, display, drawImage, image, image_context, imageData, number_of_tests, test, ui; averages = [[], []];// Where the averages for each set of tests are stored. /* I do the testing on the buffer, but to make the image bigger I draw the final image to the display with drawImage, which automatically scales it up. Only the individual drawing methods are timed, however, so don't let this distract you. */ buffer = document.createElement("canvas").getContext("2d"); display = document.querySelector("canvas").getContext("2d"); image = new Image();// Where the loaded image will be stored. image_context = document.createElement("canvas").getContext("2d"); number_of_tests = 10000;// The default number of tests to run. drawImage = function() { buffer.drawImage(image_context.canvas, 0, 0, image_context.canvas.width, image_context.canvas.height, 0, 0, image_context.canvas.width, image_context.canvas.height); }; imageData = function() { buffer.putImageData(image_context.getImageData(0, 0, image_context.canvas.width, image_context.canvas.height), 0, 0); }; test = function(draw) { /* This is the test. The time is recorded before and after the draw functions are called and the difference is displayed to the user. */ let count = 0; let start_time = window.performance.now(); while(count < number_of_tests) { count ++; draw(); } let accumulated_time = window.performance.now() - start_time; let data = "Number of tests (" + number_of_tests + "): " + accumulated_time + "ms
"; if (draw == drawImage) { ui.draw_image_output.innerHTML = data + ui.draw_image_output.innerHTML; averages[0].push(accumulated_time); let number = 0; for (let index = averages[0].length - 1; index > -1; -- index) { number += averages[0][index]; } number /= averages[0].length; ui.draw_image_average.innerHTML = (averages[0].length > 1) ? "Average (" + averages[0].length + " sets): " + number + " ms" : "Average (" + averages[0].length + " set): " + number + " ms"; } else if (draw == imageData) { ui.image_data_output.innerHTML = data + ui.image_data_output.innerHTML; averages[1].push(accumulated_time); let number = 0; for (let index = averages[1].length - 1; index > -1; -- index) { number += averages[1][index]; } number /= averages[1].length; ui.image_data_average.innerHTML = (averages[1].length > 1) ? "Average (" + averages[1].length + " sets): " + number + " ms" : "Average (" + averages[1].length + " set): " + number + " ms"; } display.imageSmoothingEnabled = false; display.drawImage(buffer.canvas, 0, 0, buffer.canvas.width, buffer.canvas.height, 0, 0, display.canvas.width, display.canvas.height); }; ui = { draw_image_average:document.getElementById("draw-image-average"), draw_image_button:document.getElementById("draw-image-button"), draw_image_output:document.getElementById("draw-image-output"), image_data_average:document.getElementById("image-data-average"), image_data_button:document.getElementById("image-data-button"), image_data_output:document.getElementById("image-data-output"), number_of_tests_input:document.getElementById("number-of-tests-input"), click:function(event) { switch(this) { case ui.draw_image_button: test(drawImage); break; case ui.image_data_button: test(imageData); break; } }, change:function(event) { let number = Number.parseInt(this.value); if (!isNaN(number)) { number_of_tests = number; this.value = "Number of tests: (" + number + ")"; } else if (this.value == "clear") { ui.draw_image_output.innerHTML = ui.image_data_output.innerHTML = "Number of tests (0): 0.0 ms"; ui.draw_image_average.innerHTML = ui.image_data_average.innerHTML = "Average(0 sets): 0.0 ms"; averages = [[], []]; this.value = "Number of tests (" + number_of_tests + ")"; } else { this.value = "Enter a valid integer"; } }, focusInOut:function(event) { switch(event.type) { case "focusin": this.value = ""; break; case "focusout": if (this.value == "") this.value = "Number of tests (" + number_of_tests + ")"; } } }; //// INITIALIZE //// image.addEventListener("load", function(event) { buffer.canvas.height = this.height; buffer.canvas.width = this.width; display.canvas.height = this.height * 2; display.canvas.width = this.width * 2; image_context.canvas.height = this.height; image_context.canvas.width = this.width; image_context.drawImage(this, 0, 0, this.width, this.height, 0, 0, this.width, this.height); ui.draw_image_button.addEventListener("click", ui.click); ui.image_data_button.addEventListener("click", ui.click); ui.number_of_tests_input.addEventListener("focusin", ui.focusInOut); ui.number_of_tests_input.addEventListener("focusout", ui.focusInOut); ui.number_of_tests_input.addEventListener("change", ui.change); ui.number_of_tests_input.value = "Number of tests (" + number_of_tests + ")"; }); image.src = "blit.png"; })(); ================================================ FILE: content/bouncing-polygons/bouncing-polygons.html ================================================ Bouncing Polygons ================================================ FILE: content/calculator/calculator.css ================================================ /* Frank Poth 12/29/2017 */ * { box-sizing:border-box; margin:0; padding:0; user-select:none; } html { --f1-color:#003399; --f2-color:#0066cc; --f3-color:#0099ff; height:100%; width:100%; } body { align-content:center; background-color:#202830; display:grid; grid-template-columns:100%; grid-template-rows:100%; justify-content:center; min-height:100%; width:100%; } #calculator { align-self:center; background-color:#999999; border-radius:16px 16px 8px 8px; display:grid; grid-column-gap:4px; grid-row-gap:4px; grid-template-areas:"screen screen screen screen screen" "q clr f1 f2 f3" "n1 n2 n3 cmd1 cmd2" "n4 n5 n6 cmd3 cmd4" "n7 n8 n9 cmd5 cmd6" "cma n0 prd cmd7 cmd8"; grid-template-columns:min-content min-content min-content min-content min-content; grid-template-rows:2fr 1fr 1fr 1fr 1fr 1fr; justify-self:center; max-width:100%; overflow:hidden; padding:4px; } #calculator-screen { align-items:center; background-color:#ffffff; border-radius:16px 16px 8px 8px; color:#202830; display:grid; font-size:2.0em; grid-area:screen; height:128px; overflow-y:auto; padding:4px 8px; text-align:center; white-space:wrap; word-wrap:break-word; word-break:break-all; user-select:text; } #calculator-q { background-color:#ff9900; grid-area:q; } #calculator-clr { background-color:#990000; grid-area:clr; } #calculator-f1 { background-color:var(--f1-color); grid-area:f1; } #calculator-f2 { background-color:var(--f2-color); grid-area:f2; } #calculator-f3 { background-color:var(--f3-color); grid-area:f3; } #calculator-0 { grid-area:n0; } #calculator-1 { grid-area:n1; } #calculator-2 { grid-area:n2; } #calculator-3 { grid-area:n3; } #calculator-4 { grid-area:n4; } #calculator-5 { grid-area:n5; } #calculator-6 { grid-area:n6; } #calculator-7 { grid-area:n7; } #calculator-8 { grid-area:n8; } #calculator-9 { grid-area:n9; } #calculator-cma { grid-area:cma; } #calculator-prd { grid-area:prd; } #calculator-plus { background-color:var(--f1-color); grid-area:cmd1; } #calculator-minus { background-color:var(--f1-color); grid-area:cmd2; } #calculator-multiply { background-color:var(--f1-color); grid-area:cmd3; } #calculator-divide { background-color:var(--f1-color); grid-area:cmd4; } #calculator-open-parenthesis { background-color:var(--f1-color); grid-area:cmd5; } #calculator-close-parenthesis { background-color:var(--f1-color); grid-area:cmd6; } #calculator-pi { background-color:var(--f2-color); grid-area:cmd1; display:none; } #calculator-pow { background-color:var(--f2-color); grid-area:cmd2; display:none; } #calculator-sqrt { background-color:var(--f2-color); grid-area:cmd3; display:none; } #calculator-ln { background-color:var(--f2-color); grid-area:cmd4; display:none; } #calculator-log { background-color:var(--f2-color); grid-area:cmd5; display:none; } #calculator-rnd { background-color:var(--f2-color); grid-area:cmd6; display:none; } #calculator-cos { background-color:var(--f3-color); grid-area:cmd1; display:none; } #calculator-sin { background-color:var(--f3-color); grid-area:cmd2; display:none; } #calculator-tan { background-color:var(--f3-color); grid-area:cmd3; display:none; } #calculator-acos { background-color:var(--f3-color); grid-area:cmd4; display:none; } #calculator-asin { background-color:var(--f3-color); grid-area:cmd5; display:none; } #calculator-atan { background-color:var(--f3-color); grid-area:cmd6; display:none; } #calculator-del { background-color:#990000; grid-area:cmd7; } #calculator-ans { background-color:#009900; grid-area:cmd8; } .calculator-button { align-items:center; background-color:#303840; border-radius:8px; color:#ffffff; cursor:pointer; display:grid; font-size:1.5em; height:64px; justify-items:center; width:64px; } .calculator-button:hover { background-color:#384048; } ================================================ FILE: content/calculator/calculator.html ================================================ Calculator
clr
f1
f2
f3
0
1
2
3
4
5
6
7
8
9
+
-
*
/
(
)
PI
pow(
sqrt(
ln(
log(
rnd()
cos(
sin(
tan(
acos(
asin(
atan(
.
,
ans
del
?
================================================ FILE: content/calculator/calculator.js ================================================ // Frank Poth 12/29/2017 (function() { "use strict"; const PI = Math.PI; const pow = Math.pow; const sqrt = Math.sqrt; const ln = Math.log; const log = Math.log10; const rnd = Math.random; const cos = Math.cos; const sin = Math.sin; const tan = Math.tan; const acos = Math.acos; const asin = Math.asin; const atan = Math.atan; var controller, resize, ui, update; controller = { active:false, state:false, value:"", click:function(event) { controller.active = true; controller.value = this.innerHTML; update(); }, keyPress:function(event) { if (!event.repeat) { controller.active = true; controller.value = String(event.key); } update(); } }; ui = { calculator:document.getElementById("calculator"), screen:document.getElementById("calculator-screen"), buttons: { "?":document.getElementById("calculator-q"), "clr":document.getElementById("calculator-clr"), "f1":document.getElementById("calculator-f1"), "f2":document.getElementById("calculator-f2"), "f3":document.getElementById("calculator-f3"), "0":document.getElementById("calculator-0"), "1":document.getElementById("calculator-1"), "2":document.getElementById("calculator-2"), "3":document.getElementById("calculator-3"), "4":document.getElementById("calculator-4"), "5":document.getElementById("calculator-5"), "6":document.getElementById("calculator-6"), "7":document.getElementById("calculator-7"), "8":document.getElementById("calculator-8"), "9":document.getElementById("calculator-9"), "+":document.getElementById("calculator-plus"), "-":document.getElementById("calculator-minus"), "/":document.getElementById("calculator-divide"), "*":document.getElementById("calculator-multiply"), "(":document.getElementById("calculator-open-parenthesis"), ")":document.getElementById("calculator-close-parenthesis"), "PI":document.getElementById("calculator-pi"), "pow(":document.getElementById("calculator-pow"), "sqrt(":document.getElementById("calculator-sqrt"), "cos(":document.getElementById("calculator-cos"), "sin(":document.getElementById("calculator-sin"), "tan(":document.getElementById("calculator-tan"), "acos(":document.getElementById("calculator-acos"), "asin(":document.getElementById("calculator-asin"), "atan(":document.getElementById("calculator-atan"), "ln(":document.getElementById("calculator-ln"), "log(":document.getElementById("calculator-log"), "rnd()":document.getElementById("calculator-rnd"), ",":document.getElementById("calculator-cma"), ".":document.getElementById("calculator-prd"), "del":document.getElementById("calculator-del"), "ans":document.getElementById("calculator-ans"), }, hitF1:function() { this.buttons["+"].style.display = "grid"; this.buttons["-"].style.display = "grid"; this.buttons["*"].style.display = "grid"; this.buttons["/"].style.display = "grid"; this.buttons["("].style.display = "grid"; this.buttons[")"].style.display = "grid"; this.buttons["PI"].style.display = "none"; this.buttons["pow("].style.display = "none"; this.buttons["sqrt("].style.display = "none"; this.buttons["ln("].style.display = "none"; this.buttons["log("].style.display = "none"; this.buttons["rnd()"].style.display = "none"; this.buttons["cos("].style.display = "none"; this.buttons["sin("].style.display = "none"; this.buttons["tan("].style.display = "none"; this.buttons["acos("].style.display = "none"; this.buttons["asin("].style.display = "none"; this.buttons["atan("].style.display = "none"; }, hitF2:function() { this.buttons["+"].style.display = "none"; this.buttons["-"].style.display = "none"; this.buttons["*"].style.display = "none"; this.buttons["/"].style.display = "none"; this.buttons["("].style.display = "none"; this.buttons[")"].style.display = "none"; this.buttons["cos("].style.display = "none"; this.buttons["sin("].style.display = "none"; this.buttons["tan("].style.display = "none"; this.buttons["acos("].style.display = "none"; this.buttons["asin("].style.display = "none"; this.buttons["atan("].style.display = "none"; this.buttons["PI"].style.display = "grid"; this.buttons["pow("].style.display = "grid"; this.buttons["sqrt("].style.display = "grid"; this.buttons["ln("].style.display = "grid"; this.buttons["log("].style.display = "grid"; this.buttons["rnd()"].style.display = "grid"; }, hitF3:function() { this.buttons["+"].style.display = "none"; this.buttons["-"].style.display = "none"; this.buttons["*"].style.display = "none"; this.buttons["/"].style.display = "none"; this.buttons["("].style.display = "none"; this.buttons[")"].style.display = "none"; this.buttons["PI"].style.display = "none"; this.buttons["pow("].style.display = "none"; this.buttons["sqrt("].style.display = "none"; this.buttons["ln("].style.display = "none"; this.buttons["log("].style.display = "none"; this.buttons["rnd()"].style.display = "none"; this.buttons["cos("].style.display = "grid"; this.buttons["sin("].style.display = "grid"; this.buttons["tan("].style.display = "grid"; this.buttons["acos("].style.display = "grid"; this.buttons["asin("].style.display = "grid"; this.buttons["atan("].style.display = "grid"; } }; resize = function(event) { let height = Math.floor(document.documentElement.clientHeight); let width = Math.floor(document.documentElement.clientWidth); ui.screen.style.maxWidth = ui.screen.clientWidth + "px"; }; update = function() { if (controller.active) { controller.active = false; switch(controller.value) { case "0": case "1": case "2": case "3": case "4": case "5": case "6": case "7": case "8": case "9": case "+": case "-": case "/": case "*": case "(": case ")": case "PI": case "pow(": case "sqrt(": case "ln(": case "log(": case "rnd()": case "cos(": case "sin(": case "tan(": case "acos(": case "asin(": case "atan(": case ",": case ".": ui.screen.innerHTML += controller.value; break; case "clr": ui.screen.innerHTML = ""; break; case "f1": ui.hitF1(); break; case "f2": ui.hitF2(); break; case "f3": ui.hitF3(); break; case "del": case "Delete": if (ui.screen.innerHTML.length > 0) ui.screen.innerHTML = ui.screen.innerHTML.slice(0, ui.screen.innerHTML.length - 1); break; case "Enter": case "ans": let answer = undefined; try { answer = parseFloat(eval(ui.screen.innerHTML).toPrecision(10)); } catch(error) { answer = error; } ui.screen.innerHTML = answer; break; case "?": ui.screen.innerHTML = "Help Menu

*Always close parenthesis.

pow(
Returns the base to the exponent power. Must be written in the form pow(base, exponent)

acos( & asin(
Return the arcCosine and arcTangent of a number in radians. The number must be between -1 and 1 or NaN will be returned.

ln( & log(
ln is base e. log is base 10.

rnd()
Returns a pseudo random number.

This is a Javascript calculator that evaluates input with the eval method. This application was written by Frank Poth."; } } }; //////////////////// //// INITIALIZE //// //////////////////// window.addEventListener("resize", resize); for (let property in ui.buttons) { ui.buttons[property].addEventListener("click", controller.click, { passive:true }); } window.addEventListener("keypress", controller.keyPress); resize(); })(); ================================================ FILE: content/canvas/canvas.css ================================================ /* Frank Poth 08/04/2017 */ * { box-sizing:border-box; margin:0; padding:0; } body, html { width:100%; height:100%; } body { align-content:space-around; background-color:#202430; color:#ffffff; display:grid; font-family:monospace; justify-items:center; padding:8px; } canvas { background-color:#ffffff; } ================================================ FILE: content/canvas/canvas.html ================================================ PoP Vlog - Canvas

PoP Vlog - Canvas

================================================ FILE: content/canvas/canvas.js ================================================ // Frank Poth 08/04/2017 var display = document.getElementById("display").getContext("2d"); display.canvas.height = 180; display.canvas.width = 320; display.fillStyle = "#008000"; display.fillRect(0, 0, 320, 180); display.strokeStyle = "#ffffff"; display.lineJoin = "round"; display.lineWidth = 4; display.fillStyle = "#00ff00"; display.beginPath(); display.moveTo(10, 10); display.lineTo(10, 90); display.lineTo(90, 10); display.closePath(); display.fill(); display.stroke(); display.beginPath(); display.moveTo(0, 180); display.bezierCurveTo(80, 0, 80, 180, 160, 90); display.bezierCurveTo(240, 0, 240, 180, 320, 0); display.stroke(); display.fillStyle = "#0000ff"; display.beginPath(); display.rect(180, 130, 40, 40); display.fill(); display.stroke(); display.fillStyle = "#ff0000"; display.beginPath(); display.arc(290, 150, 20, 0, Math.PI*2); display.fill(); display.stroke(); ================================================ FILE: content/circle-collision-detection/circle-collision-detection.html ================================================ Circle Collision Detection

Two circles collide when the distance between their center points is less than or equal to the sum of their radii. In this example, the distance between circles is represented by the white line. Its length is 0. the sum of the radii is 0. change radii

================================================ FILE: content/circle-collision-response/circle-collision-response.html ================================================ Circle Collision Response

To resolve collision between two circles, one of the circles must be "pushed" out of the other until the distance between their center points is greater than the sum of their radii. The circle should be moved out of collision along the vector between the center points of the two circles. Its length is 0. the sum of the radii is 0. change radii

================================================ FILE: content/collision/collision.css ================================================ /* Frank Poth 08/29/2017 */ * { box-sizing: border-box; margin:0; padding:0; } html, body { height:100%; width:100%; } body { align-content:space-around; background-color:#202830; color:#ffffff; display:grid; justify-items:center; } h1 { word-wrap:break-word; } ================================================ FILE: content/collision/collision.html ================================================ Collision

PoP Vlog - Square Collision

================================================ FILE: content/collision/collision.js ================================================ // Frank Poth 08/29/2017 var context, controller, Rectangle, red, white, loop, resize; context = document.querySelector("canvas").getContext("2d"); controller = { // mouse or finger position pointer_x:0, pointer_y:0, move:function(event) { // This will give us the location of our canvas element var rectangle = context.canvas.getBoundingClientRect(); // store the position of the move event inside the pointer variables controller.pointer_x = event.clientX - rectangle.left; controller.pointer_y = event.clientY - rectangle.top; } }; Rectangle = function(x, y, width, height, color) { this.x = x; this.y = y; this.width = width; this.height = height; this.color = color; }; Rectangle.prototype = { draw:function() {// draws rectangle to canvas context.beginPath(); context.rect(this.x, this.y, this.width, this.height); context.fillStyle = this.color; context.fill(); }, // get the four side coordinates of the rectangle get bottom() { return this.y + this.height; }, get left() { return this.x; }, get right() { return this.x + this.width; }, get top() { return this.y; }, testCollision:function(rectangle) { if (this.top > rectangle.bottom || this.right < rectangle.left || this.bottom < rectangle.top || this.left > rectangle.right) { return false; } return true; } }; red = new Rectangle(0, 0, 64, 64, "#ff0000"); white = new Rectangle(context.canvas.width * 0.5 - 32, context.canvas.height * 0.5 - 32, 64, 64, "#ffffff"); loop = function(time_stamp) { red.x = controller.pointer_x - 32; red.y = controller.pointer_y - 32; context.fillStyle = "#303840"; context.fillRect(0, 0, context.canvas.width, context.canvas.height); white.draw(); red.draw(); if (red.testCollision(white)) { context.beginPath(); context.rect(red.x, red.y, red.width, red.height); context.rect(white.x, white.y, white.width, white.height); context.strokeStyle = "#ffffff"; context.stroke(); } window.requestAnimationFrame(loop); }; // just keeps the canvas element sized appropriately resize = function(event) { context.canvas.width = document.documentElement.clientWidth - 32; if (context.canvas.width > document.documentElement.clientHeight) { context.canvas.width = document.documentElement.clientHeight; } context.canvas.height = Math.floor(context.canvas.width * 0.5625); white.x = context.canvas.width * 0.5 - 32; white.y = context.canvas.height * 0.5 - 32; }; context.canvas.addEventListener("mousemove", controller.move); context.canvas.addEventListener("touchmove", controller.move, {passive:true}); window.addEventListener("resize", resize, {passive:true}); resize(); // start the game loop window.requestAnimationFrame(loop); ================================================ FILE: content/control/control.css ================================================ /* Frank Poth 08/13/2017 */ * { box-sizing:border-box; margin:0; padding:0; } html, body { height:100%; width:100%; } body { align-content:space-around; background-color:#202830; color:#ffffff; display:grid; justify-content:center; } canvas { background-color:#ffffff; } ================================================ FILE: content/control/control.html ================================================ PoP Vlog - Control

PoP Vlog - Control

Use the keyboard to move the red square.

================================================ FILE: content/control/control.js ================================================ // Frank Poth 08/13/2017 var context, controller, rectangle, loop; context = document.querySelector("canvas").getContext("2d"); context.canvas.height = 180; context.canvas.width = 320; rectangle = { height:32, jumping:true, width:32, x:144, // center of the canvas x_velocity:0, y:0, y_velocity:0 }; controller = { left:false, right:false, up:false, keyListener:function(event) { var key_state = (event.type == "keydown")?true:false; switch(event.keyCode) { case 37:// left key controller.left = key_state; break; case 38:// up key controller.up = key_state; break; case 39:// right key controller.right = key_state; break; } } }; loop = function() { if (controller.up && rectangle.jumping == false) { rectangle.y_velocity -= 20; rectangle.jumping = true; } if (controller.left) { rectangle.x_velocity -= 0.5; } if (controller.right) { rectangle.x_velocity += 0.5; } rectangle.y_velocity += 1.5;// gravity rectangle.x += rectangle.x_velocity; rectangle.y += rectangle.y_velocity; rectangle.x_velocity *= 0.9;// friction rectangle.y_velocity *= 0.9;// friction // if rectangle is falling below floor line if (rectangle.y > 180 - 16 - 32) { rectangle.jumping = false; rectangle.y = 180 - 16 - 32; rectangle.y_velocity = 0; } // if rectangle is going off the left of the screen if (rectangle.x < -32) { rectangle.x = 320; } else if (rectangle.x > 320) {// if rectangle goes past right boundary rectangle.x = -32; } context.fillStyle = "#202020"; context.fillRect(0, 0, 320, 180);// x, y, width, height context.fillStyle = "#ff0000";// hex for red context.beginPath(); context.rect(rectangle.x, rectangle.y, rectangle.width, rectangle.height); context.fill(); context.strokeStyle = "#202830"; context.lineWidth = 4; context.beginPath(); context.moveTo(0, 164); context.lineTo(320, 164); context.stroke(); // call update when the browser is ready to draw again window.requestAnimationFrame(loop); }; window.addEventListener("keydown", controller.keyListener) window.addEventListener("keyup", controller.keyListener); window.requestAnimationFrame(loop); ================================================ FILE: content/cube/cube.html ================================================ Cube ================================================ FILE: content/dino/dino.css ================================================ /* Frank Poth 12/24/2017 */ * { box-sizing:border-box; margin:0; padding:0; user-select:none; } html { height:100%; width:100%; } body { align-content:space-around; background-color:#202830; color:#ffffff; display:grid; grid-template-columns:auto; grid-template-rows:auto auto auto; height:100%; justify-items:center; padding:0 8px; width:100%; } ================================================ FILE: content/dino/dino.html ================================================

PoP Vlog - Dino Run

Click or tap to jump!

================================================ FILE: content/dino/dino.js ================================================ // Frank Poth 12/24/2017 /* This example has a lot packed into it. It has a scrolling tile based background. The rightmost column is randomly generated when scrolling. There is animation. There is collision detection between all moving objects and the world as well as the player and the meteors and tarpits. There is an effect that turns the screen red when a meteor spawns using image data. I implement object pooling to avoid using "new" to create new objects. Some of this stuff I've covered in old tutorials, and some stuff I have not covered. */ (function() { "use strict"; const TILE_SIZE = 16; const WORLD_HEIGHT = 144; const WORLD_WIDTH = 256; //// CLASSES //// var Animation = function(frame_set, delay) { this.count = 0;// Counts the number of game cycles since the last frame change. this.delay = delay;// The number of game cycles to wait until the next frame change. this.frame_value = frame_set[0];// The value in the sprite sheet of the sprite image / tile to display. this.frame_index = 0;// The frame's index in the current animation frame set. this.frame_set = frame_set;// The current animation frame set that holds sprite tile values. }; Animation.prototype = { /* This changes the current animation frame set. For example, if the current set is [0, 1], and the new set is [2, 3], it changes the set to [2, 3]. It also sets the delay. */ change:function(frame_set, delay = 15) { if (this.frame_set != frame_set) {// If the frame set is different: this.count = 0;// Reset the count. this.delay = delay;// Set the delay. this.frame_index = 0;// Start at the first frame in the new frame set. this.frame_set = frame_set;// Set the new frame set. this.frame_value = this.frame_set[this.frame_index];// Set the new frame value. } }, /* Call this on each game cycle. */ update:function() { this.count ++;// Keep track of how many cycles have passed since the last frame change. if (this.count >= this.delay) {// If enough cycles have passed, we change the frame. this.count = 0;// Reset the count. /* If the frame index is on the last value in the frame set, reset to 0. If the frame index is not on the last value, just add 1 to it. */ this.frame_index = (this.frame_index == this.frame_set.length - 1) ? 0 : this.frame_index + 1; this.frame_value = this.frame_set[this.frame_index];// Change the current frame value. } } }; /* A frame just keeps track of a physical position inside the tile sheet for blitting. */ var Frame = function(x, y, width, height) { this.height = height; this.width = width; this.x = x; this.y = y; }; /* A Pool object manages objects. The objects array holds all objects that are currently in use, and the pool holds objects that are not in use. By storing objects that would otherwise be deleted, we can reuse them instead of creating totally new instances with the new operator. Recycling saves memory. Do it. */ var Pool = function(object) { this.object = object;// The constructor of the object we are pooling. this.objects = [];// The array of objects in use. this.pool = [];// The array of objects not in use. }; Pool.prototype = { /* Get an object from the pool or create a new object. Pool expects objects to have a few basic functions, like reset. */ get:function(parameters) { if (this.pool.length != 0) { let object = this.pool.pop(); object.reset(parameters); this.objects.push(object); } else { this.objects.push(new this.object(parameters.x, parameters.y)); } }, store:function(object) { let index = this.objects.indexOf(object); if (index != -1) { this.pool.push(this.objects.splice(index, 1)[0]); } }, storeAll:function() { for (let index = this.objects.length - 1; index > -1; -- index) { this.pool.push(this.objects.pop()); } } }; var Meteor = function(x, y) { this.alive = true;// Meteor dies when it goes offscreen. this.animation = new Animation(display.tile_sheet.frame_sets[1], 8); this.grounded = false; this.smoke = false;// smoke values are used for spawning smoke particles. this.smoke_count = 0; this.smoke_delay = Math.floor(Math.random() * 10 + 5); this.height = Math.floor(Math.random() * 16 + 24); this.width = this.height; this.x = x; this.y = y - this.height * 0.5; let direction = Math.PI * 1.75 + Math.random() * Math.PI * 0.1;// The trajectory. this.x_velocity = Math.cos(direction) * 3; this.y_velocity = -Math.sin(direction) * 3; }; /* All game objects are expected to have collideWorld and CollideObject functions, as well as update and reset functions. If this were a strongly typed language, I would be using a base class called GameObject or something. */ Meteor.prototype = { constructor:Meteor, collideObject:function(player) { let vector_x = player.x + player.width * 0.5 - this.x - this.width * 0.5; let vector_y = player.y + player.height * 0.5 - this.y - this.height * 0.5; let combined_radius = player.height * 0.5 + this.width * 0.5; if (vector_x * vector_x + vector_y * vector_y < combined_radius * combined_radius) { player.alive = false; player.animation.change(display.tile_sheet.frame_sets[5], 10); } }, collideWorld:function() { if (this.x + this.width < 0) { this.alive = false; return; } if (this.y + this.height > WORLD_HEIGHT - 6) { this.x_velocity = -game.speed; this.grounded = true; this.y = WORLD_HEIGHT - this.height - 6; } }, reset:function(parameters) { this.alive = true; this.animation.change(display.tile_sheet.frame_sets[1], 8); this.grounded = false; this.x = parameters.x; let direction = Math.PI * 1.75 + Math.random() * Math.PI * 0.1; this.x_velocity = Math.cos(direction) * 3; this.y = parameters.y; this.y_velocity = -Math.sin(direction) * 3; }, update:function() { if (!this.grounded) { this.animation.update(); this.y += this.y_velocity; } else { this.x_velocity = -game.speed; } this.x += this.x_velocity; this.smoke_count ++; if (this.smoke_count == this.smoke_delay) { this.smoke_count = 0; this.smoke = true; } } }; var Smoke = function(x, y, x_velocity, y_velocity) { this.alive = true; this.animation = new Animation(display.tile_sheet.frame_sets[2], 8); this.life_count = 0; this.life_time = Math.random() * 20 + 30; this.height = 8 + Math.floor(Math.random() * 8); this.width = this.height; this.x = x; this.y = y; this.x_velocity = x_velocity; this.y_velocity = y_velocity; }; Smoke.prototype = { constructor:Smoke, collideWorld:function() { if (this.x > WORLD_WIDTH || this.y > WORLD_HEIGHT - 20) { this.alive = false; } }, reset:function(parameters) { this.alive = true; this.life_count = 0; this.life_time = Math.random() * 20 + 30; this.x = parameters.x; this.x_velocity = parameters.x_velocity; this.y = parameters.y; this.y_velocity = parameters.y_velocity; }, update:function() { this.animation.update(); this.x += this.x_velocity; this.y += this.y_velocity; this.life_count ++; if (this.life_count > this.life_time) { this.alive = false; } } }; var TarPit = function(x, y) { this.alive = true; this.animation = new Animation(display.tile_sheet.frame_sets[0], 8); this.height = 30; this.width = Math.floor(Math.random() * 64 + 48); this.x = x; this.y = y; }; TarPit.prototype = { constructor:TarPit, collideObject:function(player) { }, collideObject:function(object) { if (!object.jumping && object.x + object.width * 0.5 > this.x + this.width * 0.2 && object.x + object.width * 0.5 < this.x + this.width * 0.8) { object.alive = false; object.animation.change(display.tile_sheet.frame_sets[4], 10); } }, collideWorld:function() { if (this.x + this.width < 0) this.alive = false; }, reset:function(parameters) { this.alive = true; this.width = Math.floor(Math.random() * 64 + 48); this.x = parameters.x; this.y = parameters.y; }, update:function(){ this.animation.update(); this.x -= game.speed; } }; var controller, display, game; /* This is awesome. I can use the same event handler for all mouseup, mousedown, touchstart, and touchend events. This controller works on everything! */ controller = { active:false, state:false, onOff:function(event) { event.preventDefault(); let key_state = (event.type == "mousedown" || event.type == "touchstart") ? true : false; if (controller.state != key_state) controller.active = key_state; controller.state = key_state; } }; display = { buffer:document.createElement("canvas").getContext("2d"), context:document.querySelector("canvas").getContext("2d"), tint:0,// The red tint value to add to the buffer's red channel when a meteor is on screen. tile_sheet: { columns:undefined,// Set up in INITIALIZE section. frames: [new Frame( 0, 32, 24, 16), new Frame(24, 32, 24, 16),// tar pit new Frame(64, 32, 16, 16), new Frame(80, 32, 16, 16),// Meteor new Frame(96, 32, 8, 8), new Frame(104,32, 8, 8), new Frame(96, 40, 8, 8), new Frame(104,40, 8, 8),// smoke new Frame( 0, 48, 28, 16), new Frame(28, 48, 28, 16), new Frame(56, 48, 28, 16), new Frame(84, 48, 28, 16), new Frame( 0, 64, 28, 16), new Frame(28, 64, 28, 16), new Frame(56, 64, 28, 16), new Frame(84, 64, 28, 16),//dino run new Frame( 0, 80, 28, 16), new Frame(28, 80, 28, 16), new Frame(56, 80, 28, 16), new Frame(84, 80, 28, 16), new Frame( 0, 96, 28, 16), new Frame(28, 96, 28, 16),//dino sink new Frame(56, 96, 28, 16), new Frame(84, 96, 28, 16), new Frame( 0,112, 28, 16), new Frame(28,112, 28, 16), new Frame(56,112, 28, 16), new Frame(84,112, 28, 16)//dino crisp ], frame_sets:[[ 0, 1],//tar pit [ 2, 3],//Meteor [ 4, 5, 6, 7],//smoke [ 8, 9,10,11,12,13,14,15],//dino run [16,17,18,19,20,21],//dino sink [22,23,24,25,26,27]//dino crisp ], image:new Image()// The tile sheet image is loaded at the bottom of this file. }, render:function() { // Draw Tiles for (let index = game.area.map.length - 1; index > -1; -- index) { let value = game.area.map[index]; this.buffer.drawImage(this.tile_sheet.image, (value % this.tile_sheet.columns) * TILE_SIZE, Math.floor(value / this.tile_sheet.columns) * TILE_SIZE, TILE_SIZE, TILE_SIZE, (index % game.area.columns) * TILE_SIZE - game.area.offset, Math.floor(index / game.area.columns) * TILE_SIZE, TILE_SIZE, TILE_SIZE); } // Draw distance this.buffer.font = "20px Arial"; this.buffer.fillStyle = "#ffffff"; this.buffer.fillText(String(Math.floor(game.distance/10) + " / " + Math.floor(game.max_distance/10)), 10, 20); // Draw TarPits for (let index = game.object_manager.tarpit_pool.objects.length - 1; index > -1; -- index) { let tarpit = game.object_manager.tarpit_pool.objects[index]; let frame = this.tile_sheet.frames[tarpit.animation.frame_value]; this.buffer.drawImage(this.tile_sheet.image, frame.x, frame.y, frame.width, frame.height, tarpit.x, tarpit.y, tarpit.width, tarpit.height); } // Draw Player let frame = this.tile_sheet.frames[game.player.animation.frame_value]; this.buffer.drawImage(this.tile_sheet.image, frame.x, frame.y, frame.width, frame.height, game.player.x, game.player.y, game.player.width, game.player.height); // Draw Meteors for (let index = game.object_manager.meteor_pool.objects.length - 1; index > -1; -- index) { let meteor = game.object_manager.meteor_pool.objects[index]; let frame = this.tile_sheet.frames[meteor.animation.frame_value]; this.buffer.drawImage(this.tile_sheet.image, frame.x, frame.y, frame.width, frame.height, meteor.x, meteor.y, meteor.width, meteor.height); } // Draw Smoke for (let index = game.object_manager.smoke_pool.objects.length - 1; index > -1; -- index) { let smoke = game.object_manager.smoke_pool.objects[index]; let frame = this.tile_sheet.frames[smoke.animation.frame_value]; this.buffer.drawImage(this.tile_sheet.image, frame.x, frame.y, frame.width, frame.height, smoke.x, smoke.y, smoke.width, smoke.height); } // Draw tint if a meteor is on screen if (game.object_manager.meteor_pool.objects.length != 0) { this.tint = (this.tint < 80) ? this.tint + 1 : 80; } else {// Reduce tint otherwise this.tint = (this.tint > 0) ? this.tint - 2 : 0; } if (this.tint != 0) {// If there is a tint to draw, apply it to the buffer let image_data = this.buffer.getImageData(0, 0, WORLD_WIDTH, WORLD_HEIGHT); let data = image_data.data; for (let index = data.length - 4; index > -1; index -= 4) { data[index] += this.tint; } this.buffer.putImageData(image_data, 0, 0); } this.context.drawImage(this.buffer.canvas, 0, 0, WORLD_WIDTH, WORLD_HEIGHT, 0, 0, this.context.canvas.width, this.context.canvas.height); }, resize:function(event) { display.context.canvas.width = document.documentElement.clientWidth - 16; if (display.context.canvas.width > document.documentElement.clientHeight - 16) { display.context.canvas.width = document.documentElement.clientHeight - 16; } display.context.canvas.height = display.context.canvas.width * 0.5625; display.buffer.imageSmoothingEnabled = false; display.context.imageSmoothingEnabled = false; display.render(); } }; game = { distance:0, max_distance:0, speed:3, area: { columns:17, offset:0, map:[ 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 2, 2, 2, 3, 2, 2, 3, 2, 4, 6, 7, 7, 6, 9, 2, 3, 2, 10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10], /* Takes care of scrolling the background and generating the next column to display on the far right of the map. */ scroll:function() { game.distance += game.speed; if (game.distance > game.max_distance) game.max_distance = game.distance; this.offset += game.speed; if (this.offset >= TILE_SIZE) { this.offset -= TILE_SIZE; /* This loop removes the first column and inserts a randomly generated last column for the top 7 rows. This handles random sky generation. */ for (let index = this.map.length - this.columns * 3 ; index > -1; index -= this.columns) { this.map.splice(index, 1); this.map.splice(index + this.columns - 1, 0, Math.floor(Math.random() * 2)); } /* This next part replaces the grass with an appropriate grass tile. I made it a bit more complex than it needed to be, but the tiles actually reconcile their edges with the tile directly to the left. */ this.map.splice(this.columns * 7, 1); let right_index = this.columns * 8 - 1; let value = this.map[right_index - 1]; switch(value) { case 2: case 3: value = [2, 3, 2, 3, 2, 3, 2, 3, 4, 5][Math.floor(Math.random() * 10)]; break; case 4: case 5: value = [6, 7][Math.floor(Math.random() * 2)]; break; case 6: case 7: value = [6, 7, 8, 9][Math.floor(Math.random() * 4)]; break; case 8: case 9: value = [2, 3][Math.floor(Math.random() * 2)]; break; } this.map.splice(right_index, 0, value); // The last row stays the same. It's just dirt. } } }, engine: { /* Fixed time step game loop!! */ afrequest:undefined,// animation frame request reference accumulated_time:window.performance.now(), time_step:1000/60,// update rate loop:function(time_stamp) { /* How easy does this look? This is a fixed step loop with frame dropping. Amazingly it's super simple and only a few lines. This will make your game run at the same speed on all devices. Now that I look at it, I think there may be a better way to implement this because entire frames can be dropped without updating or rendering. Rather than fixing this now, I will just leave it. Ideally, I would utilize the free time and not do both updates and renderings at the same time unless I have to... Another day... This does work fine, though. */ if (time_stamp >= game.engine.accumulated_time + game.engine.time_step) { if (time_stamp - game.engine.accumulated_time >= game.engine.time_step * 4) { game.engine.accumulated_time = time_stamp; } while(game.engine.accumulated_time < time_stamp) { game.engine.accumulated_time += game.engine.time_step; game.engine.update(); } display.render(); } window.requestAnimationFrame(game.engine.loop); }, start:function() {// Start the game loop. this.afrequest = window.requestAnimationFrame(this.loop); }, update:function() {// Update the game logic. /* Slowly increase speed and cap it when it gets too high. */ game.speed = (game.speed >= TILE_SIZE * 0.5)? TILE_SIZE * 0.5 : game.speed + 0.001; /* Make sure the player's animation delay is keeping up with the scroll speed. */ game.player.animation.delay = Math.floor(10 - game.speed); game.area.scroll();// Scroll!!! if (game.player.alive) { if (controller.active && !game.player.jumping) {// Get user input controller.active = false; game.player.jumping = true; game.player.y_velocity -= 15; game.player.animation.change([10], 15); } if (game.player.jumping == false) { game.player.animation.change(display.tile_sheet.frame_sets[3], Math.floor(TILE_SIZE - game.speed)); } game.player.update(); if (game.player.y > TILE_SIZE * 6 - TILE_SIZE * 0.25) {// Collide with floor controller.active = false; game.player.y = TILE_SIZE * 6 - TILE_SIZE * 0.25; game.player.y_velocity = 0; game.player.jumping = false; } } else { game.player.x -= game.speed; game.speed *= 0.9; if (game.player.animation.frame_index == game.player.animation.frame_set.length - 1) game.reset(); } game.player.animation.update(); game.object_manager.spawn(); game.object_manager.update(); } }, /* Manages all non player objects. */ object_manager: { count:0, delay:100, meteor_pool:new Pool(Meteor), smoke_pool:new Pool(Smoke), tarpit_pool:new Pool(TarPit), spawn:function() { this.count ++; if (this.count == this.delay) { this.count = 0; this.delay = 100;// + Math.floor(Math.random() * 200 - 10 * game.speed); /* Pick randomly between tarpits and meteors */ if (Math.random() > 0.5) { this.tarpit_pool.get( {x: WORLD_WIDTH, y:WORLD_HEIGHT - 30} ); } else { this.meteor_pool.get( {x: WORLD_WIDTH * 0.2, y: -32 } ); } } }, update:function() { for (let index = this.meteor_pool.objects.length - 1; index > -1; -- index) { let meteor = this.meteor_pool.objects[index]; meteor.update(); meteor.collideObject(game.player); meteor.collideWorld(); if (meteor.smoke) { meteor.smoke = false; let parameters = { x:meteor.x + Math.random() * meteor.width, y:undefined, x_velocity:undefined, y_velocity:undefined }; if (meteor.grounded) { parameters.y = meteor.y + Math.random() * meteor.height * 0.5; parameters.x_velocity = Math.random() * 2 - 1 - game.speed; parameters.y_velocity = Math.random() * -1; } else { parameters.y = meteor.y + Math.random() * meteor.height; parameters.x_velocity = meteor.x_velocity * Math.random(); parameters.y_velocity = meteor.y_velocity * Math.random(); } this.smoke_pool.get(parameters); } if (!meteor.alive) { this.meteor_pool.store(meteor); }; } for (let index = this.smoke_pool.objects.length - 1; index > -1; -- index) { let smoke = this.smoke_pool.objects[index]; smoke.update(); smoke.collideWorld(); if (!smoke.alive) this.smoke_pool.store(smoke); } for (let index = this.tarpit_pool.objects.length - 1; index > -1; -- index) { let tarpit = this.tarpit_pool.objects[index]; tarpit.update(); tarpit.collideObject(game.player); tarpit.collideWorld(); if (!tarpit.alive) this.tarpit_pool.store(tarpit); } } }, player: { alive:true, animation:new Animation([15], 10), jumping:false, height: 32, width: 56, x:8, y:TILE_SIZE * 6 - TILE_SIZE * 0.25, y_velocity:0, reset:function() { this.alive = true; this.x = 8; }, update:function() { game.player.y_velocity += 0.5; game.player.y += game.player.y_velocity; game.player.y_velocity *= 0.9; } }, reset:function() { this.distance = 0; this.player.reset(); /* Put all of our objects away. */ this.object_manager.meteor_pool.storeAll(); this.object_manager.smoke_pool.storeAll(); this.object_manager.tarpit_pool.storeAll(); this.speed = 3; } }; //////////////////// //// INITIALIZE //// //////////////////// display.buffer.canvas.height = WORLD_HEIGHT; display.buffer.canvas.width = WORLD_WIDTH; display.tile_sheet.image.src = "dino.png"; display.tile_sheet.image.addEventListener("load", function(event) { display.tile_sheet.columns = this.width / TILE_SIZE; display.resize(); game.engine.start(); }); window.addEventListener("resize", display.resize); window.addEventListener("mousedown", controller.onOff); window.addEventListener("mouseup", controller.onOff); window.addEventListener("touchstart", controller.onOff); window.addEventListener("touchend", controller.onOff); })(); ================================================ FILE: content/dominiques-doors/area0.json ================================================ { "message":"~The Tiny Closet~", "background_color":"#052623", "floor":62, "height":64, "width":128, "doors":[ { "area":"area1.json", "x":48, "new_x":112 } ] } ================================================ FILE: content/dominiques-doors/area1.json ================================================ { "message":"~The Main Lobby~", "background_color":"#003333", "floor":120, "height":128, "width":256, "doors":[ { "area":"area2.json", "x":48, "new_x":48 }, { "area":"area0.json", "x":112, "new_x":48 }, { "area":"area3.json", "x":176, "new_x":16 } ] } ================================================ FILE: content/dominiques-doors/area2.json ================================================ { "message":"~The Giant Pointless Room~", "background_color":"#006666", "floor":250, "height":256, "width":512, "doors":[ { "area":"area1.json", "x":48, "new_x":48 } ] } ================================================ FILE: content/dominiques-doors/area3.json ================================================ { "message":"~The Passageway~", "background_color":"#148977", "floor":60, "height":64, "width":200, "doors":[ { "area":"area1.json", "x":16, "new_x":176 } ] } ================================================ FILE: content/dominiques-doors/dominiques-doors.css ================================================ /* Frank Poth 01/14/2017 */ * { margin:0; padding:0; box-sizing:border-box; } html { height:100%; width:100%; } body { align-content:space-around; align-items:space-around; color:#ffffff; background-color:#202830; display:grid; grid-template-columns:auto; grid-template-rows:auto auto; justify-items:center; min-height:100%; width:100%; } h1 { font-size: 3.0em; text-align:center; } ================================================ FILE: content/dominiques-doors/dominiques-doors.html ================================================ PoP Vlog - Loading Levels!!!

PoP Vlog - Loading Levels!!!

================================================ FILE: content/dominiques-doors/dominiques-doors.js ================================================ // Frank Poth 01/14/2017 /* By studying this example program, you can learn how to load json levels, how to animate sprites, and other basic game design techniques including: user Input on the keyboard, some collision detection and response, image loading, and maybe some other things, too. */ /* You may notice that in the largest room the dominique sprite looks a little weird. There is a brown bar that flashes in front of her face when she walks to the left. This is because of something called "texture bleeding" where a scaled image allows pixels from around the cropped source region of the image to bleed into the desired part of the source image. This is okay for cropping from large images, but for sprite sheets it's not desireable. A way around this is to create individual canvases for each sprite image and use those to draw rather than cutting frames from the original sprite sheet. No bleeding can occur because there are no longer pixels around the edges of the sprite image. */ (function() { "use strict"; const Animation = function(frame_set, delay, mode = "loop") { this.count = 0; this.delay = delay; this.frame_index = 0; this.frame_set = frame_set; this.frame_value = frame_set[0]; this.mode = mode; }; /* I expanded the Animation class to include play, loop, and rewind modes. They're all really simple, and basically they are the same thing with very minor changes dictating how the playhead or frame_index moves. */ Animation.prototype = { constructor:Animation, change:function(frame_set, delay = this.delay) { if (frame_set != this.frame_set) { this.count = 0; this.delay = delay; this.frame_index = 0; this.frame_set = frame_set; this.frame_value = frame_set[0]; } }, loop:function() { this.count ++; if (this.count >= this.delay) { this.count = 0; this.frame_index = (this.frame_index < this.frame_set.length - 1) ? this.frame_index + 1 : 0; this.frame_value = this.frame_set[this.frame_index]; } }, play:function() { this.count ++; if (this.count >= this.delay) { this.count = 0; if (this.frame_index < this.frame_set.length - 1) { this.frame_index ++; this.frame_value = this.frame_set[this.frame_index]; } } }, rewind:function() { this.count ++; if (this.count >= this.delay) { this.count = 0; if (this.frame_index > 0) { this.frame_index --; this.frame_value = this.frame_set[this.frame_index]; } } }, update:function() { this[this.mode](); } }; /* I added offsets to the frames. This allows me to group my frames close together in the source image and save a lot of space in my image files. The offset is applied when drawing the image to the screen, ensuring that the sprite always looks centered and doesn't jump back and forth. */ const Frame = function(x, y, width, height, offset_x = 0, offset_y = 0) { this.height = height; this.offset_x = offset_x; this.offset_y = offset_y; this.width = width; this.x = x; this.y = y; }; Frame.prototype = { constructor:Frame }; /* This simplifies creation of input keys. */ const Input = function(active, state) { this.active = active; this.state = state; }; Input.prototype = { constructor:Input, update:function(state) { if (this.state != state) this.active = state; this.state = state; } }; ////////////////////// //// GAME CLASSES //// ////////////////////// const Door = function(x, y, area, new_x) { this.animation = new Animation(display.sprite_sheet.frame_set.door, 5, "play"); this.area = area; this.new_x = new_x; this.x = x; this.y = y; }; Door.prototype = { constructor:Door, }; /////////////// //// LOGIC //// /////////////// var controller, display, game; controller = { down: new Input(false, false), left: new Input(false, false), right:new Input(false, false), up:new Input(false, false), keyDownUp:function(event) { event.preventDefault(); var key_state = (event.type == "keydown") ? true : false; switch(event.keyCode) { case 37: controller.left.update(key_state); break;// left key case 38: controller.up.update(key_state); break;// up key case 39: controller.right.update(key_state); break;// right key case 40: controller.down.update(key_state); break;// down key } } }; display = { buffer:document.createElement("canvas").getContext("2d"), context:document.querySelector("canvas").getContext("2d"), height_width_ratio:undefined, sprite_sheet: { frames:[new Frame( 0, 0, 27, 30), new Frame( 27, 0, 25, 30, 1), new Frame( 52, 0, 19, 29, -1, 1), new Frame( 71, 0, 19, 30, -1), new Frame(90, 0, 18, 30), new Frame(108, 0, 18, 31, 0, -1), new Frame(126, 0, 18, 30, 1), new Frame(144, 0, 18, 31, 1, -1), new Frame(162, 0, 19, 29, 2), new Frame(181, 0, 19, 30, 2), new Frame(200, 0, 32, 32), new Frame(232, 0, 32, 32), new Frame(264, 0, 32, 32), new Frame(296, 0, 32, 32), new Frame(328, 0, 32, 32), new Frame(360, 0, 32, 32), new Frame(392, 0, 32, 32)], frame_set: { dominique_idle:[0, 1], dominique_right:[2, 3, 4, 5], dominique_left:[6, 7, 8, 9], door:[10, 11, 12, 13, 14, 15, 16] }, image:new Image() }, render:function() { var frame; /* Draw the background. */ this.buffer.fillStyle = game.area.background_color; this.buffer.fillRect(0, 0, game.area.width, game.area.height); /* Draw the floor. */ this.buffer.fillStyle = "#373641"; this.buffer.fillRect(0, game.area.floor - 3, game.area.width, game.area.height - game.area.floor + 3); /* Draw the doors. */ for (let index = game.area.doors.length - 1; index > -1; -- index) { let door = game.area.doors[index]; frame = this.sprite_sheet.frames[door.animation.frame_value]; this.buffer.drawImage(this.sprite_sheet.image, frame.x, frame.y, frame.width, frame.height, door.x, door.y, frame.width, frame.height); } /* Draw Dominique. */ frame = this.sprite_sheet.frames[game.dominique.animation.frame_value]; this.buffer.drawImage(this.sprite_sheet.image, frame.x, frame.y, frame.width, frame.height, Math.round(game.dominique.x) + frame.offset_x * 0.5 - frame.width * 0.5, Math.round(game.dominique.y) + frame.offset_y * 0.5 - frame.height * 0.5, frame.width, frame.height); this.context.drawImage(this.buffer.canvas, 0, 0, game.area.width, game.area.height, 0, 0, this.context.canvas.width, this.context.canvas.height); this.context.fillStyle = "#ffffff"; this.context.font = "20px Arial"; this.context.fillText(game.area.message, 10, 20); }, resize:function(event) { display.context.canvas.width = document.documentElement.clientWidth - 16; if (display.context.canvas.width > document.documentElement.clientHeight - 16) { display.context.canvas.width = document.documentElement.clientHeight - 16; } display.context.canvas.height = display.context.canvas.width * display.height_width_ratio; display.buffer.imageSmoothingEnabled = display.context.imageSmoothingEnabled = false; display.render(); } }; game = { area:undefined, dominique: { animation:new Animation(display.sprite_sheet.frame_set.dominique_idle, 15), half_height:15, half_width:10, jumping:false, velocity_x:0, velocity_y:0, x:100, y:100, collideWorld:function() { if (this.x - this.half_width < 0) { this.x = this.half_width; } else if (this.x + this.half_width > game.area.width) { if (game.area.message != "~The Passageway~") { this.x = game.area.width - this.half_width; } else if (this.x - this.half_width > game.area.width) { game.engine.stop(); controller.right.active = false; game.loadArea("area0.json", function() { game.reset(); }); alert("Dominique escaped the weird program full of pointless rooms and doors. She went to a much better, more interesting program."); } } if (this.y + this.half_height > game.area.floor) { this.jumping = false; this.velocity_y = 0; this.y = game.area.floor - this.half_height; } }, update:function() { this.velocity_y += 0.5; this.x += this.velocity_x; this.y += this.velocity_y; this.velocity_x *= 0.9; this.velocity_y *= 0.9; } }, engine: { accumulated_time:window.performance.now(), frame_request:undefined, time_step:1000/60, loop:function(time_stamp) { game.engine.frame_request = window.requestAnimationFrame(game.engine.loop); if (controller.left.active) { game.dominique.animation.change(display.sprite_sheet.frame_set.dominique_left, 15); game.dominique.velocity_x -= 0.1; } if (controller.right.active) { game.dominique.animation.change(display.sprite_sheet.frame_set.dominique_right, 15); game.dominique.velocity_x += 0.1; } if (!controller.left.active && !controller.right.active) { game.dominique.animation.change(display.sprite_sheet.frame_set.dominique_idle, 15); } if (controller.up.active && !game.dominique.jumping) { controller.up.active = false; game.dominique.jumping = true; game.dominique.velocity_y = -5; } game.dominique.update(); game.dominique.collideWorld(); game.dominique.animation.update(); for (let index = game.area.doors.length - 1; index > -1; -- index) { let door = game.area.doors[index]; if (game.dominique.x > door.x && game.dominique.x < door.x + 32) { door.animation.mode = "play"; if (controller.down.active) { controller.down.active = false; game.dominique.x = door.new_x + 1; game.loadArea(door.area, game.reset); return; } } else { door.animation.mode = "rewind"; } game.area.doors[index].animation.update(); } display.render(); }, start:function() { this.accumulated_time = window.performance.now(); this.frame_request = window.requestAnimationFrame(this.loop); }, stop:function() { window.cancelAnimationFrame(this.frame_request); } }, loadArea:function(url, callback) { var request, readyStateChange; request = new XMLHttpRequest(); readyStateChange = function(event) { if (this.readyState == 4 && this.status == 200) { game.area = JSON.parse(this.responseText); callback(); game.engine.start(); } }; request.addEventListener("readystatechange", readyStateChange); request.open("GET", url); request.send(null); game.engine.stop(); }, reset:function() { for (let index = game.area.doors.length - 1; index > -1; -- index) { let door = game.area.doors[index]; game.area.doors[index] = new Door(door.x, game.area.floor - 32 - 3, door.area, door.new_x); } game.dominique.y = game.area.floor - game.dominique.half_height; game.dominique.velocity_x = 0; display.buffer.canvas.height = game.area.height; display.buffer.canvas.width = game.area.width; display.height_width_ratio = game.area.height / game.area.width; display.resize(); } }; //////////////////// //// INITIALIZE //// //////////////////// display.sprite_sheet.image.addEventListener("load", function(event) { game.loadArea("area0.json", function() { game.reset(); }); }); display.sprite_sheet.image.src = "dominiques-doors.png"; window.addEventListener("resize", display.resize); window.addEventListener("keydown", controller.keyDownUp); window.addEventListener("keyup", controller.keyDownUp); })(); ================================================ FILE: content/elements/elements.html ================================================

PoP Vlog

default

================================================ FILE: content/elements/elements.js ================================================ // Frank Poth 08/03/2017 var input = document.getElementById("input"); var output = document.getElementById("output"); var counter = 0; var click = function(event) { counter += 1; output.innerHTML = "You clicked me " + counter + " times!"; }; input.addEventListener("click", click); ================================================ FILE: content/gjk/gjk.html ================================================ GJK Collision ================================================ FILE: content/hello-world/hello.html ================================================ Hello, world!

Hello, world!

================================================ FILE: content/hit-the-wall/hit-the-wall.css ================================================ /* Frank Poth 11/16/2017 */ * { box-sizing:border-box; margin:0; padding:0; } html { height:100%; width:100%; } body { align-content:space-around; background-color:#202830; color:#ffffff; display:grid; grid-template-columns:auto; grid-template-rows:auto auto auto auto; height:100%; justify-content:space-around; text-align:center; width:100%; } canvas { background-color:#303840; justify-self:center; } ================================================ FILE: content/hit-the-wall/hit-the-wall.html ================================================ PoP Vlog - Hit The Wall

PoP Vlog - Hit The Wall

 

This example requires a keyboard.

================================================ FILE: content/hit-the-wall/hit-the-wall.js ================================================ // Frank Poth 11/16/2017 (function() { "use strict" // the three main components of the example var controller, display, game; // a basic controller object that handles key input controller = { left:false, right:false, up:false, keyUpDown:function(event) { var key_state = (event.type == "keydown")?true:false; switch(event.keyCode) { case 37: controller.left = key_state; break; // left key case 38: controller.up = key_state; break; // up key case 39: controller.right = key_state; break; // right key } } }; // draws everything and handles html elements display = { buffer:document.createElement("canvas").getContext("2d"), context:document.querySelector("canvas").getContext("2d"), output:document.querySelector("p"), render:function() { for (let index = game.world.map.length - 1; index > -1; -- index) { this.buffer.fillStyle = (game.world.map[index] > 0)?("#0099" + game.world.map[index] + "f"):"#303840"; this.buffer.fillRect((index % game.world.columns) * game.world.tile_size, Math.floor(index / game.world.columns) * game.world.tile_size, game.world.tile_size, game.world.tile_size); } this.buffer.fillStyle = game.player.color; this.buffer.fillRect(game.player.x, game.player.y, game.player.width, game.player.height); this.context.drawImage(this.buffer.canvas, 0, 0, this.buffer.canvas.width, this.buffer.canvas.height, 0, 0, this.context.canvas.width, this.context.canvas.height); }, resize:function(event) { var client_height = document.documentElement.clientHeight; display.context.canvas.width = document.documentElement.clientWidth - 32; if (display.context.canvas.width > client_height) { display.context.canvas.width = client_height; } display.context.canvas.height = Math.floor(display.context.canvas.width * 0.625); display.render(); } }; // here's where things get interesting // the game object holds all of our awesome game logic game = { // there's something different about the player object, and its old_x and // old_y. these variables keep track of the last position the player occupied player: { color:"#ff9900", height:32, jumping:false, old_x:160,// used for tracking last position for collision detection old_y:160, velocity_x:0, velocity_y:0, width:24, x:160, y:90 }, // the world object holds information about our tile map world: { columns:8,// just some basic info. no worries rows:5, tile_size:40, map:[0,0,0,0,0,0,0,0,// I went with a smaller map this time 0,0,0,0,0,0,0,0,// 0s represent walkable tiles and everything else 1,0,0,0,0,0,0,2,// represents a collision tile or wall tile 3,0,0,4,0,0,2,5,// the different numbers correspond to different 5,5,5,5,5,5,5,5]// collision shapes. }, // the collision object is used to handle narrow phase collision detection // and response. Broad phase collision detection is just determining if the // player is standing on a non 0 tile in the map, narrow phase is handled // here, where the player's exact position is tested against the specific // collision boundaries of each type of tile. In this example there are 5 // types of collision tile collision: { // the reason these functions are indexed with numbers is because they // correspond directly to tile values in the map array. For instance, this // function handles collision for any tile in the map with a value of 1 // it handles collision detection and response on the specified player object // at the specified row and column in the tile map. the 1 tile collision // shape has a flat top and a flat right side, so only test for collision // and do response on those sides. 1:function(object, row, column) { /* Basically these indexed functions are like routing functions. They make it easy to connect a collision shape with a value in the tile map. The real collision code happens in these inner functions. The great thing about this approach is that it's extremely modular, so you can mix and match collision functions and easily introduce new ones. */ /* Depending on whether you want to do y or x first collision is as simple as calling the collision function for a specific axis before the other. In a platformer where players are always falling down due to gravity, it's a good idea to do y collision detection first. */ if (this.topCollision(object, row)) { return; }// if no top collision this.rightCollision(object, column); // try right side collision }, // the 2 tile type has a top and a left side to collide with 2:function(object, row, column) { if (this.topCollision(object, row)) { return; } this.leftCollision(object, column); }, // the 3 tile type only blocks movement through the right side 3:function(object, row, column) { this.rightCollision(object, column); }, // the 4 tile type has collision on all sides but the bottom 4:function(object, row, column) { if (this.topCollision(object, row)) { return; }// you only want to do one if (this.leftCollision(object, column)) { return; }// of these collision this.rightCollision(object, column);// responses. that's why there are if statements }, // the 5 tile only does collision if the object falls through the top 5:function(object, row, column) { this.topCollision(object, row); }, /* Here are the actual collision detection and response functions. The concept I am working off of with this example is that all tiles have 4 sides. Rather than doing collision for all of them in the same function, I want to mix and match collision methods for each side. By doing it this way I can create all sorts of collision shapes and resuse my side specific collision code. This works extremely well, and the only downside is having to be more specific when you lay out your level maps because each tile has a specific purpose. For example, a floor tile only tests for collision on its top side and a wall tile will only test for collision on the left and/or right side. It is a small price to pay for awesome and smooth collision detection with basically no restriction on tile based collision shapes. You can have tiles where one side is a slope and the other is a curve or a flat side, for instance. That's pretty sweet. */ /* This function handles collision with a rightward moving object. Another design requirement to use this method is that objects must have a record of their current and last physical position. A collision can only occur when a player enters into a collision shape through its boundary. It's foolproof so long as you always spawn your players on empty tiles and not in the walls. */ leftCollision(object, column) { if (object.velocity_x > 0) {// If the object is moving right var left = column * game.world.tile_size;// calculate the left side of the collision tile if (object.x + object.width * 0.5 > left && object.old_x <= left) {// If the object was to the right of the collision object, but now is to the left of it object.velocity_x = 0;// Stop moving object.x = object.old_x = left - object.width * 0.5 - 0.001;// place object outside of collision // the 0.001 is just to ensure that the object is no longer in the same tile space as the collision tile // due to the way object tile position is calculated, moving the object to the exact boundary of the collision tile // would not move it out if its tile space, meaning that another collision with an adjacent tile might not be detected in another broad phase check return true; } } return false; }, // these are all basically the same concept as the leftCollision function, // only for the different sides. rightCollision(object, column) { if (object.velocity_x < 0) { var right = (column + 1) * game.world.tile_size; if (object.x + object.width * 0.5 < right && object.old_x + object.width * 0.5 >= right) { object.velocity_x = 0; object.old_x = object.x = right - object.width * 0.5; return true; } } return false; }, topCollision(object, row) { if (object.velocity_y > 0) { var top = row * game.world.tile_size; if (object.y + object.height > top && object.old_y + object.height <= top) { object.jumping = false; object.velocity_y = 0; object.old_y = object.y = top - object.height - 0.01; return true; } } return false; } }, // Here's the game loop, where it all goes down!!! loop:function() { // get and use keyboard input if (controller.left) { game.player.velocity_x -= 0.25; } if (controller.right) { game.player.velocity_x += 0.25; } if (controller.up && !game.player.jumping) { game.player.velocity_y = -16; game.player.jumping = true; } game.player.velocity_y += 1; // add gravity game.player.old_x = game.player.x;// store the last position of the player game.player.old_y = game.player.y;// before we move it on this cycle game.player.x += game.player.velocity_x;// move the player's current position game.player.y += game.player.velocity_y; // do collision detection with the level boundaries so the player can't leave // the screen. Nothing you haven't seen before... if (game.player.x < 0) { game.player.velocity_x = 0; game.player.old_x = game.player.x = 0; } else if (game.player.x + game.player.width > display.buffer.canvas.width) { game.player.velocity_x = 0; game.player.old_x = game.player.x = display.buffer.canvas.width - game.player.width; } if (game.player.y < 0) { game.player.velocity_y = 0; game.player.old_y = game.player.y = 0; } else if (game.player.y + game.player.height > display.buffer.canvas.height) { game.player.velocity_y = 0; game.player.old_y = game.player.y = display.buffer.canvas.height - game.player.height; } // NOW FOR SOME GOOD STUFF!!! Here we do broadphase collision detection by checking // which tile value the player is standing on. If it is anything but 0 we have a possible collision. // calculate the player's x and y tile position in the tile map var tile_x = Math.floor((game.player.x + game.player.width * 0.5) / game.world.tile_size); var tile_y = Math.floor((game.player.y + game.player.height) / game.world.tile_size); // get the value at the tile position in the map var value_at_index = game.world.map[tile_y * game.world.columns + tile_x]; // do some output so we can see it all in action display.output.innerHTML = "tile_x: " + tile_x + "
tile_y: " + tile_y + "
map index: " + tile_y + " * " + game.world.columns + " + " + tile_x + " = " + String(tile_y * game.world.columns + tile_x) + "
tile value: " + game.world.map[tile_y * game.world.columns + tile_x]; // if it's not an empty tile, we need to do narrow phase collision detection and possibly response! if (value_at_index != 0) { // simply call one of the routing functions in the collision object and pass // in values for the collision tile's location in grid/map space game.collision[value_at_index](game.player, tile_y, tile_x); } // This might be confusing at first, but we actually have to do this twice, since // we are using a player object with a single point of contact, which is its bottom middle. // say we handled a collision with the floor in the check above and now the player object is // moved up above that tile. There's still a posibility that the player was moved into // an adjacent wall tile! We need to recalculate the new tile space the player is in // and then check to see if there is another collision. Don't worry, we only need // to do this twice. Once for the x axis, and one for the y axis. With this // collision method they could happen in either order, but they will both be handled. tile_x = Math.floor((game.player.x + game.player.width * 0.5) / game.world.tile_size); tile_y = Math.floor((game.player.y + game.player.height) / game.world.tile_size); value_at_index = game.world.map[tile_y * game.world.columns + tile_x]; if (value_at_index != 0) { game.collision[value_at_index](game.player, tile_y, tile_x); } // and that's it! You checked twice and resolved any collisions with the map! game.player.velocity_x *= 0.9;// apply some friction to the player's velocity game.player.velocity_y *= 0.9;// the reason we do this after the collision code // is because we use the players velocity in the collision code and don't want to change it before now. // it probably doesn't matter, though. You could also calculate the velocity for collision by // subtracting the players last position from its current position, that never fails. display.render(); window.requestAnimationFrame(game.loop); } }; // basic setup and initialization display.buffer.canvas.height = 200; display.buffer.canvas.width = 320; window.addEventListener("resize", display.resize); window.addEventListener("keydown", controller.keyUpDown); window.addEventListener("keyup", controller.keyUpDown); display.resize(); game.loop(); })(); ================================================ FILE: content/hitbox/hitbox.html ================================================ Hitbox freeze ================================================ FILE: content/https-server/index.css ================================================ /* Frank Poth 10/16/2017 */ * { box-sizing:border-box; margin:0; padding:0; } html { height:100%; width:100%; } body { align-content:center; background-color:#202830; color:#ffffff; display:grid; height:100%; justify-content:space-around; text-align:center; width:100%; } ================================================ FILE: content/https-server/index.html ================================================ HTTPS Server!

PoP Vlog - HTTPS Server!


Is there a green https indicator in your address bar? If so, this page was served over a secure server using a valid and up to date certificate chain!

================================================ FILE: content/https-server/server.js ================================================ // Frank Poth 10/16/2017 (function() { var fs, https, mimetypes, options, path, server; fs = require("fs"); // file system https = require("https"); // creates an https server path = require("path"); // used for working with url paths // used to create response headers /* If the user requests a .css file, we want to ensure we attach "text/css" to our response header, this way the browser knows how to handle it. */ mimetypes = { "css":"text/css", "html":"text/html", "ico":"image/ico", "jpg":"image/jpeg", "js":"text/javascript", "json":"application/json", "png":"image/png" }; options = { pfx: fs.readFileSync("ssl/crt.pfx"), passphrase: "password" }; // Start a secure server that uses the credentials in ssl/crt.pfx server = https.createServer(options, function(request, response) { /* When requesting the homepage of a website, we usually only type www.mysite.com, but the server returns www.mysite.com/index.html. To make it easier for users to access our site, we add "/index.html" to their url so the user doesn't have to type out the whole address of our home page. */ // If the url is empty if (request.url == "" || request.url == "/") { // The user is requesting the home page of the website, so give it to them request.url = "index.html"; } // Next we read the file at the requested url and write it to the document. /* __dirname is just the base directory of your website, so if your website is www.coolsite.com, then __dirname is www.coolsite.com. When you put it all together it looks like www.coolsite.com/index.html or whatever the requested url is */ fs.readFile(__dirname + "/" + request.url, function(error, content) { if (error) { // if there is an error reading the requested url console.log("Error: " + error); // output it to the console } else { // else, there is no error, write the file contents to the page // 200 is code for OK, and the second parameter is our content header response.writeHead(200, {'Content-Type':mimetypes[path.extname(request.url).split(".")[1]]}); response.write(content); // write that content to our response object } response.end(); // This will send our response object to the browser }); }); server.listen("2468", "192.168.0.101", function() { console.log("Server started!"); }); // listen on 192.168.0.101:2468 })(); ================================================ FILE: content/https-server/ssl/crt.cnf ================================================ [req] days = 180 serial = 1 distinguished_name = req_distinguished_name x509_extensions = v3_ca [req_distinguished_name] countryName = Country stateOrProvinceName = State localityName = Locality organizationName = Organizational Name organizationalUnitName = Organizational Unit Name commonName = Common Name (Domain Name) emailAddress = Email Address [ v3_ca ] # The extentions to add to a self-signed cert subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer:always # THIS IS VERY IMPORTANT IF YOU WANT TO USE THIS CERTIFICATION AS AN AUTHORITY!!! basicConstraints = CA:TRUE keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement, keyCertSign subjectAltName = DNS:192.168.0.101, IP:192.168.0.101 issuerAltName = issuer:copy ================================================ FILE: content/https-server/ssl/csr.cnf ================================================ [req] days = 180 distinguished_name = req_distinguished_name req_extensions = v3_req [req_distinguished_name] countryName = Country stateOrProvinceName = State localityName = Locality organizationName = Organizational Name organizationalUnitName = Organizational Unit Name commonName = Common Name (Domain Name) emailAddress = Email Address [ v3_req ] # Extensions to add to a certificate request basicConstraints = CA:FALSE keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement, keyCertSign ================================================ FILE: content/https-server/ssl/make-crt.sh ================================================ #!/bin/bash echo "" echo "Generate a self signed certificate:" echo "Enter certificate name (example: crt.crt):" read crt_path echo "Enter path to certificate configuration file (example: crt.cnf):" read cnf_path echo "Enter path to private key file (example: key.key):" read key_path echo "Enter path to CSR file (example: csr.csr):" read csr_path echo "Enter number of days until expiration (example: 365):" read days echo "" # certificate extensions must be stored in the v3_ca section in the configuration file if openssl x509 -req -days $days -in $csr_path -signkey $key_path -out $crt_path -extfile $cnf_path -extensions v3_ca then echo "" echo "Created "$crt_path" in "$PWD"/"$crt_path else echo "" echo "Could not generate certificate due to error!" fi echo "" ================================================ FILE: content/https-server/ssl/make-csr.sh ================================================ #!/bin/bash echo "" echo "Generate a Certificate Signing Request (CSR) file:" echo "Enter CSR name (example: csr.csr):" read csr_path echo "Enter path to CSR configuration file (example: csr.cnf):" read cnf_path echo "Enter path to private key file (example: key.key):" read key_path echo "" if openssl req -config $cnf_path -new -key $key_path -inform PEM -out $csr_path -outform PEM then echo "" echo "Created "$csr_path" in "$PWD"/"$csr_path else echo "" echo "Could not generate CSR due to error!" fi echo "" ================================================ FILE: content/https-server/ssl/make-key.sh ================================================ #!/bin/bash echo "" echo "Generate a private Key file in PEM format:" echo "Enter key name (example: key.key):" read key_path echo "" if openssl genpkey -algorithm RSA -out $key_path -outform PEM -pkeyopt rsa_keygen_bits:4096 then echo "" echo "Created "$key_path" in "$PWD"/"$key_path else echo "" echo "Could not create "$key_path" due to error!" fi echo "" ================================================ FILE: content/https-server/ssl/make-pfx.sh ================================================ #!/bin/bash echo "" echo "Generate a Public Key Cryptography Standards # 12 ( PKCS#12 ) file:" echo "Enter certificate name (example: crt.pfx):" read pfx_path echo "Enter path to private key file (example: key.key):" read key_path echo "Enter path to certificate file (example: crt.crt):" read crt_path echo "" if openssl pkcs12 -export -out $pfx_path -inkey $key_path -in $crt_path then echo "" echo "Created "$pfx_path" in "$PWD"/"$pfx_path else echo "" echo "Could not create "$pfx_path" due to error!" fi echo "" ================================================ FILE: content/indexed-db/index.css ================================================ /* Frank Poth 11/08/2017 */ * { box-sizing:border-box; margin:0; padding:0; } html { height:100%; width:100%; } body { align-content:space-around; background-color:#ffffff; color:#ffffff; display:grid; justify-content:space-around; justify-items:space-around; min-height:100%; padding:8px; text-align:center; width:100%; } #button-container { align-content:space-around; display:grid; grid-template-columns:auto auto auto auto; grid-template-rows:auto; justify-content:space-around; padding:16px 0; width:100%; } a { align-content:center; border-color:#c3daed; border-radius:8px; border-style:solid; border-width:2px; cursor:pointer; display:grid; font-size:1.5em; font-weight:800; height:64px; justify-content:center; user-select:none; width:64px; } p { justify-self:center; font-size:1.1em; max-width:320px; text-align:justify; } span { font-weight:800; font-size:1.1em; } ================================================ FILE: content/indexed-db/index.html ================================================ Indexed DB Test

PoP Vlog
Indexed DB

Welcome to the example! Click on a button to change the background color, and Indexed DB will save your color choice to your browser cache. When you refresh the page, you will be greeted by the color you chose!

R G B X

This example also keeps track of the number of button presses, which is 0, and the last time you visited the page, which was .

================================================ FILE: content/inheritance/inheritance.html ================================================ Inheritance ================================================ FILE: content/inheritance/inheritance.js ================================================ // Frank Poth 08/02/2017 function Human(name) { this.name = name; } function Worker(name, job) { Human.call(this, name); this.job = job; } var human = new Human("Tim"); var worker = new Worker("John", "desk jockey"); console.log(human.name); console.log(worker.name + ", " + worker.job); ================================================ FILE: content/inventory/inventory.html ================================================ Inventory ================================================ FILE: content/ipo/components/input.js ================================================ // Frank Poth 03/06/2018 /* The input class handles everything to do with user input. */ const Input = function(update) { this.handleClick = function(event) { update(); }; }; Input.prototype = { constructor:Input }; ================================================ FILE: content/ipo/components/main.js ================================================ // Frank Poth 03/07/2018 /* This is the main file where all of the different components of the application come together. Since this application is extremely simple, using MVC or IPO is probably unnecessary. There are many ways to implement this approach, and MVC gets a bit more involved than simply separating an application into three parts, but at the core of the concept is organization, so if you are organizing your application even loosely into the three basic components, you are utilizing MVC */ (function() { function update() { output.renderColor(processor.getRandomColor()); } var input = new Input(update); var processor = new Processor(); var output = new Output(document.body); window.addEventListener("click", input.handleClick); })(); ================================================ FILE: content/ipo/components/output.js ================================================ // Frank Poth 03/06/2018 /* The output class handles everything to do with displaying graphics. */ const Output = function(element) { this.element = element; this.renderColor = function(color) { this.element.style.backgroundColor = color; } }; Output.prototype = { constructor:Output }; ================================================ FILE: content/ipo/components/processor.js ================================================ // Frank Poth 03/06/2018 /* The processor class handles the application logic. */ const Processor = function() { this.getRandomColor = function() { return "#" + Math.floor(Math.random() * 16777215).toString(16); } }; Processor.prototype = { constructor:Processor, }; ================================================ FILE: content/ipo/ipo.html ================================================ IPO

PoP Vlog - IPO

Input/Processing/Output

Click the screen to change the background color!

IPO stands for Input/Processing/Output. IPO is a programming concept and organization technique much like MVC or Model/View/Controller. The concept is that most computer applications can be broken down into three basic components: taking user INPUT, PROCESSING that input, and OUTPUTing the final result. By separating the three components of a program, you can achieve more modular, maintainable code. Taking an Object Oriented approach to separating the components yields three classes that interact with each other via their public methods. Each class is totally self contained and has no internal references to any other components. It is modular because classes can be easily reused or replaced, and it is more maintainable because classes can be edited individually without disturbing other classes.

================================================ FILE: content/json/json.html ================================================ PoP Vlog - Loading JSON

PoP Vlog - Loading JSON

================================================ FILE: content/json/json.js ================================================ // Frank Poth 01/14/2017 /* This program simply loads a json file and gets the data out of it. */ (function() { "use strict"; var load, p;// The load function loads our json file. p = document.querySelector("p");// The output p element. load = function(url) {// Loads the file at the specified url. var request; request = new XMLHttpRequest();// We use an XMLHttpRequest to load the file. /* This event listener will call the specified function when the file at the specified url is loaded. */ request.addEventListener("readystatechange", function(event) { if (this.readyState == 4 && this.status == 200) { // We parse the plain text response into a JavaScript object. */ var json = JSON.parse(this.responseText); p.innerHTML = this.responseText; // Now we can use the json object just like a regular JavaScript Object. p.innerHTML += "

json.array: " + json.array; p.innerHTML += "

json.number: " + json.number; p.innerHTML += "
json.number + 2: " + (json.number + 2); p.innerHTML += "

json.string: " + json.string; p.innerHTML += "

json.object.string: " + json.object.string; } }); request.open("GET", url); request.send(null); }; load("json.json");// Our file is called json.json. Creative, I know. })(); ================================================ FILE: content/json/json.json ================================================ { "array":["apple", "sandwich", 2, "a dog"], "number":4, "string":"Hello, I'm a string!", "object": { "string":"I'm a string from inside a JSON object!" } } ================================================ FILE: content/load-image/load-image.css ================================================ /* Frank Poth 12/22/2017 */ * { box-sizing:border-box; margin:0; padding:0; } html { height:100%; width:100%; } body { align-content:space-around; background-color:#202830; color:#ffffff; display:grid; grid-template-columns:auto; grid-template-rows:auto auto auto; height:100%; justify-items:center; padding:0 8px; width:100%; } ================================================ FILE: content/load-image/load-image.html ================================================

PoP Vlog - Loading Images

This example simply loads images in three different ways.
The first image is loaded by the DOM.
The second image is loaded by setting its src attribute.
Finally, the third image is loaded with an XMLHttpRequest.

================================================ FILE: content/load-image/load-image.js ================================================ // Frank Poth 12/22/2017 (function() { "use strict"; /* load0 is the simplest way to get an image, but it's lazy and may not work. load1 is an easy way to load an image, but it won't give you load progress events. load2 is the most complex way to load an image, but it WILL give you load progress events. */ var buffer, display, images, load0, load1, load2, render, resize; /* I use a buffer and a display canvas for easy scaling of the final image. */ buffer = document.createElement("canvas").getContext("2d"); display = document.querySelector("canvas").getContext("2d"); images = new Array();// This will hold our loaded images. /* This isn't really even loading an image. The Document loads the image when it parses the tag in the HTML, then all we have to do in Javascript is get a reference to that image. A problem with this method is that if the image loads asynchronously, the script may execute before it finishes loading, meaning the image you get will have a width and height of 0, which isn't very useful. This method is really lazy, and probably shouldn't be used unless you know the image has finished loading. */ load0 = function() { images[0] = document.querySelector("img");// There's only 1 img tag, so I use querySelector. render();// Calling render draws the image }; /* This is probably the most common method. It's simple and easy, and most importantly, it works. Using a load event listener prevents you from getting progress reports, but for a small web game this probably doesn't matter much unless you're really into accurate loading screens. */ load1 = function() { let image = new Image();// First we must create a new Image object. /* We have to store the image and draw it whenever it loads, so let's make an event handler for the load event. */ image.addEventListener("load", function(event) { /* When the image loads, we store it in the images array and draw it. */ images[1] = this; render(); }); /* Setting the image's src will initiate loading, and eventually a load eventually will fire and we can have access to our image. */ image.src = "gelly.png";// jelly or gelly? Hair gel... jelly sandwich... Huh. }; /* This is the most complicated method, but has its benefits, like being able to track load progress, and feeling cool for using XMLHttpRequest instead of setting src. For some reason it just feels more like real loading. */ load2 = function() { var request = new XMLHttpRequest();// Define your request. var image = new Image();// Make a new empty image. /* Now we make an event handler to do something with the array buffer we're going to load. */ request.addEventListener("load", function(event) { /* First we convert our array buffer response to a Uint8Array. */ var bytes = new Uint8Array(this.response); //alert(bytes);// You can actually see the width and height in this one. /* Now we convert that numeric byte array to a string using fromCharCode. */ var string = String.fromCharCode.apply(null, bytes); //alert(string); /* Now we convert the string to a base64 string. */ var base64 = btoa(string);// Encode string //alert(base64); /* Finally we add the header to the base64 string to use with our png image. */ var data_url = "data:image/png;base64," + base64; //prompt("Copy data url?", data_url); /* Now we can set the src value directly. Setting it this way doesn't load anything, and you have access to the useable image directly after setting src. */ /* 04/06/2018 looking back on this, it's not true. In fact, setting the src is not synchronous even if you set it directly to a data_url. There may be a load time which will cause any immediate requests for the image's data to fail because the image has technically not loaded. */ image.src = data_url; //alert(image.width); images[2] = image; render(); }); /* You can track the progress of the load with this event listener, but for a 16x16 image, this doesn't even have a chance to do anything. */ request.addEventListener("progress", function(event) { if (event.lengthComputable) { console.log("loaded so far: " + event.loaded + " of " + event.total); } }); request.open("GET", "human.png"); request.responseType = "arraybuffer";// You must specify arraybuffer type! request.send(null); }; /* This renders the loaded images to the buffer and then to the display canvas. */ render = function() { var x = 0; buffer.fillStyle = "#283038"; buffer.fillRect(0, 0, buffer.canvas.width, buffer.canvas.height); for (let index = images.length - 1; index > -1; -- index) { let image = images[index]; buffer.drawImage(image, 0, 0, image.width, image.height, x, 0, image.width, image.height); x += image.width; } /* Handles scaling of buffer to display as well. */ display.drawImage(buffer.canvas, 0, 0, buffer.canvas.width, buffer.canvas.height, 0, 0, display.canvas.width, display.canvas.height); }; /* Make sure everything fits nicely in the window, and redraws on screen resize events. */ resize = function(event) { display.canvas.width = document.documentElement.clientWidth - 32; if (display.canvas.width > document.documentElement.clientHeight) { display.canvas.width = document.documentElement.clientHeight; } /* make sure we're maintaining aspect ratio. 1 image high, by 3 wide. */ display.canvas.height = display.canvas.width * (1/3); display.imageSmoothingEnabled = false;// This keeps the image looking sharp. render(); }; window.addEventListener("resize", resize); /* We have 3 16x16 images, so the buffer should fit them exactly. */ buffer.canvas.height = 16; buffer.canvas.width = 48; resize(); load0(); load1(); load2(); /* The code below sets the source of a new image directly to an inline base64 string. This means no loading even occurs, the image data is just there. This is the data from the human.png file. */ /* images[0] = new Image(); images[0].src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QAzQDNAM2UZCwLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QwXDhkbAgmtXgAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAF3SURBVDjLfZO/S8NAFMc/VzqkkKGr4FC0Q8AlkM6C4Ogf4NC5imPt4OCm6Ohmq1NxcnBwCKJQFFdpMGMHfxUcHIQKFnrbOcQ7r03iQQjvve/3+773Hicqno99NhfeFTln5+pTzOJN4m0Qs7vi5pIB3JJD52VepAQ02S05ueTxRGaKFN8GcQqU1z2rXpgFtdqXtNqXuCUH25EmjyeS+tyTyRf1FWxQ2Vvla9AzIB13muspF8bB/t1YbB+d/zdDNg66AEIP3QhEUTQFtLtnxXu33+zoLURRRBAEpjiKQyWc7G2UvVUBGPKhUggIgMRBv4ECWNwKU+Tn4zUAaqfJFbhW0O1S1ABNBnDu18gxQb+Bqp0iGAJDKEDEKA5VtdlDWgOWkqlY56rNHqM4NM2MAyUl9QdfXCzHqnSW0TmA+oMvHqVMyK83uhTY9iBAEaBYsr7fXL9hKTZOkr/9uiqe/yegBQEjMnMqnj/9GpNdoXCAD/5eXUZO834AfkujNjeRqMcAAAAASUVORK5CYII="; render(); */ })(); ================================================ FILE: content/multiple-inheritance/multiple-inheritance.html ================================================ Inheritance ================================================ FILE: content/multiple-inheritance/multiple-inheritance.js ================================================ // Frank Poth 08/03/2017 function Human(name) { this.name = name; } Human.prototype = { talk:function() { console.log("Hey, I'm a human and my name is " + this.name); } }; function Worker(job) { this.job = job; } Worker.prototype = { work:function() { console.log("I am a worker and my job is " + this.job); } }; function Bob(job) { Human.call(this, "Bob"); Worker.call(this, job); } Bob.prototype = Object.create(Human.prototype); Object.assign(Bob.prototype, Worker.prototype); var bob = new Bob("rocket ship captain"); bob.talk(); bob.work(); ================================================ FILE: content/objects-and-vars/objects.html ================================================ Objects And Vars ================================================ FILE: content/objects-and-vars/objects.js ================================================ var time = "3:43"; var rectangle = { x:0, y:10, width:100, height:200, print:function(what_to_say) { console.log(this.width + ", " + what_to_say); } }; rectangle.print("I'm a rectangle!"); function sayHello() { alert("Hello!"); } var sayHello = function() { alert("Hello, again!"); } sayHello(); ================================================ FILE: content/offline-web-app/manifest.json ================================================ { "author": "PoP Vlog", "background_color": "#000000", "description": "A simple Progressive Web Application", "display": "fullscreen", "icons": [ { "src": "web-app.png", "sizes": "192x192", "type": "image/png" } ], "lang": "en-US", "name": "Offline Web App", "orientation": "portrait", "scope": "./", "short_name": "Web App", "start_url": "web-app.html", "theme_color": "#0099ff", "version": "0.2" } ================================================ FILE: content/offline-web-app/server.js ================================================ // Frank Poth 10/16/2017 (function() { var fs, https, mimetypes, options, path, server; fs = require("fs"); // file system https = require("https"); // creates an https server path = require("path"); // used for working with url paths // used to create response headers /* If the user requests a .css file, we want to ensure we attach "text/css" to our response header, this way the browser knows how to handle it. */ mimetypes = { "css":"text/css", "html":"text/html", "ico":"image/ico", "jpg":"image/jpeg", "js":"text/javascript", "json":"application/json", "png":"image/png" }; options = { pfx: fs.readFileSync("ssl/crt.pfx"), passphrase: "password" }; // Start a secure server that uses the credentials in ssl/crt.pfx server = https.createServer(options, function(request, response) { console.log(request.url); /* When requesting the homepage of a website, we usually only type www.mysite.com, but the server returns www.mysite.com/index.html. To make it easier for users to access our site, we add "/index.html" to their url so the user doesn't have to type out the whole address of our home page. */ // If the url is empty if (request.url == "" || request.url == "/") { // The user is requesting the home page of the website, so give it to them request.url = "web-app.html"; } // Next we read the file at the requested url and write it to the document. /* __dirname is just the base directory of your website, so if your website is www.coolsite.com, then __dirname is www.coolsite.com. When you put it all together it looks like www.coolsite.com/index.html or whatever the requested url is */ fs.readFile(__dirname + "/" + request.url, function(error, content) { if (error) { // if there is an error reading the requested url console.log("Error: " + error); // output it to the console } else { // else, there is no error, write the file contents to the page // 200 is code for OK, and the second parameter is our content header response.writeHead(200, {'Content-Type':mimetypes[path.extname(request.url).split(".")[1]]}); response.write(content); // write that content to our response object } response.end(); // This will send our response object to the browser }); }); server.listen("2468", "192.168.0.101", function() { console.log("Server started!"); }); // listen on 192.168.0.101:2468 })(); ================================================ FILE: content/offline-web-app/web-app-service.js ================================================ // Frank Poth 10/25/2017 // here's a great resource on service workers: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers // here's where I basically copy and pasted the code for this service worker: https://airhorner.com/sw.js self.addEventListener("activate", function(event) { event.waitUntil(self.clients.claim().then(function() { self.skipWaiting(); })); }); self.addEventListener("fetch", function(event) { event.respondWith(caches.match(event.request).then(function(response) { if (response && response.ok) { return response; } })); }); self.addEventListener("install", function(event) { event.waitUntil(caches.open("web-app").then(function(cache) { return cache.addAll([ "/", "manifest.json", "web-app.css", "web-app.html", "web-app.png"]).then(function() { self.skipWaiting(); }); })); }); ================================================ FILE: content/offline-web-app/web-app.css ================================================ /* Frank Poth 10/17/2017 */ * { box-sizing:border-box; margin:0; padding:0; } html { height:100%; width:100%; } body { align-content:space-around; background-color:#202830; color:#ffffff; display:grid; height:100%; justify-content:space-around; justify-items:space-around; padding:8px; text-align:center; width:100%; } img { margin:0 auto; } p { font-size:1.1em; text-align:justify; } ================================================ FILE: content/offline-web-app/web-app.html ================================================ Web App

Offline Android Web App!

If you are viewing this app in full screen mode, you are witnessing a Progressive Web App. This app should meet the requirements to be installed to Android devices with the Add To Home Screen feature in Chrome browsers. It also tries to register a service worker which will cache files in the browser cache for use in offline mode!

================================================ FILE: content/pagination/article1.txt ================================================ 1. Pagination is a verb. To paginate content one must break that content into smaller groups. For instance, if I had a database with 1,000 items in it and wanted to display them to clients via a web page, I could make viewing the items more manageable by breaking up the 1,000 items into groups of 10, 20, 50 or even 100. The practice of breaking large chunks of data into smaller, more manageable groups for viewing is known as pagination. ================================================ FILE: content/pagination/article2.txt ================================================ 2. Pagination is used in many applications, perhaps without you even knowing it. If you have used a search engine recently, you have used a web page employing pagination. Search results are broken up into 10 to 20 items per page and at the bottom of that page there is usually a small navigator bar that you can use to view the next set of results if you wish to do so. Many paginators also allow the user to select a specific page within several pages of the currently loaded content. Some paginators allow the user to input a specific page manually with the keyboard. Another common feature of pagination is to allow the user to specify how many results are loaded per page. ================================================ FILE: content/pagination/article3.txt ================================================ 3. The most common use case of pagination is to organize search results requested from a database. Good examples of this are sites like Ebay and Craigslist. Social media sites like Facebook and Twitter also use a type of pagination to limit the number of results loaded per page in their news feeds or especially in photo albums. Because this example is written for a static web server, it cannot access server side databases, making it rather uncommon. Some good uses for a static paginator are: photo albums, articles for blog sites, and custom links inside of a personal portfolio. Ambitious static site builders might even implement a search engine for their static website. Keep in mind, doing any of this on a static website requires a lot of additional overhead and maintenance for things that can be done easily with a dynamic server. ================================================ FILE: content/pagination/article4.txt ================================================ 4. If you are planning to build a static website and want a simple paginator to help your users view photos or links, this paginator will work well. Keep in mind that it requires far more maintenance than a dynamic server. You will have to update the list of files to cycle through anytime you add new content you want to paginate to your website. If you are building a website that gets frequent updates, you should seriously consider a hosting service that supports NodeJS. NodeJS is an easy to learn and use backend scripting language that writes like JavaScript. Of course there are many great server languages to choose from, but NodeJS is a natural fit for front end developers who want to write a simple server with minimal overhead. ================================================ FILE: content/pagination/article5.txt ================================================ 5. Your static site can greatly benefit from AJAX and the XMLHttpRequest. This paginator relies entirely on XMLHttpRequest to communicate with the static file server. Luckily, if you are building a static site and want dynamic content, a static file server will allow you to use AJAX to request files. Any free hosting site will offer this functionality, so you can use AJAX to create awesome tools like this paginator for your static site. To further improve the functionality of your static website, you can use things like service workers and Indexed DB to give your users a great experience. ================================================ FILE: content/pagination/pagination.css ================================================ /* Frank Poth 01/27/2017 */ * { margin:0; padding:0; box-sizing:border-box; } html { height:100%; width:100%; } body { align-content:center; align-items:center; background-color:#ffffff; display:grid; grid-template-columns:auto; grid-template-rows:auto auto; justify-items:center; min-height:100%; width:100%; } h1 { color:#cc181e; font-size: 3.0em; font-weight:1000; margin:16px; text-align:center; } .paginator { display:grid; grid-row-gap:1.0em; grid-template-areas:"content" "navigator"; grid-template-columns:auto; grid-template-rows:auto auto; max-height:480px; max-width:480px; } .paginator-content { color:#202830; display:grid; font-size:1.25em; font-weight:800; grid-area:content; max-height:320px; overflow-y:auto; padding:8px; text-align:justify; } .paginator-navigator { align-items:center; background-color:#cc181e; border-radius:8px; color:#ffffff; display:grid; grid-area:navigator; grid-template-columns:auto auto auto; grid-template-rows:auto; justify-content:center; justify-items:center; margin:0 auto; padding:0 8px; text-align:center; } .paginator-button { cursor:pointer; font-size:1.25em; border-color:#ffffff; padding:8px; user-select:none; } .paginator-index { font-size:1.5em; font-weight:800; user-select:none; } ================================================ FILE: content/pagination/pagination.html ================================================ PoP Vlog - Pagination

PoP Vlog - Pagination

================================================ FILE: content/pagination/paginator.js ================================================ // Frank Poth 01/27/2018 /* This paginator is designed for static websites, making it quite rare. People don't usually do stuff like this for static websites because of how much maintenance it requires. Every time you add content to your site, you have to update the paginator. Sites with backend servers that can handle loading dynamic content for your site at a simple request are far better suited to handle this sort of thing. But, if you are set on making your static site as dynamic as possible and don't mind maintaining it, this will work just fine for you. */ /* The paginator takes an array of links or urls to content. Each url and its corresponding file content represent 1 item to paginate. The index is the first item in that list of links to be displayed on a page, and the limit is how many items will be displayed per page. */ const Paginator = function(links, index, limit) { this.links = links; this.index = 0; /* Make sure the limit per page is not greater than the actual amount of items we have or 0 */ this.limit = (limit <= links.length && limit > 0) ? limit : links.length; /* I created my html with JavaScript, but you could easily do this with an HTML template or create your HTML inline and use querySelector to get a reference to it. I thought it would be more portable to use this approach. That being said, All the CSS for these elements is in the pagination.css file. How you style this stuff is up to you. Even without the css, it all works. */ this.html = document.createElement("div"); this.html.setAttribute("class", "paginator"); this.html.innerHTML = "
back
next
"; /* Here we set up the buttons created above to respond to click events and give them a reference to their paginator. */ var buttons = this.html.querySelectorAll("a"); for (let index = buttons.length - 1; index > -1; -- index) {// Loop through all buttons. let button = buttons[index]; button.addEventListener("click", Paginator.click); /* Button elements need access to their paginator object so they can access its variables inside of the Paginator.click event handler. */ button.paginator = this; } }; Paginator.prototype = { constructor:Paginator, /* Changes the currently displayed items in the paginator's content div. */ changeIndex:function(new_index) { var content_div, content_strings, limit, loaded; this.index = new_index;// The new index in the list of links to start getting items from. /* Show the users what page they are on and how many pages there are. */ this.html.querySelector(".paginator-index").innerHTML = (new_index / this.limit + 1) + " of " + Math.ceil(this.links.length / this.limit); content_div = this.html.querySelector(".paginator-content"); content_div.scrollTop = 0;// Whenever the page changes, scroll content to the top. content_strings = [];// We load each file in order into this array. /* Make sure we don't try to load items that don't exist. */ limit = (new_index + this.limit <= this.links.length) ? this.limit : this.links.length - new_index; loaded = 0;// Keep track of how many files have been loaded. paginator = this;// Get a reference to this paginator. /* The reason for this seemingly convoluted method of loading content is that asynchronous requests can load at different rates, meaning if I request content in a specific order I might get it in a different order. This would be terrible if I were requesting pages in a book and they came back out of order, so to remedy this, I load each file's content into the content_strings array in the correct order and then when everything is done loading, I put the strings together. */ for (let index = 0; index < limit; index ++) { Paginator.requestContent(this.links[this.index + index], function(request) { loaded ++; // index is relative to the value it was set to in the encompassing for loop thanks to let scope. content_strings[index] = "
" + request.responseText + "
"; if (loaded >= limit) { content_div.innerHTML = ""; for (let index = 0; index < limit; index ++) { content_div.innerHTML += content_strings[index]; } } }); } } }; /* The click listener for this paginator. */ Paginator.click = function(event) { var shift;// The number of items to shift past when loading the items for the new page. switch(this.innerHTML) { case "back": shift = this.paginator.index - this.paginator.limit; if (shift < 0) return; break; case "next": shift = this.paginator.index + this.paginator.limit; if (shift >= this.paginator.links.length) return; break; } this.paginator.changeIndex(shift); }; /* Creates a paginator inside a ================================================ FILE: content/platform/platform.html ================================================ Platforms ================================================ FILE: content/platformer-ai/platformer-ai.html ================================================ Platformer AI ================================================ FILE: content/polygon/polygon.html ================================================ JS Polygon Example ================================================ FILE: content/polygon-rotation/polygon-rotation.html ================================================ Polygon Rotation ================================================ FILE: content/pre-scale-performance/pre-scale-performance.css ================================================ /* Frank Poth 02/12/2018 */ * { margin:0; padding:0; box-sizing:border-box; } html { height:100%; width:100%; } body { align-content:space-around; background-color:#202830; color:#ffffff; display:grid; grid-template-columns:auto; grid-template-rows:auto auto auto auto; justify-items:center; min-height:100%; width:100%; } h1 { font-size:3.0em; text-align:center; } canvas { user-select:none; } p { padding:0 8px; max-width:480px; text-align:justify; } #ui { display:grid; grid-template-columns:auto auto auto; grid-template-rows:auto; } #ui a { align-content:center; border-color:#ffffff; border-radius:8px; border-style:solid; border-width:1px; cursor:pointer; display:grid; font-size:1.25em; font-weight:800; justify-content:center; margin:0 8px; padding:8px; text-align:center; user-select:none; } ================================================ FILE: content/pre-scale-performance/pre-scale-performance.html ================================================ PoP Vlog - Pre-Scale Performance

PoP Vlog
Pre-Scale Performance

+ 100method: scale- 100

This application compares the rendering speed of three drawing methods: scale, pre-scale, and pre-scaled-sheet. Scale draws graphics from the source image to a buffer, which is then scaled and drawn to the screen. Pre-scale creates individual scaled canvas elements for each sprite in the source image and then draws them directly to the screen. Pre-scaled-sheet draws the source image to a larger canvas element, and then those scaled graphics are drawn directly to the screen.

================================================ FILE: content/pre-scale-performance/pre-scale-performance.js ================================================ // Frank Poth 02/12/2018 (function() { "use strict"; const Ball = function(x, y) { this.frame_index = Math.floor(Math.random() * 4); this.x = x; this.x_velocity; this.y = y; this.y_velocity; }; Ball.reset = function(ball) { var direction = Math.random() * (Math.PI * 2); ball.x = Math.random() * (game.world.width - display.tile_sheet.tile_size); ball.x_velocity = Math.cos(direction) * 2; ball.y = Math.random() * (game.world.height - display.tile_sheet.tile_size); ball.y_velocity = Math.sin(direction) * 2; }; Ball.prototype = { constructor:Ball, update:function() { this.x += this.x_velocity; this.y += this.y_velocity; }, collideWorld:function() { if (this.x < 0) { this.x = 0; this.x_velocity *= -1; } else if (this.x + display.tile_sheet.tile_size > game.world.width) { this.x = game.world.width - display.tile_sheet.tile_size; this.x_velocity *= -1; } if (this.y < 0) { this.y = 0; this.y_velocity *= -1; } else if (this.y + display.tile_sheet.tile_size > game.world.height) { this.y = game.world.height - display.tile_sheet.tile_size; this.y_velocity *= -1; } } }; const Pool = function(constructor_name) { this.constructor_name = constructor_name; this.active_objects = new Array(); this.stored_objects = new Array(); }; Pool.prototype = { constructor:Pool, activate:function(number, callback) { var object; for (let index = 0; index < number; index ++) { if (this.stored_objects.length != 0) { object = this.stored_objects.pop(); } else { object = new this.constructor_name(); } if (callback) { callback(object) } this.active_objects.push(object); } }, store:function(number) { while(this.active_objects.length != 0 && number > 0) { number --; this.stored_objects.push(this.active_objects.pop()); } } }; var display, game, renderPreScale, renderScale, renderScaledSheet, tracker; renderScaledSheet = function() { //display.context.fillStyle = "#ffffff"; //display.context.fillRect(0, 0, display.context.canvas.width, display.context.canvas.height); var scaled_size = display.tile_sheet.tile_size * display.scale; for (let index = game.object_manager.ball_pool.active_objects.length - 1; index > -1; -- index) { let ball = game.object_manager.ball_pool.active_objects[index]; display.context.drawImage(display.tile_sheet.scaled_image.canvas, (ball.frame_index % display.tile_sheet.columns) * scaled_size, Math.floor(ball.frame_index / display.tile_sheet.columns) * scaled_size, scaled_size, scaled_size, ball.x * display.scale, ball.y * display.scale, scaled_size, scaled_size); } }; renderPreScale = function() { //display.context.fillStyle = "#ffffff"; //display.context.fillRect(0, 0, display.context.canvas.width, display.context.canvas.height); for (let index = game.object_manager.ball_pool.active_objects.length - 1; index > -1; -- index) { let ball = game.object_manager.ball_pool.active_objects[index]; let graphic = display.tile_sheet.prerendered_frames[ball.frame_index]; display.context.drawImage(graphic, 0, 0, graphic.width, graphic.height, ball.x * display.scale, ball.y * display.scale, graphic.width, graphic.height); } }; renderScale = function() { //display.buffer.fillStyle = "#ffffff"; //display.buffer.fillRect(0, 0, display.buffer.canvas.width, display.buffer.canvas.height); for (let index = game.object_manager.ball_pool.active_objects.length - 1; index > -1; -- index) { let ball = game.object_manager.ball_pool.active_objects[index]; let graphic = display.tile_sheet.frames[ball.frame_index]; display.buffer.drawImage(graphic, 0, 0, graphic.width, graphic.height, ball.x, ball.y, graphic.width, graphic.height); } display.context.drawImage(display.buffer.canvas, 0, 0, display.buffer.canvas.width, display.buffer.canvas.height, 0, 0, display.context.canvas.width, display.context.canvas.height); }; display = { buffer:document.createElement("canvas").getContext("2d"), context:document.querySelector("canvas").getContext("2d"), p:document.querySelector("p"), tile_sheet: { columns:2, rows:2, image:new Image(), scaled_image:document.createElement("canvas").getContext("2d"), prerendered_frames:[], frames:[], tile_size:16, prerenderFrames:function(scale) { var graphic, scaled_graphic; for (let index = this.columns * this.rows; index > -1; -- index) { graphic = document.createElement("canvas").getContext("2d"); graphic.canvas.height = graphic.canvas.width = this.tile_size; graphic.imageSmoothingEnabled = false; graphic.drawImage(this.image, (index % this.columns) * this.tile_size, Math.floor(index / this.columns) * this.tile_size, this.tile_size, this.tile_size, 0, 0, graphic.canvas.width, graphic.canvas.height); this.frames[index] = graphic.canvas; scaled_graphic = document.createElement("canvas").getContext("2d"); scaled_graphic.canvas.height = scaled_graphic.canvas.width = this.tile_size * scale; scaled_graphic.imageSmoothingEnabled = false; scaled_graphic.drawImage(graphic.canvas, 0, 0, graphic.canvas.width, graphic.canvas.height, 0, 0, scaled_graphic.canvas.width, scaled_graphic.canvas.height); this.prerendered_frames[index] = scaled_graphic.canvas; } this.scaled_image.canvas.height = this.image.height * scale; this.scaled_image.canvas.width = this.image.width * scale; this.scaled_image.imageSmoothingEnabled = false; this.scaled_image.drawImage(this.image, 0, 0, this.image.width, this.image.height, 0, 0, this.scaled_image.canvas.width, this.scaled_image.canvas.height); } }, render:renderScale, resize:function(event) { var height, width; height = document.documentElement.clientHeight; width = document.documentElement.clientWidth; display.context.canvas.width = Math.floor(width / display.tile_sheet.tile_size) * display.tile_sheet.tile_size; if (display.context.canvas.width > height) { display.context.canvas.width = Math.floor(height / display.tile_sheet.tile_size) * display.tile_sheet.tile_size; } display.context.canvas.height = display.context.canvas.width * (game.world.height / game.world.width); display.scale = display.context.canvas.width / game.world.width; display.tile_sheet.prerenderFrames(display.scale); } }; game = { engine: { accumulated_time:undefined, animation_frame_request:undefined, time:undefined, time_step:1000/60, needs_redraw:false, loop:function(time_stamp) { game.engine.animation_frame_request = window.requestAnimationFrame(game.engine.loop); game.engine.accumulated_time += time_stamp - game.engine.time; game.engine.time = time_stamp; while (game.engine.accumulated_time >= game.engine.time_step) { game.engine.accumulated_time -= game.engine.time_step; game.engine.update(); game.engine.needs_redraw = true; } if (game.engine.needs_redraw) { game.engine.render(); } }, render:function() { var time = window.performance.now(); display.render(); time = window.performance.now() - time; tracker.iteration ++; tracker.time += time; tracker.average = tracker.time / tracker.iteration; display.p.innerHTML = display.render.name + ": " + tracker.average.toPrecision(2) + " ms / frame to render"; }, update:function() { for (let index = game.object_manager.ball_pool.active_objects.length - 1; index > -1; -- index) { let ball = game.object_manager.ball_pool.active_objects[index]; ball.update(); ball.collideWorld(); } }, start:function() { this.animation_frame_request = window.requestAnimationFrame(this.loop); this.accumulated_time = this.time_step; this.time = window.performance.now(); } }, object_manager: { ball_pool: new Pool(Ball) }, world:{ height:360, width:640 } }; tracker = { average:0, iteration:0, time:0, reset:function() { this.average = 0; this.iteration = 0; this.time = 0; } }; //// INITIALIZE //// let buttons = document.querySelectorAll("a"); for (let index = buttons.length - 1; index > -1; -- index) { buttons[index].addEventListener("click", function(event) { switch(this.innerHTML) { case "+ 100": game.object_manager.ball_pool.activate(100, Ball.reset); break; case "- 100": game.object_manager.ball_pool.store(100); break; case "method: scale": this.innerHTML = "method: pre-scale"; display.render = renderPreScale; break; case "method: pre-scale": this.innerHTML = "method: pre-scaled-sheet"; display.render = renderScaledSheet; break; case "method: pre-scaled-sheet": this.innerHTML = "method: scale"; display.render = renderScale; break; } tracker.reset(); }); } display.tile_sheet.image.addEventListener("load", function(event) { display.buffer.canvas.height = game.world.height; display.buffer.canvas.width = game.world.width; display.resize(); game.engine.start(); }); display.tile_sheet.image.src = "pre-scale-performance.png"; window.addEventListener("resize", display.resize); })(); ================================================ FILE: content/prototype-inheritance/prototype-inheritance.html ================================================ Inheritance ================================================ FILE: content/prototype-inheritance/prototype-inheritance.js ================================================ // Frank Poth 08/02/2017 function Human(name) { this.name = name; } Human.prototype = { constructor:Human, talk:function() { console.log("Hey, I'm a human and my name is " + this.name); } }; function Worker(name, job) { Human.call(this, name); this.job = job; } Worker.prototype = Object.create(Human.prototype); Worker.prototype.constructor = Worker; Worker.prototype.talk = function() { console.log("Hey, my name is " + this.name + " and I am a " + this.job + ". I am a " + this.constructor.name); }; var human = new Human("Tim"); var worker = new Worker("John", "desk jockey"); worker.talk(); ================================================ FILE: content/rabbit-trap/01/controller-01.js ================================================ // Frank Poth 02/28/2018 /* In this example, the controller only alerts the user whenever they press a key, but it also defines the ButtonInput class, which is used for tracking button states. */ const Controller = function() { this.down = new Controller.ButtonInput(); this.left = new Controller.ButtonInput(); this.right = new Controller.ButtonInput(); this.up = new Controller.ButtonInput(); this.keyDownUp = function(event) { var down = (event.type == "keydown") ? true : false; switch(event.keyCode) { case 37: this.left.getInput(down); break; case 38: this.up.getInput(down); break; case 39: this.right.getInput(down); break; case 40: this.down.getInput(down); } alert("You pressed a key (" + event.keyCode + ")!"); }; this.handleKeyDownUp = (event) => { this.keyDownUp(event); }; }; Controller.prototype = { constructor : Controller }; Controller.ButtonInput = function() { this.active = this.down = false; }; Controller.ButtonInput.prototype = { constructor : Controller.ButtonInput, getInput : function(down) { if (this.down != down) this.active = down; this.down = down; } }; ================================================ FILE: content/rabbit-trap/01/display-01.js ================================================ // Frank Poth 02/28/2018 /* This Display class contains the screen resize event handler and also handles drawing colors to the buffer and then to the display. */ const Display = function(canvas) { this.buffer = document.createElement("canvas").getContext("2d"), this.context = canvas.getContext("2d"); this.renderColor = function(color) { this.buffer.fillStyle = color; this.buffer.fillRect(0, 0, this.buffer.canvas.width, this.buffer.canvas.height); }; this.render = function() { this.context.drawImage(this.buffer.canvas, 0, 0, this.buffer.canvas.width, this.buffer.canvas.height, 0, 0, this.context.canvas.width, this.context.canvas.height); }; this.resize = function(event) { var height, width; height = document.documentElement.clientHeight; width = document.documentElement.clientWidth; this.context.canvas.height = height - 32; this.context.canvas.width = width - 32; this.render(); }; this.handleResize = (event) => { this.resize(event); }; }; Display.prototype = { constructor : Display }; ================================================ FILE: content/rabbit-trap/01/engine-01.js ================================================ // Frank Poth 02/28/2018 /* This is a fixed time step game loop. It can be used for any game and will ensure that game state is updated at the same rate across different devices which is important for uniform gameplay. Imagine playing your favorite game on a new phone and suddenly it's running at a different speed. That would be a bad user experience, so we fix it with a fixed step game loop. In addition, you can do things like frame dropping and interpolation with a fixed step loop, which allow your game to play and look smooth on slower devices rather than freezing or lagging to the point of unplayability. */ const Engine = function(time_step, update, render) { this.accumulated_time = 0;// Amount of time that's accumulated since the last update. this.animation_frame_request = undefined,// reference to the AFR this.time = undefined,// The most recent timestamp of loop execution. this.time_step = time_step,// 1000/30 = 30 frames per second this.updated = false;// Whether or not the update function has been called since the last cycle. this.update = update;// The update function this.render = render;// The render function this.run = function(time_stamp) {// This is one cycle of the game loop this.accumulated_time += time_stamp - this.time; this.time = time_stamp; /* If the device is too slow, updates may take longer than our time step. If this is the case, it could freeze the game and overload the cpu. To prevent this, we catch a memory spiral early and never allow three full frames to pass without an update. This is not ideal, but at least the user won't crash their cpu. */ if (this.accumulated_time >= this.time_step * 3) { this.accumulated_time = this.time_step; } /* Since we can only update when the screen is ready to draw and requestAnimationFrame calls the run function, we need to keep track of how much time has passed. We store that accumulated time and test to see if enough has passed to justify an update. Remember, we want to update every time we have accumulated one time step's worth of time, and if multiple time steps have accumulated, we must update one time for each of them to stay up to speed. */ while(this.accumulated_time >= this.time_step) { this.accumulated_time -= this.time_step; this.update(time_stamp); this.updated = true;// If the game has updated, we need to draw it again. } /* This allows us to only draw when the game has updated. */ if (this.updated) { this.updated = false; this.render(time_stamp); } this.animation_frame_request = window.requestAnimationFrame(this.handleRun); }; this.handleRun = (time_step) => { this.run(time_step); }; }; Engine.prototype = { constructor:Engine, start:function() { this.accumulated_time = this.time_step; this.time = window.performance.now(); this.animation_frame_request = window.requestAnimationFrame(this.handleRun); }, stop:function() { window.cancelAnimationFrame(this.animation_frame_request); } }; ================================================ FILE: content/rabbit-trap/01/game-01.js ================================================ // Frank Poth 02/28/2018 /* To keep this example from looking too boring, I made the game logic gradually change some color values which then get drawn to the display canvas in the loop. */ const Game = function() { this.color = "rgb(0,0,0)"; this.colors = [0, 0, 0]; this.shifts = [1, 1, 1]; this.update = function() { for (let index = 0; index < 3; index ++) { let color = this.colors[index]; let shift = this.shifts[index]; if (color + shift > 255 || color + shift < 0) { shift = (shift < 0) ? Math.floor(Math.random() * 2) + 1 : Math.floor(Math.random() * -2) - 1; } color += shift; this.colors[index] = color; this.shifts[index] = shift; } this.color = "rgb(" + this.colors[0] + "," + this.colors[1] + "," + this.colors[2] + ")"; }; }; Game.prototype = { constructor : Game }; ================================================ FILE: content/rabbit-trap/01/main-01.js ================================================ // Frank Poth 02/28/2018 /* This is the basic setup or "skeleton" of my program. It has three main parts: the controller, display, and game logic. It also has an engine which combines the three logical parts which are otherwise completely separate. One of the most important aspects of programming is organization. Without an organized foundation, your code will quickly become unruly and difficult to maintain. Separating code into logical groups is also a principle of object oriented programming, which lends itself to comprehensible, maintainable code as well as modularity. */ /* Since I am loading my scripts dynamically from the rabbit-trap.html, I am wrapping my main JavaScript file in a load listener. This ensures that this code will not execute until the document has finished loading and I have access to all of my classes. */ window.addEventListener("load", function(event) { "use strict"; /////////////////// //// FUNCTIONS //// /////////////////// var render = function() { display.renderColor(game.color); display.render(); }; var update = function() { game.update(); }; ///////////////// //// OBJECTS //// ///////////////// /* Usually I just write my logical sections into object literals, but the temptation to reference one inside of another is too great, and leads to sloppy coding. In an effort to attain cleaner code, I have written classes for each section and instantiate them here. */ /* The controller handles user input. */ var controller = new Controller(); /* The display handles window resizing, as well as the on screen canvas. */ var display = new Display(document.querySelector("canvas")); /* The game will eventually hold our game logic. */ var game = new Game(); /* The engine is where the above three sections can interact. */ var engine = new Engine(1000/30, render, update); //////////////////// //// INITIALIZE //// //////////////////// window.addEventListener("resize", display.handleResize); window.addEventListener("keydown", controller.handleKeyDownUp); window.addEventListener("keyup", controller.handleKeyDownUp); display.resize(); engine.start(); }); ================================================ FILE: content/rabbit-trap/02/controller-02.js ================================================ // Frank Poth 03/09/2018 /* The keyDownUp handler was moved to the main file. */ const Controller = function() { this.left = new Controller.ButtonInput(); this.right = new Controller.ButtonInput(); this.up = new Controller.ButtonInput(); this.keyDownUp = function(type, key_code) { var down = (type == "keydown") ? true : false; switch(key_code) { case 37: this.left.getInput(down); break; case 38: this.up.getInput(down); break; case 39: this.right.getInput(down); } }; }; Controller.prototype = { constructor : Controller }; Controller.ButtonInput = function() { this.active = this.down = false; }; Controller.ButtonInput.prototype = { constructor : Controller.ButtonInput, getInput : function(down) { if (this.down != down) this.active = down; this.down = down; } }; ================================================ FILE: content/rabbit-trap/02/display-02.js ================================================ // Frank Poth 03/09/2018 /* This class hasn't changed much since part 1. All it does now is resize the canvas a bit differently and draw rectangles to the buffer. */ const Display = function(canvas) { this.buffer = document.createElement("canvas").getContext("2d"), this.context = canvas.getContext("2d"); this.drawRectangle = function(x, y, width, height, color) { this.buffer.fillStyle = color; this.buffer.fillRect(Math.floor(x), Math.floor(y), width, height); }; this.fill = function(color) { this.buffer.fillStyle = color; this.buffer.fillRect(0, 0, this.buffer.canvas.width, this.buffer.canvas.height); }; this.render = function() { this.context.drawImage(this.buffer.canvas, 0, 0, this.buffer.canvas.width, this.buffer.canvas.height, 0, 0, this.context.canvas.width, this.context.canvas.height); }; this.resize = function(width, height, height_width_ratio) { if (height / width > height_width_ratio) { this.context.canvas.height = width * height_width_ratio; this.context.canvas.width = width; } else { this.context.canvas.height = height; this.context.canvas.width = height / height_width_ratio; } this.context.imageSmoothingEnabled = false; }; }; Display.prototype = { constructor : Display }; ================================================ FILE: content/rabbit-trap/02/game-02.js ================================================ // Frank Poth 03/09/2018 /* The Game class has been updated with a new Player class and given a new world object that controls the virtual game world. Players, NPCs, world dimensions, collision maps, and everything to do with the game world are stored in the world object. */ const Game = function() { this.world = { background_color:"rgba(40,48,56,0.25)", friction:0.9, gravity:3, player:new Game.Player(), height:72, width:128, collideObject:function(object) { if (object.x < 0) { object.x = 0; object.velocity_x = 0; } else if (object.x + object.width > this.width) { object.x = this.width - object.width; object.velocity_x = 0; } if (object.y < 0) { object.y = 0; object.velocity_y = 0; } else if (object.y + object.height > this.height) { object.jumping = false; object.y = this.height - object.height; object.velocity_y = 0; } }, update:function() { this.player.velocity_y += this.gravity; this.player.update(); this.player.velocity_x *= this.friction; this.player.velocity_y *= this.friction; this.collideObject(this.player); } }; this.update = function() { this.world.update(); }; }; Game.prototype = { constructor : Game }; Game.Player = function(x, y) { this.color = "#ff0000"; this.height = 16; this.jumping = true; this.velocity_x = 0; this.velocity_y = 0; this.width = 16; this.x = 100; this.y = 50; }; Game.Player.prototype = { constructor : Game.Player, jump:function() { if (!this.jumping) { this.color = "#" + Math.floor(Math.random() * 16777216).toString(16);// Change to random color /* toString(16) will not add a leading 0 to a hex value, so this: #0fffff, for example, isn't valid. toString would cut off the first 0. The code below inserts it. */ if (this.color.length != 7) { this.color = this.color.slice(0, 1) + "0" + this.color.slice(1, 6); } this.jumping = true; this.velocity_y -= 20; } }, moveLeft:function() { this.velocity_x -= 0.5; }, moveRight:function() { this.velocity_x += 0.5; }, update:function() { this.x += this.velocity_x; this.y += this.velocity_y; } }; ================================================ FILE: content/rabbit-trap/02/main-02.js ================================================ // Frank Poth 03/09/2018 /* In part 2, I added a player character to the screen. I created world boundaries with collision detection, and I added keyboard input to move the player around. His color changes whenever he jumps. */ /* Changes since the last part: I moved the event handlers out of the component files. Any functionality that might need two or more components to communicate should be in the main file. The resize function, for example, shouldn't be in Display, because it needs information from Game to size the onscreen canvas. As a general rule, anything that causes two components to communicate should be in the main file because we want to reduce internal references between components as much as possible. They should communicate via public methods that take primitives. */ window.addEventListener("load", function(event) { "use strict"; /////////////////// //// FUNCTIONS //// /////////////////// /* This used to be in the Controller class, but I moved it out to the main file. The reason being that later on in development I might need to do something with display or processing directly on an input event in addition to updating the controller. To prevent referencing those components inside of my controller logic, I moved all of my event handlers here, to the main file. */ var keyDownUp = function(event) { controller.keyDownUp(event.type, event.keyCode); }; /* I also moved this handler out of Display since part 1 of this series. The reason being that I need to reference game as well as display to resize the canvas according to the dimensions of the game world. I don't want to reference game inside of my Display class, so I moved the resize method into the main file. */ var resize = function(event) { display.resize(document.documentElement.clientWidth - 32, document.documentElement.clientHeight - 32, game.world.height / game.world.width); display.render(); }; var render = function() { display.fill(game.world.background_color);// Clear background to game's background color. display.drawRectangle(game.world.player.x, game.world.player.y, game.world.player.width, game.world.player.height, game.world.player.color); display.render(); }; var update = function() { if (controller.left.active) { game.world.player.moveLeft(); } if (controller.right.active) { game.world.player.moveRight(); } if (controller.up.active) { game.world.player.jump(); controller.up.active = false; } game.update(); }; ///////////////// //// OBJECTS //// ///////////////// var controller = new Controller(); var display = new Display(document.querySelector("canvas")); var game = new Game(); var engine = new Engine(1000/30, render, update); //////////////////// //// INITIALIZE //// //////////////////// /* This is very important. The buffer canvas must be pixel for pixel the same size as the world dimensions to properly scale the graphics. All the game knows are player location and world dimensions. We have to tell the display to match them. */ display.buffer.canvas.height = game.world.height; display.buffer.canvas.width = game.world.width; window.addEventListener("keydown", keyDownUp); window.addEventListener("keyup", keyDownUp); window.addEventListener("resize", resize); resize(); engine.start(); }); ================================================ FILE: content/rabbit-trap/03/display-03.js ================================================ // Frank Poth 03/23/2018 /* I moved some generic functions to the Display.prototype. I created the Display.TileSheet class, which handles the tile sheet image and its dimensions. I got rid of the drawRectangle function and replaced it with the drawPlayer function. */ const Display = function(canvas) { this.buffer = document.createElement("canvas").getContext("2d"), this.context = canvas.getContext("2d"); this.tile_sheet = new Display.TileSheet(16, 8); /* This function draws the map to the buffer. */ this.drawMap = function(map, columns) { for (let index = map.length - 1; index > -1; -- index) { let value = map[index] - 1; let source_x = (value % this.tile_sheet.columns) * this.tile_sheet.tile_size; let source_y = Math.floor(value / this.tile_sheet.columns) * this.tile_sheet.tile_size; let destination_x = (index % columns) * this.tile_sheet.tile_size; let destination_y = Math.floor(index / columns) * this.tile_sheet.tile_size; this.buffer.drawImage(this.tile_sheet.image, source_x, source_y, this.tile_sheet.tile_size, this.tile_sheet.tile_size, destination_x, destination_y, this.tile_sheet.tile_size, this.tile_sheet.tile_size); } }; this.drawPlayer = function(rectangle, color1, color2) { this.buffer.fillStyle = color1; this.buffer.fillRect(Math.floor(rectangle.x), Math.floor(rectangle.y), rectangle.width, rectangle.height); this.buffer.fillStyle = color2; this.buffer.fillRect(Math.floor(rectangle.x + 2), Math.floor(rectangle.y + 2), rectangle.width - 4, rectangle.height - 4); }; this.resize = function(width, height, height_width_ratio) { if (height / width > height_width_ratio) { this.context.canvas.height = width * height_width_ratio; this.context.canvas.width = width; } else { this.context.canvas.height = height; this.context.canvas.width = height / height_width_ratio; } this.context.imageSmoothingEnabled = false; }; }; Display.prototype = { constructor : Display, render:function() { this.context.drawImage(this.buffer.canvas, 0, 0, this.buffer.canvas.width, this.buffer.canvas.height, 0, 0, this.context.canvas.width, this.context.canvas.height); }, }; Display.TileSheet = function(tile_size, columns) { this.image = new Image(); this.tile_size = tile_size; this.columns = columns; }; Display.TileSheet.prototype = {}; ================================================ FILE: content/rabbit-trap/03/game-03.js ================================================ // Frank Poth 03/23/2018 /* I moved the world object into its own class and I made the Player class a class inside of Game.World. I am doing this in order to compartmentalize my objects more accurately. The Player class will never be used outside of the World class, and the World class will never be used outside of the Game class, therefore the classes will be nested: Game -> Game.World -> Game.World.Player */ const Game = function() { /* The world object is now its own class. */ this.world = new Game.World(); /* The Game.update function works the same as in part 2. */ this.update = function() { this.world.update(); }; }; Game.prototype = { constructor : Game }; /* The world is now its own class. */ Game.World = function(friction = 0.9, gravity = 3) { this.friction = friction; this.gravity = gravity; /* Player is now its own class inside of the Game.World object. */ this.player = new Game.World.Player(); /* Here is the map data. Later on I will load it from a json file, but for now I will just hardcode it here. */ this.columns = 12; this.rows = 9; this.tile_size = 16; this.map = [49,18,18,18,50,49,19,20,17,18,36,37, 11,40,40,40,17,19,40,32,32,32,40,08, 11,32,40,32,32,32,40,13,06,06,29,02, 36,07,40,40,32,40,40,20,40,40,09,10, 03,32,32,48,40,48,40,32,32,05,37,26, 11,40,40,32,40,40,40,32,32,32,40,38, 11,40,32,05,15,07,40,40,04,40,01,43, 50,03,32,32,12,40,40,32,12,01,43,10, 09,41,28,14,38,28,14,04,23,35,10,25]; /* Height and Width now depend on the map size. */ this.height = this.tile_size * this.rows; this.width = this.tile_size * this.columns; }; /* Now that world is a class, I moved its more generic functions into its prototype. */ Game.World.prototype = { constructor: Game.World, collideObject:function(object) {// Same as in part 2. if (object.x < 0) { object.x = 0; object.velocity_x = 0; } else if (object.x + object.width > this.width) { object.x = this.width - object.width; object.velocity_x = 0; } if (object.y < 0) { object.y = 0; object.velocity_y = 0; } else if (object.y + object.height > this.height) { object.jumping = false; object.y = this.height - object.height; object.velocity_y = 0; } }, update:function() { this.player.velocity_y += this.gravity; this.player.update(); this.player.velocity_x *= this.friction; this.player.velocity_y *= this.friction; this.collideObject(this.player); } }; /* The player is also its own class now. Since player only appears in the context of Game.World, that is where it is defined. */ Game.World.Player = function(x, y) { this.color1 = "#404040"; this.color2 = "#f0f0f0"; this.height = 12; this.jumping = true; this.velocity_x = 0; this.velocity_y = 0; this.width = 12; this.x = 100; this.y = 50; }; Game.World.Player.prototype = { constructor : Game.World.Player, jump:function() { if (!this.jumping) { this.jumping = true; this.velocity_y -= 20; } }, moveLeft:function() { this.velocity_x -= 0.5; }, moveRight:function() { this.velocity_x += 0.5; }, update:function() { this.x += this.velocity_x; this.y += this.velocity_y; } }; ================================================ FILE: content/rabbit-trap/03/main-03.js ================================================ // Frank Poth 03/23/2017 window.addEventListener("load", function(event) { "use strict"; /////////////////// //// FUNCTIONS //// /////////////////// var keyDownUp = function(event) { controller.keyDownUp(event.type, event.keyCode); }; var resize = function(event) { display.resize(document.documentElement.clientWidth - 32, document.documentElement.clientHeight - 32, game.world.height / game.world.width); display.render(); }; var render = function() { display.drawMap(game.world.map, game.world.columns); display.drawPlayer(game.world.player, game.world.player.color1, game.world.player.color2); display.render(); }; var update = function() { if (controller.left.active) { game.world.player.moveLeft(); } if (controller.right.active) { game.world.player.moveRight(); } if (controller.up.active) { game.world.player.jump(); controller.up.active = false; } game.update(); }; ///////////////// //// OBJECTS //// ///////////////// var controller = new Controller(); var display = new Display(document.querySelector("canvas")); var game = new Game(); var engine = new Engine(1000/30, render, update); //////////////////// //// INITIALIZE //// //////////////////// display.buffer.canvas.height = game.world.height; display.buffer.canvas.width = game.world.width; display.tile_sheet.image.addEventListener("load", function(event) { resize(); engine.start(); }, { once:true }); display.tile_sheet.image.src = "rabbit-trap.png"; window.addEventListener("keydown", keyDownUp); window.addEventListener("keyup", keyDownUp); window.addEventListener("resize", resize); }); ================================================ FILE: content/rabbit-trap/04/display-04.js ================================================ // Frank Poth 03/23/2018 /* I changed a few small things since part 3. First, I got rid of my tile value offset when drawing tiles from the game object's map. Each value used to be offset by 1 due to the export format of my tile map editor. I also changed the rounding method in the drawPlayer function from Math.floor to Math.round to better represent where the player is actually standing. */ const Display = function(canvas) { this.buffer = document.createElement("canvas").getContext("2d"), this.context = canvas.getContext("2d"); this.tile_sheet = new Display.TileSheet(16, 8); /* This function draws the map to the buffer. */ this.drawMap = function(map, columns) { for (let index = map.length - 1; index > -1; -- index) { let value = map[index]; // No longer subtracting 1. The values in my tile map have been shifted down by 1. let source_x = (value % this.tile_sheet.columns) * this.tile_sheet.tile_size; let source_y = Math.floor(value / this.tile_sheet.columns) * this.tile_sheet.tile_size; let destination_x = (index % columns) * this.tile_sheet.tile_size; let destination_y = Math.floor(index / columns) * this.tile_sheet.tile_size; this.buffer.drawImage(this.tile_sheet.image, source_x, source_y, this.tile_sheet.tile_size, this.tile_sheet.tile_size, destination_x, destination_y, this.tile_sheet.tile_size, this.tile_sheet.tile_size); } }; this.drawPlayer = function(rectangle, color1, color2) { this.buffer.fillStyle = color1; this.buffer.fillRect(Math.round(rectangle.x), Math.round(rectangle.y), rectangle.width, rectangle.height); this.buffer.fillStyle = color2; this.buffer.fillRect(Math.round(rectangle.x + 2), Math.round(rectangle.y + 2), rectangle.width - 4, rectangle.height - 4); }; this.resize = function(width, height, height_width_ratio) { if (height / width > height_width_ratio) { this.context.canvas.height = width * height_width_ratio; this.context.canvas.width = width; } else { this.context.canvas.height = height; this.context.canvas.width = height / height_width_ratio; } this.context.imageSmoothingEnabled = false; }; }; Display.prototype = { constructor : Display, render:function() { this.context.drawImage(this.buffer.canvas, 0, 0, this.buffer.canvas.width, this.buffer.canvas.height, 0, 0, this.context.canvas.width, this.context.canvas.height); }, }; Display.TileSheet = function(tile_size, columns) { this.image = new Image(); this.tile_size = tile_size; this.columns = columns; }; Display.TileSheet.prototype = {}; ================================================ FILE: content/rabbit-trap/04/game-04.js ================================================ // Frank Poth 03/28/2018 /* In part 4 I added collision detection and response for the tile map. I also fixed the tile map offset from part 3, where every graphical value was offset by 1 due to the export format of the tile map editor I used. I added the collision_map and the collider object to handle collision. I also added a superclass called Object that all other game objects will extend. It has a bunch of methods for working with object position. */ const Game = function() { this.world = new Game.World();// All the changes are in the world class. this.update = function() { this.world.update(); }; }; Game.prototype = { constructor : Game }; Game.World = function(friction = 0.9, gravity = 3) { this.collider = new Game.World.Collider();// Here's the new collider class. this.friction = friction; this.gravity = gravity; this.player = new Game.World.Player(); this.columns = 12; this.rows = 9; this.tile_size = 16; /* This map stays the same. It is the graphical map. It only places graphics and has nothing to do with collision. */ this.map = [48,17,17,17,49,48,18,19,16,17,35,36, 10,39,39,39,16,18,39,31,31,31,39,07, 10,31,39,31,31,31,39,12,05,05,28,01, 35,06,39,39,31,39,39,19,39,39,08,09, 02,31,31,47,39,47,39,31,31,04,36,25, 10,39,39,31,39,39,39,31,31,31,39,37, 10,39,31,04,14,06,39,39,03,39,00,42, 49,02,31,31,11,39,39,31,11,00,42,09, 08,40,27,13,37,27,13,03,22,34,09,24]; /* These collision values correspond to collision functions in the Collider class. 00 is nothing. everything else is run through a switch statement and routed to the appropriate collision functions. These particular values aren't arbitrary. Their binary representation can be used to describe which sides of the tile have boundaries. 0000 = 0 tile 0: 0 tile 1: 1 tile 2: 0 tile 15: 1 0001 = 1 0 0 0 0 0 1 1 1 0010 = 2 0 0 0 1 1111 = 15 No walls Wall on top Wall on Right four walls This binary representation can be used to describe which sides of a tile are boundaries. Each bit represents a side: 0 0 0 0 = l b r t (left bottom right top). Keep in mind that this is just one way to look at it. You could assign your collision values any way you want. This is just the way I chose to keep track of which values represent which tiles. I haven't tested this representation approach with more advanced shapes. */ this.collision_map = [00,04,04,04,00,00,04,04,04,04,04,00, 02,00,00,00,12,06,00,00,00,00,00,08, 02,00,00,00,00,00,00,09,05,05,01,00, 00,07,00,00,00,00,00,14,00,00,08,00, 02,00,00,01,00,01,00,00,00,13,04,00, 02,00,00,00,00,00,00,00,00,00,00,08, 02,00,00,13,01,07,00,00,11,00,09,00, 00,03,00,00,10,00,00,00,08,01,00,00, 00,00,01,01,00,01,01,01,00,00,00,00]; this.height = this.tile_size * this.rows; this.width = this.tile_size * this.columns; }; Game.World.prototype = { constructor: Game.World, /* This function has been hugely modified. */ collideObject:function(object) { /* Let's make sure we can't leave the world boundaries. */ if (object.getLeft() < 0 ) { object.setLeft(0); object.velocity_x = 0; } else if (object.getRight() > this.width ) { object.setRight(this.width); object.velocity_x = 0; } if (object.getTop() < 0 ) { object.setTop(0); object.velocity_y = 0; } else if (object.getBottom() > this.height) { object.setBottom(this.height); object.velocity_y = 0; object.jumping = false; } /* Now let's collide with some tiles!!! The side values refer to the tile grid row and column spaces that the object is occupying on each of its sides. For instance bottom refers to the row in the collision map that the bottom of the object occupies. Right refers to the column in the collision map occupied by the right side of the object. Value refers to the value of a collision tile in the map under the specified row and column occupied by the object. */ var bottom, left, right, top, value; /* First we test the top left corner of the object. We get the row and column he occupies in the collision map, then we get the value from the collision map at that row and column. In this case the row is top and the column is left. Then we hand the information to the collider's collide function. */ top = Math.floor(object.getTop() / this.tile_size); left = Math.floor(object.getLeft() / this.tile_size); value = this.collision_map[top * this.columns + left]; this.collider.collide(value, object, left * this.tile_size, top * this.tile_size, this.tile_size); /* We must redifine top since the last collision check because the object may have moved since the last collision check. Also, the reason I check the top corners first is because if the object is moved down while checking the top, he will be moved back up when checking the bottom, and it is better to look like he is standing on the ground than being pushed down through the ground by the cieling. */ top = Math.floor(object.getTop() / this.tile_size); right = Math.floor(object.getRight() / this.tile_size); value = this.collision_map[top * this.columns + right]; this.collider.collide(value, object, right * this.tile_size, top * this.tile_size, this.tile_size); bottom = Math.floor(object.getBottom() / this.tile_size); left = Math.floor(object.getLeft() / this.tile_size); value = this.collision_map[bottom * this.columns + left]; this.collider.collide(value, object, left * this.tile_size, bottom * this.tile_size, this.tile_size); bottom = Math.floor(object.getBottom() / this.tile_size); right = Math.floor(object.getRight() / this.tile_size); value = this.collision_map[bottom * this.columns + right]; this.collider.collide(value, object, right * this.tile_size, bottom * this.tile_size, this.tile_size); }, update:function() { this.player.velocity_y += this.gravity; this.player.update(); this.player.velocity_x *= this.friction; this.player.velocity_y *= this.friction; this.collideObject(this.player); } }; Game.World.Collider = function() { /* This is the function routing method. Basically, you know what the tile looks like from its value. You know which object you want to collide with, and you know the x and y position of the tile as well as its dimensions. This function just decides which collision functions to use based on the value and allows you to tweak the other values to fit the specific tile shape. */ this.collide = function(value, object, tile_x, tile_y, tile_size) { switch(value) { // which value does our tile have? /* All 15 tile types can be described with only 4 collision methods. These methods are mixed and matched for each unique tile. */ case 1: this.collidePlatformTop (object, tile_y ); break; case 2: this.collidePlatformRight (object, tile_x + tile_size); break; case 3: if (this.collidePlatformTop (object, tile_y )) return;// If there's a collision, we don't need to check for anything else. this.collidePlatformRight (object, tile_x + tile_size); break; case 4: this.collidePlatformBottom (object, tile_y + tile_size); break; case 5: if (this.collidePlatformTop (object, tile_y )) return; this.collidePlatformBottom (object, tile_y + tile_size); break; case 6: if (this.collidePlatformRight(object, tile_x + tile_size)) return; this.collidePlatformBottom (object, tile_y + tile_size); break; case 7: if (this.collidePlatformTop (object, tile_y )) return; if (this.collidePlatformRight(object, tile_x + tile_size)) return; this.collidePlatformBottom (object, tile_y + tile_size); break; case 8: this.collidePlatformLeft (object, tile_x ); break; case 9: if (this.collidePlatformTop (object, tile_y )) return; this.collidePlatformLeft (object, tile_x ); break; case 10: if (this.collidePlatformLeft (object, tile_x )) return; this.collidePlatformRight (object, tile_x + tile_size); break; case 11: if (this.collidePlatformTop (object, tile_y )) return; if (this.collidePlatformLeft (object, tile_x )) return; this.collidePlatformRight (object, tile_x + tile_size); break; case 12: if (this.collidePlatformLeft (object, tile_x )) return; this.collidePlatformBottom (object, tile_y + tile_size); break; case 13: if (this.collidePlatformTop (object, tile_y )) return; if (this.collidePlatformLeft (object, tile_x )) return; this.collidePlatformBottom (object, tile_y + tile_size); break; case 14: if (this.collidePlatformLeft (object, tile_x )) return; if (this.collidePlatformRight(object, tile_x )) return; this.collidePlatformBottom (object, tile_y + tile_size); break; case 15: if (this.collidePlatformTop (object, tile_y )) return; if (this.collidePlatformLeft (object, tile_x )) return; if (this.collidePlatformRight(object, tile_x + tile_size)) return; this.collidePlatformBottom (object, tile_y + tile_size); break; } } }; /* Here's where all of the collision functions live. */ Game.World.Collider.prototype = { constructor: Game.World.Collider, /* This will resolve a collision (if any) between an object and the y location of some tile's bottom. All of these functions are pretty much the same, just adjusted for different sides of a tile and different trajectories of the object. */ collidePlatformBottom:function(object, tile_bottom) { /* If the top of the object is above the bottom of the tile and on the previous frame the top of the object was below the bottom of the tile, we have entered into this tile. Pretty simple stuff. */ if (object.getTop() < tile_bottom && object.getOldTop() >= tile_bottom) { object.setTop(tile_bottom);// Move the top of the object to the bottom of the tile. object.velocity_y = 0; // Stop moving in that direction. return true; // Return true because there was a collision. } return false; // Return false if there was no collision. }, collidePlatformLeft:function(object, tile_left) { if (object.getRight() > tile_left && object.getOldRight() <= tile_left) { object.setRight(tile_left - 0.01);// -0.01 is to fix a small problem with rounding object.velocity_x = 0; return true; } return false; }, collidePlatformRight:function(object, tile_right) { if (object.getLeft() < tile_right && object.getOldLeft() >= tile_right) { object.setLeft(tile_right); object.velocity_x = 0; return true; } return false; }, collidePlatformTop:function(object, tile_top) { if (object.getBottom() > tile_top && object.getOldBottom() <= tile_top) { object.setBottom(tile_top - 0.01); object.velocity_y = 0; object.jumping = false; return true; } return false; } }; /* The object class is just a basic rectangle with a bunch of prototype functions to help us work with positioning this rectangle. */ Game.World.Object = function(x, y, width, height) { this.height = height; this.width = width; this.x = x; this.x_old = x; this.y = y; this.y_old = y; }; Game.World.Object.prototype = { constructor:Game.World.Object, /* These functions are used to get and set the different side positions of the object. */ getBottom: function() { return this.y + this.height; }, getLeft: function() { return this.x; }, getRight: function() { return this.x + this.width; }, getTop: function() { return this.y; }, getOldBottom:function() { return this.y_old + this.height; }, getOldLeft: function() { return this.x_old; }, getOldRight: function() { return this.x_old + this.width; }, getOldTop: function() { return this.y_old }, setBottom: function(y) { this.y = y - this.height; }, setLeft: function(x) { this.x = x; }, setRight: function(x) { this.x = x - this.width; }, setTop: function(y) { this.y = y; }, setOldBottom:function(y) { this.y_old = y - this.height; }, setOldLeft: function(x) { this.x_old = x; }, setOldRight: function(x) { this.x_old = x - this.width; }, setOldTop: function(y) { this.y_old = y; } }; Game.World.Player = function(x, y) { Game.World.Object.call(this, 100, 100, 12, 12); this.color1 = "#404040"; this.color2 = "#f0f0f0"; this.jumping = true; this.velocity_x = 0; this.velocity_y = 0; }; Game.World.Player.prototype = { jump:function() { if (!this.jumping) { this.jumping = true; this.velocity_y -= 20; } }, moveLeft:function() { this.velocity_x -= 0.5; }, moveRight:function() { this.velocity_x += 0.5; }, update:function() { this.x_old = this.x; this.y_old = this.y; this.x += this.velocity_x; this.y += this.velocity_y; } }; Object.assign(Game.World.Player.prototype, Game.World.Object.prototype); Game.World.Player.prototype.constructor = Game.World.Player; ================================================ FILE: content/rabbit-trap/05/display-05.js ================================================ // Frank Poth 04/03/2018 /* Changes: 1. Removed the TileSheet class from part 3 and added the Game.World.TileSet class to Game. 2. Changed the drawMap function to be as generic as posible. 3. Changed the drawPlayer function to the drawObject function. */ const Display = function(canvas) { this.buffer = document.createElement("canvas").getContext("2d"), this.context = canvas.getContext("2d"); /* This function draws the map to the buffer. */ this.drawMap = function(image, image_columns, map, map_columns, tile_size) { for (let index = map.length - 1; index > -1; -- index) { let value = map[index]; let source_x = (value % image_columns) * tile_size; let source_y = Math.floor(value / image_columns) * tile_size; let destination_x = (index % map_columns ) * tile_size; let destination_y = Math.floor(index / map_columns ) * tile_size; this.buffer.drawImage(image, source_x, source_y, tile_size, tile_size, destination_x, destination_y, tile_size, tile_size); } }; this.drawObject = function(image, source_x, source_y, destination_x, destination_y, width, height) { this.buffer.drawImage(image, source_x, source_y, width, height, Math.round(destination_x), Math.round(destination_y), width, height); }; this.resize = function(width, height, height_width_ratio) { if (height / width > height_width_ratio) { this.context.canvas.height = width * height_width_ratio; this.context.canvas.width = width; } else { this.context.canvas.height = height; this.context.canvas.width = height / height_width_ratio; } this.context.imageSmoothingEnabled = false; }; }; Display.prototype = { constructor : Display, render:function() { this.context.drawImage(this.buffer.canvas, 0, 0, this.buffer.canvas.width, this.buffer.canvas.height, 0, 0, this.context.canvas.width, this.context.canvas.height); }, }; ================================================ FILE: content/rabbit-trap/05/game-05.js ================================================ // Frank Poth 04/03/2018 /* Changes since part 4: 1. Added the Game.World.TileSet & Game.World.TileSet.Frame classes. 2. Added the Game.World.Object.Animator class. 3. Updated the Player object to include frame sets for his animations. Updated the Player to update position and animation separately. 4. Removed tile_size from the world object and only use tile_size from the tile_set object. 5. Moved Game.World.Player to Game.World.Object.Player. 6. Changed Game.World.prototype.update to better handle player animation updates. I'm starting to realize that perhaps the nesting constructor naming convension is getting out of hand. Game.World.Object.Animator is a very long and confusing constructor. I may just move everything under Game so everything is still namespaced, but things won't have super long ridiculous constructors. However, the long constructor is still useful to determine where a class is pertinent and what it's purpose is. Still, it feels very clunky, and I will probably change it soon. */ /* 04/11/2018 I noticed a problem with tunneling in the lower right nook of the T on the floor of the level. If you run into from the right and jump up, the player seemingly moves through the wall. This is not a problem with tile collision, but rather, tunneling. His jump velocity moves him upwards more than one full tile space. */ const Game = function() { this.world = new Game.World(); this.update = function() { this.world.update(); }; }; Game.prototype = { constructor : Game, }; Game.World = function(friction = 0.8, gravity = 2) { this.collider = new Game.World.Collider(); this.friction = friction; this.gravity = gravity; this.columns = 12; this.rows = 9; /* Here's where I define the new TileSet class. I give it complete control over tile_size because there should only be one source for tile_size for both drawing and collision as this game won't use scaling on individual objects. */ this.tile_set = new Game.World.TileSet(8, 16); this.player = new Game.World.Object.Player(100, 100);// The player in its new "namespace". this.map = [48,17,17,17,49,48,18,19,16,17,35,36, 10,39,39,39,16,18,39,31,31,31,39,07, 10,31,39,31,31,31,39,12,05,05,28,01, 35,06,39,39,31,39,39,19,39,39,08,09, 02,31,31,47,39,47,39,31,31,04,36,25, 10,39,39,31,39,39,39,31,31,31,39,37, 10,39,31,04,14,06,39,39,03,39,00,42, 49,02,31,31,11,39,39,31,11,00,42,09, 08,40,27,13,37,27,13,03,22,34,09,24]; this.collision_map = [00,04,04,04,00,00,04,04,04,04,04,00, 02,00,00,00,12,06,00,00,00,00,00,08, 02,00,00,00,00,00,00,09,05,05,01,00, 00,07,00,00,00,00,00,14,00,00,08,00, 02,00,00,01,00,01,00,00,00,13,04,00, 02,00,00,00,00,00,00,00,00,00,00,08, 02,00,00,13,01,07,00,00,11,00,09,00, 00,03,00,00,10,00,00,00,08,01,00,00, 00,00,01,01,00,01,01,01,00,00,00,00]; this.height = this.tile_set.tile_size * this.rows; // these changed to use tile_set.tile_size this.width = this.tile_set.tile_size * this.columns;// I got rid of this.tile_size in Game.World }; Game.World.prototype = { constructor: Game.World, collideObject:function(object) { if (object.getLeft() < 0 ) { object.setLeft(0); object.velocity_x = 0; } else if (object.getRight() > this.width ) { object.setRight(this.width); object.velocity_x = 0; } if (object.getTop() < 0 ) { object.setTop(0); object.velocity_y = 0; } else if (object.getBottom() > this.height) { object.setBottom(this.height); object.velocity_y = 0; object.jumping = false; } var bottom, left, right, top, value; top = Math.floor(object.getTop() / this.tile_set.tile_size); left = Math.floor(object.getLeft() / this.tile_set.tile_size); value = this.collision_map[top * this.columns + left]; this.collider.collide(value, object, left * this.tile_set.tile_size, top * this.tile_set.tile_size, this.tile_set.tile_size); top = Math.floor(object.getTop() / this.tile_set.tile_size); right = Math.floor(object.getRight() / this.tile_set.tile_size); value = this.collision_map[top * this.columns + right]; this.collider.collide(value, object, right * this.tile_set.tile_size, top * this.tile_set.tile_size, this.tile_set.tile_size); bottom = Math.floor(object.getBottom() / this.tile_set.tile_size); left = Math.floor(object.getLeft() / this.tile_set.tile_size); value = this.collision_map[bottom * this.columns + left]; this.collider.collide(value, object, left * this.tile_set.tile_size, bottom * this.tile_set.tile_size, this.tile_set.tile_size); bottom = Math.floor(object.getBottom() / this.tile_set.tile_size); right = Math.floor(object.getRight() / this.tile_set.tile_size); value = this.collision_map[bottom * this.columns + right]; this.collider.collide(value, object, right * this.tile_set.tile_size, bottom * this.tile_set.tile_size, this.tile_set.tile_size); }, /* This function changed to update the player's position and then do collision, and then update the animation based on the player's final condition. */ update:function() { this.player.updatePosition(this.gravity, this.friction); this.collideObject(this.player); this.player.updateAnimation(); } }; Game.World.Collider = function() { this.collide = function(value, object, tile_x, tile_y, tile_size) { switch(value) { case 1: this.collidePlatformTop (object, tile_y ); break; case 2: this.collidePlatformRight (object, tile_x + tile_size); break; case 3: if (this.collidePlatformTop (object, tile_y )) return;// If there's a collision, we don't need to check for anything else. this.collidePlatformRight (object, tile_x + tile_size); break; case 4: this.collidePlatformBottom (object, tile_y + tile_size); break; case 5: if (this.collidePlatformTop (object, tile_y )) return; this.collidePlatformBottom (object, tile_y + tile_size); break; case 6: if (this.collidePlatformRight(object, tile_x + tile_size)) return; this.collidePlatformBottom (object, tile_y + tile_size); break; case 7: if (this.collidePlatformTop (object, tile_y )) return; if (this.collidePlatformRight(object, tile_x + tile_size)) return; this.collidePlatformBottom (object, tile_y + tile_size); break; case 8: this.collidePlatformLeft (object, tile_x ); break; case 9: if (this.collidePlatformTop (object, tile_y )) return; this.collidePlatformLeft (object, tile_x ); break; case 10: if (this.collidePlatformLeft (object, tile_x )) return; this.collidePlatformRight (object, tile_x + tile_size); break; case 11: if (this.collidePlatformTop (object, tile_y )) return; if (this.collidePlatformLeft (object, tile_x )) return; this.collidePlatformRight (object, tile_x + tile_size); break; case 12: if (this.collidePlatformLeft (object, tile_x )) return; this.collidePlatformBottom (object, tile_y + tile_size); break; case 13: if (this.collidePlatformTop (object, tile_y )) return; if (this.collidePlatformLeft (object, tile_x )) return; this.collidePlatformBottom (object, tile_y + tile_size); break; case 14: if (this.collidePlatformLeft (object, tile_x )) return; if (this.collidePlatformRight(object, tile_x + tile_size)) return; // Had to change this since part 4. I forgot to add tile_size this.collidePlatformBottom (object, tile_y + tile_size); break; case 15: if (this.collidePlatformTop (object, tile_y )) return; if (this.collidePlatformLeft (object, tile_x )) return; if (this.collidePlatformRight(object, tile_x + tile_size)) return; this.collidePlatformBottom (object, tile_y + tile_size); break; } } }; Game.World.Collider.prototype = { constructor: Game.World.Collider, collidePlatformBottom:function(object, tile_bottom) { if (object.getTop() < tile_bottom && object.getOldTop() >= tile_bottom) { object.setTop(tile_bottom); object.velocity_y = 0; return true; } return false; }, collidePlatformLeft:function(object, tile_left) { if (object.getRight() > tile_left && object.getOldRight() <= tile_left) { object.setRight(tile_left - 0.01); object.velocity_x = 0; return true; } return false; }, collidePlatformRight:function(object, tile_right) { if (object.getLeft() < tile_right && object.getOldLeft() >= tile_right) { object.setLeft(tile_right); object.velocity_x = 0; return true; } return false; }, collidePlatformTop:function(object, tile_top) { if (object.getBottom() > tile_top && object.getOldBottom() <= tile_top) { object.setBottom(tile_top - 0.01); object.velocity_y = 0; object.jumping = false; return true; } return false; } }; Game.World.Object = function(x, y, width, height) { this.height = height; this.width = width; this.x = x; this.x_old = x; this.y = y; this.y_old = y; }; Game.World.Object.prototype = { constructor:Game.World.Object, /* These functions are used to get and set the different side positions of the object. */ getBottom: function() { return this.y + this.height; }, getLeft: function() { return this.x; }, getRight: function() { return this.x + this.width; }, getTop: function() { return this.y; }, getOldBottom:function() { return this.y_old + this.height; }, getOldLeft: function() { return this.x_old; }, getOldRight: function() { return this.x_old + this.width; }, getOldTop: function() { return this.y_old }, setBottom: function(y) { this.y = y - this.height; }, setLeft: function(x) { this.x = x; }, setRight: function(x) { this.x = x - this.width; }, setTop: function(y) { this.y = y; }, setOldBottom:function(y) { this.y_old = y - this.height; }, setOldLeft: function(x) { this.x_old = x; }, setOldRight: function(x) { this.x_old = x - this.width; }, setOldTop: function(y) { this.y_old = y; } }; Game.World.Object.Animator = function(frame_set, delay) { this.count = 0; this.delay = (delay >= 1) ? delay : 1; this.frame_set = frame_set; this.frame_index = 0; this.frame_value = frame_set[0]; this.mode = "pause"; }; Game.World.Object.Animator.prototype = { constructor:Game.World.Object.Animator, animate:function() { switch(this.mode) { case "loop" : this.loop(); break; case "pause": break; } }, changeFrameSet(frame_set, mode, delay = 10, frame_index = 0) { if (this.frame_set === frame_set) { return; } this.count = 0; this.delay = delay; this.frame_set = frame_set; this.frame_index = frame_index; this.frame_value = frame_set[frame_index]; this.mode = mode; }, loop:function() { this.count ++; while(this.count > this.delay) { this.count -= this.delay; this.frame_index = (this.frame_index < this.frame_set.length - 1) ? this.frame_index + 1 : 0; this.frame_value = this.frame_set[this.frame_index]; } } }; /* The player now also extends the Game.World.Object.Animator class. I also added a direction_x variable to help determine which way the player is facing for animation. */ Game.World.Object.Player = function(x, y) { Game.World.Object.call(this, 100, 100, 7, 14); Game.World.Object.Animator.call(this, Game.World.Object.Player.prototype.frame_sets["idle-left"], 10); this.jumping = true; this.direction_x = -1; this.velocity_x = 0; this.velocity_y = 0; }; Game.World.Object.Player.prototype = { constructor:Game.World.Object.Player, /* The values in these arrays correspond to the TileSet.Frame objects in the tile_set. They are just hardcoded in here now, but when the tileset information is eventually loaded from a json file, this will be allocated dynamically in some sort of loading function. */ frame_sets: { "idle-left" : [0], "jump-left" : [1], "move-left" : [2, 3, 4, 5], "idle-right": [6], "jump-right": [7], "move-right": [8, 9, 10, 11] }, jump: function() { if (!this.jumping) { this.jumping = true; this.velocity_y -= 20; } }, moveLeft: function() { this.direction_x = -1;// Make sure to set the player's direction. this.velocity_x -= 0.55; }, moveRight:function(frame_set) { this.direction_x = 1; this.velocity_x += 0.55; }, /* Because animation is entirely dependent on the player's movement at this point, I made a separate update function just for animation to be called after collision between the player and the world. This gives the most accurate animations for what the player is doing movement wise on the screen. */ updateAnimation:function() { if (this.velocity_y < 0) { if (this.direction_x < 0) this.changeFrameSet(this.frame_sets["jump-left"], "pause"); else this.changeFrameSet(this.frame_sets["jump-right"], "pause"); } else if (this.direction_x < 0) { if (this.velocity_x < -0.1) this.changeFrameSet(this.frame_sets["move-left"], "loop", 5); else this.changeFrameSet(this.frame_sets["idle-left"], "pause"); } else if (this.direction_x > 0) { if (this.velocity_x > 0.1) this.changeFrameSet(this.frame_sets["move-right"], "loop", 5); else this.changeFrameSet(this.frame_sets["idle-right"], "pause"); } this.animate(); }, /* This used to be the update function, but now it's a little bit better. It takes gravity and friction as parameters so the player class can decide what to do with them. */ updatePosition:function(gravity, friction) {// Changed from the update function this.x_old = this.x; this.y_old = this.y; this.velocity_y += gravity; this.x += this.velocity_x; this.y += this.velocity_y; this.velocity_x *= friction; this.velocity_y *= friction; } }; /* Double prototype inheritance from Object and Animator. */ Object.assign(Game.World.Object.Player.prototype, Game.World.Object.prototype); Object.assign(Game.World.Object.Player.prototype, Game.World.Object.Animator.prototype); Game.World.Object.Player.prototype.constructor = Game.World.Object.Player; /* The TileSheet class was taken from the Display class and renamed TileSet. It does all the same stuff, but it doesn't have an image reference and it also defines specific regions in the tile set image that correspond to the player's sprite animation frames. Later, this will all be set in a level loading function just in case I want to add functionality to add in another tile sheet graphic with different terrain. */ Game.World.TileSet = function(columns, tile_size) { this.columns = columns; this.tile_size = tile_size; let f = Game.World.TileSet.Frame; /* An array of all the frames in the tile sheet image. */ this.frames = [new f(115, 96, 13, 16, 0, -2), // idle-left new f( 50, 96, 13, 16, 0, -2), // jump-left new f(102, 96, 13, 16, 0, -2), new f(89, 96, 13, 16, 0, -2), new f(76, 96, 13, 16, 0, -2), new f(63, 96, 13, 16, 0, -2), // walk-left new f( 0, 112, 13, 16, 0, -2), // idle-right new f( 65, 112, 13, 16, 0, -2), // jump-right new f( 13, 112, 13, 16, 0, -2), new f(26, 112, 13, 16, 0, -2), new f(39, 112, 13, 16, 0, -2), new f(52, 112, 13, 16, 0, -2) // walk-right ]; }; Game.World.TileSet.prototype = { constructor: Game.World.TileSet }; /* The Frame class just defines a region in a tilesheet to cut out. It's a rectangle. It has an x and y offset used for drawing the cut out sprite image to the screen, which allows sprites to be positioned anywhere in the tile sheet image rather than being forced to adhere to a grid like tile graphics. This is more natural because sprites often fluctuate in size and won't always fit in a 16x16 grid. */ Game.World.TileSet.Frame = function(x, y, width, height, offset_x, offset_y) { this.x = x; this.y = y; this.width = width; this.height = height; this.offset_x = offset_x; this.offset_y = offset_y; }; Game.World.TileSet.Frame.prototype = { constructor: Game.World.TileSet.Frame }; ================================================ FILE: content/rabbit-trap/05/main-05.js ================================================ // Frank Poth 04/03/2018 /* Changes: 1. I added an AssetsManager class which will eventually store all my graphics and sounds. 2. The render function now draws the player's frame instead of a square like in part 4. 3. The resize function now stretches the display canvas to the full viewport capacity. The project is starting to grow unmanagable as it grows. Luckily my IPO structure is dramatically decreasing the amount of rewrites I have to do to other classes, but since most of my code is in the Game class, edits in that class are becoming rather tedious. As the project grows I will have to focus my videos more on individual changes and ignore the vast bulk of existing code. */ window.addEventListener("load", function(event) { "use strict"; //// CLASSES //// /* The assets manager will be responsible for loading and storing graphics for the game. Because it only has to load the tilesheet image right now, it's very specific about what it does. */ const AssetsManager = function() { this.tile_set_image = undefined; }; AssetsManager.prototype = { constructor: Game.AssetsManager, loadTileSetImage:function(url, callback) { this.tile_set_image = new Image(); this.tile_set_image.addEventListener("load", function(event) { callback(); }, { once : true}); this.tile_set_image.src = url; } }; /////////////////// //// FUNCTIONS //// /////////////////// var keyDownUp = function(event) { controller.keyDownUp(event.type, event.keyCode); }; var resize = function(event) { display.resize(document.documentElement.clientWidth, document.documentElement.clientHeight, game.world.height / game.world.width); display.render(); }; /* The render function uses the new display methods now. I will eventually have to create some sort of object manager when I get more objects on the screen. */ var render = function() { display.drawMap (assets_manager.tile_set_image, game.world.tile_set.columns, game.world.map, game.world.columns, game.world.tile_set.tile_size); let frame = game.world.tile_set.frames[game.world.player.frame_value]; display.drawObject(assets_manager.tile_set_image, frame.x, frame.y, game.world.player.x + Math.floor(game.world.player.width * 0.5 - frame.width * 0.5) + frame.offset_x, game.world.player.y + frame.offset_y, frame.width, frame.height); display.render(); }; var update = function() { if (controller.left.active ) { game.world.player.moveLeft (); } if (controller.right.active) { game.world.player.moveRight(); } if (controller.up.active ) { game.world.player.jump(); controller.up.active = false; } game.update(); }; ///////////////// //// OBJECTS //// ///////////////// var assets_manager = new AssetsManager();// Behold the new assets manager! var controller = new Controller(); var display = new Display(document.querySelector("canvas")); var game = new Game(); var engine = new Engine(1000/30, render, update); //////////////////// //// INITIALIZE //// //////////////////// /* This is going to have to be moved to a setup function inside of the Display class or something. Leaving it out here is kind of sloppy. */ display.buffer.canvas.height = game.world.height; display.buffer.canvas.width = game.world.width; display.buffer.imageSmoothingEnabled = false; /* Now my image is loaded into the assets manager instead of the display object. The callback starts the game engine when the graphic is loaded. */ assets_manager.loadTileSetImage("rabbit-trap.png", () => { resize(); engine.start(); }); window.addEventListener("keydown", keyDownUp); window.addEventListener("keyup", keyDownUp); window.addEventListener("resize", resize); }); ================================================ FILE: content/rabbit-trap/06/engine-06.js ================================================ // Frank Poth 04/09/2018 /* I made a minor mistake the first time I wrote this engine class. Instead of calling window.requestAnimationFrame before I update my game logic, I called it after. This meant that even if I tried to stop my game loop from inside the game logic, RAF would still be called after I requested a stop. To fix this, I simply moved the request to the top of my game loop function, which is Engine.run. */ const Engine = function(time_step, update, render) { this.accumulated_time = 0; this.animation_frame_request = undefined, this.time = undefined, this.time_step = time_step, this.updated = false; this.update = update; this.render = render; this.run = function(time_stamp) { /* I moved this line from the bottom of this function to the top. This is better anyway, because it ensures that if my game logic runs too long, a new frame will already be requested before 30 or 60 frames pass and I miss a request entirely. This could cause a "spiral of death" for my CPU, but since I have the frame dropping safety if statement, this probably won't crash the user's computer. */ this.animation_frame_request = window.requestAnimationFrame(this.handleRun); this.accumulated_time += time_stamp - this.time; this.time = time_stamp; /* This is the safety if statement. */ if (this.accumulated_time >= this.time_step * 3) { this.accumulated_time = this.time_step; } while(this.accumulated_time >= this.time_step) { this.accumulated_time -= this.time_step; this.update(time_stamp); this.updated = true; } if (this.updated) { this.updated = false; this.render(time_stamp); } }; this.handleRun = (time_step) => { this.run(time_step); }; }; Engine.prototype = { constructor:Engine, start:function() { this.accumulated_time = this.time_step; this.time = window.performance.now(); this.animation_frame_request = window.requestAnimationFrame(this.handleRun); }, stop:function() { window.cancelAnimationFrame(this.animation_frame_request); } }; ================================================ FILE: content/rabbit-trap/06/game-06.js ================================================ // Frank Poth 04/06/2018 /* Changes since part 5: 1. Simplified Class constructors by removing multiple prefixes: For example: Game.World.Object.Player is now Game.Player. 2. Added Game.World.prototype.setup to setup world from json level data. 3. Added the Game.MovingObject class to separate Objects from MovingObjects. Game.Player now inherits from MovingObject instead of Object. 4. Changed Game.World.map to Game.World.graphical_map. 5. Made the Game.Collider.collideObject routing function do all y first collision checks. This simply means that I check collision on top and bottom before left and right. 6. Removed world boundary collision from World.collideObject so the player can move off screen enough to hit a door. 7. Added the Game.Door class. 8. Added functions to get the center position of Game.Object and Game.MovingObject. 9. Organized classes by alphabeticalish order. 10. Put a limit on player velocity because there was a problem with "tunneling" through tiles due to jump movement speed. 11. Changed the player's hitbox size and his frame offsets for animation. */ const Game = function() { this.world = new Game.World(); this.update = function() { this.world.update(); }; }; Game.prototype = { constructor : Game }; Game.Animator = function(frame_set, delay) { this.count = 0; this.delay = (delay >= 1) ? delay : 1; this.frame_set = frame_set; this.frame_index = 0; this.frame_value = frame_set[0]; this.mode = "pause"; }; Game.Animator.prototype = { constructor:Game.Animator, animate:function() { switch(this.mode) { case "loop" : this.loop(); break; case "pause": break; } }, changeFrameSet(frame_set, mode, delay = 10, frame_index = 0) { if (this.frame_set === frame_set) { return; } this.count = 0; this.delay = delay; this.frame_set = frame_set; this.frame_index = frame_index; this.frame_value = frame_set[frame_index]; this.mode = mode; }, loop:function() { this.count ++; while(this.count > this.delay) { this.count -= this.delay; this.frame_index = (this.frame_index < this.frame_set.length - 1) ? this.frame_index + 1 : 0; this.frame_value = this.frame_set[this.frame_index]; } } }; Game.Collider = function() { /* I changed this so all the checks happen in y first order. */ this.collide = function(value, object, tile_x, tile_y, tile_size) { switch(value) { case 1: this.collidePlatformTop (object, tile_y ); break; case 2: this.collidePlatformRight (object, tile_x + tile_size); break; case 3: if (this.collidePlatformTop (object, tile_y )) return; this.collidePlatformRight (object, tile_x + tile_size); break; case 4: this.collidePlatformBottom (object, tile_y + tile_size); break; case 5: if (this.collidePlatformTop (object, tile_y )) return; this.collidePlatformBottom (object, tile_y + tile_size); break; case 6: if (this.collidePlatformRight (object, tile_x + tile_size)) return; this.collidePlatformBottom (object, tile_y + tile_size); break; case 7: if (this.collidePlatformTop (object, tile_y )) return; if (this.collidePlatformBottom (object, tile_y + tile_size)) return; this.collidePlatformRight (object, tile_x + tile_size); break; case 8: this.collidePlatformLeft (object, tile_x ); break; case 9: if (this.collidePlatformTop (object, tile_y )) return; this.collidePlatformLeft (object, tile_x ); break; case 10: if (this.collidePlatformLeft (object, tile_x )) return; this.collidePlatformRight (object, tile_x + tile_size); break; case 11: if (this.collidePlatformTop (object, tile_y )) return; if (this.collidePlatformLeft (object, tile_x )) return; this.collidePlatformRight (object, tile_x + tile_size); break; case 12: if (this.collidePlatformBottom (object, tile_y + tile_size)) return; this.collidePlatformLeft (object, tile_x ); break; case 13: if (this.collidePlatformTop (object, tile_y )) return; if (this.collidePlatformBottom (object, tile_y + tile_size)) return; this.collidePlatformLeft (object, tile_x ); break; case 14: if (this.collidePlatformBottom (object, tile_y + tile_size)) return; if (this.collidePlatformLeft (object, tile_x )) return; this.collidePlatformRight (object, tile_x + tile_size); break; case 15: if (this.collidePlatformTop (object, tile_y )) return; if (this.collidePlatformBottom (object, tile_y + tile_size)) return; if (this.collidePlatformLeft (object, tile_x )) return; this.collidePlatformRight (object, tile_x + tile_size); break; } } }; Game.Collider.prototype = { constructor: Game.Collider, collidePlatformBottom:function(object, tile_bottom) { if (object.getTop() < tile_bottom && object.getOldTop() >= tile_bottom) { object.setTop(tile_bottom); object.velocity_y = 0; return true; } return false; }, collidePlatformLeft:function(object, tile_left) { if (object.getRight() > tile_left && object.getOldRight() <= tile_left) { object.setRight(tile_left - 0.01); object.velocity_x = 0; return true; } return false; }, collidePlatformRight:function(object, tile_right) { if (object.getLeft() < tile_right && object.getOldLeft() >= tile_right) { object.setLeft(tile_right); object.velocity_x = 0; return true; } return false; }, collidePlatformTop:function(object, tile_top) { if (object.getBottom() > tile_top && object.getOldBottom() <= tile_top) { object.setBottom(tile_top - 0.01); object.velocity_y = 0; object.jumping = false; return true; } return false; } }; Game.Frame = function(x, y, width, height, offset_x, offset_y) { this.x = x; this.y = y; this.width = width; this.height = height; this.offset_x = offset_x; this.offset_y = offset_y; }; Game.Frame.prototype = { constructor: Game.Frame }; Game.Object = function(x, y, width, height) { this.height = height; this.width = width; this.x = x; this.y = y; }; /* I added getCenterX, getCenterY, setCenterX, and setCenterY */ Game.Object.prototype = { constructor:Game.Object, getBottom : function() { return this.y + this.height; }, getCenterX: function() { return this.x + this.width * 0.5; }, getCenterY: function() { return this.y + this.height * 0.5; }, getLeft : function() { return this.x; }, getRight : function() { return this.x + this.width; }, getTop : function() { return this.y; }, setBottom : function(y) { this.y = y - this.height; }, setCenterX: function(x) { this.x = x - this.width * 0.5; }, setCenterY: function(y) { this.y = y - this.height * 0.5; }, setLeft : function(x) { this.x = x; }, setRight : function(x) { this.x = x - this.width; }, setTop : function(y) { this.y = y; } }; Game.MovingObject = function(x, y, width, height, velocity_max = 15) { Game.Object.call(this, x, y, width, height); this.jumping = false; this.velocity_max = velocity_max;// added velocity_max so velocity can't go past 16 this.velocity_x = 0; this.velocity_y = 0; this.x_old = x; this.y_old = y; }; /* I added setCenterX, setCenterY, getCenterX, and getCenterY */ Game.MovingObject.prototype = { getOldBottom : function() { return this.y_old + this.height; }, getOldCenterX: function() { return this.x_old + this.width * 0.5; }, getOldCenterY: function() { return this.y_old + this.height * 0.5; }, getOldLeft : function() { return this.x_old; }, getOldRight : function() { return this.x_old + this.width; }, getOldTop : function() { return this.y_old; }, setOldBottom : function(y) { this.y_old = y - this.height; }, setOldCenterX: function(x) { this.x_old = x - this.width * 0.5; }, setOldCenterY: function(y) { this.y_old = y - this.height * 0.5; }, setOldLeft : function(x) { this.x_old = x; }, setOldRight : function(x) { this.x_old = x - this.width; }, setOldTop : function(y) { this.y_old = y; } }; Object.assign(Game.MovingObject.prototype, Game.Object.prototype); Game.MovingObject.prototype.constructor = Game.MovingObject; Game.Door = function(door) { Game.Object.call(this, door.x, door.y, door.width, door.height); this.destination_x = door.destination_x; this.destination_y = door.destination_y; this.destination_zone = door.destination_zone; }; Game.Door.prototype = { /* Tests for collision between this door object and a MovingObject. */ collideObject(object) { let center_x = object.getCenterX(); let center_y = object.getCenterY(); if (center_x < this.getLeft() || center_x > this.getRight() || center_y < this.getTop() || center_y > this.getBottom()) return false; return true; } }; Object.assign(Game.Door.prototype, Game.Object.prototype); Game.Door.prototype.constructor = Game.Door; Game.Player = function(x, y) { Game.MovingObject.call(this, x, y, 7, 12); Game.Animator.call(this, Game.Player.prototype.frame_sets["idle-left"], 10); this.jumping = true; this.direction_x = -1; this.velocity_x = 0; this.velocity_y = 0; }; Game.Player.prototype = { frame_sets: { "idle-left" : [0], "jump-left" : [1], "move-left" : [2, 3, 4, 5], "idle-right": [6], "jump-right": [7], "move-right": [8, 9, 10, 11] }, jump: function() { /* Made it so you can only jump if you aren't falling faster than 10px per frame. */ if (!this.jumping && this.velocity_y < 10) { this.jumping = true; this.velocity_y -= 13; } }, moveLeft: function() { this.direction_x = -1; this.velocity_x -= 0.55; }, moveRight:function(frame_set) { this.direction_x = 1; this.velocity_x += 0.55; }, updateAnimation:function() { if (this.velocity_y < 0) { if (this.direction_x < 0) this.changeFrameSet(this.frame_sets["jump-left"], "pause"); else this.changeFrameSet(this.frame_sets["jump-right"], "pause"); } else if (this.direction_x < 0) { if (this.velocity_x < -0.1) this.changeFrameSet(this.frame_sets["move-left"], "loop", 5); else this.changeFrameSet(this.frame_sets["idle-left"], "pause"); } else if (this.direction_x > 0) { if (this.velocity_x > 0.1) this.changeFrameSet(this.frame_sets["move-right"], "loop", 5); else this.changeFrameSet(this.frame_sets["idle-right"], "pause"); } this.animate(); }, updatePosition:function(gravity, friction) { this.x_old = this.x; this.y_old = this.y; this.velocity_y += gravity; this.velocity_x *= friction; /* Made it so that velocity cannot exceed velocity_max */ if (Math.abs(this.velocity_x) > this.velocity_max) this.velocity_x = this.velocity_max * Math.sign(this.velocity_x); if (Math.abs(this.velocity_y) > this.velocity_max) this.velocity_y = this.velocity_max * Math.sign(this.velocity_y); this.x += this.velocity_x; this.y += this.velocity_y; } }; Object.assign(Game.Player.prototype, Game.MovingObject.prototype); Object.assign(Game.Player.prototype, Game.Animator.prototype); Game.Player.prototype.constructor = Game.Player; Game.TileSet = function(columns, tile_size) { this.columns = columns; this.tile_size = tile_size; let f = Game.Frame; this.frames = [new f(115, 96, 13, 16, 0, -4), // idle-left new f( 50, 96, 13, 16, 0, -4), // jump-left new f(102, 96, 13, 16, 0, -4), new f(89, 96, 13, 16, 0, -4), new f(76, 96, 13, 16, 0, -4), new f(63, 96, 13, 16, 0, -4), // walk-left new f( 0, 112, 13, 16, 0, -4), // idle-right new f( 65, 112, 13, 16, 0, -4), // jump-right new f( 13, 112, 13, 16, 0, -4), new f(26, 112, 13, 16, 0, -4), new f(39, 112, 13, 16, 0, -4), new f(52, 112, 13, 16, 0, -4) // walk-right ]; }; Game.TileSet.prototype = { constructor: Game.TileSet }; Game.World = function(friction = 0.85, gravity = 2) { this.collider = new Game.Collider(); this.friction = friction; this.gravity = gravity; this.columns = 12; this.rows = 9; this.tile_set = new Game.TileSet(8, 16); this.player = new Game.Player(32, 76); this.zone_id = "00";// The current zone. this.doors = [];// The array of doors in the level. this.door = undefined; // If the player enters a door, the game will set this property to that door and the level will be loaded. this.height = this.tile_set.tile_size * this.rows; this.width = this.tile_set.tile_size * this.columns; }; Game.World.prototype = { constructor: Game.World, collideObject:function(object) { /* I got rid of the world boundary collision. Now it's up to the tiles to keep the player from falling out of the world. */ var bottom, left, right, top, value; top = Math.floor(object.getTop() / this.tile_set.tile_size); left = Math.floor(object.getLeft() / this.tile_set.tile_size); value = this.collision_map[top * this.columns + left]; this.collider.collide(value, object, left * this.tile_set.tile_size, top * this.tile_set.tile_size, this.tile_set.tile_size); top = Math.floor(object.getTop() / this.tile_set.tile_size); right = Math.floor(object.getRight() / this.tile_set.tile_size); value = this.collision_map[top * this.columns + right]; this.collider.collide(value, object, right * this.tile_set.tile_size, top * this.tile_set.tile_size, this.tile_set.tile_size); bottom = Math.floor(object.getBottom() / this.tile_set.tile_size); left = Math.floor(object.getLeft() / this.tile_set.tile_size); value = this.collision_map[bottom * this.columns + left]; this.collider.collide(value, object, left * this.tile_set.tile_size, bottom * this.tile_set.tile_size, this.tile_set.tile_size); bottom = Math.floor(object.getBottom() / this.tile_set.tile_size); right = Math.floor(object.getRight() / this.tile_set.tile_size); value = this.collision_map[bottom * this.columns + right]; this.collider.collide(value, object, right * this.tile_set.tile_size, bottom * this.tile_set.tile_size, this.tile_set.tile_size); }, /* The setup function takes a zone object generated from a zoneXX.json file. It sets all the world values to values of zone. If the player just passed through a door, it uses the this.door variable to change the player's location to wherever that door's destination goes. */ setup:function(zone) { /* Get the new tile maps, the new zone, and reset the doors array. */ this.graphical_map = zone.graphical_map; this.collision_map = zone.collision_map; this.columns = zone.columns; this.rows = zone.rows; this.doors = new Array(); this.zone_id = zone.id; /* Generate new doors. */ for (let index = zone.doors.length - 1; index > -1; -- index) { let door = zone.doors[index]; this.doors[index] = new Game.Door(door); } /* If the player entered into a door, this.door will reference that door. Here it will be used to set the player's location to the door's destination. */ if (this.door) { /* if a destination is equal to -1, that means it won't be used. Since each zone spans from 0 to its width and height, any negative number would be invalid. If a door's destination is -1, the player will keep his current position for that axis. */ if (this.door.destination_x != -1) { this.player.setCenterX (this.door.destination_x); this.player.setOldCenterX(this.door.destination_x);// It's important to reset the old position as well. } if (this.door.destination_y != -1) { this.player.setCenterY (this.door.destination_y); this.player.setOldCenterY(this.door.destination_y); } this.door = undefined;// Make sure to reset this.door so we don't trigger a zone load. } }, update:function() { this.player.updatePosition(this.gravity, this.friction); this.collideObject(this.player); /* Here we loop through all the doors in the current zone and check to see if the player is colliding with any. If he does collide with one, we set the world's door variable equal to that door, so we know to use it to load the next zone. */ for(let index = this.doors.length - 1; index > -1; -- index) { let door = this.doors[index]; if (door.collideObject(this.player)) { this.door = door; }; } this.player.updateAnimation(); } }; ================================================ FILE: content/rabbit-trap/06/main-06.js ================================================ // Frank Poth 04/06/2018 /* Changes: 1. The update function now check on every frame for game.world.door. If a door is selected, the game engine stops and the door's level is loaded. 2. When the game is first initialized at the bottom of this file, game.world is loaded using it's default values defined in its constructor. 3. The AssetsManager class has been changed to load both images and json. */ window.addEventListener("load", function(event) { "use strict"; //// CONSTANTS //// /* Each zone has a url that looks like: zoneXX.json, where XX is the current zone identifier. When loading zones, I use the game.world's zone identifier with these two constants to construct a url that points to the appropriate zone file. */ /* I updated this after I made the video. I decided to move the zone files into the 06 folder because I won't be using these levels again in future parts. */ const ZONE_PREFIX = "06/zone"; const ZONE_SUFFIX = ".json"; ///////////////// //// CLASSES //// ///////////////// const AssetsManager = function() { this.tile_set_image = undefined; }; AssetsManager.prototype = { constructor: Game.AssetsManager, /* Requests a file and hands the callback function the contents of that file parsed by JSON.parse. */ requestJSON:function(url, callback) { let request = new XMLHttpRequest(); request.addEventListener("load", function(event) { callback(JSON.parse(this.responseText)); }, { once:true }); request.open("GET", url); request.send(); }, /* Creates a new Image and sets its src attribute to the specified url. When the image loads, the callback function is called. */ requestImage:function(url, callback) { let image = new Image(); image.addEventListener("load", function(event) { callback(image); }, { once:true }); image.src = url; }, }; /////////////////// //// FUNCTIONS //// /////////////////// var keyDownUp = function(event) { controller.keyDownUp(event.type, event.keyCode); }; var resize = function(event) { display.resize(document.documentElement.clientWidth, document.documentElement.clientHeight, game.world.height / game.world.width); display.render(); }; var render = function() { display.drawMap (assets_manager.tile_set_image, game.world.tile_set.columns, game.world.graphical_map, game.world.columns, game.world.tile_set.tile_size); let frame = game.world.tile_set.frames[game.world.player.frame_value]; display.drawObject(assets_manager.tile_set_image, frame.x, frame.y, game.world.player.x + Math.floor(game.world.player.width * 0.5 - frame.width * 0.5) + frame.offset_x, game.world.player.y + frame.offset_y, frame.width, frame.height); display.render(); }; var update = function() { if (controller.left.active ) { game.world.player.moveLeft (); } if (controller.right.active) { game.world.player.moveRight(); } if (controller.up.active ) { game.world.player.jump(); controller.up.active = false; } game.update(); /* This if statement checks to see if a door has been selected by the player. If the player collides with a door, he selects it. The engine is then stopped and the assets_manager loads the door's level. */ if (game.world.door) { engine.stop(); /* Here I'm requesting the JSON file to use to populate the game.world object. */ assets_manager.requestJSON(ZONE_PREFIX + game.world.door.destination_zone + ZONE_SUFFIX, (zone) => { game.world.setup(zone); engine.start(); }); return; } }; ///////////////// //// OBJECTS //// ///////////////// var assets_manager = new AssetsManager(); var controller = new Controller(); var display = new Display(document.querySelector("canvas")); var game = new Game(); var engine = new Engine(1000/30, render, update); //////////////////// //// INITIALIZE //// //////////////////// display.buffer.canvas.height = game.world.height; display.buffer.canvas.width = game.world.width; display.buffer.imageSmoothingEnabled = false; assets_manager.requestJSON(ZONE_PREFIX + game.world.zone_id + ZONE_SUFFIX, (zone) => { game.world.setup(zone); assets_manager.requestImage("rabbit-trap.png", (image) => { assets_manager.tile_set_image = image; resize(); engine.start(); }); }); window.addEventListener("keydown", keyDownUp); window.addEventListener("keyup" , keyDownUp); window.addEventListener("resize" , resize); }); ================================================ FILE: content/rabbit-trap/06/script.txt ================================================ INTRO Hey, guys, my name is Frank and THIS is the Poth on Programming Video Log Part 6 of how to make a tile based platformer in pure HTML5 and JavaScript. This video is going to be about loading JSON levels with doors. You're definitely going to want this functionality in your game so stay tuned to find out how it's done! BULLETS 1. Example overview 2. Create JSON files 3. loading JSON files and using it a. initial load b. world.setup c. loading levels from update 4. trigger a level load by hitting a door a. hit door check in the update function b. setup loads the world and moves the player. In this video I'm going to show you how to create level data files using JSON, how to load level data and use it to populate the game world, and finally, how to trigger subsequent level loads by colliding with doors. If you have any comments or questions, be sure to leave them in the comments section, and if you start learning at any point during this video, go ahead and give a thumbs up so others can find it as well. ================================================ FILE: content/rabbit-trap/06/zone00.json ================================================ { "doors" : [ { "x":192, "y":64, "width":16, "height":16, "destination_zone":"01", "destination_x":0, "destination_y":-1 } ], "columns":12, "rows" :9, "graphical_map":[42,24,17,17,17,17,26,24,17,35,5,44,24,18,31,39,39,31,29,18,39,31,39,11,10,39,39,39,39,31,11,31,39,39,0,41,10,39,31,47,39,4,21,31,39,39,16,17,43,6,39,31,31,39,31,39,47,39,39,31,38,31,31,31,7,39,31,39,31,39,39,12,32,6,31,3,7,7,39,39,39,47,39,37,10,7,12,23,13,7,3,31,31,31,39,8,40,1,41,1,41,1,41,1,1,1,1,48], "collision_map":[0,0,4,4,4,4,0,0,4,4,4,0,0,6,0,0,0,0,8,6,0,0,0,8,2,0,0,0,0,0,10,0,0,0,9,0,2,0,0,1,0,13,6,0,0,0,12,4,0,7,0,0,0,0,0,0,1,0,0,0,2,0,0,0,11,0,0,0,0,0,0,9,0,3,0,9,0,3,0,0,0,1,0,8,0,0,1,0,0,0,3,0,0,0,0,8,0,0,0,0,0,0,0,1,1,1,1,0], "id":"00" } ================================================ FILE: content/rabbit-trap/06/zone01.json ================================================ { "doors" : [ { "x":-16, "y":64, "width":16, "height":16, "destination_zone":"00", "destination_x":192, "destination_y":-1 }, { "x":192, "y":16, "width":16, "height":16, "destination_zone":"02", "destination_x":0, "destination_y":-1 } ], "columns":12, "rows" :9, "graphical_map":[30,16,49,2,7,7,22,36,30,19,16,17,11,39,16,49,2,4,21,31,19,39,31,31,38,31,31,16,18,31,31,39,31,39,39,4,18,31,31,31,31,39,39,31,31,47,31,0,31,39,39,31,31,39,39,39,31,31,31,8,5,5,14,6,31,39,39,47,39,31,31,8,1,27,21,39,39,31,31,39,39,12,28,42,9,10,31,39,39,3,39,39,7,37,42,24,26,40,1,1,2,11,0,27,14,34,9,10], "collision_map":[0,4,0,0,0,0,0,4,0,4,4,4,2,0,12,0,0,4,6,0,14,0,0,0,2,0,0,12,6,0,0,0,0,0,0,9,6,0,0,0,0,0,0,0,0,1,0,8,0,0,0,0,0,0,0,0,0,0,0,8,1,1,1,7,0,0,0,1,0,0,0,8,0,0,6,0,0,0,0,0,0,9,1,0,0,2,0,0,0,11,0,0,9,0,0,0,0,0,1,1,1,0,1,1,0,0,0,0], "id":"01" } ================================================ FILE: content/rabbit-trap/06/zone02.json ================================================ { "doors" : [ { "x":-16, "y":16, "width":16, "height":16, "destination_zone":"01", "destination_x":192, "destination_y":-1 }, { "x":32, "y":144, "width":80, "height":40, "destination_zone":"03", "destination_x":-1, "destination_y":0 } ], "columns":12, "rows" :9, "graphical_map":[17,17,18,7,16,43,21,16,17,17,17,17,31,31,39,4,5,21,39,39,31,31,31,7,6,31,31,31,31,31,39,39,31,31,7,7,27,6,39,31,39,39,31,31,4,5,5,14,10,31,31,47,39,39,31,31,39,39,39,37,40,2,39,39,39,39,3,31,31,39,39,8,24,30,31,31,47,31,20,13,39,31,31,8,49,38,31,39,39,39,31,22,13,39,39,8,8,10,31,47,31,47,31,11,37,1,1,42], "collision_map":[4,4,4,0,0,0,4,4,4,4,4,0,0,0,0,12,4,6,0,0,0,0,0,8,3,0,0,0,0,0,0,0,0,0,9,0,0,7,0,0,0,0,0,0,13,5,4,0,2,0,0,1,0,0,0,0,0,0,0,8,0,3,0,0,0,0,11,0,0,0,0,8,0,2,0,0,1,0,12,3,0,0,0,8,0,2,0,0,0,0,0,8,3,0,0,8,0,2,0,1,0,1,0,8,0,1,1,0], "id":"02" } ================================================ FILE: content/rabbit-trap/06/zone03.json ================================================ { "doors" : [ { "x":32, "y":-16, "width":80, "height":16, "destination_zone":"02", "destination_x":-1, "destination_y":143 }, { "x":192, "y":80, "width":16, "height":64, "destination_zone":"04", "destination_x":0, "destination_y":-1 } ], "columns":12, "rows" :9, "graphical_map":[16,18,31,31,31,31,31,11,16,17,25,17,7,39,39,47,39,47,39,19,39,31,19,0,7,7,39,39,39,39,39,39,39,31,39,8,1,1,2,39,47,31,31,3,31,31,39,29,24,17,30,31,39,39,12,23,5,5,5,21,18,31,19,39,47,31,19,39,39,39,31,31,7,31,39,31,39,39,39,31,39,39,31,31,7,7,39,39,3,31,31,31,39,31,0,2,28,1,1,27,46,1,2,7,7,0,42,32], "collision_map":[0,6,0,0,0,0,0,8,4,4,0,0,2,0,0,1,0,1,0,14,0,0,12,0,0,3,0,0,0,0,0,0,0,0,0,8,0,0,3,0,1,0,0,11,0,0,0,8,0,4,2,0,0,0,9,4,5,5,5,4,2,0,14,0,1,0,14,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,11,0,0,0,0,0,9,1,0,0,1,1,0,1,1,1,1,1,0,0], "id":"03" } ================================================ FILE: content/rabbit-trap/06/zone04.json ================================================ { "doors" : [ { "x":-16, "y":80, "width":16, "height":64, "destination_zone":"03", "destination_x":192, "destination_y":-1 } ], "columns":12, "rows" :9, "graphical_map":[26,40,41,42,10,16,17,25,35,36,25,26,42,9,24,17,18,31,39,11,39,39,20,34,9,24,18,31,39,39,4,23,6,31,39,29,25,18,31,39,39,39,31,31,31,39,39,11,19,39,31,39,31,47,31,31,31,47,31,37,31,39,39,31,31,39,31,47,31,39,4,34,31,31,39,4,14,6,31,39,39,7,7,8,0,2,31,39,11,31,31,31,7,7,7,8,44,18,4,14,33,28,2,0,1,1,1,48], "collision_map":[0,0,0,0,0,4,4,0,4,4,0,0,0,0,0,4,6,0,0,10,0,0,12,0,0,0,6,0,0,0,13,4,7,0,0,8,0,6,0,0,0,0,0,0,0,0,0,8,6,0,0,0,0,1,0,0,0,1,0,8,0,0,0,0,0,0,0,1,0,0,9,0,0,0,0,13,1,7,0,0,0,9,0,0,1,3,0,0,10,0,0,0,9,0,0,0,0,0,1,1,0,1,1,1,0,0,0,0], "id":"04" } ================================================ FILE: content/rabbit-trap/07/game-07.js ================================================ // Frank Poth 04/18/2018 /* Changes since part 6: 1. Added the carrots array to the zone file. 2. Moved the collideObject method out of Game.Door and into Game.Object. 3. Renamed collideObject to collideObjectCenter and made a new collideObject function for rectangular collision detection. 4. Added the Game.Carrot class and Game.Grass class. 5. Added frames for carrots and grass to the tile_set. 6. Made a slight change to the Game.Animator constructor. 7. Added carrot_count to count carrots. 8. Added the grass array to the zone file. Also reflected in Game.World */ const Game = function() { this.world = new Game.World(); this.update = function() { this.world.update(); }; }; Game.prototype = { constructor : Game }; // Made the default animation type "loop": Game.Animator = function(frame_set, delay, mode = "loop") { this.count = 0; this.delay = (delay >= 1) ? delay : 1; this.frame_set = frame_set; this.frame_index = 0; this.frame_value = frame_set[0]; this.mode = mode; }; Game.Animator.prototype = { constructor:Game.Animator, animate:function() { switch(this.mode) { case "loop" : this.loop(); break; case "pause": break; } }, changeFrameSet(frame_set, mode, delay = 10, frame_index = 0) { if (this.frame_set === frame_set) { return; } this.count = 0; this.delay = delay; this.frame_set = frame_set; this.frame_index = frame_index; this.frame_value = frame_set[frame_index]; this.mode = mode; }, loop:function() { this.count ++; while(this.count > this.delay) { this.count -= this.delay; this.frame_index = (this.frame_index < this.frame_set.length - 1) ? this.frame_index + 1 : 0; this.frame_value = this.frame_set[this.frame_index]; } } }; Game.Collider = function() { /* I changed this so all the checks happen in y first order. */ this.collide = function(value, object, tile_x, tile_y, tile_size) { switch(value) { case 1: this.collidePlatformTop (object, tile_y ); break; case 2: this.collidePlatformRight (object, tile_x + tile_size); break; case 3: if (this.collidePlatformTop (object, tile_y )) return; this.collidePlatformRight (object, tile_x + tile_size); break; case 4: this.collidePlatformBottom (object, tile_y + tile_size); break; case 5: if (this.collidePlatformTop (object, tile_y )) return; this.collidePlatformBottom (object, tile_y + tile_size); break; case 6: if (this.collidePlatformRight (object, tile_x + tile_size)) return; this.collidePlatformBottom (object, tile_y + tile_size); break; case 7: if (this.collidePlatformTop (object, tile_y )) return; if (this.collidePlatformBottom (object, tile_y + tile_size)) return; this.collidePlatformRight (object, tile_x + tile_size); break; case 8: this.collidePlatformLeft (object, tile_x ); break; case 9: if (this.collidePlatformTop (object, tile_y )) return; this.collidePlatformLeft (object, tile_x ); break; case 10: if (this.collidePlatformLeft (object, tile_x )) return; this.collidePlatformRight (object, tile_x + tile_size); break; case 11: if (this.collidePlatformTop (object, tile_y )) return; if (this.collidePlatformLeft (object, tile_x )) return; this.collidePlatformRight (object, tile_x + tile_size); break; case 12: if (this.collidePlatformBottom (object, tile_y + tile_size)) return; this.collidePlatformLeft (object, tile_x ); break; case 13: if (this.collidePlatformTop (object, tile_y )) return; if (this.collidePlatformBottom (object, tile_y + tile_size)) return; this.collidePlatformLeft (object, tile_x ); break; case 14: if (this.collidePlatformBottom (object, tile_y + tile_size)) return; if (this.collidePlatformLeft (object, tile_x )) return; this.collidePlatformRight (object, tile_x + tile_size); break; case 15: if (this.collidePlatformTop (object, tile_y )) return; if (this.collidePlatformBottom (object, tile_y + tile_size)) return; if (this.collidePlatformLeft (object, tile_x )) return; this.collidePlatformRight (object, tile_x + tile_size); break; } } }; Game.Collider.prototype = { constructor: Game.Collider, collidePlatformBottom:function(object, tile_bottom) { if (object.getTop() < tile_bottom && object.getOldTop() >= tile_bottom) { object.setTop(tile_bottom); object.velocity_y = 0; return true; } return false; }, collidePlatformLeft:function(object, tile_left) { if (object.getRight() > tile_left && object.getOldRight() <= tile_left) { object.setRight(tile_left - 0.01); object.velocity_x = 0; return true; } return false; }, collidePlatformRight:function(object, tile_right) { if (object.getLeft() < tile_right && object.getOldLeft() >= tile_right) { object.setLeft(tile_right); object.velocity_x = 0; return true; } return false; }, collidePlatformTop:function(object, tile_top) { if (object.getBottom() > tile_top && object.getOldBottom() <= tile_top) { object.setBottom(tile_top - 0.01); object.velocity_y = 0; object.jumping = false; return true; } return false; } }; // Added default values of 0 for offset_x and offset_y Game.Frame = function(x, y, width, height, offset_x = 0, offset_y = 0) { this.x = x; this.y = y; this.width = width; this.height = height; this.offset_x = offset_x; this.offset_y = offset_y; }; Game.Frame.prototype = { constructor: Game.Frame }; Game.Object = function(x, y, width, height) { this.height = height; this.width = width; this.x = x; this.y = y; }; Game.Object.prototype = { constructor:Game.Object, /* Now does rectangular collision detection. */ collideObject:function(object) { if (this.getRight() < object.getLeft() || this.getBottom() < object.getTop() || this.getLeft() > object.getRight() || this.getTop() > object.getBottom()) return false; return true; }, /* Does rectangular collision detection with the center of the object. */ collideObjectCenter:function(object) { let center_x = object.getCenterX(); let center_y = object.getCenterY(); if (center_x < this.getLeft() || center_x > this.getRight() || center_y < this.getTop() || center_y > this.getBottom()) return false; return true; }, getBottom : function() { return this.y + this.height; }, getCenterX: function() { return this.x + this.width * 0.5; }, getCenterY: function() { return this.y + this.height * 0.5; }, getLeft : function() { return this.x; }, getRight : function() { return this.x + this.width; }, getTop : function() { return this.y; }, setBottom : function(y) { this.y = y - this.height; }, setCenterX: function(x) { this.x = x - this.width * 0.5; }, setCenterY: function(y) { this.y = y - this.height * 0.5; }, setLeft : function(x) { this.x = x; }, setRight : function(x) { this.x = x - this.width; }, setTop : function(y) { this.y = y; } }; Game.MovingObject = function(x, y, width, height, velocity_max = 15) { Game.Object.call(this, x, y, width, height); this.jumping = false; this.velocity_max = velocity_max;// added velocity_max so velocity can't go past 16 this.velocity_x = 0; this.velocity_y = 0; this.x_old = x; this.y_old = y; }; /* I added setCenterX, setCenterY, getCenterX, and getCenterY */ Game.MovingObject.prototype = { getOldBottom : function() { return this.y_old + this.height; }, getOldCenterX: function() { return this.x_old + this.width * 0.5; }, getOldCenterY: function() { return this.y_old + this.height * 0.5; }, getOldLeft : function() { return this.x_old; }, getOldRight : function() { return this.x_old + this.width; }, getOldTop : function() { return this.y_old; }, setOldBottom : function(y) { this.y_old = y - this.height; }, setOldCenterX: function(x) { this.x_old = x - this.width * 0.5; }, setOldCenterY: function(y) { this.y_old = y - this.height * 0.5; }, setOldLeft : function(x) { this.x_old = x; }, setOldRight : function(x) { this.x_old = x - this.width; }, setOldTop : function(y) { this.y_old = y; } }; Object.assign(Game.MovingObject.prototype, Game.Object.prototype); Game.MovingObject.prototype.constructor = Game.MovingObject; /* The carrot class extends Game.Object and Game.Animation. */ Game.Carrot = function(x, y) { Game.Object.call(this, x, y, 7, 14); Game.Animator.call(this, Game.Carrot.prototype.frame_sets["twirl"], 15); this.frame_index = Math.floor(Math.random() * 2); /* base_x and base_y are the point around which the carrot revolves. position_x and y are used to track the vector facing away from the base point to give the carrot the floating effect. */ this.base_x = x; this.base_y = y; this.position_x = Math.random() * Math.PI * 2; this.position_y = this.position_x * 2; }; Game.Carrot.prototype = { frame_sets: { "twirl":[12, 13] }, updatePosition:function() { this.position_x += 0.1; this.position_y += 0.2; this.x = this.base_x + Math.cos(this.position_x) * 2; this.y = this.base_y + Math.sin(this.position_y); } }; Object.assign(Game.Carrot.prototype, Game.Animator.prototype); Object.assign(Game.Carrot.prototype, Game.Object.prototype); Game.Carrot.prototype.constructor = Game.Carrot; Game.Grass = function(x, y) { Game.Animator.call(this, Game.Grass.prototype.frame_sets["wave"], 25); this.x = x; this.y = y; }; Game.Grass.prototype = { frame_sets: { "wave":[14, 15, 16, 15] } }; Object.assign(Game.Grass.prototype, Game.Animator.prototype); Game.Door = function(door) { Game.Object.call(this, door.x, door.y, door.width, door.height); this.destination_x = door.destination_x; this.destination_y = door.destination_y; this.destination_zone = door.destination_zone; }; Game.Door.prototype = {}; Object.assign(Game.Door.prototype, Game.Object.prototype); Game.Door.prototype.constructor = Game.Door; Game.Player = function(x, y) { Game.MovingObject.call(this, x, y, 7, 12); Game.Animator.call(this, Game.Player.prototype.frame_sets["idle-left"], 10); this.jumping = true; this.direction_x = -1; this.velocity_x = 0; this.velocity_y = 0; }; Game.Player.prototype = { frame_sets: { "idle-left" : [0], "jump-left" : [1], "move-left" : [2, 3, 4, 5], "idle-right": [6], "jump-right": [7], "move-right": [8, 9, 10, 11] }, jump: function() { /* Made it so you can only jump if you aren't falling faster than 10px per frame. */ if (!this.jumping && this.velocity_y < 10) { this.jumping = true; this.velocity_y -= 13; } }, moveLeft: function() { this.direction_x = -1; this.velocity_x -= 0.55; }, moveRight:function(frame_set) { this.direction_x = 1; this.velocity_x += 0.55; }, updateAnimation:function() { if (this.velocity_y < 0) { if (this.direction_x < 0) this.changeFrameSet(this.frame_sets["jump-left"], "pause"); else this.changeFrameSet(this.frame_sets["jump-right"], "pause"); } else if (this.direction_x < 0) { if (this.velocity_x < -0.1) this.changeFrameSet(this.frame_sets["move-left"], "loop", 5); else this.changeFrameSet(this.frame_sets["idle-left"], "pause"); } else if (this.direction_x > 0) { if (this.velocity_x > 0.1) this.changeFrameSet(this.frame_sets["move-right"], "loop", 5); else this.changeFrameSet(this.frame_sets["idle-right"], "pause"); } this.animate(); }, updatePosition:function(gravity, friction) { this.x_old = this.x; this.y_old = this.y; this.velocity_y += gravity; this.velocity_x *= friction; /* Made it so that velocity cannot exceed velocity_max */ if (Math.abs(this.velocity_x) > this.velocity_max) this.velocity_x = this.velocity_max * Math.sign(this.velocity_x); if (Math.abs(this.velocity_y) > this.velocity_max) this.velocity_y = this.velocity_max * Math.sign(this.velocity_y); this.x += this.velocity_x; this.y += this.velocity_y; } }; Object.assign(Game.Player.prototype, Game.MovingObject.prototype); Object.assign(Game.Player.prototype, Game.Animator.prototype); Game.Player.prototype.constructor = Game.Player; Game.TileSet = function(columns, tile_size) { this.columns = columns; this.tile_size = tile_size; let f = Game.Frame; this.frames = [new f(115, 96, 13, 16, 0, -4), // idle-left new f( 50, 96, 13, 16, 0, -4), // jump-left new f(102, 96, 13, 16, 0, -4), new f(89, 96, 13, 16, 0, -4), new f(76, 96, 13, 16, 0, -4), new f(63, 96, 13, 16, 0, -4), // walk-left new f( 0, 112, 13, 16, 0, -4), // idle-right new f( 65, 112, 13, 16, 0, -4), // jump-right new f( 13, 112, 13, 16, 0, -4), new f(26, 112, 13, 16, 0, -4), new f(39, 112, 13, 16, 0, -4), new f(52, 112, 13, 16, 0, -4), // walk-right new f( 81, 112, 14, 16), new f(96, 112, 16, 16), // carrot new f(112, 115, 16, 4), new f(112, 124, 16, 4), new f(112, 119, 16, 4) // grass ]; }; Game.TileSet.prototype = { constructor: Game.TileSet }; Game.World = function(friction = 0.85, gravity = 2) { this.collider = new Game.Collider(); this.friction = friction; this.gravity = gravity; this.columns = 12; this.rows = 9; this.tile_set = new Game.TileSet(8, 16); this.player = new Game.Player(32, 76); this.zone_id = "00"; this.carrots = [];// the array of carrots in this zone; this.carrot_count = 0;// the number of carrots you have. this.doors = []; this.door = undefined; this.height = this.tile_set.tile_size * this.rows; this.width = this.tile_set.tile_size * this.columns; }; Game.World.prototype = { constructor: Game.World, collideObject:function(object) { /* I got rid of the world boundary collision. Now it's up to the tiles to keep the player from falling out of the world. */ var bottom, left, right, top, value; top = Math.floor(object.getTop() / this.tile_set.tile_size); left = Math.floor(object.getLeft() / this.tile_set.tile_size); value = this.collision_map[top * this.columns + left]; this.collider.collide(value, object, left * this.tile_set.tile_size, top * this.tile_set.tile_size, this.tile_set.tile_size); top = Math.floor(object.getTop() / this.tile_set.tile_size); right = Math.floor(object.getRight() / this.tile_set.tile_size); value = this.collision_map[top * this.columns + right]; this.collider.collide(value, object, right * this.tile_set.tile_size, top * this.tile_set.tile_size, this.tile_set.tile_size); bottom = Math.floor(object.getBottom() / this.tile_set.tile_size); left = Math.floor(object.getLeft() / this.tile_set.tile_size); value = this.collision_map[bottom * this.columns + left]; this.collider.collide(value, object, left * this.tile_set.tile_size, bottom * this.tile_set.tile_size, this.tile_set.tile_size); bottom = Math.floor(object.getBottom() / this.tile_set.tile_size); right = Math.floor(object.getRight() / this.tile_set.tile_size); value = this.collision_map[bottom * this.columns + right]; this.collider.collide(value, object, right * this.tile_set.tile_size, bottom * this.tile_set.tile_size, this.tile_set.tile_size); }, setup:function(zone) { this.carrots = new Array(); this.doors = new Array(); this.grass = new Array(); this.collision_map = zone.collision_map; this.graphical_map = zone.graphical_map; this.columns = zone.columns; this.rows = zone.rows; this.zone_id = zone.id; for (let index = zone.carrots.length - 1; index > -1; -- index) { let carrot = zone.carrots[index]; this.carrots[index] = new Game.Carrot(carrot[0] * this.tile_set.tile_size + 5, carrot[1] * this.tile_set.tile_size - 2); } for (let index = zone.doors.length - 1; index > -1; -- index) { let door = zone.doors[index]; this.doors[index] = new Game.Door(door); } for (let index = zone.grass.length - 1; index > -1; -- index) { let grass = zone.grass[index]; this.grass[index] = new Game.Grass(grass[0] * this.tile_set.tile_size, grass[1] * this.tile_set.tile_size + 12); } if (this.door) { if (this.door.destination_x != -1) { this.player.setCenterX (this.door.destination_x); this.player.setOldCenterX(this.door.destination_x);// It's important to reset the old position as well. } if (this.door.destination_y != -1) { this.player.setCenterY (this.door.destination_y); this.player.setOldCenterY(this.door.destination_y); } this.door = undefined;// Make sure to reset this.door so we don't trigger a zone load. } }, update:function() { this.player.updatePosition(this.gravity, this.friction); this.collideObject(this.player); for (let index = this.carrots.length - 1; index > -1; -- index) { let carrot = this.carrots[index]; carrot.updatePosition(); carrot.animate(); if (carrot.collideObject(this.player)) { this.carrots.splice(this.carrots.indexOf(carrot), 1); this.carrot_count ++; } } for(let index = this.doors.length - 1; index > -1; -- index) { let door = this.doors[index]; if (door.collideObjectCenter(this.player)) { this.door = door; }; } for (let index = this.grass.length - 1; index > -1; -- index) { let grass = this.grass[index]; grass.animate(); } this.player.updateAnimation(); } }; ================================================ FILE: content/rabbit-trap/07/main-07.js ================================================ // Frank Poth 04/18/2018 /* Changes: 1. Added the drawing calls for drawing the grass and carrots in render. 2. Added a p element for showing the number of carrots collected. */ window.addEventListener("load", function(event) { "use strict"; //// CONSTANTS //// const ZONE_PREFIX = "07/zone"; const ZONE_SUFFIX = ".json"; ///////////////// //// CLASSES //// ///////////////// const AssetsManager = function() { this.tile_set_image = undefined; }; AssetsManager.prototype = { constructor: Game.AssetsManager, requestJSON:function(url, callback) { let request = new XMLHttpRequest(); request.addEventListener("load", function(event) { callback(JSON.parse(this.responseText)); }, { once:true }); request.open("GET", url); request.send(); }, requestImage:function(url, callback) { let image = new Image(); image.addEventListener("load", function(event) { callback(image); }, { once:true }); image.src = url; }, }; /////////////////// //// FUNCTIONS //// /////////////////// var keyDownUp = function(event) { controller.keyDownUp(event.type, event.keyCode); }; var resize = function(event) { display.resize(document.documentElement.clientWidth, document.documentElement.clientHeight, game.world.height / game.world.width); display.render(); var rectangle = display.context.canvas.getBoundingClientRect(); p.style.left = rectangle.left + "px"; p.style.top = rectangle.top + "px"; p.style.fontSize = game.world.tile_set.tile_size * rectangle.height / game.world.height + "px"; }; var render = function() { var frame = undefined; display.drawMap (assets_manager.tile_set_image, game.world.tile_set.columns, game.world.graphical_map, game.world.columns, game.world.tile_set.tile_size); for (let index = game.world.carrots.length - 1; index > -1; -- index) { let carrot = game.world.carrots[index]; frame = game.world.tile_set.frames[carrot.frame_value]; display.drawObject(assets_manager.tile_set_image, frame.x, frame.y, carrot.x + Math.floor(carrot.width * 0.5 - frame.width * 0.5) + frame.offset_x, carrot.y + frame.offset_y, frame.width, frame.height); } frame = game.world.tile_set.frames[game.world.player.frame_value]; display.drawObject(assets_manager.tile_set_image, frame.x, frame.y, game.world.player.x + Math.floor(game.world.player.width * 0.5 - frame.width * 0.5) + frame.offset_x, game.world.player.y + frame.offset_y, frame.width, frame.height); for (let index = game.world.grass.length - 1; index > -1; -- index) { let grass = game.world.grass[index]; frame = game.world.tile_set.frames[grass.frame_value]; display.drawObject(assets_manager.tile_set_image, frame.x, frame.y, grass.x + frame.offset_x, grass.y + frame.offset_y, frame.width, frame.height); } p.innerHTML = "Carrots: " + game.world.carrot_count; display.render(); }; var update = function() { if (controller.left.active ) { game.world.player.moveLeft (); } if (controller.right.active) { game.world.player.moveRight(); } if (controller.up.active ) { game.world.player.jump(); controller.up.active = false; } game.update(); if (game.world.door) { engine.stop(); assets_manager.requestJSON(ZONE_PREFIX + game.world.door.destination_zone + ZONE_SUFFIX, (zone) => { game.world.setup(zone); engine.start(); }); return; } }; ///////////////// //// OBJECTS //// ///////////////// var assets_manager = new AssetsManager(); var controller = new Controller(); var display = new Display(document.querySelector("canvas")); var game = new Game(); var engine = new Engine(1000/30, render, update); var p = document.createElement("p"); p.setAttribute("style", "color:#c07000; font-size:2.0em; position:fixed;"); p.innerHTML = "Carrots: 0"; document.body.appendChild(p); //////////////////// //// INITIALIZE //// //////////////////// display.buffer.canvas.height = game.world.height; display.buffer.canvas.width = game.world.width; display.buffer.imageSmoothingEnabled = false; assets_manager.requestJSON(ZONE_PREFIX + game.world.zone_id + ZONE_SUFFIX, (zone) => { game.world.setup(zone); assets_manager.requestImage("rabbit-trap.png", (image) => { assets_manager.tile_set_image = image; resize(); engine.start(); }); }); window.addEventListener("keydown", keyDownUp); window.addEventListener("keyup" , keyDownUp); window.addEventListener("resize" , resize); }); ================================================ FILE: content/rabbit-trap/07/zone00.json ================================================ { "carrots":[[1, 2], [4, 2], [6, 2], [10, 2], [3, 4], [8, 4], [6, 5], [10, 5], [1, 6], [4, 6]], "grass" :[[2, 7], [3, 7], [5, 7], [7, 7], [9, 7], [10, 7]], "doors":[], "columns":12, "rows" :9, "collision_map":[0,4,4,4,4,0,4,4,0,4,4,0,2,0,0,0,0,10,0,0,14,0,0,8,2,0,0,0,0,10,0,0,0,0,0,8,0,3,0,0,13,4,7,0,0,0,13,0,0,6,0,0,0,0,0,0,0,0,0,8,2,0,0,1,0,0,0,0,11,0,0,8,2,0,0,0,0,0,11,0,10,0,13,0,0,3,0,0,11,0,10,0,10,0,0,8,0,0,1,1,0,1,0,1,0,1,1,0], "graphical_map":[27,36,35,5,36,25,17,35,44,17,35,5,30,39,39,39,39,11,31,39,19,31,39,7,38,31,39,31,31,11,39,39,39,39,39,7,30,3,31,39,4,23,6,31,31,39,4,14,22,21,39,31,31,39,31,39,31,39,31,20,38,39,39,47,39,31,39,39,3,39,31,12,10,31,31,31,31,39,3,39,11,31,4,46,40,2,39,39,3,39,11,39,11,31,39,8,24,49,2,3,37,27,23,13,11,12,13,8], "id":"00" } ================================================ FILE: content/rabbit-trap/rabbit-trap.css ================================================ /* Frank Poth 02/28/2018 */ * { box-sizing:border-box; margin:0; padding:0; } html { height:100%; width:100%; } body { align-content:center; align-items:space-around; background-color:#202830; display:grid; justify-items:center; height:100%; width:100%; } /* 04/07/2018 */ canvas { image-rendering:pixelated; } #menu { background-color:rgba(0, 0, 0, 0.5); color:#ffffff; cursor:pointer; display:grid; max-height:100%; overflow-y:auto; padding:8px; position:fixed; right:0px; top:0px; user-select:none; -webkit-tap-highlight-color:transparent; } #menu p { color:#ffffff; font-size:2.0em; } #menu-list { display:none; } #menu-list a { color:#ffffff; font-size:1.5em; text-decoration:none; } ================================================ FILE: content/rabbit-trap/rabbit-trap.html ================================================ Rabbit Trap ================================================ FILE: content/rectangle-collision/rectangle-collision.html ================================================ ================================================ FILE: content/shoot/shoot.html ================================================ Shoot ================================================ FILE: content/snake/snake.css ================================================ /* Frank Poth 11/16/2017 */ * { box-sizing:border-box; margin:0; padding:0; } html { height:100%; width:100%; } body { align-content:space-around; background-color:#202830; color:#ffffff; display:grid; grid-template-columns:auto; grid-template-rows:auto auto auto auto; height:100%; justify-content:space-around; overflow:hidden; text-align:center; width:100%; } canvas { align-self:center; background-color:#303840; display:grid; justify-self:center; } .hideable { display:grid; } ================================================ FILE: content/snake/snake.html ================================================ SNAKE

PoP Vlog - Snake

SCORE: 0000

The goal of classic snake is to collect the segments to increase the length of your body.
NOTICE: Requires Keyboard

================================================ FILE: content/snake/snake.js ================================================ // Frank Poth 11/20/2017 (function() { /* I split the logic into three separate parts for organizational purposes. */ var controller, display, game; /* The controller handles everything to do with getting user input. */ controller = { down:false, left:false, right:false, up:false, // A very simple key up/down event handler: keyUpDown:function(event) { var key_state = (event.type == "keydown")?true:false; switch(event.keyCode) { case 37: controller.left = key_state; break; // left key case 38: controller.up = key_state; break; // up key case 39: controller.right = key_state; break; // right key case 40: controller.down = key_state; break; // down key } } }; /* display handles everything to do with the display. */ display = { /* The buffer is where we draw everything to in world space. World space is the actual size of the game world in pixels before it is scaled up to match the size of the user's viewport. */ buffer:document.createElement("canvas").getContext("2d"), /* context is the drawing context of the display canvas. */ context:document.querySelector("canvas").getContext("2d"), output:document.querySelector("p"),// The output p element. /* This object holds all of my graphics for the game. Each graphic is just a different tile. Each graphic object has a canvas and a function to draw itself. The numeric labels correspond to the tile values they represent.*/ graphics: { 0: {// background_tile canvas:document.createElement("canvas"), draw:function() { var context = this.canvas.getContext("2d"); this.canvas.height = this.canvas.width = game.world.tile_size; context.fillStyle = "#202830"; context.fillRect(0, 0, this.canvas.width, this.canvas.height); context.fillStyle = "#303840"; context.fillRect(1, 1, this.canvas.width - 2, this.canvas.height - 2); } }, 1: {// segment canvas:document.createElement("canvas"), draw:function() { var context = this.canvas.getContext("2d"); this.canvas.height = this.canvas.width = game.world.tile_size; context.fillStyle = "#202830"; context.fillRect(0, 0, this.canvas.width, this.canvas.height); context.fillStyle = "#ff9900"; context.fillRect(1, 1, game.world.tile_size - 2, game.world.tile_size - 2); } }, 2: {// apple canvas:document.createElement("canvas"), draw:function() { var context = this.canvas.getContext("2d"); this.canvas.height = this.canvas.width = game.world.tile_size; context.fillStyle = "#202830"; context.fillRect(0, 0, this.canvas.width, this.canvas.height); context.fillStyle = "#99ff00"; context.fillRect(1, 1, game.world.tile_size - 2, game.world.tile_size - 2); } }, }, /* These are just here for ease of referencing the different graphics. I figured it would be easier to remember variable names than numeric references. */ background_tile:0, segment:1, apple:2, /* Renders everything to the buffer, and then renders the buffer to the display canvas. */ render:function() { /* Loop through the tile map and draw the corresponding graphics. */ for (let index = 0; index < game.world.map.length; index ++) { /* Get the appropriate graphics canvas from the graphics object. */ let graphic = this.graphics[game.world.map[index]].canvas; /* Draw the tile at the specified x and y coordinates in the buffer using the 1d to 2d conversion formulas. */ this.buffer.drawImage(graphic, 0, 0, graphic.width, graphic.height, (index % game.world.columns) * game.world.tile_size, Math.floor(index / game.world.columns) * game.world.tile_size, game.world.tile_size, game.world.tile_size); } /* Output the score at the top of the screen, remembering to add leading zeros. */ let leading_zeros = "SCORE: "; for (let index = 4 - game.score.toString().length; index > 0; -- index) { leading_zeros += "0"; } this.output.innerHTML = leading_zeros + game.score; /* Draw the finalized buffer to the display canvas. This takes care of scaling. */ this.context.drawImage(this.buffer.canvas, 0, 0, this.buffer.canvas.width, this.buffer.canvas.height, 0, 0, this.context.canvas.width, this.context.canvas.height); }, /* Handle resize events. */ resize:function(event) { var client_height = document.documentElement.clientHeight; display.context.canvas.width = document.documentElement.clientWidth - 32; if (display.context.canvas.width > client_height - 64 || display.context.canvas.height > client_height - 64) { display.context.canvas.width = client_height - 64; } display.context.canvas.height = display.context.canvas.width; display.render(); /* Hide some elements when the display canvas gets really big, like in full screen mode. */ let elements = document.querySelectorAll(".hideable"); for (let index = elements.length - 1; index > -1; -- index) { if (document.body.offsetHeight < document.body.scrollHeight) { elements[index].style.visibility = "hidden"; } else { elements[index].style.visibility = "visible"; } } } }; /* Everything to do with game logic goes here. */ game = { /* Start the score off at 0. */ score:0, /* The apple simply records a location on the map. */ apple: { index:Math.floor(Math.random() * 400) }, /* This is the snake object. It is fairly simple, tracking only map indices and the movement vector. */ snake: { head_index:209, old_head_index:undefined, segment_indices:[209, 210], vector_x:0, vector_y:0 }, /* The world object holds information about the level. */ world:{ columns:20, tile_size:10, map:new Array(400).fill(display.background_tile)// Creates a new array with 400 spaces filled with 0s. }, /* The amount of time accumulated since the start of the application. */ accumulated_time:0, time_step:250,/* The amount of time to wait between redraws. */ /* This resets the game. */ reset:function() { this.score = 0; /* Set all the tiles under the snake to zeros. */ for (let index = this.snake.segment_indices.length - 1; index > -1; -- index) { this.world.map[this.snake.segment_indices[index]] = display.background_tile; } this.snake.segment_indices = [209, 210]; this.snake.head_index = 209; this.snake.old_head_index = undefined; this.snake.vector_x = this.snake.vector_y = 0; this.world.map[game.apple.index] = display.apple; this.world.map[game.snake.segment_indices[0]] = display.segment; this.world.map[game.snake.segment_indices[1]] = display.segment; this.time_step = 250; this.loop();// Restart the game loop display.render(); }, /* This is the game loop. All the cool stuff happens here. */ loop:function(time_stamp) { /* Get controller input. Make sure that the vector on the opposite axis is set to 0 so the snake doesn't move on a diagonal. */ if (controller.down) { game.snake.vector_x = 0; game.snake.vector_y = 1; } else if (controller.left) { game.snake.vector_x = -1; game.snake.vector_y = 0; } else if (controller.right) { game.snake.vector_x = 1; game.snake.vector_y = 0; } else if (controller.up) { game.snake.vector_x = 0; game.snake.vector_y = -1; } /* Only redraw and update the game if enough time has passed. time_stamp holds the amount of time in milliseconds that has passed since the start of the application. */ if (time_stamp >= game.accumulated_time + game.time_step) { game.accumulated_time = time_stamp; /* If the snake isn't moving, there is nothing to update. */ if (game.snake.vector_x != 0 || game.snake.vector_y != 0) { /* This allows the user to pause the game. If you try to move in the exact opposite direction it sets the movement vector on each axis to 0. since this is an early out case and nothing else needs to be executed afterwards, we setup the next game loop and use return to exit the loop. */ if (game.snake.head_index + game.snake.vector_y * game.world.columns + game.snake.vector_x == game.snake.old_head_index) { game.snake.vector_x = game.snake.vector_y = 0; window.requestAnimationFrame(game.loop); return; } /* We move the snake by removing its tail and placing it one step ahead of its current head position in the direction of its movement vector. */ let tail_index = game.snake.segment_indices.pop();// remove the tail game.world.map[tail_index] = display.background_tile;// set the tail index in the map to 0 game.snake.old_head_index = game.snake.head_index;// keep track of the last head index game.snake.head_index += game.snake.vector_y * game.world.columns + game.snake.vector_x;// move the snake's head index /* Do collision detection, testing to see if the snake's head index is offscreen or colliding with another snake segment. */ if (game.world.map[game.snake.head_index] == display.segment// hit a segment || game.snake.head_index < 0// off the top of the map || game.snake.head_index > game.world.map.length - 1// off the bottom || (game.snake.vector_x == -1 && game.snake.head_index % game.world.columns == game.world.columns - 1)// off the left of the map || (game.snake.vector_x == 1 && (game.snake.head_index % game.world.columns == 0))) {// off the right game.reset(); return; } /* Set the tile under the snake's head to a segment value. */ game.world.map[game.snake.head_index] = display.segment; /* Put the snake's head index back into the front of its segment array. */ game.snake.segment_indices.unshift(game.snake.head_index); /* Is the snake's head on the apple? */ if (game.snake.head_index == game.apple.index) { game.score ++; game.time_step = (game.time_step > 100)?game.time_step - 10:100;// increase speed // add another segment to the tail position game.snake.segment_indices.push(tail_index); game.world.map[tail_index] = display.segment; game.apple.index = Math.floor(Math.random() * game.world.map.length);// reset the apple // If the snake fills up the entire map minus 1 space, reset the game. if (game.snake.segment_indices.length == game.world.map.length - 1) { game.reset(); return; } /* If the apple is on any tile but a background tile, we need to move it to a background tile. We have to search for that background tile. */ while(game.world.map[game.apple.index] != display.background_tile) { game.apple.index ++; // If checking past the bottom of the map, jump to the top if (game.apple.index > game.world.map.length - 1) { game.apple.index = 0; } } // Once the while loop ends, we know we have a safe spot for the apple. game.world.map[game.apple.index] = display.apple; } display.render(); } } // Ensure the loop runs again. window.requestAnimationFrame(game.loop); } }; // Initialize the game: display.buffer.canvas.height = display.buffer.canvas.width = game.world.columns * game.world.tile_size; // Draw all the graphics. for(let object in display.graphics) { display.graphics[object].draw(); }; window.addEventListener("resize", display.resize); window.addEventListener("keydown", controller.keyUpDown); window.addEventListener("keyup", controller.keyUpDown); game.reset(); display.resize(); })(); ================================================ FILE: content/square-collision-response/response.css ================================================ /* Frank Poth 08/30/2017 */ * { box-sizing: border-box; margin:0; padding:0; } html, body { height:100%; width:100%; } body { align-content:space-around; background-color:#202830; color:#ffffff; display:grid; justify-items:center; } h1 { word-wrap:break-word; } canvas { cursor:none; } ================================================ FILE: content/square-collision-response/response.html ================================================ Response

PoP Vlog - Square Collision Response

================================================ FILE: content/square-collision-response/response.js ================================================ // Frank Poth 08/30/2017 // drawing context, controller object, Rectangle class, // the red and white rectangle objects, game loop, resize event handler var context, controller, Rectangle, red, white, loop, resize; context = document.querySelector("canvas").getContext("2d"); // keeps track of where the pointer is controller = { // mouse or finger position pointer_x:0, pointer_y:0, move:function(event) { // This will give us the location of our canvas element on screen var rectangle = context.canvas.getBoundingClientRect(); // store the position of the move event inside the pointer variables controller.pointer_x = event.clientX - rectangle.left; controller.pointer_y = event.clientY - rectangle.top; } }; // A simple rectangle class Rectangle = function(x, y, width, height, color) { this.x = x; this.y = y; this.width = width; this.height = height; this.color = color; }; Rectangle.prototype = { draw:function() {// draws rectangle to canvas context.beginPath(); context.rect(this.x, this.y, this.width, this.height); context.fillStyle = this.color; context.fill(); }, // get the center coordinates of the rectangle get centerX() { return this.x + this.width * 0.5; }, get centerY() { return this.y + this.height * 0.5; }, // get the four side coordinates of the rectangle get bottom() { return this.y + this.height; }, get left() { return this.x; }, get right() { return this.x + this.width; }, get top() { return this.y; }, // determines if a collision is present between two rectangles testCollision:function(rectangle) { // using early outs cuts back on performance costs if (this.top > rectangle.bottom || this.right < rectangle.left || this.bottom < rectangle.top || this.left > rectangle.right) { return false; } return true; }, // push the calling rectangle out of the callee rectangle on the // axis that has the most overlap resolveCollision:function(rectangle) { var vector_x, vector_y; // get the distance between center points vector_x = this.centerX - rectangle.centerX; vector_y = this.centerY - rectangle.centerY; // is the y vector longer than the x vector? if (vector_y * vector_y > vector_x * vector_x) {// square to remove negatives // is the y vector pointing down? if (vector_y > 0) { this.y = rectangle.bottom; } else { // the y vector is pointing up this.y = rectangle.y - this.height; } } else { // the x vector is longer than the y vector // is the x vector pointing right? if (vector_x > 0) { this.x = rectangle.right; } else { // the x vector is pointing left this.x = rectangle.x - this.width; } } } }; // create the rectangles, positioning the white one in the center of the screen red = new Rectangle(0, 0, 64, 64, "#ff0000"); white = new Rectangle(context.canvas.width * 0.5 - 32, context.canvas.height * 0.5 - 32, 64, 64, "#ffffff"); // the game loop loop = function(time_stamp) { // make the red rectangle follow the pointer red.x = controller.pointer_x - 32; red.y = controller.pointer_y - 32; // fill background color context.fillStyle = "#303840"; context.fillRect(0, 0, context.canvas.width, context.canvas.height); white.draw(); red.draw(); // if there is a collision if (red.testCollision(white)) { // resolve the collision red.resolveCollision(white); // draw the white outlines around the two rectangles context.beginPath(); context.rect(red.x, red.y, red.width, red.height); context.rect(white.x, white.y, white.width, white.height); context.lineWidth = 1; context.strokeStyle = "#ffffff"; context.stroke(); // draw the collision regions of the white rectangle (white X) context.beginPath(); context.moveTo(white.centerX - white.width, white.centerY - white.height); context.lineTo(white.centerX + white.width, white.centerY + white.height); context.stroke(); context.beginPath(); context.moveTo(white.centerX + white.width, white.centerY - white.height); context.lineTo(white.centerX - white.width, white.centerY + white.height); context.stroke(); // draw the line between the center points of the rectangles context.beginPath(); context.moveTo(controller.pointer_x, controller.pointer_y); context.lineTo(white.centerX, white.centerY); context.lineWidth = 3; context.strokeStyle = "#303840"; context.stroke(); } // perpetuate loop window.requestAnimationFrame(loop); }; // just keeps the canvas element sized appropriately resize = function(event) { context.canvas.width = document.documentElement.clientWidth - 32; if (context.canvas.width > document.documentElement.clientHeight) { context.canvas.width = document.documentElement.clientHeight; } context.canvas.height = Math.floor(context.canvas.width * 0.5625); white.x = context.canvas.width * 0.5 - 32; white.y = context.canvas.height * 0.5 - 32; }; context.canvas.addEventListener("mousemove", controller.move); context.canvas.addEventListener("touchmove", controller.move, {passive:true}); window.addEventListener("resize", resize, {passive:true}); resize(); // start the game loop window.requestAnimationFrame(loop); ================================================ FILE: content/starfield/starfield.html ================================================ StarField ================================================ FILE: content/stay-down/game-states/pause.js ================================================ STAY_DOWN.initializers.pauseState = () => { // buffers const display_buffer = STAY_DOWN.buffers.display; const text_buffer = STAY_DOWN.buffers.text; // tools const controller = STAY_DOWN.tools.controller; const state_manager = STAY_DOWN.tools.state_manager; const text = STAY_DOWN.tools.text; // utilities const Buffer = STAY_DOWN.utilities.Buffer; function activate(message) { Buffer.resize(text_buffer, 256, 256); text.write(text_buffer, 8, 20, 256, 'paused'); text.write(text_buffer, 8, 40, 256, 'press p to play'); text.write(text_buffer, 8, 60, 256, 'press q to quit'); Buffer.draw(display_buffer, text_buffer.canvas, 0, 0); } function deactivate() {} function render() {} function update() { if (controller.getKey('p')) { controller.setKey('p', false); state_manager.change('run'); } else if (controller.getKey('q')) { controller.setKey('q', false); state_manager.change('title'); } } STAY_DOWN.states.pause = { activate, deactivate, render, update }; }; ================================================ FILE: content/stay-down/game-states/run.js ================================================ STAY_DOWN.initializers.runState = () => { // buffers const background_buffer = STAY_DOWN.buffers.background; const display_buffer = STAY_DOWN.buffers.display; const spikes_buffer = STAY_DOWN.buffers.spikes; const text_box_buffer = STAY_DOWN.buffers.text_box; const text_buffer = STAY_DOWN.buffers.text; // images const dominique_image = STAY_DOWN.images.dominique; const diamond_image = STAY_DOWN.images.diamond; const platform_image = STAY_DOWN.images.platform; // tools const controller = STAY_DOWN.tools.controller; const state_manager = STAY_DOWN.tools.state_manager; const text = STAY_DOWN.tools.text; // Utilities const Buffer = STAY_DOWN.utilities.Buffer; const Collider = STAY_DOWN.utilities.Collider; const Item = STAY_DOWN.utilities.Item; const Platform = STAY_DOWN.utilities.Platform; const Player = STAY_DOWN.utilities.Player; const Rectangle2D = STAY_DOWN.utilities.Rectangle2D; const friction = 0.8; const gravity = 1; const world_width = 256; const world_height = 256; const ground_top = world_height - 32; const item = Item.create(128, 100); const player = Player.create(2, ground_top - 32); const platforms = []; const hype_words = ['oooooh, yeah!!! i got $ diamonds!', 'that\'s right, $ diamonds!', 'that makes $ diamonds.', 'hey, now! i just got $ diamonds!!!', '$ diamonds!!!', 'how am i fitting these massive diamonds on my person?', '$ diamonds! i wonder if something will happen if i get enough of these...', 'got another one, that\'s $.']; var item_count = 0; for (let x = world_width - 19; x > 0; x -= 18) { platforms.push(Platform.create(x, ground_top, 16, 4)); } function activate(message) { Buffer.resize(text_buffer, 250, 24); if (message === 'reset') reset(); else text.write(text_buffer, 0, 0, 250, 'why do i suddenly feel like i was frozen in time?'); } function deactivate() {} function render() { Buffer.draw(display_buffer, background_buffer.canvas, 0, 0); for (var index = platforms.length - 1; index > -1; -- index) { var platform = platforms[index]; Buffer.draw(display_buffer, platform_image, platform.x, platform.y); } Buffer.draw(display_buffer, diamond_image, item.x, item.y); Buffer.drawFlippedX(display_buffer, dominique_image, player.x, player.y, player.direction); Buffer.draw(display_buffer, spikes_buffer.canvas, 0, 0); Buffer.draw(display_buffer, text_box_buffer.canvas, 0, 228); Buffer.draw(display_buffer, text_buffer.canvas, 3, 230); display_buffer.fillStyle = '#202830'; display_buffer.fillRect(0, ground_top, world_width, 4); } function reset() { Rectangle2D.setLeft(player, 2); Rectangle2D.setBottom(player, ground_top); for (var p = platforms.length - 1; p > -1; -- p) Platform.reset(platforms[p], ground_top); item_count = 0; text.write(text_buffer, 0, 0, 250, 'where am i? i need to get out of here! hey, is that a diamond?', true); } function update() { // Pause if (controller.getKey('p')) { controller.setKey('p', false); state_manager.change('pause'); return; } if (controller.getKey('left')) Player.moveLeft(player); if (controller.getKey('right')) Player.moveRight(player); if (controller.getKey('up')) Player.jump(player); if (Rectangle2D.getTop(player) < 4) { if (item_count === 0) text.write(text_buffer, 0, 0, 250, 'ouch! i need to stay away from those!', true); else text.write(text_buffer, 0, 0, 250, 'oh, snap! i lost my diamonds!!! also, that was incredibly painful!', true); player.y = player.old_y = ground_top - player.height; player.x = 2; item_count = 0; } Player.updatePosition(player, gravity, friction); Collider.keepPlayerInBounds(player, 0, 256); Item.updatePosition(item, Rectangle2D.getCenterX(player), Rectangle2D.getCenterY(player), friction); Collider.keepItemInBounds(item, 0, 256, 8, 232); if (Collider.collideTop(player, ground_top)) Player.ground(player); for (var index = platforms.length - 1; index > -1; -- index) { var platform = platforms[index]; Platform.moveUp(platform); if (platform.y < 0) Platform.reset(platform, ground_top); if (Collider.collidePlayerWithPlatform(player, platform)) Player.ground(player, platform.velocity_y); } if (Collider.collideRectangleWithRectangle(item, player)) { Item.reset(item, 0, 0, world_width, world_height); item_count ++; if (item_count === 1) text.write(text_buffer, 0, 0, 250, 'hey, it is a diamond! i should get more of these!', true); else { var message = hype_words[Math.floor(Math.random() * hype_words.length)]; message = message.replace('$', item_count); text.write(text_buffer, 0, 0, 250, message, true); } }; } STAY_DOWN.states.run = { activate, deactivate, render, update }; }; ================================================ FILE: content/stay-down/game-states/title.js ================================================ STAY_DOWN.initializers.titleState = () => { // buffers const background_buffer = STAY_DOWN.buffers.background; const display_buffer = STAY_DOWN.buffers.display; const text_buffer = STAY_DOWN.buffers.text; // images const dominique_image = STAY_DOWN.images.dominique; const diamond_image = STAY_DOWN.images.diamond; // tools const controller = STAY_DOWN.tools.controller; const state_manager = STAY_DOWN.tools.state_manager; const text = STAY_DOWN.tools.text; // utilities const Buffer = STAY_DOWN.utilities.Buffer; function activate(message) { Buffer.resize(text_buffer, 256, 256); text.write(text_buffer, 48, 100, 256, 'stay down by frank poth'); text.write(text_buffer, 72, 120, 256, 'press p to start'); text.write(text_buffer, 72, 140, 256, 'press p to pause'); Buffer.draw(display_buffer, background_buffer.canvas, 0, 0); Buffer.draw(display_buffer, dominique_image, 4, 224); Buffer.draw(display_buffer, diamond_image, 236, 236); Buffer.draw(display_buffer, text_buffer.canvas, 0, 0); } function deactivate() {} function render() {} function update() { if (controller.getKey('p')) { controller.setKey('p', false); state_manager.change('run', 'reset'); } } STAY_DOWN.states.title = { activate, deactivate, render, update }; }; ================================================ FILE: content/stay-down/initialize.js ================================================ (() => { const BUFFERS = STAY_DOWN.buffers; const IMAGES = STAY_DOWN.images; const INITIALIZERS = STAY_DOWN.initializers; const TOOLS = STAY_DOWN.tools; const Buffer = STAY_DOWN.utilities.Buffer; INITIALIZERS.colliderUtility(); INITIALIZERS.itemUtility(); INITIALIZERS.platformUtility(); INITIALIZERS.playerUtility(); INITIALIZERS.stateManagerTool(); // I have no need to ever reference this again, so I'm defining it here. function resize(event) { const display = BUFFERS.display; const width_ratio = document.documentElement.clientWidth / display.canvas.width; const height_ratio = document.documentElement.clientHeight / display.canvas.height; const scale = width_ratio < height_ratio ? width_ratio : height_ratio; display.canvas.style.height = Math.floor(display.canvas.width * scale) + 'px'; display.canvas.style.width = Math.floor(display.canvas.height * scale) + 'px'; } window.addEventListener('resize', resize); // load all the images TOOLS.loader.loadImages([ 'media/images/diamond.png', 'media/images/dominique.png', 'media/images/platform.png', 'media/images/spike.png', 'media/images/tile.png', 'media/images/text.png', 'media/images/text-box.png' ], // the callback function to handle the images once they're loaded function(images_) { IMAGES.diamond = images_[0]; IMAGES.dominique = images_[1]; IMAGES.platform = images_[2]; IMAGES.spike = images_[3]; IMAGES.tile = images_[4]; IMAGES.text = images_[5]; IMAGES.text_box = images_[6]; INITIALIZERS.textTool(); // Uses the text image, so must be initialized after loading the text image. BUFFERS.background = Buffer.create(256, 256, false, true); BUFFERS.display = Buffer.create(256, 256, false, false); BUFFERS.spikes = Buffer.create(256, 8, true, true); BUFFERS.text = Buffer.create(0, 0, true, true); BUFFERS.text_box = Buffer.create(256, 32, false, true); // These use the text tool as well as some buffers, so we have to initialize them after the text tool. INITIALIZERS.runState(); INITIALIZERS.pauseState(); INITIALIZERS.titleState(); document.body.appendChild(BUFFERS.display.canvas); // once we have images, we can set up buffer images var buffer = BUFFERS.background; var image = IMAGES.tile; var x, y; for (x = 240; x > -1; x -= 16) for (y = 240; y > -1; y -= 16) { var random_x = Math.floor(Math.random() * 3) * 16; buffer.drawImage(image, random_x, 0, 16, 16, x, y, 16, 16); } buffer = BUFFERS.spikes; image = IMAGES.spike; for (x = 240; x > -1; x -= 16) buffer.drawImage(image, x, 0); buffer = BUFFERS.text_box; image = IMAGES.text_box; buffer.drawImage(image, 32, 0, 16, 28, 240, 0, 16, 28); for (x = 224; x > 0; x -= 16) buffer.drawImage(image, 16, 0, 16, 28, x, 0, 16, 28); buffer.drawImage(image, 0, 0, 16, 28, 0, 0, 16, 28); resize(); TOOLS.controller.activate(); TOOLS.state_manager.change('title'); TOOLS.engine.start(); }); })(); ================================================ FILE: content/stay-down/stay-down.html ================================================ Stay Down ================================================ FILE: content/stay-down/stay-down.js ================================================ const STAY_DOWN = (() => ({ buffers:{}, images:{}, initializers:{}, states:{}, tools:{}, utilities:{} }))(); ================================================ FILE: content/stay-down/tools/controller.js ================================================ STAY_DOWN.tools.controller = (() => { const Input = () => ({ active:false, state:false }); const keys = { 'left':Input(), 'p':Input(), 'q':Input(), 'right':Input(), 'up':Input() } function activate() { window.addEventListener('keydown', keyDownUp); window.addEventListener('keyup', keyDownUp); } function getKey(name) { return keys[name].active }; function keyDownUp(event) { event.preventDefault(); var state = event.type == 'keydown'; switch(event.keyCode) { case 37: trigger(keys.left, state); break; case 38: trigger(keys.up, state); break; case 39: trigger(keys.right, state); break; case 80: trigger(keys.p, state); break; case 81: trigger(keys.q, state); } } function setKey(name, active) { keys[name].active = active; } function trigger(input, state) { if (state !== input.state) input.active = input.state = state; } return { activate, getKey, setKey }; })(); ================================================ FILE: content/stay-down/tools/engine.js ================================================ STAY_DOWN.tools.engine = (() => { var running = false; var raf_handle; var accumulated_time = 0; var current_time = 0; var time_step = 1000/60; var state; function cycle(time_stamp) { raf_handle = window.requestAnimationFrame(cycle); accumulated_time += time_stamp - current_time; current_time = time_stamp; var updated = false; if (accumulated_time > 60) accumulated_time = time_step; while(accumulated_time >= time_step) { state.update(); updated = true; accumulated_time -= time_step; } if (updated) state.render(); } function start() { running = true; raf_handle = window.requestAnimationFrame(cycle); } function stop() { running = false; window.cancelAnimationFrame(raf_handle); } function setState(state_) { state = state_; } return { start, stop, setState }; })(); ================================================ FILE: content/stay-down/tools/loader.js ================================================ STAY_DOWN.tools.loader = (() => { function loadImages(urls, callback) { var images = []; var counter = urls.length; function resolve(event) { var image = event.target; image.removeEventListener('load', resolve); image.removeEventListener('error', resolve); counter --; if (counter === 0) callback(images); } for (var index = urls.length - 1; index > -1; index --) { var image = images[index] = new Image(); image.addEventListener('load', resolve); image.addEventListener('error', resolve); image.src = urls[index]; } } return { loadImages }; })(); ================================================ FILE: content/stay-down/tools/state-manager.js ================================================ STAY_DOWN.initializers.stateManagerTool = () => { const ENGINE = STAY_DOWN.tools.engine; const STATES = STAY_DOWN.states; var state; function change(name, message) { if (state) state.deactivate(); state = STATES[name]; state.activate(message); ENGINE.setState(state); }; STAY_DOWN.tools.state_manager = { change }; }; ================================================ FILE: content/stay-down/tools/text.js ================================================ STAY_DOWN.initializers.textTool = () => { const image = STAY_DOWN.images.text; const Frame = STAY_DOWN.utilities.Frame; const letter_spacing = 1; const line_height = 10; const space_width = 3; const frames = { 'a':Frame.create(0,3,6,6,0,3), 'b':Frame.create(7,0,5,9), 'c':Frame.create(13,4,5,5,0,4), 'd':Frame.create(19,0,5,9), 'e':Frame.create(25,4,5,5,0,4), 'f':Frame.create(31,0,5,9), 'g':Frame.create(37,1,5,8,0,4), 'h':Frame.create(43,0,5,9), 'i':Frame.create(49,2,1,7,0,2), 'j':Frame.create(51,0,4,9,-2,2), 'k':Frame.create(56,0,5,9), 'l':Frame.create(62,0,1,9), 'm':Frame.create(64,4,9,5,0,4), 'n':Frame.create(74,4,5,5,0,4), 'o':Frame.create(80,4,5,5,0,4), 'p':Frame.create(86,0,5,9,0,4), 'q':Frame.create(92,0,5,9,0,4), 'r':Frame.create(98,4,4,5,0,4), 's':Frame.create(103,4,5,5,0,4), 't':Frame.create(109,0,5,9), 'u':Frame.create(115,4,5,5,0,4), 'v':Frame.create(121,4,5,5,0,4), 'w':Frame.create(127,4,9,5,0,4), 'x':Frame.create(137,4,5,5,0,4), 'y':Frame.create(143,1,5,8,0,4), 'z':Frame.create(149,4,4,5,0,4), '0':Frame.create(154,0,5,9), '1':Frame.create(160,0,3,9), '2':Frame.create(164,0,5,9), '3':Frame.create(170,0,5,9), '4':Frame.create(176,0,4,9), '5':Frame.create(181,0,5,9), '6':Frame.create(187,0,5,9), '7':Frame.create(194,0,5,9), '8':Frame.create(199,0,5,9), '9':Frame.create(205,0,5,9), '!':Frame.create(211,0,1,9), '?':Frame.create(213,1,5,8,0,1), '.':Frame.create(1,3,1,1,0,8), "'":Frame.create(4,4,1,2,0,2), ',':Frame.create(4,4,1,2,0,8) }; function write(context, position_x, position_y, width, string, clear = false) { var start_x = position_x; if (clear) context.clearRect(0, 0, context.canvas.width, context.canvas.height); var length = string.length; for (var i = 0; i < length; i ++) { var char = string.charAt(i); if (char === ' ') { position_x += space_width; continue; } var frame = frames[char]; if (!frame) continue; context.drawImage(image, frame.x, frame.y, frame.width, frame.height, position_x + frame.offset_x, position_y + frame.offset_y, frame.width, frame.height); position_x += frame.width + letter_spacing; if (position_x > width) { position_x = start_x; position_y += line_height; } } } STAY_DOWN.tools.text = { write }; }; ================================================ FILE: content/stay-down/utilities/buffer.js ================================================ STAY_DOWN.utilities.Buffer = { create(width, height, alpha = false, desynchronized = true) { const buffer = document.createElement('canvas').getContext('2d', { alpha, desynchronized }); buffer.canvas.height = height; buffer.canvas.width = width; buffer.imageSmoothingEnabled = false; return buffer; }, draw(b, image, x, y) { b.drawImage(image, Math.floor(x), Math.floor(y)); }, drawFlippedX(b, image, x, y, scale_x) { if (scale_x === -1) { b.scale(-1, 1); b.drawImage(image, Math.floor(-Math.floor(x) - image.width), Math.floor(y)); b.scale(-1, 1); } else b.drawImage(image, Math.floor(x), Math.floor(y)); }, resize(b, width, height, smoothing = false) { b.canvas.width = width; b.canvas.height = height; b.imageSmoothingEnabled = smoothing; } }; ================================================ FILE: content/stay-down/utilities/collider.js ================================================ STAY_DOWN.initializers.colliderUtility = () => { const Rectangle2D = STAY_DOWN.utilities.Rectangle2D; STAY_DOWN.utilities.Collider = { collideRectangleWithRectangle(r1, r2) { if (Rectangle2D.getLeft(r1) > Rectangle2D.getRight(r2) || Rectangle2D.getTop(r1) > Rectangle2D.getBottom(r2) || Rectangle2D.getRight(r1) < Rectangle2D.getLeft(r2) || Rectangle2D.getBottom(r1) < Rectangle2D.getTop(r2)) return false; return true; }, collideTop(rectangle, top) { if (Rectangle2D.getBottom(rectangle) > top) { Rectangle2D.setBottom(rectangle, top); return true; } return false; }, collidePlayerWithPlatform(player, platform) { if (Rectangle2D.getRight(player) < Rectangle2D.getLeft(platform) || Rectangle2D.getLeft(player) > Rectangle2D.getRight(platform) || Rectangle2D.getBottom(player) < Rectangle2D.getTop(platform) || Rectangle2D.getOldBottom(player) > Rectangle2D.getOldTop(platform)) return false; Rectangle2D.setBottom(player, Rectangle2D.getTop(platform)); return true; }, keepItemInBounds(item, left, right, top, bottom) { if (Rectangle2D.getLeft(item) < left) { item.velocity_x += 1; item.rotation += Math.PI; } else if (Rectangle2D.getRight(item) > right) { item.rotation += Math.PI; item.velocity_x -= 1; } if (Rectangle2D.getTop(item) < top) { item.rotation += Math.PI; item.velocity_y += 1; } else if (Rectangle2D.getBottom(item) > bottom) { item.rotation += Math.PI; item.velocity_y -= 1; } }, keepPlayerInBounds(player, left, right) { if (Rectangle2D.getLeft(player) < left) player.velocity_x += 1; else if (Rectangle2D.getRight(player) > right) player.velocity_x -= 1; } }; }; ================================================ FILE: content/stay-down/utilities/frame.js ================================================ STAY_DOWN.utilities.Frame = { create(x, y, width, height, offset_x = 0, offset_y = 0) { return { x, y, width, height, offset_x, offset_y }; } }; ================================================ FILE: content/stay-down/utilities/item.js ================================================ STAY_DOWN.initializers.itemUtility = () => { const Rectangle2D = STAY_DOWN.utilities.Rectangle2D; STAY_DOWN.utilities.Item = { create(x, y) { return { r:0, velocity_r:0, velocity_x:0, velocity_y:0, ...Rectangle2D.create(x, y, 16, 16) }; }, reset(i, left, top, right, bottom) { i.velocity_x = 0; i.velocity_y = 0; i.x = Math.random() * right; i.y = Math.random() * bottom; }, updatePosition(i, x, y, friction) { var cx = Rectangle2D.getCenterX(i); var cy = Rectangle2D.getCenterY(i); var v1_x = cx - x; var v1_y = cy - y; var v2_x = 1; var v2_y = 0; var distance = v1_x * v1_x + v1_y * v1_y; var cross_product = v1_x * v2_y - v1_y * v2_x; var dot_product = v1_x * v2_x + v1_y * v2_y; var speed = 1000/distance; if (distance < 5000) { i.r = Math.atan2(Math.abs(cross_product), dot_product); if (cross_product > 0) i.r = Math.PI * 2 - i.r; } i.velocity_r += Math.random() * 1 - 0.5; i.r += i.velocity_r; i.velocity_x += Math.cos(i.r) * speed; i.velocity_y += Math.sin(i.r) * speed; i.velocity_r *= friction; i.velocity_x *= friction; i.velocity_y *= friction; i.x += i.velocity_x; i.y += i.velocity_y; } }; }; ================================================ FILE: content/stay-down/utilities/platform.js ================================================ STAY_DOWN.initializers.platformUtility = () => { const Rectangle2D = STAY_DOWN.utilities.Rectangle2D; STAY_DOWN.utilities.Platform = { create(x, y) { return { move_force:Math.random() * 0.05 + 0.01, velocity_y:0, velocity_y_max:-Math.random() * 2 - 1, ...Rectangle2D.create(x, y, 16, 16) }; }, moveUp(p) { p.velocity_y -= p.move_force; if (p.velocity_y < p.velocity_y_max) p.velocity_y = p.velocity_y_max; Rectangle2D.moveY(p, p.velocity_y); }, reset(p, y) { p.velocity_y = 0; p.velocity_y_max = -Math.random() * 2 - 1; Rectangle2D.setTop(p, y); } }; }; ================================================ FILE: content/stay-down/utilities/player.js ================================================ STAY_DOWN.initializers.playerUtility = () => { const Rectangle2D = STAY_DOWN.utilities.Rectangle2D; STAY_DOWN.utilities.Player = { create(x, y) { return { color:'#ff0000', direction:1, grounded:false, move_force:1, jump_force:18, velocity_x:0, velocity_y:0, ...Rectangle2D.create(x, y, 16, 32) } }, ground(p, velocity_y = 0) { p.grounded = true; p.velocity_y = velocity_y; }, jump(p) { if (p.grounded) { p.grounded = false; p.velocity_y -= p.jump_force; } }, moveLeft(p) { p.direction = -1; p.velocity_x -= p.move_force; }, moveRight(p) { p.direction = 1; p.velocity_x += p.move_force; }, updatePosition(p, gravity, friction) { p.velocity_x *= friction; p.velocity_y += gravity; p.velocity_y *= friction; p.x += p.velocity_x; Rectangle2D.moveY(p, p.velocity_y); } } }; ================================================ FILE: content/stay-down/utilities/rectangle-2d.js ================================================ STAY_DOWN.utilities.Rectangle2D = { create(x, y, width, height) { return { height:height, old_y:y, width:width, x:x, y:y }; }, getBottom(r) { return r.y + r.height; }, getCenterX(r) { return r.x + r.width * 0.5; }, getCenterY(r) { return r.y + r.height * 0.5; }, getLeft(r) { return r.x; }, getRight(r) { return r.x + r.width; }, getTop(r) { return r.y; }, getOldBottom(r) { return r.old_y + r.height; }, getOldTop(r) { return r.old_y; }, setBottom(r, y) { r.y = y - r.height; }, setLeft(r, x) { r.x = x; }, setRight(r, x) { r.x = x - r.width; }, setTop(r, y) { r.y = y; }, moveX(r, x) { r.x += x; }, moveY(r, y) { r.old_y = r.y; r.y += y; } }; ================================================ FILE: content/tile-animation/tile-animation.html ================================================ Tile Animation ================================================ FILE: content/tile-graphics/tile-graphics.css ================================================ /* Frank Poth 02/09/2018 */ * { margin:0; padding:0; box-sizing:border-box; } html { height:100%; width:100%; } body { align-content:space-around; align-items:center; background-color:#202830; color:#ffffff; display:grid; grid-template-columns:auto; grid-template-rows:auto auto; justify-items:center; min-height:100%; width:100%; } h1 { font-size:3.0em; text-align:center; } ================================================ FILE: content/tile-graphics/tile-graphics.html ================================================ PoP Vlog - Tile Graphics

PoP Vlog
Tile Graphics

This example simply blits tile graphics to the screen from a 1 dimensional tile map array.
You can view the page source with your browser's developer tools.

================================================ FILE: content/tile-graphics/tile-graphics.js ================================================ // Frank Poth 02/09/2018 /* This example simply blits tile graphics to a buffer canvas according to a 1d tile map. The buffer canvas is then blitted to the display canvas. */ (function() { "use strict"; /* The display handles everything to do with drawing graphics and resizing the screen. The world holds the map and its dimensions. */ var display, world; display = { /* We draw the tiles to the buffer in "world" coordinates or unscaled coordinates. All scaling is handled by drawImage when we draw the buffer to the display canvas. */ buffer:document.createElement("canvas").getContext("2d"), /* Scaling takes place on the display canvas. This is its drawing context. The height_width_ratio is used in scaling the buffer to the canvas. */ context:document.querySelector("canvas").getContext("2d"), /* The height width ratio is the height to width ratio of the tile map. It is used to size the display canvas to match the aspect ratio of the game world. */ height_width_ratio:undefined, /* The tile_sheet object holds the tile sheet graphic as well as its dimensions. */ tile_sheet: { image:new Image(),// The actual graphic will be loaded into this. columns:3, tile_height:16, tile_width:16 }, /* This function draws the tile graphics from the tile_sheet.image to the buffer one by one according to the world.map. It then draws the buffer to the display canvas and takes care of scaling the buffer image up to the display canvas size. */ render:function() { /* Here we loop through the tile map. */ for (let index = world.map.length - 1; index > -1; -- index) { /* We get the value of each tile in the map which corresponds to the tile graphic index in the tile_sheet.image. */ var value = world.map[index]; /* This is the x and y location at which to cut the tile image out of the tile_sheet.image. */ var source_x = (value % this.tile_sheet.columns) * this.tile_sheet.tile_width; var source_y = Math.floor(value / this.tile_sheet.columns) * this.tile_sheet.tile_height; /* This is the x and y location at which to draw the tile image we are cutting from the tile_sheet.image to the buffer canvas. */ var destination_x = (index % world.columns) * this.tile_sheet.tile_width; var destination_y = Math.floor(index / world.columns) * this.tile_sheet.tile_height; /* Draw the tile image to the buffer. The width and height of the tile is taken from the tile_sheet object. */ this.buffer.drawImage(this.tile_sheet.image, source_x, source_y, this.tile_sheet.tile_width, this.tile_sheet.tile_height, destination_x, destination_y, this.tile_sheet.tile_width, this.tile_sheet.tile_height); } /* Now we draw the finalized buffer to the display canvas. You don't need to use a buffer; you could draw your tiles directly to the display canvas. If you are going to scale your display canvas at all, however, I recommend this method, because it eliminates antialiasing problems that arize due to scaling individual tiles. It is somewhat slower, however. */ this.context.drawImage(this.buffer.canvas, 0, 0, world.width, world.height, 0, 0, this.context.canvas.width, this.context.canvas.height); }, /* Resizes the display canvas when the screen is resized. */ resize:function(event) { display.context.canvas.width = document.documentElement.clientWidth - 16; if (display.context.canvas.width > document.documentElement.clientHeight - 16) { display.context.canvas.width = document.documentElement.clientHeight - 16; } /* That height_width_ratio comes into play here. */ display.context.canvas.height = display.context.canvas.width * display.height_width_ratio; display.buffer.imageSmoothingEnabled = display.context.imageSmoothingEnabled = false; display.render(); } }; /* The world holds information about the tile map. */ world = { map: [7, 7, 8, 8, 8, 8, 8, 8, 0, 7, 7, 7, 7, 8, 8, 8, 1, 7, 7, 7, 7, 7, 7, 7, 2, 3, 7, 7, 6, 7, 6, 7, 4, 2, 5, 5, 5, 5, 5, 5], columns:8, height:80, width:124 }; //// INITIALIZE //// /* Before we can draw anything we have to load the tile_sheet image. */ display.tile_sheet.image.addEventListener("load", function(event) { display.buffer.canvas.height = world.height; display.buffer.canvas.width = world.width; display.height_width_ratio = world.height / world.width; display.resize(); }); /* Start loading the image. */ display.tile_sheet.image.src = "tile-graphics.png"; window.addEventListener("resize", display.resize); })(); ================================================ FILE: content/tile-grid/tile-grid.css ================================================ /* Frank Poth 09/29/2017 */ * { box-sizing:border-box; margin:0; padding:0; } html { height:100%; width:100%; } body { align-content:space-around; background-color:#202830; color:#ffffff; display:grid; justify-content:center; min-height:100%; text-align:center; width:100%; } canvas { background-color:#ffffff; } ================================================ FILE: content/tile-grid/tile-grid.html ================================================ Tile Grid

PoP Vlog - Tile Grid

output

================================================ FILE: content/tile-grid/tile-grid.js ================================================ // Frank Poth 09/29/2017 (function() { var buffer, context, controller, drawMap, loop, map, output, size; buffer = document.createElement("canvas").getContext("2d"); context = document.querySelector("canvas").getContext("2d"); output = document.querySelector("p"); size = 32; buffer.canvas.width = 16 * size; buffer.canvas.height = 9 * size; map = [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 1,0,1,1,1,0,0,1,0,0,1,0,0,1,0,1, 1,0,0,1,0,1,0,1,0,1,0,1,0,1,0,1, 1,0,0,1,0,0,0,1,0,1,1,1,0,1,0,1, 1,0,0,1,0,1,0,1,0,1,0,0,0,0,0,1, 1,0,0,1,0,1,0,1,0,0,1,1,0,1,0,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]; controller = { // mouse or finger position pointer_x:0, pointer_y:0, move:function(event) { // This will give us the location of our canvas element on screen var rectangle = context.canvas.getBoundingClientRect(); // store the position of the move event inside the pointer variables controller.pointer_x = event.clientX - rectangle.left; controller.pointer_y = event.clientY - rectangle.top; } }; drawMap = function() { for (let index = 0; index < map.length; index ++) { buffer.fillStyle = (map[index] == 1)?"#228b22":"#b0e0b6"; buffer.fillRect((index % 16) * size, Math.floor(index/16) * size, size, size); } }; loop = function(time_stamp) { var tile_x, tile_y, value; tile_x = Math.floor(controller.pointer_x / (context.canvas.width/16)); tile_y = Math.floor(controller.pointer_y / (context.canvas.height/9)); value = map[tile_y * 16 + tile_x]; drawMap(); buffer.fillStyle = "rgba(128, 128, 128, 0.5)"; buffer.fillRect(tile_x * size, tile_y * size, size, size); context.drawImage(buffer.canvas, 0, 0, buffer.canvas.width, buffer.canvas.height, 0, 0, context.canvas.width, context.canvas.height); output.innerHTML = "tile_x: " + tile_x + "
tile_y: " + tile_y + "
value: " + value; window.requestAnimationFrame(loop); }; // just keeps the canvas element sized appropriately resize = function(event) { context.canvas.width = Math.floor(document.documentElement.clientWidth - 32); if (context.canvas.width > document.documentElement.clientHeight) { context.canvas.width = Math.floor(document.documentElement.clientHeight); } context.canvas.height = Math.floor(context.canvas.width * 0.5625); drawMap(); }; window.addEventListener("resize", resize, {passive:true}); context.canvas.addEventListener("mousemove", controller.move); context.canvas.addEventListener("touchmove", controller.move, {passive:true}); context.canvas.addEventListener("touchstart", controller.move, {passive:true}); resize(); window.requestAnimationFrame(loop); })(); ================================================ FILE: content/tile-scroll/tile-scroll.html ================================================ Tile Scroll ================================================ FILE: content/tile-types/tile-types.css ================================================ /* Frank Poth 12/05/2017 */ * { box-sizing:border-box; margin:0; padding:0; } html { height:100%; width:100%; } body { align-content:space-around; background-color:#202830; color:#ffffff; display:grid; grid-template-columns:auto; grid-template-rows:auto auto auto auto; height:100%; justify-content:space-around; overflow:hidden; text-align:center; width:100%; } canvas { align-self:center; background-color:#303840; display:grid; justify-self:center; } p { font-family:monospace; font-weight:800; font-size:1.2em; } ================================================ FILE: content/tile-types/tile-types.html ================================================ PoP Vlog - Tile Types

PoP Vlog - Tile Types

The purpose of this example is to showcase some different types of tile based collision shapes. Use the keyboard to move the yellow rectangle around the level and interact with each shape.

================================================ FILE: content/tile-types/tile-types.js ================================================ // Frank Poth 12/05/2017 (function() { "use strict"; const TILE_SIZE = 16; ///////////////// //// OBJECTS //// ///////////////// var controller, display, game; controller = { down:false, left:false, right:false, up:false, // A very simple key up/down event handler: keyUpDown:function(event) { var key_state = (event.type == "keydown")?true:false; switch(event.keyCode) { case 37: controller.left = key_state; break;// left key case 38: controller.up = key_state; break;// up key case 39: controller.right = key_state; break;// right key } } }; display = { buffer:document.createElement("canvas").getContext("2d"), context:document.querySelector("canvas").getContext("2d"), output:document.querySelector("p"),// The output p element. tile_sheet: document.querySelector("img"),// The tile sheet graphic. render:function(game) { /* Loop through the map and draw all the tiles. */ for (let index = game.area.map.length - 1; index > -1; -- index) { this.buffer.drawImage(this.tile_sheet, game.area.map[index] * TILE_SIZE, 0, TILE_SIZE, TILE_SIZE, (index % game.area.columns) * TILE_SIZE, Math.floor(index / game.area.columns) * TILE_SIZE, TILE_SIZE, TILE_SIZE); } /* Draw the player character. */ this.buffer.drawImage(this.tile_sheet, 240, 0, TILE_SIZE, TILE_SIZE, game.player.x, game.player.y, game.player.width, game.player.height); /* Draw the buffer to the canvas. This takes care of scaling. */ this.context.drawImage(this.buffer.canvas, 0, 0, this.buffer.canvas.width, this.buffer.canvas.height, 0, 0, this.context.canvas.width, this.context.canvas.height); }, /* Resizes the display canvas when the window is resized. */ resize:function(event) { let height = document.documentElement.clientHeight; display.context.canvas.width = document.documentElement.clientWidth - 16; if (display.context.canvas.width >= height * 0.5) { display.context.canvas.width = height * 0.5; } display.context.canvas.height = display.context.canvas.width; display.render(game); } }; game = { /* The area object holds information about the map. */ area: { columns:8, map:[ 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 0, 4, 0, 7, 0, 0, 6, 2, 0, 4, 0, 1, 1, 1, 1, 1, 1, 1, 7, 0, 0, 0, 0, 0,13, 0, 0,11,12, 9,10, 0, 0, 7, 0, 1, 1, 1, 1, 1, 1, 1, 12, 0,14,14,14, 0, 8, 0] }, player: { jumping: true, height: TILE_SIZE - 4, width: TILE_SIZE - 4, x: TILE_SIZE * 4 - TILE_SIZE * 0.5 + 2, x_old: TILE_SIZE * 4 - TILE_SIZE * 0.5 + 2, x_velocity:0, y: TILE_SIZE * 4, y_old: TILE_SIZE * 8, y_velocity:0, }, collider: { offset:0.001, /* A platform tile with a flat top surface. */ 1:function(object, column, row) { this.collideTop(object, row); }, /* A platform tile with a flat right surface. */ 2:function(object, column, row) { this.collideRight(object, column); }, /* A platform tile with a flat bottom surface. */ 3:function(object, column, row) { this.collideBottom(object, row); }, /* A platform tile with a flat left surface. */ 4:function(object, column, row) { this.collideLeft(object, column); }, /* A platform tile with flat top, right, bottom, and left surfaces. */ 5:function(object, column, row) { if (this.collideTop(object, row)) return; if (this.collideLeft(object, column)) return; if (this.collideRight(object, column)) return; this.collideBottom(object, row); }, /* A half height platform tile residing in the bottom half of the tile space with flat top, right, bottom, and left surfaces. */ 6:function(object, column, row) { if (this.collideTop(object, row, TILE_SIZE * 0.5)) return; if (object.y + object.height > row * TILE_SIZE + TILE_SIZE * 0.5) { if (this.collideLeft(object, column)) return; if (this.collideRight(object, column)) return; } this.collideBottom(object, row); }, /* A half height platform tile residing in the bottom half of the tile space with a flat top surface. */ 7:function(object, column, row) { this.collideTop(object, row, TILE_SIZE * 0.5); }, /* A slope tile starting in the bottom left corner and rising to the top right corner of the tile space that only pushes objects up out of it. This is not a platform tile, it is a solid tile. */ 8:function(object, column, row) { let current_x = object.x + object.width - column * TILE_SIZE; let top = -1 * current_x + TILE_SIZE + row * TILE_SIZE; if (current_x > TILE_SIZE) { object.jumping = false; object.y_velocity = 0; object.y = row * TILE_SIZE - object.height - this.offset; } else if (object.y + object.height > top) { object.jumping = false; object.y_velocity = 0; object.y = top - object.height - this.offset; } }, /* A slope tile starting in the bottom left corner and rising to the top right corner of the tile space. */ 9:function(object, column, row) { this.collideSlopeTop(object, column, row, -1, TILE_SIZE); }, /* A slope tile starting in the top left corner and declining to the bottom right corner of the tile space. */ 10:function(object, column, row) { this.collideSlopeTop(object, column, row, 1, 0); }, /* A half height slope tile starting in the bottom left corner and rising to the middle right side of the tile space. */ 11:function(object, column, row) { this.collideSlopeTop(object, column, row, -0.5, TILE_SIZE); }, /* A half height slope tile starting in the middle left side and declining to the bottom right corner of the tile space. */ 12:function(object, column, row) { this.collideSlopeTop(object, column, row, 0.5, TILE_SIZE * 0.5); }, /* A slope tile starting in the top left corner and declining to the bottom right corner of the tile space, with flat surfaces on the bottom and left sides. */ 13:function(object, column, row) { if (this.collideSlopeTop(object, column, row, 1, 0)) return; if (this.collideLeft(object, column)) return; let bottom = row * TILE_SIZE + TILE_SIZE; let right = column * TILE_SIZE + TILE_SIZE; if (object.x < right && object.x_old >= right && object.y < bottom && object.y + object.height > bottom) { object.x_velocity = 0; object.x = right; } this.collideBottom(object, row); }, /* A half height curve tile that simply pushes the object up when collision is detected. */ 14:function(object, column, row) { /* x is the x center of the object adjusted so that x = 0 when the object is in the center of the tile space. */ let x = (object.x + object.width * 0.5) - (column * TILE_SIZE + TILE_SIZE * 0.5); /* y_vertex is the y value at the peak of the curve or the turn around point, or whatever you'd like to call it. In this case it's just at the top of our curve tile. */ let y_vertex = row * TILE_SIZE + TILE_SIZE * 0.5; /* The coefficient determines how wide the curve will be at its base or to be exact, how wide it will be at the y intercept. This particular formula will yield a width of the full tile size at the base of the tile should the vertex be in the center of the tile space. Changing the 2 can yield different heights. For instance, 4 would give the width of the base if the vertex were at the top of the tile space and the base were at the bottom. I haven't tested this with a TILE_SIZE other than 16, so this formula may be garbage. */ let coefficient = TILE_SIZE / ((TILE_SIZE * TILE_SIZE) / 2); /* This is the basic formula for a quadratic curve: y = a(x - h)^2 + k Where h is the x offset, and k is the y_vertex */ let top = coefficient * x * x + y_vertex; if (object.y + object.height > top) { object.jumping = false; object.y_velocity = 0; object.y = top - object.height - this.offset; } }, collideBottom:function(object, row, y_offset = TILE_SIZE) { let bottom = row * TILE_SIZE + y_offset; if (object.y < bottom && object.y_old >= bottom) { object.y_velocity = 0; object.y = bottom + this.offset; return true; } return false; }, collideLeft:function(object, column) { let left = column * TILE_SIZE; if (object.x + object.width > left && object.x_old + object.width <= left) { object.x_velocity = 0; object.x = left - object.width - this.offset; return true; } return false; }, collideRight:function(object, column) { let right = column * TILE_SIZE + TILE_SIZE; if (object.x < right && object.x_old >= right) { object.x_velocity = 0; object.x = right; return true; } return false; }, collideTop:function(object, row, y_offset = 0) { let top = row * TILE_SIZE + y_offset; if (object.y + object.height > top && object.y_old + object.height <= top) { object.jumping = false; object.y_velocity = 0; object.y = top - object.height - this.offset; return true; } return false; }, /* This function handles collision with slope tiles on the y axis. */ collideSlopeTop:function(object, column, row, slope, y_offset) { let origin_x = column * TILE_SIZE; let origin_y = row * TILE_SIZE + y_offset; let current_x = (slope < 0) ? object.x + object.width - origin_x : object.x - origin_x; let current_y = object.y + object.height - origin_y; let old_x = (slope < 0) ? object.x_old + object.width - origin_x : object.x_old - origin_x; let old_y = object.y_old + object.height - origin_y; let current_cross_product = current_x * slope - current_y; let old_cross_product = old_x * slope - old_y; let top = (slope < 0) ? row * TILE_SIZE + TILE_SIZE + y_offset * slope : row * TILE_SIZE + y_offset; if ((current_x < 0 || current_x > TILE_SIZE) && (object.y + object.height > top && object.y_old + object.height <= top || current_cross_product < 1 && old_cross_product > -1)) { object.jumping = false; object.y_velocity = 0; object.y = top - object.height - this.offset; return true; } else if (current_cross_product < 1 && old_cross_product > -1) { object.jumping = false; object.y_velocity = 0; object.y = row * TILE_SIZE + slope * current_x + y_offset - object.height - this.offset; return true; } return false; }, handleCollision:function(object, area) { var column, row, value; /* TEST TOP */ column = Math.floor(object.x / TILE_SIZE);// The column under the left side of the object: row = Math.floor(object.y / TILE_SIZE);// The row under the top side of the object: value = area.map[row * area.columns + column];// We get the tile value under the top left corner of the object: if (value != 0) this[value](object, column, row);// If it's not a walkable tile, we do narrow phase collision. column = Math.floor((object.x + object.width) / TILE_SIZE);// The column under the right side of the object: value = area.map[row * area.columns + column];// Value under the top right corner of the object. if (value != 0) this[value](object, column, row); /* TEST BOTTOM */ column = Math.floor(object.x / TILE_SIZE);// The column under the left side of the object: row = Math.floor((object.y + object.height) / TILE_SIZE);// The row under the bottom side of the object: value = area.map[row * area.columns + column]; if (value != 0) this[value](object, column, row); column = Math.floor((object.x + object.width) / TILE_SIZE);// The column under the right side of the object: value = area.map[row * area.columns + column]; if (value != 0) this[value](object, column, row); /* TEST LEFT */ column = Math.floor(object.x / TILE_SIZE);// The column under the left side of the object: row = Math.floor(object.y / TILE_SIZE);// Top side row: value = area.map[row * area.columns + column]; if (value != 0) this[value](object, column, row); row = Math.floor((object.y + object.height) / TILE_SIZE);// Bottom side row: value = area.map[row * area.columns + column]; if (value != 0) this[value](object, column, row); /* TEST RIGHT */ column = Math.floor((object.x + object.width) / TILE_SIZE);// The column under the right side of the object: row = Math.floor(object.y / TILE_SIZE);// Top side row: value = area.map[row * area.columns + column]; if (value != 0) this[value](object, column, row); row = Math.floor((object.y + object.height) / TILE_SIZE);// Bottom side row: value = area.map[row * area.columns + column]; if (value != 0) this[value](object, column, row); } }, loop: function(time_stamp) { if (controller.up && !game.player.jumping) { game.player.jumping = true; game.player.y_velocity = -10; } if (controller.left) { game.player.x_velocity -= 0.2; } if (controller.right) { game.player.x_velocity += 0.2; } game.player.x_old = game.player.x; game.player.y_old = game.player.y; game.player.y_velocity += 1; game.player.x += game.player.x_velocity; game.player.y += game.player.y_velocity; game.player.x_velocity *= 0.9; game.player.y_velocity *= 0.9; // Collision detection and response handling with the walls: if (game.player.y < 0) { game.player.y_velocity = 0; game.player.y = 0; } else if (game.player.y + game.player.height > display.buffer.canvas.height) { game.player.jumping = false; game.player.y_velocity = 0; game.player.y = display.buffer.canvas.height - game.player.height - 0.001; } if (game.player.x < 0) { game.player.x_velocity = 0; game.player.x = 0; } else if (game.player.x + game.player.width > display.buffer.canvas.width) { game.player.x_velocity = 0; game.player.x = display.buffer.canvas.width - game.player.width - 0.001; } // Handle collision with world AFTER moving the player. game.collider.handleCollision(game.player, game.area); display.render(game); display.output.innerHTML = "player bottom: " + (game.player.y + game.player.height) + "
tile top: " + Math.round(game.player.y + game.player.height) + "
tile row: " + (Math.floor((game.player.y + game.player.height) / TILE_SIZE)); window.requestAnimationFrame(game.loop); } }; //////////////////// //// INITIALIZE //// //////////////////// display.buffer.canvas.height = 128; display.buffer.canvas.width = 128; display.output.style.textAlign = "left"; display.output.style.justifySelf = "center"; display.output.style.whiteSpace = "pre"; window.addEventListener("keydown", controller.keyUpDown); window.addEventListener("keyup", controller.keyUpDown); window.addEventListener("resize", display.resize); display.resize(); game.loop(); })(); ================================================ FILE: content/tile-world/tile-world.css ================================================ /* Frank Poth 07/16/2017 */ * { box-sizing:border-box; margin:0; padding:0; } html { height:100%; width:100%; } body { align-content:space-around; background-color:#202830; color:#ffffff; display:grid; grid-template-columns:auto; grid-template-rows:auto auto; justify-items:center; min-height:100%; width:100%; } canvas { background-color:#ffffff; } ================================================ FILE: content/tile-world/tile-world.html ================================================ PoP Vlog - Tile World

PoP Vlog - Tile World

================================================ FILE: content/tile-world/tile-world.js ================================================ // Frank Poth 09/27/2017 (function() { var buffer, context, drawMap, map, size; buffer = document.createElement("canvas").getContext("2d"); context = document.querySelector("canvas").getContext("2d"); map = [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 1,0,1,1,1,0,0,1,0,0,1,0,0,1,0,1, 1,0,0,1,0,1,0,1,0,1,0,1,0,1,0,1, 1,0,0,1,0,0,0,1,0,1,1,1,0,1,0,1, 1,0,0,1,0,1,0,1,0,1,0,0,0,0,0,1, 1,0,0,1,0,1,0,1,0,0,1,1,0,1,0,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]; size = 32; buffer.canvas.width = 16 * size; buffer.canvas.height = 9 * size; drawMap = function() { for (let index = 0; index < map.length; index ++) { buffer.fillStyle = (map[index] == 1)?"#000000":"#ffffff"; buffer.fillRect((index % 16) * size, Math.floor(index/16) * size, size, size); } context.drawImage(buffer.canvas, 0, 0, buffer.canvas.width, buffer.canvas.height, 0, 0, context.canvas.width, context.canvas.height); }; // just keeps the canvas element sized appropriately resize = function(event) { context.canvas.width = Math.floor(document.documentElement.clientWidth - 32); if (context.canvas.width > document.documentElement.clientHeight) { context.canvas.width = Math.floor(document.documentElement.clientHeight); } context.canvas.height = Math.floor(context.canvas.width * 0.5625); drawMap(); }; window.addEventListener("resize", resize, {passive:true}); resize(); })(); ================================================ FILE: content/top-down-tiles/top-down-tiles.css ================================================ /* Frank Poth 11/16/2017 */ * { box-sizing:border-box; margin:0; padding:0; } html { height:100%; width:100%; } body { align-content:space-around; background-color:#202830; color:#ffffff; display:grid; grid-template-columns:auto; grid-template-rows:auto auto auto auto; height:100%; justify-content:space-around; text-align:center; width:100%; } canvas { background-color:#303840; justify-self:center; } ================================================ FILE: content/top-down-tiles/top-down-tiles.html ================================================ PoP Vlog - Top Down Tiles

PoP Vlog - Top Down Tiles

 

This example requires a keyboard.

================================================ FILE: content/top-down-tiles/top-down-tiles.js ================================================ // Frank Poth 11/20/2017 // just a note, my "j" key is very unresponsive. If there are Js missing, that's why. (function() { "use strict" var controller, display, game; controller = { down:false, left:false, right:false, up:false, keyUpDown:function(event) { var key_state = (event.type == "keydown")?true:false; switch(event.keyCode) { case 37: controller.left = key_state; break; // left key case 38: controller.up = key_state; break; // up key case 39: controller.right = key_state; break; // right key case 40: controller.down = key_state; break; // down key } } }; display = { buffer:document.createElement("canvas").getContext("2d"), context:document.querySelector("canvas").getContext("2d"), output:document.querySelector("p"), render:function() { for (let index = game.world.map.length - 1; index > -1; -- index) { this.buffer.fillStyle = (game.world.map[index] > 0)?("#0099" + game.world.map[index] + "f"):"#303840"; this.buffer.fillRect((index % game.world.columns) * game.world.tile_size, Math.floor(index / game.world.columns) * game.world.tile_size, game.world.tile_size, game.world.tile_size); } this.buffer.fillStyle = game.player.color; this.buffer.fillRect(game.player.x, game.player.y, game.player.width, game.player.height); this.context.drawImage(this.buffer.canvas, 0, 0, this.buffer.canvas.width, this.buffer.canvas.height, 0, 0, this.context.canvas.width, this.context.canvas.height); }, resize:function(event) { var client_height = document.documentElement.clientHeight; display.context.canvas.width = document.documentElement.clientWidth - 32; if (display.context.canvas.width > client_height) { display.context.canvas.width = client_height; } display.context.canvas.height = Math.floor(display.context.canvas.width * 0.625); display.render(); } }; game = { /* This is the player object. Make sure to note that for this collision method to work, you must record the current and last positions of your game objects to use in collision detection. It is used to calculate the vector used to determine if an object is entering into a collision tile and from what side. */ player: { color:"#ff9900", height:32, old_x:160,// these are what you should take note of. Don't worry, it's useful old_y:160,// to keep track of old positions for many physics methods. These aren't one trick pony's. velocity_x:0, velocity_y:0, width:32, x:160 - 16, y:100 - 16, // These functions just make it easy to read the collision code get bottom() { return this.y + this.height; }, get oldBottom() { return this.old_y + this.height; }, get left() { return this.x; },// kind of pointless, but used get oldLeft() { return this.old_x; },// to help visualize the collision methods get right() { return this.x + this.width; }, get oldRight() { return this.old_x + this.width; }, get top() { return this.y; },// equally pointless as left side calculations get oldTop() { return this.old_y; } }, world: { columns:8, rows:5, tile_size:40, map:[1,1,1,1,1,1,1,1,// 1s are ceiling tiles 2,0,0,0,0,0,0,3,// 2s and 3s are left and right wall tiles 2,0,5,0,0,5,0,3,// 5 is a solid block/box tile 2,0,0,0,0,0,0,3,// 4 is a floor tile 4,4,4,4,4,4,4,4]// the routing functions below with matching numbers // represent these tiles and route their values to // the collision methods you might expect based on their names }, /* This object is responsible for getting the collision methods that match tile values in the map. It uses what I call routing functions to group reusable narrow phase collision methods together to create a variety of different tile boundary shapes. You can get the same effect with an array of routing functions rather than giving an object's routing functions numeric indexes, if you prefer. It might be faster. */ collision: { // top wall collision tile 1:function(object, row, column) { // The player hits the bottom of ceiling tiles. this.bottomCollision(object, row); }, // left wall collision tile 2:function(object, row, column) { this.rightCollision(object, column); }, // right wall collision tile 3:function(object, row, column) { // The player collides with the left side of a wall on the right of the map. this.leftCollision(object, column);// Confusing to visualize, but true. }, // bottom wall collision tile 4:function(object, row, column) { this.topCollision(object, row); }, // block tile with four walls 5:function(object, row, column) { /* It makes sense to test the most likely side to be collided with first. In a top down game, all sides are hit about the same number of times, depending on gameplay, but in a side scroller with downward pulling gravity, the tops of tiles will usually get the most traffic, so it makes sense to test the tops first in those scenarios. */ if (this.topCollision(object, row)) { return; }// Make sure to early out if (this.leftCollision(object, column)) { return; }// if a collision is detected. if (this.rightCollision(object, column)) { return; } this.bottomCollision(object, row);// No need to early out on the last check. }, /* Here are the narrow phase collision detection and response functions. For a deeper insight into how these work, and what's going on, I'm using leftCollision as an example. The principles I explain here apply to all of the other narrow phase collision methods. Note that they're basically doing the same thing, just for different flat sides of a tile. */ leftCollision(object, column) { /* To calculate the player's movement vector, I'm no longer using its velocity (like in the last tutorial). Instead, I'm going to calculate it based on it's current and last positions. the old way was if (object.velocity_x > 0), now it's if (object.x - object.old_x > 0). This is a bit more reliable in my opinion, depending on how and when you calculate your velocity. */ /* If the object is not moving right, how will it ENTER into the left side of a tile? If not moving right, the player can't possibly ENTER the left side of a tile. We are only concerned with resolving collisions objects ENTER into. If they are spawned inside a collision tile, it is not the collision manager's job to resolve that, it is the level designer's job to fix the level design so collision can be as efficient as possible. */ if (object.x - object.old_x > 0) { // the left side of the specified tile column var left = column * game.world.tile_size; /* This tests to see if our object is ENTERING through the collision boundary along the correct movement vector. If its current position is past the boundary and its old position is before the boundary, we know that it has entered into collision with the tile. */ if (object.right > left && object.oldRight <= left) { object.velocity_x = 0; object.x = object.old_x = left - object.width - 0.001; /* You really don't need to reset the player's old position here if you are setting it at the start of each game loop, but if you are using a fixed time step game loop with interpolation, it will make your collisions look more pixel perfect. The reason for this is because with interpolation, your player may be drawn slightly away from the wall he just collided with on the next frame of animation depending on how much time has passed. So, you don't need to do this, but it doesn't hurt. It may be a problem if you end up changing old positions for player to player collisions as well, however, because old positions are used to calculate vectors for tile collision. Just make sure changing old positions are the last thing you do before the next frame, and you should be fine. */ return true; } } return false; }, rightCollision(object, column) { if (object.x - object.old_x < 0) { // the right side of the specified tile column var right = (column + 1) * game.world.tile_size; if (object.left < right && object.oldLeft >= right) { object.velocity_x = 0; object.old_x = object.x = right; return true; } } return false; }, bottomCollision(object, row) { // if the object is moving up if (object.y - object.old_y < 0) { var bottom = (row + 1) * game.world.tile_size; if (object.top < bottom && object.oldTop >= bottom) { object.velocity_y = 0; object.old_y = object.y = bottom; } } }, topCollision(object, row) { // if the object is moving down if (object.y - object.old_y > 0) { // the top side of the specified tile row var top = row * game.world.tile_size; // if the object has passed through the tile boundary since the last game cycle if (object.bottom > top && object.oldBottom <= top) { object.velocity_y = 0; object.old_y = object.y = top - object.height - 0.01; return true; } } return false; } }, // The game loop: loop:function() { // Get controller input and move that player object! if (controller.down) { game.player.velocity_y += 0.25; } if (controller.left) { game.player.velocity_x -= 0.25; } if (controller.right) { game.player.velocity_x += 0.25; } if (controller.up) { game.player.velocity_y -= 0.25; } // Update the player object: game.player.old_x = game.player.x;// Set the old position to the current position game.player.old_y = game.player.y;// before we update the current position, thus making it current game.player.x += game.player.velocity_x;// Update the current position game.player.y += game.player.velocity_y; // Do collision detection and response with the boundaries of the screen. if (game.player.x < 0) { game.player.velocity_x = 0; game.player.old_x = game.player.x = 0; } else if (game.player.x + game.player.width > display.buffer.canvas.width) { game.player.velocity_x = 0; game.player.old_x = game.player.x = display.buffer.canvas.width - game.player.width - 0.001; /* I added that - 0.001 to the equation to really push the player object back into the game world. If the edge of the world is 320 px, and you set the right edge of the player obect to 320, technically its tile position will be 1 tile to the right of the edge of the world when calculated. Say there are 10 columns in the map, and each tile is 32 pixels wide. 320/32 is 10, but since our map index starts at 0, a value of 10 falls outside of the map's number of columns. Failing to handle this can result in testing collision on tiles in another row, or tiles at undefined positions in the map array.*/ } if (game.player.y < 0) { game.player.velocity_y = 0; game.player.old_y = game.player.y = 0; } else if (game.player.y + game.player.height > display.buffer.canvas.height) { game.player.velocity_y = 0; game.player.old_y = game.player.y = display.buffer.canvas.height - game.player.height - 0.001; } /* Here is where we do broadphase collision detection with the four corners of our player object. This is very important. In the last tutorial the player only had 1 collision registration point. Collision was simple, then. Now, it's just as simple, but we need to do it for each corner of our player object. */ /* Once again, there's no point of testing collision if we're not moving. depending on which direction we are moving we must test different collision registration points on our player. For instance, if he is moving left, we test the points on his left side: */ if (game.player.x - game.player.old_x < 0) {// test collision on left side of player if moving left /* There are much more efficient ways to write this code without repeating variable names and this big monsterous block of if statements, but I thought it would be good to have specific variable names and less abstraction so you can get an idea of what's going on. */ var left_column = Math.floor(game.player.left / game.world.tile_size); var bottom_row = Math.floor(game.player.bottom / game.world.tile_size); var value_at_index = game.world.map[bottom_row * game.world.columns + left_column]; if (value_at_index != 0) {// Check the bottom left point game.collision[value_at_index](game.player, bottom_row, left_column); display.output.innerHTML = "last tile collided with: " + value_at_index; } var top_row = Math.floor(game.player.top / game.world.tile_size); value_at_index = game.world.map[top_row * game.world.columns + left_column]; if (value_at_index != 0) {// Check the top left point game.collision[value_at_index](game.player, top_row, left_column); display.output.innerHTML = "last tile collided with: " + value_at_index; } } else if (game.player.x - game.player.old_x > 0) {// Is the player moving right? var right_column = Math.floor(game.player.right / game.world.tile_size); var bottom_row = Math.floor(game.player.bottom / game.world.tile_size); var value_at_index = game.world.map[bottom_row * game.world.columns + right_column]; if (value_at_index != 0) {// Check the bottom right point game.collision[value_at_index](game.player, bottom_row, right_column); display.output.innerHTML = "last tile collided with: " + value_at_index; } var top_row = Math.floor(game.player.top / game.world.tile_size); value_at_index = game.world.map[top_row * game.world.columns + right_column]; if (value_at_index != 0) {// Check the top right point game.collision[value_at_index](game.player, top_row, right_column); display.output.innerHTML = "last tile collided with: " + value_at_index; } } if (game.player.y - game.player.old_y < 0) { var left_column = Math.floor(game.player.left / game.world.tile_size); var top_row = Math.floor(game.player.top / game.world.tile_size); var value_at_index = game.world.map[top_row * game.world.columns + left_column]; if (value_at_index != 0) {// Check the top left point game.collision[value_at_index](game.player, top_row, left_column); display.output.innerHTML = "last tile collided with: " + value_at_index; } var right_column = Math.floor(game.player.right / game.world.tile_size); value_at_index = game.world.map[top_row * game.world.columns + right_column]; if (value_at_index != 0) {// Check the top right point game.collision[value_at_index](game.player, top_row, right_column); display.output.innerHTML = "last tile collided with: " + value_at_index; } } else if (game.player.y - game.player.old_y > 0) { var left_column = Math.floor(game.player.left / game.world.tile_size); var bottom_row = Math.floor(game.player.bottom / game.world.tile_size); var value_at_index = game.world.map[bottom_row * game.world.columns + left_column]; if (value_at_index != 0) {// Check the bottom left point game.collision[value_at_index](game.player, bottom_row, left_column); display.output.innerHTML = "last tile collided with: " + value_at_index; } var right_column = Math.floor(game.player.right / game.world.tile_size); value_at_index = game.world.map[bottom_row * game.world.columns + right_column]; if (value_at_index != 0) {// Check the bottom right point game.collision[value_at_index](game.player, bottom_row, right_column); display.output.innerHTML = "last tile collided with: " + value_at_index; } } game.player.velocity_x *= 0.9; game.player.velocity_y *= 0.9; display.render(); window.requestAnimationFrame(game.loop); } }; display.buffer.canvas.height = 200; display.buffer.canvas.width = 320; window.addEventListener("resize", display.resize); window.addEventListener("keydown", controller.keyUpDown); window.addEventListener("keyup", controller.keyUpDown); display.resize(); game.loop(); })(); ================================================ FILE: content/touch-controller/touch-controller.css ================================================ /* Frank Poth 10/03/2017 */ * { box-sizing:border-box; margin:0; padding:0; } html { height:100%; width:100%; } body { align-content:space-around; background-color:#202830; color:#ffffff; display:grid; justify-content:center; min-height:100%; text-align:center; width:100%; } canvas { background-color:#ffffff; justify-self:center; } ================================================ FILE: content/touch-controller/touch-controller.html ================================================ touch controller

PoP Vlog - Touch Controller

touches: 0
- -

================================================ FILE: content/touch-controller/touch-controller.js ================================================ // Frank Poth 10/03/2017 (function() { var Button, controller, display, game; // basically a rectangle, but it's purpose here is to be a button: Button = function(x, y, width, height, color) { this.active = false; this.color = color; this.height = height; this.width = width; this.x = x; this.y = y; } Button.prototype = { // returns true if the specified point lies within the rectangle: containsPoint:function(x, y) { // if the point is outside of the rectangle return false: if (x < this.x || x > this.x + this.width || y < this.y || y > this.y + this.width) { return false; } return true; } }; // handles everything to do with user input: controller = { buttons:[ new Button(0, 160, 60, 60, "#f09000"), new Button(190, 160, 60, 60, "#0090f0"), new Button(260, 160, 60, 60, "#0090f0") ], testButtons:function(target_touches) { var button, index0, index1, touch; // loop through all buttons: for (index0 = this.buttons.length - 1; index0 > -1; -- index0) { button = this.buttons[index0]; button.active = false; // loop through all touch objects: for (index1 = target_touches.length - 1; index1 > -1; -- index1) { touch = target_touches[index1]; // make sure the touch coordinates are adjusted for both the canvas offset and the scale ratio of the buffer and output canvases: if (button.containsPoint((touch.clientX - display.bounding_rectangle.left) * display.buffer_output_ratio, (touch.clientY - display.bounding_rectangle.top) * display.buffer_output_ratio)) { button.active = true; break;// once the button is active, there's no need to check if any other points are inside, so continue } } } // this is all just for displaying the messages when buttons are pressed. This isn't necessary code. display.message.innerHTML = "touches: " + event.targetTouches.length + "
- "; if (this.buttons[0].active) { display.message.innerHTML += "jump "; } if (this.buttons[1].active) { display.message.innerHTML += "left "; } if (this.buttons[2].active) { display.message.innerHTML += "right "; } display.message.innerHTML += "-"; }, touchEnd:function(event) { event.preventDefault(); controller.testButtons(event.targetTouches); }, touchMove:function(event) { event.preventDefault(); controller.testButtons(event.targetTouches); }, touchStart:function(event) { event.preventDefault(); controller.testButtons(event.targetTouches); } }; // handles everything to do with displaying graphics on the screen: display = { // the buffer is used to scale the applications graphics to fit the screen: buffer:document.createElement("canvas").getContext("2d"), // the on screen canvas context that we will be drawing to: output:document.querySelector("canvas").getContext("2d"), // the p element for text output: message:document.querySelector("p"), // the ratio in size between the buffer and output canvases used to scale user input coordinates: buffer_output_ratio:1, // the bounding rectangle of the output canvas used to determine the location of user input on the output canvas: bounding_rectangle:undefined, // clears the display canvas to the specified color: clear:function(color) { this.buffer.fillStyle = color || "#000000"; this.buffer.fillRect(0, 0, this.buffer.canvas.width, this.buffer.canvas.height); }, // renders the buffer to the output canvas: render:function() { this.output.drawImage(this.buffer.canvas, 0, 0, this.buffer.canvas.width, this.buffer.canvas.height, 0, 0, this.output.canvas.width, this.output.canvas.height); }, // renders the buttons: renderButtons:function(buttons) { var button, index; this.buffer.fillStyle = "#202830"; this.buffer.fillRect(0, 150, this.buffer.canvas.width, this.buffer.canvas.height); for (index = buttons.length - 1; index > -1; -- index) { button = buttons[index]; this.buffer.fillStyle = button.color; this.buffer.fillRect(button.x, button.y, button.width, button.height); } }, // renders a square: renderSquare:function(square) { this.buffer.fillStyle = square.color; this.buffer.fillRect(square.x, square.y, square.width, square.height); }, // just keeps the output canvas element sized appropriately: resize:function(event) { display.output.canvas.width = Math.floor(document.documentElement.clientWidth - 32); if (display.output.canvas.width > document.documentElement.clientHeight) { display.output.canvas.width = Math.floor(document.documentElement.clientHeight); } display.output.canvas.height = Math.floor(display.output.canvas.width * 0.6875); // these next two lines are used for adjusting and scaling user touch input coordinates: display.bounding_rectangle = display.output.canvas.getBoundingClientRect(); display.buffer_output_ratio = display.buffer.canvas.width / display.output.canvas.width; } }; // handles game logic: game = { loop:function(time_stamp) { if (controller.buttons[0].active && game.square.jumping == false) { game.square.velocity_y = -20; game.square.jumping = true; } if (controller.buttons[1].active) { game.square.velocity_x -= 0.5; } if (controller.buttons[2].active) { game.square.velocity_x += 0.5; } // simulate gravity: game.square.velocity_y += 1.5; // simulate friction: game.square.velocity_x *= 0.9; game.square.velocity_y *= 0.9; // move the square: game.square.x += game.square.velocity_x; game.square.y += game.square.velocity_y; // collision detection for the square and the boundaries of the graphics buffer: if (game.square.x + game.square.width < 0) { game.square.x = display.buffer.canvas.width; } else if (game.square.x > display.buffer.canvas.width) { game.square.x = 0; } if (game.square.y + game.square.height > 150) { game.square.y = 150 - game.square.height; game.square.jumping = false; } display.clear("#303840"); display.renderSquare(game.square); display.renderButtons(controller.buttons); display.render(); window.requestAnimationFrame(game.loop); }, square: { color:"#ff0000", height:32, jumping:true, velocity_x:0, velocity_y:0, width:32, x:0, y:0, } }; // initialize the application // size the buffer: display.buffer.canvas.height = 220; display.buffer.canvas.width = 320; window.addEventListener("resize", display.resize); // setting passive:false allows you to use preventDefault in event listeners: display.output.canvas.addEventListener("touchend", controller.touchEnd, {passive:false}); display.output.canvas.addEventListener("touchmove", controller.touchMove, {passive:false}); display.output.canvas.addEventListener("touchstart", controller.touchStart, {passive:false}); // make sure the display canvas is the appropriate size on the screen: display.resize(); // start the game loop: game.loop(); })(); ================================================ FILE: content/vector-math/vector-math.html ================================================ Vector Math
Angle Cross Product Dot Product Length

Choose an example to see how it works

================================================ FILE: content/walk-on-tiles/walk-on-tiles.css ================================================ /* Frank Poth 11/15/2017 */ * { box-sizing:border-box; margin:0; padding:0; } html { height:100%; width:100%; } body { align-content:space-around; background-color:#202830; color:#ffffff; display:grid; grid-template-columns:auto; grid-template-rows:auto auto auto auto; height:100%; justify-content:space-around; text-align:center; width:100%; } canvas { background-color:#303840; justify-self:center; } ================================================ FILE: content/walk-on-tiles/walk-on-tiles.html ================================================ PoP Vlog - Walk On Tiles

PoP Vlog - Walk On Tiles

 

Blue squares are invading! Quickly! Use the keyboard to wield the mighty yellow circle and defeat them!

================================================ FILE: content/walk-on-tiles/walk-on-tiles.js ================================================ // Frank Poth 11/15/2017 (function() { "use strict" // used to throw a compiler error if a variable is not propperly defined // the three main parts of the program: var controller, display, game; // holds our controller specific code: controller = { down:false, left:false, right:false, up:false, keyUpDown:function(event) { var key_state = (event.type == "keydown")?true:false; switch(event.keyCode) { case 37: controller.left = key_state; break; // left key case 38: controller.up = key_state; break; // up key case 39: controller.right = key_state; break; // right key case 40: controller.down = key_state; break; // down key } } }; // holds display specific code used for interacting with HTML and drawing to // the canvas: display = { buffer:document.createElement("canvas").getContext("2d"), // used to draw image in game dimensions context:document.querySelector("canvas").getContext("2d"), // used to display final scaled image in browser window output:document.querySelector("p"), // used to show output in browser window render:function() { // draw the tile map for (let index = game.world.map.length - 1; index > -1; -- index) { this.buffer.fillStyle = (game.world.map[index] == 1)?"#0099ff":"#303840"; // I have another tutorial that explains the math behind converting 1d map coordinates to 2d world coordinates this.buffer.fillRect((index % game.world.columns) * game.world.tile_size, Math.floor(index / game.world.columns) * game.world.tile_size, game.world.tile_size, game.world.tile_size); } // draw the player this.buffer.fillStyle = game.player.color; this.buffer.beginPath(); this.buffer.arc(game.player.x, game.player.y, game.player.radius, 0, Math.PI * 2); this.buffer.closePath(); this.buffer.fill(); // draw the buffer to the display context. this will automatically scale your image. // note that drawing this huge image on every frame is pretty memory intensive, and is a bad technique // for an actual game. I just use it in my examples because it is quick to set up and takes care of scaling. this.context.drawImage(this.buffer.canvas, 0, 0, this.buffer.canvas.width, this.buffer.canvas.height, 0, 0, this.context.canvas.width, this.context.canvas.height); // output all that nice data! this.output.innerHTML = "tile_x: " + game.player.tile_x + "
tile_y: " + game.player.tile_y + "
map index: " + game.player.tile_y + " * " + game.world.columns + " + " + game.player.tile_x + " = " + String(game.player.tile_y * game.world.columns + game.player.tile_x); }, // keep the canvas sized properly: resize:function(event) { var client_height = document.documentElement.clientHeight; display.context.canvas.width = document.documentElement.clientWidth - 32; if (display.context.canvas.width > client_height) { display.context.canvas.width = client_height; } // everyone loves a 16/9 aspect ratio! display.context.canvas.height = Math.floor(display.context.canvas.width * 0.5625); display.render();// render the display again so you don't see a flicker on screen resize } }; // holds game logic code: game = { // when the counter reaches 0, a tile turns blue counter:Math.random() * 100, // the player is just a yellow circle, doing yellow circle things: player: { color:"#ff9900", radius:8, tile_x:undefined,// the x and y tile positions of the player tile_y:undefined, velocity_x:0, velocity_y:0, x:160,// center screen y:90 }, // the world object holds information about the game world, such as the // level map and its dimensions as well as tile size world: { columns:16,// there are 16 columns and 9 rows in the map rows:9, tile_size:20,// each tile is 20 pixels wide and high // the one dimensional tile map: map:[1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0], }, // this is the game loop. it is perpetuated by requestAnimationFrame loop:function() { // get input from the controller if (controller.down) { game.player.velocity_y += 0.25; } if (controller.left) { game.player.velocity_x -= 0.25; } if (controller.right) { game.player.velocity_x += 0.25; } if (controller.up) { game.player.velocity_y -= 0.25; } // do some collision detection with the edge of the screen // if the player is off of the left side of the screen if (game.player.x - game.player.radius < 0) { game.player.velocity_x = 0;// stop it from moving game.player.x = game.player.radius;// reposition it } else if (game.player.x + game.player.radius > display.buffer.canvas.width) { // or the right side of the screen game.player.velocity_x = 0; game.player.x = display.buffer.canvas.width - game.player.radius; } if (game.player.y - game.player.radius < 0) { game.player.velocity_y = 0; game.player.y = game.player.radius; } else if (game.player.y + game.player.radius > display.buffer.canvas.height) { game.player.velocity_y = 0; game.player.y = display.buffer.canvas.height - game.player.radius; } // add the player's velocity to its x and y positions game.player.x += game.player.velocity_x; game.player.y += game.player.velocity_y; // simulate friction and slow the player down game.player.velocity_x *= 0.9; game.player.velocity_y *= 0.9; // here is all the important stuff: // calculate the x and y tile in the map that the player is standing on game.player.tile_x = Math.floor(game.player.x / game.world.tile_size); game.player.tile_y = Math.floor(game.player.y / game.world.tile_size); // set the tile the player is standing on to 0 in the map game.world.map[game.player.tile_y * game.world.columns + game.player.tile_x] = 0; // now we will test for win conditions let victory = true; // we must check every tile to see if there are any 1s left (1s are blue tiles) for (let index = game.world.map.length - 1; index > -1; -- index) { // if any 1s exist, we cannot win and there's no point in continuing the for loop if (game.world.map[index] == 1) { victory = false; break; } } // that's right, you can win this "game" if (victory) { // reset the controller so the alert prompt doesn't freeze the keydown event and make the player keep moving in its current direction controller.down = controller.left = controller.right = controller.up = false; game.counter = -1;// reset the counter to -1 so another blue square can be generated immediately alert("You have done it! You have vanquished the evil blue squares! But they will rise again..."); } // when the counter reaches zero, make a random tile in the map a 1 game.counter --; if (game.counter < 0) { game.counter = Math.random() * 125;// reset the counter to a random value between 0 and 125 game.world.map[Math.floor(Math.random() * game.world.map.length)] = 1;// reset that random tile! } // draw all the tiles, the player, and output data display.render(); // call the loop function again once the window is ready to draw window.requestAnimationFrame(game.loop); } }; // initialize the application by setting default values: // set the buffer size. this will be the world dimensions. display.buffer.canvas.height = 180; display.buffer.canvas.width = 320; window.addEventListener("resize", display.resize); window.addEventListener("keydown", controller.keyUpDown); window.addEventListener("keyup", controller.keyUpDown); // size the canvas element display.resize(); // start the game loop! game.loop(); })(); ================================================ FILE: content/web-app/manifest.json ================================================ { "author": "PoP Vlog", "background_color": "#ffffff", "description": "Progressive Web App Example", "display": "fullscreen", "icons": [ { "src": "https://192.168.0.101:2468/web-app.png", "sizes": "192x192", "type": "image/png" } ], "manifest_version": 2, "name": "Web App", "orientation": "portrait", "short_name": "Web App", "start_url": "https://192.168.0.101:2468/web-app.html", "theme_color": "#ffffff", "version": "0.1" } ================================================ FILE: content/web-app/server.js ================================================ // Frank Poth 10/16/2017 (function() { var fs, https, mimetypes, options, path, server; fs = require("fs"); // file system https = require("https"); // creates an https server path = require("path"); // used for working with url paths // used to create response headers /* If the user requests a .css file, we want to ensure we attach "text/css" to our response header, this way the browser knows how to handle it. */ mimetypes = { "css":"text/css", "html":"text/html", "ico":"image/ico", "jpg":"image/jpeg", "js":"text/javascript", "json":"application/json", "png":"image/png" }; options = { pfx: fs.readFileSync("ssl/crt.pfx"), passphrase: "password" }; // Start a secure server that uses the credentials in ssl/crt.pfx server = https.createServer(options, function(request, response) { /* When requesting the homepage of a website, we usually only type www.mysite.com, but the server returns www.mysite.com/index.html. To make it easier for users to access our site, we add "/index.html" to their url so the user doesn't have to type out the whole address of our home page. */ // If the url is empty if (request.url == "" || request.url == "/") { // The user is requesting the home page of the website, so give it to them request.url = "web-app.html"; } // Next we read the file at the requested url and write it to the document. /* __dirname is just the base directory of your website, so if your website is www.coolsite.com, then __dirname is www.coolsite.com. When you put it all together it looks like www.coolsite.com/index.html or whatever the requested url is */ fs.readFile(__dirname + "/" + request.url, function(error, content) { if (error) { // if there is an error reading the requested url console.log("Error: " + error); // output it to the console } else { // else, there is no error, write the file contents to the page // 200 is code for OK, and the second parameter is our content header response.writeHead(200, {'Content-Type':mimetypes[path.extname(request.url).split(".")[1]]}); response.write(content); // write that content to our response object } response.end(); // This will send our response object to the browser }); }); server.listen("2468", "192.168.0.101", function() { console.log("Server started!"); }); // listen on 192.168.0.101:2468 })(); ================================================ FILE: content/web-app/web-app.css ================================================ /* Frank Poth 10/17/2017 */ * { box-sizing:border-box; margin:0; padding:0; } html { height:100%; width:100%; } body { align-content:space-around; background-color:#202830; color:#ffffff; display:grid; height:100%; justify-content:space-around; justify-items:space-around; padding:8px; text-align:center; width:100%; } img { margin:0 auto; } p { font-size:1.1em; text-align:justify; } ================================================ FILE: content/web-app/web-app.html ================================================ Web App

Android Web App!

This page can be viewed in any browser, but it can also work in a web app! If you are viewing this page in a full screened webview on your mobile device, you are looking at a fully functional web app! You can use this technology to better connect with your users or create a full screen mobile experience for your HTML5 games!

================================================ FILE: content/wmw-basic/basic.html ================================================ WMW-Basics

Learn some basic html stuff.

================================================ FILE: content/wmw-bouncing-balls/bouncing-balls.html ================================================ Ball Bounce ================================================ FILE: data/logs.json ================================================ [ { "name":"I'm Still Alive!", "note":"I know it's been a while since my last post, but I'm still out here writing code and twisting my brain in knots over interesting programming concepts. I recently posted part 17 of the Stay Down Dev Log and I intend to finish that project and post the end result to itch.io. Feel free to reach out to me in the comments on YouTube. As always, I hope you learn something while you're here! Have a good one!", "date":"2021-09-06" }, { "name":"Rumble.com", "note":"I recently discovered Rumble.com which I'm going to use in addition to YouTube. If anyone wants to check out my channel there, click the link at the top right of this page! Stay down is still the latest project and is receiving regular updates, so check back to see the game's progress!", "date":"2020-12-30" }, { "name":"Stay Down Devlog:", "note":"Lately I've been working on a small game I plan to publish on itch.io. The idea is that platforms push you up and if you don't stay down you die. Pretty genious, I know, but the real goal of the project is to share with everyone how it's made. If you follow the videos you'll learn about fixed time step loops, game states, collision detection, and much more. Hopefully by the time I'm finished you'll have enough knowledge to publish your own fully featured game on itch.io.", "date":"2020-09-05" }, { "name":"Project Trajectory", "note":"My latest projects focus on improving rendering performance by reducing draw calls. These improvements will greatly improve the speed of your HTML5 game.", "date":"2020-06-14" }, { "name":"New Website!", "note":"This is the new website. You can search for projects and easily access related videos and source code. Here in the logs section I'll post about the latest project or whatever other updates I think might be helpful.", "date":"2019-12-24" } ] ================================================ FILE: data/projects.json ================================================ [ { "name":"Stay Down", "note":"Try to stay down while collecting the items. Keyboard controls.", "path":"stay-down", "page":"stay-down", "vlog":"-8SmBDbwAsw&list=PLcN6MkgfgN4D5Fo9zL9xlaSxL-KD7iN84", "date":"2020-08-01", "tags":"js,platformer" }, { "name":"Better Tile with Graphics", "note":"Uses a canvas buffer to improve rendering performance. Shows how to load a tile sheet image and draw a tile map.", "path":"better-tile-graphics", "page":"better-tile-graphics", "vlog":"", "date":"2020-06-14", "tags":"js,tile,performance,graphics,fetch" }, { "name":"Better Tile", "note":"A more efficient way to draw a tile map.", "path":"better-tile", "page":"better-tile", "vlog":"XmmvblvSOho", "date":"2019-12-31", "tags":"js,tile,performance" }, { "name":"Bouncing Polygons", "note":"Bounce convex polygons off of a non-axis aligned line.", "path":"bouncing-polygons", "page":"bouncing-polygons", "vlog":"Newm6jnp7T0", "date":"2018-mm-dd", "tags":"JS" }, { "name":"Polygon Basics", "note":"A good place to get started with 2D polygons.", "path":"polygon", "page":"polygon", "vlog":"3kICmEJ-xyo", "date":"2018-mm-dd", "tags":"JS" }, { "name":"GJK Collision", "note":"There is no video for this project (yet).", "path":"gjk", "page":"gjk", "vlog":"", "date":"2018-mm-dd", "tags":"JS" }, { "name":"Polygon Rotation", "note":"There is no video for this project.", "path":"polygon-rotation", "page":"polygon-rotation", "vlog":"", "date":"2018-mm-dd", "tags":"JS" }, { "name":"Hitbox", "note":"Showcases moving hitboxes, tunneling, and non-tile based platforming.", "path":"hitbox", "page":"hitbox", "vlog":"VpSWuywFlC8", "date":"2018-mm-dd", "tags":"js,collision,tunneling" }, { "name":"Vector Math", "note":"There are multiple videos that go with this example. How do you get to them? I haven't implemented that functionality yet.", "path":"vector-math", "page":"vector-math", "vlog":"b5TjpTBW6yw", "date":"2018-mm-dd", "tags":"JS" }, { "name":"Particle Pool", "note":"Use object pooling to conserve memory when using lots of objects.", "path":"particle-pool", "page":"particle-pool", "vlog":"9dp0mAc2vvY", "date":"2018-mm-dd", "tags":"JS" }, { "name":"Simple Inventory", "note":"A very basic inventory system where you click an item to take it out of the world and put it into an array.", "path":"inventory", "page":"inventory", "vlog":"AKJMf73H1Jc", "date":"2018-mm-dd", "tags":"JS" }, { "name":"Tile Animation", "note":"Animate some background tiles based on frame rate.", "path":"tile-animation", "page":"tile-animation", "vlog":"AQABpi9nLfU", "date":"2018-mm-dd", "tags":"JS" }, { "name":"Rectangle Collision", "note":"Collision between two rectangles.", "path":"rectangle-collision", "page":"rectangle-collision", "vlog":"LYrge3ylccQ", "date":"2018-mm-dd", "tags":"JS" }, { "name":"Moving Platforms", "note":"Moving platforms for a 2D platforming game.", "path":"platform", "page":"platform", "vlog":"1eHAFNUPlk8", "date":"2018-mm-dd", "tags":"js,platformer" }, { "name":"Platformer AI", "note":"Shows how to make a few different AI types for a tile based platformer.", "path":"platformer-ai", "page":"platformer-ai", "vlog":"zbqwFb8DJgQ", "date":"2018-mm-dd", "tags":"js,platformer" }, { "name":"Shoot", "note":"Very basic shooting mechanism for 2D platformers. Click to shoot.", "path":"shoot", "page":"shoot", "vlog":"WML5a8QRtnw", "date":"2018-mm-dd", "tags":"js,platformer" }, { "name":"Scrolling Tile Background", "note":"Showcases a scrolling tile background as well as some very simple terrain interaction.", "path":"tile-scroll", "page":"tile-scroll", "vlog":"jabYMh9sI8Q", "date":"2018-mm-dd", "tags":"js,scrolling,side-scroller" }, { "name":"3D Cube", "note":"A 3D cube rendered on the CPU using nothing but pure JavaScript", "path":"cube", "page":"cube", "vlog":"OVQxTNd2U3w", "date":"2018-mm-dd", "tags":"js,3d" }, { "name":"Pseudo 3D Starfield", "note":"A 2D starfield with a 3D effect. Uses a valid 3D projection technique.", "path":"starfield", "page":"starfield", "vlog":"jtWvkNK2LVo", "date":"2018-mm-dd", "tags":"js,3d" }, { "name":"Bouncing Balls", "note":"Bouncing balls that use directional/polar movement rather than axis aligned movement.", "path":"wmw-bouncing-balls", "page":"bouncing-balls", "vlog":"hoWjnidQOms", "date":"2018-mm-dd", "tags":"js" }, { "name":"Basic HTML Layout", "note":"My basic layout for projects. Provides a good starting point for small projects.", "path":"wmw-basic", "page":"basic", "vlog":"hm7py_lZkL8", "date":"2018-mm-dd", "tags":"html,js" }, { "name":"Circle Collision Response", "note":"Collision response technique for colliding circles.", "path":"circle-collision-response", "page":"circle-collision-response", "vlog":"nlwtgvZCz0k", "date":"2018-mm-dd", "tags":"js,collision" }, { "name":"Circle Collision Detection", "note":"Detect collision between two circles.", "path":"circle-collision-detection", "page":"circle-collision-detection", "vlog":"STcP9P122_8", "date":"2018-mm-dd", "tags":"js,collision" }, { "name":"Input Processing Output (IPO)", "note":"I'm probably going to remove this one. It kind of makes sense, but I'm not sure how relevant or helpful it is.", "path":"ipo", "page":"ipo", "vlog":"PFd8Xw5C6Zs", "date":"2018-mm-dd", "tags":"JS" }, { "name":"Rabbit Trap", "note":"There are a few videos on this project. It's basically a how to build a platformer series that probably has too much overhead for what it is. I'm planning on redoing this one.", "path":"rabbit-trap", "page":"rabbit-trap", "vlog":"opiWzi0KWjs", "date":"2018-mm-dd", "tags":"JS,platformer" }, { "name":"Pre-Scale Performance", "note":"Tests out the performance of different scaling techniques. As of 2019, I prefer using CSS to scale.", "path":"pre-scale-performance", "page":"pre-scale-performance", "vlog":"hQD4Yw3IyZ0", "date":"2018-mm-dd", "tags":"JS" }, { "name":"Tile Graphics", "note":"Draw graphics to your tile map.", "path":"tile-graphics", "page":"tile-graphics", "vlog":"XELyA6ECDLk", "date":"2018-mm-dd", "tags":"js,tile,graphics" }, { "name":"Pagination", "note":"Paginates some static content.", "path":"pagination", "page":"pagination", "vlog":"7TSVnm5Fpj0", "date":"2018-mm-dd", "tags":"JS" }, { "name":"Dominique's Doors", "note":"A very basic level loading example using JSON.", "path":"dominiques-doors", "page":"dominiques-doors", "vlog":"96Q200cPFss", "date":"2018-mm-dd", "tags":"js,json" }, { "name":"JSON", "note":"A basic JSON example.", "path":"json", "page":"json", "vlog":"jRuz3bXBDKc", "date":"2018-mm-dd", "tags":"js,json" }, { "name":"Calculator", "note":"Make a JavaScript calculator. Note: It uses eval to calculate equations.", "path":"calculator", "page":"calculator", "vlog":"ySB3YlKqQLA", "date":"2017-mm-dd", "tags":"JS" }, { "name":"Blit", "note":"Blitting graphics.", "path":"blit", "page":"blit", "vlog":"sKf2vJBiBj0", "date":"2017-mm-dd", "tags":"JS" }, { "name":"Dino Run", "note":"A simple endless runner.", "path":"dino", "page":"dino", "vlog":"LZ0w3HAQKWU", "date":"2017-mm-dd", "tags":"JS,game" }, { "name":"Sprite Animation", "note":"Animate moving objects to make your game more interesting.", "path":"animation", "page":"animation", "vlog":"5GxoVaO58NM", "date":"2017-mm-dd", "tags":"js,animation" }, { "name":"Load Image", "note":"Load an image.", "path":"load-image", "page":"load-image", "vlog":"cbBXjqaCzbs", "date":"2017-mm-dd", "tags":"js,loading" }, { "name":"Tile Types", "note":"There are multiple videos for this example. This example covers the various types of tiles one might use in a 2D platformer.", "path":"tile-types", "page":"tile-types", "vlog":"zRO6Q8VcHNE", "date":"2017-mm-dd", "tags":"js,tile,slope" }, { "name":"Snake", "note":"The classic snake game.", "path":"snake", "page":"snake", "vlog":"pxBW6FZglrI", "date":"2017-mm-dd", "tags":"js,game" }, { "name":"Top Down Tiles", "note":"Top down tile based collision detection and response.", "path":"top-down-tiles", "page":"top-down-tiles", "vlog":"nPVWL6RlJPE", "date":"2017-mm-dd", "tags":"js,tile,collision" }, { "name":"Hit The Wall", "note":"2D platformer tile based collision detection and response.", "path":"hit-the-wall", "page":"hit-the-wall", "vlog":"r-Y-N4cLd10", "date":"2017-mm-dd", "tags":"JS" }, { "name":"Walk On Tiles", "note":"Interact with the tile grid using the player and your keyboard.", "path":"walk-on-tiles", "page":"walk-on-tiles", "vlog":"GN2Eh7UauwU", "date":"2017-mm-dd", "tags":"JS" }, { "name":"Indexed DB", "note":"An example that shows how to use Indexed DB.", "path":"indexed-db", "page":"index", "vlog":"3y_x3Nkc8Dw", "date":"2017-mm-dd", "tags":"JS" }, { "name":"Offline Web App", "note":"Store the files your app needs in the browser so you can run your app offline.", "path":"offline-web-app", "page":"web-app", "vlog":"11kCO6TxD0E", "date":"2017-mm-dd", "tags":"JS" }, { "name":"Web App", "note":"Create a web app that can run in full screen mode on any device.", "path":"web-app", "page":"web-app", "vlog":"-xFC_oqF_Bw", "date":"2017-mm-dd", "tags":"JS" }, { "name":"HTTPS Server", "note":"Make an HTTPS server in Node JS.", "path":"https-server", "page":"index", "vlog":"xvMGNGaapG0", "date":"2017-mm-dd", "tags":"JS" }, { "name":"Touch Controller", "note":"Make a responsive touch controller for your mobile game.", "path":"touch-controller", "page":"touch-controller", "vlog":"3M_aNkaFkiw", "date":"2017-mm-dd", "tags":"JS" }, { "name":"Tile Grid", "note":"Learn about tile map indexing and how to find your place in the map.", "path":"tile-grid", "page":"tile-grid", "vlog":"c32wtgMeT0U", "date":"2017-mm-dd", "tags":"JS" }, { "name":"Tile World", "note":"Tile World", "path":"tile-world", "page":"tile-world", "vlog":"WMTv6QsAp0I", "date":"2017-mm-dd", "tags":"js,tile" }, { "name":"Square Collision Response", "note":"Square Collision", "path":"square-collision-response", "page":"response", "vlog":"", "date":"2017-mm-dd", "tags":"JS,collision" }, { "name":"Collision", "note":"Collision", "path":"collision", "page":"collision", "vlog":"", "date":"2017-mm-dd", "tags":"JS,collision" }, { "name":"Control", "note":"Control", "path":"control", "page":"control", "vlog":"8uIt9a2XBrw", "date":"2017-mm-dd", "tags":"JS,controller" }, { "name":"Animation Game Loop", "note":"Animation", "path":"animation-game-loop", "page":"animation", "vlog":"fiVcbX-X_tc", "date":"2017-mm-dd", "tags":"JS" }, { "name":"Canvas", "note":"How to use HTML5 canvas", "path":"canvas", "page":"canvas", "vlog":"N3MQCi-R6KY", "date":"2017-mm-dd", "tags":"HTML" }, { "name":"Elements", "note":"Working with HTML elements.", "path":"elements", "page":"elements", "vlog":"hNYE7ReLibg", "date":"2017-mm-dd", "tags":"HTML,JS" }, { "name":"Multiple-Inheritance", "note":"Shows one way to approach multiple inheritance. I later discovered that this approach copies all parent properties, which is different from true inheritance. True inheritance uses the same properties rather than cloning them. I have still found this to be a useful technique that achieves a similar effect.", "path":"multiple-inheritance", "page":"multiple-inheritance", "vlog":"y2XSsC1UsAY", "date":"2017-mm-dd", "tags":"inheritance,JS" }, { "name":"Prototype Inheritance", "note":"Shows prototype inheritance in JS", "path":"prototype-inheritance", "page":"prototype-inheritance", "vlog":"Z3y3Y1fHcaE", "date":"2017-mm-dd", "tags":"inheritance,JS" }, { "name":"Inheritance", "note":"Shows basic class inheritance", "path":"inheritance", "page":"inheritance", "vlog":"de4Qc49lCUU", "date":"2017-mm-dd", "tags":"inheritance,JS" }, { "name":"Objects And Vars", "note":"Shows variable declaration in JavaScript", "path":"objects-and-vars", "page":"objects", "vlog":"cPM-kl9WsCg", "date":"2017-mm-dd", "tags":"JS" }, { "name":"Hello World", "note":"A simple hello world example", "path":"hello-world", "page":"hello", "vlog":"yv02WDcmxHk", "date":"2017-mm-dd", "tags":"JS" } ] ================================================ FILE: index.css ================================================ /* Frank Poth 2019-11-25 */ #main-filter-bar { display:flex; margin:8px 0px 0px 0px; padding:4px 0px 4px 0px; } #main-filter-query { align-content:center; align-items:center; border:2px solid var(--color-transparent); border-radius:8px; display:inline-flex; margin:0px 8px 0px 0px; min-width:50%; padding:32px 8px; } #main-filter-query:empty { caret-color:var(--color-transparent); } #main-filter-query:empty:before { color:var(--color-medium-gray); content:attr(data-placeholder); } #main-filter-query:focus { border-color:var(--color-signature-yellow); } ================================================ FILE: index.html ================================================ Poth On Programming
Filter
================================================ FILE: index.js ================================================ (() => { const filter_query = document.getElementById("main-filter-query"); const project_container = document.getElementById("main-project-container"); const projects_container = document.getElementById("main-projects-container"); const logs_container = document.getElementById("main-logs-container"); const projects = []; const logs = []; function clickOrTouchStart(event) { if (event.target.id == "main-filter-button") { event.preventDefault(); filter(filter_query.innerText); } } function emptyContainer(container) { while(container.firstChild) container.removeChild(container.firstChild); } function fillContainer(container, elements) { while(elements.length != 0) container.appendChild(elements.shift()); } function filter(query) { query = query.replace(/[\n\r\v]/g, ''); // filter newline and carriage return var elements = []; for (var index = 1; index < projects.length; index ++) { var project = projects[index]; var regexp = new RegExp(query, 'i'); if (regexp.test(project.data.name) || regexp.test(project.data.tags) || regexp.test(project.data.note) || regexp.test(project.data.date)) elements.push(project.element); } emptyContainer(projects_container); fillContainer(projects_container, elements); } function keyDown(event) { if (event.keyCode == 13) { event.preventDefault(); filter(filter_query.innerText); } } // Open all links in new tab (() => { const links = document.querySelectorAll('a'); for (var index = links.length - 1; index > -1; -- index) { var link = links[index]; if (link.target === '') link.setAttribute('target', '_blank'); } })(); fetch("data/logs.json").then(response => { return response.json(); }).then(data => { for (var index = 0; index < data.length; index ++) logs[index] = new Log(data[index]); logs_container.appendChild(logs[0].element); }); fetch("data/projects.json").then(response => { return response.json(); }).then(data => { for (var index = 0; index < data.length; index ++) projects[index] = new Project(data[index]); project_container.appendChild(projects[0].element); filter(""); }); window.addEventListener("touchstart", clickOrTouchStart); window.addEventListener("click", clickOrTouchStart); window.addEventListener("keydown", keyDown); })(); ================================================ FILE: library/dom-kit.js ================================================ // Frank Poth 08/18/2017 const DOMKit = function() {}; DOMKit.createElement = function(tag_name, attributes, content) { var element = document.createElement(tag_name); if (attributes) DOMKit.setAttributes(element, attributes); if (content) element.innerHTML = content; return element; }; DOMKit.parseHTMLString = function(string) { var fragment = document.createDocumentFragment(); fragment.innerHTML = string; alert(fragment.children[0]); return fragment.children[0]; }; // created 03/27/2018 DOMKit.replaceCurrentScript = function(element) { var script = document.currentScript; script.parentNode.replaceChild(element, script); return element; }; DOMKit.replaceElement = function(element, new_element) { element.parentNode.replaceChild(new_element, element); return new_element; }; DOMKit.setAttributes = function(element, attributes) { var attribute_pair; for (index = attributes.length - 1; index > -1; -- index) { attribute_pair = attributes[index].split(/=(.+)?/, 2); element.setAttribute(attribute_pair[0], attribute_pair[1]); } }; ================================================ FILE: log.css ================================================ /* Frank Poth 2019-12-29 */ .log { display:block; max-width:720px; padding:8px; } .log-date { background-color:var(--color-light-gray); display:inline-block; padding:0px 4px; text-align:right; user-select:none; } .log-name { display:inline; font-weight:800; font-size:1.25em; text-align:left; user-select:none; } .log-name:before, .log-name:after { content:" "; } .log-note { display:inline; padding:4px 0px; text-align:justify; } ================================================ FILE: project.css ================================================ /* Frank Poth 2019-12-29 */ .project { display:block; margin:8px; max-width:720px; } .project-link { background-color:var(--color-light-blue); color:var(--color-signature-gray); cursor:pointer; display:inline-block; margin:0px 0px 4px; padding:4px 8px; } .project-link:hover { background-color:var(--color-white); } .project-links { text-align:left; } .project-name { color:var(--color-signature-gray); display:inline-block; font-size:1.25em; font-weight:800; padding:4px 8px; text-align:left; user-select:none; width:100%; } .project-note { color:var(--color-signature-gray); display:inline-block; padding:0px 4px 4px 8px; text-align:left; user-select:none; width:100%; } ================================================ FILE: robots.txt ================================================ User-agent: * Disallow: Crawl-delay: 15 ================================================ FILE: server.js ================================================ // Frank Poth 10/16/2017 (function() { const ip = process.env.MY_IP || "127.0.0.1"; const port = process.env.MY_PORT || "3000"; var fs, https, mimetypes, options, path, server; fs = require("fs"); // file system http = require("http"); // creates an http server path = require("path"); // used for working with url paths // used to create response headers /* If the user requests a .css file, we want to ensure we attach "text/css" to our response header, this way the browser knows how to handle it. */ mimetypes = { "css":"text/css", "html":"text/html", "ico":"image/ico", "jpg":"image/jpeg", "js":"text/javascript", "json":"application/json", "png":"image/png", "txt":"text/plain" }; server = http.createServer((request, response) => { console.log(request.url); if (request.url == "" || request.url == "/") { // The user is requesting the home page of the website, so give it to them request.url = "index.html"; } request.url = request.url.split("?")[0]; fs.readFile(__dirname + "/" + request.url, function(error, content) { if (error) { // if there is an error reading the requested url console.log("Error: " + error); // output it to the console } else { // else, there is no error, write the file contents to the page let extension = path.extname(request.url).split(".")[1]; if (!mimetypes[extension]) extension = "txt"; // 200 is code for OK, and the second parameter is our content header response.writeHead(200, {'Content-Type':mimetypes[extension]}); response.write(content); // write that content to our response object } response.end(); // This will send our response object to the browser }); }); server.listen(port, ip, function() { console.log("Server started at " + ip + ":" + port + "!"); }); })(); ================================================ FILE: theme.css ================================================ /* Frank Poth 2019-12-29 */ :root { --color-dark-yellow:#c09000; --color-dark-blue:#202840; --color-light-blue:#d0d8f0; --color-light-gray:#e0e8f0; --color-lighter-blue:#e0e8ff; --color-lighter-gray:#f0f8ff; --color-medium-gray:#606870; --color-signature-gray:#202830; --color-signature-yellow:#f0c000; --color-transparent:rgba(0,0,0,0); --color-white:#ffffff; --header-height:64px; } * { box-sizing:border-box; color:var(--color-signature-gray); font-family:Consolas, monospace; margin:0; overflow:hidden; padding:0; text-decoration:none; } *:focus { outline:none; } body, html { height:100%; width:100%; } body { background-color:var(--color-white); overflow:auto; } footer { align-content:space-around; align-items:space-around; background-color:var(--color-signature-gray); border-top:solid 1px var(--color-light-gray); color:#ffffff; display:grid; flex-direction:column; height:auto; justify-content:center; justify-items:center; min-height:100%; overflow-wrap: break-word; padding:32px 0; width:100%; word-break:break-word; word-wrap: break-word; } footer a { color:var(--color-signature-yellow); } footer p { color:var(--color-light-gray); max-width:800px; padding:4px; } header { align-content:center; align-items:center; background-color:var(--color-signature-gray); border-bottom:solid 1px var(--color-light-gray); display:flex; flex-wrap:wrap; justify-content:space-around; justify-items:space-between; min-height:var(--header-height); padding:8px 4px 8px 4px; } main { display:inline-block; min-height:calc(100% - var(--header-height)); width:100%; } .banner { background-color:var(--color-signature-gray); color:var(--color-signature-yellow); display:block; font-size:1.5em; padding:8px 0px 8px 8px; text-align:left; user-select:none; width:100%; } .donate-button { align-items:center; background-color:var(--color-signature-yellow); border:none; border-radius:8px; box-shadow:0px 4px var(--color-dark-yellow); color:var(--color-signature-gray); cursor:pointer; display:grid; justify-items:center; margin:0 0 4px 0; max-width:312px; padding:8px 32px; text-align:center; } .donate-button:focus, .donate-button:focus-visible { outline-color:rgba(0, 0, 0, 0); outline-style:none; outline-width:0px; } .donate-button:hover { background-color:var(--color-signature-yellow); box-shadow:0px 0px rgba(0, 0, 0, 0.5); margin:4px 0 0 0; overflow:visible; position:relative; } .donate-button:hover::before { background-color:rgba(0, 0, 0, 0); content:""; height:100%; left:0px; position:absolute; top:-4px; width:100%; } .header-link { color:var(--color-light-gray); cursor:pointer; padding:12px 8px 8px 8px; } .header-link:hover { color:var(--color-white); } .list-container { align-items:flex-start; display:flex; flex-direction:column; } .logo-image { display:inline-block; height:var(--header-height); image-rendering:pixelated; margin:0px 12px 0px 0px; width:auto; } .main-button { align-items:center; background-color:var(--color-light-blue); color:var(--color-signature-gray); cursor:pointer; display:inline-flex; padding:8px; user-select:none; } .main-button:hover { background-color:var(--color-white); } #header-links { display:flex; flex-wrap:wrap; justify-content:left; justify-self:stretch; } #header-logo { align-content:center; align-items:center; display:inline-flex; } #header-logo-text { color:var(--color-signature-yellow); display:inline; font-size:2.0em; font-weight:800; text-align:left; word-wrap: wrap break-word; } ================================================ FILE: tools/log.js ================================================ const Log = function(data) { this.data = data; this.element = document.createRange().createContextualFragment('
' + data.date + '' + data.name + '

' + data.note + '

').children[0]; }; ================================================ FILE: tools/project.js ================================================ const Project = function(data) { var vlog = data.vlog ? 'video' : ''; this.data = data; this.element = document.createRange().createContextualFragment('
' + data.name + '

' + data.note + '

').children[0]; };