1 module canvas.path; 2 import canvas.math; 3 4 alias Path = AbstractPath!double; 5 alias FPath = AbstractPath!float; 6 7 enum PathCommand { 8 Move, 9 Line, 10 QuadCurve, 11 CubicCurve, 12 Close, 13 } 14 15 struct AbstractPath(T) { 16 immutable(AbstractVec2!T)[] values; 17 immutable(PathCommand)[] commands; 18 19 this(V)(AbstractPath!V base) { 20 foreach (v; base.values) { 21 values ~= AbstractVec2!T(v); 22 } 23 commands = base.commands; 24 } 25 26 void moveTo(AbstractVec2!T point) { 27 values ~= point; 28 commands ~= PathCommand.Move; 29 } 30 31 void lineTo(AbstractVec2!T point) { 32 values ~= point; 33 commands ~= PathCommand.Line; 34 } 35 36 void curveTo(AbstractVec2!T control, AbstractVec2!T point) { 37 values ~= [control, point]; 38 commands ~= PathCommand.QuadCurve; 39 } 40 41 void curveTo(AbstractVec2!T control1, AbstractVec2!T control2, AbstractVec2!T point) { 42 values ~= [control1, control2, point]; 43 commands ~= PathCommand.CubicCurve; 44 } 45 46 void close() { 47 commands ~= PathCommand.Close; 48 } 49 50 auto opBinary(string op)(const(AbstractPath!T) rhs) const if (op == "~") { 51 Path result; 52 result.values = values ~ rhs.values; 53 result.commands = commands ~ rhs.commands; 54 return result; 55 } 56 57 auto opOpAssign(string op, T)(T value) { 58 this = mixin("this" ~ op ~ "value"); 59 return this; 60 } 61 62 AbstractPath!T flatten() { 63 AbstractPath!T newPath; 64 65 AbstractVec2!T lastPoint, lastMove; 66 size_t pointIndex; 67 foreach (cmd; commands) { 68 final switch (cmd) { 69 case PathCommand.Move: 70 auto point = values[pointIndex++]; 71 newPath.moveTo(point); 72 lastMove = point; 73 lastPoint = point; 74 break; 75 case PathCommand.Close: 76 newPath.close(); 77 lastPoint = lastMove; 78 break; 79 case PathCommand.Line: 80 auto point = values[pointIndex++]; 81 newPath.lineTo(point); 82 lastPoint = point; 83 break; 84 case PathCommand.QuadCurve: 85 auto start = lastPoint; 86 auto control = values[pointIndex++]; 87 auto point = values[pointIndex++]; 88 AbstractVec2!T compute(T alpha) { 89 auto p1 = start * (1 - alpha) + control * alpha; 90 auto p2 = control * (1 - alpha) + point * alpha; 91 auto q = p1 * (1 - alpha) + p2 * alpha; 92 return q; 93 } 94 foreach (i; 0 .. 8) { 95 auto alpha = (i + 1) / 8.0; 96 auto q = compute(alpha); 97 newPath.lineTo(q); 98 lastPoint = q; 99 } 100 break; 101 case PathCommand.CubicCurve: 102 assert(0); 103 } 104 } 105 106 return newPath; 107 } 108 109 // TODO: this seems to break in some cases; try stroking some text to see 110 AbstractPath!T stroke(T distanceOutward, T distanceInward) { 111 AbstractPath!T newPath; 112 113 size_t pointIndex; 114 AbstractVec2!T[] points; 115 116 void removeDuplicatePoints() { 117 while (points.length > 0 && points[0].eq(points[$ - 1], 1e-7)) { 118 points = points[0 .. $ - 1]; 119 } 120 } 121 122 void finishClosedPath() { 123 removeDuplicatePoints(); 124 125 if (points.length < 2) { 126 return; 127 } 128 129 foreach (j; 0 .. 2) { 130 import std.range : iota; 131 import std.math : PI; 132 133 foreach (i; 0 .. points.length) { 134 int factor = j == 0 ? 1 : -1; 135 auto a = points[(cast(int)(i + 0) * factor + cast(int)(2 * points.length)) % cast(int) points.length]; 136 auto b = points[(cast(int)(i + 1) * factor + cast(int)(2 * points.length)) % cast(int) points.length]; 137 auto c = points[(cast(int)(i + 2) * factor + cast(int)(2 * points.length)) % cast(int) points.length]; 138 auto normalAB = (AbstractMat3!T.rotation(PI / 2) * (b - a)).normalize; 139 auto normalBC = (AbstractMat3!T.rotation(PI / 2) * (c - b)).normalize; 140 auto distance = j == 0 ? distanceOutward : distanceInward; 141 auto a2 = a + normalAB * distance; 142 auto ab = b + normalAB * distance; 143 auto bc = b + normalBC * distance; 144 auto c2 = c + normalBC * distance; 145 146 // we wanna find the intersection of r(t) = (a2 + (ab - a2) * t) and r(s) = (c2 + (bc - c2) * s) 147 148 AbstractVec2!T A, B, C, D; 149 A = a2; 150 B = ab - a2; 151 C = c2; 152 D = bc - c2; 153 154 T s = (B.x * A.y + C.x * B.y - A.x * B.y - B.x * C.y) / (B.x * D.y - D.x * B.y); 155 AbstractVec2!T intersection = C + D * s; 156 157 if (i == 0) { 158 newPath.moveTo(intersection); 159 } 160 else { 161 newPath.lineTo(intersection); 162 } 163 } 164 165 newPath.close(); 166 } 167 168 points = []; 169 } 170 171 void finishOpenPath() { 172 if (points.length < 2) { 173 return; 174 } 175 176 foreach (j; 0 .. 2) { 177 import std.range : iota; 178 import std.math : PI; 179 180 if (j == 0) { 181 auto a = points[0]; 182 auto b = points[1]; 183 auto normal = (AbstractMat3!T.rotation(PI / 2) * (b - a)).normalize; 184 auto a2 = a + normal * distanceOutward; 185 186 newPath.moveTo(a2); 187 } 188 else { 189 auto a = points[$ - 1]; 190 auto b = points[$ - 2]; 191 auto normal = (AbstractMat3!T.rotation(PI / 2) * (b - a)).normalize; 192 auto a2 = a + normal * distanceInward; 193 194 newPath.lineTo(a2); 195 } 196 197 foreach (i; (j == 0 ? 0 : 1) .. points.length - (j == 0 ? 2 : 1)) { 198 int factor = j == 0 ? 1 : -1; 199 auto a = points[(cast(int)(i + 0) * factor + cast(int)(2 * points.length)) % cast(int) points.length]; 200 auto b = points[(cast(int)(i + 1) * factor + cast(int)(2 * points.length)) % cast(int) points.length]; 201 auto c = points[(cast(int)(i + 2) * factor + cast(int)(2 * points.length)) % cast(int) points.length]; 202 auto normalAB = (AbstractMat3!T.rotation(PI / 2) * (b - a)).normalize; 203 auto normalBC = (AbstractMat3!T.rotation(PI / 2) * (c - b)).normalize; 204 auto distance = j == 0 ? distanceOutward : distanceInward; 205 auto a2 = a + normalAB * distance; 206 auto ab = b + normalAB * distance; 207 auto bc = b + normalBC * distance; 208 auto c2 = c + normalBC * distance; 209 210 // we wanna find the intersection of r(t) = (a2 + (ab - a2) * t) and r(s) = (c2 + (bc - c2) * s) 211 212 AbstractVec2!T A, B, C, D; 213 A = a2; 214 B = ab - a2; 215 C = c2; 216 D = bc - c2; 217 218 T s = (B.x * A.y + C.x * B.y - A.x * B.y - B.x * C.y) / (B.x * D.y - D.x * B.y); 219 AbstractVec2!T intersection = C + D * s; 220 221 newPath.lineTo(intersection); 222 } 223 224 if (j == 0) { 225 auto a = points[$ - 1]; 226 auto b = points[$ - 2]; 227 auto normal = (AbstractMat3!T.rotation(PI / 2) * (b - a)).normalize; 228 auto a2 = a - normal * distanceOutward; 229 230 newPath.lineTo(a2); 231 } 232 else { 233 auto a = points[0]; 234 auto b = points[1]; 235 auto normal = (AbstractMat3!T.rotation(PI / 2) * (b - a)).normalize; 236 auto a2 = a - normal * distanceInward; 237 238 newPath.lineTo(a2); 239 } 240 } 241 242 newPath.close(); 243 244 points = []; 245 } 246 247 AbstractVec2!T lastPoint; 248 249 foreach (cmd; commands) { 250 final switch (cmd) { 251 case PathCommand.Move: 252 auto point = values[pointIndex++]; 253 finishOpenPath(); 254 points ~= point; 255 lastPoint = point; 256 break; 257 case PathCommand.Close: 258 finishClosedPath(); 259 break; 260 case PathCommand.Line: 261 auto point = values[pointIndex++]; 262 if (!point.eq(lastPoint, 1e-7)) { 263 points ~= point; 264 lastPoint = point; 265 } 266 break; 267 case PathCommand.QuadCurve: 268 case PathCommand.CubicCurve: 269 assert(0); 270 } 271 } 272 273 finishOpenPath(); 274 275 return newPath; 276 } 277 278 AbstractPath!T stroke(double width) { 279 return stroke(width / 2, width / 2); 280 } 281 282 static AbstractPath!T rectangle(AbstractVec2!T position, AbstractVec2!T size) { 283 AbstractPath!T result; 284 result.moveTo(position); 285 result.lineTo(position + AbstractVec2!T(0, size.y)); 286 result.lineTo(position + size); 287 result.lineTo(position + AbstractVec2!T(size.x, 0)); 288 result.close(); 289 return result; 290 } 291 292 }