Add relative move and rotate to rel plugin (#794)
The relative position in rel plugin is currently based on the world coordinate. So for the same effect, like fly in from the right-hand side, we must use different `data-rel-x/y/z` value. Why not let the plugin do the hard part? So I introduce a `data-rel-position`, when set to `relative`, all relative attribute is based on the position and rotation of previous slide. So no matter the rotation of previous slide, data-rel-x="1000" always looks like fly in from the right-hand side. We can change the position and rotation of one slide, and the position of all following slides will be changed too. When `data-rel-position` is set to `relative`, relative rotation has a clear meaning. It describes the relative rotations between slides. We don't need to set rotations for all slide, setting the key slides is enough. If `data-rel-position` is not relative, the effect of `data-rel-rotate-x/y/z` is not clear, so they're only used when `data-rel-position="relative"`. After the introduction of relative rotation, there're 6 attribute that will inherit from previous slide. If we want to set a relative X move, we have to set all other 5 attributes to 0. It's boring. So a `data-rel-clear` is used to set all 6 attributes to 0, and then the value specified in current slide is applied. The `examples/3D-positions/index.html` shows some usage. As you can see, the html code of two slide ring is the same, and slides except for the first two in a ring has no position attributes. It work by inheriting the previous one. This PR invokes a lot math calculations. Basically, the rotation of a slide is translated into the coordinate describing the directions of X/Y/Z axes. And `data-rel-x/y/z` can be easily calculated by that. The rotations is the hard part, I mainly use the algorithm in the Quaternions and spatial rotation - Wikipedia to compose two and more rotations. I'm not a math guy, hope I don't make much mistakes.
This commit is contained in:
429
src/lib/rotation.js
Normal file
429
src/lib/rotation.js
Normal file
@@ -0,0 +1,429 @@
|
||||
/**
|
||||
* Helper functions for rotation.
|
||||
*
|
||||
* Tommy Tam (c) 2021
|
||||
* MIT License
|
||||
*/
|
||||
( function( document, window ) {
|
||||
"use strict";
|
||||
|
||||
// Singleton library variables
|
||||
var roots = [];
|
||||
|
||||
var libraryFactory = function( rootId ) {
|
||||
if ( roots[ "impress-root-" + rootId ] ) {
|
||||
return roots[ "impress-root-" + rootId ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Round the number to 2 decimals, it's enough for use
|
||||
*/
|
||||
var roundNumber = function( num ) {
|
||||
return Math.round( ( num + Number.EPSILON ) * 100 ) / 100;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the length/norm of a vector.
|
||||
*
|
||||
* https://en.wikipedia.org/wiki/Norm_(mathematics)
|
||||
*/
|
||||
var vectorLength = function( vec ) {
|
||||
return Math.sqrt( vec.x * vec.x + vec.y * vec.y + vec.z * vec.z );
|
||||
};
|
||||
|
||||
/**
|
||||
* Dot product of two vectors.
|
||||
*
|
||||
* https://en.wikipedia.org/wiki/Dot_product
|
||||
*/
|
||||
var vectorDotProd = function( vec1, vec2 ) {
|
||||
return vec1.x * vec2.x + vec1.y * vec2.y + vec1.z * vec2.z;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cross product of two vectors.
|
||||
*
|
||||
* https://en.wikipedia.org/wiki/Cross_product
|
||||
*/
|
||||
var vectorCrossProd = function( vec1, vec2 ) {
|
||||
return {
|
||||
x: vec1.y * vec2.z - vec1.z * vec2.y,
|
||||
y: vec1.z * vec2.x - vec1.x * vec2.z,
|
||||
z: vec1.x * vec2.y - vec1.y * vec2.x
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine wheter a vector is a zero vector
|
||||
*/
|
||||
var isZeroVector = function( vec ) {
|
||||
return !roundNumber( vec.x ) && !roundNumber( vec.y ) && !roundNumber( vec.z );
|
||||
};
|
||||
|
||||
/**
|
||||
* Scalar triple product of three vectors.
|
||||
*
|
||||
* It can be used to determine the handness of vectors.
|
||||
*
|
||||
* https://en.wikipedia.org/wiki/Triple_product#Scalar_triple_product
|
||||
*/
|
||||
var tripleProduct = function( vec1, vec2, vec3 ) {
|
||||
return vectorDotProd( vectorCrossProd( vec1, vec2 ), vec3 );
|
||||
};
|
||||
|
||||
/**
|
||||
* The world/absolute unit coordinates.
|
||||
*
|
||||
* This coordinate is used by browser to position objects.
|
||||
* It will not be affected by object rotations.
|
||||
* All relative positions will finally be converted to this
|
||||
* coordinate to be used.
|
||||
*/
|
||||
var worldUnitCoordinate = {
|
||||
x: { x:1, y:0, z:0 },
|
||||
y: { x:0, y:1, z:0 },
|
||||
z: { x:0, y:0, z:1 }
|
||||
};
|
||||
|
||||
/**
|
||||
* Make quaternion from rotation axis and angle.
|
||||
*
|
||||
* q = [ cos(½θ), sin(½θ) axis ]
|
||||
*
|
||||
* If the angle is zero, returns the corresponded quaternion
|
||||
* of axis.
|
||||
*
|
||||
* If the angle is not zero, returns the rotating quaternion
|
||||
* which corresponds to rotation about the axis, by the angle θ.
|
||||
*
|
||||
* https://en.wikipedia.org/wiki/Quaternion
|
||||
* https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation
|
||||
*/
|
||||
var makeQuaternion = function( axis, theta = 0 ) {
|
||||
var r = 0;
|
||||
var t = 1;
|
||||
|
||||
if ( theta ) {
|
||||
var radians = theta * Math.PI / 180;
|
||||
r = Math.cos( radians / 2 );
|
||||
t = Math.sin( radians / 2 ) / vectorLength( axis );
|
||||
}
|
||||
|
||||
var q = [ r, axis.x * t, axis.y * t, axis.z * t ];
|
||||
|
||||
return q;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract vector from quaternion
|
||||
*/
|
||||
var quaternionToVector = function( quaternion ) {
|
||||
return {
|
||||
x: roundNumber( quaternion[ 1 ] ),
|
||||
y: roundNumber( quaternion[ 2 ] ),
|
||||
z: roundNumber( quaternion[ 3 ] )
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the conjugate quaternion of a quaternion
|
||||
*
|
||||
* https://en.wikipedia.org/wiki/Quaternion#Conjugation,_the_norm,_and_reciprocal
|
||||
*/
|
||||
var conjugateQuaternion = function( quaternion ) {
|
||||
return [ quaternion[ 0 ], -quaternion[ 1 ], -quaternion[ 2 ], -quaternion[ 3 ] ];
|
||||
};
|
||||
|
||||
/**
|
||||
* Left multiple two quaternion.
|
||||
*
|
||||
* Is's used to combine two rotating quaternion into one.
|
||||
*/
|
||||
var leftMulQuaternion = function( q1, q2 ) {
|
||||
return [
|
||||
( q1[ 0 ] * q2[ 0 ] - q1[ 1 ] * q2[ 1 ] - q1[ 2 ] * q2[ 2 ] - q1[ 3 ] * q2[ 3 ] ),
|
||||
( q1[ 1 ] * q2[ 0 ] + q1[ 0 ] * q2[ 1 ] - q1[ 3 ] * q2[ 2 ] + q1[ 2 ] * q2[ 3 ] ),
|
||||
( q1[ 2 ] * q2[ 0 ] + q1[ 3 ] * q2[ 1 ] + q1[ 0 ] * q2[ 2 ] - q1[ 1 ] * q2[ 3 ] ),
|
||||
( q1[ 3 ] * q2[ 0 ] - q1[ 2 ] * q2[ 1 ] + q1[ 1 ] * q2[ 2 ] + q1[ 0 ] * q2[ 3 ] )
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a rotation into a quaternion
|
||||
*/
|
||||
var rotationToQuaternion = function( baseCoordinate, rotation ) {
|
||||
var order = rotation.order ? rotation.order : "xyz";
|
||||
var axes = order.split( "" );
|
||||
var result = [ 1, 0, 0, 0 ];
|
||||
|
||||
for ( var i = 0; i < axes.length; i++ ) {
|
||||
var deg = rotation[ axes[ i ] ];
|
||||
if ( !deg || ( Math.abs( deg ) < 0.0001 ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// All CSS rotation is based on the rotated coordinate
|
||||
// So we need to calculate the rotated coordinate first
|
||||
var coordinate = baseCoordinate;
|
||||
if ( i > 0 ) {
|
||||
coordinate = {
|
||||
x: rotateByQuaternion( baseCoordinate.x, result ),
|
||||
y: rotateByQuaternion( baseCoordinate.y, result ),
|
||||
z: rotateByQuaternion( baseCoordinate.z, result )
|
||||
};
|
||||
}
|
||||
|
||||
result = leftMulQuaternion(
|
||||
makeQuaternion( coordinate[ axes[ i ] ], deg ),
|
||||
result );
|
||||
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Rotate a vector by a quaternion.
|
||||
*/
|
||||
var rotateByQuaternion = function( vec, quaternion ) {
|
||||
var q = makeQuaternion( vec );
|
||||
|
||||
q = leftMulQuaternion(
|
||||
leftMulQuaternion( quaternion, q ),
|
||||
conjugateQuaternion( quaternion ) );
|
||||
|
||||
return quaternionToVector( q );
|
||||
};
|
||||
|
||||
/**
|
||||
* Rotate a vector by rotaion sequence.
|
||||
*/
|
||||
var rotateVector = function( baseCoordinate, vec, rotation ) {
|
||||
var quaternion = rotationToQuaternion( baseCoordinate, rotation );
|
||||
|
||||
return rotateByQuaternion( vec, quaternion );
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a rotation, return the rotationed coordinate
|
||||
*/
|
||||
var rotateCoordinate = function( coordinate, rotation ) {
|
||||
var quaternion = rotationToQuaternion( coordinate, rotation );
|
||||
|
||||
return {
|
||||
x: rotateByQuaternion( coordinate.x, quaternion ),
|
||||
y: rotateByQuaternion( coordinate.y, quaternion ),
|
||||
z: rotateByQuaternion( coordinate.z, quaternion )
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the angle between two vector.
|
||||
*
|
||||
* The axis is used to determine the rotation direction.
|
||||
*/
|
||||
var angleBetweenTwoVector = function( axis, vec1, vec2 ) {
|
||||
var vecLen1 = vectorLength( vec1 );
|
||||
var vecLen2 = vectorLength( vec2 );
|
||||
|
||||
if ( !vecLen1 || !vecLen2 ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var cos = vectorDotProd( vec1, vec2 ) / vecLen1 / vecLen2 ;
|
||||
var angle = Math.acos( cos ) * 180 / Math.PI;
|
||||
|
||||
if ( tripleProduct( vec1, vec2, axis ) > 0 ) {
|
||||
return angle;
|
||||
} else {
|
||||
return -angle;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the angle between a vector and a plane.
|
||||
*
|
||||
* The plane is determined by an axis and a vector on the plane.
|
||||
*/
|
||||
var angleBetweenPlaneAndVector = function( axis, planeVec, rotatedVec ) {
|
||||
var norm = vectorCrossProd( axis, planeVec );
|
||||
|
||||
if ( isZeroVector( norm ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 90 - angleBetweenTwoVector( axis, rotatedVec, norm );
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculated a order specified rotation sequence to
|
||||
* transform from the world coordinate to required coordinate.
|
||||
*/
|
||||
var coordinateToOrderedRotation = function( coordinate, order ) {
|
||||
var axis0 = order[ 0 ];
|
||||
var axis1 = order[ 1 ];
|
||||
var axis2 = order[ 2 ];
|
||||
var reversedOrder = order.split( "" ).reverse().join( "" );
|
||||
|
||||
var rotate2 = angleBetweenPlaneAndVector(
|
||||
coordinate[ axis2 ],
|
||||
worldUnitCoordinate[ axis0 ],
|
||||
coordinate[ axis0 ] );
|
||||
|
||||
// The r2 is the reverse of rotate for axis2
|
||||
// The coordinate1 is the coordinate before rotate of axis2
|
||||
var r2 = { order: reversedOrder };
|
||||
r2[ axis2 ] = -rotate2;
|
||||
|
||||
var coordinate1 = rotateCoordinate( coordinate, r2 );
|
||||
|
||||
// Calculate the rotation for axis1
|
||||
var rotate1 = angleBetweenTwoVector(
|
||||
coordinate1[ axis1 ],
|
||||
worldUnitCoordinate[ axis0 ],
|
||||
coordinate1[ axis0 ] );
|
||||
|
||||
// Calculate the rotation for axis0
|
||||
var rotate0 = angleBetweenTwoVector(
|
||||
worldUnitCoordinate[ axis0 ],
|
||||
worldUnitCoordinate[ axis1 ],
|
||||
coordinate1[ axis1 ] );
|
||||
|
||||
var rotation = { };
|
||||
rotation.order = order;
|
||||
rotation[ axis0 ] = roundNumber( rotate0 );
|
||||
rotation[ axis1 ] = roundNumber( rotate1 );
|
||||
rotation[ axis2 ] = roundNumber( rotate2 );
|
||||
|
||||
return rotation;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the possible rotations from unit coordinate
|
||||
* to specified coordinate.
|
||||
*/
|
||||
var possibleRotations = function( coordinate ) {
|
||||
var orders = [ "xyz", "xzy", "yxz", "yzx", "zxy", "zyx" ];
|
||||
var rotations = [ ];
|
||||
|
||||
for ( var i = 0; i < orders.length; ++i ) {
|
||||
rotations.push(
|
||||
coordinateToOrderedRotation( coordinate, orders[ i ] )
|
||||
);
|
||||
}
|
||||
|
||||
return rotations;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate a degree which in range (-180, 180] of baseDeg
|
||||
*/
|
||||
var nearestAngle = function( baseDeg, deg ) {
|
||||
while ( deg > baseDeg + 180 ) {
|
||||
deg -= 360;
|
||||
}
|
||||
|
||||
while ( deg < baseDeg - 180 ) {
|
||||
deg += 360;
|
||||
}
|
||||
|
||||
return deg;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a base rotation and multiple rotations, return the best one.
|
||||
*
|
||||
* The best one is the one has least rotate from base.
|
||||
*/
|
||||
var bestRotation = function( baseRotate, rotations ) {
|
||||
var bestScore;
|
||||
var bestRotation;
|
||||
|
||||
for ( var i = 0; i < rotations.length; ++i ) {
|
||||
var rotation = {
|
||||
order: rotations[ i ].order,
|
||||
x: nearestAngle( baseRotate.x, rotations[ i ].x ),
|
||||
y: nearestAngle( baseRotate.y, rotations[ i ].y ),
|
||||
z: nearestAngle( baseRotate.z, rotations[ i ].z )
|
||||
};
|
||||
|
||||
var score = Math.abs( rotation.x - baseRotate.x ) +
|
||||
Math.abs( rotation.y - baseRotate.y ) +
|
||||
Math.abs( rotation.z - baseRotate.z );
|
||||
|
||||
if ( !i || ( score < bestScore ) ) {
|
||||
bestScore = score;
|
||||
bestRotation = rotation;
|
||||
}
|
||||
}
|
||||
|
||||
return bestRotation;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a coordinate, return the best rotation to achieve it.
|
||||
*
|
||||
* The baseRotate is used to select the near rotation from it.
|
||||
*/
|
||||
var coordinateToRotation = function( baseRotate, coordinate ) {
|
||||
var rotations = possibleRotations( coordinate );
|
||||
|
||||
return bestRotation( baseRotate, rotations );
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply a relative rotation to the base rotation.
|
||||
*
|
||||
* Calculate the coordinate after the rotation on each axis,
|
||||
* and finally find out a one step rotation has the effect
|
||||
* of two rotation.
|
||||
*
|
||||
* If there're multiple way to accomplish, select the one
|
||||
* that is nearest to the base.
|
||||
*
|
||||
* Return one rotation has the same effect.
|
||||
*/
|
||||
var combineRotations = function( rotations ) {
|
||||
|
||||
// No rotation
|
||||
if ( rotations.length <= 0 ) {
|
||||
return { x:0, y:0, z:0, order:"xyz" };
|
||||
}
|
||||
|
||||
// Find out the base coordinate
|
||||
var coordinate = worldUnitCoordinate;
|
||||
|
||||
// One by one apply rotations in order
|
||||
for ( var i = 0; i < rotations.length; i++ ) {
|
||||
coordinate = rotateCoordinate( coordinate, rotations[ i ] );
|
||||
}
|
||||
|
||||
// Calculate one rotation from unit coordinate to rotated
|
||||
// coordinate. Because there're multiple possibles,
|
||||
// select the one nearest to the base
|
||||
var rotate = coordinateToRotation( rotations[ 0 ], coordinate );
|
||||
|
||||
return rotate;
|
||||
};
|
||||
|
||||
var translateRelative = function( relative, prevRotation ) {
|
||||
var result = rotateVector(
|
||||
worldUnitCoordinate, relative, prevRotation );
|
||||
result.rotate = combineRotations(
|
||||
[ prevRotation, relative.rotate ] );
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
var lib = {
|
||||
translateRelative: translateRelative
|
||||
};
|
||||
|
||||
roots[ "impress-root-" + rootId ] = lib;
|
||||
return lib;
|
||||
};
|
||||
|
||||
// Let impress core know about the existence of this library
|
||||
window.impress.addLibraryFactory( { rotation: libraryFactory } );
|
||||
|
||||
} )( document, window );
|
||||
Reference in New Issue
Block a user