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; data: number[][]; vertexSize: number; sprites: Spritesheet | undefined; } export class Graphics { ctx: WebGL2RenderingContext; program: WebGLProgram; attribs: Map = new Map(); uniforms: Map = new Map(); vao: WebGLVertexArrayObject; vbo: WebGLBuffer; toRender: Array = []; texCount: number = 0; 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.flat(); 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 { 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 { 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; } } }