- Add horizontal resize handles for browser and properties panels with localStorage persistence - Support hide/show toggles for each side panel - Fix audio layers not being interactive on canvas (hasControls, hasBorders, evented flags) - Convert hardcoded panel widths to CSS variables for responsive layout - Support narrower windows by shrinking panels while preserving minimum canvas width - Ensure old projects with audio layers don't show phantom control boxes at 0,0 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
5843 lines
164 KiB
JavaScript
5843 lines
164 KiB
JavaScript
// Resize the canvas
|
|
function resizeCanvas() {
|
|
canvas.discardActiveObject();
|
|
canvas.setHeight($('#canvas-area').height());
|
|
canvas.setWidth($('#canvas-area').width());
|
|
artboard.set({
|
|
left: canvas.get('width') / 2 - artboard.get('width') / 2,
|
|
top: canvas.get('height') / 2 - artboard.get('height') / 2,
|
|
});
|
|
canvas.renderAll();
|
|
animate(false, currenttime);
|
|
initLines();
|
|
}
|
|
window.addEventListener('resize', resizeCanvas, false);
|
|
resizeCanvas();
|
|
|
|
// Highlight layers when selecting objects in the canvas, scroll into view if needed
|
|
function updateSelection(e) {
|
|
if (e.target.type == 'activeSelection') {
|
|
$('.layer-selected').removeClass('layer-selected');
|
|
canvas.getActiveObjects().forEach(function (object) {
|
|
if (
|
|
$('.layer').length > 0 &&
|
|
$(".layer[data-object='" + object.get('id') + "']").length > 0
|
|
) {
|
|
$(".layer[data-object='" + object.get('id') + "']").addClass(
|
|
'layer-selected'
|
|
);
|
|
if (e.e != undefined && $('.layer-selected').length > 0) {
|
|
document
|
|
.getElementsByClassName('layer-selected')[0]
|
|
.scrollIntoView();
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
if (
|
|
$('.layer').length > 0 &&
|
|
$(".layer[data-object='" + e.target.get('id') + "']").length > 0
|
|
) {
|
|
$('.layer-selected').removeClass('layer-selected');
|
|
$(".layer[data-object='" + e.target.get('id') + "']").addClass(
|
|
'layer-selected'
|
|
);
|
|
if (e.e != undefined && $('.layer-selected').length > 0) {
|
|
document
|
|
.getElementsByClassName('layer-selected')[0]
|
|
.scrollIntoView();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Object has been modified, automatically add a keyframe
|
|
function autoKeyframe(object, e, multi) {
|
|
if (e.action == 'drag') {
|
|
newKeyframe(
|
|
'left',
|
|
object,
|
|
currenttime,
|
|
object.get('left'),
|
|
true
|
|
);
|
|
newKeyframe('top', object, currenttime, object.get('top'), true);
|
|
} else if (e.action == 'scale') {
|
|
newKeyframe(
|
|
'scaleX',
|
|
object,
|
|
currenttime,
|
|
object.get('scaleX'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'scaleY',
|
|
object,
|
|
currenttime,
|
|
object.get('scaleY'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'left',
|
|
object,
|
|
currenttime,
|
|
object.get('left'),
|
|
true
|
|
);
|
|
newKeyframe('top', object, currenttime, object.get('top'), true);
|
|
newKeyframe(
|
|
'width',
|
|
object,
|
|
currenttime,
|
|
object.get('width'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'height',
|
|
object,
|
|
currenttime,
|
|
object.get('height'),
|
|
true
|
|
);
|
|
} else if (e.action == 'rotate') {
|
|
newKeyframe(
|
|
'angle',
|
|
object,
|
|
currenttime,
|
|
object.get('angle'),
|
|
true
|
|
);
|
|
if (multi) {
|
|
newKeyframe(
|
|
'scaleX',
|
|
object,
|
|
currenttime,
|
|
object.get('scaleX'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'scaleY',
|
|
object,
|
|
currenttime,
|
|
object.get('scaleY'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'width',
|
|
object,
|
|
currenttime,
|
|
object.get('width'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'left',
|
|
object,
|
|
currenttime,
|
|
object.get('left'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'top',
|
|
object,
|
|
currenttime,
|
|
object.get('top'),
|
|
true
|
|
);
|
|
}
|
|
} else if (
|
|
e.action == 'resizing' ||
|
|
e.action == 'scaleX' ||
|
|
e.action == 'scaleY'
|
|
) {
|
|
newKeyframe(
|
|
'scaleX',
|
|
object,
|
|
currenttime,
|
|
object.get('scaleX'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'scaleY',
|
|
object,
|
|
currenttime,
|
|
object.get('scaleY'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'left',
|
|
object,
|
|
currenttime,
|
|
object.get('left'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'width',
|
|
object,
|
|
currenttime,
|
|
object.get('width'),
|
|
true
|
|
);
|
|
newKeyframe('top', object, currenttime, object.get('top'), true);
|
|
newKeyframe(
|
|
'height',
|
|
object,
|
|
currenttime,
|
|
object.get('height'),
|
|
true
|
|
);
|
|
}
|
|
}
|
|
|
|
// Reselect
|
|
function reselect(selection) {
|
|
tempselection = false;
|
|
if (selection.type == 'activeSelection') {
|
|
var objs = [];
|
|
for (let so of selection._objects) {
|
|
for (let obj of canvas.getObjects()) {
|
|
if (obj.get('id') === so.get('id')) {
|
|
objs.push(obj);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
canvas.setActiveObject(
|
|
new fabric.ActiveSelection(objs, {
|
|
canvas: canvas,
|
|
})
|
|
);
|
|
canvas.renderAll();
|
|
} else {
|
|
if (selection.get('type') == 'group') {
|
|
canvas.setActiveObject(canvas.getItemById(selection.get('id')));
|
|
} else {
|
|
canvas.setActiveObject(selection);
|
|
}
|
|
canvas.renderAll();
|
|
}
|
|
}
|
|
|
|
// Group objects
|
|
function group() {
|
|
var objects = canvas.getActiveObjects();
|
|
var object_ids = [];
|
|
var newgroup = new fabric.Group();
|
|
objects.forEach(function (object) {
|
|
newgroup.addWithUpdate(object);
|
|
object.set({ inGroup: true });
|
|
$(".layer[data-object='" + object.get('id') + "']").remove();
|
|
object_ids.push(object.get('id'));
|
|
$('#' + object.get('id')).remove();
|
|
canvas.remove(object);
|
|
});
|
|
//var newgroup = canvas.getActiveObject().toGroup();
|
|
newgroup.set({
|
|
id: 'Group' + layer_count,
|
|
objectCaching: false,
|
|
isGroup: true,
|
|
color: '#F1890E',
|
|
type: 'group',
|
|
stroke: '#000',
|
|
strokeUniform: true,
|
|
strokeWidth: 0,
|
|
paintFirst: 'stroke',
|
|
absolutePositioned: true,
|
|
inGroup: false,
|
|
strokeDashArray: false,
|
|
shadow: {
|
|
color: 'black',
|
|
offsetX: 0,
|
|
offsetY: 0,
|
|
blur: 0,
|
|
opacity: 0,
|
|
},
|
|
});
|
|
groups.push({ id: newgroup.get('id'), objects: object_ids });
|
|
canvas.renderAll();
|
|
newLayer(newgroup);
|
|
canvas.setActiveObject(newgroup);
|
|
sortKeyframes();
|
|
save();
|
|
}
|
|
$(document).on('click', '#group-objects', group);
|
|
|
|
// Ungroup SVG
|
|
function unGroup(group) {
|
|
canvas.discardActiveObject();
|
|
canvas.renderAll();
|
|
tempgroup = group._objects;
|
|
group._restoreObjectsState();
|
|
$(".layer[data-object='" + group.get('id') + "']").remove();
|
|
$('#' + group.get('id')).remove();
|
|
canvas.remove(group);
|
|
keyframes = $.grep(keyframes, function (e) {
|
|
return e.id != group.get('id');
|
|
});
|
|
p_keyframes = $.grep(p_keyframes, function (e) {
|
|
return e.id != group.get('id');
|
|
});
|
|
objects = $.grep(objects, function (e) {
|
|
return e.id != group.get('id');
|
|
});
|
|
canvas.renderAll();
|
|
for (var i = 0; i < tempgroup.length; i++) {
|
|
if (tempgroup[i].inGroup) {
|
|
canvas.add(tempgroup[i]);
|
|
renderLayer(tempgroup[i]);
|
|
props.forEach(function (prop) {
|
|
if (
|
|
prop != 'top' &&
|
|
prop != 'scaleY' &&
|
|
prop != 'width' &&
|
|
prop != 'height' &&
|
|
prop != 'shadow.offsetX' &&
|
|
prop != 'shadow.offsetY' &&
|
|
prop != 'shadow.opacity' &&
|
|
prop != 'shadow.blur' &&
|
|
prop != 'lineHeight'
|
|
) {
|
|
renderProp(prop, tempgroup[i]);
|
|
}
|
|
});
|
|
const keyarr = $.grep(keyframes, function (e) {
|
|
return e.id == tempgroup[i].id;
|
|
});
|
|
keyarr.forEach(function (keyframe) {
|
|
if (
|
|
keyframe.name != 'top' &&
|
|
keyframe.name != 'scaleY' &&
|
|
keyframe.name != 'width' &&
|
|
keyframe.name != 'height' &&
|
|
keyframe.name != 'shadow.offsetX' &&
|
|
keyframe.name != 'shadow.offsetY' &&
|
|
keyframe.name != 'shadow.opacity' &&
|
|
keyframe.name != 'shadow.blur' &&
|
|
keyframe.name != 'lineHeight'
|
|
) {
|
|
renderKeyframe(
|
|
canvas.getItemById(keyframe.id),
|
|
keyframe.name,
|
|
keyframe.t
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
canvas.renderAll();
|
|
save();
|
|
}
|
|
$(document).on('click', '#ungroup-objects', function () {
|
|
if (canvas.getActiveObject()) {
|
|
unGroup(canvas.getActiveObject());
|
|
}
|
|
});
|
|
|
|
// Regroup SVG
|
|
function reGroup(id) {
|
|
var group = [];
|
|
var objects = [];
|
|
const entry = groups.find((x) => x.id == id);
|
|
if (!entry) {
|
|
return;
|
|
}
|
|
entry.objects.forEach(function (object) {
|
|
const item = canvas.getItemById(object);
|
|
if (item) {
|
|
objects.push(item);
|
|
}
|
|
});
|
|
if (objects.length == 0) {
|
|
return;
|
|
}
|
|
var activeselection = new fabric.ActiveSelection(objects);
|
|
var newgroup = activeselection.toGroup();
|
|
newgroup.set({
|
|
id: id,
|
|
objectCaching: false,
|
|
});
|
|
canvas.add(newgroup);
|
|
canvas.renderAll();
|
|
}
|
|
|
|
// Keep record canvas up to date
|
|
async function updateRecordCanvas() {
|
|
canvasrecord.setWidth(artboard.width);
|
|
canvasrecord.setHeight(artboard.height);
|
|
canvasrecord.width = artboard.width;
|
|
canvasrecord.height = artboard.height;
|
|
canvas.clipPath = null;
|
|
// Sequential, not forEach(async): the snapshot below must not be taken
|
|
// while filters are still being stripped off the objects.
|
|
for (const object of objects) {
|
|
var obj = canvas.getItemById(object.id);
|
|
if (!obj) {
|
|
object.filters = [];
|
|
continue;
|
|
}
|
|
if (obj.filters) {
|
|
if (obj.filters.length > 0) {
|
|
object.filters = [];
|
|
obj.filters.forEach(function (filter) {
|
|
if (
|
|
filter.type == 'BlackWhite' ||
|
|
filter.type == 'Invert' ||
|
|
filter.type == 'Sepia' ||
|
|
filter.type == 'Kodachrome' ||
|
|
filter.type == 'Polaroid' ||
|
|
filter.type == 'Technicolor' ||
|
|
filter.type == 'Brownie' ||
|
|
filter.type == 'Vintage'
|
|
) {
|
|
object.filters.push({ type: filter.type });
|
|
} else if (filter.type == 'Brightness') {
|
|
object.filters.push({
|
|
type: filter.type,
|
|
value: filter.brightness,
|
|
});
|
|
} else if (filter.type == 'Contrast') {
|
|
object.filters.push({
|
|
type: filter.type,
|
|
value: filter.contrast,
|
|
});
|
|
} else if (filter.type == 'Vibrance') {
|
|
object.filters.push({
|
|
type: filter.type,
|
|
value: filter.vibrance,
|
|
});
|
|
} else if (filter.type == 'Saturation') {
|
|
object.filters.push({
|
|
type: filter.type,
|
|
value: filter.saturation,
|
|
});
|
|
} else if (filter.type == 'HueRotation') {
|
|
object.filters.push({
|
|
type: filter.type,
|
|
value: filter.rotation,
|
|
});
|
|
} else if (filter.type == 'Blur') {
|
|
object.filters.push({
|
|
type: filter.type,
|
|
value: filter.blur,
|
|
});
|
|
} else if (filter.type == 'Noise') {
|
|
object.filters.push({
|
|
type: filter.type,
|
|
value: filter.noise,
|
|
});
|
|
} else if (filter.type == 'RemoveColor') {
|
|
object.filters.push({
|
|
type: filter.type,
|
|
distance: filter.distance,
|
|
color: filter.color,
|
|
});
|
|
}
|
|
});
|
|
obj.filters = [];
|
|
obj.applyFilters();
|
|
var backend = fabric.filterBackend;
|
|
if (backend && backend.evictCachesForKey) {
|
|
backend.evictCachesForKey(obj.cacheKey);
|
|
backend.evictCachesForKey(obj.cacheKey + '_filtered');
|
|
}
|
|
if (
|
|
obj.filters.length > 0 &&
|
|
obj.get('id').indexOf('Video') >= 0
|
|
) {
|
|
await obj.setElement(obj.saveElem);
|
|
}
|
|
} else {
|
|
object.filters = [];
|
|
}
|
|
} else {
|
|
object.filters = [];
|
|
}
|
|
}
|
|
const canvassave = canvas.toJSON([
|
|
'volume',
|
|
'audioSrc',
|
|
'defaultLeft',
|
|
'defaultTop',
|
|
'defaultScaleX',
|
|
'defaultScaleY',
|
|
'notnew',
|
|
'starttime',
|
|
'top',
|
|
'left',
|
|
'width',
|
|
'height',
|
|
'scaleX',
|
|
'scaleY',
|
|
'flipX',
|
|
'flipY',
|
|
'originX',
|
|
'originY',
|
|
'transformMatrix',
|
|
'stroke',
|
|
'strokeWidth',
|
|
'strokeDashArray',
|
|
'strokeLineCap',
|
|
'strokeDashOffset',
|
|
'strokeLineJoin',
|
|
'strokeMiterLimit',
|
|
'angle',
|
|
'opacity',
|
|
'fill',
|
|
'globalCompositeOperation',
|
|
'shadow',
|
|
'clipTo',
|
|
'visible',
|
|
'backgroundColor',
|
|
'skewX',
|
|
'skewY',
|
|
'fillRule',
|
|
'paintFirst',
|
|
'clipPath',
|
|
'strokeUniform',
|
|
'rx',
|
|
'ry',
|
|
'cornerRadius',
|
|
'selectable',
|
|
'hasControls',
|
|
'subTargetCheck',
|
|
'id',
|
|
'hoverCursor',
|
|
'defaultCursor',
|
|
'isEditing',
|
|
'source',
|
|
'assetType',
|
|
'duration',
|
|
'inGroup',
|
|
]);
|
|
canvas.clipPath = artboard;
|
|
// loadFromJSON is asynchronous (it clears the canvas first, then revives the
|
|
// objects), so callers that record straight afterwards must await this.
|
|
return new Promise(function (resolve) {
|
|
canvasrecord.loadFromJSON(canvassave, function () {
|
|
if (canvasrecord.getItemById('center_h')) {
|
|
canvasrecord.remove(canvasrecord.getItemById('center_h'));
|
|
canvasrecord.remove(canvasrecord.getItemById('center_v'));
|
|
}
|
|
if (canvasrecord.getItemById('line_h')) {
|
|
canvasrecord.remove(canvasrecord.getItemById('line_h'));
|
|
canvasrecord.remove(canvasrecord.getItemById('line_v'));
|
|
}
|
|
canvasrecord.renderAll();
|
|
canvasrecord.setWidth(artboard.width);
|
|
canvasrecord.setHeight(artboard.height);
|
|
canvasrecord.width = artboard.width;
|
|
canvasrecord.height = artboard.height;
|
|
canvasrecord.renderAll();
|
|
objects.forEach(function (object) {
|
|
replaceSource(
|
|
canvasrecord.getItemById(object.id),
|
|
canvasrecord
|
|
);
|
|
replaceSource(canvas.getItemById(object.id), canvas);
|
|
});
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
// Frame rate the user picked in the download modal, used by every export path
|
|
const EXPORT_FPS_DEFAULT = 30;
|
|
|
|
function getExportFramerate() {
|
|
const fps = parseInt($('#framerate').val(), 10);
|
|
if (!(fps > 0)) {
|
|
return EXPORT_FPS_DEFAULT;
|
|
}
|
|
return Math.min(120, fps);
|
|
}
|
|
|
|
// A still image has no frame rate
|
|
$('input[name=radio]').on('change', function () {
|
|
$('#framerate-row').toggle(
|
|
$('input[name=radio]:checked').val() != 'image'
|
|
);
|
|
});
|
|
|
|
// Put the editor back into a usable state once a render has finished (or failed)
|
|
function resetRecordingUI() {
|
|
recording = false;
|
|
currenttime = 0;
|
|
animate(false, 0);
|
|
$('#seekbar').offset({
|
|
left:
|
|
offset_left +
|
|
$('#inner-timeline').offset().left +
|
|
currenttime / timelinetime,
|
|
});
|
|
canvas.renderAll();
|
|
resizeCanvas();
|
|
if (background_audio != false) {
|
|
background_audio.pause();
|
|
background_audio = new Audio(background_audio.src);
|
|
}
|
|
$('#download-real').html('Download');
|
|
$('#download-real').removeClass('downloading');
|
|
updateRecordCanvas();
|
|
}
|
|
|
|
// Hand a finished WebM recording to the user, converting it first if the
|
|
// chosen format is not webm.
|
|
function deliverRecording(blob) {
|
|
$('#download-real').html('Downloading...');
|
|
const format = $('input[name=radio]:checked').val();
|
|
if (format == 'mp4' || format == 'gif') {
|
|
convertStreams(blob, format);
|
|
return;
|
|
}
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.style.display = 'none';
|
|
a.href = url;
|
|
a.download = 'video.webm';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
// Give the browser a tick to start the download before releasing the blob
|
|
window.setTimeout(function () {
|
|
URL.revokeObjectURL(url);
|
|
}, 60000);
|
|
resetRecordingUI();
|
|
}
|
|
|
|
// Download recording
|
|
function downloadRecording(chunks) {
|
|
$('#download-real').html('Downloading...');
|
|
const format = $('input[name=radio]:checked').val();
|
|
if (format == 'webm') {
|
|
var url = URL.createObjectURL(
|
|
new Blob(chunks, {
|
|
type: 'video/webm',
|
|
})
|
|
);
|
|
const a = document.createElement('a');
|
|
a.style.display = 'none';
|
|
a.href = url;
|
|
a.download = 'video.webm';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
// Give the browser a tick to start the download before releasing the blob
|
|
window.setTimeout(function () {
|
|
URL.revokeObjectURL(url);
|
|
}, 60000);
|
|
resetRecordingUI();
|
|
} else if (format == 'mp4' || format == 'gif') {
|
|
// Both are transcoded from the captured webm by the ffmpeg worker
|
|
convertStreams(
|
|
new Blob(chunks, { type: 'video/webm' }),
|
|
format
|
|
);
|
|
} else {
|
|
console.warn('Unknown download format: ' + format);
|
|
resetRecordingUI();
|
|
}
|
|
}
|
|
|
|
$('#download-real').on('click', record);
|
|
|
|
// Save everything in the canvas (for undo/redo/autosave)
|
|
function save() {
|
|
redo = [];
|
|
redoarr = [];
|
|
if (state) {
|
|
undo.push(state);
|
|
undoarr.push(statearr);
|
|
}
|
|
canvas.clipPath = null;
|
|
state = canvas.toJSON([
|
|
'volume',
|
|
'audioSrc',
|
|
'top',
|
|
'left',
|
|
'width',
|
|
'height',
|
|
'scaleX',
|
|
'scaleY',
|
|
'flipX',
|
|
'flipY',
|
|
'originX',
|
|
'originY',
|
|
'transformMatrix',
|
|
'stroke',
|
|
'strokeWidth',
|
|
'strokeDashArray',
|
|
'strokeLineCap',
|
|
'strokeDashOffset',
|
|
'strokeLineJoin',
|
|
'strokeMiterLimit',
|
|
'angle',
|
|
'opacity',
|
|
'fill',
|
|
'globalCompositeOperation',
|
|
'shadow',
|
|
'clipTo',
|
|
'visible',
|
|
'backgroundColor',
|
|
'skewX',
|
|
'skewY',
|
|
'fillRule',
|
|
'paintFirst',
|
|
'clipPath',
|
|
'strokeUniform',
|
|
'rx',
|
|
'ry',
|
|
'cornerRadius',
|
|
'selectable',
|
|
'hasControls',
|
|
'subTargetCheck',
|
|
'id',
|
|
'hoverCursor',
|
|
'defaultCursor',
|
|
'isEditing',
|
|
'source',
|
|
'assetType',
|
|
'duration',
|
|
'inGroup',
|
|
'filters',
|
|
]);
|
|
canvas.clipPath = artboard;
|
|
statearr = {
|
|
keyframes: JSON.parse(JSON.stringify(keyframes)),
|
|
p_keyframes: JSON.parse(JSON.stringify(p_keyframes)),
|
|
objects: JSON.parse(JSON.stringify(objects)),
|
|
colormode: colormode,
|
|
duration: duration,
|
|
currenttime: currenttime,
|
|
};
|
|
if (undo.length >= 1) {
|
|
$('#undo').addClass('history-active');
|
|
} else {
|
|
$('#undo').removeClass('history-active');
|
|
}
|
|
if (redo.length >= 1) {
|
|
$('#redo').addClass('history-active');
|
|
} else {
|
|
$('#redo').removeClass('history-active');
|
|
}
|
|
|
|
schedulePersist();
|
|
}
|
|
|
|
// Rebuilding the record canvas and writing to IndexedDB are both expensive
|
|
// and used to run on every single edit (including every keystroke). Coalesce
|
|
// them; record() awaits updateRecordCanvas() directly when it needs it fresh.
|
|
var persistTimer;
|
|
function schedulePersist() {
|
|
window.clearTimeout(persistTimer);
|
|
persistTimer = window.setTimeout(function () {
|
|
updateRecordCanvas();
|
|
autoSave();
|
|
}, 400);
|
|
}
|
|
|
|
// Duplicate object
|
|
function copyObject() {
|
|
if (clipboard) {
|
|
if (cliptype == 'object') {
|
|
if (clipboard.type == 'activeSelection') {
|
|
clipboard._objects.forEach(function (clone) {
|
|
clone.clone(function (cloned) {
|
|
// layer_count only advances inside newLayer, so read it after
|
|
// the clone resolves or every copy shares one id.
|
|
cloned.set({
|
|
id: 'Shape' + layer_count,
|
|
});
|
|
canvas.add(cloned);
|
|
canvas.renderAll();
|
|
newLayer(cloned);
|
|
canvas.setActiveObject(cloned);
|
|
});
|
|
});
|
|
} else {
|
|
clipboard.clone(function (cloned) {
|
|
cloned.set({
|
|
id: 'Shape' + layer_count,
|
|
});
|
|
canvas.add(cloned);
|
|
canvas.renderAll();
|
|
newLayer(cloned);
|
|
canvas.setActiveObject(cloned);
|
|
});
|
|
}
|
|
save();
|
|
} else {
|
|
copyKeyframes();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Replace the source of an object when reloading the canvas (since Fabric needs a DOM reference for the objects)
|
|
function replaceSource(object, canvas) {
|
|
if (object == null) {
|
|
return false;
|
|
}
|
|
if (object.get('type') != 'group') {
|
|
if (object.type) {
|
|
if (object.type == 'image') {
|
|
if (object.get('id').indexOf('Video') >= 0) {
|
|
var vidObj = document.createElement('video');
|
|
var vidSrc = document.createElement('source');
|
|
vidSrc.src = object.get('source');
|
|
vidObj.crossOrigin = 'anonymous';
|
|
vidObj.appendChild(vidSrc);
|
|
vidObj.addEventListener('loadeddata', function () {
|
|
vidObj.width = this.videoWidth;
|
|
vidObj.height = this.videoHeight;
|
|
vidObj.currentTime = 0;
|
|
vidObj.muted = false;
|
|
async function waitLoad() {
|
|
if (vidObj.readyState >= 3) {
|
|
object.setElement(vidObj);
|
|
object.saveElem = vidObj;
|
|
await canvas.renderAll();
|
|
await animate(false, currenttime);
|
|
if (
|
|
objects.find((x) => x.id == object.get('id'))
|
|
.filters
|
|
) {
|
|
if (
|
|
objects.find((x) => x.id == object.get('id'))
|
|
.filters.length > 0
|
|
) {
|
|
objects
|
|
.find((x) => x.id == object.get('id'))
|
|
.filters.forEach(function (filter) {
|
|
if (filter.type == 'Sepia') {
|
|
object.filters.push(new f.Sepia());
|
|
} else if (filter.type == 'Invert') {
|
|
object.filters.push(new f.Invert());
|
|
} else if (filter.type == 'BlackWhite') {
|
|
object.filters.push(new f.BlackWhite());
|
|
} else if (filter.type == 'Kodachrome') {
|
|
object.filters.push(new f.Kodachrome());
|
|
} else if (filter.type == 'Polaroid') {
|
|
object.filters.push(new f.Polaroid());
|
|
} else if (filter.type == 'Technicolor') {
|
|
object.filters.push(new f.Technicolor());
|
|
} else if (filter.type == 'Vintage') {
|
|
object.filters.push(new f.Vintage());
|
|
} else if (filter.type == 'Brownie') {
|
|
object.filters.push(new f.Brownie());
|
|
} else if (filter.type == 'Brightness') {
|
|
object.filters.push(
|
|
new f.Brightness({
|
|
brightness: filter.value,
|
|
})
|
|
);
|
|
} else if (filter.type == 'Contrast') {
|
|
object.filters.push(
|
|
new f.Contrast({ contrast: filter.value })
|
|
);
|
|
} else if (filter.type == 'Saturation') {
|
|
object.filters.push(
|
|
new f.Saturation({
|
|
saturation: filter.value,
|
|
})
|
|
);
|
|
} else if (filter.type == 'Vibrance') {
|
|
object.filters.push(
|
|
new f.Vibrance({ vibrance: filter.value })
|
|
);
|
|
} else if (filter.type == 'HueRotation') {
|
|
object.filters.push(
|
|
new f.HueRotation({
|
|
rotation: filter.value,
|
|
})
|
|
);
|
|
} else if (filter.type == 'Noise') {
|
|
object.filters.push(
|
|
new f.Noise({ noise: filter.value })
|
|
);
|
|
} else if (filter.type == 'Blur') {
|
|
object.filters.push(
|
|
new f.Blur({ blur: filter.value })
|
|
);
|
|
} else if (filter.type == 'RemoveColor') {
|
|
object.filters.push(
|
|
new f.RemoveColor({
|
|
distance: filter.distance,
|
|
color: filter.color,
|
|
})
|
|
);
|
|
}
|
|
});
|
|
object.applyFilters();
|
|
canvas.renderAll();
|
|
}
|
|
}
|
|
} else {
|
|
window.setTimeout(function () {
|
|
waitLoad();
|
|
}, 100);
|
|
}
|
|
}
|
|
window.setTimeout(function () {
|
|
waitLoad();
|
|
}, 100);
|
|
});
|
|
vidObj.currentTime = 0;
|
|
} else {
|
|
//var img = new Image();
|
|
//img.onload = function(){
|
|
// object.setElement(img);
|
|
// canvas.renderAll();
|
|
if (objects.find((x) => x.id == object.get('id')).filters) {
|
|
if (
|
|
objects.find((x) => x.id == object.get('id')).filters
|
|
.length > 0
|
|
) {
|
|
objects
|
|
.find((x) => x.id == object.get('id'))
|
|
.filters.forEach(function (filter) {
|
|
if (filter.type == 'Sepia') {
|
|
object.filters.push(new f.Sepia());
|
|
} else if (filter.type == 'Invert') {
|
|
object.filters.push(new f.Invert());
|
|
} else if (filter.type == 'BlackWhite') {
|
|
object.filters.push(new f.BlackWhite());
|
|
} else if (filter.type == 'Kodachrome') {
|
|
object.filters.push(new f.Kodachrome());
|
|
} else if (filter.type == 'Polaroid') {
|
|
object.filters.push(new f.Polaroid());
|
|
} else if (filter.type == 'Technicolor') {
|
|
object.filters.push(new f.Technicolor());
|
|
} else if (filter.type == 'Vintage') {
|
|
object.filters.push(new f.Vintage());
|
|
} else if (filter.type == 'Brownie') {
|
|
object.filters.push(new f.Brownie());
|
|
} else if (filter.type == 'Brightness') {
|
|
object.filters.push(
|
|
new f.Brightness({ brightness: filter.value })
|
|
);
|
|
} else if (filter.type == 'Contrast') {
|
|
object.filters.push(
|
|
new f.Contrast({ contrast: filter.value })
|
|
);
|
|
} else if (filter.type == 'Saturation') {
|
|
object.filters.push(
|
|
new f.Saturation({ saturation: filter.value })
|
|
);
|
|
} else if (filter.type == 'Vibrance') {
|
|
object.filters.push(
|
|
new f.Vibrance({ vibrance: filter.value })
|
|
);
|
|
} else if (filter.type == 'HueRotation') {
|
|
object.filters.push(
|
|
new f.HueRotation({ rotation: filter.value })
|
|
);
|
|
} else if (filter.type == 'Noise') {
|
|
object.filters.push(
|
|
new f.Noise({ noise: filter.value })
|
|
);
|
|
} else if (filter.type == 'Blur') {
|
|
object.filters.push(
|
|
new f.Blur({ blur: filter.value })
|
|
);
|
|
} else if (filter.type == 'RemoveColor') {
|
|
object.filters.push(
|
|
new f.RemoveColor({
|
|
distance: filter.distance,
|
|
color: filter.color,
|
|
})
|
|
);
|
|
}
|
|
});
|
|
object.applyFilters();
|
|
canvas.renderAll();
|
|
} else {
|
|
object.filters = [];
|
|
object.applyFilters();
|
|
canvas.renderAll();
|
|
}
|
|
}
|
|
|
|
//}
|
|
//img.src = window.URL.createObjectURL(new Blob(files.find(x => x.name == object.get("id")).file, {type:"image/png"}));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Perform undo/redo
|
|
function undoRedo(newState, saveState, newArrState, saveArrState) {
|
|
saveState.push(state);
|
|
saveArrState.push(statearr);
|
|
statearr = newArrState.pop();
|
|
state = newState.pop();
|
|
keyframes = statearr.keyframes;
|
|
p_keyframes = statearr.p_keyframes;
|
|
objects = statearr.objects;
|
|
colormode = statearr.colormode;
|
|
duration = statearr.duration;
|
|
currenttime = statearr.currenttime;
|
|
canvas.clipPath = null;
|
|
canvas.loadFromJSON(state, function () {
|
|
canvas.clipPath = artboard;
|
|
hideGuides(canvas);
|
|
canvas.renderAll();
|
|
$('.object-props').remove();
|
|
$('.layer').remove();
|
|
objects.forEach(function (object) {
|
|
if (!canvas.getItemById(object.id)) {
|
|
return;
|
|
}
|
|
replaceSource(canvas.getItemById(object.id), canvas);
|
|
renderLayer(canvas.getItemById(object.id));
|
|
props.forEach(function (prop) {
|
|
if (
|
|
prop != 'top' &&
|
|
prop != 'scaleY' &&
|
|
prop != 'width' &&
|
|
prop != 'height' &&
|
|
prop != 'shadow.offsetX' &&
|
|
prop != 'shadow.offsetY' &&
|
|
prop != 'shadow.opacity' &&
|
|
prop != 'shadow.blur' &&
|
|
prop != 'lineHeight'
|
|
) {
|
|
renderProp(prop, canvas.getItemById(object.id));
|
|
}
|
|
});
|
|
});
|
|
keyframes.forEach(function (keyframe) {
|
|
if (!canvas.getItemById(keyframe.id)) {
|
|
return;
|
|
}
|
|
if (
|
|
keyframe.name != 'top' &&
|
|
keyframe.name != 'scaleY' &&
|
|
keyframe.name != 'width' &&
|
|
keyframe.name != 'height' &&
|
|
keyframe.name != 'shadow.offsetX' &&
|
|
keyframe.name != 'shadow.offsetY' &&
|
|
keyframe.name != 'shadow.opacity' &&
|
|
keyframe.name != 'shadow.blur' &&
|
|
keyframe.name != 'lineHeight'
|
|
) {
|
|
renderKeyframe(
|
|
canvas.getItemById(keyframe.id),
|
|
keyframe.name,
|
|
keyframe.t
|
|
);
|
|
}
|
|
});
|
|
animate(false, currenttime);
|
|
});
|
|
if (undo.length >= 1) {
|
|
$('#undo').addClass('history-active');
|
|
} else {
|
|
$('#undo').removeClass('history-active');
|
|
}
|
|
if (redo.length >= 1) {
|
|
$('#redo').addClass('history-active');
|
|
} else {
|
|
$('#redo').removeClass('history-active');
|
|
}
|
|
}
|
|
|
|
// Undo/redo buttons
|
|
$(document).on('click', '#undo', function () {
|
|
if (undo.length >= 1) {
|
|
undoRedo(undo, redo, undoarr, redoarr);
|
|
}
|
|
});
|
|
$(document).on('click', '#redo', function () {
|
|
if (redo.length >= 1) {
|
|
undoRedo(redo, undo, redoarr, undoarr);
|
|
}
|
|
});
|
|
|
|
// Corner radius, in the canvas pixels the panel advertises.
|
|
//
|
|
// fabric applies rx/ry in the object's own coordinate space, before its scale,
|
|
// and shapes are resized by scaling rather than by changing width/height. The
|
|
// raw input was written straight to rx, so a radius typed as 20 drew at 60 on a
|
|
// rect scaled 3x: the value behaved like a proportion of the shape instead of
|
|
// pixels. `cornerRadius` stores what the user asked for and rx/ry are derived
|
|
// from it, so the same number means the same number of pixels at any size.
|
|
function getCornerRadius(object) {
|
|
if (!object) {
|
|
return 0;
|
|
}
|
|
if (typeof object.cornerRadius == 'number') {
|
|
return object.cornerRadius;
|
|
}
|
|
// Projects saved before cornerRadius existed only carry the raw rx
|
|
return (
|
|
(object.get('rx') || 0) * Math.abs(object.get('scaleX') || 1)
|
|
);
|
|
}
|
|
|
|
function setCornerRadius(object, radius) {
|
|
if (!object) {
|
|
return;
|
|
}
|
|
var px = radius > 0 ? radius : 0;
|
|
object.cornerRadius = px;
|
|
object.set({
|
|
rx: px / Math.abs(object.get('scaleX') || 1),
|
|
ry: px / Math.abs(object.get('scaleY') || 1),
|
|
});
|
|
}
|
|
|
|
// Re-derive rx/ry after a resize so the drawn radius keeps the typed value
|
|
function syncCornerRadius(object) {
|
|
if (!object || object.get('type') != 'rect') {
|
|
return;
|
|
}
|
|
if (typeof object.cornerRadius != 'number') {
|
|
return;
|
|
}
|
|
setCornerRadius(object, object.cornerRadius);
|
|
}
|
|
|
|
// Generate keyframes
|
|
function keyframeChanges(object, type, id, selection) {
|
|
if (object.get('type') == 'rect') {
|
|
setCornerRadius(
|
|
object,
|
|
parseFloat($('#object-corners input').val())
|
|
);
|
|
} else if (object.get('type') == 'textbox') {
|
|
object.set({
|
|
charSpacing: parseFloat($('#text-h input').val()) * 10,
|
|
lineHeight: parseFloat($('#text-v input').val() / 100),
|
|
});
|
|
canvas.renderAll();
|
|
}
|
|
canvas.renderAll();
|
|
if (type && type == 'opacity') {
|
|
newKeyframe(
|
|
'opacity',
|
|
object,
|
|
currenttime,
|
|
object.get('opacity'),
|
|
true
|
|
);
|
|
} else if (type && type == 'opacity3') {
|
|
newKeyframe(
|
|
'charSpacing',
|
|
object,
|
|
currenttime,
|
|
object.get('charSpacing'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'lineHeight',
|
|
object,
|
|
currenttime,
|
|
object.get('lineHeight'),
|
|
true
|
|
);
|
|
} else {
|
|
if (id == 'object-x' || id == 'object-y') {
|
|
newKeyframe(
|
|
'left',
|
|
object,
|
|
currenttime,
|
|
object.get('left'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'top',
|
|
object,
|
|
currenttime,
|
|
object.get('top'),
|
|
true
|
|
);
|
|
} else if (id == 'object-w' || id == 'object-h') {
|
|
newKeyframe(
|
|
'scaleX',
|
|
object,
|
|
currenttime,
|
|
object.get('scaleX'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'scaleY',
|
|
object,
|
|
currenttime,
|
|
object.get('scaleY'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'width',
|
|
object,
|
|
currenttime,
|
|
object.get('width'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'height',
|
|
object,
|
|
currenttime,
|
|
object.get('height'),
|
|
true
|
|
);
|
|
if (selection) {
|
|
newKeyframe(
|
|
'left',
|
|
object,
|
|
currenttime,
|
|
object.get('left'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'top',
|
|
object,
|
|
currenttime,
|
|
object.get('top'),
|
|
true
|
|
);
|
|
}
|
|
} else if (id == 'object-r') {
|
|
newKeyframe(
|
|
'angle',
|
|
object,
|
|
currenttime,
|
|
object.get('angle'),
|
|
true
|
|
);
|
|
if (selection) {
|
|
newKeyframe(
|
|
'left',
|
|
object,
|
|
currenttime,
|
|
object.get('left'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'top',
|
|
object,
|
|
currenttime,
|
|
object.get('top'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'scaleX',
|
|
object,
|
|
currenttime,
|
|
object.get('scaleX'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'scaleY',
|
|
object,
|
|
currenttime,
|
|
object.get('scaleY'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'width',
|
|
object,
|
|
currenttime,
|
|
object.get('width'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'height',
|
|
object,
|
|
currenttime,
|
|
object.get('width'),
|
|
true
|
|
);
|
|
}
|
|
} else if (id == 'object-stroke') {
|
|
newKeyframe(
|
|
'strokeWidth',
|
|
object,
|
|
currenttime,
|
|
object.get('strokeWidth'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'stroke',
|
|
object,
|
|
currenttime,
|
|
object.get('stroke'),
|
|
true
|
|
);
|
|
} else if (
|
|
id == 'object-shadow-x' ||
|
|
id == 'object-shadow-y' ||
|
|
id == 'object-blur' ||
|
|
id == 'object-color-stroke-opacity'
|
|
) {
|
|
newKeyframe(
|
|
'shadow.color',
|
|
object,
|
|
currenttime,
|
|
object.shadow.color,
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'shadow.opacity',
|
|
object,
|
|
currenttime,
|
|
object.shadow.opacity,
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'shadow.offsetX',
|
|
object,
|
|
currenttime,
|
|
object.shadow.offsetX,
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'shadow.offsetY',
|
|
object,
|
|
currenttime,
|
|
object.shadow.offsetY,
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'shadow.blur',
|
|
object,
|
|
currenttime,
|
|
object.shadow.blur,
|
|
true
|
|
);
|
|
}
|
|
}
|
|
save();
|
|
}
|
|
|
|
// Play video
|
|
function play() {
|
|
paused = false;
|
|
animate(true, currenttime);
|
|
$('#play-button').attr('src', 'assets/pause-button.svg');
|
|
}
|
|
|
|
// Pause video
|
|
function pause() {
|
|
paused = true;
|
|
$('#play-button').attr('src', 'assets/play-button.svg');
|
|
}
|
|
|
|
// Write an object's stored default for a property, creating the entry if the
|
|
// object was serialized before that property existed.
|
|
function setDefaultValue(object, prop, value) {
|
|
const entry = objects.find((x) => x.id == object.get('id'));
|
|
if (!entry) {
|
|
return;
|
|
}
|
|
if (!entry.defaults) {
|
|
entry.defaults = [];
|
|
}
|
|
const def = entry.defaults.find((x) => x.name == prop);
|
|
if (def) {
|
|
def.value = value;
|
|
} else {
|
|
entry.defaults.push({ name: prop, value: value });
|
|
}
|
|
}
|
|
|
|
// Read an object's stored default for a property (undefined when absent)
|
|
function getDefaultValue(id, prop) {
|
|
const entry = objects.find((x) => x.id == id);
|
|
if (!entry || !entry.defaults) {
|
|
return undefined;
|
|
}
|
|
const def = entry.defaults.find((x) => x.name == prop);
|
|
return def ? def.value : undefined;
|
|
}
|
|
|
|
// Read a property that may be a "shadow.*" path.
|
|
// fabric's get() is flat, so object.get("shadow.blur") is always undefined.
|
|
function getPropValue(object, prop) {
|
|
if (typeof prop === 'string' && prop.indexOf('shadow.') === 0) {
|
|
return object.shadow ? object.shadow[prop.slice(7)] : undefined;
|
|
}
|
|
return object.get(prop);
|
|
}
|
|
|
|
// Set object value (while animating)
|
|
function setObjectValue(prop, object, value, inst) {
|
|
if (object.get('type') != 'group') {
|
|
if (object.group) {
|
|
var group = object.group;
|
|
tempgroup = group._objects;
|
|
group._restoreObjectsState();
|
|
canvas.setActiveObject(group);
|
|
inst.remove(canvas.getActiveObject());
|
|
canvas.discardActiveObject();
|
|
inst.renderAll();
|
|
for (var i = 0; i < tempgroup.length; i++) {
|
|
inst.add(tempgroup[i]);
|
|
}
|
|
}
|
|
}
|
|
if (prop == 'left' && !recording) {
|
|
object.set(prop, value + artboard.get('left'));
|
|
} else if (prop == 'top' && !recording) {
|
|
object.set(prop, value + artboard.get('top'));
|
|
} else if (prop == 'shadow.blur') {
|
|
object.shadow.blur = value;
|
|
} else if (prop == 'shadow.color') {
|
|
object.shadow.color = value;
|
|
} else if (prop == 'shadow.offsetX') {
|
|
object.shadow.offsetX = value;
|
|
} else if (prop == 'shadow.offsetY') {
|
|
object.shadow.offsetY = value;
|
|
} else if (object.get('type') != 'group') {
|
|
object.set(prop, value);
|
|
} else if (prop != 'width') {
|
|
object.set(prop, value);
|
|
}
|
|
object.setCoords();
|
|
inst.renderAll();
|
|
}
|
|
|
|
// Group keyframes by "id|name", each group sorted by time.
|
|
// Built once per rendered frame: without it every keyframe lookup sorted a
|
|
// copy of the whole keyframe list, making playback O(n^2 log n).
|
|
function buildKeyframeIndex() {
|
|
const idx = new Map();
|
|
keyframes.forEach(function (keyframe) {
|
|
const key = keyframe.id + '|' + keyframe.name;
|
|
let arr = idx.get(key);
|
|
if (!arr) {
|
|
arr = [];
|
|
idx.set(key, arr);
|
|
}
|
|
arr.push(keyframe);
|
|
});
|
|
idx.forEach(function (arr) {
|
|
arr.sort(function (a, b) {
|
|
return a.t - b.t;
|
|
});
|
|
});
|
|
return idx;
|
|
}
|
|
|
|
// All keyframes of one object/property, sorted by time
|
|
function keyframeSiblings(keyframe, idx) {
|
|
if (idx) {
|
|
return idx.get(keyframe.id + '|' + keyframe.name) || [];
|
|
}
|
|
return keyframes
|
|
.filter(function (e) {
|
|
return e.id == keyframe.id && e.name == keyframe.name;
|
|
})
|
|
.sort(function (a, b) {
|
|
return a.t - b.t;
|
|
});
|
|
}
|
|
|
|
// Find last keyframe in time from same object & property
|
|
function lastKeyframe(keyframe, idx) {
|
|
const arr = keyframeSiblings(keyframe, idx);
|
|
const pos = arr.indexOf(keyframe);
|
|
if (pos <= 0) {
|
|
return false;
|
|
}
|
|
return arr[pos - 1];
|
|
}
|
|
|
|
// Find next keyframe in time from same object & property
|
|
function nextKeyframe(keyframe, idx) {
|
|
const arr = keyframeSiblings(keyframe, idx);
|
|
const pos = arr.indexOf(keyframe);
|
|
if (pos == -1 || pos == arr.length - 1) {
|
|
return false;
|
|
}
|
|
return arr[pos + 1];
|
|
}
|
|
|
|
// Check whether any keyframe exists for a certain property
|
|
function checkAnyKeyframe(id, prop, inst, idx) {
|
|
const object = inst.getItemById(id);
|
|
if (!object) {
|
|
return false;
|
|
}
|
|
if (object.get('assetType') == 'audio') {
|
|
return false;
|
|
}
|
|
if (
|
|
object.get('type') != 'textbox' &&
|
|
(prop == 'charSpacing' || prop == 'lineHeight')
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
object.get('type') == 'group' &&
|
|
(prop == 'shadow.opacity' ||
|
|
prop == 'shadow.color' ||
|
|
prop == 'shadow.offsetX' ||
|
|
prop == 'shadow.offsetY' ||
|
|
prop == 'shadow.blur')
|
|
) {
|
|
return false;
|
|
}
|
|
const hasKeyframe = idx
|
|
? idx.has(id + '|' + prop)
|
|
: keyframes.some((e) => e.id == id && e.name == prop);
|
|
if (!hasKeyframe) {
|
|
const value = getDefaultValue(id, prop);
|
|
if (value === undefined) {
|
|
return false;
|
|
}
|
|
setObjectValue(prop, object, value, inst);
|
|
}
|
|
}
|
|
|
|
// Check if parameter is a DOM element
|
|
function isDomElem(el) {
|
|
return el instanceof HTMLElement || el[0] instanceof HTMLElement
|
|
? true
|
|
: false;
|
|
}
|
|
|
|
// Play videos when seeking/playing
|
|
async function playVideos(time) {
|
|
objects.forEach(async function (entry) {
|
|
var inst = recording ? canvasrecord : canvas;
|
|
var object = inst.getItemById(entry.id);
|
|
var p_keyframe = p_keyframes.find((x) => x.id == entry.id);
|
|
if (object == null || !p_keyframe) {
|
|
return false;
|
|
}
|
|
var start = false;
|
|
if (
|
|
object.get('id').indexOf('Video') >= 0 &&
|
|
p_keyframe.trimstart + p_keyframe.start <= time &&
|
|
p_keyframe.end >= time
|
|
) {
|
|
var tempfilters = object.filters;
|
|
if (object.filters.length > 0) {
|
|
object.filters = [];
|
|
object.applyFilters();
|
|
var image = object;
|
|
var backend = fabric.filterBackend;
|
|
if (backend && backend.evictCachesForKey) {
|
|
backend.evictCachesForKey(image.cacheKey);
|
|
backend.evictCachesForKey(image.cacheKey + '_filtered');
|
|
}
|
|
await object.setElement(object.saveElem);
|
|
}
|
|
object.set('visible', true);
|
|
inst.renderAll();
|
|
if ($(object.getElement())[0].paused == true) {
|
|
$(object.getElement())[0].currentTime = parseFloat(
|
|
(
|
|
(time - p_keyframe.start + p_keyframe.trimstart) /
|
|
1000
|
|
).toFixed(2)
|
|
);
|
|
}
|
|
if (!recording) {
|
|
var animation = {
|
|
value: 0,
|
|
};
|
|
var instance = anime({
|
|
targets: animation,
|
|
value: [currenttime, duration],
|
|
delay: 0,
|
|
duration: duration,
|
|
easing: 'linear',
|
|
autoplay: true,
|
|
update: async function () {
|
|
if (!paused && start) {
|
|
if (object.filters.length > 0) {
|
|
object.filters = [];
|
|
object.applyFilters();
|
|
var image = object;
|
|
var backend = fabric.filterBackend;
|
|
if (backend && backend.evictCachesForKey) {
|
|
backend.evictCachesForKey(image.cacheKey);
|
|
backend.evictCachesForKey(
|
|
image.cacheKey + '_filtered'
|
|
);
|
|
}
|
|
await object.setElement(object.saveElem);
|
|
}
|
|
if ($(object.getElement())[0].tagName == 'VIDEO') {
|
|
$(object.getElement())[0].play();
|
|
}
|
|
await inst.renderAll();
|
|
if (tempfilters.length > 0) {
|
|
object.filters = tempfilters;
|
|
object.applyFilters();
|
|
inst.renderAll();
|
|
}
|
|
} else if (paused) {
|
|
if (
|
|
isDomElem($(object.getElement())[0]) &&
|
|
$(object.getElement())[0].tagName == 'VIDEO'
|
|
) {
|
|
$(object.getElement())[0].pause();
|
|
}
|
|
animation.value = duration + 1;
|
|
anime.remove(animation);
|
|
}
|
|
},
|
|
changeBegin: function () {
|
|
start = true;
|
|
},
|
|
});
|
|
if (paused) {
|
|
$(object.getElement())[0].currentTime = parseFloat(
|
|
(
|
|
(time - p_keyframe.start + p_keyframe.trimstart) /
|
|
1000
|
|
).toFixed(2)
|
|
);
|
|
await inst.renderAll();
|
|
if (tempfilters.length > 0) {
|
|
object.filters = tempfilters;
|
|
object.applyFilters();
|
|
inst.renderAll();
|
|
}
|
|
}
|
|
} else {
|
|
if ($(object.getElement())[0].paused == true) {
|
|
$(object.getElement())[0].play();
|
|
inst.renderAll();
|
|
}
|
|
}
|
|
} else if (object.get('id').indexOf('Video') >= 0) {
|
|
$(object.getElement())[0].pause();
|
|
object.set('visible', false);
|
|
inst.renderAll();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Play background audio
|
|
function playAudio(time) {
|
|
objects.forEach(function (object) {
|
|
var start = false;
|
|
var obj = canvas.getItemById(object.id);
|
|
var p_keyframe = p_keyframes.find((x) => x.id == object.id);
|
|
if (obj && p_keyframe && obj.get('assetType') == 'audio') {
|
|
var flag = false;
|
|
var animation = {
|
|
value: 0,
|
|
};
|
|
var instance = anime({
|
|
targets: animation,
|
|
value: [currenttime, duration],
|
|
delay: 0,
|
|
duration: duration,
|
|
easing: 'linear',
|
|
autoplay: true,
|
|
update: function () {
|
|
// `paused` is the only playback flag here; `play` used to be read
|
|
// as a variable but resolves to the global play() function, so the
|
|
// condition was always true.
|
|
if (start && !paused) {
|
|
if (
|
|
!flag &&
|
|
p_keyframe.start <= currenttime &&
|
|
p_keyframe.end >= currenttime
|
|
) {
|
|
if (obj.get('src')) {
|
|
obj.get('src').currentTime =
|
|
(p_keyframe.trimstart -
|
|
p_keyframe.start +
|
|
currenttime) /
|
|
1000;
|
|
obj.get('src').volume = obj.get('volume');
|
|
obj.get('src').play();
|
|
flag = true;
|
|
} else {
|
|
var audio = new Audio(obj.get('audioSrc'));
|
|
obj.set('src', audio);
|
|
audio.volume = obj.get('volume');
|
|
audio.crossOrigin = 'anonymous';
|
|
audio.currentTime =
|
|
(p_keyframe.trimstart -
|
|
p_keyframe.start +
|
|
currenttime) /
|
|
1000;
|
|
audio.play();
|
|
flag = true;
|
|
}
|
|
} else if (
|
|
p_keyframe.start >= currenttime ||
|
|
p_keyframe.end <= currenttime
|
|
) {
|
|
if (obj.get('src')) {
|
|
obj.get('src').pause();
|
|
}
|
|
}
|
|
} else if (paused) {
|
|
if (obj.get('src')) {
|
|
obj.get('src').pause();
|
|
anime.remove(animation);
|
|
}
|
|
}
|
|
},
|
|
changeBegin: function () {
|
|
start = true;
|
|
},
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Temp animate with render callback
|
|
async function recordAnimate(time) {
|
|
anime.speed = 1;
|
|
//return new Promise(function(resolve){
|
|
var inst = canvasrecord;
|
|
const idx = buildKeyframeIndex();
|
|
if (animatedtext.length > 0) {
|
|
animatedtext.forEach(function (text) {
|
|
text.seek(time, inst);
|
|
inst.renderAll();
|
|
});
|
|
}
|
|
keyframes.forEach(function (keyframe, index) {
|
|
// Regroup if needed (groups break to animate their children, then regroup after children have animated)
|
|
if (groups.find((x) => x.id == keyframe.id)) {
|
|
if (!canvas.getItemById(keyframe.id)) {
|
|
reGroup(keyframe.id);
|
|
}
|
|
const object = inst.getItemById(keyframe.id);
|
|
const p_keyframe = p_keyframes.find((x) => x.id == keyframe.id);
|
|
if (!object || !p_keyframe) {
|
|
return;
|
|
}
|
|
if (time < p_keyframe.trimstart + p_keyframe.start) {
|
|
object.set('visible', false);
|
|
inst.renderAll();
|
|
} else if (time > p_keyframe.end || time > duration) {
|
|
object.set('visible', false);
|
|
inst.renderAll();
|
|
} else {
|
|
object.set('visible', true);
|
|
inst.renderAll();
|
|
}
|
|
if (time >= p_keyframe.trimstart + p_keyframe.start) {
|
|
props.forEach(function (prop) {
|
|
checkAnyKeyframe(keyframe.id, prop, inst, idx);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Copy of setObjectValue function, seems to perform better inside the function
|
|
function setValue(prop, object, value, inst) {
|
|
if (object.get('type') != 'group') {
|
|
if (object.group) {
|
|
var group = object.group;
|
|
tempgroup = group._objects;
|
|
group._restoreObjectsState();
|
|
canvas.setActiveObject(group);
|
|
inst.remove(canvas.getActiveObject());
|
|
canvas.discardActiveObject();
|
|
inst.renderAll();
|
|
for (var i = 0; i < tempgroup.length; i++) {
|
|
inst.add(tempgroup[i]);
|
|
}
|
|
}
|
|
}
|
|
if (prop == 'left' && !recording) {
|
|
object.set(prop, value + artboard.get('left'));
|
|
} else if (prop == 'top' && !recording) {
|
|
object.set(prop, value + artboard.get('top'));
|
|
} else if (prop == 'shadow.blur') {
|
|
object.shadow.blur = value;
|
|
} else if (prop == 'shadow.color') {
|
|
object.shadow.color = value;
|
|
} else if (prop == 'shadow.offsetX') {
|
|
object.shadow.offsetX = value;
|
|
} else if (prop == 'shadow.offsetY') {
|
|
object.shadow.offsetY = value;
|
|
} else if (object.get('type') != 'group') {
|
|
object.set(prop, value);
|
|
} else if (prop != 'width') {
|
|
object.set(prop, value);
|
|
}
|
|
object.setCoords();
|
|
inst.renderAll();
|
|
}
|
|
|
|
var object = canvasrecord.getItemById(keyframe.id);
|
|
if (!object) {
|
|
return;
|
|
}
|
|
if (
|
|
keyframe.t >= time &&
|
|
time >=
|
|
p_keyframes.find((x) => x.id == keyframe.id).trimstart +
|
|
p_keyframes.find((x) => x.id == keyframe.id).start
|
|
) {
|
|
var delay = 0;
|
|
var start = false;
|
|
var lasttime, lastprop;
|
|
// Find last keyframe in time from same object & property
|
|
var lastkey = lastKeyframe(keyframe, idx);
|
|
if (!lastkey) {
|
|
lasttime = 0;
|
|
lastprop = getDefaultValue(keyframe.id, keyframe.name);
|
|
if (lastprop === undefined) {
|
|
return;
|
|
}
|
|
} else {
|
|
lasttime = lastkey.t;
|
|
lastprop = lastkey.value;
|
|
}
|
|
// Every recorded frame is rendered like a seek, so a keyframe whose
|
|
// preceding keyframe is still in the future has not started yet.
|
|
// (This used to read an undeclared `play`, throwing a ReferenceError
|
|
// and aborting the rest of the frame.)
|
|
if (lastkey && lastkey.t >= time) {
|
|
return;
|
|
}
|
|
|
|
// Initiate the animation
|
|
var animation = {
|
|
value: lastprop,
|
|
};
|
|
var instance = anime({
|
|
targets: animation,
|
|
delay: delay,
|
|
value: keyframe.value,
|
|
duration: keyframe.t - lasttime,
|
|
easing: keyframe.easing,
|
|
autoplay: false,
|
|
update: function () {
|
|
if (start && paused) {
|
|
anime.remove(animation);
|
|
}
|
|
},
|
|
changeBegin: function () {
|
|
start = true;
|
|
},
|
|
});
|
|
|
|
if (time - lasttime <= 0) {
|
|
instance.seek(0);
|
|
} else {
|
|
instance.seek(time - lasttime);
|
|
}
|
|
|
|
if (
|
|
parseFloat(lasttime) <= parseFloat(time) &&
|
|
parseFloat(keyframe.t) >= parseFloat(time)
|
|
) {
|
|
setValue(keyframe.name, object, animation.value, inst);
|
|
}
|
|
} else if (keyframe.t < time && !nextKeyframe(keyframe, idx)) {
|
|
var prop = keyframe.name;
|
|
if (prop == 'shadow.blur') {
|
|
if (object.shadow.blur != keyframe.value) {
|
|
setValue(keyframe.name, object, keyframe.value, inst);
|
|
}
|
|
} else if (prop == 'shadow.color') {
|
|
if (object.shadow.color != keyframe.value) {
|
|
setValue(keyframe.name, object, keyframe.value, inst);
|
|
}
|
|
} else if (prop == 'shadow.offsetX') {
|
|
if (object.shadow.offsetX != keyframe.value) {
|
|
setValue(keyframe.name, object, keyframe.value, inst);
|
|
}
|
|
} else if (prop == 'shadow.offsetY') {
|
|
if (object.shadow.offsetY != keyframe.value) {
|
|
setValue(keyframe.name, object, keyframe.value, inst);
|
|
}
|
|
} else {
|
|
if (object.get(prop) != keyframe.value) {
|
|
setValue(keyframe.name, object, keyframe.value, inst);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
objects.forEach(function (object) {
|
|
if (object.id.indexOf('Group') == -1) {
|
|
// Visibility has to be applied to the object on the recording canvas,
|
|
// at the time of the frame being rendered (not the editor playhead).
|
|
const object2 = inst.getItemById(object.id);
|
|
const p_keyframe = p_keyframes.find((x) => x.id == object.id);
|
|
if (!object2 || !p_keyframe) {
|
|
return;
|
|
}
|
|
if (time < p_keyframe.trimstart + p_keyframe.start) {
|
|
object2.set('visible', false);
|
|
} else if (time > p_keyframe.end || time > duration) {
|
|
object2.set('visible', false);
|
|
} else {
|
|
object2.set('visible', true);
|
|
}
|
|
if (time >= p_keyframe.trimstart + p_keyframe.start) {
|
|
props.forEach(function (prop) {
|
|
checkAnyKeyframe(object.id, prop, inst, idx);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
inst.renderAll();
|
|
|
|
// The offline renderer seeks each video layer to the exact frame time
|
|
// itself; playVideos() would fight it by starting real-time playback.
|
|
if (!offlinerender) {
|
|
playVideos(time);
|
|
}
|
|
//});
|
|
}
|
|
|
|
// Animate timeline (or seek to specific point in time)
|
|
async function animate(play, time) {
|
|
anime.speed = speed;
|
|
if (!draggingPanel) {
|
|
var offset = time;
|
|
var inst = canvas;
|
|
const idx = buildKeyframeIndex();
|
|
keyframes.forEach(function (keyframe, index) {
|
|
// Regroup if needed (groups break to animate their children, then regroup after children have animated)
|
|
if (groups.find((x) => x.id == keyframe.id)) {
|
|
if (!canvas.getItemById(keyframe.id)) {
|
|
reGroup(keyframe.id);
|
|
}
|
|
const object = canvas.getItemById(keyframe.id);
|
|
const p_keyframe = p_keyframes.find(
|
|
(x) => x.id == keyframe.id
|
|
);
|
|
if (!object || !p_keyframe) {
|
|
return;
|
|
}
|
|
if (currenttime < p_keyframe.trimstart + p_keyframe.start) {
|
|
object.set('visible', false);
|
|
inst.renderAll();
|
|
} else if (
|
|
currenttime > p_keyframe.end ||
|
|
currenttime > duration
|
|
) {
|
|
object.set('visible', false);
|
|
inst.renderAll();
|
|
} else {
|
|
object.set('visible', true);
|
|
inst.renderAll();
|
|
}
|
|
if (currenttime >= p_keyframe.trimstart + p_keyframe.start) {
|
|
props.forEach(function (prop) {
|
|
checkAnyKeyframe(keyframe.id, prop, inst, idx);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Copy of setObjectValue function, seems to perform better inside the function
|
|
function setValue(prop, object, value, inst) {
|
|
if (object.get('assetType') == 'audio' && play) {
|
|
if (object.get('src')) {
|
|
object.get('src').volume = value;
|
|
object.set('volume', value);
|
|
}
|
|
return false;
|
|
}
|
|
if (object.get('type') != 'group') {
|
|
if (object.group) {
|
|
/*
|
|
var group = object.group;
|
|
tempgroup = group._objects;
|
|
group._restoreObjectsState();
|
|
canvas.setActiveObject(group);
|
|
inst.remove(canvas.getActiveObject());
|
|
canvas.discardActiveObject();
|
|
inst.renderAll();
|
|
for (var i = 0; i < tempgroup.length; i++) {
|
|
inst.add(tempgroup[i]);
|
|
}
|
|
*/
|
|
}
|
|
}
|
|
if (prop == 'left' && !recording) {
|
|
object.set(prop, value + artboard.get('left'));
|
|
} else if (prop == 'top' && !recording) {
|
|
object.set(prop, value + artboard.get('top'));
|
|
} else if (prop == 'shadow.blur') {
|
|
object.shadow.blur = value;
|
|
} else if (prop == 'shadow.color') {
|
|
object.shadow.color = value;
|
|
} else if (prop == 'shadow.offsetX') {
|
|
object.shadow.offsetX = value;
|
|
} else if (prop == 'shadow.offsetY') {
|
|
object.shadow.offsetY = value;
|
|
} else if (object.get('type') != 'group') {
|
|
object.set(prop, value);
|
|
} else if (prop != 'width') {
|
|
object.set(prop, value);
|
|
}
|
|
inst.renderAll();
|
|
}
|
|
|
|
var object = canvas.getItemById(keyframe.id);
|
|
var kf_p_keyframe = p_keyframes.find(
|
|
(x) => x.id == keyframe.id
|
|
);
|
|
if (!object || !kf_p_keyframe) {
|
|
return;
|
|
}
|
|
if (
|
|
keyframe.t >= time &&
|
|
currenttime >=
|
|
kf_p_keyframe.trimstart + kf_p_keyframe.start
|
|
) {
|
|
var delay = 0;
|
|
var start = false;
|
|
var lasttime, lastprop;
|
|
// Find last keyframe in time from same object & property
|
|
var lastkey = lastKeyframe(keyframe, idx);
|
|
if (!lastkey) {
|
|
lasttime = 0;
|
|
lastprop = getDefaultValue(keyframe.id, keyframe.name);
|
|
if (lastprop === undefined) {
|
|
return;
|
|
}
|
|
} else {
|
|
lasttime = lastkey.t;
|
|
lastprop = lastkey.value;
|
|
}
|
|
if (lastkey && lastkey.t >= time && !play) {
|
|
return;
|
|
}
|
|
// Set delay for the animation if playing
|
|
if (play) {
|
|
if (lasttime > currenttime) {
|
|
delay = lasttime - time;
|
|
}
|
|
}
|
|
// Initiate the animation
|
|
var animation = {
|
|
value: lastprop,
|
|
};
|
|
var instance = anime({
|
|
targets: animation,
|
|
delay: delay,
|
|
value: keyframe.value,
|
|
duration: keyframe.t - lasttime,
|
|
easing: keyframe.easing,
|
|
autoplay: false,
|
|
update: function () {
|
|
if (start && !paused) {
|
|
if (
|
|
currenttime <
|
|
kf_p_keyframe.trimstart + kf_p_keyframe.start ||
|
|
currenttime > kf_p_keyframe.end ||
|
|
currenttime > duration
|
|
) {
|
|
object.set('visible', false);
|
|
inst.renderAll();
|
|
} else {
|
|
setValue(
|
|
keyframe.name,
|
|
object,
|
|
animation.value,
|
|
inst
|
|
);
|
|
object.set('visible', true);
|
|
inst.renderAll();
|
|
}
|
|
} else if (start && paused) {
|
|
anime.remove(animation);
|
|
}
|
|
},
|
|
changeBegin: function () {
|
|
start = true;
|
|
},
|
|
});
|
|
|
|
if (time - lasttime <= 0) {
|
|
instance.seek(0);
|
|
} else {
|
|
instance.seek(time - lasttime);
|
|
}
|
|
|
|
if (play) {
|
|
instance.play();
|
|
} else if (
|
|
parseFloat(lasttime) <= parseFloat(time) &&
|
|
parseFloat(keyframe.t) >= parseFloat(time)
|
|
) {
|
|
setValue(keyframe.name, object, animation.value, inst);
|
|
}
|
|
} else if (
|
|
keyframe.t < time &&
|
|
!nextKeyframe(keyframe, idx)
|
|
) {
|
|
var prop = keyframe.name;
|
|
if (prop == 'left' && !recording) {
|
|
if (
|
|
object.get('left') - artboard.get('left') !=
|
|
keyframe.value
|
|
) {
|
|
setValue(keyframe.name, object, keyframe.value, inst);
|
|
}
|
|
} else if (prop == 'top' && !recording) {
|
|
if (
|
|
object.get('top') - artboard.get('top') !=
|
|
keyframe.value
|
|
) {
|
|
setValue(keyframe.name, object, keyframe.value, inst);
|
|
}
|
|
} else if (prop == 'shadow.blur') {
|
|
if (object.shadow.blur != keyframe.value) {
|
|
setValue(keyframe.name, object, keyframe.value, inst);
|
|
}
|
|
} else if (prop == 'shadow.color') {
|
|
if (object.shadow.color != keyframe.value) {
|
|
setValue(keyframe.name, object, keyframe.value, inst);
|
|
}
|
|
} else if (prop == 'shadow.offsetX') {
|
|
if (object.shadow.offsetX != keyframe.value) {
|
|
setValue(keyframe.name, object, keyframe.value, inst);
|
|
}
|
|
} else if (prop == 'shadow.offsetY') {
|
|
if (object.shadow.offsetY != keyframe.value) {
|
|
setValue(keyframe.name, object, keyframe.value, inst);
|
|
}
|
|
} else {
|
|
if (object.get(prop) != keyframe.value) {
|
|
setValue(keyframe.name, object, keyframe.value, inst);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
/*
|
|
if (play) {
|
|
p_keyframes.forEach(function(keyframe){
|
|
inst.getItemById(keyframe.id).set("visible", false);
|
|
window.setTimeout(function(){
|
|
if (!paused) {
|
|
inst.getItemById(keyframe.id).set("visible", true);
|
|
inst.renderAll();
|
|
}
|
|
}, keyframe.start-time)
|
|
window.setTimeout(function(){
|
|
if (!paused) {
|
|
inst.getItemById(keyframe.id).set("visible", false);
|
|
inst.renderAll();
|
|
}
|
|
}, keyframe.end-time)
|
|
})
|
|
}
|
|
*/
|
|
objects.forEach(function (object) {
|
|
if (object.id.indexOf('Group') == -1) {
|
|
const object2 = canvas.getItemById(object.id);
|
|
const p_keyframe = p_keyframes.find((x) => x.id == object.id);
|
|
if (!object2 || !p_keyframe) {
|
|
return;
|
|
}
|
|
if (currenttime < p_keyframe.trimstart + p_keyframe.start) {
|
|
object2.set('visible', false);
|
|
} else if (
|
|
currenttime > p_keyframe.end ||
|
|
currenttime > duration
|
|
) {
|
|
object2.set('visible', false);
|
|
} else {
|
|
object2.set('visible', true);
|
|
}
|
|
if (currenttime >= p_keyframe.trimstart + p_keyframe.start) {
|
|
props.forEach(function (prop) {
|
|
checkAnyKeyframe(object.id, prop, inst, idx);
|
|
});
|
|
}
|
|
}
|
|
var obj = canvas.getItemById(object.id);
|
|
if (obj && obj.type == 'lottie') {
|
|
obj.goToSeconds(currenttime);
|
|
inst.renderAll();
|
|
}
|
|
});
|
|
inst.renderAll();
|
|
|
|
if (animatedtext.length > 0) {
|
|
animatedtext.forEach(function (text) {
|
|
text.seek(currenttime, canvas);
|
|
inst.renderAll();
|
|
});
|
|
}
|
|
|
|
playVideos(time);
|
|
if (play) {
|
|
playAudio(time);
|
|
}
|
|
if (play && !paused) {
|
|
var animation = {
|
|
value: 0,
|
|
};
|
|
var main_instance = anime({
|
|
targets: animation,
|
|
value: [currenttime, duration],
|
|
duration: duration - currenttime,
|
|
easing: 'linear',
|
|
autoplay: true,
|
|
update: function () {
|
|
if (!paused) {
|
|
currenttime = animation.value;
|
|
if (animatedtext.length > 0) {
|
|
animatedtext.forEach(function (text) {
|
|
text.seek(currenttime, canvas);
|
|
inst.renderAll();
|
|
});
|
|
}
|
|
objects.forEach(function (object) {
|
|
if (object.id.indexOf('Group') == -1) {
|
|
const object2 = inst.getItemById(object.id);
|
|
const p_keyframe = p_keyframes.find(
|
|
(x) => x.id == object.id
|
|
);
|
|
if (!object2 || !p_keyframe) {
|
|
return;
|
|
}
|
|
if (
|
|
currenttime <
|
|
p_keyframe.trimstart + p_keyframe.start
|
|
) {
|
|
object2.set('visible', false);
|
|
} else if (
|
|
currenttime > p_keyframe.end ||
|
|
currenttime > duration
|
|
) {
|
|
object2.set('visible', false);
|
|
} else {
|
|
object2.set('visible', true);
|
|
}
|
|
if (
|
|
currenttime >=
|
|
p_keyframe.trimstart + p_keyframe.start
|
|
) {
|
|
props.forEach(function (prop) {
|
|
checkAnyKeyframe(object.id, prop, inst, idx);
|
|
});
|
|
}
|
|
}
|
|
var obj = canvas.getItemById(object.id);
|
|
if (obj && obj.type == 'lottie') {
|
|
obj.goToSeconds(currenttime);
|
|
inst.renderAll();
|
|
}
|
|
});
|
|
inst.renderAll();
|
|
if (!recording) {
|
|
renderTime();
|
|
$('#seekbar').css({
|
|
left: currenttime / timelinetime + offset_left,
|
|
});
|
|
}
|
|
} else {
|
|
pause();
|
|
animation.value = duration + 1;
|
|
anime.remove(animation);
|
|
}
|
|
},
|
|
complete: function () {
|
|
pause();
|
|
},
|
|
});
|
|
} else if (paused) {
|
|
currenttime = time;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Keep group keyframes last so children animate before their group regroups
|
|
function sortKeyframes() {
|
|
keyframes.sort(function (a, b) {
|
|
if (a.id.indexOf('Group') >= 0 && b.id.indexOf('Group') == -1) {
|
|
return 1;
|
|
} else if (
|
|
b.id.indexOf('Group') >= 0 &&
|
|
a.id.indexOf('Group') == -1
|
|
) {
|
|
return -1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Render a keyframe.
|
|
// data-time is ALWAYS the absolute timeline time, because every lookup keys
|
|
// off it. The CSS offset is relative, since the row it lives in is already
|
|
// shifted by the layer start.
|
|
function renderKeyframe(object, prop, time) {
|
|
const entry = objects.find((x) => x.id == object.get('id'));
|
|
const p_keyframe = p_keyframes.find(
|
|
(x) => x.id == object.get('id')
|
|
);
|
|
if (!entry || !p_keyframe) {
|
|
return;
|
|
}
|
|
const color = entry.color;
|
|
const rowclass = prop == 'shadow.color' ? 'shadowcolor' : prop;
|
|
const row = $('#' + object.get('id')).find('.' + rowclass);
|
|
if (row.length == 0) {
|
|
return;
|
|
}
|
|
const displaytime = time - parseFloat(p_keyframe.start);
|
|
row.prepend(
|
|
"<div class='keyframe' data-time='" +
|
|
time +
|
|
"' data-object='" +
|
|
object.get('id') +
|
|
"' data-property='" +
|
|
prop +
|
|
"'></div>"
|
|
);
|
|
row
|
|
.find("[data-time='" + time + "'][data-property='" + prop + "']")
|
|
.css({ left: displaytime / timelinetime, background: color });
|
|
}
|
|
|
|
// Create a keyframe
|
|
function newKeyframe(property, object, time, value, render) {
|
|
// Check if property can be animated
|
|
if (
|
|
$.inArray(
|
|
property,
|
|
objects.find((x) => x.id == object.get('id')).animate
|
|
) != -1
|
|
) {
|
|
const keyarr = $.grep(keyframes, function (e) {
|
|
return (
|
|
e.t == parseFloat(time) &&
|
|
e.id == object.get('id') &&
|
|
e.name == property
|
|
);
|
|
});
|
|
const keyarr2 = $.grep(keyframes, function (e) {
|
|
return e.id == object.get('id') && e.name == property;
|
|
});
|
|
if (keyarr2.length == 0) {
|
|
if (property == 'left') {
|
|
setDefaultValue(
|
|
object,
|
|
property,
|
|
object.get(property) - artboard.get('left')
|
|
);
|
|
} else if (property == 'top') {
|
|
setDefaultValue(
|
|
object,
|
|
property,
|
|
object.get(property) - artboard.get('top')
|
|
);
|
|
} else {
|
|
setDefaultValue(object, property, value);
|
|
}
|
|
}
|
|
if (keyarr.length == 0) {
|
|
if (property == 'left') {
|
|
keyframes.push({
|
|
t: time,
|
|
name: property,
|
|
value: value - artboard.get('left'),
|
|
id: object.get('id'),
|
|
easing: 'linear',
|
|
});
|
|
} else if (property == 'top') {
|
|
keyframes.push({
|
|
t: time,
|
|
name: property,
|
|
value: value - artboard.get('top'),
|
|
id: object.get('id'),
|
|
easing: 'linear',
|
|
});
|
|
} else {
|
|
keyframes.push({
|
|
t: time,
|
|
name: property,
|
|
value: value,
|
|
id: object.get('id'),
|
|
easing: 'linear',
|
|
});
|
|
}
|
|
if (
|
|
render &&
|
|
property != 'top' &&
|
|
property != 'scaleY' &&
|
|
property != 'width' &&
|
|
property != 'height' &&
|
|
property != 'stroke' &&
|
|
property != 'shadow.opacity' &&
|
|
property != 'shadow.offsetX' &&
|
|
property != 'shadow.offsetY' &&
|
|
property != 'shadow.blur' &&
|
|
property != 'lineHeight'
|
|
) {
|
|
renderKeyframe(object, property, time);
|
|
}
|
|
sortKeyframes();
|
|
} else if (render) {
|
|
if (
|
|
property != 'top' &&
|
|
property != 'scaleY' &&
|
|
property != 'width' &&
|
|
property != 'height' &&
|
|
property != 'stroke' &&
|
|
property != 'shadow.opacity' &&
|
|
property != 'shadow.offsetX' &&
|
|
property != 'shadow.offsetY' &&
|
|
property != 'shadow.blur' &&
|
|
property != 'lineHeight'
|
|
) {
|
|
updateKeyframe(
|
|
$('#' + object.get('id')).find(
|
|
".keyframe[data-time='" +
|
|
time +
|
|
"'][data-property='" +
|
|
property +
|
|
"']"
|
|
),
|
|
true
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
if (property == 'left') {
|
|
setDefaultValue(
|
|
object,
|
|
property,
|
|
object.get(property) - artboard.get('left')
|
|
);
|
|
} else if (property == 'top') {
|
|
setDefaultValue(
|
|
object,
|
|
property,
|
|
object.get(property) - artboard.get('top')
|
|
);
|
|
} else {
|
|
setDefaultValue(object, property, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create a keyframe (manually)
|
|
function manualKeyframe() {
|
|
var prop = $(this).parent().attr('data-property');
|
|
const object = canvas.getItemById(
|
|
$(this).parent().parent().parent().attr('data-object')
|
|
);
|
|
if (prop == 'position') {
|
|
prop = 'left';
|
|
newKeyframe('top', object, currenttime, object.get('top'), true);
|
|
} else if (prop == 'scale') {
|
|
prop = 'scaleX';
|
|
newKeyframe(
|
|
'scaleY',
|
|
object,
|
|
currenttime,
|
|
object.get('scaleY'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'width',
|
|
object,
|
|
currenttime,
|
|
object.get('width'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'height',
|
|
object,
|
|
currenttime,
|
|
object.get('height'),
|
|
true
|
|
);
|
|
} else if (prop == 'stroke') {
|
|
prop = 'strokeWidth';
|
|
newKeyframe(
|
|
'stroke',
|
|
object,
|
|
currenttime,
|
|
object.get('stroke'),
|
|
true
|
|
);
|
|
} else if (prop == 'shadow') {
|
|
prop = 'shadow.color';
|
|
// fabric's get() is not a path getter - read the shadow object directly
|
|
newKeyframe(
|
|
'shadow.opacity',
|
|
object,
|
|
currenttime,
|
|
object.shadow.opacity,
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'shadow.offsetX',
|
|
object,
|
|
currenttime,
|
|
object.shadow.offsetX,
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'shadow.offsetY',
|
|
object,
|
|
currenttime,
|
|
object.shadow.offsetY,
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'shadow.blur',
|
|
object,
|
|
currenttime,
|
|
object.shadow.blur,
|
|
true
|
|
);
|
|
} else if (prop == 'text') {
|
|
prop = 'charSpacing';
|
|
newKeyframe(
|
|
'lineHeight',
|
|
object,
|
|
currenttime,
|
|
object.get('lineHeight'),
|
|
true
|
|
);
|
|
}
|
|
newKeyframe(prop, object, currenttime, getPropValue(object, prop), true);
|
|
save();
|
|
}
|
|
$(document).on('click', '.property-keyframe', manualKeyframe);
|
|
|
|
// Freeze all properties (this is counterintuitve because I initially programmed it to work the other way around)
|
|
function toggleAnimate(e) {
|
|
e.stopPropagation();
|
|
const object = canvas.getItemById(
|
|
$(this).parent().parent().parent().attr('data-object')
|
|
);
|
|
// Turn off clock -> Stop animation
|
|
if ($(this).hasClass('frozen')) {
|
|
$(this).removeClass('frozen');
|
|
$(this).attr('src', 'assets/freeze.svg');
|
|
objects.find((x) => x.id == object.get('id')).animate = [];
|
|
// Turn on clock -> Start animation
|
|
} else {
|
|
$(this).addClass('frozen');
|
|
$(this).attr('src', 'assets/frozen.svg');
|
|
objects.find((x) => x.id == object.get('id')).animate = [];
|
|
if (object.get('assetType') == 'audio') {
|
|
objects
|
|
.find((x) => x.id == object.get('id'))
|
|
.animate.push('volume');
|
|
keyframes = $.grep(keyframes, function (e) {
|
|
return e.id != object.get('id');
|
|
});
|
|
$(".keyframe[data-object='" + object.get('id') + "']").remove();
|
|
$(this)
|
|
.parent()
|
|
.parent()
|
|
.parent()
|
|
.find('.freeze-prop')
|
|
.removeClass('frozen');
|
|
newKeyframe('volume', object, 0, 0.5, true);
|
|
} else {
|
|
props.forEach(function (prop) {
|
|
objects
|
|
.find((x) => x.id == object.get('id'))
|
|
.animate.push(prop);
|
|
});
|
|
keyframes = $.grep(keyframes, function (e) {
|
|
return e.id != object.get('id');
|
|
});
|
|
$(".keyframe[data-object='" + object.get('id') + "']").remove();
|
|
$(this)
|
|
.parent()
|
|
.parent()
|
|
.parent()
|
|
.find('.freeze-prop')
|
|
.removeClass('frozen');
|
|
|
|
props.forEach(function (prop) {
|
|
if (prop == 'lineHeight' || prop == 'charSpacing') {
|
|
if (object.get('type') == 'textbox') {
|
|
newKeyframe(prop, object, 0, object.get(prop), true);
|
|
}
|
|
} else if (
|
|
prop == 'shadow.opacity' ||
|
|
prop == 'shadow.blur' ||
|
|
prop == 'shadow.offsetX' ||
|
|
prop == 'shadow.offsetY' ||
|
|
prop == 'shadow.color'
|
|
) {
|
|
if (object.get('type') != 'group') {
|
|
if (prop == 'shadow.color') {
|
|
newKeyframe(prop, object, 0, object.shadow.color, true);
|
|
} else if (prop == 'shadow.opacity') {
|
|
newKeyframe(
|
|
prop,
|
|
object,
|
|
0,
|
|
object.shadow.opacity,
|
|
true
|
|
);
|
|
} else if (prop == 'shadow.offsetX') {
|
|
newKeyframe(
|
|
prop,
|
|
object,
|
|
0,
|
|
object.shadow.offsetX,
|
|
true
|
|
);
|
|
} else if (prop == 'shadow.offsetY') {
|
|
newKeyframe(
|
|
prop,
|
|
object,
|
|
0,
|
|
object.shadow.offsetY,
|
|
true
|
|
);
|
|
} else if (prop == 'shadow.blur') {
|
|
newKeyframe(prop, object, 0, object.shadow.blur, true);
|
|
}
|
|
}
|
|
} else {
|
|
newKeyframe(prop, object, 0, object.get(prop), true);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
save();
|
|
}
|
|
$(document).on('click', '.freeze', toggleAnimate);
|
|
|
|
function animateProp(prop, object) {
|
|
objects.find((x) => x.id == object.get('id')).animate.push(prop);
|
|
|
|
// Prop counterparts
|
|
if (prop == 'left') {
|
|
objects.find((x) => x.id == object.get('id')).animate.push('top');
|
|
newKeyframe(
|
|
'left',
|
|
object,
|
|
currenttime,
|
|
object.get('left'),
|
|
true
|
|
);
|
|
newKeyframe('top', object, currenttime, object.get('top'), true);
|
|
setDefaultValue(object, 'left', object.get('left') - artboard.get('left'));
|
|
setDefaultValue(object, 'top', object.get('top') - artboard.get('top'));
|
|
} else if (prop == 'scaleX') {
|
|
newKeyframe(
|
|
'scaleY',
|
|
object,
|
|
currenttime,
|
|
object.get('scaleY'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'width',
|
|
object,
|
|
currenttime,
|
|
object.get('width'),
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'height',
|
|
object,
|
|
currenttime,
|
|
object.get('height'),
|
|
true
|
|
);
|
|
objects
|
|
.find((x) => x.id == object.get('id'))
|
|
.animate.push('scaleY');
|
|
objects
|
|
.find((x) => x.id == object.get('id'))
|
|
.animate.push('width');
|
|
objects
|
|
.find((x) => x.id == object.get('id'))
|
|
.animate.push('height');
|
|
setDefaultValue(object, 'height', object.get('height'));
|
|
setDefaultValue(object, 'width', object.get('width'));
|
|
setDefaultValue(object, 'scaleY', object.get('scaleY'));
|
|
} else if (prop == 'strokeWidth') {
|
|
newKeyframe(
|
|
'stroke',
|
|
object,
|
|
currenttime,
|
|
object.get('stroke'),
|
|
true
|
|
);
|
|
setDefaultValue(object, 'stroke', object.get('stroke'));
|
|
objects
|
|
.find((x) => x.id == object.get('id'))
|
|
.animate.push('stroke');
|
|
} else if (prop == 'shadow.color') {
|
|
newKeyframe(
|
|
'shadow.color',
|
|
object,
|
|
currenttime,
|
|
object.shadow.color,
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'shadow.opacity',
|
|
object,
|
|
currenttime,
|
|
object.shadow.opacity,
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'shadow.offsetX',
|
|
object,
|
|
currenttime,
|
|
object.shadow.offsetX,
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'shadow.offsetY',
|
|
object,
|
|
currenttime,
|
|
object.shadow.offsetY,
|
|
true
|
|
);
|
|
newKeyframe(
|
|
'shadow.blur',
|
|
object,
|
|
currenttime,
|
|
object.shadow.blur,
|
|
true
|
|
);
|
|
setDefaultValue(object, 'shadow.color', object.shadow.color);
|
|
setDefaultValue(object, 'shadow.opacity', object.shadow.opacity);
|
|
setDefaultValue(object, 'shadow.offsetX', object.shadow.offsetX);
|
|
setDefaultValue(object, 'shadow.offsetY', object.shadow.offsetY);
|
|
setDefaultValue(object, 'shadow.blur', object.shadow.blur);
|
|
objects
|
|
.find((x) => x.id == object.get('id'))
|
|
.animate.push('shadow.opacity');
|
|
objects
|
|
.find((x) => x.id == object.get('id'))
|
|
.animate.push('shadow.offsetX');
|
|
objects
|
|
.find((x) => x.id == object.get('id'))
|
|
.animate.push('shadow.offsetY');
|
|
objects
|
|
.find((x) => x.id == object.get('id'))
|
|
.animate.push('shadow.blur');
|
|
} else if (prop == 'charSpacing') {
|
|
newKeyframe(
|
|
'lineHeight',
|
|
object,
|
|
currenttime,
|
|
object.get('lineHeight'),
|
|
true
|
|
);
|
|
setDefaultValue(object, 'lineHeight', object.get('lineHeight'));
|
|
objects
|
|
.find((x) => x.id == object.get('id'))
|
|
.animate.push('lineHeight');
|
|
}
|
|
|
|
// Exception
|
|
if (prop != 'left' && prop != 'shadow.color') {
|
|
newKeyframe(prop, object, currenttime, object.get(prop), true);
|
|
setDefaultValue(object, prop, object.get(prop));
|
|
}
|
|
}
|
|
|
|
function freezeProp(prop, object) {
|
|
objects.find((x) => x.id == object.get('id')).animate = $.grep(
|
|
objects.find((x) => x.id == object.get('id')).animate,
|
|
function (e) {
|
|
return e != prop;
|
|
}
|
|
);
|
|
// Also add prop counterparts (should probably have done in a better way)
|
|
if (prop == 'left') {
|
|
objects.find((x) => x.id == object.get('id')).animate = $.grep(
|
|
objects.find((x) => x.id == object.get('id')).animate,
|
|
function (e) {
|
|
return e != 'top';
|
|
}
|
|
);
|
|
keyframes = $.grep(keyframes, function (e) {
|
|
return e.id != object.get('id') || e.name != 'top';
|
|
});
|
|
setDefaultValue(object, 'left', object.get('left') - artboard.get('left'));
|
|
setDefaultValue(object, 'top', object.get('top') - artboard.get('top'));
|
|
} else if (prop == 'scaleX') {
|
|
setDefaultValue(object, 'height', object.get('height'));
|
|
setDefaultValue(object, 'width', object.get('width'));
|
|
setDefaultValue(object, 'scaleY', object.get('scaleY'));
|
|
objects.find((x) => x.id == object.get('id')).animate = $.grep(
|
|
objects.find((x) => x.id == object.get('id')).animate,
|
|
function (e) {
|
|
return e != 'scaleY';
|
|
}
|
|
);
|
|
keyframes = $.grep(keyframes, function (e) {
|
|
return e.id != object.get('id') || e.name != 'scaleY';
|
|
});
|
|
objects.find((x) => x.id == object.get('id')).animate = $.grep(
|
|
objects.find((x) => x.id == object.get('id')).animate,
|
|
function (e) {
|
|
return e != 'width';
|
|
}
|
|
);
|
|
keyframes = $.grep(keyframes, function (e) {
|
|
return e.id != object.get('id') || e.name != 'width';
|
|
});
|
|
objects.find((x) => x.id == object.get('id')).animate = $.grep(
|
|
objects.find((x) => x.id == object.get('id')).animate,
|
|
function (e) {
|
|
return e != 'height';
|
|
}
|
|
);
|
|
keyframes = $.grep(keyframes, function (e) {
|
|
return e.id != object.get('id') || e.name != 'height';
|
|
});
|
|
} else if (prop == 'strokeWidth') {
|
|
setDefaultValue(object, 'stroke', object.get('stroke'));
|
|
objects.find((x) => x.id == object.get('id')).animate = $.grep(
|
|
objects.find((x) => x.id == object.get('id')).animate,
|
|
function (e) {
|
|
return e != 'stroke';
|
|
}
|
|
);
|
|
keyframes = $.grep(keyframes, function (e) {
|
|
return e.id != object.get('id') || e.name != 'stroke';
|
|
});
|
|
} else if (prop == 'shadow.color') {
|
|
setDefaultValue(object, 'shadow.opacity', object.shadow.opacity);
|
|
setDefaultValue(object, 'shadow.offsetX', object.shadow.offsetX);
|
|
setDefaultValue(object, 'shadow.offsetY', object.shadow.offsetY);
|
|
setDefaultValue(object, 'shadow.blur', object.shadow.blur);
|
|
keyframes = $.grep(keyframes, function (e) {
|
|
return e.id != object.get('id') || e.name != 'shadow.opacity';
|
|
});
|
|
objects.find((x) => x.id == object.get('id')).animate = $.grep(
|
|
objects.find((x) => x.id == object.get('id')).animate,
|
|
function (e) {
|
|
return e != 'shadow.opacity';
|
|
}
|
|
);
|
|
keyframes = $.grep(keyframes, function (e) {
|
|
return e.id != object.get('id') || e.name != 'shadow.offsetX';
|
|
});
|
|
objects.find((x) => x.id == object.get('id')).animate = $.grep(
|
|
objects.find((x) => x.id == object.get('id')).animate,
|
|
function (e) {
|
|
return e != 'shadow.offsetX';
|
|
}
|
|
);
|
|
keyframes = $.grep(keyframes, function (e) {
|
|
return e.id != object.get('id') || e.name != 'shadow.offsetY';
|
|
});
|
|
objects.find((x) => x.id == object.get('id')).animate = $.grep(
|
|
objects.find((x) => x.id == object.get('id')).animate,
|
|
function (e) {
|
|
return e != 'shadow.offsetY';
|
|
}
|
|
);
|
|
keyframes = $.grep(keyframes, function (e) {
|
|
return e.id != object.get('id') || e.name != 'shadow.blur';
|
|
});
|
|
objects.find((x) => x.id == object.get('id')).animate = $.grep(
|
|
objects.find((x) => x.id == object.get('id')).animate,
|
|
function (e) {
|
|
return e != 'shadow.blur';
|
|
}
|
|
);
|
|
} else if (prop == 'charSpacing') {
|
|
setDefaultValue(object, 'lineHeight', object.get('lineHeight'));
|
|
objects.find((x) => x.id == object.get('id')).animate = $.grep(
|
|
objects.find((x) => x.id == object.get('id')).animate,
|
|
function (e) {
|
|
return e != 'lineHeight';
|
|
}
|
|
);
|
|
keyframes = $.grep(keyframes, function (e) {
|
|
return e.id != object.get('id') || e.name != 'lineHeight';
|
|
});
|
|
}
|
|
|
|
keyframes = $.grep(keyframes, function (e) {
|
|
return e.id != object.get('id') || e.name != prop;
|
|
});
|
|
|
|
// Exception
|
|
if (prop != 'left' && prop != 'shadow.color') {
|
|
setDefaultValue(object, prop, object.get(prop));
|
|
}
|
|
|
|
$(
|
|
".keyframe[data-object='" +
|
|
object.get('id') +
|
|
"'][data-property='" +
|
|
prop +
|
|
"']"
|
|
).remove();
|
|
}
|
|
|
|
// Toggle animation mode for property
|
|
function toggleAnimateProp(e) {
|
|
e.stopPropagation();
|
|
var prop = $(this).parent().attr('data-property');
|
|
const object = canvas.getItemById(
|
|
$(this).parent().parent().parent().attr('data-object')
|
|
);
|
|
if (prop == 'position') {
|
|
prop = 'left';
|
|
} else if (prop == 'scale') {
|
|
prop = 'scaleX';
|
|
} else if (prop == 'stroke') {
|
|
prop = 'strokeWidth';
|
|
} else if (prop == 'shadow') {
|
|
prop = 'shadow.color';
|
|
} else if (prop == 'text') {
|
|
prop = 'charSpacing';
|
|
}
|
|
|
|
// Check layer global "freezing" state
|
|
if (
|
|
$(this)
|
|
.parent()
|
|
.parent()
|
|
.parent()
|
|
.find('.freeze')
|
|
.hasClass('frozen')
|
|
) {
|
|
objects.find((x) => x.id == object.get('id')).animate = [];
|
|
$(this)
|
|
.parent()
|
|
.parent()
|
|
.parent()
|
|
.find('.freeze')
|
|
.removeClass('frozen');
|
|
|
|
// Stop animating all props except selected
|
|
var propmatch = [
|
|
prop,
|
|
'scaleY',
|
|
'width',
|
|
'height',
|
|
'top',
|
|
'stroke',
|
|
'shadow.opacity',
|
|
'shadow.offsetX',
|
|
'shadow.offsetY',
|
|
'shadow.blur',
|
|
'lineHeight',
|
|
];
|
|
props.forEach(function (p) {
|
|
if ($.inArray(p, propmatch) == -1) {
|
|
if (
|
|
(object.get('type') == 'textbox' && p == 'charSpacing') ||
|
|
p == 'lineHeight'
|
|
) {
|
|
freezeProp(p, object);
|
|
} else if (p != 'charSpacing' && p != 'lineHeight') {
|
|
freezeProp(p, object);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Turn off clock -> Stop animating
|
|
if ($(this).hasClass('frozen')) {
|
|
$(this).removeClass('frozen');
|
|
$(this).attr('src', 'assets/freeze.svg');
|
|
freezeProp(prop, object);
|
|
// Turn on clock -> Animate
|
|
} else {
|
|
$(this).addClass('frozen');
|
|
$(this).attr('src', 'assets/frozen.svg');
|
|
animateProp(prop, object);
|
|
}
|
|
save();
|
|
}
|
|
$(document).on('click', '.freeze-prop', toggleAnimateProp);
|
|
|
|
// Lock layer
|
|
function lockLayer(e) {
|
|
e.stopPropagation();
|
|
const object = canvas.getItemById(
|
|
$(this).parent().parent().parent().attr('data-object')
|
|
);
|
|
if ($(this).hasClass('locked')) {
|
|
$(this).removeClass('locked');
|
|
$(this).attr('src', 'assets/lock.svg');
|
|
object.selectable = true;
|
|
$(this)
|
|
.parent()
|
|
.parent()
|
|
.find('.layer-handle')
|
|
.attr('draggable', true);
|
|
} else {
|
|
$(this).addClass('locked');
|
|
$(this).attr('src', 'assets/locked.svg');
|
|
object.selectable = false;
|
|
if (canvas.getActiveObject() == object) {
|
|
canvas.discardActiveObject();
|
|
canvas.renderAll();
|
|
}
|
|
$(this)
|
|
.parent()
|
|
.parent()
|
|
.find('.layer-handle')
|
|
.attr('draggable', false);
|
|
}
|
|
save();
|
|
}
|
|
$(document).on('click', '.lock', lockLayer);
|
|
|
|
// Center an object in the canvas
|
|
function centerObject(object) {
|
|
object.set('top', artboard.get('top') + artboard.get('height') / 2);
|
|
object.set(
|
|
'left',
|
|
artboard.get('left') + artboard.get('width') / 2
|
|
);
|
|
canvas.renderAll();
|
|
save();
|
|
}
|
|
|
|
// Render a layer
|
|
function renderLayer(object, animate = false) {
|
|
const entry = objects.find((x) => x.id == object.get('id'));
|
|
const p_keyframe = p_keyframes.find(
|
|
(x) => x.id == object.get('id')
|
|
);
|
|
if (!entry || !p_keyframe) {
|
|
return;
|
|
}
|
|
$('#nolayers').addClass('yaylayers');
|
|
const color = entry.color;
|
|
var src = '';
|
|
var classlock = '';
|
|
var srclock = 'lock';
|
|
var freeze = 'freeze';
|
|
if (object.get('type') == 'textbox') {
|
|
src = 'assets/text.svg';
|
|
} else if (
|
|
object.get('type') == 'rect' ||
|
|
object.get('type') == 'group' ||
|
|
object.get('type') == 'circle' ||
|
|
object.get('type') == 'path'
|
|
) {
|
|
src = 'assets/star.svg';
|
|
if (object.get('assetType') == 'animatedText') {
|
|
src = 'assets/text.svg';
|
|
}
|
|
if (object.get('assetType') == 'audio') {
|
|
src = 'assets/audio.svg';
|
|
}
|
|
} else if (object.get('type') == 'image') {
|
|
if (
|
|
object.get('assetType') &&
|
|
object.get('assetType') == 'video'
|
|
) {
|
|
src = 'assets/video.svg';
|
|
} else {
|
|
src = 'assets/image.svg';
|
|
}
|
|
} else if (object.get('type') == 'lottie') {
|
|
src = 'assets/zappy.svg';
|
|
} else if (object.get('assetType') == 'audio') {
|
|
src = 'assets/audio.svg';
|
|
}
|
|
if (object.selectable == false) {
|
|
classlock = 'locked';
|
|
srclock = 'locked';
|
|
}
|
|
if (animate != false) {
|
|
freeze = 'frozen';
|
|
}
|
|
const leftoffset = p_keyframe.trimstart / timelinetime;
|
|
const width =
|
|
(p_keyframe.end - p_keyframe.trimstart) / timelinetime;
|
|
$('#inner-timeline').prepend(
|
|
"<div class='object-props' id='" +
|
|
object.get('id') +
|
|
"' style='width:" +
|
|
(p_keyframe.end - p_keyframe.start) / timelinetime +
|
|
"px'><div class='row main-row'><div class='row-el' style='background-color:" +
|
|
color +
|
|
"'><div class='trim-row' style='left:" +
|
|
leftoffset +
|
|
'px;width:' +
|
|
width +
|
|
"px'></div></div></div></div>"
|
|
);
|
|
if (object.get('assetType') == 'audio') {
|
|
object.setControlsVisibility({
|
|
mt: false,
|
|
mb: false,
|
|
ml: false,
|
|
mr: false,
|
|
});
|
|
$('#layer-inner-list').prepend(
|
|
"<div class='layer' data-object='" +
|
|
object.get('id') +
|
|
"'><div class='layer-name'><div class='layer-handle' title='Drag to reorder'></div><img class='droparrow' src='assets/drop-arrow.svg' ><img class='layer-icon' src=" +
|
|
src +
|
|
"><input class='layer-custom-name' value='" +
|
|
entry.label +
|
|
"' readonly></span><div class='layer-options'><img class='" +
|
|
freeze +
|
|
"' src='assets/" +
|
|
freeze +
|
|
".svg' title='Toggle animation'></div></div><div class='properties'></div></div>"
|
|
);
|
|
} else {
|
|
$('#layer-inner-list').prepend(
|
|
"<div class='layer' data-object='" +
|
|
object.get('id') +
|
|
"'><div class='layer-name'><div class='layer-handle' title='Drag to reorder'></div><img class='droparrow' src='assets/drop-arrow.svg' ><img class='layer-icon' src=" +
|
|
src +
|
|
"><input class='layer-custom-name' value='" +
|
|
entry.label +
|
|
"' readonly></span><div class='layer-options'><img class='lock " +
|
|
classlock +
|
|
"' src='assets/" +
|
|
srclock +
|
|
".svg' title='Lock layer'><img class='" +
|
|
freeze +
|
|
"' src='assets/" +
|
|
freeze +
|
|
".svg' title='Toggle animation'></div></div><div class='properties'></div></div>"
|
|
);
|
|
}
|
|
$(".layer[data-object='" + object.get('id') + "']")
|
|
.find('.properties')
|
|
.toggle();
|
|
setTimelineZoom(timelinetime);
|
|
// Make the new layer draggable (html5sortable only wires up the
|
|
// children that exist when it is initialized)
|
|
if (typeof initLayerSortable == 'function') {
|
|
initLayerSortable();
|
|
}
|
|
if (object.selectable == false) {
|
|
$(".layer[data-object='" + object.get('id') + "']")
|
|
.find('.layer-handle')
|
|
.attr('draggable', false);
|
|
}
|
|
}
|
|
|
|
// Render a property
|
|
function renderProp(prop, object) {
|
|
var classfreeze = '';
|
|
var srcfreeze = 'freeze';
|
|
const entry = objects.find((x) => x.id == object.get('id'));
|
|
if (!entry) {
|
|
return;
|
|
}
|
|
if ($.inArray(prop, entry.animate) != -1) {
|
|
classfreeze = 'frozen';
|
|
srcfreeze = 'frozen';
|
|
}
|
|
if (prop == 'shadow.color') {
|
|
prop = 'shadowcolor';
|
|
}
|
|
$('#' + object.get('id')).append(
|
|
"<div class='row " +
|
|
prop +
|
|
" keyframe-row' data-object='" +
|
|
object.get('id') +
|
|
"'><div class='row-el'></div></div>"
|
|
);
|
|
if (prop == 'left') {
|
|
$(".layer[data-object='" + object.get('id') + "']")
|
|
.find('.properties')
|
|
.append(
|
|
"<div class='property-name' data-property='position'><span class='property-keyframe' title='Create a new keyframe'></span>Position<img class='freeze-prop " +
|
|
classfreeze +
|
|
"' src='assets/" +
|
|
srcfreeze +
|
|
".svg' title='Toggle animation'></div>"
|
|
);
|
|
} else if (prop == 'scaleX') {
|
|
$(".layer[data-object='" + object.get('id') + "']")
|
|
.find('.properties')
|
|
.append(
|
|
"<div class='property-name' data-property='scale'><span class='property-keyframe' title='Create a new keyframe'></span>Scale<img class='freeze-prop " +
|
|
classfreeze +
|
|
"' src='assets/" +
|
|
srcfreeze +
|
|
".svg' title='Toggle animation'></div>"
|
|
);
|
|
} else if (prop == 'strokeWidth') {
|
|
$(".layer[data-object='" + object.get('id') + "']")
|
|
.find('.properties')
|
|
.append(
|
|
"<div class='property-name' data-property='stroke'><span class='property-keyframe' title='Create a new keyframe'></span>Stroke<img class='freeze-prop " +
|
|
classfreeze +
|
|
"' src='assets/" +
|
|
srcfreeze +
|
|
".svg' title='Toggle animation'></div>"
|
|
);
|
|
} else if (prop == 'shadowcolor') {
|
|
$(".layer[data-object='" + object.get('id') + "']")
|
|
.find('.properties')
|
|
.append(
|
|
"<div class='property-name' data-property='shadow'><span class='property-keyframe' title='Create a new keyframe'></span>Shadow<img class='freeze-prop " +
|
|
classfreeze +
|
|
"' src='assets/" +
|
|
srcfreeze +
|
|
".svg' title='Toggle animation'></div>"
|
|
);
|
|
} else if (prop == 'charSpacing') {
|
|
$(".layer[data-object='" + object.get('id') + "']")
|
|
.find('.properties')
|
|
.append(
|
|
"<div class='property-name' data-property='text'><span class='property-keyframe' title='Create a new keyframe'></span>Text<img class='freeze-prop " +
|
|
classfreeze +
|
|
"' src='assets/" +
|
|
srcfreeze +
|
|
".svg' title='Toggle animation'></div>"
|
|
);
|
|
} else {
|
|
$(".layer[data-object='" + object.get('id') + "']")
|
|
.find('.properties')
|
|
.append(
|
|
"<div class='property-name' data-property='" +
|
|
prop +
|
|
"'><span class='property-keyframe' title='Create a new keyframe'></span>" +
|
|
prop +
|
|
"<img class='freeze-prop " +
|
|
classfreeze +
|
|
"' src='assets/" +
|
|
srcfreeze +
|
|
".svg' title='Toggle animation'></div>"
|
|
);
|
|
}
|
|
$('#' + object.get('id'))
|
|
.find('.keyframe-row' + '.' + prop)
|
|
.toggle();
|
|
}
|
|
|
|
// Create a layer
|
|
function newLayer(object) {
|
|
layer_count++;
|
|
var color;
|
|
if (object.get('type') == 'image') {
|
|
if (
|
|
object.get('assetType') &&
|
|
object.get('assetType') == 'video'
|
|
) {
|
|
color = '#106CF6';
|
|
} else {
|
|
color = '#92F711';
|
|
}
|
|
} else if (object.get('type') == 'textbox') {
|
|
color = '#F7119B';
|
|
} else if (
|
|
object.get('type') == 'rect' ||
|
|
object.get('type') == 'group' ||
|
|
object.get('type') == 'circle' ||
|
|
object.get('type') == 'path'
|
|
) {
|
|
color = '#9211F7';
|
|
if (object.get('assetType') == 'animatedText') {
|
|
color = '#F7119B';
|
|
} else if (object.get('assetType') == 'audio') {
|
|
color = '#11C0F7';
|
|
}
|
|
} else if (object.get('type') == 'lottie') {
|
|
color = '#F1890E';
|
|
} else {
|
|
color = '#9211F7';
|
|
}
|
|
if (
|
|
(object.get('assetType') && object.get('assetType') == 'video') ||
|
|
object.get('type') == 'lottie' ||
|
|
object.get('assetType') == 'audio'
|
|
) {
|
|
objects.push({
|
|
object: object,
|
|
id: object.get('id'),
|
|
label: object.get('id'),
|
|
color: color,
|
|
defaults: [],
|
|
locked: [],
|
|
mask: 'none',
|
|
start: 0,
|
|
end: object.get('duration'),
|
|
});
|
|
// Clamp to the project end. This used to be `duration - currenttime`,
|
|
// which shortened every layer added after t=0.
|
|
const mediaend = Math.min(
|
|
object.get('duration') + currenttime,
|
|
duration
|
|
);
|
|
p_keyframes.push({
|
|
start: currenttime,
|
|
end: mediaend,
|
|
trimstart: 0,
|
|
trimend: mediaend,
|
|
object: object,
|
|
id: object.get('id'),
|
|
});
|
|
} else {
|
|
objects.push({
|
|
object: object,
|
|
id: object.get('id'),
|
|
label: object.get('id'),
|
|
color: color,
|
|
defaults: [],
|
|
locked: [],
|
|
mask: 'none',
|
|
});
|
|
p_keyframes.push({
|
|
start: object.get('notnew')
|
|
? object.get('starttime')
|
|
: currenttime,
|
|
end: duration,
|
|
trimstart: 0,
|
|
trimend: duration,
|
|
object: object,
|
|
id: object.get('id'),
|
|
});
|
|
}
|
|
renderLayer(object);
|
|
if (
|
|
!object.get('assetType') ||
|
|
object.get('assetType') != 'audio'
|
|
) {
|
|
props.forEach(function (prop) {
|
|
if (prop == 'lineHeight' || prop == 'charSpacing') {
|
|
if (object.get('type') == 'textbox') {
|
|
if (prop != 'lineHeight') {
|
|
renderProp(prop, object);
|
|
}
|
|
objects
|
|
.find((x) => x.id == object.id)
|
|
.defaults.push({ name: prop, value: object.get(prop) });
|
|
}
|
|
} else if (
|
|
prop == 'shadow.opacity' ||
|
|
prop == 'shadow.blur' ||
|
|
prop == 'shadow.offsetX' ||
|
|
prop == 'shadow.offsetY' ||
|
|
prop == 'shadow.color'
|
|
) {
|
|
if (object.get('type') != 'group') {
|
|
if (prop == 'shadow.color') {
|
|
renderProp(prop, object);
|
|
objects
|
|
.find((x) => x.id == object.id)
|
|
.defaults.push({
|
|
name: prop,
|
|
value: object.shadow.color,
|
|
});
|
|
} else if (prop == 'shadow.blur') {
|
|
objects
|
|
.find((x) => x.id == object.id)
|
|
.defaults.push({
|
|
name: prop,
|
|
value: object.shadow.blur,
|
|
});
|
|
} else if (prop == 'shadow.offsetX') {
|
|
objects
|
|
.find((x) => x.id == object.id)
|
|
.defaults.push({
|
|
name: prop,
|
|
value: object.shadow.offsetX,
|
|
});
|
|
} else if (prop == 'shadow.offsetY') {
|
|
objects
|
|
.find((x) => x.id == object.id)
|
|
.defaults.push({
|
|
name: prop,
|
|
value: object.shadow.offsetY,
|
|
});
|
|
} else if (prop == 'shadow.opacity') {
|
|
objects
|
|
.find((x) => x.id == object.id)
|
|
.defaults.push({
|
|
name: prop,
|
|
value: object.shadow.opacity,
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
if (
|
|
prop != 'top' &&
|
|
prop != 'scaleY' &&
|
|
prop != 'stroke' &&
|
|
prop != 'width' &&
|
|
prop != 'height'
|
|
) {
|
|
renderProp(prop, object);
|
|
}
|
|
objects
|
|
.find((x) => x.id == object.id)
|
|
.defaults.push({ name: prop, value: object.get(prop) });
|
|
}
|
|
});
|
|
} else {
|
|
renderProp('volume', object);
|
|
objects
|
|
.find((x) => x.id == object.id)
|
|
.defaults.push({ name: 'volume', value: 0 });
|
|
}
|
|
$('.layer-selected').removeClass('layer-selected');
|
|
$(".layer[data-object='" + object.get('id') + "']").addClass(
|
|
'layer-selected'
|
|
);
|
|
const selectedLayer =
|
|
document.getElementsByClassName('layer-selected')[0];
|
|
if (selectedLayer) {
|
|
selectedLayer.scrollIntoView();
|
|
}
|
|
objects.find((x) => x.id == object.id).animate = [];
|
|
animate(false, currenttime);
|
|
save();
|
|
checkFilter();
|
|
}
|
|
|
|
// Add a (complex) SVG shape to the canvas
|
|
function newSVG(svg, x, y, width, center) {
|
|
var svggroup = [];
|
|
fabric.loadSVGFromURL(svg, function (objects, options) {
|
|
var newsvg = objects[0];
|
|
if (objects.length > 1) {
|
|
newsvg = fabric.util.groupSVGElements(objects, options);
|
|
}
|
|
newsvg.set({
|
|
id: 'Shape' + layer_count,
|
|
stroke: '#000',
|
|
left: x,
|
|
top: y,
|
|
strokeWidth: 0,
|
|
strokeUniform: true,
|
|
originX: 'center',
|
|
originY: 'center',
|
|
strokeDashArray: false,
|
|
absolutePositioned: true,
|
|
paintFirst: 'stroke',
|
|
objectCaching: true,
|
|
sourcePath: svg,
|
|
inGroup: false,
|
|
shadow: {
|
|
color: '#000',
|
|
offsetX: 0,
|
|
offsetY: 0,
|
|
blur: 0,
|
|
opacity: 0,
|
|
},
|
|
});
|
|
newsvg.scaleToWidth(width);
|
|
newsvg.set({
|
|
scaleX: parseFloat(newsvg.get('scaleX').toFixed(2)),
|
|
scaleY: parseFloat(newsvg.get('scaleY').toFixed(2)),
|
|
});
|
|
canvas.add(newsvg);
|
|
newLayer(newsvg);
|
|
canvas.setActiveObject(newsvg);
|
|
canvas.bringToFront(newsvg);
|
|
canvas.renderAll();
|
|
if (center) {
|
|
newsvg.set(
|
|
'left',
|
|
artboard.get('left') + artboard.get('width') / 2
|
|
);
|
|
newsvg.set(
|
|
'top',
|
|
artboard.get('top') + artboard.get('height') / 2
|
|
);
|
|
canvas.renderAll();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add a video to the canvas
|
|
function newVideo(file, src, x, y, duration, center) {
|
|
var newvid = new fabric.Image(file, {
|
|
left: x,
|
|
top: y,
|
|
width: file.width,
|
|
height: file.height,
|
|
originX: 'center',
|
|
originY: 'center',
|
|
backgroundColor: 'rgba(255,255,255,0)',
|
|
cursorWidth: 1,
|
|
stroke: '#000',
|
|
strokeUniform: true,
|
|
paintFirst: 'stroke',
|
|
strokeWidth: 0,
|
|
cursorDuration: 1,
|
|
cursorDelay: 250,
|
|
source: src,
|
|
duration: duration * 1000,
|
|
assetType: 'video',
|
|
id: 'Video' + layer_count,
|
|
objectCaching: false,
|
|
strokeDashArray: false,
|
|
inGroup: false,
|
|
shadow: {
|
|
color: '#000',
|
|
offsetX: 0,
|
|
offsetY: 0,
|
|
blur: 0,
|
|
opacity: 0,
|
|
},
|
|
});
|
|
files.push({ name: newvid.get('id'), file: src });
|
|
newvid.saveElem = newvid.getElement();
|
|
canvas.add(newvid);
|
|
if (newvid.get('width') > artboard.get('width')) {
|
|
newvid.scaleToWidth(artboard.get('width'));
|
|
}
|
|
newvid.scaleToWidth(150);
|
|
canvas.renderAll();
|
|
if (window.duration < newvid.duration + currenttime) {
|
|
window.duration =
|
|
((newvid.duration + currenttime) / 1000).toFixed(2) * 1000;
|
|
}
|
|
newLayer(newvid);
|
|
canvas.setActiveObject(newvid);
|
|
canvas.bringToFront(newvid);
|
|
if (center) {
|
|
newvid.set(
|
|
'left',
|
|
artboard.get('left') + artboard.get('width') / 2
|
|
);
|
|
newvid.set(
|
|
'top',
|
|
artboard.get('top') + artboard.get('height') / 2
|
|
);
|
|
canvas.renderAll();
|
|
}
|
|
$('#load-video').removeClass('loading-active');
|
|
}
|
|
|
|
// Load a video
|
|
function loadVideo(src, x, y, center) {
|
|
var vidObj = document.createElement('video');
|
|
var vidSrc = document.createElement('source');
|
|
vidSrc.src = src;
|
|
vidObj.crossOrigin = 'anonymous';
|
|
vidObj.appendChild(vidSrc);
|
|
vidObj.addEventListener('error', function () {
|
|
console.error('Could not load video', src);
|
|
$('#load-video').removeClass('loading-active');
|
|
});
|
|
vidObj.addEventListener('loadeddata', function () {
|
|
vidObj.width = this.videoWidth;
|
|
vidObj.height = this.videoHeight;
|
|
vidObj.currentTime = 0;
|
|
vidObj.muted = false;
|
|
function waitLoad() {
|
|
if (vidObj.readyState >= 3) {
|
|
newVideo(vidObj, src, x, y, vidObj.duration, center);
|
|
} else {
|
|
window.setTimeout(function () {
|
|
waitLoad();
|
|
}, 100);
|
|
}
|
|
}
|
|
window.setTimeout(function () {
|
|
waitLoad();
|
|
}, 100);
|
|
});
|
|
vidObj.currentTime = 0;
|
|
}
|
|
|
|
// Check that crop controls are inside image
|
|
function checkCrop(obj) {
|
|
if (obj.isContainedWithinObject(cropobj)) {
|
|
croptop = obj.get('top');
|
|
cropleft = obj.get('left');
|
|
cropscalex = obj.get('scaleX');
|
|
cropscaley = obj.get('scaleY');
|
|
} else {
|
|
obj.top = croptop;
|
|
obj.left = cropleft;
|
|
obj.scaleX = cropscalex;
|
|
obj.scaleY = cropscaley;
|
|
obj.setCoords();
|
|
obj.saveState();
|
|
}
|
|
obj.set({
|
|
borderColor: '#51B9F9',
|
|
});
|
|
canvas.renderAll();
|
|
crop(canvas.getItemById('cropped'));
|
|
}
|
|
|
|
// Rotate a vector by `radians` around the origin
|
|
function rotateVector(x, y, radians) {
|
|
if (!radians) {
|
|
return { x: x, y: y };
|
|
}
|
|
var cos = Math.cos(radians);
|
|
var sin = Math.sin(radians);
|
|
return { x: x * cos - y * sin, y: x * sin + y * cos };
|
|
}
|
|
|
|
// Perform a crop
|
|
//
|
|
// Everything is computed in the image's own frame rather than along the canvas
|
|
// axes, so a rotated image crops the region the crop window actually covers.
|
|
// The previous version compared canvas-space edges and only handled three of
|
|
// the four quadrants, which cropped the wrong area of any rotated image (and
|
|
// nothing at all when the crop window was centred on it).
|
|
function crop(obj) {
|
|
var cropUI = canvas.getItemById('crop');
|
|
if (!obj || !cropUI || !cropobj) {
|
|
return;
|
|
}
|
|
cropobj.setCoords();
|
|
cropUI.setCoords();
|
|
// Read everything before writing: the final call passes cropobj as `obj`
|
|
var scaleX = cropobj.get('scaleX') || 1;
|
|
var scaleY = cropobj.get('scaleY') || 1;
|
|
var ogWidth = cropobj.get('width');
|
|
var ogHeight = cropobj.get('height');
|
|
var centerX = cropobj.get('left');
|
|
var centerY = cropobj.get('top');
|
|
var radians = fabric.util.degreesToRadians(
|
|
cropobj.get('angle') || 0
|
|
);
|
|
|
|
// The crop window carries the image's angle, so its own width and height are
|
|
// already axis-aligned with the image
|
|
var width = (cropUI.get('width') * cropUI.get('scaleX')) / scaleX;
|
|
var height = (cropUI.get('height') * cropUI.get('scaleY')) / scaleY;
|
|
|
|
// Where the crop window sits relative to the image centre, un-rotated
|
|
var offset = rotateVector(
|
|
cropUI.get('left') - centerX,
|
|
cropUI.get('top') - centerY,
|
|
-radians
|
|
);
|
|
var cropX = offset.x / scaleX + ogWidth / 2 - width / 2;
|
|
var cropY = offset.y / scaleY + ogHeight / 2 - height / 2;
|
|
|
|
// Never leave the source bitmap: fabric draws nothing for a negative crop
|
|
width = Math.max(1, Math.min(width, ogWidth));
|
|
height = Math.max(1, Math.min(height, ogHeight));
|
|
cropX = Math.max(0, Math.min(cropX, ogWidth - width));
|
|
cropY = Math.max(0, Math.min(cropY, ogHeight - height));
|
|
|
|
// Put the cropped object back under the (clamped) region it represents
|
|
var center = rotateVector(
|
|
(cropX + width / 2 - ogWidth / 2) * scaleX,
|
|
(cropY + height / 2 - ogHeight / 2) * scaleY,
|
|
radians
|
|
);
|
|
obj
|
|
.set({
|
|
cropX: cropX,
|
|
cropY: cropY,
|
|
width: width,
|
|
height: height,
|
|
left: centerX + center.x,
|
|
top: centerY + center.y,
|
|
})
|
|
.setCoords();
|
|
canvas.renderAll();
|
|
if (obj.get('id') != 'cropped') {
|
|
canvas.remove(cropUI);
|
|
canvas.remove(canvas.getItemById('crop-overlay'));
|
|
canvas.remove(canvas.getItemById('cropped'));
|
|
cropping = false;
|
|
resetControls();
|
|
canvas.uniformScaling = true;
|
|
canvas.renderAll();
|
|
newKeyframe('scaleX', obj, currenttime, obj.get('scaleX'), true);
|
|
newKeyframe('scaleY', obj, currenttime, obj.get('scaleY'), true);
|
|
newKeyframe('width', obj, currenttime, obj.get('width'), true);
|
|
newKeyframe('height', obj, currenttime, obj.get('height'), true);
|
|
newKeyframe('left', obj, currenttime, obj.get('left'), true);
|
|
newKeyframe('top', obj, currenttime, obj.get('top'), true);
|
|
$('#properties-overlay').removeClass('properties-disabled');
|
|
save();
|
|
}
|
|
canvas.renderAll();
|
|
}
|
|
|
|
var tlcrop = new Image();
|
|
tlcrop.src = 'assets/tlcrop.svg';
|
|
var trcrop = new Image();
|
|
trcrop.src = 'assets/trcrop.svg';
|
|
var blcrop = new Image();
|
|
blcrop.src = 'assets/blcrop.svg';
|
|
var brcrop = new Image();
|
|
brcrop.src = 'assets/brcrop.svg';
|
|
|
|
function overlay() {
|
|
canvas.add(
|
|
new fabric.Rect({
|
|
left: artboard.left,
|
|
top: artboard.top,
|
|
originX: 'left',
|
|
originY: 'top',
|
|
width: artboard.width,
|
|
height: artboard.height,
|
|
fill: 'rgba(0,0,0,0.5)',
|
|
selectable: false,
|
|
// Not "overlay": that is the artboard's id
|
|
id: 'crop-overlay',
|
|
})
|
|
);
|
|
}
|
|
|
|
// Start cropping an image
|
|
function cropImage(object) {
|
|
if (!cropping) {
|
|
$('#properties-overlay').addClass('properties-disabled');
|
|
cropping = true;
|
|
cropobj = object;
|
|
canvas.uniformScaling = false;
|
|
cropobj.setCoords();
|
|
// The visible region, in the source bitmap's own pixels, before the image
|
|
// is expanded back to its full size below
|
|
var cropx = cropobj.get('cropX');
|
|
var cropy = cropobj.get('cropY');
|
|
var shownWidth = cropobj.get('width');
|
|
var shownHeight = cropobj.get('height');
|
|
var shownLeft = cropobj.get('left');
|
|
var shownTop = cropobj.get('top');
|
|
overlay();
|
|
var cropUI = new fabric.Rect({
|
|
left: object.get('left'),
|
|
top: object.get('top'),
|
|
width: object.get('width') * object.get('scaleX') - 5,
|
|
height: object.get('height') * object.get('scaleY') - 5,
|
|
// The crop window has to turn with the image, otherwise the region it
|
|
// covers cannot be expressed as a crop at all
|
|
angle: object.get('angle') || 0,
|
|
originX: 'center',
|
|
originY: 'center',
|
|
id: 'crop',
|
|
fill: 'rgba(0,0,0,0)',
|
|
shadow: {
|
|
color: 'black',
|
|
offsetX: 0,
|
|
offsetY: 0,
|
|
blur: 0,
|
|
opacity: 0,
|
|
},
|
|
});
|
|
cropobj.clone(function (cloned) {
|
|
cloned.set({
|
|
id: 'cropped',
|
|
selectable: false,
|
|
originX: 'center',
|
|
originY: 'center',
|
|
});
|
|
canvas.add(cloned);
|
|
canvas.bringToFront(cloned);
|
|
canvas.bringToFront(cropUI);
|
|
canvas.renderAll();
|
|
cropobj = object;
|
|
});
|
|
cropobj
|
|
.set({
|
|
cropX: 0,
|
|
cropY: 0,
|
|
width: cropobj.get('ogWidth'),
|
|
height: cropobj.get('ogHeight'),
|
|
})
|
|
.setCoords();
|
|
canvas.renderAll();
|
|
// Growing back to the full bitmap moves the centre, so shift the image so
|
|
// the region that was on screen stays exactly where it was. The offset is
|
|
// in the image's own frame, hence the rotation.
|
|
var shift = rotateVector(
|
|
(cropx + shownWidth / 2 - cropobj.get('width') / 2) *
|
|
cropobj.get('scaleX'),
|
|
(cropy + shownHeight / 2 - cropobj.get('height') / 2) *
|
|
cropobj.get('scaleY'),
|
|
fabric.util.degreesToRadians(cropobj.get('angle') || 0)
|
|
);
|
|
cropobj.set({
|
|
left: shownLeft - shift.x,
|
|
top: shownTop - shift.y,
|
|
});
|
|
cropUI.setControlsVisibility({
|
|
mt: false,
|
|
mb: false,
|
|
mr: false,
|
|
ml: false,
|
|
mtr: false,
|
|
});
|
|
cropUI.controls.tl = new fabric.Control({
|
|
x: -0.5,
|
|
y: -0.5,
|
|
offsetX: 3,
|
|
offsetY: 3,
|
|
cursorStyleHandler:
|
|
fabric.controlsUtils.scaleCursorStyleHandler,
|
|
actionHandler: fabric.controlsUtils.scalingEqually,
|
|
render: function (ctx, left, top, styleOverride, fabricObject) {
|
|
const wsize = 27;
|
|
const hsize = 27;
|
|
ctx.save();
|
|
ctx.translate(left, top);
|
|
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
|
|
ctx.drawImage(tlcrop, -wsize / 2, -hsize / 2, wsize, hsize);
|
|
ctx.restore();
|
|
},
|
|
});
|
|
cropUI.controls.tr = new fabric.Control({
|
|
x: 0.5,
|
|
y: -0.5,
|
|
offsetX: -3,
|
|
offsetY: 3,
|
|
cursorStyleHandler:
|
|
fabric.controlsUtils.scaleCursorStyleHandler,
|
|
actionHandler: fabric.controlsUtils.scalingEqually,
|
|
render: function (ctx, left, top, styleOverride, fabricObject) {
|
|
const wsize = 27;
|
|
const hsize = 27;
|
|
ctx.save();
|
|
ctx.translate(left, top);
|
|
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
|
|
ctx.drawImage(trcrop, -wsize / 2, -hsize / 2, wsize, hsize);
|
|
ctx.restore();
|
|
},
|
|
});
|
|
cropUI.controls.bl = new fabric.Control({
|
|
x: -0.5,
|
|
y: 0.5,
|
|
offsetX: 3,
|
|
offsetY: -3,
|
|
cursorStyleHandler:
|
|
fabric.controlsUtils.scaleCursorStyleHandler,
|
|
actionHandler: fabric.controlsUtils.scalingEqually,
|
|
render: function (ctx, left, top, styleOverride, fabricObject) {
|
|
const wsize = 27;
|
|
const hsize = 27;
|
|
ctx.save();
|
|
ctx.translate(left, top);
|
|
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
|
|
ctx.drawImage(blcrop, -wsize / 2, -hsize / 2, wsize, hsize);
|
|
ctx.restore();
|
|
},
|
|
});
|
|
cropUI.controls.br = new fabric.Control({
|
|
x: 0.5,
|
|
y: 0.5,
|
|
offsetX: -3,
|
|
offsetY: -3,
|
|
cursorStyleHandler:
|
|
fabric.controlsUtils.scaleCursorStyleHandler,
|
|
actionHandler: fabric.controlsUtils.scalingEqually,
|
|
render: function (ctx, left, top, styleOverride, fabricObject) {
|
|
const wsize = 27;
|
|
const hsize = 27;
|
|
ctx.save();
|
|
ctx.translate(left, top);
|
|
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
|
|
ctx.drawImage(brcrop, -wsize / 2, -hsize / 2, wsize, hsize);
|
|
ctx.restore();
|
|
},
|
|
});
|
|
canvas.add(cropUI);
|
|
canvas.setActiveObject(cropUI);
|
|
canvas.renderAll();
|
|
cropleft = cropUI.get('left');
|
|
croptop = cropUI.get('top');
|
|
cropscalex = cropUI.get('scaleX') - 0.03;
|
|
cropscaley = cropUI.get('scaleY') - 0.03;
|
|
}
|
|
}
|
|
$(document).on('click', '#crop-image', function () {
|
|
if (canvas.getActiveObject()) {
|
|
cropImage(canvas.getActiveObject());
|
|
}
|
|
});
|
|
|
|
// Add an image to the canvas
|
|
function newImage(file, x, y, width, center) {
|
|
var newimg = new fabric.Image(file, {
|
|
left: x,
|
|
top: y,
|
|
originX: 'center',
|
|
originY: 'center',
|
|
stroke: '#000',
|
|
strokeUniform: true,
|
|
strokeWidth: 0,
|
|
paintFirst: 'stroke',
|
|
absolutePositioned: true,
|
|
id: 'Image' + layer_count,
|
|
inGroup: false,
|
|
strokeDashArray: false,
|
|
objectCaching: true,
|
|
shadow: {
|
|
color: 'black',
|
|
offsetX: 0,
|
|
offsetY: 0,
|
|
blur: 0,
|
|
opacity: 0,
|
|
},
|
|
});
|
|
files.push({ name: newimg.get('id'), file: file.src });
|
|
canvas.add(newimg);
|
|
newimg.scaleToWidth(width);
|
|
newimg.set({
|
|
scaleX: parseFloat(newimg.get('scaleX').toFixed(2)),
|
|
scaleY: parseFloat(newimg.get('scaleY').toFixed(2)),
|
|
ogWidth: newimg.get('width'),
|
|
ogHeight: newimg.get('height'),
|
|
});
|
|
canvas.bringToFront(newimg);
|
|
canvas.renderAll();
|
|
newLayer(newimg);
|
|
canvas.setActiveObject(newimg);
|
|
if (center) {
|
|
newimg.set(
|
|
'left',
|
|
artboard.get('left') + artboard.get('width') / 2
|
|
);
|
|
newimg.set(
|
|
'top',
|
|
artboard.get('top') + artboard.get('height') / 2
|
|
);
|
|
canvas.renderAll();
|
|
}
|
|
$('#load-image').removeClass('loading-active');
|
|
}
|
|
|
|
function loadImage(src, x, y, width, center) {
|
|
var image = new Image();
|
|
image.onload = function (img) {
|
|
newImage(image, x, y, width, center);
|
|
};
|
|
image.onerror = function () {
|
|
console.error('Could not load image', src);
|
|
$('#load-image').removeClass('loading-active');
|
|
};
|
|
image.src = src;
|
|
}
|
|
|
|
function createVideoThumbnail(file, max, seekTo = 0.0, isURL) {
|
|
return new Promise((resolve, reject) => {
|
|
const videoPlayer = document.createElement('video');
|
|
if (isURL) {
|
|
videoPlayer.setAttribute('src', file);
|
|
} else {
|
|
videoPlayer.setAttribute('src', URL.createObjectURL(file));
|
|
}
|
|
videoPlayer.setAttribute('crossorigin', 'anonymous');
|
|
videoPlayer.load();
|
|
videoPlayer.addEventListener('error', (ex) => {
|
|
reject('error when loading video file', ex);
|
|
});
|
|
videoPlayer.addEventListener('loadedmetadata', () => {
|
|
if (videoPlayer.duration < seekTo) {
|
|
reject('video is too short.');
|
|
return;
|
|
}
|
|
setTimeout(() => {
|
|
videoPlayer.currentTime = seekTo;
|
|
}, 200);
|
|
videoPlayer.addEventListener('seeked', () => {
|
|
var oc = document.createElement('canvas');
|
|
var octx = oc.getContext('2d');
|
|
if (videoPlayer.videoWidth > videoPlayer.videoHeight) {
|
|
oc.width = max;
|
|
oc.height =
|
|
(videoPlayer.videoHeight / videoPlayer.videoWidth) * max;
|
|
} else {
|
|
oc.height = max;
|
|
oc.width =
|
|
(videoPlayer.videoWidth / videoPlayer.videoHeight) * max;
|
|
}
|
|
octx.drawImage(videoPlayer, 0, 0, oc.width, oc.height);
|
|
resolve(oc.toDataURL());
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function createThumbnail(file, max) {
|
|
return new Promise(function (resolve, reject) {
|
|
var reader = new FileReader();
|
|
reader.onload = function (event) {
|
|
var img = new Image();
|
|
img.onload = function () {
|
|
if (img.width > max) {
|
|
var oc = document.createElement('canvas');
|
|
var octx = oc.getContext('2d');
|
|
if (img.width > img.height) {
|
|
oc.width = max;
|
|
oc.height = (img.height / img.width) * max;
|
|
} else {
|
|
oc.height = max;
|
|
oc.width = (img.width / img.height) * max;
|
|
}
|
|
octx.drawImage(img, 0, 0, oc.width, oc.height);
|
|
resolve(oc.toDataURL());
|
|
} else {
|
|
resolve(img.src);
|
|
}
|
|
};
|
|
img.src = event.target.result;
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
function dataURItoBlob(dataURI) {
|
|
var byteString = atob(dataURI.split(',')[1]);
|
|
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
|
|
var ab = new ArrayBuffer(byteString.length);
|
|
var ia = new Uint8Array(ab);
|
|
for (var i = 0; i < byteString.length; i++) {
|
|
ia[i] = byteString.charCodeAt(i);
|
|
}
|
|
var blob = new Blob([ab], { type: mimeString });
|
|
return blob;
|
|
}
|
|
|
|
async function uploadFromURL() {
|
|
var url = $('#upload-link-input').val();
|
|
let file = await fetch(url).then((r) => r.blob());
|
|
if (file.type.split('/')[0] === 'image') {
|
|
$('#upload-link-input').val('');
|
|
$('.upload-show').removeClass('upload-show');
|
|
createThumbnail(file, 250).then(function (data) {
|
|
saveFile(
|
|
dataURItoBlob(data),
|
|
file,
|
|
file.type.split('/')[0],
|
|
'temp',
|
|
false,
|
|
false
|
|
);
|
|
});
|
|
} else if (file.type.split('/')[0] === 'video') {
|
|
$('.upload-show').removeClass('upload-show');
|
|
createVideoThumbnail(file, 250, 0, false).then(function (data) {
|
|
saveFile(
|
|
dataURItoBlob(data),
|
|
file,
|
|
file.type.split('/')[0],
|
|
'temp',
|
|
false,
|
|
false
|
|
);
|
|
});
|
|
$('#upload-link-input').val('');
|
|
} else {
|
|
alert('File type not accepted');
|
|
}
|
|
}
|
|
$(document).on('click', '#upload-link-add', uploadFromURL);
|
|
|
|
function handleUpload(custom = false) {
|
|
var files2;
|
|
if (custom == false) {
|
|
files2 = $('#filepick').get(0).files;
|
|
} else {
|
|
files2 = custom.originalEvent.dataTransfer.files;
|
|
}
|
|
if (files2) {
|
|
Array.from(files2).forEach((file) => {
|
|
uploading = true;
|
|
if (file.size / 1024 / 1024 <= 10) {
|
|
$('#upload-button').html(
|
|
"<img src='assets/upload.svg'> Uploading..."
|
|
);
|
|
$('#upload-button').addClass('uploading');
|
|
if (file['type'].split('/')[0] === 'image') {
|
|
$('.upload-show').removeClass('upload-show');
|
|
createThumbnail(file, 250).then(function (data) {
|
|
saveFile(
|
|
dataURItoBlob(data),
|
|
file,
|
|
file['type'].split('/')[0],
|
|
'temp',
|
|
false,
|
|
false
|
|
);
|
|
});
|
|
} else if (file['type'].split('/')[0] === 'video') {
|
|
$('.upload-show').removeClass('upload-show');
|
|
createVideoThumbnail(file, 250, 0, false).then(function (
|
|
data
|
|
) {
|
|
saveFile(
|
|
dataURItoBlob(data),
|
|
file,
|
|
file['type'].split('/')[0],
|
|
'temp',
|
|
false,
|
|
false
|
|
);
|
|
});
|
|
} else {
|
|
alert('File type not accepted');
|
|
}
|
|
} else {
|
|
alert('File is too big');
|
|
}
|
|
});
|
|
if (files2.length == 1) {
|
|
if (files2[0]['type'].split('/')[0] === 'image') {
|
|
$('.upload-tab-active').removeClass('upload-tab-active');
|
|
$('#images-tab').addClass('upload-tab-active');
|
|
} else if (files2[0]['type'].split('/')[0] === 'video') {
|
|
$('.upload-tab-active').removeClass('upload-tab-active');
|
|
$('#videos-tab').addClass('upload-tab-active');
|
|
} else if (files2[0]['type'].split('/')[0] === 'audio') {
|
|
$('.upload-tab-active').removeClass('upload-tab-active');
|
|
$('#audio-tab').addClass('upload-tab-active');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$(document).on('change', '#filepick', function () {
|
|
handleUpload(false);
|
|
});
|
|
|
|
// Upload audio
|
|
function audioUpload() {
|
|
const files = $('#filepick2').get(0).files;
|
|
if (files) {
|
|
if (files.length == 1) {
|
|
if (files[0]['type'].split('/')[0] === 'audio') {
|
|
if (files[0].size / 1024 / 1024 <= 10) {
|
|
$('#audio-upload-button').html('Uploading...');
|
|
$('#audio-upload-button').addClass('uploading');
|
|
saveAudio(files[0]);
|
|
} else {
|
|
alert('File is too big');
|
|
}
|
|
} else {
|
|
alert('Wrong file type');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$(document).on('change', '#filepick2', audioUpload);
|
|
|
|
// Create a rectangle
|
|
function newRectangle(color) {
|
|
var newrect = new fabric.Rect({
|
|
left: 0,
|
|
top: 0,
|
|
originX: 'center',
|
|
originY: 'center',
|
|
width: 200,
|
|
height: 200,
|
|
stroke: '#000',
|
|
strokeWidth: 0,
|
|
strokeUniform: true,
|
|
backgroundColor: 'rgba(255,255,255,0)',
|
|
rx: 0,
|
|
ry: 0,
|
|
cornerRadius: 0,
|
|
fill: color,
|
|
cursorWidth: 1,
|
|
cursorDuration: 1,
|
|
paintFirst: 'stroke',
|
|
cursorDelay: 250,
|
|
strokeDashArray: false,
|
|
inGroup: false,
|
|
id: 'Shape' + layer_count,
|
|
shadow: {
|
|
color: '#000',
|
|
offsetX: 0,
|
|
offsetY: 0,
|
|
blur: 0,
|
|
opacity: 0,
|
|
},
|
|
});
|
|
canvas.add(newrect);
|
|
newLayer(newrect);
|
|
canvas.setActiveObject(newrect);
|
|
canvas.bringToFront(newrect);
|
|
canvas.renderAll();
|
|
}
|
|
|
|
// Change text format
|
|
function formatText() {
|
|
var isselected = false;
|
|
if (!canvas.getActiveObject().isEditing) {
|
|
canvas.getActiveObject().enterEditing();
|
|
canvas.getActiveObject().selectAll();
|
|
isselected = true;
|
|
}
|
|
if ($(this).hasClass('format-text-active')) {
|
|
if ($(this).attr('id') == 'format-bold') {
|
|
$(this).find('img').attr('src', 'assets/bold.svg');
|
|
canvas
|
|
.getActiveObject()
|
|
.setSelectionStyles({ fontWeight: 'normal' });
|
|
} else if ($(this).attr('id') == 'format-italic') {
|
|
$(this).find('img').attr('src', 'assets/italic.svg');
|
|
canvas
|
|
.getActiveObject()
|
|
.setSelectionStyles({ fontStyle: 'normal' });
|
|
} else if ($(this).attr('id') == 'format-underline') {
|
|
$(this).find('img').attr('src', 'assets/underline.svg');
|
|
canvas
|
|
.getActiveObject()
|
|
.setSelectionStyles({ underline: false });
|
|
} else {
|
|
$(this).find('img').attr('src', 'assets/strike.svg');
|
|
canvas
|
|
.getActiveObject()
|
|
.setSelectionStyles({ linethrough: false });
|
|
}
|
|
$(this).removeClass('format-text-active');
|
|
} else {
|
|
$(this).addClass('format-text-active');
|
|
if ($(this).attr('id') == 'format-bold') {
|
|
$(this).find('img').attr('src', 'assets/bold-active.svg');
|
|
canvas
|
|
.getActiveObject()
|
|
.setSelectionStyles({ fontWeight: 'bold' });
|
|
} else if ($(this).attr('id') == 'format-italic') {
|
|
$(this).find('img').attr('src', 'assets/italic-active.svg');
|
|
canvas
|
|
.getActiveObject()
|
|
.setSelectionStyles({ fontStyle: 'italic' });
|
|
} else if ($(this).attr('id') == 'format-underline') {
|
|
$(this).find('img').attr('src', 'assets/underline-active.svg');
|
|
canvas
|
|
.getActiveObject()
|
|
.setSelectionStyles({ underline: true });
|
|
} else {
|
|
$(this).find('img').attr('src', 'assets/strike-active.svg');
|
|
canvas
|
|
.getActiveObject()
|
|
.setSelectionStyles({ linethrough: true });
|
|
}
|
|
}
|
|
if (isselected) {
|
|
canvas.getActiveObject().exitEditing();
|
|
}
|
|
canvas.renderAll();
|
|
save();
|
|
}
|
|
$(document).on('click', '.format-text', formatText);
|
|
|
|
// Change stroke type (e.g. dashed), ignore naming (used to be for line join)
|
|
function lineJoin() {
|
|
if ($('.line-join-active').attr('id') == 'miter') {
|
|
$('.line-join-active')
|
|
.find('img')
|
|
.attr('src', 'assets/miter.svg');
|
|
} else if ($('.line-join-active').attr('id') == 'bevel') {
|
|
$('.line-join-active')
|
|
.find('img')
|
|
.attr('src', 'assets/bevel.svg');
|
|
} else if ($('.line-join-active').attr('id') == 'round') {
|
|
$('.line-join-active')
|
|
.find('img')
|
|
.attr('src', 'assets/round.svg');
|
|
} else if ($('.line-join-active').attr('id') == 'small-dash') {
|
|
$('.line-join-active')
|
|
.find('img')
|
|
.attr('src', 'assets/dash2.svg');
|
|
}
|
|
$('.line-join-active').removeClass('line-join-active');
|
|
$(this).addClass('line-join-active');
|
|
if ($(this).attr('id') == 'miter') {
|
|
$(this).find('img').attr('src', 'assets/miter-active.svg');
|
|
canvas
|
|
.getActiveObject()
|
|
.set({ strokeWidth: 0, strokeDashArray: false });
|
|
canvas.renderAll();
|
|
updatePanelValues();
|
|
} else if ($(this).attr('id') == 'bevel') {
|
|
$(this).find('img').attr('src', 'assets/bevel-active.svg');
|
|
canvas.getActiveObject().set({ strokeDashArray: false });
|
|
if (canvas.getActiveObject().get('strokeWidth') == 0) {
|
|
canvas.getActiveObject().set({ strokeWidth: 1 });
|
|
canvas.renderAll();
|
|
updatePanelValues();
|
|
}
|
|
} else if ($(this).attr('id') == 'round') {
|
|
$(this).find('img').attr('src', 'assets/round-active.svg');
|
|
canvas.getActiveObject().set({ strokeDashArray: [10, 5] });
|
|
if (canvas.getActiveObject().get('strokeWidth') == 0) {
|
|
canvas.getActiveObject().set({ strokeWidth: 1 });
|
|
canvas.renderAll();
|
|
updatePanelValues();
|
|
}
|
|
} else {
|
|
$(this).find('img').attr('src', 'assets/dash2-active.svg');
|
|
canvas.getActiveObject().set({ strokeDashArray: [3, 3] });
|
|
if (canvas.getActiveObject().get('strokeWidth') == 0) {
|
|
canvas.getActiveObject().set({ strokeWidth: 1 });
|
|
canvas.renderAll();
|
|
updatePanelValues();
|
|
}
|
|
}
|
|
canvas.renderAll();
|
|
save();
|
|
}
|
|
$(document).on('click', '.line-join', lineJoin);
|
|
|
|
// Change text alignment
|
|
function alignText() {
|
|
var textalign;
|
|
if ($('.align-text-active').attr('id') == 'align-text-left') {
|
|
$('.align-text-active')
|
|
.find('img')
|
|
.attr('src', 'assets/align-text-left.svg');
|
|
} else if (
|
|
$('.align-text-active').attr('id') == 'align-text-center'
|
|
) {
|
|
$('.align-text-active')
|
|
.find('img')
|
|
.attr('src', 'assets/align-text-center.svg');
|
|
} else if (
|
|
$('.align-text-active').attr('id') == 'align-text-right'
|
|
) {
|
|
$('.align-text-active')
|
|
.find('img')
|
|
.attr('src', 'assets/align-text-right.svg');
|
|
} else {
|
|
$('.align-text-active')
|
|
.find('img')
|
|
.attr('src', 'assets/align-text-justify.svg');
|
|
}
|
|
$('.align-text-active').removeClass('align-text-active');
|
|
$(this).addClass('align-text-active');
|
|
if ($(this).attr('id') == 'align-text-left') {
|
|
textalign = 'left';
|
|
$(this)
|
|
.find('img')
|
|
.attr('src', 'assets/align-text-left-active.svg');
|
|
} else if ($(this).attr('id') == 'align-text-center') {
|
|
textalign = 'center';
|
|
$(this)
|
|
.find('img')
|
|
.attr('src', 'assets/align-text-center-active.svg');
|
|
} else if ($(this).attr('id') == 'align-text-right') {
|
|
textalign = 'right';
|
|
$(this)
|
|
.find('img')
|
|
.attr('src', 'assets/align-text-right-active.svg');
|
|
} else {
|
|
textalign = 'justify';
|
|
$(this)
|
|
.find('img')
|
|
.attr('src', 'assets/align-text-justify-active.svg');
|
|
}
|
|
canvas.getActiveObject().set({ textAlign: textalign });
|
|
canvas.renderAll();
|
|
save();
|
|
}
|
|
$(document).on('click', '.align-text', alignText);
|
|
|
|
// Change font
|
|
function changeFont() {
|
|
var font = $('#font-picker').val();
|
|
const active = canvas.getActiveObject();
|
|
if (!active || !font) {
|
|
return;
|
|
}
|
|
if (active.get('assetType')) {
|
|
WebFont.load({
|
|
google: {
|
|
families: [font],
|
|
},
|
|
active: () => {
|
|
const text = animatedtext.find((x) => x.id == active.id);
|
|
if (!text) {
|
|
return;
|
|
}
|
|
text.reset(
|
|
text.text,
|
|
$.extend(text.props, { fontFamily: font }),
|
|
canvas
|
|
);
|
|
save();
|
|
},
|
|
});
|
|
} else {
|
|
WebFont.load({
|
|
google: {
|
|
families: [font],
|
|
},
|
|
active: () => {
|
|
active.set('fontFamily', font);
|
|
canvas.renderAll();
|
|
save();
|
|
},
|
|
});
|
|
}
|
|
}
|
|
$(document).on('change', '#font-picker', changeFont);
|
|
|
|
// Calculate text width to display it in 1 line
|
|
function calculateTextWidth(text, font) {
|
|
var ctx = canvas.getContext('2d');
|
|
ctx.font = font;
|
|
return ctx.measureText(text).width + 10;
|
|
}
|
|
|
|
// Create an audio layer
|
|
function newAudioLayer(src) {
|
|
var audio = new Audio(src);
|
|
audio.crossOrigin = 'anonymous';
|
|
audio.addEventListener('loadeddata', () => {
|
|
var nullobject = new fabric.Rect({
|
|
id: 'Audio' + layer_count,
|
|
width: 10,
|
|
height: 10,
|
|
audioSrc: src,
|
|
duration: audio.duration * 1000,
|
|
opacity: 0,
|
|
selectable: false,
|
|
// Audio layers are invisible placeholders. Selecting them (from the
|
|
// layer list) must not paint controls / a bounding box on the canvas.
|
|
hasControls: false,
|
|
hasBorders: false,
|
|
evented: false,
|
|
volume: 0.5,
|
|
assetType: 'audio',
|
|
shadow: {
|
|
color: '#000',
|
|
offsetX: 0,
|
|
offsetY: 0,
|
|
blur: 0,
|
|
opacity: 0,
|
|
},
|
|
});
|
|
canvas.add(nullobject);
|
|
newLayer(nullobject);
|
|
});
|
|
}
|
|
|
|
// Create a textbox
|
|
function newTextbox(
|
|
fontsize,
|
|
fontweight,
|
|
text,
|
|
x,
|
|
y,
|
|
width,
|
|
center,
|
|
font
|
|
) {
|
|
var newtext = new fabric.Textbox(text, {
|
|
left: x,
|
|
top: y,
|
|
originX: 'center',
|
|
originY: 'center',
|
|
fontFamily: 'Inter',
|
|
fill: '#000',
|
|
fontSize: fontsize,
|
|
fontWeight: fontweight,
|
|
textAlign: 'center',
|
|
cursorWidth: 1,
|
|
stroke: '#000',
|
|
strokeWidth: 0,
|
|
cursorDuration: 1,
|
|
paintFirst: 'stroke',
|
|
objectCaching: false,
|
|
absolutePositioned: true,
|
|
strokeUniform: true,
|
|
inGroup: false,
|
|
cursorDelay: 250,
|
|
strokeDashArray: false,
|
|
width: calculateTextWidth(
|
|
text,
|
|
fontweight + ' ' + fontsize + 'px ' + (font || 'Inter')
|
|
),
|
|
id: 'Text' + layer_count,
|
|
shadow: {
|
|
color: '#000',
|
|
offsetX: 0,
|
|
offsetY: 0,
|
|
blur: 0,
|
|
opacity: 0,
|
|
},
|
|
});
|
|
newtext.setControlsVisibility({
|
|
mt: false,
|
|
mb: false,
|
|
});
|
|
canvas.add(newtext);
|
|
newLayer(newtext);
|
|
canvas.setActiveObject(newtext);
|
|
canvas.bringToFront(newtext);
|
|
newtext.enterEditing();
|
|
newtext.selectAll();
|
|
canvas.renderAll();
|
|
if (center) {
|
|
newtext.set(
|
|
'left',
|
|
artboard.get('left') + artboard.get('width') / 2
|
|
);
|
|
newtext.set(
|
|
'top',
|
|
artboard.get('top') + artboard.get('height') / 2
|
|
);
|
|
canvas.renderAll();
|
|
}
|
|
canvas.getActiveObject().set('fontFamily', font);
|
|
canvas.renderAll();
|
|
}
|
|
|
|
function deleteObject(object, def = true) {
|
|
if (object.get('assetType') == 'animatedText' && def) {
|
|
animatedtext = $.grep(animatedtext, function (a) {
|
|
return a.id != object.id;
|
|
});
|
|
}
|
|
if (object.type == 'image') {
|
|
files = $.grep(files, function (a) {
|
|
return a.name != object.get('id');
|
|
});
|
|
}
|
|
// Release any media element the object was holding on to
|
|
if (typeof object.getElement === 'function') {
|
|
const el = object.getElement();
|
|
if (el && el.tagName == 'VIDEO') {
|
|
el.pause();
|
|
el.removeAttribute('src');
|
|
el.load();
|
|
}
|
|
}
|
|
if (object.get('src') && typeof object.get('src').pause === 'function') {
|
|
object.get('src').pause();
|
|
}
|
|
$(".layer[data-object='" + object.get('id') + "']").remove();
|
|
$('#' + object.get('id')).remove();
|
|
keyframes = $.grep(keyframes, function (e) {
|
|
return e.id != object.get('id');
|
|
});
|
|
p_keyframes = $.grep(p_keyframes, function (e) {
|
|
return e.id != object.get('id');
|
|
});
|
|
objects = $.grep(objects, function (e) {
|
|
return e.id != object.get('id');
|
|
});
|
|
canvas.remove(object);
|
|
canvas.renderAll();
|
|
canvas.discardActiveObject();
|
|
save();
|
|
if (objects.length == 0) {
|
|
$('#nolayers').removeClass('yaylayers');
|
|
}
|
|
}
|
|
|
|
// Delete selected object
|
|
function deleteSelection() {
|
|
if (
|
|
canvas.getActiveObject() &&
|
|
!canvas.getActiveObject().isEditing
|
|
) {
|
|
const selection = canvas.getActiveObject();
|
|
if (selection.type == 'activeSelection') {
|
|
canvas.discardActiveObject();
|
|
selection._objects.forEach(function (object) {
|
|
deleteObject(object);
|
|
});
|
|
} else {
|
|
deleteObject(canvas.getActiveObject());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Expand / collapse layer
|
|
function toggleLayer() {
|
|
const layerid = $(this).parent().parent().attr('data-object');
|
|
$(this).parent().parent().find('.properties').toggle();
|
|
$(this).parent().parent().find('.droparrow').toggleClass('layeron');
|
|
$(".keyframe-row[data-object='" + layerid + "']").toggle();
|
|
setTimelineZoom(timelinetime);
|
|
}
|
|
$(document).on('click', '.droparrow', toggleLayer);
|
|
|
|
// Select layer
|
|
function selectLayer(e) {
|
|
if (!$(e.target).hasClass('droparrow')) {
|
|
const layerid = $(this).parent().attr('data-object');
|
|
$('.layer-selected').removeClass('layer-selected');
|
|
$(this).parent().addClass('layer-selected');
|
|
canvas.setActiveObject(canvas.getItemById(layerid));
|
|
}
|
|
}
|
|
$(document).on('click', '.layer-name', selectLayer);
|
|
|
|
// Set video duration
|
|
function setDuration(length) {
|
|
$('#inner-timeline').css('width', length / timelinetime + 50);
|
|
$('#inner-seekarea').css('width', length / timelinetime + 50);
|
|
duration = length;
|
|
var minutes = Math.floor(duration / 1000 / 60);
|
|
var seconds = (duration / 1000 - minutes * 60).toFixed(2);
|
|
$('#total-time input').val(
|
|
('0' + minutes).slice(-2) +
|
|
':' +
|
|
('0' + Math.floor(seconds)).slice(-2) +
|
|
':' +
|
|
('0' + Math.floor((seconds % 1) * 100)).slice(-2)
|
|
);
|
|
$('.object-props').each(function () {
|
|
const p_keyframe = p_keyframes.find(
|
|
(x) => x.id == $(this).attr('id')
|
|
);
|
|
if (!p_keyframe) {
|
|
return;
|
|
}
|
|
$(this).css(
|
|
'width',
|
|
duration / timelinetime - p_keyframe.start / timelinetime + 'px'
|
|
);
|
|
p_keyframe.end = duration;
|
|
if (p_keyframe.trimend > p_keyframe.end) {
|
|
p_keyframe.trimend = duration;
|
|
$(this)
|
|
.find('.trim-row')
|
|
.css(
|
|
'width',
|
|
duration / timelinetime -
|
|
p_keyframe.trimstart / timelinetime +
|
|
'px'
|
|
);
|
|
}
|
|
});
|
|
setTimelineZoom(timelinetime);
|
|
save();
|
|
}
|
|
|
|
// Render time markers
|
|
function renderTimeMarkers() {
|
|
var renderoffset = 1000 / timelinetime - 20;
|
|
var timenumber = 0;
|
|
var modulo = 1;
|
|
if (timelinetime > 18) {
|
|
modulo = 5;
|
|
} else if (timelinetime > 12) {
|
|
modulo = 2;
|
|
}
|
|
$('#time-numbers').html('');
|
|
$('#time-numbers').append(
|
|
"<div class='time-number' style='margin-left:" +
|
|
offset_left +
|
|
"px'>" +
|
|
timenumber +
|
|
's<span></span></div>'
|
|
);
|
|
timenumber++;
|
|
while (timenumber * 1000 <= duration) {
|
|
$('#time-numbers').append(
|
|
"<div class='time-number' style='margin-left:" +
|
|
renderoffset +
|
|
"px'>" +
|
|
timenumber +
|
|
's<span></span></div>'
|
|
);
|
|
if (timenumber % modulo != 0) {
|
|
$('#time-numbers .time-number:last-child').css('opacity', '0');
|
|
}
|
|
timenumber++;
|
|
}
|
|
}
|
|
|
|
// Change timeline zoom level
|
|
function setTimelineZoom(time) {
|
|
$('.object-props').each(function () {
|
|
const p_keyframe = p_keyframes.find(
|
|
(x) => x.id == $(this).attr('id')
|
|
);
|
|
if (!p_keyframe) {
|
|
return;
|
|
}
|
|
$(this).offset({
|
|
left:
|
|
p_keyframe.start / time +
|
|
$('#inner-timeline').offset().left +
|
|
offset_left,
|
|
});
|
|
$(this).css({ width: ($(this).width() * timelinetime) / time });
|
|
$(this)
|
|
.find('.trim-row')
|
|
.css({ left: p_keyframe.trimstart / time });
|
|
$(this)
|
|
.find('.trim-row')
|
|
.css({
|
|
width:
|
|
($(this).find('.trim-row').width() * timelinetime) / time,
|
|
});
|
|
});
|
|
timelinetime = time;
|
|
$('.keyframe').each(function () {
|
|
$(this).offset({
|
|
left:
|
|
$(this).attr('data-time') / timelinetime +
|
|
$('#inner-timeline').offset().left +
|
|
offset_left,
|
|
});
|
|
});
|
|
$('#seekbar').offset({
|
|
left:
|
|
$('#inner-timeline').offset().left +
|
|
currenttime / timelinetime +
|
|
offset_left,
|
|
});
|
|
$('#inner-timeline').css({ width: duration / timelinetime + 50 });
|
|
$('#inner-seekarea').css({
|
|
minWidth: duration / timelinetime + 50,
|
|
});
|
|
renderTimeMarkers();
|
|
}
|
|
$(document).on('input', '#timeline-zoom', function () {
|
|
setTimelineZoom($('#timeline-zoom').val());
|
|
});
|
|
|
|
function removeKeyframe() {
|
|
if (!selectedkeyframe) {
|
|
return;
|
|
}
|
|
const time = selectedkeyframe.attr('data-time');
|
|
const id = selectedkeyframe.attr('data-object');
|
|
const prop = selectedkeyframe.attr('data-property');
|
|
const names = [prop].concat(KEYFRAME_COUNTERPARTS[prop] || []);
|
|
keyframes = $.grep(keyframes, function (e) {
|
|
return (
|
|
e.t != time || e.id != id || names.indexOf(e.name) == -1
|
|
);
|
|
});
|
|
selectedkeyframe.remove();
|
|
$('#keyframe-properties').removeClass('show-properties');
|
|
}
|
|
|
|
// Delete a keyframe
|
|
function deleteKeyframe() {
|
|
if (shiftkeys.length > 0) {
|
|
shiftkeys.forEach(function (key) {
|
|
selectedkeyframe = $(key.keyframe);
|
|
removeKeyframe();
|
|
});
|
|
shiftkeys = [];
|
|
} else {
|
|
removeKeyframe();
|
|
}
|
|
animate(false, currenttime);
|
|
save();
|
|
}
|
|
$(document).on('click', '#delete-keyframe', deleteKeyframe);
|
|
|
|
// Properties that are always keyframed together with a leading property
|
|
const KEYFRAME_COUNTERPARTS = {
|
|
left: ['top'],
|
|
scaleX: ['scaleY', 'width', 'height'],
|
|
strokeWidth: ['stroke'],
|
|
'shadow.color': [
|
|
'shadow.opacity',
|
|
'shadow.offsetX',
|
|
'shadow.offsetY',
|
|
'shadow.blur',
|
|
],
|
|
charSpacing: ['lineHeight'],
|
|
};
|
|
|
|
// Copy keyframes
|
|
function copyKeyframes() {
|
|
if (!Array.isArray(clipboard) || clipboard.length == 0) {
|
|
return;
|
|
}
|
|
clipboard.sort(function (a, b) {
|
|
return a.t - b.t;
|
|
});
|
|
var inittime = clipboard[0].t;
|
|
clipboard.forEach(function (keyframe) {
|
|
const object = canvas.getItemById(keyframe.id);
|
|
if (!object) {
|
|
return;
|
|
}
|
|
var newtime = keyframe.t - inittime + currenttime;
|
|
newKeyframe(
|
|
keyframe.name,
|
|
object,
|
|
newtime,
|
|
keyframe.value,
|
|
true
|
|
);
|
|
// Counterpart properties are stored as separate keyframes at the same
|
|
// time; copy whichever of them actually exist.
|
|
const counterparts = KEYFRAME_COUNTERPARTS[keyframe.name] || [];
|
|
counterparts.forEach(function (name) {
|
|
const keyarr2 = $.grep(keyframes, function (e) {
|
|
return (
|
|
e.t == keyframe.t &&
|
|
e.id == keyframe.id &&
|
|
e.name == name
|
|
);
|
|
});
|
|
if (keyarr2.length > 0) {
|
|
newKeyframe(name, object, newtime, keyarr2[0].value, true);
|
|
}
|
|
});
|
|
});
|
|
save();
|
|
}
|
|
|
|
// Update keyframe (after dragging, or when re-stamping it at the playhead)
|
|
function updateKeyframe(drag, newval) {
|
|
const keyprop = drag.attr('data-property');
|
|
const keytime = drag.attr('data-time');
|
|
const objectid = drag.attr('data-object');
|
|
const keyarr = $.grep(keyframes, function (e) {
|
|
return (
|
|
e.t == parseFloat(keytime) &&
|
|
e.id == objectid &&
|
|
e.name == keyprop
|
|
);
|
|
});
|
|
if (keyarr.length == 0) {
|
|
return;
|
|
}
|
|
const keyobj = canvas.getItemById(keyarr[0].id);
|
|
const p_keyframe = p_keyframes.find((x) => x.id == objectid);
|
|
if (!keyobj || !p_keyframe) {
|
|
return;
|
|
}
|
|
// data-time is always absolute; drag.position() is relative to the layer
|
|
// row, which is itself offset by the layer start.
|
|
const time = newval
|
|
? currenttime
|
|
: parseFloat(p_keyframe.start) +
|
|
parseFloat((drag.position().left * timelinetime).toFixed(1));
|
|
|
|
const names = [keyprop].concat(
|
|
KEYFRAME_COUNTERPARTS[keyprop] || []
|
|
);
|
|
names.forEach(function (name) {
|
|
const arr = $.grep(keyframes, function (e) {
|
|
return (
|
|
e.t == parseFloat(keytime) &&
|
|
e.id == objectid &&
|
|
e.name == name
|
|
);
|
|
});
|
|
if (arr.length == 0) {
|
|
return;
|
|
}
|
|
let value = arr[0].value;
|
|
if (newval) {
|
|
if (name == 'volume') {
|
|
value = parseFloat($('#object-volume input').val()) / 200;
|
|
} else {
|
|
value = getPropValue(keyobj, name);
|
|
}
|
|
} else if (name == 'left') {
|
|
// newKeyframe stores left/top relative to the artboard
|
|
value = value + artboard.get('left');
|
|
} else if (name == 'top') {
|
|
value = value + artboard.get('top');
|
|
}
|
|
keyframes = $.grep(keyframes, function (e) {
|
|
return (
|
|
e.t != parseFloat(keytime) ||
|
|
e.id != objectid ||
|
|
e.name != name
|
|
);
|
|
});
|
|
newKeyframe(name, keyobj, time, value, false);
|
|
});
|
|
drag.attr('data-time', time);
|
|
sortKeyframes();
|
|
}
|
|
|
|
function keyframeSnap(drag) {
|
|
if (shiftkeys.length == 0) {
|
|
if (
|
|
drag.offset().left > $('#seekbar').offset().left - 5 &&
|
|
drag.offset().left < $('#seekbar').offset().left + 5
|
|
) {
|
|
drag.offset({ left: $('#seekbar').offset().left });
|
|
$('#line-snap').offset({
|
|
left: $('#seekbar').offset().left,
|
|
top: drag.parent().parent().offset().top,
|
|
});
|
|
$('#line-snap').css({
|
|
height: drag.parent().parent().height(),
|
|
});
|
|
$('#line-snap').addClass('line-active');
|
|
} else {
|
|
let snapped = false;
|
|
drag
|
|
.parent()
|
|
.parent()
|
|
.find('.keyframe')
|
|
.each(function () {
|
|
if (!drag.is($(this))) {
|
|
if (
|
|
drag.offset().left > $(this).offset().left - 5 &&
|
|
drag.offset().left < $(this).offset().left + 5
|
|
) {
|
|
drag.offset({ left: $(this).offset().left });
|
|
$('#line-snap').offset({
|
|
left: $(this).offset().left,
|
|
top: drag.parent().parent().offset().top,
|
|
});
|
|
$('#line-snap').css({
|
|
height: drag.parent().parent().height(),
|
|
});
|
|
$('#line-snap').addClass('line-active');
|
|
snapped = true;
|
|
return false;
|
|
}
|
|
}
|
|
});
|
|
if (!snapped) {
|
|
$('#line-snap').removeClass('line-active');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shared plumbing for the timeline drags (seekbar, keyframes, layer bars,
|
|
// timeline resize handle). Captures the pointer so the matching pointerup
|
|
// always comes back to us, and tears the drag down on pointercancel, on a
|
|
// stray native dragstart, or when the window loses focus. Without this a
|
|
// swallowed mouseup leaves the dragged element glued to the cursor.
|
|
function bindPointerDrag(e, el, onMove, onEnd) {
|
|
var pointerId =
|
|
e.originalEvent && e.originalEvent.pointerId != undefined
|
|
? e.originalEvent.pointerId
|
|
: null;
|
|
if (pointerId != null && el && el.setPointerCapture) {
|
|
try {
|
|
el.setPointerCapture(pointerId);
|
|
} catch (err) {}
|
|
}
|
|
function end(ev) {
|
|
// Ignore another pointer going up while this one is still dragging
|
|
if (
|
|
ev &&
|
|
ev.originalEvent &&
|
|
pointerId != null &&
|
|
ev.originalEvent.pointerId != undefined &&
|
|
ev.originalEvent.pointerId != pointerId
|
|
) {
|
|
return;
|
|
}
|
|
$(document)
|
|
.off('pointermove', onMove)
|
|
.off('pointerup', end)
|
|
.off('pointercancel', end)
|
|
.off('dragstart', end);
|
|
window.removeEventListener('blur', end);
|
|
if (pointerId != null && el && el.releasePointerCapture) {
|
|
try {
|
|
el.releasePointerCapture(pointerId);
|
|
} catch (err) {}
|
|
}
|
|
onEnd(ev);
|
|
}
|
|
$(document)
|
|
.on('pointermove', onMove)
|
|
.on('pointerup', end)
|
|
.on('pointercancel', end)
|
|
.on('dragstart', end);
|
|
// Native listener on purpose: jQuery routes blur through its focus special
|
|
// event, which defers the handler and can drop it entirely
|
|
window.addEventListener('blur', end);
|
|
}
|
|
|
|
// A modal covers the timeline but does not cover pointer events, so every
|
|
// timeline drag has to opt out while one is open - otherwise the seekbar,
|
|
// keyframes, layer bars and the resize handle all still react to a drag
|
|
// happening "behind" the dialog.
|
|
function modalOpen() {
|
|
return $('.modal-open').length > 0;
|
|
}
|
|
|
|
// Dragging a keyframe
|
|
function dragKeyframe(e) {
|
|
if (e.which == 3 || modalOpen()) {
|
|
return false;
|
|
}
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
var inst = this;
|
|
var drag = $(this);
|
|
var pageX = e.pageX;
|
|
var offset = $(this).offset();
|
|
var move = false;
|
|
if (e.shiftKey) {
|
|
if (!$(this).hasClass('keyframe-selected')) {
|
|
shiftkeys.push({
|
|
keyframe: this,
|
|
offset: $(this).offset().left,
|
|
});
|
|
$(this).addClass('keyframe-selected');
|
|
} else {
|
|
const el = this;
|
|
shiftkeys = $.grep(shiftkeys, function (e) {
|
|
return e.keyframe !== el;
|
|
});
|
|
$(this).removeClass('keyframe-selected');
|
|
}
|
|
}
|
|
if (shiftkeys.length > 0) {
|
|
shiftkeys.forEach(function (key) {
|
|
key.offset = $(key.keyframe).offset().left;
|
|
});
|
|
}
|
|
function draggingKeyframe(e) {
|
|
move = true;
|
|
var left = offset.left + (e.pageX - pageX);
|
|
if (shiftkeys.length == 0) {
|
|
if (left > $('#timearea').offset().left + offset_left) {
|
|
drag.offset({ left: left });
|
|
} else {
|
|
drag.offset({
|
|
left: $('#timearea').offset().left + offset_left,
|
|
});
|
|
}
|
|
keyframeSnap(drag);
|
|
} else {
|
|
shiftkeys.forEach(function (key) {
|
|
if (key.keyframe != inst) {
|
|
$(key.keyframe).offset({
|
|
left: key.offset + (e.pageX - pageX),
|
|
});
|
|
keyframeSnap($(key.keyframe));
|
|
} else {
|
|
drag.offset({ left: left });
|
|
keyframeSnap(drag);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
function releasedKeyframe(e) {
|
|
$('#line-snap').removeClass('line-active');
|
|
if (move) {
|
|
if (shiftkeys.length == 0) {
|
|
// Check for 60FPS playback, 16ms "slots"
|
|
var time = parseFloat(
|
|
(drag.position().left * timelinetime).toFixed(1)
|
|
);
|
|
if (time % 16.666 != 0) {
|
|
drag.offset({
|
|
left:
|
|
(Math.ceil(time / 16.666) * 16.666) / timelinetime +
|
|
drag.parent().offset().left,
|
|
});
|
|
updateKeyframe(drag, false);
|
|
} else {
|
|
updateKeyframe(drag, false);
|
|
}
|
|
} else {
|
|
shiftkeys.forEach(function (key) {
|
|
// Check for 60FPS playback, 16ms "slots"
|
|
var time = parseFloat(
|
|
($(key.keyframe).position().left * timelinetime).toFixed(
|
|
1
|
|
)
|
|
);
|
|
if (time % 16.666 != 0) {
|
|
$(key.keyframe).offset({
|
|
left:
|
|
(Math.ceil(time / 16.666) * 16.666) / timelinetime +
|
|
$(key.keyframe).parent().offset().left,
|
|
});
|
|
updateKeyframe($(key.keyframe), false);
|
|
} else {
|
|
updateKeyframe($(key.keyframe), false);
|
|
}
|
|
});
|
|
}
|
|
} else if (e && e.type == 'pointerup' && !e.shiftKey) {
|
|
keyframeProperties(inst);
|
|
}
|
|
move = false;
|
|
$('.line-active').removeClass('line-active');
|
|
save();
|
|
}
|
|
bindPointerDrag(e, this, draggingKeyframe, releasedKeyframe);
|
|
}
|
|
$(document).on('pointerdown', '.keyframe', dragKeyframe);
|
|
|
|
// Render current time in the playback area
|
|
function renderTime() {
|
|
var minutes = Math.floor(currenttime / 1000 / 60);
|
|
var seconds = (currenttime / 1000 - minutes * 60).toFixed(2);
|
|
$('#current-time input').val(
|
|
('0' + minutes).slice(-2) +
|
|
':' +
|
|
('0' + Math.floor(seconds)).slice(-2) +
|
|
':' +
|
|
('0' + Math.floor((seconds % 1) * 100)).slice(-2)
|
|
);
|
|
}
|
|
|
|
// Update current time (and account for frame "slots")
|
|
function updateTime(drag, check) {
|
|
if ($('#timeline').scrollLeft() > offset_left) {
|
|
currenttime = parseFloat(
|
|
(
|
|
(drag.position().left +
|
|
$('#timeline').scrollLeft() -
|
|
offset_left) *
|
|
timelinetime
|
|
).toFixed(1)
|
|
);
|
|
} else {
|
|
currenttime = parseFloat(
|
|
(
|
|
(drag.position().left +
|
|
$('#timeline').scrollLeft() -
|
|
offset_left) *
|
|
timelinetime
|
|
).toFixed(1)
|
|
);
|
|
}
|
|
// Check for 60FPS playback, 16ms "slots"
|
|
if (currenttime % 16.666 != 0 && !check) {
|
|
currenttime = Math.ceil(currenttime / 16.666) * 16.666;
|
|
}
|
|
renderTime();
|
|
pause();
|
|
animate(false, currenttime);
|
|
}
|
|
|
|
// Dragging the seekbar
|
|
function dragSeekBar(e) {
|
|
if (e.which == 3 || modalOpen()) {
|
|
return false;
|
|
}
|
|
// Stop the browser from turning the press into a native drag-and-drop.
|
|
// When it does, the mouseup is swallowed, released() never runs and the
|
|
// seekbar stays glued to the pointer.
|
|
e.preventDefault();
|
|
var drag = $(this);
|
|
var pageX = e.pageX;
|
|
var offset = $(this).offset();
|
|
tempselection = canvas.getActiveObject();
|
|
canvas.discardActiveObject();
|
|
function dragging(e) {
|
|
paused = true;
|
|
var left = offset.left + (e.pageX - pageX);
|
|
if (
|
|
left > $('#timearea').offset().left + offset_left &&
|
|
left - $('#timearea').offset().left <
|
|
duration / timelinetime + offset_left
|
|
) {
|
|
drag.offset({ left: left });
|
|
} else if (left < $('#timearea').offset().left + offset_left) {
|
|
drag.offset({
|
|
left: offset_left + $('#timearea').offset().left,
|
|
});
|
|
}
|
|
if ($('#timeline').scrollLeft() > offset_left) {
|
|
currenttime = parseFloat(
|
|
(
|
|
(drag.position().left +
|
|
$('#timeline').scrollLeft() -
|
|
offset_left) *
|
|
timelinetime
|
|
).toFixed(1)
|
|
);
|
|
} else {
|
|
currenttime = parseFloat(
|
|
(
|
|
(drag.position().left +
|
|
$('#timeline').scrollLeft() -
|
|
offset_left) *
|
|
timelinetime
|
|
).toFixed(1)
|
|
);
|
|
}
|
|
animate(false, currenttime);
|
|
seeking = true;
|
|
renderTime();
|
|
}
|
|
function released(e) {
|
|
updateTime(drag, false);
|
|
seeking = false;
|
|
if (tempselection && tempselection.type != 'activeSelection') {
|
|
reselect(tempselection);
|
|
}
|
|
updatePanelValues();
|
|
}
|
|
bindPointerDrag(e, this, dragging, released);
|
|
}
|
|
$(document).on('pointerdown', '#seekbar', dragSeekBar);
|
|
// The timeline drag targets must never become native drag sources
|
|
$(document).on(
|
|
'dragstart',
|
|
'#seekbar, .keyframe, .main-row, .row-el, .trim-row, #timeline-handle',
|
|
function (e) {
|
|
e.preventDefault();
|
|
}
|
|
);
|
|
|
|
// Dragging layer horizontally
|
|
function dragObjectProps(e) {
|
|
if (e.which == 3 || modalOpen()) {
|
|
return false;
|
|
}
|
|
var drag = $(this).parent();
|
|
var drag2 = $(this).find('.trim-row');
|
|
var target = e.target;
|
|
var pageX = e.pageX;
|
|
var offset = drag.offset();
|
|
var offset2 = drag2.offset();
|
|
var initwidth = drag2.width();
|
|
var initpos = drag2.position().left;
|
|
var opened = false;
|
|
var trim = 'no';
|
|
// Trim layer to hovered area
|
|
if (e.metaKey) {
|
|
if (e.shiftKey) {
|
|
if (drag2.position().left + e.pageX >= 0) {
|
|
drag2.offset({
|
|
left:
|
|
hovertime / timelinetime -
|
|
p_keyframes.find((x) => x.id == drag.attr('id'))
|
|
.trimstart /
|
|
timelinetime +
|
|
offset2.left,
|
|
});
|
|
const leftval = parseFloat(
|
|
(drag2.position().left * timelinetime).toFixed(1)
|
|
);
|
|
p_keyframes.find((x) => x.id == drag.attr('id')).trimstart =
|
|
leftval;
|
|
drag2.css({
|
|
width:
|
|
(p_keyframes.find((x) => x.id == drag.attr('id'))
|
|
.trimend -
|
|
p_keyframes.find((x) => x.id == drag.attr('id'))
|
|
.trimstart) /
|
|
timelinetime,
|
|
});
|
|
return false;
|
|
}
|
|
} else {
|
|
if (
|
|
hovertime +
|
|
p_keyframes.find((x) => x.id == drag.attr('id')).start <
|
|
duration
|
|
) {
|
|
drag2.css({
|
|
width:
|
|
hovertime / timelinetime -
|
|
p_keyframes.find((x) => x.id == drag.attr('id'))
|
|
.trimstart /
|
|
timelinetime,
|
|
});
|
|
save();
|
|
p_keyframes.find((x) => x.id == drag.attr('id')).end =
|
|
hovertime;
|
|
p_keyframes.find((x) => x.id == drag.attr('id')).trimend =
|
|
hovertime;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
if (pageX - $(this).find('.trim-row').offset().left < 7) {
|
|
trim = 'left';
|
|
} else if (
|
|
pageX - $(this).find('.trim-row').offset().left >
|
|
$(this).find('.trim-row').width() - 7
|
|
) {
|
|
trim = 'right';
|
|
}
|
|
function dragging(e) {
|
|
if (trim == 'no') {
|
|
var left = offset.left + (e.pageX - pageX);
|
|
if (
|
|
left >
|
|
$('#timearea').offset().left +
|
|
offset_left -
|
|
$('#timeline').scrollLeft()
|
|
) {
|
|
drag.offset({ left: left });
|
|
} else if (
|
|
left + $('#timeline').scrollLeft() <
|
|
$('#timearea').offset().left + offset_left
|
|
) {
|
|
drag.css({ left: offset_left });
|
|
}
|
|
p_keyframes.find((x) => x.id == drag.attr('id')).start =
|
|
parseFloat(
|
|
(
|
|
(drag.position().left -
|
|
offset_left +
|
|
$('#timeline').scrollLeft()) *
|
|
timelinetime
|
|
).toFixed(1)
|
|
);
|
|
p_keyframes.find((x) => x.id == drag.attr('id')).end =
|
|
parseFloat(
|
|
(
|
|
(drag.position().left +
|
|
drag.width() -
|
|
offset_left +
|
|
$('#timeline').scrollLeft()) *
|
|
timelinetime
|
|
).toFixed(1)
|
|
);
|
|
if (
|
|
$(".keyframe-row[data-object='" + drag.attr('id') + "']").is(
|
|
':hidden'
|
|
)
|
|
) {
|
|
opened = true;
|
|
$(".layer[data-object='" + drag.attr('id') + "']")
|
|
.find('.properties')
|
|
.toggle();
|
|
$(".layer[data-object='" + drag.attr('id') + "']")
|
|
.find('.properties')
|
|
.toggleClass('layeron');
|
|
$(
|
|
".keyframe-row[data-object='" + drag.attr('id') + "']"
|
|
).toggle();
|
|
setTimelineZoom(timelinetime);
|
|
}
|
|
drag.find('.keyframe').each(function () {
|
|
updateKeyframe($(this), false);
|
|
});
|
|
animate(false, currenttime);
|
|
} else if (trim == 'left') {
|
|
if (drag2.position().left + (e.pageX - pageX) >= 0) {
|
|
drag2.offset({
|
|
left: offset2.left + (e.pageX - pageX),
|
|
});
|
|
drag2.css({
|
|
width: initwidth - (-initpos + drag2.position().left),
|
|
});
|
|
const leftval = parseFloat(
|
|
(drag2.position().left * timelinetime).toFixed(1)
|
|
);
|
|
p_keyframes.find((x) => x.id == drag.attr('id')).trimstart =
|
|
leftval;
|
|
}
|
|
} else if (trim == 'right') {
|
|
if (initwidth + (e.pageX - pageX) < duration / timelinetime) {
|
|
drag2.css({
|
|
width: initwidth + (e.pageX - pageX),
|
|
});
|
|
} else {
|
|
drag2.css({
|
|
width:
|
|
duration / timelinetime -
|
|
drag.position().left -
|
|
$('#timeline').scrollLeft() +
|
|
offset_left,
|
|
});
|
|
}
|
|
const rightval = parseFloat(
|
|
(
|
|
(drag2.position().left + drag2.width()) *
|
|
timelinetime
|
|
).toFixed(1)
|
|
);
|
|
p_keyframes.find((x) => x.id == drag.attr('id')).end = rightval;
|
|
p_keyframes.find((x) => x.id == drag.attr('id')).trimend =
|
|
rightval;
|
|
}
|
|
}
|
|
function released(e) {
|
|
if (opened) {
|
|
$(".layer[data-object='" + drag.attr('id') + "']")
|
|
.find('.properties')
|
|
.toggle();
|
|
$(".layer[data-object='" + drag.attr('id') + "']")
|
|
.find('.properties')
|
|
.toggleClass('layeron');
|
|
$(
|
|
".keyframe-row[data-object='" + drag.attr('id') + "']"
|
|
).toggle();
|
|
setTimelineZoom(timelinetime);
|
|
}
|
|
animate(false, currenttime);
|
|
save();
|
|
}
|
|
bindPointerDrag(e, this, dragging, released);
|
|
}
|
|
$(document).on('pointerdown', '.main-row', dragObjectProps);
|
|
|
|
function resetHeight() {
|
|
var top = $(window).height() - oldtimelinepos - 92;
|
|
if ($('#upload-tool').hasClass('tool-active')) {
|
|
$('#browser').css('top', '150px');
|
|
$('#browser').css(
|
|
'height',
|
|
'calc(100% - ' + (top + 97 + 150) + 'px)'
|
|
);
|
|
} else {
|
|
$('#browser').css('top', '110px');
|
|
$('#browser').css(
|
|
'height',
|
|
'calc(100% - ' + (top + 97 + 100) + 'px)'
|
|
);
|
|
}
|
|
$('#timearea').css('height', top);
|
|
$('#layer-list').css('height', top);
|
|
$('#toolbar').css('height', 'calc(100% - ' + (top + 97) + 'px)');
|
|
$('#canvas-area').css(
|
|
'height',
|
|
'calc(100% - ' + (top + 97) + 'px)'
|
|
);
|
|
$('#properties').css('height', 'calc(100% - ' + (top + 97) + 'px)');
|
|
$('.panel-handle').css(
|
|
'height',
|
|
'calc(100% - ' + (top + 97) + 'px)'
|
|
);
|
|
$('#timeline-handle').css('bottom', top + 95);
|
|
resizeCanvas();
|
|
}
|
|
|
|
// Dragging timeline vertically
|
|
function dragTimeline(e) {
|
|
if (e.which == 3 || modalOpen()) {
|
|
return false;
|
|
}
|
|
// Suppress text selection for the duration of the drag only - leaving these
|
|
// handlers installed kills mousedown for the whole document.
|
|
const disableselect = function () {
|
|
return false;
|
|
};
|
|
const previousSelectStart = document.onselectstart;
|
|
document.onselectstart = disableselect;
|
|
|
|
oldtimelinepos = e.pageY;
|
|
function draggingKeyframe(e) {
|
|
oldtimelinepos = e.pageY;
|
|
resetHeight(e);
|
|
}
|
|
function releasedKeyframe(e) {
|
|
document.onselectstart = previousSelectStart || null;
|
|
}
|
|
bindPointerDrag(e, this, draggingKeyframe, releasedKeyframe);
|
|
}
|
|
|
|
$(document).on('pointerdown', '#timeline-handle', dragTimeline);
|
|
|
|
oldtimelinepos = $(window).height() - 92 - $('#timearea').height();
|
|
|
|
// Side panels: horizontal resize and hide/show
|
|
// The widths live as CSS variables on :root - every panel rule is a calc() of
|
|
// them, so moving a handle only has to write one number.
|
|
const RAIL_WIDTH = 76;
|
|
const BROWSER_MIN_WIDTH = 210;
|
|
const BROWSER_MAX_WIDTH = 640;
|
|
const PROPS_MIN_WIDTH = 300;
|
|
const PROPS_MAX_WIDTH = 640;
|
|
// Canvas width kept free while dragging, whatever the window size
|
|
const CANVAS_MIN_WIDTH = 240;
|
|
const PANEL_LAYOUT_KEY = 'motionity-panel-layout';
|
|
|
|
var browserwidth = 299;
|
|
var propswidth = 300;
|
|
var propshidden = false;
|
|
// Tool to reopen the library with, set when it gets collapsed
|
|
var lasttool = $('.tool-active').attr('id') || 'shape-tool';
|
|
|
|
function clampWidth(value, min, max) {
|
|
return Math.max(min, Math.min(max, value));
|
|
}
|
|
|
|
function browserHidden() {
|
|
return $('#browser').hasClass('collapsed');
|
|
}
|
|
|
|
function applyPanelWidths() {
|
|
const root = document.documentElement.style;
|
|
const hidden = browserHidden();
|
|
var browser = hidden ? 0 : browserwidth;
|
|
var properties = propshidden ? 0 : propswidth;
|
|
// Shrink to fit rather than squeeze the canvas out of a narrow window. The
|
|
// stored widths are left alone, so the panels grow back on a wider one.
|
|
var over =
|
|
RAIL_WIDTH +
|
|
browser +
|
|
properties +
|
|
CANVAS_MIN_WIDTH -
|
|
$(window).width();
|
|
if (over > 0 && properties > PROPS_MIN_WIDTH) {
|
|
const cut = Math.min(over, properties - PROPS_MIN_WIDTH);
|
|
properties -= cut;
|
|
over -= cut;
|
|
}
|
|
if (over > 0 && browser > BROWSER_MIN_WIDTH) {
|
|
browser -= Math.min(over, browser - BROWSER_MIN_WIDTH);
|
|
}
|
|
root.setProperty('--browser-w', browser + 'px');
|
|
root.setProperty('--props-w', properties + 'px');
|
|
// The layer list and the timeline keep their column width while the library
|
|
// is hidden, otherwise layer names would get squashed into the tool rail.
|
|
if (!hidden) {
|
|
root.setProperty('--layers-w', RAIL_WIDTH + browser + 'px');
|
|
}
|
|
$('#browser-handle').toggleClass('panel-hidden', hidden);
|
|
$('#properties-handle').toggleClass('panel-hidden', propshidden);
|
|
$('#properties').toggleClass('collapsed', propshidden);
|
|
$('#browser-toggle').attr(
|
|
'title',
|
|
hidden ? 'Show the library' : 'Hide the library'
|
|
);
|
|
$('#properties-toggle').attr(
|
|
'title',
|
|
propshidden ? 'Show the properties' : 'Hide the properties'
|
|
);
|
|
}
|
|
|
|
function savePanelLayout() {
|
|
try {
|
|
localStorage.setItem(
|
|
PANEL_LAYOUT_KEY,
|
|
JSON.stringify({
|
|
browser: browserwidth,
|
|
properties: propswidth,
|
|
propertieshidden: propshidden,
|
|
})
|
|
);
|
|
} catch (err) {
|
|
// Private browsing or a full quota - the layout just won't persist
|
|
}
|
|
}
|
|
|
|
function restorePanelLayout() {
|
|
var stored;
|
|
try {
|
|
stored = JSON.parse(localStorage.getItem(PANEL_LAYOUT_KEY));
|
|
} catch (err) {
|
|
stored = null;
|
|
}
|
|
if (stored) {
|
|
if (typeof stored.browser == 'number') {
|
|
browserwidth = clampWidth(
|
|
stored.browser,
|
|
BROWSER_MIN_WIDTH,
|
|
BROWSER_MAX_WIDTH
|
|
);
|
|
}
|
|
if (typeof stored.properties == 'number') {
|
|
propswidth = clampWidth(
|
|
stored.properties,
|
|
PROPS_MIN_WIDTH,
|
|
PROPS_MAX_WIDTH
|
|
);
|
|
}
|
|
propshidden = stored.propertieshidden === true;
|
|
}
|
|
applyPanelWidths();
|
|
}
|
|
|
|
// Dragging a side panel border
|
|
function dragPanel(e) {
|
|
if (e.which == 3 || modalOpen() || $(this).hasClass('panel-hidden')) {
|
|
return false;
|
|
}
|
|
e.preventDefault();
|
|
const handle = $(this);
|
|
const isbrowser = handle.attr('id') == 'browser-handle';
|
|
const startx = e.pageX;
|
|
const startwidth = isbrowser ? browserwidth : propswidth;
|
|
// Suppress text selection for the duration of the drag only
|
|
const previousSelectStart = document.onselectstart;
|
|
document.onselectstart = function () {
|
|
return false;
|
|
};
|
|
handle.addClass('handle-dragging');
|
|
|
|
function draggingPanelBorder(ev) {
|
|
// The properties panel is anchored to the right, so its width grows the
|
|
// other way round
|
|
const delta = isbrowser ? ev.pageX - startx : startx - ev.pageX;
|
|
const otherwidth = isbrowser
|
|
? propshidden
|
|
? 0
|
|
: propswidth
|
|
: browserHidden()
|
|
? 0
|
|
: browserwidth;
|
|
const room =
|
|
$(window).width() - RAIL_WIDTH - CANVAS_MIN_WIDTH - otherwidth;
|
|
if (isbrowser) {
|
|
browserwidth = clampWidth(
|
|
startwidth + delta,
|
|
BROWSER_MIN_WIDTH,
|
|
Math.min(BROWSER_MAX_WIDTH, room)
|
|
);
|
|
} else {
|
|
propswidth = clampWidth(
|
|
startwidth + delta,
|
|
PROPS_MIN_WIDTH,
|
|
Math.min(PROPS_MAX_WIDTH, room)
|
|
);
|
|
}
|
|
applyPanelWidths();
|
|
resizeCanvas();
|
|
}
|
|
function releasedPanelBorder() {
|
|
document.onselectstart = previousSelectStart || null;
|
|
handle.removeClass('handle-dragging');
|
|
savePanelLayout();
|
|
}
|
|
bindPointerDrag(e, this, draggingPanelBorder, releasedPanelBorder);
|
|
}
|
|
$(document).on('pointerdown', '.panel-handle', dragPanel);
|
|
|
|
// Keep a click on a hide/show button from starting a resize drag
|
|
$(document).on('pointerdown', '.panel-toggle', function (e) {
|
|
e.stopPropagation();
|
|
});
|
|
|
|
$(document).on('click', '#browser-toggle', function (e) {
|
|
e.stopPropagation();
|
|
if (browserHidden()) {
|
|
// Reopening goes through the tool click so the rail icons stay in sync
|
|
$('#' + lasttool).trigger('click');
|
|
} else {
|
|
collapsePanel();
|
|
}
|
|
});
|
|
|
|
$(document).on('click', '#properties-toggle', function (e) {
|
|
e.stopPropagation();
|
|
propshidden = !propshidden;
|
|
applyPanelWidths();
|
|
savePanelLayout();
|
|
resizeCanvas();
|
|
});
|
|
|
|
// Re-fits the panels after the window itself changed size. Runs after the
|
|
// resizeCanvas listener registered at the top of this file, so the canvas is
|
|
// measured again once the new widths are in.
|
|
window.addEventListener(
|
|
'resize',
|
|
function () {
|
|
applyPanelWidths();
|
|
resizeCanvas();
|
|
},
|
|
false
|
|
);
|
|
|
|
restorePanelLayout();
|
|
resizeCanvas();
|
|
|
|
// Sync scrolling (vertical)
|
|
function syncScroll(el1, el2) {
|
|
var $el1 = $(el1);
|
|
var $el2 = $(el2);
|
|
var forcedScroll = false;
|
|
$el1.scroll(function () {
|
|
performScroll($el1, $el2);
|
|
});
|
|
$el2.scroll(function () {
|
|
performScroll($el2, $el1);
|
|
});
|
|
|
|
function performScroll($scrolled, $toScroll) {
|
|
if (forcedScroll) return (forcedScroll = false);
|
|
var percent =
|
|
($scrolled.scrollTop() /
|
|
($scrolled[0].scrollHeight - $scrolled.outerHeight())) *
|
|
100;
|
|
setScrollTopFromPercent($toScroll, percent);
|
|
}
|
|
|
|
function setScrollTopFromPercent($el, percent) {
|
|
var scrollTopPos =
|
|
(percent / 100) * ($el[0].scrollHeight - $el.outerHeight());
|
|
forcedScroll = true;
|
|
$el.scrollTop(scrollTopPos);
|
|
}
|
|
}
|
|
|
|
// Sync scrolling (horizontal)
|
|
function syncScrollHoz(el1, el2) {
|
|
var $el1 = $(el1);
|
|
var $el2 = $(el2);
|
|
var forcedScroll = false;
|
|
$el1.scroll(function () {
|
|
performScroll($el1, $el2);
|
|
});
|
|
$el2.scroll(function () {
|
|
performScroll($el2, $el1);
|
|
});
|
|
|
|
function performScroll($scrolled, $toScroll) {
|
|
if (forcedScroll) return (forcedScroll = false);
|
|
var percent =
|
|
($scrolled.scrollLeft() / $scrolled.outerWidth()) * 100;
|
|
setScrollLeftFromPercent($toScroll, percent);
|
|
}
|
|
|
|
function setScrollLeftFromPercent($el, percent) {
|
|
var scrollLeftPos = (percent / 100) * $el.outerWidth();
|
|
forcedScroll = true;
|
|
$el.scrollLeft(scrollLeftPos);
|
|
}
|
|
}
|
|
|
|
// Show keyframe properties
|
|
function keyframeProperties(inst) {
|
|
if (!shiftdown) {
|
|
selectedkeyframe = $(inst);
|
|
const popup = $('#keyframe-properties');
|
|
var keyarr = keyframes.filter(function (e) {
|
|
return (
|
|
e.t == selectedkeyframe.attr('data-time') &&
|
|
e.id == selectedkeyframe.attr('data-object') &&
|
|
e.name == selectedkeyframe.attr('data-property')
|
|
);
|
|
});
|
|
if (keyarr.length == 0) {
|
|
return;
|
|
}
|
|
$('#easing select').val(keyarr[0].easing);
|
|
$('#easing select').niceSelect('update');
|
|
popup.css({
|
|
left: $(inst).offset().left - popup.width() / 2,
|
|
top: $(inst).offset().top - popup.height() - 20,
|
|
});
|
|
popup.addClass('show-properties');
|
|
$(inst).addClass('keyframe-selected');
|
|
}
|
|
}
|
|
|
|
// Apply easing to keyframe
|
|
function applyEasing() {
|
|
if (!selectedkeyframe) {
|
|
return;
|
|
}
|
|
// Read the clicked item, not the underlying <select>: nice-select has not
|
|
// necessarily written the value back yet on mouseup.
|
|
const easing = $(this).attr('data-value');
|
|
const time = selectedkeyframe.attr('data-time');
|
|
const id = selectedkeyframe.attr('data-object');
|
|
const prop = selectedkeyframe.attr('data-property');
|
|
const names = [prop].concat(KEYFRAME_COUNTERPARTS[prop] || []);
|
|
names.forEach(function (name) {
|
|
keyframes
|
|
.filter(function (e) {
|
|
return e.t == time && e.id == id && e.name == name;
|
|
})
|
|
.forEach(function (e) {
|
|
e.easing = easing;
|
|
});
|
|
});
|
|
$('#keyframe-properties').removeClass('show-properties');
|
|
selectedkeyframe.removeClass('keyframe-selected');
|
|
animate(false, currenttime);
|
|
save();
|
|
}
|
|
$(document).on('mouseup', '#easing li', applyEasing);
|
|
|
|
// Click on seek area to seek (still not working properly)
|
|
function seekTo(e) {
|
|
if ($(e.target).hasClass('keyframe')) {
|
|
return false;
|
|
}
|
|
paused = true;
|
|
if ($('#seekarea').scrollLeft() > offset_left) {
|
|
currenttime = parseFloat(
|
|
(
|
|
(e.pageX +
|
|
$('#seekarea').scrollLeft() -
|
|
$('#timearea').offset().left -
|
|
offset_left) *
|
|
timelinetime
|
|
).toFixed(1)
|
|
);
|
|
} else {
|
|
currenttime = parseFloat(
|
|
(
|
|
(e.pageX +
|
|
$('#seekarea').scrollLeft() -
|
|
$('#timearea').offset().left -
|
|
offset_left) *
|
|
timelinetime
|
|
).toFixed(1)
|
|
);
|
|
}
|
|
if (currenttime < 0) {
|
|
currenttime = 0;
|
|
}
|
|
// Check for 60FPS playback, 16ms "slots"
|
|
if (currenttime % 16.666 != 0) {
|
|
currenttime = Math.ceil(currenttime / 16.666) * 16.666;
|
|
}
|
|
renderTime();
|
|
$('#seekbar').offset({
|
|
left:
|
|
offset_left +
|
|
$('#inner-timeline').offset().left +
|
|
currenttime / timelinetime,
|
|
});
|
|
animate(false, currenttime);
|
|
updatePanelValues();
|
|
}
|
|
$(document).on('click', '#seekevents', seekTo);
|
|
$(document).on('click', '#timearea', seekTo);
|
|
|
|
function hideSeekbar() {
|
|
$('#seek-hover').css({ opacity: 0 });
|
|
}
|
|
function followCursor(e) {
|
|
$('#seek-hover').css({ opacity: 0.3 });
|
|
if ($('#seekarea').scrollLeft() > offset_left) {
|
|
hovertime = parseFloat(
|
|
(
|
|
(e.pageX +
|
|
$('#seekarea').scrollLeft() -
|
|
$('#timearea').offset().left -
|
|
offset_left) *
|
|
timelinetime
|
|
).toFixed(1)
|
|
);
|
|
} else {
|
|
hovertime = parseFloat(
|
|
(
|
|
(e.pageX +
|
|
$('#seekarea').scrollLeft() -
|
|
$('#timearea').offset().left -
|
|
offset_left) *
|
|
timelinetime
|
|
).toFixed(1)
|
|
);
|
|
}
|
|
if (e.pageX >= offset_left + $('#inner-timeline').offset().left) {
|
|
$('#seek-hover').offset({ left: e.pageX });
|
|
}
|
|
}
|
|
$(document).on('mousemove', '#timearea', followCursor);
|
|
$(document).on('mousemove', '#seekevents', followCursor);
|
|
$(document).on('mousemove', '#toolbar', hideSeekbar);
|
|
$(document).on('mousemove', '#canvas-area', hideSeekbar);
|
|
$(document).on('mousemove', '#browser', hideSeekbar);
|
|
$(document).on('mousemove', '#properties', hideSeekbar);
|
|
$(document).on('mousemove', '#controls', hideSeekbar);
|
|
|
|
function orderLayers() {
|
|
$('.layer').each(function (index) {
|
|
const object = canvas.getItemById($(this).attr('data-object'));
|
|
canvas.sendToBack(object);
|
|
canvas.renderAll();
|
|
objects.splice(
|
|
$('.layer').length - index - 1,
|
|
0,
|
|
objects.splice(
|
|
objects.findIndex((x) => x.id == object.get('id')),
|
|
1
|
|
)[0]
|
|
);
|
|
});
|
|
save();
|
|
}
|
|
|
|
function handTool() {
|
|
if ($(this).hasClass('hand-active')) {
|
|
$(this).removeClass('hand-active');
|
|
$(this).find('img').attr('src', 'assets/hand-tool.svg');
|
|
handtool = false;
|
|
canvas.defaultCursor = 'default';
|
|
canvas.renderAll();
|
|
} else {
|
|
$(this).addClass('hand-active');
|
|
$(this).find('img').attr('src', 'assets/hand-tool-active.svg');
|
|
handtool = true;
|
|
canvas.defaultCursor = 'grab';
|
|
canvas.renderAll();
|
|
}
|
|
}
|
|
$(document).on('click', '#hand-tool', handTool);
|
|
// Set defaults
|
|
setDuration(10000);
|
|
checkDB();
|