- commit
- d2b869533fbea5777c4120c444523fcdd9a3ba31
- parent
- 1cf1738c0f610104fcf382767dd84d15d4420777
- author
- Dami <dami.etoile@gmail.com>
| M | cmd/ | 3 | + |
| M | cmd/ | 6 | + |
| D | nonogram-pack-01.md | 776 | ---------------------------- |
| A | pdfexport/ | 841 | ++++++++++++++++++++++++++++++ |
| A | pdfexport/ | 89 | +++ |
| M | pdfexport/ | 14 | +- |
| M | pdfexport/ | 98 | +++ |
| M | pdfexport/ | 130 | +++++ |
| M | pdfexport/ | 51 | ++ |
| M | pdfexport/ | 123 | +++-- |
| M | pdfexport/ | 212 | +------- |
| M | pdfexport/ | 1 | + |
@@ -17,6 +17,7 @@ import (
1717 var (1818 flagPDFOutput string1919 flagPDFTitle string20+ flagPDFHeader string2021 flagPDFVolume int2122 flagPDFAdvert string2223 flagPDFShuffleSeed string@@ -34,6 +35,7 @@ var exportPDFCmd = &cobra.Command{
3435 func init() {3536 exportPDFCmd.Flags().StringVarP(&flagPDFOutput, "output", "o", "", "write output PDF path (defaults to <first-input>-print.pdf)")3637 exportPDFCmd.Flags().StringVar(&flagPDFTitle, "title", "", "subtitle shown on the cover")38+ exportPDFCmd.Flags().StringVar(&flagPDFHeader, "header", "", "optional intro paragraph shown on the title page under 'PuzzleTea Puzzle Pack'")3739 exportPDFCmd.Flags().IntVar(&flagPDFVolume, "volume", 1, "volume number shown on the cover (must be >= 1)")3840 exportPDFCmd.Flags().StringVar(&flagPDFAdvert, "advert", "Find more puzzles at github.com/FelineStateMachine/puzzletea", "advert text shown on the title page")3941 exportPDFCmd.Flags().StringVar(&flagPDFShuffleSeed, "shuffle-seed", "", "seed for deterministic within-band difficulty mixing")@@ -123,6 +125,7 @@ func buildRenderConfigForPDF(docs []pdfexport.PackDocument, shuffleSeed string,
123125 124126 cfg := pdfexport.RenderConfig{125127 CoverSubtitle: subtitle,128+ HeaderText: strings.TrimSpace(flagPDFHeader),126129 VolumeNumber: flagPDFVolume,127130 AdvertText: flagPDFAdvert,128131 GeneratedAt: now,@@ -53,6 +53,7 @@ func TestBuildRenderConfigForPDFUsesTitleAsCoverSubtitle(t *testing.T) {
5353 defer reset()5454 5555 flagPDFTitle = "Catacombs & Pines"56+ flagPDFHeader = "Custom heading paragraph"5657 flagPDFVolume = 75758 flagPDFAdvert = "Custom advert"5859 flagPDFCoverColor = ""@@ -69,6 +70,9 @@ func TestBuildRenderConfigForPDFUsesTitleAsCoverSubtitle(t *testing.T) {
6970 if cfg.VolumeNumber != 7 {7071 t.Fatalf("VolumeNumber = %d, want %d", cfg.VolumeNumber, 7)7172 }73+ if cfg.HeaderText != "Custom heading paragraph" {74+ t.Fatalf("HeaderText = %q, want %q", cfg.HeaderText, "Custom heading paragraph")75+ }7276 if cfg.AdvertText != "Custom advert" {7377 t.Fatalf("AdvertText = %q, want %q", cfg.AdvertText, "Custom advert")7478 }@@ -98,6 +102,7 @@ func TestBuildRenderConfigForPDFDefaultsSubtitleFromDocs(t *testing.T) {
98102 99103 func snapshotExportPDFFlags() func() {100104 oldTitle := flagPDFTitle105+ oldHeader := flagPDFHeader101106 oldVolume := flagPDFVolume102107 oldAdvert := flagPDFAdvert103108 oldCoverColor := flagPDFCoverColor@@ -106,6 +111,7 @@ func snapshotExportPDFFlags() func() {
106111 107112 return func() {108113 flagPDFTitle = oldTitle114+ flagPDFHeader = oldHeader109115 flagPDFVolume = oldVolume110116 flagPDFAdvert = oldAdvert111117 flagPDFCoverColor = oldCoverColor@@ -1,776 +0,0 @@
1-# PuzzleTea Export2-3-- Generated: 2026-02-21T20:42:05-07:004-- Version: v1.6.0-1-gd260f2e-dirty5-- Category: Nonogram6-- Mode Selection: Standard7-- Count: 318-- Seed: meow9-10-## ember-newt - 111-12-### Puzzle Grid with Integrated Hints13-14-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |15-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |16-| . | . | . | . | 6 | 4 | 2 | 1 | . | . | . | 1 | 2 | . |17-| . | . | . | . | 1 | 1 | 4 | 5 | 2 | 5 | 5 | 2 | 5 | 2 |18-| . | . | . | . | 1 | 1 | 1 | 2 | 4 | 2 | 3 | 3 | 1 | 7 |19-| 1 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |20-| . | . | 3 | 6 | . | . | . | . | . | . | . | . | . | . |21-| . | . | . | 7 | . | . | . | . | . | . | . | . | . | . |22-| . | 2 | 1 | 5 | . | . | . | . | . | . | . | . | . | . |23-| . | . | 4 | 5 | . | . | . | . | . | . | . | . | . | . |24-| . | 1 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |25-| . | 3 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |26-| . | . | 3 | 6 | . | . | . | . | . | . | . | . | . | . |27-| . | . | 5 | 1 | . | . | . | . | . | . | . | . | . | . |28-| . | . | 4 | 3 | . | . | . | . | . | . | . | . | . | . |29-30-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.31-32----33-34-## ember-moss - 235-36-### Puzzle Grid with Integrated Hints37-38-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |39-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |40-| . | . | . | . | . | 1 | . | . | . | . | 2 | . | . | 2 |41-| . | . | . | . | . | 4 | . | . | . | . | 1 | . | . | 2 |42-| . | . | . | . | 1 | 1 | 3 | 6 | 3 | 3 | 1 | 2 | 1 | 1 |43-| . | . | . | . | 5 | 1 | 2 | 3 | 3 | 6 | 2 | 5 | 8 | 1 |44-| . | 1 | 1 | 5 | . | . | . | . | . | . | . | . | . | . |45-| . | 1 | 6 | 1 | . | . | . | . | . | . | . | . | . | . |46-| . | . | 5 | 1 | . | . | . | . | . | . | . | . | . | . |47-| . | . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |48-| 2 | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |49-| . | . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |50-| . | 1 | 1 | 5 | . | . | . | . | . | . | . | . | . | . |51-| . | 2 | 3 | 3 | . | . | . | . | . | . | . | . | . | . |52-| . | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |53-| 1 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |54-55-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.56-57----58-59-## stone-viper - 360-61-### Puzzle Grid with Integrated Hints62-63-| R1 | R2 | R3 | R4 | R5 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |64-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |65-| . | . | . | . | . | 2 | 2 | . | . | 4 | . | 1 | 1 | 2 | 2 |66-| . | . | . | . | . | 3 | 1 | 3 | 1 | 1 | 2 | 3 | 1 | 1 | 1 |67-| . | . | . | . | . | 2 | 2 | 5 | 3 | 2 | 3 | 3 | 1 | 4 | 2 |68-| . | . | . | 1 | 1 | . | . | . | . | . | . | . | . | . | . |69-| 1 | 1 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |70-| . | . | . | 6 | 3 | . | . | . | . | . | . | . | . | . | . |71-| . | . | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |72-| . | . | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |73-| . | 1 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |74-| . | . | . | 6 | 1 | . | . | . | . | . | . | . | . | . | . |75-| . | . | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |76-| . | . | 3 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |77-| . | 3 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |78-79-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.80-81----82-83-## lonely-vale - 484-85-### Puzzle Grid with Integrated Hints86-87-| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |88-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |89-| . | . | . | . | . | . | 1 | . | . | . | . | . | . |90-| . | . | . | . | . | . | 4 | . | . | 3 | 2 | . | . |91-| . | . | . | 6 | 3 | 2 | 1 | . | 5 | 3 | 3 | 5 | 6 |92-| . | . | . | 3 | 6 | 4 | 1 | 9 | 3 | 1 | 1 | 1 | 2 |93-| . | 7 | 1 | . | . | . | . | . | . | . | . | . | . |94-| 3 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |95-| . | 2 | 7 | . | . | . | . | . | . | . | . | . | . |96-| 1 | 4 | 2 | . | . | . | . | . | . | . | . | . | . |97-| . | 6 | 3 | . | . | . | . | . | . | . | . | . | . |98-| . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |99-| . | 2 | 5 | . | . | . | . | . | . | . | . | . | . |100-| 2 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |101-| 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |102-| 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |103-104-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.105-106----107-108-## arctic-basalt - 5109-110-### Puzzle Grid with Integrated Hints111-112-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |113-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |114-| . | . | . | . | . | 1 | . | . | . | . | . | . | . | . |115-| . | . | . | . | . | 1 | . | 1 | . | . | . | . | . | . |116-| . | . | . | . | 6 | 1 | 5 | 1 | 8 | 2 | 4 | 2 | 3 | 4 |117-| . | . | . | . | 1 | 4 | 3 | 2 | 1 | 3 | 4 | 5 | 2 | 2 |118-| 3 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |119-| 1 | 3 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |120-| 3 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |121-| 1 | 1 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |122-| . | . | 6 | 1 | . | . | . | . | . | . | . | . | . | . |123-| . | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |124-| . | . | 2 | 5 | . | . | . | . | . | . | . | . | . | . |125-| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |126-| . | 3 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |127-| . | 2 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |128-129-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.130-131----132-133-## calm-cloud - 6134-135-### Puzzle Grid with Integrated Hints136-137-| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |138-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |139-| . | . | . | 5 | 3 | 5 | . | . | 2 | 3 | 4 | . | 3 |140-| . | . | . | 1 | 1 | 1 | 5 | 1 | 2 | 3 | 3 | 4 | 2 |141-| . | . | . | 2 | 2 | 1 | 3 | 8 | 4 | 1 | 1 | 5 | 1 |142-| . | 6 | 3 | . | . | . | . | . | . | . | . | . | . |143-| . | 4 | 5 | . | . | . | . | . | . | . | . | . | . |144-| . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |145-| . | 1 | 7 | . | . | . | . | . | . | . | . | . | . |146-| 1 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |147-| 1 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |148-| 1 | 1 | 5 | . | . | . | . | . | . | . | . | . | . |149-| . | 1 | 6 | . | . | . | . | . | . | . | . | . | . |150-| . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |151-| . | 1 | 6 | . | . | . | . | . | . | . | . | . | . |152-153-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.154-155----156-157-## scarlet-sage - 7158-159-### Puzzle Grid with Integrated Hints160-161-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |162-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |163-| . | . | . | . | . | . | . | . | . | 1 | . | . | . | 1 |164-| . | . | . | . | . | . | 2 | . | 5 | 1 | . | 1 | 1 | 1 |165-| . | . | . | . | 8 | 4 | 1 | 4 | 1 | 1 | . | 2 | 2 | 2 |166-| . | . | . | . | 1 | 2 | 1 | 3 | 2 | 3 | 8 | 3 | 1 | 1 |167-| . | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |168-| 1 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |169-| . | . | . | 8 | . | . | . | . | . | . | . | . | . | . |170-| 2 | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |171-| . | . | 5 | 1 | . | . | . | . | . | . | . | . | . | . |172-| . | 2 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |173-| . | 1 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |174-| 2 | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |175-| . | . | . | 6 | . | . | . | . | . | . | . | . | . | . |176-| . | 1 | 4 | 2 | . | . | . | . | . | . | . | . | . | . |177-178-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.179-180----181-182-## wandering-viper - 8183-184-### Puzzle Grid with Integrated Hints185-186-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |187-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |188-| . | . | . | . | . | . | . | . | . | . | 2 | . | 1 | . |189-| . | . | . | . | . | 5 | . | 2 | 1 | . | 2 | . | 2 | 3 |190-| . | . | . | . | 4 | 1 | 5 | 4 | 1 | 2 | 1 | 1 | 1 | 2 |191-| . | . | . | . | 4 | 1 | 3 | 2 | 4 | 6 | 2 | 6 | 2 | 1 |192-| . | . | 1 | 4 | . | . | . | . | . | . | . | . | . | . |193-| . | . | 4 | 5 | . | . | . | . | . | . | . | . | . | . |194-| . | 3 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |195-| . | 4 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |196-| . | . | 4 | 4 | . | . | . | . | . | . | . | . | . | . |197-| 2 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |198-| . | . | 2 | 7 | . | . | . | . | . | . | . | . | . | . |199-| 1 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |200-| . | . | 1 | 8 | . | . | . | . | . | . | . | . | . | . |201-| . | . | 5 | 3 | . | . | . | . | . | . | . | . | . | . |202-203-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.204-205----206-207-## twilight-owl - 9208-209-### Puzzle Grid with Integrated Hints210-211-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |212-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |213-| . | . | . | . | 3 | . | . | . | 3 | 2 | . | 4 | . | . |214-| . | . | . | . | 1 | 6 | 7 | . | 1 | 1 | . | 2 | 1 | 2 |215-| . | . | . | . | 1 | 1 | 1 | 6 | 2 | 1 | 5 | 2 | 6 | 2 |216-| . | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |217-| 3 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |218-| . | 3 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |219-| . | 3 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |220-| . | 4 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |221-| . | . | 4 | 3 | . | . | . | . | . | . | . | . | . | . |222-| . | . | 2 | 4 | . | . | . | . | . | . | . | . | . | . |223-| . | 2 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |224-| 1 | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |225-| . | 1 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |226-227-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.228-229----230-231-## silver-bison - 10232-233-### Puzzle Grid with Integrated Hints234-235-| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |236-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |237-| . | . | . | . | . | . | . | . | . | . | . | . | 1 |238-| . | . | . | . | 1 | 2 | . | . | . | 2 | . | . | 2 |239-| . | . | . | 5 | 3 | 1 | . | . | . | 1 | 2 | . | 1 |240-| . | . | . | 2 | 1 | 1 | 3 | 2 | 8 | 1 | 2 | 3 | 1 |241-| . | . | . | 1 | 1 | 1 | 6 | 6 | 1 | 2 | 3 | 6 | 1 |242-| . | 2 | 7 | . | . | . | . | . | . | . | . | . | . |243-| . | 1 | 7 | . | . | . | . | . | . | . | . | . | . |244-| 4 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |245-| 2 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |246-| . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |247-| . | 3 | 2 | . | . | . | . | . | . | . | . | . | . |248-| 1 | 5 | 1 | . | . | . | . | . | . | . | . | . | . |249-| 2 | 3 | 3 | . | . | . | . | . | . | . | . | . | . |250-| . | 2 | 3 | . | . | . | . | . | . | . | . | . | . |251-| . | 4 | 5 | . | . | . | . | . | . | . | . | . | . |252-253-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.254-255----256-257-## pearl-river - 11258-259-### Puzzle Grid with Integrated Hints260-261-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |262-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |263-| . | . | . | . | 1 | . | 1 | 1 | . | . | 3 | . | . | . |264-| . | . | . | . | 4 | 6 | 3 | 6 | 1 | 4 | 3 | 5 | 5 | 1 |265-| . | . | . | . | 3 | 2 | 3 | 1 | 2 | 1 | 2 | 1 | 2 | 4 |266-| . | 1 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |267-| . | . | 2 | 3 | . | . | . | . | . | . | . | . | . | . |268-| . | 2 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |269-| . | . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |270-| . | . | 4 | 4 | . | . | . | . | . | . | . | . | . | . |271-| . | 4 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |272-| 1 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |273-| . | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |274-| . | 3 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |275-| . | . | 5 | 2 | . | . | . | . | . | . | . | . | . | . |276-277-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.278-279----280-281-## eager-cloud - 12282-283-### Puzzle Grid with Integrated Hints284-285-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |286-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |287-| . | . | . | . | . | . | . | . | 1 | . | . | . | . | . |288-| . | . | . | . | 1 | 1 | . | . | 2 | 2 | . | . | . | . |289-| . | . | . | . | 6 | 1 | 5 | 3 | 2 | 1 | . | 2 | . | 2 |290-| . | . | . | . | 1 | 2 | 3 | 4 | 1 | 2 | 9 | 4 | 6 | 3 |291-| . | . | 7 | 1 | . | . | . | . | . | . | . | . | . | . |292-| . | 2 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |293-| . | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |294-| . | 3 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |295-| 1 | 3 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |296-| . | 1 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |297-| . | . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |298-| . | . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |299-| . | . | 1 | 5 | . | . | . | . | . | . | . | . | . | . |300-| . | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |301-302-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.303-304----305-306-## sandy-quartz - 13307-308-### Puzzle Grid with Integrated Hints309-310-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |311-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |312-| . | . | . | . | . | 1 | 1 | . | . | . | . | . | . | . |313-| . | . | . | . | 1 | 1 | 3 | . | . | 3 | 1 | 1 | . | 1 |314-| . | . | . | . | 1 | 1 | 2 | 2 | 2 | 1 | 4 | 6 | 4 | 2 |315-| . | . | . | . | 6 | 1 | 1 | 4 | 3 | 1 | 2 | 1 | 3 | 2 |316-| 1 | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |317-| . | . | 2 | 1 | . | . | . | . | . | . | . | . | . | . |318-| . | 3 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |319-| . | . | 3 | 4 | . | . | . | . | . | . | . | . | . | . |320-| . | . | 4 | 2 | . | . | . | . | . | . | . | . | . | . |321-| . | . | 1 | 5 | . | . | . | . | . | . | . | . | . | . |322-| . | 1 | 3 | 4 | . | . | . | . | . | . | . | . | . | . |323-| . | . | 6 | 3 | . | . | . | . | . | . | . | . | . | . |324-| . | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |325-| . | . | 4 | 2 | . | . | . | . | . | . | . | . | . | . |326-327-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.328-329----330-331-## cosmic-tide - 14332-333-### Puzzle Grid with Integrated Hints334-335-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |336-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |337-| . | . | . | . | . | . | . | . | . | . | . | . | . | 1 |338-| . | . | . | . | . | . | 1 | 1 | . | . | 1 | . | 1 | 1 |339-| . | . | . | . | 6 | . | 1 | 3 | . | 2 | 3 | 4 | 4 | 2 |340-| . | . | . | . | 3 | 9 | 3 | 4 | 8 | 1 | 2 | 4 | 3 | 1 |341-| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |342-| . | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |343-| . | 2 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |344-| . | . | 2 | 6 | . | . | . | . | . | . | . | . | . | . |345-| 2 | 2 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |346-| . | 3 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |347-| 1 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |348-| . | . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |349-| . | . | 4 | 3 | . | . | . | . | . | . | . | . | . | . |350-| . | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |351-352-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.353-354----355-356-## copper-river - 15357-358-### Puzzle Grid with Integrated Hints359-360-| R1 | R2 | R3 | R4 | R5 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |361-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |362-| . | . | . | . | . | . | . | . | 1 | . | . | . | . | . | 3 |363-| . | . | . | . | . | 4 | 2 | 1 | 2 | 2 | . | 1 | 3 | 1 | 1 |364-| . | . | . | . | . | 1 | 3 | 3 | 1 | 1 | 4 | 3 | 1 | 4 | 1 |365-| . | . | . | . | . | 1 | 1 | 4 | 1 | 2 | 3 | 3 | 1 | 2 | 1 |366-| . | . | 5 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |367-| . | . | 2 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |368-| 1 | 2 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |369-| . | 1 | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |370-| . | . | . | 2 | 4 | . | . | . | . | . | . | . | . | . | . |371-| . | . | 1 | 4 | 2 | . | . | . | . | . | . | . | . | . | . |372-| . | . | . | 3 | 1 | . | . | . | . | . | . | . | . | . | . |373-| . | . | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |374-| . | . | . | 3 | 5 | . | . | . | . | . | . | . | . | . | . |375-| . | . | 2 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |376-377-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.378-379----380-381-## deep-badger - 16382-383-### Puzzle Grid with Integrated Hints384-385-| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |386-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |387-| . | . | . | . | 1 | . | . | . | . | . | . | . | . |388-| . | . | . | 1 | 1 | . | . | . | . | . | . | . | . |389-| . | . | . | 1 | 2 | 1 | . | . | 4 | . | 2 | . | . |390-| . | . | . | 1 | 1 | 3 | 7 | 3 | 1 | 2 | 5 | 6 | 3 |391-| . | . | . | 2 | 1 | 1 | 1 | 5 | 2 | 4 | 1 | 1 | 2 |392-| 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |393-| . | . | 8 | . | . | . | . | . | . | . | . | . | . |394-| 2 | 4 | 2 | . | . | . | . | . | . | . | . | . | . |395-| . | 3 | 2 | . | . | . | . | . | . | . | . | . | . |396-| . | 4 | 4 | . | . | . | . | . | . | . | . | . | . |397-| . | . | 9 | . | . | . | . | . | . | . | . | . | . |398-| 1 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |399-| . | 1 | 5 | . | . | . | . | . | . | . | . | . | . |400-| . | 1 | 2 | . | . | . | . | . | . | . | . | . | . |401-| . | 5 | 2 | . | . | . | . | . | . | . | . | . | . |402-403-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.404-405----406-407-## mighty-trout - 17408-409-### Puzzle Grid with Integrated Hints410-411-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |412-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |413-| . | . | . | . | . | . | . | . | . | . | 1 | . | . | 1 |414-| . | . | . | . | 1 | 5 | . | 2 | . | . | 1 | 2 | . | 1 |415-| . | . | . | . | 2 | 1 | 4 | 1 | 2 | 2 | 1 | 5 | 2 | 1 |416-| . | . | . | . | 2 | 1 | 3 | 1 | 4 | 4 | 2 | 1 | 3 | 1 |417-| . | . | 5 | 3 | . | . | . | . | . | . | . | . | . | . |418-| . | . | 5 | 3 | . | . | . | . | . | . | . | . | . | . |419-| . | . | 3 | 1 | . | . | . | . | . | . | . | . | . | . |420-| 3 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |421-| . | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |422-| . | . | . | 7 | . | . | . | . | . | . | . | . | . | . |423-| . | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |424-| . | 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |425-| . | . | 3 | 2 | . | . | . | . | . | . | . | . | . | . |426-| . | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |427-428-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.429-430----431-432-## blazing-trout - 18433-434-### Puzzle Grid with Integrated Hints435-436-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |437-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |438-| . | . | . | . | . | . | . | . | . | . | 2 | . | . | . |439-| . | . | . | . | 1 | 1 | 4 | . | . | . | 1 | . | 1 | . |440-| . | . | . | . | 2 | 3 | 1 | 1 | 4 | 3 | 2 | 7 | 2 | 6 |441-| . | . | . | . | 5 | 3 | 2 | 7 | 2 | 6 | 1 | 2 | 4 | 3 |442-| 4 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |443-| . | . | 1 | 5 | . | . | . | . | . | . | . | . | . | . |444-| . | 4 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |445-| . | . | 5 | 3 | . | . | . | . | . | . | . | . | . | . |446-| . | . | 1 | 7 | . | . | . | . | . | . | . | . | . | . |447-| 1 | 4 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |448-| . | . | 2 | 6 | . | . | . | . | . | . | . | . | . | . |449-| 2 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |450-| . | . | 6 | 3 | . | . | . | . | . | . | . | . | . | . |451-| . | 1 | 1 | 6 | . | . | . | . | . | . | . | . | . | . |452-453-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.454-455----456-457-## gilded-ivy - 19458-459-### Puzzle Grid with Integrated Hints460-461-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |462-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |463-| . | . | . | . | . | 3 | . | . | . | . | . | . | . | . |464-| . | . | . | . | . | 2 | 4 | 2 | . | . | . | . | . | 1 |465-| . | . | . | . | 2 | 1 | 1 | 4 | 4 | 6 | 4 | 1 | 6 | 4 |466-| . | . | . | . | 3 | 1 | 1 | 1 | 2 | 2 | 1 | 7 | 3 | 2 |467-| . | 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |468-| . | . | 7 | 1 | . | . | . | . | . | . | . | . | . | . |469-| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |470-| . | . | 1 | 6 | . | . | . | . | . | . | . | . | . | . |471-| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |472-| 1 | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |473-| . | . | 2 | 1 | . | . | . | . | . | . | . | . | . | . |474-| . | . | 5 | 2 | . | . | . | . | . | . | . | . | . | . |475-| . | . | 1 | 5 | . | . | . | . | . | . | . | . | . | . |476-| . | 4 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |477-478-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.479-480----481-482-## woven-lagoon - 20483-484-### Puzzle Grid with Integrated Hints485-486-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |487-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |488-| . | . | . | . | 3 | . | . | . | 1 | . | . | 1 | . | . |489-| . | . | . | . | 1 | . | 1 | . | 1 | . | . | 2 | . | 2 |490-| . | . | . | . | 1 | . | 3 | 2 | 1 | 6 | 2 | 1 | 3 | 2 |491-| . | . | . | . | 1 | 3 | 3 | 3 | 2 | 2 | 1 | 1 | 3 | 3 |492-| 1 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |493-| . | 2 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |494-| . | 3 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |495-| . | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |496-| 1 | 4 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |497-| . | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |498-| . | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |499-| . | 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |500-| . | . | 4 | 2 | . | . | . | . | . | . | . | . | . | . |501-| 1 | 3 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |502-503-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.504-505----506-507-## fading-aurora - 21508-509-### Puzzle Grid with Integrated Hints510-511-| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |512-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |513-| . | . | . | 1 | 1 | 1 | 5 | . | 3 | . | 1 | 2 | . |514-| . | . | . | 4 | 4 | 5 | 1 | 5 | 2 | 8 | 2 | 1 | 4 |515-| . | . | . | 1 | 2 | 2 | 1 | 1 | 1 | 1 | 3 | 1 | 1 |516-| 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |517-| . | 1 | 7 | . | . | . | . | . | . | . | . | . | . |518-| 1 | 5 | 2 | . | . | . | . | . | . | . | . | . | . |519-| 5 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |520-| 5 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |521-| . | 5 | 1 | . | . | . | . | . | . | . | . | . | . |522-| . | 2 | 4 | . | . | . | . | . | . | . | . | . | . |523-| 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |524-| 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |525-| . | 3 | 2 | . | . | . | . | . | . | . | . | . | . |526-527-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.528-529----530-531-## crimson-owl - 22532-533-### Puzzle Grid with Integrated Hints534-535-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |536-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |537-| . | . | . | . | . | . | 1 | 1 | 2 | 2 | . | 2 | . | 3 |538-| . | . | . | . | 2 | 2 | 1 | 3 | 4 | 4 | 6 | 5 | 5 | 3 |539-| . | . | . | . | 3 | 1 | 5 | 3 | 1 | 1 | 1 | 1 | 4 | 1 |540-| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |541-| . | . | 2 | 6 | . | . | . | . | . | . | . | . | . | . |542-| . | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |543-| . | . | . | 8 | . | . | . | . | . | . | . | . | . | . |544-| . | . | . | 6 | . | . | . | . | . | . | . | . | . | . |545-| . | 2 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |546-| . | 1 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |547-| . | 1 | 2 | 4 | . | . | . | . | . | . | . | . | . | . |548-| 1 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |549-| . | 1 | 3 | 3 | . | . | . | . | . | . | . | . | . | . |550-551-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.552-553----554-555-## timber-falcon - 23556-557-### Puzzle Grid with Integrated Hints558-559-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |560-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |561-| . | . | . | . | . | . | 2 | . | . | . | . | 2 | . | . |562-| . | . | . | . | . | . | 3 | . | . | . | 2 | 2 | 2 | . |563-| . | . | . | . | . | 5 | 1 | 6 | 6 | . | 1 | 1 | 1 | 1 |564-| . | . | . | . | 9 | 1 | 1 | 2 | 2 | 6 | 4 | 1 | 2 | 5 |565-| . | . | 2 | 2 | . | . | . | . | . | . | . | . | . | . |566-| . | . | 1 | 8 | . | . | . | . | . | . | . | . | . | . |567-| . | 2 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |568-| . | . | 6 | 1 | . | . | . | . | . | . | . | . | . | . |569-| . | . | 8 | 1 | . | . | . | . | . | . | . | . | . | . |570-| . | . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |571-| . | 2 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |572-| 1 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |573-| . | 1 | 2 | 4 | . | . | . | . | . | . | . | . | . | . |574-| . | 5 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |575-576-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.577-578----579-580-## blazing-acorn - 24581-582-### Puzzle Grid with Integrated Hints583-584-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |585-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |586-| . | . | . | . | 1 | . | . | 1 | . | . | . | . | . | . |587-| . | . | . | . | 1 | 1 | 3 | 2 | 3 | . | . | 1 | . | . |588-| . | . | . | . | 1 | 2 | 3 | 1 | 1 | 2 | 5 | 2 | 6 | 2 |589-| . | . | . | . | 2 | 2 | 1 | 2 | 2 | 3 | 2 | 4 | 2 | 4 |590-| . | 1 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |591-| 2 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |592-| . | 1 | 3 | 4 | . | . | . | . | . | . | . | . | . | . |593-| . | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |594-| . | 3 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |595-| . | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |596-| . | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |597-| . | . | 3 | 1 | . | . | . | . | . | . | . | . | . | . |598-| . | . | 2 | 7 | . | . | . | . | . | . | . | . | . | . |599-| . | . | 6 | 3 | . | . | . | . | . | . | . | . | . | . |600-601-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.602-603----604-605-## onyx-pond - 25606-607-### Puzzle Grid with Integrated Hints608-609-| R1 | R2 | R3 | R4 | R5 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |610-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |611-| . | . | . | . | . | . | . | . | 2 | . | . | . | . | . | . |612-| . | . | . | . | . | 2 | 1 | . | 1 | . | 2 | . | 3 | 2 | . |613-| . | . | . | . | . | 3 | 2 | 5 | 1 | 1 | 1 | 7 | 1 | 1 | . |614-| . | . | . | . | . | 1 | 2 | 4 | 1 | 8 | 3 | 2 | 3 | 2 | 9 |615-| . | . | . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |616-| . | . | 1 | 2 | 5 | . | . | . | . | . | . | . | . | . | . |617-| 1 | 1 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |618-| . | . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |619-| . | 2 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |620-| . | . | 1 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |621-| . | . | 1 | 5 | 1 | . | . | . | . | . | . | . | . | . | . |622-| . | 3 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |623-| . | . | . | 4 | 4 | . | . | . | . | . | . | . | . | . | . |624-| . | 1 | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |625-626-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.627-628----629-630-## desert-hollow - 26631-632-### Puzzle Grid with Integrated Hints633-634-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |635-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |636-| . | . | . | . | . | . | . | . | . | . | . | 3 | . | . |637-| . | . | . | . | . | . | 1 | . | . | . | . | 1 | . | 2 |638-| . | . | . | . | 7 | 2 | 5 | 3 | 1 | 4 | 7 | 1 | 7 | 3 |639-| . | . | . | . | 1 | 5 | 1 | 3 | 5 | 4 | 2 | 1 | 2 | 2 |640-| . | . | 1 | 5 | . | . | . | . | . | . | . | . | . | . |641-| . | . | 2 | 6 | . | . | . | . | . | . | . | . | . | . |642-| . | . | 4 | 4 | . | . | . | . | . | . | . | . | . | . |643-| . | 1 | 5 | 2 | . | . | . | . | . | . | . | . | . | . |644-| . | . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |645-| 3 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |646-| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |647-| . | . | 2 | 3 | . | . | . | . | . | . | . | . | . | . |648-| . | 1 | 1 | 5 | . | . | . | . | . | . | . | . | . | . |649-| 1 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |650-651-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.652-653----654-655-## emerald-newt - 27656-657-### Puzzle Grid with Integrated Hints658-659-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |660-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |661-| . | . | . | . | . | . | . | 1 | . | . | . | . | . | . |662-| . | . | . | . | 3 | . | . | 1 | 3 | . | . | . | . | . |663-| . | . | . | . | 2 | 6 | 4 | 3 | 1 | 6 | 6 | 7 | 2 | 2 |664-| . | . | . | . | 1 | 3 | 3 | 1 | 2 | 2 | 2 | 1 | 3 | 6 |665-| . | . | 1 | 4 | . | . | . | . | . | . | . | . | . | . |666-| . | 2 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |667-| . | . | 8 | 1 | . | . | . | . | . | . | . | . | . | . |668-| . | . | 3 | 4 | . | . | . | . | . | . | . | . | . | . |669-| . | . | 4 | 5 | . | . | . | . | . | . | . | . | . | . |670-| . | 1 | 5 | 1 | . | . | . | . | . | . | . | . | . | . |671-| . | 1 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |672-| . | 3 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |673-| . | 2 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |674-| 2 | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |675-676-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.677-678----679-680-## velvet-viper - 28681-682-### Puzzle Grid with Integrated Hints683-684-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |685-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |686-| . | . | . | . | . | . | . | 1 | 2 | 1 | . | . | . | . |687-| . | . | . | . | 1 | 2 | . | 4 | 1 | 3 | 1 | 2 | 4 | 1 |688-| . | . | . | . | 3 | 1 | 2 | 1 | 1 | 1 | 4 | 4 | 2 | 5 |689-| . | . | . | . | 3 | 2 | 6 | 1 | 3 | 1 | 1 | 1 | 1 | 1 |690-| . | . | 1 | 8 | . | . | . | . | . | . | . | . | . | . |691-| . | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |692-| 1 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |693-| . | 2 | 3 | 3 | . | . | . | . | . | . | . | . | . | . |694-| . | 4 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |695-| . | 3 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |696-| . | . | 3 | 4 | . | . | . | . | . | . | . | . | . | . |697-| . | 1 | 5 | 2 | . | . | . | . | . | . | . | . | . | . |698-| . | . | 3 | 1 | . | . | . | . | . | . | . | . | . | . |699-| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |700-701-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.702-703----704-705-## crystal-larch - 29706-707-### Puzzle Grid with Integrated Hints708-709-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |710-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |711-| . | . | . | . | . | . | . | 3 | . | . | 1 | . | 1 | 1 |712-| . | . | . | . | . | . | . | 1 | 2 | . | 1 | 1 | 1 | 1 |713-| . | . | . | . | 1 | . | . | 1 | 3 | . | 2 | 2 | 3 | 3 |714-| . | . | . | . | 6 | 7 | 6 | 1 | 3 | 8 | 1 | 2 | 1 | 1 |715-| . | 1 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |716-| . | . | 5 | 1 | . | . | . | . | . | . | . | . | . | . |717-| 3 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |718-| . | . | 2 | 5 | . | . | . | . | . | . | . | . | . | . |719-| . | . | 3 | 2 | . | . | . | . | . | . | . | . | . | . |720-| . | . | 7 | 2 | . | . | . | . | . | . | . | . | . | . |721-| . | . | 3 | 5 | . | . | . | . | . | . | . | . | . | . |722-| . | 1 | 3 | 3 | . | . | . | . | . | . | . | . | . | . |723-| . | . | 1 | 3 | . | . | . | . | . | . | . | . | . | . |724-| . | 1 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |725-726-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.727-728----729-730-## aqua-robin - 30731-732-### Puzzle Grid with Integrated Hints733-734-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |735-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |736-| . | . | . | . | . | . | . | . | . | . | 1 | . | . | . |737-| . | . | . | . | . | . | . | . | 5 | . | 1 | . | . | . |738-| . | . | . | . | 5 | 2 | 3 | 3 | 1 | 6 | 2 | 5 | 6 | 2 |739-| . | . | . | . | 4 | 7 | 5 | 2 | 1 | 1 | 3 | 3 | 3 | 3 |740-| . | . | 2 | 6 | . | . | . | . | . | . | . | . | . | . |741-| . | . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |742-| . | . | 1 | 7 | . | . | . | . | . | . | . | . | . | . |743-| . | 3 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |744-| . | . | 2 | 6 | . | . | . | . | . | . | . | . | . | . |745-| . | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |746-| 3 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |747-| . | . | 4 | 4 | . | . | . | . | . | . | . | . | . | . |748-| . | . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |749-| . | 3 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |750-751-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.752-753----754-755-## starry-trout - 31756-757-### Puzzle Grid with Integrated Hints758-759-| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |760-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |761-| . | . | . | . | . | 1 | . | . | . | . | . | 2 | . |762-| . | . | . | . | 2 | 1 | 4 | 2 | 1 | 1 | . | 1 | . |763-| . | . | . | 2 | 2 | 2 | 2 | 2 | 1 | 1 | 4 | 2 | 7 |764-| . | . | . | 4 | 4 | 2 | 2 | 3 | 1 | 5 | 3 | 1 | 2 |765-| . | 3 | 5 | . | . | . | . | . | . | . | . | . | . |766-| 1 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |767-| 1 | 6 | 1 | . | . | . | . | . | . | . | . | . | . |768-| 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |769-| 2 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |770-| 1 | 3 | 4 | . | . | . | . | . | . | . | . | . | . |771-| 2 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |772-| 3 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |773-| 5 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |774-| 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |775-776-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.@@ -0,0 +1,841 @@
1+package pdfexport2+3+import (4+ "bytes"5+ "encoding/binary"6+ "fmt"7+ "hash/fnv"8+ "image"9+ "image/color"10+ "image/png"11+ "math"12+ "math/rand"13+ "strings"14+ "time"15+16+ "github.com/go-pdf/fpdf"17+)18+19+type coverVec2 struct {20+ x float6421+ y float6422+}23+24+type coverGlow struct {25+ center coverVec226+ spread float6427+ strength float6428+ col color.RGBA29+}30+31+type coverFieldProfile struct {32+ freqX float6433+ freqY float6434+ freqMixX float6435+ freqMixY float6436+ freqCurlX float6437+ freqCurlY float6438+ ampX float6439+ ampY float6440+ ampMix float6441+ ampCurl float6442+ phaseX float6443+ phaseY float6444+ phaseMix float6445+ phaseCurl float6446+ swirl float6447+ shear float6448+ pinch float6449+ pivot coverVec250+}51+52+type coverFlowLayer struct {53+ count int54+ steps int55+ step float6456+ alpha uint857+ radius float6458+ drift float6459+ phase float6460+ waveXFreq float6461+ waveYFreq float6462+ waveXAmp float6463+ waveYAmp float6464+ a color.RGBA65+ b color.RGBA66+}67+68+type coverLatticeProfile struct {69+ enabled bool70+ layout int71+ cols int72+ rows int73+ neighbors int74+ jitterX float6475+ jitterY float6476+ center coverVec277+ radialWarp float6478+ edge color.RGBA79+ nodeOuter color.RGBA80+ nodeInner color.RGBA81+ nodeSize float6482+ coreSize float6483+}84+85+type coverOrbitProfile struct {86+ enabled bool87+ center coverVec288+ count int89+ radiusStart float6490+ radiusStep float6491+ arcCoverage float6492+ segmentsMin int93+ segmentsJitter int94+ eccentricity float6495+ wobble float6496+ dotSize float6497+ colorA color.RGBA98+ colorB color.RGBA99+ alphaBase uint8100+ alphaStep uint8101+}102+103+type coverMarkProfile struct {104+ enabled bool105+ count int106+ gridW int107+ gridH int108+ jitter float64109+ sizeMin float64110+ sizeMax float64111+ alphaBase uint8112+ alphaRange uint8113+ colorA color.RGBA114+ colorB color.RGBA115+ cutout color.RGBA116+}117+118+type coverArtDirection struct {119+ motif int120+ top color.RGBA121+ mid color.RGBA122+ bottom color.RGBA123+ verticalCurve float64124+ glows []coverGlow125+ field coverFieldProfile126+ flowLayers []coverFlowLayer127+ lattice coverLatticeProfile128+ orbit coverOrbitProfile129+ marks coverMarkProfile130+ grainCount int131+ grainWarm color.RGBA132+ grainCool color.RGBA133+}134+135+func drawCoverArtworkImage(136+ pdf *fpdf.Fpdf,137+ scene rectMM,138+ seedText string,139+ variant string,140+ base RGB,141+) {142+ seedText = strings.TrimSpace(seedText)143+ if seedText == "" {144+ seedText = "puzzletea" + time.Now().String()145+ }146+ if strings.TrimSpace(variant) == "" {147+ variant = "front"148+ }149+ imageName := fmt.Sprintf(150+ "puzzletea-cover-artwork-%016x",151+ coverSeedHash(seedText+"|"+variant),152+ )153+ artPNG := renderCoverArtworkPNG(seedText, variant, base)154+ options := fpdf.ImageOptions{155+ ImageType: "PNG",156+ ReadDpi: true,157+ }158+ pdf.RegisterImageOptionsReader(imageName, options, bytes.NewReader(artPNG))159+ pdf.ImageOptions(imageName, scene.x, scene.y, scene.w, scene.h, false, options, 0, "")160+}161+162+func renderCoverArtworkPNG(seedText, variant string, base RGB) []byte {163+ const (164+ width = 1200165+ height = 1400166+ )167+ seed := coverSeedHash(seedText + "|" + variant)168+ direction := newCoverArtDirection(seed, base)169+ img := image.NewRGBA(image.Rect(0, 0, width, height))170+171+ paintCoverBackground(img, width, height, direction)172+ drawCoverFlowTrails(img, width, height, direction, coverStreamRNG(seed, "flow"))173+ drawCoverPuzzleLattice(img, width, height, direction, coverStreamRNG(seed, "lattice"))174+ drawCoverOrbitBands(img, width, height, direction, coverStreamRNG(seed, "orbit"))175+ drawCoverSeedMarks(img, width, height, direction, coverStreamRNG(seed, "marks"))176+ drawCoverFilmGrain(img, width, height, direction, coverStreamRNG(seed, "grain"))177+178+ var out bytes.Buffer179+ if err := png.Encode(&out, img); err != nil {180+ return []byte{}181+ }182+ return out.Bytes()183+}184+185+func newCoverArtDirection(seed uint64, base RGB) coverArtDirection {186+ rng := coverStreamRNG(seed, "direction")187+ motif := int(seed % 5)188+189+ warm := blendRGB(190+ brightenRGB(base, 0.30+rng.Float64()*0.28),191+ color.RGBA{R: 255, G: 230, B: 180, A: 255},192+ 0.34+rng.Float64()*0.38,193+ )194+ cool := blendRGB(195+ brightenRGB(base, 0.18+rng.Float64()*0.24),196+ color.RGBA{R: 88, G: 212, B: 230, A: 255},197+ 0.30+rng.Float64()*0.42,198+ )199+ dusk := blendRGB(200+ darkenRGB(base, 0.50+rng.Float64()*0.22),201+ color.RGBA{R: 9, G: 18, B: 32, A: 255},202+ 0.44+rng.Float64()*0.30,203+ )204+ ink := blendRGB(dusk, color.RGBA{R: 22, G: 36, B: 52, A: 255}, 0.40+rng.Float64()*0.25)205+206+ top := blendRGB(warm, color.RGBA{R: 255, G: 255, B: 251, A: 255}, 0.15+rng.Float64()*0.18)207+ mid := blendRGB(cool, warm, 0.16+rng.Float64()*0.26)208+ bottom := blendRGB(dusk, color.RGBA{R: 4, G: 9, B: 16, A: 255}, 0.30+rng.Float64()*0.22)209+210+ glows := make([]coverGlow, 0, 4)211+ glowCount := 3 + rng.Intn(2)212+ for i := 0; i < glowCount; i++ {213+ col := warm214+ if i%2 == 1 {215+ col = cool216+ }217+ col = jitterRGBA(col, rng, 18)218+ glows = append(glows, coverGlow{219+ center: coverVec2{220+ x: 0.08 + rng.Float64()*0.84,221+ y: 0.10 + rng.Float64()*0.82,222+ },223+ spread: 0.42 + rng.Float64()*0.70,224+ strength: 0.16 + rng.Float64()*0.50,225+ col: col,226+ })227+ }228+229+ field := coverFieldProfile{230+ freqX: 4.0 + rng.Float64()*5.8,231+ freqY: 3.8 + rng.Float64()*5.6,232+ freqMixX: 0.5 + rng.Float64()*1.7,233+ freqMixY: 0.5 + rng.Float64()*1.7,234+ freqCurlX: 0.4 + rng.Float64()*1.9,235+ freqCurlY: 0.4 + rng.Float64()*1.9,236+ ampX: 0.65 + rng.Float64()*0.95,237+ ampY: 0.65 + rng.Float64()*0.95,238+ ampMix: 0.28 + rng.Float64()*0.86,239+ ampCurl: 0.20 + rng.Float64()*0.86,240+ phaseX: 0.25 + rng.Float64()*1.40,241+ phaseY: 0.25 + rng.Float64()*1.40,242+ phaseMix: 0.20 + rng.Float64()*1.10,243+ phaseCurl: 0.20 + rng.Float64()*1.10,244+ swirl: (rng.Float64()*2 - 1) * 0.34,245+ shear: (rng.Float64()*2 - 1) * 0.34,246+ pinch: (rng.Float64()*2 - 1) * 0.52,247+ pivot: coverVec2{248+ x: 0.22 + rng.Float64()*0.56,249+ y: 0.20 + rng.Float64()*0.62,250+ },251+ }252+253+ flowLayers := make([]coverFlowLayer, 0, 4)254+ layerCount := 2 + rng.Intn(3)255+ for i := 0; i < layerCount; i++ {256+ t := float64(i) / float64(maxInt(1, layerCount-1))257+ a := jitterRGBA(lerpRGB(warm, cool, 0.22+t*0.54), rng, 20)258+ b := jitterRGBA(lerpRGB(cool, warm, 0.18+t*0.66), rng, 20)259+ flowLayers = append(flowLayers, coverFlowLayer{260+ count: 250 + rng.Intn(320),261+ steps: 90 + rng.Intn(120),262+ step: 1.18 + rng.Float64()*1.72,263+ alpha: uint8(14 + rng.Intn(38)),264+ radius: 0.76 + rng.Float64()*0.86,265+ drift: 0.44 + rng.Float64()*1.68,266+ phase: rng.Float64() * math.Pi * 2,267+ waveXFreq: 5.0 + rng.Float64()*7.2,268+ waveYFreq: 5.0 + rng.Float64()*7.2,269+ waveXAmp: 0.05 + rng.Float64()*0.33,270+ waveYAmp: 0.05 + rng.Float64()*0.33,271+ a: a,272+ b: b,273+ })274+ }275+276+ lattice := coverLatticeProfile{277+ enabled: true,278+ layout: rng.Intn(3),279+ cols: 7 + rng.Intn(6),280+ rows: 6 + rng.Intn(6),281+ neighbors: 2 + rng.Intn(3),282+ jitterX: 24 + rng.Float64()*46,283+ jitterY: 28 + rng.Float64()*52,284+ center: coverVec2{x: 0.30 + rng.Float64()*0.40, y: 0.26 + rng.Float64()*0.44},285+ radialWarp: (rng.Float64()*2 - 1) * 0.34,286+ edge: color.RGBA{R: ink.R, G: ink.G, B: ink.B, A: uint8(40 + rng.Intn(60))},287+ nodeOuter: blendRGB(warm, color.RGBA{R: 255, G: 245, B: 218, A: 255}, 0.35),288+ nodeInner: blendRGB(dusk, color.RGBA{R: 16, G: 30, B: 48, A: 255}, 0.25),289+ nodeSize: 3.6 + rng.Float64()*3.6,290+ coreSize: 1.5 + rng.Float64()*1.6,291+ }292+293+ orbit := coverOrbitProfile{294+ enabled: true,295+ center: coverVec2{x: 0.34 + rng.Float64()*0.32, y: 0.30 + rng.Float64()*0.32},296+ count: 8 + rng.Intn(18),297+ radiusStart: 40 + rng.Float64()*50,298+ radiusStep: 15 + rng.Float64()*18,299+ arcCoverage: 0.42 + rng.Float64()*0.58,300+ segmentsMin: 38 + rng.Intn(34),301+ segmentsJitter: 46 + rng.Intn(52),302+ eccentricity: 0.70 + rng.Float64()*0.48,303+ wobble: 2.8 + rng.Float64()*8.4,304+ dotSize: 0.86 + rng.Float64()*0.76,305+ colorA: jitterRGBA(lerpRGB(warm, cool, 0.32), rng, 16),306+ colorB: jitterRGBA(lerpRGB(cool, warm, 0.70), rng, 16),307+ alphaBase: uint8(12 + rng.Intn(20)),308+ alphaStep: uint8(1 + rng.Intn(2)),309+ }310+311+ marks := coverMarkProfile{312+ enabled: true,313+ count: 24 + rng.Intn(54),314+ gridW: 8 + rng.Intn(8),315+ gridH: 10 + rng.Intn(9),316+ jitter: 8 + rng.Float64()*28,317+ sizeMin: 1.8 + rng.Float64()*1.3,318+ sizeMax: 3.8 + rng.Float64()*2.8,319+ alphaBase: uint8(32 + rng.Intn(36)),320+ alphaRange: uint8(12 + rng.Intn(32)),321+ colorA: jitterRGBA(lerpRGB(warm, cool, 0.20), rng, 20),322+ colorB: jitterRGBA(lerpRGB(cool, warm, 0.72), rng, 20),323+ cutout: blendRGB(dusk, color.RGBA{R: 5, G: 10, B: 18, A: 255}, 0.35),324+ }325+326+ direction := coverArtDirection{327+ motif: motif,328+ top: top,329+ mid: mid,330+ bottom: bottom,331+ verticalCurve: 1.24 + rng.Float64()*1.08,332+ glows: glows,333+ field: field,334+ flowLayers: flowLayers,335+ lattice: lattice,336+ orbit: orbit,337+ marks: marks,338+ grainCount: 32000 + rng.Intn(34000),339+ grainWarm: blendRGB(warm, color.RGBA{R: 255, G: 246, B: 226, A: 255}, 0.42),340+ grainCool: blendRGB(dusk, color.RGBA{R: 5, G: 12, B: 20, A: 255}, 0.52),341+ }342+343+ switch direction.motif {344+ case 0:345+ direction.lattice.layout = 0346+ direction.lattice.neighbors = minInt(5, direction.lattice.neighbors+1)347+ direction.orbit.arcCoverage = clamp01(direction.orbit.arcCoverage*0.72 + 0.10)348+ direction.marks.count += 10349+ case 1:350+ for i := range direction.flowLayers {351+ direction.flowLayers[i].count += 120352+ direction.flowLayers[i].steps += 18353+ direction.flowLayers[i].alpha = minUint8(70, direction.flowLayers[i].alpha+12)354+ direction.flowLayers[i].drift *= 1.24355+ }356+ direction.lattice.edge.A = maxUint8(18, direction.lattice.edge.A-22)357+ direction.orbit.count = maxInt(6, direction.orbit.count/2)358+ direction.grainCount += 6000359+ case 2:360+ direction.orbit.count += 12361+ direction.orbit.arcCoverage = clamp01(direction.orbit.arcCoverage*1.18 + 0.08)362+ direction.orbit.radiusStep *= 0.86363+ direction.lattice.enabled = rng.Float64() > 0.26364+ for i := range direction.flowLayers {365+ direction.flowLayers[i].count = maxInt(180, direction.flowLayers[i].count-60)366+ }367+ case 3:368+ direction.lattice.layout = 2369+ direction.marks.count += 30370+ direction.marks.sizeMax += 1.4371+ direction.orbit.arcCoverage *= 0.74372+ direction.field.swirl *= 1.25373+ case 4:374+ direction.lattice.layout = 1375+ direction.lattice.rows += 2376+ direction.orbit.eccentricity = clamp(0.55, 1.42, direction.orbit.eccentricity*1.16)377+ direction.field.pinch *= 1.34378+ direction.grainCount += 9000379+ }380+381+ return direction382+}383+384+func paintCoverBackground(img *image.RGBA, w, h int, direction coverArtDirection) {385+ for y := range h {386+ ty := float64(y) / float64(h-1)387+ vertical := blendRGB(388+ lerpRGB(direction.top, direction.mid, ty*1.12),389+ direction.bottom,390+ powClamp(ty, direction.verticalCurve),391+ )392+ for x := range w {393+ tx := float64(x) / float64(w-1)394+ col := vertical395+ for _, glow := range direction.glows {396+ amount := radialFalloff(tx, ty, glow.center.x, glow.center.y, glow.spread)397+ col = blendRGB(col, glow.col, amount*glow.strength)398+ }399+ img.SetRGBA(x, y, col)400+ }401+ }402+}403+404+func drawCoverFlowTrails(405+ img *image.RGBA,406+ w, h int,407+ direction coverArtDirection,408+ rng *rand.Rand,409+) {410+ for _, layer := range direction.flowLayers {411+ for i := 0; i < layer.count; i++ {412+ x := rng.Float64() * float64(w)413+ y := rng.Float64() * float64(h)414+ for step := 0; step < layer.steps; step++ {415+ nx := x / float64(w)416+ ny := y / float64(h)417+ angle := coverFieldAngle(nx, ny, direction.field, layer.phase)418+ angle += math.Sin((ny+layer.phase)*layer.waveYFreq) * layer.waveYAmp419+ angle += math.Cos((nx-layer.phase)*layer.waveXFreq) * layer.waveXAmp420+421+ x += math.Cos(angle) * layer.step422+ y += math.Sin(angle) * layer.step423+ x += math.Cos((ny-layer.phase)*math.Pi*2) * layer.drift * 0.04424+ y += math.Sin((nx+layer.phase)*math.Pi*2) * layer.drift * 0.08425+ if x < 1 || x >= float64(w-1) || y < 1 || y >= float64(h-1) {426+ break427+ }428+ t := float64(step) / float64(layer.steps)429+ c := lerpRGB(layer.a, layer.b, t)430+ c.A = layer.alpha431+ drawDisc(img, x, y, layer.radius, c)432+ }433+ }434+ }435+}436+437+func drawCoverPuzzleLattice(438+ img *image.RGBA,439+ w, h int,440+ direction coverArtDirection,441+ rng *rand.Rand,442+) {443+ if !direction.lattice.enabled {444+ return445+ }446+ nodes := buildCoverLatticeNodes(w, h, direction.lattice, rng)447+ if len(nodes) == 0 {448+ return449+ }450+451+ for i := range nodes {452+ for _, j := range nearestN(nodes, i, direction.lattice.neighbors) {453+ if j > i {454+ drawLine(img, nodes[i], nodes[j], direction.lattice.edge)455+ }456+ }457+ }458+ for _, n := range nodes {459+ drawDisc(img, n.x, n.y, direction.lattice.nodeSize, direction.lattice.nodeOuter)460+ drawDisc(img, n.x, n.y, direction.lattice.coreSize, direction.lattice.nodeInner)461+ }462+}463+464+func buildCoverLatticeNodes(465+ w, h int,466+ profile coverLatticeProfile,467+ rng *rand.Rand,468+) []coverVec2 {469+ nodes := make([]coverVec2, 0, profile.cols*profile.rows)470+ switch profile.layout {471+ case 1:472+ minDim := math.Min(float64(w), float64(h))473+ rings := maxInt(4, profile.rows+1)474+ spokes := maxInt(8, profile.cols+4)475+ center := coverVec2{476+ x: profile.center.x * float64(w),477+ y: profile.center.y * float64(h),478+ }479+ for ring := 1; ring <= rings; ring++ {480+ ringT := float64(ring) / float64(rings+1)481+ radius := ringT * minDim * (0.16 + 0.62*ringT)482+ offset := rng.Float64() * math.Pi * 2483+ for spoke := 0; spoke < spokes; spoke++ {484+ ang := offset + float64(spoke)/float64(spokes)*math.Pi*2485+ ang += math.Sin(float64(ring)*0.9+float64(spoke)*0.45) * profile.radialWarp486+ jitter := (rng.Float64() - 0.5) * profile.jitterX487+ x := center.x + math.Cos(ang)*(radius+jitter)488+ y := center.y + math.Sin(ang)*(radius+jitter*0.45)489+ nodes = append(nodes, coverVec2{x: x, y: y})490+ }491+ }492+ case 2:493+ total := maxInt(38, profile.cols*profile.rows)494+ for i := 0; i < total; i++ {495+ tx := (rng.Float64()*0.84 + 0.08)496+ ty := (rng.Float64()*0.84 + 0.08)497+ warpX := math.Sin(ty*math.Pi*4+float64(i)*0.22) * profile.radialWarp * 0.12498+ warpY := math.Cos(tx*math.Pi*3+float64(i)*0.18) * profile.radialWarp * 0.10499+ x := (tx+warpX)*float64(w) + (rng.Float64()-0.5)*profile.jitterX500+ y := (ty+warpY)*float64(h) + (rng.Float64()-0.5)*profile.jitterY501+ nodes = append(nodes, coverVec2{x: x, y: y})502+ }503+ default:504+ for row := 0; row < profile.rows; row++ {505+ for col := 0; col < profile.cols; col++ {506+ x := (float64(col) + 1) / (float64(profile.cols) + 1) * float64(w)507+ y := (float64(row) + 1) / (float64(profile.rows) + 1) * float64(h)508+ x += (rng.Float64() - 0.5) * profile.jitterX509+ y += (rng.Float64() - 0.5) * profile.jitterY510+ nodes = append(nodes, coverVec2{x: x, y: y})511+ }512+ }513+ }514+ return nodes515+}516+517+func drawCoverOrbitBands(518+ img *image.RGBA,519+ w, h int,520+ direction coverArtDirection,521+ rng *rand.Rand,522+) {523+ if !direction.orbit.enabled {524+ return525+ }526+ center := coverVec2{527+ x: float64(w) * direction.orbit.center.x,528+ y: float64(h) * direction.orbit.center.y,529+ }530+ for i := 0; i < direction.orbit.count; i++ {531+ radius := direction.orbit.radiusStart + float64(i)*direction.orbit.radiusStep532+ start := rng.Float64() * math.Pi * 2533+ sweep := direction.orbit.arcCoverage * (0.74 + rng.Float64()*0.52) * math.Pi * 2534+ sweep = math.Min(sweep, math.Pi*2)535+ segments := direction.orbit.segmentsMin + rng.Intn(direction.orbit.segmentsJitter)536+ t := float64(i) / float64(maxInt(1, direction.orbit.count-1))537+ col := lerpRGB(direction.orbit.colorA, direction.orbit.colorB, t)538+ col.A = minUint8(220, direction.orbit.alphaBase+uint8(i)*direction.orbit.alphaStep)539+ for s := 0; s < segments; s++ {540+ a := start + float64(s)/float64(segments)*sweep541+ jx := math.Sin(float64(i)*0.43+a*2.9) * direction.orbit.wobble542+ jy := math.Cos(float64(i)*0.37+a*2.3) * direction.orbit.wobble * 0.82543+ x := center.x + math.Cos(a)*radius*direction.orbit.eccentricity + jx544+ y := center.y + math.Sin(a)*radius + jy545+ drawDisc(img, x, y, direction.orbit.dotSize, col)546+ }547+ }548+}549+550+func drawCoverSeedMarks(551+ img *image.RGBA,552+ w, h int,553+ direction coverArtDirection,554+ rng *rand.Rand,555+) {556+ if !direction.marks.enabled {557+ return558+ }559+ for i := 0; i < direction.marks.count; i++ {560+ gx := 1 + rng.Intn(direction.marks.gridW)561+ gy := 1 + rng.Intn(direction.marks.gridH)562+ x := float64(gx) / float64(direction.marks.gridW+1) * float64(w)563+ y := float64(gy) / float64(direction.marks.gridH+1) * float64(h)564+ x += (rng.Float64() - 0.5) * direction.marks.jitter565+ y += (rng.Float64() - 0.5) * direction.marks.jitter566+567+ size := direction.marks.sizeMin568+ if direction.marks.sizeMax > direction.marks.sizeMin {569+ size += rng.Float64() * (direction.marks.sizeMax - direction.marks.sizeMin)570+ }571+ col := lerpRGB(direction.marks.colorA, direction.marks.colorB, rng.Float64())572+ col.A = minUint8(220, direction.marks.alphaBase+uint8(rng.Intn(int(direction.marks.alphaRange)+1)))573+ style := (i + rng.Intn(4) + direction.motif) % 4574+ switch style {575+ case 0:576+ drawDisc(img, x, y, size*0.48, col)577+ case 1:578+ drawDisc(img, x, y, size, col)579+ drawDisc(img, x, y, size*0.52, direction.marks.cutout)580+ case 2:581+ drawLine(img, coverVec2{x: x - size, y: y}, coverVec2{x: x + size, y: y}, col)582+ drawLine(img, coverVec2{x: x, y: y - size}, coverVec2{x: x, y: y + size}, col)583+ default:584+ drawLine(img, coverVec2{x: x - size, y: y - size}, coverVec2{x: x + size, y: y + size}, col)585+ drawLine(img, coverVec2{x: x - size, y: y + size}, coverVec2{x: x + size, y: y - size}, col)586+ drawDisc(img, x, y, size*0.32, direction.marks.cutout)587+ }588+ }589+}590+591+func drawCoverFilmGrain(592+ img *image.RGBA,593+ w, h int,594+ direction coverArtDirection,595+ rng *rand.Rand,596+) {597+ for i := 0; i < direction.grainCount; i++ {598+ x := rng.Intn(w)599+ y := rng.Intn(h)600+ alpha := uint8(8 + rng.Intn(18))601+ if rng.Intn(2) == 0 {602+ blendPixel(img, x, y, color.RGBA{603+ R: direction.grainWarm.R,604+ G: direction.grainWarm.G,605+ B: direction.grainWarm.B,606+ A: alpha,607+ })608+ continue609+ }610+ blendPixel(img, x, y, color.RGBA{611+ R: direction.grainCool.R,612+ G: direction.grainCool.G,613+ B: direction.grainCool.B,614+ A: alpha,615+ })616+ }617+}618+619+func coverFieldAngle(x, y float64, field coverFieldProfile, phase float64) float64 {620+ ax := math.Sin((x*field.freqX+phase*field.phaseX)*math.Pi*2) * field.ampX621+ ay := math.Cos((y*field.freqY-phase*field.phaseY)*math.Pi*2) * field.ampY622+ mix := math.Sin((x*field.freqMixX+y*field.freqMixY+phase*field.phaseMix)*math.Pi*2) * field.ampMix623+ curl := math.Cos((x*field.freqCurlX-y*field.freqCurlY+phase*field.phaseCurl)*math.Pi*2) * field.ampCurl624+ radial := math.Atan2(y-field.pivot.y, x-field.pivot.x)625+ distance := math.Hypot(x-field.pivot.x, y-field.pivot.y)626+ swirl := radial * field.swirl627+ pinch := (0.5 - distance) * field.pinch628+ shear := (x - y) * field.shear629+ return ax + ay + mix + curl + swirl + pinch + shear630+}631+632+func nearestN(nodes []coverVec2, idx, n int) []int {633+ n = maxInt(1, n)634+ bestDistance := make([]float64, n)635+ bestIndex := make([]int, n)636+ for i := range bestDistance {637+ bestDistance[i] = math.MaxFloat64638+ bestIndex[i] = -1639+ }640+ for j := range nodes {641+ if j == idx {642+ continue643+ }644+ dx := nodes[idx].x - nodes[j].x645+ dy := nodes[idx].y - nodes[j].y646+ distance := dx*dx + dy*dy647+ for k := range bestDistance {648+ if distance >= bestDistance[k] {649+ continue650+ }651+ for shift := len(bestDistance) - 1; shift > k; shift-- {652+ bestDistance[shift] = bestDistance[shift-1]653+ bestIndex[shift] = bestIndex[shift-1]654+ }655+ bestDistance[k] = distance656+ bestIndex[k] = j657+ break658+ }659+ }660+ out := make([]int, 0, len(bestIndex))661+ for _, best := range bestIndex {662+ if best >= 0 {663+ out = append(out, best)664+ }665+ }666+ return out667+}668+669+func drawDisc(img *image.RGBA, cx, cy, radius float64, c color.RGBA) {670+ minX := int(math.Floor(cx - radius))671+ maxX := int(math.Ceil(cx + radius))672+ minY := int(math.Floor(cy - radius))673+ maxY := int(math.Ceil(cy + radius))674+ r2 := radius * radius675+ for y := minY; y <= maxY; y++ {676+ for x := minX; x <= maxX; x++ {677+ dx := float64(x) + 0.5 - cx678+ dy := float64(y) + 0.5 - cy679+ if dx*dx+dy*dy <= r2 {680+ blendPixel(img, x, y, c)681+ }682+ }683+ }684+}685+686+func drawLine(img *image.RGBA, a, b coverVec2, c color.RGBA) {687+ dx := b.x - a.x688+ dy := b.y - a.y689+ steps := int(math.Max(math.Abs(dx), math.Abs(dy)))690+ if steps <= 0 {691+ blendPixel(img, int(a.x), int(a.y), c)692+ return693+ }694+ for i := 0; i <= steps; i++ {695+ t := float64(i) / float64(steps)696+ drawDisc(img, a.x+dx*t, a.y+dy*t, 0.82, c)697+ }698+}699+700+func blendPixel(img *image.RGBA, x, y int, src color.RGBA) {701+ if !image.Pt(x, y).In(img.Rect) {702+ return703+ }704+ dst := img.RGBAAt(x, y)705+ alpha := float64(src.A) / 255706+ inv := 1 - alpha707+ img.SetRGBA(x, y, color.RGBA{708+ R: uint8(float64(src.R)*alpha + float64(dst.R)*inv),709+ G: uint8(float64(src.G)*alpha + float64(dst.G)*inv),710+ B: uint8(float64(src.B)*alpha + float64(dst.B)*inv),711+ A: 255,712+ })713+}714+715+func brightenRGB(base RGB, amount float64) color.RGBA {716+ return blendRGB(rgbToColor(base), color.RGBA{R: 255, G: 255, B: 255, A: 255}, amount)717+}718+719+func darkenRGB(base RGB, amount float64) color.RGBA {720+ return blendRGB(rgbToColor(base), color.RGBA{R: 0, G: 0, B: 0, A: 255}, amount)721+}722+723+func rgbToColor(c RGB) color.RGBA {724+ return color.RGBA{R: c.R, G: c.G, B: c.B, A: 255}725+}726+727+func lerpRGB(a, b color.RGBA, t float64) color.RGBA {728+ t = clamp01(t)729+ return color.RGBA{730+ R: uint8(float64(a.R) + (float64(b.R)-float64(a.R))*t),731+ G: uint8(float64(a.G) + (float64(b.G)-float64(a.G))*t),732+ B: uint8(float64(a.B) + (float64(b.B)-float64(a.B))*t),733+ A: 255,734+ }735+}736+737+func blendRGB(base, top color.RGBA, amount float64) color.RGBA {738+ return lerpRGB(base, top, clamp01(amount))739+}740+741+func radialFalloff(x, y, cx, cy, spread float64) float64 {742+ dx := x - cx743+ dy := y - cy744+ distance := math.Sqrt(dx*dx + dy*dy)745+ if distance >= spread {746+ return 0747+ }748+ q := 1 - distance/spread749+ return q * q750+}751+752+func powClamp(v, p float64) float64 {753+ return math.Pow(clamp01(v), p)754+}755+756+func clamp01(v float64) float64 {757+ if v < 0 {758+ return 0759+ }760+ if v > 1 {761+ return 1762+ }763+ return v764+}765+766+func clamp(min, max, v float64) float64 {767+ if v < min {768+ return min769+ }770+ if v > max {771+ return max772+ }773+ return v774+}775+776+func maxInt(a, b int) int {777+ if a > b {778+ return a779+ }780+ return b781+}782+783+func minInt(a, b int) int {784+ if a < b {785+ return a786+ }787+ return b788+}789+790+func maxUint8(a, b uint8) uint8 {791+ if a > b {792+ return a793+ }794+ return b795+}796+797+func minUint8(a, b uint8) uint8 {798+ if a < b {799+ return a800+ }801+ return b802+}803+804+func jitterRGBA(c color.RGBA, rng *rand.Rand, spread int) color.RGBA {805+ if spread <= 0 {806+ return c807+ }808+ half := spread / 2809+ return color.RGBA{810+ R: shiftChannel(c.R, rng.Intn(spread)-half),811+ G: shiftChannel(c.G, rng.Intn(spread)-half),812+ B: shiftChannel(c.B, rng.Intn(spread)-half),813+ A: c.A,814+ }815+}816+817+func shiftChannel(ch uint8, delta int) uint8 {818+ v := int(ch) + delta819+ if v < 0 {820+ return 0821+ }822+ if v > 255 {823+ return 255824+ }825+ return uint8(v)826+}827+828+func coverStreamRNG(seed uint64, stream string) *rand.Rand {829+ h := fnv.New64a()830+ var raw [8]byte831+ binary.LittleEndian.PutUint64(raw[:], seed)832+ _, _ = h.Write(raw[:])833+ _, _ = h.Write([]byte(stream))834+ return rand.New(rand.NewSource(int64(h.Sum64())))835+}836+837+func coverSeedHash(text string) uint64 {838+ h := fnv.New64a()839+ h.Write([]byte(text))840+ return h.Sum64()841+}@@ -0,0 +1,89 @@
1+package pdfexport2+3+import (4+ "bytes"5+ "image"6+ "image/png"7+ "math"8+ "testing"9+)10+11+// --- Cover art seeding (P1) ---12+13+func TestRenderCoverArtworkPNGDeterministic(t *testing.T) {14+ base := RGB{R: 86, G: 124, B: 149}15+ seed := "quiet-fjord"16+17+ first := renderCoverArtworkPNG(seed, "front", base)18+ second := renderCoverArtworkPNG(seed, "front", base)19+20+ if len(first) == 0 || len(second) == 0 {21+ t.Fatalf("renderCoverArtworkPNG returned empty output")22+ }23+ if !bytes.Equal(first, second) {24+ t.Fatalf("expected identical PNG bytes for identical seed and variant")25+ }26+}27+28+func TestRenderCoverArtworkPNGVariesBySeedAndVariant(t *testing.T) {29+ if testing.Short() {30+ t.Skip("skipping cover art variation check in short mode")31+ }32+33+ base := RGB{R: 90, G: 128, B: 154}34+ frontA := renderCoverArtworkPNG("quiet-fjord", "front", base)35+ frontB := renderCoverArtworkPNG("quiet-fjord-alt", "front", base)36+ backA := renderCoverArtworkPNG("quiet-fjord", "back", base)37+38+ assertArtworkDiff(t, frontA, frontB, 0.03)39+ assertArtworkDiff(t, frontA, backA, 0.03)40+}41+42+func assertArtworkDiff(t *testing.T, left, right []byte, minMeanDiff float64) {43+ t.Helper()44+45+ if bytes.Equal(left, right) {46+ t.Fatalf("unexpected byte-identical images for different inputs")47+ }48+49+ imgA := decodePNG(t, left)50+ imgB := decodePNG(t, right)51+ diff := sampledMeanChannelDiff(imgA, imgB, 12)52+ if diff < minMeanDiff {53+ t.Fatalf("mean sampled channel diff too low: got %.4f want >= %.4f", diff, minMeanDiff)54+ }55+}56+57+func decodePNG(t *testing.T, data []byte) image.Image {58+ t.Helper()59+ img, err := png.Decode(bytes.NewReader(data))60+ if err != nil {61+ t.Fatalf("png.Decode error: %v", err)62+ }63+ return img64+}65+66+func sampledMeanChannelDiff(a, b image.Image, step int) float64 {67+ bounds := a.Bounds()68+ if step < 1 {69+ step = 170+ }71+72+ var total float6473+ var samples int74+ for y := bounds.Min.Y; y < bounds.Max.Y; y += step {75+ for x := bounds.Min.X; x < bounds.Max.X; x += step {76+ ar, ag, ab, _ := a.At(x, y).RGBA()77+ br, bg, bb, _ := b.At(x, y).RGBA()78+ total += math.Abs(float64(ar)-float64(br)) / 6553579+ total += math.Abs(float64(ag)-float64(bg)) / 6553580+ total += math.Abs(float64(ab)-float64(bb)) / 6553581+ samples += 382+ }83+ }84+85+ if samples == 0 {86+ return 087+ }88+ return total / float64(samples)89+}@@ -114,7 +114,7 @@ func ParseJSONLFile(path string) (PackDocument, error) {
114114 }115115 hydratePuzzlePrintData(&p)116116 117- if p.Nonogram == nil && p.Table == nil && strings.TrimSpace(record.Puzzle.Snippet) != "" {117+ if !hasRenderablePrintData(p) && strings.TrimSpace(record.Puzzle.Snippet) != "" {118118 nonogram, table, err := ParsePrintableFromSnippet(category, record.Puzzle.Snippet)119119 if err != nil {120120 return PackDocument{}, fmt.Errorf("%s:%d: parse printable snippet: %w", path, lineNo, err)@@ -159,3 +159,15 @@ func ParsePrintableFromSnippet(category, snippet string) (*NonogramData, *GridTa
159159 }160160 return nil, table, nil161161 }162+163+func hasRenderablePrintData(p Puzzle) bool {164+ return p.Nonogram != nil ||165+ p.Nurikabe != nil ||166+ p.Shikaku != nil ||167+ p.Hashi != nil ||168+ p.Hitori != nil ||169+ p.Takuzu != nil ||170+ p.Sudoku != nil ||171+ p.WordSearch != nil ||172+ p.Table != nil173+}@@ -47,6 +47,39 @@ func TestParseJSONLFile(t *testing.T) {
4747 }4848 }4949 50+func TestParseJSONLFileHydratesNonogramFromSaveWithoutSnippet(t *testing.T) {51+ path := filepath.Join(t.TempDir(), "nonogram-save-only.jsonl")52+ record := JSONLRecord{53+ Schema: ExportSchemaV1,54+ Pack: JSONLPackMeta{55+ Generated: "2026-02-22T10:00:00Z",56+ Version: "v-test",57+ Category: "Nonogram",58+ ModeSelection: "Mini",59+ Count: 1,60+ },61+ Puzzle: JSONLPuzzle{62+ Index: 1,63+ Name: "fern-owl",64+ Game: "Nonogram",65+ Mode: "Mini",66+ Save: json.RawMessage(`{"width":2,"height":2,"row-hints":[[1],[1]],"col-hints":[[1],[1]],"state":" \n "}`),67+ },68+ }69+ writeSingleJSONLRecord(t, path, record)70+71+ doc, err := ParseJSONLFile(path)72+ if err != nil {73+ t.Fatal(err)74+ }75+ if got, want := len(doc.Puzzles), 1; got != want {76+ t.Fatalf("puzzles = %d, want %d", got, want)77+ }78+ if doc.Puzzles[0].Nonogram == nil {79+ t.Fatal("expected nonogram print payload from save hydration")80+ }81+}82+5083 func TestParseJSONLFileRejectsNonJSONLExtension(t *testing.T) {5184 path := filepath.Join(t.TempDir(), "pack.md")5285 if err := os.WriteFile(path, []byte("x"), 0o644); err != nil {@@ -282,6 +315,71 @@ func TestParseJSONLFileHydratesHashiFromSave(t *testing.T) {
282315 }283316 }284317 318+func TestParseJSONLFileIgnoresMalformedSnippetWhenSaveHydrated(t *testing.T) {319+ path := filepath.Join(t.TempDir(), "sudoku-malformed-snippet.jsonl")320+ record := JSONLRecord{321+ Schema: ExportSchemaV1,322+ Pack: JSONLPackMeta{323+ Generated: "2026-02-22T10:00:00Z",324+ Version: "v-test",325+ Category: "Sudoku",326+ ModeSelection: "Easy",327+ Count: 1,328+ },329+ Puzzle: JSONLPuzzle{330+ Index: 1,331+ Name: "sage-briar",332+ Game: "Sudoku",333+ Mode: "Easy",334+ Save: json.RawMessage(`{"provided":[{"x":0,"y":0,"v":5}]}`),335+ Snippet: "| bad |\n",336+ },337+ }338+ writeSingleJSONLRecord(t, path, record)339+340+ doc, err := ParseJSONLFile(path)341+ if err != nil {342+ t.Fatalf("expected lenient parse when save hydration succeeds, got: %v", err)343+ }344+ if got, want := len(doc.Puzzles), 1; got != want {345+ t.Fatalf("puzzles = %d, want %d", got, want)346+ }347+ if doc.Puzzles[0].Sudoku == nil {348+ t.Fatal("expected sudoku print payload from save hydration")349+ }350+}351+352+func TestParseJSONLFileFailsMalformedSnippetWithoutRenderablePayload(t *testing.T) {353+ path := filepath.Join(t.TempDir(), "lights-malformed-snippet.jsonl")354+ record := JSONLRecord{355+ Schema: ExportSchemaV1,356+ Pack: JSONLPackMeta{357+ Generated: "2026-02-22T10:00:00Z",358+ Version: "v-test",359+ Category: "Lights Out",360+ ModeSelection: "Standard",361+ Count: 1,362+ },363+ Puzzle: JSONLPuzzle{364+ Index: 1,365+ Name: "glow-shore",366+ Game: "Lights Out",367+ Mode: "Standard",368+ Save: json.RawMessage(`{"size":5}`),369+ Snippet: "| bad |\n",370+ },371+ }372+ writeSingleJSONLRecord(t, path, record)373+374+ _, err := ParseJSONLFile(path)375+ if err == nil {376+ t.Fatal("expected parse error when neither save nor snippet produces renderable payload")377+ }378+ if !strings.Contains(err.Error(), "parse printable snippet") {379+ t.Fatalf("unexpected error: %v", err)380+ }381+}382+285383 func writeSingleJSONLRecord(t *testing.T, path string, record JSONLRecord) {286384 t.Helper()287385 data, err := json.Marshal(record)@@ -15,6 +15,14 @@ type nurikabeSave struct {
1515 Clues string `json:"clues"`1616 }1717 18+type nonogramSave struct {19+ State string `json:"state"`20+ Width int `json:"width"`21+ Height int `json:"height"`22+ RowHints [][]int `json:"row-hints"`23+ ColHints [][]int `json:"col-hints"`24+}25+1826 type hashiSave struct {1927 Width int `json:"width"`2028 Height int `json:"height"`@@ -71,6 +79,47 @@ type wordSearchWord struct {
7179 Text string `json:"text"`7280 }7381 82+func ParseNonogramPrintData(saveData []byte) (*NonogramData, error) {83+ if len(strings.TrimSpace(string(saveData))) == 0 {84+ return nil, nil85+ }86+87+ var save nonogramSave88+ if err := json.Unmarshal(saveData, &save); err != nil {89+ return nil, fmt.Errorf("decode nonogram save: %w", err)90+ }91+92+ stateRows := splitNonogramStateRows(save.State)93+94+ width := save.Width95+ if width <= 0 {96+ width = len(save.ColHints)97+ }98+ if width <= 0 {99+ width = maxRuneWidth(stateRows)100+ }101+102+ height := save.Height103+ if height <= 0 {104+ height = len(save.RowHints)105+ }106+ if height <= 0 {107+ height = len(stateRows)108+ }109+110+ if width <= 0 || height <= 0 {111+ return nil, nil112+ }113+114+ return &NonogramData{115+ Width: width,116+ Height: height,117+ RowHints: normalizeNonogramHintRows(save.RowHints, height),118+ ColHints: normalizeNonogramHintRows(save.ColHints, width),119+ Grid: normalizeNonogramStateGrid(stateRows, width, height),120+ }, nil121+}122+74123 func ParseNurikabePrintData(saveData []byte) (*NurikabeData, error) {75124 if len(strings.TrimSpace(string(saveData))) == 0 {76125 return nil, nil@@ -356,6 +405,14 @@ func hydratePuzzlePrintData(p *Puzzle) {
356405 }357406 358407 switch normalizePrintableCategory(p.Category) {408+ case "nonogram":409+ if p.Nonogram != nil {410+ return411+ }412+ nonogram, err := ParseNonogramPrintData(p.SaveData)413+ if err == nil {414+ p.Nonogram = nonogram415+ }359416 case "hashiwokakero":360417 if p.Hashi != nil {361418 return@@ -517,3 +574,76 @@ func parseNurikabeClues(raw string, width, height int) ([][]int, error) {
517574 518575 return clues, nil519576 }577+578+func splitNonogramStateRows(raw string) []string {579+ normalized := strings.ReplaceAll(strings.ReplaceAll(raw, "\r\n", "\n"), "\r", "\n")580+ if normalized == "" {581+ return nil582+ }583+ return strings.Split(normalized, "\n")584+}585+586+func maxRuneWidth(rows []string) int {587+ maxWidth := 0588+ for _, row := range rows {589+ if n := len([]rune(row)); n > maxWidth {590+ maxWidth = n591+ }592+ }593+ return maxWidth594+}595+596+func normalizeNonogramHintRows(src [][]int, size int) [][]int {597+ if size <= 0 {598+ return nil599+ }600+601+ normalized := make([][]int, size)602+ for i := 0; i < size; i++ {603+ if i >= len(src) {604+ normalized[i] = []int{0}605+ continue606+ }607+608+ filtered := make([]int, 0, len(src[i]))609+ for _, value := range src[i] {610+ if value > 0 {611+ filtered = append(filtered, value)612+ }613+ }614+ if len(filtered) == 0 {615+ filtered = []int{0}616+ }617+ normalized[i] = filtered618+ }619+620+ return normalized621+}622+623+func normalizeNonogramStateGrid(rows []string, width, height int) [][]string {624+ if width <= 0 || height <= 0 {625+ return nil626+ }627+628+ grid := make([][]string, height)629+ for y := 0; y < height; y++ {630+ grid[y] = make([]string, width)631+ for x := 0; x < width; x++ {632+ grid[y][x] = " "633+ }634+635+ if y >= len(rows) {636+ continue637+ }638+639+ runes := []rune(rows[y])640+ for x := 0; x < width && x < len(runes); x++ {641+ if runes[x] == ' ' {642+ continue643+ }644+ grid[y][x] = string(runes[x])645+ }646+ }647+648+ return grid649+}@@ -2,6 +2,39 @@ package pdfexport
22 33 import "testing"44 5+func TestParseNonogramPrintData(t *testing.T) {6+ save := []byte(`{"state":" .-\n.. ","row-hints":[[1],[2]],"col-hints":[[2],[1],[1]]}`)7+8+ data, err := ParseNonogramPrintData(save)9+ if err != nil {10+ t.Fatal(err)11+ }12+ if data == nil {13+ t.Fatal("expected nonogram print data")14+ }15+ if got, want := data.Width, 3; got != want {16+ t.Fatalf("width = %d, want %d", got, want)17+ }18+ if got, want := data.Height, 2; got != want {19+ t.Fatalf("height = %d, want %d", got, want)20+ }21+ if got, want := data.RowHints[0][0], 1; got != want {22+ t.Fatalf("row hint[0][0] = %d, want %d", got, want)23+ }24+ if got, want := data.ColHints[0][0], 2; got != want {25+ t.Fatalf("col hint[0][0] = %d, want %d", got, want)26+ }27+ if got, want := data.Grid[0][0], " "; got != want {28+ t.Fatalf("grid[0][0] = %q, want %q", got, want)29+ }30+ if got, want := data.Grid[0][1], "."; got != want {31+ t.Fatalf("grid[0][1] = %q, want %q", got, want)32+ }33+ if got, want := data.Grid[0][2], "-"; got != want {34+ t.Fatalf("grid[0][2] = %q, want %q", got, want)35+ }36+}37+538 func TestParseNurikabePrintData(t *testing.T) {639 save := []byte(`{"width":3,"height":2,"clues":"1,0,2\n0,3,0","marks":"???\n???"}`)740 @@ -167,6 +200,24 @@ func TestHydratePuzzlePrintDataForTakuzu(t *testing.T) {
167200 }168201 }169202 203+func TestHydratePuzzlePrintDataForNonogram(t *testing.T) {204+ p := Puzzle{205+ Category: "Nonogram",206+ SaveData: []byte(`{"width":2,"height":2,"row-hints":[[1],[1]],"col-hints":[[1],[1]],"state":" \n "}`),207+ }208+209+ hydratePuzzlePrintData(&p)210+ if p.Nonogram == nil {211+ t.Fatal("expected nonogram payload from save hydration")212+ }213+ if got, want := p.Nonogram.Width, 2; got != want {214+ t.Fatalf("width = %d, want %d", got, want)215+ }216+ if got, want := p.Nonogram.Height, 2; got != want {217+ t.Fatalf("height = %d, want %d", got, want)218+ }219+}220+170221 func TestHydratePuzzlePrintDataForNurikabeAndShikaku(t *testing.T) {171222 nurikabePuzzle := Puzzle{172223 Category: "Nurikabe",@@ -116,6 +116,7 @@ func renderTitlePage(pdf *fpdf.Fpdf, docs []PackDocument, puzzles []Puzzle, cfg
116116 pdf.AddPage()117117 pageW, pageH := pdf.GetPageSize()118118 margin := 12.0119+ contentWidth := pageW - 2*margin119120 120121 pdf.SetTextColor(20, 20, 20)121122 pdf.SetFont(sansFontFamily, "B", 22)@@ -132,56 +133,109 @@ func renderTitlePage(pdf *fpdf.Fpdf, docs []PackDocument, puzzles []Puzzle, cfg
132133 pdf.SetXY(0, 44)133134 pdf.CellFormat(pageW, 6, "PuzzleTea Puzzle Pack", "", 0, "C", false, 0, "")134135 136+ versionLine := fmt.Sprintf("PuzzleTea Version: %s", strings.Join(summarizeVersions(docs), ", "))137+ pdf.SetFont(sansFontFamily, "", 10)138+ wrappedVersions := pdf.SplitLines([]byte(versionLine), contentWidth)139+ if len(wrappedVersions) == 0 {140+ wrappedVersions = [][]byte{[]byte(versionLine)}141+ }142+143+ headerLineH := 4.8144+ headerGap := 1.2145+ headerStartY := 54.8135146 metaY := 56.0147+ if header := strings.TrimSpace(cfg.HeaderText); header != "" {148+ pdf.SetFont(sansFontFamily, "", 9.2)149+ pdf.SetTextColor(74, 74, 74)150+ wrappedHeader := pdf.SplitLines([]byte(header), contentWidth)151+ if len(wrappedHeader) == 0 {152+ wrappedHeader = [][]byte{[]byte(header)}153+ }154+155+ metaY = headerStartY + float64(len(wrappedHeader))*headerLineH + headerGap156+ sourceStartY := titlePageSourceTableStartY(metaY, len(wrappedVersions))157+ sourceMaxY := pageH - 45158+ if spare := titlePageSourceTableWhitespace(sourceMaxY, sourceStartY, len(docs)); spare > 0 {159+ headerGap += spare160+ }161+162+ headerY := headerStartY163+ for _, line := range wrappedHeader {164+ pdf.SetXY(margin, headerY)165+ pdf.CellFormat(contentWidth, headerLineH, string(line), "", 0, "C", false, 0, "")166+ headerY += headerLineH167+ }168+ metaY = headerY + headerGap169+ }170+136171 pdf.SetTextColor(25, 25, 25)137172 pdf.SetFont(sansFontFamily, "", 10)138173 pdf.SetXY(margin, metaY)139- pdf.CellFormat(pageW-2*margin, 6, fmt.Sprintf("Generated: %s", cfg.GeneratedAt.Format("January 2, 2006")), "", 0, "L", false, 0, "")174+ pdf.CellFormat(contentWidth, 6, fmt.Sprintf("Generated: %s", cfg.GeneratedAt.Format("January 2, 2006")), "", 0, "L", false, 0, "")140175 metaY += 6141- versionLine := fmt.Sprintf("PuzzleTea Version: %s", strings.Join(summarizeVersions(docs), ", "))142- wrappedVersions := pdf.SplitLines([]byte(versionLine), pageW-2*margin)143- if len(wrappedVersions) == 0 {144- wrappedVersions = [][]byte{[]byte(versionLine)}145- }146176 for _, line := range wrappedVersions {147177 pdf.SetXY(margin, metaY)148- pdf.CellFormat(pageW-2*margin, 5.2, string(line), "", 0, "L", false, 0, "")178+ pdf.CellFormat(contentWidth, 5.2, string(line), "", 0, "L", false, 0, "")149179 metaY += 5.2150180 }151181 metaY += 0.8152182 153- categories := summarizeCategories(puzzles)154183 pdf.SetXY(margin, metaY)155- pdf.CellFormat(pageW-2*margin, 6, fmt.Sprintf("Puzzles: %d", len(puzzles)), "", 0, "L", false, 0, "")184+ pdf.CellFormat(contentWidth, 6, fmt.Sprintf("Puzzles: %d", len(puzzles)), "", 0, "L", false, 0, "")156185 metaY += 6157- categoryLine := fmt.Sprintf("Categories: %s", strings.Join(categories, ", "))158- wrappedCategories := pdf.SplitLines([]byte(categoryLine), pageW-2*margin)159- if len(wrappedCategories) == 0 {160- wrappedCategories = [][]byte{[]byte(categoryLine)}161- }162- for _, line := range wrappedCategories {163- pdf.SetXY(margin, metaY)164- pdf.CellFormat(pageW-2*margin, 5.2, string(line), "", 0, "L", false, 0, "")165- metaY += 5.2166- }167- metaY += 2.8186+ metaY += 1.8168187 169188 pdf.SetFont(sansFontFamily, "B", 10)170189 pdf.SetTextColor(45, 45, 45)171190 pdf.SetXY(margin, metaY)172- pdf.CellFormat(pageW-2*margin, 6, "Source Exports", "", 0, "L", false, 0, "")191+ pdf.CellFormat(contentWidth, 6, "Source Exports", "", 0, "L", false, 0, "")173192 metaY += 7174193 175- renderSourceExportsTable(pdf, docs, margin, metaY, pageW-2*margin, pageH-45)194+ renderSourceExportsTable(pdf, docs, margin, metaY, contentWidth, pageH-45)176195 177196 pdf.SetTextColor(50, 50, 50)178197 pdf.SetFont(sansFontFamily, "B", 12)179198 pdf.SetXY(margin, pageH-30)180- pdf.CellFormat(pageW-2*margin, 7, "Made with PuzzleTea", "", 0, "C", false, 0, "")199+ pdf.CellFormat(contentWidth, 7, "Made with PuzzleTea", "", 0, "C", false, 0, "")181200 182201 pdf.SetFont(sansFontFamily, "", 10)183202 pdf.SetXY(margin, pageH-22)184- pdf.CellFormat(pageW-2*margin, 6, cfg.AdvertText, "", 0, "C", false, 0, "")203+ pdf.CellFormat(contentWidth, 6, cfg.AdvertText, "", 0, "C", false, 0, "")204+}205+206+func titlePageSourceTableStartY(metaY float64, versionLineCount int) float64 {207+ y := metaY208+ y += 6209+ y += float64(max(versionLineCount, 1)) * 5.2210+ y += 0.8211+ y += 6212+ y += 1.8213+ y += 7214+ return y215+}216+217+func titlePageSourceTableWhitespace(maxY, sourceStartY float64, docCount int) float64 {218+ const (219+ headerHeight = 5.2220+ rowHeight = 4.8221+ )222+223+ availableRowsHeight := maxY - sourceStartY - headerHeight224+ if availableRowsHeight < rowHeight {225+ return 0226+ }227+228+ maxRows := int(math.Floor(availableRowsHeight / rowHeight))229+ if maxRows < 1 {230+ return 0231+ }232+233+ rowCount := min(docCount, maxRows)234+ used := headerHeight + float64(rowCount)*rowHeight235+ if spare := maxY - sourceStartY - used; spare > 0 {236+ return spare237+ }238+ return 0185239 }186240 187241 func renderPuzzlePage(pdf *fpdf.Fpdf, puzzle Puzzle) {@@ -961,27 +1015,6 @@ func drawNonogramMajorLines(
9611015 }9621016 }9631017 964-func summarizeCategories(puzzles []Puzzle) []string {965- set := map[string]struct{}{}966- for _, p := range puzzles {967- category := strings.TrimSpace(p.Category)968- if category == "" {969- continue970- }971- set[category] = struct{}{}972- }973-974- categories := make([]string, 0, len(set))975- for category := range set {976- categories = append(categories, category)977- }978- sort.Strings(categories)979- if len(categories) == 0 {980- return []string{"Unknown"}981- }982- return categories983-}984-9851018 func summarizeVersions(docs []PackDocument) []string {9861019 set := map[string]struct{}{}9871020 for _, doc := range docs {@@ -3,7 +3,6 @@ package pdfexport
33 import (44 "fmt"55 "hash/fnv"6- "math"76 "math/rand"87 "strings"98 @@ -82,7 +81,7 @@ func renderCoverPage(pdf *fpdf.Fpdf, _ []Puzzle, cfg RenderConfig) {
8281 drawCoverFrame(pdf, frameInset, pageW, pageH, ink)8382 8483 scene := rectMM{x: frameInset + 4.0, y: frameInset + 10.0, w: pageW - (frameInset+4.0)*2, h: 132.0}85- drawWoodcutScene(pdf, scene, coverColor, ink, cfg.ShuffleSeed)84+ drawCoverArtwork(pdf, scene, cfg.ShuffleSeed, coverColor, ink)8685 8786 subtitle := strings.TrimSpace(cfg.CoverSubtitle)8887 if subtitle == "" {@@ -129,197 +128,16 @@ func drawCoverFrame(pdf *fpdf.Fpdf, inset, pageW, pageH float64, ink RGB) {
129128 pdf.Rect(inner, inner, pageW-2*inner, pageH-2*inner, "D")130129 }131130 132-func drawWoodcutScene(pdf *fpdf.Fpdf, scene rectMM, bg, ink RGB, seed string) {133- drawStippleTexture(pdf, scene, ink, seed)134- drawHatchingBands(pdf, scene, ink)135- drawPineForestMass(pdf, scene, ink)136- drawIsometricRuins(pdf, scene, bg, ink)137- drawSkullMotifs(pdf, scene, bg, ink)138-}139-140-func drawStippleTexture(pdf *fpdf.Fpdf, scene rectMM, ink RGB, seed string) {141- h := fnv.New64a()142- h.Write([]byte(strings.TrimSpace(seed) + "-stipple"))143- rng := rand.New(rand.NewSource(int64(h.Sum64())))144-145- pdf.SetFillColor(int(ink.R), int(ink.G), int(ink.B))146- for x := scene.x + 1.0; x < scene.x+scene.w-1.0; x += 3.1 {147- for y := scene.y + 2.0; y < scene.y+scene.h*0.58; y += 3.2 {148- if rng.Float64() < 0.22 {149- jitterX := (rng.Float64() - 0.5) * 0.9150- jitterY := (rng.Float64() - 0.5) * 0.9151- pdf.Circle(x+jitterX, y+jitterY, 0.11, "F")152- }153- }154- }155-}156-157-func drawHatchingBands(pdf *fpdf.Fpdf, scene rectMM, ink RGB) {158- pdf.SetDrawColor(int(ink.R), int(ink.G), int(ink.B))159- pdf.SetLineWidth(0.28)160-161- baseY := scene.y + scene.h*0.64162- for i := -8; i < 34; i++ {163- x0 := scene.x + float64(i)*4.4164- y0 := baseY165- x1 := x0 + scene.h*0.55166- y1 := baseY + scene.h*0.30167- pdf.Line(x0, y0, x1, y1)168- }169-170- for i := -8; i < 30; i++ {171- x0 := scene.x + float64(i)*5.0172- y0 := baseY + 7.5173- x1 := x0 + scene.h*0.48174- y1 := baseY + scene.h*0.33175- pdf.Line(x0, y0, x1, y1)176- }177-}131+func drawCoverArtwork(pdf *fpdf.Fpdf, scene rectMM, seed string, bg, ink RGB) {132+ drawCoverArtworkImage(pdf, scene, seed, "front", bg)178133 179-func drawPineForestMass(pdf *fpdf.Fpdf, scene rectMM, ink RGB) {180- pdf.SetFillColor(int(ink.R), int(ink.G), int(ink.B))181134 pdf.SetDrawColor(int(ink.R), int(ink.G), int(ink.B))135+ pdf.SetLineWidth(0.40)136+ pdf.Rect(scene.x, scene.y, scene.w, scene.h, "D")182137 183- layers := []struct {184- y float64185- count int186- w float64187- h float64188- offset float64189- }{190- {y: scene.y + scene.h*0.45, count: 22, w: 4.4, h: 13.0, offset: 0.8},191- {y: scene.y + scene.h*0.53, count: 19, w: 5.4, h: 16.0, offset: 1.4},192- {y: scene.y + scene.h*0.62, count: 16, w: 6.2, h: 18.4, offset: 2.0},193- }194-195- for _, layer := range layers {196- step := scene.w / float64(layer.count)197- for i := 0; i < layer.count; i++ {198- cx := scene.x + float64(i)*step + layer.offset199- pts := []fpdf.PointType{200- {X: cx, Y: layer.y - layer.h},201- {X: cx - layer.w, Y: layer.y},202- {X: cx + layer.w, Y: layer.y},203- }204- pdf.Polygon(pts, "F")205- pdf.SetLineWidth(0.22)206- pdf.Line(cx, layer.y, cx, layer.y+2.4)207- }208- }209-}210-211-func drawIsometricRuins(pdf *fpdf.Fpdf, scene rectMM, bg, ink RGB) {212- pdf.SetDrawColor(int(ink.R), int(ink.G), int(ink.B))213- pdf.SetFillColor(int(ink.R), int(ink.G), int(ink.B))214-215- centerX := scene.x + scene.w*0.52216- baseY := scene.y + scene.h*0.80217- drawIsometricGrid(pdf, centerX-44, baseY-17, 88, 34, ink)218-219- drawStoneArch(pdf, centerX-22, baseY-35, 25, 27, 8, bg, ink)220- drawStoneArch(pdf, centerX+8, baseY-48, 20, 23, 7, bg, ink)221-222- drawRubbleBlock(pdf, centerX-39, baseY-6, 9, 5, 5, ink)223- drawRubbleBlock(pdf, centerX+31, baseY-3, 10, 5, 6, ink)224- drawRubbleBlock(pdf, centerX+12, baseY+5, 8, 4, 4, ink)225-}226-227-func drawIsometricGrid(pdf *fpdf.Fpdf, x, y, w, h float64, ink RGB) {228- pdf.SetDrawColor(int(ink.R), int(ink.G), int(ink.B))229- pdf.SetLineWidth(0.26)230-231- for i := -8; i < 14; i++ {232- sx := x + float64(i)*4233- pdf.Line(sx, y, sx+w*0.5, y+h*0.5)234- }235- for i := -8; i < 14; i++ {236- sx := x + w + float64(i)*4237- pdf.Line(sx, y, sx-w*0.5, y+h*0.5)238- }239-}240-241-func drawStoneArch(pdf *fpdf.Fpdf, x, y, w, h, depth float64, bg, ink RGB) {242- frontLeft := []fpdf.PointType{{X: x, Y: y}, {X: x + w*0.18, Y: y - depth}, {X: x + w*0.18, Y: y + h - depth}, {X: x, Y: y + h}}243- frontRight := []fpdf.PointType{{X: x + w*0.82, Y: y - depth}, {X: x + w, Y: y}, {X: x + w, Y: y + h}, {X: x + w*0.82, Y: y + h - depth}}244-245- pdf.SetFillColor(int(ink.R), int(ink.G), int(ink.B))246- pdf.Polygon(frontLeft, "F")247- pdf.Polygon(frontRight, "F")248-249- outerR := w * 0.50250- innerR := w * 0.33251- cx := x + w*0.5252- cy := y + h*0.42253- drawArcPolyline(pdf, cx, cy, outerR, math.Pi, 2*math.Pi, 24)254-255- pdf.SetDrawColor(int(bg.R), int(bg.G), int(bg.B))256- pdf.SetLineWidth(2.0)257- drawArcPolyline(pdf, cx, cy, innerR, math.Pi, 2*math.Pi, 24)258- pdf.Line(x+w*0.33, y+h, x+w*0.33, cy)259- pdf.Line(x+w*0.67, y+h-depth*0.2, x+w*0.67, cy)260-261- pdf.SetDrawColor(int(ink.R), int(ink.G), int(ink.B))262- pdf.SetLineWidth(0.28)263- for i := 0; i < 6; i++ {264- sy := y + 3 + float64(i)*4.2265- pdf.Line(x+1.5, sy, x+w*0.16, sy-0.6)266- pdf.Line(x+w*0.84, sy-0.5, x+w-1.5, sy+0.4)267- }268-}269-270-func drawArcPolyline(pdf *fpdf.Fpdf, cx, cy, r, start, end float64, segments int) {271- if segments < 2 {272- segments = 2273- }274- step := (end - start) / float64(segments)275- px := cx + math.Cos(start)*r276- py := cy + math.Sin(start)*r277- for i := 1; i <= segments; i++ {278- a := start + float64(i)*step279- x := cx + math.Cos(a)*r280- y := cy + math.Sin(a)*r281- pdf.Line(px, py, x, y)282- px, py = x, y283- }284-}285-286-func drawRubbleBlock(pdf *fpdf.Fpdf, x, y, w, h, skew float64, ink RGB) {287- pdf.SetFillColor(int(ink.R), int(ink.G), int(ink.B))288- pts := []fpdf.PointType{289- {X: x, Y: y},290- {X: x + w, Y: y - skew*0.2},291- {X: x + w + skew, Y: y + h},292- {X: x + skew, Y: y + h + skew*0.2},293- }294- pdf.Polygon(pts, "F")295-}296-297-func drawSkullMotifs(pdf *fpdf.Fpdf, scene rectMM, bg, ink RGB) {298- centers := []struct {299- x float64300- y float64301- r float64302- }{303- {x: scene.x + scene.w*0.30, y: scene.y + scene.h*0.84, r: 2.9},304- {x: scene.x + scene.w*0.69, y: scene.y + scene.h*0.87, r: 2.4},305- }306-307- for _, c := range centers {308- pdf.SetDrawColor(int(ink.R), int(ink.G), int(ink.B))309- pdf.SetFillColor(int(ink.R), int(ink.G), int(ink.B))310- pdf.Circle(c.x, c.y, c.r, "F")311-312- pdf.SetFillColor(int(bg.R), int(bg.G), int(bg.B))313- pdf.Circle(c.x-c.r*0.35, c.y-c.r*0.1, c.r*0.22, "F")314- pdf.Circle(c.x+c.r*0.35, c.y-c.r*0.08, c.r*0.20, "F")315- jaw := []fpdf.PointType{{X: c.x - c.r*0.48, Y: c.y + c.r*0.2}, {X: c.x + c.r*0.48, Y: c.y + c.r*0.2}, {X: c.x, Y: c.y + c.r*0.72}}316- pdf.Polygon(jaw, "F")317-318- pdf.SetDrawColor(int(ink.R), int(ink.G), int(ink.B))319- pdf.SetLineWidth(0.22)320- pdf.Line(c.x-c.r*0.6, c.y-c.r*0.85, c.x-c.r*1.15, c.y-c.r*1.45)321- pdf.Line(c.x+c.r*0.5, c.y-c.r*0.7, c.x+c.r*1.0, c.y-c.r*1.3)322- }138+ pdf.SetLineWidth(0.20)139+ inset := 1.8140+ pdf.Rect(scene.x+inset, scene.y+inset, scene.w-2*inset, scene.h-2*inset, "D")323141 }324142 325143 func renderBackCoverPage(pdf *fpdf.Fpdf, cfg RenderConfig, coverColor RGB) {@@ -334,7 +152,7 @@ func renderBackCoverPage(pdf *fpdf.Fpdf, cfg RenderConfig, coverColor RGB) {
334152 drawCoverFrame(pdf, frameInset, pageW, pageH, ink)335153 336154 motif := rectMM{x: frameInset + 5.5, y: frameInset + 14.0, w: pageW - 2*(frameInset+5.5), h: 96.0}337- drawBackMotif(pdf, motif, coverColor, ink)155+ drawBackMotif(pdf, motif, cfg.ShuffleSeed, coverColor, ink)338156 339157 labelW := pageW - 2*(frameInset+6.0)340158 pdf.SetTextColor(int(ink.R), int(ink.G), int(ink.B))@@ -351,10 +169,10 @@ func renderBackCoverPage(pdf *fpdf.Fpdf, cfg RenderConfig, coverColor RGB) {
351169 pdf.CellFormat(labelW, 4.2, fmt.Sprintf("VOL. %02d", cfg.VolumeNumber), "", 0, "L", false, 0, "")352170 }353171 354-func drawBackMotif(pdf *fpdf.Fpdf, scene rectMM, bg, ink RGB) {355- drawStippleTexture(pdf, scene, ink, "back-cover")356- drawHatchingBands(pdf, scene, ink)357- drawPineForestMass(pdf, scene, ink)358- drawStoneArch(pdf, scene.x+scene.w*0.58, scene.y+scene.h*0.56, 22, 24, 7, bg, ink)359- drawRubbleBlock(pdf, scene.x+scene.w*0.28, scene.y+scene.h*0.72, 8, 4, 4, ink)172+func drawBackMotif(pdf *fpdf.Fpdf, scene rectMM, seed string, bg, ink RGB) {173+ drawCoverArtworkImage(pdf, scene, seed, "back", bg)174+175+ pdf.SetDrawColor(int(ink.R), int(ink.G), int(ink.B))176+ pdf.SetLineWidth(0.34)177+ pdf.Rect(scene.x, scene.y, scene.w, scene.h, "D")360178 }@@ -115,6 +115,7 @@ type RGB struct{ R, G, B uint8 }
115115 type RenderConfig struct {116116 Title string117117 CoverSubtitle string118+ HeaderText string118119 VolumeNumber int119120 AdvertText string120121 GeneratedAt time.Time