inkcite 1.12.1 → 1.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/inkcite/animation.rb +96 -74
- data/lib/inkcite/cli/base.rb +6 -0
- data/lib/inkcite/cli/preview.rb +1 -1
- data/lib/inkcite/cli/test.rb +3 -3
- data/lib/inkcite/renderer.rb +7 -3
- data/lib/inkcite/renderer/background.rb +153 -0
- data/lib/inkcite/renderer/base.rb +58 -29
- data/lib/inkcite/renderer/container_base.rb +11 -4
- data/lib/inkcite/renderer/div.rb +1 -2
- data/lib/inkcite/renderer/element.rb +6 -7
- data/lib/inkcite/renderer/responsive.rb +124 -37
- data/lib/inkcite/renderer/snow.rb +53 -248
- data/lib/inkcite/renderer/sparkle.rb +77 -0
- data/lib/inkcite/renderer/special_effect.rb +429 -0
- data/lib/inkcite/renderer/style.rb +81 -0
- data/lib/inkcite/renderer/table_base.rb +4 -12
- data/lib/inkcite/renderer/td.rb +6 -24
- data/lib/inkcite/renderer/video_preview.rb +17 -7
- data/lib/inkcite/version.rb +1 -1
- data/lib/inkcite/view.rb +53 -18
- data/lib/inkcite/view/media_query.rb +1 -1
- data/test/animation_spec.rb +14 -10
- data/test/renderer/background_spec.rb +59 -0
- data/test/renderer/div_spec.rb +11 -1
- data/test/renderer/image_spec.rb +1 -1
- data/test/renderer/mobile_image_spec.rb +3 -3
- data/test/renderer/mobile_style_spec.rb +1 -1
- data/test/renderer/span_spec.rb +1 -1
- data/test/renderer/table_spec.rb +22 -7
- data/test/renderer/td_spec.rb +29 -8
- data/test/renderer/video_preview_spec.rb +3 -3
- metadata +8 -5
- data/lib/inkcite/renderer/outlook_background.rb +0 -96
- data/test/renderer/outlook_background_spec.rb +0 -61
@@ -1,278 +1,83 @@
|
|
1
1
|
module Inkcite
|
2
2
|
module Renderer
|
3
|
-
class Snow <
|
3
|
+
class Snow < SpecialEffect
|
4
4
|
|
5
|
-
|
6
|
-
# http://freshinbox.com/blog/ambient-animations-in-email-snow-and-stars/
|
7
|
-
def render tag, opt, ctx
|
5
|
+
protected
|
8
6
|
|
9
|
-
|
7
|
+
def config_all_children_style style, sfx
|
10
8
|
|
11
|
-
|
12
|
-
uid = ctx.unique_id(:snow)
|
9
|
+
style[:top] = "-#{px(sfx.max_size + 4)}"
|
13
10
|
|
14
|
-
|
15
|
-
|
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
|
-
|
18
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
25
|
+
animation.duration = speed
|
26
|
+
animation.delay = ((sfx.time / sfx.count) * n).round(1)
|
27
|
+
animation.timing_function = Animation::LINEAR
|
26
28
|
|
27
|
-
|
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 =
|
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
|
-
#
|
144
|
-
#
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
-
#
|
186
|
-
|
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
|
-
|
50
|
+
# Start above the div area
|
51
|
+
animation.add_keyframe(0, { :top => px(-size), :left => pct(start_left.round) })
|
203
52
|
|
204
|
-
|
205
|
-
|
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
|
-
|
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
|
-
#
|
237
|
-
#
|
238
|
-
|
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
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
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
|