nakome
11/26/2014 - 3:25 AM

Drag to transform

Drag to transform

.box {
  margin: 20px;
  padding: 10px;
  height: 150px;
  width: 500px;
  border: 1px solid black;
}
$ = jQuery

getTransform = (from, to) ->
  console.assert from.length == to.length == 4

  A = [] # 8x8
  for i in [0 ... 4]
    A.push [from[i].x, from[i].y, 1, 0, 0, 0, -from[i].x * to[i].x, -from[i].y * to[i].x]
    A.push [0, 0, 0, from[i].x, from[i].y, 1, -from[i].x * to[i].y, -from[i].y * to[i].y]

  b = [] # 8x1
  for i in [0 ... 4]
    b.push to[i].x
    b.push to[i].y

  # Solve A * h = b for h
  h = numeric.solve(A, b)

  H = [[h[0], h[1], 0, h[2]],
       [h[3], h[4], 0, h[5]],
       [   0,    0, 1,    0],
       [h[6], h[7], 0,    1]]

  # Sanity check that H actually maps `from` to `to`
  for i in [0 ... 4]
      lhs = numeric.dot(H, [from[i].x, from[i].y, 0, 1])
      k_i = lhs[3]
      rhs = numeric.dot(k_i, [to[i].x, to[i].y, 0, 1])
      console.assert(numeric.norm2(numeric.sub(lhs, rhs)) < 1e-9, "Not equal:", lhs, rhs)
  H

applyTransform = (element, originalPos, targetPos, callback) ->
  # All offsets were calculated relative to the document
  # Make them relative to (0, 0) of the element instead
  from = for p in originalPos
    x: p[0] - originalPos[0][0]
    y: p[1] - originalPos[0][1]
  to = for p in targetPos
    x: p[0] - originalPos[0][0]
    y: p[1] - originalPos[0][1]

  # Solve for the transform
  H = getTransform(from, to)
  
  # Apply the matrix3d as H transposed because matrix3d is column major order
  # Also need use toFixed because css doesn't allow scientific notation
  $(element).css
    'transform': "matrix3d(#{(H[j][i].toFixed(20) for j in [0...4] for i in [0...4]).join(',')})"
    'transform-origin': '0 0'

  callback?(element, H)

makeTransformable = (selector, callback) ->
  $(selector).each (i, element) ->
    $(element).css('transform', '')
    
    # Add four dots to corners of `element` as control points
    controlPoints = for position in ['left top', 'left bottom', 'right top', 'right bottom']
      $('<div>')
        .css
          border: '10px solid black'
          borderRadius: '10px'
          cursor: 'move'
          position: 'absolute'
          zIndex: 100000
        .appendTo 'body'
        .position
          at: position
          of: element
          collision: 'none'

    # Record the original positions of the dots
    originalPos = ([p.offset().left, p.offset().top] for p in controlPoints)
    
    # Transform `element` to match the new positions of the dots whenever dragged
    $(controlPoints).draggable
      start: =>
        $(element).css('pointer-events', 'none') # makes dragging around iframes easier 
      drag: => 
        applyTransform(element, originalPos, ([p.offset().left, p.offset().top] for p in controlPoints), callback)
      stop: => 
        applyTransform(element, originalPos, ([p.offset().left, p.offset().top] for p in controlPoints), callback)
        $(element).css('pointer-events', 'auto')

    element

makeTransformable('.box', (element, H) ->
  console.log($(element).css('transform'))
  $(element).html(
    $('<table>')
      .append(
        $('<tr>').html(
          $('<td>').text('matrix3d(')
        )
      )
      .append(
        for i in [0 ... 4]
          $('<tr>').append(
            for j in [0 ... 4]
              $('<td>').text(H[j][i] + if i == j == 3 then '' else ',')
          )
      )
      .append(
        $('<tr>').html(
          $('<td>').text(')')
        )
      )
  )
)
<div class="box">
    Drag the points to transform the box!
</div>

Drag to transform

Allows you to figure out the CSS matrix3d transform for an element in a WYSIWYG fashion.

Blog post: http://franklinta.com/2014/09/08/computing-css-matrix3d-transforms/

Alternative usage: http://codepen.io/fta/pen/LHonf

A Pen by Franklin Ta on CodePen.

License.