Files
Motionity/src/js/functions.js
T
kawaandClaude Haiku 4.5 45c37a5e73 feat: resizable side panels, audio layer fixes
- 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>
2026-08-14 09:19:36 +02:00

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();