js-draw
Version:
Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.
276 lines (275 loc) • 11.2 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const TextComponent_1 = __importDefault(require("../../components/TextComponent"));
const math_1 = require("@js-draw/math");
const AbstractRenderer_1 = __importDefault(require("./AbstractRenderer"));
const RenderablePathSpec_1 = require("../RenderablePathSpec");
/**
* Renders onto a `CanvasRenderingContext2D`.
*
* **Example**:
* [[include:doc-pages/inline-examples/canvas-renderer.md]]
*/
class CanvasRenderer extends AbstractRenderer_1.default {
/**
* Creates a new `CanvasRenderer` that renders to the given rendering context.
* The `viewport` is used to determine the translation/rotation/scaling of the content
* to draw.
*/
constructor(ctx, viewport) {
super(viewport);
this.ctx = ctx;
this.ignoreObjectsAboveLevel = null;
this.ignoringObject = false;
this.currentObjectBBox = null;
this.clipLevels = [];
this.setDraftMode(false);
}
transformBy(transformBy) {
// From MDN, transform(a,b,c,d,e,f)
// takes input such that
// ⎡ a c e ⎤
// ⎢ b d f ⎥ transforms content drawn to [ctx].
// ⎣ 0 0 1 ⎦
this.ctx.transform(transformBy.a1, transformBy.b1, // a, b
transformBy.a2, transformBy.b2, // c, d
transformBy.a3, transformBy.b3);
}
canRenderFromWithoutDataLoss(other) {
return other instanceof CanvasRenderer;
}
renderFromOtherOfSameType(transformBy, other) {
if (!(other instanceof CanvasRenderer)) {
throw new Error(`${other} cannot be rendered onto ${this}`);
}
transformBy = this.getCanvasToScreenTransform().rightMul(transformBy);
this.ctx.save();
this.transformBy(transformBy);
this.ctx.drawImage(other.ctx.canvas, 0, 0);
this.ctx.restore();
}
// Set parameters for lower/higher quality rendering
setDraftMode(draftMode) {
if (draftMode) {
this.minSquareCurveApproxDist = 9;
this.minRenderSizeBothDimens = 1;
this.minRenderSizeAnyDimen = 0.1;
}
else {
this.minSquareCurveApproxDist = 0.5;
this.minRenderSizeBothDimens = 0.1;
this.minRenderSizeAnyDimen = 1e-6;
}
}
displaySize() {
return math_1.Vec2.of(this.ctx.canvas.clientWidth, this.ctx.canvas.clientHeight);
}
clear() {
this.ctx.save();
this.ctx.resetTransform();
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
this.ctx.restore();
}
beginPath(startPoint) {
startPoint = this.canvasToScreen(startPoint);
this.ctx.beginPath();
this.ctx.moveTo(startPoint.x, startPoint.y);
}
endPath(style) {
// Saving and restoring can be slow in some browsers
// (e.g. 0.50ms). Avoid.
//this.ctx.save();
// If not a transparent fill
if (style.fill.a > 0) {
this.ctx.fillStyle = style.fill.toHexString();
this.ctx.fill();
}
if (style.stroke) {
this.ctx.strokeStyle = style.stroke.color.toHexString();
this.ctx.lineWidth = this.getSizeOfCanvasPixelOnScreen() * style.stroke.width;
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.ctx.stroke();
this.ctx.lineWidth = 1;
}
this.ctx.closePath();
//this.ctx.restore();
}
lineTo(point) {
point = this.canvasToScreen(point);
this.ctx.lineTo(point.x, point.y);
}
moveTo(point) {
point = this.canvasToScreen(point);
this.ctx.moveTo(point.x, point.y);
}
traceCubicBezierCurve(p1, p2, p3) {
p1 = this.canvasToScreen(p1);
p2 = this.canvasToScreen(p2);
p3 = this.canvasToScreen(p3);
// Approximate the curve if small enough.
const delta1 = p2.minus(p1);
const delta2 = p3.minus(p2);
if (delta1.magnitudeSquared() < this.minSquareCurveApproxDist &&
delta2.magnitudeSquared() < this.minSquareCurveApproxDist) {
this.ctx.lineTo(p3.x, p3.y);
}
else {
this.ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
}
}
traceQuadraticBezierCurve(controlPoint, endPoint) {
controlPoint = this.canvasToScreen(controlPoint);
endPoint = this.canvasToScreen(endPoint);
// Approximate the curve with a line if small enough
const delta = controlPoint.minus(endPoint);
if (delta.magnitudeSquared() < this.minSquareCurveApproxDist) {
this.ctx.lineTo(endPoint.x, endPoint.y);
}
else {
this.ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, endPoint.x, endPoint.y);
}
}
drawPath(path) {
if (this.ignoringObject) {
return;
}
// If part of a huge object, it might be worth trimming the path
const visibleRect = this.getVisibleRect();
if (this.currentObjectBBox?.containsRect(visibleRect)) {
// Try to trim/remove parts of the path outside of the bounding box.
path = (0, RenderablePathSpec_1.visualEquivalent)(path, visibleRect);
}
super.drawPath(path);
}
drawText(text, transform, style) {
this.ctx.save();
transform = this.getCanvasToScreenTransform().rightMul(transform);
this.transformBy(transform);
TextComponent_1.default.applyTextStyles(this.ctx, style);
if (style.renderingStyle.fill.a !== 0) {
this.ctx.fillStyle = style.renderingStyle.fill.toHexString();
this.ctx.fillText(text, 0, 0);
}
if (style.renderingStyle.stroke) {
this.ctx.strokeStyle = style.renderingStyle.stroke.color.toHexString();
this.ctx.lineWidth = style.renderingStyle.stroke.width;
this.ctx.strokeText(text, 0, 0);
}
this.ctx.restore();
}
drawImage(image) {
// .drawImage can fail for zero-size images.
if (image.image.width === 0 || image.image.height === 0) {
return;
}
this.ctx.save();
const transform = this.getCanvasToScreenTransform().rightMul(image.transform);
this.transformBy(transform);
this.ctx.drawImage(image.image, 0, 0);
this.ctx.restore();
}
startObject(boundingBox, clip) {
if (this.isTooSmallToRender(boundingBox)) {
this.ignoreObjectsAboveLevel = this.getNestingLevel();
this.ignoringObject = true;
}
super.startObject(boundingBox);
this.currentObjectBBox = boundingBox;
if (!this.ignoringObject && clip) {
// Don't clip if it would only remove content already trimmed by
// the edge of the screen.
const clippedIsOutsideScreen = boundingBox.containsRect(this.getVisibleRect());
if (!clippedIsOutsideScreen) {
this.clipLevels.push(this.objectLevel);
this.ctx.save();
this.ctx.beginPath();
for (const corner of boundingBox.corners) {
const screenCorner = this.canvasToScreen(corner);
this.ctx.lineTo(screenCorner.x, screenCorner.y);
}
this.ctx.clip();
}
}
}
endObject() {
// Cache this.objectLevel — it may be decremented by super.endObject.
const objectLevel = this.objectLevel;
this.currentObjectBBox = null;
super.endObject();
if (!this.ignoringObject && this.clipLevels.length > 0) {
if (this.clipLevels[this.clipLevels.length - 1] === objectLevel) {
this.ctx.restore();
this.clipLevels.pop();
}
}
// If exiting an object with a too-small-to-draw bounding box,
if (this.ignoreObjectsAboveLevel !== null &&
this.getNestingLevel() <= this.ignoreObjectsAboveLevel) {
this.ignoreObjectsAboveLevel = null;
this.ignoringObject = false;
}
}
/**
* Returns a reference to the underlying `CanvasRenderingContext2D`.
* This can be used to render custom content not supported by {@link AbstractRenderer}.
* However, such content won't support {@link SVGRenderer} or {@link TextOnlyRenderer}
* by default.
*
* Use with caution.
*/
drawWithRawRenderingContext(callback) {
this.ctx.save();
this.transformBy(this.getCanvasToScreenTransform());
callback(this.ctx);
this.ctx.restore();
}
// @internal
drawPoints(...points) {
const pointRadius = 10;
for (let i = 0; i < points.length; i++) {
const point = this.canvasToScreen(points[i]);
this.ctx.beginPath();
this.ctx.arc(point.x, point.y, pointRadius, 0, Math.PI * 2);
this.ctx.fillStyle = math_1.Color4.ofRGBA(0.5 + Math.sin(i) / 2, 1.0, 0.5 + Math.cos(i * 0.2) / 4, 0.5).toHexString();
this.ctx.lineWidth = 2;
this.ctx.fill();
this.ctx.stroke();
this.ctx.closePath();
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
this.ctx.fillStyle = 'black';
this.ctx.fillText(`${i}`, point.x, point.y, pointRadius * 2);
}
}
// @internal
isTooSmallToRender(rect) {
// Should we ignore all objects within this object's bbox?
const diagonal = rect.size.times(this.getSizeOfCanvasPixelOnScreen());
const bothDimenMinSize = this.minRenderSizeBothDimens;
const bothTooSmall = Math.abs(diagonal.x) < bothDimenMinSize && Math.abs(diagonal.y) < bothDimenMinSize;
const anyDimenMinSize = this.minRenderSizeAnyDimen;
const anyTooSmall = Math.abs(diagonal.x) < anyDimenMinSize || Math.abs(diagonal.y) < anyDimenMinSize;
return bothTooSmall || anyTooSmall;
}
// @internal
static fromViewport(exportViewport, options = {}) {
const canvas = document.createElement('canvas');
const exportRectSize = exportViewport.getScreenRectSize();
let canvasSize = options.canvasSize ?? exportRectSize;
if (options.maxCanvasDimen && canvasSize.maximumEntryMagnitude() > options.maxCanvasDimen) {
canvasSize = canvasSize.times(options.maxCanvasDimen / canvasSize.maximumEntryMagnitude());
}
canvas.width = canvasSize.x;
canvas.height = canvasSize.y;
const ctx = canvas.getContext('2d');
// Scale to ensure that the entire output is visible.
const scaleFactor = Math.min(canvasSize.x / exportRectSize.x, canvasSize.y / exportRectSize.y);
ctx.scale(scaleFactor, scaleFactor);
return { renderer: new CanvasRenderer(ctx, exportViewport), element: canvas };
}
}
exports.default = CanvasRenderer;