├── .eslintignore
├── static
├── segment.png
├── example_uv.png
├── example_fill.png
├── example_wire.png
├── combination_test.png
├── example_decoration.png
└── combination_test_fill.png
├── .vscode
├── extensions.json
└── settings.json
├── src
├── index.ts
├── glsl.ts
├── glslUtils.ts
├── utils
│ ├── __tests__
│ │ ├── addBoundary.test.ts
│ │ ├── lineSegmentMesh.test.ts
│ │ └── calculateDistances.test.ts
│ ├── calculateDistances.ts
│ ├── addBoundaries.ts
│ └── lineSegmentMesh.ts
├── joins.ts
├── caps.ts
├── lines.ts
└── base.ts
├── babel.config.json
├── demo
├── index.html
├── index.ts
├── tests
│ ├── stress.ts
│ ├── interaction.ts
│ └── combinations.ts
└── webglLines.ts
├── tsconfig.json
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .eslintrc.json
├── package.json
└── README.md
/.eslintignore:
--------------------------------------------------------------------------------
1 | # don't ever lint node_modules
2 | node_modules
3 | dist
4 | coverage
--------------------------------------------------------------------------------
/static/segment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deluksic/regl-insta-lines/HEAD/static/segment.png
--------------------------------------------------------------------------------
/static/example_uv.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deluksic/regl-insta-lines/HEAD/static/example_uv.png
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint"
4 | ]
5 | }
--------------------------------------------------------------------------------
/static/example_fill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deluksic/regl-insta-lines/HEAD/static/example_fill.png
--------------------------------------------------------------------------------
/static/example_wire.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deluksic/regl-insta-lines/HEAD/static/example_wire.png
--------------------------------------------------------------------------------
/static/combination_test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deluksic/regl-insta-lines/HEAD/static/combination_test.png
--------------------------------------------------------------------------------
/static/example_decoration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deluksic/regl-insta-lines/HEAD/static/example_decoration.png
--------------------------------------------------------------------------------
/static/combination_test_fill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deluksic/regl-insta-lines/HEAD/static/combination_test_fill.png
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { createLineBase, CreateLineBaseOptions } from './base';
2 | export { JoinType } from './joins';
3 | export { CapType } from './caps';
4 | export { createLines, LineDescriptor, CreateLinesOptions } from './lines';
5 |
--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "targets": {
7 | "node": "current"
8 | }
9 | }
10 | ],
11 | "@babel/preset-typescript"
12 | ]
13 | }
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Lines demo
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "git.ignoreLimitWarning": true,
3 | "typescript.tsdk": "node_modules/typescript/lib",
4 | "editor.detectIndentation": false,
5 | "editor.tabSize": 2,
6 | "editor.formatOnSave": true,
7 | "editor.codeActionsOnSave": {
8 | "source.fixAll.eslint": true,
9 | }
10 | }
--------------------------------------------------------------------------------
/src/glsl.ts:
--------------------------------------------------------------------------------
1 | /** More explicit type for GLSL strings */
2 | export type GLSL = string;
3 |
4 | /**
5 | * Tagged template literal that does nothing. Useful for syntax highlighting.
6 | * @param strings
7 | * @param values
8 | */
9 | export const glsl = (s: TemplateStringsArray, ...v: unknown[]): string =>
10 | s.flatMap((s, i) => [s, v[i] === undefined ? '' : v[i]]).join('') as GLSL;
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "allowSyntheticDefaultImports": true,
5 | "downlevelIteration": true,
6 | "lib": [
7 | "ESNext",
8 | "DOM",
9 | "DOM.Iterable"
10 | ],
11 | "outDir": "dist",
12 | "strict": true
13 | },
14 | "include": [
15 | "src/index.ts"
16 | ],
17 | "exclude": [
18 | "**/__tests__"
19 | ]
20 | }
--------------------------------------------------------------------------------
/demo/index.ts:
--------------------------------------------------------------------------------
1 | import createRegl from 'regl';
2 | import { main as stress } from './tests/stress';
3 | import { main as inter } from './tests/interaction';
4 | import { main as comb } from './tests/combinations';
5 |
6 | import { createLines } from './webglLines';
7 | import { createLines as createLines3D } from '../src/lines';
8 |
9 | const regl = createRegl({
10 | extensions: ['ANGLE_instanced_arrays']
11 | });
12 | // stress(regl, createLines);
13 | // stress(regl, createLines);
14 | // inter(regl);
15 | comb(regl);
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches: [ master ]
5 | pull_request:
6 | branches: [ master ]
7 |
8 | jobs:
9 | Test:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v2
14 |
15 | - uses: actions/cache@v2
16 | with:
17 | path: '**/node_modules'
18 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
19 |
20 | - name: Install
21 | run: yarn --frozen-lockfile
22 |
23 | - name: Lint
24 | run: yarn lint
25 |
26 | - name: Type checks
27 | run: yarn type
28 |
29 | - name: Tests
30 | run: yarn test
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Dependency directories
9 | node_modules/
10 | jspm_packages/
11 |
12 | # TypeScript cache
13 | *.tsbuildinfo
14 |
15 | # Optional npm cache directory
16 | .npm
17 |
18 | # Optional eslint cache
19 | .eslintcache
20 |
21 | # Optional REPL history
22 | .node_repl_history
23 |
24 | # Output of 'npm pack'
25 | *.tgz
26 |
27 | # Yarn Integrity file
28 | .yarn-integrity
29 |
30 | # parcel-bundler cache (https://parceljs.org/)
31 | .parcel-cache
32 |
33 | # yarn v2
34 | .yarn/cache
35 | .yarn/unplugged
36 | .yarn/build-state.yml
37 | .pnp.*
38 |
39 | # production build
40 | dist
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "plugins": [
5 | "jest",
6 | "@typescript-eslint"
7 | ],
8 | "extends": [
9 | "eslint:recommended",
10 | "plugin:jest/recommended",
11 | "plugin:@typescript-eslint/eslint-recommended",
12 | "plugin:@typescript-eslint/recommended"
13 | ],
14 | "rules": {
15 | "semi": "error",
16 | "@typescript-eslint/no-use-before-define": "off",
17 | "@typescript-eslint/explicit-function-return-type": "off",
18 | "@typescript-eslint/no-empty-function": "off",
19 | "@typescript-eslint/ban-ts-ignore": "off",
20 | "quotes": [
21 | "error",
22 | "single",
23 | {
24 | "allowTemplateLiterals": true
25 | }
26 | ]
27 | }
28 | }
--------------------------------------------------------------------------------
/src/glslUtils.ts:
--------------------------------------------------------------------------------
1 | import { glsl } from './glsl';
2 |
3 | export const PI = glsl`
4 | #define PI 3.1415926535897932384626
5 | `;
6 |
7 | export const slerp = glsl`
8 | ${PI}
9 |
10 | mat2 rotMat(float angle, float t) {
11 | float cost = cos(angle * t);
12 | float sint = sin(angle * t);
13 | return mat2(cost, -sint, sint, cost);
14 | }
15 |
16 | float signedAngle(vec2 a, vec2 b, float preferSign) {
17 | vec2 diff = b + a;
18 | if(dot(diff, diff) < 1e-5) return preferSign*PI;
19 | return atan(a.x*b.y - a.y*b.x, a.x*b.x + a.y*b.y);
20 | }
21 |
22 | // assumes v0 and v1 are unit vectors
23 | vec2 slerp(vec2 v0, vec2 v1, float t, float preferSign)
24 | {
25 | float theta = signedAngle(v1, v0, preferSign);
26 | return rotMat(theta, t) * v0;
27 | }
28 | `;
--------------------------------------------------------------------------------
/demo/tests/stress.ts:
--------------------------------------------------------------------------------
1 | import { Regl } from 'regl';
2 | import { createLines } from '../webglLines';
3 | import { createLines as createLines3D } from '../../src/lines';
4 | import { glsl } from '../../src/glsl';
5 |
6 | type Algo =
7 | | typeof createLines3D
8 | | typeof createLines;
9 |
10 | function ecgPoints(nlines = 64, npoints = 1000, scale = 1) {
11 | return [...new Array(nlines).keys()]
12 | .map(i => [...new Array(npoints).keys()]
13 | .map(j => [
14 | (j + 0.5) / npoints,
15 | (i + 0.5) / nlines + (Math.random() * 2 - 1) * scale / nlines,
16 | 0
17 | ] as [number, number, number]));
18 | }
19 |
20 | export function main(regl: Regl, algo: Algo) {
21 | const points = ecgPoints(64, 2000, 0.5);
22 | const lines = algo(regl, {
23 | dimension: 3,
24 | cameraTransform: glsl`
25 | vec4 cameraTransform(vec4 pos) {
26 | return pos * 2. - 1.;
27 | }
28 | `
29 | });
30 | lines.setLines(points.map(l => ({ points: l })));
31 | lines.setWidth(1);
32 | regl.frame(() => {
33 | regl.clear({ color: [0, 0, 0, 1] });
34 | lines.render();
35 | });
36 | }
--------------------------------------------------------------------------------
/src/utils/__tests__/addBoundary.test.ts:
--------------------------------------------------------------------------------
1 | import { addBoundaries } from '../addBoundaries';
2 |
3 | describe('Test addBoundaries', () => {
4 | it('returns empty array for empty array input', () => {
5 | expect(addBoundaries([])).toHaveLength(0);
6 | expect(addBoundaries([], true)).toHaveLength(0);
7 | expect(addBoundaries([], false, undefined)).toHaveLength(0);
8 | expect(addBoundaries([], true, undefined)).toHaveLength(0);
9 | });
10 |
11 | it('returns correct boundaries for one element', () => {
12 | expect(addBoundaries([1.5], false)).toEqual([1.5, 1.5, 1.5]);
13 | expect(addBoundaries([1.5], true)).toEqual([1.5, 1.5, 1.5, 1.5]);
14 | expect(addBoundaries([1.5], false, 0)).toEqual([0, 1.5, 0]);
15 | expect(addBoundaries([1.5], true, 0)).toEqual([0, 1.5, 0, 0]);
16 | });
17 |
18 | it('returns correct boundaries for multiple elements', () => {
19 | expect(addBoundaries([1, 2, 3], false)).toEqual([1, 1, 2, 3, 3]);
20 | expect(addBoundaries([1, 2, 3], true)).toEqual([3, 1, 2, 3, 1, 2]);
21 | expect(addBoundaries([1, 2, 3], false, 0)).toEqual([0, 1, 2, 3, 0]);
22 | expect(addBoundaries([1, 2, 3], true, 0)).toEqual([0, 1, 2, 3, 0, 0]);
23 | });
24 | });
--------------------------------------------------------------------------------
/src/joins.ts:
--------------------------------------------------------------------------------
1 | import { glsl } from './glsl';
2 |
3 | export const JoinType = {
4 | bevel: glsl`
5 | vec2 join(vec2 a, vec2 b, float percent) {
6 | return mix(a, b, percent);
7 | }
8 | `,
9 | round: glsl`
10 | vec2 join(vec2 a, vec2 b, float percent) {
11 | return slerp(a, b, percent, -1.0);
12 | }
13 | `,
14 | miter: (limit = 3): string => glsl`
15 | vec2 join(vec2 a, vec2 b, float percent) {
16 | if (percent == 0.0) return a;
17 | if (percent == 1.0) return b;
18 | float limit = ${limit.toFixed(4)};
19 | vec2 mp = miterPoint(a, b);
20 | float mplen2 = dot(mp, mp);
21 | if (mplen2 < 1e-5 || mplen2 > limit*limit) {
22 | return mix(a, b, percent);
23 | } else {
24 | return mp;
25 | }
26 | }
27 | `,
28 | miterSquare: glsl`
29 | vec2 join(vec2 a, vec2 b, float percent) {
30 | if (percent == 0.0) return a;
31 | if (percent == 1.0) return b;
32 | vec2 anorm = vec2(-a.y, a.x);
33 | vec2 bnorm = vec2(b.y, -b.x);
34 | if (dot(anorm, b) <= 0.0) return vec2(0.0);
35 | vec2 mid = normalize(anorm + bnorm);
36 | return miterPoint(percent < 0.5 ? a : b, mid);
37 | }
38 | `
39 | };
40 |
--------------------------------------------------------------------------------
/src/utils/calculateDistances.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Given an array of elements and a function to specify "distance" between
3 | * elements, compute normalized index, total and relative distance for each element. E.g.:
4 | * ```
5 | * calculateDistances([1, -2, 3], (a, b) => Math.abs(b - a)) // [[0., 0, 0.], [0.333, 3, 0.375], [0.666, 8, 1.]]
6 | * ```
7 | * @param array
8 | * @param distanceFn
9 | * @param closed If closed, its as if the first point was added to the end
10 | */
11 | export function calculateDistances(
12 | array: T[],
13 | distanceFn: (prev: T, curr: T) => number,
14 | closed?: boolean
15 | ): [number, number, number][] {
16 | let sum = 0;
17 | const len = array.length + (closed ? 1 : 0);
18 | const acc = [...new Array(len).keys()].map(
19 | i => {
20 | if (i === 0) return [0, 0, 0] as [number, number, number];
21 | sum += distanceFn(array[i - 1], array[i % array.length]);
22 | return [i / array.length, sum, 0] as [number, number, number];
23 | }
24 | );
25 | // prevent div by 0
26 | sum = sum === 0 ? 1 : sum;
27 | // after everything is summed up, calculate relative distances
28 | for (let i = 0; i < len; ++i) {
29 | acc[i][2] = acc[i][1] / sum;
30 | }
31 | return acc;
32 | }
--------------------------------------------------------------------------------
/src/caps.ts:
--------------------------------------------------------------------------------
1 | import { glsl } from './glsl';
2 |
3 | export const CapType = {
4 | butt: glsl`
5 | // vec2 cap(vec2 dir, vec2 norm, float percent) {
6 | return percent < 0.5 ? norm : -norm;
7 | // }
8 | `,
9 | square: glsl`
10 | // vec2 cap(vec2 dir, vec2 norm, float percent) {
11 | if (percent == 0.0) return norm;
12 | if (percent == 1.0) return -norm;
13 | return percent < 0.5 ? dir + norm : dir - norm;
14 | //}
15 | `,
16 | round: glsl`
17 | // vec2 cap(vec2 dir, vec2 norm, float percent) {
18 | return slerp(norm, dir, percent * 2.0, -1.0);
19 | // }
20 | `,
21 | arrow: (size = 2.5, angle = 60): string => glsl`
22 | // vec2 cap(vec2 dir, vec2 norm, float percent) {
23 | float size = ${size.toFixed(4)};
24 | float angle = radians(${angle.toFixed(4)});
25 | mat2 prot = rotMat(angle, 1.0);
26 | mat2 nrot = rotMat(-angle, 1.0);
27 | vec2 shoulder1 = -nrot * dir * size;
28 | vec2 shoulder2 = -prot * dir * size;
29 | vec2 mid = dir * size;
30 | if (percent == 0.0) return norm;
31 | if (percent == 1.0) return -norm;
32 | if (percent == 0.5) return mid;
33 | if (percent < 0.5) return shoulder1;
34 | if (percent > 0.5) return shoulder2;
35 | // }
36 | `
37 | };
38 |
--------------------------------------------------------------------------------
/src/utils/__tests__/lineSegmentMesh.test.ts:
--------------------------------------------------------------------------------
1 | import { lineSegmentMesh } from '../lineSegmentMesh';
2 |
3 | describe('Test lineSegmentMesh', () => {
4 | it('throws on joinCount < 1', () => {
5 | expect(() => lineSegmentMesh(0)).toThrow('Line segment mesh got invalid value for \'joinCount\': 0.');
6 | expect(() => lineSegmentMesh(-1)).toThrow('Line segment mesh got invalid value for \'joinCount\': -1.');
7 | });
8 |
9 | it('returns correctly for joinCount=1', () => {
10 | expect(lineSegmentMesh(1)).toEqual({
11 | indices: [[0, 1, 3], [0, 3, 4], [1, 0, 2], [1, 2, 6], [0, 4, 5], [1, 6, 7]],
12 | vertices: [[-1, 0, 0], [1, 0, 0], [-1, -1, 0], [1, -1, 0], [-1, 1, 0], [-1, 1, 1], [1, 1, 0], [1, 1, 1]]
13 | });
14 | });
15 |
16 | it('returns correctly for joinCount=2', () => {
17 | expect(lineSegmentMesh(2)).toEqual({
18 | indices: [[0, 1, 3], [0, 3, 4], [1, 0, 2], [1, 2, 7], [0, 4, 5], [0, 5, 6], [1, 7, 8], [1, 8, 9]],
19 | vertices: [[-1, 0, 0], [1, 0, 0], [-1, -1, 0], [1, -1, 0], [-1, 1, 0], [-1, 1, 0.5], [-1, 1, 1], [1, 1, 0], [1, 1, 0.5], [1, 1, 1]]
20 | });
21 | });
22 |
23 | it('returns correct number of vertices/indices for joinCount > 2', () => {
24 | const mesh = lineSegmentMesh(64);
25 | // 64 + 64 + 6 main vertices = 134
26 | expect(mesh.vertices.length).toEqual(134);
27 | // 64 + 64 + 4 main triangles = 132
28 | expect(mesh.indices.length).toEqual(132);
29 | });
30 | });
--------------------------------------------------------------------------------
/src/utils/addBoundaries.ts:
--------------------------------------------------------------------------------
1 | type ArrayElement = T extends (infer E)[] ? E : never;
2 | /**
3 | * When drawing a line, each segment is specified using 4 consecutive points: p0, p1, p2, p3.
4 | * A line segment is drawn only through p1 --- p2, but p0 and p3 are important for
5 | * segment endings. If p0 === p1, a cap will be drawn on the start, and if p2 === p3
6 | * a cap will be drawn on the end. Otherwise, an appropriate "accordion" will be computed.
7 | *
8 | * Given a line specified by points (p0, p1, ..., pn) generate boundary conditions:
9 | * - closed: pn, p0, p1, ..., pn, p0, p1
10 | * - open: p0, p0, p1, ..., pn, pn
11 | *
12 | * Or, if fill specified, use that instead:
13 | * - closed: fill, p0, p1, ..., pn, fill, fill
14 | * - open: fill, p0, p1, ..., pn, fill
15 | *
16 | * @param points
17 | * @param closed
18 | * @param fill
19 | */
20 | export function addBoundaries(
21 | points: T,
22 | closed?: boolean,
23 | fill?: ArrayElement
24 | ): ArrayElement[] {
25 | if (!Array.isArray(points) || points.length === 0) {
26 | return [];
27 | }
28 | if (closed) {
29 | return [
30 | fill ?? points[points.length - 1] ?? points[0],
31 | ...points,
32 | fill ?? points[0],
33 | fill ?? points[1] ?? points[0],
34 | ];
35 | } else {
36 | return [
37 | fill ?? points[0],
38 | ...points,
39 | fill ?? points[points.length - 1] ?? points[0]
40 | ];
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "regl-insta-lines",
3 | "homepage": "https://github.com/deluksic/regl-insta-lines",
4 | "repository": {
5 | "url": "https://github.com/deluksic/regl-insta-lines"
6 | },
7 | "author": {
8 | "email": "deluksic@gmail.com",
9 | "name": "Unknown"
10 | },
11 | "version": "2.0.1",
12 | "main": "dist/index.js",
13 | "types": "dist/index.d.ts",
14 | "source": true,
15 | "license": "MIT",
16 | "scripts": {
17 | "build": "([ -d dist ] && rm -r dist || echo) && tsc",
18 | "start": "parcel demo/index.html",
19 | "lint": "eslint . --ext .jsx,.ts,.tsx",
20 | "lint:fix": "yarn lint --fix",
21 | "test": "jest",
22 | "test:watch": "yarn test --watch",
23 | "type": "tsc --noEmit"
24 | },
25 | "dependencies": {
26 | "regl": "^2.1.0"
27 | },
28 | "devDependencies": {
29 | "@babel/core": "7.19.1",
30 | "@babel/preset-env": "7.19.1",
31 | "@babel/preset-typescript": "7.18.6",
32 | "@types/dat.gui": "0.7.7",
33 | "@types/jest": "29.0.3",
34 | "@types/node": "18.7.18",
35 | "@typescript-eslint/eslint-plugin": "5.37.0",
36 | "@typescript-eslint/parser": "5.37.0",
37 | "babel-jest": "29.0.3",
38 | "dat.gui": "0.7.9",
39 | "eslint": "8.23.1",
40 | "eslint-plugin-import": "2.26.0",
41 | "eslint-plugin-jest": "27.0.4",
42 | "jest": "29.0.3",
43 | "jest-matcher-deep-close-to": "1.3.0",
44 | "parcel": "2.7.0",
45 | "typescript": "4.8.3"
46 | },
47 | "files": [
48 | "dist"
49 | ]
50 | }
51 |
--------------------------------------------------------------------------------
/src/utils/__tests__/calculateDistances.test.ts:
--------------------------------------------------------------------------------
1 | import { calculateDistances } from '../calculateDistances';
2 |
3 | describe('Test calculateDistances', () => {
4 | it('returns empty array for empty array input', () => {
5 | expect(calculateDistances([], () => 1)).toHaveLength(0);
6 | });
7 |
8 | it('returns [0, 0, 0] for one element', () => {
9 | expect(calculateDistances([1], () => 1)).toEqual([[0, 0, 0]]);
10 | });
11 |
12 | it('returns correct values for closed=false', () => {
13 | const diff = (a: number, b: number) => Math.abs(b - a);
14 | expect(calculateDistances([1, -2, 3], diff)).toEqual([
15 | [0, 0, 0],
16 | [1 / 3, 3, 3 / 8],
17 | [2 / 3, 8, 1],
18 | ]);
19 | expect(calculateDistances([6, 7, 0, -3, 4], diff)).toEqual([
20 | [0, 0, 0],
21 | [1 / 5, 1, 1 / 18],
22 | [2 / 5, 8, 8 / 18],
23 | [3 / 5, 11, 11 / 18],
24 | [4 / 5, 18, 1],
25 | ]);
26 | });
27 |
28 | it('returns correct values for closed=true', () => {
29 | const diff = (a: number, b: number) => Math.abs(b - a);
30 | expect(calculateDistances([1, -2, 3], diff, true)).toEqual([
31 | [0, 0, 0],
32 | [1 / 3, 3, 3 / 10],
33 | [2 / 3, 8, 8 / 10],
34 | [3 / 3, 10, 1],
35 | ]);
36 | expect(calculateDistances([6, 7, 0, -3, 4], diff, true)).toEqual([
37 | [0, 0, 0],
38 | [1 / 5, 1, 1 / 20],
39 | [2 / 5, 8, 8 / 20],
40 | [3 / 5, 11, 11 / 20],
41 | [4 / 5, 18, 18 / 20],
42 | [5 / 5, 20, 1],
43 | ]);
44 | });
45 | });
--------------------------------------------------------------------------------
/src/utils/lineSegmentMesh.ts:
--------------------------------------------------------------------------------
1 | type LineSegmentMesh = {
2 | vertices: [number, number, number][];
3 | indices: [number, number, number][]
4 | }
5 |
6 | /**
7 | * Create a line segment mesh as described here:
8 | * https://www.geogebra.org/geometry/uw5kurzg
9 | *
10 | * Vertices are described using 3 numbers:
11 | * [P1 | P2, c | P | a, a..b]
12 | * [-1 | 1, -1 | 0 | 1, 0..1]
13 | *
14 | * @param joinCount
15 | */
16 | export function lineSegmentMesh(joinCount: number): LineSegmentMesh {
17 | if (joinCount < 1) {
18 | throw new Error(`Line segment mesh got invalid value for 'joinCount': ${joinCount}.`);
19 | }
20 | const joinCountRange = [...new Array(joinCount).keys()];
21 | const vertices = [
22 | // P1 idx=0
23 | [-1, 0, 0],
24 | // P2 idx=1
25 | [1, 0, 0],
26 | // P1c idx=2
27 | [-1, -1, 0],
28 | // P2c idx=3
29 | [1, -1, 0],
30 | // P1a to P1b idx=4..(joinCount+4)
31 | [-1, 1, 0],
32 | ...joinCountRange.map(
33 | i => [-1, 1, (i + 1) / joinCount]
34 | ),
35 | // P2a to P2b idx=(joinCount+5)..(joinCount*2+5)
36 | [1, 1, 0],
37 | ...joinCountRange.map(
38 | i => [1, 1, (i + 1) / joinCount]
39 | )
40 | ] as [number, number, number][];
41 | const indices = [
42 | // P1, P2, P2c
43 | [0, 1, 3],
44 | // P1, P2c, P1a
45 | [0, 3, 4],
46 | // P2, P1, P1c
47 | [1, 0, 2],
48 | // P2, P1c, P2a
49 | [1, 2, joinCount + 5],
50 | // P1, P1a to P1b
51 | ...joinCountRange.map(
52 | i => [0, 4 + i, 5 + i]
53 | ),
54 | // P2, P2a to P2b
55 | ...joinCountRange.map(
56 | i => [1, joinCount + 5 + i, joinCount + 6 + i]
57 | )
58 | ] as [number, number, number][];
59 | return {
60 | vertices,
61 | indices
62 | };
63 | }
64 |
--------------------------------------------------------------------------------
/demo/webglLines.ts:
--------------------------------------------------------------------------------
1 | import REGL, { Regl } from 'regl';
2 | import { glsl, GLSL } from '../src/glsl';
3 | import { LineDescriptor } from '../src';
4 |
5 | const defaultCameraTransform = glsl`
6 | vec4 cameraTransform(vec4 pos) {
7 | return pos;
8 | }
9 | `;
10 |
11 | export type CreateLineOptions = {
12 | /**
13 | * Shader code for camera transformation:
14 | * ```glsl
15 | * vec4 cameraTransform(vec4 pos);
16 | * ```
17 | */
18 | cameraTransform?: GLSL;
19 | }
20 |
21 | export type LinesUniforms = {
22 | color: [number, number, number, number];
23 | }
24 |
25 | export type LinesAttributes = {
26 | position: [number, number, number][] | REGL.Buffer;
27 | }
28 |
29 | export type LinesProps = LinesUniforms & {
30 | points: [number, number, number][] | REGL.Buffer;
31 | count: number;
32 | };
33 |
34 | /**
35 | * Creates a regl lines drawing command.
36 | */
37 | export function createLines(regl: Regl, {
38 | cameraTransform = defaultCameraTransform
39 | }: CreateLineOptions & { width?: number }) {
40 | const points = regl.buffer({ type: 'float32' });
41 | let count = 0;
42 | function setWidth() {
43 | // noop
44 | }
45 | function setLines(lines: LineDescriptor<3>[]) {
46 | const positions = lines.flatMap(l => l.points);
47 | points(positions);
48 | count = positions.length;
49 | }
50 | const render = regl({
51 | vert: glsl`
52 | precision highp float;
53 |
54 | ${cameraTransform}
55 |
56 | attribute vec3 position;
57 |
58 | void main() {
59 | gl_Position = cameraTransform(vec4(position, 1.0));
60 | }
61 | `,
62 | frag: glsl`
63 | precision highp float;
64 | uniform vec4 color;
65 | void main() {
66 | gl_FragColor = color;
67 | }
68 | `,
69 | attributes: {
70 | position: points
71 | },
72 | uniforms: {
73 | color: (_, props) => props?.color ?? [1, 1, 1, 1]
74 | },
75 | primitive: 'line strip',
76 | count: () => count
77 | });
78 | return {
79 | render,
80 | setWidth,
81 | setLines
82 | };
83 | }
--------------------------------------------------------------------------------
/demo/tests/interaction.ts:
--------------------------------------------------------------------------------
1 | import { Regl } from 'regl';
2 | import { JoinType, CapType, createLines } from '../../src';
3 | import { glsl } from '../../src/glsl';
4 |
5 | export function main(regl: Regl) {
6 | const lines3dcmd = createLines(regl, {
7 | dimension: 2,
8 | join: JoinType.round,
9 | cap: CapType.butt,
10 | // primitive: 'line strip',
11 | joinCount: 8,
12 | frag: glsl`
13 | precision highp float;
14 | varying vec3 distanceAlongPath;
15 | varying vec2 vUv;
16 | void main() {
17 | if(gl_FrontFacing) {
18 | float dash = floor(0.5 + mod(distanceAlongPath.y, 0.05) / 0.05);
19 | vec3 color = dash > 0.5 ? vec3(.4, .2, .1) : vec3(.6, .2, .1);
20 | float radial = 1.-abs(0.5 - vUv.y)*2.;
21 | // gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
22 | gl_FragColor = vec4(color, 1.0);
23 | // gl_FragColor = vec4(0.0, distanceAlongPath.y, 0.0, 1.0);
24 | // gl_FragColor = vec4(radial, 0.0, 0.0, 1.0);
25 | // gl_FragColor = vec4(sin(vUv.y*30.), 0.0, 0.0, 1.0);
26 | } else {
27 | gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
28 | }
29 | }
30 | `,
31 | });
32 |
33 | const points: [number, number, number][][] = [
34 | [
35 | [-.2, 0.2, 0],
36 | [0.2, 0, 0],
37 | [0.15, 0.6, 0],
38 | [0.6, 0.4, 0],
39 | [0.7, 0.3, 0],
40 | [0.7, -0.5, 0],
41 | [0.2, -0.5, 0],
42 | // [0.9, -0.5, 0]
43 | ],
44 | [
45 | [-0.15, 0.7, 0],
46 | [-0.7, 0.6, 0],
47 | [-0.7, -0.0, 0],
48 | ],
49 | [
50 | [-0.15, -0.8, 0],
51 | [-0.6, -0.6, 0],
52 | [-0.3, 0., 0],
53 | ]
54 | ];
55 | document.addEventListener('mousemove', m => {
56 | points[0][0][0] = 2 * m.clientX / window.innerWidth - 1;
57 | points[0][0][1] = -2 * m.clientY / window.innerHeight + 1;
58 | });
59 | regl.frame(() => {
60 | regl._gl.lineWidth(3);
61 | regl.clear({ color: [1, 1, 1, 1] });
62 | lines3dcmd.setLines(points.map((line, i) => ({
63 | points: line.map(p => [p[0], p[1]]),
64 | radii: line.map((v, i) => 20 * (Math.sqrt(i) + 1)),
65 | closed: i === 1
66 | })));
67 | // lines3dcmd.setWidth(40 * (0.25 * Math.sin(ctx.time) + 1));
68 | lines3dcmd.setWidth(50);
69 | lines3dcmd.render();
70 | });
71 | }
--------------------------------------------------------------------------------
/demo/tests/combinations.ts:
--------------------------------------------------------------------------------
1 | import { Regl } from 'regl';
2 | import { JoinType, CapType, createLines } from '../../src';
3 | import { glsl } from '../../src/glsl';
4 | import { GUI } from 'dat.gui';
5 |
6 | function maybeString(s: string | (() => string)): string {
7 | if (typeof s === 'function') {
8 | return s();
9 | }
10 | return s;
11 | }
12 |
13 | const capjoin = Object.keys(CapType).flatMap(
14 | ct => Object.keys(JoinType).map(jt => ({
15 | cap: maybeString(CapType[ct as keyof typeof CapType]),
16 | join: maybeString(JoinType[jt as keyof typeof JoinType])
17 | })));
18 |
19 | const testLine = (angle: number, scale: number, shift: [number, number]) => [
20 | [shift[0] + scale * -Math.sin(angle / 2), shift[1] + scale * Math.cos(angle / 2) / 2],
21 | [shift[0], shift[1] - scale * Math.cos(angle / 2) / 4],
22 | [shift[0] + scale * Math.sin(angle / 2), shift[1] + scale * Math.cos(angle / 2) / 2]
23 | ] as [number, number][];
24 |
25 | const testLines = (count: number, shifty: number) => [...new Array(count).keys()]
26 | .map(i => ({
27 | points: testLine(i / (count - 1) * 2 * Math.PI, 0.08, [(i + .5) / count * 2 - 1, shifty])
28 | }));
29 |
30 | export function main(regl: Regl) {
31 | const opts = {
32 | width: 10
33 | };
34 | const gui = new GUI();
35 | gui.add(opts, 'width', 0, 30, 0.0001);
36 | const linecmds = capjoin.map(({ cap, join }) => createLines(regl, {
37 | dimension: 2,
38 | cap,
39 | join,
40 | // primitive: 'line strip',
41 | joinCount: 8,
42 | frag: glsl`
43 | precision highp float;
44 | varying vec3 distanceAlongPath;
45 | varying vec2 vUv;
46 | void main() {
47 | if(gl_FrontFacing) {
48 | float dash = floor(0.5 + mod(distanceAlongPath.y, 0.03) / 0.03);
49 | vec3 color = dash > 0.5 ? vec3(.4, .2, .1) : vec3(.6, .2, .1);
50 | gl_FragColor = vec4(color, 1.0);
51 | // gl_FragColor = vec4(0.0, distanceAlongPath.z, 0.0, 1.0);
52 | // gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
53 | } else {
54 | gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
55 | }
56 | }
57 | `,
58 | }));
59 | linecmds.forEach((cmd, i) => {
60 | cmd.setWidth(opts.width);
61 | cmd.setLines(testLines(9, -(i + .5) / linecmds.length * 2 + 1));
62 | });
63 | regl.frame(() => {
64 | linecmds.forEach(cmd => {
65 | cmd.setWidth(opts.width);
66 | });
67 | regl.clear({ color: [1, 1, 1, 1] });
68 | linecmds.forEach(cmd => cmd.render());
69 | });
70 | }
--------------------------------------------------------------------------------
/src/lines.ts:
--------------------------------------------------------------------------------
1 | import { Regl } from 'regl';
2 | import { glsl, GLSL } from './glsl';
3 | import {
4 | createLineBase,
5 | defaultCameraTransform,
6 | CreateLineBaseOptions
7 | } from './base';
8 | import { addBoundaries } from './utils/addBoundaries';
9 | import { calculateDistances } from './utils/calculateDistances';
10 |
11 | export type PositionDimension = 2 | 3;
12 | export type PositionType =
13 | PDim extends 2 ? [number, number] :
14 | PDim extends 3 ? [number, number, number] :
15 | never;
16 | export type DistanceFn = (a: PositionType, b: PositionType) => number;
17 |
18 | const defaultDistanceFn: DistanceFn<2 | 3> = (a, b) => {
19 | const [x, y, z] = [b[0] - a[0], b[1] - a[1], (b[2] ?? 0) - (a[2] ?? 0)];
20 | return Math.sqrt(x * x + y * y + z * z);
21 | };
22 |
23 | export type CreateLinesOptions = CreateLineBaseOptions & {
24 | /**
25 | * Positions can be specified using 2 or 3 floats.
26 | * In case of 2 floats, z coordinate will be zero inside the shader.
27 | * @default 3
28 | */
29 | dimension?: PDim;
30 | /**
31 | * Optional GLSL code for the camera:
32 | * transform:
33 | * ```glsl
34 | * // ...your uniforms...
35 | * vec4 cameraTransform(vec4 pos) {
36 | * return // ...your cool non-linear camera
37 | * }
38 | * ```
39 | */
40 | cameraTransformGLSL?: GLSL;
41 | /**
42 | * In addition to base frag varyings:
43 | * ```glsl
44 | * varying vec3 distanceAlongPath;
45 | * ```
46 | * Components:
47 | * x = index / number of points
48 | * y = distance
49 | * z = distance / total distance
50 | */
51 | frag?: GLSL;
52 | /**
53 | * Function to be used to calculate distanceAlongPath varying.
54 | */
55 | distanceFn?: DistanceFn;
56 | }
57 |
58 | export type LineDescriptor = {
59 | points: PositionType[];
60 | closed?: boolean;
61 | }
62 |
63 | export function createLines(
64 | regl: Regl,
65 | {
66 | cameraTransformGLSL = defaultCameraTransform,
67 | declarationsGLSL,
68 | defineVerticesGLSL,
69 | dimension = 3 as PDim,
70 | distanceFn = defaultDistanceFn,
71 | mainEndGLSL,
72 | ...linesBaseOptions
73 | }: CreateLinesOptions
74 | ): {
75 | setLines: (lines: LineDescriptor[]) => void;
76 | setWidth: (newWidth: number) => number;
77 | render: () => void;
78 | } {
79 | let count = 0;
80 | const points = regl.buffer({ type: 'float32' });
81 | const distanceAlongPath = regl.buffer({ type: 'float32' });
82 | const skipSegment = regl.buffer({ type: 'float32' });
83 | function setLines(lines: LineDescriptor[]) {
84 | const ps = lines.flatMap(
85 | line => addBoundaries(line.points, line.closed)
86 | );
87 | const ds = lines.flatMap(
88 | line => addBoundaries(calculateDistances(line.points, distanceFn, line.closed), false, [0, 0, 0])
89 | );
90 | const sk = lines.flatMap(
91 | line => addBoundaries(line.points.map(() => 0), line.closed, 1)
92 | );
93 | points(ps);
94 | distanceAlongPath(ds);
95 | skipSegment(sk);
96 | count = ps.length - 3;
97 | }
98 | const px = (x: number) => ({
99 | buffer: points,
100 | divisor: 1,
101 | offset: Float32Array.BYTES_PER_ELEMENT * dimension * x,
102 | stride: Float32Array.BYTES_PER_ELEMENT * dimension
103 | });
104 | const dx = (x: number) => ({
105 | buffer: distanceAlongPath,
106 | divisor: 1,
107 | offset: Float32Array.BYTES_PER_ELEMENT * 3 * x,
108 | stride: Float32Array.BYTES_PER_ELEMENT * 3
109 | });
110 | const positionAssign = {
111 | 2: 'p0.xy = ap0, p1.xy = ap1, p2.xy = ap2, p3.xy = ap3;',
112 | 3: 'p0 = ap0, p1 = ap1, p2 = ap2, p3 = ap3;',
113 | };
114 | const { setWidth, render } = createLineBase(regl, {
115 | ...linesBaseOptions,
116 | declarationsGLSL: glsl`
117 | ${cameraTransformGLSL}
118 | attribute vec${dimension} ap0, ap1, ap2, ap3;
119 | attribute vec3 ad1, ad2;
120 | attribute float askip;
121 | varying vec3 distanceAlongPath;
122 | ${declarationsGLSL}
123 | `,
124 | defineVerticesGLSL: glsl`
125 | ${positionAssign[dimension]}
126 | skip = askip;
127 | ${defineVerticesGLSL}
128 | `,
129 | mainEndGLSL: glsl`
130 | // use vUv.x to mix distances since vUv.x will have
131 | // corrected distance along the segment due to
132 | // reverse miter
133 | distanceAlongPath = mix(ad1, ad2, vUv.x);
134 | ${mainEndGLSL}
135 | `
136 | });
137 | const cmd = regl({
138 | attributes: {
139 | ap0: px(0),
140 | ap1: px(1),
141 | ap2: px(2),
142 | ap3: px(3),
143 | ad1: dx(1),
144 | ad2: dx(2),
145 | askip: {
146 | buffer: skipSegment,
147 | divisor: 1,
148 | offset: Float32Array.BYTES_PER_ELEMENT,
149 | stride: Float32Array.BYTES_PER_ELEMENT
150 | }
151 | },
152 | instances: () => count
153 | });
154 | return {
155 | setLines,
156 | setWidth,
157 | render: () => cmd(() => render())
158 | };
159 | }
160 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Instanced Lines for REGL
2 |
3 | ## Highly extendable instanced line rendering in a single draw call.
4 | Based on [Regl](https://github.com/regl-project/regl).
5 |
6 | Loosely based on Rye Terrell's [Instanced Line Rendering](https://wwwtyro.net/2019/11/18/instanced-lines.html), highly recommended as introduction.
7 |
8 | [Live demo](https://observablehq.com/@deluksic/regl-insta-lines-example)
9 |
10 | ## Installation
11 | ```bash
12 | npm install --save regl-insta-lines
13 | # or
14 | yarn add regl-insta-lines
15 | ```
16 |
17 | ## Features:
18 | - batch rendering of lines
19 | - vertex shader expanded
20 | - start / end caps (`butt`, `square`, `round`, `arrow(size, angle)`)
21 | - joins (`bevel`, `miter(limit)`, `miterSquare`, `round`)
22 | - closed loop support
23 | - GLSL injectable to tailor to your needs (e.g. do you have a non-linear camera?)
24 | - full type support
25 |
26 | | filled | wireframe |
27 | |-|-|
28 | |  |  |
29 |
30 | | UVs | Decoration using UV.y |
31 | |-|-|
32 | |  |  |
33 |
34 | ## API
35 |
36 | ### Example usage:
37 | ```typescript
38 | import createRegl from 'regl';
39 | import { createLines, CapType, JoinType } from 'regl-insta-lines';
40 |
41 | const regl = createRegl();
42 |
43 | // create lines
44 | const lines = createLines(regl, {
45 | // points can be specified using 2 or 3 floats, typescript will help you enforce this
46 | dimension: 2,
47 | width: 60, // in pixels
48 | cap: CapType.square,
49 | join: JoinType.miter(2), // specify limit here
50 | joinCount: 3,
51 | cameraTransform: glsl`
52 | // optional
53 | vec4 cameraTransform(vec4 pos) {
54 | return ... your cool non-linear camera
55 | }
56 | `,
57 | ... other props
58 | });
59 |
60 | // describe a batch of lines
61 | lines.setLines([{
62 | // if dimension=2
63 | points: [[0, 0], [1, 0], [1, 1]],
64 | // if dimension=3
65 | points: [[0, 0, 0], [1, 0, 0], [1, 1, 0]],
66 | // each line in the batch can be closed or open
67 | closed: true
68 | }, {
69 | ...
70 | }]);
71 |
72 |
73 | // render them
74 | regl.frame(()=>{
75 | // set width each frame
76 | lines.setWidth(30 * Math.sin(ctx.time)) // in pixels
77 | // single draw call rendering
78 | lines.render();
79 | })
80 | ```
81 | ### Constructor:
82 | | Property | Type | Default | Description |
83 | |----------|------|---------|-------------|
84 | | width | `number` | `1.0` | Width of lines in pixels. Can also be set using `.setWidth(width)` function after creation. |
85 | | cap | `string` (GLSL) or `{ start: GLSL; end: GLSL; }` | `CapType.butt` | Any of `CapType`s or your own custom GLSL function. Supported: `butt`, `square`, `round` |
86 | | join | `string` (GLSL) | `JoinType.bevel` | Any of `JoinType`s or your own custom GLSL function. Supported: `bevel`, `miter`, `round`, `miterSquare` |
87 | | joinCount | `int` | `4` | Number of triangles approximating the joins. NOTE: joins (like miter or round) effectively become bevel joins when set to 1. |
88 | | distanceFn | `(a: vec3, b: vec3) => number` | `vec3.distance` | Function that calculates distance between two points. Used to determine `distanceAlongPath` varying for fragment shader (useful for dashes for example) |
89 | | frag | `string` (GLSL) | fill white | Optional fragment shader, gets the following varyings: `vec3 vPos; vec2 vUv; vec3 distanceAlongPath;`. NOTE: Even though it's optional here, if you don't specify it, you must wrap the render call inside a regl command that does provide it, otherwise regl will scream at you. |
90 | | reverseMiterLimit | `number` | `0.5` | How far up the segment can a reverse miter go. Anything more than `0.5` runs a risk of failure, but allows sharper angles to still be reverse mitered. |
91 | | cameraTransformGLSL | `string` (GLSL) | identity | Optional GLSL code for a function of the following definition: `vec4 cameraTransform(vec4 pos);` |
92 | | declarationsGLSL | `string` (GLSL) | identity | Optional GLSL declarations code. At least `vec4 cameraTransform(vec4 pos);`. Useful for custom attributes. |
93 | | defineVerticesGLSL | `string` (GLSL) | `undefined` | Optional GLSL code that defines vertices `vec3 p0, p1, p2, p3;`, or skips a segment by setting `float skip;` to something other than `0.0` |
94 | | postprocessVerticesGLSL | `string` (GLSL) | `undefined` | Optional GLSL code that modifies clip or screen-space coordinates. |
95 | | mainEndGLSL | `string` (GLSL) | `undefined` | Optional GLSL code that runs at the end of main body. |
96 | | primitive | regl's primitive type | `triangles` | Used for debug when rendering with `lines` for example |
97 |
98 | ## Extending the base
99 | The main function `createLines` is actually a wrapper around the base implementation `createLineBase` that provides convenience around batch rendering and distance calculation. If you would like to manage your own attribute buffers, you can use `createLineBase` function directly. In this case, you can use `createLines` as a reference implementation since describing how to use it here is a bit verbose, but the interface is very close to what is described in the API section.
100 |
101 | ## Future Improvements
102 | - provide a few useful fragment shaders (e.g. dashes). Until then, check out the ones in [Live demo](https://observablehq.com/@deluksic/regl-insta-lines-example).
103 |
104 | ## Combination Test
105 | 
106 |
107 | 
108 |
109 | NOTE: "extra" lines are simply an artifact of rendering with `'line strip'`. In reality each segment is represented as 4 triangles + 2 join accordions, one on each side. Even though each segment renders an accordion on both sides, only one is visible per join because the other one gets back-face culled.
110 |
111 | ## Anatomy of a single segment
112 |
113 | You can view this at [Geogebra](https://www.geogebra.org/geometry/uw5kurzg)
114 |
115 | 
116 |
--------------------------------------------------------------------------------
/src/base.ts:
--------------------------------------------------------------------------------
1 | import { Regl, DrawConfig, DrawCommand, DefaultContext } from 'regl';
2 | import { glsl, GLSL } from './glsl';
3 | import { lineSegmentMesh } from './utils/lineSegmentMesh';
4 | import { JoinType } from './joins';
5 | import { CapType } from './caps';
6 | import { slerp } from './glslUtils';
7 |
8 | export const defaultCameraTransform = glsl`
9 | vec4 cameraTransform(vec4 pos) {
10 | return pos;
11 | }
12 | `;
13 |
14 | export type CreateLineBaseOptions = {
15 | /**
16 | * Width in pixels.
17 | * @default 1
18 | */
19 | width?: number;
20 | /**
21 | * GLSL code for calculating the caps.
22 | * ```glsl
23 | * vec2 cap(vec2 a, vec2 b, float percent);
24 | * ```
25 | * @default CapType.butt
26 | */
27 | cap?: GLSL | { start: GLSL; end: GLSL };
28 | /**
29 | * GLSL code for calculating the join.
30 | * One of JoinType.* values or your own.
31 | * ```glsl
32 | * vec2 join(vec2 a, vec2 b, float percent);
33 | * ```
34 | * @default JoinType.bevel
35 | */
36 | join?: GLSL;
37 | /**
38 | * Number of triangles approximating the joins.
39 | * NOTE: joins are effectively bevel joins when this value is set to 1.
40 | * @default 4
41 | */
42 | joinCount?: number;
43 | /**
44 | * How far up the segment can a reverse miter go. Default is 0.5.
45 | * Anything larger than 0.5 has a probability of failure, but allows
46 | * sharper angles to still be reverse-mitered. Conversely, smaller
47 | * value means that corner segments will fold earlier.
48 | * @default 0.5
49 | */
50 | reverseMiterLimit?: number;
51 | /**
52 | * Optional Fragment shader code.
53 | * You can also wrap the render call inside another cmd that
54 | * supplies the fragment shader + uniforms needed.
55 | * Following varyings are available in the base API:
56 | * ```glsl
57 | * varying vec3 vPos;
58 | * varying vec2 vUv;
59 | * ```
60 | */
61 | frag?: GLSL;
62 | /**
63 | * Optional blending mode. Alpha blending enabled by default.
64 | */
65 | blend?: DrawConfig['blend'];
66 | /**
67 | * Optional GLSL code of declarations, that at least defines a camera
68 | * transform:
69 | * ```glsl
70 | * vec4 cameraTransform(vec4 pos);
71 | * ```
72 | * @default identity
73 | */
74 | declarationsGLSL?: GLSL;
75 | /**
76 | * Required GLSL code of main() function body. After this code, these
77 | * should be defined at least:
78 | * ```glsl
79 | * vec3 p0, p1, p2, p3;
80 | * float skip;
81 | * ```
82 | * NOTE: If you set skip to something else than 0.0, the segment will
83 | * be discarded.
84 | */
85 | defineVerticesGLSL?: GLSL;
86 | /**
87 | * Optional GLSL code of main() function body. Called after clip and screen
88 | * coordinates become available:
89 | * ```glsl
90 | * // clip space coords
91 | * vec4 clip0, ..., clip3;
92 | * // screen space coords
93 | * vec2 ssp0, ..., ssp3;
94 | * ```
95 | */
96 | postprocessVerticesGLSL?: GLSL;
97 | /**
98 | * Optional GLSL code that runs at the end of main() function body.
99 | * Useful to compute aditional varyings based on some values
100 | * computed previously.
101 | */
102 | mainEndGLSL?: GLSL;
103 | /**
104 | * For setting to lines for debugging
105 | */
106 | primitive?: DrawConfig['primitive'];
107 | }
108 |
109 | /**
110 | * Creates a regl lines drawing command.
111 | */
112 | export function createLineBase(
113 | regl: Regl,
114 | {
115 | blend,
116 | cap = CapType.butt,
117 | declarationsGLSL = defaultCameraTransform,
118 | defineVerticesGLSL,
119 | frag,
120 | join = JoinType.bevel,
121 | joinCount = 4,
122 | mainEndGLSL,
123 | postprocessVerticesGLSL,
124 | primitive = 'triangles',
125 | reverseMiterLimit = 0.5,
126 | width = 1,
127 | }: CreateLineBaseOptions
128 | ): {
129 | setWidth: (newWidth: number) => number;
130 | render: DrawCommand>;
131 | } {
132 | const mesh = lineSegmentMesh(joinCount);
133 | const vertices = regl.buffer(mesh.vertices);
134 | const [startCap, endCap] = typeof cap === 'string' ?
135 | [cap, cap] : [cap.start, cap.end];
136 | return {
137 | setWidth: (newWidth: number) => width = newWidth,
138 | render: regl({
139 | frag,
140 | vert: glsl`
141 | precision highp float;
142 | uniform float radius;
143 | uniform vec2 resolution;
144 | attribute vec3 vertex;
145 |
146 | varying vec3 vPos;
147 | varying vec2 vUv;
148 |
149 | // assumes a and b are normalized
150 | vec2 miterPoint(vec2 a, vec2 b) {
151 | vec2 ab = 0.5 * (a + b);
152 | float len2 = dot(ab, ab);
153 | if (len2 < 0.0001) return vec2(0.0);
154 | return ab / len2;
155 | }
156 |
157 | vec4 tangentPoints(vec2 d) {
158 | float len = length(d);
159 | if (len < 1e-4) return vec4(0.0);
160 | vec2 dnorm = d / len;
161 | return vec4(
162 | -dnorm.y, dnorm.x,
163 | dnorm.y, -dnorm.x
164 | );
165 | }
166 |
167 | ${slerp}
168 | ${declarationsGLSL}
169 | ${join}
170 |
171 | vec2 startCap(vec2 dir, vec2 norm, float percent) {
172 | ${startCap}
173 | }
174 |
175 | vec2 endCap(vec2 dir, vec2 norm, float percent) {
176 | ${endCap}
177 | }
178 |
179 | void main() {
180 | vec2 halfRes = vec2(0.5 * resolution);
181 | vec3 p0, p1, p2, p3;
182 | float skip = 0.0;
183 | ${defineVerticesGLSL}
184 |
185 | vUv = vec2(vertex.x, -vertex.x*vertex.y)*0.5 + 0.5;
186 |
187 | // clip space coords
188 | vec4 clip0 = cameraTransform(vec4(p0, 1.0));
189 | vec4 clip1 = cameraTransform(vec4(p1, 1.0));
190 | vec4 clip2 = cameraTransform(vec4(p2, 1.0));
191 | vec4 clip3 = cameraTransform(vec4(p3, 1.0));
192 |
193 | // screen space coords
194 | vec2 ssp0 = (clip0 / clip0.w).xy * halfRes;
195 | vec2 ssp1 = (clip1 / clip1.w).xy * halfRes;
196 | vec2 ssp2 = (clip2 / clip2.w).xy * halfRes;
197 | vec2 ssp3 = (clip3 / clip3.w).xy * halfRes;
198 |
199 | ${postprocessVerticesGLSL}
200 |
201 | // detect skip
202 | if (skip != 0.0) {
203 | gl_Position.z = -1.0;
204 | return;
205 | }
206 |
207 | // position diffs
208 | vec2 p0p1 = ssp1 - ssp0;
209 | vec2 p1p2 = ssp2 - ssp1;
210 | vec2 p2p3 = ssp3 - ssp2;
211 |
212 | // tangents
213 | vec4 t01 = tangentPoints(p0p1);
214 | vec4 t12 = tangentPoints(p1p2);
215 | vec4 t23 = tangentPoints(p2p3);
216 |
217 | // abcd
218 | vec2 p1a = t12.xy, p1b = t01.xy, p1c = t12.zw, p1d = t01.zw;
219 | vec2 p2a = t12.zw, p2b = t23.zw, p2c = t12.xy, p2d = t23.xy;
220 |
221 | // choose
222 | vec4 p;
223 | vec2 a, b, c, d, sab, scd;
224 | if(vertex.x < 0.0) {
225 | p = clip1;
226 | a = p1a; b = p1b;
227 | c = p1c; d = p1d;
228 | sab = -p0p1; scd = p1p2;
229 | } else {
230 | p = clip2;
231 | a = p2a; b = p2b;
232 | c = p2c; d = p2d;
233 | sab = p2p3; scd = -p1p2;
234 | }
235 |
236 | bool isCap = b == vec2(0.0);
237 |
238 | // values required to potentially account for reverse miter
239 | float reverseMiterLimit = ${reverseMiterLimit.toFixed(4)};
240 | vec2 abrevmiter = miterPoint(a, b);
241 | vec2 cdrevmiter = miterPoint(c, d);
242 | float sablen2 = dot(sab, sab);
243 | float scdlen2 = dot(scd, scd);
244 | float ababmitlen = dot(sab, abrevmiter) * radius / sablen2;
245 | float abcdmitlen = dot(sab, cdrevmiter) * radius / sablen2;
246 | float cdabmitlen = dot(scd, abrevmiter) * radius / scdlen2;
247 | float cdcdmitlen = dot(scd, cdrevmiter) * radius / scdlen2;
248 |
249 | vec2 final = vec2(0.0);
250 | vec2 dir = vertex.x * normalize(p1p2);
251 | if (vertex.y == -1.0) {
252 | // account for reverse miter
253 | if (!isCap && cdcdmitlen > 0.0 &&
254 | cdcdmitlen < reverseMiterLimit &&
255 | abcdmitlen < reverseMiterLimit) {
256 | c = cdrevmiter;
257 | vUv.x -= vertex.x * cdcdmitlen;
258 | }
259 | final = c;
260 | } else if (vertex.y == 1.0) {
261 | // interpolate a to b (cap or join)
262 | if(isCap){
263 | if (vertex.x < 0.5) {
264 | final = startCap(dir, a, vertex.z);
265 | } else {
266 | final = endCap(dir, a, vertex.z);
267 | }
268 | } else {
269 | // account for reverse miter
270 | if (ababmitlen > 0.0 &&
271 | ababmitlen < reverseMiterLimit &&
272 | cdabmitlen < reverseMiterLimit) {
273 | a = b = abrevmiter;
274 | vUv.x -= vertex.x * cdabmitlen;
275 | }
276 | final = join(a, b, vertex.z);
277 | }
278 | }
279 |
280 | gl_Position = p;
281 | gl_Position.xy += final * radius * p.w / halfRes;
282 |
283 | // use vUv.x to mix positions since vUv.x will have
284 | // corrected distance along the segment due to
285 | // reverse miter
286 | vPos = mix(p1, p2, vUv.x);
287 |
288 | ${mainEndGLSL}
289 | }
290 | `,
291 | primitive,
292 | elements: mesh.indices,
293 | attributes: {
294 | vertex: {
295 | buffer: vertices,
296 | divisor: 0,
297 | }
298 | },
299 | uniforms: {
300 | radius: () => 0.5 * width,
301 | resolution: ctx => [ctx.viewportWidth, ctx.viewportHeight]
302 | },
303 | cull: {
304 | enable: true,
305 | face: 'back'
306 | },
307 | blend: blend ?? {
308 | enable: true,
309 | func: {
310 | src: 'src alpha',
311 | dst: 'one minus src alpha'
312 | }
313 | }
314 | })
315 | };
316 | }
--------------------------------------------------------------------------------