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