active_assets 0.2.3 → 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +20 -21
- data/active_assets.gemspec +4 -3
- data/lib/active_assets/active_sprites.rb +6 -2
- data/lib/active_assets/active_sprites/configurable.rb +0 -1
- data/lib/active_assets/active_sprites/runners/abstract_runner.rb +124 -0
- data/lib/active_assets/active_sprites/runners/chunky_png_runner.rb +62 -0
- data/lib/active_assets/active_sprites/runners/mini_magick_runner.rb +74 -0
- data/lib/active_assets/active_sprites/runners/rmagick_runner.rb +66 -0
- data/lib/active_assets/active_sprites/sprite_piece.rb +5 -1
- data/lib/active_assets/active_sprites/sprite_stylesheet.rb +3 -4
- data/lib/active_assets/active_sprites/sprites.rb +31 -6
- data/lib/tasks/active_sprites/sprites.rake +1 -0
- data/test/active_assets/active_sprites/runners/chunky_png_runner_test.rb +10 -0
- data/test/active_assets/active_sprites/runners/mini_magick_runner_test.rb +10 -0
- data/test/active_assets/active_sprites/runners/rmagick_runner_test.rb +10 -0
- data/test/fixtures/rails_root/config/application.rb +1 -1
- data/test/helper.rb +0 -8
- data/test/{active_assets/active_sprites/runner_test.rb → support/abstract_runner.rb} +16 -29
- metadata +39 -21
- data/lib/active_assets/active_sprites/chunky_png_runner.rb +0 -119
- data/lib/active_assets/active_sprites/rmagick_runner.rb +0 -124
- data/test/raster_graphics.rb +0 -623
- data/test/raster_graphics_test.rb +0 -82
@@ -1,124 +0,0 @@
|
|
1
|
-
require 'action_controller'
|
2
|
-
require 'action_view'
|
3
|
-
require 'rack/mount'
|
4
|
-
require 'action_view'
|
5
|
-
require 'rmagick'
|
6
|
-
require 'fileutils'
|
7
|
-
|
8
|
-
module ActiveAssets
|
9
|
-
module ActiveSprites
|
10
|
-
class RmagickRunner
|
11
|
-
class AssetContext < ActionView::Base
|
12
|
-
end
|
13
|
-
|
14
|
-
include Magick
|
15
|
-
|
16
|
-
DEFAULT_SPRITE = Image.new(0,0).freeze
|
17
|
-
|
18
|
-
def initialize(sprites)
|
19
|
-
@sprites = if ENV['SPRITE']
|
20
|
-
sprites.select do |name, sprite|
|
21
|
-
ENV['SPRITE'].split(',').map(&:strip).any? do |sp|
|
22
|
-
# were going to be very forgiving
|
23
|
-
name == sp ||
|
24
|
-
name == sp.to_sym ||
|
25
|
-
name == ::Rack::Mount::Utils.normalize_path(sp)
|
26
|
-
end
|
27
|
-
end.map(&:last)
|
28
|
-
else
|
29
|
-
sprites.values
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def generate!(railtie = Rails.application, debug = ENV['DEBUG'])
|
34
|
-
p "Engine Class Name: #{railtie.class.name}" if debug
|
35
|
-
|
36
|
-
context = setup_context(railtie)
|
37
|
-
|
38
|
-
@sprites.each do |sprite|
|
39
|
-
next if sprite.sprite_pieces.empty?
|
40
|
-
sprite_path = sanitize_asset_path(context.image_path(sprite.path))
|
41
|
-
p "Sprite Path: #{sprite_path}" if debug
|
42
|
-
sprite_stylesheet_path = sanitize_asset_path(context.stylesheet_path(sprite.stylesheet_path))
|
43
|
-
p "Sprite Stylesheet Path: #{sprite_stylesheet_path}" if debug
|
44
|
-
|
45
|
-
orientation = sprite.orientation.to_s
|
46
|
-
sprite_pieces = sprite.sprite_pieces
|
47
|
-
|
48
|
-
begin
|
49
|
-
sprite_piece_paths = sprite_pieces.map do |sp|
|
50
|
-
File.join(railtie.config.paths.public.to_a.first, sanitize_asset_path(context.image_path(sp.path)))
|
51
|
-
end
|
52
|
-
image_list = ImageList.new(*sprite_piece_paths)
|
53
|
-
|
54
|
-
offset = 0
|
55
|
-
|
56
|
-
image_list.each_with_index do |image, i|
|
57
|
-
sprite_pieces[i].details = SpritePiece::Details.new(
|
58
|
-
sprite.url.present? ? sprite.url : sprite_path,
|
59
|
-
orientation == Sprite::Orientation::VERTICAL ? 0 : offset,
|
60
|
-
orientation == Sprite::Orientation::VERTICAL ? offset : 0,
|
61
|
-
image.columns,
|
62
|
-
image.rows
|
63
|
-
)
|
64
|
-
offset += orientation == Sprite::Orientation::VERTICAL ? image.rows : image.columns
|
65
|
-
end
|
66
|
-
|
67
|
-
@sprite = image_list.montage do
|
68
|
-
self.tile = orientation == Sprite::Orientation::VERTICAL ? "1x#{sprite_pieces.size}" : "#{sprite_pieces.size}x1"
|
69
|
-
self.geometry = "+0+0"
|
70
|
-
self.background_color = 'transparent'
|
71
|
-
self.matte_color = sprite.matte_color || '#bdbdbd'
|
72
|
-
end
|
73
|
-
|
74
|
-
@sprite.strip!
|
75
|
-
|
76
|
-
stylesheet = SpriteStylesheet.new(sprite_pieces)
|
77
|
-
stylesheet.write File.join(railtie.config.paths.public.to_a.first, sprite_stylesheet_path)
|
78
|
-
write File.join(railtie.config.paths.public.to_a.first, sprite_path), sprite.quality
|
79
|
-
ensure
|
80
|
-
finish
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
private
|
86
|
-
def write(path, quality = nil)
|
87
|
-
FileUtils.mkdir_p(File.dirname(path))
|
88
|
-
@sprite.write("#{File.extname(path)[1..-1]}:#{path}") do
|
89
|
-
self.quality = quality || 75
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
def finish
|
94
|
-
@sprite.destroy! unless @sprite == DEFAULT_SPRITE
|
95
|
-
@sprite = DEFAULT_SPRITE
|
96
|
-
end
|
97
|
-
|
98
|
-
def sanitize_asset_path(path)
|
99
|
-
path.split('?').first
|
100
|
-
end
|
101
|
-
|
102
|
-
def setup_context(railtie)
|
103
|
-
unless railtie.config.respond_to?(:action_controller)
|
104
|
-
railtie.config.action_controller = ActiveSupport::OrderedOptions.new
|
105
|
-
|
106
|
-
paths = railtie.config.paths
|
107
|
-
options = railtie.config.action_controller
|
108
|
-
|
109
|
-
options.assets_dir ||= paths.public.to_a.first
|
110
|
-
options.javascripts_dir ||= paths.public.javascripts.to_a.first
|
111
|
-
options.stylesheets_dir ||= paths.public.stylesheets.to_a.first
|
112
|
-
|
113
|
-
ActiveSupport.on_load(:action_controller) do
|
114
|
-
options.each { |k,v| send("#{k}=", v) }
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
controller = ActionController::Base.new
|
119
|
-
AssetContext.new(railtie.config.action_controller, {}, controller)
|
120
|
-
end
|
121
|
-
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
data/test/raster_graphics.rb
DELETED
@@ -1,623 +0,0 @@
|
|
1
|
-
###########################################################################
|
2
|
-
# Represents an RGB[http://en.wikipedia.org/wiki/Rgb] colour.
|
3
|
-
class RGBColour
|
4
|
-
# Red, green and blue values must fall in the range 0..255.
|
5
|
-
def initialize(red, green, blue)
|
6
|
-
ok = [red, green, blue].inject(true) {|ok,c| ok &= c.between?(0,255)}
|
7
|
-
unless ok
|
8
|
-
raise ArgumentError, "invalid RGB parameters: #{[red, green, blue].inspect}"
|
9
|
-
end
|
10
|
-
@red, @green, @blue = red, green, blue
|
11
|
-
end
|
12
|
-
attr_reader :red, :green, :blue
|
13
|
-
alias_method :r, :red
|
14
|
-
alias_method :g, :green
|
15
|
-
alias_method :b, :blue
|
16
|
-
|
17
|
-
# the difference between two colours
|
18
|
-
def -(a_colour)
|
19
|
-
(@red - a_colour.red).abs +
|
20
|
-
(@green - a_colour.green).abs +
|
21
|
-
(@blue - a_colour.blue).abs
|
22
|
-
end
|
23
|
-
|
24
|
-
# Return the list of [red, green, blue] values.
|
25
|
-
# RGBColour.new(100,150,200).values # => [100, 150, 200]
|
26
|
-
# call-seq:
|
27
|
-
# values -> array
|
28
|
-
#
|
29
|
-
def values
|
30
|
-
[@red, @green, @blue]
|
31
|
-
end
|
32
|
-
|
33
|
-
# Equality test: two RGBColour objects are equal if they have the same
|
34
|
-
# red, green and blue values.
|
35
|
-
# call-seq:
|
36
|
-
# ==(a_colour) -> true or false
|
37
|
-
#
|
38
|
-
def ==(a_colour)
|
39
|
-
values == a_colour.values
|
40
|
-
end
|
41
|
-
|
42
|
-
# Comparison test: compares two RGBColour objects based on their #luminosity value
|
43
|
-
# call-seq:
|
44
|
-
# <=>(a_colour) -> -1, 0, +1
|
45
|
-
#
|
46
|
-
def <=>(a_colour)
|
47
|
-
self.luminosity <=> a_colour.luminosity
|
48
|
-
end
|
49
|
-
|
50
|
-
# Calculate a integer luminosity value, in the range 0..255
|
51
|
-
# RGBColour.new(100,150,200).luminosity # => 142
|
52
|
-
# call-seq:
|
53
|
-
# luminosity -> int
|
54
|
-
#
|
55
|
-
def luminosity
|
56
|
-
Integer(0.2126*@red + 0.7152*@green + 0.0722*@blue)
|
57
|
-
end
|
58
|
-
|
59
|
-
# Return a new RGBColour value where all the red, green, blue values are the
|
60
|
-
# #luminosity value.
|
61
|
-
# RGBColour.new(100,150,200).to_grayscale.values # => [142, 142, 142]
|
62
|
-
# call-seq:
|
63
|
-
# to_grayscale -> a_colour
|
64
|
-
#
|
65
|
-
def to_grayscale
|
66
|
-
l = luminosity
|
67
|
-
self.class.new(l, l, l)
|
68
|
-
end
|
69
|
-
|
70
|
-
# Return a new RGBColour object given an iteration value for the Pixmap.mandelbrot
|
71
|
-
# method.
|
72
|
-
def self.mandel_colour(i)
|
73
|
-
self.new( 16*(i % 15), 32*(i % 7), 8*(i % 31) )
|
74
|
-
end
|
75
|
-
|
76
|
-
RED = RGBColour.new(255,0,0)
|
77
|
-
GREEN = RGBColour.new(0,255,0)
|
78
|
-
BLUE = RGBColour.new(0,0,255)
|
79
|
-
YELLOW= RGBColour.new(255,255,0)
|
80
|
-
BLACK = RGBColour.new(0,0,0)
|
81
|
-
WHITE = RGBColour.new(255,255,255)
|
82
|
-
end
|
83
|
-
|
84
|
-
###########################################################################
|
85
|
-
# A Pixel represents an (x,y) point in a Pixmap.
|
86
|
-
Pixel = Struct.new(:x, :y)
|
87
|
-
|
88
|
-
###########################################################################
|
89
|
-
class Pixmap
|
90
|
-
def initialize(width, height)
|
91
|
-
@width = width
|
92
|
-
@height = height
|
93
|
-
@data = fill(RGBColour::WHITE)
|
94
|
-
end
|
95
|
-
attr_reader :width, :height
|
96
|
-
|
97
|
-
def fill(colour)
|
98
|
-
@data = Array.new(@width) {Array.new(@height, colour)}
|
99
|
-
end
|
100
|
-
|
101
|
-
def -(a_pixmap)
|
102
|
-
if @width != a_pixmap.width or @height != a_pixmap.height
|
103
|
-
raise ArgumentError, "can't compare images with different sizes"
|
104
|
-
end
|
105
|
-
sum = 0
|
106
|
-
each_pixel {|x,y| sum += self[x,y] - a_pixmap[x,y]}
|
107
|
-
Float(sum) / (@width * @height * 255 * 3)
|
108
|
-
end
|
109
|
-
|
110
|
-
def validate_pixel(x,y)
|
111
|
-
unless x.between?(0, @width-1) and y.between?(0, @height-1)
|
112
|
-
raise ArgumentError, "requested pixel (#{x}, #{y}) is outside dimensions of this bitmap"
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
###############################################
|
117
|
-
def [](x,y)
|
118
|
-
validate_pixel(x,y)
|
119
|
-
@data[x][y]
|
120
|
-
end
|
121
|
-
alias_method :get_pixel, :[]
|
122
|
-
|
123
|
-
def []=(x,y,colour)
|
124
|
-
validate_pixel(x,y)
|
125
|
-
@data[x][y] = colour
|
126
|
-
end
|
127
|
-
alias_method :set_pixel, :[]=
|
128
|
-
|
129
|
-
def each_pixel
|
130
|
-
if block_given?
|
131
|
-
@height.times {|y| @width.times {|x| yield x,y}}
|
132
|
-
else
|
133
|
-
to_enum(:each_pixel)
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
###############################################
|
138
|
-
# write to file/stream
|
139
|
-
PIXMAP_FORMATS = ["P3", "P6"] # implemented output formats
|
140
|
-
PIXMAP_BINARY_FORMATS = ["P6"] # implemented output formats which are binary
|
141
|
-
|
142
|
-
def write_ppm(ios, format="P6")
|
143
|
-
if not PIXMAP_FORMATS.include?(format)
|
144
|
-
raise NotImplementedError, "pixmap format #{format} has not been implemented"
|
145
|
-
end
|
146
|
-
ios.puts format, "#{@width} #{@height}", "255"
|
147
|
-
ios.binmode if PIXMAP_BINARY_FORMATS.include?(format)
|
148
|
-
@height.times do |y|
|
149
|
-
@width.times do |x|
|
150
|
-
case format
|
151
|
-
when "P3" then ios.print @data[x][y].values.join(" "),"\n"
|
152
|
-
when "P6" then ios.print @data[x][y].values.pack('C3')
|
153
|
-
end
|
154
|
-
end
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
def save(filename, opts={:format=>"P6"})
|
159
|
-
File.open(filename, 'w') do |f|
|
160
|
-
write_ppm(f, opts[:format])
|
161
|
-
end
|
162
|
-
end
|
163
|
-
alias_method :write, :save
|
164
|
-
|
165
|
-
def print(opts={:format=>"P6"})
|
166
|
-
write_ppm($stdout, opts[:format])
|
167
|
-
end
|
168
|
-
|
169
|
-
def save_as_jpeg(filename, quality=75)
|
170
|
-
# using the ImageMagick convert tool
|
171
|
-
begin
|
172
|
-
pipe = IO.popen("convert ppm:- -quality #{quality} jpg:#{filename}", 'w')
|
173
|
-
write_ppm(pipe)
|
174
|
-
rescue SystemCallError => e
|
175
|
-
warn "problem writing data to 'convert' utility -- does it exist in your $PATH?"
|
176
|
-
ensure
|
177
|
-
pipe.close rescue false
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
###############################################
|
182
|
-
# read from file/pipe
|
183
|
-
def self.read_ppm(ios)
|
184
|
-
format = ios.gets.chomp
|
185
|
-
width, height = ios.gets.chomp.split.map {|n| n.to_i }
|
186
|
-
max_colour = ios.gets.chomp
|
187
|
-
|
188
|
-
if (not PIXMAP_FORMATS.include?(format)) or
|
189
|
-
width < 1 or height < 1 or
|
190
|
-
max_colour != '255'
|
191
|
-
then
|
192
|
-
ios.close
|
193
|
-
raise StandardError, "file '#{filename}' does not start with the expected header"
|
194
|
-
end
|
195
|
-
ios.binmode if PIXMAP_BINARY_FORMATS.include?(format)
|
196
|
-
|
197
|
-
bitmap = self.new(width, height)
|
198
|
-
height.times do |y|
|
199
|
-
width.times do |x|
|
200
|
-
# read 3 bytes
|
201
|
-
red, green, blue = case format
|
202
|
-
when 'P3' then ios.gets.chomp.split
|
203
|
-
when 'P6' then ios.read(3).unpack('C3')
|
204
|
-
end
|
205
|
-
bitmap[x,y] = RGBColour.new(red, green, blue)
|
206
|
-
end
|
207
|
-
end
|
208
|
-
ios.close
|
209
|
-
bitmap
|
210
|
-
end
|
211
|
-
|
212
|
-
def self.open(filename)
|
213
|
-
read_ppm(File.open(filename, 'r'))
|
214
|
-
end
|
215
|
-
|
216
|
-
def self.open_from_jpeg(filename)
|
217
|
-
unless File.readable?(filename)
|
218
|
-
raise ArgumentError, "#{filename} does not exists or is not readable."
|
219
|
-
end
|
220
|
-
begin
|
221
|
-
pipe = IO.popen("convert jpg:#{filename} ppm:-", 'r')
|
222
|
-
read_ppm(pipe)
|
223
|
-
rescue SystemCallError => e
|
224
|
-
warn "problem reading data from 'convert' utility -- does it exist in your $PATH?"
|
225
|
-
ensure
|
226
|
-
pipe.close rescue false
|
227
|
-
end
|
228
|
-
end
|
229
|
-
|
230
|
-
###############################################
|
231
|
-
# conversion methods
|
232
|
-
def to_grayscale
|
233
|
-
gray = self.class.new(@width, @height)
|
234
|
-
@width.times do |x|
|
235
|
-
@height.times do |y|
|
236
|
-
gray[x,y] = self[x,y].to_grayscale
|
237
|
-
end
|
238
|
-
end
|
239
|
-
gray
|
240
|
-
end
|
241
|
-
|
242
|
-
###############################################
|
243
|
-
def draw_line(p1, p2, colour)
|
244
|
-
validate_pixel(p1.x, p2.y)
|
245
|
-
validate_pixel(p2.x, p2.y)
|
246
|
-
|
247
|
-
x1, y1 = p1.x, p1.y
|
248
|
-
x2, y2 = p2.x, p2.y
|
249
|
-
|
250
|
-
steep = (y2 - y1).abs > (x2 - x1).abs
|
251
|
-
if steep
|
252
|
-
x1, y1 = y1, x1
|
253
|
-
x2, y2 = y2, x2
|
254
|
-
end
|
255
|
-
if x1 > x2
|
256
|
-
x1, x2 = x2, x1
|
257
|
-
y1, y2 = y2, y1
|
258
|
-
end
|
259
|
-
|
260
|
-
deltax = x2 - x1
|
261
|
-
deltay = (y2 - y1).abs
|
262
|
-
error = deltax / 2
|
263
|
-
ystep = y1 < y2 ? 1 : -1
|
264
|
-
|
265
|
-
y = y1
|
266
|
-
x1.upto(x2) do |x|
|
267
|
-
pixel = steep ? [y,x] : [x,y]
|
268
|
-
self[*pixel] = colour
|
269
|
-
error -= deltay
|
270
|
-
if error < 0
|
271
|
-
y += ystep
|
272
|
-
error += deltax
|
273
|
-
end
|
274
|
-
end
|
275
|
-
end
|
276
|
-
|
277
|
-
###############################################
|
278
|
-
def draw_line_antialised(p1, p2, colour)
|
279
|
-
x1, y1 = p1.x, p1.y
|
280
|
-
x2, y2 = p2.x, p2.y
|
281
|
-
|
282
|
-
steep = (y2 - y1).abs > (x2 - x1).abs
|
283
|
-
if steep
|
284
|
-
x1, y1 = y1, x1
|
285
|
-
x2, y2 = y2, x2
|
286
|
-
end
|
287
|
-
if x1 > x2
|
288
|
-
x1, x2 = x2, x1
|
289
|
-
y1, y2 = y2, y1
|
290
|
-
end
|
291
|
-
deltax = x2 - x1
|
292
|
-
deltay = (y2 - y1).abs
|
293
|
-
gradient = 1.0 * deltay / deltax
|
294
|
-
|
295
|
-
# handle the first endpoint
|
296
|
-
xend = x1.round
|
297
|
-
yend = y1 + gradient * (xend - x1)
|
298
|
-
xgap = (x1 + 0.5).rfpart
|
299
|
-
xpxl1 = xend
|
300
|
-
ypxl1 = yend.truncate
|
301
|
-
put_colour(xpxl1, ypxl1, colour, steep, yend.rfpart * xgap)
|
302
|
-
put_colour(xpxl1, ypxl1 + 1, colour, steep, yend.fpart * xgap)
|
303
|
-
itery = yend + gradient
|
304
|
-
|
305
|
-
# handle the second endpoint
|
306
|
-
xend = x2.round
|
307
|
-
yend = y2 + gradient * (xend - x2)
|
308
|
-
xgap = (x2 + 0.5).rfpart
|
309
|
-
xpxl2 = xend
|
310
|
-
ypxl2 = yend.truncate
|
311
|
-
put_colour(xpxl2, ypxl2, colour, steep, yend.rfpart * xgap)
|
312
|
-
put_colour(xpxl2, ypxl2 + 1, colour, steep, yend.fpart * xgap)
|
313
|
-
|
314
|
-
# in between
|
315
|
-
(xpxl1 + 1).upto(xpxl2 - 1).each do |x|
|
316
|
-
put_colour(x, itery.truncate, colour, steep, itery.rfpart)
|
317
|
-
put_colour(x, itery.truncate + 1, colour, steep, itery.fpart)
|
318
|
-
itery = itery + gradient
|
319
|
-
end
|
320
|
-
end
|
321
|
-
|
322
|
-
def put_colour(x, y, colour, steep, c)
|
323
|
-
x, y = y, x if steep
|
324
|
-
self[x, y] = anti_alias(colour, self[x, y], c)
|
325
|
-
end
|
326
|
-
|
327
|
-
def anti_alias(new, old, ratio)
|
328
|
-
blended = new.values.zip(old.values).map {|n, o| (n*ratio + o*(1.0 - ratio)).round}
|
329
|
-
RGBColour.new(*blended)
|
330
|
-
end
|
331
|
-
|
332
|
-
###############################################
|
333
|
-
def draw_circle(pixel, radius, colour)
|
334
|
-
validate_pixel(pixel.x, pixel.y)
|
335
|
-
|
336
|
-
self[pixel.x, pixel.y + radius] = colour
|
337
|
-
self[pixel.x, pixel.y - radius] = colour
|
338
|
-
self[pixel.x + radius, pixel.y] = colour
|
339
|
-
self[pixel.x - radius, pixel.y] = colour
|
340
|
-
|
341
|
-
f = 1 - radius
|
342
|
-
ddF_x = 1
|
343
|
-
ddF_y = -2 * radius
|
344
|
-
x = 0
|
345
|
-
y = radius
|
346
|
-
while x < y
|
347
|
-
if f >= 0
|
348
|
-
y -= 1
|
349
|
-
ddF_y += 2
|
350
|
-
f += ddF_y
|
351
|
-
end
|
352
|
-
x += 1
|
353
|
-
ddF_x += 2
|
354
|
-
f += ddF_x
|
355
|
-
self[pixel.x + x, pixel.y + y] = colour
|
356
|
-
self[pixel.x + x, pixel.y - y] = colour
|
357
|
-
self[pixel.x - x, pixel.y + y] = colour
|
358
|
-
self[pixel.x - x, pixel.y - y] = colour
|
359
|
-
self[pixel.x + y, pixel.y + x] = colour
|
360
|
-
self[pixel.x + y, pixel.y - x] = colour
|
361
|
-
self[pixel.x - y, pixel.y + x] = colour
|
362
|
-
self[pixel.x - y, pixel.y - x] = colour
|
363
|
-
end
|
364
|
-
end
|
365
|
-
|
366
|
-
###############################################
|
367
|
-
def flood_fill(pixel, new_colour)
|
368
|
-
current_colour = self[pixel.x, pixel.y]
|
369
|
-
queue = RasterQueue.new
|
370
|
-
queue.enqueue(pixel)
|
371
|
-
until queue.empty?
|
372
|
-
p = queue.dequeue
|
373
|
-
if self[p.x, p.y] == current_colour
|
374
|
-
west = find_border(p, current_colour, :west)
|
375
|
-
east = find_border(p, current_colour, :east)
|
376
|
-
draw_line(west, east, new_colour)
|
377
|
-
q = west
|
378
|
-
while q.x <= east.x
|
379
|
-
[:north, :south].each do |direction|
|
380
|
-
n = neighbour(q, direction)
|
381
|
-
queue.enqueue(n) if self[n.x, n.y] == current_colour
|
382
|
-
end
|
383
|
-
q = neighbour(q, :east)
|
384
|
-
end
|
385
|
-
end
|
386
|
-
end
|
387
|
-
end
|
388
|
-
|
389
|
-
def neighbour(pixel, direction)
|
390
|
-
case direction
|
391
|
-
when :north then Pixel[pixel.x, pixel.y - 1]
|
392
|
-
when :south then Pixel[pixel.x, pixel.y + 1]
|
393
|
-
when :east then Pixel[pixel.x + 1, pixel.y]
|
394
|
-
when :west then Pixel[pixel.x - 1, pixel.y]
|
395
|
-
end
|
396
|
-
end
|
397
|
-
|
398
|
-
def find_border(pixel, colour, direction)
|
399
|
-
nextp = neighbour(pixel, direction)
|
400
|
-
while self[nextp.x, nextp.y] == colour
|
401
|
-
pixel = nextp
|
402
|
-
nextp = neighbour(pixel, direction)
|
403
|
-
end
|
404
|
-
pixel
|
405
|
-
end
|
406
|
-
|
407
|
-
###############################################
|
408
|
-
def median_filter(radius=3)
|
409
|
-
if radius.even?
|
410
|
-
radius += 1
|
411
|
-
end
|
412
|
-
filtered = self.class.new(@width, @height)
|
413
|
-
|
414
|
-
|
415
|
-
$stdout.puts "processing #{@height} rows"
|
416
|
-
pb = ProgressBar.new(@height) if $DEBUG
|
417
|
-
|
418
|
-
@height.times do |y|
|
419
|
-
@width.times do |x|
|
420
|
-
window = []
|
421
|
-
(x - radius).upto(x + radius).each do |win_x|
|
422
|
-
(y - radius).upto(y + radius).each do |win_y|
|
423
|
-
win_x = 0 if win_x < 0
|
424
|
-
win_y = 0 if win_y < 0
|
425
|
-
win_x = @width-1 if win_x >= @width
|
426
|
-
win_y = @height-1 if win_y >= @height
|
427
|
-
window << self[win_x, win_y]
|
428
|
-
end
|
429
|
-
end
|
430
|
-
# median
|
431
|
-
filtered[x, y] = window.sort[window.length / 2]
|
432
|
-
end
|
433
|
-
pb.update(y) if $DEBUG
|
434
|
-
end
|
435
|
-
|
436
|
-
pb.close if $DEBUG
|
437
|
-
|
438
|
-
filtered
|
439
|
-
end
|
440
|
-
|
441
|
-
###############################################
|
442
|
-
def histogram
|
443
|
-
histogram = Hash.new(0)
|
444
|
-
@height.times do |y|
|
445
|
-
@width.times do |x|
|
446
|
-
histogram[self[x,y].luminosity] += 1
|
447
|
-
end
|
448
|
-
end
|
449
|
-
histogram
|
450
|
-
end
|
451
|
-
|
452
|
-
def to_blackandwhite
|
453
|
-
hist = histogram
|
454
|
-
|
455
|
-
# find the median luminosity
|
456
|
-
median = nil
|
457
|
-
sum = 0
|
458
|
-
hist.keys.sort.each do |lum|
|
459
|
-
sum += hist[lum]
|
460
|
-
if sum > @height * @width / 2
|
461
|
-
median = lum
|
462
|
-
break
|
463
|
-
end
|
464
|
-
end
|
465
|
-
|
466
|
-
# create the black and white image
|
467
|
-
bw = self.class.new(@width, @height)
|
468
|
-
@height.times do |y|
|
469
|
-
@width.times do |x|
|
470
|
-
bw[x,y] = self[x,y].luminosity < median ? RGBColour::BLACK : RGBColour::WHITE
|
471
|
-
end
|
472
|
-
end
|
473
|
-
bw
|
474
|
-
end
|
475
|
-
|
476
|
-
def save_as_blackandwhite(filename)
|
477
|
-
to_blackandwhite.save(filename)
|
478
|
-
end
|
479
|
-
|
480
|
-
###############################################
|
481
|
-
def draw_bezier_curve(points, colour)
|
482
|
-
# ensure the points are increasing along the x-axis
|
483
|
-
points = points.sort_by {|p| [p.x, p.y]}
|
484
|
-
xmin = points[0].x
|
485
|
-
xmax = points[-1].x
|
486
|
-
increment = 2
|
487
|
-
prev = points[0]
|
488
|
-
((xmin + increment) .. xmax).step(increment) do |x|
|
489
|
-
t = 1.0 * (x - xmin) / (xmax - xmin)
|
490
|
-
p = Pixel[x, bezier(t, points).round]
|
491
|
-
draw_line(prev, p, colour)
|
492
|
-
prev = p
|
493
|
-
end
|
494
|
-
end
|
495
|
-
|
496
|
-
# the generalized n-degree Bezier summation
|
497
|
-
def bezier(t, points)
|
498
|
-
n = points.length - 1
|
499
|
-
points.each_with_index.inject(0.0) do |sum, (point, i)|
|
500
|
-
sum += n.choose(i) * (1-t)**(n - i) * t**i * point.y
|
501
|
-
end
|
502
|
-
end
|
503
|
-
|
504
|
-
###############################################
|
505
|
-
def self.mandelbrot(width, height)
|
506
|
-
mandel = Pixmap.new(width,height)
|
507
|
-
pb = ProgressBar.new(width) if $DEBUG
|
508
|
-
width.times do |x|
|
509
|
-
height.times do |y|
|
510
|
-
x_ish = Float(x - width*11/15) / (width/3)
|
511
|
-
y_ish = Float(y - height/2) / (height*3/10)
|
512
|
-
mandel[x,y] = RGBColour.mandel_colour(mandel_iters(x_ish, y_ish))
|
513
|
-
end
|
514
|
-
pb.update(x) if $DEBUG
|
515
|
-
end
|
516
|
-
pb.close if $DEBUG
|
517
|
-
mandel
|
518
|
-
end
|
519
|
-
|
520
|
-
def self.mandel_iters(cx,cy)
|
521
|
-
x = y = 0.0
|
522
|
-
count = 0
|
523
|
-
while Math.hypot(x,y) < 2 and count < 255
|
524
|
-
x, y = (x**2 - y**2 + cx), (2*x*y + cy)
|
525
|
-
count += 1
|
526
|
-
end
|
527
|
-
count
|
528
|
-
end
|
529
|
-
|
530
|
-
###############################################
|
531
|
-
# Apply a convolution kernel to a whole image
|
532
|
-
def convolute(kernel)
|
533
|
-
newimg = Pixmap.new(@width, @height)
|
534
|
-
pb = ProgressBar.new(@width) if $DEBUG
|
535
|
-
@width.times do |x|
|
536
|
-
@height.times do |y|
|
537
|
-
apply_kernel(x, y, kernel, newimg)
|
538
|
-
end
|
539
|
-
pb.update(x) if $DEBUG
|
540
|
-
end
|
541
|
-
pb.close if $DEBUG
|
542
|
-
newimg
|
543
|
-
end
|
544
|
-
|
545
|
-
# Applies a convolution kernel to produce a single pixel in the destination
|
546
|
-
def apply_kernel(x, y, kernel, newimg)
|
547
|
-
x0 = [0, x-1].max
|
548
|
-
y0 = [0, y-1].max
|
549
|
-
x1 = x
|
550
|
-
y1 = y
|
551
|
-
x2 = [@width-1, x+1].min
|
552
|
-
y2 = [@height-1, y+1].min
|
553
|
-
|
554
|
-
r = g = b = 0.0
|
555
|
-
[x0, x1, x2].zip(kernel).each do |xx, kcol|
|
556
|
-
[y0, y1, y2].zip(kcol).each do |yy, k|
|
557
|
-
r += k * self[xx,yy].r
|
558
|
-
g += k * self[xx,yy].g
|
559
|
-
b += k * self[xx,yy].b
|
560
|
-
end
|
561
|
-
end
|
562
|
-
newimg[x,y] = RGBColour.new(luma(r), luma(g), luma(b))
|
563
|
-
end
|
564
|
-
|
565
|
-
# Function for clamping values to those that we can use with colors
|
566
|
-
def luma(value)
|
567
|
-
if value < 0
|
568
|
-
0
|
569
|
-
elsif value > 255
|
570
|
-
255
|
571
|
-
else
|
572
|
-
value
|
573
|
-
end
|
574
|
-
end
|
575
|
-
end
|
576
|
-
|
577
|
-
|
578
|
-
###########################################################################
|
579
|
-
# Utilities
|
580
|
-
class ProgressBar
|
581
|
-
def initialize(max)
|
582
|
-
$stdout.sync = true
|
583
|
-
@progress_max = max
|
584
|
-
@progress_pos = 0
|
585
|
-
@progress_view = 68
|
586
|
-
$stdout.print "[#{'-'*@progress_view}]\r["
|
587
|
-
end
|
588
|
-
|
589
|
-
def update(n)
|
590
|
-
new_pos = n * @progress_view/@progress_max
|
591
|
-
if new_pos > @progress_pos
|
592
|
-
@progress_pos = new_pos
|
593
|
-
$stdout.print '='
|
594
|
-
end
|
595
|
-
end
|
596
|
-
|
597
|
-
def close
|
598
|
-
$stdout.puts '=]'
|
599
|
-
end
|
600
|
-
end
|
601
|
-
|
602
|
-
class RasterQueue < Array
|
603
|
-
alias_method :enqueue, :push
|
604
|
-
alias_method :dequeue, :shift
|
605
|
-
end
|
606
|
-
|
607
|
-
class Numeric
|
608
|
-
def fpart
|
609
|
-
self - self.truncate
|
610
|
-
end
|
611
|
-
def rfpart
|
612
|
-
1.0 - self.fpart
|
613
|
-
end
|
614
|
-
end
|
615
|
-
|
616
|
-
class Integer
|
617
|
-
def choose(k)
|
618
|
-
self.factorial / (k.factorial * (self - k).factorial)
|
619
|
-
end
|
620
|
-
def factorial
|
621
|
-
(2 .. self).reduce(1, :*)
|
622
|
-
end
|
623
|
-
end
|