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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 71ac76ef187b8148a547f8dd7b76f2216bdd034f2ba67f7974b6e1a98abe3992
4
- data.tar.gz: 6b44348938795087874a80fec20a6e1f87633ff282f62804605b42cbb232e46d
3
+ metadata.gz: 0dd959d7ee982c2634f325c6e7f91bfb46d12dc61a79f364d0b7caae529d59f1
4
+ data.tar.gz: 024f66e021b395b4141c54298e2fb0f31b47d28952a5c03d56ecfaf75c68b1be
5
5
  SHA512:
6
- metadata.gz: d94abebcda0b1d87bdc0bcdc0f6072d7ae88efed8b26a0733f4fcdccb9b8063068b9a55f24bf08ebe34705e30298ebaf5d1f619045e37fe5be3b2412a8019932
7
- data.tar.gz: 63be655c80cf3703499aae7984a901888ca2ae3fe362ea66f5324e7abd98a0b207f5a150da9f2f1c6b7b2c29d52b71e44d252b6323eb8050160bac8c52e542f0
6
+ metadata.gz: 71093d3b9977140303838bf9628c1de54c47f978a556945321fd849d3ebb26912d50846050c15efe3f1a2cf4d8b0991940c08f4a63511451148f63d2b9e788f5
7
+ data.tar.gz: 41c97279da0b7f81430a56c1c8e88be1ebc2bac61ea6b7a136be1accfe3cd71b7c5e0c3885d3a7dfa5d021852d4550e3f692b151767a18f6fcf7a8c9fd5340d0
data/Manifest.txt CHANGED
@@ -3,5 +3,11 @@ Manifest.txt
3
3
  README.md
4
4
  Rakefile
5
5
  lib/pixelart.rb
6
+ lib/pixelart/base.rb
7
+ lib/pixelart/color.rb
8
+ lib/pixelart/gradient.rb
6
9
  lib/pixelart/image.rb
10
+ lib/pixelart/misc.rb
11
+ lib/pixelart/palette.rb
12
+ lib/pixelart/pixelator.rb
7
13
  lib/pixelart/version.rb
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/cryptocopycats/mooncats](https://github.com/cryptocopycats/mooncats)
7
- * bugs :: [github.com/cryptocopycats/mooncats/issues](https://github.com/cryptocopycats/mooncats/issues)
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 = Pixelart::Image.parse( pixels, colors: colors )
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
- ![](https://github.com/cryptocopycats/mooncats/raw/master/pixelart/i/mooncat_white.png)
75
- ![](https://github.com/cryptocopycats/mooncats/raw/master/pixelart/i/mooncat_white-3x.png)
74
+ ![](https://github.com/rubycoco/pixel/raw/master/pixelart/i/mooncat_white.png)
75
+ ![](https://github.com/rubycoco/pixel/raw/master/pixelart/i/mooncat_white-3x.png)
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 = Pixelart::Image.parse( pixels, colors: colors )
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
- ![](https://github.com/cryptocopycats/mooncats/raw/master/pixelart/i/mooncat_black.png)
104
- ![](https://github.com/cryptocopycats/mooncats/raw/master/pixelart/i/mooncat_black-3x.png)
103
+ ![](https://github.com/rubycoco/pixel/raw/master/pixelart/i/mooncat_black.png)
104
+ ![](https://github.com/rubycoco/pixel/raw/master/pixelart/i/mooncat_black-3x.png)
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 = Pixelart::Image.parse( pixels, colors: colors )
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
- ![](https://github.com/cryptocopycats/mooncats/raw/master/pixelart/i/vader.png)
231
- ![](https://github.com/cryptocopycats/mooncats/raw/master/pixelart/i/vader5x.png)
230
+ ![](https://github.com/rubycoco/pixel/raw/master/pixelart/i/vader.png)
231
+ ![](https://github.com/rubycoco/pixel/raw/master/pixelart/i/vader5x.png)
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/cryptocopycats/mooncats' }
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
- ## stdlib
5
- require 'pp'
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
- ## our own code
12
- require 'pixelart/version' # note: let version always go first
13
- require 'pixelart/image'
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
+
@@ -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=ChunkyPNG::Color::TRANSPARENT )
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 parse_color_map( color_map )
122
- color_map.map do |k,v|
123
- [Color.parse(k), Color.parse(v)]
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
- color_map = parse_color_map( color_map )
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' => ChunkyPNG::Color::TRANSPARENT }
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
+
@@ -3,7 +3,7 @@ module Pixelart
3
3
 
4
4
  MAJOR = 0
5
5
  MINOR = 1
6
- PATCH = 2
6
+ PATCH = 7
7
7
  VERSION = [MAJOR,MINOR,PATCH].join('.')
8
8
 
9
9
  def self.version
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.2
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-13 00:00:00.000000000 Z
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/cryptocopycats/mooncats
84
+ homepage: https://github.com/rubycoco/pixel
79
85
  licenses:
80
86
  - Public Domain
81
87
  metadata: {}