File: ./config.go
   1 package main
   2 
   3 import (
   4     "errors"
   5     "fmt"
   6     "image/color"
   7     "math"
   8     "os"
   9     "strconv"
  10     "strings"
  11 )
  12 
  13 const (
  14     // all output formats as constants, to prevent typos
  15     pngOutput             = `png`
  16     pngFastOutput         = `fast-png`
  17     pngSmallestOutput     = `smallest-png`
  18     pngUncompressedOutput = `uncompressed-png`
  19     bmpOutput             = `bmp`
  20     jpegOutput            = `jpeg`
  21 
  22     // all colorscales as constants, to prevent typos
  23     magmaScale   = `magma`
  24     parulaScale  = `parula`
  25     viridisScale = `viridis`
  26     grayScale    = `gray`
  27     binaryScale  = `binary`
  28     signScale    = `sign`
  29 )
  30 
  31 // fmtAliases normalizes values for the output-format option
  32 var fmtAliases = map[string]string{
  33     `b`:      bmpOutput,
  34     `bitmap`: bmpOutput,
  35     `bmp`:    bmpOutput,
  36     `j`:      jpegOutput,
  37     `jpeg`:   jpegOutput,
  38     `jpg`:    jpegOutput,
  39     `p`:      pngOutput,
  40     `ping`:   pngOutput,
  41     `png`:    pngOutput,
  42 
  43     `f`:                pngFastOutput,
  44     `fast`:             pngFastOutput,
  45     `fast-png`:         pngFastOutput,
  46     `fp`:               pngFastOutput,
  47     `fpng`:             pngFastOutput,
  48     `s`:                pngSmallestOutput,
  49     `small`:            pngSmallestOutput,
  50     `smallest-png`:     pngSmallestOutput,
  51     `small-png`:        pngSmallestOutput,
  52     `sp`:               pngSmallestOutput,
  53     `spng`:             pngSmallestOutput,
  54     `u`:                pngUncompressedOutput,
  55     `unc`:              pngUncompressedOutput,
  56     `uncompressed-png`: pngUncompressedOutput,
  57 }
  58 
  59 // paletteAliases normalizes values for the colorscale/palette option
  60 var paletteAliases = map[string]string{
  61     `b`:      binaryScale,
  62     `bin`:    binaryScale,
  63     `binary`: binaryScale,
  64 
  65     `g`:    grayScale,
  66     `gr`:   grayScale,
  67     `gray`: grayScale,
  68 
  69     `m`:     magmaScale,
  70     `mag`:   magmaScale,
  71     `magma`: magmaScale,
  72 
  73     `s`:    signScale,
  74     `sgn`:  signScale,
  75     `sign`: signScale,
  76 
  77     `py`:      viridisScale,
  78     `python`:  viridisScale,
  79     `numpy`:   viridisScale,
  80     `v`:       viridisScale,
  81     `vir`:     viridisScale,
  82     `viridis`: viridisScale,
  83 
  84     `matlab`: parulaScale,
  85     `p`:      parulaScale,
  86     `par`:    parulaScale,
  87     `parula`: parulaScale,
  88 }
  89 
  90 // outputSize is the value type for the resAliases lookup table
  91 type outputSize struct {
  92     Width  int
  93     Height int
  94 }
  95 
  96 // resAliases normalizes values for option -res
  97 var resAliases = map[string]outputSize{
  98     `sq`:      {2160, 2160},
  99     `sqr`:     {2160, 2160},
 100     `square`:  {2160, 2160},
 101     `squared`: {2160, 2160},
 102 
 103     `4k`:    {3840, 2160},
 104     `2160`:  {3840, 2160},
 105     `2160p`: {3840, 2160},
 106     `3840`:  {3840, 2160},
 107 
 108     `2.5k`:  {2560, 1440},
 109     `1440`:  {2560, 1440},
 110     `1440p`: {2560, 1440},
 111     `2560`:  {2560, 1440},
 112 
 113     `2k`:     {1920, 1080},
 114     `hd`:     {1920, 1080},
 115     `fhd`:    {1920, 1080},
 116     `fullhd`: {1920, 1080},
 117     `1080`:   {1920, 1080},
 118     `1080p`:  {1920, 1080},
 119     `1920`:   {1920, 1080},
 120 
 121     `720`:  {1280, 720},
 122     `720p`: {1280, 720},
 123 
 124     `480p`: {640, 480},
 125     `480`:  {640, 480},
 126 
 127     `2ks`:   {1080, 1080},
 128     `4ks`:   {2160, 2160},
 129     `2160s`: {2160, 2160},
 130     `1440s`: {1440, 1440},
 131     `1080s`: {1080, 1080},
 132     `720s`:  {720, 720},
 133     `480s`:  {480, 480},
 134 }
 135 
 136 // config has all parsed cmd-line arguments
 137 type config struct {
 138     Width  int
 139     Height int
 140 
 141     XMin float64
 142     XMax float64
 143     YMin float64
 144     YMax float64
 145 
 146     Formula string
 147     Output  string
 148 
 149     Palette func(float64) color.RGBA
 150     Bad     color.RGBA
 151 
 152     Integers bool
 153 }
 154 
 155 // parseFlags is the constructor for type config
 156 func parseFlags(usage string) (config, error) {
 157     cfg := config{
 158         Width:  3840,
 159         Height: 2160,
 160 
 161         XMin: 0,
 162         XMax: 1,
 163         YMin: 0,
 164         YMax: 1,
 165 
 166         Output: pngOutput,
 167     }
 168 
 169     cfg.Output = pngOutput
 170     pal := palettes[parulaScale]
 171     cfg.Palette = pal.Func
 172     cfg.Bad = pal.Bad
 173 
 174     args := os.Args[1:]
 175     if len(args) == 0 {
 176         fmt.Fprint(os.Stderr, usage)
 177         os.Exit(0)
 178 
 179     }
 180 
 181     for _, s := range args {
 182         switch s {
 183         case `help`, `-h`, `--h`, `-help`, `--help`:
 184             fmt.Fprint(os.Stderr, usage)
 185             os.Exit(0)
 186         }
 187 
 188         err := cfg.handleArg(s)
 189         if err != nil {
 190             return cfg, err
 191         }
 192     }
 193 
 194     if cfg.Integers {
 195         cfg.XMin = math.Ceil(float64(cfg.XMin))
 196         cfg.XMax = math.Floor(float64(cfg.XMax))
 197         cfg.YMin = math.Ceil(float64(cfg.YMin))
 198         cfg.YMax = math.Floor(float64(cfg.YMax))
 199     }
 200 
 201     if strings.TrimSpace(cfg.Formula) == `` {
 202         return cfg, errors.New(`no main formula given`)
 203     }
 204     return cfg, nil
 205 }
 206 
 207 // handleArg parses/uses the cmd-line argument given, except for the help
 208 // option and its aliases, which can only be detected separately
 209 func (c *config) handleArg(s string) error {
 210     switch s {
 211     case `int`, `ints`, `integers`:
 212         c.Integers = true
 213         return nil
 214     }
 215 
 216     lcDotless := strings.TrimPrefix(strings.ToLower(s), `.`)
 217     if alias, ok := fmtAliases[lcDotless]; ok {
 218         c.Output = alias
 219         return nil
 220     }
 221 
 222     if w, h, ok := parseResolution(s); ok {
 223         c.Width = w
 224         c.Height = h
 225         return nil
 226     }
 227 
 228     if colors, ok := paletteAliases[s]; ok {
 229         pal := palettes[colors]
 230         c.Palette = pal.Func
 231         c.Bad = pal.Bad
 232         return nil
 233     }
 234 
 235     varname, min, max, err := parseDomain(s)
 236     if err != nil {
 237         return err
 238     }
 239 
 240     switch varname {
 241     case ``:
 242         // no variable name means it's the main formula
 243         if c.Formula != `` {
 244             const fs = `%q: can't use more than 1 main formula`
 245             return fmt.Errorf(fs, s)
 246         }
 247         c.Formula = s
 248         return nil
 249 
 250     case `x`:
 251         c.XMin = min
 252         c.XMax = max
 253         return nil
 254 
 255     case `y`:
 256         c.YMin = min
 257         c.YMax = max
 258         return nil
 259 
 260     case `xy`:
 261         c.XMin = min
 262         c.XMax = max
 263         c.YMin = min
 264         c.YMax = max
 265         return nil
 266 
 267     default:
 268         const fs = "domain variable %q isn't any of `x`, `y`, or `xy`"
 269         return fmt.Errorf(fs, varname)
 270     }
 271 }
 272 
 273 // parseResolution tries to get a width/height resolution out of the
 274 // cmd-line argument given to it
 275 func parseResolution(s string) (width int, height int, ok bool) {
 276     if res, ok := resAliases[s]; ok {
 277         return res.Width, res.Height, true
 278     }
 279 
 280     i := strings.IndexByte(s, 'x')
 281     if i < 0 {
 282         return 0, 0, false
 283     }
 284 
 285     w, werr := strconv.ParseInt(s[:i], 10, 64)
 286     h, herr := strconv.ParseInt(s[i+1:], 10, 64)
 287     if werr == nil && herr == nil && w > 0 && h > 0 {
 288         return int(w), int(h), true
 289     }
 290     return 0, 0, false
 291 }
 292 
 293 func (c config) IntegerSize() (w, h int) {
 294     w = int(math.Abs(c.XMax - c.XMin + 1))
 295     h = int(math.Abs(c.YMax - c.YMin + 1))
 296     return w, h
 297 }
 298 
 299 // parseDomain tries to parse domain/variable-range formulas of the form(s)
 300 //
 301 // - x:=a..b
 302 // - y:=a..b
 303 // - xy:=a..b
 304 //
 305 // where a and b represent valid floating-point numbers; when an empty is
 306 // returned, it means the strings given wasn't recognized as a variable's
 307 // domain, suggesting it may be another option, or the main formula instead
 308 func parseDomain(s string) (string, float64, float64, error) {
 309     i := strings.Index(s, `:=`)
 310     if i < 0 {
 311         return ``, 0, 0, nil
 312     }
 313 
 314     v := strings.TrimSpace(s[:i])
 315     rng := strings.TrimSpace(s[i+2:])
 316     min, max, err := parseSpan(rng)
 317     return v, min, max, err
 318 }
 319 
 320 // parseSpan tries to parse a pair of numbers with `..` between them
 321 func parseSpan(s string) (float64, float64, error) {
 322     pair := strings.Split(s, `..`)
 323     if len(pair) != 2 {
 324         const fs = "missing `..` in domain-span %s"
 325         return 0, 1, fmt.Errorf(fs, s)
 326     }
 327 
 328     a, err := strconv.ParseFloat(pair[0], 64)
 329     if err != nil {
 330         const fs = `can't parse %q in domain-span %s`
 331         return 0, 1, fmt.Errorf(fs, pair[0], s)
 332     }
 333     b, err := strconv.ParseFloat(pair[1], 64)
 334     if err != nil {
 335         const fs = `can't parse %q in domain-span %s`
 336         return 0, 1, fmt.Errorf(fs, pair[1], s)
 337     }
 338     return a, b, nil
 339 }

     File: ./config_test.go
   1 package main
   2 
   3 import "testing"
   4 
   5 func TestTables(t *testing.T) {
   6     for _, kind := range fmtAliases {
   7         // check all canonical format names are in the table
   8         if _, ok := fmtAliases[kind]; !ok {
   9             const fs = `format %q itself isn't in the format-table`
  10             t.Fatalf(fs, kind)
  11             return
  12         }
  13 
  14         if _, ok := encoders[kind]; !ok {
  15             const fs = `no encoder for %q`
  16             t.Fatalf(fs, kind)
  17             return
  18         }
  19     }
  20 
  21     for _, kind := range paletteAliases {
  22         // check all canonical colorscale names are in the table
  23         if _, ok := paletteAliases[kind]; !ok {
  24             const fs = `format %q itself isn't in the format-table`
  25             t.Fatalf(fs, kind)
  26             return
  27         }
  28 
  29         if _, ok := palettes[kind]; !ok {
  30             const fs = `no palette for %q`
  31             t.Fatalf(fs, kind)
  32             return
  33         }
  34     }
  35 }

     File: ./examples.txt
   1 # ripples
   2 fh xy:=-3..3 'exp(-0.5 * sin(2 * hypot(x - 2, y + 1))) + exp(-0.5 * sin(10 * hypot(x + 2, y - 3.4)))' | si
   3 
   4 # floor lights
   5 fh x:=-5..5 y:=1..5 'x.sin.abs / y**1.4' | si
   6 
   7 # beta gradient
   8 fh x:=-5..5 y:=1..5 'lbeta(x + 5.1, y + 5.1)' | si
   9 
  10 # hot bars / horizontal bars
  11 fh xy:=-5..5 'x.abs + sqrt(abs(sin(2*y)))' | si
  12 fh xy:=-5..5 'x.abs + sqrt(abs(sin(2*y)))' | si
  13 
  14 # domain hole
  15 fh xy:=-5..5 'log1p(sin(x + y) + (x - y)**2 - 1.5*x + 2.5*y + 1)' | si
  16 
  17 # crazy grids
  18 fh xy:=-10..10 'sin(x.sin+y.cos) + cos(sin(x*y)+cos(y*y))' | si
  19 
  20 # panda / smiling ghost
  21 fh xy:=-5..5 'log1p(((x - 1)**2 + y*y - 4)*((x + 1)**2 + y*y - 4)*(x*x + (y - sqrt(3))**2 - 4) - 5)' | si
  22 
  23 # lcm 200
  24 fh xy:=0.01..199.99 'lcm(x.ceil, y.ceil)' | si
  25 
  26 # light tiles
  27 fh 'gauss(2*(sin(50.0*x)*cos(50.0*9/16*y) + 1.0)/2.0)' | si
  28 
  29 # shaky results... at least for me
  30 fh 'cos(160*tau*x) + sin(90*tau*y)' | si
  31 
  32 # 90-degree square tiles
  33 fh 'sign(cos(160*tau*x) + sin(90*tau*y))' | si

     File: ./info.txt
   1 fh [options...] [x/y ranges...] formula
   2 
   3 
   4 Function Heatmapper emits a picture showing a heatmap view of the function
   5 f(x, y) implied by the math expression given. Plenty of math functions and
   6 constants are available, all their names being lowercase; the syntax is
   7 almost identical to Python/JavaScript's math notation, and has no keywords.
   8 
   9 For convenience, you can treat any 1-input func as a fake-property of its
  10 only input; you can also pretend all functions are fake-methods, where the
  11 1st input comes before the dot preceding the func name, followed by all the
  12 other args to it. All values and functions are global: without namespaces
  13 of any kind.
  14 
  15 Ranges for variables `x` and `y` are 0 to 1 by default, but you can change
  16 them via the special syntax shown on some of the examples below. Using the
  17 keyword `int`, `ints`, or `integers` enables integer-mode, where both `x`
  18 and `y` values are only sampled as integers: in that case, formula results
  19 will be used to fill whole tiles, instead of single pixels.
  20 
  21 By default, output is PNG-encoded using a good tradeoff between encoding
  22 speed and final payload size. Output resolutions can be as shown below, or
  23 consist of the width, followed by `x`, followed by the height wanted, such
  24 as `1024x768`, for example.
  25 
  26 Options have no flags/prefixes, and are accepted in any order.
  27 
  28 
  29 Options
  30 
  31     resolution                          resolution
  32 
  33     4k           3840x2160              4ks           2160x2160
  34     hd           1920x1080              hds           1080x1080
  35 
  36     2160p        3840x2160              2160s         2160x2160
  37     1440p        2560x1440              1440s         1440x1440
  38     1080p        1920x1080              1080s         1080x1080
  39     720p         1280x720               720s          720x720
  40 
  41 
  42     output     aliases                  colorscale    aliases
  43 
  44     png                                 magma         mag, m
  45     bmp        bitmap                   parula        par, p
  46     jpg        jpeg                     viridis       vir, v
  47 
  48 
  49 Concrete Examples
  50 
  51 
  52 fh 'x/(x+y)' > corner-fan-1.png
  53 
  54 fh 'y/(x+y)' > corner-fan-2.png
  55 
  56 fh 4k x:=-5..5 y:=1..5 'x.sin.abs / y**1.4' > floor-lights.png
  57 
  58 fh vir x:=-5..5 y:=1..5 'lbeta(x + 5.1, y + 5.1)' > beta-gradient.png
  59 
  60 fh mag 4k xy:=0.01..199.99 'lcm(x.ceil, y.ceil)' > lcm-200.png
  61 
  62 fh par 4k xy:=-5..5 'x.abs + sqrt(abs(sin(2*y)))' > bars.png
  63 
  64 fh x:=-1.5..0.5 y:=-1..1 'mandel(16/9*x, y)' > mandelbrot.png
  65 
  66 fh x:=-1.5..0.5 y:=-1..1 'absmandel(16/9*x, y)' > wobbly-mandelbrot.png
  67 
  68 fh 4k 'sign(cos(160*tau*x) + sin(90*tau*y))' > 90-deg-square-tiles.png
  69 
  70 fh xy:=-10..10 'sin(x.sin+y.cos) + cos(sin(x*y)+cos(y*y))' > crazy-grids.png
  71 
  72 fh 'gauss(sin(50*x) * cos(50*9/16*y) + 1)' > light-tiles.png
  73 
  74 fh xy:=-2..3 'sgn(log((x*x-1)*(x-2-y)/(x*x+2+2*y)))' > abstract-shapes.png
  75 
  76 fh xy:=-10..10 square 'sinc(0.55 * hypot(x, y))' > central-ripple.png

     File: ./logo.ico   <BINARY>

     File: ./logo.png   <BINARY>

     File: ./main.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "fmt"
   6     "image"
   7     "os"
   8 
   9     _ "embed"
  10 )
  11 
  12 //go:embed info.txt
  13 var usage string
  14 
  15 func main() {
  16     cfg, err := parseFlags(usage)
  17     if err != nil {
  18         fmt.Fprintln(os.Stderr, err.Error())
  19         os.Exit(1)
  20     }
  21 
  22     if _, ok := encoders[cfg.Output]; !ok {
  23         const fs = "unsupported output format %s\n"
  24         fmt.Fprintf(os.Stderr, fs, cfg.Output)
  25         os.Exit(1)
  26     }
  27 
  28     addDetermFuncs()
  29 
  30     if err := run(cfg); err != nil {
  31         fmt.Fprintln(os.Stderr, err.Error())
  32         os.Exit(1)
  33     }
  34 }
  35 
  36 func run(cfg config) error {
  37     // f, err := os.Create(`fh.prof`)
  38     // if err != nil {
  39     //  return err
  40     // }
  41     // defer f.Close()
  42 
  43     // pprof.StartCPUProfile(f)
  44     // defer pprof.StopCPUProfile()
  45 
  46     encode, ok := encoders[cfg.Output]
  47     if !ok {
  48         const fs = `unsupported output format %q`
  49         return fmt.Errorf(fs, cfg.Output)
  50     }
  51 
  52     if cfg.Integers {
  53         w, h := cfg.IntegerSize()
  54         cfg.Width = w
  55         cfg.Height = h
  56     }
  57 
  58     // allow runner to use up to 32 cores
  59     r, err := newRunner(cfg, 32)
  60     if err != nil {
  61         return err
  62     }
  63 
  64     res, err := r.Run(cfg)
  65     if err != nil {
  66         return err
  67     }
  68 
  69     img := image.NewRGBA(image.Rectangle{
  70         Min: image.Point{X: 0, Y: 0},
  71         Max: image.Point{X: cfg.Width, Y: cfg.Height},
  72     })
  73 
  74     w := bufio.NewWriterSize(os.Stdout, 64*1024)
  75     defer w.Flush()
  76 
  77     // give back a blank picture if results aren't usable
  78     if !res.isValid() {
  79         return encode(w, img, cfg)
  80     }
  81 
  82     // handle integers-only coordinate-inputs
  83     if cfg.Integers {
  84         width, height := cfg.IntegerSize()
  85         fillExpandedImage(img, res, cfg, width, height)
  86         return encode(w, img, cfg)
  87     }
  88 
  89     // handle domain-sampled images
  90     fillImage(img, res, cfg)
  91     return encode(w, img, cfg)
  92 }

     File: ./output.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "encoding/binary"
   6     "image"
   7     "image/color"
   8     "image/jpeg"
   9     "image/png"
  10     "math"
  11 
  12     "../../pkg/colorplus"
  13 )
  14 
  15 var (
  16     // red is the invalid color for all palettes with dark/black colors
  17     red = color.RGBA{R: 255, G: 0, B: 0, A: 255}
  18 
  19     // black is the invalid color for the more colorful palettes
  20     black = color.RGBA{R: 0, G: 0, B: 0, A: 255}
  21 )
  22 
  23 // paletteSettings describes the full behavior of a palette
  24 type paletteSettings struct {
  25     Func func(float64) color.RGBA
  26     Bad  color.RGBA
  27 }
  28 
  29 // palettes completely describes the behavior of all supported palettes
  30 var palettes = map[string]paletteSettings{
  31     grayScale:    {gray, red},
  32     magmaScale:   {colorplus.Magmify, red},
  33     viridisScale: {colorplus.Viridize, black},
  34     parulaScale:  {colorplus.Parulate, black},
  35     binaryScale:  {colorBinary, black},
  36     signScale:    {colorSign, black},
  37 }
  38 
  39 // gray implements the grayscale coloring option, and is meant to be paired
  40 // with a red color for invalid inputs, such as NaNs
  41 func gray(x float64) color.RGBA {
  42     // restrict input to range 0..1
  43     if x < 0 {
  44         x = 0
  45     } else if x > 1 {
  46         x = 1
  47     }
  48 
  49     v := uint8(math.Round(255 * x))
  50     return color.RGBA{R: v, G: v, B: v, A: 255}
  51 }
  52 
  53 // colorBinary assigns 2 colors, thresholding the number given on 0.5
  54 func colorBinary(x float64) color.RGBA {
  55     if x < 0.5 {
  56         return color.RGBA{R: 234, G: 85, B: 58, A: 255}
  57     }
  58     return color.RGBA{R: 0, G: 95, B: 0, A: 255}
  59 }
  60 
  61 // colorSign assigns 3 colors, depending on the sign of the number given
  62 func colorSign(x float64) color.RGBA {
  63     if x > 0 {
  64         return color.RGBA{R: 0, G: 95, B: 0, A: 255}
  65     }
  66     if x < 0 {
  67         return color.RGBA{R: 234, G: 85, B: 58, A: 255}
  68     }
  69     return color.RGBA{R: 0, G: 135, B: 215, A: 255}
  70 }
  71 
  72 // encoders translates output-format settings into the right func to call
  73 var encoders = map[string]func(*bufio.Writer, *image.RGBA, config) error{
  74     pngOutput:  encodePNG,
  75     bmpOutput:  encodeBMP,
  76     jpegOutput: encodeJPEG,
  77 
  78     pngFastOutput:         encodeFastPNG,
  79     pngSmallestOutput:     encodeSmallestPNG,
  80     pngUncompressedOutput: encodeUncompressedPNG,
  81 }
  82 
  83 // fillImage fills/renders an image using previously calculated values
  84 func fillImage(img *image.RGBA, res result, cfg config) {
  85     k := 0
  86     f := cfg.Palette
  87 
  88     for i := 0; i < cfg.Height; i++ {
  89         for j := 0; j < cfg.Width; j++ {
  90             v := res.Values[k]
  91 
  92             var c color.RGBA
  93             if math.IsNaN(v) || math.IsInf(v, 0) {
  94                 c = cfg.Bad
  95             } else {
  96                 c = f(colorplus.Wrap(v, res.Min, res.Max))
  97             }
  98 
  99             img.SetRGBA(j, i, c)
 100             k++
 101         }
 102     }
 103 }
 104 
 105 // fillExpandedImage is like func fillImage, but rendering stretches what
 106 // would otherwise be single pixels into rectangles, representing regions
 107 // where the integer-parts of x/y inputs stay the same
 108 func fillExpandedImage(img *image.RGBA, res result, cfg config, w, h int) {
 109     width := img.Rect.Max.X
 110     xmax := float64(img.Rect.Max.X)
 111     ymax := float64(img.Rect.Max.Y)
 112 
 113     f := cfg.Palette
 114     dx := float64(w) / xmax
 115     dy := float64(h) / ymax
 116 
 117     for i := 0; i < cfg.Height; i++ {
 118         y := int(dy * float64(i))
 119         for j := 0; j < cfg.Width; j++ {
 120             x := int(dx * float64(j))
 121             k := y*width + x
 122             v := res.Values[k]
 123 
 124             var c color.RGBA
 125             if math.IsNaN(v) || math.IsInf(v, 0) {
 126                 c = cfg.Bad
 127             } else {
 128                 c = f(colorplus.Wrap(v, res.Min, res.Max))
 129             }
 130             img.SetRGBA(j, i, c)
 131         }
 132     }
 133 }
 134 
 135 // encodePNG seems a good default both for its main format (PNG), as well as
 136 // its reasonable default tradeoff between speed and output size, compared
 137 // to the PNG-encoding alternatives available
 138 func encodePNG(w *bufio.Writer, img *image.RGBA, cfg config) error {
 139     var enc png.Encoder
 140     return enc.Encode(w, img)
 141 }
 142 
 143 // encodeFastPNG may not always be much faster than the default PNG encoder
 144 func encodeFastPNG(w *bufio.Writer, img *image.RGBA, cfg config) error {
 145     var enc png.Encoder
 146     enc.CompressionLevel = png.BestSpeed
 147     return enc.Encode(w, img)
 148 }
 149 
 150 // encodeSmallestPNG is substantially slower than the other PNG encoders
 151 func encodeSmallestPNG(w *bufio.Writer, img *image.RGBA, cfg config) error {
 152     var enc png.Encoder
 153     enc.CompressionLevel = png.BestCompression
 154     return enc.Encode(w, img)
 155 }
 156 
 157 // encodeUncompressedPNG is mostly to compare it to BMP output: it turns out
 158 // BMP is slightly smaller than this
 159 func encodeUncompressedPNG(w *bufio.Writer, img *image.RGBA, cfg config) error {
 160     var enc png.Encoder
 161     enc.CompressionLevel = png.NoCompression
 162     return enc.Encode(w, img)
 163 }
 164 
 165 // encodeJPEG encodes result at max JPEG setting: this usually results in
 166 // highly-detailed results for substantially-fewer bytes, compared to PNG
 167 // output
 168 func encodeJPEG(w *bufio.Writer, img *image.RGBA, cfg config) error {
 169     opt := jpeg.Options{Quality: 100}
 170     return jpeg.Encode(w, img, &opt)
 171 }
 172 
 173 // https://en.wikipedia.org/wiki/BMP_file_format
 174 
 175 // encodeBMP encodes as BMP/bitmap, a simple uncompressed format, which has
 176 // been widely supported for many decades
 177 func encodeBMP(w *bufio.Writer, img *image.RGBA, cfg config) error {
 178     const (
 179         dibsize = 40           // the DIB is the 2nd header
 180         hdrsize = 14 + dibsize // total size of all headers
 181     )
 182     imgsize := 3 * cfg.Width * cfg.Height
 183 
 184     w.WriteString(`BM`)
 185     binary.Write(w, binary.LittleEndian, uint32(hdrsize+imgsize))
 186     binary.Write(w, binary.LittleEndian, uint16(0))
 187     binary.Write(w, binary.LittleEndian, uint16(0))
 188     binary.Write(w, binary.LittleEndian, uint32(hdrsize))
 189     binary.Write(w, binary.LittleEndian, uint32(dibsize))
 190     binary.Write(w, binary.LittleEndian, int32(cfg.Width))
 191     binary.Write(w, binary.LittleEndian, int32(cfg.Height))
 192 
 193     // 1 color plane
 194     binary.Write(w, binary.LittleEndian, uint16(1))
 195     // 24 bits per pixel
 196     binary.Write(w, binary.LittleEndian, uint16(24))
 197     // no compression
 198     binary.Write(w, binary.LittleEndian, uint32(0))
 199     // number of bytes for the pixels
 200     binary.Write(w, binary.LittleEndian, uint32(imgsize))
 201     // horizontal & vertical pixels/m
 202     binary.Write(w, binary.LittleEndian, int32(0))
 203     binary.Write(w, binary.LittleEndian, int32(0))
 204     // 2**n palette colors
 205     binary.Write(w, binary.LittleEndian, uint32(0))
 206     // all colors are important
 207     binary.Write(w, binary.LittleEndian, uint32(0))
 208 
 209     stride := img.Stride
 210     // rows/lines are apparently stored bottom-to-top
 211     for y := cfg.Height - 1; y >= 0; y-- {
 212         start := y * stride
 213         buf := img.Pix[start : start+stride]
 214 
 215         for len(buf) >= 3 {
 216             // color-channel order seems to be BGR, instead of RGB
 217             w.WriteByte(buf[2])
 218             w.WriteByte(buf[1])
 219             err := w.WriteByte(buf[0])
 220             if err != nil {
 221                 // use errors to quit immediately: chances are
 222                 // the error is the result of a closed-pipe
 223                 return nil
 224             }
 225 
 226             // also skip the alpha channel
 227             buf = buf[4:]
 228         }
 229     }
 230     return nil
 231 }

     File: ./scripts.go
   1 package main
   2 
   3 import (
   4     "math"
   5     "math/cmplx"
   6     "math/rand"
   7     "runtime"
   8     "sync"
   9     "time"
  10 
  11     "../../pkg/fmscripts"
  12     "../../pkg/mathplus"
  13 )
  14 
  15 // result has all results, including a summary of the range of values, so the
  16 // image renderer can normalize values accordingly
  17 type result struct {
  18     // Values has all results, which can be normalized into 0..1 using
  19     // fields Min and Max.
  20     Values []float64
  21 
  22     // Min is the lowest value in Values.
  23     Min float64
  24 
  25     // Max is the highest value in Values.
  26     Max float64
  27 }
  28 
  29 // isValid checks if the result should be a non-blank picture
  30 func (r result) isValid() bool {
  31     return r.Min <= r.Max && !math.IsInf(r.Min, 0) && !math.IsInf(r.Max, 0)
  32 }
  33 
  34 // runner has various twin script-runners, and automatically multicore-splits
  35 // the load among tasks along alternating groups of lines, in a striping
  36 // manner
  37 type runner struct {
  38     numTasks int
  39 
  40     // values can have its items updated concurrently, since each vertical
  41     // image line is changed by a single task.
  42     values []float64
  43 
  44     programs []fmscripts.Program
  45 }
  46 
  47 // newRunner is the constructor for type runner
  48 func newRunner(cfg config, maxtasks int) (runner, error) {
  49     numtasks := runtime.NumCPU()
  50     if maxtasks > 0 && numtasks > maxtasks {
  51         numtasks = maxtasks
  52     }
  53     progs := make([]fmscripts.Program, 0, numtasks)
  54 
  55     for i := 0; i < numtasks; i++ {
  56         // compiling the same formula multiple times seems wasteful, but
  57         // each compilation is very quick; this repetition is necessary
  58         // to isolate each task's input variables and pseudo-random state,
  59         // anyway
  60         p, err := compile(cfg.Formula, cfg, time.Now().UnixNano())
  61         if err != nil {
  62             return runner{}, err
  63         }
  64         progs = append(progs, p)
  65     }
  66 
  67     return runner{
  68         numTasks: numtasks,
  69         values:   make([]float64, cfg.Width*cfg.Height),
  70         programs: progs,
  71     }, nil
  72 }
  73 
  74 // Run is the entry-point func which handles everything from start to finish.
  75 func (r *runner) Run(cfg config) (res result, err error) {
  76     var wg sync.WaitGroup
  77     wg.Add(r.numTasks)
  78 
  79     // fully allocate min/max slices, as appending is concurrently unsafe
  80     lmin := make([]float64, r.numTasks)
  81     lmax := make([]float64, r.numTasks)
  82 
  83     // run parallel tasks: updating the shared value-slice works, as long
  84     // as each process sticks to its own index and output lines
  85     for i := 0; i < r.numTasks; i++ {
  86         go func(i int) {
  87             defer wg.Done()
  88             min, max := r.runSlice(i, cfg)
  89             lmin[i] = min
  90             lmax[i] = max
  91         }(i)
  92     }
  93     wg.Wait()
  94 
  95     // get overall min/max
  96     min := math.Inf(+1)
  97     max := math.Inf(-1)
  98     for i := range lmin {
  99         min = math.Min(min, lmin[i])
 100         max = math.Max(max, lmax[i])
 101     }
 102     return result{Values: r.values, Min: min, Max: max}, nil
 103 }
 104 
 105 // runSlice handles the task a specific core is supposed to handle: call
 106 // run instead of this func directly
 107 func (r *runner) runSlice(task int, cfg config) (min, max float64) {
 108     p := r.programs[task]
 109     x, _ := p.Get(`x`)
 110     y, _ := p.Get(`y`)
 111     zs := r.values
 112 
 113     w := cfg.Width
 114     h := cfg.Height
 115     n := r.numTasks
 116     xmin := math.Min(cfg.XMin, cfg.XMax)
 117     ymax := math.Max(cfg.YMax, cfg.YMin)
 118     wf := float64(w)
 119 
 120     zmin := math.Inf(+1)
 121     zmax := math.Inf(-1)
 122     dx := math.Abs(cfg.XMax-cfg.XMin) / float64(cfg.Width-1)
 123     dy := math.Abs(cfg.YMax-cfg.YMin) / float64(cfg.Height-1)
 124 
 125     for i := task; i < h; i += n {
 126         k := w * i
 127         *y = ymax - dy*float64(i)
 128 
 129         for j := 0.0; j < wf; j++ {
 130             *x = dx*j + xmin
 131             z := p.Run()
 132             zs[k] = z
 133             k++
 134 
 135             if !math.IsNaN(z) {
 136                 zmin = math.Min(zmin, z)
 137                 zmax = math.Max(zmax, z)
 138             }
 139         }
 140     }
 141     return zmin, zmax
 142 }
 143 
 144 // compile extends the built-in fast-math script functionality by adding
 145 // pseudo-random generators initialized with the seed number given
 146 func compile(src string, cfg config, seed int64) (fmscripts.Program, error) {
 147     r := rand.New(rand.NewSource(seed))
 148     rand01 := func() float64 {
 149         return fmscripts.Random(r)
 150     }
 151     rint := func(min, max float64) float64 {
 152         return fmscripts.RandomInt(r, min, max)
 153     }
 154     runif := func(min, max float64) float64 {
 155         return fmscripts.RandomUnif(r, min, max)
 156     }
 157     rexp := func(scale float64) float64 {
 158         return fmscripts.RandomExp(r, scale)
 159     }
 160     rnorm := func(mu, sigma float64) float64 {
 161         return fmscripts.RandomNorm(r, mu, sigma)
 162     }
 163     rgamma := func(scale float64) float64 {
 164         return fmscripts.RandomGamma(r, scale)
 165     }
 166     rbeta := func(a, b float64) float64 {
 167         return fmscripts.RandomBeta(r, a, b)
 168     }
 169 
 170     var c fmscripts.Compiler
 171     return c.Compile(src, map[string]any{
 172         `x`: 0.0,
 173         `y`: 0.0,
 174 
 175         `w`:        float64(cfg.Width),
 176         `h`:        float64(cfg.Height),
 177         `ar`:       float64(cfg.Width) / float64(cfg.Height),
 178         `aspratio`: float64(cfg.Width) / float64(cfg.Height),
 179 
 180         `rand`:   rand01,
 181         `rbeta`:  rbeta,
 182         `rexp`:   rexp,
 183         `rgamma`: rgamma,
 184         `rint`:   rint,
 185         `rnorm`:  rnorm,
 186         `runif`:  runif,
 187 
 188         `randbeta`:  rbeta,
 189         `randexp`:   rexp,
 190         `randexpo`:  rexp,
 191         `randgam`:   rgamma,
 192         `randgamma`: rgamma,
 193         `randint`:   rint,
 194         `randnorm`:  rnorm,
 195         `randunif`:  runif,
 196 
 197         `random`: rand01,
 198         `rbet`:   rbeta,
 199         `rgam`:   rgamma,
 200         `rnd`:    rand01,
 201     })
 202 }
 203 
 204 // addDetermFuncs does what it says, ensuring these funcs are optimizable when
 205 // they're given all-constant expressions as inputs
 206 func addDetermFuncs() {
 207     fmscripts.DefineDetFuncs(map[string]any{
 208         `ascale`:       mathplus.AnchoredScale,
 209         `awrap`:        mathplus.AnchoredWrap,
 210         `choose`:       comb,
 211         `clamp`:        mathplus.Clamp,
 212         `comb`:         comb,
 213         `dbinom`:       dbinom,
 214         `dnorm`:        mathplus.NormalDensity,
 215         `epa`:          mathplus.Epanechnikov,
 216         `epanechnikov`: mathplus.Epanechnikov,
 217         `etamag`:       etamag,
 218         `etamagcap`:    etamagcap,
 219         `fract`:        mathplus.Fract,
 220         `gauss`:        mathplus.Gauss,
 221         `gcd`:          gcd,
 222         `horner`:       mathplus.Polyval,
 223         `ieta`:         etaimag,
 224         `isprime`:      isPrime,
 225         `lcm`:          lcm,
 226         `logistic`:     mathplus.Logistic,
 227         `mageta`:       etamag,
 228         `magetacap`:    etamagcap,
 229         `magzeta`:      zetamag,
 230         `magzetacap`:   zetamagcap,
 231         `mix`:          mathplus.Mix,
 232         `perm`:         perm,
 233         `pbinom`:       pbinom,
 234         `pnorm`:        mathplus.CumulativeNormalDensity,
 235         `polyval`:      mathplus.Polyval,
 236         `reta`:         etare,
 237         `scale`:        mathplus.Scale,
 238         `sign`:         mathplus.Sign,
 239         `sinc`:         mathplus.Sinc,
 240         `smoothstep`:   mathplus.SmoothStep,
 241         `step`:         mathplus.Step,
 242         `tricube`:      mathplus.Tricube,
 243         `unwrap`:       mathplus.Unwrap,
 244         `wrap`:         mathplus.Wrap,
 245         `zetamag`:      zetamag,
 246         `zetamagcap`:   zetamagcap,
 247 
 248         `absmandel`:     absmandel,
 249         `absmandelcap`:  absmandelcap,
 250         `itermandel`:    itermandel,
 251         `itermandelcap`: itermandelcap,
 252         `mandel`:        itermandel,
 253     })
 254 }
 255 
 256 // absmandel returns the abs value of the complex number used in the mandelbrot
 257 // recurrence relation; recurrence is automatically truncated to a default
 258 // threshold and/or max number of loops
 259 func absmandel(x, y float64) float64 {
 260     return absmandelcap(x, y, 50)
 261 }
 262 
 263 // absmandelcap is like func absmandel, except the cap/threshold is an explicit
 264 // parameter
 265 func absmandelcap(x, y, threshold float64) float64 {
 266     z := 0 + 0i
 267     c := complex(x, y)
 268     const max = 1000
 269     // using the threshold's square to avoid using sqrt
 270     ts := threshold * threshold
 271 
 272     for n := 0.0; n < max; n++ {
 273         sqmag := real(z)*real(z) + imag(z)*imag(z)
 274         if sqmag > ts {
 275             return math.Sqrt(sqmag)
 276         }
 277         z = z*z + c
 278     }
 279     return cmplx.Abs(z)
 280 }
 281 
 282 // itermandel returns the number of iterations used in the mandelbrot
 283 // recurrence relation; recurrence is automatically truncated to a default
 284 // threshold and/or max number of loops
 285 func itermandel(x, y float64) float64 {
 286     return itermandelcap(x, y, 50)
 287 }
 288 
 289 // itermandelcap returns the number of mandelbrot recurrence iterations like
 290 // func itermandel, except the cap/threshold is an explicit parameter
 291 func itermandelcap(x, y, threshold float64) float64 {
 292     z := 0 + 0i
 293     c := complex(x, y)
 294     const max = 1000
 295     // using the threshold's square to avoid using sqrt
 296     ts := threshold * threshold
 297 
 298     for n := 0.0; n < max; n++ {
 299         sqmag := real(z)*real(z) + imag(z)*imag(z)
 300         if sqmag > ts {
 301             return n
 302         }
 303         z = z*z + c
 304     }
 305     return max
 306 }
 307 
 308 func comb(x, y float64) float64 {
 309     return float64(mathplus.Choose(int(x), int(y)))
 310 }
 311 
 312 func perm(x, y float64) float64 {
 313     return float64(mathplus.Perm(int(x), int(y)))
 314 }
 315 
 316 func gcd(x, y float64) float64 {
 317     return float64(mathplus.GCD(int64(x), int64(y)))
 318 }
 319 
 320 func lcm(x, y float64) float64 {
 321     return float64(mathplus.LCM(int64(x), int64(y)))
 322 }
 323 
 324 func dbinom(x, n, p float64) float64 {
 325     return mathplus.BinomialMass(int(x), int(n), p)
 326 }
 327 
 328 func pbinom(x, n, p float64) float64 {
 329     return mathplus.CumulativeBinomialDensity(int(x), int(n), p)
 330 }
 331 
 332 func isPrime(x float64) float64 {
 333     if mathplus.IsPrime(int64(x)) {
 334         return 1
 335     }
 336     return 0
 337 }
 338 
 339 const (
 340     // etaTrunc is when the summation for the eta funcs stops by default
 341     etaTrunc = 50
 342 
 343     // zetaTrunc is when the summation for the zeta funcs stops by default
 344     zetaTrunc = 50
 345 )
 346 
 347 // etamag call func etamagcap with a default truncation
 348 func etamag(x, y float64) float64 {
 349     return cmplx.Abs(eta(complex(x, y)))
 350 }
 351 
 352 // etamagcap is the real-valued magnitude of the truncated approx. of func eta
 353 func etamagcap(x, y float64, max float64) float64 {
 354     return cmplx.Abs(etacap(complex(x, y), int(max)))
 355 }
 356 
 357 // etare is the real part of the truncated approx. of func eta
 358 func etare(x, y float64) float64 { return real(eta(complex(x, y))) }
 359 
 360 // etaimag is the imaginary part of the truncated approx. of func eta
 361 func etaimag(x, y float64) float64 { return imag(eta(complex(x, y))) }
 362 
 363 // eta approximates the dirichlet eta function by truncation
 364 func eta(x complex128) complex128 { return etacap(x, etaTrunc) }
 365 
 366 // etacap accepts a cap/max iteration value for the eta func truncation
 367 func etacap(x complex128, max int) complex128 {
 368     y := 0 + 0i
 369     v := 1 + 0i
 370     sign := 1 + 0i
 371 
 372     for n := 1; n <= max; n++ {
 373         y += sign * v
 374         sign *= -1
 375         v /= x
 376     }
 377     return y
 378 }
 379 
 380 // zetamag call func zetamagcap with a default truncation
 381 func zetamag(x, y float64) float64 {
 382     return cmplx.Abs(zetacap(complex(x, y), zetaTrunc))
 383 }
 384 
 385 // etamagcap is the real-valued magnitude of the truncated approx. of func eta
 386 func zetamagcap(x, y float64, max float64) float64 {
 387     return cmplx.Abs(etacap(complex(x, y), int(max)))
 388 }
 389 
 390 // zetacap accepts a cap/max iteration value for the zeta func truncation
 391 func zetacap(x complex128, max int) complex128 {
 392     y := 0 + 0i
 393     v := 1 + 0i
 394 
 395     for n := 1; n <= max; n++ {
 396         y += v
 397         v /= x
 398     }
 399     return y
 400 }

     File: ./winapp.rc
   1 // https://docs.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource
   2 // windres -o winapp_amd64.syso winapp.rc
   3 
   4 IDI_ICON1 ICON "logo.ico"

     File: ./winapp_amd64.syso   <BINARY>