class Inkcite::Renderer::Slant

@helper Slanted Edge @summary Bulletproof, responsive, sloped edges for modern email designs

@description The {slant} Helper provides consistent, reliable sloped edges between sections of your email. You can customize the size, color and dimensions of the slant. It renders using CSS in modern email clients and has a VML fallback for Outlook.

@warning Slants are not compatible with Outlook 2016 which renders a thin, white border around the slant. @warning Older email clients render angled CSS borders without anti-aliasing

@credit Slanted edges based on @M_J_Robbins sloped edges for email concept. codepen.io/M_J_Robbins/pen/rpzLNx

@usage {div width=600 padding=15 bgcolor=#009}

...

{/div} {slant bgcolor=#009 color=#909 bottom right width=600 height=50} {div width=600 padding=15 bgcolor=#909}

...

{/div}

Constants

COORDINATE_SCALE

Arbitrary multiplier present in the original codepen.

IF_OUTLOOK_LT_2016

This is the MSO conditional used to prevent the slant from appearing in Outlook 2016 where it has thin lines around the VML according to Litmus results.

NO_FALLBACK

Flags allowing the user to disable VML fallbacks.

NO_VML
NO_WRAP

Tag allowing the user to disable the wrap element if they don't want it

TRANSPARENT

Static constant for the color used where the border is transparent to create the slanted edge.

Public Instance Methods

render(tag, opt, ctx) click to toggle source
# File lib/inkcite/renderer/slant.rb, line 26
def render tag, opt, ctx

  html = ''

  # @attribute no-fallback If present, disables VML fallback so the slant doesn't appear in Outlook 2007-2013.; alias no-vml
  no_vml = opt[NO_FALLBACK] || opt[NO_VML]

  # Check to see if VML is enabled for this email
  include_fallback = ctx.vml_enabled? && !no_vml

  # The first time the slant is used, we need to initialize the VML shapes
  # and the responsive class that will be used to scale this.
  if include_fallback
    if ctx.once?(:slant)

      # Notify the context that VML has been used in this email.
      ctx.vml_used!

      html << IF_OUTLOOK_LT_2016

      # These need to be wrapped in a div with zero height otherwise unwanted whitespace will
      # appear in the email where these shapes are initialized.
      # https://litmus.com/community/discussions/538-vml-outlook-07-10-13-unwanted-20px-padding-at-the-bottom
      html << '{div font-size=0 line-height=0}'

      html << render_vml_triangle('stl', [ [0, 1], [1, 0], [0, 0] ])
      html << render_vml_triangle('str', [ [0, 0], [1, 0], [1, 1] ])
      html << render_vml_triangle('sbl', [ [0, 0], [1, 1], [0, 1] ])
      html << render_vml_triangle('sbr', [ [0, 1], [1, 1], [1, 0] ])

      html << '{/div}'
      html << '{/if}'

    end
  end

  # @attribute height The height of the slope in pixels; required
  height = opt[:height].to_i
  # @attribute width The width of the slope in pixels; required
  width = opt[:width].to_i
  if height <= 0 || width <= 0
    ctx.error('Missing slant dimensions', opt)
    return nil
  end

  # Pre-calculate half the width and height of the slant.
  half_height = (height / 2.0).round(0)
  half_width = (width / 2.0).round(0)

  # @attribute color The foreground color of the slanted edge.; default #234
  color = hex(opt[:color] || '#234')

  directions = [
      (opt[:bottom] ? :bottom : :top),
      (opt[:left] ? :left : :right)
  ]

  border_colors = []
  DIRECTIONS.each do |d|
    next if d.nil?
    border_colors << (directions.include?(d) ? color : TRANSPARENT)
  end

  # @attribute no-wrap If present, disables the table that wraps the slant. Use this if the slant is already in a container (e.g. table or div) that provides width and background color.
  no_wrap = opt[NO_WRAP]
  unless no_wrap
    wrap_div = Element.new('table')
    wrap_div[:width] = width

    # @attribute bgcolor The color that appears behind the slanted edge; default transparent; alias background-color
    bgcolor = detect_bgcolor(opt)
    wrap_div[:bgcolor] = bgcolor unless none?(bgcolor)

    wrap_div[:mobile] = :fill
    html << wrap_div.to_helper
    html << '{td}'
  end

  # This zero-sized div will have beefed up borders that create the
  # slant effect.
  slant_div = Element.new('div')
  slant_div.style[BORDER_COLOR] = border_colors.join(' ')
  slant_div.style[BORDER_STYLE] = :solid
  slant_div.style[BORDER_WIDTH] = "#{px(half_height)} #{px(half_width)}"
  slant_div.style[MSO_HIDE] = :all

  # Fix to trigger antialiasing in the slanted border on webkit clients
  # like Outlook for iOS
  # https://stackoverflow.com/a/27506977
  slant_div.style[:'-webkit-transform'] = 'rotate(0.0005deg)'

  # Create a custom mobile class name for this slant.  Then check to
  # see if that responsive rule has already been registered - otherwise
  # add it.
  klass = "slant#{half_height}"
  slant_rule = ctx.media_query.find_by_klass(klass) || Rule.new('div', klass, "border-width: #{px(half_height)} 50vw !important;")
  ctx.media_query << slant_div.add_rule(slant_rule)

  html << slant_div.to_s
  html << '</div>'

  if include_fallback
    backward = false;

    vshape = Element.new('v:shape')

    # Generate the unique VML shape ID from the direction of the
    # 90-degree corner.
    type_id = "s" # Slant
    type_id << (opt[:bottom] ? 'b' : 't')
    type_id << (opt[:left] ? 'l' : 'r')

    vshape[:type] = quote(type_id)
    vshape.style[:width] = px(width)
    vshape.style[:height] = px(height)
    vshape.style[:'mso-position-horizontal'] = :center
    vshape[:fillcolor] = color
    vshape[:stroked] = :f

    # Render the outlook-only VML-based fallback.  Once again, it needs
    # to be wrapped in a zero-height div to prevent unwanted space from
    # being injected on certain version of outlook.
    html << IF_OUTLOOK_LT_2016
    html << '{div font-size=0 line-height=0}'
    html << vshape.to_s
    html << '<o:lock selection="t"/>'
    html << '</v:shape>'
    html << '{/div}'
    html << '{/if}'

  end

  # Close the wrap element.
  html << '{/td}{/table}' unless no_wrap

  html
end

Private Instance Methods

render_vml_coordinate(coord) click to toggle source
# File lib/inkcite/renderer/slant.rb, line 166
def render_vml_coordinate coord
  x, y = coord
  "#{x * COORDINATE_SCALE},#{y * COORDINATE_SCALE}"
end
render_vml_triangle(id, coords) click to toggle source
# File lib/inkcite/renderer/slant.rb, line 171
def render_vml_triangle id, coords

  path = ''
  path << 'm' # moveto
  path << render_vml_coordinate(coords[0])
  path << 'l' # lineto
  path << render_vml_coordinate(coords[1])
  path << ','
  path << render_vml_coordinate(coords[2])
  path << 'x' # close
  path << 'e' # end

  %Q(<v:shapetype id="#{id}" path="#{path}" xmlns:v="urn:schemas-microsoft-com:vml"/>)
end