spritz 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: