pixelart 0.1.3 → 0.1.4

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