Skip to content

Commit

Permalink
Add a pure-browser version of the Pixlet UI (tidbyt#784)
Browse files Browse the repository at this point in the history
* Support compiling with GOOS=js GOARCH=wasm

`GOOS=js GOARCH=wasm make build` should work now.

* Add a pure-browser version of the Pixlet UI

Adds a WASM backend mode for Pixlet, which builds `pixlet.wasm` and
loads it in the browser. This WASM binary then runs Starlark, without
the need for the `pixlet` Go backend.

Use `npm run start:wasm` to try it.
  • Loading branch information
rohansingh authored May 23, 2023
1 parent f88aa23 commit dd68315
Show file tree
Hide file tree
Showing 35 changed files with 712 additions and 203 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ examples/*.gif
# Pixlet Binary
pixlet
pixlet.exe
pixlet.wasm

# Releases
build/
out/
dist/

# Dependency directories
node_modules
9 changes: 6 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ else
TAGS =
endif

all: build
all: build wasm

test:
go test $(TAGS) -v -cover ./...
Expand All @@ -24,7 +24,7 @@ bench:
go test -benchmem -benchtime=20s -bench BenchmarkRunAndRender tidbyt.dev/pixlet/encode

build:
go build $(LDFLAGS) $(TAGS) -o $(BINARY) tidbyt.dev/pixlet
go build $(LDFLAGS) $(TAGS) -o $(BINARY) tidbyt.dev/pixlet

embedfonts:
go run render/gen/embedfonts.go
Expand All @@ -50,4 +50,7 @@ lint:
@ buildifier --version >/dev/null 2>&1 || $(MAKE) install-buildifier
buildifier -r ./

format: lint
format: lint

wasm:
GOOS=js GOARCH=wasm go build -o ./src/pixlet.wasm tidbyt.dev/pixlet
1 change: 0 additions & 1 deletion cmd/community/community.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
)

func init() {
CommunityCmd.AddCommand(CreateManifestCmd)
CommunityCmd.AddCommand(ListIconsCmd)
CommunityCmd.AddCommand(LoadAppCmd)
CommunityCmd.AddCommand(SpellCheckCmd)
Expand Down
6 changes: 6 additions & 0 deletions cmd/community/createmanifest.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !js && !wasm

package community

import (
Expand All @@ -18,6 +20,10 @@ var CreateManifestCmd = &cobra.Command{
RunE: CreateManifest,
}

func init() {
CommunityCmd.AddCommand(CreateManifestCmd)
}

func CreateManifest(cmd *cobra.Command, args []string) error {
fileName := filepath.Base(args[0])
if fileName != manifest.ManifestFileName {
Expand Down
2 changes: 2 additions & 0 deletions cmd/community/manifestprompt.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !js && !wasm

package community

import (
Expand Down
2 changes: 2 additions & 0 deletions cmd/create.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !js && !wasm

package cmd

import (
Expand Down
110 changes: 0 additions & 110 deletions encode/encode.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
package encode

import (
"bytes"
"crypto/sha256"
"fmt"
"image"
"image/color"
"image/draw"
"image/gif"
"time"

"github.com/ericpauley/go-quantize/quantize"
"github.com/pkg/errors"
"github.com/tidbyt/go-libwebp/webp"
"github.com/vmihailenco/msgpack/v5"

"tidbyt.dev/pixlet/render"
Expand Down Expand Up @@ -92,108 +84,6 @@ func (s *Screens) Hash() ([]byte, error) {
return h[:], nil
}

// Renders a screen to WebP. Optionally pass filters for
// postprocessing each individual frame.
func (s *Screens) EncodeWebP(maxDuration int, filters ...ImageFilter) ([]byte, error) {
images, err := s.render(filters...)
if err != nil {
return nil, err
}

if len(images) == 0 {
return []byte{}, nil
}

bounds := images[0].Bounds()
anim, err := webp.NewAnimationEncoder(
bounds.Dx(),
bounds.Dy(),
WebPKMin,
WebPKMax,
)
if err != nil {
return nil, errors.Wrap(err, "initializing encoder")
}
defer anim.Close()

remainingDuration := time.Duration(maxDuration) * time.Millisecond
for _, im := range images {
frameDuration := time.Duration(s.delay) * time.Millisecond

if maxDuration > 0 {
if frameDuration > remainingDuration {
frameDuration = remainingDuration
}
remainingDuration -= frameDuration
}

if err := anim.AddFrame(im, frameDuration); err != nil {
return nil, errors.Wrap(err, "adding frame")
}

if maxDuration > 0 && remainingDuration <= 0 {
break
}
}

buf, err := anim.Assemble()
if err != nil {
return nil, errors.Wrap(err, "encoding animation")
}

return buf, nil
}

// Renders a screen to GIF. Optionally pass filters for postprocessing
// each individual frame.
func (s *Screens) EncodeGIF(maxDuration int, filters ...ImageFilter) ([]byte, error) {
images, err := s.render(filters...)
if err != nil {
return nil, err
}

if len(images) == 0 {
return []byte{}, nil
}

g := &gif.GIF{}

remainingDuration := maxDuration
for imIdx, im := range images {
imRGBA, ok := im.(*image.RGBA)
if !ok {
return nil, fmt.Errorf("image %d is %T, require RGBA", imIdx, im)
}

palette := quantize.MedianCutQuantizer{}.Quantize(make([]color.Color, 0, 256), im)
imPaletted := image.NewPaletted(imRGBA.Bounds(), palette)
draw.Draw(imPaletted, imRGBA.Bounds(), imRGBA, image.Point{0, 0}, draw.Src)

frameDelay := int(s.delay)
if maxDuration > 0 {
if frameDelay > remainingDuration {
frameDelay = remainingDuration
}
remainingDuration -= frameDelay
}

g.Image = append(g.Image, imPaletted)
g.Delay = append(g.Delay, frameDelay/10) // in 100ths of a second

if maxDuration > 0 && remainingDuration <= 0 {
break
}
}

buf := &bytes.Buffer{}
err = gif.EncodeAll(buf, g)
if err != nil {
return nil, errors.Wrap(err, "encoding")
}

return buf.Bytes(), nil
}

func (s *Screens) render(filters ...ImageFilter) ([]image.Image, error) {
if s.images == nil {
s.images = render.PaintRoots(true, s.roots...)
Expand Down
63 changes: 63 additions & 0 deletions encode/gif.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package encode

import (
"bytes"
"fmt"
"image"
"image/color"
"image/draw"
"image/gif"

"github.com/ericpauley/go-quantize/quantize"
"github.com/pkg/errors"
)

// Renders a screen to GIF. Optionally pass filters for postprocessing
// each individual frame.
func (s *Screens) EncodeGIF(maxDuration int, filters ...ImageFilter) ([]byte, error) {
images, err := s.render(filters...)
if err != nil {
return nil, err
}

if len(images) == 0 {
return []byte{}, nil
}

g := &gif.GIF{}

remainingDuration := maxDuration
for imIdx, im := range images {
imRGBA, ok := im.(*image.RGBA)
if !ok {
return nil, fmt.Errorf("image %d is %T, require RGBA", imIdx, im)
}

palette := quantize.MedianCutQuantizer{}.Quantize(make([]color.Color, 0, 256), im)
imPaletted := image.NewPaletted(imRGBA.Bounds(), palette)
draw.Draw(imPaletted, imRGBA.Bounds(), imRGBA, image.Point{0, 0}, draw.Src)

frameDelay := int(s.delay)
if maxDuration > 0 {
if frameDelay > remainingDuration {
frameDelay = remainingDuration
}
remainingDuration -= frameDelay
}

g.Image = append(g.Image, imPaletted)
g.Delay = append(g.Delay, frameDelay/10) // in 100ths of a second

if maxDuration > 0 && remainingDuration <= 0 {
break
}
}

buf := &bytes.Buffer{}
err = gif.EncodeAll(buf, g)
if err != nil {
return nil, errors.Wrap(err, "encoding")
}

return buf.Bytes(), nil
}
62 changes: 62 additions & 0 deletions encode/webp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//go:build !js && !wasm

package encode

import (
"time"

"github.com/pkg/errors"
"github.com/tidbyt/go-libwebp/webp"
)

// Renders a screen to WebP. Optionally pass filters for
// postprocessing each individual frame.
func (s *Screens) EncodeWebP(maxDuration int, filters ...ImageFilter) ([]byte, error) {
images, err := s.render(filters...)
if err != nil {
return nil, err
}

if len(images) == 0 {
return []byte{}, nil
}

bounds := images[0].Bounds()
anim, err := webp.NewAnimationEncoder(
bounds.Dx(),
bounds.Dy(),
WebPKMin,
WebPKMax,
)
if err != nil {
return nil, errors.Wrap(err, "initializing encoder")
}
defer anim.Close()

remainingDuration := time.Duration(maxDuration) * time.Millisecond
for _, im := range images {
frameDuration := time.Duration(s.delay) * time.Millisecond

if maxDuration > 0 {
if frameDuration > remainingDuration {
frameDuration = remainingDuration
}
remainingDuration -= frameDuration
}

if err := anim.AddFrame(im, frameDuration); err != nil {
return nil, errors.Wrap(err, "adding frame")
}

if maxDuration > 0 && remainingDuration <= 0 {
break
}
}

buf, err := anim.Assemble()
if err != nil {
return nil, errors.Wrap(err, "encoding animation")
}

return buf, nil
}
10 changes: 10 additions & 0 deletions encode/webp_js.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//go:build js && wasm

package encode

// Renders a screen to WebP. Optionally pass filters for
// postprocessing each individual frame.
func (s *Screens) EncodeWebP(maxDuration int, filters ...ImageFilter) ([]byte, error) {
// lol you gullible sucker, you thought you could use webp in wasm?
return s.EncodeGIF(maxDuration, filters...)
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ require (
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/nlepage/go-js-promise v1.0.0 // indirect
github.com/nlepage/go-wasm-http-server v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,10 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20220510032225-4f9f17eaec4c h1:4RYnE0ISVwRxm9Dfo7utw1dh0kdRDEmVYq2MFVLy5zI=
github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20220510032225-4f9f17eaec4c/go.mod h1:DvuJJ/w1Y59rG8UTDxsMk5U+UJXJwuvUgbiJSm9yhX8=
github.com/nlepage/go-js-promise v1.0.0 h1:K7OmJ3+0BgWJ2LfXchg2sI6RDr7AW/KWR8182epFwGQ=
github.com/nlepage/go-js-promise v1.0.0/go.mod h1:bdOP0wObXu34euibyK39K1hoBCtlgTKXGc56AGflaRo=
github.com/nlepage/go-wasm-http-server v1.1.0 h1:phw2NtSp71m/6NmGjE2veQ41PBPzWFcnE614cKucy5M=
github.com/nlepage/go-wasm-http-server v1.1.0/go.mod h1:xpffUeN97vuv8CTlMJ2oC5tPsftfPoG9HkAgI9gkiPI=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/paulmach/orb v0.1.5/go.mod h1:pPwxxs3zoAyosNSbNKn1jiXV2+oovRDObDKfTvRegDI=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
Expand Down
1 change: 0 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ func init() {
rootCmd.AddCommand(cmd.DevicesCmd)
rootCmd.AddCommand(cmd.FormatCmd)
rootCmd.AddCommand(cmd.LintCmd)
rootCmd.AddCommand(cmd.CreateCmd)
rootCmd.AddCommand(cmd.CheckCmd)
rootCmd.AddCommand(cmd.BundleCmd)
rootCmd.AddCommand(cmd.UploadCmd)
Expand Down
11 changes: 11 additions & 0 deletions main_nonjs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build !js && !wasm

package main

import (
"tidbyt.dev/pixlet/cmd"
)

func init() {
rootCmd.AddCommand(cmd.CreateCmd)
}
Loading

0 comments on commit dd68315

Please sign in to comment.