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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
|
module src/renderers/js
import src/renderer
import bytearray
// js ffi
extern type Node
extern io def getElementById(id: String): Node = jsWeb "document.getElementById(${id})"
extern io def addListener(event: String, node: Node) {handler: () => Unit}: Unit = jsWeb "${node}.addEventListener(${event}, () => $effekt.runToplevel((ks) => ${box handler}(ks)))"
// Custom logging function that logs to the console and the page
extern io def log(msg: String): Unit = jsWeb "log(${msg});"
extern io def loadRom(): ByteArray = jsWeb "rom ? rom : new Uint8Array(0)"
extern io def requestAnimationFrame {callback: () => Unit}: Unit = jsWeb "requestAnimationFrame(() => $effekt.runToplevel((ks) => ${box callback}(ks)))"
extern jsWeb """
let rom = undefined;
let keyStates = new Map(); // Change to track multiple keys
let lastKeyUpdate = performance.now();
// Global Log Function
function log(msg) {
const message = new Date().toLocaleTimeString() + ' - ' + msg + ' \n'
console.log(message);
}
function romCheck() {
document.getElementById('rom').addEventListener('change', async (event) => {
rom = await event.target.files[0].bytes();
document.getElementById('start').removeAttribute('disabled');
});
}
function keyRegister() {
// Track key state changes
document.addEventListener('keydown', (event) => {
event.preventDefault();
const key = event.key.toLowerCase();
keyStates.set(key, true);
lastKeyUpdate = performance.now();
});
document.addEventListener('keyup', (event) => {
event.preventDefault();
const key = event.key.toLowerCase();
keyStates.delete(key);
lastKeyUpdate = performance.now();
});
// Prevent key repeats
document.addEventListener('keypress', (event) => {
event.preventDefault();
});
}
"""
val pageContent = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Effekt8</title>
</head>
<body>
<h1>Effekt8: Chip8 Simulator written in Effekt Language</h1>
<label for="rom">Select a Chip8 ROM:</label>
<input type="file" id="rom" accept=".ch8" />
<button id="start" disabled>Start</button>
<hr />
<canvas id="canvas" width="640" height="320" style="border: 10px solid black;"></canvas>
<hr />
<div>
<h3>Logs:</h3>
<pre id="logs"></pre>
</div>
</body>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
canvas {
display: block;
margin: 0 auto;
}
</style>
</html>
"""
// JS Renderer has 640x320 pixels, so we need to scale the screen by 10x.
val width = 640
val height = 320
val scale = 10
extern io def renderPage(content: String): Unit = jsWeb "document.write(${content}); romCheck(); keyRegister();"
// Initialize the screen
def init(run: (ByteArray) => Unit at {io, global}) = {
renderPage(pageContent)
val startButton = getElementById("start")
addListener("click", startButton) {eventHandler {run}}
}
def eventHandler {onClick: (ByteArray) => Unit}: Unit = {
log("Start button clicked! Loading ROM...")
val rom: ByteArray = loadRom()
if (rom.size() != 0) {
log("ROM with size: " ++ show(rom.size()) ++ " loaded!")
onClick(rom)
} else {
log("No ROM loaded! Please select a ROM file.")
}
}
// Clear the screen (set black canvas)
def clear(): Unit = fill("black")
// Draw at (x, y) on the screen
extern io def draw(x: Int, y: Int, color: String): Unit = jsWeb """
(() => {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = ${color};
ctx.fillRect(10 * ${x}, 10 * ${y}, 10, 10);
})();
"""
extern io def fill(color: String): Unit = jsWeb """
(() => {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = ${color};
ctx.fillRect(0, 0, canvas.width, canvas.height);
})();
"""
extern io def get(x: Int, y: Int): Bool = jsWeb """
(() => {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const pixel = ctx.getImageData(10 * ${x}, 10 * ${y}, 1, 1).data;
return pixel[0] === 255;
})();
"""
extern io def update(f: () => Unit at {io, global}): Unit = jsWeb """
setInterval(() => {
$effekt.runToplevel((ks) => ${f}(ks));
}, 1000 / 240);
"""
extern io def getKeyPressed(): String = jsWeb """
(() => {
// Return first pressed key or "P" if no keys pressed
for (const [key, pressed] of keyStates.entries()) {
if (pressed) return key;
}
return "P";
})();
"""
namespace JSRenderer {
// make JS Renderer
def makeRenderer: Renderer = new Renderer {
def init(run) = init(run)
def clear() = clear()
def draw(x: Int, y: Int, color: String) = draw(x, y, color)
def update(f: () => Unit at {io, global}) = update(f)
def log(msg: String) = log(msg)
def fill(color: String) = fill(color)
def get(x: Int, y: Int) = get(x, y)
def getKeyPressed() = {
val key = getKeyPressed()
undefinedToOption(key)
}
}
}
|