pixelart 0.1.3 → 0.1.4

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: 5cc4a5d1c756256a1162bdab5208ff01549b7e2543ccb301457845dd1f393b48
4
- data.tar.gz: 1f07a5a8203fc4b4fc93446bc52d6abf8ca1b94368c48952649c423ef0100b15
3
+ metadata.gz: 627a2802ea7e3ad7a10490ea4f721d57a4007669fe59a0b6cc17fb798c60b7a7
4
+ data.tar.gz: 8edd801f96debe5b664f31180510ea103469f1f62e5f4ee88959046718350b7f
5
5
  SHA512:
6
- metadata.gz: 6031f337310c6935841ae3a9c2528f4aa6179c8a5b6b9a4b4aa21d0f98b9a9dd5a0578792cce660be1f15c11d95e35785efa80547e1950d28cf8e3ff21a05243
7
- data.tar.gz: 4615099b098378c2eee14cb854c2ee67237a278172b3c789be1de72a59c9ba6779e8dc2a4e9a4384c93b2771d5c8f31a8cf1f51cff308d6bf0e27468a50d0cb3
6
+ metadata.gz: 8bdff35694d0212e5c48799be188b1cfeef29bd24e48f46fab6e387f5dcdcde3063105db5fd1cab21f8b03ce12c1420fb5abed0f59ae42801b57164ff6682e08
7
+ data.tar.gz: de54f3cc88c6dd7e198abdba49eb7549c1ae18c018a24695ccc676dee94360b9fed68f0e415ce92060993fd1323f4c5cfd40831b5411bf7db0fb008d67951f60
data/Manifest.txt CHANGED
@@ -3,7 +3,10 @@ Manifest.txt
3
3
  README.md
4
4
  Rakefile
5
5
  lib/pixelart.rb
6
+ lib/pixelart/base.rb
6
7
  lib/pixelart/color.rb
7
8
  lib/pixelart/gradient.rb
8
9
  lib/pixelart/image.rb
10
+ lib/pixelart/misc.rb
11
+ lib/pixelart/palette.rb
9
12
  lib/pixelart/version.rb
data/README.md CHANGED
@@ -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
 
@@ -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 )
@@ -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
 
@@ -233,6 +233,50 @@ Voila!
233
233
 
234
234
 
235
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
+ ```
276
+
277
+
278
+
279
+
236
280
  ## Install
237
281
 
238
282
  Just install the gem:
data/lib/pixelart.rb CHANGED
@@ -1,26 +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/color'
14
- require 'pixelart/gradient'
15
- require 'pixelart/image'
6
+ ###
7
+ # add convenience top-level shortcuts / aliases
8
+ # make Image, Color, Palette8bit, etc top-level
9
+ include Pixelart
16
10
 
17
-
18
-
19
-
20
-
21
- ### add some convenience shortcuts
22
- PixelArt = Pixelart
23
-
24
-
25
-
26
- puts Pixelart.banner # say hello
@@ -0,0 +1,35 @@
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
+ class Image
29
+ Palette256 = Palette8Bit = Palette8bit
30
+ end
31
+ end
32
+
33
+
34
+
35
+ puts Pixelart.banner # say hello
@@ -7,6 +7,7 @@ class Color
7
7
  WHITE = 0xffffffff # rgba(255,255,255,255)
8
8
 
9
9
 
10
+
10
11
  def self.parse( color )
11
12
  if color.is_a?( Integer ) ## e.g. assumes ChunkyPNG::Color.rgb() or such
12
13
  color ## pass through as is 1:1
@@ -56,6 +57,75 @@ class Color
56
57
  def self.b( color ) ChunkyPNG::Color.b( color ); end
57
58
 
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
+
59
129
  end # class Color
60
130
  end # module Pixelart
61
131
 
@@ -1,8 +1,9 @@
1
1
 
2
- ## inspired by
2
+ ## inspired / helped by
3
3
  ## https://en.wikipedia.org/wiki/List_of_software_palettes#Color_gradient_palettes
4
4
  ## https://github.com/mistic100/tinygradient
5
5
  ## https://mistic100.github.io/tinygradient/
6
+ ## https://bsouthga.dev/posts/color-gradients-with-python
6
7
 
7
8
 
8
9
  module Pixelart
@@ -21,23 +22,36 @@ class Gradient
21
22
 
22
23
 
23
24
  def colors( steps )
24
- ## note: for now only support two colors
25
- ## note: gradient will include start (first)
26
- ## AND stop color (last) - stop color is NOT excluded for now!!
27
- start = @stops[0]
28
- stop = @stops[1]
29
-
30
- step = stepize( start, stop, steps-1 )
31
- ## pp step
32
-
33
- gradient = [start]
34
-
35
- 1.upto(steps-2).each do |i|
36
- color = interpolate( step, start, i )
37
- gradient << color
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
38
54
  end
39
- ## note: use original passed in stop color (should match calculated)
40
- gradient << stop
41
55
 
42
56
  ## convert to color (Integer)
43
57
  gradient.map do |color|
@@ -45,52 +59,48 @@ class Gradient
45
59
  end
46
60
  end
47
61
 
48
- ##
49
- # Linearly compute the step size between start and end
50
- ## (not normalized)
51
- # @param {StepValue} start
52
- # @param {StepValue} end
53
- # @param {number} steps - number of desired steps
54
- #@return {StepValue}
55
62
 
56
- def stepize(start, stop, steps)
57
- step = []
58
63
 
59
- [0,1,2].each do |k|
60
- step << Float(stop[k] - start[k]) / Float(steps)
61
- end
62
-
63
- step
64
- end
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
65
74
 
66
- ##
67
- # Compute the final step color
68
- # @param {StepValue} step - from `stepize`
69
- # @param {StepValue} start
70
- # @param {number} i - color index
71
- # @return {StepValue}
75
+ color << value
76
+ end
77
+ color
78
+ end
72
79
 
73
- RGB_MAX = [256, 256, 256]
74
- def interpolate( step, start, i )
75
- color = []
76
80
 
77
- [0,1,2].each do |k|
78
- color[k] = step[k]*i + start[k]
81
+ def linear_gradient( start, stop, steps,
82
+ include_stop: true )
79
83
 
80
- color[k] = if color[k] < 0.0
81
- color[k] + RGB_MAX[k]
82
- else
83
- color[k] % RGB_MAX[k]
84
- end
84
+ gradient = [start] ## auto-add start color (first color in gradient)
85
85
 
86
- ## convert back to Integer from Float
87
- ## add 0.5 for rounding up (starting with 0.5) - why? why not?
88
- color[k] = (color[k]+0.5).to_i
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
89
97
  end
90
- color
98
+
99
+ gradient
91
100
  end
92
101
 
93
102
 
103
+
94
104
  end # class Gradient
95
105
  end # module Pixelart
96
106
 
@@ -65,19 +65,71 @@ alias_method :scale, :zoom
65
65
 
66
66
 
67
67
 
68
+ #######################
69
+ ## filter / effects
68
70
 
69
- def parse_color_map( color_map )
70
- color_map.map do |k,v|
71
- [Color.parse(k), Color.parse(v)]
72
- end.to_h
71
+ def grayscale
72
+ img = @img.grayscale
73
+ Image.new( img.width, img.height, img )
73
74
  end
74
75
 
75
76
  ## add replace_colors alias too? - why? why not?
76
77
  def change_colors( color_map )
78
+ color_map = _parse_color_map( color_map )
79
+
77
80
  img = @img.dup ## note: make a deep copy!!!
78
- color_map = parse_color_map( color_map )
79
- ## pp color_map
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
80
131
 
132
+ def _change_colors!( img, color_map )
81
133
  img.width.times do |x|
82
134
  img.height.times do |y|
83
135
  color = img[x,y]
@@ -85,12 +137,7 @@ def change_colors( color_map )
85
137
  img[x,y] = new_color if new_color
86
138
  end
87
139
  end
88
-
89
- ## wrap into Pixelart::Image - lets you use zoom() and such
90
- Image.new( img.width, img.height, img )
91
140
  end
92
- alias_method :recolor, :change_colors
93
-
94
141
 
95
142
 
96
143
 
@@ -124,6 +171,7 @@ def []=( x, y, value ) @img[x,y]=value; end
124
171
  def pixels() @img.pixels; end
125
172
 
126
173
  ## return image ref - use a different name - why? why not?
174
+ ## change to to_image - why? why not?
127
175
  def image() @img; end
128
176
 
129
177
 
@@ -0,0 +1,40 @@
1
+ module Pixelart
2
+
3
+
4
+ class Image
5
+ class Palette8bit < Image # or use Palette256 alias?
6
+ def initialize( colors, size: 1, spacing: nil )
7
+ ## todo/check: change size arg to pixel or such? better name/less confusing - why? why not?
8
+
9
+ ## todo/check: assert colors MUST have 256 colors!!!!
10
+
11
+ ## use a "smart" default if no spacing set
12
+ ## 0 for for (pixel) size == 1
13
+ ## 1 for the rest
14
+ spacing = size == 1 ? 0 : 1 if spacing.nil?
15
+
16
+ img = ChunkyPNG::Image.new( 32*size+(32-1)*spacing,
17
+ 8*size+(8-1)*spacing )
18
+
19
+ colors.each_with_index do |color,i|
20
+ y,x = i.divmod( 32 )
21
+ if size > 1
22
+ size.times do |n|
23
+ size.times do |m|
24
+ img[ x*size+n+spacing*x,
25
+ y*size+m+spacing*y] = color
26
+ end
27
+ end
28
+ else
29
+ img[x,y] = color
30
+ end
31
+ end
32
+
33
+ super( img.width, img.height, img )
34
+ end
35
+ end # class Palette8bit
36
+
37
+ end # class Image
38
+ end # module Pixelart
39
+
40
+
@@ -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 = 3
6
+ PATCH = 4
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.3
4
+ version: 0.1.4
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-16 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,9 +73,12 @@ files:
73
73
  - README.md
74
74
  - Rakefile
75
75
  - lib/pixelart.rb
76
+ - lib/pixelart/base.rb
76
77
  - lib/pixelart/color.rb
77
78
  - lib/pixelart/gradient.rb
78
79
  - lib/pixelart/image.rb
80
+ - lib/pixelart/misc.rb
81
+ - lib/pixelart/palette.rb
79
82
  - lib/pixelart/version.rb
80
83
  homepage: https://github.com/cryptocopycats/mooncats
81
84
  licenses: