UNPKG

@nativescript-community/ui-htmlcanvasapi

Version:

An HTML Canvas API implementation on top of android and iOS native APIs

812 lines 28.6 kB
import { BitmapShader, createRect, createRectF, DashPathEffect, FillType, LinearGradient, Matrix, Paint, PorterDuffMode, PorterDuffXfermode, Rect, Style, TileMode } from '@nativescript-community/ui-canvas'; import { Font } from '@nativescript/core'; import { parseFont } from '@nativescript/core/ui/styling/font'; import { CanvasGradient } from '../CanvasGradient'; import { CanvasPattern } from '../CanvasPattern'; import { DOMMatrix } from '../DOMMatrix'; import { Path2D } from '../Path2D'; import { TextMetrics } from '../TextMetrics'; import { getNativeCompositeOperation, getNativeFillRule, getNativeLineCap, getNativeLineJoin, getNativeTextAlignment, isEmptyValue, radiansToDegrees, SCREEN_SCALE } from '../helpers'; const defaults = { strokeStyle: '#000', fillStyle: '#000', globalAlpha: 1.0, lineWidth: 1.0, lineCap: 'butt', lineJoin: 'miter', miterLimit: 10.0, lineDashOffset: 0.0, shadowOffsetX: 0, shadowOffsetY: 0, shadowBlur: 0, shadowColor: 'transparent', globalCompositeOperation: 'source-over', font: '10px sans-serif', textAlign: 'start', textBaseline: 'alphabetic', direction: 'inherit', imageSmoothingEnabled: true, letterSpacing: '0px', imageSmoothingQuality: 'low', fontKerning: 'auto', fontStretch: 'normal', fontVariantCaps: 'normal', textRendering: 'auto', wordSpacing: '0px', }; class AbstractCanvasRenderingContext2D { constructor() { this._restorableProps = { strokeStyle: defaults.strokeStyle, fillStyle: defaults.fillStyle, globalAlpha: defaults.globalAlpha, letterSpacing: defaults.letterSpacing, lineWidth: defaults.lineWidth, lineCap: defaults.lineCap, lineJoin: defaults.lineJoin, miterLimit: defaults.miterLimit, lineDashOffset: defaults.lineDashOffset, shadowOffsetX: defaults.shadowOffsetX, shadowOffsetY: defaults.shadowOffsetY, shadowBlur: defaults.shadowBlur, shadowColor: defaults.shadowColor, globalCompositeOperation: defaults.globalCompositeOperation, font: defaults.font, textAlign: defaults.textAlign, textBaseline: defaults.textBaseline, direction: defaults.direction, imageSmoothingEnabled: defaults.imageSmoothingEnabled, _domMatrix: new DOMMatrix(), setLineDash: undefined, }; this._savedStates = null; this._blendModeRestoreCount = 0; this._stylePaint = new Paint(); this._stylePaint.setAntiAlias(true); this._clearPaint = new Paint(); this._clearPaint.setAntiAlias(true); this._clearPaint.setXfermode(new PorterDuffXfermode(PorterDuffMode.CLEAR)); this._saveState(); this._applyStyleDefaults(); this.beginPath(); } _saveState() { if (this._savedStates == null) { this._savedStates = []; } this._savedStates.push(this._restorableProps); this._restorableProps = { ...this._restorableProps, _domMatrix: new DOMMatrix(this._domMatrix._getValues()), }; } _applyStyleDefaults() { const entries = Object.entries(defaults); for (const [key, value] of entries) { this[key] = value; } } _getTextBaseLineHeight(y) { const fontMetrics = this._stylePaint.getFontMetrics(); let baselineY; switch (this.textBaseline) { case 'top': case 'hanging': baselineY = y - fontMetrics.ascent - fontMetrics.descent; break; case 'middle': baselineY = y - (fontMetrics.ascent + fontMetrics.descent) / 2; break; case 'bottom': case 'ideographic': baselineY = y - fontMetrics.descent; break; default: baselineY = y; break; } return baselineY; } _createNativeShader(drawStyle) { let shader; if (drawStyle instanceof CanvasGradient) { const { type, params } = drawStyle._getGradientData(); switch (type) { case 'linear': const linearParams = params; shader = new LinearGradient(linearParams.x0, linearParams.y0, linearParams.x1, linearParams.y1, drawStyle._gradientColors, drawStyle._gradientOffsets, TileMode.CLAMP); break; case 'radial': console.warn('Radial gradient is not currently supported'); break; case 'conic': console.warn('Conic gradient is not currently supported'); break; } } else if (drawStyle instanceof CanvasPattern) { const { image, repetition } = drawStyle._getPatternData(); const domMatrix = drawStyle._getTransform(); let tileX; let tileY; switch (repetition) { case 'repeat': tileX = TileMode.REPEAT; tileY = TileMode.REPEAT; break; case 'repeat-x': tileX = TileMode.REPEAT; tileY = TileMode.CLAMP; break; case 'repeat-y': tileX = TileMode.CLAMP; tileY = TileMode.REPEAT; break; case 'no-repeat': tileX = TileMode.CLAMP; tileY = TileMode.CLAMP; break; default: tileX = TileMode.REPEAT; tileY = TileMode.REPEAT; break; } shader = new BitmapShader(image, tileX, tileY); if (domMatrix != null) { const matrix = new Matrix(); matrix.setValues(domMatrix._getValues()); shader.setLocalMatrix(matrix); } } return shader; } _updateShadowLayer() { this._stylePaint.setShadowLayer(this.shadowBlur, this.shadowOffsetX, this.shadowOffsetY, this.shadowColor); } get _domMatrix() { return this._restorableProps._domMatrix; } set _domMatrix(matrix) { this._restorableProps._domMatrix = matrix; } isContextLost() { return this.nativeContext == null; } get nativeContext() { return this.canvas.nativeContext; } getContextAttributes() { console.warn('Method getContextAttributes is not implemented'); return null; } isPointInPath(...args) { console.warn('Method isPointInPath is not implemented'); return false; } isPointInStroke(...args) { console.warn('Method isPointInStroke is not implemented'); return false; } createConicGradient(startAngle, x, y) { console.warn('Method createConicGradient is not implemented'); return null; } createLinearGradient(x0, y0, x1, y1) { return new CanvasGradient({ type: 'linear', params: { x0, y0, x1, y1, }, }); } createRadialGradient(x0, y0, r0, x1, y1, r1) { return new CanvasGradient({ type: 'radial', params: { x0, y0, r0, x1, y1, r1, }, }); } createPattern(image, repetition) { return new CanvasPattern({ image, repetition: repetition || 'repeat', }); } createImageData(...args) { console.warn('Method createImageData is not implemented'); return null; } getImageData(sx, sy, sw, sh, settings) { console.warn('Method getImageData is not implemented'); return null; } putImageData(imageData, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight) { console.warn('Method putImageData is not implemented'); return null; } beginPath() { this._path = new Path2D(); } closePath() { this._path.closePath(); } moveTo(x, y) { this._path.moveTo(x, y); } lineTo(x, y) { this._path.lineTo(x, y); } rect(x, y, width, height) { this._path.rect(x, y, width, height); } roundRect(x, y, width, height, radii) { this._path.roundRect(x, y, width, height, radii); } arc(x, y, radius, startAngle, endAngle, counterclockwise = false) { this._path.arc(x, y, radius, startAngle, endAngle, counterclockwise); } arcTo(x1, y1, x2, y2, radius) { this._path.arcTo(x1, y1, x2, y2, radius); } ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise = false) { this._path.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise); } quadraticCurveTo(cpx, cpy, x, y) { this._path.quadraticCurveTo(cpx, cpy, x, y); } bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) { this._path.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); } clearRect(x, y, width, height) { const context = this.nativeContext; if (context) { context.drawRect(createRect(x, y, width, height), this._clearPaint); } } fillRect(x, y, width, height) { const context = this.nativeContext; if (!context) { return; } this._stylePaint.setStyle(Style.FILL); if (this.fillStyle instanceof CanvasGradient || this.fillStyle instanceof CanvasPattern) { this._stylePaint.setShader(this._createNativeShader(this.fillStyle)); } else { this._stylePaint.setColor(this.fillStyle); } context.drawRect(createRect(x, y, width, height), this._stylePaint); } fillText(text, x, y, maxWidth) { const context = this.nativeContext; if (!context) { return; } text += ''; this._stylePaint.setStyle(Style.FILL); if (this.fillStyle instanceof CanvasGradient || this.fillStyle instanceof CanvasPattern) { this._stylePaint.setShader(this._createNativeShader(this.fillStyle)); } else { this._stylePaint.setColor(this.fillStyle); } context.drawText(text, x, this._getTextBaseLineHeight(y), this._stylePaint); } strokeRect(x, y, width, height) { const context = this.nativeContext; if (!context) { return; } this._stylePaint.setStyle(Style.STROKE); if (this.strokeStyle instanceof CanvasGradient || this.strokeStyle instanceof CanvasPattern) { this._stylePaint.setShader(this._createNativeShader(this.strokeStyle)); } else { this._stylePaint.setColor(this.strokeStyle); } context.drawRect(createRect(x, y, width, height), this._stylePaint); } strokeText(text, x, y, maxWidth) { const context = this.nativeContext; if (!context) { return; } text += ''; this._stylePaint.setStyle(Style.STROKE); if (this.strokeStyle instanceof CanvasGradient || this.strokeStyle instanceof CanvasPattern) { this._stylePaint.setShader(this._createNativeShader(this.strokeStyle)); } else { this._stylePaint.setColor(this.strokeStyle); } context.drawText(text, x, this._getTextBaseLineHeight(y), this._stylePaint); } fill(...args) { const context = this.nativeContext; if (!context) { return; } this._stylePaint.setStyle(Style.FILL); if (this.fillStyle instanceof CanvasGradient || this.fillStyle instanceof CanvasPattern) { this._stylePaint.setShader(this._createNativeShader(this.fillStyle)); } else { this._stylePaint.setColor(this.fillStyle); } if (args.length == 0) { context.drawPath(this._path.native, this._stylePaint); } else if (args.length == 1) { const arg = args[0]; if (arg instanceof Path2D) { context.drawPath(arg.native, this._stylePaint); } else { this._path.native.setFillType(getNativeFillRule(arg)); context.drawPath(this._path.native, this._stylePaint); this._path.native.setFillType(FillType.WINDING); } } else { const [path, fillRule] = args; path.native.setFillType(getNativeFillRule(fillRule)); context.drawPath(path.native, this._stylePaint); path.native.setFillType(FillType.WINDING); } } clip(...args) { const context = this.nativeContext; if (!context) { return; } if (args.length == 0) { context.clipPath(this._path.native); } else if (args.length == 1) { const arg = args[0]; if (arg instanceof Path2D) { context.clipPath(arg.native); } else { this._path.native.setFillType(getNativeFillRule(arg)); context.clipPath(this._path.native); this._path.native.setFillType(FillType.WINDING); } } else { const [path, fillRule] = args; path.native.setFillType(getNativeFillRule(fillRule)); context.clipPath(path.native); path.native.setFillType(FillType.WINDING); } } drawImage(...args) { const context = this.nativeContext; if (!context) { return; } const imageSource = args[0]; if (!this.imageSmoothingEnabled) { this._stylePaint.setAntiAlias(false); } if (args.length == 3) { context.drawBitmap(imageSource, args[1], args[2], this._stylePaint); } else if (args.length == 5) { const dst = createRectF(args[1], args[2], args[3], args[4]); context.drawBitmap(imageSource, null, dst, this._stylePaint); } else { const src = createRect(args[1], args[2], args[3], args[4]); const dst = createRectF(args[5], args[6], args[7], args[8]); context.drawBitmap(imageSource, src, dst, this._stylePaint); } // Set antialias back to default if (!this.imageSmoothingEnabled) { this._stylePaint.setAntiAlias(true); } } measureText(text) { text += ''; const width = this._stylePaint.measureText(text); const nativeFontMetrics = this._stylePaint.getFontMetrics(); const nativeTextBounds = new Rect(0, 0, 0, 0); // Populate text bounds this._stylePaint.getTextBounds(text, 0, text.length, nativeTextBounds); const textBoundsLeft = nativeTextBounds.left * -1; const textBoundsTop = nativeTextBounds.top * -1; const actualBoundingBoxLeft = textBoundsLeft; const actualBoundingBoxRight = nativeTextBounds.right; let actualBoundingBoxAscent; let actualBoundingBoxDescent; let fontBoundingBoxAscent; let fontBoundingBoxDescent; // TODO: Calculate baseline-accurate values based on original TextMetrics formula switch (this.textBaseline) { case 'top': case 'hanging': actualBoundingBoxAscent = nativeTextBounds.bottom; actualBoundingBoxDescent = textBoundsTop; fontBoundingBoxAscent = nativeFontMetrics.bottom; fontBoundingBoxDescent = nativeFontMetrics.top * -1; break; case 'middle': const actualBoundingBoxMiddle = (nativeTextBounds.top * -1 - nativeTextBounds.bottom) / 2; const fontBoundingBoxMiddle = (nativeFontMetrics.top * -1 - nativeFontMetrics.bottom) / 2; actualBoundingBoxAscent = actualBoundingBoxMiddle; actualBoundingBoxDescent = actualBoundingBoxMiddle; fontBoundingBoxAscent = fontBoundingBoxMiddle; fontBoundingBoxDescent = fontBoundingBoxMiddle; break; case 'bottom': case 'ideographic': actualBoundingBoxAscent = textBoundsTop; actualBoundingBoxDescent = nativeTextBounds.bottom; fontBoundingBoxAscent = nativeFontMetrics.top * -1; fontBoundingBoxDescent = nativeFontMetrics.bottom; break; default: actualBoundingBoxAscent = textBoundsTop; actualBoundingBoxDescent = nativeTextBounds.bottom; fontBoundingBoxAscent = nativeFontMetrics.ascent * -1; fontBoundingBoxDescent = nativeFontMetrics.descent; break; } return Object.create(TextMetrics.prototype, { width: { value: width, }, actualBoundingBoxLeft: { value: actualBoundingBoxLeft, }, actualBoundingBoxRight: { value: actualBoundingBoxRight, }, actualBoundingBoxAscent: { value: actualBoundingBoxAscent, }, actualBoundingBoxDescent: { value: actualBoundingBoxDescent, }, fontBoundingBoxAscent: { value: fontBoundingBoxAscent, }, fontBoundingBoxDescent: { value: fontBoundingBoxDescent, }, emHeightAscent: { value: 0, }, emHeightDescent: { value: 0, }, hangingBaseline: { value: 0, }, alphabeticBaseline: { value: 0, }, ideographicBaseline: { value: 0, }, }); } scale(x, y) { const context = this.nativeContext; if (context) { context.scale(x, y); } } translate(x, y) { const context = this.nativeContext; if (context) { context.translate(x, y); } } rotate(angle) { const context = this.nativeContext; if (context) { context.rotate(radiansToDegrees(angle)); } } stroke(path) { const context = this.nativeContext; if (!context) { return; } this._stylePaint.setStyle(Style.STROKE); if (this.strokeStyle instanceof CanvasGradient || this.strokeStyle instanceof CanvasPattern) { this._stylePaint.setShader(this._createNativeShader(this.strokeStyle)); } else { this._stylePaint.setColor(this.strokeStyle); } if (path != null) { context.drawPath(path.native, this._stylePaint); } else { context.drawPath(this._path.native, this._stylePaint); } } save() { const context = this.nativeContext; if (context) { context.save(); this._saveState(); } } restore() { const context = this.nativeContext; if (!context) { return; } try { context.restore(); if (this._savedStates?.length) { // Some values are not savable so keep them even after restore this._stylePaint = new Paint(this._stylePaint); this._restorableProps = this._savedStates.pop(); const entries = Object.entries(this._restorableProps); for (const [key, value] of entries) { if (typeof this[key] === 'function') { this[key](value); } else { this[key] = value; } } if (this._savedStates.length === 0) { this._savedStates = null; } } } catch (err) { } } reset() { const context = this.nativeContext; if (!context) { return; } this._path.native.reset(); this._stylePaint = new Paint(); this._applyStyleDefaults(); this.clearRect(0, 0, context.getWidth(), context.getHeight()); } transform(a, b, c, d, e, f) { const context = this.nativeContext; if (!context) { return; } const matrix = new Matrix(); const values = this._domMatrix._getValues(); values[0] = a; values[1] = c; values[2] = e; values[3] = b; values[4] = d; values[5] = f; values[6] = 0; values[7] = 0; values[8] = 1; matrix.setValues(values); context.concat(matrix); } resetTransform() { const context = this.nativeContext; if (!context) { return; } context.setMatrix(new Matrix()); this._domMatrix._reset(); if (this.canvas._isPixelScaleNeeded()) { context.scale(SCREEN_SCALE, SCREEN_SCALE); } } setTransform(...args) { const context = this.nativeContext; if (!context) { return; } const matrix = new Matrix(); const values = this._domMatrix._getValues(); let a, b, c, d, e, f; if (args.length === 1 && args[0] instanceof DOMMatrix) { a = args[0].a; b = args[0].b; c = args[0].c; d = args[0].d; e = args[0].e; f = args[0].f; } else { a = args[0]; b = args[1]; c = args[2]; d = args[3]; e = args[4]; f = args[5]; } values[0] = a; values[1] = c; values[2] = e; values[3] = b; values[4] = d; values[5] = f; values[6] = 0; values[7] = 0; values[8] = 1; matrix.setValues(values); context.setMatrix(matrix); if (this.canvas._isPixelScaleNeeded()) { context.scale(SCREEN_SCALE, SCREEN_SCALE); } } getTransform() { return this._domMatrix; } getLineDash() { return this._restorableProps.setLineDash; } setLineDash(segments) { this._restorableProps.setLineDash = Array.isArray(segments) ? segments : []; this._stylePaint.setPathEffect(this.getLineDash().length ? new DashPathEffect(this.getLineDash(), this.lineDashOffset) : null); } get strokeStyle() { return this._restorableProps.strokeStyle; } set strokeStyle(val) { this._restorableProps.strokeStyle = isEmptyValue(val) ? defaults.strokeStyle : val; } get fillStyle() { return this._restorableProps.fillStyle; } set fillStyle(val) { this._restorableProps.fillStyle = isEmptyValue(val) ? defaults.fillStyle : val; } get globalAlpha() { return this._restorableProps.globalAlpha; } set globalAlpha(val) { this._restorableProps.globalAlpha = typeof val === 'number' ? val : defaults.globalAlpha; this._stylePaint.setAlpha(this.globalAlpha * 255); } get lineWidth() { return this._restorableProps.lineWidth; } set lineWidth(val) { this._restorableProps.lineWidth = typeof val === 'number' ? val : defaults.lineWidth; this._stylePaint.setStrokeWidth(this.lineWidth); } get lineCap() { return this._restorableProps.lineCap; } set lineCap(val) { this._restorableProps.lineCap = isEmptyValue(val) ? defaults.lineCap : val; this._stylePaint.setStrokeCap(getNativeLineCap(this.lineCap)); } get lineJoin() { return this._restorableProps.lineJoin; } set lineJoin(val) { this._restorableProps.lineJoin = isEmptyValue(val) ? defaults.lineJoin : val; this._stylePaint.setStrokeJoin(getNativeLineJoin(this.lineJoin)); } get miterLimit() { return this._restorableProps.miterLimit; } set miterLimit(val) { this._restorableProps.miterLimit = typeof val === 'number' ? val : defaults.miterLimit; this._stylePaint.setStrokeMiter(this.miterLimit); } get lineDashOffset() { return this._restorableProps.lineDashOffset; } set lineDashOffset(val) { this._restorableProps.lineDashOffset = typeof val === 'number' ? val : defaults.lineDashOffset; this._stylePaint.setPathEffect(this.getLineDash()?.length ? new DashPathEffect(this.getLineDash(), this.lineDashOffset) : null); } get shadowOffsetX() { return this._restorableProps.shadowOffsetX; } set shadowOffsetX(val) { this._restorableProps.shadowOffsetX = typeof val === 'number' ? val : defaults.shadowOffsetX; if (this.shadowColor != null) { this._updateShadowLayer(); } } get shadowOffsetY() { return this._restorableProps.shadowOffsetY; } set shadowOffsetY(val) { this._restorableProps.shadowOffsetY = typeof val === 'number' ? val : defaults.shadowOffsetY; if (this.shadowColor != null) { this._updateShadowLayer(); } } get shadowBlur() { return this._restorableProps.shadowBlur; } set shadowBlur(val) { this._restorableProps.shadowBlur = typeof val === 'number' ? val : defaults.shadowBlur; if (this.shadowColor != null) { this._updateShadowLayer(); } } get shadowColor() { return this._restorableProps.shadowColor; } set shadowColor(val) { this._restorableProps.shadowColor = isEmptyValue(val) ? defaults.shadowColor : val; this._updateShadowLayer(); } get globalCompositeOperation() { return this._restorableProps.globalCompositeOperation; } set globalCompositeOperation(val) { this._restorableProps.globalCompositeOperation = isEmptyValue(val) ? defaults.globalCompositeOperation : val; this._stylePaint.setXfermode(new PorterDuffXfermode(getNativeCompositeOperation(this.globalCompositeOperation))); } get font() { return this._restorableProps.font; } set font(val) { this._restorableProps.font = val; let font; if (val) { const parsedFont = parseFont(val); const fontSize = parseFloat(parsedFont.fontSize); font = new Font(parsedFont.fontFamily, fontSize, parsedFont.fontStyle, parsedFont.fontWeight); } else { font = null; } this._stylePaint.setFont(font); this._font = font; // Some properties rely on font size this.letterSpacing = this.letterSpacing; } get textAlign() { return this._restorableProps.textAlign; } set textAlign(val) { this._restorableProps.textAlign = isEmptyValue(val) ? defaults.textAlign : val; this._stylePaint.setTextAlign(getNativeTextAlignment(this.textAlign, this.direction)); } get textBaseline() { return this._restorableProps.textBaseline; } set textBaseline(val) { this._restorableProps.textBaseline = val; } get direction() { return this._restorableProps.direction; } set direction(val) { this._restorableProps.direction = val; } get imageSmoothingEnabled() { return this._restorableProps.imageSmoothingEnabled; } set imageSmoothingEnabled(val) { this._restorableProps.imageSmoothingEnabled = !!val; } get letterSpacing() { return this._letterSpacing; } set letterSpacing(val) { this._letterSpacing = isEmptyValue(val) ? defaults.letterSpacing : val; const letterSpacingValue = parseFloat(this.letterSpacing) / (this._font.fontSize || 1); this._stylePaint.setLetterSpacing(letterSpacingValue); } } export { AbstractCanvasRenderingContext2D }; //# sourceMappingURL=AbstractCanvasRenderingContext2D.js.map