Compare commits

...

17 Commits
master ... dev

Author SHA1 Message Date
Maciej Samborski 859cf6aaec Added support for scaling into frustum culling 2025-01-16 23:15:34 +01:00
Maciej Samborski 1fd21affde Added `push` and `get` to wasmgl module 2025-01-16 23:07:37 +01:00
Maciej Samborski 2bf6e33350 Added basic occlusion culling 2025-01-16 00:11:48 +01:00
Maciej Samborski b5910d8c97 Optimized batching and drawing 2025-01-06 22:44:41 +01:00
Maciej Samborski 37a91de848 Added Sprite, Spritesheet and made batching actually do shit 2025-01-05 23:52:15 +01:00
Maciej Samborski 2e9ec2565b Fixed scaling event lag 2025-01-05 14:50:05 +01:00
Maciej Samborski 87ad6fbd9d Added camera scaling 2025-01-02 19:39:39 +01:00
Maciej Samborski 912487d264 Added basic Object Batching 2025-01-01 21:10:49 +01:00
Maciej Samborski 14d5db166e Fixed, camera translate, matrix transform and texture transparency 2024-12-31 16:03:15 +01:00
Maciej Samborski e936b626f3 Added Grid and grid based rendering 2024-12-27 22:50:07 +01:00
Maciej Samborski 95257a4ef3 Added ability to change between isometric and normal drawing 2024-12-27 14:29:07 +01:00
Maciej Samborski 2a1fe1d0a9 Added Input support 2024-12-27 14:05:20 +01:00
Maciej Samborski bdde494b5b Added isometric projection 2024-12-26 19:02:14 +01:00
Maciej Samborski ab532a2608 Improved switching between textures and colors 2024-12-24 21:12:41 +01:00
Maciej Samborski 2c81bc0684 Added support for Textures 2024-12-24 20:37:22 +01:00
Maciej Samborski 129cf05b90 Added Mat4, Vec4 and some of the mathematical operations on those 2024-12-24 16:30:00 +01:00
Maciej Samborski 3e9bdcc469 Started work on wasm interface 2024-12-19 19:07:59 +01:00
58 changed files with 3187 additions and 812 deletions

BIN
assets/character.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,3 @@
For a description of the contents of this pack, usage rights, and other useful details, go here:
http://blog.spiralgraphics.biz/2011/02/nine-cartoon-backdrops.html

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
assets/grass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

BIN
assets/grass2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 B

BIN
assets/greenary.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

BIN
assets/log.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

BIN
assets/sprites.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/wall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,74 +0,0 @@
function initializeContext(canvasId) {
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext("webgl2", { antialias: false });
return ctx;
}
class Vec2 {
x;
y;
constructor(x, y) {
this.x = x;
this.y = y;
}
add(other) {
this.x += other.x;
this.y += other.y;
}
addScalar(scalar) {
this.x += scalar;
this.y += scalar;
}
addNew(other) {
return new Vec2(this.x + other.x, this.y + other.y);
}
addScalarNew(scalar) {
return new Vec2(this.x + scalar, this.y + scalar);
}
sub(other) {
this.x -= other.x;
this.y -= other.y;
}
mult(scalar) {
this.x *= scalar;
this.y *= scalar;
}
multNew(scalar) {
return new Vec2(this.x * scalar, this.y * scalar);
}
splatToArray() {
return [this.x, this.y];
}
static angle(angle) {
const eps = 1e-6;
let x = Math.cos(angle);
let y = Math.sin(angle);
if ((x > 0 && x < eps)
|| (x < 0 && x > -eps))
x = 0;
if ((y > 0 && y < eps)
|| (y < 0 && y > -eps))
y = 0;
return new Vec2(x, y);
}
}
class Vec3 {
x;
y;
z;
constructor(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
}
add(other) {
this.x += other.x;
this.y += other.y;
this.z += other.z;
}
sub(other) {
this.x -= other.x;
this.y -= other.y;
this.z -= other.z;
}
}
export { initializeContext, Vec2, Vec3 };

View File

@ -1,96 +0,0 @@
function initializeContext(canvasId: string): WebGL2RenderingContext | null {
const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
const ctx = canvas.getContext("webgl2", {antialias: false});
return ctx;
}
type Color = [number, number, number, number]
class Vec2 {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
add(other: Vec2) {
this.x += other.x;
this.y += other.y;
}
addScalar(scalar: number) {
this.x += scalar;
this.y += scalar;
}
addNew(other: Vec2): Vec2 {
return new Vec2(this.x + other.x, this.y + other.y);
}
addScalarNew(scalar: number): Vec2 {
return new Vec2(this.x + scalar, this.y + scalar);
}
sub(other: Vec2) {
this.x -= other.x;
this.y -= other.y;
}
mult(scalar: number) {
this.x *= scalar;
this.y *= scalar;
}
multNew(scalar: number): Vec2 {
return new Vec2(this.x * scalar, this.y * scalar);
}
splatToArray(): Array<number> {
return [this.x, this.y];
}
static angle(angle: number): Vec2 {
const eps = 1e-6;
let x = Math.cos(angle);
let y = Math.sin(angle);
if ((x > 0 && x < eps)
|| (x < 0 && x > -eps))
x = 0;
if ((y > 0 && y < eps)
|| (y < 0 && y > -eps))
y = 0;
return new Vec2(x, y);
}
}
class Vec3 {
x: number;
y: number;
z: number;
constructor(x: number, y: number, z: number) {
this.x = x;
this.y = y;
this.z = z;
}
add(other: Vec3) {
this.x += other.x;
this.y += other.y;
this.z += other.z;
}
sub(other: Vec3) {
this.x -= other.x;
this.y -= other.y;
this.z -= other.z;
}
}
export { initializeContext, Vec2, Vec3, Color };

92
draw.js
View File

@ -1,92 +0,0 @@
import { Vec2 } from "./common.js";
function drawTriangle(gfx, positions, color) {
const a_position = gfx.getAttribute("a_position");
const a_color = gfx.getAttribute("a_color");
const points = [
positions[0].x, positions[0].y,
positions[1].x, positions[1].y,
positions[2].x, positions[2].y,
];
const colors = [
color,
color,
color,
];
a_position.data(gfx.ctx, points, gfx.ctx.STATIC_DRAW);
a_color.data(gfx.ctx, colors.flat(), gfx.ctx.STATIC_DRAW);
gfx.ctx.drawArrays(gfx.ctx.TRIANGLES, 0, 3);
}
function drawTriangleExts(gfx, position, exts, color) {
const a_position = gfx.getAttribute("a_position");
const a_color = gfx.getAttribute("a_color");
const points = [
position.x, position.y,
position.x + exts.x, position.y,
position.x, position.y + exts.y,
];
const colors = [
color,
color,
color,
];
a_position.data(gfx.ctx, points, gfx.ctx.STATIC_DRAW);
a_color.data(gfx.ctx, colors.flat(), gfx.ctx.STATIC_DRAW);
gfx.ctx.drawArrays(gfx.ctx.TRIANGLES, 0, 3);
}
function drawRectangle(gfx, position, exts, color) {
const a_position = gfx.getAttribute("a_position");
const a_color = gfx.getAttribute("a_color");
const points = [
position.x, position.y,
position.x + exts.x, position.y,
position.x, position.y + exts.y,
position.x + exts.x, position.y + exts.y,
position.x + exts.x, position.y,
position.x, position.y + exts.y,
];
const colors = [
color,
color,
color,
color,
color,
color,
];
a_position.data(gfx.ctx, points, gfx.ctx.STATIC_DRAW);
a_color.data(gfx.ctx, colors.flat(), gfx.ctx.STATIC_DRAW);
gfx.ctx.drawArrays(gfx.ctx.TRIANGLES, 0, 6);
}
function drawCircle(gfx, position, radius, color) {
const points = new Array();
const precision = 40;
const angle = 2.0 * Math.PI / precision;
let a = 0;
for (let i = 0; i < precision; ++i) {
var vec = Vec2.angle(a);
vec.mult(radius);
a += angle;
points.push(vec);
}
for (let i = 0; i < points.length; i++) {
const current = points[i];
const next = points[(i + 1) % points.length];
let center = position;
drawTriangle(gfx, [center, center.addNew(current), center.addNew(next)], color);
}
}
function drawLine(gfx, A, B, color) {
const a_position = gfx.getAttribute("a_position");
const a_color = gfx.getAttribute("a_color");
let points = [
A.x, A.y,
B.x, B.y,
];
const colors = [
color,
color,
];
a_position.data(gfx.ctx, points, gfx.ctx.STATIC_DRAW);
a_color.data(gfx.ctx, colors.flat(), gfx.ctx.STATIC_DRAW);
gfx.ctx.drawArrays(gfx.ctx.LINES, 0, 2);
}
export { drawTriangle, drawTriangleExts, drawRectangle, drawCircle, drawLine };

116
draw.ts
View File

@ -1,116 +0,0 @@
import { Vec2 } from "./common.js"
import { Graphics } from "./graphics.js";
import { Color } from "./common.js";
function drawTriangle(gfx: Graphics, positions: [Vec2, Vec2, Vec2], color: Color) {
const a_position = gfx.getAttribute("a_position");
const a_color = gfx.getAttribute("a_color");
const points: Array<number> = [
positions[0].x, positions[0].y,
positions[1].x, positions[1].y,
positions[2].x, positions[2].y,
]
const colors: Array<number[]> = [
color,
color,
color,
];
a_position.data(gfx.ctx, points, gfx.ctx.STATIC_DRAW);
a_color.data(gfx.ctx, colors.flat(), gfx.ctx.STATIC_DRAW);
gfx.ctx.drawArrays(gfx.ctx.TRIANGLES, 0, 3);
}
function drawTriangleExts(gfx: Graphics, position: Vec2, exts: Vec2, color: Color) {
const a_position = gfx.getAttribute("a_position");
const a_color = gfx.getAttribute("a_color");
const points: Array<number> = [
position.x, position.y,
position.x + exts.x, position.y,
position.x, position.y + exts.y,
]
const colors: Array<number[]> = [
color,
color,
color,
];
a_position.data(gfx.ctx, points, gfx.ctx.STATIC_DRAW);
a_color.data(gfx.ctx, colors.flat(), gfx.ctx.STATIC_DRAW);
gfx.ctx.drawArrays(gfx.ctx.TRIANGLES, 0, 3);
}
function drawRectangle(gfx: Graphics, position: Vec2, exts: Vec2, color: Color) {
const a_position = gfx.getAttribute("a_position");
const a_color = gfx.getAttribute("a_color");
const points: Array<number> = [
position.x, position.y,
position.x + exts.x, position.y,
position.x, position.y + exts.y,
position.x + exts.x, position.y + exts.y,
position.x + exts.x, position.y,
position.x, position.y + exts.y,
]
const colors: Array<number[]> = [
color,
color,
color,
color,
color,
color,
];
a_position.data(gfx.ctx, points, gfx.ctx.STATIC_DRAW);
a_color.data(gfx.ctx, colors.flat(), gfx.ctx.STATIC_DRAW);
gfx.ctx.drawArrays(gfx.ctx.TRIANGLES, 0, 6);
}
function drawCircle(gfx: Graphics, position: Vec2, radius: number, color: Color) {
const points: Array<Vec2> = new Array<Vec2>();
const precision = 40;
const angle = 2.0*Math.PI/precision;
let a = 0;
for (let i = 0; i < precision; ++i) {
var vec = Vec2.angle(a);
vec.mult(radius);
a += angle;
points.push(vec);
}
for (let i = 0; i < points.length; i++) {
const current = points[i];
const next = points[(i + 1) % points.length];
let center = position;
drawTriangle(gfx, [center, center.addNew(current), center.addNew(next)], color);
}
}
function drawLine(gfx: Graphics, A: Vec2, B: Vec2, color: Color) {
const a_position = gfx.getAttribute("a_position");
const a_color = gfx.getAttribute("a_color");
let points: Array<number> = [
A.x, A.y,
B.x, B.y,
];
const colors: Array<number[]> = [
color,
color,
];
a_position.data(gfx.ctx, points, gfx.ctx.STATIC_DRAW);
a_color.data(gfx.ctx, colors.flat(), gfx.ctx.STATIC_DRAW);
gfx.ctx.drawArrays(gfx.ctx.LINES, 0, 2);
}
export { drawTriangle, drawTriangleExts, drawRectangle, drawCircle, drawLine }

View File

@ -1,110 +0,0 @@
function fullscreenCanvas(gfx, id) {
const canvas = document.getElementById(id);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
gfx.ctx.viewport(0, 0, canvas.width, canvas.height);
}
function createShader(ctx, type, source) {
var shader = ctx.createShader(type);
if (!shader) {
throw new Error("Couldn't create shader: " + type);
}
ctx.shaderSource(shader, source);
ctx.compileShader(shader);
var success = ctx.getShaderParameter(shader, ctx.COMPILE_STATUS);
if (!success) {
console.log(ctx.getShaderInfoLog(shader));
ctx.deleteShader(shader);
throw new Error("Couldn't compile shader: " + type);
}
return shader;
}
function createProgram(ctx, vertexShaderSource, fragmentShaderSource) {
var program = ctx.createProgram();
if (vertexShaderSource !== null) {
const vs = createShader(ctx, ctx.VERTEX_SHADER, vertexShaderSource);
ctx.attachShader(program, vs);
}
if (fragmentShaderSource !== null) {
const fs = createShader(ctx, ctx.FRAGMENT_SHADER, fragmentShaderSource);
ctx.attachShader(program, fs);
}
ctx.linkProgram(program);
var success = ctx.getProgramParameter(program, ctx.LINK_STATUS);
if (!success) {
console.log(ctx.getProgramInfoLog(program));
ctx.deleteProgram(program);
throw new Error("Failed to create program!");
}
return program;
}
class Graphics {
ctx;
program;
attribs = new Map();
uniforms = new Map();
vao;
constructor(ctx, vs, fs) {
this.ctx = ctx;
this.program = createProgram(ctx, vs, fs);
this.vao = ctx.createVertexArray();
ctx.bindVertexArray(this.vao);
ctx.viewport(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.useProgram(this.program);
}
clear(r, g, b, a) {
this.ctx.clearColor(r, g, b, a);
this.ctx.clear(this.ctx.COLOR_BUFFER_BIT);
}
createAttribute(name) {
const attrib = new Attribute(this.ctx, this.program, name);
this.attribs.set(name, attrib);
return attrib;
}
getAttribute(name) {
const attrib = this.attribs.get(name);
if (attrib === undefined)
throw new Error("Tried to get uninitialized attribute: " + name);
return attrib;
}
createUniform(name) {
const loc = this.ctx.getUniformLocation(this.program, name);
if (loc === null)
throw new Error("Couldn't get location for uniform: " + name);
this.uniforms.set(name, loc);
}
getUniform(name) {
const loc = this.uniforms.get(name);
if (loc === undefined)
throw new Error("Tried to get uninitialized uniform: " + name);
return loc;
}
}
class Attribute {
loc;
buffer;
// TODO: maybe use undefined as default value?
size = 0;
type = 0;
normalized = false;
stride = 0;
offset = 0;
constructor(ctx, program, name) {
this.loc = ctx.getAttribLocation(program, name);
this.buffer = ctx.createBuffer();
ctx.enableVertexAttribArray(this.loc);
}
format(size, type, normalized, stride, offset) {
this.size = size;
this.type = type;
this.normalized = normalized;
this.stride = stride;
this.offset = offset;
}
data(ctx, data, usage) {
ctx.bindBuffer(ctx.ARRAY_BUFFER, this.buffer);
ctx.bufferData(ctx.ARRAY_BUFFER, new Float32Array(data), usage);
ctx.vertexAttribPointer(this.loc, this.size, this.type, this.normalized, this.stride, this.offset);
}
}
export { fullscreenCanvas, Graphics, Attribute };

View File

@ -1,148 +0,0 @@
import { Vec2 } from "./common.js"
function fullscreenCanvas(gfx: Graphics, id: string) {
const canvas = document.getElementById(id) as HTMLCanvasElement;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
gfx.ctx.viewport(0, 0, canvas.width, canvas.height);
}
function createShader(ctx: WebGL2RenderingContext, type: GLenum, source: string): WebGLShader {
var shader = ctx.createShader(type);
if (!shader) {
throw new Error("Couldn't create shader: " + type);
}
ctx.shaderSource(shader, source);
ctx.compileShader(shader);
var success = ctx.getShaderParameter(shader, ctx.COMPILE_STATUS);
if (!success) {
console.log(ctx.getShaderInfoLog(shader));
ctx.deleteShader(shader);
throw new Error("Couldn't compile shader: " + type);
}
return shader;
}
function createProgram(ctx: WebGL2RenderingContext, vertexShaderSource: string | null, fragmentShaderSource: string | null): WebGLProgram {
var program = ctx.createProgram();
if (vertexShaderSource !== null) {
const vs = createShader(ctx, ctx.VERTEX_SHADER, vertexShaderSource);
ctx.attachShader(program, vs);
}
if (fragmentShaderSource !== null) {
const fs = createShader(ctx, ctx.FRAGMENT_SHADER, fragmentShaderSource);
ctx.attachShader(program, fs);
}
ctx.linkProgram(program);
var success = ctx.getProgramParameter(program, ctx.LINK_STATUS);
if (!success) {
console.log(ctx.getProgramInfoLog(program));
ctx.deleteProgram(program);
throw new Error("Failed to create program!");
}
return program;
}
class Graphics {
ctx: WebGL2RenderingContext;
program: WebGLProgram;
attribs: Map<string, Attribute> = new Map();
uniforms: Map<string, WebGLUniformLocation> = new Map();
vao: WebGLVertexArrayObject;
constructor(ctx: WebGL2RenderingContext, vs: string, fs: string) {
this.ctx = ctx;
this.program = createProgram(ctx, vs, fs);
this.vao = ctx.createVertexArray();
ctx.bindVertexArray(this.vao);
ctx.viewport(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.useProgram(this.program);
}
clear(r: number, g: number, b: number, a: number) {
this.ctx.clearColor(r, g, b, a);
this.ctx.clear(this.ctx.COLOR_BUFFER_BIT);
}
createAttribute(name: string): Attribute {
const attrib = new Attribute(this.ctx, this.program, name);
this.attribs.set(name, attrib);
return attrib;
}
getAttribute(name: string): Attribute {
const attrib = this.attribs.get(name);
if (attrib === undefined)
throw new Error("Tried to get uninitialized attribute: " + name);
return attrib;
}
createUniform(name: string) {
const loc = this.ctx.getUniformLocation(this.program, name);
if (loc === null)
throw new Error("Couldn't get location for uniform: " + name);
this.uniforms.set(name, loc);
}
getUniform(name: string): WebGLUniformLocation {
const loc = this.uniforms.get(name);
if (loc === undefined)
throw new Error("Tried to get uninitialized uniform: " + name);
return loc;
}
}
class Attribute {
loc: GLint;
buffer: WebGLBuffer;
// TODO: maybe use undefined as default value?
size: GLint = 0;
type: GLenum = 0;
normalized: GLboolean = false;
stride: GLsizei = 0;
offset: GLintptr = 0;
constructor(ctx: WebGL2RenderingContext, program: WebGLProgram, name: string) {
this.loc = ctx.getAttribLocation(program, name);
this.buffer = ctx.createBuffer();
ctx.enableVertexAttribArray(this.loc);
}
format(
size: GLint,
type: GLenum,
normalized: GLboolean,
stride: GLsizei,
offset: GLintptr)
{
this.size = size;
this.type = type;
this.normalized = normalized;
this.stride = stride;
this.offset = offset;
}
data(ctx: WebGL2RenderingContext, data: Array<number>, usage: GLenum) {
ctx.bindBuffer(ctx.ARRAY_BUFFER, this.buffer);
ctx.bufferData(ctx.ARRAY_BUFFER, new Float32Array(data), usage);
ctx.vertexAttribPointer(this.loc, this.size, this.type, this.normalized, this.stride, this.offset);
}
}
export { fullscreenCanvas, Graphics, Attribute }

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="description" content="" />
<link rel="stylesheet" href="style.css"></link>
<script type="module" src="script.js" defer> var exports = {}; </script>
<script type="module" src="src/js/script.js" defer> var exports = {}; </script>
</head>
<body>
<canvas id="game"></canvas>

View File

@ -1,77 +0,0 @@
import { initializeContext, Vec2 } from "./common.js";
import { Graphics, fullscreenCanvas } from "./graphics.js";
import * as drawing from "./draw.js";
const vertexShader = `#version 300 es
in vec2 a_position;
in vec4 a_color;
out vec4 color;
uniform vec2 u_resolution;
void main() {
vec2 clipSpace = (a_position / u_resolution) * 2.0 - 1.0;
color = a_color;
gl_Position = vec4(clipSpace.xy, 0.0, 1.0);
}
`;
const fragmentShader = `#version 300 es
precision highp float;
in vec4 color;
out vec4 outColor;
void main() {
outColor = color;
}
`;
function draw(gfx, dt, pos, velocity) {
gfx.clear(0, 0, 0, 0);
gfx.ctx.uniform2f(gfx.getUniform("u_resolution"), gfx.ctx.canvas.width, gfx.ctx.canvas.height);
drawing.drawCircle(gfx, pos, 200, [1, 0, 0, 1]);
if (pos.x + 200 >= gfx.ctx.canvas.width) {
pos.x = gfx.ctx.canvas.width - 200;
velocity.x *= -1;
}
else if (pos.x - 200 <= 0) {
pos.x = 200;
velocity.x *= -1;
}
if (pos.y + 200 >= gfx.ctx.canvas.height) {
pos.y = gfx.ctx.canvas.height - 200;
velocity.y *= -1;
}
else if (pos.y - 200 <= 0) {
pos.y = 200;
velocity.y *= -1;
}
pos.add(velocity.multNew(dt));
}
(() => {
const canvasId = "game";
const ctx = initializeContext(canvasId);
if (ctx === null)
return;
const gfx = new Graphics(ctx, vertexShader, fragmentShader);
fullscreenCanvas(gfx, canvasId);
const a_position = gfx.createAttribute("a_position");
a_position.format(2, gfx.ctx.FLOAT, false, 0, 0);
const a_color = gfx.createAttribute("a_color");
a_color.format(4, gfx.ctx.FLOAT, false, 0, 0);
gfx.createUniform("u_resolution");
let pos = new Vec2(300, 300);
let velocity = new Vec2(200, 200);
let prevTimestamp = 0;
const frame = (timestamp) => {
const deltaTime = (timestamp - prevTimestamp) / 1000;
prevTimestamp = timestamp;
fullscreenCanvas(gfx, "game");
draw(gfx, deltaTime, pos, velocity);
window.requestAnimationFrame(frame);
};
window.requestAnimationFrame((timestamp) => {
prevTimestamp = timestamp;
window.requestAnimationFrame(frame);
});
})();

View File

@ -1,98 +0,0 @@
import { initializeContext, Vec2 } from "./common.js";
import { Graphics, fullscreenCanvas } from "./graphics.js";
import * as drawing from "./draw.js";
const vertexShader =
`#version 300 es
in vec2 a_position;
in vec4 a_color;
out vec4 color;
uniform vec2 u_resolution;
void main() {
vec2 clipSpace = (a_position / u_resolution) * 2.0 - 1.0;
color = a_color;
gl_Position = vec4(clipSpace.xy, 0.0, 1.0);
}
`;
const fragmentShader =
`#version 300 es
precision highp float;
in vec4 color;
out vec4 outColor;
void main() {
outColor = color;
}
`;
function draw(gfx: Graphics, dt: number, pos: Vec2, velocity: Vec2) {
gfx.clear(0, 0, 0, 0);
gfx.ctx.uniform2f(gfx.getUniform("u_resolution"), gfx.ctx.canvas.width, gfx.ctx.canvas.height);
drawing.drawCircle(gfx, pos, 200, [1, 0, 0, 1]);
if (pos.x + 200 >= gfx.ctx.canvas.width)
{
pos.x = gfx.ctx.canvas.width - 200;
velocity.x *= -1;
} else if (pos.x - 200 <= 0)
{
pos.x = 200;
velocity.x *= -1;
}
if (pos.y + 200 >= gfx.ctx.canvas.height)
{
pos.y = gfx.ctx.canvas.height - 200;
velocity.y *= -1;
} else if (pos.y - 200 <= 0)
{
pos.y = 200;
velocity.y *= -1;
}
pos.add(velocity.multNew(dt));
}
(() => {
const canvasId = "game";
const ctx = initializeContext(canvasId);
if (ctx === null) return;
const gfx = new Graphics(ctx, vertexShader, fragmentShader);
fullscreenCanvas(gfx, canvasId);
const a_position = gfx.createAttribute("a_position");
a_position.format(2, gfx.ctx.FLOAT, false, 0, 0);
const a_color = gfx.createAttribute("a_color");
a_color.format(4, gfx.ctx.FLOAT, false, 0, 0);
gfx.createUniform("u_resolution");
let pos = new Vec2(300, 300);
let velocity = new Vec2(200, 200);
let prevTimestamp = 0;
const frame = (timestamp: number) => {
const deltaTime = (timestamp - prevTimestamp)/1000;
prevTimestamp = timestamp;
fullscreenCanvas(gfx, "game");
draw(gfx, deltaTime, pos, velocity);
window.requestAnimationFrame(frame);
}
window.requestAnimationFrame((timestamp) => {
prevTimestamp = timestamp;
window.requestAnimationFrame(frame);
});
})();

27
src/js/assets.js Normal file
View File

@ -0,0 +1,27 @@
import { Vec2 } from "./common.js";
import { Texture, Spritesheet } from "./graphics.js";
export const Colors = {
Red: [1, 0, 0, 1],
Green: [0, 1, 0, 1],
Blue: [0, 0, 1, 1],
Brown: [0.341, 0.337, 0.204, 1],
Gray: [0.66, 0.66, 0.66, 1],
};
export class Assets {
assets = new Map();
push(name, asset) {
if (this.assets.get(name) !== undefined)
throw new Error("Asset name occupied!");
this.assets.set(name, asset);
}
get(name) {
let a = this.assets.get(name);
if (a === undefined)
throw new Error("Couldn't find asset: " + name);
return a;
}
async load(gfx) {
assets.push("sprites", new Spritesheet(await Texture.load(gfx, "../../assets/sprites.png"), new Vec2(16, 16)));
}
}
export const assets = new Assets();

39
src/js/assets.ts Normal file
View File

@ -0,0 +1,39 @@
import {Vec2} from "./common.js";
import {Graphics, Texture, Spritesheet} from "./graphics.js";
export type Color = [number, number, number, number]
export const Colors = {
Red : [1, 0, 0, 1] as Color,
Green : [0, 1, 0, 1] as Color,
Blue : [0, 0, 1, 1] as Color,
Brown : [0.341, 0.337, 0.204, 1] as Color,
Gray : [0.66, 0.66, 0.66, 1] as Color,
}
export type Asset = Texture | Spritesheet;
export class Assets {
assets: Map<string, Asset> = new Map();
push(name: string, asset: Asset) {
if (this.assets.get(name) !== undefined)
throw new Error("Asset name occupied!");
this.assets.set(name, asset);
}
get(name: string): Asset {
let a = this.assets.get(name);
if (a === undefined)
throw new Error("Couldn't find asset: " + name);
return a;
}
async load(gfx: Graphics) {
assets.push("sprites", new Spritesheet(await Texture.load(gfx, "../../assets/sprites.png"), new Vec2(16, 16)));
}
}
export const assets = new Assets();

464
src/js/common.js Normal file
View File

@ -0,0 +1,464 @@
export function initializeContext(canvasId) {
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext("webgl2", { antialias: false });
return ctx;
}
export class Vec2 {
x;
y;
constructor(x, y) {
this.x = x;
this.y = y;
}
from(scalar) {
return new Vec2(scalar, scalar);
}
static ZERO() {
return new Vec2(0, 0);
}
static ID() {
return new Vec2(1, 1);
}
copy() {
return new Vec2(this.x, this.y);
}
add(other) {
this.x += other.x;
this.y += other.y;
}
addScalar(scalar) {
this.x += scalar;
this.y += scalar;
}
addNew(other) {
return new Vec2(this.x + other.x, this.y + other.y);
}
addScalarNew(scalar) {
return new Vec2(this.x + scalar, this.y + scalar);
}
sub(other) {
this.x -= other.x;
this.y -= other.y;
}
subNew(other) {
let vec = this.copy();
vec.x -= other.x;
vec.y -= other.y;
return vec;
}
multScalar(scalar) {
this.x *= scalar;
this.y *= scalar;
}
multScalarNew(scalar) {
return new Vec2(this.x * scalar, this.y * scalar);
}
div(other) {
if (other.x == 0 ||
other.y == 0) {
throw new Error("Division by zero in Vec4");
}
this.x /= other.x;
this.y /= other.y;
}
divNew(other) {
if (other.x == 0 ||
other.y == 0) {
throw new Error("Division by zero in Vec4");
}
let vec = this.copy();
vec.x /= other.x;
vec.y /= other.y;
return vec;
}
reduce() {
throw new Error("Can't reduce Vec2!");
}
extend(value) {
return new Vec3(this.x, this.y, value);
}
splatToArray() {
return [this.x, this.y];
}
static angle(angle) {
const eps = 1e-6;
let x = Math.cos(angle);
let y = Math.sin(angle);
if ((x > 0 && x < eps)
|| (x < 0 && x > -eps))
x = 0;
if ((y > 0 && y < eps)
|| (y < 0 && y > -eps))
y = 0;
return new Vec2(x, y);
}
}
export class Vec3 {
x;
y;
z;
constructor(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
}
from(scalar) {
return new Vec3(scalar, scalar, scalar);
}
addScalar(scalar) {
this.x += scalar;
this.y += scalar;
this.z += scalar;
}
addScalarNew(scalar) {
let vec = this.copy();
vec.x += scalar;
vec.y += scalar;
vec.z += scalar;
return vec;
}
static ZERO() {
return new Vec3(0, 0, 0);
}
static ID() {
return new Vec3(1, 1, 1);
}
copy() {
return new Vec3(this.x, this.y, this.z);
}
add(other) {
this.x += other.x;
this.y += other.y;
this.z += other.z;
}
addNew(other) {
let vec = this.copy();
vec.x += other.x;
vec.y += other.y;
vec.z += other.z;
return vec;
}
sub(other) {
this.x -= other.x;
this.y -= other.y;
this.z -= other.z;
}
subNew(other) {
let vec = this.copy();
vec.x -= other.x;
vec.y -= other.y;
vec.z -= other.z;
return vec;
}
multScalar(scalar) {
this.x *= scalar;
this.y *= scalar;
this.z *= scalar;
}
multScalarNew(scalar) {
let vec = this.copy();
vec.x *= scalar;
vec.y *= scalar;
vec.z *= scalar;
return vec;
}
div(other) {
if (other.x == 0 ||
other.y == 0 ||
other.z == 0) {
throw new Error("Division by zero in Vec4");
}
this.x /= other.x;
this.y /= other.y;
this.z /= other.z;
}
divNew(other) {
if (other.x == 0 ||
other.y == 0 ||
other.z == 0) {
throw new Error("Division by zero in Vec4");
}
let vec = this.copy();
vec.x /= other.x;
vec.y /= other.y;
vec.z /= other.z;
return vec;
}
reduce() {
return new Vec2(this.x, this.y);
}
extend(value) {
return new Vec4(this.x, this.y, this.z, value);
}
splatToArray() {
return [this.x, this.y, this.z];
}
}
export class Vec4 {
x;
y;
z;
w;
constructor(x, y, z, w) {
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
from(scalar) {
return new Vec4(scalar, scalar, scalar, scalar);
}
static ZERO() {
return new Vec4(0, 0, 0, 0);
}
static ID() {
return new Vec4(1, 1, 1, 1);
}
copy() {
return new Vec4(this.x, this.y, this.z, this.w);
}
add(other) {
this.x += other.x;
this.y += other.y;
this.z += other.z;
this.w += other.w;
}
addNew(other) {
let vec = this.copy();
vec.x += other.x;
vec.y += other.y;
vec.z += other.z;
vec.w += other.w;
return vec;
}
addScalar(scalar) {
this.x += scalar;
this.y += scalar;
this.z += scalar;
this.w += scalar;
}
addScalarNew(scalar) {
let vec = this.copy();
vec.x += scalar;
vec.y += scalar;
vec.z += scalar;
vec.w += scalar;
return vec;
}
sub(other) {
this.x -= other.x;
this.y -= other.y;
this.z -= other.z;
this.w -= other.w;
}
subNew(other) {
let vec = this.copy();
vec.x -= other.x;
vec.y -= other.y;
vec.z -= other.z;
vec.w -= other.w;
return vec;
}
multScalar(scalar) {
this.x *= scalar;
this.y *= scalar;
this.z *= scalar;
this.w *= scalar;
}
multScalarNew(scalar) {
let vec = this.copy();
vec.x *= scalar;
vec.y *= scalar;
vec.z *= scalar;
vec.w *= scalar;
return vec;
}
div(other) {
if (other.x == 0 ||
other.y == 0 ||
other.z == 0 ||
other.w == 0) {
throw new Error("Division by zero in Vec4");
}
this.x /= other.x;
this.y /= other.y;
this.z /= other.z;
this.w /= other.w;
}
divNew(other) {
if (other.x == 0 ||
other.y == 0 ||
other.z == 0 ||
other.w == 0) {
throw new Error("Division by zero in Vec4");
}
let vec = this.copy();
vec.x /= other.x;
vec.y /= other.y;
vec.z /= other.z;
vec.w /= other.w;
return vec;
}
reduce() {
return new Vec3(this.x, this.y, this.z);
}
extend(_value) {
throw new Error("Can't extend Vec4");
}
}
export class Mat4 {
data;
constructor(i) {
if (i instanceof Float32Array) {
console.assert(i.length == 16, "Mat4 has to have 16 elements");
this.data = i;
return;
}
else if (typeof (i) === 'number') {
this.data = new Float32Array(16).fill(i);
return;
}
this.data = new Float32Array(16);
}
static IDENTITY() {
return new Mat4(new Float32Array([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]));
}
static orthographic(left, right, bottom, top, near, far) {
let data = new Float32Array([
2 / (right - left), 0, 0, 0,
0, 2 / (top - bottom), 0, 0,
0, 0, -2 / (far - near), 0,
-(right + left) / (right - left),
-(top + bottom) / (top - bottom),
-(far + near) / (far - near),
1,
]);
return new Mat4(data);
}
static isometric() {
return new Mat4(new Float32Array([
1, -1, 0, 0,
1, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]));
}
static isometric_inverse() {
return new Mat4(new Float32Array([
0.5, -0.5, 0, 0,
0.5, 0.5, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]));
}
static rotation_x(angle) {
let data = new Float32Array([
1, 0, 0, 0,
0, Math.cos(angle), -Math.sin(angle), 0,
0, Math.sin(angle), Math.cos(angle), 0,
0, 0, 0, 1,
]);
return new Mat4(data);
}
static rotation_y(angle) {
let data = new Float32Array([
Math.cos(angle), 0, Math.sin(angle), 0,
0, 1, 0, 0,
-Math.sin(angle), 0, Math.cos(angle), 0,
0, 0, 0, 1,
]);
return new Mat4(data);
}
static rotation_z(angle) {
let data = new Float32Array([
Math.cos(angle), -Math.sin(angle), 0, 0,
Math.sin(angle), Math.cos(angle), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]);
return new Mat4(data);
}
static translate(t) {
return new Mat4(new Float32Array([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
t.x, t.y, t.z, 1,
]));
}
static scale(scales) {
return new Mat4(new Float32Array([
scales.x, 0, 0, 0,
0, scales.y, 0, 0,
0, 0, scales.z, 0,
0, 0, 0, 1,
]));
}
x(n) {
return this.data[n];
}
y(n) {
return this.data[n + 4];
}
z(n) {
return this.data[n + 8];
}
w(n) {
return this.data[n + 12];
}
splat() {
return Array.from(this.data);
}
row(row, data) {
for (let i = 0; i < data.length; ++i)
this.data[i + 4 * row] = data[i];
}
col(col, data) {
for (let i = 0; i < data.length; ++i)
this.data[i * 4 + col] = data[i];
}
transform(v) {
let x = v.x * this.x(0) + v.y * this.y(0) + v.z * this.z(0) + v.w * this.w(0);
let y = v.x * this.x(1) + v.y * this.y(1) + v.z * this.z(1) + v.w * this.w(1);
let z = v.x * this.x(2) + v.y * this.y(2) + v.z * this.z(2) + v.w * this.w(2);
let w = v.x * this.x(3) + v.y * this.y(3) + v.z * this.z(3) + v.w * this.w(3);
v.x = x;
v.y = y;
v.z = z;
v.w = w;
}
transformNew(v) {
let vec = v.copy();
let x = v.x * this.x(0) + v.y * this.y(0) + v.z * this.z(0) + v.w * this.w(0);
let y = v.x * this.x(1) + v.y * this.y(1) + v.z * this.z(1) + v.w * this.w(1);
let z = v.x * this.x(2) + v.y * this.y(2) + v.z * this.z(2) + v.w * this.w(2);
let w = v.x * this.x(3) + v.y * this.y(3) + v.z * this.z(3) + v.w * this.w(3);
vec.x = x;
vec.y = y;
vec.z = z;
vec.w = w;
return vec;
}
multNew(other) {
let m = new Mat4(0);
for (let i = 0; i < 4; ++i) {
for (let j = 0; j < 4; ++j) {
m.data[(i * 4) + j] += this.data[(i * 4) + 0] * other.data[j + 0];
m.data[(i * 4) + j] += this.data[(i * 4) + 1] * other.data[j + 4];
m.data[(i * 4) + j] += this.data[(i * 4) + 2] * other.data[j + 8];
m.data[(i * 4) + j] += this.data[(i * 4) + 3] * other.data[j + 12];
}
}
return m;
}
multScalar(scalar) {
for (let i = 0; i < this.data.length; ++i) {
this.data[i] *= scalar;
}
}
}

629
src/js/common.ts Normal file
View File

@ -0,0 +1,629 @@
export function initializeContext(canvasId: string): WebGL2RenderingContext | null {
const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
const ctx = canvas.getContext("webgl2", {antialias: false});
return ctx;
}
// TODO: Make all vectors follow one interface
interface Vector<P, T, N> {
copy(): T;
from(scalar: number): T;
add(other: T): void;
addNew(other: T): T;
addScalar(scalar: number): void;
addScalarNew(scalar: number): T;
sub(other: T): void;
subNew(other: T): T;
multScalar(scalar: number): void;
multScalarNew(scalar: number): T;
div(other: T): void;
divNew(other: T): T;
reduce(): P;
extend(value: number): N;
}
export class Vec2 implements Vector<void, Vec2, Vec3> {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
from(scalar: number): Vec2 {
return new Vec2(scalar, scalar);
}
static ZERO(): Vec2 {
return new Vec2(0, 0);
}
static ID(): Vec2 {
return new Vec2(1, 1);
}
copy(): Vec2 {
return new Vec2(this.x, this.y);
}
add(other: Vec2) {
this.x += other.x;
this.y += other.y;
}
addScalar(scalar: number) {
this.x += scalar;
this.y += scalar;
}
addNew(other: Vec2): Vec2 {
return new Vec2(this.x + other.x, this.y + other.y);
}
addScalarNew(scalar: number): Vec2 {
return new Vec2(this.x + scalar, this.y + scalar);
}
sub(other: Vec2) {
this.x -= other.x;
this.y -= other.y;
}
subNew(other: Vec2): Vec2 {
let vec = this.copy();
vec.x -= other.x;
vec.y -= other.y;
return vec;
}
multScalar(scalar: number) {
this.x *= scalar;
this.y *= scalar;
}
multScalarNew(scalar: number): Vec2 {
return new Vec2(this.x * scalar, this.y * scalar);
}
div(other: Vec2) {
if ( other.x == 0 ||
other.y == 0)
{
throw new Error("Division by zero in Vec4");
}
this.x /= other.x;
this.y /= other.y;
}
divNew(other: Vec2): Vec2 {
if ( other.x == 0 ||
other.y == 0)
{
throw new Error("Division by zero in Vec4");
}
let vec = this.copy();
vec.x /= other.x;
vec.y /= other.y;
return vec;
}
reduce(): null {
throw new Error("Can't reduce Vec2!");
}
extend(value: number): Vec3 {
return new Vec3(this.x, this.y, value);
}
splatToArray(): Array<number> {
return [this.x, this.y];
}
static angle(angle: number): Vec2 {
const eps = 1e-6;
let x = Math.cos(angle);
let y = Math.sin(angle);
if ((x > 0 && x < eps)
|| (x < 0 && x > -eps))
x = 0;
if ((y > 0 && y < eps)
|| (y < 0 && y > -eps))
y = 0;
return new Vec2(x, y);
}
}
export class Vec3 implements Vector<Vec2, Vec3, Vec4> {
x: number;
y: number;
z: number;
constructor(x: number, y: number, z: number) {
this.x = x;
this.y = y;
this.z = z;
}
from(scalar: number): Vec3 {
return new Vec3(scalar, scalar, scalar);
}
addScalar(scalar: number): void {
this.x += scalar;
this.y += scalar;
this.z += scalar;
}
addScalarNew(scalar: number): Vec3 {
let vec = this.copy();
vec.x += scalar;
vec.y += scalar;
vec.z += scalar;
return vec;
}
static ZERO(): Vec3 {
return new Vec3(0, 0, 0);
}
static ID(): Vec3 {
return new Vec3(1, 1, 1);
}
copy(): Vec3 {
return new Vec3(this.x, this.y, this.z);
}
add(other: Vec3) {
this.x += other.x;
this.y += other.y;
this.z += other.z;
}
addNew(other: Vec3): Vec3 {
let vec = this.copy();
vec.x += other.x;
vec.y += other.y;
vec.z += other.z;
return vec;
}
sub(other: Vec3) {
this.x -= other.x;
this.y -= other.y;
this.z -= other.z;
}
subNew(other: Vec3): Vec3 {
let vec = this.copy();
vec.x -= other.x;
vec.y -= other.y;
vec.z -= other.z;
return vec;
}
multScalar(scalar: number) {
this.x *= scalar;
this.y *= scalar;
this.z *= scalar;
}
multScalarNew(scalar: number): Vec3 {
let vec = this.copy();
vec.x *= scalar;
vec.y *= scalar;
vec.z *= scalar;
return vec;
}
div(other: Vec3) {
if ( other.x == 0 ||
other.y == 0 ||
other.z == 0)
{
throw new Error("Division by zero in Vec4");
}
this.x /= other.x;
this.y /= other.y;
this.z /= other.z;
}
divNew(other: Vec3): Vec3 {
if ( other.x == 0 ||
other.y == 0 ||
other.z == 0)
{
throw new Error("Division by zero in Vec4");
}
let vec = this.copy();
vec.x /= other.x;
vec.y /= other.y;
vec.z /= other.z;
return vec;
}
reduce(): Vec2 {
return new Vec2(this.x, this.y);
}
extend(value: number): Vec4 {
return new Vec4(this.x, this.y, this.z, value);
}
splatToArray(): Array<number> {
return [this.x, this.y, this.z];
}
}
export class Vec4 implements Vector<Vec3, Vec4, void> {
x: number;
y: number;
z: number;
w: number;
constructor(x: number, y: number, z: number, w: number) {
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
from(scalar: number): Vec4 {
return new Vec4(scalar, scalar, scalar, scalar);
}
static ZERO(): Vec4 {
return new Vec4(0, 0, 0, 0);
}
static ID(): Vec4 {
return new Vec4(1, 1, 1, 1);
}
copy(): Vec4 {
return new Vec4(this.x, this.y, this.z, this.w);
}
add(other: Vec4) {
this.x += other.x;
this.y += other.y;
this.z += other.z;
this.w += other.w;
}
addNew(other: Vec4): Vec4 {
let vec = this.copy();
vec.x += other.x;
vec.y += other.y;
vec.z += other.z;
vec.w += other.w;
return vec;
}
addScalar(scalar: number): void {
this.x += scalar;
this.y += scalar;
this.z += scalar;
this.w += scalar;
}
addScalarNew(scalar: number): Vec4 {
let vec = this.copy();
vec.x += scalar;
vec.y += scalar;
vec.z += scalar;
vec.w += scalar;
return vec;
}
sub(other: Vec4) {
this.x -= other.x;
this.y -= other.y;
this.z -= other.z;
this.w -= other.w;
}
subNew(other: Vec4): Vec4 {
let vec = this.copy();
vec.x -= other.x;
vec.y -= other.y;
vec.z -= other.z;
vec.w -= other.w;
return vec;
}
multScalar(scalar: number) {
this.x *= scalar;
this.y *= scalar;
this.z *= scalar;
this.w *= scalar;
}
multScalarNew(scalar: number): Vec4 {
let vec = this.copy();
vec.x *= scalar;
vec.y *= scalar;
vec.z *= scalar;
vec.w *= scalar;
return vec;
}
div(other: Vec4) {
if ( other.x == 0 ||
other.y == 0 ||
other.z == 0 ||
other.w == 0)
{
throw new Error("Division by zero in Vec4");
}
this.x /= other.x;
this.y /= other.y;
this.z /= other.z;
this.w /= other.w;
}
divNew(other: Vec4): Vec4 {
if ( other.x == 0 ||
other.y == 0 ||
other.z == 0 ||
other.w == 0)
{
throw new Error("Division by zero in Vec4");
}
let vec = this.copy();
vec.x /= other.x;
vec.y /= other.y;
vec.z /= other.z;
vec.w /= other.w;
return vec;
}
reduce(): Vec3 {
return new Vec3(this.x, this.y, this.z);
}
extend(_value: number): void {
throw new Error("Can't extend Vec4");
}
}
type Mat4Init = number | Float32Array;
export type Mat4Row = 0 | 1 | 2 | 3
export type Mat4Col = Mat4Row
export type Mat4X = Mat4Row
export type Mat4Y = Mat4Row
export type Mat4Z = Mat4Row
export type Mat4W = Mat4Row
export class Mat4 {
data: Float32Array;
constructor(i: Mat4Init) {
if (i instanceof Float32Array) {
console.assert(i.length == 16, "Mat4 has to have 16 elements");
this.data = i;
return;
} else if (typeof(i) === 'number') {
this.data = new Float32Array(16).fill(i);
return;
}
this.data = new Float32Array(16);
}
static IDENTITY(): Mat4 {
return new Mat4(new Float32Array([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]));
}
static orthographic(left: number, right: number, bottom: number, top: number, near: number, far: number): Mat4 {
let data = new Float32Array([
2 / (right - left), 0, 0, 0,
0, 2 / (top - bottom), 0, 0,
0, 0, -2 / (far - near), 0,
-(right + left)/(right - left),
-(top + bottom)/(top - bottom),
-(far + near)/(far - near),
1,
]);
return new Mat4(data);
}
static isometric(): Mat4 {
return new Mat4(new Float32Array([
1, -1, 0, 0,
1, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]));
}
static isometric_inverse(): Mat4 {
return new Mat4(new Float32Array([
0.5, -0.5, 0, 0,
0.5, 0.5, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]));
}
static rotation_x(angle: number): Mat4 {
let data = new Float32Array([
1, 0, 0, 0,
0, Math.cos(angle), -Math.sin(angle), 0,
0, Math.sin(angle), Math.cos(angle), 0,
0, 0, 0, 1,
]);
return new Mat4(data);
}
static rotation_y(angle: number): Mat4 {
let data = new Float32Array([
Math.cos(angle), 0, Math.sin(angle), 0,
0, 1, 0, 0,
-Math.sin(angle), 0, Math.cos(angle), 0,
0, 0, 0, 1,
]);
return new Mat4(data);
}
static rotation_z(angle: number): Mat4 {
let data = new Float32Array([
Math.cos(angle), -Math.sin(angle), 0, 0,
Math.sin(angle), Math.cos(angle), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]);
return new Mat4(data);
}
static translate(t: Vec3): Mat4 {
return new Mat4(new Float32Array([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
t.x, t.y, t.z, 1,
]));
}
static scale(scales: Vec3) {
return new Mat4(new Float32Array([
scales.x, 0, 0, 0,
0, scales.y, 0, 0,
0, 0, scales.z, 0,
0, 0, 0, 1,
]));
}
x(n: Mat4X) {
return this.data[n];
}
y(n: Mat4Y) {
return this.data[n + 4];
}
z(n: Mat4Z) {
return this.data[n + 8];
}
w(n: Mat4W) {
return this.data[n + 12];
}
splat() {
return Array.from(this.data);
}
row(row: Mat4Row, data: [number, number, number, number]) {
for (let i = 0; i < data.length; ++i)
this.data[i + 4 * row] = data[i];
}
col(col: Mat4Col, data: [number, number, number, number]) {
for (let i = 0; i < data.length; ++i)
this.data[i*4 + col] = data[i];
}
transform(v: Vec4) {
let x = v.x * this.x(0) + v.y * this.y(0) + v.z * this.z(0) + v.w * this.w(0);
let y = v.x * this.x(1) + v.y * this.y(1) + v.z * this.z(1) + v.w * this.w(1);
let z = v.x * this.x(2) + v.y * this.y(2) + v.z * this.z(2) + v.w * this.w(2);
let w = v.x * this.x(3) + v.y * this.y(3) + v.z * this.z(3) + v.w * this.w(3);
v.x = x;
v.y = y;
v.z = z;
v.w = w;
}
transformNew(v: Vec4): Vec4 {
let vec = v.copy();
let x = v.x * this.x(0) + v.y * this.y(0) + v.z * this.z(0) + v.w * this.w(0);
let y = v.x * this.x(1) + v.y * this.y(1) + v.z * this.z(1) + v.w * this.w(1);
let z = v.x * this.x(2) + v.y * this.y(2) + v.z * this.z(2) + v.w * this.w(2);
let w = v.x * this.x(3) + v.y * this.y(3) + v.z * this.z(3) + v.w * this.w(3);
vec.x = x;
vec.y = y;
vec.z = z;
vec.w = w;
return vec;
}
multNew(other: Mat4): Mat4 {
let m = new Mat4(0);
for (let i = 0; i < 4; ++i) {
for (let j = 0; j < 4; ++j) {
m.data[(i * 4) + j] += this.data[(i * 4) + 0] * other.data[j + 0]
m.data[(i * 4) + j] += this.data[(i * 4) + 1] * other.data[j + 4]
m.data[(i * 4) + j] += this.data[(i * 4) + 2] * other.data[j + 8]
m.data[(i * 4) + j] += this.data[(i * 4) + 3] * other.data[j + 12]
}
}
return m;
}
multScalar(scalar: number) {
for (let i = 0; i < this.data.length; ++i) {
this.data[i] *= scalar;
}
}
}

182
src/js/draw.js Normal file
View File

@ -0,0 +1,182 @@
import { Vec3, Mat4, Vec4 } from "./common.js";
import { DrawTag, Sprite } from "./graphics.js";
import { TileEdge } from "./world.js";
import * as Assets from "./assets.js";
// Attrib format
// position color uv
// (3) (4) (3) => 3 + 4 + 3 = 10 <=> vertexSize == 10
export class Rectangle {
attribs = ["a_position", "a_color", "a_tex"];
tags = [];
data = [];
vertexSize = 10;
sprites = Assets.assets.get("sprites");
constructor(tags, sprites, attribs) {
if (sprites !== undefined) {
this.sprites = sprites;
}
if (attribs !== undefined) {
this.attribs = attribs;
}
if (tags !== undefined) {
this.tags = tags;
}
}
commit(gfx) {
gfx.toRender.push(this);
}
draw(corners, fill) {
if (fill instanceof Sprite) {
let uvs = Assets.assets.get("sprites").getUVs(fill.id);
let data = [
corners[0].x, corners[0].y, corners[0].z, 0, 0, 0, 0, uvs[0].x, uvs[0].y, 1,
corners[1].x, corners[1].y, corners[1].z, 0, 0, 0, 0, uvs[1].x, uvs[1].y, 1,
corners[3].x, corners[3].y, corners[3].z, 0, 0, 0, 0, uvs[3].x, uvs[3].y, 1,
corners[2].x, corners[2].y, corners[2].z, 0, 0, 0, 0, uvs[2].x, uvs[2].y, 1,
corners[1].x, corners[1].y, corners[1].z, 0, 0, 0, 0, uvs[1].x, uvs[1].y, 1,
corners[3].x, corners[3].y, corners[3].z, 0, 0, 0, 0, uvs[3].x, uvs[3].y, 1,
];
for (let i = 0; i < data.length; ++i) {
this.data.push(data[i]);
}
}
else {
let color = fill;
let data = [
corners[0].x, corners[0].y, corners[0].z, color[0], color[1], color[2], color[3], 0, 0, 0,
corners[1].x, corners[1].y, corners[1].z, color[0], color[1], color[2], color[3], 0, 0, 0,
corners[3].x, corners[3].y, corners[3].z, color[0], color[1], color[2], color[3], 0, 0, 0,
corners[2].x, corners[2].y, corners[2].z, color[0], color[1], color[2], color[3], 0, 0, 0,
corners[1].x, corners[1].y, corners[1].z, color[0], color[1], color[2], color[3], 0, 0, 0,
corners[3].x, corners[3].y, corners[3].z, color[0], color[1], color[2], color[3], 0, 0, 0,
];
for (let i = 0; i < data.length; ++i) {
this.data.push(data[i]);
}
}
}
drawExts(position, exts, fill) {
if (fill instanceof Sprite) {
let uvs = Assets.assets.get("sprites").getUVs(fill.id);
let data = [
position.x, position.y, position.z, 0, 0, 0, 0, uvs[0].x, uvs[0].y, 1,
position.x + exts.x, position.y, position.z, 0, 0, 0, 0, uvs[1].x, uvs[1].y, 1,
position.x, position.y + exts.y, position.z, 0, 0, 0, 0, uvs[3].x, uvs[3].y, 1,
position.x + exts.x, position.y + exts.y, position.z, 0, 0, 0, 0, uvs[2].x, uvs[2].y, 1,
position.x + exts.x, position.y, position.z, 0, 0, 0, 0, uvs[1].x, uvs[1].y, 1,
position.x, position.y + exts.y, position.z, 0, 0, 0, 0, uvs[3].x, uvs[3].y, 1,
];
for (let i = 0; i < data.length; ++i) {
this.data.push(data[i]);
}
}
else {
let color = fill;
let data = [
position.x, position.y, position.z, color[0], color[1], color[2], color[3], 0, 0, 0,
position.x + exts.x, position.y, position.z, color[0], color[1], color[2], color[3], 0, 0, 0,
position.x, position.y + exts.y, position.z, color[0], color[1], color[2], color[3], 0, 0, 0,
position.x + exts.x, position.y + exts.y, position.z, color[0], color[1], color[2], color[3], 0, 0, 0,
position.x + exts.x, position.y, position.z, color[0], color[1], color[2], color[3], 0, 0, 0,
position.x, position.y + exts.y, position.z, color[0], color[1], color[2], color[3], 0, 0, 0,
];
for (let i = 0; i < data.length; ++i) {
this.data.push(data[i]);
}
}
}
}
export function drawIsometricCube(position, exts, r, fill, edge) {
let points = [
// Left Top
position,
//Top
new Vec3(position.x, position.y + exts.y, position.z),
//Mid
new Vec3(position.x + exts.x, position.y, position.z),
//Right Top
new Vec3(position.x + exts.x, position.y + exts.y, position.z),
//Bottom
new Vec3(position.x + 1.5 * exts.x, position.y - 0.5 * exts.y, position.z + 0.25 * exts.z),
new Vec3(position.x + 1.5 * exts.x, position.y + 0.5 * exts.y, position.z + 0.25 * exts.z),
new Vec3(position.x + 0.5 * exts.x, position.y - 0.5 * exts.y, position.z + 0.25 * exts.z),
];
// Top
r.draw([
points[0],
points[1],
points[3],
points[2],
], fill.top);
// Right Edge
if (edge == TileEdge.Right || edge == TileEdge.Both) {
r.draw([
points[3],
points[2],
points[4],
points[5],
], fill.right);
}
// Left Edge
if (edge == TileEdge.Left || edge == TileEdge.Both) {
r.draw([
points[0],
points[2],
points[4],
points[6],
], fill.left);
}
}
// TODO: Optimize the shit out of this function
export function drawIsometricGrid(gfx, camera, grid) {
let position = grid.position.copy();
let exts = new Vec3(grid.tileSize, grid.tileSize, 0);
let tileCoord = new Vec3(0, 0, 0);
let rect = new Rectangle([
DrawTag.ISO,
]);
let mt = Mat4.translate(camera.position.multScalarNew(-1.0));
let ms = Mat4.scale(new Vec3(1 / camera.scale, 1 / camera.scale, 0));
let mc = Mat4.translate(new Vec3((gfx.ctx.canvas.width / 2 - camera.position.x), (gfx.ctx.canvas.height / 2 - camera.position.y), 1.0));
let mr = Mat4.translate(new Vec3(-(gfx.ctx.canvas.width / 2 - camera.position.x), -(gfx.ctx.canvas.height / 2 - camera.position.y), 1.0));
let mi = Mat4.isometric();
let bias = 4 * grid.tileSize;
let bb = [
new Vec3(-bias, -bias, 0),
new Vec3(gfx.width() + bias, -bias, 0),
new Vec3(gfx.width() + bias, gfx.height() + bias, 0),
new Vec3(-bias, gfx.height() + bias, 0),
];
for (let i = 0; i < bb.length; ++i) {
bb[i] = mt.transformNew(bb[i].extend(1.0)).reduce();
bb[i] = mr.transformNew(bb[i].extend(1.0)).reduce();
bb[i] = ms.transformNew(bb[i].extend(1.0)).reduce();
bb[i] = mc.transformNew(bb[i].extend(1.0)).reduce();
}
for (let k = 0; k < grid.topHeight; ++k) {
for (let j = 0; j < grid.breadth; ++j) {
for (let i = 0; i < grid.width; ++i) {
tileCoord.x = i;
tileCoord.y = j;
tileCoord.z = k;
let tile = grid.getTile(tileCoord);
if (tile === null) {
position.x += grid.tileSize;
continue;
}
const ipos = mi.transformNew(new Vec4(position.x - grid.tileSize / 2 * k, position.y + grid.tileSize / 2 * k, 1.0, 1.0)).reduce();
if (ipos.x >= bb[2].x || ipos.x <= bb[0].x ||
ipos.y >= bb[2].y || ipos.y <= bb[0].y) {
position.x += grid.tileSize;
continue;
}
drawIsometricCube(new Vec3(position.x - grid.tileSize / 2 * k, position.y + grid.tileSize / 2 * k, position.z), exts, rect, tile.fill, tile.edge);
position.x += grid.tileSize;
}
position.y -= grid.tileSize;
position.x = grid.position.x;
}
position.y = grid.position.y;
}
rect.commit(gfx);
}

244
src/js/draw.ts Normal file
View File

@ -0,0 +1,244 @@
import { Vec3, Vec2, Mat4, Vec4 } from "./common.js"
import { Graphics, Drawable, DrawTag, Sprite, Spritesheet, Camera } from "./graphics.js";
import { TileEdge, TileFill, TileFillament, Grid } from "./world.js";
import * as Assets from "./assets.js";
import {Input} from "./input.js";
// Attrib format
// position color uv
// (3) (4) (3) => 3 + 4 + 3 = 10 <=> vertexSize == 10
export class Rectangle implements Drawable {
attribs: string[] = ["a_position", "a_color", "a_tex"];
tags: Array<DrawTag> = [];
data: number[] = [];
vertexSize: number = 10;
sprites: Spritesheet = Assets.assets.get("sprites") as Spritesheet;
constructor(tags?: Array<DrawTag>, sprites?: Spritesheet, attribs?: string[]) {
if (sprites !== undefined) {
this.sprites = sprites;
}
if (attribs !== undefined) {
this.attribs = attribs;
}
if (tags !== undefined) {
this.tags = tags;
}
}
commit(gfx: Graphics) {
gfx.toRender.push(this);
}
draw(corners: [Vec3, Vec3, Vec3, Vec3], fill: TileFillament) {
if (fill instanceof Sprite) {
let uvs = (Assets.assets.get("sprites") as Spritesheet).getUVs(fill.id);
let data = [
corners[0].x, corners[0].y, corners[0].z, 0, 0, 0, 0, uvs[0].x, uvs[0].y, 1,
corners[1].x, corners[1].y, corners[1].z, 0, 0, 0, 0, uvs[1].x, uvs[1].y, 1,
corners[3].x, corners[3].y, corners[3].z, 0, 0, 0, 0, uvs[3].x, uvs[3].y, 1,
corners[2].x, corners[2].y, corners[2].z, 0, 0, 0, 0, uvs[2].x, uvs[2].y, 1,
corners[1].x, corners[1].y, corners[1].z, 0, 0, 0, 0, uvs[1].x, uvs[1].y, 1,
corners[3].x, corners[3].y, corners[3].z, 0, 0, 0, 0, uvs[3].x, uvs[3].y, 1,
];
for (let i = 0; i < data.length; ++i) {
this.data.push(data[i]);
}
} else {
let color = fill;
let data = [
corners[0].x, corners[0].y, corners[0].z, color[0], color[1], color[2], color[3], 0, 0, 0,
corners[1].x, corners[1].y, corners[1].z, color[0], color[1], color[2], color[3], 0, 0, 0,
corners[3].x, corners[3].y, corners[3].z, color[0], color[1], color[2], color[3], 0, 0, 0,
corners[2].x, corners[2].y, corners[2].z, color[0], color[1], color[2], color[3], 0, 0, 0,
corners[1].x, corners[1].y, corners[1].z, color[0], color[1], color[2], color[3], 0, 0, 0,
corners[3].x, corners[3].y, corners[3].z, color[0], color[1], color[2], color[3], 0, 0, 0,
];
for (let i = 0; i < data.length; ++i) {
this.data.push(data[i]);
}
}
}
drawExts(position: Vec3, exts: Vec2, fill: TileFillament) {
if (fill instanceof Sprite) {
let uvs = (Assets.assets.get("sprites") as Spritesheet).getUVs(fill.id);
let data = [
position.x, position.y, position.z, 0, 0, 0, 0, uvs[0].x, uvs[0].y, 1,
position.x + exts.x, position.y, position.z, 0, 0, 0, 0, uvs[1].x, uvs[1].y, 1,
position.x, position.y + exts.y, position.z, 0, 0, 0, 0, uvs[3].x, uvs[3].y, 1,
position.x + exts.x, position.y + exts.y, position.z, 0, 0, 0, 0, uvs[2].x, uvs[2].y, 1,
position.x + exts.x, position.y, position.z, 0, 0, 0, 0, uvs[1].x, uvs[1].y, 1,
position.x, position.y + exts.y, position.z, 0, 0, 0, 0, uvs[3].x, uvs[3].y, 1,
];
for (let i = 0; i < data.length; ++i) {
this.data.push(data[i]);
}
} else {
let color = fill;
let data = [
position.x, position.y, position.z, color[0], color[1], color[2], color[3], 0, 0, 0,
position.x + exts.x, position.y, position.z, color[0], color[1], color[2], color[3], 0, 0, 0,
position.x, position.y + exts.y, position.z, color[0], color[1], color[2], color[3], 0, 0, 0,
position.x + exts.x, position.y + exts.y, position.z, color[0], color[1], color[2], color[3], 0, 0, 0,
position.x + exts.x, position.y, position.z, color[0], color[1], color[2], color[3], 0, 0, 0,
position.x, position.y + exts.y, position.z, color[0], color[1], color[2], color[3], 0, 0, 0,
];
for (let i = 0; i < data.length; ++i) {
this.data.push(data[i]);
}
}
}
}
export function drawIsometricCube(position: Vec3, exts: Vec3, r: Rectangle, fill: TileFill, edge: TileEdge) {
let points = [
// Left Top
position,
//Top
new Vec3(position.x, position.y + exts.y, position.z),
//Mid
new Vec3(position.x + exts.x, position.y, position.z),
//Right Top
new Vec3(position.x + exts.x, position.y + exts.y, position.z),
//Bottom
new Vec3(position.x + 1.5 * exts.x, position.y - 0.5 * exts.y, position.z + 0.25 * exts.z),
new Vec3(position.x + 1.5 * exts.x, position.y + 0.5 * exts.y, position.z + 0.25 * exts.z),
new Vec3(position.x + 0.5 * exts.x, position.y - 0.5 * exts.y, position.z + 0.25 * exts.z),
];
// Top
r.draw(
[
points[0],
points[1],
points[3],
points[2],
],
fill.top
);
// Right Edge
if (edge == TileEdge.Right || edge == TileEdge.Both) {
r.draw(
[
points[3],
points[2],
points[4],
points[5],
],
fill.right
);
}
// Left Edge
if (edge == TileEdge.Left || edge == TileEdge.Both) {
r.draw(
[
points[0],
points[2],
points[4],
points[6],
],
fill.left
);
}
}
// TODO: Optimize the shit out of this function
export function drawIsometricGrid(gfx: Graphics, camera: Camera, grid: Grid) {
let position = grid.position.copy();
let exts = new Vec3(grid.tileSize, grid.tileSize, 0);
let tileCoord = new Vec3(0, 0, 0);
let rect = new Rectangle([
DrawTag.ISO,
]);
let mt = Mat4.translate(camera.position.multScalarNew(-1.0));
let ms = Mat4.scale(new Vec3(1 / camera.scale, 1 /camera.scale, 0));
let mc = Mat4.translate(
new Vec3(
(gfx.ctx.canvas.width / 2 - camera.position.x),
(gfx.ctx.canvas.height / 2 - camera.position.y),
1.0));
let mr = Mat4.translate(
new Vec3(
-(gfx.ctx.canvas.width / 2 - camera.position.x),
-(gfx.ctx.canvas.height / 2 - camera.position.y),
1.0));
let mi = Mat4.isometric();
let bias = 4*grid.tileSize;
let bb: [Vec3, Vec3, Vec3, Vec3] = [
new Vec3(-bias, -bias, 0),
new Vec3(gfx.width() + bias, -bias, 0),
new Vec3(gfx.width() + bias, gfx.height() + bias, 0),
new Vec3(-bias, gfx.height() + bias, 0),
];
for (let i = 0; i < bb.length; ++i) {
bb[i] = mt.transformNew(bb[i].extend(1.0)).reduce();
bb[i] = mr.transformNew(bb[i].extend(1.0)).reduce();
bb[i] = ms.transformNew(bb[i].extend(1.0)).reduce();
bb[i] = mc.transformNew(bb[i].extend(1.0)).reduce();
}
for (let k = 0; k < grid.topHeight; ++k) {
for (let j = 0; j < grid.breadth; ++j) {
for (let i = 0; i < grid.width; ++i) {
tileCoord.x = i;
tileCoord.y = j;
tileCoord.z = k;
let tile = grid.getTile(tileCoord);
if (tile === null) {
position.x += grid.tileSize;
continue;
}
const ipos = mi.transformNew(new Vec4(position.x - grid.tileSize / 2 * k, position.y + grid.tileSize / 2 * k, 1.0, 1.0)).reduce();
if (ipos.x >= bb[2].x || ipos.x <= bb[0].x ||
ipos.y >= bb[2].y || ipos.y <= bb[0].y) {
position.x += grid.tileSize;
continue;
}
drawIsometricCube(
new Vec3(position.x - grid.tileSize / 2 * k, position.y + grid.tileSize / 2 * k, position.z),
exts,
rect,
tile.fill,
tile.edge
);
position.x += grid.tileSize;
}
position.y -= grid.tileSize;
position.x = grid.position.x;
}
position.y = grid.position.y;
}
rect.commit(gfx);
}

264
src/js/graphics.js Normal file
View File

@ -0,0 +1,264 @@
import { Vec2, Vec4 } from "./common.js";
export function fullscreenCanvas(gfx, id) {
const canvas = document.getElementById(id);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
gfx.ctx.viewport(0, 0, canvas.width, canvas.height);
}
function createShader(ctx, type, source) {
var shader = ctx.createShader(type);
if (!shader) {
throw new Error("Couldn't create shader: " + type);
}
ctx.shaderSource(shader, source);
ctx.compileShader(shader);
var success = ctx.getShaderParameter(shader, ctx.COMPILE_STATUS);
if (!success) {
console.log(ctx.getShaderInfoLog(shader));
ctx.deleteShader(shader);
throw new Error("Couldn't compile shader: " + type);
}
return shader;
}
function createProgram(ctx, vertexShaderSource, fragmentShaderSource) {
var program = ctx.createProgram();
if (vertexShaderSource !== null) {
const vs = createShader(ctx, ctx.VERTEX_SHADER, vertexShaderSource);
ctx.attachShader(program, vs);
}
if (fragmentShaderSource !== null) {
const fs = createShader(ctx, ctx.FRAGMENT_SHADER, fragmentShaderSource);
ctx.attachShader(program, fs);
}
ctx.linkProgram(program);
var success = ctx.getProgramParameter(program, ctx.LINK_STATUS);
if (!success) {
console.log(ctx.getProgramInfoLog(program));
ctx.deleteProgram(program);
throw new Error("Failed to create program!");
}
return program;
}
export var DrawTag;
(function (DrawTag) {
DrawTag[DrawTag["ISO"] = 0] = "ISO";
})(DrawTag || (DrawTag = {}));
export class Graphics {
ctx;
program;
attribs = new Map();
uniforms = new Map();
vao;
vbo;
toRender = [];
texCount = 0;
width() {
return this.ctx.canvas.width;
}
height() {
return this.ctx.canvas.height;
}
constructor(ctx, vs, fs) {
this.ctx = ctx;
this.program = createProgram(ctx, vs, fs);
this.vao = ctx.createVertexArray();
this.vbo = ctx.createBuffer();
ctx.bindVertexArray(this.vao);
ctx.viewport(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.useProgram(this.program);
ctx.blendFunc(ctx.SRC_ALPHA, ctx.ONE_MINUS_SRC_ALPHA);
ctx.enable(ctx.BLEND);
}
clear(r, g, b, a) {
this.ctx.clearColor(r, g, b, a);
this.ctx.clear(this.ctx.COLOR_BUFFER_BIT);
}
createAttribute(name) {
const attrib = new Attribute(this.ctx, this.program, name);
this.attribs.set(name, attrib);
return attrib;
}
getAttribute(name) {
const attrib = this.attribs.get(name);
if (attrib === undefined)
throw new Error("Tried to get uninitialized attribute: " + name);
return attrib;
}
createUniform(name) {
const loc = this.ctx.getUniformLocation(this.program, name);
if (loc === null)
throw new Error("Couldn't get location for uniform: " + name);
this.uniforms.set(name, loc);
}
getUniform(name) {
const loc = this.uniforms.get(name);
if (loc === undefined)
throw new Error("Tried to get uninitialized uniform: " + name);
return loc;
}
draw() {
for (let o of this.toRender) {
const data = o.data;
this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this.vbo);
this.ctx.bufferData(this.ctx.ARRAY_BUFFER, new Float32Array(data), this.ctx.STATIC_DRAW);
let aid = 0;
for (let a of o.attribs) {
let attr = this.getAttribute(a);
if (!attr.formatted)
throw new Error("Tried to use unformatted attribute!");
this.ctx.enableVertexAttribArray(attr.loc);
this.ctx.vertexAttribPointer(attr.loc, attr.size, attr.type, attr.normalized, o.vertexSize * 4, aid * 4);
aid += attr.size;
}
// Generalize the tag uniforms aka. don't hard code them
for (let t of o.tags) {
switch (t) {
case DrawTag.ISO: {
this.ctx.uniform1ui(this.getUniform("u_isIso"), 1);
break;
}
}
}
o.sprites?.bind(this);
this.ctx.drawArrays(this.ctx.TRIANGLES, 0, data.length / o.vertexSize);
o.sprites?.unbind(this);
for (let t of o.tags) {
switch (t) {
case DrawTag.ISO: {
this.ctx.uniform1ui(this.getUniform("u_isIso"), 0);
break;
}
}
}
}
// TODO: Maybe add persistent rendering?
this.toRender = [];
}
}
export class Attribute {
loc;
formatted = false;
size = 0;
type = 0;
offset = 0;
normalized = false;
constructor(ctx, program, name) {
this.loc = ctx.getAttribLocation(program, name);
}
format(size, type, normalized, offset) {
this.size = size;
this.type = type;
this.normalized = normalized;
this.offset = offset;
this.formatted = true;
}
}
export class Texture {
tex;
texId;
width = 0;
height = 0;
constructor(texId, tex, width, height) {
this.height = height;
this.width = width;
this.tex = tex;
this.texId = texId;
}
// TODO: Load sprite sheet only once
// TODO: Allow changing sprite size
static async load(gfx, path) {
let image = await loadTexture(path);
let tex = gfx.ctx.createTexture();
gfx.ctx.bindTexture(gfx.ctx.TEXTURE_2D, tex);
gfx.ctx.texImage2D(gfx.ctx.TEXTURE_2D, 0, gfx.ctx.RGBA, gfx.ctx.RGBA, gfx.ctx.UNSIGNED_BYTE, image);
gfx.ctx.texParameteri(gfx.ctx.TEXTURE_2D, gfx.ctx.TEXTURE_MAG_FILTER, gfx.ctx.NEAREST);
gfx.ctx.texParameteri(gfx.ctx.TEXTURE_2D, gfx.ctx.TEXTURE_MIN_FILTER, gfx.ctx.NEAREST);
gfx.ctx.bindTexture(gfx.ctx.TEXTURE_2D, null);
gfx.texCount += 1;
return new Texture(gfx.texCount, tex, image.width, image.height);
}
bind(gfx) {
gfx.ctx.bindTexture(gfx.ctx.TEXTURE_2D, this.tex);
}
unbind(gfx) {
gfx.ctx.bindTexture(gfx.ctx.TEXTURE_2D, null);
}
}
export class Sprite {
id = 0;
constructor(id) {
this.id = id;
}
static id(id) {
return new Sprite(id);
}
static tile(id) {
let s = new Sprite(id);
return {
left: s,
top: s,
right: s,
};
}
}
;
export class Spritesheet {
texture; // Texture is a horizontal spritesheet
spriteSize; // width and height of one sprite
spriteCount; // number of sprites
constructor(texture, spriteSize) {
this.texture = texture;
this.spriteSize = spriteSize;
this.spriteCount = texture.width / spriteSize.x;
}
getUVs(id) {
return [
new Vec2((this.spriteSize.x * id) / this.texture.width, 0),
new Vec2((this.spriteSize.x * (id + 1)) / this.texture.width, 0),
new Vec2((this.spriteSize.x * (id + 1)) / this.texture.width, -1),
new Vec2((this.spriteSize.x * id) / this.texture.width, -1),
];
}
bind(gfx) {
this.texture.bind(gfx);
}
unbind(gfx) {
this.texture.unbind(gfx);
}
}
async function loadTexture(path) {
return new Promise(resolve => {
const img = new Image();
img.addEventListener("load", () => {
resolve(img);
});
img.src = path;
});
}
export class Camera {
dt = 0;
position;
movement;
speed = 600;
scale = 1.0;
scaling;
scaleSpeed = 1.5;
constructor(position) {
this.position = position;
this.movement = new Vec4(0, 0, 0, 0);
this.scaling = new Vec2(0, 0);
}
update(dt) {
this.dt = dt;
let newPosition = this.movement.multScalarNew(this.dt);
this.position.x += (newPosition.x + newPosition.y) * this.speed;
this.position.y += (newPosition.z + newPosition.w) * this.speed;
this.scale += (this.scaling.x + this.scaling.y) * this.dt * this.scaleSpeed;
if (this.scale < 0.5) {
this.scale = 0.5;
}
if (this.scale > 1.5) {
this.scale = 1.5;
}
}
}

353
src/js/graphics.ts Normal file
View File

@ -0,0 +1,353 @@
import {Vec2, Vec3, Vec4} from "./common.js";
import { TileFillament, TileFill } from "./world.js";
export function fullscreenCanvas(gfx: Graphics, id: string) {
const canvas = document.getElementById(id) as HTMLCanvasElement;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
gfx.ctx.viewport(0, 0, canvas.width, canvas.height);
}
function createShader(ctx: WebGL2RenderingContext, type: GLenum, source: string): WebGLShader {
var shader = ctx.createShader(type);
if (!shader) {
throw new Error("Couldn't create shader: " + type);
}
ctx.shaderSource(shader, source);
ctx.compileShader(shader);
var success = ctx.getShaderParameter(shader, ctx.COMPILE_STATUS);
if (!success) {
console.log(ctx.getShaderInfoLog(shader));
ctx.deleteShader(shader);
throw new Error("Couldn't compile shader: " + type);
}
return shader;
}
function createProgram(ctx: WebGL2RenderingContext, vertexShaderSource: string | null, fragmentShaderSource: string | null): WebGLProgram {
var program = ctx.createProgram();
if (vertexShaderSource !== null) {
const vs = createShader(ctx, ctx.VERTEX_SHADER, vertexShaderSource);
ctx.attachShader(program, vs);
}
if (fragmentShaderSource !== null) {
const fs = createShader(ctx, ctx.FRAGMENT_SHADER, fragmentShaderSource);
ctx.attachShader(program, fs);
}
ctx.linkProgram(program);
var success = ctx.getProgramParameter(program, ctx.LINK_STATUS);
if (!success) {
console.log(ctx.getProgramInfoLog(program));
ctx.deleteProgram(program);
throw new Error("Failed to create program!");
}
return program;
}
export enum DrawTag {
ISO,
}
export interface Drawable {
attribs: string[];
tags: Array<DrawTag>;
data: number[];
vertexSize: number;
sprites: Spritesheet | undefined;
}
export class Graphics {
ctx: WebGL2RenderingContext;
program: WebGLProgram;
attribs: Map<string, Attribute> = new Map();
uniforms: Map<string, WebGLUniformLocation> = new Map();
vao: WebGLVertexArrayObject;
vbo: WebGLBuffer;
toRender: Array<Drawable> = [];
texCount: number = 0;
width(): number {
return this.ctx.canvas.width;
}
height(): number {
return this.ctx.canvas.height;
}
constructor(ctx: WebGL2RenderingContext, vs: string, fs: string) {
this.ctx = ctx;
this.program = createProgram(ctx, vs, fs);
this.vao = ctx.createVertexArray();
this.vbo = ctx.createBuffer();
ctx.bindVertexArray(this.vao);
ctx.viewport(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.useProgram(this.program);
ctx.blendFunc(ctx.SRC_ALPHA, ctx.ONE_MINUS_SRC_ALPHA);
ctx.enable(ctx.BLEND);
}
clear(r: number, g: number, b: number, a: number) {
this.ctx.clearColor(r, g, b, a);
this.ctx.clear(this.ctx.COLOR_BUFFER_BIT);
}
createAttribute(name: string): Attribute {
const attrib = new Attribute(this.ctx, this.program, name);
this.attribs.set(name, attrib);
return attrib;
}
getAttribute(name: string): Attribute {
const attrib = this.attribs.get(name);
if (attrib === undefined)
throw new Error("Tried to get uninitialized attribute: " + name);
return attrib;
}
createUniform(name: string) {
const loc = this.ctx.getUniformLocation(this.program, name);
if (loc === null)
throw new Error("Couldn't get location for uniform: " + name);
this.uniforms.set(name, loc);
}
getUniform(name: string): WebGLUniformLocation {
const loc = this.uniforms.get(name);
if (loc === undefined)
throw new Error("Tried to get uninitialized uniform: " + name);
return loc;
}
draw() {
for (let o of this.toRender) {
const data = o.data;
this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this.vbo);
this.ctx.bufferData(this.ctx.ARRAY_BUFFER, new Float32Array(data), this.ctx.STATIC_DRAW);
let aid = 0;
for (let a of o.attribs) {
let attr = this.getAttribute(a);
if (!attr.formatted)
throw new Error("Tried to use unformatted attribute!");
this.ctx.enableVertexAttribArray(attr.loc);
this.ctx.vertexAttribPointer(attr.loc, attr.size, attr.type, attr.normalized, o.vertexSize * 4, aid * 4);
aid += attr.size;
}
// Generalize the tag uniforms aka. don't hard code them
for (let t of o.tags) {
switch (t) {
case DrawTag.ISO: {
this.ctx.uniform1ui(this.getUniform("u_isIso"), 1);
break;
}
}
}
o.sprites?.bind(this);
this.ctx.drawArrays(this.ctx.TRIANGLES, 0, data.length / o.vertexSize);
o.sprites?.unbind(this);
for (let t of o.tags) {
switch (t) {
case DrawTag.ISO: {
this.ctx.uniform1ui(this.getUniform("u_isIso"), 0);
break;
}
}
}
}
// TODO: Maybe add persistent rendering?
this.toRender = [];
}
}
export class Attribute {
loc: GLint;
formatted: boolean = false;
size: GLint = 0;
type: GLenum = 0;
offset: GLintptr = 0;
normalized: GLboolean = false;
constructor(ctx: WebGL2RenderingContext, program: WebGLProgram, name: string) {
this.loc = ctx.getAttribLocation(program, name);
}
format(
size: GLint,
type: GLenum,
normalized: GLboolean,
offset: GLintptr)
{
this.size = size;
this.type = type;
this.normalized = normalized;
this.offset = offset;
this.formatted = true;
}
}
export class Texture {
tex: WebGLTexture | null;
texId: number;
width: number = 0;
height: number = 0;
constructor(texId: number, tex: WebGLTexture, width: number, height: number) {
this.height = height;
this.width = width;
this.tex = tex;
this.texId = texId;
}
// TODO: Load sprite sheet only once
// TODO: Allow changing sprite size
static async load(gfx: Graphics, path: string): Promise<Texture> {
let image = await loadTexture(path);
let tex = gfx.ctx.createTexture();
gfx.ctx.bindTexture(gfx.ctx.TEXTURE_2D, tex);
gfx.ctx.texImage2D(gfx.ctx.TEXTURE_2D, 0, gfx.ctx.RGBA, gfx.ctx.RGBA, gfx.ctx.UNSIGNED_BYTE, image);
gfx.ctx.texParameteri(gfx.ctx.TEXTURE_2D, gfx.ctx.TEXTURE_MAG_FILTER, gfx.ctx.NEAREST);
gfx.ctx.texParameteri(gfx.ctx.TEXTURE_2D, gfx.ctx.TEXTURE_MIN_FILTER, gfx.ctx.NEAREST);
gfx.ctx.bindTexture(gfx.ctx.TEXTURE_2D, null);
gfx.texCount += 1;
return new Texture(gfx.texCount, tex, image.width, image.height);
}
bind(gfx: Graphics) {
gfx.ctx.bindTexture(gfx.ctx.TEXTURE_2D, this.tex);
}
unbind(gfx: Graphics) {
gfx.ctx.bindTexture(gfx.ctx.TEXTURE_2D, null);
}
}
export type SpriteId = number;
export class Sprite {
id: SpriteId = 0;
private constructor(id: SpriteId) {
this.id = id;
}
static id(id: SpriteId): Sprite {
return new Sprite(id);
}
static tile(id: SpriteId): TileFill {
let s = new Sprite(id);
return {
left: s,
top: s,
right: s,
}
}
};
export class Spritesheet {
texture: Texture; // Texture is a horizontal spritesheet
spriteSize: Vec2; // width and height of one sprite
spriteCount: number; // number of sprites
constructor(texture: Texture, spriteSize: Vec2) {
this.texture = texture;
this.spriteSize = spriteSize;
this.spriteCount = texture.width / spriteSize.x;
}
getUVs(id: number): [Vec2, Vec2, Vec2, Vec2] {
return [
new Vec2((this.spriteSize.x * id) / this.texture.width, 0),
new Vec2((this.spriteSize.x * (id + 1)) / this.texture.width, 0),
new Vec2((this.spriteSize.x * (id + 1)) / this.texture.width, -1),
new Vec2((this.spriteSize.x * id) / this.texture.width, -1),
];
}
bind(gfx: Graphics) {
this.texture.bind(gfx);
}
unbind(gfx: Graphics) {
this.texture.unbind(gfx);
}
}
async function loadTexture(path: string): Promise<HTMLImageElement> {
return new Promise(resolve => {
const img = new Image();
img.addEventListener("load", () => {
resolve(img);
})
img.src = path;
});
}
export class Camera {
dt: number = 0;
position: Vec3;
movement: Vec4;
speed: number = 600;
scale: number = 1.0;
scaling: Vec2;
scaleSpeed: number = 1.5;
constructor(position: Vec3) {
this.position = position;
this.movement = new Vec4(0, 0, 0, 0);
this.scaling = new Vec2(0, 0);
}
update(dt: number) {
this.dt = dt;
let newPosition = this.movement.multScalarNew(this.dt);
this.position.x += (newPosition.x + newPosition.y) * this.speed;
this.position.y += (newPosition.z + newPosition.w) * this.speed;
this.scale += (this.scaling.x + this.scaling.y) * this.dt * this.scaleSpeed;
if (this.scale < 0.5) {
this.scale = 0.5;
}
if (this.scale > 1.5) {
this.scale = 1.5;
}
}
}

41
src/js/input.js Normal file
View File

@ -0,0 +1,41 @@
import { Vec2 } from "./common.js";
class KeyAction {
key;
keyup;
keydown;
data;
constructor(key, ku, kd, data) {
this.key = key;
this.keyup = ku;
this.keydown = kd;
this.data = data;
}
}
export class Input {
handlers = new Map();
mousePosition = Vec2.ZERO();
constructor() {
window.addEventListener("keyup", e => {
this.handlers.forEach(ka => {
if (ka.keyup !== null && ka.key == e.code) {
ka.keyup(ka.data);
}
});
});
window.addEventListener("keydown", e => {
this.handlers.forEach(ka => {
if (ka.keydown !== null && ka.key == e.code) {
ka.keydown(ka.data);
}
});
});
window.addEventListener("mousemove", (e) => {
this.mousePosition.x = e.x;
this.mousePosition.y = e.y;
});
}
//TODO: add modifier key support
addKeyAction(key, modifiers, data, keydown, keyup) {
this.handlers.set(key, new KeyAction(key, keydown, keyup, data));
}
}

47
src/js/input.ts Normal file
View File

@ -0,0 +1,47 @@
import { Vec2 } from "./common.js";
type Action = ((data: any) => void);
class KeyAction {
key: string;
keyup: Action | null;
keydown: Action | null;
data: any;
constructor(key: string, ku: Action | null, kd: Action | null, data: any) {
this.key = key;
this.keyup = ku;
this.keydown = kd;
this.data = data;
}
}
export class Input {
handlers: Map<string, KeyAction> = new Map();
mousePosition: Vec2 = Vec2.ZERO();
constructor() {
window.addEventListener("keyup", e => {
this.handlers.forEach(ka => {
if (ka.keyup !== null && ka.key == e.code) {
ka.keyup(ka.data);
}
});
});
window.addEventListener("keydown", e => {
this.handlers.forEach(ka => {
if (ka.keydown !== null && ka.key == e.code) {
ka.keydown(ka.data);
}
});
});
window.addEventListener("mousemove", (e) => {
this.mousePosition.x = e.x;
this.mousePosition.y = e.y;
})
}
//TODO: add modifier key support
addKeyAction(key: string, modifiers: [string] | [], data: any, keydown: Action | null, keyup: Action | null) {
this.handlers.set(key, new KeyAction(key, keydown, keyup, data));
}
}

171
src/js/script.js Normal file
View File

@ -0,0 +1,171 @@
import { initializeContext, Vec3, Mat4, Vec4 } from "./common.js";
import { Graphics, fullscreenCanvas, Camera, Sprite } from "./graphics.js";
import * as drawing from "./draw.js";
import * as wasm from "./wasm.js";
import { Input } from "./input.js";
import { Grid, Tile, tree } from "./world.js";
import * as Assets from "./assets.js";
const vertexShader = `#version 300 es
layout(location = 0) in vec3 a_position;
layout(location = 1) in vec3 a_tex;
layout(location = 2) in vec4 a_color;
out vec4 v_color;
out vec3 v_tex;
uniform mat4 u_matrix;
uniform bool u_isIso;
mat4 Iso = mat4(
1, -1, 0, 0,
1, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
);
void main() {
vec4 orthographic;
if (u_isIso) {
vec4 isometric = Iso * vec4(a_position.xyz, 1.0);
orthographic = u_matrix * isometric;
//orthographic = u_matrix * vec4(a_position.xyz, 1.0);
} else {
orthographic = u_matrix * vec4(a_position.xyz, 1.0);
}
gl_Position = orthographic;
v_tex = a_tex;
v_color = a_color;
}
`;
const fragmentShader = `#version 300 es
precision highp float;
in vec4 v_color;
in vec3 v_tex;
out vec4 outColor;
uniform sampler2D u_texture;
void main() {
if (v_tex.z == 1.0) {
outColor = texture(u_texture, v_tex.xy);
} else {
outColor = v_color;
}
}
`;
function draw(gfx, camera, dt, grid) {
gfx.clear(0, 0, 0, 0);
camera.update(dt);
let right = gfx.ctx.canvas.width;
let left = 0;
let top = gfx.ctx.canvas.height;
let bottom = 0;
let near = -100;
let far = 100;
let m = Mat4.IDENTITY();
let mo = Mat4.orthographic(left, right, bottom, top, near, far);
let mc = Mat4.translate(new Vec3((gfx.ctx.canvas.width / 2 - camera.position.x), (gfx.ctx.canvas.height / 2 - camera.position.y), 1.0));
let mr = Mat4.translate(new Vec3(-(gfx.ctx.canvas.width / 2 - camera.position.x), -(gfx.ctx.canvas.height / 2 - camera.position.y), 1.0));
let mt = Mat4.translate(camera.position);
let ms = Mat4.scale(new Vec3(camera.scale, camera.scale, 1));
m = m.multNew(mr);
m = m.multNew(ms);
m = m.multNew(mc);
m = m.multNew(mt);
m = m.multNew(mo);
gfx.ctx.uniformMatrix4fv(gfx.getUniform("u_matrix"), false, m.splat());
drawing.drawIsometricGrid(gfx, camera, grid);
gfx.draw();
}
function addDefaultKeybinds(input, camera) {
input.addKeyAction("KeyA", [], camera, (c) => {
c.movement.x = 0;
}, (c) => {
c.movement.x = 1;
});
input.addKeyAction("KeyD", [], camera, (c) => {
c.movement.y = 0;
}, (c) => {
c.movement.y = -1;
});
input.addKeyAction("KeyW", [], camera, (c) => {
c.movement.z = 0;
}, (c) => {
c.movement.z = -1;
});
input.addKeyAction("KeyS", [], camera, (c) => {
c.movement.w = 0;
}, (c) => {
c.movement.w = 1;
});
input.addKeyAction("KeyQ", [], camera, (c) => {
c.scaling.x = 0.0;
}, (c) => {
c.scaling.x = 1.0;
});
input.addKeyAction("KeyE", [], camera, (c) => {
c.scaling.y = 0.0;
}, (c) => {
c.scaling.y = -1.0;
});
}
(async () => {
const canvasId = "game";
const ctx = initializeContext(canvasId);
if (ctx === null)
return;
const gfx = new Graphics(ctx, vertexShader, fragmentShader);
fullscreenCanvas(gfx, canvasId);
let a = gfx.createAttribute("a_position");
a.format(3, ctx.FLOAT, false, 0);
a = gfx.createAttribute("a_color");
a.format(4, ctx.FLOAT, false, 0);
a = gfx.createAttribute("a_tex");
a.format(3, ctx.FLOAT, false, 0);
gfx.createUniform("u_matrix");
gfx.createUniform("u_isIso");
let camera = new Camera(new Vec3(0, 0, -1));
await Assets.assets.load(gfx);
let m = Mat4.isometric();
let size = 100;
let grid = new Grid(m.transformNew(new Vec4(ctx.canvas.width / 4, ctx.canvas.height / 2, 0, 1)).reduce(), 32, size, size, 10);
grid.fillLayer(new Tile({
top: Assets.Colors.Gray,
right: Assets.Colors.Gray,
left: Assets.Colors.Gray,
}), 0);
grid.fillLayer(new Tile({
top: Assets.Colors.Gray,
right: Assets.Colors.Gray,
left: Assets.Colors.Gray,
}), 1);
grid.fillLayer(new Tile({
top: Sprite.id(0),
right: Assets.Colors.Brown,
left: Assets.Colors.Brown,
}), 2);
for (let i = 0; i <= size / 2; i++) {
tree(grid, new Vec3(Math.floor(Math.random() * size - 1), Math.floor(Math.random() * size - 1), 2));
}
const input = new Input();
addDefaultKeybinds(input, camera);
let prevTimestamp = 0;
const frame = (timestamp) => {
const deltaTime = (timestamp - prevTimestamp) / 1000;
prevTimestamp = timestamp;
fullscreenCanvas(gfx, "game");
draw(gfx, camera, deltaTime, grid);
window.requestAnimationFrame(frame);
};
window.requestAnimationFrame((timestamp) => {
prevTimestamp = timestamp;
window.requestAnimationFrame(frame);
});
const wasmgl = new wasm.WASMGL(await wasm.loadWasmModule("./src/wasm/module.wasm"));
})();

234
src/js/script.ts Normal file
View File

@ -0,0 +1,234 @@
import { initializeContext, Vec3, Mat4, Vec4, Vec2 } from "./common.js";
import { Graphics, fullscreenCanvas, Camera, Sprite, DrawTag } from "./graphics.js";
import * as drawing from "./draw.js";
import * as wasm from "./wasm.js";
import { Input } from "./input.js";
import {Grid, Tile, TileEdge, tree} from "./world.js";
import * as Assets from "./assets.js";
const vertexShader =
`#version 300 es
layout(location = 0) in vec3 a_position;
layout(location = 1) in vec3 a_tex;
layout(location = 2) in vec4 a_color;
out vec4 v_color;
out vec3 v_tex;
uniform mat4 u_matrix;
uniform bool u_isIso;
mat4 Iso = mat4(
1, -1, 0, 0,
1, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
);
void main() {
vec4 orthographic;
if (u_isIso) {
vec4 isometric = Iso * vec4(a_position.xyz, 1.0);
orthographic = u_matrix * isometric;
//orthographic = u_matrix * vec4(a_position.xyz, 1.0);
} else {
orthographic = u_matrix * vec4(a_position.xyz, 1.0);
}
gl_Position = orthographic;
v_tex = a_tex;
v_color = a_color;
}
`;
const fragmentShader =
`#version 300 es
precision highp float;
in vec4 v_color;
in vec3 v_tex;
out vec4 outColor;
uniform sampler2D u_texture;
void main() {
if (v_tex.z == 1.0) {
outColor = texture(u_texture, v_tex.xy);
} else {
outColor = v_color;
}
}
`;
function draw(gfx: Graphics, camera: Camera, dt: number, grid: Grid) {
gfx.clear(0, 0, 0, 0);
camera.update(dt);
let right = gfx.ctx.canvas.width;
let left = 0;
let top = gfx.ctx.canvas.height;
let bottom = 0;
let near = -100;
let far = 100;
let m = Mat4.IDENTITY();
let mo = Mat4.orthographic(left, right, bottom, top, near, far);
let mc = Mat4.translate(
new Vec3(
(gfx.ctx.canvas.width / 2 - camera.position.x),
(gfx.ctx.canvas.height / 2 - camera.position.y),
1.0));
let mr = Mat4.translate(
new Vec3(
-(gfx.ctx.canvas.width / 2 - camera.position.x),
-(gfx.ctx.canvas.height / 2 - camera.position.y),
1.0));
let mt = Mat4.translate(camera.position);
let ms = Mat4.scale(new Vec3(camera.scale, camera.scale, 1));
m = m.multNew(mr);
m = m.multNew(ms);
m = m.multNew(mc);
m = m.multNew(mt);
m = m.multNew(mo);
gfx.ctx.uniformMatrix4fv(
gfx.getUniform("u_matrix"),
false,
m.splat()
);
drawing.drawIsometricGrid(gfx, camera, grid);
gfx.draw();
}
function addDefaultKeybinds(input: Input, camera: Camera) {
input.addKeyAction("KeyA", [], camera,
(c) => {
c.movement.x = 0;
},
(c) => {
c.movement.x = 1;
});
input.addKeyAction("KeyD", [], camera,
(c) => {
c.movement.y = 0;
},
(c) => {
c.movement.y = -1;
});
input.addKeyAction("KeyW", [], camera,
(c) => {
c.movement.z = 0;
},
(c) => {
c.movement.z = -1;
});
input.addKeyAction("KeyS", [], camera,
(c) => {
c.movement.w = 0;
},
(c) => {
c.movement.w = 1;
});
input.addKeyAction("KeyQ", [], camera,
(c) => {
c.scaling.x = 0.0;
},
(c) => {
c.scaling.x = 1.0;
});
input.addKeyAction("KeyE", [], camera,
(c) => {
c.scaling.y = 0.0;
},
(c) => {
c.scaling.y = -1.0;
});
}
(async () => {
const canvasId = "game";
const ctx = initializeContext(canvasId);
if (ctx === null) return;
const gfx = new Graphics(ctx, vertexShader, fragmentShader);
fullscreenCanvas(gfx, canvasId);
let a = gfx.createAttribute("a_position");
a.format(3, ctx.FLOAT, false, 0)
a = gfx.createAttribute("a_color");
a.format(4, ctx.FLOAT, false, 0)
a = gfx.createAttribute("a_tex");
a.format(3, ctx.FLOAT, false, 0)
gfx.createUniform("u_matrix");
gfx.createUniform("u_isIso");
let camera = new Camera(new Vec3(0, 0, -1));
await Assets.assets.load(gfx);
let m = Mat4.isometric();
let size = 100;
let grid = new Grid(m.transformNew(new Vec4(ctx.canvas.width / 4, ctx.canvas.height / 2, 0, 1)).reduce(), 32, size, size, 10);
grid.fillLayer(new Tile({
top: Assets.Colors.Gray,
right: Assets.Colors.Gray,
left: Assets.Colors.Gray,
}), 0);
grid.fillLayer(new Tile({
top: Assets.Colors.Gray,
right: Assets.Colors.Gray,
left: Assets.Colors.Gray,
}), 1);
grid.fillLayer(new Tile({
top: Sprite.id(0),
right: Assets.Colors.Brown,
left: Assets.Colors.Brown,
}), 2);
for (let i = 0; i <= size / 2; i++) {
tree(grid, new Vec3(Math.floor(Math.random() * size - 1), Math.floor(Math.random() * size - 1), 2));
}
const input = new Input();
addDefaultKeybinds(input, camera);
let prevTimestamp = 0;
const frame = (timestamp: number) => {
const deltaTime = (timestamp - prevTimestamp)/1000;
prevTimestamp = timestamp;
fullscreenCanvas(gfx, "game");
draw(gfx, camera, deltaTime, grid);
window.requestAnimationFrame(frame);
}
window.requestAnimationFrame((timestamp) => {
prevTimestamp = timestamp;
window.requestAnimationFrame(frame);
});
const wasmgl: wasm.WASMGL = new wasm.WASMGL(await wasm.loadWasmModule("./src/wasm/module.wasm"));
})();

43
src/js/wasm.js Normal file
View File

@ -0,0 +1,43 @@
export async function loadWasmModule(path) {
return WebAssembly.instantiateStreaming(fetch(path));
}
export class WASMGL {
exports;
mem;
constructor(wasm) {
this.exports = wasm.instance.exports;
const memSize = new Float32Array(this.exports.memory.buffer, this.exports.WASMGLmemory.value + 4, 1).at(0);
this.mem = new Float32Array(this.exports.memory.buffer, this.exports.WASMGLmemory.value, memSize);
}
alloc(size) {
const head = this.mem[2];
this.mem[2] += size;
return head;
}
setstr(ptr, str) {
for (let i = 0; i < str.length; i++) {
this.mem[ptr] = str.charCodeAt(i);
++ptr;
}
}
set(ptr, data) {
data.forEach((v, i) => {
this.mem[ptr + i] = v;
});
return ptr;
}
push(data) {
let ptr = this.alloc(data.length);
data.forEach((v, i) => {
this.mem[ptr + i] = v;
});
return ptr;
}
get(ptr, length) {
let a = new Array(length);
for (let i = 0; i < length; ++i) {
a[i] = this.mem[ptr + i];
}
return a;
}
}

71
src/js/wasm.ts Normal file
View File

@ -0,0 +1,71 @@
export interface WASMGLEnvironment {
exports: any,
mem: Float32Array,
}
export async function loadWasmModule(path: string): Promise<WebAssembly.WebAssemblyInstantiatedSource> {
return WebAssembly.instantiateStreaming(fetch(path));
}
export class WASMGL {
exports: any;
mem: Float32Array;
constructor(wasm: WebAssembly.WebAssemblyInstantiatedSource) {
this.exports = wasm.instance.exports;
const memSize = new Float32Array(
this.exports.memory.buffer,
this.exports.WASMGLmemory.value + 4,
1
).at(0);
this.mem = new Float32Array(
this.exports.memory.buffer,
this.exports.WASMGLmemory.value,
memSize
);
}
alloc(size: number): number {
const head = this.mem[2];
this.mem[2] += size;
return head;
}
setstr(ptr: number, str: string) {
for (let i = 0; i < str.length; i++) {
this.mem[ptr] = str.charCodeAt(i);
++ptr;
}
}
set(ptr: number, data: number[]): number {
data.forEach((v, i) => {
this.mem[ptr + i] = v;
});
return ptr;
}
push(data: number[]): number {
let ptr = this.alloc(data.length);
data.forEach((v, i) => {
this.mem[ptr + i] = v;
});
return ptr;
}
get(ptr: number, length: number): Array<number> {
let a = new Array(length);
for (let i = 0; i < length; ++i) {
a[i] = this.mem[ptr + i];
}
return a;
}
}

106
src/js/world.js Normal file
View File

@ -0,0 +1,106 @@
import { Vec3 } from "./common.js";
import { Sprite } from "./graphics.js";
export var TileEdge;
(function (TileEdge) {
TileEdge[TileEdge["None"] = 0] = "None";
TileEdge[TileEdge["Left"] = 1] = "Left";
TileEdge[TileEdge["Right"] = 2] = "Right";
TileEdge[TileEdge["Both"] = 3] = "Both";
})(TileEdge || (TileEdge = {}));
export class Tile {
fill = {
top: [1, 0, 1, 1],
left: [0, 0, 0, 1],
right: [0, 0, 0, 1],
};
edge = TileEdge.None;
constructor(fill, edge) {
if (fill !== undefined)
this.fill = fill;
if (edge !== undefined)
this.edge = edge;
}
}
export function ColorToTile(c) {
return {
left: c,
top: c,
right: c,
};
}
export class Grid {
position;
tiles3d;
tileSize;
width;
breadth;
height;
topHeight;
constructor(position, tileSize, width, breadth, height) {
this.tiles3d = new Array(height);
this.position = position;
this.tileSize = tileSize;
this.width = width;
this.breadth = breadth;
this.height = height;
this.topHeight = 0;
let layer = new Array(width * breadth);
for (let i = 0; i < this.height; ++i)
this.tiles3d[i] = { ...layer };
}
fillLayer(tile, z) {
if (z + 1 > this.topHeight)
this.topHeight = z + 1;
for (let i = 0; i < this.width; ++i) {
for (let j = 0; j < this.breadth; ++j) {
this.tiles3d[z][i + j * this.width] = { ...tile };
}
}
for (let i = 0; i < this.width; ++i) {
this.tiles3d[z][this.breadth * i - 1] = { ...tile, edge: TileEdge.Right };
}
for (let i = 0; i < this.breadth - 1; ++i) {
this.tiles3d[z][this.width * (this.breadth - 1) + i] = { ...tile, edge: TileEdge.Left };
}
this.tiles3d[z][this.width * this.breadth - 1] = { ...tile, edge: TileEdge.Both };
}
setTile(tile, coord) {
let index = coord.x + coord.y * this.width;
this.tiles3d[coord.z][index] = { ...tile };
if (coord.z + 1 > this.topHeight)
this.topHeight = coord.z + 1;
}
getTile(coord) {
let index = coord.x + coord.y * this.width;
let tile = this.tiles3d[coord.z][index];
if (tile === undefined)
return null;
return tile;
}
}
export function tree(grid, position) {
let log = Sprite.tile(2);
let leaves = Sprite.tile(1);
grid.setTile(new Tile(log, TileEdge.Both), new Vec3(position.x, position.y, position.z + 1));
grid.setTile(new Tile(log, TileEdge.Both), new Vec3(position.x, position.y, position.z + 2));
grid.setTile(new Tile(log, TileEdge.Both), new Vec3(position.x, position.y, position.z + 3));
grid.setTile(new Tile(log, TileEdge.Both), new Vec3(position.x, position.y, position.z + 4));
grid.setTile(new Tile(log, TileEdge.None), new Vec3(position.x, position.y, position.z + 4));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x, position.y + 1, position.z + 4));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x, position.y + 1, position.z + 5));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x, position.y, position.z + 5));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x + 1, position.y, position.z + 4));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x + 1, position.y, position.z + 5));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x + 1, position.y + 1, position.z + 4));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x + 1, position.y + 1, position.z + 5));
}
export function bush(grid, position) {
let leaves = Sprite.tile(1);
grid.setTile(new Tile(leaves, TileEdge.Both), position.extend(1));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x + 1, position.y, 1));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x, position.y + 1, 1));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x + 1, position.y + 1, 1));
grid.setTile(new Tile(leaves, TileEdge.Both), position.extend(2));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x + 1, position.y, 2));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x, position.y + 1, 2));
}

141
src/js/world.ts Normal file
View File

@ -0,0 +1,141 @@
import * as Assets from "./assets.js";
import {Vec2, Vec3} from "./common.js";
import {Sprite} from "./graphics.js";
export enum TileEdge {
None,
Left,
Right,
Both,
}
export type TileFillament = Sprite | Assets.Color;
export type TileFill = {
right: TileFillament
left: TileFillament,
top: TileFillament,
};
export class Tile {
fill: TileFill = {
top : [1, 0, 1, 1],
left : [0, 0, 0, 1],
right : [0, 0, 0, 1],
};
edge: TileEdge = TileEdge.None;
constructor(fill?: TileFill, edge?: TileEdge) {
if (fill !== undefined)
this.fill = fill;
if (edge !== undefined)
this.edge = edge;
}
}
export function ColorToTile(c: Assets.Color) {
return {
left: c,
top: c,
right: c,
};
}
export class Grid {
position: Vec3;
tiles3d: Tile[][];
tileSize: number;
width: number;
breadth: number;
height: number;
topHeight: number;
constructor(position: Vec3, tileSize: number, width: number, breadth: number, height: number) {
this.tiles3d = new Array<Tile[]>(height);
this.position = position;
this.tileSize = tileSize;
this.width = width;
this.breadth = breadth;
this.height = height;
this.topHeight = 0;
let layer = new Array(width * breadth);
for (let i = 0; i < this.height; ++i)
this.tiles3d[i] = {...layer};
}
fillLayer(tile: Tile, z: number) {
if (z + 1 > this.topHeight)
this.topHeight = z + 1;
for (let i = 0; i < this.width; ++i) {
for (let j = 0; j < this.breadth; ++j) {
this.tiles3d[z][i + j * this.width] = {...tile};
}
}
for (let i = 0; i < this.width; ++i) {
this.tiles3d[z][this.breadth * i - 1] = {...tile, edge: TileEdge.Right};
}
for (let i = 0; i < this.breadth - 1; ++i) {
this.tiles3d[z][this.width * (this.breadth - 1) + i] = {...tile, edge: TileEdge.Left};
}
this.tiles3d[z][this.width * this.breadth - 1] = {...tile, edge: TileEdge.Both};
}
setTile(tile: Tile, coord: Vec3) {
let index = coord.x + coord.y * this.width;
this.tiles3d[coord.z][index] = {...tile};
if (coord.z + 1 > this.topHeight)
this.topHeight = coord.z + 1;
}
getTile(coord: Vec3): Tile | null {
let index = coord.x + coord.y * this.width;
let tile = this.tiles3d[coord.z][index];
if (tile === undefined) return null;
return tile;
}
}
export function tree(grid: Grid, position: Vec3) {
let log = Sprite.tile(2);
let leaves = Sprite.tile(1);
grid.setTile(new Tile(log, TileEdge.Both), new Vec3(position.x, position.y, position.z + 1));
grid.setTile(new Tile(log, TileEdge.Both), new Vec3(position.x, position.y, position.z + 2));
grid.setTile(new Tile(log, TileEdge.Both), new Vec3(position.x, position.y, position.z + 3));
grid.setTile(new Tile(log, TileEdge.Both), new Vec3(position.x, position.y, position.z + 4));
grid.setTile(new Tile(log, TileEdge.None), new Vec3(position.x, position.y, position.z + 4));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x, position.y + 1, position.z + 4));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x, position.y + 1, position.z + 5));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x, position.y, position.z + 5));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x + 1, position.y, position.z + 4));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x + 1, position.y, position.z + 5));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x + 1, position.y + 1, position.z + 4));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x + 1, position.y + 1, position.z + 5));
}
export function bush(grid: Grid, position: Vec2) {
let leaves = Sprite.tile(1);
grid.setTile(new Tile(leaves, TileEdge.Both), position.extend(1));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x + 1, position.y, 1));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x, position.y + 1, 1));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x + 1, position.y + 1, 1));
grid.setTile(new Tile(leaves, TileEdge.Both), position.extend(2));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x + 1, position.y, 2));
grid.setTile(new Tile(leaves, TileEdge.Both), new Vec3(position.x, position.y + 1, 2));
}

10
src/wasm/build.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
set -xe
CC="clang"
CLIBS=""
CFLAGS="--target=wasm32 -flto -Wl,--lto-O3 -nostdlib -Wl,--no-entry -Wl,--export-all"
$CC $CFLAGS -o module.wasm *.c $CLIBS

12
src/wasm/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>aaa</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="description" content="" />
</head>
<body>
<script src="test.js"></script>
</body>
</html>

20
src/wasm/main.c Normal file
View File

@ -0,0 +1,20 @@
#include "wasmgl.h"
void doubel(WASMGLvalue(data))
{
for (int i = 0; i < size_data; ++i)
WASMGLset(ptr_data + i, WASMGLmemory[ptr_data + i] * 2.0);
}
void uppercase(WASMGLvalue(data)) {
int val = 0;
for (int i = 0; i < size_data; ++i)
{
val = WASMGLmemory[ptr_data + i] - 32;
if (val < 65 || val > 122) val = WASMGLmemory[ptr_data + i];
WASMGLset(ptr_data + i, val);
}
}

BIN
src/wasm/module.wasm Executable file

Binary file not shown.

34
src/wasm/test.js Normal file
View File

@ -0,0 +1,34 @@
(async () => {
let instance = await WebAssembly.instantiateStreaming(fetch("module.wasm"));
const memSize = new Int32Array(
instance.instance.exports.memory.buffer,
instance.instance.exports.WASMGLmemory.value + 4,
1
)[0]
let mem = new Int32Array(
instance.instance.exports.memory.buffer,
instance.instance.exports.WASMGLmemory.value,
memSize
);
let ptr = alloc(mem, 4);
setstr(mem, ptr, 4, "game");
console.log(String.fromCharCode(instance.instance.exports.initialize(ptr, 4)));
})();
function alloc(mem, size) {
const head = mem[2];
console.log(head);
mem[2] += size;
return head;
}
function setstr(mem, ptr, size, str) {
for (let i = 0; i < size; i++) {
mem[ptr] = str.charCodeAt(i);
++ptr;
}
}

26
src/wasm/wasmgl.c Normal file
View File

@ -0,0 +1,26 @@
#include "wasmgl.h"
float WASMGLmemory[WASMGLmemory_size] = {
[MEM_SIZE] = WASMGLmemory_size,
[MEM_HEAD] = MEM_ELEM_COUNT,
0,
};
WASMGLptr WASMGLmalloc(WASMGLsize size)
{
WASMGLptr ptr = WASMGLmemory[MEM_HEAD];
WASMGLmemory[MEM_HEAD] += size;
return ptr;
}
void WASMGLset(WASMGLptr ptr, int value)
{
WASMGLmemory[ptr] = value;
}
void WASMGLsetstr(WASMGLptr ptr, const char * cstr, WASMGLsize size)
{
for (int i = 0; i < size; ++i)
WASMGLset(ptr + i, cstr[i]);
}

25
src/wasm/wasmgl.h Normal file
View File

@ -0,0 +1,25 @@
#ifndef WASMGL_H_
#define WASMGL_H_
#define main WASMGLmain
#define WASMGLmemory_size (1024 * 1024)
typedef enum {
MEM_NULL = 0,
MEM_SIZE = 1,
MEM_HEAD,
MEM_ELEM_COUNT,
} WASMGLmemory_layout;
extern float WASMGLmemory[WASMGLmemory_size];
typedef unsigned int WASMGLptr;
typedef unsigned int WASMGLsize;
#define WASMGLvalue(name) WASMGLptr ptr_##name, WASMGLsize size_##name
WASMGLptr WASMGLmalloc(WASMGLsize size);
void WASMGLset(WASMGLptr ptr, int value);
void WASMGLsetstr(WASMGLptr ptr, const char * cstr, WASMGLsize size);
#endif // WASMGL_H_