Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}