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
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
```
```javascript
```
## 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`
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.
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 |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| 
(created with [yEd Live](https://www.yworks.com/yed-live)) | 
|
| | |
| 
(created with [yEd Live](https://www.yworks.com/yed-live)) | 
|
| | |
| 
(created with [yEd Live](https://www.yworks.com/yed-live)) | 
|
| | |
|
|
|
| | |
|
|
|
| | |
|
|
|
## 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`
`--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} svg2roughjsArgs The SVG that should be converted
* @returns {string} HTML content of the converter page
*/
export function svg2roughjsPage(inputSvg, svg2roughjsArgs) {
return `
svg2roughjs
${inputSvg}
`
}
/**
* @param {Record} 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
================================================
Svg2Rough.js - Convert Your SVG to a Hand Drawn Image
================================================
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 | 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 = '\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 = '\r\n' + inputSvg
downloadFile(inputSvg, 'image/svg+xml', 'test.svg')
}
const spec = document.querySelector('#output svg') as SVGSVGElement
let sketchedSvg = serializer.serializeToString(spec)
sketchedSvg = '\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
================================================
///
================================================
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 = {}
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 {
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 {
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 elements, or on the owner itself for other
* elements.
*
* 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(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
Applicant
Enterprise
Application
Confirmation
of Receipt
Invitation
Employment
Contract
Signed
Employment
Contract
Question
Answer
Write Job
Application
Receive
Confirmation
of Receipt
Receive
Invitation to
Interview
Take Part in
Interview
Receive
Employment
Contract
Sign Contract
and Send It
Back
Receive
Application
Confirm
Receipt
Invite to
Interview
Carry Out
Interview
Send
Employment
Contract
Receive
Signed
Employment
Contract