pixelart 0.1.1 → 0.1.6

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: 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: {}