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