Replays the 13 open bug reports on alyssaxuu/motionity against this fork. Nine were already fixed by the earlier audit and one (#8, video from the search tab) needs a Pixabay API key to reproduce; these are the three that were still broken. #23 - border radius behaved like a percentage. fabric applies rx/ry before the object's scale and shapes are resized by scaling, so a radius typed as 20 drew at 60px on a rect scaled 3x while the panel still read 20. A new cornerRadius property keeps the pixel value the user asked for, rx/ry are derived from it and re-derived whenever the scale changes, and old projects fall back to rx * scaleX. #27 - cropping a rotated image cropped the wrong region. crop() compared canvas-space edges and covered only three of the four quadrants, so it did nothing at all when the crop window was centred on the image. It now works in the image's own frame, clamps the region to the bitmap and re-centres the object on what it kept; the crop window carries the image's angle and the expand-to-full-bitmap shift in cropImage is rotated the same way. #1 - modals are painted over the timeline but never took its pointer events, so the resize handle, seekbar, keyframes and layer bars all reacted to a drag behind the dialog. All four now bail while a modal is open. Verified in Chromium: 20px draws a 20px corner at scale 1 and scale 3 and survives save, JSON round-trip and reload; at 0, 30 and 45 degrees the pixels under the crop window are unchanged by the crop and everything outside it is dropped; a real drag on the timeline handle with the export modal open leaves the timeline height untouched. WEBM export and a non-rotated crop still work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1048 lines
29 KiB
JavaScript
1048 lines
29 KiB
JavaScript
// Remember the last valid crop rectangle so it can be restored if the user
|
|
// drags it outside the image.
|
|
function updateCropBounds() {
|
|
const cropUI = canvas.getItemById('crop');
|
|
if (!cropUI || !cropobj) {
|
|
return;
|
|
}
|
|
if (cropUI.isContainedWithinObject(cropobj)) {
|
|
cropleft = cropUI.get('left');
|
|
croptop = cropUI.get('top');
|
|
cropscalex = cropUI.get('scaleX');
|
|
cropscaley = cropUI.get('scaleY');
|
|
}
|
|
}
|
|
|
|
// Drag-to-reorder for the layer list. html5sortable only wires up the
|
|
// children present when it runs, so this is re-run every time a layer is
|
|
// added. The sortstop handler is bound only once.
|
|
let layerSortBound = false;
|
|
function initLayerSortable() {
|
|
const list = document.getElementById('layer-inner-list');
|
|
if (!list) {
|
|
return;
|
|
}
|
|
const sortableList = sortable(list, {
|
|
handle: '.layer-handle',
|
|
customDragImage: (draggedElement, elementOffset, event) => {
|
|
return {
|
|
element: document.getElementById('nothing'),
|
|
posX: event.pageX - elementOffset.left,
|
|
posY: event.pageY - elementOffset.top,
|
|
};
|
|
},
|
|
})[0];
|
|
|
|
// Re-initializing marks every handle draggable again, so locked layers
|
|
// have to be opted back out.
|
|
$('#layer-inner-list .layer').each(function () {
|
|
if ($(this).find('.lock').hasClass('locked')) {
|
|
$(this).find('.layer-handle').attr('draggable', false);
|
|
}
|
|
});
|
|
|
|
if (layerSortBound) {
|
|
return;
|
|
}
|
|
layerSortBound = true;
|
|
sortableList.addEventListener('sortupdate', function () {
|
|
syncTimelineOrder();
|
|
orderLayers();
|
|
});
|
|
}
|
|
|
|
// Re-order the timeline rows to match the layer list
|
|
function syncTimelineOrder() {
|
|
const timeline = document.getElementById('inner-timeline');
|
|
if (!timeline) {
|
|
return;
|
|
}
|
|
$('#layer-inner-list .layer').each(function () {
|
|
const row = document.getElementById(
|
|
$(this).attr('data-object')
|
|
);
|
|
if (row) {
|
|
timeline.appendChild(row);
|
|
}
|
|
});
|
|
}
|
|
|
|
$(document).ready(function () {
|
|
// An object is being moved in the canvas
|
|
canvas.on('object:moving', function (e) {
|
|
e.target.hasControls = false;
|
|
centerLines(e);
|
|
if (cropping) {
|
|
updateCropBounds();
|
|
crop(canvas.getItemById('cropped'));
|
|
} else if (
|
|
lockmovement &&
|
|
e.e.shiftKey &&
|
|
canvas.getActiveObject()
|
|
) {
|
|
if (canvasx < shiftx + 30 && canvasx > shiftx - 30) {
|
|
canvas.getActiveObject().set({ left: shiftx });
|
|
canvas.getActiveObject().lockMovementX = true;
|
|
canvas.getActiveObject().lockMovementY = false;
|
|
} else {
|
|
canvas.getActiveObject().set({ top: shifty });
|
|
canvas.getActiveObject().lockMovementX = false;
|
|
canvas.getActiveObject().lockMovementY = true;
|
|
}
|
|
} else if (canvas.getActiveObject() && !e.e.shiftKey) {
|
|
lockmovement = false;
|
|
canvas.getActiveObject().lockMovementX = false;
|
|
canvas.getActiveObject().lockMovementY = false;
|
|
}
|
|
});
|
|
|
|
// An object is being scaled in the canvas
|
|
canvas.on('object:scaling', function (e) {
|
|
e.target.hasControls = false;
|
|
centerLines(e);
|
|
// Keep the corner radius at its pixel value while the handle is dragged
|
|
syncCornerRadius(e.target);
|
|
if (cropping) {
|
|
updateCropBounds();
|
|
crop(canvas.getItemById('cropped'));
|
|
}
|
|
});
|
|
|
|
// An object is being resized in the canvas
|
|
canvas.on('object:resizing', function (e) {
|
|
e.target.hasControls = false;
|
|
centerLines(e);
|
|
if (cropping) {
|
|
updateCropBounds();
|
|
crop(canvas.getItemById('cropped'));
|
|
}
|
|
});
|
|
|
|
// An object is being rotated in the canvas
|
|
canvas.on('object:rotating', function (e) {
|
|
if (canvas.getActiveObject()) {
|
|
canvas.getActiveObject().snapAngle = e.e.shiftKey ? 15 : 0;
|
|
}
|
|
e.target.hasControls = false;
|
|
});
|
|
|
|
// An object has been modified in the canvas
|
|
canvas.on('object:modified', function (e) {
|
|
e.target.hasControls = true;
|
|
if (!editinggroup && !cropping) {
|
|
if (canvas.getActiveObject()) {
|
|
canvas.getActiveObject().lockMovementX = false;
|
|
canvas.getActiveObject().lockMovementY = false;
|
|
}
|
|
canvas.renderAll();
|
|
if (e.target.type == 'activeSelection') {
|
|
const tempselection = canvas.getActiveObject();
|
|
// Discarding first bakes the group transform into the children, so
|
|
// their scale is final by the time the radius is re-derived
|
|
canvas.discardActiveObject();
|
|
e.target._objects.forEach(function (object) {
|
|
syncCornerRadius(object);
|
|
autoKeyframe(object, e, true);
|
|
});
|
|
reselect(tempselection);
|
|
} else {
|
|
syncCornerRadius(e.target);
|
|
autoKeyframe(e.target, e, false);
|
|
}
|
|
updatePanelValues();
|
|
save();
|
|
}
|
|
if (cropping) {
|
|
var obj = e.target;
|
|
checkCrop(obj);
|
|
}
|
|
});
|
|
|
|
// A selection has been updated in the canvas
|
|
canvas.on('selection:updated', function (e) {
|
|
updatePanel(true);
|
|
updatePanelValues();
|
|
updateSelection(e);
|
|
closeFilters();
|
|
});
|
|
|
|
// A selection has been made in the canvas
|
|
canvas.on('selection:created', function (e) {
|
|
shiftx = canvas.getActiveObject().get('left');
|
|
shifty = canvas.getActiveObject().get('top');
|
|
if (!editingpanel) {
|
|
updatePanel(true);
|
|
}
|
|
updateSelection(e);
|
|
canvas.renderAll();
|
|
closeFilters();
|
|
});
|
|
|
|
// A selection has been cleared in the canvas
|
|
canvas.on('selection:cleared', function (e) {
|
|
if (!editingpanel && !setting) {
|
|
updatePanel(false);
|
|
}
|
|
$('.layer-selected').removeClass('layer-selected');
|
|
if (cropping) {
|
|
crop(cropobj);
|
|
}
|
|
closeFilters();
|
|
});
|
|
|
|
function kFormatter(num) {
|
|
return Math.abs(num) > 999
|
|
? Math.sign(num) * (Math.abs(num) / 1000).toFixed(1) + 'k'
|
|
: Math.sign(num) * Math.abs(num);
|
|
}
|
|
|
|
// Zoom in/out of the canvas
|
|
canvas.on('mouse:wheel', function (opt) {
|
|
var delta = opt.e.deltaY;
|
|
var zoom = canvas.getZoom();
|
|
zoom *= 0.999 ** delta;
|
|
$('#zoom-level span').html(
|
|
kFormatter((zoom * 100).toFixed(0)) + '%'
|
|
);
|
|
if (zoom > 20) zoom = 20;
|
|
if (zoom < 0.01) zoom = 0.01;
|
|
canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom);
|
|
opt.e.preventDefault();
|
|
opt.e.stopPropagation();
|
|
});
|
|
|
|
// Start panning if space is down or hand tool is enabled
|
|
canvas.on('mouse:down', function (opt) {
|
|
var e = opt.e;
|
|
if (spaceDown || handtool) {
|
|
this.isDragging = true;
|
|
this.selection = false;
|
|
this.lastPosX = e.clientX;
|
|
this.lastPosY = e.clientY;
|
|
}
|
|
if (opt.target) {
|
|
opt.target.hasControls = true;
|
|
wip = false;
|
|
}
|
|
});
|
|
|
|
// Pan while dragging mouse
|
|
canvas.on('mouse:move', function (opt) {
|
|
var pointer = canvas.getPointer(opt.e);
|
|
canvasx = pointer.x;
|
|
canvasy = pointer.y;
|
|
if (this.isDragging) {
|
|
var e = opt.e;
|
|
var vpt = this.viewportTransform;
|
|
vpt[4] += e.clientX - this.lastPosX;
|
|
vpt[5] += e.clientY - this.lastPosY;
|
|
this.requestRenderAll();
|
|
this.lastPosX = e.clientX;
|
|
this.lastPosY = e.clientY;
|
|
}
|
|
});
|
|
|
|
// Stop panning
|
|
canvas.on('mouse:up', function (opt) {
|
|
this.setViewportTransform(this.viewportTransform);
|
|
this.isDragging = false;
|
|
this.selection = true;
|
|
if (line_h) {
|
|
line_h.opacity = 0;
|
|
}
|
|
if (line_v) {
|
|
line_v.opacity = 0;
|
|
}
|
|
});
|
|
|
|
// Detect mouse over canvas (for dragging objects from the library)
|
|
canvas.on('mouse:move', function (e) {
|
|
overCanvas = true;
|
|
if (
|
|
e.target &&
|
|
!canvas.getActiveObject() &&
|
|
draggingPanel &&
|
|
e.target.type == 'image'
|
|
) {
|
|
wip = true;
|
|
e.target.hasControls = false;
|
|
canvas.setActiveObject(e.target);
|
|
}
|
|
});
|
|
canvas.on('mouse:out', function (e) {
|
|
overCanvas = false;
|
|
if (wip) {
|
|
if (e.target) {
|
|
e.target.hasControls = true;
|
|
}
|
|
canvas.discardActiveObject();
|
|
wip = false;
|
|
canvas.renderAll();
|
|
}
|
|
});
|
|
|
|
// Double click on image to get into cropping mode
|
|
fabric.util.addListener(
|
|
canvas.upperCanvasEl,
|
|
'dblclick',
|
|
function (e) {
|
|
var target = canvas.findTarget(e);
|
|
if (target) {
|
|
if (target.type == 'image') {
|
|
cropImage(target);
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
// Key event handling
|
|
$(document)
|
|
.keyup(function (e) {
|
|
// Space bar (panning and playback)
|
|
if (
|
|
e.keyCode == 32 &&
|
|
!editinglayer &&
|
|
!editingproject &&
|
|
$(e.target)[0].tagName != 'INPUT'
|
|
) {
|
|
spacerelease = true;
|
|
spaceDown = false;
|
|
canvas.defaultCursor = 'default';
|
|
canvas.renderAll();
|
|
if (!spacehold) {
|
|
if (
|
|
!(
|
|
canvas.getActiveObject() &&
|
|
canvas.getActiveObject().isEditing
|
|
)
|
|
) {
|
|
if (paused) {
|
|
play();
|
|
} else {
|
|
pause();
|
|
}
|
|
}
|
|
} else {
|
|
if (!handtool) {
|
|
$('#hand-tool').removeClass('hand-active');
|
|
$('#hand-tool')
|
|
.find('img')
|
|
.attr('src', 'assets/hand-tool.svg');
|
|
}
|
|
}
|
|
spacehold = false;
|
|
}
|
|
// Delete object/keyframe
|
|
if (
|
|
(e.keyCode == 46 ||
|
|
e.key == 'Delete' ||
|
|
e.code == 'Delete' ||
|
|
e.key == 'Backspace') &&
|
|
!focus &&
|
|
!editinglayer
|
|
) {
|
|
if (
|
|
$('.show-properties').length > 0 ||
|
|
shiftkeys.length > 0
|
|
) {
|
|
deleteKeyframe();
|
|
} else {
|
|
deleteSelection();
|
|
}
|
|
}
|
|
// Shift key is up (stop locking horizontal/vertical object movement)
|
|
if (e.keyCode == 16) {
|
|
lockmovement = false;
|
|
shiftdown = false;
|
|
}
|
|
})
|
|
.keydown(function (e) {
|
|
// Space bar (panning and playback)
|
|
if (
|
|
e.keyCode == 32 &&
|
|
!editinglayer &&
|
|
!editingproject &&
|
|
$(e.target)[0].tagName != 'INPUT'
|
|
) {
|
|
spacerelease = false;
|
|
spaceDown = true;
|
|
canvas.defaultCursor = 'grab';
|
|
canvas.renderAll();
|
|
window.setTimeout(function () {
|
|
if (!spacerelease) {
|
|
spacehold = true;
|
|
if (!handtool) {
|
|
$('#hand-tool').addClass('hand-active');
|
|
$('#hand-tool')
|
|
.find('img')
|
|
.attr('src', 'assets/hand-tool-active.svg');
|
|
}
|
|
}
|
|
}, 1000);
|
|
}
|
|
// Redo / undo (shift decides which; never both in one keypress)
|
|
if (e.which === 90 && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault();
|
|
if (e.shiftKey) {
|
|
if (redo.length >= 1) {
|
|
undoRedo(redo, undo, redoarr, undoarr);
|
|
}
|
|
} else if (undo.length >= 1) {
|
|
undoRedo(undo, redo, undoarr, redoarr);
|
|
}
|
|
}
|
|
// Duplicate object
|
|
if (e.which === 68 && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault();
|
|
if (canvas.getActiveObject()) {
|
|
clipboard = canvas.getActiveObject();
|
|
copyObject();
|
|
}
|
|
}
|
|
// Shift key (Lock horizontal/vertical movement for objects)
|
|
if (e.shiftKey) {
|
|
shiftdown = true;
|
|
lockmovement = true;
|
|
if (canvas.getActiveObject()) {
|
|
shiftx = canvas.getActiveObject().get('left');
|
|
shifty = canvas.getActiveObject().get('top');
|
|
}
|
|
}
|
|
// Return (save layer name)
|
|
if (e.keyCode === 13 && editinglayer) {
|
|
saveLayerName();
|
|
}
|
|
// Return (save project name)
|
|
if (e.keyCode === 13 && editingproject) {
|
|
saveProjectName();
|
|
}
|
|
// Arrow keys nudge the selection
|
|
if (
|
|
e.keyCode >= 37 &&
|
|
e.keyCode <= 40 &&
|
|
canvas.getActiveObject() &&
|
|
!canvas.getActiveObject().isEditing &&
|
|
!focus &&
|
|
!editinglayer &&
|
|
!editingproject
|
|
) {
|
|
const obj = canvas.getActiveObject();
|
|
// Bigger step if shift is down
|
|
const step = e.shiftKey ? 7 : 2;
|
|
if (e.keyCode === 37) {
|
|
obj.left = obj.left - step;
|
|
} else if (e.keyCode === 38) {
|
|
obj.top = obj.top - step;
|
|
} else if (e.keyCode === 39) {
|
|
obj.left = obj.left + step;
|
|
} else {
|
|
obj.top = obj.top + step;
|
|
}
|
|
// Without this the selection box stays where the object used to be
|
|
obj.setCoords();
|
|
canvas.renderAll();
|
|
autoKeyframe(obj, { action: 'drag' }, false);
|
|
}
|
|
|
|
// Move object up layer list
|
|
if (
|
|
e.keyCode === 221 &&
|
|
canvas.getActiveObjects() &&
|
|
(e.metaKey || e.ctrlKey)
|
|
) {
|
|
if (canvas.getActiveObjects().length == 1) {
|
|
var obj = canvas.getActiveObject();
|
|
$(".layer[data-object='" + obj.id + "']")
|
|
.prev()
|
|
.insertAfter($(".layer[data-object='" + obj.id + "']"));
|
|
$('#' + obj.id)
|
|
.prev()
|
|
.insertAfter($('#' + obj.id));
|
|
orderLayers();
|
|
} else {
|
|
canvas.getActiveObjects().forEach(function (obj) {
|
|
$(".layer[data-object='" + obj.id + "']")
|
|
.prev()
|
|
.insertAfter($(".layer[data-object='" + obj.id + "']"));
|
|
$('#' + obj.id)
|
|
.prev()
|
|
.insertAfter($('#' + obj.id));
|
|
orderLayers();
|
|
});
|
|
}
|
|
}
|
|
|
|
// Move object down layer list
|
|
if (
|
|
e.keyCode === 219 &&
|
|
canvas.getActiveObjects() &&
|
|
(e.metaKey || e.ctrlKey)
|
|
) {
|
|
if (canvas.getActiveObjects().length == 1) {
|
|
var obj = canvas.getActiveObject();
|
|
$(".layer[data-object='" + obj.id + "']")
|
|
.next()
|
|
.insertBefore($(".layer[data-object='" + obj.id + "']"));
|
|
$('#' + obj.id)
|
|
.next()
|
|
.insertBefore($('#' + obj.id));
|
|
orderLayers();
|
|
} else {
|
|
canvas.getActiveObjects().forEach(function (obj) {
|
|
$(".layer[data-object='" + obj.id + "']")
|
|
.next()
|
|
.insertBefore(
|
|
$(".layer[data-object='" + obj.id + "']")
|
|
);
|
|
$('#' + obj.id)
|
|
.next()
|
|
.insertBefore($('#' + obj.id));
|
|
orderLayers();
|
|
});
|
|
}
|
|
}
|
|
|
|
// Move object top of layer list
|
|
if (
|
|
e.keyCode === 221 &&
|
|
canvas.getActiveObjects() &&
|
|
e.altKey
|
|
) {
|
|
if (canvas.getActiveObjects().length == 1) {
|
|
var obj = canvas.getActiveObject();
|
|
$('#layer-inner-list').prepend(
|
|
$(".layer[data-object='" + obj.id + "']")
|
|
);
|
|
$('#inner-timeline').prepend($('#' + obj.id));
|
|
orderLayers();
|
|
} else {
|
|
canvas.getActiveObjects().forEach(function (obj) {
|
|
$('#layer-inner-list').prepend(
|
|
$(".layer[data-object='" + obj.id + "']")
|
|
);
|
|
$('#inner-timeline').prepend($('#' + obj.id));
|
|
orderLayers();
|
|
});
|
|
}
|
|
}
|
|
|
|
// Move object bottom of layer list
|
|
if (e.keyCode === 219 && canvas.getActiveObject() && e.altKey) {
|
|
if (canvas.getActiveObjects().length == 1) {
|
|
var obj = canvas.getActiveObject();
|
|
$('#layer-inner-list').append(
|
|
$(".layer[data-object='" + obj.id + "']")
|
|
);
|
|
$('#inner-timeline').append($('#' + obj.id));
|
|
orderLayers();
|
|
} else {
|
|
canvas.getActiveObjects().forEach(function (obj) {
|
|
$('#layer-inner-list').append(
|
|
$(".layer[data-object='" + obj.id + "']")
|
|
);
|
|
$('#inner-timeline').append($('#' + obj.id));
|
|
orderLayers();
|
|
});
|
|
}
|
|
}
|
|
|
|
// Zoom in
|
|
if (e.keyCode === 187 && e.shiftKey) {
|
|
var zoom = canvas.getZoom() + 0.2;
|
|
if (zoom > 20) zoom = 20;
|
|
if (zoom < 0.01) zoom = 0.01;
|
|
canvas.setZoom(1);
|
|
canvas.renderAll();
|
|
var vpw = canvas.width / zoom;
|
|
var vph = canvas.height / zoom;
|
|
var x = artboard.left + artboard.width / 2 - vpw / 2;
|
|
var y = artboard.top + artboard.height / 2 - vph / 2;
|
|
canvas.absolutePan({ x: x, y: y });
|
|
canvas.setZoom(zoom);
|
|
canvas.renderAll();
|
|
$('#zoom-level span').html(
|
|
(canvas.getZoom() * 100).toFixed(0) + '%'
|
|
);
|
|
}
|
|
|
|
// Zoom out
|
|
if (e.keyCode === 189 && e.shiftKey) {
|
|
var zoom = canvas.getZoom() - 0.2;
|
|
if (zoom > 20) zoom = 20;
|
|
if (zoom < 0.01) zoom = 0.01;
|
|
canvas.setZoom(1);
|
|
canvas.renderAll();
|
|
var vpw = canvas.width / zoom;
|
|
var vph = canvas.height / zoom;
|
|
var x = artboard.left + artboard.width / 2 - vpw / 2;
|
|
var y = artboard.top + artboard.height / 2 - vph / 2;
|
|
canvas.absolutePan({ x: x, y: y });
|
|
canvas.setZoom(zoom);
|
|
canvas.renderAll();
|
|
$('#zoom-level span').html(
|
|
(canvas.getZoom() * 100).toFixed(0) + '%'
|
|
);
|
|
}
|
|
});
|
|
|
|
// Copy event
|
|
window.addEventListener('copy', function (e) {
|
|
// Selecting keyframes clears the canvas selection, so there is often no
|
|
// active object at all here - never dereference it unguarded.
|
|
const activeObject = canvas.getActiveObject();
|
|
if (activeObject && activeObject.isEditing) {
|
|
return;
|
|
}
|
|
// Copy selected object
|
|
if (activeObject && shiftkeys.length == 0) {
|
|
var emptyInp = document.getElementById('emptyInput');
|
|
emptyInp.select();
|
|
emptyInp.focus();
|
|
setTimeout(function () {
|
|
document.execCommand('copy');
|
|
}, 0);
|
|
clipboard = activeObject;
|
|
cliptype = 'object';
|
|
// Copy selected keyframe(s)
|
|
} else if (shiftkeys.length > 0) {
|
|
var emptyInp = document.getElementById('emptyInput');
|
|
emptyInp.select();
|
|
emptyInp.focus();
|
|
setTimeout(function () {
|
|
document.execCommand('copy');
|
|
}, 0);
|
|
clipboard = [];
|
|
shiftkeys.forEach(function (keyframe) {
|
|
var drag = $(keyframe.keyframe);
|
|
var keyarr = $.grep(keyframes, function (e) {
|
|
return (
|
|
e.t == drag.attr('data-time') &&
|
|
e.id == drag.attr('data-object') &&
|
|
e.name == drag.attr('data-property')
|
|
);
|
|
});
|
|
if (keyarr.length > 0) {
|
|
clipboard.push(keyarr[0]);
|
|
}
|
|
});
|
|
cliptype = 'keyframe';
|
|
}
|
|
});
|
|
|
|
// Paste event
|
|
window.addEventListener('paste', function (e) {
|
|
var imgs = e.clipboardData.items;
|
|
if (imgs == undefined) return false;
|
|
|
|
// Paste object or keyframe(s)
|
|
if (imgs.length == 1 && e.clipboardData.getData('text') == ' ') {
|
|
copyObject();
|
|
// Paste external image (by uploading it)
|
|
} else {
|
|
for (var i = 0; i < imgs.length; i++) {
|
|
if (imgs[i].type.indexOf('image') == -1) continue;
|
|
// `let` so each async thumbnail callback keeps its own file
|
|
let imgObj = imgs[i].getAsFile();
|
|
if (!imgObj) continue;
|
|
if (imgObj.size / 1024 / 1024 <= 10) {
|
|
createThumbnail(imgObj, 250).then(function (data) {
|
|
saveFile(
|
|
dataURItoBlob(data),
|
|
imgObj,
|
|
imgObj['type'].split('/')[0],
|
|
'temp',
|
|
true,
|
|
true
|
|
);
|
|
});
|
|
} else {
|
|
alert('Image is too big');
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Stop cropping when clicking on the blacked out properties panel
|
|
$(document).on('click', '#properties-overlay', function () {
|
|
if (cropping) {
|
|
canvas.discardActiveObject();
|
|
}
|
|
});
|
|
|
|
// Scroll horizontally through assets in the library
|
|
$(document).on('click', '.right-arrow', function () {
|
|
$(this).parent().animate({ scrollLeft: '+=1000' }, 500);
|
|
});
|
|
$(document).on('click', '.left-arrow', function () {
|
|
$(this).parent().animate({ scrollLeft: '-=1000' }, 500);
|
|
});
|
|
|
|
// Playback
|
|
$(document).on('click', '#play-button', function () {
|
|
if (paused) {
|
|
play();
|
|
} else {
|
|
pause();
|
|
}
|
|
});
|
|
|
|
// Detect when not clicking on certain elements
|
|
$(document).on('mousedown', function (e) {
|
|
// De-select keyframes
|
|
if (
|
|
!$('#keyframe-properties').is(e.target) &&
|
|
$('#keyframe-properties').has(e.target).length === 0 &&
|
|
!$('.keyframe').is(e.target) &&
|
|
$('.keyframe').has(e.target).length === 0
|
|
) {
|
|
$('#keyframe-properties').removeClass('show-properties');
|
|
$('.keyframe-selected').removeClass('keyframe-selected');
|
|
shiftkeys = [];
|
|
}
|
|
|
|
// Hide color picker
|
|
if (
|
|
!$('.object-color').is(e.target) &&
|
|
$('.object-color').has(e.target).length === 0 &&
|
|
!$('.pcr-app').is(e.target) &&
|
|
$('.prc-app').has(e.target).length === 0 &&
|
|
!$('.pcr-selection').is(e.target) &&
|
|
$('.pcr-selection').has(e.target).length === 0 &&
|
|
!$('.pcr-swatches').is(e.target) &&
|
|
$('.pcr-swatches').has(e.target).length === 0 &&
|
|
!$('.pcr-interaction').is(e.target) &&
|
|
$('.pcr-interaction').has(e.target).length === 0
|
|
) {
|
|
o_fill.hide();
|
|
}
|
|
|
|
// Hide zoom controls
|
|
if (
|
|
!$('#other-controls').is(e.target) &&
|
|
$('#other-controls').has(e.target).length === 0
|
|
) {
|
|
$('#zoom-options').addClass('zoom-hidden');
|
|
$('#zoom-level img').removeClass('zoom-open');
|
|
}
|
|
|
|
// Hide speed settings
|
|
if (
|
|
!$('#speed').is(e.target) &&
|
|
$('#speed').has(e.target).length === 0
|
|
) {
|
|
$('#speed-settings').removeClass('show-speed');
|
|
$('#speed-arrow').removeClass('arrow-on');
|
|
}
|
|
|
|
// Hide more menu
|
|
if (
|
|
!$('#more-tool').is(e.target) &&
|
|
$('#more-tool').has(e.target).length === 0 &&
|
|
!$('#more-over').is(e.target) &&
|
|
$('#more-over').has(e.target).length === 0
|
|
) {
|
|
hideMore();
|
|
}
|
|
});
|
|
|
|
// Detect focus on an input in the properties
|
|
$(document)
|
|
.on('focus', '.property-input', function () {
|
|
focus = true;
|
|
})
|
|
.on('focusout', function () {
|
|
focus = false;
|
|
});
|
|
|
|
// Toggle zoom dropdown
|
|
$(document).on('click', '#zoom-level', function () {
|
|
$('#zoom-options').toggleClass('zoom-hidden');
|
|
$('#zoom-level img').toggleClass('zoom-open');
|
|
});
|
|
|
|
// Skip to the beginning or end
|
|
$(document).on('click', '#skip-backward', function () {
|
|
animate(false, 0);
|
|
$('#seekbar').offset({
|
|
left:
|
|
offset_left +
|
|
$('#inner-timeline').offset().left +
|
|
currenttime / timelinetime,
|
|
});
|
|
});
|
|
$(document).on('click', '#skip-forward', function () {
|
|
animate(false, duration);
|
|
$('#seekbar').offset({
|
|
left:
|
|
offset_left +
|
|
$('#inner-timeline').offset().left +
|
|
currenttime / timelinetime,
|
|
});
|
|
});
|
|
|
|
// Change layer name
|
|
$(document).on('dblclick', '.layer-custom-name', function () {
|
|
$(this).prop('readonly', false);
|
|
$(this).addClass('name-active');
|
|
$(this).focus();
|
|
document.execCommand('selectAll', false, null);
|
|
editinglayer = true;
|
|
});
|
|
|
|
// Trigger file picker when clicking the upload button
|
|
$(document).on('click', '#upload-button', function () {
|
|
$('#upload-popup').addClass('upload-show');
|
|
});
|
|
|
|
$(document).on('click', '#upload-overlay', function () {
|
|
$('.upload-show').removeClass('upload-show');
|
|
});
|
|
|
|
$(document).on('click', '#upload-popup-close', function () {
|
|
$('.upload-show').removeClass('upload-show');
|
|
});
|
|
|
|
$(document).on('click', '#upload-drop-area', function () {
|
|
$('#filepick').click();
|
|
});
|
|
|
|
$(document).on('dragover', function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
});
|
|
|
|
$(document).on('dragover', '#upload-drop-area', function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
$('#upload-drop-area').addClass('dropping');
|
|
});
|
|
|
|
$(document).on('dragenter', '#upload-drop-area', function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
$('#upload-drop-area').addClass('dropping');
|
|
});
|
|
|
|
$(document).on('drop', function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
$('#upload-drop-area').removeClass('dropping');
|
|
handleUpload(e);
|
|
});
|
|
|
|
$(document).on('dragleave', function () {
|
|
$('#upload-drop-area').removeClass('dropping');
|
|
});
|
|
|
|
$(document).on('dragend', function () {
|
|
$('#upload-drop-area').removeClass('dropping');
|
|
});
|
|
|
|
// Upload or remove background audio
|
|
$(document).on('click', '#audio-upload-button', function () {
|
|
$('#filepick2').click();
|
|
});
|
|
|
|
// Sync scrolling for the timeline
|
|
syncScroll($('#layer-inner-list'), $('#timeline'));
|
|
syncScrollHoz($('#timeline'), $('#seekarea'));
|
|
|
|
// Initialize layer sorting
|
|
initLayerSortable();
|
|
|
|
// Initialize dropdown for keyframe easing
|
|
$('#easing select').niceSelect();
|
|
|
|
// Initialize properties panel
|
|
updatePanel(false);
|
|
|
|
// Initialize library
|
|
updateBrowser('shape-tool');
|
|
|
|
function initFilterSliders() {
|
|
var filters = [
|
|
'filter-brightness',
|
|
'filter-contrast',
|
|
'filter-saturation',
|
|
'filter-hue',
|
|
'filter-vibrance',
|
|
];
|
|
filters.forEach(function (filter) {
|
|
var selectme = document.getElementById(filter);
|
|
var slider = new RangeSlider(selectme, {
|
|
design: '2d',
|
|
theme: 'default',
|
|
handle: 'round',
|
|
popup: null,
|
|
showMinMaxLabels: false,
|
|
unit: '%',
|
|
min: -100,
|
|
max: 100,
|
|
value: 0,
|
|
step: 1,
|
|
onmove: function (x) {
|
|
if (canvas.getActiveObject()) {
|
|
var obj = canvas.getActiveObject();
|
|
if (filter == 'filter-brightness') {
|
|
if (obj.filters.find((i) => i.type == 'Brightness')) {
|
|
obj.filters.find(
|
|
(i) => i.type == 'Brightness'
|
|
).brightness = x / 100;
|
|
} else {
|
|
obj.filters.push(
|
|
new f.Brightness({ brightness: x / 100 })
|
|
);
|
|
}
|
|
} else if (filter == 'filter-contrast') {
|
|
if (obj.filters.find((i) => i.type == 'Contrast')) {
|
|
obj.filters.find(
|
|
(i) => i.type == 'Contrast'
|
|
).contrast = x / 100;
|
|
} else {
|
|
obj.filters.push(
|
|
new f.Contrast({ contrast: x / 100 })
|
|
);
|
|
}
|
|
} else if (filter == 'filter-saturation') {
|
|
if (obj.filters.find((i) => i.type == 'Saturation')) {
|
|
obj.filters.find(
|
|
(i) => i.type == 'Saturation'
|
|
).saturation = x / 100;
|
|
} else {
|
|
obj.filters.push(
|
|
new f.Saturation({ saturation: x / 100 })
|
|
);
|
|
}
|
|
} else if (filter == 'filter-vibrance') {
|
|
if (obj.filters.find((i) => i.type == 'Vibrance')) {
|
|
obj.filters.find(
|
|
(i) => i.type == 'Vibrance'
|
|
).vibrance = x / 100;
|
|
} else {
|
|
obj.filters.push(
|
|
new f.Vibrance({ vibrance: x / 100 })
|
|
);
|
|
}
|
|
} else if (filter == 'filter-hue') {
|
|
if (obj.filters.find((i) => i.type == 'HueRotation')) {
|
|
obj.filters.find(
|
|
(i) => i.type == 'HueRotation'
|
|
).rotation = x / 100;
|
|
} else {
|
|
obj.filters.push(
|
|
new f.HueRotation({ rotation: x / 100 })
|
|
);
|
|
}
|
|
}
|
|
obj.applyFilters();
|
|
canvas.renderAll();
|
|
}
|
|
},
|
|
onfinish: function (x) {
|
|
save();
|
|
},
|
|
});
|
|
sliders.push({ name: filter, slider: slider });
|
|
});
|
|
}
|
|
|
|
var selectchroma = document.getElementById('chroma-distance');
|
|
chromaslider = new RangeSlider(selectchroma, {
|
|
design: '2d',
|
|
theme: 'default',
|
|
handle: 'round',
|
|
popup: null,
|
|
showMinMaxLabels: false,
|
|
unit: '%',
|
|
min: 1,
|
|
max: 100,
|
|
value: 1,
|
|
step: 1,
|
|
onmove: function (x) {
|
|
if (canvas.getActiveObject()) {
|
|
var obj = canvas.getActiveObject();
|
|
if (obj.filters.find((i) => i.type == 'RemoveColor')) {
|
|
obj.filters.find((i) => i.type == 'RemoveColor').distance =
|
|
x / 100;
|
|
}
|
|
obj.applyFilters();
|
|
canvas.renderAll();
|
|
}
|
|
},
|
|
onfinish: function (x) {
|
|
save();
|
|
},
|
|
});
|
|
|
|
var selectnoise = document.getElementById('filter-noise');
|
|
noiseslider = new RangeSlider(selectnoise, {
|
|
design: '2d',
|
|
theme: 'default',
|
|
handle: 'round',
|
|
popup: null,
|
|
showMinMaxLabels: false,
|
|
unit: '%',
|
|
min: 0,
|
|
max: 1000,
|
|
value: 0,
|
|
step: 1,
|
|
onmove: function (x) {
|
|
if (canvas.getActiveObject()) {
|
|
var obj = canvas.getActiveObject();
|
|
if (obj.filters.find((i) => i.type == 'Noise')) {
|
|
obj.filters.find((i) => i.type == 'Noise').noise = x;
|
|
} else {
|
|
obj.filters.push(
|
|
new f.Noise({
|
|
noise: x,
|
|
})
|
|
);
|
|
}
|
|
obj.applyFilters();
|
|
canvas.renderAll();
|
|
}
|
|
},
|
|
onfinish: function (x) {
|
|
save();
|
|
},
|
|
});
|
|
|
|
var selectblur = document.getElementById('filter-blur');
|
|
blurslider = new RangeSlider(selectblur, {
|
|
design: '2d',
|
|
theme: 'default',
|
|
handle: 'round',
|
|
popup: null,
|
|
showMinMaxLabels: false,
|
|
unit: '%',
|
|
min: 0,
|
|
max: 100,
|
|
value: 0,
|
|
step: 1,
|
|
onmove: function (x) {
|
|
if (canvas.getActiveObject()) {
|
|
var obj = canvas.getActiveObject();
|
|
if (obj.filters.find((i) => i.type == 'Blur')) {
|
|
obj.filters.find((i) => i.type == 'Blur').blur = x / 100;
|
|
} else {
|
|
obj.filters.push(
|
|
new f.Blur({
|
|
blur: x / 100,
|
|
})
|
|
);
|
|
}
|
|
obj.applyFilters();
|
|
canvas.renderAll();
|
|
}
|
|
},
|
|
onfinish: function (x) {
|
|
save();
|
|
},
|
|
});
|
|
|
|
$('#filters-list').val('none');
|
|
$('#filters-list').niceSelect();
|
|
|
|
initFilterSliders();
|
|
});
|