happy-dom
Version:
Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. It includes many web standards from WHATWG DOM and HTML.
305 lines (275 loc) • 9.17 kB
text/typescript
import HTMLElement from '../html-element/HTMLElement.js';
import * as PropertySymbol from '../../PropertySymbol.js';
import Blob from '../../file/Blob.js';
import OffscreenCanvas from '../../canvas/OffscreenCanvas.js';
import type Event from '../../event/Event.js';
import type MediaStream from '../html-media-element/MediaStream.js';
import ElementEventAttributeUtility from '../element/ElementEventAttributeUtility.js';
import type ICanvasAdapter from '../../canvas/ICanvasAdapter.js';
import WindowBrowserContext from '../../window/WindowBrowserContext.js';
import type ICanvas from '../../canvas/ICanvasShape.js';
import type ICanvasRenderingContext2D from '../../canvas/ICanvasRenderingContext2D.js';
import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js';
const DEVICE_ID = 'S3F/aBCdEfGHIjKlMnOpQRStUvWxYz1234567890+1AbC2DEf2GHi3jK34le+ab12C3+1aBCdEf==';
enum CanvasModeEnum {
none = 'none',
context = 'context',
offscreen = 'offscreen'
}
/**
* HTMLCanvasElement
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement
*/
export default class HTMLCanvasElement extends HTMLElement implements ICanvas {
// Private properties
#mode: CanvasModeEnum = CanvasModeEnum.none;
#offscreenCanvas: OffscreenCanvas | null = null;
// Events
/* eslint-disable jsdoc/require-jsdoc */
public get oncontextlost(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'oncontextlost');
}
public set oncontextlost(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('oncontextlost', value);
}
public get oncontextrestored(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'oncontextrestored');
}
public set oncontextrestored(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('oncontextrestored', value);
}
public get onwebglcontextcreationerror(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onwebglcontextcreationerror');
}
public set onwebglcontextcreationerror(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onwebglcontextcreationerror', value);
}
public get onwebglcontextlost(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onwebglcontextlost');
}
public set onwebglcontextlost(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onwebglcontextlost', value);
}
public get onwebglcontextrestored(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onwebglcontextrestored');
}
public set onwebglcontextrestored(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onwebglcontextrestored', value);
}
/* eslint-enable jsdoc/require-jsdoc */
/**
* Returns width.
*
* @returns Width.
*/
public get width(): number {
const width = this.getAttribute('width');
if (width === null) {
return 300;
}
const parsed = parseInt(width, 10);
return isNaN(parsed) ? 300 : parsed;
}
/**
* Sets width.
*
* @param width Width.
*/
public set width(width: number) {
this.setAttribute('width', String(width));
}
/**
* Returns height.
*
* @returns Height.
*/
public get height(): number {
const height = this.getAttribute('height');
if (height === null) {
return 150;
}
const parsed = parseInt(height, 10);
return isNaN(parsed) ? 150 : parsed;
}
/**
* Sets height.
*
* @param height Height.
*/
public set height(height: number) {
this.setAttribute('height', String(height));
}
/**
* Returns capture stream.
*
* @param [frameRate] Frame rate.
* @returns Capture stream.
*/
public captureStream(frameRate?: number): MediaStream {
const stream = new this[PropertySymbol.window].MediaStream();
const track = new this[PropertySymbol.window].CanvasCaptureMediaStreamTrack(
PropertySymbol.illegalConstructor,
this
);
track[PropertySymbol.kind] = 'video';
track[PropertySymbol.capabilities].deviceId = DEVICE_ID;
track[PropertySymbol.capabilities].aspectRatio.max = this.width;
track[PropertySymbol.capabilities].height.max = this.height;
track[PropertySymbol.capabilities].width.max = this.width;
track[PropertySymbol.settings].deviceId = DEVICE_ID;
if (frameRate !== undefined) {
track[PropertySymbol.capabilities].frameRate.max = frameRate;
track[PropertySymbol.settings].frameRate = frameRate;
}
stream.addTrack(track);
return stream;
}
/**
* Returns context.
*
* @param contextType Context type.
* @param [contextAttributes] Context attributes.
* @returns Context.
*/
public getContext(
contextType: '2d' | 'webgl' | 'webgl2' | 'webgpu' | 'bitmaprenderer',
contextAttributes?: { [key: string]: any }
): ICanvasRenderingContext2D | null {
if (this.#mode === CanvasModeEnum.offscreen) {
throw new this[PropertySymbol.window].DOMException(
`Failed to execute 'getContext' on 'HTMLCanvasElement': Cannot get context from a canvas that has transferred its control to offscreen.`,
DOMExceptionNameEnum.invalidStateError
);
}
this.#mode = CanvasModeEnum.context;
const browserFrame = new WindowBrowserContext(this[PropertySymbol.window]).getBrowserFrame();
if (!browserFrame) {
throw new this[PropertySymbol.window].Error(
`Failed to execute 'getContext' on 'HTMLCanvasElement': Browser frame is not available. This happens when the browser is closing.`
);
}
const settings = new WindowBrowserContext(this[PropertySymbol.window]).getSettings();
const adapter = settings?.canvasAdapter;
if (!adapter) {
return null;
}
return adapter.getContext(
{
browserFrame,
window: this[PropertySymbol.window],
canvas: this
},
contextType,
contextAttributes
);
}
/**
* Returns to data URL.
*
* @param [type] Type.
* @param [quality] Quality.
* @returns Data URL.
*/
public toDataURL(type?: string, quality?: number): string {
const browserFrame = new WindowBrowserContext(this[PropertySymbol.window]).getBrowserFrame();
if (!browserFrame) {
throw new this[PropertySymbol.window].Error(
`Failed to execute 'toDataURL' on 'HTMLCanvasElement': Browser frame is not available. This happens when the browser is closing.`
);
}
const settings = new WindowBrowserContext(this[PropertySymbol.window]).getSettings();
const adapter = settings?.canvasAdapter;
if (!adapter) {
return `data:${type || 'image/png'};base64,${Buffer.from([]).toString('base64')}`;
}
if (this.#offscreenCanvas) {
return (<ICanvasAdapter>adapter).toDataURL(
{
browserFrame,
window: this[PropertySymbol.window],
canvas: this.#offscreenCanvas
},
type,
quality
);
}
return (<ICanvasAdapter>adapter).toDataURL(
{
browserFrame,
window: this[PropertySymbol.window],
canvas: this
},
type,
quality
);
}
/**
* Returns to blob.
*
* @param callback Callback.
* @param [type] Type.
* @param [quality] Quality.
*/
public toBlob(callback: (blob: Blob | null) => void, type?: string, quality?: any): void {
const browserFrame = new WindowBrowserContext(this[PropertySymbol.window]).getBrowserFrame();
if (!browserFrame) {
throw new this[PropertySymbol.window].Error(
`Failed to execute 'toBlob' on 'HTMLCanvasElement': Browser frame is not available. This happens when the browser is closing.`
);
}
const settings = new WindowBrowserContext(this[PropertySymbol.window]).getSettings();
const adapter = settings?.canvasAdapter;
if (!adapter) {
this[PropertySymbol.window].requestAnimationFrame(() =>
callback(new Blob([], { type: type || 'image/png' }))
);
return;
}
if (this.#offscreenCanvas) {
(<ICanvasAdapter>adapter).toBlob(
{
browserFrame,
window: this[PropertySymbol.window],
canvas: this.#offscreenCanvas
},
callback,
type,
quality
);
return;
}
(<ICanvasAdapter>adapter).toBlob(
{
browserFrame,
window: this[PropertySymbol.window],
canvas: this
},
callback,
type,
quality
);
}
/**
* Transfers control to offscreen.
*
* @returns Offscreen canvas.
*/
public transferControlToOffscreen(): OffscreenCanvas {
if (this.#mode === CanvasModeEnum.context) {
throw new this[PropertySymbol.window].DOMException(
`Failed to execute 'transferControlToOffscreen' on 'HTMLCanvasElement': Cannot transfer control from a canvas that has a rendering context.`,
DOMExceptionNameEnum.invalidStateError
);
}
if (this.#mode === CanvasModeEnum.offscreen) {
throw new this[PropertySymbol.window].DOMException(
`Failed to execute 'transferControlToOffscreen' on 'HTMLCanvasElement': Cannot transfer control from a canvas for more than one time.`,
DOMExceptionNameEnum.invalidStateError
);
}
this.#mode = CanvasModeEnum.offscreen;
this.#offscreenCanvas = new OffscreenCanvas(this.width, this.height);
return this.#offscreenCanvas;
}
}