Skip to content

Context

TolveraContext is a shared context or environment for Tolvera instances. It is created automatically when a Tolvera instance is created, if one does not already exist. It manages the integration of packages for graphics, computer vision, communications protocols, and more. If multiple Tolvera instances are created, they must share the same TolveraContext.

Example

TolveraContext can be created manually, and shared with multiple Tolvera instances. Note that only one render function can be used at a time.

from tolvera import TolveraContext, Tolvera, run

def main(**kwargs):
    ctx = TolveraContext(**kwargs)

    tv1 = Tolvera(ctx=ctx, **kwargs)
    tv2 = Tolvera(ctx=ctx, **kwargs)

    @tv1.render
    def _():
        return tv2.px

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

TolveraContext can also be created automatically, and still shared.

from tolvera import Tolvera, run

def main(**kwargs):
    tv1 = Tolvera(**kwargs)
    tv2 = Tolvera(ctx=tv1.ctx, **kwargs)

    @tv1.render
    def _():
        return tv2.px

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

CONSTS

Dict of CONSTS that can be used in Taichi scope

Source code in src/tolvera/utils.py
class CONSTS:
    """
    Dict of CONSTS that can be used in Taichi scope
    """

    def __init__(self, dict: dict[str, (DataType, Any)]):
        self.struct = ti.types.struct(**{k: v[0] for k, v in dict.items()})
        self.consts = self.struct(**{k: v[1] for k, v in dict.items()})

    def __getattr__(self, name):
        try:
            return self.consts[name]
        except:
            raise AttributeError(f"CONSTS has no attribute {name}")

    def __getitem__(self, name):
        try:
            return self.consts[name]
        except:
            raise AttributeError(f"CONSTS has no attribute {name}")

MPFace

Source code in src/tolvera/mp/face.py
@ti.data_oriented
class MPFace:
    def __init__(self, context, **kwargs) -> None:
        self.ctx = context
        self.kwargs = kwargs
        self.n_points = 6
        self.max_faces = kwargs.get('max_faces', 4)

        self.config = {
            'min_detection_confidence': kwargs.get('detection_con', .5),
            'model_selection': kwargs.get('model_selection', 0),
        }

        """
        TODO: add bbox as separate tv.s.faces_bbox?
            format: RELATIVE_BOUNDING_BOX
            relative_bounding_box {
                xmin: 0.482601523
                ymin: 0.402242899
                width: 0.162447035
                height: 0.2887941
            }
        """

        self.faces_np = {
            'pxnorm':np.zeros((self.max_faces, self.n_points, 2), np.float32),
            'px':np.zeros((self.max_faces, self.n_points, 2), np.float32),
        }
        self.ctx.s.faces = {
            'state': {
                'pxnorm': (ti.math.vec2, 0.0, 1.0),
                'px': (ti.math.vec2, 0.0, 1.0),
                # 'metres': (ti.math.vec3, 0.0, 1.0), # face_world_landmarks
            },
            'shape': (self.max_faces, self.n_points)
        }

        self.mpFace = mp.solutions.face_detection
        self.face = self.mpFace.FaceDetection(**self.config)
        self.detected = ti.field(ti.i32, shape=())

        self.updater = Updater(self.detect, kwargs.get('face_detect_rate', 10))

    def detect(self, frame=None):
        if frame is None: return
        self.results = self.face.process(frame)
        if self.results.detections is None:
            self.ctx.s.faces.fill(0.)
            self.detected[None] = -1
            return

        if self.results.detections:
            for i, face in enumerate(self.results.detections):
                for j, lm in enumerate(face.location_data.relative_keypoints):
                    pxnorm = np.array([1-lm.x, 1-lm.y])
                    px = np.array([self.ctx.x*(1-lm.x), self.ctx.y*(1-lm.y)])
                    self.faces_np['pxnorm'][i, j] = pxnorm
                    self.faces_np['px'][i, j] = px
        self.ctx.s.faces.set_from_nddict(self.faces_np)

        self.detected[None] = len(self.results.detections)

    @ti.kernel
    def draw(self):
        if self.detected[None] > 0:
            self.draw_face_lms(5, ti.Vector([1, 1, 1, 1]))

    @ti.func
    def draw_face_lms(self, r, rgba):
        for i, lm in ti.ndrange(self.detected[None], self.n_conns):
            self.draw_lm(i, lm, r, rgba)

    @ti.func
    def draw_lm(self, face: ti.i32, lm: ti.i32, r: ti.i32, rgba: ti.math.vec4):
        px = self.ctx.s.faces[face, lm].px
        cx = ti.cast(px.x, ti.i32)
        cy = ti.cast(px.y, ti.i32)
        self.px.circle(cx, cy, r, rgba)

    def landmark_name_from_index(self, index):
        return FaceKeyPoint(index).name

    def landmark_index_from_name(self, name):
        return FaceKeyPoint[name].value

    @ti.kernel
    def get_landmark(self, face: ti.i32, landmark: ti.i32) -> ti.math.vec2:
        return self.ctx.s.faces[landmark].px

    def __call__(self, frame):
        self.updater(frame)

config = {'min_detection_confidence': kwargs.get('detection_con', 0.5), 'model_selection': kwargs.get('model_selection', 0)} instance-attribute

add bbox as separate tv.s.faces_bbox?

format: RELATIVE_BOUNDING_BOX relative_bounding_box { xmin: 0.482601523 ymin: 0.402242899 width: 0.162447035 height: 0.2887941 }

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
 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
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
@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 = kwargs.get("x", self.tv.x)
        self.y = kwargs.get("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]

    @ti.func
    def stamp(self, x: ti.i32, y: ti.i32, px: ti.template()):
        """Stamp pixels.

        Args:
            x (ti.i32): X position.
            y (ti.i32): Y position.
            px (ti.template): Pixels to stamp.
        """
        for i, j in ti.ndrange(px.px.shape[0], px.px.shape[1]):
            p = px.px.rgba[i, j]
            if p[0]+p[1]+p[2] > 0: # transparency
                self.px.rgba[x + i, y + j] = p

    @ti.kernel
    def from_numpy(self, img: ti.template()):
        for x, y in ti.ndrange(self.x, self.y):
            if img[x, y, 0]+img[x, y, 1]+img[x, y, 2] > 0.:
                self.px.rgba[x, y] = ti.Vector([
                    img[x, y, 0]/255.,
                    img[x, y, 1]/255.,
                    img[x, y, 2]/255.,
                    img[x, y, 3]/255.])

    def from_img(self, path: str):
        img = ti.tools.imread(path)
        img_fld = ti.field(dtype=ti.f32, shape=img.shape)
        img_fld.from_numpy(img)
        self.from_numpy(img_fld)
        return img_fld

    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.kernel
    def stamp(self, x: ti.i32, y: ti.i32, px: ti.template()):
        """Stamp pixels.

        Args:
            x (ti.i32): X position.
            y (ti.i32): Y position.
            px (ti.template): Pixels to stamp.
        """
        self.stamp_f(x, y, px)

    @ti.func
    def stamp_f(self, x: ti.i32, y: ti.i32, px: ti.template()):
        """Stamp pixels.

        Args:
            x (ti.i32): X position.
            y (ti.i32): Y position.
            px (ti.template): Pixels to stamp.
        """
        for i, j in ti.ndrange(px.px.shape[0], px.px.shape[1]):
            p = px.px.rgba[i, j]
            if p[0]+p[1]+p[2] > 0: # transparency
                self.px.rgba[x + i, y + j] = p

    @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
        elif isinstance(px, ScalarField):
            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 = kwargs.get("x", self.tv.x)
    self.y = kwargs.get("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,
    }

_blend_add(rgba)

Blend by adding pixels together (Taichi scope).

Parameters:

Name Type Description Default
rgba template

Pixels to blend with.

required
Source code in src/tolvera/pixels.py
@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]

_blend_diff(rgba)

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

Parameters:

Name Type Description Default
rgba template

Pixels to blend with.

required
Source code in src/tolvera/pixels.py
@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])

_blend_diff_inv(rgba)

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

Parameters:

Name Type Description Default
rgba template

Pixels to blend with.

required
Source code in src/tolvera/pixels.py
@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])

_blend_div(rgba)

Blend by dividing pixels (Taichi scope).

Parameters:

Name Type Description Default
rgba template

Pixels to blend with.

required
Source code in src/tolvera/pixels.py
@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]

_blend_max(rgba)

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

Parameters:

Name Type Description Default
rgba template

Pixels to blend with.

required
Source code in src/tolvera/pixels.py
@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])

_blend_min(rgba)

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

Parameters:

Name Type Description Default
rgba template

Pixels to blend with.

required
Source code in src/tolvera/pixels.py
@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])

_blend_mix(rgba, amount)

Blend by mixing pixels (Taichi scope).

Parameters:

Name Type Description Default
rgba template

Pixels to blend with.

required
amount f32

Amount to mix.

required
Source code in src/tolvera/pixels.py
@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)

_blend_mul(rgba)

Blend by multiplying pixels (Taichi scope).

Parameters:

Name Type Description Default
rgba template

Pixels to blend with.

required
Source code in src/tolvera/pixels.py
@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]

_blend_sub(rgba)

Blend by subtracting pixels (Taichi scope).

Parameters:

Name Type Description Default
rgba template

Pixels to blend with.

required
Source code in src/tolvera/pixels.py
@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]

_is_inside(p, x, y, l)

Check if point is inside polygon.

Parameters:

Name Type Description Default
p vec2

Point.

required
x template

X positions.

required
y template

Y positions.

required
l i32

Number of points.

required

Returns:

Name Type Description
int

1 if inside, 0 if outside.

Source code in src/tolvera/pixels.py
@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

_is_inside_crossing(p, x, y, l)

Check if point is inside polygon using crossing number algorithm.

Parameters:

Name Type Description Default
p vec2

Point.

required
x template

X positions.

required
y template

Y positions.

required
l i32

Number of points.

required

Returns:

Name Type Description
int

1 if inside, 0 if outside.

Source code in src/tolvera/pixels.py
@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

_is_inside_winding(p, x, y, l)

Check if point is inside polygon using winding number algorithm.

Parameters:

Name Type Description Default
p vec2

Point.

required
x template

X positions.

required
y template

Y positions.

required
l i32

Number of points.

required

Returns:

Name Type Description
int

1 if inside, 0 if outside.

Source code in src/tolvera/pixels.py
@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

_particles(particles, species, shape)

Draw particles.

Parameters:

Name Type Description Default
particles template

Particles.

required
species template

Species.

required
shape int

Shape enum value.

required
Source code in src/tolvera/pixels.py
@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)

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
    elif isinstance(px, ScalarField):
        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)

stamp(x, y, px)

Stamp pixels.

Parameters:

Name Type Description Default
x i32

X position.

required
y i32

Y position.

required
px template

Pixels to stamp.

required
Source code in src/tolvera/pixels.py
@ti.kernel
def stamp(self, x: ti.i32, y: ti.i32, px: ti.template()):
    """Stamp pixels.

    Args:
        x (ti.i32): X position.
        y (ti.i32): Y position.
        px (ti.template): Pixels to stamp.
    """
    self.stamp_f(x, y, px)

stamp_f(x, y, px)

Stamp pixels.

Parameters:

Name Type Description Default
x i32

X position.

required
y i32

Y position.

required
px template

Pixels to stamp.

required
Source code in src/tolvera/pixels.py
@ti.func
def stamp_f(self, x: ti.i32, y: ti.i32, px: ti.template()):
    """Stamp pixels.

    Args:
        x (ti.i32): X position.
        y (ti.i32): Y position.
        px (ti.template): Pixels to stamp.
    """
    for i, j in ti.ndrange(px.px.shape[0], px.px.shape[1]):
        p = px.px.rgba[i, j]
        if p[0]+p[1]+p[2] > 0: # transparency
            self.px.rgba[x + i, y + j] = p

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)

TolveraContext

Context for sharing between multiple Tölvera instances. Context includes Taichi, OSC, IML and CV. All Tölvera instances share the same context and are managed as a dict.

Attributes:

Name Type Description
kwargs dict

Keyword arguments for context.

name str

Name of context.

name_clean str

'Cleaned' name of context.

i int

Frame counter.

x int

Width of canvas.

y int

Height of canvas.

ti Taichi

Taichi instance.

canvas Pixels

Pixels instance.

osc OSC

OSC instance.

iml IML

IML instance.

cv CV

CV instance.

_cleanup_fns list

List of cleanup functions.

tolveras dict

Dict of Tölvera instances.

Source code in src/tolvera/context.py
class TolveraContext:
    """
    Context for sharing between multiple Tölvera instances.
    Context includes Taichi, OSC, IML and CV.
    All Tölvera instances share the same context and are managed as a dict.

    Attributes:
        kwargs (dict): Keyword arguments for context.
        name (str): Name of context.
        name_clean (str): 'Cleaned' name of context.
        i (int): Frame counter.
        x (int): Width of canvas.
        y (int): Height of canvas.
        ti (Taichi): Taichi instance.
        canvas (Pixels): Pixels instance.
        osc (OSC): OSC instance.
        iml (IML): IML instance.
        cv (CV): CV instance.
        _cleanup_fns (list): List of cleanup functions.
        tolveras (dict): Dict of Tölvera instances.
    """

    def __init__(self, **kwargs) -> None:
        """Initialise Tölvera context with given keyword arguments."""
        self.kwargs = kwargs
        self.init(**kwargs)

    def init(self, **kwargs):
        """
        Initialise wrapped external packages with given keyword arguments.
        This only happens once when Tölvera is first initialised.

        Args:
            x (int): Width of canvas. Default: 1920.
            y (int): Height of canvas. Default: 1080.
            osc (bool): Enable OSC. Default: False.
            iml (bool): Enable IML. Default: False.
            cv (bool): Enable CV. Default: False.
            see also kwargs for Taichi, OSC, IMLDict, and CV.
        """
        self.name = "Tölvera Context"
        self.name_clean = clean_name(self.name)
        print(f"[{self.name}] Initializing context...")
        self.x = kwargs.get("x", 1920)
        self.y = kwargs.get("y", 1080)
        self.ti = Taichi(self, **kwargs)
        self.i = ti.field(ti.i32, ())
        self.show = self.ti.show
        self.canvas = Pixels(self, **kwargs)
        self.s = StateDict(self)
        self.osc = kwargs.get("osc", False)
        self.iml = kwargs.get("iml", False)
        self.cv = kwargs.get("cv", False)
        self.hands = kwargs.get("hands", False)
        self.pose = kwargs.get("pose", False)
        self.face = kwargs.get("face", False)
        self.face_mesh = kwargs.get("face_mesh", False)
        if self.osc:
            self.osc = OSC(self, **kwargs)
        if self.iml:
            self.iml = IMLDict(self)
        if self.cv:
            self.cv = CV(self, **kwargs)
            if self.hands:
                self.hands = MPHands(self, **kwargs)
            if self.pose:
                self.pose = MPPose(self, **kwargs)
            if self.face:
                self.face = MPFace(self, **kwargs)
            if self.face_mesh:
                self.face_mesh = MPFaceMesh(self, **kwargs)
        self._cleanup_fns = []
        self.tolveras = {}
        print(f"[{self.name}] Context initialisation complete.")

    def run(self, f=None, **kwargs):
        """
        Run Tölvera with given render function and keyword arguments.
        This function will run inside a locked thread until KeyboardInterrupt/exit.
        It runs the render function, updates the OSC map (if enabled), and shows the pixels.

        Args:
            f: Function to run.
            **kwargs: Keyword arguments for function.
        """
        if f is not None:
            print(f"[{self.name}] Running with render function {f.__name__}...")
        else:
            print(f"[{self.name}] Running with no render function...")
        while self.ti.window.running:
            # print(kwargs)
            # exit()
            # gui = kwargs.get('gui', None)
            # if gui is not None:
            #     gui()
            # with self.ti.gui.sub_window("Sub Window", 0.1, 0.1, 0.2, 0.2) as w:
            #     w.text("text")
            with _lock:
                self.step(f, **kwargs)

    def step(self, f, **kwargs):
        [t.p() for t in self.tolveras.values()]
        if f is not None:
            self.canvas = f(**kwargs)
        if self.osc is not False:
            self.osc.map()
        if self.iml is not False:
            self.iml()
        if self.cv is not False:
            self.cv()
        self.ti.show(self.canvas)
        self.i[None] += 1

    def stop(self):
        """
        Run cleanup functions and exit.
        """
        print(f"\n[{self.name}] Stopping {self.name}...")
        for f in self._cleanup_fns:
            print(f"\n[{self.name}] Running cleanup function {f.__name__}...")
            f()
        print(f"\n[{self.name}] Exiting {self.name}...")
        exit(0)

    def render(self, f=None, **kwargs):
        """Render Tölvera with given function and keyword arguments.

        Args:
            f (function, optional): Function to run. Defaults to None.
        """
        try:
            self.run(f, **kwargs)
        except KeyboardInterrupt:
            self.stop()

    def cleanup(self, f=None):
        """
        Decorator for cleanup functions based on iipyper.
        Make functions run on KeyBoardInterrupt (before exit).
        Cleanup functions must be defined before render is called!

        Args:
            f: Function to cleanup.

        Returns:
            Decorator function if f is None, else decorated function.
        """
        print(f"\n[{self.name}] Adding cleanup function {f.__name__}...")

        def decorator(f):
            """Decorator that appends function to cleanup functions."""
            self._cleanup_fns.append(f)
            return f

        if f is None:  # return a decorator
            return decorator
        else:  # bare decorator case; return decorated function
            return decorator(f)

    def add(self, tolvera):
        """
        Add Tölvera to context.

        Args:
            tolvera (Tolvera): Tölvera to add.
        """
        print(f"[{self.name}] Adding tolvera='{tolvera.name}' to context.")
        self.tolveras[tolvera.name] = tolvera

    def get_by_name(self, name):
        """
        Get Tölvera by name.

        Args:
            name (str): Name of Tölvera to get.

        Returns:
            Tölvera: Tölvera with given name.
        """
        return self.tolveras[name]

    def get_names(self):
        """
        Get names of all Tölveras in context.

        Returns:
            list: List of Tölvera names.
        """
        return list(self.tolveras.keys())

    def remove(self, name):
        """
        Remove Tölvera by name.

        Args:
            name (str): Name of Tölvera to delete.
        """
        print(f"[{self.name}] Deleting tolvera='{name}' from context.")
        del self.tolveras[name]

__init__(**kwargs)

Initialise Tölvera context with given keyword arguments.

Source code in src/tolvera/context.py
def __init__(self, **kwargs) -> None:
    """Initialise Tölvera context with given keyword arguments."""
    self.kwargs = kwargs
    self.init(**kwargs)

add(tolvera)

Add Tölvera to context.

Parameters:

Name Type Description Default
tolvera Tolvera

Tölvera to add.

required
Source code in src/tolvera/context.py
def add(self, tolvera):
    """
    Add Tölvera to context.

    Args:
        tolvera (Tolvera): Tölvera to add.
    """
    print(f"[{self.name}] Adding tolvera='{tolvera.name}' to context.")
    self.tolveras[tolvera.name] = tolvera

cleanup(f=None)

Decorator for cleanup functions based on iipyper. Make functions run on KeyBoardInterrupt (before exit). Cleanup functions must be defined before render is called!

Parameters:

Name Type Description Default
f

Function to cleanup.

None

Returns:

Type Description

Decorator function if f is None, else decorated function.

Source code in src/tolvera/context.py
def cleanup(self, f=None):
    """
    Decorator for cleanup functions based on iipyper.
    Make functions run on KeyBoardInterrupt (before exit).
    Cleanup functions must be defined before render is called!

    Args:
        f: Function to cleanup.

    Returns:
        Decorator function if f is None, else decorated function.
    """
    print(f"\n[{self.name}] Adding cleanup function {f.__name__}...")

    def decorator(f):
        """Decorator that appends function to cleanup functions."""
        self._cleanup_fns.append(f)
        return f

    if f is None:  # return a decorator
        return decorator
    else:  # bare decorator case; return decorated function
        return decorator(f)

get_by_name(name)

Get Tölvera by name.

Parameters:

Name Type Description Default
name str

Name of Tölvera to get.

required

Returns:

Name Type Description
Tölvera

Tölvera with given name.

Source code in src/tolvera/context.py
def get_by_name(self, name):
    """
    Get Tölvera by name.

    Args:
        name (str): Name of Tölvera to get.

    Returns:
        Tölvera: Tölvera with given name.
    """
    return self.tolveras[name]

get_names()

Get names of all Tölveras in context.

Returns:

Name Type Description
list

List of Tölvera names.

Source code in src/tolvera/context.py
def get_names(self):
    """
    Get names of all Tölveras in context.

    Returns:
        list: List of Tölvera names.
    """
    return list(self.tolveras.keys())

init(**kwargs)

Initialise wrapped external packages with given keyword arguments. This only happens once when Tölvera is first initialised.

Parameters:

Name Type Description Default
x int

Width of canvas. Default: 1920.

required
y int

Height of canvas. Default: 1080.

required
osc bool

Enable OSC. Default: False.

required
iml bool

Enable IML. Default: False.

required
cv bool

Enable CV. Default: False.

required
Source code in src/tolvera/context.py
def init(self, **kwargs):
    """
    Initialise wrapped external packages with given keyword arguments.
    This only happens once when Tölvera is first initialised.

    Args:
        x (int): Width of canvas. Default: 1920.
        y (int): Height of canvas. Default: 1080.
        osc (bool): Enable OSC. Default: False.
        iml (bool): Enable IML. Default: False.
        cv (bool): Enable CV. Default: False.
        see also kwargs for Taichi, OSC, IMLDict, and CV.
    """
    self.name = "Tölvera Context"
    self.name_clean = clean_name(self.name)
    print(f"[{self.name}] Initializing context...")
    self.x = kwargs.get("x", 1920)
    self.y = kwargs.get("y", 1080)
    self.ti = Taichi(self, **kwargs)
    self.i = ti.field(ti.i32, ())
    self.show = self.ti.show
    self.canvas = Pixels(self, **kwargs)
    self.s = StateDict(self)
    self.osc = kwargs.get("osc", False)
    self.iml = kwargs.get("iml", False)
    self.cv = kwargs.get("cv", False)
    self.hands = kwargs.get("hands", False)
    self.pose = kwargs.get("pose", False)
    self.face = kwargs.get("face", False)
    self.face_mesh = kwargs.get("face_mesh", False)
    if self.osc:
        self.osc = OSC(self, **kwargs)
    if self.iml:
        self.iml = IMLDict(self)
    if self.cv:
        self.cv = CV(self, **kwargs)
        if self.hands:
            self.hands = MPHands(self, **kwargs)
        if self.pose:
            self.pose = MPPose(self, **kwargs)
        if self.face:
            self.face = MPFace(self, **kwargs)
        if self.face_mesh:
            self.face_mesh = MPFaceMesh(self, **kwargs)
    self._cleanup_fns = []
    self.tolveras = {}
    print(f"[{self.name}] Context initialisation complete.")

remove(name)

Remove Tölvera by name.

Parameters:

Name Type Description Default
name str

Name of Tölvera to delete.

required
Source code in src/tolvera/context.py
def remove(self, name):
    """
    Remove Tölvera by name.

    Args:
        name (str): Name of Tölvera to delete.
    """
    print(f"[{self.name}] Deleting tolvera='{name}' from context.")
    del self.tolveras[name]

render(f=None, **kwargs)

Render Tölvera with given function and keyword arguments.

Parameters:

Name Type Description Default
f function

Function to run. Defaults to None.

None
Source code in src/tolvera/context.py
def render(self, f=None, **kwargs):
    """Render Tölvera with given function and keyword arguments.

    Args:
        f (function, optional): Function to run. Defaults to None.
    """
    try:
        self.run(f, **kwargs)
    except KeyboardInterrupt:
        self.stop()

run(f=None, **kwargs)

Run Tölvera with given render function and keyword arguments. This function will run inside a locked thread until KeyboardInterrupt/exit. It runs the render function, updates the OSC map (if enabled), and shows the pixels.

Parameters:

Name Type Description Default
f

Function to run.

None
**kwargs

Keyword arguments for function.

{}
Source code in src/tolvera/context.py
def run(self, f=None, **kwargs):
    """
    Run Tölvera with given render function and keyword arguments.
    This function will run inside a locked thread until KeyboardInterrupt/exit.
    It runs the render function, updates the OSC map (if enabled), and shows the pixels.

    Args:
        f: Function to run.
        **kwargs: Keyword arguments for function.
    """
    if f is not None:
        print(f"[{self.name}] Running with render function {f.__name__}...")
    else:
        print(f"[{self.name}] Running with no render function...")
    while self.ti.window.running:
        # print(kwargs)
        # exit()
        # gui = kwargs.get('gui', None)
        # if gui is not None:
        #     gui()
        # with self.ti.gui.sub_window("Sub Window", 0.1, 0.1, 0.2, 0.2) as w:
        #     w.text("text")
        with _lock:
            self.step(f, **kwargs)

stop()

Run cleanup functions and exit.

Source code in src/tolvera/context.py
def stop(self):
    """
    Run cleanup functions and exit.
    """
    print(f"\n[{self.name}] Stopping {self.name}...")
    for f in self._cleanup_fns:
        print(f"\n[{self.name}] Running cleanup function {f.__name__}...")
        f()
    print(f"\n[{self.name}] Exiting {self.name}...")
    exit(0)

dotdict

Bases: dict

dot.notation access to dictionary attributes

Source code in src/tolvera/utils.py
class dotdict(dict):
    """dot.notation access to dictionary attributes"""
    __getattr__ = dict.get
    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__

create_and_validate_slice(arg, target_array)

Creates and validates a slice object based on the target array.

Source code in src/tolvera/utils.py
def create_and_validate_slice(
    arg: Union[int, tuple[int, ...], slice], target_array: np.ndarray
) -> slice:
    """
    Creates and validates a slice object based on the target array.
    """
    try:
        slice_obj = create_safe_slice(arg)
        if not validate_slice(slice_obj, target_array):
            raise ValueError(f"Invalid slice: {slice_obj}")
        return slice_obj
    except Exception as e:
        raise type(e)(f"Error creating slice: {e}")

create_ndslices(dims)

Create a multi-dimensional slice from a list of tuples.

Parameters:

Name Type Description Default
dims list[tuple]

A list of tuples containing the slice parameters for each dimension.

required

Returns:

Type Description
s_

np.s_: A multi-dimensional slice object.

Source code in src/tolvera/utils.py
def create_ndslices(dims: list[tuple]) -> np.s_:
    """
    Create a multi-dimensional slice from a list of tuples.

    Args:
        dims (list[tuple]): A list of tuples containing the slice parameters for each dimension.

    Returns:
        np.s_: A multi-dimensional slice object.
    """
    return np.s_[tuple(slice(*dim) if isinstance(dim, tuple) else dim for dim in dims)]

create_safe_slice(arg)

Creates a slice object based on the input argument.

Parameters:

Name Type Description Default
arg (int, tuple, slice)

The argument for creating the slice. It can be an integer, a tuple with slice parameters, or a slice object itself.

required

Returns:

Name Type Description
slice slice

A slice object created based on the provided argument.

Source code in src/tolvera/utils.py
def create_safe_slice(arg: Union[int, tuple[int, ...], slice]) -> slice:
    """
    Creates a slice object based on the input argument.

    Args:
        arg (int, tuple, slice): The argument for creating the slice. It can be an integer,
                                 a tuple with slice parameters, or a slice object itself.

    Returns:
        slice: A slice object created based on the provided argument.
    """
    try:
        if isinstance(arg, slice):
            return arg
        elif isinstance(arg, tuple):
            return slice(*arg)
        elif isinstance(arg, int):
            return slice(arg, arg + 1)
        else:
            raise TypeError(f"Invalid slice type: {type(arg)} {arg}")
    except Exception as e:
        raise type(e)(f"[create_safe_slice] Error creating slice: {e}")

findsource(object)

Return the entire source file and starting line number for an object. For interactively-defined objects, the 'file' is the interpreter's history.

The argument may be a module, class, method, function, traceback, frame, or code object. The source code is returned as a list of all the lines in the file and the line number indexes a line in that list. An IOError is raised if the source code cannot be retrieved, while a TypeError is raised for objects where the source code is unavailable (e.g. builtins).

Source code in src/tolvera/patches.py
def findsource(object):
    # print(f"[dill.source.findsource] PATCHED")

    """Return the entire source file and starting line number for an object.
    For interactively-defined objects, the 'file' is the interpreter's history.

    The argument may be a module, class, method, function, traceback, frame,
    or code object.  The source code is returned as a list of all the lines
    in the file and the line number indexes a line in that list.  An IOError
    is raised if the source code cannot be retrieved, while a TypeError is
    raised for objects where the source code is unavailable (e.g. builtins)."""

    def patched_getfile(module):
        # set file = None when module.__package__ == 'asyncio'
        # print(f"[dill.source.patched_getfile] module={module}\nmodule.__package__={module.__package__}\nmodule.__name__={module.__name__}")
        if module.__package__ == "asyncio":
            raise TypeError
        # if module.__package__ == 'sardine':
        #     raise TypeError
        ret = getfile(module)
        return ret

    module = getmodule(object)
    # try: file = getfile(module)
    try:
        file = patched_getfile(module)
    except TypeError:
        file = None
    # correctly compute `is_module_main` when in asyncio
    is_module_main = module and module.__name__ == "__main__" and not file
    # is_module_main = (module and module.__name__ == '__main__' or module.__name__ == 'sardine' and not file)
    print(
        f"[dill.source.findsource] module: {module}, file: {file}, is_module_main: {is_module_main}"
    )
    if IS_IPYTHON and is_module_main:
        # FIXME: quick fix for functions and classes in IPython interpreter
        try:
            file = getfile(object)
            sourcefile = getsourcefile(object)
        except TypeError:
            if isclass(object):
                for object_method in filter(isfunction, object.__dict__.values()):
                    # look for a method of the class
                    file_candidate = getfile(object_method)
                    if not file_candidate.startswith("<ipython-input-"):
                        continue
                    file = file_candidate
                    sourcefile = getsourcefile(object_method)
                    break
        if file:
            lines = linecache.getlines(file)
        else:
            # fallback to use history
            history = "\n".join(get_ipython().history_manager.input_hist_parsed)
            lines = [line + "\n" for line in history.splitlines()]
    # use readline when working in interpreter (i.e. __main__ and not file)
    elif is_module_main:
        try:
            import readline

            err = ""
        except ImportError:
            import sys

            err = sys.exc_info()[1].args[0]
            if sys.platform[:3] == "win":
                err += ", please install 'pyreadline'"
        if err:
            raise IOError(err)
        lbuf = readline.get_current_history_length()
        lines = [readline.get_history_item(i) + "\n" for i in range(1, lbuf)]
    else:
        try:  # special handling for class instances
            if not isclass(object) and isclass(type(object)):  # __class__
                file = getfile(module)
                sourcefile = getsourcefile(module)
            else:  # builtins fail with a TypeError
                file = getfile(object)
                sourcefile = getsourcefile(object)
        except (TypeError, AttributeError):  # fail with better error
            file = getfile(object)
            sourcefile = getsourcefile(object)
        if not sourcefile and file[:1] + file[-1:] != "<>":
            raise IOError("source code not available")
        file = sourcefile if sourcefile else file

        module = getmodule(object, file)
        if module:
            lines = linecache.getlines(file, module.__dict__)
        else:
            lines = linecache.getlines(file)

    if not lines:
        raise IOError("could not extract source code")

    # FIXME: all below may fail if exec used (i.e. exec('f = lambda x:x') )
    if ismodule(object):
        return lines, 0

    # NOTE: beneficial if search goes from end to start of buffer history
    name = pat1 = obj = ""
    pat2 = r"^(\s*@)"
    #   pat1b = r'^(\s*%s\W*=)' % name #FIXME: finds 'f = decorate(f)', not exec
    if ismethod(object):
        name = object.__name__
        if name == "<lambda>":
            pat1 = r"(.*(?<!\w)lambda(:|\s))"
        else:
            pat1 = r"^(\s*def\s)"
        object = object.__func__
    if isfunction(object):
        name = object.__name__
        if name == "<lambda>":
            pat1 = r"(.*(?<!\w)lambda(:|\s))"
            obj = object  # XXX: better a copy?
        else:
            pat1 = r"^(\s*def\s)"
        object = object.__code__
    if istraceback(object):
        object = object.tb_frame
    if isframe(object):
        object = object.f_code
    if iscode(object):
        if not hasattr(object, "co_firstlineno"):
            raise IOError("could not find function definition")
        # stdin = object.co_filename == '<stdin>'
        stdin = object.co_filename in ("<console>", "<stdin>")
        # print(f"[dill.source.findsource] object.co_filename: {object.co_filename}, stdin: {stdin}")
        if stdin:
            lnum = len(lines) - 1  # can't get lnum easily, so leverage pat
            if not pat1:
                pat1 = r"^(\s*def\s)|(.*(?<!\w)lambda(:|\s))|^(\s*@)"
        else:
            lnum = object.co_firstlineno - 1
            pat1 = r"^(\s*def\s)|(.*(?<!\w)lambda(:|\s))|^(\s*@)"
        pat1 = re.compile(pat1)
        pat2 = re.compile(pat2)
        # XXX: candidate_lnum = [n for n in range(lnum) if pat1.match(lines[n])]
        while lnum > 0:  # XXX: won't find decorators in <stdin> ?
            line = lines[lnum]
            if pat1.match(line):
                if not stdin:
                    break  # co_firstlineno does the job
                if name == "<lambda>":  # hackery needed to confirm a match
                    if _matchlambda(obj, line):
                        break
                else:  # not a lambda, just look for the name
                    if name in line:  # need to check for decorator...
                        hats = 0
                        for _lnum in range(lnum - 1, -1, -1):
                            if pat2.match(lines[_lnum]):
                                hats += 1
                            else:
                                break
                        lnum = lnum - hats
                        break
            lnum = lnum - 1
        return lines, lnum

    try:  # turn instances into classes
        if not isclass(object) and isclass(type(object)):  # __class__
            object = object.__class__  # XXX: sometimes type(class) is better?
            # XXX: we don't find how the instance was built
    except AttributeError:
        pass
    if isclass(object):
        name = object.__name__
        pat = re.compile(r"^(\s*)class\s*" + name + r"\b")
        # make some effort to find the best matching class definition:
        # use the one with the least indentation, which is the one
        # that's most probably not inside a function definition.
        candidates = []
        for i in range(len(lines) - 1, -1, -1):
            match = pat.match(lines[i])
            if match:
                # if it's at toplevel, it's already the best one
                if lines[i][0] == "c":
                    return lines, i
                # else add whitespace to candidate list
                candidates.append((match.group(1), i))
        if candidates:
            # this will sort by whitespace, and by line number,
            # less whitespace first  #XXX: should sort high lnum before low
            candidates.sort()
            return lines, candidates[0][1]
        else:
            raise IOError("could not find class definition")
    raise IOError("could not find code object")

flatten(lst)

Flatten a nested list or return a non-nested list as is.

Source code in src/tolvera/utils.py
def flatten(lst):
    """Flatten a nested list or return a non-nested list as is."""
    if all(isinstance(el, list) for el in lst):
        return [item for sublist in lst for item in sublist]
    return lst

generic_slice(array, slice_params)

Slices a NumPy array based on a tuple of slice parameters for each dimension.

Parameters:

Name Type Description Default
array ndarray

The array to be sliced.

required
slice_params tuple

A tuple where each item is either an integer, a tuple with slice parameters, or a slice object.

required

Returns:

Name Type Description
ndarray ndarray

The sliced array.

Source code in src/tolvera/utils.py
def generic_slice(
    array: np.ndarray,
    slice_params: Union[
        tuple[Union[int, tuple[int, ...], slice], ...],
        Union[int, tuple[int, ...], slice],
    ],
) -> np.ndarray:
    """
    Slices a NumPy array based on a tuple of slice parameters for each dimension.

    Args:
        array (np.ndarray): The array to be sliced.
        slice_params (tuple): A tuple where each item is either an integer, a tuple with
                             slice parameters, or a slice object.

    Returns:
        ndarray: The sliced array.
    """
    if not isinstance(slice_params, tuple):
        slice_params = (slice_params,)
    slices = tuple(create_safe_slice(param) for param in slice_params)
    return array.__getitem__(slices)

time_function(func, *args, **kwargs)

Time how long it takes to run a function and print the result

Source code in src/tolvera/utils.py
def time_function(func, *args, **kwargs):
    """Time how long it takes to run a function and print the result"""
    start = time.time()
    ret = func(*args, **kwargs)
    end = time.time()
    print(f"[Tolvera.utils] {func.__name__}() ran in {end-start:.4f}s")
    if ret is not None:
        return (ret, end - start)
    return end - start

validate_json_path(path)

Validate a JSON file path. It uses validate_path for initial validation.

Parameters:

Name Type Description Default
path str

The JSON file path to be validated.

required

Returns:

Name Type Description
bool bool

True if the path is a valid JSON file path, raises an exception otherwise.

Raises:

Type Description
ValueError

If the path does not end with '.json'.

Source code in src/tolvera/utils.py
def validate_json_path(path: str) -> bool:
    """
    Validate a JSON file path. It uses validate_path for initial validation.

    Args:
        path (str): The JSON file path to be validated.

    Returns:
        bool: True if the path is a valid JSON file path, raises an exception otherwise.

    Raises:
        ValueError: If the path does not end with '.json'.
    """
    # Using validate_path for basic path validation
    validate_path(path)

    if not path.endswith(".json"):
        raise ValueError("Path should end with '.json'")

    return True

validate_path(path)

Validate a path using os.path and pathlib.

Parameters:

Name Type Description Default
path str

The path to be validated.

required

Returns:

Name Type Description
bool bool

True if the path is valid, raises an exception otherwise.

Raises:

Type Description
TypeError

If the input is not a string.

FileNotFoundError

If the path does not exist.

PermissionError

If the path is not accessible.

Source code in src/tolvera/utils.py
def validate_path(path: str) -> bool:
    """
    Validate a path using os.path and pathlib.

    Args:
        path (str): The path to be validated.

    Returns:
        bool: True if the path is valid, raises an exception otherwise.

    Raises:
        TypeError: If the input is not a string.
        FileNotFoundError: If the path does not exist.
        PermissionError: If the path is not accessible.
    """
    if not isinstance(path, str):
        raise TypeError(f"Expected a string for path, but received {type(path)}")

    path_obj = Path(path)
    if not path_obj.is_file():
        raise FileNotFoundError(f"The path {path} does not exist or is not a file")

    if not os.access(path, os.R_OK):
        raise PermissionError(f"The path {path} is not accessible")

    return True

validate_slice(slice_obj, target_array)

Validates if the given slice object is applicable to the target ndarray.

Parameters:

Name Type Description Default
slice_obj tuple[slice]

A tuple containing slice objects for each dimension.

required
target_array ndarray

The array to be sliced.

required

Returns:

Name Type Description
bool bool

True if the slice is valid for the given array, False otherwise.

Source code in src/tolvera/utils.py
def validate_slice(slice_obj: tuple[slice], target_array: np.ndarray) -> bool:
    """
    Validates if the given slice object is applicable to the target ndarray.

    Args:
        slice_obj (tuple[slice]): A tuple containing slice objects for each dimension.
        target_array (np.ndarray): The array to be sliced.

    Returns:
        bool: True if the slice is valid for the given array, False otherwise.
    """
    if len(slice_obj) != target_array.ndim:
        return False

    for sl, size in zip(slice_obj, target_array.shape):
        # Check if slice start and stop are within the dimension size
        start, stop, _ = sl.indices(size)
        if not (0 <= start < size and (0 <= stop <= size or stop == -1)):
            return False
    return True