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 | 1×
1×
1×
47×
47×
1×
5×
5×
836×
836×
836×
1×
18×
18×
1152×
378×
18×
1×
21×
21×
21×
279×
464×
21×
1×
7×
1×
6×
6×
6×
6×
116×
34×
34×
34×
34×
76×
72×
72×
72×
72×
72×
4×
76×
76×
76×
76×
76×
76×
152×
152×
152×
152×
126×
126×
126×
126×
26×
26×
26×
26×
26×
26×
76×
76×
6×
1×
| BBox = require './BBox'
class Path
get = require('../get')(this)
constructor: ->
@commands = []
@_bbox = @_cbox = null
for command in ['moveTo', 'lineTo', 'quadraticCurveTo', 'bezierCurveTo', 'closePath']
do (command) ->
Path::[command] = (args...) ->
@_bbox = @_cbox = null
@commands.push
command: command
args: args
return this
# Compiles the path to a JavaScript function that can be applied with
# a graphics context in order to render the path.
toFunction: ->
cmds = []
for c in @commands
cmds.push " ctx.#{c.command}(#{c.args.join(', ')});"
return new Function 'ctx', cmds.join('\n')
SVG_COMMANDS =
moveTo: 'M'
lineTo: 'L'
quadraticCurveTo: 'Q'
bezierCurveTo: 'C'
closePath: 'Z'
# Converts the path to an SVG path data string
toSVG: ->
cmds = []
for c in @commands
args = (Math.round(arg * 100) / 100 for arg in c.args)
cmds.push "#{SVG_COMMANDS[c.command]}#{args.join(' ')}"
return cmds.join('')
# Gets the "control box" of a path.
# This is like the bounding box, but it includes all points including
# control points of bezier segments and is much faster to compute than
# the real bounding box.
get 'cbox', ->
Iif @_cbox
return @_cbox
cbox = new BBox
for command in @commands
for x, i in command.args by 2
cbox.addPoint x, command.args[i + 1]
@_cbox = Object.freeze cbox
# Gets the exact bounding box of the path by evaluating curve segments.
# Slower to compute than the control box, but more accurate.
get 'bbox', ->
if @_bbox
return @_bbox
bbox = new BBox
cx = cy = 0
f = (t) ->
Math.pow(1-t, 3) * p0[i] +
3 * Math.pow(1-t, 2) * t * p1[i] +
3 * (1-t) * Math.pow(t, 2) * p2[i] +
Math.pow(t, 3) * p3[i]
for c in @commands
switch c.command
when 'moveTo', 'lineTo'
[x, y] = c.args
bbox.addPoint x, y
cx = x
cy = y
when 'quadraticCurveTo', 'bezierCurveTo'
if c.command is 'quadraticCurveTo'
# http://fontforge.org/bezier.html
[qp1x, qp1y, p3x, p3y] = c.args
cp1x = cx + 2 / 3 * (qp1x - cx) # CP1 = QP0 + 2/3 * (QP1-QP0)
cp1y = cy + 2 / 3 * (qp1y - cy)
cp2x = p3x + 2 / 3 * (qp1x - p3x) # CP2 = QP2 + 2/3 * (QP1-QP2)
cp2y = p3y + 2 / 3 * (qp1y - p3y)
else
[cp1x, cp1y, cp2x, cp2y, p3x, p3y] = c.args
# http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html
bbox.addPoint p3x, p3y
p0 = [cx, cy]
p1 = [cp1x, cp1y]
p2 = [cp2x, cp2y]
p3 = [p3x, p3y]
for i in [0..1]
b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i]
a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i]
c = 3 * p1[i] - 3 * p0[i]
if a is 0
Icontinue if b is 0
t = -c / b
Iif 0 < t and t < 1
bbox.addPoint f(t), bbox.maxY if i is 0
bbox.addPoint bbox.maxX, f(t) if i is 1
continue
b2ac = Math.pow(b, 2) - 4 * c * a
Icontinue if b2ac < 0
t1 = (-b + Math.sqrt(b2ac)) / (2 * a)
Iif 0 < t1 and t1 < 1
bbox.addPoint f(t1), bbox.maxY if i is 0
bbox.addPoint bbox.maxX, f(t1) if i is 1
t2 = (-b - Math.sqrt(b2ac)) / (2 * a)
Iif 0 < t2 and t2 < 1
bbox.addPoint f(t2), bbox.maxY if i is 0
bbox.addPoint bbox.maxX, f(t2) if i is 1
cx = p3x
cy = p3y
@_bbox = Object.freeze bbox
module.exports = Path
|