├── 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 ∥∥aabT∥∥ and ∥∥ddTc∥∥. 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 | ∥BA (t)⋅BB (t)∥
 36 |
[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 | ∫ 1
 47 |    (∥BA (t)⋅BB (t)∥ - δ)2dt
 48 |  0
 49 |
[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 |                  2 2
 61 | (|BA (t)⋅BB (t)|- δ )
 62 |
[3]
63 | 64 | For a unit Bezier, we have: 65 | 66 |
       (    3         2          2   )
 68 | BA (t) =    t2+ 3(1- t)t + 3α3(1 - t) t 2
 69 |          3αt((1 - t)+ (1- t) + 3t(1 - t)                                        )
 70 |                (Δ + 1)t3 + 3(1- t)t2((1- *β*Δ +1) +*β*Δ + 1))+ 3*β*#x0394; + 1)(1 - t)2t
 71 |     BB(t) =  3*β*Δ + 1)t2(1 - t)+ 3t(1- t)2((1- *β*Δ + 1)+ *β*#x0394; +1))+ (Δ + 1)(1 - t)3
 72 |                                                 (  (Δ + 1)t(3*β*- 1)2 +t(3- 2t))  )
 73 |                                               =  (Δ + 1)(- (t- 1))((3*β* 2)t2 + t+ 1)
 74 |
[4]
75 | 76 | leading to a square distance 77 | 78 |
                2(       2                2           )2
 80 | |BA (t)⋅BB (t)| = t 3α (t - 1) - 3*β*#x0394; + 1)(t- 1) +Δt (2t - 3) +
 81 |                                           (t- 1)2(Δ + t(Δ + t(- 3α + 3*β*#x0394; + 1) - 2Δ )))2
 82 |
[5]
83 | and therefore an error function 84 |
            ∫ 1                         1  (
 87 | E(BA, BB ) =   (|BA (t)⋅BB (t)|- δ2)2dt = 30030 1161(α- *β*4 - 36(129*β* 148)δ(α- *β*3+
 88 |          (   02           ) 2      2                                 3
 89 |        18  387*β*+ 888*β* 146 δ (α- *β*- 4(9*β**β*3*β* 148)+ 146)- 2138)δ(α - *β*  )
 90 |                                           (*β**β*β*29*β* 592)+ 292)- 8552)+ 2916)δ4
 91 |
[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 |
               ′
101 |   *β*1 = *β*- E-(BA,-BB-)
102 |       (      E′′(BA, BB )            (                )                                 )
103 |                                 - 36 387*β*+ 888*β* 146 δ2(α - *β*      ||             - 4(9*β*129*β* 3(43*β* 148)) +9(3*β*3*β*148)+ 146))δ3(α - *β*           ||
104 |       ||          +18(774*β*888)δ2(α- *β* - 4644δ(α- *β* + 108(129*β* 148)δ(α- *β*      ||
105 |       (                      - 4644(α- *β* + (9*β*β*29*β* 592)+ 292)+                  )
106 |       --*β**β*58*β*-592)+-9(*β*29*β*-592)+-292))-- 8552)δ4 +-4(9*β**β*3*β*-148)+-146)--2138)δ3
107 | = *β*       (              - 4(2322*β* 18(129*β* 3(43*β*148)))δ3(α - *β*          )
108 |             |         13932δ2(α - *β*2 - 72(774*β* 888)δ2(α- *β*+ 27864δ(α - *β*2      |
109 |             ||                       - 216(129*β* 148)δ(α - *β*                    ||
110 |             ||               13932(α - *β*2 + 36(387*β*+ 888*β* 146)δ2+            ||
111 |             |( (*β*322*β* 18(258*β* 592))+ 2(9*β*258*β* 592)+ 9(*β*29*β* 592) + 292)))δ4+ |)
112 |                         8(9*β*29*β* 3(43*β* 148))+ 9(3*β*43*β* 148)+ 146))δ3
113 |
[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 | *β*#x03B1;,1) = 0.275985+ α
139 |                   2
140 |
[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 | *β*#x03B1;,δ) = 0.513216α - 0.025407δ+ 0.296638
150 |
[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 E′(BA,BB)-
160 | E′′(BA,BB) is 161 |
( 18α2 (414*β*#x03B4;- 1)2 + 244(δ - 1)2)+ 8α (1098*β*δ - 1)2 - 108(δ- 1)2)+ )
164 | (  4644*β*δ - 1)4 + 15984*β*δ- 1)4 + 72*β*73δ2 - 718δ + 548) (δ - 1)2 )
165 |                 - 8(1069δ2 + 3439δ- 4011)(δ- 1)2
166 | -(-----2------2-------------2--------2------4-------------4--)--
167 |   7452α (δ- 1) + 8784α((δ-21) + 13932*β*δ- 1)2+ 31968*β*δ - 1)+
168 |                    72 73δ - 718δ+ 548 (δ- 1)
169 |
[10]
170 | 171 | 172 | Equally, we can invert our approximation 9 above, giving: 174 |
177 |
178 | α = 1.9485*β* 0.0495055δ- 0.577999
180 |
[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 |
t2 (δ (t(2t- 3)- 3*β*- 1)2) + 3(t- 1)2x(α- *β*2 + (t- 1)2(δ + t(δ +t(- 3α + 3*β*#x03B4; + 1)- 2δ)))2
195 |
[12]
196 | 197 | and the total error across the curve is 198 | 199 |
             ∫ 1
201 |  E(B  ,B  ) =   (|B  (t)⋅B  (t)|- δ2)2dt
202 |     A   B     0   A      B
203 |   27(14x4 + 15x2 + 14) (α - *β* - 9δ(x+ 1)(α- *β*(3(56*β* 45)+ x(- 78*β* 3(56*β*45)x + 26))
204 |        +9 δ2(α - *β*(566*β* 9*β*x(33x + 20)+ 33)+ 2*β*283x+ 322)+ 4x(85- 6x)- 24)
205 |                     +2 (9*β*3*β*3*β* 148) +146) - 2138)δ3(x + 1)(α - *β*                      + (*β**β**β*129*β* 592) + 292) - 8552)+ 2916)δ4
206 | = --------------------------------------30030--------------------------------------
207 |
[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 |
               ′
215 |   *β*1 = *β*- E-(BA,-BB-)= *β*
216 |              E′′(BA, BB )     (                           )
217 |                        (   4  1161*β*+)3996*β*+ 1314*β*( 2138 δ4    )
218 |              - 54δ(x+ 1) 28x2 -(13x + 28 (α - *β* -)108 14x4 + 15x2 +14 (α - *β*
219 |                    (      - 18 387*β*+888*β* 146 δ3(x + 1)(α - *β*             )
220 |        - 18δ2(α - *β*566*β* 9*β*x(33x+ 20)+ 33)+ 2*β*283x+ 322)+ 4x(85- 6x)- 24 +
221 |                    9δ2(α - *β*(18*β*(33x+ 20)+ 33)+ 2x(283x+ 322)+ 566)
222 |                +27 δ(x + 1)(α - *β*(3(56*β* 45)+ x(- 78*β* 3(56*β* 45)x+ 26))+
223 | -------------------------2(9*β**β*3*β*-148)+-146)--2138)δ3(x-+1)-------------------------
224 |     18(2(387*β*+888*β* 146)δ4 + 18δ(x+ 1)(28x2 - 13x+ 28)(α - *β* + 18(14x4 + 15x2 + 14)
225 |        (α- *β* - 6(129*β* 148)δ3(x + 1)(α - *β* 9δ2(x (33x + 20)+ 33)(α - *β* - 2δ2(α - *β*         (18*β*(33x+ 20)+ 33)+ 2x(283x+ 322)+ 566)- 3δ(x+ 1)(α - *β*3(56*β* 45)+
226 | x(- 78*β* 3(56*β* 45)x+ 26))+ 2(387*β*+ 888*β* 146) δ3(x + 1) + δ2 (566*β* 9*β*x(33x + 20)+ 33)+
227 |                             2*β*283x+ 322)+ 4x(85- 6x)- 24))
228 |
[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 | δ(t) = δs(1 - t)+ δet
250 |
[15]
251 | 252 | 253 | And now our error function is 254 | 255 |
258 |
259 |                   2 2
261 | (|BA (t)⋅BB (t)|- δ(t)) = x
262 |
[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 | 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 | c 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 | b 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 | T 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 | d 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | a 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /images/offset-2.svg: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /images/offset-3.svg: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /images/offset-4.svg: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------