@awayjs/stage
Version:
Stage for AwayJS
1,574 lines (1,305 loc) • 52.7 kB
text/typescript
import { ColorTransform, Matrix, Rectangle, Point, ColorUtils, IAssetAdapter, AssetEvent, IAsset } from '@awayjs/core';
import { UnloadManager, IUnloadable, UnloadService } from './../managers/UnloadManager';
import { Image2D } from './Image2D';
import { BitmapImageChannel } from './BitmapImageChannel';
import { LehmerRng } from '../utils/LehmerRng';
import { Turbulence } from '../utils/Turbulence';
import { Settings } from './../Settings';
/**
* The BitmapImage2D export class lets you work with the data(pixels) of a Bitmap
* object. You can use the methods of the BitmapImage2D export class to create
* arbitrarily sized transparent or opaque bitmap images and manipulate them
* in various ways at runtime. You can also access the BitmapImage2D for a bitmap
* image that you load with the <code>flash.Assets</code> or
* <code>flash.display.Loader</code> classes.
*
* <p>This export class lets you separate bitmap rendering operations from the
* internal display updating routines of flash. By manipulating a
* BitmapImage2D object directly, you can create complex images without incurring
* the per-frame overhead of constantly redrawing the content from vector
* data.</p>
*
* <p>The methods of the BitmapImage2D export class support effects that are not
* available through the filters available to non-bitmap display objects.</p>
*
* <p>A BitmapImage2D object contains an array of pixel data. This data can
* represent either a fully opaque bitmap or a transparent bitmap that
* contains alpha channel data. Either type of BitmapImage2D object is stored as
* a buffer of 32-bit integers. Each 32-bit integer determines the properties
* of a single pixel in the bitmap.</p>
*
* <p>Each 32-bit integer is a combination of four 8-bit channel values(from
* 0 to 255) that describe the alpha transparency and the red, green, and blue
* (ARGB) values of the pixel.(For ARGB values, the most significant byte
* represents the alpha channel value, followed by red, green, and blue.)</p>
*
* <p>The four channels(alpha, red, green, and blue) are represented as
* numbers when you use them with the <code>BitmapImage2D.copyChannel()</code>
* method or the <code>DisplacementMapFilter.componentX</code> and
* <code>DisplacementMapFilter.componentY</code> properties, and these numbers
* are represented by the following constants in the BitmapImage2DChannel
* class:</p>
*
* <ul>
* <li><code>BitmapImage2DChannel.ALPHA</code></li>
* <li><code>BitmapImage2DChannel.RED</code></li>
* <li><code>BitmapImage2DChannel.GREEN</code></li>
* <li><code>BitmapImage2DChannel.BLUE</code></li>
* </ul>
*
* <p>You can attach BitmapImage2D objects to a Bitmap object by using the
* <code>bitmapData</code> property of the Bitmap object.</p>
*
* <p>You can use a BitmapImage2D object to fill a Graphics object by using the
* <code>Graphics.beginBitmapFill()</code> method.</p>
*
* <p>You can also use a BitmapImage2D object to perform batch tile rendering
* using the <code>flash.display.Tilesheet</code> class.</p>
*
* <p>In Flash Player 10, the maximum size for a BitmapImage2D object
* is 8,191 pixels in width or height, and the total number of pixels cannot
* exceed 16,777,215 pixels.(So, if a BitmapImage2D object is 8,191 pixels wide,
* it can only be 2,048 pixels high.) In Flash Player 9 and earlier, the limitation
* is 2,880 pixels in height and 2,880 in width.</p>
*/
let HAS_REF = ('WeakRef' in window);
let alerted = false;
function REF_ENABLED() {
if (alerted) return HAS_REF;
alerted = true;
HAS_REF = HAS_REF && Settings.ENABLE_WEAK_REF;
if (HAS_REF) {
console.debug('[ImageBitmap2D Experemental] Use WeakRef for ImageBitmap2D');
}
return HAS_REF;
}
function fastARGB_to_ABGR(val: number, hasAlpha = true) {
const a = hasAlpha ? (val & 0xff000000) : 0xff000000;
return (a
| ((val & 0xff) << 16)
| (val & 0xff00)
| ((val & 0xff0000) >> 16) & 0xff) >>> 0;
}
interface LazyImageSymbolTag {
needParse: boolean;
lazyParser(): LazyImageSymbolTag;
definition: {
width: number;
height: number;
data: Uint8ClampedArray;
isPMA?: boolean;
}
}
export class BitmapImage2D extends Image2D implements IUnloadable {
public static UNLOAD_EVENT = 'unload';
public static assetType: string = '[image BitmapImage2D]';
public static _unloadManager: UnloadManager<BitmapImage2D>;
public static getManager(stage: Stage): UnloadManager<BitmapImage2D> {
if (!Settings.ENABLE_UNLOAD_BITMAP || !stage) {
return null;
}
if (this._unloadManager) {
return this._unloadManager;
}
const hasFence = (<ContextWebGL> stage.context).hasFence;
this._unloadManager = UnloadService.createManager({
name : 'BitmapImage2D' + (hasFence ? 'async' : ''),
priority: 0,
maxUnloadTasks: (hasFence
? Settings.MAX_BITMAP_UNLOAD_TASKS_ASYNC
: Settings.MAX_BITMAP_UNLOAD_TASKS),
exectionPeriod: 100, // every 100 ms GC will runs to unload bitmap
});
return this._unloadManager;
}
private _asyncRead: Promise<boolean>;
public _lazySymbol: LazyImageSymbolTag;
protected _isSymbolSource: boolean = false;
protected _data: Uint8ClampedArray;
protected _isWeakRef: boolean = false;
protected _finalizer: FinalizationRegistry<any>;
protected _weakRefAdapter: WeakRef<IAssetAdapter>;
protected _transparent: boolean;
protected _unpackPMA: boolean = true;
protected _stage: Stage;
protected _locked: boolean = false;
protected _floodStack: number[] = [];
protected _imageDataDirty: boolean;
protected _initalFillColor: number = null;
protected _lastUsedFill: number = null;
protected syncData(async = false): boolean | Promise<boolean> {
if (async && this._asyncRead) {
return this._asyncRead;
}
if (!async && this._asyncRead) {
throw '[SceneImage2D] Synced read not allowed while async read is requested!';
}
this.applySymbol();
// update data from pixels from GPU
if (!this._imageDataDirty) {
return async ? Promise.resolve(false) : false;
}
const context = <ContextWebGL> this._stage.context;
this._stage.setRenderTarget(this, true, 0, 0, true);
this._stage.context.disableStencil();
// when we call syncData, we already loose other data
// not require apply symbol etc, because it already should be applied
if (!this._data) {
this._data = new Uint8ClampedArray(this.width * this.height * 4);
}
// mark that this internal call, avoid reqursion loop
this._asyncRead = context.drawToBitmapImage2D(this, false, async);
this._stage.setRenderTarget(null);
if (!async) {
this._imageDataDirty = false;
// we store pixel buffer already as PMA.
// we should prevent unpack what already is PMA
this._unpackPMA = false;
return true;
}
return this._asyncRead.then((_status: boolean) => {
this._imageDataDirty = false;
this._unpackPMA = false;
this._asyncRead = null;
return true;
});
}
public getDataInternal(constructEmpty = true, skipSync = false): Uint8ClampedArray {
// if it empty, fill with initlal value
if (this._initalFillColor !== null) {
//use CPU fill to avoid a readback when syncing
this.fillRect(this.rect, this._initalFillColor, !skipSync || !this._stage);
}
if (!skipSync && this._imageDataDirty) {
// sync data already should fill _data
this.syncData(false);
return this._data;
}
this.applySymbol();
if (!this._data && (constructEmpty || this._alphaChannel)) {
this._data = new Uint8ClampedArray(this.width * this.height * 4);
}
if (this._alphaChannel) {
const alpha = this._alphaChannel;
const data = this._data;
for (let i = 0; i < alpha.length; i++) {
// fix JPEG compresiion bug
// it shown when color transform is disabled
if (alpha[i] <= 1) {
data[i * 4 + 0]
= data[i * 4 + 1]
= data[i * 4 + 2] = 0;
}
data[i * 4 + 3] = alpha[i];
}
//remove alpha data once applied
this._alphaChannel = null;
this._transparent = true;
this._unpackPMA = false;
}
return this._data;
}
public needUpload: boolean = false;
/**
* @description Upload flag, marking a image that it was uploaded on GPU
* and can use GPU operations
* fillRect can be implemented on GPU or CPU
*/
public wasUpload: boolean = false;
public invalidateGPU() {
if (this.needUpload)
return;
this.needUpload = true;
this.invalidate();
}
public invalidate() {
if (this._locked)
return;
this.invalidateOwners();
super.invalidate();
}
public isUnloaded = false;
public lastUsedTime = 0;
public get canUnload(): boolean {
return !this._locked && !this._isSymbolSource;
}
public unmarkToUnload() {
BitmapImage2D.getManager(this._stage)?.removeTask(this);
}
public markToUnload() {
if (!BitmapImage2D.getManager(this._stage)) return;
if (this._isSymbolSource) return;
this.lastUsedTime = BitmapImage2D._unloadManager.correctedTime;
// add before, because task can be already exist
// and if we a run GC before - it kill texture
BitmapImage2D._unloadManager.addTask(this);
// run execution when is marked that used
// const count = BitmapImage2D.unloadManager.execute();
// count && console.debug('[BitmapImage2D Experemental] Texture was unloaded from GPU by timer:', count);
}
public unload(): void {
// copy buffer back to _data
// this.syncData();
// dispose texture
this.lastUsedTime = -1;
this.dispatchEvent(new AssetEvent(BitmapImage2D.UNLOAD_EVENT, this));
this.invalidateGPU();
}
private _customMipLevels: BitmapImage2D[];
public addMipLevel(newLevel: BitmapImage2D): void {
if (!this._customMipLevels)
this._customMipLevels = [];
this._customMipLevels.push(newLevel);
}
public get mipLevels(): BitmapImage2D[] {
return this._customMipLevels;
}
/**
*
* @returns {string}
*/
public get assetType(): string {
return BitmapImage2D.assetType;
}
/**
* Defines whether the bitmap image supports per-pixel transparency. You can
* set this value only when you construct a BitmapImage2D object by passing in
* <code>true</code> for the <code>transparent</code> parameter of the
* constructor. Then, after you create a BitmapImage2D object, you can check
* whether it supports per-pixel transparency by determining if the value of
* the <code>transparent</code> property is <code>true</code>.
*/
public get transparent(): boolean {
return this._transparent;
}
public set transparent(value: boolean) {
this._transparent = value;
}
/**
* Store a mode, witch should be uploaded to GPU
* not all case PMA is should be enabled
*/
get unpackPMA() {
return this._unpackPMA && this._transparent;
}
/**
* Creates a BitmapImage2D object with a specified width and height. If you
* specify a value for the <code>fillColor</code> parameter, every pixel in
* the bitmap is set to that color.
*
* <p>By default, the bitmap is created as transparent, unless you pass
* the value <code>false</code> for the transparent parameter. After you
* create an opaque bitmap, you cannot change it to a transparent bitmap.
* Every pixel in an opaque bitmap uses only 24 bits of color channel
* information. If you define the bitmap as transparent, every pixel uses 32
* bits of color channel information, including an alpha transparency
* channel.</p>
*
* @param width The width of the bitmap image in pixels.
* @param height The height of the bitmap image in pixels.
* @param transparent Specifies whether the bitmap image supports per-pixel
* transparency. The default value is <code>true</code>
* (transparent). To create a fully transparent bitmap,
* set the value of the <code>transparent</code>
* parameter to <code>true</code> and the value of the
* <code>fillColor</code> parameter to 0x00000000(or to
* 0). Setting the <code>transparent</code> property to
* <code>false</code> can result in minor improvements
* in rendering performance.
* @param fillColor A 32-bit ARGB color value that you use to fill the
* bitmap image area. The default value is
* 0xFFFFFFFF(solid white).
*/
constructor(
width: number, height: number, transparent: boolean = true,
fillColor: number = null, powerOfTwo: boolean = true, stage: Stage = null) {
super(width, height, powerOfTwo);
if (stage) {
// init
BitmapImage2D.getManager(stage);
}
//this._data = new Uint8ClampedArray(4 * this._rect.width * this._rect.height);
this._transparent = transparent;
this._stage = stage;
this._initalFillColor = fillColor;
this._lastUsedFill = fillColor;
}
public addLazySymbol(tag: LazyImageSymbolTag) {
this._lazySymbol = tag;
this._isSymbolSource = true;
// we should reset initial color, because a bitmap symbol has data of its own
this._initalFillColor = null;
this.invalidateGPU();
}
public applySymbol(): boolean {
if (!this._lazySymbol /*|| !this._lazySymbol.needParse*/) {
return false;
}
if (this._lazySymbol.needParse) {
this._lazySymbol.lazyParser();
}
this._data = this._lazySymbol.definition.data;
// disable UNPACK_PREMULTIPLE_ALPHA because already is PMA
this._unpackPMA = !this._lazySymbol.definition.isPMA;
this._lazySymbol = null;
return true;
}
/**
* @description transfer adapter to weak mode
* Reference will dropped, and adapter destroyed after collecting a adapter
*/
public useWeakRef() {
if (!REF_ENABLED() || this._isWeakRef) {
return;
}
this._isWeakRef = true;
if (!this._finalizer) {
this._finalizer = new FinalizationRegistry(this.onAdapterDropped.bind(this));
}
this.adapter = this._adapter;
}
public unuseWeakRef() {
if (!this._isWeakRef) {
return;
}
this._isWeakRef = false;
this._finalizer.unregister(this);
this._weakRefAdapter = null;
this.adapter = this._adapter;
}
get isWeakRef() {
return this._isWeakRef;
}
set adapter(v: IAssetAdapter) {
if (this._isWeakRef) {
this._finalizer.unregister(this);
if (v) {
this._weakRefAdapter = new WeakRef<IAssetAdapter>(v);
this._finalizer.register(v, this.id, this);
} else {
this._weakRefAdapter = null;
}
// drop hard ref
this._adapter = null;
} else {
this._adapter = v;
}
}
get adapter() {
return (this._weakRefAdapter ? this._weakRefAdapter.deref() : this._adapter) || this;
}
private onAdapterDropped(id: number) {
console.debug('[ImageBitmap2D Experemental] Disposing adaptee, GC runs for:', id);
this.dispose();
}
public copyTo(target: BitmapImage2D): void {
target.setPixels(target.rect, this.data);
target.invalidateGPU();
}
/**
* Returns a new BitmapImage2D object that is a clone of the original instance
* with an exact copy of the contained bitmap.
*
* @return A new BitmapImage2D object that is identical to the original.
*/
public clone(): BitmapImage2D {
const clone = new BitmapImage2D(
this._rect.width,
this._rect.height,
this._transparent,
null,
this._powerOfTwo);
this.copyTo(clone);
return clone;
}
/**
* Adjusts the color values in a specified area of a bitmap image by using a
* <code>ColorTransform</code> object. If the rectangle matches the
* boundaries of the bitmap image, this method transforms the color values of
* the entire image.
*
* @param rect A Rectangle object that defines the area of the
* image in which the ColorTransform object is applied.
* @param colorTransform A ColorTransform object that describes the color
* transformation values to apply.
*/
public colorTransform(rect: Rectangle, colorTransform: ColorTransform): void {
let i: number,
j: number,
index: number;
const data = this.data;
for (i = 0; i < rect.width; ++i) {
for (j = 0; j < rect.height; ++j) {
index = (i + rect.x + (j + rect.y) * this._rect.width) * 4;
data[index] = data[index] * colorTransform.redMultiplier + colorTransform.redOffset;
data[index + 1] = data[index + 1] * colorTransform.greenMultiplier + colorTransform.greenOffset;
data[index + 2] = data[index + 2] * colorTransform.blueMultiplier + colorTransform.blueOffset;
data[index + 3] = data[index + 3] * colorTransform.alphaMultiplier + colorTransform.alphaOffset;
}
}
this.invalidateGPU();
}
/**
* Transfers data from one channel of another BitmapImage2D object or the
* current BitmapImage2D object into a channel of the current BitmapImage2D object.
* All of the data in the other channels in the destination BitmapImage2D object
* are preserved.
*
* <p>The source channel value and destination channel value can be one of
* following values: </p>
*
* <ul>
* <li><code>BitmapImage2DChannel.RED</code></li>
* <li><code>BitmapImage2DChannel.GREEN</code></li>
* <li><code>BitmapImage2DChannel.BLUE</code></li>
* <li><code>BitmapImage2DChannel.ALPHA</code></li>
* </ul>
*
* @param sourceBitmapImage2D The input bitmap image to use. The source image
* can be a different BitmapImage2D object or it can
* refer to the current BitmapImage2D object.
* @param sourceRect The source Rectangle object. To copy only channel
* data from a smaller area within the bitmap,
* specify a source rectangle that is smaller than
* the overall size of the BitmapImage2D object.
* @param destPoint The destination Point object that represents the
* upper-left corner of the rectangular area where
* the new channel data is placed. To copy only
* channel data from one area to a different area in
* the destination image, specify a point other than
* (0,0).
* @param sourceChannel The source channel. Use a value from the
* BitmapImage2DChannel class
* (<code>BitmapImage2DChannel.RED</code>,
* <code>BitmapImage2DChannel.BLUE</code>,
* <code>BitmapImage2DChannel.GREEN</code>,
* <code>BitmapImage2DChannel.ALPHA</code>).
* @param destChannel The destination channel. Use a value from the
* BitmapImage2DChannel class
* (<code>BitmapImage2DChannel.RED</code>,
* <code>BitmapImage2DChannel.BLUE</code>,
* <code>BitmapImage2DChannel.GREEN</code>,
* <code>BitmapImage2DChannel.ALPHA</code>).
* @throws TypeError The sourceBitmapImage2D, sourceRect or destPoint are null.
*/
/* eslint-disable-next-line */
public copyChannel(sourceBitmap: BitmapImage2D, sourceRect: Rectangle, destPoint: Point, sourceChannel: number, destChannel: number): void {
this._lastUsedFill = null;
this.unmarkToUnload();
if (destChannel != 8 || !this._imageDataDirty && !sourceBitmap._imageDataDirty) {
const sourceData: Uint8ClampedArray = sourceBitmap.data;
const destData: Uint8ClampedArray = this.data;
const sourceOffset: number = Math.round(Math.log(sourceChannel) / Math.log(2));
const destOffset: number = Math.round(Math.log(destChannel) / Math.log(2));
const sourceX: number = Math.round(sourceRect.x);
const sourceY: number = Math.round(sourceRect.y);
const destX: number = Math.round(destPoint.x);
const destY: number = Math.round(destPoint.y);
let value: number;
let i: number, j: number, sourceIndex: number, destIndex: number;
for (i = 0; i < sourceRect.width; ++i) {
for (j = 0; j < sourceRect.height; ++j) {
sourceIndex = (i + sourceX + (j + sourceY) * sourceBitmap.width) * 4;
destIndex = (i + destX + (j + destY) * this._rect.width) * 4;
value = (sourceOffset == 3) ? sourceData[sourceIndex + 3] : sourceData[sourceIndex + 3] ? sourceData[sourceIndex + sourceOffset] * 255 / sourceData[sourceIndex + 3] : 0;
if (destOffset == 3) {
destData[destIndex + 0] = destData[sourceIndex + 3] ? destData[destIndex + 0] * value / destData[sourceIndex + 3] : 0;
destData[destIndex + 1] = destData[sourceIndex + 3] ? destData[destIndex + 1] * value / destData[sourceIndex + 3] : 0;
destData[destIndex + 2] = destData[sourceIndex + 3] ? destData[destIndex + 2] * value / destData[sourceIndex + 3] : 0;
destData[destIndex + 3] = value;
} else {
destData[destIndex + destOffset] = value;
}
}
}
this.invalidateGPU();
return;
}
if (this._initalFillColor !== null)
this.fillRect(this._rect, this._initalFillColor);
this._stage.filterManager.copyChannel(
sourceBitmap,
this,
sourceRect,
destPoint,
sourceChannel,
destChannel
);
this._imageDataDirty = true;
this.invalidateOwners();
}
/* eslint-disable-next-line */
public merge(source: BitmapImage2D, sourceRect: Rectangle, destPoint: Point, redMultiplier: number, greenMultiplier: number, blueMultiplier: number, alphaMultiplier: number) {
const dest: Uint8ClampedArray = this.getDataInternal(true);
const src: Uint8ClampedArray = source.data;
redMultiplier = ~~redMultiplier;
greenMultiplier = ~~greenMultiplier;
blueMultiplier = ~~blueMultiplier;
alphaMultiplier = ~~alphaMultiplier;
let i: number, j: number, index: number;
for (i = 0; i < sourceRect.width; ++i) {
for (j = 0; j < sourceRect.height; ++j) {
index = (i + sourceRect.x + (j + sourceRect.y) * this.width) * 4;
/* eslint-disable */
dest[index] = ~~((src[index] * redMultiplier + dest[index] * (0x100 - redMultiplier)) / 0x100);
dest[index + 1] = ~~((src[index + 1] * greenMultiplier + dest[index + 1] * (0x100 - greenMultiplier)) / 0x100);
dest[index + 2] = ~~((src[index + 2] * blueMultiplier + dest[index + 2] * (0x100 - blueMultiplier)) / 0x100);
dest[index + 3] = ~~((src[index + 3] * alphaMultiplier + dest[index + 3] * (0x100 - alphaMultiplier)) / 0x100);
/* eslint-enable */
}
}
this.invalidateGPU();
}
/**
* Frees memory that is used to store the BitmapImage2D object.
*
* <p>When the <code>dispose()</code> method is called on an image, the width
* and height of the image are set to 0. All subsequent calls to methods or
* properties of this BitmapImage2D instance fail, and an exception is thrown.
* </p>
*
* <p><code>BitmapImage2D.dispose()</code> releases the memory occupied by the
* actual bitmap data, immediately(a bitmap can consume up to 64 MB of
* memory). After using <code>BitmapImage2D.dispose()</code>, the BitmapImage2D
* object is no longer usable and an exception may be thrown if
* you call functions on the BitmapImage2D object. However,
* <code>BitmapImage2D.dispose()</code> does not garbage collect the BitmapImage2D
* object(approximately 128 bytes); the memory occupied by the actual
* BitmapImage2D object is released at the time the BitmapImage2D object is
* collected by the garbage collector.</p>
*
*/
public dispose(): void {
BitmapImage2D.getManager(this._stage)?.removeTask(this);
if (this._isWeakRef) {
this._finalizer.unregister(this);
}
this._data = null;
this._rect = null;
this._transparent = null;
this._locked = null;
super.dispose();
}
public getColorBoundsRect(mask: number, color: number, findColor: boolean = true): Rectangle {
const buffer = new Uint32Array(this.getDataInternal(true).buffer);
const size = this.rect;
color = fastARGB_to_ABGR(color, this._transparent);
mask = fastARGB_to_ABGR(mask, this._transparent);
let minX = size.width,
minY = size.height,
maxX = 0,
maxY = 0;
let has = false;
// const start = performance.now();
for (let j = 0; j < size.height; j++) {
for (let i = 0; i < size.width; i++) {
let c = buffer[j * size.width + i];
c = (c & mask) >>> 0;
if ((c === color && findColor) || (c !== color && !findColor)) {
has = true;
minX = i < minX ? i : minX; // Math.min(minX, i);
maxX = i > maxX ? i : maxX; // Math.max(maxX, i);
minY = j < minY ? j : minY; //Math.min(minY, j);
maxY = j > maxY ? j : maxY; // Math.max(maxY, j);
}
}
}
//console.log("getColorBoundsRect not implemented yet in flash/BitmapData");
const d = has
? new Rectangle(minX, minY, maxX - minX + 1, maxY - minY + 1)
: new Rectangle(0, 0, 0, 0);
/*
const delta = performance.now() - start;
console.debug(
'ColoreRect (mask, color, rect, time):',
mask.toString(16),
color.toString(16),
d._rawData,
delta);
*/
return d;
}
public hitTest(
firstPoint: Point,
firstAlphaThreshold: ui8,
secondObject: Point | Rectangle | BitmapImage2D,
secondBitmapDataPoint: Point = null,
secondAlphaThreshold: ui8 = 1
): boolean {
if (secondObject instanceof Point) {
const color = this.getPixel32(
secondObject.x - firstPoint.x,
secondObject.y - firstPoint.y);
const alpha = (color >> 24 & 0xff);
return alpha >= firstAlphaThreshold;
} else if (secondObject instanceof Rectangle) {
secondObject = secondObject.clone();
secondObject.x -= firstPoint.x;
secondObject.y -= firstPoint.y;
if (!this._rect.intersects(secondObject)) {
return false;
}
// slow, not require check a rect from left-right,
// more effective - scan from left and right to center and from to and bottom to center
// BUT for CPU this is good, because data can be cached easy
for (let j = secondObject.y; j < secondObject.bottom; j++) {
for (let i = secondObject.x; i < secondObject.right; i++) {
// slow, to many calls, we can validate outside
const color = this.getPixel32(i, j);
const alpha = (color >> 24 & 0xff);
if (alpha >= firstAlphaThreshold)
return true;
}
}
} else if (secondObject !== null) {
console.warn('[BitmapImage2D] HitTest not implemented for:', secondObject.assetType);
}
return false;
}
// https://lodev.org/cgtutor/floodfill.html
// scanline method implementation
public floodFill(x: number, y: number, color: number): void {
const startX = x, startY = y;
x = x | 0;
y = y | 0;
//const start = performance.now();
// needs update data when it use GL rendering mode
const data = new Uint32Array(this.getDataInternal(true).buffer);
const width = this._rect.width;
const height = this._rect.height;
const stack = this._floodStack;
// avoid reloaction. it costly
stack.length = width * height * 2;
const oldc32 = data[(x + y * width)];
//const rect = [100000,100000,0,0];
let [newA, newR, newG, newB] = ColorUtils.float32ColorToARGB(color);
newA = this._transparent ? newA : 0xff;
// premultiply
newR = newR * newA / 0xff | 0;
newG = newG * newA / 0xff | 0;
newB = newB * newA / 0xff | 0;
const newc32 = ((newA << 24) | (newB << 16) | (newG << 8) | (newR)) >>> 0;
if (newc32 === oldc32) {
// same, return to avoid infinity loop
return;
}
let x1 = 0;
let spanAbove, spanBelow;
let stackIndex = 0;
stack[stackIndex++] = x;
stack[stackIndex++] = y;
while (stackIndex > 0) {
if (stackIndex / 2 > data.length) {
throw `[BitmapImage2D] FloodFill bug, to many interation: ${startX}:${startY}`;
}
y = stack[--stackIndex];
x1 = x = stack[--stackIndex];
while (x1 >= 0 && data[y * width + x1] === oldc32) {
x1--;
}
x1++;
spanAbove = spanBelow = false;
while (x1 < width && data[y * width + x1] === oldc32) {
data[y * width + x1] = newc32;
/*
rect[0] = rect[0] > x1 ? x1 : rect[0];
rect[1] = rect[1] > y ? y : rect[1];
rect[2] = rect[2] < x1 ? x1 : rect[2];
rect[3] = rect[3] < y ? y : rect[3];
*/
if (!spanAbove && y > 0 && data[(y - 1) * width + x1] === oldc32) {
stack[stackIndex++] = x1;
stack[stackIndex++] = y - 1;
spanAbove = true;
} else if (spanAbove && y > 0 && data[(y - 1) * width + x1] !== oldc32) {
spanAbove = false;
}
if (!spanBelow && y < height - 1 && data[(y + 1) * width + x1] === oldc32) {
stack[stackIndex++] = x1;
stack[stackIndex++] = y + 1;
spanBelow = true;
} else if (spanBelow && y < height - 1 && data[(y + 1) * width + x1] !== oldc32) {
spanBelow = false;
}
x1++;
}
}
/*
rect[2] -= rect[0];
rect[3] -= rect[1];
if(rect[2] * rect[3]) {
rect[2] += 1;
rect[3] += 1;
}
const delta = performance.now() - start;
console.debug(
"FloodFill (sourceColor, targetColor, source rect, result, time):",
oldc32.toString(16), newc32.toString(16), this._rect._rawData, rect, delta,)
*/
this.invalidateGPU();
}
/**
* Ruffle port of noise
* @see https://github.com/ruffle-rs/ruffle/blob/d43b033caa98ed201f37558c25f9ce5f2da189d0/core/src/avm1/object/bitmap_data.rs#L326
*/
public noise(
randomSeed: number,
low: ui8 = 0,
high: ui8 = 255,
channelOptions: ui8 = 7,
grayScale: boolean = false
): void {
const rng = new LehmerRng(randomSeed);
const w = this.width;
const h = this.height;
const data = this.getDataInternal(true);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const index = (x + y * w) * 4;
if (grayScale) {
const gray = rng.genRange(low, high);
data[index] = gray | 0;
data[index + 1] = gray | 0;
data[index + 2] = gray | 0;
} else {
const r = (channelOptions & BitmapImageChannel.RED) ? rng.genRange(low, high) : 0;
const g = (channelOptions & BitmapImageChannel.GREEN) ? rng.genRange(low, high) : 0;
const b = (channelOptions & BitmapImageChannel.BLUE) ? rng.genRange(low, high) : 0;
data[index] = r | 0;
data[index + 1] = g | 0;
data[index + 2] = b | 0;
}
data[index + 3] = (channelOptions & BitmapImageChannel.ALPHA) ? rng.genRange(low, high) : 255;
}
}
this.invalidateGPU();
}
/**
* Ruffle port of perlinNoise
* There are not guarantees that it valid =)
* RESULT IS NOT EQUAL A FLASH RESULT, SEED IS WRONG!!
* @see https://github.com/ruffle-rs/ruffle/blob/d43b033caa98ed201f37558c25f9ce5f2da189d0/core/src/avm1/object/bitmap_data.rs#L713
*/
public perlinNoise (
baseX: number,
baseY: number,
numOctaves: number,
randomSeed: number,
stitch: boolean,
fractalNoise: boolean,
channelOptions: ui8 = 7,
grayScale: boolean = false,
offsets: Array<number | Point > = null
): void {
const w = this.width;
const h = this.height;
const data = this.getDataInternal(true);
const turb = Turbulence.fromSeed(randomSeed);
// translate [x, y, x, y] to [[x, y], [x, y] ...]
let octave_offsets: Array<[number, number]> = null;
if (offsets) {
octave_offsets = [];
if (typeof offsets[0] === 'number') {
for (let i = 0; i < offsets.length; i += 2) {
octave_offsets[i / 2] = [<number> offsets[i + 0], <number> offsets[i + 1]];
}
} else if (offsets[0] instanceof Point) {
for (let i = 0; i < offsets.length; i += 2) {
octave_offsets[i / 2] = [(<Point> offsets[i]).x, (<Point> offsets[i]).y];
}
}
}
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const noise = [0,0,0,0];
const index = (x + y * w) * 4;
if (grayScale) {
noise[0] = turb.turbulence(
0,
[x, y],
[1. / baseX, 1 / baseY],
numOctaves,
fractalNoise,
stitch,
[0,0],
[w, h],
octave_offsets
);
noise[1] = noise[2] = noise[0];
noise[3] = 1;
if ((channelOptions & 8) !== 0) {
noise[3] = turb.turbulence(
1,
[x, y],
[1. / baseX, 1 / baseY],
numOctaves,
fractalNoise,
stitch,
[0,0],
[w, h],
octave_offsets
);
}
// end grayscale
} else {
for (let channel = 0; channel < 4; channel++) {
noise[channel] = (channel === 3) ? 1 : -1;
if ((channelOptions & (1 << channel)) === 0) {
continue;
}
noise[channel] = turb.turbulence(
channel,
[x, y],
[1. / baseX, 1 / baseY],
numOctaves,
fractalNoise,
stitch,
[0,0],
[w, h],
octave_offsets
);
}
}
for (let i = 0; i < 4; i++) {
let mapped = 0;
// This is precisely how Adobe Flash converts the -1..1 or 0..1 floats to u8.
// Please don't touch, it was difficult to figure out the exact method. :)
if (fractalNoise) {
// Yes, the + 0.5 for correct (nearest) rounding is done before the division by 2.0,
// making it technically less correct (I think), but this is how it is!
mapped = ((noise[i] * 0xff + 0xff) + 0.5) / 2.0;
} else {
mapped = noise[i] * 0xff + 0.5;
}
data[index + i] = mapped | 0;
if (!this._transparent) {
data[index + 3] = 0xff;
}
}
}
}
this.invalidateGPU();
}
public drawBitmap(
source: Uint8ClampedArray, offsetX: number, offsetY: number,
width: number, height: number, matrix: Matrix = null): void {
BitmapImageUtils.drawBitmap(
source, offsetX, offsetY, width, height, this.data, 0, 0, this._rect.width, this._rect.height, matrix);
this.invalidateGPU();
}
/**
* Fills a rectangular area of pixels with a specified ARGB color.
*
* @param rect The rectangular area to fill.
* @param color The ARGB color value that fills the area. ARGB colors are
* often specified in hexadecimal format; for example,
* 0xFF336699.
* @throws TypeError The rect is null.
*/
public fillRect(rect: Rectangle, color: number, useCPU: boolean = false): void {
//ensure we reset initial color to stop recursive texture writes
this._initalFillColor = null;
if (useCPU) {
if (!this._data) {
try {
this._data = new Uint8ClampedArray(this.width * this.height * 4);
} catch (e) {
console.error(this.width, this.height);
}
}
const
x = ~~rect.x,
y = ~~rect.y,
width = ~~rect.width,
height = ~~rect.height,
data = new Uint32Array(this._data.buffer);
let rgba = 0;
if (this._transparent) {
const [a, r, g, b] = ColorUtils.float32ColorToARGB(color);
// PMA
// we should FLIP bytes because a use UINT32
rgba = ColorUtils.ARGBtoFloat32(
a,
b * a / 0xff | 0,
g * a / 0xff | 0,
r * a / 0xff | 0) >>> 0;
/**
* TW2 has bug with transition over timeline when used a PMA
* I think that caused by invalid blend mode
*/
this._unpackPMA = false;
} else {
rgba = fastARGB_to_ABGR(color & 0xffffff, false);
}
//fast path for complete fill
if (x == 0 && y == 0 && width == this._rect.width && height == this._rect.height) {
data.fill(rgba);
} else {
let j: number;
let index: number;
for (j = 0; j < height; ++j) {
index = x + (j + y) * this._rect.width;
data.fill(rgba, index, index + width);
}
}
this.invalidateGPU();
} else {
const argb = ColorUtils.float32ColorToARGB(color);
const alpha = this._transparent ? argb[0] / 255 : 1;
const isCrop = rect !== this._rect && !this._rect.equals(rect);
this._stage.pushRenderTargetConfig();
this._stage.setRenderTarget(this, true, 0, 0, true);
this._stage.setScissor(rect);
// we shure that color is fully filled when there are not any crops
this._lastUsedFill = isCrop ? null : color;
this._stage.clear(
(argb[1] / 0xff) * alpha,
(argb[2] / 0xff) * alpha,
(argb[3] / 0xff) * alpha,
alpha
);
this._stage.popRenderTarget();
this._stage.setScissor(null);
this._imageDataDirty = true;
this.invalidateOwners();
}
}
/**
* Returns an integer that represents an RGB pixel value from a BitmapImage2D
* object at a specific point(<i>x</i>, <i>y</i>). The
* <code>getPixel()</code> method returns an unmultiplied pixel value. No
* alpha information is returned.
*
* <p>All pixels in a BitmapImage2D object are stored as premultiplied color
* values. A premultiplied image pixel has the red, green, and blue color
* channel values already multiplied by the alpha data. For example, if the
* alpha value is 0, the values for the RGB channels are also 0, independent
* of their unmultiplied values. This loss of data can cause some problems
* when you perform operations. All BitmapImage2D methods take and return
* unmultiplied values. The internal pixel representation is converted from
* premultiplied to unmultiplied before it is returned as a value. During a
* set operation, the pixel value is premultiplied before the raw image pixel
* is set.</p>
*
* @param x The <i>x</i> position of the pixel.
* @param y The <i>y</i> position of the pixel.
* @return A number that represents an RGB pixel value. If the(<i>x</i>,
* <i>y</i>) coordinates are outside the bounds of the image, the
* method returns 0.
*/
public getPixel(x, y): number {
if (!this._rect.contains(x, y))
return 0x0;
const
index = (~~x + ~~y * this._rect.width) * 4,
data = this._data;
const
r = data[index + 0],
g = data[index + 1],
b = data[index + 2],
a = data[index + 3];
//returns black if fully transparent
if (!a)
return 0x0;
return (r * 0xFF / a << 16) | (g * 0xFF / a << 8) | b * 0xFF / a;
}
/**
* Returns an ARGB color value that contains alpha channel data and RGB data.
* This method is similar to the <code>getPixel()</code> method, which
* returns an RGB color without alpha channel data.
*
* <p>All pixels in a BitmapImage2D object are stored as premultiplied color
* values. A premultiplied image pixel has the red, green, and blue color
* channel values already multiplied by the alpha data. For example, if the
* alpha value is 0, the values for the RGB channels are also 0, independent
* of their unmultiplied values. This loss of data can cause some problems
* when you perform operations. All BitmapImage2D methods take and return
* unmultiplied values. The internal pixel representation is converted from
* premultiplied to unmultiplied before it is returned as a value. During a
* set operation, the pixel value is premultiplied before the raw image pixel
* is set.</p>
*
* @param x The <i>x</i> position of the pixel.
* @param y The <i>y</i> position of the pixel.
* @return A number representing an ARGB pixel value. If the(<i>x</i>,
* <i>y</i>) coordinates are outside the bounds of the image, 0 is
* returned.
*/
public getPixel32(x, y): number {
if (!this._rect.contains(x, y))
return 0x0;
let index: number = (~~x + ~~y * this._rect.width) * 4;
const data: Uint8ClampedArray = this.getDataInternal(true);
const
r = data[index++],
g = data[index++],
b = data[index++],
a = data[index];
if (!a)
return 0x0;
return ((a << 24) | (r * 0xFF / a << 16) | (g * 0xFF / a << 8) | b * 0xFF / a) >>> 0;
}
public getPixels(rect: Rectangle): Uint8ClampedArray {
if (rect.equals(this._rect)) {
return this.getDataInternal(true);
}
const data = this.getDataInternal(true);
const target = new Uint8ClampedArray(rect.width * rect.height * 4);
const x = rect.x | 0;
const y = rect.y | 0;
const width = rect.width | 0;
const height = rect.height | 0;
let index: number;
for (let j = 0; j < height; ++j) {
index = x + (j + y) * this._rect.width;
target.set(data.subarray(index * 4, (index + width) * 4), j * width * 4);
}
return target;
}
public getPixelData(x, y, imagePixel: Uint8ClampedArray): void {
let index: number = (x + y * this._rect.width) * 4;
const data: Uint8ClampedArray = this.getDataInternal(true);
imagePixel[0] = data[index++];
imagePixel[1] = data[index++];
imagePixel[2] = data[index++];
imagePixel[3] = data[index];
}
public setPixelData(x, y, imagePixel: Uint8ClampedArray): void {
const index: number = (x + y * this._rect.width) * 4;
const data: Uint8ClampedArray = this.getDataInternal(true);
data[index + 0] = imagePixel[0];
data[index + 1] = imagePixel[1];
data[index + 2] = imagePixel[2];
data[index + 3] = this._transparent ? imagePixel[3] : 0xFF;
this.invalidateGPU();
}
/**
* Locks an image so that any objects that reference the BitmapImage2D object,
* such as Bitmap objects, are not updated when this BitmapImage2D object
* changes. To improve performance, use this method along with the
* <code>unlock()</code> method before and after numerous calls to the
* <code>setPixel()</code> or <code>setPixel32()</code> method.
*
*/
public lock(): void {
if (this._locked)
return;
this._locked = true;
}
/**
* Converts an Array into a rectangular region of pixel data. For each pixel,
* an Array element is read and written into the BitmapImage2D pixel. The data
* in the Array is expected to be 32-bit ARGB pixel values.
*
* @param rect Specifies the rectangular region of the BitmapImage2D
* object.
* @param inputArray An Array that consists of 32-bit unmultiplied pixel
* values to be used in the rectangular region.
* @throws RangeError The vector array is not large enough to read all the
* pixel data.
*/
public setArray(rect: Rectangle, inputArray: Array<number>): void {
let i: number, j: number, index: number, argb: number[];
const data = this.getDataInternal(true);
for (i = 0; i < rect.width; ++i) {
for (j = 0; j < rect.height; ++j) {
argb = ColorUtils.float32ColorToARGB(inputArray[i + j * rect.width]);
index = (i + rect.x + (j + rect.y) * this._rect.width) * 4;
data[index + 0] = argb[1];
data[index + 1] = argb[2];
data[index + 2] = argb[3];
data[index + 3] = this._transparent ? argb[0] : 0xFF;
}
}
this.invalidateGPU();
}
/**
* Sets a single pixel of a BitmapImage2D object. The current alpha channel
* value of the image pixel is preserved during this operation. The value of
* the RGB color parameter is treated as an unmultiplied color value.
*
* <p><b>Note:</b> To increase performance, when you use the
* <code>setPixel()</code> or <code>setPixel32()</code> method repeatedly,
* call the <code>lock()</code> method before you call the
* <code>setPixel()</code> or <code>setPixel32()</code> method, and then call
* the <code>unlock()</code> method when you have made all pixel changes.
* This process prevents objects that reference this BitmapImage2D instance from
* updating until you finish making the pixel changes.</p>
*
* @param x The <i>x</i> position of the pixel whose value changes.
* @param y The <i>y</i> position of the pixel whose value changes.
* @param color The resulting RGB color for the pixel.
*/
public setPixel(x: number, y: number, color: number): void {
if (!this._rect.contains(x, y))
return;
const
index = (~~x + ~~y * this._rect.width) * 4,
argb = ColorUtils.float32ColorToARGB(color),
data = this.getDataInternal(true);
data[index + 0] = argb[1];
data[index + 1] = argb[2];
data[index + 2] = argb[3];
data[index + 3] = 0xff;
this.invalidateGPU();
}
public setPixelFromArray(x: number, y: number, colors: number[]): void {
const
index: number = (x + y * this._rect.width) * 4,
data: Uint8ClampedArray = this.getDataInternal(true);
data[index + 0] = colors[1] * colors[0] | 0;
data[index + 1] = colors[2] * colors[0] | 0;
data[index + 2] = colors[3] * colors[0] | 0;
data[index + 3] = colors[0] * 0xff | 0;
this._unpackPMA = false;
this.invalidateGPU();
}
/**
* Sets the color and alpha transparency values of a single pixel of a
* BitmapImage2D object. This method is similar to the <code>setPixel()</code>
* method; the main difference is that the <code>setPixel32()</code> method
* takes an ARGB color value that contains alpha channel information.
*
* <p>All pixels in a BitmapImage2D object are stored as premultiplied color
* values. A premultiplied image pixel has the red, green, and blue color
* channel values already multiplied by the alpha data. For example, if the
* alpha value is 0, the values for the RGB channels are also 0, independent
* of their unmultiplied values. This loss of data can cause some problems
* when you perform operations. All BitmapImage2D methods take and return
* unmultiplied values. The internal pixel representation is converted from
* premultiplied to unmultiplied before it is returned as a value. During a
* set operation, the pixel value is premultiplied before the raw image pixel
* is set.</p>
*
* <p><b>Note:</b> To increase performance, when you use the
* <code>setPixel()</code> or <code>setPixel32()</code> method repeatedly,
* call the <code>lock()</code> method before you call the
* <code>setPixel()</code> or <code>setPixel32()</code> method, and then call
* the <code>unlock()</code> method when you have made all pixel changes.
* This process prevents objects that reference this BitmapImage2D instance from
* updating until you finish making the pixel changes.</p>
*
* @param x The <i>x</i> position of the pixel whose value changes.
* @param y The <i>y</i> position of the pixel whose value changes.
* @param color The resulting ARGB color for the pixel. If the bitmap is
* opaque(not transparent), the alpha transparency portion of
* this color value is ignored.
*/
public setPixel32(x: number, y: number, color: number): void {
if (!this._rect.contains(x, y))
return;
if (this._initalFillColor !== null)
this.fillRect(this._rect, this._initalFillColor);
this.fillRect(new Rectangle(x, y, 1, 1), color);
}
/**
* Converts a byte array into a rectangular region of pixel data. For each
* pixel, the <code>ByteArray.readUnsignedInt()</code> method is called and
* the return value is written into the pixel. If the byte array ends before
* the full rectangle is written, the function returns. The data in the byte
* array is expected to be 32-bit ARGB pixel values. No seeking is performed
* on the byte array before or after the pixels are read.
*
* @param rect Specifies the rectangular region of the BitmapImage2D
* object.
* @param input A ByteArray object that consists of 32-bit
* unmultiplied pixel values to be used in the
* rectangular region.
* @throws EOFError The <code>inputByteArray</code> object does not include
* enough data to fill the area of the <code>rect</code>
* rectangle. The method fills as many pixels as possible
* before throwing the exception.
* @throws TypeError The rect or inputByteArray are null.
*/
public setPixels(rect: Rectangle, input: Uint8ClampedArray): void {
const data = this.getDataInternal(true);
//fast path for full imageData
if (rect.equals(this._rect)) {
data.set(input);
this._unpackPMA = false;
} else {
const
imageWidth: number = this._rect.width,
inputWidth: number = rect.width;
for (let i = 0; i < rect.height; ++i)
data.set(
input.subarray(i * inputWidth * 4, (i + 1) * inputWidth * 4),
(rect.x + (i + rect.y) * imageWidth) * 4);
console.warn('[BitmapImage2D] Mixed texture mode - array should be a PMA.', this.id);
}
this.invalidateGPU();
}
public applyFilter (source: BitmapImage2D, sourceRect: Rectangle, destPoint: Point, filter: any): boolean {
return false;
}
/**
* Unlocks an image so that any objects that reference the BitmapImage2D object,
* such as Bitmap objects, are updated when this BitmapImage2D object changes.
* To improve performance, use this method along with the <code>lock()</code>
* method before and after numerous calls to the <code>setPixel()</code> or
* <code>setPixel32()</code> method.
*
* @param changeRect The area of the BitmapImage2D object that has changed. If
* you do not specify a value for this parameter, the
* entire area of the BitmapImage2D object is considered
* changed.
*/
public unlock(): void {
if (!this._locked)
return;
this._locked = false;
this.invalidate();
}
/**
* @inheritdoc
*/
set alphaChannel(buff: Uint8Array) {
if (!buff) {
return;
}
if (buff.length !== this.width * this.height) {
throw (
'error when trying to merge the alpha channel into the image.' +
'the length of the alpha channel should be 1/4 of the length of the imageData');
}
// if assigments after initialiszation, apply it immediate
if (this._data && this.wasUpload) {
const buff = this._alphaChannel;
for (let i = 0; i < buff.length; i++) {
this._data[i * 4 + 3] = buff[i];
}
this._unpackPMA = false;
this.invalidateGPU();
return;
}
this._alphaChannel = buff;
this._unpackPMA = false;
}
/**
*
* @returns {ImageData}
*/
public get data(): Uint8ClampedArray {
return this.getDataInternal(true);
}
/**
*
* @param width
* @param height
* @private
*/
public _setSize(width: number, height: number): void {
const data: Uint8ClampedArray = this.data;
this._data = new Uint8ClampedArray(4 * width * height);
const inputWidth: number = (this._rect.width < width) ? this._rect.width : width;
const inputHeight: number = (this._rect.height < height) ? this._rect.height : height;
for (let i = 0; i < inputHeight; ++i)
this._data.set(data.subarray(i * inputWidth * 4, (i + 1) * inputWidth * 4), i * width * 4);
super._setSize(width, height);
}
private /*internal */ getDebugCanvas(onlyA = false) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.height = this.height;
canvas.width = this.width;
const data = this.getDataInte