Friday 23 July 2021

Email: border-radius and box-shadow

I love working with designers; they constantly challenge me to develop new designs to implement. They have their lovely pixel-based programs and design beautiful things, which I then convert into different formats. This conversion is often relatively easy, but recently I had to create an email with the content within a rounded container, a rounded container with a drop shadow.

Let me tell you, that was a pain! I did a fair bit of research and found excellent ways of generating rounded corners (not least my own from back in the day). AE Writer has an article on drop shadow for HTML email, which sort of confirmed my thoughts on needing to use tables. Alejandro Vargas has an article up on Medium about HTML email rounded corners. I spent a fair few hours over last weekend taking a screengrab of a container into The Gimp and playing with contrast to generate the appropriate data format for a chunk of JS to generate the appropriate nested divs within a table.

Given this table row:

<tr data-row="5"
    data-description="6->3-fe->2-fd->2-fc->1-fb->1-fd->ff">

This code:

(()=>{

  const setStyles = (element, declarations) => {
    for (const prop in declarations) {
      if(declarations.hasOwnProperty(prop)){
        const property = prop.split(/(?=[A-Z])/).join('-').toLowerCase()
        element.style[property] = declarations[prop]
      }
    }
  }



  document.querySelectorAll('tr').forEach(tr => {
    if(tr.dataset.description){
      const description = tr.dataset.description.split('->')
      let target = tr
      const td = document.createElement('td')
      td.setAttribute('align', 'center')
      setStyles(td, {
        paddingLeft: `${description[0]}px`,
        paddingRight:`${description[0]}px`,
      })
      if(!tr.dataset.main){
        setStyles(td, {
          height:'1px',
          lineHeight:'1px',
          fontSize:'1px'
        })
      }
      target.appendChild(td)
      target = td
      description.shift()
      for(let i = 0; i < description.length; i++){
        const parts = description[i].split('-')
        const div = document.createElement('div')
        setStyles(div, {
          display:'block',
          paddingLeft:'0px',
          paddingRight:'0px',
          backgroundColor:`#${parts[0].repeat(3)}`,
          width: '100% !important',
          minWidth: 'initial !important',
        })
        if(parts.length !== 1){
          setStyles(div, {
            paddingLeft: `${parts[0]}px`,
            paddingRight:`${parts[0]}px`,
            backgroundColor:`#${parts[1].repeat(3)}`,
          })
        }else{
          setStyles(div, {
            backgroundColor:`#${parts[0].repeat(3)}`,
          })
        }
        if(!tr.dataset.main){
          setStyles(div, {
            height:'1px',
            lineHeight:'1px',
            fontSize:'1px'
          })
        }
        target.appendChild(div)
        target = div
      }
    }
  })
})()

Would generate this markup:

<tr data-row="5"
    data-description="6->3-fe->2-fd->2-fc->1-fb->1-fd->ff">
  <td align="center"
      style="padding-left: 6px; padding-right: 6px; height: 1px; line-height: 1px; font-size: 1px;">
    <div style="display: block; padding-left: 3px; padding-right: 3px; background-color: rgb(254, 254, 254); height: 1px; line-height: 1px; font-size: 1px;">
      <div style="display: block; padding-left: 2px; padding-right: 2px; background-color: rgb(253, 253, 253); height: 1px; line-height: 1px; font-size: 1px;">
        <div style="display: block; padding-left: 2px; padding-right: 2px; background-color: rgb(252, 252, 252); height: 1px; line-height: 1px; font-size: 1px;">
          <div style="display: block; padding-left: 1px; padding-right: 1px; background-color: rgb(251, 251, 251); height: 1px; line-height: 1px; font-size: 1px;">
            <div style="display: block; padding-left: 1px; padding-right: 1px; background-color: rgb(253, 253, 253); height: 1px; line-height: 1px; font-size: 1px;">
              <div style="display: block; padding-left: 0px; padding-right: 0px; background-color: rgb(255, 255, 255); height: 1px; line-height: 1px; font-size: 1px;">
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </td>
</tr>

That whole manual process was boring, though, so I decided to automate the process. Especially as, knowing designers, I just knew that the box-shadow or border-radius would need to change in the future.

I knew that libraries for generating images from DOM elements were available, so I tried a couple. html2canvas wasn't quite what I was looking for, but dom-to-image worked a treat!

I decided to take an incremental approach and started by copying the dom to a png image format and placing that png within a canvas element of the same size as the element. This process is the code within the Immediately Invoked Function Expression (IIFE) at the bottom of the file. One thing to take note of is the onload function. I ran into all sorts of issues with the subsequent scripts failing until I clocked that the img wasn't loaded when I tried to manipulate it. Once we've set drawn the image atop the canvas, we add some data attributes using the getDimension function - I wouldn't've bothered with this except WebStorm kept complaining about the amount of repeated code I had.

trim, invoked at the end of the IIFE, strips out the remaining white space around the image, leaving us with an image that has only grey-scale colours surrounding it (except at the corners). It trims the rows and columns which contain only white colour values by referencing the values from getDimension. getDimension was clever and checked the values from iterating over the data from getImageData, if any value was not 255 then we had something what was not pure white. The array from getImageData should be chunked into sub-arrays of four as each lump of four values represent the RGBA value from a single pixel.

Once we have a trimmed image, we can build the values that equate with the data attribute we had in the original implementation. I created a simple class for this as a simple array wouldn't work here as I needed more than just the array of values; I needed to know which was the repeating row, so we had a placeholder for the actual content.

We chunk the data into sub-arrays of four and grab the hex colour value from each chunk. If the preceding HEX value is identical, the preceding classes incidence count is incremented; if not, it's added to the row array. If the row is not identical to the preceding row, then it's added to the rows as a Row object; if it is identical then the preceding Row has it's main value changed to true - this will be our placeholder.

We then build our table using the array of Row objects (rows) using code that is very similar to the one above but that is ever so slightly more nuanced and places a placeholder table within the main row. Nice eh? I'm quite pleased with it.

No comments:

Post a Comment