Interactive 3D Boxart
Below are the examples, some notes, and the code to some interactive boxarts that I made for Iron Fist Cube 2 in 2009 and 2015.
Examples
Below are some examples using cover art for Iron Fist Cube 2.
About
Implemented Changes
- Added support to pinch to zoom in-page to view model closer and allow panning (See "Known Bugs" below).
Possible Upgrades
Possible upgrades could include switching to three.js's TrackballControls as shown in this example.
Known Issues
General Issues
- On touch input devices, switching between native window-based pinch-to-zoom and trackball rotation capture requires a separate successive swipe or pinch.
- On iOS touch devices, the touch area for the box is limited to a plane, and when orthogonal to the screen can be too thin to register a hit.
- On Chrome in Windows 10 and iOS Safari,
-webkit-box-reflect
reflections only render whats on the screen, and disapear while scrolling off the page. Disable them if its a problem. - On Firefox and Edge in Windows 10,
-webkit-box-reflect
reflections are not supported. This is the reason for the empty padding at the bottom. Currently no checks are in place to check for this feature support to selectively enable this padding.
Machine-Specific Issues
A list of computer configurations and exibited issues is shown below.
Configuration | Issues |
---|---|
Acer Nitro 5 w/ R5 2500U (using integrated Vega 8 and amd gpu driver) OpenSUSE Tumbleweed (GNU + Linux) Firefox Version 85.0.2 (64-bit) | Boxart has weird clipping that limits view to top left quarter, and flicker while rotating. |
Code
HTML
Note that in the above examples I do not make use the #stage-target
, as I like having the end user interact with the boxart directly in a physical-like manner as if they would in real life. If you choose to do so as well you will need to remove or comment-out the <div id="stage-target" class="stage-target"></div>
and will need to carry through this exlusion in the in-page JavaScript below.
<div class="boxart-wrapper">
<h3 style="text-align:center;color:#fff;">Boxart Title</h3>
<div id="stage" class="boxart-stage">
<div class="boxart">
<div class="boxart__side boxart__side--front">
<img src="/image/path/to/image.front.png"/>
</div>
<div class="boxart__side boxart__side--top">
<img src="/image/path/to/image.top.png"/>
</div>
<div class="boxart__side boxart__side--bottom">
<img src="/image/path/to/image.bottom.png"/>
</div>
<div class="boxart__side boxart__side--back">
<img src="/image/path/to/image.back.png"/>
</div>
<div class="boxart__side boxart__side--left">
<img src="/image/path/to/image.left.png"/>
</div>
<div class="boxart__side boxart__side--right">
<img src="/image/path/to/image.right.png"/>
</div>
</div>
<div id="stage-target" class="stage-target"></div>
</div>
</div>
CSS
.boxart-stage {
-webkit-box-reflect: below -120px -webkit-gradient(linear, left top, left bottom, from(transparent), color-stop(0.5, transparent), to(#ffffff));
display: block;
left: 50%;
margin: 20px 0 0 -300px;
-webkit-perspective: 700px;
-moz-perspective: 700px;
-ms-perspective: 700px;
-o-perspective: 700px;
perspective: 700px;
-webkit-perspective-origin: 50% 50%;
-moz-perspective-origin: 50% 50%;
-o-perspective-origin: 50% 50%;
-ms-perspective-origin: 50% 50%;
perspective-origin: 50% 50%;
position: relative;
width: 600px;
height: 600px;
}
.boxart-stage__target {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.boxart-wrapper {
border-radius: 8px;
padding: 16px;
padding-bottom: 250px;
}
.boxart-wrapper--l {
background: #001;
background: #0018;
}
.boxart-wrapper--r {
background: #110;
background: #1108;
}
@media (min-width: 768px) { /* between Tablet & Desktop */
.boxart-wrapper--l, .boxart-wrapper--r {
// width: 50%;
width: 49%;
}
.boxart-wrapper--l {
float: left;
margin-right: 1%;
}
.boxart-wrapper--r {
float: right;
margin-left: 1%;
}
}
.boxart {
cursor: move;
margin-top: -177px;
margin-left: -125px;
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: rotate3d(1,0,0,-40deg);
-moz-transform: rotate3d(1,0,0,-40deg);
-ms-transform: rotate3d(1,0,0,-40deg);
-o-transform: rotate3d(1,0,0,-40deg);
transform: rotate3d(1,0,0,-40deg);
-webkit-transform-style: preserve-3d;
-moz-transform-style: preserve-3d;
-ms-transform-style: preserve-3d;
-o-transform-style: preserve-3d;
transform-style: preserve-3d;
width: 250px;
height: 355px;
}
.boxart__side {
-webkit-backface-visibility: hidden;
-moz-backface-visibility: hidden;
-ms-backface-visibility: hidden;
-o-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.7);
-moz-box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.7);
-o-box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.7);
-ms-box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.7);
box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.7);
display: block;
margin-top: -177px;
margin-left: -117px;
overflow: hidden;
position: absolute;
top: 50%;
left: 50%;
-webkit-transform-style: flat;
-moz-transform-style: flat;
-o-transform-style: flat;
transform-style: flat;
}
.boxart__side img {
border-radius: 0px;
width: 100%;
height: 100%;
z-index: 10;
}
.boxart__side--front, .boxart__side--back {
width: 250px;
height: 355px;
}
.boxart-wrapper--r .boxart__side--front, .boxart-wrapper--r .boxart__side--back {
width: 272px;
}
.boxart__side--front {
-webkit-transform: translate3d(0px, 0px, 25px);
-moz-transform: translate3d(0px, 0px, 25px);
-o-transform: translate3d(0px, 0px, 25px);
transform: translate3d(0px, 0px, 25px);
}
.boxart__side--back {
-webkit-transform: rotateY(180deg) translate3d(0px, 0px, 25px);
-moz-transform: rotateY(180deg) translate3d(0px, 0px, 25px);
-o-transform: rotateY(180deg) translate3d(0px, 0px, 25px);
-transform: rotateY(180deg) translate3d(0px, 0px, 25px);
}
.boxart__side--top, .boxart__side--bottom {
width: 250px;
height: 50px;
}
.boxart-wrapper--r .boxart__side--top, .boxart-wrapper--r .boxart__side--bottom {
width: 272px;
}
.boxart__side--top {
-webkit-transform: rotateX(90deg) translate3d(0px, 0px, 25px);
-moz-transform: rotateX(90deg) translate3d(0px, 0px, 25px);
-o-transform: rotateX(90deg) translate3d(0px, 0px, 25px);
-transform: rotateX(90deg) translate3d(0px, 0px, 25px);
}
.boxart__side--bottom {
-webkit-transform: rotateX(-90deg) translate3d(0px, 0px, 330px);
-moz-transform: rotateX(-90deg) translate3d(0px, 0px, 330px);
-o-transform: rotateX(-90deg) translate3d(0px, 0px, 330px);
-transform: rotateX(-90deg) translate3d(0px, 0px, 330px);
}
.boxart__side--left, .boxart__side--right {
width: 50px;
height: 355px;
}
.boxart__side--left {
-webkit-transform: rotateY(-90deg) translate3d(0px, 0px, 25px);
-moz-transform: rotateY(-90deg) translate3d(0px, 0px, 25px);
-o-transform: rotateY(-90deg) translate3d(0px, 0px, 25px);
-transform: rotateY(-90deg) translate3d(0px, 0px, 25px);
}
.boxart__side--right {
-webkit-transform: rotateY(90deg) translate3d(0px, 0px, 225px);
-moz-transform: rotateY(90deg) translate3d(0px, 0px, 225px);
-o-transform: rotateY(90deg) translate3d(0px, 0px, 225px);
-transform: rotateY(90deg) translate3d(0px, 0px, 225px);
}
.boxart-wrapper--r .boxart__side--right {
-webkit-transform: rotateY(90deg) translate3d(0px, 0px, 247px);
-moz-transform: rotateY(90deg) translate3d(0px, 0px, 247px);
-o-transform: rotateY(90deg) translate3d(0px, 0px, 247px);
-transform: rotateY(90deg) translate3d(0px, 0px, 247px);
}
JavaScript
Make sure to load the following 2 JavaScript files and the in-page script in the order they are shown from top to bottom.
traqball.js
/*
* traqball 2.2
* written by Dirk Weber
* http://www.eleqtriq.com/
* See demo at: http://www.eleqtriq.com/wp-content/static/demos/2011/traqball2011
* Copyright (c) 2011 Dirk Weber (http://www.eleqtriq.com)
* Licensed under the MIT (http://www.eleqtriq.com/wp-content/uploads/2010/11/mit-license.txt)
*/
(function() {
var userAgent = navigator.userAgent.toLowerCase(),
startEvent = document.createEvent('HTMLEvents'),
rotateEvent = document.createEvent('HTMLEvents'),
releaseEvent = document.createEvent('HTMLEvents'),
fullStopEvent = document.createEvent('HTMLEvents'),
canTouch = "ontouchstart" in window,
prefix = cssPref = "",
requestAnimFrame, cancelAnimFrame;
if (/webkit/gi.test(userAgent)) {
prefix = "-webkit-";
cssPref = "Webkit";
} else if (/msie | trident/gi.test(userAgent)) {
prefix = "-ms-";
cssPref = "ms";
} else if (/mozilla/gi.test(userAgent)) {
prefix = "-moz-";
cssPref = "Moz";
} else if (/opera/gi.test(userAgent)) {
prefix = "-o-";
cssPref = "O";
} else {
prefix = "";
}
startEvent.initEvent("traqballStartRotate", true, true);
rotateEvent.initEvent("traqballRotate", true, true);
releaseEvent.initEvent("traqballRelease", true, true);
fullStopEvent.initEvent("traqballFullStop", true, true);
function bindEvent(target, type, callback, remove) {
//translate events
var evType = type || "touchend",
mouseEvs = ["mousedown", "mouseup", "mousemove"],
touchEvs = ["touchstart", "touchend", "touchmove"],
remove = remove || "add";
evType = canTouch ? evType : mouseEvs[touchEvs.indexOf(type)];
target[remove + "EventListener"](evType, callback, false);
}
function getCoords(eventObj) {
var xTouch,
yTouch;
if (eventObj.type.indexOf("mouse") > -1) {
xTouch = eventObj.pageX;
yTouch = eventObj.pageY;
} else if (eventObj.type.indexOf("touch") > -1) {
//only do stuff if 1 single finger is used:
if (eventObj.touches.length === 1) {
var touch = eventObj.touches[0];
xTouch = touch.pageX;
yTouch = touch.pageY;
}
}
return [xTouch, yTouch];
}
function getStyle(target, prop) {
var style = document.defaultView.getComputedStyle(target, "");
return style.getPropertyValue(prop);
}
requestAnimFrame = (function() {
return window[cssPref + "RequestAnimationFrame"] ||
function(callback) {
window.setTimeout(callback, 17);
};
})();
cancelAnimFrame = (function() {
return window[cssPref + "CancelRequestAnimationFrame"] ||
clearTimeout;
})();
var Traqball = function(confObj) {
this.config = {
stage: document.body
};
this.box = null;
this.setup(confObj);
};
Traqball.prototype.disable = function() {
if (this.box !== null) {
bindEvent(this.config.activationArea, 'touchstart', this.evHandlers[0], "remove");
bindEvent(document, 'touchmove', this.evHandlers[1], "remove");
bindEvent(document, 'touchend', this.evHandlers[2], "remove");
}
}
Traqball.prototype.activate = function() {
if (this.box !== null) {
bindEvent(this.config.activationArea, 'touchstart', this.evHandlers[0]);
bindEvent(document, 'touchmove', this.evHandlers[1], "remove");
bindEvent(document, 'touchend', this.evHandlers[2], "remove");
}
}
Traqball.prototype.setup = function(conf) {
var THIS = this,
radius, // prepare a variable for storing the radius of our virtual trackball
stage, // the DOM-container of our "rotatable" element
axis = [], // The rotation-axis
mouseDownVect = [], // Vector on mousedown
mouseMoveVect = [], // Vector during mousemove
startMatrix = [], // Transformation-matrix at the moment of *starting* dragging
delta = 0,
impulse, pos, w, h, decr, angle, oldAngle, oldTime, curTime;
(function init() {
THIS.disable();
for (var prop in conf) {
THIS.config[prop] = conf[prop];
}
if (typeof THIS.config.stage === 'string') {
THIS.config.stage = document.getElementById(THIS.config.stage);
}
stage = THIS.config.stage;
pos = findPos(stage);
angle = THIS.config.angle || 0;
impulse = THIS.config.impulse === false ? false : true;
// Let's calculate some basic values from "stage" that are necessary for our virtual trackball
// 1st: determine the radius of our virtual trackball:
h = stage.offsetHeight / 2,
w = stage.offsetWidth / 2;
//take the shortest of both values as radius
radius = h < w ? h : w;
//We parse viewport. The first block-element we find will be our "victim" and made rotatable
for (var i = 0, l = stage.childNodes.length; i < l; i++) {
var child = stage.childNodes[i];
if (child.nodeType === 1) {
THIS.box = child;
break;
}
}
if (typeof THIS.config.activationArea === 'undefined') {
THIS.config.activationArea = THIS.box;
}
var perspective = getStyle(stage, prefix + "perspective"),
pOrigin = getStyle(stage, prefix + "perspective-origin"),
bTransform = getStyle(THIS.box, prefix + "transform");
//Let's define the start values. If "conf" contains angle or perspective or vector, use them.
//If not, look for css3d transforms within the CSS.
//If this fails, let's use some default values.
if (THIS.config.axis || THIS.config.angle) {
// Normalize the initAxis (initAxis = axis of rotation) because "box" will look distorted if normal is too long
axis = normalize(THIS.config.axis) || [1, 0, 0];
angle = THIS.config.angle || 0;
// Last but not least we calculate a matrix from the axis and the angle.
// This matrix will store the initial orientation in 3d-space
startMatrix = calcMatrix(axis, angle);
// if (isNaN(startMatrix[0]) || isNaN(startMatrix[1]) || isNaN(startMatrix[2]) || .....) {
// startMatrix = calcMatrix(axis, angle);
// }
} else if (bTransform !== "none") {
//already css3d transforms on element?
startMatrix = bTransform.split(",");
//Under certain circumstances some browsers report 2d Transforms.
//Translate them to 3d:
if (/matrix3d/gi.test(startMatrix[0])) {
startMatrix[0] = startMatrix[0].replace(/(matrix3d\()/g, "");
startMatrix[15] = startMatrix[15].replace(/\)/g, "");
} else {
startMatrix[0] = startMatrix[0].replace(/(matrix\()/g, "");
startMatrix[5] = startMatrix[5].replace(/\)/g, "");
startMatrix.splice(2, 0, 0, 0);
startMatrix.splice(6, 0, 0, 0);
startMatrix.splice(8, 0, 0, 0, 1, 0);
startMatrix.splice(14, 0, 0, 1);
}
for (var i = 0, l = startMatrix.length; i < l; i++) {
startMatrix[i] = parseFloat(startMatrix[i]);
}
} else {
axis = [0, 1, 0];
angle = 0;
startMatrix = calcMatrix(axis, angle);
}
if (THIS.config.perspective) {
stage.style[cssPref + "Perspective"] = THIS.config.perspective;
} else if (perspective === "none") {
stage.style[cssPref + "Perspective"] = "700px";
}
if (THIS.config.perspectiveOrigin) {
stage.style[cssPref + "PerspectiveOrigin"] = THIS.config.perspectiveOrigin;
}
// stage.classList.add("user-interacted");
THIS.box.style[cssPref + "Transform"] = "matrix3d(" + startMatrix + ")";
bindEvent(THIS.config.activationArea, 'touchstart', startrotation);
THIS.evHandlers = [startrotation, rotate, finishrotation];
})();
var isPinching = false;
var pinchTimer = null;
function pinching(e) {
// console.log(e);
// console.log(e.changedTouches.TouchList.length);
// console.log(e.targetTouches.TouchList.length);
// console.log(e.touches.TouchList.length);
// console.log(e.changedTouches.TouchList);
// console.log(e.targetTouches.TouchList);
// console.log(e.touches.TouchList);
// console.log(e.TouchEvent);
// console.log(e.changedTouches);
// console.log(e.changedTouches.length);
// console.log(e.targetTouches.length);
// console.log(e.touches.length);
// if ( e.changedTouches.length > 1 || e.targetTouches.length > 1 || e.touches.length > 1 ) alert("pinch");
// FIXME: THIS WAS THE LAST DEBUGGING BLOCK USED:
if ( ( e.changedTouches && e.changedTouches.length > 1 ) || ( e.targetTouches && e.targetTouches.length > 1 ) || ( e.touches && e.touches.length > 1 ) ) {
// document.body.style = "background:green;";
isPinching = true;
// THIS.disable();
clearTimeout(pinchTimer);
pinchTimer = setTimeout(function() {
// document.body.style = "";
isPinching = false;
// THIS.activate();
}, 1000);
return true;
}
// if ( e.changedTouches.length > 1 || e.targetTouches.length > 1 || e.touches.length > 1 ) return;
// if (e.targetTouches.TouchList.length == 2) alert("help");
// if (e.touches.length == 2) alert("help");
// if (e.changedTouches.length == 2) alert("help");
// THIS.activate();
return false;
}
function startrotation(e) {
if (delta !== 0) {
stopSlide();
};
// if (! pinching(e)) {
// e.preventDefault();
// if ( ! isPinching ) e.preventDefault();
// if ( ! pinching(e) ) e.preventDefault();
// else {
// bindEvent(document, 'touchmove', rotate, "remove");
// bindEvent(document, 'touchend', finishrotation, "remove");
// bindEvent(THIS.config.activationArea, 'touchstart', startrotation);
//
// stopSlide();
// }
// if ( pinching(e) ) finishrotation(e);
mouseDownVect = calcZvector(getCoords(e));
oldTime = curTime = new Date().getTime();
oldAngle = angle;
THIS.box.dispatchEvent(startEvent);
// THIS.box.classList.add("user-interacted");
// THIS.config.activationArea.classList.add("user-interacted");
// stage.classList.add("user-interacted");
if ( pinching(e) || isPinching ) {
// THIS.box.style[cssPref + "touch-action"] = "none";
THIS.box.style["touch-action"] = "";
// stage.style["touch-action"] = "";
finishrotation(e);
return true;
}
else {
e.preventDefault();
THIS.box.style["touch-action"] = "none";
// stage.style["touch-action"] = "none";
}
bindEvent(THIS.config.activationArea, 'touchstart', startrotation, "remove");
bindEvent(document, 'touchmove', rotate);
bindEvent(document, 'touchend', finishrotation);
// }
// } else e
}
function finishrotation(e) {
var stopMatrix;
bindEvent(document, 'touchmove', rotate, "remove");
bindEvent(document, 'touchend', finishrotation, "remove");
bindEvent(THIS.config.activationArea, 'touchstart', startrotation);
calcSpeed();
THIS.box.dispatchEvent(releaseEvent);
if (impulse && delta > 0) {
requestAnimFrame(slide);
} else if (!(isNaN(axis[0]) || isNaN(axis[1]) || isNaN(axis[2]))) {
stopSlide();
}
}
function cleanupMatrix() {
// Clean up when finishing rotation. Only thing to do: create a new "initial" matrix for the next rotation.
// If we don't, the object will flip back to the position at launch every time the user starts dragging.
// Therefore we must:
// 1. calculate a matrix from axis and the current angle
// 2. Create a new startmatrix by combining current startmatrix and stopmatrix to a new matrix.
// Matrices can be combined by multiplication, so what are we waiting for?
stopMatrix = calcMatrix(axis, angle);
startMatrix = multiplyMatrix(startMatrix, stopMatrix);
}
// The rotation:
function rotate(e) {
// if (!pinching(e)) {
// e.preventDefault();
// if ( pinching(e) ) e.preventDefault();
// if ( ! pinching(e) ) e.preventDefault();
// if ( pinching(e) ) finishrotation(e);
// startrotation(e)
// if ( ! pinching(e) ) e.preventDefault();
if ( pinching(e) ) {
// else {
// // bindEvent(document, 'touchmove', rotate, "remove");
// bindEvent(document, 'touchend', finishrotation, "remove");
// bindEvent(THIS.config.activationArea, 'touchstart', startrotation);
//
// stopSlide();
finishrotation(e);
// THIS.box.classList.remove("user-interacted");
// THIS.config.activationArea.classList.remove("user-interacted");
// stage.classList.remove("user-interacted");
THIS.box.style["touch-action"] = "";
// stage.style["touch-action"] = "";
return true;
}
else {
// e.stopPropagation();
// THIS.box.classList.add("user-interacted");
// THIS.config.activationArea.classList.add("user-interacted");
// stage.classList.add("user-interacted");
// e.preventDefault();
THIS.box.style["touch-action"] = "none";
// stage.style["touch-action"] = "none";
}
oldTime = curTime;
oldAngle = angle;
var eCoords = getCoords(e);
// Calculate the currrent z-component of the 3d-vector on the virtual trackball
mouseMoveVect = calcZvector(eCoords);
// We already calculated the z-vector-component on mousedown and the z-vector-component during mouse-movement.
// We will use them to retrieve the current rotation-axis
// (the normal-vector perpendiular to mouseDownVect and mouseMoveVect).
axis[0] = mouseDownVect[1] * mouseMoveVect[2] - mouseDownVect[2] * mouseMoveVect[1];
axis[1] = mouseDownVect[2] * mouseMoveVect[0] - mouseDownVect[0] * mouseMoveVect[2];
axis[2] = mouseDownVect[0] * mouseMoveVect[1] - mouseDownVect[1] * mouseMoveVect[0];
axis = normalize(axis);
// Now that we have the normal, we need the angle of the rotation.
// Easy to find by calculating the angle between mouseDownVect and mouseMoveVect:
angle = calcAngle(mouseDownVect, mouseMoveVect);
// if (isNaN(startMatrix[0]) || isNaN(startMatrix[1]) || isNaN(startMatrix[2])) {
// startMatrix = calcMatrix(axis, angle);
// }
//Only one thing left to do: Update the position of the box by applying a new transform:
// 2 transforms will be applied: the current rotation 3d and the start-matrix
THIS.box.style[cssPref + "Transform"] = "rotate3d(" + axis + "," + angle + "rad) matrix3d(" + startMatrix + ")";
rotateEvent.data = {
axis: axis,
angle: angle
};
THIS.box.dispatchEvent(rotateEvent);
curTime = new Date().getTime();
// }
// } else e
}
function calcSpeed() {
var dw = angle - oldAngle;
dt = curTime - oldTime;
delta = Math.abs(dw * 17 / dt);
if (isNaN(delta)) {
delta = 0;
} else if (delta > 0.2) {
delta = 0.2;
}
}
function slide() {
angle += delta;
decr = 0.01 * Math.sqrt(delta);
delta = delta > 0 ? delta - decr : 0;
THIS.box.style[cssPref + "Transform"] = "rotate3d(" + axis + "," + angle + "rad) matrix3d(" + startMatrix + ")";
rotateEvent.data = {
axis: axis,
angle: angle
};
THIS.box.dispatchEvent(rotateEvent);
if (delta === 0) {
stopSlide();
} else {
requestAnimFrame(slide);
}
}
function stopSlide() {
cancelAnimFrame(slide);
cleanupMatrix();
oldAngle = angle = 0;
delta = 0;
fullStopEvent.data = {
matrix: startMatrix
};
THIS.box.dispatchEvent(fullStopEvent);
}
//Some stupid matrix-multiplication.
function multiplyMatrix(m1, m2) {
var matrix = [];
matrix[0] = m1[0] * m2[0] + m1[1] * m2[4] + m1[2] * m2[8] + m1[3] * m2[12];
matrix[1] = m1[0] * m2[1] + m1[1] * m2[5] + m1[2] * m2[9] + m1[3] * m2[13];
matrix[2] = m1[0] * m2[2] + m1[1] * m2[6] + m1[2] * m2[10] + m1[3] * m2[14];
matrix[3] = m1[0] * m2[3] + m1[1] * m2[7] + m1[2] * m2[11] + m1[3] * m2[15];
matrix[4] = m1[4] * m2[0] + m1[5] * m2[4] + m1[6] * m2[8] + m1[7] * m2[12];
matrix[5] = m1[4] * m2[1] + m1[5] * m2[5] + m1[6] * m2[9] + m1[7] * m2[13];
matrix[6] = m1[4] * m2[2] + m1[5] * m2[6] + m1[6] * m2[10] + m1[7] * m2[14];
matrix[7] = m1[4] * m2[3] + m1[5] * m2[7] + m1[6] * m2[11] + m1[7] * m2[15];
matrix[8] = m1[8] * m2[0] + m1[9] * m2[4] + m1[10] * m2[8] + m1[11] * m2[12];
matrix[9] = m1[8] * m2[1] + m1[9] * m2[5] + m1[10] * m2[9] + m1[11] * m2[13];
matrix[10] = m1[8] * m2[2] + m1[9] * m2[6] + m1[10] * m2[10] + m1[11] * m2[14];
matrix[11] = m1[8] * m2[3] + m1[9] * m2[7] + m1[10] * m2[11] + m1[11] * m2[15];
matrix[12] = m1[12] * m2[0] + m1[13] * m2[4] + m1[14] * m2[8] + m1[15] * m2[12];
matrix[13] = m1[12] * m2[1] + m1[13] * m2[5] + m1[14] * m2[9] + m1[15] * m2[13];
matrix[14] = m1[12] * m2[2] + m1[13] * m2[6] + m1[14] * m2[10] + m1[15] * m2[14];
matrix[15] = m1[12] * m2[3] + m1[13] * m2[7] + m1[14] * m2[11] + m1[15] * m2[15];
return matrix;
}
// This function will calculate a z-component for our 3D-vector from the mouse x and y-coordinates
// (the corresponding point on our virtual trackball):
function calcZvector(coords) {
var x = THIS.config.limitAxxis === "x" ? radius : coords[0] - pos[0],
y = THIS.config.limitAxxis === "y" ? radius : coords[1] - pos[1],
vector = [(x / radius - 1), (y / radius - 1)],
z = 1 - vector[0] * vector[0] - vector[1] * vector[1];
// Make sure that dragging stops when z gets a negative value:
vector[2] = z > 0 ? Math.sqrt(z) : 0;
return vector;
}
// Normalization recalculates all coordinates in a way that the resulting vector has a length of "1".
// We achieve this by dividing the x, y and z-coordinates by the vector's length
function normalize(vect) {
var length = Math.sqrt(vect[0] * vect[0] + vect[1] * vect[1] + vect[2] * vect[2]);
vect[0] /= length;
vect[1] /= length;
vect[2] /= length;
return vect;
}
// Calculate the angle between 2 vectors.
function calcAngle(vect_1, vect_2) {
var numerator = vect_1[0] * vect_2[0] + vect_1[1] * vect_2[1] + vect_1[2] * vect_2[2],
denominator = Math.sqrt(vect_1[0] * vect_1[0] + vect_1[1] * vect_1[1] + vect_1[2] * vect_1[2]) *
Math.sqrt(vect_2[0] * vect_2[0] + vect_2[1] * vect_2[1] + vect_2[2] * vect_2[2]),
angle = Math.acos(numerator / denominator);
return angle;
}
function calcMatrix(vector, angle) {
// calculate transformation-matrix from a vector[x,y,z] and an angle
var x = vector[0],
y = vector[1],
z = vector[2],
sin = Math.sin(angle),
cos = Math.cos(angle),
cosmin = 1 - cos,
matrix = [(cos + x * x * cosmin), (y * x * cosmin + z * sin), (z * x * cosmin - y * sin), 0,
(x * y * cosmin - z * sin), (cos + y * y * cosmin), (z * y * cosmin + x * sin), 0,
(x * z * cosmin + y * sin), (y * z * cosmin - x * sin), (cos + z * z * cosmin), 0,
0, 0, 0, 1
];
return matrix;
}
//findPos-script by www.quirksmode.org
function findPos(obj) {
var curleft = 0,
curtop = 0;
if (obj.offsetParent) {
do {
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
} while (obj = obj.offsetParent);
return [curleft, curtop];
}
}
}
window.Traqball = Traqball;
})();
modernizr.custom.css3d.js
/* Modernizr 2.0.4 (Custom Build) | MIT & BSD
* Contains: csstransforms3d | iepp | teststyles | testprop | prefixes | load
*/
;
window.Modernizr = function ( a, b, c )
{
function z( a, b )
{
for ( var d in a )
if ( j[ a[ d ] ] !== c ) return b == "pfx" ? a[ d ] : !0;
return !1
}
function y( a, b )
{
return !!~( "" + a ).indexOf( b )
}
function x( a, b )
{
return typeof a === b
}
function w( a, b )
{
return v( m.join( a + ";" ) + ( b || "" ) )
}
function v( a )
{
j.cssText = a
}
var d = "2.0.4",
e = {},
f = b.documentElement,
g = b.head || b.getElementsByTagName( "head" )[ 0 ],
h = "modernizr",
i = b.createElement( h ),
j = i.style,
k, l = Object.prototype.toString,
m = " -webkit- -moz- -o- -ms- -khtml- ".split( " " ),
n = {},
o = {},
p = {},
q = [],
r = function ( a, c, d, e )
{
var g, i, j, k = b.createElement( "div" );
if ( parseInt( d, 10 ) )
while ( d-- ) j = b.createElement( "div" ), j.id = e ? e[ d ] : h + ( d + 1 ), k.appendChild( j );
g = [ "­", "<style>", a, "</style>" ].join( "" ), k.id = h, k.innerHTML += g, f.appendChild( k ), i = c( k, a ), k.parentNode.removeChild( k );
return !!i
},
s, t = {}.hasOwnProperty,
u;
!x( t, c ) && !x( t.call, c ) ? u = function ( a, b )
{
return t.call( a, b )
} : u = function ( a, b )
{
return b in a && x( a.constructor.prototype[ b ], c )
};
var A = function ( a, c )
{
var d = a.join( "" ),
f = c.length;
r( d, function ( a, c )
{
var d = b.styleSheets[ b.styleSheets.length - 1 ],
g = d.cssRules && d.cssRules[ 0 ] ? d.cssRules[ 0 ].cssText : d.cssText || "",
h = a.childNodes,
i = {};
while ( f-- ) i[ h[ f ].id ] = h[ f ];
e.csstransforms3d = i.csstransforms3d.offsetLeft === 9
}, f, c )
}( [ , [ "@media (", m.join( "transform-3d),(" ), h, ")", "{#csstransforms3d{left:9px;position:absolute}}" ].join( "" ) ], [ , "csstransforms3d" ] );
n.csstransforms3d = function ()
{
var a = !!z( [ "perspectiveProperty", "WebkitPerspective", "MozPerspective", "OPerspective", "msPerspective" ] );
a && "webkitPerspective" in f.style && ( a = e.csstransforms3d );
return a
};
for ( var B in n ) u( n, B ) && ( s = B.toLowerCase(), e[ s ] = n[ B ](), q.push( ( e[ s ] ? "" : "no-" ) + s ) );
v( "" ), i = k = null, a.attachEvent && function ()
{
var a = b.createElement( "div" );
a.innerHTML = "<elem></elem>";
return a.childNodes.length !== 1
}() && function ( a, b )
{
function s( a )
{
var b = -1;
while ( ++b < g ) a.createElement( f[ b ] )
}
a.iepp = a.iepp ||
{};
var d = a.iepp,
e = d.html5elements || "abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",
f = e.split( "|" ),
g = f.length,
h = new RegExp( "(^|\\s)(" + e + ")", "gi" ),
i = new RegExp( "<(/*)(" + e + ")", "gi" ),
j = /^\s*[\{\}]\s*$/,
k = new RegExp( "(^|[^\\n]*?\\s)(" + e + ")([^\\n]*)({[\\n\\w\\W]*?})", "gi" ),
l = b.createDocumentFragment(),
m = b.documentElement,
n = m.firstChild,
o = b.createElement( "body" ),
p = b.createElement( "style" ),
q = /print|all/,
r;
d.getCSS = function ( a, b )
{
if ( a + "" === c ) return "";
var e = -1,
f = a.length,
g, h = [];
while ( ++e < f )
{
g = a[ e ];
if ( g.disabled ) continue;
b = g.media || b, q.test( b ) && h.push( d.getCSS( g.imports, b ), g.cssText ), b = "all"
}
return h.join( "" )
}, d.parseCSS = function ( a )
{
var b = [],
c;
while ( ( c = k.exec( a ) ) != null ) b.push( ( ( j.exec( c[ 1 ] ) ? "\n" : c[ 1 ] ) + c[ 2 ] + c[ 3 ] ).replace( h, "$1.iepp_$2" ) + c[ 4 ] );
return b.join( "\n" )
}, d.writeHTML = function ()
{
var a = -1;
r = r || b.body;
while ( ++a < g )
{
var c = b.getElementsByTagName( f[ a ] ),
d = c.length,
e = -1;
while ( ++e < d ) c[ e ].className.indexOf( "iepp_" ) < 0 && ( c[ e ].className += " iepp_" + f[ a ] )
}
l.appendChild( r ), m.appendChild( o ), o.className = r.className, o.id = r.id, o.innerHTML = r.innerHTML.replace( i, "<$1font" )
}, d._beforePrint = function ()
{
p.styleSheet.cssText = d.parseCSS( d.getCSS( b.styleSheets, "all" ) ), d.writeHTML()
}, d.restoreHTML = function ()
{
o.innerHTML = "", m.removeChild( o ), m.appendChild( r )
}, d._afterPrint = function ()
{
d.restoreHTML(), p.styleSheet.cssText = ""
}, s( b ), s( l );
d.disablePP || ( n.insertBefore( p, n.firstChild ), p.media = "print", p.className = "iepp-printshim", a.attachEvent( "onbeforeprint", d._beforePrint ), a.attachEvent( "onafterprint", d._afterPrint ) )
}( a, b ), e._version = d, e._prefixes = m, e.testProp = function ( a )
{
return z( [ a ] )
}, e.testStyles = r;
return e
}( this, this.document ),
function ( a, b, c )
{
function k( a )
{
return !a || a == "loaded" || a == "complete"
}
function j()
{
var a = 1,
b = -1;
while ( p.length - ++b )
if ( p[ b ].s && !( a = p[ b ].r ) ) break;
a && g()
}
function i( a )
{
var c = b.createElement( "script" ),
d;
c.src = a.s, c.onreadystatechange = c.onload = function ()
{
!d && k( c.readyState ) && ( d = 1, j(), c.onload = c.onreadystatechange = null )
}, m( function ()
{
d || ( d = 1, j() )
}, H.errorTimeout ), a.e ? c.onload() : n.parentNode.insertBefore( c, n )
}
function h( a )
{
var c = b.createElement( "link" ),
d;
c.href = a.s, c.rel = "stylesheet", c.type = "text/css", !a.e && ( w || r ) ? function a( b )
{
m( function ()
{
if ( !d ) try
{
b.sheet.cssRules.length ? ( d = 1, j() ) : a( b )
}
catch ( c )
{
c.code == 1e3 || c.message == "security" || c.message == "denied" ? ( d = 1, m( function ()
{
j()
}, 0 ) ) : a( b )
}
}, 0 )
}( c ) : ( c.onload = function ()
{
d || ( d = 1, m( function ()
{
j()
}, 0 ) )
}, a.e && c.onload() ), m( function ()
{
d || ( d = 1, j() )
}, H.errorTimeout ), !a.e && n.parentNode.insertBefore( c, n )
}
function g()
{
var a = p.shift();
q = 1, a ? a.t ? m( function ()
{
a.t == "c" ? h( a ) : i( a )
}, 0 ) : ( a(), j() ) : q = 0
}
function f( a, c, d, e, f, h )
{
function i()
{
!o && k( l.readyState ) && ( r.r = o = 1, !q && j(), l.onload = l.onreadystatechange = null, m( function ()
{
u.removeChild( l )
}, 0 ) )
}
var l = b.createElement( a ),
o = 0,
r = {
t: d,
s: c,
e: h
};
l.src = l.data = c, !s && ( l.style.display = "none" ), l.width = l.height = "0", a != "object" && ( l.type = d ), l.onload = l.onreadystatechange = i, a == "img" ? l.onerror = i : a == "script" && ( l.onerror = function ()
{
r.e = r.r = 1, g()
} ), p.splice( e, 0, r ), u.insertBefore( l, s ? null : n ), m( function ()
{
o || ( u.removeChild( l ), r.r = r.e = o = 1, j() )
}, H.errorTimeout )
}
function e( a, b, c )
{
var d = b == "c" ? z : y;
q = 0, b = b || "j", C( a ) ? f( d, a, b, this.i++, l, c ) : ( p.splice( this.i++, 0, a ), p.length == 1 && g() );
return this
}
function d()
{
var a = H;
a.loader = {
load: e,
i: 0
};
return a
}
var l = b.documentElement,
m = a.setTimeout,
n = b.getElementsByTagName( "script" )[ 0 ],
o = {}.toString,
p = [],
q = 0,
r = "MozAppearance" in l.style,
s = r && !!b.createRange().compareNode,
t = r && !s,
u = s ? l : n.parentNode,
v = a.opera && o.call( a.opera ) == "[object Opera]",
w = "webkitAppearance" in l.style,
x = w && "async" in b.createElement( "script" ),
y = r ? "object" : v || x ? "img" : "script",
z = w ? "img" : y,
A = Array.isArray || function ( a )
{
return o.call( a ) == "[object Array]"
},
B = function ( a )
{
return typeof a == "object"
},
C = function ( a )
{
return typeof a == "string"
},
D = function ( a )
{
return o.call( a ) == "[object Function]"
},
E = [],
F = {},
G, H;
H = function ( a )
{
function f( a )
{
var b = a.split( "!" ),
c = E.length,
d = b.pop(),
e = b.length,
f = {
url: d,
origUrl: d,
prefixes: b
},
g, h;
for ( h = 0; h < e; h++ ) g = F[ b[ h ] ], g && ( f = g( f ) );
for ( h = 0; h < c; h++ ) f = E[ h ]( f );
return f
}
function e( a, b, e, g, h )
{
var i = f( a ),
j = i.autoCallback;
if ( !i.bypass )
{
b && ( b = D( b ) ? b : b[ a ] || b[ g ] || b[ a.split( "/" ).pop().split( "?" )[ 0 ] ] );
if ( i.instead ) return i.instead( a, b, e, g, h );
e.load( i.url, i.forceCSS || !i.forceJS && /css$/.test( i.url ) ? "c" : c, i.noexec ), ( D( b ) || D( j ) ) && e.load( function ()
{
d(), b && b( i.origUrl, h, g ), j && j( i.origUrl, h, g )
} )
}
}
function b( a, b )
{
function c( a )
{
if ( C( a ) ) e( a, h, b, 0, d );
else if ( B( a ) )
for ( i in a ) a.hasOwnProperty( i ) && e( a[ i ], h, b, i, d )
}
var d = !!a.test,
f = d ? a.yep : a.nope,
g = a.load || a.both,
h = a.callback,
i;
c( f ), c( g ), a.complete && b.load( a.complete )
}
var g, h, i = this.yepnope.loader;
if ( C( a ) ) e( a, 0, i, 0 );
else if ( A( a ) )
for ( g = 0; g < a.length; g++ ) h = a[ g ], C( h ) ? e( h, 0, i, 0 ) : A( h ) ? H( h ) : B( h ) && b( h, i );
else B( a ) && b( a, i )
}, H.addPrefix = function ( a, b )
{
F[ a ] = b
}, H.addFilter = function ( a )
{
E.push( a )
}, H.errorTimeout = 1e4, b.readyState == null && b.addEventListener && ( b.readyState = "loading", b.addEventListener( "DOMContentLoaded", G = function ()
{
b.removeEventListener( "DOMContentLoaded", G, 0 ), b.readyState = "complete"
}, 0 ) ), a.yepnope = d()
}( this, this.document ), Modernizr.load = function ()
{
yepnope.apply( window, [].slice.call( arguments, 0 ) )
};
In-Page JavaScript
Do not include or comment-out the activationElement: "stage-target"
option if you would rather end users have to interact with the boxart itself rather than the entire surrounding area, similar to my implementation in the above examples.
Modernizr.load({
test: Modernizr.csstransforms3d,
yep: {
'traqball': '/js/module/traqball.js'
},
nope: '/js/module/traqball_alternative.js',
callback: {
'traqball': function (url, result, key) {
var traqball_1 = new Traqball( {
// stage: "stage", // id of block element. String, default value:
// activationElement: "stage", // DOM-Element that catches the events. Default value: first child of stage (the rotating element)
// axis: [0.5,1,0.25], // X,Y,Z values of initial rotation vector. Array, default value: [1,0,0]
// angle: 0.12, // Initial rotation angle in radian. Float, default value: 0.
// perspective: perspectiveValue, // Perspective. Integer, default value 700.
// perspectiveOrigin: "xVal yVal", // Perspective Origin. String, default value "50% 50%".
// impulse: boolean, // Defines if object receives an impulse after relesing mouse/touchend. Default value: true.
// limitAxxis: "x" | "y" // limits the rotation to only one axxis.
stage: "boxart-stage",
activationElement: "stage-target",
axis: [0, 1, 0],
angle: 0.785398
} );
// var traqball_2 = new Traqball( { // follow this example to run 2 or more boxarts on one page at a time
// stage: "boxart-stage-2",
// activationElement: "stage-target-2",
// axis: [0, 1, 0],
// angle: 0.785398
// } );
}
}
});
Special Thanks
This page is comprised of my own additions and either partially or heavily modified elements from the following source(s):