Skip to content

Instantly share code, notes, and snippets.

@jm42
Created July 9, 2020 18:05
Show Gist options
  • Save jm42/5a02358c187e023a2414d1b0ab29e065 to your computer and use it in GitHub Desktop.
Save jm42/5a02358c187e023a2414d1b0ab29e065 to your computer and use it in GitHub Desktop.
canvas-library
class Sprite {
constructor(rect) {
this._visible = true
this.dirty = 1
this.rect = rect
}
get visible() {
return this._visible
}
set visible(value) {
this._visible = value ? true : false
if (this.dirty < 2)
this.dirty = 1
}
render(ctx, rect) {}
}
class Rect extends Sprite {
constructor(rect, color, border) {
super(rect)
this.color = color
this.border = border
}
render(ctx, rect) {
if (this.color) {
ctx.fillStyle = this.color
ctx.fillRect(this.rect.x, this.rect.y, this.rect.w, this.rect.h)
}
if (this.color) {
ctx.strokeStyle = this.border
ctx.strokeRect(this.rect.x, this.rect.y, this.rect.w, this.rect.h)
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>SAN: Artificial World</title>
<style>
* { margin: 0; padding: 0; }
</style>
<script src="src/logging.js" defer></script>
<script src="src/math.js" defer></script>
<script src="src/display.js" defer></script>
<script src="src/play.js" defer></script>
<script src="src/world.js" defer></script>
<script src="src/main.js" defer></script>
</head>
<body>
<canvas id="san" width="240" height="320"></canvas>
</body>
</html>
/** Simple in-memory logger that formats the message immediately. */
class Logger {
constructor() {
this.messages = []
}
log(level, message, context) {
let json = context ? JSON.stringify(context) : '{}'
let type = levelToString(level).toUpperCase()
this.messages.push(`[${type}] ${message} ${json}`)
}
}
const LOGGING_LEVELS = ['ERROR', 'WARNING', 'NOTICE', 'DEBUG']
// add constants/helpers to Logger class
LOGGING_LEVELS.forEach(function(name, level) {
Logger[name.toUpperCase()] = level
Logger.prototype[name.toLowerCase()] = function(message, context) {
this.log(level, message, context)
}
})
/** Returns given if correct or lookup in available by priority. */
function levelToString(level) {
// match by name
if (typeof level === 'string'
&& LOGGING_LEVELS.indexOf(level.toUpperCase()) !== -1)
return level
// match by level
if (typeof level === 'number' && level in LOGGING_LEVELS)
return LOGGING_LEVELS[level]
return 'UNKNOWN'
}
(function() {
let DEBUG = true
let logger = new Logger
let config = {
transparent: false,
}
function main() {
let director = new Director([
BackgroundColor,
TestScene,
])
director.push('BackgroundColor', {})
director.push('TestScene', {})
let canvas = document.getElementById('san')
let ctx = canvas.getContext('2d', { alpha: config.transparent })
let state = {}
requestAnimationFrame(function(timestamp) {
if (timestamp) {
if (!state.startstamp)
state.startstamp = timestamp
state.timestamp = timestamp
}
state = director.update(state)
ctx.save()
ctx.setTransform(1, 0, 0, 1, 0, 0)
ctx.globalAlpha = 1
director.render(ctx, {x:0, y:0, w:canvas.width, h:canvas.height})
ctx.restore()
})
}
function quit() {
if (DEBUG) {
let status
status = document.createElement('pre')
status.id = 'status'
logger.messages.forEach(function(msg) {
status.innerHTML += msg + "\n"
});
document.body.appendChild(status)
document.getElementById('san').style.display = 'none'
}
}
if (DEBUG) {
window.addEventListener('error', function(event) {
let filename = event.filename.split('/').pop()
logger.log(Logger.DEBUG, `${event.message} (${filename}:${event.lineno})`)
quit()
});
}
document.addEventListener('readystatechange', function(event) {
if (event.target.readyState === 'complete') {
main()
}
})
})()
/** Returns rect relative to parent. */
function fit(rect, parent) {
let x = rect.x + parent.x
let y = rect.y + parent.y
let w = x + rect.w > parent.w ? parent.w - x : rect.w
let h = y + rect.h > parent.h ? parent.h - y : rect.h
return {x, y, w, h}
}
/** Orchestrate stacked scenes to */
class Director {
constructor(scenes) {
this._scenes = {} // registered constructors
this._running = [] // scenes stack, current is index zero
this._queue = [] // to be pushed after scene is removed
this._resume = [] // to call resume on first update
scenes.forEach((scene) => this.register(scene))
}
get current() {
if (this._running.length > 0) {
return this._running[0]
}
return null
}
get active() {
return this._running.slice()
}
register(scene) {
this._scenes[scene.name] = scene
}
available(sceneName) {
return sceneName in this._scenes
}
queue(sceneName, state) {
this._queue.push([sceneName, state])
}
remove(scene, state, pause) {
let index = this._running.indexOf(scene)
if (index === -1)
throw new ReferenceError(`Scene ${scene.name} not running`)
if (index === 0 && this._queue.length > 0) {
let data = this._queue.shift()
this.replace(data[0], data[1])
return
}
if (pause)
scene.pause(state)
this._running.splice(index, 1)
scene.shutdown(state)
if (index === 0 && this._running.length > 0)
self.current.resume(state)
else if (this._running.length === 0)
quit()
}
pop(state) {
let previous = this.current
this.remove(previous, state)
return previous
}
push(sceneName, state) {
if (!this.available(sceneName))
throw new ReferenceError(`Scene ${sceneName} missing`)
let scene = this._scenes[sceneName]
let previous = this.current
if (previous)
previous.pause(state)
let instance = new scene(this)
this._running.unshift(instance)
instance.startup(state)
this._resume.push(instance)
return instance
}
replace(sceneName, state) {
let previous = this.current
let instance = this.push(sceneName, state)
if (previous)
this.remove(previous, state)
return instance
}
update(state) {
if (this.current === null)
quit()
else if (this.current in this._resume) {
let index = this._resume.indexOf(this.current)
this._resume.splice(index, 1)
this.current.resume(state)
}
return this.active.reduce((state, scene) => scene.update(state), state)
}
render(ctx, rect) {
this.active.reverse().forEach((scene) => scene.render(ctx, rect))
}
}
class Scene {
constructor(director) {
this._director = director
this._children = [] // active sprites
this._removed = [] // waiting to be clean
this.state = {}
}
get name() {
return this.constructor.name
}
add(sprite) {
if (sprite.dirty < 2)
sprite.dirty = 1
this._children.push(sprite)
}
remove(sprite) {
let index = this._children.indexOf(sprite)
if (index === -1)
throw new ReferenceError(`${sprite.constructor.name} not found`)
this._children.splice(index, 1)
this._removed.push(sprite)
}
has(sprite) {
return this._children.indexOf(sprite) !== -1
}
/** Called when added to the state stack. */
startup(state) {}
/** Called each time state is updated for first time. */
resume(state) {}
/** Called each frame while state is active. Returns modified state. */
update(state) {
return state
}
/** Called when there is a new input event. */
process(event, state) {}
/** Called when state is no longer active. */
pause(state) {}
/** Called before state is destroyed. */
shutdown(state) {}
/** Called each frame with context. Returns list of updated rects. */
render(ctx, rect) {
let updated = []
this._children.forEach((sprite) => {
if (sprite.dirty > 0) {
if (sprite.visible) {
let r = fit(sprite.rect, rect)
ctx.save()
sprite.render(ctx, r)
updated.push(r)
ctx.restore()
}
if (sprite.dirty === 1)
sprite.dirty = 0
}
})
this._removed.forEach((sprite) => updated.push(fit(sprite.rect, rect)))
this._removed = []
return updated
}
}
class BackgroundColor extends Scene {
startup(state) {
if (state.backgroundColor)
this.color = state.backgroundColor
else
this.color = 'white'
}
render(ctx, rect) {
ctx.fillStyle = this.color
ctx.fillRect(rect.x, rect.y, rect.w, rect.h)
}
}
class TestScene extends Scene {
startup(state) {
this.add(new Rect({x:16, y:16, w:16, h:16}, 'blue', 'red'))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment