pixelart 0.1.2 → 0.1.7
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/Manifest.txt +6 -0
- data/README.md +55 -11
- data/Rakefile +1 -1
- data/lib/pixelart.rb +6 -20
- data/lib/pixelart/base.rb +36 -0
- data/lib/pixelart/color.rb +132 -0
- data/lib/pixelart/gradient.rb +106 -0
- data/lib/pixelart/image.rb +61 -65
- data/lib/pixelart/misc.rb +37 -0
- data/lib/pixelart/palette.rb +72 -0
- data/lib/pixelart/pixelator.rb +117 -0
- data/lib/pixelart/version.rb +1 -1
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0dd959d7ee982c2634f325c6e7f91bfb46d12dc61a79f364d0b7caae529d59f1
|
4
|
+
data.tar.gz: 024f66e021b395b4141c54298e2fb0f31b47d28952a5c03d56ecfaf75c68b1be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 71093d3b9977140303838bf9628c1de54c47f978a556945321fd849d3ebb26912d50846050c15efe3f1a2cf4d8b0991940c08f4a63511451148f63d2b9e788f5
|
7
|
+
data.tar.gz: 41c97279da0b7f81430a56c1c8e88be1ebc2bac61ea6b7a136be1accfe3cd71b7c5e0c3885d3a7dfa5d021852d4550e3f692b151767a18f6fcf7a8c9fd5340d0
|
data/Manifest.txt
CHANGED
data/README.md
CHANGED
@@ -3,8 +3,8 @@
|
|
3
3
|
pixelart - mint your own pixel art images off chain using any design (in ascii text) in any colors; incl. 2x/4x/8x zoom for bigger sizes
|
4
4
|
|
5
5
|
|
6
|
-
* home :: [github.com/
|
7
|
-
* bugs :: [github.com/
|
6
|
+
* home :: [github.com/rubycoco/pixel](https://github.com/rubycoco/pixel)
|
7
|
+
* bugs :: [github.com/rubycoco/pixel/issues](https://github.com/rubycoco/pixel/issues)
|
8
8
|
* gem :: [rubygems.org/gems/pixelart](https://rubygems.org/gems/pixelart)
|
9
9
|
* rdoc :: [rubydoc.info/gems/pixelart](http://rubydoc.info/gems/pixelart)
|
10
10
|
|
@@ -58,7 +58,7 @@ Note: The color 0 (transparent) is auto-magically added / defined.
|
|
58
58
|
And let's mint a mooncat image:
|
59
59
|
|
60
60
|
``` ruby
|
61
|
-
img =
|
61
|
+
img = Image.parse( pixels, colors: colors )
|
62
62
|
img.save( './i/mooncat_white.png' )
|
63
63
|
```
|
64
64
|
|
@@ -71,8 +71,8 @@ img3x.save( './i/mooncat_white-3x.png' )
|
|
71
71
|
|
72
72
|
Voila!
|
73
73
|
|
74
|
-

|
75
|
+

|
76
76
|
|
77
77
|
|
78
78
|
|
@@ -91,7 +91,7 @@ colors = [
|
|
91
91
|
And let's start minting:
|
92
92
|
|
93
93
|
``` ruby
|
94
|
-
img =
|
94
|
+
img = Image.parse( pixels, colors: colors )
|
95
95
|
img.save( './i/mooncat_black.png' )
|
96
96
|
|
97
97
|
img3x = img.zoom( 3 )
|
@@ -100,8 +100,8 @@ img3x.save( './i/mooncat_black-3x.png' )
|
|
100
100
|
|
101
101
|
Voila! Black is the new White!
|
102
102
|
|
103
|
-

|
104
|
+

|
105
105
|
|
106
106
|
|
107
107
|
|
@@ -214,7 +214,7 @@ colors = {
|
|
214
214
|
And let's mint an imperial master image:
|
215
215
|
|
216
216
|
``` ruby
|
217
|
-
img =
|
217
|
+
img = Image.parse( pixels, colors: colors )
|
218
218
|
img.save( './i/vader.png' )
|
219
219
|
```
|
220
220
|
|
@@ -227,8 +227,52 @@ img5x.save( './i/vader5x.png' )
|
|
227
227
|
|
228
228
|
Voila!
|
229
229
|
|
230
|
-

|
231
|
+

|
232
|
+
|
233
|
+
|
234
|
+
|
235
|
+
|
236
|
+
## Modular "Base" Version
|
237
|
+
|
238
|
+
|
239
|
+
Note: By default if you require pixelart
|
240
|
+
all classes inside the `Pixelart` module such as `Image`, `Color`, `Gradient`, `Palette8bit`, etc. get "top-leveled", that is,
|
241
|
+
included in the top level e.g.:
|
242
|
+
|
243
|
+
``` ruby
|
244
|
+
require 'pixelart/base'
|
245
|
+
include Pixelart
|
246
|
+
```
|
247
|
+
|
248
|
+
And now you can use all classes without
|
249
|
+
the `Pixelart::` module scope e.g.:
|
250
|
+
|
251
|
+
``` ruby
|
252
|
+
gradient = Gradient.new( '000000', 'ffffff' )
|
253
|
+
|
254
|
+
pp colors = gradient.colors( 256 )
|
255
|
+
puts '---'
|
256
|
+
pp colors.map { |color| Color.to_hex( color ) }
|
257
|
+
```
|
258
|
+
|
259
|
+
vs
|
260
|
+
|
261
|
+
``` ruby
|
262
|
+
gradient = Pixelart::Gradient.new( '000000', 'ffffff' )
|
263
|
+
|
264
|
+
pp colors = gradient.colors( 256 )
|
265
|
+
puts '---'
|
266
|
+
pp colors.map { |color| Pixelart::Color.to_hex( color ) }
|
267
|
+
```
|
268
|
+
|
269
|
+
|
270
|
+
For a "stricter" modular version require the "base" version
|
271
|
+
that always requires the `Pixelart::` module scope e.g.:
|
272
|
+
|
273
|
+
``` ruby
|
274
|
+
require 'pixelart/base'
|
275
|
+
```
|
232
276
|
|
233
277
|
|
234
278
|
|
data/Rakefile
CHANGED
@@ -8,7 +8,7 @@ Hoe.spec 'pixelart' do
|
|
8
8
|
self.summary = "pixelart - mint your own pixel art images off chain using any design (in ascii text) in any colors; incl. 2x/4x/8x zoom for bigger sizes"
|
9
9
|
self.description = summary
|
10
10
|
|
11
|
-
self.urls = { home: 'https://github.com/
|
11
|
+
self.urls = { home: 'https://github.com/rubycoco/pixel' }
|
12
12
|
|
13
13
|
self.author = 'Gerald Bauer'
|
14
14
|
self.email = 'wwwmake@googlegroups.com'
|
data/lib/pixelart.rb
CHANGED
@@ -1,24 +1,10 @@
|
|
1
|
-
## 3rd party
|
2
|
-
require 'chunky_png'
|
3
1
|
|
4
|
-
##
|
5
|
-
require '
|
6
|
-
require 'time'
|
7
|
-
require 'date'
|
8
|
-
require 'fileutils'
|
2
|
+
## our own code (without "top-level" shortcuts e.g. "modular version")
|
3
|
+
require 'pixelart/base' # aka "strict(er)" version
|
9
4
|
|
10
5
|
|
11
|
-
|
12
|
-
|
13
|
-
|
6
|
+
###
|
7
|
+
# add convenience top-level shortcuts / aliases
|
8
|
+
# make Image, Color, Palette8bit, etc top-level
|
9
|
+
include Pixelart
|
14
10
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
### add some convenience shortcuts
|
20
|
-
PixelArt = Pixelart
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
puts Pixelart.banner # say hello
|
@@ -0,0 +1,36 @@
|
|
1
|
+
## 3rd party
|
2
|
+
require 'chunky_png'
|
3
|
+
|
4
|
+
## stdlib
|
5
|
+
require 'pp'
|
6
|
+
require 'time'
|
7
|
+
require 'date'
|
8
|
+
require 'fileutils'
|
9
|
+
|
10
|
+
|
11
|
+
## our own code
|
12
|
+
require 'pixelart/version' # note: let version always go first
|
13
|
+
require 'pixelart/color'
|
14
|
+
require 'pixelart/gradient'
|
15
|
+
require 'pixelart/palette'
|
16
|
+
require 'pixelart/image'
|
17
|
+
|
18
|
+
require 'pixelart/pixelator'
|
19
|
+
|
20
|
+
require 'pixelart/misc' ## misc helpers
|
21
|
+
|
22
|
+
|
23
|
+
##########
|
24
|
+
# add some spelling convenience variants
|
25
|
+
PixelArt = Pixelart
|
26
|
+
|
27
|
+
module Pixelart
|
28
|
+
Palette256 = Palette8Bit = Palette8bit
|
29
|
+
|
30
|
+
Palette256Image = Palette8BitImage = Palette8bitImage =
|
31
|
+
ImagePalette256 = ImagePalette8Bit = ImagePalette8bit
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
|
36
|
+
puts Pixelart.banner # say hello
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module Pixelart
|
2
|
+
|
3
|
+
|
4
|
+
class Color
|
5
|
+
TRANSPARENT = 0 # rgba( 0, 0, 0, 0)
|
6
|
+
BLACK = 0xff # rgba( 0, 0, 0,255)
|
7
|
+
WHITE = 0xffffffff # rgba(255,255,255,255)
|
8
|
+
|
9
|
+
|
10
|
+
|
11
|
+
def self.parse( color )
|
12
|
+
if color.is_a?( Integer ) ## e.g. assumes ChunkyPNG::Color.rgb() or such
|
13
|
+
color ## pass through as is 1:1
|
14
|
+
elsif color.is_a?( Array ) ## assume array of hsl(a) e. g. [180, 0.86, 0.88]
|
15
|
+
from_hsl( *color )
|
16
|
+
elsif color.is_a?( String )
|
17
|
+
if color.downcase == 'transparent' ## special case for builtin colors
|
18
|
+
TRANSPARENT
|
19
|
+
else
|
20
|
+
## note: return an Integer !!! (not a Color class or such!!! )
|
21
|
+
from_hex( color )
|
22
|
+
end
|
23
|
+
else
|
24
|
+
raise ArgumentError, "unknown color format; cannot parse - expected rgb hex string e.g. d3d3d3"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.from_hex( hex )
|
29
|
+
## Creates a color by converting it from a string in hex notation.
|
30
|
+
##
|
31
|
+
## It supports colors with (#rrggbbaa) or without (#rrggbb)
|
32
|
+
## alpha channel as well as the 3-digit short format (#rgb)
|
33
|
+
## for those without. Color strings may include
|
34
|
+
## the prefix "0x" or "#"".
|
35
|
+
ChunkyPNG::Color.from_hex( hex )
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.from_hsl( hue, saturation, lightness, alpha=255)
|
39
|
+
ChunkyPNG::Color.from_hsl( hue,
|
40
|
+
saturation,
|
41
|
+
lightness,
|
42
|
+
alpha )
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def self.to_hex( color, include_alpha: true )
|
47
|
+
ChunkyPNG::Color.to_hex( color, include_alpha )
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.to_hsl( color, include_alpha: true )
|
51
|
+
# Returns an array with the separate HSL components of a color.
|
52
|
+
ChunkyPNG::Color.to_hsl( color, include_alpha )
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.r( color ) ChunkyPNG::Color.r( color ); end
|
56
|
+
def self.g( color ) ChunkyPNG::Color.g( color ); end
|
57
|
+
def self.b( color ) ChunkyPNG::Color.b( color ); end
|
58
|
+
|
59
|
+
def self.rgb( r, g, b ) ChunkyPNG::Color.rgb( r, g, b); end
|
60
|
+
|
61
|
+
|
62
|
+
|
63
|
+
## known built-in color names
|
64
|
+
def self.build_names
|
65
|
+
names = {
|
66
|
+
'#00000000' => 'TRANSPARENT',
|
67
|
+
'#000000ff' => 'BLACK',
|
68
|
+
'#ffffffff' => 'WHITE',
|
69
|
+
}
|
70
|
+
|
71
|
+
## auto-add grayscale 1 to 254
|
72
|
+
(1..254).each do |n|
|
73
|
+
hex = "#" + ('%02x' % n)*3
|
74
|
+
hex << "ff" ## add alpha channel (255)
|
75
|
+
names[ hex ] = "8-BIT GRAYSCALE ##{n}"
|
76
|
+
end
|
77
|
+
|
78
|
+
names
|
79
|
+
end
|
80
|
+
|
81
|
+
NAMES = build_names
|
82
|
+
|
83
|
+
|
84
|
+
|
85
|
+
def self.format( color )
|
86
|
+
rgb = [r(color),
|
87
|
+
g(color),
|
88
|
+
b(color)]
|
89
|
+
|
90
|
+
# rgb in hex (string format)
|
91
|
+
# note: do NOT include alpha channel for now - why? why not?
|
92
|
+
hex = "#" + rgb.map{|num| '%02x' % num }.join
|
93
|
+
|
94
|
+
hsl = to_hsl( color )
|
95
|
+
## get alpha channel (transparency) for hsla
|
96
|
+
alpha = hsl[3]
|
97
|
+
|
98
|
+
|
99
|
+
buf = ''
|
100
|
+
buf << hex
|
101
|
+
buf << " / "
|
102
|
+
buf << "rgb("
|
103
|
+
buf << "%3d " % rgb[0]
|
104
|
+
buf << "%3d " % rgb[1]
|
105
|
+
buf << "%3d)" % rgb[2]
|
106
|
+
buf << " - "
|
107
|
+
buf << "hsl("
|
108
|
+
buf << "%3d° " % (hsl[0] % 360)
|
109
|
+
buf << "%3d%% " % (hsl[1]*100+0.5).to_i
|
110
|
+
buf << "%3d%%)" % (hsl[2]*100+0.5).to_i
|
111
|
+
|
112
|
+
if alpha != 255
|
113
|
+
buf << " - α(%3d%%)" % (alpha*100/255+0.5).to_i
|
114
|
+
else
|
115
|
+
buf << " " ## add empty for 255 (full opacity)
|
116
|
+
end
|
117
|
+
|
118
|
+
## note: add alpha channel to hex
|
119
|
+
alpha_hex = '%02x' % alpha
|
120
|
+
name = NAMES[ hex+alpha_hex ]
|
121
|
+
buf << " - #{name}" if name
|
122
|
+
|
123
|
+
buf
|
124
|
+
end
|
125
|
+
class << self
|
126
|
+
alias_method :fmt, :format
|
127
|
+
end
|
128
|
+
|
129
|
+
end # class Color
|
130
|
+
end # module Pixelart
|
131
|
+
|
132
|
+
|
@@ -0,0 +1,106 @@
|
|
1
|
+
|
2
|
+
## inspired / helped by
|
3
|
+
## https://en.wikipedia.org/wiki/List_of_software_palettes#Color_gradient_palettes
|
4
|
+
## https://github.com/mistic100/tinygradient
|
5
|
+
## https://mistic100.github.io/tinygradient/
|
6
|
+
## https://bsouthga.dev/posts/color-gradients-with-python
|
7
|
+
|
8
|
+
|
9
|
+
module Pixelart
|
10
|
+
|
11
|
+
class Gradient
|
12
|
+
|
13
|
+
def initialize( *stops )
|
14
|
+
## note: convert stop colors to rgb triplets e.g.
|
15
|
+
## from #ffffff to [255,255,255]
|
16
|
+
## #000000 to [0,0,0] etc.
|
17
|
+
@stops = stops.map do |stop|
|
18
|
+
stop = Color.parse( stop )
|
19
|
+
[Color.r(stop), Color.g(stop), Color.b(stop)]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def colors( steps )
|
25
|
+
segments = @stops.size - 1
|
26
|
+
|
27
|
+
## note: gradient will include start (first)
|
28
|
+
## AND stop color (last) - stop color is NOT excluded for now!!
|
29
|
+
if segments == 1
|
30
|
+
start = @stops[0]
|
31
|
+
stop = @stops[1]
|
32
|
+
|
33
|
+
gradient = linear_gradient( start, stop, steps,
|
34
|
+
include_stop: true )
|
35
|
+
else
|
36
|
+
steps_per_segment, mod = steps.divmod( segments )
|
37
|
+
raise ArgumentError, "steps (#{steps}) must be divisible by # of segments (#{segments}); expected mod of 0 but got #{mod}" if mod != 0
|
38
|
+
|
39
|
+
gradient = []
|
40
|
+
segments.times do |segment|
|
41
|
+
start = @stops[segment]
|
42
|
+
stop = @stops[segment+1]
|
43
|
+
include_stop = (segment == segments-1) ## note: only include stop if last segment!!
|
44
|
+
|
45
|
+
# print " segment #{segment+1}/#{segments} #{steps_per_segment} color(s) - "
|
46
|
+
# print " start: #{start.inspect} "
|
47
|
+
# print include_stop ? 'include' : 'exclude'
|
48
|
+
# print " stop: #{stop.inspect}"
|
49
|
+
# print "\n"
|
50
|
+
|
51
|
+
gradient += linear_gradient( start, stop, steps_per_segment,
|
52
|
+
include_stop: include_stop )
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
## convert to color (Integer)
|
57
|
+
gradient.map do |color|
|
58
|
+
Color.rgb( *color )
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
|
64
|
+
def interpolate( start, stop, steps, n )
|
65
|
+
## note: n - expected to start with 1,2,3,etc.
|
66
|
+
color = []
|
67
|
+
3.times do |i|
|
68
|
+
stepize = Float(stop[i] - start[i]) / Float(steps-1)
|
69
|
+
value = stepize * n
|
70
|
+
## convert back to Integer from Float
|
71
|
+
## add 0.5 for rounding up (starting with 0.5) - why? why not?
|
72
|
+
value = (value+0.5).to_i
|
73
|
+
value = start[i] + value
|
74
|
+
|
75
|
+
color << value
|
76
|
+
end
|
77
|
+
color
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
def linear_gradient( start, stop, steps,
|
82
|
+
include_stop: true )
|
83
|
+
|
84
|
+
gradient = [start] ## auto-add start color (first color in gradient)
|
85
|
+
|
86
|
+
if include_stop
|
87
|
+
1.upto( steps-2 ).each do |n| ## sub two (-2), that is, start and stop color
|
88
|
+
gradient << interpolate( start, stop, steps, n )
|
89
|
+
end
|
90
|
+
# note: use original passed in stop color (should match calculated)
|
91
|
+
gradient << stop
|
92
|
+
else
|
93
|
+
1.upto( steps-1 ).each do |n| ## sub one (-1), that is, start color only
|
94
|
+
## note: add one (+1) to steps because stop color gets excluded (not included)!!
|
95
|
+
gradient << interpolate( start, stop, steps+1, n )
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
gradient
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
|
104
|
+
end # class Gradient
|
105
|
+
end # module Pixelart
|
106
|
+
|
data/lib/pixelart/image.rb
CHANGED
@@ -1,57 +1,5 @@
|
|
1
1
|
module Pixelart
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
class Color
|
7
|
-
def self.parse( color )
|
8
|
-
if color.is_a?( Integer ) ## e.g. assumes ChunkyPNG::Color.rgb() or such
|
9
|
-
color ## pass through as is 1:1
|
10
|
-
elsif color.is_a?( Array ) ## assume array of hsl(a) e. g. [180, 0.86, 0.88]
|
11
|
-
ChunkyPNG::Color.from_hsl( *color )
|
12
|
-
elsif color.is_a?( String )
|
13
|
-
if color.downcase == 'transparent' ## special case for builtin colors
|
14
|
-
ChunkyPNG::Color::TRANSPARENT
|
15
|
-
else
|
16
|
-
## note: return an Integer !!! (not a Color class or such!!! )
|
17
|
-
ChunkyPNG::Color.from_hex( color )
|
18
|
-
end
|
19
|
-
else
|
20
|
-
raise ArgumentError, "unknown color format; cannot parse - expected rgb hex string e.g. d3d3d3"
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def self.from_hex( hex )
|
25
|
-
## Creates a color by converting it from a string in hex notation.
|
26
|
-
##
|
27
|
-
## It supports colors with (#rrggbbaa) or without (#rrggbb)
|
28
|
-
## alpha channel as well as the 3-digit short format (#rgb)
|
29
|
-
## for those without. Color strings may include
|
30
|
-
## the prefix "0x" or "#"".
|
31
|
-
ChunkyPNG::Color.from_hex( hex )
|
32
|
-
end
|
33
|
-
|
34
|
-
def self.from_hsl( hue, saturation, lightness, alpha=255)
|
35
|
-
ChunkyPNG::Color.from_hsl( hue,
|
36
|
-
saturation,
|
37
|
-
lightness,
|
38
|
-
alpha )
|
39
|
-
end
|
40
|
-
|
41
|
-
|
42
|
-
def self.to_hex( color, include_alpha: true )
|
43
|
-
ChunkyPNG::Color.to_hex( color, include_alpha )
|
44
|
-
end
|
45
|
-
|
46
|
-
def self.to_hsl( color, include_alpha: true )
|
47
|
-
# Returns an array with the separate HSL components of a color.
|
48
|
-
ChunkyPNG::Color.to_hsl( color, include_alpha )
|
49
|
-
end
|
50
|
-
end # class Color
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
3
|
class Image
|
56
4
|
|
57
5
|
def self.read( path ) ## convenience helper
|
@@ -82,7 +30,7 @@ end
|
|
82
30
|
|
83
31
|
|
84
32
|
|
85
|
-
def initialize( width, height, initial=
|
33
|
+
def initialize( width, height, initial=Color::TRANSPARENT )
|
86
34
|
|
87
35
|
if initial.is_a?( ChunkyPNG::Image )
|
88
36
|
@img = initial
|
@@ -117,19 +65,71 @@ alias_method :scale, :zoom
|
|
117
65
|
|
118
66
|
|
119
67
|
|
68
|
+
#######################
|
69
|
+
## filter / effects
|
120
70
|
|
121
|
-
def
|
122
|
-
|
123
|
-
|
124
|
-
end.to_h
|
71
|
+
def grayscale
|
72
|
+
img = @img.grayscale
|
73
|
+
Image.new( img.width, img.height, img )
|
125
74
|
end
|
126
75
|
|
127
76
|
## add replace_colors alias too? - why? why not?
|
128
77
|
def change_colors( color_map )
|
78
|
+
color_map = _parse_color_map( color_map )
|
79
|
+
|
129
80
|
img = @img.dup ## note: make a deep copy!!!
|
130
|
-
|
131
|
-
## pp color_map
|
81
|
+
_change_colors!( img, color_map )
|
132
82
|
|
83
|
+
## wrap into Pixelart::Image - lets you use zoom() and such
|
84
|
+
Image.new( img.width, img.height, img )
|
85
|
+
end
|
86
|
+
alias_method :recolor, :change_colors
|
87
|
+
|
88
|
+
|
89
|
+
|
90
|
+
## predefined palette8bit color maps
|
91
|
+
## (grayscale to sepia/blue/false/etc.)
|
92
|
+
## - todo/check - keep "shortcut" convenience predefined map - why? why not?
|
93
|
+
PALETTE8BIT = {
|
94
|
+
sepia: Palette8bit::GRAYSCALE.zip( Palette8bit::SEPIA ).to_h,
|
95
|
+
blue: Palette8bit::GRAYSCALE.zip( Palette8bit::BLUE ).to_h,
|
96
|
+
false: Palette8bit::GRAYSCALE.zip( Palette8bit::FALSE ).to_h,
|
97
|
+
}
|
98
|
+
|
99
|
+
def change_palette8bit( palette )
|
100
|
+
## step 0: mapping from grayscale to new 8bit palette (256 colors)
|
101
|
+
color_map = if palette.is_a?( String ) || palette.is_a?( Symbol )
|
102
|
+
PALETTE8BIT[ palette.to_sym ]
|
103
|
+
## todo/fix: check for missing/undefined palette not found - why? why not?
|
104
|
+
else
|
105
|
+
## make sure we have colors all in Integer not names, hex, etc.
|
106
|
+
palette = _parse_colors( palette )
|
107
|
+
Palette8bit::GRAYSCALE.zip( palette ).to_h
|
108
|
+
end
|
109
|
+
|
110
|
+
## step 1: convert to grayscale (256 colors)
|
111
|
+
img = @img.grayscale
|
112
|
+
_change_colors!( img, color_map )
|
113
|
+
|
114
|
+
## wrap into Pixelart::Image - lets you use zoom() and such
|
115
|
+
Image.new( img.width, img.height, img )
|
116
|
+
end
|
117
|
+
alias_method :change_palette256, :change_palette8bit
|
118
|
+
|
119
|
+
|
120
|
+
####
|
121
|
+
## private helpers
|
122
|
+
def _parse_colors( colors )
|
123
|
+
colors.map {|color| Color.parse( color ) }
|
124
|
+
end
|
125
|
+
|
126
|
+
def _parse_color_map( color_map )
|
127
|
+
color_map.map do |k,v|
|
128
|
+
[Color.parse(k), Color.parse(v)]
|
129
|
+
end.to_h
|
130
|
+
end
|
131
|
+
|
132
|
+
def _change_colors!( img, color_map )
|
133
133
|
img.width.times do |x|
|
134
134
|
img.height.times do |y|
|
135
135
|
color = img[x,y]
|
@@ -137,12 +137,7 @@ def change_colors( color_map )
|
|
137
137
|
img[x,y] = new_color if new_color
|
138
138
|
end
|
139
139
|
end
|
140
|
-
|
141
|
-
## wrap into Pixelart::Image - lets you use zoom() and such
|
142
|
-
Image.new( img.width, img.height, img )
|
143
140
|
end
|
144
|
-
alias_method :recolor, :change_colors
|
145
|
-
|
146
141
|
|
147
142
|
|
148
143
|
|
@@ -176,6 +171,7 @@ def []=( x, y, value ) @img[x,y]=value; end
|
|
176
171
|
def pixels() @img.pixels; end
|
177
172
|
|
178
173
|
## return image ref - use a different name - why? why not?
|
174
|
+
## change to to_image - why? why not?
|
179
175
|
def image() @img; end
|
180
176
|
|
181
177
|
|
@@ -204,7 +200,7 @@ end
|
|
204
200
|
def self.parse_colors( colors )
|
205
201
|
if colors.is_a?( Array ) ## convenience shortcut
|
206
202
|
## note: always auto-add color 0 as pre-defined transparent - why? why not?
|
207
|
-
h = { '0' =>
|
203
|
+
h = { '0' => Color::TRANSPARENT }
|
208
204
|
colors.each_with_index do |color, i|
|
209
205
|
h[ (i+1).to_s ] = Color.parse( color )
|
210
206
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Pixelart
|
2
|
+
|
3
|
+
|
4
|
+
class ImagePalette8bit < Image # or use Palette256 alias?
|
5
|
+
def initialize( colors, size: 1, spacing: nil )
|
6
|
+
## todo/check: change size arg to pixel or such? better name/less confusing - why? why not?
|
7
|
+
|
8
|
+
## todo/check: assert colors MUST have 256 colors!!!!
|
9
|
+
|
10
|
+
## use a "smart" default if no spacing set
|
11
|
+
## 0 for for (pixel) size == 1
|
12
|
+
## 1 for the rest
|
13
|
+
spacing = size == 1 ? 0 : 1 if spacing.nil?
|
14
|
+
|
15
|
+
img = ChunkyPNG::Image.new( 32*size+(32-1)*spacing,
|
16
|
+
8*size+(8-1)*spacing )
|
17
|
+
|
18
|
+
colors.each_with_index do |color,i|
|
19
|
+
y,x = i.divmod( 32 )
|
20
|
+
if size > 1
|
21
|
+
size.times do |n|
|
22
|
+
size.times do |m|
|
23
|
+
img[ x*size+n+spacing*x,
|
24
|
+
y*size+m+spacing*y] = color
|
25
|
+
end
|
26
|
+
end
|
27
|
+
else
|
28
|
+
img[x,y] = color
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
super( img.width, img.height, img )
|
33
|
+
end
|
34
|
+
end # class ImagePalette8bit
|
35
|
+
end # module Pixelart
|
36
|
+
|
37
|
+
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Pixelart
|
2
|
+
|
3
|
+
|
4
|
+
class Palette8bit # or use Palette256 alias?
|
5
|
+
|
6
|
+
|
7
|
+
## auto-add grayscale 0 to 255
|
8
|
+
## e.g. rgb(0,0,0)
|
9
|
+
## rgb(1,1,1)
|
10
|
+
## rgb(2,2,2)
|
11
|
+
## ...
|
12
|
+
## rgb(255,255,255)
|
13
|
+
GRAYSCALE = (0..255).map { |n| Color.rgb( n, n, n ) }
|
14
|
+
|
15
|
+
|
16
|
+
## 8x32 gradient color stops
|
17
|
+
## see https://en.wikipedia.org/wiki/List_of_software_palettes#Color_gradient_palettes
|
18
|
+
|
19
|
+
SEPIA_STOPS = [
|
20
|
+
['080400', '262117'],
|
21
|
+
['272218', '453E2F'],
|
22
|
+
['463F30', '645C48'],
|
23
|
+
['655D48', '837A60'],
|
24
|
+
|
25
|
+
['847A60', 'A29778'],
|
26
|
+
['A39878', 'C1B590'],
|
27
|
+
['C2B691', 'E0D2A8'],
|
28
|
+
['E1D3A9', 'FEEFBF'],
|
29
|
+
]
|
30
|
+
|
31
|
+
BLUE_STOPS = [
|
32
|
+
['000000', '001F3E'],
|
33
|
+
['002040', '003F7E'],
|
34
|
+
['004080', '005FBD'],
|
35
|
+
['0060BF', '007FFD'],
|
36
|
+
|
37
|
+
['0080FF', '009FFF'],
|
38
|
+
['00A0FF', '00BFFF'],
|
39
|
+
['00C0FF', '00DFFF'],
|
40
|
+
['00E0FF', '00FEFF'],
|
41
|
+
]
|
42
|
+
|
43
|
+
FALSE_STOPS = [
|
44
|
+
['FF00FF', '6400FF'],
|
45
|
+
['5F00FF', '003CFF'],
|
46
|
+
['0041FF', '00DCFF'],
|
47
|
+
['00E1FF', '00FF82'],
|
48
|
+
|
49
|
+
['00FF7D', '1EFF00'],
|
50
|
+
['23FF00', 'BEFF00'],
|
51
|
+
['C3FF00', 'FFA000'],
|
52
|
+
['FF9B00', 'FF0000'],
|
53
|
+
]
|
54
|
+
|
55
|
+
|
56
|
+
def self.build_palette( gradients )
|
57
|
+
colors_per_gradient, mod = 256.divmod( gradients.size )
|
58
|
+
raise ArgumentError, "8bit palette - 256 must be divisible by # of gradients (#{gradients.size}; expected mod of 0 but got #{mod}" if mod != 0
|
59
|
+
|
60
|
+
colors = []
|
61
|
+
gradients.each do |stops|
|
62
|
+
colors += Gradient.new( *stops ).colors( colors_per_gradient )
|
63
|
+
end
|
64
|
+
colors
|
65
|
+
end
|
66
|
+
|
67
|
+
SEPIA = build_palette( SEPIA_STOPS )
|
68
|
+
BLUE = build_palette( BLUE_STOPS )
|
69
|
+
FALSE = build_palette( FALSE_STOPS )
|
70
|
+
end # class Palette8bit
|
71
|
+
end # module Pixelart
|
72
|
+
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Pixelart
|
2
|
+
|
3
|
+
|
4
|
+
class Pixelator # or use Minifier or such - rename - why? why not?
|
5
|
+
|
6
|
+
def initialize( img, width=24, height=24 )
|
7
|
+
@img = img.is_a?( Image ) ? img.image : img ## "unwrap" if Pixelart::Image
|
8
|
+
@width = width
|
9
|
+
@height = height
|
10
|
+
|
11
|
+
## calculate pixel size / density / resolution
|
12
|
+
## how many pixels per pixel?
|
13
|
+
@xsize, @xoverflow = img.width.divmod( width )
|
14
|
+
@ysize, @yoverflow = img.height.divmod( height )
|
15
|
+
|
16
|
+
puts "minify image size from (#{@img.width}x#{@img.height}) to (#{width}x#{height})"
|
17
|
+
puts " pixel size (#{@xsize}x#{@ysize}) - #{@xsize*@ysize} pixel(s) per pixel"
|
18
|
+
puts " overflow x: #{@xoverflow}, y: #{@yoverflow} pixel(s)" if @xoverflow > 0 || @yoverflow > 0
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def grid( spacing: 10 )
|
23
|
+
width = @img.width + (@width-1)*spacing
|
24
|
+
height = @img.height + (@height-1)*spacing
|
25
|
+
|
26
|
+
img = ChunkyPNG::Image.new( width, height, ChunkyPNG::Color::WHITE )
|
27
|
+
|
28
|
+
@img.width.times do |x|
|
29
|
+
xpixel = x/@xsize
|
30
|
+
@img.height.times do |y|
|
31
|
+
ypixel = y/@ysize
|
32
|
+
|
33
|
+
## clip overflow pixels
|
34
|
+
xpixel = @width-1 if xpixel >= @width
|
35
|
+
ypixel = @height-1 if ypixel >= @height
|
36
|
+
|
37
|
+
color = @img[x,y]
|
38
|
+
img[x + spacing*xpixel,
|
39
|
+
y + spacing*ypixel] = color
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
Image.new( img.width, img.height, img ) ## wrap in Pixelart::Image - why? why not?
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
# pixels by coordinates (x/y) with color statistics / usage
|
48
|
+
def pixels
|
49
|
+
@pixels ||= begin
|
50
|
+
pixels = []
|
51
|
+
@img.width.times do |x|
|
52
|
+
xpixel = x/@xsize
|
53
|
+
@img.height.times do |y|
|
54
|
+
ypixel = y/@ysize
|
55
|
+
|
56
|
+
## skip/cut off overflow pixels
|
57
|
+
next if xpixel >= @width || ypixel >= @height
|
58
|
+
|
59
|
+
color = @img[x,y]
|
60
|
+
colors = pixels[xpixel+ypixel*@width] ||= Hash.new(0)
|
61
|
+
colors[ color ] += 1
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
## sort pixel colors by usage / count (highest first)
|
66
|
+
pixels = pixels.map do |pixel|
|
67
|
+
pixel.sort do |l,r|
|
68
|
+
r[1] <=> l[1]
|
69
|
+
end.to_h
|
70
|
+
end
|
71
|
+
pixels
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def pixel(x,y) pixels[x+y*@width]; end
|
76
|
+
alias_method :[], :pixel
|
77
|
+
|
78
|
+
|
79
|
+
def can_pixelate?
|
80
|
+
# check if any pixel has NOT a color with a 50% majority?
|
81
|
+
count = 0
|
82
|
+
@width.times do |x|
|
83
|
+
@height.times do |y|
|
84
|
+
pixel = pixel( x, y )
|
85
|
+
sum = pixel.values.sum
|
86
|
+
color_count = pixel.values[0]
|
87
|
+
if color_count < (sum/2)
|
88
|
+
count += 1
|
89
|
+
## todo/check: stor warn in a public errors or warns array - why? why not?
|
90
|
+
puts "!! WARN #{count} - pixel (#{x}/#{y}) - no majority (50%) color:"
|
91
|
+
pp pixel
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
count == 0 ## return true if not warnings found
|
97
|
+
end
|
98
|
+
alias_method :pixelate?, :can_pixelate?
|
99
|
+
|
100
|
+
|
101
|
+
def pixelate
|
102
|
+
img = ChunkyPNG::Image.new( @width, @height )
|
103
|
+
|
104
|
+
@width.times do |x|
|
105
|
+
@height.times do |y|
|
106
|
+
pixel = pixel( x, y )
|
107
|
+
color = pixel.keys[0]
|
108
|
+
img[x,y] = color
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
Image.new( img.width, img.height, img ) ## wrap in Pixelart::Image - why? why not?
|
113
|
+
end
|
114
|
+
end # class Pixelator
|
115
|
+
end # module Pixelart
|
116
|
+
|
117
|
+
|
data/lib/pixelart/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pixelart
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gerald Bauer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-04-
|
11
|
+
date: 2021-04-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: chunky_png
|
@@ -73,9 +73,15 @@ files:
|
|
73
73
|
- README.md
|
74
74
|
- Rakefile
|
75
75
|
- lib/pixelart.rb
|
76
|
+
- lib/pixelart/base.rb
|
77
|
+
- lib/pixelart/color.rb
|
78
|
+
- lib/pixelart/gradient.rb
|
76
79
|
- lib/pixelart/image.rb
|
80
|
+
- lib/pixelart/misc.rb
|
81
|
+
- lib/pixelart/palette.rb
|
82
|
+
- lib/pixelart/pixelator.rb
|
77
83
|
- lib/pixelart/version.rb
|
78
|
-
homepage: https://github.com/
|
84
|
+
homepage: https://github.com/rubycoco/pixel
|
79
85
|
licenses:
|
80
86
|
- Public Domain
|
81
87
|
metadata: {}
|