Bringing an iPad Sketch to Life on the Page

Drawing on the iPad with Procreate removes most of the friction that comes with traditional media: no setup, no cleanup, no ventilation. But the real payoff is what you can do with the layered files afterward. A finished painting can become a lively web graphic with just a little CSS and JavaScript.

The demo below starts with a Procreate painting of a zebra, then adds two effects: a subtle parallax on hover and a hand-drawn stroke effect on load.

Both effects rely on layers, which Procreate handles well. Keeping related elements—like the zebra's stripes and dots—on separate layers makes it easy to export and manipulate them independently in a browser.

Planning Layers for Parallax

When setting up layers for a parallax effect, extend each one beyond the edges of the layer above it. If a line ends abruptly, it will look unnatural when the image shifts. The extra canvas area gives you room to move things around without exposing hard edges.

Procreate exports to Photoshop (PSD) format, which keeps all layers intact. From there, you can merge some layers to keep the total manageable—around eight is a good target—and export each one as a separate image. A tool like tinyPNG handles the compression well.

Setting Up the Parallax Container

In the code editor, start with a parent div that holds all the layer images. The parent gets position: relative, and every image inside it gets position: absolute, stacking them on top of each other.

<div id="zebra-ill" role="presentation">
  <img class="zebraimg" src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/zebraexport6.png' />
  <img class="zebraimg" src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/zebraexport5.png' />
 …
</div>
#zebra-ill {
  position: relative;
  min-height: 650px;
  max-width: 500px;
}

.zebraimg {
  position: absolute;
  top: 0;
  left: 0;
  perspective: 600px;
  transform-style: preserve-3d;
  transform: translateZ(0);
  width: 100%;
  }

Setting each image to width: 100% confines every layer to the parent's dimensions, which makes responsive behavior easier to control. The parent itself should have max-width and min-height constraints so it stays flexible without collapsing completely—especially when placed inside a CSS Grid layout.

For the interactivity, attach a mousemove event listener to the parent div. Inside the handler, e.clientX and e.clientY give you the cursor coordinates, which you can then apply to the layers as transform values.

const zebraIll = document.querySelector('#zebra-ill')

// Hover
zebraIll.addEventListener('mousemove', e => {
  let x = e.clientX;
  let y = e.clientY;
})

The raw coordinates will produce way too much movement. Multiply each value by a small factor—around 0.05—to slow things down. Using the layer's index in the stack lets you vary the speed slightly per layer, creating a subtle depth effect instead of a jarring slide.

const zebraIll = document.querySelector('#zebra-ill')
const zebraIllImg = document.querySelectorAll('.zebraimg')
const rate = 0.05

// Hover
zebraIll.addEventListener('mousemove', e => {
  let x = e.clientX;
  let y = e.clientY;
  
  zebraIllImg.forEach((el, index) => {
    let speed = index += 1
    let xPos = speed + rate * x
    let yPos = speed + rate * y
    
    el.style.transform = 
      `rotateX(${xPos - 20}deg) rotateY(${yPos - 20}deg) translateZ(${index * 10}px)`
  })
})

Not everyone wants that motion. A checkbox gives users the option to disable the parallax entirely, which is a thoughtful touch for those with vestibular disorders who may prefer reduced motion.

<p>
  <input type="checkbox" name="motiona11y" id="motiona11y" />
  <label for="motiona11y">If you have a vestibular disorder, check this to turn off some of the effects</label>
</p>
const zebraIll = document.querySelector('#zebra-ill')
const zebraIllImg = document.querySelectorAll('.zebraimg')
const rate = 0.05
const motioncheck = document.getElementById('motiona11y')
let isChecked = false

// Check to see if someone checked the vestibular disorder part
motioncheck.addEventListener('change', e => {
  isChecked = e.target.checked;
})

// Hover
zebraIll.addEventListener('mousemove', e => {
  if (isChecked) return
  let x = e.clientX;
  let y = e.clientY;
  
  // ...
})

The Drawn-On Look

The classic way to simulate drawing on a page is with SVG paths: make a path dashed with dashoffset, set the dash to the full length of the path, then animate the offset to reveal the line progressively. That technique works well for mechanical drawings.

But hand-drawn lines are tapered—thicker in some sections, thinner in others—and tapered lines aren't yet supported on the web. To achieve that effect, you need a workaround involving a graphics editor and a clipping mask.

The process goes like this:

  • Trace the relevant lines in Illustrator.
  • Apply a tapered stroke via the Stroke panel's “More options.”
  • Duplicate the lines and give the duplicates uniform, thicker strokes underneath.
  • Convert the tapered lines into a compound path, simplify the path points, and use it as a clipping mask.

The tapered lines sit on top, and the fat uniform paths sit beneath them. When you animate the thick paths onto the page with a plugin like drawSVG and GreenSock, the tapered strokes reveal themselves naturally, giving the impression of a hand drawing the image in real time.

Screenshot of the Illustrator Stroke menu.

drawSVG requires a few setup steps:

  • Load the plugin script.
  • Register the plugin at the top of the JavaScript file.
  • Make sure you're targeting the paths themselves, not the groups that contain them—though parent groups can work in some cases.
  • Confirm that the paths have strokes applied.

With those pieces in place, you animate the thick, uniform paths and let the clipping mask do the rest.