├── .eslintignore ├── src ├── vector2 │ ├── index.ts │ └── length.ts ├── utils │ ├── index.ts │ ├── refineFloat.ts │ └── translatePoint.ts ├── types.ts ├── index.ts ├── smooth.ts └── compute.ts ├── babel.config.js ├── mochapack.opts ├── .prettierrc ├── .gitignore ├── renovate.json ├── demo ├── tsconfig.json ├── index.css ├── index.html └── index.ts ├── tests ├── tsconfig.json ├── vector2 │ └── length.spec.ts └── compute.spec.ts ├── webpack.test.config.js ├── webpack.config.js ├── .vscode ├── cspell.json └── extensions.json ├── tsconfig.json ├── webpack.common.config.js ├── scripts └── deploy.sh ├── index.html ├── .eslintrc.js ├── Makefile ├── README.md ├── webpack.demo.config.js ├── LICENSE ├── CHANGELOG.md ├── .circleci └── config.yml └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.js 3 | -------------------------------------------------------------------------------- /src/vector2/index.ts: -------------------------------------------------------------------------------- 1 | export * from './length'; 2 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/env'], 3 | }; 4 | -------------------------------------------------------------------------------- /mochapack.opts: -------------------------------------------------------------------------------- 1 | --webpack-config webpack.test.config.js 2 | tests/**/*.spec.ts 3 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './translatePoint'; 2 | export * from './refineFloat'; 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode/* 2 | !/.vscode/cspell.json 3 | !/.vscode/extensions.json 4 | 5 | node_modules 6 | /dist 7 | /dist-demo 8 | /gh-pages 9 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:js-lib", 4 | ":automergeMinor", 5 | ":pinSkipCi", 6 | ":rebaseStalePrs" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Vector2 { 2 | x: number; 3 | y: number; 4 | } 5 | 6 | export interface Point extends Vector2 { 7 | w: number; 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/refineFloat.ts: -------------------------------------------------------------------------------- 1 | /** https://stackoverflow.com/a/43550268 */ 2 | export function refineFloat(v: number): number { 3 | return v + 8 - 8; 4 | } 5 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false 5 | }, 6 | "include": ["**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["mocha"] 5 | }, 6 | "include": ["./**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /webpack.test.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (env, argv) => { 2 | const config = require('./webpack.common.config'); 3 | config.entry('index').add('./src/index.ts'); 4 | return config.toConfig(); 5 | }; 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as util from './utils'; 2 | import * as vector2 from './vector2'; 3 | export * from './compute'; 4 | export * from './types'; 5 | export { util, vector2 }; 6 | export * from './smooth'; 7 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (env, argv) => { 2 | const config = require('./webpack.common.config'); 3 | config.entry('index').add('./src/index.ts'); 4 | config.output.library('svgVariableWidthLine').libraryTarget('umd'); 5 | return config.toConfig(); 6 | }; 7 | -------------------------------------------------------------------------------- /demo/index.css: -------------------------------------------------------------------------------- 1 | svg#canvas { 2 | width: 800px; 3 | height: 600px; 4 | max-width: 100%; 5 | max-height: 100%; 6 | margin: auto; 7 | border: 1px solid black; 8 | } 9 | svg#canvas path { 10 | fill: black; 11 | fill-rule: nonzero; 12 | stroke: red 1px; 13 | } 14 | 15 | body { 16 | text-align: center; 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/cspell.json: -------------------------------------------------------------------------------- 1 | // cSpell Settings 2 | { 3 | // Version of the setting file. Always 0.1 4 | "version": "0.1", 5 | "// language": [ 6 | "\n // language - current active spelling language" 7 | ], 8 | // words - list of words to be always considered correct 9 | "words": [ 10 | "esnext" 11 | ] 12 | } -------------------------------------------------------------------------------- /src/vector2/length.ts: -------------------------------------------------------------------------------- 1 | import { Vector2 } from '../types'; 2 | 3 | /** Length between points */ 4 | export function length(...points: Vector2[]): number { 5 | let ret = 0; 6 | for (let i = 1; i < points.length; i += 1) { 7 | const pa = points[i - 1]; 8 | const pb = points[i]; 9 | ret += Math.sqrt(Math.pow(pa.x - pb.x, 2) + Math.pow(pa.y - pb.y, 2)); 10 | } 11 | return ret; 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/translatePoint.ts: -------------------------------------------------------------------------------- 1 | export function translatePoint( 2 | el: SVGSVGElement, 3 | point: { x: number; y: number } 4 | ): DOMPoint { 5 | const matrix = el.getScreenCTM(); 6 | 7 | if (!matrix) { 8 | throw new Error('Should has screen CTM.'); 9 | } 10 | const p = el.createSVGPoint(); 11 | p.x = point.x; 12 | p.y = point.y; 13 | return p.matrixTransform(matrix.inverse()); 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "esnext", 5 | "sourceMap": true, 6 | "strict": true, 7 | "moduleResolution": "node", 8 | "lib": ["dom", "esnext"], 9 | "declaration": true, 10 | "baseUrl": "./", 11 | "paths": { 12 | "@": ["src"], 13 | "@/*": ["src/*"] 14 | }, 15 | "outDir": "dist" 16 | }, 17 | "include": ["src/**/*.ts"], 18 | "exclude": ["node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /webpack.common.config.js: -------------------------------------------------------------------------------- 1 | const Config = require('webpack-chain'); 2 | const path = require('path'); 3 | const config = new Config(); 4 | 5 | config.mode('development'); 6 | config.resolve.alias.set('@', path.resolve(__dirname, 'src')); 7 | config.resolve.extensions.add('.ts').add('.js'); 8 | config.module 9 | .rule('typescript') 10 | .test(/.ts$/) 11 | .use('babel') 12 | .loader('babel-loader') 13 | .end() 14 | .use('typescript') 15 | .loader('ts-loader') 16 | .end(); 17 | 18 | module.exports = config; 19 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd gh-pages 6 | git pull 7 | 8 | set +e 9 | git ls-files | xargs rm 10 | set -e 11 | 12 | 13 | cp -r ../dist-demo/* ./ 14 | echo > .nojekyll 15 | git add --all 16 | 17 | if [[ -z "$(git status -s)" ]]; then 18 | echo No changes 19 | exit 0 20 | fi 21 | 22 | if [[ -z "$CI" ]]; then 23 | git commit -m 'chore: deploy' -m '[skip ci]' 24 | else 25 | git -c "user.name=CI User" -c "user.email=<>" commit -m 'chore: deploy' -m '[skip ci]' 26 | fi 27 | git push 28 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SVG Variable Width Line Demo 5 | 6 | 7 | 8 | 14 |

Draw in above area with pressure supported device.

15 |

Refresh page to clear canvas.

16 |

Pressure:

17 | 18 | 19 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "dbaeumer.vscode-eslint", 8 | "streetsidesoftware.code-spell-checker" 9 | ], 10 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 11 | "unwantedRecommendations": [] 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | }, 6 | extends: [ 7 | 'eslint:recommended', 8 | 'plugin:@typescript-eslint/eslint-recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'prettier', 11 | 'prettier/@typescript-eslint', 12 | ], 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 16 | }, 17 | plugins: ['@typescript-eslint'], 18 | parser: '@typescript-eslint/parser', 19 | }; 20 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SVG Variable Width Line Demo 5 | 6 | 7 | 8 | 14 |

Draw in above area with pressure supported device.

15 |

Refresh page to clear canvas.

16 |

Pressure:

17 |

Pointer type:

18 | 19 | 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: build test deploy 3 | 4 | build: dist dist-demo 5 | 6 | gh-pages/.git: 7 | git fetch -fn origin gh-pages:gh-pages 8 | git branch --set-upstream-to=origin/gh-pages gh-pages 9 | rm -rf gh-pages/* 10 | git worktree add -f gh-pages gh-pages 11 | 12 | test: 13 | npm run test 14 | 15 | dist: src/* src/*/* webpack.common.config.js webpack.config.js 16 | rm -rf dist 17 | npm run build 18 | 19 | dist-demo: src/* src/*/* demo/* webpack.common.config.js webpack.demo.config.js 20 | rm -rf dist-demo 21 | npm run build:demo 22 | 23 | deploy: gh-pages/.git dist-demo 24 | ./scripts/deploy.sh 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SVG Variable width line 2 | 3 | [![npm package](https://img.shields.io/npm/v/svg-variable-width-line)](https://www.npmjs.com/package/svg-variable-width-line) 4 | [![Build Status](https://img.shields.io/circleci/project/github/NateScarlet/svg-variable-width-line.svg)](https://circleci.com/gh/NateScarlet/svg-variable-width-line) 5 | 6 | Create svg `path` with each point can have variable width. 7 | 8 | Can create line with `PointerEvent.pressure`. 9 | 10 | [Demo](https://natescarlet.github.io/svg-variable-width-line/) 11 | 12 | ```javascript 13 | import * as svgVariableWidthLine from 'svg-variable-width-line'; 14 | 15 | svgVariableWidthLine.compute({ 16 | points: [{ x: 0, y: 0, w: 1 }, { x: 1, y: 0, w: 0 }], 17 | }); 18 | // { d: '' } 19 | ``` 20 | -------------------------------------------------------------------------------- /tests/vector2/length.spec.ts: -------------------------------------------------------------------------------- 1 | import * as lib from '@'; 2 | import { expect } from 'chai'; 3 | 4 | describe('vector2-length', function() { 5 | it('simple-x', function() { 6 | const result = lib.vector2.length({ x: 0, y: 0 }, { x: 1, y: 0 }); 7 | expect(result).to.equals(1); 8 | }); 9 | it('simple-y', function() { 10 | const result = lib.vector2.length({ x: 0, y: 0 }, { x: 0, y: 1 }); 11 | expect(result).to.equals(1); 12 | }); 13 | it('simple-xy', function() { 14 | const result = lib.vector2.length({ x: 0, y: 0 }, { x: 1, y: 1 }); 15 | expect(result).to.equals(1.4142135623730951); 16 | }); 17 | it('multiple', function() { 18 | const result = lib.vector2.length( 19 | { x: 0, y: 0 }, 20 | { x: 0, y: 1 }, 21 | { x: 0, y: 0 } 22 | ); 23 | expect(result).to.equals(2); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /webpack.demo.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | 4 | const path = require('path'); 5 | module.exports = (env, argv) => { 6 | const config = require('./webpack.common.config'); 7 | config.entry('index').add('./demo/index.ts'); 8 | config.output 9 | .path(path.resolve(__dirname, 'dist-demo')) 10 | .filename('[name].[contenthash].js'); 11 | config.devServer.contentBase('demo'); 12 | config.mode('development'); 13 | config.devtool('inline-source-map'); 14 | config 15 | .plugin('copy') 16 | .use(CopyWebpackPlugin, [ 17 | [{ from: 'demo', to: '.' }], 18 | { ignore: ['*.ts', 'tsconfig.json'] }, 19 | ]); 20 | config 21 | .plugin('html') 22 | .use(HtmlWebpackPlugin, [{ template: 'demo/index.html' }]); 23 | return config.toConfig(); 24 | }; 25 | -------------------------------------------------------------------------------- /src/smooth.ts: -------------------------------------------------------------------------------- 1 | import { Point } from './types'; 2 | 3 | // Refer: https://github.com/Jam3/chaikin-smooth/blob/master/index.js 4 | 5 | export function smoothOnce(...points: Point[]): Point[] { 6 | const ret: Point[] = []; 7 | if (points.length === 0) { 8 | return []; 9 | } 10 | ret.push({ ...points[0] }); 11 | for (let i = 0; i < points.length - 1; i++) { 12 | const p0 = points[i]; 13 | const p1 = points[i + 1]; 14 | 15 | ret.push( 16 | { 17 | x: 0.75 * p0.x + 0.25 * p1.x, 18 | y: 0.75 * p0.y + 0.25 * p1.y, 19 | w: p0.w * 0.75 + p1.w * 0.25, 20 | }, 21 | { 22 | x: 0.25 * p0.x + 0.75 * p1.x, 23 | y: 0.25 * p0.y + 0.75 * p1.y, 24 | w: p0.w * 0.25 + p1.w * 0.75, 25 | } 26 | ); 27 | } 28 | if (points.length > 2) { 29 | ret.push({ ...points[points.length - 1] }); 30 | } 31 | return ret; 32 | } 33 | 34 | export function smooth(points: Point[], times = 1): Point[] { 35 | let ret = points; 36 | for (let count = 0; count < times; count += 1) { 37 | ret = smoothOnce(...ret); 38 | } 39 | return ret; 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 NateScarlet 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 | -------------------------------------------------------------------------------- /tests/compute.spec.ts: -------------------------------------------------------------------------------- 1 | import * as lib from '@'; 2 | import { expect } from 'chai'; 3 | describe('compute', function() { 4 | describe('side-points', function() { 5 | it('x', function() { 6 | const result = lib.computeSidePoints( 7 | { x: 0, y: 0, w: 2 }, 8 | { x: -1, y: 0, w: 0 } 9 | ); 10 | expect(result).to.deep.equals({ 11 | left: { x: 0, y: 1 }, 12 | right: { x: 0, y: -1 }, 13 | }); 14 | }); 15 | it('y', function() { 16 | const result = lib.computeSidePoints( 17 | { x: 0, y: 0, w: 2 }, 18 | { x: 0, y: -1, w: 0 } 19 | ); 20 | expect(result).to.deep.equals({ 21 | left: { x: -1, y: 0 }, 22 | right: { x: 1, y: 0 }, 23 | }); 24 | }); 25 | it('xy', function() { 26 | const result = lib.computeSidePoints( 27 | { x: 0, y: 0, w: 2 }, 28 | { x: -1, y: -1, w: 0 } 29 | ); 30 | const angle = Math.atan(1); 31 | const dx = lib.util.refineFloat(Math.cos(angle)); 32 | const dy = lib.util.refineFloat(Math.sin(angle)); 33 | expect(result).to.deep.equals({ 34 | left: { x: -dx, y: dy }, 35 | right: { x: dx, y: -dy }, 36 | }); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [0.1.2](https://github.com/NateScarlet/svg-variable-width-line/compare/v0.1.1...v0.1.2) (2019-08-19) 6 | 7 | 8 | ### Build System 9 | 10 | * disable declaration generation for demo ([1672d5c](https://github.com/NateScarlet/svg-variable-width-line/commit/1672d5c)) 11 | * fix library build ([d148318](https://github.com/NateScarlet/svg-variable-width-line/commit/d148318)) 12 | * improve webpack config ([ed427a8](https://github.com/NateScarlet/svg-variable-width-line/commit/ed427a8)) 13 | 14 | 15 | 16 | ### [0.1.1](https://github.com/NateScarlet/svg-variable-width-line/compare/v0.1.0...v0.1.1) (2019-08-17) 17 | 18 | 19 | ### Build System 20 | 21 | * correct script permission ([fae359b](https://github.com/NateScarlet/svg-variable-width-line/commit/fae359b)) 22 | 23 | 24 | 25 | ## 0.1.0 (2019-08-17) 26 | 27 | 28 | ### Build System 29 | 30 | * setup ([c9a30d0](https://github.com/NateScarlet/svg-variable-width-line/commit/c9a30d0)) 31 | 32 | 33 | ### Features 34 | 35 | * add smooth to demo ([a5e084e](https://github.com/NateScarlet/svg-variable-width-line/commit/a5e084e)) 36 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | executors: 3 | node: 4 | docker: 5 | - image: circleci/node:lts 6 | jobs: 7 | build: 8 | executor: node 9 | steps: 10 | - checkout 11 | - restore_cache: 12 | key: dependency-cache-{{ checksum "package.json" }} 13 | - run: 14 | name: 'Install dependencies' 15 | command: npm install 16 | - save_cache: 17 | key: dependency-cache-{{ checksum "package.json" }} 18 | paths: 19 | - ./node_modules 20 | - run: 21 | name: Test 22 | command: make test 23 | - run: 24 | name: Build 25 | command: make 26 | - persist_to_workspace: 27 | root: dist-demo 28 | paths: 29 | - '*' 30 | deploy-gh-pages: 31 | executor: node 32 | steps: 33 | - checkout 34 | - attach_workspace: 35 | at: dist-demo 36 | - add_ssh_keys: 37 | fingerprints: 38 | - '2f:0f:ce:3e:4a:c0:89:77:08:17:5f:a2:9f:df:77:ca' 39 | - run: 40 | name: 'Deploy' 41 | command: make deploy 42 | workflows: 43 | version: 2 44 | main: 45 | jobs: 46 | - build: 47 | filters: 48 | branches: 49 | ignore: gh-pages 50 | - deploy-gh-pages: 51 | requires: 52 | - build 53 | filters: 54 | branches: 55 | only: master 56 | -------------------------------------------------------------------------------- /src/compute.ts: -------------------------------------------------------------------------------- 1 | import { Point, Vector2 } from './types'; 2 | import { refineFloat } from './utils/refineFloat'; 3 | import * as vector2 from './vector2'; 4 | 5 | export function computeSidePoints( 6 | current: Point, 7 | prev?: Point 8 | ): { left: Vector2; right: Vector2 } { 9 | const r = current.w / 2; 10 | if (!prev) { 11 | return { 12 | left: { 13 | x: current.x, 14 | y: current.y + r, 15 | }, 16 | right: { 17 | x: current.x, 18 | y: current.y - r, 19 | }, 20 | }; 21 | } 22 | const angle = Math.atan((current.y - prev.y) / (current.x - prev.x)); 23 | const dx = refineFloat(Math.sin(angle) * r); 24 | const dy = refineFloat(Math.cos(angle) * r); 25 | return { 26 | left: { 27 | x: current.x - dx, 28 | y: current.y + dy, 29 | }, 30 | right: { 31 | x: current.x + dx, 32 | y: current.y - dy, 33 | }, 34 | }; 35 | } 36 | 37 | export function compute(...points: Point[]): { d: string } { 38 | const operations: string[] = ['M']; 39 | const edgePoints: Vector2[] = []; 40 | for (let i = 0; i < points.length; i += 1) { 41 | const { left, right } = computeSidePoints( 42 | points[i], 43 | points[i - 1] || points[i + 1] 44 | ); 45 | const lastLeft = edgePoints.slice(i)[0]; 46 | if ( 47 | lastLeft && 48 | vector2.length(lastLeft, left) > vector2.length(lastLeft, right) 49 | ) { 50 | edgePoints.splice(i, 0, left, right); 51 | } else { 52 | edgePoints.splice(i, 0, right, left); 53 | } 54 | } 55 | for (const p of edgePoints) { 56 | operations.push(`${p.x},${p.y}`); 57 | } 58 | const d = operations.join(' '); 59 | return { d }; 60 | } 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svg-variable-width-line", 3 | "version": "0.1.2", 4 | "description": "Create svg variable width line from points", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "test": "mochapack", 12 | "build": "webpack --mode production", 13 | "build:demo": "webpack --mode production --config webpack.demo.config.js", 14 | "start": "webpack-dev-server --config webpack.demo.config.js", 15 | "prepare": "make dist", 16 | "prepublishOnly": "npm run test" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/NateScarlet/svg-variable-width-line.git" 21 | }, 22 | "keywords": [ 23 | "svg", 24 | "stroke", 25 | "line" 26 | ], 27 | "author": { 28 | "name": "NateScarlet", 29 | "email": "NateScarlet@Gmail.com" 30 | }, 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/NateScarlet/svg-variable-width-line/issues" 34 | }, 35 | "homepage": "https://github.com/NateScarlet/svg-variable-width-line#readme", 36 | "devDependencies": { 37 | "@babel/core": "7.24.9", 38 | "@babel/preset-env": "7.24.8", 39 | "@types/chai": "4.3.16", 40 | "@types/mocha": "5.2.7", 41 | "@typescript-eslint/eslint-plugin": "2.34.0", 42 | "@typescript-eslint/parser": "2.34.0", 43 | "babel-loader": "8.3.0", 44 | "chai": "4.5.0", 45 | "copy-webpack-plugin": "5.1.2", 46 | "eslint": "6.8.0", 47 | "eslint-config-prettier": "6.15.0", 48 | "html-webpack-plugin": "3.2.0", 49 | "mocha": "6.2.3", 50 | "mochapack": "1.1.15", 51 | "prettier": "1.19.1", 52 | "ts-loader": "6.2.2", 53 | "typescript": "3.9.10", 54 | "webpack": "4.47.0", 55 | "webpack-chain": "6.5.1", 56 | "webpack-cli": "3.3.12", 57 | "webpack-dev-server": "3.11.3" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /demo/index.ts: -------------------------------------------------------------------------------- 1 | import * as svgVariableWidthLine from '@'; 2 | import { Point } from '@'; 3 | 4 | class DrawingHandler { 5 | public el: SVGSVGElement; 6 | public isDrawing = false; 7 | public points: svgVariableWidthLine.Point[] = []; 8 | public target?: SVGPathElement; 9 | public width = 20; 10 | public lastDrawTime?: number; 11 | 12 | public constructor(el: SVGSVGElement) { 13 | this.el = el; 14 | } 15 | onPointerdown(e: PointerEvent): void { 16 | e.preventDefault(); 17 | this.isDrawing = true; 18 | this.points = []; 19 | const target = document.createElementNS( 20 | 'http://www.w3.org/2000/svg', 21 | 'path' 22 | ); 23 | this.el.appendChild(target); 24 | this.target = target; 25 | } 26 | get mustTarget(): SVGPathElement { 27 | if (!this.target) { 28 | throw new Error('Should has target'); 29 | } 30 | return this.target; 31 | } 32 | onPointermove(e: PointerEvent): void { 33 | if (!this.isDrawing) { 34 | return; 35 | } 36 | e.preventDefault(); 37 | if (this.lastDrawTime && Date.now() - this.lastDrawTime < 4) { 38 | return; 39 | } 40 | this.lastDrawTime = Date.now(); 41 | const { x, y } = svgVariableWidthLine.util.translatePoint(this.el, e); 42 | const w = e.pressure * this.width; 43 | const p: Point = { x, y, w }; 44 | const lastPoint = this.points.slice(-1)[0]; 45 | if ( 46 | lastPoint && 47 | svgVariableWidthLine.vector2.length(lastPoint, p) < this.width 48 | ) { 49 | return; 50 | } 51 | this.points.push(p); 52 | if (e.pointerType === 'mouse') { 53 | this.points = this.points.map((v, i, array) => ({ 54 | ...v, 55 | w: this.width * (i / array.length), 56 | })); 57 | } 58 | this.update(); 59 | } 60 | onPointerup(e: PointerEvent): void { 61 | e.preventDefault(); 62 | this.isDrawing = false; 63 | delete this.target; 64 | } 65 | install(): void { 66 | this.el.addEventListener('pointerdown', this.onPointerdown.bind(this)); 67 | this.el.addEventListener('pointermove', this.onPointermove.bind(this)); 68 | this.el.addEventListener('pointerup', this.onPointerup.bind(this)); 69 | } 70 | update(): void { 71 | const { d } = svgVariableWidthLine.compute( 72 | ...svgVariableWidthLine.smooth(this.points, 4) 73 | ); 74 | this.mustTarget.setAttribute('d', d); 75 | } 76 | } 77 | 78 | ((): void => { 79 | const el = document.querySelector('svg#canvas'); 80 | if (!(el instanceof SVGSVGElement)) { 81 | throw Error('Missing canvas element'); 82 | } 83 | new DrawingHandler(el).install(); 84 | el.addEventListener('pointermove', e => { 85 | const el = document.querySelector('#pressure'); 86 | if (!el) { 87 | return; 88 | } 89 | el.textContent = e.pressure.toString(); 90 | }); 91 | el.addEventListener('pointermove', e => { 92 | const el = document.querySelector('#pointer-type'); 93 | if (!el) { 94 | return; 95 | } 96 | el.textContent = e.pointerType; 97 | }); 98 | })(); 99 | --------------------------------------------------------------------------------