├── README.md
├── images
├── offset-1.svg
├── offset-2.svg
├── offset-3.svg
├── offset-4.svg
├── offset0x.png
├── offset10x.png
├── offset11x.png
├── offset12x.png
├── offset13x.png
├── offset14x.png
├── offset15x.png
├── offset16x.png
├── offset17x.png
├── offset18x.png
├── offset1x.png
├── offset2x.png
├── offset3x.png
├── offset4x.png
├── offset5x.png
├── offset6x.png
├── offset7x.png
├── offset8x.png
└── offset9x.png
├── offset.pdf
├── offset.py
├── offset.tex
└── test.py
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Offsetting parameterised Bezier curves
4 | ### Simon Cozens
5 |
6 | A common problem in type design is the creation of pairs of curves representing the stroke of a pen: an inner curve and an outer curve delimit the contours of a writing implement of fixed or flexible thickness. While it is impossible to precisely offset a Bezier curve at a given width, this paper presents a simple approximation by minimizing the error between desired distance and actual distance. This can also be applied to situations where the thickness varies linearly across the width of the
7 | curve.
8 |
9 | We use a simplification due to Tunni, who postulates that any curve **a**, **b**, **c**, **d** with straight handles (i.e. where the control points **b**and **c** are positioned orthogonally to **a** and **d** respectively) can be represented in terms of start and end points **a**and **d** and a curve tension *τ*. To determine curve tension, compute the point **T** where **ab** intersects **cd**:
10 |
11 |
12 |
13 | The curve tension is given by the mean of the ratios and . Given the points **a**, **d** and a tension τ we can compute the Bezier control points **b**, **c** by setting them at the appropriate
14 | ratios.
15 |
16 | This conceptualization enables us to find similar parameters for an offset Bezier curve. We will approach the problem in small pieces, demonstrating the technique first before solving the general case.
17 |
18 | ## Outer offset of a unit Bezier curve
19 |
20 | Consider first the unit Bezier curve **B****A** with **a** = (0,1), **d** = (1,0) and **c** and **d** chosen as orthogonal control
21 | points with a curve tension *α*. What are the parameters for a Bezier curve **B****B** offsetting this curve on the
22 | outside at a fixed distance *δ*?
23 |
24 |
25 |
26 | Clearly we have ***a** = (0, 1 + *δ*), **d** = (1 + *δ*, 0)*, so it remains to find the curve tension *β*.
27 |
28 | As a function of time, the distance between the two curves is:
29 |
30 |
33 |
34 |
[1]
37 |
38 | and at any point on the curve, the expected distance is *δ*. Knowing it is impossible to achieve a perfect
39 | offset, we can treat this as an optimization problem: find the value of *β* which minimizes the total error function
40 |
41 |
44 |
45 |
[2]
50 |
51 | This integral turns out to be tricky to compute due to the presence of the square root, so instead we create an equivalent error function using the square of the distance. We expect the square of the distance to be *δ*2, and
52 | we square the difference of these two values to perform a least squares optimization. This leads to an error
53 | function of
54 |
55 |
58 |
59 |
[3]
63 |
64 | For a unit Bezier, we have:
65 |
66 |
[4]
75 |
76 | leading to a square distance
77 |
78 |
[5]
83 | and therefore an error function
84 |
[6]
92 |
93 | This looks horrific, but it’s only a quartic, and is easily optimizable. Rather than solving the differential equation for the general case, let’s be practical, remember that *α* and *δ* will be given and go for a numerical method to minimize the error function.
94 |
95 | Beginning with *β*1 = *α* and applying the Newton-Raphson optimization method gives us an iterated
96 | function
97 |
98 |
[7]
114 |
115 | which quickly converges to the minimum error, giving us the optimal curve tension.
116 |
117 | As an example, plugging in *α = 0.55, δ = 1*:
118 |
119 | *β*1 = 0.55
120 |
121 | *β*2 = 0.550987
122 |
123 | *β*3 = 0.550985
124 |
125 | *β*4 = 0.550985
126 |
127 | ## We can cheat
128 |
129 | Thankfully, we find by inspection that the optimal value of *β* given *α* and *δ*, *β(α, δ)* turns out to be pretty much linear in both *α* and *δ* when *α >= 0.3*. A very pleasing result is:
130 |
131 |
134 |
135 |
136 |
137 |
[8]
141 |
142 |
143 | Note that this gives exactly the answer given by our Newton-Raphson method above. A more general, but less accurate, approximation is:
144 |
145 |
147 |
148 |
[9]
151 |
152 | ## Inner offsetting of a unit Bezier
153 |
154 | What if we want to go the other way, and find the inner curve at a fixed distance?
155 |
156 |
157 |
158 | A very similar pattern applies, but this time we construct ***B****B*** as ***a** = (0, 1-δ), **d** = (1-δ, 0)* and the Newton step is
161 |
[10]
170 |
171 |
172 | Equally, we can invert our approximation 9 above, giving:
174 |
177 |
178 |
[11]
181 |
182 | ## Outer offsetting of an arbitrary normalized curve
183 |
184 | Real-world curves are not unit curves (0,1)⋅⋅⋅(1, 0). However, we can always use affine transformation to locate
185 | the start at **a** = (1, 0), leaving the end at **d** = (0,*x*). The control points for a Bezier curve with tension *τ* **B****A**
186 |
187 | would then be set at ***b** = (1-τ,0), **c** = (0,x(1-τ))*. The problem, again, is to find the offset curve **B****B** which
188 | best approximates a fixed distance *δ* from **B****A**.
189 |
190 | Now we have ***a** = (0, 1 + *δ*), **d** = (x + δ, 0)*. Following exactly the procedure above, the square
191 | of the distance between the two curves at point *t* , is
192 |
193 |
[12]
196 |
197 | and the total error across the curve is
198 |
199 |
[13]
208 |
209 | Once again, it’s only a quartic and three of the variables are given. We can apply the Newton-Raphson
210 | method again, giving:
211 |
212 |
[14]
229 |
230 | By iterating this approximation, we can derive the tension for a curve at an offset of a given distance *δ* from
231 | an arbitrary Bezier curve specified by two points and a curve tension parameter.
232 |
233 | But wait, it gets more complicated.
234 |
235 | ## Offsetting at a linear-gradiated distance
236 |
237 | Strokes in fonts often have a feature called *contrast*, meaning that the horizontal offset is not the same as the
238 | vertical offset:
239 |
240 |
241 |
242 | To model this we will assume that the desired distance between curves is a linear function of curve time *t* :
243 |
244 |
247 |
248 |
[15]
251 |
252 |
253 | And now our error function is
254 |
255 |
258 |
259 |
[16]
263 |
264 | The total integrated error across the curve becomes... very complicated, but computable. We can apply a similar Newton-Raphson method as above, leading to the functions given in the associated Python script.
265 |
--------------------------------------------------------------------------------
/images/offset-1.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
206 |
--------------------------------------------------------------------------------
/images/offset-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
106 |
--------------------------------------------------------------------------------
/images/offset-3.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
106 |
--------------------------------------------------------------------------------
/images/offset-4.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
36 |
--------------------------------------------------------------------------------
/images/offset0x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/images/offset0x.png
--------------------------------------------------------------------------------
/images/offset10x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/images/offset10x.png
--------------------------------------------------------------------------------
/images/offset11x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/images/offset11x.png
--------------------------------------------------------------------------------
/images/offset12x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/images/offset12x.png
--------------------------------------------------------------------------------
/images/offset13x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/images/offset13x.png
--------------------------------------------------------------------------------
/images/offset14x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/images/offset14x.png
--------------------------------------------------------------------------------
/images/offset15x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/images/offset15x.png
--------------------------------------------------------------------------------
/images/offset16x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/images/offset16x.png
--------------------------------------------------------------------------------
/images/offset17x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/images/offset17x.png
--------------------------------------------------------------------------------
/images/offset18x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/images/offset18x.png
--------------------------------------------------------------------------------
/images/offset1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/images/offset1x.png
--------------------------------------------------------------------------------
/images/offset2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/images/offset2x.png
--------------------------------------------------------------------------------
/images/offset3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/images/offset3x.png
--------------------------------------------------------------------------------
/images/offset4x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/images/offset4x.png
--------------------------------------------------------------------------------
/images/offset5x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/images/offset5x.png
--------------------------------------------------------------------------------
/images/offset6x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/images/offset6x.png
--------------------------------------------------------------------------------
/images/offset7x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/images/offset7x.png
--------------------------------------------------------------------------------
/images/offset8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/images/offset8x.png
--------------------------------------------------------------------------------
/images/offset9x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/images/offset9x.png
--------------------------------------------------------------------------------
/offset.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simoncozens/curve-offsetting/b3afdd2e8008a502b32357197471ffab7c8c2f9b/offset.pdf
--------------------------------------------------------------------------------
/offset.py:
--------------------------------------------------------------------------------
1 | # "Offsetting parameterised Bezier curves"
2 |
3 | from math import sin,cos,radians,atan2,pi
4 |
5 | # Preparatory stuff: Affine transforms
6 | def rotate(pt,angle):
7 | return [cos(angle)*pt[0] - sin(angle)*pt[1],sin(angle)*pt[0]+cos(angle)*pt[1]]
8 |
9 | def unitize(a,b,c,d):
10 | rotationAngle = atan2(b[1]-a[1],b[0]-a[0])
11 | a = rotate(a, rotationAngle)
12 | b = rotate(b, rotationAngle)
13 | c = rotate(c, rotationAngle)
14 | d = rotate(d, rotationAngle)
15 | return { "rotate": rotationAngle, "dx": -a[0], "dy": -d[1], "scale": float(a[1]-d[1])}
16 |
17 | def apply1(a, transform):
18 | a = rotate(a,transform["rotate"])
19 | return [ (a[0]+transform["dx"]) / transform["scale"],
20 | (a[1]+transform["dy"]) / transform["scale"] ]
21 |
22 | def unapply1(a, transform):
23 | a = rotate(a,-transform["rotate"])
24 | return [ (a[0]*transform["scale"])-transform["dx"],
25 | (a[1]*transform["scale"])-transform["dy"] ]
26 |
27 | def applyTransform(bez, transform):
28 | return map(lambda (z): apply1(z,transform), bez)
29 |
30 | def applyInvertedTransform(bez, transform):
31 | return map(lambda (z): unapply1(z,transform), bez)
32 |
33 | # Preparatory stuff: Determining curve tension
34 | # (Mostly stolen with modification from mekkablue)
35 |
36 | # We could simplify this dramatically given that we're expecting
37 | # orthogonality, but hey, generalize first and things are free later.
38 | def lineLineIntersection(a,b,c,d):
39 | xdiff = (a[0] - b[0], c[0] - d[0])
40 | ydiff = (a[1] - b[1], c[1] - d[1])
41 |
42 | def det(a, b):
43 | return a[0] * b[1] - a[1] * b[0]
44 |
45 | div = det(xdiff, ydiff)
46 | if div == 0:
47 | raise Exception('lines do not intersect')
48 |
49 | d = (det(a,b), det(c,d))
50 | x = det(d, xdiff) / float(div)
51 | y = det(d, ydiff) / float(div)
52 | return [x, y]
53 |
54 | def pointDistance( a, b):
55 | return ( (float(b[0]) - float(a[0]))**2 + (float(b[1]) - float(a[1]))**2 ) ** 0.5
56 |
57 | def lerp(t,a,b):
58 | return [int((1-t)*a[0] + t*b[0]), int((1-t)*a[1] + t*b[1])]
59 |
60 | def normalizedTunniPoint(a,b, sAngle=None, eAngle=None):
61 | if sAngle == None:
62 | return [max(a[0],b[0]),max(a[1],b[1])]
63 |
64 | # An offset curve which starts to the north of the original
65 | # will have its first BCP east of point 1
66 | sHandleAngle = sAngle - pi/2.0
67 | # An offset curve which ends to the east of the original
68 | # will have its second BCP north of point 1
69 | eHandleAngle = eAngle + pi/2.0
70 |
71 | aHandle = [ a[0] + 1 * cos(sHandleAngle), a[1] + 1 * sin(sHandleAngle) ]
72 | bHandle = [ b[0] + 1 * cos(eHandleAngle), b[1] + 1 * sin(eHandleAngle) ]
73 | print(a,b)
74 | print(aHandle, bHandle)
75 | return lineLineIntersection(a, aHandle, bHandle, b)
76 |
77 | def tension(bez):
78 | tunniP = lineLineIntersection( *bez )
79 | percentageP1P2 = pointDistance( bez[0], bez[1] ) / pointDistance( bez[0], tunniP )
80 | percentageP3P4 = pointDistance( bez[3], bez[2] ) / pointDistance( bez[3], tunniP )
81 | return ( percentageP1P2 + percentageP3P4 ) / 2
82 |
83 | def curveWithTension(start, end, tension,sAngle, eAngle):
84 | return [start,
85 | lerp(tension, start, normalizedTunniPoint(start, end, sAngle, eAngle)),
86 | lerp(tension, end, normalizedTunniPoint(start, end, sAngle, eAngle)),
87 | end]
88 |
89 | # Glyphs stuff
90 | # def arrayToGSPath(bez):
91 | # p = GSPath.new()
92 | # nodes = map(lambda f: GSNode(NSMakePoint(f[0],f[1]),OFFCURVE), bez)
93 | # nodes[0].type = LINE
94 | # nodes[3].type = CURVE
95 | # p.nodes = nodes
96 | # p.closed = False
97 | # return p
98 |
99 | # def segToArray(s):
100 | # return map(lambda f: [f.x,f.y], s)
101 |
102 | # Now the offsetting code
103 |
104 | def findBeta(x,a,s,e,dStart,dEnd):
105 | i = 0
106 | b = a
107 | x1,y1 = s[0],s[1]
108 | x2,y2 = e[0],e[1]
109 | while i < 5:
110 | i = i + 1
111 | newtonstep = ((2867*x*x1)/30030 + (270*a*x*x1)/1001 + (207*a**2*x*x1)/1430 + (27*a**3*x*x1)/1001 - (5*dEnd**2*x*x1)/42 - (2*a*dEnd**2*x*x1)/35 - (17*dEnd*dStart*x*x1)/105 - (a*dEnd*dStart*x*x1)/7 - (19*dStart**2*x*x1)/210 - (a*dStart**2*x*x1)/7 + (191*x**3*x1)/1430 + (804*a*x**3*x1)/5005 + (243*a**2*x**3*x1)/2002 + (36*a**3*x**3*x1)/715 - (262*x1**2)/715 - (111*a*x1**2)/715 - (75*a**2*x1**2)/2002 + (24*b*x1**2)/143 + (69*a*b*x1**2)/715 + (27*a**2*b*x1**2)/1001 + (17*dEnd**2*x1**2)/210 - (2*b*dEnd**2*x1**2)/35 + (5*dEnd*dStart*x1**2)/21 - (b*dEnd*dStart*x1**2)/7 + (13*dStart**2*x1**2)/42 - (b*dStart**2*x1**2)/7 - (1069*x**2*x1**2)/5005 - (1824*a*x**2*x1**2)/5005 - (189*a**2*x**2*x1**2)/715 + (804*b*x**2*x1**2)/5005 + (243*a*b*x**2*x1**2)/1001 + (108*a**2*b*x**2*x1**2)/715 + (2867*x*x1**3)/10010 + (72*a*x*x1**3)/143 - (1824*b*x*x1**3)/5005 - (378*a*b*x*x1**3)/715 + (243*b**2*x*x1**3)/2002 + (108*a*b**2*x*x1**3)/715 - (262*x1**4)/715 + (72*b*x1**4)/143 - (189*b**2*x1**4)/715 + (36*b**3*x1**4)/715 - (2867*x*x2)/30030 - (270*a*x*x2)/1001 - (207*a**2*x*x2)/1430 - (27*a**3*x*x2)/1001 + (5*dEnd**2*x*x2)/42 + (2*a*dEnd**2*x*x2)/35 + (17*dEnd*dStart*x*x2)/105 + (a*dEnd*dStart*x*x2)/7 + (19*dStart**2*x*x2)/210 + (a*dStart**2*x*x2)/7 - (191*x**3*x2)/1430 - (804*a*x**3*x2)/5005 - (243*a**2*x**3*x2)/2002 - (36*a**3*x**3*x2)/715 + (8137*x1*x2)/30030 + (267*a*x1*x2)/5005 - (54*a**2*x1*x2)/5005 - (48*b*x1*x2)/143 - (138*a*b*x1*x2)/715 - (54*a**2*b*x1*x2)/1001 + (4*dEnd**2*x1*x2)/105 + (4*b*dEnd**2*x1*x2)/35 - (8*dEnd*dStart*x1*x2)/105 + (2*b*dEnd*dStart*x1*x2)/7 - (23*dStart**2*x1*x2)/105 + (2*b*dStart**2*x1*x2)/7 - (1873*x**2*x1*x2)/10010 + (216*a*x**2*x1*x2)/5005 + (1431*a**2*x**2*x1*x2)/10010 - (1608*b*x**2*x1*x2)/5005 - (486*a*b*x**2*x1*x2)/1001 - (216*a**2*b*x**2*x1*x2)/715 + (1409*x*x1**2*x2)/10010 - (696*a*x*x1**2*x2)/5005 + (408*b*x*x1**2*x2)/1001 + (4077*a*b*x*x1**2*x2)/5005 - (729*b**2*x*x1**2*x2)/2002 - (324*a*b**2*x*x1**2*x2)/715 + (801*x1**3*x2)/10010 - (3216*b*x1**3*x2)/5005 + (6723*b**2*x1**3*x2)/10010 - (144*b**3*x1**3*x2)/715 + (2867*x2**2)/30030 + (102*a*x2**2)/1001 + (69*a**2*x2**2)/1430 + (24*b*x2**2)/143 + (69*a*b*x2**2)/715 + (27*a**2*b*x2**2)/1001 - (5*dEnd**2*x2**2)/42 - (2*b*dEnd**2*x2**2)/35 - (17*dEnd*dStart*x2**2)/105 - (b*dEnd*dStart*x2**2)/7 - (19*dStart**2*x2**2)/210 - (b*dStart**2*x2**2)/7 + (573*x**2*x2**2)/1430 + (1608*a*x**2*x2**2)/5005 + (243*a**2*x**2*x2**2)/2002 + (804*b*x**2*x2**2)/5005 + (243*a*b*x**2*x2**2)/1001 + (108*a**2*b*x**2*x2**2)/715 - (53*x*x1*x2**2)/2002 - (204*a*x*x1*x2**2)/1001 + (1392*b*x*x1*x2**2)/5005 - (216*a*b*x*x1*x2**2)/5005 + (729*b**2*x*x1*x2**2)/2002 + (324*a*b**2*x*x1*x2**2)/715 + (729*x1**2*x2**2)/10010 - (324*b*x1**2*x2**2)/5005 - (4293*b**2*x1**2*x2**2)/10010 + (216*b**3*x1**2*x2**2)/715 - (573*x*x2**3)/1430 - (804*a*x*x2**3)/5005 - (1608*b*x*x2**3)/5005 - (243*a*b*x*x2**3)/1001 - (243*b**2*x*x2**3)/2002 - (108*a*b**2*x*x2**3)/715 + (801*x1*x2**3)/10010 + (216*b*x1*x2**3)/5005 - (999*b**2*x1*x2**3)/10010 - (144*b**3*x1*x2**3)/715 + (191*x2**4)/1430 + (804*b*x2**4)/5005 + (243*b**2*x2**4)/2002 + (36*b**3*x2**4)/715 - (191*y1)/1430 - (804*a*y1)/5005 - (243*a**2*y1)/2002 - (36*a**3*y1)/715 + (19*dEnd**2*y1)/210 + (a*dEnd**2*y1)/7 + (17*dEnd*dStart*y1)/105 + (a*dEnd*dStart*y1)/7 + (5*dStart**2*y1)/42 + (2*a*dStart**2*y1)/35 - (2867*x**2*y1)/30030 - (270*a*x**2*y1)/1001 - (207*a**2*x**2*y1)/1430 - (27*a**3*x**2*y1)/1001 - (243*x*x1*y1)/5005 - (23*a*x*x1*y1)/143 - (108*a**2*x*x1*y1)/5005 - (204*b*x*x1*y1)/1001 - (276*a*b*x*x1*y1)/715 - (108*a**2*b*x*x1*y1)/1001 + (857*x1**2*y1)/1430 + (509*a*x1**2*y1)/5005 - (18*b*x1**2*y1)/715 + (267*a*b*x1**2*y1)/5005 - (207*b**2*x1**2*y1)/1430 - (81*a*b**2*x1**2*y1)/1001 + (5734*x*x2*y1)/15015 + (876*a*x*x2*y1)/1001 + (138*a**2*x*x2*y1)/715 + (204*b*x*x2*y1)/1001 + (276*a*b*x*x2*y1)/715 + (108*a**2*b*x*x2*y1)/1001 - (685*x1*x2*y1)/1001 - (25*a*x1*x2*y1)/143 + (2826*b*x1*x2*y1)/5005 + (1182*a*b*x1*x2*y1)/5005 + (207*b**2*x1*x2*y1)/715 + (162*a*b**2*x1*x2*y1)/1001 - (2867*x2**2*y1)/10010 - (270*a*x2**2*y1)/1001 - (540*b*x2**2*y1)/1001 - (207*a*b*x2**2*y1)/715 - (207*b**2*x2**2*y1)/1430 - (81*a*b**2*x2**2*y1)/1001 + (573*y1**2)/1430 + (1608*a*y1**2)/5005 + (243*a**2*y1**2)/2002 + (804*b*y1**2)/5005 + (243*a*b*y1**2)/1001 + (108*a**2*b*y1**2)/715 - (19*dEnd**2*y1**2)/210 - (b*dEnd**2*y1**2)/7 - (17*dEnd*dStart*y1**2)/105 - (b*dEnd*dStart*y1**2)/7 - (5*dStart**2*y1**2)/42 - (2*b*dStart**2*y1**2)/35 + (2867*x**2*y1**2)/30030 + (102*a*x**2*y1**2)/1001 + (69*a**2*x**2*y1**2)/1430 + (24*b*x**2*y1**2)/143 + (69*a*b*x**2*y1**2)/715 + (27*a**2*b*x**2*y1**2)/1001 - (1409*x*x1*y1**2)/30030 + (9*a*x*x1*y1**2)/715 + (412*b*x*x1*y1**2)/5005 + (591*a*b*x*x1*y1**2)/5005 + (207*b**2*x*x1*y1**2)/1430 + (81*a*b**2*x*x1*y1**2)/1001 - (333*x1**2*y1**2)/1430 - (446*b*x1**2*y1**2)/5005 + (162*b**2*x1**2*y1**2)/5005 + (54*b**3*x1**2*y1**2)/1001 - (2867*x*x2*y1**2)/10010 - (270*a*x*x2*y1**2)/1001 - (540*b*x*x2*y1**2)/1001 - (207*a*b*x*x2*y1**2)/715 - (207*b**2*x*x2*y1**2)/1430 - (81*a*b**2*x*x2*y1**2)/1001 + (12413*x1*x2*y1**2)/30030 - (538*b*x1*x2*y1**2)/5005 - (1611*b**2*x1*x2*y1**2)/5005 - (108*b**3*x1*x2*y1**2)/1001 + (2867*x2**2*y1**2)/15015 + (540*b*x2**2*y1**2)/1001 + (207*b**2*x2**2*y1**2)/715 + (54*b**3*x2**2*y1**2)/1001 - (573*y1**3)/1430 - (804*a*y1**3)/5005 - (1608*b*y1**3)/5005 - (243*a*b*y1**3)/1001 - (243*b**2*y1**3)/2002 - (108*a*b**2*y1**3)/715 + (191*y1**4)/1430 + (804*b*y1**4)/5005 + (243*b**2*y1**4)/2002 + (36*b**3*y1**4)/715 + (191*y2)/1430 + (804*a*y2)/5005 + (243*a**2*y2)/2002 + (36*a**3*y2)/715 - (19*dEnd**2*y2)/210 - (a*dEnd**2*y2)/7 - (17*dEnd*dStart*y2)/105 - (a*dEnd*dStart*y2)/7 - (5*dStart**2*y2)/42 - (2*a*dStart**2*y2)/35 + (2867*x**2*y2)/30030 + (270*a*x**2*y2)/1001 + (207*a**2*x**2*y2)/1430 + (27*a**3*x**2*y2)/1001 - (4276*x*x1*y2)/15015 - (554*a*x*x1*y2)/1001 - (150*a**2*x*x1*y2)/1001 + (204*b*x*x1*y2)/1001 + (276*a*b*x*x1*y2)/715 + (108*a**2*b*x*x1*y2)/1001 + (1949*x1**2*y2)/6006 + (778*a*x1**2*y2)/5005 - (2162*b*x1**2*y2)/5005 - (225*a*b*x1**2*y2)/1001 + (207*b**2*x1**2*y2)/1430 + (81*a*b**2*x1**2*y2)/1001 - (243*x*x2*y2)/5005 - (23*a*x*x2*y2)/143 - (108*a**2*x*x2*y2)/5005 - (204*b*x*x2*y2)/1001 - (276*a*b*x*x2*y2)/715 - (108*a**2*b*x*x2*y2)/1001 + (1409*x1*x2*y2)/15015 + (25*a*x1*x2*y2)/143 + (50*b*x1*x2*y2)/143 + (534*a*b*x1*x2*y2)/5005 - (207*b**2*x1*x2*y2)/715 - (162*a*b**2*x1*x2*y2)/1001 - (1409*x2**2*y2)/30030 + (9*a*x2**2*y2)/715 + (412*b*x2**2*y2)/5005 + (591*a*b*x2**2*y2)/5005 + (207*b**2*x2**2*y2)/1430 + (81*a*b**2*x2**2*y2)/1001 - (1873*y1*y2)/10010 + (216*a*y1*y2)/5005 + (1431*a**2*y1*y2)/10010 - (1608*b*y1*y2)/5005 - (486*a*b*y1*y2)/1001 - (216*a**2*b*y1*y2)/715 - (23*dEnd**2*y1*y2)/105 + (2*b*dEnd**2*y1*y2)/7 - (8*dEnd*dStart*y1*y2)/105 + (2*b*dEnd*dStart*y1*y2)/7 + (4*dStart**2*y1*y2)/105 + (4*b*dStart**2*y1*y2)/35 + (8137*x**2*y1*y2)/30030 + (267*a*x**2*y1*y2)/5005 - (54*a**2*x**2*y1*y2)/5005 - (48*b*x**2*y1*y2)/143 - (138*a*b*x**2*y1*y2)/715 - (54*a**2*b*x**2*y1*y2)/1001 + (1409*x*x1*y1*y2)/15015 + (25*a*x*x1*y1*y2)/143 + (50*b*x*x1*y1*y2)/143 + (534*a*b*x*x1*y1*y2)/5005 - (207*b**2*x*x1*y1*y2)/715 - (162*a*b**2*x*x1*y1*y2)/1001 - (7607*x1**2*y1*y2)/30030 + (606*b*x1**2*y1*y2)/5005 + (963*b**2*x1**2*y1*y2)/5005 - (108*b**3*x1**2*y1*y2)/1001 - (685*x*x2*y1*y2)/1001 - (25*a*x*x2*y1*y2)/143 + (2826*b*x*x2*y1*y2)/5005 + (1182*a*b*x*x2*y1*y2)/5005 + (207*b**2*x*x2*y1*y2)/715 + (162*a*b**2*x*x2*y1*y2)/1001 + (486*x1*x2*y1*y2)/5005 - (100*b*x1*x2*y1*y2)/143 + (648*b**2*x1*x2*y1*y2)/5005 + (216*b**3*x1*x2*y1*y2)/1001 + (12413*x2**2*y1*y2)/30030 - (538*b*x2**2*y1*y2)/5005 - (1611*b**2*x2**2*y1*y2)/5005 - (108*b**3*x2**2*y1*y2)/1001 - (53*y1**2*y2)/2002 - (204*a*y1**2*y2)/1001 + (1392*b*y1**2*y2)/5005 - (216*a*b*y1**2*y2)/5005 + (729*b**2*y1**2*y2)/2002 + (324*a*b**2*y1**2*y2)/715 + (801*y1**3*y2)/10010 + (216*b*y1**3*y2)/5005 - (999*b**2*y1**3*y2)/10010 - (144*b**3*y1**3*y2)/715 - (1069*y2**2)/5005 - (1824*a*y2**2)/5005 - (189*a**2*y2**2)/715 + (804*b*y2**2)/5005 + (243*a*b*y2**2)/1001 + (108*a**2*b*y2**2)/715 + (13*dEnd**2*y2**2)/42 - (b*dEnd**2*y2**2)/7 + (5*dEnd*dStart*y2**2)/21 - (b*dEnd*dStart*y2**2)/7 + (17*dStart**2*y2**2)/210 - (2*b*dStart**2*y2**2)/35 - (262*x**2*y2**2)/715 - (111*a*x**2*y2**2)/715 - (75*a**2*x**2*y2**2)/2002 + (24*b*x**2*y2**2)/143 + (69*a*b*x**2*y2**2)/715 + (27*a**2*b*x**2*y2**2)/1001 + (1949*x*x1*y2**2)/6006 + (778*a*x*x1*y2**2)/5005 - (2162*b*x*x1*y2**2)/5005 - (225*a*b*x*x1*y2**2)/1001 + (207*b**2*x*x1*y2**2)/1430 + (81*a*b**2*x*x1*y2**2)/1001 - (2138*x1**2*y2**2)/15015 + (1556*b*x1**2*y2**2)/5005 - (225*b**2*x1**2*y2**2)/1001 + (54*b**3*x1**2*y2**2)/1001 + (857*x*x2*y2**2)/1430 + (509*a*x*x2*y2**2)/5005 - (18*b*x*x2*y2**2)/715 + (267*a*b*x*x2*y2**2)/5005 - (207*b**2*x*x2*y2**2)/1430 - (81*a*b**2*x*x2*y2**2)/1001 - (7607*x1*x2*y2**2)/30030 + (606*b*x1*x2*y2**2)/5005 + (963*b**2*x1*x2*y2**2)/5005 - (108*b**3*x1*x2*y2**2)/1001 - (333*x2**2*y2**2)/1430 - (446*b*x2**2*y2**2)/5005 + (162*b**2*x2**2*y2**2)/5005 + (54*b**3*x2**2*y2**2)/1001 + (1409*y1*y2**2)/10010 - (696*a*y1*y2**2)/5005 + (408*b*y1*y2**2)/1001 + (4077*a*b*y1*y2**2)/5005 - (729*b**2*y1*y2**2)/2002 - (324*a*b**2*y1*y2**2)/715 + (729*y1**2*y2**2)/10010 - (324*b*y1**2*y2**2)/5005 - (4293*b**2*y1**2*y2**2)/10010 + (216*b**3*y1**2*y2**2)/715 + (2867*y2**3)/10010 + (72*a*y2**3)/143 - (1824*b*y2**3)/5005 - (378*a*b*y2**3)/715 + (243*b**2*y2**3)/2002 + (108*a*b**2*y2**3)/715 + (801*y1*y2**3)/10010 - (3216*b*y1*y2**3)/5005 + (6723*b**2*y1*y2**3)/10010 - (144*b**3*y1*y2**3)/715 - (262*y2**4)/715 + (72*b*y2**4)/143 - (189*b**2*y2**4)/715 + (36*b**3*y2**4)/715)/((24*x1**2)/143 + (69*a*x1**2)/715 + (27*a**2*x1**2)/1001 - (2*dEnd**2*x1**2)/35 - (dEnd*dStart*x1**2)/7 - (dStart**2*x1**2)/7 + (804*x**2*x1**2)/5005 + (243*a*x**2*x1**2)/1001 + (108*a**2*x**2*x1**2)/715 - (1824*x*x1**3)/5005 - (378*a*x*x1**3)/715 + (243*b*x*x1**3)/1001 + (216*a*b*x*x1**3)/715 + (72*x1**4)/143 - (378*b*x1**4)/715 + (108*b**2*x1**4)/715 - (48*x1*x2)/143 - (138*a*x1*x2)/715 - (54*a**2*x1*x2)/1001 + (4*dEnd**2*x1*x2)/35 + (2*dEnd*dStart*x1*x2)/7 + (2*dStart**2*x1*x2)/7 - (1608*x**2*x1*x2)/5005 - (486*a*x**2*x1*x2)/1001 - (216*a**2*x**2*x1*x2)/715 + (408*x*x1**2*x2)/1001 + (4077*a*x*x1**2*x2)/5005 - (729*b*x*x1**2*x2)/1001 - (648*a*b*x*x1**2*x2)/715 - (3216*x1**3*x2)/5005 + (6723*b*x1**3*x2)/5005 - (432*b**2*x1**3*x2)/715 + (24*x2**2)/143 + (69*a*x2**2)/715 + (27*a**2*x2**2)/1001 - (2*dEnd**2*x2**2)/35 - (dEnd*dStart*x2**2)/7 - (dStart**2*x2**2)/7 + (804*x**2*x2**2)/5005 + (243*a*x**2*x2**2)/1001 + (108*a**2*x**2*x2**2)/715 + (1392*x*x1*x2**2)/5005 - (216*a*x*x1*x2**2)/5005 + (729*b*x*x1*x2**2)/1001 + (648*a*b*x*x1*x2**2)/715 - (324*x1**2*x2**2)/5005 - (4293*b*x1**2*x2**2)/5005 + (648*b**2*x1**2*x2**2)/715 - (1608*x*x2**3)/5005 - (243*a*x*x2**3)/1001 - (243*b*x*x2**3)/1001 - (216*a*b*x*x2**3)/715 + (216*x1*x2**3)/5005 - (999*b*x1*x2**3)/5005 - (432*b**2*x1*x2**3)/715 + (804*x2**4)/5005 + (243*b*x2**4)/1001 + (108*b**2*x2**4)/715 - (204*x*x1*y1)/1001 - (276*a*x*x1*y1)/715 - (108*a**2*x*x1*y1)/1001 - (18*x1**2*y1)/715 + (267*a*x1**2*y1)/5005 - (207*b*x1**2*y1)/715 - (162*a*b*x1**2*y1)/1001 + (204*x*x2*y1)/1001 + (276*a*x*x2*y1)/715 + (108*a**2*x*x2*y1)/1001 + (2826*x1*x2*y1)/5005 + (1182*a*x1*x2*y1)/5005 + (414*b*x1*x2*y1)/715 + (324*a*b*x1*x2*y1)/1001 - (540*x2**2*y1)/1001 - (207*a*x2**2*y1)/715 - (207*b*x2**2*y1)/715 - (162*a*b*x2**2*y1)/1001 + (804*y1**2)/5005 + (243*a*y1**2)/1001 + (108*a**2*y1**2)/715 - (dEnd**2*y1**2)/7 - (dEnd*dStart*y1**2)/7 - (2*dStart**2*y1**2)/35 + (24*x**2*y1**2)/143 + (69*a*x**2*y1**2)/715 + (27*a**2*x**2*y1**2)/1001 + (412*x*x1*y1**2)/5005 + (591*a*x*x1*y1**2)/5005 + (207*b*x*x1*y1**2)/715 + (162*a*b*x*x1*y1**2)/1001 - (446*x1**2*y1**2)/5005 + (324*b*x1**2*y1**2)/5005 + (162*b**2*x1**2*y1**2)/1001 - (540*x*x2*y1**2)/1001 - (207*a*x*x2*y1**2)/715 - (207*b*x*x2*y1**2)/715 - (162*a*b*x*x2*y1**2)/1001 - (538*x1*x2*y1**2)/5005 - (3222*b*x1*x2*y1**2)/5005 - (324*b**2*x1*x2*y1**2)/1001 + (540*x2**2*y1**2)/1001 + (414*b*x2**2*y1**2)/715 + (162*b**2*x2**2*y1**2)/1001 - (1608*y1**3)/5005 - (243*a*y1**3)/1001 - (243*b*y1**3)/1001 - (216*a*b*y1**3)/715 + (804*y1**4)/5005 + (243*b*y1**4)/1001 + (108*b**2*y1**4)/715 + (204*x*x1*y2)/1001 + (276*a*x*x1*y2)/715 + (108*a**2*x*x1*y2)/1001 - (2162*x1**2*y2)/5005 - (225*a*x1**2*y2)/1001 + (207*b*x1**2*y2)/715 + (162*a*b*x1**2*y2)/1001 - (204*x*x2*y2)/1001 - (276*a*x*x2*y2)/715 - (108*a**2*x*x2*y2)/1001 + (50*x1*x2*y2)/143 + (534*a*x1*x2*y2)/5005 - (414*b*x1*x2*y2)/715 - (324*a*b*x1*x2*y2)/1001 + (412*x2**2*y2)/5005 + (591*a*x2**2*y2)/5005 + (207*b*x2**2*y2)/715 + (162*a*b*x2**2*y2)/1001 - (1608*y1*y2)/5005 - (486*a*y1*y2)/1001 - (216*a**2*y1*y2)/715 + (2*dEnd**2*y1*y2)/7 + (2*dEnd*dStart*y1*y2)/7 + (4*dStart**2*y1*y2)/35 - (48*x**2*y1*y2)/143 - (138*a*x**2*y1*y2)/715 - (54*a**2*x**2*y1*y2)/1001 + (50*x*x1*y1*y2)/143 + (534*a*x*x1*y1*y2)/5005 - (414*b*x*x1*y1*y2)/715 - (324*a*b*x*x1*y1*y2)/1001 + (606*x1**2*y1*y2)/5005 + (1926*b*x1**2*y1*y2)/5005 - (324*b**2*x1**2*y1*y2)/1001 + (2826*x*x2*y1*y2)/5005 + (1182*a*x*x2*y1*y2)/5005 + (414*b*x*x2*y1*y2)/715 + (324*a*b*x*x2*y1*y2)/1001 - (100*x1*x2*y1*y2)/143 + (1296*b*x1*x2*y1*y2)/5005 + (648*b**2*x1*x2*y1*y2)/1001 - (538*x2**2*y1*y2)/5005 - (3222*b*x2**2*y1*y2)/5005 - (324*b**2*x2**2*y1*y2)/1001 + (1392*y1**2*y2)/5005 - (216*a*y1**2*y2)/5005 + (729*b*y1**2*y2)/1001 + (648*a*b*y1**2*y2)/715 + (216*y1**3*y2)/5005 - (999*b*y1**3*y2)/5005 - (432*b**2*y1**3*y2)/715 + (804*y2**2)/5005 + (243*a*y2**2)/1001 + (108*a**2*y2**2)/715 - (dEnd**2*y2**2)/7 - (dEnd*dStart*y2**2)/7 - (2*dStart**2*y2**2)/35 + (24*x**2*y2**2)/143 + (69*a*x**2*y2**2)/715 + (27*a**2*x**2*y2**2)/1001 - (2162*x*x1*y2**2)/5005 - (225*a*x*x1*y2**2)/1001 + (207*b*x*x1*y2**2)/715 + (162*a*b*x*x1*y2**2)/1001 + (1556*x1**2*y2**2)/5005 - (450*b*x1**2*y2**2)/1001 + (162*b**2*x1**2*y2**2)/1001 - (18*x*x2*y2**2)/715 + (267*a*x*x2*y2**2)/5005 - (207*b*x*x2*y2**2)/715 - (162*a*b*x*x2*y2**2)/1001 + (606*x1*x2*y2**2)/5005 + (1926*b*x1*x2*y2**2)/5005 - (324*b**2*x1*x2*y2**2)/1001 - (446*x2**2*y2**2)/5005 + (324*b*x2**2*y2**2)/5005 + (162*b**2*x2**2*y2**2)/1001 + (408*y1*y2**2)/1001 + (4077*a*y1*y2**2)/5005 - (729*b*y1*y2**2)/1001 - (648*a*b*y1*y2**2)/715 - (324*y1**2*y2**2)/5005 - (4293*b*y1**2*y2**2)/5005 + (648*b**2*y1**2*y2**2)/715 - (1824*y2**3)/5005 - (378*a*y2**3)/715 + (243*b*y2**3)/1001 + (216*a*b*y2**3)/715 - (3216*y1*y2**3)/5005 + (6723*b*y1*y2**3)/5005 - (432*b**2*y1*y2**3)/715 + (72*y2**4)/143 - (378*b*y2**4)/715 + (108*b**2*y2**4)/715)
112 | b = b - newtonstep
113 | return b
114 |
115 | # And this is the main routine
116 | def offset(bez, sAngle, eAngle, d1, d2=None):
117 | if not d2:
118 | d2 = d1
119 | tr = unitize(*bez)
120 | s = [ bez[0][0] + d1 * cos(sAngle), bez[0][1] + d1 * sin(sAngle) ]
121 | e = [ bez[3][0] + d2 * cos(eAngle), bez[3][1] + d2 * sin(eAngle) ]
122 | bez2 = applyTransform(bez,tr)
123 | alpha = tension(bez2)
124 | scaledD1 = d1 / tr["scale"]
125 | scaledD2 = d2 / tr["scale"]
126 | scaledS = apply1(s,tr)
127 | scaledE = apply1(e,tr)
128 |
129 | #
130 |
131 | beta = findBeta(bez2[3][0], alpha, scaledS, scaledE, scaledD1, scaledD2)
132 | if beta < 0 or beta > 1:
133 | raise ValueError
134 | print(s,e)
135 | offsetCurve = curveWithTension(
136 | s,
137 | e,
138 | beta,
139 | sAngle,
140 | eAngle
141 | )
142 | return offsetCurve
143 |
144 | # p = Glyphs.font.selectedLayers[0].paths[0]
145 | # arr = segToArray(p.segments[0])
146 | # dStart = 120
147 | # dEnd = 60
148 | # path2 = arrayToGSPath(offset(arr, 0, 0, dStart, dEnd))
149 | # Glyphs.font.selectedLayers[0].paths.append(path2)
--------------------------------------------------------------------------------
/offset.tex:
--------------------------------------------------------------------------------
1 | \documentclass[a4paper]{article}
2 | \ifdefined\HCode
3 | \def\pgfsysdriver{pgfsys-tex4ht.def}
4 | \fi
5 | \usepackage{fullpage}
6 | \title{Offsetting parameterised Bezier curves}
7 | \author{Simon Cozens}
8 | \usepackage{amsmath}
9 | \usepackage{mathtools}
10 |
11 | \newtagform{brackets}{[}{]}
12 | \usetagform{brackets}
13 | \usepackage{breqn}
14 | \newcommand{\icol}[1]{% inline column vector
15 | \left(\begin{smallmatrix}#1\end{smallmatrix}\right)%
16 | }
17 | \newcommand{\Norm}[1]{\lVert #1 \rVert}
18 | \newcommand{\norm}[1]{\lvert #1 \rvert}
19 |
20 | \usepackage{tikz}
21 | \newcommand\DrawControl[3]{
22 | node[#2,circle,fill=#2,inner sep=1pt,label=#3] at #1 {}
23 | }
24 | \begin{document}
25 | \maketitle
26 |
27 | A common problem in type design is the creation of pairs of curves representing the stroke of a pen: an inner curve and an outer curve delimit the contours of a writing implement of fixed or flexible thickness. While it is impossible to precisely offset a Bezier curve at a given width, this paper presents a simple approximation by minimizing the error between desired distance and actual distance. This can also be applied to situations where the thickness varies linearly across the width of the curve.
28 |
29 | We use a simplification due to Tunni, who postulates that any curve $\mathbf{a,b,c,d}$ with straight handles (i.e. where the control points $\mathbf{b}$ and $\mathbf{c}$ are positioned orthogonally to $\mathbf{a}$ and $\mathbf{d}$respectively) can be represented in terms of start and end points $\mathbf{a}$ and $\mathbf{d}$ and a curve tension $\tau$. To determine curve tension, compute the point $\mathbf{T}$ where $\vec{\mathbf{ab}}$ intersects $\vec{\mathbf{cd}}$:
30 |
31 | \bigskip
32 | \resizebox{100pt}{!}{
33 | \begin{tikzpicture}[baseline]
34 | \draw[help lines] (0,0) grid (2,2);
35 | \draw[ultra thick]
36 | (1,0)
37 | .. controls (1,0.5) and (0.5,1) ..
38 | (0,1) \DrawControl{(1,0.5)}{blue}{right:c}\DrawControl{(0.5,1)}{blue}{b} \DrawControl{(1,1)}{blue}{T}\DrawControl{(1,0)}{blue}{right:d}\DrawControl{(0,1)}{blue}{a};
39 | \end{tikzpicture}
40 | }
41 | \bigskip
42 |
43 | The curve tension is given by the mean of the ratios $\frac{\Norm{\mathbf{ab}}}{\Norm{\mathbf{aT}}}$ and $\frac{\Norm{\mathbf{dc}}}{\Norm{\mathbf{dT}}}$. Given the points $\mathbf{a,d}$ and a tension $\tau$ we can compute the Bezier control points $\mathbf{b,c}$ by setting them at the appropriate ratios.
44 |
45 | This conceptualization enables us to find similar parameters for an offset Bezier curve. We will approach the problem in small pieces, demonstrating the technique first before solving the general case.
46 |
47 | \section{Outer offset of a unit Bezier curve}
48 |
49 | Consider first the unit Bezier curve $\mathbf{B_A}$ with $\mathbf{a} = (0, 1), \mathbf{d} = (1, 0)$ and $\mathbf{c}$ and $\mathbf{d}$ chosen as orthogonal control points with a curve tension $\alpha$. What are the parameters for a Bezier curve $\mathbf{B_B}$ offsetting this curve on the outside at a fixed distance $\delta$?
50 |
51 | \bigskip
52 | \resizebox{100pt}{!}{
53 | \begin{tikzpicture}[baseline]
54 | \draw[help lines] (0,0) grid (2,2);
55 | \draw[ultra thick]
56 | (1,0)
57 | .. controls (1,0.5) and (0.5,1) ..
58 | (0,1) \DrawControl{(1,0.5)}{blue}{}\DrawControl{(0.5,1)}{blue}{} ;
59 | \draw[thick](2,0) .. controls (2,0.77) and (0.77,2) .. (0,2);
60 | \end{tikzpicture}
61 | }
62 | \bigskip
63 |
64 | Clearly we have $\mathbf{a} = (0, 1+\delta), \mathbf{d} = (1+\delta, 0)$, so it remains to find the curve tension $\beta$.
65 |
66 | As a function of time, the distance between the two curves is:
67 |
68 | \begin{equation}\Norm{\mathbf{B_A}(t)\cdot\mathbf{B_B}(t)}\end{equation}
69 |
70 | and at any point on the curve, the expected distance is $\delta$. Knowing
71 | it is impossible to achieve a perfect offset, we can treat this as an optimization problem: find the value of $\beta$ which minimizes the total error function
72 |
73 | \begin{equation}\int_{0}^{1} (\Norm{\mathbf{B_A}(t)\cdot\mathbf{B_B}(t)} - \delta)^2 dt\end{equation}
74 |
75 | This integral turns out to be tricky to compute due to the presence of the square root, so instead we create an equivalent error function using the square of the distance. We expect the square of the distance to be $\delta^2$, and we square the difference of these two values to perform a least squares optimization. This leads to an error function of
76 |
77 | \begin{equation}(\norm{\mathbf{B_A}(t)\cdot\mathbf{B_B}(t)} - \delta^2)^2\end{equation}
78 |
79 | For a unit Bezier, we have:
80 |
81 | \begin{multline}
82 | \mathbf{B_A}(t) = \left(\begin{matrix}
83 | t^3+3 (1-t) t^2+3 \alpha (1-t)^2 t \\
84 | 3 \alpha t^2 (1-t)+(1-t)^3+3 t (1-t)^2
85 | \end{matrix}\right)\\
86 | \mathbf{B_B}(t) = \left(\begin{matrix}
87 | (\Delta +1) t^3+3 (1-t) t^2 ((1-\beta ) (\Delta +1)+\beta (\Delta +1))+3 \beta (\Delta +1) (1-t)^2 t \\
88 | 3 \beta (\Delta +1) t^2 (1-t)+3 t (1-t)^2 ((1-\beta ) (\Delta +1)+\beta (\Delta +1))+(\Delta +1) (1-t)^3
89 | \end{matrix}\right) \\
90 | = \left(\begin{matrix}
91 | (\Delta +1) t \left(3 \beta (t-1)^2+t (3-2 t)\right) \\
92 | (\Delta +1) (-(t-1)) \left((3 \beta -2) t^2+t+1\right)
93 | \end{matrix}
94 | \right)
95 | \end{multline}
96 |
97 | leading to a square distance
98 |
99 | \begin{multline}\norm{\mathbf{B_A}(t)\cdot\mathbf{B_B}(t)} =
100 | t^2 \left(3 \alpha (t-1)^2-3 \beta (\Delta +1) (t-1)^2+\Delta t (2 t-3)\right)^2+ \\ (t-1)^2 (\Delta +t (\Delta +t (-3 \alpha +3 \beta (\Delta +1)-2 \Delta )))^2
101 | \end{multline}
102 |
103 | and therefore an error function
104 |
105 | \begin{multline}E(\mathbf{B_A},\mathbf{B_B}) = \int_{0}^{1}(\norm{\mathbf{B_A}(t)\cdot\mathbf{B_B}(t)} - \delta^2)^2 dt = \frac{1}{30030}\left(
106 | 1161 (\alpha -\beta )^4 - 36 (129 \beta +148) \delta (\alpha -\beta )^3 + \right. \\
107 | \left. 18 \left(387 \beta ^2+888 \beta +146\right) \delta ^2 (\alpha -\beta )^2 - 4 (9 \beta (3 \beta (43 \beta +148)+146)-2138) \delta ^3 (\alpha -\beta ) \right.\\
108 | \left. (\beta (9 \beta (\beta (129 \beta +592)+292)-8552)+2916) \delta ^4\right)
109 | \end{multline}
110 |
111 | This looks horrific, but it's only a quartic, and is easily optimizable. Rather than solving the differential equation for the general case, let's be practical, remember that $\alpha$ and $\delta$ will be given and go for a numerical method to minimize the error function.
112 |
113 | Beginning with $\beta_1 = \alpha$ and applying the Newton-Raphson optimization method gives us an iterated function
114 |
115 | \begin{multline}
116 | \beta_{n+1} = \beta_n -\frac{E'(\mathbf{B_A},\mathbf{B_B})}{E''(\mathbf{B_A},\mathbf{B_B})} \\
117 | = \beta_n -
118 | \frac{
119 | \left(\begin{matrix}
120 | -36 \left(387 \beta ^2+888 \beta +146\right) \delta ^2 (\alpha -\beta ) \\
121 | -4 (9 \beta (129 \beta +3 (43 \beta +148))+9 (3 \beta (43 \beta +148)+146)) \delta ^3 (\alpha -\beta ) \\
122 | +18 (774 \beta +888) \delta ^2 (\alpha -\beta )^2-4644 \delta (\alpha -\beta )^3+108 (129 \beta +148) \delta (\alpha -\beta )^2 \\
123 | -4644 (\alpha -\beta )^3+(9 \beta (\beta (129 \beta +592)+292)+ \\
124 | \beta (9 \beta (258 \beta +592)+9 (\beta (129 \beta +592)+292))-8552) \delta ^4+4 (9 \beta (3 \beta (43 \beta +148)+146)-2138) \delta ^3
125 | \end{matrix}\right)
126 | }{\left(\begin{matrix}
127 | -4 (2322 \beta +18 (129 \beta +3 (43 \beta +148))) \delta ^3 (\alpha -\beta )+ \\
128 | 13932 \delta ^2 (\alpha -\beta )^2-72 (774 \beta +888) \delta ^2 (\alpha -\beta )+27864 \delta (\alpha -\beta )^2\\
129 | -216 (129 \beta +148) \delta (\alpha -\beta )+\\
130 | 13932 (\alpha -\beta )^2+36 \left(387 \beta ^2+888 \beta +146\right) \delta ^2+\\
131 | (\beta (2322 \beta +18 (258 \beta +592))+2 (9 \beta (258 \beta +592)+9 (\beta (129 \beta +592)+292))) \delta ^4 + \\
132 | 8 (9 \beta (129 \beta +3 (43 \beta +148))+9 (3 \beta (43 \beta +148)+146)) \delta ^3
133 | \end{matrix}\right)}
134 | \end{multline}
135 |
136 | quickly converges to the minimum error, giving us the optimal curve tension.
137 |
138 | As an example, plugging in $\alpha = 0.55, \Delta = 1$:
139 |
140 | \begin{align*}
141 | \beta_1 = 0.55 \\
142 | \beta_2 = 0.550987 \\
143 | \beta_3 = 0.550985 \\
144 | \beta_4 = 0.550985
145 | \end{align*}
146 |
147 | \subsection{We can cheat}
148 |
149 | Thankfully, we find by inspection that the optimal value of $\beta$ given $\alpha$ and $\Delta$, $\beta(\alpha, \Delta)$ turns out to be pretty much linear in both $\alpha$ and $\Delta$ when $\alpha \ge 0.3$. A very pleasing result is:
150 |
151 | \begin{equation}
152 | \beta(\alpha,1) = 0.275985 + \frac{\alpha}{2}
153 | \end{equation}
154 |
155 | Note that this gives exactly the answer given by our Newton-Raphson method above. A more general, but less accurate, approximation is:
156 |
157 | \begin{equation}\label{approx}
158 | \beta(\alpha, \delta) = 0.513216 \alpha -0.025407 \delta+0.296638
159 | \end{equation}
160 |
161 | \section{Inner offsetting of a unit Bezier}
162 |
163 | What if we want to go the other way, and find the inner curve at a fixed distance?
164 |
165 | \bigskip
166 | \begin{tikzpicture}[baseline]
167 | \draw[help lines] (0,0) grid (2,2);
168 | \draw[ultra thick]
169 | (2,0)
170 | .. controls (2,0.77) and (0.77,2) ..
171 | (0,2) \DrawControl{(2,0.77)}{blue}{}\DrawControl{(0.77,2)}{blue}{} ;
172 | \draw[thick](1,0) .. controls (1,0.55) and (0.55,1) .. (0,1);
173 | \end{tikzpicture}
174 | \bigskip
175 |
176 | A very similar pattern applies, but this time we construct $\mathbf{B_B}$ as
177 | $\mathbf{a} = (0, 1-\delta, \mathbf{d} = (1-\delta, 0)$
178 | and the Newton step $\frac{E'(\mathbf{B_A},\mathbf{B_B})}{E''(\mathbf{B_A},\mathbf{B_B})}$ is
179 |
180 | \begin{multline}
181 | \frac{\left(
182 | \begin{matrix}
183 | 18 \alpha ^2 \left(414 \beta (\delta -1)^2+244 (\delta -1)^2\right)+8 \alpha \left(1098 \beta (\delta -1)^2-108 (\delta -1)^2\right)+ \\
184 | 4644 \beta ^3 (\delta -1)^4+15984 \beta ^2 (\delta -1)^4+72 \beta \left(73 \delta ^2-718 \delta +548\right) (\delta -1)^2 \\
185 | -8 \left(1069 \delta ^2+3439 \delta -4011\right) (\delta -1)^2
186 | \end{matrix}\right)
187 | }{\left(
188 | \begin{matrix}7452 \alpha ^2 (\delta -1)^2+8784 \alpha (\delta -1)^2+13932 \beta ^2 (\delta -1)^4+31968 \beta (\delta -1)^4+ \\
189 | 72 \left(73 \delta ^2-718 \delta +548\right) (\delta -1)^2
190 | \end{matrix}\right)
191 | }
192 | \end{multline}
193 |
194 | Equally, we can invert our approximation \ref{approx} above, giving:
195 |
196 | \begin{equation}
197 | \alpha = 1.9485 \beta + 0.0495055 \delta -0.577999
198 | \end{equation}
199 |
200 | \section{Outer offsetting of an arbitrary normalized curve}
201 |
202 | Real-world curves are not unit curves $(0, 1)\cdots(1, 0)$. However, we can always use affine transformation to locate the start at $\mathbf{a} = (1, 0)$, leaving the end at $\mathbf{d} = (0, x)$. The control points for a Bezier curve with tension $\tau$ $\mathbf{B_A}$ would then be set at $\mathbf{b} = (1-\tau, 0), \mathbf{c} = (0, x(1-\tau))$. The problem, again, is to find the offset curve $\mathbf{B_B}$ which best approximates a fixed distance $\delta$ from $\mathbf{B_A}$.
203 |
204 | Now we have $\mathbf{a} = (0, 1+\delta), \mathbf{d} = (x+\delta, 0)$. Following exactly the procedure above, $\norm{\mathbf{B_A}(t)\cdot\mathbf{B_B}(t)}$, the square of the distance between the two curves at point $t$, is
205 |
206 | \begin{multline}
207 | t^2 \left(\delta \left(t (2 t-3)-3 \beta (t-1)^2\right)+3 (t-1)^2 x (\alpha -\beta )\right)^2+(t-1)^2 (\delta +t (\delta +t (-3 \alpha +3 \beta (\delta +1)-2 \delta )))^2
208 | \end{multline}
209 |
210 | and the total error across the curve is
211 |
212 | \begin{multline}E(\mathbf{B_A},\mathbf{B_B}) = \int_{0}^{1}(\norm{\mathbf{B_A}(t)\cdot\mathbf{B_B}(t)} - \delta^2)^2 dt \\
213 | = \frac{
214 | \begin{matrix}27 \left(14 x^4+15 x^2+14\right) (\alpha -\beta )^4-9 \delta (x+1) (\alpha -\beta )^3 (3 (56 \beta +45)+x (-78 \beta +3 (56 \beta +45) x+26)) \\
215 | + 9 \delta ^2 (\alpha -\beta )^2 \left(566 \beta +9 \beta ^2 (x (33 x+20)+33)+2 \beta x (283 x+322)+4 x (85-6 x)-24\right) \\
216 | + 2 (9 \beta (3 \beta (43 \beta +148)+146)-2138) \delta ^3 (x+1) (\alpha -\beta ) \\
217 | + (\beta (9 \beta (\beta (129 \beta +592)+292)-8552)+2916) \delta ^4
218 | \end{matrix}
219 | }{30030}
220 | \end{multline}
221 |
222 | Once again, it's only a quartic and three of the variables are given. We can apply the Newton-Raphson method again, giving:
223 |
224 | \begin{multline}
225 | \beta_{n+1} = \beta_n -\frac{E'(\mathbf{B_A},\mathbf{B_B})}{E''(\mathbf{B_A},\mathbf{B_B})} = \beta_n - \\
226 | \frac{\begin{matrix}4 \left(1161 \beta ^3+3996 \beta ^2+1314 \beta -2138\right) \delta ^4\\
227 | -54 \delta (x+1) \left(28 x^2-13 x+28\right) (\alpha -\beta )^3-108 \left(14 x^4+15 x^2+14\right) (\alpha -\beta )^3 \\
228 | -18 \left(387 \beta ^2+888 \beta +146\right) \delta ^3 (x+1) (\alpha -\beta ) \\
229 | -18 \delta ^2 (\alpha -\beta ) \left(566 \beta +9 \beta ^2 (x (33 x+20)+33)+2 \beta x (283 x+322)+4 x (85-6 x)-24\right)+ \\
230 | 9 \delta ^2 (\alpha -\beta )^2 (18 \beta (x (33 x+20)+33)+2 x (283 x+322)+566) \\
231 | +27 \delta (x+1) (\alpha -\beta )^2 (3 (56 \beta +45)+x (-78 \beta +3 (56 \beta +45) x+26))+ \\
232 | 2 (9 \beta (3 \beta (43 \beta +148)+146)-2138) \delta ^3 (x+1)\end{matrix}}{\begin{matrix}
233 | 18 \left(2 \left(387 \beta ^2+888 \beta +146\right) \delta ^4+18 \delta (x+1) \left(28 x^2-13 x+28\right) (\alpha -\beta )^2+18 \left(14 x^4+15 x^2+14\right) \right. \\
234 | \left. (\alpha -\beta )^2 -6 (129 \beta +148) \delta ^3 (x+1) (\alpha -\beta )+9 \delta ^2 (x (33 x+20)+33) (\alpha -\beta )^2-2 \delta ^2 (\alpha -\beta ) \right. \\
235 | \left. (18 \beta (x (33 x+20)+33)+2 x (283 x+322)+566)-3 \delta (x+1) (\alpha -\beta ) (3 (56 \beta +45)+ \right.\\
236 | \left.x (-78 \beta +3 (56 \beta +45) x+26))+ 2 \left(387 \beta ^2+888 \beta +146\right) \delta ^3 (x+1)+\delta ^2 \left(566 \beta +9 \beta ^2 (x (33 x+20)+33)+ \right. \right. \\
237 | \left. \left. 2 \beta x (283 x+322)+4 x (85-6 x)-24\right)\right)\end{matrix}}
238 | \end{multline}
239 |
240 | By iterating this approximation, we can derive the tension for a curve at an offset of a given distance $\delta$ from an arbitrary Bezier curve specified by two points and a curve tension parameter.
241 |
242 | But wait, it gets more complicated.
243 |
244 | \section{Offsetting at a linear-gradiated distance}
245 |
246 | Strokes in fonts often have a feature called \textit{contrast}, meaning that the horizontal offset is not the same as the vertical offset:
247 |
248 | \bigskip
249 | \resizebox{150pt}{!}{
250 | \begin{tikzpicture}
251 | \draw[help lines] (0,0) grid (2.5,2);
252 | \draw[ultra thick]
253 | (0,1.27) .. controls (0.65,1.27) and (1.17, 0.71) .. (1.17,0);
254 | \draw[thick](0,1.77) .. controls (1.18,1.77) and (2.17,0.97) .. (2.17,0);
255 | \end{tikzpicture}
256 | }
257 | \bigskip
258 |
259 | To model this we will assume that the desired distance between curves is a linear function of curve time $t$:
260 |
261 | \begin{equation}
262 | \delta(t) = \delta_s(1-t) + \delta_e t
263 | \end{equation}
264 |
265 | And now our error function is
266 | \begin{equation}
267 | (\norm{\mathbf{B_A}(t)\cdot\mathbf{B_B}(t)} - \delta(t)^2)^2 = x
268 | \end{equation}
269 |
270 | The total integrated error across the curve becomes\dots\ very complicated, but computable. We can apply a similar Newton-Raphson method as above, leading to the functions given in the associated Python script.
271 |
272 | \end{document}
273 |
--------------------------------------------------------------------------------
/test.py:
--------------------------------------------------------------------------------
1 | from offset import offset
2 | from math import radians
3 |
4 | bez = [ [54, 326], [232,326], [328,279], [328,191] ]
5 | print(offset(bez, 0, 0, 120,60))
6 |
7 |
--------------------------------------------------------------------------------