tessella 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +900 -0
- data/doc/README.md +74 -0
- data/doc/bitfield.md +123 -0
- data/doc/charts.md +1370 -0
- data/doc/check.rb +75 -0
- data/doc/cli.md +483 -0
- data/doc/composition.md +599 -0
- data/doc/getting-started.md +121 -0
- data/doc/glyphs.md +134 -0
- data/doc/images.md +325 -0
- data/doc/library.md +239 -0
- data/doc/lint.md +270 -0
- data/doc/meter.md +210 -0
- data/doc/rule.md +51 -0
- data/doc/seq.md +169 -0
- data/doc/sparkline.md +63 -0
- data/doc/table.md +347 -0
- data/doc/targets.md +222 -0
- data/doc/text.md +84 -0
- data/doc/tree.md +108 -0
- data/doc/wave.md +122 -0
- data/examples/README.md +103 -0
- data/examples/ages.txt +9 -0
- data/examples/bad.csv +3 -0
- data/examples/boxes.txt +3 -0
- data/examples/broken.txt +4 -0
- data/examples/calibration.rb +12 -0
- data/examples/cities.csv +4 -0
- data/examples/cjk.rb +13 -0
- data/examples/comment.rb +9 -0
- data/examples/comment.sh +13 -0
- data/examples/cpu.txt +60 -0
- data/examples/dashboard.sh +25 -0
- data/examples/datasheet.rb +16 -0
- data/examples/disc.pgm +0 -0
- data/examples/disks.sh +18 -0
- data/examples/disks.txt +5 -0
- data/examples/dose.txt +7 -0
- data/examples/drift.txt +4 -0
- data/examples/ethernet.txt +3 -0
- data/examples/figure.c +5 -0
- data/examples/fit.rb +16 -0
- data/examples/floors.rb +21 -0
- data/examples/gallery.sh +20 -0
- data/examples/gauges.sh +31 -0
- data/examples/gradient.rb +16 -0
- data/examples/growth.txt +6 -0
- data/examples/hosts.csv +4 -0
- data/examples/icmp.txt +3 -0
- data/examples/ipv4.txt +12 -0
- data/examples/kinds.csv +4 -0
- data/examples/langs.csv +4 -0
- data/examples/lint.rb +16 -0
- data/examples/load.rb +14 -0
- data/examples/login.txt +10 -0
- data/examples/neg.csv +4 -0
- data/examples/normal.txt +200 -0
- data/examples/note.txt +2 -0
- data/examples/notes-fixed.md +17 -0
- data/examples/notes.md +17 -0
- data/examples/ohlc.txt +7 -0
- data/examples/os.csv +10 -0
- data/examples/outline.txt +8 -0
- data/examples/plot.txt +5 -0
- data/examples/refusals.rb +17 -0
- data/examples/report.sh +21 -0
- data/examples/rfc.sh +22 -0
- data/examples/samples.txt +7 -0
- data/examples/shapes.pgm +0 -0
- data/examples/sizes.tsv +3 -0
- data/examples/spi.txt +6 -0
- data/examples/status.sh +23 -0
- data/examples/stem.txt +12 -0
- data/examples/table-drift.txt +4 -0
- data/examples/temps.txt +31 -0
- data/examples/trig.sh +13 -0
- data/examples/trig.txt +102 -0
- data/examples/week.txt +8 -0
- data/examples/wide.txt +2 -0
- data/exe/tessella +6 -0
- data/lib/tessella/axis.rb +71 -0
- data/lib/tessella/bitfield.rb +194 -0
- data/lib/tessella/block.rb +56 -0
- data/lib/tessella/borders.rb +85 -0
- data/lib/tessella/canvas/area.rb +155 -0
- data/lib/tessella/canvas/ascii.rb +120 -0
- data/lib/tessella/canvas/block.rb +40 -0
- data/lib/tessella/canvas/braille.rb +30 -0
- data/lib/tessella/canvas/clip.rb +49 -0
- data/lib/tessella/canvas/density.rb +51 -0
- data/lib/tessella/canvas/dot.rb +27 -0
- data/lib/tessella/canvas/eighths.rb +43 -0
- data/lib/tessella/canvas/half.rb +51 -0
- data/lib/tessella/canvas/sextant.rb +36 -0
- data/lib/tessella/canvas.rb +168 -0
- data/lib/tessella/cell.rb +10 -0
- data/lib/tessella/chart.rb +211 -0
- data/lib/tessella/check.rb +52 -0
- data/lib/tessella/cli/bitfield.rb +121 -0
- data/lib/tessella/cli/chart.rb +615 -0
- data/lib/tessella/cli/chart_candle.rb +36 -0
- data/lib/tessella/cli/chart_heatmap.rb +21 -0
- data/lib/tessella/cli/chart_help.rb +85 -0
- data/lib/tessella/cli/chart_pie.rb +38 -0
- data/lib/tessella/cli/compose.rb +156 -0
- data/lib/tessella/cli/compose_options.rb +259 -0
- data/lib/tessella/cli/glyphs.rb +97 -0
- data/lib/tessella/cli/image.rb +250 -0
- data/lib/tessella/cli/input.rb +44 -0
- data/lib/tessella/cli/iso_time.rb +28 -0
- data/lib/tessella/cli/lint.rb +102 -0
- data/lib/tessella/cli/lint_options.rb +99 -0
- data/lib/tessella/cli/main.rb +121 -0
- data/lib/tessella/cli/meter.rb +80 -0
- data/lib/tessella/cli/rule.rb +57 -0
- data/lib/tessella/cli/seq.rb +117 -0
- data/lib/tessella/cli/sparkline.rb +72 -0
- data/lib/tessella/cli/table.rb +167 -0
- data/lib/tessella/cli/text.rb +78 -0
- data/lib/tessella/cli/tree.rb +100 -0
- data/lib/tessella/cli/wave.rb +148 -0
- data/lib/tessella/cli.rb +36 -0
- data/lib/tessella/color.rb +106 -0
- data/lib/tessella/compose.rb +243 -0
- data/lib/tessella/compose_fill.rb +176 -0
- data/lib/tessella/compose_grid.rb +36 -0
- data/lib/tessella/compose_shadow.rb +41 -0
- data/lib/tessella/error.rb +13 -0
- data/lib/tessella/frame.rb +170 -0
- data/lib/tessella/glyphs.rb +106 -0
- data/lib/tessella/image.rb +454 -0
- data/lib/tessella/image_octant.rb +46 -0
- data/lib/tessella/image_petscii.rb +116 -0
- data/lib/tessella/kinds/bar.rb +344 -0
- data/lib/tessella/kinds/box.rb +213 -0
- data/lib/tessella/kinds/box_upright.rb +110 -0
- data/lib/tessella/kinds/candlestick.rb +164 -0
- data/lib/tessella/kinds/columns.rb +259 -0
- data/lib/tessella/kinds/date_axis.rb +102 -0
- data/lib/tessella/kinds/error_bars.rb +71 -0
- data/lib/tessella/kinds/heatmap.rb +130 -0
- data/lib/tessella/kinds/line.rb +173 -0
- data/lib/tessella/kinds/multibar.rb +98 -0
- data/lib/tessella/kinds/multibar_columns.rb +130 -0
- data/lib/tessella/kinds/multibar_fine.rb +45 -0
- data/lib/tessella/kinds/multibar_rows.rb +93 -0
- data/lib/tessella/kinds/pie.rb +123 -0
- data/lib/tessella/kinds/pie_rim.rb +78 -0
- data/lib/tessella/kinds/sideways.rb +62 -0
- data/lib/tessella/kinds/stem.rb +134 -0
- data/lib/tessella/kinds/threshold.rb +73 -0
- data/lib/tessella/legend.rb +31 -0
- data/lib/tessella/lint/alphabet.rb +56 -0
- data/lib/tessella/lint/drift.rb +82 -0
- data/lib/tessella/lint/fences.rb +51 -0
- data/lib/tessella/lint/geometry.rb +120 -0
- data/lib/tessella/lint/geometry_checks.rb +224 -0
- data/lib/tessella/lint/lines.rb +115 -0
- data/lib/tessella/lint/mixing.rb +61 -0
- data/lib/tessella/lint/runner.rb +73 -0
- data/lib/tessella/lint/shadows.rb +120 -0
- data/lib/tessella/lint/target_rows.rb +66 -0
- data/lib/tessella/lint.rb +158 -0
- data/lib/tessella/meter.rb +133 -0
- data/lib/tessella/meter_upright.rb +43 -0
- data/lib/tessella/png.rb +456 -0
- data/lib/tessella/raster.rb +92 -0
- data/lib/tessella/rmagick_reader.rb +52 -0
- data/lib/tessella/rule.rb +59 -0
- data/lib/tessella/seq.rb +522 -0
- data/lib/tessella/sparkline.rb +57 -0
- data/lib/tessella/stats.rb +199 -0
- data/lib/tessella/table.rb +547 -0
- data/lib/tessella/table_cells.rb +225 -0
- data/lib/tessella/target.rb +105 -0
- data/lib/tessella/text.rb +76 -0
- data/lib/tessella/text_forms.rb +150 -0
- data/lib/tessella/tree.rb +221 -0
- data/lib/tessella/version.rb +5 -0
- data/lib/tessella/wave.rb +353 -0
- data/lib/tessella/width.rb +122 -0
- data/lib/tessella/width_class.rb +77 -0
- data/lib/tessella/width_invisible.rb +66 -0
- data/lib/tessella/width_invisible_high.rb +60 -0
- data/lib/tessella.rb +94 -0
- metadata +226 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tessella
|
|
4
|
+
class Canvas
|
|
5
|
+
# 2x2 sub-pixels per cell, packed into the Unicode quadrant
|
|
6
|
+
# block characters (Block Elements / Symbols for Legacy
|
|
7
|
+
# Computing): each of the four Unicode glyphs for a quadrant
|
|
8
|
+
# combination is the only character that means that exact
|
|
9
|
+
# combination, so this table is fixed by the Unicode names,
|
|
10
|
+
# not by any library.
|
|
11
|
+
class Block < Canvas
|
|
12
|
+
TOP_LEFT = 8
|
|
13
|
+
TOP_RIGHT = 4
|
|
14
|
+
BOTTOM_LEFT = 2
|
|
15
|
+
BOTTOM_RIGHT = 1
|
|
16
|
+
|
|
17
|
+
BITS = [
|
|
18
|
+
[TOP_LEFT, BOTTOM_LEFT],
|
|
19
|
+
[TOP_RIGHT, BOTTOM_RIGHT]
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
GLYPHS = [
|
|
23
|
+
" ", "▗", "▖", "▄", "▝", "▐", "▞", "▟",
|
|
24
|
+
"▘", "▚", "▌", "▙", "▀", "▜", "▛", "█"
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def x_pixel_per_char = 2
|
|
30
|
+
def y_pixel_per_char = 2
|
|
31
|
+
|
|
32
|
+
def encode(code, sub_x, sub_y) = code | BITS[sub_x][sub_y]
|
|
33
|
+
def decode(code) = GLYPHS[code]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
register(:block, Block)
|
|
37
|
+
# The image module's name for the same 2x2 quadrant glyphs.
|
|
38
|
+
register(:quadrant, Block)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tessella
|
|
4
|
+
class Canvas
|
|
5
|
+
# 2x4 sub-pixels per cell, packed into the Unicode Braille
|
|
6
|
+
# Patterns block: dot n sets bit 2**(n-1) of the base U+2800,
|
|
7
|
+
# dots 1/2/3/7 in the left column (top to bottom), 4/5/6/8 in
|
|
8
|
+
# the right -- the Unicode standard's own dot layout, so this
|
|
9
|
+
# table is fixed by the codepoint scheme, not by any library.
|
|
10
|
+
class Braille < Canvas
|
|
11
|
+
BITS = [
|
|
12
|
+
[0x01, 0x02, 0x04, 0x40],
|
|
13
|
+
[0x08, 0x10, 0x20, 0x80]
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def x_pixel_per_char = 2
|
|
19
|
+
def y_pixel_per_char = 4
|
|
20
|
+
|
|
21
|
+
def encode(code, sub_x, sub_y) = code | BITS[sub_x][sub_y]
|
|
22
|
+
|
|
23
|
+
def decode(code)
|
|
24
|
+
code.zero? ? " " : [0x2800 | code].pack("U")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
register(:braille, Braille)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tessella
|
|
4
|
+
class Canvas
|
|
5
|
+
# A segment far longer than the canvas (a spike past ylim:)
|
|
6
|
+
# steps only where it crosses the canvas: stepping it all, one
|
|
7
|
+
# rejected pixel at a time, took minutes for a value a million
|
|
8
|
+
# times the limits. Shorter segments keep the running sum, and
|
|
9
|
+
# so unicode_plot's exact pixels.
|
|
10
|
+
module Clip
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def long?(steps) = steps > 2 * (pixel_width + pixel_height)
|
|
14
|
+
|
|
15
|
+
def clipped_line(from, inc, steps, color)
|
|
16
|
+
first, last = on_canvas(from, inc, steps)
|
|
17
|
+
first.upto(last) do |i|
|
|
18
|
+
draw_pixel((from[0] + (inc[0] * i)).floor,
|
|
19
|
+
(from[1] + (inc[1] * i)).floor, color)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The steps [first, last] at which both coordinates are on
|
|
24
|
+
# the canvas, a pixel of margin either side.
|
|
25
|
+
def on_canvas(from, inc, steps)
|
|
26
|
+
x = span(from[0], inc[0], pixel_width)
|
|
27
|
+
y = span(from[1], inc[1], pixel_height)
|
|
28
|
+
within([x[0], y[0]].max, [x[1], y[1]].min, steps)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def within(first, last, steps)
|
|
32
|
+
[first.clamp(1, steps.ceil), last.clamp(0, steps.floor)]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def span(start, inc, size)
|
|
36
|
+
return flat_span(start, size) if inc.zero?
|
|
37
|
+
|
|
38
|
+
a, b = [(-1 - start) / inc, (size + 1 - start) / inc].minmax
|
|
39
|
+
[a.floor, b.ceil]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def flat_span(start, size)
|
|
43
|
+
start.between?(-1, size + 1) ? [0, Float::INFINITY] : [1, 0]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
include Clip
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tessella
|
|
4
|
+
class Canvas
|
|
5
|
+
# 1x2 sub-pixels per cell; the glyph is chosen by hit count
|
|
6
|
+
# relative to the densest cell on the canvas, not by position
|
|
7
|
+
# (DESIGN.md, Canvas). ascii: true swaps the default shade
|
|
8
|
+
# ramp for an ASCII one (brief rules).
|
|
9
|
+
class Density < Canvas
|
|
10
|
+
SIGNS = [" ", "░", "▒", "▓", "█"].freeze
|
|
11
|
+
ASCII_SIGNS = [" ", ".", ":", "+", "#"].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(width, height, xlim:, ylim:, ascii: false)
|
|
14
|
+
@signs = ascii ? ASCII_SIGNS : SIGNS
|
|
15
|
+
@max_density = 1
|
|
16
|
+
super(width, height, xlim: xlim, ylim: ylim)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def x_pixel_per_char = 1
|
|
22
|
+
def y_pixel_per_char = 2
|
|
23
|
+
|
|
24
|
+
# The oracle maps a pixel to its cell by floor(float
|
|
25
|
+
# round-trip), not exact integer division, and this can
|
|
26
|
+
# round a whole pixel short at some widths/heights (e.g.
|
|
27
|
+
# 13.fdiv(23) * 23 == 12.999999999999998, D1). Matched
|
|
28
|
+
# here verbatim for byte parity rather than "fixed", since
|
|
29
|
+
# the oracle's own output is what rendering must match.
|
|
30
|
+
def cell_index(px, py)
|
|
31
|
+
cx = (px.fdiv(pixel_width) * width).floor
|
|
32
|
+
cy = (py.fdiv(pixel_height) * height).floor
|
|
33
|
+
(cy * width) + cx
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def encode(code, _sub_x, _sub_y)
|
|
37
|
+
count = code + 1
|
|
38
|
+
@max_density = count if count > @max_density
|
|
39
|
+
count
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def decode(code)
|
|
43
|
+
return " " if code.zero?
|
|
44
|
+
scale = (@signs.size - 1).fdiv(@max_density)
|
|
45
|
+
@signs[(code * scale).round]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
register(:density, Density)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tessella
|
|
4
|
+
class Canvas
|
|
5
|
+
# 1x2 sub-pixels per cell: the top pixel alone draws a raised
|
|
6
|
+
# mark, the bottom alone a mark on the baseline, both a colon.
|
|
7
|
+
class Dot < Canvas
|
|
8
|
+
TOP = 2
|
|
9
|
+
BOTTOM = 1
|
|
10
|
+
|
|
11
|
+
GLYPHS = [" ", ".", "'", ":"].freeze
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def x_pixel_per_char = 1
|
|
16
|
+
def y_pixel_per_char = 2
|
|
17
|
+
|
|
18
|
+
def encode(code, _sub_x, sub_y)
|
|
19
|
+
code | (sub_y.zero? ? TOP : BOTTOM)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def decode(code) = GLYPHS[code]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
register(:dot, Dot)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tessella
|
|
4
|
+
class Canvas
|
|
5
|
+
# Eighth blocks, 1x8 sub-pixels per cell, for fills only (an
|
|
6
|
+
# area): a cell reads as filled from its bottom up to its
|
|
7
|
+
# highest pixel -- " ▁▂▃▄▅▆▇█" -- one pixel column per
|
|
8
|
+
# character, eight height steps. A pixel alone in a cell (a
|
|
9
|
+
# line, a dot) would read as a bar under it, so the charts take
|
|
10
|
+
# this canvas only where they fill.
|
|
11
|
+
class Eighths < Canvas
|
|
12
|
+
GLYPHS = " ▁▂▃▄▅▆▇█".chars.freeze
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def x_pixel_per_char = 1
|
|
17
|
+
def y_pixel_per_char = 8
|
|
18
|
+
|
|
19
|
+
def encode(code, _sub_x, sub_y) = code | (1 << (7 - sub_y))
|
|
20
|
+
|
|
21
|
+
# The highest pixel set, as a height in eighths.
|
|
22
|
+
def decode(code) = GLYPHS[code.bit_length]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Eighth blocks across, 8x1: a cell reads as filled from its
|
|
26
|
+
# left edge out to its rightmost pixel -- " ▏▎▍▌▋▊▉█" -- for an
|
|
27
|
+
# area filled sideways.
|
|
28
|
+
class EighthsWide < Canvas
|
|
29
|
+
GLYPHS = " ▏▎▍▌▋▊▉█".chars.freeze
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def x_pixel_per_char = 8
|
|
34
|
+
def y_pixel_per_char = 1
|
|
35
|
+
|
|
36
|
+
def encode(code, sub_x, _sub_y) = code | (1 << sub_x)
|
|
37
|
+
def decode(code) = GLYPHS[code.bit_length]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
register(:eighths, Eighths)
|
|
41
|
+
register(:eighths_wide, EighthsWide)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tessella
|
|
4
|
+
class Canvas
|
|
5
|
+
# Half blocks, 1x2 sub-pixels per cell: the upper half, the
|
|
6
|
+
# lower, or both -- "▀", "▄", "█" -- one pixel column per
|
|
7
|
+
# character, two rows. A column chart's columns rise by half a
|
|
8
|
+
# cell; an area fills by halves.
|
|
9
|
+
class Half < Canvas
|
|
10
|
+
TOP = 1
|
|
11
|
+
BOTTOM = 2
|
|
12
|
+
|
|
13
|
+
GLYPHS = [" ", "▀", "▄", "█"].freeze
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def x_pixel_per_char = 1
|
|
18
|
+
def y_pixel_per_char = 2
|
|
19
|
+
|
|
20
|
+
def encode(code, _sub_x, sub_y)
|
|
21
|
+
code | (sub_y.zero? ? TOP : BOTTOM)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def decode(code) = GLYPHS[code]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Half blocks across, 2x1 sub-pixels per cell: the left half,
|
|
28
|
+
# the right, or both -- "▌", "▐", "█" -- so a horizontal bar
|
|
29
|
+
# grows by half a cell, one bar per text row.
|
|
30
|
+
class HalfWide < Canvas
|
|
31
|
+
LEFT = 1
|
|
32
|
+
RIGHT = 2
|
|
33
|
+
|
|
34
|
+
GLYPHS = [" ", "▌", "▐", "█"].freeze
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def x_pixel_per_char = 2
|
|
39
|
+
def y_pixel_per_char = 1
|
|
40
|
+
|
|
41
|
+
def encode(code, sub_x, _sub_y)
|
|
42
|
+
code | (sub_x.zero? ? LEFT : RIGHT)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def decode(code) = GLYPHS[code]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
register(:half, Half)
|
|
49
|
+
register(:half_wide, HalfWide)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tessella
|
|
4
|
+
class Canvas
|
|
5
|
+
# Teletext sextants, 2x3 sub-pixels per cell, solid where
|
|
6
|
+
# braille is dotted: Unicode 13's Symbols for Legacy Computing
|
|
7
|
+
# (U+1FB00..1FB3B, with ▌ ▐ █ for the halves and the whole),
|
|
8
|
+
# Image::SEXTANT's table. Bit (row * 2 + column), top left 1.
|
|
9
|
+
class Sextant < Canvas
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def x_pixel_per_char = 2
|
|
13
|
+
def y_pixel_per_char = 3
|
|
14
|
+
|
|
15
|
+
def encode(code, sub_x, sub_y) = code | (1 << ((sub_y * 2) + sub_x))
|
|
16
|
+
def decode(code) = Image::SEXTANT[code]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Octants, 2x4 sub-pixels per cell: braille's resolution,
|
|
20
|
+
# solid -- Unicode 16 (U+1CD00..1CDE5 and older glyphs for the
|
|
21
|
+
# patterns that had one), Image::OCTANT's table, so only the
|
|
22
|
+
# newest terminals draw them.
|
|
23
|
+
class Octant < Canvas
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def x_pixel_per_char = 2
|
|
27
|
+
def y_pixel_per_char = 4
|
|
28
|
+
|
|
29
|
+
def encode(code, sub_x, sub_y) = code | (1 << ((sub_y * 2) + sub_x))
|
|
30
|
+
def decode(code) = Image::OCTANT[code]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
register(:sextant, Sextant)
|
|
34
|
+
register(:octant, Octant)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tessella
|
|
4
|
+
# Glyphs written over a canvas's cells, whatever those cells draw:
|
|
5
|
+
# text (annotate) and point marks (mark). Kept apart from Canvas so
|
|
6
|
+
# Canvas's own class body stays under budget.
|
|
7
|
+
module Overlay
|
|
8
|
+
# char in the cell holding (x, y), over whatever that cell
|
|
9
|
+
# draws; outside the limits, nothing.
|
|
10
|
+
def mark(x, y, char, color: nil)
|
|
11
|
+
return unless x.between?(@xlo, @xhi) && y.between?(@ylo, @yhi)
|
|
12
|
+
|
|
13
|
+
px, py = to_pixel(x, y).map(&:floor)
|
|
14
|
+
px = clip(px, pixel_width)
|
|
15
|
+
py = clip(py, pixel_height)
|
|
16
|
+
@overlay[cell_index(px, py)] =
|
|
17
|
+
Cell.new(char: char, color: cell_color(color, px, py))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def annotate(col, row, text, color: nil)
|
|
21
|
+
text.each_char.with_index do |ch, i|
|
|
22
|
+
next unless (col + i).between?(0, width - 1) &&
|
|
23
|
+
row.between?(0, height - 1)
|
|
24
|
+
@overlay[(row * width) + col + i] =
|
|
25
|
+
Cell.new(char: ch, color: color)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# A grid of character cells a chart draws data into: data
|
|
31
|
+
# coordinates map onto pixels, pixel (0, 0) is the bottom-left, and
|
|
32
|
+
# sub-pixels pack into one Cell per character (DESIGN.md, Canvas).
|
|
33
|
+
# Concrete sub-pixel layouts live in canvas/*.rb, each registering
|
|
34
|
+
# itself under its type with Canvas.register.
|
|
35
|
+
class Canvas
|
|
36
|
+
include Overlay
|
|
37
|
+
|
|
38
|
+
def self.types = (@types ||= {})
|
|
39
|
+
def self.register(type, klass) = (types[type] = klass)
|
|
40
|
+
|
|
41
|
+
# type, width:, height:, xlim:, ylim: and a type's own extras
|
|
42
|
+
# (density's ascii:). A missing required keyword raises
|
|
43
|
+
# ArgumentError naming it, as Ruby's own "missing keyword:
|
|
44
|
+
# :width" does, rather than failing deeper in a subclass.
|
|
45
|
+
def self.build(type, **opts)
|
|
46
|
+
klass = types.fetch(type) do
|
|
47
|
+
raise Error, "unknown canvas type: #{type.inspect}"
|
|
48
|
+
end
|
|
49
|
+
missing = %i[width height xlim ylim] - opts.keys
|
|
50
|
+
unless missing.empty?
|
|
51
|
+
s = missing.size > 1 ? "s" : ""
|
|
52
|
+
names = missing.map { ":#{it}" }.join(", ")
|
|
53
|
+
raise ArgumentError, "missing keyword#{s}: #{names}"
|
|
54
|
+
end
|
|
55
|
+
klass.new(opts.delete(:width), opts.delete(:height), **opts)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
attr_reader :width, :height, :pixel_width, :pixel_height
|
|
59
|
+
|
|
60
|
+
def initialize(width, height, xlim:, ylim:)
|
|
61
|
+
@width = width
|
|
62
|
+
@height = height
|
|
63
|
+
@pixel_width = width * x_pixel_per_char
|
|
64
|
+
@pixel_height = height * y_pixel_per_char
|
|
65
|
+
@xlo, @xhi = xlim
|
|
66
|
+
@ylo, @yhi = ylim
|
|
67
|
+
@grid = Array.new(width * height, blank_code)
|
|
68
|
+
@colors = Array.new(width * height)
|
|
69
|
+
@overlay = Array.new(width * height)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def point(x, y, color: nil)
|
|
73
|
+
return unless x.between?(@xlo, @xhi) && y.between?(@ylo, @yhi)
|
|
74
|
+
draw_pixel(*to_pixel(x, y).map(&:floor), color)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def points(xs, ys, color: nil)
|
|
78
|
+
xs.zip(ys).each { |x, y| point(x, y, color: color) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def line(x0, y0, x1, y1, color: nil)
|
|
82
|
+
return if out?(x0, x1, @xlo, @xhi) || out?(y0, y1, @ylo, @yhi)
|
|
83
|
+
draw_line(x0, y0, x1, y1, color)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def lines(xs, ys, color: nil)
|
|
87
|
+
(0...(xs.size - 1)).each do |i|
|
|
88
|
+
line(xs[i], ys[i], xs[i + 1], ys[i + 1], color: color)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def rows
|
|
93
|
+
Array.new(height) { |r| Array.new(width) { |c| cell_at(r, c) } }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def blank_code = 0
|
|
99
|
+
|
|
100
|
+
def out?(a, b, lo, hi) = (a < lo && b < lo) || (a > hi && b > hi)
|
|
101
|
+
|
|
102
|
+
def to_pixel(x, y)
|
|
103
|
+
fx = (x - @xlo).fdiv(@xhi - @xlo) * pixel_width
|
|
104
|
+
fy = pixel_height - ((y - @ylo).fdiv(@yhi - @ylo) * pixel_height)
|
|
105
|
+
[fx, fy]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def draw_line(x0, y0, x1, y1, color)
|
|
109
|
+
from = to_pixel(x0, y0)
|
|
110
|
+
to = to_pixel(x1, y1)
|
|
111
|
+
draw_pixel(from[0].floor, from[1].floor, color)
|
|
112
|
+
dda(from, to, color)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def dda(from, to, color)
|
|
116
|
+
dx = to[0] - from[0]
|
|
117
|
+
dy = to[1] - from[1]
|
|
118
|
+
steps = [dx.abs, dy.abs].max
|
|
119
|
+
return if steps.zero?
|
|
120
|
+
|
|
121
|
+
inc = [dx.fdiv(steps), dy.fdiv(steps)]
|
|
122
|
+
line = long?(steps) ? :clipped_line : :step_line
|
|
123
|
+
send(line, from, inc, steps, color)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# A running sum, as unicode_plot (from + inc * i rounds apart).
|
|
127
|
+
def step_line(from, inc, steps, color)
|
|
128
|
+
x, y = from
|
|
129
|
+
1.upto(steps) do
|
|
130
|
+
draw_pixel((x += inc[0]).floor, (y += inc[1]).floor, color)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# color: a name, or a callable of the cell's row and column.
|
|
135
|
+
def draw_pixel(px, py, color)
|
|
136
|
+
return unless px.between?(0, pixel_width) &&
|
|
137
|
+
py.between?(0, pixel_height)
|
|
138
|
+
x = clip(px, pixel_width)
|
|
139
|
+
y = clip(py, pixel_height)
|
|
140
|
+
set_sub_pixel(x, y, cell_color(color, x, y))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def cell_color(color, px, py)
|
|
144
|
+
return color unless color.respond_to?(:call) && color.arity == 2
|
|
145
|
+
|
|
146
|
+
color.call(py / y_pixel_per_char, px / x_pixel_per_char)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def clip(v, max) = v == max ? v - 1 : v
|
|
150
|
+
|
|
151
|
+
def set_sub_pixel(px, py, color)
|
|
152
|
+
i = cell_index(px, py)
|
|
153
|
+
@grid[i] = encode(@grid[i], px % x_pixel_per_char,
|
|
154
|
+
py % y_pixel_per_char)
|
|
155
|
+
@colors[i] = color unless color.nil?
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Overridden by density.rb, D1.
|
|
159
|
+
def cell_index(px, py)
|
|
160
|
+
((py / y_pixel_per_char) * width) + (px / x_pixel_per_char)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def cell_at(row, col)
|
|
164
|
+
i = (row * width) + col
|
|
165
|
+
@overlay[i] || Cell.new(char: decode(@grid[i]), color: @colors[i])
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tessella
|
|
4
|
+
# One character cell of a chart body: its glyph, and the colour name
|
|
5
|
+
# it is drawn in (nil: none). Canvases produce rows of these; the
|
|
6
|
+
# frame renders them (DESIGN.md).
|
|
7
|
+
Cell = Data.define(:char, :color) do
|
|
8
|
+
def self.blank = new(char: " ", color: nil)
|
|
9
|
+
end
|
|
10
|
+
end
|