inkcite 1.13.0 → 1.14.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.
@@ -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