Skip to content

Pixels

Pixels module.

Example

Draw a red rectangle in the centre of the screen.

import taichi as ti
from tolvera import Tolvera, run

def main(**kwargs):
    tv = Tolvera(**kwargs)

    @ti.kernel
    def draw():
        w = 100
        tv.px.rect(tv.x/2-w/2, tv.y/2-w/2, w, w, ti.Vector([1., 0., 0., 1.]))

    @tv.render
    def _():
        tv.p()
        draw()
        return tv.px

if __name__ == '__main__':
    run(main)

Pixels

Pixels class for drawing pixels to the screen.

This class is used to draw pixels to the screen. It contains methods for drawing points, lines, rectangles, circles, triangles, and polygons. It also contains methods for blending pixels together, flipping pixels, inverting pixels, and diffusing, decaying and clearing pixels.

It tries to follow a similar API to the Processing library.

Source code in src/tolvera/pixels.py
 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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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
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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
@ti.data_oriented
class Pixels:
    """Pixels class for drawing pixels to the screen.

    This class is used to draw pixels to the screen. It contains methods for drawing
    points, lines, rectangles, circles, triangles, and polygons. It also contains
    methods for blending pixels together, flipping pixels, inverting pixels, and
    diffusing, decaying and clearing pixels.

    It tries to follow a similar API to the Processing library.
    """
    def __init__(self, tolvera, **kwargs):
        """Initialise Pixels

        Args:
            tolvera (Tolvera): Tölvera instance.
            **kwargs: Keyword arguments.
                polygon_mode (str): Polygon mode. Defaults to "crossing".
                brightness (float): Brightness. Defaults to 1.0. 
        """
        self.tv = tolvera
        self.kwargs = kwargs
        self.polygon_mode = kwargs.get("polygon_mode", "crossing")
        self.x = self.tv.x
        self.y = self.tv.y
        self.px = Pixel.field(shape=(self.x, self.y))
        brightness = kwargs.get("brightness", 1.0)
        self.CONSTS = CONSTS(
            {
                "BRIGHTNESS": (ti.f32, brightness),
            }
        )
        self.shape_enum = {
            "point": 0,
            "line": 1,
            "rect": 2,
            "circle": 3,
            "triangle": 4,
            "polygon": 5,
        }


    def set(self, px: Any):
        """Set pixels.

        Args:
            px (Any): Pixels to set. Can be Pixels, StructField, MatrixField, etc (see rgba_from_px).
        """
        self.px.rgba = self.rgba_from_px(px)

    @ti.kernel
    def k_set(self, px: ti.template()):
        for x, y in ti.ndrange(self.x, self.y):
            self.px.rgba[x, y] = px.px.rgba[x, y]

    @ti.kernel
    def f_set(self, px: ti.template()):
        for x, y in ti.ndrange(self.x, self.y):
            self.px.rgba[x, y] = px.px.rgba[x, y]

    def get(self):
        """Get pixels."""
        return self.px

    @ti.kernel
    def clear(self):
        """Clear pixels."""
        self.px.rgba.fill(0)

    @ti.kernel
    def diffuse(self, evaporate: ti.f32):
        """Diffuse pixels.

        Args:
            evaporate (float): Evaporation rate.
        """
        for i, j in ti.ndrange(self.x, self.y):
            d = ti.Vector([0.0, 0.0, 0.0, 0.0])
            for di in ti.static(range(-1, 2)):
                for dj in ti.static(range(-1, 2)):
                    dx = (i + di) % self.x
                    dy = (j + dj) % self.y
                    d += self.px.rgba[dx, dy]
            d *= 0.99 / 9.0
            self.px.rgba[i, j] = d

    @ti.func
    def background(self, r: ti.f32, g: ti.f32, b: ti.f32):
        """Set background colour.

        Args:
            r (ti.f32): Red.
            g (ti.f32): Green.
            b (ti.f32): Blue.
        """
        bg = ti.Vector([r, g, b, 1.0])
        self.rect(0, 0, self.x, self.y, bg)

    @ti.func
    def point(self, x: ti.i32, y: ti.i32, rgba: vec4):
        """Draw point.

        Args:
            x (ti.i32): X position.
            y (ti.i32): Y position.
            rgba (vec4): Colour.
        """
        self.px.rgba[x, y] = rgba

    @ti.func
    def points(self, x: ti.template(), y: ti.template(), rgba: vec4):
        """Draw points with the same colour.

        Args:
            x (ti.template): X positions.
            y (ti.template): Y positions.
            rgba (vec4): Colour.
        """
        for i in ti.static(range(len(x))):
            self.point(x[i], y[i], rgba)

    @ti.func
    def rect(self, x: ti.i32, y: ti.i32, w: ti.i32, h: ti.i32, rgba: vec4):
        """Draw a filled rectangle.

        Args:
            x (ti.i32): X position.
            y (ti.i32): Y position.
            w (ti.i32): Width.
            h (ti.i32): Height.
            rgba (vec4): Colour.
        """
        # TODO: fill arg
        # TODO: gradients, lerp with ti.math.mix(x, y, a)
        for i, j in ti.ndrange(w, h):
            self.px.rgba[x + i, y + j] = rgba

    @ti.func
    def plot(self, x, y, c, rgba):
        """Set the pixel color with blending."""
        self.px.rgba[x, y] = self.px.rgba[x, y] * (1 - c) + rgba * c

    @ti.func
    def ipart(self, x):
        return ti.math.floor(x)

    @ti.func
    def round(self, x):
        return self.ipart(x + 0.5)

    @ti.func
    def fpart(self, x):
        return x - ti.math.floor(x)

    @ti.func
    def rfpart(self, x):
        return 1 - self.fpart(x)

    @ti.func
    def line(self, x0: ti.f32, y0: ti.f32, x1: ti.f32, y1: ti.f32, rgba: vec4):
        """Draw an anti-aliased line using Xiaolin Wu's algorithm."""
        steep = ti.abs(y1 - y0) > ti.abs(x1 - x0)
        if steep:
            x0, y0 = y0, x0
            x1, y1 = y1, x1

        if x0 > x1:
            x0, x1 = x1, x0
            y0, y1 = y1, y0

        dx = x1 - x0
        dy = y1 - y0
        gradient = dy / dx if dx != 0 else 1.0

        xend = ti.math.round(x0)
        yend = y0 + gradient * (xend - x0)
        xgap = self.rfpart(x0 + 0.5)
        xpxl1 = int(xend)
        ypxl1 = int(self.ipart(yend))
        if steep:
            self.plot(ypxl1, xpxl1, self.rfpart(yend) * xgap, rgba)
            self.plot(ypxl1 + 1, xpxl1, self.fpart(yend) * xgap, rgba)
        else:
            self.plot(xpxl1, ypxl1, self.rfpart(yend) * xgap, rgba)
            self.plot(xpxl1, ypxl1 + 1, self.fpart(yend) * xgap, rgba)

        intery = yend + gradient

        xend = ti.math.round(x1)
        yend = y1 + gradient * (xend - x1)
        xgap = self.fpart(x1 + 0.5)
        xpxl2 = int(xend)
        ypxl2 = int(self.ipart(yend))
        if steep:
            self.plot(ypxl2, xpxl2, self.rfpart(yend) * xgap, rgba)
            self.plot(ypxl2 + 1, xpxl2, self.fpart(yend) * xgap, rgba)
        else:
            self.plot(xpxl2, ypxl2, self.rfpart(yend) * xgap, rgba)
            self.plot(xpxl2, ypxl2 + 1, self.fpart(yend) * xgap, rgba)

        if steep:
            for x in range(xpxl1 + 1, xpxl2):
                self.plot(int(self.ipart(intery)), x, self.rfpart(intery), rgba)
                self.plot(int(self.ipart(intery)) + 1, x, self.fpart(intery), rgba)
                intery += gradient
        else:
            for x in range(xpxl1 + 1, xpxl2):
                self.plot(x, int(self.ipart(intery)), self.rfpart(intery), rgba)
                self.plot(x, int(self.ipart(intery)) + 1, self.fpart(intery), rgba)
                intery += gradient

    # @ti.func
    # def line(self, x0: ti.i32, y0: ti.i32, x1: ti.i32, y1: ti.i32, rgba: vec4):
    #     """Draw a line using Bresenham's algorithm.

    #     Args:
    #         x0 (ti.i32): X start position.
    #         y0 (ti.i32): Y start position.
    #         x1 (ti.i32): X end position.
    #         y1 (ti.i32): Y end position.
    #         rgba (vec4): Colour.

    #     TODO: thickness
    #     TODO: anti-aliasing
    #     TODO: should lines wrap around (as two lines)?
    #     """
    #     dx = ti.abs(x1 - x0)
    #     dy = ti.abs(y1 - y0)
    #     x, y = x0, y0
    #     sx = -1 if x0 > x1 else 1
    #     sy = -1 if y0 > y1 else 1
    #     if dx > dy:
    #         err = dx / 2.0
    #         while x != x1:
    #             self.px.rgba[x, y] = rgba
    #             err -= dy
    #             if err < 0:
    #                 y += sy
    #                 err += dx
    #             x += sx
    #     else:
    #         err = dy / 2.0
    #         while y != y1:
    #             self.px.rgba[x, y] = rgba
    #             err -= dx
    #             if err < 0:
    #                 x += sx
    #                 err += dy
    #             y += sy
    #     self.px.rgba[x, y] = rgba

    @ti.func
    def lines(self, points: ti.template(), rgba: vec4):
        """Draw lines with the same colour.

        Args:
            points (ti.template): Points.
            rgba (vec4): Colour.
        """
        for i in range(points.shape[0] - 1):
            self.line(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1], rgba)

    @ti.func
    def circle(self, x: ti.i32, y: ti.i32, r: ti.i32, rgba: vec4):
        """Draw a filled circle.

        Args:
            x (ti.i32): X position.
            y (ti.i32): Y position.
            r (ti.i32): Radius.
            rgba (vec4): Colour.
        """
        for i in range(r + 1):
            d = ti.sqrt(r**2 - i**2)
            d_int = ti.cast(d, ti.i32)
            # TODO: parallelise ?
            for j in range(d_int):
                self.px.rgba[x + i, y + j] = rgba
                self.px.rgba[x + i, y - j] = rgba
                self.px.rgba[x - i, y - j] = rgba
                self.px.rgba[x - i, y + j] = rgba

    @ti.func
    def circles(self, x: ti.template(), y: ti.template(), r: ti.template(), rgba: vec4):
        """Draw circles with the same colour.

        Args:
            x (ti.template): X positions.
            y (ti.template): Y positions.
            r (ti.template): Radii.
            rgba (vec4): Colour.
        """
        for i in ti.static(range(len(x))):
            self.circle(x[i], y[i], r[i], rgba)

    @ti.func
    def triangle(self, a, b, c, rgba: vec4):
        """Draw a filled triangle.

        Args:
            a (vec2): Point A.
            b (vec2): Point B.
            c (vec2): Point C.
            rgba (vec4): Colour.
        """
        # TODO: fill arg
        x = ti.Vector([a[0], b[0], c[0]])
        y = ti.Vector([a[1], b[1], c[1]])
        self.polygon(x, y, rgba)

    @ti.func
    def polygon(self, x: ti.template(), y: ti.template(), rgba: vec4):
        """Draw a filled polygon.

        Polygons are drawn according to the polygon mode, which can be "crossing" 
        (default) or "winding". First, the bounding box of the polygon is calculated.
        Then, we check if each pixel in the bounding box is inside the polygon. If it
        is, we draw it (along with each neighbour pixel).

        Reference for point in polygon inclusion testing:
        http://www.dgp.toronto.edu/~mac/e-stuff/point_in_polygon.py

        Args:
            x (ti.template): X positions.
            y (ti.template): Y positions.
            rgba (vec4): Colour.

        TODO: fill arg
        """
        x_min, x_max = ti.cast(x.min(), ti.i32), ti.cast(x.max(), ti.i32)
        y_min, y_max = ti.cast(y.min(), ti.i32), ti.cast(y.max(), ti.i32)
        l = len(x)
        for i, j in ti.ndrange(x_max - x_min, y_max - y_min):
            p = ti.Vector([x_min + i, y_min + j])
            if self._is_inside(p, x, y, l) != 0:
                # TODO: abstract out, weight?
                """
                x-1,y-1  x,y-1  x+1,y-1
                x-1,y    x,y    x+1,y
                x-1,y+1  x,y+1  x+1,y+1
                """
                _x, _y = p[0], p[1]
                self.px.rgba[_x - 1, _y - 1] = rgba
                self.px.rgba[_x - 1, _y] = rgba
                self.px.rgba[_x - 1, _y + 1] = rgba

                self.px.rgba[_x, _y - 1] = rgba
                self.px.rgba[_x, _y] = rgba
                self.px.rgba[_x, _y + 1] = rgba

                self.px.rgba[_x + 1, _y - 1] = rgba
                self.px.rgba[_x + 1, _y] = rgba
                self.px.rgba[_x + 1, _y + 1] = rgba

    @ti.func
    def _is_inside(self, p: vec2, x: ti.template(), y: ti.template(), l: ti.i32):
        """Check if point is inside polygon.

        Args:
            p (vec2): Point.
            x (ti.template): X positions.
            y (ti.template): Y positions.
            l (ti.i32): Number of points.

        Returns:
            int: 1 if inside, 0 if outside.
        """
        is_inside = 0
        if self.polygon_mode == "crossing":
            is_inside = self._is_inside_crossing(p, x, y, l)
        elif self.polygon_mode == "winding":
            is_inside = self._is_inside_winding(p, x, y, l)
        return is_inside

    @ti.func
    def _is_inside_crossing(self, p: vec2, x: ti.template(), y: ti.template(), l: ti.i32):
        """Check if point is inside polygon using crossing number algorithm.

        Args:
            p (vec2): Point.
            x (ti.template): X positions.
            y (ti.template): Y positions.
            l (ti.i32): Number of points.

        Returns:
            int: 1 if inside, 0 if outside.
        """
        n = 0
        v0, v1 = ti.Vector([0.0, 0.0]), ti.Vector([0.0, 0.0])
        for i in range(l):
            i1 = i + 1 if i < l - 1 else 0
            v0, v1 = [x[i], y[i]], [x[i1], y[i1]]
            if (v0[1] <= p[1] and v1[1] > p[1]) or (v0[1] > p[1] and v1[1] <= p[1]):
                vt = (p[1] - v0[1]) / (v1[1] - v0[1])
                if p[0] < v0[0] + vt * (v1[0] - v0[0]):
                    n += 1
        return n % 2

    @ti.func
    def _is_inside_winding(self, p: vec2, x: ti.template(), y: ti.template(), l: ti.i32):
        """Check if point is inside polygon using winding number algorithm.

        Args:
            p (vec2): Point.
            x (ti.template): X positions.
            y (ti.template): Y positions.
            l (ti.i32): Number of points.

        Returns:
            int: 1 if inside, 0 if outside.
        """
        n = 0
        v0, v1 = ti.Vector([0.0, 0.0]), ti.Vector([0.0, 0.0])
        for i in range(l):
            i1 = i + 1 if i < l - 1 else 0
            v0, v1 = [x[i], y[i]], [x[i1], y[i1]]
            if v0[1] <= p[1] and v1[1] > p[1] and (v0 - v1).cross(p - v1) > 0:
                n += 1
            elif v1[1] <= p[1] and (v0 - v1).cross(p - v1) < 0:
                n -= 1
        return n

    @ti.kernel
    def flip_x(self):
        """Flip image in x-axis."""
        for i, j in ti.ndrange(self.x, self.y):
            self.px.rgba[i, j] = self.px.rgba[self.x - 1 - i, j]

    @ti.kernel
    def flip_y(self):
        """Flip image in y-axis."""
        for i, j in ti.ndrange(self.x, self.y):
            self.px.rgba[i, j] = self.px.rgba[i, self.y - 1 - j]

    @ti.kernel
    def invert(self):
        """Invert image."""
        for i, j in ti.ndrange(self.x, self.y):
            self.px.rgba[i, j] = 1.0 - self.px.rgba[i, j]

    @ti.kernel
    def decay(self, rate: ti.f32):
        """Decay pixels.

        Args:
            rate (ti.f32): decay rate.
        """
        for i, j in ti.ndrange(self.x, self.y):
            self.px.rgba[i, j] *= rate

    def blend_add(self, px: ti.template()):
        """Blend by adding pixels together (Python scope).

        Args:
            px (ti.template): Pixels to blend with.
        """
        self._blend_add(self.rgba_from_px(px))

    @ti.kernel
    def _blend_add(self, rgba: ti.template()):
        """Blend by adding pixels together (Taichi scope).

        Args:
            rgba (ti.template): Pixels to blend with.
        """
        for i, j in ti.ndrange(self.x, self.y):
            self.px.rgba[i, j] += rgba[i, j]

    def blend_sub(self, px: ti.template()):
        """Blend by subtracting pixels (Python scope).

        Args:
            px (ti.template): Pixels to blend with.
        """
        self._blend_sub(self.rgba_from_px(px))

    @ti.kernel
    def _blend_sub(self, rgba: ti.template()):
        """Blend by subtracting pixels (Taichi scope).

        Args:
            rgba (ti.template): Pixels to blend with.
        """
        for i, j in ti.ndrange(self.x, self.y):
            self.px.rgba[i, j] -= rgba[i, j]

    def blend_mul(self, px: ti.template()):
        """Blend by multiplying pixels (Python scope).

        Args:
            px (ti.template): Pixels to blend with.
        """
        self._blend_mul(self.rgba_from_px(px))

    @ti.kernel
    def _blend_mul(self, rgba: ti.template()):
        """Blend by multiplying pixels (Taichi scope).

        Args:
            rgba (ti.template): Pixels to blend with.
        """
        for i, j in ti.ndrange(self.x, self.y):
            self.px.rgba[i, j] *= rgba[i, j]

    def blend_div(self, px: ti.template()):
        """Blend by dividing pixels (Python scope).

        Args:
            px (ti.template): Pixels to blend with.
        """
        self._blend_div(self.rgba_from_px(px))

    @ti.kernel
    def _blend_div(self, rgba: ti.template()):
        """Blend by dividing pixels (Taichi scope).

        Args:
            rgba (ti.template): Pixels to blend with.
        """
        for i, j in ti.ndrange(self.x, self.y):
            self.px.rgba[i, j] /= rgba[i, j]

    def blend_min(self, px: ti.template()):
        """Blend by taking the minimum of each pixel (Python scope).

        Args:
            px (ti.template): Pixels to blend with.
        """
        self._blend_min(self.rgba_from_px(px))

    @ti.kernel
    def _blend_min(self, rgba: ti.template()):
        """Blend by taking the minimum of each pixel (Taichi scope).

        Args:
            rgba (ti.template): Pixels to blend with.
        """
        for i, j in ti.ndrange(self.x, self.y):
            self.px.rgba[i, j] = ti.min(self.px.rgba[i, j], rgba[i, j])

    def blend_max(self, px: ti.template()):
        """Blend by taking the maximum of each pixel (Python scope).

        Args:
            px (ti.template): Pixels to blend with.
        """
        self._blend_max(self.rgba_from_px(px))

    @ti.kernel
    def _blend_max(self, rgba: ti.template()):
        """Blend by taking the maximum of each pixel (Taichi scope).

        Args:
            rgba (ti.template): Pixels to blend with.
        """
        for i, j in ti.ndrange(self.x, self.y):
            self.px.rgba[i, j] = ti.max(self.px.rgba[i, j], rgba[i, j])

    def blend_diff(self, px: ti.template()):
        """Blend by taking the difference of each pixel (Python scope).

        Args:
            px (ti.template): Pixels to blend with.
        """
        self._blend_diff(self.rgba_from_px(px))

    @ti.kernel
    def _blend_diff(self, rgba: ti.template()):
        """Blend by taking the difference of each pixel (Taichi scope).

        Args:
            rgba (ti.template): Pixels to blend with.
        """
        for i, j in ti.ndrange(self.x, self.y):
            self.px.rgba[i, j] = ti.abs(self.px.rgba[i, j] - rgba[i, j])

    def blend_diff_inv(self, px: ti.template()):
        """Blend by taking the inverse difference of each pixel (Python scope).

        Args:
            px (ti.template): Pixels to blend with.
        """
        self._blend_diff_inv(self.rgba_from_px(px))

    @ti.kernel
    def _blend_diff_inv(self, rgba: ti.template()):
        """Blend by taking the inverse difference of each pixel (Taichi scope).

        Args:
            rgba (ti.template): Pixels to blend with.
        """
        for i, j in ti.ndrange(self.x, self.y):
            self.px.rgba[i, j] = ti.abs(rgba[i, j] - self.px.rgba[i, j])

    def blend_mix(self, px: ti.template(), amount: ti.f32):
        """Blend by mixing pixels (Python scope).

        Args:
            px (ti.template): Pixels to blend with.
            amount (ti.f32): Amount to mix.
        """
        self._blend_mix(self.rgba_from_px(px), amount)

    @ti.kernel
    def _blend_mix(self, rgba: ti.template(), amount: ti.f32):
        """Blend by mixing pixels (Taichi scope).

        Args:
            rgba (ti.template): Pixels to blend with.
            amount (ti.f32): Amount to mix.
        """
        for i, j in ti.ndrange(self.x, self.y):
            self.px.rgba[i, j] = ti.math.mix(self.px.rgba[i, j], rgba[i, j], amount)

    @ti.kernel
    def blur(self, radius: ti.i32):
        """Blur pixels.

        Args:
            radius (ti.i32): Blur radius.
        """
        for i, j in ti.ndrange(self.x, self.y):
            d = ti.Vector([0.0, 0.0, 0.0, 0.0])
            for di in range(-radius, radius + 1):
                for dj in range(-radius, radius + 1):
                    dx = (i + di) % self.x
                    dy = (j + dj) % self.y
                    d += self.px.rgba[dx, dy]
            d /= (radius * 2 + 1) ** 2
            self.px.rgba[i, j] = d

    def particles(
        self, particles: ti.template(), species: ti.template(), shape="circle"
    ):
        """Draw particles.

        Args:
            particles (ti.template): Particles.
            species (ti.template): Species.
            shape (str, optional): Shape. Defaults to "circle".
        """
        shape = self.shape_enum[shape]
        self._particles(particles, species, shape)

    @ti.kernel
    def _particles(self, particles: ti.template(), species: ti.template(), shape: int):
        """Draw particles.

        Args:
            particles (ti.template): Particles.
            species (ti.template): Species.
            shape (int): Shape enum value.
        """
        for i in range(self.tv.p.n):
            p = particles.field[i]
            s = species[p.species]
            if p.active == 0.0:
                continue
            px = ti.cast(p.pos[0], ti.i32)
            py = ti.cast(p.pos[1], ti.i32)
            vx = ti.cast(p.pos[0] + p.vel[0] * 20, ti.i32)
            vy = ti.cast(p.pos[1] + p.vel[1] * 20, ti.i32)
            rgba = s.rgba * self.CONSTS.BRIGHTNESS
            if shape == 0:
                self.point(px, py, rgba)
            elif shape == 1:
                self.line(px, py, vx, vy, rgba)
            elif shape == 2:
                side = int(s.size) * 2
                self.rect(px, py, side, side, rgba)
            elif shape == 3:
                self.circle(px, py, p.size, rgba)
            elif shape == 4:
                a = p.pos
                b = p.pos + 1
                c = a + b
                self.triangle(a, b, c, rgba)
            # elif shape == 5:
            #     self.polygon(px, py, rgba)

    def rgba_from_px(self, px):
        """Get rgba from pixels.

        Args:
            px (Any): Pixels to get rgba from.

        Raises:
            TypeError: If pixel field cannot be found.

        Returns:
            MatrixField: RGBA matrix field.
        """
        if isinstance(px, Pixels):
            return px.px.rgba
        elif isinstance(px, StructField):
            return px.rgba
        elif isinstance(px, MatrixField):
            return px
        else:
            try:
                return px.px.px.rgba
            except:
                raise TypeError(f"Cannot find pixel field in {type(px)}")

    def __call__(self):
        """Call returns pixels."""
        return self.get()

__call__()

Call returns pixels.

Source code in src/tolvera/pixels.py
def __call__(self):
    """Call returns pixels."""
    return self.get()

__init__(tolvera, **kwargs)

Initialise Pixels

Parameters:

Name Type Description Default
tolvera Tolvera

Tölvera instance.

required
**kwargs

Keyword arguments. polygon_mode (str): Polygon mode. Defaults to "crossing". brightness (float): Brightness. Defaults to 1.0.

{}
Source code in src/tolvera/pixels.py
def __init__(self, tolvera, **kwargs):
    """Initialise Pixels

    Args:
        tolvera (Tolvera): Tölvera instance.
        **kwargs: Keyword arguments.
            polygon_mode (str): Polygon mode. Defaults to "crossing".
            brightness (float): Brightness. Defaults to 1.0. 
    """
    self.tv = tolvera
    self.kwargs = kwargs
    self.polygon_mode = kwargs.get("polygon_mode", "crossing")
    self.x = self.tv.x
    self.y = self.tv.y
    self.px = Pixel.field(shape=(self.x, self.y))
    brightness = kwargs.get("brightness", 1.0)
    self.CONSTS = CONSTS(
        {
            "BRIGHTNESS": (ti.f32, brightness),
        }
    )
    self.shape_enum = {
        "point": 0,
        "line": 1,
        "rect": 2,
        "circle": 3,
        "triangle": 4,
        "polygon": 5,
    }

background(r, g, b)

Set background colour.

Parameters:

Name Type Description Default
r f32

Red.

required
g f32

Green.

required
b f32

Blue.

required
Source code in src/tolvera/pixels.py
@ti.func
def background(self, r: ti.f32, g: ti.f32, b: ti.f32):
    """Set background colour.

    Args:
        r (ti.f32): Red.
        g (ti.f32): Green.
        b (ti.f32): Blue.
    """
    bg = ti.Vector([r, g, b, 1.0])
    self.rect(0, 0, self.x, self.y, bg)

blend_add(px)

Blend by adding pixels together (Python scope).

Parameters:

Name Type Description Default
px template

Pixels to blend with.

required
Source code in src/tolvera/pixels.py
def blend_add(self, px: ti.template()):
    """Blend by adding pixels together (Python scope).

    Args:
        px (ti.template): Pixels to blend with.
    """
    self._blend_add(self.rgba_from_px(px))

blend_diff(px)

Blend by taking the difference of each pixel (Python scope).

Parameters:

Name Type Description Default
px template

Pixels to blend with.

required
Source code in src/tolvera/pixels.py
def blend_diff(self, px: ti.template()):
    """Blend by taking the difference of each pixel (Python scope).

    Args:
        px (ti.template): Pixels to blend with.
    """
    self._blend_diff(self.rgba_from_px(px))

blend_diff_inv(px)

Blend by taking the inverse difference of each pixel (Python scope).

Parameters:

Name Type Description Default
px template

Pixels to blend with.

required
Source code in src/tolvera/pixels.py
def blend_diff_inv(self, px: ti.template()):
    """Blend by taking the inverse difference of each pixel (Python scope).

    Args:
        px (ti.template): Pixels to blend with.
    """
    self._blend_diff_inv(self.rgba_from_px(px))

blend_div(px)

Blend by dividing pixels (Python scope).

Parameters:

Name Type Description Default
px template

Pixels to blend with.

required
Source code in src/tolvera/pixels.py
def blend_div(self, px: ti.template()):
    """Blend by dividing pixels (Python scope).

    Args:
        px (ti.template): Pixels to blend with.
    """
    self._blend_div(self.rgba_from_px(px))

blend_max(px)

Blend by taking the maximum of each pixel (Python scope).

Parameters:

Name Type Description Default
px template

Pixels to blend with.

required
Source code in src/tolvera/pixels.py
def blend_max(self, px: ti.template()):
    """Blend by taking the maximum of each pixel (Python scope).

    Args:
        px (ti.template): Pixels to blend with.
    """
    self._blend_max(self.rgba_from_px(px))

blend_min(px)

Blend by taking the minimum of each pixel (Python scope).

Parameters:

Name Type Description Default
px template

Pixels to blend with.

required
Source code in src/tolvera/pixels.py
def blend_min(self, px: ti.template()):
    """Blend by taking the minimum of each pixel (Python scope).

    Args:
        px (ti.template): Pixels to blend with.
    """
    self._blend_min(self.rgba_from_px(px))

blend_mix(px, amount)

Blend by mixing pixels (Python scope).

Parameters:

Name Type Description Default
px template

Pixels to blend with.

required
amount f32

Amount to mix.

required
Source code in src/tolvera/pixels.py
def blend_mix(self, px: ti.template(), amount: ti.f32):
    """Blend by mixing pixels (Python scope).

    Args:
        px (ti.template): Pixels to blend with.
        amount (ti.f32): Amount to mix.
    """
    self._blend_mix(self.rgba_from_px(px), amount)

blend_mul(px)

Blend by multiplying pixels (Python scope).

Parameters:

Name Type Description Default
px template

Pixels to blend with.

required
Source code in src/tolvera/pixels.py
def blend_mul(self, px: ti.template()):
    """Blend by multiplying pixels (Python scope).

    Args:
        px (ti.template): Pixels to blend with.
    """
    self._blend_mul(self.rgba_from_px(px))

blend_sub(px)

Blend by subtracting pixels (Python scope).

Parameters:

Name Type Description Default
px template

Pixels to blend with.

required
Source code in src/tolvera/pixels.py
def blend_sub(self, px: ti.template()):
    """Blend by subtracting pixels (Python scope).

    Args:
        px (ti.template): Pixels to blend with.
    """
    self._blend_sub(self.rgba_from_px(px))

blur(radius)

Blur pixels.

Parameters:

Name Type Description Default
radius i32

Blur radius.

required
Source code in src/tolvera/pixels.py
@ti.kernel
def blur(self, radius: ti.i32):
    """Blur pixels.

    Args:
        radius (ti.i32): Blur radius.
    """
    for i, j in ti.ndrange(self.x, self.y):
        d = ti.Vector([0.0, 0.0, 0.0, 0.0])
        for di in range(-radius, radius + 1):
            for dj in range(-radius, radius + 1):
                dx = (i + di) % self.x
                dy = (j + dj) % self.y
                d += self.px.rgba[dx, dy]
        d /= (radius * 2 + 1) ** 2
        self.px.rgba[i, j] = d

circle(x, y, r, rgba)

Draw a filled circle.

Parameters:

Name Type Description Default
x i32

X position.

required
y i32

Y position.

required
r i32

Radius.

required
rgba vec4

Colour.

required
Source code in src/tolvera/pixels.py
@ti.func
def circle(self, x: ti.i32, y: ti.i32, r: ti.i32, rgba: vec4):
    """Draw a filled circle.

    Args:
        x (ti.i32): X position.
        y (ti.i32): Y position.
        r (ti.i32): Radius.
        rgba (vec4): Colour.
    """
    for i in range(r + 1):
        d = ti.sqrt(r**2 - i**2)
        d_int = ti.cast(d, ti.i32)
        # TODO: parallelise ?
        for j in range(d_int):
            self.px.rgba[x + i, y + j] = rgba
            self.px.rgba[x + i, y - j] = rgba
            self.px.rgba[x - i, y - j] = rgba
            self.px.rgba[x - i, y + j] = rgba

circles(x, y, r, rgba)

Draw circles with the same colour.

Parameters:

Name Type Description Default
x template

X positions.

required
y template

Y positions.

required
r template

Radii.

required
rgba vec4

Colour.

required
Source code in src/tolvera/pixels.py
@ti.func
def circles(self, x: ti.template(), y: ti.template(), r: ti.template(), rgba: vec4):
    """Draw circles with the same colour.

    Args:
        x (ti.template): X positions.
        y (ti.template): Y positions.
        r (ti.template): Radii.
        rgba (vec4): Colour.
    """
    for i in ti.static(range(len(x))):
        self.circle(x[i], y[i], r[i], rgba)

clear()

Clear pixels.

Source code in src/tolvera/pixels.py
@ti.kernel
def clear(self):
    """Clear pixels."""
    self.px.rgba.fill(0)

decay(rate)

Decay pixels.

Parameters:

Name Type Description Default
rate f32

decay rate.

required
Source code in src/tolvera/pixels.py
@ti.kernel
def decay(self, rate: ti.f32):
    """Decay pixels.

    Args:
        rate (ti.f32): decay rate.
    """
    for i, j in ti.ndrange(self.x, self.y):
        self.px.rgba[i, j] *= rate

diffuse(evaporate)

Diffuse pixels.

Parameters:

Name Type Description Default
evaporate float

Evaporation rate.

required
Source code in src/tolvera/pixels.py
@ti.kernel
def diffuse(self, evaporate: ti.f32):
    """Diffuse pixels.

    Args:
        evaporate (float): Evaporation rate.
    """
    for i, j in ti.ndrange(self.x, self.y):
        d = ti.Vector([0.0, 0.0, 0.0, 0.0])
        for di in ti.static(range(-1, 2)):
            for dj in ti.static(range(-1, 2)):
                dx = (i + di) % self.x
                dy = (j + dj) % self.y
                d += self.px.rgba[dx, dy]
        d *= 0.99 / 9.0
        self.px.rgba[i, j] = d

flip_x()

Flip image in x-axis.

Source code in src/tolvera/pixels.py
@ti.kernel
def flip_x(self):
    """Flip image in x-axis."""
    for i, j in ti.ndrange(self.x, self.y):
        self.px.rgba[i, j] = self.px.rgba[self.x - 1 - i, j]

flip_y()

Flip image in y-axis.

Source code in src/tolvera/pixels.py
@ti.kernel
def flip_y(self):
    """Flip image in y-axis."""
    for i, j in ti.ndrange(self.x, self.y):
        self.px.rgba[i, j] = self.px.rgba[i, self.y - 1 - j]

get()

Get pixels.

Source code in src/tolvera/pixels.py
def get(self):
    """Get pixels."""
    return self.px

invert()

Invert image.

Source code in src/tolvera/pixels.py
@ti.kernel
def invert(self):
    """Invert image."""
    for i, j in ti.ndrange(self.x, self.y):
        self.px.rgba[i, j] = 1.0 - self.px.rgba[i, j]

line(x0, y0, x1, y1, rgba)

Draw an anti-aliased line using Xiaolin Wu's algorithm.

Source code in src/tolvera/pixels.py
@ti.func
def line(self, x0: ti.f32, y0: ti.f32, x1: ti.f32, y1: ti.f32, rgba: vec4):
    """Draw an anti-aliased line using Xiaolin Wu's algorithm."""
    steep = ti.abs(y1 - y0) > ti.abs(x1 - x0)
    if steep:
        x0, y0 = y0, x0
        x1, y1 = y1, x1

    if x0 > x1:
        x0, x1 = x1, x0
        y0, y1 = y1, y0

    dx = x1 - x0
    dy = y1 - y0
    gradient = dy / dx if dx != 0 else 1.0

    xend = ti.math.round(x0)
    yend = y0 + gradient * (xend - x0)
    xgap = self.rfpart(x0 + 0.5)
    xpxl1 = int(xend)
    ypxl1 = int(self.ipart(yend))
    if steep:
        self.plot(ypxl1, xpxl1, self.rfpart(yend) * xgap, rgba)
        self.plot(ypxl1 + 1, xpxl1, self.fpart(yend) * xgap, rgba)
    else:
        self.plot(xpxl1, ypxl1, self.rfpart(yend) * xgap, rgba)
        self.plot(xpxl1, ypxl1 + 1, self.fpart(yend) * xgap, rgba)

    intery = yend + gradient

    xend = ti.math.round(x1)
    yend = y1 + gradient * (xend - x1)
    xgap = self.fpart(x1 + 0.5)
    xpxl2 = int(xend)
    ypxl2 = int(self.ipart(yend))
    if steep:
        self.plot(ypxl2, xpxl2, self.rfpart(yend) * xgap, rgba)
        self.plot(ypxl2 + 1, xpxl2, self.fpart(yend) * xgap, rgba)
    else:
        self.plot(xpxl2, ypxl2, self.rfpart(yend) * xgap, rgba)
        self.plot(xpxl2, ypxl2 + 1, self.fpart(yend) * xgap, rgba)

    if steep:
        for x in range(xpxl1 + 1, xpxl2):
            self.plot(int(self.ipart(intery)), x, self.rfpart(intery), rgba)
            self.plot(int(self.ipart(intery)) + 1, x, self.fpart(intery), rgba)
            intery += gradient
    else:
        for x in range(xpxl1 + 1, xpxl2):
            self.plot(x, int(self.ipart(intery)), self.rfpart(intery), rgba)
            self.plot(x, int(self.ipart(intery)) + 1, self.fpart(intery), rgba)
            intery += gradient

lines(points, rgba)

Draw lines with the same colour.

Parameters:

Name Type Description Default
points template

Points.

required
rgba vec4

Colour.

required
Source code in src/tolvera/pixels.py
@ti.func
def lines(self, points: ti.template(), rgba: vec4):
    """Draw lines with the same colour.

    Args:
        points (ti.template): Points.
        rgba (vec4): Colour.
    """
    for i in range(points.shape[0] - 1):
        self.line(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1], rgba)

particles(particles, species, shape='circle')

Draw particles.

Parameters:

Name Type Description Default
particles template

Particles.

required
species template

Species.

required
shape str

Shape. Defaults to "circle".

'circle'
Source code in src/tolvera/pixels.py
def particles(
    self, particles: ti.template(), species: ti.template(), shape="circle"
):
    """Draw particles.

    Args:
        particles (ti.template): Particles.
        species (ti.template): Species.
        shape (str, optional): Shape. Defaults to "circle".
    """
    shape = self.shape_enum[shape]
    self._particles(particles, species, shape)

plot(x, y, c, rgba)

Set the pixel color with blending.

Source code in src/tolvera/pixels.py
@ti.func
def plot(self, x, y, c, rgba):
    """Set the pixel color with blending."""
    self.px.rgba[x, y] = self.px.rgba[x, y] * (1 - c) + rgba * c

point(x, y, rgba)

Draw point.

Parameters:

Name Type Description Default
x i32

X position.

required
y i32

Y position.

required
rgba vec4

Colour.

required
Source code in src/tolvera/pixels.py
@ti.func
def point(self, x: ti.i32, y: ti.i32, rgba: vec4):
    """Draw point.

    Args:
        x (ti.i32): X position.
        y (ti.i32): Y position.
        rgba (vec4): Colour.
    """
    self.px.rgba[x, y] = rgba

points(x, y, rgba)

Draw points with the same colour.

Parameters:

Name Type Description Default
x template

X positions.

required
y template

Y positions.

required
rgba vec4

Colour.

required
Source code in src/tolvera/pixels.py
@ti.func
def points(self, x: ti.template(), y: ti.template(), rgba: vec4):
    """Draw points with the same colour.

    Args:
        x (ti.template): X positions.
        y (ti.template): Y positions.
        rgba (vec4): Colour.
    """
    for i in ti.static(range(len(x))):
        self.point(x[i], y[i], rgba)

polygon(x, y, rgba)

Draw a filled polygon.

Polygons are drawn according to the polygon mode, which can be "crossing" (default) or "winding". First, the bounding box of the polygon is calculated. Then, we check if each pixel in the bounding box is inside the polygon. If it is, we draw it (along with each neighbour pixel).

Reference for point in polygon inclusion testing: http://www.dgp.toronto.edu/~mac/e-stuff/point_in_polygon.py

Parameters:

Name Type Description Default
x template

X positions.

required
y template

Y positions.

required
rgba vec4

Colour.

required

TODO: fill arg

Source code in src/tolvera/pixels.py
@ti.func
def polygon(self, x: ti.template(), y: ti.template(), rgba: vec4):
    """Draw a filled polygon.

    Polygons are drawn according to the polygon mode, which can be "crossing" 
    (default) or "winding". First, the bounding box of the polygon is calculated.
    Then, we check if each pixel in the bounding box is inside the polygon. If it
    is, we draw it (along with each neighbour pixel).

    Reference for point in polygon inclusion testing:
    http://www.dgp.toronto.edu/~mac/e-stuff/point_in_polygon.py

    Args:
        x (ti.template): X positions.
        y (ti.template): Y positions.
        rgba (vec4): Colour.

    TODO: fill arg
    """
    x_min, x_max = ti.cast(x.min(), ti.i32), ti.cast(x.max(), ti.i32)
    y_min, y_max = ti.cast(y.min(), ti.i32), ti.cast(y.max(), ti.i32)
    l = len(x)
    for i, j in ti.ndrange(x_max - x_min, y_max - y_min):
        p = ti.Vector([x_min + i, y_min + j])
        if self._is_inside(p, x, y, l) != 0:
            # TODO: abstract out, weight?
            """
            x-1,y-1  x,y-1  x+1,y-1
            x-1,y    x,y    x+1,y
            x-1,y+1  x,y+1  x+1,y+1
            """
            _x, _y = p[0], p[1]
            self.px.rgba[_x - 1, _y - 1] = rgba
            self.px.rgba[_x - 1, _y] = rgba
            self.px.rgba[_x - 1, _y + 1] = rgba

            self.px.rgba[_x, _y - 1] = rgba
            self.px.rgba[_x, _y] = rgba
            self.px.rgba[_x, _y + 1] = rgba

            self.px.rgba[_x + 1, _y - 1] = rgba
            self.px.rgba[_x + 1, _y] = rgba
            self.px.rgba[_x + 1, _y + 1] = rgba

rect(x, y, w, h, rgba)

Draw a filled rectangle.

Parameters:

Name Type Description Default
x i32

X position.

required
y i32

Y position.

required
w i32

Width.

required
h i32

Height.

required
rgba vec4

Colour.

required
Source code in src/tolvera/pixels.py
@ti.func
def rect(self, x: ti.i32, y: ti.i32, w: ti.i32, h: ti.i32, rgba: vec4):
    """Draw a filled rectangle.

    Args:
        x (ti.i32): X position.
        y (ti.i32): Y position.
        w (ti.i32): Width.
        h (ti.i32): Height.
        rgba (vec4): Colour.
    """
    # TODO: fill arg
    # TODO: gradients, lerp with ti.math.mix(x, y, a)
    for i, j in ti.ndrange(w, h):
        self.px.rgba[x + i, y + j] = rgba

rgba_from_px(px)

Get rgba from pixels.

Parameters:

Name Type Description Default
px Any

Pixels to get rgba from.

required

Raises:

Type Description
TypeError

If pixel field cannot be found.

Returns:

Name Type Description
MatrixField

RGBA matrix field.

Source code in src/tolvera/pixels.py
def rgba_from_px(self, px):
    """Get rgba from pixels.

    Args:
        px (Any): Pixels to get rgba from.

    Raises:
        TypeError: If pixel field cannot be found.

    Returns:
        MatrixField: RGBA matrix field.
    """
    if isinstance(px, Pixels):
        return px.px.rgba
    elif isinstance(px, StructField):
        return px.rgba
    elif isinstance(px, MatrixField):
        return px
    else:
        try:
            return px.px.px.rgba
        except:
            raise TypeError(f"Cannot find pixel field in {type(px)}")

set(px)

Set pixels.

Parameters:

Name Type Description Default
px Any

Pixels to set. Can be Pixels, StructField, MatrixField, etc (see rgba_from_px).

required
Source code in src/tolvera/pixels.py
def set(self, px: Any):
    """Set pixels.

    Args:
        px (Any): Pixels to set. Can be Pixels, StructField, MatrixField, etc (see rgba_from_px).
    """
    self.px.rgba = self.rgba_from_px(px)

triangle(a, b, c, rgba)

Draw a filled triangle.

Parameters:

Name Type Description Default
a vec2

Point A.

required
b vec2

Point B.

required
c vec2

Point C.

required
rgba vec4

Colour.

required
Source code in src/tolvera/pixels.py
@ti.func
def triangle(self, a, b, c, rgba: vec4):
    """Draw a filled triangle.

    Args:
        a (vec2): Point A.
        b (vec2): Point B.
        c (vec2): Point C.
        rgba (vec4): Colour.
    """
    # TODO: fill arg
    x = ti.Vector([a[0], b[0], c[0]])
    y = ti.Vector([a[1], b[1], c[1]])
    self.polygon(x, y, rgba)