spritz 0.0.2

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.
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.png
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org/'
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011-2015 Alexander Staubo
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,88 @@
1
+ # Spritz
2
+
3
+ Spritz is a tool for working with raw 2D assets to generate packed ones.
4
+
5
+ Spritz can take a bunch of images and create tiled atlas texture bitmaps (also known as sprite sheets) for drawing many sprites from a single, packed bitmap image.
6
+
7
+ ## Features
8
+
9
+ * Easily integrates into makefiles.
10
+ * Writes easily readable JSON.
11
+ * Supports all image formats supported by ImageMagick (ie., a lot).
12
+ * Supports outputting to PNG and iOS `pvrtc` (includ gzipped) formats.
13
+ * Can generates setup code for Moai.
14
+
15
+ ## Installation
16
+
17
+ gem install spritz
18
+
19
+ ## Installation from source
20
+
21
+ rake install
22
+
23
+ ## Dependencies
24
+
25
+ * Ruby 1.8.7 or later.
26
+ * ImageMagick 6.x.
27
+ * `textiletool` from iOS SDK, if you want to generate `pvrtc` textures.
28
+
29
+ ## Usage
30
+
31
+ This will generate a `dungeon` package:
32
+
33
+ spritz pack sheets/dungeon dungeon/*.png
34
+
35
+ The command will generate:
36
+
37
+ * `sheets/dungeon.0.png`, `sheets/dungeon.1.png` etc. — the texture bitmaps.
38
+ * `sheets/dungeon.json.gz` — a gzipped JSON file describing the necessary vertex and texture coordinates for each image, ready to be used with drawing functions.
39
+
40
+ For more options:
41
+
42
+ spritz pack --help
43
+
44
+ ## Plugins
45
+
46
+ ### Moai
47
+
48
+ To generate a quad deck for [Moai](http://getmoai.com), use the option:
49
+
50
+ --moai:quad-decks myfile.lua
51
+
52
+ ## Tips
53
+
54
+ For texture-mapping applications, you will want to specify `--padding 1` in order to ensure that texture coordinates round evenly to pixel coordinates.
55
+
56
+ The default texture size is 2048x2048, which corresponds to the largest texture size allowed on the current generation of iOS devices.
57
+
58
+ If an image does not fit within a single texture, it will be split across multiple textures. In other words, there is no upper limit on the dimensions of any image.
59
+
60
+ ## Package format
61
+
62
+ The format of the package JSON file is:
63
+
64
+ * `version`: File format version. Currently 1.
65
+ * `textures`: A hash of textures, indexed by a numeric key.
66
+ * `images`: A hash of image slices for each file. (An image will have multiple slices if its dimensions exceed the maximum texture dimensions.)
67
+
68
+ Each texture hash has the following format:
69
+
70
+ * `file`: File name, relative to the JSON file.
71
+ * `format`: The format, eg. `png`.
72
+
73
+ Each image hash has the following format:
74
+
75
+ * `i`: The index of the texture, which can be looked up in the `textures` key.
76
+ * `v`: The vertex coordinates for this slice.
77
+ * `t`: The texture coordinates for this slice.
78
+ * `w`: The width of the slice.
79
+ * `h`: The haight of the slice.
80
+ * `r`: Will be `true` if rotated -90 degrees.
81
+
82
+ ## Credits
83
+
84
+ The "MaxRects" algorithm was ported from C++ code originally by Jukka Jylänki.
85
+
86
+ ## License
87
+
88
+ See accompanying `LICENSE` file.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/TODO.md ADDED
@@ -0,0 +1,2 @@
1
+ * Generate collision outlines
2
+ * Optimize per-texture for bitmap format and compression: RGBA 4444, RGBA 5551 etc.
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'rubygems'
5
+ rescue LoadError => e
6
+ abort "Rubygems must be installed."
7
+ end
8
+
9
+ require 'spritz'
10
+
11
+ Spritz::CLI.new.run!
@@ -0,0 +1,13 @@
1
+ require 'tempfile'
2
+ require 'rmagick'
3
+ require 'optparse'
4
+ require 'yajl/json_gem'
5
+ require 'zlib'
6
+
7
+ require 'spritz/plugins/moai_plugin'
8
+ require 'spritz/version'
9
+ require 'spritz/logger'
10
+ require 'spritz/rect'
11
+ require 'spritz/max_rects_packer'
12
+ require 'spritz/package'
13
+ require 'spritz/cli'
@@ -0,0 +1,108 @@
1
+ module Spritz
2
+
3
+ class CLI
4
+
5
+ def run!(args = ARGV)
6
+ args.options do |opts|
7
+ opts.banner = %{
8
+ Usage:
9
+ #{File.basename($0)} pack [OPTIONS] PACKAGE FILE ...
10
+ #{File.basename($0)} pack -h | --help
11
+
12
+ }
13
+ opts.on("-h", "--help", "Show this help message.") do
14
+ puts opts
15
+ exit
16
+ end
17
+ opts.on("-v", "--version", "Show version.") do
18
+ puts Spritz::VERSION
19
+ exit
20
+ end
21
+ opts.order! { |x| opts.terminate(x) }
22
+ if args.empty?
23
+ abort "Nothing to do. Run with -h for help."
24
+ end
25
+ end
26
+
27
+ command = args.shift
28
+ case command
29
+ when 'pack'
30
+ pack_command(args)
31
+ else
32
+ abort "Unknown command '#{command}'. Run with -h for help."
33
+ end
34
+ end
35
+
36
+ def pack_command(args)
37
+ texture_width = nil
38
+ texture_height = nil
39
+ padding = 0
40
+ format = nil
41
+ quiet = false
42
+ steps = []
43
+
44
+ args.options do |opts|
45
+ opts.banner = "Usage: #{File.basename($0)} pack [OPTIONS] PACKAGE FILE ...\n\n"
46
+ opts.on("-h", "--help", "Show this help message.") do
47
+ puts opts
48
+ exit
49
+ end
50
+ opts.on("-q", "--[no-]quiet", "Don't output anything.") do |v|
51
+ quiet = !!v
52
+ end
53
+ opts.on("-s", "--size WIDTHxHEIGHT", String,
54
+ "Maximum texture size (defaults to 2048x2048)") do |v|
55
+ if v =~ /(\d+)x(\d+)/i
56
+ texture_width, texture_height = $1.to_i, $2.to_i
57
+ else
58
+ abort "Expected WIDTHxHEIGHT, got #{v.inspect}"
59
+ end
60
+ end
61
+ opts.on("--padding PIXELS", Integer,
62
+ "Padding between packed images (defaults to 0)") do |v|
63
+ padding = v
64
+ end
65
+ opts.on("-f FORMAT", "--format FORMAT", String,
66
+ "Bitmap format for textures. Defaults to 'png', may be set to 'pvrtc'",
67
+ "(requires textiletool from iOS SDK) or 'pvrtc-gz' (same as pvrtc,",
68
+ "but gzip-compressed).") do |v|
69
+ unless %w(png pvrtc pvrtc-gz).include?(v.downcase)
70
+ abort "Unexpected format #{v.inspect}"
71
+ end
72
+ format = v.to_sym
73
+ end
74
+
75
+ Plugins::MoaiPlugin.add_options(opts, steps)
76
+
77
+ opts.order! { |x| opts.terminate(x) }
78
+ if args.empty?
79
+ abort "Nothing to do. Run with -h for help."
80
+ end
81
+ end
82
+
83
+ package_name = args.shift
84
+ abort "Package name not specified." unless package_name
85
+
86
+ Logger.instance.device = $stdout unless quiet
87
+
88
+ package = Spritz::Package.new(
89
+ :name => package_name,
90
+ :width => texture_width,
91
+ :height => texture_height,
92
+ :padding => padding,
93
+ :format => format)
94
+ args.each do |file_glob|
95
+ Dir.glob(file_glob).each do |file_name|
96
+ package.add_file(file_name)
97
+ end
98
+ end
99
+ package.write
100
+
101
+ steps.each do |step|
102
+ step.call(package)
103
+ end
104
+ end
105
+
106
+ end
107
+
108
+ end
@@ -0,0 +1,54 @@
1
+ require 'singleton'
2
+
3
+ module Spritz
4
+
5
+ class Logger
6
+
7
+ include Singleton
8
+
9
+ attr_accessor :device
10
+
11
+ def self.log_write(*args, &block)
12
+ instance.log_write(*args, &block)
13
+ end
14
+
15
+ def self.log_action(*args)
16
+ instance.log_action(*args)
17
+ end
18
+
19
+ def self.log(*args)
20
+ instance.log(*args)
21
+ end
22
+
23
+ def log_write(path, &block)
24
+ if File.exist?(path)
25
+ log_action "Overwrite", path
26
+ else
27
+ log_action "Create", path
28
+ end
29
+ yield path
30
+ end
31
+
32
+ def log_action(what, *rest)
33
+ case what
34
+ when 'Render'
35
+ color = '33'
36
+ when 'Overwrite'
37
+ color = '31'
38
+ when 'Create'
39
+ color = '32'
40
+ else
41
+ color = '36'
42
+ end
43
+ log("\e[#{color};1m%12s\e[0m %s\n" % [what, rest.join(' ')])
44
+ end
45
+
46
+ def log(line)
47
+ if @device
48
+ @device.write(line)
49
+ end
50
+ end
51
+
52
+ end
53
+
54
+ end
@@ -0,0 +1,266 @@
1
+ module Spritz
2
+
3
+ # Adapted from C++ version originally by Jukka Jylänki.
4
+ class MaxRectsPacker
5
+
6
+ def initialize(width, height)
7
+ @width = width
8
+ @height = height
9
+ @free_rects = [Rect.new(nil, 0, 0, @width, @height)]
10
+ @used_rects = []
11
+ end
12
+
13
+ def insert(value, width, height)
14
+ best_score1 = 2 ** 32
15
+ best_score2 = 2 ** 32
16
+ best_rect = nil
17
+
18
+ [:bottom_left, :best_short_side_fit, :best_long_side_fit, :best_area_fit,
19
+ :contact_point_rule].each do |method|
20
+ new_rect, score1, score2 = self.send("find_position_for_#{method}",
21
+ width, height, 2 ** 32, 2 ** 32)
22
+ if new_rect
23
+ score1 = -score1 if method == :contact_point_rule
24
+ if new_rect and score1 < best_score1 or (score1 == best_score1 and score2 < best_score2)
25
+ best_score1 = score1
26
+ best_score2 = score2
27
+ best_rect = new_rect
28
+ end
29
+ end
30
+ end
31
+
32
+ if best_rect
33
+ @free_rects.dup.each do |free|
34
+ if split_free_node(free, best_rect)
35
+ @free_rects.delete(free)
36
+ end
37
+ end
38
+
39
+ @free_rects.dup.each_with_index do |free, i|
40
+ (@free_rects[(i + 1)..-1] || []).each do |free2|
41
+ @free_rects.delete(free) if free.contained_in?(free2)
42
+ @free_rects.delete(free2) if free2.contained_in?(free)
43
+ end
44
+ end
45
+
46
+ best_rect.value = value
47
+ @used_rects.push(best_rect)
48
+ true
49
+ else
50
+ false
51
+ end
52
+ end
53
+
54
+ def coverage_ratio
55
+ return @used_rects.inject(0) { |sum, rect| sum + rect.width * rect.height } /
56
+ (@width * @height).to_f
57
+ end
58
+
59
+ def rects
60
+ @used_rects
61
+ end
62
+
63
+ attr_reader :width
64
+ attr_reader :height
65
+
66
+ private
67
+
68
+ def find_position_for_bottom_left(width, height, best_y, best_x)
69
+ best_rect = nil
70
+ best_y = 2 ** 32
71
+ @free_rects.each do |free|
72
+ if free.width >= width and free.height >= height
73
+ top_side_y = free.y + height
74
+ if top_side_y < best_y or (top_side_y == best_y and free.x < best_x)
75
+ best_rect = Rect.new(nil, free.x, free.y, width, height)
76
+ best_y = top_side_y
77
+ best_x = free.x
78
+ end
79
+ end
80
+ if free.width >= height and free.height >= width
81
+ top_side_y = free.y + width
82
+ if top_side_y < best_y or (top_side_y == best_y and free.x < best_x)
83
+ best_rect = Rect.new(nil, free.x, free.y, height, width, true)
84
+ best_y = top_side_y
85
+ best_x = free.x
86
+ end
87
+ end
88
+ end
89
+ return best_rect, best_y, best_x
90
+ end
91
+
92
+ def find_position_for_best_short_side_fit(width, height,
93
+ best_short_side_fit, best_long_side_fit)
94
+ best_rect = nil
95
+ best_short_side_fit = 2 ** 32
96
+ @free_rects.each do |free|
97
+ if free.width >= width and free.height >= height
98
+ leftover_horiz = (free.width - width).abs
99
+ leftover_vert = (free.height - height).abs
100
+ short_side_fit = [leftover_horiz, leftover_vert].min
101
+ long_side_fit = [leftover_horiz, leftover_vert].max
102
+ if short_side_fit < best_short_side_fit or
103
+ (short_side_fit == best_short_side_fit and long_side_fit < best_long_side_fit)
104
+ best_rect = Rect.new(nil, free.x, free.y, width, height)
105
+ best_short_side_fit = short_side_fit
106
+ best_long_side_fit = long_side_fit
107
+ end
108
+ end
109
+ if free.width >= height and free.height >= width
110
+ flipped_leftover_horiz = (free.width - height).abs
111
+ flipped_leftover_vert = (free.height - width).abs
112
+ flipped_short_side_fit = [flipped_leftover_horiz, flipped_leftover_vert].min
113
+ flipped_long_side_fit = [flipped_leftover_horiz, flipped_leftover_vert].max
114
+ if flipped_short_side_fit < best_short_side_fit or
115
+ (flipped_short_side_fit == best_short_side_fit and flipped_long_side_fit < best_long_side_fit)
116
+ best_rect = Rect.new(nil, free.x, free.y, height, width, true)
117
+ best_short_side_fit = flipped_short_side_fit
118
+ best_long_side_fit = flipped_long_side_fit
119
+ end
120
+ end
121
+ end
122
+ return best_rect, best_short_side_fit, best_long_side_fit
123
+ end
124
+
125
+ def find_position_for_best_long_side_fit(width, height,
126
+ best_short_side_fit, best_long_side_fit)
127
+ best_rect = nil
128
+ best_long_side_fit = 2 ** 32
129
+ @free_rects.each do |free|
130
+ if free.width >= width and free.height >= height
131
+ leftover_horiz = (free.width - width).abs
132
+ leftover_vert = (free.height - height).abs
133
+ short_side_fit = [leftover_horiz, leftover_vert].min
134
+ long_side_fit = [leftover_horiz, leftover_vert].max
135
+ if long_side_fit < best_long_side_fit or
136
+ (long_side_fit == best_long_side_fit and short_side_fit < best_short_side_fit)
137
+ best_rect = Rect.new(nil, free.x, free.y, width, height)
138
+ best_short_side_fit = short_side_fit
139
+ best_long_side_fit = long_side_fit
140
+ end
141
+ end
142
+ if free.width >= height and free.height >= width
143
+ leftover_horiz = (free.width - height).abs
144
+ leftover_vert = (free.height - width).abs
145
+ short_side_fit = [leftover_horiz, leftover_vert].min
146
+ long_side_fit = [leftover_horiz, leftover_vert].max
147
+ if long_side_fit < best_long_side_fit or
148
+ (long_side_fit == best_long_side_fit and short_side_fit < best_short_side_fit)
149
+ best_rect = Rect.new(nil, free.x, free.y, height, width, true)
150
+ best_short_side_fit = short_side_fit
151
+ best_long_side_fit = long_side_fit
152
+ end
153
+ end
154
+ end
155
+ return best_rect, best_short_side_fit, best_long_side_fit
156
+ end
157
+
158
+ def find_position_for_best_area_fit(width, height,
159
+ best_area_fit, best_short_side_fit)
160
+ best_rect = nil
161
+ best_area_fit = 2 ** 32
162
+ @free_rects.each do |free|
163
+ area_fit = free.width * free.height - width * height
164
+ if free.width >= width and free.height >= height
165
+ leftover_horiz = (free.width - width).abs
166
+ leftover_vert = (free.height - height).abs
167
+ short_side_fit = [leftover_horiz, leftover_vert].min
168
+ if area_fit < best_area_fit or
169
+ (area_fit == best_area_fit and short_side_fit < best_short_side_fit)
170
+ best_rect = Rect.new(nil, free.x, free.y, width, height)
171
+ best_short_side_fit = short_side_fit
172
+ best_area_fit = area_fit
173
+ end
174
+ end
175
+ if free.width >= height and free.height >= width
176
+ leftover_horiz = (free.width - height).abs
177
+ leftover_vert = (free.height - width).abs
178
+ short_side_fit = [leftover_horiz, leftover_vert].min
179
+ if area_fit < best_area_fit or
180
+ (area_fit == best_area_fit and short_side_fit < best_short_side_fit)
181
+ best_rect = Rect.new(nil, free.x, free.y, height, width, true)
182
+ best_short_side_fit = short_side_fit
183
+ best_area_fit = area_fit
184
+ end
185
+ end
186
+ end
187
+ return best_rect, best_area_fit, best_short_side_fit
188
+ end
189
+
190
+ def common_interval_length(a, b)
191
+ if a.end < b.begin or b.end < a.begin
192
+ 0
193
+ else
194
+ [a.end, b.end].min - [a.begin, b.begin].max
195
+ end
196
+ end
197
+
198
+ def contact_point_score_node(x, y, width, height)
199
+ score = 0
200
+ score += height if x == 0 or x + width == @width
201
+ score += width if y == 0 or y + height == @height
202
+ @used_rects.each do |rect|
203
+ if rect.x == x + width or rect.x + rect.width == x
204
+ score += common_interval_length(
205
+ rect.y..(rect.y + rect.height),
206
+ y..(y + height))
207
+ end
208
+ if rect.y == y + height or rect.y + rect.height == y
209
+ score += common_interval_length(
210
+ rect.x..(rect.x + rect.width),
211
+ x..(x + width))
212
+ end
213
+ end
214
+ score
215
+ end
216
+
217
+ def find_position_for_contact_point_rule(width, height, best_contact_score, _)
218
+ best_rect = nil
219
+ best_contact_score = -1
220
+ @free_rects.each do |free|
221
+ if free.width >= width and free.height >= height
222
+ score = contact_point_score_node(free.x, free.y, width, height)
223
+ if score > best_contact_score
224
+ best_rect = Rect.new(nil, free.x, free.y, width, height)
225
+ best_contact_score = score
226
+ end
227
+ end
228
+ if free.width >= height and free.height >= width
229
+ score = contact_point_score_node(free.x, free.y, width, height)
230
+ if score > best_contact_score
231
+ best_rect = Rect.new(nil, free.x, free.y, height, width, true)
232
+ best_contact_score = score
233
+ end
234
+ end
235
+ end
236
+ return best_rect, best_contact_score, 0
237
+ end
238
+
239
+ def split_free_node(free, used)
240
+ if used.intersects?(free)
241
+ false
242
+ else
243
+ if used.x < free.x + free.width and used.x + used.width > free.x
244
+ if used.y > free.y and used.y < free.y + free.height
245
+ @free_rects.push(Rect.new(nil, free.x, free.y, free.width, used.y - free.y))
246
+ end
247
+ if used.y + used.height < free.y + free.height
248
+ @free_rects.push(Rect.new(nil, free.x, used.y + used.height,
249
+ free.width, free.y + free.height - (used.y + used.height)))
250
+ end
251
+ end
252
+ if used.y < free.y + free.height and used.y + used.height > free.y
253
+ if used.x > free.x and used.x < free.x + free.width
254
+ @free_rects.push(Rect.new(nil, free.x, free.y, used.x - free.x, free.height))
255
+ end
256
+ if used.x + used.width < free.x + free.width
257
+ @free_rects.push(Rect.new(nil, used.x + used.width, free.y,
258
+ free.x + free.width - (used.x + used.width), free.height))
259
+ end
260
+ end
261
+ true
262
+ end
263
+ end
264
+
265
+ end
266
+ end
@@ -0,0 +1,249 @@
1
+ module Spritz
2
+
3
+ class Package
4
+
5
+ class Slice
6
+
7
+ attr_reader :file_name, :x, :y, :width, :height, :padding
8
+
9
+ def initialize(file_name, x, y, width, height, padding)
10
+ @file_name, @x, @y, @width, @height, @padding =
11
+ file_name, x, y, width, height, padding
12
+ end
13
+
14
+ def image
15
+ image = Magick::Image.from_blob(File.read(@file_name)).first
16
+ image.crop!(@x, @y, @width, @height)
17
+ image
18
+ end
19
+
20
+ end
21
+
22
+ attr_reader :name
23
+ attr_reader :texture_width, :texture_height
24
+ attr_reader :format
25
+ attr_reader :padding
26
+ attr_reader :sheets
27
+
28
+ def initialize(options = {})
29
+ @name = options[:name]
30
+ @texture_width = options[:width] || 2048
31
+ @texture_height = options[:height] || 2048
32
+ @format = options[:format] || :png
33
+ @padding = options[:padding]
34
+ @sheets = []
35
+ @rendered_images = {}
36
+ end
37
+
38
+ def add_file(file_name)
39
+ Logger.log_action "Add", file_name
40
+
41
+ image = Magick::Image.ping(file_name).first
42
+ t_width = @texture_width - @padding * 2
43
+ t_height = @texture_height - @padding * 2
44
+ (0..(image.columns / t_width.to_f).floor).to_a.each do |tile_x|
45
+ (0..(image.rows / t_height.to_f).floor).to_a.each do |tile_y|
46
+ if image.columns - tile_x * t_width + @padding > 0 and
47
+ image.rows - tile_y * t_height + @padding > 0
48
+ add_slice(
49
+ Slice.new(file_name,
50
+ tile_x * t_width,
51
+ tile_y * t_height,
52
+ [image.columns - tile_x * t_width + @padding, t_width].min,
53
+ [image.rows - tile_y * t_height + @padding, t_height].min,
54
+ @padding))
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ def add_slice(slice)
61
+ inserted = false
62
+ @sheets.each do |sheet|
63
+ if sheet.insert(slice, slice.width, slice.height)
64
+ inserted = true
65
+ break
66
+ end
67
+ end
68
+ unless inserted
69
+ sheet = new_sheet!
70
+ unless sheet.insert(slice, slice.width, slice.height)
71
+ raise "Unexpectedly unable to add image to any sheet: #{slice.file_name}"
72
+ end
73
+ end
74
+ end
75
+
76
+ def render
77
+ @sheets.each_with_index do |sheet, index|
78
+ Logger.log_action "Render", "Sheet ##{index}"
79
+ @rendered_images[index] ||= render_sheet(sheet)
80
+ end
81
+ @rendered_images
82
+ end
83
+
84
+ def write
85
+ render
86
+
87
+ @rendered_images.each_pair do |index, image|
88
+ base_name = "#{@name}.#{index}"
89
+ case @format
90
+ when :png
91
+ Logger.log_write(file_name_for_format(base_name, @format)) do |path|
92
+ File.open(path, 'w:binary') do |file|
93
+ file.write as_png(image)
94
+ end
95
+ end
96
+ when :pvrtc, :'pvrtc-gz'
97
+ Logger.log_write(file_name_for_format(base_name, @format)) do |path|
98
+ Tempfile.open('spritz') do |tempfile|
99
+ tempfile.write as_png(image)
100
+ tempfile.close
101
+
102
+ texturetool_flags = '--alpha-is-opacity --bits-per-pixel-4'
103
+
104
+ system("texturetool -f raw -e PVRTC #{texturetool_flags} " <<
105
+ "-o #{file_name_for_format base_name, :pvrtc} #{tempfile.path}")
106
+ unless $?.exited? and $?.exitstatus == 0
107
+ raise "Failed to run texturetool. Is it installed?"
108
+ end
109
+
110
+ if @format == :'pvrtc-gz'
111
+ system("gzip -c #{file_name_for_format base_name, :pvrtc} >#{path}")
112
+ unless $?.exited? and $?.exitstatus == 0
113
+ raise "Failed to run gzip. Is it installed?"
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ image.destroy!
120
+ end
121
+
122
+ rects_by_slice = {}
123
+ @sheets.each_with_index do |sheet, index|
124
+ sheet.rects.each do |rect|
125
+ (rects_by_slice[rect.value.file_name] ||= []).push([index, rect])
126
+ end
127
+ end
128
+
129
+ frames = {}
130
+ rects_by_slice.each do |file_name, rects|
131
+ key = file_name.gsub(/\.\w+$/, '')
132
+ frames[key] = rects.map { |(sheet_index, rect)|
133
+ {
134
+ :i => sheet_index,
135
+ :w => rect.width,
136
+ :h => rect.height,
137
+ :r => rect.rotated?,
138
+ :v => vertex_coords_for(rect),
139
+ :t => texture_coords_for(rect)
140
+ }
141
+ }
142
+ end
143
+ structure = {
144
+ :version => 1,
145
+ :textures => Hash[*(0...@sheets.length).to_a.map { |i|
146
+ [i, {
147
+ :format => @format,
148
+ :file => "#{File.basename(@name)}.#{i}.#{@format}"
149
+ }]
150
+ }.flatten],
151
+ :texture_width => @texture_width,
152
+ :texture_height => @texture_height,
153
+ :padding => @padding,
154
+ :frames => frames
155
+ }
156
+
157
+ Logger.log_write("#{@name}.json.gz") do |path|
158
+ Zlib::GzipWriter.open(path) do |file|
159
+ file.write JSON.pretty_generate(structure)
160
+ end
161
+ end
162
+ end
163
+
164
+ def file_name_for_format(base, format)
165
+ case format
166
+ when :png, :pvrtc
167
+ return "#{base}.#{format}"
168
+ when :'pvrtc-gz'
169
+ return "#{base}.pvr.gz"
170
+ end
171
+ end
172
+
173
+ private
174
+
175
+ def as_png(image)
176
+ image.to_blob {
177
+ self.format = 'png'
178
+ self.compression = Magick::ZipCompression
179
+ self.depth = 16
180
+ self.quality = 100
181
+ }
182
+ end
183
+
184
+ def vertex_coords_for(rect)
185
+ x1 = 0
186
+ y1 = 0
187
+ x2 = rect.width
188
+ y2 = rect.height
189
+ return [
190
+ [x1, y1],
191
+ [x2, x1],
192
+ [y1, y2],
193
+ [x2, y2]
194
+ ]
195
+ end
196
+
197
+ def texture_coords_for(rect)
198
+ if @padding > 0
199
+ tx1 = ((2 * rect.x) + 1) / (2 * @texture_width.to_f)
200
+ ty1 = ((2 * rect.y) + 1) / (2 * @texture_height.to_f)
201
+ if rect.rotated?
202
+ tx2 = (((2 * rect.x) + 1) + (rect.height * 2) - 2) / (2 * @texture_width.to_f)
203
+ ty2 = (((2 * rect.y) + 1) + (rect.width * 2) - 2) / (2 * @texture_height.to_f)
204
+ else
205
+ tx2 = (((2 * rect.x) + 1) + (rect.width * 2) - 2) / (2 * @texture_width.to_f)
206
+ ty2 = (((2 * rect.y) + 1) + (rect.height * 2) - 2) / (2 * @texture_height.to_f)
207
+ end
208
+ else
209
+ tx1 = rect.x / @texture_width.to_f
210
+ ty1 = rect.y / @texture_height.to_f
211
+ if rect.rotated?
212
+ tx2 = (rect.x + rect.height) / @texture_height.to_f
213
+ ty2 = (rect.y + rect.width) / @texture_width.to_f
214
+ else
215
+ tx2 = (rect.x + rect.width) / @texture_width.to_f
216
+ ty2 = (rect.y + rect.height) / @texture_height.to_f
217
+ end
218
+ end
219
+ return [
220
+ [tx1, ty1],
221
+ [tx2, ty1],
222
+ [tx2, ty2],
223
+ [tx1, ty2]
224
+ ]
225
+ end
226
+
227
+ def render_sheet(sheet)
228
+ image = Magick::Image.new(@texture_width, @texture_height) do |i|
229
+ i.background_color = 'transparent'
230
+ end
231
+ sheet.rects.each do |rect|
232
+ slice = rect.value
233
+ frame = slice.image
234
+ frame.rotate!(-90) if rect.rotated?
235
+ image.composite!(frame, Magick::NorthWestGravity,
236
+ @padding + rect.x, @padding + rect.y, Magick::ReplaceCompositeOp)
237
+ end
238
+ image
239
+ end
240
+
241
+ def new_sheet!
242
+ sheet = MaxRectsPacker.new(@texture_width - @padding, @texture_height - @padding)
243
+ @sheets.push(sheet)
244
+ sheet
245
+ end
246
+
247
+ end
248
+
249
+ end
@@ -0,0 +1,93 @@
1
+ module Spritz
2
+ module Plugins
3
+
4
+ class MoaiPlugin
5
+
6
+ def self.add_options(opts, steps)
7
+ opts.on("--moai:quad-decks PATH", String,
8
+ "Generate code to create quads and decks. One MOAIGfxQuadDeck2D is " \
9
+ "created for each base name, with suffix number being the index.") do |v|
10
+ steps.push(MoaiQuadDeckStep.new(v))
11
+ end
12
+ end
13
+
14
+ class MoaiQuadDeckStep
15
+
16
+ def initialize(path)
17
+ @path = path
18
+ @path = "#{@path}.lua" unless path =~ /\.lua$/
19
+ end
20
+
21
+ def call(package)
22
+ grouped, indexes = {}, {}
23
+
24
+ package.sheets.each_with_index do |sheet, index|
25
+ sheet.rects.each do |rect|
26
+ if rect.value.file_name =~ /^(.+)\d+.*/
27
+ key = $1.gsub(/\//, '_').gsub(/[_\.\-]+$/, '')
28
+ if indexes.include?(key) and indexes[key] != index
29
+ abort "Image #{rect.value.file_name} straddles multiple textures. Not supported yet, sorry."
30
+ end
31
+ (grouped[key] ||= []).push(rect)
32
+ indexes[key] = index
33
+ end
34
+ end
35
+ end
36
+
37
+ script = []
38
+ script << "-- Generated by Spritz (https://github.com/atombender/spritz)"
39
+ if grouped.any?
40
+ script << ''
41
+ package.sheets.each_with_index do |sheet, index|
42
+ texture_variable = "texture#{index}"
43
+ base_name = "#{package.name}.#{index}"
44
+ script << %{local #{texture_variable} = MOAITexture.new()}
45
+ script << %{#{texture_variable}:load('#{package.file_name_for_format(base_name, package.format)}')}
46
+ script << %{#{texture_variable}:setFilter(MOAITexture.GL_LINEAR, MOAITexture.GL_LINEAR)}
47
+ end
48
+ grouped.each do |key, rects|
49
+ index = indexes[key]
50
+ sheet = package.sheets[index]
51
+ quad_variable = "#{key}_quad"
52
+
53
+ script << %{#{quad_variable} = MOAIGfxQuadDeck2D.new()}
54
+ script << %{#{quad_variable}:setTexture(texture#{index})}
55
+ script << %{#{quad_variable}:reserve(#{rects.length})}
56
+ rects.each_with_index do |rect, i|
57
+ box = [-rect.width / 2.0, -rect.height / 2.0, rect.width / 2.0, rect.height / 2.0]
58
+
59
+ quad = [
60
+ rect.x / sheet.width.to_f,
61
+ rect.y / sheet.height.to_f,
62
+
63
+ (rect.x + rect.width) / sheet.width.to_f,
64
+ rect.y / sheet.height.to_f,
65
+
66
+ (rect.x + rect.width) / sheet.width.to_f,
67
+ (rect.y + rect.height) / sheet.height.to_f,
68
+
69
+ rect.x / sheet.width.to_f,
70
+ (rect.y + rect.height) / sheet.height.to_f,
71
+ ]
72
+
73
+ script << %{#{quad_variable}:setRect(#{i + 1}, #{box.join(', ')})}
74
+ script << %{#{quad_variable}:setUVQuad(#{i + 1}, #{quad.join(', ')})}
75
+ end
76
+ end
77
+ else
78
+ script << "-- No images"
79
+ end
80
+
81
+ Logger.log_write(@path) do |path|
82
+ File.open(path, 'w') do |f|
83
+ f.write script.join("\n")
84
+ f.write "\n"
85
+ end
86
+ end
87
+ end
88
+
89
+ end
90
+
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,36 @@
1
+ module Spritz
2
+
3
+ class Rect
4
+
5
+ attr_accessor :x, :y, :width, :height, :value
6
+
7
+ def initialize(value, x, y, width, height, rotated = false)
8
+ @value, @x, @y, @width, @height, @rotated = value, x, y, width, height, rotated
9
+ end
10
+
11
+ def rotated?
12
+ @rotated
13
+ end
14
+
15
+ def intersects?(other)
16
+ return @x >= other.x + other.width || @x + @width <= other.x ||
17
+ @y >= other.y + other.height || @y + @height <= other.y
18
+ end
19
+
20
+ def contained_in?(other)
21
+ return @x >= other.x && @y >= other.y &&
22
+ @x + @width <= other.x + other.width &&
23
+ @y + @height <= other.y + other.height
24
+ end
25
+
26
+ def x2
27
+ @x + @width
28
+ end
29
+
30
+ def y2
31
+ @y + @height
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,3 @@
1
+ module Spritz
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/spritz/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Alexander Staubo"]
6
+ gem.email = ["alex@purefiction.net"]
7
+ gem.description = gem.summary =
8
+ %q{Tool for packing images into compact "sprite sheets" and otherwise managing 2D engine assets}
9
+ gem.homepage = 'https://github.com/alexstaubo/spritz'
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "spritz"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Spritz::VERSION
17
+
18
+ gem.add_runtime_dependency 'rmagick', ['= 2.13.4']
19
+ gem.add_runtime_dependency 'yajl-ruby', ['~> 1.1']
20
+
21
+ gem.add_development_dependency "rspec"
22
+ gem.add_development_dependency "rake"
23
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: spritz
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alexander Staubo
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-01-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rmagick
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - '='
20
+ - !ruby/object:Gem::Version
21
+ version: 2.13.4
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - '='
28
+ - !ruby/object:Gem::Version
29
+ version: 2.13.4
30
+ - !ruby/object:Gem::Dependency
31
+ name: yajl-ruby
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '1.1'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '1.1'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rake
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: Tool for packing images into compact "sprite sheets" and otherwise managing
79
+ 2D engine assets
80
+ email:
81
+ - alex@purefiction.net
82
+ executables:
83
+ - spritz
84
+ extensions: []
85
+ extra_rdoc_files: []
86
+ files:
87
+ - .gitignore
88
+ - Gemfile
89
+ - LICENSE
90
+ - README.md
91
+ - Rakefile
92
+ - TODO.md
93
+ - bin/spritz
94
+ - lib/spritz.rb
95
+ - lib/spritz/cli.rb
96
+ - lib/spritz/logger.rb
97
+ - lib/spritz/max_rects_packer.rb
98
+ - lib/spritz/package.rb
99
+ - lib/spritz/plugins/moai_plugin.rb
100
+ - lib/spritz/rect.rb
101
+ - lib/spritz/version.rb
102
+ - spritz.gemspec
103
+ homepage: https://github.com/alexstaubo/spritz
104
+ licenses: []
105
+ post_install_message:
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ none: false
111
+ requirements:
112
+ - - ! '>='
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ! '>='
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubyforge_project:
123
+ rubygems_version: 1.8.23.2
124
+ signing_key:
125
+ specification_version: 3
126
+ summary: Tool for packing images into compact "sprite sheets" and otherwise managing
127
+ 2D engine assets
128
+ test_files: []
129
+ has_rdoc: