polymer 1.0.0.beta.3
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +8 -0
- data/History.md +126 -0
- data/LICENSE +28 -0
- data/README.md +229 -0
- data/Rakefile +186 -0
- data/bin/polymer +10 -0
- data/lib/polymer/cache.rb +106 -0
- data/lib/polymer/cli.rb +340 -0
- data/lib/polymer/core_ext.rb +78 -0
- data/lib/polymer/css_generator.rb +32 -0
- data/lib/polymer/deviant_finder.rb +76 -0
- data/lib/polymer/dsl.rb +283 -0
- data/lib/polymer/man/polymer-bond.1 +60 -0
- data/lib/polymer/man/polymer-bond.1.txt +66 -0
- data/lib/polymer/man/polymer-init.1 +33 -0
- data/lib/polymer/man/polymer-init.1.txt +42 -0
- data/lib/polymer/man/polymer-optimise.1 +23 -0
- data/lib/polymer/man/polymer-optimise.1.txt +25 -0
- data/lib/polymer/man/polymer-position.1 +39 -0
- data/lib/polymer/man/polymer-position.1.txt +42 -0
- data/lib/polymer/man/polymer.1 +50 -0
- data/lib/polymer/man/polymer.1.txt +60 -0
- data/lib/polymer/man/polymer.5 +130 -0
- data/lib/polymer/man/polymer.5.txt +145 -0
- data/lib/polymer/optimisation.rb +130 -0
- data/lib/polymer/project.rb +164 -0
- data/lib/polymer/sass_generator.rb +38 -0
- data/lib/polymer/source.rb +55 -0
- data/lib/polymer/sprite.rb +130 -0
- data/lib/polymer/templates/polymer.tt +28 -0
- data/lib/polymer/templates/sass_mixins.erb +29 -0
- data/lib/polymer/templates/sources/one/book.png +0 -0
- data/lib/polymer/templates/sources/one/box-label.png +0 -0
- data/lib/polymer/templates/sources/one/calculator.png +0 -0
- data/lib/polymer/templates/sources/one/calendar-month.png +0 -0
- data/lib/polymer/templates/sources/one/camera.png +0 -0
- data/lib/polymer/templates/sources/one/eraser.png +0 -0
- data/lib/polymer/templates/sources/two/inbox-image.png +0 -0
- data/lib/polymer/templates/sources/two/magnet.png +0 -0
- data/lib/polymer/templates/sources/two/newspaper.png +0 -0
- data/lib/polymer/templates/sources/two/television.png +0 -0
- data/lib/polymer/templates/sources/two/wand-hat.png +0 -0
- data/lib/polymer/templates/sources/two/wooden-box-label.png +0 -0
- data/lib/polymer/version.rb +4 -0
- data/lib/polymer.rb +49 -0
- data/polymer.gemspec +94 -0
- metadata +206 -0
data/lib/polymer/cli.rb
ADDED
@@ -0,0 +1,340 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'thor'
|
3
|
+
|
4
|
+
module Polymer
|
5
|
+
class CLI < Thor
|
6
|
+
|
7
|
+
include Thor::Actions
|
8
|
+
|
9
|
+
class_option 'no-color', :type => :boolean, :default => false,
|
10
|
+
:desc => 'Disable colours in output', :aliases => '--no-colour'
|
11
|
+
|
12
|
+
def initialize(*args)
|
13
|
+
super
|
14
|
+
self.shell = Thor::Shell::Basic.new if options['no-color']
|
15
|
+
end
|
16
|
+
|
17
|
+
# --- bond ---------------------------------------------------------------
|
18
|
+
|
19
|
+
desc 'bond [SPRITES]',
|
20
|
+
'Creates the sprites specified by your .polymer or polymer.rb file'
|
21
|
+
|
22
|
+
long_desc <<-DESC
|
23
|
+
The bond task reads your project configuration and creates your shiny
|
24
|
+
new sprites. If enabled, CSS and/or SCSS will also be written so as to
|
25
|
+
make working with your sprites a little easier.
|
26
|
+
|
27
|
+
You may specify exactly which sprites you want generated, otherwise
|
28
|
+
Polymer will generate all sprites defined in your config file. Any
|
29
|
+
sprite which has not changed since you last ran this command will not be
|
30
|
+
re-generated unless you pass the --force option.
|
31
|
+
DESC
|
32
|
+
|
33
|
+
method_option :force, :type => :boolean, :default => false,
|
34
|
+
:desc => 'Re-generates sprites whose sources have not changed'
|
35
|
+
|
36
|
+
method_option :fast, :type => :boolean, :default => false,
|
37
|
+
:desc => "Skip optimisation of images after they are generated"
|
38
|
+
|
39
|
+
def bond(*sprites)
|
40
|
+
project = find_project!
|
41
|
+
|
42
|
+
# Determine which sprites we'll be working on.
|
43
|
+
sprites = project.sprites.select do |sprite|
|
44
|
+
if sprites.empty? or sprites.include?(sprite.name)
|
45
|
+
# The user specified no sprites, or this sprite was requested.
|
46
|
+
if project.cache.stale?(sprite) or options[:force]
|
47
|
+
# Digest is different, user is forcing update or sprite file
|
48
|
+
# has been deleted.
|
49
|
+
project.cache.set(sprite)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# There's nothing to generate.
|
55
|
+
return if sprites.empty?
|
56
|
+
|
57
|
+
# Get on with it.
|
58
|
+
sprites.each do |sprite|
|
59
|
+
next unless sprite.save
|
60
|
+
|
61
|
+
say_status('generated', sprite.name, :green)
|
62
|
+
|
63
|
+
unless options[:fast]
|
64
|
+
say " optimising #{sprite.name} ... "
|
65
|
+
before = sprite.save_path.size
|
66
|
+
|
67
|
+
reduction = Polymer::Optimisation.optimise_file(sprite.save_path)
|
68
|
+
|
69
|
+
if reduction > 0
|
70
|
+
saved = '- saved %.2fkb (%.1f' %
|
71
|
+
[reduction.to_f / 1024, (reduction.to_f / before) * 100]
|
72
|
+
say_status "\r\e[0K optimised", "#{sprite.name} #{saved}%)", :green
|
73
|
+
else
|
74
|
+
print "\r\e[0K"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Stylesheets.
|
80
|
+
if SassGenerator.generate(project)
|
81
|
+
say_status('written', 'Sass mixin', :green)
|
82
|
+
end
|
83
|
+
|
84
|
+
#process Processors::CSS, project
|
85
|
+
|
86
|
+
# Find sprites with deviant-width sources.
|
87
|
+
sprites.each do |sprite|
|
88
|
+
if deviants = DeviantFinder.find_deviants(sprite)
|
89
|
+
say DeviantFinder.format_ui_message(sprite, deviants)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Clean up the cache, removing sprites which no longer exist.
|
94
|
+
project.cache.remove_all_except(project.sprites)
|
95
|
+
|
96
|
+
# Finish by writing the new cache.
|
97
|
+
project.cache.write
|
98
|
+
|
99
|
+
rescue Polymer::MissingSource, Polymer::TargetNotWritable => e
|
100
|
+
say e.message.compress_lines, :red
|
101
|
+
exit 1
|
102
|
+
end
|
103
|
+
|
104
|
+
# --- help ---------------------------------------------------------------
|
105
|
+
|
106
|
+
# Provides customised help information using the man pages.
|
107
|
+
# Nod-of-the-hat to Bundler.
|
108
|
+
def help(command = nil)
|
109
|
+
page_map = {
|
110
|
+
# Main manual page.
|
111
|
+
nil => 'polymer.1',
|
112
|
+
'polymer' => 'polymer.1',
|
113
|
+
|
114
|
+
# Sub-commands.
|
115
|
+
'init' => 'polymer-init.1',
|
116
|
+
'bond' => 'polymer-bond.1',
|
117
|
+
'optimise' => 'polymer-optimise.1',
|
118
|
+
'optimize' => 'polymer-optimise.1',
|
119
|
+
'position' => 'polymer-position.1',
|
120
|
+
|
121
|
+
# Configuration format.
|
122
|
+
'polymer(5)' => 'polymer.5',
|
123
|
+
'polymer.5' => 'polymer.5',
|
124
|
+
'.polymer' => 'polymer.5',
|
125
|
+
'polymer.rb' => 'polymer.5',
|
126
|
+
'config' => 'polymer.5'
|
127
|
+
}
|
128
|
+
|
129
|
+
if page_map.has_key?(command)
|
130
|
+
root = File.expand_path('../man', __FILE__)
|
131
|
+
|
132
|
+
if groff_available?
|
133
|
+
groff = 'groff -Wall -mtty-char -mandoc -Tascii'
|
134
|
+
pager = ENV['MANPAGER'] || ENV['PAGER'] || 'more'
|
135
|
+
|
136
|
+
Kernel.exec "#{groff} #{root}/#{page_map[command]} | #{pager}"
|
137
|
+
else
|
138
|
+
puts File.read("#{root}/#{page_map[command]}.txt")
|
139
|
+
end
|
140
|
+
else
|
141
|
+
super
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# --- init ---------------------------------------------------------------
|
146
|
+
|
147
|
+
desc 'init', 'Creates a new Polymer project in the current directory'
|
148
|
+
|
149
|
+
long_desc <<-DESC
|
150
|
+
In order to use Polymer, a .polymer configuration file must be created.
|
151
|
+
The init task creates a sample configuration, and also adds a couple of
|
152
|
+
example source images to demonstrate how to use Polymer to create your
|
153
|
+
own sprite images.
|
154
|
+
DESC
|
155
|
+
|
156
|
+
method_option :sprites, :type => :string, :default => 'public/images',
|
157
|
+
:desc => 'Default location to which generated sprites are saved'
|
158
|
+
|
159
|
+
method_option :sources, :type => :string, :default => '<sprites>/sprites',
|
160
|
+
:desc => 'Default location of source images'
|
161
|
+
|
162
|
+
method_option 'no-examples', :type => :boolean, :default => false,
|
163
|
+
:desc => "Disables copying of example source files"
|
164
|
+
|
165
|
+
method_option :windows, :type => :boolean, :default => false,
|
166
|
+
:desc => 'Create polymer.rb instead of .polymer for easier editing on ' \
|
167
|
+
'Windows systems.'
|
168
|
+
|
169
|
+
def init
|
170
|
+
if File.exists?('.polymer')
|
171
|
+
say 'A .polymer file already exists in this directory.', :red
|
172
|
+
exit 1
|
173
|
+
end
|
174
|
+
|
175
|
+
project_dir = Pathname.new(Dir.pwd)
|
176
|
+
|
177
|
+
config = {
|
178
|
+
:sprites => options[:sprites],
|
179
|
+
:sources => options[:sources].gsub(/<sprites>/, options[:sprites]),
|
180
|
+
:windows => options[:windows]
|
181
|
+
}
|
182
|
+
|
183
|
+
filename = options[:windows] ? 'polymer.rb' : '.polymer'
|
184
|
+
polymerfile = project_dir + filename
|
185
|
+
|
186
|
+
template 'polymer.tt', polymerfile, config
|
187
|
+
|
188
|
+
# Clean up the template.
|
189
|
+
contents = polymerfile.read.gsub(/\n{3,}/, "\n\n")
|
190
|
+
polymerfile.open('w') { |file| file.puts contents }
|
191
|
+
|
192
|
+
unless options['no-examples']
|
193
|
+
directory 'sources', project_dir + config[:sources]
|
194
|
+
end
|
195
|
+
|
196
|
+
say_status '', '-------------------------'
|
197
|
+
say_status '', 'Your project was created!'
|
198
|
+
end
|
199
|
+
|
200
|
+
# --- optimise -----------------------------------------------------------
|
201
|
+
|
202
|
+
desc 'optimise PATHS', 'Optimises PNG images at the given PATHS'
|
203
|
+
|
204
|
+
long_desc <<-DESC
|
205
|
+
Given a path to an image (or multiple images), runs Polymer's optimisers
|
206
|
+
on the image. Requires that the paths be images to PNG files. Image
|
207
|
+
paths are relative to the current working directory.
|
208
|
+
DESC
|
209
|
+
|
210
|
+
map 'optimize' => :optimise
|
211
|
+
|
212
|
+
def optimise(*paths)
|
213
|
+
dir = Pathname.new(Dir.pwd)
|
214
|
+
paths = paths.map { |path| dir + path }
|
215
|
+
|
216
|
+
paths.each do |path|
|
217
|
+
fpath = path.relative_path_from(dir).to_s
|
218
|
+
|
219
|
+
# Ensure the file is a PNG.
|
220
|
+
unless path.to_s =~ /\.png/
|
221
|
+
say_status 'skipped', "#{fpath} - not a PNG", :yellow
|
222
|
+
next
|
223
|
+
end
|
224
|
+
|
225
|
+
before = path.size
|
226
|
+
say " optimising #{fpath} "
|
227
|
+
reduction = Polymer::Optimisation.optimise_file(path)
|
228
|
+
|
229
|
+
if reduction > 0
|
230
|
+
saved = '- saved %.2fkb (%.1f' %
|
231
|
+
[reduction.to_f / 1024, (reduction.to_f / before) * 100]
|
232
|
+
say_status "\r\e[0K optimised", "#{fpath} #{saved}%)", :green
|
233
|
+
else
|
234
|
+
say_status "\r\e[0K optimised", "#{fpath} - no savings", :green
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# --- position -----------------------------------------------------------
|
240
|
+
|
241
|
+
desc 'position SOURCE', 'Shows the position of a source within a sprite'
|
242
|
+
|
243
|
+
long_desc <<-DESC
|
244
|
+
The position task shows you the position of a source image within a
|
245
|
+
sprite and also shows the appropriate CSS statement for the source
|
246
|
+
should you wish to create your own CSS files.
|
247
|
+
|
248
|
+
You may supply the name of a source image; if a source image with the
|
249
|
+
same name exists in multiple sprites, the positions of each of them will
|
250
|
+
be shown to you. If you want a particular source, you may instead
|
251
|
+
provide a "sprite/source" pair.
|
252
|
+
DESC
|
253
|
+
|
254
|
+
def position(source)
|
255
|
+
project = find_project!
|
256
|
+
|
257
|
+
if source.index('/')
|
258
|
+
# Full sprite/source pair given.
|
259
|
+
sprite, source = source.split('/', 2)
|
260
|
+
|
261
|
+
if project.sprite(sprite)
|
262
|
+
sprites = [project.sprite(sprite)]
|
263
|
+
else
|
264
|
+
say "No such sprite: #{sprite}", :red
|
265
|
+
exit 1
|
266
|
+
end
|
267
|
+
else
|
268
|
+
# Only a source name was given.
|
269
|
+
sprites = project.sprites
|
270
|
+
end
|
271
|
+
|
272
|
+
# Remove sprites which don't have a matching source.
|
273
|
+
sprites.reject! { |sprite| not sprite.source(source) }
|
274
|
+
say("No such source: #{source}") && exit(1) if sprites.empty?
|
275
|
+
|
276
|
+
say ""
|
277
|
+
|
278
|
+
sprites.each do |sprite|
|
279
|
+
say "#{sprite.name}/#{source}: #{sprite.position_of(source)}px", :green
|
280
|
+
say " #{Polymer::CSSGenerator.background_statement(sprite, source)}"
|
281
|
+
say " - or -"
|
282
|
+
say " #{Polymer::CSSGenerator.position_statement(sprite, source)}"
|
283
|
+
say ""
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# --- version ------------------------------------------------------------
|
288
|
+
|
289
|
+
desc 'version', "Shows the version of Polymer you're using"
|
290
|
+
map '--version' => :version
|
291
|
+
|
292
|
+
def version
|
293
|
+
say "Polymer #{Polymer::VERSION}"
|
294
|
+
end
|
295
|
+
|
296
|
+
private # ----------------------------------------------------------------
|
297
|
+
|
298
|
+
# Returns the Project for the current directory. Exits with a message if
|
299
|
+
# no project could be found.
|
300
|
+
#
|
301
|
+
# @return [Polymer::Project]
|
302
|
+
#
|
303
|
+
def find_project!
|
304
|
+
Polymer::DSL.load Polymer::Project.find_config(Dir.pwd)
|
305
|
+
rescue Polymer::MissingProject
|
306
|
+
say <<-ERROR.compress_lines, :red
|
307
|
+
Couldn't find a Polymer project in the current directory, or any of
|
308
|
+
the parent directories. Run "polymer init" if you want to create a new
|
309
|
+
project here.
|
310
|
+
ERROR
|
311
|
+
exit 1
|
312
|
+
end
|
313
|
+
|
314
|
+
# Returns if the current machine has groff available.
|
315
|
+
#
|
316
|
+
# @return [Boolean]
|
317
|
+
#
|
318
|
+
def groff_available?
|
319
|
+
require 'rbconfig'
|
320
|
+
|
321
|
+
if RbConfig::CONFIG["host_os"] =~ /(msdos|mswin|djgpp|mingw)/
|
322
|
+
`which groff 2>NUL`
|
323
|
+
else
|
324
|
+
`which groff 2>/dev/null`
|
325
|
+
end
|
326
|
+
|
327
|
+
$? == 0
|
328
|
+
end
|
329
|
+
|
330
|
+
def self.source_root
|
331
|
+
File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
|
332
|
+
end
|
333
|
+
|
334
|
+
# Temporary -- until the next Thor release.
|
335
|
+
def self.banner(task, namespace = nil, subcommand = false)
|
336
|
+
super.gsub(/^.*polymer/, 'polymer')
|
337
|
+
end
|
338
|
+
|
339
|
+
end # CLI
|
340
|
+
end # Polymer
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# Some simple string extensions to make things easier.
|
2
|
+
class String
|
3
|
+
|
4
|
+
# Replace sequences of whitespace (including newlines) with either
|
5
|
+
# a single space or remove them entirely (according to param _spaced_)
|
6
|
+
#
|
7
|
+
# <<QUERY.compress_lines
|
8
|
+
# SELECT name
|
9
|
+
# FROM users
|
10
|
+
# QUERY => "SELECT name FROM users"
|
11
|
+
#
|
12
|
+
# @return [String] Receiver with whitespace (including newlines) replaced
|
13
|
+
#
|
14
|
+
def compress_lines
|
15
|
+
split($/).map { |line| line.strip }.join(' ')
|
16
|
+
end
|
17
|
+
|
18
|
+
# Removes leading whitespace from each line, such as might be added when
|
19
|
+
# using a HEREDOC string.
|
20
|
+
#
|
21
|
+
# @return [String] Receiver with leading whitespace removed.
|
22
|
+
#
|
23
|
+
def unindent
|
24
|
+
(other = dup) and other.unindent! and other
|
25
|
+
end
|
26
|
+
|
27
|
+
# Bang version of #unindent.
|
28
|
+
#
|
29
|
+
# @return [String] Receiver with leading whitespace removed.
|
30
|
+
#
|
31
|
+
def unindent!
|
32
|
+
gsub!(/^[ \t]{#{minimum_leading_whitespace}}/, '')
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Checks each line and determines the minimum amount of leading whitespace.
|
38
|
+
#
|
39
|
+
# @return [Integer] The number of leading whitespace characters.
|
40
|
+
#
|
41
|
+
def minimum_leading_whitespace
|
42
|
+
whitespace = split("\n", -1).inject(0) do |indent, line|
|
43
|
+
if line.strip.empty?
|
44
|
+
indent # Ignore completely blank lines.
|
45
|
+
elsif line =~ /^(\s+)/
|
46
|
+
(1.0 / $1.length) > indent ? 1.0 / $1.length : indent
|
47
|
+
else
|
48
|
+
1.0
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
whitespace == 1.0 ? 0 : (1.0 / whitespace).to_i
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
# String#compress_lines is extracted from the extlib gem
|
58
|
+
# ------------------------------------------------------
|
59
|
+
#
|
60
|
+
# Copyright (c) 2009 Dan Kubb
|
61
|
+
#
|
62
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
63
|
+
# of this software and associated documentation files (the "Software"), to
|
64
|
+
# deal in the Software without restriction, including without limitation the
|
65
|
+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
66
|
+
# sell copies of the Software, and to permit persons to whom the Software is
|
67
|
+
# furnished to do so, subject to the following conditions:
|
68
|
+
#
|
69
|
+
# The above copyright notice and this permission notice shall be included in
|
70
|
+
# all copies or substantial portions of the Software.
|
71
|
+
#
|
72
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
73
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
74
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
75
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
76
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
77
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
78
|
+
# IN THE SOFTWARE.
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Polymer
|
2
|
+
class CSSGenerator
|
3
|
+
|
4
|
+
# --- Class Methods ------------------------------------------------------
|
5
|
+
|
6
|
+
# Returns a string which may be used as the background statement for the
|
7
|
+
# given sprite and source pair.
|
8
|
+
#
|
9
|
+
# @param [Polymer::Sprite] sprite
|
10
|
+
# @param [Polymer::Source] source
|
11
|
+
#
|
12
|
+
# @return [String]
|
13
|
+
#
|
14
|
+
def self.background_statement(sprite, source)
|
15
|
+
"background: url(#{sprite.url}) 0 " \
|
16
|
+
"#{-sprite.position_of(source)}px no-repeat;"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns a string which may be used as the background-position statement
|
20
|
+
# for the given sprite and source pair.
|
21
|
+
#
|
22
|
+
# @param [Polymer::Sprite] sprite
|
23
|
+
# @param [Polymer::Source] source
|
24
|
+
#
|
25
|
+
# @return [String]
|
26
|
+
#
|
27
|
+
def self.position_statement(sprite, source)
|
28
|
+
"background-position: 0 #{-sprite.position_of(source)}px;"
|
29
|
+
end
|
30
|
+
|
31
|
+
end # CSSGenerator
|
32
|
+
end # Polymer
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Polymer
|
2
|
+
# Given a Sprite, DeviantFinder checks to see if the sprite has any sources
|
3
|
+
# which are significantly wider than the average.
|
4
|
+
class DeviantFinder
|
5
|
+
|
6
|
+
# Given a Sprite, checks to see if the sprite has any sources which are
|
7
|
+
# significantly wider than the average. Images with a small standard
|
8
|
+
# deviation in source width or fewer than 3 sources will be skipped.
|
9
|
+
#
|
10
|
+
# @param [Polymer::Sprite] sprite
|
11
|
+
# The sprite whose source widths are to be checked.
|
12
|
+
#
|
13
|
+
# @return [Array<String>]
|
14
|
+
# Returns an array of source images whose width is greater than the
|
15
|
+
# standard deviation.
|
16
|
+
# @return [nil]
|
17
|
+
# Returns nil if all of the source images are an approriate width.
|
18
|
+
#
|
19
|
+
def self.find_deviants(sprite)
|
20
|
+
# Need more than two sources to find deviants.
|
21
|
+
return false if sprite.sources.size < 2
|
22
|
+
|
23
|
+
mean, std_dev = standard_deviation(sprite.sources.map do |source|
|
24
|
+
source.image.columns
|
25
|
+
end)
|
26
|
+
|
27
|
+
return false if std_dev < 100 # Skip images with a < 100px deviation.
|
28
|
+
|
29
|
+
deviants = sprite.sources.select do |source|
|
30
|
+
width = source.image.columns
|
31
|
+
width > mean + std_dev || width < mean - std_dev
|
32
|
+
end
|
33
|
+
|
34
|
+
deviants.any? and deviants
|
35
|
+
end
|
36
|
+
|
37
|
+
# Print a warning if the sprite contains wide sources.
|
38
|
+
def self.format_ui_message(sprite, deviants)
|
39
|
+
if deviants
|
40
|
+
<<-MESSAGE.compress_lines
|
41
|
+
Your "#{sprite.name}" sprite contains one or more source images
|
42
|
+
which deviate significantly from the average source width. You might
|
43
|
+
want to consider removing these sources from the sprite in order to
|
44
|
+
reduce the sprite filesize.
|
45
|
+
|
46
|
+
Wide sources: #{deviants.map(&:name).join(', ')}
|
47
|
+
MESSAGE
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private # ----------------------------------------------------------------
|
52
|
+
|
53
|
+
# Knuth.
|
54
|
+
#
|
55
|
+
# @param [Array<Integer>] data
|
56
|
+
# An array containing the widths of each source image.
|
57
|
+
#
|
58
|
+
# @return [Array<Integer, Integer>]
|
59
|
+
# Returns a two-element array whose first element is the mean width of
|
60
|
+
# the source images; the second element is the standard deviation.
|
61
|
+
#
|
62
|
+
def self.standard_deviation(data)
|
63
|
+
n, mean, m2 = 0, 0, 0
|
64
|
+
|
65
|
+
data.each do |x|
|
66
|
+
n = n + 1
|
67
|
+
delta = x - mean
|
68
|
+
mean = mean + delta / n
|
69
|
+
m2 = m2 + delta * (x - mean)
|
70
|
+
end
|
71
|
+
|
72
|
+
[ mean, Math.sqrt(m2 / (n - 1)) ]
|
73
|
+
end
|
74
|
+
|
75
|
+
end # DeviantFinder
|
76
|
+
end # Polymer
|