pixelart 1.2.3 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,154 +1,154 @@
1
- module Pixelart
2
-
3
- class ImageComposite < Image # check: (re)name to Collage, Sheet, Sprites, or such?
4
-
5
- ## default tile width / height in pixel -- check: (re)name to sprite or such? why? why not?
6
- TILE_WIDTH = 24
7
- TILE_HEIGHT = 24
8
-
9
-
10
- def self.read( path, width: TILE_WIDTH, height: TILE_WIDTH ) ## convenience helper
11
- img = ChunkyPNG::Image.from_file( path )
12
- new( img, width: width,
13
- height: width )
14
- end
15
-
16
-
17
- def initialize( *args, **kwargs )
18
- @tile_width = kwargs[:width] || kwargs[:tile_width] || TILE_WIDTH
19
- @tile_height = kwargs[:height] || kwargs[:tile_height] || TILE_HEIGHT
20
-
21
- ## check for background
22
- background = kwargs[:background] || kwargs[:tile_background]
23
-
24
- if background
25
- ## wrap into an array if not already an array
26
- ## and convert all colors to true rgba colors as integer numbers
27
- background = [background] unless background.is_a?( Array )
28
- @background_colors = background.map { |color| Color.parse( color ) }
29
- else
30
- ## todo/check: use empty array instead of nil - why? why not?
31
- @background_colors = nil
32
- end
33
-
34
-
35
- ## todo/fix: check type - args[0] is Image!!!
36
- if args.size == 1 ## assume "copy" c'tor with passed in image
37
- img = args[0] ## pass image through as-is
38
-
39
- @tile_cols = img.width / @tile_width ## e.g. 2400/24 = 100
40
- @tile_rows = img.height / @tile_height ## e.g. 2400/24 = 100
41
- @tile_count = @tile_cols * @tile_rows ## ## 10000 = 100x100 (2400x2400 pixel)
42
- elsif args.size == 2 || args.size == 0 ## cols, rows
43
- ## todo/fix: check type - args[0] & args[1] is Integer!!!!!
44
- ## todo/check - find a better name for cols/rows - why? why not?
45
- @tile_cols = args[0] || 3
46
- @tile_rows = args[1] || 3
47
- @tile_count = 0 # (track) current index (of added images)
48
-
49
- background_color = if @background_colors && @background_colors.size == 1
50
- @background_colors[0]
51
- else
52
- 0 # note: 0 is transparent (0) true color
53
- end
54
-
55
- ## todo/check - always auto-fill complete image (even if empty/no tiles)
56
- ## with background color if only one background color
57
- ## why? why not??? or always follow the "model"
58
- ## with more than one background color???
59
- img = ChunkyPNG::Image.new( @tile_cols * @tile_width,
60
- @tile_rows * @tile_height,
61
- background_color )
62
-
63
- else
64
- raise ArgumentError, "cols, rows or image arguments expected; got: #{args.inspect}"
65
- end
66
-
67
-
68
- puts " #{img.height}x#{img.width} (height x width)"
69
-
70
- super( nil, nil, img )
71
- end
72
-
73
-
74
- def count() @tile_count; end
75
- alias_method :size, :count ## add size alias (confusing if starting with 0?) - why? why not?
76
- alias_method :tile_count, :count
77
-
78
- def tile_width() @tile_width; end
79
- def tile_height() @tile_height; end
80
-
81
-
82
-
83
- #####
84
- # set / add tile
85
- def _add( image )
86
- y, x = @tile_count.divmod( @tile_cols )
87
-
88
- puts " [#{@tile_count}] @ (#{x}/#{y}) #{image.width}x#{image.height} (height x width)"
89
-
90
- ## note: only used if more than one background color specified
91
- ## needs to cycle through
92
- if @background_colors && @background_colors.size > 1
93
- i = x + y*@tile_cols
94
-
95
- ## note: cycle through background color for now
96
- background_color = @background_colors[i % @background_colors.size]
97
- background = Image.new( @tile_width, @tile_height, background_color ) ## todo/chekc: use "raw" ChunkyPNG:Image here - why? why not?
98
- background.compose!( image )
99
- image = background ## switch - make image with background new image
100
- end
101
-
102
- ## note: image.image - "unwrap" the "raw" ChunkyPNG::Image
103
- @img.compose!( image.image, x*@tile_width, y*@tile_height )
104
- @tile_count += 1
105
- end
106
-
107
- def add( image_or_images ) ## note: allow adding of image OR array of images
108
- if image_or_images.is_a?( Array )
109
- images = image_or_images
110
- images.each { |image| _add( image ) }
111
- else
112
- image = image_or_images
113
- _add( image )
114
- end
115
- end
116
- alias_method :<<, :add
117
-
118
-
119
-
120
- ######
121
- # get tile
122
-
123
- def tile( index )
124
- y, x = index.divmod( @tile_cols )
125
- img = @img.crop( x*@tile_width, y*@tile_height, @tile_width, @tile_height )
126
- Image.new( img.width, img.height, img ) ## wrap in pixelart image
127
- end
128
-
129
- def []( *args ) ## overload - why? why not?
130
- if args.size == 1
131
- index = args[0]
132
- tile( index )
133
- else
134
- super ## e.g [x,y] --- get pixel
135
- end
136
- end
137
-
138
-
139
- ## convenience helpers to loop over composite
140
- def each( &block )
141
- count.times do |i|
142
- block.call( tile( i ) )
143
- end
144
- end
145
-
146
- def each_with_index( &block )
147
- count.times do |i|
148
- block.call( tile( i ), i )
149
- end
150
- end
151
-
152
-
153
- end # class ImageComposite
154
- end # module Pixelart
1
+ module Pixelart
2
+
3
+ class ImageComposite < Image # check: (re)name to Collage, Sheet, Sprites, or such?
4
+
5
+ ## default tile width / height in pixel -- check: (re)name to sprite or such? why? why not?
6
+ TILE_WIDTH = 24
7
+ TILE_HEIGHT = 24
8
+
9
+
10
+ def self.read( path, width: TILE_WIDTH, height: TILE_WIDTH ) ## convenience helper
11
+ img = ChunkyPNG::Image.from_file( path )
12
+ new( img, width: width,
13
+ height: width )
14
+ end
15
+
16
+
17
+ def initialize( *args, **kwargs )
18
+ @tile_width = kwargs[:width] || kwargs[:tile_width] || TILE_WIDTH
19
+ @tile_height = kwargs[:height] || kwargs[:tile_height] || TILE_HEIGHT
20
+
21
+ ## check for background
22
+ background = kwargs[:background] || kwargs[:tile_background]
23
+
24
+ if background
25
+ ## wrap into an array if not already an array
26
+ ## and convert all colors to true rgba colors as integer numbers
27
+ background = [background] unless background.is_a?( Array )
28
+ @background_colors = background.map { |color| Color.parse( color ) }
29
+ else
30
+ ## todo/check: use empty array instead of nil - why? why not?
31
+ @background_colors = nil
32
+ end
33
+
34
+
35
+ ## todo/fix: check type - args[0] is Image!!!
36
+ if args.size == 1 ## assume "copy" c'tor with passed in image
37
+ img = args[0] ## pass image through as-is
38
+
39
+ @tile_cols = img.width / @tile_width ## e.g. 2400/24 = 100
40
+ @tile_rows = img.height / @tile_height ## e.g. 2400/24 = 100
41
+ @tile_count = @tile_cols * @tile_rows ## ## 10000 = 100x100 (2400x2400 pixel)
42
+ elsif args.size == 2 || args.size == 0 ## cols, rows
43
+ ## todo/fix: check type - args[0] & args[1] is Integer!!!!!
44
+ ## todo/check - find a better name for cols/rows - why? why not?
45
+ @tile_cols = args[0] || 3
46
+ @tile_rows = args[1] || 3
47
+ @tile_count = 0 # (track) current index (of added images)
48
+
49
+ background_color = if @background_colors && @background_colors.size == 1
50
+ @background_colors[0]
51
+ else
52
+ 0 # note: 0 is transparent (0) true color
53
+ end
54
+
55
+ ## todo/check - always auto-fill complete image (even if empty/no tiles)
56
+ ## with background color if only one background color
57
+ ## why? why not??? or always follow the "model"
58
+ ## with more than one background color???
59
+ img = ChunkyPNG::Image.new( @tile_cols * @tile_width,
60
+ @tile_rows * @tile_height,
61
+ background_color )
62
+
63
+ else
64
+ raise ArgumentError, "cols, rows or image arguments expected; got: #{args.inspect}"
65
+ end
66
+
67
+
68
+ puts " #{img.height}x#{img.width} (height x width)"
69
+
70
+ super( nil, nil, img )
71
+ end
72
+
73
+
74
+ def count() @tile_count; end
75
+ alias_method :size, :count ## add size alias (confusing if starting with 0?) - why? why not?
76
+ alias_method :tile_count, :count
77
+
78
+ def tile_width() @tile_width; end
79
+ def tile_height() @tile_height; end
80
+
81
+
82
+
83
+ #####
84
+ # set / add tile
85
+ def _add( image )
86
+ y, x = @tile_count.divmod( @tile_cols )
87
+
88
+ puts " [#{@tile_count}] @ (#{x}/#{y}) #{image.width}x#{image.height} (height x width)"
89
+
90
+ ## note: only used if more than one background color specified
91
+ ## needs to cycle through
92
+ if @background_colors && @background_colors.size > 1
93
+ i = x + y*@tile_cols
94
+
95
+ ## note: cycle through background color for now
96
+ background_color = @background_colors[i % @background_colors.size]
97
+ background = Image.new( @tile_width, @tile_height, background_color ) ## todo/chekc: use "raw" ChunkyPNG:Image here - why? why not?
98
+ background.compose!( image )
99
+ image = background ## switch - make image with background new image
100
+ end
101
+
102
+ ## note: image.image - "unwrap" the "raw" ChunkyPNG::Image
103
+ @img.compose!( image.image, x*@tile_width, y*@tile_height )
104
+ @tile_count += 1
105
+ end
106
+
107
+ def add( image_or_images ) ## note: allow adding of image OR array of images
108
+ if image_or_images.is_a?( Array )
109
+ images = image_or_images
110
+ images.each { |image| _add( image ) }
111
+ else
112
+ image = image_or_images
113
+ _add( image )
114
+ end
115
+ end
116
+ alias_method :<<, :add
117
+
118
+
119
+
120
+ ######
121
+ # get tile
122
+
123
+ def tile( index )
124
+ y, x = index.divmod( @tile_cols )
125
+ img = @img.crop( x*@tile_width, y*@tile_height, @tile_width, @tile_height )
126
+ Image.new( img.width, img.height, img ) ## wrap in pixelart image
127
+ end
128
+
129
+ def []( *args ) ## overload - why? why not?
130
+ if args.size == 1
131
+ index = args[0]
132
+ tile( index )
133
+ else
134
+ super ## e.g [x,y] --- get pixel
135
+ end
136
+ end
137
+
138
+
139
+ ## convenience helpers to loop over composite
140
+ def each( &block )
141
+ count.times do |i|
142
+ block.call( tile( i ) )
143
+ end
144
+ end
145
+
146
+ def each_with_index( &block )
147
+ count.times do |i|
148
+ block.call( tile( i ), i )
149
+ end
150
+ end
151
+
152
+
153
+ end # class ImageComposite
154
+ end # module Pixelart
@@ -1,106 +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
-
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
+