Class: Inkcite::Renderer::VideoPreview

Inherits:
ImageBase show all
Defined in:
lib/inkcite/renderer/video_preview.rb

Overview

Constant Summary

Constants inherited from Responsive

Responsive::BUTTON, Responsive::DROP, Responsive::FILL, Responsive::FLUID, Responsive::FLUID_DROP, Responsive::FLUID_STACK, Responsive::HIDE, Responsive::IMAGE, Responsive::MOBILE_BACKGROUND, Responsive::MOBILE_BACKGROUND_COLOR, Responsive::MOBILE_BACKGROUND_IMAGE, Responsive::MOBILE_BACKGROUND_POSITION, Responsive::MOBILE_BACKGROUND_REPEAT, Responsive::MOBILE_BACKGROUND_SIZE, Responsive::MOBILE_BGCOLOR, Responsive::MOBILE_PADDING, Responsive::MOBILE_SRC, Responsive::SHOW, Responsive::SHOW_INLINE, Responsive::SWITCH, Responsive::SWITCH_UP, Responsive::TOGGLE

Constants inherited from Base

Base::BACKGROUND_COLOR, Base::BACKGROUND_GRADIENT, Base::BACKGROUND_IMAGE, Base::BACKGROUND_POSITION, Base::BACKGROUND_REPEAT, Base::BACKGROUND_SIZE, Base::BORDER_BOTTOM, Base::BORDER_COLLAPSE, Base::BORDER_LEFT, Base::BORDER_RADIUS, Base::BORDER_RIGHT, Base::BORDER_SPACING, Base::BORDER_TOP, Base::BOX_SHADOW, Base::DIMENSIONS, Base::DIRECTIONS, Base::FONT_FAMILY, Base::FONT_SIZE, Base::FONT_WEIGHT, Base::LETTER_SPACING, Base::LINE_HEIGHT, Base::LINK_COLOR, Base::MARGIN, Base::MARGIN_BOTTOM, Base::MARGIN_LEFT, Base::MARGIN_RIGHT, Base::MARGIN_TOP, Base::MAX_WIDTH, Base::NONE, Base::PADDING_X, Base::PADDING_Y, Base::POUND_SIGN, Base::TEXT_ALIGN, Base::TEXT_DECORATION, Base::TEXT_SHADOW, Base::TEXT_SHADOW_BLUR, Base::TEXT_SHADOW_OFFSET, Base::VERTICAL_ALIGN, Base::WEBKIT_ANIMATION, Base::WHITE_SPACE, Base::ZERO_WIDTH_NON_BREAKING_SPACE, Base::ZERO_WIDTH_SPACE

Instance Method Summary collapse

Methods inherited from Responsive

presets

Instance Method Details

#render(tag, opt, ctx) ⇒ Object



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/inkcite/renderer/video_preview.rb', line 8

def render tag, opt, ctx

  # Get a unique ID for this video which will make its CSS classes
  # distinct from other videos in the email.
  uid = ctx.unique_id(:video)

  # Links need an ID
  id = opt[:id] || "video-preview#{uid}"

  # Grab the URL for the video - this is passed on to the {a} Helper and
  # the user will be warned appropriately if the URL is missing.
  href = opt[:href].freeze

  # Grab the name of the source file that can be optionally embeded with %1
  # which will increment for each frame (e.g. video%1.jpg becomes video1.jpg,
  # video2.jpg etc. up to the total number of frames).  The original source
  # image name is frozen to ensure it isn't modified later.
  src = opt[:src].freeze

  # This will hold all frame source file names interpolated to include
  # index (e.g. %1 being replaced with the frame number, if present).
  frames = []

  # For each frame, create a fully-qualified image source and
  # add it to the frame list.
  frame_count = (opt[:frames] || 1).to_i

  # True if the video clip will animate using smooth fading
  # between several frames of the video.
  has_animation = frame_count > 1

  # Iterate through the frames and replace %1 with the frame number.
  # this loop also verifies that the referenced image exists.
  frame_count.times do |n|
    frame_src = src.gsub('%1', "#{n + 1}")
    frames << image_url(frame_src, opt, ctx, false, false)
  end

  # Grab the first fully-qualified frame
  first_frame = frames[0]

  # Duration of the animated frame cycling, if multiple frames are provided.
  duration = (opt[:duration] || 15).to_i

  # Desired dimensions of the video clip.
  width = opt[:width].to_i
  height = opt[:height].to_i
  ctx.error("Video preview #{uid} is missing dimensions", { :width => width, :height => height, :src => src, :href => href }) unless width > 0 && height > 0

  # Calculate the scaled width for the left-side of the table
  # which is a crafty way to preserve the aspect ratio of the
  # video while it still fluidly scales.
  scaled_width = (width * SCALE).round

  # Background color and edge gradient - which defaults to a darker
  # version of the background color if not specified.
  bgcolor = detect_bgcolor(opt, '#5b5f66')
  gradient = opt[:gradient] || Util::darken(bgcolor, 0.25)

  # This is the name of the class applied to the anchor tag
  # to animate the hover.
  hover_klass = 'video'
  play_button_klass = 'play-button'

  # This is the name of the animation, if any, that will be
  # assigned to the table and defined in the CSS.
  animation_name = "#{hover_klass}#{uid}-frames"

  # Size calculations based on the specified arrow size or
  # the defaulted 30px arrow.  The border_* variables control
  # the circular border around the play arrow.
  play_arrow_size = (opt[PLAY_ARROW_SIZE] || 30).to_i
  play_arrow_height = (play_arrow_size * 0.5666).round
  play_border_radius = (play_arrow_size * 1.1333).round
  play_border_top_bottom = (play_arrow_size * 0.6).round
  play_border_right = (play_arrow_size * 0.5333).round
  play_border_left = (play_arrow_size * 0.8).round

  html = []
  html << '{not-outlook}'

  # Using an Element to produce the appropriate anchor helper with
  # the desired
  html << Element.new('a', { :id => id, :href => quote(href), :class => hover_klass, :bgcolor => bgcolor, :bggradient => gradient, :block => true }).to_helper

  table = Element.new('table', {
          :width => '100%', :background => first_frame, BACKGROUND_SIZE => 'cover',
          Table::TR_TRANSITION => %q("all .5s cubic-bezier(0.075, 0.82, 0.165, 1)")
      })

  # Will hold the Animation if animation is enabled for this
  # video-preview.
  animation = nil

  if has_animation
    animation = Animation.new(animation_name, ctx)
    animation.timing_function = Animation::EASE
    animation.duration = duration

    table[:animation] = quote(animation)
  end

  html << table.to_helper

  # Transparent spacer for preserving aspect ratio.
  spacer_image_name = "vp-#{scaled_width}x#{height}.png"
  spacer_image = File.join(ctx.email.image_dir, spacer_image_name)

  # Test if the file exists
  unless File.exist?(spacer_image)

    # Requiring on-demand, don't load chunky_png unless the user has
    # started using video_preview.
    require 'chunky_png'

    # Creating an image from scratch, save as an interlaced PNG
    png = ChunkyPNG::Image.new(scaled_width, height, ChunkyPNG::Color::TRANSPARENT)
    png.save(spacer_image, :interlace => true)

  end

  # Assembling the first <td> which manages the aspect ratio of the
  # video as a separate string to avoid unnecessary line breaks in
  # the resulting HTML.
  aspect_ratio_td = Element.new('td', :width => '25%').to_helper
  aspect_ratio_td << Element.new('img', { :src => ctx.image_url(spacer_image_name), :alt => quote(''), :width => '100%', :border => 0,
          :style => { :height => :auto, :display => :block, :opacity => 0, :visibility => :hidden } }).to_s
  aspect_ratio_td << '{/td}'
  html << aspect_ratio_td

  # Center column holds the CSS-based arrow
  html << Element.new('td', :width => '50%', :align => :center, :valign => :middle).to_helper

  # These are the arrow and circle border, respectively.  Not currently
  # configurable in terms of size or color.
  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)};">)
  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;">&nbsp;</div>)
  html << '</div>'

  html << '{/td}'
  html << '{td width=25%}&nbsp;{/td}'
  html << '{/table}'
  html << '{/a}'

  # Pre-loading the images prevents a flash that can occur because the
  # browser only loads the frames once the animation demands them.
  if has_animation && !opt[NO_PRELOAD]
    all_frames = frames.collect { |f| %Q(url(#{f})) }.join(',')
    html << Element.new('div', :style => { BACKGROUND_IMAGE => %Q(#{all_frames}), :display => 'none' }).to_s + '</div>'
  end

  # Concludes the if [if !vml] section targeting non-outlook.
  html << '{/not-outlook}'

  # Check for the outlook-src attribute which will be used in place of
  # the first frame if it is specified.
  outlook_src = opt[OUTLOOK_SRC]
  outlook_src = outlook_src.blank? ? first_frame : image_url(outlook_src, opt, ctx, false, false)

  html << '{outlook-only}'
  if ctx.vml_enabled?

    # Calculations necessary to render the play arrow in VML.
    outlook_arrow_size = (play_arrow_size * 2.6).round
    outlook_arrow_width = (play_arrow_size * 1.0666).round
    outlook_arrow_height = (play_arrow_size * 0.5333).round
    outlook_arrow_left = width / 2 - play_arrow_size / 2
    outlook_arrow_top = height / 2 - play_arrow_size / 2
    outlook_border_left = width / 2 - outlook_arrow_size / 2
    outlook_border_top = height / 2 - outlook_arrow_size / 2

    # Use the link central processing routine to ensure a viable link has
    # been provided and tag/track it from Outlook.
    outlook_id, outlook_href, target_blank = Link.process(id, href, false, ctx)

    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;">)
    html << %Q(<v:rect fill="t" stroked="f" style="position:absolute;width:#{width};height:#{height};"><v:fill src="#{outlook_src}" type="frame"/></v:rect>)
    html << %Q(<v:oval fill="t" strokecolor="white" strokeweight="4px" style="position:absolute;left:#{outlook_border_left};top:#{outlook_border_top};width:#{outlook_arrow_size};height:#{outlook_arrow_size}"><v:fill color="black" opacity="30%"/></v:oval>)
    html << %Q(<v:shape coordsize="#{play_border_left},#{outlook_arrow_width}" path="m,l,#{outlook_arrow_width},#{play_border_left},#{outlook_arrow_height},xe" fillcolor="white" stroked="f" style="position:absolute;left:#{outlook_arrow_left};top:#{outlook_arrow_top};width:#{play_arrow_size};height:#{play_arrow_size};"/>)
    html << '</v:group>'

    # Notify the context that VML was used in this version.
    ctx.vml_used!

  else

    # This is the only version that can support alt text.
    alt = opt[:alt] || 'Click to play'

    html << Element.new('a', { :id => id, :href => quote(href) }).to_helper +
        Element.new('img', { :src => quote(outlook_src), :height => height, :width => width, :alt => quote(alt) }).to_helper +
        '{/a}'

  end
  html << '{/outlook-only}'

  # Will hold any CSS styles, if there are some necessary
  # to inject into the email.
  styles = []

  # If this is the first video clip in the email, we need
  # to include the general styles shared across all clips.
  if uid == 1
    styles << ".#{hover_klass}:hover .#{play_button_klass} {"
    styles << '  transform: scale(1.1);'
    styles << '}'
    styles << ".#{hover_klass}:hover tr {"
    styles << '  background-color: rgba(255, 255, 255, .2);'
    styles << '}'
  end

  # If this video clip has animation, then we need to include
  # the keyframes necessary to smoothly animate between each.
  if has_animation

    # The time spent in each frame is based on a weighted distribution
    # of frames vs. transition time between frames.
    total_weight = ((FRAME_WEIGHT * frame_count) + (TRANSITION_WEIGHT * frame_count)).to_f
    percent_per_frame = (FRAME_WEIGHT / total_weight * 100.0).round
    percent_per_transition = (TRANSITION_WEIGHT / total_weight * 100.0).round

    # This will hold the total percentage as we increment toward the
    # end of the animation.
    percent = 0.0

    # Iterate through each frame and add two keyframes, the first
    # being the time at which the frame appears plus another frame
    # after the duration it should be on screen.
    frames.each do |f|
      this_frame_url = "url(#{f})"

      animation.add_keyframe(percent, { BACKGROUND_IMAGE => this_frame_url })
      percent += percent_per_frame
      animation.add_keyframe(percent, { BACKGROUND_IMAGE => this_frame_url })
      percent += percent_per_transition

    end

    # Transition back to the first frame.
    animation.add_keyframe(100, { BACKGROUND_IMAGE => "url(#{first_frame})" })

    # Add the keyframes to the styles array.
    styles << animation.to_keyframe_css

  end

  # Add the styles to the email's header
  ctx.styles << styles.join("\n") unless styles.blank?

  html.join("\n")

end