canvasimo
Version:
An HTML5 canvas drawing library, with 150+ useful methods, jQuery-like fluent interface, and cross-browser compatibility enhancements.
1,771 lines (1,672 loc) • 62.9 kB
text/typescript
// tslint:disable-next-line:no-var-requires
const { version: VERSION } = require('../package.json');
import {
CONTEXT_TYPE,
DEFAULT_CONTEXT_VALUES,
DEFAULT_DENSITY,
DEFAULT_FONT_PARTS,
DEFAULT_IMAGE_SMOOTHING_VALUES,
DEFAULT_LINE_DASH,
IMAGE_SMOOTHING_KEYS,
IMAGE_SMOOTHING_QUALITY_KEYS,
INCORRECT_GET_ANGLE_ARGUMENTS,
MATCHES_ALL_WHITESPACE,
MATCHES_WORD_BREAKS,
} from './constants';
import logger from './logger';
import {
BooleanFalsy,
CanvasContext,
CanvasContextAttributes,
CreateImageData,
DrawImage,
Fill,
FillOrStrokeStyle,
FillRule,
ForEach,
GetAngle,
GlobalCompositeOperation,
LimitedTextMetrics,
LineCap,
LineJoin,
MaxWidth,
Points,
PutImageData,
Repeat,
Segments,
SetSize,
Size,
StoredContextValues,
Stroke,
TextAlign,
TextBaseline,
WordBreak,
} from './types';
import {
formatFont,
forPoints,
getFontParts,
isFillRule,
} from './utils';
const unsupportedMethodErrors: string[] = [];
const logUnsupportedMethodError = (method: string) => {
if (unsupportedMethodErrors.indexOf(method) < 0) {
unsupportedMethodErrors.push(method);
logger.warn(`${method} is not supported by this browser`);
}
};
export class Canvasimo {
private element: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
private storedContextValues: StoredContextValues;
private ctxType: typeof CONTEXT_TYPE = CONTEXT_TYPE;
private density: number = DEFAULT_DENSITY;
public constructor (element: HTMLCanvasElement) {
this.element = element;
const ctx = this.element.getContext(CONTEXT_TYPE);
if (!ctx) {
throw new Error('Could not get a CanvasRenderingContext from the provided element');
}
this.ctx = ctx;
this.setDefaultContextValues();
this.storedContextValues = this.saveContextValues();
}
/**
* @group Canvas element
* @description A collection of methods for getting and setting various properties of the canvas element.
*/
/**
* Get the canvas element.
* @alias getElement
*/
public getCanvas = (): HTMLCanvasElement => this.element;
public getElement = (): HTMLCanvasElement => this.getCanvas();
/**
* Set the canvas pixel density.
*/
public setDensity = (density: number): Canvasimo => {
if (this.density !== density) {
const {
width: prevWidth,
height: prevHeight,
} = this.getSize();
this.saveContextValues();
this.density = density;
this.element.width = prevWidth * density;
this.element.height = prevHeight * density;
this.restoreContextValues();
}
return this;
}
/**
* Get the canvas pixel density.
*/
public getDensity = (): number => this.density;
/**
* Set the canvas dimensions.
*/
public setSize: SetSize = (width: number | Size, height?: number): Canvasimo => {
this.saveContextValues();
if (typeof width === 'object') {
this.element.width = width.width * this.density;
this.element.height = width.height * this.density;
} else if (typeof height === 'number') {
this.element.width = width * this.density;
this.element.height = height * this.density;
}
this.restoreContextValues();
return this;
}
/**
* Get the canvas dimensions.
*/
public getSize = (): Size => ({
width: this.element.width / this.density,
height: this.element.height / this.density,
})
/**
* Set the canvas width.
*/
public setWidth = (width: number): Canvasimo => {
this.saveContextValues();
this.element.width = width * this.density;
this.restoreContextValues();
return this;
}
/**
* Get the canvas width.
*/
public getWidth = (): number => this.element.width / this.density;
/**
* Set the canvas height.
*/
public setHeight = (height: number): Canvasimo => {
this.saveContextValues();
this.element.height = height * this.density;
this.restoreContextValues();
return this;
}
/**
* Get the canvas height.
*/
public getHeight = (): number => this.element.height / this.density;
/**
* Get the canvas size & position on screen.
*/
public getBoundingClientRect = (): ClientRect => this.element.getBoundingClientRect();
/**
* @group Context
* @description 'A collection of methods for retrieving a canvas context or information about the context.
*/
/**
* Get the standard canvas context (used for drawing).
*/
public getContext = (type: string, contextAttributes?: CanvasContextAttributes): CanvasContext | null => {
return this.element.getContext(type, contextAttributes);
}
/**
* Get canvas context used by Canvasimo (2d).
*/
public getCurrentContext = (): CanvasRenderingContext2D => this.ctx;
/**
* Get the context type used by Canvasimo ('2d', 'webgl', etc).
*/
public getCurrentContextType = (): typeof CONTEXT_TYPE => this.ctxType;
/**
* Get the context attributes used.
*/
public getContextAttributes = (): CanvasContextAttributes | null => {
if (typeof (this.ctx as any).getContextAttributes !== 'function') {
logUnsupportedMethodError('getContextAttributes');
return null;
}
return (this.ctx as any).getContextAttributes();
}
/**
* @group Solid Shapes
* @description A collection of methods for plotting or drawing solid shapes -
* those that create a new shape when invoked, and are self closing.
*/
// plotRect',
/**
* Plot a rectangle that can then have a fill or stroke applied to it.
* @alias rect
*/
public plotRect = (x: number, y: number, width: number, height: number): Canvasimo => this.rect(x, y, width, height);
public rect = (x: number, y: number, width: number, height: number): Canvasimo => {
this.ctx.rect(x * this.density, y * this.density, width * this.density, height * this.density);
return this;
}
/**
* Plot a rectangle and apply a stroke to it.
*/
public strokeRect = (x: number, y: number, width: number, height: number, color?: string): Canvasimo => {
if (typeof color !== 'undefined') {
this.setStroke(color);
}
this.ctx.strokeRect(x * this.density, y * this.density, width * this.density, height * this.density);
return this;
}
/**
* Plot a rectangle and apply a fill to it.
*/
public fillRect = (x: number, y: number, width: number, height: number, color?: string): Canvasimo => {
if (typeof color !== 'undefined') {
this.setFill(color);
}
this.ctx.fillRect(x * this.density, y * this.density, width * this.density, height * this.density);
return this;
}
/**
* Plot a rounded rectangle that can then have a fill or stroke applied to it.
*/
public plotRoundedRect = (x: number, y: number, width: number, height: number, radius: number): Canvasimo => {
const minRadius = Math.min(width / 2, height / 2, radius);
return this
.beginPath()
.moveTo(x + minRadius, y)
.lineTo(x + width - minRadius, y)
.arcTo(x + width, y, x + width, y + minRadius, minRadius)
.lineTo(x + width, y + height - minRadius)
.arcTo(x + width, y + height, x + width - minRadius, y + height, minRadius)
.lineTo(x + minRadius, y + height)
.arcTo(x, y + height, x, y + height - minRadius, minRadius)
.lineTo(x, y + minRadius)
.arcTo(x, y, x + minRadius, y, minRadius)
.closePath();
}
/**
* Plot a rounded rectangle and apply a stroke to it.
*/
public strokeRoundedRect = (
x: number,
y: number,
width: number,
height: number,
radius: number,
color?: string
): Canvasimo => {
return this
.plotRoundedRect(x, y, width, height, radius)
.stroke(color);
}
/**
* Plot a rounded rectangle and apply a fill to it.
*/
public fillRoundedRect = (
x: number,
y: number,
width: number,
height: number,
radius: number,
color?: string
): Canvasimo => {
return this
.plotRoundedRect(x, y, width, height, radius)
.fill(color);
}
/**
* Plot a circle that can then have a stroke or fill applied to it.
*/
public plotCircle = (x: number, y: number, radius: number, anticlockwise?: BooleanFalsy): Canvasimo => {
return this
.beginPath()
.plotArc(x, y, radius, 0, Math.PI * 2, anticlockwise)
.closePath();
}
/**
* Plot a circle and apply a stroke to it.
*/
public strokeCircle = (
x: number,
y: number,
radius: number,
anticlockwise?: BooleanFalsy,
color?: string
): Canvasimo => {
return this
.plotCircle(x, y, radius, anticlockwise)
.stroke(color);
}
/**
* Plot a circle and apply a fill to it.
*/
public fillCircle = (
x: number,
y: number,
radius: number,
anticlockwise?: BooleanFalsy,
color?: string
): Canvasimo => {
return this
.plotCircle(x, y, radius, anticlockwise)
.fill(color);
}
/**
* Plot a polygon that can then have a stroke or fill applied to it.
*/
public plotPoly = (x: number, y: number, radius: number, sides: number, anticlockwise?: BooleanFalsy): Canvasimo => {
sides = Math.round(sides);
if (!sides || sides < 3) {
return this;
}
const direction = anticlockwise ? -1 : 1;
const beforeEnd = (i: number) => anticlockwise ? i > -sides : i < sides;
this
.beginPath()
.moveTo(x + radius, y);
for (let i = 0; beforeEnd(i); i += direction) {
const angle = Math.PI * 2 / sides * i;
this.lineTo(x + radius * Math.cos(angle), y + radius * Math.sin(angle));
}
return this.closePath();
}
/**
* Plot a polygon and apply a stoke to it.
*/
public strokePoly = (
x: number,
y: number,
radius: number,
sides: number,
anticlockwise?: BooleanFalsy,
color?: string
): Canvasimo => {
sides = Math.round(sides);
if (!sides || sides < 3) {
return this;
}
return this
.plotPoly(x, y, radius, sides, anticlockwise)
.stroke(color);
}
/**
* Plot a polygon and apply a fill to it.
*/
public fillPoly = (
x: number,
y: number,
radius: number,
sides: number,
anticlockwise?: BooleanFalsy,
color?: string
): Canvasimo => {
sides = Math.round(sides);
if (!sides || sides < 3) {
return this;
}
return this
.plotPoly(x, y, radius, sides, anticlockwise)
.fill(color);
}
/**
* Plot a star that can then have a stroke or fill applied to it.
*/
public plotStar = (x: number, y: number, radius1: number, sides: number, anticlockwise?: BooleanFalsy): Canvasimo => {
sides = Math.round(sides);
if (!sides || sides < 3) {
return this;
} else if (sides === 3 || sides === 4) {
return this.plotPoly(x, y, radius1, sides);
}
sides = sides * 2;
const direction = anticlockwise ? -1 : 1;
const offset = Math.PI * 2 / sides;
const cross = Math.cos(offset * 2) * radius1;
const radius2 = cross / Math.cos(offset);
const beforeEnd = (i: number) => anticlockwise ? i > -sides : i < sides;
this
.beginPath()
.moveTo(x + radius1, y);
for (let i = 0; beforeEnd(i); i += direction) {
const angle = offset * i;
const radius = i % 2 ? radius2 : radius1;
this.lineTo(x + radius * Math.cos(angle), y + radius * Math.sin(angle));
}
return this.closePath();
}
/**
* Plot a star and apply a stoke to it.
*/
public strokeStar = (
x: number,
y: number,
radius1: number,
sides: number,
anticlockwise?: BooleanFalsy,
color?: string
): Canvasimo => {
sides = Math.round(sides);
if (!sides || sides < 3) {
return this;
}
return this
.plotStar(x, y, radius1, sides, anticlockwise)
.stroke(color);
}
/**
* Plot a star and apply a fill to it.
*/
public fillStar = (
x: number,
y: number,
radius1: number,
sides: number,
anticlockwise?: BooleanFalsy,
color?: string
): Canvasimo => {
sides = Math.round(sides);
if (!sides || sides < 3) {
return this;
}
return this
.plotStar(x, y, radius1, sides, anticlockwise)
.fill(color);
}
/**
* Plot a burst that can then have a stroke or fill applied to it.
*/
public plotBurst = (
x: number,
y: number,
radius1: number,
radius2: number,
sides: number,
anticlockwise?: BooleanFalsy
): Canvasimo => {
sides = Math.round(sides);
if (!sides || sides < 3) {
return this;
}
sides = sides * 2;
const direction = anticlockwise ? -1 : 1;
const offset = Math.PI * 2 / sides;
const beforeEnd = (i: number) => anticlockwise ? i > -sides : i < sides;
this
.beginPath()
.moveTo(x + radius1, y);
for (let i = 0; beforeEnd(i); i += direction) {
const angle = offset * i;
const radius = i % 2 ? radius2 : radius1;
this.lineTo(x + radius * Math.cos(angle), y + radius * Math.sin(angle));
}
return this
.closePath();
}
/**
* Plot a burst and apply a stoke to it.
*/
public strokeBurst = (
x: number,
y: number,
radius1: number,
radius2: number,
sides: number,
anticlockwise?: BooleanFalsy,
color?: string
): Canvasimo => {
sides = Math.round(sides);
if (!sides || sides < 3) {
return this;
}
return this
.plotBurst(x, y, radius1, radius2, sides, anticlockwise)
.stroke(color);
}
/**
* Plot a burst and apply a fill to it.
*/
public fillBurst = (
x: number,
y: number,
radius1: number,
radius2: number,
sides: number,
anticlockwise?: BooleanFalsy,
color?: string
): Canvasimo => {
sides = Math.round(sides);
if (!sides || sides < 3) {
return this;
}
return this
.plotBurst(x, y, radius1, radius2, sides, anticlockwise)
.fill(color);
}
/**
* Plot a single pixel that can then have a stroke or fill applied to it.
*/
public plotPixel = (x: number, y: number): Canvasimo => {
return this
.plotRect(x, y, 1, 1);
}
/**
* Plot a single pixel and apply a stroke to it.
*/
public strokePixel = (x: number, y: number, color?: string): Canvasimo => {
return this
.strokeRect(x, y, 1, 1, color);
}
/**
* Plot a single pixel and apply a fill to it.
*/
public fillPixel = (x: number, y: number, color?: string): Canvasimo => {
return this
.fillRect(x, y, 1, 1, color);
}
/**
* Plot a closed path that can then have a stroke or fill applied to it.
*/
public plotClosedPath = (points: Points): Canvasimo => {
return this
.beginPath()
.plotPath(points)
.closePath();
}
/**
* Plot a closed path and apply a stroke to it.
*/
public strokeClosedPath = (points: Points, color?: string): Canvasimo => {
return this
.plotClosedPath(points)
.stroke(color);
}
/**
* Plot a closed path and apply a fill to it.
*/
public fillClosedPath = (points: Points, color?: string): Canvasimo => {
return this
.plotClosedPath(points)
.fill(color);
}
/**
* @group Open Shapes
* @description A collection of methods for plotting or drawing open shapes -
* those that create a new shape when invoked, but are not self closing.
*/
/**
* Plot a line that can then have a stroke or fill applied to it.
*/
public plotLine = (x1: number, y1: number, x2: number, y2: number): Canvasimo => {
return this
.moveTo(x1, y1)
.lineTo(x2, y2);
}
/**
* Plot a line and apply a stroke to it.
*/
public strokeLine = (x1: number, y1: number, x2: number, y2: number, color?: string): Canvasimo => {
return this
.plotLine(x1, y1, x2, y2)
.stroke(color);
}
/**
* Plot a line, by length & angle, that can then have a stroke or fill applied to it.
*/
public plotLength = (x1: number, y1: number, length: number, angle: number): Canvasimo => {
const x2 = x1 + length * Math.cos(angle);
const y2 = y1 + length * Math.sin(angle);
return this
.moveTo(x1, y1)
.lineTo(x2, y2);
}
/**
* Plot a line, by length & angle, and apply a stroke to it.
*/
public strokeLength = (x1: number, y1: number, length: number, angle: number, color?: string): Canvasimo => {
return this
.plotLength(x1, y1, length, angle)
.stroke(color);
}
/**
* Plot a path, that is not self closing, that can have a stroke or fill applied to it.
*/
public plotPath = (points: Points): Canvasimo => {
forPoints(points, (x: number, y: number, i: number) => {
if (i === 0) {
this.moveTo(x, y);
} else {
this.lineTo(x, y);
}
});
return this;
}
/**
* Plot a path, that is not self closing, and apply a stroke to it.
*/
public strokePath = (points: Points, color?: string): Canvasimo => {
return this
.plotPath(points)
.stroke(color);
}
/**
* Plot a path, that is not self closing, and apply a fill to it.
*/
public fillPath = (points: Points, color?: string): Canvasimo => {
return this
.plotPath(points)
.fill(color);
}
/**
* @group Paths
* @description A collection of methods for plotting or drawing paths -
* shapes that can be connected to create more complex shapes.
*/
/**
* Plot an arc that can have a stroke or fill applied to it.
* @alias arc
*/
public plotArc = (
x: number,
y: number,
radius: number,
startAngle: number,
endAngle: number,
anticlockwise?: BooleanFalsy
): Canvasimo => {
return this.arc(x, y, radius, startAngle, endAngle, anticlockwise);
}
public arc = (
x: number,
y: number,
radius: number,
startAngle: number,
endAngle: number,
anticlockwise?: BooleanFalsy
): Canvasimo => {
this.ctx.arc(
x * this.density,
y * this.density,
radius * this.density,
startAngle,
endAngle,
anticlockwise || false
);
return this;
}
/**
* Plot an arc and apply a stroke to it.
*/
public strokeArc = (
x: number,
y: number,
radius: number,
startAngle: number,
endAngle: number,
anticlockwise?: BooleanFalsy,
color?: string
): Canvasimo => {
return this
.plotArc(x, y, radius, startAngle, endAngle, anticlockwise)
.stroke(color);
}
/**
* Plot an arc and apply a fill to it.
*/
public fillArc = (
x: number,
y: number,
radius: number,
startAngle: number,
endAngle: number,
anticlockwise?: BooleanFalsy,
color?: string
): Canvasimo => {
return this
.plotArc(x, y, radius, startAngle, endAngle, anticlockwise)
.fill(color);
}
/**
* Plot an ellipse that can then have a stroke or fill applied to it.
* @alias ellipse
*/
public plotEllipse = (
x: number,
y: number,
radiusX: number,
radiusY: number,
rotation: number,
startAngle: number,
endAngle: number,
anticlockwise?: BooleanFalsy
): Canvasimo => {
return this.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise);
}
public ellipse = (
x: number,
y: number,
radiusX: number,
radiusY: number,
rotation: number,
startAngle: number,
endAngle: number,
anticlockwise?: BooleanFalsy
): Canvasimo => {
// tslint:disable-next-line:strict-type-predicates
if (typeof this.ctx.ellipse === 'function') {
this.ctx.ellipse(
x * this.density,
y * this.density,
radiusX * this.density,
radiusY * this.density,
rotation,
startAngle,
endAngle,
anticlockwise || false
);
return this;
}
return this
.save()
.translate(x, y)
.rotate(rotation)
.scale(1, radiusY / radiusX)
.plotArc(0, 0, radiusX, startAngle, endAngle, anticlockwise)
.restore();
}
/**
* Plot an ellipse and apply a stroke to it.
*/
public strokeEllipse = (
x: number,
y: number,
radiusX: number,
radiusY: number,
rotation: number,
startAngle: number,
endAngle: number,
anticlockwise?: BooleanFalsy,
color?: string
): Canvasimo => {
return this
.plotEllipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise)
.stroke(color);
}
/**
* Plot an ellipse and apply a fill to it.
*/
public fillEllipse = (
x: number,
y: number,
radiusX: number,
radiusY: number,
rotation: number,
startAngle: number,
endAngle: number,
anticlockwise?: BooleanFalsy,
color?: string
): Canvasimo => {
return this
.plotEllipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise)
.fill(color);
}
/**
* @group Text
* @description A collection of methods for drawing text,
* and getting and setting properties related to text rendering.
*/
/**
* Draw a text with a stroke.
*/
public strokeText = (text: string, x: number, y: number, maxWidth?: MaxWidth, color?: string): Canvasimo => {
if (typeof color !== 'undefined') {
this.setStroke(color);
}
if (typeof maxWidth !== 'number') {
this.ctx.strokeText(text, x * this.density, y * this.density);
} else {
this.ctx.strokeText(text, x * this.density, y * this.density, maxWidth * this.density);
}
return this;
}
/**
* Draw a text with a fill.
*/
public fillText = (text: string, x: number, y: number, maxWidth?: MaxWidth, color?: string): Canvasimo => {
if (typeof color !== 'undefined') {
this.setFill(color);
}
// If max width is not a number (e.g. undefined) then iOS does not draw anything
if (typeof maxWidth !== 'number') {
this.ctx.fillText(text, x * this.density, y * this.density);
} else {
this.ctx.fillText(text, x * this.density, y * this.density, maxWidth * this.density);
}
return this;
}
/**
* Draw text with a stroke, wrapped at newlines and automatically wrapped if the text exceeds the maxWidth.
* If no maxWidth is specified text will only wrap at newlines (wordBreak is ignore).
* Words will not break by default (normal) and therefore may overflow.
* break-all will break words wherever possible, and break-word will only break words if there is not enough room.
* The lineHeight parameter is a multiplier for the font size, and defaults to 1.
*/
public strokeTextMultiline = (
text: string,
x: number,
y: number,
maxWidth?: MaxWidth,
wordBreak?: WordBreak,
lineHeight?: number,
color?: string
): Canvasimo => {
return this.textMultiline(
this.strokeText,
text,
x,
y,
maxWidth,
wordBreak,
lineHeight,
color
);
}
/**
* Draw text with a fill, wrapped at newlines and automatically wrapped if the text exceeds the maxWidth.
* If no maxWidth is specified text will only wrap at newlines (wordBreak is ignore).
* Words will not break by default (normal) and therefore may overflow.
* break-all will break words wherever possible, and break-word will only break words if there is not enough room.
* The lineHeight parameter is a multiplier for the font size, and defaults to 1.
*/
public fillTextMultiline = (
text: string,
x: number,
y: number,
maxWidth?: MaxWidth,
wordBreak?: WordBreak,
lineHeight?: number,
color?: string
): Canvasimo => {
return this.textMultiline(
this.fillText,
text,
x,
y,
maxWidth,
wordBreak,
lineHeight,
color
);
}
/**
* Get information about the size text will be drawn.
* @alias measureText
*/
public getTextSize = (text: string): LimitedTextMetrics => this.measureText(text);
public measureText = (text: string): LimitedTextMetrics => {
const metrics = this.ctx.measureText(text);
return {
width: (metrics.width || 0) / this.density,
};
}
/**
* Set the horizontal text alignment.
*/
public setTextAlign = (value: TextAlign): Canvasimo => this.setCanvasProperty('textAlign', value);
/**
* Get the horizontal text alignment.
*/
public getTextAlign = (): TextAlign => this.getCanvasProperty('textAlign');
/**
* Set the vertical text alignment.
*/
public setTextBaseline = (value: TextBaseline): Canvasimo => this.setCanvasProperty('textBaseline', value);
/**
* Get the vertical text alignment.
*/
public getTextBaseline = (): TextBaseline => this.getCanvasProperty('textBaseline');
/**
* @group Fonts
* @description A collection of methods for getting and setting font styles and variations.
*/
/**
* Set the font to use.
*/
public setFont = (font: string): Canvasimo => {
this.ctx.font = formatFont(font, this.density, false);
return this;
}
/**
* Get the font that is being used.
* This returns the exact CanvasRenderingContext2D.font string.
*/
public getFont = (): string => {
return formatFont(this.ctx.font, this.density, true);
}
/**
* Set the font family to use.
*/
public setFontFamily = (family: string): Canvasimo => {
const parts = getFontParts(this.ctx.font, this.density, true);
if (parts.length < 5) {
return this.setFont('');
}
parts[4] = family || DEFAULT_FONT_PARTS[4];
this.ctx.font = formatFont(parts.join(' '), this.density, false);
return this;
}
/**
* Get the font that is being used.
*/
public getFontFamily = (): string | null => {
const parts = getFontParts(this.ctx.font, this.density, true);
if (parts.length < 5) {
return null;
}
return parts[4];
}
/**
* Set the font size to use.
*/
public setFontSize = (size: string | number): Canvasimo => {
const parts = getFontParts(this.ctx.font, this.density, true);
if (parts.length < 5) {
return this.setFont('');
}
parts[3] = (typeof size === 'number' ? size + 'px' : size) || DEFAULT_FONT_PARTS[3];
this.ctx.font = formatFont(parts.join(' '), this.density, false);
return this;
}
/**
* Get the font size that is being used.
* Returns null if using a special font e.g. caption, icon, menu.
*/
public getFontSize = (): number | null => {
const parts = getFontParts(this.ctx.font, this.density, true);
if (parts.length < 5) {
return null;
}
return parseFloat(parts[3]);
}
/**
* Set the font style to use.
*/
public setFontStyle = (style: string): Canvasimo => {
const parts = getFontParts(this.ctx.font, this.density, true);
if (parts.length < 5) {
return this.setFont('');
}
parts[0] = style || DEFAULT_FONT_PARTS[0];
this.ctx.font = formatFont(parts.join(' '), this.density, false);
return this;
}
/**
* Get the font style that is being used.
* Returns null if using a special font e.g. caption, icon, menu.
*/
public getFontStyle = (): string | null => {
const parts = getFontParts(this.ctx.font, this.density, true);
if (parts.length < 5) {
return null;
}
return parts[0];
}
/**
* Set the font variant to use.
*/
public setFontVariant = (variant: string): Canvasimo => {
const parts = getFontParts(this.ctx.font, this.density, true);
if (parts.length < 5) {
return this.setFont('');
}
parts[1] = variant || DEFAULT_FONT_PARTS[1];
this.ctx.font = formatFont(parts.join(' '), this.density, false);
return this;
}
/**
* Get the font variant that is being used.
* Returns null if using a special font e.g. caption, icon, menu.
*/
public getFontVariant = (): string | null => {
const parts = getFontParts(this.ctx.font, this.density, true);
if (parts.length < 5) {
return null;
}
return parts[1];
}
/**
* Set the font weight to use.
*/
public setFontWeight = (weight: string | number): Canvasimo => {
const parts = getFontParts(this.ctx.font, this.density, true);
if (parts.length < 5) {
return this.setFont('');
}
parts[2] = weight.toString() || DEFAULT_FONT_PARTS[2];
this.ctx.font = formatFont(parts.join(' '), this.density, false);
return this;
}
/**
* Get the font weight that is being used.
* Returns null if using a special font e.g. caption, icon, menu.
*/
public getFontWeight = (): string | number | null => {
const parts = getFontParts(this.ctx.font, this.density, true);
if (parts.length < 5) {
return null;
}
return parts[2];
}
/**
* @group Stroke Styles
* @description A collection of methods for getting and setting stroke styles,
* and applying strokes to existing shapes.
*/
/**
* Apply a stroke to the current shape.
*/
public stroke: Stroke = (color?: string | Path2D, path?: Path2D): Canvasimo => {
if (typeof color === 'string') {
this.setStroke(color);
// tslint:disable-next-line:strict-type-predicates
if (path && typeof path === 'object') {
this.ctx.stroke(path);
} else {
this.ctx.stroke();
}
// tslint:disable-next-line:strict-type-predicates
} else if (color && typeof color === 'object') {
this.ctx.stroke(color);
// tslint:disable-next-line:strict-type-predicates
} else if (path && typeof path === 'object') {
this.ctx.stroke(path);
} else {
this.ctx.stroke();
}
return this;
}
/**
* Set the stroke style to use.
* @alias setStrokeStyle
*/
public setStroke = (value: FillOrStrokeStyle): Canvasimo => this.setStrokeStyle(value);
public setStrokeStyle = (value: FillOrStrokeStyle): Canvasimo => this.setCanvasProperty('strokeStyle', value);
/**
* Get the stroke style that is being used.
* @alias getStrokeStyle
*/
public getStroke = (): FillOrStrokeStyle => this.getStrokeStyle();
public getStrokeStyle = (): string => this.getCanvasProperty('strokeStyle');
/**
* Set the stroke cap to use.
* @alias setLineCap
*/
public setStrokeCap = (value: LineCap): Canvasimo => this.setLineCap(value);
public setLineCap = (value: LineCap): Canvasimo => this.setCanvasProperty('lineCap', value);
/**
* Get the stroke cap that is being used.
* @alias getLineCap
*/
public getStrokeCap = (): LineCap => this.getLineCap();
public getLineCap = (): LineCap => this.getCanvasProperty('lineCap');
/**
* Set the stroke dash to use.
* @alias setLineDash
*/
public setStrokeDash = (segments: Segments): Canvasimo => this.setLineDash(segments);
public setLineDash = (segments: Segments): Canvasimo => {
// tslint:disable-next-line:strict-type-predicates
if (typeof this.ctx.setLineDash !== 'function') {
logUnsupportedMethodError('setLineDash');
return this;
}
this.ctx.setLineDash(segments.map((segment) => segment * this.density));
return this;
}
/**
* Get the stroke dash that is being used.
* @alias getLineDash
*/
public getStrokeDash = (): Segments => this.getLineDash();
public getLineDash = (): Segments => {
// tslint:disable-next-line:strict-type-predicates
if (typeof this.ctx.getLineDash !== 'function') {
logUnsupportedMethodError('getLineDash');
return [];
}
return (this.ctx.getLineDash() || []).map((value) => value / this.density);
}
/**
* Set the stroke dash offset to use.
* @alias setLineDashOffset
*/
public setStrokeDashOffset = (value: number): Canvasimo => this.setLineDashOffset(value);
public setLineDashOffset = (value: number): Canvasimo => {
return this.setCanvasProperty('lineDashOffset', value * this.density);
}
/**
* Get the stroke dash offset that is being used.
* @alias getLineDashOffset
*/
public getStrokeDashOffset = (): number => this.getLineDashOffset();
public getLineDashOffset = (): number => this.getCanvasProperty('lineDashOffset') / this.density;
/**
* Set the stroke join to use.
* @alias setLineJoin
*/
public setStrokeJoin = (value: LineJoin): Canvasimo => this.setLineJoin(value);
public setLineJoin = (value: LineJoin): Canvasimo => this.setCanvasProperty('lineJoin', value);
/**
* Get the stroke join that is being used.
* @alias getLineJoin
*/
public getStrokeJoin = (): LineJoin => this.getLineJoin();
public getLineJoin = (): LineJoin => this.getCanvasProperty('lineJoin');
/**
* Set the stroke width to use.
* @alias setLineWidth
*/
public setStrokeWidth = (value: number): Canvasimo => this.setLineWidth(value);
public setLineWidth = (value: number): Canvasimo => this.setCanvasProperty('lineWidth', value * this.density);
/**
* Get the stroke width that is being used.
* @alias getLineWidth
*/
public getStrokeWidth = (): number => this.getLineWidth();
public getLineWidth = (): number => this.getCanvasProperty('lineWidth') / this.density;
/**
* Set the miter limit to use.
*/
public setMiterLimit = (value: number): Canvasimo => this.setCanvasProperty('miterLimit', value * this.density);
/**
* Get the miter limit that is being used.
*/
public getMiterLimit = (): number => this.getCanvasProperty('miterLimit') / this.density;
/**
* @group Fill styles
* @description A collection of methods for getting and setting fill styles,
* and applying fills to existing shapes.
*/
/**
* Apply a fill to the current shape.
*/
public fill: Fill = (color?: string | FillRule, fillRule?: FillRule): Canvasimo => {
if (isFillRule(color)) {
this.ctx.fill(color);
} else if (typeof color === 'string') {
this.setFill(color);
if (fillRule) {
this.ctx.fill(fillRule);
} else {
this.ctx.fill();
}
} else {
this.ctx.fill(fillRule);
}
return this;
}
/**
* Apply a fill to the entire canvas area.
*/
public fillCanvas = (color?: string): Canvasimo => {
return this
.resetTransform()
.fillRect(0, 0, this.getWidth(), this.getHeight(), color);
}
/**
* Clear the entire canvas area
*/
public clearCanvas = (): Canvasimo => {
return this.setWidth(this.getWidth());
}
/**
* Clear a rectangular area of the canvas.
*/
public clearRect = (x: number, y: number, width: number, height: number): Canvasimo => {
this.ctx.clearRect(x * this.density, y * this.density, width * this.density, height * this.density);
return this;
}
/**
* Set the fill to use.
* @alias setFillStyle
*/
public setFill = (value: FillOrStrokeStyle): Canvasimo => this.setFillStyle(value);
public setFillStyle = (value: FillOrStrokeStyle): Canvasimo => this.setCanvasProperty('fillStyle', value);
/**
* Get the fill that is being used.
* @alias getFillStyle
*/
public getFill = (): FillOrStrokeStyle => this.getFillStyle();
public getFillStyle = (): FillOrStrokeStyle => this.getCanvasProperty('fillStyle');
/**
* Create a linear gradient to use as a fill.
*/
public createLinearGradient = (x0: number, y0: number, x1: number, y1: number): CanvasGradient => {
return this.ctx.createLinearGradient(x0 * this.density, y0 * this.density, x1 * this.density, y1 * this.density);
}
/**
* Create a radial gradient to use as a fill.
*/
public createRadialGradient = (
x0: number,
y0: number,
r0: number,
x1: number,
y1: number,
r1: number
): CanvasGradient => {
return this.ctx.createRadialGradient(
x0 * this.density,
y0 * this.density,
r0 * this.density,
x1 * this.density,
y1 * this.density,
r1 * this.density
);
}
/**
* Create a pattern to be used as a fill.
*/
public createPattern = (
image: HTMLImageElement | HTMLCanvasElement | HTMLVideoElement,
repetition: string
): CanvasPattern | null => {
return this.ctx.createPattern(image, repetition);
}
/**
* Draw an image to the canvas.
* If the second position / size arguments are supplied, the first will be used for cropping the image,
* and the second for the position and size it will be drawn.
*/
public drawImage: DrawImage = (
image: HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | ImageBitmap,
srcX: number,
srcY: number,
srcW?: number,
srcH?: number,
dstX?: number,
dstY?: number,
dstW?: number,
dstH?: number
): Canvasimo => {
if (
typeof srcW !== 'undefined' &&
typeof srcH !== 'undefined'
) {
if (
typeof dstX !== 'undefined' &&
typeof dstY !== 'undefined' &&
typeof dstW !== 'undefined' &&
typeof dstH !== 'undefined'
) {
this.ctx.drawImage(
image,
srcX * this.density,
srcY * this.density,
srcW * this.density,
srcH * this.density,
dstX * this.density,
dstY * this.density,
dstW * this.density,
dstH * this.density
);
} else {
this.ctx.drawImage(image, srcX * this.density, srcY * this.density, srcW * this.density, srcH * this.density);
}
} else {
this.ctx.drawImage(image, srcX * this.density, srcY * this.density);
}
return this;
}
/**
* @group Image Data
* @description A collection of methods for creating, putting, or getting image data about the canvas.
*/
/**
* Get a data URL of the current canvas state.
*/
public getDataURL = (type?: string, ...args: any[]): string => this.element.toDataURL(type, ...args);
/**
* Create image data with either the width and height specified,
* or with the width and height of a the image data supplied.
*/
public createImageData: CreateImageData = (width: number | ImageData, height?: number): ImageData => {
if (typeof width !== 'number') {
return this.ctx.createImageData(width);
}
return this.ctx.createImageData(width * this.density, height || 0 * this.density);
}
/**
* Get the image data from an area of the canvas.
*/
public getImageData = (sx: number, sy: number, sw: number, sh: number): ImageData => {
return this.ctx.getImageData(sx * this.density, sy * this.density, sw * this.density, sh * this.density);
}
/**
* Draw image data onto the canvas.
*/
public putImageData: PutImageData = (
imagedata: ImageData,
dx: number,
dy: number,
dirtyX?: number,
dirtyY?: number,
dirtyWidth?: number,
dirtyHeight?: number
): Canvasimo => {
if (
typeof dirtyX === 'undefined' ||
typeof dirtyY === 'undefined' ||
typeof dirtyWidth === 'undefined' ||
typeof dirtyHeight === 'undefined'
) {
this.ctx.putImageData(
imagedata,
dx * this.density,
dy * this.density
);
} else {
this.ctx.putImageData(
imagedata,
dx * this.density,
dy * this.density,
dirtyX * this.density,
dirtyY * this.density,
dirtyWidth * this.density,
dirtyHeight * this.density
);
}
return this;
}
/**
* Get image data about a specific pixel.
*/
public getPixelData = (x: number, y: number): Uint8ClampedArray => {
return this.getImageData(x, y, 1, 1).data;
}
/**
* Get the color of a specific pixel.
*/
public getPixelColor = (x: number, y: number): string => {
const data = this.getImageData(x, y, 1, 1).data;
return this.createRGBA(data[0], data[1], data[2], data[3]);
}
/**
* @group Color Helpers
* @description A collection of methods to help with creating color strings.
*/
/**
* Create an HSL color string from the given values.
*/
public createHSL = (h: number, s: number, l: number): string => {
return 'hsl(' + h + ',' + s + '%,' + l + '%)';
}
/**
* Create an HSLA color string from the given values.
*/
public createHSLA = (h: number, s: number, l: number, a: number): string => {
return 'hsla(' + h + ',' + s + '%,' + l + '%,' + a + ')';
}
/**
* Create an RGB color string from the given values.
*/
public createRGB = (r: number, g: number, b: number): string => {
return 'rgb(' + r + ',' + g + ',' + b + ')';
}
/**
* Create an RGBA color string from the given values.
*/
public createRGBA = (r: number, g: number, b: number, a: number): string => {
return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
}
/**
* Return an HSL color string from the given HSLA color string.
*/
public getHSLFromHSLA = (color: string): string => this.getRGBFromRGBA(color);
/**
* Return an RGB color string from the given RGBA color string.
*/
public getRGBFromRGBA = (color: string): string => {
const lastCommaIndex = color.lastIndexOf(',');
return color.replace(/^(\w{3})a/, '$1').substring(0, lastCommaIndex - 1) + ')';
}
/**
* @group Converting Sizes
* @description A collection of methods to help with calculating and converting sizes, and distances.
*/
/**
* Get a fraction from the provided percent value e.g. 80 returns 0.8.
*/
public getFractionFromPercent = (percent: number): number => {
return (percent / 100);
}
/**
* Get a percent from the provided fraction value e.g. 0.7 returns 70.
*/
public getPercentFromFraction = (fraction: number): number => {
return (fraction * 100);
}
/**
* Returns the actual value of a fraction of the canvas width e.g.
* a canvas with a width of 200 returns 100 if the provided value is 0.5.
*/
public getFractionOfWidth = (fraction: number): number => {
return this.getWidth() * fraction;
}
/**
* Returns the actual value of a fraction of the canvas height e.g.
* a canvas with a height of 100 returns 20 if the provided value is 0.2.
*/
public getFractionOfHeight = (fraction: number): number => {
return this.getHeight() * fraction;
}
/**
* Returns the actual value of a percentage of the canvas width e.g.
* a canvas with a width of 200 returns 100 if the provided value is 50.
*/
public getPercentOfWidth = (percent: number): number => {
return this.getWidth() / 100 * percent;
}
/**
* Returns the actual value of a percentage of the canvas height e.g.
* a canvas with a height of 100 returns 20 if the provided value is 20.
*/
public getPercentOfHeight = (percent: number): number => {
return this.getHeight() / 100 * percent;
}
/**
* Returns the distance between 2 points.
*/
public getDistance = (x1: number, y1: number, x2: number, y2: number): number => {
return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
}
/**
* @group Converting Angles
* @description A collection of methods to help with calculating and converting angles.
*/
/**
* Get a radian value from the provided degrees e.g. 90 returns 1.5708.
*/
public getRadiansFromDegrees = (degrees: number): number => {
return degrees * Math.PI / 180;
}
/**
* Get a degree value from the provided radians e.g. 3.14159 returns 180.
*/
public getDegreesFromRadians = (radians: number): number => {
return radians * 180 / Math.PI;
}
/**
* Get the angle (in radians) between 2 or 3 points.
*/
public getAngle: GetAngle = (...args: number[]): number => {
if (!args.length || !(args.length === 4 || args.length === 6)) {
throw new Error(INCORRECT_GET_ANGLE_ARGUMENTS);
}
const x1 = args[0];
const y1 = args[1];
const x2 = args[2];
const y2 = args[3];
if (args.length === 4) {
return Math.atan2(y2 - y1, x2 - x1);
}
const x3 = args[4];
const y3 = args[5];
const a = this.getAngle(x1, y1, x2, y2);
const b = this.getAngle(x2, y2, x3, y3);
const c = b - a;
if (c >= 0) {
return Math.PI - c;
}
return -Math.PI - c;
}
/**
* @group Path Plotting
* @description A collection of methods for path drawing.
*/
/**
* Begin a new path (shape).
*/
public beginPath = (): Canvasimo => {
this.ctx.beginPath();
return this;
}
/**
* Close the current path (shape).
*/
public closePath = (): Canvasimo => {
this.ctx.closePath();
return this;
}
/**
* Move the starting point of a the next sub-path.
*/
public moveTo = (x: number, y: number): Canvasimo => {
this.ctx.moveTo(x * this.density, y * this.density);
return this;
}
/**
* Connect the last point to the provided coordinates.
*/
public lineTo = (x: number, y: number): Canvasimo => {
this.ctx.lineTo(x * this.density, y * this.density);
return this;
}
/**
* Arc from one point to another.
*/
public arcTo = (x1: number, y1: number, x2: number, y2: number, radius: number): Canvasimo => {
this.ctx.arcTo(x1 * this.density, y1 * this.density, x2 * this.density, y2 * this.density, radius * this.density);
return this;
}
/**
* Connect the last point to the provided coordinates with a bezier curve (2 control points).
*/
public bezierCurveTo = (cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): Canvasimo => {
this.ctx.bezierCurveTo(
cp1x * this.density,
cp1y * this.density,
cp2x * this.density,
cp2y * this.density,
x * this.density,
y * this.density
);
return this;
}
/**
* Connect the last point to the provided coordinates with a quadratic curve (1 control point).
*/
public quadraticCurveTo = (cpx: number, cpy: number, x: number, y: number): Canvasimo => {
this.ctx.quadraticCurveTo(cpx * this.density, cpy * this.density, x * this.density, y * this.density);
return this;
}
/**
* @group Canvas State
* @description A collection of methods to save, restore, or transform the canvas state.
*/
/**
* Push the current state of the canvas into a stack that can later be restored.
*/
public save = (): Canvasimo => {
this.ctx.save();
return this;
}
/**
* Restore the most recent state of the canvas that was saved.
*/
public restore = (): Canvasimo => {
this.ctx.restore();
return this;
}
/**
* Add rotation (in radians) to the transform matrix so that shapes can be drawn at an angle.
*/
public rotate = (angle: number): Canvasimo => {
this.ctx.rotate(angle);
return this;
}
/**
* Scale the transform matrix so that shapes can be drawn at the provided scale.
*/
public scale = (x: number, y: number): Canvasimo => {
this.ctx.scale(x, y);
return this;
}
/**
* Move the canvas origin.
*/
public translate = (x: number, y: number): Canvasimo => {
this.ctx.translate(x * this.density, y * this.density);
return this;
}
/**
* Multiply the current transformation with the provided matrix.
*/
public transform = (m11: number, m12: number, m21: number, m22: number, dx: number, dy: number): Canvasimo => {
this.ctx.transform(m11, m12, m21, m22, dx, dy);
return this;
}
/**
* Replace the current transformation with the provided matrix.
*/
public setTransform = (m11: number, m12: number, m21: number, m22: number, dx: number, dy: number): Canvasimo => {
this.ctx.setTransform(m11, m12, m21, m22, dx, dy);
return this;
}
/**
* Replace the current transformation with the default matrix: [1, 0, 0, 1, 0, 0].
*/
public resetTransform = (): Canvasimo => {
if (typeof (this.ctx as any).resetTransform === 'function') {
(this.ctx as any).resetTransform();
return this;
}
return this.setTransform(1, 0, 0, 1, 0, 0);
}
/**
* Use the current path as a clipping path.
*/
public clip = (fillRule?: FillRule): Canvasimo => {
this.ctx.clip(fillRule);
return this;
}
/**
* Set the opacity to use for drawing.
* @alias setGlobalAlpha
*/
public setOpacity = (value: number): Canvasimo => this.setGlobalAlpha(value);
public setGlobalAlpha = (value: number): Canvasimo => this.setCanvasProperty('globalAlpha', value);
/**
* Get the opacity that is being used.
* @alias getGlobalAlpha
*/
public getOpacity = (): number => this.getGlobalAlpha();
public getGlobalAlpha = (): number => this.getCanvasProperty('globalAlpha');
/**
* Set the composite operation to use for drawing.
* @alias setGlobalCompositeOperation
*/
public setCompositeOperation = (value: GlobalCompositeOperation): Canvasimo => {
return this.setGlobalCompositeOperation(value);
}
public setGlobalCompositeOperation = (value: GlobalCompositeOperation): Canvasimo => {
return this.setCanvasProperty('globalCompositeOperation', value);
}
/**
* Get the composite operation that is being used.
* @alias getGlobalCompositeOperation
*/
public getCompositeOperation = (): GlobalCompositeOperation => this.getGlobalCompositeOperation();
public getGlobalCompositeOperation = (): GlobalCompositeOperation => {
return this.getCanvasProperty('globalCompositeOperation');
}
/**
* Set whether image smoothing should be used.
*/
public setImageSmoothingEnabled = (value: BooleanFalsy): Canvasimo => {
for (const key of IMAGE_SMOOTHING_KEYS) {
if (key in this.ctx) {
this.ctx[key] = value || false;
return this;
}
}
return this;
}
/**
* Get whether image smoothing is being used.
*/
public getImageSmoothingEnabled = (): boolean => {
for (const key of IMAGE_SMOOTHING_KEYS) {
if (key in this.ctx) {
return this.ctx[key];
}
}
return false;
}
/**
* Set the image smoothing quality.
*/
public setImageSmoothingQuality = (value: ImageSmoothingQuality): Canvasimo => {
for (const key of IMAGE_SMOOTHING_QUALITY_KEYS) {
if (key in this.ctx) {
this.ctx[key] = value;
return this;
}
}
return this;
}
/**
* Get the current image smoothing quality.
*/
public getImageSmoothingQuality = (): ImageSmoothingQuality => {
for (const key of IMAGE_SMO