├── .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 | [![CircleCI](https://img.shields.io/circleci/project/github/RedSparr0w/node-csgo-parser.svg?style=flat-square)](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 | 10 | 11 | 27 | 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 | 6 | 7 | SVG Logo 8 | Designed for the SVG Logo Contest in 2006 by Harvey Rayner, and adopted by W3C in 2009. It is available under the Creative Commons license for those who have an SVG product or who are using SVG on their site. 9 | 10 | 11 | 15 | 16 | SVG Logo 17 | 14-08-2009 18 | 19 | W3C 20 | Harvey Rayner, designer 21 | 22 | See document description 23 | 24 | image/svg+xml 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/__tests__/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | xvg 6 | 7 | 8 | 9 |
10 |

xvg tests

11 |
12 | 13 |
14 | 15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 |

109 | Icon Set by Matthew Skiles 110 |

111 |
112 | 113 |
114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 141 | 142 | 143 | 146 | 149 | 152 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 174 | 175 |
176 | 177 |
178 |

Other Document Types

179 | 180 |

181 | 183 | SVG Logo 184 | 185 | 187 | Ghostscript Tiger 188 | 189 |

190 | 191 |

Chris Coyier's Kiwi's

192 |

193 | 194 | 195 | 196 |

197 | 198 |

SVG Logo

199 |

200 | 201 | 202 | 203 |

204 | 205 |

As an Image (doesn't work)

206 |

207 | 208 |

209 |
210 | 211 |
212 |

Additonal Test Cases

213 | 215 | 217 | 218 | 220 | 222 | 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 | 15 | 16 |
17 |

Settings

18 | 20 | 22 | 23 |
24 | 25 |
26 |
27 | Control Point 28 | 29 |
30 | 35 | 38 |
39 | 40 |
41 |
42 | 46 |
47 | 50 |
52 |
53 |
54 | 55 |
56 | 60 |
61 | 64 |
66 |
67 |
68 |
69 |
70 | 71 |
72 | Path Outline 73 | 74 |
75 |
76 | 81 | 84 |
85 |
86 | 90 |
91 | 94 |
96 |
97 |
98 |
99 |
100 | 101 |
102 | Handle 103 | 104 |
105 |
106 | 111 | 114 |
115 |
116 | 120 |
121 | 124 |
126 |
127 |
128 |
129 |
130 | 131 |
132 | Arc Guides 133 | 134 |
135 |
136 | 141 | 144 |
145 |
146 | 150 |
151 | 154 |
156 |
157 |
158 |
159 |
160 | 161 |
162 | 163 | 164 |
165 | 166 |
167 | 172 | 177 |
178 |
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 | 17 | info icon 18 | 19 | 20 | This is an early release. Any feedback and bug reports are much appreciated. Please submit them here. 21 |
22 | 23 |
24 |

xvg

25 |
26 | 27 | 29 | 30 |
31 |
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 |
40 | Install 41 |
42 |
43 |
44 | 45 |
46 | 47 | 52 | 53 | 54 | 56 | 57 | 58 | 60 | 61 | 62 | 63 | 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 --------------------------------------------------------------------------------