wezen 0.1.0 → 0.1.1
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 +4 -0
- data/README.md +151 -14
- data/lib/wezen/gif.rb +15 -15
- data/lib/wezen/image.rb +15 -4
- data/lib/wezen/quantize.rb +97 -7
- data/lib/wezen/version.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f7f7a2045f7c3c436e4d3baeb78b64195f9512c88a2727faa679ac53115d8f17
|
|
4
|
+
data.tar.gz: 5f92c2a5bb410f6805f7a976a6ba64b42b8b79a4227b9c12c65471f017dfb93f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3b2d904c4dfe01a254043657f021fa210b5c4b962e58b800f8bef577d882a68bd66ccc6b204ee0b4a854f1760962e4fdadd77a3de8282bcb72940c5a37b04d2f
|
|
7
|
+
data.tar.gz: 20d8675fc875ca2bfcb609688fe20d1acd528f43bdb0624d4628ba5d3b7ae73833be58701deed845b312a1d29554176b4ebb19364b40e904bfff8464bf0c4c39
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -1,25 +1,69 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">Wezen</h1>
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>Dependency-free Ruby encoders for deterministic APNG, GIF, PNG, and asciinema demo media.</strong>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://rubygems.org/gems/wezen"><img src="https://img.shields.io/gem/v/wezen.svg?color=b7a66f" alt="Gem version"></a>
|
|
9
|
+
<a href="https://github.com/noxdea/wezen/actions/workflows/main.yml"><img src="https://github.com/noxdea/wezen/actions/workflows/main.yml/badge.svg" alt="CI status"></a>
|
|
10
|
+
<img src="https://img.shields.io/badge/Ruby-3.1%2B-cc342d" alt="Ruby 3.1 or newer">
|
|
11
|
+
<a href="LICENSE.txt"><img src="https://img.shields.io/badge/license-MIT-4c8eda" alt="MIT license"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="#features">Features</a> ·
|
|
16
|
+
<a href="#installation">Installation</a> ·
|
|
17
|
+
<a href="#quick-start">Quick start</a> ·
|
|
18
|
+
<a href="#output-formats">Output formats</a> ·
|
|
19
|
+
<a href="#development">Development</a>
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
Wezen turns RGBA frames and terminal events into portable, reproducible demo
|
|
25
|
+
assets. It is built for documentation, fixtures, and recording pipelines where
|
|
26
|
+
the same input should produce the same bytes.
|
|
27
|
+
|
|
28
|
+
The name comes from δ Canis Majoris: its IAU name derives from the Arabic
|
|
29
|
+
*al-wazn*, meaning “the weight.”
|
|
6
30
|
|
|
7
31
|
## Features
|
|
8
32
|
|
|
9
|
-
- Lossless APNG
|
|
10
|
-
- GIF89a
|
|
11
|
-
-
|
|
12
|
-
- Asciinema v2 JSON Lines
|
|
33
|
+
- Lossless APNG with optional changed-frame rectangles, plus PNG stills
|
|
34
|
+
- GIF89a encoding with deterministic median-cut palettes and dithering
|
|
35
|
+
- Frame coalescing, scaling, cropping, sampling, blending, and change bounds
|
|
36
|
+
- Asciinema v2 JSON Lines output for terminal recordings
|
|
37
|
+
- Byte-stable output for identical inputs
|
|
13
38
|
- No runtime dependencies beyond Ruby's `zlib` and `json` standard libraries
|
|
14
39
|
|
|
15
40
|
## Installation
|
|
16
41
|
|
|
42
|
+
Add Wezen to your Gemfile:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
gem "wezen"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then install dependencies:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
bundle install
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or install the gem directly:
|
|
55
|
+
|
|
17
56
|
```sh
|
|
18
57
|
gem install wezen
|
|
19
58
|
```
|
|
20
59
|
|
|
60
|
+
Wezen requires Ruby 3.1 or newer.
|
|
61
|
+
|
|
21
62
|
## Quick start
|
|
22
63
|
|
|
64
|
+
Create a two-frame animation and write lossless APNG and compatible GIF
|
|
65
|
+
versions:
|
|
66
|
+
|
|
23
67
|
```ruby
|
|
24
68
|
require "wezen"
|
|
25
69
|
|
|
@@ -31,22 +75,115 @@ Wezen::APNG.write("demo.apng", animation)
|
|
|
31
75
|
Wezen::GIF.write("demo.gif", animation, dither: :bayer)
|
|
32
76
|
```
|
|
33
77
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
78
|
+
Frames are packed RGBA strings in row-major order. `Animation#add` merges
|
|
79
|
+
identical consecutive frames and adds their delays automatically.
|
|
80
|
+
|
|
81
|
+
## Output formats
|
|
82
|
+
|
|
83
|
+
| Format | Use case | Behavior |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| APNG | Lossless animation | Preserves RGBA pixels and stores changed rectangles when possible |
|
|
86
|
+
| GIF89a | Broad compatibility | Uses a deterministic global palette with optional dithering |
|
|
87
|
+
| PNG | Still frames | Writes a single lossless RGBA image |
|
|
88
|
+
| Asciinema v2 | Terminal sessions | Writes chronological output, input, and resize events as JSON Lines |
|
|
89
|
+
|
|
90
|
+
### APNG
|
|
91
|
+
|
|
92
|
+
APNG is the preferred animation format when color and alpha fidelity matter:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
bytes = Wezen::APNG.encode(animation, compression: 9, diff: true)
|
|
96
|
+
File.binwrite("demo.apng", bytes)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### GIF
|
|
100
|
+
|
|
101
|
+
Control palette size, palette strategy, dithering, and frame differences:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
Wezen::GIF.write(
|
|
105
|
+
"demo.gif",
|
|
106
|
+
animation,
|
|
107
|
+
colors: 128,
|
|
108
|
+
palette: :global,
|
|
109
|
+
dither: :floyd_steinberg,
|
|
110
|
+
diff: true
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Provide a `Wezen::Palette` when the recording needs a fixed application palette.
|
|
115
|
+
Supported dithering modes are `:floyd_steinberg`, `:bayer`, and `:none`.
|
|
116
|
+
|
|
117
|
+
### PNG
|
|
118
|
+
|
|
119
|
+
Write a single packed RGBA frame:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
frame = animation.each.first
|
|
123
|
+
Wezen::PNG.write("frame.png", animation.width, animation.height, frame.rgba)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Asciinema casts
|
|
127
|
+
|
|
128
|
+
Create an asciinema v2 recording from chronological terminal events:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
events = [
|
|
132
|
+
Wezen::Cast::Event.new(0.0, :output, "$ bundle exec rake\r\n"),
|
|
133
|
+
Wezen::Cast::Event.new(0.4, :output, "12 examples, 0 failures\r\n")
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
Wezen::Cast.write(
|
|
137
|
+
"demo.cast",
|
|
138
|
+
width: 80,
|
|
139
|
+
height: 24,
|
|
140
|
+
title: "Test run",
|
|
141
|
+
events: events
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Event kinds are `:output`, `:input`, and `:resize`.
|
|
146
|
+
|
|
147
|
+
## Transform animations
|
|
148
|
+
|
|
149
|
+
Every transformation returns a new `Wezen::Animation`:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
animation = animation.coalesce(tolerance: 2)
|
|
153
|
+
animation = animation.scale(to_width: 640, to_height: 360)
|
|
154
|
+
animation = animation.crop([0, 0, 320, 180])
|
|
155
|
+
animation = animation.drop_to(fps: 12)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Use `Wezen::Image` directly for lower-level scaling, cropping, alpha blending,
|
|
159
|
+
and changed-pixel bounds.
|
|
160
|
+
|
|
161
|
+
## Design decisions
|
|
162
|
+
|
|
163
|
+
Wezen deliberately ships encoders without runtime decoders. This keeps the gem
|
|
164
|
+
small, portable, and focused while round-trip tests validate generated media.
|
|
165
|
+
|
|
166
|
+
The architecture records explain the durable format choices:
|
|
167
|
+
|
|
168
|
+
- [Encode-only scope](docs/adr/001-encode-only.md)
|
|
169
|
+
- [Deterministic global palette](docs/adr/002-global-palette.md)
|
|
170
|
+
- [APNG as the default animation format](docs/adr/003-apng-default.md)
|
|
37
171
|
|
|
38
172
|
## Development
|
|
39
173
|
|
|
40
174
|
```sh
|
|
41
175
|
bundle install
|
|
42
176
|
bundle exec rake
|
|
177
|
+
bundle exec rake bench
|
|
43
178
|
bundle exec rbs -I sig validate
|
|
44
179
|
gem build --strict wezen.gemspec
|
|
45
180
|
```
|
|
46
181
|
|
|
47
|
-
|
|
48
|
-
|
|
182
|
+
## Contributing
|
|
183
|
+
|
|
184
|
+
Bug reports and pull requests are welcome in the
|
|
185
|
+
[GitHub repository](https://github.com/noxdea/wezen).
|
|
49
186
|
|
|
50
187
|
## License
|
|
51
188
|
|
|
52
|
-
|
|
189
|
+
Wezen is available under the [MIT License](LICENSE.txt).
|
data/lib/wezen/gif.rb
CHANGED
|
@@ -24,18 +24,25 @@ module Wezen
|
|
|
24
24
|
|
|
25
25
|
previous = nil
|
|
26
26
|
animation.each.with_index do |frame, index|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
[[0, 0, animation.width, animation.height], indices]
|
|
27
|
+
rect, patch_rgba = if index.zero? || !diff || previous.nil?
|
|
28
|
+
[[0, 0, animation.width, animation.height], frame.rgba]
|
|
30
29
|
else
|
|
31
30
|
bounds = Image.diff_bounds(previous, frame.rgba, animation.width, animation.height)
|
|
32
31
|
if bounds
|
|
33
|
-
[bounds,
|
|
32
|
+
[bounds, Image.crop(frame.rgba, animation.width, animation.height, bounds)]
|
|
34
33
|
else
|
|
35
|
-
[[0, 0, 1, 1],
|
|
34
|
+
[[0, 0, 1, 1], nil]
|
|
36
35
|
end
|
|
37
36
|
end
|
|
38
|
-
|
|
37
|
+
patch = if patch_rgba
|
|
38
|
+
width = rect[2]
|
|
39
|
+
height = rect[3]
|
|
40
|
+
transparent = source_palette.transparent_index && patch_rgba.bytes.each_slice(4).any? { |pixel| pixel[3] < 128 }
|
|
41
|
+
Quantize.apply_with_size(patch_rgba, source_palette, width: width, height: height, dither: dither)
|
|
42
|
+
else
|
|
43
|
+
transparent = source_palette.transparent_index
|
|
44
|
+
patch_rgba
|
|
45
|
+
end
|
|
39
46
|
output << graphic_control(frame.delay_ms, transparent ? source_palette.transparent_index : nil)
|
|
40
47
|
output << image_descriptor(rect, bits)
|
|
41
48
|
output << sub_blocks(lzw(patch, [bits, 2].max), [bits, 2].max)
|
|
@@ -69,13 +76,6 @@ module Wezen
|
|
|
69
76
|
[44, x, y, width, height, 0].pack("CvvvvC")
|
|
70
77
|
end
|
|
71
78
|
|
|
72
|
-
def indexed_crop(indices, width, rect)
|
|
73
|
-
x, y, crop_width, crop_height = rect
|
|
74
|
-
output = String.new(capacity: crop_width * crop_height, encoding: Encoding::BINARY)
|
|
75
|
-
crop_height.times { |row| output << indices.byteslice((y + row) * width + x, crop_width) }
|
|
76
|
-
output
|
|
77
|
-
end
|
|
78
|
-
|
|
79
79
|
def sub_blocks(bytes, minimum_code_size)
|
|
80
80
|
output = minimum_code_size.chr
|
|
81
81
|
bytes.bytes.each_slice(255) { |slice| output << slice.length.chr << slice.pack("C*") }
|
|
@@ -105,7 +105,7 @@ module Wezen
|
|
|
105
105
|
if next_code < 4096
|
|
106
106
|
dictionary[key] = next_code
|
|
107
107
|
next_code += 1
|
|
108
|
-
code_size += 1 if next_code
|
|
108
|
+
code_size += 1 if next_code > (1 << code_size) && code_size < 12
|
|
109
109
|
else
|
|
110
110
|
writer.write(clear, code_size)
|
|
111
111
|
dictionary.clear
|
|
@@ -143,6 +143,6 @@ module Wezen
|
|
|
143
143
|
end
|
|
144
144
|
|
|
145
145
|
private_class_method :reserve_transparency, :loop_extension, :graphic_control, :image_descriptor,
|
|
146
|
-
:
|
|
146
|
+
:sub_blocks, :lzw
|
|
147
147
|
end
|
|
148
148
|
end
|
data/lib/wezen/image.rb
CHANGED
|
@@ -81,10 +81,21 @@ module Wezen
|
|
|
81
81
|
def diff_bounds(previous, current, width, height)
|
|
82
82
|
validate!(previous, width, height); validate!(current, width, height)
|
|
83
83
|
min_x = width; min_y = height; max_x = -1; max_y = -1
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
row_bytes = width * 4
|
|
85
|
+
height.times do |y|
|
|
86
|
+
row_offset = y * row_bytes
|
|
87
|
+
next if previous.byteslice(row_offset, row_bytes) == current.byteslice(row_offset, row_bytes)
|
|
88
|
+
|
|
89
|
+
width.times do |x|
|
|
90
|
+
offset = row_offset + x * 4
|
|
91
|
+
different = previous.getbyte(offset) != current.getbyte(offset) ||
|
|
92
|
+
previous.getbyte(offset + 1) != current.getbyte(offset + 1) ||
|
|
93
|
+
previous.getbyte(offset + 2) != current.getbyte(offset + 2) ||
|
|
94
|
+
previous.getbyte(offset + 3) != current.getbyte(offset + 3)
|
|
95
|
+
if different
|
|
96
|
+
min_x = x if x < min_x; min_y = y if y < min_y; max_x = x if x > max_x; max_y = y if y > max_y
|
|
97
|
+
end
|
|
98
|
+
end
|
|
88
99
|
end
|
|
89
100
|
max_x.negative? ? nil : [min_x, min_y, max_x - min_x + 1, max_y - min_y + 1]
|
|
90
101
|
end
|
data/lib/wezen/quantize.rb
CHANGED
|
@@ -4,13 +4,20 @@ module Wezen
|
|
|
4
4
|
module Quantize
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
|
+
MAX_PALETTE_SAMPLES = 32_768
|
|
8
|
+
|
|
7
9
|
def palette(animation, colors:, method: :median_cut)
|
|
8
10
|
raise ArgumentError, "unsupported quantizer: #{method}" unless %i[median_cut octree].include?(method)
|
|
9
11
|
limit = Integer(colors)
|
|
10
12
|
raise ArgumentError, "colors must be between 2 and 256" unless limit.between?(2, 256)
|
|
11
|
-
|
|
13
|
+
total_pixels = animation.width * animation.height * animation.size
|
|
14
|
+
pixels, transparent = if total_pixels <= MAX_PALETTE_SAMPLES
|
|
15
|
+
[animation.each.flat_map { |frame| frame.rgba.bytes.each_slice(4).reject { |pixel| pixel[3] < 128 }.map { |pixel| pixel[0, 3] } },
|
|
16
|
+
animation.each.any? { |frame| frame.rgba.bytes.each_slice(4).any? { |pixel| pixel[3] < 128 } }]
|
|
17
|
+
else
|
|
18
|
+
sample_pixels(animation, total_pixels)
|
|
19
|
+
end
|
|
12
20
|
pixels = [[0, 0, 0]] if pixels.empty?
|
|
13
|
-
transparent = animation.each.any? { |frame| frame.rgba.bytes.each_slice(4).any? { |pixel| pixel[3] < 128 } }
|
|
14
21
|
opaque_limit = transparent ? limit - 1 : limit
|
|
15
22
|
colors_out = median_cut(pixels, opaque_limit)
|
|
16
23
|
if transparent
|
|
@@ -21,6 +28,30 @@ module Wezen
|
|
|
21
28
|
end
|
|
22
29
|
end
|
|
23
30
|
|
|
31
|
+
def sample_pixels(animation, total_pixels)
|
|
32
|
+
# ponytail: cap median-cut input at 32K samples; replace with a weighted histogram if palette fidelity matters.
|
|
33
|
+
step = (total_pixels.fdiv(MAX_PALETTE_SAMPLES)).ceil
|
|
34
|
+
pixels = []
|
|
35
|
+
transparent = false
|
|
36
|
+
pixel_index = 0
|
|
37
|
+
next_sample = 0
|
|
38
|
+
animation.each do |frame|
|
|
39
|
+
rgba = frame.rgba
|
|
40
|
+
offset = 0
|
|
41
|
+
while offset < rgba.bytesize
|
|
42
|
+
alpha = rgba.getbyte(offset + 3)
|
|
43
|
+
transparent ||= alpha < 128
|
|
44
|
+
if pixel_index == next_sample
|
|
45
|
+
pixels << [rgba.getbyte(offset), rgba.getbyte(offset + 1), rgba.getbyte(offset + 2)] if alpha >= 128
|
|
46
|
+
next_sample += step
|
|
47
|
+
end
|
|
48
|
+
pixel_index += 1
|
|
49
|
+
offset += 4
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
[pixels, transparent]
|
|
53
|
+
end
|
|
54
|
+
|
|
24
55
|
def apply(rgba, palette, width: nil, height: nil, dither: :floyd_steinberg)
|
|
25
56
|
raise ArgumentError, "unknown dither: #{dither}" unless %i[none bayer floyd_steinberg].include?(dither)
|
|
26
57
|
raise ArgumentError, "palette must be a Wezen::Palette" unless palette.respond_to?(:colors)
|
|
@@ -30,7 +61,11 @@ module Wezen
|
|
|
30
61
|
end
|
|
31
62
|
|
|
32
63
|
def apply_with_size(rgba, palette, width:, height:, dither: :floyd_steinberg)
|
|
33
|
-
|
|
64
|
+
width = Integer(width)
|
|
65
|
+
height = Integer(height)
|
|
66
|
+
raise ArgumentError, "invalid RGBA pixels" unless rgba.bytesize == width * height * 4
|
|
67
|
+
return apply_without_error(rgba, palette, width, height, dither) unless dither == :floyd_steinberg
|
|
68
|
+
|
|
34
69
|
colors = palette.colors
|
|
35
70
|
output = String.new(capacity: width * height, encoding: Encoding::BINARY)
|
|
36
71
|
errors = Array.new(width * 3, 0.0)
|
|
@@ -65,6 +100,48 @@ module Wezen
|
|
|
65
100
|
output
|
|
66
101
|
end
|
|
67
102
|
|
|
103
|
+
def apply_without_error(rgba, palette, width, height, dither)
|
|
104
|
+
colors = palette.colors
|
|
105
|
+
transparent_index = palette.transparent_index
|
|
106
|
+
output = String.new(capacity: width * height, encoding: Encoding::BINARY)
|
|
107
|
+
cache = {}
|
|
108
|
+
offset = 0
|
|
109
|
+
x = 0
|
|
110
|
+
y = 0
|
|
111
|
+
while offset < rgba.bytesize
|
|
112
|
+
alpha = rgba.getbyte(offset + 3)
|
|
113
|
+
if alpha < 128 && transparent_index
|
|
114
|
+
output << transparent_index
|
|
115
|
+
else
|
|
116
|
+
red = rgba.getbyte(offset)
|
|
117
|
+
green = rgba.getbyte(offset + 1)
|
|
118
|
+
blue = rgba.getbyte(offset + 2)
|
|
119
|
+
if dither == :bayer
|
|
120
|
+
adjustment = (bayer(x, y) * 16 - 8).to_i
|
|
121
|
+
red = red + adjustment
|
|
122
|
+
green = green + adjustment
|
|
123
|
+
blue = blue + adjustment
|
|
124
|
+
red = 0 if red < 0
|
|
125
|
+
green = 0 if green < 0
|
|
126
|
+
blue = 0 if blue < 0
|
|
127
|
+
red = 255 if red > 255
|
|
128
|
+
green = 255 if green > 255
|
|
129
|
+
blue = 255 if blue > 255
|
|
130
|
+
end
|
|
131
|
+
key = (red << 16) | (green << 8) | blue
|
|
132
|
+
cache[key] ||= nearest_rgb(red, green, blue, colors, transparent_index)
|
|
133
|
+
output << cache[key]
|
|
134
|
+
end
|
|
135
|
+
offset += 4
|
|
136
|
+
x += 1
|
|
137
|
+
if x == width
|
|
138
|
+
x = 0
|
|
139
|
+
y += 1
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
output
|
|
143
|
+
end
|
|
144
|
+
|
|
68
145
|
def median_cut(pixels, limit)
|
|
69
146
|
boxes = [pixels.sort_by { |pixel| pixel[0] * 65_536 + pixel[1] * 256 + pixel[2] }]
|
|
70
147
|
while boxes.length < limit
|
|
@@ -87,9 +164,22 @@ module Wezen
|
|
|
87
164
|
end
|
|
88
165
|
|
|
89
166
|
def nearest(pixel, colors, transparent_index)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
167
|
+
nearest_rgb(pixel[0], pixel[1], pixel[2], colors, transparent_index)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def nearest_rgb(red, green, blue, colors, transparent_index)
|
|
171
|
+
best_index = nil
|
|
172
|
+
best_distance = nil
|
|
173
|
+
colors.each_with_index do |color, index|
|
|
174
|
+
next if index == transparent_index
|
|
175
|
+
|
|
176
|
+
distance = (red - color[0])**2 + (green - color[1])**2 + (blue - color[2])**2
|
|
177
|
+
if best_distance.nil? || distance < best_distance
|
|
178
|
+
best_distance = distance
|
|
179
|
+
best_index = index
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
best_index
|
|
93
183
|
end
|
|
94
184
|
|
|
95
185
|
def bayer(x, y)
|
|
@@ -103,6 +193,6 @@ module Wezen
|
|
|
103
193
|
add.call(next_errors, x, 5.0 / 16)
|
|
104
194
|
add.call(next_errors, x + 1, 1.0 / 16)
|
|
105
195
|
end
|
|
106
|
-
private_class_method :median_cut, :nearest, :bayer, :distribute
|
|
196
|
+
private_class_method :sample_pixels, :median_cut, :nearest, :nearest_rgb, :apply_without_error, :bayer, :distribute
|
|
107
197
|
end
|
|
108
198
|
end
|
data/lib/wezen/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wezen
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yudai Takada
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-09-20 00:00:00.000000000 Z
|
|
11
12
|
dependencies: []
|
|
12
13
|
description: Pure Ruby encoders and pixel primitives for deterministic demo recordings.
|
|
13
14
|
email:
|
|
@@ -44,6 +45,7 @@ metadata:
|
|
|
44
45
|
source_code_uri: https://github.com/noxdea/wezen/tree/main
|
|
45
46
|
changelog_uri: https://github.com/noxdea/wezen/blob/main/CHANGELOG.md
|
|
46
47
|
rubygems_mfa_required: 'true'
|
|
48
|
+
post_install_message:
|
|
47
49
|
rdoc_options: []
|
|
48
50
|
require_paths:
|
|
49
51
|
- lib
|
|
@@ -58,7 +60,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
58
60
|
- !ruby/object:Gem::Version
|
|
59
61
|
version: '0'
|
|
60
62
|
requirements: []
|
|
61
|
-
rubygems_version: 4.
|
|
63
|
+
rubygems_version: 3.4.19
|
|
64
|
+
signing_key:
|
|
62
65
|
specification_version: 4
|
|
63
66
|
summary: Deterministic GIF, APNG, PNG, and asciinema encoders for Ruby
|
|
64
67
|
test_files: []
|