1 module canvas.canvas; 2 import canvas.backend.common; 3 import canvas.backend.opengl; 4 import canvas.color; 5 import canvas.math; 6 import canvas.path; 7 import std.typecons; 8 9 final class CanvasRenderingContext { 10 11 private { 12 CanvasBackend backend; 13 14 alias backend this; 15 16 Shader vectorShader; 17 Shader subpixelShader; 18 Shader grayscaleShader; 19 Shader blitShader; 20 21 Mesh quad; 22 Mesh buffer; 23 24 void delegate() makeContextCurrent; 25 } 26 27 this(void delegate() makeContextCurrent) { 28 this.makeContextCurrent = makeContextCurrent; 29 makeContextCurrent(); 30 backend = new OpenGLBackend(); 31 vectorShader = backend.shader([ 32 ShaderSource(ShaderType.Vertex, import("vector.vert.glsl")), 33 ShaderSource(ShaderType.Fragment, import("vector.frag.glsl")), 34 ]); 35 subpixelShader = backend.shader([ 36 ShaderSource(ShaderType.Vertex, import("identity.vert.glsl")), 37 ShaderSource(ShaderType.Fragment, import("subpixel.frag.glsl")), 38 ]); 39 grayscaleShader = backend.shader([ 40 ShaderSource(ShaderType.Vertex, import("identity.vert.glsl")), 41 ShaderSource(ShaderType.Fragment, import("grayscale.frag.glsl")), 42 ]); 43 blitShader = backend.shader([ 44 ShaderSource(ShaderType.Vertex, import("identity.vert.glsl")), 45 ShaderSource(ShaderType.Fragment, import("blit.frag.glsl")), 46 ]); 47 48 VertexFormat quadVertexFormat; 49 quadVertexFormat.add("aPos", AttributeType.FVec2); 50 quadVertexFormat.add("aUv", AttributeType.FVec2); 51 quad = backend.mesh(quadVertexFormat, MeshUsage.Static); 52 quad.upload(cast(void[]) [ 53 -1.0f, -1.0f, 0.0f, 0.0f, 54 1.0f, -1.0f, 1.0f, 0.0f, 55 1.0f, 1.0f, 1.0f, 1.0f, 56 -1.0f, 1.0f, 0.0f, 1.0f, 57 ]); 58 59 VertexFormat bufferVertexFormat; 60 bufferVertexFormat.add("aPos", AttributeType.FVec2); 61 bufferVertexFormat.add("aUv", AttributeType.FVec2); 62 buffer = backend.mesh(bufferVertexFormat, MeshUsage.Dynamic); 63 } 64 65 void dispose() { 66 backend.dispose(); 67 } 68 69 } 70 71 enum FillRule { 72 NonZero, 73 EvenOdd, 74 } 75 76 enum PixelOrder { 77 Flat, 78 HorizontalRGB, 79 HorizontalBGR, 80 VerticalRGB, 81 VerticalBGR, 82 } 83 84 struct LcdPixelLayout { 85 PixelOrder order = PixelOrder.HorizontalRGB; 86 double contrast = 1.0; 87 } 88 89 enum Antialias { 90 91 None, 92 93 Grayscale, 94 95 /** 96 97 Uses subpixel antialiasing. 98 99 This antialiasing mode requires the entire canvas to be opaque; unintended visual artifacts may occur if this is not the case. 100 101 This will also ignore any blending mode specified and use $(REF BlendingMode.Normal). 102 103 */ 104 Subpixel, 105 106 } 107 108 struct FillOptions { 109 Color tint = Color(0, 0, 0); 110 Mat3 transform; 111 FillRule fillRule = FillRule.NonZero; 112 Antialias antialias = Antialias.Grayscale; 113 // BlendingMode blendingMode = BlendingMode.Normal; 114 LcdPixelLayout subpixelLayout; 115 Material material; 116 Mat3 materialTransform; 117 Canvas texture; 118 Mat3 textureTransform; 119 } 120 121 final class Material { 122 private: 123 Shader grayscale; 124 Shader subpixel; 125 } 126 127 final class Canvas { 128 129 private CanvasRenderingContext _context; 130 131 /** The primary framebuffer onto which canvas operations render */ 132 private Framebuffer target; 133 134 /** A framebuffer used temporarily during drawing operations by the canvas implementation */ 135 private Framebuffer temp; 136 137 private IVec2 _size; 138 139 this(CanvasRenderingContext context, IVec2 size) { 140 this._context = context; 141 this.size = size; 142 } 143 144 inout(CanvasRenderingContext) context() inout @property { 145 return _context; 146 } 147 148 Material createMaterial(string source) { 149 import std.array : replaceFirst; 150 151 context.makeContextCurrent(); 152 153 Material result = new Material; 154 result.grayscale = context.shader([ 155 ShaderSource(ShaderType.Vertex, import("identity.vert.glsl")), 156 ShaderSource(ShaderType.Fragment, import("custom-grayscale.frag.glsl").replaceFirst("//%mainImage%", source)), 157 ]); 158 result.subpixel = context.shader([ 159 ShaderSource(ShaderType.Vertex, import("identity.vert.glsl")), 160 ShaderSource(ShaderType.Fragment, import("custom-subpixel.frag.glsl").replaceFirst("//%mainImage%", source)), 161 ]); 162 return result; 163 } 164 165 IVec2 size() { 166 return _size; 167 } 168 169 /** Resizes the canvas. The contents of the canvas after this operation are unspecified */ 170 void size(IVec2 newSize) { 171 if (newSize.x < 1) newSize.x = 1; 172 if (newSize.y < 1) newSize.y = 1; 173 174 if (newSize == _size) { 175 return; 176 } 177 178 if (target) { 179 target.dispose(); 180 temp.dispose(); 181 } 182 183 scope (failure) { 184 target = null; 185 temp = null; 186 } 187 188 context.makeContextCurrent(); 189 190 target = context.framebuffer(newSize); 191 temp = context.framebuffer(newSize); 192 193 _size = newSize; 194 } 195 196 void clear(Color color) { 197 context.makeContextCurrent(); 198 199 context.renderTarget = target; 200 context.clearColor(color); 201 } 202 203 void fill(Path path, FillOptions options) { 204 import std.math : floor, ceil; 205 import std.algorithm : map; 206 207 struct Vertex { 208 Vec2 point; 209 Vec2 uv; 210 } 211 212 auto points = path.values.map!(x => options.transform * x); 213 if (points.length == 0) { 214 return; 215 } 216 217 context.makeContextCurrent(); 218 219 context.renderTarget = target; 220 context.viewport(IVec2(0, 0), size); 221 222 try { 223 context.vectorShader.setUniform("uViewportSize", cast(FVec2) size); 224 } 225 catch (OpenGLException e) { // TODO: wtf???? sometimes the above just *fails* but retrying seems to work 100% of the time 226 context.vectorShader.setUniform("uViewportSize", cast(FVec2) size); 227 } 228 229 double minX = double.infinity; 230 double maxX = -double.infinity; 231 double minY = double.infinity; 232 double maxY = -double.infinity; 233 foreach (point; points) { 234 if (point.x < minX) minX = point.x; 235 if (point.x > maxX) maxX = point.x; 236 if (point.y < minY) minY = point.y; 237 if (point.y > maxY) maxY = point.y; 238 } 239 minX = floor(minX - 1); 240 minY = floor(minY - 1); 241 maxX = ceil(maxX + 1); 242 maxY = ceil(maxY + 1); 243 244 Vertex[3][] geometry; 245 246 size_t pointIndex; 247 Vec2 lastMove; 248 Vec2 lastPoint; 249 foreach (cmd; path.commands) { 250 final switch (cmd) { 251 case PathCommand.Move: 252 auto point = points[pointIndex++]; 253 lastMove = point; 254 lastPoint = point; 255 break; 256 case PathCommand.Close: 257 lastPoint = lastMove; 258 break; 259 case PathCommand.Line: 260 auto point = points[pointIndex++]; 261 geometry ~= [ 262 Vertex(lastMove, Vec2()), 263 Vertex(lastPoint, Vec2()), 264 Vertex(point, Vec2()), 265 ]; 266 lastPoint = point; 267 break; 268 case PathCommand.QuadCurve: 269 auto control = points[pointIndex++]; 270 auto point = points[pointIndex++]; 271 geometry ~= [ 272 Vertex(lastMove, Vec2()), 273 Vertex(lastPoint, Vec2()), 274 Vertex(point, Vec2()), 275 ]; 276 geometry ~= [ 277 Vertex(lastPoint, Vec2(0, 0)), 278 Vertex(control, Vec2(1, 0)), 279 Vertex(point, Vec2(0, 1)), 280 ]; 281 lastPoint = point; 282 break; 283 case PathCommand.CubicCurve: 284 auto start = lastPoint; 285 auto control1 = points[pointIndex++]; 286 auto control2 = points[pointIndex++]; 287 auto point = points[pointIndex++]; 288 Vec2 compute(double alpha) { 289 auto p1 = start * (1 - alpha) + control1 * alpha; 290 auto p2 = control1 * (1 - alpha) + control2 * alpha; 291 auto p3 = control2 * (1 - alpha) + point * alpha; 292 auto q1 = p1 * (1 - alpha) + p2 * alpha; 293 auto q2 = p2 * (1 - alpha) + p3 * alpha; 294 return q1 * (1 - alpha) + q2 * alpha; 295 } 296 foreach (i; 0 .. 64) { 297 auto alpha = (i + 1) / 64.0; 298 auto r = compute(alpha); 299 auto c = compute((i + 0.5) / 64.0); // TODO: more accurate rendering 300 geometry ~= [ 301 Vertex(lastMove, Vec2()), 302 Vertex(lastPoint, Vec2()), 303 Vertex(r, Vec2()), 304 ]; 305 geometry ~= [ 306 Vertex(lastPoint, Vec2(0, 0)), 307 Vertex(c, Vec2(1, 0)), 308 Vertex(r, Vec2(0, 1)), 309 ]; 310 lastPoint = r; 311 } 312 break; 313 } 314 } 315 316 FVec2[] data; 317 318 foreach (triangle; geometry) { 319 data ~= FVec2(triangle[0].point); 320 data ~= FVec2(triangle[0].uv); 321 data ~= FVec2(triangle[1].point); 322 data ~= FVec2(triangle[1].uv); 323 data ~= FVec2(triangle[2].point); 324 data ~= FVec2(triangle[2].uv); 325 } 326 327 // cover 328 data ~= FVec2(minX, minY); 329 data ~= FVec2(0, 0); 330 data ~= FVec2(minX, maxY); 331 data ~= FVec2(0, 0); 332 data ~= FVec2(maxX, maxY); 333 data ~= FVec2(0, 0); 334 data ~= FVec2(maxX, minY); 335 data ~= FVec2(0, 0); 336 337 // subpixel cover 338 data ~= FVec2(minX, minY) / FVec2(size) * FVec2(2, -2) + FVec2(-1, 1); 339 data ~= FVec2(minX, minY) / FVec2(size) * FVec2(1, -1); 340 data ~= FVec2(minX, maxY) / FVec2(size) * FVec2(2, -2) + FVec2(-1, 1); 341 data ~= FVec2(minX, maxY) / FVec2(size) * FVec2(1, -1); 342 data ~= FVec2(maxX, maxY) / FVec2(size) * FVec2(2, -2) + FVec2(-1, 1); 343 data ~= FVec2(maxX, maxY) / FVec2(size) * FVec2(1, -1); 344 data ~= FVec2(maxX, minY) / FVec2(size) * FVec2(2, -2) + FVec2(-1, 1); 345 data ~= FVec2(maxX, minY) / FVec2(size) * FVec2(1, -1); 346 347 context.buffer.upload(cast(void[]) data); 348 349 context.renderTarget = temp; 350 context.clearColor(Color(0, 0, 0, 1)); 351 context.clearStencil(0x00); 352 context.blend = BlendingFunctions.Add; 353 354 const(Sample)[] samples; 355 356 if (options.subpixelLayout.order == PixelOrder.Flat 357 && options.antialias == Antialias.Subpixel) { 358 options.antialias = Antialias.Grayscale; 359 } 360 361 final switch (options.antialias) { 362 case Antialias.None: 363 samples = aliasedSamples; 364 break; 365 case Antialias.Grayscale: 366 samples = grayscaleSamples; 367 break; 368 case Antialias.Subpixel: 369 samples = rgbSubpixelSamples; 370 break; 371 } 372 373 foreach (sample; samples) { 374 if (options.fillRule == FillRule.EvenOdd) { 375 Stencil stencil; 376 stencil.pass = StencilOp.Inv; 377 context.stencil = stencil; 378 } 379 else { 380 Stencil stencilFront, stencilBack; 381 stencilFront.pass = StencilOp.IncWrap; 382 stencilBack.pass = StencilOp.DecWrap; 383 context.stencilSeparate(stencilFront, stencilBack); 384 } 385 386 context.colorWriteMask(false, false, false, false); 387 388 context.vectorShader.setUniform("uColor", sample.color); 389 context.vectorShader.setUniform("uTranslate", -sample.translate); 390 391 context.draw(DrawMode.Triangles, context.vectorShader, context.buffer, 0, geometry.length * 3); 392 393 context.colorWriteMask(true, true, true, true); 394 Stencil stencil; 395 stencil.func = StencilFunction.Neq; 396 stencil.refValue = 0; 397 stencil.stencilFail = StencilOp.Zero; 398 stencil.depthFail = StencilOp.Zero; 399 stencil.pass = StencilOp.Zero; 400 context.stencil = stencil; 401 context.draw(DrawMode.TriangleFan, context.vectorShader, context.buffer, geometry.length * 3, 4); 402 } 403 404 context.renderTarget = target; 405 context.stencil = Stencil.init; 406 407 Shader shader; 408 409 final switch (options.antialias) { 410 case Antialias.Subpixel: 411 context.blend = BlendingFunctions.Overwrite; 412 shader = options.material ? options.material.subpixel : context.subpixelShader; 413 shader.setUniform("uViewportSize", cast(FVec2) size); 414 shader.setUniform("uContrastFactor", options.subpixelLayout.contrast); 415 shader.setUniform("uTarget", target); 416 break; 417 case Antialias.None: 418 case Antialias.Grayscale: 419 context.blend = BlendingFunctions.Normal; 420 shader = options.material ? options.material.grayscale : context.grayscaleShader; 421 break; 422 } 423 424 shader.setUniform("uColor", options.tint); 425 shader.setUniform("uSource", temp); 426 427 if (options.texture !is null) { 428 shader.setUniform("uTextureEnabled", 1); 429 430 if (options.texture.context !is context) { 431 throw new Exception("Cannot use canvas from another context as texture"); 432 } 433 434 shader.setUniform("uTexture", options.texture.target); 435 } 436 else { 437 shader.setUniform("uTextureEnabled", 0); 438 } 439 440 shader.setUniform("uTextureTransform", FMat3(options.textureTransform)); 441 442 if (options.material !is null) { 443 shader.setUniform("uMaterialTransform", FMat3(options.materialTransform)); 444 } 445 446 // shader.setUniform("uTexture", fill.texture); 447 // shader.setUniform("uTextureTransform", FMat3(fill.textureTransform)); 448 // shader.setUniform("uTextureEnabled", fill.texture ? 1 : 0); 449 450 context.draw(DrawMode.TriangleFan, shader, context.buffer, geometry.length * 3 + 4, 4); 451 452 if (options.texture !is null) { 453 shader.setUniform("uTexture", null); // TODO: figure out why this is needed 454 // otherwise, if you draw a canvas onto another canvas and you resize the former canvas, it crashes 455 } 456 } 457 458 void fillIRect(IVec2 pos, IVec2 size, FillOptions options) { 459 import std.math : floor, ceil; 460 import std.algorithm : map; 461 462 context.makeContextCurrent(); 463 464 context.renderTarget = target; 465 context.viewport(IVec2(0, 0), this.size); 466 467 double minX = pos.x, minY = pos.y; 468 double maxX = pos.x + size.x, maxY = pos.y + size.y; 469 470 FVec2[] data; 471 472 data ~= FVec2(minX, minY) / FVec2(this.size) * FVec2(2, -2) + FVec2(-1, 1); 473 data ~= FVec2(minX, minY) / FVec2(this.size) * FVec2(1, -1); 474 data ~= FVec2(minX, maxY) / FVec2(this.size) * FVec2(2, -2) + FVec2(-1, 1); 475 data ~= FVec2(minX, maxY) / FVec2(this.size) * FVec2(1, -1); 476 data ~= FVec2(maxX, maxY) / FVec2(this.size) * FVec2(2, -2) + FVec2(-1, 1); 477 data ~= FVec2(maxX, maxY) / FVec2(this.size) * FVec2(1, -1); 478 data ~= FVec2(maxX, minY) / FVec2(this.size) * FVec2(2, -2) + FVec2(-1, 1); 479 data ~= FVec2(maxX, minY) / FVec2(this.size) * FVec2(1, -1); 480 481 context.buffer.upload(cast(void[]) data); 482 483 context.renderTarget = temp; 484 context.clearColor(Color(1, 0, 0, 1)); 485 context.renderTarget = target; 486 context.stencil = Stencil.init; 487 488 Shader shader; 489 490 context.blend = BlendingFunctions.Normal; 491 shader = options.material ? options.material.grayscale : context.grayscaleShader; 492 493 shader.setUniform("uColor", options.tint); 494 shader.setUniform("uSource", temp); 495 496 if (options.texture !is null) { 497 shader.setUniform("uTextureEnabled", 1); 498 499 if (options.texture.context !is context) { 500 throw new Exception("Cannot use canvas from another context as texture"); 501 } 502 503 shader.setUniform("uTexture", options.texture.target); 504 } 505 else { 506 shader.setUniform("uTextureEnabled", 0); 507 } 508 509 shader.setUniform("uTextureTransform", FMat3(options.textureTransform)); 510 511 if (options.material !is null) { 512 shader.setUniform("uMaterialTransform", FMat3(options.materialTransform)); 513 } 514 515 context.draw(DrawMode.TriangleFan, shader, context.buffer, 0, 4); 516 517 if (options.texture !is null) { 518 shader.setUniform("uTexture", null); // TODO: figure out why this is needed 519 // otherwise, if you draw a canvas onto another canvas and you resize the former canvas, it crashes 520 } 521 } 522 523 } 524 525 void blitToScreen(Canvas canvas, IVec2 viewportSize) { 526 canvas.context.viewport(IVec2(0, 0), viewportSize); 527 canvas.context.renderTarget = null; 528 529 canvas.context.blitShader.setUniform("uTexture", canvas.target); 530 531 canvas.context.draw(DrawMode.TriangleFan, canvas.context.blitShader, canvas.context.quad, 0, 4); 532 } 533 534 private: 535 536 struct Sample { 537 FVec2 translate; 538 FVec4 color; 539 } 540 541 immutable(Sample[]) rgbSubpixelSamples = [ 542 // near subpixel 543 Sample(FVec2(1 - 5.5 / 12.0, 0.5 / 4.0), FVec4(0.25, 0, 0, 0)), 544 Sample(FVec2(1 - 4.5 / 12.0, -1.5 / 4.0), FVec4(0.25, 0, 0, 0)), 545 Sample(FVec2(1 - 3.5 / 12.0, 1.5 / 4.0), FVec4(0.25, 0, 0, 0)), 546 Sample(FVec2(1 - 2.5 / 12.0, -0.5 / 4.0), FVec4(0.25, 0, 0, 0)), 547 548 // center subpixel 549 Sample(FVec2(-1.5 / 12.0, 0.5 / 4.0), FVec4(0, 0.25, 0, 0)), 550 Sample(FVec2(-0.5 / 12.0, -1.5 / 4.0), FVec4(0, 0.25, 0, 0)), 551 Sample(FVec2( 0.5 / 12.0, 1.5 / 4.0), FVec4(0, 0.25, 0, 0)), 552 Sample(FVec2( 1.5 / 12.0, -0.5 / 4.0), FVec4(0, 0.25, 0, 0)), 553 554 // far subpixel 555 Sample(FVec2( 2.5 / 12.0, 0.5 / 4.0), FVec4(0, 0, 0.25, 0)), 556 Sample(FVec2( 3.5 / 12.0, -1.5 / 4.0), FVec4(0, 0, 0.25, 0)), 557 Sample(FVec2( 4.5 / 12.0, 1.5 / 4.0), FVec4(0, 0, 0.25, 0)), 558 Sample(FVec2( 5.5 / 12.0, -0.5 / 4.0), FVec4(0, 0, 0.25, 0)), 559 ]; 560 561 immutable(Sample[]) grayscaleSamples = [ 562 Sample(FVec2(-3.5 / 8.0, -1.5 / 8.0), FVec4(0.125, 0, 0, 0)), 563 Sample(FVec2(-2.5 / 8.0, 2.5 / 8.0), FVec4(0.125, 0, 0, 0)), 564 Sample(FVec2(-1.5 / 8.0, -2.5 / 8.0), FVec4(0.125, 0, 0, 0)), 565 Sample(FVec2(-0.5 / 8.0, 0.5 / 8.0), FVec4(0.125, 0, 0, 0)), 566 Sample(FVec2( 0.5 / 8.0, -3.5 / 8.0), FVec4(0.125, 0, 0, 0)), 567 Sample(FVec2( 1.5 / 8.0, 3.5 / 8.0), FVec4(0.125, 0, 0, 0)), 568 Sample(FVec2( 2.5 / 8.0, -0.5 / 8.0), FVec4(0.125, 0, 0, 0)), 569 Sample(FVec2( 3.5 / 8.0, 1.5 / 8.0), FVec4(0.125, 0, 0, 0)), 570 ]; 571 572 immutable(Sample[]) aliasedSamples = [ 573 Sample(FVec2(0, 0), FVec4(1, 0, 0, 0)), 574 ];