├── .babelrc
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .yarnclean
├── LICENSE
├── README.md
├── circle.yml
├── config
├── clear-console.js
├── webpack.config.js
└── webpack.site.config.js
├── icons
├── favicon.ico
├── icon.sketch
├── icon128.png
├── icon16.png
├── icon19.png
├── icon38.png
└── icon48.png
├── manifest.json
├── package.json
├── src
├── __tests__
│ ├── anchors.spec.js
│ ├── arc-guides.spec.js
│ ├── expand.spec.js
│ ├── fixtures.js
│ ├── kiwi.svg
│ ├── skeletonize.spec.js
│ ├── svglogo.svg
│ ├── test.html
│ ├── utils.spec.js
│ └── visual-test.js
├── anchors.js
├── arc-guides.js
├── dom.js
├── expand.js
├── index.js
├── options
│ ├── options.html
│ └── options.js
├── settings.js
├── skeletonize.js
├── utils.js
├── x-ray.js
├── xvg.js
└── zoom.js
├── website
├── index.css
├── index.html
└── index.js
├── xvg.gif
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"],
3 | "plugins": ["ramda"],
4 | }
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "plugins": [],
4 | "env": {
5 | "browser": true,
6 | "node": true
7 | },
8 | "parserOptions": {
9 | "ecmaVersion": 6
10 | },
11 | "rules": {
12 | /**
13 | * Strict mode
14 | */
15 | "strict": [2, "safe"],
16 |
17 | /**
18 | * ES6
19 | */
20 | "no-var": 2,
21 | "prefer-const": 2,
22 |
23 | /**
24 | * Variables
25 | */
26 | "no-shadow": 2,
27 | "no-shadow-restricted-names": 2,
28 | "no-unused-vars": [2, {
29 | "vars": "local",
30 | "args": "after-used"
31 | }],
32 | "no-use-before-define": 0,
33 |
34 | /**
35 | * Possible errors
36 | */
37 | "comma-dangle": [2, {
38 | "arrays": "always-multiline",
39 | "objects": "always-multiline",
40 | "imports": "always-multiline",
41 | "exports": "always-multiline",
42 | "functions": "always-multiline",
43 | }],
44 | "no-cond-assign": [2, "always"],
45 | "no-console": 1,
46 | "no-debugger": 1,
47 | "no-alert": 1,
48 | "no-constant-condition": 1,
49 | "no-dupe-keys": 2,
50 | "no-duplicate-case": 2,
51 | "no-empty": 2,
52 | "no-ex-assign": 2,
53 | "no-extra-boolean-cast": 0,
54 | "no-extra-semi": 2,
55 | "no-func-assign": 2,
56 | "no-inner-declarations": 2,
57 | "no-invalid-regexp": 2,
58 | "no-irregular-whitespace": 2,
59 | "no-obj-calls": 2,
60 | "no-sparse-arrays": 2,
61 | "no-unreachable": 2,
62 | "use-isnan": 2,
63 | "block-scoped-var": 0,
64 |
65 | /**
66 | * Best practices
67 | */
68 | "consistent-return": 2,
69 | "curly": [2, "multi-line"],
70 | "default-case": 2,
71 | "dot-notation": [2, {
72 | "allowKeywords": true
73 | }],
74 | "eqeqeq": 2,
75 | "guard-for-in": 0,
76 | "no-caller": 2,
77 | "no-else-return": 2,
78 | "no-eq-null": 2,
79 | "no-eval": 2,
80 | "no-extend-native": 2,
81 | "no-extra-bind": 2,
82 | "no-fallthrough": 2,
83 | "no-floating-decimal": 2,
84 | "no-implied-eval": 2,
85 | "no-lone-blocks": 2,
86 | "no-loop-func": 2,
87 | "no-multi-str": 2,
88 | "no-native-reassign": 2,
89 | "no-new": 2,
90 | "no-new-func": 2,
91 | "no-new-wrappers": 2,
92 | "no-octal": 2,
93 | "no-octal-escape": 2,
94 | "no-param-reassign": 2,
95 | "no-proto": 2,
96 | "no-redeclare": 2,
97 | "no-return-assign": 2,
98 | "no-script-url": 2,
99 | "no-self-compare": 2,
100 | "no-sequences": 2,
101 | "no-throw-literal": 2,
102 | "no-with": 2,
103 | "radix": 2,
104 | "vars-on-top": 2,
105 | "wrap-iife": [2, "any"],
106 | "yoda": 2,
107 |
108 | /**
109 | * Style
110 | */
111 | "indent": [2, 2],
112 | "brace-style": [2,
113 | "1tbs", {
114 | "allowSingleLine": true
115 | }],
116 | "quotes": [
117 | 2, "single", "avoid-escape"
118 | ],
119 | "camelcase": [2, {
120 | "properties": "never"
121 | }],
122 | "comma-spacing": [2, {
123 | "before": false,
124 | "after": true
125 | }],
126 | "comma-style": [2, "last"],
127 | "eol-last": 2,
128 | "func-names": 1,
129 | "key-spacing": [2, {
130 | "beforeColon": false,
131 | "afterColon": true
132 | }],
133 | "new-cap": [2, {
134 | "newIsCap": true,
135 | "capIsNew": false
136 | }],
137 | "no-multiple-empty-lines": [2, {
138 | "max": 2
139 | }],
140 | "no-nested-ternary": 2,
141 | "no-new-object": 2,
142 | "no-spaced-func": 2,
143 | "no-trailing-spaces": 2,
144 | "no-extra-parens": [2, "functions"],
145 | "no-underscore-dangle": 0,
146 | "one-var": [2, "never"],
147 | "padded-blocks": [2, "never"],
148 | "semi": [2, "always"],
149 | "semi-spacing": [2, {
150 | "before": false,
151 | "after": true
152 | }],
153 | "keyword-spacing": 2,
154 | "space-before-blocks": 2,
155 | "space-before-function-paren": [2, "never"],
156 | "space-infix-ops": 2,
157 | "spaced-comment": 2,
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | # Logs
4 | logs
5 | *.log
6 | npm-debug.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 |
13 | # Directory for instrumented libs generated by jscoverage/JSCover
14 | lib-cov
15 |
16 | # Coverage directory used by tools like istanbul
17 | coverage
18 |
19 | # nyc test coverage
20 | .nyc_output
21 |
22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
23 | .grunt
24 |
25 | # node-waf configuration
26 | .lock-wscript
27 |
28 | # Compiled binary addons (http://nodejs.org/api/addons.html)
29 | build/Release
30 |
31 | # Dependency directories
32 | node_modules
33 | jspm_packages
34 |
35 | # Optional npm cache directory
36 | .npm
37 |
38 | # Optional REPL history
39 | .node_repl_history
40 |
41 | build
42 | screenshots
43 | stats.json
44 |
--------------------------------------------------------------------------------
/.yarnclean:
--------------------------------------------------------------------------------
1 | !.svgo.yml
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Varun Vachhar
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### No longer maintained
2 |
3 | [](https://github.com/winkerVSbecks/xvg)
4 |
5 |
6 |
7 | A Chrome extension for debugging SVG paths by converting them to outlines and displaying anchors, control points, handles and arc ellipses.
8 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | environment:
3 | YARN_VERSION: 0.18.1
4 | PATH: "${PATH}:${HOME}/.yarn/bin:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin"
5 |
6 | dependencies:
7 | pre:
8 | - |
9 | if [[ ! -e ~/.yarn/bin/yarn || $(yarn --version) != "${YARN_VERSION}" ]]; then
10 | curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version $YARN_VERSION
11 | fi
12 | cache_directories:
13 | - ~/.yarn
14 | - ~/.cache/yarn
15 | override:
16 | - yarn install
17 |
18 | test:
19 | override:
20 | - yarn test
21 |
--------------------------------------------------------------------------------
/config/clear-console.js:
--------------------------------------------------------------------------------
1 | (function clearConsole() {
2 | process.stdout.write(process.platform === 'win32' ? '\x1Bc' : '\x1B[2J\x1B[3J\x1B[H');
3 | process.exit(0);
4 | })();
5 |
--------------------------------------------------------------------------------
/config/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const {
3 | addPlugins, createConfig, entryPoint, env, setOutput, sourceMaps, webpack,
4 | customConfig,
5 | } = require('@webpack-blocks/webpack2');
6 |
7 | const CopyWebpackPlugin = require('copy-webpack-plugin');
8 | const devServer = require('@webpack-blocks/dev-server2');
9 |
10 | const babel = require('@webpack-blocks/babel6');
11 | const HtmlWebpackPlugin = require('html-webpack-plugin');
12 | const DashboardPlugin = require('webpack-dashboard/plugin');
13 |
14 | const basePlugins = [
15 | new webpack.DefinePlugin({
16 | 'process.env': JSON.stringify(process.env || 'development'),
17 | }),
18 | ];
19 |
20 | const devPlugins = [
21 | new DashboardPlugin(),
22 | new HtmlWebpackPlugin({
23 | inject: true,
24 | template: 'src/__tests__/test.html',
25 | }),
26 | new CopyWebpackPlugin([
27 | { from: 'src/__tests__/svglogo.svg' },
28 | { from: 'src/__tests__/kiwi.svg' },
29 | ]),
30 | ];
31 |
32 | const productionPlugins = [
33 | new webpack.LoaderOptionsPlugin({
34 | minimize: true,
35 | debug: false,
36 | }),
37 | new HtmlWebpackPlugin({
38 | inject: true,
39 | template: 'src/options/options.html',
40 | excludeChunks: ['xvg-injector', 'xvg'],
41 | }),
42 | new webpack.optimize.UglifyJsPlugin({
43 | compress: {
44 | warnings: false,
45 | },
46 | output: {
47 | comments: false,
48 | },
49 | screwIe8: true,
50 | sourceMap: false,
51 | }),
52 | new CopyWebpackPlugin([
53 | { from: 'manifest.json' },
54 | { from: 'icons/icon19.png' },
55 | { from: 'icons/icon38.png' },
56 | { from: 'icons/icon16.png' },
57 | { from: 'icons/icon48.png' },
58 | { from: 'icons/icon128.png' },
59 | { from: 'icons/favicon.ico' },
60 | ]),
61 | ];
62 |
63 | module.exports = createConfig([
64 | babel(),
65 | addPlugins(basePlugins),
66 | env('development', [
67 | devServer(),
68 | sourceMaps(),
69 | entryPoint('./src/__tests__/visual-test.js'),
70 | setOutput('./build/bundle.js'),
71 | addPlugins(devPlugins),
72 | ]),
73 | env('production', [
74 | entryPoint({
75 | 'xvg-injector': './src/index.js',
76 | xvg: './src/xvg.js',
77 | options: './src/options/options.js',
78 | }),
79 | setOutput({
80 | filename: '[name].js',
81 | path: path.join(__dirname, '../build'),
82 | }),
83 | addPlugins(productionPlugins),
84 | ]),
85 | customConfig({
86 | performance: { hints: false },
87 | }),
88 | ]);
89 |
--------------------------------------------------------------------------------
/config/webpack.site.config.js:
--------------------------------------------------------------------------------
1 | const {
2 | addPlugins, createConfig, entryPoint, env, setOutput, sourceMaps, webpack,
3 | customConfig,
4 | } = require('@webpack-blocks/webpack2');
5 | const extractText = require('@webpack-blocks/extract-text2');
6 | const HtmlWebpackPlugin = require('html-webpack-plugin');
7 | const babel = require('@webpack-blocks/babel6');
8 | const CopyWebpackPlugin = require('copy-webpack-plugin');
9 |
10 | const path = require('path');
11 |
12 | const basePlugins = [
13 | new webpack.optimize.CommonsChunkPlugin({
14 | name: 'vendor',
15 | }),
16 | new HtmlWebpackPlugin({
17 | inject: true,
18 | template: 'website/index.html',
19 | }),
20 | new webpack.DefinePlugin({
21 | 'process.env': JSON.stringify(process.env || 'development'),
22 | }),
23 | new CopyWebpackPlugin([
24 | { from: 'icons/favicon.ico' },
25 | ]),
26 | ];
27 |
28 | const productionPlugins = [
29 | new webpack.LoaderOptionsPlugin({
30 | minimize: true,
31 | debug: false,
32 | }),
33 | new webpack.optimize.UglifyJsPlugin({
34 | compress: {
35 | warnings: false,
36 | },
37 | output: {
38 | comments: false,
39 | },
40 | screwIe8: true,
41 | sourceMap: false,
42 | }),
43 | ];
44 |
45 | module.exports = createConfig([
46 | babel(),
47 | addPlugins(basePlugins),
48 | entryPoint({
49 | main: './website/index.js',
50 | vendor: ['ramda'],
51 | }),
52 | setOutput({
53 | filename: '[name].js',
54 | path: path.join(__dirname, '../build'),
55 | }),
56 | extractText(),
57 | env('development', [
58 | sourceMaps(),
59 | ]),
60 | env('production', [
61 | addPlugins(productionPlugins),
62 | customConfig({
63 | performance: { hints: false },
64 | }),
65 | ]),
66 | ]);
67 |
--------------------------------------------------------------------------------
/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/winkerVSbecks/xvg/6232e1833201e0cfe00179e74a0ac1a3874d2dfb/icons/favicon.ico
--------------------------------------------------------------------------------
/icons/icon.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/winkerVSbecks/xvg/6232e1833201e0cfe00179e74a0ac1a3874d2dfb/icons/icon.sketch
--------------------------------------------------------------------------------
/icons/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/winkerVSbecks/xvg/6232e1833201e0cfe00179e74a0ac1a3874d2dfb/icons/icon128.png
--------------------------------------------------------------------------------
/icons/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/winkerVSbecks/xvg/6232e1833201e0cfe00179e74a0ac1a3874d2dfb/icons/icon16.png
--------------------------------------------------------------------------------
/icons/icon19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/winkerVSbecks/xvg/6232e1833201e0cfe00179e74a0ac1a3874d2dfb/icons/icon19.png
--------------------------------------------------------------------------------
/icons/icon38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/winkerVSbecks/xvg/6232e1833201e0cfe00179e74a0ac1a3874d2dfb/icons/icon38.png
--------------------------------------------------------------------------------
/icons/icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/winkerVSbecks/xvg/6232e1833201e0cfe00179e74a0ac1a3874d2dfb/icons/icon48.png
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "xvg",
4 | "short_name": "xvg",
5 | "description": "An extension for debugging SVG paths by converting them to outlines and displaying anchors, control points, handles and arc ellipses",
6 | "version": "1.2.0",
7 | "options_page": "index.html",
8 | "permissions": [
9 | "activeTab",
10 | "storage"
11 | ],
12 | "background": {
13 | "persistent": false,
14 | "scripts": ["xvg-injector.js"]
15 | },
16 | "browser_action": {
17 | "default_title": "xvg",
18 | "default_icon": {
19 | "19": "icon19.png",
20 | "38": "icon38.png"
21 | }
22 | },
23 | "icons": {
24 | "16": "icon16.png",
25 | "48": "icon48.png",
26 | "128": "icon128.png"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xvg",
3 | "version": "1.2.0",
4 | "description": "🔬 debug SVG paths in the browser",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "NODE_ENV=test tape -r babel-register './src/**/*.spec.js' | faucet",
8 | "test:watch": "NODE_ENV=test tape-watch -r babel-register './src/**/*.spec.js' | faucet",
9 | "clean": "rm -rf build",
10 | "postclean": "node config/clear-console.js",
11 | "start": "npm run clean && NODE_ENV=development webpack-dashboard -- webpack-dev-server --config config/webpack.config.js",
12 | "build": "npm run clean && NODE_ENV=production webpack -p --config config/webpack.config.js",
13 | "build:profile": "npm run clean && NODE_ENV=production webpack -p --config config/webpack.config.js --profile --json > stats.json && webpack-bundle-analyzer stats.json",
14 | "start:site": "npm run clean && NODE_ENV=development webpack-dashboard -- webpack-dev-server --config config/webpack.site.config.js --progress --colors",
15 | "build:site": "npm run clean && NODE_ENV=production webpack -p --config config/webpack.site.config.js"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/winkerVSbecks/svg-x-ray.git"
20 | },
21 | "keywords": [
22 | "SVG",
23 | "debug"
24 | ],
25 | "author": "Varun Vachhar",
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/winkerVSbecks/svg-x-ray/issues"
29 | },
30 | "homepage": "https://xvg.now.sh",
31 | "dependencies": {
32 | "@webpack-blocks/babel6": "^0.3.0",
33 | "@webpack-blocks/dev-server2": "^0.3.0",
34 | "@webpack-blocks/extract-text2": "^0.3.0",
35 | "@webpack-blocks/webpack2": "^0.3.0",
36 | "babel-eslint": "^7.1.1",
37 | "babel-plugin-ramda": "^1.1.6",
38 | "babel-preset-es2015": "^6.18.0",
39 | "copy-webpack-plugin": "^4.0.1",
40 | "eslint": "^3.12.2",
41 | "faucet": "^0.0.1",
42 | "html-webpack-plugin": "^2.24.1",
43 | "ramda": "^0.22.1",
44 | "raw-loader": "^0.5.1",
45 | "svgpath": "^2.2.0",
46 | "tachyons": "^4.6.1",
47 | "tape": "^4.6.3",
48 | "tape-watch": "^2.2.4",
49 | "webpack": "2.2.0-rc.3",
50 | "webpack-bundle-analyzer": "^2.2.0",
51 | "webpack-dashboard": "^0.2.1",
52 | "webpack-dev-server": "2.2.0-rc.0"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/__tests__/anchors.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { convertArcToEndPoint, getSegmentAnchors, getPolygonAnchors,
3 | } from '../anchors';
4 | import { complexPath, hardEdgePath, pathWithArc, pointsList } from './fixtures';
5 |
6 | test('anchors.convertArcToEndPoint', (t) => {
7 | t.deepEqual(
8 | convertArcToEndPoint(['A', 45, 45, 0, 0, 1, 125, 275]),
9 | ['A', 125, 275],
10 | 'should strip an arc command down to just the end point',
11 | );
12 | t.deepEqual(
13 | convertArcToEndPoint(['M', 80, 230]),
14 | ['M', 80, 230],
15 | 'should leave other commands untouched',
16 | );
17 | t.end();
18 | });
19 |
20 | test('anchors.getSegmentAnchors', (t) => {
21 | const msg = 'should reduce a path to a list of anchor point tuples';
22 |
23 | t.deepEqual(
24 | getSegmentAnchors(complexPath),
25 | [
26 | [ 10, 18.374 ], [ -1.533, -1.533 ], [ 0.25, -1.533 ], [ 0.236, 0 ],
27 | [ 0.428, -0.191 ], [ 0.428, -0.428 ], [ 0.428, -2.138 ],
28 | [ 1.709, -2.138 ], [ 1.709, 2.138 ], [ 0, 0.236 ], [ 0.192, 0.428 ],
29 | [ 0.428, 0.428 ], [ 0.251, 0.428 ], [ 10, 18.374 ],
30 | ],
31 | msg,
32 | );
33 | t.deepEqual(
34 | getSegmentAnchors(hardEdgePath),
35 | [
36 | [ 10, 10 ], [ 90, 10 ], [ 90, 90 ], [ 10, 90 ], [ 10, 90 ], [ 10, 20 ],
37 | [ 10, 30 ], [ 10, 10 ],
38 | ],
39 | msg,
40 | );
41 | t.deepEqual(
42 | getSegmentAnchors(pathWithArc),
43 | [
44 | [ 80, 230 ], [ 125, 275 ], [ 30, 275 ], [ 125, 275 ], [ 125, 30 ],
45 | [ 125, 230 ],
46 | ],
47 | msg,
48 | );
49 | t.end();
50 | });
51 |
52 | test('anchors.getPolygonAnchors', (t) => {
53 | t.deepEqual(
54 | getPolygonAnchors(pointsList),
55 | [
56 | [13.12399959564209, 6.6610002517700195],
57 | [10.607999801635742, 9.944000244140625],
58 | [13.12399959564209, 13.338000297546387],
59 | [14.420999526977539, 13.338000297546387],
60 | [11.902000427246094, 9.944000244140625],
61 | [14.418000221252441, 6.6610002517700195],
62 | ],
63 | );
64 | t.end();
65 | });
66 |
--------------------------------------------------------------------------------
/src/__tests__/arc-guides.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { makeArcGuides } from '../arc-guides';
3 | import { mdnArcs, mdnArcs2 } from './fixtures';
4 |
5 | test('ellipses.makeArcGuides', (t) => {
6 | t.deepEqual(
7 | makeArcGuides({ segments: mdnArcs }),
8 | [
9 | 'M 80 80 A 45 45 0 0 0 125 125 M 80 80 A 45 45 0 1 1 125 125',
10 | 'M 230 80 A 45 45 0 1 0 275 125 M 230 80 A 45 45 0 0 1 275 125',
11 | 'M 80 230 A 45 45 0 0 1 125 275 M 80 230 A 45 45 0 1 0 125 275',
12 | 'M 230 230 A 45 45 0 1 1 275 275 M 230 230 A 45 45 0 0 0 275 275',
13 | ],
14 | );
15 |
16 | t.deepEqual(
17 | makeArcGuides({ segments: mdnArcs2 }),
18 | [
19 | 'M 110 215 A 30 50 0 0 1 162.55 162.45 M 110 215 A 30 50 0 1 0 162.55 162.45',
20 | 'M 172.55 152.45 A 30 50 -45 0 1 215.1 109.9 M 172.55 152.45 A 30 50 -45 1 0 215.1 109.9',
21 | ],
22 | );
23 |
24 | t.end();
25 | });
26 |
--------------------------------------------------------------------------------
/src/__tests__/expand.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { expand } from '../expand';
3 | import { hardEdgePath, pathWithArc } from './fixtures';
4 |
5 | test('expand H segment', (t) => {
6 | t.deepEqual(
7 | expand(['H', 10], 3, hardEdgePath),
8 | ['H', 10, 90],
9 | );
10 |
11 | t.deepEqual(
12 | expand(['H', 90], 1, hardEdgePath),
13 | ['H', 90, 10],
14 | );
15 |
16 | t.deepEqual(
17 | expand(['H', 30], 2, pathWithArc),
18 | ['H', 30, 275],
19 | );
20 |
21 | t.end();
22 | });
23 |
24 | test('expand V segment', (t) => {
25 | t.deepEqual(
26 | expand(['V', 90], 2, hardEdgePath),
27 | ['V', 90, 90],
28 | );
29 |
30 | t.deepEqual(
31 | expand(['V', 30], 6, hardEdgePath),
32 | ['V', 10, 30],
33 | );
34 |
35 | t.deepEqual(
36 | expand(['V', 30], 4, pathWithArc),
37 | ['V', 125, 30],
38 | );
39 |
40 | t.end();
41 | });
42 |
--------------------------------------------------------------------------------
/src/__tests__/fixtures.js:
--------------------------------------------------------------------------------
1 | export const complexPath = [
2 | ['M', 10, 18.374],
3 | ['L', -1.533, -1.533],
4 | ['H', 0.25],
5 | ['C', 0.236, 0, 0.428, -0.191, 0.428, -0.428],
6 | ['V', -2.138],
7 | ['H', 1.709],
8 | ['V', 2.138],
9 | ['C', 0, 0.236, 0.192, 0.428, 0.428, 0.428],
10 | ['H', 0.251],
11 | ['L', 10, 18.374],
12 | ['Z'],
13 | ];
14 |
15 | export const qAndTPath = [
16 | ['M', 10, 80],
17 | ['Q', 52.5, 10, 95, 80],
18 | ['T', 180, 80],
19 | ];
20 |
21 | export const hardEdgePath = [
22 | ['M', 10, 10],
23 | ['H', 90 ],
24 | ['V', 90],
25 | ['H', 10],
26 | ['H', 10],
27 | ['V', 20],
28 | ['V', 30],
29 | ['L', 10, 10],
30 | ];
31 |
32 | export const pathWithArc = [
33 | ['M', 80, 230],
34 | ['A', 45, 45, 0, 0, 1, 125, 275],
35 | ['H', 30],
36 | ['A', 45, 45, 0, 0, 1, 125, 275],
37 | ['V', 30],
38 | ['L', 125, 230],
39 | ['Z'],
40 | ];
41 |
42 | export const mdnArcs = [
43 | ['M', 80, 80],
44 | ['A', 45, 45, 0, 0, 0, 125, 125],
45 | ['L', 125, 80],
46 | ['Z'],
47 | ['M', 230, 80],
48 | ['A', 45, 45, 0, 1, 0, 275, 125],
49 | ['L', 275, 80 ],
50 | ['Z'],
51 | ['M', 80, 230],
52 | ['A', 45, 45, 0, 0, 1, 125, 275],
53 | ['L', 125, 230 ],
54 | ['Z'],
55 | ['M', 230, 230],
56 | ['A', 45, 45, 0, 1, 1, 275, 275],
57 | ['L', 275, 230 ],
58 | ['Z'],
59 | ];
60 |
61 | export const mdnArcs2 = [
62 | ['M', 10, 315],
63 | ['L', 110, 215],
64 | ['A', 30, 50, 0, 0, 1, 162.55, 162.45],
65 | ['L', 172.55, 152.45],
66 | ['A', 30, 50, -45, 0, 1, 215.1, 109.9],
67 | ['L', 315, 10],
68 | ];
69 |
70 | export const mdnCubic = [
71 | ['M', 10, 10],
72 | ['C', 20, 20, 40, 20, 50, 10],
73 | ['M', 70, 10],
74 | ['C', 70, 20, 120, 20, 120, 10],
75 | ['M', 130, 10],
76 | ['C', 120, 20, 180, 20, 170, 10],
77 | ['M', 10, 60],
78 | ['C', 20, 80, 40, 80, 50, 60],
79 | ['M', 70, 60],
80 | ['C', 70, 80, 110, 80, 110, 60],
81 | ['M', 130, 60],
82 | ['C', 120, 80, 180, 80, 170, 60],
83 | ['M', 10, 110],
84 | ['C', 20, 140, 40, 140, 50, 110],
85 | ['M', 70, 110],
86 | ['C', 70, 140, 110, 140, 110, 110],
87 | ['M', 130, 110],
88 | ['C', 120, 140, 180, 140, 170, 110],
89 | ];
90 |
91 | export const mdnReflect = [
92 | ['M', 10, 80],
93 | ['C', 40, 10, 65, 10, 95, 80],
94 | ['S', 150, 150, 180, 80],
95 | ];
96 |
97 | export const mdnQuad = [
98 | ['M', 10, 80],
99 | ['Q', 95, 10, 180, 80],
100 | ];
101 |
102 | export const mdnChain = [
103 | ['M', 10, 80],
104 | ['Q', 52.5, 10, 95, 80],
105 | ['T', 180, 80],
106 | ];
107 |
108 | export const pointsList = {
109 | 0: { x: 13.12399959564209, y: 6.6610002517700195 },
110 | 1: { x: 10.607999801635742, y: 9.944000244140625 },
111 | 2: { x: 13.12399959564209, y: 13.338000297546387 },
112 | 3: { x: 14.420999526977539, y: 13.338000297546387 },
113 | 4: { x: 11.902000427246094, y: 9.944000244140625 },
114 | 5: { x: 14.418000221252441, y: 6.6610002517700195 },
115 | };
116 |
--------------------------------------------------------------------------------
/src/__tests__/kiwi.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
28 |
--------------------------------------------------------------------------------
/src/__tests__/skeletonize.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { getHandleDescriptions } from '../skeletonize';
3 | import { mdnCubic, mdnReflect, mdnQuad, mdnChain } from './fixtures';
4 |
5 | test('skeletonize.getHandleDescriptions', (t) => {
6 | t.deepEqual(
7 | getHandleDescriptions({ segments: mdnCubic }),
8 | ' M 10 10 L 20 20 M 40 20 L 50 10 M 70 10 L 70 20 M 120 20 L 120 10 M 130 10 L 120 20 M 180 20 L 170 10 M 10 60 L 20 80 M 40 80 L 50 60 M 70 60 L 70 80 M 110 80 L 110 60 M 130 60 L 120 80 M 180 80 L 170 60 M 10 110 L 20 140 M 40 140 L 50 110 M 70 110 L 70 140 M 110 140 L 110 110 M 130 110 L 120 140 M 180 140 L 170 110',
9 | );
10 |
11 | t.deepEqual(
12 | getHandleDescriptions({ segments: mdnReflect }),
13 | ' M 10 80 L 40 10 M 65 10 L 95 80 M 150 150 L 180 80',
14 | );
15 |
16 | t.deepEqual(
17 | getHandleDescriptions({ segments: mdnQuad }),
18 | ' M 10 80 L 95 10 L 180 80',
19 | );
20 |
21 | t.deepEqual(
22 | getHandleDescriptions({ segments: mdnChain }),
23 | ' M 10 80 L 52.5 10 L 95 80 ',
24 | );
25 | t.end();
26 | });
27 |
--------------------------------------------------------------------------------
/src/__tests__/svglogo.svg:
--------------------------------------------------------------------------------
1 |
51 |
--------------------------------------------------------------------------------
/src/__tests__/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | xvg
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
16 |
17 |
20 |
23 |
26 |
29 |
32 |
35 |
38 |
41 |
44 |
47 |
50 |
53 |
56 |
59 |
62 |
65 |
68 |
71 |
74 |
77 |
80 |
83 |
86 |
89 |
92 |
95 |
98 |
101 |
104 |
107 |
108 |
109 | Icon Set by Matthew Skiles
110 |
111 |
112 |
113 |
114 |
125 |
128 |
131 |
134 |
142 |
156 |
159 |
160 |
163 |
164 |
175 |
176 |
177 |
210 |
211 |
212 | Additonal Test Cases
213 |
223 |
224 |
225 |
226 |
227 |
--------------------------------------------------------------------------------
/src/__tests__/utils.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { isArc, getCommandOrigin, removeZ } from '../utils';
3 | import { complexPath, qAndTPath } from './fixtures';
4 |
5 | test('utils.isArc', (t) => {
6 | t.ok(isArc(['A', 30, 50, 0, 0, 1, 162.55, 162.45]));
7 | t.notOk(isArc(['V', 10, 30]));
8 | t.notOk(isArc([]));
9 | t.end();
10 | });
11 |
12 | test('utils.getCommandOrigin', (t) => {
13 | t.deepEqual(getCommandOrigin(1, qAndTPath), [10, 80]);
14 | t.deepEqual(getCommandOrigin(2, qAndTPath), [95, 80]);
15 | t.end();
16 | });
17 |
18 | test('utils.removeZ', (t) => {
19 | t.deepEqual(
20 | removeZ(complexPath),
21 | complexPath.slice(0, complexPath.length - 1),
22 | );
23 | t.end();
24 | });
25 |
--------------------------------------------------------------------------------
/src/__tests__/visual-test.js:
--------------------------------------------------------------------------------
1 | import '../xvg';
2 | import '!style-loader!css-loader!tachyons';
3 |
--------------------------------------------------------------------------------
/src/anchors.js:
--------------------------------------------------------------------------------
1 | import R from 'ramda';
2 | import { expand } from './expand';
3 | import { isArc, removeZ } from './utils';
4 | import { make } from './dom';
5 | import settings from './settings';
6 |
7 | export const convertArcToEndPoint = R.ifElse(isArc,
8 | R.juxt([R.nth(0), R.nth(6), R.nth(7)]),
9 | R.identity,
10 | );
11 |
12 | export const getSegmentAnchors = R.compose(
13 | R.splitEvery(2),
14 | R.unnest,
15 | R.map(R.tail),
16 | R.addIndex(R.map)(expand),
17 | R.map(convertArcToEndPoint),
18 | removeZ,
19 | );
20 |
21 | const makeCircle = make('circle', ([x, y]) => [
22 | ['cx', x],
23 | ['cy', y],
24 | ['r', settings.xvg.xvgCpSize],
25 | ['fill', settings.xvg.xvgCpFill],
26 | ['stroke', settings.xvg.xvgCpStroke],
27 | ['stroke-width', settings.xvg.xvgCpSize],
28 | ['style', `stroke: ${settings.xvg.xvgCpStroke}; fill: ${settings.xvg.xvgCpFill}; stroke-width: ${settings.xvg.xvgCpSize}`],
29 | ]);
30 |
31 | const makePathAnchors = R.compose(
32 | R.map(makeCircle),
33 | getSegmentAnchors,
34 | R.prop('segments'),
35 | );
36 |
37 | export const getPolygonAnchors = R.map(R.props(['x', 'y']));
38 |
39 | const makePolygonAnchors = R.compose(
40 | R.map(makeCircle),
41 | getPolygonAnchors,
42 | R.prop('points'),
43 | );
44 |
45 | function draw(makeAnchors) {
46 | return R.converge(
47 | (circles, path) => {
48 | circles.forEach(c => {
49 | path.parentElement.appendChild(c);
50 | });
51 | },
52 | [makeAnchors, R.prop('node')],
53 | );
54 | }
55 |
56 | export const drawPolygonAnchors = draw(makePolygonAnchors);
57 | export const drawPathAnchors = draw(makePathAnchors);
58 |
--------------------------------------------------------------------------------
/src/arc-guides.js:
--------------------------------------------------------------------------------
1 | import R from 'ramda';
2 | import { getCommandOrigin, isArc } from './utils';
3 | import { make } from './dom';
4 | import { expand } from './expand';
5 | import settings from './settings';
6 |
7 | const makeArcGuide = make('path', (d) => [
8 | ['d', d],
9 | ['fill', 'transparent'],
10 | ['stroke', settings.xvg.xvgArcGuideColour],
11 | ['stroke-width', '0.25%'],
12 | ['style', `stroke: ${settings.xvg.xvgArcGuideColour}; fill: transparent; stroke-width: ${settings.xvg.xvgArcGuideSize}`],
13 | ]);
14 |
15 | // 1 ↔️ 0
16 | const flip = R.ifElse(R.equals(0), R.always(1), R.always(0));
17 |
18 | const reflect = R.compose(
19 | R.adjust(flip, 4),
20 | R.adjust(flip, 5),
21 | );
22 |
23 | const makeArcs = R.compose(
24 | R.join(' '),
25 | R.useWith((moveToOrigin, arc) => {
26 | return [
27 | ...moveToOrigin,
28 | ...arc,
29 | ...moveToOrigin,
30 | ...reflect(arc),
31 | ];
32 | }, [R.prepend('M'), R.identity]),
33 | );
34 |
35 | const makeArcGuideDef = R.converge(
36 | makeArcs,
37 | [
38 | R.converge(
39 | getCommandOrigin,
40 | [R.nthArg(1), R.nthArg(2)],
41 | ),
42 | R.identity,
43 | ],
44 | );
45 |
46 | export const makeArcGuides = R.compose(
47 | R.reject(R.isNil),
48 | R.addIndex(R.map)(
49 | R.ifElse(isArc,
50 | makeArcGuideDef,
51 | R.always(undefined),
52 | ),
53 | ),
54 | R.addIndex(R.map)(expand),
55 | R.prop('segments'),
56 | );
57 |
58 | export const drawArcGuides = R.converge(
59 | (ellipses, path) => {
60 | ellipses.forEach(e => {
61 | path.parentElement.insertBefore(e, path);
62 | });
63 | },
64 | [R.pipe(makeArcGuides, R.map(makeArcGuide)), R.prop('node')],
65 | );
66 |
--------------------------------------------------------------------------------
/src/dom.js:
--------------------------------------------------------------------------------
1 | import R from 'ramda';
2 |
3 | // mock document for testing
4 | if (process.env.NODE_ENV === 'test') { global.document = {}; }
5 |
6 | export const createElement = R.invoker(1, 'createElement')(R.__, document);
7 | export const createSvgElement = R.invoker(2, 'createElementNS')(
8 | 'http://www.w3.org/2000/svg',
9 | R.__,
10 | document,
11 | );
12 | export const setAttribute = R.invoker(2, 'setAttribute');
13 |
14 | export const makeWith = R.compose(
15 | R.tap,
16 | R.juxt,
17 | R.map(R.apply(setAttribute)),
18 | );
19 |
20 | export function make(type, generateAttrs) {
21 | return (...args) => {
22 | return makeWith(
23 | generateAttrs(...args),
24 | )(createSvgElement(type));
25 | };
26 | }
27 |
28 | export function insertStyleSheet(id, rules) {
29 | const style = createElement('style');
30 | style.id = id;
31 | document.head.appendChild(style);
32 |
33 | rules.forEach((rule, idx) => {
34 | style.sheet.insertRule(rule, idx);
35 | });
36 | }
37 |
38 |
39 | export function insertSvgStyleSheet() {
40 | // const ss = document.createElementNS('http://www.w3.org/2000/svg', 'style');
41 | // const svg = document.querySelector('svg');
42 | //
43 | // svg.appendChild(ss);
44 | // const sheets = document.styleSheets;
45 | // let sheet;
46 | //
47 | // for (let i = 0, length = sheets.length; i < length; i++) {
48 | // sheet = sheets.item(i);
49 | // if (sheet.ownerNode == ss) break;
50 | // }
51 | // sheet.insertRule('.xvg-inspect:hover { transform: scale3d(2, 2, 2); }', 0);
52 | }
53 |
--------------------------------------------------------------------------------
/src/expand.js:
--------------------------------------------------------------------------------
1 | import R from 'ramda';
2 | import { hasX, hasY } from './utils';
3 |
4 | function getPrevX(idx, segments) {
5 | return R.compose(
6 | R.ifElse(
7 | R.compose(R.equals('H'), R.head),
8 | R.last,
9 | R.nth(-2),
10 | ),
11 | R.findLast(R.compose(hasX, R.head)),
12 | R.slice(0, idx),
13 | )(segments);
14 | }
15 |
16 | function getPrevY(idx, segments) {
17 | return R.compose(
18 | R.last,
19 | R.findLast(R.compose(hasY, R.head)),
20 | R.slice(0, idx),
21 | )(segments);
22 | }
23 |
24 | /**
25 | * Expand abbreviated commands
26 | * adds the x coordinate for V
27 | * adds the y coordinate for H
28 | */
29 | export function expand(segment, idx, segments) {
30 | const type = segment[0];
31 |
32 | if (idx === 0) {
33 | return segment;
34 | } else if (type === 'H') {
35 | return [segment[0], segment[1], getPrevY(idx, segments)];
36 | } else if (type === 'V') {
37 | return [segment[0], getPrevX(idx, segments), segment[1]];
38 | }
39 |
40 | return segment;
41 | }
42 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | chrome.browserAction.onClicked.addListener(() => {
2 | chrome.tabs.executeScript({
3 | file: 'xvg.js',
4 | });
5 | });
6 |
--------------------------------------------------------------------------------
/src/options/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | xvg - settings
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
16 |
17 | Settings
18 |
23 |
24 |
25 |
179 |
180 |
181 |
182 |
--------------------------------------------------------------------------------
/src/options/options.js:
--------------------------------------------------------------------------------
1 | import '../../website/index.css';
2 | import R from 'ramda';
3 |
4 |
5 | const settingsFormDefinition = {
6 | xvgCpSize: 'cp-size',
7 | xvgCpStroke: 'cp-stroke',
8 | xvgCpFill: 'cp-fill',
9 | xvgOutlineSize: 'outline-size',
10 | xvgOutlineColour: 'outline-colour',
11 | xvgHandleSize: 'handle-size',
12 | xvgHandleColour: 'handle-colour',
13 | xvgArcGuideSize: 'arc-guide-size',
14 | xvgArcGuideColour: 'arc-guide-colour',
15 | xvgZoom: 'zoom',
16 | };
17 |
18 | const getElementById = R.invoker(1, 'getElementById')(R.__, document);
19 | const getForm = R.map(getElementById);
20 | const getSettings = R.evolve({
21 | xvgCpSize: R.prop('value'),
22 | xvgCpStroke: R.prop('value'),
23 | xvgCpFill: R.prop('value'),
24 | xvgOutlineSize: R.prop('value'),
25 | xvgOutlineColour: R.prop('value'),
26 | xvgHandleSize: R.prop('value'),
27 | xvgHandleColour: R.prop('value'),
28 | xvgArcGuideSize: R.prop('value'),
29 | xvgArcGuideColour: R.prop('value'),
30 | xvgZoom: R.prop('checked'),
31 | });
32 |
33 | const updateStatus = message => () => {
34 | const status = document.getElementById('status');
35 | status.classList.toggle('active');
36 | status.textContent = message;
37 |
38 | setTimeout(() => {
39 | status.classList.toggle('active');
40 | status.textContent = '';
41 | }, 1500);
42 | };
43 |
44 |
45 | /**
46 | * Saves options to chrome.storage
47 | */
48 | const saveOptions = formDef => () => {
49 | const settings = R.compose(
50 | getSettings,
51 | getForm,
52 | )(formDef);
53 |
54 | chrome.storage.sync.set(settings, updateStatus('Settings Saved'));
55 | };
56 |
57 |
58 | /**
59 | * Update colour indicator for inputs
60 | */
61 | function setColour(target) {
62 | const indicator = getElementById(`${target.id}-indicator`);
63 | if (indicator) indicator.style.backgroundColor = target.value;
64 | }
65 |
66 | const onInputChange = R.compose(
67 | setColour,
68 | R.prop('target'),
69 | );
70 |
71 | const attachOnInput = R.compose(
72 | R.forEach(node => { node.oninput = onInputChange; }),
73 | R.map(getElementById),
74 | );
75 |
76 |
77 | /**
78 | * Restores settings and updates DOM accordingly
79 | */
80 | export const defaultSettings = {
81 | xvgCpSize: '0.75%',
82 | xvgCpStroke: '#ff41b4',
83 | xvgCpFill: '#fff',
84 | xvgOutlineSize: '1%',
85 | xvgOutlineColour: '#5e2ca5',
86 | xvgHandleSize: '0.5%',
87 | xvgHandleColour: '#ff41b4',
88 | xvgArcGuideSize: '0.25%',
89 | xvgArcGuideColour: '#96CCFF',
90 | xvgZoom: true,
91 | };
92 |
93 | const setValue = value => node => {
94 | node.value = value;
95 | setColour(node);
96 | };
97 | const setChecked = value => node => { node.checked = value; };
98 | const restoreSettings = R.evolve({
99 | xvgCpSize: setValue,
100 | xvgCpStroke: setValue,
101 | xvgCpFill: setValue,
102 | xvgOutlineSize: setValue,
103 | xvgOutlineColour: setValue,
104 | xvgHandleSize: setValue,
105 | xvgHandleColour: setValue,
106 | xvgArcGuideSize: setValue,
107 | xvgArcGuideColour: setValue,
108 | xvgZoom: setChecked,
109 | });
110 |
111 | const onLoadOptions = formDef => settings => {
112 | const form = getForm(formDef);
113 | const restoreSettingsInDom = R.evolve(restoreSettings(settings));
114 | restoreSettingsInDom(form);
115 | };
116 |
117 | function restoreOptions(formDef) {
118 | chrome.storage.sync.get(defaultSettings, onLoadOptions(formDef));
119 | }
120 |
121 |
122 | /**
123 | * Reset to defaults
124 | */
125 | const resetToDefaults = formDef => settings => () => {
126 | const resetForm = onLoadOptions(formDef);
127 | chrome.storage.sync.set(settings, () => {
128 | updateStatus('Reset Settings to Default')();
129 | resetForm(settings);
130 | });
131 | };
132 |
133 |
134 | /**
135 | * Add event listeners
136 | */
137 | document
138 | .addEventListener('DOMContentLoaded', () => {
139 | restoreOptions(settingsFormDefinition);
140 | attachOnInput(['cp-stroke', 'cp-fill', 'outline-colour', 'handle-colour',
141 | 'arc-guide-colour']);
142 | });
143 |
144 | document
145 | .getElementById('save')
146 | .addEventListener('click', saveOptions(settingsFormDefinition));
147 |
148 | document
149 | .getElementById('reset-to-default')
150 | .addEventListener(
151 | 'click',
152 | resetToDefaults(settingsFormDefinition)(defaultSettings),
153 | );
154 |
--------------------------------------------------------------------------------
/src/settings.js:
--------------------------------------------------------------------------------
1 | export default {
2 | xvg: {
3 | xvgCpSize: '0.75%',
4 | xvgCpStroke: '#ff41b4',
5 | xvgCpFill: '#fff',
6 | xvgOutlineSize: '1%',
7 | xvgOutlineColour: '#5e2ca5',
8 | xvgHandleSize: '0.5%',
9 | xvgHandleColour: '#ff41b4',
10 | xvgArcGuideSize: '0.25%',
11 | xvgArcGuideColour: '#96CCFF',
12 | xvgZoom: true,
13 | },
14 |
15 | set: function setXvgSetting(newSettings) {
16 | this.xvg = newSettings;
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/src/skeletonize.js:
--------------------------------------------------------------------------------
1 | import R from 'ramda';
2 | import { expand } from './expand';
3 | import { hasControlPoints, getCommandOrigin } from './utils';
4 | import { setAttribute } from './dom';
5 | import settings from './settings';
6 |
7 | /**
8 | * Draw outlines for a shape
9 | */
10 | export const drawOutline = R.converge(R.call,
11 | [
12 | () => R.juxt([
13 | setAttribute('stroke', settings.xvg.xvgOutlineColour),
14 | setAttribute('stroke-width', settings.xvg.xvgOutlineSize),
15 | setAttribute('fill', 'transparent'),
16 | setAttribute(
17 | 'style',
18 | `stroke: ${settings.xvg.xvgOutlineColour}; fill: transparent; stroke-width: ${settings.xvg.xvgOutlineSize};`,
19 | ),
20 | ]),
21 | R.prop('node'),
22 | ],
23 | );
24 |
25 | const handleDescriptions = {
26 | 'C': (s, o) => (
27 | `M ${o[0]} ${o[1]} L ${s[1]} ${s[2]} M ${s[3]} ${s[4]} L ${s[5]} ${s[6]}`
28 | ),
29 | 'S': (s) => (`M ${s[1]} ${s[2]} L ${s[3]} ${s[4]}`),
30 | 'Q': (s, o) => (`M ${o[0]} ${o[1]} L ${s[1]} ${s[2]} L ${s[3]} ${s[4]}`),
31 | };
32 |
33 | function handleDescriptionFn(segment, origin) {
34 | const segmentType = R.head(segment);
35 | const descriptionFn = handleDescriptions[segmentType];
36 | return descriptionFn ? descriptionFn(segment, origin) : '';
37 | }
38 |
39 | const getHandleDescription = R.ifElse(hasControlPoints,
40 | R.converge(handleDescriptionFn,
41 | [
42 | R.nthArg(0),
43 | R.compose(
44 | R.apply(getCommandOrigin),
45 | R.juxt([R.nthArg(1), R.nthArg(2)]),
46 | ),
47 | ],
48 | ),
49 | R.always(''),
50 | );
51 |
52 | export const getHandleDescriptions = R.compose(
53 | R.join(' '),
54 | R.addIndex(R.map)(getHandleDescription),
55 | R.addIndex(R.map)(expand),
56 | R.prop('segments'),
57 | );
58 |
59 | /**
60 | * Draw handles for control points
61 | */
62 | export const drawHandles = R.converge(
63 | (handle, path) => {
64 | if (handle) {
65 | const handleNode = path.cloneNode();
66 | handleNode.setAttribute('stroke', settings.xvg.xvgHandleColour);
67 | handleNode.setAttribute('stroke-width', settings.xvg.xvgHandleSize);
68 | handleNode.setAttribute('fill', 'transparent');
69 | handleNode.setAttribute('d', handle);
70 | handleNode.setAttribute(
71 | 'style',
72 | `stroke: ${settings.xvg.xvgHandleColour}; fill: transparent; stroke-width: ${settings.xvg.xvgHandleSize}`,
73 | );
74 |
75 | path.parentElement.appendChild(handleNode);
76 | }
77 | },
78 | [getHandleDescriptions, R.prop('node')],
79 | );
80 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import R from 'ramda';
2 |
3 | export const isArc = R.compose(R.equals('A'), R.head);
4 |
5 | export const hasControlPoints = R.test(/(q|t|c|s)/ig);
6 | export const hasX = R.test(/(m|l|h|q|t|c|s|a)/ig);
7 | export const hasY = R.test(/(m|l|v|q|t|c|s|a)/ig);
8 |
9 | export const getCommandOrigin = R.compose(
10 | R.takeLast(2),
11 | (idx, segments) => segments[idx - 1],
12 | );
13 |
14 | export const removeZ = R.filter(R.compose(R.not, R.equals('Z'), R.head));
15 |
16 | export function getAttribute(type) {
17 | return path => path.getAttribute(type);
18 | }
19 |
20 | export function getNodes(type) {
21 | return (svg) => svg.querySelectorAll(type);
22 | }
23 |
--------------------------------------------------------------------------------
/src/x-ray.js:
--------------------------------------------------------------------------------
1 | import R from 'ramda';
2 | import svgpath from 'svgpath';
3 | import { drawOutline, drawHandles } from './skeletonize';
4 | import { drawPathAnchors, drawPolygonAnchors } from './anchors';
5 | import { drawArcGuides } from './arc-guides';
6 | import { getAttribute, getNodes } from './utils';
7 |
8 | const toAbsolute = R.invoker(0, 'abs');
9 | const unshort = R.invoker(0, 'unshort');
10 |
11 | const getPathSegments = R.compose(
12 | R.prop('segments'),
13 | unshort,
14 | toAbsolute,
15 | svgpath,
16 | getAttribute('d'),
17 | );
18 |
19 | const parseSegments = R.compose(
20 | R.converge(R.assoc('segments'),
21 | [getPathSegments, R.objOf('node')],
22 | ),
23 | );
24 |
25 | const drawPathDebugArtifacts = R.juxt([
26 | drawOutline,
27 | drawHandles,
28 | drawArcGuides,
29 | drawPathAnchors,
30 | ]);
31 |
32 | const xRayPaths = R.compose(
33 | R.forEach(drawPathDebugArtifacts),
34 | R.map(parseSegments),
35 | getNodes('path'),
36 | );
37 |
38 | const parsePoints = R.compose(
39 | R.converge(R.assoc('points'),
40 | [R.prop('points'), R.objOf('node')],
41 | ),
42 | );
43 |
44 | const xRayPolygons = R.compose(
45 | R.forEach(R.juxt([
46 | drawOutline,
47 | drawPolygonAnchors,
48 | ])),
49 | R.map(parsePoints),
50 | getNodes('polygon, polyline'),
51 | );
52 |
53 | export const xRay = R.juxt([
54 | xRayPaths,
55 | xRayPolygons,
56 | ]);
57 |
--------------------------------------------------------------------------------
/src/xvg.js:
--------------------------------------------------------------------------------
1 | import R from 'ramda';
2 | import { xRay } from './x-ray';
3 | import { attachZoom } from './zoom';
4 | import settings from './settings';
5 |
6 | const removeNulls = R.filter(R.complement(R.isNil));
7 | const cssQuery = R.invoker(1, 'querySelectorAll');
8 |
9 | const getSubDocument = R.ifElse(R.has('contentDocument'),
10 | R.prop('contentDocument'),
11 | R.invoker(0, 'getSVGDocument'),
12 | );
13 |
14 | const findSvgElements = R.compose(
15 | R.compose(
16 | removeNulls,
17 | R.flatten,
18 | ),
19 | R.map(cssQuery('svg')),
20 | R.converge(R.concat, [
21 | R.of,
22 | R.compose(
23 | removeNulls,
24 | R.map(getSubDocument),
25 | cssQuery('object, embed, iframe'),
26 | ),
27 | ]),
28 | );
29 |
30 | if (process.env.NODE_ENV === 'development') {
31 | window.onload = function onload() {
32 | R.compose(
33 | R.forEach(xRay),
34 | R.tap(attachZoom),
35 | findSvgElements,
36 | )(document);
37 | };
38 | } else {
39 | chrome.storage.sync.get(settings.xvg, userSettings => {
40 | settings.set(userSettings);
41 | R.compose(
42 | R.forEach(xRay),
43 | R.tap(attachZoom),
44 | findSvgElements,
45 | )(document);
46 | });
47 | }
48 |
--------------------------------------------------------------------------------
/src/zoom.js:
--------------------------------------------------------------------------------
1 | import R from 'ramda';
2 | import { insertStyleSheet } from './dom';
3 |
4 | function addZoomStyles() {
5 | insertStyleSheet('xvg-stylesheet', [
6 | `.xvg-inspect {
7 | transform: scale3d(1, 1, 1);
8 | transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
9 | cursor: zoom-in;
10 | z-index: 1;
11 | }`,
12 | '.xvg-inspect:hover { transform: scale3d(2, 2, 2); }',
13 | ]);
14 | }
15 |
16 | function addZoomClass(node) {
17 | node.classList.add('xvg-inspect');
18 | }
19 |
20 | export const attachZoom = R.compose(
21 | R.when(
22 | R.compose(
23 | R.complement(R.isNil),
24 | R.always(document.head),
25 | ),
26 | R.compose(
27 | R.tap(R.forEach(addZoomClass)),
28 | R.tap(addZoomStyles),
29 | ),
30 | ),
31 | );
32 |
--------------------------------------------------------------------------------
/website/index.css:
--------------------------------------------------------------------------------
1 | @import '../node_modules/tachyons/css/tachyons.css';
2 |
3 | .slide-in {
4 | transition: transform 200ms ease-in-out;
5 | transform: translate3d(0, -100%, 0);
6 | }
7 |
8 | .slide-in.active { transform: translate3d(0, 0, 0); }
9 |
--------------------------------------------------------------------------------
/website/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | xvg
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
20 |
This is an early release. Any feedback and bug reports are much appreciated. Please submit them here.
21 |
22 |
23 |
32 |
33 |
34 |
35 |
36 | A Chrome extension for debugging SVG paths by converting them to outlines and displaying anchors, control points, handles and arc ellipses.
37 |
38 |
39 |
42 |
43 |
44 |
45 |
46 |
53 |
57 |
61 |
64 |
65 |
66 |
87 |
88 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/website/index.js:
--------------------------------------------------------------------------------
1 | import R from 'ramda';
2 | import './index.css';
3 | import { xRay } from '../src/x-ray';
4 |
5 | const cssQuery = R.invoker(1, 'querySelectorAll');
6 |
7 | R.compose(
8 | R.addIndex(R.forEach)((node, idx) => {
9 | setTimeout(() => xRay(node), 600 + idx * 300);
10 | }),
11 | cssQuery('.js-icon'),
12 | )(document);
13 |
--------------------------------------------------------------------------------
/xvg.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/winkerVSbecks/xvg/6232e1833201e0cfe00179e74a0ac1a3874d2dfb/xvg.gif
--------------------------------------------------------------------------------