Skip to content

Commit fcc495c

Browse files
committed
feat: add autoZoom param to GIF/MP4 renderers
Adjusts camera distance multiplier from 1.5 to 1.25 when enabled, reducing empty space around character while leaving room for cosmetics.
1 parent f8947fd commit fcc495c

7 files changed

Lines changed: 29 additions & 13 deletions

File tree

internal/api/handlers.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ func (h *Handlers) HandlePNG(w http.ResponseWriter, r *http.Request) {
6666
return
6767
}
6868

69-
pngBytes, err := render.RenderPNG(result.GLBBytes, result.Atlas, req.Rotation, req.Background, req.Width, req.Height)
69+
pngBytes, err := render.RenderPNG(result.GLBBytes, result.Atlas, req.Rotation, req.Background, req.Width, req.Height, true)
7070
if err != nil {
7171
writeError(w, http.StatusInternalServerError, "render failed: "+err.Error())
7272
return
@@ -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, *req.Dithering)
106+
gifBytes, err := render.RenderGIF(result.GLBBytes, result.Atlas, req.Background, req.Frames, req.Width, req.Height, req.Delay, *req.Dithering, *req.AutoZoom)
107107
if err != nil {
108108
writeError(w, http.StatusInternalServerError, "render failed: "+err.Error())
109109
return
@@ -140,7 +140,7 @@ func (h *Handlers) HandleMP4(w http.ResponseWriter, r *http.Request) {
140140
return
141141
}
142142

143-
mp4Bytes, err := render.RenderMP4(result.GLBBytes, result.Atlas, req.Background, req.Frames, req.Width, req.Height, req.FPS)
143+
mp4Bytes, err := render.RenderMP4(result.GLBBytes, result.Atlas, req.Background, req.Frames, req.Width, req.Height, req.FPS, *req.AutoZoom)
144144
if err != nil {
145145
writeError(w, http.StatusInternalServerError, "render failed: "+err.Error())
146146
return

internal/api/openapi.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,8 @@ const OpenAPISpec = `{
238238
"width": {"type": "integer", "default": 512, "description": "Image width in pixels"},
239239
"height": {"type": "integer", "default": 512, "description": "Image height in pixels"},
240240
"delay": {"type": "integer", "default": 5, "description": "Centiseconds between frames"},
241-
"dithering": {"type": "boolean", "default": true, "description": "Enable Floyd-Steinberg dithering (disable for faster rendering)"}
241+
"dithering": {"type": "boolean", "default": true, "description": "Enable Floyd-Steinberg dithering (disable for faster rendering)"},
242+
"autoZoom": {"type": "boolean", "default": true, "description": "Auto-zoom camera to fit character tightly in frame"}
242243
}
243244
},
244245
"MP4Request": {
@@ -250,7 +251,8 @@ const OpenAPISpec = `{
250251
"frames": {"type": "integer", "default": 36, "description": "Number of frames (36 = 10° per frame)"},
251252
"width": {"type": "integer", "default": 512, "description": "Video width in pixels"},
252253
"height": {"type": "integer", "default": 512, "description": "Video height in pixels"},
253-
"fps": {"type": "integer", "default": 12, "description": "Frames per second"}
254+
"fps": {"type": "integer", "default": 12, "description": "Frames per second"},
255+
"autoZoom": {"type": "boolean", "default": true, "description": "Auto-zoom camera to fit character tightly in frame"}
254256
}
255257
},
256258
"ErrorResponse": {

internal/api/types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type GIFRequest struct {
2020
Height int `json:"height"` // default 512
2121
Delay int `json:"delay"` // centiseconds between frames, default 5
2222
Dithering *bool `json:"dithering"` // Floyd-Steinberg dithering, default true
23+
AutoZoom *bool `json:"autoZoom"` // auto-zoom to fit character, default true
2324
}
2425

2526
// MP4Request represents a request to render a character as MP4 video
@@ -30,6 +31,7 @@ type MP4Request struct {
3031
Width int `json:"width"` // default 512
3132
Height int `json:"height"` // default 512
3233
FPS int `json:"fps"` // frames per second, default 12
34+
AutoZoom *bool `json:"autoZoom"` // auto-zoom to fit character, default true
3335
}
3436

3537
// ErrorResponse represents an error returned by the API
@@ -71,6 +73,10 @@ func (r *GIFRequest) ApplyDefaults() {
7173
defaultDithering := true
7274
r.Dithering = &defaultDithering
7375
}
76+
if r.AutoZoom == nil {
77+
defaultAutoZoom := true
78+
r.AutoZoom = &defaultAutoZoom
79+
}
7480
}
7581

7682
// ApplyDefaults fills in default values for MP4Request
@@ -90,4 +96,8 @@ func (r *MP4Request) ApplyDefaults() {
9096
if r.Background == "" {
9197
r.Background = "#FFFFFF"
9298
}
99+
if r.AutoZoom == nil {
100+
defaultAutoZoom := true
101+
r.AutoZoom = &defaultAutoZoom
102+
}
93103
}

internal/render/gif.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
)
1515

1616
// RenderGIF renders a GLB model to an animated GIF rotating 360 degrees
17-
func RenderGIF(glbBytes []byte, atlas *texture.Atlas, background string, frames, width, height, delay int, dithering bool) ([]byte, error) {
17+
func RenderGIF(glbBytes []byte, atlas *texture.Atlas, background string, frames, width, height, delay int, dithering, autoZoom bool) ([]byte, error) {
1818
// Parse background color
1919
bgColor, err := ParseHexColor(background)
2020
if err != nil {
@@ -41,7 +41,7 @@ func RenderGIF(glbBytes []byte, atlas *texture.Atlas, background string, frames,
4141
go func(frameIdx int) {
4242
defer wg.Done()
4343
rotation := float64(frameIdx) * rotationPerFrame
44-
renderedFrames[frameIdx] = RenderScene(mesh, atlasImage, rotation, width, height, bgColor)
44+
renderedFrames[frameIdx] = RenderScene(mesh, atlasImage, rotation, width, height, bgColor, autoZoom)
4545
}(i)
4646
}
4747
wg.Wait()

internal/render/mp4.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
)
1313

1414
// RenderMP4 renders a GLB model to an MP4 video rotating 360 degrees
15-
func RenderMP4(glbBytes []byte, atlas *texture.Atlas, background string, frames, width, height, fps int) ([]byte, error) {
15+
func RenderMP4(glbBytes []byte, atlas *texture.Atlas, background string, frames, width, height, fps int, autoZoom bool) ([]byte, error) {
1616
// Parse background color
1717
bgColor, err := ParseHexColor(background)
1818
if err != nil {
@@ -47,7 +47,7 @@ func RenderMP4(glbBytes []byte, atlas *texture.Atlas, background string, frames,
4747
go func(frameIdx int) {
4848
defer wg.Done()
4949
rotation := float64(frameIdx) * rotationPerFrame
50-
img := RenderScene(mesh, atlasImage, rotation, width, height, bgColor)
50+
img := RenderScene(mesh, atlasImage, rotation, width, height, bgColor, autoZoom)
5151

5252
// Write frame to temp file
5353
framePath := filepath.Join(tempDir, fmt.Sprintf("frame_%04d.png", frameIdx))

internal/render/png.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
)
1010

1111
// RenderPNG renders a GLB model to PNG with the given parameters
12-
func RenderPNG(glbBytes []byte, atlas *texture.Atlas, rotation float64, background string, width, height int) ([]byte, error) {
12+
func RenderPNG(glbBytes []byte, atlas *texture.Atlas, rotation float64, background string, width, height int, autoZoom bool) ([]byte, error) {
1313
// Parse background color
1414
bgColor, err := ParseHexColor(background)
1515
if err != nil {
@@ -26,7 +26,7 @@ func RenderPNG(glbBytes []byte, atlas *texture.Atlas, rotation float64, backgrou
2626
}
2727

2828
// Render the scene
29-
img := RenderScene(mesh, atlasImage, rotation, width, height, bgColor)
29+
img := RenderScene(mesh, atlasImage, rotation, width, height, bgColor, autoZoom)
3030

3131
// Encode to PNG
3232
var buf bytes.Buffer

internal/render/software.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ func quaternionToMatrix(x, y, z, w float64) fauxgl.Matrix {
313313
}
314314

315315
// RenderScene renders a mesh with the given parameters
316-
func RenderScene(mesh *fauxgl.Mesh, atlasImage image.Image, rotationY float64, width, height int, bgColor color.Color) image.Image {
316+
func RenderScene(mesh *fauxgl.Mesh, atlasImage image.Image, rotationY float64, width, height int, bgColor color.Color, autoZoom bool) image.Image {
317317
context := fauxgl.NewContext(width, height)
318318
context.Cull = fauxgl.CullNone
319319
context.AlphaBlend = false
@@ -342,7 +342,11 @@ func RenderScene(mesh *fauxgl.Mesh, atlasImage image.Image, rotationY float64, w
342342
far := 100.0
343343

344344
maxDim := math.Max(modelSize.X, math.Max(modelSize.Y, modelSize.Z))
345-
cameraDistance := maxDim / (2 * math.Tan(fauxgl.Radians(fovy/2))) * 1.5
345+
multiplier := 1.5
346+
if autoZoom {
347+
multiplier = 1.25
348+
}
349+
cameraDistance := maxDim / (2 * math.Tan(fauxgl.Radians(fovy/2))) * multiplier
346350

347351
eye := fauxgl.V(modelCenter.X, modelCenter.Y, modelCenter.Z+cameraDistance)
348352
center := modelCenter

0 commit comments

Comments
 (0)