pixelart 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,100 +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
- ## todo/fix: check type - args[0] is Image!!!
22
- if args.size == 1 ## assume "copy" c'tor with passed in image
23
- img = args[0] ## pass image through as-is
24
-
25
- @tile_cols = img.width / @tile_width ## e.g. 2400/24 = 100
26
- @tile_rows = img.height / @tile_height ## e.g. 2400/24 = 100
27
- @tile_count = @tile_cols * @tile_rows ## ## 10000 = 100x100 (2400x2400 pixel)
28
- elsif args.size == 2 || args.size == 0 ## cols, rows
29
- ## todo/fix: check type - args[0] & args[1] is Integer!!!!!
30
- ## todo/check - find a better name for cols/rows - why? why not?
31
- @tile_cols = args[0] || 3
32
- @tile_rows = args[1] || 3
33
- @tile_count = 0 # (track) current index (of added images)
34
-
35
- img = ChunkyPNG::Image.new( @tile_cols * @tile_width,
36
- @tile_rows * @tile_height )
37
- else
38
- raise ArgumentError, "cols, rows or image arguments expected; got: #{args.inspect}"
39
- end
40
-
41
- puts " #{img.height}x#{img.width} (height x width)"
42
-
43
- super( nil, nil, img )
44
- end
45
-
46
-
47
- def count() @tile_count; end
48
- alias_method :size, :count ## add size alias (confusing if starting with 0?) - why? why not?
49
-
50
- #####
51
- # set / add tile
52
-
53
- def add( image )
54
- y, x = @tile_count.divmod( @tile_cols )
55
-
56
- puts " [#{@tile_count}] @ (#{x}/#{y}) #{image.width}x#{image.height} (height x width)"
57
-
58
- ## note: image.image - "unwrap" the "raw" ChunkyPNG::Image
59
- @img.compose!( image.image, x*@tile_width, y*@tile_height )
60
- @tile_count += 1
61
- end
62
- alias_method :<<, :add
63
-
64
-
65
-
66
- ######
67
- # get tile
68
-
69
- def tile( index )
70
- y, x = index.divmod( @tile_cols )
71
- img = @img.crop( x*@tile_width, y*@tile_height, @tile_width, @tile_height )
72
- Image.new( img.width, img.height, img ) ## wrap in pixelart image
73
- end
74
-
75
- def []( *args ) ## overload - why? why not?
76
- if args.size == 1
77
- index = args[0]
78
- tile( index )
79
- else
80
- super ## e.g [x,y] --- get pixel
81
- end
82
- end
83
-
84
-
85
- ## convenience helpers to loop over composite
86
- def each( &block )
87
- count.times do |i|
88
- block.call( tile( i ) )
89
- end
90
- end
91
-
92
- def each_with_index( &block )
93
- count.times do |i|
94
- block.call( tile( i ), i )
95
- end
96
- end
97
-
98
-
99
- end # class ImageComposite
100
- 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
+