refactor: tile rendering

This commit is contained in:
cha0s 2021-03-26 15:05:46 -05:00
parent 869829fffa
commit 69ec257012
8 changed files with 281 additions and 239 deletions

View File

@ -1,17 +1,5 @@
export default (Layers) => class PhysicsLayers extends Layers {
addLayer(layer) {
super.addLayer(layer);
layer.on('tile-data-changed', layer.onTileDataChanged, layer);
}
removeLayer(layer) {
if (-1 !== this.layers.indexOf(layer)) {
layer.off('tile-data-changed', layer.onTileDataChanged);
}
super.removeLayer(layer);
}
set world(world) {
for (let index = 0; index < this.layers.length; index++) {
this.layers[index].world = world;

View File

@ -1,8 +1,7 @@
import {EntityListView} from '@avocado/entity';
import {Container, Sprite} from '@avocado/graphics';
import {Vector} from '@avocado/math';
import {Container} from '@avocado/graphics';
import TilesRenderer from './tiles-renderer';
import TilesView from './tiles-view';
const views = new Set();
@ -11,15 +10,9 @@ export default class LayerView extends Container {
constructor(layer, renderer) {
super();
this.entityListView = new EntityListView(layer.entityList);
this.tilesView = new TilesView(layer.tiles, layer.tileset, renderer);
this.layer = layer;
this.layerContainer = new Container();
this.renderer = renderer;
// Handle entity add/remove.
layer.on('tile-data-changed', this.onLayerTileDataChanged, this);
this.onLayerTileDataChanged();
layer.on('tilesetChanged', this.onLayerTilesetChanged, this);
this.onLayerTilesetChanged(layer.tileset, undefined);
this.addChild(this.layerContainer);
this.addChild(this.tilesView);
this.addChild(this.entityListView);
if (module.hot) {
views.add(this);
@ -31,54 +24,20 @@ export default class LayerView extends Container {
if (module.hot) {
views.delete(this);
}
this.layer.off('tile-data-changed', this.onLayerTileDataChanged);
this.layer.off('tilesetChanged', this.onLayerTilesetChanged);
}
onLayerTileDataChanged() {
this.render();
}
onLayerTilesetChanged(oldTileset, newTileset) {
if (oldTileset) {
oldTileset.off('imageChanged', this.onLayerTilesetImageChanged, this);
}
if (newTileset) {
newTileset.on('imageChanged', this.onLayerTilesetImageChanged, this);
}
this.onLayerTilesetImageChanged();
}
onLayerTilesetImageChanged() {
this.render();
}
render() {
this.layerContainer.removeAllChildren();
if (!this.renderer) {
return;
}
if (!this.layer.tileset) {
return;
}
if (Vector.isZero(this.layer.tiles.size)) {
return;
}
const tilesRenderer = new TilesRenderer(this.layer.tiles, this.layer.tileset);
const chunk = tilesRenderer.renderChunk(this.renderer);
const tilesSprite = new Sprite(chunk);
tilesSprite.anchor = [0, 0];
this.layerContainer.addChild(tilesSprite);
renderChunksForExtent(extent) {
this.tilesView.renderChunksForExtent(extent);
}
}
if (module.hot) {
module.hot.accept('./tiles-renderer', () => {
const it = views.values();
for (let value = it.next(); value.done !== true; value = it.next()) {
const {value: view} = value;
view.render();
}
});
}
// if (module.hot) {
// module.hot.accept('./tiles-renderer', () => {
// const it = views.values();
// for (let value = it.next(); value.done !== true; value = it.next()) {
// const {value: view} = value;
// view.render();
// }
// });
// }

View File

@ -7,7 +7,7 @@ export default class LayersView extends Container {
constructor(layers, renderer) {
super();
this.layers = layers;
this.layerViews = {};
this.layerViews = [];
this.renderer = renderer;
this.layers.on('layerAdded', this.onLayerAdded, this);
for (let i = 0; i < this.layers.layers.length; i++) {
@ -30,4 +30,10 @@ export default class LayersView extends Container {
this.removeChild(layerView);
}
renderChunksForExtent(extent) {
for (let i = 0; i < this.layerViews.length; i++) {
this.layerViews[i].renderChunksForExtent(extent);
}
}
}

View File

@ -30,7 +30,6 @@ export default (latus) => {
this.entityList.destroy();
this.entityList.off('entityAdded', this.onEntityAddedToLayer);
this.entityList.off('entityRemoved', this.onEntityRemovedFromLayer);
this.tiles.off('tile-data-changed', this.onTileDataChanged);
if (this.tileset) {
this.tileset.destroy();
}
@ -89,10 +88,6 @@ export default (latus) => {
this.emit('entityRemoved', entity);
}
onTileDataChanged() {
this.emit('tile-data-changed');
}
removeEntity(entity) {
this.entityList.removeEntity(entity);
}
@ -126,10 +121,8 @@ export default (latus) => {
setTiles(tiles) {
if (this.tiles) {
this.stopSynchronizing(this.tiles);
this.tiles.off('tile-data-changed', this.onTileDataChanged);
}
this.tiles = tiles;
this.tiles.on('tile-data-changed', this.onTileDataChanged, this);
this.startSynchronizing(tiles);
}
@ -139,7 +132,6 @@ export default (latus) => {
tick(elapsed) {
this.entityList.tick(elapsed);
this.tiles.tick();
}
tileAt(position) {

View File

@ -19,8 +19,6 @@ export default (latus) => {
);
return class Tiles extends decorate(Class) {
#tileDataChanged = false;
#packets = [];
#updates = [];
@ -37,6 +35,7 @@ export default (latus) => {
if ('TilesUpdate' === packet.constructor.type) {
const {position: [x, y], size: [w, h], tiles} = packet.data;
this.stampAt([x, y, w, h], tiles);
this.emit('update', [x, y, w, h]);
}
}
@ -140,7 +139,6 @@ export default (latus) => {
if ('client' !== process.env.SIDE) {
this.#updates.push([[x, y], [1, 1], [tile]]);
}
this.#tileDataChanged = true;
}
slice(rectangle) {
@ -198,18 +196,8 @@ export default (latus) => {
sx -= w;
sy += 1;
}
if (isDirty) {
this.#tileDataChanged = true;
if ('client' !== process.env.SIDE) {
this.#updates.push([[x, y], [w, h], tiles]);
}
}
}
tick() {
if (this.#tileDataChanged) {
this.emit('tile-data-changed');
this.#tileDataChanged = false;
if (isDirty && 'client' !== process.env.SIDE) {
this.#updates.push([[x, y], [w, h], tiles]);
}
}

View File

@ -12,4 +12,8 @@ export default class RoomView extends Container {
this.sort();
}
renderChunksForExtent(extent) {
this.layersView.renderChunksForExtent(extent);
}
}

View File

@ -1,147 +0,0 @@
import {
Canvas,
Container,
Image,
Sprite,
} from '@avocado/graphics';
import {
noise,
Rectangle,
Vector,
Vertice,
} from '@avocado/math';
export default class TilesRenderer {
constructor(tiles, tileset) {
this.tiles = tiles;
this.tileset = tileset;
}
renderChunk(renderer, rectangle) {
// eslint-disable-next-line no-param-reassign
rectangle ||= Rectangle.compose([0, 0], this.tiles.size);
const slice = this.tiles.slice(rectangle);
if (!slice) {
return undefined;
}
const mask = this.renderMask(rectangle);
const size = Rectangle.size(rectangle);
const {tileSize} = this.tileset;
if (mask) {
mask.anchor = [0, 0];
}
const rowWidth = size[0] * tileSize[0];
// Render all tiles.
const container = new Container();
if (mask) {
container.mask = mask;
}
const position = [0, 0];
for (let i = 0; i < slice.length; ++i) {
const index = slice[i];
if (index > 0) {
// Lookup subimage.
const subimage = this.tileset.subimage(index);
if (subimage) {
const sprite = new Sprite(subimage);
sprite.anchor = [0, 0];
sprite.position = position;
container.addChild(sprite);
}
}
// Only adds, please.
position[0] += tileSize[0];
if (rowWidth === position[0]) {
position[0] = 0;
position[1] += tileSize[1];
}
}
const renderable = new Container();
renderable.addChild(container);
if (mask) {
renderable.addChild(mask);
}
const canvasSize = Vector.mul(size, tileSize);
const canvas = new Canvas(canvasSize);
canvas.render(renderable, renderer);
container.destroy();
if (mask) {
mask.destroy();
}
// Convert to image and return.
const image = canvas.toImage();
canvas.destroy();
return image;
}
renderMask(rectangle) {
const hulls = this.tiles.indexHulls(0);
if (!hulls.length > 0) {
return undefined;
}
const size = Rectangle.size(rectangle);
const {tileSize} = this.tileset;
const canvasSize = Vector.mul(size, tileSize);
const canvas = window.document.createElement('canvas');
[canvas.width, canvas.height] = canvasSize;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'rgba(255, 255, 255, 1)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgba(0, 0, 0, 1)';
for (let i = 0; i < hulls.length; ++i) {
const scaled = [];
for (let j = 0; j < hulls[i].length; j++) {
scaled.push(Vector.scale(hulls[i][j], 16));
}
scaled.push(Vector.scale(hulls[i][0], 16));
ctx.beginPath();
ctx.moveTo(scaled[0][0], scaled[0][1]);
for (let j = 0; j < scaled.length - 1; j++) {
const p0 = scaled[j + 0];
const p1 = scaled[j + 1];
const points = Vertice.bresenham(p0, p1);
const isReversed = (
(p0[0] > p1[0] && points[0][0] < points[points.length - 1][0])
|| (p0[1] > p1[1] && points[0][1] < points[points.length - 1][1])
);
const u = Vector.scale(
Vector.fromRadians((Math.PI * 0.5) + Vector.toRadians(Vector.sub(p1, p0))),
2,
);
for (
let k = (isReversed ? points.length - 1 : 0);
(isReversed ? k >= 0 : k < points.length);
k += (isReversed ? -1 : 1)
) {
const [x, y] = points[k];
const shift = Vector.add(
u,
Vector.scale(
Vector.mul(
[
noise(Vector.scale([x, y], 10)),
noise(Vector.scale([y, x], 10)),
],
[
noise(Vector.scale([x, y], 20)),
noise(Vector.scale([y, x], 20)),
],
),
5,
),
);
const [vx, vy] = Vector.add(
points[k],
shift,
);
ctx.lineTo(vx, vy);
}
}
ctx.closePath();
ctx.fill();
}
return new Sprite(Image.from(canvas));
}
}

View File

@ -0,0 +1,252 @@
import {
Canvas,
Container,
// Image,
Sprite,
} from '@avocado/graphics';
import {Vector} from '@avocado/math';
export default class TilesView extends Container {
static CHUNK_SIZE = [16, 16];
constructor(tiles, tileset, renderer) {
super();
this.tiles = tiles;
this.tiles.on('update', ([x, y, w, h]) => {
const chunks = this.chunksForUnitExtent([x, y, w, h]);
for (let i = 0; i < chunks.length; ++i) {
const chunk = chunks[i];
const rendered = this.rendered[this.constructor.hashChunk(chunk)];
if (rendered) {
rendered.destroy(true);
}
this.renderChunk(chunk);
}
});
this.tileset = tileset;
this.renderer = renderer;
this.rendered = [];
}
chunksForExtent([x, y, w, h]) {
const [tw, th] = this.tileset.tileSize;
/* eslint-disable no-param-reassign */
x /= tw;
w /= tw;
y /= th;
h /= th;
/* eslint-enable no-param-reassign */
return this.chunksForUnitExtent([x, y, w, h]);
}
chunksForUnitExtent([x, y, w, h]) {
const [fw, fh] = this.tiles.size;
/* eslint-disable no-param-reassign */
if (
x >= fw
|| y >= fh
) {
return [];
}
if (x < 0) {
w += x;
x = 0;
}
if (y < 0) {
h += y;
y = 0;
}
if (
w <= 0
|| h <= 0
) {
return [];
}
if (w > fw - x) {
w = fw - x;
}
if (h > fh - y) {
h = fh - y;
}
/* eslint-enable no-param-reassign */
const [cw, ch] = this.constructor.CHUNK_SIZE;
let [cx, cy] = Vector.floor(Vector.div([x, y], [cw, ch]));
let [chunksX, chunksY] = Vector.floor(
Vector.div([w - 1, h - 1], [cw, ch]),
);
const [cmx, cmy] = Vector.floor(Vector.mod([x, y], [cw, ch]));
const [cmw, cmh] = Vector.mod([w, h], [cw, ch]);
if (cmx + cmw >= cw) {
chunksX += 1;
}
if (cmy + cmh >= ch) {
chunksY += 1;
}
const chunks = [];
for (let iy = 0; iy <= chunksY; ++iy) {
for (let ix = 0; ix <= chunksX; ++ix) {
chunks.push([cx, cy]);
cx += 1;
}
cx -= chunksX + 1;
cy += 1;
}
return chunks;
}
static hashChunk(chunk) {
// eslint-disable-next-line no-bitwise
return (chunk[1] << 16) + chunk[0];
}
renderChunk(chunk) {
if (
chunk[0] < 0
|| chunk[1] < 0
) {
return;
}
const [fw, fh] = this.tiles.size;
const [cw, ch] = this.constructor.CHUNK_SIZE;
const [cx, cy] = Vector.mul(chunk, [cw, ch]);
if (
cx >= fw
|| cy >= fh
) {
return;
}
const sw = cx + cw > fw ? (cx + cw) - fw : cw;
const sh = cy + ch > fh ? (cy + ch) - fh : ch;
const slice = this.tiles.slice([cx, cy, sw, sh]);
if (!slice) {
return;
}
// const mask = this.renderMask(rectangle);
const [tw, th] = this.tileset.tileSize;
// if (mask) {
// mask.anchor = [0, 0];
// }
const rw = cw * tw;
const container = new Container();
// if (mask) {
// container.mask = mask;
// }
let [x, y] = [0, 0];
for (let i = 0; i < slice.length; ++i) {
const index = slice[i];
if (index > 0) {
const subimage = this.tileset.subimage(index);
if (subimage) {
const sprite = new Sprite(subimage);
sprite.anchor = [0, 0];
sprite.position = [x, y];
container.addChild(sprite);
}
}
x += tw;
if (rw === x) {
x = 0;
y += th;
}
}
const renderable = new Container();
renderable.addChild(container);
// if (mask) {
// renderable.addChild(mask);
// }
const canvas = new Canvas([rw, ch * th]);
canvas.render(renderable, this.renderer);
container.destroy();
// if (mask) {
// mask.destroy();
// }
const sprite = new Sprite(canvas.toImage());
sprite.anchor = [0, 0];
sprite.position = [chunk[0] * cw * tw, chunk[1] * ch * th];
canvas.destroy();
this.rendered[this.constructor.hashChunk(chunk)] = sprite;
this.addChild(sprite);
}
// eslint-disable-next-line class-methods-use-this
renderChunksForExtent(extent) {
const chunks = this.chunksForExtent(extent);
for (let i = 0; i < chunks.length; ++i) {
const chunk = chunks[i];
if (!this.rendered[this.constructor.hashChunk(chunk)]) {
this.renderChunk(chunk);
}
}
}
// renderMask(rectangle) {
// const hulls = this.tiles.indexHulls(0);
// if (!hulls.length > 0) {
// return undefined;
// }
// const size = Rectangle.size(rectangle);
// const {tileSize} = this.tileset;
// const canvasSize = Vector.mul(size, tileSize);
// const canvas = window.document.createElement('canvas');
// [canvas.width, canvas.height] = canvasSize;
// const ctx = canvas.getContext('2d');
// ctx.fillStyle = 'rgba(255, 255, 255, 1)';
// ctx.fillRect(0, 0, canvas.width, canvas.height);
// ctx.fillStyle = 'rgba(0, 0, 0, 1)';
// for (let i = 0; i < hulls.length; ++i) {
// const scaled = [];
// for (let j = 0; j < hulls[i].length; j++) {
// scaled.push(Vector.scale(hulls[i][j], 16));
// }
// scaled.push(Vector.scale(hulls[i][0], 16));
// ctx.beginPath();
// ctx.moveTo(scaled[0][0], scaled[0][1]);
// for (let j = 0; j < scaled.length - 1; j++) {
// const p0 = scaled[j + 0];
// const p1 = scaled[j + 1];
// const points = Vertice.bresenham(p0, p1);
// const isReversed = (
// (p0[0] > p1[0] && points[0][0] < points[points.length - 1][0])
// || (p0[1] > p1[1] && points[0][1] < points[points.length - 1][1])
// );
// const u = Vector.scale(
// Vector.fromRadians((Math.PI * 0.5) + Vector.toRadians(Vector.sub(p1, p0))),
// 2,
// );
// for (
// let k = (isReversed ? points.length - 1 : 0);
// (isReversed ? k >= 0 : k < points.length);
// k += (isReversed ? -1 : 1)
// ) {
// const [x, y] = points[k];
// const shift = Vector.add(
// u,
// Vector.scale(
// Vector.mul(
// [
// noise(Vector.scale([x, y], 10)),
// noise(Vector.scale([y, x], 10)),
// ],
// [
// noise(Vector.scale([x, y], 20)),
// noise(Vector.scale([y, x], 20)),
// ],
// ),
// 5,
// ),
// );
// const [vx, vy] = Vector.add(
// points[k],
// shift,
// );
// ctx.lineTo(vx, vy);
// }
// }
// ctx.closePath();
// ctx.fill();
// }
// return new Sprite(Image.from(canvas));
// }
}