Move renderer into "runtime" subdirectory
This commit is contained in:
parent
33926af829
commit
3c51a1994f
23 changed files with 3 additions and 3 deletions
65
src/renderer/runtime/controller/collision.ts
Normal file
65
src/renderer/runtime/controller/collision.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { Collision } from '../model/data/collision';
|
||||
|
||||
import { Circle } from '../math/circle';
|
||||
import { Collidable } from '../math/collision';
|
||||
import { LineSegment, Movement } from '../math/line';
|
||||
import { Point } from '../math/point';
|
||||
|
||||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
export function mkCollision(collision: Collision[]): Collidable[] {
|
||||
const ret: Collidable[] = [];
|
||||
|
||||
for (const c of collision) {
|
||||
switch (c.type) {
|
||||
case 'polygon':
|
||||
if (!c.vertices.length)
|
||||
continue;
|
||||
|
||||
let prev = c.vertices[c.vertices.length - 1];
|
||||
|
||||
for (const v of c.vertices) {
|
||||
ret.push(LineSegment.fromPoints(vec2.clone(prev), vec2.clone(v)));
|
||||
prev = v;
|
||||
}
|
||||
|
||||
for (const v of c.vertices) {
|
||||
ret.push(new Point(vec2.clone(v)));
|
||||
prev = v;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'circle':
|
||||
ret.push(new Circle(vec2.clone(c.center), c.radius));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export interface CollidableGroup {
|
||||
getTranslation(): vec2|null;
|
||||
getCollidables(): Collidable[];
|
||||
}
|
||||
|
||||
export function collide(collision: CollidableGroup, out: vec2, move: Movement, radius: number): boolean {
|
||||
const t = collision.getTranslation();
|
||||
if (t)
|
||||
move = move.translate(vec2.negate(vec2.create(), t));
|
||||
|
||||
for (const c of collision.getCollidables()) {
|
||||
if (!c.collide(out, move, radius))
|
||||
continue;
|
||||
|
||||
if (vec2.squaredDistance(move.src, out) >= vec2.squaredDistance(move.src, move.dest))
|
||||
continue;
|
||||
|
||||
if (t)
|
||||
vec2.add(out, out, t);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
47
src/renderer/runtime/controller/entitycontext.ts
Normal file
47
src/renderer/runtime/controller/entitycontext.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { EntityView } from '../view/entity';
|
||||
import { Renderer } from '../view/renderer/renderer';
|
||||
|
||||
import { Collidable } from '../math/collision';
|
||||
|
||||
import { CollidableGroup, mkCollision } from './collision';
|
||||
|
||||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
export class EntityContext implements CollidableGroup {
|
||||
public static async load(renderer: Renderer, name: string): Promise<EntityContext> {
|
||||
return new EntityContext(
|
||||
renderer,
|
||||
name,
|
||||
await EntityView.load(renderer, name),
|
||||
);
|
||||
}
|
||||
|
||||
public readonly pos: vec2 = vec2.create();
|
||||
|
||||
private readonly collision: Collidable[];
|
||||
|
||||
private constructor(
|
||||
private readonly renderer: Renderer,
|
||||
private readonly name: string,
|
||||
private readonly view: EntityView,
|
||||
) {
|
||||
this.collision = mkCollision(view.data.collision);
|
||||
}
|
||||
|
||||
public render(time: number) {
|
||||
this.renderer.setTranslation(this.pos);
|
||||
this.view.renderByTime(time);
|
||||
}
|
||||
|
||||
public getTranslation(): vec2 {
|
||||
return this.pos;
|
||||
}
|
||||
|
||||
public getCollidables(): Collidable[] {
|
||||
return this.collision;
|
||||
}
|
||||
|
||||
public interact() {
|
||||
alert(`You've interacted with ${this.name}!`);
|
||||
}
|
||||
}
|
174
src/renderer/runtime/controller/gamecontext.ts
Normal file
174
src/renderer/runtime/controller/gamecontext.ts
Normal file
|
@ -0,0 +1,174 @@
|
|||
import { CollidableGroup, collide, mkCollision } from './collision';
|
||||
import { EntityContext } from './entitycontext';
|
||||
|
||||
import { MapData } from '../model/data/map';
|
||||
|
||||
import { ButtonCode, GameInputHandler } from '../view/input/gameinput';
|
||||
import { MapView } from '../view/map';
|
||||
import { Renderer } from '../view/renderer/renderer';
|
||||
|
||||
import { Collidable } from '../math/collision';
|
||||
import { Movement } from '../math/line';
|
||||
import { getJSON, nextAnimationFrame } from '../util';
|
||||
|
||||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
export class GameContext implements CollidableGroup {
|
||||
public static async load(renderer: Renderer): Promise<GameContext> {
|
||||
const map = this.loadMap(renderer, 'test');
|
||||
const loadPlayer = EntityContext.load(renderer, 'green_circle');
|
||||
const loadEntity = EntityContext.load(renderer, 'red_circle');
|
||||
|
||||
const [mapView, mapCollision] = await map;
|
||||
const player = await loadPlayer;
|
||||
const entity = await loadEntity;
|
||||
|
||||
vec2.set(player.pos, 7, 6);
|
||||
vec2.set(entity.pos, 4, 3);
|
||||
|
||||
return new GameContext(
|
||||
renderer,
|
||||
mapView,
|
||||
player,
|
||||
[entity],
|
||||
mapCollision,
|
||||
);
|
||||
}
|
||||
|
||||
private static async loadMap(renderer: Renderer, name: string): Promise<[MapView, Collidable[]]> {
|
||||
const map = new MapData(await getJSON(`resources/map/${name}.json`));
|
||||
return [await MapView.load(renderer, map), mkCollision(map.collision)];
|
||||
}
|
||||
|
||||
private readonly initTime: number = performance.now();
|
||||
private time = 0;
|
||||
|
||||
private readonly tick = 10; // ms per tick
|
||||
private readonly maxSpeed = 0.04; // movement per tick
|
||||
private readonly maxSkip = 20; // maximum ticks to process in a single render step
|
||||
|
||||
private readonly input: GameInputHandler;
|
||||
|
||||
private readonly playerDir: vec2 = vec2.fromValues(0, 1);
|
||||
private speed: number = 0;
|
||||
|
||||
private readonly collisionRadius = 15 / 32;
|
||||
private readonly interactLength = 1 / 32;
|
||||
|
||||
private constructor(
|
||||
private readonly renderer: Renderer,
|
||||
private readonly mapView: MapView,
|
||||
private readonly player: EntityContext,
|
||||
private readonly entities: EntityContext[],
|
||||
private readonly collision: Collidable[],
|
||||
) {
|
||||
this.input = new GameInputHandler();
|
||||
this.input.addListener((input) => {
|
||||
switch (input.type) {
|
||||
case 'button':
|
||||
if (input.button === ButtonCode.Action)
|
||||
this.interact();
|
||||
break;
|
||||
|
||||
case 'direction':
|
||||
if (vec2.sqrLen(input.direction) > 0) {
|
||||
vec2.copy(this.playerDir, input.direction);
|
||||
this.speed = this.maxSpeed;
|
||||
} else {
|
||||
this.speed = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
this.renderLoop();
|
||||
}
|
||||
|
||||
public getTranslation(): null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public getCollidables(): Collidable[] {
|
||||
return this.collision;
|
||||
}
|
||||
|
||||
private updateTime(time: number): number {
|
||||
const diff = Math.round(time / this.tick) - Math.round(this.time / this.tick);
|
||||
this.time = time;
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
private canInteract(c: CollidableGroup): boolean {
|
||||
const dest = vec2.scaleAndAdd(vec2.create(), this.player.pos, this.playerDir, this.interactLength);
|
||||
const move = new Movement(this.player.pos, dest);
|
||||
|
||||
return collide(c, vec2.create(), move, this.collisionRadius);
|
||||
}
|
||||
|
||||
private interact(): void {
|
||||
for (const e of this.entities) {
|
||||
if (!this.canInteract(e))
|
||||
continue;
|
||||
|
||||
e.interact();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private updateStepCollide(out: vec2, dest: vec2): boolean {
|
||||
const move = new Movement(this.player.pos, dest);
|
||||
|
||||
for (const c of [this, ...this.entities]) {
|
||||
if (collide(c, out, move, this.collisionRadius)) {
|
||||
if (vec2.squaredDistance(move.src, out) >= vec2.squaredDistance(move.src, move.dest))
|
||||
continue;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private updateStep(): void {
|
||||
const dest = vec2.scaleAndAdd(vec2.create(), this.player.pos, this.playerDir, this.speed);
|
||||
const newDest = vec2.create();
|
||||
|
||||
while (this.updateStepCollide(newDest, dest)) {
|
||||
if (vec2.equals(newDest, this.player.pos))
|
||||
return;
|
||||
|
||||
vec2.copy(dest, newDest);
|
||||
}
|
||||
|
||||
vec2.copy(this.player.pos, dest);
|
||||
}
|
||||
|
||||
private update(time: number): void {
|
||||
const diff = Math.min(this.maxSkip, this.updateTime(time));
|
||||
|
||||
if (!this.speed) {
|
||||
this.renderer.snapToGrid(this.player.pos, this.player.pos);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < diff; i++)
|
||||
this.updateStep();
|
||||
}
|
||||
|
||||
private render(): void {
|
||||
this.renderer.setCenter(this.player.pos);
|
||||
this.renderer.clear();
|
||||
|
||||
for (const r of [this.mapView, ...this.entities, this.player])
|
||||
r.render(this.time);
|
||||
}
|
||||
|
||||
private async renderLoop(): Promise<void> {
|
||||
while (true) {
|
||||
this.update(await nextAnimationFrame() - this.initTime);
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
}
|
12
src/renderer/runtime/index.css
Normal file
12
src/renderer/runtime/index.css
Normal file
|
@ -0,0 +1,12 @@
|
|||
html, body, div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
border: 0px;
|
||||
background: #223;
|
||||
overflow: hidden;
|
||||
}
|
15
src/renderer/runtime/math/circle.ts
Normal file
15
src/renderer/runtime/math/circle.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Collidable } from './collision';
|
||||
import { Movement } from './line';
|
||||
import { Point } from './point';
|
||||
|
||||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
export class Circle implements Collidable, Point {
|
||||
private pointCollide = Point.prototype.collide;
|
||||
|
||||
constructor(public readonly p: vec2, public readonly r: number) {}
|
||||
|
||||
public collide(out: vec2, move: Movement, r: number): boolean {
|
||||
return this.pointCollide(out, move, r + this.r);
|
||||
}
|
||||
}
|
7
src/renderer/runtime/math/collision.ts
Normal file
7
src/renderer/runtime/math/collision.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { Movement } from './line';
|
||||
|
||||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
export interface Collidable {
|
||||
collide(out: vec2, move: Movement, r: number): boolean;
|
||||
}
|
141
src/renderer/runtime/math/line.ts
Normal file
141
src/renderer/runtime/math/line.ts
Normal file
|
@ -0,0 +1,141 @@
|
|||
import { mat2, vec2 } from 'gl-matrix';
|
||||
import { Collidable } from './collision';
|
||||
|
||||
const rot90 = mat2.fromValues(
|
||||
0, 1,
|
||||
-1, 0,
|
||||
);
|
||||
|
||||
export function normal(out: vec2, a: vec2): vec2 {
|
||||
return vec2.transformMat2(out, a, rot90);
|
||||
}
|
||||
|
||||
export function crossz(a: vec2, b: vec2): number {
|
||||
return a[0] * b[1] - a[1] * b[0];
|
||||
}
|
||||
|
||||
export class Line {
|
||||
constructor(
|
||||
public readonly p: vec2,
|
||||
public readonly v: vec2,
|
||||
) {}
|
||||
|
||||
public getNormal(out: vec2): vec2 {
|
||||
return normal(out, this.v);
|
||||
}
|
||||
|
||||
public projectPointDistance(p2: vec2): number {
|
||||
const v2 = vec2.sub(vec2.create(), p2, this.p);
|
||||
return vec2.dot(this.v, v2);
|
||||
}
|
||||
|
||||
public projectPoint(out: vec2, p2: vec2): vec2 {
|
||||
const d = this.projectPointDistance(p2);
|
||||
return vec2.scaleAndAdd(out, this.p, this.v, d);
|
||||
}
|
||||
|
||||
public distancePoint(p2: vec2): number {
|
||||
const v2 = vec2.sub(vec2.create(), p2, this.p);
|
||||
return crossz(this.v, v2);
|
||||
}
|
||||
|
||||
public intersectLine(out: vec2, l2: Line): vec2 {
|
||||
const vp = vec2.sub(vec2.create(), l2.p, this.p);
|
||||
const d = crossz(vp, this.v);
|
||||
const d2 = d / crossz(this.v, l2.v);
|
||||
return vec2.scaleAndAdd(out, l2.p, l2.v, d2);
|
||||
}
|
||||
}
|
||||
|
||||
export class Movement {
|
||||
public readonly v: vec2;
|
||||
|
||||
constructor(
|
||||
public readonly src: vec2,
|
||||
public readonly dest: vec2,
|
||||
) {
|
||||
this.v = vec2.sub(vec2.create(), dest, src);
|
||||
}
|
||||
|
||||
public intersectLine(out: vec2, l: Line): vec2 {
|
||||
const vp = vec2.sub(vec2.create(), l.p, this.src);
|
||||
const d = crossz(vp, this.v);
|
||||
const d2 = d / crossz(this.v, l.v);
|
||||
return vec2.scaleAndAdd(out, l.p, l.v, d2);
|
||||
}
|
||||
|
||||
public passes(p: vec2): boolean {
|
||||
const vp = vec2.sub(vec2.create(), p, this.src);
|
||||
const d = vec2.dot(this.v, vp);
|
||||
return d >= 0 && d <= vec2.sqrLen(this.v);
|
||||
}
|
||||
|
||||
public toLineSegment(): LineSegment {
|
||||
return LineSegment.fromPoints(this.src, this.dest);
|
||||
}
|
||||
|
||||
public translate(t: vec2): Movement {
|
||||
const src = vec2.add(vec2.create(), this.src, t);
|
||||
const dest = vec2.add(vec2.create(), this.dest, t);
|
||||
return new Movement(src, dest);
|
||||
}
|
||||
}
|
||||
|
||||
export class LineSegment extends Line implements Collidable {
|
||||
public static fromPoints(p1: vec2, p2: vec2): LineSegment {
|
||||
const d = vec2.dist(p1, p2);
|
||||
const v = vec2.sub(vec2.create(), p2, p1);
|
||||
vec2.scale(v, v, 1 / d);
|
||||
|
||||
return new LineSegment(p1, v, d);
|
||||
}
|
||||
|
||||
constructor(
|
||||
p: vec2,
|
||||
v: vec2,
|
||||
public readonly l: number,
|
||||
) {
|
||||
super(p, v);
|
||||
}
|
||||
|
||||
public getP2(out: vec2): vec2 {
|
||||
return vec2.scaleAndAdd(out, this.p, this.v, this.l);
|
||||
}
|
||||
|
||||
public containsPoint(p2: vec2): boolean {
|
||||
const d = this.projectPointDistance(p2);
|
||||
return (d >= 0 && d <= this.l);
|
||||
}
|
||||
|
||||
public collide(out: vec2, move: Movement, r: number): boolean {
|
||||
if (this.distancePoint(move.src) < 0)
|
||||
return false;
|
||||
|
||||
if (crossz(move.v, this.v) < 0)
|
||||
return false;
|
||||
|
||||
const t = this.getNormal(vec2.create());
|
||||
vec2.scale(t, t, -r);
|
||||
|
||||
const refMove = move.translate(t);
|
||||
|
||||
if (!this.collideRef(out, refMove))
|
||||
return false;
|
||||
|
||||
vec2.sub(out, out, t);
|
||||
return true;
|
||||
}
|
||||
|
||||
private collideRef(out: vec2, move: Movement): boolean {
|
||||
if (this.distancePoint(move.dest) >= 0)
|
||||
return false;
|
||||
|
||||
const x = move.intersectLine(vec2.create(), this);
|
||||
if (!this.containsPoint(x))
|
||||
return false;
|
||||
|
||||
this.projectPoint(out, move.dest);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
40
src/renderer/runtime/math/point.ts
Normal file
40
src/renderer/runtime/math/point.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Collidable } from './collision';
|
||||
import { Line, Movement, normal } from './line';
|
||||
|
||||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
export class Point implements Collidable {
|
||||
constructor(public readonly p: vec2) {}
|
||||
|
||||
public collide(out: vec2, move: Movement, r: number): boolean {
|
||||
const moveLine = move.toLineSegment();
|
||||
|
||||
if (moveLine.projectPointDistance(this.p) < 0)
|
||||
return false;
|
||||
|
||||
const d = moveLine.distancePoint(this.p) / r;
|
||||
if (Math.abs(d) >= 1)
|
||||
return false;
|
||||
|
||||
const e = Math.sqrt(1 - d * d);
|
||||
|
||||
const t = moveLine.getNormal(vec2.create());
|
||||
vec2.scale(t, t, d);
|
||||
vec2.scaleAndAdd(t, t, moveLine.v, e);
|
||||
|
||||
const tr = vec2.scale(vec2.create(), t, r);
|
||||
|
||||
const refMove = move.translate(tr);
|
||||
|
||||
if (vec2.sqrDist(this.p, move.src) > r * r && !refMove.passes(this.p))
|
||||
return false;
|
||||
|
||||
normal(t, t);
|
||||
|
||||
const tang = new Line(this.p, t);
|
||||
tang.projectPoint(out, refMove.dest);
|
||||
vec2.sub(out, out, tr);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
12
src/renderer/runtime/model/data/collision.ts
Normal file
12
src/renderer/runtime/model/data/collision.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export interface PolygonCollision {
|
||||
readonly type: 'polygon';
|
||||
readonly vertices: Array<[number, number]>;
|
||||
}
|
||||
|
||||
export interface CircleCollision {
|
||||
readonly type: 'circle';
|
||||
readonly center: [number, number];
|
||||
readonly radius: number;
|
||||
}
|
||||
|
||||
export type Collision = PolygonCollision | CircleCollision;
|
29
src/renderer/runtime/model/data/entity.ts
Normal file
29
src/renderer/runtime/model/data/entity.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { Collision } from './collision';
|
||||
|
||||
export interface EntityAnimation {
|
||||
readonly sequence: ReadonlyArray<[number, number]>;
|
||||
}
|
||||
|
||||
export interface EntityDataInput {
|
||||
readonly sprite: string;
|
||||
readonly anchor?: [number, number];
|
||||
readonly collision?: Collision[];
|
||||
readonly frames?: number;
|
||||
readonly animation?: EntityAnimation;
|
||||
}
|
||||
|
||||
export class EntityData {
|
||||
public readonly sprite: string;
|
||||
public readonly anchor: [number, number];
|
||||
public readonly collision: Collision[];
|
||||
public readonly frames: number;
|
||||
public readonly animation?: EntityAnimation;
|
||||
|
||||
constructor(input: EntityDataInput) {
|
||||
this.sprite = input.sprite;
|
||||
this.anchor = input.anchor || [0.5, 0.5];
|
||||
this.collision = input.collision || [];
|
||||
this.frames = input.frames || 1;
|
||||
this.animation = input.animation;
|
||||
}
|
||||
}
|
29
src/renderer/runtime/model/data/map.ts
Normal file
29
src/renderer/runtime/model/data/map.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { Collision } from './collision';
|
||||
|
||||
export interface MapLayer {
|
||||
readonly tiles: number[][];
|
||||
}
|
||||
|
||||
export interface MapDataInput {
|
||||
readonly tiles: string[];
|
||||
readonly layers: MapLayer[];
|
||||
readonly collision: Collision[];
|
||||
}
|
||||
|
||||
export class MapData {
|
||||
public readonly tiles: string[];
|
||||
public readonly layers: MapLayer[];
|
||||
public readonly collision: Collision[];
|
||||
|
||||
public readonly width: number;
|
||||
public readonly height: number;
|
||||
|
||||
constructor(data: MapDataInput) {
|
||||
this.tiles = data.tiles;
|
||||
this.layers = data.layers;
|
||||
this.collision = data.collision;
|
||||
|
||||
this.height = this.layers[0].tiles.length;
|
||||
this.width = this.layers[0].tiles[0].length;
|
||||
}
|
||||
}
|
59
src/renderer/runtime/util.ts
Normal file
59
src/renderer/runtime/util.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
export function recordToMap<T>(r: Record<string, T>): Map<string, T> {
|
||||
const ret = new Map();
|
||||
|
||||
for (const k of Object.keys(r))
|
||||
ret.set(k, r[k]);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function mapValues<K, V1, V2>(f: (v: V1) => V2, map: Map<K, V1>): Map<K, V2> {
|
||||
const ret: Map<K, V2> = new Map();
|
||||
|
||||
for (const [k, v] of map)
|
||||
ret.set(k, f(v));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function mapValuesAsync<K, V1, V2>(f: (v: V1) => Promise<V2>, map: Map<K, V1>): Promise<Map<K, V2>> {
|
||||
const ret: Map<K, V2> = new Map();
|
||||
|
||||
for (const [k, v] of mapValues(f, map))
|
||||
ret.set(k, await v);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function nextPowerOf2(n: number): number {
|
||||
let i = 1;
|
||||
|
||||
while (i < n)
|
||||
i *= 2;
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
export class Listenable<T extends any[]> {
|
||||
private readonly listeners: Array<(...args: T) => void> = [];
|
||||
|
||||
public addListener(listener: (...args: T) => void): void {
|
||||
this.listeners.push(listener);
|
||||
}
|
||||
|
||||
protected runListeners(...args: T): void {
|
||||
this.listeners.forEach((l) => l(...args));
|
||||
}
|
||||
}
|
||||
|
||||
export async function getJSON(url: string): Promise<any> {
|
||||
const res = await window.fetch(url);
|
||||
if (res.status < 200 || res.status >= 300)
|
||||
throw new Error(res.statusText);
|
||||
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
export function nextAnimationFrame(): Promise<DOMHighResTimeStamp> {
|
||||
return new Promise((resolve) => window.requestAnimationFrame(resolve));
|
||||
}
|
72
src/renderer/runtime/view/entity.ts
Normal file
72
src/renderer/runtime/view/entity.ts
Normal file
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
78
src/renderer/runtime/view/input/gameinput.ts
Normal file
78
src/renderer/runtime/view/input/gameinput.ts
Normal file
|
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
44
src/renderer/runtime/view/input/inputhandler.ts
Normal file
44
src/renderer/runtime/view/input/inputhandler.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
173
src/renderer/runtime/view/map.ts
Normal file
173
src/renderer/runtime/view/map.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
100
src/renderer/runtime/view/renderer/renderer.ts
Normal file
100
src/renderer/runtime/view/renderer/renderer.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
79
src/renderer/runtime/view/renderer/shaders.ts
Normal file
79
src/renderer/runtime/view/renderer/shaders.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
10
src/renderer/runtime/view/renderer/shaders/default.fs
Normal file
10
src/renderer/runtime/view/renderer/shaders/default.fs
Normal file
|
@ -0,0 +1,10 @@
|
|||
precision highp float;
|
||||
|
||||
varying vec2 vTextureCoord;
|
||||
|
||||
uniform sampler2D uSampler;
|
||||
|
||||
|
||||
void main(void) {
|
||||
gl_FragColor = texture2D(uSampler, vTextureCoord);
|
||||
}
|
13
src/renderer/runtime/view/renderer/shaders/default.vs
Normal file
13
src/renderer/runtime/view/renderer/shaders/default.vs
Normal file
|
@ -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;
|
||||
}
|
72
src/renderer/runtime/view/sprite.ts
Normal file
72
src/renderer/runtime/view/sprite.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
33
src/renderer/runtime/view/util/image.ts
Normal file
33
src/renderer/runtime/view/util/image.ts
Normal file
|
@ -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];
|
||||
}
|
Reference in a new issue