pixelart 0.1.0 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61a062dde7cebbcee3dfcf1f39e4ff384961bd773736f54c4959efc8d7c0a768
4
- data.tar.gz: 0d533bfef5d9b4cc13c3323a64acba128a39ba0d97d5c87748033c7c2f2ceba6
3
+ metadata.gz: 3022f23a6bfd66ef0e1c6f266fe2fd2ad24c802844be57737c2f202988e45292
4
+ data.tar.gz: b8d11d3128aa897ef91f21dc727f3f9f713a94343142bbeb0cce228dfdba0e6b
5
5
  SHA512:
6
- metadata.gz: 3dccdefa4552f08db7b118f63e6d33d9c0925ccd05bb0b4b6b92e5f419400ad313837e0c38afa9d74a7e8dd9a39bd8919ad4069563ff35901f8f1d18e877c035
7
- data.tar.gz: fc33580f828311ffc4f73b329f2e22d93d387dc7d3b7146aa15d0045d048289860d4639469479fad057f6c3a1ca49124db474311540984a8f7ddef7ed00a172e
6
+ metadata.gz: '086d0b53c8ef6a949025fcd2cda50f505deae181e53bc6cc6241beec8470bbc9f6869a03f189cbf56c6706635e83e16a799dda4e383823dd032a1bb0029717c2'
7
+ data.tar.gz: 3f577a57cd276d6a9eefdcf5374e5ea69e4f0f8f75cd69957098ec8c051258f70755c0883f8287bcf6e3768e35934bca4863d7ac41f55df7d1ae2395b4e12eed
data/Manifest.txt CHANGED
@@ -3,4 +3,10 @@ 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
9
+ lib/pixelart/image.rb
10
+ lib/pixelart/misc.rb
11
+ lib/pixelart/palette.rb
6
12
  lib/pixelart/version.rb
data/README.md CHANGED
@@ -58,21 +58,22 @@ 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.new( pixels, colors: colors )
61
+ img = Image.parse( pixels, colors: colors )
62
62
  img.save( './i/mooncat_white.png' )
63
63
  ```
64
64
 
65
65
  And let's try a 3x zoom factor:
66
66
 
67
67
  ``` ruby
68
- img = Pixelart::Image.new( pixels, colors: colors, zoom: 3 )
69
- img.save( './i/mooncat_white-3x.png' )
68
+ img3x = img.zoom( 3 )
69
+ img3x.save( './i/mooncat_white-3x.png' )
70
70
  ```
71
71
 
72
72
  Voila!
73
73
 
74
- ![](i/mooncat_white.png)
75
- ![](i/mooncat_white-3x.png)
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)
76
+
76
77
 
77
78
 
78
79
  Let's change the colors to use the genesis black color scheme:
@@ -90,17 +91,17 @@ colors = [
90
91
  And let's start minting:
91
92
 
92
93
  ``` ruby
93
- img = Pixelart::Image.new( pixels, colors: colors )
94
+ img = Image.parse( pixels, colors: colors )
94
95
  img.save( './i/mooncat_black.png' )
95
96
 
96
- img = Pixelart::Image.new( pixels, colors: colors, zoom: 3 )
97
- img.save( './i/mooncat_black-3x.png' )
97
+ img3x = img.zoom( 3 )
98
+ img3x.save( './i/mooncat_black-3x.png' )
98
99
  ```
99
100
 
100
101
  Voila! Black is the new White!
101
102
 
102
- ![](i/mooncat_black.png)
103
- ![](i/mooncat_black-3x.png)
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)
104
105
 
105
106
 
106
107
 
@@ -213,21 +214,65 @@ colors = {
213
214
  And let's mint an imperial master image:
214
215
 
215
216
  ``` ruby
216
- img = Pixelart::Image.new( pixels, colors: colors )
217
+ img = Image.parse( pixels, colors: colors )
217
218
  img.save( './i/vader.png' )
218
219
  ```
219
220
 
220
221
  And let's try a 5x zoom factor:
221
222
 
222
223
  ``` ruby
223
- img = Pixelart::Image.new( pixels, colors: colors, zoom: 5 )
224
- img.save( './i/vader5x.png' )
224
+ img5x = img.zoom( 5 )
225
+ img5x.save( './i/vader5x.png' )
225
226
  ```
226
227
 
227
228
  Voila!
228
229
 
229
- ![](i/vader.png)
230
- ![](i/vader5x.png)
230
+ ![](https://github.com/cryptocopycats/mooncats/raw/master/pixelart/i/vader.png)
231
+ ![](https://github.com/cryptocopycats/mooncats/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
+ ```
231
276
 
232
277
 
233
278
 
data/lib/pixelart.rb CHANGED
@@ -1,119 +1,10 @@
1
- ## 3rd party
2
- require 'chunky_png'
3
1
 
4
- ## stdlib
5
- require 'pp'
2
+ ## our own code (without "top-level" shortcuts e.g. "modular version")
3
+ require 'pixelart/base' # aka "strict(er)" version
6
4
 
7
- ## our own code
8
- require 'pixelart/version' # note: let version always go first
9
5
 
6
+ ###
7
+ # add convenience top-level shortcuts / aliases
8
+ # make Image, Color, Palette8bit, etc top-level
9
+ include Pixelart
10
10
 
11
-
12
- module Pixelart
13
- class Image
14
-
15
- def initialize( pixels, colors:, zoom: 1 )
16
- @colors = parse_colors( colors )
17
- @pixels = parse_pixels( pixels )
18
-
19
- max_width = @pixels.reduce(1) {|max_width,row| row.size > max_width ? row.size : max_width }
20
- max_height = @pixels.size
21
-
22
- @img = ChunkyPNG::Image.new( max_width*zoom,
23
- max_height*zoom,
24
- ChunkyPNG::Color::TRANSPARENT )
25
-
26
- @pixels.each_with_index do |row,y|
27
- row.each_with_index do |color,x|
28
- pixel = @colors[color]
29
- zoom.times do |n|
30
- zoom.times do |m|
31
- @img[n+zoom*x,m+zoom*y] = pixel
32
- end
33
- end
34
- end # each row
35
- end # each data
36
- end
37
-
38
-
39
- def parse_pixels( pixels )
40
- data = []
41
- pixels.each_line do |line|
42
- line = line.strip
43
- if line.empty?
44
- puts "!! WARN: skipping empty line in pixel art source"
45
- next
46
- end
47
-
48
- ## note: allow multiple spaces or tabs to separate pixel codes
49
- ## e.g. o o o o o o o o o o o o dg lg w w lg w lg lg dg dg w w lg dg o o o o o o o o o o o
50
- ## or
51
- data << line.split( /[ \t]+/)
52
- end
53
- data
54
- end
55
-
56
-
57
- #####
58
- # (image) delegates
59
- ## todo/check: add some more??
60
- def save( path, constraints = {} )
61
- @img.save( path, constraints )
62
- end
63
-
64
- def width() @img.width; end
65
- def height() @img.height; end
66
-
67
- ## return image ref - use a different name - why? why not?
68
- def image() @img; end
69
-
70
- def colors() @colors; end ## todo/check - return color map (hash table) or just color values (array) - why? why not?
71
-
72
-
73
-
74
- ######
75
- # helpers
76
- def parse_colors( colors )
77
- if colors.is_a?( Array ) ## convenience shortcut
78
- ## note: always auto-add color 0 as pre-defined transparent - why? why not?
79
- h = { '0' => ChunkyPNG::Color::TRANSPARENT }
80
- colors.each_with_index do |color, i|
81
- h[ (i+1).to_s ] = parse_color( color )
82
- end
83
- h
84
- else ## assume hash table with color map
85
- ## convert into ChunkyPNG::Color
86
- colors.map do |key,color|
87
- ## always convert key to string why? why not? use symbol?
88
- [ key.to_s, parse_color( color ) ]
89
- end.to_h
90
- end
91
- end
92
-
93
- def parse_color( color )
94
- if color.is_a?( Integer ) ## e.g. Assumess ChunkyPNG::Color.rgb() or such
95
- color ## pass through as is 1:1
96
- elsif color.is_a?(String)
97
- if color.downcase == 'transparent' ## special case for builtin colors
98
- ChunkyPNG::Color::TRANSPARENT
99
- else
100
- ## note: return an Integer !!! (not a Color class or such!!! )
101
- ChunkyPNG::Color.from_hex( color )
102
- end
103
- else
104
- raise ArgumentError, "unknown color format; cannot parse - expected rgb hex string e.g. d3d3d3"
105
- end
106
- end
107
-
108
-
109
- end # class Image
110
- end # module Pixelart
111
-
112
-
113
-
114
- ### add some convenience shortcuts
115
- PixelArt = Pixelart
116
-
117
-
118
-
119
- puts Pixelart.banner # say hello
@@ -0,0 +1,34 @@
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/misc' ## misc helpers
19
+
20
+
21
+ ##########
22
+ # add some spelling convenience variants
23
+ PixelArt = Pixelart
24
+
25
+ module Pixelart
26
+ Palette256 = Palette8Bit = Palette8bit
27
+
28
+ Palette256Image = Palette8BitImage = Palette8bitImage =
29
+ ImagePalette256 = ImagePalette8Bit = ImagePalette8bit
30
+ end
31
+
32
+
33
+
34
+ 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
+
@@ -0,0 +1,221 @@
1
+ module Pixelart
2
+
3
+ class Image
4
+
5
+ def self.read( path ) ## convenience helper
6
+ img_inner = ChunkyPNG::Image.from_file( path )
7
+ img = new( img_inner.width, img_inner.height, img_inner )
8
+ img
9
+ end
10
+
11
+
12
+ def self.parse( pixels, colors: )
13
+ colors = parse_colors( colors )
14
+ pixels = parse_pixels( pixels )
15
+
16
+ width = pixels.reduce(1) {|width,row| row.size > width ? row.size : width }
17
+ height = pixels.size
18
+
19
+ img = new( width, height )
20
+
21
+ pixels.each_with_index do |row,y|
22
+ row.each_with_index do |color,x|
23
+ pixel = colors[color]
24
+ img[x,y] = pixel
25
+ end # each row
26
+ end # each data
27
+
28
+ img
29
+ end
30
+
31
+
32
+
33
+ def initialize( width, height, initial=Color::TRANSPARENT )
34
+
35
+ if initial.is_a?( ChunkyPNG::Image )
36
+ @img = initial
37
+ else
38
+ ## todo/check - initial - use parse_color here too e.g. allow "#fff" too etc.
39
+ @img = ChunkyPNG::Image.new( width, height, initial )
40
+ end
41
+ end
42
+
43
+
44
+
45
+ def zoom( zoom=2 )
46
+ ## create a new zoom factor x image (2x, 3x, etc.)
47
+
48
+ img = Image.new( @img.width*zoom,
49
+ @img.height*zoom )
50
+
51
+ @img.height.times do |y|
52
+ @img.width.times do |x|
53
+ pixel = @img[x,y]
54
+ zoom.times do |n|
55
+ zoom.times do |m|
56
+ img[n+zoom*x,m+zoom*y] = pixel
57
+ end
58
+ end
59
+ end # each x
60
+ end # each y
61
+
62
+ img
63
+ end
64
+ alias_method :scale, :zoom
65
+
66
+
67
+
68
+ #######################
69
+ ## filter / effects
70
+
71
+ def grayscale
72
+ img = @img.grayscale
73
+ Image.new( img.width, img.height, img )
74
+ end
75
+
76
+ ## add replace_colors alias too? - why? why not?
77
+ def change_colors( color_map )
78
+ color_map = _parse_color_map( color_map )
79
+
80
+ img = @img.dup ## note: make a deep copy!!!
81
+ _change_colors!( img, color_map )
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
+ img.width.times do |x|
134
+ img.height.times do |y|
135
+ color = img[x,y]
136
+ new_color = color_map[color]
137
+ img[x,y] = new_color if new_color
138
+ end
139
+ end
140
+ end
141
+
142
+
143
+
144
+
145
+ #####
146
+ # (image) delegates
147
+ ## todo/check: add some more??
148
+ def save( path, constraints = {} )
149
+ # step 1: make sure outdir exits
150
+ outdir = File.dirname( path )
151
+ FileUtils.mkdir_p( outdir ) unless Dir.exist?( outdir )
152
+
153
+ # step 2: save
154
+ @img.save( path, constraints )
155
+ end
156
+ alias_method :write, :save
157
+
158
+
159
+ def compose!( other, x=0, y=0 )
160
+ @img.compose!( other.image, x, y ) ## note: "unwrap" inner image ref
161
+ end
162
+ alias_method :paste!, :compose!
163
+
164
+
165
+ def width() @img.width; end
166
+ def height() @img.height; end
167
+
168
+ def []( x, y ) @img[x,y]; end
169
+ def []=( x, y, value ) @img[x,y]=value; end
170
+
171
+ def pixels() @img.pixels; end
172
+
173
+ ## return image ref - use a different name - why? why not?
174
+ ## change to to_image - why? why not?
175
+ def image() @img; end
176
+
177
+
178
+
179
+
180
+ ######
181
+ # helpers
182
+ def self.parse_pixels( pixels )
183
+ data = []
184
+ pixels.each_line do |line|
185
+ line = line.strip
186
+ if line.empty?
187
+ puts "!! WARN: skipping empty line in pixel art source"
188
+ next
189
+ end
190
+
191
+ ## note: allow multiple spaces or tabs to separate pixel codes
192
+ ## e.g. o o o o o o o o o o o o dg lg w w lg w lg lg dg dg w w lg dg o o o o o o o o o o o
193
+ ## or
194
+ data << line.split( /[ \t]+/)
195
+ end
196
+ data
197
+ end
198
+
199
+
200
+ def self.parse_colors( colors )
201
+ if colors.is_a?( Array ) ## convenience shortcut
202
+ ## note: always auto-add color 0 as pre-defined transparent - why? why not?
203
+ h = { '0' => Color::TRANSPARENT }
204
+ colors.each_with_index do |color, i|
205
+ h[ (i+1).to_s ] = Color.parse( color )
206
+ end
207
+ h
208
+ else ## assume hash table with color map
209
+ ## convert into ChunkyPNG::Color
210
+ colors.map do |key,color|
211
+ ## always convert key to string why? why not? use symbol?
212
+ [ key.to_s, Color.parse( color ) ]
213
+ end.to_h
214
+ end
215
+ end
216
+
217
+
218
+
219
+ end # class Image
220
+ end # module Pixelart
221
+
@@ -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
+
@@ -3,7 +3,7 @@ module Pixelart
3
3
 
4
4
  MAJOR = 0
5
5
  MINOR = 1
6
- PATCH = 0
6
+ PATCH = 5
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.0
4
+ version: 0.1.5
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-08 00:00:00.000000000 Z
11
+ date: 2021-04-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chunky_png
@@ -73,6 +73,12 @@ 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
79
+ - lib/pixelart/image.rb
80
+ - lib/pixelart/misc.rb
81
+ - lib/pixelart/palette.rb
76
82
  - lib/pixelart/version.rb
77
83
  homepage: https://github.com/cryptocopycats/mooncats
78
84
  licenses: