diff --git a/index.html b/index.html index f22181e..d789c45 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,7 @@ position: absolute; top: 0; left: 0; + background-color: #6096ff; } diff --git a/js/Key.js b/js/Key.js new file mode 100644 index 0000000..9821e99 --- /dev/null +++ b/js/Key.js @@ -0,0 +1,31 @@ +export default class Key +{ + constructor(name) + { + this.name = name; + this.pressed = false; + + window.addEventListener( + 'keydown', + (event) => { + if (event.code === this.name) { + this.pressed = true; + } + } + ); + + window.addEventListener( + 'keyup', + (event) => { + if (event.code === this.name) { + this.pressed = false; + } + } + ); + } + + isPressed() + { + return this.pressed; + } +} diff --git a/js/MediaImage.js b/js/MediaImage.js new file mode 100644 index 0000000..b7dd2da --- /dev/null +++ b/js/MediaImage.js @@ -0,0 +1,14 @@ +import InterfaceEvent from "./events/InterfaceEvent.js"; +import ImageLoadedEvent from "./events/ImageLoadedEvent"; + +export default class MediaImage +{ + constructor(filename) + { + this.image = new Image(); + this.image.src = filename; + this.image.onload = function () { + window.dispatchEvent(new ImageLoadedEvent(filename)); + } + } +} \ No newline at end of file diff --git a/js/MediaImageCollection.js b/js/MediaImageCollection.js new file mode 100644 index 0000000..792e326 --- /dev/null +++ b/js/MediaImageCollection.js @@ -0,0 +1,28 @@ +import InterfaceEvent from "./events/InterfaceEvent.js"; +import ImageLoadedEvent from "./events/ImageLoadedEvent"; + +export default class MediaImageCollection +{ + constructor() + { + this.mediaImages = []; + this.numberImagesLoaded = 0; + + window.addEventListener( + ImageLoadedEvent.NAME, + () => { + this.numberImagesLoaded++; + + if (this.numberImagesLoaded === this.mediaImages.length) { + window.dispatchEvent(new InterfaceEvent.MEDIA_COLLECTION_LOADED); + } + } + ) + } + + addMediaImage(mediaImage) + { + this.mediaImages.push(mediaImage); + + } +} \ No newline at end of file diff --git a/js/Movable.js b/js/Movable.js new file mode 100644 index 0000000..558da18 --- /dev/null +++ b/js/Movable.js @@ -0,0 +1,41 @@ +import GeometryPoint from "./geometry/GeometryPoint.js"; + +export default class Movable +{ + constructor(defaultAnimation, speed = 1) + { + this.currentAnimation = 'DEFAULT'; + this.animations = { + DEFAULT: defaultAnimation, + }; + this.position = new GeometryPoint(); + this.speed = speed; + } + + playAnimation(animation, timestamp) + { + this.currentAnimation = animation; + this.animations[animation].play(timestamp); + } + + addAnimation(name, animation) + { + this.animations[name] = animation; + } + + moveLeft(delta = 1) + { + this.position.x -= this.speed * delta; + } + + moveRight(delta = 1) + { + this.position.x += this.speed * delta; + } + + draw(context) + { + this.animations[this.currentAnimation].setFootPosition(this.position.x, this.position.y); + this.animations[this.currentAnimation].draw(context); + } +} \ No newline at end of file diff --git a/js/MrCroc.js b/js/MrCroc.js new file mode 100644 index 0000000..341fd81 --- /dev/null +++ b/js/MrCroc.js @@ -0,0 +1,24 @@ +import Movable from "./Movable.js"; +import RetroAnimation from "./retro/RetroAnimation.js"; + +export default class MrCroc extends Movable +{ + constructor() { + super(new RetroAnimation('graphics/mr-croc-walk-right.png', 2, 5), 5); + + this.addAnimation('WALK_RIGHT', new RetroAnimation('graphics/mr-croc-walk-right.png', 2, 5, 10)); + this.addAnimation('WALK_LEFT', new RetroAnimation('graphics/mr-croc-walk-left.png', 2, 5, 10)); + } + + moveRight(timestamp, delta = 1) + { + this.playAnimation('WALK_RIGHT', timestamp); + super.moveRight(delta); + } + + moveLeft(timestamp, delta = 1) + { + this.playAnimation('WALK_LEFT', timestamp); + super.moveLeft(delta); + } +} \ No newline at end of file diff --git a/js/events/AudioLoadedEvent.js b/js/events/AudioLoadedEvent.js new file mode 100644 index 0000000..8696007 --- /dev/null +++ b/js/events/AudioLoadedEvent.js @@ -0,0 +1,11 @@ +export default class AudioLoadedEvent extends CustomEvent +{ + constructor(filename) { + super('audioloaded', {detail: filename}); + } + + getFilename() + { + this.detail; + } +} \ No newline at end of file diff --git a/js/events/BaseEvent.js b/js/events/BaseEvent.js new file mode 100644 index 0000000..b4ed64c --- /dev/null +++ b/js/events/BaseEvent.js @@ -0,0 +1,7 @@ +export default class BaseEvent extends CustomEvent +{ + constructor(name, detail) { + super(name, {detail: filename}); + this.NAME = name; + } +} \ No newline at end of file diff --git a/js/events/ImageCollectionLoadedEvent.js b/js/events/ImageCollectionLoadedEvent.js new file mode 100644 index 0000000..e69de29 diff --git a/js/events/ImageLoadedEvent.js b/js/events/ImageLoadedEvent.js new file mode 100644 index 0000000..60d1096 --- /dev/null +++ b/js/events/ImageLoadedEvent.js @@ -0,0 +1,13 @@ +import BaseEvent from "./BaseEvent.js"; + +export default class ImageLoadedEvent extends BaseEvent +{ + constructor(filename) { + super('imgloaded', filename); + } + + getFilename() + { + this.detail; + } +} \ No newline at end of file diff --git a/js/events/InterfaceEvent.js b/js/events/InterfaceEvent.js new file mode 100644 index 0000000..cbe1d37 --- /dev/null +++ b/js/events/InterfaceEvent.js @@ -0,0 +1,6 @@ +export default InterfaceEvent; + +const InterfaceEvent = { + IMAGE_LOADED: 'imgloaded', + MEDIA_COLLECTION_LOADED: 'mediacollectionloaded', +}; \ No newline at end of file diff --git a/js/geometry/GeometryPoint.js b/js/geometry/GeometryPoint.js index bcbb722..5ede9b8 100644 --- a/js/geometry/GeometryPoint.js +++ b/js/geometry/GeometryPoint.js @@ -1,6 +1,6 @@ export default class GeometryPoint { - constructor(x, y) + constructor(x = 0, y = 0) { this.x = x; this.y = y; diff --git a/js/geometry/GeometryRect.js b/js/geometry/GeometryRect.js index 7f73865..b987f23 100644 --- a/js/geometry/GeometryRect.js +++ b/js/geometry/GeometryRect.js @@ -1,6 +1,7 @@ import GeometryPoint from "./GeometryPoint.js"; import GeometryStroke from "./GeometryStroke.js"; import GeometryPointCollection from "./GeometryPointCollection.js"; +import GeometryLine from "./GeometryLine.js"; export default class GeometryRect { @@ -31,6 +32,11 @@ export default class GeometryRect return containsHorizontally && containsVertically; } + hasIntersectionWithRect(rect) + { + return this.getBorderIntersectonsWithRect(rect).getLength() > 0; + } + getBorderTop() { return new GeometryStroke( @@ -92,6 +98,12 @@ export default class GeometryRect return intersections; } + /** + * Returns the intersection points between the border lines and the given stroke. + * + * @param stroke + * @returns {GeometryPointCollection} + */ getBorderIntersectionsWithStroke(stroke) { let borders = [ @@ -172,6 +184,12 @@ export default class GeometryRect */ getRectIntersection(rect) { + let distanceToOrigin = this.getCenter().getDistanceToPoint(rect.getCenter()); + + if (distanceToOrigin > this.getDiagonal() * 0.5 + rect.getDiagonal() * 0.5) { + return null; + } + let intersectionPoints = new GeometryPointCollection(); this.getRectanglePoints().forEach( @@ -211,6 +229,22 @@ export default class GeometryRect return null; } + getCenter() + { + return new GeometryPoint( + this.position.x + this.width * 0.5, + this.position.y + this.height * 0.5 + ); + } + + getDiagonal() + { + return new GeometryStroke( + this.position, + new GeometryPoint(this.position.x + this.width, this.position.y + this.height) + ); + } + /** * Visualizes the rect. * diff --git a/js/geometry/GeometryStroke.js b/js/geometry/GeometryStroke.js index 3316715..57e1fde 100644 --- a/js/geometry/GeometryStroke.js +++ b/js/geometry/GeometryStroke.js @@ -21,6 +21,11 @@ export default class GeometryStroke extends GeometryLine return new GeometryLine(this.pointA, this.pointB); } + getLength() + { + return this.pointA.getDistanceToPoint(this.pointB); + } + getIntersectionWithStroke(stroke) { let intersection = super.getIntersectionWithLine(stroke); diff --git a/js/module.js b/js/module.js index be35390..6643323 100644 --- a/js/module.js +++ b/js/module.js @@ -1,17 +1,117 @@ import RetroSprite from "./retro/RetroSprite.js"; +import Key from "./Key.js"; +import MrCroc from "./MrCroc.js"; + +const MEDIA_READY_EVENT = 'mediaready'; +const IMAGE_READY_EVENT = 'imgready'; + +class ImageLoader +{ + images = []; + numberImagesLoaded = 0; + + update() + { + this.numberImagesLoaded++; + + if (this.numberImagesLoaded === this.images.length) { + window.dispatchEvent(new Event('imagesloaded')); + } + } + + getCurrentProgress() + { + return this.numberImagesLoaded / this.images.length; + } + + addImage(image) + { + image.addEventListener( + 'load', () => { + this.update();1 + } + ); + + this.images.push(image); + } +} + +function MainLoop(timestamp) +{ + if (lastRendered === undefined && lastTimestamp === undefined) { + lastRendered = timestamp; + lastTimestamp = timestamp; + } + + let delta = (timestamp - lastTimestamp) / (10 / GAME_SPEED); + + if (KeyLeft.isPressed()) { + mrCroc.moveLeft(timestamp, delta); + } else if (KeyRight.isPressed()) { + mrCroc.moveRight(timestamp, delta); + } + + if (lastRendered >= FRAME_DURATION) { + context.clearRect(0, 0, window.innerWidth, window.innerHeight); + + ground.draw(context); + mrCroc.draw(context); + lastRendered = timestamp; + } + + lastTimestamp = timestamp; + + window.requestAnimationFrame(MainLoop); +} + +const FPS = 60; +const FRAME_DURATION = 1000 / FPS; +const GAME_SPEED = 1; + +let lastRendered = undefined; +let lastTimestamp = undefined; +let context; +let ground, mrCroc; + +let KeyLeft = new Key('ArrowLeft'); +let KeyRight = new Key('ArrowRight'); + +let loader = new ImageLoader(); let image = new Image(); -image.src = 'graphics/mario.png'; +image.src = 'graphics/mr-croc-stand.png'; +loader.addImage(image); -image.onload = function () { - let sprite = new RetroSprite(image, 10); - let canvas = document.getElementById('canvas'); - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; +let imgAnimation = new Image(); +imgAnimation.src = 'graphics/mr-croc-walk-right.png'; +loader.addImage(imgAnimation); - let context = canvas.getContext('2d'); +let imgAnimationB = new Image(); +imgAnimationB.src = 'graphics/mr-croc-walk-left.png'; +loader.addImage(imgAnimationB); - console.log(context); +let imgBackground = new Image(); +imgBackground.src = 'graphics/ground.jpg'; +loader.addImage(imgBackground); - sprite.draw(context); -}; \ No newline at end of file +window.addEventListener( + 'imagesloaded', + () => { + console.log('Loaded'); + ground = new RetroSprite('graphics/ground.jpg', 4); + ground.position.y = window.innerHeight - ground.getRect().height; + + let canvas = document.getElementById('canvas'); + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + + context = canvas.getContext('2d'); + + mrCroc = new MrCroc(); + mrCroc.position.x = 100; + mrCroc.position.y = window.innerHeight - ground.getHeight(); + ground.draw(context); + + window.requestAnimationFrame(MainLoop); + } +); diff --git a/js/retro/RetroAnimation.js b/js/retro/RetroAnimation.js new file mode 100644 index 0000000..5ecadd7 --- /dev/null +++ b/js/retro/RetroAnimation.js @@ -0,0 +1,60 @@ +import RetroSprite from "./RetroSprite.js"; + +export default class RetroAnimation extends RetroSprite +{ + constructor(image, frames, scale = 1, fps = 6) + { + super(image, scale); + this.frames = frames; + this.fps = fps; + this.frameDuration = 1000 / this.fps; + this.currentFrame = 0; + this.lastUpdate = undefined; + this.frameWidth = this.canvas.width / this.frames; + this.isPlaying = false; + } + + getWidth() { + return this.frameWidth; + } + + setFootPosition(x, y) { + this.position.x = x - this.frameWidth * 0.5; + this.position.y = y - this.canvas.height; + } + + play(timestamp) + { + this.isPlaying = true; + + if (this.lastUpdate === undefined) { + this.lastUpdate = timestamp; + } + + if (timestamp - this.lastUpdate >= this.frameDuration) { + this.currentFrame = (this.currentFrame + 1) % this.frames; + this.lastUpdate = timestamp; + } + } + + draw(context) + { + if (!this.isPlaying) { + this.currentFrame = 0; + } + + context.drawImage( + this.canvas, + this.frameWidth * this.currentFrame, + 0, + this.frameWidth, + this.canvas.height, + this.position.x, + this.position.y, + this.frameWidth, + this.canvas.height + ); + + this.isPlaying = false; + } +} \ No newline at end of file diff --git a/js/retro/RetroSprite.js b/js/retro/RetroSprite.js index f953ec3..0553290 100644 --- a/js/retro/RetroSprite.js +++ b/js/retro/RetroSprite.js @@ -1,10 +1,12 @@ import GeometryPoint from "../geometry/GeometryPoint.js"; +import GeometryRect from "../geometry/GeometryRect.js"; export default class RetroSprite { constructor(image, scale = 1) { - this.image = image; + this.image = new Image(); + this.image.src = image; this.canvas = document.createElement('canvas'); this.context = this.canvas.getContext('2d'); this.position = new GeometryPoint(); @@ -13,6 +15,37 @@ export default class RetroSprite this.render(); } + /** + * Returns the position of the sprite's center + * + * @returns {GeometryPoint} + */ + getCenter() + { + this.getRect().getCenter(); + } + + setFootPosition(x, y) + { + this.position.x = x - this.canvas.width * 0.5; + this.position.y = y - this.canvas.height; + } + + getRect() + { + return new GeometryRect(this.position.x, this.position.y, this.canvas.width, this.canvas.height); + } + + getWidth() + { + return this.canvas.width; + } + + getHeight() + { + return this.canvas.height; + } + draw(context) { context.drawImage(this.canvas, this.position.x, this.position.y); @@ -20,7 +53,7 @@ export default class RetroSprite hasRectCollisionWith(sprite) { - + return this.getRect().getRectIntersection(sprite.getRect()) !== null; } hasCollisionWith(sprite) @@ -31,6 +64,9 @@ export default class RetroSprite render() { let canvasTemp = document.createElement('canvas'); + canvasTemp.width = this.image.width * this.scale; + canvasTemp.height = this.image.height * this.scale; + let contextTemp = canvasTemp.getContext('2d'); contextTemp.drawImage(this.image, 0, 0);