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 ];