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.
@@ -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