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.
- data/.gitignore +18 -0
- data/Gemfile +2 -0
- data/LICENSE +20 -0
- data/README.md +88 -0
- data/Rakefile +2 -0
- data/TODO.md +2 -0
- data/bin/spritz +11 -0
- data/lib/spritz.rb +13 -0
- data/lib/spritz/cli.rb +108 -0
- data/lib/spritz/logger.rb +54 -0
- data/lib/spritz/max_rects_packer.rb +266 -0
- data/lib/spritz/package.rb +249 -0
- data/lib/spritz/plugins/moai_plugin.rb +93 -0
- data/lib/spritz/rect.rb +36 -0
- data/lib/spritz/version.rb +3 -0
- data/spritz.gemspec +23 -0
- metadata +129 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
data/TODO.md
ADDED
data/bin/spritz
ADDED
data/lib/spritz.rb
ADDED
@@ -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'
|
data/lib/spritz/cli.rb
ADDED
@@ -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
|
data/lib/spritz/rect.rb
ADDED
@@ -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
|
data/spritz.gemspec
ADDED
@@ -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:
|