-
Notifications
You must be signed in to change notification settings - Fork 662
Expand file tree
/
Copy pathpattern.js
More file actions
162 lines (142 loc) · 5.39 KB
/
pattern.js
File metadata and controls
162 lines (142 loc) · 5.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import { createCanvas } from 'canvas' // this is a simple shim in browsers
import getScalingRatio from './utils/getScalingRatio'
const isBrowser = (typeof window !== 'undefined' && typeof document !== 'undefined')
const doc = isBrowser && document
// utility for building up SVG node trees with the DOM API
const sDOM = (tagName, attrs = {}, children, existingRoot) => {
const elem = existingRoot || doc.createElementNS('http://www.w3.org/2000/svg', tagName)
Object.keys(attrs).forEach(
k => attrs[k] !== undefined && elem.setAttribute(k, attrs[k])
)
children && children.forEach(c => elem.appendChild(c))
return elem
}
// serialize attrs object to XML attributes. Assumes everything is already
// escaped (safe input).
const serializeAttrs = attrs => (
Object.entries(attrs)
.filter(([_, v]) => v !== undefined)
.map(([k, v]) => `${k}='${v}'`)
.join(' ')
)
// minimal XML-tree builder for use in Node
const sNode = (tagName, attrs = {}, children) => ({
tagName,
attrs,
children,
toString: () => `<${tagName} ${serializeAttrs(attrs)}>${children ? children.join('') : ''}</${tagName}>`
})
export default class Pattern {
constructor (points, polys, opts) {
this.points = points
this.polys = polys
this.opts = opts
}
_toSVG = (serializer, destSVG, _svgOpts = {}) => {
const s = serializer
const defaultSVGOptions = { includeNamespace: true, coordinateDecimals: 1 }
const svgOpts = { ...defaultSVGOptions, ..._svgOpts }
const { points, opts, polys } = this
const { width, height } = opts
// only round points if the coordinateDecimals option is non-negative
// set coordinateDecimals to -1 to disable point rounding
const roundedPoints = (svgOpts.coordinateDecimals < 0) ? points : points.map(
p => p.map(x => +x.toFixed(svgOpts.coordinateDecimals))
)
const paths = polys.map((poly) => {
const xys = poly.vertexIndices.map(i => `${roundedPoints[i][0]},${roundedPoints[i][1]}`)
const d = 'M' + xys.join('L') + 'Z'
const hasStroke = opts.strokeWidth > 0
// shape-rendering crispEdges resolves the antialiasing issues, at the
// potential cost of some visual degradation. For the best performance
// *and* best visual rendering, use Canvas.
return s('path', {
d,
fill: opts.fill ? poly.color.css() : undefined,
stroke: hasStroke ? opts.strokeColor || optspoly.color.css() : undefined,
'stroke-width': hasStroke ? opts.strokeWidth : undefined,
'stroke-linejoin': hasStroke ? 'round' : undefined,
'shape-rendering': opts.fill ? 'crispEdges' : undefined
})
})
const svg = s(
'svg',
{
xmlns: svgOpts.includeNamespace ? 'http://www.w3.org/2000/svg' : undefined,
width,
height
},
paths,
destSVG
)
return svg
}
toSVGTree = (svgOpts) => this._toSVG(sNode, null, svgOpts)
toSVG = isBrowser
? (destSVG, svgOpts) => this._toSVG(sDOM, destSVG, svgOpts)
: (destSVG, svgOpts) => this.toSVGTree(svgOpts)
toCanvas = (destCanvas, _canvasOpts = {}) => {
const defaultCanvasOptions = {
scaling: isBrowser ? 'auto' : false,
applyCssScaling: !!isBrowser
}
const canvasOpts = { ...defaultCanvasOptions, ..._canvasOpts }
const { points, polys, opts } = this
const canvas = destCanvas || createCanvas(opts.width, opts.height) // doc.createElement('canvas')
const ctx = canvas.getContext('2d')
if (canvasOpts.scaling) {
const drawRatio = canvasOpts.scaling === 'auto'
? getScalingRatio(ctx)
: canvasOpts.scaling
if (drawRatio !== 1) {
// set the 'real' canvas size to the higher width/height
canvas.width = opts.width * drawRatio
canvas.height = opts.height * drawRatio
if (canvasOpts.applyCssScaling) {
// ...then scale it back down with CSS
canvas.style.width = opts.width + 'px'
canvas.style.height = opts.height + 'px'
}
} else {
// this is a normal 1:1 device: don't apply scaling
canvas.width = opts.width
canvas.height = opts.height
if (canvasOpts.applyCssScaling) {
canvas.style.width = ''
canvas.style.height = ''
}
}
ctx.scale(drawRatio, drawRatio)
}
const drawPoly = (poly, fill, stroke) => {
const vertexIndices = poly.vertexIndices
ctx.lineJoin = 'round'
ctx.beginPath()
ctx.moveTo(points[vertexIndices[0]][0], points[vertexIndices[0]][1])
ctx.lineTo(points[vertexIndices[1]][0], points[vertexIndices[1]][1])
ctx.lineTo(points[vertexIndices[2]][0], points[vertexIndices[2]][1])
ctx.closePath()
if (fill) {
ctx.fillStyle = fill.color.css()
ctx.fill()
}
if (stroke) {
ctx.strokeStyle = stroke.color.css()
ctx.lineWidth = stroke.width
ctx.stroke()
}
}
if (opts.fill && opts.strokeWidth < 1) {
// draw background strokes at edge bounds to solve for white gaps due to
// canvas antialiasing. See https://stackoverflow.com/q/19319963/381299
polys.forEach(poly => drawPoly(poly, null, { color: poly.color, width: 2 }))
}
// draw visible fills and strokes
polys.forEach(poly => drawPoly(
poly,
opts.fill && { color: poly.color },
(opts.strokeWidth > 0) && { color: opts.strokeColor || poly.color, width: opts.strokeWidth }
))
return canvas
}
}