compass-magick 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.md +22 -0
- data/README.md +67 -0
- data/lib/magick/canvas.rb +127 -0
- data/lib/magick/command.rb +32 -0
- data/lib/magick/effect.rb +34 -0
- data/lib/magick/functions/canvas.rb +41 -0
- data/lib/magick/functions/drawing.rb +156 -0
- data/lib/magick/functions/operations/effects.rb +143 -0
- data/lib/magick/functions/operations.rb +211 -0
- data/lib/magick/functions/sprites.rb +28 -0
- data/lib/magick/functions/types.rb +77 -0
- data/lib/magick/functions.rb +31 -0
- data/lib/magick/plugins.rb +14 -0
- data/lib/magick/scriptable.rb +31 -0
- data/lib/magick/shapes.rb +94 -0
- data/lib/magick/types/gradients.rb +182 -0
- data/lib/magick/types/solid.rb +40 -0
- data/lib/magick/types.rb +39 -0
- data/lib/magick/utils.rb +92 -0
- data/lib/magick.rb +79 -0
- data/lib/plugins/corners.rb +29 -0
- data/lib/plugins/pattern.rb +49 -0
- data/lib/stylesheets/_magick.sass +0 -0
- data/spec/canvas_spec.rb +19 -0
- data/spec/helpers.rb +5 -0
- data/spec/types/gradients_spec.rb +29 -0
- data/spec/types/solid_spec.rb +19 -0
- metadata +144 -0
@@ -0,0 +1,211 @@
|
|
1
|
+
require 'magick/functions/operations/effects'
|
2
|
+
|
3
|
+
module Compass::Magick
|
4
|
+
module Functions
|
5
|
+
# Methods for operating on a {Compass::Magick::Canvas}, e.g., crop,
|
6
|
+
# compose, etc.
|
7
|
+
module Operations
|
8
|
+
# Composes one {Canvas} on top of another.
|
9
|
+
#
|
10
|
+
# @param [Canvas] overlay The Canvas object to compose.
|
11
|
+
# @param [Integer] x The left coordinate of the composition operation.
|
12
|
+
# @param [Integer] y The top coordinate of the composition operation.
|
13
|
+
# @return {Command} A command which composes the two canvas objects
|
14
|
+
# together.
|
15
|
+
def magick_compose(overlay, x = nil, y = nil)
|
16
|
+
Compass::Magick::Utils.assert_type 'overlay', overlay, ChunkyPNG::Canvas
|
17
|
+
Compass::Magick::Utils.assert_type 'x', x, Sass::Script::Number
|
18
|
+
Compass::Magick::Utils.assert_type 'y', y, Sass::Script::Number
|
19
|
+
Command.new do |canvas|
|
20
|
+
canvas_x = Compass::Magick::Utils.value_of(x, canvas.width - overlay.width, 0).to_i
|
21
|
+
canvas_y = Compass::Magick::Utils.value_of(y, canvas.height - overlay.height, 0).to_i
|
22
|
+
raise ChunkyPNG::OutOfBounds, 'Canvas image width is too small to compose overlay' if canvas.width < overlay.width + canvas_x
|
23
|
+
raise ChunkyPNG::OutOfBounds, 'Canvas image height is too small to compose overlay' if canvas.height < overlay.height + canvas_y
|
24
|
+
for y in 0...overlay.height do
|
25
|
+
for x in 0...overlay.width do
|
26
|
+
overlay_pixel = overlay.get_pixel(x, y)
|
27
|
+
canvas_pixel = canvas.get_pixel(x + canvas_x, y + canvas_y)
|
28
|
+
canvas.set_pixel(x + canvas_x, y + canvas_y, ChunkyPNG::Color.compose_precise(overlay_pixel, canvas_pixel))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Crops the {Canvas} to the given region.
|
35
|
+
#
|
36
|
+
# @param [Integer] x1 The left coordinate of the crop operation.
|
37
|
+
# @param [Integer] y1 The top coordinate of the crop operation.
|
38
|
+
# @param [Integer] x2 The right coordinate of the crop operation.
|
39
|
+
# @param [Integer] y2 The bottom coordinate of the crop operation.
|
40
|
+
# @return {Command} A command which applies the crop to the canvas.
|
41
|
+
def magick_crop(x1 = nil, y1 = nil, x2 = nil, y2 = nil)
|
42
|
+
Compass::Magick::Utils.assert_type 'x1', x1, Sass::Script::Number
|
43
|
+
Compass::Magick::Utils.assert_type 'y1', y1, Sass::Script::Number
|
44
|
+
Compass::Magick::Utils.assert_type 'x2', x2, Sass::Script::Number
|
45
|
+
Compass::Magick::Utils.assert_type 'y2', y2, Sass::Script::Number
|
46
|
+
Command.new do |canvas|
|
47
|
+
canvas_x1 = Compass::Magick::Utils.value_of(x1, canvas.width, 0)
|
48
|
+
canvas_y1 = Compass::Magick::Utils.value_of(y1, canvas.height, 0)
|
49
|
+
canvas_x2 = Compass::Magick::Utils.value_of(x2, canvas.width, canvas.width)
|
50
|
+
canvas_y2 = Compass::Magick::Utils.value_of(y2, canvas.height, canvas.height)
|
51
|
+
canvas_x1, canvas_x2 = canvas_x2, canvas_x1 if canvas_x1 > canvas_x2
|
52
|
+
canvas_y1, canvas_y2 = canvas_y2, canvas_y1 if canvas_y1 > canvas_y2
|
53
|
+
width = canvas_x2 - canvas_x1
|
54
|
+
height = canvas_y2 - canvas_y1
|
55
|
+
canvas.crop(canvas_x1, canvas_y1, width, height)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Applies the mask on the {Canvas}.
|
60
|
+
#
|
61
|
+
# Composes the alpha channel from the <tt>mask</tt> image with the
|
62
|
+
# one from the canvas and return the original canvas with the
|
63
|
+
# alpha-channel modified. Any opaque pixels from the <tt>mask</tt> are
|
64
|
+
# converted to grayscale using BT709 luminosity factors, i.e. black is
|
65
|
+
# fully transparent and white is fully opaque.
|
66
|
+
#
|
67
|
+
# @param [Integer] x The left coordinate of the mask operation.
|
68
|
+
# @param [Integer] y The top coordinate of the mask operation.
|
69
|
+
# @return {Command} A command which applies the mask on the canvas.
|
70
|
+
def magick_mask(mask, x = nil, y = nil)
|
71
|
+
Compass::Magick::Utils.assert_type 'mask', mask, ChunkyPNG::Canvas
|
72
|
+
Compass::Magick::Utils.assert_type 'x', x, Sass::Script::Number
|
73
|
+
Compass::Magick::Utils.assert_type 'y', y, Sass::Script::Number
|
74
|
+
Command.new do |canvas|
|
75
|
+
canvas_x = Compass::Magick::Utils.value_of(x, canvas.width - 1, 0)
|
76
|
+
canvas_y = Compass::Magick::Utils.value_of(y, canvas.height - 1, 0)
|
77
|
+
raise ChunkyPNG::OutOfBounds, 'Canvas image width is too small to fit mask' if canvas.width < mask.width + canvas_x
|
78
|
+
raise ChunkyPNG::OutOfBounds, 'Canvas image height is too small to fit mask' if canvas.height < mask.height + canvas_y
|
79
|
+
for y in 0...mask.height do
|
80
|
+
for x in 0...mask.width do
|
81
|
+
canvas_pixel = canvas.get_pixel(x + canvas_x, y + canvas_y)
|
82
|
+
mask_pixel = mask.get_pixel(x, y)
|
83
|
+
mask_alpha = (ChunkyPNG::Color.r(mask_pixel) * 0.2125 + ChunkyPNG::Color.g(mask_pixel) * 0.7154 + ChunkyPNG::Color.b(mask_pixel) * 0.0721) * (ChunkyPNG::Color.a(mask_pixel) / 255.0)
|
84
|
+
canvas.set_pixel(x + canvas_x, y + canvas_y, ChunkyPNG::Color.rgba(
|
85
|
+
ChunkyPNG::Color.r(canvas_pixel),
|
86
|
+
ChunkyPNG::Color.g(canvas_pixel),
|
87
|
+
ChunkyPNG::Color.b(canvas_pixel),
|
88
|
+
ChunkyPNG::Color.a(canvas_pixel) * (mask_alpha / 255.0)
|
89
|
+
))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Apply an effect on the {Canvas}.
|
96
|
+
#
|
97
|
+
# @param [Sass::Script::String] name The name of the effect to apply.
|
98
|
+
# @param [Array] args The arguments to pass to the effect.
|
99
|
+
# @return {Effect} A command which applies the effect to the canvas.
|
100
|
+
def magick_effect(name, *args)
|
101
|
+
Compass::Magick::Utils.assert_type 'name', name, Sass::Script::String
|
102
|
+
Compass::Magick::Functions::Operations::Effects.send(name.value, *args)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Apply drop shadow on the {Canvas}.
|
106
|
+
#
|
107
|
+
# The alpha channel is used to construct a mask of the original image
|
108
|
+
# which is then used as a base for the horizontal/vertical shadow pass.
|
109
|
+
#
|
110
|
+
# Copyright (c) 2005 by Romain Guy
|
111
|
+
# http://www.curious-creature.org/2005/07/06/non-rectangular-shadow/
|
112
|
+
#
|
113
|
+
# @param [Sass::Script::Number] angle The angle of the shadow.
|
114
|
+
# @param [Sass::Script::Number] distance The distance of the shadow from
|
115
|
+
# the original canvas.
|
116
|
+
# @param [Sass::Script::Number] size The size (blur) of the shadow.
|
117
|
+
# @param [Sass::Script::Color] color The color of the shadow.
|
118
|
+
# @return {Command} A command which applies the drop shadow to the
|
119
|
+
# canvas.
|
120
|
+
def magick_drop_shadow(angle = nil, distance = nil, size = nil, color = nil)
|
121
|
+
Compass::Magick::Utils.assert_type 'angle', angle, Sass::Script::Number
|
122
|
+
Compass::Magick::Utils.assert_type 'distance', distance, Sass::Script::Number
|
123
|
+
Compass::Magick::Utils.assert_type 'size', size, Sass::Script::Number
|
124
|
+
Compass::Magick::Utils.assert_type 'color', color, Sass::Script::Color
|
125
|
+
Command.new do |canvas|
|
126
|
+
shadow_angle = Compass::Magick::Utils.value_of(angle, 360, 0)
|
127
|
+
shadow_distance = Compass::Magick::Utils.value_of(distance, [canvas.width, canvas.height].min, 5)
|
128
|
+
shadow_size = Compass::Magick::Utils.value_of(size, [canvas.width, canvas.height].min, 5)
|
129
|
+
shadow_color = Compass::Magick::Utils.to_chunky_color(color.nil? ? Sass::Script::Color.new([0, 0, 0, 1]) : color)
|
130
|
+
shadow_canvas = ChunkyPNG::Canvas.new(canvas.width + shadow_size * 2, canvas.height + shadow_size * 2).replace(canvas, shadow_size, shadow_size)
|
131
|
+
shadow_pixels = shadow_canvas.pixels
|
132
|
+
shadow_red = ChunkyPNG::Color.r(shadow_color)
|
133
|
+
shadow_green = ChunkyPNG::Color.g(shadow_color)
|
134
|
+
shadow_blue = ChunkyPNG::Color.b(shadow_color)
|
135
|
+
shadow_alpha = ChunkyPNG::Color.a(shadow_color)
|
136
|
+
angle_radians = shadow_angle * Math::PI / 180
|
137
|
+
distance_x = (Math.cos(angle_radians) * shadow_distance).to_i
|
138
|
+
distance_y = (Math.sin(angle_radians) * shadow_distance).to_i
|
139
|
+
left = (shadow_size - 1) >> 1
|
140
|
+
right = shadow_size - left
|
141
|
+
x_start = left
|
142
|
+
x_finish = shadow_canvas.width - right
|
143
|
+
y_start = left
|
144
|
+
y_finish = shadow_canvas.height - right
|
145
|
+
a_history = Array.new(shadow_size)
|
146
|
+
history_index = 0
|
147
|
+
sum_divider = (shadow_alpha / 255.0) / shadow_size
|
148
|
+
buffer_offset = 0
|
149
|
+
last_pixel_offset = right * shadow_canvas.width
|
150
|
+
for y in (0...shadow_canvas.height)
|
151
|
+
a_sum = 0
|
152
|
+
history_index = 0
|
153
|
+
for x in (0...shadow_size)
|
154
|
+
a = ChunkyPNG::Color.a(shadow_pixels[buffer_offset])
|
155
|
+
a_history[x] = a
|
156
|
+
a_sum = a_sum + a
|
157
|
+
buffer_offset = buffer_offset + 1
|
158
|
+
end
|
159
|
+
buffer_offset = buffer_offset - right
|
160
|
+
for x in (x_start...x_finish)
|
161
|
+
a = a_sum * sum_divider
|
162
|
+
shadow_pixels[buffer_offset] = ChunkyPNG::Color.rgba(shadow_red, shadow_green, shadow_blue, (shadow_alpha * (a / 255.0)).to_i)
|
163
|
+
a_sum = a_sum - a_history[history_index]
|
164
|
+
a = ChunkyPNG::Color.a(shadow_pixels[buffer_offset + right])
|
165
|
+
a_history[history_index] = a
|
166
|
+
a_sum = a_sum + a
|
167
|
+
history_index = history_index + 1
|
168
|
+
history_index = history_index - shadow_size if history_index >= shadow_size
|
169
|
+
buffer_offset = buffer_offset + 1
|
170
|
+
end
|
171
|
+
buffer_offset = y * shadow_canvas.width
|
172
|
+
end
|
173
|
+
buffer_offset = 0
|
174
|
+
for x in (0...shadow_canvas.width)
|
175
|
+
a_sum = 0
|
176
|
+
history_index = 0
|
177
|
+
for y in (0...shadow_size)
|
178
|
+
a = ChunkyPNG::Color.a(shadow_pixels[buffer_offset])
|
179
|
+
a_history[y] = a
|
180
|
+
a_sum = a_sum + a
|
181
|
+
buffer_offset = buffer_offset + shadow_canvas.width
|
182
|
+
end
|
183
|
+
buffer_offset = buffer_offset - last_pixel_offset
|
184
|
+
for y in (y_start...y_finish)
|
185
|
+
a = a_sum * sum_divider
|
186
|
+
shadow_pixels[buffer_offset] = ChunkyPNG::Color.rgba(shadow_red, shadow_green, shadow_blue, (shadow_alpha * (a / 255.0)).to_i)
|
187
|
+
a_sum = a_sum - a_history[history_index]
|
188
|
+
a = ChunkyPNG::Color.a(shadow_pixels[buffer_offset + last_pixel_offset])
|
189
|
+
a_history[history_index] = a
|
190
|
+
a_sum = a_sum + a
|
191
|
+
history_index = history_index + 1
|
192
|
+
history_index = history_index - shadow_size if history_index >= shadow_size
|
193
|
+
buffer_offset = buffer_offset + shadow_canvas.width
|
194
|
+
end
|
195
|
+
buffer_offset = x
|
196
|
+
end
|
197
|
+
for y in 0...canvas.height do
|
198
|
+
for x in 0...canvas.width do
|
199
|
+
shadow_x = x + shadow_size + distance_x
|
200
|
+
shadow_y = y + shadow_size + distance_y
|
201
|
+
canvas.set_pixel(x, y, ChunkyPNG::Color.compose_precise(
|
202
|
+
canvas.get_pixel(x, y),
|
203
|
+
shadow_pixels[shadow_y * shadow_canvas.width + shadow_x]
|
204
|
+
))
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Compass::Magick
|
2
|
+
module Functions
|
3
|
+
# Methods for generating sprites from on a {Compass::Magick::Canvas}.
|
4
|
+
module Sprites
|
5
|
+
# Writes the canvas to a file, encoded as a PNG image. The output is
|
6
|
+
# optimized for best compression.
|
7
|
+
#
|
8
|
+
# @param [Sass::Script::String] basename The PNG image basename. The
|
9
|
+
# generated file is saved in the configured images directory with a
|
10
|
+
# .png extension. Directory names are allowed and can be used to
|
11
|
+
# group a set of objects together.
|
12
|
+
# @param [Canvas] canvas The Canvas object to write.
|
13
|
+
# @return [Sass::Script::String] A URL to the generated sprite image.
|
14
|
+
# (Depending on your configuration) The path is relative and has the
|
15
|
+
# cache buster appended after the file extension.
|
16
|
+
def magick_sprite(basename, canvas)
|
17
|
+
Compass::Magick::Utils.assert_type 'basename', basename, Sass::Script::String
|
18
|
+
Compass::Magick::Utils.assert_type 'canvas', canvas, ChunkyPNG::Canvas
|
19
|
+
extension = '.png'
|
20
|
+
filename = basename.value.chomp(extension) + extension
|
21
|
+
filepath = File.join(Compass.configuration.images_path, filename)
|
22
|
+
FileUtils.mkpath(File.dirname(filepath))
|
23
|
+
canvas.save(filepath, :best_compression)
|
24
|
+
image_url(Sass::Script::String.new(filename))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Compass::Magick
|
2
|
+
module Functions
|
3
|
+
# Methods for creating new {Compass::Magick::Types}.
|
4
|
+
module Types
|
5
|
+
# Creates a new {Compass::Magick::Types::Solid} instance.
|
6
|
+
#
|
7
|
+
# @param [Sass::Script::Color] color The solid background color.
|
8
|
+
# @return [Compass::Magick::Types::Solid] A type that generates a
|
9
|
+
# {Canvas} from a region filled with a solid color.
|
10
|
+
def magick_solid(color)
|
11
|
+
Compass::Magick::Types::Solid.new(color)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Creates a new {Compass::Magick::Types::ColorStop} instance.
|
15
|
+
#
|
16
|
+
# @param [Sass::Script::Number] offset The color stop offset, 0-100.
|
17
|
+
# @param [Sass::Script::Color] color The color at the offset.
|
18
|
+
# @return [Compass::Magick::Types::ColorStop] A type that is used when
|
19
|
+
# constructing gradients to control color stops.
|
20
|
+
def magick_color_stop(offset, color)
|
21
|
+
Compass::Magick::Types::Gradients::ColorStop.new(offset, color)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Creates a new {Compass::Magick::Types::Gradients::Linear} instance.
|
25
|
+
#
|
26
|
+
# @overload magick_linear_gradient(angle, stops)
|
27
|
+
# @param [Sass::Script::Number] angle The angle of the linear
|
28
|
+
# gradient.
|
29
|
+
# @param [Array<Compass::Magick::Types::Gradients::ColorStop>] stops
|
30
|
+
# A list of color stops to interpolate between.
|
31
|
+
# @overload magick_linear_gradient(stops)
|
32
|
+
# @param [Array<Compass::Magick::Types::Gradients::ColorStop>] stops
|
33
|
+
# A list of color stops to interpolate between.
|
34
|
+
# @return [Compass::Magick::Types::Gradients::Linear] A type that
|
35
|
+
# generates a {Canvas} from a region filled using point-to-point
|
36
|
+
# linear gradient at an angle.
|
37
|
+
def magick_linear_gradient(*args)
|
38
|
+
angle = args.shift if args[0].kind_of?(Sass::Script::Number)
|
39
|
+
angle ||= Sass::Script::Number.new(90)
|
40
|
+
stops = []
|
41
|
+
last_offset = 0
|
42
|
+
args.each_with_index do |stop, index|
|
43
|
+
if stop.kind_of?(Sass::Script::Color)
|
44
|
+
if index > 0
|
45
|
+
if index == args.length - 1
|
46
|
+
offset = 100
|
47
|
+
else
|
48
|
+
next_index = 0
|
49
|
+
next_offset = nil
|
50
|
+
args.slice(index, args.length).each do |next_stop|
|
51
|
+
next_index = next_index + 1
|
52
|
+
if next_stop.kind_of?(Compass::Magick::Types::Gradients::ColorStop)
|
53
|
+
next_offset = next_stop.offset.value
|
54
|
+
break
|
55
|
+
end
|
56
|
+
end
|
57
|
+
next_offset ||= 100
|
58
|
+
offset = last_offset + (next_offset - last_offset) / next_index
|
59
|
+
end
|
60
|
+
else
|
61
|
+
offset = 0
|
62
|
+
end
|
63
|
+
stops.push(Compass::Magick::Types::Gradients::ColorStop.new(Sass::Script::Number.new(offset), stop))
|
64
|
+
last_offset = offset
|
65
|
+
elsif stop.kind_of?(Sass::Script::List)
|
66
|
+
stops.push(Compass::Magick::Types::Gradients::ColorStop.new(stop.value[1], stop.value[0]))
|
67
|
+
last_offset = stop.value[1].value
|
68
|
+
else
|
69
|
+
stops.push(stop)
|
70
|
+
last_offset = stop.offset.value if stop.respond_to?(:offset)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
Compass::Magick::Types::Gradients::Linear.new(angle, stops)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'magick/functions/canvas'
|
2
|
+
require 'magick/functions/types'
|
3
|
+
require 'magick/functions/drawing'
|
4
|
+
require 'magick/functions/operations'
|
5
|
+
require 'magick/functions/sprites'
|
6
|
+
|
7
|
+
require 'magick/plugins'
|
8
|
+
|
9
|
+
module Compass::Magick
|
10
|
+
# The Functions module includes all public Compass Magick functions.
|
11
|
+
#
|
12
|
+
# @see Functions::Canvas
|
13
|
+
# @see Functions::Types
|
14
|
+
# @see Functions::Drawing
|
15
|
+
module Functions
|
16
|
+
include Functions::Canvas
|
17
|
+
include Functions::Types
|
18
|
+
include Functions::Drawing
|
19
|
+
include Functions::Operations
|
20
|
+
include Functions::Sprites
|
21
|
+
include Compass::Magick::Plugins
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Functions defined in this module are exported for usage in stylesheets
|
26
|
+
# (.sass and .scss documents).
|
27
|
+
#
|
28
|
+
# Compass Magick exports all available {Compass::Magick::Functions functions}.
|
29
|
+
module Sass::Script::Functions
|
30
|
+
include Compass::Magick::Functions
|
31
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Compass::Magick
|
2
|
+
# The Plugins module includes all external Compass Magick functions.
|
3
|
+
#
|
4
|
+
# @see Compass::Magick::PLUGINS_PATH
|
5
|
+
module Plugins; end
|
6
|
+
|
7
|
+
PLUGINS_PATH.each do |path|
|
8
|
+
if File.exists?(path)
|
9
|
+
Dir.glob(File.join(path, '**', '*.rb')).each do |plugin|
|
10
|
+
require plugin
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Compass::Magick
|
2
|
+
# Common Sass node methods module.
|
3
|
+
#
|
4
|
+
# In order to export certain classes for Sass, they must implement a set of
|
5
|
+
# methods. This module provides a no-op implementation.
|
6
|
+
module Scriptable
|
7
|
+
# Sets the options hash for this node.
|
8
|
+
#
|
9
|
+
# @param [{Symbol => Object}] options The options hash.
|
10
|
+
def options=(options)
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [{Symbol => Object}] The options hash.
|
15
|
+
attr_reader :options
|
16
|
+
|
17
|
+
# Sets the context for this node.
|
18
|
+
#
|
19
|
+
# @param [Symbol] context
|
20
|
+
# @see #context
|
21
|
+
def context=(context)
|
22
|
+
@context = context
|
23
|
+
end
|
24
|
+
|
25
|
+
# The context in which this node was parsed, which determines how some
|
26
|
+
# operations are performed.
|
27
|
+
#
|
28
|
+
# @return [Symbol]
|
29
|
+
attr_reader :context
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Compass::Magick
|
2
|
+
# Drawing methods shared by Compass Magick commands.
|
3
|
+
#
|
4
|
+
# All of the drawing is done using B/W pixels with varying alpha. These
|
5
|
+
# shapes are used to build masks which are then applied to fill
|
6
|
+
# {Compass::Magick::Type}s to generate the final canvas.
|
7
|
+
module Shapes
|
8
|
+
extend self
|
9
|
+
|
10
|
+
# Draws a circle mask.
|
11
|
+
#
|
12
|
+
# Copyright (c) 2003 by Nils Haeck M.Sc. (Simdesign)
|
13
|
+
# http://www.simdesign.nl
|
14
|
+
#
|
15
|
+
# > The [..] DrawDisk routines is optimized quite well but do not claim
|
16
|
+
# > to be the fastest solution :) It is a floating point precision
|
17
|
+
# > implementation. Further optimisation would be possible if an
|
18
|
+
# > integer approach was chosen (but that would also loose
|
19
|
+
# > functionality).
|
20
|
+
#
|
21
|
+
# @param [Integer] radius The radius of the circle.
|
22
|
+
# @param [Float] feather The feater value determines the
|
23
|
+
# anti-aliasing the circle will get, defaults to <tt>1.0</tt>.
|
24
|
+
# @return {Canvas} A Canvas with a circle B/W mask.
|
25
|
+
def circle(radius, width, feather = 1.0)
|
26
|
+
mask = ChunkyPNG::Canvas.new(radius, radius, ChunkyPNG::Color.rgba(0, 0, 0, 0))
|
27
|
+
if radius <= width
|
28
|
+
center = (radius - 1) / 2.0
|
29
|
+
rpf2 = (center + feather / 2.0) ** 2
|
30
|
+
rmf2 = (center - feather / 2.0) ** 2
|
31
|
+
lx = [(center - rpf2).floor, 0].max
|
32
|
+
ly = [(center - rpf2).floor, 0].max
|
33
|
+
rx = [(center + rpf2).ceil, radius - 1].min
|
34
|
+
ry = [(center + rpf2).ceil, radius - 1].min
|
35
|
+
sqx = Array.new(rx - lx + 1)
|
36
|
+
for x in lx..rx
|
37
|
+
sqx[x - lx] = (x - center) ** 2
|
38
|
+
end
|
39
|
+
for y in ly..ry
|
40
|
+
sqy = (y - center) ** 2
|
41
|
+
for x in lx..rx
|
42
|
+
sqdist = sqy + sqx[x - lx]
|
43
|
+
if sqdist < rmf2
|
44
|
+
mask.set_pixel(x, y, ChunkyPNG::Color::WHITE)
|
45
|
+
else
|
46
|
+
if sqdist < rpf2
|
47
|
+
fact = (((center - Math.sqrt(sqdist)) * 2.0 / feather) * 0.5 + 0.5)
|
48
|
+
mask.set_pixel(x, y, ChunkyPNG::Color.rgba(255, 255, 255, 255 * [0, [fact, 1].min].max))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
else
|
54
|
+
center = (radius - 1) / 2.0
|
55
|
+
inrad = (center + feather / 2.0) - width
|
56
|
+
ropf2 = (center + feather / 2.0) ** 2
|
57
|
+
romf2 = (center - feather / 2.0) ** 2
|
58
|
+
ripf2 = (inrad + feather / 2.0) ** 2
|
59
|
+
rimf2 = (inrad - feather / 2.0) ** 2
|
60
|
+
lx = [(center - ropf2).floor, 0].max
|
61
|
+
ly = [(center - ropf2).floor, 0].max
|
62
|
+
rx = [(center + ropf2).ceil, radius - 1].min
|
63
|
+
ry = [(center + ropf2).ceil, radius - 1].min
|
64
|
+
feather = width if feather > width
|
65
|
+
sqx = Array.new(rx - lx + 1)
|
66
|
+
for x in lx..rx
|
67
|
+
sqx[x - lx] = (x - center) ** 2
|
68
|
+
end
|
69
|
+
for y in ly..ry
|
70
|
+
sqy = (y - center) ** 2
|
71
|
+
for x in lx..rx
|
72
|
+
sqdist = sqy + sqx[x - lx]
|
73
|
+
if sqdist >= rimf2
|
74
|
+
if sqdist < ropf2
|
75
|
+
if sqdist < romf2
|
76
|
+
if sqdist < ripf2
|
77
|
+
fact = (((Math.sqrt(sqdist) - inrad) * 2 / feather) * 0.5 + 0.5)
|
78
|
+
mask.set_pixel(x, y, ChunkyPNG::Color.rgba(255, 255, 255, 255 * [0, [fact, 1].min].max))
|
79
|
+
else
|
80
|
+
mask.set_pixel(x, y, ChunkyPNG::Color::WHITE)
|
81
|
+
end
|
82
|
+
else
|
83
|
+
fact = (((center - Math.sqrt(sqdist)) * 2 / feather) * 0.5 + 0.5)
|
84
|
+
mask.set_pixel(x, y, ChunkyPNG::Color.rgba(255, 255, 255, 255 * [0, [fact, 1].min].max))
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
mask
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
module Compass::Magick
|
2
|
+
module Types
|
3
|
+
# The Gradients module defines all available Compass Magick gradient
|
4
|
+
# fills.
|
5
|
+
#
|
6
|
+
# At present, we only support point-to-point linear fill at an angle. The
|
7
|
+
# two points are determined automatically such as the center of the
|
8
|
+
# linear gradient is the center if the given region as well.
|
9
|
+
module Gradients
|
10
|
+
# A type that is used when constructing gradients to have precise
|
11
|
+
# control over color stops.
|
12
|
+
class ColorStop < Type
|
13
|
+
include Utils
|
14
|
+
|
15
|
+
# Initializes a new ColorStop instance.
|
16
|
+
#
|
17
|
+
# @param [Sass::Script::Number] offset The color stop offset, 0-100.
|
18
|
+
# @param [Sass::Script::Color] color The color at the offset.
|
19
|
+
def initialize(offset, color)
|
20
|
+
assert_type 'offset', offset, Sass::Script::Number
|
21
|
+
assert_type 'color', color, Sass::Script::Color
|
22
|
+
@offset = offset
|
23
|
+
@color = color
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Sass::Script::Number] The color stop offset, 0-100.
|
27
|
+
attr_reader :offset
|
28
|
+
|
29
|
+
# @return [Sass::Script::Color] The color at the offset.
|
30
|
+
attr_reader :color
|
31
|
+
end
|
32
|
+
|
33
|
+
# A type that generates a {Canvas} from a region filled using
|
34
|
+
# point-to-point linear gradient at an angle.
|
35
|
+
#
|
36
|
+
# @example
|
37
|
+
#
|
38
|
+
# Linear.new(
|
39
|
+
# Sass::Script::Number.new(45), [
|
40
|
+
# ColorStop.new(Sass::Script::Number.new(0), Sass::Script::Color.new([255, 0, 0])),
|
41
|
+
# ColorStop.new(Sass::Script::Number.new(50), Sass::Script::Color.new([ 0, 255, 0])),
|
42
|
+
# ColorStop.new(Sass::Script::Number.new(100), Sass::Script::Color.new([ 0, 0, 255]))
|
43
|
+
# ]
|
44
|
+
# )
|
45
|
+
class Linear < Type
|
46
|
+
include Utils
|
47
|
+
|
48
|
+
# Initializes a new Linear instance.
|
49
|
+
#
|
50
|
+
# @param [Sass::Script::Number] angle The angle of the linear
|
51
|
+
# gradient. The two points that form the point-to-point linear fill
|
52
|
+
# are determined based on this value.
|
53
|
+
# @param [Array<ColorStop>] stops A list of color stops to interpolate
|
54
|
+
# between.
|
55
|
+
def initialize(angle, stops)
|
56
|
+
assert_type 'angle', angle, Sass::Script::Number
|
57
|
+
assert_type 'stops', stops, Array
|
58
|
+
stops.each_with_index { |stop, index| assert_type "stop[#{index}]", stop, ColorStop }
|
59
|
+
@angle = angle
|
60
|
+
@stops = cache_stops(stops)
|
61
|
+
end
|
62
|
+
|
63
|
+
# @return [Sass::Script::Number] angle The angle of the linear
|
64
|
+
# gradient.
|
65
|
+
attr_reader :angle
|
66
|
+
|
67
|
+
# @return [Array<ColorStop>] A list of color stops to interpolate
|
68
|
+
# between.
|
69
|
+
attr_reader :stops
|
70
|
+
|
71
|
+
def to_canvas(width, height)
|
72
|
+
assert_type 'width', width, Sass::Script::Number
|
73
|
+
assert_type 'height', height, Sass::Script::Number
|
74
|
+
canvas = Canvas.new(width, height)
|
75
|
+
point_center = Point.new(canvas.width / 2, canvas.height / 2)
|
76
|
+
length_diagonal = Math.sqrt(canvas.width ** 2 + canvas.height ** 2);
|
77
|
+
segments_rectangle = [
|
78
|
+
[Point.new(0, 0), Point.new(canvas.width - 1, 0)],
|
79
|
+
[Point.new(canvas.width - 1, 0), Point.new(canvas.width - 1, canvas.height - 1)],
|
80
|
+
[Point.new(canvas.width - 1, canvas.height - 1), Point.new(0, canvas.height - 1)],
|
81
|
+
[Point.new(0, canvas.height - 1), Point.new(0, 0)]
|
82
|
+
]
|
83
|
+
point_start = nil
|
84
|
+
point_finish = nil
|
85
|
+
[-1, 1].each do |direction|
|
86
|
+
segment = [
|
87
|
+
Point.new(point_center.x, point_center.y),
|
88
|
+
Point.new(point_center.x + direction * length_diagonal * Math.cos(@angle.value * Math::PI / 180), point_center.y + direction * length_diagonal * Math.sin(@angle.value * Math::PI / 180))
|
89
|
+
];
|
90
|
+
segments_rectangle.each do |edge|
|
91
|
+
point = intersect(segment, edge)
|
92
|
+
if point
|
93
|
+
point_start ||= point if direction == -1
|
94
|
+
point_finish ||= point if direction == 1
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
# Michael Madsen & dash-tom-bang
|
99
|
+
# http://stackoverflow.com/questions/2869785/point-to-point-linear-gradient#answer-2870275
|
100
|
+
vector_gradient = [point_finish.x - point_start.x, point_finish.y - point_start.y]
|
101
|
+
length_gradient = Math.sqrt(vector_gradient[0] * vector_gradient[0] + vector_gradient[1] * vector_gradient[1])
|
102
|
+
vector_normalized = [vector_gradient[0] * (1 / length_gradient), vector_gradient[1] * (1 / length_gradient)]
|
103
|
+
(0...canvas.height).each do |y|
|
104
|
+
(0...canvas.width).each do |x|
|
105
|
+
result_normalized = (vector_normalized[0] * (x - point_start.x) + vector_normalized[1] * (y - point_start.y)) / length_gradient
|
106
|
+
canvas.set_pixel(x, y, interpolate(100 * (result_normalized < 0 ? 0 : result_normalized > 1 ? 1 : result_normalized)))
|
107
|
+
end
|
108
|
+
end
|
109
|
+
canvas
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def cache_stops(stops)
|
115
|
+
@stops = stops.sort { |left, right| left.offset.value <=> right.offset.value }.map do |stop|
|
116
|
+
{
|
117
|
+
:color => to_chunky_color(stop.color),
|
118
|
+
:offset => stop.offset.value,
|
119
|
+
:rgba => [
|
120
|
+
stop.color.red,
|
121
|
+
stop.color.green,
|
122
|
+
stop.color.blue,
|
123
|
+
stop.color.alpha * 255
|
124
|
+
]
|
125
|
+
}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def intersect(segment1, segment2)
|
130
|
+
# Andre LeMothe
|
131
|
+
# http://www.amazon.com/dp/0672323699/
|
132
|
+
# http://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect#answer-1968345
|
133
|
+
s1_x = segment1[1].x - segment1[0].x
|
134
|
+
s1_y = segment1[1].y - segment1[0].y
|
135
|
+
s2_x = segment2[1].x - segment2[0].x
|
136
|
+
s2_y = segment2[1].y - segment2[0].y
|
137
|
+
s = (- s1_y * (segment1[0].x - segment2[0].x) + s1_x * (segment1[0].y - segment2[0].y)) / (- s2_x * s1_y + s1_x * s2_y)
|
138
|
+
t = ( s2_x * (segment1[0].y - segment2[0].y) - s2_y * (segment1[0].x - segment2[0].x)) / (- s2_x * s1_y + s1_x * s2_y)
|
139
|
+
if s >= 0 && s <= 1 && t >= 0 && t <= 1
|
140
|
+
Point.new(
|
141
|
+
segment1[0].x + (t * s1_x),
|
142
|
+
segment1[0].y + (t * s1_y)
|
143
|
+
)
|
144
|
+
else
|
145
|
+
false
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def interpolate(offset)
|
150
|
+
start = nil
|
151
|
+
finish = nil
|
152
|
+
@stops.each do |stop|
|
153
|
+
if offset >= stop[:offset]
|
154
|
+
if start
|
155
|
+
start = stop unless start[:offset] > stop[:offset]
|
156
|
+
else
|
157
|
+
start = stop
|
158
|
+
end
|
159
|
+
end
|
160
|
+
if offset <= stop[:offset]
|
161
|
+
if finish
|
162
|
+
finish = stop unless finish[:offset] < stop[:offset]
|
163
|
+
else
|
164
|
+
finish = stop
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
return @stops[0][:color] unless start
|
169
|
+
return @stops[-1][:color] unless finish
|
170
|
+
return finish[:color] if start[:offset] == finish[:offset]
|
171
|
+
rgba = []
|
172
|
+
# walkytalky
|
173
|
+
# http://stackoverflow.com/questions/3017019/non-linear-color-interpolation#answer-3030245
|
174
|
+
for i in (0..3)
|
175
|
+
rgba[i] = (start[:rgba][i] + (offset - start[:offset]) * (finish[:rgba][i] - start[:rgba][i]) / (finish[:offset] - start[:offset])).to_i
|
176
|
+
end
|
177
|
+
ChunkyPNG::Color.rgba(*rgba)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|