Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}