pixelart 1.2.3 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/pixelart/misc.rb CHANGED
@@ -1,66 +1,66 @@
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
-
19
- colors =colors.map {|color| Color.parse( color ) }
20
-
21
- colors.each_with_index do |color,i|
22
- y,x = i.divmod( 32 )
23
- if size > 1
24
- size.times do |n|
25
- size.times do |m|
26
- img[ x*size+n+spacing*x,
27
- y*size+m+spacing*y] = color
28
- end
29
- end
30
- else
31
- img[x,y] = color
32
- end
33
- end
34
-
35
- super( img.width, img.height, img )
36
- end
37
- end # class ImagePalette8bit
38
-
39
-
40
-
41
- class ImageColorBar < Image
42
- ## make a color bar
43
- ## keep auto-zoom 24x or such - why? why not?
44
- def initialize( colors, size: 24 )
45
- img = ChunkyPNG::Image.new( colors.size*size,
46
- size,
47
- ChunkyPNG::Color::WHITE ) # why? why not?
48
-
49
- colors = colors.map {|color| Color.parse( color ) }
50
-
51
- colors.each_with_index do |color,i|
52
- size.times do |x|
53
- size.times do |y|
54
- img[x+size*i,y] = color
55
- end
56
- end
57
- end
58
-
59
- super( img.width, img.height, img )
60
- end
61
- end # class ImageColorBar
62
-
63
-
64
- end # module Pixelart
65
-
66
-
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
+
19
+ colors =colors.map {|color| Color.parse( color ) }
20
+
21
+ colors.each_with_index do |color,i|
22
+ y,x = i.divmod( 32 )
23
+ if size > 1
24
+ size.times do |n|
25
+ size.times do |m|
26
+ img[ x*size+n+spacing*x,
27
+ y*size+m+spacing*y] = color
28
+ end
29
+ end
30
+ else
31
+ img[x,y] = color
32
+ end
33
+ end
34
+
35
+ super( img.width, img.height, img )
36
+ end
37
+ end # class ImagePalette8bit
38
+
39
+
40
+
41
+ class ImageColorBar < Image
42
+ ## make a color bar
43
+ ## keep auto-zoom 24x or such - why? why not?
44
+ def initialize( colors, size: 24 )
45
+ img = ChunkyPNG::Image.new( colors.size*size,
46
+ size,
47
+ ChunkyPNG::Color::WHITE ) # why? why not?
48
+
49
+ colors = colors.map {|color| Color.parse( color ) }
50
+
51
+ colors.each_with_index do |color,i|
52
+ size.times do |x|
53
+ size.times do |y|
54
+ img[x+size*i,y] = color
55
+ end
56
+ end
57
+ end
58
+
59
+ super( img.width, img.height, img )
60
+ end
61
+ end # class ImageColorBar
62
+
63
+
64
+ end # module Pixelart
65
+
66
+
@@ -1,72 +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
-
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
+
@@ -1,165 +1,165 @@
1
- module Pixelart
2
-
3
-
4
- class Pixelator # or use Minifier or such - rename - why? why not?
5
-
6
- def initialize( img, width=24, height=24 )
7
- @img = img.is_a?( Image ) ? img.image : img ## "unwrap" if Pixelart::Image
8
- @width = width
9
- @height = height
10
-
11
- ## calculate pixel size / density / resolution
12
- ## how many pixels per pixel?
13
- @xsize, @xoverflow = img.width.divmod( width )
14
- @ysize, @yoverflow = img.height.divmod( height )
15
-
16
- puts "minify image size from (#{@img.width}x#{@img.height}) to (#{width}x#{height})"
17
- puts " pixel size (#{@xsize}x#{@ysize}) - #{@xsize*@ysize} pixel(s) per pixel"
18
- puts " overflow x: #{@xoverflow}, y: #{@yoverflow} pixel(s)" if @xoverflow > 0 || @yoverflow > 0
19
- end
20
-
21
-
22
- def grid( spacing: 10 )
23
- width = @img.width + (@width-1)*spacing
24
- height = @img.height + (@height-1)*spacing
25
-
26
- img = ChunkyPNG::Image.new( width, height, ChunkyPNG::Color::WHITE )
27
-
28
- @img.width.times do |x|
29
- xpixel = x/@xsize
30
- @img.height.times do |y|
31
- ypixel = y/@ysize
32
-
33
- ## clip overflow pixels
34
- xpixel = @width-1 if xpixel >= @width
35
- ypixel = @height-1 if ypixel >= @height
36
-
37
- color = @img[x,y]
38
- img[x + spacing*xpixel,
39
- y + spacing*ypixel] = color
40
- end
41
- end
42
-
43
- Image.new( img.width, img.height, img ) ## wrap in Pixelart::Image - why? why not?
44
- end
45
-
46
-
47
- # pixels by coordinates (x/y) with color statistics / usage
48
- def pixels
49
- @pixels ||= begin
50
- pixels = []
51
- @img.width.times do |x|
52
- xpixel = x/@xsize
53
- @img.height.times do |y|
54
- ypixel = y/@ysize
55
-
56
- ## skip/cut off overflow pixels
57
- next if xpixel >= @width || ypixel >= @height
58
-
59
- color = @img[x,y]
60
- colors = pixels[xpixel+ypixel*@width] ||= Hash.new(0)
61
- colors[ color ] += 1
62
- end
63
- end
64
-
65
- ## sort pixel colors by usage / count (highest first)
66
- pixels = pixels.map do |pixel|
67
- pixel.sort do |l,r|
68
- r[1] <=> l[1]
69
- end.to_h
70
- end
71
- pixels
72
- end
73
- end
74
-
75
- def pixel(x,y) pixels[x+y*@width]; end
76
- alias_method :[], :pixel
77
-
78
-
79
- def can_pixelate?( threshold: 50 )
80
- # check if any pixel has NOT a color with a 50% majority?
81
- count = 0
82
- @width.times do |x|
83
- @height.times do |y|
84
- pixel = pixel( x, y )
85
- sum = pixel.values.sum
86
- color_count = pixel.values[0]
87
-
88
- threshold_count = sum / (100/threshold)
89
- if color_count < threshold_count
90
- count += 1
91
- puts "!! #{color_count} < #{threshold_count} (#{threshold}%)"
92
- ## todo/check: stor warn in a public errors or warns array - why? why not?
93
- puts "!! WARN #{count} - pixel (#{x}/#{y}) - no majority (#{threshold}%) color:"
94
- pp pixel
95
- end
96
- end
97
- end
98
-
99
- count == 0 ## return true if not warnings found
100
- end
101
- alias_method :pixelate?, :can_pixelate?
102
-
103
-
104
- def pixelate
105
- img = ChunkyPNG::Image.new( @width, @height )
106
-
107
- @width.times do |x|
108
- @height.times do |y|
109
- pixel = pixel( x, y )
110
- color = pixel.keys[0]
111
- img[x,y] = color
112
- end
113
- end
114
-
115
- Image.new( img.width, img.height, img ) ## wrap in Pixelart::Image - why? why not?
116
- end
117
-
118
- def outline
119
- ## create a two color outline (transparent and non-transparent color)
120
- img = ChunkyPNG::Image.new( @width, @height )
121
-
122
- @width.times do |x|
123
- @height.times do |y|
124
- pixel = pixel( x, y )
125
- ## calculate pixel count for transparent and non-transparent parts
126
- ## note:
127
- ## also count all colors with alpha channel < 200 to transparent!!
128
- transparent_count, color_count = pixel.reduce([0,0]) do |mem, (color,count)|
129
- hsl = Color.to_hsl( color )
130
- ## get alpha channel (transparency) for hsla
131
- ## 0-255 max.
132
- alpha = hsl[3]
133
- if color == 0x00 || alpha < 200
134
- mem[0] += count
135
- else
136
- mem[1] += count
137
- end
138
- mem
139
- end
140
-
141
- print "."
142
- if transparent_count > 0 && color_count > 0
143
- print "(#{x}/#{y}=>#{transparent_count}/#{color_count})"
144
- end
145
-
146
- ## todo/check:
147
- ## warn if sum_transparent == sum_color
148
- ## or within "threshold" e.g. below 55% or 58% or such - why? why not?
149
- ## or add treshold as param to outline?
150
- color = if transparent_count > color_count
151
- 0x0
152
- else
153
- 0x0000ffff ## use blue for now
154
- end
155
-
156
- img[x,y] = color
157
- end
158
- end
159
- print "\n"
160
-
161
- Image.new( img.width, img.height, img ) ## wrap in Pixelart::Image - why? why not?
162
- end
163
- end # class Pixelator
164
- end # module Pixelart
165
-
1
+ module Pixelart
2
+
3
+
4
+ class Pixelator # or use Minifier or such - rename - why? why not?
5
+
6
+ def initialize( img, width=24, height=24 )
7
+ @img = img.is_a?( Image ) ? img.image : img ## "unwrap" if Pixelart::Image
8
+ @width = width
9
+ @height = height
10
+
11
+ ## calculate pixel size / density / resolution
12
+ ## how many pixels per pixel?
13
+ @xsize, @xoverflow = img.width.divmod( width )
14
+ @ysize, @yoverflow = img.height.divmod( height )
15
+
16
+ puts "minify image size from (#{@img.width}x#{@img.height}) to (#{width}x#{height})"
17
+ puts " pixel size (#{@xsize}x#{@ysize}) - #{@xsize*@ysize} pixel(s) per pixel"
18
+ puts " overflow x: #{@xoverflow}, y: #{@yoverflow} pixel(s)" if @xoverflow > 0 || @yoverflow > 0
19
+ end
20
+
21
+
22
+ def grid( spacing: 10 )
23
+ width = @img.width + (@width-1)*spacing
24
+ height = @img.height + (@height-1)*spacing
25
+
26
+ img = ChunkyPNG::Image.new( width, height, ChunkyPNG::Color::WHITE )
27
+
28
+ @img.width.times do |x|
29
+ xpixel = x/@xsize
30
+ @img.height.times do |y|
31
+ ypixel = y/@ysize
32
+
33
+ ## clip overflow pixels
34
+ xpixel = @width-1 if xpixel >= @width
35
+ ypixel = @height-1 if ypixel >= @height
36
+
37
+ color = @img[x,y]
38
+ img[x + spacing*xpixel,
39
+ y + spacing*ypixel] = color
40
+ end
41
+ end
42
+
43
+ Image.new( img.width, img.height, img ) ## wrap in Pixelart::Image - why? why not?
44
+ end
45
+
46
+
47
+ # pixels by coordinates (x/y) with color statistics / usage
48
+ def pixels
49
+ @pixels ||= begin
50
+ pixels = []
51
+ @img.width.times do |x|
52
+ xpixel = x/@xsize
53
+ @img.height.times do |y|
54
+ ypixel = y/@ysize
55
+
56
+ ## skip/cut off overflow pixels
57
+ next if xpixel >= @width || ypixel >= @height
58
+
59
+ color = @img[x,y]
60
+ colors = pixels[xpixel+ypixel*@width] ||= Hash.new(0)
61
+ colors[ color ] += 1
62
+ end
63
+ end
64
+
65
+ ## sort pixel colors by usage / count (highest first)
66
+ pixels = pixels.map do |pixel|
67
+ pixel.sort do |l,r|
68
+ r[1] <=> l[1]
69
+ end.to_h
70
+ end
71
+ pixels
72
+ end
73
+ end
74
+
75
+ def pixel(x,y) pixels[x+y*@width]; end
76
+ alias_method :[], :pixel
77
+
78
+
79
+ def can_pixelate?( threshold: 50 )
80
+ # check if any pixel has NOT a color with a 50% majority?
81
+ count = 0
82
+ @width.times do |x|
83
+ @height.times do |y|
84
+ pixel = pixel( x, y )
85
+ sum = pixel.values.sum
86
+ color_count = pixel.values[0]
87
+
88
+ threshold_count = sum / (100/threshold)
89
+ if color_count < threshold_count
90
+ count += 1
91
+ puts "!! #{color_count} < #{threshold_count} (#{threshold}%)"
92
+ ## todo/check: stor warn in a public errors or warns array - why? why not?
93
+ puts "!! WARN #{count} - pixel (#{x}/#{y}) - no majority (#{threshold}%) color:"
94
+ pp pixel
95
+ end
96
+ end
97
+ end
98
+
99
+ count == 0 ## return true if not warnings found
100
+ end
101
+ alias_method :pixelate?, :can_pixelate?
102
+
103
+
104
+ def pixelate
105
+ img = ChunkyPNG::Image.new( @width, @height )
106
+
107
+ @width.times do |x|
108
+ @height.times do |y|
109
+ pixel = pixel( x, y )
110
+ color = pixel.keys[0]
111
+ img[x,y] = color
112
+ end
113
+ end
114
+
115
+ Image.new( img.width, img.height, img ) ## wrap in Pixelart::Image - why? why not?
116
+ end
117
+
118
+ def outline
119
+ ## create a two color outline (transparent and non-transparent color)
120
+ img = ChunkyPNG::Image.new( @width, @height )
121
+
122
+ @width.times do |x|
123
+ @height.times do |y|
124
+ pixel = pixel( x, y )
125
+ ## calculate pixel count for transparent and non-transparent parts
126
+ ## note:
127
+ ## also count all colors with alpha channel < 200 to transparent!!
128
+ transparent_count, color_count = pixel.reduce([0,0]) do |mem, (color,count)|
129
+ hsl = Color.to_hsl( color )
130
+ ## get alpha channel (transparency) for hsla
131
+ ## 0-255 max.
132
+ alpha = hsl[3]
133
+ if color == 0x00 || alpha < 200
134
+ mem[0] += count
135
+ else
136
+ mem[1] += count
137
+ end
138
+ mem
139
+ end
140
+
141
+ print "."
142
+ if transparent_count > 0 && color_count > 0
143
+ print "(#{x}/#{y}=>#{transparent_count}/#{color_count})"
144
+ end
145
+
146
+ ## todo/check:
147
+ ## warn if sum_transparent == sum_color
148
+ ## or within "threshold" e.g. below 55% or 58% or such - why? why not?
149
+ ## or add treshold as param to outline?
150
+ color = if transparent_count > color_count
151
+ 0x0
152
+ else
153
+ 0x0000ffff ## use blue for now
154
+ end
155
+
156
+ img[x,y] = color
157
+ end
158
+ end
159
+ print "\n"
160
+
161
+ Image.new( img.width, img.height, img ) ## wrap in Pixelart::Image - why? why not?
162
+ end
163
+ end # class Pixelator
164
+ end # module Pixelart
165
+
@@ -1,35 +1,35 @@
1
- module Pixelart
2
-
3
-
4
- ## todo/check:
5
- ## use a different name for silhouette
6
- ## - why not - outline ???
7
- ## or - shadow ???
8
- ## or - profile ???
9
- ## or - figure ???
10
- ## or - shape ???
11
- ## or - form ???
12
-
13
- class Image
14
- def silhouette( color='#000000' )
15
- color = Color.parse( color )
16
-
17
- img = Image.new( @img.width, @img.height )
18
-
19
- @img.width.times do |x|
20
- @img.height.times do |y|
21
- pixel = @img[x,y]
22
-
23
- img[x,y] = if pixel == Color::TRANSPARENT # transparent (0)
24
- Color::TRANSPARENT
25
- else
26
- color
27
- end
28
- end
29
- end
30
- img
31
- end
32
-
33
- end # class Image
34
-
35
- end # module Pixelart
1
+ module Pixelart
2
+
3
+
4
+ ## todo/check:
5
+ ## use a different name for silhouette
6
+ ## - why not - outline ???
7
+ ## or - shadow ???
8
+ ## or - profile ???
9
+ ## or - figure ???
10
+ ## or - shape ???
11
+ ## or - form ???
12
+
13
+ class Image
14
+ def silhouette( color='#000000' )
15
+ color = Color.parse( color )
16
+
17
+ img = Image.new( @img.width, @img.height )
18
+
19
+ @img.width.times do |x|
20
+ @img.height.times do |y|
21
+ pixel = @img[x,y]
22
+
23
+ img[x,y] = if pixel == Color::TRANSPARENT # transparent (0)
24
+ Color::TRANSPARENT
25
+ else
26
+ color
27
+ end
28
+ end
29
+ end
30
+ img
31
+ end
32
+
33
+ end # class Image
34
+
35
+ end # module Pixelart