UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

1,277 lines 48.6 kB
/** * Based on https://github.com/gliffy/canvas2svg */ import { AffineTransform } from "./affine"; import { isString, isNumber } from "./types"; import { css4_parse, transparent } from "./color"; import { typed_entries } from "./object"; import { random } from "./random"; import { float32_epsilon } from "./math"; import { empty } from "../dom"; // helper function to map named to numbered entities function createNamedToNumberedLookup(input, radix) { const lookup = new Map(); const items = input.split(","); radix = radix ?? 10; // Map from named to numbered entities. for (let i = 0; i < items.length; i += 2) { const entity = `&${items[i + 1]};`; const base10 = parseInt(items[i], radix); lookup.set(entity, `&#${base10};`); } // FF and IE need to create a regex from hex values ie &nbsp; == \xa0 lookup.set("\\xa0", "&#160;"); return lookup; } // helper function to map canvas-textAlign to svg-textAnchor function getTextAnchor(textAlign) { // TODO: support rtl languages const mapping = { left: "start", right: "end", center: "middle", start: "start", end: "end" }; return textAlign in mapping ? mapping[textAlign] : mapping.start; } // helper function to map canvas-textBaseline to svg-dominantBaseline function getDominantBaseline(textBaseline) { // INFO: not supported in all browsers const mapping = { alphabetic: "alphabetic", hanging: "hanging", top: "text-before-edge", bottom: "text-after-edge", middle: "central" }; return textBaseline in mapping ? mapping[textBaseline] : mapping.alphabetic; } // Unpack entities lookup where the numbers are in radix 32 to reduce the size // entity mapping courtesy of tinymce const namedEntities = createNamedToNumberedLookup("50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy," + "5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute," + "5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34," + "5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil," + "68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde," + "6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute," + "6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml," + "75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc," + "7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash," + "7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta," + "sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu," + "st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi," + "t9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota," + "tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau," + "u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip," + "81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym," + "8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr," + "8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod," + "8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup," + "8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4," + "nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob," + "rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0," + "Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm," + "80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger," + "811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro", 32); // Some basic mappings for attributes and default values. const STYLES = { strokeStyle: { svgAttr: "stroke", // corresponding svg attribute canvas: "#000000", // canvas default svg: "none", // svg default apply: "stroke", // apply on stroke() or fill() }, fillStyle: { svgAttr: "fill", canvas: "#000000", svg: null, // svg default is black, but we need to special case this to handle canvas stroke without fill apply: "fill", }, lineCap: { svgAttr: "stroke-linecap", canvas: "butt", svg: "butt", apply: "stroke", }, lineJoin: { svgAttr: "stroke-linejoin", canvas: "miter", svg: "miter", apply: "stroke", }, miterLimit: { svgAttr: "stroke-miterlimit", canvas: 10, svg: 4, apply: "stroke", }, lineWidth: { svgAttr: "stroke-width", canvas: 1, svg: 1, apply: "stroke", }, globalAlpha: { svgAttr: "opacity", canvas: 1, svg: 1, apply: "fill stroke", }, shadowColor: { canvas: "#000000", }, shadowOffsetX: { canvas: 0, }, shadowOffsetY: { canvas: 0, }, shadowBlur: { canvas: 0, }, lineDash: { svgAttr: "stroke-dasharray", canvas: [], svg: null, apply: "stroke", }, lineDashOffset: { svgAttr: "stroke-dashoffset", canvas: 0, svg: 0, apply: "stroke", }, direction: { canvas: "inherit", }, font: { // font converts to multiple svg attributes, there is custom logic for this canvas: "10px sans-serif", }, fontKerning: { canvas: "auto", }, fontStretch: { canvas: "normal", }, fontVariantCaps: { canvas: "normal", }, letterSpacing: { canvas: "0px", }, textAlign: { canvas: "start", }, textBaseline: { canvas: "alphabetic", }, textRendering: { canvas: "auto", }, wordSpacing: { canvas: "0px", }, }; function is_VideoFrame(image) { return typeof VideoFrame !== "undefined" && image instanceof VideoFrame; } function width_height(image) { if (is_VideoFrame(image)) { return [image.codedWidth, image.codedHeight]; // TODO not sure if this makes any sense } else { function to_number(val) { return isNumber(val) ? val : val.baseVal.value; } const { width, height } = image; return [to_number(width), to_number(height)]; } } class CanvasGradient { static __name__ = "CanvasGradient"; __root; __ctx; constructor(gradientNode, ctx) { this.__root = gradientNode; this.__ctx = ctx; } /** * Adds a color stop to the gradient root */ addColorStop(offset, color) { if (this.__root.nodeName === "linearGradient" && this.__root.getAttribute("x1") === this.__root.getAttribute("x2") && this.__root.getAttribute("y1") === this.__root.getAttribute("y2")) { return; } if (this.__root.nodeName === "radialGradient" && this.__root.getAttribute("cx") === this.__root.getAttribute("fx") && this.__root.getAttribute("cy") === this.__root.getAttribute("fy") && this.__root.getAttribute("r") === this.__root.getAttribute("r0")) { return; } const stop = this.__ctx.__createElement("stop"); stop.setAttribute("offset", `${offset}`); if (color.indexOf("rgba") !== -1) { // separate alpha value, since webkit can't handle it const regex = /rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d?\.?\d*)\s*\)/gi; const matches = regex.exec(color); const [, r, g, b, a] = matches; stop.setAttribute("stop-color", `rgb(${r},${g},${b})`); stop.setAttribute("stop-opacity", a); } else { stop.setAttribute("stop-color", color); } this.__root.appendChild(stop); } } class CanvasPattern { static __name__ = "CanvasPattern"; __root; __ctx; constructor(pattern, ctx) { this.__root = pattern; this.__ctx = ctx; } setTransform(_transform) { throw new Error("not implemented"); } } export class SVGRenderingContext2D { static __name__ = "SVGRenderingContext2D"; __canvas; __ctx; __root; __ids; __defs; __stack; __document; __currentElement; __currentDefaultPath = ""; __currentPosition = null; //__currentElementsToStyle: {element: SVGElement, children: SVGElement[]} | null = null static __random = random; get canvas() { // XXX: point back to this instance return this; } strokeStyle; fillStyle; lineCap; lineJoin; miterLimit; lineWidth; globalAlpha = 1.0; globalCompositeOperation; // TODO: implement shadowColor; shadowOffsetX; shadowOffsetY; shadowBlur; lineDash; lineDashOffset; filter; // TODO: implement imageSmoothingEnabled; // TODO: implement imageSmoothingQuality; // TODO: implement direction; // TODO font; fontKerning; // TODO fontStretch; // TODO fontVariantCaps; // TODO letterSpacing; // TODO textAlign; textBaseline; textRendering; // TODO wordSpacing; // TODO _width; _height; get width() { return this._width; } set width(width) { this._width = width; this.__root.setAttribute("width", `${width}`); } get height() { return this._height; } set height(height) { this._height = height; this.__root.setAttribute("height", `${height}`); } _transform = new AffineTransform(); constructor(options) { this.__document = options?.document ?? document; // allow passing in an existing context to wrap around // if a context is passed in, we know a canvas already exist if (options?.ctx != null) { this.__ctx = options.ctx; } else { this.__canvas = this.__document.createElement("canvas"); this.__ctx = this.__canvas.getContext("2d"); } this.reset(); // the root svg element this.__root = this.__document.createElementNS("http://www.w3.org/2000/svg", "svg"); this.__root.setAttribute("version", "1.1"); this.__root.setAttribute("xmlns", "http://www.w3.org/2000/svg"); this.__currentElement = this.__root; this.width = options?.width ?? 500; this.height = options?.height ?? 500; // make sure we don't generate the same ids in defs this.__ids = new Set(); // defs tag this.__defs = this.__document.createElementNS("http://www.w3.org/2000/svg", "defs"); this.__root.appendChild(this.__defs); } // helper function that generates a random string _random_string() { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz"; let str; do { str = SVGRenderingContext2D.__random.choices(12, chars).join(""); } while (this.__ids.has(str)); return str; } /** * Creates the specified svg element */ __createElement(elementName, properties = {}, resetFill = false) { const element = this.__document.createElementNS("http://www.w3.org/2000/svg", elementName); if (resetFill) { // if fill or stroke is not specified, the svg element should not display. By default SVG's fill is black. element.setAttribute("fill", "none"); element.setAttribute("stroke", "none"); } const keys = Object.keys(properties); for (const key of keys) { element.setAttribute(key, `${properties[key]}`); } return element; } /** * Applies default canvas styles to the context */ __setDefaultStyles() { // default 2d canvas context properties see:http://www.w3.org/TR/2dcontext/ for (const [key, val] of typed_entries(STYLES)) { this[key] = val.canvas; } } /** * Applies styles on restore */ __applyStyleState(style_state) { for (const [key, val] of typed_entries(style_state)) { this[key] = val; } } /** * Gets the current style state */ __getStyleState() { const style_state = {}; for (const [key, _val] of typed_entries(STYLES)) { style_state[key] = this[key]; } return style_state; } /** * Apples the current styles to the current SVG element. On "ctx.fill" or "ctx.stroke" */ __applyStyleToCurrentElement(type) { const currentElement = this.__currentElement; /* const currentStyleGroup = this.__currentElementsToStyle if (currentStyleGroup != null) { currentElement.setAttribute(type, "") currentElement = currentStyleGroup.element for (const node of currentStyleGroup.children) { node.setAttribute(type, "") } } */ for (const [key, style] of typed_entries(STYLES)) { const value = this[key]; if (style.apply != null && style.apply.includes(type)) { if (value instanceof CanvasPattern) { for (const def of [...value.__ctx.__defs.childNodes]) { if (def instanceof Element) { const id = def.getAttribute("id"); this.__ids.add(id); this.__defs.appendChild(def); } } const id = value.__root.getAttribute("id"); currentElement.setAttribute(style.apply, `url(#${id})`); } else if (value instanceof CanvasGradient) { const id = value.__root.getAttribute("id"); currentElement.setAttribute(style.apply, `url(#${id})`); } else if (style.svg !== value) { if ((style.svgAttr === "stroke" || style.svgAttr === "fill") && isString(value) && value.indexOf("rgb") !== -1) { // convert CSS4 -> CSS3 and separate alpha value, since illustrator can't handle it const [r, g, b, a] = css4_parse(value) ?? transparent(); const opacity = a * this.globalAlpha; currentElement.setAttribute(style.svgAttr, `rgb(${r},${g},${b})`); currentElement.setAttribute(`${style.svgAttr}-opacity`, `${opacity}`); } else { let attr = style.svgAttr; if (key === "globalAlpha") { attr = `${type}-${style.svgAttr}`; if (currentElement.getAttribute(attr) != null) { // fill-opacity or stroke-opacity has already been set by stroke or fill. continue; } } // otherwise only update attribute if right type, and not svg default currentElement.setAttribute(attr, `${value}`); } } } } } /** * Returns the serialized value of the svg so far * @param fixNamedEntities - Standalone SVG doesn't support named entities, which document.createTextNode encodes. * If true, we attempt to find all named entities and encode it as a numeric entity. * @return serialized svg */ get_serialized_svg(fixNamedEntities = false) { let serialized = new XMLSerializer().serializeToString(this.__root); if (fixNamedEntities) { // loop over each named entity and replace with the proper equivalent. for (const [key, value] of namedEntities) { const regexp = new RegExp(key, "gi"); if (regexp.test(serialized)) { serialized = serialized.replace(regexp, value); } } } return serialized; } get_svg() { return this.__root; } /** * Will generate a group tag. */ save() { this.__stack.push({ transform: this._transform, clip_path: this._clip_path, attributes: this.__getStyleState(), }); this._transform = this._transform.clone(); } /** * Sets current element to parent, or just root if already root */ restore() { if (this.__stack.length == 0) { return; } const { transform, clip_path, attributes } = this.__stack.pop(); this._transform = transform; this._clip_path = clip_path; this.__applyStyleState(attributes); } isContextLost() { return false; } reset() { this.__setDefaultStyles(); this.__stack = []; } _apply_transform(element, transform = this._transform) { if (!transform.is_identity) { element.setAttribute("transform", transform.toString()); } } _apply_global_alpha(element) { if (this.globalAlpha != 1.0) { element.setAttribute("opacity", `${this.globalAlpha}`); } } _apply_clip_path(element) { if (this._clip_path != null) { element.setAttribute("clip-path", this._clip_path); } } /** * scales the current element */ scale(x, y) { if (!isFinite(x) || (y != null && !isFinite(y))) { return; } this._transform.scale(x, y ?? x); } /** * rotates the current element */ rotate(angle) { if (!isFinite(angle)) { return; } this._transform.rotate(angle); } /** * translates the current element */ translate(x, y) { if (!isFinite(x + y)) { return; } this._transform.translate(x, y); } /** * applies a transform to the current element */ transform(a, b, c, d, e, f) { if (!isFinite(a + b + c + d + e + f)) { return; } this._transform.transform(a, b, c, d, e, f); } /** * Create a new Path Element */ beginPath() { // Note that there is only one current default path, it is not part of the drawing state. // See also: https://html.spec.whatwg.org/multipage/scripting.html#current-default-path this.__currentDefaultPath = ""; this.__currentPosition = null; this.__init_element(); } __init_element() { const path = this.__createElement("path", {}, true); this.__root.appendChild(path); this.__currentElement = path; } /** * Helper function to apply currentDefaultPath to current path element */ __applyCurrentDefaultPath() { const currentElement = this.__currentElement; if (currentElement.nodeName === "path") { currentElement.setAttribute("d", this.__currentDefaultPath); } else { console.error("Attempted to apply path command to node", currentElement.nodeName); } } /** * Helper function to add path command */ __addPathCommand(x, y, path) { const separator = this.__currentDefaultPath == "" ? "" : " "; this.__currentDefaultPath += separator + path; this.__currentPosition = { x, y }; } get _hasCurrentDefaultPath() { return this.__currentDefaultPath != ""; } /** * Adds the move command to the current path element, * if the currentPathElement is not empty create a new path element */ moveTo(x, y) { if (!isFinite(x + y)) { return; } const currentElement = this.__currentElement; if (currentElement.nodeName !== "path") { this.beginPath(); } // creates a new subpath with the given point const [tx, ty] = this._transform.apply(x, y); this.__addPathCommand(tx, ty, `M ${tx} ${ty}`); } /** * Closes the current path */ closePath() { if (this._hasCurrentDefaultPath) { this.__addPathCommand(NaN, NaN, "Z"); } } /** * Adds a line to command */ lineTo(x, y) { if (!isFinite(x + y)) { return; } if (!this._hasCurrentDefaultPath) { this.moveTo(x, y); } else { const [tx, ty] = this._transform.apply(x, y); this.__addPathCommand(tx, ty, `L ${tx} ${ty}`); } } /** * Add a bezier command */ bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) { if (!isFinite(cp1x + cp1y + cp2x + cp2y + x + y)) { return; } const [tx, ty] = this._transform.apply(x, y); const [tcp1x, tcp1y] = this._transform.apply(cp1x, cp1y); const [tcp2x, tcp2y] = this._transform.apply(cp2x, cp2y); this.__addPathCommand(tx, ty, `C ${tcp1x} ${tcp1y} ${tcp2x} ${tcp2y} ${tx} ${ty}`); } /** * Adds a quadratic curve to command */ quadraticCurveTo(cpx, cpy, x, y) { if (!isFinite(cpx + cpy + x + y)) { return; } const [tx, ty] = this._transform.apply(x, y); const [tcpx, tcpy] = this._transform.apply(cpx, cpy); this.__addPathCommand(tx, ty, `Q ${tcpx} ${tcpy} ${tx} ${ty}`); } /** * Adds the arcTo to the current path * * @see http://www.w3.org/TR/2015/WD-2dcontext-20150514/#dom-context-2d-arcto */ arcTo(x1, y1, x2, y2, radius) { if (!isFinite(x1 + y1 + x2 + y2 + radius)) { return; } // Let the point (x0, y0) be the last point in the subpath. if (this.__currentPosition == null) { return; } // Issue #12699. // this.__currentPosition has already been transformed using this._transform. // Arguments to this function have not been transformed, so to compare the two need // to inverse transform this.__currentPosition. const inverse = this._transform.inverse(); const [x0, y0] = inverse.apply(this.__currentPosition.x, this.__currentPosition.y); // Negative values for radius must cause the implementation to throw an IndexSizeError exception. if (radius < 0) { throw new Error(`IndexSizeError: The radius provided (${radius}) is negative.`); } // If the point (x0, y0) is equal to the point (x1, y1), // or if the point (x1, y1) is equal to the point (x2, y2), // or if the radius radius is zero, // then the method must add the point (x1, y1) to the subpath, // and connect that point to the previous point (x0, y0) by a straight line. if (((x0 === x1) && (y0 === y1)) || ((x1 === x2) && (y1 === y2)) || (radius === 0)) { this.lineTo(x1, y1); return; } function normalize([x, y]) { const len = Math.sqrt(x ** 2 + y ** 2); return [x / len, y / len]; } // Otherwise, if the points (x0, y0), (x1, y1), and (x2, y2) all lie on a single straight line, // then the method must add the point (x1, y1) to the subpath, // and connect that point to the previous point (x0, y0) by a straight line. const unit_vec_p1_p0 = normalize([x0 - x1, y0 - y1]); const unit_vec_p1_p2 = normalize([x2 - x1, y2 - y1]); if (unit_vec_p1_p0[0] * unit_vec_p1_p2[1] === unit_vec_p1_p0[1] * unit_vec_p1_p2[0]) { this.lineTo(x1, y1); return; } // Otherwise, let The Arc be the shortest arc given by circumference of the circle that has radius radius, // and that has one point tangent to the half-infinite line that crosses the point (x0, y0) and ends at the point (x1, y1), // and that has a different point tangent to the half-infinite line that ends at the point (x1, y1), and crosses the point (x2, y2). // The points at which this circle touches these two lines are called the start and end tangent points respectively. // note that both vectors are unit vectors, so the length is 1 const cos = (unit_vec_p1_p0[0] * unit_vec_p1_p2[0] + unit_vec_p1_p0[1] * unit_vec_p1_p2[1]); const theta = Math.acos(Math.abs(cos)); // Calculate origin const unit_vec_p1_origin = normalize([ unit_vec_p1_p0[0] + unit_vec_p1_p2[0], unit_vec_p1_p0[1] + unit_vec_p1_p2[1], ]); const len_p1_origin = radius / Math.sin(theta / 2); const x = x1 + len_p1_origin * unit_vec_p1_origin[0]; const y = y1 + len_p1_origin * unit_vec_p1_origin[1]; // Calculate start angle and end angle // rotate 90deg clockwise (note that y axis points to its down) const unit_vec_origin_start_tangent = [ -unit_vec_p1_p0[1], unit_vec_p1_p0[0], ]; // rotate 90deg counter clockwise (note that y axis points to its down) const unit_vec_origin_end_tangent = [ unit_vec_p1_p2[1], -unit_vec_p1_p2[0], ]; function getAngle(vector) { // get angle (clockwise) between vector and (1, 0) const x = vector[0]; const y = vector[1]; if (y >= 0) { // note that y axis points to its down return Math.acos(x); } else { return -Math.acos(x); } } const startAngle = getAngle(unit_vec_origin_start_tangent); const endAngle = getAngle(unit_vec_origin_end_tangent); // Connect the point (x0, y0) to the start tangent point by a straight line this.lineTo(x + unit_vec_origin_start_tangent[0] * radius, y + unit_vec_origin_start_tangent[1] * radius); // Connect the start tangent point to the end tangent point by arc // and adding the end tangent point to the subpath. this.arc(x, y, radius, startAngle, endAngle); } /** * Sets the stroke property on the current element */ stroke() { if (this.__currentElement.nodeName === "path") { this.__currentElement.setAttribute("paint-order", "fill"); } this.__applyCurrentDefaultPath(); this.__applyStyleToCurrentElement("stroke"); if (this._clip_path != null) { this.__currentElement.setAttribute("clip-path", this._clip_path); } } fill(path_or_fill_rule, fill_rule) { let path = null; if (path_or_fill_rule instanceof Path2D) { path = path_or_fill_rule; } else if (fill_rule == null) { fill_rule = path_or_fill_rule; } else { throw new Error("invalid arguments"); } if (path != null) { throw new Error("not implemented"); } // XXX: hack (?) to allow fill and hatch visuals on same canvas path if (this.__currentElement.getAttribute("fill") != "none") { this.__init_element(); } if (this.__currentElement.nodeName === "path") { this.__currentElement.setAttribute("paint-order", "stroke"); } this.__applyCurrentDefaultPath(); this.__applyStyleToCurrentElement("fill"); if (fill_rule != null && fill_rule != "nonzero") { // don't apply the default this.__currentElement.setAttribute("fill-rule", fill_rule); } if (this._clip_path != null) { this.__currentElement.setAttribute("clip-path", this._clip_path); } } /** * Adds a rectangle to the path. */ rect(x, y, width, height) { if (!isFinite(x + y + width + height)) { return; } this.moveTo(x, y); this.lineTo(x + width, y); this.lineTo(x + width, y + height); this.lineTo(x, y + height); this.lineTo(x, y); this.closePath(); } /** * adds a rectangle element */ fillRect(x, y, width, height) { if (!isFinite(x + y + width + height)) { return; } this.beginPath(); this.rect(x, y, width, height); this.fill(); } /** * Draws a rectangle with no fill * @param x * @param y * @param width * @param height */ strokeRect(x, y, width, height) { if (!isFinite(x + y + width + height)) { return; } this.beginPath(); this.rect(x, y, width, height); this.stroke(); } /** * Clear entire canvas: * 1. save current transforms * 2. remove all the childNodes of the root g element */ __clearCanvas() { empty(this.__defs); empty(this.__root); this.__root.appendChild(this.__defs); this.__currentElement = this.__root; } /** * "Clears" a canvas by just drawing a white rectangle in the current group. */ clearRect(x, y, width, height) { if (!isFinite(x + y + width + height)) { return; } if (x === 0 && y === 0 && width === this.width && height === this.height) { this.__clearCanvas(); return; } const rect = this.__createElement("rect", { x, y, width, height, fill: "#FFFFFF" }, true); this._apply_transform(rect); this.__root.appendChild(rect); } roundRect(_x, _y, _w, _h, _radii) { throw new Error("not implemented"); } /** * Adds a linear gradient to a defs tag. * Returns a canvas gradient object that has a reference to it's parent def */ createLinearGradient(x1, y1, x2, y2) { if (!isFinite(x1 + y1 + x2 + y2)) { throw new Error("The provided double value is non-finite"); } const [tx1, ty1] = this._transform.apply(x1, y1); const [tx2, ty2] = this._transform.apply(x2, y2); const grad = this.__createElement("linearGradient", { id: this._random_string(), x1: `${tx1}px`, x2: `${tx2}px`, y1: `${ty1}px`, y2: `${ty2}px`, gradientUnits: "userSpaceOnUse", }, false); this.__defs.appendChild(grad); return new CanvasGradient(grad, this); } /** * Adds a radial gradient to a defs tag. * Returns a canvas gradient object that has a reference to it's parent def */ createRadialGradient(x0, y0, r0, x1, y1, r1) { if (!isFinite(x0 + y0 + r0 + x1 + y1 + r1)) { throw new Error("The provided double value is non-finite"); } const [tx0, ty0] = this._transform.apply(x0, y0); const [tx1, ty1] = this._transform.apply(x1, y1); const grad = this.__createElement("radialGradient", { id: this._random_string(), cx: `${tx1}px`, cy: `${ty1}px`, r: `${r1}px`, r0: `${r0}px`, fx: `${tx0}px`, fy: `${ty0}px`, gradientUnits: "userSpaceOnUse", }, false); this.__defs.appendChild(grad); return new CanvasGradient(grad, this); } createConicGradient(_start_angle, _x, _y) { throw Error("not implemented"); } /** * Parses the font string and returns svg mapping */ __parseFont() { const regex = /^\s*(?=(?:(?:[-a-z]+\s*){0,2}(italic|oblique))?)(?=(?:(?:[-a-z]+\s*){0,2}(small-caps))?)(?=(?:(?:[-a-z]+\s*){0,2}(bold(?:er)?|lighter|[1-9]00))?)(?:(?:normal|\1|\2|\3)\s*){0,3}((?:xx?-)?(?:small|large)|medium|smaller|larger|[.\d]+(?:\%|in|[cem]m|ex|p[ctx]))(?:\s*\/\s*(normal|[.\d]+(?:\%|in|[cem]m|ex|p[ctx])))?\s*([-,\'\"\sa-z0-9]+?)\s*$/i; const [, style, decoration, weight, size, , family] = regex.exec(this.font); // XXX: RegExpExecArray is incorrecdt const data = { style: style ?? "normal", size: size ?? "10px", family: family ?? "sans-serif", weight: weight ?? "normal", decoration: decoration ?? "normal", }; return data; } /** * Fills or strokes text */ __applyText(text, x, y, action) { const font = this.__parseFont(); const text_el = this.__createElement("text", { "font-family": font.family, "font-size": font.size, "font-style": font.style, "font-weight": font.weight, "text-decoration": font.decoration, x, y, "text-anchor": getTextAnchor(this.textAlign), "dominant-baseline": getDominantBaseline(this.textBaseline), }, true); text_el.appendChild(this.__document.createTextNode(text)); this._apply_transform(text_el); this.__currentElement = text_el; this.__applyStyleToCurrentElement(action); const el = (() => { if (this._clip_path != null) { const g = this.__createElement("g"); g.setAttribute("clip-path", this._clip_path); g.appendChild(text_el); return g; } else { return text_el; } })(); this.__root.appendChild(el); } /** * Creates a text element, in position x,y */ fillText(text, x, y) { if (!isFinite(x + y)) { return; } this.__applyText(text, x, y, "fill"); } /** * Strokes text */ strokeText(text, x, y) { if (!isFinite(x + y)) { return; } this.__applyText(text, x, y, "stroke"); } /** * No need to implement this for svg. */ measureText(text) { this.__ctx.font = this.font; return this.__ctx.measureText(text); } arc(x, y, radius, start_angle, end_angle, counterclockwise = false) { this.ellipse(x, y, radius, radius, 0, start_angle, end_angle, counterclockwise); } ellipse(x, y, radius_x, radius_y, rotation, start_angle, end_angle, counterclockwise = false) { if (!isFinite(x + y + radius_x + radius_y + rotation + start_angle + end_angle)) { return; } if (radius_x < 0 || radius_y < 0) { throw new DOMException("IndexSizeError, radius can't be negative"); } const initial_diff = counterclockwise ? end_angle - start_angle : start_angle - end_angle; start_angle = start_angle % (2 * Math.PI); end_angle = end_angle % (2 * Math.PI); const transform = new AffineTransform().translate(x, y).rotate(rotation); const dx = radius_x * Math.cos(start_angle); const dy = radius_y * Math.sin(start_angle); const [start_x, start_y] = transform.apply(dx, dy); this.lineTo(start_x, start_y); // Canvas ellipse defines rotation in radians and SVG elliptical arc is defined in degrees const rotation_in_degrees = rotation * 180 / Math.PI; const sweep_flag = counterclockwise ? 0 : 1; /** * Check if need to draw full ellipse (issue #11475). When testing if angular difference is * equal to an integer multiple of 2*pi radians, need to account for float64 to float32 rounding * error on both the start and end angles, hence the factor of 2. Do not assume full circle if * start and end angles are close, but test here depends on combination of sign of angular * difference and wedge direction (clockwise/counterclockwise) to obtain the same results as on * canvas. */ if (Math.abs(start_angle - end_angle) < 2 * float32_epsilon && !(Math.abs(initial_diff) < 2 * float32_epsilon && initial_diff < 0)) { // Draw full ellipse. SVG elliptical arc cannot do this, so instead use two semi ellipses. const [tstart_x, tstart_y] = this._transform.apply(start_x, start_y); const dx = radius_x * Math.cos(start_angle + Math.PI); const dy = radius_y * Math.sin(start_angle + Math.PI); const [mid_x, mid_y] = transform.apply(dx, dy); const [tmid_x, tmid_y] = this._transform.apply(mid_x, mid_y); this.__addPathCommand(tstart_x, tstart_y, `A ${radius_x} ${radius_y} ${rotation_in_degrees} 0 ${sweep_flag} ${tmid_x} ${tmid_y} A ${radius_x} ${radius_y} ${rotation_in_degrees} 0 ${sweep_flag} ${tstart_x} ${tstart_y}`); } else { // Draw partial ellipse only. const dx = radius_x * Math.cos(end_angle); const dy = radius_y * Math.sin(end_angle); const [end_x, end_y] = transform.apply(dx, dy); let diff = end_angle - start_angle; // https://github.com/gliffy/canvas2svg/issues/4 if (diff < 0) { diff += 2 * Math.PI; } const large_arc_flag = (counterclockwise !== diff > Math.PI) ? 1 : 0; // imitation xor const [tend_x, tend_y] = this._transform.apply(end_x, end_y); this.__addPathCommand(tend_x, tend_y, `A ${radius_x} ${radius_y} ${rotation_in_degrees} ${large_arc_flag} ${sweep_flag} ${tend_x} ${tend_y}`); } } _clip_path = null; clip(path_or_fill_rule, fill_rule) { let path = null; if (path_or_fill_rule instanceof Path2D) { path = path_or_fill_rule; } else if (fill_rule == null) { fill_rule = path_or_fill_rule; } else { throw new Error("invalid arguments"); } if (path != null) { throw new Error("not implemented"); } const clip_path = this.__createElement("clipPath"); const id = this._random_string(); this.__applyCurrentDefaultPath(); if (fill_rule != null) { this.__currentElement.setAttribute("clip-rule", fill_rule); } clip_path.setAttribute("id", id); clip_path.appendChild(this.__currentElement); this.__defs.appendChild(clip_path); this._clip_path = `url(#${id})`; } /** * Draws a canvas, image or mock context to this canvas. * Note that all svg dom manipulation uses node.childNodes rather than node.children for IE support. * http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-drawimage */ drawImage(image, ...args) { let dx, dy; let dw, dh; let sx, sy; let sw, sh; if (args.length == 2) { [dx, dy] = args; if (!isFinite(dx + dy)) { return; } sx = 0; sy = 0; const [w, h] = width_height(image); dw = sw = w; dh = sh = h; } else if (args.length == 4) { [dx, dy, dw, dh] = args; if (!isFinite(dx + dy + dw + dh)) { return; } sx = 0; sy = 0; const [w, h] = width_height(image); sw = w; sh = h; } else if (args.length === 8) { [sx, sy, sw, sh, dx, dy, dw, dh] = args; if (!isFinite(sx + sy + sw + sh + dx + dy + dw + dh)) { return; } } else { throw new Error(`Invalid number of arguments passed to drawImage: ${arguments.length}`); } // parent, svg, defs, group, currentElement, svgImage, canvas, context, id const parent = this.__root; const transform = this._transform.clone().translate(dx, dy); if (image instanceof SVGRenderingContext2D || image instanceof SVGSVGElement) { // In the future we may want to clone nodes instead. // also I'm currently ignoring dw, dh, sw, sh, sx, sy for a mock context. const svg_node = image instanceof SVGSVGElement ? image : image.get_svg(); const svg = svg_node.cloneNode(true); let scope; if (transform.is_identity && this.globalAlpha == 1.0 && this._clip_path == null) { scope = parent; } else { scope = this.__createElement("g"); parent.appendChild(scope); // `transform` affects `clip-path`, so if both are present, then apply // them separately in `clip-path`, `transform` order. if (this._clip_path != null) { this._apply_clip_path(scope); if (!transform.is_identity) { const outer = scope; scope = this.__createElement("g"); outer.appendChild(scope); } } this._apply_transform(scope, transform); this._apply_global_alpha(scope); } for (const child of [...svg.childNodes]) { if (child instanceof SVGDefsElement) { for (const def of [...child.childNodes]) { if (def instanceof Element) { const id = def.getAttribute("id"); this.__ids.add(id); this.__defs.appendChild(def.cloneNode(true)); } } } else { scope.appendChild(child.cloneNode(true)); } } } else if (image instanceof HTMLImageElement || image instanceof SVGImageElement) { const svgImage = this.__createElement("image"); svgImage.setAttribute("width", `${dw}`); svgImage.setAttribute("height", `${dh}`); svgImage.setAttribute("preserveAspectRatio", "none"); if (this.globalAlpha != 1.0) { svgImage.setAttribute("opacity", `${this.globalAlpha}`); } if (sx != 0 || sy != 0 || sw !== image.width || sh !== image.height) { // crop the image using a temporary canvas const canvas = this.__document.createElement("canvas"); canvas.width = dw; canvas.height = dh; const context = canvas.getContext("2d"); context.drawImage(image, sx, sy, sw, sh, 0, 0, dw, dh); image = canvas; } this._apply_transform(svgImage, transform); const url = image instanceof HTMLCanvasElement ? image.toDataURL() : image.getAttribute("src"); svgImage.setAttribute("href", url); if (this._clip_path != null) { const scope = this.__createElement("g"); scope.setAttribute("clip-path", this._clip_path); scope.appendChild(svgImage); parent.appendChild(scope); } else { parent.appendChild(svgImage); } } else if (image instanceof HTMLCanvasElement) { const svgImage = this.__createElement("image"); svgImage.setAttribute("width", `${dw}`); svgImage.setAttribute("height", `${dh}`); svgImage.setAttribute("preserveAspectRatio", "none"); if (this.globalAlpha != 1.0) { svgImage.setAttribute("opacity", `${this.globalAlpha}`); } // draw canvas onto temporary canvas so that smoothing can be handled const canvas = this.__document.createElement("canvas"); canvas.width = dw; canvas.height = dh; const context = canvas.getContext("2d"); context.imageSmoothingEnabled = false; context.drawImage(image, sx, sy, sw, sh, 0, 0, dw, dh); image = canvas; this._apply_transform(svgImage, transform); svgImage.setAttribute("href", image.toDataURL()); if (this._clip_path != null) { const scope = this.__createElement("g"); scope.setAttribute("clip-path", this._clip_path); scope.appendChild(svgImage); parent.appendChild(scope); } else { parent.appendChild(svgImage); } } } /** * Generates a pattern tag */ createPattern(image, _repetition) { const pattern = this.__document.createElementNS("http://www.w3.org/2000/svg", "pattern"); const id = this._random_string(); const [width, height] = width_height(image); pattern.setAttribute("id", id); pattern.setAttribute("width", `${width}`); pattern.setAttribute("height", `${height}`); pattern.setAttribute("patternUnits", "userSpaceOnUse"); if (image instanceof HTMLCanvasElement || image instanceof HTMLImageElement || image instanceof SVGImageElement) { const img = this.__document.createElementNS("http://www.w3.org/2000/svg", "image"); const url = image instanceof HTMLCanvasElement ? image.toDataURL() : image.getAttribute("src"); img.setAttribute("href", url); pattern.appendChild(img); this.__defs.appendChild(pattern); } else if (image instanceof SVGRenderingContext2D) { for (const child of [...image.__root.childNodes]) { if (!(child instanceof SVGDefsElement)) { pattern.appendChild(child.cloneNode(true)); } } //pattern.appendChild(image.__root.childNodes[1]) this.__defs.appendChild(pattern); } else if (image instanceof SVGSVGElement) { for (const child of [...image.childNodes]) { if (!(child instanceof SVGDefsElement)) { pattern.appendChild(child.cloneNode(true)); } } //pattern.appendChild(image.__root.childNodes[1]) this.__defs.appendChild(pattern); } else { throw new Error("unsupported"); } return new CanvasPattern(pattern, this); } getLineDash() { const { lineDash } = this; if (isString(lineDash)) { return lineDash.split(",").map((v) => parseInt(v)); } else if (lineDash == null) { return []; } else { return lineDash; } } setLineDash(segments) { if (segments.length > 0) { this.lineDash = segments.join(","); } else { this.lineDash = null; } } getTransform() { return this._transform.to_DOMMatrix(); } setTransform(...args) { let matrix; if (isNumber(args[0])) { matrix = new DOMMatrix(args); } else if (args[0] instanceof DOMMatrix) { matrix = args[0]; } else { matrix = new DOMMatrix(Object.values(args[0] == null)); } this._transform = AffineTransform.from_DOMMatrix(matrix); } resetTransform() { this._transform = new AffineTransform(); } isPointInPath(..._args) { throw new Error("not implemented"); } isPointInStroke(..._args) { throw new Error("not implemented"); } createImageData(..._args) { throw new Error("not implemented"); } getImageData(_sx, _sy, _sw, _sh) { throw new Error("not implemented"); } putImageData(..._args) { throw new Error("not implemented"); } drawFocusIfNeeded(..._args) { throw new Error("not implemented"); } scrollPathIntoView(..._args) { throw new Error("not implemented"); } } //# sourceMappingURL=svg.js.map