UNPKG

@amcharts/amcharts5

Version:
165 lines 6.67 kB
import { Pattern } from "./Pattern"; /** * A pattern that repeats an SVG path as a motif on a grid. * * Note: `rotation` rotates each motif around its own centre (the grid stays * axis-aligned), so the pattern tiles seamlessly at any angle. * * @since 5.2.33 * @see {@link https://www.amcharts.com/docs/v5/concepts/colors-gradients-and-patterns/patterns/} for more info */ export class PathPattern extends Pattern { constructor() { super(...arguments); this.canvas = document.createElement("canvas"); this.context = this.canvas.getContext("2d"); this._clearPath = false; this._bbox = { x: 0, y: 0, w: 1, h: 1 }; } _beforeChanged() { this._optimizeTileSize(); super._beforeChanged(); // Assigning canvas.width/height clears and reallocates the backing store, // so only do it when the dimensions actually change. const width = this.get("width", 100); const height = this.get("height", 100); if (this.canvas.width !== width) { this.canvas.width = width; } if (this.canvas.height !== height) { this.canvas.height = height; } if (this._clear || this.isDirty("svgPath") || this.isDirty("gap") || this.isDirty("maxWidth") || this.isDirty("maxHeight") || this.isDirty("checkered") || this.isDirty("centered")) { this._clearPath = true; } } /** * The tile is only a repeat unit (the grid repeats every cell), so shrink an * oversized `width`/`height` to a single cell (2x2 when `checkered`). Skipped * for non-`repeat` repetitions. * @ignore */ _optimizeTileSize() { const repetition = this.get("repetition"); if (repetition && repetition !== "repeat") { return; } const gap = this.get("gap", 0); const cellW = this.get("maxWidth", 10) + gap; const cellH = this.get("maxHeight", 10) + gap; if (cellW <= 0 || cellH <= 0) { return; } const n = this.get("checkered", false) ? 2 : 1; const optimalW = Math.max(1, Math.round(cellW * n)); const optimalH = Math.max(1, Math.round(cellH * n)); if (this.get("width", 100) !== optimalW) { this.setRaw("width", optimalW); } if (this.get("height", 100) !== optimalH) { this.setRaw("height", optimalH); } } _changed() { super._changed(); if (this._clearPath) { const svgPath = this.get("svgPath"); if (svgPath != null) { this._renderPattern(svgPath); } } this._clearPath = false; } _renderPattern(svgPath) { if (svgPath !== this._bboxPath) { this._bbox = this._getPathBounds(svgPath); this._bboxPath = svgPath; } const width = this.get("width", 100); const height = this.get("height", 100); const gap = this.get("gap", 0); const maxWidth = this.get("maxWidth", 10); const maxHeight = this.get("maxHeight", 10); const checkered = this.get("checkered", false); const centered = this.get("centered", true); const rotation = this.get("rotation", 0) * Math.PI / 180; const ctx = this.context; ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.clearRect(0, 0, width, height); // Background (gap colour) const fill = this.get("fill"); const fillOpacity = this.get("fillOpacity", 1); if (fill && fillOpacity > 0) { ctx.fillStyle = fill.toCSS(fillOpacity); ctx.fillRect(0, 0, width, height); } // Motif colour const color = this.get("color"); ctx.fillStyle = color ? color.toCSS(this.get("colorOpacity", 1)) : "#000000"; const path = new Path2D(svgPath); const bbox = this._bbox; // Uniform scale so the motif fits into maxWidth x maxHeight. const scale = Math.min(maxWidth / bbox.w, maxHeight / bbox.h); const bcx = bbox.x + bbox.w / 2; const bcy = bbox.y + bbox.h / 2; let cellW = maxWidth + gap; let cellH = maxHeight + gap; const cols = Math.max(1, Math.round(width / cellW)); const rows = Math.max(1, Math.round(height / cellH)); cellW = width / cols; cellH = height / rows; // One extra ring of cells so a (rotated) motif straddling the tile edge is // drawn from both sides and the tile stays seamless. for (let r = -1; r <= rows; r++) { for (let c = -1; c <= cols; c++) { if (checkered && ((r & 1) !== (c & 1))) { continue; } const cx = centered ? c * cellW + cellW / 2 : c * cellW + gap / 2 + maxWidth / 2; const cy = centered ? r * cellH + cellH / 2 : r * cellH + gap / 2 + maxHeight / 2; ctx.save(); ctx.translate(cx, cy); if (rotation !== 0) { ctx.rotate(rotation); } ctx.scale(scale, scale); ctx.translate(-bcx, -bcy); ctx.fill(path); ctx.restore(); } } this._pattern = ctx.createPattern(this.canvas, this.get("repetition", "repeat")); } /** * Native geometry bounding box of an SVG path, via a throwaway `<path>`. * @ignore */ _getPathBounds(svgPath) { const svgNS = "http://www.w3.org/2000/svg"; const svg = document.createElementNS(svgNS, "svg"); svg.style.cssText = "position:absolute;left:-10000px;top:-10000px;width:0;height:0;overflow:hidden;"; const path = document.createElementNS(svgNS, "path"); path.setAttribute("d", svgPath); svg.appendChild(path); document.body.appendChild(svg); let x = 0; let y = 0; let w = 1; let h = 1; try { const box = path.getBBox(); x = box.x; y = box.y; w = box.width || 1; h = box.height || 1; } catch (e) { // getBBox can throw for an empty/invalid path; fall back to a unit box. } document.body.removeChild(svg); return { x, y, w, h }; } } PathPattern.className = "PathPattern"; PathPattern.classNames = Pattern.classNames.concat([PathPattern.className]); //# sourceMappingURL=PathPattern.js.map