I was trying to create some laptop mockups from scratch with CSS 3D transforms (pen) and I noticed that it was harder than I thought. Here's the code I was messing with:
transform: scaleX(1) scaleY(1) scaleZ(1)
rotateX(48deg) rotateY(-35deg) rotateZ(37deg)
translateX(360px) translateY(103px) translateZ(0px)
skewX(0) skewY(0);
I haven't yet worked out how to do it, but I believe that if we can get the corners of the image on top of the corresponding corners of the laptop image, then it will look realistic. Am I wrong?
I think that this could be done with a function that takes 5 inputs (ratio of source image's rectangle and the coordinates of the four corners of the destination (laptop) image) and returns an object containing valid CSS properties for layering the image.
Would you have any insight on how it could be done?
This Mathematics question has the doorway:
It leads to these nice live examples:
Here's the pen from the article ported from CoffeeScript to this answer in plain JavaScript:
// Original was CoffeeScript at https://codepen.io/fta/pen/ifnqH?editors=0010.
// This is ported back to plain JavaScript. It could be cleaned up!
{
const $ = jQuery;
function getTransform (from, to) {
console.assert(from.length === to.length && to.length === 4);
const A = []; // 8x8
for (let i = 0; i < 4; i++) {
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]);
}
const b = []; // 8x1
for (let i = 0; i < 4; i++) {
b.push(to[i].x);
b.push(to[i].y);
}
// Solve A * h = b for h
const h = numeric.solve(A, b);
const 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 (let i = 0; i < 4; i++) {
const lhs = numeric.dot(H, [from[i].x, from[i].y, 0, 1]);
const k_i = lhs[3];
const 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);
}
return H;
};
function applyTransform(element, originalPos, targetPos, callback) {
// All offsets were calculated relative to the document
// Make them relative to (0, 0) of the element instead
const from = (function() {
const results = [];
for (let k = 0, len = originalPos.length; k < len; k++) {
const p = originalPos[k];
results.push({
x: p[0] - originalPos[0][0],
y: p[1] - originalPos[0][1]
});
}
return results;
})();
const to = (function() {
const results = [];
for (let k = 0, len = targetPos.length; k < len; k++) {
const p = targetPos[k];
results.push({
x: p[0] - originalPos[0][0],
y: p[1] - originalPos[0][1]
});
}
return results;
})();
// Solve for the transform
const 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(${((function() {
const results = [];
for (let i = 0; i < 4; i++) {
results.push((function() {
const results1 = [];
for (let j = 0; j < 4; j++) {
results1.push(H[j][i].toFixed(20));
}
return results1;
})());
}
return results;
})()).join(',')})`,
'transform-origin': '0 0'
});
return typeof callback === "function" ? callback(element, H) : void 0;
};
function makeTransformable(selector, callback) {
return $(selector).each(function(i, element) {
$(element).css('transform', '');
// Add four dots to corners of `element` as control points
const controlPoints = (function() {
const ref = ['left top', 'left bottom', 'right top', 'right bottom'];
const results = [];
for (let k = 0, len = ref.length; k < len; k++) {
const position = ref[k];
results.push($('<div>').css({
border: '10px solid black',
borderRadius: '10px',
cursor: 'move',
position: 'absolute',
zIndex: 100000
}).appendTo('body').position({
at: position,
of: element,
collision: 'none'
}));
}
return results;
})();
// Record the original positions of the dots
const originalPos = (function() {
const results = [];
for (let k = 0, len = controlPoints.length; k < len; k++) {
const p = controlPoints[k];
results.push([p.offset().left, p.offset().top]);
}
return results;
})();
// Transform `element` to match the new positions of the dots whenever dragged
$(controlPoints).draggable({
start: () => {
return $(element).css('pointer-events', 'none'); // makes dragging around iframes easier
},
drag: () => {
return applyTransform(element, originalPos, (function() {
const results = [];
for (let k = 0, len = controlPoints.length; k < len; k++) {
const p = controlPoints[k];
results.push([p.offset().left, p.offset().top]);
}
return results;
})(), callback);
},
stop: () => {
applyTransform(element, originalPos, (function() {
const results = [];
for (let k = 0, len = controlPoints.length; k < len; k++) {
const p = controlPoints[k];
results.push([p.offset().left, p.offset().top]);
}
return results;
})(), callback);
return $(element).css('pointer-events', 'auto');
}
});
return element;
});
};
makeTransformable('.box', function(element, H) {
console.log($(element).css('transform'));
return $(element).html($('<table>').append($('<tr>').html($('<td>').text('matrix3d('))).append((function() {
const results = [];
for (let i = 0; i < 4; i++) {
results.push($('<tr>').append((function() {
const results1 = [];
for (let j = 0; j < 4; j++) {
results1.push($('<td>').text(H[j][i] + ((i === j && j === 3) ? '' : ',')));
}
return results1;
})()));
}
return results;
})()).append($('<tr>').html($('<td>').text(')'))));
});
}
.box {
margin: 20px;
padding: 10px;
height: 150px;
width: 500px;
border: 1px solid black;
}
<div class="box">
Drag the points to transform the box!
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.2/jquery-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/numeric/1.2.6/numeric.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.3/jquery.ui.touch-punch.min.js"></script>