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

hero-image

npm github npm npm

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
================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "module": "es6", "target": "es2015", "allowJs": true, "allowSyntheticDefaultImports": true, "moduleResolution": "node", "lib": ["dom", "es2015"], "outDir": "out-tsc", "declaration": true, /* Linting */ "strict": true, "noImplicitAny": false, "noUnusedLocals": true, "noFallthroughCasesInSwitch": true }, "include": ["src/**/*.ts"] } ================================================ FILE: web-test-runner.config.mjs ================================================ import { defaultReporter, summaryReporter } from '@web/test-runner' import { rollupBundlePlugin } from '@web/dev-server-rollup' import { nodeResolve } from '@rollup/plugin-node-resolve' import commonjs from '@rollup/plugin-commonjs' const filteredLogs = ['Running in dev mode', 'lit-html is in dev mode', 'Lit is in dev mode'] // https://modern-web.dev/docs/test-runner/cli-and-configuration/ export default /** @type {import("@web/test-runner").TestRunnerConfig} */ ({ /** Test files to run */ files: ['test/runner/spec.test.js', 'test/runner/complex.test.js'], plugins: [ // We need to use rollup here unfortunately, because esbuild in the open-wc test-runner // only works on esbuild's single file transform API that doesn't bundle dependencies // automatically. This does not work for the external dependencies here (e.g. rough, // etc.), which cannot be resolved then. // // So use rollup with a configuration that creates one big-fat bundle without external deps. rollupBundlePlugin({ rollupConfig: { input: 'out-tsc/index.js', plugins: [nodeResolve(), commonjs()] } }) ], reporters: [ // use the default reporter only for reporting test progress // enable reportTestResults to see the actual diff of the results defaultReporter({ reportTestResults: false, reportTestProgress: true }), summaryReporter() ], /** Whether to analyze code coverage */ coverage: false, /** Run tests manually in the browser (e.g. useful for debugging) */ manual: false, /** Resolve bare module imports */ nodeResolve: { exportConditions: ['browser', 'development'] }, rootDir: './', /** Filter out lit dev mode logs */ filterBrowserLogs(log) { for (const arg of log.args) { if (typeof arg === 'string' && filteredLogs.some(l => arg.includes(l))) { return false } } return true }, /** Amount of browsers to run concurrently */ // concurrentBrowsers: 2, /** Amount of test files per browser to test concurrently */ concurrency: 1 /** Browsers to run tests on */ // browsers: [ // playwrightLauncher({ product: 'chromium' }), // playwrightLauncher({ product: 'firefox' }), // playwrightLauncher({ product: 'webkit' }), // ], // See documentation for all available options })