├── wasm ├── wasm_exec.js ├── go_worker.js ├── main.go └── index.html ├── source.jpg ├── go.mod ├── server └── server.go ├── go.sum ├── LICENSE.md ├── examples ├── grayscale │ └── main.go ├── resizing │ └── main.go └── circular │ └── main.go ├── cmd └── main.go ├── README.md └── sketch └── sketch.go /wasm/wasm_exec.js: -------------------------------------------------------------------------------- 1 | // Provide yours here 2 | -------------------------------------------------------------------------------- /source.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preslavrachev/generative-art-in-go/HEAD/source.jpg -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/preslavrachev/generative-art-in-go 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/fogleman/gg v1.3.0 7 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect 8 | golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect 9 | ) 10 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | func main() { 11 | port := flag.String("p", "8100", "port to serve on") 12 | directory := flag.String("d", ".", "the directory of static file to host") 13 | flag.Parse() 14 | 15 | http.Handle("/", http.FileServer(http.Dir(*directory))) 16 | 17 | log.Printf("Serving %s on HTTP port: %s\n", *directory, *port) 18 | log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", *port), nil)) 19 | } 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= 2 | github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 3 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 4 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 5 | golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= 6 | golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 7 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Preslav Rachev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/grayscale/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/color" 7 | _ "image/jpeg" 8 | "image/png" 9 | "log" 10 | "os" 11 | ) 12 | 13 | type GrayscaleFilter struct { 14 | image.Image 15 | } 16 | 17 | func (f *GrayscaleFilter) At(x, y int) color.Color { 18 | r, g, b, a := f.Image.At(x, y).RGBA() 19 | grey := uint16(float64(r)*0.21 + float64(g)*0.72 + float64(b)*0.07) 20 | 21 | return color.RGBA64{ 22 | R: grey, 23 | G: grey, 24 | B: grey, 25 | A: uint16(a), 26 | } 27 | } 28 | 29 | func main() { 30 | img, err := loadImage("source.jpg") 31 | if err != nil { 32 | log.Panic(err) 33 | } 34 | 35 | saveOutput(&GrayscaleFilter{img}, "out.png") 36 | } 37 | 38 | func loadImage(filePath string) (image.Image, error) { 39 | file, err := os.Open(filePath) 40 | if err != nil { 41 | return nil, fmt.Errorf("source image could not be loaded: %w", err) 42 | } 43 | defer file.Close() 44 | img, _, err := image.Decode(file) 45 | if err != nil { 46 | return nil, fmt.Errorf("source image format could not be decoded: %w", err) 47 | } 48 | 49 | return img, nil 50 | } 51 | 52 | func saveOutput(img image.Image, filePath string) error { 53 | f, err := os.Create(filePath) 54 | if err != nil { 55 | return err 56 | } 57 | defer f.Close() 58 | 59 | // Encode to `PNG` with `DefaultCompression` level 60 | // then save to file 61 | err = png.Encode(f, img) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /wasm/go_worker.js: -------------------------------------------------------------------------------- 1 | // hijack the console.log function to capture stdout 2 | let oldLog = console.log; 3 | console.log = (line) => { 4 | postMessage({ 5 | log: line, 6 | }); 7 | }; 8 | 9 | self.importScripts("wasm_exec.js"); 10 | //console.log(self.Go); 11 | 12 | // initialize the Go WASM glue 13 | const go = new self.Go(); 14 | 15 | let mod, inst; 16 | let result; 17 | WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject) 18 | .then((result) => { 19 | mod = result.module; 20 | inst = result.instance; 21 | 22 | go.run(inst); 23 | console.log("WASM Loaded"); 24 | }) 25 | .catch((err) => { 26 | console.error(err); 27 | }); 28 | 29 | self.onmessage = async (msg) => { 30 | switch (msg.data.type) { 31 | case "call": 32 | //console.log("Message received: " + msg.data.call); 33 | console.log("Start Rendering"); 34 | args = msg.data.args || []; 35 | await self[msg.data.func](...args); 36 | console.log("Stop Rendering"); 37 | break; 38 | case "set": 39 | self[msg.data.prop] = msg.data.value; 40 | break; 41 | default: 42 | console.error("Unavailable message type!"); 43 | } 44 | }; 45 | 46 | function displayImage(buf) { 47 | let blob = new Blob([buf], { type: imageType }); 48 | console.log("Bytes received" + blob); 49 | postMessage({ 50 | image: blob, 51 | }); 52 | } 53 | 54 | function trackProgress(progress) { 55 | postMessage({ 56 | progress: progress, 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /examples/resizing/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | _ "image/jpeg" 7 | "image/png" 8 | "log" 9 | "os" 10 | ) 11 | 12 | func main() { 13 | img, err := loadImage("source.jpg") 14 | if err != nil { 15 | log.Panic(err) 16 | } 17 | 18 | saveOutput(resizeImage(img, 500, 500), "out.png") 19 | } 20 | 21 | func resizeImage(in image.Image, newWidth int, newHeight int) image.Image { 22 | originalWidth, originalHeight := in.Bounds().Dx(), in.Bounds().Dy() 23 | scalingRatioX := float64(originalWidth) / float64(newWidth) 24 | scalingRatioY := float64(originalHeight) / float64(newHeight) 25 | 26 | out := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight)) 27 | 28 | for x := 0; x < newWidth; x++ { 29 | for y := 0; y < newHeight; y++ { 30 | projectedX := int(float64(x) * scalingRatioX) 31 | projectedY := int(float64(y) * scalingRatioY) 32 | out.Set(x, y, in.At(projectedX, projectedY)) 33 | } 34 | } 35 | 36 | return out 37 | } 38 | 39 | func loadImage(filePath string) (image.Image, error) { 40 | file, err := os.Open(filePath) 41 | if err != nil { 42 | return nil, fmt.Errorf("source image could not be loaded: %w", err) 43 | } 44 | defer file.Close() 45 | img, _, err := image.Decode(file) 46 | if err != nil { 47 | return nil, fmt.Errorf("source image format could not be decoded: %w", err) 48 | } 49 | 50 | return img, nil 51 | } 52 | 53 | func saveOutput(img image.Image, filePath string) error { 54 | f, err := os.Create(filePath) 55 | if err != nil { 56 | return err 57 | } 58 | defer f.Close() 59 | 60 | // Encode to `PNG` with `DefaultCompression` level 61 | // then save to file 62 | err = png.Encode(f, img) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /examples/circular/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/color" 7 | _ "image/jpeg" 8 | "image/png" 9 | "log" 10 | "os" 11 | ) 12 | 13 | type CircularMask struct { 14 | source image.Image 15 | center image.Point 16 | radius int 17 | } 18 | 19 | func (c *CircularMask) ColorModel() color.Model { 20 | return c.source.ColorModel() 21 | } 22 | 23 | func (c *CircularMask) Bounds() image.Rectangle { 24 | return image.Rect(c.center.X-c.radius, c.center.Y-c.radius, c.center.X+c.radius, c.center.Y+c.radius) 25 | } 26 | 27 | func (c *CircularMask) At(x, y int) color.Color { 28 | xx, yy, rr := float64(x-c.center.X), float64(y-c.center.Y), float64(c.radius) 29 | if xx*xx+yy*yy < rr*rr { 30 | return c.source.At(x, y) 31 | } 32 | return color.Alpha{0} 33 | } 34 | 35 | func main() { 36 | img, err := loadImage("source.jpg") 37 | if err != nil { 38 | log.Panic(err) 39 | } 40 | 41 | saveOutput(&CircularMask{ 42 | source: img, 43 | center: image.Point{ 44 | X: 1000, 45 | Y: 2000, 46 | }, 47 | radius: 100, 48 | }, "out.png") 49 | } 50 | 51 | func loadImage(filePath string) (image.Image, error) { 52 | file, err := os.Open(filePath) 53 | if err != nil { 54 | return nil, fmt.Errorf("source image could not be loaded: %w", err) 55 | } 56 | defer file.Close() 57 | img, _, err := image.Decode(file) 58 | if err != nil { 59 | return nil, fmt.Errorf("source image format could not be decoded: %w", err) 60 | } 61 | 62 | return img, nil 63 | } 64 | 65 | func saveOutput(img image.Image, filePath string) error { 66 | f, err := os.Create(filePath) 67 | if err != nil { 68 | return err 69 | } 70 | defer f.Close() 71 | 72 | // Encode to `PNG` with `DefaultCompression` level 73 | // then save to file 74 | err = png.Encode(f, img) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/png" 7 | "log" 8 | "math/rand" 9 | "net/http" 10 | "os" 11 | "time" 12 | 13 | "github.com/preslavrachev/generative-art-in-go/sketch" 14 | ) 15 | 16 | var ( 17 | sourceImgName = "source.jpg" 18 | outputImgName = "out.png" 19 | totalCycleCount = 5000 20 | ) 21 | 22 | func main() { 23 | 24 | img, err := loadImage(sourceImgName) 25 | /* 26 | use this for completely random results 27 | img, err := loadRandomUnsplashImage(2000, 2000) 28 | */ 29 | if err != nil { 30 | log.Panicln(err) 31 | } 32 | 33 | destWidth := 2000 34 | s := sketch.NewSketch(img, sketch.UserParams{ 35 | StrokeRatio: 0.75, 36 | DestWidth: destWidth, 37 | DestHeight: 2000, 38 | InitialAlpha: 0.1, 39 | StrokeReduction: 0.002, 40 | AlphaIncrease: 0.06, 41 | StrokeInversionThreshold: 0.05, 42 | StrokeJitter: int(0.1 * float64(destWidth)), 43 | MinEdgeCount: 3, 44 | MaxEdgeCount: 4, 45 | }) 46 | 47 | rand.Seed(time.Now().Unix()) 48 | 49 | for i := 0; i < totalCycleCount; i++ { 50 | s.Update() 51 | } 52 | 53 | saveOutput(s.Output(), outputImgName) 54 | } 55 | 56 | func loadRandomUnsplashImage(width, height int) (image.Image, error) { 57 | url := fmt.Sprintf("https://source.unsplash.com/random/%dx%d", width, height) 58 | res, err := http.Get(url) 59 | if err != nil { 60 | return nil, err 61 | } 62 | defer res.Body.Close() 63 | 64 | img, _, err := image.Decode(res.Body) 65 | return img, err 66 | } 67 | 68 | func loadImage(src string) (image.Image, error) { 69 | file, _ := os.Open(sourceImgName) 70 | defer file.Close() 71 | img, _, err := image.Decode(file) 72 | return img, err 73 | } 74 | 75 | func saveOutput(img image.Image, filePath string) error { 76 | f, err := os.Create(filePath) 77 | if err != nil { 78 | return err 79 | } 80 | defer f.Close() 81 | 82 | // Encode to `PNG` with `DefaultCompression` level 83 | // then save to file 84 | err = png.Encode(f, img) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is the companion source code repository to my book, ["Generative Art in Go."](https://preslav.me/generative-art-in-golang/) The book is available as an "early access" release on both [Gumroad](https://gumroad.com/l/generative-art-in-golang) and [Leanpub](https://leanpub.com/generative-art-in-golang). While most of the content and surrounding source code are already available, the book is subject to further updates based on readers' feedback. All future updates will be availalble to all exisiting buyers free of charge. 2 | 3 | - [Buy on Gumroad](https://gumroad.com/l/generative-art-in-golang) 4 | - [Buy on Leanpub](https://leanpub.com/generative-art-in-golang) 5 | 6 | # Demo 7 | 8 | To see what you will be able to do by the end of the book, make sure to check out this experimental [Web application](https://goart.netlify.app/) I built a couple of weeks ago. It uses WebAssembly to generate beatiful imagery in the browser. And that, using Go code! 9 | # About the Book 10 | 11 | 12 | 13 | Generative Art in Go is a short eBook intended to introduce novice and experienced programmers to algorithmic art, and hopefully, get non-programmers interested in the [Go programming language](https://golang.org/). Unlike popular choices such as [Processing](https://processing.org/), [NodeBox](https://www.nodebox.net/), [openFrameworks](https://openframeworks.cc/), etc., I picked Go for two simple reasons: 14 | 15 | - Go's syntax is easy enough that virtually anyone can learn it quickly. 16 | - The language is far more versatile than what most programmers use it for. 17 | 18 | Generative art is a unique form of artistic expression, building bridges between computer programming, randomness, and visual aesthetics. It has kept my imagination for over a decade. While I have never been particularly skilled at drawing, I have always been amazed by what one could achieve with a couple of lines of code. 19 | 20 | This book will introduce readers to graphics programming in the Go programming language context through a series of concise examples. It will also guide them in implementing their version of one of my favorite generative art sketches. As a bonus, readers will learn how to turn their command-line sketches into interactive Web applications, using [WebAssembly](https://webassembly.org/) and Web technology's latest standards. -------------------------------------------------------------------------------- /sketch/sketch.go: -------------------------------------------------------------------------------- 1 | package sketch 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "math/rand" 7 | 8 | "github.com/fogleman/gg" 9 | ) 10 | 11 | type UserParams struct { 12 | StrokeRatio float64 13 | DestWidth int 14 | DestHeight int 15 | InitialAlpha float64 16 | StrokeReduction float64 17 | AlphaIncrease float64 18 | StrokeInversionThreshold float64 19 | StrokeJitter int 20 | MinEdgeCount int 21 | MaxEdgeCount int 22 | } 23 | 24 | type Sketch struct { 25 | UserParams // embed for easier access 26 | source image.Image 27 | dc *gg.Context 28 | sourceWidth int 29 | sourceHeight int 30 | strokeSize float64 31 | initialStrokeSize float64 32 | } 33 | 34 | func NewSketch(source image.Image, userParams UserParams) *Sketch { 35 | s := &Sketch{UserParams: userParams} 36 | bounds := source.Bounds() 37 | s.sourceWidth, s.sourceHeight = bounds.Max.X, bounds.Max.Y 38 | s.initialStrokeSize = s.StrokeRatio * float64(s.DestWidth) 39 | s.strokeSize = s.initialStrokeSize 40 | 41 | canvas := gg.NewContext(s.DestWidth, s.DestHeight) 42 | canvas.SetColor(color.Black) 43 | canvas.DrawRectangle(0, 0, float64(s.DestWidth), float64(s.DestHeight)) 44 | canvas.FillPreserve() 45 | 46 | s.source = source 47 | s.dc = canvas 48 | return s 49 | } 50 | 51 | func (s *Sketch) Update() { 52 | rndX := rand.Float64() * float64(s.sourceWidth) 53 | rndY := rand.Float64() * float64(s.sourceHeight) 54 | r, g, b := rgb255(s.source.At(int(rndX), int(rndY))) 55 | 56 | destX := rndX * float64(s.DestWidth) / float64(s.sourceWidth) 57 | destX += float64(randRange(s.StrokeJitter)) 58 | destY := rndY * float64(s.DestHeight) / float64(s.sourceHeight) 59 | destY += float64(randRange(s.StrokeJitter)) 60 | edges := s.MinEdgeCount + rand.Intn(s.MaxEdgeCount-s.MinEdgeCount+1) 61 | 62 | s.dc.SetRGBA255(r, g, b, int(s.InitialAlpha)) 63 | s.dc.DrawRegularPolygon(edges, destX, destY, s.strokeSize, rand.Float64()) 64 | s.dc.FillPreserve() 65 | 66 | if s.strokeSize <= s.StrokeInversionThreshold*s.initialStrokeSize { 67 | if (r+g+b)/3 < 128 { 68 | s.dc.SetRGBA255(255, 255, 255, int(s.InitialAlpha*2)) 69 | } else { 70 | s.dc.SetRGBA255(0, 0, 0, int(s.InitialAlpha*2)) 71 | } 72 | } 73 | s.dc.Stroke() 74 | 75 | s.strokeSize -= s.StrokeReduction * s.strokeSize 76 | s.InitialAlpha += s.AlphaIncrease 77 | } 78 | 79 | func (s *Sketch) Output() image.Image { 80 | return s.dc.Image() 81 | } 82 | 83 | func rgb255(c color.Color) (r, g, b int) { 84 | r0, g0, b0, _ := c.RGBA() 85 | return int(r0 / 257), int(g0 / 257), int(b0 / 257) 86 | } 87 | 88 | func randRange(max int) int { 89 | return -max + rand.Intn(2*max) 90 | } 91 | -------------------------------------------------------------------------------- /wasm/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "image" 6 | _ "image/jpeg" 7 | "image/png" 8 | _ "image/png" 9 | "syscall/js" 10 | 11 | "github.com/preslavrachev/generative-art-in-go/sketch" 12 | ) 13 | 14 | type app struct { 15 | console js.Value 16 | loadImageFunc js.Func 17 | startRenderingFunc js.Func 18 | inputBuffer []uint8 19 | sourceImg image.Image 20 | done chan struct{} 21 | } 22 | 23 | func newApp() *app { 24 | app := app{} 25 | app.setup() 26 | 27 | return &app 28 | } 29 | 30 | func (app *app) setup() { 31 | app.console = js.Global().Get("console") 32 | app.loadImageFunc = js.FuncOf(func(this js.Value, args []js.Value) interface{} { 33 | array := args[0] 34 | app.inputBuffer = make([]uint8, array.Get("byteLength").Int()) 35 | js.CopyBytesToGo(app.inputBuffer, array) 36 | 37 | app.console.Call("log", array.Get("byteLength").Int()) 38 | 39 | reader := bytes.NewReader(app.inputBuffer) 40 | var err error 41 | app.sourceImg, _, err = image.Decode(reader) 42 | if err != nil { 43 | app.console.Call("log", err.Error()) 44 | return nil 45 | } 46 | return nil 47 | }) 48 | 49 | app.startRenderingFunc = js.FuncOf(func(this js.Value, args []js.Value) interface{} { 50 | app.console.Call("log", "creating context") 51 | destWidth := app.sourceImg.Bounds().Dx() 52 | s := sketch.NewSketch(app.sourceImg, sketch.UserParams{ 53 | StrokeRatio: 0.75, 54 | DestWidth: destWidth, 55 | DestHeight: app.sourceImg.Bounds().Dy(), 56 | InitialAlpha: 0.1, 57 | StrokeReduction: 0.002, 58 | AlphaIncrease: 0.06, 59 | StrokeInversionThreshold: 0.05, 60 | StrokeJitter: int(0.1 * float64(destWidth)), 61 | MinEdgeCount: 3, 62 | MaxEdgeCount: 4, 63 | }) 64 | app.console.Call("log", "drawing starts") 65 | for i := 0; i < 5000; i++ { 66 | s.Update() 67 | if i%500 == 0 { 68 | app.updateImage(s.Output()) 69 | } 70 | if i%100 == 0 { 71 | app.trackProgress(int(float64(i) / 5000 * 100)) 72 | } 73 | } 74 | 75 | app.console.Call("log", "drawing done") 76 | app.trackProgress(100) 77 | return nil 78 | }) 79 | } 80 | 81 | func (app *app) trackProgress(progress int) { 82 | js.Global().Call("trackProgress", progress) 83 | } 84 | 85 | func (app *app) updateImage(img image.Image) { 86 | buf := bytes.NewBuffer(make([]byte, 0)) 87 | png.Encode(buf, img) 88 | dst := js.Global().Get("Uint8Array").New(len(buf.Bytes())) 89 | js.CopyBytesToJS(dst, buf.Bytes()) 90 | js.Global().Call("displayImage", dst) 91 | } 92 | 93 | func main() { 94 | app := newApp() 95 | js.Global().Set("loadImage", app.loadImageFunc) 96 | js.Global().Set("startRendering", app.startRenderingFunc) 97 | <-app.done 98 | } 99 | -------------------------------------------------------------------------------- /wasm/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Generate Art With Go and WebAssembly 9 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 |
36 |
37 |
38 |
39 |
40 |

42 | Generate Art With Go and WebAssembly

43 |

This is 44 | a simple experiment at using WebAssembly (WASM) for 45 | rendering a sketch from a source image. This demo is part of my 46 | upcoming book Generative Art in 49 | Go. 50 | Note: This 51 | is an 52 | experiment built for educational purposes. Don't expect 53 | production-grade performance! I can also not guarantee that it 54 | will work with all browsers. 55 |

56 |
57 |
58 |
60 | 70 |
71 | 92 | 93 | 94 | 126 | 127 | 161 | 162 | 163 | --------------------------------------------------------------------------------