import { Vector, vec } from '../math/vector'; import type { ExcaliburGraphicsContext } from './context/excalibur-graphics-context'; import { BoundingBox } from '../collision/bounding-box'; import type { Color } from '../color'; import { watch } from '../util/watch'; import { AffineMatrix } from '../math/affine-matrix'; export interface GraphicOptions { /** * The width of the graphic */ width?: number; /** * The height of the graphic */ height?: number; /** * Should the graphic be flipped horizontally */ flipHorizontal?: boolean; /** * Should the graphic be flipped vertically */ flipVertical?: boolean; /** * The rotation of the graphic */ rotation?: number; /** * The scale of the graphic */ scale?: Vector; /** * The opacity of the graphic between (0 -1) */ opacity?: number; /** * The tint of the graphic, this color will be multiplied by the original pixel colors */ tint?: Color; /** * The origin of the drawing in pixels to use when applying transforms, by default it will be the center of the image in pixels */ origin?: Vector; } /** * A Graphic is the base Excalibur primitive for something that can be drawn to the {@apilink ExcaliburGraphicsContext}. * {@apilink Sprite}, {@apilink Animation}, {@apilink GraphicsGroup}, {@apilink Canvas}, {@apilink Rectangle}, {@apilink Circle}, and {@apilink Polygon} all derive from the * {@apilink Graphic} abstract class. * * Implementors of a Graphic must override the abstract {@apilink Graphic._drawImage} method to render an image to the graphics context. Graphic * handles all the position, rotation, and scale transformations in {@apilink Graphic._preDraw} and {@apilink Graphic._postDraw} */ export abstract class Graphic { private static _ID: number = 0; readonly id = Graphic._ID++; public transform: AffineMatrix = AffineMatrix.identity(); public tint?: Color; private _transformStale = true; public isStale() { return this._transformStale; } /** * Gets or sets wether to show debug information about the graphic */ public showDebug: boolean = false; private _flipHorizontal = false; /** * Gets or sets the flipHorizontal, which will flip the graphic horizontally (across the y axis) */ public get flipHorizontal(): boolean { return this._flipHorizontal; } public set flipHorizontal(value: boolean) { this._flipHorizontal = value; this._transformStale = true; } private _flipVertical = false; /** * Gets or sets the flipVertical, which will flip the graphic vertically (across the x axis) */ public get flipVertical(): boolean { return this._flipVertical; } public set flipVertical(value: boolean) { this._flipVertical = value; this._transformStale = true; } private _rotation = 0; /** * Gets or sets the rotation of the graphic */ public get rotation(): number { return this._rotation; } public set rotation(value: number) { this._rotation = value; this._transformStale = true; } /** * Gets or sets the opacity of the graphic, 0 is transparent, 1 is solid (opaque). */ public opacity: number = 1; private _scale = Vector.One; /** * Gets or sets the scale of the graphic, this affects the width and */ public get scale() { return this._scale; } public set scale(value: Vector) { this._scale = watch(value, () => { this._transformStale = true; }); this._transformStale = true; } private _origin?: Vector; /** * Gets or sets the origin of the graphic, if not set the center of the graphic is the origin */ public get origin(): Vector | undefined { return this._origin; } public set origin(value: Vector | undefined) { if (value) { this._origin = watch(value, () => { this._transformStale = true; }); } this._transformStale = true; } constructor(options?: GraphicOptions) { if (options) { this.origin = options.origin ?? this.origin; this.flipHorizontal = options.flipHorizontal ?? this.flipHorizontal; this.flipVertical = options.flipVertical ?? this.flipVertical; this.rotation = options.rotation ?? this.rotation; this.opacity = options.opacity ?? this.opacity; this.scale = options.scale ?? this.scale; this.tint = options.tint ?? this.tint; if (options.width) { this._width = options.width; } if (options.height) { this._height = options.height; } } } public cloneGraphicOptions(): GraphicOptions { return { width: this.width / this.scale.x, height: this.height / this.scale.y, origin: this.origin ? this.origin.clone() : undefined, flipHorizontal: this.flipHorizontal, flipVertical: this.flipVertical, rotation: this.rotation, opacity: this.opacity, scale: this.scale ? this.scale.clone() : undefined, tint: this.tint ? this.tint.clone() : undefined }; } private _width: number = 0; /** * Gets or sets the width of the graphic (always positive) */ public get width() { return Math.abs(this._width * this.scale.x); } private _height: number = 0; /** * Gets or sets the height of the graphic (always positive) */ public get height() { return Math.abs(this._height * this.scale.y); } public set width(value: number) { this._width = value; this._transformStale = true; } public set height(value: number) { this._height = value; this._transformStale = true; } /** * Gets a copy of the bounds in pixels occupied by the graphic on the the screen. This includes scale. */ public get localBounds(): BoundingBox { return BoundingBox.fromDimension(this.width, this.height, Vector.Zero); } /** * Draw the whole graphic to the context including transform * @param ex The excalibur graphics context * @param x * @param y */ public draw(ex: ExcaliburGraphicsContext, x: number, y: number): void { this._preDraw(ex, x, y); this._drawImage(ex, 0, 0); this._postDraw(ex); } /** * Meant to be overridden by the graphic implementation to draw the underlying image (HTMLCanvasElement or HTMLImageElement) * to the graphics context without transform. Transformations like position, rotation, and scale are handled by {@apilink Graphic._preDraw} * and {@apilink Graphic._postDraw} * @param ex The excalibur graphics context * @param x * @param y */ protected abstract _drawImage(ex: ExcaliburGraphicsContext, x: number, y: number): void; /** * Apply affine transformations to the graphics context to manipulate the graphic before {@apilink Graphic._drawImage} * @param ex * @param x * @param y */ protected _preDraw(ex: ExcaliburGraphicsContext, x: number, y: number): void { ex.save(); ex.translate(x, y); if (this._transformStale) { this.transform.reset(); this.transform.scale(Math.abs(this.scale.x), Math.abs(this.scale.y)); this._rotate(this.transform); this._flip(this.transform); this._transformStale = false; } ex.multiply(this.transform); // it is important to multiply alphas so graphics respect the current context ex.opacity = ex.opacity * this.opacity; if (this.tint) { ex.tint = this.tint; } } protected _rotate(ex: ExcaliburGraphicsContext | AffineMatrix) { const scaleDirX = this.scale.x > 0 ? 1 : -1; const scaleDirY = this.scale.y > 0 ? 1 : -1; const origin = this.origin ?? vec(this.width / 2, this.height / 2); ex.translate(origin.x, origin.y); ex.rotate(this.rotation); // This is for handling direction changes 1 or -1, that way we don't have mismatched translates() ex.scale(scaleDirX, scaleDirY); ex.translate(-origin.x, -origin.y); } protected _flip(ex: ExcaliburGraphicsContext | AffineMatrix) { if (this.flipHorizontal) { ex.translate(this.width / this.scale.x, 0); ex.scale(-1, 1); } if (this.flipVertical) { ex.translate(0, this.height / this.scale.y); ex.scale(1, -1); } } /** * Apply any additional work after {@apilink Graphic._drawImage} and restore the context state. * @param ex */ protected _postDraw(ex: ExcaliburGraphicsContext): void { if (this.showDebug) { ex.debug.drawRect(0, 0, this.width, this.height); } ex.restore(); } /** * Returns a new instance of the graphic that has the same properties */ abstract clone(): Graphic; }