summaryrefslogtreecommitdiffstats
path: root/src/renderer/runtime/view
diff options
context:
space:
mode:
Diffstat (limited to 'src/renderer/runtime/view')
-rw-r--r--src/renderer/runtime/view/entity.ts72
-rw-r--r--src/renderer/runtime/view/input/gameinput.ts78
-rw-r--r--src/renderer/runtime/view/input/inputhandler.ts44
-rw-r--r--src/renderer/runtime/view/map.ts173
-rw-r--r--src/renderer/runtime/view/renderer/renderer.ts100
-rw-r--r--src/renderer/runtime/view/renderer/shaders.ts79
-rw-r--r--src/renderer/runtime/view/renderer/shaders/default.fs10
-rw-r--r--src/renderer/runtime/view/renderer/shaders/default.vs13
-rw-r--r--src/renderer/runtime/view/sprite.ts72
-rw-r--r--src/renderer/runtime/view/util/image.ts33
10 files changed, 674 insertions, 0 deletions
diff --git a/src/renderer/runtime/view/entity.ts b/src/renderer/runtime/view/entity.ts
new file mode 100644
index 0000000..ec91503
--- /dev/null
+++ b/src/renderer/runtime/view/entity.ts
@@ -0,0 +1,72 @@
+import { EntityData } from '../model/data/entity';
+import { Renderer } from './renderer/renderer';
+import { SpriteCoords, SpriteView, SpriteViewBuilder } from './sprite';
+import { loadImage, mkTexture } from './util/image';
+
+import { getJSON } from '../util';
+
+import { vec2 } from 'gl-matrix';
+
+export class EntityView {
+ public static async load(r: Renderer, name: string): Promise<EntityView> {
+ const data = new EntityData(await getJSON(`resources/entity/${name}.json`));
+ const tile = await loadImage(`resources/sprite/entity/${data.sprite}.png`);
+
+ const [texture, size] = mkTexture(r, tile);
+ const frameSize = [size[0], size[1] / data.frames];
+
+ const offset = vec2.mul(vec2.create(), frameSize, data.anchor);
+ r.snapToGrid(offset, offset);
+
+ const coords: SpriteCoords = [
+ -offset[0],
+ -offset[1],
+ -offset[0] + frameSize[0],
+ -offset[1] + frameSize[1],
+ ];
+
+ const sprites: SpriteView[] = [];
+
+ for (let frame = 0; frame < data.frames; frame++) {
+ const builder = new SpriteViewBuilder(r, texture);
+ builder.addSprite(coords, [0, frame / data.frames, 1, (frame + 1) / data.frames]);
+ sprites.push(builder.build());
+ }
+
+ return new EntityView(
+ data,
+ sprites,
+ );
+ }
+
+ private readonly totalTime: number;
+
+ private constructor(
+ public readonly data: EntityData,
+ public readonly sprites: SpriteView[],
+ ) {
+ if (data.animation)
+ this.totalTime = data.animation.sequence.reduce((a, s) => a + s[0], 0);
+ else
+ this.totalTime = 0;
+ }
+
+ public getSpriteByTime(time: number): SpriteView {
+ time %= this.totalTime;
+
+ if (this.data.animation) {
+ for (const [len, sprite] of this.data.animation.sequence) {
+ time -= len;
+ if (time < 0)
+ return this.sprites[sprite];
+ }
+ }
+
+ return this.sprites[0];
+ }
+
+ public renderByTime(time: number) {
+ this.getSpriteByTime(time).render();
+ }
+
+}
diff --git a/src/renderer/runtime/view/input/gameinput.ts b/src/renderer/runtime/view/input/gameinput.ts
new file mode 100644
index 0000000..67fbe0c
--- /dev/null
+++ b/src/renderer/runtime/view/input/gameinput.ts
@@ -0,0 +1,78 @@
+import { InputHandler } from './inputhandler';
+
+import { Listenable } from '../../util';
+
+import { vec2 } from 'gl-matrix';
+
+export enum ButtonCode {
+ Action,
+ Back,
+ Menu,
+}
+
+const buttonMapping: Record<string, ButtonCode> = {
+ KeyZ: ButtonCode.Action,
+ KeyX: ButtonCode.Back,
+ KeyC: ButtonCode.Menu,
+};
+
+export interface DirectionInput {
+ type: 'direction';
+ direction: vec2;
+}
+
+export interface ButtonInput {
+ type: 'button';
+ button: ButtonCode;
+}
+
+export type GameInput = DirectionInput | ButtonInput;
+
+export class GameInputHandler extends Listenable<[GameInput]> {
+ private readonly input: InputHandler;
+
+ constructor() {
+ super();
+
+ this.input = new InputHandler(
+ new Set([
+ 'ArrowLeft',
+ 'ArrowUp',
+ 'ArrowRight',
+ 'ArrowDown',
+ ...Object.keys(buttonMapping),
+ ]));
+
+ this.input.addListener((key: string, pressed: boolean) => {
+ const button = buttonMapping[key];
+ if (button !== undefined) {
+ if (pressed)
+ this.runListeners({
+ type: 'button',
+ button,
+ });
+
+ return;
+ }
+
+ const dir = vec2.create();
+
+ if (this.input.has('ArrowLeft'))
+ vec2.add(dir, dir, [-1, 0]);
+ if (this.input.has('ArrowUp'))
+ vec2.add(dir, dir, [0, -1]);
+ if (this.input.has('ArrowRight'))
+ vec2.add(dir, dir, [1, 0]);
+ if (this.input.has('ArrowDown'))
+ vec2.add(dir, dir, [0, 1]);
+
+ if (vec2.sqrLen(dir) > 0)
+ vec2.normalize(dir, dir);
+
+ this.runListeners({
+ type: 'direction',
+ direction: dir,
+ });
+ });
+ }
+}
diff --git a/src/renderer/runtime/view/input/inputhandler.ts b/src/renderer/runtime/view/input/inputhandler.ts
new file mode 100644
index 0000000..17abfe6
--- /dev/null
+++ b/src/renderer/runtime/view/input/inputhandler.ts
@@ -0,0 +1,44 @@
+import { Listenable } from '../../util';
+
+export class InputHandler extends Listenable<[string, boolean]> {
+ private readonly keys: Set<string> = new Set();
+
+ constructor(relevantKeys: Set<string>) {
+ super();
+
+ window.addEventListener('keydown', (ev) => {
+ if (!relevantKeys.has(ev.code))
+ return;
+
+ ev.preventDefault();
+
+ if (ev.repeat)
+ return;
+
+ this.keys.add(ev.code);
+ this.runListeners(ev.code, true);
+ });
+
+ window.addEventListener('keyup', (ev) => {
+ if (!relevantKeys.has(ev.code))
+ return;
+
+ ev.preventDefault();
+
+ if (!this.keys.has(ev.code))
+ return;
+
+ this.keys.delete(ev.code);
+ this.runListeners(ev.code, false);
+ });
+
+ window.addEventListener('blur', () => {
+ this.keys.clear();
+ this.runListeners('', false);
+ });
+ }
+
+ public has(key: string): boolean {
+ return this.keys.has(key);
+ }
+}
diff --git a/src/renderer/runtime/view/map.ts b/src/renderer/runtime/view/map.ts
new file mode 100644
index 0000000..18def05
--- /dev/null
+++ b/src/renderer/runtime/view/map.ts
@@ -0,0 +1,173 @@
+import { EntityView } from './entity';
+import { Renderer } from './renderer/renderer';
+import { SpriteCoords, SpriteView, SpriteViewBuilder } from './sprite';
+import { loadImage, mkTexture } from './util/image';
+
+import { MapData } from '../model/data/map';
+
+import { nextPowerOf2 } from '../util';
+
+interface StaticMapTile {
+ type: 'static';
+ image: HTMLImageElement;
+}
+
+interface EntityTile {
+ type: 'entity';
+ entity: EntityView;
+}
+
+type MapTile = StaticMapTile | EntityTile;
+
+interface StaticTilesetTile {
+ type: 'static';
+ coords: SpriteCoords;
+}
+
+type TilesetTile = StaticTilesetTile | EntityTile;
+
+interface Tileset {
+ texture: WebGLTexture;
+ tiles: TilesetTile[];
+}
+
+async function loadTile(r: Renderer, tile: string): Promise<MapTile> {
+ const name = tile.substr(1);
+ switch (tile[0]) {
+ case '-':
+ return {
+ type: 'static',
+ image: await loadImage(`resources/sprite/tile/${name}.png`),
+ };
+
+ case '@':
+ return {
+ type: 'entity',
+ entity: await EntityView.load(r, name),
+ };
+
+ default:
+ throw new Error('invalid tile specifier');
+ }
+}
+
+function loadTiles(r: Renderer, tiles: string[]): Promise<MapTile[]> {
+ return Promise.all(tiles.map((tile) => loadTile(r, tile)));
+}
+
+function mkTileset(
+ r: Renderer,
+ mapTiles: MapTile[],
+): Tileset {
+ const tileSize = 32;
+
+ const canvasDim = nextPowerOf2(Math.sqrt(mapTiles.length));
+ const canvasSize = canvasDim * tileSize;
+
+ const canvas = document.createElement('canvas');
+ canvas.width = canvas.height = canvasSize;
+
+ let x = 0, y = 0;
+ const tiles: TilesetTile[] = [];
+ const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
+
+ for (const tile of mapTiles) {
+ switch (tile.type) {
+ case 'static':
+ ctx.drawImage(tile.image, x * tileSize, y * tileSize);
+ tiles.push({
+ type: 'static',
+ coords: [x / canvasDim, y / canvasDim, (x + 1) / canvasDim, (y + 1) / canvasDim],
+ });
+
+ x++;
+ if (x === canvasDim) {
+ x = 0;
+ y++;
+ }
+ break;
+
+ case 'entity':
+ tiles.push(tile);
+ break;
+ }
+ }
+
+ const [texture] = mkTexture(r, canvas);
+
+ return {
+ texture,
+ tiles,
+ };
+}
+
+function addSprite(
+ builder: SpriteViewBuilder,
+ entityTiles: Array<[[number, number], EntityView]>,
+ tileset: Tileset,
+ x: number,
+ y: number,
+ tile: number) {
+ if (!tile)
+ return;
+
+ const tilesetTile = tileset.tiles[tile - 1];
+
+ switch (tilesetTile.type) {
+ case 'static':
+ builder.addSprite([x, y, x + 1, y + 1], tilesetTile.coords);
+ break;
+
+ case 'entity':
+ entityTiles.push([[x + 0.5, y + 0.5], tilesetTile.entity]);
+ break;
+ }
+}
+
+function buildMapLayer(r: Renderer, tileset: Tileset, layer: number[][]): MapLayerView {
+ const builder = new SpriteViewBuilder(r, tileset.texture);
+ const entityTiles: Array<[[number, number], EntityView]> = [];
+
+ for (let x = 0; x < layer[0].length; x++)
+ for (let y = 0; y < layer.length; y++)
+ addSprite(builder, entityTiles, tileset, x, y, layer[y][x]);
+
+ return new MapLayerView(r, builder.build(), entityTiles);
+}
+
+class MapLayerView {
+ public constructor(
+ private r: Renderer,
+ private staticTiles: SpriteView,
+ private entityTiles: Array<[[number, number], EntityView]>,
+ ) {
+ }
+
+ public render(time: number): void {
+ this.r.setTranslation([0, 0]);
+ this.staticTiles.render();
+
+ for (const [coords, entity] of this.entityTiles) {
+ this.r.setTranslation(coords);
+ entity.renderByTime(time);
+ }
+ }
+}
+
+export class MapView {
+ public static async load(r: Renderer, map: MapData): Promise<MapView> {
+ const tiles = await loadTiles(r, map.tiles);
+ const tileset = mkTileset(r, tiles);
+
+ const layers = map.layers.map((layer) => buildMapLayer(r, tileset, layer.tiles));
+ return new MapView(layers);
+ }
+
+ private constructor(private layers: MapLayerView[]) {
+ }
+
+ public render(time: number): void {
+ for (const layer of this.layers)
+ layer.render(time);
+ }
+}
diff --git a/src/renderer/runtime/view/renderer/renderer.ts b/src/renderer/runtime/view/renderer/renderer.ts
new file mode 100644
index 0000000..9f711d3
--- /dev/null
+++ b/src/renderer/runtime/view/renderer/renderer.ts
@@ -0,0 +1,100 @@
+import { nextPowerOf2 } from '../../util';
+import { Shaders } from './shaders';
+
+import { mat4, vec2 } from 'gl-matrix';
+
+export class Renderer {
+ public readonly coordScale = 32;
+ private readonly viewScale = 2;
+
+ private readonly gl: WebGLRenderingContext;
+ private readonly shaders: Shaders;
+
+ private readonly center: vec2 = vec2.create();
+ private readonly translation: vec2 = vec2.create();
+ private readonly viewport: mat4 = mat4.create();
+
+ constructor(private readonly canvas: HTMLCanvasElement) {
+ this.gl = this.mkContext();
+
+ this.shaders = new Shaders(this.gl);
+
+ this.gl.clearColor(0.0, 0.0, 0.0, 1.0);
+
+ this.gl.enable(this.gl.BLEND);
+ this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
+ this.gl.pixelStorei(this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1);
+ }
+
+ public createBuffer(): WebGLBuffer {
+ const ret = this.gl.createBuffer();
+ if (!ret)
+ throw new Error('unable to create buffer');
+
+ return ret;
+ }
+
+ public getContext(): WebGLRenderingContext {
+ return this.gl;
+ }
+
+ public getVertexPosLoc(): number {
+ return this.shaders.vertexPosLoc;
+ }
+
+ public getTextureCoordLoc(): number {
+ return this.shaders.textureCoordLoc;
+ }
+
+ public getSamplerLoc(): WebGLUniformLocation {
+ return this.shaders.samplerLoc;
+ }
+
+ public setCenter(v: vec2|number[]) {
+ this.snapToGrid(this.center, v);
+ }
+
+ public setTranslation(v: vec2|number[]) {
+ vec2.sub(this.translation, v, this.center);
+ this.snapToGrid(this.translation, this.translation);
+ this.gl.uniform2fv(this.shaders.translateLoc, this.translation);
+ }
+
+ public clear(): void {
+ this.gl.clear(this.gl.COLOR_BUFFER_BIT);
+
+ this.setTranslation([0, 0]);
+ }
+
+ public snapToGrid(out: vec2, a: vec2|number[]): void {
+ vec2.scale(out, a, this.coordScale);
+ vec2.round(out, out);
+ vec2.scale(out, out, 1 / this.coordScale);
+ }
+
+ public resize(): void {
+ const w = this.canvas.width;
+ const h = this.canvas.height;
+ const ws = nextPowerOf2(w);
+ const hs = nextPowerOf2(h);
+
+ this.gl.viewport((w - ws) / 2, (h - hs) / 2, ws, hs);
+ this.clear();
+
+ const scale = this.viewScale * this.coordScale;
+
+ mat4.identity(this.viewport);
+ mat4.scale(this.viewport, this.viewport, [2 * scale / ws, -2 * scale / hs, 1.0]);
+ this.gl.uniformMatrix4fv(this.shaders.viewportLoc, false, this.viewport);
+ }
+
+ private mkContext(): WebGLRenderingContext {
+ const gl = (
+ this.canvas.getContext('webgl') || this.canvas.getContext('experimental-webgl')
+ );
+ if (!gl)
+ throw new Error('unable to initialize WebGL context');
+
+ return gl;
+ }
+}
diff --git a/src/renderer/runtime/view/renderer/shaders.ts b/src/renderer/runtime/view/renderer/shaders.ts
new file mode 100644
index 0000000..8fd1fda
--- /dev/null
+++ b/src/renderer/runtime/view/renderer/shaders.ts
@@ -0,0 +1,79 @@
+import fragmentShaderSrc from './shaders/default.fs';
+import vertexShaderSrc from './shaders/default.vs';
+
+export class Shaders {
+ public readonly viewportLoc: WebGLUniformLocation;
+ public readonly translateLoc: WebGLUniformLocation;
+
+ public readonly vertexPosLoc: number;
+ public readonly textureCoordLoc: number;
+ public readonly samplerLoc: WebGLUniformLocation;
+
+ constructor(private readonly gl: WebGLRenderingContext) {
+ const shaderProgram = this.gl.createProgram();
+ if (!shaderProgram)
+ throw new Error('Unable to create shader program');
+
+ const vertexShader = this.compileShader(this.gl.VERTEX_SHADER, vertexShaderSrc);
+ const fragmentShader = this.compileShader(this.gl.FRAGMENT_SHADER, fragmentShaderSrc);
+
+ this.gl.attachShader(shaderProgram, vertexShader);
+ this.gl.attachShader(shaderProgram, fragmentShader);
+
+ this.gl.linkProgram(shaderProgram);
+ if (!this.gl.getProgramParameter(shaderProgram, this.gl.LINK_STATUS)) {
+ const err = this.gl.getProgramInfoLog(shaderProgram);
+
+ this.gl.deleteShader(vertexShader);
+ this.gl.deleteShader(fragmentShader);
+ this.gl.deleteProgram(shaderProgram);
+
+ throw new Error('Unable to link shader: ' + err);
+ }
+
+ this.gl.useProgram(shaderProgram);
+
+ this.vertexPosLoc = this.getAttribLocation(shaderProgram, 'aVertexPos');
+ this.gl.enableVertexAttribArray(this.vertexPosLoc);
+
+ this.textureCoordLoc = this.getAttribLocation(shaderProgram, 'aTextureCoord');
+ this.gl.enableVertexAttribArray(this.textureCoordLoc);
+
+ this.viewportLoc = this.getUniformLocation(shaderProgram, 'uViewport');
+ this.translateLoc = this.getUniformLocation(shaderProgram, 'uTranslate');
+ this.samplerLoc = this.getUniformLocation(shaderProgram, 'uSampler');
+ }
+
+ private compileShader(type: number, src: string): WebGLShader {
+ const shader = this.gl.createShader(type);
+ if (!shader)
+ throw new Error('Unable to create shader');
+
+ this.gl.shaderSource(shader, src);
+ this.gl.compileShader(shader);
+
+ if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
+ const err = this.gl.getShaderInfoLog(shader);
+ this.gl.deleteShader(shader);
+ throw new Error('Unable to compile shader: ' + err);
+ }
+
+ return shader;
+ }
+
+ private getAttribLocation(program: WebGLProgram, name: string): number {
+ const ret = this.gl.getAttribLocation(program, name);
+ if (ret < 0)
+ throw new Error("unable to get location of attribute '" + name + "'");
+
+ return ret;
+ }
+
+ private getUniformLocation(program: WebGLProgram, name: string): WebGLUniformLocation {
+ const ret = this.gl.getUniformLocation(program, name);
+ if (!ret)
+ throw new Error("unable to get location of uniform '" + name + "'");
+
+ return ret;
+ }
+}
diff --git a/src/renderer/runtime/view/renderer/shaders/default.fs b/src/renderer/runtime/view/renderer/shaders/default.fs
new file mode 100644
index 0000000..2c8ad15
--- /dev/null
+++ b/src/renderer/runtime/view/renderer/shaders/default.fs
@@ -0,0 +1,10 @@
+precision highp float;
+
+varying vec2 vTextureCoord;
+
+uniform sampler2D uSampler;
+
+
+void main(void) {
+ gl_FragColor = texture2D(uSampler, vTextureCoord);
+}
diff --git a/src/renderer/runtime/view/renderer/shaders/default.vs b/src/renderer/runtime/view/renderer/shaders/default.vs
new file mode 100644
index 0000000..4715a17
--- /dev/null
+++ b/src/renderer/runtime/view/renderer/shaders/default.vs
@@ -0,0 +1,13 @@
+attribute vec2 aVertexPos;
+attribute vec2 aTextureCoord;
+
+uniform mat4 uViewport;
+uniform vec2 uTranslate;
+
+varying highp vec2 vTextureCoord;
+
+
+void main(void) {
+ gl_Position = uViewport * vec4(aVertexPos + uTranslate, 0.0, 1.0);
+ vTextureCoord = aTextureCoord;
+}
diff --git a/src/renderer/runtime/view/sprite.ts b/src/renderer/runtime/view/sprite.ts
new file mode 100644
index 0000000..3a5ebcb
--- /dev/null
+++ b/src/renderer/runtime/view/sprite.ts
@@ -0,0 +1,72 @@
+import { Renderer } from './renderer/renderer';
+
+export type SpriteCoords = [number, number, number, number];
+
+export class SpriteViewBuilder {
+ private static pushSprite(buf: number[], coords: SpriteCoords): void {
+ const [x1, y1, x2, y2] = coords;
+
+ buf.push(x1); buf.push(y1);
+ buf.push(x2); buf.push(y1);
+ buf.push(x1); buf.push(y2);
+
+ buf.push(x1); buf.push(y2);
+ buf.push(x2); buf.push(y1);
+ buf.push(x2); buf.push(y2);
+ }
+
+ private readonly vertexData: number[] = [];
+ private readonly textureData: number[] = [];
+
+ constructor(private readonly r: Renderer, private readonly texture: WebGLTexture) {}
+
+ public addSprite(vertexCoords: SpriteCoords, texCoords: SpriteCoords): void {
+ SpriteViewBuilder.pushSprite(this.vertexData, vertexCoords);
+ SpriteViewBuilder.pushSprite(this.textureData, texCoords);
+ }
+
+ public build(): SpriteView {
+ return new SpriteView(this.r, this.texture, this.vertexData, this.textureData);
+ }
+}
+
+export class SpriteView {
+ private readonly vertexCount: number;
+ private readonly vertexBuffer: WebGLBuffer;
+ private readonly textureBuffer: WebGLBuffer;
+
+ constructor(
+ private readonly r: Renderer,
+ private readonly texture: WebGLTexture,
+ vertexData: number[],
+ textureData: number[],
+ ) {
+ const gl = r.getContext();
+
+ this.vertexBuffer = r.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexData), gl.STATIC_DRAW);
+
+ this.textureBuffer = r.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.textureBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureData), gl.STATIC_DRAW);
+
+ this.vertexCount = vertexData.length / 2;
+ }
+
+ public render(): void {
+ const gl = this.r.getContext();
+
+ gl.activeTexture(gl.TEXTURE0);
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
+ gl.uniform1i(this.r.getSamplerLoc(), 0);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
+ gl.vertexAttribPointer(this.r.getVertexPosLoc(), 2, gl.FLOAT, false, 0, 0);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.textureBuffer);
+ gl.vertexAttribPointer(this.r.getTextureCoordLoc(), 2, gl.FLOAT, false, 0, 0);
+
+ gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
+ }
+}
diff --git a/src/renderer/runtime/view/util/image.ts b/src/renderer/runtime/view/util/image.ts
new file mode 100644
index 0000000..37c5e7f
--- /dev/null
+++ b/src/renderer/runtime/view/util/image.ts
@@ -0,0 +1,33 @@
+import { Renderer } from '../renderer/renderer';
+
+export function loadImage(url: string): Promise<HTMLImageElement> {
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ img.addEventListener('load', () => { resolve(img); });
+ img.addEventListener('error', () => { reject(new Error('failed to load ' + url)); });
+ img.src = url;
+ });
+}
+
+export function mkTexture(
+ r: Renderer,
+ src: HTMLCanvasElement|HTMLImageElement,
+): [WebGLTexture, [number, number]] {
+ const gl = r.getContext();
+ const texture = gl.createTexture();
+ if (!texture)
+ throw new Error('unable to create texture');
+
+ gl.bindTexture(gl.TEXTURE_2D, texture);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, src);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+
+ const size: [number, number] = [
+ src.width / r.coordScale, src.height / r.coordScale,
+ ];
+
+ return [texture, size];
+}