inkcite 1.12.1 → 1.13.0

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.
@@ -1,278 +1,83 @@
1
1
  module Inkcite
2
2
  module Renderer
3
- class Snow < ContainerBase
3
+ class Snow < SpecialEffect
4
4
 
5
- # Ambient snow special effect renderer courtesy of
6
- # http://freshinbox.com/blog/ambient-animations-in-email-snow-and-stars/
7
- def render tag, opt, ctx
5
+ protected
8
6
 
9
- return '</div>' if tag == '/snow'
7
+ def config_all_children_style style, sfx
10
8
 
11
- # Get a unique ID for this wrap element.
12
- uid = ctx.unique_id(:snow)
9
+ style[:top] = "-#{px(sfx.max_size + 4)}"
13
10
 
14
- # Total number of flakes to animate
15
- flakes = (opt[:flakes] || 3).to_i
11
+ end
12
+
13
+ def config_child n, child, style, animation, sfx
14
+
15
+ speed = sfx.rand_speed
16
+
17
+ size = sfx.rand_size
18
+ style[:width] = px(size)
19
+ style[:height] = px(size)
20
+ style[BORDER_RADIUS] = px((size / 2.0).round) if sfx.src.blank?
16
21
 
17
- # This is the general class applied to all snow elements within this
18
- # wrapping container.
19
- all_flakes_class = ctx.development? ? "snow#{uid}-flakes" : "s#{uid}fs"
20
- flake_prefix = ctx.development? ? "snow#{uid}-flake" : "s#{uid}f"
21
- anim_prefix = ctx.development? ? "snow#{uid}-anim" : "s#{uid}a"
22
+ opacity = sfx.rand_opacity
23
+ style[:opacity] = opacity if opacity < OPACITY_CEIL
22
24
 
23
- # Grab the min and max sizes for the flakes or inherit default values.
24
- flake_min_size = (opt[FLAKE_SIZE_MIN] || 6).to_i
25
- flake_max_size = (opt[FLAKE_SIZE_MAX] || 18).to_i
25
+ animation.duration = speed
26
+ animation.delay = ((sfx.time / sfx.count) * n).round(1)
27
+ animation.timing_function = Animation::LINEAR
26
28
 
27
- # Grab the min and max speeds for the flakes, the smaller the value the
28
- # faster the flake moves.
29
- flake_min_speed = (opt[FLAKE_SPEED_MIN] || 3).to_f
30
- flake_max_speed = (opt[FLAKE_SPEED_MAX] || 8).to_f
29
+ start_left = sfx.positions_x[n]
31
30
 
32
31
  # Determine the spread of the flakes - the bigger the spread, the larger
33
32
  # the variance between where the flake starts and where it ends.
34
33
  # Measured in %-width of the overall area.
35
- spread = (opt[:spread] || 20).to_i
34
+ spread = sfx.opt[:spread].to_i
36
35
  half_spread = spread / 2.0
37
-
38
- # Determine the opacity variance.
39
- flake_min_opacity = (opt[FLAKE_OPACITY_MIN] || 0.5).to_f
40
- flake_max_opacity = (opt[FLAKE_OPACITY_MAX] || 0.9).to_f
41
-
42
- # Overall time for the initial distribution of flakes.
43
- end_time = (opt[:time] || 4).to_f
44
-
45
- # Setup some ranges for easier random numbering.
46
- size_range = (flake_min_size..flake_max_size)
47
- speed_range = (flake_min_speed..flake_max_speed)
48
36
  spread_range = (-half_spread..half_spread)
49
- opacity_range = (flake_min_opacity..flake_max_opacity)
50
-
51
- # Snowflake color.
52
- color = hex(opt[:color] || '#fff')
53
-
54
- # Check to see if a source image has been specified for the snowflakes.
55
- src = opt[:src]
56
-
57
- # If the image is missing, record an error to the console and
58
- # clear the image allowing the color to take precedence instead.
59
- src = nil if src && !ctx.assert_image_exists(src)
60
-
61
- # Flake rotation, used if an image is present. (Rotating a colored
62
- # circle has no visual distinction.) For convenience this tag accepts
63
- # boolean rotate and rotation
64
- rotation_enabled = src && (opt[:rotate] || opt[:rotation])
65
-
66
- # Initialize the wrap that will hold each of the snowflakes and the
67
- # content within that will have it's
68
- div_wrap = Element.new('div')
69
-
70
- # Resolve the wrapping class name - readable name in development,
71
- # space-saving name in all other environments.
72
- wrap_class = ctx.development? ? "snow#{uid}-wrap" : "s#{uid}w"
73
- div_wrap[:class] = wrap_class
74
-
75
- # Background color gets applied directly to the div so it renders
76
- # consistently in all clients - even those that don't support the
77
- # snow effect.
78
- mix_background div_wrap, opt, ctx
79
-
80
- # Text alignment within the wrapper
81
- mix_text_align div_wrap, opt, ctx
82
-
83
- # Kick things off by rendering the wrapping div.
84
- html = div_wrap.to_s
85
-
86
- # Get the number of flakes that should be included. Create a child div for
87
- # each flake that can be sized, animated uniquely.
88
- flakes.times do |flake|
89
- html << %Q(<div class="#{all_flakes_class} #{flake_prefix}#{flake + 1}"></div>)
90
- end
91
-
92
- # Check to see if there is a height required for the wrap element - otherwise
93
- # the wrap will simply enlarge to hold all of the contents within.
94
- wrap_height = opt[:height].to_i
95
-
96
- # Will hold all of the styles as they're assembled.
97
- style = []
98
-
99
- # True if we're limiting the animation to webkit only. In development
100
- # or in the browser version of the email, the animation should be as
101
- # compatible as possible but in all other cases it should be webkit only.
102
- webkit_only = !(ctx.development? || ctx.browser?)
103
-
104
- # Hide the snow effect from any non-webkit email clients.
105
- style << '@media screen and (-webkit-min-device-pixel-ratio: 0) {' if webkit_only
106
-
107
- # Snow wrapping element in-which the snow flakes will be animated.
108
- style << " .#{wrap_class} {"
109
- style << ' position: relative;'
110
- style << ' overflow: hidden;'
111
- style << ' width: 100%;'
112
- style << " height: #{px(wrap_height)};" if wrap_height > 0
113
- style << ' }'
114
-
115
- # Common attributes for all snowflakes.
116
- style << " .#{all_flakes_class} {"
117
- style << ' position: absolute;'
118
- style << " top: -#{flake_max_size + 4}px;"
119
-
120
- # If no image has been provided, make the background a solid color
121
- # otherwise set the background to the image source and fill the
122
- # available space.
123
- if src.blank?
124
- style << " background-color: #{color};"
125
- else
126
- style << " background-image: url(#{ctx.image_url(src)});"
127
- style << " background-size: 100%;"
128
- end
129
-
130
- style << ' }'
131
-
132
- # Space the snowflakes generally equally across the width of the
133
- # container div. Random distribution sometimes ends up with
134
- # snowflakes clumped at one edge or the other.
135
- flake_spacing = 100 / flakes.to_f
136
-
137
- # Now build up a pool of equally-spaced starting positions.
138
- # TODO: This is probably a perfect spot to use inject()
139
- start_left = flake_spacing / 2.0
140
- start_positions = [start_left]
141
- (flakes - 1).times { |f| start_positions << start_left += flake_spacing }
142
37
 
143
- # Randomize the starting positions - otherwise they draw right-to-left
144
- # as starting positions are popped from the pool.
145
- start_positions.shuffle!
146
-
147
- # Snowflakes will be dispersed equally across the total time
148
- # of the animation making for a smoother, more balanced show.
149
- start_interval = end_time / flakes.to_f
150
- start_time = 0
151
-
152
- # Will hold the sizes of each flake and will be used to offset their
153
- # initial starting position in the animation section.
154
- flake_sizes = {}
155
-
156
- # Now add individual class definitions for each flake with unique size,
157
- # speed and starting position. Also add the animation trigger that loops
158
- # infinitely, starts at a random time and uses a random speed to completion.
159
- flakes.times do |flake|
160
-
161
- speed = rand(speed_range).round(1)
162
- size = flake_sizes[flake] = rand(size_range)
163
-
164
- opacity = rand(opacity_range).round(1)
165
- if opacity < OPACITY_FLOOR
166
- opacity = OPACITY_FLOOR
167
- elsif opacity > OPACITY_CEIL
168
- opacity = OPACITY_CEIL
169
- end
170
-
171
- style << " .#{flake_prefix}#{flake + 1} {"
172
- style << " height: #{px(size)};"
173
- style << " width: #{px(size)};"
174
-
175
- # Only apply a border radius if the snowflake lacks an image.
176
- style << " border-radius: #{px((size / 2.0).round)};" unless src
177
-
178
- style << " opacity: #{opacity};" if opacity < OPACITY_CEIL
179
- style << Animation.with_browser_prefixes("animation: #{anim_prefix}#{flake + 1} #{speed}s linear #{start_time.round(1)}s infinite;", ctx)
180
- style << ' }'
181
-
182
- start_time += start_interval
38
+ # Randomly choose where the snow will end its animation. Prevent
39
+ # it from going outside of the container.
40
+ end_left = (start_left + rand(spread_range)).round
41
+ if end_left < POSITION_FLOOR
42
+ end_left = POSITION_FLOOR
43
+ elsif end_left > POSITION_CEIL
44
+ end_left = POSITION_CEIL
183
45
  end
184
46
 
185
- # Declare each of the flake animations.
186
- flakes.times do |flake|
187
-
188
- start_left = start_positions.pop
189
-
190
- # Randomly choose where the snow will end its animation. Prevent
191
- # it from going outside of the container.
192
- end_left = (start_left + rand(spread_range)).round
193
- if end_left < POSITION_FLOOR
194
- end_left = POSITION_FLOOR
195
- elsif end_left > POSITION_CEIL
196
- end_left = POSITION_CEIL
197
- end
198
-
199
- # Calculate the ending rotation for the flake, if rotation is enabled.
200
- end_rotation = rotation_enabled ? rand(ROTATION_RANGE) : 0
47
+ # Calculate the ending rotation for the flake, if rotation is enabled.
48
+ end_rotation = sfx.rotation?? sfx.rand_rotation : 0
201
49
 
202
- keyframes = Animation::Keyframes.new("#{anim_prefix}#{flake + 1}", ctx)
50
+ # Start above the div area
51
+ animation.add_keyframe(0, { :top => px(-size), :left => pct(start_left.round) })
203
52
 
204
- # Start above the div area
205
- keyframes.add_keyframe(0, :top => px(-flake_sizes[flake]), :left => pct(start_left.round))
206
-
207
- # End below the div area, applying rotation if necessary.
208
- keyframe = Animation::Keyframe.new(100, :top => '100%', :left => pct(end_left))
209
- keyframe.add_with_prefixes(:transform, "rotate(#{end_rotation}deg)", ctx) if end_rotation != 0
210
- keyframes << keyframe
211
-
212
- style << keyframes.to_s
213
-
214
- end
215
-
216
- style << '}' if webkit_only
217
-
218
- ctx.styles << style.join("\n")
219
-
220
- html
53
+ # End below the div area, applying rotation if necessary.
54
+ keyframe = animation.add_keyframe(100, { :top => '100%', :left => pct(end_left) })
55
+ keyframe[:transform] = "rotate(#{end_rotation}deg)" if end_rotation != 0
221
56
 
222
57
  end
223
58
 
224
- private
225
-
226
- # Renders the CSS with the appropriate browser prefixes based
227
- # on whether or not this version of the email is webkit only.
228
- def with_browser_prefixes indentation, css, webkit_only, separator="\n"
229
-
230
- # Determine which prefixes will be applied.
231
- browser_prefixes = webkit_only ? WEBKIT_BROWSERS : ALL_BROWSERS
232
-
233
- # This will hold the completed CSS with all prefixes applied.
234
- _css = ''
59
+ def config_effect_context sfx
235
60
 
236
- # Iterate through the prefixes and apply them with the indentation
237
- # and CSS declaration with line breaks.
238
- browser_prefixes.each do |prefix|
239
- _css << indentation
240
- _css << prefix
241
- _css << css
242
- _css << separator
243
- end
61
+ # Shuffle the x-positions so that the snowflakes fall across
62
+ # the container's width in random order.
63
+ sfx.positions_x.shuffle!
244
64
 
245
- _css
246
65
  end
247
66
 
248
- # Size constraints on the flakes.
249
- FLAKE_SIZE_MIN = :'min-size'
250
- FLAKE_SIZE_MAX = :'max-size'
251
-
252
- # Speed constraints on the flakes.
253
- FLAKE_SPEED_MIN = :'min-speed'
254
- FLAKE_SPEED_MAX = :'max-speed'
255
-
256
- # Opacity constraints.
257
- FLAKE_OPACITY_MIN = :'min-opacity'
258
- FLAKE_OPACITY_MAX = :'max-opacity'
259
- OPACITY_FLOOR = 0.2
260
- OPACITY_CEIL = 1.0
261
-
262
- # Rotation angles when image rotation is enabled.
263
- ROTATION_RANGE = (-270..270)
264
-
265
- # Position min and max preventing snow flakes
266
- # from leaving the bounds of the container.
267
- POSITION_FLOOR = 0
268
- POSITION_CEIL = 100
269
-
270
- # Static arrays with browser prefixes. Turns out that
271
- # Firefox, IE and Opera don't require a prefix so to
272
- # target everything we need the non-prefixed version
273
- # (hence the blank entry) plus the webkit prefix.
274
- WEBKIT_BROWSERS = ['-webkit-']
275
- ALL_BROWSERS = [''] + WEBKIT_BROWSERS
67
+ def defaults opt, ctx
68
+ {
69
+ :color => '#fff',
70
+ :count => 3,
71
+ SIZE_MIN => 6,
72
+ SIZE_MAX => 18,
73
+ SPEED_MIN => 3,
74
+ SPEED_MAX => 8,
75
+ :spread => 20,
76
+ OPACITY_MIN => 0.5,
77
+ OPACITY_MAX => 0.9,
78
+ :time => 4
79
+ }
80
+ end
276
81
 
277
82
  end
278
83
 
@@ -0,0 +1,77 @@
1
+ module Inkcite
2
+ module Renderer
3
+ class Sparkle < SpecialEffect
4
+
5
+ protected
6
+
7
+ def config_child n, child, style, animation, sfx
8
+
9
+ # Random size
10
+ size = sfx.rand_size
11
+ style[:height] = px(size)
12
+ style[:width] = px(size)
13
+ style[BORDER_RADIUS] = px((size / 2).round) unless sfx.src
14
+
15
+ # Random opacity
16
+ opacity = sfx.rand_opacity
17
+ style[:opacity] = opacity if opacity < OPACITY_CEIL
18
+
19
+ # Random position
20
+ style[:top] = pct(sfx.positions_y[n].round(0))
21
+ style[:left] = pct(sfx.positions_x[n].round(0))
22
+
23
+ # Calculate the ending rotation for the flake, if rotation is enabled.
24
+ end_rotation = sfx.rotation? ? sfx.rand_rotation : 0
25
+ half_rotation = (end_rotation / 2.0).round(1)
26
+
27
+ #animation.timing_function = Animation::EASE
28
+
29
+ speed = sfx.rand_speed
30
+
31
+ max_delay = speed * 0.1
32
+ delay = rand(max_delay).round(1)
33
+ animation.duration = (speed + delay).round(1)
34
+
35
+ delay_percent = 100 - ((delay / animation.duration) * 100).round(0)
36
+ midpoint_percent = (delay_percent / 2.0).round(0)
37
+
38
+ # Start above the div area
39
+ keyframe = animation.add_keyframe(0, { :transform => 'scale(0.0)' })
40
+ keyframe.append(:transform, "rotate(0deg)") if half_rotation != 0
41
+
42
+ keyframe = animation.add_keyframe(midpoint_percent, { :transform => 'scale(1.0)' })
43
+ keyframe.append(:transform, "rotate(#{half_rotation}deg)") if half_rotation != 0
44
+
45
+ keyframe = animation.add_keyframe(delay_percent, { :transform => 'scale(0.0)' })
46
+ keyframe.append(:transform, "rotate(#{end_rotation}deg)") if end_rotation != 0
47
+
48
+ keyframe = animation.add_keyframe(100, { :transform => 'scale(0.0)' })
49
+ keyframe.append(:transform, "rotate(#{end_rotation}deg)") if end_rotation != 0
50
+
51
+ end
52
+
53
+ def config_effect_context sfx
54
+
55
+ # Randomly shuffle the x- and y-positions for the sparkles.
56
+ sfx.positions_x.shuffle!
57
+ sfx.positions_y.shuffle!
58
+
59
+ end
60
+
61
+ def defaults opt, ctx
62
+ {
63
+ :count => 6,
64
+ :color => '#fff',
65
+ OPACITY_MIN => 0.33,
66
+ OPACITY_MAX => 1.0,
67
+ SIZE_MIN => 6,
68
+ SIZE_MAX => 24,
69
+ SPEED_MIN => 0.5,
70
+ SPEED_MAX => 2.0,
71
+ :time => 4
72
+ }
73
+ end
74
+
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,429 @@
1
+ module Inkcite
2
+ module Renderer
3
+ class SpecialEffect < ContainerBase
4
+
5
+ # A convenience class for accessing the attributes that are
6
+ # common to special effects like snow and sparkle.
7
+ class EffectContext
8
+
9
+ attr_reader :uuid
10
+
11
+ # Expose the opt and ctx attributes
12
+ attr_reader :opt, :ctx
13
+
14
+ # Allow sfx to be treated equivalent to the options map it wraps.
15
+ # This makes it easy for the consumer to set custom state into
16
+ # the context (during config_effect_context) and then use those
17
+ # values during the rendering methods.
18
+ delegate :[], :[]=, :to => :opt
19
+
20
+ def initialize tag, opt, ctx, defaults={}
21
+
22
+ @tag = tag
23
+
24
+ # Merge the provided opts over the defaults to ensure the values
25
+ # provided by the developer take precedence.
26
+ @opt = defaults.merge(opt)
27
+ @ctx = ctx
28
+
29
+ # Request a unique ID for this special effect which will be used
30
+ # to uniquely identify the classes and animations associated with
31
+ # this special effect.
32
+ @uuid = ctx.unique_id(:sfx)
33
+
34
+ end
35
+
36
+ def all_children_class_name
37
+ obfuscate_class_names? ? "sfx#{@uuid}c" : "#{@tag}#{@uuid}-children"
38
+ end
39
+
40
+ def animation_class_name child_index
41
+ obfuscate_class_names? ? "#{@tag}#{@uuid}-anim#{child_index + 1}" : "sfx#{@uuid}a#{child_index + 1}"
42
+ end
43
+
44
+ def child_class_name child_index
45
+ obfuscate_class_names? ? "sfx#{@uuid}c#{child_index + 1}" : "#{@tag}#{@uuid}-child#{child_index + 1}"
46
+ end
47
+
48
+ def color
49
+ Renderer.hex(@opt[:color])
50
+ end
51
+
52
+ # Returns the number of children in the special effects - e.g. the number
53
+ # of snowflakes or sparkles in the animation. :flakes and :sparks are
54
+ # technically deprecated.
55
+ def count
56
+ (@opt[:sparks] || @opt[:flakes] || @opt[:count]).to_i
57
+ end
58
+
59
+ def equal_distribution qty
60
+
61
+ # Space the children generally equally across the width of the
62
+ # container div. Random distribution sometimes ends up with
63
+ # children clumped at one edge or the other.
64
+ spacing = POSITION_CEIL / qty.to_f
65
+
66
+ # Now build up a pool of equally-spaced starting positions.
67
+ # TODO: This is probably a perfect spot to use inject()
68
+ start_left = spacing / 2.0
69
+
70
+ # This array will hold all of the positions
71
+ positions = [start_left]
72
+
73
+ # Now, for the remaining positions needed, adjust by the
74
+ # spacing and push onto the list.
75
+ (qty - 1).times { |f| positions << start_left += spacing }
76
+
77
+ positions
78
+ end
79
+
80
+ def height
81
+ @opt[:height].to_i
82
+ end
83
+
84
+ def max_opacity
85
+ @opt[OPACITY_MAX].to_f
86
+ end
87
+
88
+ def max_size
89
+ @opt[SIZE_MAX].to_i
90
+ end
91
+
92
+ def min_opacity
93
+ @opt[OPACITY_MIN].to_f
94
+ end
95
+
96
+ def max_speed
97
+ @opt[SPEED_MAX].to_f
98
+ end
99
+
100
+ def min_size
101
+ @opt[SIZE_MIN].to_i
102
+ end
103
+
104
+ def min_speed
105
+ @opt[SPEED_MIN].to_f
106
+ end
107
+
108
+ def obfuscate_class_names?
109
+ false && @ctx.production?
110
+ end
111
+
112
+ def opacity_range
113
+ (min_opacity..max_opacity)
114
+ end
115
+
116
+ def rand_opacity
117
+ rand(opacity_range).round(1)
118
+ end
119
+
120
+ def rand_rotation
121
+ rand(rotation_range).round(1)
122
+ end
123
+
124
+ def rand_size
125
+ rand(size_range).round(0)
126
+ end
127
+
128
+ def rand_speed
129
+ rand(speed_range).round(1)
130
+ end
131
+
132
+ def rotation?
133
+ @opt[:rotate] || @opt[:rotation]
134
+ end
135
+
136
+ def rotation_range
137
+ (-270..270)
138
+ end
139
+
140
+ def size_range
141
+ (min_size..max_size)
142
+ end
143
+
144
+ def speed_range
145
+ (min_speed..max_speed)
146
+ end
147
+
148
+ def src
149
+ return @src if defined?(@src)
150
+
151
+ # Check to see if a source image has been specified for the snowflakes.
152
+ @src = @opt[:src]
153
+
154
+ # Release the image name if one has been provided but doesn't exist in
155
+ # the project - this will cause the special effect to default to the
156
+ # non-image default behavior.
157
+ @src = nil if @src && !@ctx.assert_image_exists(@src)
158
+
159
+ @src
160
+ end
161
+
162
+ # Creates a permanent list of positions (as percentages of the wrap container's
163
+ # total width) which can be used for starting or ending position to equally
164
+ # space animated elements.
165
+ def positions_x
166
+ @positions_x ||= equal_distribution(count)
167
+ end
168
+
169
+ # Creates a permanent list of positions (as percentages of the wrap container's
170
+ # total height) which can be used for starting or ending position to equally
171
+ # space animated elements.
172
+ def positions_y
173
+ @positions_y ||= equal_distribution(count)
174
+ end
175
+
176
+ def start_time child_index
177
+ ((time / count) * child_index).round(1)
178
+ end
179
+
180
+ def time
181
+ @opt[:time].to_f
182
+ end
183
+
184
+ # Amount of time between each child starting its animation to create a
185
+ # even but varied distribution.
186
+ def time_interval
187
+ time / count.to_f
188
+ end
189
+
190
+ # Returns true if CSS animations should be limited to webkit-
191
+ # powered email clients.
192
+ def webkit_only?
193
+ !(@ctx.development? || @ctx.browser?)
194
+ end
195
+
196
+ def wrap_class_name
197
+ obfuscate_class_names? ? "sfx#{@uuid}w" : "#{@tag}#{@uuid}-wrap"
198
+ end
199
+
200
+ end
201
+
202
+ public
203
+
204
+ def render tag, opt, ctx
205
+
206
+ # If the closing tag was received (e.g. /snow) then close the wrapper
207
+ # div that was rendered by the opening tag.
208
+ return '</div>' if tag.start_with?('/')
209
+
210
+ # Retrieve the special effects default values (times, number of units, etc.)
211
+ _defaults = defaults(opt, ctx)
212
+
213
+ # Create a special effects context that simplifies working with the
214
+ # opts, defaults and manages the styles/classes necessary to animate
215
+ # the special effect.
216
+ sfx = EffectContext.new(tag, opt, ctx, _defaults)
217
+
218
+ # Provide the extending class with an opportunity to configure the
219
+ # effect context prior to any rendering.
220
+ config_effect_context sfx
221
+
222
+ html = []
223
+ styles = []
224
+
225
+ # If this is the first special effect to be included in the email
226
+ # we need to disable the CSS animation from Gmail - which only
227
+ # accepts part of its <styles> leading to unexpected whitespace.
228
+ # By putting this invalid CSS into the <style> block, Gmail's
229
+ # very strict parser will exclude the entire block, preventing
230
+ # the animation from running.
231
+ # https://emails.hteumeuleu.com/troubleshooting-gmails-responsive-design-support-ad124178bf81#.8jh1vn9mw
232
+ if ctx.email? && sfx.uuid == 1
233
+ styles << Inkcite::Renderer::Style.new(".gmail-fix", sfx.ctx, { FONT_SIZE => '3*px' })
234
+ end
235
+
236
+ # Create the <div> that wraps the entire animation.
237
+ create_wrap_element html, sfx
238
+
239
+ # Create the Style that defines the look of the wrapping container
240
+ create_wrap_style styles, sfx
241
+
242
+ # Create the Style that is applied to all children in the animation.
243
+ create_all_children_style styles, sfx
244
+
245
+ # Now create each of the child elements (e.g. the snowflakes) that
246
+ # will be animated in this effect. Each child is created and animated
247
+ # at the same time.
248
+ create_child_elements html, styles, sfx
249
+
250
+ # Push the completed list of styles into the context's stack.
251
+ ctx.styles << styles.join("\n")
252
+
253
+ html.join("\n")
254
+
255
+ end
256
+
257
+ protected
258
+
259
+ # Position min and max preventing animated elements
260
+ # from leaving the bounds of the container.
261
+ POSITION_FLOOR = 0
262
+ POSITION_CEIL = 100
263
+
264
+ # Size constraints on the animated children.
265
+ SIZE_MIN = :'min-size'
266
+ SIZE_MAX = :'max-size'
267
+
268
+ # Speed constraints on the children.
269
+ SPEED_MIN = :'min-speed'
270
+ SPEED_MAX = :'max-speed'
271
+
272
+ # Opacity constraints on the children
273
+ OPACITY_MIN = :'min-opacity'
274
+ OPACITY_MAX = :'max-opacity'
275
+ OPACITY_CEIL = 1.0
276
+
277
+ # The extending class can override this method to perform any
278
+ # additional configuration on the style that affects all
279
+ # children in the animation.
280
+ def config_all_children_style style, sfx
281
+ # This space left intentionally blank
282
+ end
283
+
284
+ # The extending class must implement this method to customize
285
+ # and animate each child.
286
+ def config_child n, child, style, animation, sfx
287
+ raise 'Classes extending SpecialEffect must implement defaults(child, style, animation, keyframes, sfx)'
288
+ end
289
+
290
+ # The extending class can implement this method to customize
291
+ # the EffectContext prior to any HTML or CSS generation.
292
+ def config_effect_context sfx
293
+ # This space left intentionally blank
294
+ end
295
+
296
+ # The extending class can override this method to perform any
297
+ # additional configuration on the <div> that wraps the entire
298
+ # animation.
299
+ def config_wrap_element div, sfx
300
+ # This space left intentionally blank
301
+ end
302
+
303
+ # The extending class can override this method to customize
304
+ # the wrap <div>'s style.
305
+ def config_wrap_style style, sfx
306
+ # This space left intentionally blank
307
+ end
308
+
309
+ # The extending class must override this method and return the defaults
310
+ # for the special effect as a map.
311
+ def defaults opt, ctx
312
+ raise 'Classes extending SpecialEffect must implement defaults(opt, ctx)'
313
+ end
314
+
315
+ private
316
+
317
+ # Creates the Style that applies to /all/ children.
318
+ def create_all_children_style styles, sfx
319
+
320
+ style = Inkcite::Renderer::Style.new(".#{sfx.all_children_class_name}", sfx.ctx, { :position => :absolute })
321
+
322
+ # If no image has been provided, make the background a solid color
323
+ # otherwise set the background to the image source and fill the
324
+ # available space.
325
+ src = sfx.src
326
+ if src.blank?
327
+ color = sfx.color
328
+ style[BACKGROUND_COLOR] = color unless none?(color)
329
+ else
330
+ style[BACKGROUND_IMAGE] = "url(#{sfx.ctx.image_url(src)})"
331
+ style[BACKGROUND_SIZE] = '100%'
332
+ end
333
+
334
+ # Provide the extending class with a chance to apply additional
335
+ # styles to all children.
336
+ config_all_children_style style, sfx
337
+
338
+ styles << style
339
+
340
+ end
341
+
342
+ # Creates n-number of child <div> objects and assigns the all-child and each-child
343
+ # CSS classes to them allowing each to be sized, animated uniquely.
344
+ def create_child_elements html, styles, sfx
345
+
346
+ # Will hold all of the Animations while the children are
347
+ # assembled and then added to the styles array at the end
348
+ # so that all keyframes are together in the source.
349
+ animations = []
350
+
351
+ sfx.count.times do |n|
352
+
353
+ child_class_name = sfx.child_class_name(n)
354
+
355
+ # This is the child HTML element
356
+ child = Inkcite::Renderer::Element.new('div', { :class => quote("#{sfx.all_children_class_name} #{child_class_name}") })
357
+
358
+ # This is the custom style to be applied to the child.
359
+ style = Inkcite::Renderer::Style.new(".#{child_class_name}", sfx.ctx)
360
+
361
+ # This is the animation declaration (timing, duration, etc.) for this child.
362
+ animation = Inkcite::Animation.new(sfx.animation_class_name(n), sfx.ctx)
363
+
364
+ # Provide the extending class with a chance to configure the child
365
+ # and its style.
366
+ config_child n, child, style, animation, sfx
367
+
368
+ # Now that it is configured, install the animation into the
369
+ # style - this adds itself with the appropriate browser prefixes.
370
+ style[:animation] = animation
371
+
372
+ # Inject the various pieces into the appropriate lists.
373
+ html << child.to_s + '</div>'
374
+ styles << style
375
+ animations << animation
376
+
377
+ end
378
+
379
+ # Append all of the Keyframes to the end of the styles, now that
380
+ # the individual children are configured.
381
+ animations.each { |a| styles << a.to_keyframe_css }
382
+
383
+ end
384
+
385
+ # Creates the <div> that wraps the entire animation. The children of this container
386
+ # will be animated based on the parameters of the effect.
387
+ def create_wrap_element html, sfx
388
+
389
+ # Initialize the wrap that will hold each of the children and wraps the content
390
+ # over which the special effect will animate.
391
+ div = Inkcite::Renderer::Element.new('div', { :class => quote(sfx.wrap_class_name) })
392
+
393
+ # Background color gets applied directly to the div so it renders consistently
394
+ # in all clients - even those that don't support special effects.
395
+ mix_background div, sfx.opt, sfx.ctx
396
+
397
+ # Text alignment within the wrapper
398
+ mix_text_align div, sfx.opt, sfx.ctx
399
+
400
+ # Provide the extending class with a chance to make additional
401
+ # configuration changes to the wrap element.
402
+ config_wrap_element div, sfx
403
+
404
+ html << div.to_s
405
+
406
+ end
407
+
408
+ def create_wrap_style styles, sfx
409
+
410
+ # Initialize the class declaration that will be applied to the
411
+ # wrapping container.
412
+ style = Inkcite::Renderer::Style.new(".#{sfx.wrap_class_name}", sfx.ctx, { :position => :relative, :overflow => :hidden, :width => '100%' })
413
+
414
+ # If a specific height has been specified for the wrap class, add
415
+ # it to the style.
416
+ height = sfx.height
417
+ style[:height] = px(height) if height > 0
418
+
419
+ # Provide the extending class with a chance to do any additional
420
+ # customization to this style.
421
+ config_wrap_style style, sfx
422
+
423
+ styles << style
424
+
425
+ end
426
+
427
+ end
428
+ end
429
+ end