So here's first cut of what I've written. Still has lots of holes, haven't implemented flipV and flipH, but rotation and scaling basically work. Haven't put it up on GitHub yet but interested in anyone's feedback in the meantime.... // Github:
// By: Lucian Holland
// Contact:
var DynamicLightRecorder = DynamicLightRecorder || (function() {
'use strict';
var version = '0.1',
lastUpdate = 1455059736,
schemaVersion = 0.1,
checkInstall = function() {
log('-=> DynamicLightRecorder v'+version+' <=- ['+(new Date(lastUpdate*1000))+']');
if( ! _.has(state,'DynamicLightRecorder') || state.DynamicLightRecorder.version !== schemaVersion) {
log(' > Updating Schema to v'+schemaVersion+' <');
switch(state.DynamicLightRecorder && state.DynamicLightRecorder.version) {
default:
state.DynamicLightRecorder = {
version: schemaVersion,
tilePaths: {},
tileTemplates: {},
config: {
}
};
break;
}
}
},
handleInput = function(msg) {
if (msg.type !== "api" ) {
return;
}
var args = msg.content.split(/\s+--/);
switch(args.shift()) {
case '!dl-attach':
attach(msg.selected, !_.isEmpty(args) && args.shift() === 'overwrite');
break;
case '!dl-dump':
sendChat('DynamicLightRecorder', JSON.stringify(state.DynamicLightRecorder));
break;
case '!dl-wipe':
sendChat('DynamicLightRecorder', 'Wiping all data');
state.DynamicLightRecorder.tilePaths = {};
state.DynamicLightRecorder.tileTemplates = {};
break;
case '!dump-obj':
sendChat('', JSON.stringify(_.map(msg.selected, function(obj){ return getObj(obj._type, obj._id)})));
break;
default:
//Do nothing
}
},
attach = function(selection, overwrite) {
if (!selection || _.isEmpty(selection) || selection.length < 2) {
sendChat('DynamicLightRecorder', 'You must have one map tile and at least one path select to run attach');
return;
}
var objects = _.map(selection, function(object) {
return getObj(object._type, object._id);
});
var tiles = [];
var paths = [];
_.each(objects, function(object) {
if(object.get('_type') === 'path') {
paths.push(object);
}
else {
tiles.push(object);
}
});
if (_.isEmpty(tiles) || tiles.length !== 1) {
sendChat('DynamicLightRecorder', 'You must have exactly one map tile to run attach');
return;
}
var tile = tiles[0];
if (tile.get('_subtype') !== 'token' || !tile.get('imgsrc') || tile.get('imgsrc').indexOf('marketplace') === -1 || tile.get('layer') !== 'map') {
sendChat('DynamicLightRecorder', 'Selected tile must be from marketplace and must be on the map layer.');
return;
}
if (state.DynamicLightRecorder.tileTemplates[tile.get('imgsrc')] && !overwrite) {
sendChat('DynamicLightRecorder', 'Tile already has dynamic lighting paths recorded. Call with --overwrite to replace them');
return;
}
var template = {
top: tile.get('top'),
left: tile.get('left'),
width: tile.get('width'),
height: tile.get('height'),
flipv: tile.get('flipv'),
fliph: tile.get('fliph'),
rotation: tile.get('rotation'),
paths: _.map(paths, function(path) {
var savedPath = {
path: path.get('_path'),
offsetY: path.get('top') - tile.get('top'),
offsetX: path.get('left') - tile.get('left'),
width: path.get('width'),
height: path.get('height'),
layer: 'walls'
};
return savedPath;
})
};
state.DynamicLightRecorder.tileTemplates[tile.get('imgsrc')] = template;
},
handleTokenChange = function(token, previous) {
var paths = getPathsForToken(token);
var template = state.DynamicLightRecorder.tileTemplates[token.get('imgsrc')];
if (template) {
drawTokenPaths(token, paths, template);
}
},
getPathsForToken = function(token) {
var tile = state.DynamicLightRecorder.tilePaths[token.id];
if (!tile) {
return [];
}
var paths = _.map(tile.pathIds, function(pathId) {
var path = getObj('path', pathId);
if (!path) {
log('Warning, path with id [' + pathId + '] that should have been attached to token ' + JSON.stringify(token) + ' was not present.');
}
return path;
});
return paths;
},
SIZE_ATTRIBUTE_LOOKUP = {
'height': ['top', 'scaleY'],
'width': ['left', 'scaleX']
},
rotatePoint = function(point, centre, angle) {
angle = angle % 360;
var s = Math.sin(angle * Math.PI / 180.0);
var c = Math.cos(angle * Math.PI / 180.0);
// translate point back to origin:
var x = point[0] - centre[0];
var y = point[1] - centre[1];
// rotate point
var xnew = (x * c) - (y * s);
var ynew = (x * s) + (y * c);
// translate point back:
x = xnew + centre[0];
y = ynew + centre[1];
return [x,y];
},
scalePaths = function(token, template, paths) {
_.each(_.keys(SIZE_ATTRIBUTE_LOOKUP), function(attribute) {
var scaleFactor = token.get(attribute) / template[attribute];
_.chain(paths).compact().each(function(path) {
path.set(SIZE_ATTRIBUTE_LOOKUP[attribute][1], scaleFactor);
var positionAttribute = SIZE_ATTRIBUTE_LOOKUP[attribute][0];
var existingOffset = path.get(positionAttribute) - token.get(positionAttribute);
var scaledOffset = existingOffset * scaleFactor;
path.set(positionAttribute, token.get(positionAttribute) + scaledOffset);
});
});
},
buildRotatedPaths = function(token, template) {
var angle = token.get('rotation');
angle -= template.rotation;
var tokenCentre = [token.get('left'), token.get('top')];
return _.map(template.paths, function(path) {
var existingCentre = [token.get('left') + path.offsetX, token.get('top') + path.offsetY];
var newCentreOfPath = rotatePoint(existingCentre, tokenCentre, angle);
var points = JSON.parse(path.path);
log(points);
var bottomRight = _.reduce(points, function(result, point) {
result[0] = Math.max(point[1], result[0]);
result[1] = Math.max(point[2], result[1]);
return result;
}, [0,0]);
var pointsCentre = [bottomRight[0]/2, bottomRight[1]/2];
var transformedOrigin = rotatePoint([0,0], pointsCentre, angle);
var transformedBottomRight = rotatePoint(bottomRight, pointsCentre, angle);
var xMin = Math.min(transformedOrigin[0], transformedBottomRight[0]);
var yMin = Math.min(transformedOrigin[1], transformedBottomRight[1]);
var xMax = Math.max(transformedOrigin[0], transformedBottomRight[0]);
var yMax = Math.max(transformedOrigin[1], transformedBottomRight[1]);
var xAdjustment = Math.min(0, xMin);
var yAdjustment = Math.min(0, yMin);
var newHeight = yMax - yAdjustment;
var newWidth = xMax - xAdjustment;
var newPoints = _.map(points, function(point) {
var result = rotatePoint(_.rest(point, 1), pointsCentre, angle);
//Adjust to reset the top-left of the final shape to 0,0
result[0] -= xAdjustment;
result[1] -= yAdjustment;
result.unshift(point[0]);
return result;
});
var newPath = {
layer: 'walls',
path: JSON.stringify(newPoints),
left: newCentreOfPath[0],
top: newCentreOfPath[1],
pageid: token.get('_pageid'),
width: newWidth,
height: newHeight
};
return createObj('path', newPath);
});
},
drawTokenPaths = function(token, existingPaths, template) {
_.invoke(_.compact(existingPaths), 'remove');
var paths;
if(token.get('rotation') !== template.rotation) {
paths = buildRotatedPaths(token, template);
}
else {
paths = _.map(template.paths, function(templatePath) {
var attributes = _.clone(templatePath);
attributes.pageid = token.get('_pageid');
attributes.left = token.get('left') + attributes.offsetX;
attributes.top = token.get('top') + attributes.offsetY;
return createObj('path', attributes);
});
}
scalePaths(token, template, paths);
state.DynamicLightRecorder.tilePaths[token.id].pathIds = _.pluck(paths, 'id');
},
handleNewToken = function(token) {
var template = state.DynamicLightRecorder.tileTemplates[token.get('imgsrc')];
if (!template) {
return;
}
var tileObject = {
tileId: token.id
};
state.DynamicLightRecorder.tilePaths[token.id] = tileObject;
drawTokenPaths(token, [], template);
},
handleDeleteToken = function(token) {
log('deleting paths for token ' + token.id);
var paths = getPathsForToken(token);
_.invoke(_.compact(paths), 'remove');
delete state.DynamicLightRecorder.tilePaths[token.id];
},
registerEventHandlers = function() {
on('chat:message', handleInput);
on('change:token', handleTokenChange);
on('add:token', handleNewToken);
on('destroy:graphic', handleDeleteToken);
};
return {
RegisterEventHandlers: registerEventHandlers,
CheckInstall: checkInstall
};
}());
on("ready",function(){
'use strict';
DynamicLightRecorder.CheckInstall();
DynamicLightRecorder.RegisterEventHandlers();
});