// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. /*----------------------------------------------------------------------------- | Copyright (c) 2014-2019, PhosphorJS Contributors | | Distributed under the terms of the BSD 3-Clause License. | | The full license is in the file LICENSE, distributed with this software. |----------------------------------------------------------------------------*/ import { IDisposable } from '@lumino/disposable'; /** * A thin caching wrapper around a 2D canvas rendering context. * * #### Notes * This class is mostly a transparent wrapper around a canvas rendering * context which improves performance when writing context state. * * For best performance, avoid reading state from the `gc`. Writes are * cached based on the previously written value. * * Unless otherwise specified, the API and semantics of this class are * identical to the builtin 2D canvas rendering context: * https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D * * The wrapped canvas context should not be manipulated externally * until the wrapping `GraphicsContext` object is disposed. */ export class GraphicsContext implements IDisposable { /** * Create a new graphics context object. * * @param context - The 2D canvas rendering context to wrap. */ constructor(context: CanvasRenderingContext2D) { this._context = context; this._state = Private.State.create(context); } dispose(): void { // Bail if the gc is already disposed. if (this._disposed) { return; } // Mark the gc as disposed. this._disposed = true; // Pop any unrestored saves. while (this._state.next) { this._state = this._state.next; this._context.restore(); } } get isDisposed(): boolean { return this._disposed; } get fillStyle(): string | CanvasGradient | CanvasPattern { return this._context.fillStyle; } set fillStyle(value: string | CanvasGradient | CanvasPattern) { if (this._state.fillStyle !== value) { this._state.fillStyle = value; this._context.fillStyle = value; } } get strokeStyle(): string | CanvasGradient | CanvasPattern { return this._context.strokeStyle; } set strokeStyle(value: string | CanvasGradient | CanvasPattern) { if (this._state.strokeStyle !== value) { this._state.strokeStyle = value; this._context.strokeStyle = value; } } get font(): string { return this._context.font; } set font(value: string) { if (this._state.font !== value) { this._state.font = value; this._context.font = value; } } get textAlign(): CanvasTextAlign { return this._context.textAlign; } set textAlign(value: CanvasTextAlign) { if (this._state.textAlign !== value) { this._state.textAlign = value; this._context.textAlign = value; } } get textBaseline(): CanvasTextBaseline { return this._context.textBaseline; } set textBaseline(value: CanvasTextBaseline) { if (this._state.textBaseline !== value) { this._state.textBaseline = value; this._context.textBaseline = value; } } get lineCap(): CanvasLineCap { return this._context.lineCap; } set lineCap(value: CanvasLineCap) { if (this._state.lineCap !== value) { this._state.lineCap = value; this._context.lineCap = value; } } get lineDashOffset(): number { return this._context.lineDashOffset; } set lineDashOffset(value: number) { if (this._state.lineDashOffset !== value) { this._state.lineDashOffset = value; this._context.lineDashOffset = value; } } get lineJoin(): CanvasLineJoin { return this._context.lineJoin; } set lineJoin(value: CanvasLineJoin) { if (this._state.lineJoin !== value) { this._state.lineJoin = value; this._context.lineJoin = value; } } get lineWidth(): number { return this._context.lineWidth; } set lineWidth(value: number) { if (this._state.lineWidth !== value) { this._state.lineWidth = value; this._context.lineWidth = value; } } get miterLimit(): number { return this._context.miterLimit; } set miterLimit(value: number) { if (this._state.miterLimit !== value) { this._state.miterLimit = value; this._context.miterLimit = value; } } get shadowBlur(): number { return this._context.shadowBlur; } set shadowBlur(value: number) { if (this._state.shadowBlur !== value) { this._state.shadowBlur = value; this._context.shadowBlur = value; } } get shadowColor(): string { return this._context.shadowColor; } set shadowColor(value: string) { if (this._state.shadowColor !== value) { this._state.shadowColor = value; this._context.shadowColor = value; } } get shadowOffsetX(): number { return this._context.shadowOffsetX; } set shadowOffsetX(value: number) { if (this._state.shadowOffsetX !== value) { this._state.shadowOffsetX = value; this._context.shadowOffsetX = value; } } get shadowOffsetY(): number { return this._context.shadowOffsetY; } set shadowOffsetY(value: number) { if (this._state.shadowOffsetY !== value) { this._state.shadowOffsetY = value; this._context.shadowOffsetY = value; } } get imageSmoothingEnabled(): boolean { return this._context.imageSmoothingEnabled; } set imageSmoothingEnabled(value: boolean) { if (this._state.imageSmoothingEnabled !== value) { this._state.imageSmoothingEnabled = value; this._context.imageSmoothingEnabled = value; } } get globalAlpha(): number { return this._context.globalAlpha; } set globalAlpha(value: number) { if (this._state.globalAlpha !== value) { this._state.globalAlpha = value; this._context.globalAlpha = value; } } get globalCompositeOperation(): GlobalCompositeOperation { return this._context.globalCompositeOperation; } set globalCompositeOperation(value: GlobalCompositeOperation) { if (this._state.globalCompositeOperation !== value) { this._state.globalCompositeOperation = value; this._context.globalCompositeOperation = value; } } getLineDash(): number[] { return this._context.getLineDash(); } setLineDash(segments: number[]): void { this._context.setLineDash(segments); } rotate(angle: number): void { this._context.rotate(angle); } scale(x: number, y: number): void { this._context.scale(x, y); } transform( m11: number, m12: number, m21: number, m22: number, dx: number, dy: number ): void { this._context.transform(m11, m12, m21, m22, dx, dy); } translate(x: number, y: number): void { this._context.translate(x, y); } setTransform( m11: number, m12: number, m21: number, m22: number, dx: number, dy: number ): void { this._context.setTransform(m11, m12, m21, m22, dx, dy); } save(): void { // Clone an push the current state to the stack. this._state = Private.State.push(this._state); // Save the wrapped context state. this._context.save(); } restore(): void { // Bail if there is no state to restore. if (!this._state.next) { return; } // Pop the saved state from the stack. this._state = Private.State.pop(this._state); // Restore the wrapped context state. this._context.restore(); } beginPath(): void { return this._context.beginPath(); } closePath(): void { this._context.closePath(); } isPointInPath(x: number, y: number, fillRule?: CanvasFillRule): boolean { let result: boolean; if (arguments.length === 2) { result = this._context.isPointInPath(x, y); } else { result = this._context.isPointInPath(x, y, fillRule); } return result; } arc( x: number, y: number, radius: number, startAngle: number, endAngle: number, anticlockwise?: boolean ): void { if (arguments.length === 5) { this._context.arc(x, y, radius, startAngle, endAngle); } else { this._context.arc(x, y, radius, startAngle, endAngle, anticlockwise); } } arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void { this._context.arcTo(x1, y1, x2, y2, radius); } bezierCurveTo( cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number ): void { this._context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); } ellipse( x: number, y: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number, anticlockwise?: boolean ): void { if (arguments.length === 7) { this._context.ellipse( x, y, radiusX, radiusY, rotation, startAngle, endAngle ); } else { this._context.ellipse( x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise ); } } lineTo(x: number, y: number): void { this._context.lineTo(x, y); } moveTo(x: number, y: number): void { this._context.moveTo(x, y); } quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void { this._context.quadraticCurveTo(cpx, cpy, x, y); } rect(x: number, y: number, w: number, h: number): void { this._context.rect(x, y, w, h); } clip(fillRule?: CanvasFillRule): void { if (arguments.length === 0) { this._context.clip(); } else { this._context.clip(fillRule); } } fill(fillRule?: CanvasFillRule): void { if (arguments.length === 0) { this._context.fill(); } else { this._context.fill(fillRule); } } stroke(): void { this._context.stroke(); } clearRect(x: number, y: number, w: number, h: number): void { return this._context.clearRect(x, y, w, h); } fillRect(x: number, y: number, w: number, h: number): void { this._context.fillRect(x, y, w, h); } fillText(text: string, x: number, y: number, maxWidth?: number): void { if (arguments.length === 3) { this._context.fillText(text, x, y); } else { this._context.fillText(text, x, y, maxWidth); } } strokeRect(x: number, y: number, w: number, h: number): void { this._context.strokeRect(x, y, w, h); } strokeText(text: string, x: number, y: number, maxWidth?: number): void { if (arguments.length === 3) { this._context.strokeText(text, x, y); } else { this._context.strokeText(text, x, y, maxWidth); } } measureText(text: string): TextMetrics { return this._context.measureText(text); } createLinearGradient( x0: number, y0: number, x1: number, y1: number ): CanvasGradient { return this._context.createLinearGradient(x0, y0, x1, y1); } createRadialGradient( x0: number, y0: number, r0: number, x1: number, y1: number, r1: number ): CanvasGradient { return this._context.createRadialGradient(x0, y0, r0, x1, y1, r1); } createPattern( image: HTMLImageElement | HTMLCanvasElement | HTMLVideoElement, repetition: string ): CanvasPattern | null { return this._context.createPattern(image, repetition); } createImageData(imageData: ImageData): ImageData; createImageData(sw: number, sh: number): ImageData; createImageData(): ImageData { // eslint-disable-next-line prefer-spread, prefer-rest-params return this._context.createImageData.apply(this._context, arguments); } getImageData(sx: number, sy: number, sw: number, sh: number): ImageData { return this._context.getImageData(sx, sy, sw, sh); } putImageData(imagedata: ImageData, dx: number, dy: number): void; putImageData( imagedata: ImageData, dx: number, dy: number, dirtyX: number, dirtyY: number, dirtyWidth: number, dirtyHeight: number ): void; putImageData(): void { // eslint-disable-next-line prefer-spread, prefer-rest-params this._context.putImageData.apply(this._context, arguments); } drawImage( image: | HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | ImageBitmap, dstX: number, dstY: number ): void; drawImage( image: | HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | ImageBitmap, dstX: number, dstY: number, dstW: number, dstH: number ): void; drawImage( image: | HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | ImageBitmap, srcX: number, srcY: number, srcW: number, srcH: number, dstX: number, dstY: number, dstW: number, dstH: number ): void; drawImage(): void { // eslint-disable-next-line prefer-spread, prefer-rest-params this._context.drawImage.apply(this._context, arguments); } drawFocusIfNeeded(element: Element): void { this._context.drawFocusIfNeeded(element); } private _disposed = false; private _state: Private.State; private _context: CanvasRenderingContext2D; } /** * The namespace for the module implementation details. */ namespace Private { /** * The index of next valid pool object. */ let pi = -1; /** * A state object allocation pool. */ const pool: State[] = []; /** * An object which holds the state for a gc. */ export class State { /** * Create a gc state object from a 2D canvas context. */ static create(context: CanvasRenderingContext2D): State { let state = pi < 0 ? new State() : pool[pi--]; state.next = null; state.fillStyle = context.fillStyle; state.font = context.font; state.globalAlpha = context.globalAlpha; state.globalCompositeOperation = context.globalCompositeOperation; state.imageSmoothingEnabled = context.imageSmoothingEnabled; state.lineCap = context.lineCap; state.lineDashOffset = context.lineDashOffset; state.lineJoin = context.lineJoin; state.lineWidth = context.lineWidth; state.miterLimit = context.miterLimit; state.shadowBlur = context.shadowBlur; state.shadowColor = context.shadowColor; state.shadowOffsetX = context.shadowOffsetX; state.shadowOffsetY = context.shadowOffsetY; state.strokeStyle = context.strokeStyle; state.textAlign = context.textAlign; state.textBaseline = context.textBaseline; return state; } /** * Clone an existing gc state object and add it to the state stack. */ static push(other: State): State { let state = pi < 0 ? new State() : pool[pi--]; state.next = other; state.fillStyle = other.fillStyle; state.font = other.font; state.globalAlpha = other.globalAlpha; state.globalCompositeOperation = other.globalCompositeOperation; state.imageSmoothingEnabled = other.imageSmoothingEnabled; state.lineCap = other.lineCap; state.lineDashOffset = other.lineDashOffset; state.lineJoin = other.lineJoin; state.lineWidth = other.lineWidth; state.miterLimit = other.miterLimit; state.shadowBlur = other.shadowBlur; state.shadowColor = other.shadowColor; state.shadowOffsetX = other.shadowOffsetX; state.shadowOffsetY = other.shadowOffsetY; state.strokeStyle = other.strokeStyle; state.textAlign = other.textAlign; state.textBaseline = other.textBaseline; return state; } /** * Pop the next state object and return the current to the pool */ static pop(state: State): State { state.fillStyle = ''; state.strokeStyle = ''; pool[++pi] = state; return state.next!; } next: State | null; fillStyle: string | CanvasGradient | CanvasPattern; font: string; globalAlpha: number; globalCompositeOperation: string; imageSmoothingEnabled: boolean; lineCap: string; lineDashOffset: number; lineJoin: string; lineWidth: number; miterLimit: number; shadowBlur: number; shadowColor: string; shadowOffsetX: number; shadowOffsetY: number; strokeStyle: string | CanvasGradient | CanvasPattern; textAlign: string; textBaseline: string; } }