Engine refactors 1
This commit is contained in:
147
src/engine/datatypes.ts
Normal file
147
src/engine/datatypes.ts
Normal file
@ -0,0 +1,147 @@
|
||||
export interface IGame {
|
||||
update(): void;
|
||||
draw(): void;
|
||||
}
|
||||
|
||||
export class Color {
|
||||
readonly r: number;
|
||||
readonly g: number;
|
||||
readonly b: number;
|
||||
readonly a: number;
|
||||
|
||||
constructor(r: number, g: number, b: number, a?: number) {
|
||||
this.r = r;
|
||||
this.g = g;
|
||||
this.b = b;
|
||||
this.a = a ?? 255;
|
||||
}
|
||||
|
||||
static parseHexCode(hexCode: string) {
|
||||
const regex1 = /#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})?/;
|
||||
const regex2 = /#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})?/;
|
||||
let result = regex1.exec(hexCode) ?? regex2.exec(hexCode);
|
||||
if (result == null) {
|
||||
throw `could not parse color: ${hexCode}`
|
||||
}
|
||||
|
||||
let parseGroup = (s: string | undefined): number => {
|
||||
if (s === undefined) {
|
||||
return 255;
|
||||
}
|
||||
if (s.length == 1) {
|
||||
return 17 * parseInt(s, 16);
|
||||
}
|
||||
return parseInt(s, 16);
|
||||
}
|
||||
return new Color(
|
||||
parseGroup(result[1]),
|
||||
parseGroup(result[2]),
|
||||
parseGroup(result[3]),
|
||||
parseGroup(result[4]),
|
||||
);
|
||||
}
|
||||
|
||||
toStyle(): string {
|
||||
return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a / 255.0})`
|
||||
}
|
||||
}
|
||||
|
||||
export class Point {
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
|
||||
constructor(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
offset(other: Point | Size): Point {
|
||||
if (other instanceof Point) {
|
||||
return new Point(this.x + other.x, this.y + other.y);
|
||||
}
|
||||
|
||||
return new Point(this.x + other.w, this.y + other.h);
|
||||
}
|
||||
|
||||
negate() {
|
||||
return new Point(-this.x, -this.y);
|
||||
}
|
||||
}
|
||||
|
||||
export class Size {
|
||||
readonly w: number;
|
||||
readonly h: number;
|
||||
|
||||
constructor(w: number, h: number) {
|
||||
this.w = w;
|
||||
this.h = h;
|
||||
}
|
||||
}
|
||||
|
||||
export class Grid<T> {
|
||||
data: T[][];
|
||||
|
||||
constructor({size, cbDefault}: {size: Size, cbDefault: (xy: Point) => T}) {
|
||||
this.data = [];
|
||||
|
||||
for (let y = 0; y < size.h; y++) {
|
||||
let row = [];
|
||||
for (let x = 0; x < size.w; x++) {
|
||||
row.push(cbDefault(new Point(x, y)))
|
||||
}
|
||||
this.data.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
static createGridFromStringArray(ary: Array<string>): Grid<string> {
|
||||
let w = 0;
|
||||
let h = ary.length;
|
||||
for (let i = 0; i < h - 1; i++) {
|
||||
let w1 = ary[i].length;
|
||||
let w2 = ary[i + 1].length;
|
||||
if (w1 != w2) {
|
||||
throw `createGridFromStringArray: must be grid-shaped, got ${ary}`
|
||||
}
|
||||
w = w1;
|
||||
}
|
||||
|
||||
return new Grid({
|
||||
size: new Size(w, h),
|
||||
cbDefault: (xy) => {
|
||||
return ary[xy.y].charAt(xy.x);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static createGridFromJaggedArray<T>(ary: Array<Array<T>>): Grid<T> {
|
||||
let w = 0;
|
||||
let h = ary.length;
|
||||
for (let i = 0; i < h - 1; i++) {
|
||||
let w1 = ary[i].length;
|
||||
let w2 = ary[i + 1].length;
|
||||
if (w1 != w2) {
|
||||
throw `createGridFromJaggedArray: must be grid-shaped, got ${ary}`
|
||||
}
|
||||
w = w1;
|
||||
}
|
||||
|
||||
return new Grid({
|
||||
size: new Size(w, h),
|
||||
cbDefault: (xy) => {
|
||||
return ary[xy.y][xy.x];
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export enum AlignX {
|
||||
Left = 0,
|
||||
Center = 1,
|
||||
Right = 2
|
||||
}
|
||||
|
||||
export enum AlignY {
|
||||
Top = 0,
|
||||
Middle = 1,
|
||||
Right = 2,
|
||||
}
|
0
src/engine/internal/abstract.ts
Normal file
0
src/engine/internal/abstract.ts
Normal file
41
src/engine/internal/assets.ts
Normal file
41
src/engine/internal/assets.ts
Normal file
@ -0,0 +1,41 @@
|
||||
class Assets {
|
||||
#images: Record<string, HTMLImageElement>;
|
||||
|
||||
constructor() {
|
||||
this.#images = {};
|
||||
}
|
||||
|
||||
isLoaded(): boolean {
|
||||
// you could use this, if so inclined, to check if a certain
|
||||
// list of assets had been loaded prior to game start
|
||||
//
|
||||
// (to do so, you would call getImage() for each desired asset
|
||||
// and then wait for isLoaded to return true)
|
||||
for (let filename in this.#images) {
|
||||
if (!this.#images[filename].complete) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getImage(filename: string): HTMLImageElement {
|
||||
let element: HTMLImageElement;
|
||||
if (this.#images[filename]) {
|
||||
element = this.#images[filename];
|
||||
} else {
|
||||
element = document.createElement("img");
|
||||
element.src = filename;
|
||||
this.#images[filename] = element;
|
||||
}
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
let active: Assets = new Assets();
|
||||
|
||||
export function getAssets(): Assets {
|
||||
return active;
|
||||
}
|
||||
|
44
src/engine/internal/clock.ts
Normal file
44
src/engine/internal/clock.ts
Normal file
@ -0,0 +1,44 @@
|
||||
const MAX_UPDATES_BANKED: number = 20.0;
|
||||
|
||||
// always run physics at 240 hz
|
||||
const UPDATES_PER_MS: number = 1/(1000.0/240.0);
|
||||
|
||||
class Clock {
|
||||
#lastTimestamp: number | undefined;
|
||||
#updatesBanked: number
|
||||
|
||||
constructor() {
|
||||
this.#lastTimestamp = undefined;
|
||||
this.#updatesBanked = 0.0
|
||||
}
|
||||
|
||||
|
||||
recordTimestamp(timestamp: number) {
|
||||
if (this.#lastTimestamp) {
|
||||
let delta = timestamp - this.#lastTimestamp;
|
||||
this.#bankDelta(delta);
|
||||
}
|
||||
this.#lastTimestamp = timestamp;
|
||||
}
|
||||
|
||||
popUpdate() {
|
||||
// return true if we should do an update right now
|
||||
// and remove one draw from the bank
|
||||
if (this.#updatesBanked > 1) {
|
||||
this.#updatesBanked -= 1;
|
||||
return true
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#bankDelta(delta: number) {
|
||||
this.#updatesBanked = Math.min(delta * UPDATES_PER_MS, MAX_UPDATES_BANKED);
|
||||
}
|
||||
}
|
||||
|
||||
let active: Clock = new Clock();
|
||||
|
||||
export function getClock(): Clock {
|
||||
return active;
|
||||
}
|
||||
|
||||
|
88
src/engine/internal/drawing.ts
Normal file
88
src/engine/internal/drawing.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import {getScreen} from "./screen.ts";
|
||||
import {AlignX, AlignY, Color, Point, Size} from "../datatypes.ts";
|
||||
import {mainFont} from "./font.ts";
|
||||
import {Sprite} from "./sprite.ts";
|
||||
|
||||
class Drawing {
|
||||
camera: Point;
|
||||
|
||||
constructor() {
|
||||
this.camera = new Point(0, 0);
|
||||
}
|
||||
|
||||
get size() { return getScreen().size; }
|
||||
|
||||
invertRect(position: Point, size: Size) {
|
||||
position = this.camera.negate().offset(position);
|
||||
|
||||
let ctx = getScreen().unsafeMakeContext();
|
||||
ctx.globalCompositeOperation = "difference";
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.fillRect(
|
||||
Math.floor(position.x),
|
||||
Math.floor(position.y),
|
||||
Math.floor(size.w),
|
||||
Math.floor(size.h)
|
||||
)
|
||||
}
|
||||
|
||||
fillRect(position: Point, size: Size, color: Color) {
|
||||
position = this.camera.negate().offset(position);
|
||||
|
||||
let ctx = getScreen().unsafeMakeContext();
|
||||
ctx.fillStyle = color.toStyle();
|
||||
ctx.fillRect(
|
||||
Math.floor(position.x),
|
||||
Math.floor(position.y),
|
||||
Math.floor(size.w),
|
||||
Math.floor(size.h)
|
||||
);
|
||||
}
|
||||
|
||||
drawRect(position: Point, size: Size, color: Color) {
|
||||
position = this.camera.negate().offset(position);
|
||||
|
||||
let ctx = getScreen().unsafeMakeContext();
|
||||
ctx.strokeStyle = color.toStyle();
|
||||
ctx.strokeRect(
|
||||
Math.floor(position.x) + 0.5,
|
||||
Math.floor(position.y) + 0.5,
|
||||
Math.floor(size.w),
|
||||
Math.floor(size.h)
|
||||
)
|
||||
}
|
||||
|
||||
drawText(text: string, position: Point, color: Color, options?: {alignX?: AlignX, alignY?: AlignY, forceWidth?: number}) {
|
||||
position = this.camera.negate().offset(position);
|
||||
|
||||
let ctx = getScreen().unsafeMakeContext();
|
||||
mainFont.internalDrawText({
|
||||
ctx,
|
||||
text,
|
||||
position,
|
||||
alignX: options?.alignX,
|
||||
alignY: options?.alignY,
|
||||
forceWidth: options?.forceWidth,
|
||||
color
|
||||
})
|
||||
}
|
||||
|
||||
measureText(text: string, forceWidth?: number): Size {
|
||||
return mainFont.measureText({text, forceWidth})
|
||||
}
|
||||
|
||||
drawSprite(sprite: Sprite, position: Point, ix?: number, options?: {xScale?: number, yScale: number, angle: number}) {
|
||||
position = this.camera.negate().offset(position);
|
||||
|
||||
let ctx = getScreen().unsafeMakeContext();
|
||||
sprite.internalDraw(ctx, {position, ix, xScale: options?.xScale, yScale: options?.yScale, angle: options?.angle})
|
||||
}
|
||||
}
|
||||
|
||||
let active: Drawing = new Drawing();
|
||||
|
||||
export function getDrawing(): Drawing {
|
||||
return active;
|
||||
}
|
||||
|
||||
|
133
src/engine/internal/font.ts
Normal file
133
src/engine/internal/font.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import {getAssets} from "./assets.ts";
|
||||
import fontSheet from '../../art/fonts/vga_8x16.png';
|
||||
import {AlignX, AlignY, Color, Point, Size} from "../datatypes.ts";
|
||||
|
||||
class Font {
|
||||
#filename: string;
|
||||
#cellsPerSheet: Size;
|
||||
#pixelsPerCell: Size;
|
||||
#tintingCanvas: HTMLCanvasElement;
|
||||
#tintedVersions: Record<string, HTMLImageElement>;
|
||||
|
||||
constructor(filename: string, cellsPerSheet: Size, pixelsPerCell: Size) {
|
||||
this.#filename = filename;
|
||||
this.#cellsPerSheet = cellsPerSheet;
|
||||
this.#pixelsPerCell = pixelsPerCell;
|
||||
this.#tintingCanvas = document.createElement("canvas");
|
||||
this.#tintedVersions = {}
|
||||
}
|
||||
|
||||
get #cx(): number { return this.#cellsPerSheet.w }
|
||||
get #cy(): number { return this.#cellsPerSheet.h }
|
||||
get #px(): number { return this.#pixelsPerCell.w }
|
||||
get #py(): number { return this.#pixelsPerCell.h }
|
||||
|
||||
#getTintedImage(color: string): HTMLImageElement | null {
|
||||
let image = getAssets().getImage(this.#filename);
|
||||
|
||||
if (!image.complete) { return null; }
|
||||
|
||||
let tintedVersion = this.#tintedVersions[color];
|
||||
if (tintedVersion != undefined) {
|
||||
return tintedVersion;
|
||||
}
|
||||
|
||||
let w = image.width;
|
||||
let h = image.height;
|
||||
|
||||
if (!(w == this.#cx * this.#px && h == this.#cy * this.#py)) {
|
||||
throw `unexpected image dimensions for font ${this.#filename}: ${w} x ${h}`
|
||||
}
|
||||
|
||||
this.#tintingCanvas.width = w;
|
||||
this.#tintingCanvas.height = h;
|
||||
let ctx = this.#tintingCanvas.getContext("2d")!;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.drawImage(image, 0, 0);
|
||||
ctx.globalCompositeOperation = "source-in";
|
||||
ctx.fillStyle = color;
|
||||
ctx.rect(0, 0, w, h);
|
||||
ctx.fill();
|
||||
|
||||
let result = new Image();
|
||||
result.src = this.#tintingCanvas.toDataURL();
|
||||
this.#tintedVersions[color] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
internalDrawText({ctx, text, position, alignX, alignY, forceWidth, color}: {
|
||||
ctx: CanvasRenderingContext2D,
|
||||
text: string,
|
||||
position: Point, alignX?: AlignX, alignY?: AlignY,
|
||||
forceWidth?: number, color: Color
|
||||
}) {
|
||||
alignX = alignX == undefined ? AlignX.Left : alignX;
|
||||
alignY = alignY == undefined ? AlignY.Top : alignY;
|
||||
forceWidth = forceWidth == undefined ? 65535 : forceWidth;
|
||||
|
||||
let image = this.#getTintedImage(color.toStyle())
|
||||
if (image == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let sz = this.#glyphwise(text, forceWidth, () => {});
|
||||
let offsetX = position.x;
|
||||
let offsetY = position.y;
|
||||
offsetX += (alignX == AlignX.Left ? 0 : alignX == AlignX.Center ? -sz.w / 2 : - sz.w)
|
||||
offsetY += (alignY == AlignY.Top ? 0 : alignY == AlignY.Middle ? -sz.h / 2 : - sz.h)
|
||||
|
||||
this.#glyphwise(text, forceWidth, (cx, cy, char) => {
|
||||
let srcIx = char.charCodeAt(0);
|
||||
this.#drawGlyph({ctx: ctx, image: image, ix: srcIx, x: offsetX + cx * this.#px, y: offsetY + cy * this.#py});
|
||||
})
|
||||
}
|
||||
|
||||
#drawGlyph({ctx, image, ix, x, y}: {ctx: CanvasRenderingContext2D, image: HTMLImageElement, ix: number, x: number, y: number}) {
|
||||
let srcCx = ix % this.#cx;
|
||||
let srcCy = Math.floor(ix / this.#cx);
|
||||
let srcPx = srcCx * this.#px;
|
||||
let srcPy = srcCy * this.#py;
|
||||
ctx.drawImage(
|
||||
image,
|
||||
srcPx, srcPy, this.#px, this.#py,
|
||||
Math.floor(x), Math.floor(y), this.#px, this.#py
|
||||
);
|
||||
}
|
||||
|
||||
measureText({text, forceWidth}: {text: string, forceWidth?: number}): Size {
|
||||
return this.#glyphwise(text, forceWidth, () => {});
|
||||
}
|
||||
|
||||
#glyphwise(text: string, forceWidth: number | undefined, callback: (x: number, y: number, char: string) => void): {w: number, h: number} {
|
||||
let cx = 0;
|
||||
let cy = 0;
|
||||
let cw = 0;
|
||||
let ch = 0;
|
||||
let wcx = forceWidth == undefined ? undefined : Math.floor(forceWidth / this.#px);
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
let char = text[i]
|
||||
if (char == '\n') {
|
||||
cx = 0;
|
||||
cy += 1;
|
||||
ch = cy + 1;
|
||||
} else {
|
||||
// console.log("cx, cy, char", [cx, cy, char])
|
||||
if (wcx != undefined && cx >= wcx) {
|
||||
cx = 0;
|
||||
cy += 1;
|
||||
ch = cy + 1;
|
||||
}
|
||||
|
||||
callback(cx, cy, char)
|
||||
cx += 1;
|
||||
cw = Math.max(cw, cx);
|
||||
ch = cy + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return { w: cw * this.#px, h: ch * this.#py };
|
||||
}
|
||||
}
|
||||
|
||||
export let mainFont = new Font(fontSheet, new Size(32, 8), new Size(8, 16));
|
34
src/engine/internal/host.ts
Normal file
34
src/engine/internal/host.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import './style.css'
|
||||
|
||||
import {pollAndTouch} from "./screen.ts";
|
||||
import {getClock} from "./clock.ts";
|
||||
import {getInput, setupInput} from "./input.ts";
|
||||
import {IGame} from "../datatypes.ts";
|
||||
|
||||
export function hostGame(game: IGame) {
|
||||
let gameCanvas = document.getElementById("game") as HTMLCanvasElement;
|
||||
setupInput(gameCanvas);
|
||||
onFrame(game, undefined); // start on-frame draw loop, set up screen
|
||||
}
|
||||
|
||||
function onFrame(game: IGame, timestamp: number | undefined) {
|
||||
let gameCanvas = document.getElementById("game") as HTMLCanvasElement;
|
||||
requestAnimationFrame((timestamp: number) => onFrame(game, timestamp));
|
||||
|
||||
if (timestamp) {
|
||||
getClock().recordTimestamp(timestamp);
|
||||
}
|
||||
onFrameFixScreen(gameCanvas);
|
||||
|
||||
while (getClock().popUpdate()) {
|
||||
game.update();
|
||||
getInput().update();
|
||||
}
|
||||
|
||||
game.draw();
|
||||
}
|
||||
|
||||
function onFrameFixScreen(canvas: HTMLCanvasElement) {
|
||||
pollAndTouch(canvas);
|
||||
}
|
||||
|
120
src/engine/internal/input.ts
Normal file
120
src/engine/internal/input.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import {getScreen} from "./screen.ts";
|
||||
import {Point} from "../datatypes.ts";
|
||||
|
||||
function handleKey(e: KeyboardEvent, down: boolean) {
|
||||
active.handleKeyDown(e.key, down);
|
||||
}
|
||||
function handleMouseOut() {
|
||||
active.handleMouseMove(-1, -1);
|
||||
}
|
||||
|
||||
function handleMouseMove(canvas: HTMLCanvasElement, m: MouseEvent) {
|
||||
if (canvas.offsetWidth == 0 || canvas.offsetHeight == 0) {
|
||||
return;
|
||||
}
|
||||
active.handleMouseMove(m.offsetX / canvas.offsetWidth, m.offsetY / canvas.offsetHeight);
|
||||
}
|
||||
|
||||
function handleMouseButton(canvas: HTMLCanvasElement, m: MouseEvent, down: boolean) {
|
||||
if (canvas.offsetWidth == 0 || canvas.offsetHeight == 0) {
|
||||
return;
|
||||
}
|
||||
active.handleMouseMove(m.offsetX / canvas.offsetWidth, m.offsetY / canvas.offsetHeight);
|
||||
let button: MouseButton | null = (
|
||||
m.button == 0 ? "leftMouse" :
|
||||
m.button == 1 ? "rightMouse" :
|
||||
null
|
||||
)
|
||||
if (button != null) {
|
||||
active.handleMouseDown(button, down);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function setupInput(canvas: HTMLCanvasElement) {
|
||||
canvas.addEventListener("keyup", (k) => handleKey(k, false));
|
||||
document.addEventListener("keyup", (k) => handleKey(k, false));
|
||||
canvas.addEventListener("keydown", (k) => handleKey(k, true));
|
||||
document.addEventListener("keydown", (k) => handleKey(k, true));
|
||||
canvas.addEventListener("mouseout", (_) => handleMouseOut());
|
||||
canvas.addEventListener("mousemove", (m) => handleMouseMove(canvas, m));
|
||||
canvas.addEventListener("mousedown", (m) => handleMouseButton(canvas, m, true));
|
||||
canvas.addEventListener("mouseup", (m) => handleMouseButton(canvas, m, false));
|
||||
}
|
||||
|
||||
export type MouseButton = "leftMouse" | "rightMouse";
|
||||
|
||||
class Input {
|
||||
#keyDown: Record<string, boolean>;
|
||||
#previousKeyDown: Record<string, boolean>;
|
||||
#mouseDown: Record<string, boolean>;
|
||||
#previousMouseDown: Record<string, boolean>;
|
||||
#mousePosition: Point | null;
|
||||
|
||||
constructor() {
|
||||
this.#keyDown = {};
|
||||
this.#previousKeyDown = {};
|
||||
this.#mouseDown = {};
|
||||
this.#previousMouseDown = {};
|
||||
this.#mousePosition = null;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.#previousKeyDown = {...this.#keyDown};
|
||||
this.#previousMouseDown = {...this.#mouseDown};
|
||||
}
|
||||
|
||||
handleMouseDown(name: string, down: boolean) {
|
||||
this.#mouseDown[name] = down;
|
||||
}
|
||||
handleKeyDown(name: string, down: boolean) {
|
||||
this.#keyDown[name] = down;
|
||||
}
|
||||
|
||||
handleMouseMove(x: number, y: number) {
|
||||
let screen = getScreen();
|
||||
if (x < 0.0 || x >= 1.0) { this.#mousePosition = null; }
|
||||
if (y < 0.0 || y >= 1.0) { this.#mousePosition = null; }
|
||||
|
||||
let w = screen.size.w;
|
||||
let h = screen.size.h;
|
||||
this.#mousePosition = new Point(
|
||||
Math.floor(x * w),
|
||||
Math.floor(y * h),
|
||||
)
|
||||
}
|
||||
|
||||
isMouseDown(btn: MouseButton) : boolean {
|
||||
return this.#mouseDown[btn];
|
||||
}
|
||||
|
||||
isMouseClicked(btn: MouseButton) : boolean {
|
||||
return this.#mouseDown[btn] && !this.#previousMouseDown[btn];
|
||||
}
|
||||
|
||||
isMouseReleased(btn: MouseButton) : boolean {
|
||||
return !this.#mouseDown[btn] && this.#previousMouseDown[btn];
|
||||
}
|
||||
|
||||
get mousePosition(): Point | null {
|
||||
return this.#mousePosition
|
||||
}
|
||||
|
||||
isKeyDown(key: string) : boolean {
|
||||
return this.#keyDown[key];
|
||||
}
|
||||
|
||||
isKeyPressed(key: string) : boolean {
|
||||
return this.#keyDown[key] && !this.#previousKeyDown[key];
|
||||
}
|
||||
|
||||
isKeyReleased(key: string) : boolean {
|
||||
return !this.#keyDown[key] && this.#previousKeyDown[key];
|
||||
}
|
||||
}
|
||||
|
||||
let active = new Input();
|
||||
|
||||
export function getInput(): Input {
|
||||
return active;
|
||||
}
|
66
src/engine/internal/screen.ts
Normal file
66
src/engine/internal/screen.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import {Size} from "../datatypes.ts";
|
||||
|
||||
// TODO: Just switch to the same pattern as everywhere else
|
||||
// (without repeatedly reassigning the variable)
|
||||
class Screen {
|
||||
#canvas: HTMLCanvasElement
|
||||
size: Size
|
||||
|
||||
constructor(canvas: HTMLCanvasElement, size: Size) {
|
||||
this.#canvas = canvas;
|
||||
this.size = size
|
||||
}
|
||||
|
||||
unsafeMakeContext(): CanvasRenderingContext2D {
|
||||
let ctx = this.#canvas.getContext("2d")!;
|
||||
|
||||
// TODO: Other stuff to do here?
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
ctx.filter = "none";
|
||||
ctx.strokeStyle = "#000";
|
||||
ctx.fillStyle = "#000";
|
||||
ctx.resetTransform();
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let active: Screen | undefined = undefined
|
||||
export let desiredWidth = 384;
|
||||
export let desiredHeight = 384;
|
||||
|
||||
export function pollAndTouch(canvas: HTMLCanvasElement) {
|
||||
let realWidth = canvas.offsetWidth;
|
||||
let realHeight = canvas.offsetHeight;
|
||||
|
||||
let divisors = [0.25, 0.5, 1, 2, 3, 4, 5, 6];
|
||||
for (let i = 0; i < divisors.length; i++) {
|
||||
// TODO: Is this necessary?
|
||||
divisors[i] /= window.devicePixelRatio;
|
||||
}
|
||||
|
||||
let div = 0;
|
||||
while (
|
||||
(div < divisors.length - 1) &&
|
||||
(realWidth / divisors[div + 1] >= desiredWidth) &&
|
||||
(realHeight / divisors[div + 1] >= desiredHeight)
|
||||
) {
|
||||
div += 1;
|
||||
}
|
||||
realWidth = Math.floor(canvas.offsetWidth / divisors[div]);
|
||||
realHeight = Math.floor(canvas.offsetHeight / divisors[div]);
|
||||
canvas.width = realWidth;
|
||||
canvas.height = realHeight;
|
||||
active = new Screen(canvas, new Size(realWidth, realHeight));
|
||||
}
|
||||
|
||||
export function getScreen(): Screen {
|
||||
if (active === undefined) {
|
||||
throw `screen should have been defined: ${active}`
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
|
47
src/engine/internal/sprite.ts
Normal file
47
src/engine/internal/sprite.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import {getAssets} from "./assets.ts";
|
||||
import {Point, Size} from "../datatypes.ts";
|
||||
|
||||
|
||||
export class Sprite {
|
||||
readonly imageSet: string;
|
||||
// spritesheet params
|
||||
readonly pixelsPerSubimage: Size;
|
||||
readonly origin: Point;
|
||||
readonly cellsPerSheet: Size;
|
||||
// number of frames
|
||||
readonly nFrames: number;
|
||||
|
||||
constructor(imageSet: string, pixelsPerSubimage: Size, origin: Point, cellsPerSheet: Size, nFrames: number) {
|
||||
this.imageSet = imageSet;
|
||||
this.pixelsPerSubimage = pixelsPerSubimage;
|
||||
this.origin = origin;
|
||||
this.cellsPerSheet = cellsPerSheet;
|
||||
this.nFrames = nFrames;
|
||||
|
||||
let nPossibleFrames = this.cellsPerSheet.w * this.cellsPerSheet.h;
|
||||
if (this.nFrames > nPossibleFrames) {
|
||||
throw `can't have ${this.nFrames} with a spritesheet dimension of ${nPossibleFrames}`;
|
||||
}
|
||||
}
|
||||
|
||||
internalDraw(ctx: CanvasRenderingContext2D, {position, ix, xScale, yScale, angle}: {position: Point, ix?: number, xScale?: number, yScale?: number, angle?: number}) {
|
||||
ix = ix == undefined ? 0 : ix;
|
||||
xScale = xScale == undefined ? 1.0 : xScale;
|
||||
yScale = yScale == undefined ? 1.0 : yScale;
|
||||
angle = angle == undefined ? 0.0 : angle;
|
||||
|
||||
// ctx.translate(Math.floor(x), Math.floor(y));
|
||||
ctx.translate(position.x, position.y);
|
||||
ctx.rotate(angle * Math.PI / 180);
|
||||
ctx.scale(xScale, yScale);
|
||||
ctx.translate(-this.origin.x, -this.origin.y);
|
||||
|
||||
let me = getAssets().getImage(this.imageSet);
|
||||
let srcCx = ix % this.cellsPerSheet.w;
|
||||
let srcCy = Math.floor(ix / this.cellsPerSheet.w);
|
||||
let srcPx = srcCx * this.pixelsPerSubimage.w;
|
||||
let srcPy = srcCy * this.pixelsPerSubimage.h;
|
||||
console.log(`src px and py ${srcPx} ${srcPy}`)
|
||||
ctx.drawImage(me, srcPx, srcPy, this.pixelsPerSubimage.w, this.pixelsPerSubimage.h, 0, 0, this.pixelsPerSubimage.w, this.pixelsPerSubimage.h);
|
||||
}
|
||||
}
|
12
src/engine/internal/style.css
Normal file
12
src/engine/internal/style.css
Normal file
@ -0,0 +1,12 @@
|
||||
html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
#game {
|
||||
image-rendering: pixelated;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
8
src/engine/public.ts
Normal file
8
src/engine/public.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import {getInput} from "./internal/input.ts";
|
||||
import {getDrawing} from "./internal/drawing.ts";
|
||||
|
||||
// input reexports
|
||||
export let I = getInput();
|
||||
|
||||
// drawing reexports
|
||||
export let D = getDrawing();
|
Reference in New Issue
Block a user