import EventEmitter from 'eventemitter3';
import { groupD8 } from '../../../../maths/matrix/groupD8';
import { Rectangle } from '../../../../maths/shapes/Rectangle';
import { uid } from '../../../../utils/data/uid';
import { deprecation, v8_0_0 } from '../../../../utils/logging/deprecation';
import { NOOP } from '../../../../utils/misc/NOOP';
import { BufferImageSource } from './sources/BufferImageSource';
import { TextureSource } from './sources/TextureSource';
import { TextureMatrix } from './TextureMatrix';
import type { TextureResourceOrOptions } from './utils/textureFrom';
/**
* Stores the width of the non-scalable borders, for example when used with NineSlicePlane texture.
* @memberof rendering
*/
export interface TextureBorders
{
/** left border in pixels */
left: number;
/** top border in pixels */
top: number;
/** right border in pixels */
right: number;
/** bottom border in pixels */
bottom: number;
}
/**
* The UVs data structure for a texture.
* @memberof rendering
*/
export type UVs = {
x0: number;
y0: number;
x1: number;
y1: number;
x2: number;
y2: number;
x3: number;
y3: number;
};
/**
* The options that can be passed to a new Texture
* @memberof rendering
*/
export interface TextureOptions
{
/** the underlying texture data that this texture will use */
source?: TextureSource;
/** optional label, for debugging */
label?: string;
/** The rectangle frame of the texture to show */
frame?: Rectangle;
/** The area of original texture */
orig?: Rectangle;
/** Trimmed rectangle of original texture */
trim?: Rectangle;
/** Default anchor point used for sprite placement / rotation */
defaultAnchor?: { x: number; y: number };
/** Default borders used for 9-slice scaling NineSlicePlane*/
defaultBorders?: TextureBorders;
/** indicates how the texture was rotated by texture packer. See groupD8 */
rotate?: number;
/** set to true if you plan on modifying the uvs of this texture - can affect performance with high numbers of sprites*/
dynamic?: boolean;
}
export interface BindableTexture
{
source: TextureSource;
}
export type TextureSourceLike = TextureSource | TextureResourceOrOptions | string;
/**
* A texture stores the information that represents an image or part of an image.
*
* A texture must have a loaded resource passed to it to work. It does not contain any
* loading mechanisms.
*
* The Assets class can be used to load an texture from a file. This is the recommended
* way as it will handle the loading and caching for you.
*
* ```js
*
* const texture = await Asset.load('assets/image.png');
*
* // once Assets has loaded the image it will be available via the from method
* const sameTexture = Texture.from('assets/image.png');
* // another way to acces the texture once loaded
* const sameAgainTexture = Asset.get('assets/image.png');
*
* const sprite1 = new Sprite(texture);
*
* ```
*
* It cannot be added to the display list directly; instead use it as the texture for a Sprite.
* If no frame is provided for a texture, then the whole image is used.
*
* You can directly create a texture from an image and then reuse it multiple times like this :
*
* ```js
* import { Sprite, Texture } from 'pixi.js';
*
* const texture = await Asset.load('assets/image.png');
* const sprite1 = new Sprite(texture);
* const sprite2 = new Sprite(texture);
* ```
*
* If you didn't pass the texture frame to constructor, it enables `noFrame` mode:
* it subscribes on baseTexture events, it automatically resizes at the same time as baseTexture.
* @memberof rendering
* @class
*/
export class Texture extends EventEmitter<{
update: Texture
destroy: Texture
}> implements BindableTexture
{
/**
* Helper function that creates a returns Texture based on the source you provide.
* The source should be loaded and ready to go. If not its best to grab the asset using Assets.
* @param id - String or Source to create texture from
* @param skipCache - Skip adding the texture to the cache
* @returns The texture based on the Id provided
*/
public static from: (id: TextureSourceLike, skipCache?: boolean) => Texture;
/** label used for debugging */
public label?: string;
/** unique id for this texture */
public uid = uid('texture');
/**
* Has the texture been destroyed?
* @readonly
*/
public destroyed: boolean;
public _source: TextureSource;
/**
* Indicates whether the texture is rotated inside the atlas
* set to 2 to compensate for texture packer rotation
* set to 6 to compensate for spine packer rotation
* can be used to rotate or mirror sprites
* See groupD8 for explanation
*/
public readonly rotate: number;
/** A uvs object based on the given frame and the texture source */
public readonly uvs: UVs = { x0: 0, y0: 0, x1: 0, y1: 0, x2: 0, y2: 0, x3: 0, y3: 0 };
/**
* Anchor point that is used as default if sprite is created with this texture.
* Changing the `defaultAnchor` at a later point of time will not update Sprite's anchor point.
* @default {0,0}
*/
public readonly defaultAnchor?: { x: number; y: number };
/**
* Default width of the non-scalable border that is used if 9-slice plane is created with this texture.
* @since 7.2.0
* @see scene.NineSliceSprite
*/
public readonly defaultBorders?: TextureBorders;
/**
* This is the area of the BaseTexture image to actually copy to the Canvas / WebGL when rendering,
* irrespective of the actual frame size or placement (which can be influenced by trimmed texture atlases)
*/
public readonly frame = new Rectangle();
/** This is the area of original texture, before it was put in atlas. */
public readonly orig: Rectangle;
/**
* This is the trimmed area of original texture, before it was put in atlas
* Please call `updateUvs()` after you change coordinates of `trim` manually.
*/
public readonly trim: Rectangle;
/**
* Does this Texture have any frame data assigned to it?
*
* This mode is enabled automatically if no frame was passed inside constructor.
*
* In this mode texture is subscribed to baseTexture events, and fires `update` on any change.
*
* Beware, after loading or resize of baseTexture event can fired two times!
* If you want more control, subscribe on baseTexture itself.
* @example
* texture.on('update', () => {});
*/
public noFrame = false;
/**
* Set to true if you plan on modifying the uvs of this texture.
* When this is the case, sprites and other objects using the texture will
* make sure to listen for changes to the uvs and update their vertices accordingly.
*/
public dynamic = false;
private _textureMatrix: TextureMatrix;
/** is it a texture? yes! used for type checking */
public readonly isTexture = true;
/**
* @param {TextureOptions} param0 - Options for the texture
*/
constructor({
source,
label,
frame,
orig,
trim,
defaultAnchor,
defaultBorders,
rotate,
dynamic
}: TextureOptions = {})
{
super();
this.label = label;
this.source = source?.source ?? new TextureSource();
this.noFrame = !frame;
if (frame)
{
this.frame.copyFrom(frame);
}
else
{
const { width, height } = this._source;
this.frame.width = width;
this.frame.height = height;
}
this.orig = orig || this.frame;
this.trim = trim;
this.rotate = rotate ?? 0;
this.defaultAnchor = defaultAnchor;
this.defaultBorders = defaultBorders;
this.destroyed = false;
this.dynamic = dynamic || false;
this.updateUvs();
}
set source(value: TextureSource)
{
if (this._source)
{
this._source.off('resize', this.update, this);
}
this._source = value;
value.on('resize', this.update, this);
this.emit('update', this);
}
/** the underlying source of the texture (equivalent of baseTexture in v7) */
get source(): TextureSource
{
return this._source;
}
/** returns a TextureMatrix instance for this texture. By default, that object is not created because its heavy. */
get textureMatrix()
{
if (!this._textureMatrix)
{
this._textureMatrix = new TextureMatrix(this);
}
return this._textureMatrix;
}
/** The width of the Texture in pixels. */
get width(): number
{
return this.orig.width;
}
/** The height of the Texture in pixels. */
get height(): number
{
return this.orig.height;
}
/** Call this function when you have modified the frame of this texture. */
public updateUvs()
{
const { uvs, frame } = this;
const { width, height } = this._source;
const nX = frame.x / width;
const nY = frame.y / height;
const nW = frame.width / width;
const nH = frame.height / height;
let rotate = this.rotate;
if (rotate)
{
// width and height div 2 div baseFrame size
const w2 = nW / 2;
const h2 = nH / 2;
// coordinates of center
const cX = nX + w2;
const cY = nY + h2;
rotate = groupD8.add(rotate, groupD8.NW); // NW is top-left corner
uvs.x0 = cX + (w2 * groupD8.uX(rotate));
uvs.y0 = cY + (h2 * groupD8.uY(rotate));
rotate = groupD8.add(rotate, 2); // rotate 90 degrees clockwise
uvs.x1 = cX + (w2 * groupD8.uX(rotate));
uvs.y1 = cY + (h2 * groupD8.uY(rotate));
rotate = groupD8.add(rotate, 2);
uvs.x2 = cX + (w2 * groupD8.uX(rotate));
uvs.y2 = cY + (h2 * groupD8.uY(rotate));
rotate = groupD8.add(rotate, 2);
uvs.x3 = cX + (w2 * groupD8.uX(rotate));
uvs.y3 = cY + (h2 * groupD8.uY(rotate));
}
else
{
uvs.x0 = nX;
uvs.y0 = nY;
uvs.x1 = nX + nW;
uvs.y1 = nY;
uvs.x2 = nX + nW;
uvs.y2 = nY + nH;
uvs.x3 = nX;
uvs.y3 = nY + nH;
}
}
/**
* Destroys this texture
* @param destroySource - Destroy the source when the texture is destroyed.
*/
public destroy(destroySource = false)
{
if (this._source)
{
if (destroySource)
{
this._source.destroy();
this._source = null;
}
}
this._textureMatrix = null;
this.destroyed = true;
this.emit('destroy', this);
this.removeAllListeners();
}
/** call this if you have modified the `texture outside` of the constructor */
public update(): void
{
if (this.noFrame)
{
this.frame.width = this._source.width;
this.frame.height = this._source.height;
}
this.updateUvs();
this.emit('update', this);
}
/** @deprecated since 8.0.0 */
get baseTexture(): TextureSource
{
// #if _DEBUG
deprecation(v8_0_0, 'Texture.baseTexture is now Texture.source');
// #endif
return this._source;
}
/** an Empty Texture used internally by the engine */
public static EMPTY: Texture;
/** a White texture used internally by the engine */
public static WHITE: Texture;
}
Texture.EMPTY = new Texture({
label: 'EMPTY',
source: new TextureSource({
label: 'EMPTY',
})
});
Texture.EMPTY.destroy = NOOP;
Texture.WHITE = new Texture({
source: new BufferImageSource({
resource: new Uint8Array([255, 255, 255, 255]),
width: 1,
height: 1,
alphaMode: 'premultiply-alpha-on-upload',
label: 'WHITE',
}),
label: 'WHITE',
});
Texture.WHITE.destroy = NOOP;