clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

chore/regenerate-vhs-gifs
puzzletea / pdfexport / render_cover.go
render_cover.go 36.2K
1package pdfexport23import (4 "fmt"5 "math"6 "reflect"7 "sort"8 "strings"910 "codeberg.org/go-pdf/fpdf"11)1213type coverOutsideSlice int1415const (16 coverOutsideBack coverOutsideSlice = iota17 coverOutsideFront18)1920type coverCompositionFamily int2122const (23 coverFamilyFrame coverCompositionFamily = iota24 coverFamilyStack25 coverFamilyHinge26 coverFamilyIsland27)2829type coverDirection int3031const (32 coverDirectionVertical coverDirection = iota33 coverDirectionHorizontal34 coverDirectionDiagonal35 coverDirectionClustered36)3738type coverAspectBucket int3940const (41 coverAspectTall coverAspectBucket = iota42 coverAspectWide43 coverAspectCompact44 coverAspectOffset45)4647type coverTextureKind int4849const (50 coverTextureNone coverTextureKind = iota51 coverTextureStripes52 coverTextureHatch53 coverTextureDots54 coverTextureChecker55 coverTextureLattice56)5758type coverFillMode int5960const (61 coverFillSolid coverFillMode = iota62 coverFillSlits63 coverFillOpenGrid64 coverFillPunctured65 coverFillBanded66)6768type coverCutoutKind int6970const (71 coverCutoutRect coverCutoutKind = iota72 coverCutoutCircle73)7475type coverShapeKind int7677const (78 coverShapeRect coverShapeKind = iota79 coverShapeCircle80 coverShapeCroppedCircle81 coverShapeStepped82)8384type coverShapeEdge int8586const (87 coverEdgeLeft coverShapeEdge = iota88 coverEdgeRight89 coverEdgeTop90 coverEdgeBottom91)9293type coverCutout struct {94 Kind coverCutoutKind95 X float6496 Y float6497 W float6498 H float6499 R float64100}101102type coverShape struct {103 Kind coverShapeKind104 Edge coverShapeEdge105 Bounds rectMM106 PivotX float64107 PivotY float64108 Rotate float64109 Locked bool110 Fill coverFillMode111 Texture coverTextureKind112 Step float64113 Weight float64114 Cutouts []coverCutout115}116117type coverArtLayout struct {118 ArtArea rectMM119 LockupExclusion rectMM120 Family coverCompositionFamily121 Direction coverDirection122 Shapes []coverShape123}124125type coverRand interface {126 Float64() float64127 IntN(int) int128}129130func splitCoverTextLines(pdf *fpdf.Fpdf, text string, maxW float64) []string {131 trimmed := strings.TrimSpace(text)132 if trimmed == "" {133 return []string{"PuzzleTea Collection"}134 }135 chunks := pdf.SplitLines([]byte(trimmed), maxW)136 if len(chunks) == 0 {137 return []string{trimmed}138 }139 lines := make([]string, 0, len(chunks))140 for _, c := range chunks {141 lines = append(lines, string(c))142 }143 return lines144}145146func splitClampedTextLines(pdf *fpdf.Fpdf, text string, maxW float64, maxLines int) []string {147 if maxLines < 1 {148 maxLines = 1149 }150151 lines := splitCoverTextLines(pdf, text, maxW)152 if len(lines) <= maxLines {153 return lines154 }155156 out := make([]string, 0, maxLines)157 out = append(out, lines[:maxLines-1]...)158 out = append(out, strings.Join(lines[maxLines-1:], " "))159 return out160}161162func splitCoverSubtitleLines(pdf *fpdf.Fpdf, subtitle string, maxW float64, maxLines int) []string {163 return splitClampedTextLines(pdf, subtitle, maxW, maxLines)164}165166func renderOutsideCoverPage(pdf *fpdf.Fpdf, cfg RenderConfig, slice coverOutsideSlice) {167 const frameInset = 7.5168169 pageW, pageH := pdf.GetPageSize()170 drawOutsideSpreadFrameSlice(pdf, slice, frameInset, pageW, pageH)171172 if slice == coverOutsideFront {173 layout := buildCoverArtLayout(cfg, pageW, pageH)174 drawCoverArtLayout(pdf, layout)175 renderFrontCoverLockup(pdf, cfg, pageW, frameInset)176 return177 }178179 renderBackCoverImprint(pdf, cfg, pageW, pageH, frameInset)180}181182func renderCoverBlankPage(_ *fpdf.Fpdf) {}183184func drawOutsideSpreadFrameSlice(pdf *fpdf.Fpdf, slice coverOutsideSlice, inset, pageW, pageH float64) {185 drawCoverSpreadBorder(pdf, slice, inset, 1.1, pageW, pageH)186 drawCoverSpreadBorder(pdf, slice, inset+1.7, 0.28, pageW, pageH)187}188189func drawCoverSpreadBorder(pdf *fpdf.Fpdf, slice coverOutsideSlice, inset, lineW, pageW, pageH float64) {190 pdf.SetDrawColor(8, 8, 8)191 pdf.SetLineWidth(lineW)192193 leftX, rightX := coverSliceHorizontalSpan(slice, inset, pageW)194 pdf.Line(leftX, inset, rightX, inset)195 pdf.Line(leftX, pageH-inset, rightX, pageH-inset)196197 if slice == coverOutsideBack {198 pdf.Line(inset, inset, inset, pageH-inset)199 return200 }201 pdf.Line(pageW-inset, inset, pageW-inset, pageH-inset)202}203204func coverSliceHorizontalSpan(slice coverOutsideSlice, inset, pageW float64) (left, right float64) {205 if slice == coverOutsideBack {206 return inset, pageW207 }208 return 0, pageW - inset209}210211func renderFrontCoverLockup(pdf *fpdf.Fpdf, cfg RenderConfig, pageW, frameInset float64) {212 labelW := min(pageW*0.54, 74.0)213 labelX := pageW - frameInset - 7.0 - labelW214215 subtitle := strings.TrimSpace(cfg.CoverSubtitle)216 if subtitle == "" {217 subtitle = "PuzzleTea Collection"218 }219220 fontSize := 18.0 + pdfFontSizeDelta221 for fontSize >= 13.5+pdfFontSizeDelta {222 pdf.SetFont(coverFontFamily, "", fontSize)223 if len(splitCoverSubtitleLines(pdf, subtitle, labelW, 3)) <= 3 {224 break225 }226 fontSize -= 0.8227 }228229 pdf.SetTextColor(8, 8, 8)230 pdf.SetFont(sansFontFamily, "B", 9.6+pdfFontSizeDelta)231 pdf.SetXY(labelX, frameInset+5.0)232 pdf.CellFormat(labelW, 4.5, fmt.Sprintf("VOL. %02d", cfg.VolumeNumber), "", 0, "R", false, 0, "")233234 pdf.SetFont(coverFontFamily, "", fontSize)235 lineH := fontSize * 0.44236 y := frameInset + 13.2237 for _, line := range splitCoverSubtitleLines(pdf, subtitle, labelW, 3) {238 pdf.SetXY(labelX, y)239 pdf.CellFormat(labelW, lineH, line, "", 0, "R", false, 0, "")240 y += lineH241 }242}243244func renderBackCoverImprint(pdf *fpdf.Fpdf, cfg RenderConfig, pageW, pageH, frameInset float64) {245 labelX := frameInset + 6.0246 labelW := min(pageW*0.42, 52.0)247248 pdf.SetTextColor(8, 8, 8)249 pdf.SetFont(sansFontFamily, "B", 8.2+pdfFontSizeDelta)250 pdf.SetXY(labelX, pageH-frameInset-13.4)251 pdf.CellFormat(labelW, 4.0, "PuzzleTea", "", 0, "L", false, 0, "")252253 pdf.SetFont(sansFontFamily, "", 7.3+pdfFontSizeDelta)254 pdf.SetXY(labelX, pageH-frameInset-8.5)255 pdf.CellFormat(labelW, 3.8, fmt.Sprintf("Vol. %02d", cfg.VolumeNumber), "", 0, "L", false, 0, "")256}257258func buildCoverArtLayout(cfg RenderConfig, pageW, pageH float64) coverArtLayout {259 rng := seededRand(fmt.Sprintf("cover:%s:%d", cfg.ShuffleSeed, cfg.VolumeNumber))260 layout := coverArtLayout{261 ArtArea: coverArtArea(pageW, pageH),262 LockupExclusion: coverLockupExclusion(pageW),263 Family: coverCompositionFamily(rng.IntN(4)),264 }265266 switch layout.Family {267 case coverFamilyFrame:268 layout.Direction = buildFrameFamily(&layout, rng)269 case coverFamilyStack:270 layout.Direction = buildStackFamily(&layout, rng)271 case coverFamilyHinge:272 layout.Direction = buildHingeFamily(&layout, rng)273 default:274 layout.Direction = buildIslandFamily(&layout, rng)275 }276277 return layout278}279280func coverArtArea(pageW, pageH float64) rectMM {281 return rectMM{282 x: 12.5,283 y: 37.0,284 w: pageW - 25.0,285 h: pageH - 56.0,286 }287}288289func coverLockupExclusion(pageW float64) rectMM {290 return rectMM{291 x: pageW - 89.0,292 y: 8.0,293 w: 78.0,294 h: 32.0,295 }296}297298func buildFrameFamily(layout *coverArtLayout, rng coverRand) coverDirection {299 outer := rectMM{300 x: layout.ArtArea.x + layout.ArtArea.w*(0.06+rng.Float64()*0.14),301 y: layout.ArtArea.y + layout.ArtArea.h*(0.08+rng.Float64()*0.10),302 w: layout.ArtArea.w * (0.42 + rng.Float64()*0.20),303 h: layout.ArtArea.h * (0.48 + rng.Float64()*0.20),304 }305 thickness := min(outer.w, outer.h) * (0.16 + rng.Float64()*0.07)306 openSide := coverShapeEdge(rng.IntN(4))307 direction := coverDirectionVertical308 if outer.w > outer.h {309 direction = coverDirectionHorizontal310 }311312 add := func(kind coverShapeKind, edge coverShapeEdge, bounds rectMM) {313 layout.Shapes = append(layout.Shapes, decoratedCoverShape(layout, kind, edge, bounds, coverFamilyFrame, true, rng))314 }315316 if openSide != coverEdgeTop {317 add(shapeFamilyKind(rng, coverShapeRect, coverShapeStepped), coverEdgeTop, rectMM{318 x: outer.x,319 y: outer.y,320 w: outer.w,321 h: thickness,322 })323 }324 if openSide != coverEdgeLeft {325 add(shapeFamilyKind(rng, coverShapeRect, coverShapeStepped), coverEdgeLeft, rectMM{326 x: outer.x,327 y: outer.y + thickness*0.55,328 w: thickness * (0.9 + rng.Float64()*0.35),329 h: outer.h - thickness*0.55,330 })331 }332 if openSide != coverEdgeRight {333 add(shapeFamilyKind(rng, coverShapeRect, coverShapeStepped), coverEdgeRight, rectMM{334 x: outer.x + outer.w - thickness*(0.9+rng.Float64()*0.35),335 y: outer.y + thickness*0.35,336 w: thickness * (0.9 + rng.Float64()*0.35),337 h: outer.h - thickness*0.35,338 })339 }340 if openSide != coverEdgeBottom {341 add(shapeFamilyKind(rng, coverShapeRect, coverShapeStepped), coverEdgeBottom, rectMM{342 x: outer.x + outer.w*(0.04+rng.Float64()*0.10),343 y: outer.y + outer.h - thickness*(0.95+rng.Float64()*0.25),344 w: outer.w * (0.56 + rng.Float64()*0.20),345 h: thickness * (0.95 + rng.Float64()*0.25),346 })347 }348349 if rng.IntN(2) == 0 {350 layout.Shapes = append(layout.Shapes, decoratedCoverShape(layout, coverShapeCroppedCircle, oppositeEdge(openSide), rectMM{351 x: outer.x + outer.w*(0.52+rng.Float64()*0.16),352 y: outer.y + outer.h*(0.48+rng.Float64()*0.16),353 w: outer.w * (0.16 + rng.Float64()*0.12),354 h: outer.w * (0.16 + rng.Float64()*0.12),355 }, coverFamilyFrame, false, rng))356 }357358 return direction359}360361func buildStackFamily(layout *coverArtLayout, rng coverRand) coverDirection {362 vertical := rng.IntN(2) == 0363 if vertical {364 x := layout.ArtArea.x + layout.ArtArea.w*(0.06+rng.Float64()*0.18)365 y := layout.ArtArea.y + layout.ArtArea.h*(0.08+rng.Float64()*0.10)366 widths := []float64{367 layout.ArtArea.w * (0.26 + rng.Float64()*0.10),368 layout.ArtArea.w * (0.48 + rng.Float64()*0.16),369 layout.ArtArea.w * (0.34 + rng.Float64()*0.16),370 }371 heights := []float64{372 layout.ArtArea.h * (0.18 + rng.Float64()*0.08),373 layout.ArtArea.h * (0.22 + rng.Float64()*0.10),374 layout.ArtArea.h * (0.16 + rng.Float64()*0.08),375 }376 gap := 4.0 + rng.Float64()*4.0377 curY := y378 for i := range widths {379 bounds := rectMM{x: x, y: curY, w: widths[i], h: heights[i]}380 kind := coverShapeRect381 if i == 0 && rng.IntN(2) == 0 {382 kind = coverShapeStepped383 }384 layout.Shapes = append(layout.Shapes, decoratedCoverShape(layout, kind, coverEdgeLeft, bounds, coverFamilyStack, false, rng))385 curY += heights[i] + gap386 }387 layout.Shapes = append(layout.Shapes, decoratedCoverShape(layout, coverShapeCircle, coverEdgeRight, rectMM{388 x: x + layout.ArtArea.w*(0.44+rng.Float64()*0.12),389 y: y + layout.ArtArea.h*(0.18+rng.Float64()*0.18),390 w: layout.ArtArea.w * (0.16 + rng.Float64()*0.10),391 h: layout.ArtArea.w * (0.16 + rng.Float64()*0.10),392 }, coverFamilyStack, false, rng))393 return coverDirectionVertical394 }395396 x := layout.ArtArea.x + layout.ArtArea.w*(0.06+rng.Float64()*0.10)397 y := layout.ArtArea.y + layout.ArtArea.h*(0.14+rng.Float64()*0.16)398 widths := []float64{399 layout.ArtArea.w * (0.18 + rng.Float64()*0.10),400 layout.ArtArea.w * (0.30 + rng.Float64()*0.12),401 layout.ArtArea.w * (0.20 + rng.Float64()*0.08),402 }403 heights := []float64{404 layout.ArtArea.h * (0.28 + rng.Float64()*0.12),405 layout.ArtArea.h * (0.18 + rng.Float64()*0.08),406 layout.ArtArea.h * (0.32 + rng.Float64()*0.10),407 }408 gap := 4.0 + rng.Float64()*4.0409 curX := x410 for i := range widths {411 bounds := rectMM{x: curX, y: y, w: widths[i], h: heights[i]}412 kind := coverShapeRect413 if i == 1 && rng.IntN(2) == 0 {414 kind = coverShapeStepped415 }416 layout.Shapes = append(layout.Shapes, decoratedCoverShape(layout, kind, coverEdgeBottom, bounds, coverFamilyStack, false, rng))417 curX += widths[i] + gap418 }419 layout.Shapes = append(layout.Shapes, decoratedCoverShape(layout, coverShapeCroppedCircle, coverEdgeBottom, rectMM{420 x: x + layout.ArtArea.w*(0.30+rng.Float64()*0.18),421 y: y + layout.ArtArea.h*(0.42+rng.Float64()*0.10),422 w: layout.ArtArea.w * (0.18 + rng.Float64()*0.09),423 h: layout.ArtArea.w * (0.18 + rng.Float64()*0.09),424 }, coverFamilyStack, false, rng))425 return coverDirectionHorizontal426}427428func buildHingeFamily(layout *coverArtLayout, rng coverRand) coverDirection {429 baseX := layout.ArtArea.x + layout.ArtArea.w*(0.06+rng.Float64()*0.08)430 baseY := layout.ArtArea.y + layout.ArtArea.h*(0.42+rng.Float64()*0.12)431 upperX := layout.ArtArea.x + layout.ArtArea.w*(0.48+rng.Float64()*0.10)432 upperY := layout.ArtArea.y + layout.ArtArea.h*(0.08+rng.Float64()*0.10)433434 layout.Shapes = append(layout.Shapes,435 decoratedCoverShape(layout, shapeFamilyKind(rng, coverShapeRect, coverShapeStepped), coverEdgeBottom, rectMM{436 x: baseX,437 y: baseY,438 w: layout.ArtArea.w * (0.30 + rng.Float64()*0.10),439 h: layout.ArtArea.h * (0.26 + rng.Float64()*0.10),440 }, coverFamilyHinge, false, rng),441 )442 layout.Shapes = append(layout.Shapes,443 decoratedCoverShape(layout, shapeFamilyKind(rng, coverShapeRect, coverShapeCroppedCircle), coverEdgeTop, rectMM{444 x: upperX,445 y: upperY,446 w: layout.ArtArea.w * (0.24 + rng.Float64()*0.12),447 h: layout.ArtArea.h * (0.26 + rng.Float64()*0.10),448 }, coverFamilyHinge, false, rng),449 )450 layout.Shapes = append(layout.Shapes,451 decoratedCoverShape(layout, coverShapeRect, coverEdgeRight, rectMM{452 x: layout.ArtArea.x + layout.ArtArea.w*(0.38+rng.Float64()*0.08),453 y: layout.ArtArea.y + layout.ArtArea.h*(0.28+rng.Float64()*0.10),454 w: layout.ArtArea.w * (0.09 + rng.Float64()*0.04),455 h: layout.ArtArea.h * (0.34 + rng.Float64()*0.12),456 }, coverFamilyHinge, true, rng),457 )458 layout.Shapes = append(layout.Shapes,459 decoratedCoverShape(layout, coverShapeCircle, coverEdgeRight, rectMM{460 x: layout.ArtArea.x + layout.ArtArea.w*(0.34+rng.Float64()*0.10),461 y: layout.ArtArea.y + layout.ArtArea.h*(0.22+rng.Float64()*0.12),462 w: layout.ArtArea.w * (0.12 + rng.Float64()*0.08),463 h: layout.ArtArea.w * (0.12 + rng.Float64()*0.08),464 }, coverFamilyHinge, false, rng),465 )466 return coverDirectionDiagonal467}468469func buildIslandFamily(layout *coverArtLayout, rng coverRand) coverDirection {470 clustered := rng.IntN(2) == 0471 layout.Shapes = append(layout.Shapes,472 decoratedCoverShape(layout, shapeFamilyKind(rng, coverShapeStepped, coverShapeCircle), coverEdgeLeft, rectMM{473 x: layout.ArtArea.x + layout.ArtArea.w*(0.06+rng.Float64()*0.14),474 y: layout.ArtArea.y + layout.ArtArea.h*(0.34+rng.Float64()*0.16),475 w: layout.ArtArea.w * (0.30 + rng.Float64()*0.14),476 h: layout.ArtArea.h * (0.24 + rng.Float64()*0.14),477 }, coverFamilyIsland, false, rng),478 )479480 if clustered {481 layout.Shapes = append(layout.Shapes,482 decoratedCoverShape(layout, coverShapeCircle, coverEdgeTop, rectMM{483 x: layout.ArtArea.x + layout.ArtArea.w*(0.40+rng.Float64()*0.14),484 y: layout.ArtArea.y + layout.ArtArea.h*(0.18+rng.Float64()*0.14),485 w: layout.ArtArea.w * (0.14 + rng.Float64()*0.10),486 h: layout.ArtArea.w * (0.14 + rng.Float64()*0.10),487 }, coverFamilyIsland, false, rng),488 decoratedCoverShape(layout, coverShapeRect, coverEdgeBottom, rectMM{489 x: layout.ArtArea.x + layout.ArtArea.w*(0.46+rng.Float64()*0.14),490 y: layout.ArtArea.y + layout.ArtArea.h*(0.48+rng.Float64()*0.12),491 w: layout.ArtArea.w * (0.18 + rng.Float64()*0.10),492 h: layout.ArtArea.h * (0.18 + rng.Float64()*0.10),493 }, coverFamilyIsland, false, rng),494 decoratedCoverShape(layout, coverShapeCroppedCircle, coverEdgeRight, rectMM{495 x: layout.ArtArea.x + layout.ArtArea.w*(0.28+rng.Float64()*0.18),496 y: layout.ArtArea.y + layout.ArtArea.h*(0.06+rng.Float64()*0.10),497 w: layout.ArtArea.w * (0.12 + rng.Float64()*0.08),498 h: layout.ArtArea.w * (0.12 + rng.Float64()*0.08),499 }, coverFamilyIsland, false, rng),500 )501 return coverDirectionClustered502 }503504 layout.Shapes = append(layout.Shapes,505 decoratedCoverShape(layout, coverShapeCircle, coverEdgeTop, rectMM{506 x: layout.ArtArea.x + layout.ArtArea.w*(0.34+rng.Float64()*0.10),507 y: layout.ArtArea.y + layout.ArtArea.h*(0.18+rng.Float64()*0.08),508 w: layout.ArtArea.w * (0.16 + rng.Float64()*0.10),509 h: layout.ArtArea.w * (0.16 + rng.Float64()*0.10),510 }, coverFamilyIsland, false, rng),511 decoratedCoverShape(layout, coverShapeRect, coverEdgeBottom, rectMM{512 x: layout.ArtArea.x + layout.ArtArea.w*(0.56+rng.Float64()*0.10),513 y: layout.ArtArea.y + layout.ArtArea.h*(0.48+rng.Float64()*0.08),514 w: layout.ArtArea.w * (0.18 + rng.Float64()*0.10),515 h: layout.ArtArea.h * (0.20 + rng.Float64()*0.10),516 }, coverFamilyIsland, false, rng),517 )518 return coverDirectionDiagonal519}520521func shapeFamilyKind(rng coverRand, a, b coverShapeKind) coverShapeKind {522 if rng.IntN(2) == 0 {523 return a524 }525 return b526}527528func oppositeEdge(edge coverShapeEdge) coverShapeEdge {529 switch edge {530 case coverEdgeLeft:531 return coverEdgeRight532 case coverEdgeRight:533 return coverEdgeLeft534 case coverEdgeTop:535 return coverEdgeBottom536 default:537 return coverEdgeTop538 }539}540541func decoratedCoverShape(542 layout *coverArtLayout,543 kind coverShapeKind,544 edge coverShapeEdge,545 bounds rectMM,546 family coverCompositionFamily,547 locked bool,548 rng coverRand,549) coverShape {550 shape := coverShape{551 Kind: kind,552 Edge: edge,553 Bounds: bounds,554 Locked: locked,555 Fill: pickCoverFillMode(family, rng),556 Texture: pickCoverTexture(family, rng),557 Step: 4.0 + rng.Float64()*3.8,558 Weight: 0.7 + rng.Float64()*0.8,559 }560 shape.Rotate = pickCoverRotationDeg(locked, rng)561 shape = sanitizeCoverShape(shape, layout.ArtArea, layout.LockupExclusion)562 shape.Cutouts = buildFamilyCutouts(family, shape, rng)563 return shape564}565566func pickCoverRotationDeg(locked bool, rng coverRand) float64 {567 minDeg := 8.0568 maxDeg := 20.0569 if locked {570 minDeg = 4.0571 maxDeg = 10.0572 }573 angle := minDeg + rng.Float64()*(maxDeg-minDeg)574 if rng.IntN(2) == 0 {575 return -angle576 }577 return angle578}579580func sanitizeCoverShape(shape coverShape, artArea, exclusion rectMM) coverShape {581 if shape.Bounds.w < 8 {582 shape.Bounds.w = 8583 }584 if shape.Bounds.h < 8 {585 shape.Bounds.h = 8586 }587 if shape.Bounds.w > artArea.w {588 shape.Bounds.w = artArea.w589 }590 if shape.Bounds.h > artArea.h {591 shape.Bounds.h = artArea.h592 }593594 if shape.Bounds.x < artArea.x {595 shape.Bounds.x = artArea.x596 }597 if shape.Bounds.y < artArea.y {598 shape.Bounds.y = artArea.y599 }600 if shape.Bounds.x+shape.Bounds.w > artArea.x+artArea.w {601 shape.Bounds.x = artArea.x + artArea.w - shape.Bounds.w602 }603 if shape.Bounds.y+shape.Bounds.h > artArea.y+artArea.h {604 shape.Bounds.y = artArea.y + artArea.h - shape.Bounds.h605 }606607 shape = withCoverShapePivot(shape)608609 for range 4 {610 env := coverShapeEnvelope(shape)611 dx := 0.0612 dy := 0.0613614 if env.x < artArea.x {615 dx += artArea.x - env.x616 }617 if env.x+env.w > artArea.x+artArea.w {618 dx -= env.x + env.w - (artArea.x + artArea.w)619 }620 if env.y < artArea.y {621 dy += artArea.y - env.y622 }623 if env.y+env.h > artArea.y+artArea.h {624 dy -= env.y + env.h - (artArea.y + artArea.h)625 }626627 if dx != 0 || dy != 0 {628 shape.Bounds.x += dx629 shape.Bounds.y += dy630 shape = withCoverShapePivot(shape)631 continue632 }633634 if rectsIntersect(env, exclusion) {635 clearBelow := exclusion.y + exclusion.h + 5.0636 if shape.Bounds.y < clearBelow {637 shape.Bounds.y += clearBelow - shape.Bounds.y638 }639 shape = withCoverShapePivot(shape)640 env = coverShapeEnvelope(shape)641 if rectsIntersect(env, exclusion) {642 targetRight := exclusion.x - 5.0643 shape.Bounds.x -= env.x + env.w - targetRight644 shape = withCoverShapePivot(shape)645 }646 continue647 }648649 break650 }651652 return shape653}654655func withCoverShapePivot(shape coverShape) coverShape {656 shape.PivotX = shape.Bounds.x + shape.Bounds.w*0.5657 shape.PivotY = shape.Bounds.y + shape.Bounds.h*0.5658 return shape659}660661func pickCoverFillMode(family coverCompositionFamily, rng coverRand) coverFillMode {662 switch family {663 case coverFamilyFrame:664 return []coverFillMode{coverFillSolid, coverFillSlits, coverFillOpenGrid, coverFillBanded}[rng.IntN(4)]665 case coverFamilyStack:666 return []coverFillMode{coverFillSolid, coverFillBanded, coverFillSlits, coverFillOpenGrid}[rng.IntN(4)]667 case coverFamilyHinge:668 return []coverFillMode{coverFillSolid, coverFillBanded, coverFillPunctured, coverFillSlits}[rng.IntN(4)]669 default:670 return []coverFillMode{coverFillPunctured, coverFillOpenGrid, coverFillSolid, coverFillBanded}[rng.IntN(4)]671 }672}673674func pickCoverTexture(family coverCompositionFamily, rng coverRand) coverTextureKind {675 switch family {676 case coverFamilyFrame:677 return []coverTextureKind{coverTextureNone, coverTextureStripes, coverTextureChecker, coverTextureDots, coverTextureLattice}[rng.IntN(5)]678 case coverFamilyStack:679 return []coverTextureKind{coverTextureNone, coverTextureStripes, coverTextureHatch, coverTextureChecker, coverTextureLattice}[rng.IntN(5)]680 case coverFamilyHinge:681 return []coverTextureKind{coverTextureNone, coverTextureHatch, coverTextureStripes, coverTextureDots, coverTextureLattice}[rng.IntN(5)]682 default:683 return []coverTextureKind{coverTextureNone, coverTextureDots, coverTextureChecker, coverTextureHatch, coverTextureStripes}[rng.IntN(5)]684 }685}686687func buildFamilyCutouts(family coverCompositionFamily, shape coverShape, rng coverRand) []coverCutout {688 switch family {689 case coverFamilyFrame:690 return buildFrameCutouts(shape, rng)691 case coverFamilyStack:692 return buildStackCutouts(shape, rng)693 case coverFamilyHinge:694 return buildHingeCutouts(shape, rng)695 default:696 return buildIslandCutouts(shape, rng)697 }698}699700func buildFrameCutouts(shape coverShape, rng coverRand) []coverCutout {701 cutouts := []coverCutout{702 {703 Kind: coverCutoutRect,704 X: shape.Bounds.x + shape.Bounds.w*(0.14+rng.Float64()*0.32),705 Y: shape.Bounds.y + shape.Bounds.h*(0.18+rng.Float64()*0.40),706 W: shape.Bounds.w * (0.12 + rng.Float64()*0.18),707 H: shape.Bounds.h * (0.12 + rng.Float64()*0.22),708 },709 }710 if rng.IntN(2) == 0 {711 cutouts = append(cutouts, coverCutout{712 Kind: coverCutoutCircle,713 X: shape.Bounds.x + shape.Bounds.w*(0.26+rng.Float64()*0.48),714 Y: shape.Bounds.y + shape.Bounds.h*(0.22+rng.Float64()*0.48),715 R: min(shape.Bounds.w, shape.Bounds.h) * (0.08 + rng.Float64()*0.10),716 })717 }718 return cutouts719}720721func buildStackCutouts(shape coverShape, rng coverRand) []coverCutout {722 if rng.IntN(2) == 0 {723 return []coverCutout{724 {725 Kind: coverCutoutRect,726 X: shape.Bounds.x + shape.Bounds.w*(0.10+rng.Float64()*0.12),727 Y: shape.Bounds.y + shape.Bounds.h*(0.36+rng.Float64()*0.18),728 W: shape.Bounds.w * (0.42 + rng.Float64()*0.22),729 H: shape.Bounds.h * (0.10 + rng.Float64()*0.10),730 },731 }732 }733 return []coverCutout{734 {735 Kind: coverCutoutRect,736 X: shape.Bounds.x + shape.Bounds.w*(0.36+rng.Float64()*0.18),737 Y: shape.Bounds.y + shape.Bounds.h*(0.08+rng.Float64()*0.12),738 W: shape.Bounds.w * (0.12 + rng.Float64()*0.12),739 H: shape.Bounds.h * (0.34 + rng.Float64()*0.20),740 },741 }742}743744func buildHingeCutouts(shape coverShape, rng coverRand) []coverCutout {745 cutouts := []coverCutout{746 {747 Kind: coverCutoutCircle,748 X: shape.Bounds.x + shape.Bounds.w*(0.34+rng.Float64()*0.24),749 Y: shape.Bounds.y + shape.Bounds.h*(0.30+rng.Float64()*0.26),750 R: min(shape.Bounds.w, shape.Bounds.h) * (0.08 + rng.Float64()*0.08),751 },752 }753 if rng.IntN(2) == 0 {754 cutouts = append(cutouts, coverCutout{755 Kind: coverCutoutRect,756 X: shape.Bounds.x + shape.Bounds.w*(0.58+rng.Float64()*0.10),757 Y: shape.Bounds.y + shape.Bounds.h*(0.56+rng.Float64()*0.10),758 W: shape.Bounds.w * (0.18 + rng.Float64()*0.12),759 H: shape.Bounds.h * (0.14 + rng.Float64()*0.10),760 })761 }762 return cutouts763}764765func buildIslandCutouts(shape coverShape, rng coverRand) []coverCutout {766 return []coverCutout{767 {768 Kind: coverCutoutCircle,769 X: shape.Bounds.x + shape.Bounds.w*(0.18+rng.Float64()*0.54),770 Y: shape.Bounds.y + shape.Bounds.h*(0.18+rng.Float64()*0.54),771 R: min(shape.Bounds.w, shape.Bounds.h) * (0.10 + rng.Float64()*0.10),772 },773 }774}775776func drawCoverArtLayout(pdf *fpdf.Fpdf, layout coverArtLayout) {777 for _, shape := range layout.Shapes {778 drawCoverShape(pdf, shape)779 }780}781782func drawCoverShape(pdf *fpdf.Fpdf, shape coverShape) {783 pdf.TransformBegin()784 pdf.TransformRotate(shape.Rotate, shape.PivotX, shape.PivotY)785 switch shape.Kind {786 case coverShapeCircle:787 drawCircleCoverShape(pdf, shape)788 case coverShapeCroppedCircle:789 drawCroppedCircleCoverShape(pdf, shape)790 case coverShapeStepped:791 drawSteppedCoverShape(pdf, shape)792 default:793 drawRectCoverShape(pdf, shape)794 }795 pdf.TransformEnd()796}797798func drawRectCoverShape(pdf *fpdf.Fpdf, shape coverShape) {799 pdf.SetFillColor(8, 8, 8)800 pdf.Rect(shape.Bounds.x, shape.Bounds.y, shape.Bounds.w, shape.Bounds.h, "F")801 pdf.ClipRect(shape.Bounds.x, shape.Bounds.y, shape.Bounds.w, shape.Bounds.h, false)802 drawCoverFill(pdf, shape)803 drawCoverTexture(pdf, shape)804 pdf.ClipEnd()805 drawCoverCutouts(pdf, shape.Cutouts)806}807808func drawCircleCoverShape(pdf *fpdf.Fpdf, shape coverShape) {809 cx, cy, r := coverCircleMetrics(shape.Bounds)810 pdf.SetFillColor(8, 8, 8)811 pdf.Circle(cx, cy, r, "F")812 pdf.ClipCircle(cx, cy, r, false)813 drawCoverFill(pdf, shape)814 drawCoverTexture(pdf, shape)815 pdf.ClipEnd()816 drawCoverCutouts(pdf, shape.Cutouts)817}818819func drawCroppedCircleCoverShape(pdf *fpdf.Fpdf, shape coverShape) {820 cx, cy, r := croppedCircleMetrics(shape)821 pdf.SetFillColor(8, 8, 8)822 pdf.ClipRect(shape.Bounds.x, shape.Bounds.y, shape.Bounds.w, shape.Bounds.h, false)823 pdf.Circle(cx, cy, r, "F")824 pdf.ClipCircle(cx, cy, r, false)825 drawCoverFill(pdf, shape)826 drawCoverTexture(pdf, shape)827 pdf.ClipEnd()828 pdf.ClipEnd()829 drawCoverCutouts(pdf, shape.Cutouts)830}831832func drawSteppedCoverShape(pdf *fpdf.Fpdf, shape coverShape) {833 points := steppedShapePoints(shape)834 pdf.SetFillColor(8, 8, 8)835 pdf.Polygon(points, "F")836 pdf.ClipPolygon(points, false)837 drawCoverFill(pdf, shape)838 drawCoverTexture(pdf, shape)839 pdf.ClipEnd()840 drawCoverCutouts(pdf, shape.Cutouts)841}842843func coverCircleMetrics(bounds rectMM) (cx, cy, r float64) {844 r = min(bounds.w, bounds.h) * 0.5845 cx = bounds.x + bounds.w*0.5846 cy = bounds.y + bounds.h*0.5847 return cx, cy, r848}849850func croppedCircleMetrics(shape coverShape) (cx, cy, r float64) {851 cx, cy, _ = coverCircleMetrics(shape.Bounds)852 r = max(shape.Bounds.w, shape.Bounds.h) * 0.56853 switch shape.Edge {854 case coverEdgeLeft:855 cx -= shape.Bounds.w * 0.18856 case coverEdgeRight:857 cx += shape.Bounds.w * 0.18858 case coverEdgeTop:859 cy -= shape.Bounds.h * 0.18860 default:861 cy += shape.Bounds.h * 0.18862 }863 return cx, cy, r864}865866func steppedShapePoints(shape coverShape) []fpdf.PointType {867 b := shape.Bounds868 notch := min(b.w, b.h) * 0.18869 step1 := min(b.w, b.h) * 0.10870 step2 := min(b.w, b.h) * 0.20871872 switch shape.Edge {873 case coverEdgeLeft:874 return []fpdf.PointType{875 {X: b.x + notch, Y: b.y},876 {X: b.x + b.w, Y: b.y},877 {X: b.x + b.w, Y: b.y + b.h},878 {X: b.x + notch, Y: b.y + b.h},879 {X: b.x + notch, Y: b.y + b.h*0.66},880 {X: b.x, Y: b.y + b.h*0.66 - step2},881 {X: b.x, Y: b.y + b.h*0.34 + step2},882 {X: b.x + notch, Y: b.y + b.h*0.34},883 }884 case coverEdgeRight:885 return []fpdf.PointType{886 {X: b.x, Y: b.y},887 {X: b.x + b.w - notch, Y: b.y},888 {X: b.x + b.w - notch, Y: b.y + b.h*0.34},889 {X: b.x + b.w, Y: b.y + b.h*0.34 + step1},890 {X: b.x + b.w, Y: b.y + b.h*0.66 - step1},891 {X: b.x + b.w - notch, Y: b.y + b.h*0.66},892 {X: b.x + b.w - notch, Y: b.y + b.h},893 {X: b.x, Y: b.y + b.h},894 }895 case coverEdgeTop:896 return []fpdf.PointType{897 {X: b.x, Y: b.y + notch},898 {X: b.x + b.w*0.34, Y: b.y + notch},899 {X: b.x + b.w*0.34 + step1, Y: b.y},900 {X: b.x + b.w*0.66 - step1, Y: b.y},901 {X: b.x + b.w*0.66, Y: b.y + notch},902 {X: b.x + b.w, Y: b.y + notch},903 {X: b.x + b.w, Y: b.y + b.h},904 {X: b.x, Y: b.y + b.h},905 }906 default:907 return []fpdf.PointType{908 {X: b.x, Y: b.y},909 {X: b.x + b.w, Y: b.y},910 {X: b.x + b.w, Y: b.y + b.h - notch},911 {X: b.x + b.w*0.66, Y: b.y + b.h - notch},912 {X: b.x + b.w*0.66 - step1, Y: b.y + b.h},913 {X: b.x + b.w*0.34 + step1, Y: b.y + b.h},914 {X: b.x + b.w*0.34, Y: b.y + b.h - notch},915 {X: b.x, Y: b.y + b.h - notch},916 }917 }918}919920func drawCoverFill(pdf *fpdf.Fpdf, shape coverShape) {921 pdf.SetFillColor(255, 255, 255)922923 switch shape.Fill {924 case coverFillSlits:925 drawCoverSlits(pdf, shape.Bounds)926 case coverFillOpenGrid:927 drawCoverOpenGrid(pdf, shape.Bounds)928 case coverFillPunctured:929 drawCoverPunctures(pdf, shape.Bounds)930 case coverFillBanded:931 drawCoverBands(pdf, shape.Bounds)932 default:933 return934 }935}936937func drawCoverSlits(pdf *fpdf.Fpdf, bounds rectMM) {938 if bounds.w >= bounds.h {939 slitW := max(1.6, bounds.w*0.05)940 for x := bounds.x + bounds.w*0.12; x < bounds.x+bounds.w*0.88; x += slitW * 1.9 {941 pdf.Rect(x, bounds.y-1, slitW, bounds.h+2, "F")942 }943 return944 }945946 slitH := max(1.6, bounds.h*0.05)947 for y := bounds.y + bounds.h*0.12; y < bounds.y+bounds.h*0.88; y += slitH * 1.9 {948 pdf.Rect(bounds.x-1, y, bounds.w+2, slitH, "F")949 }950}951952func drawCoverOpenGrid(pdf *fpdf.Fpdf, bounds rectMM) {953 cell := max(3.2, min(bounds.w, bounds.h)*0.12)954 for row, y := 0, bounds.y; y < bounds.y+bounds.h; row, y = row+1, y+cell {955 for col, x := 0, bounds.x; x < bounds.x+bounds.w; col, x = col+1, x+cell {956 if (row+col)%2 == 0 {957 pdf.Rect(x, y, cell*0.78, cell*0.78, "F")958 }959 }960 }961}962963func drawCoverPunctures(pdf *fpdf.Fpdf, bounds rectMM) {964 step := max(4.4, min(bounds.w, bounds.h)*0.16)965 radius := max(1.0, step*0.18)966 for row, y := 0, bounds.y+radius; y < bounds.y+bounds.h-radius; row, y = row+1, y+step {967 shift := 0.0968 if row%2 == 1 {969 shift = step * 0.45970 }971 for x := bounds.x + radius + shift; x < bounds.x+bounds.w-radius; x += step {972 pdf.Circle(x, y, radius, "F")973 }974 }975}976977func drawCoverBands(pdf *fpdf.Fpdf, bounds rectMM) {978 if bounds.w >= bounds.h {979 bandH := max(3.4, bounds.h*0.16)980 for i := 0; i < 2; i++ {981 y := bounds.y + bounds.h*(0.18+float64(i)*0.34)982 pdf.Rect(bounds.x-1, y, bounds.w+2, bandH, "F")983 }984 return985 }986987 bandW := max(3.4, bounds.w*0.16)988 for i := 0; i < 2; i++ {989 x := bounds.x + bounds.w*(0.18+float64(i)*0.34)990 pdf.Rect(x, bounds.y-1, bandW, bounds.h+2, "F")991 }992}993994func drawCoverTexture(pdf *fpdf.Fpdf, shape coverShape) {995 setCoverRemovalStroke(pdf, shape.Weight)996997 switch shape.Texture {998 case coverTextureStripes:999 barW := max(1.2, shape.Step*0.34)1000 for x := shape.Bounds.x - shape.Step; x <= shape.Bounds.x+shape.Bounds.w+shape.Step; x += shape.Step {1001 pdf.SetFillColor(255, 255, 255)1002 pdf.Rect(x, shape.Bounds.y-1, barW, shape.Bounds.h+2, "F")1003 }1004 case coverTextureHatch:1005 for offset := -shape.Bounds.h; offset <= shape.Bounds.w+shape.Bounds.h; offset += shape.Step {1006 pdf.Line(shape.Bounds.x+offset, shape.Bounds.y+shape.Bounds.h, shape.Bounds.x+offset+shape.Bounds.h, shape.Bounds.y)1007 }1008 case coverTextureDots:1009 radius := max(0.9, shape.Step*0.16)1010 rowStep := shape.Step * 0.941011 for row, y := 0, shape.Bounds.y+radius; y <= shape.Bounds.y+shape.Bounds.h-radius; row, y = row+1, y+rowStep {1012 shift := 0.01013 if row%2 == 1 {1014 shift = shape.Step * 0.421015 }1016 for x := shape.Bounds.x + radius + shift; x <= shape.Bounds.x+shape.Bounds.w-radius; x += shape.Step {1017 pdf.SetFillColor(255, 255, 255)1018 pdf.Circle(x, y, radius, "F")1019 }1020 }1021 case coverTextureChecker:1022 cell := max(2.6, shape.Step*0.68)1023 for row, y := 0, shape.Bounds.y; y <= shape.Bounds.y+shape.Bounds.h; row, y = row+1, y+cell {1024 for col, x := 0, shape.Bounds.x; x <= shape.Bounds.x+shape.Bounds.w; col, x = col+1, x+cell {1025 if (row+col)%2 == 0 {1026 pdf.SetFillColor(255, 255, 255)1027 pdf.Rect(x, y, cell, cell, "F")1028 }1029 }1030 }1031 case coverTextureLattice:1032 barW := max(0.95, shape.Step*0.16)1033 for x := shape.Bounds.x; x <= shape.Bounds.x+shape.Bounds.w; x += shape.Step {1034 pdf.SetFillColor(255, 255, 255)1035 pdf.Rect(x, shape.Bounds.y-1, barW, shape.Bounds.h+2, "F")1036 }1037 for y := shape.Bounds.y; y <= shape.Bounds.y+shape.Bounds.h; y += shape.Step * 0.84 {1038 pdf.SetFillColor(255, 255, 255)1039 pdf.Rect(shape.Bounds.x-1, y, shape.Bounds.w+2, barW, "F")1040 }1041 default:1042 return1043 }1044}10451046func setCoverRemovalStroke(pdf *fpdf.Fpdf, weight float64) {1047 pdf.SetDrawColor(255, 255, 255)1048 pdf.SetLineWidth(weight)1049}10501051func drawCoverCutouts(pdf *fpdf.Fpdf, cutouts []coverCutout) {1052 pdf.SetFillColor(255, 255, 255)1053 for _, cutout := range cutouts {1054 switch cutout.Kind {1055 case coverCutoutCircle:1056 pdf.Circle(cutout.X, cutout.Y, cutout.R, "F")1057 default:1058 pdf.Rect(cutout.X, cutout.Y, cutout.W, cutout.H, "F")1059 }1060 }1061}10621063func rectsIntersect(a, b rectMM) bool {1064 return a.x < b.x+b.w && a.x+a.w > b.x && a.y < b.y+b.h && a.y+a.h > b.y1065}10661067func rotatePoint(x, y, pivotX, pivotY, angleDeg float64) (float64, float64) {1068 rad := angleDeg * math.Pi / 180.01069 sinA, cosA := math.Sincos(rad)1070 dx := x - pivotX1071 dy := y - pivotY1072 return pivotX + dx*cosA - dy*sinA, pivotY + dx*sinA + dy*cosA1073}10741075func rotatedRectAABB(bounds rectMM, pivotX, pivotY, angleDeg float64) rectMM {1076 if angleDeg == 0 {1077 return bounds1078 }1079 points := [4][2]float64{1080 {bounds.x, bounds.y},1081 {bounds.x + bounds.w, bounds.y},1082 {bounds.x + bounds.w, bounds.y + bounds.h},1083 {bounds.x, bounds.y + bounds.h},1084 }1085 minX := math.MaxFloat641086 minY := math.MaxFloat641087 maxX := -math.MaxFloat641088 maxY := -math.MaxFloat641089 for _, pt := range points {1090 x, y := rotatePoint(pt[0], pt[1], pivotX, pivotY, angleDeg)1091 minX = min(minX, x)1092 minY = min(minY, y)1093 maxX = max(maxX, x)1094 maxY = max(maxY, y)1095 }1096 return rectMM{x: minX, y: minY, w: maxX - minX, h: maxY - minY}1097}10981099func coverShapeEnvelope(shape coverShape) rectMM {1100 return rotatedRectAABB(shape.Bounds, shape.PivotX, shape.PivotY, shape.Rotate)1101}11021103func coverArtLayoutEqual(a, b coverArtLayout) bool {1104 return reflect.DeepEqual(a, b)1105}11061107func coverArtStaysWithinArea(layout coverArtLayout) bool {1108 for _, shape := range layout.Shapes {1109 env := coverShapeEnvelope(shape)1110 if env.x < layout.ArtArea.x || env.y < layout.ArtArea.y {1111 return false1112 }1113 if env.x+env.w > layout.ArtArea.x+layout.ArtArea.w {1114 return false1115 }1116 if env.y+env.h > layout.ArtArea.y+layout.ArtArea.h {1117 return false1118 }1119 }1120 return true1121}11221123func coverArtRespectsLockup(layout coverArtLayout) bool {1124 for _, shape := range layout.Shapes {1125 if rectsIntersect(coverShapeEnvelope(shape), layout.LockupExclusion) {1126 return false1127 }1128 }1129 return true1130}11311132func coverArtDiffers(a, b coverArtLayout) bool {1133 return !reflect.DeepEqual(a, b)1134}11351136func coverFamilyName(family coverCompositionFamily) string {1137 switch family {1138 case coverFamilyFrame:1139 return "frame"1140 case coverFamilyStack:1141 return "stack"1142 case coverFamilyHinge:1143 return "hinge"1144 default:1145 return "island"1146 }1147}11481149func coverDirectionName(direction coverDirection) string {1150 switch direction {1151 case coverDirectionVertical:1152 return "vertical"1153 case coverDirectionHorizontal:1154 return "horizontal"1155 case coverDirectionDiagonal:1156 return "diagonal"1157 default:1158 return "clustered"1159 }1160}11611162func coverShapeKindName(kind coverShapeKind) string {1163 switch kind {1164 case coverShapeRect:1165 return "rect"1166 case coverShapeCircle:1167 return "circle"1168 case coverShapeCroppedCircle:1169 return "cropped-circle"1170 default:1171 return "stepped"1172 }1173}11741175func coverTextureCount(layout coverArtLayout) int {1176 seen := map[coverTextureKind]struct{}{}1177 for _, shape := range layout.Shapes {1178 if shape.Texture == coverTextureNone {1179 continue1180 }1181 seen[shape.Texture] = struct{}{}1182 }1183 return len(seen)1184}11851186func coverPrimitiveMixSignature(layout coverArtLayout) string {1187 parts := map[string]struct{}{}1188 for _, shape := range layout.Shapes {1189 parts[coverShapeKindName(shape.Kind)] = struct{}{}1190 }11911192 keys := make([]string, 0, len(parts))1193 for key := range parts {1194 keys = append(keys, key)1195 }1196 sort.Strings(keys)1197 return strings.Join(keys, "+")1198}11991200func coverRotationSignature(layout coverArtLayout) string {1201 parts := make([]string, 0, len(layout.Shapes))1202 for _, shape := range layout.Shapes {1203 parts = append(parts, fmt.Sprintf("%.1f", shape.Rotate))1204 }1205 sort.Strings(parts)1206 return strings.Join(parts, ",")1207}12081209func coverRotationPolaritySignature(layout coverArtLayout) string {1210 hasPos := false1211 hasNeg := false1212 for _, shape := range layout.Shapes {1213 if shape.Rotate > 0 {1214 hasPos = true1215 }1216 if shape.Rotate < 0 {1217 hasNeg = true1218 }1219 }1220 switch {1221 case hasPos && hasNeg:1222 return "mixed"1223 case hasPos:1224 return "positive"1225 case hasNeg:1226 return "negative"1227 default:1228 return "flat"1229 }1230}12311232func coverArtInkBounds(layout coverArtLayout) rectMM {1233 if len(layout.Shapes) == 0 {1234 return rectMM{}1235 }12361237 minX := math.MaxFloat641238 minY := math.MaxFloat641239 maxX := 0.01240 maxY := 0.01241 for _, shape := range layout.Shapes {1242 env := coverShapeEnvelope(shape)1243 minX = min(minX, env.x)1244 minY = min(minY, env.y)1245 maxX = max(maxX, env.x+env.w)1246 maxY = max(maxY, env.y+env.h)1247 }1248 return rectMM{x: minX, y: minY, w: maxX - minX, h: maxY - minY}1249}12501251func coverInkAspectBucket(layout coverArtLayout) coverAspectBucket {1252 bounds := coverArtInkBounds(layout)1253 if bounds.w <= 0 || bounds.h <= 0 {1254 return coverAspectCompact1255 }12561257 if bounds.h/bounds.w >= 1.28 {1258 return coverAspectTall1259 }1260 if bounds.w/bounds.h >= 1.28 {1261 return coverAspectWide1262 }12631264 centerX := bounds.x + bounds.w*0.51265 centerY := bounds.y + bounds.h*0.51266 artCenterX := layout.ArtArea.x + layout.ArtArea.w*0.51267 artCenterY := layout.ArtArea.y + layout.ArtArea.h*0.51268 offset := math.Hypot(centerX-artCenterX, centerY-artCenterY)1269 if offset > min(layout.ArtArea.w, layout.ArtArea.h)*0.12 {1270 return coverAspectOffset1271 }1272 return coverAspectCompact1273}12741275func coverAspectBucketName(bucket coverAspectBucket) string {1276 switch bucket {1277 case coverAspectTall:1278 return "tall"1279 case coverAspectWide:1280 return "wide"1281 case coverAspectOffset:1282 return "offset"1283 default:1284 return "compact"1285 }1286}