inkcite 1.12.1 → 1.13.0

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