compass-magick 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|