Hey Patrick. I've been working on learning the API so I took a crack at this. My approach is similar to what Warlock was doing but I ended up with a lot more code :) Also I know JavaScript but my math isn't so great. I needed to go and review a bunch of trig and stuff to do this. At this point my solution works but feels a bit convoluted. If anyone has any tips for how I can simplify it I'm all ears! (function() {
// Start Config
var rotateOnMove = {
// The direction tokens face when initially added to map. Options are "up" or "down"
defaultOrientation: 'down',
// The animation frequency in ms. Increasing will make animation slower and may help performance.
animationSpeed: 50,
// The degrees to rotate per animation step. Decreasing will make animation slower and smoother but may hurt performance.
turnRate: 20
}, // End Config
_2PI = 2 * Math.PI;
rotateOnMove._rotatingTokens = [];
on("change:graphic", function(obj, prev) {
var line, oldAngle, newAngle, angleDelta, otherAngleDelta, direction;
// Sanity check inputs
try {
if (!validNumber(obj.get('left')) || !validNumber(obj.get('top')) ||
!validNumber(prev.left) || !validNumber(prev.top) ||
!validNumber(obj.get('rotation'))) {
log('Error: Bad environment state.');
return;
}
} catch(e) {
log('Error: ' + e.message);
return;
}
// Return if token isn't on the objects layer or its already rotating
if (_.contains(rotateOnMove._rotatingTokens, obj.id) ||
(obj.get('layer') !== 'objects')) {
return;
}
// Calculate the starting and ending angles
oldAngle = translateAngle(toRadians(obj.get('rotation')));
line = translateLine([
[ Math.round(prev.left), Math.round(prev.top) ],
[ Math.round(obj.get('left')), Math.round(obj.get('top')) ]
]);
if (_.isEqual(line[0], line[1])) return; // Points are the same so token didn't move
newAngle = getAngleOfLine(line);
// Calculate the difference of the angles along the closest rotation direction
angleDelta = getAngleDifference(Math.abs(newAngle), Math.abs(oldAngle));
otherAngleDelta = _2PI - angleDelta;
if (angleDelta < otherAngleDelta) {
direction = 1;
} else {
direction = -1;
angleDelta = otherAngleDelta;
}
(function doRotate(current, turnRate, progress) {
var intervalHandle = setInterval(rotate, rotateOnMove.animationSpeed);
function complete() {
clearInterval(intervalHandle);
rotateOnMove._rotatingTokens = _.without(rotateOnMove._rotatingTokens, obj.id);
}
function rotate() {
progress += turnRate;
current += turnRate;
if (Math.abs(progress) < Math.abs(angleDelta)) {
obj.set('rotation', toDegrees(translateAngle(current)));
} else {
obj.set('rotation', toDegrees(translateAngle(newAngle)));
complete();
}
}
})(oldAngle, toRadians(rotateOnMove.turnRate) * direction, 0);
// Track that this token's rotation is in progress
rotateOnMove._rotatingTokens.push(obj.id);
});
// Convert to trig quad space by flipping coords and reversing original Y
// to account for 0,0 being top left
function translateLine(line) {
return [
translatePoint(line[0]),
translatePoint(line[1])
];
}
// Note: Flip values to account for translation to trig quad.
// Reverse original Y to account for top left as 0, 0
function translatePoint(point) {
return [point[1] * - 1, point[0]];
}
function getAngleOfLine(line) {
var deltaX = line[1][0] - line[0][0],
deltaY = line[1][1] - line[0][1];
// Points are the same
if (deltaX === 0 && deltaY === 0) {
return null;
}
return (Math.atan2(deltaY, deltaX) + _2PI) % _2PI;
}
function getAngleDifference(angle1, angle2) {
var diff = angle1 - angle2;
return diff > 0 && diff < 2 * Math.PI ?
diff : (_2PI + diff) % _2PI; // Handle rollover
}
function validNumber(num) {
return _.isNumber(num) && !_.isNaN(num);
}
function toDegrees(angle) {
return Math.round(angle * (180 / Math.PI));
}
function toRadians(angle) {
return angle * (Math.PI / 180);
}
// Accomodate different default token orientations
function translateAngle(angle) {
return rotateOnMove.defaultOrientation === 'up' ?
angle : (angle + Math.PI) % _2PI;
}
})();