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