active_assets 0.2.3 → 0.2.4
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/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
|