Add JAX typography, xector primitives, deferred effect chains, and GPU streaming
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m28s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m28s
- Add JAX text rendering with font atlas, styled text placement, and typography primitives - Add xector (element-wise/reduction) operations library and sexp effects - Add deferred effect chain fusion for JIT-compiled effect pipelines - Expand drawing primitives with font management, alignment, shadow, and outline - Add interpreter support for function-style define and require - Add GPU persistence mode and hardware decode support to streaming - Add new sexp effects: cell_pattern, halftone, mosaic, and derived definitions - Add path registry for asset resolution - Add integration, primitives, and xector tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
206
sexp_effects/derived.sexp
Normal file
206
sexp_effects/derived.sexp
Normal file
@@ -0,0 +1,206 @@
|
||||
;; Derived Operations
|
||||
;;
|
||||
;; These are built from true primitives using S-expressions.
|
||||
;; Load with: (require "derived")
|
||||
|
||||
;; =============================================================================
|
||||
;; Math Helpers (derivable from where + basic ops)
|
||||
;; =============================================================================
|
||||
|
||||
;; Absolute value
|
||||
(define (abs x) (where (< x 0) (- x) x))
|
||||
|
||||
;; Minimum of two values
|
||||
(define (min2 a b) (where (< a b) a b))
|
||||
|
||||
;; Maximum of two values
|
||||
(define (max2 a b) (where (> a b) a b))
|
||||
|
||||
;; Clamp x to range [lo, hi]
|
||||
(define (clamp x lo hi) (max2 lo (min2 hi x)))
|
||||
|
||||
;; Square of x
|
||||
(define (sq x) (* x x))
|
||||
|
||||
;; Linear interpolation: a*(1-t) + b*t
|
||||
(define (lerp a b t) (+ (* a (- 1 t)) (* b t)))
|
||||
|
||||
;; Smooth interpolation between edges
|
||||
(define (smoothstep edge0 edge1 x)
|
||||
(let ((t (clamp (/ (- x edge0) (- edge1 edge0)) 0 1)))
|
||||
(* t (* t (- 3 (* 2 t))))))
|
||||
|
||||
;; =============================================================================
|
||||
;; Channel Shortcuts (derivable from channel primitive)
|
||||
;; =============================================================================
|
||||
|
||||
;; Extract red channel as xector
|
||||
(define (red frame) (channel frame 0))
|
||||
|
||||
;; Extract green channel as xector
|
||||
(define (green frame) (channel frame 1))
|
||||
|
||||
;; Extract blue channel as xector
|
||||
(define (blue frame) (channel frame 2))
|
||||
|
||||
;; Convert to grayscale xector (ITU-R BT.601)
|
||||
(define (gray frame)
|
||||
(+ (* (red frame) 0.299)
|
||||
(* (green frame) 0.587)
|
||||
(* (blue frame) 0.114)))
|
||||
|
||||
;; Alias for gray
|
||||
(define (luminance frame) (gray frame))
|
||||
|
||||
;; =============================================================================
|
||||
;; Coordinate Generators (derivable from iota + repeat/tile)
|
||||
;; =============================================================================
|
||||
|
||||
;; X coordinate for each pixel [0, width)
|
||||
(define (x-coords frame) (tile (iota (width frame)) (height frame)))
|
||||
|
||||
;; Y coordinate for each pixel [0, height)
|
||||
(define (y-coords frame) (repeat (iota (height frame)) (width frame)))
|
||||
|
||||
;; Normalized X coordinate [0, 1]
|
||||
(define (x-norm frame) (/ (x-coords frame) (max2 1 (- (width frame) 1))))
|
||||
|
||||
;; Normalized Y coordinate [0, 1]
|
||||
(define (y-norm frame) (/ (y-coords frame) (max2 1 (- (height frame) 1))))
|
||||
|
||||
;; Distance from frame center for each pixel
|
||||
(define (dist-from-center frame)
|
||||
(let* ((cx (/ (width frame) 2))
|
||||
(cy (/ (height frame) 2))
|
||||
(dx (- (x-coords frame) cx))
|
||||
(dy (- (y-coords frame) cy)))
|
||||
(sqrt (+ (sq dx) (sq dy)))))
|
||||
|
||||
;; Normalized distance from center [0, ~1]
|
||||
(define (dist-norm frame)
|
||||
(let ((d (dist-from-center frame)))
|
||||
(/ d (max2 1 (βmax d)))))
|
||||
|
||||
;; =============================================================================
|
||||
;; Cell/Grid Operations (derivable from floor + basic math)
|
||||
;; =============================================================================
|
||||
|
||||
;; Cell row index for each pixel
|
||||
(define (cell-row frame cell-size) (floor (/ (y-coords frame) cell-size)))
|
||||
|
||||
;; Cell column index for each pixel
|
||||
(define (cell-col frame cell-size) (floor (/ (x-coords frame) cell-size)))
|
||||
|
||||
;; Number of cell rows
|
||||
(define (num-rows frame cell-size) (floor (/ (height frame) cell-size)))
|
||||
|
||||
;; Number of cell columns
|
||||
(define (num-cols frame cell-size) (floor (/ (width frame) cell-size)))
|
||||
|
||||
;; Flat cell index for each pixel
|
||||
(define (cell-indices frame cell-size)
|
||||
(+ (* (cell-row frame cell-size) (num-cols frame cell-size))
|
||||
(cell-col frame cell-size)))
|
||||
|
||||
;; Total number of cells
|
||||
(define (num-cells frame cell-size)
|
||||
(* (num-rows frame cell-size) (num-cols frame cell-size)))
|
||||
|
||||
;; X position within cell [0, cell-size)
|
||||
(define (local-x frame cell-size) (mod (x-coords frame) cell-size))
|
||||
|
||||
;; Y position within cell [0, cell-size)
|
||||
(define (local-y frame cell-size) (mod (y-coords frame) cell-size))
|
||||
|
||||
;; Normalized X within cell [0, 1]
|
||||
(define (local-x-norm frame cell-size)
|
||||
(/ (local-x frame cell-size) (max2 1 (- cell-size 1))))
|
||||
|
||||
;; Normalized Y within cell [0, 1]
|
||||
(define (local-y-norm frame cell-size)
|
||||
(/ (local-y frame cell-size) (max2 1 (- cell-size 1))))
|
||||
|
||||
;; =============================================================================
|
||||
;; Fill Operations (derivable from iota)
|
||||
;; =============================================================================
|
||||
|
||||
;; Xector of n zeros
|
||||
(define (zeros n) (* (iota n) 0))
|
||||
|
||||
;; Xector of n ones
|
||||
(define (ones n) (+ (zeros n) 1))
|
||||
|
||||
;; Xector of n copies of val
|
||||
(define (fill val n) (+ (zeros n) val))
|
||||
|
||||
;; Xector of zeros matching x's length
|
||||
(define (zeros-like x) (* x 0))
|
||||
|
||||
;; Xector of ones matching x's length
|
||||
(define (ones-like x) (+ (zeros-like x) 1))
|
||||
|
||||
;; =============================================================================
|
||||
;; Pooling (derivable from group-reduce)
|
||||
;; =============================================================================
|
||||
|
||||
;; Pool a channel by cell index
|
||||
(define (pool-channel chan cell-idx num-cells)
|
||||
(group-reduce chan cell-idx num-cells "mean"))
|
||||
|
||||
;; Pool red channel to cells
|
||||
(define (pool-red frame cell-size)
|
||||
(pool-channel (red frame)
|
||||
(cell-indices frame cell-size)
|
||||
(num-cells frame cell-size)))
|
||||
|
||||
;; Pool green channel to cells
|
||||
(define (pool-green frame cell-size)
|
||||
(pool-channel (green frame)
|
||||
(cell-indices frame cell-size)
|
||||
(num-cells frame cell-size)))
|
||||
|
||||
;; Pool blue channel to cells
|
||||
(define (pool-blue frame cell-size)
|
||||
(pool-channel (blue frame)
|
||||
(cell-indices frame cell-size)
|
||||
(num-cells frame cell-size)))
|
||||
|
||||
;; Pool grayscale to cells
|
||||
(define (pool-gray frame cell-size)
|
||||
(pool-channel (gray frame)
|
||||
(cell-indices frame cell-size)
|
||||
(num-cells frame cell-size)))
|
||||
|
||||
;; =============================================================================
|
||||
;; Blending (derivable from math)
|
||||
;; =============================================================================
|
||||
|
||||
;; Additive blend
|
||||
(define (blend-add a b) (clamp (+ a b) 0 255))
|
||||
|
||||
;; Multiply blend (normalized)
|
||||
(define (blend-multiply a b) (* (/ a 255) b))
|
||||
|
||||
;; Screen blend
|
||||
(define (blend-screen a b) (- 255 (* (/ (- 255 a) 255) (- 255 b))))
|
||||
|
||||
;; Overlay blend
|
||||
(define (blend-overlay a b)
|
||||
(where (< a 128)
|
||||
(* 2 (/ (* a b) 255))
|
||||
(- 255 (* 2 (/ (* (- 255 a) (- 255 b)) 255)))))
|
||||
|
||||
;; =============================================================================
|
||||
;; Simple Effects (derivable from primitives)
|
||||
;; =============================================================================
|
||||
|
||||
;; Invert a channel (255 - c)
|
||||
(define (invert-channel c) (- 255 c))
|
||||
|
||||
;; Binary threshold
|
||||
(define (threshold-channel c thresh) (where (> c thresh) 255 0))
|
||||
|
||||
;; Reduce to n levels
|
||||
(define (posterize-channel c levels)
|
||||
(let ((step (/ 255 (- levels 1))))
|
||||
(* (round (/ c step)) step)))
|
||||
@@ -5,7 +5,7 @@
|
||||
:params (
|
||||
(char_size :type int :default 8 :range [4 32])
|
||||
(alphabet :type string :default "standard")
|
||||
(color_mode :type string :default "color" :desc ""color", "mono", "invert", or any color name/hex")
|
||||
(color_mode :type string :default "color" :desc "color, mono, invert, or any color name/hex")
|
||||
(background_color :type string :default "black" :desc "background color name/hex")
|
||||
(invert_colors :type int :default 0 :desc "swap foreground and background colors")
|
||||
(contrast :type float :default 1.5 :range [1 3])
|
||||
|
||||
65
sexp_effects/effects/cell_pattern.sexp
Normal file
65
sexp_effects/effects/cell_pattern.sexp
Normal file
@@ -0,0 +1,65 @@
|
||||
;; Cell Pattern effect - custom patterns within cells
|
||||
;;
|
||||
;; Demonstrates building arbitrary per-cell visuals from primitives.
|
||||
;; Uses local coordinates within cells to draw patterns scaled by luminance.
|
||||
|
||||
(require-primitives "xector")
|
||||
|
||||
(define-effect cell_pattern
|
||||
:params (
|
||||
(cell-size :type int :default 16 :range [8 48] :desc "Cell size")
|
||||
(pattern :type string :default "diagonal" :desc "Pattern: diagonal, cross, ring")
|
||||
)
|
||||
(let* (
|
||||
;; Pool to get cell colors
|
||||
(pooled (pool-frame frame cell-size))
|
||||
(cell-r (nth pooled 0))
|
||||
(cell-g (nth pooled 1))
|
||||
(cell-b (nth pooled 2))
|
||||
(cell-lum (α/ (nth pooled 3) 255))
|
||||
|
||||
;; Cell indices for each pixel
|
||||
(cell-idx (cell-indices frame cell-size))
|
||||
|
||||
;; Look up cell values for each pixel
|
||||
(pix-r (gather cell-r cell-idx))
|
||||
(pix-g (gather cell-g cell-idx))
|
||||
(pix-b (gather cell-b cell-idx))
|
||||
(pix-lum (gather cell-lum cell-idx))
|
||||
|
||||
;; Local position within cell [0, 1]
|
||||
(lx (local-x-norm frame cell-size))
|
||||
(ly (local-y-norm frame cell-size))
|
||||
|
||||
;; Pattern mask based on pattern type
|
||||
(mask
|
||||
(cond
|
||||
;; Diagonal lines - thickness based on luminance
|
||||
((= pattern "diagonal")
|
||||
(let* ((diag (αmod (α+ lx ly) 0.25))
|
||||
(thickness (α* pix-lum 0.125)))
|
||||
(α< diag thickness)))
|
||||
|
||||
;; Cross pattern
|
||||
((= pattern "cross")
|
||||
(let* ((cx (αabs (α- lx 0.5)))
|
||||
(cy (αabs (α- ly 0.5)))
|
||||
(thickness (α* pix-lum 0.25)))
|
||||
(αor (α< cx thickness) (α< cy thickness))))
|
||||
|
||||
;; Ring pattern
|
||||
((= pattern "ring")
|
||||
(let* ((dx (α- lx 0.5))
|
||||
(dy (α- ly 0.5))
|
||||
(dist (αsqrt (α+ (α² dx) (α² dy))))
|
||||
(target (α* pix-lum 0.4))
|
||||
(thickness 0.05))
|
||||
(α< (αabs (α- dist target)) thickness)))
|
||||
|
||||
;; Default: solid
|
||||
(else (α> pix-lum 0)))))
|
||||
|
||||
;; Apply mask: show cell color where mask is true, black elsewhere
|
||||
(rgb (where mask pix-r 0)
|
||||
(where mask pix-g 0)
|
||||
(where mask pix-b 0))))
|
||||
@@ -6,10 +6,10 @@
|
||||
(num_echoes :type int :default 4 :range [1 20])
|
||||
(decay :type float :default 0.5 :range [0 1])
|
||||
)
|
||||
(let* ((buffer (state-get 'buffer (list)))
|
||||
(let* ((buffer (state-get "buffer" (list)))
|
||||
(new-buffer (take (cons frame buffer) (+ num_echoes 1))))
|
||||
(begin
|
||||
(state-set 'buffer new-buffer)
|
||||
(state-set "buffer" new-buffer)
|
||||
;; Blend frames with decay
|
||||
(if (< (length new-buffer) 2)
|
||||
frame
|
||||
|
||||
49
sexp_effects/effects/halftone.sexp
Normal file
49
sexp_effects/effects/halftone.sexp
Normal file
@@ -0,0 +1,49 @@
|
||||
;; 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)))
|
||||
30
sexp_effects/effects/mosaic.sexp
Normal file
30
sexp_effects/effects/mosaic.sexp
Normal file
@@ -0,0 +1,30 @@
|
||||
;; Mosaic effect - built from primitive xector operations
|
||||
;;
|
||||
;; Uses:
|
||||
;; pool-frame - downsample to cell averages
|
||||
;; cell-indices - which cell each pixel belongs to
|
||||
;; gather - look up cell value for each pixel
|
||||
|
||||
(require-primitives "xector")
|
||||
|
||||
(define-effect mosaic
|
||||
:params (
|
||||
(cell-size :type int :default 16 :range [4 64] :desc "Size of mosaic cells")
|
||||
)
|
||||
(let* (
|
||||
;; Pool frame to get average color per cell (returns r,g,b,lum xectors)
|
||||
(pooled (pool-frame frame cell-size))
|
||||
(cell-r (nth pooled 0))
|
||||
(cell-g (nth pooled 1))
|
||||
(cell-b (nth pooled 2))
|
||||
|
||||
;; For each output pixel, get its cell index
|
||||
(cell-idx (cell-indices frame cell-size))
|
||||
|
||||
;; Gather: look up cell color for each pixel
|
||||
(out-r (gather cell-r cell-idx))
|
||||
(out-g (gather cell-g cell-idx))
|
||||
(out-b (gather cell-b cell-idx)))
|
||||
|
||||
;; Reconstruct frame
|
||||
(rgb out-r out-g out-b)))
|
||||
@@ -5,9 +5,9 @@
|
||||
:params (
|
||||
(thickness :type int :default 2 :range [1 10])
|
||||
(threshold :type int :default 100 :range [20 300])
|
||||
(color :type list :default (list 0 0 0)
|
||||
(color :type list :default (list 0 0 0))
|
||||
(fill_mode :type string :default "original")
|
||||
)
|
||||
(fill_mode "original"))
|
||||
(let* ((edge-img (image:edge-detect frame (/ threshold 2) threshold))
|
||||
(dilated (if (> thickness 1)
|
||||
(dilate edge-img thickness)
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
:params (
|
||||
(frame_rate :type int :default 12 :range [1 60])
|
||||
)
|
||||
(let* ((held (state-get 'held nil))
|
||||
(held-until (state-get 'held-until 0))
|
||||
(let* ((held (state-get "held" nil))
|
||||
(held-until (state-get "held-until" 0))
|
||||
(frame-duration (/ 1 frame_rate)))
|
||||
(if (or (core:is-nil held) (>= t held-until))
|
||||
(begin
|
||||
(state-set 'held (copy frame))
|
||||
(state-set 'held-until (+ t frame-duration))
|
||||
(state-set "held" (copy frame))
|
||||
(state-set "held-until" (+ t frame-duration))
|
||||
frame)
|
||||
held)))
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
:params (
|
||||
(persistence :type float :default 0.8 :range [0 0.99])
|
||||
)
|
||||
(let* ((buffer (state-get 'buffer nil))
|
||||
(let* ((buffer (state-get "buffer" nil))
|
||||
(current frame))
|
||||
(if (= buffer nil)
|
||||
(begin
|
||||
(state-set 'buffer (copy frame))
|
||||
(state-set "buffer" (copy frame))
|
||||
frame)
|
||||
(let* ((faded (blending:blend-images buffer
|
||||
(make-image (image:width frame) (image:height frame) (list 0 0 0))
|
||||
(- 1 persistence)))
|
||||
(result (blending:blend-mode faded current "lighten")))
|
||||
(begin
|
||||
(state-set 'buffer result)
|
||||
(state-set "buffer" result)
|
||||
result)))))
|
||||
|
||||
44
sexp_effects/effects/xector_feathered_blend.sexp
Normal file
44
sexp_effects/effects/xector_feathered_blend.sexp
Normal file
@@ -0,0 +1,44 @@
|
||||
;; Feathered blend - blend two same-size frames with distance-based falloff
|
||||
;; Center shows overlay, edges show background, with smooth transition
|
||||
|
||||
(require-primitives "xector")
|
||||
|
||||
(define-effect xector_feathered_blend
|
||||
:params (
|
||||
(inner-radius :type float :default 0.3 :range [0 1] :desc "Radius where overlay is 100% (fraction of size)")
|
||||
(fade-width :type float :default 0.2 :range [0 0.5] :desc "Width of fade region (fraction of size)")
|
||||
(overlay :type frame :default nil :desc "Frame to blend in center")
|
||||
)
|
||||
(let* (
|
||||
;; Get normalized distance from center (0 at center, ~1 at corners)
|
||||
(dist (dist-from-center frame))
|
||||
(max-dist (βmax dist))
|
||||
(dist-norm (α/ dist max-dist))
|
||||
|
||||
;; Calculate blend factor:
|
||||
;; - 1.0 when dist-norm < inner-radius (fully overlay)
|
||||
;; - 0.0 when dist-norm > inner-radius + fade-width (fully background)
|
||||
;; - linear ramp between
|
||||
(t (α/ (α- dist-norm inner-radius) fade-width))
|
||||
(blend (α- 1 (αclamp t 0 1)))
|
||||
(inv-blend (α- 1 blend))
|
||||
|
||||
;; Background channels
|
||||
(bg-r (red frame))
|
||||
(bg-g (green frame))
|
||||
(bg-b (blue frame)))
|
||||
|
||||
(if (nil? overlay)
|
||||
;; No overlay - visualize the blend mask
|
||||
(let ((vis (α* blend 255)))
|
||||
(rgb vis vis vis))
|
||||
|
||||
;; Blend overlay with background using the mask
|
||||
(let* ((ov-r (red overlay))
|
||||
(ov-g (green overlay))
|
||||
(ov-b (blue overlay))
|
||||
;; lerp: bg * (1-blend) + overlay * blend
|
||||
(r-out (α+ (α* bg-r inv-blend) (α* ov-r blend)))
|
||||
(g-out (α+ (α* bg-g inv-blend) (α* ov-g blend)))
|
||||
(b-out (α+ (α* bg-b inv-blend) (α* ov-b blend))))
|
||||
(rgb r-out g-out b-out)))))
|
||||
34
sexp_effects/effects/xector_grain.sexp
Normal file
34
sexp_effects/effects/xector_grain.sexp
Normal file
@@ -0,0 +1,34 @@
|
||||
;; Film grain effect using xector operations
|
||||
;; Demonstrates random xectors and mixing scalar/xector math
|
||||
|
||||
(require-primitives "xector")
|
||||
|
||||
(define-effect xector_grain
|
||||
:params (
|
||||
(intensity :type float :default 0.2 :range [0 1] :desc "Grain intensity")
|
||||
(colored :type bool :default false :desc "Use colored grain")
|
||||
)
|
||||
(let* (
|
||||
;; Extract channels
|
||||
(r (red frame))
|
||||
(g (green frame))
|
||||
(b (blue frame))
|
||||
|
||||
;; Generate noise xector(s)
|
||||
;; randn-x generates normal distribution noise
|
||||
(grain-amount (* intensity 50)))
|
||||
|
||||
(if colored
|
||||
;; Colored grain: different noise per channel
|
||||
(let* ((nr (randn-x frame 0 grain-amount))
|
||||
(ng (randn-x frame 0 grain-amount))
|
||||
(nb (randn-x frame 0 grain-amount)))
|
||||
(rgb (αclamp (α+ r nr) 0 255)
|
||||
(αclamp (α+ g ng) 0 255)
|
||||
(αclamp (α+ b nb) 0 255)))
|
||||
|
||||
;; Monochrome grain: same noise for all channels
|
||||
(let ((n (randn-x frame 0 grain-amount)))
|
||||
(rgb (αclamp (α+ r n) 0 255)
|
||||
(αclamp (α+ g n) 0 255)
|
||||
(αclamp (α+ b n) 0 255))))))
|
||||
57
sexp_effects/effects/xector_inset_blend.sexp
Normal file
57
sexp_effects/effects/xector_inset_blend.sexp
Normal file
@@ -0,0 +1,57 @@
|
||||
;; Inset blend - fade a smaller frame into a larger background
|
||||
;; Uses distance-based alpha for smooth transition (no hard edges)
|
||||
|
||||
(require-primitives "xector")
|
||||
|
||||
(define-effect xector_inset_blend
|
||||
:params (
|
||||
(x :type int :default 0 :desc "X position of inset")
|
||||
(y :type int :default 0 :desc "Y position of inset")
|
||||
(fade-width :type int :default 50 :desc "Width of fade region in pixels")
|
||||
(overlay :type frame :default nil :desc "The smaller frame to inset")
|
||||
)
|
||||
(let* (
|
||||
;; Get dimensions
|
||||
(bg-h (first (list (nth (list (red frame)) 0)))) ;; TODO: need image:height
|
||||
(bg-w bg-h) ;; placeholder
|
||||
|
||||
;; For now, create a simple centered circular blend
|
||||
;; Distance from center of overlay position
|
||||
(cx (+ x (/ (- bg-w (* 2 x)) 2)))
|
||||
(cy (+ y (/ (- bg-h (* 2 y)) 2)))
|
||||
|
||||
;; Get coordinates as xectors
|
||||
(px (x-coords frame))
|
||||
(py (y-coords frame))
|
||||
|
||||
;; Distance from center
|
||||
(dx (α- px cx))
|
||||
(dy (α- py cy))
|
||||
(dist (αsqrt (α+ (α* dx dx) (α* dy dy))))
|
||||
|
||||
;; Inner radius (fully overlay) and outer radius (fully background)
|
||||
(inner-r (- (/ bg-w 2) x fade-width))
|
||||
(outer-r (- (/ bg-w 2) x))
|
||||
|
||||
;; Blend factor: 1.0 inside inner-r, 0.0 outside outer-r, linear between
|
||||
(t (α/ (α- dist inner-r) fade-width))
|
||||
(blend (α- 1 (αclamp t 0 1)))
|
||||
|
||||
;; Extract channels from both frames
|
||||
(bg-r (red frame))
|
||||
(bg-g (green frame))
|
||||
(bg-b (blue frame)))
|
||||
|
||||
;; If overlay provided, blend it
|
||||
(if overlay
|
||||
(let* ((ov-r (red overlay))
|
||||
(ov-g (green overlay))
|
||||
(ov-b (blue overlay))
|
||||
;; Linear blend: result = bg * (1-blend) + overlay * blend
|
||||
(r-out (α+ (α* bg-r (α- 1 blend)) (α* ov-r blend)))
|
||||
(g-out (α+ (α* bg-g (α- 1 blend)) (α* ov-g blend)))
|
||||
(b-out (α+ (α* bg-b (α- 1 blend)) (α* ov-b blend))))
|
||||
(rgb r-out g-out b-out))
|
||||
;; No overlay - just show the blend mask for debugging
|
||||
(let ((mask-vis (α* blend 255)))
|
||||
(rgb mask-vis mask-vis mask-vis)))))
|
||||
27
sexp_effects/effects/xector_threshold.sexp
Normal file
27
sexp_effects/effects/xector_threshold.sexp
Normal file
@@ -0,0 +1,27 @@
|
||||
;; Threshold effect using xector operations
|
||||
;; Demonstrates where (conditional select) and β (reduction) for normalization
|
||||
|
||||
(require-primitives "xector")
|
||||
|
||||
(define-effect xector_threshold
|
||||
:params (
|
||||
(threshold :type float :default 0.5 :range [0 1] :desc "Brightness threshold (0-1)")
|
||||
(invert :type bool :default false :desc "Invert the threshold")
|
||||
)
|
||||
(let* (
|
||||
;; Get grayscale luminance as xector
|
||||
(luma (gray frame))
|
||||
|
||||
;; Normalize to 0-1 range
|
||||
(luma-norm (α/ luma 255))
|
||||
|
||||
;; Create boolean mask: pixels above threshold
|
||||
(mask (if invert
|
||||
(α< luma-norm threshold)
|
||||
(α>= luma-norm threshold)))
|
||||
|
||||
;; Use where to select: white (255) if above threshold, black (0) if below
|
||||
(out (where mask 255 0)))
|
||||
|
||||
;; Output as grayscale (same value for R, G, B)
|
||||
(rgb out out out)))
|
||||
36
sexp_effects/effects/xector_vignette.sexp
Normal file
36
sexp_effects/effects/xector_vignette.sexp
Normal file
@@ -0,0 +1,36 @@
|
||||
;; Vignette effect using xector operations
|
||||
;; Demonstrates α (element-wise) and β (reduction) patterns
|
||||
|
||||
(require-primitives "xector")
|
||||
|
||||
(define-effect xector_vignette
|
||||
:params (
|
||||
(strength :type float :default 0.5 :range [0 1])
|
||||
(radius :type float :default 1.0 :range [0.5 2])
|
||||
)
|
||||
(let* (
|
||||
;; Get normalized distance from center for each pixel
|
||||
(dist (dist-from-center frame))
|
||||
|
||||
;; Calculate max distance (corner distance)
|
||||
(max-dist (* (βmax dist) radius))
|
||||
|
||||
;; Calculate brightness factor per pixel: 1 - (dist/max-dist * strength)
|
||||
;; Using explicit α operators
|
||||
(factor (α- 1 (α* (α/ dist max-dist) strength)))
|
||||
|
||||
;; Clamp factor to [0, 1]
|
||||
(factor (αclamp factor 0 1))
|
||||
|
||||
;; Extract channels as xectors
|
||||
(r (red frame))
|
||||
(g (green frame))
|
||||
(b (blue frame))
|
||||
|
||||
;; Apply factor to each channel (implicit element-wise via Xector operators)
|
||||
(r-out (* r factor))
|
||||
(g-out (* g factor))
|
||||
(b-out (* b factor)))
|
||||
|
||||
;; Combine back to frame
|
||||
(rgb r-out g-out b-out)))
|
||||
@@ -156,11 +156,21 @@ class Interpreter:
|
||||
if form == 'define':
|
||||
name = expr[1]
|
||||
if _is_symbol(name):
|
||||
# Simple define: (define name value)
|
||||
value = self.eval(expr[2], env)
|
||||
self.global_env.set(name.name, value)
|
||||
return value
|
||||
elif isinstance(name, list) and len(name) >= 1 and _is_symbol(name[0]):
|
||||
# Function define: (define (fn-name args...) body)
|
||||
# Desugars to: (define fn-name (lambda (args...) body))
|
||||
fn_name = name[0].name
|
||||
params = [p.name if _is_symbol(p) else p for p in name[1:]]
|
||||
body = expr[2]
|
||||
fn = Lambda(params, body, env)
|
||||
self.global_env.set(fn_name, fn)
|
||||
return fn
|
||||
else:
|
||||
raise SyntaxError(f"define requires symbol, got {name}")
|
||||
raise SyntaxError(f"define requires symbol or (name args...), got {name}")
|
||||
|
||||
# Define-effect
|
||||
if form == 'define-effect':
|
||||
@@ -276,6 +286,10 @@ class Interpreter:
|
||||
if form == 'require-primitives':
|
||||
return self._eval_require_primitives(expr, env)
|
||||
|
||||
# require - load .sexp file into current scope
|
||||
if form == 'require':
|
||||
return self._eval_require(expr, env)
|
||||
|
||||
# Function call
|
||||
fn = self.eval(head, env)
|
||||
args = [self.eval(arg, env) for arg in expr[1:]]
|
||||
@@ -488,6 +502,61 @@ class Interpreter:
|
||||
from .primitive_libs import load_primitive_library
|
||||
return load_primitive_library(name, path)
|
||||
|
||||
def _eval_require(self, expr: Any, env: Environment) -> Any:
|
||||
"""
|
||||
Evaluate require: load a .sexp file and evaluate its definitions.
|
||||
|
||||
Syntax:
|
||||
(require "derived") ; loads derived.sexp from sexp_effects/
|
||||
(require "path/to/file.sexp") ; loads from explicit path
|
||||
|
||||
Definitions from the file are added to the current environment.
|
||||
"""
|
||||
for lib_expr in expr[1:]:
|
||||
if _is_symbol(lib_expr):
|
||||
lib_name = lib_expr.name
|
||||
else:
|
||||
lib_name = lib_expr
|
||||
|
||||
# Find the .sexp file
|
||||
sexp_path = self._find_sexp_file(lib_name)
|
||||
if sexp_path is None:
|
||||
raise ValueError(f"Cannot find sexp file: {lib_name}")
|
||||
|
||||
# Parse and evaluate the file
|
||||
content = parse_file(sexp_path)
|
||||
|
||||
# Evaluate all top-level expressions
|
||||
if isinstance(content, list) and content and isinstance(content[0], list):
|
||||
for e in content:
|
||||
self.eval(e, env)
|
||||
else:
|
||||
self.eval(content, env)
|
||||
|
||||
return None
|
||||
|
||||
def _find_sexp_file(self, name: str) -> Optional[str]:
|
||||
"""Find a .sexp file by name."""
|
||||
# Try various locations
|
||||
candidates = [
|
||||
# Explicit path
|
||||
name,
|
||||
name + '.sexp',
|
||||
# In sexp_effects directory
|
||||
Path(__file__).parent / f'{name}.sexp',
|
||||
Path(__file__).parent / name,
|
||||
# In effects directory
|
||||
Path(__file__).parent / 'effects' / f'{name}.sexp',
|
||||
Path(__file__).parent / 'effects' / name,
|
||||
]
|
||||
|
||||
for path in candidates:
|
||||
p = Path(path) if not isinstance(path, Path) else path
|
||||
if p.exists() and p.is_file():
|
||||
return str(p)
|
||||
|
||||
return None
|
||||
|
||||
def _eval_ascii_fx_zone(self, expr: Any, env: Environment) -> Any:
|
||||
"""
|
||||
Evaluate ascii-fx-zone special form.
|
||||
@@ -876,8 +945,8 @@ class Interpreter:
|
||||
for pname, pdefault in effect.params.items():
|
||||
value = params.get(pname)
|
||||
if value is None:
|
||||
# Evaluate default if it's an expression (list)
|
||||
if isinstance(pdefault, list):
|
||||
# Evaluate default if it's an expression (list) or a symbol (like 'nil')
|
||||
if isinstance(pdefault, list) or _is_symbol(pdefault):
|
||||
value = self.eval(pdefault, env)
|
||||
else:
|
||||
value = pdefault
|
||||
|
||||
@@ -71,7 +71,8 @@ class Tokenizer:
|
||||
STRING = re.compile(r'"(?:[^"\\]|\\.)*"')
|
||||
NUMBER = re.compile(r'-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?')
|
||||
KEYWORD = re.compile(r':[a-zA-Z_][a-zA-Z0-9_-]*')
|
||||
SYMBOL = re.compile(r'[a-zA-Z_*+\-><=/!?][a-zA-Z0-9_*+\-><=/!?.:]*')
|
||||
# Symbol pattern includes Greek letters α (alpha) and β (beta) for xector operations
|
||||
SYMBOL = re.compile(r'[a-zA-Z_*+\-><=/!?αβ²λ][a-zA-Z0-9_*+\-><=/!?.:αβ²λ]*')
|
||||
|
||||
def __init__(self, text: str):
|
||||
self.text = text
|
||||
|
||||
@@ -1,126 +1,680 @@
|
||||
"""
|
||||
Drawing Primitives Library
|
||||
|
||||
Draw shapes, text, and characters on images.
|
||||
Draw shapes, text, and characters on images with sophisticated text handling.
|
||||
|
||||
Text Features:
|
||||
- Font loading from files or system fonts
|
||||
- Text measurement and fitting
|
||||
- Alignment (left/center/right, top/middle/bottom)
|
||||
- Opacity for fade effects
|
||||
- Multi-line text support
|
||||
- Shadow and outline effects
|
||||
"""
|
||||
import numpy as np
|
||||
import cv2
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import os
|
||||
import glob as glob_module
|
||||
from typing import Optional, Tuple, List, Union
|
||||
|
||||
|
||||
# Default font (will be loaded lazily)
|
||||
_default_font = None
|
||||
# =============================================================================
|
||||
# Font Management
|
||||
# =============================================================================
|
||||
|
||||
# Font cache: (path, size) -> font object
|
||||
_font_cache = {}
|
||||
|
||||
# Common system font directories
|
||||
FONT_DIRS = [
|
||||
"/usr/share/fonts",
|
||||
"/usr/local/share/fonts",
|
||||
"~/.fonts",
|
||||
"~/.local/share/fonts",
|
||||
"/System/Library/Fonts", # macOS
|
||||
"/Library/Fonts", # macOS
|
||||
"C:/Windows/Fonts", # Windows
|
||||
]
|
||||
|
||||
# Default fonts to try (in order of preference)
|
||||
DEFAULT_FONTS = [
|
||||
"DejaVuSans.ttf",
|
||||
"DejaVuSansMono.ttf",
|
||||
"Arial.ttf",
|
||||
"Helvetica.ttf",
|
||||
"FreeSans.ttf",
|
||||
"LiberationSans-Regular.ttf",
|
||||
]
|
||||
|
||||
|
||||
def _get_default_font(size=16):
|
||||
"""Get default font, creating if needed."""
|
||||
global _default_font
|
||||
if _default_font is None or _default_font.size != size:
|
||||
def _find_font_file(name: str) -> Optional[str]:
|
||||
"""Find a font file by name in system directories."""
|
||||
# If it's already a full path
|
||||
if os.path.isfile(name):
|
||||
return name
|
||||
|
||||
# Expand user paths
|
||||
expanded = os.path.expanduser(name)
|
||||
if os.path.isfile(expanded):
|
||||
return expanded
|
||||
|
||||
# Search in font directories
|
||||
for font_dir in FONT_DIRS:
|
||||
font_dir = os.path.expanduser(font_dir)
|
||||
if not os.path.isdir(font_dir):
|
||||
continue
|
||||
|
||||
# Direct match
|
||||
direct = os.path.join(font_dir, name)
|
||||
if os.path.isfile(direct):
|
||||
return direct
|
||||
|
||||
# Recursive search
|
||||
for root, dirs, files in os.walk(font_dir):
|
||||
for f in files:
|
||||
if f.lower() == name.lower():
|
||||
return os.path.join(root, f)
|
||||
# Also match without extension
|
||||
base = os.path.splitext(f)[0]
|
||||
if base.lower() == name.lower():
|
||||
return os.path.join(root, f)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_default_font(size: int = 24) -> ImageFont.FreeTypeFont:
|
||||
"""Get a default font at the given size."""
|
||||
for font_name in DEFAULT_FONTS:
|
||||
path = _find_font_file(font_name)
|
||||
if path:
|
||||
try:
|
||||
return ImageFont.truetype(path, size)
|
||||
except:
|
||||
continue
|
||||
|
||||
# Last resort: PIL default
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def prim_make_font(name_or_path: str, size: int = 24) -> ImageFont.FreeTypeFont:
|
||||
"""
|
||||
Load a font by name or path.
|
||||
|
||||
(make-font "Arial" 32) ; system font by name
|
||||
(make-font "/path/to/font.ttf" 24) ; font file path
|
||||
(make-font "DejaVuSans" 48) ; searches common locations
|
||||
|
||||
Returns a font object for use with text primitives.
|
||||
"""
|
||||
size = int(size)
|
||||
|
||||
# Check cache
|
||||
cache_key = (name_or_path, size)
|
||||
if cache_key in _font_cache:
|
||||
return _font_cache[cache_key]
|
||||
|
||||
# Find the font file
|
||||
path = _find_font_file(name_or_path)
|
||||
if not path:
|
||||
raise FileNotFoundError(f"Font not found: {name_or_path}")
|
||||
|
||||
# Load and cache
|
||||
font = ImageFont.truetype(path, size)
|
||||
_font_cache[cache_key] = font
|
||||
return font
|
||||
|
||||
|
||||
def prim_list_fonts() -> List[str]:
|
||||
"""
|
||||
List available system fonts.
|
||||
|
||||
(list-fonts) ; -> ("Arial.ttf" "DejaVuSans.ttf" ...)
|
||||
|
||||
Returns list of font filenames found in system directories.
|
||||
"""
|
||||
fonts = set()
|
||||
|
||||
for font_dir in FONT_DIRS:
|
||||
font_dir = os.path.expanduser(font_dir)
|
||||
if not os.path.isdir(font_dir):
|
||||
continue
|
||||
|
||||
for root, dirs, files in os.walk(font_dir):
|
||||
for f in files:
|
||||
if f.lower().endswith(('.ttf', '.otf', '.ttc')):
|
||||
fonts.add(f)
|
||||
|
||||
return sorted(fonts)
|
||||
|
||||
|
||||
def prim_font_size(font: ImageFont.FreeTypeFont) -> int:
|
||||
"""
|
||||
Get the size of a font.
|
||||
|
||||
(font-size my-font) ; -> 24
|
||||
"""
|
||||
return font.size
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Text Measurement
|
||||
# =============================================================================
|
||||
|
||||
def prim_text_size(text: str, font=None, font_size: int = 24) -> Tuple[int, int]:
|
||||
"""
|
||||
Measure text dimensions.
|
||||
|
||||
(text-size "Hello" my-font) ; -> (width height)
|
||||
(text-size "Hello" :font-size 32) ; -> (width height) with default font
|
||||
|
||||
For multi-line text, returns total bounding box.
|
||||
"""
|
||||
if font is None:
|
||||
font = _get_default_font(int(font_size))
|
||||
elif isinstance(font, (int, float)):
|
||||
font = _get_default_font(int(font))
|
||||
|
||||
# Create temporary image for measurement
|
||||
img = Image.new('RGB', (1, 1))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
bbox = draw.textbbox((0, 0), str(text), font=font)
|
||||
width = bbox[2] - bbox[0]
|
||||
height = bbox[3] - bbox[1]
|
||||
|
||||
return (width, height)
|
||||
|
||||
|
||||
def prim_text_metrics(font=None, font_size: int = 24) -> dict:
|
||||
"""
|
||||
Get font metrics.
|
||||
|
||||
(text-metrics my-font) ; -> {ascent: 20, descent: 5, height: 25}
|
||||
|
||||
Useful for precise text layout.
|
||||
"""
|
||||
if font is None:
|
||||
font = _get_default_font(int(font_size))
|
||||
elif isinstance(font, (int, float)):
|
||||
font = _get_default_font(int(font))
|
||||
|
||||
ascent, descent = font.getmetrics()
|
||||
return {
|
||||
'ascent': ascent,
|
||||
'descent': descent,
|
||||
'height': ascent + descent,
|
||||
'size': font.size,
|
||||
}
|
||||
|
||||
|
||||
def prim_fit_text_size(text: str, max_width: int, max_height: int,
|
||||
font_name: str = None, min_size: int = 8,
|
||||
max_size: int = 500) -> int:
|
||||
"""
|
||||
Calculate font size to fit text within bounds.
|
||||
|
||||
(fit-text-size "Hello World" 400 100) ; -> 48
|
||||
(fit-text-size "Title" 800 200 :font-name "Arial")
|
||||
|
||||
Returns the largest font size that fits within max_width x max_height.
|
||||
"""
|
||||
max_width = int(max_width)
|
||||
max_height = int(max_height)
|
||||
min_size = int(min_size)
|
||||
max_size = int(max_size)
|
||||
text = str(text)
|
||||
|
||||
# Binary search for optimal size
|
||||
best_size = min_size
|
||||
low, high = min_size, max_size
|
||||
|
||||
while low <= high:
|
||||
mid = (low + high) // 2
|
||||
|
||||
if font_name:
|
||||
try:
|
||||
font = prim_make_font(font_name, mid)
|
||||
except:
|
||||
font = _get_default_font(mid)
|
||||
else:
|
||||
font = _get_default_font(mid)
|
||||
|
||||
w, h = prim_text_size(text, font)
|
||||
|
||||
if w <= max_width and h <= max_height:
|
||||
best_size = mid
|
||||
low = mid + 1
|
||||
else:
|
||||
high = mid - 1
|
||||
|
||||
return best_size
|
||||
|
||||
|
||||
def prim_fit_font(text: str, max_width: int, max_height: int,
|
||||
font_name: str = None, min_size: int = 8,
|
||||
max_size: int = 500) -> ImageFont.FreeTypeFont:
|
||||
"""
|
||||
Create a font sized to fit text within bounds.
|
||||
|
||||
(fit-font "Hello World" 400 100) ; -> font object
|
||||
(fit-font "Title" 800 200 :font-name "Arial")
|
||||
|
||||
Returns a font object at the optimal size.
|
||||
"""
|
||||
size = prim_fit_text_size(text, max_width, max_height,
|
||||
font_name, min_size, max_size)
|
||||
|
||||
if font_name:
|
||||
try:
|
||||
_default_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", size)
|
||||
return prim_make_font(font_name, size)
|
||||
except:
|
||||
_default_font = ImageFont.load_default()
|
||||
return _default_font
|
||||
pass
|
||||
|
||||
return _get_default_font(size)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Text Drawing
|
||||
# =============================================================================
|
||||
|
||||
def prim_text(img: np.ndarray, text: str,
|
||||
x: int = None, y: int = None,
|
||||
width: int = None, height: int = None,
|
||||
font=None, font_size: int = 24, font_name: str = None,
|
||||
color=None, opacity: float = 1.0,
|
||||
align: str = "left", valign: str = "top",
|
||||
fit: bool = False,
|
||||
shadow: bool = False, shadow_color=None, shadow_offset: int = 2,
|
||||
outline: bool = False, outline_color=None, outline_width: int = 1,
|
||||
line_spacing: float = 1.2) -> np.ndarray:
|
||||
"""
|
||||
Draw text with alignment, opacity, and effects.
|
||||
|
||||
Basic usage:
|
||||
(text frame "Hello" :x 100 :y 50)
|
||||
|
||||
Centered in frame:
|
||||
(text frame "Title" :align "center" :valign "middle")
|
||||
|
||||
Fit to box:
|
||||
(text frame "Big Text" :x 50 :y 50 :width 400 :height 100 :fit true)
|
||||
|
||||
With fade (for animations):
|
||||
(text frame "Fading" :x 100 :y 100 :opacity 0.5)
|
||||
|
||||
With effects:
|
||||
(text frame "Shadow" :x 100 :y 100 :shadow true)
|
||||
(text frame "Outline" :x 100 :y 100 :outline true :outline-color (0 0 0))
|
||||
|
||||
Args:
|
||||
img: Input frame
|
||||
text: Text to draw
|
||||
x, y: Position (if not specified, uses alignment in full frame)
|
||||
width, height: Bounding box (for fit and alignment within box)
|
||||
font: Font object from make-font
|
||||
font_size: Size if no font specified
|
||||
font_name: Font name to load
|
||||
color: RGB tuple (default white)
|
||||
opacity: 0.0 (invisible) to 1.0 (opaque) for fading
|
||||
align: "left", "center", "right"
|
||||
valign: "top", "middle", "bottom"
|
||||
fit: If true, auto-size font to fit in box
|
||||
shadow: Draw drop shadow
|
||||
shadow_color: Shadow color (default black)
|
||||
shadow_offset: Shadow offset in pixels
|
||||
outline: Draw text outline
|
||||
outline_color: Outline color (default black)
|
||||
outline_width: Outline thickness
|
||||
line_spacing: Multiplier for line height (for multi-line)
|
||||
|
||||
Returns:
|
||||
Frame with text drawn
|
||||
"""
|
||||
h, w = img.shape[:2]
|
||||
text = str(text)
|
||||
|
||||
# Default colors
|
||||
if color is None:
|
||||
color = (255, 255, 255)
|
||||
else:
|
||||
color = tuple(int(c) for c in color)
|
||||
|
||||
if shadow_color is None:
|
||||
shadow_color = (0, 0, 0)
|
||||
else:
|
||||
shadow_color = tuple(int(c) for c in shadow_color)
|
||||
|
||||
if outline_color is None:
|
||||
outline_color = (0, 0, 0)
|
||||
else:
|
||||
outline_color = tuple(int(c) for c in outline_color)
|
||||
|
||||
# Determine bounding box
|
||||
if x is None:
|
||||
x = 0
|
||||
if width is None:
|
||||
width = w
|
||||
if y is None:
|
||||
y = 0
|
||||
if height is None:
|
||||
height = h
|
||||
|
||||
x, y = int(x), int(y)
|
||||
box_width = int(width) if width else w - x
|
||||
box_height = int(height) if height else h - y
|
||||
|
||||
# Get or create font
|
||||
if font is None:
|
||||
if fit:
|
||||
font = prim_fit_font(text, box_width, box_height, font_name)
|
||||
elif font_name:
|
||||
try:
|
||||
font = prim_make_font(font_name, int(font_size))
|
||||
except:
|
||||
font = _get_default_font(int(font_size))
|
||||
else:
|
||||
font = _get_default_font(int(font_size))
|
||||
|
||||
# Measure text
|
||||
text_w, text_h = prim_text_size(text, font)
|
||||
|
||||
# Calculate position based on alignment
|
||||
if align == "center":
|
||||
draw_x = x + (box_width - text_w) // 2
|
||||
elif align == "right":
|
||||
draw_x = x + box_width - text_w
|
||||
else: # left
|
||||
draw_x = x
|
||||
|
||||
if valign == "middle":
|
||||
draw_y = y + (box_height - text_h) // 2
|
||||
elif valign == "bottom":
|
||||
draw_y = y + box_height - text_h
|
||||
else: # top
|
||||
draw_y = y
|
||||
|
||||
# Create RGBA image for compositing with opacity
|
||||
pil_img = Image.fromarray(img).convert('RGBA')
|
||||
|
||||
# Create text layer with transparency
|
||||
text_layer = Image.new('RGBA', (w, h), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(text_layer)
|
||||
|
||||
# Draw shadow first (if enabled)
|
||||
if shadow:
|
||||
shadow_x = draw_x + shadow_offset
|
||||
shadow_y = draw_y + shadow_offset
|
||||
shadow_rgba = shadow_color + (int(255 * opacity * 0.5),)
|
||||
draw.text((shadow_x, shadow_y), text, fill=shadow_rgba, font=font)
|
||||
|
||||
# Draw outline (if enabled)
|
||||
if outline:
|
||||
outline_rgba = outline_color + (int(255 * opacity),)
|
||||
ow = int(outline_width)
|
||||
for dx in range(-ow, ow + 1):
|
||||
for dy in range(-ow, ow + 1):
|
||||
if dx != 0 or dy != 0:
|
||||
draw.text((draw_x + dx, draw_y + dy), text,
|
||||
fill=outline_rgba, font=font)
|
||||
|
||||
# Draw main text
|
||||
text_rgba = color + (int(255 * opacity),)
|
||||
draw.text((draw_x, draw_y), text, fill=text_rgba, font=font)
|
||||
|
||||
# Composite
|
||||
result = Image.alpha_composite(pil_img, text_layer)
|
||||
return np.array(result.convert('RGB'))
|
||||
|
||||
|
||||
def prim_text_box(img: np.ndarray, text: str,
|
||||
x: int, y: int, width: int, height: int,
|
||||
font=None, font_size: int = 24, font_name: str = None,
|
||||
color=None, opacity: float = 1.0,
|
||||
align: str = "center", valign: str = "middle",
|
||||
fit: bool = True,
|
||||
padding: int = 0,
|
||||
background=None, background_opacity: float = 0.5,
|
||||
**kwargs) -> np.ndarray:
|
||||
"""
|
||||
Draw text fitted within a box, optionally with background.
|
||||
|
||||
(text-box frame "Title" 50 50 400 100)
|
||||
(text-box frame "Subtitle" 50 160 400 50
|
||||
:background (0 0 0) :background-opacity 0.7)
|
||||
|
||||
Convenience wrapper around text() for common box-with-text pattern.
|
||||
"""
|
||||
x, y = int(x), int(y)
|
||||
width, height = int(width), int(height)
|
||||
padding = int(padding)
|
||||
|
||||
result = img.copy()
|
||||
|
||||
# Draw background if specified
|
||||
if background is not None:
|
||||
bg_color = tuple(int(c) for c in background)
|
||||
|
||||
# Create background with opacity
|
||||
pil_img = Image.fromarray(result).convert('RGBA')
|
||||
bg_layer = Image.new('RGBA', (pil_img.width, pil_img.height), (0, 0, 0, 0))
|
||||
bg_draw = ImageDraw.Draw(bg_layer)
|
||||
bg_rgba = bg_color + (int(255 * background_opacity),)
|
||||
bg_draw.rectangle([x, y, x + width, y + height], fill=bg_rgba)
|
||||
result = np.array(Image.alpha_composite(pil_img, bg_layer).convert('RGB'))
|
||||
|
||||
# Draw text within padded box
|
||||
return prim_text(result, text,
|
||||
x=x + padding, y=y + padding,
|
||||
width=width - 2 * padding, height=height - 2 * padding,
|
||||
font=font, font_size=font_size, font_name=font_name,
|
||||
color=color, opacity=opacity,
|
||||
align=align, valign=valign, fit=fit,
|
||||
**kwargs)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Legacy text functions (keep for compatibility)
|
||||
# =============================================================================
|
||||
|
||||
def prim_draw_char(img, char, x, y, font_size=16, color=None):
|
||||
"""Draw a single character at (x, y)."""
|
||||
if color is None:
|
||||
color = [255, 255, 255]
|
||||
|
||||
pil_img = Image.fromarray(img)
|
||||
draw = ImageDraw.Draw(pil_img)
|
||||
font = _get_default_font(font_size)
|
||||
draw.text((x, y), char, fill=tuple(color), font=font)
|
||||
return np.array(pil_img)
|
||||
"""Draw a single character at (x, y). Legacy function."""
|
||||
return prim_text(img, str(char), x=int(x), y=int(y),
|
||||
font_size=int(font_size), color=color)
|
||||
|
||||
|
||||
def prim_draw_text(img, text, x, y, font_size=16, color=None):
|
||||
"""Draw text string at (x, y)."""
|
||||
"""Draw text string at (x, y). Legacy function."""
|
||||
return prim_text(img, str(text), x=int(x), y=int(y),
|
||||
font_size=int(font_size), color=color)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Shape Drawing
|
||||
# =============================================================================
|
||||
|
||||
def prim_fill_rect(img, x, y, w, h, color=None, opacity: float = 1.0):
|
||||
"""
|
||||
Fill a rectangle with color.
|
||||
|
||||
(fill-rect frame 10 10 100 50 (255 0 0))
|
||||
(fill-rect frame 10 10 100 50 (255 0 0) :opacity 0.5)
|
||||
"""
|
||||
if color is None:
|
||||
color = [255, 255, 255]
|
||||
|
||||
pil_img = Image.fromarray(img)
|
||||
draw = ImageDraw.Draw(pil_img)
|
||||
font = _get_default_font(font_size)
|
||||
draw.text((x, y), text, fill=tuple(color), font=font)
|
||||
return np.array(pil_img)
|
||||
|
||||
|
||||
def prim_fill_rect(img, x, y, w, h, color=None):
|
||||
"""Fill a rectangle with color."""
|
||||
if color is None:
|
||||
color = [255, 255, 255]
|
||||
|
||||
result = img.copy()
|
||||
x, y, w, h = int(x), int(y), int(w), int(h)
|
||||
result[y:y+h, x:x+w] = color
|
||||
return result
|
||||
|
||||
if opacity >= 1.0:
|
||||
result = img.copy()
|
||||
result[y:y+h, x:x+w] = color
|
||||
return result
|
||||
|
||||
# With opacity, use alpha compositing
|
||||
pil_img = Image.fromarray(img).convert('RGBA')
|
||||
layer = Image.new('RGBA', (pil_img.width, pil_img.height), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(layer)
|
||||
fill_rgba = tuple(int(c) for c in color) + (int(255 * opacity),)
|
||||
draw.rectangle([x, y, x + w, y + h], fill=fill_rgba)
|
||||
result = Image.alpha_composite(pil_img, layer)
|
||||
return np.array(result.convert('RGB'))
|
||||
|
||||
|
||||
def prim_draw_rect(img, x, y, w, h, color=None, thickness=1):
|
||||
def prim_draw_rect(img, x, y, w, h, color=None, thickness=1, opacity: float = 1.0):
|
||||
"""Draw rectangle outline."""
|
||||
if color is None:
|
||||
color = [255, 255, 255]
|
||||
|
||||
result = img.copy()
|
||||
cv2.rectangle(result, (int(x), int(y)), (int(x+w), int(y+h)),
|
||||
tuple(color), thickness)
|
||||
return result
|
||||
if opacity >= 1.0:
|
||||
result = img.copy()
|
||||
cv2.rectangle(result, (int(x), int(y)), (int(x+w), int(y+h)),
|
||||
tuple(int(c) for c in color), int(thickness))
|
||||
return result
|
||||
|
||||
# With opacity
|
||||
pil_img = Image.fromarray(img).convert('RGBA')
|
||||
layer = Image.new('RGBA', (pil_img.width, pil_img.height), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(layer)
|
||||
outline_rgba = tuple(int(c) for c in color) + (int(255 * opacity),)
|
||||
draw.rectangle([int(x), int(y), int(x+w), int(y+h)],
|
||||
outline=outline_rgba, width=int(thickness))
|
||||
result = Image.alpha_composite(pil_img, layer)
|
||||
return np.array(result.convert('RGB'))
|
||||
|
||||
|
||||
def prim_draw_line(img, x1, y1, x2, y2, color=None, thickness=1):
|
||||
def prim_draw_line(img, x1, y1, x2, y2, color=None, thickness=1, opacity: float = 1.0):
|
||||
"""Draw a line from (x1, y1) to (x2, y2)."""
|
||||
if color is None:
|
||||
color = [255, 255, 255]
|
||||
|
||||
result = img.copy()
|
||||
cv2.line(result, (int(x1), int(y1)), (int(x2), int(y2)),
|
||||
tuple(color), thickness)
|
||||
return result
|
||||
if opacity >= 1.0:
|
||||
result = img.copy()
|
||||
cv2.line(result, (int(x1), int(y1)), (int(x2), int(y2)),
|
||||
tuple(int(c) for c in color), int(thickness))
|
||||
return result
|
||||
|
||||
# With opacity
|
||||
pil_img = Image.fromarray(img).convert('RGBA')
|
||||
layer = Image.new('RGBA', (pil_img.width, pil_img.height), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(layer)
|
||||
line_rgba = tuple(int(c) for c in color) + (int(255 * opacity),)
|
||||
draw.line([(int(x1), int(y1)), (int(x2), int(y2))],
|
||||
fill=line_rgba, width=int(thickness))
|
||||
result = Image.alpha_composite(pil_img, layer)
|
||||
return np.array(result.convert('RGB'))
|
||||
|
||||
|
||||
def prim_draw_circle(img, cx, cy, radius, color=None, thickness=1, fill=False):
|
||||
def prim_draw_circle(img, cx, cy, radius, color=None, thickness=1,
|
||||
fill=False, opacity: float = 1.0):
|
||||
"""Draw a circle."""
|
||||
if color is None:
|
||||
color = [255, 255, 255]
|
||||
|
||||
result = img.copy()
|
||||
t = -1 if fill else thickness
|
||||
cv2.circle(result, (int(cx), int(cy)), int(radius), tuple(color), t)
|
||||
return result
|
||||
if opacity >= 1.0:
|
||||
result = img.copy()
|
||||
t = -1 if fill else int(thickness)
|
||||
cv2.circle(result, (int(cx), int(cy)), int(radius),
|
||||
tuple(int(c) for c in color), t)
|
||||
return result
|
||||
|
||||
# With opacity
|
||||
pil_img = Image.fromarray(img).convert('RGBA')
|
||||
layer = Image.new('RGBA', (pil_img.width, pil_img.height), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(layer)
|
||||
cx, cy, r = int(cx), int(cy), int(radius)
|
||||
rgba = tuple(int(c) for c in color) + (int(255 * opacity),)
|
||||
|
||||
if fill:
|
||||
draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill=rgba)
|
||||
else:
|
||||
draw.ellipse([cx - r, cy - r, cx + r, cy + r],
|
||||
outline=rgba, width=int(thickness))
|
||||
|
||||
result = Image.alpha_composite(pil_img, layer)
|
||||
return np.array(result.convert('RGB'))
|
||||
|
||||
|
||||
def prim_draw_ellipse(img, cx, cy, rx, ry, angle=0, color=None, thickness=1, fill=False):
|
||||
def prim_draw_ellipse(img, cx, cy, rx, ry, angle=0, color=None,
|
||||
thickness=1, fill=False, opacity: float = 1.0):
|
||||
"""Draw an ellipse."""
|
||||
if color is None:
|
||||
color = [255, 255, 255]
|
||||
|
||||
result = img.copy()
|
||||
t = -1 if fill else thickness
|
||||
cv2.ellipse(result, (int(cx), int(cy)), (int(rx), int(ry)),
|
||||
angle, 0, 360, tuple(color), t)
|
||||
return result
|
||||
if opacity >= 1.0:
|
||||
result = img.copy()
|
||||
t = -1 if fill else int(thickness)
|
||||
cv2.ellipse(result, (int(cx), int(cy)), (int(rx), int(ry)),
|
||||
float(angle), 0, 360, tuple(int(c) for c in color), t)
|
||||
return result
|
||||
|
||||
# With opacity (note: PIL doesn't support rotated ellipses easily)
|
||||
# Fall back to cv2 on a separate layer
|
||||
layer = np.zeros((img.shape[0], img.shape[1], 4), dtype=np.uint8)
|
||||
t = -1 if fill else int(thickness)
|
||||
rgba = tuple(int(c) for c in color) + (int(255 * opacity),)
|
||||
cv2.ellipse(layer, (int(cx), int(cy)), (int(rx), int(ry)),
|
||||
float(angle), 0, 360, rgba, t)
|
||||
|
||||
pil_img = Image.fromarray(img).convert('RGBA')
|
||||
pil_layer = Image.fromarray(layer)
|
||||
result = Image.alpha_composite(pil_img, pil_layer)
|
||||
return np.array(result.convert('RGB'))
|
||||
|
||||
|
||||
def prim_draw_polygon(img, points, color=None, thickness=1, fill=False):
|
||||
def prim_draw_polygon(img, points, color=None, thickness=1,
|
||||
fill=False, opacity: float = 1.0):
|
||||
"""Draw a polygon from list of [x, y] points."""
|
||||
if color is None:
|
||||
color = [255, 255, 255]
|
||||
|
||||
result = img.copy()
|
||||
pts = np.array(points, dtype=np.int32).reshape((-1, 1, 2))
|
||||
if opacity >= 1.0:
|
||||
result = img.copy()
|
||||
pts = np.array(points, dtype=np.int32).reshape((-1, 1, 2))
|
||||
if fill:
|
||||
cv2.fillPoly(result, [pts], tuple(int(c) for c in color))
|
||||
else:
|
||||
cv2.polylines(result, [pts], True,
|
||||
tuple(int(c) for c in color), int(thickness))
|
||||
return result
|
||||
|
||||
# With opacity
|
||||
pil_img = Image.fromarray(img).convert('RGBA')
|
||||
layer = Image.new('RGBA', (pil_img.width, pil_img.height), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(layer)
|
||||
|
||||
pts_flat = [(int(p[0]), int(p[1])) for p in points]
|
||||
rgba = tuple(int(c) for c in color) + (int(255 * opacity),)
|
||||
|
||||
if fill:
|
||||
cv2.fillPoly(result, [pts], tuple(color))
|
||||
draw.polygon(pts_flat, fill=rgba)
|
||||
else:
|
||||
cv2.polylines(result, [pts], True, tuple(color), thickness)
|
||||
draw.polygon(pts_flat, outline=rgba, width=int(thickness))
|
||||
|
||||
return result
|
||||
result = Image.alpha_composite(pil_img, layer)
|
||||
return np.array(result.convert('RGB'))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PRIMITIVES Export
|
||||
# =============================================================================
|
||||
|
||||
PRIMITIVES = {
|
||||
# Text
|
||||
# Font management
|
||||
'make-font': prim_make_font,
|
||||
'list-fonts': prim_list_fonts,
|
||||
'font-size': prim_font_size,
|
||||
|
||||
# Text measurement
|
||||
'text-size': prim_text_size,
|
||||
'text-metrics': prim_text_metrics,
|
||||
'fit-text-size': prim_fit_text_size,
|
||||
'fit-font': prim_fit_font,
|
||||
|
||||
# Text drawing
|
||||
'text': prim_text,
|
||||
'text-box': prim_text_box,
|
||||
|
||||
# Legacy text (compatibility)
|
||||
'draw-char': prim_draw_char,
|
||||
'draw-text': prim_draw_text,
|
||||
|
||||
|
||||
@@ -8,12 +8,18 @@ GPU Acceleration:
|
||||
- Set STREAMING_GPU_PERSIST=1 to output CuPy arrays (frames stay on GPU)
|
||||
- Hardware video decoding (NVDEC) is used when available
|
||||
- Dramatically improves performance on GPU nodes
|
||||
|
||||
Async Prefetching:
|
||||
- Set STREAMING_PREFETCH=1 to enable background frame prefetching
|
||||
- Decodes upcoming frames while current frame is being processed
|
||||
"""
|
||||
|
||||
import os
|
||||
import numpy as np
|
||||
import subprocess
|
||||
import json
|
||||
import threading
|
||||
from collections import deque
|
||||
from pathlib import Path
|
||||
|
||||
# Try to import CuPy for GPU acceleration
|
||||
@@ -28,6 +34,10 @@ except ImportError:
|
||||
# Disabled by default until all primitives support GPU frames
|
||||
GPU_PERSIST = os.environ.get("STREAMING_GPU_PERSIST", "0") == "1" and CUPY_AVAILABLE
|
||||
|
||||
# Async prefetch mode - decode frames in background thread
|
||||
PREFETCH_ENABLED = os.environ.get("STREAMING_PREFETCH", "1") == "1"
|
||||
PREFETCH_BUFFER_SIZE = int(os.environ.get("STREAMING_PREFETCH_SIZE", "10"))
|
||||
|
||||
# Check for hardware decode support (cached)
|
||||
_HWDEC_AVAILABLE = None
|
||||
|
||||
@@ -283,6 +293,122 @@ class VideoSource:
|
||||
self._proc = None
|
||||
|
||||
|
||||
class PrefetchingVideoSource:
|
||||
"""
|
||||
Video source with background prefetching for improved performance.
|
||||
|
||||
Wraps VideoSource and adds a background thread that pre-decodes
|
||||
upcoming frames while the main thread processes the current frame.
|
||||
"""
|
||||
|
||||
def __init__(self, path: str, fps: float = 30, buffer_size: int = None):
|
||||
self._source = VideoSource(path, fps)
|
||||
self._buffer_size = buffer_size or PREFETCH_BUFFER_SIZE
|
||||
self._buffer = {} # time -> frame
|
||||
self._buffer_lock = threading.Lock()
|
||||
self._prefetch_time = 0.0
|
||||
self._frame_time = 1.0 / fps
|
||||
self._stop_event = threading.Event()
|
||||
self._request_event = threading.Event()
|
||||
self._target_time = 0.0
|
||||
|
||||
# Start prefetch thread
|
||||
self._thread = threading.Thread(target=self._prefetch_loop, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
import sys
|
||||
print(f"PrefetchingVideoSource: {path} buffer_size={self._buffer_size}", file=sys.stderr)
|
||||
|
||||
def _prefetch_loop(self):
|
||||
"""Background thread that pre-reads frames."""
|
||||
while not self._stop_event.is_set():
|
||||
# Wait for work or timeout
|
||||
self._request_event.wait(timeout=0.01)
|
||||
self._request_event.clear()
|
||||
|
||||
if self._stop_event.is_set():
|
||||
break
|
||||
|
||||
# Prefetch frames ahead of target time
|
||||
target = self._target_time
|
||||
with self._buffer_lock:
|
||||
# Clean old frames (more than 1 second behind)
|
||||
old_times = [t for t in self._buffer.keys() if t < target - 1.0]
|
||||
for t in old_times:
|
||||
del self._buffer[t]
|
||||
|
||||
# Count how many frames we have buffered ahead
|
||||
buffered_ahead = sum(1 for t in self._buffer.keys() if t >= target)
|
||||
|
||||
# Prefetch if buffer not full
|
||||
if buffered_ahead < self._buffer_size:
|
||||
# Find next time to prefetch
|
||||
prefetch_t = target
|
||||
with self._buffer_lock:
|
||||
existing_times = set(self._buffer.keys())
|
||||
for _ in range(self._buffer_size):
|
||||
if prefetch_t not in existing_times:
|
||||
break
|
||||
prefetch_t += self._frame_time
|
||||
|
||||
# Read the frame (this is the slow part)
|
||||
try:
|
||||
frame = self._source.read_at(prefetch_t)
|
||||
with self._buffer_lock:
|
||||
self._buffer[prefetch_t] = frame
|
||||
except Exception as e:
|
||||
import sys
|
||||
print(f"Prefetch error at t={prefetch_t}: {e}", file=sys.stderr)
|
||||
|
||||
def read_at(self, t: float) -> np.ndarray:
|
||||
"""Read frame at specific time, using prefetch buffer if available."""
|
||||
self._target_time = t
|
||||
self._request_event.set() # Wake up prefetch thread
|
||||
|
||||
# Round to frame time for buffer lookup
|
||||
t_key = round(t / self._frame_time) * self._frame_time
|
||||
|
||||
# Check buffer first
|
||||
with self._buffer_lock:
|
||||
if t_key in self._buffer:
|
||||
return self._buffer[t_key]
|
||||
# Also check for close matches (within half frame time)
|
||||
for buf_t, frame in self._buffer.items():
|
||||
if abs(buf_t - t) < self._frame_time * 0.5:
|
||||
return frame
|
||||
|
||||
# Not in buffer - read directly (blocking)
|
||||
frame = self._source.read_at(t)
|
||||
|
||||
# Store in buffer
|
||||
with self._buffer_lock:
|
||||
self._buffer[t_key] = frame
|
||||
|
||||
return frame
|
||||
|
||||
def read(self) -> np.ndarray:
|
||||
"""Read frame (uses last cached or t=0)."""
|
||||
return self.read_at(0)
|
||||
|
||||
def skip(self):
|
||||
"""No-op for seek-based reading."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self._source.size
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._source.path
|
||||
|
||||
def close(self):
|
||||
self._stop_event.set()
|
||||
self._request_event.set() # Wake up thread to exit
|
||||
self._thread.join(timeout=1.0)
|
||||
self._source.close()
|
||||
|
||||
|
||||
class AudioAnalyzer:
|
||||
"""Audio analyzer for energy and beat detection."""
|
||||
|
||||
@@ -394,7 +520,12 @@ class AudioAnalyzer:
|
||||
# === Primitives ===
|
||||
|
||||
def prim_make_video_source(path: str, fps: float = 30):
|
||||
"""Create a video source from a file path."""
|
||||
"""Create a video source from a file path.
|
||||
|
||||
Uses PrefetchingVideoSource if STREAMING_PREFETCH=1 (default).
|
||||
"""
|
||||
if PREFETCH_ENABLED:
|
||||
return PrefetchingVideoSource(path, fps)
|
||||
return VideoSource(path, fps)
|
||||
|
||||
|
||||
|
||||
1382
sexp_effects/primitive_libs/xector.py
Normal file
1382
sexp_effects/primitive_libs/xector.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -797,31 +797,63 @@ def prim_tan(x: float) -> float:
|
||||
return math.tan(x)
|
||||
|
||||
|
||||
def prim_atan2(y: float, x: float) -> float:
|
||||
def prim_atan2(y, x):
|
||||
if hasattr(y, '_data'): # Xector
|
||||
from sexp_effects.primitive_libs.xector import Xector
|
||||
return Xector(np.arctan2(y._data, x._data if hasattr(x, '_data') else x), y._shape)
|
||||
return math.atan2(y, x)
|
||||
|
||||
|
||||
def prim_sqrt(x: float) -> float:
|
||||
def prim_sqrt(x):
|
||||
if hasattr(x, '_data'): # Xector
|
||||
from sexp_effects.primitive_libs.xector import Xector
|
||||
return Xector(np.sqrt(np.maximum(0, x._data)), x._shape)
|
||||
if isinstance(x, np.ndarray):
|
||||
return np.sqrt(np.maximum(0, x))
|
||||
return math.sqrt(max(0, x))
|
||||
|
||||
|
||||
def prim_pow(x: float, y: float) -> float:
|
||||
def prim_pow(x, y):
|
||||
if hasattr(x, '_data'): # Xector
|
||||
from sexp_effects.primitive_libs.xector import Xector
|
||||
y_data = y._data if hasattr(y, '_data') else y
|
||||
return Xector(np.power(x._data, y_data), x._shape)
|
||||
return math.pow(x, y)
|
||||
|
||||
|
||||
def prim_abs(x: float) -> float:
|
||||
def prim_abs(x):
|
||||
if hasattr(x, '_data'): # Xector
|
||||
from sexp_effects.primitive_libs.xector import Xector
|
||||
return Xector(np.abs(x._data), x._shape)
|
||||
if isinstance(x, np.ndarray):
|
||||
return np.abs(x)
|
||||
return abs(x)
|
||||
|
||||
|
||||
def prim_floor(x: float) -> int:
|
||||
def prim_floor(x):
|
||||
if hasattr(x, '_data'): # Xector
|
||||
from sexp_effects.primitive_libs.xector import Xector
|
||||
return Xector(np.floor(x._data), x._shape)
|
||||
if isinstance(x, np.ndarray):
|
||||
return np.floor(x)
|
||||
return int(math.floor(x))
|
||||
|
||||
|
||||
def prim_ceil(x: float) -> int:
|
||||
def prim_ceil(x):
|
||||
if hasattr(x, '_data'): # Xector
|
||||
from sexp_effects.primitive_libs.xector import Xector
|
||||
return Xector(np.ceil(x._data), x._shape)
|
||||
if isinstance(x, np.ndarray):
|
||||
return np.ceil(x)
|
||||
return int(math.ceil(x))
|
||||
|
||||
|
||||
def prim_round(x: float) -> int:
|
||||
def prim_round(x):
|
||||
if hasattr(x, '_data'): # Xector
|
||||
from sexp_effects.primitive_libs.xector import Xector
|
||||
return Xector(np.round(x._data), x._shape)
|
||||
if isinstance(x, np.ndarray):
|
||||
return np.round(x)
|
||||
return int(round(x))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user