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> |
| | |
| <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> |
| | |
| <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> |
| | |
| <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"> |
| | |
| <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"> |
| | |
| <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
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
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.