├── .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 | | ![](static/example_fill.png) | ![](static/example_wire.png) | 29 | 30 | | UVs | Decoration using UV.y | 31 | |-|-| 32 | | ![](static/example_uv.png) | ![](static/example_decoration.png) | 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 | ![Combination Test Filled](static/combination_test_fill.png) 106 | 107 | ![Combination Test](static/combination_test.png) 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 | ![Segment](static/segment.png) 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 | } --------------------------------------------------------------------------------