================================================
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.
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.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
================================================
Hitboxfreeze
================================================
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!
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.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 = "
";
/* 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