├── .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 | [](https://www.npmjs.com/package/svg-variable-width-line)
4 | [](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 |
--------------------------------------------------------------------------------