;; Halftone/dot effect - built from primitive xector operations ;; ;; Uses: ;; pool-frame - downsample to cell luminances ;; cell-indices - which cell each pixel belongs to ;; gather - look up cell value for each pixel ;; local-x/y-norm - position within cell [0,1] ;; where - conditional per-pixel (require-primitives "xector") (define-effect halftone :params ( (cell-size :type int :default 12 :range [4 32] :desc "Size of halftone cells") (dot-scale :type float :default 0.9 :range [0.1 1.0] :desc "Max dot radius") (invert :type bool :default false :desc "Invert (white dots on black)") ) (let* ( ;; Pool frame to get luminance per cell (pooled (pool-frame frame cell-size)) (cell-lum (nth pooled 3)) ; luminance is 4th element ;; For each output pixel, get its cell index (cell-idx (cell-indices frame cell-size)) ;; Get cell luminance for each pixel (pixel-lum (α/ (gather cell-lum cell-idx) 255)) ;; Position within cell, normalized to [-0.5, 0.5] (lx (α- (local-x-norm frame cell-size) 0.5)) (ly (α- (local-y-norm frame cell-size) 0.5)) ;; Distance from cell center (0 at center, ~0.7 at corners) (dist (αsqrt (α+ (α² lx) (α² ly)))) ;; Radius based on luminance (brighter = bigger dot) (radius (α* (if invert (α- 1 pixel-lum) pixel-lum) (α* dot-scale 0.5))) ;; Is this pixel inside the dot? (inside (α< dist radius)) ;; Output color (fg (if invert 255 0)) (bg (if invert 0 255)) (out (where inside fg bg))) ;; Grayscale output (rgb out out out)))