Created
June 1, 2020 23:39
-
-
Save justinpage/b47e7706a4c8a0d9973f402594f252e9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Surface computes an SVG that renders a 3-D surface within the browser. | |
| package main | |
| import ( | |
| "errors" | |
| "fmt" | |
| "log" | |
| "math" | |
| "net/http" | |
| "strconv" | |
| "sync" | |
| ) | |
| const ( | |
| width, height = 600, 320 // canvas size in pixels | |
| cells = 100 // number of grid cells | |
| xyrange = 30.0 // axis ranges (-xyrange..+xyrange) | |
| angle = math.Pi / 6 // angle of x, y axes (=30°) | |
| ) | |
| type options struct { | |
| width int | |
| height int | |
| color string | |
| xyscale float64 | |
| zscale float64 | |
| } | |
| type point struct { | |
| x, y float64 | |
| } | |
| type square struct { | |
| pointA, pointB, pointC, pointD *point | |
| } | |
| var sin30, cos30 = math.Sin(angle), math.Cos(angle) // sin(30°), cos(30°) | |
| func main() { | |
| handler := func(w http.ResponseWriter, r *http.Request) { | |
| if err := r.ParseForm(); err != nil { | |
| log.Print(err) | |
| } | |
| option := &options{ | |
| width, height, "white", | |
| (width / 2 / xyrange), (height * 0.4), | |
| } | |
| if w := r.Form.Get("width"); w != "" { | |
| option.width, _ = strconv.Atoi(w) | |
| option.xyscale = (float64(option.width) / 2 / xyrange) | |
| } | |
| if h := r.Form.Get("height"); h != "" { | |
| option.height, _ = strconv.Atoi(h) | |
| option.zscale = (float64(option.height) * 0.4) | |
| } | |
| if c := r.Form.Get("color"); c != "" { | |
| option.color = c | |
| } | |
| surface(w, option) | |
| } | |
| http.HandleFunc("/", handler) | |
| log.Fatal(http.ListenAndServe(":8080", nil)) | |
| } | |
| func surface(out http.ResponseWriter, opt *options) { | |
| out.Header().Set("Content-Type", "image/svg+xml") | |
| fmt.Fprintf(out, "<svg xmlns='http://www.w3.org/2000/svg' "+ | |
| "style='stroke: grey; fill: %s; stroke-width: 0.7' "+ | |
| "width='%d' height='%d'>", opt.color, opt.width, opt.height) | |
| var wg sync.WaitGroup | |
| ch := make(chan *square) | |
| for i := 0; i < cells; i++ { | |
| for j := 0; j < cells; j++ { | |
| wg.Add(1) | |
| go func(i, j int) { | |
| defer wg.Done() | |
| pointA, err := corner(i+1, j, opt) | |
| if err != nil { | |
| return | |
| } | |
| pointB, err := corner(i, j, opt) | |
| if err != nil { | |
| return | |
| } | |
| pointC, err := corner(i, j+1, opt) | |
| if err != nil { | |
| return | |
| } | |
| pointD, err := corner(i+1, j+1, opt) | |
| if err != nil { | |
| return | |
| } | |
| ch <- &square{pointA, pointB, pointC, pointD} | |
| }(i, j) | |
| } | |
| } | |
| go func() { | |
| wg.Wait() | |
| close(ch) | |
| fmt.Fprintf(out, "</svg>") | |
| }() | |
| for sq := range ch { | |
| fmt.Fprintf(out, | |
| "<polygon points='%g,%g %g,%g %g,%g %g,%g'/>\n", | |
| sq.pointA.x, sq.pointA.y, sq.pointB.x, sq.pointB.y, | |
| sq.pointC.x, sq.pointC.y, sq.pointD.x, sq.pointD.y, | |
| ) | |
| } | |
| } | |
| func corner(i, j int, opt *options) (*point, error) { | |
| // Find point (x,y) at corner of cell (i,j) | |
| x := xyrange * (float64(i)/cells - 0.5) | |
| y := xyrange * (float64(j)/cells - 0.5) | |
| z := f(x, y) | |
| if math.IsNaN(z) { | |
| return &point{}, errors.New("not a number") | |
| } | |
| // Project (x,y,z) isometrically onto 2-D SVG canvas (sx,sy) | |
| sx := float64(opt.width/2) + (x-y)*cos30*opt.xyscale | |
| sy := float64(opt.height/2) + (x+y)*sin30*opt.xyscale - z*opt.zscale | |
| if z > 0 { | |
| return &point{sx, sy}, nil | |
| } | |
| return &point{sx, sy}, nil | |
| } | |
| func f(x, y float64) float64 { | |
| r := math.Hypot(x, y) // distance from (0,0) | |
| return math.Sin(r) / r | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment