r2ui 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +64 -0
- data/README.md +79 -0
- data/lib/r2ui/app.rb +311 -34
- data/lib/r2ui/canvas.rb +135 -14
- data/lib/r2ui/cli/builder.rb +72 -0
- data/lib/r2ui/cli/context.rb +45 -0
- data/lib/r2ui/cli/definition.rb +174 -0
- data/lib/r2ui/cli/ext/ask.rb +133 -0
- data/lib/r2ui/cli/ext/box.rb +112 -0
- data/lib/r2ui/cli/ext/choose.rb +162 -0
- data/lib/r2ui/cli/ext/choose_many.rb +241 -0
- data/lib/r2ui/cli/ext/color_option.rb +64 -0
- data/lib/r2ui/cli/ext/completion.rb +520 -0
- data/lib/r2ui/cli/ext/config.rb +179 -0
- data/lib/r2ui/cli/ext/confirm.rb +110 -0
- data/lib/r2ui/cli/ext/dashboard.rb +105 -0
- data/lib/r2ui/cli/ext/diff.rb +195 -0
- data/lib/r2ui/cli/ext/editor.rb +94 -0
- data/lib/r2ui/cli/ext/env.rb +108 -0
- data/lib/r2ui/cli/ext/examples.rb +62 -0
- data/lib/r2ui/cli/ext/filter.rb +269 -0
- data/lib/r2ui/cli/ext/format.rb +104 -0
- data/lib/r2ui/cli/ext/list.rb +117 -0
- data/lib/r2ui/cli/ext/log.rb +89 -0
- data/lib/r2ui/cli/ext/pager.rb +97 -0
- data/lib/r2ui/cli/ext/pairs.rb +62 -0
- data/lib/r2ui/cli/ext/parallel.rb +274 -0
- data/lib/r2ui/cli/ext/password.rb +158 -0
- data/lib/r2ui/cli/ext/progress.rb +330 -0
- data/lib/r2ui/cli/ext/reference.rb +129 -0
- data/lib/r2ui/cli/ext/spinner.rb +165 -0
- data/lib/r2ui/cli/ext/summary.rb +63 -0
- data/lib/r2ui/cli/ext/table.rb +141 -0
- data/lib/r2ui/cli/ext/tasks.rb +216 -0
- data/lib/r2ui/cli/ext/trace.rb +64 -0
- data/lib/r2ui/cli/ext/tree.rb +114 -0
- data/lib/r2ui/cli/ext/version.rb +67 -0
- data/lib/r2ui/cli/extension.rb +129 -0
- data/lib/r2ui/cli/help.rb +115 -0
- data/lib/r2ui/cli/helpers.rb +20 -0
- data/lib/r2ui/cli/live.rb +150 -0
- data/lib/r2ui/cli/parser.rb +165 -0
- data/lib/r2ui/cli/prompt.rb +133 -0
- data/lib/r2ui/cli/runner.rb +86 -0
- data/lib/r2ui/cli/shell.rb +185 -0
- data/lib/r2ui/cli/testing.rb +49 -0
- data/lib/r2ui/cli.rb +129 -0
- data/lib/r2ui/compat/bubbletea/LICENSE-bubbletea.txt +21 -0
- data/lib/r2ui/compat/bubbletea/ansi.rb +424 -0
- data/lib/r2ui/compat/bubbletea/commands.rb +143 -0
- data/lib/r2ui/compat/bubbletea/input.rb +269 -0
- data/lib/r2ui/compat/bubbletea/input_reader.rb +66 -0
- data/lib/r2ui/compat/bubbletea/messages.rb +251 -0
- data/lib/r2ui/compat/bubbletea/model.rb +65 -0
- data/lib/r2ui/compat/bubbletea/native.rb +254 -0
- data/lib/r2ui/compat/bubbletea/renderer.rb +192 -0
- data/lib/r2ui/compat/bubbletea/runner.rb +425 -0
- data/lib/r2ui/compat/bubbletea/terminal.rb +230 -0
- data/lib/r2ui/compat/bubbletea/version.rb +8 -0
- data/lib/r2ui/compat/bubbletea/width_table.rb +228 -0
- data/lib/r2ui/compat/bubbletea.rb +16 -0
- data/lib/r2ui/compat/lipgloss/border.rb +17 -0
- data/lib/r2ui/compat/lipgloss/color.rb +78 -0
- data/lib/r2ui/compat/lipgloss/color_blend.rb +166 -0
- data/lib/r2ui/compat/lipgloss/colorful.rb +585 -0
- data/lib/r2ui/compat/lipgloss/layout.rb +461 -0
- data/lib/r2ui/compat/lipgloss/list.rb +85 -0
- data/lib/r2ui/compat/lipgloss/position.rb +30 -0
- data/lib/r2ui/compat/lipgloss/style/border.rb +134 -0
- data/lib/r2ui/compat/lipgloss/style.rb +1062 -0
- data/lib/r2ui/compat/lipgloss/table/render.rb +518 -0
- data/lib/r2ui/compat/lipgloss/table.rb +185 -0
- data/lib/r2ui/compat/lipgloss/termenv.rb +768 -0
- data/lib/r2ui/compat/lipgloss/text/unicode_tables.rb +428 -0
- data/lib/r2ui/compat/lipgloss/text.rb +1555 -0
- data/lib/r2ui/compat/lipgloss/tree/go_tree.rb +243 -0
- data/lib/r2ui/compat/lipgloss/tree.rb +105 -0
- data/lib/r2ui/compat/lipgloss/version.rb +15 -0
- data/lib/r2ui/compat/lipgloss.rb +64 -0
- data/lib/r2ui/compat/load_path/bubbletea.rb +4 -0
- data/lib/r2ui/compat/load_path/lipgloss.rb +4 -0
- data/lib/r2ui/component.rb +67 -0
- data/lib/r2ui/context.rb +87 -0
- data/lib/r2ui/drop_in.rb +16 -0
- data/lib/r2ui/dsl/column.rb +11 -3
- data/lib/r2ui/dsl/dashboard.rb +45 -9
- data/lib/r2ui/dsl/resource.rb +12 -0
- data/lib/r2ui/ext/after.rb +54 -0
- data/lib/r2ui/ext/async.rb +81 -0
- data/lib/r2ui/ext/batch_sequence.rb +54 -0
- data/lib/r2ui/ext/countdown.rb +52 -0
- data/lib/r2ui/ext/data_table.rb +71 -0
- data/lib/r2ui/ext/every.rb +66 -0
- data/lib/r2ui/ext/exec.rb +116 -0
- data/lib/r2ui/ext/file_picker.rb +78 -0
- data/lib/r2ui/ext/focus_report.rb +69 -0
- data/lib/r2ui/ext/help.rb +62 -0
- data/lib/r2ui/ext/keys.rb +64 -0
- data/lib/r2ui/ext/list.rb +145 -0
- data/lib/r2ui/ext/messages.rb +87 -0
- data/lib/r2ui/ext/mouse.rb +134 -0
- data/lib/r2ui/ext/paginator.rb +86 -0
- data/lib/r2ui/ext/paste.rb +86 -0
- data/lib/r2ui/ext/println.rb +21 -0
- data/lib/r2ui/ext/progress.rb +85 -0
- data/lib/r2ui/ext/refresh.rb +56 -0
- data/lib/r2ui/ext/resize.rb +62 -0
- data/lib/r2ui/ext/screen.rb +47 -0
- data/lib/r2ui/ext/screen_mode.rb +63 -0
- data/lib/r2ui/ext/spinner.rb +59 -0
- data/lib/r2ui/ext/state.rb +45 -0
- data/lib/r2ui/ext/stopwatch.rb +67 -0
- data/lib/r2ui/ext/suspend.rb +56 -0
- data/lib/r2ui/ext/text_area.rb +73 -0
- data/lib/r2ui/ext/text_input.rb +83 -0
- data/lib/r2ui/ext/theme.rb +112 -0
- data/lib/r2ui/ext/view.rb +37 -0
- data/lib/r2ui/ext/viewport.rb +92 -0
- data/lib/r2ui/ext/window_title.rb +51 -0
- data/lib/r2ui/extension.rb +199 -0
- data/lib/r2ui/history.rb +4 -1
- data/lib/r2ui/keys.rb +35 -8
- data/lib/r2ui/motion.rb +169 -0
- data/lib/r2ui/panel_state.rb +46 -2
- data/lib/r2ui/renderer.rb +87 -22
- data/lib/r2ui/version.rb +1 -1
- data/lib/r2ui/widgets/box.rb +6 -3
- data/lib/r2ui/widgets/glyphs.rb +149 -0
- data/lib/r2ui/widgets/table.rb +109 -25
- data/lib/r2ui.rb +12 -1
- metadata +119 -4
- data/lib/r2ui/terminal.rb +0 -53
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module R2UI
|
|
4
|
+
module Compat
|
|
5
|
+
module Gloss
|
|
6
|
+
# Port of the parts of lucasb-eyer/go-colorful v1.2.0 that lipgloss and termenv use: hex parsing
|
|
7
|
+
# and formatting, RGB/Luv/HCL blending, HSL, and the HSLuv distance termenv uses to pick the
|
|
8
|
+
# nearest ANSI/ANSI256 color.
|
|
9
|
+
#
|
|
10
|
+
# c = Colorful.hex("#ff8000") # => Colorful::Color, or nil where Go returns an error
|
|
11
|
+
# c.blend_luv(other, 0.5).hex # => "#rrggbb"
|
|
12
|
+
#
|
|
13
|
+
# Float fidelity: the gem ships Go code compiled for arm64, where the Go compiler fuses `a + x*y`,
|
|
14
|
+
# `a - x*y` and `x*y - a` into single-rounding FMA instructions, and evaluates constant
|
|
15
|
+
# expressions exactly. The formulas below spell out each fused operation (GoMath.madd/msub/nmsub)
|
|
16
|
+
# and use exactly folded constants, so blends land on the same side of rounding ties as the gem.
|
|
17
|
+
module Colorful
|
|
18
|
+
# Float helpers reproducing how Go computes on arm64.
|
|
19
|
+
module GoMath
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# Correctly rounded x*y + z (one rounding, like the FMADD instruction).
|
|
23
|
+
def fma(x, y, z)
|
|
24
|
+
return x * y + z unless x.finite? && y.finite? && z.finite?
|
|
25
|
+
return x * y + z if x.zero? || y.zero?
|
|
26
|
+
|
|
27
|
+
mx, ex = split(x)
|
|
28
|
+
my, ey = split(y)
|
|
29
|
+
mz, ez = split(z)
|
|
30
|
+
mp = mx * my
|
|
31
|
+
ep = ex + ey
|
|
32
|
+
return round_scaled(mp, ep) if mz.zero?
|
|
33
|
+
|
|
34
|
+
e = ep < ez ? ep : ez
|
|
35
|
+
round_scaled((mp << (ep - e)) + (mz << (ez - e)), e)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# a + x*y, fused.
|
|
39
|
+
def madd(a, x, y)
|
|
40
|
+
fma(x, y, a)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# a - x*y, fused.
|
|
44
|
+
def msub(a, x, y)
|
|
45
|
+
fma(-x, y, a)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# x*y - a, fused.
|
|
49
|
+
def nmsub(x, y, a)
|
|
50
|
+
fma(x, y, -a)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Exact integer mantissa and exponent: f == m * 2**e.
|
|
54
|
+
def split(f)
|
|
55
|
+
return [0, 0] if f.zero?
|
|
56
|
+
|
|
57
|
+
frac, exp = Math.frexp(f)
|
|
58
|
+
[Math.ldexp(frac, 53).to_i, exp - 53]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Round m * 2**e to the nearest float, ties to even.
|
|
62
|
+
def round_scaled(m, e)
|
|
63
|
+
return 0.0 if m.zero?
|
|
64
|
+
|
|
65
|
+
sign = m.negative? ? -1.0 : 1.0
|
|
66
|
+
m = m.abs
|
|
67
|
+
shift = m.bit_length - 53
|
|
68
|
+
if shift.positive?
|
|
69
|
+
q = m >> shift
|
|
70
|
+
rem = m & ((1 << shift) - 1)
|
|
71
|
+
half = 1 << (shift - 1)
|
|
72
|
+
q += 1 if rem > half || (rem == half && q.odd?)
|
|
73
|
+
sign * Math.ldexp(q.to_f, e + shift)
|
|
74
|
+
else
|
|
75
|
+
sign * Math.ldexp(m.to_f, e)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# math.Mod: like C fmod, exact, and the result takes the sign of x.
|
|
80
|
+
def mod(x, y)
|
|
81
|
+
r = x.abs % y.abs
|
|
82
|
+
x.negative? ? -r : r
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# math.Pow as Go implements it: x**frac via Exp(frac*Log(x)), then the integer part by
|
|
86
|
+
# repeated squaring of the Frexp mantissa (each product rounded), finally Ldexp.
|
|
87
|
+
def pow(x, y)
|
|
88
|
+
return 1.0 if y.zero? || x == 1.0
|
|
89
|
+
return x if y == 1.0
|
|
90
|
+
return Float::NAN if x.nan? || y.nan?
|
|
91
|
+
return x**y if x.zero? || x.infinite? || y.infinite?
|
|
92
|
+
return Math.sqrt(x) if y == 0.5
|
|
93
|
+
return 1 / Math.sqrt(x) if y == -0.5
|
|
94
|
+
|
|
95
|
+
yi = y.abs.floor.to_f
|
|
96
|
+
yf = y.abs - yi
|
|
97
|
+
return Float::NAN if yf != 0 && x.negative?
|
|
98
|
+
|
|
99
|
+
a1 = 1.0
|
|
100
|
+
ae = 0
|
|
101
|
+
if yf != 0
|
|
102
|
+
if yf > 0.5
|
|
103
|
+
yf -= 1
|
|
104
|
+
yi += 1
|
|
105
|
+
end
|
|
106
|
+
a1 = Math.exp(yf * Math.log(x))
|
|
107
|
+
end
|
|
108
|
+
x1, xe = Math.frexp(x)
|
|
109
|
+
i = yi.to_i
|
|
110
|
+
while i != 0
|
|
111
|
+
if xe < -(1 << 12) || (1 << 12) < xe
|
|
112
|
+
ae += xe
|
|
113
|
+
break
|
|
114
|
+
end
|
|
115
|
+
if i.odd?
|
|
116
|
+
a1 *= x1
|
|
117
|
+
ae += xe
|
|
118
|
+
end
|
|
119
|
+
x1 *= x1
|
|
120
|
+
xe <<= 1
|
|
121
|
+
if x1 < 0.5
|
|
122
|
+
x1 += x1
|
|
123
|
+
xe -= 1
|
|
124
|
+
end
|
|
125
|
+
i >>= 1
|
|
126
|
+
end
|
|
127
|
+
if y.negative?
|
|
128
|
+
a1 = 1 / a1
|
|
129
|
+
ae = -ae
|
|
130
|
+
end
|
|
131
|
+
Math.ldexp(a1, ae)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
CBRT_B1 = 715_094_163
|
|
135
|
+
CBRT_B2 = 696_219_795
|
|
136
|
+
CBRT_C = 5.42857142857142815906e-01
|
|
137
|
+
CBRT_D = -7.05306122448979611050e-01
|
|
138
|
+
CBRT_E = 1.41428571428571436819e+00
|
|
139
|
+
CBRT_F = 1.60714285714285720630e+00
|
|
140
|
+
CBRT_G = 3.57142857142857150787e-01
|
|
141
|
+
SMALLEST_NORMAL = 2.22507385850720138309e-308
|
|
142
|
+
|
|
143
|
+
# math.Cbrt (pure Go, FreeBSD s_cbrt.c), with arm64's fused multiply-adds.
|
|
144
|
+
def cbrt(x)
|
|
145
|
+
return x if x.zero? || x.nan? || x.infinite?
|
|
146
|
+
|
|
147
|
+
sign = x.negative?
|
|
148
|
+
x = -x if sign
|
|
149
|
+
t = from_bits(bits(x) / 3 + (CBRT_B1 << 32))
|
|
150
|
+
if x < SMALLEST_NORMAL
|
|
151
|
+
t = (1 << 54).to_f * x
|
|
152
|
+
t = from_bits(bits(t) / 3 + (CBRT_B2 << 32))
|
|
153
|
+
end
|
|
154
|
+
r = t * t / x
|
|
155
|
+
s = madd(CBRT_C, r, t)
|
|
156
|
+
t *= CBRT_G + CBRT_F / (s + CBRT_E + CBRT_D / s)
|
|
157
|
+
t = from_bits((bits(t) & (0xFFFFFFFFC << 28)) + (1 << 30))
|
|
158
|
+
s = t * t
|
|
159
|
+
r = x / s
|
|
160
|
+
w = t + t
|
|
161
|
+
r = (r - t) / (w + r)
|
|
162
|
+
t = madd(t, t, r)
|
|
163
|
+
sign ? -t : t
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def bits(f)
|
|
167
|
+
[f].pack("G").unpack1("Q>")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def from_bits(i)
|
|
171
|
+
[i & 0xFFFF_FFFF_FFFF_FFFF].pack("Q>").unpack1("G")
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
extend GoMath
|
|
176
|
+
|
|
177
|
+
D65 = [0.95047, 1.00000, 1.08883].freeze
|
|
178
|
+
HSLUV_D65 = [0.95045592705167, 1.0, 1.089057750759878].freeze
|
|
179
|
+
|
|
180
|
+
# Go constant expressions are folded exactly, then rounded once.
|
|
181
|
+
LAB_EPS = Rational(216, 24_389).to_f # 6.0/29.0*6.0/29.0*6.0/29.0
|
|
182
|
+
LAB_6_29 = Rational(6, 29).to_f # 6.0/29.0
|
|
183
|
+
LAB_4_29 = Rational(4, 29).to_f # 4.0/29.0
|
|
184
|
+
LAB_FINV_K = Rational(108, 841).to_f # 3.0*6.0/29.0*6.0/29.0
|
|
185
|
+
LUV_KAPPA = Rational(24_389, 27).to_f # 29.0/3.0*29.0/3.0*29.0/3.0
|
|
186
|
+
ONE_THIRD = Rational(1, 3).to_f
|
|
187
|
+
TWO_THIRDS = Rational(2, 3).to_f
|
|
188
|
+
INV_GAMMA = Rational(5, 12).to_f # 1.0/2.4
|
|
189
|
+
RAD2DEG = 57.29577951308232087721
|
|
190
|
+
DEG2RAD = 0.01745329251994329576
|
|
191
|
+
|
|
192
|
+
# Go's fmt scanning treats these as space (fmt/scan.go `space` table).
|
|
193
|
+
SPACE_RANGES = [
|
|
194
|
+
[0x0009, 0x000d], [0x0020, 0x0020], [0x0085, 0x0085], [0x00a0, 0x00a0], [0x1680, 0x1680],
|
|
195
|
+
[0x2000, 0x200a], [0x2028, 0x2029], [0x202f, 0x202f], [0x205f, 0x205f], [0x3000, 0x3000]
|
|
196
|
+
].freeze
|
|
197
|
+
HEX_SCAN_DIGITS = "0123456789aAbBcCdDeEfF_"
|
|
198
|
+
|
|
199
|
+
module_function
|
|
200
|
+
|
|
201
|
+
# colorful.Hex: parses "#rrggbb", or "#rgb" when the string is 4 bytes long. Mirrors
|
|
202
|
+
# fmt.Sscanf(scol, "#%02x%02x%02x") exactly, including its quirks: spaces before a number are
|
|
203
|
+
# skipped, a short final component is accepted ("#12345"), and trailing input is ignored
|
|
204
|
+
# ("#ffffffzz"). Returns nil where Go returns an error.
|
|
205
|
+
def hex(scol)
|
|
206
|
+
scol = scol.to_s
|
|
207
|
+
width = 2
|
|
208
|
+
factor = 1.0 / 255.0
|
|
209
|
+
if scol.bytesize == 4
|
|
210
|
+
width = 1
|
|
211
|
+
factor = 1.0 / 15.0
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
runes = go_runes(scol)
|
|
215
|
+
return nil unless runes[0] == 0x23 # '#'
|
|
216
|
+
|
|
217
|
+
pos = 1
|
|
218
|
+
values = []
|
|
219
|
+
3.times do
|
|
220
|
+
value, pos = scan_hex_uint8(runes, pos, width)
|
|
221
|
+
return nil if value.nil?
|
|
222
|
+
|
|
223
|
+
values << value
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
Color.new(values[0] * factor, values[1] * factor, values[2] * factor)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# One "%0Nx" verb into a uint8, as fmt's doScanf/scanUint do it: SkipSpace (not counted in the
|
|
230
|
+
# width), notEOF, then up to `width` runes of hex digits or '_', parsed with strconv.ParseUint.
|
|
231
|
+
def scan_hex_uint8(runes, pos, width)
|
|
232
|
+
while pos < runes.size
|
|
233
|
+
r = runes[pos]
|
|
234
|
+
if r == 0x0d && runes[pos + 1] == 0x0a
|
|
235
|
+
pos += 1
|
|
236
|
+
next
|
|
237
|
+
end
|
|
238
|
+
return [nil, pos] if r == 0x0a # "unexpected newline"
|
|
239
|
+
break unless go_space?(r)
|
|
240
|
+
|
|
241
|
+
pos += 1
|
|
242
|
+
end
|
|
243
|
+
return [nil, pos] if pos >= runes.size # notEOF
|
|
244
|
+
|
|
245
|
+
limit = pos + width
|
|
246
|
+
start = pos
|
|
247
|
+
pos += 1 while pos < limit && pos < runes.size && runes[pos] < 0x80 && HEX_SCAN_DIGITS.include?(runes[pos].chr)
|
|
248
|
+
return [nil, pos] if pos == start # "expected integer"
|
|
249
|
+
|
|
250
|
+
token = runes[start...pos].pack("U*")
|
|
251
|
+
return [nil, pos] if token.include?("_") # ParseUint with base 16 rejects underscores
|
|
252
|
+
|
|
253
|
+
[token.to_i(16), pos]
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def go_space?(r)
|
|
257
|
+
SPACE_RANGES.any? { |lo, hi| r >= lo && r <= hi }
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Go decodes strings as UTF-8, turning each invalid byte into U+FFFD.
|
|
261
|
+
def go_runes(str)
|
|
262
|
+
str.b.force_encoding(Encoding::UTF_8).scrub("�").codepoints
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# math.Max(0, math.Min(v, 1)); NaN stays NaN as in Go.
|
|
266
|
+
def clamp01(v)
|
|
267
|
+
return v if v.nan?
|
|
268
|
+
|
|
269
|
+
v = 1.0 if v > 1.0
|
|
270
|
+
v < 0.0 ? 0.0 : v
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Go's uint8(float64) as compiled on arm64 (the gem's platform): a saturating conversion to
|
|
274
|
+
# int32 (FCVTZS, NaN -> 0), then the low 8 bits. So -2.0 * 255 + 0.5 wraps to 3, while huge
|
|
275
|
+
# values and +Inf give 0xff. Observed from the real gem.
|
|
276
|
+
def go_uint8(f)
|
|
277
|
+
return 0 if f.nan?
|
|
278
|
+
return 0xff if f >= 2_147_483_647
|
|
279
|
+
return 0 if f <= -2_147_483_648
|
|
280
|
+
|
|
281
|
+
f.to_i & 0xff
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def interp_angle(a0, a1, t)
|
|
285
|
+
delta = mod(mod(a1 - a0, 360.0) + 540, 360.0) - 180.0
|
|
286
|
+
mod(madd(a0, t, delta) + 360.0, 360.0)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def linearize(v)
|
|
290
|
+
return v / 12.92 if v <= 0.04045
|
|
291
|
+
|
|
292
|
+
pow((v + 0.055) / 1.055, 2.4)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def delinearize(v)
|
|
296
|
+
return 12.92 * v if v <= 0.0031308
|
|
297
|
+
|
|
298
|
+
nmsub(1.055, pow(v, INV_GAMMA), 0.055)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def linear_rgb(r, g, b)
|
|
302
|
+
Color.new(delinearize(r), delinearize(g), delinearize(b))
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def xyz_to_linear_rgb(x, y, z)
|
|
306
|
+
[
|
|
307
|
+
msub(msub(3.2409699419045214 * x, 1.5373831775700935, y), 0.49861076029300328, z),
|
|
308
|
+
madd(madd(-0.96924363628087983 * x, 1.8759675015077207, y), 0.041555057407175613, z),
|
|
309
|
+
madd(msub(0.055630079696993609 * x, 0.20397695888897657, y), 1.0569715142428786, z)
|
|
310
|
+
]
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def linear_rgb_to_xyz(r, g, b)
|
|
314
|
+
[
|
|
315
|
+
madd(madd(0.41239079926595948 * r, 0.35758433938387796, g), 0.18048078840183429, b),
|
|
316
|
+
madd(madd(0.21263900587151036 * r, 0.71516867876775593, g), 0.072192315360733715, b),
|
|
317
|
+
madd(madd(0.019330818715591851 * r, 0.11919477979462599, g), 0.95053215224966058, b)
|
|
318
|
+
]
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def xyz(x, y, z)
|
|
322
|
+
linear_rgb(*xyz_to_linear_rgb(x, y, z))
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def lab_f(t)
|
|
326
|
+
return cbrt(t) if t > LAB_EPS
|
|
327
|
+
|
|
328
|
+
t / 3.0 * 29.0 / 6.0 * 29.0 / 6.0 + LAB_4_29
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def lab_finv(t)
|
|
332
|
+
return t * t * t if t > LAB_6_29
|
|
333
|
+
|
|
334
|
+
LAB_FINV_K * (t - LAB_4_29)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def xyz_to_lab_white_ref(x, y, z, wref)
|
|
338
|
+
fy = lab_f(y / wref[1])
|
|
339
|
+
[nmsub(1.16, fy, 0.16), 5.0 * (lab_f(x / wref[0]) - fy), 2.0 * (fy - lab_f(z / wref[2]))]
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def lab_to_xyz_white_ref(l, a, b, wref)
|
|
343
|
+
l2 = (l + 0.16) / 1.16
|
|
344
|
+
[wref[0] * lab_finv(l2 + a / 5.0), wref[1] * lab_finv(l2), wref[2] * lab_finv(l2 - b / 2.0)]
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def lab_white_ref(l, a, b, wref)
|
|
348
|
+
xyz(*lab_to_xyz_white_ref(l, a, b, wref))
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def xyz_to_uv(x, y, z)
|
|
352
|
+
denom = madd(madd(x, 15.0, y), 3.0, z)
|
|
353
|
+
return [0.0, 0.0] if denom == 0.0
|
|
354
|
+
|
|
355
|
+
[4.0 * x / denom, 9.0 * y / denom]
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def xyz_to_luv_white_ref(x, y, z, wref)
|
|
359
|
+
l = if y / wref[1] <= LAB_EPS
|
|
360
|
+
y / wref[1] * LUV_KAPPA / 100.0
|
|
361
|
+
else
|
|
362
|
+
nmsub(1.16, cbrt(y / wref[1]), 0.16)
|
|
363
|
+
end
|
|
364
|
+
ubis, vbis = xyz_to_uv(x, y, z)
|
|
365
|
+
un, vn = xyz_to_uv(wref[0], wref[1], wref[2])
|
|
366
|
+
[l, 13.0 * l * (ubis - un), 13.0 * l * (vbis - vn)]
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def luv_to_xyz_white_ref(l, u, v, wref)
|
|
370
|
+
y = if l <= 0.08
|
|
371
|
+
wref[1] * l * 100.0 * 3.0 / 29.0 * 3.0 / 29.0 * 3.0 / 29.0
|
|
372
|
+
else
|
|
373
|
+
q = (l + 0.16) / 1.16
|
|
374
|
+
wref[1] * (q * q * q)
|
|
375
|
+
end
|
|
376
|
+
un, vn = xyz_to_uv(wref[0], wref[1], wref[2])
|
|
377
|
+
if l != 0.0
|
|
378
|
+
ubis = u / (13.0 * l) + un
|
|
379
|
+
vbis = v / (13.0 * l) + vn
|
|
380
|
+
x = y * 9.0 * ubis / (4.0 * vbis)
|
|
381
|
+
z = y * msub(msub(12.0, 3.0, ubis), 20.0, vbis) / (4.0 * vbis)
|
|
382
|
+
else
|
|
383
|
+
x = 0.0
|
|
384
|
+
y = 0.0
|
|
385
|
+
z = 0.0
|
|
386
|
+
end
|
|
387
|
+
[x, y, z]
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def luv_white_ref(l, u, v, wref)
|
|
391
|
+
xyz(*luv_to_xyz_white_ref(l, u, v, wref))
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def luv(l, u, v)
|
|
395
|
+
luv_white_ref(l, u, v, D65)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def lab_to_hcl(l, a, b)
|
|
399
|
+
h = if (b - a).abs > 1e-4 && a.abs > 1e-4
|
|
400
|
+
mod(madd(360.0, RAD2DEG, Math.atan2(b, a)), 360.0)
|
|
401
|
+
else
|
|
402
|
+
0.0
|
|
403
|
+
end
|
|
404
|
+
[h, Math.sqrt(madd(a * a, b, b)), l]
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def hcl_to_lab(h, c, l)
|
|
408
|
+
rad = DEG2RAD * h
|
|
409
|
+
[l, c * Math.cos(rad), c * Math.sin(rad)]
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def hcl(h, c, l)
|
|
413
|
+
lab_white_ref(*hcl_to_lab(h, c, l), D65)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def luv_to_luv_lch(l, u, v)
|
|
417
|
+
h = if (v - u).abs > 1e-4 && u.abs > 1e-4
|
|
418
|
+
mod(madd(360.0, RAD2DEG, Math.atan2(v, u)), 360.0)
|
|
419
|
+
else
|
|
420
|
+
0.0
|
|
421
|
+
end
|
|
422
|
+
[l, Math.sqrt(madd(u * u, v, v)), h]
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# colorful.Hsl: hue in [0..360], saturation and lightness in [0..1].
|
|
426
|
+
def hsl(h, s, l)
|
|
427
|
+
return Color.new(l, l, l) if s == 0
|
|
428
|
+
|
|
429
|
+
t1 = l < 0.5 ? l * (1.0 + s) : msub(l + s, l, s)
|
|
430
|
+
t2 = 2 * l - t1
|
|
431
|
+
h /= 360
|
|
432
|
+
channel = lambda do |t|
|
|
433
|
+
t += 1 if t < 0
|
|
434
|
+
t -= 1 if t > 1
|
|
435
|
+
if 6 * t < 1 then madd(t2, (t1 - t2) * 6, t)
|
|
436
|
+
elsif 2 * t < 1 then t1
|
|
437
|
+
elsif 3 * t < 2 then madd(t2, (t1 - t2) * (TWO_THIRDS - t), 6.0)
|
|
438
|
+
else t2
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
Color.new(channel.call(h + ONE_THIRD), channel.call(h), channel.call(h - ONE_THIRD))
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# --- HSLuv (hsluv.go) ---
|
|
445
|
+
|
|
446
|
+
M = [
|
|
447
|
+
[3.2409699419045214, -1.5373831775700935, -0.49861076029300328],
|
|
448
|
+
[-0.96924363628087983, 1.8759675015077207, 0.041555057407175613],
|
|
449
|
+
[0.055630079696993609, -0.20397695888897657, 1.0569715142428786]
|
|
450
|
+
].freeze
|
|
451
|
+
KAPPA = 903.2962962962963
|
|
452
|
+
EPSILON = 0.0088564516790356308
|
|
453
|
+
|
|
454
|
+
def luv_lch_to_hsluv(l, c, h)
|
|
455
|
+
c *= 100.0
|
|
456
|
+
l *= 100.0
|
|
457
|
+
s = if l > 99.9999999 || l < 0.00000001
|
|
458
|
+
0.0
|
|
459
|
+
else
|
|
460
|
+
c / max_chroma_for_lh(l, h) * 100.0
|
|
461
|
+
end
|
|
462
|
+
[h, clamp01(s / 100.0), clamp01(l / 100.0)]
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def max_chroma_for_lh(l, h)
|
|
466
|
+
h_rad = h / 360.0 * Math::PI * 2.0
|
|
467
|
+
sin = Math.sin(h_rad)
|
|
468
|
+
cos = Math.cos(h_rad)
|
|
469
|
+
min_length = Float::MAX
|
|
470
|
+
get_bounds(l).each do |x, y|
|
|
471
|
+
length = y / msub(sin, x, cos)
|
|
472
|
+
min_length = length if length > 0.0 && length < min_length
|
|
473
|
+
end
|
|
474
|
+
min_length
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def get_bounds(l)
|
|
478
|
+
sub1 = pow(l + 16.0, 3.0) / 1_560_896.0
|
|
479
|
+
sub2 = sub1 > EPSILON ? sub1 : l / KAPPA
|
|
480
|
+
M.flat_map do |m|
|
|
481
|
+
[0.0, 1.0].map do |k|
|
|
482
|
+
top1 = msub(284_517.0 * m[0], 94_839.0, m[2]) * sub2
|
|
483
|
+
sum = madd(madd(838_422.0 * m[2], 769_860.0, m[1]), 731_718.0, m[0])
|
|
484
|
+
top2 = msub(sum * l * sub2, 769_860.0 * k, l)
|
|
485
|
+
bottom = madd(msub(632_260.0 * m[2], 126_452.0, m[1]) * sub2, 126_452.0, k)
|
|
486
|
+
[top1 / bottom, top2 / bottom]
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# A color in sRGB, each channel nominally in [0..1] (blends may leave the gamut).
|
|
492
|
+
Color = Struct.new(:r, :g, :b) do
|
|
493
|
+
include GoMath
|
|
494
|
+
|
|
495
|
+
# Color.Hex: "#rrggbb" with Go's +0.5 rounding and uint8 conversion.
|
|
496
|
+
def hex
|
|
497
|
+
format("#%02x%02x%02x", *rgb255)
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def rgb255
|
|
501
|
+
[Colorful.go_uint8(madd(0.5, r, 255.0)), Colorful.go_uint8(madd(0.5, g, 255.0)),
|
|
502
|
+
Colorful.go_uint8(madd(0.5, b, 255.0))]
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def clamped
|
|
506
|
+
Color.new(Colorful.clamp01(r), Colorful.clamp01(g), Colorful.clamp01(b))
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def linear_rgb
|
|
510
|
+
[Colorful.linearize(r), Colorful.linearize(g), Colorful.linearize(b)]
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def xyz
|
|
514
|
+
Colorful.linear_rgb_to_xyz(*linear_rgb)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def lab
|
|
518
|
+
Colorful.xyz_to_lab_white_ref(*xyz, D65)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def luv_white_ref(wref)
|
|
522
|
+
Colorful.xyz_to_luv_white_ref(*xyz, wref)
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def luv
|
|
526
|
+
luv_white_ref(D65)
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def hcl
|
|
530
|
+
Colorful.lab_to_hcl(*lab)
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def hsl
|
|
534
|
+
min = [[r, g].min, b].min
|
|
535
|
+
max = [[r, g].max, b].max
|
|
536
|
+
l = (max + min) / 2
|
|
537
|
+
return [0.0, 0.0, l] if min == max
|
|
538
|
+
|
|
539
|
+
s = l < 0.5 ? (max - min) / (max + min) : (max - min) / (2.0 - max - min)
|
|
540
|
+
h = if max == r then (g - b) / (max - min)
|
|
541
|
+
elsif max == g then 2.0 + (b - r) / (max - min)
|
|
542
|
+
else 4.0 + (r - g) / (max - min)
|
|
543
|
+
end
|
|
544
|
+
h *= 60
|
|
545
|
+
h += 360 if h < 0
|
|
546
|
+
[h, s, l]
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def hsluv
|
|
550
|
+
Colorful.luv_lch_to_hsluv(*Colorful.luv_to_luv_lch(*luv_white_ref(HSLUV_D65)))
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def distance_hsluv(other)
|
|
554
|
+
h1, s1, l1 = hsluv
|
|
555
|
+
h2, s2, l2 = other.hsluv
|
|
556
|
+
dh = (h1 - h2) / 100.0
|
|
557
|
+
Math.sqrt(madd(madd(dh * dh, s1 - s2, s1 - s2), l1 - l2, l1 - l2))
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def distance_luv(other)
|
|
561
|
+
l1, u1, v1 = luv
|
|
562
|
+
l2, u2, v2 = other.luv
|
|
563
|
+
Math.sqrt(madd(madd((l1 - l2) * (l1 - l2), u1 - u2, u1 - u2), v1 - v2, v1 - v2))
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def blend_rgb(other, t)
|
|
567
|
+
Color.new(madd(r, t, other.r - r), madd(g, t, other.g - g), madd(b, t, other.b - b))
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def blend_luv(other, t)
|
|
571
|
+
l1, u1, v1 = luv
|
|
572
|
+
l2, u2, v2 = other.luv
|
|
573
|
+
Colorful.luv(madd(l1, t, l2 - l1), madd(u1, t, u2 - u1), madd(v1, t, v2 - v1))
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def blend_hcl(other, t)
|
|
577
|
+
h1, c1, l1 = hcl
|
|
578
|
+
h2, c2, l2 = other.hcl
|
|
579
|
+
Colorful.hcl(Colorful.interp_angle(h1, h2, t), madd(c1, t, c2 - c1), madd(l1, t, l2 - l1)).clamped
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
end
|