inkcite 1.13.0 → 1.14.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -22,7 +22,7 @@ module Inkcite
22
22
  # Provide the block with the area that was matched, sans wrapper brackets.
23
23
  # Replace the brackets and original value with the block's results.
24
24
  result = yield(match[1].to_s) || EMPTY_STRING
25
- str[offset.first, offset.last - offset.first] = result
25
+ str[offset.first, offset.last - offset.first] = result.to_s
26
26
 
27
27
  # Ensure we don't infinite loop.
28
28
  failsafe += 1
@@ -1,15 +1,14 @@
1
1
  require_relative 'renderer/base'
2
- require_relative 'renderer/element'
3
2
  require_relative 'renderer/responsive'
4
3
  require_relative 'renderer/container_base'
5
4
  require_relative 'renderer/special_effect'
6
5
  require_relative 'renderer/image_base'
7
6
  require_relative 'renderer/table_base'
8
- require_relative 'renderer/style'
9
7
 
10
8
  require_relative 'renderer/background'
11
9
  require_relative 'renderer/button'
12
10
  require_relative 'renderer/div'
11
+ require_relative 'renderer/fireworks'
13
12
  require_relative 'renderer/footnote'
14
13
  require_relative 'renderer/google_analytics'
15
14
  require_relative 'renderer/image'
@@ -174,6 +173,7 @@ module Inkcite
174
173
  :button => Button.new,
175
174
  :div => Div.new,
176
175
  :facebook => Social::Facebook.new,
176
+ :fireworks => Fireworks.new,
177
177
  :footnote => Footnote.new,
178
178
  :footnotes => Footnotes.new,
179
179
  :google => GoogleAnalytics.new,
@@ -145,7 +145,7 @@ module Inkcite
145
145
  BACKGROUND_POSITION, :border, BORDER_BOTTOM, BORDER_LEFT, BORDER_RADIUS, BORDER_RIGHT,
146
146
  BORDER_SPACING, BORDER_TOP, :mobile, MOBILE_BGCOLOR, MOBILE_BACKGROUND, MOBILE_BACKGROUND_COLOR,
147
147
  MOBILE_BACKGROUND_IMAGE, MOBILE_BACKGROUND_REPEAT, MOBILE_BACKGROUND_POSITION, MOBILE_PADDING,
148
- MOBILE_SRC, MOBILE_BACKGROUND_SIZE
148
+ MOBILE_SRC, MOBILE_BACKGROUND_SIZE, MOBILE_WIDTH
149
149
  ]
150
150
 
151
151
  end
@@ -168,7 +168,7 @@ module Inkcite
168
168
  dir_opt_key = add_directional_suffix(opt_key, dir)
169
169
  dir_css_key = add_directional_suffix(css_key, dir)
170
170
  value = opt[dir_opt_key]
171
- next if none?(value)
171
+ next if value.blank?
172
172
 
173
173
  value = px(value) if as_px
174
174
  into[dir_css_key] = value
@@ -18,10 +18,6 @@ module Inkcite
18
18
  padding = opt[:padding]
19
19
  element.style[:padding] = px(padding) unless none?(padding)
20
20
 
21
- # Supports custom padding on mobile - e.g. mobile-padding="15px 5px"
22
- mobile_padding = opt[MOBILE_PADDING]
23
- element.mobile_style[:padding] = px(mobile_padding) unless none?(mobile_padding)
24
-
25
21
  # Vertical alignment - top, middle, bottom.
26
22
  valign = opt[:valign]
27
23
  element.style[VERTICAL_ALIGN] = valign unless none?(valign)
@@ -33,6 +29,9 @@ module Inkcite
33
29
  # style to the element.
34
30
  element.style[WHITE_SPACE] = :nowrap if opt[:nowrap]
35
31
 
32
+ # Support for mobile-padding and mobile-padding-(direction)
33
+ mix_mobile_padding element, opt, ctx
34
+
36
35
  mix_responsive element, opt, ctx
37
36
 
38
37
  element.to_s
@@ -0,0 +1,231 @@
1
+ module Inkcite
2
+ module Renderer
3
+
4
+ # Animated CSS fireworks special effect based on the technique
5
+ # by Eddie Lin https://codepen.io/yshlin/pen/ylDEk
6
+ class Fireworks < SpecialEffect
7
+
8
+ private
9
+
10
+ # Each firework will move this many positions during the
11
+ # entire animation.
12
+ TOTAL_POSITIONS = 5
13
+
14
+ # The number of hue degrees to randomly alter the hue of each spark
15
+ HUE_RANGE = :'hue-range'
16
+
17
+ # Names of the attributes controlling min and max explosion size.
18
+ RADIUS_MIN = :'min-radius'
19
+ RADIUS_MAX = :'max-radius'
20
+
21
+ # Attributes used for color generation
22
+ SATURATION = 100
23
+ LUMINANCE = 50
24
+
25
+ def config_effect_context sfx
26
+
27
+ count = sfx.count
28
+
29
+ # Total number of firework instances multiplied by the number
30
+ # of positions each firework will cycle through.
31
+ position_count = TOTAL_POSITIONS * count
32
+ positions = sfx.equal_distribution(sfx.position_range, position_count)
33
+
34
+ sfx[:x_positions] = positions
35
+ sfx[:y_positions] = positions.dup
36
+
37
+ # Equal distribution of hues based on the number of fireworks
38
+ # if the rainbow option is selected
39
+ sfx[:hues] = sfx.equal_distribution(0..360, count)
40
+
41
+ end
42
+
43
+ def create_explosion_animation n, hue, duration, delay, sfx
44
+
45
+ # Calculate the radius size for this explosion
46
+ min_radius = sfx[RADIUS_MIN].to_i
47
+ max_radius = sfx[RADIUS_MAX].to_i
48
+ radius_range = (min_radius..max_radius)
49
+ radius = rand(radius_range).round(0)
50
+ half_radius = radius / 2.0
51
+
52
+ hue_range = (sfx[HUE_RANGE] || 40).to_i
53
+ hue_range_2x = hue_range * 2
54
+
55
+ sparks = sfx[:sparks].to_i
56
+
57
+ box_shadow = sparks.times.collect do |n|
58
+
59
+ # Pick a random position for this spark to move to
60
+ x = (rand(radius) - half_radius).round(0)
61
+ y = (rand(radius) - half_radius).round(0)
62
+
63
+ # Randomly pick a slightly different hue for this spark
64
+ _hue = hue + hue_range - rand(hue_range_2x)
65
+ color = Inkcite::Util::hsl_to_color(_hue, SATURATION, LUMINANCE)
66
+
67
+ "#{px(x)} #{px(y)} #{color}"
68
+ end
69
+
70
+ anim = Inkcite::Animation.new(sfx.animation_class_name(n, 'bang'), sfx.ctx)
71
+ anim.duration = duration
72
+ anim.delay = delay if delay > 0
73
+ anim.timing_function = Inkcite::Animation::EASE_OUT
74
+ anim.add_keyframe 100, { BOX_SHADOW => box_shadow.join(', ') }
75
+
76
+ sfx.animations << anim
77
+
78
+ anim
79
+ end
80
+
81
+ def create_gravity_animation sfx
82
+
83
+ anim = Animation.new('gravity', sfx.ctx)
84
+
85
+ # All fireworks fade to zero opacity and size by the end of the gravity cycle.
86
+ keyframe = anim.add_keyframe 100, { :opacity => 0, :width => 0, :height => 0 }
87
+
88
+ # Check to see if gravity has been specified for the fireworks. If so
89
+ # apply it as a vertical translation (positive equals downward)
90
+ gravity = sfx[:gravity].to_i
91
+ keyframe[:transform] = "translateY(#{px(gravity)})" if gravity != 0
92
+
93
+ sfx.animations << anim
94
+
95
+ end
96
+
97
+ def create_position_animation n, duration, delay, sfx
98
+
99
+ # This is the percentage amount of the total animation that will
100
+ # be spent in each position.
101
+ keyframe_duration = (100 / TOTAL_POSITIONS.to_f)
102
+
103
+ anim = Inkcite::Animation.new(sfx.animation_class_name(n, 'position'), sfx.ctx)
104
+ anim.duration = duration
105
+ anim.delay = delay if delay > 0
106
+ anim.timing_function = Inkcite::Animation::LINEAR
107
+
108
+ x_positions = sfx[:x_positions]
109
+ y_positions = sfx[:y_positions]
110
+
111
+ percent = 0
112
+ TOTAL_POSITIONS.times do |n|
113
+
114
+ # Pick a random position for this firework
115
+ top = y_positions.delete_at(rand(y_positions.length))
116
+ left = x_positions.delete_at(rand(x_positions.length))
117
+
118
+ keyframe = anim.add_keyframe(percent, { :top => pct(top), :left => pct(left) })
119
+ keyframe.duration = keyframe_duration - 0.1
120
+
121
+ percent += keyframe_duration
122
+ end
123
+
124
+ sfx.animations << anim
125
+
126
+ anim
127
+ end
128
+
129
+ protected
130
+
131
+ def config_all_children style, sfx
132
+
133
+ # If all of the sparks in the firework have the same size
134
+ # (e.g. min-size equals max-size) then save some CSS space
135
+ # by defining it once for all children.
136
+ if sfx.same_size?
137
+ style[:width] = px(sfx.min_size)
138
+ style[:height] = px(sfx.min_size)
139
+ end
140
+
141
+ # Make sure all explosions start off-screen.
142
+ style[:top] = "-#{px(sfx.max_size)}"
143
+
144
+ style[BORDER_RADIUS] = '50%'
145
+
146
+ sparks = sfx[:sparks].to_i
147
+
148
+ # All sparks start with a box shadow at their exact center,
149
+ # all in white.
150
+ box_shadow = sparks.times.collect { |n| '0 0 #fff' }
151
+ style[BOX_SHADOW] = box_shadow.join(', ')
152
+
153
+ # Create the global gravity animation that is consistent for all fireworks.
154
+ # There is no variance in this animation so it is created and added to the
155
+ # context only once.
156
+ create_gravity_animation(sfx)
157
+
158
+ end
159
+
160
+ def config_child n, child, style, animation, sfx
161
+
162
+ # If all of the fireworks are different possible sizes
163
+ # then pick a random size for this child.
164
+ unless sfx.same_size?
165
+ size = sfx.rand_size
166
+ style[:width] = px(size)
167
+ style[:height] = px(size)
168
+ end
169
+
170
+ # If rainbow is specified, choose the next color in the array - otherwise
171
+ # choose a random hue unless a specific one has been specified.
172
+ hue = sfx[:rainbow] ? sfx[:hues][n] : (sfx[:hue] || rand(360)).to_i
173
+
174
+ # Randomly pick a color for this explosion by choosing a
175
+ # random hue and then converting it to a hex color
176
+ color = Inkcite::Util::hsl_to_color(hue, 100, 50)
177
+ style[BACKGROUND_COLOR] = color
178
+
179
+ # After the first child, each firework should have a random
180
+ # delay before its animation starts - giving the different
181
+ # fireworks a staggered launch.
182
+ delay = n > 0 ? 0.25 + rand(sfx.count).round(2) : 0
183
+
184
+ # This is the total amount of time it will take the firework to
185
+ # move through each of its positions.
186
+ position_speed = sfx.rand_speed
187
+
188
+ # This is the speed the firework animates it's explosion and gravity
189
+ # components - which need to repeat n-number of times based on the
190
+ # total number of positions.
191
+ explosion_speed = (position_speed / TOTAL_POSITIONS.to_f).round(2)
192
+
193
+ gravity_animation = Inkcite::Animation.new('gravity', sfx.ctx)
194
+ gravity_animation.duration = explosion_speed
195
+ gravity_animation.delay = delay if n > 0
196
+ gravity_animation.timing_function = Inkcite::Animation::EASE_IN
197
+
198
+ composite_animation = Inkcite::Animation::Composite.new
199
+ composite_animation << create_explosion_animation(n, hue, explosion_speed, delay, sfx)
200
+ composite_animation << gravity_animation
201
+ composite_animation << create_position_animation(n, position_speed, delay, sfx)
202
+
203
+ style[:animation] = composite_animation
204
+
205
+ # # Each firework consists of three separate animations - one to animate
206
+ # # the explosion, one to fade out/apply gravity and one to move the
207
+ # # firework through it's fixed positions.
208
+ # style[:animation] = "1s bang ease-out infinite, 1s gravity ease-in infinite, #{TOTAL_POSITIONS}s position linear infinite"
209
+
210
+ # style[ANIMATION_DURATION] = "#{explosion_speed}s, #{explosion_speed}s, #{position_speed}s"
211
+
212
+ end
213
+
214
+ def defaults opt, ctx
215
+ {
216
+ :bgcolor => '#000000',
217
+ :sparks => 50,
218
+ :count => 2,
219
+ :gravity => 200,
220
+ RADIUS_MIN => 150,
221
+ RADIUS_MAX => 400,
222
+ SIZE_MIN => 10,
223
+ SIZE_MAX => 10,
224
+ SPEED_MIN => 5,
225
+ SPEED_MAX => 10,
226
+ }
227
+ end
228
+
229
+ end
230
+ end
231
+ end
@@ -1,4 +1,3 @@
1
-
2
1
  module Inkcite
3
2
  module Renderer
4
3
  class InBrowser < Base
@@ -20,7 +19,7 @@ module Inkcite
20
19
  color = opt[:color]
21
20
 
22
21
  # Optional call-to-action override - otherwise defaults to view in browser.
23
- cta = opt[:cta] || ctx.production?? 'View in Browser' : 'Preview in Browser'
22
+ cta = opt[:cta] || ctx.production?? 'View&nbsp;in&nbsp;Browser' : 'Preview&nbsp;in&nbsp;Browser'
24
23
 
25
24
  id = opt[:id] || 'in-browser'
26
25
 
@@ -28,8 +28,9 @@ module Inkcite
28
28
  MOBILE_SRC = :'mobile-src'
29
29
 
30
30
  # Other mobile-specific properties
31
+ MOBILE_HEIGHT = :'mobile-height'
31
32
  MOBILE_PADDING = :'mobile-padding'
32
-
33
+ MOBILE_WIDTH = :'mobile-width'
33
34
 
34
35
  class Rule
35
36
 
@@ -241,8 +242,28 @@ module Inkcite
241
242
  mix_directional element, element.mobile_style, opt, ctx, MOBILE_MARGIN, :margin, true
242
243
  end
243
244
 
245
+ def mix_mobile_padding element, opt, ctx
246
+ mix_directional element, element.mobile_style, opt, ctx, MOBILE_PADDING, :padding, true
247
+ end
248
+
249
+ # A separate method for mixing in text alignment because the table cell
250
+ # helper handles alignment different from normal container elements.
251
+ def mix_mobile_text_align element, opt, ctx
252
+
253
+ # Support for mobile-text-align
254
+ align = opt[MOBILE_TEXT_ALIGN]
255
+ element.mobile_style[TEXT_ALIGN] = align unless none?(align)
256
+
257
+ end
258
+
244
259
  def mix_responsive element, opt, ctx, klass=nil
245
260
 
261
+ mobile_style = opt[MOBILE_STYLE]
262
+ ctx.error 'mobile-style is no longer supported', { :element => element.to_s, MOBILE_STYLE => mobile_style } unless mobile_style.blank?
263
+
264
+ mobile_display = opt[MOBILE_DISPLAY]
265
+ element.mobile_style[:display] = mobile_display unless none?(mobile_display)
266
+
246
267
  # Apply the "mobile" attribute or use the override if one was provided.
247
268
  mix_responsive_klass element, opt, ctx, klass || opt[:mobile]
248
269
 
@@ -398,6 +419,11 @@ module Inkcite
398
419
 
399
420
  end
400
421
 
422
+ def mix_text_align element, opt, ctx
423
+ super
424
+ mix_mobile_text_align element, opt, ctx
425
+ end
426
+
401
427
  def unique_klass ctx
402
428
  'm%1d' % ctx.unique_id(:m)
403
429
  end
@@ -406,9 +432,10 @@ module Inkcite
406
432
 
407
433
  # Attribute used to declare custom mobile styles for an element.
408
434
  MOBILE_BORDER = :'mobile-border'
435
+ MOBILE_DISPLAY = :'mobile-display'
409
436
  MOBILE_MARGIN = :'mobile-margin'
410
437
  MOBILE_STYLE = :'mobile-style'
411
- MOBILE_WIDTH = :'mobile-width'
438
+ MOBILE_TEXT_ALIGN = :'mobile-text-align'
412
439
 
413
440
  # Universal CSS selector.
414
441
  UNIVERSAL = '*'
@@ -4,7 +4,7 @@ module Inkcite
4
4
 
5
5
  protected
6
6
 
7
- def config_all_children_style style, sfx
7
+ def config_all_children style, sfx
8
8
 
9
9
  style[:top] = "-#{px(sfx.max_size + 4)}"
10
10
 
@@ -6,7 +6,7 @@ module Inkcite
6
6
  # common to special effects like snow and sparkle.
7
7
  class EffectContext
8
8
 
9
- attr_reader :uuid
9
+ attr_reader :uuid, :animations
10
10
 
11
11
  # Expose the opt and ctx attributes
12
12
  attr_reader :opt, :ctx
@@ -31,14 +31,26 @@ module Inkcite
31
31
  # this special effect.
32
32
  @uuid = ctx.unique_id(:sfx)
33
33
 
34
+ # Will hold all of the Animations while the children are
35
+ # assembled and then added to the styles array at the end
36
+ # so that all keyframes are together in the source.
37
+ @animations = []
38
+
34
39
  end
35
40
 
36
41
  def all_children_class_name
37
42
  obfuscate_class_names? ? "sfx#{@uuid}c" : "#{@tag}#{@uuid}-children"
38
43
  end
39
44
 
40
- def animation_class_name child_index
41
- obfuscate_class_names? ? "#{@tag}#{@uuid}-anim#{child_index + 1}" : "sfx#{@uuid}a#{child_index + 1}"
45
+ def animation_class_name child_index, suffix=nil
46
+
47
+ base = obfuscate_class_names? ? "#{@tag}#{@uuid}-anim#{child_index + 1}" : "sfx#{@uuid}a#{child_index + 1}"
48
+ unless suffix.blank?
49
+ base << '-'
50
+ base << (obfuscate_class_names? ? suffix[0] : suffix)
51
+ end
52
+
53
+ base
42
54
  end
43
55
 
44
56
  def child_class_name child_index
@@ -53,19 +65,19 @@ module Inkcite
53
65
  # of snowflakes or sparkles in the animation. :flakes and :sparks are
54
66
  # technically deprecated.
55
67
  def count
56
- (@opt[:sparks] || @opt[:flakes] || @opt[:count]).to_i
68
+ @opt[:count].to_i
57
69
  end
58
70
 
59
- def equal_distribution qty
71
+ def equal_distribution range, qty
60
72
 
61
73
  # Space the children generally equally across the width of the
62
74
  # container div. Random distribution sometimes ends up with
63
75
  # children clumped at one edge or the other.
64
- spacing = POSITION_CEIL / qty.to_f
76
+ spacing = (range.last - range.first) / qty.to_f
65
77
 
66
78
  # Now build up a pool of equally-spaced starting positions.
67
79
  # TODO: This is probably a perfect spot to use inject()
68
- start_left = spacing / 2.0
80
+ start_left = range.first + (spacing / 2.0)
69
81
 
70
82
  # This array will hold all of the positions
71
83
  positions = [start_left]
@@ -74,13 +86,17 @@ module Inkcite
74
86
  # spacing and push onto the list.
75
87
  (qty - 1).times { |f| positions << start_left += spacing }
76
88
 
77
- positions
89
+ positions.collect { |p| p.round(0) }
78
90
  end
79
91
 
80
92
  def height
81
93
  @opt[:height].to_i
82
94
  end
83
95
 
96
+ def insets
97
+ @opt[:insets].to_i
98
+ end
99
+
84
100
  def max_opacity
85
101
  @opt[OPACITY_MAX].to_f
86
102
  end
@@ -106,7 +122,7 @@ module Inkcite
106
122
  end
107
123
 
108
124
  def obfuscate_class_names?
109
- false && @ctx.production?
125
+ @ctx.production?
110
126
  end
111
127
 
112
128
  def opacity_range
@@ -137,6 +153,10 @@ module Inkcite
137
153
  (-270..270)
138
154
  end
139
155
 
156
+ def same_size?
157
+ min_size == max_size
158
+ end
159
+
140
160
  def size_range
141
161
  (min_size..max_size)
142
162
  end
@@ -159,18 +179,24 @@ module Inkcite
159
179
  @src
160
180
  end
161
181
 
182
+ def position_range
183
+ min = POSITION_FLOOR + insets
184
+ max = POSITION_CEIL - insets
185
+ (min..max)
186
+ end
187
+
162
188
  # Creates a permanent list of positions (as percentages of the wrap container's
163
189
  # total width) which can be used for starting or ending position to equally
164
190
  # space animated elements.
165
191
  def positions_x
166
- @positions_x ||= equal_distribution(count)
192
+ @positions_x ||= equal_distribution(position_range, count)
167
193
  end
168
194
 
169
195
  # Creates a permanent list of positions (as percentages of the wrap container's
170
196
  # total height) which can be used for starting or ending position to equally
171
197
  # space animated elements.
172
198
  def positions_y
173
- @positions_y ||= equal_distribution(count)
199
+ @positions_y ||= equal_distribution(position_range, count)
174
200
  end
175
201
 
176
202
  def start_time child_index
@@ -247,6 +273,10 @@ module Inkcite
247
273
  # at the same time.
248
274
  create_child_elements html, styles, sfx
249
275
 
276
+ # Append all of the Keyframes to the end of the styles, now that
277
+ # the individual children are configured.
278
+ sfx.animations.each { |a| styles << a.to_keyframe_css }
279
+
250
280
  # Push the completed list of styles into the context's stack.
251
281
  ctx.styles << styles.join("\n")
252
282
 
@@ -274,10 +304,14 @@ module Inkcite
274
304
  OPACITY_MAX = :'max-opacity'
275
305
  OPACITY_CEIL = 1.0
276
306
 
307
+ # Static constants for animation-specific CSS
308
+ ANIMATION_DELAY = :'animation-delay'
309
+ ANIMATION_DURATION = :'animation-duration'
310
+
277
311
  # The extending class can override this method to perform any
278
312
  # additional configuration on the style that affects all
279
313
  # children in the animation.
280
- def config_all_children_style style, sfx
314
+ def config_all_children style, sfx
281
315
  # This space left intentionally blank
282
316
  end
283
317
 
@@ -333,7 +367,7 @@ module Inkcite
333
367
 
334
368
  # Provide the extending class with a chance to apply additional
335
369
  # styles to all children.
336
- config_all_children_style style, sfx
370
+ config_all_children style, sfx
337
371
 
338
372
  styles << style
339
373
 
@@ -343,11 +377,6 @@ module Inkcite
343
377
  # CSS classes to them allowing each to be sized, animated uniquely.
344
378
  def create_child_elements html, styles, sfx
345
379
 
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
380
  sfx.count.times do |n|
352
381
 
353
382
  child_class_name = sfx.child_class_name(n)
@@ -365,20 +394,28 @@ module Inkcite
365
394
  # and its style.
366
395
  config_child n, child, style, animation, sfx
367
396
 
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.
397
+ # Add the child's HTML element into the email.
373
398
  html << child.to_s + '</div>'
374
- styles << style
375
- animations << animation
376
399
 
377
- end
400
+ # If the extending class actually defined an animation for this child
401
+ # then assign it and add it to the list of animations to be appended
402
+ # after the styles are injected.
403
+ unless animation.blank?
378
404
 
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 }
405
+ sfx.animations << animation
406
+
407
+ # If the extending class didn't assign the animation already, then
408
+ # assign it to the child's style - this adds itself with the appropriate
409
+ # browser prefixes.
410
+ style[:animation] = animation if style[:animation].blank?
411
+
412
+ end
413
+
414
+ # Append the child's custom style, unless blank (meaning the extending
415
+ # class did not customize the child's styles directly).
416
+ styles << style unless style.blank?
417
+
418
+ end
382
419
 
383
420
  end
384
421