Full Code of fskpf/svg2roughjs for AI

master fa88f518e2d6 cached
164 files
226.6 KB
67.6k tokens
143 symbols
1 requests
Download .txt
Showing preview only (266K chars total). Download the full file or copy to clipboard to get everything.
Repository: fskpf/svg2roughjs
Branch: master
Commit: fa88f518e2d6
Files: 164
Total size: 226.6 KB

Directory structure:
gitextract_3crykq0g/

├── .github/
│   └── ISSUE_TEMPLATE/
│       ├── bug_report.md
│       └── feature_request.md
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .vscode/
│   ├── launch.json
│   └── settings.json
├── CODE_OF_CONDUCT.md
├── LICENSE.md
├── README.md
├── eslint.config.js
├── nodejs-cli/
│   ├── README.md
│   ├── package.json
│   └── src/
│       ├── svg2roughjs-page.js
│       └── svg2roughjs.js
├── package.json
├── rollup.config.js
├── sample-application/
│   ├── index.html
│   ├── package.json
│   ├── src/
│   │   ├── assets/
│   │   │   └── styles.css
│   │   ├── index.ts
│   │   ├── testing.ts
│   │   ├── types.d.ts
│   │   ├── utils.ts
│   │   └── vite-env.d.ts
│   ├── tsconfig.json
│   └── vite.config.js
├── src/
│   ├── OutputType.ts
│   ├── RandomNumberGenerator.ts
│   ├── Svg2Roughjs.ts
│   ├── clipping.ts
│   ├── dom-helpers.ts
│   ├── geom/
│   │   ├── circle.ts
│   │   ├── ellipse.ts
│   │   ├── foreign-object.ts
│   │   ├── image.ts
│   │   ├── line.ts
│   │   ├── marker.ts
│   │   ├── path.ts
│   │   ├── polygon.ts
│   │   ├── polyline.ts
│   │   ├── primitives.ts
│   │   ├── rect.ts
│   │   ├── text.ts
│   │   └── use.ts
│   ├── index.ts
│   ├── processor.ts
│   ├── styles/
│   │   ├── colors.ts
│   │   ├── effective-attributes.ts
│   │   ├── pattern.ts
│   │   ├── pens.ts
│   │   ├── styles.ts
│   │   └── textures.ts
│   ├── svg-units.ts
│   ├── transformation.ts
│   ├── types.ts
│   └── utils.ts
├── test/
│   ├── complex/
│   │   ├── bpmn-diagram/
│   │   │   └── config.json
│   │   ├── computer-network-diagram/
│   │   │   └── config.json
│   │   ├── flowchart-diagram/
│   │   │   └── config.json
│   │   ├── hierarchical1-diagram/
│   │   │   └── config.json
│   │   ├── hierarchical2-diagram/
│   │   │   └── config.json
│   │   ├── mindmap-diagram/
│   │   │   └── config.json
│   │   ├── movies-diagram/
│   │   │   └── config.json
│   │   ├── organic1-diagram/
│   │   │   └── config.json
│   │   ├── organic2-diagram/
│   │   │   └── config.json
│   │   ├── tree-diagram/
│   │   │   └── config.json
│   │   └── venn-diagram/
│   │       └── config.json
│   ├── runner/
│   │   ├── complex.test.js
│   │   ├── spec.test.js
│   │   └── utils.js
│   ├── specs/
│   │   ├── circle-transform/
│   │   │   └── config.json
│   │   ├── clippath-circle/
│   │   │   └── config.json
│   │   ├── clippath-circle-transformed/
│   │   │   └── config.json
│   │   ├── clippath-ellipse/
│   │   │   └── config.json
│   │   ├── clippath-ellipse-transformed/
│   │   │   └── config.json
│   │   ├── clippath-g-element/
│   │   │   └── config.json
│   │   ├── clippath-path/
│   │   │   └── config.json
│   │   ├── clippath-path-transformed/
│   │   │   └── config.json
│   │   ├── clippath-polygon/
│   │   │   └── config.json
│   │   ├── clippath-rect/
│   │   │   └── config.json
│   │   ├── clippath-rect-rounded/
│   │   │   └── config.json
│   │   ├── clippath-rect-rounded-transformed/
│   │   │   └── config.json
│   │   ├── clippath-rect-rounded-transformed2/
│   │   │   └── config.json
│   │   ├── clippath-rect-text/
│   │   │   └── config.json
│   │   ├── clippath-rect-transformed/
│   │   │   └── config.json
│   │   ├── clipped-text-scaling/
│   │   │   └── config.json
│   │   ├── css-units/
│   │   │   └── config.json
│   │   ├── dotted-stroke/
│   │   │   └── config.json
│   │   ├── ellipse-transform/
│   │   │   └── config.json
│   │   ├── fill-attribute/
│   │   │   └── config.json
│   │   ├── fill-attribute-ancestor-g/
│   │   │   └── config.json
│   │   ├── fill-attribute-ancestor-svg/
│   │   │   └── config.json
│   │   ├── fill-css-attribute-precedence/
│   │   │   └── config.json
│   │   ├── fill-css-attribute-precedence2/
│   │   │   └── config.json
│   │   ├── fill-css-class/
│   │   │   └── config.json
│   │   ├── fill-css-inline/
│   │   │   └── config.json
│   │   ├── fill-css-selector/
│   │   │   └── config.json
│   │   ├── fill-missing/
│   │   │   └── config.json
│   │   ├── foreign-object-mermaid/
│   │   │   └── config.json
│   │   ├── icons/
│   │   │   └── config.json
│   │   ├── markers/
│   │   │   └── config.json
│   │   ├── markers-fixed-orientation/
│   │   │   └── config.json
│   │   ├── markers-line/
│   │   │   └── config.json
│   │   ├── markers-on-line/
│   │   │   └── config.json
│   │   ├── markers-paths/
│   │   │   └── config.json
│   │   ├── markers-polygon/
│   │   │   └── config.json
│   │   ├── markers-polyline/
│   │   │   └── config.json
│   │   ├── nested-svg-translate/
│   │   │   └── config.json
│   │   ├── path-transform/
│   │   │   └── config.json
│   │   ├── path-transform2/
│   │   │   └── config.json
│   │   ├── pattern-circle/
│   │   │   └── config.json
│   │   ├── pattern-ellipse/
│   │   │   └── config.json
│   │   ├── pattern-line/
│   │   │   └── config.json
│   │   ├── pattern-path/
│   │   │   └── config.json
│   │   ├── pattern-polygon/
│   │   │   └── config.json
│   │   ├── pattern-polyline/
│   │   │   └── config.json
│   │   ├── pattern-rect/
│   │   │   └── config.json
│   │   ├── rect-not-rounded/
│   │   │   └── config.json
│   │   ├── rect-plain/
│   │   │   └── config.json
│   │   ├── rect-rounded-large-rx/
│   │   │   └── config.json
│   │   ├── rect-rounded-large-rx-ry/
│   │   │   └── config.json
│   │   ├── rect-rounded-large-ry/
│   │   │   └── config.json
│   │   ├── rect-rounded-rx/
│   │   │   └── config.json
│   │   ├── rect-rounded-rx-ry/
│   │   │   └── config.json
│   │   ├── rect-rounded-ry/
│   │   │   └── config.json
│   │   ├── rect-rounded-transform/
│   │   │   └── config.json
│   │   ├── rect-rounded-transform-mirror/
│   │   │   └── config.json
│   │   ├── rect-transform/
│   │   │   └── config.json
│   │   ├── rect-transform-from-g/
│   │   │   └── config.json
│   │   ├── stroke-attribute/
│   │   │   └── config.json
│   │   ├── stroke-attribute-ancestor-g/
│   │   │   └── config.json
│   │   ├── stroke-attribute-ancestor-g-override/
│   │   │   └── config.json
│   │   ├── stroke-attribute-ancestor-g2/
│   │   │   └── config.json
│   │   ├── stroke-attribute-ancestor-svg/
│   │   │   └── config.json
│   │   ├── stroke-missing-is-transparent/
│   │   │   └── config.json
│   │   ├── stroke-none-is-transparent/
│   │   │   └── config.json
│   │   ├── stroke-width-attribute/
│   │   │   └── config.json
│   │   ├── stroke-width-scale-transform/
│   │   │   └── config.json
│   │   ├── svg-image-element/
│   │   │   └── config.json
│   │   ├── symbols/
│   │   │   └── config.json
│   │   ├── symbols-non-uniform-scale/
│   │   │   └── config.json
│   │   ├── symbols2/
│   │   │   └── config.json
│   │   ├── text-css/
│   │   │   └── config.json
│   │   ├── text-dominant-baseline-basic/
│   │   │   └── config.json
│   │   ├── text-glyph-positioning/
│   │   │   └── config.json
│   │   ├── text-rotated-glyphs/
│   │   │   └── config.json
│   │   ├── text-simple-tspans/
│   │   │   └── config.json
│   │   ├── text-stroked-and-decorated/
│   │   │   └── config.json
│   │   ├── text-tspan-styling/
│   │   │   └── config.json
│   │   ├── text-tspans-mixed/
│   │   │   └── config.json
│   │   ├── text-tspans-repositioned/
│   │   │   └── config.json
│   │   ├── text-whitespace/
│   │   │   └── config.json
│   │   ├── text-width-custom-font/
│   │   │   └── config.json
│   │   ├── uml-node-style/
│   │   │   └── config.json
│   │   ├── use-element-styling/
│   │   │   └── config.json
│   │   ├── use-reference-group/
│   │   │   └── config.json
│   │   ├── viewbox-negative/
│   │   │   └── config.json
│   │   ├── viewbox-non-uniform/
│   │   │   └── config.json
│   │   └── viewbox-non-uniform-translated/
│   │       └── config.json
│   ├── tests.js
│   └── umd-bundle/
│       └── umd-bundle-test.html
├── tsconfig.json
└── web-test-runner.config.mjs

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
An SVG snippet that is not sketched correctly or creates an unexpected result.

**Expected behavior**
A clear and concise description of what you expected to happen.

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .gitignore
================================================
node_modules
sample-application/dist
svg2roughjs-*.tgz
.idea
dist
debug.log
out-tsc
coverage
/nodejs-cli/*.svg

================================================
FILE: .prettierignore
================================================
bundled/*
dist/*
out-tsc/*

================================================
FILE: .prettierrc
================================================
{
  "tabWidth": 2,
  "useTabs": false,
  "semi": false,
  "singleQuote": true,
  "printWidth": 100,
  "endOfLine": "auto",
  "arrowParens": "avoid",
  "trailingComma": "none"
}


================================================
FILE: .vscode/launch.json
================================================
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "chrome",
            "request": "launch",
            "name": "Launch Chrome against localhost",
            "url": "http://localhost:8080",
            "webRoot": "${workspaceFolder}"
        }
    ]
}

================================================
FILE: .vscode/settings.json
================================================
{
  "eslint.validate": ["typescript"]
}


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
  and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall
  community

Examples of unacceptable behavior include:

- The use of sexualized language or imagery, and sexual attention or advances of
  any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address,
  without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
fskopf@gmail.com.
All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.

## Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

### 2. Warning

**Community Impact**: A violation through a single incident or series of
actions.

**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction within the
community.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].

Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][mozilla coc].

For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][faq]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].

[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[mozilla coc]: https://github.com/mozilla/diversity
[faq]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations


================================================
FILE: LICENSE.md
================================================
Copyright 2022 Fabian Schwarzkopf, Johannes Rössel

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
# svg2rough.js

<p align="center">
  <img src="https://fskpf.github.io/static/assets/svg2roughjs-hero-sketch.svg" alt="hero-image">
</p>

<p align="center">
    <a href="https://github.com/fskpf/svg2roughjs"><img src="https://img.shields.io/github/stars/fskpf/svg2roughjs?style=for-the-badge&logo=github&color=%23eac54f" alt="npm"></a>
    <a href="https://github.com/fskpf/svg2roughjs/blob/master/LICENSE.md"><img src="https://img.shields.io/github/license/fskpf/svg2roughjs?style=for-the-badge&logo=github" alt="github"></a>
    <a href="https://www.npmjs.com/package/svg2roughjs" target="_blank"><img src="https://img.shields.io/npm/dt/svg2roughjs?style=for-the-badge&logo=npm" alt="npm"></a>
    <a href="https://www.npmjs.com/package/svg2roughjs" target="_blank"><img src="https://img.shields.io/npm/v/svg2roughjs?style=for-the-badge&logo=npm" alt="npm"></a>
</p>

Utilizes [Rough.js](https://github.com/pshihn/rough) to convert an SVG to a hand-drawn visualization.

Try the sample application [here](https://fskpf.github.io/).

## NPM

Install from the NPM registry with

```shell
npm install --save svg2roughjs
```

## Usage

Just import `Svg2Roughjs` and instantiate it with an output div in which the
SVG sketch should be created. Calling `sketch()` outputs the current `svg` to the given element
as hand-drawn sketch.

For reference, a [sample application](https://fskpf.github.io/) is provided in
[`/sample-application/`](https://github.com/fskpf/svg2roughjs/tree/master/sample-application).

### ES Module

```javascript
import { Svg2Roughjs } from 'svg2roughjs'

const svg2roughjs = new Svg2Roughjs('#output-div')
svg2roughjs.svg = document.getElementById('input-svg')
svg2roughjs.sketch()
```

### UMD Bundle

An UMD bundle that ca be loaded via script tags or a module loader e.g.
[RequireJS](https://requirejs.org/) is included in the NPM package or can
be loaded from [unpkg](https://unpkg.com/):

```
https://unpkg.com/svg2roughjs/dist/svg2roughjs.umd.min.js
```

```javascript
<!-- script loading -->
<script src="https://unpkg.com/svg2roughjs/dist/svg2roughjs.umd.min.js"></script>
<script>
  const svgConverter = new svg2roughjs.Svg2Roughjs('#output-div')
  svgConverter.svg = document.getElementById('input-svg')
  svgConverter.sketch()
</script>
```

```javascript
<!-- requirejs -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
<script>
  window.requirejs(['https://unpkg.com/svg2roughjs/dist/svg2roughjs.umd.min.js'], svg2roughjs => {
    const svgConverter = new svg2roughjs.Svg2Roughjs('#output-div')
    svgConverter.svg = document.getElementById('input-svg')
    svgConverter.sketch()
  });
</script>
```

## API

### Module Exports

- `Svg2Roughjs`: The main class for the conversion.
- `OutputType`: An enum that is used to switch between `SVG` and `CANVAS` output when targetting an `HTMLDivElement` as output container.

### Methods

- `constructor(target, outputType?, roughConfig?)`

  Creates a new Svg2Rough.js instance.

  `target` may either be a container `HTMLDivElement` (or a selector for the container) into which a new sketch should be created, or directly an `SVGSVGElement` or `HTMLCanvasElement` that should be used for the output.

  The optional `outputType` defaults to the respective mode if `target` is either `SVGSVGElement` or `HTMLCanvasElement`. If targetting an HTML container element, then `OutputType.SVG` is used by default.

- `sketch(sourceSvgChanged = false): Promise<SVGSVGElement | HTMLCanvasElement | null>`

  Clears the configured `target` element and converts the current `svg2roughj.svg` to a hand-drawn sketch.

  Setting `sourceSvgChanged` to `true` re-evaluates the given `svg2roughj.svg` as it was set anew. May be used to re-use the same Svg2Rough.js instance and the same SVG element as source container.

### Properties

| Property          | Description                                                                                                                       | Default                    |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
| `svg`             | The given `SVGSVGElement` that should be converted.                                                                               | `undefined`                |
| `outputType`      | Switch between canvas or SVG output.                                                                                              | `OutputType.SVG`           |
| `roughConfig`     | [Rough.js options](https://github.com/rough-stuff/rough/wiki#options) object, e.g. to change the fill-style, roughness or bowing. | `{}`                       |
| `fontFamily`      | Font with which text elements should be drawn.<br>If set to `null`, the text element's original font-family is used.              | `'Comic Sans MS, cursive'` |
| `backgroundColor` | Sets a background color onto which the sketch is drawn.                                                                           | `transparent`              |
| `randomize`       | Randomize Rough.js' fillWeight, hachureAngle and hachureGap.                                                                      | `true`                     |
| `seed`            | Optional, numerical seed for the randomness when creating the sketch.                                                             | `null`                     |
| `sketchPatterns`  | Whether to sketch pattern fills/strokes or just copy them to the output                                                           | `true`                     |
| `pencilFilter`    | Applies a pencil effect on the SVG rendering.                                                                                     | `false`                    |

## Sample Images

Some sample images with different Svg2rough.js settings. Try it yourself [here](https://fskpf.github.io/).

| SVG                                                                                                                                                         | Sketch                                                                                                         |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| <img src="https://fskpf.github.io/static/sample-images/bpmn1.svg" width="400px"><br>(created with [yEd Live](https://www.yworks.com/yed-live))              | <img src="https://fskpf.github.io/static/sample-images/bpmn1_sketch.svg" width="400px"><br>&nbsp;              |
| &nbsp;                                                                                                                                                      | &nbsp;                                                                                                         |
| <img src="https://fskpf.github.io/static/sample-images/hierarchic_diagram.svg" width="400px"><br>(created with [yEd Live](https://www.yworks.com/yed-live)) | <img src="https://fskpf.github.io/static/sample-images/hierarchic_diagram_sketch.svg" width="400px"><br>&nbsp; |
| &nbsp;                                                                                                                                                      | &nbsp;                                                                                                         |
| <img src="https://fskpf.github.io/static/sample-images/flowchart.svg" width="400px"><br>(created with [yEd Live](https://www.yworks.com/yed-live))          | <img src="https://fskpf.github.io/static/sample-images/flowchart_sketch.svg" width="400px"><br>&nbsp;          |
| &nbsp;                                                                                                                                                      | &nbsp;                                                                                                         |
| <img src="https://fskpf.github.io/static/sample-images/chirality.svg" width="400px">                                                                        | <img src="https://fskpf.github.io/static/sample-images/chirality_sketch.svg" width="400px">                    |
| &nbsp;                                                                                                                                                      | &nbsp;                                                                                                         |
| <img src="https://fskpf.github.io/static/sample-images/comic_boy.svg" width="400px">                                                                        | <img src="https://fskpf.github.io/static/sample-images/comic_boy_sketch.svg" width="400px">                    |
| &nbsp;                                                                                                                                                      | &nbsp;                                                                                                         |
| <img src="https://fskpf.github.io/static/sample-images/mars_rover_blueprint.svg" width="400px">                                                             | <img src="https://fskpf.github.io/static/sample-images/mars_rover_sketch.svg" width="400px">                   |

## Local Build

To build the project locally, make sure to have [Node.js](https://nodejs.org/en) installed and run

```
> npm install
> npm run build
```

This creates a local `/dist/` folder containing both, the ES module and UMD build of `svg2roughjs`.

### Tests

To perform all tests on the current build, run

```
npm run test-all
```

This converts all given tests in [`/test/`](https://github.com/fskpf/svg2roughjs/tree/master/test) and
compares the output SVG with the expected string. Each test contains a configuration file with different
settings and a fixed seed to account for the randomness in the sketched output.

## Credits

- [Rough.js](https://github.com/pshihn/rough) – Draws the hand-drawn elements
- [svg-pathdata](https://github.com/nfroidure/svg-pathdata) – Parses SVGPathElements
- [TinyColor](https://github.com/bgrins/TinyColor) – Color manipulation

## License

[MIT License](https://github.com/fskpf/svg2roughjs/blob/master/LICENSE.md) © Fabian Schwarzkopf and Johannes Rössel


================================================
FILE: eslint.config.js
================================================
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import prettier from 'eslint-plugin-prettier/recommended'

export default [
  js.configs.recommended,
  ...tseslint.configs.recommended,
  prettier,
  {
    rules: {
      '@typescript-eslint/no-non-null-assertion': 'off',
      '@typescript-eslint/no-inferrable-types': 'off',
      '@typescript-eslint/ban-ts-comment': 'off',
      '@typescript-eslint/no-var-requires': 'off',
      // note you must disable the base rule
      // as it can report incorrect errors
      'no-unused-vars': 'off',
      '@typescript-eslint/no-unused-vars': [
        'error',
        {
          argsIgnorePattern: '_',
          varsIgnorePattern: '_',
          caughtErrorsIgnorePattern: '_'
        }
      ]
    },
    ignores: ['test/**'],
    files: ['**/*.ts']
  }
]


================================================
FILE: nodejs-cli/README.md
================================================
# CLI

Create sketchs of an SVG on the command-line with [Node.js](https://nodejs.org/)
and [Puppeteer](https://pptr.dev/).

## Usage

In `/nodejs-cli/`

```
> npm install
> node src/svg2roughjs ../test/complex/bpmn-diagram/test.svg -o ./test.svg
```

## CLI Parameters

Pass the SVG file that should be converted as first parameter.

Additionally, the following optional options are available:

| Parameter                        | Description                                | Default                                    |
| -------------------------------- | ------------------------------------------ | ------------------------------------------ |
| `-o`<br>`--output`               | Output path for the resulting sketch.      | `'./sketch.svg'`                           |
| `--roughConfig.ROUGHJS_PROPERTY` | see [svg2roughjs](../README.md#properties) | see [svg2roughjs](../README.md#properties) |
| `--fontFamily`                   | see [svg2roughjs](../README.md#properties) | see [svg2roughjs](../README.md#properties) |
| `--backgroundColor`              | see [svg2roughjs](../README.md#properties) | see [svg2roughjs](../README.md#properties) |
| `--randomize`                    | see [svg2roughjs](../README.md#properties) | see [svg2roughjs](../README.md#properties) |
| `--seed`                         | see [svg2roughjs](../README.md#properties) | see [svg2roughjs](../README.md#properties) |
| `--sketchPatterns`               | see [svg2roughjs](../README.md#properties) | see [svg2roughjs](../README.md#properties) |
| `--pencilFilter`                 | see [svg2roughjs](../README.md#properties) | see [svg2roughjs](../README.md#properties) |

For example:

```
node src/svg2roughjs ../test/complex/bpmn-diagram/test.svg -o ./test.svg --roughConfig.roughness 1.5 --roughConfig.bowing 2 --backgroundColor white --fontFamily null
```

## Credits

- [Puppeteer](https://pptr.dev/) - Headless browser for Node.js in which the conversion is done


================================================
FILE: nodejs-cli/package.json
================================================
{
  "name": "svg2roughjs-nodejs",
  "version": "1.0.0",
  "description": "An svg2roughjs CLI that runs in Nodejs with a headless browser",
  "main": "svg2roughjs.js",
  "type": "module",
  "scripts": {
    "test": "node src/svg2roughjs ../test/complex/bpmn-diagram/test.svg -o ./test.svg --roughConfig.roughness 1.5 --roughConfig.bowing 2 --backgroundColor white"
  },
  "author": "Fabian Schwarzkopf",
  "license": "MIT",
  "dependencies": {
    "minimist": "^1.2.8",
    "puppeteer": "^24.37.5"
  }
}


================================================
FILE: nodejs-cli/src/svg2roughjs-page.js
================================================
/**
 * @param {string} inputSvg The SVG that should be converted
 * @param {Record<string,unknown>} svg2roughjsArgs The SVG that should be converted
 * @returns {string} HTML content of the converter page
 */
export function svg2roughjsPage(inputSvg, svg2roughjsArgs) {
  return `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>svg2roughjs</title>
  <script src="https://unpkg.com/svg2roughjs/dist/svg2roughjs.umd.min.js"></script>
</head>
<body>
  <div id="input-svg-container">${inputSvg}</div>
  <div id="output-container"></div>
  <script type="module">
    const { Svg2Roughjs, OutputType } = svg2roughjs
    ${createSvg2RoughjsInstance(svg2roughjsArgs)}
    svgConverter.svg = document.querySelector('#input-svg-container > svg')
    await svgConverter.sketch()
  </script>
</body>
</html>
`
}

/**
 * @param {Record<string, unknown>} args
 * @returns {string}
 */
function createSvg2RoughjsInstance(args) {
  let instance = "const svgConverter = new Svg2Roughjs('#output-container', OutputType.SVG)"
  for (const [key, value] of Object.entries(args)) {
    if (typeof value === 'string') {
      instance += `\nsvgConverter.${key} = '${value}'`
    } else if (typeof value === 'object') {
      instance += `\nsvgConverter.${key} = ${JSON.stringify(value)}`
    } else {
      instance += `\nsvgConverter.${key} = ${value}`
    }
  }
  return instance
}


================================================
FILE: nodejs-cli/src/svg2roughjs.js
================================================
import puppeteer from 'puppeteer'
import minimist from 'minimist'
import fs from 'fs'
import { svg2roughjsPage } from './svg2roughjs-page.js'
;(async () => {
  const argv = minimist(process.argv.slice(2), {
    string: ['backgroundColor', 'fontFamily'],
    boolean: ['randomize', 'sketchPatterns', 'pencilFilter'],
    alias: {
      o: 'output'
    }
  })

  const inputSvg = loadInputSvg(argv)

  const browser = await puppeteer.launch({ headless: 'new' })
  const page = await browser.newPage()

  // load the input SVG as part of the HTML file and run svg2roughjs on the input
  const svg2roughjsArgs = getSvg2RoughjsArgs(argv)
  await page.setContent(svg2roughjsPage(inputSvg, svg2roughjsArgs))

  // get the sketch from the DOM
  const sketch = await page.$eval('#output-container > svg', el => el.outerHTML)

  const outputFilePath = argv.output
  saveSketch(sketch, outputFilePath)

  // for debugging, disable close and use headless: false
  await browser.close()
})()

/**
 * Loads the input SVG from the CLI.
 * @param {object} argv The CLI arguments
 * @returns {string} The content of the input file
 */
function loadInputSvg(argv) {
  const inputFile = argv._[0]
  if (!inputFile) {
    throw new Error('No input file provided. Please pass the input SVG as first parameter.')
  }

  if (!fs.existsSync(inputFile)) {
    throw new Error(`File "${inputFile}" does not exist.`)
  }

  const content = fs.readFileSync(inputFile, 'utf8')

  // TODO validate as SVG?

  return content
}

/**
 * Downloads the sketch as file.
 * @param {string} content The SVG string of the sketch
 * @param {string?} outputFilePath The file path to save the output to.
 */
function saveSketch(content, outputFilePath) {
  if (!content) {
    throw new Error('Could not save file, no sketch given.')
  }
  // TODO validate SVG?

  fs.writeFileSync(outputFilePath ?? './sketch.svg', content)
}

function getSvg2RoughjsArgs(argv) {
  const args = { ...argv }
  // remove arguments for the CLI
  delete args._
  delete args.o
  delete args.output

  return Object.fromEntries(
    Object.entries(args).map(([key, value]) => {
      // replace null arguments with the actual null value
      if (value === 'null') {
        return [key, null]
      }
      return [key, value]
    })
  )
}


================================================
FILE: package.json
================================================
{
  "name": "svg2roughjs",
  "version": "3.2.3",
  "description": "Leverages Rough.js to convert SVGs to a hand-drawn, sketchy representation",
  "author": "Fabian Schwarzkopf",
  "contributors": [
    "Johannes Rössel"
  ],
  "type": "module",
  "main": "dist/svg2roughjs.umd.min.js",
  "browser": "dist/svg2roughjs.es.min.js",
  "module": "dist/svg2roughjs.es.min.js",
  "types": "dist/index.d.ts",
  "homepage": "https://github.com/fskpf/svg2roughjs#readme",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/fskpf/svg2roughjs.git"
  },
  "keywords": [
    "svg",
    "roughjs",
    "javascript",
    "hand-drawn",
    "sketch"
  ],
  "license": "MIT",
  "scripts": {
    "prepare": "npm run lint && npm run build",
    "build": "npm run clean && tsc && rollup -c rollup.config.js",
    "clean": "rimraf -g *.tgz && rimraf dist/ && rimraf out-tsc/",
    "lint": "eslint ./src",
    "tsc": "tsc --noEmit",
    "test-all": "npm run clean && tsc && wtr"
  },
  "files": [
    "dist/*",
    "LICENSE.md",
    "README.md"
  ],
  "dependencies": {
    "roughjs": "^4.6.6",
    "svg-pathdata": "^8.0.0",
    "tinycolor2": "^1.6.0"
  },
  "devDependencies": {
    "@eslint/js": "^10.0.1",
    "@open-wc/testing": "4.0.0",
    "@rollup/plugin-commonjs": "^29.0.0",
    "@rollup/plugin-node-resolve": "^16.0.3",
    "@rollup/plugin-terser": "^1.0.0",
    "@types/node": "^25.3.2",
    "@types/tinycolor2": "^1.4.6",
    "@web/dev-server-rollup": "0.6.4",
    "@web/test-runner": "0.18.3",
    "eslint": "^10.0.2",
    "eslint-config-prettier": "^10.1.8",
    "eslint-plugin-prettier": "^5.5.5",
    "prettier": "3.8.1",
    "rimraf": "^6.1.3",
    "rollup": "^4.59.0",
    "rollup-plugin-dts": "^6.3.0",
    "typescript": "^5.9.3",
    "typescript-eslint": "^8.56.1"
  }
}


================================================
FILE: rollup.config.js
================================================
import commonjs from '@rollup/plugin-commonjs'
import resolve from '@rollup/plugin-node-resolve'
import dts from 'rollup-plugin-dts'
import terser from '@rollup/plugin-terser'
import fs from 'fs'

const pkg = JSON.parse(fs.readFileSync('./package.json'))

function matchSubmodules(externals) {
  return externals.map(e => new RegExp(`^${e}(?:[/\\\\]|$)`))
}

const externalsUmd = matchSubmodules([
  ...Object.keys(pkg.peerDependencies || {}),
  ...Object.keys(pkg.optionalDependencies || {})
])
const externals = matchSubmodules([...Object.keys(pkg.dependencies || {}), ...externalsUmd])

const es = {
  input: 'out-tsc/index.js',
  output: [
    {
      file: pkg.module.replace('.min', ''),
      format: 'es',
      name: 'svg2roughjs',
      sourcemap: true,
      plugins: []
    },
    {
      file: pkg.module,
      format: 'es',
      name: 'svg2roughjs',
      sourcemap: true,
      plugins: [terser({})]
    }
  ],
  external: externals,
  plugins: []
}

const umd = {
  input: 'out-tsc/index.js',
  output: [
    {
      file: pkg.browser.replace('.es.min.', '.umd.'),
      format: 'umd',
      name: 'svg2roughjs',
      sourcemap: true,
      plugins: []
    },
    {
      file: pkg.browser.replace('.es.', '.umd.'),
      format: 'umd',
      name: 'svg2roughjs',
      sourcemap: true,
      plugins: [terser({})]
    }
  ],
  external: externalsUmd,
  plugins: [commonjs(), resolve()]
}

const typings = {
  input: 'out-tsc/index.d.ts',
  output: [{ file: 'dist/index.d.ts', format: 'es' }],
  plugins: [dts()]
}

export default [es, umd, typings]


================================================
FILE: sample-application/index.html
================================================
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <meta name="description"
    content="Convert your SVG file to a hand drawn sketch with Svg2Rough.js, a small open-source library.">
  <meta name="keywords" content="svg, conversion, hand drawn, sketch, open source">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>Svg2Rough.js - Convert Your SVG to a Hand Drawn Image</title>
</head>

<body>

  <div class="header">
    <img class="header-img" src="./static/assets/svg-logo.svg" alt="W3C SVG Logo">
    <div class="title">
      <h1><a href="https://github.com/fskpf/svg2roughjs" target="_blank">Svg2Rough.js</a></h1>
      <a class="subtitle" href="https://github.com/fskpf/svg2roughjs" target="_blank">on GitHub</a>
    </div>
    <img class="header-img" src="./static/assets/svg-logo-rough.png" alt="Sketchy W3C SVG Logo">
  </div>

  <div class="main">
    <div class="sidebar">
      <h2>Drawing Options</h2>

      <div class="separator" style="margin-top: 5px;"></div>

      <label for="fill-style">Fill Style</label>
      <select id="fill-style" style="margin: 2px 0;">
        <option value="hachure">hachure</option>
        <option value="solid">solid</option>
        <option value="zigzag">zigzag</option>
        <option value="cross-hatch">cross-hatch</option>
        <option value="dots">dots (excruciatingly slow)</option>
        <option value="dashed">dashed (somewhat slow)</option>
        <option value="zigzag-line">zigzag-line (slow)</option>
      </select>
      <div class="separator"></div>

      <label for="output-format">Output Format</label>
      <select id="output-format" style="margin: 2px 0;">
        <option value="svg">SVG</option>
        <option value="canvas">Canvas</option>
      </select>
      <div class="separator"></div>

      <label for="roughness-input">Roughness</label>
      <input id="roughness-input" type="range" value="1" min="0" max="4" step="0.5">
      <div class="separator"></div>
      <label for="bowing-input">Bowing</label>
      <input id="bowing-input" type="range" value="1" min="0" max="15" step="1">
      <div class="separator"></div>
      <div class="checkbox">
        <input id="original-font" type="checkbox">
        <label for="original-font">Use original font</label>
      </div>
      <div class="separator"></div>
      <div class="checkbox">
        <input id="randomize" type="checkbox" checked>
        <label for="randomize">Randomize hatching per shape</label>
      </div>
      <div class="separator"></div>
      <div class="checkbox">
        <input id="sketchPatterns" type="checkbox" checked>
        <label for="sketchPatterns" title="Whether patterns should be sketched or just copied">
          Sketch patterns
        </label>
      </div>
      <div class="separator"></div>
      <div class="checkbox">
        <input id="pencilFilter" type="checkbox">
        <label for="pencilFilter">Apply pencil texture</label>
      </div>
    </div>

    <div id="preview">

      <div class="toolbar">

        <input type="checkbox" id="source-toggle" class="toggle-btn">
        <label for="source-toggle" class="icon icon-raw" title="Toggle code editor"></label>

        <input type="file" id="file-chooser" accept="image/svg+xml">
        <select id="sample-select">
          <option value="bpmn1">bpmn1.svg</option>
          <option value="computer-network">computer-network.svg</option>
          <option value="flowchart4">flowchart4.svg</option>
          <option value="hierarchical1">hierarchical1.svg</option>
          <option value="hierarchical2">hierarchical2.svg</option>
          <option value="mindmap">mindmap.svg</option>
          <option value="movies">movies.svg</option>
          <option value="organic1">organic1.svg</option>
          <option value="organic2">organic2.svg</option>
          <option value="tree1">tree1.svg</option>
          <option value="venn">venn.svg</option>
        </select>
        <label for="opacity">Compare: </label>
        <input id="opacity" type="range" min="0" max="1" step="0.05" value="0">
        <button id="download-btn">Download Sketch</button>
        <div id="local-testing">
          <button id="prev-testcase" title="Previous testcase" class="icon-button"><span
              class="icon icon-chevron-left"></span></button>
          <select id="select-testcase">
            <option value=""></option>
          </select>
          <button id="next-testcase" title="Previous testcase" class="icon-button"><span
              class="icon icon-chevron-right"></span></button>
          <button id="download-testcase" title="Create a testcase for the repository" class="icon-button"><span
              class="icon icon-flask"></span></button>
        </div>
      </div>

      <div class="separator" style="margin: 0;"></div>

      <div class="content-container">
        <div class="raw-svg-container hidden"></div>
        <div class="image-container">
          <div id="output"></div>
          <div id="input"></div>
        </div>
      </div>
    </div>
  </div>

  <script type="module" src="/src/index.ts"></script>

</body>

</html>

================================================
FILE: sample-application/package.json
================================================
{
  "name": "svg2roughjs-sample",
  "description": "A simple sample application to test and try svg2roughjs",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "author": "Fabian Schwarzkopf",
  "license": "MIT",
  "dependencies": {
    "@codemirror/lang-xml": "^6.1.0",
    "codemirror": "^6.0.2"
  },
  "devDependencies": {
    "@types/codemirror": "^5.60.17",
    "prettier": "3.8.1",
    "typescript": "^5.9.3",
    "vite": "^7.3.1"
  }
}


================================================
FILE: sample-application/src/assets/styles.css
================================================
body * {
  box-sizing: border-box;
}

body {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  left: 0;
  margin: 0;
  font-family: Arial, Helvetica, sans-serif;
  color: #2a333d;
}

#preview {
  display: flex;
  flex-direction: column;
  overflow: auto;
  width: 100%;
}

#input {
  position: absolute;
  top: 0;
  left: 0;
  opacity: 0;
  background-color: white;
}

.header {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 20px 0;
  width: 100%;
  background-image: linear-gradient(60deg, #29323c 0%, #485563 100%);
}

.header-img {
  width: 100px;
  height: 100px;
  margin: 0 40px;
}

.title {
  text-align: center;
  font-family: Arial, Helvetica, sans-serif;
}

.title h1 {
  margin-bottom: 5px;
  font-weight: bold;
  letter-spacing: 5px;
  font-size: 35px;
}

a:link,
a:visited,
a:hover,
a:active {
  color: #fff;
  text-decoration: none;
}

.sidebar {
  border-right: 1px solid rgba(0, 0, 0, 0.14);
  display: flex;
  flex-direction: column;
  padding: 0 15px;
  flex: 0 0 200px;
}

.sidebar h2 {
  margin: 10px 0;
  font-size: 18px;
  text-align: center;
  letter-spacing: 1px;
}

.toolbar {
  display: flex;
  justify-items: center;
  align-items: center;
  flex-wrap: wrap;
  padding: 5px 10px;
}

.toolbar > * {
  margin: 2px 5px;
}

.toolbar input.toggle-btn:checked:hover + label {
  background-color: #b2b2b2;
}

.toolbar label.toggle-btn:hover,
.toolbar input.toggle-btn:checked + label {
  background-color: #dedede;
}

label {
  user-select: none;
}

.icon {
  display: block;
  width: 32px;
  height: 32px;
  background-repeat: no-repeat;
  background-position: center;
  border: none;
  cursor: pointer;
  background-size: 24px;
  border-radius: 5px;
}

.icon-raw {
  background-image: url('./raw-source.svg');
}

.icon-flask {
  background-image: url('./flask.svg');
}

.icon-chevron-left {
  background-image: url('./chevron-left.svg');
}

.icon-chevron-right {
  background-image: url('./chevron-right.svg');
}

.toggle-btn {
  display: none;
}

.main {
  position: absolute;
  top: 140px;
  right: 0;
  left: 0;
  bottom: 0;
  overflow: hidden;
  display: flex;
}

.content-container {
  overflow: hidden;
  height: 100%;
  position: relative;
  display: flex;
}

.raw-svg-container {
  flex: 0 0 40%;
  overflow: auto;
  border-right: 1px solid rgba(0, 0, 0, 0.14);
  transition: flex-basis 0.2s ease-in-out;
}

.raw-svg-container.hidden {
  flex-basis: 0;
}

.raw-svg-container .CodeMirror {
  height: 100%;
}

.image-container {
  flex: 1 1 auto;
  overflow: auto;
  position: relative;
}

.separator {
  border-top: 1px solid rgba(0, 0, 0, 0.14);
  margin: 15px 20px;
}

.checkbox {
  display: flex;
  align-items: center;
}

.checkbox input {
  margin: 8px;
}

.checkbox * {
  cursor: pointer;
}

label[for='opacity'] {
  cursor: pointer;
}

.icon-button {
  width: 24px;
  height: 24px;
  padding: 3px;
  margin: 0 6px;
}

.icon-button .icon {
  width: 100%;
  height: 100%;
  background-size: contain;
}

#local-testing {
  margin: 0 0 0 auto;
  display: flex;
  align-items: center;
}


================================================
FILE: sample-application/src/index.ts
================================================
import './assets/styles.css'
import { EditorView } from 'codemirror'
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language'
import { lineNumbers } from '@codemirror/view'
import { xml } from '@codemirror/lang-xml'

import SAMPLE_BPMN from './samples/bpmn1.svg?raw'
import SAMPLE_COMPUTER_NETWORK from './samples/computer-network.svg?raw'
import SAMPLE_FLOWCHART from './samples/flowchart4.svg?raw'
import SAMPLE_HIERARCHICAL1 from './samples/hierarchical1.svg?raw'
import SAMPLE_HIERARCHICAL2 from './samples/hierarchical2.svg?raw'
import SAMPLE_MINDMAP from './samples/mindmap.svg?raw'
import SAMPLE_MOVIES from './samples/movies.svg?raw'
import SAMPLE_ORGANIC1 from './samples/organic1.svg?raw'
import SAMPLE_ORGANIC2 from './samples/organic2.svg?raw'
import SAMPLE_TREE from './samples/tree1.svg?raw'
import SAMPLE_VENN from './samples/venn.svg?raw'

// debug lib import for better debugging...
import { OutputType, Svg2Roughjs } from '../../src/index'
import { initializeTestUI } from './testing'

let svg2roughjs: Svg2Roughjs
let loadingSvg = false
let scheduledLoad: string | null = null
let debouncedTimer: ReturnType<typeof setTimeout> | null = null
let codeMirror: EditorView
let silentCodeMirrorChange = false

// just for easier access, it's a debug project, so who cares...
const patternsCheckbox = document.getElementById('sketchPatterns') as HTMLInputElement
const pencilCheckbox = document.getElementById('pencilFilter') as HTMLInputElement
const sampleSelect = document.getElementById('sample-select') as HTMLSelectElement
const codeContainer = document.querySelector('.raw-svg-container') as HTMLDivElement
const fillStyleSelect = document.getElementById('fill-style') as HTMLSelectElement
const outputFormatSelect = document.getElementById('output-format') as HTMLSelectElement
const roughnessInput = document.getElementById('roughness-input') as HTMLInputElement
const bowingInput = document.getElementById('bowing-input') as HTMLInputElement
const opacityInput = document.getElementById('opacity') as HTMLInputElement
const fileInput = document.getElementById('file-chooser') as HTMLInputElement
const originalFontCheckbox = document.getElementById('original-font') as HTMLInputElement
const randomizeCheckbox = document.getElementById('randomize') as HTMLInputElement

const onCodeMirrorChange = (newCode: string) => {
  if (debouncedTimer) {
    clearTimeout(debouncedTimer)
  }
  debouncedTimer = setTimeout(() => {
    debouncedTimer = null
    try {
      loadSvgString(newCode)
    } catch (_) {
      /* do nothing */
    }
  }, 500)
}

/**
 * Sets CodeMirror content without triggering the change listener
 */
function setCodeMirrorValue(value: string) {
  silentCodeMirrorChange = true
  codeMirror.dispatch({
    changes: { from: 0, to: codeMirror.state.doc.length, insert: value }
  })
}

function getSvgSize(svg: SVGSVGElement): { width: number; height: number } {
  let width, height
  const hasViewbox = svg.hasAttribute('viewBox')
  if (svg.hasAttribute('width')) {
    // percantage sizes for the root SVG are unclear, thus use viewBox if available
    if (svg.width.baseVal.unitType === SVGLength.SVG_LENGTHTYPE_PERCENTAGE && hasViewbox) {
      width = svg.viewBox.baseVal.width
    } else {
      width = svg.width.baseVal.value
    }
  } else if (hasViewbox) {
    width = svg.viewBox.baseVal.width
  } else {
    width = 300
  }

  if (svg.hasAttribute('height')) {
    // percantage sizes for the root SVG are unclear, thus use viewBox if available
    if (svg.height.baseVal.unitType === SVGLength.SVG_LENGTHTYPE_PERCENTAGE && hasViewbox) {
      height = svg.viewBox.baseVal.height
    } else {
      height = svg.height.baseVal.value
    }
  } else if (hasViewbox) {
    height = svg.viewBox.baseVal.height
  } else {
    height = 150
  }

  return { width, height }
}

export function loadSvgString(fileContent: string) {
  if (loadingSvg) {
    scheduledLoad = fileContent
    return
  }
  setUIState(false)

  loadingSvg = true

  const inputElement = document.getElementById('input')!
  const outputElement = document.getElementById('output')!
  const canvas = outputElement.querySelector('canvas')

  const parser = new DOMParser()
  const doc = parser.parseFromString(fileContent, 'image/svg+xml')
  const svg = doc.querySelector('svg')

  while (inputElement.childElementCount > 0) {
    inputElement.removeChild(inputElement.firstChild!)
  }

  if (!svg) {
    console.error('Could not load SVG file')
    setUIState(true)
    loadingSvg = false
    return
  }

  const svgSize = getSvgSize(svg)
  if (svgSize) {
    inputElement.style.width = `${svgSize.width}px`
    inputElement.style.height = `${svgSize.height}px`
  }
  inputElement.appendChild(svg)

  // make sure the SVG is part of the DOM and rendered, before it is converted by
  // Svg2Rough.js. Otherwise, CSS percentaged width/height might not be applied yet
  setTimeout(async () => {
    if (svg.tagName === 'HTML') {
      console.error('Error parsing XML')
      inputElement.style.opacity = '1'
      inputElement.style.width = '100%'
      inputElement.style.height = '100%'
      if (canvas) {
        canvas.style.opacity = '0'
      }
    } else {
      const opacityInput = document.getElementById('opacity') as HTMLInputElement
      inputElement.style.opacity = opacityInput.value
      if (canvas) {
        canvas.style.opacity = '1'
      }
      try {
        svg2roughjs.svg = svg
        await svg2roughjs.sketch()
      } catch (e) {
        console.error("Couldn't sketch content")
        throw e // re-throw to show error on console
      } finally {
        setUIState(true)
        loadingSvg = false
      }

      // maybe there was a load during the rendering.. so load this instead
      if (scheduledLoad) {
        loadSvgString(scheduledLoad)
        scheduledLoad = null
      }
    }
  }, 0)
}

function loadSample(sample: string) {
  let sampleString = ''
  switch (sample) {
    case 'bpmn1':
      sampleString = SAMPLE_BPMN
      break
    case 'computer-network':
      sampleString = SAMPLE_COMPUTER_NETWORK
      break
    case 'flowchart4':
      sampleString = SAMPLE_FLOWCHART
      break
    case 'hierarchical1':
      sampleString = SAMPLE_HIERARCHICAL1
      break
    case 'hierarchical2':
      sampleString = SAMPLE_HIERARCHICAL2
      break
    case 'mindmap':
      sampleString = SAMPLE_MINDMAP
      break
    case 'movies':
      sampleString = SAMPLE_MOVIES
      break
    case 'organic1':
      sampleString = SAMPLE_ORGANIC1
      break
    case 'organic2':
      sampleString = SAMPLE_ORGANIC2
      break
    case 'tree1':
      sampleString = SAMPLE_TREE
      break
    case 'venn':
      sampleString = SAMPLE_VENN
      break
  }

  setCodeMirrorValue(sampleString)

  loadSvgString(sampleString)
}

function updateOpacity(inputContainerOpacity: number) {
  const inputContainer = document.getElementById('input')!
  const outputContainer = document.getElementById('output')!
  inputContainer.style.opacity = `${inputContainerOpacity}`
  outputContainer.style.opacity = `${1 - inputContainerOpacity}`
}

function run() {
  svg2roughjs = new Svg2Roughjs('#output', OutputType.SVG)
  svg2roughjs.backgroundColor = 'white'
  svg2roughjs.pencilFilter = !!pencilCheckbox.checked
  svg2roughjs.sketchPatterns = !!patternsCheckbox.checked
  svg2roughjs.roughConfig = {
    bowing: parseInt(bowingInput.value),
    roughness: parseInt(roughnessInput.value),
    fillStyle: fillStyleSelect.value
  }
  sampleSelect.addEventListener('change', () => {
    loadSample(sampleSelect.value)
  })

  const toggleSourceBtn = document.getElementById('source-toggle') as HTMLInputElement
  toggleSourceBtn.addEventListener('change', () => {
    if (toggleSourceBtn.checked) {
      codeContainer.classList.remove('hidden')
      setTimeout(() => {
        codeMirror.requestMeasure()
        codeMirror.focus()
      }, 20)
    } else {
      codeContainer.classList.add('hidden')
    }
  })

  codeMirror = new EditorView({
    parent: codeContainer,
    extensions: [
      lineNumbers(),
      xml(),
      syntaxHighlighting(defaultHighlightStyle),
      EditorView.updateListener.of(e => {
        if (e.docChanged && !silentCodeMirrorChange) {
          onCodeMirrorChange(e.state.doc.toString())
        }
        silentCodeMirrorChange = false
      })
    ]
  })

  // codeMirrorInstance = CodeMirror(codeContainer, {
  //   mode: 'xml',
  //   lineNumbers: true
  // })

  // make sure codemirror is rendered when the expand animation has finished
  codeContainer.addEventListener('transitionend', () => {
    if (toggleSourceBtn.checked) {
      codeMirror.requestMeasure()
      codeMirror.focus()
    }
  })

  // pre-select a sample
  sampleSelect.selectedIndex = 0
  loadSample(sampleSelect.value)

  outputFormatSelect.addEventListener('change', async () => {
    setUIState(false)
    svg2roughjs.outputType = outputFormatSelect.value === 'svg' ? OutputType.SVG : OutputType.CANVAS
    await svg2roughjs.sketch()
    setUIState(true)
  })
  fillStyleSelect.addEventListener('change', async () => {
    svg2roughjs.roughConfig = {
      bowing: parseInt(bowingInput.value),
      roughness: parseInt(roughnessInput.value),
      fillStyle: fillStyleSelect.value
    }
    setUIState(false)
    await svg2roughjs.sketch()
    setUIState(true)
  })
  roughnessInput.addEventListener('change', async () => {
    svg2roughjs.roughConfig = {
      bowing: parseInt(bowingInput.value),
      roughness: parseInt(roughnessInput.value),
      fillStyle: fillStyleSelect.value
    }
    setUIState(false)
    await svg2roughjs.sketch()
    setUIState(true)
  })
  bowingInput.addEventListener('change', async () => {
    svg2roughjs.roughConfig = {
      bowing: parseInt(bowingInput.value),
      roughness: parseInt(roughnessInput.value),
      fillStyle: fillStyleSelect.value
    }
    setUIState(false)
    await svg2roughjs.sketch()
    setUIState(true)
  })

  opacityInput.addEventListener('change', () => {
    updateOpacity(parseFloat(opacityInput.value))
  })
  const opacityLabel = document.querySelector('label[for=opacity]') as HTMLLabelElement
  opacityLabel.addEventListener('click', () => {
    const currentOpacity = parseFloat(opacityInput.value)
    const newOpacity = currentOpacity < 1 ? 1 : 0
    opacityInput.value = `${newOpacity}`
    updateOpacity(newOpacity)
  })

  function loadFile(file: File) {
    const reader = new FileReader()
    reader.readAsText(file)
    reader.addEventListener('load', () => {
      const fileContent = reader.result as string
      setCodeMirrorValue(fileContent)
      loadSvgString(fileContent)
    })
  }

  fileInput.addEventListener('change', () => {
    const files = fileInput.files
    if (files && files.length > 0) {
      loadFile(files[0])
    }
  })

  const body = document.getElementsByTagName('body')[0]
  body.addEventListener('dragover', e => {
    e.preventDefault()
  })
  body.addEventListener('drop', e => {
    e.preventDefault()
    if (e.dataTransfer && e.dataTransfer.items) {
      for (let i = 0; i < e.dataTransfer.items.length; i++) {
        if (e.dataTransfer.items[i].kind === 'file') {
          const file = e.dataTransfer.items[i].getAsFile()
          if (file) {
            loadFile(file)
          }
          return
        }
      }
    } else if (e.dataTransfer) {
      // Use DataTransfer interface to access the file(s)
      for (let i = 0; i < e.dataTransfer.files.length; i++) {
        loadFile(e.dataTransfer.files[i])
        return
      }
    }
  })

  const downloadBtn = document.getElementById('download-btn') as HTMLButtonElement
  downloadBtn.addEventListener('click', () => {
    const link = document.createElement('a')

    if (svg2roughjs.outputType === OutputType.CANVAS) {
      const canvas = document.querySelector('#output canvas') as HTMLCanvasElement
      const image = canvas.toDataURL('image/png', 1.0).replace('image/png', 'image/octet-stream')
      link.download = 'svg2roughjs.png'
      link.href = image
    } else {
      const serializer = new XMLSerializer()
      const svg = document.querySelector('#output svg') as SVGSVGElement
      let svgString = serializer.serializeToString(svg)
      svgString = '<?xml version="1.0" standalone="no"?>\r\n' + svgString
      const svgBlob = new Blob([svgString], { type: 'image/svg+xml' })
      link.download = 'svg2roughjs.svg'
      link.href = URL.createObjectURL(svgBlob)
    }

    link.click()
  })

  initializeTestUI(svg2roughjs)

  originalFontCheckbox.addEventListener('change', async () => {
    if (originalFontCheckbox.checked) {
      svg2roughjs.fontFamily = null
    } else {
      svg2roughjs.fontFamily = 'Comic Sans MS, cursive'
    }
    setUIState(false)
    await svg2roughjs.sketch()
    setUIState(true)
  })

  randomizeCheckbox.addEventListener('change', async () => {
    svg2roughjs.randomize = !!randomizeCheckbox.checked
    setUIState(false)
    await svg2roughjs.sketch()
    setUIState(true)
  })
  pencilCheckbox.addEventListener('change', async () => {
    svg2roughjs.pencilFilter = !!pencilCheckbox.checked
    setUIState(false)
    await svg2roughjs.sketch()
    setUIState(true)
  })
  patternsCheckbox.addEventListener('change', async () => {
    svg2roughjs.sketchPatterns = !!patternsCheckbox.checked
    setUIState(false)
    await svg2roughjs.sketch()
    setUIState(true)
  })
}

function setUIState(enabled: boolean) {
  const elements = [
    patternsCheckbox,
    pencilCheckbox,
    sampleSelect,
    fillStyleSelect,
    outputFormatSelect,
    roughnessInput,
    bowingInput,
    opacityInput,
    fileInput,
    originalFontCheckbox,
    randomizeCheckbox
  ]

  for (const ele of elements) {
    ele.disabled = !enabled
  }
}

run()


================================================
FILE: sample-application/src/testing.ts
================================================
import { OutputType, Svg2Roughjs } from '../../src/index'
import { downloadFile } from './utils'
import { specTests } from '../../test/tests.js'
import { loadSvgString } from './index'

const localTestsContainer = document.getElementById('local-testing') as HTMLDivElement
const downloadTestcaseBtn = document.getElementById('download-testcase') as HTMLButtonElement
const testcaseSelect = document.getElementById('select-testcase') as HTMLSelectElement
const prevTestcaseBtn = document.getElementById('prev-testcase') as HTMLButtonElement
const nextTestcaseBtn = document.getElementById('next-testcase') as HTMLButtonElement

export function initializeTestUI(svg2roughjs: Svg2Roughjs) {
  if (location.hostname !== 'localhost') {
    localTestsContainer.style.display = 'none'
  }

  for (const testName of specTests) {
    const option = document.createElement('option')
    option.value = testName
    option.text = testName
    testcaseSelect.appendChild(option)
  }

  testcaseSelect.addEventListener('change', e =>
    onTestcaseChange((e.target as HTMLOptionElement).value)
  )
  prevTestcaseBtn.addEventListener('click', () => {
    const idx = testcaseSelect.selectedIndex
    if (idx > 1) {
      testcaseSelect.selectedIndex = idx - 1
    }
    onTestcaseChange(testcaseSelect.options[testcaseSelect.selectedIndex].value)
  })
  nextTestcaseBtn.addEventListener('click', () => {
    const idx = testcaseSelect.selectedIndex
    if (idx < testcaseSelect.childElementCount - 1) {
      testcaseSelect.selectedIndex = idx + 1
    }
    onTestcaseChange(testcaseSelect.options[testcaseSelect.selectedIndex].value)
  })
  downloadTestcaseBtn.addEventListener('click', () => downloadTestcase(svg2roughjs))
}

async function onTestcaseChange(testName: string) {
  const svgString = loadSvg(`/specs/${testName}/test.svg`)
  loadSvgString(svgString)
}

function loadSvg(url: string) {
  const request = new XMLHttpRequest()
  request.open('GET', url, false)
  request.overrideMimeType('text/plain; charset=utf-8')
  request.send()
  if (request.status !== 200) {
    throw new Error(`Unable to fetch ${url}, status code: ${request.status}`)
  }
  return request.responseText
}

function isExistingTestcase(testcase: string): boolean {
  return specTests.indexOf(testcase) !== -1
}

/**
 * Creates a reproducible testcase
 */
async function downloadTestcase(svg2roughjs: Svg2Roughjs) {
  const prevRandomize = svg2roughjs.randomize
  const prevPencilFilter = svg2roughjs.pencilFilter
  const prevOutputType = svg2roughjs.outputType
  const prevSketchPatters = svg2roughjs.sketchPatterns
  const prevConfig = Object.assign({}, svg2roughjs.roughConfig)
  svg2roughjs.randomize = false
  svg2roughjs.pencilFilter = false
  svg2roughjs.sketchPatterns = true
  svg2roughjs.outputType = OutputType.SVG
  svg2roughjs.backgroundColor = 'white'
  svg2roughjs.roughConfig = {
    ...svg2roughjs.roughConfig,
    fixedDecimalPlaceDigits: 3,
    fillStyle: 'solid', // just use solid for tests, to make the more stable on lib changes
    seed: 4242
  }
  await svg2roughjs.sketch()
  const serializer = new XMLSerializer()

  const testcaseName = testcaseSelect.options[testcaseSelect.selectedIndex].value
  if (!isExistingTestcase(testcaseName)) {
    const test = document.querySelector('#input svg') as SVGSVGElement
    let inputSvg = serializer.serializeToString(test)
    inputSvg = '<?xml version="1.0" standalone="no"?>\r\n' + inputSvg
    downloadFile(inputSvg, 'image/svg+xml', 'test.svg')
  }

  const spec = document.querySelector('#output svg') as SVGSVGElement
  let sketchedSvg = serializer.serializeToString(spec)
  sketchedSvg = '<?xml version="1.0" standalone="no"?>\r\n' + sketchedSvg
  downloadFile(sketchedSvg, 'image/svg+xml', 'expect.svg')
  const config = {
    roughConfig: svg2roughjs.roughConfig,
    outputType: svg2roughjs.outputType,
    pencilFilter: svg2roughjs.pencilFilter,
    sketchPatterns: svg2roughjs.sketchPatterns,
    backgroundColor: 'white'
  }
  downloadFile(JSON.stringify(config), 'text/json', 'config.json')
  // reset state to before testcase creation
  svg2roughjs.randomize = prevRandomize
  svg2roughjs.pencilFilter = prevPencilFilter
  svg2roughjs.outputType = prevOutputType
  svg2roughjs.sketchPatterns = prevSketchPatters
  svg2roughjs.roughConfig = prevConfig
  await svg2roughjs.sketch()
}


================================================
FILE: sample-application/src/types.d.ts
================================================
declare module '*.svg' {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const content: any
  export default content
}


================================================
FILE: sample-application/src/utils.ts
================================================
export function downloadFile(content: string, mime: string, fileName: string) {
  const link = document.createElement('a')
  const configBlob = new Blob([content], { type: mime })
  link.download = fileName
  link.href = URL.createObjectURL(configBlob)
  link.click()
}


================================================
FILE: sample-application/src/vite-env.d.ts
================================================
/// <reference types="vite/client" />


================================================
FILE: sample-application/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": false
  },
  "include": ["../dist/index.d.ts", "src/types.d.ts", "src/**/*.ts"]
}


================================================
FILE: sample-application/vite.config.js
================================================
import { defineConfig } from 'vite'

export default defineConfig({
  publicDir: '../test/'
})


================================================
FILE: src/OutputType.ts
================================================
export enum OutputType {
  SVG,
  CANVAS
}


================================================
FILE: src/RandomNumberGenerator.ts
================================================
import { Random } from 'roughjs/bin/math'

/**
 * A simple random number generator that allows for seeding.
 */
export class RandomNumberGenerator {
  private readonly rng: Random | null
  constructor(seed: number | null) {
    // since we already depend on Rough.js, we may just use its seedable RNG implementation
    this.rng = seed ? new Random(seed) : null
  }

  /**
   * Returns a random number in the given range.
   */
  next(range?: [number, number]): number {
    const rnd = this.rng?.next() ?? Math.random()
    if (range) {
      const min = range[0]
      const max = range[1]
      return rnd * (max - min) + min
    }
    return rnd
  }
}


================================================
FILE: src/Svg2Roughjs.ts
================================================
import { Options } from 'roughjs/bin/core'
import rough from 'roughjs/bin/rough'
import { OutputType } from './OutputType'
import { processRoot } from './processor'
import { createPencilFilter } from './styles/textures'
import { RenderContext } from './types'
import { getDefsElement } from './utils'
import { RandomNumberGenerator } from './RandomNumberGenerator'

/**
 * Svg2Roughjs parses an SVG and converts it to a hand-drawn sketch.
 */
export class Svg2Roughjs {
  /**
   * Optional solid background color with which the canvas should be initialized.
   * It is drawn on a transparent canvas by default.
   */
  backgroundColor: string | null = null

  /**
   * Set a font-family for the rendering of text elements.
   * If set to `null`, then the font-family of the SVGTextElement is used.
   * By default, 'Comic Sans MS, cursive' is used.
   */
  fontFamily: string | null = 'Comic Sans MS, cursive'

  /**
   * Whether to randomize Rough.js' fillWeight, hachureAngle and hachureGap.
   * Also randomizes the disableMultiStroke option of Rough.js.
   * The randomness may be seeded with the `seed` property.
   * By default `true`.
   */
  randomize: boolean = true

  /**
   * Optional seed for the randomness when creating the sketch.
   * Providing a value implicitly seeds Rough.js which may be overwritten
   * by provding a different seed with the optional `roughConfig` property.
   * By default `null`.
   */
  seed: number | null = null

  /**
   * Whether pattern elements should be sketched or just copied to the output.
   * For smaller pattern base sizes, it's often beneficial to just copy it over
   * as the sketch will be too smalle to actually look sketched at all.
   */
  sketchPatterns: boolean = true

  /**
   * Whether to apply a pencil filter.
   */
  pencilFilter: boolean = false

  private $svg?: SVGSVGElement
  private width: number = 0
  private height: number = 0
  private $outputType: OutputType
  private $roughConfig: Options = {}
  private idElements: Record<string, SVGElement | string> = {}

  private outputElement: Element
  private lastResult: SVGSVGElement | HTMLCanvasElement | null = null

  /**
   * Set the SVG that should be sketched.
   */
  set svg(svg: SVGSVGElement) {
    if (this.$svg !== svg) {
      this.$svg = svg
      this.sourceSvgChanged()
    }
  }

  /**
   * Returns the SVG that should be sketched.
   */
  get svg(): SVGSVGElement | undefined {
    return this.$svg
  }

  /**
   * Sets the output format of the sketch.
   *
   * Applies only to instances that have been created with a
   * container as output element instead of an actual SVG or canvas
   * element.
   *
   * Throws when the given mode does not match the output element
   * with which this instance was created.
   */
  set outputType(type: OutputType) {
    if (this.$outputType === type) {
      return
    }

    const incompatible =
      (type === OutputType.CANVAS && this.outputElement instanceof SVGSVGElement) ||
      (type === OutputType.SVG && this.outputElement instanceof HTMLCanvasElement)
    if (incompatible) {
      throw new Error(
        `Output format ${type} incompatible with given output element ${this.outputElement.tagName}`
      )
    }

    this.$outputType = type
  }

  /**
   * Returns the currently configured output type.
   */
  get outputType(): OutputType {
    return this.$outputType
  }

  /**
   * Sets the config object that is passed to Rough.js and considered
   * during rendering of the `SVGElement`s.
   *
   * Sets `fixedDecimalPlaceDigits` to `3` if not specified otherwise.
   */
  set roughConfig(config: Options) {
    if (typeof config.fixedDecimalPlaceDigits === 'undefined') {
      config.fixedDecimalPlaceDigits = 3
    }
    this.$roughConfig = config
  }

  /**
   * Returns the currently configured rendering configuration.
   */
  get roughConfig(): Options {
    return this.$roughConfig
  }

  /**
   * Creates a new instance of Svg2roughjs.
   * @param target Either a container `HTMLDivElement` (or a selector for the container) to which a sketch should be added
   * or an `HTMLCanvasElement` or `SVGSVGElement` that should be used as output target.
   * @param outputType Whether the output should be an SVG or drawn to an HTML canvas.
   * Defaults to SVG or CANVAS depending if the given target is of type `HTMLCanvasElement` or `SVGSVGElement`,
   * otherwise it defaults to SVG.
   * @param roughConfig Config object that is passed to Rough.js and considered during
   * rendering of the `SVGElement`s.
   */
  constructor(
    target: string | HTMLDivElement | HTMLCanvasElement | SVGSVGElement,
    outputType: OutputType = OutputType.SVG,
    roughConfig: Options = {}
  ) {
    if (!target) {
      throw new Error('No target provided')
    }

    const targetElement = typeof target === 'string' ? document.querySelector(target) : target
    if (!targetElement) {
      throw new Error('Could not find target in document')
    }

    this.roughConfig = roughConfig

    this.outputElement = targetElement
    if (targetElement instanceof HTMLCanvasElement) {
      this.$outputType = OutputType.CANVAS
    } else if (targetElement instanceof SVGSVGElement) {
      this.$outputType = OutputType.SVG
    } else {
      this.$outputType = outputType
    }
  }

  /**
   * Triggers an entire redraw of the SVG which
   * processes the input element anew.
   * @param sourceSvgChanged When `true`, the given {@link svg} is re-evaluated as if it was set anew.
   *  This allows the Svg2Rough.js instance to be used mutliple times with the same source SVG container but different contents.
   * @returns A promise that resolves with the sketched output element or null if no {@link svg} is set.
   */
  sketch(sourceSvgChanged = false): Promise<SVGSVGElement | HTMLCanvasElement | null> {
    if (!this.svg) {
      return Promise.resolve(null)
    }

    if (sourceSvgChanged) {
      this.sourceSvgChanged()
    }

    const sketchContainer = this.prepareRenderContainer()
    const renderContext = this.createRenderContext(sketchContainer)

    // prepare filter effects
    if (this.pencilFilter) {
      const defs = getDefsElement(renderContext)
      defs.appendChild(createPencilFilter())
    }

    // sketchify the SVG
    renderContext.processElement(renderContext, this.svg, null, this.width, this.height)

    if (this.outputElement instanceof SVGSVGElement) {
      // sketch already in the outputElement
      return Promise.resolve(this.outputElement)
    } else if (this.outputElement instanceof HTMLCanvasElement) {
      return this.drawToCanvas(renderContext, this.outputElement)
    }

    // remove the previous attached result
    this.lastResult?.parentNode?.removeChild(this.lastResult)
    // assume that the given output element is a container, thus append the sketch to it
    if (this.outputType === OutputType.SVG) {
      const svgSketch = renderContext.svgSketch
      this.outputElement.appendChild(svgSketch)
      this.lastResult = svgSketch
      return Promise.resolve(svgSketch)
    } else {
      // canvas output type
      const canvas = document.createElement('canvas')
      this.outputElement.appendChild(canvas)
      this.lastResult = canvas
      return this.drawToCanvas(renderContext, canvas)
    }
  }

  /**
   * Creates a new context which contains the current state of the
   * Svg2Roughs instance for rendering.
   */
  private createRenderContext(sketchContainer: SVGSVGElement): RenderContext {
    if (!this.svg) {
      throw new Error('No source SVG set yet.')
    }
    let roughConfig = this.roughConfig
    if (this.seed !== null) {
      roughConfig = { seed: this.seed, ...roughConfig }
    }
    return {
      rc: rough.svg(sketchContainer, { options: roughConfig }),
      roughConfig: this.roughConfig,
      fontFamily: this.fontFamily,
      pencilFilter: this.pencilFilter,
      randomize: this.randomize,
      rng: new RandomNumberGenerator(this.seed),
      sketchPatterns: this.sketchPatterns,
      idElements: this.idElements,
      sourceSvg: this.svg,
      svgSketch: sketchContainer,
      svgSketchIsInDOM: document.body.contains(sketchContainer),
      styleSheets: Array.from(this.svg.querySelectorAll('style'))
        .map(s => s.sheet)
        .filter(s => s !== null) as CSSStyleSheet[],
      processElement: processRoot
    }
  }

  /**
   * Helper method to draw the sketched SVG to a HTMLCanvasElement.
   */
  private drawToCanvas(
    renderContext: RenderContext,
    canvas: HTMLCanvasElement
  ): Promise<HTMLCanvasElement> {
    canvas.width = this.width
    canvas.height = this.height
    const canvasCtx = canvas.getContext('2d') as CanvasRenderingContext2D
    canvasCtx.clearRect(0, 0, this.width, this.height)
    return new Promise(resolve => {
      const svgString = new XMLSerializer().serializeToString(renderContext.svgSketch)
      const img = new Image()
      img.onload = function () {
        canvasCtx.drawImage(this as HTMLImageElement, 0, 0)
        resolve(canvas)
      }
      img.src = `data:image/svg+xml;charset=utf8,${encodeURIComponent(svgString)}`
    })
  }

  /**
   * Prepares the given SVG element depending on the set properties.
   */
  private prepareRenderContainer(): SVGSVGElement {
    let svgElement: SVGSVGElement

    if (this.outputElement instanceof SVGSVGElement) {
      // just use the user given outputElement directly as sketch-container
      svgElement = this.outputElement
    } else {
      // we need a separate svgElement as output element
      svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
    }

    // make sure it has all the proper namespaces
    svgElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
    svgElement.setAttributeNS(
      'http://www.w3.org/2000/xmlns/',
      'xmlns:xlink',
      'http://www.w3.org/1999/xlink'
    )

    // clear SVG element
    while (svgElement.firstChild) {
      svgElement.removeChild(svgElement.firstChild)
    }

    // set size
    svgElement.setAttribute('width', this.width.toString())
    svgElement.setAttribute('height', this.height.toString())

    // apply backgroundColor
    let backgroundElement
    if (this.backgroundColor) {
      backgroundElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
      backgroundElement.width.baseVal.value = this.width
      backgroundElement.height.baseVal.value = this.height
      backgroundElement.setAttribute('fill', this.backgroundColor)
      svgElement.appendChild(backgroundElement)
    }

    // use round linecap to emphasize a ballpoint pen like drawing
    svgElement.setAttribute('stroke-linecap', 'round')

    return svgElement
  }

  /**
   * Initializes the size based on the currently set SVG and collects elements
   * with an ID property that may be referenced in the SVG.
   */
  private sourceSvgChanged() {
    const svg = this.$svg
    if (svg) {
      const precision = this.roughConfig.fixedDecimalPlaceDigits
      this.width = parseFloat(this.coerceSize(svg, 'width', 300).toFixed(precision))
      this.height = parseFloat(this.coerceSize(svg, 'height', 150).toFixed(precision))

      // pre-process defs for subsequent references
      this.collectElementsWithID()
    }
  }

  /**
   * Stores elements with IDs for later use.
   */
  private collectElementsWithID() {
    this.idElements = {}
    const elementsWithID: SVGElement[] = Array.prototype.slice.apply(
      this.svg!.querySelectorAll('*[id]')
    )
    for (const elt of elementsWithID) {
      const id = elt.getAttribute('id')
      if (id) {
        this.idElements[id] = elt
      }
    }
  }

  /**
   * Helper to handle percentage values for width / height of the input SVG.
   */
  private coerceSize(svg: SVGSVGElement, property: 'width' | 'height', fallback: number): number {
    let size = fallback
    const hasViewbox = svg.hasAttribute('viewBox')
    if (svg.hasAttribute(property)) {
      // percentage sizes for the root SVG are unclear, thus use viewBox if available
      if (svg[property].baseVal.unitType === SVGLength.SVG_LENGTHTYPE_PERCENTAGE && hasViewbox) {
        size = svg.viewBox.baseVal[property]
      } else {
        size = svg[property].baseVal.value
      }
    } else if (hasViewbox) {
      size = svg.viewBox.baseVal[property]
    }
    return size
  }
}


================================================
FILE: src/clipping.ts
================================================
import { getIdFromUrl, getNodeChildren } from './dom-helpers'
import { applyCircleClip } from './geom/circle'
import { applyEllipseClip } from './geom/ellipse'
import { applyPathClip } from './geom/path'
import { applyPolygonClip } from './geom/polygon'
import { applyRectClip } from './geom/rect'
import { getCombinedTransform } from './transformation'
import { RenderContext } from './types'
import { getDefsElement, SKETCH_CLIP_ATTRIBUTE } from './utils'

/**
 * Applies the clip-path to the CanvasContext.
 */
export function applyClipPath(
  context: RenderContext,
  owner: SVGElement,
  clipPathAttr: string,
  svgTransform: SVGTransform | null
): void {
  const id = getIdFromUrl(clipPathAttr)
  if (!id) {
    return
  }

  const clipPath = context.idElements[id] as SVGElement
  if (!clipPath) {
    return
  }

  // TODO clipPath: consider clipPathUnits
  //  create clipPath defs
  const targetDefs = getDefsElement(context)
  // unfortunately, we cannot reuse clip-paths due to the 'global transform' approach
  const sketchClipPathId = `${id}_${targetDefs.childElementCount}`
  const clipContainer = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath')
  clipContainer.id = sketchClipPathId
  storeSketchClipId(owner, sketchClipPathId)

  // traverse clip-path elements in DFS
  const stack: { element: SVGElement; transform: SVGTransform | null }[] = []
  const children = getNodeChildren(clipPath)
  for (let i = children.length - 1; i >= 0; i--) {
    const childElement = children[i] as SVGGraphicsElement
    const childTransform = getCombinedTransform(context, childElement, svgTransform)
    stack.push({ element: childElement, transform: childTransform })
  }

  while (stack.length > 0) {
    const { element, transform } = stack.pop()!

    try {
      applyElementClip(context, element, clipContainer, transform)
    } catch (e) {
      console.error(e)
    }

    if (
      element.tagName === 'defs' ||
      element.tagName === 'svg' ||
      element.tagName === 'clipPath' ||
      element.tagName === 'text'
    ) {
      // some elements are ignored on clippaths
      continue
    }
    // process children
    const children = getNodeChildren(element)
    for (let i = children.length - 1; i >= 0; i--) {
      const childElement = children[i] as SVGGraphicsElement
      const childTransform = getCombinedTransform(context, childElement, transform)
      stack.push({ element: childElement, transform: childTransform })
    }
  }

  if (clipContainer.childNodes.length > 0) {
    // add the clip-path only if it contains converted elements
    // some elements are not yet supported
    targetDefs.appendChild(clipContainer)
  }
}

/**
 * Creates a clip element and appends it to the given container.
 */
function applyElementClip(
  context: RenderContext,
  element: SVGElement,
  container: SVGClipPathElement,
  svgTransform: SVGTransform | null
) {
  switch (element.tagName) {
    case 'rect':
      applyRectClip(context, element as SVGRectElement, container, svgTransform)
      break
    case 'circle':
      applyCircleClip(context, element as SVGCircleElement, container, svgTransform)
      break
    case 'ellipse':
      applyEllipseClip(context, element as SVGEllipseElement, container, svgTransform)
      break
    case 'polygon':
      applyPolygonClip(context, element as SVGPolygonElement, container, svgTransform)
      break
    case 'path':
      applyPathClip(context, element as SVGPathElement, container, svgTransform)
      break
  }
}

/**
 * Store clippath-id on each child for <g> elements, or on the owner itself for other
 * elements.
 *
 * <g> elements are skipped in the processing loop, thus the clip-path id must be stored
 * on the child elements.
 */
function storeSketchClipId(element: SVGElement, id: string): void {
  if (element.tagName !== 'g') {
    element.setAttribute(SKETCH_CLIP_ATTRIBUTE, id)
    return
  }

  const stack: SVGElement[] = []
  const children = getNodeChildren(element)
  for (let i = children.length - 1; i >= 0; i--) {
    stack.push(children[i] as SVGElement)
  }

  while (stack.length > 0) {
    const element = stack.pop()!
    element.setAttribute(SKETCH_CLIP_ATTRIBUTE, id)

    const children = getNodeChildren(element)
    for (let i = children.length - 1; i >= 0; i--) {
      stack.push(children[i] as SVGElement)
    }
  }
}


================================================
FILE: src/dom-helpers.ts
================================================
import { RenderContext } from './types'

/**
 * Returns the Node's children, since Node.prototype.children is not available on all browsers.
 * https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/children
 */
export function getNodeChildren(element: Element): Element[] {
  if (typeof element.children !== 'undefined') {
    return element.children as unknown as Element[]
  }
  let i = 0
  let node
  const nodes = element.childNodes
  const children: Element[] = []
  while ((node = nodes[i++])) {
    if (node.nodeType === 1) {
      children.push(node as Element)
    }
  }
  return children
}

/**
 * IE doesn't support `element.parentElement` in SVG documents.
 * This helper utilizes `parentNode` and checks for the `nodeType`.
 */
export function getParentElement(node: Node): Element | null {
  const parentNode = node.parentNode
  if (parentNode && parentNode.nodeType === Node.ELEMENT_NODE) {
    return parentNode as Element
  }
  return null
}

/**
 * Returns the CSS rules that apply to the given element (ignoring inheritance).
 *
 * Based on https://stackoverflow.com/a/22638396
 */
export function getMatchedCssRules(context: RenderContext, el: Element): CSSStyleRule[] {
  const ret: CSSStyleRule[] = []
  el.matches =
    el.matches ||
    el.webkitMatchesSelector ||
    // @ts-expect-error: legacy browser support
    el.mozMatchesSelector ||
    // @ts-expect-error: legacy browser support
    el.msMatchesSelector ||
    // @ts-expect-error: legacy browser support
    el.oMatchesSelector

  context.styleSheets.forEach(sheet => {
    const rules = sheet.rules || sheet.cssRules
    for (const r in rules) {
      const rule = rules[r] as CSSStyleRule
      if (el.matches(rule.selectorText)) {
        ret.push(rule)
      }
    }
  })
  return ret
}

/**
 * Moves the child-nodes from the source to a new parent.
 */
export function reparentNodes<T extends SVGElement>(newParent: T, source: SVGElement): T {
  while (source.firstChild) {
    newParent.append(source.firstChild)
  }
  return newParent
}

/**
 * Returns the id from the url string
 */
export function getIdFromUrl(url: string | null): string | null {
  if (url === null) {
    return null
  }
  const result =
    /url\('#?(.*?)'\)/.exec(url) || /url\("#?(.*?)"\)/.exec(url) || /url\(#?(.*?)\)/.exec(url)
  if (result && result.length > 1) {
    return result[1]
  }
  return null
}


================================================
FILE: src/geom/circle.ts
================================================
import { appendPatternPaint } from '../styles/pattern'
import { parseStyleConfig } from '../styles/styles'
import {
  applyTransform,
  applyMatrix,
  isIdentityTransform,
  isTranslationTransform
} from '../transformation'
import { RenderContext } from '../types'
import { appendSketchElement, sketchPath } from '../utils'
import { str } from './primitives'

export function drawCircle(
  context: RenderContext,
  circle: SVGCircleElement,
  svgTransform: SVGTransform | null
): void {
  const cx = circle.cx.baseVal.value
  const cy = circle.cy.baseVal.value
  const r = circle.r.baseVal.value

  if (r === 0) {
    // zero-radius circle is not rendered
    return
  }

  const center = applyMatrix({ x: cx, y: cy }, svgTransform)
  const radiusPoint = applyMatrix({ x: cx + r, y: cy + r }, svgTransform)
  const transformedRadius = radiusPoint.x - center.x

  let result
  if (isIdentityTransform(svgTransform) || isTranslationTransform(svgTransform)) {
    // transform a point on the ellipse to get the transformed radius
    result = context.rc.circle(center.x, center.y, 2 * transformedRadius, {
      ...parseStyleConfig(context, circle, svgTransform),
      preserveVertices: true
    })
  } else {
    // in other cases we need to construct the path manually.
    const factor = (4 / 3) * (Math.sqrt(2) - 1)
    const p1 = applyMatrix({ x: cx + r, y: cy }, svgTransform)
    const p2 = applyMatrix({ x: cx, y: cy + r }, svgTransform)
    const p3 = applyMatrix({ x: cx - r, y: cy }, svgTransform)
    const p4 = applyMatrix({ x: cx, y: cy - r }, svgTransform)
    const c1 = applyMatrix({ x: cx + r, y: cy + factor * r }, svgTransform)
    const c2 = applyMatrix({ x: cx + factor * r, y: cy + r }, svgTransform)
    const c4 = applyMatrix({ x: cx - r, y: cy + factor * r }, svgTransform)
    const c6 = applyMatrix({ x: cx - factor * r, y: cy - r }, svgTransform)
    const c8 = applyMatrix({ x: cx + r, y: cy - factor * r }, svgTransform)
    const path = `M ${str(p1)} C ${str(c1)} ${str(c2)} ${str(p2)} S ${str(c4)} ${str(p3)} S ${str(
      c6
    )} ${str(p4)} S ${str(c8)} ${str(p1)}z`
    result = sketchPath(context, path, parseStyleConfig(context, circle, svgTransform))
  }

  appendPatternPaint(context, circle, () => {
    const proxy = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
    proxy.cx.baseVal.value = center.x
    proxy.cy.baseVal.value = center.y
    proxy.r.baseVal.value = transformedRadius
    return proxy
  })
  appendSketchElement(context, circle, result)
}

export function applyCircleClip(
  context: RenderContext,
  circle: SVGCircleElement,
  container: SVGClipPathElement,
  svgTransform: SVGTransform | null
): void {
  const cx = circle.cx.baseVal.value
  const cy = circle.cy.baseVal.value
  const r = circle.r.baseVal.value

  if (r === 0) {
    // zero-radius circle is not rendered
    return
  }

  const clip = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
  clip.cx.baseVal.value = cx
  clip.cy.baseVal.value = cy
  clip.r.baseVal.value = r
  applyTransform(context, svgTransform, clip)
  container.appendChild(clip)
}


================================================
FILE: src/geom/ellipse.ts
================================================
import { appendPatternPaint } from '../styles/pattern'
import { parseStyleConfig } from '../styles/styles'
import {
  applyTransform,
  applyMatrix,
  isIdentityTransform,
  isTranslationTransform
} from '../transformation'
import { RenderContext } from '../types'
import { appendSketchElement, sketchPath } from '../utils'
import { str } from './primitives'

export function drawEllipse(
  context: RenderContext,
  ellipse: SVGEllipseElement,
  svgTransform: SVGTransform | null
): void {
  const cx = ellipse.cx.baseVal.value
  const cy = ellipse.cy.baseVal.value
  const rx = ellipse.rx.baseVal.value
  const ry = ellipse.ry.baseVal.value

  if (rx === 0 || ry === 0) {
    // zero-radius ellipse is not rendered
    return
  }

  const center = applyMatrix({ x: cx, y: cy }, svgTransform)
  // transform a point on the ellipse to get the transformed radius
  const radiusPoint = applyMatrix({ x: cx + rx, y: cy + ry }, svgTransform)
  const transformedRx = radiusPoint.x - center.x
  const transformedRy = radiusPoint.y - center.y

  let result
  if (isIdentityTransform(svgTransform) || isTranslationTransform(svgTransform)) {
    // Simple case, there's no transform and we can use the ellipse command
    result = context.rc.ellipse(center.x, center.y, 2 * transformedRx, 2 * transformedRy, {
      ...parseStyleConfig(context, ellipse, svgTransform),
      preserveVertices: true
    })
  } else {
    // in other cases we need to construct the path manually.
    const factor = (4 / 3) * (Math.sqrt(2) - 1)
    const p1 = applyMatrix({ x: cx + rx, y: cy }, svgTransform)
    const p2 = applyMatrix({ x: cx, y: cy + ry }, svgTransform)
    const p3 = applyMatrix({ x: cx - rx, y: cy }, svgTransform)
    const p4 = applyMatrix({ x: cx, y: cy - ry }, svgTransform)
    const c1 = applyMatrix({ x: cx + rx, y: cy + factor * ry }, svgTransform)
    const c2 = applyMatrix({ x: cx + factor * rx, y: cy + ry }, svgTransform)
    const c4 = applyMatrix({ x: cx - rx, y: cy + factor * ry }, svgTransform)
    const c6 = applyMatrix({ x: cx - factor * rx, y: cy - ry }, svgTransform)
    const c8 = applyMatrix({ x: cx + rx, y: cy - factor * ry }, svgTransform)
    const path = `M ${str(p1)} C ${str(c1)} ${str(c2)} ${str(p2)} S ${str(c4)} ${str(p3)} S ${str(
      c6
    )} ${str(p4)} S ${str(c8)} ${str(p1)}z`
    result = sketchPath(context, path, parseStyleConfig(context, ellipse, svgTransform))
  }

  appendPatternPaint(context, ellipse, () => {
    const proxy = document.createElementNS('http://www.w3.org/2000/svg', 'ellipse')
    proxy.cx.baseVal.value = center.x
    proxy.cy.baseVal.value = center.y
    proxy.rx.baseVal.value = transformedRx
    proxy.ry.baseVal.value = transformedRy
    return proxy
  })
  appendSketchElement(context, ellipse, result)
}

export function applyEllipseClip(
  context: RenderContext,
  ellipse: SVGEllipseElement,
  container: SVGClipPathElement,
  svgTransform: SVGTransform | null
): void {
  const cx = ellipse.cx.baseVal.value
  const cy = ellipse.cy.baseVal.value
  const rx = ellipse.rx.baseVal.value
  const ry = ellipse.ry.baseVal.value

  if (rx === 0 || ry === 0) {
    // zero-radius ellipse is not rendered
    return
  }

  const clip = document.createElementNS('http://www.w3.org/2000/svg', 'ellipse')
  clip.cx.baseVal.value = cx
  clip.cy.baseVal.value = cy
  clip.rx.baseVal.value = rx
  clip.ry.baseVal.value = ry
  applyTransform(context, svgTransform, clip)
  container.appendChild(clip)
}


================================================
FILE: src/geom/foreign-object.ts
================================================
import { applyTransform } from '../transformation'
import { RenderContext } from '../types'
import { appendSketchElement } from '../utils'

export function drawForeignObject(
  context: RenderContext,
  foreignObject: SVGForeignObjectElement,
  svgTransform: SVGTransform | null
): void {
  const foreignObjectClone = foreignObject.cloneNode(true) as SVGForeignObjectElement
  const container = document.createElementNS('http://www.w3.org/2000/svg', 'g')

  // foreignObject often relies on CSS styling, and just copying the <style> element
  // won't do the trick, because sketching the SVG rebuilds the entire element tree, thus
  // existing CSS rules don't apply anymore in most cases.
  //
  // To to make the MOST SIMPLE cases of foreignObject text elements work better,
  // try to apply the computed style on the new SVG container.
  // To properly fix it, we'd need to inline all computed styles recursively on the
  // foreignObject tree.

  const copyStyleProperties = [
    'color',
    'font-family',
    'font-size',
    'font-style',
    'font-variant',
    'font-weight'
  ]
  const style = getComputedStyle(foreignObject)
  for (const prop of copyStyleProperties) {
    container.style.setProperty(prop, style.getPropertyValue(prop))
  }

  // transform is already considered in svgTransform
  foreignObjectClone.transform.baseVal.clear()

  // transform the foreignObject to its destination location
  applyTransform(context, svgTransform, container)
  container.appendChild(foreignObjectClone)
  appendSketchElement(context, foreignObjectClone, container)
}


================================================
FILE: src/geom/image.ts
================================================
import { applyTransform } from '../transformation'
import { RenderContext } from '../types'
import { appendSketchElement } from '../utils'

export function drawImage(
  context: RenderContext,
  svgImage: SVGImageElement,
  svgTransform: SVGTransform | null
): void {
  const href = svgImage.href.baseVal
  const x = svgImage.x.baseVal.value
  const y = svgImage.y.baseVal.value
  let width, height
  if (svgImage.getAttribute('width') && svgImage.getAttribute('height')) {
    width = svgImage.width.baseVal.value
    height = svgImage.height.baseVal.value
  }
  if (href.startsWith('data:') && href.indexOf('image/svg+xml') !== -1) {
    // data:[<media type>][;charset=<character set>][;base64],<data>
    const dataUrlRegex = /^data:([^,]*),(.*)/
    const match = dataUrlRegex.exec(href)
    if (match && match.length > 2) {
      const meta = match[1]
      let svgString = match[2]
      const isBase64 = meta.indexOf('base64') !== -1
      const isUtf8 = meta.indexOf('utf8') !== -1
      if (isBase64) {
        svgString = atob(svgString)
      }
      if (!isUtf8) {
        svgString = decodeURIComponent(svgString)
      }
      const parser = new DOMParser()
      const doc = parser.parseFromString(svgString, 'image/svg+xml')
      const svg = doc.firstChild as SVGSVGElement

      let matrix = context.sourceSvg.createSVGMatrix().translate(x, y)
      matrix = svgTransform ? svgTransform.matrix.multiply(matrix) : matrix

      context.processElement(
        context,
        svg,
        context.sourceSvg.createSVGTransformFromMatrix(matrix),
        width,
        height
      )
      return
    }
  } else {
    const imageClone = svgImage.cloneNode()
    const container = document.createElementNS('http://www.w3.org/2000/svg', 'g')
    applyTransform(context, svgTransform, container)
    container.appendChild(imageClone)
    appendSketchElement(context, svgImage, container)
  }
}


================================================
FILE: src/geom/line.ts
================================================
import { appendPatternPaint } from '../styles/pattern'
import { parseStyleConfig } from '../styles/styles'
import { applyMatrix } from '../transformation'
import { RenderContext } from '../types'
import { appendSketchElement } from '../utils'
import { drawMarkers } from './marker'

export function drawLine(
  context: RenderContext,
  line: SVGLineElement,
  svgTransform: SVGTransform | null
): void {
  const p1 = { x: line.x1.baseVal.value, y: line.y1.baseVal.value }
  const p2 = { x: line.x2.baseVal.value, y: line.y2.baseVal.value }
  const { x: tp1x, y: tp1y } = applyMatrix(p1, svgTransform)
  const { x: tp2x, y: tp2y } = applyMatrix(p2, svgTransform)

  if (tp1x === tp2x && tp1y === tp2y) {
    // zero-length line is not rendered
    return
  }

  const lineSketch = context.rc.line(
    tp1x,
    tp1y,
    tp2x,
    tp2y,
    parseStyleConfig(context, line, svgTransform)
  )

  appendPatternPaint(context, line, () => {
    const proxy = document.createElementNS('http://www.w3.org/2000/svg', 'line')
    proxy.x1.baseVal.value = tp1x
    proxy.y1.baseVal.value = tp1y
    proxy.x2.baseVal.value = tp2x
    proxy.y2.baseVal.value = tp2y
    return proxy
  })

  appendSketchElement(context, line, lineSketch)

  drawMarkers(context, line, [p1, p2], svgTransform)
}


================================================
FILE: src/geom/marker.ts
================================================
import { getIdFromUrl } from '../dom-helpers'
import { getEffectiveAttribute } from '../styles/effective-attributes'
import { convertToPixelUnit } from '../svg-units'
import { RenderContext } from '../types'
import { equals, Point } from './primitives'

export function drawMarkers(
  context: RenderContext,
  element: SVGPathElement | SVGLineElement | SVGPolylineElement | SVGPolygonElement,
  points: Point[],
  svgTransform: SVGTransform | null
): void {
  if (points.length === 0) {
    return
  }

  const startPt = points[0]
  const endPt = points[points.length - 1]

  // start marker
  const markerStartId = getIdFromUrl(element.getAttribute('marker-start'))
  const markerStartElement = markerStartId
    ? (context.idElements[markerStartId] as SVGMarkerElement)
    : null

  // marker-start is only rendered when there are at least two points
  if (markerStartElement && points.length > 1) {
    let angle = markerStartElement.orientAngle.baseVal.value

    const nextPt = points[1]
    const orientAttr = markerStartElement.getAttribute('orient')
    if (orientAttr === 'auto' || orientAttr === 'auto-start-reverse') {
      const reverse = orientAttr === 'auto' ? 0 : 180
      const prevPt = points[points.length - 2]
      if (isClosedPath(points)) {
        // https://www.w3.org/TR/SVG11/painting.html#OrientAttribute
        // use angle bisector of incoming and outgoing angle
        angle = getBisectingAngle(prevPt, endPt, nextPt) - reverse
      } else {
        const vOut = { x: nextPt.x - startPt.x, y: nextPt.y - startPt.y }
        angle = getAngle({ x: 1, y: 0 }, vOut) - reverse
      }
    }

    const matrix = context.sourceSvg
      .createSVGMatrix()
      .translate(startPt.x, startPt.y)
      .rotate(angle)
      .scale(getScaleFactor(context, markerStartElement, element))

    const combinedMatrix = svgTransform ? svgTransform.matrix.multiply(matrix) : matrix
    const markerTransform = context.sourceSvg.createSVGTransformFromMatrix(combinedMatrix)

    context.processElement(context, markerStartElement, markerTransform)
  }

  // end marker
  const markerEndId = getIdFromUrl(element.getAttribute('marker-end'))
  const markerEndElement = markerEndId
    ? (context.idElements[markerEndId] as SVGMarkerElement)
    : null

  // marker-end is also rendered if the path has only one point
  if (markerEndElement) {
    let angle = markerEndElement.orientAngle.baseVal.value

    if (points.length > 1) {
      const orientAttr = markerEndElement.getAttribute('orient')
      if (orientAttr === 'auto' || orientAttr === 'auto-start-reverse') {
        // by spec, "auto-start-reverse" has no effect on marker end
        const prevPt = points[points.length - 2]
        if (isClosedPath(points)) {
          // https://www.w3.org/TR/SVG11/painting.html#OrientAttribute
          // use angle bisector of incoming and outgoing angle
          const nextPt = points[1] // start and end points are equal, take second point
          angle = getBisectingAngle(prevPt, endPt, nextPt)
        } else {
          const vIn = { x: endPt.x - prevPt.x, y: endPt.y - prevPt.y }
          angle = getAngle({ x: 1, y: 0 }, vIn)
        }
      }
    }

    const matrix = context.sourceSvg
      .createSVGMatrix()
      .translate(endPt.x, endPt.y)
      .rotate(angle)
      .scale(getScaleFactor(context, markerEndElement, element))

    const combinedMatrix = svgTransform ? svgTransform.matrix.multiply(matrix) : matrix
    const markerTransform = context.sourceSvg.createSVGTransformFromMatrix(combinedMatrix)

    context.processElement(context, markerEndElement, markerTransform)
  }

  // mid marker(s)
  const markerMidId = getIdFromUrl(element.getAttribute('marker-mid'))
  const markerMidElement = markerMidId
    ? (context.idElements[markerMidId] as SVGMarkerElement)
    : null

  if (markerMidElement && points.length > 2) {
    for (let i = 0; i < points.length; i++) {
      const loc = points[i]
      if (i === 0 || i === points.length - 1) {
        // mid markers are not drawn on first or last point
        continue
      }

      let angle = markerMidElement.orientAngle.baseVal.value
      const orientAttr = markerMidElement.getAttribute('orient')
      if (orientAttr === 'auto' || orientAttr === 'auto-start-reverse') {
        // by spec, "auto-start-reverse" has no effect on marker mid
        const prevPt = points[i - 1]
        const nextPt = points[i + 1]
        // https://www.w3.org/TR/SVG11/painting.html#OrientAttribute
        // use angle bisector of incoming and outgoing angle
        angle = getBisectingAngle(prevPt, loc, nextPt)
      }

      const matrix = context.sourceSvg
        .createSVGMatrix()
        .translate(loc.x, loc.y)
        .rotate(angle)
        .scale(getScaleFactor(context, markerMidElement, element))

      const combinedMatrix = svgTransform ? svgTransform.matrix.multiply(matrix) : matrix
      const markerTransform = context.sourceSvg.createSVGTransformFromMatrix(combinedMatrix)

      context.processElement(context, markerMidElement, markerTransform)
    }
  }
}

/**
 * Consider scaled coordinate system for markerWidth/markerHeight.
 */
function getScaleFactor(
  context: RenderContext,
  marker: SVGMarkerElement,
  referrer: SVGElement
): number {
  const markerUnits = marker.getAttribute('markerUnits')
  let scaleFactor = 1
  if (!markerUnits || markerUnits === 'strokeWidth') {
    // default is strokeWidth by SVG spec
    const strokeWidth = getEffectiveAttribute(context, referrer, 'stroke-width')
    if (strokeWidth) {
      scaleFactor = convertToPixelUnit(context, referrer, strokeWidth, 'stroke-width')
    }
  }
  return scaleFactor
}

/**
 * Whether the path is closed, i.e. the start and end points are identical
 */
function isClosedPath(points: Point[]): boolean {
  return equals(points[0], points[points.length - 1])
}

/**
 * Returns the bisection angle of the angle that is spanned by the given points.
 * @param prevPt The point from which the incoming flank is pointing
 * @param crossingPt The anchor point of the angle
 * @param nextPt Th point to which the outgoing flank is pointing
 * @returns The bisecting angle
 */
function getBisectingAngle(prevPt: Point, crossingPt: Point, nextPt: Point): number {
  const vIn = { x: nextPt.x - crossingPt.x, y: nextPt.y - crossingPt.y }
  const vOut = { x: prevPt.x - crossingPt.x, y: prevPt.y - crossingPt.y }

  // the relative angle between the two vectors
  const vectorAngle = getAngle(vIn, vOut)

  // calculate the absolute angle of the vectors considering the x-axis as reference
  const refPoint = { x: crossingPt.x + 1, y: crossingPt.y }
  const refVector = { x: refPoint.x - crossingPt.x, y: refPoint.y - crossingPt.y }
  const refAngle = getAngle(vIn, refVector)

  // return the absolute bisector
  return getOppositeAngle(vectorAngle) / 2 - refAngle
}

/**
 * Returns the opposite angle of the line. Considers the direction of the angle
 * (i.e. positive for clockwise, negative for counter-clickwise).
 */
function getOppositeAngle(angle: number): number {
  return angle - Math.sign(angle) * 180
}

/**
 * Returns the signed angle between the vectors (i.e. positive for clockwise,
 * negative for counter-clickwise).
 * @param v1 2-dimensional vector
 * @param v2 2-dimensional vector
 * @returns The signed angle between the vectors
 */
function getAngle(v1: Point, v2: Point): number {
  const a1 = Math.atan2(v1.y, v1.x)
  const a2 = Math.atan2(v2.y, v2.x)
  const angle = a2 - a1
  const K = -Math.sign(angle) * Math.PI * 2
  const a = Math.abs(K + angle) < Math.abs(angle) ? K + angle : angle
  return Math.round((360 * a) / (Math.PI * 2))
}


================================================
FILE: src/geom/path.ts
================================================
import { encodeSVGPath, SVGPathData, SVGPathDataTransformer } from 'svg-pathdata'
import { appendPatternPaint } from '../styles/pattern'
import { parseStyleConfig } from '../styles/styles'
import { applyTransform } from '../transformation'
import { RenderContext } from '../types'
import { appendSketchElement, sketchPath } from '../utils'
import { drawMarkers } from './marker'
import { Point } from './primitives'

export function drawPath(
  context: RenderContext,
  path: SVGPathElement,
  svgTransform: SVGTransform | null
): void {
  const dataAttrs = path.getAttribute('d')
  const pathData =
    // Parse path data and convert to absolute coordinates
    new SVGPathData(dataAttrs!)
      .toAbs()
      // Normalize H and V to L commands - those cannot work with how we draw transformed paths otherwise
      .transform(SVGPathDataTransformer.NORMALIZE_HVZ())
      // Normalize S and T to Q and C commands - Rough.js has a bug with T where it expects 4 parameters instead of 2
      .transform(SVGPathDataTransformer.NORMALIZE_ST())

  // If there's a transform, transform the whole path accordingly
  const transformedPathData = new SVGPathData(
    // clone the commands, we might need them untransformed for markers
    pathData.commands.map(cmd => Object.assign({}, cmd))
  )
  if (svgTransform) {
    transformedPathData.transform(
      SVGPathDataTransformer.MATRIX(
        svgTransform.matrix.a,
        svgTransform.matrix.b,
        svgTransform.matrix.c,
        svgTransform.matrix.d,
        svgTransform.matrix.e,
        svgTransform.matrix.f
      )
    )
  }

  const encodedPathData = encodeSVGPath(transformedPathData.commands)
  if (encodedPathData.indexOf('undefined') !== -1) {
    // DEBUG STUFF
    console.error('broken path data')
    return
  }

  const pathSketch = sketchPath(
    context,
    encodedPathData,
    parseStyleConfig(context, path, svgTransform)
  )

  appendPatternPaint(context, path, () => {
    const proxy = document.createElementNS('http://www.w3.org/2000/svg', 'path')
    proxy.setAttribute('d', encodedPathData)
    return proxy
  })

  appendSketchElement(context, path, pathSketch)

  // https://www.w3.org/TR/SVG11/painting.html#MarkerProperties
  // Note that for a ‘path’ element which ends with a closed sub-path,
  // the last vertex is the same as the initial vertex on the given
  // sub-path (same applies to polygon).
  const points: Point[] = []
  let currentSubPathBegin: Point
  pathData.commands.forEach(cmd => {
    switch (cmd.type) {
      case SVGPathData.MOVE_TO: {
        const p = { x: cmd.x, y: cmd.y }
        points.push(p)
        // each moveto starts a new subpath
        currentSubPathBegin = p
        break
      }
      case SVGPathData.LINE_TO:
      case SVGPathData.QUAD_TO:
      case SVGPathData.SMOOTH_QUAD_TO:
      case SVGPathData.CURVE_TO:
      case SVGPathData.SMOOTH_CURVE_TO:
      case SVGPathData.ARC:
        points.push({ x: cmd.x, y: cmd.y })
        break
      case SVGPathData.HORIZ_LINE_TO:
        points.push({ x: cmd.x, y: 0 })
        break
      case SVGPathData.VERT_LINE_TO:
        points.push({ x: 0, y: cmd.y })
        break
      case SVGPathData.CLOSE_PATH:
        if (currentSubPathBegin) {
          points.push(currentSubPathBegin)
        }
        break
    }
  })
  drawMarkers(context, path, points, svgTransform)
}

export function applyPathClip(
  context: RenderContext,
  path: SVGPathElement,
  container: SVGClipPathElement,
  svgTransform: SVGTransform | null
): void {
  const clip = document.createElementNS('http://www.w3.org/2000/svg', 'path')
  clip.setAttribute('d', path.getAttribute('d')!)
  applyTransform(context, svgTransform, clip)
  container.appendChild(clip)
}


================================================
FILE: src/geom/polygon.ts
================================================
import { Point } from 'roughjs/bin/geometry'
import { appendPatternPaint } from '../styles/pattern'
import { parseStyleConfig } from '../styles/styles'
import { applyTransform, applyMatrix } from '../transformation'
import { RenderContext } from '../types'
import { appendSketchElement, getPointsArray } from '../utils'
import { drawMarkers } from './marker'

export function drawPolygon(
  context: RenderContext,
  polygon: SVGPolygonElement,
  svgTransform: SVGTransform | null
): void {
  const points = getPointsArray(polygon)

  const transformed = points.map(p => {
    const pt = applyMatrix(p, svgTransform)
    return [pt.x, pt.y] as Point
  })

  const polygonSketch = context.rc.polygon(
    transformed,
    parseStyleConfig(context, polygon, svgTransform)
  )

  appendPatternPaint(context, polygon, () => {
    const proxy = document.createElementNS('http://www.w3.org/2000/svg', 'polygon')
    proxy.setAttribute('points', transformed.join(' '))
    return proxy
  })

  appendSketchElement(context, polygon, polygonSketch)

  // https://www.w3.org/TR/SVG11/painting.html#MarkerProperties
  // Note that for a ‘path’ element which ends with a closed sub-path,
  // the last vertex is the same as the initial vertex on the given
  // sub-path (same applies to polygon).
  if (points.length > 0) {
    points.push(points[0])
    drawMarkers(context, polygon, points, svgTransform)
  }
}

export function applyPolygonClip(
  context: RenderContext,
  polygon: SVGPolygonElement,
  container: SVGClipPathElement,
  svgTransform: SVGTransform | null
): void {
  const clip = document.createElementNS('http://www.w3.org/2000/svg', 'polygon')
  clip.setAttribute('points', polygon.getAttribute('points')!)
  applyTransform(context, svgTransform, clip)
  container.appendChild(clip)
}


================================================
FILE: src/geom/polyline.ts
================================================
import { Point } from 'roughjs/bin/geometry'
import { appendPatternPaint } from '../styles/pattern'
import { parseStyleConfig } from '../styles/styles'
import { applyMatrix } from '../transformation'
import { RenderContext } from '../types'
import { appendSketchElement, getPointsArray } from '../utils'
import { drawMarkers } from './marker'

export function drawPolyline(
  context: RenderContext,
  polyline: SVGPolylineElement,
  svgTransform: SVGTransform | null
): void {
  const points = getPointsArray(polyline)
  const transformed = points.map(p => {
    const pt = applyMatrix(p, svgTransform)
    return [pt.x, pt.y] as Point
  })
  const style = parseStyleConfig(context, polyline, svgTransform)

  appendPatternPaint(context, polyline, () => {
    const proxy = document.createElementNS('http://www.w3.org/2000/svg', 'polyline')
    proxy.setAttribute('points', transformed.join(' '))
    return proxy
  })

  if (style.fill && style.fill !== 'none') {
    const fillStyle = { ...style, stroke: 'none' }
    appendSketchElement(context, polyline, context.rc.polygon(transformed, fillStyle))
  }
  appendSketchElement(context, polyline, context.rc.linearPath(transformed, style))

  drawMarkers(context, polyline, points, svgTransform)
}


================================================
FILE: src/geom/primitives.ts
================================================
export type Point = { x: number; y: number }
export type Size = { w: number; h: number }

export type Rectangle = Point & Size

export function str(p: Point) {
  return `${p.x},${p.y}`
}

export function equals(p0: Point, p1: Point): boolean {
  return p0.x === p1.x && p0.y === p1.y
}


================================================
FILE: src/geom/rect.ts
================================================
import { appendPatternPaint } from '../styles/pattern'
import { parseStyleConfig } from '../styles/styles'
import {
  applyTransform,
  applyMatrix,
  isIdentityTransform,
  isTranslationTransform
} from '../transformation'
import { RenderContext } from '../types'
import { appendSketchElement, sketchPath } from '../utils'
import { Rectangle, str } from './primitives'

export function drawRect(
  context: RenderContext,
  rect: SVGRectElement,
  svgTransform: SVGTransform | null
): void {
  const x = rect.x.baseVal.value
  const y = rect.y.baseVal.value
  const width = rect.width.baseVal.value
  const height = rect.height.baseVal.value

  if (width === 0 || height === 0) {
    // zero-width or zero-height rect will not be rendered
    return
  }

  // Negative values are an error and result in the default value, and clamp both values to half their sides' lengths
  let rx = rect.hasAttribute('rx') ? Math.min(Math.max(0, rect.rx.baseVal.value), width / 2) : null
  let ry = rect.hasAttribute('ry') ? Math.min(Math.max(0, rect.ry.baseVal.value), height / 2) : null
  if (rx !== null || ry !== null) {
    // If only one of the two values is specified, the other has the same value
    rx = rx === null ? ry : rx
    ry = ry === null ? rx : ry
  }

  // the transformed, rectangular bounds
  const p1 = applyMatrix({ x, y }, svgTransform)
  const p2 = applyMatrix({ x: x + width, y: y + height }, svgTransform)
  const transformedWidth = p2.x - p1.x
  const transformedHeight = p2.y - p1.y
  const transformedBounds = { x: p1.x, y: p1.y, w: transformedWidth, h: transformedHeight }

  if ((isIdentityTransform(svgTransform) || isTranslationTransform(svgTransform)) && !rx && !ry) {
    // Simple case; just a rectangle
    const sketchRect = context.rc.rectangle(
      transformedBounds.x,
      transformedBounds.y,
      transformedBounds.w,
      transformedBounds.h,
      parseStyleConfig(context, rect, svgTransform)
    )

    applyPatternPaint(context, rect, transformedBounds)
    appendSketchElement(context, rect, sketchRect)
  } else {
    let path = ''
    if (rx !== null && ry !== null) {
      const factor = (4 / 3) * (Math.sqrt(2) - 1)

      // Construct path for the rounded rectangle
      // perform an absolute moveto operation to location (x+rx,y), where x is the value of the ‘rect’ element's ‘x’ attribute converted to user space, rx is the effective value of the ‘rx’ attribute converted to user space and y is the value of the ‘y’ attribute converted to user space
      const p1 = applyMatrix({ x: x + rx, y }, svgTransform)
      path += `M ${str(p1)}`
      // perform an absolute horizontal lineto operation to location (x+width-rx,y), where width is the ‘rect’ element's ‘width’ attribute converted to user space
      const p2 = applyMatrix({ x: x + width - rx, y }, svgTransform)
      path += `L ${str(p2)}`
      // perform an absolute elliptical arc operation to coordinate (x+width,y+ry), where the effective values for the ‘rx’ and ‘ry’ attributes on the ‘rect’ element converted to user space are used as the rx and ry attributes on the elliptical arc command, respectively, the x-axis-rotation is set to zero, the large-arc-flag is set to zero, and the sweep-flag is set to one
      const p3c1 = applyMatrix({ x: x + width - rx + factor * rx, y }, svgTransform)
      const p3c2 = applyMatrix({ x: x + width, y: y + factor * ry }, svgTransform)
      const p3 = applyMatrix({ x: x + width, y: y + ry }, svgTransform)
      path += `C ${str(p3c1)} ${str(p3c2)} ${str(p3)}` // We cannot use the arc command, since we no longer draw in the expected coordinates. So approximate everything with lines and béziers

      // perform a absolute vertical lineto to location (x+width,y+height-ry), where height is the ‘rect’ element's ‘height’ attribute converted to user space
      const p4 = applyMatrix({ x: x + width, y: y + height - ry }, svgTransform)
      path += `L ${str(p4)}`
      // perform an absolute elliptical arc operation to coordinate (x+width-rx,y+height)
      const p5c1 = applyMatrix({ x: x + width, y: y + height - ry + factor * ry }, svgTransform)
      const p5c2 = applyMatrix({ x: x + width - factor * rx, y: y + height }, svgTransform)
      const p5 = applyMatrix({ x: x + width - rx, y: y + height }, svgTransform)
      path += `C ${str(p5c1)} ${str(p5c2)} ${str(p5)}`
      // perform an absolute horizontal lineto to location (x+rx,y+height)
      const p6 = applyMatrix({ x: x + rx, y: y + height }, svgTransform)
      path += `L ${str(p6)}`
      // perform an absolute elliptical arc operation to coordinate (x,y+height-ry)
      const p7c1 = applyMatrix({ x: x + rx - factor * rx, y: y + height }, svgTransform)
      const p7c2 = applyMatrix({ x, y: y + height - factor * ry }, svgTransform)
      const p7 = applyMatrix({ x, y: y + height - ry }, svgTransform)
      path += `C ${str(p7c1)} ${str(p7c2)} ${str(p7)}`
      // perform an absolute absolute vertical lineto to location (x,y+ry)
      const p8 = applyMatrix({ x, y: y + ry }, svgTransform)
      path += `L ${str(p8)}`
      // perform an absolute elliptical arc operation to coordinate (x+rx,y)
      const p9c1 = applyMatrix({ x, y: y + factor * ry }, svgTransform)
      const p9c2 = applyMatrix({ x: x + factor * rx, y }, svgTransform)
      path += `C ${str(p9c1)} ${str(p9c2)} ${str(p1)}`
      path += 'z'
    } else {
      // No rounding, so just construct the respective path as a simple polygon
      const p1 = applyMatrix({ x, y }, svgTransform)
      const p2 = applyMatrix({ x: x + width, y }, svgTransform)
      const p3 = applyMatrix({ x: x + width, y: y + height }, svgTransform)
      const p4 = applyMatrix({ x, y: y + height }, svgTransform)
      path += `M ${str(p1)}`
      path += `L ${str(p2)}`
      path += `L ${str(p3)}`
      path += `L ${str(p4)}`
      path += `z`
    }

    const result = sketchPath(context, path, parseStyleConfig(context, rect, svgTransform))

    applyPatternPaint(context, rect, transformedBounds)
    appendSketchElement(context, rect, result)
  }
}

function applyPatternPaint(
  context: RenderContext,
  rect: SVGRectElement,
  { x, y, w, h }: Rectangle
): void {
  appendPatternPaint(context, rect, () => {
    const proxy = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
    proxy.x.baseVal.value = x
    proxy.y.baseVal.value = y
    proxy.width.baseVal.value = w
    proxy.height.baseVal.value = h
    return proxy
  })
}

export function applyRectClip(
  context: RenderContext,
  rect: SVGRectElement,
  container: SVGClipPathElement,
  svgTransform: SVGTransform | null
): void {
  const x = rect.x.baseVal.value
  const y = rect.y.baseVal.value
  const width = rect.width.baseVal.value
  const height = rect.height.baseVal.value

  if (width === 0 || height === 0) {
    // zero-width or zero-height rect will not be rendered
    return
  }

  const rx = rect.hasAttribute('rx') ? rect.rx.baseVal.value : null
  const ry = rect.hasAttribute('ry') ? rect.ry.baseVal.value : null

  const clip = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
  clip.x.baseVal.value = x
  clip.y.baseVal.value = y
  clip.width.baseVal.value = width
  clip.height.baseVal.value = height
  if (rx) {
    clip.rx.baseVal.value = rx
  }
  if (ry) {
    clip.ry.baseVal.value = ry
  }
  applyTransform(context, svgTransform, clip)
  container.appendChild(clip)
}


================================================
FILE: src/geom/text.ts
================================================
import { getNodeChildren } from '../dom-helpers'
import { getEffectiveAttribute } from '../styles/effective-attributes'
import { concatStyleStrings } from '../styles/styles'
import { convertToPixelUnit } from '../svg-units'
import { applyTransform } from '../transformation'
import { RenderContext } from '../types'
import { SKETCH_CLIP_ATTRIBUTE, appendSketchElement, measureText } from '../utils'
import { Size } from './primitives'

type FontAttributes = Partial<{
  fontStyle: string
  fontWeight: string
  fontSize: string
  fontFamiliy: string
}>

export function drawText(
  context: RenderContext,
  text: SVGTextElement,
  svgTransform: SVGTransform | null
): void {
  const container = document.createElementNS('http://www.w3.org/2000/svg', 'g')
  container.setAttribute('class', 'text-container')
  applyTransform(context, svgTransform, container)
  const textClone = text.cloneNode(true) as SVGTextElement
  if (textClone.transform.baseVal.numberOfItems > 0) {
    // remove transformation, since it is transformed globally by its parent container
    textClone.transform.baseVal.clear()
  }

  // clip-path is applied on the container
  textClone.removeAttribute('clip-path')

  const { cssFont, fontSize: effectiveFontSize } = getCssFont(context, text, true)
  textClone.setAttribute('style', concatStyleStrings(textClone.getAttribute('style'), cssFont))
  copyTextStyleAttributes(context, text, textClone)

  // apply styling to any tspan
  if (textClone.childElementCount > 0) {
    const children = getNodeChildren(textClone)
    const origChildren = getNodeChildren(text) as SVGElement[]
    for (let i = 0; i < children.length; i++) {
      const child = children[i]
      if (child instanceof SVGTSpanElement) {
        copyTextStyleAttributes(context, origChildren[i] as SVGTSpanElement, child)
      }
    }
  }

  container.appendChild(textClone)
  appendSketchElement(context, text, container)

  // avoid text clipping by scaling the text when changing the font
  const useCustomFontFamily = context.fontFamily !== null
  const hasClipPath = textClone.hasAttribute(SKETCH_CLIP_ATTRIBUTE)
  if (useCustomFontFamily && hasClipPath && effectiveFontSize) {
    fitFontSize(context, text, textClone, effectiveFontSize)
  }
}

/**
 * Applies a font-size on the clone such that the clone has a smaller width than the original element.
 * Only fits the width because the height is usually no problem wrt. clipping.
 */
function fitFontSize(
  context: RenderContext,
  original: SVGTextElement,
  clone: SVGTextElement,
  effectiveFontSize: string
): void {
  const { width, height } = original.getBBox()
  if (width <= 0 || height <= 0) {
    return
  }
  const fontSizePx = convertToPixelUnit(context, clone, effectiveFontSize, 'font-size')
  fitFontSizeCore(context, { w: width, h: height }, clone, fontSizePx)
}

/**
 * Recursively shrinks the font-size on the element until its width is smaller than the original width.
 */
function fitFontSizeCore(
  context: RenderContext,
  originalSize: Size,
  clone: SVGTextElement,
  fontSizePx: number
): void {
  const STEP_SIZE = 1
  const { w: cloneWidth } = measureText(context, clone)

  if (cloneWidth < originalSize.w) {
    // fits original width
    return
  }

  if (fontSizePx <= 1) {
    // already too small
    return
  }

  // try a smaller size
  const newFontSize = fontSizePx - STEP_SIZE
  clone.style.fontSize = `${newFontSize}px`

  // check again
  fitFontSizeCore(context, originalSize, clone, newFontSize)
}

/**
 * @param asStyleString Formats the return value as inline style string
 */
function getCssFont(
  context: RenderContext,
  text: SVGTextElement,
  asStyleString: boolean = false
): FontAttributes & { cssFont: string } {
  const effectiveAttributes: FontAttributes = {}

  let cssFont = ''
  const fontStyle = getEffectiveAttribute(context, text, 'font-style', context.useElementContext)
  if (fontStyle) {
    cssFont += asStyleString ? `font-style: ${fontStyle};` : fontStyle
    effectiveAttributes.fontStyle = fontStyle
  }
  const fontWeight = getEffectiveAttribute(context, text, 'font-weight', context.useElementContext)
  if (fontWeight) {
    cssFont += asStyleString ? `font-weight: ${fontWeight};` : ` ${fontWeight}`
    effectiveAttributes.fontWeight = fontWeight
  }
  const fontSize = getEffectiveAttribute(context, text, 'font-size', context.useElementContext)
  if (fontSize) {
    cssFont += asStyleString ? `font-size: ${fontSize};` : ` ${fontSize}`
    effectiveAttributes.fontSize = fontSize
  }
  if (context.fontFamily) {
    cssFont += asStyleString ? `font-family: ${context.fontFamily};` : ` ${context.fontFamily}`
    effectiveAttributes.fontFamiliy = context.fontFamily
  } else {
    const fontFamily = getEffectiveAttribute(
      context,
      text,
      'font-family',
      context.useElementContext
    )
    if (fontFamily) {
      cssFont += asStyleString ? `font-family: ${fontFamily};` : ` ${fontFamily}`
      effectiveAttributes.fontFamiliy = fontFamily
    }
  }

  cssFont = cssFont.trim()
  return { ...effectiveAttributes, cssFont }
}

function copyTextStyleAttributes(
  context: RenderContext,
  srcElement: SVGTextElement | SVGTSpanElement,
  tgtElement: SVGTextElement | SVGTSpanElement
): void {
  const stroke = getEffectiveAttribute(context, srcElement, 'stroke')
  const strokeWidth = stroke ? getEffectiveAttribute(context, srcElement, 'stroke-width') : null
  const fill = getEffectiveAttribute(context, srcElement, 'fill')
  const dominantBaseline = getEffectiveAttribute(context, srcElement, 'dominant-baseline')
  const textAnchor = getEffectiveAttribute(
    context,
    srcElement,
    'text-anchor',
    context.useElementContext
  )

  if (stroke) {
    tgtElement.setAttribute('stroke', stroke)
  }
  if (strokeWidth) {
    tgtElement.setAttribute('stroke-width', strokeWidth)
  }
  if (fill) {
    tgtElement.setAttribute('fill', fill)
  }
  if (textAnchor) {
    tgtElement.setAttribute('text-anchor', textAnchor)
  }
  if (dominantBaseline) {
    tgtElement.setAttribute('dominant-baseline', dominantBaseline)
  }
}


================================================
FILE: src/geom/use.ts
================================================
import { getCombinedTransform } from '../transformation'
import { RenderContext } from '../types'

export function drawUse(
  context: RenderContext,
  use: SVGUseElement,
  svgTransform: SVGTransform | null
): void {
  let href = use.href.baseVal
  if (href.startsWith('#')) {
    href = href.substring(1)
  }
  const defElement = context.idElements[href] as SVGElement
  if (defElement) {
    let useWidth, useHeight
    if (use.getAttribute('width') && use.getAttribute('height')) {
      // Use elements can overwrite the width which is important if it is a nested SVG
      useWidth = use.width.baseVal.value
      useHeight = use.height.baseVal.value
    }
    // We need to account for x and y attributes as well. Those change where the element is drawn.
    // We can simply change the transform to include that.
    const x = use.x.baseVal.value
    const y = use.y.baseVal.value
    let matrix = context.sourceSvg.createSVGMatrix().translate(x, y)
    matrix = svgTransform ? svgTransform.matrix.multiply(matrix) : matrix

    // the defsElement itself might have a transform that needs to be incorporated
    const elementTransform = context.sourceSvg.createSVGTransformFromMatrix(matrix)

    // use elements must be processed in their context, particularly regarding
    // the styling of them
    if (!context.useElementContext) {
      context.useElementContext = { root: use, referenced: defElement, parentContext: null }
    } else {
      const newContext = {
        root: use,
        referenced: defElement,
        parentContext: Object.assign({}, context.useElementContext)
      }
      context.useElementContext = newContext
    }

    // draw the referenced element
    context.processElement(
      context,
      defElement,
      getCombinedTransform(context, defElement as SVGGraphicsElement, elementTransform),
      useWidth,
      useHeight
    )

    // restore default context
    if (context.useElementContext.parentContext) {
      context.useElementContext = context.useElementContext.parentContext
    } else {
      context.useElementContext = null
    }
  }
}


================================================
FILE: src/index.ts
================================================
export * from './Svg2Roughjs'
export * from './OutputType'


================================================
FILE: src/processor.ts
================================================
import { applyClipPath } from './clipping'
import { getNodeChildren } from './dom-helpers'
import { drawCircle } from './geom/circle'
import { drawEllipse } from './geom/ellipse'
import { drawForeignObject } from './geom/foreign-object'
import { drawImage } from './geom/image'
import { drawLine } from './geom/line'
import { drawPath } from './geom/path'
import { drawPolygon } from './geom/polygon'
import { drawPolyline } from './geom/polyline'
import { Rectangle } from './geom/primitives'
import { drawRect } from './geom/rect'
import { drawText } from './geom/text'
import { drawUse } from './geom/use'
import { isHidden } from './styles/styles'
import { getCombinedTransform } from './transformation'
import { RenderContext } from './types'

/**
 * Traverses the SVG in DFS and draws each element to the canvas.
 * @param root either an SVG- or g-element
 * @param width Use elements can overwrite width
 * @param height Use elements can overwrite height
 */
export function processRoot(
  context: RenderContext,
  root: SVGSVGElement | SVGGElement | SVGSymbolElement | SVGMarkerElement | SVGElement,
  svgTransform: SVGTransform | null,
  width?: number,
  height?: number
): void {
  // traverse svg in DFS
  const stack: { element: SVGElement; transform: SVGTransform | null; viewBox: Rectangle }[] = []

  const currentViewBox: Rectangle = { x: 0, y: 0, w: width ?? 0, h: height ?? 0 }

  if (
    root instanceof SVGSVGElement ||
    root instanceof SVGSymbolElement ||
    root instanceof SVGMarkerElement
  ) {
    let rootX = 0
    let rootY = 0
    if (root instanceof SVGSymbolElement) {
      rootX = parseFloat(root.getAttribute('x') ?? '') || 0
      rootY = parseFloat(root.getAttribute('y') ?? '') || 0
      width = width ?? (parseFloat(root.getAttribute('width')!) || void 0)
      height = height ?? (parseFloat(root.getAttribute('height')!) || void 0)
    } else if (root instanceof SVGMarkerElement) {
      // markers use refX / refY which is applied after user-space transformation
      const mw = root.getAttribute('markerWidth')
      const mh = root.getAttribute('markerHeight')
      width = mw !== null ? parseFloat(mw) : 3 // marker-size is 3 by SVG spec
      height = mh !== null ? parseFloat(mh) : 3
    } else if (root !== context.sourceSvg) {
      // apply translation of nested elements
      rootX = root.x.baseVal.value
      rootY = root.y.baseVal.value
    }

    let rootTransform = context.sourceSvg.createSVGMatrix()

    if (root.getAttribute('viewBox')) {
      const {
        x: viewBoxX,
        y: viewBoxY,
        width: viewBoxWidth,
        height: viewBoxHeight
      } = root.viewBox.baseVal

      currentViewBox.x = viewBoxX
      currentViewBox.y = viewBoxY
      currentViewBox.w = viewBoxWidth
      currentViewBox.h = viewBoxHeight

      if (typeof width !== 'undefined' && typeof height !== 'undefined') {
        // viewBox values might scale the SVGs content
        const sx = width / viewBoxWidth
        const sy = height / viewBoxHeight
        const centerviewportX = rootX + width * 0.5
        const centerviewportY = rootY + height * 0.5
        const centerViewBoxX = viewBoxX + viewBoxWidth * 0.5
        const centerViewBoxY = viewBoxY + viewBoxHeight * 0.5
        // only support scaling from the center, e.g. xMidYMid
        rootTransform = rootTransform.translate(centerviewportX, centerviewportY)
        if (root.getAttribute('preserveAspectRatio') === 'none') {
          rootTransform = rootTransform.scaleNonUniform(sx, sy)
        } else {
          rootTransform = rootTransform.scale(Math.min(sx, sy))
        }
        rootTransform = rootTransform.translate(-centerViewBoxX, -centerViewBoxY)
      }
    } else {
      rootTransform = rootTransform.translate(rootX, rootY)
    }

    if (root instanceof SVGMarkerElement) {
      // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/refX#symbol
      // ref coordinates are interpreted as being in the coordinate system of the element contents,
      // after application of the viewBox and preserveAspectRatio attributes.
      rootTransform = rootTransform.translate(-root.refX.baseVal.value, -root.refY.baseVal.value)
    }

    const combinedMatrix = svgTransform
      ? svgTransform.matrix.multiply(rootTransform)
      : rootTransform
    svgTransform = context.sourceSvg.createSVGTransformFromMatrix(combinedMatrix)

    // don't put the SVG itself into the stack, so start with the children of it
    const children = getNodeChildren(root)
    for (let i = children.length - 1; i >= 0; i--) {
      const child = children[i] as SVGGraphicsElement
      if (child instanceof SVGSymbolElement || child instanceof SVGMarkerElement) {
        // symbols and marker can only be instantiated by specific elements
        continue
      }
      const childTransform = getCombinedTransform(context, child, svgTransform)
      stack.push({ element: child, transform: childTransform, viewBox: currentViewBox })
    }
  } else {
    stack.push({ element: root, transform: svgTransform, viewBox: currentViewBox })
  }

  while (stack.length > 0) {
    const { element, transform, viewBox } = stack.pop()!

    // maybe draw the element
    try {
      context.viewBox = viewBox
      drawElement(context, element, transform)
    } catch (e) {
      console.error(e)
    }

    if (
      element.tagName === 'defs' ||
      element.tagName === 'symbol' ||
      element.tagName === 'marker' ||
      element.tagName === 'svg' ||
      element.tagName === 'clipPath'
    ) {
      // Defs are prepocessed separately.
      // Symbols and marker can only be instantiated by specific elements.
      // Don't traverse the SVG element itself. This is done by drawElement -> processRoot.
      // ClipPaths are not drawn and processed separately.
      continue
    }
    // process children
    const children = getNodeChildren(element)
    for (let i = children.length - 1; i >= 0; i--) {
      const childElement = children[i] as SVGGraphicsElement
      const newTransform = getCombinedTransform(context, childElement, transform)
      stack.push({ element: childElement, transform: newTransform, viewBox })
    }
  }
}

export function drawRoot(
  context: RenderContext,
  element: SVGSVGElement | SVGSymbolElement,
  svgTransform: SVGTransform | null
): void {
  let width: number | undefined = parseFloat(element.getAttribute('width')!)
  let height: number | undefined = parseFloat(element.getAttribute('height')!)
  if (isNaN(width) || isNaN(height)) {
    // use only if both are set
    width = height = undefined
  }
  processRoot(context, element, svgTransform, width, height)
}

/**
 * The main switch to delegate drawing of `SVGElement`s
 * to different subroutines.
 */
function drawElement(
  context: RenderContext,
  element: SVGElement,
  svgTransform: SVGTransform | null
): void {
  if (isHidden(element)) {
    // just skip hidden elements
    return
  }

  // possibly apply a clip on the canvas before drawing on it
  const clipPath = element.getAttribute('clip-path')
  if (clipPath) {
    applyClipPath(context, element, clipPath, svgTransform)
  }

  switch (element.tagName) {
    case 'svg':
    case 'symbol':
      drawRoot(context, element as SVGSVGElement | SVGSymbolElement, svgTransform)
      break
    case 'rect':
      drawRect(context, element as SVGRectElement, svgTransform)
      break
    case 'path':
      drawPath(context, element as SVGPathElement, svgTransform)
      break
    case 'use':
      drawUse(context, element as SVGUseElement, svgTransform)
      break
    case 'line':
      drawLine(context, element as SVGLineElement, svgTransform)
      break
    case 'circle':
      drawCircle(context, element as SVGCircleElement, svgTransform)
      break
    case 'ellipse':
      drawEllipse(context, element as SVGEllipseElement, svgTransform)
      break
    case 'polyline':
      drawPolyline(context, element as SVGPolylineElement, svgTransform)
      break
    case 'polygon':
      drawPolygon(context, element as SVGPolygonElement, svgTransform)
      break
    case 'text':
      drawText(context, element as SVGTextElement, svgTransform)
      break
    case 'image':
      drawImage(context, element as SVGImageElement, svgTransform)
      break
    case 'foreignObject':
      drawForeignObject(context, element as SVGForeignObjectElement, svgTransform)
      break
  }
}


================================================
FILE: src/styles/colors.ts
================================================
import tinycolor, { Instance as TinyColorInstance } from 'tinycolor2'

/**
 * Converts an SVG gradient to a color by mixing all stop colors
 * with `tinycolor.mix`.
 */
export function gradientToColor(
  gradient: SVGLinearGradientElement | SVGRadialGradientElement,
  opacity: number
): string {
  const stops = Array.prototype.slice.apply(gradient.querySelectorAll('stop'))
  if (stops.length === 0) {
    return 'transparent'
  } else if (stops.length === 1) {
    const color = getStopColor(stops[0])
    color.setAlpha(opacity)
    return color.toString()
  } else {
    // Because roughjs can only deal with solid colors, we try to calculate
    // the average color of the gradient here.
    // The idea is to create an array of discrete (average) colors that represents the
    // gradient under consideration of the stop's offset. Thus, larger offsets
    // result in more entries of the same mixed color (of the two adjacent color stops).
    // At the end, this array is averaged again, to create a single solid color.
    const resolution = 10
    const discreteColors: TinyColorInstance[] = []

    let lastColor: TinyColorInstance | null = null
    for (let i = 0; i < stops.length; i++) {
      const currentColor = getStopColor(stops[i])
      const currentOffset = getStopOffset(stops[i])

      // combine the adjacent colors
      const combinedColor = lastColor ? averageColor([lastColor, currentColor]) : currentColor

      // fill the discrete color array depending on the offset size
      let entries = Math.max(1, (currentOffset / resolution) | 0)
      while (entries > 0) {
        discreteColors.push(combinedColor)
        entries--
      }

      lastColor = currentColor
    }

    // average the discrete colors again for the final result
    const mixedColor = averageColor(discreteColors)
    mixedColor.setAlpha(opacity)
    return mixedColor.toString()
  }
}

/**
 * Returns the `stop-color` of an `SVGStopElement`.
 */
export function getStopColor(stop: SVGStopElement): TinyColorInstance {
  let stopColorStr = stop.getAttribute('stop-color')
  if (!stopColorStr) {
    const style = stop.getAttribute('style') ?? ''
    const match = /stop-color:\s?(.*);?/.exec(style)
    if (match && match.length > 1) {
      stopColorStr = match[1]
    }
  }
  return stopColorStr ? tinycolor(stopColorStr) : tinycolor('white')
}

/**
 * Calculates the average color of the colors in the given array.
 * @returns The average color
 */
export function averageColor(colorArray: TinyColorInstance[]): TinyColorInstance {
  const count = colorArray.length
  let r = 0
  let g = 0
  let b = 0
  let a = 0
  colorArray.forEach(tinycolor => {
    const color = tinycolor.toRgb()
    r += color.r * color.r
    g += color.g * color.g
    b += color.b * color.b
    a += color.a
  })
  return tinycolor({
    r: Math.sqrt(r / count),
    g: Math.sqrt(g / count),
    b: Math.sqrt(b / count),
    a: a / count
  })
}

/**
 * Returns the `offset` of an `SVGStopElement`.
 * @return stop percentage
 */
export function getStopOffset(stop: SVGStopElement): number {
  const offset = stop.getAttribute('offset')
  if (!offset) {
    return 0
  }
  if (offset.indexOf('%')) {
    return parseFloat(offset.substring(0, offset.length - 1))
  } else {
    return parseFloat(offset) * 100
  }
}


================================================
FILE: src/styles/effective-attributes.ts
================================================
import { getParentElement } from '../dom-helpers'
import { RenderContext, UseContext } from '../types'

/**
 * Returns the attribute value of an element under consideration
 * of inherited attributes from the `parentElement`.
 * @param attributeName Name of the attribute to look up
 * @param currentUseCtx Consider different DOM hierarchy for use elements
 * @return attribute value if it exists
 */

export function getEffectiveAttribute(
  context: RenderContext,
  element: SVGElement,
  attributeName: string,
  currentUseCtx?: UseContext | null
): string | undefined {
  // getComputedStyle doesn't work for, e.g. <svg fill='rgba(...)'>
  let attr
  if (!currentUseCtx) {
    attr =
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      (getComputedStyle(element) as any)[attributeName] || element.getAttribute(attributeName)
  } else {
    // use elements traverse a different parent-hierarchy, thus we cannot use getComputedStyle here
    attr = element.getAttribute(attributeName)
  }

  if (!attr) {
    let parent: Node | null = getParentElement(element)

    const useCtx = currentUseCtx
    let nextCtx = useCtx

    if (useCtx && useCtx.referenced === element) {
      // switch context and traverse the use-element parent now
      parent = useCtx.root
      nextCtx = useCtx.parentContext
    }

    if (!parent || parent === context.sourceSvg) {
      return
    }
    return getEffectiveAttribute(context, parent as SVGElement, attributeName, nextCtx)
  }
  return attr
}

/**
 * Traverses the given elements hierarchy bottom-up to determine its effective
 * opacity attribute.
 * @param currentUseCtx Consider different DOM hierarchy for use elements
 */
export function getEffectiveElementOpacity(
  context: RenderContext,
  element: SVGElement,
  currentOpacity: number,
  currentUseCtx?: UseContext | null
): number {
  let attr: string | null
  if (!currentUseCtx) {
    attr = getComputedStyle(element)['opacity'] || element.getAttribute('opacity')
  } else {
    // use elements traverse a different parent-hierarchy, thus we cannot use getComputedStyle here
    attr = element.getAttribute('opacity')
  }
  if (attr) {
    let elementOpacity: number
    if (attr.indexOf('%') !== -1) {
      elementOpacity = Math.min(
        1,
        Math.max(0, parseFloat(attr.substring(0, attr.length - 1)) / 100)
      )
    } else {
      elementOpacity = Math.min(1, Math.max(0, parseFloat(attr)))
    }
    // combine opacities
    currentOpacity *= elementOpacity
  }
  // traverse upwards to combine parent opacities as well
  let parent: Node | null = getParentElement(element)

  const useCtx = currentUseCtx
  let nextUseCtx = useCtx

  if (useCtx && useCtx.referenced === element) {
    // switch context and traverse the use-element parent now
    parent = useCtx.root
    nextUseCtx = useCtx.parentContext
  }

  if (!parent || parent === context.sourceSvg) {
    return currentOpacity
  }

  return getEffectiveElementOpacity(context, parent as SVGElement, currentOpacity, nextUseCtx)
}


================================================
FILE: src/styles/pattern.ts
================================================
import { getIdFromUrl, reparentNodes } from '../dom-helpers'
import { RenderContext } from '../types'
import { appendSketchElement, getDefsElement, sketchFragment } from '../utils'
import { getEffectiveAttribute } from './effective-attributes'

/**
 * If the input element has a pattern stroke/fill, an additional element is added to the result,
 * which just provides the pattern storke/fill.
 * @param patternProxyCreator Should return the transformed `SVGElement` that holds the stroke/fill pattern.
 */
export function appendPatternPaint(
  context: RenderContext,
  sourceElement: SVGElement,
  patternProxyCreator: () => SVGElement
): void {
  const { fillId, strokeId } = getPatternPaintIds(context, sourceElement)
  if (fillId !== null || strokeId !== null) {
    // the additional element that should provide the pattern
    const patternProxy = patternProxyCreator()
    patternProxy.setAttribute('fill', fillId !== null ? `url(#${fillId})` : 'none')
    patternProxy.setAttribute('stroke', strokeId !== null ? `url(#${strokeId})` : 'none')

    const strokeWidth = getEffectiveAttribute(
      context,
      sourceElement,
      'stroke-width',
      context.useElementContext
    )
    patternProxy.setAttribute('stroke-width', strokeWidth ?? '0')

    // append the proxy
    appendSketchElement(context, sourceElement, patternProxy)

    // add the pattern defs
    appendPatternDefsElement(context, fillId)
    appendPatternDefsElement(context, strokeId)
  }
}

/**
 * Returns the element's referenced fill and stroke pattern ids if there are any.
 */
function getPatternPaintIds(
  context: RenderContext,
  element: SVGElement
): { fillId: string | null; strokeId: string | null } {
  function getPatternId(attributeName: string): string | null {
    const attr = element.getAttribute(attributeName)
    if (attr && attr.indexOf('url') !== -1) {
      const id = getIdFromUrl(attr)
      if (id) {
        const paint = context.idElements[id]
        if (paint instanceof SVGPatternElement) {
          return id
        }
      }
    }
    return null
  }
  return { fillId: getPatternId('fill'), strokeId: getPatternId('stroke') }
}

/**
 * Obtains the pattern fill element from the source SVG and provides it as defs element
 * in the output sketch element if missing.
 */
function appendPatternDefsElement(context: RenderContext, patternId: string | null): void {
  if (patternId === null) {
    return
  }

  const sketchDefs = getDefsElement(context)
  const defId = `#${patternId}`
  if (!sketchDefs.querySelector(defId)) {
    const sourceDefElement = context.sourceSvg.querySelector(defId) as SVGPatternElement
    if (sourceDefElement) {
      if (!context.sketchPatterns) {
        // just copy the pattern to the output
        sketchDefs.appendChild(sourceDefElement.cloneNode(true))
        return
      }

      // create a proxy for the pattern element to be sketched separately
      const patternElement = reparentNodes(
        document.createElementNS('http://www.w3.org/2000/svg', 'g'),
        sourceDefElement.cloneNode(true) as SVGPatternElement
      )

      // sketch the pattern separately from the main processor loop
      const sketchPattern = sketchFragment(context, patternElement, {
        // patterns usually don't benefit from too crazy sketch values due to their base-size
        fillStyle: 'solid',
        roughness: 0.5 // TODO ideally this should scale with the pattern's size
      })

      // move the result into an copy of the original def element
      const defElementRoot = sourceDefElement.cloneNode() as SVGPatternElement
      sketchDefs.appendChild(reparentNodes(defElementRoot, sketchPattern))
    }
  }
}


================================================
FILE: src/styles/pens.ts
================================================
import { RenderContext } from '../types'

type Range = [number, number]
type AngleConfig = { normal: Range; horizontal: Range; vertical: Range }
type WeightConfig = { normal: Range; small: Range }
type GapConfig = { normal: Range; small: Range }
type PenConfiguration = { angle: AngleConfig; weight: WeightConfig; gap: GapConfig }
export type Pen = { angle: number; weight: number; gap: number }

function getPenConfiguration(fillStyle?: string): PenConfiguration {
  // the svg2roughjs v2 config
  const legacyConfig: PenConfiguration = {
    angle: {
      normal: [-30, -50],
      horizontal: [-30, -50],
      vertical: [-30, -50]
    },
    weight: {
      normal: [0.5, 3],
      small: [0.5, 3]
    },
    gap: {
      normal: [3, 5],
      small: [3, 5]
    }
  }

  // adjusted config for more variation
  const defaultConfig: PenConfiguration = {
    angle: {
      // just lean more into the direction of the aspect ratio
      normal: [-30, -50],
      horizontal: [-50, -75],
      vertical: [-30, -15]
    },
    weight: {
      normal: [1, 3],
      small: [0.5, 1.7]
    },
    gap: {
      normal: [2, 5],
      small: [1, 3]
    }
  }

  // fine-tune configs depending on fill-style
  switch (fillStyle) {
    default:
      return defaultConfig
    case 'zigzag':
    case 'zigzag-line':
      return {
        ...defaultConfig,
        weight: { normal: [0.5, 3], small: [0.5, 2] },
        gap: { normal: [2, 6], small: [2, 5] }
      }
    case 'cross-hatch':
      return {
        ...defaultConfig,
        weight: { normal: [1, 3], small: [0.5, 1.3] },
        gap: { normal: [4, 8], small: [2, 5] }
      }
    case 'dots':
      return legacyConfig
  }
}

/**
 * Creates a random rendering configuration for the given element.
 * The returned pen is specific of the `config.fillStyle` and the element's shape.
 */
export function createPen(context: RenderContext, element: SVGElement): Pen {
  if (context.roughConfig.fillStyle === 'solid') {
    // config doesn't affect drawing
    return { angle: 0, gap: 0, weight: 0 }
  }

  // Only works when the element is in the DOM, but no need to check it here,
  // since the related methods can cope with non-finite or zero cases.
  const { width, height } = element.getBoundingClientRect()
  const aspectRatio = width / height
  const sideLength = Math.sqrt(width * height)

  const { angle, gap, weight } = getPenConfiguration(context.roughConfig.fillStyle)
  return {
    angle: getHachureAngle(context, angle, aspectRatio),
    gap: getHachureGap(context, gap, sideLength),
    weight: getFillWeight(context, weight, sideLength)
  }
}

/**
 * Returns a random hachure angle in the range of the given config.
 *
 * Rough.js default is -41deg
 */
function getHachureAngle(
  { rng }: RenderContext,
  { normal, horizontal, vertical }: AngleConfig,
  aspectRatio: number
): number {
  if (isFinite(aspectRatio)) {
    // sketch elements along the smaller side
    if (aspectRatio < 0.25) {
      return rng.next(horizontal)
    } else if (aspectRatio > 6) {
      return rng.next(vertical)
    }
  }
  return rng.next(normal)
}

/**
 * Returns a random hachure gap in the range of the given config.
 *
 * Rough.js default is 4 * strokeWidth
 */
function getHachureGap(
  { rng }: RenderContext,
  { normal, small }: GapConfig,
  sideLength: number
): number {
  return sideLength < 45 ? rng.next(small) : rng.next(normal)
}

/**
 * Returns a random fill weight in the range of the given config.
 *
 * Rough.js default is 0.5 * strokeWidth
 */
function getFillWeight(
  { rng }: RenderContext,
  { normal, small }: WeightConfig,
  sideLength: number
): number {
  return sideLength < 45 ? rng.next(small) : rng.next(normal)
}


================================================
FILE: src/styles/styles.ts
================================================
import { Options } from 'roughjs/bin/core'
import tinycolor from 'tinycolor2'
import { getIdFromUrl } from '../dom-helpers'
import { convertToPixelUnit } from '../svg-units'
import { isIdentityTransform } from '../transformation'
import { RenderContext } from '../types'
import { gradientToColor } from './colors'
import { getEffectiveAttribute, getEffectiveElementOpacity } from './effective-attributes'
import { createPen } from './pens'

/**
 * Converts the effective style attributes of the given `SVGElement`
 * to a Rough.js config object that is used to draw the element with
 * Rough.js.
 * @return config for Rough.js drawing
 */
export function parseStyleConfig(
  context: RenderContext,
  element: SVGElement,
  svgTransform: SVGTransform | null
): Options {
  const precision = context.roughConfig.fixedDecimalPlaceDigits ?? 15
  const config = Object.assign({}, context.roughConfig)

  // Scalefactor for certain style attributes. For lack of a better option here, use the determinant
  let scaleFactor = 1
  if (!isIdentityTransform(svgTransform)) {
    const m = svgTransform!.matrix
    const det = m.a * m.d - m.c * m.b
    scaleFactor = Math.sqrt(Math.abs(det))
  }

  // incorporate the elements base opacity
  const elementOpacity = getEffectiveElementOpacity(context, element, 1, context.useElementContext)

  const fill = getEffectiveAttribute(context, element, 'fill', context.useElementContext) || 'black'
  const fillOpacity = elementOpacity * getOpacity(element, 'fill-opacity')
  if (fill) {
    if (fill.indexOf('url') !== -1) {
      const gradientColor = convertGradient(context, fill, fillOpacity)
      if (gradientColor !== 'none') {
        config.fill = gradientColor
      } else {
        // delete fill, otherwise it may create an invisible 'hachure' element
        delete config.fill
      }
    } else if (fill === 'none') {
      // delete fill, otherwise it may create an invisible 'hachure' element
      delete config.fill
    } else {
      const color = tinycolor(fill)
      color.setAlpha(fillOpacity)
      config.fill = color.toString()
    }
  }

  const stroke = getEffectiveAttribute(context, element, 'stroke', context.useElementContext)
  const strokeOpacity = elementOpacity * getOpacity(element, 'stroke-opacity')
  if (stroke) {
    if (stroke.indexOf('url') !== -1) {
      config.stroke = convertGradient(context, stroke, strokeOpacity)
    } else if (stroke === 'none') {
      config.stroke = 'none'
    } else {
      const color = tinycolor(stroke)
      color.setAlpha(strokeOpacity)
      config.stroke = color.toString()
    }
  } else {
    config.stroke = 'none'
  }

  const strokeWidth = getEffectiveAttribute(
    context,
    element,
    'stroke-width',
    context.useElementContext
  )
  if (strokeWidth) {
    // Convert to user space units (px)
    const scaledWidth =
      convertToPixelUnit(context, element, strokeWidth, 'stroke-width') * scaleFactor
    config.strokeWidth = parseFloat(scaledWidth.toFixed(precision))
  } else {
    // default stroke-width is 1
    config.strokeWidth = 1
  }

  const strokeDashArray = getEffectiveAttribute(
    context,
    element,
    'stroke-dasharray',
    context.useElementContext
  )
  if (strokeDashArray && strokeDashArray !== 'none') {
    config.strokeLineDash = strokeDashArray
      .split(/[\s,]+/)
      .filter(entry => entry.length > 0)
      // make sure that dashes/dots are at least somewhat visible
      .map(dash => {
        const scaledLineDash =
          convertToPixelUnit(context, element, dash, 'stroke-dasharray') * scaleFactor
        return Math.max(0.5, parseFloat(scaledLineDash.toFixed(precision)))
      })
  }

  const strokeDashOffset = getEffectiveAttribute(
    context,
    element,
    'stroke-dashoffset',
    context.useElementContext
  )
  if (strokeDashOffset) {
    const scaledOffset =
      convertToPixelUnit(context, element, strokeDashOffset, 'stroke-dashoffset') * scaleFactor
    config.strokeLineDashOffset = parseFloat(scaledOffset.toFixed(precision))
  }

  // unstroked but filled shapes look weird, so always apply a stroke if we fill something
  if (config.fill && config.stroke === 'none') {
    config.stroke = config.fill
    config.strokeWidth = 1
  }

  if (context.randomize) {
    const { angle, gap, weight } = createPen(context, element)
    config.hachureAngle = angle
    config.hachureGap = Math.round(gap) // must be integer (avg gap in pixels)
    config.fillWeight = parseFloat(weight.toFixed(precision)) // value is used in the sketched output as-is
    // randomize double stroke effect if not explicitly set through user config
    if (typeof config.disableMultiStroke === 'undefined') {
      config.disableMultiStroke = context.rng.next() > 0.3
    }
  }

  return config
}

/**
 * Converts SVG opacity attributes to a [0, 1] range.
 */
export function getOpacity(element: SVGElement, attribute: string): number {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const attr = (getComputedStyle(element) as any)[attribute] || element.getAttribute(attribute)
  if (attr) {
    if (attr.indexOf('%') !== -1) {
      return Math.min(1, Math.max(0, parseFloat(attr.substring(0, attr.length - 1)) / 100))
    }
    return Math.min(1, Math.max(0, parseFloat(attr)))
  }
  return 1
}

/**
 * Parses a `fill` url by looking in the SVG `defs` element.
 * When a gradient is found, it is converted to a color and stored
 * in the internal defs store for this url.
 *
 * Patterns are ignored and returned with 'none'.
 *
 * @returns The parsed color
 */
export function convertGradient(context: RenderContext, url: string, opacity: number): string {
  const id = getIdFromUrl(url)
  if (!id) {
    return 'none'
  }

  const paint = context.idElements[id]
  if (!paint) {
    return 'none'
  }

  if (typeof paint === 'string') {
    // maybe it was already parsed and replaced with a color
    return paint
  } else if (
    paint instanceof SVGLinearGradientElement ||
    paint instanceof SVGRadialGradientElement
  ) {
    const color = gradientToColor(paint, opacity)
    context.idElements[id] = color
    return color
  } else {
    // pattern or something else that cannot be directly used in the roughjs config
    return 'none'
  }
}

export function isHidden(element: SVGElement): boolean {
  const style = element.style
  if (!style) {
    return false
  }
  return style.display === 'none' || style.visibility === 'hidden'
}

export function concatStyleStrings(...args: (string | null)[]): string {
  let ret = ''
  args = args.filter(s => s !== null)
  for (const style of args) {
    if (ret.length > 0 && ret[ret.length - 1] !== ';') {
      ret += ';'
    }
    ret += style
  }
  return ret
}


================================================
FILE: src/styles/textures.ts
================================================
export function createPencilFilter(): SVGFilterElement {
  const filter = document.createElementNS('http://www.w3.org/2000/svg', 'filter')
  filter.setAttribute('id', 'pencilTextureFilter')
  filter.setAttribute('x', '0%')
  filter.setAttribute('y', '0%')
  filter.setAttribute('width', '100%')
  filter.setAttribute('height', '100%')
  filter.setAttribute('filterUnits', 'objectBoundingBox')

  const feTurbulence = document.createElementNS('http://www.w3.org/2000/svg', 'feTurbulence')
  feTurbulence.setAttribute('type', 'fractalNoise')
  feTurbulence.setAttribute('baseFrequency', '2')
  feTurbulence.setAttribute('numOctaves', '5')
  feTurbulence.setAttribute('stitchTiles', 'stitch')
  feTurbulence.setAttribute('result', 'f1')
  filter.appendChild(feTurbulence)

  const feColorMatrix = document.createElementNS('http://www.w3.org/2000/svg', 'feColorMatrix')
  feColorMatrix.setAttribute('type', 'matrix')
  feColorMatrix.setAttribute('values', '0 0 0 0 0, 0 0 0 0 0, 0 0 0 0 0, 0 0 0 -1.5 1.5')
  feColorMatrix.setAttribute('result', 'f2')
  filter.appendChild(feColorMatrix)

  const feComposite = document.createElementNS('http://www.w3.org/2000/svg', 'feComposite')
  feComposite.setAttribute('operator', 'in')
  feComposite.setAttribute('in', 'SourceGraphic')
  feComposite.setAttribute('in2', 'f2')
  feComposite.setAttribute('result', 'f3')
  filter.appendChild(feComposite)

  return filter
}


================================================
FILE: src/svg-units.ts
================================================
import { getParentElement } from './dom-helpers'
import { Size } from './geom/primitives'
import { getEffectiveAttribute } from './styles/effective-attributes'
import { RenderContext } from './types'

/**
 * Dimension parsing regexp.
 *
 * https://www.w3.org/TR/css3-values/#numbers
 * "a number is either an integer, or zero or more decimal digits
 * followed by a dot (.) followed by one or more decimal digits and
 * optionally an exponent composed of "e" or "E" and an integer."
 *
 * Don't forget the signs though...
 * => ([+-]?(?:\d+|\d*\.\d+(?:[eE][+-]?\d+)?))
 *
 * To get the unit, itself, just allow any alphabetic sequence and the '%' char.
 * => ([a-z]*|%)
 */
const DIMENSION_REGEX = /^([+-]?(?:\d+|\d*\.\d+(?:[eE][+-]?\d+)?))([a-z]*|%)$/

/**
 * Commonly used dpi for unit conversion.
 */
const DPI = 96

/**
 * Conversion factors for absolute units.
 * https://developer.mozilla.org/en-US/docs/web/css/length
 */
const ABSOLUTE_UNITS: Record<string, number> = {
  in: DPI,
  cm: DPI / 2.54,
  mm: DPI / 25.4,
  pt: DPI / 72,
  pc: DPI / 6,
  px: 1
}

// pre-calculated factor for % conversion
const SQRT2 = Math.sqrt(2)

/**
 * https://www.w3.org/TR/css3-values/#dimensions
 */
type Dimension = { value: number; unit: string }

/**
 * Converts the given string to px unit. May be either a
 * [length](https://developer.mozilla.org/de/docs/Web/SVG/Content_type#Length)
 * or a [percentage](https://developer.mozilla.org/de/docs/Web/SVG/Content_type#Percentage).
 * @returns The value in px unit
 */
export function convertToPixelUnit(
  context: RenderContext,
  element: SVGElement,
  dimensionValue: string,
  attribute: string
): number {
  const { value, unit } = parseDimension(dimensionValue)
  if (isAbsoluteUnit(unit)) {
    return absToPixel(value, unit)
  }
  return relToPixel(context, element, attribute, value, unit)
}

/**
 * Parses the given string and returns a dimension, which is a
 * [number](https://www.w3.org/TR/css3-values/#numbers) followed
 * by a unit identifier.
 */
function parseDimension(dimension: string): Dimension {
  const match = dimension.match(DIMENSION_REGEX)
  if (match === null || match.length !== 3) {
    throw new Error(`Cannot parse dimension: ${dimension}`)
  }
  return { value: parseFloat(match[1]), unit: match[2].toLowerCase() || 'px' }
}

/**
 * unit-css converts per HTML spec, which is differently for percentages in SVG
 * https://www.w3.org/TR/SVG/coords.html#Units
 * https://oreillymedia.github.io/Using_SVG/guide/units.html
 * @param percentage [0, 100]
 * @param viewBox The coordinate system to evaluate the percentage against
 */
function percentageToPixel(
  attribute: string,
  percentage: number,
  { w: width, h: height }: Size = { w: 0, h: 0 }
): number {
  const fraction = percentage / 100

  // x and y are relative to the coordinate system's width or height
  if (attribute === 'x') {
    return fraction * width
  }
  if (attribute === 'y') {
    return fraction * height
  }

  return fraction * (Math.sqrt(width * width + height * height) / SQRT2)
}

/**
 * Converts an absolute unit to pixels.
 */
function absToPixel(value: number, unit: string): number {
  const conversion = ABSOLUTE_UNITS[unit] ?? 1
  return value * conversion
}

/**
 * Converts a relative unit to pixels.
 */
function relToPixel(
  context: RenderContext,
  element: SVGElement,
  attribute: string,
  value: number,
  unit: string
): number {
  const coordinateSystemSize = context.viewBox ?? { w: 0, h: 0 }

  if (unit === '%') {
    return percentageToPixel(attribute, value, coordinateSystemSize)
  }

  if (unit === 'vw' || unit === 'vh' || unit === 'vmin' || unit === 'vmax') {
    return viewportLengthToPixel(value, unit, coordinateSystemSize)
  }

  if (unit === 'em' || unit === 'ex' || unit === 'ch' || unit === 'rem') {
    return fontRelativeToPixel(context, element, value, unit)
  }

  throw new Error(`Unsupported relative length unit: ${unit}`)
}

/**
 * https://oreillymedia.github.io/Using_SVG/guide/units.html#units-viewport-reference
 */
function viewportLengthToPixel(
  value: number,
  unit: string,
  { w: width, h: height }: Size = { w: 0, h: 0 }
): number {
  const fraction = value / 100
  const refWidth = window.innerWidth ?? width
  const refHeight = window.innerHeight ?? height

  if (unit === 'vw') {
    return fraction * refWidth
  }

  if (unit === 'vh') {
    return fraction * refHeight
  }

  if (unit === 'vmin') {
    return fraction * Math.min(refWidth, refHeight)
  }

  if (unit === 'vmax') {
    return fraction * Math.max(refWidth, refHeight)
  }

  throw new Error(`Not a viewport length unit: ${unit}`)
}

/**
 * https://oreillymedia.github.io/Using_SVG/guide/units.html#units-relative-reference
 */
function fontRelativeToPixel(
  context: RenderContext,
  element: SVGElement,
  value: number,
  unit: string
): number {
  if (unit === 'rem') {
    const rootElement = document.documentElement
    const fontSizeDimension = parseDimension(getComputedStyle(rootElement).fontSize)
    const fontSizePx = fontSizeDimension.unit === 'px' ? fontSizeDimension.value : 16
    return value * fontSizePx
  }

  if (unit === 'ch') {
    const zeroCharWidth = measureZeroCharacter(element)
    return value * zeroCharWidth
  }

  // this should return a px font-size due to the getComputedStyle, otherwise use 16px as default fallback
  const effectiveFontSize =
    getEffectiveAttribute(context, element, 'font-size', context.useElementContext) ?? '16px'
  const fontSizeDimension = parseDimension(effectiveFontSize)
  const fontSizePx = fontSizeDimension.unit === 'px' ? fontSizeDimension.value : 16

  if (unit === 'em') {
    return value * fontSizePx
  }

  if (unit === 'ex') {
    return value * fontSizePx * 0.5
  }

  throw new Error(`Not a font relative unit: ${unit}`)
}

/**
 * Whether the given unit is an absolute unit.
 */
function isAbsoluteUnit(unit: string): boolean {
  return !!ABSOLUTE_UNITS[unit]
}

/**
 * Returns the width of the '0' character in the context of the element.
 */
function measureZeroCharacter(element: SVGElement): number {
  const parent = getParentElement(element)
  if (!parent) {
    return 1
  }
  const measureContainer = document.createElementNS('http://www.w3.org/2000/svg', 'text')
  measureContainer.style.visibility = 'hidden'
  measureContainer.appendChild(document.createTextNode('0'))
  parent.appendChild(measureContainer)
  const bbox = measureContainer.getBBox()
  parent.removeChild(measureContainer)
  return bbox.width
}


================================================
FILE: src/transformation.ts
================================================
import { Point } from './geom/primitives'
import { RenderContext } from './types'

/**
 * Whether the given SVGTransform resembles an identity transform.
 * @returns Whether the transform is an identity transform.
 *  Returns true if transform is undefined.
 */
export function isIdentityTransform(svgTransform: SVGTransform | null): boolean {
  if (!svgTransform) {
    return true
  }
  const matrix = svgTransform.matrix
  return (
    !matrix ||
    (matrix.a === 1 &&
      matrix.b === 0 &&
      matrix.c === 0 &&
      matrix.d === 1 &&
      matrix.e === 0 &&
      matrix.f === 0)
  )
}

/**
 * Whether the given SVGTransform does not scale nor skew.
 * @returns Whether the given SVGTransform does not scale nor skew.
 *  Returns true if transform is undefined.
 */
export function isTranslationTransform(svgTransform: SVGTransform | null): boolean {
  if (!svgTransform) {
    return true
  }
  const matrix = svgTransform.matrix
  return !matrix || (matrix.a === 1 && matrix.b === 0 && matrix.c === 0 && matrix.d === 1)
}

/**
 * Applies a given `SVGTransform` to the point.
 *
 * [a c e] [x] = (a*x + c*y + e)
 * [b d f] [y] = (b*x + d*y + f)
 * [0 0 1] [1] = (0 + 0 + 1)
 */
export function applyMatrix(point: Point, svgTransform: SVGTransform | null): Point {
  if (!svgTransform) {
    return point
  }
  const matrix = svgTransform.matrix
  const x = matrix.a * point.x + matrix.c * point.y + matrix.e
  const y = matrix.b * point.x + matrix.d * point.y + matrix.f
  return { x, y }
}

/**
 * Returns the consolidated transform of the given element.
 */
export function getSvgTransform(element: SVGGraphicsElement): SVGTransform | null {
  if (element.transform && element.transform.baseVal.numberOfItems > 0) {
    return element.transform.baseVal.consolidate()
  }
  return null
}

/**
 * Combines the given transform with the element's transform.
 * If no transform is given, it returns the SVGTransform of the element.
 */
export function getCombinedTransform(
  context: RenderContext,
  element: SVGGraphicsElement,
  transform: SVGTransform | null
): SVGTransform | null {
  if (!transform) {
    return getSvgTransform(element)
  }

  const elementTransform = getSvgTransform(element)
  if (elementTransform) {
    const elementTransformMatrix = elementTransform.matrix
    const combinedMatrix = transform.matrix.multiply(elementTransformMatrix)
    return context.sourceSvg.createSVGTransformFromMatrix(combinedMatrix)
  }
  return transform
}

/**
 * Applies the given svgTransform to the given element.
 * @param element The element to which the transform should be applied.
 */
export function applyTransform(
  context: RenderContext,
  svgTransform: SVGTransform | null,
  element: SVGGraphicsElement
): void {
  if (svgTransform && svgTransform.matrix && !isIdentityTransform(svgTransform)) {
    const matrix = svgTransform.matrix
    if (element.transform.baseVal.numberOfItems > 0) {
      element.transform.baseVal.getItem(0).setMatrix(matrix)
    } else {
      element.transform.baseVal.appendItem(svgTransform)
    }
  }
}


================================================
FILE: src/types.ts
================================================
import { Options } from 'roughjs/bin/core'
import { RoughSVG } from 'roughjs/bin/svg'
import { Rectangle } from './geom/primitives'
import { RandomNumberGenerator } from './RandomNumberGenerator'

/**
 * A context that represents the current state of the rendering,
 * which is used in the rendering functions.
 */
export type RenderContext = {
  rc: RoughSVG
  roughConfig: Options
  fontFamily: string | null
  pencilFilter: boolean
  randomize: boolean
  rng: RandomNumberGenerator
  sketchPatterns: boolean
  idElements: Record<string, SVGElement | string>
  sourceSvg: SVGSVGElement
  svgSketch: SVGSVGElement
  svgSketchIsInDOM: boolean
  svgSketchDefs?: SVGDefsElement
  useElementContext?: UseContext | null
  viewBox?: Rectangle
  styleSheets: CSSStyleSheet[]
  processElement: (
    context: RenderContext,
    root: SVGSVGElement | SVGGElement | SVGSymbolElement | SVGMarkerElement | SVGElement,
    svgTransform: SVGTransform | null,
    width?: number,
    height?: number
  ) => void
}

/**
 * The context for rendering use elements.
 */
export type UseContext = {
  referenced: SVGElement
  root: Element | null
  parentContext: UseContext | null
}


================================================
FILE: src/utils.ts
================================================
import { Options } from 'roughjs/bin/core'
import { reparentNodes } from './dom-helpers'
import { Point, Size } from './geom/primitives'
import { RenderContext } from './types'

/**
 * Attribute for storing the new clip-path IDs for the sketch output.
 */
export const SKETCH_CLIP_ATTRIBUTE = 'data-sketchy-clip-path'

/**
 * Regexp that detects curved commands in path data.
 */
const PATH_CURVES_REGEX = /[acsqt]/i

/**
 * Returns the <defs> element of the output SVG sketch.
 */
export function getDefsElement(context: RenderContext): SVGDefsElement {
  if (context.svgSketchDefs) {
    return context.svgSketchDefs
  }

  const parent = context.svgSketch
  const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs')
  if (parent.childElementCount > 0) {
    parent.insertBefore(defs, parent.firstElementChild)
  } else {
    parent.appendChild(defs)
  }

  context.svgSketchDefs = defs

  return defs
}

export function getPointsArray(element: SVGPolygonElement | SVGPolylineElement): Array<Point> {
  const pointsAttr = element.getAttribute('points')
  if (!pointsAttr) {
    return []
  }

  let coordinateRegexp
  if (pointsAttr.indexOf(' ') > 0) {
    // just assume that the coordinates (or pairs) are separated with space
    coordinateRegexp = /\s+/g
  } else {
    // there are no spaces, so assume comma separators
    coordinateRegexp = /,/g
  }

  const pointList = pointsAttr.split(coordinateRegexp)
  const points: Point[] = []
  for (let i = 0; i < pointList.length; i++) {
    const currentEntry = pointList[i]
    const coordinates = currentEntry.split(',')
    if (coordinates.length === 2) {
      points.push({ x: parseFloat(coordinates[0]), y: parseFloat(coordinates[1]) })
    } else {
      // space as separators, take next entry as y coordinate
      const next = i + 1
      if (next < pointList.length) {
        points.push({ x: parseFloat(currentEntry), y: parseFloat(pointList[next]) })
        // skip the next entry
        i = next
      }
    }
  }
  return points
}

/**
 * Helper method to append the returned `SVGGElement` from Rough.js which
 * also post processes the result e.g. by applying the clip.
 */
export function appendSketchElement(
  context: RenderContext,
  element: SVGElement,
  sketchElement: SVGElement
): void {
  let sketch = sketchElement

  // original element may have a clip-path
  const sketchClipPathId = element.getAttribute(SKETCH_CLIP_ATTRIBUTE)
  const applyPencilFilter = context.pencilFilter && element.tagName !== 'text'

  // wrap it in another container to safely apply post-processing attributes,
  // though avoid no-op <g> containers
  const isPlainContainer = sketch.tagName === 'g' && sketch.attributes.length === 0
  if (!isPlainContainer && (sketchClipPathId || applyPencilFilter)) {
    const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
    g.appendChild(sketch)
    sketch = g
  }

  if (sketchClipPathId) {
    sketch.setAttribute('clip-path', `url(#${sketchClipPathId})`)
    element.removeAttribute(SKETCH_CLIP_ATTRIBUTE)
  }

  if (applyPencilFilter) {
    sketch.setAttribute('filter', 'url(#pencilTextureFilter)')
  }

  context.svgSketch.appendChild(sketch)
}

/**
 * Helper method to sketch a path.
 * Paths with curves should utilize the preserverVertices option to avoid line disjoints.
 * For non-curved paths it looks nicer to actually allow these diskoints.
 * @returns Returns the sketched SVGElement
 */
export function sketchPath(context: RenderContext, path: string, options?: Options): SVGElement {
  if (PATH_CURVES_REGEX.test(path)) {
    options = options ? { ...options, preserveVertices: true } : { preserveVertices: true }
  }
  return context.rc.path(path, options)
}

/**
 * Helper funtion to sketch a DOM fragment.
 * Wraps the given element in an SVG and runs the processor on it to sketch the fragment.
 * The result is then unpacked and returned.
 */
export function sketchFragment(
  context: RenderContext,
  g: SVGGElement,
  roughOverwrites?: Options
): SVGGElement {
  const proxySource = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
  proxySource.appendChild(g)
  const proxyContext: RenderContext = {
    ...context,
    sourceSvg: proxySource,
    svgSketch: document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
    roughConfig: { ...context.roughConfig, ...roughOverwrites }
  }
  proxyContext.processElement(proxyContext, g, null)
  return reparentNodes(
    document.createElementNS('http://www.w3.org/2000/svg', 'g'),
    proxyContext.svgSketch
  )
}

/**
 * Measures the text in the context of the sketchSvg to account for inherited text
 * attributes.
 * The given text element must be a child of the svgSketch.
 */
export function measureText(
  { svgSketch, svgSketchIsInDOM }: RenderContext,
  text: SVGTextElement
): Size {
  const hiddenElementStyle = 'visibility:hidden;position:absolute;left:-100%;top-100%;'
  const origStyle = svgSketch.getAttribute('style')
  if (origStyle) {
    svgSketch.setAttribute('style', `${origStyle};${hiddenElementStyle}`)
  } else {
    svgSketch.setAttribute('style', hiddenElementStyle)
  }

  // the element must be in the DOM for getBBox
  const body = document.body
  const previousParent = svgSketch.parentElement
  if (!svgSketchIsInDOM) {
    body.appendChild(svgSketch)
  }
  const { width, height } = text.getBBox()

  // make sure to not change the DOM hierarchy of the element
  if (!svgSketchIsInDOM) {
    body.removeChild(svgSketch)
    if (previousParent) {
      previousParent.appendChild(svgSketch)
    }
  }

  if (origStyle) {
    svgSketch.setAttribute('style', origStyle)
  } else {
    svgSketch.removeAttribute('style')
  }

  return { w: width, h: height }
}


================================================
FILE: test/complex/bpmn-diagram/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/complex/computer-network-diagram/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/complex/flowchart-diagram/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/complex/hierarchical1-diagram/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/complex/hierarchical2-diagram/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/complex/mindmap-diagram/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/complex/movies-diagram/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/complex/organic1-diagram/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/complex/organic2-diagram/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/complex/tree-diagram/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/complex/venn-diagram/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"backgroundColor":"white"}

================================================
FILE: test/runner/complex.test.js
================================================
import { expect, fixture } from '@open-wc/testing'
import { OutputType, Svg2Roughjs } from '../../out-tsc/index'
import { compareRootElements, loadConfig, loadSvg, repackage } from './utils'
import { complexTests } from '../tests'

for (const name of complexTests) {
  describe(name, () => {
    it(`Testing complex SVG ${name}`, async () => {
      const svgTestText = loadSvg(`/test/complex/${name}/test.svg`)
      const svgExpectedText = loadSvg(`/test/complex/${name}/expect.svg`)
      const testConfig = loadConfig(`/test/complex/${name}/config.json`)

      const svgTestElement = await fixture(svgTestText)
      const svgExpectedElement = await fixture(svgExpectedText)

      const svgSketchResult = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
      const svg2roughjs = new Svg2Roughjs(svgSketchResult, OutputType.SVG, testConfig.roughConfig)
      svg2roughjs.randomize = false
      svg2roughjs.pencilFilter = false
      svg2roughjs.backgroundColor = testConfig.backgroundColor
      svg2roughjs.svg = svgTestElement
      await svg2roughjs.sketch()

      // diff the <svg> attributes
      compareRootElements(svgSketchResult, svgExpectedElement)

      // <svg> tags are not supported and ignore entirely, so move children into a div
      // https://github.com/open-wc/open-wc/issues/1229
      const sketchElement = repackage(svgSketchResult)
      const expectElement = repackage(svgExpectedElement)

      // Diff the DOMs
      // https://github.com/open-wc/open-wc/blob/master/docs/docs/testing/helpers.md
      //
      // Ensure that the expected element is provided as string, otherwise the internal
      // 'getDiffableHTML' results in different casing from the expect element (which is
      // provided as string internally as well).
      expect(sketchElement).dom.to.equal(expectElement.outerHTML, {
        ignoreAttributes: [
          'xmlns:xlink' // the downloaded expect file may have this attribute, while the generated one doesn't have it
        ]
      })
    })
  })
}


================================================
FILE: test/runner/spec.test.js
================================================
import { expect, fixture } from '@open-wc/testing'
import { OutputType, Svg2Roughjs } from '../../out-tsc/index'
import { compareRootElements, loadConfig, loadSvg, repackage } from './utils'
import { specTests } from '../tests'

for (const name of specTests) {
  describe(name, () => {
    it(`Testing render spec ${name}`, async () => {
      const svgTestText = loadSvg(`/test/specs/${name}/test.svg`)
      const svgExpectedText = loadSvg(`/test/specs/${name}/expect.svg`)
      const testConfig = loadConfig(`/test/specs/${name}/config.json`)

      const svgTestElement = await fixture(svgTestText)
      const svgExpectedElement = await fixture(svgExpectedText)

      const svgSketchResult = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
      const svg2roughjs = new Svg2Roughjs(svgSketchResult, OutputType.SVG, testConfig.roughConfig)
      svg2roughjs.randomize = false
      svg2roughjs.pencilFilter = false
      svg2roughjs.backgroundColor = testConfig.backgroundColor
      svg2roughjs.svg = svgTestElement
      await svg2roughjs.sketch()

      // diff the <svg> attributes
      compareRootElements(svgSketchResult, svgExpectedElement)

      // <svg> tags are not supported and ignore entirely, so move children into a div
      // https://github.com/open-wc/open-wc/issues/1229
      const sketchElement = repackage(svgSketchResult)
      const expectElement = repackage(svgExpectedElement)

      // Diff the DOMs
      // https://github.com/open-wc/open-wc/blob/master/docs/docs/testing/helpers.md
      //
      // Ensure that the expected element is provided as string, otherwise the internal
      // 'getDiffableHTML' results in different casing from the expect element (which is
      // provided as string internally as well).
      expect(sketchElement).dom.to.equal(expectElement.outerHTML, {
        ignoreAttributes: [
          'xmlns:xlink' // the downloaded expect file may have this attribute, while the generated one doesn't have it
        ]
      })
    })
  })
}


================================================
FILE: test/runner/utils.js
================================================
import { expect } from '@open-wc/testing'

export function loadSvg(url) {
  const request = new XMLHttpRequest()
  request.open('GET', url, false)
  request.overrideMimeType('text/plain; charset=utf-8')
  request.send()
  if (request.status !== 200) {
    throw new Error(`Unable to fetch ${url}, status code: ${request.status}`)
  }
  return request.responseText
}

export function loadConfig(url) {
  const request = new XMLHttpRequest()
  request.open('GET', url, false)
  request.send()
  if (request.status !== 200) {
    throw new Error(`Unable to fetch ${url}, status code: ${request.status}`)
  }
  return JSON.parse(request.responseText)
}

/**
 * Moves all children of the given SVG into a div to workaround
 * https://github.com/open-wc/open-wc/issues/1229
 * @param {SVGSVGElement} svg
 * @returns {HTMLDivElement}
 */
export function repackage(svg) {
  const newParent = document.createElement('div')
  newParent.className = 'svg-surrogate'
  while (svg.childNodes.length > 0) {
    newParent.appendChild(svg.childNodes[0])
  }
  return newParent
}

/**
 * Compares the attributes of the svg root elements
 * @param {SVGSVGElement} result
 * @param {SVGSVGElement} expected
 */
export function compareRootElements(result, expected) {
  const checkAttributes = ['width', 'height', 'viewBox', 'stroke-linecap']
  for (const attr of checkAttributes) {
    expect(result.getAttribute(attr)).to.equal(
      expected.getAttribute(attr),
      `<svg> attribute ${attr} not matching`
    )
  }
}


================================================
FILE: test/specs/circle-transform/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/clippath-circle/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/clippath-circle-transformed/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/clippath-ellipse/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/clippath-ellipse-transformed/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/clippath-g-element/config.json
================================================
{"roughConfig":{"bowing":0,"roughness":0,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/clippath-path/config.json
================================================
{"roughConfig":{"bowing":0,"roughness":0,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/clippath-path-transformed/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/clippath-polygon/config.json
================================================
{"roughConfig":{"bowing":0,"roughness":0,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/clippath-rect/config.json
================================================
{"roughConfig":{"bowing":0,"roughness":0,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/clippath-rect-rounded/config.json
================================================
{"roughConfig":{"bowing":0,"roughness":0,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/clippath-rect-rounded-transformed/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"backgroundColor":"white"}

================================================
FILE: test/specs/clippath-rect-rounded-transformed2/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"backgroundColor":"white"}

================================================
FILE: test/specs/clippath-rect-text/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/clippath-rect-transformed/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"backgroundColor":"white"}

================================================
FILE: test/specs/clipped-text-scaling/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/css-units/config.json
================================================
{"roughConfig":{"bowing":0,"roughness":0,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/dotted-stroke/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/ellipse-transform/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/fill-attribute/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/fill-attribute-ancestor-g/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/fill-attribute-ancestor-svg/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/fill-css-attribute-precedence/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/fill-css-attribute-precedence2/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/fill-css-class/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/fill-css-inline/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/fill-css-selector/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/fill-missing/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/foreign-object-mermaid/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/icons/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/markers/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/markers-fixed-orientation/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/markers-line/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/markers-on-line/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/markers-paths/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/markers-polygon/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/markers-polyline/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/nested-svg-translate/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/path-transform/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/path-transform2/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/pattern-circle/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"hachure","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/pattern-ellipse/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/pattern-line/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"hachure","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/pattern-path/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"hachure","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/pattern-polygon/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"hachure","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/pattern-polyline/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"hachure","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/pattern-rect/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"hachure","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/rect-not-rounded/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/rect-plain/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/rect-rounded-large-rx/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/rect-rounded-large-rx-ry/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/rect-rounded-large-ry/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/rect-rounded-rx/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fillStyle":"solid","fixedDecimalPlaceDigits":3,"seed":4242},"outputType":0,"pencilFilter":false,"sketchPatterns":true,"backgroundColor":"white"}

================================================
FILE: test/specs/rect-rounded-rx-ry/config.json
================================================
{"roughConfig":{"bowing":1,"roughness":1,"fil
Download .txt
gitextract_3crykq0g/

├── .github/
│   └── ISSUE_TEMPLATE/
│       ├── bug_report.md
│       └── feature_request.md
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .vscode/
│   ├── launch.json
│   └── settings.json
├── CODE_OF_CONDUCT.md
├── LICENSE.md
├── README.md
├── eslint.config.js
├── nodejs-cli/
│   ├── README.md
│   ├── package.json
│   └── src/
│       ├── svg2roughjs-page.js
│       └── svg2roughjs.js
├── package.json
├── rollup.config.js
├── sample-application/
│   ├── index.html
│   ├── package.json
│   ├── src/
│   │   ├── assets/
│   │   │   └── styles.css
│   │   ├── index.ts
│   │   ├── testing.ts
│   │   ├── types.d.ts
│   │   ├── utils.ts
│   │   └── vite-env.d.ts
│   ├── tsconfig.json
│   └── vite.config.js
├── src/
│   ├── OutputType.ts
│   ├── RandomNumberGenerator.ts
│   ├── Svg2Roughjs.ts
│   ├── clipping.ts
│   ├── dom-helpers.ts
│   ├── geom/
│   │   ├── circle.ts
│   │   ├── ellipse.ts
│   │   ├── foreign-object.ts
│   │   ├── image.ts
│   │   ├── line.ts
│   │   ├── marker.ts
│   │   ├── path.ts
│   │   ├── polygon.ts
│   │   ├── polyline.ts
│   │   ├── primitives.ts
│   │   ├── rect.ts
│   │   ├── text.ts
│   │   └── use.ts
│   ├── index.ts
│   ├── processor.ts
│   ├── styles/
│   │   ├── colors.ts
│   │   ├── effective-attributes.ts
│   │   ├── pattern.ts
│   │   ├── pens.ts
│   │   ├── styles.ts
│   │   └── textures.ts
│   ├── svg-units.ts
│   ├── transformation.ts
│   ├── types.ts
│   └── utils.ts
├── test/
│   ├── complex/
│   │   ├── bpmn-diagram/
│   │   │   └── config.json
│   │   ├── computer-network-diagram/
│   │   │   └── config.json
│   │   ├── flowchart-diagram/
│   │   │   └── config.json
│   │   ├── hierarchical1-diagram/
│   │   │   └── config.json
│   │   ├── hierarchical2-diagram/
│   │   │   └── config.json
│   │   ├── mindmap-diagram/
│   │   │   └── config.json
│   │   ├── movies-diagram/
│   │   │   └── config.json
│   │   ├── organic1-diagram/
│   │   │   └── config.json
│   │   ├── organic2-diagram/
│   │   │   └── config.json
│   │   ├── tree-diagram/
│   │   │   └── config.json
│   │   └── venn-diagram/
│   │       └── config.json
│   ├── runner/
│   │   ├── complex.test.js
│   │   ├── spec.test.js
│   │   └── utils.js
│   ├── specs/
│   │   ├── circle-transform/
│   │   │   └── config.json
│   │   ├── clippath-circle/
│   │   │   └── config.json
│   │   ├── clippath-circle-transformed/
│   │   │   └── config.json
│   │   ├── clippath-ellipse/
│   │   │   └── config.json
│   │   ├── clippath-ellipse-transformed/
│   │   │   └── config.json
│   │   ├── clippath-g-element/
│   │   │   └── config.json
│   │   ├── clippath-path/
│   │   │   └── config.json
│   │   ├── clippath-path-transformed/
│   │   │   └── config.json
│   │   ├── clippath-polygon/
│   │   │   └── config.json
│   │   ├── clippath-rect/
│   │   │   └── config.json
│   │   ├── clippath-rect-rounded/
│   │   │   └── config.json
│   │   ├── clippath-rect-rounded-transformed/
│   │   │   └── config.json
│   │   ├── clippath-rect-rounded-transformed2/
│   │   │   └── config.json
│   │   ├── clippath-rect-text/
│   │   │   └── config.json
│   │   ├── clippath-rect-transformed/
│   │   │   └── config.json
│   │   ├── clipped-text-scaling/
│   │   │   └── config.json
│   │   ├── css-units/
│   │   │   └── config.json
│   │   ├── dotted-stroke/
│   │   │   └── config.json
│   │   ├── ellipse-transform/
│   │   │   └── config.json
│   │   ├── fill-attribute/
│   │   │   └── config.json
│   │   ├── fill-attribute-ancestor-g/
│   │   │   └── config.json
│   │   ├── fill-attribute-ancestor-svg/
│   │   │   └── config.json
│   │   ├── fill-css-attribute-precedence/
│   │   │   └── config.json
│   │   ├── fill-css-attribute-precedence2/
│   │   │   └── config.json
│   │   ├── fill-css-class/
│   │   │   └── config.json
│   │   ├── fill-css-inline/
│   │   │   └── config.json
│   │   ├── fill-css-selector/
│   │   │   └── config.json
│   │   ├── fill-missing/
│   │   │   └── config.json
│   │   ├── foreign-object-mermaid/
│   │   │   └── config.json
│   │   ├── icons/
│   │   │   └── config.json
│   │   ├── markers/
│   │   │   └── config.json
│   │   ├── markers-fixed-orientation/
│   │   │   └── config.json
│   │   ├── markers-line/
│   │   │   └── config.json
│   │   ├── markers-on-line/
│   │   │   └── config.json
│   │   ├── markers-paths/
│   │   │   └── config.json
│   │   ├── markers-polygon/
│   │   │   └── config.json
│   │   ├── markers-polyline/
│   │   │   └── config.json
│   │   ├── nested-svg-translate/
│   │   │   └── config.json
│   │   ├── path-transform/
│   │   │   └── config.json
│   │   ├── path-transform2/
│   │   │   └── config.json
│   │   ├── pattern-circle/
│   │   │   └── config.json
│   │   ├── pattern-ellipse/
│   │   │   └── config.json
│   │   ├── pattern-line/
│   │   │   └── config.json
│   │   ├── pattern-path/
│   │   │   └── config.json
│   │   ├── pattern-polygon/
│   │   │   └── config.json
│   │   ├── pattern-polyline/
│   │   │   └── config.json
│   │   ├── pattern-rect/
│   │   │   └── config.json
│   │   ├── rect-not-rounded/
│   │   │   └── config.json
│   │   ├── rect-plain/
│   │   │   └── config.json
│   │   ├── rect-rounded-large-rx/
│   │   │   └── config.json
│   │   ├── rect-rounded-large-rx-ry/
│   │   │   └── config.json
│   │   ├── rect-rounded-large-ry/
│   │   │   └── config.json
│   │   ├── rect-rounded-rx/
│   │   │   └── config.json
│   │   ├── rect-rounded-rx-ry/
│   │   │   └── config.json
│   │   ├── rect-rounded-ry/
│   │   │   └── config.json
│   │   ├── rect-rounded-transform/
│   │   │   └── config.json
│   │   ├── rect-rounded-transform-mirror/
│   │   │   └── config.json
│   │   ├── rect-transform/
│   │   │   └── config.json
│   │   ├── rect-transform-from-g/
│   │   │   └── config.json
│   │   ├── stroke-attribute/
│   │   │   └── config.json
│   │   ├── stroke-attribute-ancestor-g/
│   │   │   └── config.json
│   │   ├── stroke-attribute-ancestor-g-override/
│   │   │   └── config.json
│   │   ├── stroke-attribute-ancestor-g2/
│   │   │   └── config.json
│   │   ├── stroke-attribute-ancestor-svg/
│   │   │   └── config.json
│   │   ├── stroke-missing-is-transparent/
│   │   │   └── config.json
│   │   ├── stroke-none-is-transparent/
│   │   │   └── config.json
│   │   ├── stroke-width-attribute/
│   │   │   └── config.json
│   │   ├── stroke-width-scale-transform/
│   │   │   └── config.json
│   │   ├── svg-image-element/
│   │   │   └── config.json
│   │   ├── symbols/
│   │   │   └── config.json
│   │   ├── symbols-non-uniform-scale/
│   │   │   └── config.json
│   │   ├── symbols2/
│   │   │   └── config.json
│   │   ├── text-css/
│   │   │   └── config.json
│   │   ├── text-dominant-baseline-basic/
│   │   │   └── config.json
│   │   ├── text-glyph-positioning/
│   │   │   └── config.json
│   │   ├── text-rotated-glyphs/
│   │   │   └── config.json
│   │   ├── text-simple-tspans/
│   │   │   └── config.json
│   │   ├── text-stroked-and-decorated/
│   │   │   └── config.json
│   │   ├── text-tspan-styling/
│   │   │   └── config.json
│   │   ├── text-tspans-mixed/
│   │   │   └── config.json
│   │   ├── text-tspans-repositioned/
│   │   │   └── config.json
│   │   ├── text-whitespace/
│   │   │   └── config.json
│   │   ├── text-width-custom-font/
│   │   │   └── config.json
│   │   ├── uml-node-style/
│   │   │   └── config.json
│   │   ├── use-element-styling/
│   │   │   └── config.json
│   │   ├── use-reference-group/
│   │   │   └── config.json
│   │   ├── viewbox-negative/
│   │   │   └── config.json
│   │   ├── viewbox-non-uniform/
│   │   │   └── config.json
│   │   └── viewbox-non-uniform-translated/
│   │       └── config.json
│   ├── tests.js
│   └── umd-bundle/
│       └── umd-bundle-test.html
├── tsconfig.json
└── web-test-runner.config.mjs
Download .txt
SYMBOL INDEX (143 symbols across 37 files)

FILE: nodejs-cli/src/svg2roughjs-page.js
  function svg2roughjsPage (line 6) | function svg2roughjsPage(inputSvg, svg2roughjsArgs) {
  function createSvg2RoughjsInstance (line 33) | function createSvg2RoughjsInstance(args) {

FILE: nodejs-cli/src/svg2roughjs.js
  function loadInputSvg (line 38) | function loadInputSvg(argv) {
  function saveSketch (line 60) | function saveSketch(content, outputFilePath) {
  function getSvg2RoughjsArgs (line 69) | function getSvg2RoughjsArgs(argv) {

FILE: rollup.config.js
  function matchSubmodules (line 9) | function matchSubmodules(externals) {

FILE: sample-application/src/index.ts
  function setCodeMirrorValue (line 61) | function setCodeMirrorValue(value: string) {
  function getSvgSize (line 68) | function getSvgSize(svg: SVGSVGElement): { width: number; height: number...
  function loadSvgString (line 100) | function loadSvgString(fileContent: string) {
  function loadSample (line 172) | function loadSample(sample: string) {
  function updateOpacity (line 215) | function updateOpacity(inputContainerOpacity: number) {
  function run (line 222) | function run() {
  function setUIState (line 426) | function setUIState(enabled: boolean) {

FILE: sample-application/src/testing.ts
  function initializeTestUI (line 12) | function initializeTestUI(svg2roughjs: Svg2Roughjs) {
  function onTestcaseChange (line 44) | async function onTestcaseChange(testName: string) {
  function loadSvg (line 49) | function loadSvg(url: string) {
  function isExistingTestcase (line 60) | function isExistingTestcase(testcase: string): boolean {
  function downloadTestcase (line 67) | async function downloadTestcase(svg2roughjs: Svg2Roughjs) {

FILE: sample-application/src/utils.ts
  function downloadFile (line 1) | function downloadFile(content: string, mime: string, fileName: string) {

FILE: src/OutputType.ts
  type OutputType (line 1) | enum OutputType {

FILE: src/RandomNumberGenerator.ts
  class RandomNumberGenerator (line 6) | class RandomNumberGenerator {
    method constructor (line 8) | constructor(seed: number | null) {
    method next (line 16) | next(range?: [number, number]): number {

FILE: src/Svg2Roughjs.ts
  class Svg2Roughjs (line 13) | class Svg2Roughjs {
    method svg (line 68) | set svg(svg: SVGSVGElement) {
    method svg (line 78) | get svg(): SVGSVGElement | undefined {
    method outputType (line 92) | set outputType(type: OutputType) {
    method outputType (line 112) | get outputType(): OutputType {
    method roughConfig (line 122) | set roughConfig(config: Options) {
    method roughConfig (line 132) | get roughConfig(): Options {
    method constructor (line 146) | constructor(
    method sketch (line 179) | sketch(sourceSvgChanged = false): Promise<SVGSVGElement | HTMLCanvasEl...
    method createRenderContext (line 228) | private createRenderContext(sketchContainer: SVGSVGElement): RenderCon...
    method drawToCanvas (line 258) | private drawToCanvas(
    method prepareRenderContainer (line 280) | private prepareRenderContainer(): SVGSVGElement {
    method sourceSvgChanged (line 328) | private sourceSvgChanged() {
    method collectElementsWithID (line 343) | private collectElementsWithID() {
    method coerceSize (line 359) | private coerceSize(svg: SVGSVGElement, property: 'width' | 'height', f...

FILE: src/clipping.ts
  function applyClipPath (line 14) | function applyClipPath(
  function applyElementClip (line 85) | function applyElementClip(
  function storeSketchClipId (line 117) | function storeSketchClipId(element: SVGElement, id: string): void {

FILE: src/dom-helpers.ts
  function getNodeChildren (line 7) | function getNodeChildren(element: Element): Element[] {
  function getParentElement (line 27) | function getParentElement(node: Node): Element | null {
  function getMatchedCssRules (line 40) | function getMatchedCssRules(context: RenderContext, el: Element): CSSSty...
  function reparentNodes (line 67) | function reparentNodes<T extends SVGElement>(newParent: T, source: SVGEl...
  function getIdFromUrl (line 77) | function getIdFromUrl(url: string | null): string | null {

FILE: src/geom/circle.ts
  function drawCircle (line 13) | function drawCircle(
  function applyCircleClip (line 66) | function applyCircleClip(

FILE: src/geom/ellipse.ts
  function drawEllipse (line 13) | function drawEllipse(
  function applyEllipseClip (line 70) | function applyEllipseClip(

FILE: src/geom/foreign-object.ts
  function drawForeignObject (line 5) | function drawForeignObject(

FILE: src/geom/image.ts
  function drawImage (line 5) | function drawImage(

FILE: src/geom/line.ts
  function drawLine (line 8) | function drawLine(

FILE: src/geom/marker.ts
  function drawMarkers (line 7) | function drawMarkers(
  function getScaleFactor (line 138) | function getScaleFactor(
  function isClosedPath (line 158) | function isClosedPath(points: Point[]): boolean {
  function getBisectingAngle (line 169) | function getBisectingAngle(prevPt: Point, crossingPt: Point, nextPt: Poi...
  function getOppositeAngle (line 189) | function getOppositeAngle(angle: number): number {
  function getAngle (line 200) | function getAngle(v1: Point, v2: Point): number {

FILE: src/geom/path.ts
  function drawPath (line 10) | function drawPath(
  function applyPathClip (line 103) | function applyPathClip(

FILE: src/geom/polygon.ts
  function drawPolygon (line 9) | function drawPolygon(
  function applyPolygonClip (line 44) | function applyPolygonClip(

FILE: src/geom/polyline.ts
  function drawPolyline (line 9) | function drawPolyline(

FILE: src/geom/primitives.ts
  type Point (line 1) | type Point = { x: number; y: number }
  type Size (line 2) | type Size = { w: number; h: number }
  type Rectangle (line 4) | type Rectangle = Point & Size
  function str (line 6) | function str(p: Point) {
  function equals (line 10) | function equals(p0: Point, p1: Point): boolean {

FILE: src/geom/rect.ts
  function drawRect (line 13) | function drawRect(
  function applyPatternPaint (line 118) | function applyPatternPaint(
  function applyRectClip (line 133) | function applyRectClip(

FILE: src/geom/text.ts
  type FontAttributes (line 10) | type FontAttributes = Partial<{
  function drawText (line 17) | function drawText(
  function fitFontSize (line 65) | function fitFontSize(
  function fitFontSizeCore (line 82) | function fitFontSizeCore(
  function getCssFont (line 112) | function getCssFont(
  function copyTextStyleAttributes (line 155) | function copyTextStyleAttributes(

FILE: src/geom/use.ts
  function drawUse (line 4) | function drawUse(

FILE: src/processor.ts
  function processRoot (line 25) | function processRoot(
  function drawRoot (line 158) | function drawRoot(
  function drawElement (line 176) | function drawElement(

FILE: src/styles/colors.ts
  function gradientToColor (line 7) | function gradientToColor(
  function getStopColor (line 56) | function getStopColor(stop: SVGStopElement): TinyColorInstance {
  function averageColor (line 72) | function averageColor(colorArray: TinyColorInstance[]): TinyColorInstance {
  function getStopOffset (line 97) | function getStopOffset(stop: SVGStopElement): number {

FILE: src/styles/effective-attributes.ts
  function getEffectiveAttribute (line 12) | function getEffectiveAttribute(
  function getEffectiveElementOpacity (line 54) | function getEffectiveElementOpacity(

FILE: src/styles/pattern.ts
  function appendPatternPaint (line 11) | function appendPatternPaint(
  function getPatternPaintIds (line 43) | function getPatternPaintIds(
  function appendPatternDefsElement (line 67) | function appendPatternDefsElement(context: RenderContext, patternId: str...

FILE: src/styles/pens.ts
  type Range (line 3) | type Range = [number, number]
  type AngleConfig (line 4) | type AngleConfig = { normal: Range; horizontal: Range; vertical: Range }
  type WeightConfig (line 5) | type WeightConfig = { normal: Range; small: Range }
  type GapConfig (line 6) | type GapConfig = { normal: Range; small: Range }
  type PenConfiguration (line 7) | type PenConfiguration = { angle: AngleConfig; weight: WeightConfig; gap:...
  type Pen (line 8) | type Pen = { angle: number; weight: number; gap: number }
  function getPenConfiguration (line 10) | function getPenConfiguration(fillStyle?: string): PenConfiguration {
  function createPen (line 72) | function createPen(context: RenderContext, element: SVGElement): Pen {
  function getHachureAngle (line 97) | function getHachureAngle(
  function getHachureGap (line 118) | function getHachureGap(
  function getFillWeight (line 131) | function getFillWeight(

FILE: src/styles/styles.ts
  function parseStyleConfig (line 17) | function parseStyleConfig(
  function getOpacity (line 142) | function getOpacity(element: SVGElement, attribute: string): number {
  function convertGradient (line 163) | function convertGradient(context: RenderContext, url: string, opacity: n...
  function isHidden (line 190) | function isHidden(element: SVGElement): boolean {
  function concatStyleStrings (line 198) | function concatStyleStrings(...args: (string | null)[]): string {

FILE: src/styles/textures.ts
  function createPencilFilter (line 1) | function createPencilFilter(): SVGFilterElement {

FILE: src/svg-units.ts
  constant DIMENSION_REGEX (line 20) | const DIMENSION_REGEX = /^([+-]?(?:\d+|\d*\.\d+(?:[eE][+-]?\d+)?))([a-z]...
  constant DPI (line 25) | const DPI = 96
  constant ABSOLUTE_UNITS (line 31) | const ABSOLUTE_UNITS: Record<string, number> = {
  constant SQRT2 (line 41) | const SQRT2 = Math.sqrt(2)
  type Dimension (line 46) | type Dimension = { value: number; unit: string }
  function convertToPixelUnit (line 54) | function convertToPixelUnit(
  function parseDimension (line 72) | function parseDimension(dimension: string): Dimension {
  function percentageToPixel (line 87) | function percentageToPixel(
  function absToPixel (line 108) | function absToPixel(value: number, unit: string): number {
  function relToPixel (line 116) | function relToPixel(
  function viewportLengthToPixel (line 143) | function viewportLengthToPixel(
  function fontRelativeToPixel (line 174) | function fontRelativeToPixel(
  function isAbsoluteUnit (line 212) | function isAbsoluteUnit(unit: string): boolean {
  function measureZeroCharacter (line 219) | function measureZeroCharacter(element: SVGElement): number {

FILE: src/transformation.ts
  function isIdentityTransform (line 9) | function isIdentityTransform(svgTransform: SVGTransform | null): boolean {
  function isTranslationTransform (line 30) | function isTranslationTransform(svgTransform: SVGTransform | null): bool...
  function applyMatrix (line 45) | function applyMatrix(point: Point, svgTransform: SVGTransform | null): P...
  function getSvgTransform (line 58) | function getSvgTransform(element: SVGGraphicsElement): SVGTransform | nu...
  function getCombinedTransform (line 69) | function getCombinedTransform(
  function applyTransform (line 91) | function applyTransform(

FILE: src/types.ts
  type RenderContext (line 10) | type RenderContext = {
  type UseContext (line 38) | type UseContext = {

FILE: src/utils.ts
  constant SKETCH_CLIP_ATTRIBUTE (line 9) | const SKETCH_CLIP_ATTRIBUTE = 'data-sketchy-clip-path'
  constant PATH_CURVES_REGEX (line 14) | const PATH_CURVES_REGEX = /[acsqt]/i
  function getDefsElement (line 19) | function getDefsElement(context: RenderContext): SVGDefsElement {
  function getPointsArray (line 37) | function getPointsArray(element: SVGPolygonElement | SVGPolylineElement)...
  function appendSketchElement (line 76) | function appendSketchElement(
  function sketchPath (line 114) | function sketchPath(context: RenderContext, path: string, options?: Opti...
  function sketchFragment (line 126) | function sketchFragment(
  function measureText (line 151) | function measureText(

FILE: test/runner/utils.js
  function loadSvg (line 3) | function loadSvg(url) {
  function loadConfig (line 14) | function loadConfig(url) {
  function repackage (line 30) | function repackage(svg) {
  function compareRootElements (line 44) | function compareRootElements(result, expected) {

FILE: web-test-runner.config.mjs
  method filterBrowserLogs (line 49) | filterBrowserLogs(log) {
Condensed preview — 164 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (254K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 432,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 467,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Is"
  },
  {
    "path": ".gitignore",
    "chars": 110,
    "preview": "node_modules\nsample-application/dist\nsvg2roughjs-*.tgz\n.idea\ndist\ndebug.log\nout-tsc\ncoverage\n/nodejs-cli/*.svg"
  },
  {
    "path": ".prettierignore",
    "chars": 26,
    "preview": "bundled/*\ndist/*\nout-tsc/*"
  },
  {
    "path": ".prettierrc",
    "chars": 177,
    "preview": "{\n  \"tabWidth\": 2,\n  \"useTabs\": false,\n  \"semi\": false,\n  \"singleQuote\": true,\n  \"printWidth\": 100,\n  \"endOfLine\": \"auto"
  },
  {
    "path": ".vscode/launch.json",
    "chars": 484,
    "preview": "{\n    // Use IntelliSense to learn about possible attributes.\n    // Hover to view descriptions of existing attributes.\n"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 40,
    "preview": "{\n  \"eslint.validate\": [\"typescript\"]\n}\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 5481,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
  },
  {
    "path": "LICENSE.md",
    "chars": 1075,
    "preview": "Copyright 2022 Fabian Schwarzkopf, Johannes Rössel\n\nPermission is hereby granted, free of charge, to any person obtainin"
  },
  {
    "path": "README.md",
    "chars": 10546,
    "preview": "# svg2rough.js\n\n<p align=\"center\">\n  <img src=\"https://fskpf.github.io/static/assets/svg2roughjs-hero-sketch.svg\" alt=\"h"
  },
  {
    "path": "eslint.config.js",
    "chars": 828,
    "preview": "import js from '@eslint/js'\nimport tseslint from 'typescript-eslint'\nimport prettier from 'eslint-plugin-prettier/recomm"
  },
  {
    "path": "nodejs-cli/README.md",
    "chars": 1964,
    "preview": "# CLI\n\nCreate sketchs of an SVG on the command-line with [Node.js](https://nodejs.org/)\nand [Puppeteer](https://pptr.dev"
  },
  {
    "path": "nodejs-cli/package.json",
    "chars": 503,
    "preview": "{\n  \"name\": \"svg2roughjs-nodejs\",\n  \"version\": \"1.0.0\",\n  \"description\": \"An svg2roughjs CLI that runs in Nodejs with a "
  },
  {
    "path": "nodejs-cli/src/svg2roughjs-page.js",
    "chars": 1388,
    "preview": "/**\n * @param {string} inputSvg The SVG that should be converted\n * @param {Record<string,unknown>} svg2roughjsArgs The "
  },
  {
    "path": "nodejs-cli/src/svg2roughjs.js",
    "chars": 2278,
    "preview": "import puppeteer from 'puppeteer'\nimport minimist from 'minimist'\nimport fs from 'fs'\nimport { svg2roughjsPage } from '."
  },
  {
    "path": "package.json",
    "chars": 1788,
    "preview": "{\n  \"name\": \"svg2roughjs\",\n  \"version\": \"3.2.3\",\n  \"description\": \"Leverages Rough.js to convert SVGs to a hand-drawn, s"
  },
  {
    "path": "rollup.config.js",
    "chars": 1569,
    "preview": "import commonjs from '@rollup/plugin-commonjs'\nimport resolve from '@rollup/plugin-node-resolve'\nimport dts from 'rollup"
  },
  {
    "path": "sample-application/index.html",
    "chars": 5163,
    "preview": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"description\"\n    content=\"Convert your SVG file to"
  },
  {
    "path": "sample-application/package.json",
    "chars": 558,
    "preview": "{\n  \"name\": \"svg2roughjs-sample\",\n  \"description\": \"A simple sample application to test and try svg2roughjs\",\n  \"version"
  },
  {
    "path": "sample-application/src/assets/styles.css",
    "chars": 3059,
    "preview": "body * {\n  box-sizing: border-box;\n}\n\nbody {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  right: 0;\n  left: 0;\n  margi"
  },
  {
    "path": "sample-application/src/index.ts",
    "chars": 13767,
    "preview": "import './assets/styles.css'\nimport { EditorView } from 'codemirror'\nimport { syntaxHighlighting, defaultHighlightStyle "
  },
  {
    "path": "sample-application/src/testing.ts",
    "chars": 4337,
    "preview": "import { OutputType, Svg2Roughjs } from '../../src/index'\nimport { downloadFile } from './utils'\nimport { specTests } fr"
  },
  {
    "path": "sample-application/src/types.d.ts",
    "chars": 138,
    "preview": "declare module '*.svg' {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  const content: any\n  export "
  },
  {
    "path": "sample-application/src/utils.ts",
    "chars": 270,
    "preview": "export function downloadFile(content: string, mime: string, fileName: string) {\n  const link = document.createElement('a"
  },
  {
    "path": "sample-application/src/vite-env.d.ts",
    "chars": 38,
    "preview": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "sample-application/tsconfig.json",
    "chars": 571,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"module\": \"ESNext\",\n    \"lib\":"
  },
  {
    "path": "sample-application/vite.config.js",
    "chars": 94,
    "preview": "import { defineConfig } from 'vite'\n\nexport default defineConfig({\n  publicDir: '../test/'\n})\n"
  },
  {
    "path": "src/OutputType.ts",
    "chars": 43,
    "preview": "export enum OutputType {\n  SVG,\n  CANVAS\n}\n"
  },
  {
    "path": "src/RandomNumberGenerator.ts",
    "chars": 656,
    "preview": "import { Random } from 'roughjs/bin/math'\n\n/**\n * A simple random number generator that allows for seeding.\n */\nexport c"
  },
  {
    "path": "src/Svg2Roughjs.ts",
    "chars": 12317,
    "preview": "import { Options } from 'roughjs/bin/core'\nimport rough from 'roughjs/bin/rough'\nimport { OutputType } from './OutputTyp"
  },
  {
    "path": "src/clipping.ts",
    "chars": 4352,
    "preview": "import { getIdFromUrl, getNodeChildren } from './dom-helpers'\nimport { applyCircleClip } from './geom/circle'\nimport { a"
  },
  {
    "path": "src/dom-helpers.ts",
    "chars": 2381,
    "preview": "import { RenderContext } from './types'\n\n/**\n * Returns the Node's children, since Node.prototype.children is not availa"
  },
  {
    "path": "src/geom/circle.ts",
    "chars": 3113,
    "preview": "import { appendPatternPaint } from '../styles/pattern'\nimport { parseStyleConfig } from '../styles/styles'\nimport {\n  ap"
  },
  {
    "path": "src/geom/ellipse.ts",
    "chars": 3461,
    "preview": "import { appendPatternPaint } from '../styles/pattern'\nimport { parseStyleConfig } from '../styles/styles'\nimport {\n  ap"
  },
  {
    "path": "src/geom/foreign-object.ts",
    "chars": 1577,
    "preview": "import { applyTransform } from '../transformation'\nimport { RenderContext } from '../types'\nimport { appendSketchElement"
  },
  {
    "path": "src/geom/image.ts",
    "chars": 1910,
    "preview": "import { applyTransform } from '../transformation'\nimport { RenderContext } from '../types'\nimport { appendSketchElement"
  },
  {
    "path": "src/geom/line.ts",
    "chars": 1282,
    "preview": "import { appendPatternPaint } from '../styles/pattern'\nimport { parseStyleConfig } from '../styles/styles'\nimport { appl"
  },
  {
    "path": "src/geom/marker.ts",
    "chars": 7650,
    "preview": "import { getIdFromUrl } from '../dom-helpers'\nimport { getEffectiveAttribute } from '../styles/effective-attributes'\nimp"
  },
  {
    "path": "src/geom/path.ts",
    "chars": 3726,
    "preview": "import { encodeSVGPath, SVGPathData, SVGPathDataTransformer } from 'svg-pathdata'\nimport { appendPatternPaint } from '.."
  },
  {
    "path": "src/geom/polygon.ts",
    "chars": 1793,
    "preview": "import { Point } from 'roughjs/bin/geometry'\nimport { appendPatternPaint } from '../styles/pattern'\nimport { parseStyleC"
  },
  {
    "path": "src/geom/polyline.ts",
    "chars": 1250,
    "preview": "import { Point } from 'roughjs/bin/geometry'\nimport { appendPatternPaint } from '../styles/pattern'\nimport { parseStyleC"
  },
  {
    "path": "src/geom/primitives.ts",
    "chars": 286,
    "preview": "export type Point = { x: number; y: number }\nexport type Size = { w: number; h: number }\n\nexport type Rectangle = Point "
  },
  {
    "path": "src/geom/rect.ts",
    "chars": 7387,
    "preview": "import { appendPatternPaint } from '../styles/pattern'\nimport { parseStyleConfig } from '../styles/styles'\nimport {\n  ap"
  },
  {
    "path": "src/geom/text.ts",
    "chars": 6084,
    "preview": "import { getNodeChildren } from '../dom-helpers'\nimport { getEffectiveAttribute } from '../styles/effective-attributes'\n"
  },
  {
    "path": "src/geom/use.ts",
    "chars": 2101,
    "preview": "import { getCombinedTransform } from '../transformation'\nimport { RenderContext } from '../types'\n\nexport function drawU"
  },
  {
    "path": "src/index.ts",
    "chars": 59,
    "preview": "export * from './Svg2Roughjs'\nexport * from './OutputType'\n"
  },
  {
    "path": "src/processor.ts",
    "chars": 8390,
    "preview": "import { applyClipPath } from './clipping'\nimport { getNodeChildren } from './dom-helpers'\nimport { drawCircle } from '."
  },
  {
    "path": "src/styles/colors.ts",
    "chars": 3304,
    "preview": "import tinycolor, { Instance as TinyColorInstance } from 'tinycolor2'\n\n/**\n * Converts an SVG gradient to a color by mix"
  },
  {
    "path": "src/styles/effective-attributes.ts",
    "chars": 3038,
    "preview": "import { getParentElement } from '../dom-helpers'\nimport { RenderContext, UseContext } from '../types'\n\n/**\n * Returns t"
  },
  {
    "path": "src/styles/pattern.ts",
    "chars": 3680,
    "preview": "import { getIdFromUrl, reparentNodes } from '../dom-helpers'\nimport { RenderContext } from '../types'\nimport { appendSke"
  },
  {
    "path": "src/styles/pens.ts",
    "chars": 3700,
    "preview": "import { RenderContext } from '../types'\n\ntype Range = [number, number]\ntype AngleConfig = { normal: Range; horizontal: "
  },
  {
    "path": "src/styles/styles.ts",
    "chars": 6719,
    "preview": "import { Options } from 'roughjs/bin/core'\nimport tinycolor from 'tinycolor2'\nimport { getIdFromUrl } from '../dom-helpe"
  },
  {
    "path": "src/styles/textures.ts",
    "chars": 1408,
    "preview": "export function createPencilFilter(): SVGFilterElement {\n  const filter = document.createElementNS('http://www.w3.org/20"
  },
  {
    "path": "src/svg-units.ts",
    "chars": 6486,
    "preview": "import { getParentElement } from './dom-helpers'\nimport { Size } from './geom/primitives'\nimport { getEffectiveAttribute"
  },
  {
    "path": "src/transformation.ts",
    "chars": 3064,
    "preview": "import { Point } from './geom/primitives'\nimport { RenderContext } from './types'\n\n/**\n * Whether the given SVGTransform"
  },
  {
    "path": "src/types.ts",
    "chars": 1164,
    "preview": "import { Options } from 'roughjs/bin/core'\nimport { RoughSVG } from 'roughjs/bin/svg'\nimport { Rectangle } from './geom/"
  },
  {
    "path": "src/utils.ts",
    "chars": 5717,
    "preview": "import { Options } from 'roughjs/bin/core'\nimport { reparentNodes } from './dom-helpers'\nimport { Point, Size } from './"
  },
  {
    "path": "test/complex/bpmn-diagram/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/complex/computer-network-diagram/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/complex/flowchart-diagram/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/complex/hierarchical1-diagram/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/complex/hierarchical2-diagram/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/complex/mindmap-diagram/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/complex/movies-diagram/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/complex/organic1-diagram/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/complex/organic2-diagram/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/complex/tree-diagram/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/complex/venn-diagram/config.json",
    "chars": 164,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/runner/complex.test.js",
    "chars": 2030,
    "preview": "import { expect, fixture } from '@open-wc/testing'\nimport { OutputType, Svg2Roughjs } from '../../out-tsc/index'\nimport "
  },
  {
    "path": "test/runner/spec.test.js",
    "chars": 2018,
    "preview": "import { expect, fixture } from '@open-wc/testing'\nimport { OutputType, Svg2Roughjs } from '../../out-tsc/index'\nimport "
  },
  {
    "path": "test/runner/utils.js",
    "chars": 1502,
    "preview": "import { expect } from '@open-wc/testing'\n\nexport function loadSvg(url) {\n  const request = new XMLHttpRequest()\n  reque"
  },
  {
    "path": "test/specs/circle-transform/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/clippath-circle/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/clippath-circle-transformed/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/clippath-ellipse/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/clippath-ellipse-transformed/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/clippath-g-element/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":0,\"roughness\":0,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/clippath-path/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":0,\"roughness\":0,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/clippath-path-transformed/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/clippath-polygon/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":0,\"roughness\":0,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/clippath-rect/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":0,\"roughness\":0,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/clippath-rect-rounded/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":0,\"roughness\":0,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/clippath-rect-rounded-transformed/config.json",
    "chars": 164,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/clippath-rect-rounded-transformed2/config.json",
    "chars": 164,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/clippath-rect-text/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/clippath-rect-transformed/config.json",
    "chars": 164,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/clipped-text-scaling/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/css-units/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":0,\"roughness\":0,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/dotted-stroke/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/ellipse-transform/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/fill-attribute/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/fill-attribute-ancestor-g/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/fill-attribute-ancestor-svg/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/fill-css-attribute-precedence/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/fill-css-attribute-precedence2/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/fill-css-class/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/fill-css-inline/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/fill-css-selector/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/fill-missing/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/foreign-object-mermaid/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/icons/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/markers/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/markers-fixed-orientation/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/markers-line/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/markers-on-line/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/markers-paths/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/markers-polygon/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/markers-polyline/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/nested-svg-translate/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/path-transform/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/path-transform2/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/pattern-circle/config.json",
    "chars": 188,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"hachure\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\""
  },
  {
    "path": "test/specs/pattern-ellipse/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/pattern-line/config.json",
    "chars": 188,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"hachure\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\""
  },
  {
    "path": "test/specs/pattern-path/config.json",
    "chars": 188,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"hachure\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\""
  },
  {
    "path": "test/specs/pattern-polygon/config.json",
    "chars": 188,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"hachure\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\""
  },
  {
    "path": "test/specs/pattern-polyline/config.json",
    "chars": 188,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"hachure\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\""
  },
  {
    "path": "test/specs/pattern-rect/config.json",
    "chars": 188,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"hachure\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\""
  },
  {
    "path": "test/specs/rect-not-rounded/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/rect-plain/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/rect-rounded-large-rx/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/rect-rounded-large-rx-ry/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/rect-rounded-large-ry/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/rect-rounded-rx/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/rect-rounded-rx-ry/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/rect-rounded-ry/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/rect-rounded-transform/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/rect-rounded-transform-mirror/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/rect-transform/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/rect-transform-from-g/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/stroke-attribute/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/stroke-attribute-ancestor-g/config.json",
    "chars": 166,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"hachure\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\""
  },
  {
    "path": "test/specs/stroke-attribute-ancestor-g-override/config.json",
    "chars": 166,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"hachure\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\""
  },
  {
    "path": "test/specs/stroke-attribute-ancestor-g2/config.json",
    "chars": 166,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"hachure\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\""
  },
  {
    "path": "test/specs/stroke-attribute-ancestor-svg/config.json",
    "chars": 166,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"hachure\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\""
  },
  {
    "path": "test/specs/stroke-missing-is-transparent/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/stroke-none-is-transparent/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/stroke-width-attribute/config.json",
    "chars": 166,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"hachure\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\""
  },
  {
    "path": "test/specs/stroke-width-scale-transform/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/svg-image-element/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/symbols/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/symbols-non-uniform-scale/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/symbols2/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/text-css/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/text-dominant-baseline-basic/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/text-glyph-positioning/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/text-rotated-glyphs/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/text-simple-tspans/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":0,\"roughness\":0,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/text-stroked-and-decorated/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/text-tspan-styling/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":0,\"roughness\":0,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/text-tspans-mixed/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/text-tspans-repositioned/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/text-whitespace/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":0,\"roughness\":0,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/text-width-custom-font/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/uml-node-style/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/use-element-styling/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/use-reference-group/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/viewbox-negative/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/viewbox-non-uniform/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/specs/viewbox-non-uniform-translated/config.json",
    "chars": 186,
    "preview": "{\"roughConfig\":{\"bowing\":1,\"roughness\":1,\"fillStyle\":\"solid\",\"fixedDecimalPlaceDigits\":3,\"seed\":4242},\"outputType\":0,\"pe"
  },
  {
    "path": "test/tests.js",
    "chars": 2533,
    "preview": "export const specTests = [\n  'circle-transform',\n  'clippath-circle',\n  'clippath-circle-transformed',\n  'clippath-ellip"
  },
  {
    "path": "test/umd-bundle/umd-bundle-test.html",
    "chars": 37550,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\""
  },
  {
    "path": "tsconfig.json",
    "chars": 415,
    "preview": "{\n  \"compilerOptions\": {\n    \"module\": \"es6\",\n    \"target\": \"es2015\",\n    \"allowJs\": true,\n    \"allowSyntheticDefaultImp"
  },
  {
    "path": "web-test-runner.config.mjs",
    "chars": 2343,
    "preview": "import { defaultReporter, summaryReporter } from '@web/test-runner'\nimport { rollupBundlePlugin } from '@web/dev-server-"
  }
]

About this extraction

This page contains the full source code of the fskpf/svg2roughjs GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 164 files (226.6 KB), approximately 67.6k tokens, and a symbol index with 143 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!