cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / e2e / stillboxes_test.go
stillboxes_test.go 8.2K
1package e2e23import (4 "image"5 "image/color"6 "math"7)89// Box drawing and block elements, drawn to the cell's edges as10// terminals draw them, so a frame's lines meet across cells.1112// boxArms gives each box-drawing character the weight of its four arms,13// up, right, down and left: 0 none, 1 light, 2 heavy, 3 double.14var boxArms = map[rune]string{15 '─': "0101", '━': "0202", '│': "1010", '┃': "2020",16 '╴': "0001", '╵': "1000", '╶': "0100", '╷': "0010",17 '╸': "0002", '╹': "2000", '╺': "0200", '╻': "0020",18 '╼': "0201", '╽': "1020", '╾': "0102", '╿': "2010",19 '╭': "0110", '╮': "0011", '╯': "1001", '╰': "1100",20}2122// boxRuns are the characters U+250C to U+254B and U+2550 to U+256C, in23// order, as their arms.24const (25 boxRun1 = "0110 0210 0120 0220 0011 0012 0021 0022 1100 1200 2100 2200 1001 1002 2001 2002 " +26 "1110 1210 2110 1120 2120 2210 1220 2220 1011 1012 2011 1021 2021 2012 1022 2022 " +27 "0111 0112 0211 0212 0121 0122 0221 0222 1101 1102 1201 1202 2101 2102 2201 2202 " +28 "1111 1112 1211 1212 2111 1121 2121 2112 2211 1122 1221 2212 1222 2122 2221 2222"29 boxRun2 = "0303 3030 0310 0130 0330 0013 0031 0033 1300 3100 3300 1003 3001 3003 " +30 "1310 3130 3330 1013 3031 3033 0313 0131 0333 1303 3101 3303 1313 3131 3333"31)3233// boxDashes are the dashed lines: vertical or not, heavy or not, and34// how many dashes a cell.35var boxDashes = map[rune]struct {36 vertical, heavy bool37 n int38}{39 '┄': {false, false, 3}, '┅': {false, true, 3}, '┆': {true, false, 3}, '┇': {true, true, 3},40 '┈': {false, false, 4}, '┉': {false, true, 4}, '┊': {true, false, 4}, '┋': {true, true, 4},41 '╌': {false, false, 2}, '╍': {false, true, 2}, '╎': {true, false, 2}, '╏': {true, true, 2},42}4344func init() {45 for i, arms := range splitFields(boxRun1) {46 boxArms[rune(0x250c+i)] = arms47 }48 for i, arms := range splitFields(boxRun2) {49 boxArms[rune(0x2550+i)] = arms50 }51}5253func splitFields(s string) []string {54 var out []string55 for i := 0; i+4 <= len(s); i += 5 {56 out = append(out, s[i:i+4])57 }58 return out59}6061// boxCell is one cell being drawn: its rectangle, center and the widths62// of its lines.63type boxCell struct {64 img *image.NRGBA65 cell image.Rectangle66 c color.RGBA67 mx, my float32 // the center68 t, d float32 // a light line's width; a double line's offset from center69 w [4]int // arms' weights70}7172// boxDrawing draws U+2500 to U+257F; false for one it doesn't know.73func boxDrawing(img *image.NRGBA, r rune, cell image.Rectangle, c color.RGBA) bool {74 t := float32(lineWidth(cell.Dx()))75 b := boxCell{img: img, cell: cell, c: c, t: t, d: t * 2,76 mx: float32(cell.Min.X) + float32(cell.Dx())/2, my: float32(cell.Min.Y) + float32(cell.Dy())/2}77 if dash, ok := boxDashes[r]; ok {78 b.dashes(dash.vertical, dash.heavy, dash.n)79 return true80 }81 if r >= '╱' && r <= '╳' {82 b.diagonal(r)83 return true84 }85 arms, ok := boxArms[r]86 if !ok {87 return false88 }89 for i := range b.w {90 b.w[i] = int(arms[i] - '0')91 }92 if r >= '╭' && r <= '╰' {93 b.rounded(r)94 return true95 }96 for i := range b.w {97 b.arm(i)98 }99 return true100}101102func (b *boxCell) thickness(weight int) float32 {103 if weight == 2 {104 return 2.5 * b.t105 }106 return b.t107}108109// rect fills the part of arm i from s0 to its cell edge, s measured from110// the center outward, and q across it (down or right positive).111func (b *boxCell) rect(i int, s0, q0, q1 float32) {112 half := float32(b.cell.Dx()) / 2113 if i%2 == 0 {114 half = float32(b.cell.Dy()) / 2115 }116 var x0, y0, x1, y1 float32117 switch i {118 case 0:119 x0, x1, y0, y1 = b.mx+q0, b.mx+q1, b.my-half, b.my-s0120 case 1:121 x0, x1, y0, y1 = b.mx+s0, b.mx+half, b.my+q0, b.my+q1122 case 2:123 x0, x1, y0, y1 = b.mx+q0, b.mx+q1, b.my+s0, b.my+half124 default:125 x0, x1, y0, y1 = b.mx-half, b.mx-s0, b.my+q0, b.my+q1126 }127 rd := func(v float32) int { return int(math.Round(float64(v))) }128 fill(b.img, image.Rect(rd(x0), rd(y0), rd(x1), rd(y1)), b.c)129}130131// arm draws arm i, joined to the lines across it.132func (b *boxCell) arm(i int) {133 k := b.w[i]134 if k == 0 {135 return136 }137 perpA, perpB, opp := b.w[(i+1)%4], b.w[(i+3)%4], b.w[(i+2)%4]138 if k != 3 {139 th := b.thickness(k)140 s0 := -th / 2141 switch perp := max(perpA, perpB); {142 case perp == 3 && opp == 0 && min(perpA, perpB) == 0:143 s0 = -b.d - b.t/2 // a corner: reach the far line144 case perp == 3 && opp == 0:145 s0 = b.d - b.t/2 // a tee: stop at the near line146 case perp == 3:147 s0 = -b.d - b.t/2148 case perp > 0:149 s0 = -b.thickness(perp) / 2150 }151 b.rect(i, s0, -th/2, th/2)152 return153 }154 for _, o := range []float32{-1, 1} {155 b.rect(i, b.doubleStart(i, o), o*b.d-b.t/2, o*b.d+b.t/2)156 }157}158159// doubleStart is where the line of double arm i on side o starts: at160// the inner line of a double arm on its side, the outer line of one on161// the other side, or the line across it.162func (b *boxCell) doubleStart(i int, o float32) float32 {163 side, other := 2, 0 // for a horizontal arm, o > 0 is down164 if i%2 == 0 {165 side, other = 1, 3166 }167 if o < 0 {168 side, other = other, side169 }170 switch {171 case b.w[side] == 3:172 return b.d - b.t/2173 case b.w[other] == 3:174 return -b.d - b.t/2175 case b.w[side] > 0 || b.w[other] > 0:176 return -b.thickness(max(b.w[side], b.w[other])) / 2177 }178 return -b.t / 2179}180181// rounded draws ╭ ╮ ╯ ╰: a quarter circle between two arms.182func (b *boxCell) rounded(r rune) {183 w, h := float32(b.cell.Dx()), float32(b.cell.Dy())184 cx, cy, rad := w/2, h/2, w/2185 var a0, a1, ox, oy float32186 switch r {187 case '╭':188 ox, oy, a0, a1 = rad, rad, 1.5*math.Pi, math.Pi189 case '╮':190 ox, oy, a0, a1 = -rad, rad, -math.Pi/2, 0191 case '╯':192 ox, oy, a0, a1 = -rad, -rad, math.Pi/2, 0193 default:194 ox, oy, a0, a1 = rad, -rad, math.Pi/2, math.Pi195 }196 fillShape(b.img, b.cell, b.c, func(s *shape) {197 pts := arc(cx+ox, cy+oy, rad, a0, a1, 16)198 if oy > 0 {199 pts = append(pts, pt{cx, h + 1})200 } else {201 pts = append(pts, pt{cx, -1})202 }203 s.stroke(pts, b.t)204 })205}206207func (b *boxCell) diagonal(r rune) {208 w, h := float32(b.cell.Dx()), float32(b.cell.Dy())209 fillShape(b.img, b.cell, b.c, func(s *shape) {210 if r != '╲' {211 s.seg(pt{0, h}, pt{w, 0}, b.t)212 }213 if r != '╱' {214 s.seg(pt{0, 0}, pt{w, h}, b.t)215 }216 })217}218219// dashes draws a dashed line of n dashes a cell.220func (b *boxCell) dashes(vertical, heavy bool, n int) {221 th := b.thickness(1)222 if heavy {223 th = b.thickness(2)224 }225 length, i := float32(b.cell.Dx()), 1226 if vertical {227 length, i = float32(b.cell.Dy()), 2228 }229 step := length / float32(n)230 for k := range n {231 s0 := -length/2 + step*float32(k) + step/4232 b.segment(i, s0, s0+step/2, th)233 }234}235236// segment fills a stretch of the line along arm i's axis from s0 to s1,237// measured from the center.238func (b *boxCell) segment(i int, s0, s1, th float32) {239 rd := func(v float32) int { return int(math.Round(float64(v))) }240 if i == 2 {241 fill(b.img, image.Rect(rd(b.mx-th/2), rd(b.my+s0), rd(b.mx+th/2), rd(b.my+s1)), b.c)242 return243 }244 fill(b.img, image.Rect(rd(b.mx+s0), rd(b.my-th/2), rd(b.mx+s1), rd(b.my+th/2)), b.c)245}246247// quadrants are U+2596 to U+259F as the quarters they fill: 1 upper248// left, 2 upper right, 4 lower left, 8 lower right.249var quadrants = [10]int{4, 8, 1, 1 | 4 | 8, 1 | 8, 1 | 2 | 4, 1 | 2 | 8, 2, 2 | 4, 2 | 4 | 8}250251// blockElement draws U+2580 to U+259F.252func blockElement(img *image.NRGBA, r rune, cell image.Rectangle, c color.RGBA) bool {253 x0, y0, w, h := cell.Min.X, cell.Min.Y, cell.Dx(), cell.Dy()254 ex := func(n int) int { return x0 + (w*n+4)/8 } // n eighths across255 ey := func(n int) int { return y0 + (h*n+4)/8 } // n eighths down256 box := func(xa, ya, xb, yb int) { fill(img, image.Rect(xa, ya, xb, yb), c) }257 switch {258 case r == '▀':259 box(x0, y0, cell.Max.X, ey(4))260 case r >= '▁' && r <= '█':261 box(x0, ey(8-int(r-'▀')), cell.Max.X, cell.Max.Y)262 case r >= '▉' && r <= '▏':263 box(x0, y0, ex(8-int(r-'█')), cell.Max.Y)264 case r == '▐':265 box(ex(4), y0, cell.Max.X, cell.Max.Y)266 case r >= '░' && r <= '▓':267 fill(img, cell, color.NRGBA{c.R, c.G, c.B, uint8(64 * (r - '░' + 1))})268 case r == '▔':269 box(x0, y0, cell.Max.X, ey(1))270 case r == '▕':271 box(ex(7), y0, cell.Max.X, cell.Max.Y)272 default:273 q := quadrants[r-'▖']274 for bit, qr := range [4][4]int{{x0, y0, ex(4), ey(4)}, {ex(4), y0, cell.Max.X, ey(4)}, {x0, ey(4), ex(4), cell.Max.Y}, {ex(4), ey(4), cell.Max.X, cell.Max.Y}} {275 if q&(1<<bit) != 0 {276 box(qr[0], qr[1], qr[2], qr[3])277 }278 }279 }280 return true281}