Skip to content

Instantly share code, notes, and snippets.

@stemcstudio
Last active July 7, 2020 03:36
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stemcstudio/3aa5e6f25c184b1f0d41ddf9491f9d83 to your computer and use it in GitHub Desktop.
Save stemcstudio/3aa5e6f25c184b1f0d41ddf9491f9d83 to your computer and use it in GitHub Desktop.
HTML5 Canvas Sprite Game

HTML5 Canvas Starter Project

Overview

This project is a template for creating new projects that use the HTML5 Canvas API.

Features

It has the following features:

  1. index.html and script.ts work together to create a CanvasRenderingContext2D.

  2. style.css provides CSS styling information for the HTML.

  3. Vector.ts provides a starting point for a 2D Vector class.

  4. Vector.spec.ts provides unit tests for the Vector class.

  5. tests.html and tests.ts work together to run the Jasmine unit testing framework.

  6. README.md provides this Markdown description of this project.

  7. package.json provides technical information for running this project and descriptive information for publishing to the STEMC arXiv.

Getting Started

  1. Using the Project Menu, copy this project to a new project and give your project its own title. You should also use the Labels and Tags dialog to set yourself as the author. Finally, it is a good idea to use the Settings dialog to give your project a name such as stemcstudio-my-awesome-project. The stemcstudio- prefix is merely a convention to identify your project as coming from the STEMCstudio community.

  2. A search for HTML5 Canvas on the internet will yield many resources providing documentation and examples.

  3. Expect to modify script.ts. You are encouraged to make your project modular by creating new files when appropriate, making them into ES6 modules using export statements, and importing them into other modules using the import statement.

<!doctype html>
<html>
<head>
<base href='/'>
<!-- STYLES-MARKER -->
<style>
/* STYLE-MARKER */
</style>
<!-- SHADERS-MARKER -->
<!-- SCRIPTS-MARKER -->
<!-- SYSTEM-SHIM-MARKER -->
</head>
<body>
<div id='container'></div>
<script>
// CODE-MARKER
</script>
<script>
System.defaultJSExtensions = true
System.import('./main')
</script>
</body>
</html>
export function trackKeys(codes: { [keyCode: number]: string }): { [decode: string]: boolean } {
const tracker: { [decode: string]: boolean } = {}
function handler(event: KeyboardEvent) {
// console.log(`${event.keyCode}`)
if (codes.hasOwnProperty(event.keyCode)) {
const down = event.type === 'keydown'
// const keyPressed = String.fromCharCode(event.keyCode)
tracker[codes[event.keyCode]] = down
}
}
window.addEventListener("keydown", handler)
window.addEventListener("keyup", handler)
return tracker
}
import { Model, Direction } from './model'
import { View } from './view'
import { trackKeys } from './keyboard'
const arrowCodes = { 37: "left", 38: "up", 39: "right", 40: "down" }
const keyDown = trackKeys(arrowCodes)
const model = new Model()
const view = new View(model, document.getElementById('container') as HTMLElement)
const update = function(elapsedTime: number) {
let moving = false
if (keyDown['right']) {
model.hero.direction = Direction.East
model.hero.pos.x += model.hero.speed * elapsedTime
moving = true
}
else if (keyDown['left']) {
model.hero.direction = Direction.West
model.hero.pos.x -= model.hero.speed * elapsedTime
moving = true
}
else if (keyDown['up']) {
model.hero.direction = Direction.North
model.hero.pos.y -= model.hero.speed * elapsedTime
moving = true
}
else if (keyDown['down']) {
model.hero.direction = Direction.South
model.hero.pos.y += model.hero.speed * elapsedTime
moving = true
}
model.hero.moving = moving
if (moving) {
model.hero.pose = (model.hero.pose + 1) % 2
}
else {
model.hero.pose = 1
}
}
// The main game loop.
const main = function() {
const now = Date.now()
const delta = now - then
update(delta / 1000)
view.draw()
then = now
// Request to do this again ASAP.
requestAnimationFrame(main)
}
window.onunload = () => {
console.log("Goodbye")
}
// Let's play this game!
let then = Date.now()
window.requestAnimationFrame(main)
import { Vector } from './vector'
export class Rectangle {
pos = new Vector()
width: number
height: number
fillStyle: string
constructor(width: number, height: number) {
this.width = width
this.height = height
}
}
export enum Direction {
East = 0,
North = 1,
West = 2,
South = 3
}
export class Figure {
direction: Direction
pos = new Vector()
moving = false
pose = 0
speed = 256
constructor() {
this.direction = Direction.South
}
}
export class Model {
hero = new Figure()
monster = new Figure()
monstersCaught = 0
constructor() {
}
}
{
"uuid": "ba223414-560d-4bac-b702-894c8e9a3788",
"description": "HTML5 Canvas Sprite Game",
"dependencies": {
"DomReady": "1.0.0",
"jasmine": "3.4.0"
},
"name": "html5-canvas-game",
"version": "0.1.0",
"keywords": [
"HTML5",
"Canvas",
"STEMCstudio",
"Game",
"Sprite"
],
"operatorOverloading": true,
"hideConfigFiles": true,
"linting": true,
"author": "David Geo Holmes"
}
body {
overflow: hidden;
}
<!DOCTYPE html>
<html>
<head>
<base href='/'>
<!-- STYLES-MARKER -->
<style>
/* STYLE-MARKER */
</style>
<!-- SYSTEM-SHIM-MARKER -->
<!-- SHADERS-MARKER -->
<!-- SCRIPTS-MARKER -->
</head>
<body>
<script>
// CODE-MARKER
</script>
<script>
System.defaultJSExtensions = true
System.import('./tests')
</script>
</body>
</html>
import { vectorSpec } from './vector.spec.js'
window['jasmine'] = jasmineRequire.core(jasmineRequire)
jasmineRequire.html(window['jasmine'])
const env = jasmine.getEnv()
const jasmineInterface = jasmineRequire.interface(window['jasmine'], env)
extend(window, jasmineInterface)
const htmlReporter = new jasmine.HtmlReporter({
env: env,
getContainer: function() { return document.body },
createElement: function() { return document.createElement.apply(document, arguments) },
createTextNode: function() { return document.createTextNode.apply(document, arguments) },
timer: new jasmine.Timer()
})
env.addReporter(htmlReporter)
/*
* Helper function for extending the properties on objects.
*/
export default function extend<T>(destination: T, source: any): T {
for (const property in source) {
if (source.hasOwnProperty(property)) {
destination[property] = source[property]
}
}
return destination
}
htmlReporter.initialize()
describe("Vector", vectorSpec)
env.execute()
{
"allowJs": true,
"checkJs": true,
"declaration": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"jsx": "react",
"module": "system",
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"preserveConstEnums": true,
"removeComments": false,
"skipLibCheck": true,
"sourceMap": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"target": "es5",
"traceResolution": true
}
{
"rules": {
"array-type": [
true,
"array"
],
"curly": false,
"comment-format": [
true,
"check-space"
],
"eofline": true,
"forin": true,
"jsdoc-format": true,
"new-parens": true,
"no-conditional-assignment": false,
"no-consecutive-blank-lines": true,
"no-construct": true,
"no-for-in-array": true,
"no-inferrable-types": [
true
],
"no-magic-numbers": false,
"no-shadowed-variable": true,
"no-string-throw": true,
"no-trailing-whitespace": [
true,
"ignore-jsdoc"
],
"no-var-keyword": true,
"one-variable-per-declaration": [
true,
"ignore-for-loop"
],
"prefer-const": true,
"prefer-for-of": true,
"prefer-function-over-method": false,
"prefer-method-signature": true,
"radix": true,
"semicolon": [
true,
"never"
],
"trailing-comma": [
true,
{
"multiline": "never",
"singleline": "never"
}
],
"triple-equals": true,
"use-isnan": true
}
}
import { Vector } from './vector'
export function vectorSpec() {
describe("constructor", function() {
const x = Math.random()
const y = Math.random()
const v = new Vector(x, y)
it("should preserve x coordinate", function() {
expect(v.x).toBe(x)
})
it("should preserve y coordinate", function() {
expect(v.y).toBe(y)
})
})
}
export class Vector {
x = 0
y = 0
constructor(x?: number, y?: number) {
if (typeof x === 'number') {
this.x = x
}
if (typeof y === 'number') {
this.y = y
}
}
}
import { Model, Direction, Figure } from './model'
/**
* Background image.
*/
const bgImage = new Image()
let bgReady = false
bgImage.onload = function() {
bgReady = true
}
bgImage.src = 'https://www.stemcstudio.com/img/games/background.png'
/**
* Hero image.
*/
const heroImage = new Image()
let heroReady = false
heroImage.onload = function() {
heroReady = true
}
heroImage.src = 'https://www.stemcstudio.com/img/games/sprites/girl.png'
/**
* Monster image.
*/
const monsterImage = new Image()
let monsterReady = false
monsterImage.onload = function() {
monsterReady = true
}
monsterImage.src = 'https://www.stemcstudio.com/img/games/sprites/girl.png'
export enum SpriteSide {
Back = 0,
Right = 1,
Front = 2,
Left = 3
}
export class View {
private canvas: HTMLCanvasElement
private context: CanvasRenderingContext2D
constructor(private model: Model, container: HTMLElement) {
this.canvas = document.createElement('canvas')
this.canvas.width = 512
this.canvas.height = 480
container.appendChild(this.canvas)
this.context = this.canvas.getContext('2d') as CanvasRenderingContext2D
}
/**
*
*/
draw(): void {
const context = this.context
context.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.drawBackground()
this.drawScore()
this.drawMonster()
this.drawHero()
}
private drawBackground() {
const context = this.context
if (heroReady) {
context.drawImage(bgImage, 0, 0)
}
}
private drawHero(): void {
this.drawFigure(this.model.hero, monsterImage, heroReady)
}
private drawMonster(): void {
this.drawFigure(this.model.monster, monsterImage, monsterReady)
}
private drawFigure(figure: Figure, image: HTMLImageElement, imageReady: boolean): void {
const context = this.context
let sX = 72
if (figure.moving) {
sX = 72 * figure.pose
}
let sY = 96 * SpriteSide.Right
switch (figure.direction) {
case Direction.East: {
sY = 96 * SpriteSide.Right
break
}
case Direction.North: {
sY = 96 * SpriteSide.Back
break
}
case Direction.West: {
sY = 96 * SpriteSide.Left
break
}
case Direction.South: {
sY = 96 * SpriteSide.Front
break
}
}
const sWidth = 72
const sHeight = 96
const dX = figure.pos.x
const dY = figure.pos.y
const dWidth = sWidth
const dHeight = sHeight
if (imageReady) {
context.drawImage(image, sX, sY, sWidth, sHeight, dX, dY, dWidth, dHeight)
}
}
private drawScore() {
const context = this.context
// Score
context.fillStyle = "rgb(250, 250, 250)"
context.font = "24px Helvetica"
context.textAlign = "left"
context.textBaseline = "top"
context.fillText(`Monsters caught: ${this.model.monstersCaught}`, 32, 32)
}
/**
*
*/
/*
private drawRectangle(rectangle: Rectangle) {
const cx = this.context
if (typeof rectangle.fillStyle === 'string') {
cx.fillStyle = rectangle.fillStyle
}
else {
cx.fillStyle = 'gray'
}
cx.fillRect(rectangle.pos.x, rectangle.pos.y, rectangle.width, rectangle.height)
}
*/
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment