pixelart 0.1.1 → 0.1.6

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: 68fa84133edc2cb7f9742514af4ef188e1e358b3ae408f6ffd282c4c815e1ae5
4
- data.tar.gz: 2048f7c4e4a29cf6aff876b273c25a58bb3c2ef1c9836756847c13e59f381d18
3
+ metadata.gz: df39f58ffbca991ae1fbbdc2f4a3b83545074f7872dd96bf9b23ac5be7259a0d
4
+ data.tar.gz: 865908e9a481ef8bd74c98572dbe6c17fd2c7dde2bd5f0f8613247716be4065a
5
5
  SHA512:
6
- metadata.gz: 188f2845bf2f64102829f1840eaf56ded7ba7f2f2f931e07aa951d3b29e2e82b7570cd1564a40e8afb54ecd8f040874a9e1e589a5284025836dd81adf73549d3
7
- data.tar.gz: c12fa48855d076acd7516fa02d97a5da56467ce8ce78e7dc41e305681be1e1251fdb82ce638d6921726d5d6455e2849a75806730f314b93efe2146a70eb89d7c
6
+ metadata.gz: 65305e097f184eb6e62e6d0b392f2a9e42c2a705abbb198752fdb905681bc35ac9fb263efd663bb77d5ab3cef440127e55eeee2eb33ba841fde35338dff50d63
7
+ data.tar.gz: 94df6b1fced1c835ae8132922350b9d50d2fe21c29c020cc2b4595246af2d42fc0e0346bcfd6aacbc1ae45e153d58cfb5592ee90ab9c9da01b4ce07313d2b23e
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
@@ -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/pixelarti/mooncat_white.png)
75
- ![](https://github.com/cryptocopycats/mooncats/raw/master/pixelarti/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/pixelarti/mooncat_black.png)
104
- ![](https://github.com/cryptocopycats/mooncats/raw/master/pixelarti/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/pixelarti/vader.png)
231
- ![](https://github.com/cryptocopycats/mooncats/raw/master/pixelarti/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,180 +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
6
+ ###
7
+ # add convenience top-level shortcuts / aliases
8
+ # make Image, Color, Palette8bit, etc top-level
9
+ include Pixelart
13
10
 
14
-
15
-
16
- module Pixelart
17
- class Image
18
-
19
-
20
-
21
- def self.read( path ) ## convenience helper
22
- img_inner = ChunkyPNG::Image.from_file( path )
23
- img = new( img_inner.width, img_inner.height, img_inner )
24
- img
25
- end
26
-
27
-
28
- def self.parse( pixels, colors: )
29
- colors = parse_colors( colors )
30
- pixels = parse_pixels( pixels )
31
-
32
- width = pixels.reduce(1) {|width,row| row.size > width ? row.size : width }
33
- height = pixels.size
34
-
35
- img = new( width, height )
36
-
37
- pixels.each_with_index do |row,y|
38
- row.each_with_index do |color,x|
39
- pixel = colors[color]
40
- img[x,y] = pixel
41
- end # each row
42
- end # each data
43
-
44
- img
45
- end
46
-
47
-
48
-
49
- def initialize( width, height, initial=ChunkyPNG::Color::TRANSPARENT )
50
-
51
- if initial.is_a?( ChunkyPNG::Image )
52
- @img = initial
53
- else
54
- ## todo/check - initial - use parse_color here too e.g. allow "#fff" too etc.
55
- @img = ChunkyPNG::Image.new( width, height, initial )
56
- end
57
- end
58
-
59
-
60
-
61
- def zoom( zoom=2 )
62
- ## create a new zoom factor x image (2x, 3x, etc.)
63
-
64
- img = Image.new( @img.width*zoom,
65
- @img.height*zoom )
66
-
67
- @img.height.times do |y|
68
- @img.width.times do |x|
69
- pixel = @img[x,y]
70
- zoom.times do |n|
71
- zoom.times do |m|
72
- img[n+zoom*x,m+zoom*y] = pixel
73
- end
74
- end
75
- end # each x
76
- end # each y
77
-
78
- img
79
- end
80
- alias_method :scale, :zoom
81
-
82
-
83
-
84
-
85
- #####
86
- # (image) delegates
87
- ## todo/check: add some more??
88
- def save( path, constraints = {} )
89
- # step 1: make sure outdir exits
90
- outdir = File.dirname( path )
91
- FileUtils.mkdir_p( outdir ) unless Dir.exist?( outdir )
92
-
93
- # step 2: save
94
- @img.save( path, constraints )
95
- end
96
- alias_method :write, :save
97
-
98
-
99
- def compose!( other, x=0, y=0 )
100
- @img.compose!( other.image, x, y ) ## note: "unwrap" inner image ref
101
- end
102
- alias_method :paste!, :compose!
103
-
104
-
105
- def width() @img.width; end
106
- def height() @img.height; end
107
-
108
- def []( x, y ) @img[x,y]; end
109
- def []=( x, y, value ) @img[x,y]=value; end
110
-
111
- ## return image ref - use a different name - why? why not?
112
- def image() @img; end
113
-
114
-
115
-
116
-
117
- ######
118
- # helpers
119
- def self.parse_pixels( pixels )
120
- data = []
121
- pixels.each_line do |line|
122
- line = line.strip
123
- if line.empty?
124
- puts "!! WARN: skipping empty line in pixel art source"
125
- next
126
- end
127
-
128
- ## note: allow multiple spaces or tabs to separate pixel codes
129
- ## 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
130
- ## or
131
- data << line.split( /[ \t]+/)
132
- end
133
- data
134
- end
135
-
136
-
137
- def self.parse_colors( colors )
138
- if colors.is_a?( Array ) ## convenience shortcut
139
- ## note: always auto-add color 0 as pre-defined transparent - why? why not?
140
- h = { '0' => ChunkyPNG::Color::TRANSPARENT }
141
- colors.each_with_index do |color, i|
142
- h[ (i+1).to_s ] = parse_color( color )
143
- end
144
- h
145
- else ## assume hash table with color map
146
- ## convert into ChunkyPNG::Color
147
- colors.map do |key,color|
148
- ## always convert key to string why? why not? use symbol?
149
- [ key.to_s, parse_color( color ) ]
150
- end.to_h
151
- end
152
- end
153
-
154
- def self.parse_color( color )
155
- if color.is_a?( Integer ) ## e.g. Assumess ChunkyPNG::Color.rgb() or such
156
- color ## pass through as is 1:1
157
- elsif color.is_a?(String)
158
- if color.downcase == 'transparent' ## special case for builtin colors
159
- ChunkyPNG::Color::TRANSPARENT
160
- else
161
- ## note: return an Integer !!! (not a Color class or such!!! )
162
- ChunkyPNG::Color.from_hex( color )
163
- end
164
- else
165
- raise ArgumentError, "unknown color format; cannot parse - expected rgb hex string e.g. d3d3d3"
166
- end
167
- end
168
-
169
-
170
- end # class Image
171
- end # module Pixelart
172
-
173
-
174
-
175
- ### add some convenience shortcuts
176
- PixelArt = Pixelart
177
-
178
-
179
-
180
- 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 = 1
6
+ PATCH = 6
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.1
4
+ version: 0.1.6
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-12 00:00:00.000000000 Z
11
+ date: 2021-04-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chunky_png
@@ -73,8 +73,14 @@ 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
- homepage: https://github.com/cryptocopycats/mooncats
83
+ homepage: https://github.com/rubycoco/pixel
78
84
  licenses:
79
85
  - Public Domain
80
86
  metadata: {}