diff --git a/internal/api/handlers.go b/internal/api/handlers.go index b98d511..2a93437 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -103,7 +103,7 @@ func (h *Handlers) HandleGIF(w http.ResponseWriter, r *http.Request) { return } - gifBytes, err := render.RenderGIF(result.GLBBytes, result.Atlas, req.Background, req.Frames, req.Width, req.Height, req.Delay) + gifBytes, err := render.RenderGIF(result.GLBBytes, result.Atlas, req.Background, req.Frames, req.Width, req.Height, req.Delay, *req.Dithering) if err != nil { writeError(w, http.StatusInternalServerError, "render failed: "+err.Error()) return diff --git a/internal/api/openapi.go b/internal/api/openapi.go index e7573dc..dc681f5 100644 --- a/internal/api/openapi.go +++ b/internal/api/openapi.go @@ -200,7 +200,8 @@ const OpenAPISpec = `{ "frames": {"type": "integer", "default": 36, "description": "Number of frames (36 = 10° per frame)"}, "width": {"type": "integer", "default": 512, "description": "Image width in pixels"}, "height": {"type": "integer", "default": 512, "description": "Image height in pixels"}, - "delay": {"type": "integer", "default": 5, "description": "Centiseconds between frames"} + "delay": {"type": "integer", "default": 5, "description": "Centiseconds between frames"}, + "dithering": {"type": "boolean", "default": true, "description": "Enable Floyd-Steinberg dithering (disable for faster rendering)"} } }, "ErrorResponse": { diff --git a/internal/api/types.go b/internal/api/types.go index 8379712..14300a3 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -19,6 +19,7 @@ type GIFRequest struct { Width int `json:"width"` // default 512 Height int `json:"height"` // default 512 Delay int `json:"delay"` // centiseconds between frames, default 5 + Dithering *bool `json:"dithering"` // Floyd-Steinberg dithering, default true } // ErrorResponse represents an error returned by the API @@ -56,4 +57,8 @@ func (r *GIFRequest) ApplyDefaults() { if r.Background == "" { r.Background = "#FFFFFF" } + if r.Dithering == nil { + defaultDithering := true + r.Dithering = &defaultDithering + } } diff --git a/internal/render/gif.go b/internal/render/gif.go index 6762d52..8c5ecac 100644 --- a/internal/render/gif.go +++ b/internal/render/gif.go @@ -7,12 +7,13 @@ import ( "image/color/palette" "image/draw" "image/gif" + "sync" "github.com/hytale-tools/blockymodel-merger/pkg/texture" ) // RenderGIF renders a GLB model to an animated GIF rotating 360 degrees -func RenderGIF(glbBytes []byte, atlas *texture.Atlas, background string, frames, width, height, delay int) ([]byte, error) { +func RenderGIF(glbBytes []byte, atlas *texture.Atlas, background string, frames, width, height, delay int, dithering bool) ([]byte, error) { // Parse background color bgColor, err := ParseHexColor(background) if err != nil { @@ -38,20 +39,30 @@ func RenderGIF(glbBytes []byte, atlas *texture.Atlas, background string, frames, LoopCount: 0, // 0 = infinite loop } - // Render each frame + // Render frames in parallel + var wg sync.WaitGroup for i := 0; i < frames; i++ { - rotation := float64(i) * rotationPerFrame + wg.Add(1) + go func(frameIdx int) { + defer wg.Done() + rotation := float64(frameIdx) * rotationPerFrame - // Render frame - img := RenderScene(mesh, atlasImage, rotation, width, height, bgColor) + // Render frame + img := RenderScene(mesh, atlasImage, rotation, width, height, bgColor) - // Quantize to palette - paletted := image.NewPaletted(img.Bounds(), palette.Plan9) - draw.FloydSteinberg.Draw(paletted, img.Bounds(), img, image.Point{}) + // Quantize to palette + paletted := image.NewPaletted(img.Bounds(), palette.Plan9) + if dithering { + draw.FloydSteinberg.Draw(paletted, img.Bounds(), img, image.Point{}) + } else { + draw.Draw(paletted, img.Bounds(), img, image.Point{}, draw.Src) + } - g.Image[i] = paletted - g.Delay[i] = delay + g.Image[frameIdx] = paletted + g.Delay[frameIdx] = delay + }(i) } + wg.Wait() // Encode GIF var buf bytes.Buffer