Skip to content

Commit fb3c4a0

Browse files
committed
feat: add dithering param and parallel frame rendering for GIF
- Add dithering bool param to GIFRequest (default true) - Parallelize frame rendering with goroutines - Conditional Floyd-Steinberg dithering for speed vs quality tradeoff
1 parent 0cbbe64 commit fb3c4a0

4 files changed

Lines changed: 29 additions & 12 deletions

File tree

internal/api/handlers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func (h *Handlers) HandleGIF(w http.ResponseWriter, r *http.Request) {
103103
return
104104
}
105105

106-
gifBytes, err := render.RenderGIF(result.GLBBytes, result.Atlas, req.Background, req.Frames, req.Width, req.Height, req.Delay)
106+
gifBytes, err := render.RenderGIF(result.GLBBytes, result.Atlas, req.Background, req.Frames, req.Width, req.Height, req.Delay, *req.Dithering)
107107
if err != nil {
108108
writeError(w, http.StatusInternalServerError, "render failed: "+err.Error())
109109
return

internal/api/openapi.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,8 @@ const OpenAPISpec = `{
200200
"frames": {"type": "integer", "default": 36, "description": "Number of frames (36 = 10° per frame)"},
201201
"width": {"type": "integer", "default": 512, "description": "Image width in pixels"},
202202
"height": {"type": "integer", "default": 512, "description": "Image height in pixels"},
203-
"delay": {"type": "integer", "default": 5, "description": "Centiseconds between frames"}
203+
"delay": {"type": "integer", "default": 5, "description": "Centiseconds between frames"},
204+
"dithering": {"type": "boolean", "default": true, "description": "Enable Floyd-Steinberg dithering (disable for faster rendering)"}
204205
}
205206
},
206207
"ErrorResponse": {

internal/api/types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type GIFRequest struct {
1919
Width int `json:"width"` // default 512
2020
Height int `json:"height"` // default 512
2121
Delay int `json:"delay"` // centiseconds between frames, default 5
22+
Dithering *bool `json:"dithering"` // Floyd-Steinberg dithering, default true
2223
}
2324

2425
// ErrorResponse represents an error returned by the API
@@ -56,4 +57,8 @@ func (r *GIFRequest) ApplyDefaults() {
5657
if r.Background == "" {
5758
r.Background = "#FFFFFF"
5859
}
60+
if r.Dithering == nil {
61+
defaultDithering := true
62+
r.Dithering = &defaultDithering
63+
}
5964
}

internal/render/gif.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import (
77
"image/color/palette"
88
"image/draw"
99
"image/gif"
10+
"sync"
1011

1112
"github.com/hytale-tools/blockymodel-merger/pkg/texture"
1213
)
1314

1415
// RenderGIF renders a GLB model to an animated GIF rotating 360 degrees
15-
func RenderGIF(glbBytes []byte, atlas *texture.Atlas, background string, frames, width, height, delay int) ([]byte, error) {
16+
func RenderGIF(glbBytes []byte, atlas *texture.Atlas, background string, frames, width, height, delay int, dithering bool) ([]byte, error) {
1617
// Parse background color
1718
bgColor, err := ParseHexColor(background)
1819
if err != nil {
@@ -38,20 +39,30 @@ func RenderGIF(glbBytes []byte, atlas *texture.Atlas, background string, frames,
3839
LoopCount: 0, // 0 = infinite loop
3940
}
4041

41-
// Render each frame
42+
// Render frames in parallel
43+
var wg sync.WaitGroup
4244
for i := 0; i < frames; i++ {
43-
rotation := float64(i) * rotationPerFrame
45+
wg.Add(1)
46+
go func(frameIdx int) {
47+
defer wg.Done()
48+
rotation := float64(frameIdx) * rotationPerFrame
4449

45-
// Render frame
46-
img := RenderScene(mesh, atlasImage, rotation, width, height, bgColor)
50+
// Render frame
51+
img := RenderScene(mesh, atlasImage, rotation, width, height, bgColor)
4752

48-
// Quantize to palette
49-
paletted := image.NewPaletted(img.Bounds(), palette.Plan9)
50-
draw.FloydSteinberg.Draw(paletted, img.Bounds(), img, image.Point{})
53+
// Quantize to palette
54+
paletted := image.NewPaletted(img.Bounds(), palette.Plan9)
55+
if dithering {
56+
draw.FloydSteinberg.Draw(paletted, img.Bounds(), img, image.Point{})
57+
} else {
58+
draw.Draw(paletted, img.Bounds(), img, image.Point{}, draw.Src)
59+
}
5160

52-
g.Image[i] = paletted
53-
g.Delay[i] = delay
61+
g.Image[frameIdx] = paletted
62+
g.Delay[frameIdx] = delay
63+
}(i)
5464
}
65+
wg.Wait()
5566

5667
// Encode GIF
5768
var buf bytes.Buffer

0 commit comments

Comments
 (0)