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