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 audioContext = null;
let oscillator = null;
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);
document.getElementById('logs').textContent += 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 = """
Effekt8
"""
// 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";
})();
"""
extern io def stopBeep(): Unit = jsWeb """
(() => {
if (oscillator) {
oscillator.stop();
oscillator.disconnect();
oscillator = null;
}
})();
"""
extern io def beep(): Unit = jsWeb """
(() => {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
if (!oscillator) {
oscillator = audioContext.createOscillator();
oscillator.type = 'square';
oscillator.frequency.setValueAtTime(440, audioContext.currentTime); // 440Hz = A4 note
oscillator.connect(audioContext.destination);
oscillator.start();
}
})();
"""
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)
}
def beep() = beep()
def stopBeep() = stopBeep()
}
}