mirror of
https://github.com/mineracks/seedhammer-v1-companion.git
synced 2026-06-26 20:51:06 +10:00
#4 v1 emulator scaffolding — boots in browser
Phase 2 kickoff. The cmd/emulator binary now builds to WebAssembly,
loads a 240×240 LCD-equivalent canvas in the browser, and accepts
8-button input from either on-screen buttons or mapped keyboard
keys. The actual v1 gui/ package isn't lifted yet — this is the
scaffolding that proves the platform/v1.Platform interface contract
and the build/serve pipeline so the GUI lift can land cleanly later.
platform/v1/platform.go (interface)
Button enum: Up Down Left Right Center Button1 Button2 Button3
Event{Button, Pressed}
Platform interface: Events() <-chan Event, Display(image.Image)
The interface is intentionally minimal — engrave/font/QR access
happens via direct package imports, not through Platform.
Future additions (camera, persistent storage) widen the
interface, not the data model.
cmd/emulator/main.go (//go:build js && wasm)
browserPlatform implements Platform against the browser:
- Display() copies the RGBA buffer into a JS Uint8ClampedArray
and calls back into emulatorPaint(pixels, w, h)
- Events() returns a 32-slot buffered channel that
exportPushEvent feeds from JS
Boot screen draws a stub frame (orange border, centre cross-hair
tick) so the canvas isn't blank during the GUI-less stage.
Future: replace drawBootScreen with gui.Loop() once gui/ is
lifted.
cmd/emulator/emulator_host.go (//go:build !js || !wasm)
Host placeholder so `go build ./...` succeeds on non-WASM
targets. Emits a helpful "build with GOOS=js GOARCH=wasm".
web/emulator/{index.html, app.css, app.js, build.sh}
Static PWA shell. Lays out an LCD frame (canvas, 320×320
rendered with image-rendering:pixelated for crisp 240×240
upscale), an on-screen joystick grid, three side-key buttons,
and a keyboard-shortcut reference table. Keyboard mapping
matches docs/architecture/v1-buttons-and-ui.md proposal.
Reuses ../composer/{wasm_exec.js, app.css} — DRY base styling,
no duplication.
The static server now needs to run one directory level higher
(`-d ./web` instead of `-d ./web/composer`) so both /composer/
and /emulator/ are reachable.
Build with: ./web/emulator/build.sh
Serve with: python3 -m http.server -d ./web 38080
Open: http://localhost:38080/emulator/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0203be2e8f
commit
ded32c8f07
@ -1,24 +0,0 @@
|
||||
// Command emulator runs the SeedHammer v1 controller firmware in a browser.
|
||||
//
|
||||
// Compiles to WebAssembly via GOOS=js GOARCH=wasm. The HTML/CSS shell that
|
||||
// hosts this WASM lives under web/emulator/.
|
||||
//
|
||||
// The WASM hosts the real v1 gui/, input/, and engrave/ packages, with
|
||||
// hardware-side drivers (driver/wshat for buttons, driver/drm for the LCD,
|
||||
// driver/libcamera for the camera, driver/mjolnir for the engraver)
|
||||
// replaced by browser-side mocks:
|
||||
//
|
||||
// - Buttons → keyboard events (arrows / Enter / 1, 2, 3)
|
||||
// - LCD → HTML5 canvas
|
||||
// - Camera → mock that reads QRs from sibling panes on the same page
|
||||
// - Engraver → null sink (preview mode) or visual playback animation
|
||||
//
|
||||
// Status: STUB — implementation will lift upstream v1.3.0 gui/ and input/
|
||||
// packages, with browser-side platform adapters in platform/v1/.
|
||||
//
|
||||
// Modelled architecturally on Gangleri42's cmd/wasmemu at:
|
||||
// https://github.com/Gangleri42/seedhammer/tree/seedhammer-features/cmd/wasmemu
|
||||
// (pinned at 0a3c63efb125d17d8ec86ce739ecd058c8747cfe), but his wasmemu
|
||||
// emulates SH-II's touch UI — v1 is button-driven, so we rebuild from scratch
|
||||
// while keeping his PWA shell pattern.
|
||||
package main
|
||||
16
cmd/emulator/emulator_host.go
Normal file
16
cmd/emulator/emulator_host.go
Normal file
@ -0,0 +1,16 @@
|
||||
//go:build !js || !wasm
|
||||
|
||||
// Host-build placeholder for cmd/emulator. The emulator is WASM-only.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Fprintln(os.Stderr, "emulator: this binary is WebAssembly-only.")
|
||||
fmt.Fprintln(os.Stderr, "Build it with:")
|
||||
fmt.Fprintln(os.Stderr, " GOOS=js GOARCH=wasm go build -o ./web/emulator/emulator.wasm ./cmd/emulator")
|
||||
os.Exit(2)
|
||||
}
|
||||
@ -1,11 +1,190 @@
|
||||
//go:build js && wasm
|
||||
|
||||
// Command emulator is a browser-based SeedHammer v1 firmware runner.
|
||||
//
|
||||
// Phase 2 scaffolding stage. This binary boots a 240×240 canvas-backed
|
||||
// LCD mock and an 8-button input layer that maps keyboard events to the
|
||||
// v1's joystick + 3 keys. The real firmware GUI lift (upstream's gui/
|
||||
// package + its assets/layout/op/saver/text/widget subpackages) lands in
|
||||
// a follow-up commit; today this stage proves the build pipeline + the
|
||||
// platform/v1.Platform interface contract.
|
||||
//
|
||||
// Build:
|
||||
//
|
||||
// GOOS=js GOARCH=wasm go build -o ./web/emulator/emulator.wasm ./cmd/emulator
|
||||
//
|
||||
// The static shell (HTML/CSS/JS) lives under web/emulator/. JS exports
|
||||
// documented in web/emulator/app.js.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"strconv"
|
||||
"syscall/js"
|
||||
|
||||
"github.com/mineracks/seedhammer-v1-companion/platform/v1"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Fprintln(os.Stderr, "emulator: not yet implemented — see docs/architecture/v1-buttons-and-ui.md and cmd/emulator/doc.go")
|
||||
os.Exit(2)
|
||||
const emulatorVersion = "v0.1-phase2-stub"
|
||||
|
||||
// v1 hardware native LCD resolution.
|
||||
const (
|
||||
lcdWidth = 240
|
||||
lcdHeight = 240
|
||||
)
|
||||
|
||||
// browserPlatform implements v1.Platform against the JS host.
|
||||
//
|
||||
// Display() draws into an *image.RGBA buffer then ships it to JS via a
|
||||
// Uint8ClampedArray that the JS shell paints onto a <canvas>. Events()
|
||||
// returns a channel fed by exportPushEvent calls from JS.
|
||||
type browserPlatform struct {
|
||||
frame *image.RGBA
|
||||
events chan v1.Event
|
||||
}
|
||||
|
||||
func newBrowserPlatform() *browserPlatform {
|
||||
return &browserPlatform{
|
||||
frame: image.NewRGBA(image.Rect(0, 0, lcdWidth, lcdHeight)),
|
||||
events: make(chan v1.Event, 32),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *browserPlatform) Events() <-chan v1.Event { return p.events }
|
||||
|
||||
func (p *browserPlatform) Display(frame image.Image) {
|
||||
draw.Draw(p.frame, p.frame.Bounds(), frame, frame.Bounds().Min, draw.Src)
|
||||
// Convert RGBA buffer → JS Uint8ClampedArray and call back into JS.
|
||||
jsBuf := js.Global().Get("Uint8ClampedArray").New(len(p.frame.Pix))
|
||||
js.CopyBytesToJS(jsBuf, p.frame.Pix)
|
||||
js.Global().Call("emulatorPaint", jsBuf, lcdWidth, lcdHeight)
|
||||
}
|
||||
|
||||
// Push an event from JS into the events channel.
|
||||
func (p *browserPlatform) push(button v1.Button, pressed bool) {
|
||||
select {
|
||||
case p.events <- v1.Event{Button: button, Pressed: pressed}:
|
||||
default:
|
||||
// Drop on full — shouldn't happen with the modest buffer the
|
||||
// firmware needs, but better than blocking the JS thread.
|
||||
}
|
||||
}
|
||||
|
||||
var plat *browserPlatform
|
||||
|
||||
func main() {
|
||||
plat = newBrowserPlatform()
|
||||
|
||||
js.Global().Set("emulatorVersion", js.FuncOf(exportVersion))
|
||||
js.Global().Set("emulatorPushEvent", js.FuncOf(exportPushEvent))
|
||||
js.Global().Set("emulatorBootScreen", js.FuncOf(exportBootScreen))
|
||||
js.Global().Set("emulatorLCDSize", js.ValueOf(map[string]any{
|
||||
"w": lcdWidth,
|
||||
"h": lcdHeight,
|
||||
}))
|
||||
|
||||
// Show the placeholder boot screen so the canvas isn't blank on load.
|
||||
drawBootScreen(plat.frame)
|
||||
plat.Display(plat.frame)
|
||||
|
||||
// Future: wire plat to gui.Loop or whatever the lifted GUI exposes.
|
||||
// For now, just consume events so the channel doesn't fill up.
|
||||
go func() {
|
||||
for ev := range plat.events {
|
||||
// Echo to console for debugging; the real GUI will replace this.
|
||||
js.Global().Get("console").Call("log",
|
||||
fmt.Sprintf("emu: %s %s",
|
||||
buttonName(ev.Button),
|
||||
boolStr(ev.Pressed, "press", "release"),
|
||||
),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
select {} // keep the runtime alive
|
||||
}
|
||||
|
||||
func exportVersion(this js.Value, args []js.Value) any {
|
||||
return emulatorVersion
|
||||
}
|
||||
|
||||
// exportPushEvent: emulatorPushEvent(buttonId:number, pressed:boolean)
|
||||
func exportPushEvent(this js.Value, args []js.Value) any {
|
||||
if len(args) != 2 {
|
||||
return nil
|
||||
}
|
||||
id := args[0].Int()
|
||||
pressed := args[1].Bool()
|
||||
if id < 0 || id > int(v1.Button3) {
|
||||
return nil
|
||||
}
|
||||
plat.push(v1.Button(id), pressed)
|
||||
return nil
|
||||
}
|
||||
|
||||
// exportBootScreen redraws the boot placeholder, useful for re-testing
|
||||
// after manual mucking.
|
||||
func exportBootScreen(this js.Value, args []js.Value) any {
|
||||
drawBootScreen(plat.frame)
|
||||
plat.Display(plat.frame)
|
||||
return nil
|
||||
}
|
||||
|
||||
// drawBootScreen fills the frame with a minimal welcome image so users see
|
||||
// SOMETHING the moment the WASM finishes loading. Future: this is replaced
|
||||
// by gui.Loop()'s first frame.
|
||||
func drawBootScreen(dst *image.RGBA) {
|
||||
// Background.
|
||||
draw.Draw(dst, dst.Bounds(), &image.Uniform{color.RGBA{0x12, 0x12, 0x12, 0xff}}, image.Point{}, draw.Src)
|
||||
|
||||
// Frame border to make the LCD area unambiguous.
|
||||
border := color.RGBA{0xff, 0x88, 0x00, 0xff} // accent orange
|
||||
for x := 0; x < lcdWidth; x++ {
|
||||
dst.SetRGBA(x, 0, border)
|
||||
dst.SetRGBA(x, lcdHeight-1, border)
|
||||
}
|
||||
for y := 0; y < lcdHeight; y++ {
|
||||
dst.SetRGBA(0, y, border)
|
||||
dst.SetRGBA(lcdWidth-1, y, border)
|
||||
}
|
||||
|
||||
// Eight tick marks around the perimeter to show the button positions.
|
||||
// Just a visual cue that this is real hardware-resolution rendering.
|
||||
tick := color.RGBA{0xaa, 0xaa, 0xaa, 0xff}
|
||||
for i := 0; i < 16; i++ {
|
||||
dst.SetRGBA(lcdWidth/2-1+i-8, lcdHeight/2, tick)
|
||||
dst.SetRGBA(lcdWidth/2, lcdHeight/2-1+i-8, tick)
|
||||
}
|
||||
}
|
||||
|
||||
func buttonName(b v1.Button) string {
|
||||
switch b {
|
||||
case v1.ButtonUp:
|
||||
return "Up"
|
||||
case v1.ButtonDown:
|
||||
return "Down"
|
||||
case v1.ButtonLeft:
|
||||
return "Left"
|
||||
case v1.ButtonRight:
|
||||
return "Right"
|
||||
case v1.ButtonCenter:
|
||||
return "Center"
|
||||
case v1.Button1:
|
||||
return "Button1"
|
||||
case v1.Button2:
|
||||
return "Button2"
|
||||
case v1.Button3:
|
||||
return "Button3"
|
||||
}
|
||||
return "btn-" + strconv.Itoa(int(b))
|
||||
}
|
||||
|
||||
func boolStr(b bool, t, f string) string {
|
||||
if b {
|
||||
return t
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
@ -19,8 +19,6 @@ package v1
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"github.com/mineracks/seedhammer-v1-companion/font/constant"
|
||||
)
|
||||
|
||||
// Button identifies one of the eight physical inputs on the v1 hardware.
|
||||
@ -55,15 +53,4 @@ type Platform interface {
|
||||
// Display writes a frame to the LCD-equivalent. The image is the
|
||||
// full screen at the platform's native resolution (240×240 for v1).
|
||||
Display(frame image.Image)
|
||||
|
||||
// EngraveFont returns the vector engraving face. Both real and
|
||||
// emulator backends return the same data — it's bundled with the
|
||||
// firmware.
|
||||
EngraveFont() *constant.Face
|
||||
}
|
||||
|
||||
// constant.Face is the type alias we use in the public surface, re-exported
|
||||
// here so callers don't have to import font/vector directly.
|
||||
// (Today this is just *vector.Face under the hood; the alias lets us swap
|
||||
// implementations without rippling change through the GUI.)
|
||||
type Face = constant.Face
|
||||
|
||||
104
web/emulator/app.css
Normal file
104
web/emulator/app.css
Normal file
@ -0,0 +1,104 @@
|
||||
/* SeedHammer v1 emulator-specific styles, layered on top of the
|
||||
composer's app.css for the shared shell/card/glass primitives. */
|
||||
|
||||
.emu-device-card { padding: 24px; }
|
||||
|
||||
.emu-device {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.emu-lcd-frame {
|
||||
background: #0a0a0a;
|
||||
border-radius: 18px;
|
||||
padding: 18px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35), inset 0 0 0 2px #2a2a2a;
|
||||
}
|
||||
#lcd {
|
||||
display: block;
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
image-rendering: pixelated;
|
||||
background: #000;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.emu-controls {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
gap: 28px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.emu-joystick {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 56px);
|
||||
grid-template-rows: repeat(3, 56px);
|
||||
gap: 4px;
|
||||
}
|
||||
.emu-joystick .jb.up { grid-column: 2; grid-row: 1; }
|
||||
.emu-joystick .jb.left { grid-column: 1; grid-row: 2; }
|
||||
.emu-joystick .jb.center { grid-column: 2; grid-row: 2; }
|
||||
.emu-joystick .jb.right { grid-column: 3; grid-row: 2; }
|
||||
.emu-joystick .jb.down { grid-column: 2; grid-row: 3; }
|
||||
|
||||
.emu-keys {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.emu-btn {
|
||||
background: var(--surface-strong);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 12px;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
user-select: none;
|
||||
transition: transform 0.05s, background 0.1s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.emu-btn:active,
|
||||
.emu-btn.pressed {
|
||||
background: var(--accent);
|
||||
color: var(--accent-text);
|
||||
transform: scale(0.94);
|
||||
box-shadow: 0 2px 8px rgb(255 136 0 / 0.4);
|
||||
}
|
||||
|
||||
.emu-btn.key {
|
||||
width: 64px;
|
||||
height: 56px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.emu-keymap {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
.emu-keymap td {
|
||||
padding: 6px 12px;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
vertical-align: top;
|
||||
}
|
||||
.emu-keymap td:first-child { width: 14em; }
|
||||
.emu-keymap td:nth-child(2) { width: 8em; color: var(--text-dim); font-variant-numeric: tabular-nums; }
|
||||
.emu-keymap tr:last-child td { border-bottom: none; }
|
||||
.emu-keymap kbd {
|
||||
background: var(--surface-strong);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
}
|
||||
136
web/emulator/app.js
Normal file
136
web/emulator/app.js
Normal file
@ -0,0 +1,136 @@
|
||||
// SeedHammer v1 emulator — DOM shell + WASM bridge.
|
||||
//
|
||||
// Go exports (built from cmd/emulator):
|
||||
//
|
||||
// emulatorVersion() -> string
|
||||
// emulatorLCDSize -> {w, h}
|
||||
// emulatorPushEvent(buttonId, pressed) -> void
|
||||
// emulatorBootScreen() -> void (debug helper)
|
||||
//
|
||||
// The Go runtime calls back into JS via:
|
||||
// globalThis.emulatorPaint(pixels: Uint8ClampedArray, w: number, h: number)
|
||||
// every time the firmware redraws the LCD.
|
||||
|
||||
const els = {
|
||||
status: document.getElementById("status"),
|
||||
lcd: document.getElementById("lcd"),
|
||||
buttons: document.querySelectorAll(".emu-btn"),
|
||||
};
|
||||
|
||||
const ctx = els.lcd.getContext("2d", { alpha: false });
|
||||
|
||||
// Button enum mirrors platform/v1.Button.
|
||||
const BTN = {
|
||||
Up: 0, Down: 1, Left: 2, Right: 3, Center: 4,
|
||||
Button1: 5, Button2: 6, Button3: 7,
|
||||
};
|
||||
|
||||
const KEYMAP = {
|
||||
ArrowUp: BTN.Up,
|
||||
ArrowDown: BTN.Down,
|
||||
ArrowLeft: BTN.Left,
|
||||
ArrowRight: BTN.Right,
|
||||
Enter: BTN.Center,
|
||||
" ": BTN.Center, // spacebar
|
||||
"1": BTN.Button1,
|
||||
"2": BTN.Button2,
|
||||
"3": BTN.Button3,
|
||||
};
|
||||
|
||||
let wasmReady = false;
|
||||
|
||||
function setStatus(text, error = false) {
|
||||
els.status.textContent = text;
|
||||
els.status.classList.toggle("error", error);
|
||||
}
|
||||
|
||||
function pushEvent(id, pressed) {
|
||||
if (!wasmReady) return;
|
||||
globalThis.emulatorPushEvent(id, pressed);
|
||||
}
|
||||
|
||||
// emulatorPaint: called from Go after every Display(). pixels is an
|
||||
// RGBA byte buffer w*h*4 bytes long, row-major, top-left origin.
|
||||
globalThis.emulatorPaint = function (pixels, w, h) {
|
||||
const img = new ImageData(pixels, w, h);
|
||||
// Resize canvas if Go reports a different LCD resolution.
|
||||
if (els.lcd.width !== w || els.lcd.height !== h) {
|
||||
els.lcd.width = w;
|
||||
els.lcd.height = h;
|
||||
}
|
||||
ctx.putImageData(img, 0, 0);
|
||||
};
|
||||
|
||||
// Wire up the on-screen buttons. Pointerdown/up gives us press+release
|
||||
// semantics that match the underlying GPIO event model.
|
||||
for (const b of els.buttons) {
|
||||
const id = Number(b.dataset.btn);
|
||||
b.addEventListener("pointerdown", (e) => {
|
||||
e.preventDefault();
|
||||
b.classList.add("pressed");
|
||||
pushEvent(id, true);
|
||||
});
|
||||
const release = (e) => {
|
||||
e?.preventDefault();
|
||||
if (!b.classList.contains("pressed")) return;
|
||||
b.classList.remove("pressed");
|
||||
pushEvent(id, false);
|
||||
};
|
||||
b.addEventListener("pointerup", release);
|
||||
b.addEventListener("pointerleave", release);
|
||||
b.addEventListener("pointercancel", release);
|
||||
// Stop the button itself from grabbing keyboard focus on click.
|
||||
b.addEventListener("mousedown", (e) => e.preventDefault());
|
||||
}
|
||||
|
||||
// Track currently-pressed keys so a held key doesn't repeatedly fire
|
||||
// press events on autorepeat.
|
||||
const heldKeys = new Set();
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
const id = KEYMAP[e.key];
|
||||
if (id === undefined) return;
|
||||
if (heldKeys.has(e.key)) return; // autorepeat
|
||||
heldKeys.add(e.key);
|
||||
e.preventDefault();
|
||||
// Visual feedback on the on-screen button too.
|
||||
const dom = document.querySelector(`.emu-btn[data-btn="${id}"]`);
|
||||
if (dom) dom.classList.add("pressed");
|
||||
pushEvent(id, true);
|
||||
});
|
||||
|
||||
document.addEventListener("keyup", (e) => {
|
||||
const id = KEYMAP[e.key];
|
||||
if (id === undefined) return;
|
||||
heldKeys.delete(e.key);
|
||||
e.preventDefault();
|
||||
const dom = document.querySelector(`.emu-btn[data-btn="${id}"]`);
|
||||
if (dom) dom.classList.remove("pressed");
|
||||
pushEvent(id, false);
|
||||
});
|
||||
|
||||
async function loadWasm() {
|
||||
setStatus("Loading WASM…");
|
||||
const go = new Go();
|
||||
const resp = await fetch("./emulator.wasm");
|
||||
if (!resp.ok) {
|
||||
setStatus(`Failed to fetch emulator.wasm (${resp.status})`, true);
|
||||
return;
|
||||
}
|
||||
const result = await WebAssembly.instantiateStreaming(resp, go.importObject);
|
||||
go.run(result.instance);
|
||||
for (let i = 0; i < 100; i++) {
|
||||
if (typeof globalThis.emulatorVersion === "function") break;
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
}
|
||||
if (typeof globalThis.emulatorVersion !== "function") {
|
||||
setStatus("WASM loaded but exports never appeared", true);
|
||||
return;
|
||||
}
|
||||
wasmReady = true;
|
||||
setStatus(`Ready — ${globalThis.emulatorVersion()}`);
|
||||
}
|
||||
|
||||
loadWasm().catch((e) => {
|
||||
setStatus(`Boot failed: ${e?.message ?? e}`, true);
|
||||
});
|
||||
17
web/emulator/build.sh
Executable file
17
web/emulator/build.sh
Executable file
@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the SeedHammer v1 emulator WASM bundle.
|
||||
# wasm_exec.js is shared with the composer at ../composer/wasm_exec.js.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
REPO_ROOT=$(cd ../.. && pwd)
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
GOOS=js GOARCH=wasm go build -trimpath -ldflags="-s -w" \
|
||||
-o ./web/emulator/emulator.wasm \
|
||||
./cmd/emulator
|
||||
|
||||
size=$(stat -f%z ./web/emulator/emulator.wasm 2>/dev/null || stat -c%s ./web/emulator/emulator.wasm)
|
||||
echo "built: ./web/emulator/emulator.wasm (${size} bytes)"
|
||||
echo "serve: python3 -m http.server -d ./web 38080"
|
||||
echo " → open http://localhost:38080/emulator/"
|
||||
80
web/emulator/index.html
Normal file
80
web/emulator/index.html
Normal file
@ -0,0 +1,80 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#1a1a1a" media="(prefers-color-scheme: dark)">
|
||||
<meta name="theme-color" content="#f5f5f5" media="(prefers-color-scheme: light)">
|
||||
<title>SeedHammer v1 — emulator</title>
|
||||
<link rel="stylesheet" href="../composer/app.css">
|
||||
<link rel="stylesheet" href="./app.css">
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 36.6 36.6'%3E%3Crect width='36.6' height='36.6' rx='7' fill='%23ff8800'/%3E%3C/svg%3E">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>This app requires JavaScript and WebAssembly.</noscript>
|
||||
|
||||
<header class="bar glass">
|
||||
<h1>SeedHammer v1 Emulator</h1>
|
||||
<div class="bar-actions">
|
||||
<span id="status" class="status" aria-live="polite">Loading…</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="card glass emu-device-card" aria-label="Device">
|
||||
<div class="emu-device">
|
||||
<div class="emu-lcd-frame">
|
||||
<canvas id="lcd" width="240" height="240" aria-label="v1 LCD"></canvas>
|
||||
</div>
|
||||
<div class="emu-controls">
|
||||
<div class="emu-joystick" role="group" aria-label="Joystick">
|
||||
<button class="emu-btn jb up" data-btn="0" aria-label="Up">▲</button>
|
||||
<button class="emu-btn jb left" data-btn="2" aria-label="Left">◀</button>
|
||||
<button class="emu-btn jb center" data-btn="4" aria-label="Center">●</button>
|
||||
<button class="emu-btn jb right" data-btn="3" aria-label="Right">▶</button>
|
||||
<button class="emu-btn jb down" data-btn="1" aria-label="Down">▼</button>
|
||||
</div>
|
||||
<div class="emu-keys" role="group" aria-label="Side keys">
|
||||
<button class="emu-btn key" data-btn="5" aria-label="Button 1">K1</button>
|
||||
<button class="emu-btn key" data-btn="6" aria-label="Button 2">K2</button>
|
||||
<button class="emu-btn key" data-btn="7" aria-label="Button 3">K3</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card glass" aria-label="Keyboard mapping">
|
||||
<div class="editor-head">
|
||||
<h2>Keyboard shortcuts</h2>
|
||||
<small>Hardware buttons map 1:1 to keys.</small>
|
||||
</div>
|
||||
<table class="emu-keymap">
|
||||
<tbody>
|
||||
<tr><td><kbd>↑</kbd></td><td>Up</td><td>Joystick north</td></tr>
|
||||
<tr><td><kbd>↓</kbd></td><td>Down</td><td>Joystick south</td></tr>
|
||||
<tr><td><kbd>←</kbd></td><td>Left</td><td>Joystick west</td></tr>
|
||||
<tr><td><kbd>→</kbd></td><td>Right</td><td>Joystick east</td></tr>
|
||||
<tr><td><kbd>Enter</kbd> / <kbd>Space</kbd></td><td>Center</td><td>Joystick press / primary confirm</td></tr>
|
||||
<tr><td><kbd>1</kbd></td><td>Button 1</td><td>Back</td></tr>
|
||||
<tr><td><kbd>2</kbd></td><td>Button 2</td><td>Secondary action</td></tr>
|
||||
<tr><td><kbd>3</kbd></td><td>Button 3</td><td>Primary confirm</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<footer class="card glass footer-card">
|
||||
<p>
|
||||
Phase 2 scaffolding stage — the GUI itself isn't lifted yet, but the
|
||||
LCD canvas, button input layer, and platform.Platform contract are
|
||||
all in place. The boot screen below shows the 240×240 frame rendered
|
||||
by the Go runtime; press any mapped key (or click any button) and
|
||||
the Go side echoes the event to the browser console.
|
||||
See <a href="https://github.com/mineracks/seedhammer-v1-companion">the repo</a>.
|
||||
</p>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<script src="../composer/wasm_exec.js"></script>
|
||||
<script src="./app.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user