-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
157 lines (138 loc) · 4.05 KB
/
index.html
File metadata and controls
157 lines (138 loc) · 4.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
<html>
<head>
<script type="text/javascript" src="/node_modules/jsnes/dist/jsnes.min.js"></script>
<style>
html {
min-height: 100%;
}
body {
margin: 0;
padding: 0;
}
canvas {
image-rendering: pixelated;
background: #000;
object-fit: contain;
width: 100%;
height: 100%;
}
</style>
</head>
<body bgcolor="#CCC" onload="init()">
<script>
async function init() {
const canvas = createCanvas(256, 240)
const ctx = canvas.getContext('2d')
const onFrame = writeFrameToCanvas(ctx)
const onAudioSample = createAudioSampler()
const nes = new jsnes.NES({ onFrame, onAudioSample, onStatusUpdate })
nes.ppu.clipToTvSize = false
bindKeys(nes)
const game = await loadGame('./game.nes')
playGame(game, nes)
}
function createCanvas (width, height) {
const c = document.createElement('canvas')
c.width = width
c.height = height
document.body.appendChild(c)
return c
}
const BITMASK_RED = 0xFF
const BITMASK_GREEN = 0xFF00
const BITMASK_BLUE = 0xFF0000
const OFFSET_GREEN = 8
const OFFSET_BLUE = 16
function writeFrameToCanvas (context) {
return function writeFrameBuffer (frameBuffer) {
const arr = new Uint8ClampedArray(frameBuffer.length * 4)
for (let i = 0; i < frameBuffer.length; i++) {
arr[i * 4] = frameBuffer[i] & BITMASK_RED
arr[i * 4 + 1] = (frameBuffer[i] & BITMASK_GREEN) >>> OFFSET_GREEN
arr[i * 4 + 2] = (frameBuffer[i] & BITMASK_BLUE) >>> OFFSET_BLUE
arr[i * 4 + 3] = 0xFF
}
const d = new ImageData(arr, 256, 240)
context.putImageData(d, 0, 0)
}
}
function onStatusUpdate(...args) {
console.log(...args)
}
async function loadGame (filename) {
const response = await fetch(filename)
const romData = await response.blob()
return new Promise(done => {
const f = new FileReader()
f.onloadend = () => done(f.result)
f.readAsBinaryString(romData)
})
}
function playGame (rom, nes) {
nes.loadROM(rom)
function loop () {
nes.frame()
requestAnimationFrame(loop)
}
loop()
}
function whichToJsnesButton (which) {
switch(which) {
case 37:
return jsnes.Controller.BUTTON_LEFT;
case 39:
return jsnes.Controller.BUTTON_RIGHT;
case 38:
return jsnes.Controller.BUTTON_UP;
case 38:
return jsnes.Controller.BUTTON_DOWN;
case 65:
return jsnes.Controller.BUTTON_A;
case 83:
return jsnes.Controller.BUTTON_B;
}
}
function bindKey(event, method, target) {
document.addEventListener(event, function (e) {
e.preventDefault()
target[method](1, whichToJsnesButton(e.which))
});
}
function bindKeys(nes) {
bindKey('keydown', 'buttonDown', nes)
bindKey('keyup', 'buttonUp', nes)
}
function createAudioSampler () {
const audioCtx = new AudioContext()
const scriptNode = audioCtx.createScriptProcessor(1024, 0, 2)
const audioBuffers = {
left: [],
right: []
}
scriptNode.onaudioprocess = connectOutputBuffers(audioBuffers)
scriptNode.connect(audioCtx.destination)
return function onAudioSample(left, right) {
audioBuffers.left.push(left)
audioBuffers.right.push(right)
}
}
function connectOutputBuffers (buffers) {
return function onAudioFrame (e) {
const left = e.outputBuffer.getChannelData(0)
const right = e.outputBuffer.getChannelData(1)
if (buffers.left.length < left.length) {
for(let i = 0; i < left.length; i++) {
// Buffer underrun, fill with 0
left[i] = right[i] = 0
}
} else {
for(let i = 0; i < left.length; i++) {
left[i] = buffers.left.shift()
right[i] = buffers.right.shift()
}
}
}
}
</script>
</body>
</html>