├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── context.js ├── element.js ├── image.js ├── index.js ├── karma.conf.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── test ├── css │ └── styles.css ├── index.html ├── index.js ├── pattern.png ├── rendering.test.js └── tests │ ├── arc.js │ ├── arcTo.js │ ├── arcTo2.js │ ├── arcToScaled.js │ ├── ellipse.js │ ├── ellipse2.js │ ├── emptyArc.js │ ├── fillstyle.js │ ├── globalalpha.js │ ├── gradient.js │ ├── linecap.js │ ├── linewidth.js │ ├── pattern.js │ ├── rgba.js │ ├── rotate.js │ ├── saveandrestore.js │ ├── scaledLine.js │ ├── setLineDash.js │ ├── text.js │ ├── tiger.js │ └── transform.js └── utils.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [20.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm ci 29 | - run: npm run build --if-present 30 | - run: npm test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | .vscode -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## CHANGELOG 2 | 3 | ### v2.6.0 4 | 5 | - feat: roundRect 6 | 7 | ### v2.5.0 8 | 9 | - fix: ellipse translate and rotate problem ([Chris Waters](https://github.com/k1w1)) https://github.com/zenozeng/svgcanvas/pull/19 10 | - fix: scale lineWidth ([Chris Waters](https://github.com/k1w1)) https://github.com/zenozeng/svgcanvas/pull/21 11 | 12 | ### v2.4.0 13 | 14 | - fix: rendering of arcTo when a scale is applied ([Chris Waters](https://github.com/k1w1)) [#17](https://github.com/zenozeng/svgcanvas/pull/17) 15 | - feat: Context.prototype.ellipse ([Chris Waters](https://github.com/k1w1)) [#18](https://github.com/zenozeng/svgcanvas/pull/18) 16 | 17 | ### v2.3.0 18 | 19 | - fix: update Regular Expression to handle decimal values 20 | ([Validark](https://github.com/Validark)) 21 | [#10](https://github.com/zenozeng/svgcanvas/pull/10) 22 | - feat: use browser's built-in font parser 23 | ([Validark](https://github.com/Validark)) 24 | [#11](https://github.com/zenozeng/svgcanvas/pull/11) 25 | - test: rendering test for svgcanvas 26 | [#14](https://github.com/zenozeng/svgcanvas/pull/14) 27 | 28 | ### v2.2.2 29 | 30 | - Delegate getAttribute/setAttribute ([Validark](https://github.com/Validark)) 31 | [#8](https://github.com/zenozeng/svgcanvas/pull/8) 32 | 33 | ### v2.2.1 34 | 35 | - fix(SVGCanvasElement): addEventListener 36 | 37 | ### v2.2.0 38 | 39 | - feat: Context.prototype.getImageData (experimental) for 40 | https://github.com/gliffy/canvas2svg/issues/3 and 41 | https://github.com/zenozeng/p5.js-svg/issues/203 42 | 43 | ### v2.1.0 44 | 45 | - feat: SVGCanvasElement(options) 46 | - feat: options.debug 47 | - refactor 48 | 49 | ### v2.0.7 50 | 51 | - fix typo 52 | - rollup 2.67.0 53 | 54 | ### v2.0.6 55 | 56 | - utils.toString for https://github.com/zenozeng/p5.js-svg/issues/204 57 | 58 | ### v2.0.5 59 | 60 | - Fix adding CanvasPattern ([Xavier Delamotte](https://github.com/x4d3)) 61 | [#7](https://github.com/zenozeng/svgcanvas/pull/7) 62 | 63 | ### v2.0.4 64 | 65 | - fix: push/pop transformMatrixStack when save/restore, for 66 | https://github.com/zenozeng/p5.js-svg/issues/191 67 | 68 | ### v2.0.3 69 | 70 | - feat: sync element's width and height to context 71 | 72 | ### v2.0.2 73 | 74 | - feat: Implement CanvasTransform Interface, 75 | https://github.com/gliffy/canvas2svg/pull/83 76 | - feat: ClearCanvas in fillRect 77 | - feat: Element API 78 | - feat: ESM 79 | - fix: Recreate root `` when __clearCanvas to remove all attributes 80 | - chore: Bundle JavaScript using Rollup 81 | - chore: GitHub Actions 82 | 83 | ### v1.x 84 | 85 | - v1.0.19 Fix __parseFont to not crash 86 | - v1.0.18 clip was not working, the path never made it to the clip area 87 | - v1.0.17 Fix bug with drawing in an empty context. Fix image translation 88 | problem. Fix globalAlpha issue. 89 | - v1.0.16 Add npm publishing support, bower file and optimize for arcs with no 90 | angles. 91 | - v1.0.15 Setup travis, add testharness and debug playground, and fix regression 92 | for __createElement refactor 93 | - v1.0.14 bugfix for gradients, move __createElement to scoped createElement 94 | function, so all classes have access. 95 | - v1.0.13 set paint order before stroke and fill to make them behavior like 96 | canvas 97 | - v1.0.12 Implementation of ctx.prototype.arcTo. 98 | - v1.0.11 call lineTo instead moveTo in ctx.arc, fixes closePath issue and 99 | straight line issue 100 | - v1.0.10 when lineTo called, use M instead of L unless subpath exists 101 | - v1.0.9 use currentDefaultPath instead of 's d attribute, fixes stroke's 102 | different behavior in SVG and canvas. 103 | - v1.0.8 reusing __createElement and adding a properties undefined check 104 | - v1.0.7 fixes for multiple transforms and fills and better text support from 105 | stafyniaksacha 106 | - v1.0.6 basic support for text baseline (contribution from KoKuToru) 107 | - v1.0.5 fixes for #5 and #6 (with contributions from KoKuToru) 108 | - v1.0.4 generate ids that start with a letter 109 | - v1.0.3 fixed #4 where largeArcFlag was set incorrectly in some cases 110 | - v1.0.2 Split up rgba values set in fill/stroke to allow illustrator import 111 | support. 112 | - v1.0.1 Allow C2S to be called as a function. 113 | https://github.com/gliffy/canvas2svg/issues/2 114 | - v1.0.0 Initial release 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Gliffy Inc. 4 | Copyright (c) 2021 Zeno Zeng 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SVGCanvas 2 | 3 | Draw on SVG using Canvas's 2D Context API. A maintained fork of 4 | [gliffy's canvas2svg](https://github.com/gliffy/canvas2svg). 5 | 6 | ## Demo 7 | 8 | https://zenozeng.github.io/svgcanvas/test/ 9 | 10 | ## How it works 11 | 12 | We create a mock 2d canvas context. Use the canvas context like you would on a 13 | normal canvas. As you call methods, we build up a scene graph in SVG. 14 | 15 | ## Usage 16 | 17 | ```javascript 18 | import { Context } from "svgcanvas"; 19 | 20 | const ctx = new Context(500, 500); 21 | 22 | // draw your canvas like you would normally 23 | ctx.fillStyle = "red"; 24 | ctx.fillRect(100, 100, 100, 100); 25 | 26 | // serialize your SVG 27 | const mySerializedSVG = ctx.getSerializedSvg(); 28 | ``` 29 | 30 | Wrapping canvas elements: 31 | 32 | ```javascript 33 | import { Context, Element } from "svgcanvas"; 34 | 35 | const canvas = document.createElement("canvas"); 36 | const context2D = canvas.getContext("2d"); 37 | 38 | // more options to pass into constructor: 39 | const options = { 40 | height: 2000, // falsy values get converted to 500 41 | width: 0 / 0, // falsy values get converted to 500 42 | ctx: context2D, // existing Context2D to wrap around 43 | enableMirroring: false, // whether canvas mirroring (get image data) is enabled (defaults to false) 44 | document: undefined, // overrides default document object 45 | }; 46 | 47 | // Creates a mock canvas context (mocks `context2D` above) 48 | const ctx = new Context(options); 49 | 50 | // draw your canvas like you would normally 51 | ctx.fillStyle = "red"; 52 | ctx.fillRect(100, 100, 100, 100); 53 | 54 | ctx.getSerializedSvg(); // returns the serialized SVG 55 | ctx.getSvg(); // returns the inline svg element 56 | 57 | // Creates a mock canvas element (mocks `canvas` above) 58 | const dom = new Element(options); 59 | dom.ctx; // the internal context, via `new Context(options)` 60 | dom.wrapper; // a div with the svg as a child 61 | dom.svg; // the inline svg element 62 | ``` 63 | 64 | ## Tests 65 | 66 | https://zenozeng.github.io/p5.js-svg/test/ 67 | 68 | ## License 69 | 70 | This library is licensed under the MIT license. 71 | -------------------------------------------------------------------------------- /context.js: -------------------------------------------------------------------------------- 1 | /*!! 2 | * SVGCanvas v2.0.3 3 | * Draw on SVG using Canvas's 2D Context API. 4 | * 5 | * Licensed under the MIT license: 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 8 | * Author: 9 | * Kerry Liu 10 | * Zeno Zeng 11 | * 12 | * Copyright (c) 2014 Gliffy Inc. 13 | * Copyright (c) 2021 Zeno Zeng 14 | */ 15 | 16 | import * as utils from './utils'; 17 | import imageUtils from './image'; 18 | 19 | export default (function () { 20 | "use strict"; 21 | 22 | var STYLES, Context, CanvasGradient, CanvasPattern, namedEntities; 23 | 24 | //helper function to format a string 25 | function format(str, args) { 26 | var keys = Object.keys(args), i; 27 | for (i=0; i 1) { 232 | options = defaultOptions; 233 | options.width = arguments[0]; 234 | options.height = arguments[1]; 235 | } else if ( !o ) { 236 | options = defaultOptions; 237 | } else { 238 | options = o; 239 | } 240 | 241 | if (!(this instanceof Context)) { 242 | //did someone call this without new? 243 | return new Context(options); 244 | } 245 | 246 | //setup options 247 | this.width = options.width || defaultOptions.width; 248 | this.height = options.height || defaultOptions.height; 249 | this.enableMirroring = options.enableMirroring !== undefined ? options.enableMirroring : defaultOptions.enableMirroring; 250 | 251 | this.canvas = this; ///point back to this instance! 252 | this.__document = options.document || document; 253 | 254 | // allow passing in an existing context to wrap around 255 | // if a context is passed in, we know a canvas already exist 256 | if (options.ctx) { 257 | this.__ctx = options.ctx; 258 | } else { 259 | this.__canvas = this.__document.createElement("canvas"); 260 | this.__ctx = this.__canvas.getContext("2d"); 261 | } 262 | 263 | this.__setDefaultStyles(); 264 | this.__styleStack = [this.__getStyleState()]; 265 | this.__groupStack = []; 266 | 267 | //the root svg element 268 | this.__root = this.__document.createElementNS("http://www.w3.org/2000/svg", "svg"); 269 | this.__root.setAttribute("version", 1.1); 270 | this.__root.setAttribute("xmlns", "http://www.w3.org/2000/svg"); 271 | this.__root.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"); 272 | this.__root.setAttribute("width", this.width); 273 | this.__root.setAttribute("height", this.height); 274 | 275 | //make sure we don't generate the same ids in defs 276 | this.__ids = {}; 277 | 278 | //defs tag 279 | this.__defs = this.__document.createElementNS("http://www.w3.org/2000/svg", "defs"); 280 | this.__root.appendChild(this.__defs); 281 | 282 | //also add a group child. the svg element can't use the transform attribute 283 | this.__currentElement = this.__document.createElementNS("http://www.w3.org/2000/svg", "g"); 284 | this.__root.appendChild(this.__currentElement); 285 | 286 | // init transformation matrix 287 | this.resetTransform(); 288 | 289 | this.__options = options; 290 | this.__id = Math.random().toString(16).substring(2, 8); 291 | this.__debug(`new`, o); 292 | }; 293 | 294 | /** 295 | * Log 296 | * 297 | * @private 298 | */ 299 | Context.prototype.__debug = function(...data) { 300 | if (!this.__options.debug) { 301 | return 302 | } 303 | console.debug(`svgcanvas#${this.__id}:`, ...data) 304 | } 305 | 306 | /** 307 | * Creates the specified svg element 308 | * @private 309 | */ 310 | Context.prototype.__createElement = function (elementName, properties, resetFill) { 311 | if (typeof properties === "undefined") { 312 | properties = {}; 313 | } 314 | 315 | var element = this.__document.createElementNS("http://www.w3.org/2000/svg", elementName), 316 | keys = Object.keys(properties), i, key; 317 | if (resetFill) { 318 | //if fill or stroke is not specified, the svg element should not display. By default SVG's fill is black. 319 | element.setAttribute("fill", "none"); 320 | element.setAttribute("stroke", "none"); 321 | } 322 | for (i=0; i 0) { 535 | this.setTransform(this.__transformMatrixStack.pop()) 536 | } 537 | 538 | }; 539 | 540 | /** 541 | * Create a new Path Element 542 | */ 543 | Context.prototype.beginPath = function () { 544 | var path, parent; 545 | 546 | // Note that there is only one current default path, it is not part of the drawing state. 547 | // See also: https://html.spec.whatwg.org/multipage/scripting.html#current-default-path 548 | this.__currentDefaultPath = ""; 549 | this.__currentPosition = {}; 550 | 551 | path = this.__createElement("path", {}, true); 552 | parent = this.__closestGroupOrSvg(); 553 | parent.appendChild(path); 554 | this.__currentElement = path; 555 | }; 556 | 557 | /** 558 | * Helper function to apply currentDefaultPath to current path element 559 | * @private 560 | */ 561 | Context.prototype.__applyCurrentDefaultPath = function () { 562 | var currentElement = this.__currentElement; 563 | if (currentElement.nodeName === "path") { 564 | currentElement.setAttribute("d", this.__currentDefaultPath); 565 | } else { 566 | console.error("Attempted to apply path command to node", currentElement.nodeName); 567 | } 568 | }; 569 | 570 | /** 571 | * Helper function to add path command 572 | * @private 573 | */ 574 | Context.prototype.__addPathCommand = function (command) { 575 | this.__currentDefaultPath += " "; 576 | this.__currentDefaultPath += command; 577 | }; 578 | 579 | /** 580 | * Adds the move command to the current path element, 581 | * if the currentPathElement is not empty create a new path element 582 | */ 583 | Context.prototype.moveTo = function (x,y) { 584 | if (this.__currentElement.nodeName !== "path") { 585 | this.beginPath(); 586 | } 587 | 588 | // creates a new subpath with the given point 589 | this.__currentPosition = {x: x, y: y}; 590 | this.__addPathCommand(format("M {x} {y}", { 591 | x: this.__matrixTransform(x, y).x, 592 | y: this.__matrixTransform(x, y).y 593 | })); 594 | }; 595 | 596 | /** 597 | * Closes the current path 598 | */ 599 | Context.prototype.closePath = function () { 600 | if (this.__currentDefaultPath) { 601 | this.__addPathCommand("Z"); 602 | } 603 | }; 604 | 605 | /** 606 | * Adds a line to command 607 | */ 608 | Context.prototype.lineTo = function (x, y) { 609 | this.__currentPosition = {x: x, y: y}; 610 | if (this.__currentDefaultPath.indexOf('M') > -1) { 611 | this.__addPathCommand(format("L {x} {y}", { 612 | x: this.__matrixTransform(x, y).x, 613 | y: this.__matrixTransform(x, y).y 614 | })); 615 | } else { 616 | this.__addPathCommand(format("M {x} {y}", { 617 | x: this.__matrixTransform(x, y).x, 618 | y: this.__matrixTransform(x, y).y 619 | })); 620 | } 621 | }; 622 | 623 | /** 624 | * Add a bezier command 625 | */ 626 | Context.prototype.bezierCurveTo = function (cp1x, cp1y, cp2x, cp2y, x, y) { 627 | this.__currentPosition = {x: x, y: y}; 628 | this.__addPathCommand(format("C {cp1x} {cp1y} {cp2x} {cp2y} {x} {y}", 629 | { 630 | cp1x: this.__matrixTransform(cp1x, cp1y).x, 631 | cp1y: this.__matrixTransform(cp1x, cp1y).y, 632 | cp2x: this.__matrixTransform(cp2x, cp2y).x, 633 | cp2y: this.__matrixTransform(cp2x, cp2y).y, 634 | x: this.__matrixTransform(x, y).x, 635 | y: this.__matrixTransform(x, y).y 636 | })); 637 | }; 638 | 639 | /** 640 | * Adds a quadratic curve to command 641 | */ 642 | Context.prototype.quadraticCurveTo = function (cpx, cpy, x, y) { 643 | this.__currentPosition = {x: x, y: y}; 644 | this.__addPathCommand(format("Q {cpx} {cpy} {x} {y}", { 645 | cpx: this.__matrixTransform(cpx, cpy).x, 646 | cpy: this.__matrixTransform(cpx, cpy).y, 647 | x: this.__matrixTransform(x, y).x, 648 | y: this.__matrixTransform(x, y).y 649 | })); 650 | }; 651 | 652 | 653 | /** 654 | * Return a new normalized vector of given vector 655 | */ 656 | var normalize = function (vector) { 657 | var len = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1]); 658 | return [vector[0] / len, vector[1] / len]; 659 | }; 660 | 661 | /** 662 | * Adds the arcTo to the current path 663 | * 664 | * @see http://www.w3.org/TR/2015/WD-2dcontext-20150514/#dom-context-2d-arcto 665 | */ 666 | Context.prototype.arcTo = function (x1, y1, x2, y2, radius) { 667 | // Let the point (x0, y0) be the last point in the subpath. 668 | var x0 = this.__currentPosition && this.__currentPosition.x; 669 | var y0 = this.__currentPosition && this.__currentPosition.y; 670 | 671 | // First ensure there is a subpath for (x1, y1). 672 | if (typeof x0 == "undefined" || typeof y0 == "undefined") { 673 | return; 674 | } 675 | 676 | // Negative values for radius must cause the implementation to throw an IndexSizeError exception. 677 | if (radius < 0) { 678 | throw new Error("IndexSizeError: The radius provided (" + radius + ") is negative."); 679 | } 680 | 681 | // If the point (x0, y0) is equal to the point (x1, y1), 682 | // or if the point (x1, y1) is equal to the point (x2, y2), 683 | // or if the radius radius is zero, 684 | // then the method must add the point (x1, y1) to the subpath, 685 | // and connect that point to the previous point (x0, y0) by a straight line. 686 | if (((x0 === x1) && (y0 === y1)) 687 | || ((x1 === x2) && (y1 === y2)) 688 | || (radius === 0)) { 689 | this.lineTo(x1, y1); 690 | return; 691 | } 692 | 693 | // Otherwise, if the points (x0, y0), (x1, y1), and (x2, y2) all lie on a single straight line, 694 | // then the method must add the point (x1, y1) to the subpath, 695 | // and connect that point to the previous point (x0, y0) by a straight line. 696 | var unit_vec_p1_p0 = normalize([x0 - x1, y0 - y1]); 697 | var unit_vec_p1_p2 = normalize([x2 - x1, y2 - y1]); 698 | if (unit_vec_p1_p0[0] * unit_vec_p1_p2[1] === unit_vec_p1_p0[1] * unit_vec_p1_p2[0]) { 699 | this.lineTo(x1, y1); 700 | return; 701 | } 702 | 703 | // Otherwise, let The Arc be the shortest arc given by circumference of the circle that has radius radius, 704 | // and that has one point tangent to the half-infinite line that crosses the point (x0, y0) and ends at the point (x1, y1), 705 | // 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). 706 | // The points at which this circle touches these two lines are called the start and end tangent points respectively. 707 | 708 | // note that both vectors are unit vectors, so the length is 1 709 | var cos = (unit_vec_p1_p0[0] * unit_vec_p1_p2[0] + unit_vec_p1_p0[1] * unit_vec_p1_p2[1]); 710 | var theta = Math.acos(Math.abs(cos)); 711 | 712 | // Calculate origin 713 | var unit_vec_p1_origin = normalize([ 714 | unit_vec_p1_p0[0] + unit_vec_p1_p2[0], 715 | unit_vec_p1_p0[1] + unit_vec_p1_p2[1] 716 | ]); 717 | var len_p1_origin = radius / Math.sin(theta / 2); 718 | var x = x1 + len_p1_origin * unit_vec_p1_origin[0]; 719 | var y = y1 + len_p1_origin * unit_vec_p1_origin[1]; 720 | 721 | // Calculate start angle and end angle 722 | // rotate 90deg clockwise (note that y axis points to its down) 723 | var unit_vec_origin_start_tangent = [ 724 | -unit_vec_p1_p0[1], 725 | unit_vec_p1_p0[0] 726 | ]; 727 | // rotate 90deg counter clockwise (note that y axis points to its down) 728 | var unit_vec_origin_end_tangent = [ 729 | unit_vec_p1_p2[1], 730 | -unit_vec_p1_p2[0] 731 | ]; 732 | var getAngle = function (vector) { 733 | // get angle (clockwise) between vector and (1, 0) 734 | var x = vector[0]; 735 | var y = vector[1]; 736 | if (y >= 0) { // note that y axis points to its down 737 | return Math.acos(x); 738 | } else { 739 | return -Math.acos(x); 740 | } 741 | }; 742 | var startAngle = getAngle(unit_vec_origin_start_tangent); 743 | var endAngle = getAngle(unit_vec_origin_end_tangent); 744 | 745 | // Connect the point (x0, y0) to the start tangent point by a straight line 746 | this.lineTo(x + unit_vec_origin_start_tangent[0] * radius, 747 | y + unit_vec_origin_start_tangent[1] * radius); 748 | 749 | // Connect the start tangent point to the end tangent point by arc 750 | // and adding the end tangent point to the subpath. 751 | this.arc(x, y, radius, startAngle, endAngle); 752 | }; 753 | 754 | /** 755 | * Sets the stroke property on the current element 756 | */ 757 | Context.prototype.stroke = function () { 758 | if (this.__currentElement.nodeName === "path") { 759 | this.__currentElement.setAttribute("paint-order", "fill stroke markers"); 760 | } 761 | this.__applyCurrentDefaultPath(); 762 | this.__applyStyleToCurrentElement("stroke"); 763 | }; 764 | 765 | /** 766 | * Sets fill properties on the current element 767 | */ 768 | Context.prototype.fill = function () { 769 | if (this.__currentElement.nodeName === "path") { 770 | this.__currentElement.setAttribute("paint-order", "stroke fill markers"); 771 | } 772 | this.__applyCurrentDefaultPath(); 773 | this.__applyStyleToCurrentElement("fill"); 774 | }; 775 | 776 | /** 777 | * Adds a rectangle to the path. 778 | */ 779 | Context.prototype.rect = function (x, y, width, height) { 780 | if (this.__currentElement.nodeName !== "path") { 781 | this.beginPath(); 782 | } 783 | this.moveTo(x, y); 784 | this.lineTo(x+width, y); 785 | this.lineTo(x+width, y+height); 786 | this.lineTo(x, y+height); 787 | this.lineTo(x, y); 788 | this.closePath(); 789 | }; 790 | 791 | /** 792 | * Adds a rectangle to the path. 793 | * https://github.com/processing/p5.js/commit/a975515e9ea0a43d89ed522f281d8c9433ede96c 794 | */ 795 | Context.prototype.roundRect = function (x, y, w, h, radii) { 796 | if (this.__currentElement.nodeName !== "path") { 797 | this.beginPath(); 798 | } 799 | let tl, tr, br, bl; 800 | if (typeof radii == 'number') { 801 | tl = radii; 802 | tr = radii; 803 | br = radii; 804 | bl = radii; 805 | } else { 806 | [tl, tr, br, bl] = radii; 807 | } 808 | this.moveTo(x + tl, y); 809 | this.arcTo(x + w, y, x + w, y + h, tr); 810 | this.arcTo(x + w, y + h, x, y + h, br); 811 | this.arcTo(x, y + h, x, y, bl); 812 | this.arcTo(x, y, x + w, y, tl); 813 | this.closePath(); 814 | }; 815 | 816 | /** 817 | * adds a rectangle element 818 | */ 819 | Context.prototype.fillRect = function (x, y, width, height) { 820 | let {a, b, c, d, e, f} = this.getTransform(); 821 | if (JSON.stringify([a, b, c, d, e, f]) === JSON.stringify([1, 0, 0, 1, 0, 0])) { 822 | //clear entire canvas 823 | if (x === 0 && y === 0 && width === this.width && height === this.height) { 824 | this.__clearCanvas(); 825 | } 826 | } 827 | var rect, parent; 828 | rect = this.__createElement("rect", { 829 | x : x, 830 | y : y, 831 | width : width, 832 | height : height 833 | }, true); 834 | parent = this.__closestGroupOrSvg(); 835 | parent.appendChild(rect); 836 | this.__currentElement = rect; 837 | this.__applyTransformation(rect); 838 | this.__applyStyleToCurrentElement("fill"); 839 | }; 840 | 841 | /** 842 | * Draws a rectangle with no fill 843 | * @param x 844 | * @param y 845 | * @param width 846 | * @param height 847 | */ 848 | Context.prototype.strokeRect = function (x, y, width, height) { 849 | var rect, parent; 850 | rect = this.__createElement("rect", { 851 | x : x, 852 | y : y, 853 | width : width, 854 | height : height 855 | }, true); 856 | parent = this.__closestGroupOrSvg(); 857 | parent.appendChild(rect); 858 | this.__currentElement = rect; 859 | this.__applyTransformation(rect); 860 | this.__applyStyleToCurrentElement("stroke"); 861 | }; 862 | 863 | 864 | /** 865 | * Clear entire canvas: 866 | * 1. save current transforms 867 | * 2. remove all the childNodes of the root g element 868 | */ 869 | Context.prototype.__clearCanvas = function () { 870 | var rootGroup = this.__root.childNodes[1]; 871 | this.__root.removeChild(rootGroup); 872 | this.__currentElement = this.__document.createElementNS("http://www.w3.org/2000/svg", "g"); 873 | this.__root.appendChild(this.__currentElement); 874 | //reset __groupStack as all the child group nodes are all removed. 875 | this.__groupStack = []; 876 | }; 877 | 878 | /** 879 | * "Clears" a canvas by just drawing a white rectangle in the current group. 880 | */ 881 | Context.prototype.clearRect = function (x, y, width, height) { 882 | let {a, b, c, d, e, f} = this.getTransform(); 883 | if (JSON.stringify([a, b, c, d, e, f]) === JSON.stringify([1, 0, 0, 1, 0, 0])) { 884 | //clear entire canvas 885 | if (x === 0 && y === 0 && width === this.width && height === this.height) { 886 | this.__clearCanvas(); 887 | return; 888 | } 889 | } 890 | var rect, parent = this.__closestGroupOrSvg(); 891 | rect = this.__createElement("rect", { 892 | x : x, 893 | y : y, 894 | width : width, 895 | height : height, 896 | fill : "#FFFFFF" 897 | }, true); 898 | this.__applyTransformation(rect) 899 | parent.appendChild(rect); 900 | }; 901 | 902 | /** 903 | * Adds a linear gradient to a defs tag. 904 | * Returns a canvas gradient object that has a reference to it's parent def 905 | */ 906 | Context.prototype.createLinearGradient = function (x1, y1, x2, y2) { 907 | var grad = this.__createElement("linearGradient", { 908 | id : randomString(this.__ids), 909 | x1 : x1+"px", 910 | x2 : x2+"px", 911 | y1 : y1+"px", 912 | y2 : y2+"px", 913 | "gradientUnits" : "userSpaceOnUse" 914 | }, false); 915 | this.__defs.appendChild(grad); 916 | return new CanvasGradient(grad, this); 917 | }; 918 | 919 | /** 920 | * Adds a radial gradient to a defs tag. 921 | * Returns a canvas gradient object that has a reference to it's parent def 922 | */ 923 | Context.prototype.createRadialGradient = function (x0, y0, r0, x1, y1, r1) { 924 | var grad = this.__createElement("radialGradient", { 925 | id : randomString(this.__ids), 926 | cx : x1+"px", 927 | cy : y1+"px", 928 | r : r1+"px", 929 | fx : x0+"px", 930 | fy : y0+"px", 931 | "gradientUnits" : "userSpaceOnUse" 932 | }, false); 933 | this.__defs.appendChild(grad); 934 | return new CanvasGradient(grad, this); 935 | 936 | }; 937 | 938 | /** 939 | * Fills or strokes text 940 | * @param text 941 | * @param x 942 | * @param y 943 | * @param action - stroke or fill 944 | * @private 945 | */ 946 | Context.prototype.__applyText = function (text, x, y, action) { 947 | var el = document.createElement("span"); 948 | el.setAttribute("style", 'font:' + this.font); 949 | 950 | var style = el.style, // CSSStyleDeclaration object 951 | parent = this.__closestGroupOrSvg(), 952 | textElement = this.__createElement("text", { 953 | "font-family": style.fontFamily, 954 | "font-size": style.fontSize, 955 | "font-style": style.fontStyle, 956 | "font-weight": style.fontWeight, 957 | 958 | // canvas doesn't support underline natively, but we do :) 959 | "text-decoration": this.__fontUnderline, 960 | "x": x, 961 | "y": y, 962 | "text-anchor": getTextAnchor(this.textAlign), 963 | "dominant-baseline": getDominantBaseline(this.textBaseline) 964 | }, true); 965 | 966 | textElement.appendChild(this.__document.createTextNode(text)); 967 | this.__currentElement = textElement; 968 | this.__applyTransformation(textElement); 969 | this.__applyStyleToCurrentElement(action); 970 | 971 | if (this.__fontHref) { 972 | var a = this.__createElement("a"); 973 | // canvas doesn't natively support linking, but we do :) 974 | a.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", this.__fontHref); 975 | a.appendChild(textElement); 976 | textElement = a; 977 | } 978 | 979 | parent.appendChild(textElement); 980 | }; 981 | 982 | /** 983 | * Creates a text element 984 | * @param text 985 | * @param x 986 | * @param y 987 | */ 988 | Context.prototype.fillText = function (text, x, y) { 989 | this.__applyText(text, x, y, "fill"); 990 | }; 991 | 992 | /** 993 | * Strokes text 994 | * @param text 995 | * @param x 996 | * @param y 997 | */ 998 | Context.prototype.strokeText = function (text, x, y) { 999 | this.__applyText(text, x, y, "stroke"); 1000 | }; 1001 | 1002 | /** 1003 | * No need to implement this for svg. 1004 | * @param text 1005 | * @return {TextMetrics} 1006 | */ 1007 | Context.prototype.measureText = function (text) { 1008 | this.__ctx.font = this.font; 1009 | return this.__ctx.measureText(text); 1010 | }; 1011 | 1012 | /** 1013 | * Arc command! 1014 | */ 1015 | Context.prototype.arc = function (x, y, radius, startAngle, endAngle, counterClockwise) { 1016 | // in canvas no circle is drawn if no angle is provided. 1017 | if (startAngle === endAngle) { 1018 | return; 1019 | } 1020 | startAngle = startAngle % (2*Math.PI); 1021 | endAngle = endAngle % (2*Math.PI); 1022 | if (startAngle === endAngle) { 1023 | //circle time! subtract some of the angle so svg is happy (svg elliptical arc can't draw a full circle) 1024 | endAngle = ((endAngle + (2*Math.PI)) - 0.001 * (counterClockwise ? -1 : 1)) % (2*Math.PI); 1025 | } 1026 | var endX = x+radius*Math.cos(endAngle), 1027 | endY = y+radius*Math.sin(endAngle), 1028 | startX = x+radius*Math.cos(startAngle), 1029 | startY = y+radius*Math.sin(startAngle), 1030 | sweepFlag = counterClockwise ? 0 : 1, 1031 | largeArcFlag = 0, 1032 | diff = endAngle - startAngle; 1033 | 1034 | // https://github.com/gliffy/canvas2svg/issues/4 1035 | if (diff < 0) { 1036 | diff += 2*Math.PI; 1037 | } 1038 | 1039 | if (counterClockwise) { 1040 | largeArcFlag = diff > Math.PI ? 0 : 1; 1041 | } else { 1042 | largeArcFlag = diff > Math.PI ? 1 : 0; 1043 | } 1044 | 1045 | var scaleX = Math.hypot(this.__transformMatrix.a, this.__transformMatrix.b); 1046 | var scaleY = Math.hypot(this.__transformMatrix.c, this.__transformMatrix.d); 1047 | 1048 | this.lineTo(startX, startY); 1049 | this.__addPathCommand(format("A {rx} {ry} {xAxisRotation} {largeArcFlag} {sweepFlag} {endX} {endY}", 1050 | { 1051 | rx:radius * scaleX, 1052 | ry:radius * scaleY, 1053 | xAxisRotation:0, 1054 | largeArcFlag:largeArcFlag, 1055 | sweepFlag:sweepFlag, 1056 | endX: this.__matrixTransform(endX, endY).x, 1057 | endY: this.__matrixTransform(endX, endY).y 1058 | })); 1059 | 1060 | this.__currentPosition = {x: endX, y: endY}; 1061 | }; 1062 | 1063 | /** 1064 | * Ellipse command! 1065 | */ 1066 | Context.prototype.ellipse = function(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterClockwise) { 1067 | if (startAngle === endAngle) { 1068 | return; 1069 | } 1070 | 1071 | var transformedCenter = this.__matrixTransform(x, y); 1072 | x = transformedCenter.x; 1073 | y = transformedCenter.y; 1074 | var scale = this.__getTransformScale(); 1075 | radiusX = radiusX * scale.x; 1076 | radiusY = radiusY * scale.y; 1077 | rotation = rotation + this.__getTransformRotation() 1078 | 1079 | startAngle = startAngle % (2*Math.PI); 1080 | endAngle = endAngle % (2*Math.PI); 1081 | if(startAngle === endAngle) { 1082 | endAngle = ((endAngle + (2*Math.PI)) - 0.001 * (counterClockwise ? -1 : 1)) % (2*Math.PI); 1083 | } 1084 | var endX = x + Math.cos(-rotation) * radiusX * Math.cos(endAngle) 1085 | + Math.sin(-rotation) * radiusY * Math.sin(endAngle), 1086 | endY = y - Math.sin(-rotation) * radiusX * Math.cos(endAngle) 1087 | + Math.cos(-rotation) * radiusY * Math.sin(endAngle), 1088 | startX = x + Math.cos(-rotation) * radiusX * Math.cos(startAngle) 1089 | + Math.sin(-rotation) * radiusY * Math.sin(startAngle), 1090 | startY = y - Math.sin(-rotation) * radiusX * Math.cos(startAngle) 1091 | + Math.cos(-rotation) * radiusY * Math.sin(startAngle), 1092 | sweepFlag = counterClockwise ? 0 : 1, 1093 | largeArcFlag = 0, 1094 | diff = endAngle - startAngle; 1095 | 1096 | if(diff < 0) { 1097 | diff += 2*Math.PI; 1098 | } 1099 | 1100 | if(counterClockwise) { 1101 | largeArcFlag = diff > Math.PI ? 0 : 1; 1102 | } else { 1103 | largeArcFlag = diff > Math.PI ? 1 : 0; 1104 | } 1105 | 1106 | // Transform is already applied, so temporarily remove since lineTo 1107 | // will apply it again. 1108 | var currentTransform = this.__transformMatrix; 1109 | this.resetTransform(); 1110 | this.lineTo(startX, startY); 1111 | this.__transformMatrix = currentTransform; 1112 | 1113 | this.__addPathCommand(format("A {rx} {ry} {xAxisRotation} {largeArcFlag} {sweepFlag} {endX} {endY}", 1114 | { 1115 | rx:radiusX, 1116 | ry:radiusY, 1117 | xAxisRotation:rotation*(180/Math.PI), 1118 | largeArcFlag:largeArcFlag, 1119 | sweepFlag:sweepFlag, 1120 | endX:endX, 1121 | endY:endY 1122 | })); 1123 | 1124 | this.__currentPosition = {x: endX, y: endY}; 1125 | }; 1126 | 1127 | /** 1128 | * Generates a ClipPath from the clip command. 1129 | */ 1130 | Context.prototype.clip = function () { 1131 | var group = this.__closestGroupOrSvg(), 1132 | clipPath = this.__createElement("clipPath"), 1133 | id = randomString(this.__ids), 1134 | newGroup = this.__createElement("g"); 1135 | 1136 | this.__applyCurrentDefaultPath(); 1137 | group.removeChild(this.__currentElement); 1138 | clipPath.setAttribute("id", id); 1139 | clipPath.appendChild(this.__currentElement); 1140 | 1141 | this.__defs.appendChild(clipPath); 1142 | 1143 | //set the clip path to this group 1144 | group.setAttribute("clip-path", format("url(#{id})", {id:id})); 1145 | 1146 | //clip paths can be scaled and transformed, we need to add another wrapper group to avoid later transformations 1147 | // to this path 1148 | group.appendChild(newGroup); 1149 | 1150 | this.__currentElement = newGroup; 1151 | 1152 | }; 1153 | 1154 | /** 1155 | * Draws a canvas, image or mock context to this canvas. 1156 | * Note that all svg dom manipulation uses node.childNodes rather than node.children for IE support. 1157 | * http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-drawimage 1158 | */ 1159 | Context.prototype.drawImage = function () { 1160 | //convert arguments to a real array 1161 | var args = Array.prototype.slice.call(arguments), 1162 | image=args[0], 1163 | dx, dy, dw, dh, sx=0, sy=0, sw, sh, parent, svg, defs, group, 1164 | currentElement, svgImage, canvas, context, id; 1165 | 1166 | if (args.length === 3) { 1167 | dx = args[1]; 1168 | dy = args[2]; 1169 | sw = image.width; 1170 | sh = image.height; 1171 | dw = sw; 1172 | dh = sh; 1173 | } else if (args.length === 5) { 1174 | dx = args[1]; 1175 | dy = args[2]; 1176 | dw = args[3]; 1177 | dh = args[4]; 1178 | sw = image.width; 1179 | sh = image.height; 1180 | } else if (args.length === 9) { 1181 | sx = args[1]; 1182 | sy = args[2]; 1183 | sw = args[3]; 1184 | sh = args[4]; 1185 | dx = args[5]; 1186 | dy = args[6]; 1187 | dw = args[7]; 1188 | dh = args[8]; 1189 | } else { 1190 | throw new Error("Invalid number of arguments passed to drawImage: " + arguments.length); 1191 | } 1192 | 1193 | parent = this.__closestGroupOrSvg(); 1194 | currentElement = this.__currentElement; 1195 | const matrix = this.getTransform().translate(dx, dy); 1196 | if (image instanceof Context) { 1197 | //canvas2svg mock canvas context. In the future we may want to clone nodes instead. 1198 | //also I'm currently ignoring dw, dh, sw, sh, sx, sy for a mock context. 1199 | svg = image.getSvg().cloneNode(true); 1200 | if (svg.childNodes && svg.childNodes.length > 1) { 1201 | defs = svg.childNodes[0]; 1202 | while(defs.childNodes.length) { 1203 | id = defs.childNodes[0].getAttribute("id"); 1204 | this.__ids[id] = id; 1205 | this.__defs.appendChild(defs.childNodes[0]); 1206 | } 1207 | group = svg.childNodes[1]; 1208 | if (group) { 1209 | this.__applyTransformation(group, matrix); 1210 | parent.appendChild(group); 1211 | } 1212 | } 1213 | } else if (image.nodeName === "CANVAS" || image.nodeName === "IMG") { 1214 | //canvas or image 1215 | svgImage = this.__createElement("image"); 1216 | svgImage.setAttribute("width", dw); 1217 | svgImage.setAttribute("height", dh); 1218 | svgImage.setAttribute("preserveAspectRatio", "none"); 1219 | 1220 | if (sx || sy || sw !== image.width || sh !== image.height) { 1221 | //crop the image using a temporary canvas 1222 | canvas = this.__document.createElement("canvas"); 1223 | canvas.width = dw; 1224 | canvas.height = dh; 1225 | context = canvas.getContext("2d"); 1226 | context.drawImage(image, sx, sy, sw, sh, 0, 0, dw, dh); 1227 | image = canvas; 1228 | } 1229 | this.__applyTransformation(svgImage, matrix); 1230 | svgImage.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", 1231 | image.nodeName === "CANVAS" ? image.toDataURL() : image.getAttribute("src")); 1232 | parent.appendChild(svgImage); 1233 | } 1234 | }; 1235 | 1236 | /** 1237 | * Generates a pattern tag 1238 | */ 1239 | Context.prototype.createPattern = function (image, repetition) { 1240 | var pattern = this.__document.createElementNS("http://www.w3.org/2000/svg", "pattern"), id = randomString(this.__ids), 1241 | img; 1242 | pattern.setAttribute("id", id); 1243 | pattern.setAttribute("width", image.width); 1244 | pattern.setAttribute("height", image.height); 1245 | // We want the pattern sizing to be absolute, and not relative 1246 | // https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Patterns 1247 | // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/patternUnits 1248 | pattern.setAttribute("patternUnits", "userSpaceOnUse"); 1249 | 1250 | if (image.nodeName === "CANVAS" || image.nodeName === "IMG") { 1251 | img = this.__document.createElementNS("http://www.w3.org/2000/svg", "image"); 1252 | img.setAttribute("width", image.width); 1253 | img.setAttribute("height", image.height); 1254 | img.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", 1255 | image.nodeName === "CANVAS" ? image.toDataURL() : image.getAttribute("src")); 1256 | pattern.appendChild(img); 1257 | this.__defs.appendChild(pattern); 1258 | } else if (image instanceof Context) { 1259 | pattern.appendChild(image.__root.childNodes[1]); 1260 | this.__defs.appendChild(pattern); 1261 | } 1262 | return new CanvasPattern(pattern, this); 1263 | }; 1264 | 1265 | Context.prototype.setLineDash = function (dashArray) { 1266 | if (dashArray && dashArray.length > 0) { 1267 | this.lineDash = dashArray.join(","); 1268 | } else { 1269 | this.lineDash = null; 1270 | } 1271 | }; 1272 | 1273 | /** 1274 | * SetTransform changes the current transformation matrix to 1275 | * the matrix given by the arguments as described below. 1276 | * 1277 | * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setTransform 1278 | */ 1279 | Context.prototype.setTransform = function (a, b, c, d, e, f) { 1280 | if (a instanceof DOMMatrix) { 1281 | this.__transformMatrix = new DOMMatrix([a.a, a.b, a.c, a.d, a.e, a.f]); 1282 | } else { 1283 | this.__transformMatrix = new DOMMatrix([a, b, c, d, e, f]); 1284 | } 1285 | }; 1286 | 1287 | /** 1288 | * GetTransform Returns a copy of the current transformation matrix, 1289 | * as a newly created DOMMAtrix Object 1290 | * 1291 | * @returns A DOMMatrix Object 1292 | */ 1293 | Context.prototype.getTransform = function () { 1294 | let {a, b, c, d, e, f} = this.__transformMatrix; 1295 | return new DOMMatrix([a, b, c, d, e, f]); 1296 | }; 1297 | 1298 | /** 1299 | * ResetTransform resets the current transformation matrix to the identity matrix 1300 | * 1301 | * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/resetTransform 1302 | */ 1303 | Context.prototype.resetTransform = function () { 1304 | this.setTransform(1, 0, 0, 1, 0, 0); 1305 | }; 1306 | 1307 | /** 1308 | * Add the scaling transformation described by the arguments to the current transformation matrix. 1309 | * 1310 | * @param x The x argument represents the scale factor in the horizontal direction 1311 | * @param y The y argument represents the scale factor in the vertical direction. 1312 | * @see https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-scale 1313 | */ 1314 | Context.prototype.scale = function (x, y) { 1315 | if (y === undefined) { 1316 | y = x; 1317 | } 1318 | // If either of the arguments are infinite or NaN, then return. 1319 | if (isNaN(x) || isNaN(y) || !isFinite(x) || !isFinite(y)) { 1320 | return 1321 | } 1322 | let matrix = this.getTransform().scale(x, y); 1323 | this.setTransform(matrix); 1324 | }; 1325 | 1326 | /** 1327 | * Rotate adds a rotation to the transformation matrix. 1328 | * 1329 | * @param angle The rotation angle, clockwise in radians. You can use degree * Math.PI / 180 to calculate a radian from a degree. 1330 | * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/rotate 1331 | * @see https://www.w3.org/TR/css-transforms-1 1332 | */ 1333 | Context.prototype.rotate = function (angle) { 1334 | let matrix = this.getTransform().multiply(new DOMMatrix([ 1335 | Math.cos(angle), 1336 | Math.sin(angle), 1337 | -Math.sin(angle), 1338 | Math.cos(angle), 1339 | 0, 1340 | 0 1341 | ])) 1342 | this.setTransform(matrix); 1343 | }; 1344 | 1345 | /** 1346 | * Translate adds a translation transformation to the current matrix. 1347 | * 1348 | * @param x Distance to move in the horizontal direction. Positive values are to the right, and negative to the left. 1349 | * @param y Distance to move in the vertical direction. Positive values are down, and negative are up. 1350 | * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/translate 1351 | */ 1352 | Context.prototype.translate = function (x, y) { 1353 | const matrix = this.getTransform().translate(x, y); 1354 | this.setTransform(matrix); 1355 | }; 1356 | 1357 | /** 1358 | * Transform multiplies the current transformation with the matrix described by the arguments of this method. 1359 | * This lets you scale, rotate, translate (move), and skew the context. 1360 | * 1361 | * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/transform 1362 | */ 1363 | Context.prototype.transform = function (a, b, c, d, e, f) { 1364 | const matrix = this.getTransform().multiply(new DOMMatrix([a, b, c, d, e, f])); 1365 | this.setTransform(matrix); 1366 | }; 1367 | 1368 | Context.prototype.__matrixTransform = function(x, y) { 1369 | return new DOMPoint(x, y).matrixTransform(this.__transformMatrix) 1370 | } 1371 | 1372 | /** 1373 | * 1374 | * @returns The scale component of the transform matrix as {x,y}. 1375 | */ 1376 | Context.prototype.__getTransformScale = function() { 1377 | return { 1378 | x: Math.hypot(this.__transformMatrix.a, this.__transformMatrix.b), 1379 | y: Math.hypot(this.__transformMatrix.c, this.__transformMatrix.d) 1380 | }; 1381 | } 1382 | 1383 | /** 1384 | * 1385 | * @returns The rotation component of the transform matrix in radians. 1386 | */ 1387 | Context.prototype.__getTransformRotation = function() { 1388 | return Math.atan2(this.__transformMatrix.b, this.__transformMatrix.a); 1389 | } 1390 | 1391 | /** 1392 | * 1393 | * @param {*} sx The x-axis coordinate of the top-left corner of the rectangle from which the ImageData will be extracted. 1394 | * @param {*} sy The y-axis coordinate of the top-left corner of the rectangle from which the ImageData will be extracted. 1395 | * @param {*} sw The width of the rectangle from which the ImageData will be extracted. Positive values are to the right, and negative to the left. 1396 | * @param {*} sh The height of the rectangle from which the ImageData will be extracted. Positive values are down, and negative are up. 1397 | * @param {Boolean} options.async Will return a Promise if true, must be set to true 1398 | * @returns An ImageData object containing the image data for the rectangle of the canvas specified. The coordinates of the rectangle's top-left corner are (sx, sy), while the coordinates of the bottom corner are (sx + sw, sy + sh). 1399 | */ 1400 | Context.prototype.getImageData = function(sx, sy, sw, sh, options) { 1401 | return imageUtils.getImageData(this.getSvg(), this.width, this.height, sx, sy, sw, sh, options); 1402 | }; 1403 | 1404 | /** 1405 | * Not yet implemented 1406 | */ 1407 | Context.prototype.drawFocusRing = function () {}; 1408 | Context.prototype.createImageData = function () {}; 1409 | Context.prototype.putImageData = function () {}; 1410 | Context.prototype.globalCompositeOperation = function () {}; 1411 | 1412 | return Context; 1413 | }()); 1414 | -------------------------------------------------------------------------------- /element.js: -------------------------------------------------------------------------------- 1 | import Context from './context'; 2 | import imageUtils from './image'; 3 | 4 | function SVGCanvasElement(options) { 5 | 6 | this.ctx = new Context(options); 7 | this.svg = this.ctx.__root; 8 | 9 | // sync attributes to svg 10 | var svg = this.svg; 11 | var _this = this; 12 | 13 | var wrapper = document.createElement('div'); 14 | wrapper.style.display = 'inline-block'; 15 | wrapper.appendChild(svg); 16 | this.wrapper = wrapper; 17 | 18 | Object.defineProperty(this, 'className', { 19 | get: function() { 20 | return wrapper.getAttribute('class') || ''; 21 | }, 22 | set: function(val) { 23 | return wrapper.setAttribute('class', val); 24 | } 25 | }); 26 | 27 | Object.defineProperty(this, 'tagName', { 28 | get: function() { 29 | return "CANVAS"; 30 | }, 31 | set: function() {} // no-op 32 | }); 33 | 34 | ["width", "height"].forEach(function(prop) { 35 | Object.defineProperty(_this, prop, { 36 | get: function() { 37 | return svg.getAttribute(prop) | 0; 38 | }, 39 | set: function(val) { 40 | if (isNaN(val) || (typeof val === "undefined")) { 41 | return; 42 | } 43 | _this.ctx[prop] = val; 44 | svg.setAttribute(prop, val); 45 | return wrapper[prop] = val; 46 | } 47 | }); 48 | }); 49 | 50 | ["style", "id"].forEach(function(prop) { 51 | Object.defineProperty(_this, prop, { 52 | get: function() { 53 | return wrapper[prop]; 54 | }, 55 | set: function(val) { 56 | if (typeof val !== "undefined") { 57 | return wrapper[prop] = val; 58 | } 59 | } 60 | }); 61 | }); 62 | 63 | ["getBoundingClientRect"].forEach(function(fn) { 64 | _this[fn] = function() { 65 | return svg[fn](); 66 | }; 67 | }); 68 | } 69 | 70 | SVGCanvasElement.prototype.getContext = function(type) { 71 | if (type !== '2d') { 72 | throw new Error('Unsupported type of context for SVGCanvas'); 73 | } 74 | 75 | return this.ctx; 76 | }; 77 | 78 | // you should always use URL.revokeObjectURL after your work done 79 | SVGCanvasElement.prototype.toObjectURL = function() { 80 | var data = new XMLSerializer().serializeToString(this.svg); 81 | var svg = new Blob([data], {type: 'image/svg+xml;charset=utf-8'}); 82 | return URL.createObjectURL(svg); 83 | }; 84 | 85 | /** 86 | * toDataURL returns a data URI containing a representation of the image in the format specified by the type parameter. 87 | * 88 | * @param {String} type A DOMString indicating the image format. The default type is image/svg+xml; this image format will be also used if the specified type is not supported. 89 | * @param {Number} encoderOptions A Number between 0 and 1 indicating the image quality to be used when creating images using file formats that support lossy compression (such as image/jpeg or image/webp). A user agent will use its default quality value if this option is not specified, or if the number is outside the allowed range. 90 | * @param {Boolean} options.async Will return a Promise if true, must be set to true if type is not image/svg+xml 91 | */ 92 | SVGCanvasElement.prototype.toDataURL = function(type, encoderOptions, options) { 93 | return imageUtils.toDataURL(this.svg, this.width, this.height, type, encoderOptions, options) 94 | }; 95 | 96 | SVGCanvasElement.prototype.addEventListener = function() { 97 | return this.svg.addEventListener.apply(this.svg, arguments); 98 | }; 99 | 100 | // will return wrapper element:
101 | SVGCanvasElement.prototype.getElement = function() { 102 | return this.wrapper; 103 | }; 104 | 105 | SVGCanvasElement.prototype.getAttribute = function(prop) { 106 | return this.wrapper.getAttribute(prop); 107 | }; 108 | 109 | SVGCanvasElement.prototype.setAttribute = function(prop, val) { 110 | this.wrapper.setAttribute(prop, val); 111 | }; 112 | 113 | export default SVGCanvasElement; 114 | -------------------------------------------------------------------------------- /image.js: -------------------------------------------------------------------------------- 1 | class ImageUtils { 2 | 3 | /** 4 | * Convert svg dataurl to canvas element 5 | * 6 | * @private 7 | */ 8 | async svg2canvas(svgDataURL, width, height) { 9 | const svgImage = await new Promise((resolve) => { 10 | var svgImage = new Image(); 11 | svgImage.onload = function() { 12 | resolve(svgImage); 13 | } 14 | svgImage.src = svgDataURL; 15 | }) 16 | var canvas = document.createElement('canvas'); 17 | canvas.width = width; 18 | canvas.height = height; 19 | const ctx = canvas.getContext('2d'); 20 | ctx.drawImage(svgImage, 0, 0); 21 | return canvas; 22 | } 23 | 24 | toDataURL(svgNode, width, height, type, encoderOptions, options) { 25 | var xml = new XMLSerializer().serializeToString(svgNode); 26 | 27 | // documentMode is an IE-only property 28 | // http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx 29 | // http://stackoverflow.com/questions/10964966/detect-ie-version-prior-to-v9-in-javascript 30 | var isIE = document.documentMode; 31 | 32 | if (isIE) { 33 | // This is patch from canvas2svg 34 | // IE search for a duplicate xmnls because they didn't implement setAttributeNS correctly 35 | var xmlns = /xmlns="http:\/\/www\.w3\.org\/2000\/svg".+xmlns="http:\/\/www\.w3\.org\/2000\/svg/gi; 36 | if(xmlns.test(xml)) { 37 | xml = xml.replace('xmlns="http://www.w3.org/2000/svg','xmlns:xlink="http://www.w3.org/1999/xlink'); 38 | } 39 | } 40 | 41 | if (!options) { 42 | options = {} 43 | } 44 | 45 | var SVGDataURL = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(xml); 46 | if (type === "image/svg+xml" || !type) { 47 | if (options.async) { 48 | return Promise.resolve(SVGDataURL) 49 | } 50 | return SVGDataURL; 51 | } 52 | if (type === "image/jpeg" || type === "image/png") { 53 | if (!options.async) { 54 | throw new Error('svgcanvas: options.async must be set to true if type is image/jpeg | image/png') 55 | } 56 | return (async () => { 57 | const canvas = await this.svg2canvas(SVGDataURL, width, height); 58 | const dataUrl = canvas.toDataURL(type, encoderOptions); 59 | canvas.remove(); 60 | return dataUrl; 61 | })() 62 | } 63 | throw new Error('svgcanvas: Unknown type for toDataURL, please use image/jpeg | image/png | image/svg+xml.'); 64 | } 65 | 66 | getImageData(svgNode, width, height, sx, sy, sw, sh, options) { 67 | if (!options) { 68 | options = {} 69 | } 70 | if (!options.async) { 71 | throw new Error('svgcanvas: options.async must be set to true for getImageData') 72 | } 73 | const svgDataURL = this.toDataURL(svgNode, width, height, 'image/svg+xml'); 74 | return (async () => { 75 | const canvas = await this.svg2canvas(svgDataURL, width, height); 76 | const ctx = canvas.getContext('2d') 77 | const imageData = ctx.getImageData(sx, sy, sw, sh); 78 | canvas.remove(); 79 | return imageData; 80 | })() 81 | } 82 | } 83 | 84 | const utils = new ImageUtils(); 85 | 86 | export default utils; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import Context from './context'; 2 | import Element from './element'; 3 | 4 | export {Context}; 5 | export {Element}; -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | process.env.CHROME_BIN = require('puppeteer').executablePath(); 2 | 3 | module.exports = function(config) { 4 | config.set({ 5 | basePath: '', 6 | frameworks: ['mocha'], 7 | files: [ 8 | 'dist/rendering.test.js', 9 | ], 10 | preprocessors: { 11 | '**/*.js': ['sourcemap'] 12 | }, 13 | reporters: ['progress', 'coverage', 'mocha'], 14 | coverageReporter: { 15 | type: 'lcovonly', 16 | dir : 'coverage/', 17 | subdir: '.', 18 | file: 'lcov.info' 19 | }, 20 | port: 9876, 21 | colors: true, 22 | logLevel: config.LOG_INFO, 23 | autoWatch: false, 24 | browsers: ['ChromeHeadlessNoSandbox'], 25 | customLaunchers: { 26 | ChromeHeadlessNoSandbox: { 27 | base: 'ChromeHeadless', 28 | flags: ['--no-sandbox'] 29 | } 30 | }, 31 | singleRun: true // output all logs to stdout instead of click debug button 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svgcanvas", 3 | "version": "2.6.0", 4 | "description": "svgcanvas", 5 | "main": "dist/svgcanvas.js", 6 | "scripts": { 7 | "watch": "rollup -c -w", 8 | "build": "rollup -c", 9 | "prepublishOnly": "npm run build", 10 | "test": "karma start" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/zenozeng/svgcanvas.git" 15 | }, 16 | "keywords": [ 17 | "canvas", 18 | "svg", 19 | "canvas2svg" 20 | ], 21 | "author": "Zeno Zeng", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "@rollup/plugin-commonjs": "^21.0.2", 25 | "@rollup/plugin-node-resolve": "^13.1.3", 26 | "chai": "^4.3.6", 27 | "karma": "^6.3.17", 28 | "karma-chrome-launcher": "^3.1.0", 29 | "karma-coverage": "^2.2.0", 30 | "karma-mocha": "^2.0.1", 31 | "karma-mocha-reporter": "^2.2.5", 32 | "karma-sourcemap-loader": "^0.3.8", 33 | "mocha": "^11.1.0", 34 | "puppeteer": "^24.6.0", 35 | "rollup": "^2.67.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | 4 | export default [ 5 | { 6 | input: 'index.js', 7 | output: { 8 | file: 'dist/svgcanvas.js', 9 | format: 'cjs' 10 | } 11 | }, 12 | { 13 | input: 'index.js', 14 | output: { 15 | file: 'dist/svgcanvas.esm.js', 16 | format: 'esm' 17 | } 18 | }, 19 | { 20 | input: 'test/index.js', 21 | output: { 22 | file: 'dist/test.js', 23 | format: 'iife' 24 | } 25 | }, 26 | { 27 | input: 'test/rendering.test.js', 28 | output: { 29 | file: 'dist/rendering.test.js', 30 | format: 'iife', 31 | sourcemap: true, 32 | }, 33 | plugins: [nodeResolve(), commonjs()] 34 | }, 35 | ] -------------------------------------------------------------------------------- /test/css/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | } 4 | 5 | .example { 6 | margin-bottom: 50px; 7 | } 8 | 9 | h2 { 10 | font-weight: normal; 11 | } 12 | 13 | canvas, svg { 14 | width: 500px; 15 | height: 500px; 16 | border: 1px solid #ccc; 17 | margin-right: 50px; 18 | } 19 | 20 | .canvas, .svg { 21 | display: inline-block; 22 | } 23 | 24 | .svg::after, .canvas::after { 25 | display: block; 26 | position: absolute; 27 | width: 500px; 28 | text-align: center; 29 | font-size: 14px; 30 | color: #aaa; 31 | } 32 | 33 | .canvas::after { 34 | content: 'Canvas'; 35 | } 36 | 37 | .svg::after { 38 | content: 'SVG'; 39 | } 40 | 41 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import {Element} from '../index' 2 | import arc from './tests/arc' 3 | import arcTo from './tests/arcTo' 4 | import arcTo2 from './tests/arcTo2' 5 | import arcToScaled from './tests/arcToScaled' 6 | import emptyArc from './tests/emptyArc' 7 | import ellipse from './tests/ellipse' 8 | import ellipse2 from './tests/ellipse2' 9 | import fillstyle from './tests/fillstyle' 10 | import globalAlpha from './tests/globalalpha' 11 | import gradient from './tests/gradient' 12 | import linecap from './tests/linecap' 13 | import linewidth from './tests/linewidth' 14 | import scaledLine from './tests/scaledLine' 15 | import rgba from './tests/rgba' 16 | import rotate from './tests/rotate' 17 | import saveandrestore from './tests/saveandrestore' 18 | import setLineDash from './tests/setLineDash' 19 | import text from './tests/text' 20 | import tiger from './tests/tiger' 21 | import transform from './tests/transform' 22 | import pattern from "./tests/pattern"; 23 | 24 | const tests = [ 25 | tiger, 26 | arc, 27 | arcTo, 28 | arcTo2, 29 | arcToScaled, 30 | emptyArc, 31 | ellipse, 32 | ellipse2, 33 | fillstyle, 34 | globalAlpha, 35 | gradient, 36 | linecap, 37 | linewidth, 38 | scaledLine, 39 | rgba, 40 | rotate, 41 | saveandrestore, 42 | setLineDash, 43 | text, 44 | transform, 45 | pattern 46 | ]; 47 | 48 | for (let fn of tests) { 49 | let name = fn.name; 50 | // Container 51 | const container = document.createElement('div'); 52 | container.className = 'example'; 53 | container.id = 'example-' + name; 54 | container.innerHTML = `

${name}

` 55 | // Canvas 56 | const canvas = document.createElement('canvas'); 57 | container.querySelector('.canvas').appendChild(canvas); 58 | // SVGCanvas 59 | const svgcanvas = new Element(); 60 | container.querySelector('.svg').appendChild(svgcanvas.getElement()); 61 | document.querySelector('body').appendChild(container); 62 | // Render 63 | for (let c of [canvas, svgcanvas]) { 64 | c.width = 500; 65 | c.height = 500; 66 | const ctx = c.getContext('2d'); 67 | fn(ctx); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenozeng/svgcanvas/c22ead1de7d9ea0639bc1704ca0b2625493731f5/test/pattern.png -------------------------------------------------------------------------------- /test/rendering.test.js: -------------------------------------------------------------------------------- 1 | import { Element } from '../index' 2 | import { expect } from 'chai' 3 | import arc from './tests/arc' 4 | import arcTo from './tests/arcTo' 5 | import arcTo2 from './tests/arcTo2' 6 | import arcToScaled from './tests/arcToScaled' 7 | import emptyArc from './tests/emptyArc' 8 | import ellipse from './tests/ellipse' 9 | import ellipse2 from './tests/ellipse2' 10 | import fillstyle from './tests/fillstyle' 11 | import globalAlpha from './tests/globalalpha' 12 | import gradient from './tests/gradient' 13 | import linecap from './tests/linecap' 14 | import linewidth from './tests/linewidth' 15 | import scaledLine from './tests/scaledLine' 16 | import rgba from './tests/rgba' 17 | import rotate from './tests/rotate' 18 | import saveandrestore from './tests/saveandrestore' 19 | import setLineDash from './tests/setLineDash' 20 | import text from './tests/text' 21 | import tiger from './tests/tiger' 22 | import transform from './tests/transform' 23 | import pattern from "./tests/pattern"; 24 | 25 | const tests = { 26 | tiger, 27 | arc, 28 | arcTo, 29 | arcTo2, 30 | arcToScaled, 31 | emptyArc, 32 | ellipse, 33 | ellipse2, 34 | fillstyle, 35 | globalAlpha, 36 | gradient, 37 | linecap, 38 | linewidth, 39 | scaledLine, 40 | rgba, 41 | rotate, 42 | saveandrestore, 43 | setLineDash, 44 | text, 45 | transform, 46 | pattern 47 | }; 48 | 49 | const config = { 50 | pixelDensity: 3 // for 200% and 150% 51 | } 52 | 53 | class RenderingTester { 54 | constructor(name, fn) { 55 | this.name = name; 56 | this.fn = fn; 57 | this.width = 600; 58 | this.height = 600; 59 | } 60 | 61 | async test() { 62 | const canvas = document.createElement('canvas'); 63 | const svgcanvas = new Element(); 64 | 65 | [canvas, svgcanvas].forEach((canvas) => { 66 | canvas.width = this.width 67 | canvas.height = this.height 68 | const ctx = canvas.getContext('2d') 69 | this.fn(ctx) 70 | }) 71 | 72 | // Pixels 73 | const svg = svgcanvas.toDataURL("image/svg+xml"); 74 | const svgImage = await new Promise((resolve) => { 75 | var svgImage = new Image(); 76 | svgImage.onload = function () { 77 | resolve(svgImage) 78 | } 79 | svgImage.src = svg; 80 | }) 81 | const svgPixels = this.getPixels(svgImage); 82 | const canvasPixels = this.getPixels(canvas); 83 | const diffPixels = this.diffPixels(svgPixels, canvasPixels); 84 | const removeThinLinesPixels = this.removeThinLines(this.removeThinLines(diffPixels)); 85 | const svgPixelsCount = this.countPixels(svgPixels); 86 | const canvasPixelsCount = this.countPixels(canvasPixels) 87 | const count = Math.max(svgPixelsCount, canvasPixelsCount); 88 | const diffPixelsCount = this.countPixels(removeThinLinesPixels); 89 | console.log({ fn: this.name, count, diffCount: diffPixelsCount, svgPixelsCount, canvasPixelsCount }) 90 | if (count === 0 && diffPixelsCount === 0) { 91 | return 0 92 | } 93 | const diffRate = diffPixelsCount / count; 94 | return diffRate; 95 | } 96 | 97 | getPixels(image) { 98 | const canvas = document.createElement('canvas'); 99 | const width = this.width; 100 | const height = this.height; 101 | canvas.width = width; 102 | canvas.height = height; 103 | const ctx = canvas.getContext('2d'); 104 | ctx.drawImage(image, 0, 0, width, height); 105 | return ctx.getImageData(0, 0, width, height); 106 | } 107 | 108 | // count non transparent pixels 109 | countPixels(imgData) { 110 | var count = 0; 111 | for (var i = 3; i < imgData.data.length; i += 4) { 112 | if (imgData.data[i] > 0) { 113 | count++; 114 | } 115 | } 116 | return count; 117 | }; 118 | 119 | 120 | diffPixels(imgData1, imgData2) { 121 | const canvas = document.createElement('canvas'); 122 | const width = this.width; 123 | const height = this.height; 124 | const diffImgData = canvas.getContext('2d').getImageData(0, 0, width, height); 125 | for (var i = 0; i < imgData1.data.length; i += 4) { 126 | var indexes = [i, i + 1, i + 2, i + 3]; 127 | indexes.forEach(function (i) { 128 | diffImgData.data[i] = 0; 129 | }); 130 | if (indexes.some(function (i) { 131 | return Math.abs(imgData1.data[i] - imgData2.data[i]) > 0; 132 | })) { 133 | diffImgData.data[i + 3] = 255; // set black 134 | } 135 | } 136 | return diffImgData; 137 | } 138 | 139 | removeThinLines(imageData) { 140 | const canvas = document.createElement('canvas'); 141 | const width = this.width; 142 | const height = this.height; 143 | canvas.width = width; 144 | canvas.height = height; 145 | const ctx = canvas.getContext('2d'); 146 | ctx.putImageData(imageData, 0, 0); 147 | var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); 148 | var imgDataCopy = ctx.getImageData(0, 0, canvas.width, canvas.height); 149 | 150 | var getPixelIndex = function (x, y) { 151 | return (y * width + x) * 4 + 3; 152 | }; 153 | 154 | var getPixel = function (x, y) { 155 | var alphaIndex = getPixelIndex(x, y); 156 | return imgDataCopy.data[alphaIndex]; 157 | }; 158 | 159 | var setPixel = function (x, y, value) { 160 | imgData.data[getPixelIndex(x, y)] = value; 161 | }; 162 | 163 | for (var x = 1; x < width - 1; x++) { 164 | for (var y = 1; y < height - 1; y++) { 165 | if (getPixel(x, y) == 0) { 166 | continue; // ignore transparents 167 | } 168 | var links = [ 169 | { x: x - 1, y: y - 1 }, 170 | { x: x, y: y - 1 }, 171 | { x: x + 1, y: y - 1 }, 172 | { x: x - 1, y: y }, 173 | { x: x + 1, y: y }, 174 | { x: x - 1, y: y + 1 }, 175 | { x: x, y: y + 1 }, 176 | { x: x + 1, y: y + 1 } 177 | ].map(function (p) { 178 | return getPixel(p.x, p.y); 179 | }).filter(function (val) { 180 | return val > 0; // not transparent? 181 | }).length; 182 | 183 | if (links < 5) { // is a thin line 184 | setPixel(x, y, 0); // make it transparent 185 | } 186 | } 187 | } 188 | return imgData; 189 | } 190 | } 191 | 192 | describe('RenderTest', () => { 193 | for (let fn of Object.keys(tests)) { 194 | it(`should render same results for ${fn}`, async () => { 195 | const tester = new RenderingTester(fn, tests[fn]); 196 | const diffRate = await tester.test(); 197 | expect(diffRate).to.lessThan(0.05); 198 | }) 199 | } 200 | }) 201 | -------------------------------------------------------------------------------- /test/tests/arc.js: -------------------------------------------------------------------------------- 1 | export default function arc(ctx) { 2 | 3 | // Draw shapes 4 | for (let i = 0; i < 4; i++) { 5 | for (let j = 0; j < 3; j++) { 6 | ctx.beginPath(); 7 | var x = 25 + j * 50; // x coordinate 8 | var y = 25 + i * 50; // y coordinate 9 | var radius = 20; // Arc radius 10 | var startAngle = 0; // Starting point on circle 11 | var endAngle = Math.PI + (Math.PI * j) / 2; // End point on circle 12 | var clockwise = i % 2 == 0 ? false : true; // clockwise or anticlockwise 13 | 14 | ctx.arc(x, y, radius, startAngle, endAngle, clockwise); 15 | 16 | if (i > 1) { 17 | ctx.fill(); 18 | } else { 19 | ctx.stroke(); 20 | } 21 | } 22 | } 23 | 24 | }; -------------------------------------------------------------------------------- /test/tests/arcTo.js: -------------------------------------------------------------------------------- 1 | export default function arcTo(ctx) { 2 | ctx.beginPath(); 3 | ctx.moveTo(150, 20); 4 | ctx.arcTo(150, 100, 50, 20, 30); 5 | ctx.stroke(); 6 | 7 | ctx.fillStyle = 'blue'; 8 | // base point 9 | ctx.fillRect(150, 20, 10, 10); 10 | 11 | ctx.fillStyle = 'red'; 12 | // control point one 13 | ctx.fillRect(150, 100, 10, 10); 14 | // control point two 15 | ctx.fillRect(50, 20, 10, 10); 16 | }; -------------------------------------------------------------------------------- /test/tests/arcTo2.js: -------------------------------------------------------------------------------- 1 | export default function arcTo(ctx) { 2 | ctx.beginPath(); 3 | ctx.moveTo(100, 225); // P0 4 | ctx.arcTo(300, 25, 500, 225, 75); // P1, P2 and the radius 5 | ctx.lineTo(500, 225); // P2 6 | ctx.stroke(); 7 | }; -------------------------------------------------------------------------------- /test/tests/arcToScaled.js: -------------------------------------------------------------------------------- 1 | export default function arcToScaled(ctx) { 2 | ctx.scale(2, 0.5); 3 | ctx.beginPath(); 4 | ctx.moveTo(100, 50); 5 | ctx.arcTo(150, 50, 150, 100, 50); 6 | ctx.arcTo(150, 150, 100, 150, 50); 7 | ctx.arcTo(50, 150, 50, 100, 50); 8 | ctx.arcTo(50, 50, 100, 50, 50); 9 | 10 | // Reset the scale before we stroke since SVG stroke is not scaled. 11 | ctx.setTransform(1, 0, 0, 1, 0, 0); 12 | ctx.stroke(); 13 | }; -------------------------------------------------------------------------------- /test/tests/ellipse.js: -------------------------------------------------------------------------------- 1 | export default function ellipse(ctx) { 2 | // Draw shapes 3 | for (let i = 0; i < 4; i++) { 4 | for (let j = 0; j < 3; j++) { 5 | ctx.beginPath(); 6 | var x = 25 + j * 50; // x coordinate 7 | var y = 25 + i * 50; // y coordinate 8 | var radiusX = 20; // Arc radius 9 | var radiusY = 10; // Arc radius 10 | var rotation = Math.PI + (Math.PI * (i+j)) / 8; 11 | var startAngle = 0; // Starting point on circle 12 | var endAngle = Math.PI + (Math.PI * j) / 2; // End point on circle 13 | var clockwise = i % 2 == 0 ? false : true; // clockwise or anticlockwise 14 | 15 | ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, clockwise); 16 | 17 | if (i > 1) { 18 | ctx.fill(); 19 | } else { 20 | ctx.stroke(); 21 | } 22 | } 23 | } 24 | 25 | }; -------------------------------------------------------------------------------- /test/tests/ellipse2.js: -------------------------------------------------------------------------------- 1 | 2 | export default function ellipse2(ctx) { 3 | // Draw a cylinder using ellipses and lines. 4 | var w = 100, h = 100, rx = 50, ry = 10; 5 | var scaleX = 1.5, scaleY = 1.2; 6 | 7 | ctx.rotate(Math.PI / 10); 8 | ctx.scale(scaleX, scaleY); 9 | ctx.translate(200, 25); 10 | 11 | ctx.beginPath(); 12 | ctx.moveTo(-w / 2, -h / 2 + ry); 13 | // upper arc top 14 | ctx.ellipse(0, -h / 2 + ry, rx, ry, Math.PI, 0, Math.PI, 0); 15 | ctx.moveTo(-w / 2, -h / 2 + ry); 16 | // upper arc bottom 17 | ctx.ellipse(0, -h / 2 + ry, rx, ry, Math.PI, 0, Math.PI, 1); 18 | ctx.moveTo(-w / 2, -h / 2 + ry); 19 | // left line 20 | ctx.lineTo(-w / 2, + h / 2 - ry); 21 | // lower arc 22 | ctx.ellipse(0, h / 2 - ry, rx, ry, Math.PI, 0, Math.PI, 1); 23 | // right line 24 | ctx.lineTo(w / 2, -h / 2 + ry); 25 | ctx.moveTo(-w / 2, -h / 2 + ry); 26 | ctx.closePath(); 27 | 28 | // Remove scale before stroking because the SVG conversion is not correctly 29 | // scaling the stroke as well. Without this the pixel differences are too 30 | // high. 31 | ctx.resetTransform(); 32 | ctx.stroke(); 33 | }; -------------------------------------------------------------------------------- /test/tests/emptyArc.js: -------------------------------------------------------------------------------- 1 | export default function emptyArc(ctx) { 2 | 3 | // Draw shapes 4 | for (let i = 0; i < 4; i++) { 5 | for (let j = 0; j < 3; j++) { 6 | ctx.beginPath(); 7 | ctx.arc(100, 100, 100, Math.PI, Math.PI); 8 | ctx.fill(); 9 | } 10 | } 11 | 12 | }; 13 | -------------------------------------------------------------------------------- /test/tests/fillstyle.js: -------------------------------------------------------------------------------- 1 | export default function fillStyle(ctx) { 2 | for (var i = 0; i < 6; i++) { 3 | for (var j = 0; j < 6; j++) { 4 | ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + 5 | Math.floor(255 - 42.5 * j) + ',0)'; 6 | ctx.fillRect(j * 25, i * 25, 25, 25); 7 | } 8 | } 9 | }; -------------------------------------------------------------------------------- /test/tests/globalalpha.js: -------------------------------------------------------------------------------- 1 | export default function globalAlpha(ctx) { 2 | ctx.fillStyle = '#FD0'; 3 | ctx.fillRect(0,0,75,75); 4 | ctx.fillStyle = '#6C0'; 5 | ctx.fillRect(75,0,75,75); 6 | ctx.fillStyle = '#09F'; 7 | ctx.fillRect(0,75,75,75); 8 | ctx.fillStyle = '#F30'; 9 | ctx.fillRect(75,75,75,75); 10 | ctx.fillStyle = '#FFF'; 11 | 12 | // set transparency value 13 | ctx.globalAlpha = 0.2; 14 | 15 | // Draw semi transparent circles 16 | for (let i=0;i<7;i++){ 17 | ctx.beginPath(); 18 | ctx.arc(75,75,10+10*i,0,Math.PI*2,true); 19 | ctx.fill(); 20 | } 21 | 22 | ctx.globalAlpha = 1.0; 23 | }; -------------------------------------------------------------------------------- /test/tests/gradient.js: -------------------------------------------------------------------------------- 1 | export default function gradient(ctx) { 2 | ctx.save(); 3 | ctx.strokeStyle='rgba(0,0,0,0)'; 4 | ctx.lineCap='butt'; 5 | ctx.lineJoin='miter'; 6 | ctx.miterLimit=10.0; 7 | ctx.font='10px sans-serif'; 8 | ctx.save(); 9 | var radialGradient_1389130830351 = ctx.createRadialGradient(6E1,6E1,0.0,6E1,6E1,5E1); 10 | radialGradient_1389130830351.addColorStop(0E0,'red'); 11 | radialGradient_1389130830351.addColorStop(1E0,'blue'); 12 | ctx.fillStyle=radialGradient_1389130830351; 13 | ctx.font='10px sans-serif'; 14 | ctx.beginPath(); 15 | ctx.moveTo(2.5E1,1E1); 16 | ctx.lineTo(9.5E1,1E1); 17 | ctx.quadraticCurveTo(1.1E2,1E1,1.1E2,2.5E1); 18 | ctx.lineTo(1.1E2,9.5E1); 19 | ctx.quadraticCurveTo(1.1E2,1.1E2,9.5E1,1.1E2); 20 | ctx.lineTo(2.5E1,1.1E2); 21 | ctx.quadraticCurveTo(1E1,1.1E2,1E1,9.5E1); 22 | ctx.lineTo(1E1,2.5E1); 23 | ctx.quadraticCurveTo(1E1,1E1,2.5E1,1E1); 24 | ctx.closePath(); 25 | ctx.fill(); 26 | ctx.stroke(); 27 | ctx.restore(); 28 | ctx.save(); 29 | var radialGradient_1389130830351 = ctx.createRadialGradient(3.5E1,1.45E2,0.0,3.5E1,1.45E2,2.5E1); 30 | radialGradient_1389130830351.addColorStop(0E0,'red'); 31 | radialGradient_1389130830351.addColorStop(1E0,'blue'); 32 | ctx.fillStyle=radialGradient_1389130830351; 33 | ctx.font='10px sans-serif'; 34 | ctx.beginPath(); 35 | ctx.moveTo(2.5E1,1.2E2); 36 | ctx.lineTo(9.5E1,1.2E2); 37 | ctx.quadraticCurveTo(1.1E2,1.2E2,1.1E2,1.35E2); 38 | ctx.lineTo(1.1E2,2.05E2); 39 | ctx.quadraticCurveTo(1.1E2,2.2E2,9.5E1,2.2E2); 40 | ctx.lineTo(2.5E1,2.2E2); 41 | ctx.quadraticCurveTo(1E1,2.2E2,1E1,2.05E2); 42 | ctx.lineTo(1E1,1.35E2); 43 | ctx.quadraticCurveTo(1E1,1.2E2,2.5E1,1.2E2); 44 | ctx.closePath(); 45 | ctx.fill(); 46 | ctx.stroke(); 47 | ctx.restore(); 48 | ctx.restore(); 49 | }; 50 | -------------------------------------------------------------------------------- /test/tests/linecap.js: -------------------------------------------------------------------------------- 1 | export default function linecap(ctx) { 2 | var lineCap = ['butt','round','square']; 3 | 4 | // Draw guides 5 | ctx.strokeStyle = '#09f'; 6 | ctx.beginPath(); 7 | ctx.moveTo(10,10); 8 | ctx.lineTo(140,10); 9 | ctx.moveTo(10,140); 10 | ctx.lineTo(140,140); 11 | ctx.stroke(); 12 | 13 | // Draw lines 14 | ctx.strokeStyle = 'black'; 15 | for (let i=0;i