inkcite 1.11.0 → 1.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Rakefile +2 -2
- data/assets/init/helpers.tsv +7 -0
- data/assets/social/facebook.png +0 -0
- data/assets/social/pintrest.png +0 -0
- data/assets/social/twitter.png +0 -0
- data/inkcite.gemspec +5 -4
- data/lib/inkcite.rb +17 -6
- data/lib/inkcite/animation.rb +135 -0
- data/lib/inkcite/cli/base.rb +5 -14
- data/lib/inkcite/cli/build.rb +3 -3
- data/lib/inkcite/cli/init.rb +3 -3
- data/lib/inkcite/cli/preview.rb +25 -1
- data/lib/inkcite/cli/server.rb +1 -1
- data/lib/inkcite/cli/validate.rb +2 -11
- data/lib/inkcite/email.rb +1 -1
- data/lib/inkcite/mailer.rb +5 -0
- data/lib/inkcite/minifier.rb +58 -11
- data/lib/inkcite/renderer.rb +10 -2
- data/lib/inkcite/renderer/base.rb +45 -3
- data/lib/inkcite/renderer/button.rb +16 -12
- data/lib/inkcite/renderer/container_base.rb +14 -4
- data/lib/inkcite/renderer/element.rb +13 -3
- data/lib/inkcite/renderer/image.rb +46 -19
- data/lib/inkcite/renderer/image_base.rb +3 -3
- data/lib/inkcite/renderer/in_browser.rb +1 -1
- data/lib/inkcite/renderer/link.rb +91 -59
- data/lib/inkcite/renderer/lorem.rb +9 -1
- data/lib/inkcite/renderer/mobile_image.rb +3 -11
- data/lib/inkcite/renderer/mobile_only.rb +48 -0
- data/lib/inkcite/renderer/outlook_background.rb +61 -13
- data/lib/inkcite/renderer/property.rb +1 -1
- data/lib/inkcite/renderer/responsive.rb +10 -8
- data/lib/inkcite/renderer/snow.rb +20 -10
- data/lib/inkcite/renderer/social.rb +128 -0
- data/lib/inkcite/renderer/table.rb +13 -1
- data/lib/inkcite/renderer/table_base.rb +3 -3
- data/lib/inkcite/renderer/td.rb +32 -7
- data/lib/inkcite/renderer/video_preview.rb +257 -0
- data/lib/inkcite/uploader.rb +3 -3
- data/lib/inkcite/util.rb +19 -5
- data/lib/inkcite/version.rb +1 -1
- data/lib/inkcite/view.rb +29 -18
- data/lib/inkcite/view/context.rb +30 -0
- data/test/animation_spec.rb +38 -0
- data/test/email_spec.rb +0 -4
- data/test/minifier_spec.rb +243 -4
- data/test/parser_spec.rb +0 -4
- data/test/project/helpers.tsv +3 -0
- data/test/renderer/button_spec.rb +15 -13
- data/test/renderer/div_spec.rb +13 -4
- data/test/renderer/element_spec.rb +1 -5
- data/test/renderer/footnote_spec.rb +0 -4
- data/test/renderer/image_spec.rb +27 -11
- data/test/renderer/link_spec.rb +14 -4
- data/test/renderer/lorem_spec.rb +0 -4
- data/test/renderer/mobile_image_spec.rb +3 -11
- data/test/renderer/mobile_only_spec.rb +21 -0
- data/test/renderer/mobile_style_spec.rb +1 -5
- data/test/renderer/outlook_background_spec.rb +61 -0
- data/test/renderer/redacted_spec.rb +0 -4
- data/test/renderer/social_spec.rb +53 -0
- data/test/renderer/span_spec.rb +0 -4
- data/test/renderer/table_spec.rb +8 -4
- data/test/renderer/td_spec.rb +0 -4
- data/test/renderer/video_preview_spec.rb +19 -0
- data/test/renderer_spec.rb +0 -4
- data/test/test_helper.rb +7 -0
- data/test/view_spec.rb +12 -4
- metadata +89 -56
@@ -56,7 +56,7 @@ module Inkcite
|
|
56
56
|
|
57
57
|
# If the image is missing, record an error to the console and
|
58
58
|
# clear the image allowing the color to take precedence instead.
|
59
|
-
src = nil
|
59
|
+
src = nil if src && !ctx.assert_image_exists(src)
|
60
60
|
|
61
61
|
# Flake rotation, used if an image is present. (Rotating a colored
|
62
62
|
# circle has no visual distinction.) For convenience this tag accepts
|
@@ -77,6 +77,9 @@ module Inkcite
|
|
77
77
|
# snow effect.
|
78
78
|
mix_background div_wrap, opt, ctx
|
79
79
|
|
80
|
+
# Text alignment within the wrapper
|
81
|
+
mix_text_align div_wrap, opt, ctx
|
82
|
+
|
80
83
|
# Kick things off by rendering the wrapping div.
|
81
84
|
html = div_wrap.to_s
|
82
85
|
|
@@ -146,13 +149,17 @@ module Inkcite
|
|
146
149
|
start_interval = end_time / flakes.to_f
|
147
150
|
start_time = 0
|
148
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
|
+
|
149
156
|
# Now add individual class definitions for each flake with unique size,
|
150
157
|
# speed and starting position. Also add the animation trigger that loops
|
151
158
|
# infinitely, starts at a random time and uses a random speed to completion.
|
152
159
|
flakes.times do |flake|
|
153
160
|
|
154
161
|
speed = rand(speed_range).round(1)
|
155
|
-
size = rand(size_range)
|
162
|
+
size = flake_sizes[flake] = rand(size_range)
|
156
163
|
|
157
164
|
opacity = rand(opacity_range).round(1)
|
158
165
|
if opacity < OPACITY_FLOOR
|
@@ -169,7 +176,7 @@ module Inkcite
|
|
169
176
|
style << " border-radius: #{px((size / 2.0).round)};" unless src
|
170
177
|
|
171
178
|
style << " opacity: #{opacity};" if opacity < OPACITY_CEIL
|
172
|
-
style << with_browser_prefixes(
|
179
|
+
style << Animation.with_browser_prefixes("animation: #{anim_prefix}#{flake + 1} #{speed}s linear #{start_time.round(1)}s infinite;", ctx)
|
173
180
|
style << ' }'
|
174
181
|
|
175
182
|
start_time += start_interval
|
@@ -192,14 +199,17 @@ module Inkcite
|
|
192
199
|
# Calculate the ending rotation for the flake, if rotation is enabled.
|
193
200
|
end_rotation = rotation_enabled ? rand(ROTATION_RANGE) : 0
|
194
201
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
202
|
+
keyframes = Animation::Keyframes.new("#{anim_prefix}#{flake + 1}", ctx)
|
203
|
+
|
204
|
+
# Start above the div area
|
205
|
+
keyframes.add_keyframe(0, :top => px(-flake_sizes[flake]), :left => pct(start_left.round))
|
206
|
+
|
207
|
+
# End below the div area, applying rotation if necessary.
|
208
|
+
keyframe = Animation::Keyframe.new(100, :top => '100%', :left => pct(end_left))
|
209
|
+
keyframe.add_with_prefixes(:transform, "rotate(#{end_rotation}deg)", ctx) if end_rotation != 0
|
210
|
+
keyframes << keyframe
|
201
211
|
|
202
|
-
style <<
|
212
|
+
style << keyframes.to_s
|
203
213
|
|
204
214
|
end
|
205
215
|
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module Inkcite
|
2
|
+
module Renderer
|
3
|
+
class Social < Base
|
4
|
+
|
5
|
+
class Facebook < Social
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
super(:src => 'facebook.png', :alt => 'Facebook', :cta => 'Share')
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def get_share_href url, text, opts, ctx
|
14
|
+
%Q(https://www.facebook.com/sharer/sharer.php?u=#{url}&t=#{text})
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
class Pintrest < Social
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
super(:src => 'pintrest.png', :alt => 'Pintrest', :cta => 'Pin it', :color => '#CB2027')
|
23
|
+
end
|
24
|
+
|
25
|
+
protected_methods
|
26
|
+
|
27
|
+
def get_share_href url, text, opts, ctx
|
28
|
+
|
29
|
+
media = opts.delete(:media).to_s
|
30
|
+
ctx.error("Pintrest sharing 'media' attribute can't be blank", :id => opts[:id]) if media.blank?
|
31
|
+
|
32
|
+
%Q(https://www.pinterest.com/pin/create/button/?url=#{url}&media=#{Util::escape(media)}&description=#{text})
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
class Twitter < Social
|
38
|
+
|
39
|
+
def initialize
|
40
|
+
super(:src => 'twitter.png', :alt => 'Twitter', :cta => 'Tweet', :scale => 81 / 100.0)
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
def get_share_href url, text, opts, ctx
|
46
|
+
%Q(https://twitter.com/share?url=#{url}&text=#{text})
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize defaults
|
52
|
+
@defaults = defaults
|
53
|
+
|
54
|
+
# Ensure a default scale of 1:1 is installed into the defaults
|
55
|
+
# if one is not otherwise provided.
|
56
|
+
@defaults[:scale] ||= 1.0
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
def render tag, opts, ctx
|
61
|
+
|
62
|
+
id = opts[:id]
|
63
|
+
|
64
|
+
share_url = opts.delete(:href).to_s
|
65
|
+
ctx.error("Social sharing 'href' attribute can't be blank", :tag => tag, :id => id) if share_url.blank?
|
66
|
+
|
67
|
+
share_text = opts.delete(:text).to_s
|
68
|
+
ctx.error("Social sharing 'text' attribute can't be blank", :tag => tag, :id => id, :href => share_url) if share_text.blank?
|
69
|
+
|
70
|
+
# Let the extending class format the fully-qualified URL to the sharing service.
|
71
|
+
service_href = get_share_href Util.escape(share_url), Util.escape(share_text), opts, ctx
|
72
|
+
|
73
|
+
# Check to see if there is a special color for this link (e.g. Pintrest) or
|
74
|
+
# if it has been specified by the caller.
|
75
|
+
opts[:color] ||= @defaults[:color]
|
76
|
+
|
77
|
+
if icon = !opts[:noicon]
|
78
|
+
|
79
|
+
# Ensure that the sharing icon exists in the project.
|
80
|
+
ensure_icon_exists ctx
|
81
|
+
|
82
|
+
height = (opts.delete(:size) || opts.delete(:height) || 15).to_i
|
83
|
+
width = (height / @defaults[:scale]).round
|
84
|
+
|
85
|
+
# Force the font size and line height to match the size of the
|
86
|
+
# icon being used - this ensures proper vertical middle alignment.
|
87
|
+
opts[FONT_SIZE] = height
|
88
|
+
opts[LINE_HEIGHT] = height
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
cta = opts[:cta] || @defaults[:cta]
|
93
|
+
|
94
|
+
html = %Q({a href="#{service_href}" #{Renderer.join_hash(opts)}})
|
95
|
+
html << %Q({img src=#{@defaults[:src]} height=#{height} width=#{width} display=inline alt="#{@defaults[:alt]}"} ) if icon
|
96
|
+
html << %Q(#{cta}{/a})
|
97
|
+
|
98
|
+
html
|
99
|
+
end
|
100
|
+
|
101
|
+
protected
|
102
|
+
|
103
|
+
def ensure_icon_exists ctx
|
104
|
+
|
105
|
+
src = @defaults[:src]
|
106
|
+
|
107
|
+
# Get the full destination path to the icon in the current project. If
|
108
|
+
# the icon already exists, then there is nothing left to do.
|
109
|
+
dest_icon_path = ctx.email.image_path(src)
|
110
|
+
return if File.exist?(dest_icon_path)
|
111
|
+
|
112
|
+
# Get the full path to the source icon bundled with Inkcite.
|
113
|
+
source_icon_path = File.join(Inkcite.asset_path, 'social', src)
|
114
|
+
|
115
|
+
# Ensure that the images/ directory exists in the project, then copy
|
116
|
+
# the image into it.
|
117
|
+
FileUtils.mkpath(ctx.email.image_dir)
|
118
|
+
FileUtils.cp(source_icon_path, dest_icon_path)
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
def get_share_href url, text, opts, ctx
|
123
|
+
raise NotImplementedError
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -2,6 +2,10 @@ module Inkcite
|
|
2
2
|
module Renderer
|
3
3
|
class Table < TableBase
|
4
4
|
|
5
|
+
# Name of the attribute allowing a transition attribute to be placed
|
6
|
+
# on the automatically-added <tr> element.
|
7
|
+
TR_TRANSITION = :'tr-transition'
|
8
|
+
|
5
9
|
def render tag, opt, ctx
|
6
10
|
|
7
11
|
html = ''
|
@@ -108,7 +112,15 @@ module Inkcite
|
|
108
112
|
mix_responsive table, opt, ctx, mobile
|
109
113
|
|
110
114
|
html << table.to_s
|
111
|
-
|
115
|
+
|
116
|
+
tr = Element.new('tr')
|
117
|
+
|
118
|
+
# Check for a row transition which is necessary to facilitate
|
119
|
+
# cross-column animation - e.g. tr-transition="all .5s cubic-bezier(0.075, 0.82, 0.165, 1)"
|
120
|
+
tr_transition = opt[TR_TRANSITION]
|
121
|
+
tr.style[:transition] = tr_transition unless none?(tr_transition)
|
122
|
+
|
123
|
+
html << tr.to_s
|
112
124
|
|
113
125
|
if is_fluid_drop
|
114
126
|
|
@@ -12,6 +12,7 @@ module Inkcite
|
|
12
12
|
|
13
13
|
def mix_all element, opt, ctx
|
14
14
|
|
15
|
+
mix_animation element, opt, ctx
|
15
16
|
mix_background element, opt, ctx
|
16
17
|
mix_border element, opt, ctx
|
17
18
|
mix_dimensions element, opt, ctx
|
@@ -20,12 +21,11 @@ module Inkcite
|
|
20
21
|
|
21
22
|
def mix_background element, opt, ctx
|
22
23
|
|
23
|
-
bgcolor = opt
|
24
|
-
bgcolor = nil if bgcolor == NONE
|
24
|
+
bgcolor = detect_bgcolor(opt)
|
25
25
|
|
26
26
|
# Set the bgcolor attribute of the element as a fallback if
|
27
27
|
# css isn't supported.
|
28
|
-
element[:bgcolor] =
|
28
|
+
element[:bgcolor] = bgcolor unless bgcolor.nil?
|
29
29
|
|
30
30
|
bgimage = opt[:background]
|
31
31
|
bgposition = opt[BACKGROUND_POSITION]
|
data/lib/inkcite/renderer/td.rb
CHANGED
@@ -24,9 +24,12 @@ module Inkcite
|
|
24
24
|
|
25
25
|
if tag == CLOSE_TD
|
26
26
|
|
27
|
-
# Retrieve the opts that were used to open this TD.
|
28
|
-
|
29
|
-
|
27
|
+
# Retrieve the opts that were used to open this TD.
|
28
|
+
open_opt = tag_stack.pop
|
29
|
+
|
30
|
+
# If the opening tag initiated an automatic outlook background
|
31
|
+
# then we need to inject the close tag here.
|
32
|
+
html << "{/outlook-bg}\n" if outlook_bg?(open_opt)
|
30
33
|
|
31
34
|
# Normal HTML produced by the Helper to close the cell.
|
32
35
|
html << '</td>'
|
@@ -126,7 +129,7 @@ module Inkcite
|
|
126
129
|
:color => NONE,
|
127
130
|
FONT_SIZE => 1,
|
128
131
|
LINE_HEIGHT => 1
|
129
|
-
})
|
132
|
+
}) if opt[:flush]
|
130
133
|
|
131
134
|
# Custom handling for text align on TDs rather than Base's mix_text_align
|
132
135
|
# because if possible, using align= rather than a style keeps emails
|
@@ -165,11 +168,23 @@ module Inkcite
|
|
165
168
|
|
166
169
|
mix_responsive td, opt, ctx, mobile
|
167
170
|
|
168
|
-
#outlook-bg <!--[if gte mso 9]>[n]<v:rect style="width:%width%px;height:%height%px;" strokecolor="none"><v:fill type="tile" src="%src%" /></v:fill></v:rect><v:shape id="theText[rnd]" style="position:absolute;width:%width%px;height:%height%px;margin:0;padding:0;%style%">[n]<![endif]-->
|
169
|
-
#/outlook-bg <!--[if gte mso 9]></v:shape><![endif]-->
|
170
|
-
|
171
171
|
html << td.to_s
|
172
172
|
|
173
|
+
# For convenience and to keep code DRY, support the outlook-bg boolean
|
174
|
+
# attribute which causes an {outlook-bg} Helper to be injected automatically
|
175
|
+
# inside of the {td}.
|
176
|
+
if outlook_bg?(opt)
|
177
|
+
|
178
|
+
html << "\n"
|
179
|
+
html << Element.new('outlook-bg', {
|
180
|
+
:bgcolor => detect_bgcolor(opt),
|
181
|
+
:width => opt[:width],
|
182
|
+
:height => opt[:height],
|
183
|
+
:src => opt[:background]
|
184
|
+
}).to_helper
|
185
|
+
|
186
|
+
end
|
187
|
+
|
173
188
|
end
|
174
189
|
|
175
190
|
html
|
@@ -177,9 +192,19 @@ module Inkcite
|
|
177
192
|
|
178
193
|
private
|
179
194
|
|
195
|
+
# Returns true if the conditions are met that enable the
|
196
|
+
# automatic {outlook-bg} helper integration.
|
197
|
+
def outlook_bg? opt
|
198
|
+
opt && opt[OUTLOOK_BG] && !opt[:background].blank?
|
199
|
+
end
|
200
|
+
|
180
201
|
CLOSE_TD = '/td'
|
181
202
|
LEFT = 'left'
|
182
203
|
|
204
|
+
# Boolean attribute triggering automatic outlook background
|
205
|
+
# integration in the TD.
|
206
|
+
OUTLOOK_BG = :'outlook-bg'
|
207
|
+
|
183
208
|
end
|
184
209
|
end
|
185
210
|
end
|
@@ -0,0 +1,257 @@
|
|
1
|
+
module Inkcite
|
2
|
+
module Renderer
|
3
|
+
|
4
|
+
# Better video preview courtesy of @stigm
|
5
|
+
# https://medium.com/cm-engineering/better-video-previews-for-email-12432ce71846#.2o9qgc7hd
|
6
|
+
class VideoPreview < Base
|
7
|
+
|
8
|
+
def render tag, opt, ctx
|
9
|
+
|
10
|
+
# Get a unique ID for this video which will make its CSS classes
|
11
|
+
# distinct from other videos in the email.
|
12
|
+
uid = ctx.unique_id(:video)
|
13
|
+
|
14
|
+
# Links need an ID
|
15
|
+
id = opt[:id] || "video-preview#{uid}"
|
16
|
+
|
17
|
+
# Grab the URL for the video - this is passed on to the {a} Helper and
|
18
|
+
# the user will be warned appropriately if the URL is missing.
|
19
|
+
href = opt[:href].freeze
|
20
|
+
|
21
|
+
# Grab the name of the source file that can be optionally embeded with %1
|
22
|
+
# which will increment for each frame (e.g. video%1.jpg becomes video1.jpg,
|
23
|
+
# video2.jpg etc. up to the total number of frames). The original source
|
24
|
+
# image name is frozen to ensure it isn't modified later.
|
25
|
+
src = opt[:src].freeze
|
26
|
+
|
27
|
+
# This will hold all frame source file names interpolated to include
|
28
|
+
# index (e.g. %1 being replaced with the frame number, if present).
|
29
|
+
frames = []
|
30
|
+
|
31
|
+
# For each frame, create a fully-qualified image source and
|
32
|
+
# add it to the frame list.
|
33
|
+
frame_count = (opt[:frames] || 1).to_i
|
34
|
+
|
35
|
+
# True if the video clip will animate using smooth fading
|
36
|
+
# between several frames of the video.
|
37
|
+
has_animation = frame_count > 1
|
38
|
+
|
39
|
+
# Iterate through the frames and replace %1 with the frame number.
|
40
|
+
# this loop also verifies that the referenced image exists.
|
41
|
+
frame_count.times do |n|
|
42
|
+
frame_src = src.gsub('%1', "#{n + 1}")
|
43
|
+
|
44
|
+
# Check that non-fully-qualified images exist in the project's
|
45
|
+
# images/ directory.
|
46
|
+
unless Util::is_fully_qualified?(frame_src)
|
47
|
+
ctx.assert_image_exists(frame_src)
|
48
|
+
frame_src = ctx.image_url(frame_src)
|
49
|
+
end
|
50
|
+
|
51
|
+
frames << frame_src
|
52
|
+
end
|
53
|
+
|
54
|
+
# Grab the first fully-qualified frame
|
55
|
+
first_frame = frames[0]
|
56
|
+
|
57
|
+
# Duration of the animated frame cycling, if multiple frames are provided.
|
58
|
+
duration = (opt[:duration] || 15).to_i
|
59
|
+
|
60
|
+
# Desired dimensions of the video clip.
|
61
|
+
width = opt[:width].to_i
|
62
|
+
height = opt[:height].to_i
|
63
|
+
ctx.error("Video preview #{uid} is missing dimensions", { :width => width, :height => height, :src => src, :href => href }) unless width > 0 && height > 0
|
64
|
+
|
65
|
+
# Calculate the scaled width for the left-side of the table
|
66
|
+
# which is a crafty way to preserve the aspect ratio of the
|
67
|
+
# video while it still fluidly scales.
|
68
|
+
scaled_width = (width * SCALE).round
|
69
|
+
|
70
|
+
# Background color and edge gradient - which defaults to a darker
|
71
|
+
# version of the background color if not specified.
|
72
|
+
bgcolor = detect_bgcolor(opt, '#5b5f66')
|
73
|
+
gradient = opt[:gradient] || Util::darken(bgcolor, 0.25)
|
74
|
+
|
75
|
+
# This is the name of the class applied to the anchor tag
|
76
|
+
# to animate the hover.
|
77
|
+
hover_klass = 'video'
|
78
|
+
play_button_klass = 'play-button'
|
79
|
+
|
80
|
+
# This is the name of the animation, if any, that will be
|
81
|
+
# assigned to the table and defined in the CSS.
|
82
|
+
animation_name = "#{hover_klass}#{uid}-frames"
|
83
|
+
|
84
|
+
html = []
|
85
|
+
html << '<!--[if !vml]-->'
|
86
|
+
|
87
|
+
# Using an Element to produce the appropriate anchor helper with
|
88
|
+
# the desired
|
89
|
+
html << Element.new('a', { :id => id, :href => href, :class => hover_klass, :bgcolor => bgcolor, :bggradient => gradient, :block => true }).to_helper
|
90
|
+
|
91
|
+
table = Element.new('table', {
|
92
|
+
:width => '100%', :background => first_frame, BACKGROUND_SIZE => 'cover',
|
93
|
+
Table::TR_TRANSITION => %q("all .5s cubic-bezier(0.075, 0.82, 0.165, 1)")
|
94
|
+
})
|
95
|
+
table[:animation] = %Q("#{animation_name} #{duration}s ease infinite") if has_animation
|
96
|
+
html << table.to_helper
|
97
|
+
|
98
|
+
html << Element.new('td', :width => '25%').to_helper
|
99
|
+
|
100
|
+
# Transparent spacer for preserving aspect ratio.
|
101
|
+
spacer_image_name = "vp-#{scaled_width}x#{height}.png"
|
102
|
+
spacer_image = File.join(ctx.email.image_dir, spacer_image_name)
|
103
|
+
|
104
|
+
# Test if the file exists
|
105
|
+
unless File.exist?(spacer_image)
|
106
|
+
|
107
|
+
# Requiring on-demand, don't load chunky_png unless the user has
|
108
|
+
# started using video_preview.
|
109
|
+
require 'chunky_png'
|
110
|
+
|
111
|
+
# Creating an image from scratch, save as an interlaced PNG
|
112
|
+
png = ChunkyPNG::Image.new(scaled_width, height, ChunkyPNG::Color::TRANSPARENT)
|
113
|
+
png.save(spacer_image, :interlace => true)
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
# 12/21/16: Using placehold.it is sub-optimal. It would be better
|
118
|
+
# to generate a transparent gif using something like Rmagick. I've
|
119
|
+
# seen some instances where placeholdit can crash outlook but I guess
|
120
|
+
# this part is hidden from outlook so it isn't so bad.
|
121
|
+
html << Element.new('img', { :src => ctx.image_url(spacer_image_name), :alt => '', :width => '100%', :border => 0,
|
122
|
+
:style => { :height => :auto, :opacity => 0, :visibility => :hidden } }).to_s
|
123
|
+
|
124
|
+
html << '{/td}'
|
125
|
+
|
126
|
+
# Center column holds the CSS-based arrow
|
127
|
+
html << Element.new('td', :width => '50%', :align => :center, :valign => :middle).to_helper
|
128
|
+
|
129
|
+
play_arrow_size = (opt[PLAY_ARROW_SIZE] || 30).to_i
|
130
|
+
|
131
|
+
play_border_radius = (play_arrow_size * 1.1333).round
|
132
|
+
play_border_top_bottom = (play_arrow_size * 0.6).round
|
133
|
+
play_border_right = (play_arrow_size * 0.5333).round
|
134
|
+
play_border_left = (play_arrow_size * 0.8).round
|
135
|
+
play_arrow_height = (play_arrow_size * 0.5666).round
|
136
|
+
|
137
|
+
# These are the arrow and circle border, respectively. Not currently
|
138
|
+
# configurable in terms of size or color.
|
139
|
+
html << %Q(<div class="#{play_button_klass}" style="background-image: linear-gradient(rgba(0,0,0,0.5), rgba(0,0,0,0.1)); border: 4px solid white; border-radius: 50%; box-shadow: 0 1px 2px rgba(0,0,0,0.3), inset 0 1px 2px rgba(0,0,0,0.3); height: #{px(play_border_radius)}; margin: 0 auto; padding: #{px(play_border_top_bottom)} #{px(play_border_right)} #{px(play_border_top_bottom)} #{px(play_border_left)}; transition: transform .5s cubic-bezier(0.075, 0.82, 0.165, 1); width: #{px(play_border_radius)};">)
|
140
|
+
html << %Q(<div style="border-color: transparent transparent transparent white; border-style: solid; border-width: #{px(play_arrow_height)} 0 #{px(play_arrow_height)} #{px(play_arrow_size)}; display: block; font-size: 0; height: 0; Margin: 0 auto; width: 0;"> </div>)
|
141
|
+
html << '</div>'
|
142
|
+
|
143
|
+
html << '{/td}'
|
144
|
+
html << '{td width=25%} {/td}'
|
145
|
+
html << '{/table}'
|
146
|
+
html << '{/a}'
|
147
|
+
|
148
|
+
# Pre-loading the images prevents a flash that can occur because the
|
149
|
+
# browser only loads the frames once the animation demands them.
|
150
|
+
if has_animation && !opt[NO_PRELOAD]
|
151
|
+
all_frames = frames.collect { |f| %Q(url(#{f})) }.join(',')
|
152
|
+
html << Element.new('div', :style => { BACKGROUND_IMAGE => %Q("#{all_frames}"), :display => 'none' }).to_s + '</div>'
|
153
|
+
end
|
154
|
+
|
155
|
+
# Concludes the if [if !vml] section targeting non-outlook.
|
156
|
+
html << '<![endif]-->'
|
157
|
+
|
158
|
+
# Outlook arrow size also not configurable at this time.
|
159
|
+
outlook_arrow_size = 78
|
160
|
+
outlook_left = width / 2 - outlook_arrow_size / 2
|
161
|
+
outlook_top = height / 2 - outlook_arrow_size / 2
|
162
|
+
|
163
|
+
# Use the link central processing routine to ensure a viable link has
|
164
|
+
# been provided and tag/track it from Outlook.
|
165
|
+
outlook_href = Link.process(id, href, false, ctx)
|
166
|
+
|
167
|
+
html << '<!--[if vml]>'
|
168
|
+
html << %Q(<v:group xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" coordsize="#{width},#{height}" coordorigin="0,0" href="#{outlook_href}" style="width:#{width}px;height:#{height}px;">)
|
169
|
+
html << %Q(<v:rect fill="t" stroked="f" style="position:absolute;width:#{width};height:#{height};"><v:fill src="#{first_frame}" type="frame"/></v:rect>)
|
170
|
+
html << %Q(<v:oval fill="t" strokecolor="white" strokeweight="4px" style="position:absolute;left:#{outlook_left};top:"#{outlook_top};width:#{outlook_arrow_size};height:#{outlook_arrow_size}"><v:fill color="black" opacity="30%" /></v:oval>)
|
171
|
+
html << %q(<v:shape coordsize="24,32" path="m,l,32,24,16,xe" fillcolor="white" stroked="f" style="position:absolute;left:289;top:151;width:30;height:34;" />)
|
172
|
+
html << '</v:group>'
|
173
|
+
html << '<![endif]-->'
|
174
|
+
|
175
|
+
# Will hold any CSS styles, if there are some necessary
|
176
|
+
# to inject into the email.
|
177
|
+
styles = []
|
178
|
+
|
179
|
+
# If this is the first video clip in the email, we need
|
180
|
+
# to include the general styles shared across all clips.
|
181
|
+
if uid == 1
|
182
|
+
styles << ".#{hover_klass}:hover .#{play_button_klass} {"
|
183
|
+
styles << ' transform: scale(1.1);'
|
184
|
+
styles << '}'
|
185
|
+
styles << ".#{hover_klass}:hover tr {"
|
186
|
+
styles << ' background-color: rgba(255, 255, 255, .2);'
|
187
|
+
styles << '}'
|
188
|
+
end
|
189
|
+
|
190
|
+
# If this video clip has animation, then we need to include
|
191
|
+
# the keyframes necessary to smoothly animate between each.
|
192
|
+
if has_animation
|
193
|
+
|
194
|
+
# The time spent in each frame is based on a weighted distribution
|
195
|
+
# of frames vs. transition time between frames.
|
196
|
+
total_weight = ((FRAME_WEIGHT * frame_count) + (TRANSITION_WEIGHT * frame_count)).to_f
|
197
|
+
percent_per_frame = (FRAME_WEIGHT / total_weight * 100.0).round
|
198
|
+
percent_per_transition = (TRANSITION_WEIGHT / total_weight * 100.0).round
|
199
|
+
|
200
|
+
# This will hold the total percentage as we increment toward the
|
201
|
+
# end of the animation.
|
202
|
+
percent = 0.0
|
203
|
+
|
204
|
+
keyframes = Animation::Keyframes.new(animation_name, ctx)
|
205
|
+
|
206
|
+
# Iterate through each frame and add two keyframes, the first
|
207
|
+
# being the time at which the frame appears plus another frame
|
208
|
+
# after the duration it should be on screen.
|
209
|
+
frames.each do |f|
|
210
|
+
this_frame_url = "url(#{f})"
|
211
|
+
|
212
|
+
keyframes.add_keyframe(percent, { BACKGROUND_IMAGE => this_frame_url })
|
213
|
+
percent += percent_per_frame
|
214
|
+
keyframes.add_keyframe(percent, { BACKGROUND_IMAGE => this_frame_url })
|
215
|
+
percent += percent_per_transition
|
216
|
+
|
217
|
+
end
|
218
|
+
|
219
|
+
# Transition back to the first frame.
|
220
|
+
keyframes.add_keyframe(100, { BACKGROUND_IMAGE => "url(#{first_frame})" })
|
221
|
+
|
222
|
+
# Add the keyframes to the styles array.
|
223
|
+
styles << keyframes.to_s
|
224
|
+
|
225
|
+
end
|
226
|
+
|
227
|
+
# Add the styles to the email's header
|
228
|
+
ctx.styles << styles.join("\n") unless styles.blank?
|
229
|
+
|
230
|
+
html.join("\n")
|
231
|
+
|
232
|
+
end
|
233
|
+
|
234
|
+
private
|
235
|
+
|
236
|
+
# Name of the attribute that controls the size of the play button arrow.
|
237
|
+
PLAY_ARROW_SIZE = :'play-size'
|
238
|
+
|
239
|
+
# Name of the boolean attribute that can be provided to disable the
|
240
|
+
# preloading of images in an animation.
|
241
|
+
NO_PRELOAD = :'no-preload'
|
242
|
+
|
243
|
+
# Constants defining the weight of a frame relative to the weight of
|
244
|
+
# the transition. In this case, 2-to-1 means each frame will be on
|
245
|
+
# the screen for twice as long as it takes to transition between
|
246
|
+
# them.
|
247
|
+
FRAME_WEIGHT = 2.0
|
248
|
+
TRANSITION_WEIGHT = 1.0
|
249
|
+
|
250
|
+
# Scale applied to the width of the image to preserve the aspect
|
251
|
+
# ratio of the video that fluidly scales on mobile devices.
|
252
|
+
SCALE = 0.25
|
253
|
+
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|