Colin McMillen
5 years ago
2 changed files with 287 additions and 0 deletions
@ -0,0 +1,33 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="utf-8" /> |
||||
|
<title>snej</title> |
||||
|
<style> |
||||
|
* { padding: 0; margin: 0; } |
||||
|
body { background: black; color: white; font-family: monospace; } |
||||
|
div { text-align: center; } |
||||
|
canvas { |
||||
|
background: magenta; |
||||
|
display: block; |
||||
|
margin: 0 auto; |
||||
|
margin-top: 8px; |
||||
|
margin-bottom: 8px; |
||||
|
padding: 1px; |
||||
|
image-rendering: pixelated; |
||||
|
} |
||||
|
</style> |
||||
|
</head> |
||||
|
<body> |
||||
|
<!-- SNES resolution: 256x224. 4x scaled: 1024 x 896. --> |
||||
|
<canvas id="canvas" width="256" height="224"></canvas> |
||||
|
<div id="debug"></div> |
||||
|
<button id="1x">1x</button> |
||||
|
<button id="2x">2x</button> |
||||
|
<button id="3x">3x</button> |
||||
|
<button id="4x">4x</button> |
||||
|
<button id="5x">5x</button> |
||||
|
<script src="main.js"></script> |
||||
|
|
||||
|
</body> |
||||
|
</html> |
@ -0,0 +1,254 @@ |
|||||
|
class Input { |
||||
|
constructor() { |
||||
|
this.left = false; |
||||
|
this.right = false; |
||||
|
this.up = false; |
||||
|
this.down = false; |
||||
|
this.a = false; |
||||
|
this.b = false; |
||||
|
this.x = false; |
||||
|
this.y = false; |
||||
|
this.l = false; |
||||
|
this.r = false; |
||||
|
this.select = false; |
||||
|
this.start = false; |
||||
|
|
||||
|
window.addEventListener('gamepadconnected', this.gamepadConnected); |
||||
|
window.addEventListener('gamepaddisconnected', this.gamepadDisconnected); |
||||
|
} |
||||
|
|
||||
|
update() { |
||||
|
// TODO: have a config screen instead of hard-coding the 8Bitdo SNES30 pad.
|
||||
|
// TODO: handle connects / disconnects more correctly.
|
||||
|
const gamepad = navigator.getGamepads()[0]; |
||||
|
if (gamepad == null || !gamepad.connected || gamepad.axes.length < 2 || |
||||
|
gamepad.buttons.length < 12) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.left = gamepad.axes[0] < 0; |
||||
|
this.right = gamepad.axes[0] > 0; |
||||
|
this.up = gamepad.axes[1] < 0; |
||||
|
this.down = gamepad.axes[1] > 0; |
||||
|
this.a = gamepad.buttons[0].pressed; |
||||
|
this.b = gamepad.buttons[1].pressed; |
||||
|
this.x = gamepad.buttons[3].pressed; |
||||
|
this.y = gamepad.buttons[4].pressed; |
||||
|
this.l = gamepad.buttons[6].pressed; |
||||
|
this.r = gamepad.buttons[7].pressed; |
||||
|
this.select = gamepad.buttons[10].pressed; |
||||
|
this.start = gamepad.buttons[11].pressed; |
||||
|
debug(this.toString()); |
||||
|
} |
||||
|
|
||||
|
gamepadConnected(e) { |
||||
|
debug('gamepad connected! :)'); |
||||
|
console.log('gamepad connected @ index %d: %d buttons, %d axes\n[%s]', |
||||
|
e.gamepad.index, e.gamepad.buttons.length, e.gamepad.axes.length, |
||||
|
e.gamepad.id); |
||||
|
} |
||||
|
|
||||
|
gamepadDisconnected(e) { |
||||
|
debug('gamepad disconnected :('); |
||||
|
console.log('gamepad disconnected @ index %d:\n[%s]', e.gamepad.index, |
||||
|
e.gamepad.id); |
||||
|
} |
||||
|
|
||||
|
toString() { |
||||
|
let result = ''; |
||||
|
|
||||
|
if (this.up) { |
||||
|
result += '^'; |
||||
|
} else if (this.down) { |
||||
|
result += 'v'; |
||||
|
} else { |
||||
|
result += '-'; |
||||
|
} |
||||
|
|
||||
|
if (this.left) { |
||||
|
result += '<'; |
||||
|
} else if (this.right) { |
||||
|
result += '>'; |
||||
|
} else { |
||||
|
result += '-'; |
||||
|
} |
||||
|
|
||||
|
result += ' '; |
||||
|
|
||||
|
if (this.a) { |
||||
|
result += 'A'; |
||||
|
} |
||||
|
if (this.b) { |
||||
|
result += 'B'; |
||||
|
} |
||||
|
if (this.x) { |
||||
|
result += 'X'; |
||||
|
} |
||||
|
if (this.y) { |
||||
|
result += 'Y'; |
||||
|
} |
||||
|
if (this.l) { |
||||
|
result += 'L'; |
||||
|
} |
||||
|
if (this.r) { |
||||
|
result += 'R'; |
||||
|
} |
||||
|
if (this.select) { |
||||
|
result += 's'; |
||||
|
} |
||||
|
if (this.start) { |
||||
|
result += 'S'; |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const input = new Input(); |
||||
|
|
||||
|
class Graphics { |
||||
|
constructor(canvas) { |
||||
|
this.canvas_ = canvas; |
||||
|
this.ctx_ = canvas.getContext('2d'); |
||||
|
} |
||||
|
|
||||
|
get width() { |
||||
|
return this.canvas_.width; |
||||
|
} |
||||
|
|
||||
|
get height() { |
||||
|
return this.canvas_.height; |
||||
|
} |
||||
|
|
||||
|
fill(color) { |
||||
|
this.ctx_.fillStyle = color; |
||||
|
this.ctx_.beginPath(); |
||||
|
this.ctx_.rect(0, 0, this.canvas_.width, this.canvas_.height); |
||||
|
this.ctx_.fill(); |
||||
|
this.ctx_.closePath(); |
||||
|
} |
||||
|
|
||||
|
circle(x, y, radius, color) { |
||||
|
this.ctx_.fillStyle = color; |
||||
|
this.ctx_.beginPath(); |
||||
|
this.ctx_.arc(x, y, radius, 0, 2 * Math.PI) |
||||
|
this.ctx_.fill(); |
||||
|
this.ctx_.closePath(); |
||||
|
} |
||||
|
|
||||
|
// TODO: replace with custom sprite-based text rendering.
|
||||
|
text(string, x, y, size, color) { |
||||
|
this.ctx_.imageSmoothingEnabled = false; |
||||
|
this.ctx_.fillStyle = color; |
||||
|
this.ctx_.font = '' + size + 'px monospace'; |
||||
|
this.ctx_.fillText(string, x, y); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class FpsCounter { |
||||
|
constructor() { |
||||
|
this.fps = 0; |
||||
|
this.frameTimes_ = new Array(60); |
||||
|
this.idx_ = 0; |
||||
|
} |
||||
|
|
||||
|
update(timestampMs) { |
||||
|
if (this.frameTimes_[this.idx_]) { |
||||
|
const timeElapsed = (timestampMs - this.frameTimes_[this.idx_]) / 1000; |
||||
|
this.fps = this.frameTimes_.length / timeElapsed; |
||||
|
} |
||||
|
this.frameTimes_[this.idx_] = timestampMs; |
||||
|
this.idx_++; |
||||
|
if (this.idx_ == this.frameTimes_.length) { |
||||
|
this.idx_ = 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
draw(gfx) { |
||||
|
gfx.text('FPS: ' + Math.round(this.fps), 8, 16, 16, 'yellow'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class World { |
||||
|
constructor() { |
||||
|
this.state_ = null; |
||||
|
this.fpsCounter_ = new FpsCounter(); |
||||
|
this.input_ = new Input(); |
||||
|
this.gamepadRenderer_ = new GamepadRenderer(); |
||||
|
} |
||||
|
|
||||
|
update(timestampMs) { |
||||
|
this.fpsCounter_.update(timestampMs); |
||||
|
this.input_.update(); |
||||
|
} |
||||
|
|
||||
|
draw(gfx) { |
||||
|
this.gamepadRenderer_.draw(gfx, this.input_); |
||||
|
this.fpsCounter_.draw(gfx); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class GamepadRenderer { |
||||
|
draw(gfx, input) { |
||||
|
const centerX = gfx.width / 2; |
||||
|
const centerY = gfx.height / 2; |
||||
|
|
||||
|
gfx.fill('black'); |
||||
|
|
||||
|
// Select & Start
|
||||
|
gfx.circle(centerX + 12, centerY, 8, input.start ? 'cyan' : 'grey'); |
||||
|
gfx.circle(centerX - 12, centerY, 8, input.select ? 'cyan' : 'grey'); |
||||
|
|
||||
|
// Y X B A
|
||||
|
gfx.circle(centerX + 48, centerY, 8, input.y ? 'cyan' : 'grey'); |
||||
|
gfx.circle(centerX + 64, centerY - 16, 8, input.x ? 'cyan' : 'grey'); |
||||
|
gfx.circle(centerX + 64, centerY + 16, 8, input.b ? 'cyan' : 'grey'); |
||||
|
gfx.circle(centerX + 80, centerY, 8, input.a ? 'cyan' : 'grey'); |
||||
|
|
||||
|
// dpad
|
||||
|
gfx.circle(centerX - 48, centerY, 8, input.right ? 'cyan' : 'grey'); |
||||
|
gfx.circle(centerX - 64, centerY - 16, 8, input.up ? 'cyan' : 'grey'); |
||||
|
gfx.circle(centerX - 64, centerY + 16, 8, input.down ? 'cyan' : 'grey'); |
||||
|
gfx.circle(centerX - 80, centerY, 8, input.left ? 'cyan' : 'grey'); |
||||
|
|
||||
|
// L & R
|
||||
|
gfx.circle(centerX + 30, centerY - 32, 8, input.r ? 'cyan' : 'grey'); |
||||
|
gfx.circle(centerX - 30, centerY - 32, 8, input.l ? 'cyan' : 'grey'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function debug(message) { |
||||
|
const debugDiv = document.getElementById('debug'); |
||||
|
debugDiv.innerText = message; |
||||
|
} |
||||
|
|
||||
|
function loop(world, gfx) { |
||||
|
return timestampMs => { |
||||
|
world.update(timestampMs); |
||||
|
world.draw(gfx); |
||||
|
window.requestAnimationFrame(loop(world, gfx)); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
function setCanvasScale(scale) { |
||||
|
const snesWidth = 256; |
||||
|
const snesHeight = 224; |
||||
|
const canvas = document.getElementById('canvas'); |
||||
|
canvas.style.width = '' + snesWidth * scale + 'px'; |
||||
|
canvas.style.height = '' + snesHeight * scale + 'px'; |
||||
|
} |
||||
|
|
||||
|
function init() { |
||||
|
const world = new World(); |
||||
|
const gfx = new Graphics(document.getElementById('canvas')); |
||||
|
|
||||
|
document.getElementById('1x').onclick = () => setCanvasScale(1); |
||||
|
document.getElementById('2x').onclick = () => setCanvasScale(2); |
||||
|
document.getElementById('3x').onclick = () => setCanvasScale(3); |
||||
|
document.getElementById('4x').onclick = () => setCanvasScale(4); |
||||
|
document.getElementById('5x').onclick = () => setCanvasScale(5); |
||||
|
setCanvasScale(4); |
||||
|
window.requestAnimationFrame(loop(world, gfx)); |
||||
|
debug('initialized!'); |
||||
|
} |
||||
|
|
||||
|
init(); |
Write
Preview
Loading…
Cancel
Save
Reference in new issue