├── .nvmrc
├── .gitignore
├── .browserslistrc
├── .babelrc
├── examples
├── save-as-svg.js
├── save-as-png.js
├── destsvg-example.html
├── custom-points-example.html
├── basic-web-example.html
├── transparency-example.html
└── color-function-example.html
├── src
├── utils
│ ├── getScalingRatio.js
│ ├── geom.js
│ ├── mulberry32.js
│ ├── debugRender.js
│ ├── colorFunctions.js
│ └── colorbrewer.js
├── trianglify.node.test.js
├── pattern.js
├── trianglify.js
├── trianglify.browser.test.js
└── __snapshots__
│ ├── trianglify.node.test.js.snap
│ └── trianglify.browser.test.js.snap
├── .github
└── workflows
│ └── build.yml
├── changelog.txt
├── CONTRIBUTING.md
├── rollup.config.js
├── package.json
├── Readme.md
└── LICENSE
/.nvmrc:
--------------------------------------------------------------------------------
1 | v12
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | defaults
2 | not IE 11
3 | not IE_Mob 11
4 | maintained node versions
5 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["@babel/plugin-proposal-class-properties"],
3 | "presets": [
4 | ["@babel/preset-env"]
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/examples/save-as-svg.js:
--------------------------------------------------------------------------------
1 | // Basic command-line example
2 | // Usage: node save-as-svg.js
3 | var fs = require('fs')
4 | var trianglify = require('../dist/trianglify.js')
5 |
6 | // Generate a pattern and render to an SVG node tree
7 | const svg = trianglify({
8 | width: 1920,
9 | height: 1080,
10 | cellSize: Math.random() * 200 + 40,
11 | xColors: 'random',
12 | variance: Math.random()
13 | }).toSVG()
14 |
15 | // Save the string to a file
16 | fs.writeFileSync('trianglify.svg', svg.toString())
17 |
--------------------------------------------------------------------------------
/src/utils/getScalingRatio.js:
--------------------------------------------------------------------------------
1 | export default function (ctx) {
2 | // adapted from https://gist.github.com/callumlocke/cc258a193839691f60dd
3 | const backingStoreRatio = (
4 | ctx.webkitBackingStorePixelRatio ||
5 | ctx.mozBackingStorePixelRatio ||
6 | ctx.msBackingStorePixelRatio ||
7 | ctx.oBackingStorePixelRatio ||
8 | ctx.backingStorePixelRatio || 1
9 | )
10 |
11 | const devicePixelRatio = (typeof window !== 'undefined' && window.devicePixelRatio) || 1
12 | const drawRatio = devicePixelRatio / backingStoreRatio
13 | return drawRatio
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/geom.js:
--------------------------------------------------------------------------------
1 | // Given an array of coordinates representing a triangle, find the centroid
2 | // of the triangle and return it as an {x, y} object
3 | // Accepts a single [[x, y], [x, y], [x, y]] argument
4 | export const getCentroid = d => {
5 | return {
6 | x: (d[0][0] + d[1][0] + d[2][0]) / 3,
7 | y: (d[0][1] + d[1][1] + d[2][1]) / 3
8 | }
9 | }
10 |
11 | export const getTopmostVertexIndex = (vertexIndices, points) => (
12 | vertexIndices.reduce(
13 | (topmost, i) => (points[i][1] < points[topmost][1] ? i : topmost),
14 | vertexIndices[0]
15 | )
16 | )
17 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 | types: [synchronize]
9 |
10 | jobs:
11 | test:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | node-version: [10.x, 12.x, 13.x]
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Use Node.js ${{ matrix.node-version }}
20 | uses: actions/setup-node@v1
21 | with:
22 | node-version: ${{ matrix.node-version }}
23 | - run: npm ci # install dependencies in cleanroom
24 | - run: npm run ci # run the "ci" script defined in package.json
25 |
--------------------------------------------------------------------------------
/examples/save-as-png.js:
--------------------------------------------------------------------------------
1 | // Basic command-line example
2 | // Usage: node save-as-png.js
3 | var fs = require('fs')
4 | var trianglify = require('../dist/trianglify.js')
5 |
6 | // Generate a pattern and then grab the PNG data uri
7 | const canvas = trianglify({
8 | width: 1920,
9 | height: 1080,
10 | cellSize: Math.random() * 200 + 40,
11 | xColors: 'random',
12 | variance: Math.random()
13 | }).toCanvas()
14 |
15 | // Save the buffer to a file. See the node-canvas docs for a full
16 | // list of all the things you can do with this Canvas object:
17 | // https://github.com/Automattic/node-canvas
18 | const file = fs.createWriteStream('trianglify.png')
19 | canvas.createPNGStream().pipe(file)
20 |
--------------------------------------------------------------------------------
/changelog.txt:
--------------------------------------------------------------------------------
1 | v4
2 | - ground-up port to ES2015
3 | - support for node-canvas 2.6
4 | - comprehensive test suite
5 | - support for rendering wireframe patterns (no fill, just stroke)
6 | - colorFunction has a new method signature that allows for more powerful customization
7 | - new built-in colorFunctions: 'sparkle' and 'shadows'
8 | - all-new example code, see examples directory
9 | - all option keys are now camelCase, not snake_case
10 | - antialiasing issues resolved for both SVG and Canvas rendering engines (#87, #30, #76)
11 | - canvas rendering is now retina-ready by default
12 | - removed dependency on seedrandom
13 | - removed dependency on jsdom
14 |
15 | v3
16 | - close paths when rendering to canvas (thanks @Allidylls)
17 | - bump a few dependencies for security
18 |
--------------------------------------------------------------------------------
/src/utils/mulberry32.js:
--------------------------------------------------------------------------------
1 | // Fast seeded RNG adapted from the public-domain implementation
2 | // by @byrc: https://github.com/bryc/code/blob/master/jshash/PRNGs.md
3 | //
4 | // Usage:
5 | // const randFn = mulberry32('string seed')
6 | // const randomNumber = randFn() // [0, 1] random float
7 | export default function mulberry32 (seed) {
8 | if (!seed) { seed = Math.random().toString(36) } // support no-seed usage
9 | var a = xmur3(seed)()
10 | return function () {
11 | a |= 0; a = a + 0x6D2B79F5 | 0
12 | var t = Math.imul(a ^ a >>> 15, 1 | a)
13 | t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t
14 | return ((t ^ t >>> 14) >>> 0) / 4294967296
15 | }
16 | }
17 |
18 | function xmur3 (str) {
19 | for (var i = 0, h = 1779033703 ^ str.length; i < str.length; i++) {
20 | h = Math.imul(h ^ str.charCodeAt(i), 3432918353)
21 | h = h << 13 | h >>> 19
22 | }
23 | return function () {
24 | h = Math.imul(h ^ h >>> 16, 2246822507)
25 | h = Math.imul(h ^ h >>> 13, 3266489909)
26 | return (h ^= h >>> 16) >>> 0
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Pull requests and issues are welcome! For all contributions, please:
4 |
5 | 1. Read the [Readme](Readme.md)
6 | 2. Search the existing [issues](https://github.com/qrohlf/trianglify/issues?q=is%3Aissue+) and [pull requests](https://github.com/qrohlf/trianglify/pulls?q=is%3Apr) to make sure your contribution isn't a duplicate
7 |
8 | ## Issues
9 |
10 | If you're submitting a bug, please include the enivronment (browser/node) and relevant environment version(s) that you have encountered the bug in.
11 |
12 | ## Pull Requests
13 |
14 | *Important: if you are submitting a pull request that does not address an open issue in the issue tracker, it would be a very good idea to create an issue to discuss your proposed changes/additions before working on them.*
15 |
16 | 1. Fork the repo on GitHub.
17 | 2. Install dependencies with `npm install`
18 | 2. Create a topic branch and make your changes.
19 | 3. Run `npm run ci` to test your code and compile it to trianglify.min.js.
20 | 4. Submit a pull request to merge your topic branch into `master`.
21 |
--------------------------------------------------------------------------------
/src/utils/debugRender.js:
--------------------------------------------------------------------------------
1 | export const debugRender = (opts, points) => {
2 | const doc = window.document
3 | const svg = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg')
4 | svg.setAttribute('width', opts.width + 400)
5 | svg.setAttribute('height', opts.height + 400)
6 |
7 | points.forEach(p => {
8 | const circle = doc.createElementNS('http://www.w3.org/2000/svg', 'circle')
9 | circle.setAttribute('cx', p[0])
10 | circle.setAttribute('cy', p[1])
11 | circle.setAttribute('r', 2)
12 | svg.appendChild(circle)
13 | })
14 |
15 | const bounds = doc.createElementNS('http://www.w3.org/2000/svg', 'rect')
16 | bounds.setAttribute('x', 0)
17 | bounds.setAttribute('y', 0)
18 | bounds.setAttribute('width', opts.width)
19 | bounds.setAttribute('height', opts.height)
20 | bounds.setAttribute('stroke-width', 1)
21 | bounds.setAttribute('stroke', 'blue')
22 | bounds.setAttribute('fill', 'none')
23 | svg.appendChild(bounds)
24 |
25 | svg.setAttribute('viewBox', `-100 -100 ${opts.width + 200} ${opts.height + 200}`)
26 | return svg
27 | }
28 |
--------------------------------------------------------------------------------
/examples/destsvg-example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Trianglify Basic Example
8 |
22 |
23 |
24 |
25 |
28 |
29 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import commonjs from '@rollup/plugin-commonjs'
2 | import resolve from '@rollup/plugin-node-resolve'
3 | import babel from '@rollup/plugin-babel'
4 | import { terser } from 'rollup-plugin-terser'
5 | import bundleSize from 'rollup-plugin-bundle-size'
6 | import pkg from './package.json'
7 |
8 | export default [
9 | { // build for node & module bundlers
10 | input: 'src/trianglify.js',
11 | external: ['chroma-js', 'delaunator', 'canvas'],
12 | plugins: [babel({ babelHelpers: 'bundled' }), bundleSize()],
13 | output: { file: pkg.main, format: 'cjs' }
14 | },
15 | {
16 | // build minified bundle to be used standalone for browser use
17 | // note: // chroma.js weighs 40k minified, a smaller solution would be nice
18 | input: 'src/trianglify.js',
19 | plugins: [terser({ output: { comments: false } }), resolve({ browser: true }), commonjs(), babel({ babelHelpers: 'bundled' }), bundleSize()],
20 | output: { file: 'dist/trianglify.bundle.js', format: 'umd', name: 'trianglify' }
21 | },
22 | {
23 | // build non-minified bundle to be used for debugging
24 | input: 'src/trianglify.js',
25 | plugins: [resolve({ browser: true }), commonjs(), babel({ babelHelpers: 'bundled' }), bundleSize()],
26 | output: { file: 'dist/trianglify.bundle.debug.js', format: 'umd', name: 'trianglify' }
27 | }
28 | ]
29 |
--------------------------------------------------------------------------------
/examples/custom-points-example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Trianglify custom points example
8 |
16 |
17 |
18 |
19 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/trianglify.node.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment node
3 | */
4 | /* eslint-env jest */
5 | // Here, we test the node-specific functionality of Trianglify.
6 | const trianglify = require('../dist/trianglify.js')
7 | const { Canvas } = require('canvas')
8 | const Pattern = trianglify.Pattern
9 |
10 | describe('Pattern generation', () => {
11 | test('return a Pattern given valid options', () => {
12 | expect(trianglify({ height: 100, width: 100 })).toBeInstanceOf(Pattern)
13 | })
14 |
15 | test('should be random by default', () => {
16 | const pattern1 = trianglify()
17 | const pattern2 = trianglify()
18 | expect(pattern1.toSVG()).not.toEqual(pattern2.toSVG())
19 | })
20 |
21 | test('should match snapshot for non-breaking version bumps', () => {
22 | expect(trianglify({ seed: 'snapshotText' }).toSVG().toString()).toMatchSnapshot()
23 | })
24 | })
25 |
26 | describe('Pattern outputs in a node environment', () => {
27 | describe('#toSVG', () => {
28 | const pattern = trianglify()
29 | const svgTree = pattern.toSVG()
30 |
31 | test('returns a synthetic tree of object literals', () => {
32 | expect(Object.keys(svgTree)).toEqual(['tagName', 'attrs', 'children', 'toString'])
33 | })
34 | })
35 |
36 | describe('#toCanvas', () => {
37 | const pattern = trianglify()
38 | const canvas = pattern.toCanvas()
39 | test('returns a node-canvas Canvas object', () => {
40 | expect(canvas).toBeInstanceOf(Canvas)
41 | expect(canvas.createPNGStream).toBeInstanceOf(Function)
42 | })
43 | })
44 | })
45 |
--------------------------------------------------------------------------------
/examples/basic-web-example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Trianglify Basic Example
8 |
22 |
23 |
24 |
27 |
28 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/utils/colorFunctions.js:
--------------------------------------------------------------------------------
1 | import chroma from 'chroma-js'
2 | // Built in color functions provided for your convenience.
3 | //
4 | // Usage example:
5 | //
6 | // const pattern = trianglify({
7 | // width: 300,
8 | // height: 200,
9 | // colorFunction: trianglify.colorFunctions.sparkle(0.2)
10 | // })
11 | //
12 | // the snippet above gives you a trianglify pattern with a 20% random
13 | // jitter applied to the x and y gradient scales
14 |
15 | // Linear interpolation of two gradients, one for the x and one for the y
16 | // axis. This is the default Trianglify color function.
17 | // The bias parameter controls how prevalent the y axis is versus the x axis
18 | export const interpolateLinear = (bias = 0.5) =>
19 | ({ xPercent, yPercent, xScale, yScale, opts }) =>
20 | chroma.mix(xScale(xPercent), yScale(yPercent), bias, opts.colorSpace)
21 |
22 | // Give the pattern a 'sparkle' effect by introducing random noise into the
23 | // x and y gradients, making for higher contrast between cells.
24 | export const sparkle = (jitterFactor = 0.15) =>
25 | ({ xPercent, yPercent, xScale, yScale, opts, random }) => {
26 | const jitter = () => (random() - 0.5) * jitterFactor
27 | const a = xScale(xPercent + jitter())
28 | const b = yScale(yPercent + jitter())
29 | return chroma.mix(a, b, 0.5, opts.colorSpace)
30 | }
31 |
32 | // This is similar to the sparkle effect, but instead of swapping colors around
33 | // it darkens cells by a random amount. The shadowIntensity parameter controls
34 | // how dark the darkest shadows are.
35 | export const shadows = (shadowIntensity = 0.8) => {
36 | return ({ xPercent, yPercent, xScale, yScale, opts, random }) => {
37 | const a = xScale(xPercent)
38 | const b = yScale(yPercent)
39 | const color = chroma.mix(a, b, 0.5, opts.colorSpace)
40 | return color.darken(shadowIntensity * random())
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "trianglify",
3 | "version": "4.1.1",
4 | "description": "Trianglify is a javascript library for generating colorful triangle meshes that can be used as SVG images and CSS backgrounds.",
5 | "main": "dist/trianglify.js",
6 | "scripts": {
7 | "lint": "standard",
8 | "test": "jest",
9 | "build": "rollup -c",
10 | "dev": "rollup -c -w",
11 | "publish-beta": "npm run build && npm publish --tag next",
12 | "ci": "npm run lint && npm run build && npm run test"
13 | },
14 | "dependencies": {
15 | "chroma-js": "^2.1.0",
16 | "delaunator": "^4.0.1",
17 | "canvas": "^2.6.1"
18 | },
19 | "directories": {
20 | "example": "examples"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "git://github.com/qrohlf/trianglify.git"
25 | },
26 | "keywords": [
27 | "svg",
28 | "canvas",
29 | "visualization",
30 | "pattern",
31 | "lowpoly"
32 | ],
33 | "standard": {
34 | "parser": "babel-eslint",
35 | "ignore": [
36 | "dist",
37 | "**/vendor/**"
38 | ]
39 | },
40 | "jest": {
41 | "setupFiles": [
42 | "jest-canvas-mock"
43 | ]
44 | },
45 | "author": "Quinn Rohlf ",
46 | "license": "GPL-3.0",
47 | "bugs": {
48 | "url": "https://github.com/qrohlf/trianglify/issues"
49 | },
50 | "homepage": "https://github.com/qrohlf/trianglify",
51 | "devDependencies": {
52 | "@babel/plugin-proposal-class-properties": "^7.8.3",
53 | "@babel/plugin-syntax-class-properties": "^7.8.3",
54 | "@babel/preset-env": "^7.9.6",
55 | "@rollup/plugin-babel": "^5.0.0",
56 | "@rollup/plugin-commonjs": "^11.1.0",
57 | "@rollup/plugin-node-resolve": "^7.1.3",
58 | "babel-eslint": "^10.1.0",
59 | "jest": "^25.5.4",
60 | "jest-canvas-mock": "^2.2.0",
61 | "rollup": "^2.7.2",
62 | "rollup-plugin-bundle-size": "^1.0.3",
63 | "rollup-plugin-terser": "^5.3.0",
64 | "standard": "^14.3.3"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/utils/colorbrewer.js:
--------------------------------------------------------------------------------
1 | /*
2 | * colorbrewer.js
3 | *
4 | * Colorbrewer colors, by Cindy Brewer
5 | */
6 |
7 | export default {
8 | YlGn: ['#ffffe5', '#f7fcb9', '#d9f0a3', '#addd8e', '#78c679', '#41ab5d', '#238443', '#006837', '#004529'],
9 | YlGnBu: ['#ffffd9', '#edf8b1', '#c7e9b4', '#7fcdbb', '#41b6c4', '#1d91c0', '#225ea8', '#253494', '#081d58'],
10 | GnBu: ['#f7fcf0', '#e0f3db', '#ccebc5', '#a8ddb5', '#7bccc4', '#4eb3d3', '#2b8cbe', '#0868ac', '#084081'],
11 | BuGn: ['#f7fcfd', '#e5f5f9', '#ccece6', '#99d8c9', '#66c2a4', '#41ae76', '#238b45', '#006d2c', '#00441b'],
12 | PuBuGn: ['#fff7fb', '#ece2f0', '#d0d1e6', '#a6bddb', '#67a9cf', '#3690c0', '#02818a', '#016c59', '#014636'],
13 | PuBu: ['#fff7fb', '#ece7f2', '#d0d1e6', '#a6bddb', '#74a9cf', '#3690c0', '#0570b0', '#045a8d', '#023858'],
14 | BuPu: ['#f7fcfd', '#e0ecf4', '#bfd3e6', '#9ebcda', '#8c96c6', '#8c6bb1', '#88419d', '#810f7c', '#4d004b'],
15 | RdPu: ['#fff7f3', '#fde0dd', '#fcc5c0', '#fa9fb5', '#f768a1', '#dd3497', '#ae017e', '#7a0177', '#49006a'],
16 | PuRd: ['#f7f4f9', '#e7e1ef', '#d4b9da', '#c994c7', '#df65b0', '#e7298a', '#ce1256', '#980043', '#67001f'],
17 | OrRd: ['#fff7ec', '#fee8c8', '#fdd49e', '#fdbb84', '#fc8d59', '#ef6548', '#d7301f', '#b30000', '#7f0000'],
18 | YlOrRd: ['#ffffcc', '#ffeda0', '#fed976', '#feb24c', '#fd8d3c', '#fc4e2a', '#e31a1c', '#bd0026', '#800026'],
19 | YlOrBr: ['#ffffe5', '#fff7bc', '#fee391', '#fec44f', '#fe9929', '#ec7014', '#cc4c02', '#993404', '#662506'],
20 | Purples: ['#fcfbfd', '#efedf5', '#dadaeb', '#bcbddc', '#9e9ac8', '#807dba', '#6a51a3', '#54278f', '#3f007d'],
21 | Blues: ['#f7fbff', '#deebf7', '#c6dbef', '#9ecae1', '#6baed6', '#4292c6', '#2171b5', '#08519c', '#08306b'],
22 | Greens: ['#f7fcf5', '#e5f5e0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#006d2c', '#00441b'],
23 | Oranges: ['#fff5eb', '#fee6ce', '#fdd0a2', '#fdae6b', '#fd8d3c', '#f16913', '#d94801', '#a63603', '#7f2704'],
24 | Reds: ['#fff5f0', '#fee0d2', '#fcbba1', '#fc9272', '#fb6a4a', '#ef3b2c', '#cb181d', '#a50f15', '#67000d'],
25 | Greys: ['#ffffff', '#f0f0f0', '#d9d9d9', '#bdbdbd', '#969696', '#737373', '#525252', '#252525', '#000000'],
26 | PuOr: ['#7f3b08', '#b35806', '#e08214', '#fdb863', '#fee0b6', '#f7f7f7', '#d8daeb', '#b2abd2', '#8073ac', '#542788', '#2d004b'],
27 | BrBG: ['#543005', '#8c510a', '#bf812d', '#dfc27d', '#f6e8c3', '#f5f5f5', '#c7eae5', '#80cdc1', '#35978f', '#01665e', '#003c30'],
28 | PRGn: ['#40004b', '#762a83', '#9970ab', '#c2a5cf', '#e7d4e8', '#f7f7f7', '#d9f0d3', '#a6dba0', '#5aae61', '#1b7837', '#00441b'],
29 | PiYG: ['#8e0152', '#c51b7d', '#de77ae', '#f1b6da', '#fde0ef', '#f7f7f7', '#e6f5d0', '#b8e186', '#7fbc41', '#4d9221', '#276419'],
30 | RdBu: ['#67001f', '#b2182b', '#d6604d', '#f4a582', '#fddbc7', '#f7f7f7', '#d1e5f0', '#92c5de', '#4393c3', '#2166ac', '#053061'],
31 | RdGy: ['#67001f', '#b2182b', '#d6604d', '#f4a582', '#fddbc7', '#ffffff', '#e0e0e0', '#bababa', '#878787', '#4d4d4d', '#1a1a1a'],
32 | RdYlBu: ['#a50026', '#d73027', '#f46d43', '#fdae61', '#fee090', '#ffffbf', '#e0f3f8', '#abd9e9', '#74add1', '#4575b4', '#313695'],
33 | Spectral: ['#9e0142', '#d53e4f', '#f46d43', '#fdae61', '#fee08b', '#ffffbf', '#e6f598', '#abdda4', '#66c2a5', '#3288bd', '#5e4fa2'],
34 | RdYlGn: ['#a50026', '#d73027', '#f46d43', '#fdae61', '#fee08b', '#ffffbf', '#d9ef8b', '#a6d96a', '#66bd63', '#1a9850', '#006837']
35 | }
36 |
--------------------------------------------------------------------------------
/examples/transparency-example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Trianglify transparency example
8 |
101 |
102 |
103 |
113 |
114 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/examples/color-function-example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Trianglify Basic Example
8 |
28 |
29 |
30 |
33 |
34 |
123 |
124 |
125 |
--------------------------------------------------------------------------------
/src/pattern.js:
--------------------------------------------------------------------------------
1 | import { createCanvas } from 'canvas' // this is a simple shim in browsers
2 | import getScalingRatio from './utils/getScalingRatio'
3 | const isBrowser = (typeof window !== 'undefined' && typeof document !== 'undefined')
4 | const doc = isBrowser && document
5 |
6 | // utility for building up SVG node trees with the DOM API
7 | const sDOM = (tagName, attrs = {}, children, existingRoot) => {
8 | const elem = existingRoot || doc.createElementNS('http://www.w3.org/2000/svg', tagName)
9 | Object.keys(attrs).forEach(
10 | k => attrs[k] !== undefined && elem.setAttribute(k, attrs[k])
11 | )
12 | children && children.forEach(c => elem.appendChild(c))
13 | return elem
14 | }
15 |
16 | // serialize attrs object to XML attributes. Assumes everything is already
17 | // escaped (safe input).
18 | const serializeAttrs = attrs => (
19 | Object.entries(attrs)
20 | .filter(([_, v]) => v !== undefined)
21 | .map(([k, v]) => `${k}='${v}'`)
22 | .join(' ')
23 | )
24 |
25 | // minimal XML-tree builder for use in Node
26 | const sNode = (tagName, attrs = {}, children) => ({
27 | tagName,
28 | attrs,
29 | children,
30 | toString: () => `<${tagName} ${serializeAttrs(attrs)}>${children ? children.join('') : ''}${tagName}>`
31 | })
32 |
33 | export default class Pattern {
34 | constructor (points, polys, opts) {
35 | this.points = points
36 | this.polys = polys
37 | this.opts = opts
38 | }
39 |
40 | _toSVG = (serializer, destSVG, _svgOpts = {}) => {
41 | const s = serializer
42 | const defaultSVGOptions = { includeNamespace: true, coordinateDecimals: 1 }
43 | const svgOpts = { ...defaultSVGOptions, ..._svgOpts }
44 | const { points, opts, polys } = this
45 | const { width, height } = opts
46 |
47 | // only round points if the coordinateDecimals option is non-negative
48 | // set coordinateDecimals to -1 to disable point rounding
49 | const roundedPoints = (svgOpts.coordinateDecimals < 0) ? points : points.map(
50 | p => p.map(x => +x.toFixed(svgOpts.coordinateDecimals))
51 | )
52 |
53 | const paths = polys.map((poly) => {
54 | const xys = poly.vertexIndices.map(i => `${roundedPoints[i][0]},${roundedPoints[i][1]}`)
55 | const d = 'M' + xys.join('L') + 'Z'
56 | const hasStroke = opts.strokeWidth > 0
57 | // shape-rendering crispEdges resolves the antialiasing issues, at the
58 | // potential cost of some visual degradation. For the best performance
59 | // *and* best visual rendering, use Canvas.
60 | return s('path', {
61 | d,
62 | fill: opts.fill ? poly.color.css() : undefined,
63 | stroke: hasStroke ? poly.color.css() : undefined,
64 | 'stroke-width': hasStroke ? opts.strokeWidth : undefined,
65 | 'stroke-linejoin': hasStroke ? 'round' : undefined,
66 | 'shape-rendering': opts.fill ? 'crispEdges' : undefined
67 | })
68 | })
69 |
70 | const svg = s(
71 | 'svg',
72 | {
73 | xmlns: svgOpts.includeNamespace ? 'http://www.w3.org/2000/svg' : undefined,
74 | width,
75 | height
76 | },
77 | paths,
78 | destSVG
79 | )
80 |
81 | return svg
82 | }
83 |
84 | toSVGTree = (svgOpts) => this._toSVG(sNode, null, svgOpts)
85 |
86 | toSVG = isBrowser
87 | ? (destSVG, svgOpts) => this._toSVG(sDOM, destSVG, svgOpts)
88 | : (destSVG, svgOpts) => this.toSVGTree(svgOpts)
89 |
90 | toCanvas = (destCanvas, _canvasOpts = {}) => {
91 | const defaultCanvasOptions = {
92 | scaling: isBrowser ? 'auto' : false,
93 | applyCssScaling: !!isBrowser
94 | }
95 | const canvasOpts = { ...defaultCanvasOptions, ..._canvasOpts }
96 | const { points, polys, opts } = this
97 |
98 | const canvas = destCanvas || createCanvas(opts.width, opts.height) // doc.createElement('canvas')
99 | const ctx = canvas.getContext('2d')
100 |
101 | if (canvasOpts.scaling) {
102 | const drawRatio = canvasOpts.scaling === 'auto'
103 | ? getScalingRatio(ctx)
104 | : canvasOpts.scaling
105 |
106 | if (drawRatio !== 1) {
107 | // set the 'real' canvas size to the higher width/height
108 | canvas.width = opts.width * drawRatio
109 | canvas.height = opts.height * drawRatio
110 |
111 | if (canvasOpts.applyCssScaling) {
112 | // ...then scale it back down with CSS
113 | canvas.style.width = opts.width + 'px'
114 | canvas.style.height = opts.height + 'px'
115 | }
116 | } else {
117 | // this is a normal 1:1 device: don't apply scaling
118 | canvas.width = opts.width
119 | canvas.height = opts.height
120 | if (canvasOpts.applyCssScaling) {
121 | canvas.style.width = ''
122 | canvas.style.height = ''
123 | }
124 | }
125 | ctx.scale(drawRatio, drawRatio)
126 | }
127 |
128 | const drawPoly = (poly, fill, stroke) => {
129 | const vertexIndices = poly.vertexIndices
130 | ctx.lineJoin = 'round'
131 | ctx.beginPath()
132 | ctx.moveTo(points[vertexIndices[0]][0], points[vertexIndices[0]][1])
133 | ctx.lineTo(points[vertexIndices[1]][0], points[vertexIndices[1]][1])
134 | ctx.lineTo(points[vertexIndices[2]][0], points[vertexIndices[2]][1])
135 | ctx.closePath()
136 | if (fill) {
137 | ctx.fillStyle = fill.color.css()
138 | ctx.fill()
139 | }
140 | if (stroke) {
141 | ctx.strokeStyle = stroke.color.css()
142 | ctx.lineWidth = stroke.width
143 | ctx.stroke()
144 | }
145 | }
146 |
147 | if (opts.fill && opts.strokeWidth < 1) {
148 | // draw background strokes at edge bounds to solve for white gaps due to
149 | // canvas antialiasing. See https://stackoverflow.com/q/19319963/381299
150 | polys.forEach(poly => drawPoly(poly, null, { color: poly.color, width: 2 }))
151 | }
152 |
153 | // draw visible fills and strokes
154 | polys.forEach(poly => drawPoly(
155 | poly,
156 | opts.fill && { color: poly.color },
157 | (opts.strokeWidth > 0) && { color: poly.color, width: opts.strokeWidth }
158 | ))
159 |
160 | return canvas
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/src/trianglify.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Trianglify.js
3 | * by @qrohlf
4 | *
5 | * Licensed under the GPLv3
6 | */
7 |
8 | import Delaunator from 'delaunator'
9 | // TODO - evaluate smaller alternatives
10 | // (chroma bloats bundle by 40k, minified)
11 | import chroma from 'chroma-js'
12 |
13 | import colorbrewer from './utils/colorbrewer'
14 | import Pattern from './pattern'
15 | import mulberry32 from './utils/mulberry32'
16 | import * as geom from './utils/geom'
17 | import * as colorFunctions from './utils/colorFunctions'
18 |
19 | const defaultOptions = {
20 | width: 600,
21 | height: 400,
22 | cellSize: 75,
23 | variance: 0.75,
24 | seed: null,
25 | xColors: 'random',
26 | yColors: 'match',
27 | palette: colorbrewer,
28 | colorSpace: 'lab',
29 | colorFunction: colorFunctions.interpolateLinear(0.5),
30 | fill: true,
31 | strokeWidth: 0,
32 | points: null
33 | }
34 |
35 | // This function does the "core" render-independent work:
36 | //
37 | // 1. Parse and munge options
38 | // 2. Setup cell geometry
39 | // 3. Generate random points within cell geometry
40 | // 4. Use the Delaunator library to run the triangulation
41 | // 5. Do color interpolation to establish the fundamental coloring of the shapes
42 | export default function trianglify (_opts = {}) {
43 | Object.keys(_opts).forEach(k => {
44 | if (defaultOptions[k] === undefined) {
45 | throw TypeError(`Unrecognized option: ${k}`)
46 | }
47 | })
48 | const opts = { ...defaultOptions, ..._opts }
49 |
50 | if (!(opts.height > 0)) {
51 | throw TypeError(`invalid height: ${opts.height}`)
52 | }
53 | if (!(opts.width > 0)) {
54 | throw TypeError(`invalid width: ${opts.width}`)
55 | }
56 |
57 | // standard randomizer, used for point gen and layout
58 | const rand = mulberry32(opts.seed)
59 |
60 | const randomFromPalette = () => {
61 | if (opts.palette instanceof Array) {
62 | return opts.palette[Math.floor(rand() * opts.palette.length)]
63 | }
64 | const keys = Object.keys(opts.palette)
65 | return opts.palette[keys[Math.floor(rand() * keys.length)]]
66 | }
67 |
68 | // The first step here is to set up our color scales for the X and Y axis.
69 | // First, munge the shortcut options like 'random' or 'match' into real color
70 | // arrays. Then, set up a Chroma scale in the appropriate color space.
71 | const processColorOpts = (colorOpt) => {
72 | switch (true) {
73 | case Array.isArray(colorOpt):
74 | return colorOpt
75 | case !!opts.palette[colorOpt]:
76 | return opts.palette[colorOpt]
77 | case colorOpt === 'random':
78 | return randomFromPalette()
79 | default:
80 | throw TypeError(`Unrecognized color option: ${colorOpt}`)
81 | }
82 | }
83 |
84 | const xColors = processColorOpts(opts.xColors)
85 | const yColors = opts.yColors === 'match'
86 | ? xColors
87 | : processColorOpts(opts.yColors)
88 |
89 | const xScale = chroma.scale(xColors).mode(opts.colorSpace)
90 | const yScale = chroma.scale(yColors).mode(opts.colorSpace)
91 |
92 | // Our next step is to generate a pseudo-random grid of {x, y} points,
93 | // (or to simply utilize the points that were passed to us)
94 | const points = opts.points || getPoints(opts, rand)
95 |
96 | // Once we have the points array, run the triangulation
97 | var geomIndices = Delaunator.from(points).triangles
98 | // ...and then generate geometry and color data:
99 |
100 | // use a different (salted) randomizer for the color function so that
101 | // swapping out color functions doesn't change the pattern geometry itself
102 | const salt = 42
103 | const cRand = mulberry32(opts.seed ? opts.seed + salt : null)
104 | const polys = []
105 |
106 | for (let i = 0; i < geomIndices.length; i += 3) {
107 | // convert shallow array-packed vertex indices into 3-tuples
108 | const vertexIndices = [
109 | geomIndices[i],
110 | geomIndices[i + 1],
111 | geomIndices[i + 2]
112 | ]
113 |
114 | // grab a copy of the actual vertices to use for calculations
115 | const vertices = vertexIndices.map(i => points[i])
116 |
117 | const { width, height } = opts
118 | const norm = num => Math.max(0, Math.min(1, num))
119 | const centroid = geom.getCentroid(vertices)
120 | const xPercent = norm(centroid.x / width)
121 | const yPercent = norm(centroid.y / height)
122 |
123 | const color = opts.colorFunction({
124 | centroid, // centroid of polygon, non-normalized
125 | xPercent, // x-coordinate of centroid, normalized to [0, 1]
126 | yPercent, // y-coordinate of centroid, normalized to [0, 1]
127 | vertexIndices, // vertex indices of the polygon
128 | vertices, // [x, y] vertices of the polygon
129 | xScale, // x-colors scale for the pattern
130 | yScale, // y-colors scale for the pattern
131 | points, // array of generated points for the pattern
132 | opts, // options used to initialize the pattern
133 | random: cRand // seeded randomization function for use by color functions
134 | })
135 |
136 | polys.push({
137 | vertexIndices,
138 | centroid,
139 | color // chroma color object
140 | })
141 | }
142 |
143 | return new Pattern(points, polys, opts)
144 | }
145 |
146 | const getPoints = (opts, random) => {
147 | const { width, height, cellSize, variance } = opts
148 |
149 | // pad by 2 cells outside the visible area on each side to ensure we fully
150 | // cover the 'artboard'
151 | const colCount = Math.floor(width / cellSize) + 4
152 | const rowCount = Math.floor(height / cellSize) + 4
153 |
154 | // determine bleed values to ensure that the grid is centered within the
155 | // artboard
156 | const bleedX = ((colCount * cellSize) - width) / 2
157 | const bleedY = ((rowCount * cellSize) - height) / 2
158 |
159 | // apply variance to cellSize to get cellJitter in pixels
160 | const cellJitter = cellSize * variance
161 | const getJitter = () => (random() - 0.5) * cellJitter
162 |
163 | const pointCount = colCount * rowCount
164 |
165 | const halfCell = cellSize / 2
166 |
167 | const points = Array(pointCount).fill(null).map((_, i) => {
168 | const col = i % colCount
169 | const row = Math.floor(i / colCount)
170 |
171 | // [x, y, z]
172 | return [
173 | -bleedX + col * cellSize + halfCell + getJitter(),
174 | -bleedY + row * cellSize + halfCell + getJitter()
175 | ]
176 | })
177 |
178 | return points
179 | }
180 |
181 | // tweak some of the exports here
182 | trianglify.utils = {
183 | mix: chroma.mix,
184 | colorbrewer
185 | }
186 |
187 | trianglify.colorFunctions = colorFunctions
188 | trianglify.Pattern = Pattern
189 | trianglify.defaultOptions = defaultOptions
190 |
--------------------------------------------------------------------------------
/src/trianglify.browser.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 | /* eslint-env jest */
5 | // Because Trianglify is authored using ES modules syntax (which Rollup likes)
6 | // it can't be unit-tested on a per-file basis using Jest without maintaining
7 | // a separate compiler configuration for test files.
8 | //
9 | // In the interest of sanity, this means that instead of unit-testing Trianglify,
10 | // I'm importing the built `dist` files and running a set of integration tests
11 | // against the public API only.
12 | //
13 | // I hope to start unit-testing in the future, when native support for ES modules
14 | // lands in Node and Jest (See https://github.com/facebook/jest/issues/9430)
15 |
16 | // pull in the transpiled, browser-bundle version of trianglify.
17 | // this is needed so that we get the browser-targeted Canvas shim, and
18 | // NOT the node library
19 | const trianglify = require('../dist/trianglify.bundle.debug.js')
20 | const Pattern = trianglify.Pattern
21 |
22 | describe('Public API', () => {
23 | test('default export should be a function', () => {
24 | expect(trianglify).toBeInstanceOf(Function)
25 | })
26 |
27 | test('should export the colorbrewer palette', () => {
28 | expect(trianglify.utils.colorbrewer).toBeDefined()
29 | expect(trianglify.utils.colorbrewer.YlGn).toBeDefined()
30 | })
31 |
32 | test('should export the mix utility', () => {
33 | expect(trianglify.utils.mix).toBeDefined()
34 | expect(trianglify.utils.mix).toBeInstanceOf(Function)
35 | })
36 |
37 | test('should export the color function generators', () => {
38 | expect(trianglify.colorFunctions.interpolateLinear).toBeDefined()
39 | expect(trianglify.colorFunctions.interpolateLinear).toBeInstanceOf(Function)
40 | expect(trianglify.colorFunctions.sparkle).toBeInstanceOf(Function)
41 | expect(trianglify.colorFunctions.shadows).toBeInstanceOf(Function)
42 | })
43 | })
44 |
45 | describe('Options Parsing', () => {
46 | test('should throw an error on unrecognized options', () => {
47 | expect(
48 | () => trianglify({ height: 100, width: 100, bad_option: true })
49 | ).toThrow()
50 | })
51 |
52 | test('should throw an error on invalid dimensions', () => {
53 | expect(
54 | () => trianglify({ height: 100, width: -1 })
55 | ).toThrow()
56 |
57 | expect(
58 | () => trianglify({ height: -1, width: 100 })
59 | ).toThrow()
60 | })
61 | })
62 |
63 | describe('Pattern generation', () => {
64 | test('return a Pattern given valid options', () => {
65 | expect(trianglify({ height: 100, width: 100 })).toBeInstanceOf(Pattern)
66 | })
67 |
68 | test('should use default options when invoked', () => {
69 | const pattern = trianglify()
70 | expect(pattern.opts).toEqual(trianglify.defaultOptions)
71 | })
72 |
73 | test('should override opts with user-provided options', () => {
74 | const pattern = trianglify({ height: 100, width: 100, cellSize: 1234 })
75 | expect(pattern.opts.cellSize).toEqual(1234)
76 | })
77 |
78 | test('should accept the random color option without erroring', () => {
79 | expect(() => {
80 | trianglify({ xColors: 'random' })
81 | trianglify({ yColors: 'random' })
82 | }).not.toThrow()
83 | })
84 |
85 | test('should accept the match color option without erroring', () => {
86 | expect(() => {
87 | trianglify({ xColors: 'random', yColors: 'match' })
88 | }).not.toThrow()
89 | })
90 |
91 | test('should accept a named colorbrewer palette without erroring', () => {
92 | expect(() => {
93 | trianglify({ xColors: 'RdBu' })
94 | trianglify({ yColors: 'OrRd' })
95 | }).not.toThrow()
96 | })
97 |
98 | test('should error on a names palette that does not exist', () => {
99 | expect(() => {
100 | trianglify({ xColors: 'Foo' })
101 | trianglify({ yColors: 'Bar' })
102 | }).toThrow()
103 | })
104 |
105 | test('should generate well-formed geometry', () => {
106 | const pattern = trianglify({ height: 100, width: 100, cellSize: 20 })
107 | // we care about pattern.points and pattern.polys here
108 | expect(pattern.points).toBeInstanceOf(Array)
109 | // assert that points is an array of [x, y] tuples
110 | pattern.points.forEach(point => {
111 | expect(point).toBeInstanceOf(Array)
112 | expect(point).toHaveLength(2)
113 | })
114 |
115 | // asset the polys looks right
116 | expect(pattern.polys).toBeInstanceOf(Array)
117 | pattern.polys.forEach(poly => {
118 | expect(poly).toBeInstanceOf(Object)
119 | expect(Object.keys(poly)).toEqual(['vertexIndices', 'centroid', 'color'])
120 | })
121 | })
122 |
123 | test('should be random by default', () => {
124 | const pattern1 = trianglify()
125 | const pattern2 = trianglify()
126 | expect(pattern1.toSVG()).not.toEqual(pattern2.toSVG())
127 | })
128 |
129 | test('should be deterministic when seeded', () => {
130 | const pattern1 = trianglify({ seed: 'deadbeef' })
131 | const pattern2 = trianglify({ seed: 'deadbeef' })
132 | expect(pattern1.toSVG()).toEqual(pattern2.toSVG())
133 | })
134 |
135 | test('should match snapshot for non-breaking version bumps', () => {
136 | expect(trianglify({ seed: 'snapshotText' }).toSVG()).toMatchSnapshot()
137 | })
138 | })
139 |
140 | describe('Pattern outputs in browser environment', () => {
141 | describe('#toSVG', () => {
142 | test('returns a well-formed SVG node', () => {
143 | const pattern = trianglify()
144 | const svgDOM = pattern.toSVG()
145 | expect(svgDOM.tagName).toEqual('svg')
146 | expect(svgDOM.children).toBeInstanceOf(global.HTMLCollection)
147 | Array.from(svgDOM.children).forEach(node => {
148 | expect(node.tagName).toEqual('path')
149 | })
150 | expect(svgDOM.children).toHaveLength(pattern.polys.length)
151 | })
152 |
153 | test('supports rendering to the destSVG target', () => {
154 | const destSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
155 | expect(destSVG.children).toHaveLength(0)
156 | const pattern = trianglify({ seed: 'destSVG works' })
157 | // side-effect-ful render to destSVG
158 | pattern.toSVG(destSVG)
159 | expect(destSVG.children).toHaveLength(pattern.polys.length)
160 | expect(destSVG).toMatchSnapshot()
161 | })
162 | })
163 |
164 | describe('#toSVGTree', () => {
165 | const pattern = trianglify({ seed: 'foobar' })
166 | const svgTree = pattern.toSVGTree()
167 |
168 | test('returns a synthetic tree of object literals', () => {
169 | expect(Object.keys(svgTree)).toEqual(['tagName', 'attrs', 'children', 'toString'])
170 | })
171 |
172 | test('serializes to an SVG string', () => {
173 | expect(svgTree.toString()).toMatchSnapshot()
174 | })
175 | })
176 |
177 | describe('#toCanvas', () => {
178 | test('returns a Canvas node', () => {
179 | const pattern = trianglify()
180 | const canvas = pattern.toCanvas()
181 | expect(canvas).toBeInstanceOf(global.HTMLElement)
182 | expect(canvas.tagName).toEqual('CANVAS')
183 | // there's not really any way to test the canvas contents here
184 | })
185 | })
186 | })
187 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # Trianglify
2 |
3 |
4 | Trianglify is a library that I wrote to generate nice SVG background images like this one:
5 |
6 | 
7 |
8 | # Contents
9 | [📦 Getting Trianglify](#-getting-trianglify)
10 | [🏎 Quickstart](#-quickstart)
11 | [⚖️ Licensing](#%EF%B8%8F-licensing)
12 | [📖 API](#-api)
13 | [🎨 Configuration](#-configuration)
14 |
15 | # 📦 Getting Trianglify
16 |
17 | You can grab Trianglify with npm/yarn (recommended):
18 |
19 | ```
20 | npm install --save trianglify
21 | ```
22 |
23 | Include it in your application via the unpkg CDN:
24 |
25 | ```
26 |
27 | ```
28 |
29 | Or download a .zip from the [**releases page**](https://github.com/qrohlf/trianglify/releases).
30 |
31 |
32 | # 🏎 Quickstart
33 |
34 | **Browsers**
35 | ```html
36 |
37 |
44 | ```
45 |
46 | **Node**
47 | ```js
48 | const trianglify = require('trianglify')
49 | const fs = require('fs')
50 |
51 | const canvas = trianglify({
52 | width: 1920,
53 | height: 1080
54 | }).toCanvas()
55 |
56 | const file = fs.createWriteStream('trianglify.png')
57 | canvas.createPNGStream().pipe(file)
58 | ```
59 |
60 | You can see the [`examples/`](./examples) folder for more usage examples.
61 |
62 | The https://trianglify.io/ GUI is a good place to play around with the various configuration parameters and see their effect on the generated output, live.
63 |
64 | # ⚖️ Licensing
65 |
66 | The source code of Trianglify is licensed under version 3 of the GNU General Public License ([GPLv3](https://www.gnu.org/licenses/gpl-3.0.html)). This means that any websites, apps, or other projects that include the Trianglify javascript library need to be released under a compatible open-source license. If you are interested in using Trianglify in a closed-source project, please email qr@qrohlf.com to purchase a commercial license.
67 |
68 | **However**, it's worth noting that you own the copyright to the output image files which you create using Trianglify, just like you own the copyright to an image created using something like [GIMP](https://www.gimp.org/). If you just want to use an image file that was generated using Trianglify in your project, and do not plan to distribute the Trianglify source code or compiled versions of it, you do not need to worry about the license restrictions described above.
69 |
70 |
71 | # 📖 API
72 |
73 | Trianglify is primarily used by calling the `trianglify` function, which returns a `trianglify.Pattern` object.
74 |
75 | ```js
76 | // load the library, either via a window global (browsers) or require call (node)
77 | // in es-module environments, you can `import trianglify from 'trianglify'` as well
78 | const trianglify = window.trianglify || require('trianglify')
79 |
80 | const options = { height: 400, width: 600 }
81 | const pattern = trianglify(options)
82 | console.log(pattern instanceof trianglify.Pattern) // true
83 | ```
84 |
85 | ## pattern
86 |
87 | This object holds the generated geometry and colors, and exposes a number of methods for rendering this geometry to the DOM or a Canvas.
88 |
89 |
90 | **`pattern.opts`**
91 |
92 | Object containing the options used to generate the pattern.
93 |
94 |
95 | **`pattern.points`**
96 |
97 | The pseudo-random point grid used for the pattern geometry, in the following format:
98 |
99 | ```js
100 | [
101 | [x, y],
102 | [x, y],
103 | [x, y],
104 | // and so on...
105 | ]
106 | ```
107 |
108 |
109 | **`pattern.polys`**
110 |
111 | The array of colored polygons that make up the pattern, in the following format:
112 |
113 | ```js
114 | // {x, y} center of the first polygon in the pattern
115 | pattern.polys[0].centroid
116 |
117 | // [i, i, i] three indexes into the pattern.points array,
118 | // defining the shape corners
119 | pattern.polys[0].vertexIndices
120 |
121 | // Chroma.js color object defining the color of the polygon
122 | pattern.polys[0].color
123 | ```
124 |
125 |
126 | **`pattern.toSVG(destSVG?, svgOpts?)`**
127 |
128 | Rendering function for SVG. In browser or browser-like (e.g. JSDOM) environments, this will return a SVGElement DOM node. In node environments, this will return a lightweight node tree structure that can be serialized to a valid SVG string using the `toString()` function.
129 |
130 | If an existing svg element is passed as the `destSVG`, this function will render the pattern to the pre-existing element instead of creating a new one.
131 |
132 | The `svgOpts` option allows for some svg-specific customizations to the output:
133 |
134 | ```js
135 | const svgOpts = {
136 | // Include or exclude the xmlns='http://www.w3.org/2000/svg' attribute on
137 | // the root