compass-magick 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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