Skip to content

Commit

Permalink
cache generated sprites
Browse files Browse the repository at this point in the history
  • Loading branch information
trymnilsen committed Oct 13, 2024
1 parent 2c1895a commit 1a6951b
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 33 deletions.
4 changes: 4 additions & 0 deletions ts/src/common/Size.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type Size = {
width: number;
height: number;
};
14 changes: 13 additions & 1 deletion ts/src/game/component/actor/mob/workerSpriteComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,26 @@ import { EntityComponent } from "../../entityComponent.js";
import { EquipmentComponent } from "../../inventory/equipmentComponent.js";

export class WorkerSpriteComponent extends EntityComponent {
private tintMe = false;
override onUpdate(_tick: number): void {
this.tintMe = !this.tintMe;
}
override onDraw(context: RenderScope, screenPosition: Point): void {
const sprite = this.getSprite();
let tint: string | undefined = undefined;
const equipment = this.entity.requireComponent(EquipmentComponent);
const mainItem = equipment.mainItem.getItem();
if (mainItem?.category == ItemCategory.Melee) {
tint = "white";
}

context.drawScreenSpaceSprite({
sprite: sprite,
x: screenPosition.x + 3,
y: screenPosition.y + 2,
targetHeight: 32,
targetWidth: 32,
tint: tint,
});
}

Expand All @@ -26,7 +38,7 @@ export class WorkerSpriteComponent extends EntityComponent {
case ItemCategory.Melee:
return sprites2.knight;
case ItemCategory.Ranged:
return sprites2.bowman;
return sprites2.knight;
case ItemCategory.Magic:
return sprites2.mage;
case ItemCategory.Productivity:
Expand Down
3 changes: 2 additions & 1 deletion ts/src/rendering/items/sprite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type SpriteConfiguration = {
frame?: number;
targetWidth?: number;
targetHeight?: number;
tint?: string;
} & RenderItemConfiguration;

export type NinePatchSpriteConfiguration = {
Expand All @@ -28,7 +29,7 @@ export function spriteRenderer(
targetWidth: number,
targetHeight: number,
frame: number,
binAsset: HTMLImageElement,
binAsset: CanvasImageSource,
context: CanvasContext,
) {
x = Math.floor(x);
Expand Down
106 changes: 76 additions & 30 deletions ts/src/rendering/renderScope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { UIRenderScope } from "./uiRenderContext.js";
import { Bounds } from "../common/bounds.js";
import { sprites } from "../../generated/sprites.js";
import { CanvasContext } from "./canvasContext.js";
import { SpriteCache } from "./spriteCache.js";

export type DrawFunction = (context: RenderScope) => void;

Expand All @@ -33,6 +34,7 @@ export type DrawFunction = (context: RenderScope) => void;
*/
export class RenderScope implements UIRenderScope, UILayoutScope {
private canvasContext: CanvasContext;
private spriteCache: SpriteCache;
private _camera: Camera;
private _assetLoader: AssetLoader;
private _deferredRenderCalls: DrawFunction[] = [];
Expand Down Expand Up @@ -72,9 +74,11 @@ export class RenderScope implements UIRenderScope, UILayoutScope {
canvasContext: CanvasRenderingContext2D,
camera: Camera,
assetLoader: AssetLoader,
spriteCache: SpriteCache,
width: number,
height: number,
) {
this.spriteCache = spriteCache;
this.canvasContext = canvasContext;
this._camera = camera;
this._assetLoader = assetLoader;
Expand Down Expand Up @@ -161,23 +165,6 @@ export class RenderScope implements UIRenderScope, UILayoutScope {
}
}

drawWithTint(
mode: GlobalCompositeOperation,
source: DrawFunction,
color: string,
): void {
this._offscreenContext.clearRect(0, 0, this._width, this._height);
const bitmap = this._offscreenCanvas.transferToImageBitmap();

try {
this.canvasContext.drawImage(bitmap, 0, 0);
this.canvasContext.save();
this.canvasContext.globalCompositeOperation = mode;
} finally {
this.canvasContext.restore();
}
}

drawDeferred(drawFunction: DrawFunction): void {
this._deferredRenderCalls.push(drawFunction);
}
Expand Down Expand Up @@ -220,6 +207,7 @@ export class RenderScope implements UIRenderScope, UILayoutScope {
const transformedConfiguration = Object.assign({}, sprite);
transformedConfiguration.x = transformedX;
transformedConfiguration.y = transformedY;

this.drawScreenSpaceSprite(transformedConfiguration);
}

Expand All @@ -242,19 +230,77 @@ export class RenderScope implements UIRenderScope, UILayoutScope {
if (sprite.frame) {
frame = sprite.frame;
}
spriteRenderer(
sprite.x,
sprite.y,
spriteBounds.x,
spriteBounds.y,
spriteBounds.w,
spriteBounds.h,
targetWidth,
targetHeight,
frame,
this._assetLoader.getBinAsset(sprite.sprite.bin),
this.canvasContext,
);

if (!!sprite.tint) {
// If we are tinting the sprite we first need to draw it to an
// offscreen canvas. We can use compositing to change the color
// and then save it as an image. This image will be used with the
// sprite renderer method rather than the "raw" sprite
let cachedBitmap = this.spriteCache.getSprite(sprite);
if (!cachedBitmap) {
const offscreenCanvas = new OffscreenCanvas(
targetWidth,
targetHeight,
);

const context = offscreenCanvas.getContext("2d");

if (!context) {
console.error("No context available, cannot tint");
return;
}
context.imageSmoothingEnabled = false;

context.fillStyle = sprite.tint;
context.fillRect(0, 0, targetWidth, targetHeight);
context.globalCompositeOperation = "destination-in";
spriteRenderer(
0,
0,
spriteBounds.x,
spriteBounds.y,
spriteBounds.w,
spriteBounds.h,
targetWidth,
targetHeight,
frame,
this._assetLoader.getBinAsset(sprite.sprite.bin),
context,
);

const bitmap = offscreenCanvas.transferToImageBitmap();
this.spriteCache.setSprite(bitmap, sprite);
cachedBitmap = bitmap;
}

spriteRenderer(
sprite.x,
sprite.y,
0,
0,
targetWidth,
targetHeight,
targetWidth,
targetHeight,
frame,
cachedBitmap,
this.canvasContext,
);
} else {
spriteRenderer(
sprite.x,
sprite.y,
spriteBounds.x,
spriteBounds.y,
spriteBounds.w,
spriteBounds.h,
targetWidth,
targetHeight,
frame,
this._assetLoader.getBinAsset(sprite.sprite.bin),
this.canvasContext,
);
}
}

/**
Expand Down
3 changes: 3 additions & 0 deletions ts/src/rendering/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { rgbToHex } from "../common/color.js";
import { GameTime } from "../common/time.js";
import { Camera } from "./camera.js";
import { RenderScope } from "./renderScope.js";
import { SpriteCache } from "./spriteCache.js";

export class Renderer {
private canvasContext: CanvasRenderingContext2D;
Expand All @@ -22,6 +23,7 @@ export class Renderer {
if (!context) {
throw Error("Unable to get 2d context from canvas");
}
const spriteCache = new SpriteCache();
this.currentCamera = new Camera({
x: window.innerWidth,
y: window.innerHeight,
Expand All @@ -34,6 +36,7 @@ export class Renderer {
context,
this.camera,
assetLoader,
spriteCache,
window.innerWidth,
window.innerHeight,
);
Expand Down
27 changes: 27 additions & 0 deletions ts/src/rendering/spriteCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { BitmapImage } from "../../tool/bitmapImage.js";
import { SpriteConfiguration } from "./items/sprite.js";

export class SpriteCache {
private entries: { [id: string]: ImageBitmap } = {};
getSprite(sprite: SpriteConfiguration): ImageBitmap | null {
const cacheKey = makeCacheKey(sprite);
return this.entries[cacheKey] || null;
}
setSprite(image: ImageBitmap, sprite: SpriteConfiguration): void {
const cacheKey = makeCacheKey(sprite);
this.entries[cacheKey] = image;
}
}

function makeCacheKey(sprite: SpriteConfiguration): string {
let key = sprite.sprite.id;
if (sprite.frame) {
key += ":" + sprite.frame;
}

if (sprite.tint) {
key += ":" + sprite.tint;
}

return key;
}
4 changes: 4 additions & 0 deletions ts/src/ui/view/uiColumn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ export class UIColumn extends UIViewGroup {
const layoutSize = child.layout(context, constraints);
const newTotalHeight = measuredHeight + layoutSize.height;
if (newTotalHeight > constraints.height) {
console.log(
`Attempted to layout column with not enough space constraints: ${constraints.height} newTotal: ${newTotalHeight} children: ${this.children.length} i: ${i}`,
);

throw new Error("Non weighted column children height overflow");
}

Expand Down
5 changes: 4 additions & 1 deletion ts/src/ui/view/uiRow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ export class UIRow extends UIViewGroup {
const layoutSize = child.layout(context, constraints);
const newTotalWidth = measuredWidth + layoutSize.width;
if (newTotalWidth > constraints.width) {
throw new Error("Non weighted column children width overflow");
console.log(
`Attempted to layout row with not enough space constraints: ${constraints.width} newTotal: ${newTotalWidth} children: ${this.children.length} i: ${i}`,
);
throw new Error(`Non weighted row children width overflow`);
}

// Set the offset of this item to the total measure height of past
Expand Down

0 comments on commit 1a6951b

Please sign in to comment.