@nativescript-community/ui-htmlcanvasapi
Version:
An HTML Canvas API implementation on top of android and iOS native APIs
812 lines • 28.6 kB
JavaScript
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