// Command screenshots regenerates the README images in screenshots/ from the // sample script "The Last Reel". Run it from the repository root with // `make screenshots`, which builds the frontend and installs puppeteer-core // first. // // It serves frontend/dist with a stand-in for the Wails bridge whose calls are // answered by the real internal packages, so pagination, locked pages and // revision sets on the images are real. capture.mjs drives Firefox through it // (1440×900, device scale 2); the window corners are then masked, since the // browser cannot capture a transparent background. pdf-output.png is the // PDF export of the script, rendered by pdftoppm and laid out on the app // background. package main import ( "bytes" "encoding/json" "flag" "fmt" "image" "image/color" "image/draw" "image/png" "log" "math" "net" "net/http" "os" "os/exec" "path/filepath" "strings" "plotzilla/internal/fountain" "plotzilla/internal/layout" "plotzilla/internal/pdf" "plotzilla/internal/screenplay" "plotzilla/internal/spell" ) const ( dist = "frontend/dist" sample = "tools/screenshots/the-last-reel.fountain" ) // bridge replaces the Wails runtime in the page. Each call is posted to // /api/, with the page's query string naming the script variant. const bridge = ` ` // typos are the misspellings of the "typos" variant, with their // corrections: spelling.png needs no Hunspell dictionary. var typos = map[string][]string{"projecter": {"projector", "projected"}, "thraeds": {"threads"}, "wacth": {"watch"}} // variant returns the sample script, or one of its variants: "typos" for the // spelling image, "outline" (sections, synopses, a note) for the outline and // revision images. func variant(src, name string) string { var pairs []string switch name { case "typos": pairs = []string{ "of the projector. ELODIE", "of the projecter. ELODIE", "threads a reel by hand", "thraeds a reel by hand", "Soldiers watch from", "Soldiers wacth from", } case "outline": pairs = []string{ "FADE IN:\n", "# Act One\n\n= Elodie finds the reel her grandmother hid in 1942.\n\nFADE IN:\n", "of the projector. ELODIE", "of the projector. [[Warmer light here?]] ELODIE", "> PARIS, 1942 <\n", "> PARIS, 1942 <\n\n## The Rialto\n\n= The young woman smuggles the can into the cinema.\n", "MATCH CUT TO:\n", "MATCH CUT TO:\n\n# Act Two\n", } } for i := 0; i < len(pairs); i += 2 { src = strings.Replace(src, pairs[i], pairs[i+1], 1) } return src } type server struct { src string // the sample script path string // its absolute path, shown as the file name } func (s *server) call(name, v string, raw []json.RawMessage) any { doc := func() *screenplay.Document { var d screenplay.Document if len(raw) > 0 { json.Unmarshal(raw[0], &d) } return &d } switch name { case "NewDocument": return screenplay.New() case "InitialFile": return map[string]any{"path": s.path, "document": fountain.Parse(variant(s.src, v))} case "Save", "SaveAs": return s.path // nothing is written case "Paginate": res := layout.Paginate(doc(), layout.Options{}) return map[string]any{"pages": len(res.Pages), "breaks": res.Breaks, "scenes": res.Scenes} case "ParseFountain": var text string json.Unmarshal(raw[0], &text) return fountain.ParseText(text) case "RevisionSet": var n int json.Unmarshal(raw[0], &n) r := screenplay.RevisionSet(n) r.Date = []string{"09/14/26", "09/28/26"}[min(max(n, 1), 2)-1] return r case "WindowFrame": return map[string]any{"buttons": "appmenu:close", "rounded": true} case "SpellLanguages": return map[string]any{"languages": []string{"en_US"}, "default": "en_US"} case "SpellCheck": out := []spell.Misspelling{} for i, el := range doc().Elements { for _, w := range spell.Words(el.Text()) { if _, ok := typos[w.Text]; ok { out = append(out, spell.Misspelling{Element: i, Offset: w.Offset, Length: w.Length, Word: w.Text}) } } } return out case "SpellSuggest": var word string json.Unmarshal(raw[0], &word) return typos[word] } return nil // Recovery, SetDirty… } func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { if name, ok := strings.CutPrefix(r.URL.Path, "/api/"); ok { var raw []json.RawMessage json.NewDecoder(r.Body).Decode(&raw) json.NewEncoder(w).Encode(s.call(name, r.URL.Query().Get("v"), raw)) return } if r.URL.Path == "/" { html, err := os.ReadFile(filepath.Join(dist, "index.html")) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html") fmt.Fprint(w, strings.Replace(string(html), `