view.gno
3.21 Kb · 115 lines
1package gno4
2
3import (
4 "strings"
5
6 "gno.land/p/nt/svg/v0"
7 "gno.land/p/nt/ufmt/v0"
8)
9
10// Board geometry, in SVG user units. Everything else is derived, so changing
11// cell changes the whole drawing and nothing goes out of step.
12const (
13 pad = 10
14 cell = 74
15 hole = 30
16
17 svgW = Cols*cell + 2*pad
18 svgH = Rows*cell + 2*pad
19 labY = svgH + 22
20)
21
22// The palette. Fixed hex rather than CSS variables: gnoweb renders the SVG
23// inside a data URI, which is its own document and inherits nothing from the
24// page, so a theme-aware colour would resolve to nothing at all.
25const (
26 colBoard = "#1d4ed8"
27 colHole = "#f8fafc"
28 colRed = "#ef4444"
29 colYellow = "#facc15"
30 colLabel = "#64748b"
31 colWin = "#0f172a"
32)
33
34// SVG returns the board as standalone <svg> markup.
35//
36// It is a drawing of a position, not a control: there is no script, no link
37// and no interactivity, because gnoweb serves this through a data URI and
38// would not run any of it. The clickable column menu is the realm's job, as
39// ordinary markdown links next to the image.
40func (b *Board) SVG() string { return b.canvas().String() }
41
42// Image returns the board as a markdown image, ready to drop into a realm's
43// Render output. The SVG travels inline as a data URI, which gnoweb allows for
44// image/svg+xml specifically, so the board needs no hosting and no asset
45// pipeline: there is nothing to go missing later, and no storage is spent on
46// it because it is drawn at render time rather than stored.
47func (b *Board) Image(alt string) string { return b.canvas().Render(alt) }
48
49func (b *Board) canvas() *svg.Canvas {
50 c := svg.NewCanvas(svgW, svgH+30).WithViewBox(0, 0, svgW, svgH+30)
51 c.AddStyle(".win", "stroke:"+colWin+";stroke-width:5")
52 c.AddStyle(".last", "stroke:#ffffff;stroke-width:4")
53 c.AddStyle(".lab", "font:600 22px system-ui,sans-serif;text-anchor:middle")
54
55 c.Append(svg.NewRectangle(0, 0, svgW, svgH, colBoard))
56
57 last := -1
58 if n := len(b.moves); n > 0 {
59 lastCol := b.moves[n-1]
60 last = idx(lastCol, b.Height(lastCol)-1)
61 }
62 win := b.line
63
64 for row := Rows - 1; row >= 0; row-- {
65 for col := 0; col < Cols; col++ {
66 cx := pad + col*cell + cell/2
67 cy := pad + (Rows-1-row)*cell + cell/2
68 fill := colHole
69 switch b.At(col, row) {
70 case Red:
71 fill = colRed
72 case Yellow:
73 fill = colYellow
74 }
75 disc := svg.NewCircle(cx, cy, hole, fill)
76 switch {
77 case contains(win, idx(col, row)):
78 disc.WithClass("win")
79 case idx(col, row) == last:
80 disc.WithClass("last")
81 }
82 c.Append(disc)
83 }
84 }
85
86 for col := 0; col < Cols; col++ {
87 x := pad + col*cell + cell/2
88 c.Append(svg.NewText(x, labY, ufmt.Sprintf("%d", col+1), colLabel).WithClass("lab"))
89 }
90
91 return c
92}
93
94// Emoji returns the board as seven columns of coloured squares, top row first.
95//
96// The text fallback, for the places an image cannot go: a chain event, a
97// terminal, a commit message, a diff. It is also what makes the whole package
98// legible in a test failure.
99func (b *Board) Emoji() string {
100 var sb strings.Builder
101 for row := Rows - 1; row >= 0; row-- {
102 for col := 0; col < Cols; col++ {
103 switch b.At(col, row) {
104 case Red:
105 sb.WriteString("🔴")
106 case Yellow:
107 sb.WriteString("🟡")
108 default:
109 sb.WriteString("⚫")
110 }
111 }
112 sb.WriteByte('\n')
113 }
114 return sb.String()
115}